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,253 @@
|
|
|
1
|
+
from .linesearch import LineSearch
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
class LBFGS:
|
|
5
|
+
"""Limited-memory BFGS optimizer.
|
|
6
|
+
|
|
7
|
+
A limited memory version of the BFGS algorithm that approximates the inverse Hessian
|
|
8
|
+
matrix using a limited amount of memory. Unlike the standard BFGS algorithm,
|
|
9
|
+
LBFGS does not explicitly store the Hessian matrix, but instead builds it implicitly
|
|
10
|
+
from previous gradients and positions.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, **config):
|
|
14
|
+
# Configuration parameters
|
|
15
|
+
self.config = config
|
|
16
|
+
|
|
17
|
+
# Initialize flags
|
|
18
|
+
self.Initialization = True
|
|
19
|
+
self.linesearchflag = False
|
|
20
|
+
self.optimal_step_flag = False
|
|
21
|
+
|
|
22
|
+
# Set default parameters
|
|
23
|
+
self.DELTA = config.get("delta", 1.0) # Step size scaling factor
|
|
24
|
+
self.FC_COUNT = config.get("fc_count", -1) # Frequency of computing full Hessian
|
|
25
|
+
self.saddle_order = 0
|
|
26
|
+
self.iter = 0
|
|
27
|
+
self.memory = config.get("memory", 30) # Number of previous steps to remember
|
|
28
|
+
self.damping = config.get("damping", 0.75) # Damping factor for step size
|
|
29
|
+
self.alpha = config.get("alpha", 10.0) # Initial Hessian scaling
|
|
30
|
+
self.beta = config.get("beta", 0.1) # Momentum factor
|
|
31
|
+
|
|
32
|
+
# Storage for L-BFGS vectors
|
|
33
|
+
self.s = [] # Position differences
|
|
34
|
+
self.y = [] # Gradient differences
|
|
35
|
+
self.rho = [] # 1 / (y_k^T s_k)
|
|
36
|
+
|
|
37
|
+
# Initialize Hessian related variables
|
|
38
|
+
self.hessian = None # Not explicitly stored in L-BFGS
|
|
39
|
+
self.bias_hessian = None # For additional Hessian terms
|
|
40
|
+
self.H0 = 1.0 / self.alpha # Initial approximation of inverse Hessian
|
|
41
|
+
|
|
42
|
+
# For line search
|
|
43
|
+
self.prev_move_vector = None
|
|
44
|
+
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
def project_out_hess_tr_and_rot_for_coord(self, hessian, geometry):
|
|
48
|
+
"""Project out translation and rotation from Hessian.
|
|
49
|
+
|
|
50
|
+
This is only used for line search in this implementation.
|
|
51
|
+
"""
|
|
52
|
+
natoms = len(geometry)
|
|
53
|
+
|
|
54
|
+
geometry -= self.calc_center(geometry)
|
|
55
|
+
|
|
56
|
+
tr_x = (np.tile(np.array([1, 0, 0]), natoms)).reshape(-1, 3)
|
|
57
|
+
tr_y = (np.tile(np.array([0, 1, 0]), natoms)).reshape(-1, 3)
|
|
58
|
+
tr_z = (np.tile(np.array([0, 0, 1]), natoms)).reshape(-1, 3)
|
|
59
|
+
|
|
60
|
+
rot_x = np.cross(geometry, tr_x).flatten()
|
|
61
|
+
rot_y = np.cross(geometry, tr_y).flatten()
|
|
62
|
+
rot_z = np.cross(geometry, tr_z).flatten()
|
|
63
|
+
tr_x = tr_x.flatten()
|
|
64
|
+
tr_y = tr_y.flatten()
|
|
65
|
+
tr_z = tr_z.flatten()
|
|
66
|
+
|
|
67
|
+
TR_vectors = np.vstack([tr_x, tr_y, tr_z, rot_x, rot_y, rot_z])
|
|
68
|
+
|
|
69
|
+
Q, R = np.linalg.qr(TR_vectors.T)
|
|
70
|
+
keep_indices = ~np.isclose(np.diag(R), 0, atol=1e-6, rtol=0)
|
|
71
|
+
TR_vectors = Q.T[keep_indices]
|
|
72
|
+
n_tr = len(TR_vectors)
|
|
73
|
+
|
|
74
|
+
P = np.identity(natoms * 3)
|
|
75
|
+
for vector in TR_vectors:
|
|
76
|
+
P -= np.outer(vector, vector)
|
|
77
|
+
|
|
78
|
+
hess_proj = np.dot(np.dot(P.T, hessian), P)
|
|
79
|
+
|
|
80
|
+
return hess_proj
|
|
81
|
+
|
|
82
|
+
def calc_center(self, geometry, element_list=[]):
|
|
83
|
+
"""Calculate center of geometry."""
|
|
84
|
+
center = np.array([0.0, 0.0, 0.0], dtype="float64")
|
|
85
|
+
for i in range(len(geometry)):
|
|
86
|
+
center += geometry[i]
|
|
87
|
+
center /= float(len(geometry))
|
|
88
|
+
|
|
89
|
+
return center
|
|
90
|
+
|
|
91
|
+
def set_hessian(self, hessian):
|
|
92
|
+
"""Set explicit Hessian matrix (not used in LBFGS)."""
|
|
93
|
+
self.hessian = hessian
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
def set_bias_hessian(self, bias_hessian):
|
|
97
|
+
"""Set bias Hessian matrix."""
|
|
98
|
+
self.bias_hessian = bias_hessian
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
def get_hessian(self):
|
|
102
|
+
"""Get Hessian matrix.
|
|
103
|
+
|
|
104
|
+
Note: In LBFGS, the Hessian is not explicitly stored,
|
|
105
|
+
but this method is provided for compatibility.
|
|
106
|
+
"""
|
|
107
|
+
return self.hessian
|
|
108
|
+
|
|
109
|
+
def get_bias_hessian(self):
|
|
110
|
+
"""Get bias Hessian matrix."""
|
|
111
|
+
return self.bias_hessian
|
|
112
|
+
|
|
113
|
+
def update_vectors(self, displacement, delta_grad):
|
|
114
|
+
"""Update the vectors used for the L-BFGS approximation."""
|
|
115
|
+
# Flatten vectors
|
|
116
|
+
s = displacement.flatten()
|
|
117
|
+
y = delta_grad.flatten()
|
|
118
|
+
|
|
119
|
+
# Calculate rho = 1 / (y^T * s)
|
|
120
|
+
dot_product = np.dot(y, s)
|
|
121
|
+
if abs(dot_product) < 1e-10:
|
|
122
|
+
# Avoid division by very small numbers
|
|
123
|
+
rho = 1000.0
|
|
124
|
+
else:
|
|
125
|
+
rho = 1.0 / dot_product
|
|
126
|
+
|
|
127
|
+
# Add to history
|
|
128
|
+
self.s.append(s)
|
|
129
|
+
self.y.append(y)
|
|
130
|
+
self.rho.append(rho)
|
|
131
|
+
|
|
132
|
+
# Remove oldest vectors if exceeding memory limit
|
|
133
|
+
if len(self.s) > self.memory:
|
|
134
|
+
self.s.pop(0)
|
|
135
|
+
self.y.pop(0)
|
|
136
|
+
self.rho.pop(0)
|
|
137
|
+
|
|
138
|
+
def compute_lbfgs_direction(self, gradient):
|
|
139
|
+
"""Compute the search direction using the L-BFGS algorithm."""
|
|
140
|
+
# Flatten gradient
|
|
141
|
+
q = gradient.flatten()
|
|
142
|
+
|
|
143
|
+
# Number of vectors to use
|
|
144
|
+
loopmax = min(self.memory, len(self.s))
|
|
145
|
+
a = np.empty((loopmax,), dtype=np.float64)
|
|
146
|
+
|
|
147
|
+
# First loop: compute alpha_i = rho_i * s_i^T * q
|
|
148
|
+
for i in range(loopmax - 1, -1, -1):
|
|
149
|
+
a[i] = self.rho[i] * np.dot(self.s[i], q)
|
|
150
|
+
q = q - a[i] * self.y[i]
|
|
151
|
+
|
|
152
|
+
# Apply initial Hessian approximation: z = H_0 * q
|
|
153
|
+
z = self.H0 * q
|
|
154
|
+
|
|
155
|
+
# Second loop: compute search direction
|
|
156
|
+
for i in range(loopmax):
|
|
157
|
+
b = self.rho[i] * np.dot(self.y[i], z)
|
|
158
|
+
z = z + self.s[i] * (a[i] - b)
|
|
159
|
+
|
|
160
|
+
# Reshape to original gradient shape
|
|
161
|
+
z = z.reshape(gradient.shape)
|
|
162
|
+
|
|
163
|
+
return z
|
|
164
|
+
|
|
165
|
+
def determine_step(self, dr):
|
|
166
|
+
"""Determine step to take according to maxstep."""
|
|
167
|
+
if self.config.get("maxstep") is None:
|
|
168
|
+
return dr
|
|
169
|
+
|
|
170
|
+
# Get maxstep from config
|
|
171
|
+
maxstep = self.config.get("maxstep")
|
|
172
|
+
|
|
173
|
+
# Calculate step lengths
|
|
174
|
+
dr_reshaped = dr.reshape(-1, 3) if dr.size % 3 == 0 else dr.reshape(-1, dr.size)
|
|
175
|
+
steplengths = np.sqrt((dr_reshaped**2).sum(axis=1))
|
|
176
|
+
longest_step = np.max(steplengths)
|
|
177
|
+
|
|
178
|
+
# Scale step if necessary
|
|
179
|
+
if longest_step > maxstep:
|
|
180
|
+
dr = dr * (maxstep / longest_step)
|
|
181
|
+
|
|
182
|
+
return dr
|
|
183
|
+
|
|
184
|
+
def normal(self, geom_num_list, B_g, pre_B_g, pre_geom, B_e, pre_B_e, pre_g, g):
|
|
185
|
+
"""Normal L-BFGS optimization step."""
|
|
186
|
+
|
|
187
|
+
if self.linesearchflag:
|
|
188
|
+
print("linesearch mode")
|
|
189
|
+
else:
|
|
190
|
+
print("normal mode")
|
|
191
|
+
|
|
192
|
+
# First iteration - just return scaled gradient
|
|
193
|
+
if self.Initialization:
|
|
194
|
+
self.Initialization = False
|
|
195
|
+
return self.DELTA * B_g
|
|
196
|
+
|
|
197
|
+
# Calculate displacement and gradient difference
|
|
198
|
+
delta_grad = (g - pre_g).reshape(len(geom_num_list), 1)
|
|
199
|
+
displacement = (geom_num_list - pre_geom).reshape(len(geom_num_list), 1)
|
|
200
|
+
|
|
201
|
+
# Update L-BFGS vectors
|
|
202
|
+
self.update_vectors(displacement, delta_grad)
|
|
203
|
+
|
|
204
|
+
# Compute L-BFGS search direction
|
|
205
|
+
move_vector = self.compute_lbfgs_direction(B_g)
|
|
206
|
+
|
|
207
|
+
# Scale the step
|
|
208
|
+
move_vector = self.DELTA * move_vector
|
|
209
|
+
|
|
210
|
+
# Apply step size constraints
|
|
211
|
+
move_vector = self.determine_step(move_vector)
|
|
212
|
+
|
|
213
|
+
# Handle line search if enabled
|
|
214
|
+
if self.iter > 1 and self.linesearchflag:
|
|
215
|
+
# For line search, we need an explicit Hessian approximation
|
|
216
|
+
if self.hessian is not None:
|
|
217
|
+
tmp_hess = self.project_out_hess_tr_and_rot_for_coord(
|
|
218
|
+
self.hessian, geom_num_list.reshape(-1, 3)
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
tmp_hess = None
|
|
222
|
+
|
|
223
|
+
if self.optimal_step_flag or self.iter == 2:
|
|
224
|
+
self.LS = LineSearch(
|
|
225
|
+
self.prev_move_vector, move_vector,
|
|
226
|
+
B_g, pre_B_g, B_e, pre_B_e, tmp_hess
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
new_move_vector, self.optimal_step_flag = self.LS.linesearch(
|
|
230
|
+
self.prev_move_vector, move_vector,
|
|
231
|
+
B_g, pre_B_g, B_e, pre_B_e, tmp_hess
|
|
232
|
+
)
|
|
233
|
+
move_vector = new_move_vector
|
|
234
|
+
|
|
235
|
+
if self.optimal_step_flag or self.iter == 1:
|
|
236
|
+
self.prev_move_vector = move_vector
|
|
237
|
+
else:
|
|
238
|
+
self.optimal_step_flag = True
|
|
239
|
+
|
|
240
|
+
print("step size: ", self.DELTA, "\n")
|
|
241
|
+
|
|
242
|
+
self.iter += 1
|
|
243
|
+
return move_vector
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def run(self, geom_num_list, B_g, pre_B_g, pre_geom, B_e, pre_B_e, pre_move_vector, initial_geom_num_list, g, pre_g):
|
|
247
|
+
"""Run the optimization step."""
|
|
248
|
+
method = self.config.get("method", "lbfgs").lower()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
move_vector = self.normal(geom_num_list, B_g, pre_B_g, pre_geom, B_e, pre_B_e, pre_g, g)
|
|
252
|
+
|
|
253
|
+
return move_vector
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from numpy.linalg import norm
|
|
3
|
+
|
|
4
|
+
from multioptpy.Parameters.parameter import UnitValueLib
|
|
5
|
+
|
|
6
|
+
class LBFGS_NEB:
|
|
7
|
+
"""Limited-memory BFGS optimizer for Nudged Elastic Band calculations.
|
|
8
|
+
|
|
9
|
+
This implementation uses standard L-BFGS algorithm to approximate the inverse Hessian
|
|
10
|
+
matrix using a limited amount of memory, maintaining separate approximations
|
|
11
|
+
for each image in the NEB chain.
|
|
12
|
+
|
|
13
|
+
Step length is controlled through direct scaling rather than trust regions.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, **config):
|
|
17
|
+
# Configuration parameters
|
|
18
|
+
self.config = config
|
|
19
|
+
|
|
20
|
+
# Initialize flags
|
|
21
|
+
self.Initialization = True
|
|
22
|
+
self.iter = 0
|
|
23
|
+
|
|
24
|
+
# Set default parameters
|
|
25
|
+
self.memory = config.get("memory", 40) # Number of previous steps to remember
|
|
26
|
+
self.maxstep = config.get("maxstep", 0.1) # Maximum step size
|
|
27
|
+
|
|
28
|
+
# Scaling parameters
|
|
29
|
+
self.initial_step_scale = config.get("initial_step_scale", 1.0)
|
|
30
|
+
self.step_scale = self.initial_step_scale # Current step scaling factor
|
|
31
|
+
|
|
32
|
+
# Storage for L-BFGS history per image
|
|
33
|
+
self.s_images = [] # Position differences per image
|
|
34
|
+
self.y_images = [] # Force differences per image
|
|
35
|
+
self.rho_images = [] # 1 / (y_k^T s_k) per image
|
|
36
|
+
self.gamma_images = [] # Scaling factors per image
|
|
37
|
+
|
|
38
|
+
# Unit conversion
|
|
39
|
+
self.bohr2angstroms = UnitValueLib().bohr2angstroms
|
|
40
|
+
if 'bohr2angstroms' in config:
|
|
41
|
+
self.bohr2angstroms = config['bohr2angstroms']
|
|
42
|
+
|
|
43
|
+
self.TR_NEB = config.get("TR_NEB", None) # Keep this for compatibility
|
|
44
|
+
|
|
45
|
+
print(f"Initialized LBFGS_NEB optimizer with memory={self.memory}, "
|
|
46
|
+
f"maxstep={self.maxstep}")
|
|
47
|
+
|
|
48
|
+
def initialize_per_node_data(self, num_images):
|
|
49
|
+
"""Initialize data structures for per-node L-BFGS history management.
|
|
50
|
+
|
|
51
|
+
Parameters:
|
|
52
|
+
----------
|
|
53
|
+
num_images : int
|
|
54
|
+
Number of images in the NEB calculation
|
|
55
|
+
"""
|
|
56
|
+
# Initialize L-BFGS history storage for each image
|
|
57
|
+
self.s_images = [[] for _ in range(num_images)]
|
|
58
|
+
self.y_images = [[] for _ in range(num_images)]
|
|
59
|
+
self.rho_images = [[] for _ in range(num_images)]
|
|
60
|
+
self.gamma_images = [1.0] * num_images # Initial gamma value for each image
|
|
61
|
+
|
|
62
|
+
# Previous data for updates
|
|
63
|
+
self.prev_forces = [None] * num_images
|
|
64
|
+
self.prev_positions = [None] * num_images
|
|
65
|
+
self.prev_energies = [None] * num_images
|
|
66
|
+
|
|
67
|
+
print(f"Initialized L-BFGS history storage for {num_images} images")
|
|
68
|
+
|
|
69
|
+
def update_vectors(self, displacements, delta_forces):
|
|
70
|
+
"""Update the vectors used for the L-BFGS approximation for each image.
|
|
71
|
+
|
|
72
|
+
Parameters:
|
|
73
|
+
----------
|
|
74
|
+
displacements : list of ndarray
|
|
75
|
+
Position displacement vectors for each image
|
|
76
|
+
delta_forces : list of ndarray
|
|
77
|
+
Force difference vectors for each image
|
|
78
|
+
"""
|
|
79
|
+
# Initialize storage if this is the first update
|
|
80
|
+
if not self.s_images:
|
|
81
|
+
self.initialize_per_node_data(len(displacements))
|
|
82
|
+
|
|
83
|
+
# Update vectors for each image
|
|
84
|
+
for i, (s, y) in enumerate(zip(displacements, delta_forces)):
|
|
85
|
+
# Flatten vectors
|
|
86
|
+
s_flat = s.flatten()
|
|
87
|
+
y_flat = y.flatten()
|
|
88
|
+
|
|
89
|
+
# Calculate rho = 1 / (y^T * s)
|
|
90
|
+
dot_product = np.dot(y_flat, s_flat)
|
|
91
|
+
if abs(dot_product) < 1e-10:
|
|
92
|
+
# Avoid division by very small numbers
|
|
93
|
+
rho = 1000.0
|
|
94
|
+
print(f"Warning: Image {i} has very small y^T·s value ({dot_product:.2e})")
|
|
95
|
+
else:
|
|
96
|
+
rho = 1.0 / dot_product
|
|
97
|
+
|
|
98
|
+
# Update gamma (scaling factor for initial Hessian approximation)
|
|
99
|
+
y_norm_squared = np.dot(y_flat, y_flat)
|
|
100
|
+
if y_norm_squared > 1e-10:
|
|
101
|
+
self.gamma_images[i] = y_norm_squared / dot_product
|
|
102
|
+
print(f"Updated gamma for image {i} to {self.gamma_images[i]:.4f}")
|
|
103
|
+
|
|
104
|
+
# Add to history
|
|
105
|
+
self.s_images[i].append(s_flat)
|
|
106
|
+
self.y_images[i].append(y_flat)
|
|
107
|
+
self.rho_images[i].append(rho)
|
|
108
|
+
|
|
109
|
+
# Remove oldest vectors if exceeding memory limit
|
|
110
|
+
if len(self.s_images[i]) > self.memory:
|
|
111
|
+
self.s_images[i].pop(0)
|
|
112
|
+
self.y_images[i].pop(0)
|
|
113
|
+
self.rho_images[i].pop(0)
|
|
114
|
+
|
|
115
|
+
def compute_lbfgs_step(self, force, image_idx):
|
|
116
|
+
"""Compute the L-BFGS step direction for a single image.
|
|
117
|
+
|
|
118
|
+
Parameters:
|
|
119
|
+
----------
|
|
120
|
+
force : ndarray
|
|
121
|
+
Current force vector for the image
|
|
122
|
+
image_idx : int
|
|
123
|
+
Index of the image in the NEB chain
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
-------
|
|
127
|
+
ndarray
|
|
128
|
+
Step direction
|
|
129
|
+
"""
|
|
130
|
+
# Flatten force vector
|
|
131
|
+
f = force.flatten()
|
|
132
|
+
orig_shape = force.shape
|
|
133
|
+
|
|
134
|
+
# If we have no history yet for this image, just return force direction
|
|
135
|
+
if not self.s_images or image_idx >= len(self.s_images) or len(self.s_images[image_idx]) == 0:
|
|
136
|
+
return f.reshape(orig_shape)
|
|
137
|
+
|
|
138
|
+
# Get history for this image
|
|
139
|
+
s_list = self.s_images[image_idx]
|
|
140
|
+
y_list = self.y_images[image_idx]
|
|
141
|
+
rho_list = self.rho_images[image_idx]
|
|
142
|
+
gamma = self.gamma_images[image_idx]
|
|
143
|
+
|
|
144
|
+
# Apply two-loop recursion algorithm for L-BFGS
|
|
145
|
+
q = -f # Negative gradient (negative of negative force)
|
|
146
|
+
k = len(s_list)
|
|
147
|
+
alpha = np.zeros(k)
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
# First loop
|
|
151
|
+
for i in range(k-1, -1, -1):
|
|
152
|
+
alpha[i] = rho_list[i] * np.dot(s_list[i], q)
|
|
153
|
+
q = q - alpha[i] * y_list[i]
|
|
154
|
+
|
|
155
|
+
# Initial Hessian approximation
|
|
156
|
+
r = gamma * q
|
|
157
|
+
|
|
158
|
+
# Second loop
|
|
159
|
+
for i in range(k):
|
|
160
|
+
beta = rho_list[i] * np.dot(y_list[i], r)
|
|
161
|
+
r = r + s_list[i] * (alpha[i] - beta)
|
|
162
|
+
|
|
163
|
+
# r is now the product B^(-1) * g where B is the Hessian approximation
|
|
164
|
+
return r.reshape(orig_shape)
|
|
165
|
+
except (ValueError, np.linalg.LinAlgError) as e:
|
|
166
|
+
print(f"Error in L-BFGS calculation for image {image_idx}: {e}")
|
|
167
|
+
print("Falling back to steepest descent")
|
|
168
|
+
# Fallback to steepest descent
|
|
169
|
+
return f.reshape(orig_shape)
|
|
170
|
+
|
|
171
|
+
def determine_step(self, dr):
|
|
172
|
+
"""Determine step to take according to maxstep.
|
|
173
|
+
|
|
174
|
+
Parameters:
|
|
175
|
+
----------
|
|
176
|
+
dr : ndarray or list of ndarray
|
|
177
|
+
Step vector(s)
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
-------
|
|
181
|
+
ndarray or list of ndarray
|
|
182
|
+
Step vector(s) constrained by maxstep if necessary
|
|
183
|
+
"""
|
|
184
|
+
if self.maxstep is None:
|
|
185
|
+
return dr
|
|
186
|
+
|
|
187
|
+
# Handle single step vector or list of step vectors
|
|
188
|
+
if isinstance(dr, list):
|
|
189
|
+
result = []
|
|
190
|
+
for step in dr:
|
|
191
|
+
# Calculate step lengths
|
|
192
|
+
step_reshaped = step.reshape(-1, 3) if step.size % 3 == 0 else step
|
|
193
|
+
steplengths = np.sqrt((step_reshaped**2).sum(axis=1))
|
|
194
|
+
longest_step = np.max(steplengths)
|
|
195
|
+
|
|
196
|
+
# Scale step if necessary
|
|
197
|
+
if longest_step > self.maxstep:
|
|
198
|
+
result.append(step * (self.maxstep / longest_step))
|
|
199
|
+
else:
|
|
200
|
+
result.append(step)
|
|
201
|
+
return result
|
|
202
|
+
else:
|
|
203
|
+
# Single step vector
|
|
204
|
+
step_reshaped = dr.reshape(-1, 3) if dr.size % 3 == 0 else dr
|
|
205
|
+
steplengths = np.sqrt((step_reshaped**2).sum(axis=1))
|
|
206
|
+
longest_step = np.max(steplengths)
|
|
207
|
+
|
|
208
|
+
# Scale step if necessary
|
|
209
|
+
if longest_step > self.maxstep:
|
|
210
|
+
return dr * (self.maxstep / longest_step)
|
|
211
|
+
return dr
|
|
212
|
+
|
|
213
|
+
def adjust_step_scale(self, energies, prev_energies):
|
|
214
|
+
"""Adjust the global step scaling factor based on energy changes.
|
|
215
|
+
|
|
216
|
+
Parameters:
|
|
217
|
+
----------
|
|
218
|
+
energies : list of float
|
|
219
|
+
Current energies for all images
|
|
220
|
+
prev_energies : list of float
|
|
221
|
+
Previous energies for all images
|
|
222
|
+
"""
|
|
223
|
+
if prev_energies is None or None in prev_energies:
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
# Calculate energy changes
|
|
227
|
+
energy_changes = [prev - curr for prev, curr in zip(prev_energies, energies) if prev is not None and curr is not None]
|
|
228
|
+
|
|
229
|
+
if not energy_changes:
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
# Count improvements and deteriorations
|
|
233
|
+
improvements = sum(1 for change in energy_changes if change > 0)
|
|
234
|
+
deteriorations = sum(1 for change in energy_changes if change < 0)
|
|
235
|
+
|
|
236
|
+
# Adjust scale based on overall improvement ratio
|
|
237
|
+
if improvements > deteriorations:
|
|
238
|
+
# More improvements than deteriorations - increase step scale
|
|
239
|
+
self.step_scale = min(self.step_scale * 1.1, 2.0)
|
|
240
|
+
print(f"Energy improved for {improvements} images, increasing step scale to {self.step_scale:.4f}")
|
|
241
|
+
elif deteriorations > improvements:
|
|
242
|
+
# More deteriorations than improvements - decrease step scale
|
|
243
|
+
self.step_scale = max(self.step_scale * 0.5, 0.1)
|
|
244
|
+
print(f"Energy deteriorated for {deteriorations} images, decreasing step scale to {self.step_scale:.4f}")
|
|
245
|
+
else:
|
|
246
|
+
# Equal number - maintain current scale
|
|
247
|
+
print(f"Maintaining current step scale at {self.step_scale:.4f}")
|
|
248
|
+
|
|
249
|
+
def LBFGS_NEB_calc(self, geometry_num_list, total_force_list, pre_total_velocity,
|
|
250
|
+
optimize_num, total_velocity, cos_list,
|
|
251
|
+
biased_energy_list, pre_biased_energy_list, pre_geom):
|
|
252
|
+
"""Calculate step using standard L-BFGS for NEB.
|
|
253
|
+
|
|
254
|
+
Parameters:
|
|
255
|
+
----------
|
|
256
|
+
geometry_num_list : ndarray
|
|
257
|
+
Current geometry coordinates for all images
|
|
258
|
+
total_force_list : ndarray
|
|
259
|
+
Current forces for all images
|
|
260
|
+
pre_total_velocity : ndarray
|
|
261
|
+
Previous velocities for all images
|
|
262
|
+
optimize_num : int
|
|
263
|
+
Current optimization iteration number
|
|
264
|
+
total_velocity : ndarray
|
|
265
|
+
Current velocities for all images
|
|
266
|
+
cos_list : ndarray
|
|
267
|
+
Cosines between adjacent images
|
|
268
|
+
biased_energy_list : ndarray
|
|
269
|
+
Current energy for all images
|
|
270
|
+
pre_biased_energy_list : ndarray
|
|
271
|
+
Previous energy for all images
|
|
272
|
+
pre_geom : ndarray
|
|
273
|
+
Previous geometry coordinates for all images
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
-------
|
|
277
|
+
ndarray
|
|
278
|
+
Updated geometry coordinates for all images
|
|
279
|
+
"""
|
|
280
|
+
print(f"\n{'='*50}\nNEB-LBFGS Iteration {self.iter}\n{'='*50}")
|
|
281
|
+
|
|
282
|
+
# Get number of images
|
|
283
|
+
num_images = len(geometry_num_list)
|
|
284
|
+
|
|
285
|
+
# Initialize data structures if first iteration
|
|
286
|
+
if self.Initialization:
|
|
287
|
+
self.initialize_per_node_data(num_images)
|
|
288
|
+
self.Initialization = False
|
|
289
|
+
print("First iteration")
|
|
290
|
+
|
|
291
|
+
# Adjust step scale based on energy changes (if not first iteration)
|
|
292
|
+
if not self.Initialization and self.iter > 0 and self.prev_energies[0] is not None:
|
|
293
|
+
self.adjust_step_scale(biased_energy_list, self.prev_energies)
|
|
294
|
+
|
|
295
|
+
# Store current energies for next iteration
|
|
296
|
+
self.prev_energies = biased_energy_list.copy()
|
|
297
|
+
|
|
298
|
+
# List to store move vectors for each image
|
|
299
|
+
move_vectors = []
|
|
300
|
+
|
|
301
|
+
# Process each image
|
|
302
|
+
for i, force in enumerate(total_force_list):
|
|
303
|
+
# Compute L-BFGS step direction
|
|
304
|
+
step_direction = self.compute_lbfgs_step(force, i)
|
|
305
|
+
|
|
306
|
+
# Scale step by the current step scaling factor
|
|
307
|
+
step = self.step_scale * step_direction
|
|
308
|
+
|
|
309
|
+
move_vectors.append(step)
|
|
310
|
+
|
|
311
|
+
# Apply maxstep constraint if needed
|
|
312
|
+
move_vectors = self.determine_step(move_vectors)
|
|
313
|
+
|
|
314
|
+
# If this is not the first iteration, update L-BFGS vectors
|
|
315
|
+
if optimize_num > 0:
|
|
316
|
+
# Calculate displacement and force difference for each image
|
|
317
|
+
displacements = []
|
|
318
|
+
delta_forces = []
|
|
319
|
+
|
|
320
|
+
for i in range(num_images):
|
|
321
|
+
if pre_geom is not None and pre_total_velocity is not None and len(pre_total_velocity) > 0:
|
|
322
|
+
# Get previous force if available
|
|
323
|
+
if len(pre_total_velocity) > i:
|
|
324
|
+
prev_force = pre_total_velocity[i]
|
|
325
|
+
curr_force = total_force_list[i]
|
|
326
|
+
delta_force = curr_force - prev_force
|
|
327
|
+
|
|
328
|
+
# Get displacement
|
|
329
|
+
curr_geom = geometry_num_list[i]
|
|
330
|
+
prev_geom = pre_geom[i]
|
|
331
|
+
displacement = curr_geom - prev_geom
|
|
332
|
+
|
|
333
|
+
displacements.append(displacement)
|
|
334
|
+
delta_forces.append(delta_force)
|
|
335
|
+
|
|
336
|
+
if displacements and delta_forces:
|
|
337
|
+
self.update_vectors(displacements, delta_forces)
|
|
338
|
+
|
|
339
|
+
# Apply trust region correction if TR_NEB is provided for compatibility
|
|
340
|
+
if self.TR_NEB:
|
|
341
|
+
move_vectors = self.TR_NEB.TR_calc(geometry_num_list, total_force_list, move_vectors,
|
|
342
|
+
biased_energy_list, pre_biased_energy_list, pre_geom)
|
|
343
|
+
|
|
344
|
+
# Update geometry using move vectors
|
|
345
|
+
new_geometry = []
|
|
346
|
+
for i, (geom, move) in enumerate(zip(geometry_num_list, move_vectors)):
|
|
347
|
+
new_geom = geom + move
|
|
348
|
+
new_geometry.append(new_geom)
|
|
349
|
+
|
|
350
|
+
# Convert to numpy array and apply unit conversion
|
|
351
|
+
new_geometry = np.array(new_geometry)
|
|
352
|
+
new_geometry *= self.bohr2angstroms
|
|
353
|
+
|
|
354
|
+
self.iter += 1
|
|
355
|
+
return new_geometry
|