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.
- multioptpy/Calculator/__init__.py +0 -0
- multioptpy/Calculator/ase_calculation_tools.py +424 -0
- multioptpy/Calculator/ase_tools/__init__.py +0 -0
- multioptpy/Calculator/ase_tools/fairchem.py +28 -0
- multioptpy/Calculator/ase_tools/gamess.py +19 -0
- multioptpy/Calculator/ase_tools/gaussian.py +165 -0
- multioptpy/Calculator/ase_tools/mace.py +28 -0
- multioptpy/Calculator/ase_tools/mopac.py +19 -0
- multioptpy/Calculator/ase_tools/nwchem.py +31 -0
- multioptpy/Calculator/ase_tools/orca.py +22 -0
- multioptpy/Calculator/ase_tools/pygfn0.py +37 -0
- multioptpy/Calculator/dxtb_calculation_tools.py +344 -0
- multioptpy/Calculator/emt_calculation_tools.py +458 -0
- multioptpy/Calculator/gpaw_calculation_tools.py +183 -0
- multioptpy/Calculator/lj_calculation_tools.py +314 -0
- multioptpy/Calculator/psi4_calculation_tools.py +334 -0
- multioptpy/Calculator/pwscf_calculation_tools.py +189 -0
- multioptpy/Calculator/pyscf_calculation_tools.py +327 -0
- multioptpy/Calculator/sqm1_calculation_tools.py +611 -0
- multioptpy/Calculator/sqm2_calculation_tools.py +376 -0
- multioptpy/Calculator/tblite_calculation_tools.py +352 -0
- multioptpy/Calculator/tersoff_calculation_tools.py +818 -0
- multioptpy/Constraint/__init__.py +0 -0
- multioptpy/Constraint/constraint_condition.py +834 -0
- multioptpy/Coordinate/__init__.py +0 -0
- multioptpy/Coordinate/polar_coordinate.py +199 -0
- multioptpy/Coordinate/redundant_coordinate.py +638 -0
- multioptpy/IRC/__init__.py +0 -0
- multioptpy/IRC/converge_criteria.py +28 -0
- multioptpy/IRC/dvv.py +544 -0
- multioptpy/IRC/euler.py +439 -0
- multioptpy/IRC/hpc.py +564 -0
- multioptpy/IRC/lqa.py +540 -0
- multioptpy/IRC/modekill.py +662 -0
- multioptpy/IRC/rk4.py +579 -0
- multioptpy/Interpolation/__init__.py +0 -0
- multioptpy/Interpolation/adaptive_interpolation.py +283 -0
- multioptpy/Interpolation/binomial_interpolation.py +179 -0
- multioptpy/Interpolation/geodesic_interpolation.py +785 -0
- multioptpy/Interpolation/interpolation.py +156 -0
- multioptpy/Interpolation/linear_interpolation.py +473 -0
- multioptpy/Interpolation/savitzky_golay_interpolation.py +252 -0
- multioptpy/Interpolation/spline_interpolation.py +353 -0
- multioptpy/MD/__init__.py +0 -0
- multioptpy/MD/thermostat.py +185 -0
- multioptpy/MEP/__init__.py +0 -0
- multioptpy/MEP/pathopt_bneb_force.py +443 -0
- multioptpy/MEP/pathopt_dmf_force.py +448 -0
- multioptpy/MEP/pathopt_dneb_force.py +130 -0
- multioptpy/MEP/pathopt_ewbneb_force.py +207 -0
- multioptpy/MEP/pathopt_gpneb_force.py +512 -0
- multioptpy/MEP/pathopt_lup_force.py +113 -0
- multioptpy/MEP/pathopt_neb_force.py +225 -0
- multioptpy/MEP/pathopt_nesb_force.py +205 -0
- multioptpy/MEP/pathopt_om_force.py +153 -0
- multioptpy/MEP/pathopt_qsm_force.py +174 -0
- multioptpy/MEP/pathopt_qsmv2_force.py +304 -0
- multioptpy/ModelFunction/__init__.py +7 -0
- multioptpy/ModelFunction/avoiding_model_function.py +29 -0
- multioptpy/ModelFunction/binary_image_ts_search_model_function.py +47 -0
- multioptpy/ModelFunction/conical_model_function.py +26 -0
- multioptpy/ModelFunction/opt_meci.py +50 -0
- multioptpy/ModelFunction/opt_mesx.py +47 -0
- multioptpy/ModelFunction/opt_mesx_2.py +49 -0
- multioptpy/ModelFunction/seam_model_function.py +27 -0
- multioptpy/ModelHessian/__init__.py +0 -0
- multioptpy/ModelHessian/approx_hessian.py +147 -0
- multioptpy/ModelHessian/calc_params.py +227 -0
- multioptpy/ModelHessian/fischer.py +236 -0
- multioptpy/ModelHessian/fischerd3.py +360 -0
- multioptpy/ModelHessian/fischerd4.py +398 -0
- multioptpy/ModelHessian/gfn0xtb.py +633 -0
- multioptpy/ModelHessian/gfnff.py +709 -0
- multioptpy/ModelHessian/lindh.py +165 -0
- multioptpy/ModelHessian/lindh2007d2.py +707 -0
- multioptpy/ModelHessian/lindh2007d3.py +822 -0
- multioptpy/ModelHessian/lindh2007d4.py +1030 -0
- multioptpy/ModelHessian/morse.py +106 -0
- multioptpy/ModelHessian/schlegel.py +144 -0
- multioptpy/ModelHessian/schlegeld3.py +322 -0
- multioptpy/ModelHessian/schlegeld4.py +559 -0
- multioptpy/ModelHessian/shortrange.py +346 -0
- multioptpy/ModelHessian/swartd2.py +496 -0
- multioptpy/ModelHessian/swartd3.py +706 -0
- multioptpy/ModelHessian/swartd4.py +918 -0
- multioptpy/ModelHessian/tshess.py +40 -0
- multioptpy/Optimizer/QHAdam.py +61 -0
- multioptpy/Optimizer/__init__.py +0 -0
- multioptpy/Optimizer/abc_fire.py +83 -0
- multioptpy/Optimizer/adabelief.py +58 -0
- multioptpy/Optimizer/adabound.py +68 -0
- multioptpy/Optimizer/adadelta.py +65 -0
- multioptpy/Optimizer/adaderivative.py +56 -0
- multioptpy/Optimizer/adadiff.py +68 -0
- multioptpy/Optimizer/adafactor.py +70 -0
- multioptpy/Optimizer/adam.py +65 -0
- multioptpy/Optimizer/adamax.py +62 -0
- multioptpy/Optimizer/adamod.py +83 -0
- multioptpy/Optimizer/adamw.py +65 -0
- multioptpy/Optimizer/adiis.py +523 -0
- multioptpy/Optimizer/afire_neb.py +282 -0
- multioptpy/Optimizer/block_hessian_update.py +709 -0
- multioptpy/Optimizer/c2diis.py +491 -0
- multioptpy/Optimizer/component_wise_scaling.py +405 -0
- multioptpy/Optimizer/conjugate_gradient.py +82 -0
- multioptpy/Optimizer/conjugate_gradient_neb.py +345 -0
- multioptpy/Optimizer/coordinate_locking.py +405 -0
- multioptpy/Optimizer/dic_rsirfo.py +1015 -0
- multioptpy/Optimizer/ediis.py +417 -0
- multioptpy/Optimizer/eve.py +76 -0
- multioptpy/Optimizer/fastadabelief.py +61 -0
- multioptpy/Optimizer/fire.py +77 -0
- multioptpy/Optimizer/fire2.py +249 -0
- multioptpy/Optimizer/fire_neb.py +92 -0
- multioptpy/Optimizer/gan_step.py +486 -0
- multioptpy/Optimizer/gdiis.py +609 -0
- multioptpy/Optimizer/gediis.py +203 -0
- multioptpy/Optimizer/geodesic_step.py +433 -0
- multioptpy/Optimizer/gpmin.py +633 -0
- multioptpy/Optimizer/gpr_step.py +364 -0
- multioptpy/Optimizer/gradientdescent.py +78 -0
- multioptpy/Optimizer/gradientdescent_neb.py +52 -0
- multioptpy/Optimizer/hessian_update.py +433 -0
- multioptpy/Optimizer/hybrid_rfo.py +998 -0
- multioptpy/Optimizer/kdiis.py +625 -0
- multioptpy/Optimizer/lars.py +21 -0
- multioptpy/Optimizer/lbfgs.py +253 -0
- multioptpy/Optimizer/lbfgs_neb.py +355 -0
- multioptpy/Optimizer/linesearch.py +236 -0
- multioptpy/Optimizer/lookahead.py +40 -0
- multioptpy/Optimizer/nadam.py +64 -0
- multioptpy/Optimizer/newton.py +200 -0
- multioptpy/Optimizer/prodigy.py +70 -0
- multioptpy/Optimizer/purtubation.py +16 -0
- multioptpy/Optimizer/quickmin_neb.py +245 -0
- multioptpy/Optimizer/radam.py +75 -0
- multioptpy/Optimizer/rfo_neb.py +302 -0
- multioptpy/Optimizer/ric_rfo.py +842 -0
- multioptpy/Optimizer/rl_step.py +627 -0
- multioptpy/Optimizer/rmspropgrave.py +65 -0
- multioptpy/Optimizer/rsirfo.py +1647 -0
- multioptpy/Optimizer/rsprfo.py +1056 -0
- multioptpy/Optimizer/sadam.py +60 -0
- multioptpy/Optimizer/samsgrad.py +63 -0
- multioptpy/Optimizer/tr_lbfgs.py +678 -0
- multioptpy/Optimizer/trim.py +273 -0
- multioptpy/Optimizer/trust_radius.py +207 -0
- multioptpy/Optimizer/trust_radius_neb.py +121 -0
- multioptpy/Optimizer/yogi.py +60 -0
- multioptpy/OtherMethod/__init__.py +0 -0
- multioptpy/OtherMethod/addf.py +1150 -0
- multioptpy/OtherMethod/dimer.py +895 -0
- multioptpy/OtherMethod/elastic_image_pair.py +629 -0
- multioptpy/OtherMethod/modelfunction.py +456 -0
- multioptpy/OtherMethod/newton_traj.py +454 -0
- multioptpy/OtherMethod/twopshs.py +1095 -0
- multioptpy/PESAnalyzer/__init__.py +0 -0
- multioptpy/PESAnalyzer/calc_irc_curvature.py +125 -0
- multioptpy/PESAnalyzer/cmds_analysis.py +152 -0
- multioptpy/PESAnalyzer/koopman_analysis.py +268 -0
- multioptpy/PESAnalyzer/pca_analysis.py +314 -0
- multioptpy/Parameters/__init__.py +0 -0
- multioptpy/Parameters/atomic_mass.py +20 -0
- multioptpy/Parameters/atomic_number.py +22 -0
- multioptpy/Parameters/covalent_radii.py +44 -0
- multioptpy/Parameters/d2.py +61 -0
- multioptpy/Parameters/d3.py +63 -0
- multioptpy/Parameters/d4.py +103 -0
- multioptpy/Parameters/dreiding.py +34 -0
- multioptpy/Parameters/gfn0xtb_param.py +137 -0
- multioptpy/Parameters/gfnff_param.py +315 -0
- multioptpy/Parameters/gnb.py +104 -0
- multioptpy/Parameters/parameter.py +22 -0
- multioptpy/Parameters/uff.py +72 -0
- multioptpy/Parameters/unit_values.py +20 -0
- multioptpy/Potential/AFIR_potential.py +55 -0
- multioptpy/Potential/LJ_repulsive_potential.py +345 -0
- multioptpy/Potential/__init__.py +0 -0
- multioptpy/Potential/anharmonic_keep_potential.py +28 -0
- multioptpy/Potential/asym_elllipsoidal_potential.py +718 -0
- multioptpy/Potential/electrostatic_potential.py +69 -0
- multioptpy/Potential/flux_potential.py +30 -0
- multioptpy/Potential/gaussian_potential.py +101 -0
- multioptpy/Potential/idpp.py +516 -0
- multioptpy/Potential/keep_angle_potential.py +146 -0
- multioptpy/Potential/keep_dihedral_angle_potential.py +105 -0
- multioptpy/Potential/keep_outofplain_angle_potential.py +70 -0
- multioptpy/Potential/keep_potential.py +99 -0
- multioptpy/Potential/mechano_force_potential.py +74 -0
- multioptpy/Potential/nanoreactor_potential.py +52 -0
- multioptpy/Potential/potential.py +896 -0
- multioptpy/Potential/spacer_model_potential.py +221 -0
- multioptpy/Potential/switching_potential.py +258 -0
- multioptpy/Potential/universal_potential.py +34 -0
- multioptpy/Potential/value_range_potential.py +36 -0
- multioptpy/Potential/void_point_potential.py +25 -0
- multioptpy/SQM/__init__.py +0 -0
- multioptpy/SQM/sqm1/__init__.py +0 -0
- multioptpy/SQM/sqm1/sqm1_core.py +1792 -0
- multioptpy/SQM/sqm2/__init__.py +0 -0
- multioptpy/SQM/sqm2/calc_tools.py +95 -0
- multioptpy/SQM/sqm2/sqm2_basis.py +850 -0
- multioptpy/SQM/sqm2/sqm2_bond.py +119 -0
- multioptpy/SQM/sqm2/sqm2_core.py +303 -0
- multioptpy/SQM/sqm2/sqm2_data.py +1229 -0
- multioptpy/SQM/sqm2/sqm2_disp.py +65 -0
- multioptpy/SQM/sqm2/sqm2_eeq.py +243 -0
- multioptpy/SQM/sqm2/sqm2_overlapint.py +704 -0
- multioptpy/SQM/sqm2/sqm2_qm.py +578 -0
- multioptpy/SQM/sqm2/sqm2_rep.py +66 -0
- multioptpy/SQM/sqm2/sqm2_srb.py +70 -0
- multioptpy/Thermo/__init__.py +0 -0
- multioptpy/Thermo/normal_mode_analyzer.py +865 -0
- multioptpy/Utils/__init__.py +0 -0
- multioptpy/Utils/bond_connectivity.py +264 -0
- multioptpy/Utils/calc_tools.py +884 -0
- multioptpy/Utils/oniom.py +96 -0
- multioptpy/Utils/pbc.py +48 -0
- multioptpy/Utils/riemann_curvature.py +208 -0
- multioptpy/Utils/symmetry_analyzer.py +482 -0
- multioptpy/Visualization/__init__.py +0 -0
- multioptpy/Visualization/visualization.py +156 -0
- multioptpy/WFAnalyzer/MO_analysis.py +104 -0
- multioptpy/WFAnalyzer/__init__.py +0 -0
- multioptpy/Wrapper/__init__.py +0 -0
- multioptpy/Wrapper/autots.py +1239 -0
- multioptpy/Wrapper/ieip_wrapper.py +93 -0
- multioptpy/Wrapper/md_wrapper.py +92 -0
- multioptpy/Wrapper/neb_wrapper.py +94 -0
- multioptpy/Wrapper/optimize_wrapper.py +76 -0
- multioptpy/__init__.py +5 -0
- multioptpy/entrypoints.py +916 -0
- multioptpy/fileio.py +660 -0
- multioptpy/ieip.py +340 -0
- multioptpy/interface.py +1086 -0
- multioptpy/irc.py +529 -0
- multioptpy/moleculardynamics.py +432 -0
- multioptpy/neb.py +1267 -0
- multioptpy/optimization.py +1553 -0
- multioptpy/optimizer.py +709 -0
- multioptpy-1.20.2.dist-info/METADATA +438 -0
- multioptpy-1.20.2.dist-info/RECORD +246 -0
- multioptpy-1.20.2.dist-info/WHEEL +5 -0
- multioptpy-1.20.2.dist-info/entry_points.txt +9 -0
- multioptpy-1.20.2.dist-info/licenses/LICENSE +674 -0
- 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()
|