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,1792 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SQM1 Implementation - Rigorous and Theory-Compliant
4
+
5
+ Complete implementation of SQM1 following the theoretical framework
6
+ based on ChemRxiv: 60c742abbdbb890c7ba3851a.
7
+
8
+ This corrected implementation addresses:
9
+ 1. Parameter defects - reads from external param_sqm1.txt file
10
+ 2. Physical model simplifications - proper EEQ, SimpleDispersion, SRB implementations
11
+ 3. Functional/algorithmic defects - proper unit handling and calculations
12
+
13
+ Total Energy Expression:
14
+ E_total = E_EHT + E_IES + E_rep + E_dispSimple + E_SRB
15
+
16
+ Supported Elements: H, C, N, O, F, Cl, Br (Z=1, 6, 7, 8, 9, 17, 35)
17
+ """
18
+
19
+ import torch
20
+ import torch.nn.functional as F
21
+
22
+ # --- Constants ---
23
+ BOHR_TO_ANGSTROM = 0.529177210903
24
+ ANGSTROM_TO_BOHR = 1.0 / BOHR_TO_ANGSTROM
25
+ HARTREE_TO_EV = 27.211386245988
26
+ EV_TO_HARTREE = 1.0 / HARTREE_TO_EV
27
+
28
+ # --- SimpleDispersion Coordination Number Parameters ---
29
+ # Reference: Caldeweyher et al., J. Chem. Phys. 150, 154122 (2019)
30
+ # and D3 method: Grimme et al., J. Chem. Phys. 132, 154104 (2010)
31
+ SIMPLE_DISP_CN_K1 = 16.0 # Steepness parameter for CN counting function
32
+ SIMPLE_DISP_CN_K2 = 4.0 / 3.0 # Scaling factor for covalent radii sum
33
+ SIMPLE_DISP_CN_CUTOFF = 20.0 # Cutoff radius in Angstrom for CN calculation
34
+
35
+ # Covalent radii for SimpleDispersion (in Angstrom)
36
+ # Reference: Pyykkö and Atsumi, Chem. Eur. J. 15, 186 (2009)
37
+ # These values are consistent with xTB parameterization
38
+ SIMPLE_DISP_COVALENT_RADII = {
39
+ 1: 0.32, # H
40
+ 2: 0.46, # He
41
+ 3: 1.33, # Li
42
+ 4: 1.02, # Be
43
+ 5: 0.85, # B
44
+ 6: 0.75, # C
45
+ 7: 0.71, # N
46
+ 8: 0.63, # O
47
+ 9: 0.64, # F
48
+ 10: 0.67, # Ne
49
+ 11: 1.55, # Na
50
+ 12: 1.39, # Mg
51
+ 13: 1.26, # Al
52
+ 14: 1.16, # Si
53
+ 15: 1.11, # P
54
+ 16: 1.03, # S
55
+ 17: 0.99, # Cl
56
+ 18: 0.96, # Ar
57
+ 19: 1.96, # K
58
+ 20: 1.71, # Ca
59
+ 21: 1.48, # Sc
60
+ 22: 1.36, # Ti
61
+ 23: 1.34, # V
62
+ 24: 1.22, # Cr
63
+ 25: 1.19, # Mn
64
+ 26: 1.16, # Fe
65
+ 27: 1.11, # Co
66
+ 28: 1.10, # Ni
67
+ 29: 1.12, # Cu
68
+ 30: 1.18, # Zn
69
+ 31: 1.24, # Ga
70
+ 32: 1.21, # Ge
71
+ 33: 1.21, # As
72
+ 34: 1.16, # Se
73
+ 35: 1.14, # Br
74
+ 36: 1.17, # Kr
75
+ 37: 2.10, # Rb
76
+ 38: 1.85, # Sr
77
+ 39: 1.63, # Y
78
+ 40: 1.54, # Zr
79
+ 41: 1.47, # Nb
80
+ 42: 1.38, # Mo
81
+ 43: 1.28, # Tc
82
+ 44: 1.25, # Ru
83
+ 45: 1.25, # Rh
84
+ 46: 1.20, # Pd
85
+ 47: 1.28, # Ag
86
+ 48: 1.36, # Cd
87
+ 49: 1.42, # In
88
+ 50: 1.40, # Sn
89
+ 51: 1.40, # Sb
90
+ 52: 1.36, # Te
91
+ 53: 1.33, # I
92
+ 54: 1.31, # Xe
93
+ 55: 2.32, # Cs
94
+ 56: 1.96, # Ba
95
+ 57: 1.80, # La
96
+ 58: 1.63, # Ce
97
+ 59: 1.76, # Pr
98
+ 60: 1.74, # Nd
99
+ 61: 1.73, # Pm
100
+ 62: 1.72, # Sm
101
+ 63: 1.68, # Eu
102
+ 64: 1.69, # Gd
103
+ 65: 1.68, # Tb
104
+ 66: 1.67, # Dy
105
+ 67: 1.66, # Ho
106
+ 68: 1.65, # Er
107
+ 69: 1.64, # Tm
108
+ 70: 1.70, # Yb
109
+ 71: 1.62, # Lu
110
+ 72: 1.52, # Hf
111
+ 73: 1.46, # Ta
112
+ 74: 1.37, # W
113
+ 75: 1.31, # Re
114
+ 76: 1.29, # Os
115
+ 77: 1.22, # Ir
116
+ 78: 1.23, # Pt
117
+ 79: 1.24, # Au
118
+ 80: 1.33, # Hg
119
+ 81: 1.44, # Tl
120
+ 82: 1.44, # Pb
121
+ 83: 1.51, # Bi
122
+ 84: 1.45, # Po
123
+ 85: 1.47, # At
124
+ 86: 1.42, # Rn
125
+ }
126
+
127
+
128
+ class SQM1Parameters:
129
+ """
130
+ Stores all SQM1 parameters embedded directly in the class.
131
+ Parameters from param_gfn0_xtb.txt are now included in the code,
132
+ eliminating the need for an external parameter file.
133
+ """
134
+ def __init__(self):
135
+ # Initialize parameter dictionaries with embedded data
136
+ self.element_params = {}
137
+ self.sk_params = {}
138
+ self.rep_params = {}
139
+ self.simple_disp_params = {}
140
+ self.srb_params = {}
141
+ self.global_params = {}
142
+ self._initialize_parameters()
143
+
144
+ def _initialize_parameters(self):
145
+ """Initialize all parameters from embedded data."""
146
+ # Embedded parameter data from param_gfn0_xtb.txt
147
+ # Format: (Z, symbol, valence_e, h_s, h_p, Z_eff, EN_A, J_AA_param, alpha, C6_ref, alpha_ref)
148
+ element_data = [
149
+ (1, 'H', 1, -11.92, -2.81, 1.25, 1.92, -0.3023, 0.749, 0.81, 2.7),
150
+ (2, 'He', 2, -20.95, -1.13, 1.2912, 2.0, 0.7743, 0.4197, 0.2, 1.4),
151
+ (3, 'Li', 1, -7.0, -3.27, 0.854, 2.0, 0.5303, 1.4256, 164.0, 164.0),
152
+ (4, 'Be', 2, -9.81, -4.17, 1.1724, 2.0, 0.2176, 2.0699, 75.0, 38.0),
153
+ (5, 'B', 3, -11.53, -7.18, 1.1094, 2.0, 0.1956, 1.7359, 25.0, 13.0),
154
+ (6, 'C', 4, -15.75, -9.8, 1.386, 2.48, 0.0308, 1.71, 28.0, 10.0),
155
+ (7, 'N', 5, -18.84, -11.54, 1.5342, 2.97, 0.056, 1.8256, 19.0, 7.5),
156
+ (8, 'O', 6, -17.93, -11.84, 1.5379, 2.0, 0.0581, 1.5927, 13.0, 5.8),
157
+ (9, 'F', 7, -21.18, -12.1, 1.5891, 3.5, 0.1574, 0.8986, 9.5, 4.2),
158
+ (10, 'Ne', 8, -23.81, -12.73, 1.2894, 3.5, 0.6826, 0.6138, 6.5, 2.7),
159
+ (11, 'Na', 1, -8.02, -3.54, 0.7891, 2.0, 0.3922, 1.7294, 163.0, 163.0),
160
+ (12, 'Mg', 2, -8.9, -3.39, 0.9983, 2.0, 0.5582, 1.7925, 147.0, 71.0),
161
+ (13, 'Al', 3, -11.42, -5.5, 0.9621, 2.0, 0.3018, 1.2157, 65.0, 58.0),
162
+ (14, 'Si', 4, -14.13, -8.28, 1.0441, 2.0, 0.1039, 1.5314, 80.0, 37.0),
163
+ (15, 'P', 5, -15.71, -9.87, 1.479, 2.0, 0.2125, 1.3731, 57.0, 25.0),
164
+ (16, 'S', 6, -20.16, -11.19, 1.3926, 2.0, 0.0581, 1.7936, 60.0, 20.0),
165
+ (17, 'Cl', 7, -26.27, -12.37, 1.4749, 2.0, 0.2537, 2.6682, 60.0, 20.0),
166
+ (18, 'Ar', 8, -22.03, -14.31, 1.225, 2.0, 0.578, 1.5892, 50.0, 16.0),
167
+ (19, 'K', 1, -6.69, -3.11, 0.8162, 1.45, 0.3921, 2.183, 309.0, 290.0),
168
+ (20, 'Ca', 2, -8.05, -2.18, 1.1252, 1.8, -0.0025, 1.4178, 210.0, 160.0),
169
+ (21, 'Sc', 3, -8.71, -9.02, 0.9641, 1.73, -0.0062, 1.5181, 155.0, 80.0),
170
+ (22, 'Ti', 4, -8.57, -9.49, 0.881, 2.0, 0.1663, 1.992, 125.0, 70.0),
171
+ (23, 'V', 5, -8.76, -9.87, 0.9742, 2.0, 0.1052, 1.7172, 115.0, 65.0),
172
+ (24, 'Cr', 6, -8.82, -7.1, 1.1029, 2.0, 0.001, 2.0655, 105.0, 60.0),
173
+ (25, 'Mn', 7, -9.58, -6.08, 1.0077, 2.0, 0.0977, 1.3318, 100.0, 55.0),
174
+ (26, 'Fe', 8, -10.15, -5.54, 0.7744, 2.0, 0.0612, 1.366, 95.0, 50.0),
175
+ (27, 'Co', 9, -10.53, -4.96, 0.7554, 2.0, 0.0562, 1.5694, 90.0, 45.0),
176
+ (28, 'Ni', 10, -10.59, -6.64, 1.0183, 2.0, 0.09, 1.2763, 85.0, 40.0),
177
+ (29, 'Cu', 11, -11.36, -8.46, 1.0316, 2.0, 0.1313, 1.004, 120.0, 50.0),
178
+ (30, 'Zn', 12, -11.05, -2.78, 1.6317, 2.0, 0.5728, 0.7339, 120.0, 50.0),
179
+ (31, 'Ga', 3, -11.23, -4.64, 1.1187, 2.0, 0.1742, 3.2596, 90.0, 45.0),
180
+ (32, 'Ge', 4, -15.56, -9.18, 1.0346, 2.0, 0.2672, 1.753, 85.0, 40.0),
181
+ (33, 'As', 5, -16.8, -10.2, 1.3091, 2.0, 0.2352, 1.5282, 65.0, 35.0),
182
+ (34, 'Se', 6, -20.69, -11.35, 1.4119, 2.0, 0.0718, 2.1838, 75.0, 30.0),
183
+ (35, 'Br', 7, -19.9, -11.63, 1.4501, 2.0, 0.3458, 2.3806, 90.0, 28.0),
184
+ (36, 'Kr', 8, -17.74, -13.32, 1.1747, 2.0, 0.8203, 2.7281, 60.0, 25.0),
185
+ (37, 'Rb', 1, -6.66, -3.3, 0.6686, 1.5, 0.4288, 0.7838, 20.0, 10.0),
186
+ (38, 'Sr', 2, -6.36, -1.69, 1.0745, 1.5, 0.2667, 1.4275, 20.0, 10.0),
187
+ (39, 'Y', 3, -7.33, -10.52, 0.9108, 1.55, 0.0874, 1.8024, 20.0, 10.0),
188
+ (40, 'Zr', 4, -8.35, -9.42, 0.7876, 2.0, 0.0599, 1.6093, 20.0, 10.0),
189
+ (41, 'Nb', 5, -8.99, -9.38, 1.004, 2.0, 0.1582, 1.3834, 20.0, 10.0),
190
+ (42, 'Mo', 6, -8.34, -5.04, 0.9225, 2.0, 0.1716, 1.1741, 20.0, 10.0),
191
+ (43, 'Tc', 7, -9.59, -4.12, 0.9036, 2.0, 0.2722, 1.5768, 20.0, 10.0),
192
+ (44, 'Ru', 8, -10.42, -4.67, 1.0332, 2.0, 0.2818, 1.3205, 20.0, 10.0),
193
+ (45, 'Rh', 9, -11.24, -6.32, 1.0294, 2.0, 0.1392, 1.4259, 20.0, 10.0),
194
+ (46, 'Pd', 10, -11.05, -7.52, 1.055, 2.0, 0.1176, 1.15, 20.0, 10.0),
195
+ (47, 'Ag', 11, -12.21, -7.81, 1.1301, 2.0, 0.0668, 1.1423, 20.0, 10.0),
196
+ (48, 'Cd', 12, -11.27, -2.61, 1.3935, 2.0, 0.5725, 0.6877, 20.0, 10.0),
197
+ (49, 'In', 3, -11.8, -4.99, 1.2124, 2.0, 0.2002, 2.6507, 20.0, 10.0),
198
+ (50, 'Sn', 4, -16.13, -9.32, 1.1587, 2.0, 0.1603, 1.9834, 20.0, 10.0),
199
+ (51, 'Sb', 5, -17.16, -9.32, 1.2824, 2.0, 0.1716, 1.7405, 20.0, 10.0),
200
+ (52, 'Te', 6, -18.5, -10.49, 1.3608, 2.0, 0.1016, 2.1537, 20.0, 10.0),
201
+ (53, 'I', 7, -13.69, -10.14, 1.4131, 2.0, 0.3082, 2.0992, 20.0, 10.0),
202
+ (54, 'Xe', 8, -11.7, -11.59, 1.188, 2.0, 0.7857, 2.6331, 20.0, 10.0),
203
+ (55, 'Cs', 1, -6.18, -3.52, 0.5875, 1.4, 0.5055, 0.4975, 20.0, 10.0),
204
+ (56, 'Ba', 2, -5.85, -1.72, 0.9976, 1.55, 0.2916, 1.184, 20.0, 10.0),
205
+ (57, 'La', 3, -7.25, -10.38, 0.8626, 1.6, 0.0814, 1.6773, 20.0, 10.0),
206
+ (58, 'Ce', 4, -7.4, -10.36, 0.8596, 1.65, 0.0742, 1.6882, 20.0, 10.0),
207
+ (59, 'Pr', 5, -7.54, -10.34, 0.8566, 1.7, 0.0669, 1.6991, 20.0, 10.0),
208
+ (60, 'Nd', 6, -7.69, -10.32, 0.8537, 1.75, 0.0597, 1.71, 20.0, 10.0),
209
+ (61, 'Pm', 7, -7.84, -10.29, 0.8507, 1.8, 0.0524, 1.7209, 20.0, 10.0),
210
+ (62, 'Sm', 8, -7.99, -10.27, 0.8478, 1.85, 0.0452, 1.7318, 20.0, 10.0),
211
+ (63, 'Eu', 9, -8.13, -10.25, 0.8448, 1.9, 0.0379, 1.7427, 20.0, 10.0),
212
+ (64, 'Gd', 10, -8.28, -10.23, 0.8419, 1.95, 0.0307, 1.7536, 20.0, 10.0),
213
+ (65, 'Tb', 11, -8.43, -10.21, 0.8389, 2.0, 0.0234, 1.7645, 20.0, 10.0),
214
+ (66, 'Dy', 12, -8.5, -10.12, 0.8359, 2.0, 0.0243, 1.8433, 20.0, 10.0),
215
+ (67, 'Ho', 13, -8.55, -10.06, 0.8329, 2.0, 0.0276, 1.9221, 20.0, 10.0),
216
+ (68, 'Er', 14, -8.6, -10.0, 1.0158, 1.5, 0.0301, 2.677, 20.0, 10.0),
217
+ (69, 'Tm', 15, -8.63, -9.87, 1.0117, 1.5, 0.0307, 2.7533, 20.0, 10.0),
218
+ (70, 'Yb', 16, -8.66, -9.74, 1.0075, 1.5, 0.0313, 2.8297, 20.0, 10.0),
219
+ (71, 'Lu', 3, -8.7, -9.61, 1.0034, 1.5, 0.032, 2.906, 20.0, 10.0),
220
+ (72, 'Hf', 4, -8.33, -9.27, 0.8613, 2.0, 0.0263, 1.6423, 20.0, 10.0),
221
+ (73, 'Ta', 5, -9.15, -10.52, 1.0422, 2.0, 0.1715, 1.3568, 20.0, 10.0),
222
+ (74, 'W', 6, -9.64, -8.4, 0.7633, 2.0, 0.1804, 1.8967, 20.0, 10.0),
223
+ (75, 'Re', 7, -10.24, -4.94, 0.602, 2.0, 0.3632, 0.8253, 20.0, 10.0),
224
+ (76, 'Os', 8, -10.01, -5.48, 0.7499, 2.0, 0.3011, 0.7412, 20.0, 10.0),
225
+ (77, 'Ir', 9, -11.14, -7.58, 0.9512, 2.0, 0.11, 1.0351, 20.0, 10.0),
226
+ (78, 'Pt', 10, -11.32, -8.89, 0.9357, 2.0, 0.0278, 0.9692, 20.0, 10.0),
227
+ (79, 'Au', 11, -12.1, -9.51, 1.3555, 2.0, 0.0555, 1.0048, 20.0, 10.0),
228
+ (80, 'Hg', 12, -12.17, -2.67, 1.2007, 2.0, 0.7723, 2.3139, 20.0, 10.0),
229
+ (81, 'Tl', 3, -20.16, -4.99, 1.2092, 2.0, 0.1288, 2.8056, 20.0, 10.0),
230
+ (82, 'Pb', 4, -22.07, -8.12, 1.1737, 2.0, 0.1035, 3.0969, 20.0, 10.0),
231
+ (83, 'Bi', 5, -19.85, -8.18, 1.1937, 2.0, 0.0115, 1.6598, 20.0, 10.0),
232
+ (84, 'Po', 6, -22.73, -10.66, 1.3045, 2.0, 0.0161, 3.2192, 20.0, 10.0),
233
+ (85, 'At', 7, -16.22, -10.58, 1.1965, 2.0, 0.337, 1.5388, 20.0, 10.0),
234
+ (86, 'Rn', 8, -13.64, -12.17, 1.2654, 2.0, 0.1844, 2.1222, 20.0, 10.0),
235
+ ]
236
+
237
+ # Parse element parameters
238
+ for data in element_data:
239
+ Z, symbol, valence_e, h_s, h_p, Z_eff, EN_A, J_AA_param, alpha, C6_ref, alpha_ref = data
240
+ self.element_params[Z] = {
241
+ 'symbol': symbol,
242
+ 'valence_e': valence_e,
243
+ 'h_s': h_s,
244
+ 'h_p': h_p,
245
+ 'Z_eff': Z_eff,
246
+ 'EN_A': EN_A,
247
+ 'J_AA_param': J_AA_param,
248
+ 'alpha': alpha,
249
+ 'C6_ref': C6_ref,
250
+ 'alpha_ref': alpha_ref
251
+ }
252
+
253
+ # SK integral data
254
+ # Format: (z1, z2, type, A, alpha)
255
+ sk_data = [
256
+ (1, 1, 'ss_sigma', 2.5, 3.0),
257
+ (1, 6, 'ss_sigma', 2.8, 3.3),
258
+ (1, 6, 'sp_sigma', 3.7, 3.45),
259
+ (6, 6, 'ss_sigma', 3.1, 3.6),
260
+ (6, 6, 'sp_sigma', 4.6, 3.75),
261
+ (6, 6, 'pp_sigma', 6.2, 3.9),
262
+ (6, 6, 'pp_pi', 4.6, 4.05),
263
+ (1, 8, 'ss_sigma', 2.9, 3.36),
264
+ (1, 8, 'sp_sigma', 3.8, 3.51),
265
+ (6, 8, 'ss_sigma', 3.2, 3.66),
266
+ (6, 8, 'sp_sigma', 4.7, 3.81),
267
+ (6, 8, 'pp_sigma', 6.8, 3.96),
268
+ (6, 8, 'pp_pi', 5.2, 4.11),
269
+ (1, 7, 'ss_sigma', 2.8, 3.33),
270
+ (1, 7, 'sp_sigma', 3.7, 3.48),
271
+ (6, 7, 'ss_sigma', 3.1, 3.63),
272
+ (6, 7, 'sp_sigma', 4.6, 3.78),
273
+ (6, 7, 'pp_sigma', 6.2, 3.9),
274
+ (6, 7, 'pp_pi', 4.7, 4.05),
275
+ (7, 7, 'ss_sigma', 3.1, 3.6),
276
+ (7, 7, 'sp_sigma', 4.4, 3.75),
277
+ (7, 7, 'pp_sigma', 6.5, 3.93),
278
+ (7, 7, 'pp_pi', 4.9, 4.08),
279
+ (7, 8, 'ss_sigma', 3.0, 3.57),
280
+ (7, 8, 'sp_sigma', 4.3, 3.72),
281
+ (7, 8, 'pp_sigma', 6.6, 3.945),
282
+ (7, 8, 'pp_pi', 5.0, 4.095),
283
+ (8, 8, 'ss_sigma', 3.1, 3.54),
284
+ (8, 8, 'sp_sigma', 4.5, 3.69),
285
+ (8, 8, 'pp_sigma', 6.9, 3.99),
286
+ (8, 8, 'pp_pi', 5.3, 4.14),
287
+ (1, 35, 'ss_sigma', 2.7, 3.24),
288
+ (1, 35, 'sp_sigma', 3.5, 3.42),
289
+ (6, 35, 'ss_sigma', 3.0, 3.54),
290
+ (6, 35, 'sp_sigma', 4.4, 3.72),
291
+ (6, 35, 'pp_sigma', 6.3, 3.87),
292
+ (6, 35, 'pp_pi', 4.9, 4.02),
293
+ (35, 35, 'ss_sigma', 2.9, 3.45),
294
+ (35, 35, 'sp_sigma', 4.3, 3.66),
295
+ (35, 35, 'pp_sigma', 6.2, 3.84),
296
+ (35, 35, 'pp_pi', 4.7, 3.99),
297
+ ]
298
+
299
+ for z1, z2, sk_type, A, alpha in sk_data:
300
+ self.sk_params[(z1, z2, sk_type)] = {'A': A, 'alpha': alpha}
301
+
302
+ # Repulsive parameters
303
+ # Format: (z1, z2, a, b, c)
304
+ rep_data = [
305
+ (1, 1, 0.8, 2.0, 1.0),
306
+ (1, 6, 1.0, 2.8, 1.15),
307
+ (6, 6, 0.9, 2.2, 1.1),
308
+ (1, 8, 0.85, 2.05, 1.02),
309
+ (6, 8, 0.95, 2.35, 1.1),
310
+ (8, 8, 1.0, 2.25, 1.12),
311
+ (1, 7, 0.875, 2.075, 1.035),
312
+ (6, 7, 0.9, 2.125, 1.085),
313
+ (7, 7, 0.975, 2.225, 1.115),
314
+ (7, 8, 0.925, 2.2, 1.095),
315
+ (1, 35, 1.1, 2.4, 1.15),
316
+ (6, 35, 1.15, 2.5, 1.2),
317
+ (35, 35, 1.2, 2.6, 1.25),
318
+ ]
319
+
320
+ for z1, z2, a, b, c in rep_data:
321
+ self.rep_params[(z1, z2)] = {'a': a, 'b': b, 'c': c}
322
+
323
+ # D4 dispersion parameters
324
+ self.simple_disp_params = {'s6': 1.0, 's8': 2.97, 'a1': 0.546, 'a2': 5.0}
325
+
326
+ # SRB parameters
327
+ # Format: (z1, z2, k, R0, alpha)
328
+ srb_data = [
329
+ (1, 6, -0.115, 1.9, 1.0),
330
+ (6, 6, -0.173, 2.66, 1.2),
331
+ (1, 8, -0.138, 1.71, 1.1),
332
+ (6, 8, -0.138, 2.47, 1.3),
333
+ (8, 8, -0.173, 2.28, 1.4),
334
+ (1, 7, -0.127, 1.81, 1.05),
335
+ (6, 7, -0.184, 2.57, 1.25),
336
+ (7, 7, -0.219, 2.38, 1.35),
337
+ (7, 8, -0.15, 2.42, 1.32),
338
+ (1, 35, -0.092, 2.66, 1.05),
339
+ (6, 35, -0.115, 3.04, 1.15),
340
+ (35, 35, -0.092, 3.42, 1.2),
341
+ ]
342
+
343
+ for z1, z2, k, R0, alpha in srb_data:
344
+ self.srb_params[(z1, z2)] = {'k': k, 'R0': R0, 'alpha': alpha}
345
+
346
+ # Global parameters
347
+ self.global_params = {'k_wh': 1.925}
348
+
349
+
350
+ def is_covalently_bonded(z1, z2, distance_angstrom, tolerance=1.2):
351
+ """
352
+ Determine if two atoms are covalently bonded based on their distance and covalent radii.
353
+
354
+ Args:
355
+ z1: Atomic number of first atom
356
+ z2: Atomic number of second atom
357
+ distance_angstrom: Distance between atoms in Angstrom (can be scalar or tensor)
358
+ tolerance: Multiplier for the sum of covalent radii (default: 1.2)
359
+ If distance <= tolerance * (r1 + r2), atoms are considered bonded
360
+
361
+ Returns:
362
+ Boolean or boolean tensor indicating if atoms are covalently bonded
363
+ """
364
+ # Get covalent radii from the SIMPLE_DISP_COVALENT_RADII dictionary
365
+ r1 = SIMPLE_DISP_COVALENT_RADII.get(z1, 1.5) # Default to 1.5 Å if not found
366
+ r2 = SIMPLE_DISP_COVALENT_RADII.get(z2, 1.5)
367
+
368
+ # Calculate threshold distance
369
+ threshold = tolerance * (r1 + r2)
370
+
371
+ # Handle both scalar and tensor inputs
372
+ if isinstance(distance_angstrom, torch.Tensor):
373
+ return distance_angstrom <= threshold
374
+ else:
375
+ return distance_angstrom <= threshold
376
+
377
+
378
+ class SQM1Calculator:
379
+ """
380
+ Main SQM1 calculator implementing the complete theory.
381
+
382
+ All internal calculations use atomic units (Hartree, Bohr).
383
+ Conversions to eV and Angstrom are only for output.
384
+ """
385
+ # All elements with parameters in param_sqm1.txt (86 elements from Z=1 to Z=86)
386
+ SUPPORTED_ELEMENTS = set(range(1, 87)) # H through Rn
387
+
388
+ def __init__(self, atomic_numbers, positions, charge=0, uhf=0, params=None, device='cpu', dtype=torch.float64):
389
+ """
390
+ Initialize SQM1 calculator.
391
+
392
+ Args:
393
+ atomic_numbers: List of atomic numbers
394
+ positions: Nx3 array of positions in Angstrom
395
+ charge: Total molecular charge
396
+ uhf: Number of unpaired electrons (0 for closed-shell)
397
+ params: SQM1Parameters object
398
+ device: PyTorch device ('cpu' or 'cuda')
399
+ dtype: PyTorch data type (torch.float32 or torch.float64)
400
+ """
401
+ self.device = device
402
+ self.dtype = dtype
403
+
404
+ # Convert to tensors
405
+ self.atomic_numbers = torch.tensor(atomic_numbers, dtype=torch.long, device=device)
406
+ positions_tensor = torch.tensor(positions, dtype=dtype, device=device)
407
+ self.positions = positions_tensor * ANGSTROM_TO_BOHR # Convert to Bohr
408
+ self.positions.requires_grad_(True) # Enable gradients for forces
409
+
410
+ self.charge = charge
411
+ self.uhf = uhf
412
+ self.params = params
413
+
414
+ if uhf != 0:
415
+ raise NotImplementedError("This implementation only supports closed-shell systems (uhf=0).")
416
+
417
+ for z in atomic_numbers:
418
+ if z not in self.SUPPORTED_ELEMENTS:
419
+ raise ValueError(f"Element with atomic number {z} is not supported.")
420
+
421
+ self.n_atoms = len(atomic_numbers)
422
+ self.valence_electrons = sum(self.params.element_params[z]['valence_e'] for z in atomic_numbers)
423
+ self.n_electrons = self.valence_electrons - self.charge
424
+ if self.n_electrons % 2 != 0:
425
+ raise ValueError("Odd number of electrons for a closed-shell calculation.")
426
+ self.n_occ = self.n_electrons // 2
427
+
428
+ self._build_basis_map()
429
+
430
+ def _build_basis_map(self):
431
+ """Build mapping from basis functions to atoms."""
432
+ self.basis_map = []
433
+ self.atom_map = []
434
+ current_ao = 0
435
+ for i, z in enumerate(self.atomic_numbers.tolist()):
436
+ self.atom_map.append(current_ao)
437
+ # s orbital
438
+ self.basis_map.append({'atom_idx': i, 'type': 's'})
439
+ current_ao += 1
440
+ # p orbitals
441
+ if self.params.element_params[z]['h_p'] is not None:
442
+ # Store specific p-orbital types ('px', 'py', 'pz')
443
+ # as required by the Slater-Koster transformation function.
444
+ for orbital_type in ['px', 'py', 'pz']:
445
+ self.basis_map.append({'atom_idx': i, 'type': orbital_type})
446
+ current_ao += 1
447
+ self.n_basis = len(self.basis_map)
448
+ self.atom_map.append(self.n_basis)
449
+
450
+ def _get_sk_integral(self, z1, z2, sk_type, R):
451
+ """
452
+ Get Slater-Koster integral value for given element pair and distance.
453
+
454
+ Args:
455
+ z1, z2: Atomic numbers
456
+ sk_type: Type of SK integral (e.g., 'ss_sigma', 'pp_sigma')
457
+ R: Distance in Bohr (tensor)
458
+
459
+ Returns:
460
+ Integral value (tensor)
461
+ """
462
+ z1_key, z2_key = sorted((z1, z2))
463
+ key = (z1_key, z2_key, sk_type)
464
+ if key in self.params.sk_params:
465
+ p = self.params.sk_params[key]
466
+ return p['A'] * torch.exp(-p['alpha'] * R)
467
+ return torch.tensor(0.0, dtype=self.dtype, device=self.device)
468
+
469
+ @staticmethod
470
+ def _slater_koster_transform(orbital_i_type, orbital_j_type, cosines, H_integrals, S_integrals):
471
+ """
472
+ Applies Slater-Koster transformation to get H and S matrix elements.
473
+
474
+ This is the rigorous implementation that handles:
475
+ - s, p, and d orbitals with complete angular dependence
476
+ - Proper sigma, pi, and delta bonding contributions
477
+ - Direction cosine-based angular factors
478
+
479
+ Args:
480
+ orbital_i_type, orbital_j_type: orbital types (e.g., 's', 'px', 'dxy')
481
+ cosines: direction cosines (l, m, n) - tensor
482
+ H_integrals, S_integrals: dictionaries of sigma, pi, delta integrals (tensors)
483
+
484
+ Returns:
485
+ H_ij, S_ij: Hamiltonian and overlap matrix elements (tensors)
486
+ """
487
+ l, m, n = cosines
488
+ type_i = orbital_i_type[0]
489
+ type_j = orbital_j_type[0]
490
+
491
+ # Get integral values, defaulting to 0 if not found
492
+ def get_H_integral(key):
493
+ return H_integrals.get(key, torch.tensor(0.0, dtype=l.dtype, device=l.device))
494
+ def get_S_integral(key):
495
+ return S_integrals.get(key, torch.tensor(0.0, dtype=l.dtype, device=l.device))
496
+
497
+ # s-s
498
+ if type_i == 's' and type_j == 's':
499
+ return get_H_integral('ss_sigma'), get_S_integral('ss_sigma')
500
+
501
+ # s-p
502
+ if type_i == 's' and type_j == 'p':
503
+ if orbital_j_type == 'px': return l * get_H_integral('sp_sigma'), l * get_S_integral('sp_sigma')
504
+ if orbital_j_type == 'py': return m * get_H_integral('sp_sigma'), m * get_S_integral('sp_sigma')
505
+ if orbital_j_type == 'pz': return n * get_H_integral('sp_sigma'), n * get_S_integral('sp_sigma')
506
+
507
+ if type_i == 'p' and type_j == 's':
508
+ if orbital_i_type == 'px': return l * get_H_integral('sp_sigma'), l * get_S_integral('sp_sigma')
509
+ if orbital_i_type == 'py': return m * get_H_integral('sp_sigma'), m * get_S_integral('sp_sigma')
510
+ if orbital_i_type == 'pz': return n * get_H_integral('sp_sigma'), n * get_S_integral('sp_sigma')
511
+
512
+ # p-p
513
+ if type_i == 'p' and type_j == 'p':
514
+ V_pp_sigma = get_H_integral('pp_sigma')
515
+ V_pp_pi = get_H_integral('pp_pi')
516
+ S_pp_sigma = get_S_integral('pp_sigma')
517
+ S_pp_pi = get_S_integral('pp_pi')
518
+
519
+ if orbital_i_type == 'px' and orbital_j_type == 'px':
520
+ return l*l*V_pp_sigma + (1-l*l)*V_pp_pi, l*l*S_pp_sigma + (1-l*l)*S_pp_pi
521
+ if orbital_i_type == 'py' and orbital_j_type == 'py':
522
+ return m*m*V_pp_sigma + (1-m*m)*V_pp_pi, m*m*S_pp_sigma + (1-m*m)*S_pp_pi
523
+ if orbital_i_type == 'pz' and orbital_j_type == 'pz':
524
+ return n*n*V_pp_sigma + (1-n*n)*V_pp_pi, n*n*S_pp_sigma + (1-n*n)*S_pp_pi
525
+ if (orbital_i_type == 'px' and orbital_j_type == 'py') or (orbital_i_type == 'py' and orbital_j_type == 'px'):
526
+ return l*m*(V_pp_sigma - V_pp_pi), l*m*(S_pp_sigma - S_pp_pi)
527
+ if (orbital_i_type == 'px' and orbital_j_type == 'pz') or (orbital_i_type == 'pz' and orbital_j_type == 'px'):
528
+ return l*n*(V_pp_sigma - V_pp_pi), l*n*(S_pp_sigma - S_pp_pi)
529
+ if (orbital_i_type == 'py' and orbital_j_type == 'pz') or (orbital_i_type == 'pz' and orbital_j_type == 'py'):
530
+ return m*n*(V_pp_sigma - V_pp_pi), m*n*(S_pp_sigma - S_pp_pi)
531
+
532
+ # s-d
533
+ if type_i == 's' and type_j == 'd':
534
+ V_sd_sigma = get_H_integral('sd_sigma')
535
+ S_sd_sigma = get_S_integral('sd_sigma')
536
+ sqrt3 = torch.tensor(3.0, dtype=l.dtype, device=l.device).sqrt()
537
+ if orbital_j_type == 'dxy': return sqrt3*l*m*V_sd_sigma, sqrt3*l*m*S_sd_sigma
538
+ if orbital_j_type == 'dyz': return sqrt3*m*n*V_sd_sigma, sqrt3*m*n*S_sd_sigma
539
+ if orbital_j_type == 'dzx': return sqrt3*n*l*V_sd_sigma, sqrt3*n*l*S_sd_sigma
540
+ if orbital_j_type == 'dx2-y2': return sqrt3/2*(l*l-m*m)*V_sd_sigma, sqrt3/2*(l*l-m*m)*S_sd_sigma
541
+ if orbital_j_type == 'd3z2-r2': return (n*n-0.5*(l*l+m*m))*V_sd_sigma, (n*n-0.5*(l*l+m*m))*S_sd_sigma
542
+
543
+ if type_i == 'd' and type_j == 's':
544
+ return SQM1Calculator._slater_koster_transform(orbital_j_type, orbital_i_type, cosines, H_integrals, S_integrals)
545
+
546
+ # p-d
547
+ if type_i == 'p' and type_j == 'd':
548
+ V_pd_sigma = get_H_integral('pd_sigma')
549
+ V_pd_pi = get_H_integral('pd_pi')
550
+ S_pd_sigma = get_S_integral('pd_sigma')
551
+ S_pd_pi = get_S_integral('pd_pi')
552
+ sqrt3 = torch.sqrt(torch.tensor(3.0, dtype=l.dtype, device=l.device))
553
+
554
+ if orbital_i_type == 'px':
555
+ if orbital_j_type == 'dxy': return sqrt3*l*l*m*V_pd_sigma + m*(1-2*l*l)*V_pd_pi, sqrt3*l*l*m*S_pd_sigma + m*(1-2*l*l)*S_pd_pi
556
+ if orbital_j_type == 'dyz': return sqrt3*l*m*n*V_pd_sigma - 2*l*m*n*V_pd_pi, sqrt3*l*m*n*S_pd_sigma - 2*l*m*n*S_pd_pi
557
+ if orbital_j_type == 'dzx': return sqrt3*l*l*n*V_pd_sigma + n*(1-2*l*l)*V_pd_pi, sqrt3*l*l*n*S_pd_sigma + n*(1-2*l*l)*S_pd_pi
558
+ if orbital_j_type == 'dx2-y2': return sqrt3/2*l*(l*l-m*m)*V_pd_sigma + l*(1-(l*l-m*m))*V_pd_pi, sqrt3/2*l*(l*l-m*m)*S_pd_sigma + l*(1-(l*l-m*m))*S_pd_pi
559
+ if orbital_j_type == 'd3z2-r2': return l*(n*n-0.5*(l*l+m*m))*V_pd_sigma - sqrt3*l*n*n*V_pd_pi, l*(n*n-0.5*(l*l+m*m))*S_pd_sigma - sqrt3*l*n*n*S_pd_pi
560
+ if orbital_i_type == 'py':
561
+ if orbital_j_type == 'dxy': return sqrt3*l*m*m*V_pd_sigma + l*(1-2*m*m)*V_pd_pi, sqrt3*l*m*m*S_pd_sigma + l*(1-2*m*m)*S_pd_pi
562
+ if orbital_j_type == 'dyz': return sqrt3*m*m*n*V_pd_sigma + n*(1-2*m*m)*V_pd_pi, sqrt3*m*m*n*S_pd_sigma + n*(1-2*m*m)*S_pd_pi
563
+ if orbital_j_type == 'dzx': return sqrt3*l*m*n*V_pd_sigma - 2*l*m*n*V_pd_pi, sqrt3*l*m*n*S_pd_sigma - 2*l*m*n*S_pd_pi
564
+ if orbital_j_type == 'dx2-y2': return sqrt3/2*m*(l*l-m*m)*V_pd_sigma - m*(1+(l*l-m*m))*V_pd_pi, sqrt3/2*m*(l*l-m*m)*S_pd_sigma - m*(1+(l*l-m*m))*S_pd_pi
565
+ if orbital_j_type == 'd3z2-r2': return m*(n*n-0.5*(l*l+m*m))*V_pd_sigma - sqrt3*m*n*n*V_pd_pi, m*(n*n-0.5*(l*l+m*m))*S_pd_sigma - sqrt3*m*n*n*S_pd_pi
566
+ if orbital_i_type == 'pz':
567
+ if orbital_j_type == 'dxy': return sqrt3*l*m*n*V_pd_sigma - 2*l*m*n*V_pd_pi, sqrt3*l*m*n*S_pd_sigma - 2*l*m*n*S_pd_pi
568
+ if orbital_j_type == 'dyz': return sqrt3*m*n*n*V_pd_sigma + m*(1-2*n*n)*V_pd_pi, sqrt3*m*n*n*S_pd_sigma + m*(1-2*n*n)*S_pd_pi
569
+ if orbital_j_type == 'dzx': return sqrt3*n*n*l*V_pd_sigma + l*(1-2*n*n)*V_pd_pi, sqrt3*n*n*l*S_pd_sigma + l*(1-2*n*n)*S_pd_pi
570
+ if orbital_j_type == 'dx2-y2': return sqrt3/2*n*(l*l-m*m)*V_pd_sigma - n*(l*l-m*m)*V_pd_pi, sqrt3/2*n*(l*l-m*m)*S_pd_sigma - n*(l*l-m*m)*S_pd_pi
571
+ if orbital_j_type == 'd3z2-r2': return n*(n*n-0.5*(l*l+m*m))*V_pd_sigma + sqrt3*n*(l*l+m*m)*V_pd_pi, n*(n*n-0.5*(l*l+m*m))*S_pd_sigma + sqrt3*n*(l*l+m*m)*S_pd_pi
572
+
573
+ if type_i == 'd' and type_j == 'p':
574
+ return SQM1Calculator._slater_koster_transform(orbital_j_type, orbital_i_type, cosines, H_integrals, S_integrals)
575
+
576
+ # d-d
577
+ if type_i == 'd' and type_j == 'd':
578
+ V_dd_sigma = get_H_integral('dd_sigma')
579
+ V_dd_pi = get_H_integral('dd_pi')
580
+ V_dd_delta = get_H_integral('dd_delta')
581
+ S_dd_sigma = get_S_integral('dd_sigma')
582
+ S_dd_pi = get_S_integral('dd_pi')
583
+ S_dd_delta = get_S_integral('dd_delta')
584
+ sqrt3 = torch.sqrt(torch.tensor(3.0, dtype=l.dtype, device=l.device))
585
+
586
+ if orbital_i_type == 'dxy':
587
+ if orbital_j_type == 'dxy': return 3*l*l*m*m*V_dd_sigma + (l*l+m*m-4*l*l*m*m)*V_dd_pi + (n*n+l*l*m*m)*V_dd_delta, 3*l*l*m*m*S_dd_sigma + (l*l+m*m-4*l*l*m*m)*S_dd_pi + (n*n+l*l*m*m)*S_dd_delta
588
+ if orbital_j_type == 'dyz': return 3*l*m*m*n*V_dd_sigma + l*n*(1-4*m*m)*V_dd_pi + l*n*(m*m-1)*V_dd_delta, 3*l*m*m*n*S_dd_sigma + l*n*(1-4*m*m)*S_dd_pi + l*n*(m*m-1)*S_dd_delta
589
+ if orbital_j_type == 'dzx': return 3*l*l*m*n*V_dd_sigma + m*n*(1-4*l*l)*V_dd_pi + m*n*(l*l-1)*V_dd_delta, 3*l*l*m*n*S_dd_sigma + m*n*(1-4*l*l)*S_dd_pi + m*n*(l*l-1)*S_dd_delta
590
+ if orbital_j_type == 'dx2-y2': return 1.5*l*m*(l*l-m*m)*V_dd_sigma + 2*l*m*(m*m-l*l)*V_dd_pi + 0.5*l*m*(l*l-m*m)*V_dd_delta, 1.5*l*m*(l*l-m*m)*S_dd_sigma + 2*l*m*(m*m-l*l)*S_dd_pi + 0.5*l*m*(l*l-m*m)*S_dd_delta
591
+ if orbital_j_type == 'd3z2-r2': return sqrt3*l*m*(n*n-0.5*(l*l+m*m))*V_dd_sigma - 2*sqrt3*l*m*n*n*V_dd_pi + 0.5*sqrt3*l*m*(l*l+m*m)*V_dd_delta, sqrt3*l*m*(n*n-0.5*(l*l+m*m))*S_dd_sigma - 2*sqrt3*l*m*n*n*S_dd_pi + 0.5*sqrt3*l*m*(l*l+m*m)*S_dd_delta
592
+ if orbital_i_type == 'dyz':
593
+ if orbital_j_type == 'dyz': return 3*m*m*n*n*V_dd_sigma + (m*m+n*n-4*m*m*n*n)*V_dd_pi + (l*l+m*m*n*n)*V_dd_delta, 3*m*m*n*n*S_dd_sigma + (m*m+n*n-4*m*m*n*n)*S_dd_pi + (l*l+m*m*n*n)*S_dd_delta
594
+ if orbital_j_type == 'dzx': return 3*l*m*n*n*V_dd_sigma + l*m*(1-4*n*n)*V_dd_pi + l*m*(n*n-1)*V_dd_delta, 3*l*m*n*n*S_dd_sigma + l*m*(1-4*n*n)*S_dd_pi + l*m*(n*n-1)*S_dd_delta
595
+ if orbital_j_type == 'dx2-y2': return 1.5*m*n*(l*l-m*m)*V_dd_sigma - m*n*(1+2*(l*l-m*m))*V_dd_pi + 0.5*m*n*(2-(l*l-m*m))*V_dd_delta, 1.5*m*n*(l*l-m*m)*S_dd_sigma - m*n*(1+2*(l*l-m*m))*S_dd_pi + 0.5*m*n*(2-(l*l-m*m))*S_dd_delta
596
+ if orbital_j_type == 'd3z2-r2': return sqrt3*m*n*(n*n-0.5*(l*l+m*m))*V_dd_sigma + sqrt3*m*n*(l*l+m*m-n*n)*V_dd_pi - 0.5*sqrt3*m*n*(l*l+m*m)*V_dd_delta, sqrt3*m*n*(n*n-0.5*(l*l+m*m))*S_dd_sigma + sqrt3*m*n*(l*l+m*m-n*n)*S_dd_pi - 0.5*sqrt3*m*n*(l*l+m*m)*S_dd_delta
597
+ if orbital_i_type == 'dzx':
598
+ if orbital_j_type == 'dzx': return 3*n*n*l*l*V_dd_sigma + (n*n+l*l-4*n*n*l*l)*V_dd_pi + (m*m+n*n*l*l)*V_dd_delta, 3*n*n*l*l*S_dd_sigma + (n*n+l*l-4*n*n*l*l)*S_dd_pi + (m*m+n*n*l*l)*S_dd_delta
599
+ if orbital_j_type == 'dx2-y2': return 1.5*n*l*(l*l-m*m)*V_dd_sigma + n*l*(1-2*(l*l-m*m))*V_dd_pi - 0.5*n*l*(2+(l*l-m*m))*V_dd_delta, 1.5*n*l*(l*l-m*m)*S_dd_sigma + n*l*(1-2*(l*l-m*m))*S_dd_pi - 0.5*n*l*(2+(l*l-m*m))*S_dd_delta
600
+ if orbital_j_type == 'd3z2-r2': return sqrt3*n*l*(n*n-0.5*(l*l+m*m))*V_dd_sigma + sqrt3*n*l*(m*m+n*n-l*l)*V_dd_pi - 0.5*sqrt3*n*l*(l*l+m*m)*V_dd_delta, sqrt3*n*l*(n*n-0.5*(l*l+m*m))*S_dd_sigma + sqrt3*n*l*(m*m+n*n-l*l)*S_dd_pi - 0.5*sqrt3*n*l*(l*l+m*m)*S_dd_delta
601
+ if orbital_i_type == 'dx2-y2':
602
+ if orbital_j_type == 'dx2-y2': return 0.75*(l*l-m*m)*(l*l-m*m)*V_dd_sigma + (l*l+m*m)*(1-0.5*(l*l-m*m)*(l*l-m*m))*V_dd_pi + (1-0.5*(l*l+m*m)*(l*l+m*m))*V_dd_delta, 0.75*(l*l-m*m)*(l*l-m*m)*S_dd_sigma + (l*l+m*m)*(1-0.5*(l*l-m*m)*(l*l-m*m))*S_dd_pi + (1-0.5*(l*l+m*m)*(l*l+m*m))*S_dd_delta
603
+ if orbital_j_type == 'd3z2-r2': return 0.5*sqrt3*(l*l-m*m)*(n*n-0.5*(l*l+m*m))*V_dd_sigma - sqrt3*(l*l-m*m)*n*n*V_dd_pi - 0.5*sqrt3*(l*l-m*m)*(l*l+m*m)*V_dd_delta, 0.5*sqrt3*(l*l-m*m)*(n*n-0.5*(l*l+m*m))*S_dd_sigma - sqrt3*(l*l-m*m)*n*n*S_dd_pi - 0.5*sqrt3*(l*l-m*m)*(l*l+m*m)*S_dd_delta
604
+ if orbital_i_type == 'd3z2-r2':
605
+ if orbital_j_type == 'd3z2-r2': return (n*n-0.5*(l*l+m*m))*(n*n-0.5*(l*l+m*m))*V_dd_sigma + 3*n*n*(l*l+m*m)*V_dd_pi + 0.75*(l*l+m*m)*(l*l+m*m)*V_dd_delta, (n*n-0.5*(l*l+m*m))*(n*n-0.5*(l*l+m*m))*S_dd_sigma + 3*n*n*(l*l+m*m)*S_dd_pi + 0.75*(l*l+m*m)*(l*l+m*m)*S_dd_delta
606
+
607
+ zero = torch.tensor(0.0, dtype=l.dtype, device=l.device)
608
+ return zero, zero
609
+
610
+ def _build_matrices(self):
611
+ """
612
+ Build Hamiltonian and Overlap matrices using rigorous Slater-Koster method.
613
+
614
+ Returns:
615
+ Tuple of (H, S) matrices in atomic units (tensors)
616
+ """
617
+ S = torch.zeros((self.n_basis, self.n_basis), dtype=self.dtype, device=self.device)
618
+ H = torch.zeros((self.n_basis, self.n_basis), dtype=self.dtype, device=self.device)
619
+
620
+ # On-site (diagonal) blocks
621
+ for i in range(self.n_atoms):
622
+ z = self.atomic_numbers[i].item()
623
+ p = self.params.element_params[z]
624
+ start, end = self.atom_map[i], self.atom_map[i+1]
625
+ S[start:end, start:end] = torch.eye(end-start, dtype=self.dtype, device=self.device)
626
+ H[start, start] = p['h_s'] * EV_TO_HARTREE # Convert to Hartree
627
+ if end - start == 4: # s and p
628
+ H[start+1:end, start+1:end] = torch.eye(3, dtype=self.dtype, device=self.device) * p['h_p'] * EV_TO_HARTREE
629
+
630
+ # Off-site (off-diagonal) blocks
631
+ sk_types = [
632
+ 'ss_sigma', 'sp_sigma', 'pp_sigma', 'pp_pi',
633
+ 'sd_sigma', 'pd_sigma', 'pd_pi',
634
+ 'dd_sigma', 'dd_pi', 'dd_delta'
635
+ ]
636
+
637
+ for i in range(self.n_atoms):
638
+ zi = self.atomic_numbers[i].item()
639
+ i_start, i_end = self.atom_map[i], self.atom_map[i+1]
640
+
641
+ for j in range(i + 1, self.n_atoms):
642
+ zj = self.atomic_numbers[j].item()
643
+ j_start, j_end = self.atom_map[j], self.atom_map[j+1]
644
+
645
+ R_vec = self.positions[i] - self.positions[j]
646
+ dist = torch.linalg.norm(R_vec)
647
+
648
+ if dist < 1e-9:
649
+ continue
650
+
651
+ cosines = R_vec / dist
652
+
653
+ # Build the dictionary of two-center integrals
654
+ sk_integrals = {}
655
+ for sk_type in sk_types:
656
+ val = self._get_sk_integral(zi, zj, sk_type, dist)
657
+ sk_integrals[sk_type] = val
658
+
659
+ # Iterate over basis functions for this atom pair
660
+ for mu in range(i_start, i_end):
661
+ type_i = self.basis_map[mu]['type']
662
+
663
+ for nu in range(j_start, j_end):
664
+ type_j = self.basis_map[nu]['type']
665
+
666
+ # Calculate H_ij and S_ij using the rigorous SK transform
667
+ H_ij, S_ij = self._slater_koster_transform(
668
+ type_i, type_j, cosines,
669
+ sk_integrals, sk_integrals
670
+ )
671
+
672
+ S[mu, nu] = S_ij
673
+ S[nu, mu] = S_ij
674
+ H[mu, nu] = H_ij
675
+ H[nu, mu] = H_ij
676
+
677
+ return H, S
678
+
679
+ def _solve_eht(self):
680
+ """
681
+ Solve Extended Hückel Theory eigenvalue problem.
682
+
683
+ Returns:
684
+ E_EHT in Hartree (relative to isolated atoms)
685
+ """
686
+ H, S = self._build_matrices()
687
+
688
+ # Solve generalized eigenvalue problem HC = SCE
689
+ # Use Cholesky decomposition: S = L L^T
690
+ # Then solve L^{-1} H L^{-T} y = y E, where C = L^{-T} y
691
+ try:
692
+ L = torch.linalg.cholesky(S)
693
+ L_inv = torch.linalg.inv(L)
694
+ H_prime = L_inv @ H @ L_inv.T
695
+ eigvals, y = torch.linalg.eigh(H_prime)
696
+ eigvecs = L_inv.T @ y
697
+ except RuntimeError:
698
+ # If Cholesky fails, use alternative approach
699
+ # Transform to standard eigenvalue problem using eigendecomposition of S
700
+ eigvals_S, eigvecs_S = torch.linalg.eigh(S)
701
+ # Filter out near-zero eigenvalues
702
+ threshold = 1e-10
703
+ idx = eigvals_S > threshold
704
+ eigvals_S = eigvals_S[idx]
705
+ eigvecs_S = eigvecs_S[:, idx]
706
+ # Transform H
707
+ S_sqrt_inv = eigvecs_S @ torch.diag(1.0 / torch.sqrt(eigvals_S)) @ eigvecs_S.T
708
+ H_prime = S_sqrt_inv @ H @ S_sqrt_inv
709
+ eigvals, y = torch.linalg.eigh(H_prime)
710
+ eigvecs = S_sqrt_inv @ y
711
+
712
+ # Sort eigenvalues
713
+ idx = eigvals.argsort()
714
+ self.eigvals = eigvals[idx]
715
+ self.eigvecs = eigvecs[:, idx]
716
+
717
+ # Density matrix for occupied orbitals
718
+ self.density_matrix = 2 * self.eigvecs[:, :self.n_occ] @ self.eigvecs[:, :self.n_occ].T
719
+
720
+ # Band structure energy
721
+ e_bs = torch.sum(self.eigvals[:self.n_occ]) * 2
722
+
723
+ # Calculate atomic reference energy
724
+ e_ref = self._calculate_atomic_reference_energy()
725
+
726
+ # Return molecular energy relative to isolated atoms
727
+ e_eht = e_bs - e_ref
728
+ return e_eht
729
+
730
+ def _calculate_atomic_reference_energy(self):
731
+ """
732
+ Calculate the sum of isolated atomic energies.
733
+
734
+ Returns:
735
+ E_ref in Hartree (tensor)
736
+ """
737
+ e_ref = torch.tensor(0.0, dtype=self.dtype, device=self.device)
738
+ for z in self.atomic_numbers.tolist():
739
+ p = self.params.element_params[z]
740
+ valence_e = p['valence_e']
741
+
742
+ e_s = p['h_s'] * EV_TO_HARTREE
743
+ remaining_e = valence_e
744
+
745
+ # Fill s orbital first (max 2 electrons)
746
+ if remaining_e >= 2:
747
+ e_ref += 2 * e_s
748
+ remaining_e -= 2
749
+ else:
750
+ e_ref += remaining_e * e_s
751
+ remaining_e = 0
752
+
753
+ # Fill p orbitals if present and if there are remaining electrons
754
+ if p['h_p'] is not None and remaining_e > 0:
755
+ e_p = p['h_p'] * EV_TO_HARTREE
756
+ e_ref += min(remaining_e, 6) * e_p
757
+
758
+ return e_ref
759
+
760
+ def _solve_eeq(self):
761
+ """
762
+ Solve Electronegativity Equilibration Model (EEQ) equations.
763
+
764
+ Reference: SQM1 Paper, Section 2.1.1
765
+
766
+ Returns:
767
+ E_IES in Hartree (tensor)
768
+ """
769
+ A = torch.zeros((self.n_atoms, self.n_atoms), dtype=self.dtype, device=self.device)
770
+ b = torch.zeros(self.n_atoms, dtype=self.dtype, device=self.device)
771
+
772
+ for i in range(self.n_atoms):
773
+ zi = self.atomic_numbers[i].item()
774
+ p_i = self.params.element_params[zi]
775
+ b[i] = -p_i['EN_A'] * EV_TO_HARTREE
776
+
777
+ gamma_AA = p_i['J_AA_param'] * (p_i['Z_eff'] ** p_i['alpha'])
778
+ A[i, i] = gamma_AA * EV_TO_HARTREE
779
+
780
+ for j in range(i + 1, self.n_atoms):
781
+ zj = self.atomic_numbers[j].item()
782
+ pj = self.params.element_params[zj]
783
+ R_ij = torch.linalg.norm(self.positions[i] - self.positions[j])
784
+
785
+ sigma_A = 0.7 / torch.sqrt(torch.tensor(max(abs(p_i['EN_A']), 0.5), dtype=self.dtype, device=self.device))
786
+ sigma_B = 0.7 / torch.sqrt(torch.tensor(max(abs(pj['EN_A']), 0.5), dtype=self.dtype, device=self.device))
787
+ sigma_sum = sigma_A + sigma_B
788
+
789
+ gamma_AB = 1.0 / torch.sqrt(R_ij**2 + sigma_sum**2)
790
+ A[i, j] = A[j, i] = gamma_AB
791
+
792
+ # Lagrange multiplier for charge constraint
793
+ A_ext = torch.ones((self.n_atoms + 1, self.n_atoms + 1), dtype=self.dtype, device=self.device)
794
+ A_ext[:self.n_atoms, :self.n_atoms] = A
795
+ A_ext[self.n_atoms, self.n_atoms] = 0
796
+
797
+ b_ext = torch.zeros(self.n_atoms + 1, dtype=self.dtype, device=self.device)
798
+ b_ext[:self.n_atoms] = b
799
+ b_ext[self.n_atoms] = self.charge
800
+
801
+ x = torch.linalg.solve(A_ext, b_ext)
802
+ self.eeq_charges = x[:self.n_atoms]
803
+
804
+ # Calculate electrostatic energy (IES)
805
+ E_ies = 0.5 * self.eeq_charges @ A @ self.eeq_charges + b @ self.eeq_charges
806
+ return E_ies
807
+
808
+ def _calculate_coordination_numbers(self):
809
+ """
810
+ Calculate fractional coordination numbers for all atoms.
811
+ Uses SimpleDispersion-style coordination number definition.
812
+
813
+ Returns:
814
+ Array of coordination numbers (tensor)
815
+ """
816
+ cn = torch.zeros(self.n_atoms, dtype=self.dtype, device=self.device)
817
+ for i in range(self.n_atoms):
818
+ zi = self.atomic_numbers[i].item()
819
+ r_cov_i = SIMPLE_DISP_COVALENT_RADII.get(zi, 1.5)
820
+
821
+ for j in range(self.n_atoms):
822
+ if i == j:
823
+ continue
824
+
825
+ zj = self.atomic_numbers[j].item()
826
+ R_ij_bohr = torch.linalg.norm(self.positions[i] - self.positions[j])
827
+ R_ij_ang = R_ij_bohr * BOHR_TO_ANGSTROM
828
+
829
+ if R_ij_ang > SIMPLE_DISP_CN_CUTOFF:
830
+ continue
831
+
832
+ r_cov_j = SIMPLE_DISP_COVALENT_RADII.get(zj, 1.5)
833
+ r_cov_sum = r_cov_i + r_cov_j
834
+ argument = SIMPLE_DISP_CN_K1 * (SIMPLE_DISP_CN_K2 * r_cov_sum / R_ij_ang - 1.0)
835
+ cn[i] += 1.0 / (1.0 + torch.exp(-argument))
836
+
837
+ return cn
838
+
839
+ def _calculate_repulsive_energy(self):
840
+ """
841
+ Calculate repulsive potential energy with environment-dependent scaling.
842
+
843
+ Returns:
844
+ E_rep in Hartree (tensor)
845
+ """
846
+ E_rep = torch.tensor(0.0, dtype=self.dtype, device=self.device)
847
+
848
+ cn = self._calculate_coordination_numbers()
849
+
850
+ for i in range(self.n_atoms):
851
+ for j in range(i + 1, self.n_atoms):
852
+ zi, zj = sorted((self.atomic_numbers[i].item(), self.atomic_numbers[j].item()))
853
+ if (zi, zj) not in self.params.rep_params:
854
+ continue
855
+
856
+ p = self.params.rep_params[(zi, zj)]
857
+ R_ij = torch.linalg.norm(self.positions[i] - self.positions[j])
858
+
859
+ Z_eff_i = self.params.element_params[self.atomic_numbers[i].item()]['Z_eff']
860
+ Z_eff_j = self.params.element_params[self.atomic_numbers[j].item()]['Z_eff']
861
+
862
+ # Exponential repulsive potential
863
+ term = torch.exp(p['b'] * (1.0 - (R_ij / (p['a'] * (1/Z_eff_i + 1/Z_eff_j)))**p['c']))
864
+ base_rep = (Z_eff_i * Z_eff_j / R_ij) * term
865
+
866
+ # Environment-dependent scaling
867
+ cn_i = cn[i] if self.atomic_numbers[i].item() == zi else cn[j]
868
+ cn_j = cn[j] if self.atomic_numbers[j].item() == zj else cn[i]
869
+
870
+ cn_scale_i = 1.0 + 0.5 * torch.exp(-(cn_i - 1.0) / 2.0)
871
+ cn_scale_j = 1.0 + 0.5 * torch.exp(-(cn_j - 1.0) / 2.0)
872
+ cn_scaling = torch.sqrt(cn_scale_i * cn_scale_j)
873
+
874
+ E_rep += base_rep * cn_scaling
875
+
876
+ return E_rep * EV_TO_HARTREE
877
+
878
+ def _calculate_simple_dispersion(self):
879
+ """
880
+ Calculate SimpleDispersion energy with coordination number dependence and charge scaling.
881
+
882
+ Reference: Caldeweyher et al., J. Chem. Phys. 150, 154122 (2019)
883
+
884
+ Returns:
885
+ E_SimpleDisp in Hartree (tensor)
886
+ """
887
+ E_simple_disp = torch.tensor(0.0, dtype=self.dtype, device=self.device)
888
+ p_simple_disp = self.params.simple_disp_params
889
+
890
+ # Calculate coordination numbers
891
+ cn = torch.zeros(self.n_atoms, dtype=self.dtype, device=self.device)
892
+ for i in range(self.n_atoms):
893
+ zi = self.atomic_numbers[i].item()
894
+ r_cov_i = SIMPLE_DISP_COVALENT_RADII.get(zi, 1.5)
895
+
896
+ for j in range(self.n_atoms):
897
+ if i == j:
898
+ continue
899
+
900
+ zj = self.atomic_numbers[j].item()
901
+ R_ij_bohr = torch.linalg.norm(self.positions[i] - self.positions[j])
902
+ R_ij_ang = R_ij_bohr * BOHR_TO_ANGSTROM
903
+
904
+ if R_ij_ang > SIMPLE_DISP_CN_CUTOFF:
905
+ continue
906
+
907
+ r_cov_j = SIMPLE_DISP_COVALENT_RADII.get(zj, 1.5)
908
+ r_cov_sum = r_cov_i + r_cov_j
909
+ argument = SIMPLE_DISP_CN_K1 * (SIMPLE_DISP_CN_K2 * r_cov_sum / R_ij_ang - 1.0)
910
+ cn[i] += 1.0 / (1.0 + torch.exp(-argument))
911
+
912
+ # Calculate charge-dependent C6 coefficients
913
+ c6 = torch.zeros(self.n_atoms, dtype=self.dtype, device=self.device)
914
+ for i in range(self.n_atoms):
915
+ zi = self.atomic_numbers[i].item()
916
+ p_i = self.params.element_params[zi]
917
+
918
+ c6_ref = p_i['C6_ref']
919
+ q_i = self.eeq_charges[i]
920
+
921
+ charge_scaling = 1.0 / (1.0 + 0.5 * torch.abs(q_i))
922
+ cn_scaling = 1.0 / (1.0 + 0.08 * cn[i])
923
+
924
+ c6[i] = c6_ref * charge_scaling * cn_scaling
925
+
926
+ # Identify bonded pairs
927
+ bonded_pairs = set()
928
+ for i in range(self.n_atoms):
929
+ zi = self.atomic_numbers[i].item()
930
+ r_cov_i = SIMPLE_DISP_COVALENT_RADII.get(zi, 1.5)
931
+
932
+ for j in range(i + 1, self.n_atoms):
933
+ zj = self.atomic_numbers[j].item()
934
+ r_cov_j = SIMPLE_DISP_COVALENT_RADII.get(zj, 1.5)
935
+ R_ij_ang = torch.linalg.norm(self.positions[i] - self.positions[j]) * BOHR_TO_ANGSTROM
936
+
937
+ if R_ij_ang < 1.3 * (r_cov_i + r_cov_j):
938
+ bonded_pairs.add((i, j))
939
+
940
+ # Calculate dispersion energy
941
+ for i in range(self.n_atoms):
942
+ for j in range(i + 1, self.n_atoms):
943
+ R_ij_bohr = torch.linalg.norm(self.positions[i] - self.positions[j])
944
+
945
+ c6_ij = torch.sqrt(c6[i] * c6[j])
946
+
947
+ alpha_i = self.params.element_params[self.atomic_numbers[i].item()]['alpha_ref']
948
+ alpha_j = self.params.element_params[self.atomic_numbers[j].item()]['alpha_ref']
949
+ c8_ij = 3.0 * c6_ij * torch.sqrt(torch.tensor(alpha_i * alpha_j, dtype=self.dtype, device=self.device))
950
+
951
+ if c6_ij > 1e-10:
952
+ sqrt3 = torch.tensor(3.0, dtype=self.dtype, device=self.device).sqrt()
953
+ R0_ij_base = p_simple_disp['a1'] * torch.sqrt(sqrt3 * c8_ij / c6_ij) + p_simple_disp['a2']
954
+ else:
955
+ R0_ij_base = p_simple_disp['a2']
956
+
957
+ # Enhanced damping for intramolecular interactions
958
+ if (i, j) in bonded_pairs:
959
+ zi = self.atomic_numbers[i].item()
960
+ zj = self.atomic_numbers[j].item()
961
+ EN_i = self.params.element_params[zi]['EN_A']
962
+ EN_j = self.params.element_params[zj]['EN_A']
963
+ delta_EN = abs(EN_i - EN_j)
964
+
965
+ avg_alpha = 0.5 * (alpha_i + alpha_j)
966
+ Pol_AB = min(1.0, (delta_EN / 3.0) * (avg_alpha / 20.0))
967
+
968
+ k_damp = 0.5
969
+ R0_ij = R0_ij_base * (1.0 + k_damp * Pol_AB)
970
+ else:
971
+ R0_ij = R0_ij_base
972
+
973
+ R6 = R_ij_bohr**6
974
+ R8 = R_ij_bohr**8
975
+ R0_6 = R0_ij**6
976
+ R0_8 = R0_ij**8
977
+
978
+ term6 = p_simple_disp['s6'] * c6_ij / (R6 + R0_6)
979
+ term8 = p_simple_disp['s8'] * c8_ij / (R8 + R0_8)
980
+
981
+ E_simple_disp -= (term6 + term8)
982
+
983
+ return E_simple_disp
984
+
985
+ def _calculate_srb_energy(self):
986
+ """
987
+ Calculate Short-Range Basis (SRB) correction energy.
988
+
989
+ Returns:
990
+ E_SRB in Hartree (tensor)
991
+ """
992
+ E_srb = torch.tensor(0.0, dtype=self.dtype, device=self.device)
993
+ for i in range(self.n_atoms):
994
+ for j in range(i + 1, self.n_atoms):
995
+ zi, zj = sorted((self.atomic_numbers[i].item(), self.atomic_numbers[j].item()))
996
+ if (zi, zj) not in self.params.srb_params:
997
+ continue
998
+
999
+ p = self.params.srb_params[(zi, zj)]
1000
+ R_ij = torch.linalg.norm(self.positions[i] - self.positions[j])
1001
+
1002
+ p_i = self.params.element_params[self.atomic_numbers[i].item()]
1003
+ p_j = self.params.element_params[self.atomic_numbers[j].item()]
1004
+
1005
+ delta_EN = abs(p_i['EN_A'] - p_j['EN_A'])
1006
+
1007
+ alpha_sum = p_i['alpha_ref'] + p_j['alpha_ref']
1008
+ k_pol_damp = 0.02
1009
+
1010
+ g_scal_base = delta_EN * delta_EN
1011
+ g_scal = g_scal_base / (1.0 + k_pol_damp * alpha_sum)
1012
+
1013
+ E_srb += p['k'] * g_scal * torch.exp(-p['alpha'] * (R_ij - p['R0'])**2)
1014
+
1015
+ return E_srb * EV_TO_HARTREE
1016
+
1017
+ def calculate_total_energy(self, coords=None, atomic_numbers=None, total_charge=None, external_electric_field=None):
1018
+ """
1019
+ Calculate total SQM1 energy.
1020
+
1021
+ E_total = E_EHT + E_IES + E_rep + E_SimpleDisp + E_SRB + E_field
1022
+
1023
+ Args:
1024
+ coords: Optional coordinates tensor (n_atoms, 3) in Bohr with requires_grad=True
1025
+ atomic_numbers: Optional atomic numbers tensor
1026
+ total_charge: Optional total charge
1027
+ external_electric_field: Optional external electric field vector (3,) in atomic units
1028
+
1029
+ Returns:
1030
+ Total energy as a scalar torch.Tensor
1031
+ """
1032
+ # Use instance attributes if not provided
1033
+ if coords is None:
1034
+ coords = self.positions
1035
+ if atomic_numbers is None:
1036
+ atomic_numbers = self.atomic_numbers
1037
+ if total_charge is None:
1038
+ total_charge = self.charge
1039
+
1040
+ # Temporarily update positions for calculation
1041
+ original_positions = self.positions
1042
+ self.positions = coords
1043
+
1044
+ e_eht = self._solve_eht()
1045
+ e_ies = self._solve_eeq()
1046
+ e_rep = self._calculate_repulsive_energy()
1047
+ e_simple_disp = self._calculate_simple_dispersion()
1048
+ e_srb = self._calculate_srb_energy()
1049
+
1050
+ # Calculate electric field interaction if field is provided
1051
+ e_field = torch.tensor(0.0, dtype=self.dtype, device=self.device)
1052
+ if external_electric_field is not None:
1053
+ # E_field = - sum_i (q_i * r_i) · F_ext
1054
+ # where q_i are the EEQ charges and r_i are positions
1055
+ dipole_component = torch.sum(self.eeq_charges.unsqueeze(1) * coords, dim=0)
1056
+ e_field = -torch.dot(dipole_component, external_electric_field)
1057
+
1058
+ # Restore original positions
1059
+ self.positions = original_positions
1060
+
1061
+ total_energy = e_eht + e_ies + e_rep + e_simple_disp + e_srb + e_field
1062
+
1063
+ return total_energy
1064
+
1065
+ def calculate_energies(self):
1066
+ """
1067
+ Calculate and return energy components as a dictionary (for backward compatibility).
1068
+
1069
+ Returns:
1070
+ Dictionary of energy components in Hartree (tensors)
1071
+ """
1072
+ e_eht = self._solve_eht()
1073
+ e_ies = self._solve_eeq()
1074
+ e_rep = self._calculate_repulsive_energy()
1075
+ e_simple_disp = self._calculate_simple_dispersion()
1076
+ e_srb = self._calculate_srb_energy()
1077
+
1078
+ # Calculate dipole moment using AD
1079
+ dipole = self._calculate_dipole_moment_ad()
1080
+
1081
+ self.energies = {
1082
+ 'EHT': e_eht,
1083
+ 'IES': e_ies,
1084
+ 'Repulsive': e_rep,
1085
+ 'SimpleDispersion': e_simple_disp,
1086
+ 'SRB': e_srb,
1087
+ 'Total': e_eht + e_ies + e_rep + e_simple_disp + e_srb,
1088
+ 'dipole_moment': dipole
1089
+ }
1090
+ return self.energies
1091
+
1092
+ def calculate_energy_and_gradient(self, coords=None, atomic_numbers=None, total_charge=None):
1093
+ """
1094
+ Calculate total energy and gradient using PyTorch autograd.
1095
+
1096
+ Args:
1097
+ coords: Coordinates tensor (n_atoms, 3) in Bohr with requires_grad=True.
1098
+ If None, uses self.positions (which must have requires_grad=True)
1099
+ atomic_numbers: Optional atomic numbers tensor
1100
+ total_charge: Optional total charge
1101
+
1102
+ Returns:
1103
+ Tuple of (total_energy, gradient):
1104
+ - total_energy: Total energy as a scalar torch.Tensor
1105
+ - gradient: Gradient of energy w.r.t. coords, shape (n_atoms, 3)
1106
+ """
1107
+ # Use instance positions if coords not provided
1108
+ if coords is None:
1109
+ coords = self.positions
1110
+ if not coords.requires_grad:
1111
+ coords.requires_grad_(True)
1112
+
1113
+ # Ensure coords requires gradients
1114
+ if not coords.requires_grad:
1115
+ coords = coords.clone().detach().requires_grad_(True)
1116
+
1117
+ # Calculate total energy
1118
+ total_energy = self.calculate_total_energy(coords, atomic_numbers, total_charge)
1119
+
1120
+ # Calculate gradient using autograd.grad
1121
+ gradient = torch.autograd.grad(
1122
+ outputs=total_energy,
1123
+ inputs=coords,
1124
+ create_graph=False
1125
+ )[0]
1126
+
1127
+ return total_energy, gradient
1128
+
1129
+ def _calculate_dipole_moment_ad(self):
1130
+ """
1131
+ Calculate dipole moment using automatic differentiation.
1132
+
1133
+ The dipole moment is defined as the negative gradient of the total energy
1134
+ with respect to an external electric field, evaluated at zero field:
1135
+ μ = -∂E_total/∂F_ext |_{F_ext=0}
1136
+
1137
+ Returns:
1138
+ Dipole moment vector (3,) in atomic units (e·Bohr)
1139
+ """
1140
+ # Create a zero electric field tensor with gradient tracking
1141
+ field_tensor = torch.zeros(3, dtype=self.dtype, device=self.device, requires_grad=True)
1142
+
1143
+ # Calculate energy with this field
1144
+ energy = self.calculate_total_energy(external_electric_field=field_tensor)
1145
+
1146
+ # Calculate dipole as negative gradient
1147
+ dipole = -torch.autograd.grad(
1148
+ outputs=energy,
1149
+ inputs=field_tensor,
1150
+ create_graph=False
1151
+ )[0]
1152
+
1153
+ return dipole
1154
+
1155
+ def calculate_hessian(self, coords=None, atomic_numbers=None, total_charge=None, method='analytical'):
1156
+ """
1157
+ Calculate the Hessian matrix of the total energy with respect to atomic coordinates.
1158
+
1159
+ Args:
1160
+ coords: Coordinates tensor (n_atoms, 3) in Bohr. If None, uses self.positions
1161
+ atomic_numbers: Optional atomic numbers tensor
1162
+ total_charge: Optional total charge
1163
+ method: 'analytical' uses torch.autograd.functional.hessian,
1164
+ 'numerical' uses finite differences
1165
+
1166
+ Returns:
1167
+ Hessian matrix of shape (3*n_atoms, 3*n_atoms) in Hartree/Bohr^2
1168
+ """
1169
+ if coords is None:
1170
+ coords = self.positions.detach().clone()
1171
+ else:
1172
+ coords = coords.detach().clone()
1173
+
1174
+ n_atoms = coords.shape[0]
1175
+
1176
+ if method == 'analytical':
1177
+ # Use PyTorch's native hessian function
1178
+ # Create a wrapper that computes energy for flat coordinates
1179
+ def energy_func(coords_flat):
1180
+ coords_reshaped = coords_flat.reshape(n_atoms, 3)
1181
+ return self.calculate_total_energy(coords_reshaped, atomic_numbers, total_charge)
1182
+
1183
+ # Flatten coordinates for hessian computation
1184
+ coords_flat = coords.flatten()
1185
+ coords_flat.requires_grad_(True)
1186
+
1187
+ # Calculate Hessian - output shape will be (3*n_atoms, 3*n_atoms)
1188
+ try:
1189
+ hessian = torch.autograd.functional.hessian(energy_func, coords_flat)
1190
+ except RuntimeError as e:
1191
+ print(f"Warning: Analytical Hessian calculation failed: {e}")
1192
+ print("Falling back to numerical method")
1193
+ return self.calculate_hessian(coords, atomic_numbers, total_charge, method='numerical')
1194
+
1195
+ return hessian
1196
+
1197
+ elif method == 'numerical':
1198
+ # Numerical finite difference method
1199
+ hessian = torch.zeros((n_atoms * 3, n_atoms * 3), dtype=self.dtype, device=self.device)
1200
+ h = 1e-5 # Finite difference step in Bohr
1201
+
1202
+ for i in range(n_atoms):
1203
+ for j in range(3):
1204
+ idx = i * 3 + j
1205
+ coords_copy = coords.clone()
1206
+
1207
+ # Forward difference
1208
+ coords_copy[i, j] = coords[i, j] + h
1209
+ _, grad_plus = self.calculate_energy_and_gradient(coords_copy, atomic_numbers, total_charge)
1210
+
1211
+ # Backward difference
1212
+ coords_copy[i, j] = coords[i, j] - h
1213
+ _, grad_minus = self.calculate_energy_and_gradient(coords_copy, atomic_numbers, total_charge)
1214
+
1215
+ # Central difference for Hessian row
1216
+ hessian[idx, :] = (grad_plus - grad_minus).flatten() / (2 * h)
1217
+
1218
+ return hessian
1219
+ else:
1220
+ raise ValueError(f"Unknown method: {method}. Use 'analytical' or 'numerical'.")
1221
+
1222
+ def get_forces(self, use_numerical=False):
1223
+ """
1224
+ Calculate forces via automatic differentiation or numerical differentiation.
1225
+
1226
+ Args:
1227
+ use_numerical: If True, use numerical differentiation as fallback
1228
+
1229
+ Returns:
1230
+ Forces in Hartree/Bohr (tensor)
1231
+ """
1232
+ if not use_numerical:
1233
+ # Use automatic differentiation via calculate_energy_and_gradient
1234
+ try:
1235
+ _, gradient = self.calculate_energy_and_gradient()
1236
+ # Forces are negative gradient
1237
+ forces = -gradient
1238
+ return forces
1239
+ except (RuntimeError, ValueError) as e:
1240
+ print(f"Warning: Autograd failed ({e}), falling back to numerical differentiation")
1241
+ use_numerical = True
1242
+
1243
+ if use_numerical:
1244
+ # Numerical differentiation fallback
1245
+ forces = torch.zeros_like(self.positions)
1246
+ h = 1e-5 # Finite difference step in Bohr
1247
+
1248
+ for i in range(self.n_atoms):
1249
+ for j in range(3):
1250
+ original_pos = self.positions[i, j].item()
1251
+
1252
+ self.positions.data[i, j] = original_pos + h
1253
+ e_plus = self.calculate_total_energy()
1254
+
1255
+ self.positions.data[i, j] = original_pos - h
1256
+ e_minus = self.calculate_total_energy()
1257
+
1258
+ self.positions.data[i, j] = original_pos # Restore
1259
+
1260
+ forces[i, j] = -(e_plus - e_minus) / (2 * h)
1261
+
1262
+ # Recalculate energy at original position
1263
+ self.calculate_total_energy()
1264
+ return forces
1265
+
1266
+ def optimize_geometry(self, method='Adam', lr=0.01, max_steps=1000, gtol=1e-3, max_distance_deviation=0.10, disp=False):
1267
+ """
1268
+ Optimize molecular geometry using PyTorch optimizer.
1269
+
1270
+ Args:
1271
+ method: Optimization method ('LBFGS' or 'Adam'). Default is 'Adam' for stability.
1272
+ lr: Learning rate for optimizer (default: 0.01 for Adam, will be scaled down for LBFGS)
1273
+ max_steps: Maximum optimization steps
1274
+ gtol: Gradient tolerance for convergence
1275
+ max_distance_deviation: Maximum allowed deviation of interatomic distances
1276
+ disp: If True, display verbose output during optimization
1277
+
1278
+ Returns:
1279
+ Optimized positions in Angstrom (tensor)
1280
+ """
1281
+ # Store initial positions and distances
1282
+ initial_positions = self.positions.detach().clone()
1283
+ initial_distances = {}
1284
+ bonded_pairs = set()
1285
+
1286
+ # Only store distances for covalently bonded pairs
1287
+ for i in range(self.n_atoms):
1288
+ for j in range(i+1, self.n_atoms):
1289
+ d = torch.linalg.norm(initial_positions[i] - initial_positions[j])
1290
+ d_angstrom = d * BOHR_TO_ANGSTROM
1291
+
1292
+ # Check if atoms are covalently bonded
1293
+ if is_covalently_bonded(self.atomic_numbers[i].item(), self.atomic_numbers[j].item(), d_angstrom.item()):
1294
+ initial_distances[(i, j)] = d
1295
+ bonded_pairs.add((i, j))
1296
+
1297
+ # Set up optimizer based on method with more conservative parameters
1298
+ if method.upper() == 'LBFGS':
1299
+ # Use very conservative LBFGS settings to prevent gradient explosion
1300
+ # Scale down learning rate for LBFGS
1301
+ lbfgs_lr = min(lr * 0.1, 0.001)
1302
+ optimizer = torch.optim.LBFGS(
1303
+ [self.positions],
1304
+ lr=lbfgs_lr,
1305
+ max_iter=5, # Very conservative max iterations per step
1306
+ line_search_fn='strong_wolfe',
1307
+ tolerance_grad=1e-9,
1308
+ tolerance_change=1e-12,
1309
+ history_size=10 # Limit memory
1310
+ )
1311
+ elif method.upper() == 'ADAM':
1312
+ # Adam is generally more stable for molecular optimization
1313
+ optimizer = torch.optim.Adam([self.positions], lr=lr)
1314
+ else:
1315
+ raise ValueError(f"Unknown optimization method: {method}. Use 'LBFGS' or 'Adam'")
1316
+
1317
+ # Track consecutive failed steps for early stopping
1318
+ consecutive_failures = 0
1319
+ max_consecutive_failures = 5
1320
+
1321
+ # Store best state for recovery
1322
+ best_loss = float('inf')
1323
+ best_positions = self.positions.detach().clone()
1324
+
1325
+ def closure():
1326
+ optimizer.zero_grad()
1327
+ try:
1328
+ E = self.calculate_total_energy()
1329
+
1330
+ # Check for NaN or inf in energy
1331
+ if not torch.isfinite(E):
1332
+ if disp:
1333
+ print("Warning: Energy is NaN or inf, returning large penalty")
1334
+ return torch.tensor(1e10, dtype=self.dtype, device=self.device)
1335
+
1336
+ # Sanity check: energy shouldn't be extremely negative (indication of numerical issues)
1337
+ if E < -1000.0: # -1000 Hartree is extremely negative for small molecules
1338
+ if disp:
1339
+ print(f"Warning: Energy is unrealistically negative ({E.item():.2f} Hartree)")
1340
+ # Restore best positions and return penalty
1341
+ self.positions.data.copy_(best_positions)
1342
+ return torch.tensor(1e10, dtype=self.dtype, device=self.device)
1343
+
1344
+ # Add penalty for distance constraint violations with adaptive scaling
1345
+ penalty = torch.tensor(0.0, dtype=self.dtype, device=self.device)
1346
+ max_deviation = 0.0
1347
+ for (i, j), d_init in initial_distances.items():
1348
+ d_current = torch.linalg.norm(self.positions[i] - self.positions[j])
1349
+ deviation = torch.abs(d_current - d_init) / d_init
1350
+ max_deviation = max(max_deviation, deviation.item())
1351
+ if deviation > max_distance_deviation:
1352
+ # Adaptive penalty: stronger penalty as deviation increases
1353
+ penalty += 100.0 * (deviation - max_distance_deviation)**2
1354
+
1355
+ total_loss = E + penalty
1356
+
1357
+ # Check for NaN or inf in total loss before backward
1358
+ if not torch.isfinite(total_loss):
1359
+ if disp:
1360
+ print("Warning: Total loss is NaN or inf, returning large penalty")
1361
+ self.positions.data.copy_(best_positions)
1362
+ return torch.tensor(1e10, dtype=self.dtype, device=self.device)
1363
+
1364
+ total_loss.backward()
1365
+
1366
+ # Gradient clipping to prevent explosion
1367
+ if self.positions.grad is not None:
1368
+ # Check for NaN or inf in gradients
1369
+ if not torch.all(torch.isfinite(self.positions.grad)):
1370
+ if disp:
1371
+ print("Warning: Gradient contains NaN or inf")
1372
+ self.positions.grad.zero_()
1373
+ self.positions.data.copy_(best_positions)
1374
+ return torch.tensor(1e10, dtype=self.dtype, device=self.device)
1375
+
1376
+ # Clip gradients to prevent explosion
1377
+ grad_norm = torch.linalg.norm(self.positions.grad)
1378
+ max_grad_norm = 1.0 # Maximum allowed gradient norm in Hartree/Bohr
1379
+ if grad_norm > max_grad_norm:
1380
+ clip_factor = max_grad_norm / grad_norm
1381
+ self.positions.grad *= clip_factor
1382
+ if disp:
1383
+ print(f" Gradient clipped: {grad_norm:.2e} -> {max_grad_norm:.2e}")
1384
+
1385
+ return total_loss
1386
+ except (RuntimeError, ValueError) as e:
1387
+ if disp:
1388
+ print(f"Error during energy calculation: {e}")
1389
+ print("Returning large penalty and restoring best positions")
1390
+ # Restore best positions on error
1391
+ self.positions.data.copy_(best_positions)
1392
+ return torch.tensor(1e10, dtype=self.dtype, device=self.device)
1393
+
1394
+ # Optimization loop
1395
+ prev_loss = None
1396
+ for step in range(max_steps):
1397
+ try:
1398
+ if method.upper() == 'LBFGS':
1399
+ loss = optimizer.step(closure)
1400
+ elif method.upper() == 'ADAM':
1401
+ # For Adam, we need to manually call closure and step
1402
+ loss = closure()
1403
+ if loss.item() < 1e9: # Only step if not a penalty value
1404
+ optimizer.step()
1405
+
1406
+ # Check for NaN or inf in loss
1407
+ if not torch.isfinite(loss):
1408
+ consecutive_failures += 1
1409
+ if disp:
1410
+ print(f"Step {step}: Loss is NaN or inf")
1411
+ if consecutive_failures >= max_consecutive_failures:
1412
+ print(f"Optimization stopped at step {step}: too many consecutive failures")
1413
+ # Restore best positions
1414
+ self.positions.data.copy_(best_positions)
1415
+ break
1416
+ continue
1417
+
1418
+ loss_val = loss.item()
1419
+
1420
+ # Update best state if this is better
1421
+ if loss_val < best_loss and loss_val < 1e9: # Not a penalty value
1422
+ best_loss = loss_val
1423
+ best_positions = self.positions.detach().clone()
1424
+ consecutive_failures = 0
1425
+ else:
1426
+ consecutive_failures += 1
1427
+
1428
+ # Check for loss explosion (sudden large increase)
1429
+ if prev_loss is not None and prev_loss < 1e9: # Previous wasn't a penalty
1430
+ loss_ratio = abs(loss_val / prev_loss) if prev_loss != 0 else 1.0
1431
+ if loss_ratio > 100: # Loss increased by more than 100x
1432
+ print(f"Optimization stopped at step {step}: loss explosion detected")
1433
+ print(f" Previous loss: {prev_loss:.8f}")
1434
+ print(f" Current loss: {loss_val:.8f}")
1435
+ # Restore best positions
1436
+ self.positions.data.copy_(best_positions)
1437
+ break
1438
+
1439
+ # Check for unrealistic energy
1440
+ if loss_val < -1000.0:
1441
+ print(f"Optimization stopped at step {step}: unrealistic energy detected")
1442
+ print(f" Current loss: {loss_val:.8f}")
1443
+ # Restore best positions
1444
+ self.positions.data.copy_(best_positions)
1445
+ break
1446
+
1447
+ prev_loss = loss_val
1448
+
1449
+ # Check convergence
1450
+ if self.positions.grad is not None:
1451
+ max_grad = torch.max(torch.abs(self.positions.grad))
1452
+ if disp and step % 10 == 0:
1453
+ print(f"Step {step}: Loss = {loss_val:.8f}, Max gradient = {max_grad.item():.6f}")
1454
+ if max_grad < gtol and loss_val < 1e9: # Converged and not a penalty
1455
+ if disp:
1456
+ print(f"Converged at step {step}")
1457
+ break
1458
+
1459
+ # Stop if too many consecutive non-improving steps
1460
+ if consecutive_failures >= max_consecutive_failures:
1461
+ if disp:
1462
+ print(f"Step {step}: No improvement for {max_consecutive_failures} steps")
1463
+ # Restore best positions
1464
+ self.positions.data.copy_(best_positions)
1465
+ break
1466
+
1467
+ except (RuntimeError, ValueError) as e:
1468
+ consecutive_failures += 1
1469
+ if disp:
1470
+ print(f"Optimization error at step {step}: {e}")
1471
+ # Restore best positions on error
1472
+ self.positions.data.copy_(best_positions)
1473
+ if consecutive_failures >= max_consecutive_failures:
1474
+ print(f"Optimization stopped at step {step} due to repeated errors")
1475
+ break
1476
+
1477
+ final_energy_dict = self.calculate_energies()
1478
+ print("\n--- Optimization Finished ---")
1479
+ print(f"Final Energy: {final_energy_dict['Total'].item():.8f} Hartree")
1480
+ print(f"Final Energy: {final_energy_dict['Total'].item() * HARTREE_TO_EV:.4f} eV")
1481
+
1482
+ print(f"\nCovalent bonds identified: {len(bonded_pairs)}")
1483
+ print(f"Total atom pairs: {self.n_atoms * (self.n_atoms - 1) // 2}")
1484
+
1485
+ # Report distance deviations for bonded pairs
1486
+ print("\nDistance Deviations (Bonded Pairs Only):")
1487
+ max_dev = 0.0
1488
+ for (i, j), d_init in initial_distances.items():
1489
+ d_final = torch.linalg.norm(self.positions[i] - self.positions[j])
1490
+ deviation = torch.abs(d_final - d_init) / d_init * 100.0
1491
+ if deviation > 1.0:
1492
+ print(f" Atoms {i}-{j}: {deviation.item():.2f}%")
1493
+ max_dev = max(max_dev, deviation.item())
1494
+ print(f"Maximum deviation: {max_dev:.2f}%")
1495
+
1496
+ return self.positions.detach() * BOHR_TO_ANGSTROM
1497
+
1498
+
1499
+
1500
+ def optimize_molecule(name, atoms, coords, params, device='cpu', dtype=torch.float64):
1501
+ """Helper function to optimize a molecule and print results."""
1502
+ print("\n" + "="*70)
1503
+ print(f"{name} (Geometry Optimization)")
1504
+ print("="*70)
1505
+
1506
+ print("Initial Geometry (Angstrom):")
1507
+ for i, coord in enumerate(coords):
1508
+ symbol = params.element_params[atoms[i]]['symbol']
1509
+ print(f" {symbol} {coord[0]:10.6f} {coord[1]:10.6f} {coord[2]:10.6f}")
1510
+
1511
+ calc = SQM1Calculator(atoms, coords, params=params, device=device, dtype=dtype)
1512
+
1513
+ # Test automatic differentiation before optimization
1514
+ print("\n--- Testing Automatic Differentiation ---")
1515
+ test_coords = torch.tensor(coords, dtype=dtype, device=device) * ANGSTROM_TO_BOHR
1516
+ test_coords.requires_grad_(True)
1517
+
1518
+ # Calculate energy and gradient using autograd
1519
+ energy, gradient = calc.calculate_energy_and_gradient(test_coords)
1520
+
1521
+ print(f"Energy (Hartree): {energy.item():.8f}")
1522
+ print(f"Gradient norm (Hartree/Bohr): {torch.linalg.norm(gradient).item():.8f}")
1523
+
1524
+ # Verify gradient using numerical differentiation
1525
+ numerical_grad = torch.zeros_like(test_coords)
1526
+ h = 1e-5 # Small step for numerical differentiation
1527
+ for i in range(test_coords.shape[0]):
1528
+ for j in range(3):
1529
+ # Create new tensor for positive step
1530
+ test_coords_plus = test_coords.detach().clone()
1531
+ test_coords_plus[i, j] = test_coords_plus[i, j] + h
1532
+ test_coords_plus.requires_grad_(False)
1533
+ e_plus = calc.calculate_total_energy(test_coords_plus)
1534
+
1535
+ # Create new tensor for negative step
1536
+ test_coords_minus = test_coords.detach().clone()
1537
+ test_coords_minus[i, j] = test_coords_minus[i, j] - h
1538
+ test_coords_minus.requires_grad_(False)
1539
+ e_minus = calc.calculate_total_energy(test_coords_minus)
1540
+
1541
+ numerical_grad[i, j] = (e_plus - e_minus) / (2 * h)
1542
+
1543
+ grad_diff = torch.linalg.norm(gradient - numerical_grad)
1544
+ print(f"Gradient difference (AD vs Numerical): {grad_diff.item():.2e}")
1545
+
1546
+ if grad_diff < 1e-6:
1547
+ print("✓ Automatic differentiation validated successfully!")
1548
+ else:
1549
+ print("⚠ Warning: Gradient difference is larger than expected")
1550
+
1551
+ try:
1552
+ optimized_coords = calc.optimize_geometry()
1553
+
1554
+ print("\nOptimized Geometry (Angstrom):")
1555
+ for i, coord in enumerate(optimized_coords):
1556
+ symbol = params.element_params[atoms[i]]['symbol']
1557
+ print(f" {symbol} {coord[0].item():10.6f} {coord[1].item():10.6f} {coord[2].item():10.6f}")
1558
+ except Exception as e:
1559
+ print(f"\nOptimization failed: {e}")
1560
+ print("Calculating single-point energy instead...")
1561
+ energies = calc.calculate_energies()
1562
+ print(f"Total Energy: {energies['Total'].item():.8f} Hartree ({energies['Total'].item() * HARTREE_TO_EV:.4f} eV)")
1563
+
1564
+
1565
+ def main():
1566
+ """Main function to run test cases."""
1567
+ # Parameters are now embedded in SQM1Parameters class
1568
+ params = SQM1Parameters()
1569
+
1570
+ print("\n" + "="*70)
1571
+ print("SQM1 Implementation - Comprehensive Test Suite")
1572
+ print("Testing geometry optimization for various molecules")
1573
+ print("="*70)
1574
+
1575
+ # --- Test Case 1: Water (H2O) ---
1576
+ water_atoms = [8, 1, 1]
1577
+ water_coords = [
1578
+ [0.000000, 0.000000, 0.117300],
1579
+ [0.000000, 0.757200, -0.469200],
1580
+ [0.000000, -0.757200, -0.469200]
1581
+ ]
1582
+ optimize_molecule("Water (H2O)", water_atoms, water_coords, params)
1583
+
1584
+ # --- Test Case 2: Ammonia (NH3) ---
1585
+ ammonia_atoms = [7, 1, 1, 1]
1586
+ ammonia_coords = [
1587
+ [ 0.0000, 0.0000, 0.1 ],
1588
+ [ 0.9400, 0.0000, -0.3 ],
1589
+ [-0.4700, 0.8141, -0.3 ],
1590
+ [-0.4700, -0.8141, -0.3 ]
1591
+ ]
1592
+ optimize_molecule("Ammonia (NH3)", ammonia_atoms, ammonia_coords, params)
1593
+
1594
+ # --- Test Case 3: Methane (CH4) ---
1595
+ methane_atoms = [6, 1, 1, 1, 1]
1596
+ methane_coords = [
1597
+ [0.000, 0.000, 0.000],
1598
+ [0.629, 0.629, 0.629],
1599
+ [-0.629, -0.629, 0.629],
1600
+ [-0.629, 0.629, -0.629],
1601
+ [0.629, -0.629, -0.629]
1602
+ ]
1603
+ optimize_molecule("Methane (CH4)", methane_atoms, methane_coords, params)
1604
+
1605
+ # --- Test Case 4: Ethanol (C2H5OH) ---
1606
+ ethanol_atoms = [6, 6, 8, 1, 1, 1, 1, 1, 1]
1607
+ ethanol_coords = [
1608
+ [ 1.185, -0.184, 0.000], # C
1609
+ [-0.274, 0.095, 0.000], # C
1610
+ [-1.048, -1.089, 0.000], # O
1611
+ [ 1.554, -0.719, 0.886], # H
1612
+ [ 1.554, -0.719, -0.886], # H
1613
+ [ 1.621, 0.796, 0.000], # H
1614
+ [-0.630, 0.646, -0.884], # H
1615
+ [-0.630, 0.646, 0.884], # H
1616
+ [-1.982, -0.861, 0.000], # H
1617
+ ]
1618
+ optimize_molecule("Ethanol (C2H5OH)", ethanol_atoms, ethanol_coords, params)
1619
+
1620
+ # --- Test Case 5: Glycine (C2H5NO2) ---
1621
+ glycine_atoms = [6, 6, 7, 8, 8, 1, 1, 1, 1, 1]
1622
+ glycine_coords = [
1623
+ [-0.714, 1.280, 0.000], # C
1624
+ [ 0.000, 0.000, 0.000], # C
1625
+ [ 1.420, 0.000, 0.000], # N
1626
+ [-0.593, -1.116, -0.203], # O
1627
+ [-0.276, 2.346, 0.173], # O
1628
+ [-1.773, 1.244, 0.000], # H
1629
+ [ 1.819, -0.003, 0.928], # H
1630
+ [ 1.792, 0.790, -0.501], # H
1631
+ [-1.573, -1.040, -0.203], # H
1632
+ [-0.062, 2.356, -0.663], # H (OH hydrogen)
1633
+ ]
1634
+ optimize_molecule("Glycine (C2H5NO2)", glycine_atoms, glycine_coords, params)
1635
+
1636
+ # --- Test Case 6: Benzene (C6H6) ---
1637
+ benzene_atoms = [6, 6, 6, 6, 6, 6, 1, 1, 1, 1, 1, 1]
1638
+ r = 1.39 # C-C bond length in benzene
1639
+ a = 1.08 # C-H bond length
1640
+ benzene_coords = [
1641
+ [ r, 0.0, 0.0], # C
1642
+ [ r/2, r*0.866, 0.0], # C
1643
+ [-r/2, r*0.866, 0.0], # C
1644
+ [-r, 0.0, 0.0], # C
1645
+ [-r/2, -r*0.866, 0.0], # C
1646
+ [ r/2, -r*0.866, 0.0], # C
1647
+ [ (r+a), 0.0, 0.0], # H
1648
+ [ (r+a)/2, (r+a)*0.866, 0.0], # H
1649
+ [-(r+a)/2, (r+a)*0.866, 0.0], # H
1650
+ [-(r+a), 0.0, 0.0], # H
1651
+ [-(r+a)/2, -(r+a)*0.866, 0.0], # H
1652
+ [ (r+a)/2, -(r+a)*0.866, 0.0], # H
1653
+ ]
1654
+ optimize_molecule("Benzene (C6H6)", benzene_atoms, benzene_coords, params)
1655
+
1656
+ # --- Test Case 7: Acetylene (C2H2) ---
1657
+ acetylene_atoms = [6, 6, 1, 1]
1658
+ acetylene_coords = [
1659
+ [0.000, 0.000, 0.600], # C
1660
+ [0.000, 0.000, -0.600], # C
1661
+ [0.000, 0.000, 1.665], # H
1662
+ [0.000, 0.000, -1.665], # H
1663
+ ]
1664
+ optimize_molecule("Acetylene (C2H2)", acetylene_atoms, acetylene_coords, params)
1665
+
1666
+ # --- Test Case 8: Dichloromethane (CH2Cl2) ---
1667
+ dichloromethane_atoms = [6, 17, 17, 1, 1]
1668
+ dichloromethane_coords = [
1669
+ [0.000, 0.000, 0.000], # C
1670
+ [0.000, 1.772, 0.000], # Cl
1671
+ [0.000, -1.772, 0.000], # Cl
1672
+ [1.030, 0.000, 0.000], # H
1673
+ [-1.030, 0.000, 0.000], # H
1674
+ ]
1675
+ optimize_molecule("Dichloromethane (CH2Cl2)", dichloromethane_atoms, dichloromethane_coords, params)
1676
+
1677
+ # --- Test Case 9: Bromomethane (CH3Br) ---
1678
+ bromomethane_atoms = [6, 35, 1, 1, 1]
1679
+ bromomethane_coords = [
1680
+ [0.000, 0.000, 0.000], # C
1681
+ [0.000, 0.000, 1.939], # Br
1682
+ [1.025, 0.000, -0.377], # H
1683
+ [-0.512, 0.887, -0.377], # H
1684
+ [-0.512, -0.887, -0.377], # H
1685
+ ]
1686
+ optimize_molecule("Bromomethane (CH3Br)", bromomethane_atoms, bromomethane_coords, params)
1687
+
1688
+ # --- Test Case 10: Cyclohexane (C6H12) ---
1689
+ # Chair conformation
1690
+ cyclohexane_atoms = [6, 6, 6, 6, 6, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
1691
+ cyclohexane_coords = [
1692
+ # C atoms in chair conformation
1693
+ [ 1.261, 0.728, 0.000], # C1
1694
+ [ 1.261, -0.728, 0.000], # C2
1695
+ [ 0.000, -1.457, 0.000], # C3
1696
+ [-1.261, -0.728, 0.000], # C4
1697
+ [-1.261, 0.728, 0.000], # C5
1698
+ [ 0.000, 1.457, 0.000], # C6
1699
+ # H atoms (axial and equatorial)
1700
+ [ 2.180, 1.259, 0.000], # H (axial on C1)
1701
+ [ 1.261, 0.728, 0.990], # H (equatorial on C1)
1702
+ [ 2.180, -1.259, 0.000], # H (axial on C2)
1703
+ [ 1.261, -0.728, 0.990], # H (equatorial on C2)
1704
+ [ 0.000, -2.518, 0.000], # H (axial on C3)
1705
+ [ 0.000, -1.457, 0.990], # H (equatorial on C3)
1706
+ [-2.180, -1.259, 0.000], # H (axial on C4)
1707
+ [-1.261, -0.728, 0.990], # H (equatorial on C4)
1708
+ [-2.180, 1.259, 0.000], # H (axial on C5)
1709
+ [-1.261, 0.728, 0.990], # H (equatorial on C5)
1710
+ [ 0.000, 2.518, 0.000], # H (axial on C6)
1711
+ [ 0.000, 1.457, 0.990], # H (equatorial on C6)
1712
+ ]
1713
+ optimize_molecule("Cyclohexane (C6H12)", cyclohexane_atoms, cyclohexane_coords, params)
1714
+
1715
+ # --- Test Case 11: Bromobenzene (C6H5Br) ---
1716
+ r = 1.39 # C-C bond length
1717
+ a = 1.08 # C-H bond length
1718
+ bromobenzene_atoms = [6, 6, 6, 6, 6, 6, 35, 1, 1, 1, 1, 1]
1719
+ bromobenzene_coords = [
1720
+ [ r, 0.0, 0.0], # C1
1721
+ [ r/2, r*0.866, 0.0], # C2
1722
+ [-r/2, r*0.866, 0.0], # C3
1723
+ [-r, 0.0, 0.0], # C4
1724
+ [-r/2, -r*0.866, 0.0], # C5
1725
+ [ r/2, -r*0.866, 0.0], # C6
1726
+ [ (r+1.9), 0.0, 0.0], # Br on C1
1727
+ [ (r+a)/2, (r+a)*0.866, 0.0], # H on C2
1728
+ [-(r+a)/2, (r+a)*0.866, 0.0], # H on C3
1729
+ [-(r+a), 0.0, 0.0], # H on C4
1730
+ [-(r+a)/2, -(r+a)*0.866, 0.0], # H on C5
1731
+ [ (r+a)/2, -(r+a)*0.866, 0.0], # H on C6
1732
+ ]
1733
+ optimize_molecule("Bromobenzene (C6H5Br)", bromobenzene_atoms, bromobenzene_coords, params)
1734
+
1735
+ # --- Test Case 12: 1,2-Dichloroethane (C2H4Cl2) ---
1736
+ dichloroethane_atoms = [6, 6, 17, 17, 1, 1, 1, 1]
1737
+ dichloroethane_coords = [
1738
+ [ 0.765, 0.000, 0.000], # C1
1739
+ [-0.765, 0.000, 0.000], # C2
1740
+ [ 1.265, 1.772, 0.000], # Cl on C1
1741
+ [-1.265, -1.772, 0.000], # Cl on C2
1742
+ [ 1.134, 0.000, 1.027], # H on C1
1743
+ [ 1.134, 0.000, -1.027], # H on C1
1744
+ [-1.134, 0.000, 1.027], # H on C2
1745
+ [-1.134, 0.000, -1.027], # H on C2
1746
+ ]
1747
+ optimize_molecule("1,2-Dichloroethane (C2H4Cl2)", dichloroethane_atoms, dichloroethane_coords, params)
1748
+
1749
+ # --- Test Case 13: Acetone (C3H6O) ---
1750
+ acetone_atoms = [6, 6, 6, 8, 1, 1, 1, 1, 1, 1]
1751
+ acetone_coords = [
1752
+ [ 0.000, 0.000, 0.000], # C (carbonyl)
1753
+ [ 1.520, 0.000, 0.000], # C (methyl)
1754
+ [-1.520, 0.000, 0.000], # C (methyl)
1755
+ [ 0.000, 0.000, 1.220], # O (carbonyl)
1756
+ [ 1.900, 0.000, 1.027], # H on C2
1757
+ [ 1.900, 0.887, -0.513], # H on C2
1758
+ [ 1.900, -0.887, -0.513], # H on C2
1759
+ [-1.900, 0.000, 1.027], # H on C3
1760
+ [-1.900, 0.887, -0.513], # H on C3
1761
+ [-1.900, -0.887, -0.513], # H on C3
1762
+ ]
1763
+ optimize_molecule("Acetone (C3H6O)", acetone_atoms, acetone_coords, params)
1764
+
1765
+ # --- Test Case 14: n-Butane (C4H10) ---
1766
+ nbutane_atoms = [6, 6, 6, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
1767
+ nbutane_coords = [
1768
+ [ 1.950, 0.000, 0.000], # C1
1769
+ [ 0.650, 0.000, 0.000], # C2
1770
+ [-0.650, 0.000, 0.000], # C3
1771
+ [-1.950, 0.000, 0.000], # C4
1772
+ [ 2.333, 0.000, 1.027], # H on C1
1773
+ [ 2.333, 0.887, -0.513], # H on C1
1774
+ [ 2.333, -0.887, -0.513], # H on C1
1775
+ [ 0.650, 0.000, 1.090], # H on C2
1776
+ [ 0.650, 0.890, -0.545], # H on C2
1777
+ [-0.650, 0.000, 1.090], # H on C3
1778
+ [-0.650, -0.890, -0.545], # H on C3
1779
+ [-2.333, 0.000, 1.027], # H on C4
1780
+ [-2.333, 0.887, -0.513], # H on C4
1781
+ [-2.333, -0.887, -0.513], # H on C4
1782
+ ]
1783
+ optimize_molecule("n-Butane (C4H10)", nbutane_atoms, nbutane_coords, params)
1784
+
1785
+ print("\n" + "="*70)
1786
+ print("SQM1 Implementation Test Complete!")
1787
+ print("All test cases processed.")
1788
+ print("="*70)
1789
+
1790
+
1791
+ if __name__ == "__main__":
1792
+ main()