from collections import namedtuple
import numpy as np
"""
Templates for amino acidic residues
"""

AA_info = namedtuple('AA_info', 'coords atom_names bonds bb sc offset')

A_info = AA_info(coords=np.array([[-0.75, -1.26, -0.51],
                                  [-0.04,  0.03, -0.48],
                                  [1.47, -0.14, -0.46],
                                  [2.04, -1.21, -0.48],
                                  [-0.5,  0.86,  0.73],
                                  [-0.07, -2.02, -0.5],
                                  [-0.27,  0.58, -1.4],
                                  [-1.59,  1.01,  0.69],
                                  [-0.26,  0.35,  1.66],
                                  [-0.02,  1.84,  0.74]]),
                 atom_names=['N', 'CA', 'C', 'O', 'CB',
                             'H', 'HA', 'HB1', 'HB2', 'HB3'],
                 bb=[0, 1, 2, 3, 5, 6],
                 sc=[4, 7, 8, 9],
                 bonds=[(0, 1), (0, 5), (1, 2), (1, 4), (1, 6),
                        (2, 3), (4, 7), (4, 8), (4, 9)],
                 offset=10)

R_info = AA_info(coords=np.array([[-0.07, -3.66,  2.53],
                                  [0.51, -2.96,  1.34],
                                  [2.04, -2.96,  1.38],
                                  [2.67, -3.46,  2.27],
                                  [-0.07, -1.52,  1.29],
                                  [0.2, -0.77, -0.05],
                                  [-0.38,  0.65, -0.],
                                  [-0.14,  1.37, -1.3],
                                  [-0.49,  2.57, -1.61],
                                  [-1.11,  3.37, -0.79],
                                  [-0.21,  3.05, -2.81],
                                  [0.61, -4.02,  3.2],
                                  [0.22, -3.52,  0.45],
                                  [0.34, -0.95,  2.13],
                                  [-1.15, -1.57,  1.45],
                                  [-0.25, -1.33, -0.88],
                                  [1.27, -0.71, -0.25],
                                  [0.08,  1.22,  0.81],
                                  [-1.45,  0.62,  0.2],
                                  [0.34,  0.81, -2.],
                                  [-1.35,  3.05,  0.16],
                                  [-1.39,  4.32, -1.01],
                                  [0.28,  2.45, -3.46],
                                  [-0.48,  4.01, -3.04]]),
                 atom_names=['N', 'CA', 'C', 'O', 'CB', 'CG', 'CD', 'NE', 'CZ', 'NH1', 'NH2', 'H', 'HA',
                             'HB2', 'HB3', 'HG2', 'HG3', 'HD2', 'HD3', 'HE', 'HH11', 'HH12', 'HH21', 'HH22'],
                 bb=[0, 1, 2, 3, 11, 12],
                 sc=[4, 5, 6, 7, 8, 9, 10, 13, 14, 15,
                     16, 17, 18, 19, 20, 21, 22, 23],
                 bonds=[(0, 1), (0, 11), (1, 2), (1, 4), (1, 12), (2, 3), (4, 5), (4, 13), (4, 14), (5, 6), (5, 15), (5, 16),
                        (6, 7), (6, 17), (6, 18), (7, 8), (7, 19), (8, 9), (8, 10), (9, 20), (9, 21), (10, 22), (10, 23)],
                 offset=24)

N_info = AA_info(coords=np.array([[0.15, -1.78, -0.63],
                                  [0.76, -0.44, -0.59],
                                  [2.28, -0.5, -0.63],
                                  [2.93, -1.53, -0.6],
                                  [0.3,  0.37,  0.64],
                                  [-1.21,  0.48,  0.76],
                                  [-1.84,  1.38, -0.01],
                                  [-1.85, -0.21,  1.53],
                                  [0.87, -2.5, -0.61],
                                  [0.47,  0.1, -1.5],
                                  [0.73,  1.37,  0.61],
                                  [0.68, -0.11,  1.55],
                                  [-2.85,  1.45,  0.04],
                                  [-1.33,  1.96, -0.66]]),
                 atom_names=['N', 'CA', 'C', 'O', 'CB', 'CG', 'ND2',
                             'OD1', 'H', 'HA', 'HB2', 'HB3', 'HD21', 'HD22'],
                 bb=[0, 1, 2, 3, 8, 9],
                 sc=[4, 5, 6, 7, 10, 11, 12, 13],
                 bonds=[(0, 1), (0, 8), (1, 2), (1, 4), (1, 9), (2, 3), (4, 5),
                        (4, 10), (4, 11), (5, 6), (5, 7), (6, 12), (6, 13)],
                 offset=14)

D_info = AA_info(coords=np.array([[-0.67, -1.84, -0.63],
                                  [-0.06, -0.49, -0.62],
                                  [1.46, -0.53, -0.62],
                                  [2.11, -1.56, -0.64],
                                  [-0.58,  0.33,  0.59],
                                  [-0.08,  1.77,  0.62],
                                  [-0.,  2.38, -0.43],
                                  [0.17,  2.25,  1.71],
                                  [0.06, -2.55, -0.64],
                                  [-0.34,  0.03, -1.54],
                                  [-0.29, -0.16,  1.51],
                                  [-1.67,  0.36,  0.56]]),
                 atom_names=['N', 'CA', 'C', 'O', 'CB', 'CG',
                             'OD1', 'OD2', 'H', 'HA', 'HB2', 'HB3'],
                 bb=[0, 1, 2, 3, 8, 9],
                 sc=[4, 5, 6, 7, 10, 11],
                 bonds=[(0, 1), (0, 8), (1, 2), (1, 4), (1, 9), (2, 3),
                        (4, 5), (4, 10), (4, 11), (5, 6), (5, 7)],
                 offset=12)

C_info = AA_info(coords=np.array([[-0.72, -1.62, -0.64],
                                  [-0.12, -0.27, -0.64],
                                  [1.39, -0.31, -0.63],
                                  [2.05, -1.33, -0.62],
                                  [-0.67,  0.53,  0.56],
                                  [-0.17,  2.29,  0.55],
                                  [0.01, -2.32, -0.65],
                                  [-0.41,  0.24, -1.56],
                                  [-0.32,  0.08,  1.5],
                                  [-1.76,  0.5,  0.54],
                                  [0.79,  2.19,  1.49]]),
                 atom_names=['N', 'CA', 'C', 'O', 'CB',
                             'SG', 'H', 'HA', 'HB2', 'HB3', 'HG'],
                 bb=[0, 1, 2, 3, 6, 7],
                 sc=[4, 5, 8, 9, 10],
                 bonds=[(0, 1), (0, 6), (1, 2), (1, 4), (1, 7),
                        (2, 3), (4, 5), (4, 8), (4, 9), (5, 10)],
                 offset=11)

E_info = AA_info(coords=np.array([[-0.55, -2.38, -1.05],
                                  [0.04, -1.01, -1.02],
                                  [1.56, -1.05, -1.05],
                                  [2.19, -2.09, -1.05],
                                  [-0.48, -0.21,  0.21],
                                  [0.01,  1.27,  0.29],
                                  [-0.41,  2.11,  1.49],
                                  [-1.17,  1.52,  2.39],
                                  [-0.01,  3.23,  1.6],
                                  [0.12, -3.15, -1.04],
                                  [-0.26, -0.48, -1.95],
                                  [-0.18, -0.74,  1.12],
                                  [-1.58, -0.22,  0.2],
                                  [-0.31,  1.83, -0.58],
                                  [1.1,  1.31,  0.31]]),
                 atom_names=['N', 'CA', 'C', 'O', 'CB', 'CG', 'CD',
                             'OE1', 'OE2', 'H', 'HA', 'HB2', 'HB3', 'HG2', 'HG3'],
                 bb=[0, 1, 2, 3, 9, 10],
                 sc=[4, 5, 6, 7, 8, 11, 12, 13, 14],
                 bonds=[(0, 1), (0, 9), (1, 2), (1, 4), (1, 10), (2, 3), (4, 5),
                        (4, 11), (4, 12), (5, 6), (5, 13), (5, 14), (6, 7), (6, 8)],
                 offset=15)

Q_info = AA_info(coords=np.array([[-0.39, -2.48, -1.42],
                                  [0.22, -1.13, -1.37],
                                  [1.74, -1.19, -1.41],
                                  [2.39, -2.22, -1.38],
                                  [-0.29, -0.37, -0.12],
                                  [0.19,  1.11, -0.04],
                                  [-0.26,  1.91,  1.18],
                                  [-1.02,  1.3,  2.12],
                                  [0.09,  3.06,  1.34],
                                  [0.33, -3.2, -1.4],
                                  [-0.07, -0.6, -2.28],
                                  [0.02, -0.91,  0.77],
                                  [-1.39, -0.38, -0.15],
                                  [-0.14,  1.65, -0.93],
                                  [1.28,  1.13, -0.04],
                                  [-1.28,  1.82,  2.94],
                                  [-1.3,  0.34,  2.01]]),
                 atom_names=['N', 'CA', 'C', 'O', 'CB', 'CG', 'CD', 'NE2',
                             'OE1', 'H', 'HA', 'HB2', 'HB3', 'HG2', 'HG3', 'HE21', 'HE22'],
                 bb=[0, 1, 2, 3, 9, 10],
                 sc=[4, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16],
                 bonds=[(0, 1), (0, 9), (1, 2), (1, 4), (1, 10), (2, 3), (4, 5), (4, 11),
                        (4, 12), (5, 6), (5, 13), (5, 14), (6, 7), (6, 8), (7, 15), (7, 16)],
                 offset=17)

G_info = AA_info(coords=np.array([[-1.25,  0.2, -0.25],
                                  [0.2,  0.29, -0.51],
                                  [1.04, -0.41,  0.54],
                                  [0.6, -0.99,  1.5],
                                  [-1.42, -0.33,  0.61],
                                  [0.43, -0.17, -1.47],
                                  [0.51,  1.34, -0.52]]),
                 atom_names=['N', 'CA', 'C', 'O', 'H', 'HA2', 'HA3'],
                 bb=[0, 1, 2, 3, 4, 5, 6],
                 sc=[],
                 bonds=[(0, 1), (0, 4), (1, 2), (1, 5), (1, 6), (2, 3)],
                 offset=7)

H_info = AA_info(coords=np.array([[0.48, -2.42, -1.17],
                                  [1.07, -1.1, -0.85],
                                  [2.58, -1.1, -0.89],
                                  [3.22, -2.11, -1.12],
                                  [0.58, -0.67,  0.57],
                                  [-0.26,  0.58,  0.55],
                                  [-1.64,  0.55,  0.39],
                                  [0.19,  1.83,  0.69],
                                  [-0.95,  2.55,  0.61],
                                  [-2.06,  1.81,  0.42],
                                  [1.15, -3.17, -1.33],
                                  [0.75, -0.38, -1.62],
                                  [1.41, -0.53,  1.25],
                                  [-0.03, -1.48,  1.],
                                  [-2.26, -0.33,  0.25],
                                  [-0.89,  3.63,  0.69],
                                  [-3.07,  2.19,  0.33]]),
                 atom_names=['N', 'CA', 'C', 'O', 'CB', 'CG', 'CD2', 'ND1',
                             'CE1', 'NE2', 'H', 'HA', 'HB2', 'HB3', 'HD2', 'HE1', 'HE2'],
                 bb=[0, 1, 2, 3, 10, 11],
                 sc=[4, 5, 6, 7, 8, 9, 12, 13, 14, 15, 16],
                 bonds=[(0, 1), (0, 10), (1, 2), (1, 4), (1, 11), (2, 3), (4, 5), (4, 12),
                        (4, 13), (5, 6), (5, 7), (6, 9), (6, 14), (7, 8), (8, 9), (8, 15), (9, 16)],
                 offset=17)

I_info = AA_info(coords=np.array([[0.35, -2.2, -1.14],
                                  [0.87, -0.81, -1.13],
                                  [2.39, -0.8, -1.14],
                                  [3.1, -1.79, -1.14],
                                  [0.3,  0.03,  0.06],
                                  [-1.26, -0.04,  0.14],
                                  [0.75,  1.51,  0.02],
                                  [-1.85,  0.5,  1.47],
                                  [1.11, -2.88, -1.16],
                                  [0.58, -0.33, -2.07],
                                  [0.7, -0.42,  0.98],
                                  [-1.6, -1.07,  0.05],
                                  [-1.7,  0.51, -0.69],
                                  [1.84,  1.61,  0.05],
                                  [0.39,  2.02, -0.88],
                                  [0.38,  2.06,  0.89],
                                  [-1.43, -0.04,  2.33],
                                  [-1.66,  1.57,  1.61],
                                  [-2.93,  0.36,  1.49]]),
                 atom_names=['N', 'CA', 'C', 'O', 'CB', 'CG1', 'CG2', 'CD1', 'H', 'HA',
                             'HB', 'HG12', 'HG13', 'HG21', 'HG22', 'HG23', 'HD11', 'HD12', 'HD13'],
                 bb=[0, 1, 2, 3, 8, 9],
                 sc=[4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 16, 17, 18],
                 bonds=[(0, 1), (0, 8), (1, 2), (1, 4), (1, 9), (2, 3), (4, 5), (4, 6), (4, 10),
                        (5, 7), (5, 11), (5, 12), (6, 13), (6, 14), (6, 15), (7, 16), (7, 17), (7, 18)],
                 offset=19)

