MultiOptPy 1.20.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. multioptpy/Calculator/__init__.py +0 -0
  2. multioptpy/Calculator/ase_calculation_tools.py +424 -0
  3. multioptpy/Calculator/ase_tools/__init__.py +0 -0
  4. multioptpy/Calculator/ase_tools/fairchem.py +28 -0
  5. multioptpy/Calculator/ase_tools/gamess.py +19 -0
  6. multioptpy/Calculator/ase_tools/gaussian.py +165 -0
  7. multioptpy/Calculator/ase_tools/mace.py +28 -0
  8. multioptpy/Calculator/ase_tools/mopac.py +19 -0
  9. multioptpy/Calculator/ase_tools/nwchem.py +31 -0
  10. multioptpy/Calculator/ase_tools/orca.py +22 -0
  11. multioptpy/Calculator/ase_tools/pygfn0.py +37 -0
  12. multioptpy/Calculator/dxtb_calculation_tools.py +344 -0
  13. multioptpy/Calculator/emt_calculation_tools.py +458 -0
  14. multioptpy/Calculator/gpaw_calculation_tools.py +183 -0
  15. multioptpy/Calculator/lj_calculation_tools.py +314 -0
  16. multioptpy/Calculator/psi4_calculation_tools.py +334 -0
  17. multioptpy/Calculator/pwscf_calculation_tools.py +189 -0
  18. multioptpy/Calculator/pyscf_calculation_tools.py +327 -0
  19. multioptpy/Calculator/sqm1_calculation_tools.py +611 -0
  20. multioptpy/Calculator/sqm2_calculation_tools.py +376 -0
  21. multioptpy/Calculator/tblite_calculation_tools.py +352 -0
  22. multioptpy/Calculator/tersoff_calculation_tools.py +818 -0
  23. multioptpy/Constraint/__init__.py +0 -0
  24. multioptpy/Constraint/constraint_condition.py +834 -0
  25. multioptpy/Coordinate/__init__.py +0 -0
  26. multioptpy/Coordinate/polar_coordinate.py +199 -0
  27. multioptpy/Coordinate/redundant_coordinate.py +638 -0
  28. multioptpy/IRC/__init__.py +0 -0
  29. multioptpy/IRC/converge_criteria.py +28 -0
  30. multioptpy/IRC/dvv.py +544 -0
  31. multioptpy/IRC/euler.py +439 -0
  32. multioptpy/IRC/hpc.py +564 -0
  33. multioptpy/IRC/lqa.py +540 -0
  34. multioptpy/IRC/modekill.py +662 -0
  35. multioptpy/IRC/rk4.py +579 -0
  36. multioptpy/Interpolation/__init__.py +0 -0
  37. multioptpy/Interpolation/adaptive_interpolation.py +283 -0
  38. multioptpy/Interpolation/binomial_interpolation.py +179 -0
  39. multioptpy/Interpolation/geodesic_interpolation.py +785 -0
  40. multioptpy/Interpolation/interpolation.py +156 -0
  41. multioptpy/Interpolation/linear_interpolation.py +473 -0
  42. multioptpy/Interpolation/savitzky_golay_interpolation.py +252 -0
  43. multioptpy/Interpolation/spline_interpolation.py +353 -0
  44. multioptpy/MD/__init__.py +0 -0
  45. multioptpy/MD/thermostat.py +185 -0
  46. multioptpy/MEP/__init__.py +0 -0
  47. multioptpy/MEP/pathopt_bneb_force.py +443 -0
  48. multioptpy/MEP/pathopt_dmf_force.py +448 -0
  49. multioptpy/MEP/pathopt_dneb_force.py +130 -0
  50. multioptpy/MEP/pathopt_ewbneb_force.py +207 -0
  51. multioptpy/MEP/pathopt_gpneb_force.py +512 -0
  52. multioptpy/MEP/pathopt_lup_force.py +113 -0
  53. multioptpy/MEP/pathopt_neb_force.py +225 -0
  54. multioptpy/MEP/pathopt_nesb_force.py +205 -0
  55. multioptpy/MEP/pathopt_om_force.py +153 -0
  56. multioptpy/MEP/pathopt_qsm_force.py +174 -0
  57. multioptpy/MEP/pathopt_qsmv2_force.py +304 -0
  58. multioptpy/ModelFunction/__init__.py +7 -0
  59. multioptpy/ModelFunction/avoiding_model_function.py +29 -0
  60. multioptpy/ModelFunction/binary_image_ts_search_model_function.py +47 -0
  61. multioptpy/ModelFunction/conical_model_function.py +26 -0
  62. multioptpy/ModelFunction/opt_meci.py +50 -0
  63. multioptpy/ModelFunction/opt_mesx.py +47 -0
  64. multioptpy/ModelFunction/opt_mesx_2.py +49 -0
  65. multioptpy/ModelFunction/seam_model_function.py +27 -0
  66. multioptpy/ModelHessian/__init__.py +0 -0
  67. multioptpy/ModelHessian/approx_hessian.py +147 -0
  68. multioptpy/ModelHessian/calc_params.py +227 -0
  69. multioptpy/ModelHessian/fischer.py +236 -0
  70. multioptpy/ModelHessian/fischerd3.py +360 -0
  71. multioptpy/ModelHessian/fischerd4.py +398 -0
  72. multioptpy/ModelHessian/gfn0xtb.py +633 -0
  73. multioptpy/ModelHessian/gfnff.py +709 -0
  74. multioptpy/ModelHessian/lindh.py +165 -0
  75. multioptpy/ModelHessian/lindh2007d2.py +707 -0
  76. multioptpy/ModelHessian/lindh2007d3.py +822 -0
  77. multioptpy/ModelHessian/lindh2007d4.py +1030 -0
  78. multioptpy/ModelHessian/morse.py +106 -0
  79. multioptpy/ModelHessian/schlegel.py +144 -0
  80. multioptpy/ModelHessian/schlegeld3.py +322 -0
  81. multioptpy/ModelHessian/schlegeld4.py +559 -0
  82. multioptpy/ModelHessian/shortrange.py +346 -0
  83. multioptpy/ModelHessian/swartd2.py +496 -0
  84. multioptpy/ModelHessian/swartd3.py +706 -0
  85. multioptpy/ModelHessian/swartd4.py +918 -0
  86. multioptpy/ModelHessian/tshess.py +40 -0
  87. multioptpy/Optimizer/QHAdam.py +61 -0
  88. multioptpy/Optimizer/__init__.py +0 -0
  89. multioptpy/Optimizer/abc_fire.py +83 -0
  90. multioptpy/Optimizer/adabelief.py +58 -0
  91. multioptpy/Optimizer/adabound.py +68 -0
  92. multioptpy/Optimizer/adadelta.py +65 -0
  93. multioptpy/Optimizer/adaderivative.py +56 -0
  94. multioptpy/Optimizer/adadiff.py +68 -0
  95. multioptpy/Optimizer/adafactor.py +70 -0
  96. multioptpy/Optimizer/adam.py +65 -0
  97. multioptpy/Optimizer/adamax.py +62 -0
  98. multioptpy/Optimizer/adamod.py +83 -0
  99. multioptpy/Optimizer/adamw.py +65 -0
  100. multioptpy/Optimizer/adiis.py +523 -0
  101. multioptpy/Optimizer/afire_neb.py +282 -0
  102. multioptpy/Optimizer/block_hessian_update.py +709 -0
  103. multioptpy/Optimizer/c2diis.py +491 -0
  104. multioptpy/Optimizer/component_wise_scaling.py +405 -0
  105. multioptpy/Optimizer/conjugate_gradient.py +82 -0
  106. multioptpy/Optimizer/conjugate_gradient_neb.py +345 -0
  107. multioptpy/Optimizer/coordinate_locking.py +405 -0
  108. multioptpy/Optimizer/dic_rsirfo.py +1015 -0
  109. multioptpy/Optimizer/ediis.py +417 -0
  110. multioptpy/Optimizer/eve.py +76 -0
  111. multioptpy/Optimizer/fastadabelief.py +61 -0
  112. multioptpy/Optimizer/fire.py +77 -0
  113. multioptpy/Optimizer/fire2.py +249 -0
  114. multioptpy/Optimizer/fire_neb.py +92 -0
  115. multioptpy/Optimizer/gan_step.py +486 -0
  116. multioptpy/Optimizer/gdiis.py +609 -0
  117. multioptpy/Optimizer/gediis.py +203 -0
  118. multioptpy/Optimizer/geodesic_step.py +433 -0
  119. multioptpy/Optimizer/gpmin.py +633 -0
  120. multioptpy/Optimizer/gpr_step.py +364 -0
  121. multioptpy/Optimizer/gradientdescent.py +78 -0
  122. multioptpy/Optimizer/gradientdescent_neb.py +52 -0
  123. multioptpy/Optimizer/hessian_update.py +433 -0
  124. multioptpy/Optimizer/hybrid_rfo.py +998 -0
  125. multioptpy/Optimizer/kdiis.py +625 -0
  126. multioptpy/Optimizer/lars.py +21 -0
  127. multioptpy/Optimizer/lbfgs.py +253 -0
  128. multioptpy/Optimizer/lbfgs_neb.py +355 -0
  129. multioptpy/Optimizer/linesearch.py +236 -0
  130. multioptpy/Optimizer/lookahead.py +40 -0
  131. multioptpy/Optimizer/nadam.py +64 -0
  132. multioptpy/Optimizer/newton.py +200 -0
  133. multioptpy/Optimizer/prodigy.py +70 -0
  134. multioptpy/Optimizer/purtubation.py +16 -0
  135. multioptpy/Optimizer/quickmin_neb.py +245 -0
  136. multioptpy/Optimizer/radam.py +75 -0
  137. multioptpy/Optimizer/rfo_neb.py +302 -0
  138. multioptpy/Optimizer/ric_rfo.py +842 -0
  139. multioptpy/Optimizer/rl_step.py +627 -0
  140. multioptpy/Optimizer/rmspropgrave.py +65 -0
  141. multioptpy/Optimizer/rsirfo.py +1647 -0
  142. multioptpy/Optimizer/rsprfo.py +1056 -0
  143. multioptpy/Optimizer/sadam.py +60 -0
  144. multioptpy/Optimizer/samsgrad.py +63 -0
  145. multioptpy/Optimizer/tr_lbfgs.py +678 -0
  146. multioptpy/Optimizer/trim.py +273 -0
  147. multioptpy/Optimizer/trust_radius.py +207 -0
  148. multioptpy/Optimizer/trust_radius_neb.py +121 -0
  149. multioptpy/Optimizer/yogi.py +60 -0
  150. multioptpy/OtherMethod/__init__.py +0 -0
  151. multioptpy/OtherMethod/addf.py +1150 -0
  152. multioptpy/OtherMethod/dimer.py +895 -0
  153. multioptpy/OtherMethod/elastic_image_pair.py +629 -0
  154. multioptpy/OtherMethod/modelfunction.py +456 -0
  155. multioptpy/OtherMethod/newton_traj.py +454 -0
  156. multioptpy/OtherMethod/twopshs.py +1095 -0
  157. multioptpy/PESAnalyzer/__init__.py +0 -0
  158. multioptpy/PESAnalyzer/calc_irc_curvature.py +125 -0
  159. multioptpy/PESAnalyzer/cmds_analysis.py +152 -0
  160. multioptpy/PESAnalyzer/koopman_analysis.py +268 -0
  161. multioptpy/PESAnalyzer/pca_analysis.py +314 -0
  162. multioptpy/Parameters/__init__.py +0 -0
  163. multioptpy/Parameters/atomic_mass.py +20 -0
  164. multioptpy/Parameters/atomic_number.py +22 -0
  165. multioptpy/Parameters/covalent_radii.py +44 -0
  166. multioptpy/Parameters/d2.py +61 -0
  167. multioptpy/Parameters/d3.py +63 -0
  168. multioptpy/Parameters/d4.py +103 -0
  169. multioptpy/Parameters/dreiding.py +34 -0
  170. multioptpy/Parameters/gfn0xtb_param.py +137 -0
  171. multioptpy/Parameters/gfnff_param.py +315 -0
  172. multioptpy/Parameters/gnb.py +104 -0
  173. multioptpy/Parameters/parameter.py +22 -0
  174. multioptpy/Parameters/uff.py +72 -0
  175. multioptpy/Parameters/unit_values.py +20 -0
  176. multioptpy/Potential/AFIR_potential.py +55 -0
  177. multioptpy/Potential/LJ_repulsive_potential.py +345 -0
  178. multioptpy/Potential/__init__.py +0 -0
  179. multioptpy/Potential/anharmonic_keep_potential.py +28 -0
  180. multioptpy/Potential/asym_elllipsoidal_potential.py +718 -0
  181. multioptpy/Potential/electrostatic_potential.py +69 -0
  182. multioptpy/Potential/flux_potential.py +30 -0
  183. multioptpy/Potential/gaussian_potential.py +101 -0
  184. multioptpy/Potential/idpp.py +516 -0
  185. multioptpy/Potential/keep_angle_potential.py +146 -0
  186. multioptpy/Potential/keep_dihedral_angle_potential.py +105 -0
  187. multioptpy/Potential/keep_outofplain_angle_potential.py +70 -0
  188. multioptpy/Potential/keep_potential.py +99 -0
  189. multioptpy/Potential/mechano_force_potential.py +74 -0
  190. multioptpy/Potential/nanoreactor_potential.py +52 -0
  191. multioptpy/Potential/potential.py +896 -0
  192. multioptpy/Potential/spacer_model_potential.py +221 -0
  193. multioptpy/Potential/switching_potential.py +258 -0
  194. multioptpy/Potential/universal_potential.py +34 -0
  195. multioptpy/Potential/value_range_potential.py +36 -0
  196. multioptpy/Potential/void_point_potential.py +25 -0
  197. multioptpy/SQM/__init__.py +0 -0
  198. multioptpy/SQM/sqm1/__init__.py +0 -0
  199. multioptpy/SQM/sqm1/sqm1_core.py +1792 -0
  200. multioptpy/SQM/sqm2/__init__.py +0 -0
  201. multioptpy/SQM/sqm2/calc_tools.py +95 -0
  202. multioptpy/SQM/sqm2/sqm2_basis.py +850 -0
  203. multioptpy/SQM/sqm2/sqm2_bond.py +119 -0
  204. multioptpy/SQM/sqm2/sqm2_core.py +303 -0
  205. multioptpy/SQM/sqm2/sqm2_data.py +1229 -0
  206. multioptpy/SQM/sqm2/sqm2_disp.py +65 -0
  207. multioptpy/SQM/sqm2/sqm2_eeq.py +243 -0
  208. multioptpy/SQM/sqm2/sqm2_overlapint.py +704 -0
  209. multioptpy/SQM/sqm2/sqm2_qm.py +578 -0
  210. multioptpy/SQM/sqm2/sqm2_rep.py +66 -0
  211. multioptpy/SQM/sqm2/sqm2_srb.py +70 -0
  212. multioptpy/Thermo/__init__.py +0 -0
  213. multioptpy/Thermo/normal_mode_analyzer.py +865 -0
  214. multioptpy/Utils/__init__.py +0 -0
  215. multioptpy/Utils/bond_connectivity.py +264 -0
  216. multioptpy/Utils/calc_tools.py +884 -0
  217. multioptpy/Utils/oniom.py +96 -0
  218. multioptpy/Utils/pbc.py +48 -0
  219. multioptpy/Utils/riemann_curvature.py +208 -0
  220. multioptpy/Utils/symmetry_analyzer.py +482 -0
  221. multioptpy/Visualization/__init__.py +0 -0
  222. multioptpy/Visualization/visualization.py +156 -0
  223. multioptpy/WFAnalyzer/MO_analysis.py +104 -0
  224. multioptpy/WFAnalyzer/__init__.py +0 -0
  225. multioptpy/Wrapper/__init__.py +0 -0
  226. multioptpy/Wrapper/autots.py +1239 -0
  227. multioptpy/Wrapper/ieip_wrapper.py +93 -0
  228. multioptpy/Wrapper/md_wrapper.py +92 -0
  229. multioptpy/Wrapper/neb_wrapper.py +94 -0
  230. multioptpy/Wrapper/optimize_wrapper.py +76 -0
  231. multioptpy/__init__.py +5 -0
  232. multioptpy/entrypoints.py +916 -0
  233. multioptpy/fileio.py +660 -0
  234. multioptpy/ieip.py +340 -0
  235. multioptpy/interface.py +1086 -0
  236. multioptpy/irc.py +529 -0
  237. multioptpy/moleculardynamics.py +432 -0
  238. multioptpy/neb.py +1267 -0
  239. multioptpy/optimization.py +1553 -0
  240. multioptpy/optimizer.py +709 -0
  241. multioptpy-1.20.2.dist-info/METADATA +438 -0
  242. multioptpy-1.20.2.dist-info/RECORD +246 -0
  243. multioptpy-1.20.2.dist-info/WHEEL +5 -0
  244. multioptpy-1.20.2.dist-info/entry_points.txt +9 -0
  245. multioptpy-1.20.2.dist-info/licenses/LICENSE +674 -0
  246. multioptpy-1.20.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,918 @@
