MultiOptPy 1.20.2__py3-none-any.whl → 1.20.3__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/ase_calculation_tools.py +13 -0
- multioptpy/Calculator/ase_tools/fairchem.py +12 -7
- multioptpy/Constraint/constraint_condition.py +208 -245
- multioptpy/ModelFunction/binary_image_ts_search_model_function.py +111 -18
- multioptpy/ModelFunction/opt_meci.py +94 -27
- multioptpy/ModelFunction/opt_mesx.py +47 -15
- multioptpy/ModelFunction/opt_mesx_2.py +35 -18
- multioptpy/Optimizer/crsirfo.py +182 -0
- multioptpy/Optimizer/mf_rsirfo.py +266 -0
- multioptpy/Optimizer/mode_following.py +273 -0
- multioptpy/Utils/calc_tools.py +1 -0
- multioptpy/fileio.py +13 -6
- multioptpy/interface.py +3 -2
- multioptpy/optimization.py +2139 -1259
- multioptpy/optimizer.py +158 -6
- {multioptpy-1.20.2.dist-info → multioptpy-1.20.3.dist-info}/METADATA +497 -438
- {multioptpy-1.20.2.dist-info → multioptpy-1.20.3.dist-info}/RECORD +21 -18
- {multioptpy-1.20.2.dist-info → multioptpy-1.20.3.dist-info}/WHEEL +0 -0
- {multioptpy-1.20.2.dist-info → multioptpy-1.20.3.dist-info}/entry_points.txt +0 -0
- {multioptpy-1.20.2.dist-info → multioptpy-1.20.3.dist-info}/licenses/LICENSE +0 -0
- {multioptpy-1.20.2.dist-info → multioptpy-1.20.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from multioptpy.Optimizer.hessian_update import ModelHessianUpdate
|
|
3
|
+
from multioptpy.Optimizer.block_hessian_update import BlockHessianUpdate
|
|
4
|
+
from multioptpy.Utils.calc_tools import Calculationtools
|
|
5
|
+
|
|
6
|
+
# Import the original RSIRFO class for inheritance
|
|
7
|
+
from multioptpy.Optimizer.rsirfo import RSIRFO
|
|
8
|
+
from multioptpy.Optimizer.mode_following import ModeFollowing
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MF_RSIRFO(RSIRFO):
|
|
12
|
+
"""
|
|
13
|
+
Mode-Following RS-I-RFO Optimizer.
|
|
14
|
+
|
|
15
|
+
References:
|
|
16
|
+
[1] Banerjee et al., Phys. Chem., 89, 52-57 (1985)
|
|
17
|
+
[2] Heyden et al., J. Chem. Phys., 123, 224101 (2005)
|
|
18
|
+
[3] Baker, J. Comput. Chem., 7, 385-395 (1986)
|
|
19
|
+
[4] Besalú and Bofill, Theor. Chem. Acc., 100, 265-274 (1998)
|
|
20
|
+
|
|
21
|
+
This code is made based on the below codes.
|
|
22
|
+
1, https://github.com/eljost/pysisyphus/blob/master/pysisyphus/tsoptimizers/TSHessianOptimizer.py
|
|
23
|
+
2, https://github.com/eljost/pysisyphus/blob/master/pysisyphus/tsoptimizers/RSIRFOptimizer.py
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
Extended 'method' string support:
|
|
27
|
+
"method_name : target_index : ema<val> : grad<val>"
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
"block_fsb" -> Default (Index 0, EMA=1.0 if adaptive, Grad=0.0)
|
|
31
|
+
"block_fsb:1" -> Track Mode 1
|
|
32
|
+
"block_fsb:0:ema0.5" -> Track Mode 0 with EMA alpha=0.5
|
|
33
|
+
"block_fsb:ema0.1:grad0.3" -> Track Mode 0, EMA=0.1, Gradient Bias=0.3
|
|
34
|
+
"""
|
|
35
|
+
def __init__(self, **config):
|
|
36
|
+
# 1. Parse 'method' string for advanced configs
|
|
37
|
+
raw_method_str = config.get("method", "auto")
|
|
38
|
+
|
|
39
|
+
# Initial Defaults
|
|
40
|
+
update_method = raw_method_str
|
|
41
|
+
target_mode_index = 0
|
|
42
|
+
|
|
43
|
+
# Check config for fallback defaults
|
|
44
|
+
is_adaptive_config = config.get("adaptive_mode_following", True)
|
|
45
|
+
|
|
46
|
+
# Placeholders for parsed values
|
|
47
|
+
parsed_update_rate = None
|
|
48
|
+
parsed_gradient_weight = 0.0
|
|
49
|
+
|
|
50
|
+
# Parse logic
|
|
51
|
+
if ":" in raw_method_str:
|
|
52
|
+
parts = raw_method_str.split(":")
|
|
53
|
+
update_method = parts[0].strip() # First part is always method name
|
|
54
|
+
|
|
55
|
+
for part in parts[1:]:
|
|
56
|
+
part = part.strip().lower()
|
|
57
|
+
if not part: continue
|
|
58
|
+
|
|
59
|
+
if part.isdigit():
|
|
60
|
+
# "1", "2" -> Target Index
|
|
61
|
+
target_mode_index = int(part)
|
|
62
|
+
|
|
63
|
+
elif part.startswith("ema"):
|
|
64
|
+
# "ema0.5" -> Update Rate
|
|
65
|
+
try:
|
|
66
|
+
val = float(part[3:])
|
|
67
|
+
parsed_update_rate = val
|
|
68
|
+
except ValueError:
|
|
69
|
+
print(f"Warning: Invalid ema value in '{part}'. Ignoring.")
|
|
70
|
+
|
|
71
|
+
elif part.startswith("grad"):
|
|
72
|
+
# "grad0.5" -> Gradient Weight
|
|
73
|
+
try:
|
|
74
|
+
val = float(part[4:])
|
|
75
|
+
parsed_gradient_weight = val
|
|
76
|
+
except ValueError:
|
|
77
|
+
print(f"Warning: Invalid grad value in '{part}'. Ignoring.")
|
|
78
|
+
|
|
79
|
+
# Resolve Update Rate (EMA) and Adaptive Flag
|
|
80
|
+
if parsed_update_rate is not None:
|
|
81
|
+
# Explicit string config overrides config dict
|
|
82
|
+
update_rate = parsed_update_rate
|
|
83
|
+
# If ema > 0, we must enable adaptive mode
|
|
84
|
+
adaptive = (update_rate > 1e-12)
|
|
85
|
+
else:
|
|
86
|
+
# Fallback to config dict or defaults
|
|
87
|
+
# "If ema not specified: 0 if static, 1 if adaptive"
|
|
88
|
+
adaptive = is_adaptive_config
|
|
89
|
+
update_rate = 1.0 if adaptive else 0.0
|
|
90
|
+
|
|
91
|
+
# Resolve Gradient Weight
|
|
92
|
+
gradient_weight = parsed_gradient_weight
|
|
93
|
+
|
|
94
|
+
# Update config for parent class
|
|
95
|
+
config['method'] = update_method
|
|
96
|
+
|
|
97
|
+
# Initialize parent RSIRFO
|
|
98
|
+
super().__init__(**config)
|
|
99
|
+
self.hessian_update_method = update_method
|
|
100
|
+
|
|
101
|
+
self.use_mode_following = config.get("use_mode_following", True)
|
|
102
|
+
|
|
103
|
+
# Other configs
|
|
104
|
+
use_hungarian = config.get("use_hungarian", True)
|
|
105
|
+
element_list = config.get("element_list", None)
|
|
106
|
+
|
|
107
|
+
# Initialize Mode Following with resolved parameters
|
|
108
|
+
self.mode_follower = ModeFollowing(
|
|
109
|
+
self.saddle_order,
|
|
110
|
+
atoms=element_list,
|
|
111
|
+
initial_target_index=target_mode_index,
|
|
112
|
+
adaptive=adaptive,
|
|
113
|
+
update_rate=update_rate,
|
|
114
|
+
use_hungarian=use_hungarian,
|
|
115
|
+
gradient_weight=gradient_weight,
|
|
116
|
+
debug_mode=config.get("debug_mode", False)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if self.display_flag:
|
|
120
|
+
print(f"MF-RS-I-RFO Initialized:")
|
|
121
|
+
print(f" - Update Method: {self.hessian_update_method}")
|
|
122
|
+
print(f" - Target Index: {target_mode_index}")
|
|
123
|
+
print(f" - Mode Following: Adaptive={adaptive} (EMA Rate={update_rate})")
|
|
124
|
+
print(f" - Gradient Bias: {gradient_weight}")
|
|
125
|
+
print(f" - Matching: {'Hungarian' if use_hungarian else 'Greedy'}")
|
|
126
|
+
print(f" - Mass-Weighted: {'Yes' if element_list else 'No'}")
|
|
127
|
+
|
|
128
|
+
def run(self, geom_num_list, B_g, pre_B_g=[], pre_geom=[], B_e=0.0, pre_B_e=0.0, pre_move_vector=[], initial_geom_num_list=[], g=[], pre_g=[]):
|
|
129
|
+
"""
|
|
130
|
+
Execute one step of RS-I-RFO with Advanced Mode Following.
|
|
131
|
+
"""
|
|
132
|
+
self.log(f"\n{'='*50}\nMF-RS-I-RFO Iteration {self.iteration}\n{'='*50}", force=True)
|
|
133
|
+
|
|
134
|
+
if self.Initialization:
|
|
135
|
+
self.prev_eigvec_min = None
|
|
136
|
+
self.prev_eigvec_size = None
|
|
137
|
+
self.predicted_energy_changes = []
|
|
138
|
+
self.actual_energy_changes = []
|
|
139
|
+
self.prev_geometry = None
|
|
140
|
+
self.prev_gradient = None
|
|
141
|
+
self.prev_energy = None
|
|
142
|
+
self.converged = False
|
|
143
|
+
self.iteration = 0
|
|
144
|
+
self.Initialization = False
|
|
145
|
+
|
|
146
|
+
if self.hessian is None:
|
|
147
|
+
raise ValueError("Hessian matrix must be set")
|
|
148
|
+
|
|
149
|
+
if self.prev_geometry is not None and self.prev_gradient is not None and len(pre_g) > 0 and len(pre_geom) > 0:
|
|
150
|
+
self.update_hessian(geom_num_list, g, pre_geom, pre_g)
|
|
151
|
+
|
|
152
|
+
gradient_norm = np.linalg.norm(B_g)
|
|
153
|
+
self.log(f"Gradient norm: {gradient_norm:.6f}", force=True)
|
|
154
|
+
|
|
155
|
+
if gradient_norm < self.gradient_norm_threshold:
|
|
156
|
+
self.log(f"Converged: Gradient norm", force=True)
|
|
157
|
+
self.converged = True
|
|
158
|
+
|
|
159
|
+
if self.actual_energy_changes and abs(self.actual_energy_changes[-1]) < self.energy_change_threshold:
|
|
160
|
+
self.log(f"Converged: Energy change", force=True)
|
|
161
|
+
self.converged = True
|
|
162
|
+
|
|
163
|
+
current_energy = B_e
|
|
164
|
+
gradient = np.asarray(B_g).ravel()
|
|
165
|
+
|
|
166
|
+
tmp_hess = self.hessian
|
|
167
|
+
if self.bias_hessian is not None:
|
|
168
|
+
H_base = tmp_hess + self.bias_hessian
|
|
169
|
+
else:
|
|
170
|
+
H_base = tmp_hess
|
|
171
|
+
|
|
172
|
+
H = Calculationtools().project_out_hess_tr_and_rot_for_coord(
|
|
173
|
+
H_base, geom_num_list.reshape(-1, 3), geom_num_list.reshape(-1, 3), False
|
|
174
|
+
)
|
|
175
|
+
H = 0.5 * (H + H.T)
|
|
176
|
+
|
|
177
|
+
eigvals, eigvecs = self.compute_eigendecomposition_with_shift(H)
|
|
178
|
+
self.check_hessian_conditioning(eigvals)
|
|
179
|
+
|
|
180
|
+
# =========================================================================
|
|
181
|
+
# Mode Following: Identify Targets
|
|
182
|
+
# =========================================================================
|
|
183
|
+
target_indices = []
|
|
184
|
+
|
|
185
|
+
if self.saddle_order > 0:
|
|
186
|
+
if self.use_mode_following:
|
|
187
|
+
if self.iteration == 0:
|
|
188
|
+
self.log(f"Init Mode Following...")
|
|
189
|
+
self.mode_follower.set_references(eigvecs, eigvals)
|
|
190
|
+
# For iter 0, use start indices directly
|
|
191
|
+
start = self.mode_follower.target_offset
|
|
192
|
+
target_indices = list(range(start, start + self.saddle_order))
|
|
193
|
+
else:
|
|
194
|
+
# Find matching modes (pass gradient for optional bias)
|
|
195
|
+
target_indices = self.mode_follower.get_matched_indices(
|
|
196
|
+
eigvecs, eigvals, current_gradient=gradient
|
|
197
|
+
)
|
|
198
|
+
else:
|
|
199
|
+
target_indices = list(range(self.saddle_order))
|
|
200
|
+
|
|
201
|
+
# =========================================================================
|
|
202
|
+
# Trust Radius
|
|
203
|
+
# =========================================================================
|
|
204
|
+
if self.iteration > 0 and self.prev_energy is not None:
|
|
205
|
+
actual_change = B_e - self.prev_energy
|
|
206
|
+
if len(self.actual_energy_changes) >= 3:
|
|
207
|
+
self.actual_energy_changes.pop(0)
|
|
208
|
+
self.actual_energy_changes.append(actual_change)
|
|
209
|
+
|
|
210
|
+
if self.predicted_energy_changes:
|
|
211
|
+
# Use curvature of the TRACKED mode
|
|
212
|
+
min_eigval_for_tr = eigvals[0]
|
|
213
|
+
if target_indices and target_indices[0] < len(eigvals):
|
|
214
|
+
min_eigval_for_tr = eigvals[target_indices[0]]
|
|
215
|
+
|
|
216
|
+
self.adjust_trust_radius(
|
|
217
|
+
actual_change,
|
|
218
|
+
self.predicted_energy_changes[-1],
|
|
219
|
+
min_eigval_for_tr,
|
|
220
|
+
gradient_norm
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# =========================================================================
|
|
224
|
+
# Image Surface Construction
|
|
225
|
+
# =========================================================================
|
|
226
|
+
P = np.eye(gradient.size)
|
|
227
|
+
|
|
228
|
+
for idx in target_indices:
|
|
229
|
+
if idx < len(eigvals) and np.abs(eigvals[idx]) > 1e-10:
|
|
230
|
+
trans_vec = eigvecs[:, idx]
|
|
231
|
+
if self.NEB_mode:
|
|
232
|
+
P -= np.outer(trans_vec, trans_vec)
|
|
233
|
+
else:
|
|
234
|
+
P -= 2 * np.outer(trans_vec, trans_vec)
|
|
235
|
+
|
|
236
|
+
H_star = np.dot(P, H)
|
|
237
|
+
H_star = 0.5 * (H_star + H_star.T)
|
|
238
|
+
grad_star = np.dot(P, gradient)
|
|
239
|
+
|
|
240
|
+
eigvals_star, eigvecs_star = self.compute_eigendecomposition_with_shift(H_star)
|
|
241
|
+
eigvals_star, eigvecs_star = self.filter_small_eigvals(eigvals_star, eigvecs_star)
|
|
242
|
+
|
|
243
|
+
current_eigvec_size = eigvecs_star.shape[1]
|
|
244
|
+
if self.prev_eigvec_size is not None and self.prev_eigvec_size != current_eigvec_size:
|
|
245
|
+
self.prev_eigvec_min = None
|
|
246
|
+
self.prev_eigvec_size = current_eigvec_size
|
|
247
|
+
|
|
248
|
+
move_vector = self.get_rs_step(eigvals_star, eigvecs_star, grad_star)
|
|
249
|
+
|
|
250
|
+
predicted_change = self.rfo_model(gradient, H, move_vector)
|
|
251
|
+
|
|
252
|
+
if len(self.predicted_energy_changes) >= 3:
|
|
253
|
+
self.predicted_energy_changes.pop(0)
|
|
254
|
+
self.predicted_energy_changes.append(predicted_change)
|
|
255
|
+
|
|
256
|
+
self.log(f"Predicted energy change: {predicted_change:.6f}", force=True)
|
|
257
|
+
|
|
258
|
+
if self.actual_energy_changes and len(self.predicted_energy_changes) > 1:
|
|
259
|
+
self.evaluate_step_quality()
|
|
260
|
+
|
|
261
|
+
self.prev_geometry = geom_num_list
|
|
262
|
+
self.prev_gradient = B_g
|
|
263
|
+
self.prev_energy = current_energy
|
|
264
|
+
self.iteration += 1
|
|
265
|
+
|
|
266
|
+
return -1 * move_vector.reshape(-1, 1)
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.optimize import linear_sum_assignment
|
|
3
|
+
# Import atomic_mass from your package
|
|
4
|
+
from multioptpy.Parameters.atomic_mass import atomic_mass
|
|
5
|
+
|
|
6
|
+
class ModeFollowing:
|
|
7
|
+
"""
|
|
8
|
+
Mode Tracking Class.
|
|
9
|
+
|
|
10
|
+
Features:
|
|
11
|
+
1. Mass-Weighted Overlap (MWO): Physically correct projection.
|
|
12
|
+
2. Adaptive / Static Reference: Follows mode rotation.
|
|
13
|
+
3. Gradient Overlap: Biases selection towards the current force direction.
|
|
14
|
+
4. Maximum Weight Matching (Hungarian Algo): Solves the global assignment problem
|
|
15
|
+
to prevent mode swapping in dense spectra.
|
|
16
|
+
5. Exponential Moving Average (EMA): Filters noise while adapting to mode rotation.
|
|
17
|
+
"""
|
|
18
|
+
def __init__(self, saddle_order, atoms=None, initial_target_index=0,
|
|
19
|
+
adaptive=True, update_rate=1.0,
|
|
20
|
+
use_hungarian=True, gradient_weight=0.0, debug_mode=False):
|
|
21
|
+
"""
|
|
22
|
+
Parameters:
|
|
23
|
+
saddle_order (int): Number of modes to track.
|
|
24
|
+
atoms (list): List of atomic numbers/symbols for MWO.
|
|
25
|
+
initial_target_index (int): 0-based index of the starting mode.
|
|
26
|
+
adaptive (bool): Update reference vectors (True) or keep static (False).
|
|
27
|
+
update_rate (float): EMA coefficient (alpha) for adaptive update.
|
|
28
|
+
1.0 = Full replacement (Standard Adaptive MOM).
|
|
29
|
+
0.5 = Balanced (Half old, half new).
|
|
30
|
+
0.0 = No update (Same as adaptive=False).
|
|
31
|
+
use_hungarian (bool): Use Kuhn-Munkres algorithm for global matching.
|
|
32
|
+
gradient_weight (float): Weight for Gradient Overlap (0.0 to 1.0).
|
|
33
|
+
debug_mode (bool): Verbose logging.
|
|
34
|
+
"""
|
|
35
|
+
self.saddle_order = saddle_order
|
|
36
|
+
self.debug_mode = debug_mode
|
|
37
|
+
self.reference_modes = []
|
|
38
|
+
self.reference_indices = []
|
|
39
|
+
self.is_initialized = False
|
|
40
|
+
self.target_offset = initial_target_index
|
|
41
|
+
|
|
42
|
+
self.adaptive = adaptive
|
|
43
|
+
self.update_rate = update_rate # EMA alpha
|
|
44
|
+
self.use_hungarian = use_hungarian
|
|
45
|
+
self.gradient_weight = gradient_weight
|
|
46
|
+
|
|
47
|
+
# Prepare Mass Weights
|
|
48
|
+
self.mass_weights = None
|
|
49
|
+
self.mass_sqrt = None
|
|
50
|
+
strategies = []
|
|
51
|
+
|
|
52
|
+
if atoms is not None:
|
|
53
|
+
try:
|
|
54
|
+
masses = [atomic_mass(a) for a in atoms]
|
|
55
|
+
weights_list = []
|
|
56
|
+
for m in masses:
|
|
57
|
+
weights_list.extend([m, m, m])
|
|
58
|
+
self.mass_weights = np.array(weights_list)
|
|
59
|
+
# Sqrt weights for norm calculation: |v|_M = |v * sqrt(M)|
|
|
60
|
+
self.mass_sqrt = np.sqrt(self.mass_weights)
|
|
61
|
+
self.log(f"Config: MWO enabled ({len(atoms)} atoms).")
|
|
62
|
+
strategies.append("Mass-Weighted")
|
|
63
|
+
except Exception as e:
|
|
64
|
+
print(f"[ModeFollowing] Warning: Mass init failed ({e}). Using Cartesian.")
|
|
65
|
+
self.mass_weights = None
|
|
66
|
+
strategies.append("Cartesian")
|
|
67
|
+
else:
|
|
68
|
+
strategies.append("Cartesian")
|
|
69
|
+
|
|
70
|
+
if self.adaptive:
|
|
71
|
+
strategies.append(f"Adaptive(EMA={self.update_rate})")
|
|
72
|
+
else:
|
|
73
|
+
strategies.append("Static")
|
|
74
|
+
|
|
75
|
+
if self.use_hungarian:
|
|
76
|
+
strategies.append("Hungarian")
|
|
77
|
+
else:
|
|
78
|
+
strategies.append("Greedy")
|
|
79
|
+
|
|
80
|
+
if self.gradient_weight > 0:
|
|
81
|
+
strategies.append(f"GradBias({self.gradient_weight})")
|
|
82
|
+
|
|
83
|
+
self.log(f"Config: {', '.join(strategies)}")
|
|
84
|
+
self.strategies = strategies
|
|
85
|
+
|
|
86
|
+
def log(self, message):
|
|
87
|
+
if self.debug_mode:
|
|
88
|
+
print(f"[ModeFollowing] {message}")
|
|
89
|
+
|
|
90
|
+
def _calc_overlap(self, v1, v2):
|
|
91
|
+
"""
|
|
92
|
+
Calculate normalized Overlap (S_ij).
|
|
93
|
+
Returns signed value (-1.0 to 1.0).
|
|
94
|
+
"""
|
|
95
|
+
if self.mass_weights is None:
|
|
96
|
+
dot_val = np.dot(v1, v2)
|
|
97
|
+
norm1 = np.linalg.norm(v1)
|
|
98
|
+
norm2 = np.linalg.norm(v2)
|
|
99
|
+
else:
|
|
100
|
+
dot_val = np.dot(v1 * self.mass_weights, v2)
|
|
101
|
+
# Use pre-calculated sqrt weights for efficiency if available
|
|
102
|
+
if self.mass_sqrt is not None:
|
|
103
|
+
norm1 = np.linalg.norm(v1 * self.mass_sqrt)
|
|
104
|
+
norm2 = np.linalg.norm(v2 * self.mass_sqrt)
|
|
105
|
+
else:
|
|
106
|
+
norm1 = np.sqrt(np.dot(v1 * self.mass_weights, v1))
|
|
107
|
+
norm2 = np.sqrt(np.dot(v2 * self.mass_weights, v2))
|
|
108
|
+
|
|
109
|
+
if norm1 < 1e-12 or norm2 < 1e-12:
|
|
110
|
+
return 0.0
|
|
111
|
+
return dot_val / (norm1 * norm2)
|
|
112
|
+
|
|
113
|
+
def _normalize(self, v):
|
|
114
|
+
"""Normalize vector according to the current metric (Mass-Weighted or Cartesian)."""
|
|
115
|
+
if self.mass_weights is None:
|
|
116
|
+
norm = np.linalg.norm(v)
|
|
117
|
+
else:
|
|
118
|
+
if self.mass_sqrt is not None:
|
|
119
|
+
norm = np.linalg.norm(v * self.mass_sqrt)
|
|
120
|
+
else:
|
|
121
|
+
norm = np.sqrt(np.dot(v * self.mass_weights, v))
|
|
122
|
+
|
|
123
|
+
if norm < 1e-12:
|
|
124
|
+
return v # Avoid division by zero
|
|
125
|
+
return v / norm
|
|
126
|
+
|
|
127
|
+
def set_references(self, eigvecs, eigvals=None):
|
|
128
|
+
"""Set initial reference modes."""
|
|
129
|
+
self.reference_modes = []
|
|
130
|
+
self.reference_indices = []
|
|
131
|
+
n_modes = eigvecs.shape[1]
|
|
132
|
+
|
|
133
|
+
start_idx = self.target_offset
|
|
134
|
+
end_idx = start_idx + self.saddle_order
|
|
135
|
+
|
|
136
|
+
if end_idx > n_modes:
|
|
137
|
+
start_idx = 0
|
|
138
|
+
end_idx = self.saddle_order
|
|
139
|
+
print(f"[ModeFollowing] Error: Index out of bounds. Fallback to 0.")
|
|
140
|
+
|
|
141
|
+
self.log(f"Initializing references using modes [{start_idx} to {end_idx-1}]")
|
|
142
|
+
|
|
143
|
+
for i in range(start_idx, end_idx):
|
|
144
|
+
self.reference_modes.append(eigvecs[:, i].copy())
|
|
145
|
+
self.reference_indices.append(i)
|
|
146
|
+
val_str = f"{eigvals[i]:.6f}" if eigvals is not None else "N/A"
|
|
147
|
+
print(f" [ModeFollowing] Target: Mode {i} (Val: {val_str})")
|
|
148
|
+
|
|
149
|
+
self.is_initialized = True
|
|
150
|
+
|
|
151
|
+
def get_matched_indices(self, current_eigvecs, current_eigvals=None, current_gradient=None):
|
|
152
|
+
"""
|
|
153
|
+
Find best matching modes using configured strategies.
|
|
154
|
+
Updates references using EMA if adaptive=True.
|
|
155
|
+
"""
|
|
156
|
+
if not self.is_initialized:
|
|
157
|
+
raise RuntimeError("References not set.")
|
|
158
|
+
|
|
159
|
+
n_refs = len(self.reference_modes)
|
|
160
|
+
n_curr = current_eigvecs.shape[1]
|
|
161
|
+
|
|
162
|
+
# 1. Build Similarity Matrix (Cost Matrix for Hungarian)
|
|
163
|
+
# Rows: References, Cols: Current Modes
|
|
164
|
+
# Values: Absolute Overlap (0.0 to 1.0)
|
|
165
|
+
similarity_matrix = np.zeros((n_refs, n_curr))
|
|
166
|
+
sign_matrix = np.zeros((n_refs, n_curr)) # Store signs for phase correction
|
|
167
|
+
|
|
168
|
+
# Pre-calculate Gradient Overlaps if enabled
|
|
169
|
+
grad_overlaps = np.zeros(n_curr)
|
|
170
|
+
if self.gradient_weight > 0 and current_gradient is not None:
|
|
171
|
+
g_norm = np.linalg.norm(current_gradient)
|
|
172
|
+
if g_norm > 1e-10:
|
|
173
|
+
normalized_grad = current_gradient / g_norm
|
|
174
|
+
for j in range(n_curr):
|
|
175
|
+
# Use same metric (MWO/Cartesian) for consistency
|
|
176
|
+
ov = abs(self._calc_overlap(normalized_grad, current_eigvecs[:, j]))
|
|
177
|
+
grad_overlaps[j] = ov
|
|
178
|
+
|
|
179
|
+
for i in range(n_refs):
|
|
180
|
+
ref_vec = self.reference_modes[i]
|
|
181
|
+
for j in range(n_curr):
|
|
182
|
+
overlap_signed = self._calc_overlap(ref_vec, current_eigvecs[:, j])
|
|
183
|
+
overlap_abs = abs(overlap_signed)
|
|
184
|
+
|
|
185
|
+
# Base Score: Eigenvector Overlap
|
|
186
|
+
score = overlap_abs
|
|
187
|
+
|
|
188
|
+
# Add Gradient Bias
|
|
189
|
+
if self.gradient_weight > 0:
|
|
190
|
+
score += self.gradient_weight * grad_overlaps[j]
|
|
191
|
+
|
|
192
|
+
similarity_matrix[i, j] = score
|
|
193
|
+
sign_matrix[i, j] = 1.0 if overlap_signed >= 0 else -1.0
|
|
194
|
+
|
|
195
|
+
matched_indices = []
|
|
196
|
+
matched_pairs = [] # List of (ref_idx, curr_idx)
|
|
197
|
+
|
|
198
|
+
# 2. Solve Matching Problem
|
|
199
|
+
if self.use_hungarian:
|
|
200
|
+
# Hungarian Algorithm (Minimizes cost, so we neglect similarity)
|
|
201
|
+
cost_matrix = -1.0 * similarity_matrix
|
|
202
|
+
row_ind, col_ind = linear_sum_assignment(cost_matrix)
|
|
203
|
+
|
|
204
|
+
# Extract pairs
|
|
205
|
+
for r, c in zip(row_ind, col_ind):
|
|
206
|
+
matched_pairs.append((r, c))
|
|
207
|
+
|
|
208
|
+
# Sort by reference index order to keep list consistent
|
|
209
|
+
matched_pairs.sort(key=lambda x: x[0])
|
|
210
|
+
|
|
211
|
+
else:
|
|
212
|
+
# Greedy Algorithm
|
|
213
|
+
used_cols = set()
|
|
214
|
+
for i in range(n_refs):
|
|
215
|
+
best_col = -1
|
|
216
|
+
max_sim = -1.0
|
|
217
|
+
for j in range(n_curr):
|
|
218
|
+
if j in used_cols: continue
|
|
219
|
+
if similarity_matrix[i, j] > max_sim:
|
|
220
|
+
max_sim = similarity_matrix[i, j]
|
|
221
|
+
best_col = j
|
|
222
|
+
|
|
223
|
+
if best_col != -1:
|
|
224
|
+
matched_pairs.append((i, best_col))
|
|
225
|
+
used_cols.add(best_col)
|
|
226
|
+
else:
|
|
227
|
+
# Fallback (Lost track)
|
|
228
|
+
print(f" [ModeFollowing] LOST TRACK of Ref {i}")
|
|
229
|
+
matched_pairs.append((i, self.reference_indices[i] if self.reference_indices[i] < n_curr else 0))
|
|
230
|
+
|
|
231
|
+
# 3. Process Matches and Update References
|
|
232
|
+
print(f" [ModeFollowing] --- Tracking Status ---")
|
|
233
|
+
print(" Strategies: ", self.strategies)
|
|
234
|
+
|
|
235
|
+
for ref_i, curr_j in matched_pairs:
|
|
236
|
+
matched_indices.append(curr_j)
|
|
237
|
+
|
|
238
|
+
# Stats for logging
|
|
239
|
+
sim_score = similarity_matrix[ref_i, curr_j]
|
|
240
|
+
prev_idx = self.reference_indices[ref_i]
|
|
241
|
+
val_str = f"{current_eigvals[curr_j]:.5f}" if current_eigvals is not None else ""
|
|
242
|
+
|
|
243
|
+
# Phase correction sign
|
|
244
|
+
best_sign = sign_matrix[ref_i, curr_j]
|
|
245
|
+
|
|
246
|
+
print(f" Ref {ref_i} (was {prev_idx}) -> Mode {curr_j} (Score: {sim_score:.4f}) {val_str}")
|
|
247
|
+
|
|
248
|
+
if sim_score < 0.3:
|
|
249
|
+
print(f" WARNING: Very low match score!")
|
|
250
|
+
|
|
251
|
+
# --- Adaptive Update (EMA) ---
|
|
252
|
+
if self.adaptive:
|
|
253
|
+
alpha = self.update_rate
|
|
254
|
+
|
|
255
|
+
old_vec = self.reference_modes[ref_i]
|
|
256
|
+
new_vec_aligned = current_eigvecs[:, curr_j] * best_sign
|
|
257
|
+
|
|
258
|
+
# Linear combination: v_new = (1-a)*v_old + a*v_curr
|
|
259
|
+
if alpha >= 1.0:
|
|
260
|
+
updated_vec = new_vec_aligned
|
|
261
|
+
elif alpha <= 0.0:
|
|
262
|
+
updated_vec = old_vec
|
|
263
|
+
else:
|
|
264
|
+
updated_vec = (1.0 - alpha) * old_vec + alpha * new_vec_aligned
|
|
265
|
+
|
|
266
|
+
# Normalize (Important: Length must be 1 for next overlap calc)
|
|
267
|
+
# This normalization respects mass-weighting if enabled
|
|
268
|
+
self.reference_modes[ref_i] = self._normalize(updated_vec)
|
|
269
|
+
|
|
270
|
+
self.reference_indices[ref_i] = curr_j
|
|
271
|
+
|
|
272
|
+
print(f" [ModeFollowing] -----------------------")
|
|
273
|
+
return matched_indices
|
multioptpy/Utils/calc_tools.py
CHANGED
multioptpy/fileio.py
CHANGED
|
@@ -383,10 +383,10 @@ class FileIO:
|
|
|
383
383
|
return [start_data], element_list, electric_charge_and_multiplicity
|
|
384
384
|
|
|
385
385
|
|
|
386
|
-
def print_geometry_list(self, new_geometry, element_list, electric_charge_and_multiplicity):
|
|
386
|
+
def print_geometry_list(self, new_geometry, element_list, electric_charge_and_multiplicity, display_flag=True):
|
|
387
387
|
"""load structure updated geometry for next QM calculation"""
|
|
388
388
|
new_geometry = new_geometry.tolist()
|
|
389
|
-
|
|
389
|
+
|
|
390
390
|
|
|
391
391
|
# Process all geometries at once with list comprehension
|
|
392
392
|
formatted_geometries = []
|
|
@@ -394,17 +394,24 @@ class FileIO:
|
|
|
394
394
|
element = element_list[num]
|
|
395
395
|
formatted_geometry = [element] + list(map(str, geometry))
|
|
396
396
|
formatted_geometries.append(formatted_geometry)
|
|
397
|
-
print(f"{element:2} {float(geometry[0]):>17.12f} {float(geometry[1]):>17.12f} {float(geometry[2]):>17.12f}")
|
|
398
397
|
|
|
398
|
+
if display_flag:
|
|
399
|
+
for num, geometry in enumerate(new_geometry):
|
|
400
|
+
element = element_list[num]
|
|
401
|
+
print(f"{element:2} {float(geometry[0]):>17.12f} {float(geometry[1]):>17.12f} {float(geometry[2]):>17.12f}")
|
|
402
|
+
print("\n")
|
|
403
|
+
|
|
399
404
|
geometry_list = [[electric_charge_and_multiplicity, *formatted_geometries]]
|
|
400
|
-
print("")
|
|
401
405
|
|
|
402
406
|
return geometry_list
|
|
403
407
|
|
|
404
408
|
|
|
405
|
-
def make_psi4_input_file(self, geometry_list, iter):#geometry_list: ang.
|
|
409
|
+
def make_psi4_input_file(self, geometry_list, iter, path=None):#geometry_list: ang.
|
|
406
410
|
"""structure updated geometry is saved."""
|
|
407
|
-
|
|
411
|
+
if path is not None:
|
|
412
|
+
file_directory = os.path.join(path, f"samples_{self.NOEXT_START_FILE}_{iter}")
|
|
413
|
+
else:
|
|
414
|
+
file_directory = self.work_directory+"samples_"+self.NOEXT_START_FILE+"_"+str(iter)
|
|
408
415
|
tmp_cs = ["SAMPLE"+str(iter), ""]
|
|
409
416
|
|
|
410
417
|
|
multioptpy/interface.py
CHANGED
|
@@ -189,11 +189,12 @@ def call_optimizeparser(parser):
|
|
|
189
189
|
parser.add_argument('-modelhess','--use_model_hessian', nargs='?', help="use model hessian. (Default: not using model hessian If you specify only option, Improved Lindh + Grimme's D3 dispersion model hessian is used.) (ex. lindh, gfnff, gfn0xtb, fischer, fischerd3, fischerd4, schlegel, swart, lindh2007, lindh2007d3, lindh2007d4)", action=ModelhessAction, default=None)
|
|
190
190
|
parser.add_argument("-sc", "--shape_conditions", nargs="*", type=str, default=[], help="Exit optimization if these conditions are not satisfied. (e.g.) [[(ang.) gt(lt) 2,3 (bond)] [(deg.) gt(lt) 2,3,4 (bend)] ...] [[(deg.) gt(lt) 2,3,4,5 (torsion)] ...]")
|
|
191
191
|
parser.add_argument("-pc", "--projection_constrain", nargs="*", type=str, default=[], help='apply constrain conditions with projection of gradient and hessian (ex.) [[(constraint condition name) (atoms(ex. 1,2))] ...] ')
|
|
192
|
-
parser.add_argument("-oniom", "--oniom_flag", nargs="*", type=str, default=[], help='apply ONIOM method (
|
|
192
|
+
parser.add_argument("-oniom", "--oniom_flag", nargs="*", type=str, default=[], help='apply ONIOM method (Warning: This option is unavailable.)')
|
|
193
193
|
parser.add_argument("-freq", "--frequency_analysis", help="Perform normal vibrational analysis after converging geometry optimization. (Caution: Unable to use this analysis with oniom method)", action='store_true')
|
|
194
194
|
parser.add_argument("-temp", "--temperature", type=float, default='298.15', help='temperatrue to calculate thermochemistry (Unit: K) (default: 298.15K)')
|
|
195
195
|
parser.add_argument("-press", "--pressure", type=float, default='101325', help='pressure to calculate thermochemistry (Unit: Pa) (default: 101325Pa)')
|
|
196
|
-
parser.add_argument("-negeigval", "--detect_negative_eigenvalues", help="Detect negative eigenvalues in the Hessian matrix at ITR. 0 if you
|
|
196
|
+
parser.add_argument("-negeigval", "--detect_negative_eigenvalues", help="Detect negative eigenvalues in the Hessian matrix at ITR. 0 if you calculate exact hessian (-fc >0). If negative eigenvalues are not detected and saddle_order > 0, the optimization is stopped.", action='store_true')
|
|
197
|
+
parser.add_argument("-mf", "--model_function", nargs="*", type=str, default=[], help='minimize model function(ex.) [[model function type (seam, avoid, conical etc.)] [electronic charge] [spin multiplicity]] ')
|
|
197
198
|
|
|
198
199
|
return parser
|
|
199
200
|
|