L_info = AA_info(coords=np.array([[0.63, -2.16, -0.9],
                                  [1.25, -0.81, -0.9],
                                  [2.77, -0.86, -0.91],
                                  [3.41, -1.9, -0.9],
                                  [0.75,  0.03,  0.31],
                                  [-0.79,  0.2,  0.44],
                                  [-1.11,  1.13,  1.64],
                                  [-1.45,  0.77, -0.84],
                                  [1.33, -2.9, -0.91],
                                  [0.98, -0.29, -1.82],
                                  [1.21,  1.02,  0.27],
                                  [1.12, -0.44,  1.23],
                                  [-1.24, -0.77,  0.65],
                                  [-0.69,  0.73,  2.57],
                                  [-0.7,  2.13,  1.49],
                                  [-2.19,  1.23,  1.79],
                                  [-1.,  1.72, -1.13],
                                  [-1.35,  0.06, -1.67],
                                  [-2.52,  0.93, -0.68]]),
                 atom_names=['N', 'CA', 'C', 'O', 'CB', 'CG', 'CD1', 'CD2', 'H', 'HA',
                             'HB2', 'HB3', 'HG', 'HD11', 'HD12', 'HD13', 'HD21', 'HD22', 'HD23'],
                 bb=[0, 1, 2, 3, 8, 9],
                 sc=[4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 16, 17, 18],
                 bonds=[(0, 1), (0, 8), (1, 2), (1, 4), (1, 9), (2, 3), (4, 5), (4, 10), (4, 11),
                        (5, 6), (5, 7), (5, 12), (6, 13), (6, 14), (6, 15), (7, 16), (7, 17), (7, 18)],
                 offset=19)

K_info = AA_info(coords=np.array([[-4.94e-01,  -3.60e+00,  -1.67e+00],
                                  [1.09e-01,  -2.26e+00,  -1.68e+00],
                                  [1.62e+00,  -2.32e+00,  -1.73e+00],
                                  [2.28e+00,  -3.33e+00,  -1.62e+00],
                                  [-3.98e-01,  -1.46e+00,  -4.42e-01],
                                  [1.06e-01,  -1.00e-03,  -4.40e-01],
                                  [-3.72e-01,   7.71e-01,   8.00e-01],
                                  [2.11e-01,   2.19e+00,   8.10e-01],
                                  [-3.47e-01,   3.07e+00,   1.89e+00],
                                  [2.46e-01,  -4.30e+00,  -1.69e+00],
                                  [-2.06e-01,  -1.74e+00,  -2.59e+00],
                                  [-7.70e-02,  -1.97e+00,   4.71e-01],
                                  [-1.49e+00,  -1.46e+00,  -4.51e-01],
                                  [-2.38e-01,   5.00e-01,  -1.35e+00],
                                  [1.20e+00,   8.00e-03,  -4.58e-01],
                                  [-6.10e-02,   2.36e-01,   1.71e+00],
                                  [-1.47e+00,   8.16e-01,   8.01e-01],
                                  [-1.10e-02,   2.70e+00,  -1.29e-01],
                                  [1.30e+00,   2.13e+00,   9.27e-01],
                                  [-4.74e-01,   2.51e+00,   2.76e+00],
                                  [2.81e-01,   3.86e+00,   2.10e+00],
                                  [-1.27e+00,   3.44e+00,   1.62e+00]]),
                 atom_names=['N', 'CA', 'C', 'O', 'CB', 'CG', 'CD', 'CE', 'NZ', 'H', 'HA',
                             'HB2', 'HB3', 'HG2', 'HG3', 'HD2', 'HD3', 'HE2', 'HE3', 'HZ1', 'HZ2', 'HZ3'],
                 bb=[0, 1, 2, 3, 9, 10],
                 sc=[4, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21],
                 bonds=[(0, 1), (0, 9), (1, 2), (1, 4), (1, 10), (2, 3), (4, 5), (4, 11), (4, 12), (5, 6), (5, 13),
                        (5, 14), (6, 7), (6, 15), (6, 16), (7, 8), (7, 17), (7, 18), (8, 19), (8, 20), (8, 21)],
                 offset=22)

M_info = AA_info(coords=np.array([[1., -1.99, -1.18],
                                  [1.53, -0.61, -1.18],
                                  [3.04, -0.58, -1.22],
                                  [3.76, -1.56, -1.15],
                                  [1.02,  0.18,  0.05],
                                  [-0.52,  0.32,  0.08],
                                  [-1.04,  1.47,  1.4],
                                  [-2.82,  1.13,  1.44],
                                  [1.78, -2.65, -1.19],
                                  [1.2, -0.1, -2.1],
                                  [1.46,  1.18,  0.03],
                                  [1.35, -0.31,  0.96],
                                  [-0.97, -0.65,  0.26],
                                  [-0.88,  0.71, -0.88],
                                  [-3.25,  1.3,  0.45],
                                  [-2.98,  0.1,  1.75],
                                  [-3.3,  1.81,  2.16]]),
                 atom_names=['N', 'CA', 'C', 'O', 'CB', 'CG', 'SD', 'CE', 'H',
                             'HA', 'HB2', 'HB3', 'HG2', 'HG3', 'HE1', 'HE2', 'HE3'],
                 bb=[0, 1, 2, 3, 8, 9],
                 sc=[4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 16],
                 bonds=[(0, 1), (0, 8), (1, 2), (1, 4), (1, 9), (2, 3), (4, 5), (4, 10),
                        (4, 11), (5, 6), (5, 12), (5, 13), (6, 7), (7, 14), (7, 15), (7, 16)],
                 offset=17)

F_info = AA_info(coords=np.array([[1.34, -1.84, -1.06],
                                  [1.9, -0.47, -1.04],
                                  [3.41, -0.47, -1.1],
                                  [4.11, -1.47, -1.04],
                                  [1.42,  0.32,  0.21],
                                  [-0.1,  0.39,  0.34],
                                  [-0.79, -0.61,  1.03],
                                  [-0.82,  1.46, -0.2],
                                  [-2.18, -0.57,  1.14],
                                  [-2.21,  1.5, -0.09],
                                  [-2.89,  0.49,  0.58],
                                  [2.09, -2.53, -1.06],
                                  [1.57,  0.06, -1.93],
                                  [1.82,  1.33,  0.17],
                                  [1.83, -0.14,  1.11],
                                  [-0.25, -1.44,  1.47],
                                  [-0.31,  2.26, -0.74],
                                  [-2.71, -1.37,  1.66],
                                  [-2.77,  2.33, -0.52],
                                  [-3.98,  0.51,  0.66]]),
                 atom_names=['N', 'CA', 'C', 'O', 'CB', 'CG', 'CD1', 'CD2', 'CE1', 'CE2',
                             'CZ', 'H', 'HA', 'HB2', 'HB3', 'HD1', 'HD2', 'HE1', 'HE2', 'HZ'],
                 bb=[0, 1, 2, 3, 11, 12],
                 sc=[4, 5, 6, 7, 8, 9, 10, 13, 14, 15, 16, 17, 18, 19],
                 bonds=[(0, 1), (0, 11), (1, 2), (1, 4), (1, 12), (2, 3), (4, 5), (4, 13), (4, 14), (5, 6),
                        (5, 7), (6, 8), (6, 15), (7, 9), (7, 16), (8, 10), (8, 17), (9, 10), (9, 18), (10, 19)],
                 offset=20)

P_info = AA_info(coords=np.array([[0.73, -0.63,  1.11],
                                  [1.17,  0.41,  0.14],
                                  [1.07,  1.82,  0.72],
                                  [0.6,  2.1,  1.8],
                                  [0.27,  0.24, -1.09],
                                  [-1.04, -0.26, -0.49],
                                  [-0.57, -1.17,  0.65],
                                  [2.22,  0.26, -0.14],
                                  [0.69, -0.53, -1.75],
                                  [0.14,  1.16, -1.66],
                                  [-1.65, -0.8, -1.22],
                                  [-1.62,  0.58, -0.09],
                                  [-0.42, -2.18,  0.28],
                                  [-1.29, -1.18,  1.47]]),
                 atom_names=['N', 'CA', 'C', 'O', 'CB', 'CG', 'CD',
                             'HA', 'HB2', 'HB3', 'HG2', 'HG3', 'HD2', 'HD3'],
                 bb=[0, 1, 2, 3, 7],
                 sc=[4, 5, 6, 8, 9, 10, 11, 12, 13],
                 bonds=[(0, 1), (0, 6), (1, 2), (1, 4), (1, 7), (2, 3), (4, 5),
                        (4, 8), (4, 9), (5, 6), (5, 10), (5, 11), (6, 12), (6, 13)],
                 offset=14)

S_info = AA_info(coords=np.array([[-0.66, -1.27, -0.65],
                                  [0.01,  0.04, -0.69],
                                  [1.52, -0.08, -0.71],
                                  [2.12, -1.14, -0.71],
                                  [-0.47,  0.93,  0.48],
                                  [-0.02,  0.42,  1.73],
                                  [0.04, -2.01, -0.72],
                                  [-0.25,  0.54, -1.63],
                                  [-1.56,  0.96,  0.47],
                                  [-0.09,  1.94,  0.34],
                                  [-0.38, -0.48,  1.86]]),
                 atom_names=['N', 'CA', 'C', 'O', 'CB',
                             'OG', 'H', 'HA', 'HB2', 'HB3', 'HG'],
                 bb=[0, 1, 2, 3, 6, 7],
                 sc=[4, 5, 8, 9, 10],
                 bonds=[(0, 1), (0, 6), (1, 2), (1, 4), (1, 7),
                        (2, 3), (4, 5), (4, 8), (4, 9), (5, 10)],
                 offset=11)

T_info = AA_info(coords=np.array([[-0.55, -2.02, -0.81],
                                  [0.11, -0.69, -0.81],
                                  [1.63, -0.81, -0.83],
                                  [2.23, -1.87, -0.79],
                                  [-0.38,  0.14,  0.42],
                                  [0.23,  1.56,  0.53],
                                  [-1.79,  0.31,  0.35],
                                  [0.13, -2.78, -0.82],
                                  [-0.16, -0.17, -1.72],
                                  [-0.13, -0.41,  1.33],
                                  [-2.07,  0.79,  1.15],
                                  [-0.19,  2.08,  1.4],
                                  [1.32,  1.53,  0.67],
                                  [0.,  2.16, -0.36]]),
                 atom_names=['N', 'CA', 'C', 'O', 'CB', 'CG2', 'OG1',
                             'H', 'HA', 'HB', 'HG1', 'HG21', 'HG22', 'HG23'],
                 bb=[0, 1, 2, 3, 7, 8],
                 sc=[4, 5, 6, 9, 10, 11, 12, 13],
                 bonds=[(0, 1), (0, 7), (1, 2), (1, 4), (1, 8), (2, 3), (4, 5),
                        (4, 6), (4, 9), (5, 11), (5, 12), (5, 13), (6, 10)],
                 offset=14)

W_info = AA_info(coords=np.array([[1.58, -2.74, -0.43],
                                  [2.2, -1.39, -0.43],
                                  [3.71, -1.44, -0.46],
                                  [4.36, -2.47, -0.42],
                                  [1.71, -0.57,  0.79],
                                  [0.2, -0.32,  0.78],
                                  [-0.75, -1.07,  1.46],
                                  [-0.5,  0.65,  0.1],
                                  [-1.84,  0.48,  0.37],
                                  [-0.05,  1.67, -0.75],
                                  [-2., -0.58,  1.2],
                                  [-2.8,  1.31, -0.21],
                                  [-1.01,  2.51, -1.33],
                                  [-2.38,  2.33, -1.06],
                                  [2.29, -3.47, -0.41],
                                  [1.91, -0.87, -1.35],
                                  [2.23,  0.38,  0.84],
                                  [1.98, -1.11,  1.7],
                                  [-0.59, -1.94,  2.09],
                                  [-2.96, -0.99,  1.56],
                                  [1.,  1.81, -0.96],
                                  [-3.87,  1.16, -0.01],
                                  [-0.7,  3.3, -2.],
                                  [-3.1,  2.99, -1.53]]),
                 atom_names=['N', 'CA', 'C', 'O', 'CB', 'CG', 'CD1', 'CD2', 'CE2', 'CE3', 'NE1', 'CZ2',
                             'CZ3', 'CH2', 'H', 'HA', 'HB2', 'HB3', 'HD1', 'HE1', 'HE3', 'HZ2', 'HZ3', 'HH2'],
                 bb=[0, 1, 2, 3, 14, 15],
                 sc=[4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
                     16, 17, 18, 19, 20, 21, 22, 23],
                 bonds=[(0, 1), (0, 14), (1, 2), (1, 4), (1, 15), (2, 3), (4, 5), (4, 16), (4, 17), (5, 6), (5, 7), (6, 10), (6, 18),
                        (7, 8), (7, 9), (8, 10), (8, 11), (9, 12), (9, 20), (10, 19), (11, 13), (11, 21), (12, 13), (12, 22), (13, 23)],
                 offset=24)