1
+ import numpy as np
2
+
3
+ from multioptpy.Parameters.parameter import UnitValueLib, covalent_radii_lib, UFF_VDW_distance_lib, D4Parameters, triple_covalent_radii_lib
4
+ from multioptpy.Utils.calc_tools import Calculationtools
5
+ from multioptpy.ModelHessian.calc_params import torsion2, outofplane2
6
+ from multioptpy.Parameters.parameter import UFF_VDW_distance_lib
7
+
8
+
9
+ class SwartD4ApproxHessian:
10
+ def __init__(self):
11
+ #Swart's Model Hessian augmented with D4 dispersion
12
+ #ref.: M. Swart, F. M. Bickelhaupt, Int. J. Quantum Chem., 2006, 106, 2536–2544.
13
+ #ref.: E. Caldeweyher, C. Bannwarth, S. Grimme, J. Chem. Phys., 2017, 147, 034112
14
+ self.bohr2angstroms = UnitValueLib().bohr2angstroms
15
+ self.hartree2kcalmol = UnitValueLib().hartree2kcalmol
16
+ self.kd = 2.00 # Dispersion scaling factor
17
+
18
+ self.kr = 0.35 # Bond stretching force constant scaling
19
+ self.kf = 0.15 # Angle bending force constant scaling
20
+ self.kt = 0.005 # Torsional force constant scaling
21
+
22
+ self.cutoff = 70.0 # Cutoff for long-range interactions
23
+ self.eps = 1.0e-12 # Small number for numerical stability
24
+
25
+ # D4 parameters
26
+ self.d4_params = D4Parameters()
27
+
28
+ # Cyano group parameters
29
+ self.cn_kr = 0.70 # Enhanced force constant for C≡N triple bond
30
+ self.cn_kf = 0.20 # Enhanced force constant for angles involving C≡N
31
+ self.cn_kt = 0.002 # Reduced force constant for torsions involving C≡N
32
+ return
33
+
34
+ def detect_cyano_groups(self, coord, element_list):
35
+ """Detect C≡N triple bonds in the structure"""
36
+ cyano_atoms = [] # List of (C_idx, N_idx) tuples
37
+
38
+ for i in range(len(coord)):
39
+ if element_list[i] != 'C':
40
+ continue
41
+
42
+ for j in range(len(coord)):
43
+ if i == j or element_list[j] != 'N':
44
+ continue
45
+
46
+ # Calculate distance between C and N
47
+ x_ij = coord[i][0] - coord[j][0]
48
+ y_ij = coord[i][1] - coord[j][1]
49
+ z_ij = coord[i][2] - coord[j][2]
50
+ r_ij = np.sqrt(x_ij**2 + y_ij**2 + z_ij**2)
51
+
52
+ # Check if distance is close to a triple bond length
53
+ cn_triple_bond = triple_covalent_radii_lib('C') + triple_covalent_radii_lib('N')
54
+
55
+ if abs(r_ij - cn_triple_bond) < 0.3: # Within 0.3 bohr of ideal length
56
+ # Check if C is connected to only one other atom (besides N)
57
+ connections_to_c = 0
58
+ for k in range(len(coord)):
59
+ if k == i or k == j:
60
+ continue
61
+
62
+ x_ik = coord[i][0] - coord[k][0]
63
+ y_ik = coord[i][1] - coord[k][1]
64
+ z_ik = coord[i][2] - coord[k][2]
65
+ r_ik = np.sqrt(x_ik**2 + y_ik**2 + z_ik**2)
66
+
67
+ cov_dist = covalent_radii_lib(element_list[i]) + covalent_radii_lib(element_list[k])
68
+ if r_ik < 1.3 * cov_dist: # Using 1.3 as a factor to account for bond length variations
69
+ connections_to_c += 1
70
+
71
+ # If C has only one other connection, it's likely a terminal cyano group
72
+ if connections_to_c <= 1:
73
+ cyano_atoms.append((i, j))
74
+
75
+ return cyano_atoms
76
+
77
+ def calculate_coordination_numbers(self, coord, element_list):
78
+ """Calculate atomic coordination numbers for D4 scaling"""
79
+ n_atoms = len(coord)
80
+ cn = np.zeros(n_atoms)
81
+
82
+ for i in range(n_atoms):
83
+ for j in range(n_atoms):
84
+ if i == j:
85
+ continue
86
+
87
+ # Calculate distance
88
+ r_ij = np.linalg.norm(coord[i] - coord[j])
89
+
90
+ # Get covalent radii
91
+ r_cov_i = covalent_radii_lib(element_list[i])
92
+ r_cov_j = covalent_radii_lib(element_list[j])
93
+
94
+ # Coordination number contribution using counting function
95
+ # k1 = 16.0, k2 = 4.0/3.0 (standard values from DFT-D4)
96
+ k1 = 16.0
97
+ k2 = 4.0/3.0
98
+ r0 = r_cov_i + r_cov_j
99
+
100
+ # Avoid overflow in exp
101
+ if k1 * (r_ij / r0 - 1.0) > 25.0:
102
+ continue
103
+
104
+ cn_contrib = 1.0 / (1.0 + np.exp(-k1 * (k2 * r0 / r_ij - 1.0)))
105
+ cn[i] += cn_contrib
106
+
107
+ return cn
108
+
109
+ def estimate_atomic_charges(self, coord, element_list):
110
+ """
111
+ Estimate atomic charges using electronegativity equalization
112
+ Simplified version for Hessian generation
113
+ """
114
+ n_atoms = len(coord)
115
+ charges = np.zeros(n_atoms)
116
+
117
+ # Calculate reference electronegativities
118
+ en_list = [self.d4_params.get_electronegativity(elem) for elem in element_list]
119
+
120
+ # Simple charge estimation based on electronegativity differences
121
+ # This is a very simplified model
122
+ for i in range(n_atoms):
123
+ for j in range(i+1, n_atoms):
124
+ r_ij = np.linalg.norm(coord[i] - coord[j])
125
+ r_cov_sum = covalent_radii_lib(element_list[i]) + covalent_radii_lib(element_list[j])
126
+
127
+ # Only consider bonded atoms
128
+ if r_ij < 1.3 * r_cov_sum:
129
+ en_diff = en_list[j] - en_list[i]
130
+ charge_transfer = 0.1 * en_diff # Simple approximation
131
+ charges[i] += charge_transfer
132
+ charges[j] -= charge_transfer
133
+
134
+ return charges
135
+
136
+ def calc_force_const(self, alpha, covalent_length, distance):
137
+ """Calculate force constant with exponential damping"""
138
+ force_const = np.exp(-1 * alpha * (distance / covalent_length - 1.0))
139
+ return force_const
140
+
141
+ def calc_vdw_force_const(self, alpha, r0, distance):
142
+ """Calculate van der Waals force constant with exponential damping"""
143
+ vdw_force_const = np.exp(-1 * alpha * (r0 - distance) ** 2)
144
+ return vdw_force_const
145
+
146
+ def d4_damping_function(self, r_ij, r0, order=6):
147
+ """D4 rational damping function"""
148
+ a1 = self.d4_params.a1
149
+ a2 = self.d4_params.a2
150
+
151
+ if order == 6:
152
+ return 1.0 / (1.0 + 6.0 * (r_ij / (a1 * r0)) ** a2)
153
+ elif order == 8:
154
+ return 1.0 / (1.0 + 6.0 * (r_ij / (a2 * r0)) ** a1)
155
+ return 0.0
156
+
157
+ def charge_scale_factor(self, charge, element):
158
+ """Calculate charge scaling factor for D4"""
159
+ ga = self.d4_params.ga # D4 charge scaling parameter
160
+ q_ref = self.d4_params.get_electronegativity(element)
161
+
162
+ # Prevent numerical issues with large exponents
163
+ exp_arg = -ga * abs(charge)
164
+ if exp_arg < -50.0: # Avoid underflow
165
+ return 0.0
166
+
167
+ return np.exp(exp_arg)
168
+
169
+ def get_d4_parameters(self, elem1, elem2, q1=0.0, q2=0.0, cn1=None, cn2=None):
170
+ """Get D4 parameters for a pair of elements with charge scaling"""
171
+ # Get polarizabilities
172
+ alpha1 = self.d4_params.get_polarizability(elem1)
173
+ alpha2 = self.d4_params.get_polarizability(elem2)
174
+
175
+ # Charge scaling
176
+ qscale1 = self.charge_scale_factor(q1, elem1)
177
+ qscale2 = self.charge_scale_factor(q2, elem2)
178
+
179
+ # Get R4/R2 values
180
+ r4r2_1 = self.d4_params.get_r4r2(elem1)
181
+ r4r2_2 = self.d4_params.get_r4r2(elem2)
182
+
183
+ # C6 coefficients with charge scaling
184
+ c6_1 = alpha1 * r4r2_1 * qscale1
185
+ c6_2 = alpha2 * r4r2_2 * qscale2
186
+ c6_param = 2.0 * c6_1 * c6_2 / (c6_1 + c6_2) # Effective C6 using harmonic mean
187
+
188
+ # C8 coefficients
189
+ c8_param = 3.0 * c6_param * np.sqrt(r4r2_1 * r4r2_2)
190
+
191
+ # r0 parameter (combined vdW radii)
192
+ r0_param = np.sqrt(UFF_VDW_distance_lib(elem1) * UFF_VDW_distance_lib(elem2))
193
+
194
+ return c6_param, c8_param, r0_param
195
+
196
+ def calc_d4_force_const(self, r_ij, c6_param, c8_param, r0_param):
197
+ """Calculate D4 dispersion force constant"""
198
+ s6 = self.d4_params.s6
199
+ s8 = self.d4_params.s8
200
+
201
+ # Apply damping functions
202
+ damp6 = self.d4_damping_function(r_ij, r0_param, order=6)
203
+ damp8 = self.d4_damping_function(r_ij, r0_param, order=8)
204
+
205
+ # Energy terms (negative because dispersion is attractive)
206
+ e6 = -s6 * c6_param / r_ij ** 6 * damp6
207
+ e8 = -s8 * c8_param / r_ij ** 8 * damp8
208
+
209
+ # Force constant is the second derivative of energy
210
+ fc6 = s6 * c6_param * (42.0 / r_ij ** 8) * damp6
211
+ fc8 = s8 * c8_param * (72.0 / r_ij ** 10) * damp8
212
+
213
+ return fc6 + fc8
214
+
215
+ def swart_bond(self, coord, element_list, charges, cn):
216
+ """Calculate bond stretching contributions to the Hessian with D4 dispersion"""
217
+ # Detect cyano groups
218
+ cyano_atoms = self.detect_cyano_groups(coord, element_list)
219
+ cyano_set = set()
220
+ for c_idx, n_idx in cyano_atoms:
221
+ cyano_set.add(c_idx)
222
+ cyano_set.add(n_idx)
223
+
224
+ for i in range(len(coord)):
225
+ for j in range(i):
226
+
227
+ x_ij = coord[i][0] - coord[j][0]
228
+ y_ij = coord[i][1] - coord[j][1]
229
+ z_ij = coord[i][2] - coord[j][2]
230
+ r_ij_2 = x_ij**2 + y_ij**2 + z_ij**2
231
+ r_ij = np.sqrt(r_ij_2)
232
+ covalent_length = covalent_radii_lib(element_list[i]) + covalent_radii_lib(element_list[j])
233
+
234
+ # Get D4 parameters with charge scaling
235
+ c6_param, c8_param, r0_param = self.get_d4_parameters(
236
+ element_list[i], element_list[j],
237
+ q1=charges[i], q2=charges[j],
238
+ cn1=cn[i], cn2=cn[j]
239
+ )
240
+
241
+ # Calculate D4 dispersion contribution
242
+ d4_force_const = self.calc_d4_force_const(r_ij, c6_param, c8_param, r0_param)
243
+
244
+ # Check if this is a cyano bond
245
+ is_cyano_bond = False
246
+ for c_idx, n_idx in cyano_atoms:
247
+ if (i == c_idx and j == n_idx) or (i == n_idx and j == c_idx):
248
+ is_cyano_bond = True
249
+ break
250
+
251
+ # Apply appropriate force constant
252
+ if is_cyano_bond:
253
+ # Special force constant for C≡N triple bond
254
+ g_mm = self.cn_kr * self.calc_force_const(1.0, covalent_length, r_ij) + self.kd * d4_force_const
255
+ else:
256
+ # Regular Swart force constant with D4 dispersion
257
+ g_mm = self.kr * self.calc_force_const(1.0, covalent_length, r_ij) + self.kd * d4_force_const
258
+
259
+ # Calculate Hessian components
260
+ hess_xx = g_mm * x_ij ** 2 / r_ij_2
261
+ hess_xy = g_mm * x_ij * y_ij / r_ij_2
262
+ hess_xz = g_mm * x_ij * z_ij / r_ij_2
263
+ hess_yy = g_mm * y_ij ** 2 / r_ij_2
264
+ hess_yz = g_mm * y_ij * z_ij / r_ij_2
265
+ hess_zz = g_mm * z_ij ** 2 / r_ij_2
266
+
267
+ # Fill the Hessian matrix
268
+ self.cart_hess[i * 3][i * 3] += hess_xx
269
+ self.cart_hess[i * 3 + 1][i * 3] += hess_xy
270
+ self.cart_hess[i * 3 + 1][i * 3 + 1] += hess_yy
271
+ self.cart_hess[i * 3 + 2][i * 3] += hess_xz
272
+ self.cart_hess[i * 3 + 2][i * 3 + 1] += hess_yz
273
+ self.cart_hess[i * 3 + 2][i * 3 + 2] += hess_zz
274
+
275
+ self.cart_hess[j * 3][j * 3] += hess_xx
276
+ self.cart_hess[j * 3 + 1][j * 3] += hess_xy
277
+ self.cart_hess[j * 3 + 1][j * 3 + 1] += hess_yy
278
+ self.cart_hess[j * 3 + 2][j * 3] += hess_xz
279
+ self.cart_hess[j * 3 + 2][j * 3 + 1] += hess_yz
280
+ self.cart_hess[j * 3 + 2][j * 3 + 2] += hess_zz
281
+
282
+ self.cart_hess[i * 3][j * 3] -= hess_xx
283
+ self.cart_hess[i * 3][j * 3 + 1] -= hess_xy
284
+ self.cart_hess[i * 3][j * 3 + 2] -= hess_xz
285
+ self.cart_hess[i * 3 + 1][j * 3] -= hess_xy
286
+ self.cart_hess[i * 3 + 1][j * 3 + 1] -= hess_yy
287
+ self.cart_hess[i * 3 + 1][j * 3 + 2] -= hess_yz
288
+ self.cart_hess[i * 3 + 2][j * 3] -= hess_xz
289
+ self.cart_hess[i * 3 + 2][j * 3 + 1] -= hess_yz
290
+ self.cart_hess[i * 3 + 2][j * 3 + 2] -= hess_zz
291
+
292
+ self.cart_hess[j * 3][i * 3] -= hess_xx
293
+ self.cart_hess[j * 3][i * 3 + 1] -= hess_xy
294
+ self.cart_hess[j * 3][i * 3 + 2] -= hess_xz
295
+ self.cart_hess[j * 3 + 1][i * 3] -= hess_xy
296
+ self.cart_hess[j * 3 + 1][i * 3 + 1] -= hess_yy
297
+ self.cart_hess[j * 3 + 1][i * 3 + 2] -= hess_yz
298
+ self.cart_hess[j * 3 + 2][i * 3] -= hess_xz
299
+ self.cart_hess[j * 3 + 2][i * 3 + 1] -= hess_yz
300
+ self.cart_hess[j * 3 + 2][i * 3 + 2] -= hess_zz
301
+
302
+ return
303
+
304
+ def swart_angle(self, coord, element_list, charges, cn):
305
+ """Calculate angle bending contributions to the Hessian with D4 dispersion"""
306
+ # Detect cyano groups
307
+ cyano_atoms = self.detect_cyano_groups(coord, element_list)
308
+ cyano_set = set()
309
+ for c_idx, n_idx in cyano_atoms:
310
+ cyano_set.add(c_idx)
311
+ cyano_set.add(n_idx)
312
+
313
+ for i in range(len(coord)):
314
+ for j in range(len(coord)):
315
+ if i == j:
316
+ continue
317
+ x_ij = coord[i][0] - coord[j][0]
318
+ y_ij = coord[i][1] - coord[j][1]
319
+ z_ij = coord[i][2] - coord[j][2]
320
+ r_ij_2 = x_ij**2 + y_ij**2 + z_ij**2
321
+ r_ij = np.sqrt(r_ij_2)
322
+ covalent_length_ij = covalent_radii_lib(element_list[i]) + covalent_radii_lib(element_list[j])
323
+
324
+ # Get D4 parameters with charge scaling for i-j pair
325
+ c6_ij, c8_ij, r0_ij = self.get_d4_parameters(
326
+ element_list[i], element_list[j],
327
+ q1=charges[i], q2=charges[j],
328
+ cn1=cn[i], cn2=cn[j]
329
+ )
330
+
331
+ for k in range(j):
332
+ if i == k:
333
+ continue
334
+ x_ik = coord[i][0] - coord[k][0]
335
+ y_ik = coord[i][1] - coord[k][1]
336
+ z_ik = coord[i][2] - coord[k][2]
337
+ r_ik_2 = x_ik**2 + y_ik**2 + z_ik**2
338
+ r_ik = np.sqrt(r_ik_2)
339
+ covalent_length_ik = covalent_radii_lib(element_list[i]) + covalent_radii_lib(element_list[k])
340
+
341
+ # Check for linear arrangement (cos_theta ~ 1.0)
342
+ error_check = x_ij * x_ik + y_ij * y_ik + z_ij * z_ik
343
+ error_check = error_check / (r_ij * r_ik)
344
+
345
+ if abs(error_check - 1.0) < self.eps:
346
+ continue
347
+
348
+ x_jk = coord[j][0] - coord[k][0]
349
+ y_jk = coord[j][1] - coord[k][1]
350
+ z_jk = coord[j][2] - coord[k][2]
351
+ r_jk_2 = x_jk**2 + y_jk**2 + z_jk**2
352
+ r_jk = np.sqrt(r_jk_2)
353
+
354
+ # Get D4 parameters with charge scaling for i-k pair
355
+ c6_ik, c8_ik, r0_ik = self.get_d4_parameters(
356
+ element_list[i], element_list[k],
357
+ q1=charges[i], q2=charges[k],
358
+ cn1=cn[i], cn2=cn[k]
359
+ )
360
+
361
+ # Calculate D4 dispersion contributions
362
+ d4_ij = self.calc_d4_force_const(r_ij, c6_ij, c8_ij, r0_ij)
363
+ d4_ik = self.calc_d4_force_const(r_ik, c6_ik, c8_ik, r0_ik)
364
+
365
+ # Calculate bond force constants with D4 dispersion
366
+ g_ij = self.calc_force_const(1.0, covalent_length_ij, r_ij) + 0.5 * self.kd * d4_ij
367
+ g_ik = self.calc_force_const(1.0, covalent_length_ik, r_ik) + 0.5 * self.kd * d4_ik
368
+
369
+ # Check if angle involves cyano group
370
+ is_cyano_angle = (i in cyano_set or j in cyano_set or k in cyano_set)
371
+
372
+ # Apply appropriate force constant
373
+ if is_cyano_angle:
374
+ # Special force constant for angles involving cyano groups
375
+ g_jk = self.cn_kf * g_ij * g_ik
376
+ else:
377
+ # Regular Swart force constant
378
+ g_jk = self.kf * g_ij * g_ik
379
+
380
+ # Calculate cross product for sin(theta)
381
+ r_cross_2 = (y_ij * z_ik - z_ij * y_ik) ** 2 + (z_ij * x_ik - x_ij * z_ik) ** 2 + (x_ij * y_ik - y_ij * x_ik) ** 2
382
+
383
+ if r_cross_2 < 1.0e-12:
384
+ r_cross = 0.0
385
+ else:
386
+ r_cross = np.sqrt(r_cross_2)
387
+
388
+ if r_ik > self.eps and r_ij > self.eps and r_jk > self.eps:
389
+ cos_theta = (r_ij_2 + r_ik_2 - r_jk_2) / (2.0 * r_ij * r_ik)
390
+ sin_theta = r_cross / (r_ij * r_ik)
391
+
392
+ dot_product_r_ij_r_ik = x_ij * x_ik + y_ij * y_ik + z_ij * z_ik
393
+
394
+ if sin_theta > self.eps: # non-linear
395
+ # Calculate derivatives for non-linear case
396
+ s_xj = (x_ij / r_ij * cos_theta - x_ik / r_ik) / (r_ij * sin_theta)
397
+ s_yj = (y_ij / r_ij * cos_theta - y_ik / r_ik) / (r_ij * sin_theta)
398
+ s_zj = (z_ij / r_ij * cos_theta - z_ik / r_ik) / (r_ij * sin_theta)
399
+
400
+ s_xk = (x_ik / r_ik * cos_theta - x_ij / r_ij) / (r_ik * sin_theta)
401
+ s_yk = (y_ik / r_ik * cos_theta - y_ij / r_ij) / (r_ik * sin_theta)
402
+ s_zk = (z_ik / r_ik * cos_theta - z_ij / r_ij) / (r_ik * sin_theta)
403
+
404
+ s_xi = -1 * s_xj - s_xk
405
+ s_yi = -1 * s_yj - s_yk
406
+ s_zi = -1 * s_zj - s_zk
407
+
408
+ s_j = [s_xj, s_yj, s_zj]
409
+ s_k = [s_xk, s_yk, s_zk]
410
+ s_i = [s_xi, s_yi, s_zi]
411
+
412
+ # Update Hessian for non-linear case
413
+ for l in range(3):
414
+ for m in range(3):
415
+ #-------------------------------------
416
+ if i > j:
417
+ tmp_val = g_jk * s_i[l] * s_j[m]
418
+ self.cart_hess[i * 3 + l][j * 3 + m] += tmp_val
419
+ else:
420
+ tmp_val = g_jk * s_j[l] * s_i[m]
421
+ self.cart_hess[j * 3 + l][i * 3 + m] += tmp_val
422
+
423
+ #-------------------------------------
424
+ if i > k:
425
+ tmp_val = g_jk * s_i[l] * s_k[m]
426
+ self.cart_hess[i * 3 + l][k * 3 + m] += tmp_val
427
+ else:
428
+ tmp_val = g_jk * s_k[l] * s_i[m]
429
+ self.cart_hess[k * 3 + l][i * 3 + m] += tmp_val
430
+
431
+ #-------------------------------------
432
+ if j > k:
433
+ tmp_val = g_jk * s_j[l] * s_k[m]
434
+ self.cart_hess[j * 3 + l][k * 3 + m] += tmp_val
435
+ else:
436
+ tmp_val = g_jk * s_k[l] * s_j[m]
437
+ self.cart_hess[k * 3 + l][j * 3 + m] += tmp_val
438
+ #-------------------------------------
439
+
440
+ # Update diagonal blocks
441
+ for l in range(3):
442
+ for m in range(l):
443
+ tmp_val_1 = g_jk * s_j[l] * s_j[m]
444
+ tmp_val_2 = g_jk * s_i[l] * s_i[m]
445
+ tmp_val_3 = g_jk * s_k[l] * s_k[m]
446
+
447
+ self.cart_hess[j * 3 + l][j * 3 + m] += tmp_val_1
448
+ self.cart_hess[i * 3 + l][i * 3 + m] += tmp_val_2
449
+ self.cart_hess[k * 3 + l][k * 3 + m] += tmp_val_3
450
+
451
+ else: # linear
452
+ # Special handling for linear arrangements
453
+ if abs(y_ij) < self.eps and abs(z_ij) < self.eps:
454
+ x_1 = -1 * y_ij
455
+ y_1 = x_ij
456
+ z_1 = 0.0
457
+ x_2 = -1 * x_ij * z_ij
458
+ y_2 = -1 * y_ij * z_ij
459
+ z_2 = x_ij ** 2 + y_ij ** 2
460
+ else:
461
+ x_1 = 1.0
462
+ y_1 = 0.0
463
+ z_1 = 0.0
464
+ x_2 = 0.0
465
+ y_2 = 1.0
466
+ z_2 = 0.0
467
+
468
+ x = [x_1, x_2]
469
+ y = [y_1, y_2]
470
+ z = [z_1, z_2]
471
+
472
+ # Iterate over two perpendicular directions
473
+ for ii in range(2):
474
+ r_1 = np.sqrt(x[ii] ** 2 + y[ii] ** 2 + z[ii] ** 2)
475
+ cos_theta_x = x[ii] / r_1
476
+ cos_theta_y = y[ii] / r_1
477
+ cos_theta_z = z[ii] / r_1
478
+
479
+ s_xj = -1 * cos_theta_x / r_ij
480
+ s_yj = -1 * cos_theta_y / r_ij
481
+ s_zj = -1 * cos_theta_z / r_ij
482
+ s_xk = -1 * cos_theta_x / r_ik
483
+ s_yk = -1 * cos_theta_y / r_ik
484
+ s_zk = -1 * cos_theta_z / r_ik
485
+
486
+ s_xi = -1 * s_xj - s_xk
487
+ s_yi = -1 * s_yj - s_yk
488
+ s_zi = -1 * s_zj - s_zk
489
+
490
+ s_j = [s_xj, s_yj, s_zj]
491
+ s_k = [s_xk, s_yk, s_zk]
492
+ s_i = [s_xi, s_yi, s_zi]
493
+
494
+ # Update Hessian for linear case
495
+ for l in range(3):
496
+ for m in range(3):
497
+ #-------------------------------------
498
+ if i > j:
499
+ tmp_val = g_jk * s_i[l] * s_j[m]
500
+ self.cart_hess[i * 3 + l][j * 3 + m] += tmp_val
501
+ else:
502
+ tmp_val = g_jk * s_j[l] * s_i[m]
503
+ self.cart_hess[j * 3 + l][i * 3 + m] += tmp_val
504
+ #-------------------------------------
505
+ if i > k:
506
+ tmp_val = g_jk * s_i[l] * s_k[m]
507
+ self.cart_hess[i * 3 + l][k * 3 + m] += tmp_val
508
+ else:
509
+ tmp_val = g_jk * s_k[l] * s_i[m]
510
+ self.cart_hess[k * 3 + l][i * 3 + m] += tmp_val
511
+ #-------------------------------------
512
+ if j > k:
513
+ tmp_val = g_jk * s_j[l] * s_k[m]
514
+ self.cart_hess[j * 3 + l][k * 3 + m] += tmp_val
515
+ else:
516
+ tmp_val = g_jk * s_k[l] * s_j[m]
517
+ self.cart_hess[k * 3 + l][j * 3 + m] += tmp_val
518
+ #-------------------------------------
519
+
520
+ # Update diagonal blocks for linear case
521
+ for l in range(3):
522
+ for m in range(l):
523
+ tmp_val_1 = g_jk * s_j[l] * s_j[m]
524
+ tmp_val_2 = g_jk * s_i[l] * s_i[m]
525
+ tmp_val_3 = g_jk * s_k[l] * s_k[m]
526
+
527
+ self.cart_hess[j * 3 + l][j * 3 + m] += tmp_val_1
528
+ self.cart_hess[i * 3 + l][i * 3 + m] += tmp_val_2
529
+ self.cart_hess[k * 3 + l][k * 3 + m] += tmp_val_3
530
+ else:
531
+ pass # Skip if any distance is too small
532
+
533
+ # Make the Hessian symmetric for angle terms
534
+ n_basis = len(coord) * 3
535
+ for i in range(n_basis):
536
+ for j in range(i):
537
+ if abs(self.cart_hess[i][j] - self.cart_hess[j][i]) > 1.0e-10:
538
+ avg = (self.cart_hess[i][j] + self.cart_hess[j][i]) / 2.0
539
+ self.cart_hess[i][j] = avg
540
+ self.cart_hess[j][i] = avg
541
+
542
+ return
543
+
544
+ def swart_dihedral_angle(self, coord, element_list, charges, cn):
545
+ """Calculate dihedral angle contributions to the Hessian with D4 dispersion"""
546
+ # Detect cyano groups
547
+ cyano_atoms = self.detect_cyano_groups(coord, element_list)
548
+ cyano_set = set()
549
+ for c_idx, n_idx in cyano_atoms:
550
+ cyano_set.add(c_idx)
551
+ cyano_set.add(n_idx)
552
+
553
+ for j in range(len(coord)):
554
+ t_xyz_2 = coord[j]
555
+
556
+ for k in range(len(coord)):
557
+ if j >= k:
558
+ continue
559
+ t_xyz_3 = coord[k]
560
+ for i in range(len(coord)):
561
+ ij = (len(coord) * j) + (i + 1)
562
+ if i >= j:
563
+ continue
564
+ if i >= k:
565
+ continue
566
+
567
+ t_xyz_1 = coord[i]
568
+
569
+ for l in range(len(coord)):
570
+ kl = (len(coord) * k) + (l + 1)
571
+
572
+ if ij <= kl:
573
+ continue
574
+ if l >= i:
575
+ continue
576
+ if l >= j:
577
+ continue
578
+ if l >= k:
579
+ continue
580
+
581
+ t_xyz_4 = coord[l]
582
+ r_ij = coord[i] - coord[j]
583
+ r_jk = coord[j] - coord[k]
584
+ r_kl = coord[k] - coord[l]
585
+
586
+ covalent_length_ij = covalent_radii_lib(element_list[i]) + covalent_radii_lib(element_list[j])
587
+ covalent_length_jk = covalent_radii_lib(element_list[j]) + covalent_radii_lib(element_list[k])
588
+ covalent_length_kl = covalent_radii_lib(element_list[k]) + covalent_radii_lib(element_list[l])
589
+
590
+ # Calculate vector magnitudes
591
+ r_ij_2 = np.sum(r_ij ** 2)
592
+ r_jk_2 = np.sum(r_jk ** 2)
593
+ r_kl_2 = np.sum(r_kl ** 2)
594
+ norm_r_ij = np.sqrt(r_ij_2)
595
+ norm_r_jk = np.sqrt(r_jk_2)
596
+ norm_r_kl = np.sqrt(r_kl_2)
597
+
598
+ # Skip if angle is too shallow (less than 35 degrees)
599
+ a35 = (35.0/180)* np.pi
600
+ cosfi_max = np.cos(a35)
601
+ cosfi2 = np.dot(r_ij, r_jk) / np.sqrt(r_ij_2 * r_jk_2)
602
+ if abs(cosfi2) > cosfi_max:
603
+ continue
604
+ cosfi3 = np.dot(r_kl, r_jk) / np.sqrt(r_kl_2 * r_jk_2)
605
+ if abs(cosfi3) > cosfi_max:
606
+ continue
607
+
608
+ # Get D4 parameters for each atom pair with charge scaling
609
+ c6_ij, c8_ij, r0_ij = self.get_d4_parameters(
610
+ element_list[i], element_list[j],
611
+ q1=charges[i], q2=charges[j],
612
+ cn1=cn[i], cn2=cn[j]
613
+ )
614
+
615
+ c6_jk, c8_jk, r0_jk = self.get_d4_parameters(
616
+ element_list[j], element_list[k],
617
+ q1=charges[j], q2=charges[k],
618
+ cn1=cn[j], cn2=cn[k]
619
+ )
620
+
621
+ c6_kl, c8_kl, r0_kl = self.get_d4_parameters(
622
+ element_list[k], element_list[l],
623
+ q1=charges[k], q2=charges[l],
624
+ cn1=cn[k], cn2=cn[l]
625
+ )
626
+
627
+ # Calculate D4 dispersion contributions
628
+ d4_ij = self.calc_d4_force_const(norm_r_ij, c6_ij, c8_ij, r0_ij)
629
+ d4_jk = self.calc_d4_force_const(norm_r_jk, c6_jk, c8_jk, r0_jk)
630
+ d4_kl = self.calc_d4_force_const(norm_r_kl, c6_kl, c8_kl, r0_kl)
631
+
632
+ # Calculate bond force constants with D4 dispersion
633
+ g_ij = self.calc_force_const(1.0, covalent_length_ij, norm_r_ij) + 0.5 * self.kd * d4_ij
634
+ g_jk = self.calc_force_const(1.0, covalent_length_jk, norm_r_jk) + 0.5 * self.kd * d4_jk
635
+ g_kl = self.calc_force_const(1.0, covalent_length_kl, norm_r_kl) + 0.5 * self.kd * d4_kl
636
+
637
+ # Check if torsion involves cyano group
638
+ is_cyano_torsion = False
639
+ if i in cyano_set or j in cyano_set or k in cyano_set or l in cyano_set:
640
+ is_cyano_torsion = True
641
+
642
+ # Adjust force constant for cyano groups - they have flatter torsional potentials
643
+ if is_cyano_torsion:
644
+ t_ij = self.cn_kt * g_ij * g_jk * g_kl
645
+ else:
646
+ t_ij = self.kt * g_ij * g_jk * g_kl
647
+
648
+ # Calculate torsion angle and derivatives
649
+ t_xyz = np.array([t_xyz_1, t_xyz_2, t_xyz_3, t_xyz_4])
650
+ tau, c = torsion2(t_xyz)
651
+ s_i = c[0]
652
+ s_j = c[1]
653
+ s_k = c[2]
654
+ s_l = c[3]
655
+
656
+ # Update Hessian with torsional contributions
657
+ for n in range(3):
658
+ for m in range(3):
659
+ self.cart_hess[3 * i + n][3 * j + m] += t_ij * s_i[n] * s_j[m]
660
+ self.cart_hess[3 * i + n][3 * k + m] += t_ij * s_i[n] * s_k[m]
661
+ self.cart_hess[3 * i + n][3 * l + m] += t_ij * s_i[n] * s_l[m]
662
+ self.cart_hess[3 * j + n][3 * k + m] += t_ij * s_j[n] * s_k[m]
663
+ self.cart_hess[3 * j + n][3 * l + m] += t_ij * s_j[n] * s_l[m]
664
+ self.cart_hess[3 * k + n][3 * l + m] += t_ij * s_k[n] * s_l[m]
665
+
666
+ # Update diagonal blocks
667
+ for n in range(3):
668
+ for m in range(n):
669
+ self.cart_hess[3 * i + n][3 * i + m] += t_ij * s_i[n] * s_i[m]
670
+ self.cart_hess[3 * j + n][3 * j + m] += t_ij * s_j[n] * s_j[m]
671
+ self.cart_hess[3 * k + n][3 * k + m] += t_ij * s_k[n] * s_k[m]
672
+ self.cart_hess[3 * l + n][3 * l + m] += t_ij * s_l[n] * s_l[m]
673
+
674
+ return
675
+
676
+ def swart_out_of_plane(self, coord, element_list, charges, cn):
677
+ """Calculate out-of-plane bending contributions to the Hessian with D4 dispersion"""
678
+ # Detect cyano groups
679
+ cyano_atoms = self.detect_cyano_groups(coord, element_list)
680
+ cyano_set = set()
681
+ for c_idx, n_idx in cyano_atoms:
682
+ cyano_set.add(c_idx)
683
+ cyano_set.add(n_idx)
684
+
685
+ for i in range(len(coord)):
686
+ t_xyz_4 = coord[i]
687
+ for j in range(len(coord)):
688
+ if i >= j:
689
+ continue
690
+ t_xyz_1 = coord[j]
691
+ for k in range(len(coord)):
692
+ ij = (len(coord) * j) + (i + 1)
693
+ if i >= k:
694
+ continue
695
+ if j >= k:
696
+ continue
697
+ t_xyz_2 = coord[k]
698
+
699
+ for l in range(len(coord)):
700
+ kl = (len(coord) * k) + (l + 1)
701
+ if i >= l:
702
+ continue
703
+ if j >= l:
704
+ continue
705
+ if k >= l:
706
+ continue
707
+ if ij <= kl:
708
+ continue
709
+ t_xyz_3 = coord[l]
710
+
711
+ r_ij = coord[i] - coord[j]
712
+ r_ik = coord[i] - coord[k]
713
+ r_il = coord[i] - coord[l]
714
+
715
+ covalent_length_ij = covalent_radii_lib(element_list[i]) + covalent_radii_lib(element_list[j])
716
+ covalent_length_ik = covalent_radii_lib(element_list[i]) + covalent_radii_lib(element_list[k])
717
+ covalent_length_il = covalent_radii_lib(element_list[i]) + covalent_radii_lib(element_list[l])
718
+
719
+ r_ij_2 = np.sum(r_ij ** 2)
720
+ r_ik_2 = np.sum(r_ik ** 2)
721
+ r_il_2 = np.sum(r_il ** 2)
722
+ norm_r_ij = np.sqrt(r_ij_2)
723
+ norm_r_ik = np.sqrt(r_ik_2)
724
+ norm_r_il = np.sqrt(r_il_2)
725
+
726
+ # Skip if atoms are nearly collinear
727
+ cosfi2 = np.dot(r_ij, r_ik) / np.sqrt(r_ij_2 * r_ik_2)
728
+ if abs(abs(cosfi2) - 1.0) < 1.0e-1:
729
+ continue
730
+ cosfi3 = np.dot(r_ij, r_il) / np.sqrt(r_ij_2 * r_il_2)
731
+ if abs(abs(cosfi3) - 1.0) < 1.0e-1:
732
+ continue
733
+ cosfi4 = np.dot(r_ik, r_il) / np.sqrt(r_ik_2 * r_il_2)
734
+ if abs(abs(cosfi4) - 1.0) < 1.0e-1:
735
+ continue
736
+
737
+ # Get D4 parameters with charge scaling for each atom pair
738
+ c6_ij, c8_ij, r0_ij = self.get_d4_parameters(
739
+ element_list[i], element_list[j],
740
+ q1=charges[i], q2=charges[j],
741
+ cn1=cn[i], cn2=cn[j]
742
+ )
743
+
744
+ c6_ik, c8_ik, r0_ik = self.get_d4_parameters(
745
+ element_list[i], element_list[k],
746
+ q1=charges[i], q2=charges[k],
747
+ cn1=cn[i], cn2=cn[k]
748
+ )
749
+
750
+ c6_il, c8_il, r0_il = self.get_d4_parameters(
751
+ element_list[i], element_list[l],
752
+ q1=charges[i], q2=charges[l],
753
+ cn1=cn[i], cn2=cn[l]
754
+ )
755
+
756
+ # Calculate D4 dispersion contributions
757
+ d4_ij = self.calc_d4_force_const(norm_r_ij, c6_ij, c8_ij, r0_ij)
758
+ d4_ik = self.calc_d4_force_const(norm_r_ik, c6_ik, c8_ik, r0_ik)
759
+ d4_il = self.calc_d4_force_const(norm_r_il, c6_il, c8_il, r0_il)
760
+
761
+ # Calculate bond force constants with D4 dispersion
762
+ g_ij = self.calc_force_const(1.0, covalent_length_ij, norm_r_ij) + 0.5 * self.kd * d4_ij
763
+ g_ik = self.calc_force_const(1.0, covalent_length_ik, norm_r_ik) + 0.5 * self.kd * d4_ik
764
+ g_il = self.calc_force_const(1.0, covalent_length_il, norm_r_il) + 0.5 * self.kd * d4_il
765
+
766
+ # Check if any atom is part of a cyano group
767
+ is_cyano_involved = (i in cyano_set or j in cyano_set or
768
+ k in cyano_set or l in cyano_set)
769
+
770
+ # Adjust force constant if cyano group is involved
771
+ if is_cyano_involved:
772
+ t_ij = 0.5 * self.kt * g_ij * g_ik * g_il # Reduce force constant for cyano
773
+ else:
774
+ t_ij = self.kt * g_ij * g_ik * g_il
775
+
776
+ # Calculate out-of-plane angle and derivatives
777
+ t_xyz = np.array([t_xyz_1, t_xyz_2, t_xyz_3, t_xyz_4])
778
+ theta, c = outofplane2(t_xyz)
779
+
780
+ s_i = c[0]
781
+ s_j = c[1]
782
+ s_k = c[2]
783
+ s_l = c[3]
784
+
785
+ # Update Hessian with out-of-plane contributions
786
+ for n in range(3):
787
+ for m in range(3):
788
+ self.cart_hess[i * 3 + n][j * 3 + m] += t_ij * s_i[n] * s_j[m]
789
+ self.cart_hess[i * 3 + n][k * 3 + m] += t_ij * s_i[n] * s_k[m]
790
+ self.cart_hess[i * 3 + n][l * 3 + m] += t_ij * s_i[n] * s_l[m]
791
+ self.cart_hess[j * 3 + n][k * 3 + m] += t_ij * s_j[n] * s_k[m]
792
+ self.cart_hess[j * 3 + n][l * 3 + m] += t_ij * s_j[n] * s_l[m]
793
+ self.cart_hess[k * 3 + n][l * 3 + m] += t_ij * s_k[n] * s_l[m]
794
+
795
+ # Update diagonal blocks
796
+ for n in range(3):
797
+ for m in range(n):
798
+ self.cart_hess[i * 3 + n][i * 3 + m] += t_ij * s_i[n] * s_i[m]
799
+ self.cart_hess[j * 3 + n][j * 3 + m] += t_ij * s_j[n] * s_j[m]
800
+ self.cart_hess[k * 3 + n][k * 3 + m] += t_ij * s_k[n] * s_k[m]
801
+ self.cart_hess[l * 3 + n][l * 3 + m] += t_ij * s_l[n] * s_l[m]
802
+
803
+ return
804
+
805
+ def calculate_three_body_term(self, coord, element_list, charges, cn):
806
+ """Calculate three-body dispersion contribution to the Hessian (D4 specific)"""
807
+ s9 = self.d4_params.s9 # Scaling parameter for three-body term
808
+ if abs(s9) < 1e-12:
809
+ return # Skip if three-body term is turned off
810
+
811
+ n_atoms = len(coord)
812
+
813
+ # Loop over all atom triplets
814
+ for i in range(n_atoms):
815
+ for j in range(i):
816
+ for k in range(j):
817
+ # Get positions
818
+ r_i = coord[i]
819
+ r_j = coord[j]
820
+ r_k = coord[k]
821
+
822
+ # Calculate interatomic distances
823
+ r_ij = np.linalg.norm(r_i - r_j)
824
+ r_jk = np.linalg.norm(r_j - r_k)
825
+ r_ki = np.linalg.norm(r_k - r_i)
826
+
827
+ # Get coordination-number scaled C6 coefficients
828
+ c6_ij, _, r0_ij = self.get_d4_parameters(
829
+ element_list[i], element_list[j],
830
+ q1=charges[i], q2=charges[j],
831
+ cn1=cn[i], cn2=cn[j]
832
+ )
833
+
834
+ c6_jk, _, r0_jk = self.get_d4_parameters(
835
+ element_list[j], element_list[k],
836
+ q1=charges[j], q2=charges[k],
837
+ cn1=cn[j], cn2=cn[k]
838
+ )
839
+
840
+ c6_ki, _, r0_ki = self.get_d4_parameters(
841
+ element_list[k], element_list[i],
842
+ q1=charges[k], q2=charges[i],
843
+ cn1=cn[k], cn2=cn[i]
844
+ )
845
+
846
+ # Calculate geometric mean of C6 coefficients
847
+ c9 = (c6_ij * c6_jk * c6_ki) ** (1.0/3.0)
848
+
849
+ # Calculate three-body damping
850
+ damp_ij = self.d4_damping_function(r_ij, r0_ij)
851
+ damp_jk = self.d4_damping_function(r_jk, r0_jk)
852
+ damp_ki = self.d4_damping_function(r_ki, r0_ki)
853
+ damp = damp_ij * damp_jk * damp_ki
854
+
855
+ # Calculate angle factor
856
+ cos_ijk = np.dot(r_i - r_j, r_k - r_j) / (r_ij * r_jk)
857
+ cos_jki = np.dot(r_j - r_k, r_i - r_k) / (r_jk * r_ki)
858
+ cos_kij = np.dot(r_k - r_i, r_j - r_i) / (r_ki * r_ij)
859
+ angle_factor = 1.0 + 3.0 * cos_ijk * cos_jki * cos_kij
860
+
861
+ # Calculate three-body energy term
862
+ e_3 = s9 * angle_factor * c9 * damp / (r_ij * r_jk * r_ki) ** 3
863
+
864
+ # Calculate force constants (second derivatives)
865
+ # This is a simplified approximation - full D4 three-body Hessian is complex
866
+ fc_scale = 0.01 * s9 * angle_factor * c9 * damp
867
+
868
+ # Add approximate three-body contributions to Hessian
869
+ for n in range(3):
870
+ for m in range(3):
871
+ # Diagonal blocks (diagonal atoms)
872
+ if n == m:
873
+ self.cart_hess[i * 3 + n][i * 3 + m] += fc_scale / r_ij**6 + fc_scale / r_ki**6
874
+ self.cart_hess[j * 3 + n][j * 3 + m] += fc_scale / r_ij**6 + fc_scale / r_jk**6
875
+ self.cart_hess[k * 3 + n][k * 3 + m] += fc_scale / r_jk**6 + fc_scale / r_ki**6
876
+
877
+ # Off-diagonal blocks (between atoms)
878
+ self.cart_hess[i * 3 + n][j * 3 + m] -= fc_scale / r_ij**6
879
+ self.cart_hess[j * 3 + n][i * 3 + m] -= fc_scale / r_ij**6
880
+
881
+ self.cart_hess[j * 3 + n][k * 3 + m] -= fc_scale / r_jk**6
882
+ self.cart_hess[k * 3 + n][j * 3 + m] -= fc_scale / r_jk**6
883
+
884
+ self.cart_hess[k * 3 + n][i * 3 + m] -= fc_scale / r_ki**6
885
+ self.cart_hess[i * 3 + n][k * 3 + m] -= fc_scale / r_ki**6
886
+
887
+ return
888
+
889
+ def main(self, coord, element_list, cart_gradient):
890
+ """Main method to calculate the approximate Hessian using Swart's model with D4 dispersion"""
891
+ print("Generating Swart's approximate hessian with D4 dispersion correction...")
892
+ self.cart_hess = np.zeros((len(coord) * 3, len(coord) * 3), dtype="float64")
893
+
894
+ # Calculate coordination numbers and atomic charges for D4
895
+ cn = self.calculate_coordination_numbers(coord, element_list)
896
+ charges = self.estimate_atomic_charges(coord, element_list)
897
+
898
+ # Calculate all contributions to the Hessian
899
+ self.swart_bond(coord, element_list, charges, cn)
900
+ self.swart_angle(coord, element_list, charges, cn)
901
+ self.swart_dihedral_angle(coord, element_list, charges, cn)
902
+ self.swart_out_of_plane(coord, element_list, charges, cn)
903
+
904
+ # Add D4-specific three-body term
905
+ self.calculate_three_body_term(coord, element_list, charges, cn)
906
+
907
+ # Ensure symmetry of the Hessian matrix
908
+ n_basis = len(coord) * 3
909
+ for i in range(n_basis):
910
+ for j in range(i):
911
+ if abs(self.cart_hess[i][j] - self.cart_hess[j][i]) > 1.0e-10:
912
+ avg = (self.cart_hess[i][j] + self.cart_hess[j][i]) / 2.0
913
+ self.cart_hess[i][j] = avg
914
+ self.cart_hess[j][i] = avg
915
+
916
+ # Project out translational and rotational modes
917
+ hess_proj = Calculationtools().project_out_hess_tr_and_rot_for_coord(self.cart_hess, element_list, coord)
918
+ return hess_proj