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