Y_info = AA_info(coords=np.array([[-0.84, -2.19,  1.38],
                                  [0.62, -2.08,  1.12],
                                  [1.44, -2.84,  2.14],
                                  [0.98, -3.39,  3.12],
                                  [1.08, -0.6,  1.08],
                                  [0.41,  0.26,  0.02],
                                  [0.83,  0.23, -1.31],
                                  [-0.63,  1.14,  0.38],
                                  [0.21,  1.03, -2.27],
                                  [-1.24,  1.94, -0.57],
                                  [-0.82,  1.89, -1.9],
                                  [-1.42,  2.68, -2.84],
                                  [-1.05, -2.73,  2.23],
                                  [0.85, -2.55,  0.16],
                                  [2.16, -0.56,  0.92],
                                  [0.92, -0.16,  2.07],
                                  [1.64, -0.43, -1.61],
                                  [-0.96,  1.19,  1.42],
                                  [0.54,  1., -3.31],
                                  [-2.04,  2.61, -0.27],
                                  [-2.12,  3.21, -2.41]]),
                 atom_names=['N', 'CA', 'C', 'O', 'CB', 'CG', 'CD1', 'CD2', 'CE1', 'CE2',
                             'CZ', 'OH', 'H', 'HA', 'HB2', 'HB3', 'HD1', 'HD2', 'HE1', 'HE2', 'HH'],
                 bb=[0, 1, 2, 3, 12, 13],
                 sc=[4, 5, 6, 7, 8, 9, 10, 11, 14, 15, 16, 17, 18, 19, 20],
                 bonds=[(0, 1), (0, 12), (1, 2), (1, 4), (1, 13), (2, 3), (4, 5), (4, 14), (4, 15), (5, 6), (5, 7),
                        (6, 8), (6, 16), (7, 9), (7, 17), (8, 10), (8, 18), (9, 10), (9, 19), (10, 11), (11, 20)],
                 offset=21)

V_info = AA_info(coords=np.array([[-0.13, -2.06, -0.86],
                                  [0.43, -0.69, -0.82],
                                  [1.95, -0.72, -0.85],
                                  [2.63, -1.73, -0.84],
                                  [-0.12,  0.08,  0.42],
                                  [0.53,  1.48,  0.61],
                                  [-1.65,  0.27,  0.34],
                                  [0.63, -2.74, -0.87],
                                  [0.13, -0.17, -1.73],
                                  [0.11, -0.51,  1.31],
                                  [0.35,  2.13, -0.25],
                                  [0.09,  1.97,  1.49],
                                  [1.6,  1.41,  0.79],
                                  [-2.17, -0.68,  0.23],
                                  [-2.02,  0.74,  1.26],
                                  [-1.93,  0.92, -0.49]]),
                 atom_names=['N', 'CA', 'C', 'O', 'CB', 'CG1', 'CG2', 'H', 'HA',
                             'HB', 'HG11', 'HG12', 'HG13', 'HG21', 'HG22', 'HG23'],
                 bb=[0, 1, 2, 3, 7, 8],
                 sc=[4, 5, 6, 9, 10, 11, 12, 13, 14, 15],
                 bonds=[(0, 1), (0, 7), (1, 2), (1, 4), (1, 8), (2, 3), (4, 5), (4, 6),
                        (4, 9), (5, 10), (5, 11), (5, 12), (6, 13), (6, 14), (6, 15)],
                 offset=16)

B_info = AA_info(coords=np.array([[ 1.12,  0.22, -0.24],
                                  [ 1.74, -0.73, -0.65],
                                  [-0.34,  0.14,  0.17],
                                  [-0.47, -0.57,  0.98],
                                  [-0.68,  1.12,  0.5 ],
                                  [-0.95, -0.17, -0.69]]),
             atom_names = ['C', 'O', 'CH3', 'HH31', 'HH32', 'HH33'],
             bb = [0, 2],
             sc = [1, 3, 4, 5],
             bonds = [(0, 1), (0, 2), (2, 3), (2, 4), (2, 5)],
             offset = 6)

Z_info = AA_info(coords=np.array([[ 2.23,  0.97, -0.68],
                                  [ 3.68,  0.81, -0.52],
                                  [ 1.91,  1.81, -0.21],
                                  [ 3.93,  0.74,  0.55],
                                  [ 4.02, -0.09, -1.03],
                                  [ 4.2 ,  1.68, -0.94]]),
                  atom_names = ['N', 'CH3', 'H', 'HH31', 'HH32', 'HH33'],
                  bb = [0, 2],
                  sc = [1, 3, 4, 5],
                  bonds = [(0, 1), (0, 2), (1, 3), (1, 4), (1, 5)],
                  offset = 6)

templates_aa = {'A': A_info, 'C': C_info, 'D': D_info, 'E': E_info,
                'F': F_info, 'G': G_info, 'H': H_info, 'I': I_info,
                'K': K_info, 'L': L_info, 'M': M_info, 'N': N_info,
                'P': P_info, 'Q': Q_info, 'R': R_info, 'S': S_info,
                'T': T_info, 'V': V_info, 'W': W_info, 'Y': Y_info,
                'B': B_info, 'Z': Z_info}

one_to_three_aa = {'A': 'ALA', 'C': 'CYS', 'D': 'ASP', 'E': 'GLU', 'F': 'PHE',
                'G': 'GLY', 'H': 'HIS', 'I': 'ILE', 'K': 'LYS', 'L': 'LEU',
                'M': 'MET', 'N': 'ASN', 'P': 'PRO', 'Q': 'GLN', 'R': 'ARG',
                'S': 'SER', 'T': 'THR', 'V': 'VAL', 'W': 'TRP', 'Y': 'TYR',
                'B': 'ACE', 'Z': 'NME'}

three_to_one_aa = {val: key for key, val in one_to_three_aa.items()}


"""
Coordinate Transformation Functions

This module contains the functions for converting one
`sunpy.coordinates.frames` object to another.

.. warning::

  The functions in this submodule should never be called directly, transforming
  between coordinate frames should be done using the ``.transform_to`` methods
  on `~astropy.coordinates.BaseCoordinateFrame` or
  `~astropy.coordinates.SkyCoord` instances.

"""
import logging
from copy import deepcopy
from functools import wraps
from contextlib import contextmanager

import numpy as np

import astropy.units as u
from astropy.constants import c as speed_of_light
from astropy.coordinates import (
    HCRS,
    ICRS,
    BaseCoordinateFrame,
    ConvertError,
    HeliocentricMeanEcliptic,
    get_body_barycentric,
)
from astropy.coordinates.baseframe import frame_transform_graph
from astropy.coordinates.builtin_frames import make_transform_graph_docs
from astropy.coordinates.builtin_frames.utils import get_jd12
from astropy.coordinates.matrix_utilities import matrix_product, matrix_transpose, rotation_matrix
from astropy.coordinates.representation import (
    CartesianRepresentation,
    SphericalRepresentation,
    UnitSphericalRepresentation,
)
# Import erfa via astropy to make sure we are using the same ERFA library as Astropy
from astropy.coordinates.sky_coordinate import erfa
from astropy.coordinates.transformations import FunctionTransform, FunctionTransformWithFiniteDifference
from astropy.time import Time

from sunpy import log
from sunpy.sun import constants
from .frames import (
    _J2000,
    GeocentricEarthEquatorial,
    GeocentricSolarEcliptic,
    Heliocentric,
    HeliocentricEarthEcliptic,
    HeliocentricInertial,
    HeliographicCarrington,
    HeliographicStonyhurst,
    Helioprojective,
)

RSUN_METERS = constants.get('radius').si.to(u.m)

__all__ = ['transform_with_sun_center',
           'propagate_with_solar_surface',
           'hgs_to_hgc', 'hgc_to_hgs', 'hcc_to_hpc',
           'hpc_to_hcc', 'hcc_to_hgs', 'hgs_to_hcc',
           'hpc_to_hpc',
           'hcrs_to_hgs', 'hgs_to_hcrs',
           'hgs_to_hgs', 'hgc_to_hgc', 'hcc_to_hcc',
           'hme_to_hee', 'hee_to_hme', 'hee_to_hee',
           'hee_to_gse', 'gse_to_hee', 'gse_to_gse',
           'hgs_to_hci', 'hci_to_hgs', 'hci_to_hci',
           'hme_to_gei', 'gei_to_hme', 'gei_to_gei']

# Boolean flag for whether to ignore the motion of the center of the Sun in inertial space
_ignore_sun_motion = False

# If not None, the name of the differential-rotation model to use for any obstime change
_autoapply_diffrot = None

@contextmanager
def transform_with_sun_center():
    """
    Context manager for coordinate transformations to ignore the motion of the center of the Sun.

    Normally, coordinates refer to a point in inertial space (relative to the barycenter of the
    solar system).  Transforming to a different observation time does not move the point at all,
    but rather only updates the coordinate representation as needed for the origin and axis
    orientations at the new observation time.  However, the center of the Sun moves over time.
    Thus, for example, a coordinate that lies on the surface of the Sun at one observation time
    will not continue to lie on the surface of the Sun at other observation times.

    Under this context manager, transformations will instead move the coordinate over time to
    "follow" the translational motion of the center of Sun, thus maintaining the position of the
    coordinate relative to the center of the Sun.

    Notes
    -----
    This context manager accounts only for the motion of the center of the Sun, i.e.,
    translational motion.  The motion of solar features due to any rotation of the Sun about its
    rotational axis is not accounted for.

    Due to the implementation approach, this context manager modifies transformations between only
    these five coordinate frames:
    `~sunpy.coordinates.frames.HeliographicStonyhurst`,
    `~sunpy.coordinates.frames.HeliographicCarrington`,
    `~sunpy.coordinates.frames.HeliocentricInertial`,
    `~sunpy.coordinates.frames.Heliocentric`, and
    `~sunpy.coordinates.frames.Helioprojective`.

    Examples
    --------
    >>> from astropy.coordinates import SkyCoord
    >>> from sunpy.coordinates import HeliographicStonyhurst, transform_with_sun_center
    >>> import astropy.units as u
    >>> start_frame = HeliographicStonyhurst(obstime="2001-01-01")
    >>> end_frame = HeliographicStonyhurst(obstime="2001-02-01")
    >>> sun_center = SkyCoord(0*u.deg, 0*u.deg, 0*u.AU, frame=start_frame)
    >>> sun_center
    <SkyCoord (HeliographicStonyhurst: obstime=2001-01-01T00:00:00.000, rsun=695700.0 km): (lon, lat, radius) in (deg, deg, AU)
        (0., 0., 0.)>
    >>> sun_center.transform_to(end_frame)  # transformations do not normally follow Sun center
    <SkyCoord (HeliographicStonyhurst: obstime=2001-02-01T00:00:00.000, rsun=695700.0 km): (lon, lat, radius) in (deg, deg, AU)
        (23.33174233, -5.96399877, 0.00027959)>
    >>> with transform_with_sun_center():
    ...     sun_center.transform_to(end_frame)  # now following Sun center
    <SkyCoord (HeliographicStonyhurst: obstime=2001-02-01T00:00:00.000, rsun=695700.0 km): (lon, lat, radius) in (deg, deg, AU)
        (0., 0., 0.)>
    """
    try:
        global _ignore_sun_motion

        old_ignore_sun_motion = _ignore_sun_motion  # nominally False

        if not old_ignore_sun_motion:
            log.debug("Ignoring the motion of the center of the Sun for transformations")
        _ignore_sun_motion = True
        yield
    finally:
        if not old_ignore_sun_motion:
            log.debug("Stop ignoring the motion of the center of the Sun for transformations")
        _ignore_sun_motion = old_ignore_sun_motion

