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,405 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CoordinateLocking:
|
|
5
|
+
"""
|
|
6
|
+
Coordinate Locking/Unlocking optimization method.
|
|
7
|
+
|
|
8
|
+
This method selectively freezes (locks) and releases (unlocks) coordinates
|
|
9
|
+
during optimization to improve convergence and control structural changes.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
# Basic parameters
|
|
14
|
+
self.lock_mode = 'adaptive' # 'manual', 'adaptive', 'scheduled', 'threshold'
|
|
15
|
+
self.lock_schedule = 'gradual' # 'gradual', 'stepwise', 'oscillating'
|
|
16
|
+
self.lock_fraction_initial = 0.5 # Initial fraction of coordinates to lock
|
|
17
|
+
self.lock_fraction_final = 0.0 # Final fraction of coordinates to lock
|
|
18
|
+
self.lock_transition_steps = 10 # Steps to transition from initial to final
|
|
19
|
+
|
|
20
|
+
# Adaptive locking parameters
|
|
21
|
+
self.threshold_type = 'gradient' # 'gradient', 'displacement', 'both'
|
|
22
|
+
self.gradient_threshold = 0.05 # Lock coordinates with gradient below this
|
|
23
|
+
self.displacement_threshold = 0.03 # Lock coordinates with displacement below this
|
|
24
|
+
self.gradient_threshold_factor = 2.0 # Multiplier for adaptive gradient threshold
|
|
25
|
+
|
|
26
|
+
# Group locking parameters
|
|
27
|
+
self.use_groups = False # Whether to lock coordinates in groups
|
|
28
|
+
self.group_definitions = [] # List of coordinate groups (indices)
|
|
29
|
+
self.group_coupling_threshold = 0.7 # Threshold for detecting coupled coordinates
|
|
30
|
+
|
|
31
|
+
# Technical parameters
|
|
32
|
+
self.update_frequency = 2 # How often to update locked coordinates
|
|
33
|
+
self.lock_inertia = 3 # Steps a coordinate stays locked before review
|
|
34
|
+
self.lock_max_fraction = 0.8 # Maximum fraction that can be locked at once
|
|
35
|
+
|
|
36
|
+
# State variables
|
|
37
|
+
self.lock_mask = None # Boolean mask of locked coordinates (True = locked)
|
|
38
|
+
self.lock_history = {} # History of when coordinates were locked
|
|
39
|
+
self.step_history = [] # History of previous steps
|
|
40
|
+
self.gradient_history = [] # History of previous gradients
|
|
41
|
+
self.current_lock_fraction = None # Current fraction of locked coordinates
|
|
42
|
+
|
|
43
|
+
# Iteration tracking
|
|
44
|
+
self.iter = 0
|
|
45
|
+
|
|
46
|
+
def _initialize_lock_mask(self, n_coords):
|
|
47
|
+
"""
|
|
48
|
+
Initialize the lock mask based on the current mode.
|
|
49
|
+
|
|
50
|
+
Parameters:
|
|
51
|
+
-----------
|
|
52
|
+
n_coords : int
|
|
53
|
+
Number of coordinates
|
|
54
|
+
"""
|
|
55
|
+
if self.lock_mask is not None and len(self.lock_mask) == n_coords:
|
|
56
|
+
return # Already initialized
|
|
57
|
+
|
|
58
|
+
# Create initial lock mask (False = unlocked, True = locked)
|
|
59
|
+
self.lock_mask = np.zeros(n_coords, dtype=bool)
|
|
60
|
+
self.lock_history = {i: 0 for i in range(n_coords)}
|
|
61
|
+
|
|
62
|
+
# Initialize according to mode
|
|
63
|
+
if self.lock_mode == 'manual':
|
|
64
|
+
# In manual mode, all coordinates start unlocked
|
|
65
|
+
pass
|
|
66
|
+
elif self.lock_mode == 'scheduled' or self.lock_mode == 'adaptive':
|
|
67
|
+
# Lock initial fraction of coordinates based on schedule
|
|
68
|
+
self.current_lock_fraction = self.lock_fraction_initial
|
|
69
|
+
self._apply_lock_fraction(n_coords)
|
|
70
|
+
|
|
71
|
+
print(f"Lock mask initialized: {np.sum(self.lock_mask)}/{n_coords} coordinates locked")
|
|
72
|
+
|
|
73
|
+
def _apply_lock_fraction(self, n_coords):
|
|
74
|
+
"""
|
|
75
|
+
Apply the current lock fraction by locking coordinates.
|
|
76
|
+
|
|
77
|
+
Parameters:
|
|
78
|
+
-----------
|
|
79
|
+
n_coords : int
|
|
80
|
+
Number of coordinates
|
|
81
|
+
"""
|
|
82
|
+
if self.current_lock_fraction is None:
|
|
83
|
+
self.current_lock_fraction = self.lock_fraction_initial
|
|
84
|
+
|
|
85
|
+
# Calculate how many coordinates should be locked
|
|
86
|
+
target_locked = int(n_coords * min(self.current_lock_fraction, self.lock_max_fraction))
|
|
87
|
+
currently_locked = np.sum(self.lock_mask)
|
|
88
|
+
|
|
89
|
+
# Adjust locking
|
|
90
|
+
if target_locked > currently_locked:
|
|
91
|
+
# Need to lock more coordinates
|
|
92
|
+
unlocked_indices = np.where(~self.lock_mask)[0]
|
|
93
|
+
to_lock_count = min(target_locked - currently_locked, len(unlocked_indices))
|
|
94
|
+
|
|
95
|
+
if to_lock_count > 0 and len(unlocked_indices) > 0:
|
|
96
|
+
# Prefer locking coordinates with lower historical activity
|
|
97
|
+
if len(self.gradient_history) > 0:
|
|
98
|
+
gradient_activity = np.zeros(n_coords)
|
|
99
|
+
for grad in self.gradient_history[-5:]:
|
|
100
|
+
gradient_activity += np.abs(grad.flatten())
|
|
101
|
+
|
|
102
|
+
# Get indices of unlocked coordinates with lowest activity
|
|
103
|
+
activity_scores = gradient_activity[unlocked_indices]
|
|
104
|
+
lowest_activity_idx = np.argsort(activity_scores)[:to_lock_count]
|
|
105
|
+
to_lock = unlocked_indices[lowest_activity_idx]
|
|
106
|
+
else:
|
|
107
|
+
# No history, just choose randomly
|
|
108
|
+
to_lock = np.random.choice(unlocked_indices, to_lock_count, replace=False)
|
|
109
|
+
|
|
110
|
+
self.lock_mask[to_lock] = True
|
|
111
|
+
for idx in to_lock:
|
|
112
|
+
self.lock_history[idx] = self.iter
|
|
113
|
+
|
|
114
|
+
elif target_locked < currently_locked:
|
|
115
|
+
# Need to unlock some coordinates
|
|
116
|
+
locked_indices = np.where(self.lock_mask)[0]
|
|
117
|
+
to_unlock_count = min(currently_locked - target_locked, len(locked_indices))
|
|
118
|
+
|
|
119
|
+
if to_unlock_count > 0 and len(locked_indices) > 0:
|
|
120
|
+
# Prefer unlocking coordinates with higher historical activity
|
|
121
|
+
if len(self.gradient_history) > 0:
|
|
122
|
+
gradient_activity = np.zeros(n_coords)
|
|
123
|
+
for grad in self.gradient_history[-5:]:
|
|
124
|
+
gradient_activity += np.abs(grad.flatten())
|
|
125
|
+
|
|
126
|
+
# Get indices of locked coordinates with highest activity
|
|
127
|
+
activity_scores = gradient_activity[locked_indices]
|
|
128
|
+
highest_activity_idx = np.argsort(activity_scores)[-to_unlock_count:]
|
|
129
|
+
to_unlock = locked_indices[highest_activity_idx]
|
|
130
|
+
else:
|
|
131
|
+
# No history, just choose randomly
|
|
132
|
+
to_unlock = np.random.choice(locked_indices, to_unlock_count, replace=False)
|
|
133
|
+
|
|
134
|
+
self.lock_mask[to_unlock] = False
|
|
135
|
+
for idx in to_unlock:
|
|
136
|
+
self.lock_history[idx] = self.iter
|
|
137
|
+
|
|
138
|
+
def _update_lock_fraction(self):
|
|
139
|
+
"""
|
|
140
|
+
Update the current lock fraction based on the schedule.
|
|
141
|
+
"""
|
|
142
|
+
if self.lock_mode != 'scheduled':
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
# Skip if we've reached the final value
|
|
146
|
+
if self.current_lock_fraction == self.lock_fraction_final:
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
# Update based on schedule type
|
|
150
|
+
if self.lock_schedule == 'gradual':
|
|
151
|
+
# Linear interpolation
|
|
152
|
+
progress = min(1.0, self.iter / self.lock_transition_steps)
|
|
153
|
+
self.current_lock_fraction = self.lock_fraction_initial + progress * (
|
|
154
|
+
self.lock_fraction_final - self.lock_fraction_initial)
|
|
155
|
+
|
|
156
|
+
elif self.lock_schedule == 'stepwise':
|
|
157
|
+
# Step changes at specific intervals
|
|
158
|
+
step_interval = self.lock_transition_steps / 5
|
|
159
|
+
num_steps = int(self.iter / step_interval)
|
|
160
|
+
progress = min(1.0, num_steps / 5)
|
|
161
|
+
self.current_lock_fraction = self.lock_fraction_initial + progress * (
|
|
162
|
+
self.lock_fraction_final - self.lock_fraction_initial)
|
|
163
|
+
|
|
164
|
+
elif self.lock_schedule == 'oscillating':
|
|
165
|
+
# Oscillate between initial and final, eventually settling at final
|
|
166
|
+
if self.iter > self.lock_transition_steps:
|
|
167
|
+
self.current_lock_fraction = self.lock_fraction_final
|
|
168
|
+
else:
|
|
169
|
+
progress = self.iter / self.lock_transition_steps
|
|
170
|
+
oscillation = 0.5 + 0.5 * np.cos(4 * np.pi * progress)
|
|
171
|
+
oscillation_factor = 1.0 - progress
|
|
172
|
+
self.current_lock_fraction = self.lock_fraction_final + (
|
|
173
|
+
oscillation * oscillation_factor *
|
|
174
|
+
(self.lock_fraction_initial - self.lock_fraction_final))
|
|
175
|
+
|
|
176
|
+
def _update_adaptive_locks(self, gradient, move_vector):
|
|
177
|
+
"""
|
|
178
|
+
Update locked coordinates based on gradient and displacement thresholds.
|
|
179
|
+
|
|
180
|
+
Parameters:
|
|
181
|
+
-----------
|
|
182
|
+
gradient : numpy.ndarray
|
|
183
|
+
Current gradient
|
|
184
|
+
move_vector : numpy.ndarray
|
|
185
|
+
Current optimization step
|
|
186
|
+
"""
|
|
187
|
+
if self.lock_mode != 'adaptive':
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
# Skip if we're not at an update iteration
|
|
191
|
+
if self.iter % self.update_frequency != 0:
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
n_coords = len(gradient)
|
|
195
|
+
grad_flat = gradient.flatten()
|
|
196
|
+
move_flat = move_vector.flatten()
|
|
197
|
+
|
|
198
|
+
# Calculate gradient and displacement magnitudes
|
|
199
|
+
grad_mag = np.abs(grad_flat)
|
|
200
|
+
move_mag = np.abs(move_flat)
|
|
201
|
+
|
|
202
|
+
# Calculate adaptive thresholds
|
|
203
|
+
if len(self.gradient_history) > 2:
|
|
204
|
+
# Use median of recent history to set thresholds
|
|
205
|
+
recent_grads = np.vstack([g.flatten() for g in self.gradient_history[-3:]])
|
|
206
|
+
median_grad = np.median(np.abs(recent_grads))
|
|
207
|
+
grad_threshold = median_grad / self.gradient_threshold_factor
|
|
208
|
+
else:
|
|
209
|
+
grad_threshold = self.gradient_threshold
|
|
210
|
+
|
|
211
|
+
# Create masks for coordinates meeting thresholds
|
|
212
|
+
low_grad_mask = grad_mag < grad_threshold
|
|
213
|
+
low_move_mask = move_mag < self.displacement_threshold
|
|
214
|
+
|
|
215
|
+
# Combine criteria based on threshold type
|
|
216
|
+
if self.threshold_type == 'gradient':
|
|
217
|
+
criteria_mask = low_grad_mask
|
|
218
|
+
elif self.threshold_type == 'displacement':
|
|
219
|
+
criteria_mask = low_move_mask
|
|
220
|
+
else: # 'both'
|
|
221
|
+
criteria_mask = low_grad_mask & low_move_mask
|
|
222
|
+
|
|
223
|
+
# Apply inertia: only lock/unlock if criteria have been met for multiple steps
|
|
224
|
+
new_locks = []
|
|
225
|
+
new_unlocks = []
|
|
226
|
+
|
|
227
|
+
for i in range(n_coords):
|
|
228
|
+
if criteria_mask[i] and not self.lock_mask[i]:
|
|
229
|
+
# Potential new lock
|
|
230
|
+
if i in self.lock_history and self.iter - self.lock_history[i] > self.lock_inertia:
|
|
231
|
+
new_locks.append(i)
|
|
232
|
+
self.lock_history[i] = self.iter
|
|
233
|
+
elif not criteria_mask[i] and self.lock_mask[i]:
|
|
234
|
+
# Potential new unlock
|
|
235
|
+
if i in self.lock_history and self.iter - self.lock_history[i] > self.lock_inertia:
|
|
236
|
+
new_unlocks.append(i)
|
|
237
|
+
self.lock_history[i] = self.iter
|
|
238
|
+
|
|
239
|
+
# Apply new locks and unlocks
|
|
240
|
+
if new_locks:
|
|
241
|
+
self.lock_mask[new_locks] = True
|
|
242
|
+
print(f"Newly locked coordinates: {len(new_locks)}")
|
|
243
|
+
|
|
244
|
+
if new_unlocks:
|
|
245
|
+
self.lock_mask[new_unlocks] = False
|
|
246
|
+
print(f"Newly unlocked coordinates: {len(new_unlocks)}")
|
|
247
|
+
|
|
248
|
+
def _apply_group_locking(self, gradient):
|
|
249
|
+
"""
|
|
250
|
+
Apply locking to coordinate groups based on coupling.
|
|
251
|
+
|
|
252
|
+
Parameters:
|
|
253
|
+
-----------
|
|
254
|
+
gradient : numpy.ndarray
|
|
255
|
+
Current gradient
|
|
256
|
+
"""
|
|
257
|
+
if not self.use_groups:
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
if not self.group_definitions:
|
|
261
|
+
# Try to detect coordinate groups automatically
|
|
262
|
+
self._detect_coordinate_groups(gradient)
|
|
263
|
+
|
|
264
|
+
# Apply consistent locking within groups
|
|
265
|
+
for group in self.group_definitions:
|
|
266
|
+
# Check if majority of group is locked
|
|
267
|
+
lock_count = np.sum(self.lock_mask[group])
|
|
268
|
+
if lock_count > len(group) / 2:
|
|
269
|
+
# Lock the entire group
|
|
270
|
+
self.lock_mask[group] = True
|
|
271
|
+
|
|
272
|
+
# Check if majority of group is unlocked
|
|
273
|
+
unlock_count = np.sum(~self.lock_mask[group])
|
|
274
|
+
if unlock_count > len(group) / 2:
|
|
275
|
+
# Unlock the entire group
|
|
276
|
+
self.lock_mask[group] = False
|
|
277
|
+
|
|
278
|
+
def _detect_coordinate_groups(self, gradient):
|
|
279
|
+
"""
|
|
280
|
+
Attempt to automatically detect coordinate groups based on coupling.
|
|
281
|
+
|
|
282
|
+
Parameters:
|
|
283
|
+
-----------
|
|
284
|
+
gradient : numpy.ndarray
|
|
285
|
+
Current gradient
|
|
286
|
+
"""
|
|
287
|
+
n_coords = len(gradient)
|
|
288
|
+
|
|
289
|
+
# Need history to detect coupling
|
|
290
|
+
if len(self.gradient_history) < 5:
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
# Calculate correlation matrix between coordinates using gradient history
|
|
294
|
+
grad_history_array = np.vstack([g.flatten() for g in self.gradient_history[-5:]])
|
|
295
|
+
|
|
296
|
+
if grad_history_array.shape[0] < 3:
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
# Calculate correlation coefficient matrix
|
|
300
|
+
correlation = np.corrcoef(grad_history_array.T)
|
|
301
|
+
|
|
302
|
+
# Replace NaN with 0
|
|
303
|
+
correlation = np.nan_to_num(correlation)
|
|
304
|
+
|
|
305
|
+
# Find highly correlated coordinates
|
|
306
|
+
groups = []
|
|
307
|
+
visited = set()
|
|
308
|
+
|
|
309
|
+
for i in range(n_coords):
|
|
310
|
+
if i in visited:
|
|
311
|
+
continue
|
|
312
|
+
|
|
313
|
+
# Find coordinates highly correlated with i
|
|
314
|
+
group = {i}
|
|
315
|
+
for j in range(n_coords):
|
|
316
|
+
if j != i and j not in visited and abs(correlation[i, j]) > self.group_coupling_threshold:
|
|
317
|
+
group.add(j)
|
|
318
|
+
|
|
319
|
+
if len(group) > 1:
|
|
320
|
+
groups.append(list(group))
|
|
321
|
+
visited.update(group)
|
|
322
|
+
|
|
323
|
+
if groups:
|
|
324
|
+
self.group_definitions = groups
|
|
325
|
+
print(f"Detected {len(groups)} coordinate groups")
|
|
326
|
+
|
|
327
|
+
def _apply_lock_mask(self, move_vector):
|
|
328
|
+
"""
|
|
329
|
+
Apply the current lock mask to a move vector.
|
|
330
|
+
|
|
331
|
+
Parameters:
|
|
332
|
+
-----------
|
|
333
|
+
move_vector : numpy.ndarray
|
|
334
|
+
Original move vector
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
--------
|
|
338
|
+
numpy.ndarray
|
|
339
|
+
Modified move vector with locked coordinates zeroed out
|
|
340
|
+
"""
|
|
341
|
+
# Create a mask in the same shape as move_vector
|
|
342
|
+
mask_expanded = np.logical_not(self.lock_mask).reshape(-1, 1)
|
|
343
|
+
|
|
344
|
+
# Apply mask to zero out locked coordinates
|
|
345
|
+
return move_vector * mask_expanded
|
|
346
|
+
|
|
347
|
+
def run(self, geom_num_list, energy, gradient, original_move_vector):
|
|
348
|
+
"""
|
|
349
|
+
Run Coordinate Locking optimization step.
|
|
350
|
+
|
|
351
|
+
Parameters:
|
|
352
|
+
-----------
|
|
353
|
+
geom_num_list : numpy.ndarray
|
|
354
|
+
Current geometry
|
|
355
|
+
energy : float
|
|
356
|
+
Current energy value
|
|
357
|
+
gradient : numpy.ndarray
|
|
358
|
+
Current gradient
|
|
359
|
+
original_move_vector : numpy.ndarray
|
|
360
|
+
Original optimization step
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
--------
|
|
364
|
+
numpy.ndarray
|
|
365
|
+
Modified optimization step
|
|
366
|
+
"""
|
|
367
|
+
print("Coordinate Locking method")
|
|
368
|
+
n_coords = len(geom_num_list)
|
|
369
|
+
|
|
370
|
+
# Store gradient history
|
|
371
|
+
self.gradient_history.append(gradient.copy())
|
|
372
|
+
if len(self.gradient_history) > 10:
|
|
373
|
+
self.gradient_history.pop(0)
|
|
374
|
+
|
|
375
|
+
# Initialize or update lock mask
|
|
376
|
+
if self.lock_mask is None:
|
|
377
|
+
self._initialize_lock_mask(n_coords)
|
|
378
|
+
|
|
379
|
+
# Update lock fraction based on schedule
|
|
380
|
+
self._update_lock_fraction()
|
|
381
|
+
|
|
382
|
+
# Apply scheduled lock fraction if applicable
|
|
383
|
+
if self.lock_mode == 'scheduled':
|
|
384
|
+
self._apply_lock_fraction(n_coords)
|
|
385
|
+
|
|
386
|
+
# Update adaptive locks based on gradient and movement
|
|
387
|
+
self._update_adaptive_locks(gradient, original_move_vector)
|
|
388
|
+
|
|
389
|
+
# Apply group locking logic
|
|
390
|
+
self._apply_group_locking(gradient)
|
|
391
|
+
|
|
392
|
+
# Apply lock mask to move vector
|
|
393
|
+
modified_move_vector = self._apply_lock_mask(original_move_vector)
|
|
394
|
+
|
|
395
|
+
# Store step history
|
|
396
|
+
self.step_history.append(modified_move_vector.copy())
|
|
397
|
+
if len(self.step_history) > 10:
|
|
398
|
+
self.step_history.pop(0)
|
|
399
|
+
|
|
400
|
+
# Print status
|
|
401
|
+
locked_count = np.sum(self.lock_mask)
|
|
402
|
+
print(f"Locked coordinates: {locked_count}/{n_coords} ({locked_count/n_coords*100:.1f}%)")
|
|
403
|
+
|
|
404
|
+
self.iter += 1
|
|
405
|
+
return modified_move_vector
|