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,203 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .gdiis import GDIIS
|
|
3
|
+
from .ediis import EDIIS
|
|
4
|
+
|
|
5
|
+
class GEDIIS:
|
|
6
|
+
"""
|
|
7
|
+
Combined GDIIS and EDIIS optimization method.
|
|
8
|
+
|
|
9
|
+
This class implements a combined gradient- and energy-based DIIS approach
|
|
10
|
+
that adaptively blends the two methods based on their performance.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
# Create individual optimizers
|
|
15
|
+
self.gdiis = GDIIS()
|
|
16
|
+
self.ediis = EDIIS()
|
|
17
|
+
|
|
18
|
+
# Configuration parameters
|
|
19
|
+
self.gediis_mode = 'adaptive' # 'adaptive', 'sequential', or 'blend'
|
|
20
|
+
self.gediis_ediis_weight = 0.6 # Initial EDIIS weight in blend mode
|
|
21
|
+
self.gediis_auto_switch = True # Automatically switch based on performance
|
|
22
|
+
|
|
23
|
+
# Tracking variables
|
|
24
|
+
self.energy_history = []
|
|
25
|
+
self.grad_rms_history = []
|
|
26
|
+
self.gdiis_success_count = 0
|
|
27
|
+
self.ediis_success_count = 0
|
|
28
|
+
self.iter = 0
|
|
29
|
+
|
|
30
|
+
# For sequential mode
|
|
31
|
+
self.gediis_phase = 'ediis' # Start with EDIIS ('ediis' or 'gdiis')
|
|
32
|
+
self.gediis_phase_steps = 0
|
|
33
|
+
self.gediis_phase_switch = 3 # Switch phases every N steps
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
def _evaluate_performance(self):
|
|
37
|
+
"""
|
|
38
|
+
Evaluate the relative performance of GDIIS and EDIIS
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
--------
|
|
42
|
+
tuple
|
|
43
|
+
(ediis_weight, gdiis_weight) - Relative weights for the two methods
|
|
44
|
+
"""
|
|
45
|
+
# With limited history, favor EDIIS early and GDIIS later
|
|
46
|
+
if len(self.energy_history) < 3:
|
|
47
|
+
if self.iter < 10:
|
|
48
|
+
# Early iterations: favor EDIIS (better for large steps)
|
|
49
|
+
return 0.7, 0.3
|
|
50
|
+
else:
|
|
51
|
+
# Later iterations: favor GDIIS (better for fine convergence)
|
|
52
|
+
return 0.3, 0.7
|
|
53
|
+
|
|
54
|
+
# Look at recent energy and gradient trends
|
|
55
|
+
energy_decreasing = self.energy_history[-1] < self.energy_history[-2]
|
|
56
|
+
grad_decreasing = self.grad_rms_history[-1] < self.grad_rms_history[-2]
|
|
57
|
+
|
|
58
|
+
# Adjust success counters
|
|
59
|
+
if energy_decreasing:
|
|
60
|
+
self.ediis_success_count += 1
|
|
61
|
+
else:
|
|
62
|
+
self.ediis_success_count = max(0, self.ediis_success_count - 1)
|
|
63
|
+
|
|
64
|
+
if grad_decreasing:
|
|
65
|
+
self.gdiis_success_count += 1
|
|
66
|
+
else:
|
|
67
|
+
self.gdiis_success_count = max(0, self.gdiis_success_count - 1)
|
|
68
|
+
|
|
69
|
+
# Calculate weights based on success counts and iteration phase
|
|
70
|
+
total_success = self.ediis_success_count + self.gdiis_success_count + 1 # +1 to avoid division by zero
|
|
71
|
+
ediis_raw_weight = self.ediis_success_count / total_success
|
|
72
|
+
|
|
73
|
+
# Adjust based on optimization phase (EDIIS early, GDIIS late)
|
|
74
|
+
phase_factor = max(0.0, min(1.0, (20 - self.iter) / 20)) # 1.0→0.0 over 20 iterations
|
|
75
|
+
ediis_weight = 0.3 + ediis_raw_weight * 0.4 + phase_factor * 0.3
|
|
76
|
+
|
|
77
|
+
# Ensure weights are in reasonable range
|
|
78
|
+
ediis_weight = max(0.2, min(0.8, ediis_weight))
|
|
79
|
+
gdiis_weight = 1.0 - ediis_weight
|
|
80
|
+
|
|
81
|
+
return ediis_weight, gdiis_weight
|
|
82
|
+
|
|
83
|
+
def run(self, geom_num_list, energy, B_g, pre_B_g, original_move_vector):
|
|
84
|
+
"""
|
|
85
|
+
Run combined GEDIIS optimization
|
|
86
|
+
|
|
87
|
+
Parameters:
|
|
88
|
+
-----------
|
|
89
|
+
geom_num_list : numpy.ndarray
|
|
90
|
+
Current geometry
|
|
91
|
+
energy : float
|
|
92
|
+
Current energy value
|
|
93
|
+
B_g : numpy.ndarray
|
|
94
|
+
Current gradient
|
|
95
|
+
pre_B_g : numpy.ndarray
|
|
96
|
+
Previous gradient
|
|
97
|
+
original_move_vector : numpy.ndarray
|
|
98
|
+
Original optimization step
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
--------
|
|
102
|
+
numpy.ndarray
|
|
103
|
+
Optimized step vector
|
|
104
|
+
"""
|
|
105
|
+
print("GEDIIS method (combined GDIIS and EDIIS)")
|
|
106
|
+
n_coords = len(geom_num_list)
|
|
107
|
+
grad_rms = np.sqrt(np.mean(B_g ** 2))
|
|
108
|
+
|
|
109
|
+
# Update history
|
|
110
|
+
self.energy_history.append(energy)
|
|
111
|
+
self.grad_rms_history.append(grad_rms)
|
|
112
|
+
|
|
113
|
+
# Always run both methods to update their histories
|
|
114
|
+
gdiis_step = self.gdiis.run(geom_num_list, B_g, pre_B_g, original_move_vector)
|
|
115
|
+
ediis_step = self.ediis.run(geom_num_list, energy, B_g, original_move_vector)
|
|
116
|
+
|
|
117
|
+
# Determine mode of operation
|
|
118
|
+
if self.gediis_mode == 'sequential':
|
|
119
|
+
# Sequential mode switches between methods periodically
|
|
120
|
+
self.gediis_phase_steps += 1
|
|
121
|
+
if self.gediis_phase_steps >= self.gediis_phase_switch:
|
|
122
|
+
self.gediis_phase = 'ediis' if self.gediis_phase == 'gdiis' else 'gdiis'
|
|
123
|
+
self.gediis_phase_steps = 0
|
|
124
|
+
print(f"Switching to {self.gediis_phase.upper()} phase")
|
|
125
|
+
|
|
126
|
+
if self.gediis_phase == 'ediis':
|
|
127
|
+
print("Using EDIIS step (sequential mode)")
|
|
128
|
+
step = ediis_step
|
|
129
|
+
else:
|
|
130
|
+
print("Using GDIIS step (sequential mode)")
|
|
131
|
+
step = gdiis_step
|
|
132
|
+
|
|
133
|
+
elif self.gediis_mode == 'adaptive':
|
|
134
|
+
# Adaptive mode chooses the best method based on performance
|
|
135
|
+
if self.gediis_auto_switch and len(self.energy_history) > 1:
|
|
136
|
+
ediis_weight, gdiis_weight = self._evaluate_performance()
|
|
137
|
+
|
|
138
|
+
# Blend with emphasis on better-performing method
|
|
139
|
+
if ediis_weight > 0.7:
|
|
140
|
+
print(f"Using EDIIS step (adaptive mode, {ediis_weight:.2f})")
|
|
141
|
+
step = ediis_step
|
|
142
|
+
elif gdiis_weight > 0.7:
|
|
143
|
+
print(f"Using GDIIS step (adaptive mode, {gdiis_weight:.2f})")
|
|
144
|
+
step = gdiis_step
|
|
145
|
+
else:
|
|
146
|
+
# Blend the steps
|
|
147
|
+
step = ediis_weight * ediis_step + gdiis_weight * gdiis_step
|
|
148
|
+
print(f"Using blended step: {ediis_weight:.2f}*EDIIS + {gdiis_weight:.2f}*GDIIS")
|
|
149
|
+
else:
|
|
150
|
+
# Default behavior early in optimization
|
|
151
|
+
if self.iter < 5:
|
|
152
|
+
print("Using EDIIS step (early iterations)")
|
|
153
|
+
step = ediis_step
|
|
154
|
+
else:
|
|
155
|
+
print("Using GDIIS step (later iterations)")
|
|
156
|
+
step = gdiis_step
|
|
157
|
+
|
|
158
|
+
else: # blend mode
|
|
159
|
+
# Blend mode always combines both methods with fixed weights
|
|
160
|
+
ediis_weight = self.gediis_ediis_weight
|
|
161
|
+
gdiis_weight = 1.0 - ediis_weight
|
|
162
|
+
step = ediis_weight * ediis_step + gdiis_weight * gdiis_step
|
|
163
|
+
print(f"Using blended step: {ediis_weight:.2f}*EDIIS + {gdiis_weight:.2f}*GDIIS")
|
|
164
|
+
|
|
165
|
+
# Final safety checks
|
|
166
|
+
step_norm = np.linalg.norm(step)
|
|
167
|
+
orig_norm = np.linalg.norm(original_move_vector)
|
|
168
|
+
|
|
169
|
+
# Check for problematic steps
|
|
170
|
+
if np.any(np.isnan(step)) or np.any(np.isinf(step)) or step_norm < 1e-10:
|
|
171
|
+
print("Warning: Invalid GEDIIS step, falling back to original step")
|
|
172
|
+
step = original_move_vector
|
|
173
|
+
elif step_norm > 3.0 * orig_norm and orig_norm > 1e-10:
|
|
174
|
+
# Cap step size to avoid large jumps
|
|
175
|
+
print(f"Warning: GEDIIS step too large ({step_norm:.4f} > {3.0 * orig_norm:.4f}), scaling down")
|
|
176
|
+
scale_factor = 3.0 * orig_norm / step_norm
|
|
177
|
+
step = scale_factor * step
|
|
178
|
+
|
|
179
|
+
# Monitor convergence behavior
|
|
180
|
+
if len(self.energy_history) > 3:
|
|
181
|
+
# Check if optimization is oscillating
|
|
182
|
+
oscillating = False
|
|
183
|
+
energy_diffs = np.diff(self.energy_history[-4:])
|
|
184
|
+
if np.all(np.abs(energy_diffs) > 0): # All non-zero differences
|
|
185
|
+
signs = np.sign(energy_diffs)
|
|
186
|
+
if np.sum(np.abs(np.diff(signs))) >= 2: # Sign changes indicate oscillation
|
|
187
|
+
oscillating = True
|
|
188
|
+
|
|
189
|
+
if oscillating:
|
|
190
|
+
print("Warning: Possible oscillatory behavior detected")
|
|
191
|
+
# Switch mode or adjust parameters to stabilize
|
|
192
|
+
if self.gediis_mode == 'adaptive':
|
|
193
|
+
# Increase EDIIS weight for stability
|
|
194
|
+
ediis_weight = max(0.7, ediis_weight)
|
|
195
|
+
gdiis_weight = 1.0 - ediis_weight
|
|
196
|
+
# Recalculate step with higher EDIIS weight
|
|
197
|
+
step = ediis_weight * ediis_step + gdiis_weight * gdiis_step
|
|
198
|
+
print(f"Stabilizing oscillation: {ediis_weight:.2f}*EDIIS + {gdiis_weight:.2f}*GDIIS")
|
|
199
|
+
|
|
200
|
+
# Update iteration counter
|
|
201
|
+
self.iter += 1
|
|
202
|
+
|
|
203
|
+
return step
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.integrate import solve_ivp
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from multioptpy.Parameters.parameter import covalent_radii_lib
|
|
6
|
+
|
|
7
|
+
class GeodesicStepper:
|
|
8
|
+
"""
|
|
9
|
+
Geodesic step calculation for geometry optimization based on
|
|
10
|
+
J. Chem. Phys. 155, 094105 (2021) "Geometry optimization speedup through a geodesic coordinate system"
|
|
11
|
+
|
|
12
|
+
This class implements geodesic-based step calculation to improve optimization convergence
|
|
13
|
+
by following the intrinsic curved geometry of the internal coordinate space.
|
|
14
|
+
"""
|
|
15
|
+
def __init__(self, element_list):
|
|
16
|
+
"""
|
|
17
|
+
Initialize the GeodesicStep optimizer
|
|
18
|
+
|
|
19
|
+
Parameters:
|
|
20
|
+
-----------
|
|
21
|
+
element_list : list
|
|
22
|
+
List of element symbols as strings
|
|
23
|
+
"""
|
|
24
|
+
self.element_list = element_list
|
|
25
|
+
self.natoms = len(element_list)
|
|
26
|
+
self.ndim = 3 * self.natoms
|
|
27
|
+
self.logger = logging.getLogger(__name__)
|
|
28
|
+
self.bond_scale = 1.5 # Scaling factor for covalent radii
|
|
29
|
+
|
|
30
|
+
def determine_bonds(self):
|
|
31
|
+
"""
|
|
32
|
+
Determine bonded atoms based on covalent radii
|
|
33
|
+
Only considers bond lengths as internal coordinates
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
--------
|
|
37
|
+
list
|
|
38
|
+
List of (i,j) tuples representing bonded atoms
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Get covalent radii for all atoms
|
|
43
|
+
radii = [covalent_radii_lib(elem) for elem in self.element_list]
|
|
44
|
+
|
|
45
|
+
bonds = []
|
|
46
|
+
for i in range(self.natoms):
|
|
47
|
+
for j in range(i+1, self.natoms):
|
|
48
|
+
# Calculate distance threshold based on covalent radii with scaling
|
|
49
|
+
threshold = (radii[i] + radii[j]) * self.bond_scale
|
|
50
|
+
# Store bond if it exists
|
|
51
|
+
bonds.append((i, j, threshold))
|
|
52
|
+
|
|
53
|
+
return bonds
|
|
54
|
+
|
|
55
|
+
def calculate_internal_coordinates(self, geometry):
|
|
56
|
+
"""
|
|
57
|
+
Calculate internal coordinates (bond lengths) from Cartesian coordinates
|
|
58
|
+
|
|
59
|
+
Parameters:
|
|
60
|
+
-----------
|
|
61
|
+
geometry : numpy.ndarray
|
|
62
|
+
Cartesian coordinates (natoms×3)
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
--------
|
|
66
|
+
tuple
|
|
67
|
+
(bond_values, bond_pairs, bond_thresholds)
|
|
68
|
+
"""
|
|
69
|
+
# Get bonds
|
|
70
|
+
bonds = self.determine_bonds()
|
|
71
|
+
|
|
72
|
+
# Calculate bond lengths and filter by threshold
|
|
73
|
+
bond_values = []
|
|
74
|
+
bond_pairs = []
|
|
75
|
+
bond_thresholds = []
|
|
76
|
+
|
|
77
|
+
for i, j, threshold in bonds:
|
|
78
|
+
r_ij = np.linalg.norm(geometry[i] - geometry[j])
|
|
79
|
+
if r_ij < threshold:
|
|
80
|
+
bond_values.append(r_ij)
|
|
81
|
+
bond_pairs.append((i, j))
|
|
82
|
+
bond_thresholds.append(threshold)
|
|
83
|
+
|
|
84
|
+
return np.array(bond_values), bond_pairs, bond_thresholds
|
|
85
|
+
|
|
86
|
+
def calculate_b_matrix(self, geometry, bond_pairs):
|
|
87
|
+
"""
|
|
88
|
+
Calculate Wilson's B-matrix (derivatives of internal coordinates w.r.t. Cartesian)
|
|
89
|
+
|
|
90
|
+
Parameters:
|
|
91
|
+
-----------
|
|
92
|
+
geometry : numpy.ndarray
|
|
93
|
+
Cartesian coordinates (natoms×3)
|
|
94
|
+
bond_pairs : list
|
|
95
|
+
List of (i,j) tuples representing bonded atoms
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
--------
|
|
99
|
+
numpy.ndarray
|
|
100
|
+
B matrix
|
|
101
|
+
"""
|
|
102
|
+
ncoords = len(bond_pairs)
|
|
103
|
+
ndim = self.ndim
|
|
104
|
+
B = np.zeros((ncoords, ndim))
|
|
105
|
+
|
|
106
|
+
# Calculate B matrix elements
|
|
107
|
+
for idx, (i, j) in enumerate(bond_pairs):
|
|
108
|
+
# Vector pointing from atom j to atom i
|
|
109
|
+
rij = geometry[i] - geometry[j]
|
|
110
|
+
# Bond length
|
|
111
|
+
r = np.linalg.norm(rij)
|
|
112
|
+
# Unit vector
|
|
113
|
+
if r > 1e-10: # Avoid division by zero
|
|
114
|
+
unit_vec = rij / r
|
|
115
|
+
else:
|
|
116
|
+
unit_vec = np.zeros(3)
|
|
117
|
+
|
|
118
|
+
# Fill B matrix elements
|
|
119
|
+
B[idx, 3*i:3*i+3] = unit_vec
|
|
120
|
+
B[idx, 3*j:3*j+3] = -unit_vec
|
|
121
|
+
|
|
122
|
+
return B
|
|
123
|
+
|
|
124
|
+
def calculate_b_derivatives(self, geometry, bond_pairs):
|
|
125
|
+
"""
|
|
126
|
+
Calculate derivatives of the B-matrix w.r.t. Cartesian coordinates
|
|
127
|
+
Using vectorized operations
|
|
128
|
+
|
|
129
|
+
Parameters:
|
|
130
|
+
-----------
|
|
131
|
+
geometry : numpy.ndarray
|
|
132
|
+
Cartesian coordinates (natoms×3)
|
|
133
|
+
bond_pairs : list
|
|
134
|
+
List of (i,j) tuples representing bonded atoms
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
--------
|
|
138
|
+
numpy.ndarray
|
|
139
|
+
Derivatives of B matrix
|
|
140
|
+
"""
|
|
141
|
+
ncoords = len(bond_pairs)
|
|
142
|
+
ndim = self.ndim
|
|
143
|
+
dB = np.zeros((ncoords, ndim, ndim))
|
|
144
|
+
|
|
145
|
+
coords = geometry.reshape(-1, 3)
|
|
146
|
+
|
|
147
|
+
# Vectorized calculation for all bonds
|
|
148
|
+
for idx, (i, j) in enumerate(bond_pairs):
|
|
149
|
+
# Vector pointing from atom j to atom i
|
|
150
|
+
rij = coords[i] - coords[j]
|
|
151
|
+
r = np.linalg.norm(rij)
|
|
152
|
+
|
|
153
|
+
if r < 1e-10: # Avoid numerical issues
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
# Precompute terms
|
|
157
|
+
r3 = r**3
|
|
158
|
+
|
|
159
|
+
# Create unit matrices for i and j blocks
|
|
160
|
+
eye3 = np.eye(3)
|
|
161
|
+
outer_prod = np.outer(rij, rij) / r3
|
|
162
|
+
|
|
163
|
+
# Term for each 3x3 block is: δ_ab/r - r_a*r_b/r^3
|
|
164
|
+
block = eye3 / r - outer_prod
|
|
165
|
+
|
|
166
|
+
# Assign to the 4 blocks efficiently
|
|
167
|
+
ii_idx = slice(3*i, 3*i+3)
|
|
168
|
+
jj_idx = slice(3*j, 3*j+3)
|
|
169
|
+
|
|
170
|
+
dB[idx, ii_idx, ii_idx] = block
|
|
171
|
+
dB[idx, jj_idx, jj_idx] = block
|
|
172
|
+
dB[idx, ii_idx, jj_idx] = -block
|
|
173
|
+
dB[idx, jj_idx, ii_idx] = -block
|
|
174
|
+
|
|
175
|
+
return dB
|
|
176
|
+
|
|
177
|
+
def calculate_metric_tensor(self, B):
|
|
178
|
+
"""
|
|
179
|
+
Calculate metric tensor G = B·Bᵀ
|
|
180
|
+
|
|
181
|
+
Parameters:
|
|
182
|
+
-----------
|
|
183
|
+
B : numpy.ndarray
|
|
184
|
+
Wilson's B matrix
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
--------
|
|
188
|
+
numpy.ndarray
|
|
189
|
+
Metric tensor G
|
|
190
|
+
"""
|
|
191
|
+
G = np.dot(B, B.T)
|
|
192
|
+
return G
|
|
193
|
+
|
|
194
|
+
def calculate_christoffel_symbols(self, B, dB, G_inv):
|
|
195
|
+
"""
|
|
196
|
+
Calculate Christoffel symbols of the second kind using vectorized operations
|
|
197
|
+
|
|
198
|
+
Parameters:
|
|
199
|
+
-----------
|
|
200
|
+
B : numpy.ndarray
|
|
201
|
+
Wilson's B matrix
|
|
202
|
+
dB : numpy.ndarray
|
|
203
|
+
Derivatives of B matrix
|
|
204
|
+
G_inv : numpy.ndarray
|
|
205
|
+
Inverse of G matrix
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
--------
|
|
209
|
+
gamma : numpy.ndarray
|
|
210
|
+
Christoffel symbols
|
|
211
|
+
"""
|
|
212
|
+
ncoords = B.shape[0]
|
|
213
|
+
|
|
214
|
+
# Initialize Christoffel symbols tensor
|
|
215
|
+
gamma = np.zeros((ncoords, ncoords, ncoords))
|
|
216
|
+
|
|
217
|
+
# Step 1: Calculate partial_j B_ia for all combinations
|
|
218
|
+
partial_j_B_i_a = np.zeros((ncoords, ncoords, self.ndim))
|
|
219
|
+
for i in range(ncoords):
|
|
220
|
+
for j in range(ncoords):
|
|
221
|
+
for a in range(self.ndim):
|
|
222
|
+
partial_j_B_i_a[i, j, a] = np.sum(dB[i, a, :] * B[j, :])
|
|
223
|
+
|
|
224
|
+
# Step 2: Sum over dimension a and contract with G_inv
|
|
225
|
+
for i in range(ncoords):
|
|
226
|
+
for j in range(ncoords):
|
|
227
|
+
for k in range(ncoords):
|
|
228
|
+
gamma[i, j, k] = G_inv[i, k] * np.sum(partial_j_B_i_a[i, j, :])
|
|
229
|
+
|
|
230
|
+
# Step 3: Symmetrize over j,k indices
|
|
231
|
+
for i in range(ncoords):
|
|
232
|
+
for j in range(ncoords):
|
|
233
|
+
for k in range(j+1, ncoords):
|
|
234
|
+
avg = (gamma[i, j, k] + gamma[i, k, j]) / 2
|
|
235
|
+
gamma[i, j, k] = avg
|
|
236
|
+
gamma[i, k, j] = avg
|
|
237
|
+
|
|
238
|
+
return gamma
|
|
239
|
+
|
|
240
|
+
def geodesic_equation(self, t, y, gamma, ncoords):
|
|
241
|
+
"""
|
|
242
|
+
Vectorized implementation of the geodesic equation ODE function
|
|
243
|
+
|
|
244
|
+
Parameters:
|
|
245
|
+
-----------
|
|
246
|
+
t : float
|
|
247
|
+
Integration parameter (0 to 1)
|
|
248
|
+
y : numpy.ndarray
|
|
249
|
+
State vector [q, q_dot], where q are internal coordinates
|
|
250
|
+
gamma : numpy.ndarray
|
|
251
|
+
Christoffel symbols
|
|
252
|
+
ncoords : int
|
|
253
|
+
Number of internal coordinates
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
--------
|
|
257
|
+
dydt : numpy.ndarray
|
|
258
|
+
Derivative of state vector
|
|
259
|
+
"""
|
|
260
|
+
# Extract q and q_dot from state vector
|
|
261
|
+
q = y[:ncoords]
|
|
262
|
+
q_dot = y[ncoords:]
|
|
263
|
+
|
|
264
|
+
# Calculate acceleration using vectorized operations
|
|
265
|
+
# Compute the contraction of Christoffel symbols with velocities
|
|
266
|
+
q_ddot = np.zeros(ncoords)
|
|
267
|
+
for i in range(ncoords):
|
|
268
|
+
for j in range(ncoords):
|
|
269
|
+
for k in range(ncoords):
|
|
270
|
+
q_ddot[i] -= gamma[i, j, k] * q_dot[j] * q_dot[k]
|
|
271
|
+
|
|
272
|
+
# Return combined derivative [q_dot, q_ddot]
|
|
273
|
+
return np.concatenate([q_dot, q_ddot])
|
|
274
|
+
|
|
275
|
+
def parallel_transport(self, v0, B0, G0_inv, B1, G1_inv):
|
|
276
|
+
"""
|
|
277
|
+
Parallel transport a vector from one point to another
|
|
278
|
+
|
|
279
|
+
Parameters:
|
|
280
|
+
-----------
|
|
281
|
+
v0 : numpy.ndarray
|
|
282
|
+
Vector in Cartesian coordinates at the initial point
|
|
283
|
+
B0, B1 : numpy.ndarray
|
|
284
|
+
B matrices at initial and final points
|
|
285
|
+
G0_inv, G1_inv : numpy.ndarray
|
|
286
|
+
Inverse metric tensors at initial and final points
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
--------
|
|
290
|
+
numpy.ndarray
|
|
291
|
+
Transported vector in Cartesian coordinates
|
|
292
|
+
"""
|
|
293
|
+
# Transform to internal coordinates
|
|
294
|
+
v0_int = np.dot(B0, v0)
|
|
295
|
+
|
|
296
|
+
# Parallel transport in internal coordinates (simplified)
|
|
297
|
+
# In this implementation, we use a direct transformation approach
|
|
298
|
+
# For more accuracy, one should solve the parallel transport equations
|
|
299
|
+
|
|
300
|
+
# Transform back to Cartesian coordinates
|
|
301
|
+
v1 = np.dot(B1.T, np.dot(G1_inv, np.dot(G0_inv, v0_int)))
|
|
302
|
+
|
|
303
|
+
return v1
|
|
304
|
+
|
|
305
|
+
def solve_geodesic(self, q0, v0, gamma, ncoords):
|
|
306
|
+
"""
|
|
307
|
+
Solve the geodesic equation using LSODA method
|
|
308
|
+
|
|
309
|
+
Parameters:
|
|
310
|
+
-----------
|
|
311
|
+
q0 : numpy.ndarray
|
|
312
|
+
Initial internal coordinates
|
|
313
|
+
v0 : numpy.ndarray
|
|
314
|
+
Initial velocity in internal coordinates
|
|
315
|
+
gamma : numpy.ndarray
|
|
316
|
+
Christoffel symbols
|
|
317
|
+
ncoords : int
|
|
318
|
+
Number of internal coordinates
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
--------
|
|
322
|
+
numpy.ndarray
|
|
323
|
+
Final internal coordinates after following the geodesic
|
|
324
|
+
"""
|
|
325
|
+
# Initial state [q0, v0]
|
|
326
|
+
y0 = np.concatenate([q0, v0])
|
|
327
|
+
|
|
328
|
+
# ODE solver with LSODA method
|
|
329
|
+
solution = solve_ivp(
|
|
330
|
+
lambda t, y: self.geodesic_equation(t, y, gamma, ncoords),
|
|
331
|
+
[0, 1], # Integration from t=0 to t=1
|
|
332
|
+
y0,
|
|
333
|
+
method='LSODA',
|
|
334
|
+
rtol=1e-6,
|
|
335
|
+
atol=1e-8
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# Return final coordinates
|
|
339
|
+
q_final = solution.y[:ncoords, -1]
|
|
340
|
+
return q_final
|
|
341
|
+
|
|
342
|
+
def transform_to_cartesian(self, geometry, q_final, B, bond_pairs):
|
|
343
|
+
"""
|
|
344
|
+
Transform internal coordinate step to Cartesian coordinates
|
|
345
|
+
|
|
346
|
+
Parameters:
|
|
347
|
+
-----------
|
|
348
|
+
geometry : numpy.ndarray
|
|
349
|
+
Current Cartesian geometry
|
|
350
|
+
q_final : numpy.ndarray
|
|
351
|
+
Final internal coordinates
|
|
352
|
+
B : numpy.ndarray
|
|
353
|
+
Wilson's B matrix
|
|
354
|
+
bond_pairs : list
|
|
355
|
+
List of (i,j) tuples for bonds
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
--------
|
|
359
|
+
numpy.ndarray
|
|
360
|
+
Step vector in Cartesian coordinates
|
|
361
|
+
"""
|
|
362
|
+
# Calculate current internal coordinates
|
|
363
|
+
q_current = np.array([np.linalg.norm(geometry[i] - geometry[j])
|
|
364
|
+
for i, j in bond_pairs])
|
|
365
|
+
|
|
366
|
+
# Calculate change in internal coordinates
|
|
367
|
+
dq = q_final - q_current
|
|
368
|
+
|
|
369
|
+
# Transform to Cartesian step using the pseudo-inverse of B
|
|
370
|
+
B_pinv = np.linalg.pinv(B)
|
|
371
|
+
cartesian_step = np.dot(B_pinv, dq)
|
|
372
|
+
|
|
373
|
+
return cartesian_step
|
|
374
|
+
|
|
375
|
+
def run(self, geometry, original_move_vector):
|
|
376
|
+
"""
|
|
377
|
+
Run geodesic step calculation
|
|
378
|
+
|
|
379
|
+
Parameters:
|
|
380
|
+
-----------
|
|
381
|
+
geometry : numpy.ndarray
|
|
382
|
+
Current geometry in Cartesian coordinates
|
|
383
|
+
original_move_vector : numpy.ndarray
|
|
384
|
+
Original step vector from optimizer
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
--------
|
|
388
|
+
numpy.ndarray
|
|
389
|
+
Modified step vector following geodesic path
|
|
390
|
+
"""
|
|
391
|
+
# Reshape inputs if needed
|
|
392
|
+
geom = geometry.reshape(self.natoms, 3)
|
|
393
|
+
move_vec = original_move_vector.reshape(-1)
|
|
394
|
+
|
|
395
|
+
# Calculate internal coordinates
|
|
396
|
+
q, bond_pairs, _ = self.calculate_internal_coordinates(geom)
|
|
397
|
+
ncoords = len(q)
|
|
398
|
+
|
|
399
|
+
if ncoords == 0:
|
|
400
|
+
self.logger.warning("No internal coordinates found. Using original step.")
|
|
401
|
+
return original_move_vector
|
|
402
|
+
|
|
403
|
+
# Calculate Wilson's B-matrix
|
|
404
|
+
B = self.calculate_b_matrix(geom, bond_pairs)
|
|
405
|
+
|
|
406
|
+
# Calculate metric tensor and its inverse
|
|
407
|
+
G = self.calculate_metric_tensor(B)
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
G_inv = np.linalg.inv(G)
|
|
411
|
+
except np.linalg.LinAlgError:
|
|
412
|
+
self.logger.warning("Singular G matrix. Using pseudo-inverse instead.")
|
|
413
|
+
G_inv = np.linalg.pinv(G)
|
|
414
|
+
|
|
415
|
+
# Calculate derivatives of B-matrix
|
|
416
|
+
dB = self.calculate_b_derivatives(geom, bond_pairs)
|
|
417
|
+
|
|
418
|
+
# Calculate Christoffel symbols
|
|
419
|
+
gamma = self.calculate_christoffel_symbols(B, dB, G_inv)
|
|
420
|
+
|
|
421
|
+
# Transform original move vector to internal coordinates
|
|
422
|
+
v0_int = np.dot(B, move_vec)
|
|
423
|
+
|
|
424
|
+
# Solve geodesic equation
|
|
425
|
+
q_final = self.solve_geodesic(q, v0_int, gamma, ncoords)
|
|
426
|
+
|
|
427
|
+
# Transform back to Cartesian coordinates
|
|
428
|
+
geodesic_step = self.transform_to_cartesian(geom, q_final, B, bond_pairs)
|
|
429
|
+
print(f"Geodesic step norm: {np.linalg.norm(geodesic_step):<.6f}")
|
|
430
|
+
print(f"Original step norm: {np.linalg.norm(move_vec):<.6f}")
|
|
431
|
+
print(f"Ratio (geodesic/original): {np.linalg.norm(geodesic_step)/np.linalg.norm(move_vec):<.6f}")
|
|
432
|
+
# Reshape to match input format
|
|
433
|
+
return geodesic_step.reshape(original_move_vector.shape)
|