@contextmanager
def propagate_with_solar_surface(rotation_model='howard'):
    """
    Context manager for coordinate transformations to automatically apply solar
    differential rotation for any change in observation time.

    Normally, coordinates refer to a point in inertial space (relative to the
    barycenter of the solar system).  Transforming to a different observation time
    does not move the point at all, but rather only updates the coordinate
    representation as needed for the origin and axis orientations at the new
    observation time.

    Under this context manager, transformations will instead treat the coordinate
    as if it were referring to a point on the solar surface instead of a point in
    inertial space.  If a transformation has a change in observation time, the
    heliographic longitude of the point will be updated according to the specified
    rotation model.

    Parameters
    ----------
    rotation_model : `str`
        Accepted model names are ``'howard'`` (default), ``'snodgrass'``,
        ``'allen'``, and ``'rigid'``.  See the documentation for
        :func:`~sunpy.physics.differential_rotation.diff_rot` for the differences
        between these models.

    Notes
    -----
    This context manager also ignores the motion of the center of the Sun (see
    :func:`~sunpy.coordinates.transformations.transform_with_sun_center`).

    Due to the implementation approach, this context manager modifies
    transformations between only these five coordinate frames:
    `~sunpy.coordinates.frames.HeliographicStonyhurst`,
    `~sunpy.coordinates.frames.HeliographicCarrington`,
    `~sunpy.coordinates.frames.HeliocentricInertial`,
    `~sunpy.coordinates.frames.Heliocentric`, and
    `~sunpy.coordinates.frames.Helioprojective`.

    Examples
    --------
    .. minigallery:: sunpy.coordinates.propagate_with_solar_surface

    >>> import astropy.units as u
    >>> from astropy.coordinates import SkyCoord
    >>> from sunpy.coordinates import HeliocentricInertial, propagate_with_solar_surface
    >>> meridian = SkyCoord(0*u.deg, [-60, -30, 0, 30, 60]*u.deg, 1*u.AU,
    ...                     frame=HeliocentricInertial, obstime='2021-09-15')
    >>> out_frame = HeliocentricInertial(obstime='2021-09-21')
    >>> with propagate_with_solar_surface():
    ...     print(meridian.transform_to(out_frame))
    <SkyCoord (HeliocentricInertial: obstime=2021-09-21T00:00:00.000): (lon, lat, distance) in (deg, deg, AU)
        [(70.24182965, -60., 1.),
         (82.09298036, -30., 1.),
         (85.9579703 ,   0., 1.),
         (82.09298036,  30., 1.),
         (70.24182965,  60., 1.)]>
    >>> with propagate_with_solar_surface(rotation_model='rigid'):
    ...     print(meridian.transform_to(out_frame))
    <SkyCoord (HeliocentricInertial: obstime=2021-09-21T00:00:00.000): (lon, lat, distance) in (deg, deg, AU)
        [(85.1064, -60., 1.), (85.1064, -30., 1.),
         (85.1064,   0., 1.), (85.1064,  30., 1.),
         (85.1064,  60., 1.)]>
    """
    with transform_with_sun_center():
        try:
            global _autoapply_diffrot

            old_autoapply_diffrot = _autoapply_diffrot  # nominally False

            log.debug("Enabling automatic solar differential rotation "
                      f"('{rotation_model}') for any changes in obstime")
            _autoapply_diffrot = rotation_model
            yield
        finally:
            if not old_autoapply_diffrot:
                log.debug("Disabling automatic solar differential rotation "
                          "for any changes in obstime")
            _autoapply_diffrot = old_autoapply_diffrot

# Global counter to keep track of the layer of transformation
_layer_level = 0

def _transformation_debug(description):
    """
    Decorator to produce debugging output for a transformation function: its description, inputs,
    and output.  Unicode box-drawing characters are used.
    """
    def decorator(func):
        @wraps(func)
        def wrapped_func(*args, **kwargs):
            global _layer_level

            # Check if the logging level is at least DEBUG (for performance reasons)
            debug_output = log.getEffectiveLevel() <= logging.DEBUG

            if debug_output:
                # Indention for transformation layer
                indention = u"\u2502   " * _layer_level

                # For the input arguments, add indention to any lines after the first line
                from_str = str(args[0]).replace("\n", f"\n       {indention}\u2502       ")
                to_str = str(args[1]).replace("\n", f"\n       {indention}\u2502       ")

                # Log the description and the input arguments
                log.debug(f"{indention}{description}")
                log.debug(f"{indention}\u251c\u2500From: {from_str}")
                log.debug(f"{indention}\u251c\u2500To  : {to_str}")

                # Increment the layer level to increase the indention for nested transformations
                _layer_level += 1

            result = func(*args, **kwargs)

            if debug_output:
                # Decrement the layer level
                _layer_level -= 1

                # For the output, add intention to any lines after the first line
                out_str = str(result).replace("\n", f"\n       {indention}        ")

                # Log the output
                log.debug(f"{indention}\u2514\u2500Out : {out_str}")

            return result
        return wrapped_func
    return decorator

def _observers_are_equal(obs_1, obs_2):
    # Note that this also lets pass the situation where both observers are None
    if obs_1 is obs_2:
        return True

    # obs_1 != obs_2
    if obs_1 is None:
        raise ConvertError("The source observer is set to None, but the transformation requires "
                           "the source observer to be specified, as the destination observer "
                           f"is set to {obs_2}.")
    if obs_2 is None:
        raise ConvertError("The destination observer is set to None, but the transformation "
                           "requires the destination observer to be specified, as the "
                           f"source observer is set to {obs_1}.")
    if isinstance(obs_1, str):
        if obs_1 == "self":
            return False
        raise ConvertError("The source observer needs to have `obstime` set because the "
                           "destination observer is different.")
    if isinstance(obs_2, str):
        if obs_2 == "self":
            return False
        raise ConvertError("The destination observer needs to have `obstime` set because the "
                           "source observer is different.")

    return np.atleast_1d((u.allclose(obs_1.lat, obs_2.lat) and
                          u.allclose(obs_1.lon, obs_2.lon) and
                          u.allclose(obs_1.radius, obs_2.radius) and
                          _times_are_equal(obs_1.obstime, obs_2.obstime))).all()

def _check_observer_defined(frame):
    if frame.observer is None:
        raise ConvertError("This transformation cannot be performed because the "
                           f"{frame.__class__.__name__} frame has observer=None.")
    elif isinstance(frame.observer, str):
        if frame.observer != "self":
            raise ConvertError("This transformation cannot be performed because the "
                               f"{frame.__class__.__name__} frame needs a specified obstime "
                               f"to fully resolve observer='{frame.observer}'.")
        elif not isinstance(frame, HeliographicCarrington):
            raise ConvertError(f"The {frame.__class__.__name__} frame has observer='self' "
                               "but this is valid for only HeliographicCarrington frames.")

def _times_are_equal(time_1, time_2):
    # Checks whether times are equal
    if isinstance(time_1, Time) and isinstance(time_2, Time):
        # We explicitly perform the check in TAI to avoid possible numerical precision differences
        # between a time in UTC and the same time after a UTC->TAI->UTC conversion
        return np.all(time_1.tai == time_2.tai)

    # We also deem the times equal if they are both None
    return time_1 is None and time_2 is None

# =============================================================================
# ------------------------- Transformation Framework --------------------------
# =============================================================================

def _transform_obstime(frame, obstime):
    """
    Transform a frame to a new obstime using the appropriate loopback transformation.
    If the new obstime is None, no transformation is performed.
    If the frame's obstime is None, the frame is copied with the new obstime.
    """
    # If obstime is None or the obstime matches, nothing needs to be done
    if obstime is None or _times_are_equal(frame.obstime, obstime):
        return frame

    # Transform to the new obstime using the appropriate loopback transformation
    new_frame = frame.replicate(obstime=obstime)
    if frame.obstime is not None:
        return frame.transform_to(new_frame)
    else:
        return new_frame

def _rotation_matrix_hgs_to_hgc(obstime, observer_distance_from_sun):
    """
    Return the rotation matrix from HGS to HGC at the same observation time
    """
    if obstime is None:
        raise ConvertError("To perform this transformation, the coordinate"
                           " frame needs a specified `obstime`.")

    # Import here to avoid a circular import
    from .sun import L0, earth_distance

    # Calculate the difference in light travel time if the observer is at a different distance from
    # the Sun than the Earth is
    delta_time = (observer_distance_from_sun - earth_distance(obstime)) / speed_of_light

    # Calculate the corresponding difference in apparent longitude
    delta_lon = delta_time * constants.sidereal_rotation_rate

    # Rotation is only in longitude, so only around the Z axis
    return rotation_matrix(-(L0(obstime) + delta_lon), 'z')

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 HeliographicStonyhurst, HeliographicCarrington)
@_transformation_debug("HGS->HGC")
def hgs_to_hgc(hgscoord, hgcframe):
    """
    Convert from Heliographic Stonyhurst to Heliographic Carrington.
    """
    _check_observer_defined(hgcframe)
    if isinstance(hgcframe.observer, str) and hgcframe.observer == "self":
        observer_radius = hgscoord.radius
    else:
        observer_radius = hgcframe.observer.radius

    # First transform the HGS coord to the HGC obstime
    int_coord = _transform_obstime(hgscoord, hgcframe.obstime)

    # Rotate from HGS to HGC
    total_matrix = _rotation_matrix_hgs_to_hgc(int_coord.obstime, observer_radius)
    newrepr = int_coord.cartesian.transform(total_matrix)

    return hgcframe._replicate(newrepr, obstime=int_coord.obstime)

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 HeliographicCarrington, HeliographicStonyhurst)
@_transformation_debug("HGC->HGS")
def hgc_to_hgs(hgccoord, hgsframe):
    """
    Convert from Heliographic Carrington to Heliographic Stonyhurst.
    """
    _check_observer_defined(hgccoord)

    hgccoord = hgccoord.make_3d()

    if isinstance(hgccoord.observer, str) and hgccoord.observer == "self":
        observer_radius = hgccoord.radius
    else:
        observer_radius = hgccoord.observer.radius

    # First transform the HGC coord to the HGS obstime
    int_coord = _transform_obstime(hgccoord, hgsframe.obstime)

    # Rotate from HGC to HGS
    total_matrix = matrix_transpose(_rotation_matrix_hgs_to_hgc(int_coord.obstime,
                                                                observer_radius))
    newrepr = int_coord.cartesian.transform(total_matrix)

    return hgsframe._replicate(newrepr, obstime=int_coord.obstime)

def _matrix_hcc_to_hpc():
    # Returns the transformation matrix that permutes/swaps axes from HCC to HPC

    # HPC spherical coordinates are a left-handed frame with these equivalent Cartesian axes:
    #   HPC_X = -HCC_Z
    #   HPC_Y = HCC_X
    #   HPC_Z = HCC_Y
    # (HPC_X and HPC_Y are not to be confused with HPC_Tx and HPC_Ty)
    return np.array([[0, 0, -1],
                     [1, 0, 0],
                     [0, 1, 0]])

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 Heliocentric, Helioprojective)
@_transformation_debug("HCC->HPC")
def hcc_to_hpc(helioccoord, heliopframe):
    """
    Convert from Heliocentric Cartesian to Helioprojective Cartesian.
    """
    _check_observer_defined(helioccoord)
    _check_observer_defined(heliopframe)

    # Transform the HPC observer (in HGS) to the HPC obstime in case it's different
    observer = _transform_obstime(heliopframe.observer, heliopframe.obstime)

    # Loopback transform HCC coord to obstime and observer of HPC frame
    int_frame = Heliocentric(obstime=observer.obstime, observer=observer)
    int_coord = helioccoord.transform_to(int_frame)

    # Shift the origin from the Sun to the observer
    distance = int_coord.observer.radius
    newrepr = int_coord.cartesian - CartesianRepresentation(0*u.m, 0*u.m, distance)

    # Permute/swap axes from HCC to HPC equivalent Cartesian
    newrepr = newrepr.transform(_matrix_hcc_to_hpc())

    # Explicitly represent as spherical because external code (e.g., wcsaxes) expects it
    return heliopframe.realize_frame(newrepr.represent_as(SphericalRepresentation))

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 Helioprojective, Heliocentric)
@_transformation_debug("HPC->HCC")
def hpc_to_hcc(heliopcoord, heliocframe):
    """
    Convert from Helioprojective Cartesian to Heliocentric Cartesian.
    """
    _check_observer_defined(heliopcoord)
    _check_observer_defined(heliocframe)

    heliopcoord = heliopcoord.make_3d()

    # Permute/swap axes from HPC equivalent Cartesian to HCC
    newrepr = heliopcoord.cartesian.transform(matrix_transpose(_matrix_hcc_to_hpc()))

    # Transform the HPC observer (in HGS) to the HPC obstime in case it's different
    observer = _transform_obstime(heliopcoord.observer, heliopcoord.obstime)

    # Shift the origin from the observer to the Sun
    distance = observer.radius
    newrepr += CartesianRepresentation(0*u.m, 0*u.m, distance)

    # Complete the conversion of HPC to HCC at the obstime and observer of the HPC coord
    int_coord = Heliocentric(newrepr, obstime=observer.obstime, observer=observer)

    # Loopback transform HCC as needed
    return int_coord.transform_to(heliocframe)

