"""Tools for working with molecular geometry data.
"""


import numpy as np
import molgemtools.constants as constants


def open_xyz(path, encoding="utf-8", sep=" "):
    """Opens a text file containing molecular geometry information in
    `.xyz` format and creates a dictionary.

    Parameters
    ----------
    path : str
        The path of the file to be opened.
    encoding : str, optional
        The character encoding of the text file.
    sep : str, optional
        Separator.

    Returns
    -------
    x_dict : dict
        A dict containing molecular geometry information.

        x_dict['n'] : int
            The number of atoms.
        x_dict['name'] : str
            Comment line.
        x_dict['atoms'] : list of str
            The chemical symbols of the atoms in order.
        x_dict['xyz'] : ndarray
            The 3-by-n matrix of the atomic coordinates.

    See Also
    --------
    molgemtools.geom.read_xyz
    molgemtools.geom.write_xyz
    molgemtools.geom.Geom

    Examples
    --------
    Opening a file.

    >>> import molgemtools.geom as mg
    >>> x_dict = mg.open_xyz('data/CH4.xyz')

    `x_dict` contains the molecular geometry information of the `.xyz`
    file.

    >>> x_dict['n']
    5
    >>> x_dict['name']
    'methane'
    >>> x_dict['atoms']
    ['C', 'H', 'H', 'H', 'H']
    >>> x_dict['xyz']
    array([[-2.09343, -1.02343, -2.4501 , -2.4501 , -2.4501 ],
           [ 1.74059,  1.74059,  2.64313,  0.89901,  1.67963],
           [ 0.     ,  0.     ,  0.45069,  0.55627, -1.00696]])
    """
    file = open(path, encoding=encoding)
    read = file.read().strip()
    file.close()
    rows = read.split("\n")
    data = [rows[0], rows[1]]
    data = data + [row for row in rows[2:] if row.strip != ""]
    x_dict = read_xyz(data, sep=sep)
    return x_dict


def read_xyz(row_list, sep=" "):
    """Creates a dictionary object from a list containing strings
    representing rows of a molecular `.xyz` file.

    Parameters
    ----------
    row_list : list of str
        These strings are representing the rows of the `.xyz` file.
    sep : str, optional
        Separator.

    Returns
    -------
    x_dict : dict
        A dict containing molecular geometry information.

        x_dict['n'] : int
            The number of atoms.
        x_dict['name'] : str
            Comment line.
        x_dict['atoms'] : list of str
            The chemical symbols of the atoms in order.
        x_dict['xyz'] : ndarray
            The 3-by-n matrix of the atomic coordinates.

    See Also
    --------
    molgemtools.geom.open_xyz
    molgemtools.geom.write_xyz
    molgemtools.geom.Geom

    Examples
    --------
    Converting `row_list` to `x_dict`.

    >>> import molgemtools.geom as mg
    >>> row_list = ['5',
    ...             'methane',
    ...             'C  -2.0934300000   1.7405900000   0.0000000000',
    ...             'H  -1.0234300000   1.7405900000   0.0000000000',
    ...             'H  -2.4501000000   2.6431300000   0.4506900000',
    ...             'H  -2.4501000000   0.8990100000   0.5562700000',
    ...             'H  -2.4501000000   1.6796300000  -1.0069600000']
    >>> x_dict = mg.read_xyz(row_list)

    `x_dict` contains the molecular geometry information in a
    dictionary.

    >>> x_dict['n']
    5
    >>> x_dict['name']
    'methane'
    >>> x_dict['atoms']
    ['C', 'H', 'H', 'H', 'H']
    >>> x_dict['xyz']
    array([[-2.09343, -1.02343, -2.4501 , -2.4501 , -2.4501 ],
           [ 1.74059,  1.74059,  2.64313,  0.89901,  1.67963],
           [ 0.     ,  0.     ,  0.45069,  0.55627, -1.00696]])
    """
    n = int(row_list[0])
    x_dict = {"n": n,
              "name": row_list[1].strip(),
              "atoms": [],
              "xyz": []}
    if n != len(row_list) - 2:
        raise ValueError("Invalid data.")
    for i in range(n):
        sep_row = row_list[i + 2].split(sep)
        row = [item for item in sep_row if item.strip() != ""]
        x_dict["atoms"].append(row[0].strip())
        coordinates = [float(j) for j in row[1:]]
        x_dict["xyz"].append(coordinates)
    x_dict["xyz"] = np.transpose(x_dict["xyz"])
    if len(x_dict["xyz"]) != 3:
        raise ValueError("Atoms should have 3 coordinates.")
    return x_dict


def write_xyz(x_dict,
              title="",
              ending="",
              sep=" ",
              c=1,
              prec=10,
              width=5,
              ordering=None):
    """Creates a string that can be saved as a valid `.xyz` file from
    a dictionary which can be generated by `read_xyz()`. Using keyword
    arguments valid Molpro or Gaussian input files can be created.

    Parameters
    ----------
    x_dict : dict
        A dict containing molecular geometry information.

        x_dict['n'] : int
            The number of atoms.
        x_dict['name'] : str
            Comment line.
        x_dict['atoms'] : list of str
            The chemical symbols of the atoms in order.
        x_dict['xyz'] : ndarray
            The 3-by-n matrix of the atomic coordinates.
    title : str, optional
        A string to be appended before the block of coordinates.
    ending : str, optional
        A string to be appended after the block of coordinates.
    sep : str, optional
        Separator.
    c : int or float, optional
        A constant for multiplying the atomic coordinates defined in
        `x_dict['xyz']`. It can be useful for unit conversion,
        otherwise `c` = 1.
    prec : int, optional
        The number of decimals.
    width : int, optional
        The width of the integer parts of the atomic coordinates.
    ordering : None or list of str, optional
        If `ordering` is a list containing strings representing
        chemical symbols, the rows of the atomic coordinates will be
        sorted accordingly.

    Returns
    -------
    string : str

    See Also
    --------
    molgemtools.geom.open_xyz
    molgemtools.geom.read_xyz
    molgemtools.geom.xyz_from_zmat

    Examples
    --------
    Opening the `CH4.xyz` file and converting its contents to `x_dict`
    dictionary representation.

    >>> import molgemtools.geom as mg
    >>> x_dict = mg.open_xyz('data/CH4.xyz')

    Converting `x_dict` to `string`.

    >>> string = mg.write_xyz(x_dict)
    >>> print(string)
    5
    methane
    C    -2.0934300000     1.7405900000     0.0000000000
    H    -1.0234300000     1.7405900000     0.0000000000
    H    -2.4501000000     2.6431300000     0.4506900000
    H    -2.4501000000     0.8990100000     0.5562700000
    H    -2.4501000000     1.6796300000    -1.0069600000

    Creating a Molpro input file from `CH2Cl2.xyz`.

    >>> x_dict = mg.open_xyz('data/CH2Cl2.xyz')
    >>> title = '''***,CH2Cl2_hf_opt
    ... memory,240,m
    ... basis={
    ... default=avdz
    ... }
    ... geomtyp=xyz
    ... geometry={'''
    >>> ending = '''}
    ... {int}
    ... {hf}
    ... {optg}
    ... ---'''
    >>> string = mg.write_xyz(x_dict, title=title, ending=ending)
    >>> print(string)
    ***,CH2Cl2_hf_opt
    memory,240,m
    basis={
    default=avdz
    }
    geomtyp=xyz
    geometry={
    5
    dichloromethane
    C     -0.0933300000    -0.1303400000    -0.0208000000
    Cl     1.6766600000    -0.1303400000    -0.0208000000
    H     -0.4499900000    -0.7661600000     0.7624100000
    H     -0.4500000000    -0.4907200000    -0.9630400000
    Cl    -0.6833300000     1.5175600000     0.2422400000
    }
    {int}
    {hf}
    {optg}
    ---

    Creating a sorted version of `CH2Cl2.xyz`.

    >>> string = mg.write_xyz(x_dict, ordering=['H', 'Cl', 'C'])
    >>> print(string)
    5
    dichloromethane
    H     -0.4499900000    -0.7661600000     0.7624100000
    H     -0.4500000000    -0.4907200000    -0.9630400000
    Cl     1.6766600000    -0.1303400000    -0.0208000000
    Cl    -0.6833300000     1.5175600000     0.2422400000
    C     -0.0933300000    -0.1303400000    -0.0208000000
    """
    n = x_dict["n"]
    atoms = x_dict["atoms"]
    if n != len(atoms):
        raise ValueError("Invalid number of atoms.")
    if len(x_dict["xyz"]) != 3:
        raise ValueError("Atoms should have 3 coordinates.")
    for row in x_dict["xyz"]:
        if n != len(row):
            raise ValueError("x_dict['xyz'] is invalid.")
    xt_array = np.transpose(x_dict["xyz"])
    if ordering is not None:
        temp_list = [[atoms[i], xt_array[i]] for i in range(n)]
        sort_list = sorted(temp_list, key=lambda x: ordering.index(x[0]))
        atoms = [item[0] for item in sort_list]
        xt_array = np.array([item[1] for item in sort_list])
    p = "." + str(prec) + "f"
    block = str(n) + "\n" + x_dict["name"]
    w_atoms = 0
    for atom in atoms:
        if len(atom) > w_atoms:
            w_atoms = len(atom)
    for i in range(n):
        w = w_atoms - len(atoms[i])
        xyz = [format(j*c, p) for j in xt_array[i]]
        for j in range(3):
            spaces = width - len(xyz[j].split(".")[0])
            if spaces < 0:
                raise ValueError("Too low value for width.")
            xyz[j] = " "*spaces + xyz[j]
        block = (block
                 + "\n"
                 + sep.join([atoms[i] + " "*w]
                            + xyz))
    string = "\n".join([title,
                        block,
                        ending])
    string = string.strip()
    return string


