MultiOptPy 1.20.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. multioptpy/Calculator/__init__.py +0 -0
  2. multioptpy/Calculator/ase_calculation_tools.py +424 -0
  3. multioptpy/Calculator/ase_tools/__init__.py +0 -0
  4. multioptpy/Calculator/ase_tools/fairchem.py +28 -0
  5. multioptpy/Calculator/ase_tools/gamess.py +19 -0
  6. multioptpy/Calculator/ase_tools/gaussian.py +165 -0
  7. multioptpy/Calculator/ase_tools/mace.py +28 -0
  8. multioptpy/Calculator/ase_tools/mopac.py +19 -0
  9. multioptpy/Calculator/ase_tools/nwchem.py +31 -0
  10. multioptpy/Calculator/ase_tools/orca.py +22 -0
  11. multioptpy/Calculator/ase_tools/pygfn0.py +37 -0
  12. multioptpy/Calculator/dxtb_calculation_tools.py +344 -0
  13. multioptpy/Calculator/emt_calculation_tools.py +458 -0
  14. multioptpy/Calculator/gpaw_calculation_tools.py +183 -0
  15. multioptpy/Calculator/lj_calculation_tools.py +314 -0
  16. multioptpy/Calculator/psi4_calculation_tools.py +334 -0
  17. multioptpy/Calculator/pwscf_calculation_tools.py +189 -0
  18. multioptpy/Calculator/pyscf_calculation_tools.py +327 -0
  19. multioptpy/Calculator/sqm1_calculation_tools.py +611 -0
  20. multioptpy/Calculator/sqm2_calculation_tools.py +376 -0
  21. multioptpy/Calculator/tblite_calculation_tools.py +352 -0
  22. multioptpy/Calculator/tersoff_calculation_tools.py +818 -0
  23. multioptpy/Constraint/__init__.py +0 -0
  24. multioptpy/Constraint/constraint_condition.py +834 -0
  25. multioptpy/Coordinate/__init__.py +0 -0
  26. multioptpy/Coordinate/polar_coordinate.py +199 -0
  27. multioptpy/Coordinate/redundant_coordinate.py +638 -0
  28. multioptpy/IRC/__init__.py +0 -0
  29. multioptpy/IRC/converge_criteria.py +28 -0
  30. multioptpy/IRC/dvv.py +544 -0
  31. multioptpy/IRC/euler.py +439 -0
  32. multioptpy/IRC/hpc.py +564 -0
  33. multioptpy/IRC/lqa.py +540 -0
  34. multioptpy/IRC/modekill.py +662 -0
  35. multioptpy/IRC/rk4.py +579 -0
  36. multioptpy/Interpolation/__init__.py +0 -0
  37. multioptpy/Interpolation/adaptive_interpolation.py +283 -0
  38. multioptpy/Interpolation/binomial_interpolation.py +179 -0
  39. multioptpy/Interpolation/geodesic_interpolation.py +785 -0
  40. multioptpy/Interpolation/interpolation.py +156 -0
  41. multioptpy/Interpolation/linear_interpolation.py +473 -0
  42. multioptpy/Interpolation/savitzky_golay_interpolation.py +252 -0
  43. multioptpy/Interpolation/spline_interpolation.py +353 -0
  44. multioptpy/MD/__init__.py +0 -0
  45. multioptpy/MD/thermostat.py +185 -0
  46. multioptpy/MEP/__init__.py +0 -0
  47. multioptpy/MEP/pathopt_bneb_force.py +443 -0
  48. multioptpy/MEP/pathopt_dmf_force.py +448 -0
  49. multioptpy/MEP/pathopt_dneb_force.py +130 -0
  50. multioptpy/MEP/pathopt_ewbneb_force.py +207 -0
  51. multioptpy/MEP/pathopt_gpneb_force.py +512 -0
  52. multioptpy/MEP/pathopt_lup_force.py +113 -0
  53. multioptpy/MEP/pathopt_neb_force.py +225 -0
  54. multioptpy/MEP/pathopt_nesb_force.py +205 -0
  55. multioptpy/MEP/pathopt_om_force.py +153 -0
  56. multioptpy/MEP/pathopt_qsm_force.py +174 -0
  57. multioptpy/MEP/pathopt_qsmv2_force.py +304 -0
  58. multioptpy/ModelFunction/__init__.py +7 -0
  59. multioptpy/ModelFunction/avoiding_model_function.py +29 -0
  60. multioptpy/ModelFunction/binary_image_ts_search_model_function.py +47 -0
  61. multioptpy/ModelFunction/conical_model_function.py +26 -0
  62. multioptpy/ModelFunction/opt_meci.py +50 -0
  63. multioptpy/ModelFunction/opt_mesx.py +47 -0
  64. multioptpy/ModelFunction/opt_mesx_2.py +49 -0
  65. multioptpy/ModelFunction/seam_model_function.py +27 -0
  66. multioptpy/ModelHessian/__init__.py +0 -0
  67. multioptpy/ModelHessian/approx_hessian.py +147 -0
  68. multioptpy/ModelHessian/calc_params.py +227 -0
  69. multioptpy/ModelHessian/fischer.py +236 -0
  70. multioptpy/ModelHessian/fischerd3.py +360 -0
  71. multioptpy/ModelHessian/fischerd4.py +398 -0
  72. multioptpy/ModelHessian/gfn0xtb.py +633 -0
  73. multioptpy/ModelHessian/gfnff.py +709 -0
  74. multioptpy/ModelHessian/lindh.py +165 -0
  75. multioptpy/ModelHessian/lindh2007d2.py +707 -0
  76. multioptpy/ModelHessian/lindh2007d3.py +822 -0
  77. multioptpy/ModelHessian/lindh2007d4.py +1030 -0
  78. multioptpy/ModelHessian/morse.py +106 -0
  79. multioptpy/ModelHessian/schlegel.py +144 -0
  80. multioptpy/ModelHessian/schlegeld3.py +322 -0
  81. multioptpy/ModelHessian/schlegeld4.py +559 -0
  82. multioptpy/ModelHessian/shortrange.py +346 -0
  83. multioptpy/ModelHessian/swartd2.py +496 -0
  84. multioptpy/ModelHessian/swartd3.py +706 -0
  85. multioptpy/ModelHessian/swartd4.py +918 -0
  86. multioptpy/ModelHessian/tshess.py +40 -0
  87. multioptpy/Optimizer/QHAdam.py +61 -0
  88. multioptpy/Optimizer/__init__.py +0 -0
  89. multioptpy/Optimizer/abc_fire.py +83 -0
  90. multioptpy/Optimizer/adabelief.py +58 -0
  91. multioptpy/Optimizer/adabound.py +68 -0
  92. multioptpy/Optimizer/adadelta.py +65 -0
  93. multioptpy/Optimizer/adaderivative.py +56 -0
  94. multioptpy/Optimizer/adadiff.py +68 -0
  95. multioptpy/Optimizer/adafactor.py +70 -0
  96. multioptpy/Optimizer/adam.py +65 -0
  97. multioptpy/Optimizer/adamax.py +62 -0
  98. multioptpy/Optimizer/adamod.py +83 -0
  99. multioptpy/Optimizer/adamw.py +65 -0
  100. multioptpy/Optimizer/adiis.py +523 -0
  101. multioptpy/Optimizer/afire_neb.py +282 -0
  102. multioptpy/Optimizer/block_hessian_update.py +709 -0
  103. multioptpy/Optimizer/c2diis.py +491 -0
  104. multioptpy/Optimizer/component_wise_scaling.py +405 -0
  105. multioptpy/Optimizer/conjugate_gradient.py +82 -0
  106. multioptpy/Optimizer/conjugate_gradient_neb.py +345 -0
  107. multioptpy/Optimizer/coordinate_locking.py +405 -0
  108. multioptpy/Optimizer/dic_rsirfo.py +1015 -0
  109. multioptpy/Optimizer/ediis.py +417 -0
  110. multioptpy/Optimizer/eve.py +76 -0
  111. multioptpy/Optimizer/fastadabelief.py +61 -0
  112. multioptpy/Optimizer/fire.py +77 -0
  113. multioptpy/Optimizer/fire2.py +249 -0
  114. multioptpy/Optimizer/fire_neb.py +92 -0
  115. multioptpy/Optimizer/gan_step.py +486 -0
  116. multioptpy/Optimizer/gdiis.py +609 -0
  117. multioptpy/Optimizer/gediis.py +203 -0
  118. multioptpy/Optimizer/geodesic_step.py +433 -0
  119. multioptpy/Optimizer/gpmin.py +633 -0
  120. multioptpy/Optimizer/gpr_step.py +364 -0
  121. multioptpy/Optimizer/gradientdescent.py +78 -0
  122. multioptpy/Optimizer/gradientdescent_neb.py +52 -0
  123. multioptpy/Optimizer/hessian_update.py +433 -0
  124. multioptpy/Optimizer/hybrid_rfo.py +998 -0
  125. multioptpy/Optimizer/kdiis.py +625 -0
  126. multioptpy/Optimizer/lars.py +21 -0
  127. multioptpy/Optimizer/lbfgs.py +253 -0
  128. multioptpy/Optimizer/lbfgs_neb.py +355 -0
  129. multioptpy/Optimizer/linesearch.py +236 -0
  130. multioptpy/Optimizer/lookahead.py +40 -0
  131. multioptpy/Optimizer/nadam.py +64 -0
  132. multioptpy/Optimizer/newton.py +200 -0
  133. multioptpy/Optimizer/prodigy.py +70 -0
  134. multioptpy/Optimizer/purtubation.py +16 -0
  135. multioptpy/Optimizer/quickmin_neb.py +245 -0
  136. multioptpy/Optimizer/radam.py +75 -0
  137. multioptpy/Optimizer/rfo_neb.py +302 -0
  138. multioptpy/Optimizer/ric_rfo.py +842 -0
  139. multioptpy/Optimizer/rl_step.py +627 -0
  140. multioptpy/Optimizer/rmspropgrave.py +65 -0
  141. multioptpy/Optimizer/rsirfo.py +1647 -0
  142. multioptpy/Optimizer/rsprfo.py +1056 -0
  143. multioptpy/Optimizer/sadam.py +60 -0
  144. multioptpy/Optimizer/samsgrad.py +63 -0
  145. multioptpy/Optimizer/tr_lbfgs.py +678 -0
  146. multioptpy/Optimizer/trim.py +273 -0
  147. multioptpy/Optimizer/trust_radius.py +207 -0
  148. multioptpy/Optimizer/trust_radius_neb.py +121 -0
  149. multioptpy/Optimizer/yogi.py +60 -0
  150. multioptpy/OtherMethod/__init__.py +0 -0
  151. multioptpy/OtherMethod/addf.py +1150 -0
  152. multioptpy/OtherMethod/dimer.py +895 -0
  153. multioptpy/OtherMethod/elastic_image_pair.py +629 -0
  154. multioptpy/OtherMethod/modelfunction.py +456 -0
  155. multioptpy/OtherMethod/newton_traj.py +454 -0
  156. multioptpy/OtherMethod/twopshs.py +1095 -0
  157. multioptpy/PESAnalyzer/__init__.py +0 -0
  158. multioptpy/PESAnalyzer/calc_irc_curvature.py +125 -0
  159. multioptpy/PESAnalyzer/cmds_analysis.py +152 -0
  160. multioptpy/PESAnalyzer/koopman_analysis.py +268 -0
  161. multioptpy/PESAnalyzer/pca_analysis.py +314 -0
  162. multioptpy/Parameters/__init__.py +0 -0
  163. multioptpy/Parameters/atomic_mass.py +20 -0
  164. multioptpy/Parameters/atomic_number.py +22 -0
  165. multioptpy/Parameters/covalent_radii.py +44 -0
  166. multioptpy/Parameters/d2.py +61 -0
  167. multioptpy/Parameters/d3.py +63 -0
  168. multioptpy/Parameters/d4.py +103 -0
  169. multioptpy/Parameters/dreiding.py +34 -0
  170. multioptpy/Parameters/gfn0xtb_param.py +137 -0
  171. multioptpy/Parameters/gfnff_param.py +315 -0
  172. multioptpy/Parameters/gnb.py +104 -0
  173. multioptpy/Parameters/parameter.py +22 -0
  174. multioptpy/Parameters/uff.py +72 -0
  175. multioptpy/Parameters/unit_values.py +20 -0
  176. multioptpy/Potential/AFIR_potential.py +55 -0
  177. multioptpy/Potential/LJ_repulsive_potential.py +345 -0
  178. multioptpy/Potential/__init__.py +0 -0
  179. multioptpy/Potential/anharmonic_keep_potential.py +28 -0
  180. multioptpy/Potential/asym_elllipsoidal_potential.py +718 -0
  181. multioptpy/Potential/electrostatic_potential.py +69 -0
  182. multioptpy/Potential/flux_potential.py +30 -0
  183. multioptpy/Potential/gaussian_potential.py +101 -0
  184. multioptpy/Potential/idpp.py +516 -0
  185. multioptpy/Potential/keep_angle_potential.py +146 -0
  186. multioptpy/Potential/keep_dihedral_angle_potential.py +105 -0
  187. multioptpy/Potential/keep_outofplain_angle_potential.py +70 -0
  188. multioptpy/Potential/keep_potential.py +99 -0
  189. multioptpy/Potential/mechano_force_potential.py +74 -0
  190. multioptpy/Potential/nanoreactor_potential.py +52 -0
  191. multioptpy/Potential/potential.py +896 -0
  192. multioptpy/Potential/spacer_model_potential.py +221 -0
  193. multioptpy/Potential/switching_potential.py +258 -0
  194. multioptpy/Potential/universal_potential.py +34 -0
  195. multioptpy/Potential/value_range_potential.py +36 -0
  196. multioptpy/Potential/void_point_potential.py +25 -0
  197. multioptpy/SQM/__init__.py +0 -0
  198. multioptpy/SQM/sqm1/__init__.py +0 -0
  199. multioptpy/SQM/sqm1/sqm1_core.py +1792 -0
  200. multioptpy/SQM/sqm2/__init__.py +0 -0
  201. multioptpy/SQM/sqm2/calc_tools.py +95 -0
  202. multioptpy/SQM/sqm2/sqm2_basis.py +850 -0
  203. multioptpy/SQM/sqm2/sqm2_bond.py +119 -0
  204. multioptpy/SQM/sqm2/sqm2_core.py +303 -0
  205. multioptpy/SQM/sqm2/sqm2_data.py +1229 -0
  206. multioptpy/SQM/sqm2/sqm2_disp.py +65 -0
  207. multioptpy/SQM/sqm2/sqm2_eeq.py +243 -0
  208. multioptpy/SQM/sqm2/sqm2_overlapint.py +704 -0
  209. multioptpy/SQM/sqm2/sqm2_qm.py +578 -0
  210. multioptpy/SQM/sqm2/sqm2_rep.py +66 -0
  211. multioptpy/SQM/sqm2/sqm2_srb.py +70 -0
  212. multioptpy/Thermo/__init__.py +0 -0
  213. multioptpy/Thermo/normal_mode_analyzer.py +865 -0
  214. multioptpy/Utils/__init__.py +0 -0
  215. multioptpy/Utils/bond_connectivity.py +264 -0
  216. multioptpy/Utils/calc_tools.py +884 -0
  217. multioptpy/Utils/oniom.py +96 -0
  218. multioptpy/Utils/pbc.py +48 -0
  219. multioptpy/Utils/riemann_curvature.py +208 -0
  220. multioptpy/Utils/symmetry_analyzer.py +482 -0
  221. multioptpy/Visualization/__init__.py +0 -0
  222. multioptpy/Visualization/visualization.py +156 -0
  223. multioptpy/WFAnalyzer/MO_analysis.py +104 -0
  224. multioptpy/WFAnalyzer/__init__.py +0 -0
  225. multioptpy/Wrapper/__init__.py +0 -0
  226. multioptpy/Wrapper/autots.py +1239 -0
  227. multioptpy/Wrapper/ieip_wrapper.py +93 -0
  228. multioptpy/Wrapper/md_wrapper.py +92 -0
  229. multioptpy/Wrapper/neb_wrapper.py +94 -0
  230. multioptpy/Wrapper/optimize_wrapper.py +76 -0
  231. multioptpy/__init__.py +5 -0
  232. multioptpy/entrypoints.py +916 -0
  233. multioptpy/fileio.py +660 -0
  234. multioptpy/ieip.py +340 -0
  235. multioptpy/interface.py +1086 -0
  236. multioptpy/irc.py +529 -0
  237. multioptpy/moleculardynamics.py +432 -0
  238. multioptpy/neb.py +1267 -0
  239. multioptpy/optimization.py +1553 -0
  240. multioptpy/optimizer.py +709 -0
  241. multioptpy-1.20.2.dist-info/METADATA +438 -0
  242. multioptpy-1.20.2.dist-info/RECORD +246 -0
  243. multioptpy-1.20.2.dist-info/WHEEL +5 -0
  244. multioptpy-1.20.2.dist-info/entry_points.txt +9 -0
  245. multioptpy-1.20.2.dist-info/licenses/LICENSE +674 -0
  246. multioptpy-1.20.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,482 @@