def _rotation_matrix_hcc_to_hgs(longitude, latitude):
    # Returns the rotation matrix from HCC to HGS based on the observer longitude and latitude

    # Permute the axes of HCC to match HGS Cartesian equivalent
    #   HGS_X = HCC_Z
    #   HGS_Y = HCC_X
    #   HGS_Z = HCC_Y
    axes_matrix = np.array([[0, 0, 1],
                            [1, 0, 0],
                            [0, 1, 0]])

    # Rotate in latitude and longitude (sign difference because of direction difference)
    lat_matrix = rotation_matrix(latitude, 'y')
    lon_matrix = rotation_matrix(-longitude, 'z')

    return lon_matrix @ lat_matrix @ axes_matrix

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 Heliocentric, HeliographicStonyhurst)
@_transformation_debug("HCC->HGS")
def hcc_to_hgs(helioccoord, heliogframe):
    """
    Convert from Heliocentric Cartesian to Heliographic Stonyhurst.
    """
    _check_observer_defined(helioccoord)

    # Transform the HCC observer (in HGS) to the HCC obstime in case it's different
    hcc_observer_at_hcc_obstime = _transform_obstime(helioccoord.observer, helioccoord.obstime)

    total_matrix = _rotation_matrix_hcc_to_hgs(hcc_observer_at_hcc_obstime.lon,
                                               hcc_observer_at_hcc_obstime.lat)

    # Transform from HCC to HGS at the HCC obstime
    newrepr = helioccoord.cartesian.transform(total_matrix)
    int_coord = HeliographicStonyhurst(newrepr, obstime=hcc_observer_at_hcc_obstime.obstime)

    # For historical reasons, we support HCC with no obstime transforming to HGS with an obstime
    if int_coord.obstime is None and heliogframe.obstime is not None:
        int_coord = int_coord.replicate(obstime=heliogframe.obstime)

    # Loopback transform HGS as needed
    return int_coord.transform_to(heliogframe)

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 HeliographicStonyhurst, Heliocentric)
@_transformation_debug("HGS->HCC")
def hgs_to_hcc(heliogcoord, heliocframe):
    """
    Convert from Heliographic Stonyhurst to Heliocentric Cartesian.
    """
    _check_observer_defined(heliocframe)

    heliogcoord = heliogcoord.make_3d()

    # Loopback transform HGS if there is a change in obstime
    int_coord = _transform_obstime(heliogcoord, heliocframe.obstime)

    # Transform the HCC observer (in HGS) to the HCC obstime in case it's different
    hcc_observer_at_hcc_obstime = _transform_obstime(heliocframe.observer, int_coord.obstime)

    total_matrix = matrix_transpose(_rotation_matrix_hcc_to_hgs(hcc_observer_at_hcc_obstime.lon,
                                                                hcc_observer_at_hcc_obstime.lat))

    # Transform from HGS to HCC at the same obstime
    newrepr = int_coord.cartesian.transform(total_matrix)
    return heliocframe.realize_frame(newrepr)

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 Helioprojective, Helioprojective)
@_transformation_debug("HPC->HPC")
def hpc_to_hpc(from_coo, to_frame):
    """
    This converts from HPC to HPC, with different observer location parameters.
    It does this by transforming through HGS.
    """
    if _observers_are_equal(from_coo.observer, to_frame.observer) and \
       _times_are_equal(from_coo.obstime, to_frame.obstime):
        return to_frame.realize_frame(from_coo.data)

    _check_observer_defined(from_coo)
    _check_observer_defined(to_frame)

    hgs = from_coo.transform_to(HeliographicStonyhurst(obstime=to_frame.obstime))
    hpc = hgs.transform_to(to_frame)

    return hpc

def _rotation_matrix_reprs_to_reprs(start_representation, end_representation):
    """
    Return the matrix for the direct rotation from one representation to a second representation.
    The representations need not be normalized first, and can be arrays of representations.
    """
    A = start_representation.to_cartesian()
    B = end_representation.to_cartesian()
    rotation_axis = A.cross(B)
    rotation_angle = -np.arccos(A.dot(B) / (A.norm() * B.norm()))  # negation is required

    if rotation_angle.isscalar:
        # This line works around some input/output quirks of Astropy's rotation_matrix()
        matrix = np.array(rotation_matrix(rotation_angle, rotation_axis.xyz.value.tolist()))
    else:
        matrix_list = [np.array(rotation_matrix(angle, axis.xyz.value.tolist()))
                       for angle, axis in zip(rotation_angle, rotation_axis)]
        matrix = np.stack(matrix_list)

    return matrix

def _rotation_matrix_reprs_to_xz_about_z(representations):
    """
    Return one or more matrices for rotating one or more representations around the Z axis into the
    XZ plane.
    """
    A = representations.to_cartesian()

    # Zero out the Z components
    # (The additional transpose operations are to handle both scalar and array inputs)
    A_no_z = CartesianRepresentation((A.xyz.T * [1, 1, 0]).T)

    # Rotate the resulting vector to the X axis
    x_axis = CartesianRepresentation(1, 0, 0)
    matrix = _rotation_matrix_reprs_to_reprs(A_no_z, x_axis)

    return matrix

def _sun_earth_icrf(time):
    """
    Return the Sun-Earth vector for ICRF-based frames.
    """
    sun_pos_icrs = get_body_barycentric('sun', time)
    earth_pos_icrs = get_body_barycentric('earth', time)
    return earth_pos_icrs - sun_pos_icrs

# The Sun's north pole is oriented RA=286.13 deg, dec=63.87 deg in ICRS, and thus HCRS as well
# (See Archinal et al. 2011,
#   "Report of the IAU Working Group on Cartographic Coordinates and Rotational Elements: 2009")
# The orientation of the north pole in ICRS/HCRS is assumed to be constant in time
_SOLAR_NORTH_POLE_HCRS = UnitSphericalRepresentation(lon=constants.get('alpha_0'),
                                                     lat=constants.get('delta_0'))

# Calculate the rotation matrix to de-tilt the Sun's rotation axis to be parallel to the Z axis
_SUN_DETILT_MATRIX = _rotation_matrix_reprs_to_reprs(_SOLAR_NORTH_POLE_HCRS,
                                                     CartesianRepresentation(0, 0, 1))

def _affine_params_hcrs_to_hgs(hcrs_time, hgs_time):
    """
    Return the affine parameters (matrix and offset) from HCRS to HGS

    HGS shares the same origin (the Sun) as HCRS, but has its Z axis aligned with the Sun's
    rotation axis and its X axis aligned with the projection of the Sun-Earth vector onto the Sun's
    equatorial plane (i.e., the component of the Sun-Earth vector perpendicular to the Z axis).
    Thus, the transformation matrix is the product of the matrix to align the Z axis (by de-tilting
    the Sun's rotation axis) and the matrix to align the X axis.  The first matrix is independent
    of time and is pre-computed, while the second matrix depends on the time-varying Sun-Earth
    vector.
    """
    # Determine the Sun-Earth vector in ICRS
    # Since HCRS is ICRS with an origin shift, this is also the Sun-Earth vector in HCRS
    sun_pos_icrs = get_body_barycentric('sun', hgs_time)
    earth_pos_icrs = get_body_barycentric('earth', hgs_time)
    sun_earth = earth_pos_icrs - sun_pos_icrs

    # De-tilt the Sun-Earth vector to the frame with the Sun's rotation axis parallel to the Z axis
    sun_earth_detilt = sun_earth.transform(_SUN_DETILT_MATRIX)

    # Rotate the Sun-Earth vector about the Z axis so that it lies in the XZ plane
    rot_matrix = _rotation_matrix_reprs_to_xz_about_z(sun_earth_detilt)

    total_matrix = rot_matrix @ _SUN_DETILT_MATRIX

    # All of the above is calculated for the HGS observation time
    # If the HCRS observation time is different, calculate the translation in origin
    if not _ignore_sun_motion and np.any(hcrs_time != hgs_time):
        sun_pos_old_icrs = get_body_barycentric('sun', hcrs_time)
        offset_icrf = sun_pos_old_icrs - sun_pos_icrs
    else:
        offset_icrf = sun_pos_icrs * 0  # preserves obstime shape

    offset = offset_icrf.transform(total_matrix)
    return total_matrix, offset

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 HCRS, HeliographicStonyhurst)
@_transformation_debug("HCRS->HGS")
def hcrs_to_hgs(hcrscoord, hgsframe):
    """
    Convert from HCRS to Heliographic Stonyhurst (HGS).

    Even though we calculate the parameters for the affine transform, we use
    ``FunctionTransformWithFiniteDifference`` because otherwise there is no way to account for the
    induced angular velocity when transforming a coordinate with velocity information.
    """
    if hgsframe.obstime is None:
        raise ConvertError("To perform this transformation, the HeliographicStonyhurst"
                           " frame needs a specified `obstime`.")

    rot_matrix, offset = _affine_params_hcrs_to_hgs(hcrscoord.obstime, hgsframe.obstime)

    return hgsframe.realize_frame(hcrscoord.cartesian.transform(rot_matrix) + offset)

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 HeliographicStonyhurst, HCRS)
@_transformation_debug("HGS->HCRS")
def hgs_to_hcrs(hgscoord, hcrsframe):
    """
    Convert from Heliographic Stonyhurst to HCRS.

    Even though we calculate the parameters for the affine transform, we use
    ``FunctionTransformWithFiniteDifference`` because otherwise there is no way to account for the
    induced angular velocity when transforming a coordinate with velocity information.
    """
    if hgscoord.obstime is None:
        raise ConvertError("To perform this transformation, the HeliographicStonyhurst"
                           " frame needs a specified `obstime`.")

    hgscoord = hgscoord.make_3d()

    # Calculate the matrix and offset in the HCRS->HGS direction
    forward_matrix, forward_offset = _affine_params_hcrs_to_hgs(hcrsframe.obstime, hgscoord.obstime)

    # Invert the transformation to get the HGS->HCRS transformation
    reverse_matrix = matrix_transpose(forward_matrix)
    reverse_offset = (-forward_offset).transform(reverse_matrix)

    return hcrsframe.realize_frame(hgscoord.cartesian.transform(reverse_matrix) + reverse_offset)

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 HeliographicStonyhurst, HeliographicStonyhurst)
@_transformation_debug("HGS->HGS")
def hgs_to_hgs(from_coo, to_frame):
    """
    Convert between two Heliographic Stonyhurst frames.
    """
    if to_frame.obstime is None:
        return from_coo.replicate()
    elif _times_are_equal(from_coo.obstime, to_frame.obstime):
        return to_frame.realize_frame(from_coo.data)
    else:
        if _autoapply_diffrot:
            from_coo = from_coo._apply_diffrot((to_frame.obstime - from_coo.obstime).to('day'),
                                               _autoapply_diffrot)
        return from_coo.transform_to(HCRS(obstime=to_frame.obstime)).transform_to(to_frame)

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 HeliographicCarrington, HeliographicCarrington)
@_transformation_debug("HGC->HGC")
def hgc_to_hgc(from_coo, to_frame):
    """
    Convert between two Heliographic Carrington frames.
    """
    if _observers_are_equal(from_coo.observer, to_frame.observer) and \
       _times_are_equal(from_coo.obstime, to_frame.obstime):
        return to_frame.realize_frame(from_coo.data)

    _check_observer_defined(from_coo)
    _check_observer_defined(to_frame)

    # Convert through HGS
    hgscoord = from_coo.transform_to(HeliographicStonyhurst(obstime=from_coo.obstime))

    return hgscoord.transform_to(to_frame)

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 Heliocentric, Heliocentric)
@_transformation_debug("HCC->HCC")
def hcc_to_hcc(from_coo, to_frame):
    """
    Convert between two Heliocentric frames.
    """
    if _observers_are_equal(from_coo.observer, to_frame.observer) and \
       _times_are_equal(from_coo.obstime, to_frame.obstime):
        return to_frame.realize_frame(from_coo.data)

    _check_observer_defined(from_coo)
    _check_observer_defined(to_frame)

    # Convert through HGS
    hgscoord = from_coo.transform_to(HeliographicStonyhurst(obstime=to_frame.obstime))

    return hgscoord.transform_to(to_frame)

