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,611 @@
1
+ import glob
2
+ import os
3
+ import copy
4
+ import numpy as np
5
+ import torch
6
+
7
+
8
+ from multioptpy.SQM.sqm1.sqm1_core import SQM1Calculator, SQM1Parameters, ANGSTROM_TO_BOHR, BOHR_TO_ANGSTROM, is_covalently_bonded
9
+ from multioptpy.Utils.calc_tools import Calculationtools
10
+ from multioptpy.Parameters.parameter import UnitValueLib, element_number
11
+ from multioptpy.fileio import xyz2list
12
+ from multioptpy.Visualization.visualization import NEBVisualizer
13
+
14
+
15
+ """
16
+ Experimental semiempirical electronic structure approach inspired by GFN0-xTB (SQM1)
17
+
18
+ This module provides calculator utility helpers wrapping the Python implementation
19
+ of an experimental semiempirical electronic structure approach inspired by GFN0-xTB.
20
+ It mirrors the interface style of tblite_calculation_tools for convenience.
21
+ """
22
+
23
+ class Calculation:
24
+ def __init__(self, **kwarg):
25
+ if UnitValueLib is not None:
26
+ UVL = UnitValueLib()
27
+ self.bohr2angstroms = UVL.bohr2angstroms
28
+ else:
29
+ self.bohr2angstroms = BOHR_TO_ANGSTROM
30
+ # Optional keys kept for interface parity; use .get to avoid KeyError
31
+ self.START_FILE = kwarg.get("START_FILE")
32
+ self.N_THREAD = kwarg.get("N_THREAD", 1)
33
+ self.SET_MEMORY = kwarg.get("SET_MEMORY")
34
+ self.FUNCTIONAL = kwarg.get("FUNCTIONAL")
35
+ self.FC_COUNT = kwarg.get("FC_COUNT", -1)
36
+ self.BPA_FOLDER_DIRECTORY = kwarg.get("BPA_FOLDER_DIRECTORY", "./")
37
+ self.Model_hess = kwarg.get("Model_hess")
38
+ self.unrestrict = kwarg.get("unrestrict", False)
39
+ self.dft_grid = kwarg.get("dft_grid")
40
+ self.hessian_flag = False
41
+ # Load SQM1 parameters (now embedded, no file needed)
42
+ self.params = SQM1Parameters()
43
+ self.device = kwarg.get("device", "cpu")
44
+ self.dtype = kwarg.get("dtype", torch.float64)
45
+ # Calculator instance will be created per calculation with appropriate geometry
46
+ self.calculator = None
47
+ # Distance constraint parameters
48
+ self.use_distance_constraints = kwarg.get("use_distance_constraints", True)
49
+ self.max_distance_deviation = kwarg.get("max_distance_deviation", 0.10) # 10% default
50
+ self.constraint_penalty_strength = kwarg.get("constraint_penalty_strength", 1000.0)
51
+ self.initial_distances = {} # Will store bonded pair distances on first calculation
52
+ self.bonded_pairs = set()
53
+ self.constraints_initialized = False
54
+
55
+ def _update_distance_constraints(self, positions, element_number_list):
56
+ """
57
+ Update distance constraints for covalently bonded atom pairs.
58
+ This is called every time energy is calculated to track dynamic bond formation/breaking.
59
+
60
+ Args:
61
+ positions: Atomic positions in Angstrom (n_atoms, 3)
62
+ element_number_list: Atomic numbers
63
+ """
64
+ if not self.use_distance_constraints:
65
+ return
66
+
67
+ positions = np.array(positions, dtype='float64').reshape(-1, 3)
68
+ n_atoms = len(positions)
69
+
70
+ # Update bonded pairs based on current geometry
71
+ current_bonded_pairs = set()
72
+
73
+ # Identify covalently bonded pairs at current geometry
74
+ for i in range(n_atoms):
75
+ for j in range(i + 1, n_atoms):
76
+ # Calculate distance in Angstrom
77
+ d_angstrom = np.linalg.norm(positions[i] - positions[j])
78
+
79
+ # Check if atoms are covalently bonded at current geometry
80
+ if is_covalently_bonded(element_number_list[i], element_number_list[j], d_angstrom):
81
+ current_bonded_pairs.add((i, j))
82
+
83
+ # If this is a new bond, record its initial distance
84
+ if (i, j) not in self.initial_distances:
85
+ d_bohr = d_angstrom * ANGSTROM_TO_BOHR
86
+ self.initial_distances[(i, j)] = d_bohr
87
+
88
+ # Update the bonded pairs to reflect current state
89
+ self.bonded_pairs = current_bonded_pairs
90
+ self.constraints_initialized = True
91
+
92
+ def _calculate_distance_constraint_penalty(self, positions):
93
+ """
94
+ Calculate penalty energy and gradient for distance constraint violations.
95
+
96
+ Args:
97
+ positions: Current positions in Angstrom (n_atoms, 3)
98
+
99
+ Returns:
100
+ Tuple of (penalty_energy, penalty_gradient)
101
+ - penalty_energy: Penalty energy in Hartree
102
+ - penalty_gradient: Gradient of penalty in Hartree/Bohr (n_atoms, 3)
103
+ """
104
+ if not self.use_distance_constraints or not self.constraints_initialized:
105
+ return 0.0, None
106
+
107
+ positions = np.array(positions, dtype='float64').reshape(-1, 3)
108
+ n_atoms = len(positions)
109
+ penalty = 0.0
110
+ penalty_gradient = np.zeros((n_atoms, 3), dtype='float64')
111
+
112
+ # Only apply penalty to CURRENTLY bonded pairs
113
+ for (i, j) in self.bonded_pairs:
114
+ if (i, j) not in self.initial_distances:
115
+ continue # Skip if we don't have a reference distance
116
+
117
+ d_init_bohr = self.initial_distances[(i, j)]
118
+
119
+ # Calculate distance vector in Angstrom
120
+ r_ij_angstrom = positions[i] - positions[j]
121
+ d_current_angstrom = np.linalg.norm(r_ij_angstrom)
122
+
123
+ # Convert to Bohr
124
+ r_ij_bohr = r_ij_angstrom * ANGSTROM_TO_BOHR
125
+ d_current_bohr = d_current_angstrom * ANGSTROM_TO_BOHR
126
+
127
+ # Calculate relative deviation (dimensionless)
128
+ deviation = abs(d_current_bohr - d_init_bohr) / d_init_bohr
129
+
130
+ # Apply penalty if deviation exceeds threshold
131
+ if deviation > self.max_distance_deviation:
132
+ # Energy penalty (in Hartree)
133
+ penalty += self.constraint_penalty_strength * (deviation - self.max_distance_deviation)**2
134
+
135
+ # Gradient penalty calculation:
136
+ # E = k * (dev - thresh)²
137
+ # dev = |d_curr - d_init| / d_init
138
+ #
139
+ # Using chain rule:
140
+ # ∂E/∂r_bohr = ∂E/∂dev * ∂dev/∂d_curr * ∂d_curr/∂r_bohr
141
+ #
142
+ # ∂E/∂dev = 2*k*(dev - thresh)
143
+ # ∂dev/∂d_curr = sign(d_curr - d_init) / d_init
144
+ # ∂d_curr/∂r_bohr = r_ij_bohr / d_curr_bohr (unit vector in Bohr)
145
+ #
146
+ # Combined:
147
+ # ∂E/∂r_bohr = 2*k*(dev - thresh) * sign(d_curr - d_init)/d_init * r_ij_bohr/d_curr_bohr
148
+
149
+ # Sign of deviation
150
+ sign = 1.0 if d_current_bohr > d_init_bohr else -1.0
151
+
152
+ # Gradient prefactor (in Hartree/Bohr)
153
+ grad_prefactor = 2.0 * self.constraint_penalty_strength * (deviation - self.max_distance_deviation) * sign / d_init_bohr
154
+
155
+ # Direction vector (unit vector in Bohr space)
156
+ direction_bohr = r_ij_bohr / d_current_bohr
157
+
158
+ # Gradient in Hartree/Bohr
159
+ grad_contribution = grad_prefactor * direction_bohr
160
+
161
+ # Apply to both atoms (i and j)
162
+ penalty_gradient[i] += grad_contribution
163
+ penalty_gradient[j] -= grad_contribution
164
+
165
+ return penalty, penalty_gradient
166
+
167
+ def _calculate_distance_constraint_penalty_hessian(self, positions):
168
+ """
169
+ Calculate Hessian of distance constraint penalty.
170
+
171
+ Args:
172
+ positions: Current positions in Angstrom (n_atoms, 3)
173
+
174
+ Returns:
175
+ Hessian of penalty in Hartree/Bohr^2 (3*n_atoms, 3*n_atoms)
176
+ """
177
+ if not self.use_distance_constraints or not self.constraints_initialized:
178
+ return None
179
+
180
+ positions = np.array(positions, dtype='float64').reshape(-1, 3)
181
+ n_atoms = len(positions)
182
+ penalty_hessian = np.zeros((3*n_atoms, 3*n_atoms), dtype='float64')
183
+
184
+ # Only apply penalty to CURRENTLY bonded pairs
185
+ for (i, j) in self.bonded_pairs:
186
+ if (i, j) not in self.initial_distances:
187
+ continue # Skip if we don't have a reference distance
188
+
189
+ d_init_bohr = self.initial_distances[(i, j)]
190
+ # Calculate distance vector in Angstrom
191
+ r_ij_angstrom = positions[i] - positions[j]
192
+ d_current_angstrom = np.linalg.norm(r_ij_angstrom)
193
+
194
+ # Convert to Bohr
195
+ r_ij_bohr = r_ij_angstrom * ANGSTROM_TO_BOHR
196
+ d_current_bohr = d_current_angstrom * ANGSTROM_TO_BOHR
197
+
198
+ # Calculate relative deviation (dimensionless)
199
+ deviation = abs(d_current_bohr - d_init_bohr) / d_init_bohr
200
+
201
+ # Only calculate if deviation exceeds threshold
202
+ if deviation > self.max_distance_deviation:
203
+ # Sign of deviation
204
+ sign = 1.0 if d_current_bohr > d_init_bohr else -1.0
205
+
206
+ # Unit vector along bond (in Bohr)
207
+ u = r_ij_bohr / d_current_bohr
208
+
209
+ # For harmonic penalty E = k * (dev - thresh)²
210
+ # where dev = sign * (d - d0) / d0
211
+ #
212
+ # The Hessian has two terms:
213
+ # H = ∂²E/∂r_i∂r_j
214
+ #
215
+ # For a pair of atoms (atom_a, atom_b):
216
+ # H_aa = projection along bond + projection perpendicular
217
+ # H_ab = -H_aa (Newton's 3rd law)
218
+ # H_bb = H_aa
219
+ #
220
+ # The full formula for harmonic penalty Hessian:
221
+ # H_αβ = (2k/d0²) * [u_α u_β + (dev-thresh)/sign * (δ_αβ - u_α u_β) / d]
222
+ #
223
+ # Simplified for the case where dev > thresh:
224
+ # Second derivative along bond direction:
225
+ k = self.constraint_penalty_strength
226
+ d0 = d_init_bohr
227
+ d = d_current_bohr
228
+
229
+ # Prefactor for second derivative (in Hartree/Bohr²)
230
+ # ∂²E/∂d² = 2k/d0²
231
+ prefactor = 2.0 * k / (d0 * d0)
232
+
233
+ # Additional term from directional derivative
234
+ # When dev > thresh, we have an active constraint
235
+ # The Hessian includes both the force constant and geometric terms
236
+ if abs(d - d0) > 1e-10: # Avoid division by zero
237
+ # Coefficient for the projection onto bond direction
238
+ coeff_parallel = prefactor
239
+ # Coefficient for the projection perpendicular to bond
240
+ # This comes from the d/dr term in the gradient
241
+ coeff_perp = prefactor * (deviation - self.max_distance_deviation) * sign / d
242
+ else:
243
+ coeff_parallel = prefactor
244
+ coeff_perp = 0.0
245
+
246
+ # Build 3x3 block for this atom pair
247
+ # H_block = coeff_parallel * u⊗u + coeff_perp * (I - u⊗u)
248
+ identity = np.eye(3, dtype='float64')
249
+ u_outer = np.outer(u, u)
250
+ H_block = coeff_parallel * u_outer + coeff_perp * (identity - u_outer)
251
+
252
+ # Add to Hessian (symmetric contributions)
253
+ # H_ii += H_block
254
+ penalty_hessian[3*i:3*i+3, 3*i:3*i+3] += H_block
255
+ # H_jj += H_block (symmetric)
256
+ penalty_hessian[3*j:3*j+3, 3*j:3*j+3] += H_block
257
+ # H_ij -= H_block (off-diagonal)
258
+ penalty_hessian[3*i:3*i+3, 3*j:3*j+3] -= H_block
259
+ # H_ji -= H_block (symmetric off-diagonal)
260
+ penalty_hessian[3*j:3*j+3, 3*i:3*i+3] -= H_block
261
+
262
+ return penalty_hessian
263
+
264
+ def numerical_hessian(self, geom_num_list, element_list, total_charge):
265
+ """
266
+ Calculate numerical Hessian using finite differences of gradients.
267
+
268
+ Args:
269
+ geom_num_list: Atomic positions in Angstrom (n_atoms, 3)
270
+ element_list: Atomic numbers (n_atoms,)
271
+ total_charge: Total molecular charge
272
+
273
+ Returns:
274
+ Hessian matrix (3*n_atoms, 3*n_atoms) in Hartree/Bohr^2
275
+ """
276
+ numerical_delivative_delta = 1.0e-4 # in Angstrom
277
+ geom_num_list = np.array(geom_num_list, dtype="float64")
278
+ n_atoms = len(geom_num_list)
279
+ hessian = np.zeros((3*n_atoms, 3*n_atoms))
280
+ count = 0
281
+
282
+ # Update distance constraints based on current geometry
283
+ self._update_distance_constraints(geom_num_list, element_list)
284
+
285
+ for a in range(n_atoms):
286
+ for i in range(3):
287
+ for b in range(n_atoms):
288
+ for j in range(3):
289
+ if count > 3*b + j:
290
+ continue
291
+ tmp_grad = []
292
+ for direction in [1, -1]:
293
+ shifted = geom_num_list.copy()
294
+ shifted[a, i] += direction * numerical_delivative_delta
295
+ # Create calculator for this geometry
296
+ calc = SQM1Calculator(
297
+ atomic_numbers=element_list,
298
+ positions=shifted,
299
+ charge=total_charge,
300
+ uhf=0,
301
+ params=self.params,
302
+ device=self.device,
303
+ dtype=self.dtype
304
+ )
305
+ # Get gradient in Hartree/Bohr
306
+ _, grad = calc.calculate_energy_and_gradient()
307
+ grad_np = grad.cpu().detach().numpy()
308
+
309
+ # Add penalty gradient
310
+ _, penalty_grad = self._calculate_distance_constraint_penalty(shifted)
311
+ if penalty_grad is not None:
312
+ grad_np += penalty_grad
313
+
314
+ tmp_grad.append(grad_np[b, j])
315
+ # Finite difference in Angstrom, convert to Bohr
316
+ val = (tmp_grad[0] - tmp_grad[1]) / (2*numerical_delivative_delta)
317
+ hessian[3*a+i, 3*b+j] = val
318
+ hessian[3*b+j, 3*a+i] = val
319
+ count += 1
320
+ return hessian
321
+
322
+ def exact_hessian(self, element_number_list, total_charge, positions):
323
+ """
324
+ Calculate exact Hessian using automatic differentiation.
325
+
326
+ Args:
327
+ element_number_list: Atomic numbers (n_atoms,)
328
+ total_charge: Total molecular charge
329
+ positions: Atomic positions in Angstrom (n_atoms, 3)
330
+
331
+ Returns:
332
+ Projected Hessian matrix (3*n_atoms, 3*n_atoms) in Hartree/Bohr^2
333
+ """
334
+ # Update distance constraints based on current geometry
335
+ self._update_distance_constraints(positions, element_number_list)
336
+
337
+ # Create calculator for this geometry
338
+ calc = SQM1Calculator(
339
+ atomic_numbers=element_number_list,
340
+ positions=positions,
341
+ charge=total_charge,
342
+ uhf=0,
343
+ params=self.params,
344
+ device=self.device,
345
+ dtype=self.dtype
346
+ )
347
+ # Calculate Hessian using automatic differentiation
348
+ exact_hess = calc.calculate_hessian(method='analytical')
349
+ exact_hess_np = exact_hess.cpu().detach().numpy()
350
+
351
+ # Add penalty Hessian
352
+ penalty_hess = self._calculate_distance_constraint_penalty_hessian(positions)
353
+ if penalty_hess is not None:
354
+ exact_hess_np += penalty_hess
355
+
356
+ # Project out translation and rotation if Calculationtools is available
357
+ if Calculationtools is not None:
358
+ exact_hess_np = Calculationtools().project_out_hess_tr_and_rot_for_coord(
359
+ exact_hess_np, element_number_list.tolist(), positions, display_eigval=False
360
+ )
361
+ self.Model_hess = exact_hess_np
362
+
363
+ def single_point(self, file_directory, element_number_list, iter_index, electric_charge_and_multiplicity, method="", geom_num_list=None):
364
+ """
365
+ Calculate single point energy and gradient.
366
+
367
+ Args:
368
+ file_directory: Directory containing xyz files
369
+ element_number_list: Atomic numbers
370
+ iter_index: Iteration index
371
+ electric_charge_and_multiplicity: [charge, multiplicity]
372
+ geom_num_list: Optional geometry (n_atoms, 3) in Angstrom
373
+
374
+ Returns:
375
+ Tuple of (energy, gradient, positions, finish_flag)
376
+ """
377
+ gradient_list = []
378
+ energy_list = []
379
+ geometry_num_list = []
380
+ finish_frag = False
381
+
382
+ if isinstance(element_number_list[0], str):
383
+ tmp = copy.copy(element_number_list)
384
+ element_number_list = []
385
+ if element_number is not None:
386
+ for elem in tmp:
387
+ element_number_list.append(element_number(elem))
388
+ element_number_list = np.array(element_number_list)
389
+
390
+ try:
391
+ os.mkdir(file_directory)
392
+ except Exception:
393
+ pass
394
+
395
+ if file_directory is None:
396
+ file_list = ["dummy"]
397
+ else:
398
+ file_list = glob.glob(file_directory+"/*_[0-9].xyz")
399
+
400
+ total_charge = int(electric_charge_and_multiplicity[0])
401
+
402
+ for num, input_file in enumerate(file_list):
403
+ if True:
404
+ if geom_num_list is None and xyz2list is not None:
405
+ tmp_positions, _, electric_charge_and_multiplicity = xyz2list(input_file, electric_charge_and_multiplicity)
406
+ else:
407
+ tmp_positions = geom_num_list
408
+
409
+ positions = np.array(tmp_positions, dtype="float64").reshape(-1, 3) # Angstrom
410
+
411
+ # Update distance constraints on every calculation to track dynamic bonding
412
+ self._update_distance_constraints(positions, element_number_list)
413
+
414
+ # Create calculator for this geometry
415
+ calc = SQM1Calculator(
416
+ atomic_numbers=element_number_list,
417
+ positions=positions,
418
+ charge=total_charge,
419
+ uhf=0,
420
+ params=self.params,
421
+ device=self.device,
422
+ dtype=self.dtype
423
+ )
424
+
425
+ # Calculate energy and gradient
426
+ e, g = calc.calculate_energy_and_gradient()
427
+ e = e.cpu().detach().numpy().item() # Hartree
428
+ g = g.cpu().detach().numpy() # Hartree/Bohr
429
+
430
+ # Apply distance constraint penalty
431
+ penalty, penalty_grad = self._calculate_distance_constraint_penalty(positions)
432
+ e += penalty
433
+ if penalty_grad is not None:
434
+ g += penalty_grad
435
+ positions /= BOHR_TO_ANGSTROM # Convert back to Angstrom for output
436
+ # Save results
437
+ self.energy = e
438
+ self.gradient = g
439
+ self.coordinate = positions
440
+
441
+ if self.FC_COUNT == -1 or isinstance(iter_index, str):
442
+ if self.hessian_flag:
443
+ self.exact_hessian(element_number_list, total_charge, positions)
444
+ elif iter_index % self.FC_COUNT == 0 or self.hessian_flag:
445
+ self.exact_hessian(element_number_list, total_charge, positions)
446
+
447
+ #except Exception as error:
448
+ # print(error)
449
+ # print("This molecule could not be optimized.")
450
+ # print("Input file: ", file_list, "\n")
451
+ # finish_frag = True
452
+ # return np.array([0]), np.array([0]), positions, finish_frag
453
+
454
+ return e, g, positions, finish_frag
455
+
456
+ def single_point_no_directory(self, positions, element_number_list, electric_charge_and_multiplicity):
457
+ """
458
+ Calculate single point energy and gradient without file I/O.
459
+
460
+ Args:
461
+ positions: Atomic positions in Angstrom (n_atoms, 3)
462
+ element_number_list: Atomic numbers
463
+ electric_charge_and_multiplicity: [charge, multiplicity]
464
+
465
+ Returns:
466
+ Tuple of (energy, gradient, finish_flag)
467
+ """
468
+ finish_frag = False
469
+ if isinstance(element_number_list[0], str):
470
+ tmp = copy.copy(element_number_list)
471
+ element_number_list = []
472
+ if element_number is not None:
473
+ for elem in tmp:
474
+ element_number_list.append(element_number(elem))
475
+ element_number_list = np.array(element_number_list)
476
+ try:
477
+ positions = np.array(positions, dtype='float64')
478
+ total_charge = int(electric_charge_and_multiplicity[0])
479
+
480
+ # Update distance constraints on every calculation to track dynamic bonding
481
+ self._update_distance_constraints(positions, element_number_list)
482
+
483
+ # Create calculator for this geometry
484
+ calc = SQM1Calculator(
485
+ atomic_numbers=element_number_list,
486
+ positions=positions,
487
+ charge=total_charge,
488
+ uhf=0,
489
+ params=self.params,
490
+ device=self.device,
491
+ dtype=self.dtype
492
+ )
493
+
494
+ # Calculate energy and gradient
495
+ e, g = calc.calculate_energy_and_gradient()
496
+ e = e.cpu().detach().numpy().item() # Hartree
497
+ g = g.cpu().detach().numpy() # Hartree/Bohr
498
+
499
+ # Apply distance constraint penalty
500
+ penalty, penalty_grad = self._calculate_distance_constraint_penalty(positions)
501
+ e += penalty
502
+ if penalty_grad is not None:
503
+ g += penalty_grad
504
+
505
+ self.energy = e
506
+ self.gradient = g
507
+ except Exception as error:
508
+ print(error)
509
+ print("This molecule could not be optimized.")
510
+ finish_frag = True
511
+ return np.array([0]), np.array([0]), finish_frag
512
+ return e, g, finish_frag
513
+
514
+
515
+ class CalculationEngine:
516
+ def calculate(self, file_directory, optimize_num, pre_total_velocity, config):
517
+ raise NotImplementedError
518
+
519
+ def _get_file_list(self, file_directory):
520
+ return sum([sorted(glob.glob(os.path.join(file_directory, f"*_" + "[0-9]" * i + ".xyz"))) for i in range(1, 7)], [])
521
+
522
+ def _process_visualization(self, energy_list, gradient_list, num_list, optimize_num, config):
523
+ try:
524
+ if getattr(config, 'save_pict', False):
525
+ visualizer = NEBVisualizer(config)
526
+ tmp_ene_list = np.array(energy_list, dtype='float64') * config.hartree2kcalmol
527
+ visualizer.plot_energy(num_list, tmp_ene_list - tmp_ene_list[0], optimize_num)
528
+ print("energy graph plotted.")
529
+ gradient_norm_list = [np.sqrt(np.linalg.norm(g)**2/(len(g)*3)) for g in gradient_list]
530
+ visualizer.plot_gradient(num_list, gradient_norm_list, optimize_num)
531
+ print("gradient graph plotted.")
532
+ except Exception as e:
533
+ print(f"Visualization error: {e}")
534
+
535
+
536
+ class SQM1Engine(CalculationEngine):
537
+ """SQM1 calculation engine wrapping SQM1Calculator"""
538
+ def __init__(self, param_file=None, device="cpu", dtype=torch.float64):
539
+ # Parameters are now embedded, param_file is ignored (kept for backward compatibility)
540
+ self.params = SQM1Parameters()
541
+ self.device = device
542
+ self.dtype = dtype
543
+
544
+ def calculate(self, file_directory, optimize_num, pre_total_velocity, config):
545
+ gradient_list = []
546
+ energy_list = []
547
+ geometry_num_list = []
548
+ gradient_norm_list = []
549
+ delete_pre_total_velocity = []
550
+ num_list = []
551
+
552
+ os.makedirs(file_directory, exist_ok=True)
553
+ file_list = self._get_file_list(file_directory)
554
+
555
+ if xyz2list is None:
556
+ raise ImportError("xyz2list is required for this method")
557
+
558
+ geometry_list_tmp, element_list, _ = xyz2list(file_list[0], None)
559
+ element_number_list = []
560
+ if element_number is not None:
561
+ for elem in element_list:
562
+ element_number_list.append(element_number(elem))
563
+ element_number_list = np.array(element_number_list, dtype='int')
564
+
565
+ for num, input_file in enumerate(file_list):
566
+ try:
567
+ print(input_file)
568
+ positions, _, electric_charge_and_multiplicity = xyz2list(input_file, None)
569
+ positions = np.array(positions, dtype='float64').reshape(-1, 3) # Angstrom
570
+ total_charge = int(electric_charge_and_multiplicity[0])
571
+
572
+ # Create calculator for this geometry
573
+ calc = SQM1Calculator(
574
+ atomic_numbers=element_number_list,
575
+ positions=positions,
576
+ charge=total_charge,
577
+ uhf=0,
578
+ params=self.params,
579
+ device=self.device,
580
+ dtype=self.dtype
581
+ )
582
+
583
+ # Calculate energy and gradient
584
+ e, g = calc.calculate_energy_and_gradient()
585
+ e = e.cpu().detach().numpy().item() # Hartree
586
+ g = g.cpu().detach().numpy() # Hartree/Bohr
587
+
588
+ print("\n")
589
+ energy_list.append(e)
590
+ gradient_list.append(g)
591
+ gradient_norm_list.append(np.sqrt(np.linalg.norm(g)**2/(len(g)*3)))
592
+ geometry_num_list.append(positions)
593
+ num_list.append(num)
594
+ except Exception as error:
595
+ print(error)
596
+ print("This molecule could not be optimized.")
597
+ if optimize_num != 0:
598
+ delete_pre_total_velocity.append(num)
599
+
600
+ self._process_visualization(energy_list, gradient_list, num_list, optimize_num, config)
601
+
602
+ if optimize_num != 0 and len(pre_total_velocity) != 0:
603
+ pre_total_velocity = np.array(pre_total_velocity, dtype='float64').tolist()
604
+ for i in sorted(delete_pre_total_velocity, reverse=True):
605
+ pre_total_velocity.pop(i)
606
+ pre_total_velocity = np.array(pre_total_velocity, dtype='float64')
607
+
608
+ return (np.array(energy_list, dtype='float64'),
609
+ np.array(gradient_list, dtype='float64'),
610
+ np.array(geometry_num_list, dtype='float64'),
611
+ pre_total_velocity)