def write_zmat(z_dict,
               title="",
               variable_title="",
               ending="",
               sep=" ",
               name_sep="",
               v_sep="",
               indexing_from=1,
               c=1,
               prec=10,
               width=10,
               w_vector=(4, 8, 10, 12)):
    """Creates a string that can be saved as an internal coordinate
    representation of the molecule from a dictionary which can be
    generated by `Geom(x_dict).zmat()`, where `x_dict` is a dictionary
    generated by `write_xyz()`. Using keyword arguments valid Molpro
    or Gaussian input files can be created.

    Parameters
    ----------
    z_dict : dict

        z_dict['n'] : int
            The number of atoms.
        z_dict['atoms'] : list of str
            The chemical symbols of the atoms in order.
        z_dict['indices'] : list of list of int
            A nested list containing indices of atoms. The atomic
            indexing starts from 0. The first column of indices
            corresponds to the indices of atoms themselves in order.
            The second, third and fourth colunms containing the
            indices of atoms relevant for calculating intramolecular
            distances, angles and dihedral angles.
        z_dict['distances'] : array_like
            Intramolecular distances.
        z_dict['angles'] : array_like
            Bond angles. An optional empty array if the number of
            atoms is less than three.
        z_dict['dihedral'] : array_like
            Dihedral angles. An optional empty array if the number of
            atoms is less than four.
    title : str, optional
        A string to be appended before the block of internal
        coordinates.
    variable_title : str, optional
        A string to be appended after the block of internal
        coordinates and before the block of variables.
    ending : str, optional
        A string to be appended after the block of variables.
    sep : str, optional
        The separator in the block of internal coordinates.
    name_sep : str, optional
        The separator in the variable names.
    v_sep : str, optional
        The separator in the block of variables.
    indexing_from : 0 or 1, optional
        The starting value of atomic indexing in the block of internal
        coordinates.
    c : int or float, optional
        A constant for multiplying the atomic coordinates defined in
        `z_dict['distances']`. It can be useful for unit conversion,
        otherwise `c` = 1.
    prec : int, optional
        The number of decimals.
    width : int, optional
        The width of the integer parts of the internal coordinates in
        the block of variables.
    w_vector : list or tuple of int, optional
        Containing the width of columns of atomic indices, distance
        variables, angle variables and dihedral angle variables in the
        block of internal coordinates.

    Returns
    -------
    string : str

    See Also
    --------
    molgemtools.geom.Geom.zmat

    Examples
    --------
    Converting a diatomic molecule to an internal coordinate
    representation. Only a bond length was calculated.

    >>> import molgemtools.geom as mg
    >>> x_dict = mg.open_xyz('data/HCl.xyz')
    >>> z_dict = mg.Geom(x_dict).zmat()
    >>> string = mg.write_zmat(z_dict)
    >>> print(string)
    Cl
    H     1    H2Cl1
    <BLANKLINE>
    H2Cl1         1.3300000000

    Converting a triatomic molecule to an internal coordinate
    representation. Two bond length values and a bond angle were
    calculated.

    >>> x_dict = mg.open_xyz('data/H2O.xyz')
    >>> z_dict = mg.Geom(x_dict).zmat()
    >>> string = mg.write_zmat(z_dict)
    >>> print(string)
    O
    H    1     H2O1
    H    1     H3O1    2     H3O1H2
    <BLANKLINE>
    H2O1           0.9700000000
    H3O1           0.9700038208
    H3O1H2       109.4709320082

    Converting a molecule of four atoms to an internal coordinate
    representation. Bond length values, bond angles and a dihedral
    angle were calculated.

    >>> x_dict = mg.open_xyz('data/NH3.xyz')
    >>> z_dict = mg.Geom(x_dict).zmat()
    >>> string = mg.write_zmat(z_dict)
    >>> print(string)
    N
    H    1     H2N1
    H    1     H3N1    2     H3N1H2
    H    1     H4N1    2     H4N1H2    3     H4N1H2H3
    <BLANKLINE>
    H2N1             1.0200000000
    H3N1             1.0200008044
    H4N1             1.0200017385
    H3N1H2         109.4712046599
    H4N1H2         109.4711861085
    H4N1H2H3      -120.0001118191

    Changing the default `w_vector` value to shrink the block of
    internal coordinates.

    >>> x_dict = mg.open_xyz('data/CH4.xyz')
    >>> z_dict = mg.Geom(x_dict).zmat()
    >>> string = mg.write_zmat(z_dict, w_vector=(2, 6, 8, 10))
    >>> print(string)
    C
    H  1   H2C1
    H  1   H3C1  2   H3C1H2
    H  1   H4C1  2   H4C1H2  3   H4C1H2H3
    H  1   H5C1  2   H5C1H2  3   H5C1H2H3
    <BLANKLINE>
    H2C1             1.0700000000
    H3C1             1.0700062694
    H4C1             1.0700031300
    H5C1             1.0699990898
    H3C1H2         109.4712912610
    H4C1H2         109.4713506963
    H5C1H2         109.4714271861
    H4C1H2H3      -120.0003902274
    H5C1H2H3       119.9999686713

    Creating a Molpro input file to perform a geometry optimization.

    >>> x_dict = mg.open_xyz('data/CH2Cl2.xyz')
    >>> z_dict = mg.Geom(x_dict).zmat()
    >>> title = '''***,CH2Cl2_hf_opt
    ... memory,240,m
    ... basis={
    ... default=avdz
    ... }
    ... symmetry, nosym
    ... geometry={
    ... angstrom;'''
    >>> variable_title = '}'
    >>> ending = '''{int}
    ... {hf}
    ... {optg}
    ... ---'''
    >>> string = mg.write_zmat(z_dict,
    ...                        title=title,
    ...                        variable_title=variable_title,
    ...                        ending=ending,
    ...                        sep=',',
    ...                        v_sep=' =')
    >>> print(string)
    ***,CH2Cl2_hf_opt
    memory,240,m
    basis={
    default=avdz
    }
    symmetry, nosym
    geometry={
    angstrom;
    C
    Cl,   1,   Cl2C1
    H,    1,    H3C1,   2,   H3C1Cl2
    H,    1,    H4C1,   2,   H4C1Cl2,   3,   H4C1Cl2H3
    Cl,   1,   Cl5C1,   2,  Cl5C1Cl2,   3,  Cl5C1Cl2H3
    }
    Cl2C1      =         1.7699900000
    H3C1       =         1.0699959496
    H4C1       =         1.0700016126
    Cl5C1      =         1.7699899581
    H3C1Cl2    =       109.4709186782
    H4C1Cl2    =       109.4713794242
    Cl5C1Cl2   =       109.4713355623
    H4C1Cl2H3  =      -119.9994307889
    Cl5C1Cl2H3 =       120.0010169552
    <BLANKLINE>
    {int}
    {hf}
    {optg}
    ---
    """
    n = z_dict["n"]
    if n == 1:
        raise ValueError("Monatomic molecule.")
    atoms = z_dict["atoms"]
    if n != len(atoms):
        raise ValueError("Invalid number of atoms.")
    if indexing_from not in [0, 1]:
        raise ValueError("indexing_from should be 0 or 1.")
    ind = [[j + indexing_from for j in item]
           for item in z_dict["indices"]]
    distances = z_dict["distances"]
    if n - 1 != len(distances):
        raise ValueError("Invalid number of distances.")
    if len(ind[0]) != 1:
        raise ValueError("z_dict['indices'] is invalid.")
    if len(ind[1]) != 2:
        raise ValueError("z_dict['indices'] is invalid.")
    if n > 2:
        angles = z_dict["angles"]
        if n - 2 != len(angles):
            raise ValueError("Invalid number of angles.")
        if len(ind[2]) != 3:
            raise ValueError("z_dict['indices'] is invalid.")
    if n > 3:
        dihedral = z_dict["dihedral"]
        if n - 3 != len(dihedral):
            raise ValueError("Invalid number of dihedral angles.")
        for i in range(3, n):
            if len(ind[i]) != 4:
                raise ValueError("z_dict['indices'] is invalid.")
    if len(w_vector) != 4:
        raise ValueError("w_vector should contain 4 integers.")
    if c < 0.0:
        raise ValueError("c should be positive.")
    # Lists containing the corresponding distance, angle and dihedral
    # angle variable names.
    r_list = []
    a_list = []
    d_list = []
    # Generating the variable names from the chemical symbols, the
    # optional variable separator and the atomic indices. The indexing
    # starts from indexing_from (0 or 1).
    for i in range(1, n):
        # The first distance variable name.
        if i == 1:
            r_list.append(atoms[ind[i][0] - indexing_from]
                          + str(ind[i][0])
                          + name_sep
                          + atoms[ind[i][1] - indexing_from]
                          + str(ind[i][1]))
        # The second distance variable name and the first angle
        # variable name if the number of atoms is bigger than two.
        if i == 2 and n > 2:
            r_list.append(atoms[ind[i][0] - indexing_from]
                          + str(ind[i][0])
                          + name_sep
                          + atoms[ind[i][1] - indexing_from]
                          + str(ind[i][1]))
            a_list.append(r_list[-1]
                          + name_sep
                          + atoms[ind[i][2] - indexing_from]
                          + str(ind[i][2]))
        # All the other distance and angle variable names and the
        # dihedral angle variable names if the number of atoms is
        # bigger than three.
        if i > 2 and n > 3:
            r_list.append(atoms[ind[i][0] - indexing_from]
                          + str(ind[i][0])
                          + name_sep
                          + atoms[ind[i][1] - indexing_from]
                          + str(ind[i][1]))
            a_list.append(r_list[-1]
                          + name_sep
                          + atoms[ind[i][2] - indexing_from]
                          + str(ind[i][2]))
            d_list.append(a_list[-1]
                          + name_sep
                          + atoms[ind[i][3] - indexing_from]
                          + str(ind[i][3]))
    w_atoms = 0
    for atom in atoms:
        if len(atom) > w_atoms:
            w_atoms = len(atom)
    # Building up the geometry block from the chemical symbols, the
    # atomic indices (indexing from 0 or 1) and the variable names.
    geometry = atoms[0]
    for i in range(1, n):
        w_i = w_vector[0] + w_atoms - len(str(ind[i][1])) - len(atoms[i])
        if w_i < 0:
            raise ValueError("w_vector["
                             + str(0)
                             + "] is too low.")
        w_r = w_vector[1] - len(r_list[i - 1])
        if w_r < 0:
            raise ValueError("w_vector["
                             + str(1)
                             + "] is too low.")
        if i > 1 and n > 2:
            w_j = w_vector[0] - len(str(ind[i][2]))
            if w_j < 0:
                raise ValueError("w_vector["
                                 + str(0)
                                 + "] is too low.")
            w_a = w_vector[2] - len(a_list[i - 2])
            if w_a < 0:
                raise ValueError("w_vector["
                                 + str(2)
                                 + "] is too low.")
        if i > 2 and n > 3:
            w_k = w_vector[0] - len(str(ind[i][3]))
            if w_k < 0:
                raise ValueError("w_vector["
                                 + str(0)
                                 + "] is too low.")
            w_d = w_vector[3] - len(d_list[i - 3])
            if w_d < 0:
                raise ValueError("w_vector["
                                 + str(3)
                                 + "] is too low.")
        if i == 1:
            geometry = (geometry
                        + "\n"
                        + sep.join([atoms[i],
                                    " "*w_i + str(ind[i][1]),
                                    " "*w_r + r_list[i - 1]]))
        if i == 2:
            geometry = (geometry
                        + "\n"
                        + sep.join([atoms[i],
                                    " "*w_i + str(ind[i][1]),
                                    " "*w_r + r_list[i - 1],
                                    " "*w_j + str(ind[i][2]),
                                    " "*w_a + a_list[i - 2]]))
        if i > 2:
            geometry = (geometry
                        + "\n"
                        + sep.join([atoms[i],
                                    " "*w_i + str(ind[i][1]),
                                    " "*w_r + r_list[i - 1],
                                    " "*w_j + str(ind[i][2]),
                                    " "*w_a + a_list[i - 2],
                                    " "*w_k + str(ind[i][3]),
                                    " "*w_d + d_list[i - 3]]))
    # Building up the block of variables from the variable names, the
    # optional separator and the corresponding values of distances,
    # angles and dihedral angles.
    variables = ""
    # w_m is the width of the column of variable names in the block of
    # variables. If the molecule is diatomic, w_m is equal to the
    # length of the distance variable, if triatomic, it is equal to
    # the length of the angle variable. If the number of atoms is
    # bigger than three, w_m is equal to the length of the longest
    # dihedral angle variable name.
    w_m = 0
    if n == 2:
        w_m = len(r_list[0])
    if n == 3:
        w_m = len(a_list[0])
    if n > 3:
        for item in d_list:
            if len(item) > w_m:
                w_m = len(item)
    # Denoting the decimal precision.
    p = "." + str(prec) + "f"
    for i in range(n - 1):
        r = format(distances[i]*c, p)
        w = width - len(r.split(".")[0])
        if w < 0:
            raise ValueError("width is too low.")
        variables = (variables
                     + r_list[i]
                     + " "*(w_m - len(r_list[i]))
                     + v_sep
                     + " "*w
                     + r
                     + "\n")
    if n > 2:
        for i in range(n - 2):
            a = format(angles[i], p)
            w = width - len(a.split(".")[0])
            if w < 0:
                raise ValueError("width is too low.")
            variables = (variables
                         + a_list[i]
                         + " "*(w_m - len(a_list[i]))
                         + v_sep
                         + " "*w
                         + a
                         + "\n")
    if n > 3:
        for i in range(n - 3):
            d = format(dihedral[i], p)
            w = width - len(d.split(".")[0])
            if w < 0:
                raise ValueError("width is too low.")
            variables = (variables
                         + d_list[i]
                         + " "*(w_m - len(d_list[i]))
                         + v_sep
                         + " "*w
                         + d
                         + "\n")
    string = "\n".join([title,
                        geometry,
                        variable_title,
                        variables,
                        ending])
    string = string.strip()
    return string


