Example #1
0
def twisting_faces(ao, co, save="not_save"):
    """
    Display twisting triangular faces and vector projection.

    Parameters
    ----------
    ao : list
        Atomic labels of octahedral structure.
    co : list
        Atomic coordinates of octahedral structure.
    save : str
        Name of image file to save. If this argument is not set by user,
        do not save a figure as image.

    Returns
    -------
    None : None

    """
    _, c_ref, _, c_oppo = tools.find_faces_octa(co)

    ref_vertices_list = []
    for i in range(4):
        get_vertices = c_ref[i].tolist()
        x, y, z = zip(*get_vertices)
        vertices = [list(zip(x, y, z))]
        ref_vertices_list.append(vertices)

    fig = plt.figure()
    st = fig.suptitle("Projected twisting triangular faces",
                      fontsize="x-large")

    for i in range(4):
        a, b, c, d = plane.find_eq_of_plane(c_ref[i][0], c_ref[i][1],
                                            c_ref[i][2])
        m = projection.project_atom_onto_plane(co[0], a, b, c, d)
        ax = fig.add_subplot(2, 2, int(i + 1), projection='3d')
        ax.set_title(f"Projection plane {i + 1}", fontsize='10')

        # Projected metal center atom
        ax.scatter(m[0],
                   m[1],
                   m[2],
                   color='orange',
                   s=100,
                   marker='o',
                   linewidths=1,
                   edgecolors='black',
                   label="Metal center")

        ax.text(m[0] + 0.1, m[1] + 0.1, m[2] + 0.1, f"{ao[0]}'", fontsize=9)

        # Reference atoms
        pl = []
        for j in range(3):
            ax.scatter(c_ref[i][j][0],
                       c_ref[i][j][1],
                       c_ref[i][j][2],
                       color='red',
                       s=50,
                       marker='o',
                       linewidths=1,
                       edgecolors='black',
                       label="Reference atom")

            ax.text(c_ref[i][j][0] + 0.1,
                    c_ref[i][j][1] + 0.1,
                    c_ref[i][j][2] + 0.1,
                    f"{j + 1}",
                    fontsize=9)

            # Project ligand atom onto the reference face
            pl.append(
                projection.project_atom_onto_plane(c_oppo[i][j], a, b, c, d))

        # Projected opposite atoms
        for j in range(3):
            ax.scatter(pl[j][0],
                       pl[j][1],
                       pl[j][2],
                       color='blue',
                       s=50,
                       marker='o',
                       linewidths=1,
                       edgecolors='black',
                       label="Projected ligand atom")

            ax.text(pl[j][0] + 0.1,
                    pl[j][1] + 0.1,
                    pl[j][2] + 0.1,
                    f"{j + 1}'",
                    fontsize=9)

        # Draw plane
        x, y, z = zip(*pl)
        projected_oppo_vertices_list = [list(zip(x, y, z))]
        ax.add_collection3d(
            Poly3DCollection(ref_vertices_list[i], alpha=0.5, color="yellow"))
        ax.add_collection3d(
            Poly3DCollection(projected_oppo_vertices_list,
                             alpha=0.5,
                             color="blue"))

        # Draw line
        for j in range(3):
            merge = list(zip(m.tolist(), c_ref[i][j].tolist()))
            x, y, z = merge
            ax.plot(x, y, z, 'k-', color="black")

        for j in range(3):
            merge = list(zip(m.tolist(), pl[j].tolist()))
            x, y, z = merge
            ax.plot(x, y, z, 'k->', color="black")

        # Set axis
        ax.set_xlabel(r'X', fontsize=10)
        ax.set_ylabel(r'Y', fontsize=10)
        ax.set_zlabel(r'Z', fontsize=10)
        ax.grid(True)

    # Shift subplots down
    st.set_y(1.0)
    fig.subplots_adjust(top=0.25)

    # plt.legend(bbox_to_anchor=(1.05, 1), loc=2)
    plt.tight_layout()

    if save != "not_save":
        plt.savefig('{0}.png'.format(save))

    plt.show()
