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()
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
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
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
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