def _rotation_matrix_hme_to_hee(hmeframe):
    """
    Return the rotation matrix from HME to HEE at the same observation time
    """
    # Get the Sun-Earth vector
    sun_earth = HCRS(_sun_earth_icrf(hmeframe.obstime), obstime=hmeframe.obstime)
    sun_earth_hme = sun_earth.transform_to(hmeframe).cartesian

    # Rotate the Sun-Earth vector about the Z axis so that it lies in the XZ plane
    rot_matrix = _rotation_matrix_reprs_to_xz_about_z(sun_earth_hme)

    # Tilt the rotated Sun-Earth vector so that it is aligned with the X axis
    tilt_matrix = _rotation_matrix_reprs_to_reprs(sun_earth_hme.transform(rot_matrix),
                                                  CartesianRepresentation(1, 0, 0))

    return matrix_product(tilt_matrix, rot_matrix)

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 HeliocentricMeanEcliptic, HeliocentricEarthEcliptic)
@_transformation_debug("HME->HEE")
def hme_to_hee(hmecoord, heeframe):
    """
    Convert from Heliocentric Mean Ecliptic to Heliocentric Earth Ecliptic
    """
    if heeframe.obstime is None:
        raise ConvertError("To perform this transformation, the coordinate"
                           " frame needs a specified `obstime`.")

    # Convert to the HME frame with mean equinox of date at the HEE obstime, through HCRS
    int_frame = HeliocentricMeanEcliptic(obstime=heeframe.obstime, equinox=heeframe.obstime)
    int_coord = hmecoord.transform_to(HCRS(obstime=hmecoord.obstime)).transform_to(int_frame)

    # Rotate the intermediate coord to the HEE frame
    total_matrix = _rotation_matrix_hme_to_hee(int_frame)
    newrepr = int_coord.cartesian.transform(total_matrix)

    return heeframe.realize_frame(newrepr)

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 HeliocentricEarthEcliptic, HeliocentricMeanEcliptic)
@_transformation_debug("HEE->HME")
def hee_to_hme(heecoord, hmeframe):
    """
    Convert from Heliocentric Earth Ecliptic to Heliocentric Mean Ecliptic
    """
    if heecoord.obstime is None:
        raise ConvertError("To perform this transformation, the coordinate"
                           " frame needs a specified `obstime`.")

    int_frame = HeliocentricMeanEcliptic(obstime=heecoord.obstime, equinox=heecoord.obstime)

    # Rotate the HEE coord to the intermediate frame
    total_matrix = matrix_transpose(_rotation_matrix_hme_to_hee(int_frame))
    int_repr = heecoord.cartesian.transform(total_matrix)
    int_coord = int_frame.realize_frame(int_repr)

    # Convert to the HME frame through HCRS
    return int_coord.transform_to(HCRS(obstime=int_coord.obstime)).transform_to(hmeframe)

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 HeliocentricEarthEcliptic, HeliocentricEarthEcliptic)
@_transformation_debug("HEE->HEE")
def hee_to_hee(from_coo, to_frame):
    """
    Convert between two Heliocentric Earth Ecliptic frames.
    """
    if _times_are_equal(from_coo.obstime, to_frame.obstime):
        return to_frame.realize_frame(from_coo.data)
    elif to_frame.obstime is None:
        return from_coo
    else:
        return from_coo.transform_to(HCRS(obstime=from_coo.obstime)).transform_to(to_frame)

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 HeliocentricEarthEcliptic, GeocentricSolarEcliptic)
@_transformation_debug("HEE->GSE")
def hee_to_gse(heecoord, gseframe):
    """
    Convert from Heliocentric Earth Ecliptic to Geocentric Solar Ecliptic
    """
    # First transform the HEE coord to the GSE obstime
    int_coord = _transform_obstime(heecoord, gseframe.obstime)

    if int_coord.obstime is None:
        raise ConvertError("To perform this transformation, the coordinate"
                           " frame needs a specified `obstime`.")

    # Import here to avoid a circular import
    from .sun import earth_distance

    # Find the Earth-object vector in the intermediate frame
    sun_earth_int = earth_distance(int_coord.obstime) * CartesianRepresentation(1, 0, 0)
    earth_object_int = int_coord.cartesian - sun_earth_int

    # Flip the vector in X and Y, but leave Z untouched
    # (The additional transpose operations are to handle both scalar and array inputs)
    newrepr = CartesianRepresentation((earth_object_int.xyz.T * [-1, -1, 1]).T)

    return gseframe._replicate(newrepr, obstime=int_coord.obstime)

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 GeocentricSolarEcliptic, HeliocentricEarthEcliptic)
@_transformation_debug("GSE->HEE")
def gse_to_hee(gsecoord, heeframe):
    """
    Convert from Geocentric Solar Ecliptic to Heliocentric Earth Ecliptic
    """
    # First transform the GSE coord to the HEE obstime
    int_coord = _transform_obstime(gsecoord, heeframe.obstime)

    if int_coord.obstime is None:
        raise ConvertError("To perform this transformation, the coordinate"
                           " frame needs a specified `obstime`.")

    # Import here to avoid a circular import
    from .sun import earth_distance

    # Find the Sun-object vector in the intermediate frame
    earth_sun_int = earth_distance(int_coord.obstime) * CartesianRepresentation(1, 0, 0)
    sun_object_int = int_coord.cartesian - earth_sun_int

    # Flip the vector in X and Y, but leave Z untouched
    # (The additional transpose operations are to handle both scalar and array inputs)
    newrepr = CartesianRepresentation((sun_object_int.xyz.T * [-1, -1, 1]).T)

    return heeframe._replicate(newrepr, obstime=int_coord.obstime)

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 GeocentricSolarEcliptic, GeocentricSolarEcliptic)
@_transformation_debug("GSE->GSE")
def gse_to_gse(from_coo, to_frame):
    """
    Convert between two Geocentric Solar Ecliptic frames.
    """
    if _times_are_equal(from_coo.obstime, to_frame.obstime):
        return to_frame.realize_frame(from_coo.data)
    else:
        heecoord = from_coo.transform_to(HeliocentricEarthEcliptic(obstime=from_coo.obstime))
        return heecoord.transform_to(to_frame)

def _rotation_matrix_hgs_to_hci(obstime):
    """
    Return the rotation matrix from HGS to HCI at the same observation time
    """
    z_axis = CartesianRepresentation(0, 0, 1)*u.m
    if not obstime.isscalar:
        z_axis = z_axis._apply('repeat', obstime.size)

    # Get the ecliptic pole in HGS
    ecliptic_pole = HeliocentricMeanEcliptic(z_axis, obstime=obstime, equinox=_J2000)
    ecliptic_pole_hgs = ecliptic_pole.transform_to(HeliographicStonyhurst(obstime=obstime))

    # Rotate the ecliptic pole to the -YZ plane, which aligns the solar ascending node with the X
    # axis
    rot_matrix = _rotation_matrix_reprs_to_xz_about_z(ecliptic_pole_hgs.cartesian)
    xz_to_yz_matrix = rotation_matrix(-90*u.deg, 'z')

    return xz_to_yz_matrix @ rot_matrix

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 HeliographicStonyhurst, HeliocentricInertial)
@_transformation_debug("HGS->HCI")
def hgs_to_hci(hgscoord, hciframe):
    """
    Convert from Heliographic Stonyhurst to Heliocentric Inertial
    """

    hgscoord = hgscoord.make_3d()

    # First transform the HGS coord to the HCI obstime
    int_coord = _transform_obstime(hgscoord, hciframe.obstime)

    if int_coord.obstime is None:
        raise ConvertError("To perform this transformation, the coordinate"
                           " frame needs a specified `obstime`.")

    # Rotate from HGS to HCI
    total_matrix = _rotation_matrix_hgs_to_hci(int_coord.obstime)
    newrepr = int_coord.cartesian.transform(total_matrix)

    return hciframe._replicate(newrepr, obstime=int_coord.obstime)

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 HeliocentricInertial, HeliographicStonyhurst)
@_transformation_debug("HCI->HGS")
def hci_to_hgs(hcicoord, hgsframe):
    """
    Convert from Heliocentric Inertial to Heliographic Stonyhurst
    """
    # First transform the HCI coord to the HGS obstime
    int_coord = _transform_obstime(hcicoord, hgsframe.obstime)

    if int_coord.obstime is None:
        raise ConvertError("To perform this transformation, the coordinate"
                           " frame needs a specified `obstime`.")

    # Rotate from HCI to HGS
    total_matrix = matrix_transpose(_rotation_matrix_hgs_to_hci(int_coord.obstime))
    newrepr = int_coord.cartesian.transform(total_matrix)

    return hgsframe._replicate(newrepr, obstime=int_coord.obstime)

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 HeliocentricInertial, HeliocentricInertial)
@_transformation_debug("HCI->HCI")
def hci_to_hci(from_coo, to_frame):
    """
    Convert between two Heliocentric Inertial frames.
    """
    if _times_are_equal(from_coo.obstime, to_frame.obstime):
        return to_frame.realize_frame(from_coo.data)
    else:
        return from_coo.transform_to(HeliographicStonyhurst(obstime=from_coo.obstime)).\
            transform_to(to_frame)

def _rotation_matrix_obliquity(time):
    """
    Return the rotation matrix from Earth equatorial to ecliptic coordinates
    """
    return rotation_matrix(erfa.obl06(*get_jd12(time, 'tt'))*u.radian, 'x')

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 HeliocentricMeanEcliptic, GeocentricEarthEquatorial)
@_transformation_debug("HME->GEI")
def hme_to_gei(hmecoord, geiframe):
    """
    Convert from Heliocentric Mean Ecliptic to Geocentric Earth Equatorial
    """
    if geiframe.obstime is None:
        raise ConvertError("To perform this transformation, the coordinate"
                           " frame needs a specified `obstime`.")

    # Use an intermediate frame of HME at the GEI observation time, through HCRS
    int_frame = HeliocentricMeanEcliptic(obstime=geiframe.obstime, equinox=geiframe.equinox)
    int_coord = hmecoord.transform_to(HCRS(obstime=int_frame.obstime)).transform_to(int_frame)

    # Get the Sun-Earth vector in the intermediate frame
    sun_earth = HCRS(_sun_earth_icrf(int_frame.obstime), obstime=int_frame.obstime)
    sun_earth_int = sun_earth.transform_to(int_frame).cartesian

    # Find the Earth-object vector in the intermediate frame
    earth_object_int = int_coord.cartesian - sun_earth_int

    # Rotate from ecliptic to Earth equatorial
    rot_matrix = matrix_transpose(_rotation_matrix_obliquity(int_frame.equinox))
    newrepr = earth_object_int.transform(rot_matrix)

    return geiframe.realize_frame(newrepr)

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 GeocentricEarthEquatorial, HeliocentricMeanEcliptic)
@_transformation_debug("GEI->HME")
def gei_to_hme(geicoord, hmeframe):
    """
    Convert from Geocentric Earth Equatorial to Heliocentric Mean Ecliptic
    """
    if geicoord.obstime is None:
        raise ConvertError("To perform this transformation, the coordinate"
                           " frame needs a specified `obstime`.")

    # Use an intermediate frame of HME at the GEI observation time
    int_frame = HeliocentricMeanEcliptic(obstime=geicoord.obstime, equinox=geicoord.equinox)

    # Get the Sun-Earth vector in the intermediate frame
    sun_earth = HCRS(_sun_earth_icrf(int_frame.obstime), obstime=int_frame.obstime)
    sun_earth_int = sun_earth.transform_to(int_frame).cartesian

    # Rotate from Earth equatorial to ecliptic
    rot_matrix = _rotation_matrix_obliquity(int_frame.equinox)
    earth_object_int = geicoord.cartesian.transform(rot_matrix)

    # Find the Sun-object vector in the intermediate frame
    sun_object_int = sun_earth_int + earth_object_int
    int_coord = int_frame.realize_frame(sun_object_int)

    # Convert to the final frame through HCRS
    return int_coord.transform_to(HCRS(obstime=int_coord.obstime)).transform_to(hmeframe)

@frame_transform_graph.transform(FunctionTransformWithFiniteDifference,
                                 GeocentricEarthEquatorial, GeocentricEarthEquatorial)
@_transformation_debug("GEI->GEI")
def gei_to_gei(from_coo, to_frame):
    """
    Convert between two Geocentric Earth Equatorial frames.
    """
    if _times_are_equal(from_coo.equinox, to_frame.equinox) and \
       _times_are_equal(from_coo.obstime, to_frame.obstime):
        return to_frame.realize_frame(from_coo.data)
    else:
        return from_coo.transform_to(HCRS(obstime=from_coo.obstime)).transform_to(to_frame)

def _make_sunpy_graph():
    """
    Culls down the full transformation graph for SunPy purposes and returns the string version
    """
    # Frames to keep in the transformation graph
    keep_list = ['icrs', 'hcrs', 'heliocentrictrueecliptic', 'heliocentricmeanecliptic',
                 'heliographic_stonyhurst', 'heliographic_carrington',
                 'heliocentric', 'helioprojective',
                 'heliocentricearthecliptic', 'geocentricsolarecliptic',
                 'heliocentricinertial', 'geocentricearthequatorial',
                 'gcrs', 'precessedgeocentric', 'geocentrictrueecliptic', 'geocentricmeanecliptic',
                 'cirs', 'altaz', 'itrs']

    small_graph = deepcopy(frame_transform_graph)
    cull_list = [name for name in small_graph.get_names() if name not in keep_list]
    cull_frames = [small_graph.lookup_name(name) for name in cull_list]

    for frame in cull_frames:
        # Remove the part of the graph where the unwanted frame is the source frame
        if frame in small_graph._graph:
            del small_graph._graph[frame]

        # Remove all instances of the unwanted frame as the destination frame
        for entry in small_graph._graph:
            if frame in small_graph._graph[entry]:
                del (small_graph._graph[entry])[frame]

    # Clean up the node list
    for name in cull_list:
        small_graph._cached_names.pop(name)

    _add_astropy_node(small_graph)

    docstr = make_transform_graph_docs(small_graph)

    # Make adjustments to the graph
    docstr = _tweak_graph(docstr)

    return docstr

def _add_astropy_node(graph):
    """
    Add an 'Astropy' node that links to an ICRS node in the graph
    """
    class Astropy(BaseCoordinateFrame):
        name = "REPLACE"

    @graph.transform(FunctionTransform, Astropy, ICRS)
    def fake_transform1():
        pass

    @graph.transform(FunctionTransform, ICRS, Astropy)
    def fake_transform2():
        pass