Example #2
0
def find_faces_octa(c_octa):
    """
    Find the eight faces of octahedral structure.

    1. Choose 3 atoms out of 6 ligand atoms.
        The total number of combination is 20.
    2. Orthogonally project metal center atom onto the face:
        m ----> m'
    3. Calculate the shortest distance between original metal center to its projected point.
    4. Sort the 20 faces in ascending order of the shortest distance.
    5. Delete 12 faces that closest to metal center atom (first 12 faces).
    6. The remaining 8 faces are the (reference) face of octahedral structure.
    7. Find 8 opposite faces.

    | Reference plane             Opposite plane
    |    [[1 2 3]                    [[4 5 6]
    |     [1 2 4]        --->         [3 5 6]
    |       ...                         ...
    |     [2 3 5]]                    [1 4 6]]

    Parameters
    ----------
    c_octa : array_like
        Atomic coordinates of octahedral structure.

    Returns
    -------
    a_ref_f : list
        Atomic labels of reference face.
    c_ref_f : ndarray
        Atomic coordinates of reference face.
    a_oppo_f : list
        Atomic labels of opposite face.
    c_oppo_f : ndarray
        Atomic coordinates of opposite face.

    See Also
    --------
    octadist.src.plane.find_eq_of_plane :
        Find the equation of the plane.
    octadist.src.projection.project_atom_onto_plane :
        Orthogonal projection of point onto the plane.

    Examples
    --------
    >>> coord = [[14.68572 18.49228  6.66716]
                 [14.86476 16.48821  7.43379]
                 [14.44181 20.594    6.21555]
                 [13.37473 17.23453  5.45099]
                 [16.26114 18.54903  8.20527]
                 [13.04897 19.25464  7.93122]
                 [16.09157 18.9617   5.02956]]
    >>> a_ref, c_ref, a_oppo, c_oppo = find_faces_octa(coord)
    >>> a_ref
    [[1, 3, 6], [1, 4, 6], [2, 3, 6], [2, 3, 5],
     [2, 4, 5], [1, 4, 5], [1, 3, 5], [2, 4, 6]]
    >>> c_ref
    [[[14.86476 16.48821  7.43379]
      [13.37473 17.23453  5.45099]
      [16.09157 18.9617   5.02956]],
     ...,
     ...,
     [[14.44181 20.594    6.21555]
      [16.26114 18.54903  8.20527]
      [16.09157 18.9617   5.02956]]]
    >>> a_octa
    [[2, 4, 5], [2, 3, 5], [1, 4, 5], [1, 4, 6],
     [1, 3, 6], [2, 3, 6], [2, 4, 6], [1, 3, 5]]
    >>> c_octa
    [[[14.44181 20.594    6.21555]
      [16.26114 18.54903  8.20527]
      [13.04897 19.25464  7.93122]],
     ...,
     ...,
     [[14.86476 16.48821  7.43379]
      [13.37473 17.23453  5.45099]
      [13.04897 19.25464  7.93122]]]

    """
    ########################
    # Find reference faces #
    ########################

    c_octa = np.asarray(c_octa, dtype=np.float64)

    # Find the shortest distance from metal center to each triangle
    dist = []
    a_ref_f = []
    c_ref_f = []

    for i in range(1, 5):
        for j in range(i + 1, 6):
            for k in range(j + 1, 7):
                a, b, c, d = plane.find_eq_of_plane(c_octa[i], c_octa[j],
                                                    c_octa[k])
                m = projection.project_atom_onto_plane(c_octa[0], a, b, c, d)
                d_btw = distance.euclidean(m, c_octa[0])
                dist.append(d_btw)

                a_ref_f.append([i, j, k])
                c_ref_f.append([c_octa[i], c_octa[j], c_octa[k]])

    # Sort faces by distance in ascending order
    dist_a_c = list(zip(dist, a_ref_f, c_ref_f))
    dist_a_c.sort()
    dist, a_ref_f, c_ref_f = list(zip(*dist_a_c))
    c_ref_f = np.asarray(c_ref_f, dtype=np.float64)

    # Remove first 12 triangles, the rest of triangles is 8 faces of octahedron
    a_ref_f = a_ref_f[12:]
    c_ref_f = c_ref_f[12:]

    #######################
    # Find opposite faces #
    #######################

    all_atom = [1, 2, 3, 4, 5, 6]
    a_oppo_f = []

    for i in range(len(a_ref_f)):
        new_a_ref_f = []
        for j in all_atom:
            if j not in (a_ref_f[i][0], a_ref_f[i][1], a_ref_f[i][2]):
                new_a_ref_f.append(j)
        a_oppo_f.append(new_a_ref_f)

    c_oppo_f = []

    for i in range(len(a_oppo_f)):
        coord_oppo = []
        for j in range(3):
            coord_oppo.append([
                c_octa[int(a_oppo_f[i][j])][0],
                c_octa[int(a_oppo_f[i][j])][1],
                c_octa[int(a_oppo_f[i][j])],
            ][2])
        c_oppo_f.append(coord_oppo)

    a_ref_f = list(a_ref_f)
    c_ref_f = list(c_ref_f)

    c_ref_f = np.asarray(c_ref_f, dtype=np.float64)
    c_oppo_f = np.asarray(c_oppo_f, dtype=np.float64)

    return a_ref_f, c_ref_f, a_oppo_f, c_oppo_f
