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.
@@ -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
@@ -599,6 +599,7 @@ def calc_dihedral_angle_from_vec(vector1, vector2, vector3):
599
599
  norm_v1 = np.linalg.norm(v1)
600
600
  if np.abs(norm_v1) < 1e-15:
601
601
  norm_v1 += 1e-15
602
+ norm_v2 = np.linalg.norm(v2)
602
603
  if np.abs(norm_v2) < 1e-15:
603
604
  norm_v2 += 1e-15
604
605
 
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
- print("\n")
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
- file_directory = self.work_directory+"samples_"+self.NOEXT_START_FILE+"_"+str(iter)
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 (low layer: GFN1-xTB) (ex.) [(atom_number of high layer (ex. 1,2))] (caution) -pc option is not available. If there are not link atoms, please input "none"')
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 caluculate exact hessian (-fc >0). If negative eigenvalues are not detected and saddle_order > 0, the optimization is stopped.", action='store_true')
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