def _tweak_graph(docstr):
    # Remove Astropy's diagram description
    output = docstr[docstr.find('.. Wrap the graph'):]

    # Change the Astropy node
    output = output.replace('Astropy [shape=oval label="Astropy\\n`REPLACE`"]',
                            'Astropy [shape=box3d style=filled fillcolor=lightcyan '
                            'label="Other frames\\nin Astropy"]')

    # Change the Astropy<->ICRS links to black
    output = output.replace('ICRS -> Astropy[  color = "#783001" ]',
                            'ICRS -> Astropy[  color = "#000000" ]')
    output = output.replace('Astropy -> ICRS[  color = "#783001" ]',
                            'Astropy -> ICRS[  color = "#000000" ]')

    # Set the nodes to be filled and cyan by default
    output = output.replace('AstropyCoordinateTransformGraph {',
                            'AstropyCoordinateTransformGraph {\n'
                            '        node [style=filled fillcolor=lightcyan]')

    # Set the nodes for SunPy frames to be white
    sunpy_frames = ['HeliographicStonyhurst', 'HeliographicCarrington',
                    'Heliocentric', 'Helioprojective',
                    'HeliocentricEarthEcliptic', 'GeocentricSolarEcliptic',
                    'HeliocentricInertial', 'GeocentricEarthEquatorial']
    for frame in sunpy_frames:
        output = output.replace(frame + ' [', frame + ' [fillcolor=white ')

    # Set the rank direction to be left->right (as opposed to top->bottom)
    # Force nodes for ICRS, HCRS, and "Other frames in Astropy" to be at the same rank
    output = output.replace('        overlap=false',
                            '        overlap=false\n'
                            '        rankdir=LR\n'
                            '        {rank=same; ICRS; HCRS; Astropy}')

    output = output.replace('<ul>\n\n',
                            '<ul>\n\n' +
                            _add_legend_row('SunPy frames', 'white') +
                            _add_legend_row('Astropy frames', 'lightcyan'))

    return output

def _add_legend_row(label, color):
    row = '        <li style="list-style: none;">\n'\
          '            <p style="font-size: 12px;line-height: 24px;font-weight: normal;'\
          'color: #848484;padding: 0;margin: 0;">\n'\
          '                <b>' + label + ':</b>\n'\
          '                    <span class="dot" style="height: 20px;width: 40px;'\
          'background-color: ' + color + ';border-radius: 50%;border: 1px solid black;'\
          'display: inline-block;"></span>\n'\
          '            </p>\n'\
          '        </li>\n\n\n'
    return row

import numpy,import astropy,from astropy


# ============================================================================
# 付録 I 電気ヒートポンプ・ガス瞬間式併用型給湯温水暖房機
#    （給湯熱源：電気ヒートポンプ・ガス瞬間式併用、暖房熱源：電気ヒートポンプ・ガス瞬間式併用）
# ============================================================================

import numpy as np

# ============================================================================
# I.2 消費電力量
# ============================================================================

def calc_E_E_hs_d_t(L_HWH, hybrid_category, theta_ex_d_Ave_d, L_dashdash_k_d_t, L_dashdash_s_d_t, L_dashdash_w_d_t,
                    L_dashdash_b2_d_t,
                    L_dashdash_ba2_d_t):
    """# 1時間当たりの給湯機の消費電力量 (1)

    Args:
      L_HWH(ndarray): 1日当たりの温水暖房の熱負荷 (MJ/d)
      hybrid_category(str): 電気ヒートポンプ・ガス瞬間式併用型給湯温水暖房機の区分
      theta_ex_d_Ave_d(ndarray): 日平均外気温度 (℃)
      L_dashdash_k_d_t(ndarray): 1時間当たりの台所水栓における太陽熱補正給湯熱負荷 (MJ/h)
      L_dashdash_s_d_t(ndarray): 1時間当たりの浴室シャワー水栓における太陽熱補正給湯熱負荷 (MJ/h)
      L_dashdash_w_d_t(ndarray): 1時間当たりの洗面水栓における太陽熱補正給湯熱負荷 (MJ/h)
      L_dashdash_b2_d_t(ndarray): 1時間当たりの浴槽追焚時における太陽熱補正給湯熱負荷 (MJ/h)
      L_dashdash_ba2_d_t(ndarray): 1時間当たりの浴槽追焚時における太陽熱補正給湯熱負荷 (MJ/h)

    Returns:
      ndarray: 1時間当たりの給湯機の消費電力量 (kWh/h)

    """
    # 1日当たりの太陽熱補正給湯熱負荷
    L_dashdash_k_d = get_L_dashdash_k_d(L_dashdash_k_d_t)
    L_dashdash_s_d = get_L_dashdash_s_d(L_dashdash_s_d_t)
    L_dashdash_w_d = get_L_dashdash_w_d(L_dashdash_w_d_t)
    L_dashdash_b2_d = get_L_dashdash_b2_d(L_dashdash_b2_d_t)
    L_dashdash_ba2_d = get_L_dashdash_ba2_d(L_dashdash_ba2_d_t)

    E_E_hs_d = calc_E_E_hs_d(L_HWH, hybrid_category, theta_ex_d_Ave_d, L_dashdash_k_d, L_dashdash_s_d, L_dashdash_w_d,
                             L_dashdash_b2_d, L_dashdash_ba2_d)

    # 1日当たりの太陽熱補正給湯熱負荷、給湯機の消費電力量の配列要素を1時間ごとに引き延ばす(合計値は24倍になることに注意)
    E_E_hs_d = np.repeat(E_E_hs_d, 24)
    L_dashdash_k_d = np.repeat(L_dashdash_k_d, 24)
    L_dashdash_s_d = np.repeat(L_dashdash_s_d, 24)
    L_dashdash_w_d = np.repeat(L_dashdash_w_d, 24)
    L_dashdash_b2_d = np.repeat(L_dashdash_b2_d, 24)
    L_dashdash_ba2_d = np.repeat(L_dashdash_ba2_d, 24)

    E_E_hs_d_t = np.zeros(24 * 365)

    # (1-1) 太陽熱補正給湯熱負荷が発生しない日 => 24時間で単純分割
    f1 = (L_dashdash_k_d + L_dashdash_s_d + L_dashdash_w_d + L_dashdash_b2_d + L_dashdash_ba2_d == 0)
    E_E_hs_d_t[f1] = E_E_hs_d[f1] / 24

    # (1-2) 太陽熱補正給湯熱負荷が発生する日 => 負荷で按分
    f2 = (L_dashdash_k_d + L_dashdash_s_d + L_dashdash_w_d + L_dashdash_b2_d + L_dashdash_ba2_d > 0)
    E_E_hs_d_t[f2] = E_E_hs_d[f2] * (
            L_dashdash_k_d_t[f2] + L_dashdash_s_d_t[f2] + L_dashdash_w_d_t[f2] + L_dashdash_b2_d_t[f2] +
            L_dashdash_ba2_d_t[f2]) / (
                             L_dashdash_k_d[f2] + L_dashdash_s_d[f2] + L_dashdash_w_d[f2] + L_dashdash_b2_d[f2] +
                             L_dashdash_ba2_d[f2])

    return E_E_hs_d_t

def calc_E_E_hs_d(L_HWH, hybrid_category, theta_ex_d_Ave_d, L_dashdash_k_d, L_dashdash_s_d, L_dashdash_w_d,
                  L_dashdash_b2_d,
                  L_dashdash_ba2_d):
    """# 1日当たりの給湯機の消費電力量 (2)

    Args:
      L_HWH(ndarray): 1日当たりの温水暖房の熱負荷 (MJ/d)
      hybrid_category(str): 電気ヒートポンプ・ガス瞬間式併用型給湯温水暖房機の区分
      theta_ex_d_Ave_d(ndarray): 日平均外気温度 (℃)
      L_dashdash_k_d(ndarray): 1日当たりの台所水栓における太陽熱補正給湯熱負荷 (MJ/d)
      L_dashdash_s_d(ndarray): 1日当たりの浴室シャワー水栓における太陽熱補正給湯熱負荷 (MJ/d)
      L_dashdash_w_d(ndarray): 1日当たりの洗面水栓における太陽熱補正給湯熱負荷 (MJ/d)
      L_dashdash_b2_d(ndarray): 1日当たりの浴槽追焚時における太陽熱補正給湯熱負荷 (MJ/d)
      L_dashdash_ba2_d(ndarray): 1日当たりの浴槽追焚時における太陽熱補正給湯熱負荷 (MJ/d)

    Returns:
      ndarray: 1日当たりの給湯機の消費電力量 (kWh/d)

    """
    # 係数
    a_1, a_2, a_3, a_4 = get_coeff_a(L_HWH, hybrid_category)

    # デフロスト係数
    C_E_def_d = get_C_E_def_d(theta_ex_d_Ave_d)

    E_E_hs_d = ((a_1 * theta_ex_d_Ave_d + a_2 * (
            L_dashdash_k_d + L_dashdash_s_d + L_dashdash_w_d + L_dashdash_b2_d) + a_3 * L_HWH + a_4) * C_E_def_d
                + (0.01723 * L_dashdash_ba2_d + 0.06099)) * 10 ** 3 / 3600

    return E_E_hs_d

def get_coeff_a(L_HWH, hybrid_category):
    """# 係数a_1, a_2, a_3, a_4

    Args:
      L_HWH(ndarray): 1日当たりの温水暖房の熱負荷 (MJ/d)
      hybrid_category(str): 電気ヒートポンプ・ガス瞬間式併用型給湯温水暖房機の区分

    Returns:
      tuple: 係数a_1, a_2, a_3, a_4

    """

    a_1 = np.zeros(365)
    a_2 = np.zeros(365)
    a_3 = np.zeros(365)
    a_4 = np.zeros(365)
    if hybrid_category == '区分1':
        a_1[L_HWH > 0] = get_table_i_3()[0][0]
        a_1[L_HWH == 0] = get_table_i_3()[0][2]
        a_2[L_HWH > 0] = get_table_i_3()[1][0]
        a_2[L_HWH == 0] = get_table_i_3()[1][2]
        a_3[L_HWH > 0] = get_table_i_3()[2][0]
        a_3[L_HWH == 0] = get_table_i_3()[2][2]
        a_4[L_HWH > 0] = get_table_i_3()[3][0]
        a_4[L_HWH == 0] = get_table_i_3()[3][2]
    elif hybrid_category == '区分2':
        a_1[L_HWH > 0] = get_table_i_3()[0][1]
        a_1[L_HWH == 0] = get_table_i_3()[0][3]
        a_2[L_HWH > 0] = get_table_i_3()[1][1]
        a_2[L_HWH == 0] = get_table_i_3()[1][3]
        a_3[L_HWH > 0] = get_table_i_3()[2][1]
        a_3[L_HWH == 0] = get_table_i_3()[2][3]
        a_4[L_HWH > 0] = get_table_i_3()[3][1]
        a_4[L_HWH == 0] = get_table_i_3()[3][3]
    else:
        raise ValueError(hybrid_category)
    return a_1, a_2, a_3, a_4

def get_table_i_3():
    """表I.3 式(1)における係数

    Args:

    Returns:
      list: 表I.3 式(1)における係数

    """
    # 表I.3 式(1)における係数
    table_i_3 = [
        (-0.51375, -0.57722, -0.18114, -0.30429),
        (-0.01782, 0.03865, 0.10483, 0.08497),
        (0.27640, 0.18173, 0.0, 0.0),
        (9.40671, 15.30711, 5.85285, 10.66158)
    ]
    return table_i_3

def get_C_E_def_d(theta_ex_d_Ave_d):
    """1日当たりのデフロスト運転による消費電力量の補正係数 (3)

    Args:
      theta_ex_d_Ave_d: 日平均外気温度 (℃)

    Returns:
      ndarray: 1日当たりのデフロスト運転による消費電力量の補正係数 (3)

    """
    C_E_def_d = np.ones(365)

    f = theta_ex_d_Ave_d < 7
    C_E_def_d[f] = 1 + (7 - theta_ex_d_Ave_d[f]) * 0.0091

    return C_E_def_d

# ============================================================================
# I.3 ガス消費量
# ============================================================================

