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.
Files changed (246) hide show
  1. multioptpy/Calculator/__init__.py +0 -0
  2. multioptpy/Calculator/ase_calculation_tools.py +424 -0
  3. multioptpy/Calculator/ase_tools/__init__.py +0 -0
  4. multioptpy/Calculator/ase_tools/fairchem.py +28 -0
  5. multioptpy/Calculator/ase_tools/gamess.py +19 -0
  6. multioptpy/Calculator/ase_tools/gaussian.py +165 -0
  7. multioptpy/Calculator/ase_tools/mace.py +28 -0
  8. multioptpy/Calculator/ase_tools/mopac.py +19 -0
  9. multioptpy/Calculator/ase_tools/nwchem.py +31 -0
  10. multioptpy/Calculator/ase_tools/orca.py +22 -0
  11. multioptpy/Calculator/ase_tools/pygfn0.py +37 -0
  12. multioptpy/Calculator/dxtb_calculation_tools.py +344 -0
  13. multioptpy/Calculator/emt_calculation_tools.py +458 -0
  14. multioptpy/Calculator/gpaw_calculation_tools.py +183 -0
  15. multioptpy/Calculator/lj_calculation_tools.py +314 -0
  16. multioptpy/Calculator/psi4_calculation_tools.py +334 -0
  17. multioptpy/Calculator/pwscf_calculation_tools.py +189 -0
  18. multioptpy/Calculator/pyscf_calculation_tools.py +327 -0
  19. multioptpy/Calculator/sqm1_calculation_tools.py +611 -0
  20. multioptpy/Calculator/sqm2_calculation_tools.py +376 -0
  21. multioptpy/Calculator/tblite_calculation_tools.py +352 -0
  22. multioptpy/Calculator/tersoff_calculation_tools.py +818 -0
  23. multioptpy/Constraint/__init__.py +0 -0
  24. multioptpy/Constraint/constraint_condition.py +834 -0
  25. multioptpy/Coordinate/__init__.py +0 -0
  26. multioptpy/Coordinate/polar_coordinate.py +199 -0
  27. multioptpy/Coordinate/redundant_coordinate.py +638 -0
  28. multioptpy/IRC/__init__.py +0 -0
  29. multioptpy/IRC/converge_criteria.py +28 -0
  30. multioptpy/IRC/dvv.py +544 -0
  31. multioptpy/IRC/euler.py +439 -0
  32. multioptpy/IRC/hpc.py +564 -0
  33. multioptpy/IRC/lqa.py +540 -0
  34. multioptpy/IRC/modekill.py +662 -0
  35. multioptpy/IRC/rk4.py +579 -0
  36. multioptpy/Interpolation/__init__.py +0 -0
  37. multioptpy/Interpolation/adaptive_interpolation.py +283 -0
  38. multioptpy/Interpolation/binomial_interpolation.py +179 -0
  39. multioptpy/Interpolation/geodesic_interpolation.py +785 -0
  40. multioptpy/Interpolation/interpolation.py +156 -0
  41. multioptpy/Interpolation/linear_interpolation.py +473 -0
  42. multioptpy/Interpolation/savitzky_golay_interpolation.py +252 -0
  43. multioptpy/Interpolation/spline_interpolation.py +353 -0
  44. multioptpy/MD/__init__.py +0 -0
  45. multioptpy/MD/thermostat.py +185 -0
  46. multioptpy/MEP/__init__.py +0 -0
  47. multioptpy/MEP/pathopt_bneb_force.py +443 -0
  48. multioptpy/MEP/pathopt_dmf_force.py +448 -0
  49. multioptpy/MEP/pathopt_dneb_force.py +130 -0
  50. multioptpy/MEP/pathopt_ewbneb_force.py +207 -0
  51. multioptpy/MEP/pathopt_gpneb_force.py +512 -0
  52. multioptpy/MEP/pathopt_lup_force.py +113 -0
  53. multioptpy/MEP/pathopt_neb_force.py +225 -0
  54. multioptpy/MEP/pathopt_nesb_force.py +205 -0
  55. multioptpy/MEP/pathopt_om_force.py +153 -0
  56. multioptpy/MEP/pathopt_qsm_force.py +174 -0
  57. multioptpy/MEP/pathopt_qsmv2_force.py +304 -0
  58. multioptpy/ModelFunction/__init__.py +7 -0
  59. multioptpy/ModelFunction/avoiding_model_function.py +29 -0
  60. multioptpy/ModelFunction/binary_image_ts_search_model_function.py +47 -0
  61. multioptpy/ModelFunction/conical_model_function.py +26 -0
  62. multioptpy/ModelFunction/opt_meci.py +50 -0
  63. multioptpy/ModelFunction/opt_mesx.py +47 -0
  64. multioptpy/ModelFunction/opt_mesx_2.py +49 -0
  65. multioptpy/ModelFunction/seam_model_function.py +27 -0
  66. multioptpy/ModelHessian/__init__.py +0 -0
  67. multioptpy/ModelHessian/approx_hessian.py +147 -0
  68. multioptpy/ModelHessian/calc_params.py +227 -0
  69. multioptpy/ModelHessian/fischer.py +236 -0
  70. multioptpy/ModelHessian/fischerd3.py +360 -0
  71. multioptpy/ModelHessian/fischerd4.py +398 -0
  72. multioptpy/ModelHessian/gfn0xtb.py +633 -0
  73. multioptpy/ModelHessian/gfnff.py +709 -0
  74. multioptpy/ModelHessian/lindh.py +165 -0
  75. multioptpy/ModelHessian/lindh2007d2.py +707 -0
  76. multioptpy/ModelHessian/lindh2007d3.py +822 -0
  77. multioptpy/ModelHessian/lindh2007d4.py +1030 -0
  78. multioptpy/ModelHessian/morse.py +106 -0
  79. multioptpy/ModelHessian/schlegel.py +144 -0
  80. multioptpy/ModelHessian/schlegeld3.py +322 -0
  81. multioptpy/ModelHessian/schlegeld4.py +559 -0
  82. multioptpy/ModelHessian/shortrange.py +346 -0
  83. multioptpy/ModelHessian/swartd2.py +496 -0
  84. multioptpy/ModelHessian/swartd3.py +706 -0
  85. multioptpy/ModelHessian/swartd4.py +918 -0
  86. multioptpy/ModelHessian/tshess.py +40 -0
  87. multioptpy/Optimizer/QHAdam.py +61 -0
  88. multioptpy/Optimizer/__init__.py +0 -0
  89. multioptpy/Optimizer/abc_fire.py +83 -0
  90. multioptpy/Optimizer/adabelief.py +58 -0
  91. multioptpy/Optimizer/adabound.py +68 -0
  92. multioptpy/Optimizer/adadelta.py +65 -0
  93. multioptpy/Optimizer/adaderivative.py +56 -0
  94. multioptpy/Optimizer/adadiff.py +68 -0
  95. multioptpy/Optimizer/adafactor.py +70 -0
  96. multioptpy/Optimizer/adam.py +65 -0
  97. multioptpy/Optimizer/adamax.py +62 -0
  98. multioptpy/Optimizer/adamod.py +83 -0
  99. multioptpy/Optimizer/adamw.py +65 -0
  100. multioptpy/Optimizer/adiis.py +523 -0
  101. multioptpy/Optimizer/afire_neb.py +282 -0
  102. multioptpy/Optimizer/block_hessian_update.py +709 -0
  103. multioptpy/Optimizer/c2diis.py +491 -0
  104. multioptpy/Optimizer/component_wise_scaling.py +405 -0
  105. multioptpy/Optimizer/conjugate_gradient.py +82 -0
  106. multioptpy/Optimizer/conjugate_gradient_neb.py +345 -0
  107. multioptpy/Optimizer/coordinate_locking.py +405 -0
  108. multioptpy/Optimizer/dic_rsirfo.py +1015 -0
  109. multioptpy/Optimizer/ediis.py +417 -0
  110. multioptpy/Optimizer/eve.py +76 -0
  111. multioptpy/Optimizer/fastadabelief.py +61 -0
  112. multioptpy/Optimizer/fire.py +77 -0
  113. multioptpy/Optimizer/fire2.py +249 -0
  114. multioptpy/Optimizer/fire_neb.py +92 -0
  115. multioptpy/Optimizer/gan_step.py +486 -0
  116. multioptpy/Optimizer/gdiis.py +609 -0
  117. multioptpy/Optimizer/gediis.py +203 -0
  118. multioptpy/Optimizer/geodesic_step.py +433 -0
  119. multioptpy/Optimizer/gpmin.py +633 -0
  120. multioptpy/Optimizer/gpr_step.py +364 -0
  121. multioptpy/Optimizer/gradientdescent.py +78 -0
  122. multioptpy/Optimizer/gradientdescent_neb.py +52 -0
  123. multioptpy/Optimizer/hessian_update.py +433 -0
  124. multioptpy/Optimizer/hybrid_rfo.py +998 -0
  125. multioptpy/Optimizer/kdiis.py +625 -0
  126. multioptpy/Optimizer/lars.py +21 -0
  127. multioptpy/Optimizer/lbfgs.py +253 -0
  128. multioptpy/Optimizer/lbfgs_neb.py +355 -0
  129. multioptpy/Optimizer/linesearch.py +236 -0
  130. multioptpy/Optimizer/lookahead.py +40 -0
  131. multioptpy/Optimizer/nadam.py +64 -0
  132. multioptpy/Optimizer/newton.py +200 -0
  133. multioptpy/Optimizer/prodigy.py +70 -0
  134. multioptpy/Optimizer/purtubation.py +16 -0
  135. multioptpy/Optimizer/quickmin_neb.py +245 -0
  136. multioptpy/Optimizer/radam.py +75 -0
  137. multioptpy/Optimizer/rfo_neb.py +302 -0
  138. multioptpy/Optimizer/ric_rfo.py +842 -0
  139. multioptpy/Optimizer/rl_step.py +627 -0
  140. multioptpy/Optimizer/rmspropgrave.py +65 -0
  141. multioptpy/Optimizer/rsirfo.py +1647 -0
  142. multioptpy/Optimizer/rsprfo.py +1056 -0
  143. multioptpy/Optimizer/sadam.py +60 -0
  144. multioptpy/Optimizer/samsgrad.py +63 -0
  145. multioptpy/Optimizer/tr_lbfgs.py +678 -0
  146. multioptpy/Optimizer/trim.py +273 -0
  147. multioptpy/Optimizer/trust_radius.py +207 -0
  148. multioptpy/Optimizer/trust_radius_neb.py +121 -0
  149. multioptpy/Optimizer/yogi.py +60 -0
  150. multioptpy/OtherMethod/__init__.py +0 -0
  151. multioptpy/OtherMethod/addf.py +1150 -0
  152. multioptpy/OtherMethod/dimer.py +895 -0
  153. multioptpy/OtherMethod/elastic_image_pair.py +629 -0
  154. multioptpy/OtherMethod/modelfunction.py +456 -0
  155. multioptpy/OtherMethod/newton_traj.py +454 -0
  156. multioptpy/OtherMethod/twopshs.py +1095 -0
  157. multioptpy/PESAnalyzer/__init__.py +0 -0
  158. multioptpy/PESAnalyzer/calc_irc_curvature.py +125 -0
  159. multioptpy/PESAnalyzer/cmds_analysis.py +152 -0
  160. multioptpy/PESAnalyzer/koopman_analysis.py +268 -0
  161. multioptpy/PESAnalyzer/pca_analysis.py +314 -0
  162. multioptpy/Parameters/__init__.py +0 -0
  163. multioptpy/Parameters/atomic_mass.py +20 -0
  164. multioptpy/Parameters/atomic_number.py +22 -0
  165. multioptpy/Parameters/covalent_radii.py +44 -0
  166. multioptpy/Parameters/d2.py +61 -0
  167. multioptpy/Parameters/d3.py +63 -0
  168. multioptpy/Parameters/d4.py +103 -0
  169. multioptpy/Parameters/dreiding.py +34 -0
  170. multioptpy/Parameters/gfn0xtb_param.py +137 -0
  171. multioptpy/Parameters/gfnff_param.py +315 -0
  172. multioptpy/Parameters/gnb.py +104 -0
  173. multioptpy/Parameters/parameter.py +22 -0
  174. multioptpy/Parameters/uff.py +72 -0
  175. multioptpy/Parameters/unit_values.py +20 -0
  176. multioptpy/Potential/AFIR_potential.py +55 -0
  177. multioptpy/Potential/LJ_repulsive_potential.py +345 -0
  178. multioptpy/Potential/__init__.py +0 -0
  179. multioptpy/Potential/anharmonic_keep_potential.py +28 -0
  180. multioptpy/Potential/asym_elllipsoidal_potential.py +718 -0
  181. multioptpy/Potential/electrostatic_potential.py +69 -0
  182. multioptpy/Potential/flux_potential.py +30 -0
  183. multioptpy/Potential/gaussian_potential.py +101 -0
  184. multioptpy/Potential/idpp.py +516 -0
  185. multioptpy/Potential/keep_angle_potential.py +146 -0
  186. multioptpy/Potential/keep_dihedral_angle_potential.py +105 -0
  187. multioptpy/Potential/keep_outofplain_angle_potential.py +70 -0
  188. multioptpy/Potential/keep_potential.py +99 -0
  189. multioptpy/Potential/mechano_force_potential.py +74 -0
  190. multioptpy/Potential/nanoreactor_potential.py +52 -0
  191. multioptpy/Potential/potential.py +896 -0
  192. multioptpy/Potential/spacer_model_potential.py +221 -0
  193. multioptpy/Potential/switching_potential.py +258 -0
  194. multioptpy/Potential/universal_potential.py +34 -0
  195. multioptpy/Potential/value_range_potential.py +36 -0
  196. multioptpy/Potential/void_point_potential.py +25 -0
  197. multioptpy/SQM/__init__.py +0 -0
  198. multioptpy/SQM/sqm1/__init__.py +0 -0
  199. multioptpy/SQM/sqm1/sqm1_core.py +1792 -0
  200. multioptpy/SQM/sqm2/__init__.py +0 -0
  201. multioptpy/SQM/sqm2/calc_tools.py +95 -0
  202. multioptpy/SQM/sqm2/sqm2_basis.py +850 -0
  203. multioptpy/SQM/sqm2/sqm2_bond.py +119 -0
  204. multioptpy/SQM/sqm2/sqm2_core.py +303 -0
  205. multioptpy/SQM/sqm2/sqm2_data.py +1229 -0
  206. multioptpy/SQM/sqm2/sqm2_disp.py +65 -0
  207. multioptpy/SQM/sqm2/sqm2_eeq.py +243 -0
  208. multioptpy/SQM/sqm2/sqm2_overlapint.py +704 -0
  209. multioptpy/SQM/sqm2/sqm2_qm.py +578 -0
  210. multioptpy/SQM/sqm2/sqm2_rep.py +66 -0
  211. multioptpy/SQM/sqm2/sqm2_srb.py +70 -0
  212. multioptpy/Thermo/__init__.py +0 -0
  213. multioptpy/Thermo/normal_mode_analyzer.py +865 -0
  214. multioptpy/Utils/__init__.py +0 -0
  215. multioptpy/Utils/bond_connectivity.py +264 -0
  216. multioptpy/Utils/calc_tools.py +884 -0
  217. multioptpy/Utils/oniom.py +96 -0
  218. multioptpy/Utils/pbc.py +48 -0
  219. multioptpy/Utils/riemann_curvature.py +208 -0
  220. multioptpy/Utils/symmetry_analyzer.py +482 -0
  221. multioptpy/Visualization/__init__.py +0 -0
  222. multioptpy/Visualization/visualization.py +156 -0
  223. multioptpy/WFAnalyzer/MO_analysis.py +104 -0
  224. multioptpy/WFAnalyzer/__init__.py +0 -0
  225. multioptpy/Wrapper/__init__.py +0 -0
  226. multioptpy/Wrapper/autots.py +1239 -0
  227. multioptpy/Wrapper/ieip_wrapper.py +93 -0
  228. multioptpy/Wrapper/md_wrapper.py +92 -0
  229. multioptpy/Wrapper/neb_wrapper.py +94 -0
  230. multioptpy/Wrapper/optimize_wrapper.py +76 -0
  231. multioptpy/__init__.py +5 -0
  232. multioptpy/entrypoints.py +916 -0
  233. multioptpy/fileio.py +660 -0
  234. multioptpy/ieip.py +340 -0
  235. multioptpy/interface.py +1086 -0
  236. multioptpy/irc.py +529 -0
  237. multioptpy/moleculardynamics.py +432 -0
  238. multioptpy/neb.py +1267 -0
  239. multioptpy/optimization.py +1553 -0
  240. multioptpy/optimizer.py +709 -0
  241. multioptpy-1.20.2.dist-info/METADATA +438 -0
  242. multioptpy-1.20.2.dist-info/RECORD +246 -0
  243. multioptpy-1.20.2.dist-info/WHEEL +5 -0
  244. multioptpy-1.20.2.dist-info/entry_points.txt +9 -0
  245. multioptpy-1.20.2.dist-info/licenses/LICENSE +674 -0
  246. 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