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,1015 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.optimize import brentq
|
|
3
|
+
from multioptpy.Optimizer.hessian_update import ModelHessianUpdate
|
|
4
|
+
from multioptpy.Optimizer.block_hessian_update import BlockHessianUpdate
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InternalCoordinates:
|
|
9
|
+
"""
|
|
10
|
+
Handles the construction and transformation of Delocalized Internal Coordinates (DIC)
|
|
11
|
+
as described by Baker, Kessi, and Delley (1996).
|
|
12
|
+
"""
|
|
13
|
+
def __init__(self, **config):
|
|
14
|
+
self.log_func = config.get("log_func", print)
|
|
15
|
+
self.g_tol = config.get("g_tol", 1e-6)
|
|
16
|
+
# B, U, Lambda_inv are calculated once and stored in the instance
|
|
17
|
+
self.B_prim = None # Primitive B-Matrix (n x 3N)
|
|
18
|
+
self.U = None # Active Eigenvectors (n x k)
|
|
19
|
+
self.Lambda_inv = None # Inverse Eigenvalues (k x k)
|
|
20
|
+
self.k = 0 # Number of active coordinates
|
|
21
|
+
|
|
22
|
+
def log(self, message, force=False):
|
|
23
|
+
if self.log_func and force: self.log_func(message, force=True)
|
|
24
|
+
elif self.log_func: self.log_func(message)
|
|
25
|
+
|
|
26
|
+
def _build_primitive_B_stretches(self, geom_cart_3N):
|
|
27
|
+
""" (Unchanged) Build B-matrix for all stretches (N*(N-1)/2) """
|
|
28
|
+
geom_cart_N3 = geom_cart_3N.reshape(-1, 3); N = geom_cart_N3.shape[0]; M = N * (N - 1) // 2
|
|
29
|
+
if M <= 0: return np.zeros((0, 3*N))
|
|
30
|
+
B_prim_ic = np.zeros((M, 3 * N)); row_idx = 0
|
|
31
|
+
for i in range(N):
|
|
32
|
+
for j in range(i + 1, N):
|
|
33
|
+
ri = geom_cart_N3[i, :]; rj = geom_cart_N3[j, :]
|
|
34
|
+
rij_vec = ri - rj; rij_norm = np.linalg.norm(rij_vec)
|
|
35
|
+
s_vec = np.zeros(3)
|
|
36
|
+
if rij_norm > 1e-8: s_vec = rij_vec / rij_norm
|
|
37
|
+
B_prim_ic[row_idx, 3*i : 3*i+3] = s_vec
|
|
38
|
+
B_prim_ic[row_idx, 3*j : 3*j+3] = -s_vec
|
|
39
|
+
row_idx += 1
|
|
40
|
+
return B_prim_ic
|
|
41
|
+
|
|
42
|
+
def _build_primitive_B_bends(self, geom_cart_3N):
|
|
43
|
+
"""
|
|
44
|
+
【TODO】Build B-matrix for all planar bends
|
|
45
|
+
Calculate B-matrix for all i-j-k tuples based on molecular connectivity.
|
|
46
|
+
"""
|
|
47
|
+
N_atoms = len(geom_cart_3N) // 3
|
|
48
|
+
self.log("Warning: _build_primitive_B_bends is not implemented.", force=True)
|
|
49
|
+
# Placeholder:
|
|
50
|
+
return np.zeros((0, 3 * N_atoms))
|
|
51
|
+
|
|
52
|
+
def _build_primitive_B_torsions(self, geom_cart_3N):
|
|
53
|
+
"""
|
|
54
|
+
【TODO】Build B-matrix for all proper torsions
|
|
55
|
+
Calculate B-matrix for all i-j-k-l tuples based on molecular connectivity.
|
|
56
|
+
"""
|
|
57
|
+
N_atoms = len(geom_cart_3N) // 3
|
|
58
|
+
self.log("Warning: _build_primitive_B_torsions is not implemented.", force=True)
|
|
59
|
+
# Placeholder:
|
|
60
|
+
return np.zeros((0, 3 * N_atoms))
|
|
61
|
+
|
|
62
|
+
def build_active_subspace(self, geom_cart_3N):
|
|
63
|
+
"""
|
|
64
|
+
Build B-matrix from stretches, bends, torsions based on the paper.
|
|
65
|
+
Removed Cartesian coordinates (B_CC).
|
|
66
|
+
"""
|
|
67
|
+
N_atoms = len(geom_cart_3N) // 3
|
|
68
|
+
if N_atoms == 0:
|
|
69
|
+
self.log("Warning: No atoms.", force=True); self.k=0; self.B_prim=np.zeros((0,0)); self.U=np.zeros((0,0)); self.Lambda_inv=np.zeros((0,0)); return 0
|
|
70
|
+
|
|
71
|
+
# Stack primitive internal coordinates according to the paper
|
|
72
|
+
B_Stretches = self._build_primitive_B_stretches(geom_cart_3N)
|
|
73
|
+
B_Bends = self._build_primitive_B_bends(geom_cart_3N)
|
|
74
|
+
B_Torsions = self._build_primitive_B_torsions(geom_cart_3N)
|
|
75
|
+
|
|
76
|
+
# Remove B_CC (Cartesian) and construct B_prim using only internal coordinates
|
|
77
|
+
self.B_prim = np.vstack((B_Stretches, B_Bends, B_Torsions))
|
|
78
|
+
|
|
79
|
+
if self.B_prim.shape[0] == 0:
|
|
80
|
+
self.log("FATAL: Primitive B-Matrix is empty. (Bends/Torsions not implemented?)", force=True)
|
|
81
|
+
# Fallback to Cartesian (differs from the paper's intent)
|
|
82
|
+
# self.B_prim = np.eye(3 * N_atoms)
|
|
83
|
+
raise ValueError("Primitive B-Matrix is empty. Implement Bends and Torsions.")
|
|
84
|
+
|
|
85
|
+
M_total, N_cart = self.B_prim.shape
|
|
86
|
+
self.log(f"Building G-Matrix ({M_total} x {M_total}) from {M_total} primitives...")
|
|
87
|
+
|
|
88
|
+
# G = B * B.T
|
|
89
|
+
G = np.dot(self.B_prim, self.B_prim.T); G = 0.5 * (G + G.T)
|
|
90
|
+
|
|
91
|
+
self.log("Diagonalizing G-Matrix...")
|
|
92
|
+
try:
|
|
93
|
+
eigvals_g, U_g = np.linalg.eigh(G)
|
|
94
|
+
except np.linalg.LinAlgError as e:
|
|
95
|
+
self.log(f"FATAL: G-matrix eigh failed: {e}. Check geom.", force=True); raise
|
|
96
|
+
|
|
97
|
+
active_indices = eigvals_g > self.g_tol; k = np.sum(active_indices)
|
|
98
|
+
|
|
99
|
+
# According to the paper, k should be 3N-6 (linear) or 3N-5 (non-linear)
|
|
100
|
+
expected_k = 3*N_atoms
|
|
101
|
+
if N_atoms == 1: expected_k = 3
|
|
102
|
+
elif N_atoms > 1: expected_k = 3*N_atoms - 6 # (Assuming non-linear for simplicity)
|
|
103
|
+
if expected_k < 1: expected_k = 1
|
|
104
|
+
|
|
105
|
+
if k == 0:
|
|
106
|
+
self.log("No active coords! Check g_tol/geom. Forcing k=1.", force=True); k = 1
|
|
107
|
+
active_indices = np.array([G.shape[0] - 1])
|
|
108
|
+
|
|
109
|
+
self.log(f"DIC: Found {k} active coordinates (Expected ~{expected_k})")
|
|
110
|
+
|
|
111
|
+
self.k = k
|
|
112
|
+
self.U = U_g[:, active_indices] # (n x k)
|
|
113
|
+
active_eigvals = eigvals_g[active_indices]
|
|
114
|
+
|
|
115
|
+
if np.any(active_eigvals <= 0):
|
|
116
|
+
num_neg = np.sum(active_eigvals <= 0); self.log(f"Warning: {num_neg} non-positive active eigvals. Clamping.", force=True)
|
|
117
|
+
active_eigvals[active_eigvals <= 0] = 1e-12
|
|
118
|
+
|
|
119
|
+
self.Lambda_inv = np.diag(1.0 / active_eigvals) # (k x k)
|
|
120
|
+
return k
|
|
121
|
+
|
|
122
|
+
# ===================================================================
|
|
123
|
+
# Coordinate Transformation Methods
|
|
124
|
+
# ===================================================================
|
|
125
|
+
|
|
126
|
+
def project_cart_to_dic(self, vec_cart_3N):
|
|
127
|
+
"""
|
|
128
|
+
Projects a Cartesian vector (3N,) to a DIC vector (k,).
|
|
129
|
+
g_q = T @ g_x = (Lambda_inv @ U.T @ B_prim) @ g_x
|
|
130
|
+
"""
|
|
131
|
+
if self.B_prim is None: raise ValueError("Coordinate system not built.")
|
|
132
|
+
# 1. B_prim @ g_x
|
|
133
|
+
vec_prim = np.dot(self.B_prim, vec_cart_3N)
|
|
134
|
+
# 2. U.T @ (B_prim @ g_x)
|
|
135
|
+
vec_dic_temp = np.dot(self.U.T, vec_prim)
|
|
136
|
+
# 3. Lambda_inv @ (U.T @ B_prim @ g_x)
|
|
137
|
+
vec_dic = np.dot(self.Lambda_inv, vec_dic_temp)
|
|
138
|
+
return vec_dic
|
|
139
|
+
|
|
140
|
+
def back_transform_dic_to_cart(self, vec_dic_k):
|
|
141
|
+
"""
|
|
142
|
+
Back-transforms a DIC vector (k,) to a Cartesian vector (3N,).
|
|
143
|
+
dx = T_dagger @ dq = (B_prim.T @ U @ Lambda_inv) @ dq
|
|
144
|
+
"""
|
|
145
|
+
if self.B_prim is None: raise ValueError("Coordinate system not built.")
|
|
146
|
+
# 1. Lambda_inv @ dq
|
|
147
|
+
tmp_vec = np.dot(self.Lambda_inv, vec_dic_k)
|
|
148
|
+
# 2. U @ (Lambda_inv @ dq)
|
|
149
|
+
vec_prim = np.dot(self.U, tmp_vec)
|
|
150
|
+
# 3. B_prim.T @ (U @ Lambda_inv @ dq)
|
|
151
|
+
vec_cart_3N = np.dot(self.B_prim.T, vec_prim)
|
|
152
|
+
return vec_cart_3N
|
|
153
|
+
|
|
154
|
+
def transform_hessian_cart_to_dic(self, H_cart_3N):
|
|
155
|
+
"""
|
|
156
|
+
Transforms a Cartesian Hessian (3N x 3N) to a DIC Hessian (k x k).
|
|
157
|
+
H_q = T @ H_x @ T_dagger = (Lambda_inv @ U.T @ B_prim) @ H_x @ (B_prim.T @ U @ Lambda_inv)
|
|
158
|
+
"""
|
|
159
|
+
if self.B_prim is None: raise ValueError("Coordinate system not built.")
|
|
160
|
+
k = self.k
|
|
161
|
+
|
|
162
|
+
if H_cart_3N is None:
|
|
163
|
+
self.log("No Cartesian Hessian provided, initializing DIC Hessian as Identity.", force=True)
|
|
164
|
+
return np.eye(k)
|
|
165
|
+
|
|
166
|
+
dim_cart = H_cart_3N.shape[0]
|
|
167
|
+
if dim_cart != self.B_prim.shape[1]:
|
|
168
|
+
self.log(f"ERROR: Cartesian Hessian dimension ({dim_cart}) mismatch with B_prim ({self.B_prim.shape[1]}). Using Identity.", force=True)
|
|
169
|
+
return np.eye(k)
|
|
170
|
+
|
|
171
|
+
self.log("Transforming Cartesian Hessian to DIC...", force=True)
|
|
172
|
+
try:
|
|
173
|
+
# T = Lambda_inv @ U.T @ B_prim
|
|
174
|
+
T_part1 = np.dot(self.U.T, self.B_prim) # (k x 3N)
|
|
175
|
+
T = np.dot(self.Lambda_inv, T_part1) # (k x 3N)
|
|
176
|
+
|
|
177
|
+
# T_dagger = B_prim.T @ U @ Lambda_inv
|
|
178
|
+
T_dagger_part1 = np.dot(self.B_prim.T, self.U) # (3N x k)
|
|
179
|
+
T_dagger = np.dot(T_dagger_part1, self.Lambda_inv) # (3N x k)
|
|
180
|
+
|
|
181
|
+
# H_q = T @ H_cart @ T_dagger
|
|
182
|
+
H_q_temp = np.dot(T, H_cart_3N) # (k x 3N)
|
|
183
|
+
H_q = np.dot(H_q_temp, T_dagger) # (k x k)
|
|
184
|
+
|
|
185
|
+
H_q = 0.5 * (H_q + H_q.T) # Ensure symmetry
|
|
186
|
+
self.log("Transformation complete.", force=True)
|
|
187
|
+
except Exception as e:
|
|
188
|
+
self.log(f"WARNING: Cartesian Hessian to DIC transformation failed: {e}. Using Identity.", force=True)
|
|
189
|
+
H_q = np.eye(k)
|
|
190
|
+
|
|
191
|
+
return H_q
|
|
192
|
+
|
|
193
|
+
# ===================================================================
|
|
194
|
+
# Modified DIC_RSIRFO Class
|
|
195
|
+
# ===================================================================
|
|
196
|
+
|
|
197
|
+
class DIC_RSIRFO:
|
|
198
|
+
def __init__(self, **config):
|
|
199
|
+
"""
|
|
200
|
+
Delocalized Internal Coordinates (DIC) RS-I-RFO Optimizer.
|
|
201
|
+
"""
|
|
202
|
+
# --- Common RSIRFO configuration ---
|
|
203
|
+
self.alpha0 = config.get("alpha0", 1.0)
|
|
204
|
+
self.max_micro_cycles = config.get("max_micro_cycles", 40)
|
|
205
|
+
self.saddle_order = config.get("saddle_order", 1)
|
|
206
|
+
self.hessian_update_method = config.get("method", "auto")
|
|
207
|
+
self.small_eigval_thresh = config.get("small_eigval_thresh", 1e-6)
|
|
208
|
+
self.alpha_max = config.get("alpha_max", 1e6)
|
|
209
|
+
self.alpha_step_max = config.get("alpha_step_max", 10.0)
|
|
210
|
+
if self.saddle_order == 0:
|
|
211
|
+
self.trust_radius_initial = config.get("trust_radius", 0.5)
|
|
212
|
+
self.trust_radius_max = config.get("trust_radius_max", 0.5)
|
|
213
|
+
else:
|
|
214
|
+
self.trust_radius_initial = config.get("trust_radius", 0.1)
|
|
215
|
+
self.trust_radius_max = config.get("trust_radius_max", 0.1)
|
|
216
|
+
self.trust_radius = self.trust_radius_initial
|
|
217
|
+
self.trust_radius_min = config.get("trust_radius_min", 0.01)
|
|
218
|
+
self.good_step_threshold = config.get("good_step_threshold", 0.75)
|
|
219
|
+
self.poor_step_threshold = config.get("poor_step_threshold", 0.25)
|
|
220
|
+
self.trust_radius_increase_factor = config.get("trust_radius_increase_factor", 1.2)
|
|
221
|
+
self.trust_radius_decrease_factor = config.get("trust_radius_decrease_factor", 0.5)
|
|
222
|
+
self.energy_change_threshold = config.get("energy_change_threshold", 1e-6)
|
|
223
|
+
self.gradient_norm_threshold = config.get("gradient_norm_threshold", 1e-4)
|
|
224
|
+
self.step_norm_tolerance = config.get("step_norm_tolerance", 1e-3)
|
|
225
|
+
self.debug_mode = config.get("debug_mode", False)
|
|
226
|
+
self.display_flag = config.get("display_flag", True)
|
|
227
|
+
self.Initialization = True
|
|
228
|
+
|
|
229
|
+
# --- Hessian storage (Cartesian) ---
|
|
230
|
+
self.hessian = None # Stores the INITIAL/CURRENT Cartesian Hessian (3N x 3N)
|
|
231
|
+
self.bias_hessian = None # Stores the Cartesian Bias Hessian (3N x 3N)
|
|
232
|
+
|
|
233
|
+
# --- DIC-specific storage ---
|
|
234
|
+
self.dic_hessian = None # The operational Hessian in DIC space (k x k)
|
|
235
|
+
self.dic_bias_hessian = None # The operational Bias Hessian in DIC space (k x k)
|
|
236
|
+
|
|
237
|
+
# --- State variables ---
|
|
238
|
+
self.prev_eigvec_min = None
|
|
239
|
+
self.prev_eigvec_size = None
|
|
240
|
+
self.predicted_energy_changes = []
|
|
241
|
+
self.actual_energy_changes = []
|
|
242
|
+
self.prev_geometry = None # Cartesian
|
|
243
|
+
self.prev_gradient = None # Cartesian
|
|
244
|
+
self.prev_energy = None
|
|
245
|
+
self.converged = False
|
|
246
|
+
self.iteration = 0
|
|
247
|
+
self.roots = list(range(self.saddle_order))
|
|
248
|
+
|
|
249
|
+
# --- Updaters and Helpers ---
|
|
250
|
+
self.hessian_updater = ModelHessianUpdate()
|
|
251
|
+
self.block_hessian_updater = BlockHessianUpdate()
|
|
252
|
+
self.alpha_init_values = [0.001 + (10.0 - 0.001) * i / 14 for i in range(15)]
|
|
253
|
+
self.NEB_mode = False
|
|
254
|
+
|
|
255
|
+
# --- DIC coordinate system ---
|
|
256
|
+
config["log_func"] = self.log
|
|
257
|
+
self.coord_system = InternalCoordinates(**config) # Instance to hold the coordinate system
|
|
258
|
+
|
|
259
|
+
def log(self, message, force=False):
|
|
260
|
+
display = getattr(self, 'display_flag', True)
|
|
261
|
+
debug = getattr(self, 'debug_mode', False)
|
|
262
|
+
if display and (force or debug):
|
|
263
|
+
print(message)
|
|
264
|
+
|
|
265
|
+
# === Main Methods ===
|
|
266
|
+
|
|
267
|
+
def run(self, geom_num_list, B_g, pre_B_g=[], pre_geom=[], B_e=0.0, pre_B_e=0.0, pre_move_vector=[], initial_geom_num_list=[], g=[], pre_g=[]):
|
|
268
|
+
self.log(f"\n{'='*50}\nDIC-RS-I-RFO Iteration {self.iteration}\n{'='*50}", force=True)
|
|
269
|
+
|
|
270
|
+
geom_cart_3N = np.asarray(geom_num_list).ravel()
|
|
271
|
+
g_cart_for_step = np.asarray(B_g).ravel()
|
|
272
|
+
|
|
273
|
+
# --- 1. Build Coordinate System (Once) ---
|
|
274
|
+
try:
|
|
275
|
+
# Build coordinate system only on the first step
|
|
276
|
+
if self.coord_system.B_prim is None or self.Initialization:
|
|
277
|
+
self.log("Building DIC coordinate system (first step)...", force=True)
|
|
278
|
+
k = self.coord_system.build_active_subspace(geom_cart_3N)
|
|
279
|
+
if k <= 0: raise ValueError("Invalid number of active coordinates (k<=0).")
|
|
280
|
+
|
|
281
|
+
# Initialize DIC Hessian
|
|
282
|
+
self.dic_hessian = self.coord_system.transform_hessian_cart_to_dic(self.hessian)
|
|
283
|
+
|
|
284
|
+
self.Initialization = False
|
|
285
|
+
self.predicted_energy_changes = []; self.actual_energy_changes = []
|
|
286
|
+
self.converged = False; self.iteration = 0
|
|
287
|
+
else:
|
|
288
|
+
k = self.coord_system.k # Use existing coordinate system
|
|
289
|
+
|
|
290
|
+
# --- 2. Hessian Update (Subsequent steps) ---
|
|
291
|
+
if self.prev_geometry is not None and len(pre_g) > 0 and len(pre_geom) > 0:
|
|
292
|
+
g_cart_for_update = np.asarray(g).ravel()
|
|
293
|
+
pre_g_cart_for_update = np.asarray(pre_g).ravel()
|
|
294
|
+
|
|
295
|
+
self.update_hessian(geom_cart_3N, g_cart_for_update,
|
|
296
|
+
np.asarray(pre_geom).ravel(), pre_g_cart_for_update)
|
|
297
|
+
|
|
298
|
+
# --- Trust Radius Update ---
|
|
299
|
+
if self.prev_energy is not None:
|
|
300
|
+
actual_energy_change = B_e - self.prev_energy
|
|
301
|
+
if len(self.actual_energy_changes) >= 3: self.actual_energy_changes.pop(0)
|
|
302
|
+
self.actual_energy_changes.append(actual_energy_change)
|
|
303
|
+
if self.predicted_energy_changes:
|
|
304
|
+
self.adjust_trust_radius(actual_energy_change, self.predicted_energy_changes[-1])
|
|
305
|
+
|
|
306
|
+
except Exception as e:
|
|
307
|
+
self.log(f"FATAL: DIC coordinate generation/update failed: {e}", force=True)
|
|
308
|
+
self.log("Aborting optimization.", force=True); self.converged = True
|
|
309
|
+
return np.zeros_like(geom_num_list).reshape(-1, 1)
|
|
310
|
+
|
|
311
|
+
# --- Bias Hessian Prep ---
|
|
312
|
+
if self.bias_hessian is not None and self.dic_bias_hessian is None:
|
|
313
|
+
self.log("Transforming Cartesian Bias Hessian to DIC...")
|
|
314
|
+
self.dic_bias_hessian = self.coord_system.transform_hessian_cart_to_dic(self.bias_hessian)
|
|
315
|
+
if self.dic_bias_hessian.shape[0] != k:
|
|
316
|
+
self.log(f"Warn: Transformed Bias shape != k. Ignoring.", force=True)
|
|
317
|
+
self.dic_bias_hessian = None
|
|
318
|
+
elif self.bias_hessian is None:
|
|
319
|
+
self.dic_bias_hessian = None
|
|
320
|
+
|
|
321
|
+
# --- 3. Convergence Check (Cartesian) ---
|
|
322
|
+
gradient_norm = np.linalg.norm(g_cart_for_step)
|
|
323
|
+
self.log(f"Gradient norm (Cartesian): {gradient_norm:.6f}", force=True)
|
|
324
|
+
if gradient_norm < self.gradient_norm_threshold:
|
|
325
|
+
self.log(f"Converged: Gradient norm {gradient_norm:.6f} < {self.gradient_norm_threshold:.6f}", force=True)
|
|
326
|
+
self.converged = True
|
|
327
|
+
|
|
328
|
+
if self.actual_energy_changes:
|
|
329
|
+
last_energy_change = abs(self.actual_energy_changes[-1])
|
|
330
|
+
if last_energy_change < self.energy_change_threshold:
|
|
331
|
+
self.log(f"Converged: Energy change {last_energy_change:.6f} < {self.energy_change_threshold:.6f}", force=True)
|
|
332
|
+
self.converged = True
|
|
333
|
+
|
|
334
|
+
if self.converged:
|
|
335
|
+
return np.zeros_like(geom_num_list).reshape(-1, 1)
|
|
336
|
+
|
|
337
|
+
# --- 4. RFO Step (in DIC space) ---
|
|
338
|
+
|
|
339
|
+
# g_q = T @ g_x
|
|
340
|
+
g_q = self.coord_system.project_cart_to_dic(g_cart_for_step)
|
|
341
|
+
|
|
342
|
+
H_q = self.dic_hessian
|
|
343
|
+
|
|
344
|
+
if self.dic_bias_hessian is not None:
|
|
345
|
+
if H_q.shape == self.dic_bias_hessian.shape:
|
|
346
|
+
H_q = H_q + self.dic_bias_hessian
|
|
347
|
+
else:
|
|
348
|
+
self.log(f"Warn: DIC Bias shape mismatch. Ignoring.", force=True)
|
|
349
|
+
|
|
350
|
+
H_q = 0.5 * (H_q + H_q.T)
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
eigvals_q, eigvecs_q = np.linalg.eigh(H_q)
|
|
354
|
+
except np.linalg.LinAlgError:
|
|
355
|
+
self.log("FATAL: DIC Hessian diagonalization failed. Using Identity.", force=True)
|
|
356
|
+
H_q = np.eye(k); self.dic_hessian = H_q
|
|
357
|
+
eigvals_q, eigvecs_q = np.linalg.eigh(H_q)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
if self.display_flag:
|
|
361
|
+
self.log(f"--- DIC Hessian Eigenvalues (k={k}) ---", force=True)
|
|
362
|
+
chunk_size = 6
|
|
363
|
+
for i in range(0, k, chunk_size):
|
|
364
|
+
chunk = eigvals_q[i:i + chunk_size]
|
|
365
|
+
line_str = " ".join([f"{val:10.6f}" for val in chunk])
|
|
366
|
+
self.log(f" {line_str}", force=True)
|
|
367
|
+
self.log(f"----------------------------------------", force=True)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
neg_eigvals = np.sum(eigvals_q < -1e-10)
|
|
372
|
+
self.log(f"Found {neg_eigvals} negative eigenvalues in DIC Hessian (target: {self.saddle_order})", force=True)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# (RFO/Image Projection logic - Unchanged)
|
|
377
|
+
P_q = np.eye(k)
|
|
378
|
+
root_num = 0; i = 0
|
|
379
|
+
while root_num < len(self.roots) and i < k:
|
|
380
|
+
if i < len(eigvals_q) and np.abs(eigvals_q[i]) > 1e-10:
|
|
381
|
+
trans_vec_q = eigvecs_q[:, i]
|
|
382
|
+
if self.NEB_mode: P_q -= np.outer(trans_vec_q, trans_vec_q)
|
|
383
|
+
else: P_q -= 2 * np.outer(trans_vec_q, trans_vec_q)
|
|
384
|
+
root_num += 1
|
|
385
|
+
elif i >= len(eigvals_q): self.log(f"Warn: Index i={i} out of bounds for eigvals_q ({len(eigvals_q)}).", force=True); break
|
|
386
|
+
i += 1
|
|
387
|
+
|
|
388
|
+
H_q_star = np.dot(P_q, H_q); H_q_star = 0.5 * (H_q_star + H_q_star.T)
|
|
389
|
+
g_q_star = np.dot(P_q, g_q)
|
|
390
|
+
eigvals_q_star, eigvecs_q_star = np.linalg.eigh(H_q_star)
|
|
391
|
+
eigvals_q_star_filt, eigvecs_q_star_filt = self.filter_small_eigvals(eigvals_q_star, eigvecs_q_star)
|
|
392
|
+
|
|
393
|
+
current_eigvec_size = eigvecs_q_star_filt.shape[1]
|
|
394
|
+
if current_eigvec_size == 0:
|
|
395
|
+
self.log("ERROR: No eigenvalues after filtering. Using steepest descent.", force=True)
|
|
396
|
+
step_q = -g_q
|
|
397
|
+
step_norm_q = np.linalg.norm(step_q)
|
|
398
|
+
if step_norm_q > self.trust_radius: step_q *= self.trust_radius / step_norm_q
|
|
399
|
+
else:
|
|
400
|
+
self.log(f"Using {current_eigvec_size} eigenvalues/vectors after filtering")
|
|
401
|
+
if self.prev_eigvec_size is not None and self.prev_eigvec_size != current_eigvec_size:
|
|
402
|
+
self.log(f"Resetting prev eigvec info (dim change: {self.prev_eigvec_size} -> {current_eigvec_size})")
|
|
403
|
+
self.prev_eigvec_min = None
|
|
404
|
+
|
|
405
|
+
g_q_star_in_filt_basis = np.dot(eigvecs_q_star_filt.T, g_q_star)
|
|
406
|
+
step_q_filt_trans = self.get_rs_step(eigvals_q_star_filt, g_q_star_in_filt_basis)
|
|
407
|
+
step_q = np.dot(eigvecs_q_star_filt, step_q_filt_trans)
|
|
408
|
+
self.prev_eigvec_size = current_eigvec_size
|
|
409
|
+
|
|
410
|
+
# --- 5. Back-transform & Save State ---
|
|
411
|
+
|
|
412
|
+
# dx = T_dagger @ dq
|
|
413
|
+
move_vector_cart_3N = self.coord_system.back_transform_dic_to_cart(step_q)
|
|
414
|
+
|
|
415
|
+
step_norm_cart = np.linalg.norm(move_vector_cart_3N)
|
|
416
|
+
max_step_allowed = 2.0 * self.trust_radius_max
|
|
417
|
+
if step_norm_cart > max_step_allowed:
|
|
418
|
+
self.log(f"Warn: Cart step norm {step_norm_cart:.4f} > limit {max_step_allowed:.4f}. Scaling.", force=True)
|
|
419
|
+
move_vector_cart_3N *= max_step_allowed / step_norm_cart
|
|
420
|
+
|
|
421
|
+
predicted_energy_change = self.rfo_model(g_q, H_q, step_q)
|
|
422
|
+
|
|
423
|
+
if len(self.predicted_energy_changes) >= 3: self.predicted_energy_changes.pop(0)
|
|
424
|
+
self.predicted_energy_changes.append(predicted_energy_change)
|
|
425
|
+
self.log(f"Predicted energy change (DIC): {predicted_energy_change:.6f}", force=True)
|
|
426
|
+
|
|
427
|
+
self.prev_geometry = geom_cart_3N
|
|
428
|
+
self.prev_gradient = g_cart_for_step
|
|
429
|
+
self.prev_energy = B_e
|
|
430
|
+
self.iteration += 1
|
|
431
|
+
|
|
432
|
+
return -1 * move_vector_cart_3N.reshape(-1, 1)
|
|
433
|
+
|
|
434
|
+
def update_hessian(self, current_geom_cart, current_grad_cart, previous_geom_cart, previous_grad_cart):
|
|
435
|
+
"""
|
|
436
|
+
Updates the DIC Hessian (self.dic_hessian).
|
|
437
|
+
Uses the *fixed* coordinate system (self.coord_system).
|
|
438
|
+
"""
|
|
439
|
+
|
|
440
|
+
# Check if coordinate system is built
|
|
441
|
+
if self.coord_system.B_prim is None or self.dic_hessian is None:
|
|
442
|
+
self.log("Warning: Coordinate system or DIC Hessian not ready. Skipping Hessian update.")
|
|
443
|
+
return
|
|
444
|
+
|
|
445
|
+
displacement_cart = np.asarray(current_geom_cart - previous_geom_cart).ravel()
|
|
446
|
+
delta_grad_cart = np.asarray(current_grad_cart - previous_grad_cart).ravel()
|
|
447
|
+
|
|
448
|
+
# 【P2 Fix】Use correct projection (T @ dx)
|
|
449
|
+
displacement_q = self.coord_system.project_cart_to_dic(displacement_cart)
|
|
450
|
+
delta_grad_q = self.coord_system.project_cart_to_dic(delta_grad_cart)
|
|
451
|
+
|
|
452
|
+
disp_norm = np.linalg.norm(displacement_q)
|
|
453
|
+
grad_diff_norm = np.linalg.norm(delta_grad_q)
|
|
454
|
+
if disp_norm < 1e-10 or grad_diff_norm < 1e-10:
|
|
455
|
+
self.log("Skipping Hessian update (DIC changes too small)")
|
|
456
|
+
return
|
|
457
|
+
|
|
458
|
+
dot_product_q = np.dot(displacement_q, delta_grad_q)
|
|
459
|
+
if dot_product_q <= 1e-8:
|
|
460
|
+
self.log(f"Skipping Hessian update (DIC poor alignment: dot={dot_product_q:.2e})")
|
|
461
|
+
return
|
|
462
|
+
|
|
463
|
+
self.log(f"Hessian update (DIC): disp_norm={disp_norm:.6f}, grad_diff_norm={grad_diff_norm:.6f}, dot={dot_product_q:.6f}")
|
|
464
|
+
|
|
465
|
+
displacement_q = displacement_q.reshape(-1, 1)
|
|
466
|
+
delta_grad_q = delta_grad_q.reshape(-1, 1)
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
# --- (This part is unchanged) Select and call the update method ---
|
|
470
|
+
method_name = self.hessian_update_method.lower()
|
|
471
|
+
delta_hess = None
|
|
472
|
+
|
|
473
|
+
if "flowchart" in method_name:
|
|
474
|
+
# ... (Logic below is same as original update_hessian) ...
|
|
475
|
+
self.log(f"Hessian update method: flowchart")
|
|
476
|
+
delta_hess = self.hessian_updater.flowchart_hessian_update(
|
|
477
|
+
self.dic_hessian, displacement_q, delta_grad_q, "auto"
|
|
478
|
+
)
|
|
479
|
+
elif "block_cfd_fsb" in method_name:
|
|
480
|
+
self.log(f"Hessian update method: block_cfd_fsb")
|
|
481
|
+
delta_hess = self.block_hessian_updater.block_CFD_FSB_hessian_update(
|
|
482
|
+
self.dic_hessian, displacement_q, delta_grad_q
|
|
483
|
+
)
|
|
484
|
+
elif "block_cfd_bofill" in method_name:
|
|
485
|
+
self.log(f"Hessian update method: block_cfd_bofill")
|
|
486
|
+
delta_hess = self.block_hessian_updater.block_CFD_Bofill_hessian_update(
|
|
487
|
+
self.dic_hessian, displacement_q, delta_grad_q
|
|
488
|
+
)
|
|
489
|
+
elif "block_bfgs" in method_name:
|
|
490
|
+
self.log(f"Hessian update method: block_bfgs")
|
|
491
|
+
delta_hess = self.block_hessian_updater.block_BFGS_hessian_update(
|
|
492
|
+
self.dic_hessian, displacement_q, delta_grad_q
|
|
493
|
+
)
|
|
494
|
+
elif "block_fsb" in method_name:
|
|
495
|
+
self.log(f"Hessian update method: block_fsb")
|
|
496
|
+
delta_hess = self.block_hessian_updater.block_FSB_hessian_update(
|
|
497
|
+
self.dic_hessian, displacement_q, delta_grad_q
|
|
498
|
+
)
|
|
499
|
+
elif "block_bofill" in method_name:
|
|
500
|
+
self.log(f"Hessian update method: block_bofill")
|
|
501
|
+
delta_hess = self.block_hessian_updater.block_Bofill_hessian_update(
|
|
502
|
+
self.dic_hessian, displacement_q, delta_grad_q
|
|
503
|
+
)
|
|
504
|
+
elif "bfgs" in method_name:
|
|
505
|
+
self.log(f"Hessian update method: bfgs")
|
|
506
|
+
delta_hess = self.hessian_updater.BFGS_hessian_update(
|
|
507
|
+
self.dic_hessian, displacement_q, delta_grad_q
|
|
508
|
+
)
|
|
509
|
+
elif "sr1" in method_name:
|
|
510
|
+
self.log(f"Hessian update method: sr1")
|
|
511
|
+
delta_hess = self.hessian_updater.SR1_hessian_update(
|
|
512
|
+
self.dic_hessian, displacement_q, delta_grad_q
|
|
513
|
+
)
|
|
514
|
+
elif "pcfd_bofill" in method_name:
|
|
515
|
+
self.log(f"Hessian update method: pcfd_bofill")
|
|
516
|
+
delta_hess = self.hessian_updater.pCFD_Bofill_hessian_update(
|
|
517
|
+
self.dic_hessian, displacement_q, delta_grad_q
|
|
518
|
+
)
|
|
519
|
+
elif "cfd_fsb" in method_name:
|
|
520
|
+
self.log(f"Hessian update method: cfd_fsb")
|
|
521
|
+
delta_hess = self.hessian_updater.CFD_FSB_hessian_update(
|
|
522
|
+
self.dic_hessian, displacement_q, delta_grad_q
|
|
523
|
+
)
|
|
524
|
+
elif "cfd_bofill" in method_name:
|
|
525
|
+
self.log(f"Hessian update method: cfd_bofill")
|
|
526
|
+
delta_hess = self.hessian_updater.CFD_Bofill_hessian_update(
|
|
527
|
+
self.dic_hessian, displacement_q, delta_grad_q
|
|
528
|
+
)
|
|
529
|
+
elif "fsb" in method_name:
|
|
530
|
+
self.log(f"Hessian update method: fsb")
|
|
531
|
+
delta_hess = self.hessian_updater.FSB_hessian_update(
|
|
532
|
+
self.dic_hessian, displacement_q, delta_grad_q
|
|
533
|
+
)
|
|
534
|
+
elif "bofill" in method_name:
|
|
535
|
+
self.log(f"Hessian update method: bofill")
|
|
536
|
+
delta_hess = self.hessian_updater.Bofill_hessian_update(
|
|
537
|
+
self.dic_hessian, displacement_q, delta_grad_q
|
|
538
|
+
)
|
|
539
|
+
elif "psb" in method_name:
|
|
540
|
+
self.log(f"Hessian update method: psb")
|
|
541
|
+
delta_hess = self.hessian_updater.PSB_hessian_update(
|
|
542
|
+
self.dic_hessian, displacement_q, delta_grad_q
|
|
543
|
+
)
|
|
544
|
+
elif "msp" in method_name:
|
|
545
|
+
self.log(f"Hessian update method: msp")
|
|
546
|
+
delta_hess = self.hessian_updater.MSP_hessian_update(
|
|
547
|
+
self.dic_hessian, displacement_q, delta_grad_q
|
|
548
|
+
)
|
|
549
|
+
else: # Default fallback
|
|
550
|
+
self.log(f"Unknown Hessian update method: '{self.hessian_update_method}'. Using flowchart/auto.")
|
|
551
|
+
delta_hess = self.hessian_updater.flowchart_hessian_update(
|
|
552
|
+
self.dic_hessian, displacement_q, delta_grad_q, "auto"
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
# --- Apply the update ---
|
|
556
|
+
if delta_hess is None:
|
|
557
|
+
self.log("Error: delta_hess is None. Skipping update.", force=True)
|
|
558
|
+
elif not np.all(np.isfinite(delta_hess)):
|
|
559
|
+
self.log("Warning: Hessian update resulted in non-finite values. Skipping update.", force=True)
|
|
560
|
+
else:
|
|
561
|
+
self.dic_hessian += delta_hess
|
|
562
|
+
self.dic_hessian = 0.5 * (self.dic_hessian + self.dic_hessian.T)
|
|
563
|
+
|
|
564
|
+
except Exception as e:
|
|
565
|
+
self.log(f"Error during Hessian update method call for '{method_name}': {e}. Skipping update.")
|
|
566
|
+
if self.debug_mode: raise e
|
|
567
|
+
|
|
568
|
+
# --- (Helper methods below are unchanged) ---
|
|
569
|
+
# (Unchanged helper methods are omitted for brevity)
|
|
570
|
+
# (Please copy from the original code)
|
|
571
|
+
|
|
572
|
+
def set_hessian(self, hessian_cart):
|
|
573
|
+
self.log("Cartesian Hessian received.")
|
|
574
|
+
if hessian_cart is not None:
|
|
575
|
+
self.hessian = np.asarray(hessian_cart)
|
|
576
|
+
else:
|
|
577
|
+
self.hessian = None
|
|
578
|
+
# DIC Hessian will be transformed in run() (if needed)
|
|
579
|
+
self.dic_hessian = None
|
|
580
|
+
# Reset coordinate system as well
|
|
581
|
+
self.coord_system = InternalCoordinates(**self.coord_system.__dict__)
|
|
582
|
+
self.Initialization = True
|
|
583
|
+
return
|
|
584
|
+
|
|
585
|
+
def get_hessian(self):
|
|
586
|
+
return self.hessian
|
|
587
|
+
|
|
588
|
+
def set_bias_hessian(self, bias_hessian_cart):
|
|
589
|
+
self.log("Cartesian Bias Hessian received.")
|
|
590
|
+
if bias_hessian_cart is not None:
|
|
591
|
+
self.bias_hessian = np.asarray(bias_hessian_cart)
|
|
592
|
+
else:
|
|
593
|
+
self.bias_hessian = None
|
|
594
|
+
self.dic_bias_hessian = None # Recalculated in run()
|
|
595
|
+
return
|
|
596
|
+
|
|
597
|
+
def get_bias_hessian(self):
|
|
598
|
+
return self.bias_hessian
|
|
599
|
+
|
|
600
|
+
# (Copy from original code)
|
|
601
|
+
def switch_NEB_mode(self):
|
|
602
|
+
if self.NEB_mode: self.NEB_mode = False
|
|
603
|
+
else: self.NEB_mode = True
|
|
604
|
+
|
|
605
|
+
def filter_small_eigvals(self, eigvals, eigvecs, mask=False):
|
|
606
|
+
small_inds = np.abs(eigvals) < self.small_eigval_thresh
|
|
607
|
+
small_num = np.sum(small_inds)
|
|
608
|
+
|
|
609
|
+
if small_num > 0:
|
|
610
|
+
self.log(f"Found {small_num} small eigenvalues. Removed corresponding.")
|
|
611
|
+
|
|
612
|
+
filtered_eigvals = eigvals[~small_inds]
|
|
613
|
+
filtered_eigvecs = eigvecs[:, ~small_inds]
|
|
614
|
+
|
|
615
|
+
if small_num > 6 and eigvals.shape[0] > 10: # Only warn if DoF is large
|
|
616
|
+
self.log(f"Warning: Found {small_num} small eigenvalues (>6).", force=True)
|
|
617
|
+
|
|
618
|
+
if mask:
|
|
619
|
+
return filtered_eigvals, filtered_eigvecs, small_inds
|
|
620
|
+
else:
|
|
621
|
+
return filtered_eigvals, filtered_eigvecs
|
|
622
|
+
|
|
623
|
+
def adjust_trust_radius(self, actual_change, predicted_change):
|
|
624
|
+
if abs(predicted_change) < 1e-10:
|
|
625
|
+
self.log("Skipping trust radius update: predicted change too small")
|
|
626
|
+
return
|
|
627
|
+
# Avoid division by zero if actual_change is also tiny
|
|
628
|
+
if abs(actual_change) < 1e-10:
|
|
629
|
+
ratio = 1.0 # Treat as perfect agreement
|
|
630
|
+
else:
|
|
631
|
+
ratio = actual_change / predicted_change
|
|
632
|
+
|
|
633
|
+
self.log(f"Energy change: actual={actual_change:.6f}, predicted={predicted_change:.6f}, ratio={ratio:.3f}", force=True)
|
|
634
|
+
old_trust_radius = self.trust_radius
|
|
635
|
+
|
|
636
|
+
if ratio > self.good_step_threshold:
|
|
637
|
+
self.trust_radius = min(self.trust_radius * self.trust_radius_increase_factor,
|
|
638
|
+
self.trust_radius_max)
|
|
639
|
+
if self.trust_radius != old_trust_radius:
|
|
640
|
+
self.log(f"Good step (ratio={ratio:.3f}), increasing trust radius to {self.trust_radius:.6f}", force=True)
|
|
641
|
+
elif ratio < self.poor_step_threshold:
|
|
642
|
+
self.trust_radius = max(self.trust_radius * self.trust_radius_decrease_factor,
|
|
643
|
+
self.trust_radius_min)
|
|
644
|
+
if self.trust_radius != old_trust_radius:
|
|
645
|
+
self.log(f"Poor step (ratio={ratio:.3f}), decreasing trust radius to {self.trust_radius:.6f}", force=True)
|
|
646
|
+
else:
|
|
647
|
+
self.log(f"Acceptable step (ratio={ratio:.3f}), keeping trust radius at {self.trust_radius:.6f}", force=True)
|
|
648
|
+
|
|
649
|
+
def evaluate_step_quality(self):
|
|
650
|
+
if len(self.predicted_energy_changes) < 2 or len(self.actual_energy_changes) < 2:
|
|
651
|
+
return "unknown"
|
|
652
|
+
ratios = []
|
|
653
|
+
for actual, predicted in zip(self.actual_energy_changes[-2:], self.predicted_energy_changes[-2:]):
|
|
654
|
+
if abs(predicted) > 1e-10:
|
|
655
|
+
if abs(actual) < 1e-10: # Handle tiny actual change
|
|
656
|
+
ratios.append(1.0)
|
|
657
|
+
else:
|
|
658
|
+
ratios.append(actual / predicted)
|
|
659
|
+
if not ratios: return "unknown"
|
|
660
|
+
avg_ratio = sum(ratios) / len(ratios)
|
|
661
|
+
# Check if steps generally decrease energy (allow small positive actual changes)
|
|
662
|
+
generally_downhill = all(a < 1e-6 or (a > 0 and abs(a/p) < 0.1)
|
|
663
|
+
for a, p in zip(self.actual_energy_changes[-2:], self.predicted_energy_changes[-2:]) if abs(p) > 1e-10)
|
|
664
|
+
|
|
665
|
+
if 0.8 < avg_ratio < 1.2 and generally_downhill: quality = "good"
|
|
666
|
+
elif 0.5 < avg_ratio < 1.5 and generally_downhill: quality = "acceptable"
|
|
667
|
+
else: quality = "poor"
|
|
668
|
+
self.log(f"Step quality assessment: {quality} (avg ratio: {avg_ratio:.3f})", force=True)
|
|
669
|
+
return quality
|
|
670
|
+
|
|
671
|
+
def get_rs_step(self, eigvals, gradient_trans):
|
|
672
|
+
try:
|
|
673
|
+
initial_step, _, _, _ = self.solve_rfo(eigvals, gradient_trans, self.alpha0)
|
|
674
|
+
initial_step_norm = np.linalg.norm(initial_step)
|
|
675
|
+
self.log(f"Initial step with alpha={self.alpha0:.6f} has norm={initial_step_norm:.6f}", force=True)
|
|
676
|
+
|
|
677
|
+
if initial_step_norm <= self.trust_radius:
|
|
678
|
+
self.log(f"Initial step is within trust radius ({self.trust_radius:.6f}), using it directly", force=True)
|
|
679
|
+
return initial_step # Return the step in the eigenvector basis
|
|
680
|
+
|
|
681
|
+
self.log(f"Initial step exceeds trust radius, optimizing alpha...", force=True)
|
|
682
|
+
except Exception as e:
|
|
683
|
+
self.log(f"Error calculating initial step: {str(e)}", force=True)
|
|
684
|
+
|
|
685
|
+
best_overall_step = None
|
|
686
|
+
best_overall_norm_diff = float('inf')
|
|
687
|
+
best_alpha_value = None
|
|
688
|
+
|
|
689
|
+
self.log(f"Trying {len(self.alpha_init_values)} different initial alpha values:", force=True)
|
|
690
|
+
|
|
691
|
+
for trial_idx, alpha_init in enumerate(self.alpha_init_values):
|
|
692
|
+
if self.debug_mode:
|
|
693
|
+
self.log(f"\n--- Alpha Trial {trial_idx+1}/{len(self.alpha_init_values)}: alpha_init = {alpha_init:.6f} ---")
|
|
694
|
+
|
|
695
|
+
try:
|
|
696
|
+
step_, step_norm, alpha_final = self.compute_rsprfo_step(
|
|
697
|
+
eigvals, gradient_trans, alpha_init
|
|
698
|
+
)
|
|
699
|
+
norm_diff = abs(step_norm - self.trust_radius)
|
|
700
|
+
|
|
701
|
+
if self.debug_mode:
|
|
702
|
+
self.log(f"Alpha trial {trial_idx+1}: ... step_norm={step_norm:.6f}, diff={norm_diff:.6f}")
|
|
703
|
+
|
|
704
|
+
is_better = False
|
|
705
|
+
if best_overall_step is None: is_better = True
|
|
706
|
+
elif step_norm <= self.trust_radius and best_overall_norm_diff > self.trust_radius: is_better = True
|
|
707
|
+
elif (step_norm <= self.trust_radius) == (best_overall_norm_diff <= self.trust_radius):
|
|
708
|
+
if norm_diff < best_overall_norm_diff: is_better = True
|
|
709
|
+
|
|
710
|
+
if is_better:
|
|
711
|
+
best_overall_step = step_
|
|
712
|
+
best_overall_norm_diff = norm_diff
|
|
713
|
+
best_alpha_value = alpha_init
|
|
714
|
+
|
|
715
|
+
except Exception as e:
|
|
716
|
+
if self.debug_mode:
|
|
717
|
+
self.log(f"Error in alpha trial {trial_idx+1}: {str(e)}")
|
|
718
|
+
|
|
719
|
+
if best_overall_step is None:
|
|
720
|
+
self.log("All alpha trials failed, using steepest descent step as fallback", force=True)
|
|
721
|
+
sd_step = -gradient_trans
|
|
722
|
+
sd_norm = np.linalg.norm(sd_step)
|
|
723
|
+
best_overall_step = sd_step / sd_norm * self.trust_radius if sd_norm > self.trust_radius else sd_step
|
|
724
|
+
else:
|
|
725
|
+
self.log(f"Selected alpha value: {best_alpha_value:.6f}", force=True)
|
|
726
|
+
|
|
727
|
+
step = best_overall_step
|
|
728
|
+
step_norm = np.linalg.norm(step)
|
|
729
|
+
self.log(f"Final norm(step)={step_norm:.6f}", force=True)
|
|
730
|
+
|
|
731
|
+
return step # Return the step in the eigenvector basis
|
|
732
|
+
|
|
733
|
+
def compute_rsprfo_step(self, eigvals, gradient_trans, alpha_init):
|
|
734
|
+
def calculate_step(alpha):
|
|
735
|
+
try:
|
|
736
|
+
step, eigval_min, _, _ = self.solve_rfo(eigvals, gradient_trans, alpha)
|
|
737
|
+
return step, eigval_min
|
|
738
|
+
except Exception as e:
|
|
739
|
+
self.log(f"Error in step calculation: {str(e)}")
|
|
740
|
+
raise
|
|
741
|
+
def step_norm_squared(alpha):
|
|
742
|
+
step, _ = calculate_step(alpha)
|
|
743
|
+
return np.dot(step, step)
|
|
744
|
+
def objective_function(alpha):
|
|
745
|
+
return step_norm_squared(alpha) - self.trust_radius**2
|
|
746
|
+
|
|
747
|
+
alpha_lo, alpha_hi = 1e-6, self.alpha_max
|
|
748
|
+
try:
|
|
749
|
+
step_lo, _ = calculate_step(alpha_lo)
|
|
750
|
+
obj_lo = np.dot(step_lo, step_lo) - self.trust_radius**2
|
|
751
|
+
step_hi, _ = calculate_step(alpha_hi)
|
|
752
|
+
obj_hi = np.dot(step_hi, step_hi) - self.trust_radius**2
|
|
753
|
+
|
|
754
|
+
self.log(f"Bracket search: alpha_lo={alpha_lo:.6e}, obj={obj_lo:.6e}")
|
|
755
|
+
self.log(f"Bracket search: alpha_hi={alpha_hi:.6e}, obj={obj_hi:.6e}")
|
|
756
|
+
|
|
757
|
+
if obj_lo * obj_hi >= 0:
|
|
758
|
+
self.log("Could not establish bracket, proceeding with Newton")
|
|
759
|
+
alpha = alpha_init
|
|
760
|
+
else:
|
|
761
|
+
self.log("Bracket established, using Brent's method")
|
|
762
|
+
try:
|
|
763
|
+
alpha = brentq(objective_function, alpha_lo, alpha_hi,
|
|
764
|
+
xtol=1e-6, rtol=1e-6, maxiter=50)
|
|
765
|
+
self.log(f"Brent's method converged to alpha={alpha:.6e}")
|
|
766
|
+
except Exception as e:
|
|
767
|
+
self.log(f"Brent's method failed: {str(e)}, using initial alpha")
|
|
768
|
+
alpha = alpha_init
|
|
769
|
+
except Exception as e:
|
|
770
|
+
self.log(f"Error establishing bracket: {str(e)}, using initial alpha")
|
|
771
|
+
alpha = alpha_init
|
|
772
|
+
|
|
773
|
+
alpha = alpha_init if 'alpha' not in locals() else alpha
|
|
774
|
+
step_norm_history = np.zeros(self.max_micro_cycles)
|
|
775
|
+
history_count = 0
|
|
776
|
+
best_step = None
|
|
777
|
+
best_step_norm_diff = float('inf')
|
|
778
|
+
alpha_left, alpha_right = None, None
|
|
779
|
+
|
|
780
|
+
for mu in range(self.max_micro_cycles):
|
|
781
|
+
self.log(f"RS-I-RFO micro cycle {mu:02d}, alpha={alpha:.6f}")
|
|
782
|
+
try:
|
|
783
|
+
step, eigval_min = calculate_step(alpha)
|
|
784
|
+
step_norm = np.linalg.norm(step)
|
|
785
|
+
self.log(f"norm(step)={step_norm:.6f}")
|
|
786
|
+
|
|
787
|
+
norm_diff = abs(step_norm - self.trust_radius)
|
|
788
|
+
if norm_diff < best_step_norm_diff:
|
|
789
|
+
if best_step is None:
|
|
790
|
+
best_step = step.copy()
|
|
791
|
+
else:
|
|
792
|
+
best_step = np.copyto(best_step, step)
|
|
793
|
+
best_step_norm_diff = norm_diff
|
|
794
|
+
|
|
795
|
+
objval = step_norm**2 - self.trust_radius**2
|
|
796
|
+
self.log(f"U(a)={objval:.6e}")
|
|
797
|
+
|
|
798
|
+
if objval < 0 and (alpha_left is None or alpha > alpha_left): alpha_left = alpha
|
|
799
|
+
elif objval > 0 and (alpha_right is None or alpha < alpha_right): alpha_right = alpha
|
|
800
|
+
|
|
801
|
+
if abs(objval) < 1e-8 or norm_diff < self.step_norm_tolerance:
|
|
802
|
+
self.log(f"Step norm {step_norm:.6f} is sufficiently close to trust radius")
|
|
803
|
+
if mu >= 1: break
|
|
804
|
+
|
|
805
|
+
if history_count < self.max_micro_cycles:
|
|
806
|
+
step_norm_history[history_count] = step_norm
|
|
807
|
+
history_count += 1
|
|
808
|
+
|
|
809
|
+
dstep2_dalpha = self.get_step_derivative(alpha, eigvals, gradient_trans,
|
|
810
|
+
step=step, eigval_min=eigval_min)
|
|
811
|
+
self.log(f"d(||step||^2)/dα={dstep2_dalpha:.6e}")
|
|
812
|
+
|
|
813
|
+
if abs(dstep2_dalpha) < 1e-10:
|
|
814
|
+
if alpha_left is not None and alpha_right is not None:
|
|
815
|
+
alpha_new = (alpha_left + alpha_right) / 2
|
|
816
|
+
self.log(f"Small derivative, using bisection")
|
|
817
|
+
else:
|
|
818
|
+
alpha_new = max(alpha / 2, 1e-6) if objval > 0 else min(alpha * 2, self.alpha_max)
|
|
819
|
+
self.log(f"Small derivative, no bracket, using heuristic")
|
|
820
|
+
else:
|
|
821
|
+
alpha_step_raw = -objval / dstep2_dalpha
|
|
822
|
+
alpha_step = np.clip(alpha_step_raw, -self.alpha_step_max, self.alpha_step_max)
|
|
823
|
+
if abs(alpha_step) != abs(alpha_step_raw):
|
|
824
|
+
self.log(f"Limited alpha step from {alpha_step_raw:.6f} to {alpha_step:.6f}")
|
|
825
|
+
alpha_new = alpha + alpha_step
|
|
826
|
+
if alpha_left is not None and alpha_right is not None:
|
|
827
|
+
alpha_new = max(min(alpha_new, alpha_right * 0.99), alpha_left * 1.01)
|
|
828
|
+
|
|
829
|
+
old_alpha = alpha
|
|
830
|
+
alpha = min(max(alpha_new, 1e-6), self.alpha_max)
|
|
831
|
+
self.log(f"Updated alpha: {old_alpha:.6f} -> {alpha:.6f}")
|
|
832
|
+
|
|
833
|
+
if alpha == self.alpha_max or alpha == 1e-6:
|
|
834
|
+
self.log(f"Alpha hit boundary at {alpha:.6e}, stopping iterations")
|
|
835
|
+
break
|
|
836
|
+
|
|
837
|
+
if history_count >= 3:
|
|
838
|
+
idx = history_count - 1
|
|
839
|
+
changes = [abs(step_norm_history[idx] - step_norm_history[idx-1]),
|
|
840
|
+
abs(step_norm_history[idx-1] - step_norm_history[idx-2])]
|
|
841
|
+
if all(c < 1e-6 for c in changes):
|
|
842
|
+
self.log("Step norm not changing significantly, stopping iterations")
|
|
843
|
+
break
|
|
844
|
+
except Exception as e:
|
|
845
|
+
self.log(f"Error in micro-cycle {mu}: {str(e)}")
|
|
846
|
+
if best_step is not None:
|
|
847
|
+
self.log("Using best step found so far due to error")
|
|
848
|
+
step, step_norm = best_step, np.linalg.norm(best_step)
|
|
849
|
+
break
|
|
850
|
+
else:
|
|
851
|
+
self.log("Falling back to steepest descent due to errors")
|
|
852
|
+
step = -gradient_trans
|
|
853
|
+
step_norm = np.linalg.norm(step)
|
|
854
|
+
if step_norm > self.trust_radius:
|
|
855
|
+
step = step / step_norm * self.trust_radius
|
|
856
|
+
step_norm = self.trust_radius
|
|
857
|
+
break
|
|
858
|
+
else:
|
|
859
|
+
self.log(f"RS-I-RFO did not converge in {self.max_micro_cycles} cycles")
|
|
860
|
+
if best_step is not None:
|
|
861
|
+
self.log("Using best step found during iterations")
|
|
862
|
+
step, step_norm = best_step, np.linalg.norm(best_step)
|
|
863
|
+
|
|
864
|
+
return step, step_norm, alpha
|
|
865
|
+
|
|
866
|
+
def get_step_derivative(self, alpha, eigvals, gradient_trans, step=None, eigval_min=None):
|
|
867
|
+
if step is None or eigval_min is None:
|
|
868
|
+
try:
|
|
869
|
+
step, eigval_min, _, _ = self.solve_rfo(eigvals, gradient_trans, alpha)
|
|
870
|
+
except Exception as e:
|
|
871
|
+
self.log(f"Error in step calculation for derivative: {str(e)}")
|
|
872
|
+
return 1e-8
|
|
873
|
+
|
|
874
|
+
try:
|
|
875
|
+
denominators = eigvals - eigval_min * alpha
|
|
876
|
+
small_denoms = np.abs(denominators) < 1e-8
|
|
877
|
+
if np.any(small_denoms):
|
|
878
|
+
safe_denoms = denominators.copy()
|
|
879
|
+
safe_denoms[small_denoms] = np.sign(safe_denoms[small_denoms]) * np.maximum(1e-8, np.abs(safe_denoms[small_denoms]))
|
|
880
|
+
zero_mask = safe_denoms[small_denoms] == 0
|
|
881
|
+
if np.any(zero_mask): safe_denoms[small_denoms][zero_mask] = 1e-8
|
|
882
|
+
denominators = safe_denoms
|
|
883
|
+
|
|
884
|
+
numerator = gradient_trans**2
|
|
885
|
+
denominator = denominators**3
|
|
886
|
+
valid_indices = np.abs(denominator) > 1e-10
|
|
887
|
+
if not np.any(valid_indices): return 1e-8
|
|
888
|
+
|
|
889
|
+
sum_terms = np.zeros_like(numerator)
|
|
890
|
+
sum_terms[valid_indices] = numerator[valid_indices] / denominator[valid_indices]
|
|
891
|
+
|
|
892
|
+
max_magnitude = 1e20
|
|
893
|
+
large_values = np.abs(sum_terms) > max_magnitude
|
|
894
|
+
if np.any(large_values):
|
|
895
|
+
sum_terms[large_values] = np.sign(sum_terms[large_values]) * max_magnitude
|
|
896
|
+
sum_term = np.sum(sum_terms)
|
|
897
|
+
|
|
898
|
+
dstep2_dalpha = 2.0 * eigval_min * sum_term
|
|
899
|
+
|
|
900
|
+
if not np.isfinite(dstep2_dalpha) or abs(dstep2_dalpha) > max_magnitude:
|
|
901
|
+
dstep2_dalpha = np.sign(dstep2_dalpha) * max_magnitude if dstep2_dalpha != 0 else 1e-8
|
|
902
|
+
|
|
903
|
+
return dstep2_dalpha
|
|
904
|
+
|
|
905
|
+
except Exception as e:
|
|
906
|
+
self.log(f"Error in derivative calculation: {str(e)}")
|
|
907
|
+
return 1e-8
|
|
908
|
+
|
|
909
|
+
def _solve_secular_equation(self, eigvals, grad_comps, alpha):
|
|
910
|
+
# 1. Prepare scaled values
|
|
911
|
+
eigvals_prime = eigvals / alpha
|
|
912
|
+
grad_comps_prime = grad_comps / alpha
|
|
913
|
+
grad_comps_prime_sq = grad_comps_prime**2
|
|
914
|
+
|
|
915
|
+
# 2. Define the secular function f(lambda)
|
|
916
|
+
def f(lambda_aug):
|
|
917
|
+
denoms = eigvals_prime - lambda_aug
|
|
918
|
+
# Strictly avoid division by zero
|
|
919
|
+
denoms[np.abs(denoms) < 1e-30] = np.sign(denoms[np.abs(denoms) < 1e-30]) * 1e-30
|
|
920
|
+
terms = grad_comps_prime_sq / denoms
|
|
921
|
+
return lambda_aug + np.sum(terms)
|
|
922
|
+
|
|
923
|
+
# --- 3. Robust bracket (asymptote) search ---
|
|
924
|
+
|
|
925
|
+
# Sort eigenvalues and rearrange corresponding gradients
|
|
926
|
+
sort_indices = np.argsort(eigvals_prime)
|
|
927
|
+
eigvals_sorted = eigvals_prime[sort_indices]
|
|
928
|
+
grad_comps_sorted_sq = grad_comps_prime_sq[sort_indices]
|
|
929
|
+
|
|
930
|
+
b_upper = None
|
|
931
|
+
min_eig_val_overall = eigvals_sorted[0] # Fallback value
|
|
932
|
+
|
|
933
|
+
# Find the "first" asymptote where the gradient is non-zero
|
|
934
|
+
for i in range(len(eigvals_sorted)):
|
|
935
|
+
if grad_comps_sorted_sq[i] > 1e-20: # Gradient is non-zero
|
|
936
|
+
# This is the first asymptote
|
|
937
|
+
b_upper = eigvals_sorted[i] - 1e-10
|
|
938
|
+
break
|
|
939
|
+
|
|
940
|
+
if b_upper is None:
|
|
941
|
+
# All gradient components are zero (already at a stationary point)
|
|
942
|
+
self.log("All gradient components in RFO space are zero.", force=True)
|
|
943
|
+
return 0.0 # Step will be zero
|
|
944
|
+
|
|
945
|
+
# --- 4. Set the lower bracket bound (b_lower) ---
|
|
946
|
+
g_norm_sq = np.sum(grad_comps_prime_sq)
|
|
947
|
+
# Add small constant to avoid b_lower == b_upper if g_norm_sq is tiny
|
|
948
|
+
b_lower = b_upper - (1e6 + g_norm_sq) # A robust heuristic lower bound
|
|
949
|
+
|
|
950
|
+
# --- 5. Check bracket validity ---
|
|
951
|
+
try:
|
|
952
|
+
f_upper = f(b_upper)
|
|
953
|
+
f_lower = f(b_lower)
|
|
954
|
+
except Exception as e:
|
|
955
|
+
self.log(f"f(lambda) calculation failed: {e}. Using fallback.", force=True)
|
|
956
|
+
return min_eig_val_overall - 1e-6 # Worst-case fallback
|
|
957
|
+
|
|
958
|
+
if f_lower * f_upper >= 0:
|
|
959
|
+
# Bracket is invalid (meaning f(b_upper) did not go to +inf or b_lower not low enough)
|
|
960
|
+
self.log(f"brentq bracket invalid: f(lower)={f_lower:.2e}, f(upper)={f_upper:.2e}", force=True)
|
|
961
|
+
|
|
962
|
+
# Try a much lower b_lower
|
|
963
|
+
b_lower = b_upper - 1e12 # Even lower
|
|
964
|
+
try:
|
|
965
|
+
f_lower = f(b_lower)
|
|
966
|
+
except Exception as e:
|
|
967
|
+
self.log(f"f(lambda) calculation failed for lower fallback: {e}. Using fallback.", force=True)
|
|
968
|
+
return min_eig_val_overall - 1e-6
|
|
969
|
+
|
|
970
|
+
if f_lower * f_upper >= 0:
|
|
971
|
+
self.log("FATAL: Could not find valid bracket. Using fallback.", force=True)
|
|
972
|
+
return min_eig_val_overall - 1e-6 # Worst-case fallback
|
|
973
|
+
|
|
974
|
+
# --- 6. Root finding ---
|
|
975
|
+
try:
|
|
976
|
+
root = brentq(f, b_lower, b_upper, xtol=1e-10, rtol=1e-10, maxiter=100)
|
|
977
|
+
return root
|
|
978
|
+
except Exception as e:
|
|
979
|
+
self.log(f"brentq failed: {e}. Using fallback.", force=True)
|
|
980
|
+
return min_eig_val_overall - 1e-6
|
|
981
|
+
|
|
982
|
+
def solve_rfo(self, eigvals, gradient_components, alpha, mode="min"):
|
|
983
|
+
if mode != "min":
|
|
984
|
+
raise NotImplementedError("Secular equation solver is only implemented for RFO minimization (mode='min')")
|
|
985
|
+
|
|
986
|
+
eigval_min = self._solve_secular_equation(eigvals, gradient_components, alpha)
|
|
987
|
+
denominators = (eigvals / alpha) - eigval_min
|
|
988
|
+
|
|
989
|
+
safe_denoms = denominators
|
|
990
|
+
small_denoms = np.abs(safe_denoms) < 1e-10
|
|
991
|
+
if np.any(small_denoms):
|
|
992
|
+
# Use copy() to avoid modifying original denominators if it's passed by reference elsewhere
|
|
993
|
+
safe_denoms = safe_denoms.copy()
|
|
994
|
+
safe_denoms[small_denoms] = np.sign(safe_denoms[small_denoms]) * np.maximum(1e-10, np.abs(safe_denoms[small_denoms]))
|
|
995
|
+
zero_mask = safe_denoms[small_denoms] == 0
|
|
996
|
+
if np.any(zero_mask): safe_denoms[small_denoms][zero_mask] = 1e-10
|
|
997
|
+
|
|
998
|
+
step = -(gradient_components / alpha) / safe_denoms
|
|
999
|
+
return step, eigval_min, 1.0, None
|
|
1000
|
+
|
|
1001
|
+
def rfo_model(self, gradient, hessian, step):
|
|
1002
|
+
return np.dot(gradient, step) + 0.5 * np.dot(step, np.dot(hessian, step))
|
|
1003
|
+
|
|
1004
|
+
def is_converged(self):
|
|
1005
|
+
return self.converged
|
|
1006
|
+
|
|
1007
|
+
def get_predicted_energy_changes(self):
|
|
1008
|
+
return self.predicted_energy_changes
|
|
1009
|
+
|
|
1010
|
+
def get_actual_energy_changes(self):
|
|
1011
|
+
return self.actual_energy_changes
|
|
1012
|
+
|
|
1013
|
+
def reset_trust_radius(self):
|
|
1014
|
+
self.trust_radius = self.trust_radius_initial
|
|
1015
|
+
self.log(f"Trust radius reset to initial value: {self.trust_radius:.6f}", force=True)
|