def calc_E_G_hs_d_t(L_HWH, hybrid_category, Theta_ex_Ave, L_dashdash_k_d_t, L_dashdash_s_d_t, L_dashdash_w_d_t, L_dashdash_b2_d_t,
                   L_dashdash_ba2_d_t):
    """# 1時間当たりの給湯機のガス消費量 (4)

    Args:
      L_HWH(ndarray): 1日当たりの温水暖房の熱負荷 (MJ/d)
      hybrid_category(str): 電気ヒートポンプ・ガス瞬間式併用型給湯温水暖房機の区分
      Theta_ex_Nave: 夜間平均外気温 (℃)
      L_dashdash_k_d_t(ndarray): 1時間当たりの台所水栓における節湯補正給湯熱負荷 (MJ/hd)
      L_dashdash_s_d_t(ndarray): 1時間当たりの浴室シャワー水栓における節湯補正給湯熱負荷 (MJ/hd)
      L_dashdash_w_d_t(ndarray): 1時間当たりの洗面水栓における節湯補正給湯熱負荷 (MJ/h)
      L_dashdash_b2_d_t(ndarray): 1時間当たりの浴槽追焚時における節湯補正給湯熱負荷 (MJ/h)
      L_dashdash_ba2_d_t(ndarray): 1時間当たりの浴槽追焚時における節湯補正給湯熱負荷 (MJ/h)
      Theta_ex_Ave: returns: 1時間当たりの給湯機のガス消費量  (MJ/h)

    Returns:
      ndarray: 1時間当たりの給湯機のガス消費量  (MJ/h)

    """

    # 1日当たりの太陽熱補正給湯熱負荷
    L_dashdash_k_d = get_L_dashdash_k_d(L_dashdash_k_d_t)
    L_dashdash_s_d = get_L_dashdash_s_d(L_dashdash_s_d_t)
    L_dashdash_w_d = get_L_dashdash_w_d(L_dashdash_w_d_t)
    L_dashdash_b2_d = get_L_dashdash_b2_d(L_dashdash_b2_d_t)
    L_dashdash_ba2_d = get_L_dashdash_ba2_d(L_dashdash_ba2_d_t)

    # 1日当たりの給湯機のガス消費量 (5)
    E_G_hs_d = calc_E_G_hs_d(L_HWH, hybrid_category, Theta_ex_Ave, L_dashdash_k_d, L_dashdash_s_d, L_dashdash_w_d, L_dashdash_b2_d,
               L_dashdash_ba2_d)

    # 1日当たりの太陽熱補正給湯熱負荷、給湯機のガス消費量の配列要素を1時間ごとに引き延ばす(合計値は24倍になることに注意)
    E_G_hs_d = np.repeat(E_G_hs_d, 24)
    L_dashdash_k_d = np.repeat(L_dashdash_k_d, 24)
    L_dashdash_s_d = np.repeat(L_dashdash_s_d, 24)
    L_dashdash_w_d = np.repeat(L_dashdash_w_d, 24)
    L_dashdash_b2_d = np.repeat(L_dashdash_b2_d, 24)
    L_dashdash_ba2_d = np.repeat(L_dashdash_ba2_d, 24)

    E_G_hs_d_t = np.zeros(24 * 365)

    # (4-1) 太陽熱補正給湯熱負荷が発生しない日 => 24時間で単純分割
    f1 = (L_dashdash_k_d + L_dashdash_s_d + L_dashdash_w_d + L_dashdash_b2_d + L_dashdash_ba2_d == 0)
    E_G_hs_d_t[f1] = E_G_hs_d[f1] / 24

    # (4-2) 太陽熱補正給湯熱負荷が発生する日 => 負荷で按分
    f2 = (L_dashdash_k_d + L_dashdash_s_d + L_dashdash_w_d + L_dashdash_b2_d + L_dashdash_ba2_d > 0)
    E_G_hs_d_t[f2] = E_G_hs_d[f2] * (
            L_dashdash_k_d_t[f2] + L_dashdash_s_d_t[f2] + L_dashdash_w_d_t[f2] + L_dashdash_b2_d_t[f2] +
            L_dashdash_ba2_d_t[f2]) / (
                             L_dashdash_k_d[f2] + L_dashdash_s_d[f2] + L_dashdash_w_d[f2] + L_dashdash_b2_d[f2] +
                             L_dashdash_ba2_d[f2])

    return E_G_hs_d_t

def calc_E_G_hs_d(L_HWH, hybrid_category, Theta_ex_Ave, L_dashdash_k_d, L_dashdash_s_d, L_dashdash_w_d, L_dashdash_b2_d,
               L_dashdash_ba2_d):
    """# 1日当たりの給湯機のガス消費量 (5)

    Args:
      L_HWH(ndarray): 1日当たりの温水暖房の熱負荷 (MJ/d)
      hybrid_category(str): 電気ヒートポンプ・ガス瞬間式併用型給湯温水暖房機の区分
      Theta_ex_Ave(ndarray): 夜間平均外気温 (℃)
      L_dashdash_k_d(ndarray): 1日当たりの台所水栓における節湯補正給湯熱負荷 (MJ/d)
      L_dashdash_s_d(ndarray): 1日当たりの浴室シャワー水栓における節湯補正給湯熱負荷 (MJ/d)
      L_dashdash_w_d(ndarray): 1日当たりの洗面水栓における節湯補正給湯熱負荷 (MJ/d)
      L_dashdash_b2_d(ndarray): 1日当たりの浴槽追焚時における節湯補正給湯熱負荷 (MJ/d)
      L_dashdash_ba2_d(ndarray): 1日当たりの浴槽追焚時における節湯補正給湯熱負荷 (MJ/d)

    Returns:
      ndarray: 1日当たりの給湯機のガス消費量  (MJ/d)

    """
    # 係数
    b_1, b_2, b_3, b_4 = get_coeff_b(L_HWH, hybrid_category)

    # デフロスト係数
    C_G_def_d = get_C_G_def_d(Theta_ex_Ave)

    # 浴槽追焚時における日平均給湯機効率
    e_ba2 = get_e_ba2_d(Theta_ex_Ave, L_dashdash_ba2_d)

    return ((b_1 * Theta_ex_Ave + b_2 * (
            L_dashdash_k_d + L_dashdash_s_d + L_dashdash_w_d + L_dashdash_b2_d) + b_3 * L_HWH + b_4) * C_G_def_d + (
                    L_dashdash_ba2_d / e_ba2))

def get_coeff_b(L_HWH, hybrid_category):
    """# 係数b_1, b_2, b_3, b_4

    Args:
      L_HWH(ndarray): 1日当たりの温水暖房の熱負荷 (MJ/d)
      hybrid_category(str): 電気ヒートポンプ・ガス瞬間式併用型給湯温水暖房機の区分

    Returns:
      tuple: 係数b_1, b_2, b_3, b_4

    """

    b_1 = np.zeros(365)
    b_2 = np.zeros(365)
    b_3 = np.zeros(365)
    b_4 = np.zeros(365)
    if hybrid_category == '区分1':
        b_1[L_HWH > 0] = get_table_i_4()[0][0]
        b_1[L_HWH == 0] = get_table_i_4()[0][2]
        b_2[L_HWH > 0] = get_table_i_4()[1][0]
        b_2[L_HWH == 0] = get_table_i_4()[1][2]
        b_3[L_HWH > 0] = get_table_i_4()[2][0]
        b_3[L_HWH == 0] = get_table_i_4()[2][2]
        b_4[L_HWH > 0] = get_table_i_4()[3][0]
        b_4[L_HWH == 0] = get_table_i_4()[3][2]
    elif hybrid_category == '区分2':
        b_1[L_HWH > 0] = get_table_i_4()[0][1]
        b_1[L_HWH == 0] = get_table_i_4()[0][3]
        b_2[L_HWH > 0] = get_table_i_4()[1][1]
        b_2[L_HWH == 0] = get_table_i_4()[1][3]
        b_3[L_HWH > 0] = get_table_i_4()[2][1]
        b_3[L_HWH == 0] = get_table_i_4()[2][3]
        b_4[L_HWH > 0] = get_table_i_4()[3][1]
        b_4[L_HWH == 0] = get_table_i_4()[3][3]
    else:
        raise ValueError(hybrid_category)
    return b_1, b_2, b_3, b_4

def get_table_i_4():
    """表I.4 係数

    Args:

    Returns:
      list: 表I.4 係数

    """
    # 表I.4 係数
    table_i_4 = [
        (-0.19841, -0.5782, -0.05770, 0.14061),
        (1.10632, 0.75066, 0.47525, 0.3227),
        (0.19307, 0.46244, 0.0, 0.0),
        (-10.36669, -12.55999, -6.34593, -13.43567)
    ]
    return table_i_4

def get_e_ba2_d(theta_ex_d_Ave_d, L_dashdash_ba2_d):
    """# 浴槽追焚時における日平均給湯機効率 (6)

    Args:
      theta_ex_d_Ave_d(ndarray): 日平均外気温度 (℃)
      L_dashdash_ba2_d(ndarray): 1日当たりの浴槽追焚時における太陽熱補正給湯負荷 (MJ/d)

    Returns:
      ndarray: 浴槽追焚時における日平均給湯機効率 (6)

    """
    # 係数
    c_1, c_2, c_3 = get_coeff_c()

    e_ba2_d = c_1 * theta_ex_d_Ave_d + c_2 * L_dashdash_ba2_d + c_3

    # 効率が1.0を超えない範囲で
    e_ba2_d = np.clip(e_ba2_d, None, 1)

    return e_ba2_d

def get_coeff_c():
    """表I.5 係数

    Args:

    Returns:
      tuple: 表I.5 係数

    """
    # 表I.5 係数
    table_i_5 = (0.0048, 0.0060, 0.7544)
    return table_i_5

def get_C_G_def_d(theta_ex_d_Ave_d):
    """# 1日当たりのデフロスト運転によるガス消費量の補正係数 (7)

    Args:
      theta_ex_d_Ave_d(ndarray): 日平均外気温度 (℃)

    Returns:
      ndarray: 1日当たりのデフロスト運転によるガス消費量の補正係数 (7)

    """
    C_G_def = np.ones(365)

    f = theta_ex_d_Ave_d < 7
    C_G_def[f] = 1 + (7 - theta_ex_d_Ave_d[f]) * 0.0205

    return C_G_def

# ============================================================================
# I.4 灯油消費量
# ============================================================================

def get_E_K_hs_d_t():
    """# 1時間当たりの給湯機の灯油消費量

    Args:

    Returns:
      ndarray: 1時間当たりの給湯機の灯油消費量

    """
    # 1日当たりの給湯機の灯油消費量は0とする
    return np.zeros(24*365)

# ============================================================================
# I.5 温水暖房における熱源機の往き温水温度の候補
# ============================================================================

def get_hotwater_temp_list():
    """# 温水暖房における熱源機の往き温水温度の候補

    Args:

    Returns:
      温水暖房における熱源機の往き温水温度の候補

    """
    return [60, 40]

def get_L_dashdash_k_d(L_dashdash_k_d_t):
    """# 1日当たりの台所水栓における太陽熱補正給湯熱負荷 (MJ/d)

    Args:
      L_dashdash_k_d_t(ndarray): 1時間当たりの台所水栓における太陽熱補正給湯熱負荷 (MJ/h)

    Returns:
      ndarray: 1日当たりの台所水栓における太陽熱補正給湯熱負荷 (MJ/d)

    """
    return np.sum(L_dashdash_k_d_t.reshape((365, 24)), axis=1)

def get_L_dashdash_s_d(L_dashdash_s_d_t):
    """# 1日当たりの浴室シャワー水栓における太陽熱補正給湯負荷 (MJ/d)

    Args:
      L_dashdash_s_d_t(ndarray): 1時間当たりの浴室シャワー水栓における太陽熱補正給湯負荷 (MJ/h)

    Returns:
      ndarray: 1日当たりの浴室シャワー水栓における太陽熱補正給湯負荷 (MJ/d)

    """
    return np.sum(L_dashdash_s_d_t.reshape((365, 24)), axis=1)

def get_L_dashdash_w_d(L_dashdash_w_d_t):
    """# 1日当たりの洗面水栓における太陽熱補正給湯負荷 (MJ/d)

    Args:
      L_dashdash_w_d_t(ndarray): 1時間当たりの洗面水栓における太陽熱補正給湯負荷 (MJ/h)

    Returns:
      ndarray: 1日当たりの洗面水栓における太陽熱補正給湯負荷 (MJ/d)

    """
    return np.sum(L_dashdash_w_d_t.reshape((365, 24)), axis=1)

def get_L_dashdash_b1_d(L_dashdash_b1_d_t):
    """# 1日当たりの浴槽水栓湯はり時における太陽熱補正給湯負荷 (MJ/d)

    Args:
      L_dashdash_b1_d_t: 1時間当たりの浴槽水栓湯はり時における太陽熱補正給湯負荷 (MJ/h)

    Returns:
      ndarray: 1日当たりの浴槽水栓湯はり時における太陽熱補正給湯負荷 (MJ/d)

    """
    return np.sum(L_dashdash_b1_d_t.reshape((365, 24)), axis=1)

def get_L_dashdash_b2_d(L_dashdash_b2_d_t):
    """# 1日当たりの浴槽自動湯はり時における太陽熱補正給湯負荷 (MJ/d)

    Args:
      L_dashdash_b2_d_t(ndarray): 1時間当たりの浴槽自動湯はり時における太陽熱補正給湯負荷 (MJ/h)

    Returns:
      ndarray: 1日当たりの浴槽自動湯はり時における太陽熱補正給湯負荷 (MJ/d)

    """
    return np.sum(L_dashdash_b2_d_t.reshape((365, 24)), axis=1)

def get_L_dashdash_ba1_d(L_dashdash_ba1_d_t):
    """# 1日当たりの浴槽水栓さし湯時における太陽熱補正給湯負荷 (MJ/d)

    Args:
      L_dashdash_ba1_d_t(ndarray): 1時間当たりの浴槽水栓さし湯時における太陽熱補正給湯負荷 (MJ/h)

    Returns:
      ndarray: 1日当たりの浴槽水栓さし湯時における太陽熱補正給湯負荷 (MJ/d)

    """
    return np.sum(L_dashdash_ba1_d_t.reshape((365, 24)), axis=1)

def get_L_dashdash_ba2_d(L_dashdash_ba2_d_t):
    """# 1日当たりの浴槽追焚時における太陽熱補正給湯負荷 (MJ/d)

    Args:
      L_dashdash_ba2_d_t(ndarray): 1時間当たりの浴槽追焚時における太陽熱補正給湯負荷 (MJ/h)

    Returns:
      ndarray: 1日当たりの浴槽追焚時における太陽熱補正給湯負荷 (MJ/d)

    """
    return np.sum(L_dashdash_ba2_d_t.reshape((365, 24)), axis=1)