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,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)
|