def calc_bond_angle(c_octa): """ Calculate 12 cis and 3 trans unique angles in octahedral structure. Parameters ---------- c_octa : array or list Atomic coordinates of octahedral structure. Returns ------- cis_angle : list List of 12 cis angles. trans_angle : list List of 3 trans angles. 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]] >>> cis, trans = calc_bond_angle(coord) >>> cis [82.75749025175044, 83.11074412601039, 87.07433386260743, 87.217426970301, 87.59010057569893, 88.49485029114034, 89.71529920540053, 94.09217259687905, 94.2481644447923, 94.51379613736219, 95.40490801797102, 95.62773246517462] >>> trans [173.8820603662232, 176.07966588060893, 176.58468599461276] """ if type(c_octa) == np.ndarray: pass else: c_octa = np.asarray(c_octa) all_angle = [] for i in range(1, 7): for j in range(i + 1, 7): vec1 = c_octa[i] - c_octa[0] vec2 = c_octa[j] - c_octa[0] angle = linear.angle_btw_vectors(vec1, vec2) all_angle.append(angle) # Sort the angle from the lowest to the highest sorted_angle = sorted(all_angle) cis_angle = [sorted_angle[i] for i in range(12)] trans_angle = [sorted_angle[i] for i in range(12, 15)] return cis_angle, trans_angle
def add_coord(self, atom, coord): """ Add atomic symbols and coordinates to box. Parameters ---------- atom : array_like Atomic labels of full complex. coord : array_like Atomic coordinates of full complex. """ self.box.insert(tk.END, "Bond distance (Å)") # Bond distance for i in range(len(coord)): for j in range(i + 1, len(coord)): dist = distance.euclidean(coord[i], coord[j]) if i == 0: texts = f"{atom[i]}-{atom[j]}{j}\t\t{dist:10.6f}" else: texts = f"{atom[i]}{i}-{atom[j]}{j}\t\t{dist:10.6f}" self.box.insert(tk.END, "\n" + texts) self.box.insert(tk.END, "\n\nBond angle (°)") # Bond angle. for i in range(len(coord)): for j in range(i + 1, len(coord)): for k in range(j + 1, 7): vec1 = coord[i] - coord[j] vec2 = coord[k] - coord[j] angle = linear.angle_btw_vectors(vec1, vec2) if i == 0: texts = f"{atom[k]}{k}-{atom[i]}-{atom[j]}{j}\t\t{angle:10.6f}" else: texts = ( f"{atom[k]}{k}-{atom[i]}{i}-{atom[j]}{j}\t\t{angle:10.6f}" ) self.box.insert(tk.END, "\n" + texts) self.box.insert(tk.END, "\n\n=================================\n\n")
def calc_bond_angle(self): """ Calculate 12 cis and 3 trans unique angles in octahedral structure. See Also -------- calc_sigma : Calculate Sigma parameter. """ all_angle = [] for i in range(1, 7): for j in range(i + 1, 7): vec1 = self.coord[i] - self.coord[0] vec2 = self.coord[j] - self.coord[0] angle = linear.angle_btw_vectors(vec1, vec2) all_angle.append(angle) # Sort the angle from the lowest to the highest sorted_angle = sorted(all_angle) self.cis_angle = [sorted_angle[i] for i in range(12)] self.trans_angle = [sorted_angle[i] for i in range(12, 15)]
def param_octa(aco): """ Show structural parameters of selected octahedral structure. Parameters ---------- aco : list Atomic labels and coordinates of octahedral structure. Returns ------- None : None """ master = tk.Tk() master.title("Results") master.geometry("380x530") master.option_add("*Font", "Arial 10") frame = tk.Frame(master) frame.grid() lbl = tk.Label(frame, text="Structural parameters of octahedral structure") lbl.grid(row=0, pady="5", padx="5", sticky=tk.W) box = tkscrolled.ScrolledText(frame, wrap="word", width="50", height="30", undo="True") box.grid(row=1, pady="5", padx="5") for n in range(len(aco)): if n > 0: # separator between files box.insert(tk.END, "\n\n=================================\n\n") box.insert(tk.INSERT, f"File : {aco[n][0]}\n") box.insert(tk.END, f"Metal: {aco[n][1]}\n") box.insert(tk.END, "Bond distance (Å)") for i in range(7): for j in range(i + 1, 7): distance = linear.euclidean_dist(aco[n][3][i], aco[n][3][j]) if i == 0: texts = f"{aco[n][2][i]}-{aco[n][2][j]}{j} {distance:10.6f}" else: texts = f"{aco[n][2][i]}{i}-{aco[n][2][j]}{j} {distance:10.6f}" box.insert(tk.END, "\n" + texts) box.insert(tk.END, "\n\nBond angle (°)") for i in range(7): for j in range(i + 1, 7): for k in range(j + 1, 7): vec1 = aco[n][3][i] - aco[n][3][j] vec2 = aco[n][3][k] - aco[n][3][j] angle = linear.angle_btw_vectors(vec1, vec2) if i == 0: texts = f"{aco[n][2][k]}{k}-{aco[n][2][i]}-{aco[n][2][j]}{j} {angle:10.6f}" else: texts = f"{aco[n][2][k]}{k}-{aco[n][2][i]}{i}-{aco[n][2][j]}{j} {angle:10.6f}" box.insert(tk.END, "\n" + texts) box.insert(tk.END, "\n")
def param_complex(acf): """ Show structural parameters of the complex. Parameters ---------- acf : list Atomic labels and coordinates of full complex. Returns ------- None : None """ master = tk.Tk() master.title("Results") master.geometry("380x530") master.option_add("*Font", "Arial 10") frame = tk.Frame(master) frame.grid() lbl = tk.Label(frame, text="Structural parameters of octahedral structure") lbl.grid(row=0, pady="5", padx="5", sticky=tk.W) box = tkscrolled.ScrolledText(frame, wrap="word", width="50", height="30", undo="True") box.grid(row=1, pady="5", padx="5") box.insert(tk.INSERT, "Bond distance (Å)") fal, fcl = acf[0] for i in range(len(fcl)): for j in range(i + 1, len(fcl)): distance = linear.euclidean_dist(fcl[i], fcl[j]) if i == 0: texts = f"{fal[i]}-{fal[j]}{j} {distance:10.6f}" else: texts = f"{fal[i]}{i}-{fal[j]}{j} {distance:10.6f}" box.insert(tk.END, "\n" + texts) box.insert(tk.END, "\n\nBond angle (°)") for i in range(len(fcl)): for j in range(i + 1, len(fcl)): for k in range(j + 1, len(fcl)): vec1 = fcl[i] - fcl[j] vec2 = fcl[k] - fcl[j] angle = linear.angle_btw_vectors(vec1, vec2) if i == 0: texts = f"{fal[k]}{k}-{fal[i]}-{fal[j]}{j} {angle:10.6f}" else: texts = f"{fal[k]}{k}-{fal[i]}{i}-{fal[j]}{j} {angle:10.6f}" box.insert(tk.END, "\n" + texts) box.insert(tk.END, "\n")
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 determine_faces(self): """ Refine the order of ligand atoms in order to find the plane for projection. Returns ------- coord_metal : nd.array Coordinate of metal atom. coord_lig : nd.array Coordinate of ligand atoms. See Also -------- calc_theta : Calculate mean Theta parameter Examples -------- >>> bef = np.array([[4.0674, 7.2040, 13.6117] [4.3033, 7.3750, 11.7292] [3.8326, 6.9715, 15.4926] [5.8822, 6.4461, 13.4312] [3.3002, 5.3828, 13.6316] [4.8055, 8.9318, 14.2716] [2.3184, 8.0165, 13.1152] ]) >>> metal, coord = self.determine_faces(bef) >>> metal [ 4.0674 7.204 13.6117] >>> coord_lig [[ 4.3033 7.375 11.7292] # Front face [ 4.8055 8.9318 14.2716] # Front face [ 5.8822 6.4461 13.4312] # Front face [ 2.3184 8.0165 13.1152] # Back face [ 3.8326 6.9715 15.4926] # Back face [ 3.3002 5.3828 13.6316]] # Back face """ # Metal and ligand atoms coord_metal = self.coord[0] ligands = self.coord[1:] coord_lig = np.array([self.coord[i] for i in range(1, 7)]) # Find vector from metal to ligand atoms metal_to_lig = coord_lig - coord_metal # Find maximum angle max_angle = self.trans_angle[0] # Identify which N is in line with ligand 1 def_change = 6 for n in range(6): test = linear.angle_btw_vectors(metal_to_lig[0], metal_to_lig[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(metal_to_lig[0], metal_to_lig[n]) if test > test_max: test_max = test new_change = n # Check if the structure is octahedron or not if def_change != new_change: self.non_octa = True def_change = new_change # Swap ligand # As ligands in 2-dimensions array, we use the following technique # to swap two lists of elements with avoiding passing reference value ligands[[4, def_change]] = ligands[[def_change, 4]] # Update vector from metal to ligand atoms metal_to_lig = ligands - coord_metal # Identify which N is in line with ligand 2 for n in range(6): test = linear.angle_btw_vectors(metal_to_lig[1], metal_to_lig[n]) if test > (max_angle - 1): def_change = n test_max = 0 for n in range(6): test = linear.angle_btw_vectors(metal_to_lig[1], metal_to_lig[n]) if test > test_max: test_max = test new_change = n if def_change != new_change: self.non_octa = True def_change = new_change # Swap ligand ligands[[5, def_change]] = ligands[[def_change, 5]] # Update vector from metal to ligand atoms metal_to_lig = ligands - coord_metal # Identify which N is in line with ligand 3 for n in range(6): test = linear.angle_btw_vectors(metal_to_lig[2], metal_to_lig[n]) if test > (max_angle - 1): def_change = n test_max = 0 for n in range(6): test = linear.angle_btw_vectors(metal_to_lig[2], metal_to_lig[n]) if test > test_max: test_max = test new_change = n if def_change != new_change: self.non_octa = True def_change = new_change # Swap ligand ligands[[3, def_change]] = ligands[[def_change, 3]] # New atom order coord_lig = np.array([ligands[i] for i in range(6)]) return coord_metal, coord_lig
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