1
+ import numpy as np
2
+ from scipy.spatial import distance_matrix
3
+ from scipy.optimize import linear_sum_assignment
4
+
5
+
6
+ def rotation_matrix(axis, theta):
7
+ """
8
+ Create rotation matrix for rotation around an axis by angle theta
9
+
10
+ Parameters
11
+ ----------
12
+ axis : np.ndarray
13
+ Axis of rotation (3D unit vector)
14
+ theta : float
15
+ Angle of rotation in radians
16
+
17
+ Returns
18
+ -------
19
+ np.ndarray
20
+ 3x3 rotation matrix
21
+ """
22
+ # Ensure axis is normalized
23
+ axis = axis / np.linalg.norm(axis)
24
+
25
+ # Components of axis
26
+ x, y, z = axis
27
+
28
+ # Compute rotation matrix using axis-angle formula
29
+ c = np.cos(theta)
30
+ s = np.sin(theta)
31
+ t = 1 - c
32
+
33
+ matrix = np.array([
34
+ [t*x*x + c, t*x*y - s*z, t*x*z + s*y],
35
+ [t*x*y + s*z, t*y*y + c, t*y*z - s*x],
36
+ [t*x*z - s*y, t*y*z + s*x, t*z*z + c]
37
+ ])
38
+
39
+ return matrix
40
+
41
+
42
+ class SymmetryAnalyzer:
43
+ """Analyzer for molecular symmetry elements and point group determination"""
44
+
45
+ def __init__(self, atoms, coordinates, tol=1e-2, angle_tol=1e-4, max_n_fold=6):
46
+ """
47
+ Parameters
48
+ ----------
49
+ atoms : list of str
50
+ List of atomic symbols
51
+ coordinates : np.ndarray
52
+ Atomic coordinates (n_atoms, 3)
53
+ tol : float
54
+ Distance tolerance for matching atoms (e.g., in Angstroms)
55
+ angle_tol : float
56
+ Tolerance for comparing axis vectors (dot product deviation from 1.0)
57
+ max_n_fold : int
58
+ Maximum n-fold rotation to check
59
+ """
60
+
61
+ self.atoms = atoms
62
+ if coordinates is None or len(coordinates) == 0:
63
+ raise ValueError("Valid coordinates must be provided.")
64
+
65
+ self.coordinates = np.array(coordinates, dtype=np.float64)
66
+ self.n_atoms = len(atoms)
67
+ self.tol = tol
68
+ self.angle_tol = angle_tol
69
+ self.inertia_tol = 1e-6
70
+ self.max_n_fold = max_n_fold
71
+
72
+ # Center molecule at COM
73
+ self.com = np.mean(self.coordinates, axis=0)
74
+ self.coordinates -= self.com
75
+
76
+ # Group coordinates by atom type for efficient checking
77
+ self.pcoords = self._create_pcoords()
78
+
79
+ # Symmetry elements
80
+ self.cn_axes = {}
81
+ self.reflection_planes = []
82
+ self.has_inversion = False
83
+ self.sn_axes = {}
84
+
85
+ # Properties
86
+ self.is_linear_mol = self._check_linearity()
87
+
88
+ def _check_linearity(self):
89
+ """Check if molecule is linear based on moments of inertia"""
90
+ if self.n_atoms <= 2:
91
+ return True
92
+
93
+ # Calculate principal moments of inertia
94
+ inertia = np.zeros((3, 3))
95
+ for i in range(self.n_atoms):
96
+ r = self.coordinates[i]
97
+ r2 = np.sum(r * r)
98
+ # We don't have atomic masses, so assume all are 1.
99
+ # This is fine for symmetry, as we only care about the *ratio* of moments.
100
+ inertia += r2 * np.eye(3) - np.outer(r, r)
101
+
102
+ evals = np.linalg.eigvalsh(inertia)
103
+
104
+ # For a linear molecule, one moment is zero, and the other two are equal.
105
+ # evals[0] is the smallest.
106
+ return evals[0] < self.inertia_tol
107
+
108
+ def _create_pcoords(self):
109
+ """
110
+ Group coordinates by atom type into a dictionary of dense arrays.
111
+ This is much faster than the previous sparse array method.
112
+ """
113
+ pcoords = {}
114
+ unique_atoms = set(self.atoms)
115
+ for atom_type in unique_atoms:
116
+ indices = [i for i, atom in enumerate(self.atoms) if atom == atom_type]
117
+ pcoords[atom_type] = self.coordinates[indices]
118
+ return pcoords
119
+
120
+ def _check_op(self, op_matrix):
121
+ """
122
+ Check if the molecule is invariant under a given 3x3 operation matrix.
123
+ This is the new, robust core of the analyzer.
124
+ """
125
+ for atom_type, coords in self.pcoords.items():
126
+ if coords.shape[0] == 0:
127
+ continue
128
+
129
+ # Apply the operation: (N_atoms, 3) @ (3, 3) -> (N_atoms, 3)
130
+ transformed_coords = coords @ op_matrix.T
131
+
132
+ # Find the distance between all original and transformed atoms
133
+ dist_mat = distance_matrix(coords, transformed_coords)
134
+
135
+ try:
136
+ # Use the Hungarian algorithm to find the *best one-to-one mapping*
137
+ # This enforces a correct permutation.
138
+ row_ind, col_ind = linear_sum_assignment(dist_mat)
139
+
140
+ # Find the largest distance in this optimal mapping
141
+ max_dist = dist_mat[row_ind, col_ind].max()
142
+
143
+ # If the largest distance is over tolerance, this operation is not a symmetry
144
+ if max_dist > self.tol:
145
+ return False
146
+ except ValueError:
147
+ # linear_sum_assignment can fail if matrix is empty or NaN
148
+ return False
149
+
150
+ # If all atom types pass, it's a valid symmetry operation
151
+ return True
152
+
153
+ def analyze(self):
154
+ """Analyze molecular symmetry and determine point group"""
155
+ # Get candidate axes *once*
156
+ candidate_axes = get_possible_axes(self.coordinates, self.com, self.angle_tol)
157
+
158
+ # Find all symmetry elements
159
+ self._find_inversion_center()
160
+ self._find_rotation_axes(candidate_axes)
161
+ self._find_reflection_planes(candidate_axes)
162
+ self._find_improper_rotation_axes(candidate_axes)
163
+
164
+ return self._determine_point_group()
165
+
166
+ def _find_rotation_axes(self, axes):
167
+ """Detect rotation axes (Cn)"""
168
+ self.cn_axes = {i: [] for i in range(2, self.max_n_fold + 1)}
169
+
170
+ for n in range(self.max_n_fold, 1, -1):
171
+ theta = 2.0 * np.pi / n
172
+ for axis in axes:
173
+ op = rotation_matrix(axis, theta)
174
+ if self._check_op(op):
175
+ self.cn_axes[n].append(axis)
176
+ # De-duplicate axes
177
+ self.cn_axes[n] = strip_identical_axes(self.cn_axes[n], self.angle_tol)
178
+
179
+ def _find_reflection_planes(self, axes):
180
+ """Detect reflection planes (σ), using axes as plane normals"""
181
+ planes = []
182
+ for normal in axes:
183
+ # Reflection matrix: I - 2 * (n @ n.T)
184
+ op = np.eye(3) - 2 * np.outer(normal, normal)
185
+ if self._check_op(op):
186
+ planes.append(normal)
187
+ self.reflection_planes = strip_identical_axes(planes, self.angle_tol)
188
+
189
+ def _find_inversion_center(self):
190
+ """Detect inversion center (i)"""
191
+ op = -np.eye(3)
192
+ self.has_inversion = self._check_op(op)
193
+
194
+ def _find_improper_rotation_axes(self, axes):
195
+ """Detect improper rotation axes (Sn)"""
196
+ self.sn_axes = {i: [] for i in range(2, self.max_n_fold + 1)}
197
+
198
+ for n in range(self.max_n_fold, 1, -1):
199
+ theta = 2.0 * np.pi / n
200
+ for axis in axes:
201
+ # S_n = C_n followed by sigma_h (reflection in plane perp to axis)
202
+ rot_op = rotation_matrix(axis, theta)
203
+ refl_op = np.eye(3) - 2 * np.outer(axis, axis)
204
+ op = refl_op @ rot_op
205
+
206
+ if self._check_op(op):
207
+ self.sn_axes[n].append(axis)
208
+ self.sn_axes[n] = strip_identical_axes(self.sn_axes[n], self.angle_tol)
209
+
210
+ def _determine_point_group(self):
211
+ """Determine point group based on detected symmetry elements (standard flowchart)"""
212
+
213
+ # 1. Linear molecules
214
+ if self.is_linear_mol:
215
+ return "D∞h" if self.has_inversion else "C∞v"
216
+
217
+ # 2. High symmetry groups
218
+ if self._has_icosahedral_symmetry():
219
+ return "Ih" # I (no inversion) is very rare
220
+
221
+ if self._has_octahedral_symmetry():
222
+ return "Oh" if self.has_inversion else "O"
223
+
224
+ if self._has_tetrahedral_symmetry():
225
+ if self.has_inversion:
226
+ return "Th"
227
+ elif len(self.reflection_planes) > 0:
228
+ # More robust check: Td has 6 reflection planes
229
+ if len(self.reflection_planes) >= 6:
230
+ return "Td"
231
+ else:
232
+ return "T" # Or Th if S4 axes exist but no planes
233
+ else:
234
+ return "T"
235
+
236
+ # 3. Find highest order rotation axis
237
+ max_n = 1
238
+ for n in range(self.max_n_fold, 1, -1):
239
+ if len(self.cn_axes[n]) > 0:
240
+ max_n = n
241
+ break
242
+
243
+ # 4. No principal axis (max_n = 1)
244
+ if max_n == 1:
245
+ if self.has_inversion:
246
+ return "Ci"
247
+ elif len(self.reflection_planes) > 0:
248
+ return "Cs"
249
+ else:
250
+ return "C1"
251
+
252
+ # 5. Has a principal C_n axis
253
+ principal_axis = self.cn_axes[max_n][0]
254
+
255
+ # Check for C2 axes perpendicular to principal axis (D groups)
256
+ perp_c2_axes = [ax for ax in self.cn_axes.get(2, [])
257
+ if abs(np.dot(ax, principal_axis)) < self.angle_tol]
258
+
259
+ if len(perp_c2_axes) >= max_n:
260
+ # --- D Groups ---
261
+ # Check for horizontal plane (sigma_h)
262
+ has_sigma_h = any(abs(np.dot(pl, principal_axis)) > (1.0 - self.angle_tol)
263
+ for pl in self.reflection_planes)
264
+ if has_sigma_h:
265
+ return f"D{max_n}h"
266
+
267
+ # Check for dihedral planes (sigma_d)
268
+ # These are vertical planes that bisect the perp. C2 axes
269
+ n_dihedral_planes = sum(1 for pl in self.reflection_planes
270
+ if abs(np.dot(pl, principal_axis)) < self.angle_tol)
271
+ if n_dihedral_planes >= max_n:
272
+ return f"D{max_n}d"
273
+
274
+ return f"D{max_n}"
275
+
276
+ else:
277
+ # --- C and S Groups ---
278
+ # Check for horizontal plane (sigma_h)
279
+ has_sigma_h = any(abs(np.dot(pl, principal_axis)) > (1.0 - self.angle_tol)
280
+ for pl in self.reflection_planes)
281
+ if has_sigma_h:
282
+ return f"C{max_n}h"
283
+
284
+ # Check for vertical planes (sigma_v)
285
+ n_vertical_planes = sum(1 for pl in self.reflection_planes
286
+ if abs(np.dot(pl, principal_axis)) < self.angle_tol)
287
+ if n_vertical_planes >= max_n:
288
+ return f"C{max_n}v"
289
+
290
+ # Check for S_2n axis coincident with C_n
291
+ n_s2n = 2 * max_n
292
+ if n_s2n in self.sn_axes:
293
+ has_s2n = any(abs(np.dot(sn_ax, principal_axis)) > (1.0 - self.angle_tol)
294
+ for sn_ax in self.sn_axes.get(n_s2n, []))
295
+ if has_s2n:
296
+ return f"S{n_s2n}"
297
+
298
+ return f"C{max_n}"
299
+
300
+ def _has_icosahedral_symmetry(self):
301
+ """Check for icosahedral symmetry"""
302
+ c5_count = len(self.cn_axes.get(5, []))
303
+ c3_count = len(self.cn_axes.get(3, []))
304
+ return c5_count >= 6 and c3_count >= 10
305
+
306
+ def _has_octahedral_symmetry(self):
307
+ """Check for octahedral symmetry"""
308
+ c4_count = len(self.cn_axes.get(4, []))
309
+ c3_count = len(self.cn_axes.get(3, []))
310
+ return c4_count >= 3 and c3_count >= 4
311
+
312
+ def _has_tetrahedral_symmetry(self):
313
+ """Check for tetrahedral symmetry"""
314
+ c3_count = len(self.cn_axes.get(3, []))
315
+ c2_count = len(self.cn_axes.get(2, []))
316
+ return c3_count >= 4 and c2_count >= 3
317
+
318
+
319
+ def analyze_symmetry(atoms, coordinates, tol=1e-2, angle_tol=1e-4, max_n_fold=6):
320
+ """
321
+ Analyze molecular symmetry and determine point group
322
+
323
+ Parameters
324
+ ----------
325
+ atoms : list of str
326
+ List of atomic symbols
327
+ coordinates : np.ndarray
328
+ Atomic coordinates (n_atoms, 3)
329
+ tol : float
330
+ Distance tolerance for matching atoms (e.g., in Angstroms)
331
+ angle_tol : float
332
+ Tolerance for comparing axis vectors (dot product deviation from 1.0)
333
+ max_n_fold : int
334
+ Maximum n-fold rotation to check
335
+
336
+ Returns
337
+ -------
338
+ str
339
+ Molecular point group
340
+ """
341
+ try:
342
+ analyzer = SymmetryAnalyzer(atoms, coordinates, tol, angle_tol, max_n_fold)
343
+ return analyzer.analyze()
344
+ except Exception as e:
345
+ print(f"Error during symmetry analysis: {e}")
346
+ return "Unknown"
347
+
348
+
349
+ # --- Support functions ---
350
+
351
+ def strip_identical_axes(axes, tol):
352
+ """Remove similar or inverse axes within tolerance using dot product"""
353
+ unique_axes = []
354
+ for axis in axes:
355
+ # Check if axis is parallel or anti-parallel to any already found
356
+ is_duplicate = False
357
+ for unique_axis in unique_axes:
358
+ dot_product = abs(np.dot(axis, unique_axis))
359
+ if dot_product > (1.0 - tol):
360
+ is_duplicate = True
361
+ break
362
+
363
+ if not is_duplicate:
364
+ unique_axes.append(axis)
365
+ return unique_axes
366
+
367
+
368
+ def get_possible_axes(coords, com, tol):
369
+ """
370
+ Get possible rotation axes in a molecule.
371
+ Now includes body-diagonals and vector sums/differences.
372
+ """
373
+ possible_axes = []
374
+ n_atoms = len(coords)
375
+
376
+ # 1. Cartesian axes
377
+ possible_axes.append(np.array([1., 0., 0.]))
378
+ possible_axes.append(np.array([0., 1., 0.]))
379
+ possible_axes.append(np.array([0., 0., 1.]))
380
+
381
+ # 2. Body diagonals (for cubic symmetries)
382
+ diag_axes = [
383
+ [1., 1., 1.], [1., 1., -1.], [1., -1., 1.], [-1., 1., 1.]
384
+ ]
385
+ for ax in diag_axes:
386
+ possible_axes.append(np.array(ax) / np.linalg.norm(ax))
387
+
388
+ # 3. Vectors from COM to each atom
389
+ for i in range(n_atoms):
390
+ vec = coords[i] # Coords are already COM-centered
391
+ norm = np.linalg.norm(vec)
392
+ if norm > 1e-6:
393
+ possible_axes.append(vec / norm)
394
+
395
+ # 4. Vectors between atom pairs and their cross/sum/diff
396
+ for i in range(n_atoms):
397
+ for j in range(i + 1, n_atoms):
398
+ # Atom-atom pair vector
399
+ vec = coords[j] - coords[i]
400
+ norm = np.linalg.norm(vec)
401
+ if norm > 1e-6:
402
+ possible_axes.append(vec / norm)
403
+
404
+ # Cross, sum, and diff of COM-atom vectors
405
+ vec1 = coords[i]
406
+ vec2 = coords[j]
407
+
408
+ cross_vec = np.cross(vec1, vec2)
409
+ norm = np.linalg.norm(cross_vec)
410
+ if norm > 1e-6:
411
+ possible_axes.append(cross_vec / norm)
412
+
413
+ sum_vec = vec1 + vec2
414
+ norm = np.linalg.norm(sum_vec)
415
+ if norm > 1e-6:
416
+ possible_axes.append(sum_vec / norm)
417
+
418
+ diff_vec = vec1 - vec2
419
+ norm = np.linalg.norm(diff_vec)
420
+ if norm > 1e-6:
421
+ possible_axes.append(diff_vec / norm)
422
+
423
+ return strip_identical_axes(possible_axes, tol)
424
+
425
+
426
+ # --- Example Usage ---
427
+
428
+ if __name__ == '__main__':
429
+ # Water (C2v)
430
+ atoms_water = ['O', 'H', 'H']
431
+ coords_water = np.array([
432
+ [0.000000, 0.000000, 0.117300],
433
+ [0.000000, 0.757200, -0.469200],
434
+ [0.000000, -0.757200, -0.469200]
435
+ ])
436
+ print(f"Molecule: Water")
437
+ print(f"Point Group: {analyze_symmetry(atoms_water, coords_water)}\n")
438
+
439
+ # Methane (Td)
440
+ atoms_methane = ['C', 'H', 'H', 'H', 'H']
441
+ coords_methane = np.array([
442
+ [0.0, 0.0, 0.0],
443
+ [0.6291, 0.6291, 0.6291],
444
+ [-0.6291, -0.6291, 0.6291],
445
+ [-0.6291, 0.6291, -0.6291],
446
+ [0.6291, -0.6291, -0.6291]
447
+ ])
448
+ print(f"Molecule: Methane")
449
+ print(f"Point Group: {analyze_symmetry(atoms_methane, coords_methane)}\n")
450
+
451
+ # Benzene (D6h)
452
+ atoms_benzene = ['C', 'C', 'C', 'C', 'C', 'C', 'H', 'H', 'H', 'H', 'H', 'H']
453
+ coords_benzene = np.array([
454
+ [ 0.0000, 1.397, 0.0], [ 1.210, 0.698, 0.0], [ 1.210, -0.698, 0.0],
455
+ [ 0.0000, -1.397, 0.0], [-1.210, -0.698, 0.0], [-1.210, 0.698, 0.0],
456
+ [ 0.0000, 2.484, 0.0], [ 2.151, 1.242, 0.0], [ 2.151, -1.242, 0.0],
457
+ [ 0.0000, -2.484, 0.0], [-2.151, -1.242, 0.0], [-2.151, 1.242, 0.0]
458
+ ])
459
+ print(f"Molecule: Benzene")
460
+ print(f"Point Group: {analyze_symmetry(atoms_benzene, coords_benzene)}\n")
461
+
462
+
463
+ # Allene (D2d)
464
+ atoms_allene = ['C', 'C', 'C', 'H', 'H', 'H', 'H']
465
+ coords_allene_new = np.array([
466
+ [ 0.0, 0.0, 0.0], [ 0.0, 0.0, 1.308], [ 0.0, 0.0, -1.308],
467
+ [ 0.0, 0.95, 1.848], [ 0.0, -0.95, 1.848],
468
+ [ 0.95, 0.0, -1.848], [-0.95, 0.0, -1.848]
469
+ ])
470
+ print(f"Molecule: Allene")
471
+ print(f"Point Group: {analyze_symmetry(atoms_allene, coords_allene_new)}\n")
472
+
473
+ # SF6 (Oh)
474
+ atoms_sf6 = ['S'] + ['F'] * 6
475
+ coords_sf6 = np.array([
476
+ [0.0, 0.0, 0.0],
477
+ [1.5, 0.0, 0.0], [-1.5, 0.0, 0.0],
478
+ [0.0, 1.5, 0.0], [0.0, -1.5, 0.0],
479
+ [0.0, 0.0, 1.5], [0.0, 0.0, -1.5]
480
+ ])
481
+ print(f"Molecule: SF6")
482
+ print(f"Point Group: {analyze_symmetry(atoms_sf6, coords_sf6)}\n")
File without changes
@@ -0,0 +1,156 @@
1
+ import matplotlib.pyplot as plt
2
+ import numpy as np
3
+
4
+ from multioptpy.Parameters.parameter import UnitValueLib
5
+
6
+ class Graph:
7
+ def __init__(self, folder_directory):
8
+ self.BPA_FOLDER_DIRECTORY = folder_directory
9
+ return
10
+
11
+ def stem_plot(self, freq_list, int_list, add_file_name=""):
12
+ fig, ax = plt.subplots()
13
+ ax.stem(freq_list, int_list)
14
+ ax.set_title(add_file_name)
15
+ y_max = np.max(int_list) + (np.max(int_list) * 0.1)
16
+ ax.set_xlim(0, 5000)
17
+ ax.set_ylim(0, y_max)
18
+ ax.set_xlabel('Wave number [/ nm]')
19
+ ax.set_ylabel('intensity [a.u.]')
20
+ fig.tight_layout()
21
+ fig.savefig(self.BPA_FOLDER_DIRECTORY+"stem_plot_"+add_file_name+".png", format="png", dpi=300)
22
+ plt.close()
23
+ return
24
+
25
+ def double_plot(self, num_list, energy_list, energy_list_2, add_file_name=""):
26
+ """
27
+ Plot two energy profiles on a single figure using primary and secondary y-axes
28
+ with proper legends.
29
+
30
+ Args:
31
+ num_list: Iteration numbers (x-axis)
32
+ energy_list: First energy profile (primary y-axis)
33
+ energy_list_2: Second energy profile (secondary y-axis)
34
+ add_file_name: Additional text for the output file name
35
+ """
36
+ fig, ax1 = plt.subplots(figsize=(10, 6))
37
+
38
+ # Primary axis (left) - Normal energy
39
+ color1 = 'green'
40
+ line1, = ax1.plot(num_list, energy_list, color=color1, linestyle='--', marker='.', label='Normal Energy')
41
+ ax1.set_xlabel('ITR.')
42
+ ax1.set_ylabel('Electronic Energy [kcal/mol]', color=color1)
43
+ ax1.tick_params(axis='y', labelcolor=color1)
44
+
45
+ # Secondary axis (right) - Bias energy
46
+ color2 = 'blue'
47
+ ax2 = ax1.twinx()
48
+ line2, = ax2.plot(num_list, energy_list_2, color=color2, linestyle='--', marker='.', label='Bias Energy')
49
+ ax2.set_ylabel('Electronic Energy [kcal/mol]', color=color2)
50
+ ax2.tick_params(axis='y', labelcolor=color2)
51
+
52
+ # Add legend
53
+ lines = [line1, line2]
54
+ labels = [line.get_label() for line in lines]
55
+ ax1.legend(lines, labels, loc='best')
56
+
57
+ # Title and layout
58
+ plt.title('Energy Profile')
59
+ plt.grid(True, linestyle='--', alpha=0.7)
60
+ plt.tight_layout()
61
+
62
+ # Save figure
63
+ plt.savefig(self.BPA_FOLDER_DIRECTORY + "energy_plot_" + add_file_name + ".png", format="png", dpi=300)
64
+ plt.close()
65
+ return
66
+
67
+ def single_plot(self, num_list, energy_list, file_directory, atom_num, axis_name_1="ITR. ", axis_name_2="cosθ", name="orthogonality"):
68
+ fig, ax = plt.subplots()
69
+ ax.plot(num_list,energy_list, "b--o" , markersize=3)
70
+
71
+ ax.set_title(str(atom_num))
72
+ ax.set_xlabel(axis_name_1)
73
+ ax.set_ylabel(axis_name_2)
74
+ fig.tight_layout()
75
+ fig.savefig(self.BPA_FOLDER_DIRECTORY+"plot_"+name+"_"+str(atom_num)+".png", format="png", dpi=200)
76
+ plt.close()
77
+
78
+ return
79
+
80
+
81
+
82
+ class NEBVisualizer:
83
+ """Visualization functionality for NEB calculations"""
84
+
85
+ def __init__(self, config):
86
+ self.config = config
87
+ self.color_list = ["b"] # for matplotlib
88
+
89
+ def simple_plot(self, num_list, data_list, file_directory, optimize_num,
90
+ axis_name_1="NODE #", axis_name_2="Value", name="data"):
91
+ """Create a simple plot"""
92
+ fig, ax = plt.subplots()
93
+ ax.plot(num_list, data_list,
94
+ self.color_list[0] + "--o")
95
+
96
+ ax.set_title(str(optimize_num))
97
+ ax.set_xlabel(axis_name_1)
98
+ ax.set_ylabel(axis_name_2)
99
+ fig.tight_layout()
100
+ fig.savefig(f"{self.config.NEB_FOLDER_DIRECTORY}plot_{name}_{optimize_num}.png",
101
+ format="png", dpi=200)
102
+ plt.close()
103
+
104
+ def simple_scatter_plot(self, num_list, data_list, file_directory, optimize_num,
105
+ axis_name_1="NODE #", axis_name_2="Value", name="data"):
106
+ """Create a simple scatter plot"""
107
+ fig, ax = plt.subplots()
108
+ ax.scatter(num_list, data_list, color=self.color_list[0], marker='o')
109
+ ax.plot(num_list, data_list, self.color_list[0]+'--o')
110
+ ax.set_title(str(optimize_num))
111
+ ax.set_xlabel(axis_name_1)
112
+ ax.set_ylabel(axis_name_2)
113
+ fig.tight_layout()
114
+ fig.savefig(f"{self.config.NEB_FOLDER_DIRECTORY}plot_{name}_{optimize_num}.png",
115
+ format="png", dpi=200)
116
+ plt.close()
117
+
118
+ def plot_energy(self, num_list, energy_list, optimize_num,
119
+ axis_name_1="NODE #", axis_name_2="Electronic Energy [kcal/mol]", name="energy"):
120
+ """Plot energy profile"""
121
+ self.simple_plot(num_list, energy_list, "", optimize_num, axis_name_1, axis_name_2, name)
122
+
123
+ def plot_gradient(self, num_list, gradient_norm_list, optimize_num,
124
+ axis_name_1="NODE #", axis_name_2="Gradient (RMS) [a.u.]", name="gradient"):
125
+ """Plot gradient profile"""
126
+ self.simple_plot(num_list, gradient_norm_list, "", optimize_num, axis_name_1, axis_name_2, name)
127
+
128
+ def plot_orthogonality(self, num_list, cos_list, optimize_num):
129
+ """Plot orthogonality profile"""
130
+ self.simple_plot(num_list, cos_list, "", optimize_num,
131
+ axis_name_1="NODE #", axis_name_2="cosθ", name="orthogonality")
132
+
133
+ def plot_perpendicular_gradient(self, num_list, force_list, optimize_num, force_type="rms"):
134
+ """Plot perpendicular gradient profile"""
135
+ if force_type == "rms":
136
+ axis_name_2 = "Perpendicular Gradient (RMS) [a.u.]"
137
+ name = "perp_rms_gradient"
138
+ else:
139
+ axis_name_2 = "Perpendicular Gradient (MAX) [a.u.]"
140
+ name = "perp_max_gradient"
141
+
142
+ self.simple_plot(num_list, force_list, "", optimize_num,
143
+ axis_name_1="NODE #", axis_name_2=axis_name_2, name=name)
144
+
145
+ # For ADDF-like method in ieip.py
146
+ def plot_potential_energy_path(energy_list, path, additional_name=""):
147
+ min_energy = np.min(energy_list)
148
+ energy_list -= min_energy
149
+ energy_list *= UnitValueLib().hartree2kcalmol
150
+ plt.plot(energy_list, marker='.', linestyle='--', color='b')
151
+ plt.xlabel("Iteration")
152
+ plt.ylabel("Energy (kcal/mol)")
153
+ plt.title("Potential Energy Path")
154
+ plt.savefig(path+"/"+additional_name+"_energy_profile.png")
155
+ plt.close()
156
+ return