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,818 @@
1
+ import glob
2
+ import os
3
+ import numpy as np
4
+ from abc import ABC, abstractmethod
5
+ import warnings
6
+
7
+ # Suppress specific warnings during calculation
8
+ warnings.filterwarnings('ignore', category=RuntimeWarning, message='overflow encountered')
9
+ warnings.filterwarnings('ignore', category=RuntimeWarning, message='invalid value encountered')
10
+
11
+ from multioptpy.Utils.calc_tools import Calculationtools
12
+ from multioptpy.Parameters.parameter import UnitValueLib, number_element
13
+ from multioptpy.fileio import xyz2list
14
+ from multioptpy.Visualization.visualization import NEBVisualizer
15
+
16
+ class TersoffCore:
17
+ """
18
+ Core calculator for Tersoff potential.
19
+ Handles both homo- and hetero-atomic clusters with appropriate parameters.
20
+ """
21
+ # Tersoff parameters for common elements
22
+ # Source: Tersoff, J. Physical Review B, 1989, 39, 5566-5568 (Si)
23
+ # Tersoff, J. Physical Review Letters, 1988, 61, 2879-2882 (C)
24
+ TERSOFF_PARAMETERS = {
25
+ 'Si': {
26
+ 'A': 1830.8, 'B': 471.18, 'lambda': 2.4799, 'mu': 1.7322,
27
+ 'beta': 1.1e-6, 'n': 0.78734, 'c': 1.0039e5, 'd': 16.217,
28
+ 'h': -0.59825, 'R': 2.7, 'D': 0.3
29
+ },
30
+ 'C': {
31
+ 'A': 1393.6, 'B': 346.74, 'lambda': 3.4879, 'mu': 2.2119,
32
+ 'beta': 1.5724e-7, 'n': 0.72751, 'c': 3.8049e4, 'd': 4.3484,
33
+ 'h': -0.57058, 'R': 1.95, 'D': 0.15
34
+ },
35
+ 'Ge': {
36
+ 'A': 1769.0, 'B': 419.23, 'lambda': 2.4451, 'mu': 1.7047,
37
+ 'beta': 9.0166e-7, 'n': 0.75627, 'c': 1.0643e5, 'd': 15.652,
38
+ 'h': -0.43884, 'R': 2.95, 'D': 0.15
39
+ },
40
+ }
41
+
42
+ # Mixing rules parameters for hetero-atomic interactions
43
+ # Based on common approaches in literature
44
+ MIX_PARAMETERS = {
45
+ ('Si', 'C'): {
46
+ 'A': 1612.2, 'B': 395.15, 'lambda': 2.9839, 'mu': 1.9720,
47
+ 'beta': 1.1e-6, 'n': 0.75743, 'c': 6.0e4, 'd': 13.0,
48
+ 'h': -0.585, 'R': 2.4, 'D': 0.2
49
+ },
50
+ ('Si', 'Ge'): {
51
+ 'A': 1800.0, 'B': 445.0, 'lambda': 2.46, 'mu': 1.72,
52
+ 'beta': 1.0e-6, 'n': 0.77, 'c': 1.03e5, 'd': 15.9,
53
+ 'h': -0.52, 'R': 2.8, 'D': 0.2
54
+ },
55
+ ('C', 'Ge'): {
56
+ 'A': 1580.0, 'B': 380.0, 'lambda': 2.97, 'mu': 1.96,
57
+ 'beta': 1.0e-6, 'n': 0.74, 'c': 7.0e4, 'd': 12.0,
58
+ 'h': -0.5, 'R': 2.5, 'D': 0.2
59
+ },
60
+ }
61
+
62
+ # Constants for numerical stability - much more restrictive limits
63
+ MAX_EXPONENT = 50.0 # Much lower maximum exponent to prevent overflow
64
+ MIN_DISTANCE = 1e-8 # Minimum distance to avoid division by zero
65
+ EPSILON = 1e-8 # Small value to avoid division by zero
66
+ MAX_VALUE = 1e6 # Maximum value for any intermediate result
67
+
68
+ def __init__(self):
69
+ """Initializes a general Tersoff potential calculator."""
70
+ self.UVL = UnitValueLib()
71
+ # Cache for memoizing parameters of atom types
72
+ self._param_cache = {}
73
+ # Conversion factors
74
+ self.angstrom_to_bohr = self.UVL.bohr2angstroms
75
+ self.ev_to_hartree = 1.0 / self.UVL.hartree2eV
76
+
77
+ def get_parameters(self, atom_i, atom_j):
78
+ """
79
+ Retrieves Tersoff parameters for a pair of atoms.
80
+ Returns a dictionary of parameters for the interaction.
81
+ """
82
+ # Check cache first
83
+ pair_key = (atom_i, atom_j)
84
+ if pair_key in self._param_cache:
85
+ return self._param_cache[pair_key]
86
+
87
+ # For homo-atomic pairs, use direct parameters
88
+ if atom_i == atom_j:
89
+ if atom_i not in self.TERSOFF_PARAMETERS:
90
+ raise ValueError(f"Atom symbol '{atom_i}' is not supported. "
91
+ f"Supported: {list(self.TERSOFF_PARAMETERS.keys())}")
92
+ params = self.TERSOFF_PARAMETERS[atom_i].copy()
93
+
94
+ # For hetero-atomic pairs, use mixing parameters if available
95
+ else:
96
+ sorted_pair = tuple(sorted([atom_i, atom_j]))
97
+ if sorted_pair in self.MIX_PARAMETERS:
98
+ params = self.MIX_PARAMETERS[sorted_pair].copy()
99
+ else:
100
+ # Apply simple mixing rules if specific parameters not available
101
+ if atom_i not in self.TERSOFF_PARAMETERS or atom_j not in self.TERSOFF_PARAMETERS:
102
+ raise ValueError(f"One or both atoms {atom_i}, {atom_j} are not supported.")
103
+
104
+ params_i = self.TERSOFF_PARAMETERS[atom_i]
105
+ params_j = self.TERSOFF_PARAMETERS[atom_j]
106
+
107
+ params = {
108
+ 'A': np.sqrt(params_i['A'] * params_j['A']),
109
+ 'B': np.sqrt(params_i['B'] * params_j['B']),
110
+ 'lambda': 0.5 * (params_i['lambda'] + params_j['lambda']),
111
+ 'mu': 0.5 * (params_i['mu'] + params_j['mu']),
112
+ 'beta': np.sqrt(params_i['beta'] * params_j['beta']),
113
+ 'n': 0.5 * (params_i['n'] + params_j['n']),
114
+ 'c': np.sqrt(params_i['c'] * params_j['c']),
115
+ 'd': np.sqrt(params_i['d'] * params_j['d']),
116
+ 'h': 0.5 * (params_i['h'] + params_j['h']),
117
+ 'R': 0.5 * (params_i['R'] + params_j['R']),
118
+ 'D': 0.5 * (params_i['D'] + params_j['D']),
119
+ }
120
+
121
+ # Convert parameters from eV to Hartree and Å to Bohr
122
+ # Energy parameters: A, B
123
+ params['A'] *= self.ev_to_hartree
124
+ params['B'] *= self.ev_to_hartree
125
+
126
+ # Distance parameters: lambda, mu, R, D
127
+ params['lambda'] /= self.angstrom_to_bohr
128
+ params['mu'] /= self.angstrom_to_bohr
129
+ params['R'] /= self.angstrom_to_bohr
130
+ params['D'] /= self.angstrom_to_bohr
131
+
132
+ self._param_cache[pair_key] = params
133
+ return params
134
+
135
+ def safe_exp(self, x):
136
+ """
137
+ Safely compute exponential function to avoid overflow.
138
+ Uses a much more restrictive cap on the exponent.
139
+ """
140
+ if isinstance(x, np.ndarray):
141
+ # For arrays, clip element-wise
142
+ clipped_x = np.clip(x, -self.MAX_EXPONENT, self.MAX_EXPONENT)
143
+ return np.exp(clipped_x)
144
+ else:
145
+ # For scalar values
146
+ if x > self.MAX_EXPONENT:
147
+ return np.exp(self.MAX_EXPONENT)
148
+ elif x < -self.MAX_EXPONENT:
149
+ return np.exp(-self.MAX_EXPONENT)
150
+ return np.exp(x)
151
+
152
+ def safe_value(self, x):
153
+ """
154
+ Clip any value to a safe range to prevent overflow in subsequent calculations.
155
+ """
156
+ if isinstance(x, np.ndarray):
157
+ return np.clip(x, -self.MAX_VALUE, self.MAX_VALUE)
158
+ else:
159
+ return max(min(x, self.MAX_VALUE), -self.MAX_VALUE)
160
+
161
+ def calculate_cutoff(self, r, R, D):
162
+ """Calculate the cutoff function f_c(r)."""
163
+ if r <= (R - D):
164
+ return 1.0
165
+ elif r >= (R + D):
166
+ return 0.0
167
+ else:
168
+ return 0.5 - 0.5 * np.sin(np.pi * (r - R) / (2 * D))
169
+
170
+ def calculate_cutoff_derivative(self, r, R, D):
171
+ """Calculate the derivative of the cutoff function df_c(r)/dr."""
172
+ if r <= (R - D) or r >= (R + D):
173
+ return 0.0
174
+ else:
175
+ return -0.5 * np.pi / (2 * D) * np.cos(np.pi * (r - R) / (2 * D))
176
+
177
+ def calculate_bond_angle(self, r_ij, r_ik):
178
+ """Calculate the cosine of the angle between bonds ij and ik."""
179
+ # Add small values to avoid division by zero
180
+ norm_rij = np.linalg.norm(r_ij)
181
+ norm_rik = np.linalg.norm(r_ik)
182
+
183
+ # Check for extremely small values
184
+ if norm_rij < self.MIN_DISTANCE or norm_rik < self.MIN_DISTANCE:
185
+ return 0.0
186
+
187
+ cos_theta = np.dot(r_ij, r_ik) / (norm_rij * norm_rik)
188
+ # Ensure numerical stability by clamping to [-1, 1]
189
+ return np.clip(cos_theta, -1.0, 1.0)
190
+
191
+ def calculate_g(self, cos_theta, c, d, h):
192
+ """Calculate the angular function g(theta)."""
193
+ # Ensure d is not too small to avoid division by very small numbers
194
+ d_safe = max(d, self.EPSILON)
195
+ denom = d_safe**2 + (h - cos_theta)**2
196
+
197
+ # Avoid division by zero
198
+ if denom < self.EPSILON:
199
+ denom = self.EPSILON
200
+
201
+ result = 1.0 + (c**2 / d_safe**2) - (c**2 / denom)
202
+ return self.safe_value(result)
203
+
204
+ def calculate_g_derivative(self, cos_theta, c, d, h):
205
+ """Calculate the derivative of g(theta) with respect to cos_theta."""
206
+ # Ensure d is not too small
207
+ d_safe = max(d, self.EPSILON)
208
+ term = d_safe**2 + (h - cos_theta)**2
209
+
210
+ # Avoid division by very small numbers
211
+ if term < self.EPSILON:
212
+ term = self.EPSILON
213
+
214
+ result = 2.0 * c**2 * (h - cos_theta) / (term**2)
215
+ return self.safe_value(result)
216
+
217
+ def safe_bond_order_term(self, beta, zeta, n):
218
+ """Safely calculate bond order term to avoid numerical issues."""
219
+ # Avoid issues with very small zeta values
220
+ if zeta < self.EPSILON:
221
+ return 1.0
222
+
223
+ # Limit the exponent to avoid overflow
224
+ try:
225
+ # Use logarithmic calculation for numerical stability
226
+ log_power_term = n * np.log(beta) + n * np.log(max(zeta, self.EPSILON))
227
+
228
+ # If exponent is too large, cap the result
229
+ if log_power_term > np.log(self.MAX_VALUE):
230
+ power_term = self.MAX_VALUE
231
+ else:
232
+ power_term = np.exp(log_power_term)
233
+
234
+ # Ensure we don't divide by zero
235
+ denom = 1.0 + power_term
236
+ if denom < self.EPSILON:
237
+ denom = self.EPSILON
238
+
239
+ # Apply the power with safe exponent
240
+ exponent = -1.0/(2.0*n)
241
+ if exponent < 0 and denom < self.EPSILON:
242
+ return 0.0 # Avoid division by zero for negative exponents
243
+
244
+ result = denom**exponent
245
+ return self.safe_value(result)
246
+
247
+ except (OverflowError, FloatingPointError, RuntimeWarning):
248
+ # If any numerical issues occur, return a safe default
249
+ if n > 0:
250
+ return 0.0 # Small value for positive n
251
+ else:
252
+ return 1.0 # Default for negative n
253
+
254
+ def calculate_three_body_term(self, r_ij, r_ik, lambda1):
255
+ """
256
+ Safely calculate the three-body exponential term to avoid overflow.
257
+ Uses a much more aggressive approach to prevent overflow.
258
+ """
259
+ # Calculate cubic term with tight bounds to prevent overflow
260
+ diff = r_ij - r_ik
261
+
262
+ # Very aggressive limiting to prevent overflow
263
+ # Limit diff to prevent cubic overflow
264
+ diff_limited = np.clip(diff, -2.0, 2.0)
265
+ diff_cubed = diff_limited**3
266
+
267
+ # Scale lambda to prevent overflow when cubed
268
+ lambda_scaled = min(lambda1, np.cbrt(self.MAX_EXPONENT/8.0))
269
+
270
+ # Calculate exponent with strict limits
271
+ exponent = lambda_scaled**3 * diff_cubed
272
+ exponent_limited = np.clip(exponent, -self.MAX_EXPONENT, self.MAX_EXPONENT)
273
+
274
+ # Use safe exponential
275
+ result = self.safe_exp(exponent_limited)
276
+
277
+ # Ensure result is finite and within bounds
278
+ if not np.isfinite(result):
279
+ if exponent_limited > 0:
280
+ return self.MAX_VALUE
281
+ else:
282
+ return 0.0
283
+
284
+ return min(result, self.MAX_VALUE)
285
+
286
+ def calculate_energy_and_gradient(self, coords_bohr, atom_symbols):
287
+ """
288
+ Calculates the Tersoff energy and gradient with aggressive numerical safeguards.
289
+
290
+ Args:
291
+ coords_bohr: Atomic coordinates in Bohr
292
+ atom_symbols: List of atomic symbols
293
+
294
+ Returns:
295
+ Dictionary containing energy and gradient
296
+ """
297
+ num_atoms = coords_bohr.shape[0]
298
+ if num_atoms <= 1:
299
+ return {"energy": 0.0, "gradient": np.zeros_like(coords_bohr)}
300
+
301
+ # Initialize energy and gradient
302
+ total_energy = 0.0
303
+ gradient = np.zeros_like(coords_bohr)
304
+
305
+ # Precompute all distances and direction vectors
306
+ diffs = {}
307
+ dists = {}
308
+ unit_vectors = {}
309
+
310
+ for i in range(num_atoms):
311
+ for j in range(num_atoms):
312
+ if i == j:
313
+ continue
314
+ r_ij = coords_bohr[j] - coords_bohr[i]
315
+ dist = np.linalg.norm(r_ij)
316
+ # Ensure distance is not too small
317
+ dist = max(dist, self.MIN_DISTANCE)
318
+ diffs[(i, j)] = r_ij
319
+ dists[(i, j)] = dist
320
+ unit_vectors[(i, j)] = r_ij / dist
321
+
322
+ # Main Tersoff calculation loop
323
+ for i in range(num_atoms):
324
+ for j in range(num_atoms):
325
+ if i == j:
326
+ continue
327
+
328
+ atom_i = atom_symbols[i]
329
+ atom_j = atom_symbols[j]
330
+ params = self.get_parameters(atom_i, atom_j)
331
+
332
+ A = params['A']
333
+ B = params['B']
334
+ lambda1 = params['lambda']
335
+ mu = params['mu']
336
+ beta = params['beta']
337
+ n = params['n']
338
+ c = params['c']
339
+ d = params['d']
340
+ h = params['h']
341
+ R = params['R']
342
+ D = params['D']
343
+
344
+ r_ij = dists[(i, j)]
345
+ e_ij = unit_vectors[(i, j)]
346
+
347
+ # Calculate cutoff
348
+ fc_ij = self.calculate_cutoff(r_ij, R, D)
349
+ if fc_ij < self.EPSILON:
350
+ continue # Skip if beyond cutoff
351
+
352
+ # Calculate repulsive and attractive terms with safe exponential
353
+ exp_lambda_r = self.safe_exp(-lambda1 * r_ij)
354
+ exp_mu_r = self.safe_exp(-mu * r_ij)
355
+
356
+ # Calculate bond order term
357
+ b_ij = 1.0 # Default if no neighbors
358
+ db_ij_dcos = np.zeros(num_atoms)
359
+ zeta_ij = 0.0
360
+
361
+ for k in range(num_atoms):
362
+ if k == i or k == j:
363
+ continue
364
+
365
+ atom_k = atom_symbols[k]
366
+ params_ik = self.get_parameters(atom_i, atom_k)
367
+
368
+ r_ik = dists[(i, k)]
369
+ fc_ik = self.calculate_cutoff(r_ik, params_ik['R'], params_ik['D'])
370
+
371
+ if fc_ik < self.EPSILON:
372
+ continue
373
+
374
+ cos_theta = self.calculate_bond_angle(-diffs[(i, j)], -diffs[(i, k)])
375
+ g_ijk = self.calculate_g(cos_theta, c, d, h)
376
+
377
+ # Calculate three-body term with strict overflow prevention
378
+ exp_term = self.calculate_three_body_term(r_ij, r_ik, lambda1)
379
+
380
+ # Calculate zeta term with bounded multiplication
381
+ zeta_term = 0.0
382
+ try:
383
+ # Use log arithmetic to prevent overflow
384
+ if exp_term > 0:
385
+ log_term = np.log(fc_ik) + np.log(g_ijk) + np.log(exp_term)
386
+ if log_term < np.log(self.MAX_VALUE):
387
+ zeta_term = np.exp(log_term)
388
+ else:
389
+ zeta_term = self.MAX_VALUE
390
+ except (ValueError, RuntimeWarning, OverflowError):
391
+ # If log calculation fails, try direct multiplication with safeguards
392
+ if fc_ik < 1.0 and g_ijk < self.MAX_VALUE and exp_term < self.MAX_VALUE:
393
+ # Start with smallest value and multiply cautiously
394
+ temp = fc_ik * g_ijk
395
+ if temp < self.MAX_VALUE:
396
+ zeta_term = temp * min(exp_term, self.MAX_VALUE/temp if temp > 0 else self.MAX_VALUE)
397
+
398
+ # Only add if the result is finite
399
+ if np.isfinite(zeta_term):
400
+ zeta_ij += min(zeta_term, self.MAX_VALUE - zeta_ij)
401
+
402
+ # Store derivatives for later gradient calculation - with safeguards
403
+ dg_dcos = self.calculate_g_derivative(cos_theta, c, d, h)
404
+ db_term = 0.0
405
+
406
+ # Use similar approach for derivative term
407
+ try:
408
+ if fc_ik > 0 and dg_dcos != 0 and exp_term > 0:
409
+ # Calculate using log arithmetic if possible
410
+ log_db = np.log(abs(fc_ik)) + np.log(abs(dg_dcos)) + np.log(exp_term)
411
+ if log_db < np.log(self.MAX_VALUE):
412
+ db_term = np.sign(dg_dcos) * np.exp(log_db)
413
+ else:
414
+ db_term = np.sign(dg_dcos) * self.MAX_VALUE
415
+ except (ValueError, RuntimeWarning, OverflowError):
416
+ # Fall back to cautious multiplication
417
+ if abs(fc_ik) < 1.0 and abs(dg_dcos) < self.MAX_VALUE and exp_term < self.MAX_VALUE:
418
+ temp = fc_ik * dg_dcos
419
+ if abs(temp) < self.MAX_VALUE:
420
+ db_term = temp * min(exp_term, self.MAX_VALUE/abs(temp) if abs(temp) > 0 else self.MAX_VALUE)
421
+
422
+ # Store if finite
423
+ if np.isfinite(db_term):
424
+ db_ij_dcos[k] = self.safe_value(db_term)
425
+
426
+ # Cap zeta value to prevent overflow in bond order calculation
427
+ zeta_ij = min(zeta_ij, self.MAX_VALUE)
428
+
429
+ # Safely compute bond order with zeta
430
+ b_ij = self.safe_bond_order_term(beta, zeta_ij, n)
431
+
432
+ # Calculate pair energy contribution with safeguards
433
+ repulsive = min(A * exp_lambda_r, self.MAX_VALUE)
434
+ attractive = max(-min(b_ij * B * exp_mu_r, self.MAX_VALUE), -self.MAX_VALUE)
435
+ pair_energy = fc_ij * (repulsive + attractive)
436
+
437
+ # Check for NaN or infinity in energy
438
+ if not np.isfinite(pair_energy):
439
+ pair_energy = 0.0
440
+
441
+ # Half the energy to avoid double counting
442
+ total_energy += 0.5 * pair_energy
443
+
444
+ # Calculate force components
445
+ dfc_dr = self.calculate_cutoff_derivative(r_ij, R, D)
446
+ drepulsive_dr = -lambda1 * repulsive
447
+ dattractive_dr = mu * attractive
448
+
449
+ # Direct force term (without bond-order derivatives)
450
+ direct_force = fc_ij * (drepulsive_dr + dattractive_dr) + dfc_dr * (repulsive + attractive)
451
+ direct_force = self.safe_value(direct_force)
452
+
453
+ # Apply direct forces
454
+ gradient[i] -= 0.5 * direct_force * e_ij
455
+ gradient[j] += 0.5 * direct_force * e_ij
456
+
457
+ # Bond-order derivative contributions (many-body forces)
458
+ if zeta_ij > self.EPSILON and b_ij > self.EPSILON:
459
+ # Calculate derivative of bond order term safely
460
+ try:
461
+ # Using logarithmic approach for better numerical stability
462
+ log_term = np.log(beta) * n + np.log(zeta_ij) * (n-1) + np.log(b_ij) * (1+2*n)
463
+ if log_term < np.log(self.MAX_VALUE):
464
+ db_dzeta = -0.5 * np.exp(log_term)
465
+ else:
466
+ db_dzeta = -0.5 * self.MAX_VALUE
467
+ except (ValueError, RuntimeWarning, OverflowError):
468
+ # Fall back to direct calculation with safeguards
469
+ beta_n = min(beta**n, self.MAX_VALUE)
470
+ zeta_n_1 = min(zeta_ij**(n-1), self.MAX_VALUE)
471
+ b_ij_term = min(b_ij**(1+2*n), self.MAX_VALUE)
472
+
473
+ # Apply bounds at each step
474
+ temp1 = min(beta_n * zeta_n_1, self.MAX_VALUE)
475
+ temp2 = min(temp1 * b_ij_term, self.MAX_VALUE)
476
+ db_dzeta = -0.5 * temp2
477
+
478
+ # Ensure value is finite and bounded
479
+ db_dzeta = self.safe_value(db_dzeta)
480
+
481
+ # Calculate bond force with bounds
482
+ dbond_force = self.safe_value(fc_ij * B * exp_mu_r * db_dzeta)
483
+
484
+ for k in range(num_atoms):
485
+ if k == i or k == j:
486
+ continue
487
+
488
+ atom_k = atom_symbols[k]
489
+ params_ik = self.get_parameters(atom_i, atom_k)
490
+
491
+ r_ik = dists[(i, k)]
492
+ fc_ik = self.calculate_cutoff(r_ik, params_ik['R'], params_ik['D'])
493
+
494
+ if fc_ik < self.EPSILON:
495
+ continue
496
+
497
+ cos_theta = self.calculate_bond_angle(-diffs[(i, j)], -diffs[(i, k)])
498
+ g_ijk = self.calculate_g(cos_theta, c, d, h)
499
+
500
+ # Calculate exponential term with extreme safeguards
501
+ exp_term = self.calculate_three_body_term(r_ij, r_ik, lambda1)
502
+
503
+ # Angular derivative terms with strict bounds
504
+ if dists[(i, j)] * dists[(i, k)] > self.MIN_DISTANCE:
505
+ sin_theta = np.sqrt(max(0.0, 1.0 - cos_theta**2))
506
+
507
+ if sin_theta > self.EPSILON:
508
+ # Angular forces on i, j, k with numerical safeguards
509
+ dcos_di = (e_ij / dists[(i, j)]) + (unit_vectors[(i, k)] / dists[(i, k)])
510
+ dcos_dj = -e_ij / dists[(i, j)]
511
+ dcos_dk = -unit_vectors[(i, k)] / dists[(i, k)]
512
+
513
+ # Safe calculation of angular force
514
+ angular_force = 0.0
515
+ if np.isfinite(dbond_force) and np.isfinite(db_ij_dcos[k]):
516
+ angular_force = self.safe_value(dbond_force * db_ij_dcos[k])
517
+
518
+ # Apply angular forces with bounds checks
519
+ if np.all(np.isfinite(dcos_di)) and np.isfinite(angular_force):
520
+ force_i = self.safe_value(angular_force) * dcos_di
521
+ gradient[i] -= force_i
522
+
523
+ if np.all(np.isfinite(dcos_dj)) and np.isfinite(angular_force):
524
+ force_j = self.safe_value(angular_force) * dcos_dj
525
+ gradient[j] -= force_j
526
+
527
+ if np.all(np.isfinite(dcos_dk)) and np.isfinite(angular_force):
528
+ force_k = self.safe_value(angular_force) * dcos_dk
529
+ gradient[k] -= force_k
530
+
531
+ # Radial derivative terms from three-body interactions
532
+ # Use very strict bounds for the derivative calculation
533
+ dexp_factor = 0.0
534
+ if abs(r_ij - r_ik) < 1.0: # Stricter threshold
535
+ dexp_factor = self.safe_value(3 * lambda1**3 * (r_ij - r_ik)**2)
536
+ else:
537
+ dexp_factor = self.safe_value(3 * lambda1**3 * np.sign(r_ij - r_ik))
538
+
539
+ # Calculate derivatives safely
540
+ if exp_term > 0 and exp_term < self.MAX_VALUE and dexp_factor < self.MAX_VALUE:
541
+ dexp_drij = self.safe_value(dexp_factor * exp_term)
542
+ dexp_drik = self.safe_value(-dexp_factor * exp_term)
543
+ else:
544
+ dexp_drij = 0.0
545
+ dexp_drik = 0.0
546
+
547
+ # Safe calculation of radial forces
548
+ radial_force_ij = 0.0
549
+ radial_force_ik = 0.0
550
+
551
+ # Compute forces cautiously
552
+ if dbond_force != 0.0:
553
+ # Calculate each term separately and check bounds
554
+ temp1 = self.safe_value(fc_ik * g_ijk)
555
+ if temp1 != 0.0 and dexp_drij != 0.0:
556
+ radial_force_ij = self.safe_value(dbond_force * temp1 * dexp_drij)
557
+
558
+ if temp1 != 0.0 and dexp_drik != 0.0:
559
+ radial_force_ik = self.safe_value(dbond_force * temp1 * dexp_drik)
560
+
561
+ # Apply radial forces with strict bounds checking
562
+ if np.isfinite(radial_force_ij) and abs(radial_force_ij) < self.MAX_VALUE:
563
+ gradient[i] -= radial_force_ij * e_ij
564
+ gradient[j] += radial_force_ij * e_ij
565
+
566
+ if np.isfinite(radial_force_ik) and abs(radial_force_ik) < self.MAX_VALUE:
567
+ gradient[i] -= radial_force_ik * unit_vectors[(i, k)]
568
+ gradient[k] += radial_force_ik * unit_vectors[(i, k)]
569
+
570
+ # Cutoff derivative for three-body
571
+ dfc_ik_dr = self.calculate_cutoff_derivative(r_ik, params_ik['R'], params_ik['D'])
572
+
573
+ # Safe calculation of cutoff force
574
+ cutoff_force = 0.0
575
+ if dbond_force != 0.0 and dfc_ik_dr != 0.0:
576
+ temp1 = self.safe_value(g_ijk * exp_term)
577
+ if temp1 != 0.0:
578
+ cutoff_force = self.safe_value(dbond_force * dfc_ik_dr * temp1)
579
+
580
+ # Apply cutoff forces with bounds checking
581
+ if np.isfinite(cutoff_force) and abs(cutoff_force) < self.MAX_VALUE:
582
+ gradient[i] -= cutoff_force * unit_vectors[(i, k)]
583
+ gradient[k] += cutoff_force * unit_vectors[(i, k)]
584
+
585
+ # Final check of energy and gradient for numerical stability
586
+ if not np.isfinite(total_energy):
587
+ total_energy = 0.0
588
+
589
+ # Ensure gradient is finite and within bounds
590
+ for i in range(num_atoms):
591
+ for j in range(3):
592
+ if not np.isfinite(gradient[i, j]):
593
+ gradient[i, j] = 0.0
594
+ else:
595
+ gradient[i, j] = self.safe_value(gradient[i, j])
596
+
597
+ return {"energy": total_energy, "gradient": gradient}
598
+
599
+ def calculate_hessian(self, coords_bohr, atom_symbols):
600
+ """
601
+ Calculates the Tersoff Hessian matrix using finite difference of gradients.
602
+ Uses aggressive numerical safeguards to avoid overflow issues.
603
+
604
+ Args:
605
+ coords_bohr: Atomic coordinates in Bohr
606
+ atom_symbols: List of atomic symbols
607
+
608
+ Returns:
609
+ Dictionary containing the Hessian matrix
610
+ """
611
+ print("Warning: Hessian calculation via finite difference is not tested well.")
612
+ num_atoms = coords_bohr.shape[0]
613
+ hessian = np.zeros((num_atoms * 3, num_atoms * 3))
614
+
615
+ if num_atoms <= 1:
616
+ return {"hessian": hessian}
617
+
618
+ # Compute base energy and gradient
619
+ base_results = self.calculate_energy_and_gradient(coords_bohr, atom_symbols)
620
+ base_gradient = base_results["gradient"]
621
+
622
+ # Small displacement for finite difference
623
+ delta = 1e-5
624
+
625
+ # For each degree of freedom
626
+ for i in range(num_atoms):
627
+ for j in range(3): # x, y, z
628
+ # Create displaced coordinates
629
+ displaced_coords = coords_bohr.copy()
630
+ displaced_coords[i, j] += delta
631
+
632
+ # Calculate gradient at displaced position
633
+ displaced_results = self.calculate_energy_and_gradient(displaced_coords, atom_symbols)
634
+ displaced_gradient = displaced_results["gradient"]
635
+
636
+ # Finite difference approximation of Hessian
637
+ gradient_diff = (displaced_gradient - base_gradient) / delta
638
+
639
+ # Fill in the Hessian matrix
640
+ for k in range(num_atoms):
641
+ for l in range(3):
642
+ # Ensure value is finite and bounded
643
+ hess_val = gradient_diff[k, l]
644
+ if not np.isfinite(hess_val):
645
+ hess_val = 0.0
646
+ # Cap extremely large values
647
+ hess_val = self.safe_value(hess_val)
648
+ hessian[3*i+j, 3*k+l] = hess_val
649
+
650
+ # Ensure Hessian is symmetric
651
+ hessian = 0.5 * (hessian + hessian.T)
652
+
653
+ return {"hessian": hessian}
654
+
655
+ class Calculation:
656
+ """
657
+ High-level wrapper for Tersoff calculations.
658
+ """
659
+ def __init__(self, **kwarg):
660
+ UVL = UnitValueLib()
661
+ self.bohr2angstroms = UVL.bohr2angstroms
662
+ self.atom_symbol = kwarg.get("atom_symbol", None) # Can be None initially
663
+ self.FC_COUNT = kwarg.get("FC_COUNT", -1)
664
+ self.Model_hess = kwarg.get("Model_hess")
665
+ self.hessian_flag = kwarg.get("hessian_flag", False)
666
+ self.calculator = TersoffCore()
667
+ self.energy = None
668
+ self.gradient = None
669
+ self.coordinate = None
670
+
671
+ def exact_hessian(self, element_list, positions_bohr):
672
+ """Calculates and projects the Hessian."""
673
+ results = self.calculator.calculate_hessian(positions_bohr, self.atom_symbol)
674
+ exact_hess = results['hessian']
675
+
676
+ self.Model_hess = Calculationtools().project_out_hess_tr_and_rot_for_coord(
677
+ exact_hess, element_list, positions_bohr, display_eigval=False
678
+ )
679
+
680
+ def single_point(self, file_directory, element_list, iter, electric_charge_and_multiplicity, geom_num_list=None):
681
+ """
682
+ Executes a Tersoff single point calculation, reading from a file
683
+ or using a provided geometry.
684
+ """
685
+ finish_frag = False
686
+ e, g, positions_bohr = None, None, None
687
+
688
+ try:
689
+ os.makedirs(file_directory, exist_ok=True)
690
+ except (OSError, TypeError): # TypeError if file_directory is None
691
+ pass
692
+
693
+ if file_directory is None:
694
+ file_list = ["dummy"] # To run the loop once for geom_num_list
695
+ else:
696
+ file_list = sorted(glob.glob(os.path.join(file_directory, "*_[0-9].xyz")))
697
+ if not file_list and geom_num_list is None:
698
+ raise FileNotFoundError(f"No XYZ files found in {file_directory}")
699
+
700
+ for num, input_file in enumerate(file_list):
701
+ try:
702
+ positions_angstrom = None
703
+ if geom_num_list is None:
704
+ positions_angstrom, read_elements, _ = xyz2list(input_file, electric_charge_and_multiplicity)
705
+ # **FIX**: Check if element_list is None or empty.
706
+ if element_list is None or len(element_list) == 0:
707
+ element_list = read_elements
708
+ else:
709
+ positions_angstrom = geom_num_list
710
+
711
+ if self.atom_symbol is None:
712
+ if element_list is None or len(element_list) == 0:
713
+ raise ValueError("Element list is empty. Cannot determine atom symbol.")
714
+ first_element = element_list[0]
715
+ if type(element_list[0]) is not str:
716
+ first_element = []
717
+ for i in range(len(element_list)):
718
+ first_element.append(number_element(element_list[i]))
719
+
720
+ self.atom_symbol = first_element
721
+ print(f"Atom symbol set to '{self.atom_symbol}' based on the first structure.")
722
+
723
+ positions_bohr = np.array(positions_angstrom, dtype="float64") / self.bohr2angstroms
724
+ results = self.calculator.calculate_energy_and_gradient(positions_bohr, self.atom_symbol)
725
+ e = results['energy']
726
+ g = results['gradient']
727
+
728
+ if self.FC_COUNT == -1 or isinstance(iter, str):
729
+ if self.hessian_flag:
730
+ self.exact_hessian(element_list, positions_bohr)
731
+ elif iter % self.FC_COUNT == 0 or self.hessian_flag:
732
+ self.exact_hessian(element_list, positions_bohr)
733
+
734
+ break
735
+
736
+ except Exception as error:
737
+ print(f"Error during Tersoff calculation for {input_file}: {error}")
738
+ finish_frag = True
739
+ return np.array([0]), np.array([0]), np.array([0]), finish_frag
740
+
741
+ self.energy = e
742
+ self.gradient = g
743
+ self.coordinate = positions_bohr
744
+ return e, g, positions_bohr, finish_frag
745
+
746
+ class CalculationEngine(ABC):
747
+ @abstractmethod
748
+ def calculate(self, file_directory, optimize_num, pre_total_velocity, config):
749
+ pass
750
+ def _get_file_list(self, file_directory):
751
+ return sum([sorted(glob.glob(os.path.join(file_directory, f"*_" + "[0-9]" * i + ".xyz"))) for i in range(1, 7)], [])
752
+ def _process_visualization(self, energy_list, gradient_list, num_list, optimize_num, config):
753
+ try:
754
+ if hasattr(config, 'save_pict') and config.save_pict:
755
+ visualizer = NEBVisualizer(config)
756
+ tmp_ene_list = np.array(energy_list, dtype="float64") * config.hartree2kcalmol
757
+ visualizer.plot_energy(num_list, tmp_ene_list - tmp_ene_list[0], optimize_num)
758
+ print("energy graph plotted.")
759
+ # Fix for overflow in square calculation
760
+ gradient_norm_list = []
761
+ for g in gradient_list:
762
+ if g.size > 0:
763
+ # Safely calculate gradient norm to avoid overflow
764
+ g_squared = np.square(np.clip(g, -1e3, 1e3)) # Clip before squaring
765
+ mean_squared = g_squared.mean()
766
+ gradient_norm_list.append(np.sqrt(mean_squared))
767
+ visualizer.plot_gradient(num_list, gradient_norm_list, optimize_num)
768
+ print("gradient graph plotted.")
769
+ except Exception as e:
770
+ print(f"Visualization error: {e}")
771
+
772
+ class TersoffEngine(CalculationEngine):
773
+ def __init__(self, atom_symbol=None):
774
+ super().__init__()
775
+ self.atom_symbol = atom_symbol
776
+ self.calculator = TersoffCore()
777
+ self.bohr2angstroms = UnitValueLib().bohr2angstroms
778
+
779
+ def calculate(self, file_directory, optimize_num, pre_total_velocity, config):
780
+ gradient_list, energy_list, geometry_num_list, num_list = [], [], [], []
781
+ delete_pre_total_velocity = []
782
+ os.makedirs(file_directory, exist_ok=True)
783
+ file_list = self._get_file_list(file_directory)
784
+
785
+ if not file_list:
786
+ print(f"No XYZ files found in directory: {file_directory}")
787
+ return np.array([]), np.array([]), np.array([]), pre_total_velocity
788
+
789
+ for num, input_file in enumerate(file_list):
790
+ try:
791
+ print(f"Processing file: {input_file}")
792
+ positions_angstrom, element_list, _ = xyz2list(input_file, None)
793
+
794
+ if self.atom_symbol is None:
795
+ if element_list is None or len(element_list) == 0:
796
+ raise ValueError("Element list from file is empty.")
797
+ self.atom_symbol = element_list
798
+ print(f"Engine atom symbols set based on the first file.")
799
+
800
+ positions_bohr = np.array(positions_angstrom, dtype='float64').reshape(-1, 3) / self.bohr2angstroms
801
+ results = self.calculator.calculate_energy_and_gradient(positions_bohr, self.atom_symbol)
802
+
803
+ energy_list.append(results['energy'])
804
+ gradient_list.append(results['gradient'])
805
+ geometry_num_list.append(positions_angstrom)
806
+ num_list.append(num)
807
+ except Exception as error:
808
+ print(f"Error processing {input_file}: {error}")
809
+ if optimize_num != 0:
810
+ delete_pre_total_velocity.append(num)
811
+
812
+ self._process_visualization(energy_list, gradient_list, num_list, optimize_num, config)
813
+ if optimize_num != 0 and len(pre_total_velocity) > 0 and delete_pre_total_velocity:
814
+ pre_total_velocity = np.delete(np.array(pre_total_velocity), delete_pre_total_velocity, axis=0)
815
+ return (np.array(energy_list, dtype='float64'),
816
+ np.array(gradient_list, dtype='float64'),
817
+ np.array(geometry_num_list, dtype='float64'),
818
+ pre_total_velocity)