Example #3
0
    def calc_theta(self):
        """
        Calculate Theta parameter and value in degree.

        See Also
        --------
        calc_theta_min :
            Calculate minimum Theta parameter.
        calc_theta_max :
            Calculate maximum Theta parameter.
        octadist.src.linear.angle_btw_vectors :
            Calculate cosine angle between two vectors.
        octadist.src.linear.angle_sign
            Calculate cosine angle between two vectors sensitive to CW/CCW direction.
        octadist.src.plane.find_eq_of_plane :
            Find the equation of the plane.
        octadist.src.projection.project_atom_onto_plane :
            Orthogonal projection of point onto the plane.

        References
        ----------
        .. [4] M. Marchivie, P. Guionneau, J.-F. Létard, D. Chasseau.
            Photo‐induced spin‐transition: the role of the iron(II)
            environment distortion. Acta Crystal-logr. Sect. B Struct.
            Sci. 2005, 61, 25.

        """
        # Get refined atomic coordinates
        coord_metal, coord_lig = self.determine_faces()

        # loop over 8 faces
        for r in range(8):
            a, b, c, d = plane.find_eq_of_plane(
                coord_lig[0], coord_lig[1], coord_lig[2]
            )
            self.eq_of_plane.append([a, b, c, d])

            # Project metal and other three ligand atom onto the plane
            projected_m = projection.project_atom_onto_plane(coord_metal, a, b, c, d)
            projected_lig4 = projection.project_atom_onto_plane(
                coord_lig[3], a, b, c, d
            )
            projected_lig5 = projection.project_atom_onto_plane(
                coord_lig[4], a, b, c, d
            )
            projected_lig6 = projection.project_atom_onto_plane(
                coord_lig[5], a, b, c, d
            )

            # Find the vectors between atoms that are on the same plane
            # These vectors will be used to calculate Theta afterward.
            vector_theta = np.array(
                [
                    coord_lig[0] - projected_m,
                    coord_lig[1] - projected_m,
                    coord_lig[2] - projected_m,
                    projected_lig4 - projected_m,
                    projected_lig5 - projected_m,
                    projected_lig6 - projected_m,
                ]
            )

            # Check if the direction is CW or CCW
            a12 = linear.angle_btw_vectors(vector_theta[0], vector_theta[1])
            a13 = linear.angle_btw_vectors(vector_theta[0], vector_theta[2])

            # If angle of interest is smaller than its neighbor,
            # define it as CW direction, if not, it will be CCW instead.
            if a12 < a13:
                direction = np.cross(vector_theta[0], vector_theta[1])
            else:
                direction = np.cross(vector_theta[2], vector_theta[0])

            # Calculate individual theta angle
            theta1 = linear.angle_sign(vector_theta[0], vector_theta[3], direction)
            theta2 = linear.angle_sign(vector_theta[3], vector_theta[1], direction)
            theta3 = linear.angle_sign(vector_theta[1], vector_theta[4], direction)
            theta4 = linear.angle_sign(vector_theta[4], vector_theta[2], direction)
            theta5 = linear.angle_sign(vector_theta[2], vector_theta[5], direction)
            theta6 = linear.angle_sign(vector_theta[5], vector_theta[0], direction)

            indi_theta = np.array([theta1, theta2, theta3, theta4, theta5, theta6])

            self.eight_theta.append(sum(abs(indi_theta - 60)))

            # Use deep copy so as to avoid pass by reference
            tmp = coord_lig[1].copy()
            coord_lig[1] = coord_lig[3].copy()
            coord_lig[3] = coord_lig[5].copy()
            coord_lig[5] = coord_lig[2].copy()
            coord_lig[2] = tmp.copy()

            # If 3rd round, permutation face will be switched
            # from N1N2N3
            # to N1N4N2,
            # to N1N6N4,
            # to N1N3N6, and then back to N1N2N3
            if r == 3:
                coord_lig[[0, 4]] = coord_lig[[4, 0]]
                coord_lig[[1, 5]] = coord_lig[[5, 1]]
                coord_lig[[2, 3]] = coord_lig[[3, 2]]

        self.theta = sum(self.eight_theta) / 2