def xyz_from_zmat(z_dict, z=179.5):
    r"""Converting the internal coordinate representation of a
    molecule to Cartesian coordinates.

    Parameters
    ----------
    z_dict : dict

        z_dict['n'] : int
            The number of atoms.
        z_dict['name'] : str
            Comment line.
        z_dict['atoms'] : list of str
            The chemical symbols of the atoms in order.
        z_dict['indices'] : list of list of int
            A nested list containing indices of atoms. The atomic
            indexing starts from 0. The first column of indices
            corresponds to the indices of atoms themselves in order.
            The second, third and fourth colunms containing the
            indices of atoms relevant for calculating intramolecular
            distances, angles and dihedral angles.
        z_dict['distances'] : array_like
            Intramolecular distances.
        z_dict['angles'] : array_like
            Bond angles. An optional empty array if the number of
            atoms is less than three.
        z_dict['dihedral'] : array_like
            Dihedral angles. An optional empty array if the number of
            atoms is less than four.
    z : int or float, optional
        The maximum acceptable value of the items in z_dict['angles'].
        If the angle between three consecutive atoms is bigger than
        `z`, the atoms are considered collinear. The value of `z`
        should be bigger than 170 and smaller than 180.

    Returns
    -------
    x_dict : dict

        x_dict['n'] : int
            The number of atoms.
        x_dict['name'] : str
            Comment line.
        x_dict['atoms'] : list of str
            The chemical symbols of the atoms in order.
        x_dict['xyz'] : ndarray
            The 3-by-n matrix of the atomic coordinates.

    See Also
    --------
    molgemtools.geom.write_xyz
    molgemtools.geom.Geom.zmat

    Notes
    -----
    The geometric structure of a molecule of `n` atoms can be
    represented by a set of :math:`n-1` atomic distances, :math:`n-2`
    bond angles and :math:`n-3` dihedral angles called internal
    coordinates. The internal coordinate representation can be
    converted to Cartesian coordinates after anchoring the positions
    of the first three atoms. One of the easiest ways is setting the
    position of the middle atom, which is the first or second
    depending on connectivity, to be at the origin, the next atom to
    be on the negative `x`-axis and the third to be lie in the
    `xy`-plane. Anchoring the Cartesian frame if the first atom is
    connected to the second, but not connected to the third:

    .. math::
        \begin{bmatrix}
        \textbf{a}_0 & \textbf{b}_0 & \textbf{c}_0
        \end{bmatrix}
        =\begin{bmatrix}
         -d_0 & 0 & d_1cos(\pi-\Theta_0) \\
            0 & 0 & d_1sin(\pi-\Theta_0) \\
            0 & 0 & 0
         \end{bmatrix}
        =\begin{bmatrix}
         -d_0 & 0 & -d_1cos(\Theta_0) \\
            0 & 0 &  d_1sin(\Theta_0) \\
            0 & 0 &  0
         \end{bmatrix},

    where :math:`\textbf{a}_0`, :math:`\textbf{b}_0` and
    :math:`\textbf{c}_0` are column vectors representing the atomic
    coordinates, :math:`d_0` is the distance of the first two atoms,
    :math:`d_1` is the distance of the second and third atoms and
    :math:`\Theta_0` is the bond angle in radians. If the first atom
    is connected to the second and the third, the equation becomes:

    .. math::
        \begin{bmatrix}
        \textbf{a}_0 & \textbf{b}_0 & \textbf{c}_0
        \end{bmatrix}
        =\begin{bmatrix}
         0 & -d_0 & d_1cos(\pi-\Theta_0) \\
         0 &    0 & d_1sin(\pi-\Theta_0) \\
         0 &    0 & 0
         \end{bmatrix}
        =\begin{bmatrix}
         0 & -d_0 & -d_1cos(\Theta_0) \\
         0 &    0 &  d_1sin(\Theta_0) \\
         0 &    0 &  0
         \end{bmatrix}.

    Using the natural extension reference frame (NeRF) method a set of
    internal coordinates including the
    :math:`\Phi_{\textbf{a},\textbf{b},\textbf{c},\textbf{d}}`
    dihedral angle and the Cartesian coordinates of three
    already defined points can be converted to the coordinates of a
    new point. Let :math:`\textbf{a}`, :math:`\textbf{b}` and
    :math:`\textbf{c}` be these coordinates and let
    :math:`\textbf{q}_i` and :math:`\textbf{q}_j` be the vectors
    between the points:

    .. math::
        \textbf{q}_i=\textbf{b}-\textbf{a},

        \textbf{q}_j=\textbf{c}-\textbf{b}.

    In a coordinate system, where :math:`\textbf{c}` is at the origin,
    :math:`\textbf{b}` is on the negative `x`-axis and
    :math:`\textbf{a}` lies in the `xy`-plane, the local coordinates
    of :math:`\textbf{d}_{loc}`:

    .. math::
        \textbf{d}_{loc}
        =\begin{bmatrix}
         d_{\textbf{c},\textbf{d}}
         cos(\pi-\Theta_{\textbf{b},\textbf{c},\textbf{d}}) \\
         d_{\textbf{c},\textbf{d}}
         sin(\pi-\Theta_{\textbf{b},\textbf{c},\textbf{d}})
         cos(\Phi_{\textbf{a},\textbf{b},\textbf{c},\textbf{d}}) \\
         d_{\textbf{c},\textbf{d}}
         sin(\pi-\Theta_{\textbf{b},\textbf{c},\textbf{d}})
         sin(\Phi_{\textbf{a},\textbf{b},\textbf{c},\textbf{d}})
         \end{bmatrix}
        =\begin{bmatrix}
         -d_{\textbf{c},\textbf{d}}
         cos(\Theta_{\textbf{b},\textbf{c},\textbf{d}}) \\
         d_{\textbf{c},\textbf{d}}
         sin(\Theta_{\textbf{b},\textbf{c},\textbf{d}})
         cos(\Phi_{\textbf{a},\textbf{b},\textbf{c},\textbf{d}}) \\
         d_{\textbf{c},\textbf{d}}
         sin(\Theta_{\textbf{b},\textbf{c},\textbf{d}})
         sin(\Phi_{\textbf{a},\textbf{b},\textbf{c},\textbf{d}})
         \end{bmatrix}.

    Let :math:`\textbf{M}` be a 3-by-3 rotation matrix consists of the
    following column vectors :math:`\textbf{m}_x`,
    :math:`\textbf{m}_z` and :math:`\textbf{m}_y`:

    .. math::
        \textbf{m}_x=\frac{\textbf{q}_j}
                     {\Vert \textbf{q}_j\Vert},

        \textbf{m}_z=\frac{\textbf{q}_i\times \textbf{m}_x}
                     {\Vert \textbf{q}_i\times \textbf{m}_x\Vert},

        \textbf{m}_y=\textbf{m}_z\times \textbf{m}_x,

        \textbf{M}
        =\begin{bmatrix}
         \textbf{m}_x & \textbf{m}_y & \textbf{m}_z
         \end{bmatrix}.

    The final position of :math:`\textbf{d}` in the global Cartesian
    coordinate system can be calculated:

    .. math::
        \textbf{d}=\textbf{M}\textbf{d}_{loc}+\textbf{c}.

    References
    ----------
    .. [Parsons2005] J. Parsons; J. B. Holmes; J. M. Rojas; J. Tsai;
        C. E. Strauss; Practical conversion from torsion space to
        Cartesian space for in silico protein synthesis., Journal of
        computational chemistry, Vol. 26, No. 10, 2005, 1063-1068.

    Examples
    --------
    Calculating the internal coordinate representation of a molecule.

    >>> import molgemtools.geom as mg
    >>> z_dict = mg.Geom(mg.open_xyz('data/CH2Cl2.xyz')).zmat()

    Converting the internal coordinates to Cartesian.

    >>> x_dict = mg.xyz_from_zmat(z_dict)
    >>> string = mg.write_xyz(x_dict)
    >>> print(string)
    5
    dichloromethane
    C      0.0000000000     0.0000000000     0.0000000000
    Cl    -1.7699900000     0.0000000000     0.0000000000
    H      0.3566600000     1.0088037354     0.0000000000
    H      0.3566700000    -0.5043944238    -0.8736568132
    Cl     0.5900000000    -0.8344063271     1.4451749143
    """
    # The number of atoms.
    n = z_dict["n"]
    if n == 1:
        raise ValueError("Monatomic molecule.")
    # Atomic coordinates. Initially an n-by-3 matrix of zeros.
    xyz = np.zeros((n, 3))
    # Array of atomic distances.
    r = z_dict["distances"]
    if min(r) <= 0.0:
        raise ValueError("Atomic distances are should be positive.")
    if not 170.0 < z < 180.0:
        raise ValueError("z should be between 170 and 180.")
    # A degree to radians conversion factor.
    deg_to_rad = np.pi/180.0
    # List of angles in radians.
    t = [i*deg_to_rad for i in z_dict["angles"]]
    # List of dihedral angles in radians.
    f = [i*deg_to_rad for i in z_dict["dihedral"]]
    # Block of indices.
    ind = z_dict["indices"]
    if n != len(ind):
        raise ValueError("Invalid number of rows in z_dict['indices'].")
    if type(z_dict["name"]) != str:
        raise TypeError("z_dict['name'] should be an str.")
    if n != len(z_dict["atoms"]):
        raise ValueError("Invalid number of atoms.")
    for atom in z_dict["atoms"]:
        if type(atom) != str:
            raise TypeError("z_dict['atoms'] should be a list of strings.")
    if n - 1 != len(r):
        raise ValueError("Invalid number of distances.")
    if list(ind[0]) != [0]:
        raise ValueError("z_dict['indices'] is invalid.")
    if list(ind[1]) != [1, 0]:
        raise ValueError("z_dict['indices'] is invalid.")
    if n > 2:
        if n - 2 != len(t):
            raise ValueError("Invalid number of angles.")
        if min(z_dict["angles"]) <= 0.0 or max(z_dict["angles"]) > z:
            raise ValueError("z_dict['angles'] is invalid.")
    if n > 3:
        if n - 3 != len(f):
            raise ValueError("Invalid number of dihedral angles.")
        for i in range(3, n):
            if ind[i][0] != i or max(ind[i]) != i:
                raise ValueError("z_dict['indices'] is invalid.")
            if min(ind[i]) < 0:
                raise IndexError("An atomic index should be positive.")
            if len(ind[i]) != 4:
                raise ValueError("z_dict['indices'] is invalid.")
            for j in range(4):
                for k in range(j + 1, 4):
                    if ind[i][j] == ind[i][k]:
                        raise ValueError("Repeated atomic index.")
    # Calculating the atomic coordinates of the second atom in case of
    # a diatomic molecule.
    if n == 2:
        xyz[1][0] = -r[0]
    # Calculating the Cartesian coordinates of the first three atoms
    # in case of a bigger molecule.
    if n > 2:
        # The first atom is connected to the second and the third.
        if list(ind[2]) == [2, 0, 1]:
            xyz[1][0] = -r[0]
            xyz[2][0] = -r[1]*np.cos(t[0])
            xyz[2][1] = r[1]*np.sin(t[0])
        # The first atom is connected to the second, but not connected
        # to the third.
        elif list(ind[2]) == [2, 1, 0]:
            xyz[0][0] = -r[0]
            xyz[2][0] = -r[1]*np.cos(t[0])
            xyz[2][1] = r[1]*np.sin(t[0])
        else:
            raise ValueError("z_dict['indices'] is invalid.")
    # Calculating the Cartesian coordinates of the leftover atoms
    # using the NeRF algorithm.
    if n > 3:
        for i in range(3, n):
            # 3 dimensional vectors a, b and c representing the atomic
            # coordinates of the corresponding atoms.
            a_array = xyz[ind[i][3]]
            b_array = xyz[ind[i][2]]
            c_array = xyz[ind[i][1]]
            # 3 dimensional vector qi, the difference of b and a.
            # qi = b - a.
            qi = b_array - a_array
            # 3 dimensional vector qj, the difference of c and b.
            # qj = c - b.
            qj = c_array - b_array
            # Vector mx is qj divided by its norm.
            mx = qj/sum(qj**2)**0.5
            # ni is the cross product of qi and mx.
            ni = np.cross(qi, mx)
            # Vector mz is ni divided by its norm.
            mz = ni/sum(ni**2)**0.5
            # Vector my is the cross product of mz and mx.
            my = np.cross(mz, mx)
            # The 3-by-3 matrix M consists of columns mx, my and mz.
            m_array = np.transpose([mx, my, mz])
            # d_loc is the vector of Cartesian coordinates of d in a
            # coordinate system where c is at the origin, b is on the
            # negative x-axis and a is in the xy-plane.
            d_loc = np.array([-r[i - 1]*np.cos(t[i - 2]),
                              r[i - 1]*np.sin(t[i - 2])*np.cos(f[i - 3]),
                              r[i - 1]*np.sin(t[i - 2])*np.sin(f[i - 3])])
            # Calculating the global Cartesian coordinates of atom d.
            # d = M*d_loc + c.
            d_array = np.matmul(m_array, d_loc) + c_array
            xyz[i] = d_array
    x_dict = {"n": n,
              "name": z_dict["name"],
              "atoms": z_dict["atoms"],
              "xyz": np.transpose(xyz)}
    return x_dict


