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,842 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy import linalg
|
|
3
|
+
from multioptpy.Parameters.parameter import UnitValueLib, covalent_radii_lib
|
|
4
|
+
from .hessian_update import ModelHessianUpdate
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Redundant Internal Coordinate RFO implementation
|
|
8
|
+
Implementation without dihedral angles in primitive coordinates
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
class RedundantInternalRFO:
|
|
12
|
+
def __init__(self, **config):
|
|
13
|
+
self.config = config
|
|
14
|
+
self.hess_update = ModelHessianUpdate()
|
|
15
|
+
|
|
16
|
+
# Initialize RIC specific parameters
|
|
17
|
+
self.Initialization = True
|
|
18
|
+
self.saddle_order = self.config.get("saddle_order", 0)
|
|
19
|
+
self.trust_radius = float(self.config.get("trust_radius", 0.1))
|
|
20
|
+
self.trust_radius /= UnitValueLib().bohr2angstroms
|
|
21
|
+
self.DELTA = 0.1 # Step scale factor
|
|
22
|
+
|
|
23
|
+
# For internal coordinate handling
|
|
24
|
+
self.B_matrix = None # Wilson B matrix (transformation matrix)
|
|
25
|
+
self.G_matrix = None # G matrix (metric matrix)
|
|
26
|
+
self.primitive_coords = None
|
|
27
|
+
|
|
28
|
+
# RIC specific parameters
|
|
29
|
+
self.redundant_thresh = 1e-5 # Threshold for numerical stability
|
|
30
|
+
self.coord_type = self.config.get("coord_type", "redundant")
|
|
31
|
+
self.iter = 0
|
|
32
|
+
self.max_backsteps = 200
|
|
33
|
+
self.max_micro_cycles = 1000
|
|
34
|
+
self.FC_COUNT = self.config.get("FC_COUNT", -1)
|
|
35
|
+
self.backconv_method = self.config.get("backconv_method", "scf").lower()
|
|
36
|
+
|
|
37
|
+
# Tracking variables
|
|
38
|
+
self.prev_cartesian = None
|
|
39
|
+
self.prev_gradient = None
|
|
40
|
+
self.internal_hessian = None
|
|
41
|
+
self.hessian = None
|
|
42
|
+
self.bias_hessian = None
|
|
43
|
+
self.prev_connectivity = None
|
|
44
|
+
|
|
45
|
+
self.element_list = self.config.get("element_list", None)
|
|
46
|
+
|
|
47
|
+
def define_internal_coordinates(self, geometry):
|
|
48
|
+
"""
|
|
49
|
+
Define primitive internal coordinates based on molecular connectivity
|
|
50
|
+
Returns the connectivity table and primitive internal coordinates
|
|
51
|
+
Using only bonds and angles (no dihedrals as requested)
|
|
52
|
+
"""
|
|
53
|
+
atomic_numbers = self.element_list
|
|
54
|
+
# Convert geometry from (3N, 1) to (N, 3) format
|
|
55
|
+
natoms = len(atomic_numbers)
|
|
56
|
+
geom_reshaped = geometry.reshape(natoms, 3)
|
|
57
|
+
|
|
58
|
+
primitive_coords = []
|
|
59
|
+
connectivity = np.zeros((natoms, natoms), dtype=int)
|
|
60
|
+
|
|
61
|
+
# Build connectivity based on covalent radii
|
|
62
|
+
for i in range(natoms):
|
|
63
|
+
for j in range(i+1, natoms):
|
|
64
|
+
# Calculate distance between atoms
|
|
65
|
+
dist = np.linalg.norm(geom_reshaped[i] - geom_reshaped[j])
|
|
66
|
+
|
|
67
|
+
# Get covalent radii based on atomic numbers
|
|
68
|
+
r_i = covalent_radii_lib(atomic_numbers[i])
|
|
69
|
+
r_j = covalent_radii_lib(atomic_numbers[j])
|
|
70
|
+
|
|
71
|
+
# Check if atoms are bonded (using a bond tolerance factor of 1.2)
|
|
72
|
+
if dist < 1.2 * (r_i + r_j):
|
|
73
|
+
connectivity[i, j] = 1
|
|
74
|
+
connectivity[j, i] = 1
|
|
75
|
+
|
|
76
|
+
# Add bond as primitive coordinate
|
|
77
|
+
primitive_coords.append({
|
|
78
|
+
'type': 'bond',
|
|
79
|
+
'atoms': [i, j]
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
# Add angle coordinates
|
|
83
|
+
for j in range(natoms):
|
|
84
|
+
bonded_to_j = np.where(connectivity[j] > 0)[0]
|
|
85
|
+
for i in bonded_to_j:
|
|
86
|
+
for k in bonded_to_j:
|
|
87
|
+
if i < k: # Avoid duplicates
|
|
88
|
+
primitive_coords.append({
|
|
89
|
+
'type': 'angle',
|
|
90
|
+
'atoms': [i, j, k]
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
# Note: As requested, no dihedral angles are added
|
|
94
|
+
|
|
95
|
+
self.primitive_coords = primitive_coords
|
|
96
|
+
return connectivity, primitive_coords
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def calc_center(self, geometry):
|
|
101
|
+
"""Calculate center of mass of the geometry"""
|
|
102
|
+
n_atoms = len(geometry) // 3
|
|
103
|
+
return np.mean(geometry.reshape(n_atoms, 3), axis=0)
|
|
104
|
+
|
|
105
|
+
def build_B_matrix(self, geometry, primitive_coords=None):
|
|
106
|
+
"""
|
|
107
|
+
Build Wilson B matrix (∂q/∂x) for coordinate transformation
|
|
108
|
+
q: internal coordinates
|
|
109
|
+
x: Cartesian coordinates
|
|
110
|
+
|
|
111
|
+
The geometry is expected in (3N, 1) format
|
|
112
|
+
"""
|
|
113
|
+
if primitive_coords is None:
|
|
114
|
+
primitive_coords = self.primitive_coords
|
|
115
|
+
|
|
116
|
+
natoms = len(geometry) // 3
|
|
117
|
+
geom_reshaped = geometry.reshape(natoms, 3)
|
|
118
|
+
ncoords = len(primitive_coords)
|
|
119
|
+
|
|
120
|
+
B = np.zeros((ncoords, 3*natoms))
|
|
121
|
+
|
|
122
|
+
for i, coord in enumerate(primitive_coords):
|
|
123
|
+
if coord['type'] == 'bond':
|
|
124
|
+
a1, a2 = coord['atoms']
|
|
125
|
+
B[i] = self._bond_B_elements(geom_reshaped, a1, a2)
|
|
126
|
+
elif coord['type'] == 'angle':
|
|
127
|
+
a1, a2, a3 = coord['atoms']
|
|
128
|
+
B[i] = self._angle_B_elements(geom_reshaped, a1, a2, a3)
|
|
129
|
+
|
|
130
|
+
return B
|
|
131
|
+
|
|
132
|
+
def _bond_B_elements(self, geometry, a1, a2):
|
|
133
|
+
"""Calculate B matrix elements for bond stretching"""
|
|
134
|
+
r1 = geometry[a1]
|
|
135
|
+
r2 = geometry[a2]
|
|
136
|
+
|
|
137
|
+
# Vector pointing from atom a1 to atom a2
|
|
138
|
+
vec = r2 - r1
|
|
139
|
+
|
|
140
|
+
# Distance between the atoms
|
|
141
|
+
distance = np.linalg.norm(vec)
|
|
142
|
+
|
|
143
|
+
if distance < 1e-10:
|
|
144
|
+
unit_vec = np.zeros(3)
|
|
145
|
+
else:
|
|
146
|
+
unit_vec = vec / distance
|
|
147
|
+
|
|
148
|
+
# B matrix elements (derivatives of the bond distance w.r.t. Cartesian coordinates)
|
|
149
|
+
B_elements = np.zeros(3 * len(geometry))
|
|
150
|
+
|
|
151
|
+
# For atom a1
|
|
152
|
+
B_elements[3*a1:3*a1+3] = -unit_vec
|
|
153
|
+
|
|
154
|
+
# For atom a2
|
|
155
|
+
B_elements[3*a2:3*a2+3] = unit_vec
|
|
156
|
+
|
|
157
|
+
return B_elements
|
|
158
|
+
|
|
159
|
+
def _angle_B_elements(self, geometry, a1, a2, a3):
|
|
160
|
+
"""Calculate B matrix elements for angle bending"""
|
|
161
|
+
r1 = geometry[a1]
|
|
162
|
+
r2 = geometry[a2]
|
|
163
|
+
r3 = geometry[a3]
|
|
164
|
+
|
|
165
|
+
# Vectors from central atom to the other two atoms
|
|
166
|
+
v1 = r1 - r2
|
|
167
|
+
v3 = r3 - r2
|
|
168
|
+
|
|
169
|
+
# Normalize vectors
|
|
170
|
+
d1 = np.linalg.norm(v1)
|
|
171
|
+
d3 = np.linalg.norm(v3)
|
|
172
|
+
|
|
173
|
+
if d1 < 1e-10 or d3 < 1e-10:
|
|
174
|
+
return np.zeros(3 * len(geometry))
|
|
175
|
+
|
|
176
|
+
v1_normalized = v1 / d1
|
|
177
|
+
v3_normalized = v3 / d3
|
|
178
|
+
|
|
179
|
+
# Cosine of the angle
|
|
180
|
+
cos_angle = np.dot(v1_normalized, v3_normalized)
|
|
181
|
+
|
|
182
|
+
# Ensure numerical stability
|
|
183
|
+
if cos_angle > 1.0:
|
|
184
|
+
cos_angle = 1.0
|
|
185
|
+
elif cos_angle < -1.0:
|
|
186
|
+
cos_angle = -1.0
|
|
187
|
+
|
|
188
|
+
sin_angle = np.sqrt(1.0 - cos_angle**2)
|
|
189
|
+
|
|
190
|
+
if sin_angle < 1e-10:
|
|
191
|
+
return np.zeros(3 * len(geometry))
|
|
192
|
+
|
|
193
|
+
# Calculate derivatives
|
|
194
|
+
term1 = 1.0 / (d1 * sin_angle)
|
|
195
|
+
term3 = 1.0 / (d3 * sin_angle)
|
|
196
|
+
|
|
197
|
+
# Cross products
|
|
198
|
+
cp1 = np.cross(v1_normalized, v3_normalized)
|
|
199
|
+
cp3 = np.cross(v3_normalized, v1_normalized)
|
|
200
|
+
|
|
201
|
+
# B matrix elements
|
|
202
|
+
B_elements = np.zeros(3 * len(geometry))
|
|
203
|
+
|
|
204
|
+
# For atom a1
|
|
205
|
+
B_elements[3*a1:3*a1+3] = term1 * cp1
|
|
206
|
+
|
|
207
|
+
# For atom a3
|
|
208
|
+
B_elements[3*a3:3*a3+3] = term3 * cp3
|
|
209
|
+
|
|
210
|
+
# For central atom a2
|
|
211
|
+
B_elements[3*a2:3*a2+3] = -(B_elements[3*a1:3*a1+3] + B_elements[3*a3:3*a3+3])
|
|
212
|
+
|
|
213
|
+
return B_elements
|
|
214
|
+
|
|
215
|
+
def build_G_matrix(self, B_matrix):
|
|
216
|
+
"""
|
|
217
|
+
Build G matrix (metric matrix) from Wilson B matrix
|
|
218
|
+
G = B·B^T
|
|
219
|
+
"""
|
|
220
|
+
G = np.dot(B_matrix, B_matrix.T)
|
|
221
|
+
return G
|
|
222
|
+
|
|
223
|
+
def check_connectivity_change(self, geometry):
|
|
224
|
+
"""
|
|
225
|
+
Check if the molecular connectivity has changed significantly
|
|
226
|
+
"""
|
|
227
|
+
if self.prev_connectivity is None:
|
|
228
|
+
# First call, just store the current connectivity
|
|
229
|
+
connectivity, _ = self.define_internal_coordinates(geometry)
|
|
230
|
+
self.prev_connectivity = connectivity
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
# Check current connectivity
|
|
234
|
+
current_connectivity, _ = self.define_internal_coordinates(geometry)
|
|
235
|
+
|
|
236
|
+
# Compare with previous connectivity matrix
|
|
237
|
+
diff = np.sum(np.abs(current_connectivity - self.prev_connectivity))
|
|
238
|
+
|
|
239
|
+
# If there are changes in connectivity, reset coordinate system
|
|
240
|
+
if diff > 0:
|
|
241
|
+
print(f"WARNING: Detected {diff} changes in molecular connectivity")
|
|
242
|
+
print("Resetting internal coordinates due to bond rearrangement")
|
|
243
|
+
self.prev_connectivity = current_connectivity
|
|
244
|
+
return True
|
|
245
|
+
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
def update_coordinates(self, geometry):
|
|
249
|
+
"""
|
|
250
|
+
Update the internal coordinates for the current geometry
|
|
251
|
+
"""
|
|
252
|
+
# Build B matrix for current geometry
|
|
253
|
+
B = self.build_B_matrix(geometry)
|
|
254
|
+
self.B_matrix = B
|
|
255
|
+
|
|
256
|
+
# Build G matrix
|
|
257
|
+
G = self.build_G_matrix(B)
|
|
258
|
+
self.G_matrix = G
|
|
259
|
+
|
|
260
|
+
return B
|
|
261
|
+
|
|
262
|
+
def transform_gradient(self, cart_gradient):
|
|
263
|
+
"""
|
|
264
|
+
Transform Cartesian gradient to internal coordinate gradient
|
|
265
|
+
g_int = B·g_cart (direct transformation using Wilson B matrix)
|
|
266
|
+
"""
|
|
267
|
+
B = self.B_matrix
|
|
268
|
+
|
|
269
|
+
# Direct transformation using B matrix
|
|
270
|
+
int_gradient = np.dot(B, cart_gradient)
|
|
271
|
+
return int_gradient
|
|
272
|
+
|
|
273
|
+
def transform_hessian(self, cart_hessian):
|
|
274
|
+
"""
|
|
275
|
+
Transform Cartesian Hessian to internal coordinate Hessian
|
|
276
|
+
H_int = B·H_cart·B^T (direct transformation)
|
|
277
|
+
"""
|
|
278
|
+
B = self.B_matrix
|
|
279
|
+
|
|
280
|
+
# Direct transformation
|
|
281
|
+
int_hessian = np.dot(B, np.dot(cart_hessian, B.T))
|
|
282
|
+
return int_hessian
|
|
283
|
+
|
|
284
|
+
def internal_to_cartesian_scf(self, step_int, geometry,
|
|
285
|
+
max_iterations=100,
|
|
286
|
+
convergence_threshold=1e-8):
|
|
287
|
+
"""
|
|
288
|
+
SCF-inspired algorithm for internal to Cartesian coordinate conversion.
|
|
289
|
+
Uses techniques from electronic structure theory: line search when far from
|
|
290
|
+
convergence and Newton-Raphson near convergence, with DIIS acceleration.
|
|
291
|
+
|
|
292
|
+
Parameters:
|
|
293
|
+
-----------
|
|
294
|
+
step_int : ndarray
|
|
295
|
+
Target step in internal coordinates
|
|
296
|
+
geometry : ndarray
|
|
297
|
+
Starting geometry in Cartesian coordinates
|
|
298
|
+
max_iterations : int
|
|
299
|
+
Maximum number of iterations
|
|
300
|
+
convergence_threshold : float
|
|
301
|
+
Convergence criterion for RMS error
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
--------
|
|
305
|
+
ndarray
|
|
306
|
+
Step in Cartesian coordinates
|
|
307
|
+
"""
|
|
308
|
+
# Initialize variables
|
|
309
|
+
reference_geom = geometry.copy()
|
|
310
|
+
current_geom = geometry.copy()
|
|
311
|
+
best_geom = current_geom.copy()
|
|
312
|
+
best_error = float('inf')
|
|
313
|
+
|
|
314
|
+
# DIIS parameters
|
|
315
|
+
diis_start = 3
|
|
316
|
+
diis_max = 8
|
|
317
|
+
diis_errors = []
|
|
318
|
+
diis_geometries = []
|
|
319
|
+
|
|
320
|
+
# Dynamic algorithm control
|
|
321
|
+
use_newton = False # Start with line search, switch to Newton later
|
|
322
|
+
damping_factor = 0.7 # Initial damping
|
|
323
|
+
level_shift = 0.0 # Level shifting parameter
|
|
324
|
+
|
|
325
|
+
# Last iteration data for backtracks
|
|
326
|
+
prev_error = float('inf')
|
|
327
|
+
prev_geom = None
|
|
328
|
+
|
|
329
|
+
# Calculate initial values
|
|
330
|
+
B = self.build_B_matrix(current_geom)
|
|
331
|
+
current_q = np.dot(B, current_geom - reference_geom)
|
|
332
|
+
delta_q = step_int - current_q
|
|
333
|
+
error = np.linalg.norm(delta_q)
|
|
334
|
+
rms_error = error / np.sqrt(len(delta_q))
|
|
335
|
+
|
|
336
|
+
print(f"Starting SCF-like optimization: initial RMS error = {rms_error:.3e}")
|
|
337
|
+
|
|
338
|
+
# Main iteration loop
|
|
339
|
+
for iteration in range(max_iterations):
|
|
340
|
+
# Save best solution
|
|
341
|
+
if error < best_error:
|
|
342
|
+
best_error = error
|
|
343
|
+
best_geom = current_geom.copy()
|
|
344
|
+
|
|
345
|
+
# Report progress
|
|
346
|
+
if iteration % 20 == 0 or iteration < 2:
|
|
347
|
+
print(f"Iteration {iteration}: RMS error = {rms_error:.3e}, "
|
|
348
|
+
f"{'Newton' if use_newton else 'LineSearch'}, damping={damping_factor:.2f}")
|
|
349
|
+
|
|
350
|
+
# Check convergence
|
|
351
|
+
if rms_error < convergence_threshold:
|
|
352
|
+
print(f"Conversion converged in {iteration+1} iterations")
|
|
353
|
+
break
|
|
354
|
+
|
|
355
|
+
# Prepare for this iteration
|
|
356
|
+
B = self.build_B_matrix(current_geom)
|
|
357
|
+
|
|
358
|
+
# Calculate step based on current method
|
|
359
|
+
if use_newton:
|
|
360
|
+
# Calculate B+ (pseudoinverse of B) using SVD with level shifting
|
|
361
|
+
try:
|
|
362
|
+
U, s, Vh = np.linalg.svd(B, full_matrices=False)
|
|
363
|
+
|
|
364
|
+
# Apply level shifting to singular values (similar to SCF level shifting)
|
|
365
|
+
s_inv = np.where(s > 1e-7, 1.0 / (s + level_shift), 0.0)
|
|
366
|
+
|
|
367
|
+
B_pinv = np.dot(Vh.T * s_inv, U.T)
|
|
368
|
+
|
|
369
|
+
# Calculate Newton step
|
|
370
|
+
step = np.dot(B_pinv, delta_q)
|
|
371
|
+
except np.linalg.LinAlgError:
|
|
372
|
+
# Fall back to a more stable approach
|
|
373
|
+
print("SVD failed, using pinv with increased regularization")
|
|
374
|
+
B_pinv = np.linalg.pinv(B, rcond=1e-6)
|
|
375
|
+
step = np.dot(B_pinv, delta_q)
|
|
376
|
+
else:
|
|
377
|
+
# Line search along steepest descent direction
|
|
378
|
+
gradient = np.dot(B.T, delta_q)
|
|
379
|
+
grad_norm = np.linalg.norm(gradient)
|
|
380
|
+
|
|
381
|
+
if grad_norm < 1e-10:
|
|
382
|
+
# Gradient too small, try Newton step
|
|
383
|
+
use_newton = True
|
|
384
|
+
B_pinv = np.linalg.pinv(B, rcond=1e-7)
|
|
385
|
+
step = np.dot(B_pinv, delta_q)
|
|
386
|
+
else:
|
|
387
|
+
# Normalize gradient and scale by dynamic step size
|
|
388
|
+
step_dir = gradient / grad_norm
|
|
389
|
+
|
|
390
|
+
# Determine step size (larger when further from convergence)
|
|
391
|
+
# Similar to trust radius in SCF
|
|
392
|
+
step_size = min(0.2, 0.1 * (1.0 + 10.0 * rms_error))
|
|
393
|
+
|
|
394
|
+
step = step_size * step_dir
|
|
395
|
+
|
|
396
|
+
# Apply DIIS acceleration if we have enough iterations
|
|
397
|
+
diis_step = None
|
|
398
|
+
if iteration >= diis_start:
|
|
399
|
+
# Store current error and geometry for DIIS
|
|
400
|
+
diis_errors.append(delta_q.flatten())
|
|
401
|
+
diis_geometries.append(current_geom.copy())
|
|
402
|
+
|
|
403
|
+
# Limit DIIS vector storage
|
|
404
|
+
if len(diis_errors) > diis_max:
|
|
405
|
+
diis_errors.pop(0)
|
|
406
|
+
diis_geometries.pop(0)
|
|
407
|
+
|
|
408
|
+
# Apply DIIS if we have at least 2 vectors
|
|
409
|
+
if len(diis_errors) >= 2:
|
|
410
|
+
try:
|
|
411
|
+
diis_step = self._compute_diis_solution(diis_errors, diis_geometries)
|
|
412
|
+
except Exception as e:
|
|
413
|
+
print(f"DIIS failed: {e}")
|
|
414
|
+
diis_step = None
|
|
415
|
+
|
|
416
|
+
# Try step with current damping
|
|
417
|
+
damped_step = step * damping_factor
|
|
418
|
+
trial_geom = current_geom + damped_step
|
|
419
|
+
|
|
420
|
+
# Evaluate trial geometry
|
|
421
|
+
trial_B = self.build_B_matrix(trial_geom)
|
|
422
|
+
trial_q = np.dot(trial_B, trial_geom - reference_geom)
|
|
423
|
+
trial_delta_q = step_int - trial_q
|
|
424
|
+
trial_error = np.linalg.norm(trial_delta_q)
|
|
425
|
+
trial_rms = trial_error / np.sqrt(len(trial_delta_q))
|
|
426
|
+
|
|
427
|
+
# If we have a DIIS solution, evaluate it too
|
|
428
|
+
if diis_step is not None:
|
|
429
|
+
diis_B = self.build_B_matrix(diis_step)
|
|
430
|
+
diis_q = np.dot(diis_B, diis_step - reference_geom)
|
|
431
|
+
diis_delta_q = step_int - diis_q
|
|
432
|
+
diis_error = np.linalg.norm(diis_delta_q)
|
|
433
|
+
diis_rms = diis_error / np.sqrt(len(diis_delta_q))
|
|
434
|
+
|
|
435
|
+
# Use DIIS if it's better
|
|
436
|
+
if diis_error < trial_error and diis_error < error:
|
|
437
|
+
#print(f"DIIS improvement: {trial_rms:.3e} -> {diis_rms:.3e}")
|
|
438
|
+
current_geom = diis_step
|
|
439
|
+
delta_q = diis_delta_q
|
|
440
|
+
error = diis_error
|
|
441
|
+
rms_error = diis_rms
|
|
442
|
+
|
|
443
|
+
# DIIS was successful, try to reduce level shifting
|
|
444
|
+
if level_shift > 0:
|
|
445
|
+
level_shift = max(0, level_shift * 0.5)
|
|
446
|
+
continue # Skip to next iteration
|
|
447
|
+
|
|
448
|
+
# Dynamically adjust the algorithm based on progress
|
|
449
|
+
if trial_error < error:
|
|
450
|
+
# Step is good, accept it
|
|
451
|
+
current_geom = trial_geom
|
|
452
|
+
delta_q = trial_delta_q
|
|
453
|
+
prev_error = error
|
|
454
|
+
error = trial_error
|
|
455
|
+
rms_error = trial_rms
|
|
456
|
+
|
|
457
|
+
# Increase damping for more aggressive steps
|
|
458
|
+
damping_factor = min(1.0, damping_factor * 1.2)
|
|
459
|
+
|
|
460
|
+
# Switch to Newton method when close enough to solution
|
|
461
|
+
if not use_newton and rms_error < 0.1:
|
|
462
|
+
#print(f"Switching to Newton-Raphson at iteration {iteration+1}")
|
|
463
|
+
use_newton = True
|
|
464
|
+
|
|
465
|
+
# Reduce level shifting since things are going well
|
|
466
|
+
if level_shift > 0:
|
|
467
|
+
level_shift = max(0, level_shift * 0.5)
|
|
468
|
+
|
|
469
|
+
else:
|
|
470
|
+
# Step is bad, adjust strategy
|
|
471
|
+
if use_newton:
|
|
472
|
+
# Newton step made things worse
|
|
473
|
+
if level_shift == 0:
|
|
474
|
+
# Start with modest level shifting
|
|
475
|
+
level_shift = 0.1
|
|
476
|
+
else:
|
|
477
|
+
# Increase level shifting (similar to SCF when oscillating)
|
|
478
|
+
level_shift = min(1.0, level_shift * 2.0)
|
|
479
|
+
|
|
480
|
+
#print(f"Increasing level shift to {level_shift:.3e}")
|
|
481
|
+
|
|
482
|
+
# If level shift is getting too high, try line search
|
|
483
|
+
if level_shift > 0.5:
|
|
484
|
+
#print("Switching to line search due to unstable Newton steps")
|
|
485
|
+
use_newton = False
|
|
486
|
+
|
|
487
|
+
# Reduce damping for more conservative steps
|
|
488
|
+
damping_factor = max(0.2, damping_factor * 0.5)
|
|
489
|
+
|
|
490
|
+
# If we have previous good geometry, backtrack halfway
|
|
491
|
+
if prev_geom is not None:
|
|
492
|
+
#print(f"Backtracking: {error:.3e} -> {prev_error:.3e}")
|
|
493
|
+
current_geom = 0.5 * (current_geom + prev_geom)
|
|
494
|
+
|
|
495
|
+
# Recalculate at backtracked position
|
|
496
|
+
B = self.build_B_matrix(current_geom)
|
|
497
|
+
current_q = np.dot(B, current_geom - reference_geom)
|
|
498
|
+
delta_q = step_int - current_q
|
|
499
|
+
error = np.linalg.norm(delta_q)
|
|
500
|
+
rms_error = error / np.sqrt(len(delta_q))
|
|
501
|
+
|
|
502
|
+
# Save current position for potential backtracking
|
|
503
|
+
prev_geom = current_geom.copy()
|
|
504
|
+
|
|
505
|
+
# Handle non-convergence
|
|
506
|
+
if iteration == max_iterations - 1 and rms_error > convergence_threshold:
|
|
507
|
+
print(f"Warning: Conversion did not converge. Best RMS error = {best_error/np.sqrt(len(step_int)):.3e}")
|
|
508
|
+
current_geom = best_geom
|
|
509
|
+
|
|
510
|
+
# Return the Cartesian step
|
|
511
|
+
return current_geom - reference_geom
|
|
512
|
+
|
|
513
|
+
def _compute_diis_solution(self, error_vectors, geometries):
|
|
514
|
+
"""
|
|
515
|
+
Compute DIIS extrapolated solution using Pulay's method
|
|
516
|
+
|
|
517
|
+
Parameters:
|
|
518
|
+
-----------
|
|
519
|
+
error_vectors : list
|
|
520
|
+
List of error vectors (flattened)
|
|
521
|
+
geometries : list
|
|
522
|
+
List of corresponding geometries
|
|
523
|
+
|
|
524
|
+
Returns:
|
|
525
|
+
--------
|
|
526
|
+
ndarray
|
|
527
|
+
DIIS extrapolated geometry
|
|
528
|
+
"""
|
|
529
|
+
n_vecs = len(error_vectors)
|
|
530
|
+
|
|
531
|
+
# Build DIIS B matrix for Pulay method
|
|
532
|
+
B_diis = np.zeros((n_vecs + 1, n_vecs + 1))
|
|
533
|
+
|
|
534
|
+
# Fill error vector dot products
|
|
535
|
+
for i in range(n_vecs):
|
|
536
|
+
for j in range(n_vecs):
|
|
537
|
+
B_diis[i, j] = np.dot(error_vectors[i], error_vectors[j])
|
|
538
|
+
|
|
539
|
+
# Add constraint rows/columns
|
|
540
|
+
B_diis[n_vecs, :n_vecs] = 1.0
|
|
541
|
+
B_diis[:n_vecs, n_vecs] = 1.0
|
|
542
|
+
B_diis[n_vecs, n_vecs] = 0.0
|
|
543
|
+
|
|
544
|
+
# RHS vector [0,0,...,0,1]
|
|
545
|
+
rhs = np.zeros(n_vecs + 1)
|
|
546
|
+
rhs[n_vecs] = 1.0
|
|
547
|
+
|
|
548
|
+
# Add small regularization to diagonal for numerical stability
|
|
549
|
+
for i in range(n_vecs):
|
|
550
|
+
B_diis[i, i] += 1e-8 * (1.0 + abs(B_diis[i, i]))
|
|
551
|
+
|
|
552
|
+
# Solve DIIS equations
|
|
553
|
+
try:
|
|
554
|
+
c = np.linalg.solve(B_diis, rhs)
|
|
555
|
+
except np.linalg.LinAlgError:
|
|
556
|
+
# Use SVD-based solution if direct solve fails
|
|
557
|
+
c = np.linalg.lstsq(B_diis, rhs, rcond=1e-10)[0]
|
|
558
|
+
|
|
559
|
+
# Construct DIIS solution
|
|
560
|
+
diis_geom = np.zeros_like(geometries[0])
|
|
561
|
+
for i in range(n_vecs):
|
|
562
|
+
diis_geom += c[i] * geometries[i]
|
|
563
|
+
|
|
564
|
+
return diis_geom
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _evaluate_error(self, geom, reference_geom, target_step):
|
|
568
|
+
"""
|
|
569
|
+
Helper function to evaluate the error for a given geometry
|
|
570
|
+
"""
|
|
571
|
+
B = self.build_B_matrix(geom)
|
|
572
|
+
current_q = np.dot(B, geom - reference_geom)
|
|
573
|
+
delta_q = target_step - current_q
|
|
574
|
+
return np.linalg.norm(delta_q)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def internal_to_cartesian(self, step_int, geometry):
|
|
578
|
+
"""
|
|
579
|
+
Transform step in internal coordinates to Cartesian coordinates
|
|
580
|
+
using enhanced iterative methods
|
|
581
|
+
"""
|
|
582
|
+
# Choose the algorithm based on configuration or problem characteristics
|
|
583
|
+
|
|
584
|
+
if self.backconv_method == "scf":
|
|
585
|
+
return self.internal_to_cartesian_scf(step_int, geometry)
|
|
586
|
+
else:
|
|
587
|
+
return self.internal_to_cartesian_scf(step_int, geometry)
|
|
588
|
+
|
|
589
|
+
def get_cleaned_hessian(self, hessian):
|
|
590
|
+
"""Ensure the Hessian is clean and well-conditioned"""
|
|
591
|
+
# Ensure symmetry
|
|
592
|
+
hessian = 0.5 * (hessian + hessian.T)
|
|
593
|
+
|
|
594
|
+
try:
|
|
595
|
+
# Use more stable eigenvalue decomposition
|
|
596
|
+
eigval, eigvec = linalg.eigh(hessian)
|
|
597
|
+
except np.linalg.LinAlgError:
|
|
598
|
+
# Fallback to more robust algorithm if standard fails
|
|
599
|
+
print("Warning: Using more robust eigenvalue decomposition")
|
|
600
|
+
eigval, eigvec = linalg.eigh(hessian, driver='evr')
|
|
601
|
+
|
|
602
|
+
# Find valid eigenvalues (|λ| > 1e-7)
|
|
603
|
+
valid_mask = np.abs(eigval) > 1e-7
|
|
604
|
+
n_removed = np.sum(~valid_mask)
|
|
605
|
+
|
|
606
|
+
# Create diagonal matrix with only valid eigenvalues
|
|
607
|
+
# Replace small eigenvalues with small positive values
|
|
608
|
+
cleaned_eigval = np.where(valid_mask, eigval, 1e-7)
|
|
609
|
+
|
|
610
|
+
# Reconstruct Hessian using only valid components
|
|
611
|
+
# H = U Λ U^T where Λ contains only valid eigenvalues
|
|
612
|
+
cleaned_hessian = np.dot(np.dot(eigvec, np.diag(cleaned_eigval)), eigvec.T)
|
|
613
|
+
|
|
614
|
+
# Ensure symmetry of final result
|
|
615
|
+
cleaned_hessian = 0.5 * (cleaned_hessian + cleaned_hessian.T)
|
|
616
|
+
|
|
617
|
+
return cleaned_hessian, n_removed
|
|
618
|
+
|
|
619
|
+
def run_rfo_step(self, int_gradient, int_hessian):
|
|
620
|
+
"""
|
|
621
|
+
Calculate the RFO step in internal coordinates with improved stability
|
|
622
|
+
"""
|
|
623
|
+
n_coords = len(int_gradient)
|
|
624
|
+
|
|
625
|
+
# Ensure symmetry and clean the Hessian
|
|
626
|
+
new_hess = 0.5 * (int_hessian + int_hessian.T)
|
|
627
|
+
new_hess, _ = self.get_cleaned_hessian(new_hess)
|
|
628
|
+
|
|
629
|
+
# Construct RFO matrix
|
|
630
|
+
matrix_for_RFO = np.block([
|
|
631
|
+
[new_hess, int_gradient.reshape(n_coords, 1)],
|
|
632
|
+
[int_gradient.reshape(1, n_coords), np.zeros((1, 1))]
|
|
633
|
+
])
|
|
634
|
+
|
|
635
|
+
# Get eigenvalues of the RFO matrix
|
|
636
|
+
try:
|
|
637
|
+
RFO_eigenvalues, RFO_eigenvectors = linalg.eigh(matrix_for_RFO)
|
|
638
|
+
except np.linalg.LinAlgError:
|
|
639
|
+
print("Warning: Using more robust eigenvalue algorithm")
|
|
640
|
+
RFO_eigenvalues, RFO_eigenvectors = linalg.eigh(matrix_for_RFO, driver='evr')
|
|
641
|
+
|
|
642
|
+
# Sort eigenvalues
|
|
643
|
+
idx = np.argsort(RFO_eigenvalues)
|
|
644
|
+
RFO_eigenvalues = RFO_eigenvalues[idx]
|
|
645
|
+
RFO_eigenvectors = RFO_eigenvectors[:, idx]
|
|
646
|
+
|
|
647
|
+
# Select appropriate eigenvalue based on saddle order
|
|
648
|
+
lambda_for_calc = float(RFO_eigenvalues[self.saddle_order])
|
|
649
|
+
|
|
650
|
+
# Calculate step using direct RFO approach
|
|
651
|
+
shifted_hessian = new_hess - lambda_for_calc * np.eye(n_coords)
|
|
652
|
+
|
|
653
|
+
# Solve the RFO equations
|
|
654
|
+
try:
|
|
655
|
+
# LU decomposition for stable solving
|
|
656
|
+
move_vector = -np.linalg.solve(shifted_hessian, int_gradient)
|
|
657
|
+
except np.linalg.LinAlgError:
|
|
658
|
+
print("Warning: Linear solve failed, using pseudoinverse")
|
|
659
|
+
# Use pseudoinverse as fallback
|
|
660
|
+
shifted_hessian_inv = np.linalg.pinv(shifted_hessian, rcond=1e-10)
|
|
661
|
+
move_vector = -np.dot(shifted_hessian_inv, int_gradient)
|
|
662
|
+
|
|
663
|
+
print(f"Lambda for RFO step: {lambda_for_calc}")
|
|
664
|
+
print(f"Gradient RMS: {np.sqrt(np.mean(int_gradient**2))}")
|
|
665
|
+
print(f"Step RMS: {np.sqrt(np.mean(move_vector**2))}")
|
|
666
|
+
|
|
667
|
+
# Limit step size if it exceeds trust radius
|
|
668
|
+
step_norm = np.linalg.norm(move_vector)
|
|
669
|
+
if step_norm > self.trust_radius:
|
|
670
|
+
scale_factor = self.trust_radius / step_norm
|
|
671
|
+
move_vector *= scale_factor
|
|
672
|
+
print(f"Step scaled by {scale_factor} to meet trust radius")
|
|
673
|
+
|
|
674
|
+
return move_vector.reshape(-1, 1)
|
|
675
|
+
|
|
676
|
+
def reset_system(self, geometry):
|
|
677
|
+
"""
|
|
678
|
+
Reset internal coordinates when molecular structure changes significantly
|
|
679
|
+
"""
|
|
680
|
+
print("Resetting redundant internal coordinate system")
|
|
681
|
+
|
|
682
|
+
# Clear previous coordinates
|
|
683
|
+
self.primitive_coords = None
|
|
684
|
+
|
|
685
|
+
# Define new internal coordinates
|
|
686
|
+
_, self.primitive_coords = self.define_internal_coordinates(geometry)
|
|
687
|
+
print(f"Defined {len(self.primitive_coords)} new primitive coordinates")
|
|
688
|
+
|
|
689
|
+
# Update B matrix
|
|
690
|
+
B = self.update_coordinates(geometry)
|
|
691
|
+
|
|
692
|
+
# Reset Hessian to identity
|
|
693
|
+
self.internal_hessian = np.eye(len(self.primitive_coords))
|
|
694
|
+
self.Initialization = False
|
|
695
|
+
|
|
696
|
+
return B
|
|
697
|
+
|
|
698
|
+
def run(self, geom_num_list, B_g, pre_B_g, pre_geom, B_e, pre_B_e, pre_move_vector, initial_geom_num_list, g, pre_g):
|
|
699
|
+
"""
|
|
700
|
+
Main optimization step function using Redundant Internal Coordinates
|
|
701
|
+
"""
|
|
702
|
+
print(f"======= RIC-RFO Iteration {self.iter} =======")
|
|
703
|
+
|
|
704
|
+
# Define internal coordinates if not already defined
|
|
705
|
+
if self.primitive_coords is None:
|
|
706
|
+
_, self.primitive_coords = self.define_internal_coordinates(geom_num_list)
|
|
707
|
+
print(f"Defined {len(self.primitive_coords)} primitive internal coordinates")
|
|
708
|
+
|
|
709
|
+
# Check if molecular connectivity has changed
|
|
710
|
+
coords_reset = False
|
|
711
|
+
if pre_geom is not None:
|
|
712
|
+
coords_reset = self.check_connectivity_change(geom_num_list)
|
|
713
|
+
|
|
714
|
+
if coords_reset:
|
|
715
|
+
# Reset coordinate system if connectivity changed
|
|
716
|
+
B = self.reset_system(geom_num_list)
|
|
717
|
+
else:
|
|
718
|
+
# Update coordinate system for current geometry
|
|
719
|
+
B = self.update_coordinates(geom_num_list)
|
|
720
|
+
|
|
721
|
+
# Transform gradient to internal coordinates
|
|
722
|
+
int_gradient = self.transform_gradient(B_g)
|
|
723
|
+
|
|
724
|
+
# Initialize or update the Hessian
|
|
725
|
+
if self.Initialization or self.internal_hessian is None:
|
|
726
|
+
# Start with identity Hessian in internal coordinates
|
|
727
|
+
self.internal_hessian = np.eye(len(self.primitive_coords))
|
|
728
|
+
self.Initialization = False
|
|
729
|
+
else:
|
|
730
|
+
# Update the internal coordinate Hessian if we have previous geometry and gradient
|
|
731
|
+
if pre_geom is not None and pre_B_g is not None and not coords_reset:
|
|
732
|
+
# Calculate previous internal gradient
|
|
733
|
+
prev_B = self.build_B_matrix(pre_geom)
|
|
734
|
+
prev_G = self.build_G_matrix(prev_B)
|
|
735
|
+
|
|
736
|
+
try:
|
|
737
|
+
U, s, Vh = np.linalg.svd(prev_G, full_matrices=False)
|
|
738
|
+
s_inv = np.array([1.0/x if x > self.redundant_thresh else 0.0 for x in s])
|
|
739
|
+
G_inv = np.dot(U * s_inv[:, np.newaxis], U.T)
|
|
740
|
+
|
|
741
|
+
prev_B_pinv = np.dot(G_inv, prev_B)
|
|
742
|
+
except np.linalg.LinAlgError:
|
|
743
|
+
prev_B_pinv = np.linalg.pinv(prev_B, rcond=1e-8)
|
|
744
|
+
|
|
745
|
+
prev_int_gradient = np.dot(prev_B_pinv, pre_B_g)
|
|
746
|
+
|
|
747
|
+
# Calculate displacement in internal coordinates
|
|
748
|
+
cart_displacement = (geom_num_list - pre_geom).reshape(-1, 1)
|
|
749
|
+
int_displacement = np.dot(B, cart_displacement)
|
|
750
|
+
|
|
751
|
+
# Delta gradient in internal coordinates
|
|
752
|
+
delta_grad = (int_gradient - prev_int_gradient).reshape(-1, 1)
|
|
753
|
+
|
|
754
|
+
# Update Hessian using the hess_update methods
|
|
755
|
+
if "msp" in self.config.get("method", "").lower():
|
|
756
|
+
print("RIC-RFO: Using MSP Hessian update")
|
|
757
|
+
delta_hess = self.hess_update.MSP_hessian_update(
|
|
758
|
+
self.internal_hessian, int_displacement, delta_grad
|
|
759
|
+
)
|
|
760
|
+
elif "bfgs" in self.config.get("method", "").lower():
|
|
761
|
+
print("RIC-RFO: Using BFGS Hessian update")
|
|
762
|
+
delta_hess = self.hess_update.BFGS_hessian_update(
|
|
763
|
+
self.internal_hessian, int_displacement, delta_grad
|
|
764
|
+
)
|
|
765
|
+
elif "fsb" in self.config.get("method", "").lower():
|
|
766
|
+
print("RIC-RFO: Using FSB Hessian update")
|
|
767
|
+
delta_hess = self.hess_update.FSB_hessian_update(
|
|
768
|
+
self.internal_hessian, int_displacement, delta_grad
|
|
769
|
+
)
|
|
770
|
+
elif "bofill" in self.config.get("method", "").lower():
|
|
771
|
+
print("RIC-RFO: Using Bofill Hessian update")
|
|
772
|
+
delta_hess = self.hess_update.Bofill_hessian_update(
|
|
773
|
+
self.internal_hessian, int_displacement, delta_grad
|
|
774
|
+
)
|
|
775
|
+
elif "sr1" in self.config.get("method", "").lower():
|
|
776
|
+
print("RIC-RFO: Using SR1 Hessian update")
|
|
777
|
+
delta_hess = self.hess_update.SR1_hessian_update(
|
|
778
|
+
self.internal_hessian, int_displacement, delta_grad
|
|
779
|
+
)
|
|
780
|
+
elif "psb" in self.config.get("method", "").lower():
|
|
781
|
+
print("RIC-RFO: Using PSB Hessian update")
|
|
782
|
+
delta_hess = self.hess_update.PSB_hessian_update(
|
|
783
|
+
self.internal_hessian, int_displacement, delta_grad
|
|
784
|
+
)
|
|
785
|
+
elif "flowchart" in self.config.get("method", "").lower():
|
|
786
|
+
print("RIC-RFO: Using flowchart Hessian update")
|
|
787
|
+
delta_hess = self.hess_update.flowchart_hessian_update(
|
|
788
|
+
self.internal_hessian, int_displacement, delta_grad, self.config["method"]
|
|
789
|
+
)
|
|
790
|
+
else:
|
|
791
|
+
# Default to BFGS if no method is specified
|
|
792
|
+
print("RIC-RFO: Using BFGS Hessian update (default)")
|
|
793
|
+
delta_hess = self.hess_update.BFGS_hessian_update(
|
|
794
|
+
self.internal_hessian, int_displacement, delta_grad
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
# Apply the Hessian update
|
|
798
|
+
self.internal_hessian += delta_hess
|
|
799
|
+
|
|
800
|
+
# Ensure the Hessian is symmetric
|
|
801
|
+
self.internal_hessian = 0.5 * (self.internal_hessian + self.internal_hessian.T)
|
|
802
|
+
|
|
803
|
+
# Apply bias Hessian if provided
|
|
804
|
+
working_hessian = self.internal_hessian.copy()
|
|
805
|
+
if self.bias_hessian is not None:
|
|
806
|
+
int_bias_hessian = self.transform_hessian(self.bias_hessian)
|
|
807
|
+
working_hessian += int_bias_hessian
|
|
808
|
+
|
|
809
|
+
# Calculate RFO step in internal coordinates
|
|
810
|
+
int_step = self.run_rfo_step(int_gradient, working_hessian)
|
|
811
|
+
|
|
812
|
+
# Transform step back to Cartesian coordinates
|
|
813
|
+
cart_step = self.internal_to_cartesian(int_step, geom_num_list)
|
|
814
|
+
|
|
815
|
+
# Store current state for next iteration
|
|
816
|
+
self.prev_cartesian = geom_num_list.copy()
|
|
817
|
+
self.prev_gradient = B_g.copy()
|
|
818
|
+
|
|
819
|
+
# Increment iteration counter
|
|
820
|
+
self.iter += 1
|
|
821
|
+
|
|
822
|
+
# Apply DELTA scaling and reshape
|
|
823
|
+
return -1 * self.DELTA * cart_step.reshape(-1, 1)
|
|
824
|
+
|
|
825
|
+
def set_hessian(self, hessian):
|
|
826
|
+
"""Set Cartesian Hessian"""
|
|
827
|
+
self.hessian = hessian.copy()
|
|
828
|
+
if self.B_matrix is not None:
|
|
829
|
+
# Transform to internal coordinates
|
|
830
|
+
self.internal_hessian = self.transform_hessian(hessian)
|
|
831
|
+
|
|
832
|
+
def set_bias_hessian(self, bias_hessian):
|
|
833
|
+
"""Set bias Hessian (in Cartesian coordinates)"""
|
|
834
|
+
self.bias_hessian = bias_hessian.copy()
|
|
835
|
+
|
|
836
|
+
def get_hessian(self):
|
|
837
|
+
"""Return current Hessian (in Cartesian coordinates)"""
|
|
838
|
+
return self.hessian if self.hessian is not None else None
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def get_bias_hessian(self):
|
|
842
|
+
return self.bias_hessian
|