Example #4
0
def find_faces_octa(c_octa):
    """
    Find the eight faces of octahedral structure.

    1) Choose 3 atoms out of 6 ligand atoms.
        The total number of combination is 20.
    2) Orthogonally project metal center atom onto the face:
        m ----> m'
    3) Calculate the shortest distance between original metal center to its projected point.
    4) Sort the 20 faces in ascending order of the shortest distance.
    5) Delete 12 faces that closest to metal center atom (first 12 faces).
    6) The remaining 8 faces are the (reference) face of octahedral structure.
    7) Find 8 opposite faces.

    Parameters
    ----------
    c_octa : array
        Atomic coordinates of octahedral structure.

    Returns
    -------
    a_ref_f : list
        Atomic labels of reference face.
    c_ref_f : array
        Atomic coordinates of reference face.
    a_oppo_f : list
        Atomic labels of opposite face.
    c_oppo_f : array
        Atomic coordinates of opposite face.

    Examples
    --------
    Reference plane             Opposite plane
        [[1 2 3]                   [[4 5 6]
        [1 2 4]        --->        [3 5 6]
          ...                        ...
        [2 3 5]]                   [1 4 6]]

    """
    ########################
    # Find reference faces #
    ########################

    # Find the shortest distance from metal center to each triangle
    distance = []
    a_ref_f = []
    c_ref_f = []
    for i in range(1, 5):
        for j in range(i + 1, 6):
            for k in range(j + 1, 7):
                a, b, c, d = octadist.src.plane.find_eq_of_plane(
                    c_octa[i], c_octa[j], c_octa[k])
                m = projection.project_atom_onto_plane(c_octa[0], a, b, c, d)
                d_btw = linear.euclidean_dist(m, c_octa[0])
                distance.append(d_btw)

                a_ref_f.append([i, j, k])
                c_ref_f.append([c_octa[i], c_octa[j], c_octa[k]])

    # Sort faces by distance in ascending order
    dist_a_c = list(zip(distance, a_ref_f, c_ref_f))
    dist_a_c.sort()
    distance, a_ref_f, c_ref_f = list(zip(*dist_a_c))
    c_ref_f = np.asarray(c_ref_f)

    # Remove first 12 triangles, the rest of triangles is 8 faces of octahedron
    a_ref_f = a_ref_f[12:]
    c_ref_f = c_ref_f[12:]

    #######################
    # Find opposite faces #
    #######################

    all_atom = [1, 2, 3, 4, 5, 6]
    a_oppo_f = []

    for i in range(len(a_ref_f)):
        new_a_ref_f = []
        for j in all_atom:
            if j not in (a_ref_f[i][0], a_ref_f[i][1], a_ref_f[i][2]):
                new_a_ref_f.append(j)
        a_oppo_f.append(new_a_ref_f)

    v = np.array(c_octa)
    c_oppo_f = []

    for i in range(len(a_oppo_f)):
        coord_oppo = []
        for j in range(3):
            coord_oppo.append([
                v[int(a_oppo_f[i][j])][0], v[int(a_oppo_f[i][j])][1],
                v[int(a_oppo_f[i][j])]
            ][2])
        c_oppo_f.append(coord_oppo)

    return a_ref_f, c_ref_f, a_oppo_f, c_oppo_f
