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,633 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.linalg import cholesky, cho_solve, LinAlgError
|
|
3
|
+
from scipy.optimize import minimize
|
|
4
|
+
from scipy.spatial.distance import pdist, squareform
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
class GPmin:
|
|
8
|
+
def __init__(self, **config):
|
|
9
|
+
"""
|
|
10
|
+
Reference: https://doi.org/10.1063/1.5009347
|
|
11
|
+
|
|
12
|
+
Gaussian Process Minimizer (GPmin) with inverse distance coordinates.
|
|
13
|
+
Uses 1/R internal coordinates as input to predict energy and Cartesian forces.
|
|
14
|
+
|
|
15
|
+
Uses RBF kernel and performs surrogate PES optimization with improved step size control.
|
|
16
|
+
"""
|
|
17
|
+
# GP parameters
|
|
18
|
+
self.n_points = 0
|
|
19
|
+
self.kernel_func = 'RBF'
|
|
20
|
+
self.noise_energy = 1e-8
|
|
21
|
+
self.length_scale = 0.5
|
|
22
|
+
self.sigma_f = 1.0
|
|
23
|
+
|
|
24
|
+
# Trust radius parameters - larger values for bigger steps
|
|
25
|
+
self.trust_radius = 1.0 # Increased initial trust radius
|
|
26
|
+
self.trust_radius_max = 5.0 # Increased maximum trust radius
|
|
27
|
+
self.trust_radius_min = 2.0 # Minimum trust radius
|
|
28
|
+
self.trust_increase = 5.0 # Larger trust radius increase factor
|
|
29
|
+
self.trust_decrease = 0.2 # Trust radius decrease factor
|
|
30
|
+
|
|
31
|
+
# Step size control
|
|
32
|
+
self.min_step_size = 0.1 # Minimum allowed step size for non-converged systems
|
|
33
|
+
self.step_scale = 1.0 # Global scale factor for step sizes
|
|
34
|
+
self.force_scale_factor = 0.3 # Scaling factor for force-based steps
|
|
35
|
+
self.adaptive_step = True # Enable adaptive step sizing
|
|
36
|
+
|
|
37
|
+
# Internal coordinates parameters
|
|
38
|
+
self.min_dist = 1e-7 # Minimum distance to avoid singularity in 1/R
|
|
39
|
+
self.dist_scale = 1.0 # Scaling factor for distances
|
|
40
|
+
|
|
41
|
+
# Surrogate PES optimization parameters
|
|
42
|
+
self.surrogate_maxiter = 20 # Maximum iterations for surrogate optimization
|
|
43
|
+
self.surrogate_ftol = 1e-1 # Function tolerance for surrogate optimization
|
|
44
|
+
self.surrogate_gtol = 1e-1 # Gradient tolerance for surrogate optimization
|
|
45
|
+
|
|
46
|
+
# Data selection parameters
|
|
47
|
+
self.max_gp_pts = 100
|
|
48
|
+
self.selection_method = 'recent' # 'recent', 'best', 'diverse'
|
|
49
|
+
|
|
50
|
+
# Optimization parameters
|
|
51
|
+
self.alpha = 0.3 # Increased step size factor (was 0.1)
|
|
52
|
+
|
|
53
|
+
# Flags
|
|
54
|
+
self.display_flag = True
|
|
55
|
+
self.Initialization = True
|
|
56
|
+
|
|
57
|
+
# Override defaults with provided config
|
|
58
|
+
for key, value in config.items():
|
|
59
|
+
if hasattr(self, key):
|
|
60
|
+
setattr(self, key, value)
|
|
61
|
+
self.hessian = None
|
|
62
|
+
self.bias_hessian = None
|
|
63
|
+
|
|
64
|
+
def run(self, geom_num_list, B_g, pre_B_g=[], pre_geom=[], B_e=0.0, pre_B_e=0.0,
|
|
65
|
+
pre_move_vector=[], initial_geom_num_list=[], g=[], pre_g=[]):
|
|
66
|
+
"""
|
|
67
|
+
Perform one optimization step using GP regression with inverse distance coordinates.
|
|
68
|
+
"""
|
|
69
|
+
# Start timer for performance measurement
|
|
70
|
+
start_time = time.time()
|
|
71
|
+
|
|
72
|
+
# Convert inputs to numpy arrays
|
|
73
|
+
geom_array = np.asarray(geom_num_list)
|
|
74
|
+
geom_shape = geom_array.shape
|
|
75
|
+
geom_flat = geom_array.flatten()
|
|
76
|
+
forces_flat = -np.asarray(B_g).flatten() # Forces are negative of gradients
|
|
77
|
+
|
|
78
|
+
if self.Initialization:
|
|
79
|
+
if self.display_flag:
|
|
80
|
+
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] Initializing GPmin with RBF kernel")
|
|
81
|
+
print(f"Trust radius: {self.trust_radius:.3f}, Max trust radius: {self.trust_radius_max:.3f}")
|
|
82
|
+
|
|
83
|
+
# Initialize data structures
|
|
84
|
+
self.dim = len(geom_flat)
|
|
85
|
+
self.n_atoms = len(geom_flat) // 3
|
|
86
|
+
self.geom_shape = geom_shape
|
|
87
|
+
|
|
88
|
+
# Calculate number of distances (n_atoms choose 2)
|
|
89
|
+
self.n_dist = self.n_atoms * (self.n_atoms - 1) // 2
|
|
90
|
+
|
|
91
|
+
# Convert to internal coordinates (inverse distances)
|
|
92
|
+
coords_3d = geom_array.reshape(-1, 3)
|
|
93
|
+
inv_dist = self._cart_to_inverse_dist(coords_3d)
|
|
94
|
+
|
|
95
|
+
# Store historical data
|
|
96
|
+
self.X_cart_all = np.array([geom_flat]) # Cartesian positions
|
|
97
|
+
self.X_all = np.array([inv_dist]) # Inverse distances
|
|
98
|
+
self.Y_all = np.array([float(B_e)]) # Energies
|
|
99
|
+
self.F_all = np.array([forces_flat]) # Forces (Cartesian)
|
|
100
|
+
|
|
101
|
+
# Selected training data
|
|
102
|
+
self.X = np.array([inv_dist])
|
|
103
|
+
self.Y = np.array([float(B_e)])
|
|
104
|
+
self.X_cart = np.array([geom_flat])
|
|
105
|
+
self.F = np.array([forces_flat])
|
|
106
|
+
|
|
107
|
+
# Store Jacobians for coordinate conversion
|
|
108
|
+
self.jacobians = [self._calc_jacobian(coords_3d)]
|
|
109
|
+
|
|
110
|
+
# Initialize GP model
|
|
111
|
+
self._init_gp()
|
|
112
|
+
self.Initialization = False
|
|
113
|
+
self.n_points = 1
|
|
114
|
+
|
|
115
|
+
# First step: take larger force-based step
|
|
116
|
+
force_norm = np.linalg.norm(forces_flat)
|
|
117
|
+
if force_norm > 1e-10:
|
|
118
|
+
# Use alpha * force direction * force_scale_factor
|
|
119
|
+
move_vector_flat = self.alpha * (forces_flat / force_norm) * self.force_scale_factor
|
|
120
|
+
|
|
121
|
+
# Scale the step to be at least min_step_size
|
|
122
|
+
step_size = np.linalg.norm(move_vector_flat)
|
|
123
|
+
if step_size < self.min_step_size:
|
|
124
|
+
move_vector_flat *= self.min_step_size / step_size
|
|
125
|
+
|
|
126
|
+
# Cap at trust radius
|
|
127
|
+
step_size = np.linalg.norm(move_vector_flat)
|
|
128
|
+
if step_size > self.trust_radius:
|
|
129
|
+
move_vector_flat *= self.trust_radius / step_size
|
|
130
|
+
else:
|
|
131
|
+
# If forces are nearly zero, use small random step
|
|
132
|
+
move_vector_flat = np.random.normal(0, 0.1, size=self.dim)
|
|
133
|
+
move_vector_flat *= self.min_step_size / np.linalg.norm(move_vector_flat)
|
|
134
|
+
else:
|
|
135
|
+
# Update data with new point
|
|
136
|
+
coords_3d = geom_array.reshape(-1, 3)
|
|
137
|
+
inv_dist = self._cart_to_inverse_dist(coords_3d)
|
|
138
|
+
jacobian = self._calc_jacobian(coords_3d)
|
|
139
|
+
|
|
140
|
+
self._update_data(geom_flat, inv_dist, jacobian, float(B_e), forces_flat)
|
|
141
|
+
|
|
142
|
+
# Optimize on the surrogate PES with improved step size control
|
|
143
|
+
move_vector_flat = self._optimize_on_surrogate_pes(geom_flat, forces_flat, float(B_e), inv_dist, jacobian)
|
|
144
|
+
|
|
145
|
+
# Apply adaptive step sizing if enabled
|
|
146
|
+
if self.adaptive_step:
|
|
147
|
+
move_vector_flat = self._adapt_step_size(move_vector_flat, forces_flat)
|
|
148
|
+
|
|
149
|
+
# Apply global step scale factor
|
|
150
|
+
move_vector_flat *= self.step_scale
|
|
151
|
+
|
|
152
|
+
# Final check on step size (trust radius constraint)
|
|
153
|
+
step_size = np.linalg.norm(move_vector_flat)
|
|
154
|
+
if step_size > self.trust_radius:
|
|
155
|
+
move_vector_flat *= self.trust_radius / step_size
|
|
156
|
+
|
|
157
|
+
# Reshape move vector to match input geometry shape
|
|
158
|
+
move_vector = move_vector_flat.reshape(geom_shape)
|
|
159
|
+
|
|
160
|
+
# Calculate elapsed time
|
|
161
|
+
elapsed_time = time.time() - start_time
|
|
162
|
+
|
|
163
|
+
if self.display_flag:
|
|
164
|
+
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] GPmin with RBF kernel")
|
|
165
|
+
print(f"Points: {self.n_points}, Active points: {len(self.X)}")
|
|
166
|
+
print(f"Trust radius: {self.trust_radius:.6f}, Step magnitude: {np.linalg.norm(move_vector_flat):.6f}")
|
|
167
|
+
|
|
168
|
+
if self.n_points > 1:
|
|
169
|
+
# Evaluate model quality
|
|
170
|
+
pred_energy, pred_forces = self._predict(inv_dist, jacobian)
|
|
171
|
+
energy_error = abs(pred_energy - float(B_e))
|
|
172
|
+
force_error = np.linalg.norm(pred_forces - forces_flat)
|
|
173
|
+
|
|
174
|
+
print(f"GP energy prediction error: {energy_error:.6f}")
|
|
175
|
+
print(f"GP force prediction error: {force_error:.6f}")
|
|
176
|
+
print(f"Computation time: {elapsed_time:.3f} seconds")
|
|
177
|
+
|
|
178
|
+
return -1 * move_vector
|
|
179
|
+
|
|
180
|
+
def _adapt_step_size(self, step, forces):
|
|
181
|
+
"""
|
|
182
|
+
Adaptively adjust step size based on forces and minimum step size.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
step: Current step vector
|
|
186
|
+
forces: Current forces
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Adjusted step vector
|
|
190
|
+
"""
|
|
191
|
+
step_size = np.linalg.norm(step)
|
|
192
|
+
force_norm = np.linalg.norm(forces)
|
|
193
|
+
|
|
194
|
+
# If step is too small but forces are significant, increase step size
|
|
195
|
+
if step_size < self.min_step_size and force_norm > 1e-3:
|
|
196
|
+
# Scale up to minimum step size while preserving direction
|
|
197
|
+
new_step = step * (self.min_step_size / step_size)
|
|
198
|
+
|
|
199
|
+
if self.display_flag:
|
|
200
|
+
print(f"Step size increased: {step_size:.6f} → {np.linalg.norm(new_step):.6f}")
|
|
201
|
+
|
|
202
|
+
return new_step
|
|
203
|
+
|
|
204
|
+
# If forces are large and step is relatively small compared to trust radius,
|
|
205
|
+
# consider taking a larger step in the force direction
|
|
206
|
+
if force_norm > 0.1 and step_size < 0.3 * self.trust_radius:
|
|
207
|
+
# Mix the current step with force direction
|
|
208
|
+
force_dir = forces / force_norm
|
|
209
|
+
mixed_step = 0.7 * step + 0.3 * force_dir * self.trust_radius
|
|
210
|
+
mixed_step_size = np.linalg.norm(mixed_step)
|
|
211
|
+
|
|
212
|
+
# Make sure we don't exceed trust radius
|
|
213
|
+
if mixed_step_size > self.trust_radius:
|
|
214
|
+
mixed_step *= self.trust_radius / mixed_step_size
|
|
215
|
+
|
|
216
|
+
if self.display_flag and np.linalg.norm(mixed_step) > step_size * 1.2:
|
|
217
|
+
print(f"Step enhanced with force direction: {step_size:.6f} → {np.linalg.norm(mixed_step):.6f}")
|
|
218
|
+
|
|
219
|
+
return mixed_step
|
|
220
|
+
|
|
221
|
+
return step
|
|
222
|
+
|
|
223
|
+
def _cart_to_inverse_dist(self, coords):
|
|
224
|
+
"""Convert Cartesian coordinates to inverse distances."""
|
|
225
|
+
# Calculate pairwise distances
|
|
226
|
+
dist_matrix = squareform(pdist(coords))
|
|
227
|
+
|
|
228
|
+
# Extract upper triangular part (excluding diagonal)
|
|
229
|
+
upper_indices = np.triu_indices(len(coords), k=1)
|
|
230
|
+
distances = dist_matrix[upper_indices]
|
|
231
|
+
|
|
232
|
+
# Apply minimum distance threshold to avoid singularities
|
|
233
|
+
distances = np.maximum(distances, self.min_dist)
|
|
234
|
+
|
|
235
|
+
# Calculate inverse distances (1/R)
|
|
236
|
+
inv_dist = 1.0 / (distances * self.dist_scale)
|
|
237
|
+
|
|
238
|
+
return inv_dist
|
|
239
|
+
|
|
240
|
+
def _calc_jacobian(self, coords):
|
|
241
|
+
"""
|
|
242
|
+
Calculate the Jacobian matrix for mapping between internal and Cartesian coordinates.
|
|
243
|
+
Returns a matrix of shape (n_dist, n_atoms*3).
|
|
244
|
+
"""
|
|
245
|
+
n_atoms = len(coords)
|
|
246
|
+
n_cart = n_atoms * 3
|
|
247
|
+
n_dist = self.n_dist
|
|
248
|
+
|
|
249
|
+
jacobian = np.zeros((n_dist, n_cart))
|
|
250
|
+
|
|
251
|
+
# For each atom pair
|
|
252
|
+
idx = 0
|
|
253
|
+
for i in range(n_atoms):
|
|
254
|
+
for j in range(i+1, n_atoms):
|
|
255
|
+
# Calculate distance vector r_ij and its norm
|
|
256
|
+
r_ij = coords[j] - coords[i]
|
|
257
|
+
r_norm = np.linalg.norm(r_ij)
|
|
258
|
+
|
|
259
|
+
# Avoid division by zero
|
|
260
|
+
if r_norm < self.min_dist:
|
|
261
|
+
r_norm = self.min_dist
|
|
262
|
+
|
|
263
|
+
# Unit vector in direction of r_ij
|
|
264
|
+
r_unit = r_ij / r_norm
|
|
265
|
+
|
|
266
|
+
# For inverse distance (1/R), the derivative is -1/R^2 * unit_vector
|
|
267
|
+
factor = -1.0 / (r_norm**2 * self.dist_scale)
|
|
268
|
+
|
|
269
|
+
# Fill in the Jacobian
|
|
270
|
+
jacobian[idx, i*3:i*3+3] = -r_unit * factor # For atom i
|
|
271
|
+
jacobian[idx, j*3:j*3+3] = r_unit * factor # For atom j
|
|
272
|
+
|
|
273
|
+
idx += 1
|
|
274
|
+
|
|
275
|
+
return jacobian
|
|
276
|
+
|
|
277
|
+
def _init_gp(self):
|
|
278
|
+
"""Initialize the Gaussian Process model."""
|
|
279
|
+
# Set length scale if not provided as array
|
|
280
|
+
if isinstance(self.length_scale, (int, float)):
|
|
281
|
+
self.length_scale = np.ones(self.n_dist) * self.length_scale
|
|
282
|
+
|
|
283
|
+
self._update_kernel()
|
|
284
|
+
|
|
285
|
+
def _update_data(self, cart_pos, inv_dist, jacobian, energy, forces):
|
|
286
|
+
"""Add new data point to GP model and select training data."""
|
|
287
|
+
energy = float(energy)
|
|
288
|
+
|
|
289
|
+
# Add to history
|
|
290
|
+
self.X_cart_all = np.vstack([self.X_cart_all, [cart_pos]])
|
|
291
|
+
self.X_all = np.vstack([self.X_all, [inv_dist]])
|
|
292
|
+
self.Y_all = np.append(self.Y_all, energy)
|
|
293
|
+
self.F_all = np.vstack([self.F_all, [forces]])
|
|
294
|
+
self.jacobians.append(jacobian)
|
|
295
|
+
self.n_points += 1
|
|
296
|
+
|
|
297
|
+
# Select training data
|
|
298
|
+
self._select_training_data()
|
|
299
|
+
|
|
300
|
+
# Update kernel matrix
|
|
301
|
+
self._update_kernel()
|
|
302
|
+
|
|
303
|
+
# Update trust radius based on prediction accuracy
|
|
304
|
+
if self.n_points > 1:
|
|
305
|
+
prev_energy = float(self.Y_all[-2])
|
|
306
|
+
current_energy = float(self.Y_all[-1])
|
|
307
|
+
|
|
308
|
+
# Calculate relative energy improvement
|
|
309
|
+
rel_improvement = (prev_energy - current_energy) / (abs(prev_energy) + 1e-10)
|
|
310
|
+
|
|
311
|
+
# Update trust radius based on energy improvement
|
|
312
|
+
if current_energy < prev_energy:
|
|
313
|
+
# Good step - increase trust radius more aggressively
|
|
314
|
+
if rel_improvement > 0.05: # Significant improvement
|
|
315
|
+
self.trust_radius = min(self.trust_radius * self.trust_increase, self.trust_radius_max)
|
|
316
|
+
else: # Modest improvement
|
|
317
|
+
self.trust_radius = min(self.trust_radius * 1.2, self.trust_radius_max)
|
|
318
|
+
else:
|
|
319
|
+
# Energy increased - decrease trust radius
|
|
320
|
+
self.trust_radius = max(self.trust_radius * self.trust_decrease, self.trust_radius_min)
|
|
321
|
+
|
|
322
|
+
def _select_training_data(self):
|
|
323
|
+
"""Select data points for GP training."""
|
|
324
|
+
n_total = len(self.X_all)
|
|
325
|
+
max_pts = min(self.max_gp_pts, n_total)
|
|
326
|
+
|
|
327
|
+
if self.selection_method == 'recent':
|
|
328
|
+
# Keep most recent points
|
|
329
|
+
indices = np.arange(n_total - max_pts, n_total)
|
|
330
|
+
elif self.selection_method == 'best':
|
|
331
|
+
# Keep lowest energy points
|
|
332
|
+
indices = np.argsort(self.Y_all)[:max_pts]
|
|
333
|
+
elif self.selection_method == 'diverse':
|
|
334
|
+
# Select diverse points + most recent
|
|
335
|
+
indices = self._select_diverse_points(max_pts)
|
|
336
|
+
else:
|
|
337
|
+
# Default to most recent
|
|
338
|
+
indices = np.arange(n_total - max_pts, n_total)
|
|
339
|
+
|
|
340
|
+
# Update active training data
|
|
341
|
+
self.X = self.X_all[indices]
|
|
342
|
+
self.X_cart = self.X_cart_all[indices]
|
|
343
|
+
self.Y = self.Y_all[indices]
|
|
344
|
+
self.F = self.F_all[indices]
|
|
345
|
+
self.active_jacobians = [self.jacobians[i] for i in indices]
|
|
346
|
+
|
|
347
|
+
def _select_diverse_points(self, max_pts):
|
|
348
|
+
"""Select diverse set of points."""
|
|
349
|
+
n_total = len(self.X_all)
|
|
350
|
+
|
|
351
|
+
# Always include most recent point
|
|
352
|
+
selected_indices = [n_total - 1]
|
|
353
|
+
|
|
354
|
+
if n_total <= max_pts:
|
|
355
|
+
return np.arange(n_total)
|
|
356
|
+
|
|
357
|
+
# Calculate distances in inverse distance space
|
|
358
|
+
distances = squareform(pdist(self.X_all))
|
|
359
|
+
|
|
360
|
+
# Greedy selection
|
|
361
|
+
remaining = list(set(range(n_total)) - set(selected_indices))
|
|
362
|
+
while len(selected_indices) < max_pts and remaining:
|
|
363
|
+
# Find point with maximum minimum distance to selected points
|
|
364
|
+
min_distances = []
|
|
365
|
+
for idx in remaining:
|
|
366
|
+
min_dist = min(distances[idx, sel_idx] for sel_idx in selected_indices)
|
|
367
|
+
min_distances.append((min_dist, idx))
|
|
368
|
+
|
|
369
|
+
_, next_idx = max(min_distances)
|
|
370
|
+
selected_indices.append(next_idx)
|
|
371
|
+
remaining.remove(next_idx)
|
|
372
|
+
|
|
373
|
+
return np.array(sorted(selected_indices))
|
|
374
|
+
|
|
375
|
+
def _update_kernel(self):
|
|
376
|
+
"""Update the kernel matrices for GP regression."""
|
|
377
|
+
n = len(self.X)
|
|
378
|
+
|
|
379
|
+
if n == 0:
|
|
380
|
+
return
|
|
381
|
+
|
|
382
|
+
# Create kernel matrix for energy prediction
|
|
383
|
+
K_ee = np.zeros((n, n))
|
|
384
|
+
|
|
385
|
+
# Calculate kernel between all pairs of points
|
|
386
|
+
for i in range(n):
|
|
387
|
+
for j in range(i+1):
|
|
388
|
+
K_ee[i, j] = self._rbf_kernel(self.X[i], self.X[j])
|
|
389
|
+
if i != j:
|
|
390
|
+
K_ee[j, i] = K_ee[i, j]
|
|
391
|
+
|
|
392
|
+
# Add noise to diagonal
|
|
393
|
+
np.fill_diagonal(K_ee, np.diag(K_ee) + self.noise_energy)
|
|
394
|
+
|
|
395
|
+
self.K_ee = K_ee
|
|
396
|
+
|
|
397
|
+
# Compute Cholesky decomposition with fallbacks for numerical stability
|
|
398
|
+
try:
|
|
399
|
+
self.L_ee = cholesky(self.K_ee, lower=True)
|
|
400
|
+
except LinAlgError:
|
|
401
|
+
np.fill_diagonal(self.K_ee, np.diag(self.K_ee) + self.noise_energy * 10.0)
|
|
402
|
+
try:
|
|
403
|
+
self.L_ee = cholesky(self.K_ee, lower=True)
|
|
404
|
+
except LinAlgError:
|
|
405
|
+
np.fill_diagonal(self.K_ee, np.diag(self.K_ee) + self.noise_energy * 100.0)
|
|
406
|
+
self.L_ee = cholesky(self.K_ee, lower=True)
|
|
407
|
+
|
|
408
|
+
def _rbf_kernel(self, x1, x2):
|
|
409
|
+
"""
|
|
410
|
+
RBF (Squared Exponential) kernel function for inverse distance coordinates.
|
|
411
|
+
k(x, x') = σ² * exp(-0.5 * r²)
|
|
412
|
+
where r² is the squared distance between x and x'.
|
|
413
|
+
"""
|
|
414
|
+
# Calculate squared distance with scaling
|
|
415
|
+
sq_dist = np.sum(((x1 - x2) / self.length_scale) ** 2)
|
|
416
|
+
|
|
417
|
+
# RBF kernel formula
|
|
418
|
+
return self.sigma_f * np.exp(-0.5 * sq_dist)
|
|
419
|
+
|
|
420
|
+
def _rbf_kernel_gradient(self, x1, x2):
|
|
421
|
+
"""Gradient of the RBF kernel with respect to x2."""
|
|
422
|
+
diff = (x1 - x2) / (self.length_scale ** 2)
|
|
423
|
+
k = self._rbf_kernel(x1, x2)
|
|
424
|
+
return k * diff
|
|
425
|
+
|
|
426
|
+
def _predict(self, inv_dist, jacobian):
|
|
427
|
+
"""
|
|
428
|
+
Predict energy and forces using the GP model.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
inv_dist: Inverse distances (1/R) for the test point
|
|
432
|
+
jacobian: Jacobian matrix for coordinate conversion
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
(pred_energy, pred_forces): Predicted energy and Cartesian forces
|
|
436
|
+
"""
|
|
437
|
+
n = len(self.X)
|
|
438
|
+
|
|
439
|
+
if n == 0:
|
|
440
|
+
return 0.0, np.zeros(self.dim)
|
|
441
|
+
|
|
442
|
+
# Create kernel vector between test point and training points
|
|
443
|
+
k_e = np.zeros(n)
|
|
444
|
+
for i in range(n):
|
|
445
|
+
k_e[i] = self._rbf_kernel(inv_dist, self.X[i])
|
|
446
|
+
|
|
447
|
+
# Predict energy
|
|
448
|
+
alpha_e = cho_solve((self.L_ee, True), self.Y)
|
|
449
|
+
pred_energy = float(np.dot(k_e, alpha_e))
|
|
450
|
+
|
|
451
|
+
# Calculate partial derivatives dE/d(1/R)
|
|
452
|
+
grad_internal = np.zeros(self.n_dist)
|
|
453
|
+
for j in range(self.n_dist):
|
|
454
|
+
for i in range(n):
|
|
455
|
+
# Compute derivative of kernel wrt each inverse distance
|
|
456
|
+
x1 = inv_dist.copy()
|
|
457
|
+
x2 = self.X[i].copy()
|
|
458
|
+
|
|
459
|
+
# Compute analytical gradient for RBF kernel
|
|
460
|
+
grad_k = self._rbf_kernel_gradient(x1, x2)
|
|
461
|
+
dk_dr = -grad_k[j] # Negative because we're taking derivative wrt x1
|
|
462
|
+
|
|
463
|
+
grad_internal[j] += alpha_e[i] * dk_dr
|
|
464
|
+
|
|
465
|
+
# Convert from internal coordinate gradients to Cartesian forces
|
|
466
|
+
# F = -dE/dR = -J^T * dE/d(1/R)
|
|
467
|
+
pred_forces = -np.dot(jacobian.T, grad_internal)
|
|
468
|
+
|
|
469
|
+
return pred_energy, pred_forces
|
|
470
|
+
|
|
471
|
+
def _optimize_on_surrogate_pes(self, position, forces, energy, inv_dist, jacobian):
|
|
472
|
+
"""
|
|
473
|
+
Perform optimization on the surrogate PES constructed by GPR.
|
|
474
|
+
This is a more thorough optimization than a single step.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
position: Current Cartesian coordinates
|
|
478
|
+
forces: Current forces
|
|
479
|
+
energy: Current energy
|
|
480
|
+
inv_dist: Current inverse distances
|
|
481
|
+
jacobian: Current Jacobian matrix
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
best_step: Optimized step vector
|
|
485
|
+
"""
|
|
486
|
+
if self.n_points <= 1:
|
|
487
|
+
# If only one data point, use gradient-based step
|
|
488
|
+
norm_f = np.linalg.norm(forces)
|
|
489
|
+
if norm_f > 1e-10:
|
|
490
|
+
# Take a larger step using force_scale_factor
|
|
491
|
+
step = self.alpha * (forces / norm_f) * self.force_scale_factor
|
|
492
|
+
step_size = np.linalg.norm(step)
|
|
493
|
+
|
|
494
|
+
# Ensure minimum step size
|
|
495
|
+
if step_size < self.min_step_size:
|
|
496
|
+
step *= self.min_step_size / step_size
|
|
497
|
+
|
|
498
|
+
# Cap at trust radius
|
|
499
|
+
if step_size > self.trust_radius:
|
|
500
|
+
step *= self.trust_radius / step_size
|
|
501
|
+
|
|
502
|
+
return step
|
|
503
|
+
else:
|
|
504
|
+
# Small random step
|
|
505
|
+
step = np.random.normal(0, 0.1, size=self.dim)
|
|
506
|
+
step *= self.min_step_size / np.linalg.norm(step)
|
|
507
|
+
return step
|
|
508
|
+
|
|
509
|
+
if self.display_flag:
|
|
510
|
+
print(f"Starting optimization on surrogate PES (trust radius: {self.trust_radius:.4f})")
|
|
511
|
+
|
|
512
|
+
# Objective function for minimization on surrogate PES
|
|
513
|
+
def surrogate_objective(step):
|
|
514
|
+
# Calculate inverse distances for new position
|
|
515
|
+
new_coords = (position + step).reshape(-1, 3)
|
|
516
|
+
new_inv_dist = self._cart_to_inverse_dist(new_coords)
|
|
517
|
+
new_jacobian = self._calc_jacobian(new_coords)
|
|
518
|
+
|
|
519
|
+
# Predict energy at new position
|
|
520
|
+
pred_energy, _ = self._predict(new_inv_dist, new_jacobian)
|
|
521
|
+
return float(pred_energy)
|
|
522
|
+
|
|
523
|
+
# Gradient function (negative of predicted forces)
|
|
524
|
+
def surrogate_gradient(step):
|
|
525
|
+
# Calculate inverse distances and Jacobian for new position
|
|
526
|
+
new_coords = (position + step).reshape(-1, 3)
|
|
527
|
+
new_inv_dist = self._cart_to_inverse_dist(new_coords)
|
|
528
|
+
new_jacobian = self._calc_jacobian(new_coords)
|
|
529
|
+
|
|
530
|
+
# Predict forces (negative gradient)
|
|
531
|
+
_, pred_forces = self._predict(new_inv_dist, new_jacobian)
|
|
532
|
+
return -pred_forces # Negative because we're minimizing energy
|
|
533
|
+
|
|
534
|
+
# Initial guess: take larger step in force direction
|
|
535
|
+
norm_f = np.linalg.norm(forces)
|
|
536
|
+
if norm_f > 1e-10:
|
|
537
|
+
initial_step = self.trust_radius * 0.8 * forces / norm_f # 80% of trust radius
|
|
538
|
+
else:
|
|
539
|
+
# Random step with significant magnitude
|
|
540
|
+
initial_step = np.random.normal(0, 0.1, size=self.dim)
|
|
541
|
+
initial_step *= self.trust_radius * 0.5 / max(np.linalg.norm(initial_step), 1e-10)
|
|
542
|
+
|
|
543
|
+
# Trust region as spherical constraint
|
|
544
|
+
constraint = {'type': 'ineq',
|
|
545
|
+
'fun': lambda x: self.trust_radius**2 - np.sum(x**2),
|
|
546
|
+
'jac': lambda x: -2*x}
|
|
547
|
+
|
|
548
|
+
# Perform thorough optimization on surrogate PES
|
|
549
|
+
try:
|
|
550
|
+
result = minimize(
|
|
551
|
+
surrogate_objective,
|
|
552
|
+
initial_step,
|
|
553
|
+
method='SLSQP',
|
|
554
|
+
jac=surrogate_gradient,
|
|
555
|
+
constraints=[constraint],
|
|
556
|
+
options={
|
|
557
|
+
'maxiter': self.surrogate_maxiter,
|
|
558
|
+
'ftol': self.surrogate_ftol,
|
|
559
|
+
'disp': False
|
|
560
|
+
}
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
if self.display_flag:
|
|
564
|
+
if result.success:
|
|
565
|
+
print(f"Surrogate optimization successful: {result.message}")
|
|
566
|
+
print(f"Final surrogate energy: {result.fun:.8f}")
|
|
567
|
+
print(f"Iterations: {result.nit}")
|
|
568
|
+
else:
|
|
569
|
+
print(f"Surrogate optimization warning: {result.message}")
|
|
570
|
+
|
|
571
|
+
move_vector = result.x
|
|
572
|
+
|
|
573
|
+
except Exception as e:
|
|
574
|
+
if self.display_flag:
|
|
575
|
+
print(f"Surrogate optimization failed: {e}")
|
|
576
|
+
# Use force-based step as fallback
|
|
577
|
+
if norm_f > 1e-10:
|
|
578
|
+
move_vector = self.trust_radius * 0.8 * forces / norm_f
|
|
579
|
+
else:
|
|
580
|
+
# Random step
|
|
581
|
+
move_vector = np.random.normal(0, 0.1, size=self.dim)
|
|
582
|
+
move_vector *= self.min_step_size / np.linalg.norm(move_vector)
|
|
583
|
+
|
|
584
|
+
# Calculate predicted improvement
|
|
585
|
+
new_coords = (position + move_vector).reshape(-1, 3)
|
|
586
|
+
new_inv_dist = self._cart_to_inverse_dist(new_coords)
|
|
587
|
+
new_jacobian = self._calc_jacobian(new_coords)
|
|
588
|
+
pred_new_energy, _ = self._predict(new_inv_dist, new_jacobian)
|
|
589
|
+
|
|
590
|
+
# Check if the optimization actually improved energy
|
|
591
|
+
if pred_new_energy >= energy:
|
|
592
|
+
# If no improvement, take force-based step instead
|
|
593
|
+
if self.display_flag:
|
|
594
|
+
print("Optimization didn't reduce energy, using force-based step")
|
|
595
|
+
|
|
596
|
+
if norm_f > 1e-10:
|
|
597
|
+
move_vector = self.trust_radius * 0.8 * forces / norm_f
|
|
598
|
+
else:
|
|
599
|
+
# Random step as last resort
|
|
600
|
+
move_vector = np.random.normal(0, 0.1, size=self.dim)
|
|
601
|
+
move_vector *= self.min_step_size / np.linalg.norm(move_vector)
|
|
602
|
+
|
|
603
|
+
# Ensure we don't exceed trust radius
|
|
604
|
+
step_size = np.linalg.norm(move_vector)
|
|
605
|
+
if step_size > self.trust_radius:
|
|
606
|
+
move_vector *= self.trust_radius / step_size
|
|
607
|
+
|
|
608
|
+
# Ensure minimum step size when forces are significant
|
|
609
|
+
if step_size < self.min_step_size and norm_f > 1e-3:
|
|
610
|
+
move_vector *= self.min_step_size / step_size
|
|
611
|
+
|
|
612
|
+
if self.display_flag:
|
|
613
|
+
pred_improvement = energy - pred_new_energy
|
|
614
|
+
final_step_size = np.linalg.norm(move_vector)
|
|
615
|
+
print(f"Predicted energy improvement: {pred_improvement:.6f}")
|
|
616
|
+
print(f"Step magnitude: {final_step_size:.6f}")
|
|
617
|
+
|
|
618
|
+
return move_vector
|
|
619
|
+
|
|
620
|
+
def set_hessian(self, hessian):
|
|
621
|
+
"""Store the Hessian matrix (required interface)"""
|
|
622
|
+
self.hessian = hessian
|
|
623
|
+
|
|
624
|
+
def set_bias_hessian(self, bias_hessian):
|
|
625
|
+
"""Store the bias Hessian matrix (required interface)"""
|
|
626
|
+
self.bias_hessian = bias_hessian
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def get_hessian(self):
|
|
630
|
+
return self.hessian
|
|
631
|
+
|
|
632
|
+
def get_bias_hessian(self):
|
|
633
|
+
return self.bias_hessian
|