def center(a_list):
    """Moves matrix `a_list` to the center of the 3-dimensional space
    by subtracting the average values of rows from the corresponding
    elements.

    Parameters
    ----------
    a_list : array_like
        A 3-by-n dimensional matrix. Every column of the matrix
        corresponds to coordinates of a given point.

    Returns
    -------
    b_array : ndarray

    Examples
    --------
    >>> import molgemtools.geom as mg
    >>> a_list = [[-2.09343, -1.02343, -2.45010, -2.45010, -2.45010],
    ...           [ 1.74059,  1.74059,  2.64313,  0.89901,  1.67963],
    ...           [ 0.00000,  0.00000,  0.45069,  0.55627, -1.00696]]

    A geometrically centered 3-by-n matrix created from `a_list`.

    >>> b_array = mg.center(a_list)
    >>> b_array
    array([[ 2.00000000e-06,  1.07000200e+00, -3.56668000e-01,
            -3.56668000e-01, -3.56668000e-01],
           [ 2.22044605e-16,  2.22044605e-16,  9.02540000e-01,
            -8.41580000e-01, -6.09600000e-02],
           [ 0.00000000e+00,  0.00000000e+00,  4.50690000e-01,
             5.56270000e-01, -1.00696000e+00]])
    """
    n = len(a_list[0])
    # a_list as a 3-by-n dimensional array representing the matrix of
    # atomic coordinates. The rows of the matrix are the vectors of
    # the x, y and z components of the coordinates.
    a_array = np.array(a_list)
    if len(a_list) != 3:
        raise ValueError("a_list should contain 3 rows.")
    for item in a_array:
        if n != len(item):
            raise ValueError("Mismatching dimensions.")
    # List containing average x, y and z values.
    xyz_list = [sum(a_array[i])/n for i in range(3)]
    # Element-wise subtraction of the average x, y and z values from
    # the corresponding vector of x, y and z coordinates.
    b_array = np.array([a_array[i] - xyz_list[i] for i in range(3)])
    return b_array


def rmsd(a_list, b_list):
    r"""Calculates the root-mean-square deviation (RMSD) of two
    3-dimensional point sets.

    Parameters
    ----------
    a_list, b_list : array_like
        3-by-n matrix representing a 3-dimensional shape in the
        Cartesian coordinate system.

    Returns
    -------
    r : float

    Notes
    -----
    For 3-by-`n` dimensional matrices :math:`\textbf{A}` and
    :math:`\textbf{B}` the root-mean-square deviation can be defined
    as:

    .. math::
        RMSD(\textbf{A},\textbf{B})
        =\sqrt{\frac{1}{n}
               \sum\limits_{i=0}^{2}
               \sum\limits_{j=0}^{n-1}
               (A_{ij}-B_{ij})^2}.

    Examples
    --------
    >>> import molgemtools.geom as mg
    >>> a_list = [[-2.09343, -1.02343, -2.45010, -2.45010, -2.45010],
    ...           [ 1.74059,  1.74059,  2.64313,  0.89901,  1.67963],
    ...           [ 0.00000,  0.00000,  0.45069,  0.55627, -1.00696]]
    >>>
    >>> b_list = [[-4.74175, -3.67175, -5.09842, -5.09842, -5.09842],
    ...           [ 3.68299,  3.68299,  2.67951,  4.27442,  4.09503],
    ...           [ 0.00000,  0.00000, -0.10357, -0.81725,  0.92082]]

    The RMSD value between `a_list` and `b_list`.

    >>> mg.rmsd(a_list, b_list)
    3.6263700867644495
    """
    n = len(a_list[0])
    # 3-by-n dimensional matrix A.
    a_array = np.array(a_list)
    # 3-by-n dimensional matrix B.
    b_array = np.array(b_list)
    if len(a_list) != 3:
        raise ValueError("a_list should contain 3 rows.")
    if len(b_list) != 3:
        raise ValueError("b_list should contain 3 rows.")
    for item in a_array:
        if n != len(item):
            raise ValueError("Mismatching dimensions.")
    for item in b_array:
        if n != len(item):
            raise ValueError("Mismatching dimensions.")
    # The sum of the squared errors of matrix A and B.
    s = sum(sum((a_array - b_array)**2))
    # RMSD, the square root of the average of squared errors.
    r = (s/n)**0.5
    return r


def smsvd(x_list, y_list, imp_rot=False):
    r"""Shape matching using singular value decomposition. The
    function returns a list containing the RMSD value of the two
    optimally aligned point sets and a 3-by-3 matrix representing the
    optimal orthogonal transformation. The function only performs
    proper or improper rotations, therefore `x_list` and `y_list` are
    should be geometrically centered, otherwise a misleading high RMSD
    value will be calculated.

    Parameters
    ----------
    x_list, y_list : array_like
        3-by-n matrix representing a 3-dimensional shape.
    imp_rot : bool, optional
        If improper rotation is allowed during shape matching
        `imp_rot` should be set to `True`, otherwise `False`.

    Returns
    -------
    smsvd_list : list

        smsvd_list[0] : float
            The calculated RMSD value.
        smsvd_list[1] : ndarray
            An ndarray representing the optimal orthogonal
            transformation (3-by-3 matrix).

    See Also
    --------
    molgemtools.geom.center
    molgemtools.geom.rmsd

    Notes
    -----
    This method of shape matching is based upon the Kabsch-Umeyama
    algorithm. Let :math:`\textbf{X}` and :math:`\textbf{Y}` be
    3-by-`n` dimensional matrices representing geometrically centered
    atomic coordinates of two molecules of `n` atoms. The
    :math:`\textbf{S}` cross-covariance matrix can be defined as:
    :math:`\textbf{S}=\textbf{X}\textbf{Y}^T`. The singular value
    decomposition of :math:`\textbf{S}` is:

    .. math::
        \textbf{S}
        =\textbf{U}
         \mathbf{\Sigma}
         \textbf{V}^T.

    Matrices :math:`\textbf{U}` and :math:`\textbf{V}^T` are
    orthogonal and :math:`\mathbf{\Sigma}` is diagonal. The diagonal
    entries of :math:`\mathbf{\Sigma}` are the singular values. By
    convention the singular values are sorted in descending order. If
    improper rotation is allowed matrix :math:`\textbf{R}`
    representing the optimal orthogonal transformation minimizing
    :math:`RMSD(\textbf{RX},\textbf{Y})` can be calculated:

    .. math::
        \textbf{R}
        =\textbf{V}
         \textbf{U}^T.

    If improper rotation is not allowed the equation becomes:

    .. math::
        \textbf{R}
        =\textbf{V}
         \begin{bmatrix}
         1 & 0 & 0 \\
         0 & 1 & 0 \\
         0 & 0 & c
         \end{bmatrix}
         \textbf{U}^T,

    where :math:`c=det(\textbf{V}\textbf{U}^T)`. The value of `c` can
    only be :math:`\pm 1`. :math:`\textbf{S}`, :math:`\textbf{U}`,
    :math:`\mathbf{\Sigma}`, :math:`\textbf{V}` and :math:`\textbf{R}`
    are 3-by-3 dimensional matrices.

    References
    ----------
    .. [Kabsch1976] W. Kabsch; A solution for the best rotation to
        relate two sets of vectors., In Acta Crystallographica, Vol.
        32, 1976, 922-923.

    .. [Umeyama1991] S. Umeyama; Least-squares estimation of
        transformation parameters between two point patterns., IEEE
        Transactions on Pattern Analysis & Machine Intelligence, 4,
        1991, 376-380.

    .. [Lawrence2019] J. Lawrence; J. Bernal; C. Witzgall; A Purely
        Algebraic Justification of the Kabsch-Umeyama Algorithm,
        Journal of Research of the National Institute of Standards and
        Technology, 124, 2019, 1-6.

    .. [Sorkine2017] O. Sorkine-Hornung; M. Rabinovich; Least-squares
        rigid motion using SVD, Zurich, 2017.

    .. [Sehnal2007] D. Sehnal; Algorithms for Comparing Molecule
        Conformations, Bachelor's Thesis, Brno, 2007.

    Examples
    --------
    >>> import molgemtools.geom as mg
    >>> x_list = [[-2.09343, -1.02343, -2.45010, -2.45010, -2.45010],
    ...           [ 1.74059,  1.74059,  2.64313,  0.89901,  1.67963],
    ...           [ 0.00000,  0.00000,  0.45069,  0.55627, -1.00696]]
    >>>
    >>> y_list = [[-4.74175, -3.67175, -5.09842, -5.09842, -5.09842],
    ...           [ 3.68299,  3.68299,  2.67951,  4.27442,  4.09503],
    ...           [ 0.00000,  0.00000, -0.10357, -0.81725,  0.92082]]

    Shape matching between geometrically centered matrices.

    >>> smsvd_list = mg.smsvd(mg.center(x_list), mg.center(y_list))

    The RMSD value.

    >>> smsvd_list[0]
    3.1420355112161644e-06

    The matrix representing the optimal orthogonal transformation.

    >>> smsvd_list[1]
    array([[ 1.00000000e+00,  1.09321571e-06,  4.11850653e-07],
           [ 1.16822153e-06, -9.35795218e-01, -3.52544053e-01],
           [ 1.17583959e-12,  3.52544053e-01, -9.35795218e-01]])
    """
    # 3-by-n matrix X.
    x_array = np.array(x_list)
    # 3-by-n matrix Y.
    y_array = np.array(y_list)
    # n-by-3 matrix YT, the transpose of Y.
    yt_array = np.transpose(y_array)
    # 3-by-3 cross-covariance matrix S. S = X*YT.
    s_array = np.matmul(x_array, yt_array)
    # The singular value decomposition of matrix S. A list containing
    # matrix U, the singular values of S and VT.
    d_list = np.linalg.svd(s_array)
    # 3-by-3 matrix U.
    u_array = d_list[0]
    # 3-by-3 matrix UT, the transpose of U.
    ut_array = np.transpose(u_array)
    # 3-by-3 matrix VT.
    vt_array = d_list[2]
    # 3-by-3 matrix V, the transpose of V.
    v_array = np.transpose(vt_array)
    if imp_rot is False:
        # c is the determinant of the matrix product V*UT. If the
        # optimal orthogonal transformation is a proper rotation,
        # c = 1. If c = -1, this transformation is an improper
        # rotation (rotation and reflection).
        c = np.linalg.det(np.matmul(v_array, ut_array))
        # If we want to exclude the possibility of the improper
        # rotation, we have to introduce matrix C.
        c_array = np.array([[1.0, 0.0, 0.0],
                            [0.0, 1.0, 0.0],
                            [0.0, 0.0,   c]])
        # 3-by-3 matrix representing the optimal proper rotation.
        # R = V*C*UT.
        r_array = np.matmul(np.matmul(v_array, c_array), ut_array)
    elif imp_rot is True:
        # If the reflection is allowed, the optimal orthogonal
        # transformation can be a proper or an improper rotation.
        # R = V*UT.
        r_array = np.matmul(v_array, ut_array)
    else:
        raise TypeError("imp_rot can only be True or False.")
    # 3-by-n matrix Z representing X after the transformation.
    # Z = R*X.
    z_array = np.matmul(r_array, x_array)
    # The RMSD value between Z and Y.
    r = rmsd(z_array, y_array)
    # The list containing the RMSD value and matrix R.
    smsvd_list = [r, r_array]
    return smsvd_list