Example #5
0
def calc_theta(c_octa):
    """
    Calculate Theta parameter and value in degree.

          24
    Θ = sigma < 60 - angle_i >
         i=1

    where angle_i is an unique angle between two vectors of two twisting face.

    Parameters
    ----------
    c_octa : array or list
        Atomic coordinates of octahedral structure.

    Returns
    -------
    theta_mean : float
        Mean Theta value.

    References
    ----------
    M. Marchivie et al. Acta Crystal-logr. Sect. B Struct. Sci. 2005, 61, 25.

    Examples
    --------
    >>> coord
    [[2.298354000, 5.161785000, 7.971898000],  # <- Metal atom
     [1.885657000, 4.804777000, 6.183726000],
     [1.747515000, 6.960963000, 7.932784000],
     [4.094380000, 5.807257000, 7.588689000],
     [0.539005000, 4.482809000, 8.460004000],
     [2.812425000, 3.266553000, 8.131637000],
     [2.886404000, 5.392925000, 9.848966000]]
    >>> calc_theta(coord)
    122.68897277454599

    """
    if type(c_octa) == np.ndarray:
        pass
    else:
        c_octa = np.asarray(c_octa)

    ligands = list(c_octa[1:])

    TM = c_octa[0]
    N1 = c_octa[1]
    N2 = c_octa[2]
    N3 = c_octa[3]
    N4 = c_octa[4]
    N5 = c_octa[5]
    N6 = c_octa[6]

    # Vector from metal to ligand atom
    TMN1 = N1 - TM
    TMN2 = N2 - TM
    TMN3 = N3 - TM
    TMN4 = N4 - TM
    TMN5 = N5 - TM
    TMN6 = N6 - TM

    ligands_vec = [TMN1, TMN2, TMN3, TMN4, TMN5, TMN6]

    ###########################################
    # Determine the order of atoms in complex #
    ###########################################

    _, trans_angle = calc_bond_angle(c_octa)

    max_angle = trans_angle[0]

    # This loop is used to identify which N is in line with N1
    def_change = 6
    for n in range(6):
        test = linear.angle_btw_vectors(ligands_vec[0], ligands_vec[n])
        if test > (max_angle - 1):
            def_change = n

    test_max = 0
    new_change = 0
    for n in range(6):
        test = linear.angle_btw_vectors(ligands_vec[0], ligands_vec[n])
        if test > test_max:
            test_max = test
            new_change = n

    # geometry is used to identify the type of octahedral structure
    geometry = False
    if def_change != new_change:
        geometry = True
        def_change = new_change

    tp = ligands[4]
    ligands[4] = ligands[def_change]
    ligands[def_change] = tp

    N1 = ligands[0]
    N2 = ligands[1]
    N3 = ligands[2]
    N4 = ligands[3]
    N5 = ligands[4]
    N6 = ligands[5]

    TMN1 = N1 - TM
    TMN2 = N2 - TM
    TMN3 = N3 - TM
    TMN4 = N4 - TM
    TMN5 = N5 - TM
    TMN6 = N6 - TM

    ligands_vec = [TMN1, TMN2, TMN3, TMN4, TMN5, TMN6]

    # This loop is used to identify which N is in line with N2
    for n in range(6):
        test = linear.angle_btw_vectors(ligands_vec[1], ligands_vec[n])
        if test > (max_angle - 1):
            def_change = n

    # This loop is used to identify which N is in line with N2
    test_max = 0
    for n in range(6):
        test = linear.angle_btw_vectors(ligands_vec[1], ligands_vec[n])
        if test > test_max:
            test_max = test
            new_change = n

    if def_change != new_change:
        geometry = True
        def_change = new_change

    # Swapping the atom (n+1) just identified above with N6
    tp = ligands[5]
    ligands[5] = ligands[def_change]
    ligands[def_change] = tp

    # New atom order is stored into the N1 - N6 lists
    N1 = ligands[0]
    N2 = ligands[1]
    N3 = ligands[2]
    N4 = ligands[3]
    N5 = ligands[4]
    N6 = ligands[5]

    TMN1 = N1 - TM
    TMN2 = N2 - TM
    TMN3 = N3 - TM
    TMN4 = N4 - TM
    TMN5 = N5 - TM
    TMN6 = N6 - TM

    ligands_vec = [TMN1, TMN2, TMN3, TMN4, TMN5, TMN6]

    # This loop is used to identify which N is in line with N3
    for n in range(6):
        test = linear.angle_btw_vectors(ligands_vec[2], ligands_vec[n])
        if test > (max_angle - 1):
            def_change = n

    # This loop is used to identify which N is in line with N3
    test_max = 0
    for n in range(6):
        test = linear.angle_btw_vectors(ligands_vec[2], ligands_vec[n])
        if test > test_max:
            test_max = test
            new_change = n

    if def_change != new_change:
        geometry = True
        def_change = new_change

    # Swapping of the atom (n+1) just identified above with N4
    tp = ligands[3]
    ligands[3] = ligands[def_change]
    ligands[def_change] = tp

    # New atom order is stored into the N1 - N6 lists
    N1 = ligands[0]
    N2 = ligands[1]
    N3 = ligands[2]
    N4 = ligands[3]
    N5 = ligands[4]
    N6 = ligands[5]

    #####################################################
    # Calculate the Theta parameter ans its derivatives #
    #####################################################

    eqOfPlane = []
    indiTheta = []
    allTheta = []

    # loop over 8 faces
    for proj in range(8):
        a, b, c, d = octadist.src.plane.find_eq_of_plane(N1, N2, N3)
        eqOfPlane.append([a, b, c, d])

        # Project M, N4, N5, and N6 onto the plane defined by N1, N2, and N3
        TMP = projection.project_atom_onto_plane(TM, a, b, c, d)
        N4P = projection.project_atom_onto_plane(N4, a, b, c, d)
        N5P = projection.project_atom_onto_plane(N5, a, b, c, d)
        N6P = projection.project_atom_onto_plane(N6, a, b, c, d)

        VTh1 = N1 - TMP
        VTh2 = N2 - TMP
        VTh3 = N3 - TMP
        VTh4 = N4P - TMP
        VTh5 = N5P - TMP
        VTh6 = N6P - TMP

        a12 = linear.angle_btw_vectors(VTh1, VTh2)
        a13 = linear.angle_btw_vectors(VTh1, VTh3)
        if a12 < a13:
            direction = np.cross(VTh1, VTh2)
        else:
            direction = np.cross(VTh3, VTh1)

        theta1 = linear.angle_sign(VTh1, VTh4, direction)
        theta2 = linear.angle_sign(VTh4, VTh2, direction)
        theta3 = linear.angle_sign(VTh2, VTh5, direction)
        theta4 = linear.angle_sign(VTh5, VTh3, direction)
        theta5 = linear.angle_sign(VTh3, VTh6, direction)
        theta6 = linear.angle_sign(VTh6, VTh1, direction)

        indiTheta.append([theta1, theta2, theta3, theta4, theta5, theta6])

        sum_theta = sum(abs(indiTheta[proj][i] - 60) for i in range(6))

        allTheta.append(sum_theta)

        tp = N2
        N2 = N4
        N4 = N6
        N6 = N3
        N3 = tp

        # If the proj = 3, permutation face will be switched from N1N2N3
        # to N1N4N2, to N1N6N4, then to N1N3N6, and then back to N1N2N3
        if proj == 3:
            tp = N1
            N1 = N5
            N5 = tp
            tp = N2
            N2 = N6
            N6 = tp
            tp = N3
            N3 = N4
            N4 = tp

        # End of the loop that calculate the 8 projections.

    theta_mean = sum(allTheta[i] for i in range(8)) / 2

    # If geometry is True, the structure is non-octahedron
    if geometry:
        print("Non-octahedral structure detected!")

    return theta_mean