def rmat(e_list):
    r"""Creates a 3-by-3 rotation matrix.

    Parameters
    ----------
    e_list : array_like
        An array of the three Euler angles in degrees corresponding
        rotations about x-, y- and z-axes.

    Returns
    -------
    r_array : ndarray

    See Also
    --------
    molgemtools.geom.euler

    Notes
    -----
    A rotation of :math:`\Psi` radians about the `x`-axis,
    :math:`\Theta` about the `y`-axis and :math:`\Phi` about the
    `z`-axis can be represented by the following matrices:

    .. math::
        \textbf{R}_x(\Psi)
        =\begin{bmatrix}
         1 &         0 &          0 \\
         0 & cos(\Psi) & -sin(\Psi) \\
         0 & sin(\Psi) &  cos(\Psi)
         \end{bmatrix},

    .. math::
        \textbf{R}_y(\Theta)
        =\begin{bmatrix}
          cos(\Theta) & 0 & cos(\Theta) \\
                    0 & 1 &           0 \\
         -sin(\Theta) & 0 & cos(\Theta)
         \end{bmatrix},

    .. math::
        \textbf{R}_z(\Phi)
        =\begin{bmatrix}
         cos(\Phi) & -sin(\Phi) & 0 \\
         sin(\Phi) &  cos(\Phi) & 0 \\
                 0 &          0 & 1
         \end{bmatrix}.

    The general 3-by-3 rotation matrix
    :math:`\textbf{R}(\Psi,\Theta,\Phi)` can be defined as:

    .. math::
        \textbf{R}(\Psi,\Theta,\Phi)
        =\textbf{R}_z(\Phi)
         \textbf{R}_y(\Theta)
         \textbf{R}_x(\Psi).

    :math:`\Psi`, :math:`\Theta` and :math:`\Phi` are the Euler
    angles.

    References
    ----------
    .. [Slabaugh1999] G. G. Slabaugh; Computing Euler angles from a
        rotation matrix., 1999.

    Examples
    --------
    >>> import molgemtools.geom as mg
    >>> r_array = mg.rmat([45, 30, 60])
    >>> r_array
    array([[ 0.4330127 , -0.43559574,  0.78914913],
           [ 0.75      ,  0.65973961, -0.04736717],
           [-0.5       ,  0.61237244,  0.61237244]])
    """
    if len(e_list) != 3:
        raise ValueError("e_list should contain three numbers.")
    for i in e_list:
        if not -180.0 <= i <= 180.0:
            raise ValueError("Euler angles should be between -180 and 180 "
                             + "degrees.")
    # x_rad is the angle of rotation about the x-axis in radians.
    x_rad = e_list[0]*np.pi/180.0
    cx = np.cos(x_rad)
    sx = np.sin(x_rad)
    # Matrix Rx representing the rotation about the x-axis.
    rx_array = np.array([[1.0, 0.0, 0.0],
                         [0.0,  cx, -sx],
                         [0.0,  sx,  cx]])
    # y_rad is the angle of rotation about the y-axis in radians.
    y_rad = e_list[1]*np.pi/180.0
    cy = np.cos(y_rad)
    sy = np.sin(y_rad)
    # Matrix Ry representing the rotation about the y-axis.
    ry_array = np.array([[ cy, 0.0,  sy],
                         [0.0, 1.0, 0.0],
                         [-sy, 0.0,  cy]])
    # z_rad is the angle of rotation about the z-axis in radians.
    z_rad = e_list[2]*np.pi/180.0
    cz = np.cos(z_rad)
    sz = np.sin(z_rad)
    # Matrix Rz representing the rotation about the z-axis.
    rz_array = np.array([[ cz, -sz, 0.0],
                         [ sz,  cz, 0.0],
                         [0.0, 0.0, 1.0]])
    # Rotation matrix R is the product of Rz, Ry and Rx.
    # R = Rz*Ry*Rx.
    r_array = np.matmul(np.matmul(rz_array, ry_array), rx_array)
    return r_array


def euler(r_list, pz=10):
    r"""Calculates the Euler angles of a 3-by-3 rotation matrix.

    Parameters
    ----------
    r_list : array_like
        A 3-by-3 dimensional orthogonal matrix.

    pz : int or float, optional
        A number less than 10 to the negative power of `pz`
        considered 0.

    Returns
    -------
    e_array : array_like
        Array of the three Euler angles in degrees.

    See Also
    --------
    molgemtools.geom.rmat

    Notes
    -----
    The general 3-by-3 rotation matrix :math:`\textbf{R}` can be
    defined as:

    .. math::
        \textbf{R}
        =\begin{bmatrix}
         cos(\Theta)cos(\Phi) &
         sin(\Psi)sin(\Theta)cos(\Phi)-cos(\Psi)sin(\Phi) &
         cos(\Psi)sin(\Theta)cos(\Phi)+sin(\Psi)sin(\Phi) \\
         cos(\Theta)sin(\Phi) &
         sin(\Psi)sin(\Theta)sin(\Phi)+cos(\Psi)cos(\Phi) &
         cos(\Psi)sin(\Theta)sin(\Phi)-sin(\Psi)cos(\Phi) \\
         -sin(\Theta) &
         sin(\Psi)cos(\Theta) &
         cos(\Psi)cos(\Theta)
         \end{bmatrix}.

    Using trigonometric identities Euler angles :math:`\Psi`,
    :math:`\Theta` and :math:`\Phi`, corresponding rotations about
    `x`-, `y`- and `z`-axes, can be calculated.

    Examples
    --------
    >>> import molgemtools.geom as mg
    >>> r_array = mg.rmat([45, 30, 60])
    >>> e_array = mg.euler(r_array)
    >>> e_array
    array([45., 30., 60.])
    >>> r_list = [[ 0,  0,  1],
    ...           [ 0,  1,  0],
    ...           [-1,  0,  0]]
    >>> e_array = mg.euler(r_list)
    >>> e_array
    array([ 0., 90.,  0.])
    """
    r_array = np.array(r_list)
    if len(r_array) != 3:
        raise ValueError("Expected a 3-by-3 dimensional matrix.")
    for i in range(3):
        if len(r_array[i]) != 3:
            raise ValueError("Expected a 3-by-3 dimensional matrix.")
    # Calculating Euler angles in radians.
    if round(abs(r_array[2][0]), pz) != 1.0:
        y_rad = -np.arcsin(r_array[2][0])
        x_rad = np.arctan2(r_array[2][1]/np.cos(y_rad),
                           r_array[2][2]/np.cos(y_rad))
        z_rad = np.arctan2(r_array[1][0]/np.cos(y_rad),
                           r_array[0][0]/np.cos(y_rad))
    else:
        z_rad = 0.0
        if round(r_array[2][0], pz) == -1.0:
            y_rad = np.pi/2
            x_rad = np.arctan2(r_array[0][1],
                               r_array[0][2])
        else:
            y_rad = -np.pi/2
            x_rad = np.arctan2(-r_array[0][1],
                               -r_array[0][2])
    # Converting Euler angles to degrees.
    x_deg = x_rad*180.0/np.pi
    y_deg = y_rad*180.0/np.pi
    z_deg = z_rad*180.0/np.pi
    # An array of the three Euler angles.
    e_array = np.array([x_deg, y_deg, z_deg])
    # Constructing a rotation matrix from the calculated angles.
    test_array = rmat(e_array)
    # The sum of absolute differences between r_array and the
    # reconstructed rotation matrix.
    error = sum(sum(abs(r_array - test_array)))
    # If the reconstructed matrix differs from the original, the
    # original matrix r_array considered an invalid rotation matrix.
    if round(error, pz) != 0.0:
        raise ValueError("Invalid rotation matrix.")
    return e_array


class Geom:
    """The `Geom` class contains some methods for operating on
    molecular geometry data.

    Parameters
    ----------
    x_dict : dict
        A dict containing molecular geometry information.

        x_dict['n'] : int
            The number of atoms.
        x_dict['name'] : str
            Comment line.
        x_dict['atoms'] : list of str
            The chemical symbols of the atoms in order.
        x_dict['xyz'] : ndarray
            The 3-by-n matrix of the atomic coordinates.

    See Also
    --------
    molgemtools.geom.open_xyz
    """

    def __init__(self, x_dict):
        self.n = x_dict["n"]
        if type(self.n) != int:
            raise TypeError("x_dict['n'] should be an integer.")
        self.name = x_dict["name"]
        if type(self.name) != str:
            raise TypeError("x_dict['name'] should be a string.")
        self.atoms = x_dict["atoms"]
        if len(self.atoms) != self.n:
            raise ValueError("x_dict['atoms'] should contain x_dict['n'] "
                             + "number of atom(s).")
        for atom in self.atoms:
            if type(atom) != str:
                raise TypeError("x_dict['atoms'] should only contain "
                                + "strings.")
        self.xyz = np.array(x_dict["xyz"])
        if len(self.xyz) != 3:
            raise ValueError("x_dict['xyz'] should have 3 rows.")
        for row in self.xyz:
            if len(row) != self.n:
                raise ValueError("x_dict['xyz'] should have x_dict['n'] "
                                 + "number of column(s).")
        self.m_dict = {}
        for atom in self.atoms:
            if atom not in self.m_dict:
                self.m_dict[atom] = constants.Constants.m_dict[atom]
        self.mass = sum(self.m_dict[atom] for atom in self.atoms)

    def adjacency(self, c=0.4):
        """Creates an ordered adjacency list of the neighbouring atoms
        in the molecule. The adjacency of an atomic pair determined by
        their 3-dimensional distance. If the distance is smaller than
        the sum of their covalent radii plus the constant `c`, the two
        atoms are judged to be adjacent. The distance is calculated in
        angstroms and the unit of `c` is also angstrom. The atomic
        indexing starts from 0.

        Parameters
        ----------
        c : int or float, optional
            A constant.

        Returns
        -------
        adj_list : list of list of int

        See Also
        --------
        molgemtools.graph.Graph
        molgemtools.geom.Geom.distance
        molgemtools.geom.Geom.zmat

        Examples
        --------
        >>> import molgemtools.geom as mg
        >>> x_dict = mg.open_xyz('data/CH4.xyz')
        >>> adj_list = mg.Geom(x_dict).adjacency()
        >>> adj_list
        [[1, 2, 3, 4], [0], [0], [0], [0]]
        """
        if c < 0.0:
            raise ValueError("c should not be negative.")
        adj_list = []
        for i in range(self.n):
            adj_list.append([])
        r_dict = {}
        for atom in self.atoms:
            if atom not in r_dict:
                r_dict[atom] = constants.Constants.r_dict[atom]
        for i in range(self.n):
            for j in range(i + 1, self.n):
                d_max = (r_dict[self.atoms[i]] + r_dict[self.atoms[j]] + c)
                dist = self.distance(i, j)
                if dist < d_max:
                    adj_list[i].append(j)
                    adj_list[j].append(i)
        return adj_list

    def distance(self, a, b):
        r"""Calculates the distance of two atoms in the molecule. The
        unit of distance is the same as the unit of length of the
        atomic coordinates.

        Parameters
        ----------
        a, b : int
            Atomic index. The atomic indexing starts from 0.

        Returns
        -------
        d : float

        See Also
        --------
        molgemtools.geom.Geom.adjacency
        molgemtools.geom.Geom.zmat

        Notes
        -----
        The Euclidean distance between 3-dimensional vectors
        :math:`\textbf{a}` and :math:`\textbf{b}` is the square root
        of the sum of the squared differences.

        .. math::
            d_{\textbf{a},\textbf{b}}
            =\sqrt{(a_{0}-b_{0})^2
                   +(a_{1}-b_{1})^2
                   +(a_{2}-b_{2})^2}.

        Examples
        --------
        >>> import molgemtools.geom as mg
        >>> x_dict = mg.open_xyz('data/CH4.xyz')
        >>> d = mg.Geom(x_dict).distance(0, 1)
        >>> d
        1.07
        >>> d = mg.Geom(x_dict).distance(0, 2)
        >>> d
        1.0700062694208852
        >>> d = mg.Geom(x_dict).distance(1, 2)
        >>> d
        1.7473085636486763
        """
        if a == b:
            raise ValueError("a and b should be different.")
        # The n-by-3 dimensional matrix of atomic coordinates.
        xyz = np.transpose(self.xyz)
        # 3 dimensional vectors a and b representing the atomic
        # coordinates of the corresponding atoms.
        a_array = xyz[a]
        b_array = xyz[b]
        # The Euclidean distance between vectors a and b is the square
        # root of the sum of the squared differences.
        d = sum((a_array - b_array)**2)**0.5
        return d

    def angle(self, a, b, c):
        r"""Calculates the angle between three points (atoms) in
        degrees.

        Parameters
        ----------
        a, b, c : int
            Atomic index. The indexing starts from 0.

        Returns
        -------
        t_deg : float

        See Also
        --------
        molgemtools.geom.Geom.zmat

        Notes
        -----
        Let :math:`\textbf{a}`, :math:`\textbf{b}` and
        :math:`\textbf{c}` be 3-dimensional points. The angle between
        distance vectors :math:`\textbf{a}-\textbf{b}` and
        :math:`\textbf{c}-\textbf{b}` in degrees can be defined as:

        .. math::
            \Theta_{\textbf{a},\textbf{b},\textbf{c}}
            =\frac{180}{\pi}
             cos^{-1}\left( \frac{(\textbf{a}-\textbf{b})\cdot
                                  (\textbf{c}-\textbf{b})}
                                 {\Vert \textbf{a}-\textbf{b}\Vert
                                  \Vert \textbf{c}-\textbf{b}\Vert}
                     \right) .

        Examples
        --------
        >>> import molgemtools.geom as mg
        >>> x_dict = mg.open_xyz('data/CH4.xyz')
        >>> t_deg = mg.Geom(x_dict).angle(1, 0, 2)
        >>> t_deg
        109.47129126098761
        """
        abc_list = [a, b, c]
        for i in range(3):
            for j in range(i + 1, 3):
                if abc_list[i] == abc_list[j]:
                    raise ValueError("a, b and c should be different.")
        # The n-by-3 dimensional matrix of atomic coordinates.
        xyz = np.transpose(self.xyz)
        # 3 dimensional vectors a, b and c representing the atomic
        # coordinates of the corresponding atoms.
        a_array = xyz[a]
        b_array = xyz[b]
        c_array = xyz[c]
        # 3 dimensional vector p, the difference of vectors a and b.
        # p = a - b.
        p_array = a_array - b_array
        # 3 dimensional vector r, the difference of vectors c and b.
        # r = c - b.
        r_array = c_array - b_array
        # The dot product of vectors p and r.
        p_r_dot_product = sum(p_array*r_array)
        # The norm of vector p.
        p_norm = sum(p_array**2)**0.5
        # The norm of vector r.
        r_norm = sum(r_array**2)**0.5
        # The cosine of the angle between vectors p and r in radians.
        cos_t_rad = p_r_dot_product/(p_norm*r_norm)
        # Angle t in radians.
        t_rad = np.arccos(cos_t_rad)
        # Converting angle t to degrees.
        t_deg = t_rad*180.0/np.pi
        return t_deg

    def dihedral(self, a, b, c, d, z=179.5):
        r"""Calculates the dihedral angle defined by four
        non-collinear points (atoms) in degrees.

        Parameters
        ----------
        a, b, c, d : int
            Atomic index. The indexing starts from 0.
        z : int or float, optional
            The maximum acceptable value of `angle(a, b, c)` and
            `angle(b, c, d)` in degrees. If the angle between three
            consecutive atoms is bigger than `z`, the atoms are
            considered collinear and the dihedral angle of `a`, `b`,
            `c`, `d` can not be calculated. The value of `z` should be
            bigger than 170 and smaller than 180.

        Returns
        -------
        f_deg : float

        See Also
        --------
        molgemtools.geom.Geom.zmat

        Notes
        -----
        Let :math:`\textbf{a}`, :math:`\textbf{b}`, :math:`\textbf{c}`
        and :math:`\textbf{d}` be 3-dimensional points and let
        :math:`\textbf{q}_i`, :math:`\textbf{q}_j`,
        :math:`\textbf{q}_k` be the corresponding difference vectors.

        .. math::
            \textbf{q}_i=\textbf{b}-\textbf{a},

            \textbf{q}_j=\textbf{c}-\textbf{b},

            \textbf{q}_k=\textbf{d}-\textbf{c}.

        Let :math:`\textbf{n}_i`, :math:`\textbf{u}_i`,
        :math:`\textbf{u}_k` and :math:`\textbf{u}_j` be the following
        vectors:

        .. math::
            \textbf{n}_i=\frac{\textbf{q}_i\times \textbf{q}_j}
                         {\Vert \textbf{q}_i\times \textbf{q}_j\Vert},

            \textbf{u}_i=\frac{\textbf{q}_j\times \textbf{q}_k}
                         {\Vert \textbf{q}_j\times \textbf{q}_k\Vert},

            \textbf{u}_k=\frac{\textbf{q}_j}
                         {\Vert \textbf{q}_j\Vert},

            \textbf{u}_j=\textbf{u}_k\times \textbf{u}_i .

        The dihedral angle in degrees between the two planes defined
        by points :math:`\textbf{a}`, :math:`\textbf{b}`,
        :math:`\textbf{c}` and :math:`\textbf{b}`, :math:`\textbf{c}`,
        :math:`\textbf{d}` can be calculated using the 2-argument
        arctangent function:

        .. math::
            \Phi_{\textbf{a},\textbf{b},\textbf{c},\textbf{d}}
            =-\frac{180}{\pi}
             arctan2(\textbf{n}_i \cdot \textbf{u}_j,
                     \textbf{n}_i \cdot \textbf{u}_i).

        References
        ----------
        .. [Emery2001] J. D. Emery; Computing The Protein Angles That
            Characterize Backbone Structure. Creating Ribbon Models.,
            2001.

        .. [Bayati2015] M. Bayati; Parallel methods for protein
            coordinate conversion., Thesis, Northeastern University,
            Boston, Massachusetts, 2015.

        Examples
        --------
        >>> import molgemtools.geom as mg
        >>> x_dict = mg.open_xyz('data/CH4.xyz')
        >>> f_deg = mg.Geom(x_dict).dihedral(3, 0, 1, 2)
        >>> f_deg
        -120.00039022740549
        """
        abcd_list = [a, b, c, d]
        for i in range(4):
            for j in range(i + 1, 4):
                if abcd_list[i] == abcd_list[j]:
                    raise ValueError("a, b, c and d should be different.")
        abc = abs(self.angle(a, b, c))
        bcd = abs(self.angle(b, c, d))
        if not 170.0 < z < 180.0:
            raise ValueError("z should be between 170 and 180.")
        if abc > z or bcd > z:
            raise ValueError("Three collinear atoms.")
        # The n-by-3 dimensional matrix of atomic coordinates.
        xyz = np.transpose(self.xyz)
        # 3 dimensional vectors a, b, c and d representing the atomic
        # coordinates of the corresponding atoms.
        a_array = xyz[a]
        b_array = xyz[b]
        c_array = xyz[c]
        d_array = xyz[d]
        # 3 dimensional vector qi, the difference of vectors b and a.
        # qi = b - a.
        qi = b_array - a_array
        # 3 dimensional vector qj, the difference of vectors c and b.
        # qj = c - b.
        qj = c_array - b_array
        # 3 dimensional vector qk, the difference of vectors d and c.
        # qk = d - c.
        qk = d_array - c_array
        # Vector ri, the cross product of vectors qi and qj.
        ri = np.cross(qi, qj)
        # Vector ni is vector ri divided by its norm.
        ni = ri/sum(ri**2)**0.5
        # Vector rj, the cross product of vectors qj and qk.
        rj = np.cross(qj, qk)
        # Vector ui is vector rj divided by its norm.
        ui = rj/sum(rj**2)**0.5
        # Vector uk is vector qj divided by its norm.
        uk = qj/sum(qj**2)**0.5
        # Vector uj, the cross product of vectors uk and ui.
        uj = np.cross(uk, ui)
        # x, the dot product of vectors ni and ui.
        x = sum(ni*ui)
        # y, the dot product of vectors ni and uj.
        y = sum(ni*uj)
        # The negative of the 2-argument arctangent of y/x.
        f_rad = -np.arctan2(y, x)
        # Converting angle t to degrees.
        f_deg = f_rad*180.0/np.pi
        return f_deg

    def conformer(self, y_list, imp_rot=False, w=10, z=0.01):
        """The goal of `conformer()` is the identification of an
        unknown geometry by assigning it to a well-defined one. The
        comparison is based on calculating the RMSD values of the
        optimally aligned (by rotation and translation) molecular
        geometries.

        Parameters
        ----------
        y_list : list of dict
            The geometry information of the well-defined conformers.
            A list containing dictionaries can be generated by the
            function `read_xyz()`.
        imp_rot : bool, optional
            If we allow improper rotation during shape matching
            `imp_rot` should be set to `True`, otherwise `False`.
        w : int or float, optional
            The minimum acceptable ratio of the second lowest RMSD
            value and the lowest RMSD value between the unknown
            conformer and the two closest reference conformers. If the
            second lowest RMSD minimum `w`-times bigger than the
            lowest, we can assign the unknown conformer to the lowest
            RMSD producing reference geometry. The value of `w` should
            be bigger than 1. If `y_list` contains only one conformer,
            `w` will be ignored.
        z : int or float, optional
            The maximum acceptable value of the lowest RMSD. If the
            lowest RMSD is bigger than `z`, the conformer will be
            undetermined. The unit of `z` depends on the unit of the
            atomic coordinates. The value of `z` should be positive.

        Returns
        -------
        conf_dict : dict

            conf_dict['name'] : str
                The name of the unknown geometry.
            conf_dict['conformer'] : str
                The name of the identified conformer if the
                identification was successful. This name is the name
                from the reference conformer identical to the unknown.
                If the identification was unsuccessful, it will be
                'undetermined'.
            conf_dict['RMSD_list'] : list of [str, float]
                The first element is the name of the reference
                geometry and the second is the corresponding RMSD
                value. The inner lists are sorted by the RMSD values.

        See Also
        --------
        molgemtools.geom.center
        molgemtools.geom.rmsd
        molgemtools.geom.smsvd
        molgemtools.geom.Geom.permute

        Examples
        --------
        Creating a list of molecular geometries of the well-defined
        conformers.

        >>> import os
        >>> import molgemtools.geom as mg
        >>> folder = 'data/alanine/conformers'
        >>> files = os.listdir(folder)
        >>> y_list = [mg.open_xyz(folder + '/' + file)
        ...           for file in files]

        `x_dict` representing an unknown molecular geometry.

        >>> x_dict = mg.open_xyz('data/alanine/ala_unknown_1.xyz')

        Shape matching between `x_dict` and the geometries in
        `y_list`.

        >>> conf_dict = mg.Geom(x_dict).conformer(y_list,
        ...                                       w=5,
        ...                                       z=0.075)

        The name of the unknown molecule.

        >>> conf_dict['name']
        'ala_unknown_1'

        The name of the conformer identical to the unknown.

        >>> conf_dict['conformer']
        'ala_2'

        The first three values of the sorted list of RMSD values
        between the unknown geometry and the reference conformers. The
        best RMSD value is much lower than the second.

        >>> for rmsd_list in conf_dict['RMSD_list'][:3]:
        ...     print(rmsd_list)
        ['ala_2', 0.043268371800158426]
        ['ala_4', 0.9410341026355801]
        ['ala_1', 1.2456803995697054]

        The identification of the following geometry turned out to be
        unsuccessful with the same settings.

        >>> x_dict = mg.open_xyz('data/alanine/ala_unknown_2.xyz')
        >>> conf_dict = mg.Geom(x_dict).conformer(y_list,
        ...                                       w=5,
        ...                                       z=0.075)
        >>> conf_dict['name']
        'ala_unknown_2'
        >>> conf_dict['conformer']
        'undetermined'

        All RMSD values are high.

        >>> for rmsd_list in conf_dict['RMSD_list'][:3]:
        ...     print(rmsd_list)
        ['ala_2', 1.248255066456825]
        ['ala_3', 1.357172124387427]
        ['ala_4', 1.4593002538678603]

        Allowing improper rotation during shape matching.

        >>> conf_dict = mg.Geom(x_dict).conformer(y_list,
        ...                                       imp_rot=True,
        ...                                       w=5,
        ...                                       z=0.075)

        Proper rotation did not help, but using improper rotation a
        successful identification happened. The unknown geometry and
        the corresponding reference conformer are conformational
        enantiomers.

        >>> conf_dict['conformer']
        'ala_3'

        The best RMSD value is much lower than the second lowest.

        >>> for rmsd_list in conf_dict['RMSD_list'][:3]:
        ...     print(rmsd_list)
        ['ala_3', 0.051228308837390685]
        ['ala_4', 1.0085037710741924]
        ['ala_1', 1.2251517108155412]

        The identification of the following alanine conformer was
        unsuccessful even with allowed improper rotation.

        >>> x_dict = mg.open_xyz('data/alanine/ala_unknown_3.xyz')
        >>> conf_dict = mg.Geom(x_dict).conformer(y_list,
        ...                                       imp_rot=True,
        ...                                       w=5,
        ...                                       z=0.075)
        >>> conf_dict['name']
        'ala_unknown_3'
        >>> conf_dict['conformer']
        'undetermined'

        Generating reference geometries with the hydrogens of the
        amino (8, 9) and the methyl group (10, 11, 12) altered.

        >>> y_list_altered = []
        >>> for y_dict in y_list:
        ...     data_list = mg.Geom(y_dict).permute([[8, 9],
        ...                                          [10, 11, 12]])
        ...     y_list_altered = y_list_altered + data_list

        With `y_list_altered` the identification was successful.

        >>> conf_dict = mg.Geom(x_dict).conformer(y_list_altered,
        ...                                       imp_rot=True,
        ...                                       w=5,
        ...                                       z=0.075)
        >>> conf_dict['name']
        'ala_unknown_3'
        >>> conf_dict['conformer']
        'ala_4'
        """
        if w <= 1.0:
            raise ValueError("w must be bigger than 1.")
        if z <= 0.0:
            raise ValueError("z must be positive.")
        # Number of reference conformers.
        y_len = len(y_list)
        rmsd_list = []
        for i in range(y_len):
            y_dict = y_list[i]
            # 3-by-n matrix containing atomic coordinates of the i-th
            # reference conformer.
            y_xyz = y_dict["xyz"]
            if self.atoms == y_dict["atoms"]:
                rmsd = smsvd(center(self.xyz),
                             center(y_xyz),
                             imp_rot=imp_rot)[0]
                rmsd_list.append([y_dict["name"],
                                  rmsd])
            else:
                raise ValueError("Mismatching atoms.")
        if y_len > 1:
            # Sorting the elements of the rmsd_list.
            rmsd_list = sorted(rmsd_list, key=lambda x: x[1])
            rmsd_ratio = rmsd_list[1][1]/rmsd_list[0][1]
            if rmsd_ratio > w and rmsd_list[0][1] < z:
                conformer = rmsd_list[0][0]
            else:
                conformer = "undetermined"
        else:
            if rmsd_list[0][1] < z:
                conformer = rmsd_list[0][0]
            else:
                conformer = "undetermined"
        conf_dict = {"name": self.name,
                     "conformer": conformer,
                     "RMSD_list": rmsd_list}
        return conf_dict

    def inertia(self):
        r"""Returns a sorted array of the principal moments of inertia
        in unit :math:`ul^2` (`u` is the atomic unit of mass and `l`
        is same unit of length as the unit of length of the atomic
        coordinates in `x_dict['xyz']`).

        Returns
        -------
        i_abc_array : ndarray

        Notes
        -----
        The inertia tensor of a rigid molecule can be defined as:

        .. math::
            \textbf{I}
            =\begin{bmatrix}
             I_{xx} & I_{xy} & I_{xz} \\
             I_{xy} & I_{yy} & I_{yz} \\
             I_{xz} & I_{yz} & I_{zz}
             \end{bmatrix}.

        Let :math:`x_{i}`, :math:`y_{i}` and :math:`z_{i}` be the
        Cartesian coordinates and :math:`m_{i}` the average atomic
        mass of the :math:`i^{th}` atom in a molecule of `n` atoms.
        The diagonal components of :math:`\textbf{I}` in the
        center-of-mass reference frame are

        .. math::
            I_{xx}
            =\sum\limits_{i=0}^{n-1}
             m_{i}(y_{i}^2+z_{i}^2),

        .. math::
            I_{yy}
            =\sum\limits_{i=0}^{n-1}
             m_{i}(x_{i}^2+z_{i}^2),

        .. math::
            I_{zz}
            =\sum\limits_{i=0}^{n-1}
             m_{i}(x_{i}^2+y_{i}^2).

        The off-diagonal components

        .. math::
            I_{xy}
            =-\sum\limits_{i=0}^{n-1}
             m_{i}x_{i}y_{i},

        .. math::
            I_{xz}
            =-\sum\limits_{i=0}^{n-1}
             m_{i}x_{i}z_{i},

        .. math::
            I_{yz}
            =-\sum\limits_{i=0}^{n-1}
             m_{i}y_{i}z_{i}.

        The eigenvalues of the matrix :math:`\textbf{I}` are the
        :math:`I_{a}`, :math:`I_{b}` and :math:`I_{c}` principal
        moments of inertia. By convention
        :math:`I_{a}\leq I_{b}\leq I_{c}`.

        References
        ----------
        .. [Cowgill1964] G. R. Cowgill; J. G. Ehlers; Fortran program
            for computing the principal moments of inertia of a rigid
            molecule, 1964.

        Examples
        --------
        >>> import molgemtools.geom as mg
        >>> x_dict = mg.open_xyz('data/CH4.xyz')
        >>> i_abc_array = mg.Geom(x_dict).inertia()
        >>> i_abc_array
        array([3.07732869, 3.07734051, 3.07735559])
        """
        # An array of the average atomic masses (in unit u).
        m_array = np.array([self.m_dict[atom] for atom in self.atoms])
        # Coordinates of the center of the mass.
        x_com = sum(self.xyz[0]*m_array)/self.mass
        y_com = sum(self.xyz[1]*m_array)/self.mass
        z_com = sum(self.xyz[2]*m_array)/self.mass
        # Shifting the coordinates to the center of the mass.
        x_array = self.xyz[0] - x_com
        y_array = self.xyz[1] - y_com
        z_array = self.xyz[2] - z_com
        # Diagonal elements of the inertia tensor.
        ixx = sum(m_array*(y_array**2 + z_array**2))
        iyy = sum(m_array*(x_array**2 + z_array**2))
        izz = sum(m_array*(x_array**2 + y_array**2))
        # Off-diagonal elements of the inertia tensor.
        ixy = -sum(m_array*x_array*y_array)
        ixz = -sum(m_array*x_array*z_array)
        iyz = -sum(m_array*y_array*z_array)
        # The tensor of inertia. A symmetric real matrix.
        i_array = np.array([[ixx, ixy, ixz],
                            [ixy, iyy, iyz],
                            [ixz, iyz, izz]])
        # Diagonalization of the inertia tensor.
        i_abc_list = sorted(np.linalg.eig(i_array)[0])
        # Converting i_abc_list to an array.
        i_abc_array = np.array(i_abc_list)
        return i_abc_array

    def permute(self, i_list):
        """Creates a list of permuted geometries according to
        permutable atoms.

        Parameters
        ----------
        i_list : list of list of int
            A list of indices of permutable atoms. The indexing of
            atoms starts from 0.

        Returns
        -------
        data_list : list of dict

            [{'n' : int,
            'name' : str,
            'atoms' : list of str,
            'xyz' : ndarray}]

        See Also
        --------
        molgemtools.geom.Geom.conformer

        References
        ----------
        .. [Heap1963] B. R. Heap; Permutations by Interchanges, The
            Computer Journal, Volume 6, Issue 3, 1963, 293-298.

        Examples
        --------
        Creating permuted versions of `CH4.xyz` with atoms 1, 2 and 3,
        4 altered.

        >>> import molgemtools.geom as mg
        >>> x_dict = mg.open_xyz('data/CH4.xyz')
        >>> data_list = mg.Geom(x_dict).permute([[1, 2], [3, 4]])
        >>> data_list[0]['atoms']
        ['C', 'H', 'H', 'H', 'H']
        >>> data_list[0]['xyz']
        array([[-2.09343, -1.02343, -2.4501 , -2.4501 , -2.4501 ],
               [ 1.74059,  1.74059,  2.64313,  0.89901,  1.67963],
               [ 0.     ,  0.     ,  0.45069,  0.55627, -1.00696]])
        >>> data_list[1]['xyz']
        array([[-2.09343, -2.4501 , -1.02343, -2.4501 , -2.4501 ],
               [ 1.74059,  2.64313,  1.74059,  0.89901,  1.67963],
               [ 0.     ,  0.45069,  0.     ,  0.55627, -1.00696]])
        >>> data_list[2]['xyz']
        array([[-2.09343, -1.02343, -2.4501 , -2.4501 , -2.4501 ],
               [ 1.74059,  1.74059,  2.64313,  1.67963,  0.89901],
               [ 0.     ,  0.     ,  0.45069, -1.00696,  0.55627]])
        >>> data_list[3]['xyz']
        array([[-2.09343, -2.4501 , -1.02343, -2.4501 , -2.4501 ],
               [ 1.74059,  2.64313,  1.74059,  1.67963,  0.89901],
               [ 0.     ,  0.45069,  0.     , -1.00696,  0.55627]])
        """

        def perm(b_list, j_list):
            """Inner function `perm()` creates a list of permuted
            versions of `b_list` according to `j_list`.

            Parameters
            ----------
            b_list : array_like
                An array to be permuted.
            j_list : list of int
                A list of integers denoting which elements of `b_list`
                should be altered.

            Returns
            -------
            p_list : list of array_like
                The list of altered versions of `b_list`.
            """

            def heap(c, k):
                """Inner function `heap()` recursively permutes list
                `c` according to `k`.

                Parameters
                ----------
                c : list
                    The list to be permuted.
                k : list of int
                    A list of integers denoting which elements of `c`
                    should be altered.
                """
                # Generate permutations of c with c[k[-1]] unaltered.
                if len(k) > 2:
                    heap(c, k[:-1])
                # Generate permutations by swapping c[i] with c[k[-1]]
                # if list k contains even number of indices. If the
                # length of k is odd, c[0] and c[k[-1]] will be
                # swapped. The copy of the altered list will be
                # appended to p_list.
                for i in k[:-1]:
                    if len(k) % 2 == 0:
                        c[i], c[k[-1]] = c[k[-1]], c[i]
                        p_list.append([item for item in c])
                    else:
                        c[k[0]], c[k[-1]] = c[k[-1]], c[k[0]]
                        p_list.append([item for item in c])
                    if len(k) > 2:
                        heap(c, k[:-1])

            # Creating p_list containing the copy of b_list.
            p_list = [[item for item in b_list]]
            # Calling inner function heap() to append p_list with all
            # possible permutations of the copy of b_list according to
            # j_list.
            heap([item for item in p_list[0]], j_list)
            return p_list

        def generate(a_list, indices, n):
            """Inner function `generate()` adds permuted versions of
            `a_list` to `permuted_list` recursively.

            Parameters
            ----------
            a_list : list
                A list to be permuted.

            indices : list of list of int
                A list of indices of permutable atoms.

            n : int
                Denoting an inner list of `indices`.
            """
            # List of permuted versions of a_list according to
            # indices[n - 1].
            item_list = perm(a_list, indices[n - 1])
            if n > 1:
                for item in item_list:
                    generate(item, indices, n - 1)
            if n == 1:
                for item in item_list:
                    permuted_list.append(item)

        if not i_list:
            data_list = [{"n": self.n,
                          "name": self.name,
                          "atoms": self.atoms,
                          "xyz": self.xyz}]
            return data_list
        # Checking the validity of i_list.
        i_items = []
        for inner_list in i_list:
            if type(inner_list) != list:
                raise TypeError("i_list should contain lists of integers.")
            if len(inner_list) < 2:
                raise ValueError("Minimum two indices are required for every "
                                 + "permutation.")
            i_items = i_items + inner_list
            atom = self.atoms[inner_list[0]]
            for j in range(1, len(inner_list)):
                if self.atoms[inner_list[j]] != atom:
                    raise ValueError("The permutation of different atoms is "
                                     + "not possible.")
        i_count = {}
        for i in i_items:
            if i < 0:
                raise IndexError("The atomic index should be positive.")
            if i not in i_count:
                i_count[i] = 0
            i_count[i] = i_count[i] + 1
        for v in i_count.values():
            if v > 1:
                raise ValueError("Repeated indices in i_list.")
        # n-by-3 matrix, the transpose of x_dict['xyz'].
        a_array = np.transpose(self.xyz)
        permuted_list = []
        # Generate all permuted versions of a_list and add them to
        # permuted_list.
        generate(a_array, i_list, len(i_list))
        # Contains the transposed elements of permuted_list. These
        # transposed elements are 3-by-n matrices.
        coord_list = [np.transpose(item) for item in permuted_list]
        # List of dictionaries of the permuted molecular geometries.
        data_list = [{"n": self.n,
                      "name": self.name,
                      "atoms": self.atoms,
                      "xyz": item} for item in coord_list]
        return data_list

    def zmat(self, c=0.4, z=179.5):
        """Calculates the internal coordinate representation of the
        molecule.

        Parameters
        ----------
        c : int or float
            The tolerance of separation of two atoms. Atoms `a` and
            `b` are judged to be connected by a covalent bond if their
            distance is smaller than the sum of their covalent radii
            plus the constant `c`. The unit of length is calculated in
            angstrom.
        z : int or float
            The maximum value for the angles of three consecutive
            atoms in degrees. Relevant for calculating dihedral
            angles.

        Returns
        -------
        z_dict : dict

            z_dict['n'] : int
                The number of atoms.
            z_dict['name'] : str
                Comment line.
            z_dict['atoms'] : list of str
                The chemical symbols of the atoms in order.
            z_dict['indices'] : list of list of int
                A nested list containing indices of atoms. The atomic
                index starts from 0. The first column of indices
                corresponds to the indices of atoms themselves
                in order. The second, third and fourth colunms
                containing the indices of atoms relevant for
                calculating intramolecular distances, angles and
                dihedral angles.
            z_dict['distances'] : array_like
                Intramolecular distances.
            z_dict['angles'] : array_like
                Bond angles.
            z_dict['dihedral'] : array_like
                Dihedral angles.

        See Also
        --------
        molgemtools.geom.write_zmat
        molgemtools.geom.xyz_from_zmat
        molgemtools.geom.Geom.adjacency
        molgemtools.geom.Geom.distance
        molgemtools.geom.Geom.angle
        molgemtools.geom.Geom.dihedral

        Examples
        --------
        >>> import molgemtools.geom as mg
        >>> x_dict = mg.open_xyz('data/CH4.xyz')
        >>> z_dict = mg.Geom(x_dict).zmat()
        >>> z_dict['n']
        5
        >>> z_dict['name']
        'methane'
        >>> z_dict['atoms']
        ['C', 'H', 'H', 'H', 'H']
        >>> z_dict['indices']
        [[0], [1, 0], [2, 0, 1], [3, 0, 1, 2], [4, 0, 1, 2]]
        >>> z_dict['distances']
        array([1.07      , 1.07000627, 1.07000313, 1.06999909])
        >>> z_dict['angles']
        array([109.47129126, 109.4713507 , 109.47142719])
        >>> z_dict['dihedral']
        array([-120.00039023,  119.99996867])
        """
        # The adjacency list of the molecule.
        adj = self.adjacency(c=c)
        for item in adj:
            if len(item) == 0:
                raise ValueError("Fragmented molecule.")
        # List temp collects the lowest possible atomic indices
        # corresponding intramolecular distance, angle and dihedral
        # angle calculations.
        temp = []
        # The first element of every inner list of temp is the atomic
        # index of the corresponding atom itself.
        for i in range(self.n):
            temp.append([i])
        # The second element of the i-th inner list of temp is the
        # atomic index of the neighbouring atom with the smallest
        # possible index. If the smallest index is not smaller than i,
        # the molecular geometry can not be converted to z-matrix.
        for i in range(1, self.n):
            if adj[i][0] >= i:
                raise ValueError("Can not be converted to z-matrix.")
            temp[i].append(adj[i][0])
        # The third element of the i-th inner list of temp is the
        # smallest possible atomic index for calculating bond angle.
        # If possible, the i-th inner list is appended with the second
        # smallest number for calculating improper dihedral angle.
        for i in range(2, self.n):
            temp[i].append(adj[temp[i][1]][0])
            if i > 2 and len(adj[temp[i][1]]) > 1:
                if adj[temp[i][1]][1] not in temp[i]:
                    temp[i].append(adj[temp[i][1]][1])
        # Appending the i-th inner list of temp with the smallest
        # possible atomic index corresponding a proper dihedral angle.
        for i in range(3, self.n):
            if adj[temp[i][2]][0] not in temp[i]:
                temp[i].append(adj[temp[i][2]][0])
            elif len(adj[temp[i][2]]) > 1:
                if adj[temp[i][2]][1] not in temp[i]:
                    temp[i].append(adj[temp[i][2]][1])
        # The first three inner lists of temp are copied to i_list.
        i_list = [item for item in temp[:3]]
        # From temp[3], the first three index (corresponding to the
        # i-th atom, the distances and the angles) are copied to
        # i_list and the smallest index from the leftover part of
        # temp[i] denoting a proper or an improper dihedral angle.
        for i in range(3, self.n):
            i_list.append(temp[i][:3] + [min(temp[i][3:])])
        # Calculating distances.
        r_list = []
        for i in range(1, self.n):
            dist = self.distance(i_list[i][0],
                                 i_list[i][1])
            r_list.append(dist)
        # Calculating angles.
        a_list = []
        for i in range(2, self.n):
            ang = self.angle(i_list[i][0],
                             i_list[i][1],
                             i_list[i][2])
            a_list.append(ang)
        # Calculating dihedral angles.
        d_list = []
        for i in range(3, self.n):
            dihedral = self.dihedral(i_list[i][0],
                                     i_list[i][1],
                                     i_list[i][2],
                                     i_list[i][3],
                                     z=z)
            d_list.append(dihedral)
        z_dict = {"n": self.n,
                  "name": self.name,
                  "atoms": self.atoms,
                  "indices": i_list,
                  "distances": np.array(r_list),
                  "angles": np.array(a_list),
                  "dihedral": np.array(d_list)}
        return z_dict


if __name__ == "__main__":
    import os
    import doctest

    os.chdir(os.path.dirname(__file__))
    doctest.testmod()
    print("Doctest of molgemtools.geom...")
