MultiOptPy 1.20.4__py3-none-any.whl → 1.20.5__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.
@@ -22,6 +22,8 @@ from multioptpy.Calculator.ase_tools.fairchem import ASE_FAIRCHEM
22
22
  from multioptpy.Calculator.ase_tools.mace import ASE_MACE
23
23
  from multioptpy.Calculator.ase_tools.mopac import ASE_MOPAC
24
24
  from multioptpy.Calculator.ase_tools.pygfn0 import ASE_GFN0
25
+ from multioptpy.Calculator.ase_tools.gxtb_dev import ASE_gxTB_Dev
26
+
25
27
 
26
28
  """
27
29
  referrence:
@@ -89,7 +91,9 @@ class Calculation:
89
91
  if self.software_type == "gaussian":
90
92
  print("Calculating exact Hessian using Gaussian...")
91
93
  exact_hess = calc_obj.calc_analytic_hessian() # in hartree/Bohr^2
92
-
94
+ elif self.software_type == "orca":
95
+ hess_path = calc_obj.run_frequency_analysis()
96
+ exact_hess = calc_obj.get_hessian_matrix(hess_path)
93
97
  else:
94
98
  vib = Vibrations(atoms=calc_obj.atom_obj, delta=0.001, name="z_hess_"+timestamp)
95
99
  vib.run()
@@ -124,7 +128,7 @@ class Calculation:
124
128
  file_list = glob.glob(file_directory+"/*_[0-9].xyz")
125
129
 
126
130
  for num, input_file in enumerate(file_list):
127
- try:
131
+ if True:#try:
128
132
  if geom_num_list is None:
129
133
  positions, _, electric_charge_and_multiplicity = xyz2list(input_file, electric_charge_and_multiplicity)
130
134
  else:
@@ -155,11 +159,11 @@ class Calculation:
155
159
  elif iter % self.FC_COUNT == 0 or self.hessian_flag:
156
160
  # exact numerical hessian
157
161
  _ = self.calc_exact_hess(calc_obj, positions, element_list)
158
- except Exception as error:
159
- print(error)
160
- print("This molecule could not be optimized.")
161
- finish_frag = True
162
- return np.array([0]), np.array([0]), np.array([0]), finish_frag
162
+ #except Exception as error:
163
+ # print(error)
164
+ # print("This molecule could not be optimized.")
165
+ # finish_frag = True
166
+ # return np.array([0]), np.array([0]), np.array([0]), finish_frag
163
167
 
164
168
  positions /= self.bohr2angstroms
165
169
  self.energy = e
@@ -286,7 +290,10 @@ class ASEEngine(CalculationEngine):
286
290
  timestamp = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S_%f")[:-2]
287
291
  if software_type == "gaussian":
288
292
  exact_hess = calc_obj.calc_analytic_hessian() # in hartree/Bohr^2
289
- exact_hess = exact_hess
293
+
294
+ elif software_type == "orca":
295
+ hess_path = calc_obj.run_frequency_analysis()
296
+ exact_hess = calc_obj.get_hessian_matrix(hess_path)
290
297
  else:
291
298
  vib = Vibrations(atoms=calc_obj.atom_obj, delta=0.001, name="z_hess_"+timestamp)
292
299
  vib.run()
@@ -431,6 +438,12 @@ def setup_calculator(atom_obj, software_type, electric_charge_and_multiplicity,
431
438
  input_file=input_file)
432
439
  return calc_obj
433
440
 
441
+ if software_type == "gxtb_dev":
442
+ calc_obj = ASE_gxTB_Dev(atom_obj=atom_obj,
443
+ electric_charge_and_multiplicity=electric_charge_and_multiplicity,
444
+ software_type=software_type)
445
+ return calc_obj
446
+
434
447
  # Unknown software type
435
448
  raise ValueError(f"Unsupported software type: {software_type}")
436
449
 
@@ -0,0 +1,41 @@
1
+ class ASE_gxTB_Dev:
2
+ """
3
+ Wrapper class to set up and run g-xTB (preliminary version) calculations via ASE.
4
+ $ pip install pygxtb==0.7.0
5
+
6
+ """
7
+ def __init__(self, **kwargs):
8
+
9
+ self.atom_obj = kwargs.get('atom_obj', None)
10
+ self.electric_charge_and_multiplicity = kwargs.get('electric_charge_and_multiplicity', None)
11
+ self.software_path = kwargs.get('software_path', None)
12
+ self.software_type = kwargs.get('software_type', None)
13
+ from pygxtb import PygxTB
14
+ self.pygxtb = PygxTB
15
+
16
+ def set_calculator(self):
17
+ """
18
+ Sets the ASE calculator object based on software_type.
19
+ """
20
+
21
+ charge = 0 # Default charge
22
+ if self.electric_charge_and_multiplicity is not None:
23
+ try:
24
+ # Get charge from [charge, multiplicity] list
25
+ charge = int(self.electric_charge_and_multiplicity[0])
26
+ except (IndexError, TypeError, ValueError):
27
+ print(f"Warning: Could not parse charge from {self.electric_charge_and_multiplicity}. Defaulting to 0.")
28
+ pass
29
+
30
+ # Instantiate GFN0 class and pass the charge
31
+ gxtb_calc = self.pygxtb(charge=charge)
32
+ return gxtb_calc
33
+
34
+
35
+ def run(self):
36
+ """
37
+ Attaches the calculator to the atoms object and returns it.
38
+ """
39
+ calc_obj = self.set_calculator()
40
+ self.atom_obj.calc = calc_obj
41
+ return self.atom_obj
@@ -1,22 +1,236 @@
1
1
  import os
2
+ import shutil
3
+ import subprocess
4
+ import numpy as np
5
+ from ase.calculators.orca import ORCA, OrcaProfile
6
+ from ase.data import atomic_numbers
7
+
8
+ """
9
+ Please specify the absolute path to the ORCA executable in software_path.conf using the format orca::<path>. For Linux, provide the path to the binary (e.g., /absolute/path/to/orca), and for Windows, provide the path to the executable file (e.g., C:\absolute\path\to\orca.exe).
10
+ """
11
+
2
12
 
3
13
  class ASE_ORCA:
14
+ TARGET_ORCA_VERSION = '6.1.0'
15
+
4
16
  def __init__(self, **kwargs):
5
17
  self.atom_obj = kwargs.get('atom_obj', None)
6
18
  self.electric_charge_and_multiplicity = kwargs.get('electric_charge_and_multiplicity', None)
7
- self.input_file = kwargs.get('input_file', None)
19
+
20
+ # NOTE: self.input_file is updated in _setup_calculator to enforce 'orca.inp' in CWD.
21
+ raw_input_file = kwargs.get('input_file', 'orca.inp')
22
+ self.input_file = os.path.abspath(raw_input_file).replace('\\', '/')
23
+
8
24
  self.orca_path = kwargs.get('orca_path', None)
9
- self.functional = kwargs.get('functional', None)
10
- self.basis_set = kwargs.get('basis_set', None)
11
-
25
+ self.functional = kwargs.get('functional', 'B3LYP')
26
+ self.basis_set = kwargs.get('basis_set', 'def2-SVP')
27
+
28
+ # Optional ORCA input blocks (raw string)
29
+ self.orca_blocks = kwargs.get('orca_blocks', '')
30
+
31
+ # Auto-fix for unsupported Pople basis sets on heavy elements
32
+ self.auto_fix_basis = kwargs.get('auto_fix_basis', True)
33
+ self.heavy_atom_basis = kwargs.get('heavy_atom_basis', 'def2-SVP')
34
+
35
+ def _resolve_orca_exe(self, provided_path):
36
+ """
37
+ Resolve the absolute path to the ORCA executable.
38
+ Handles directories, stripping whitespace from config files, and WSL/Windows paths.
39
+ """
40
+ if not provided_path:
41
+ return None
42
+
43
+ # CRITICAL FIX: Strip whitespace/newlines that might come from config parsing
44
+ clean_path = provided_path.strip()
45
+ # Expand ~ to home directory if present
46
+ clean_path = os.path.expanduser(clean_path)
47
+ clean_path = os.path.normpath(clean_path)
48
+
49
+ candidates = []
50
+ # If the path is a directory, look for the executable inside it
51
+ if os.path.isdir(clean_path):
52
+ candidates.append(os.path.join(clean_path, 'orca'))
53
+ candidates.append(os.path.join(clean_path, 'orca.exe'))
54
+ else:
55
+ # If it's a file path (or doesn't exist yet), use it as is
56
+ candidates.append(clean_path)
57
+
58
+ for candidate in candidates:
59
+ # Check if file exists
60
+ if os.path.exists(candidate) and os.path.isfile(candidate):
61
+ return os.path.abspath(candidate)
62
+
63
+ # Check system PATH
64
+ resolved = shutil.which(candidate)
65
+ if resolved and os.path.exists(resolved):
66
+ return os.path.abspath(resolved)
67
+
68
+ # Use repr() in error message to reveal hidden characters like \n
69
+ candidate_reprs = [repr(c) for c in candidates]
70
+ raise FileNotFoundError(f"Cannot locate ORCA executable. Checked: {', '.join(candidate_reprs)}")
71
+
72
+ def _is_pople_basis(self, basis_name):
73
+ if not basis_name: return False
74
+ b = basis_name.strip().lower()
75
+ return b.startswith("6-31") or b.startswith("6-311")
76
+
77
+ def _get_heavy_elements_for_pople(self):
78
+ if self.atom_obj is None: return []
79
+ symbols = self.atom_obj.get_chemical_symbols()
80
+ return sorted({s for s in symbols if atomic_numbers.get(s, 0) > 30})
81
+
82
+ def _build_orca_blocks(self):
83
+ blocks = (self.orca_blocks or "").strip()
84
+ heavy_elements = []
85
+ if self._is_pople_basis(self.basis_set):
86
+ heavy_elements = self._get_heavy_elements_for_pople()
87
+
88
+ if heavy_elements:
89
+ if not self.auto_fix_basis:
90
+ raise ValueError("Unsupported Pople basis for heavy elements.")
91
+
92
+ basis_lines = ["%basis"]
93
+ for elem in heavy_elements:
94
+ # Per user instruction: No ECP, just GTO
95
+ basis_lines.append(f' NewGTO {elem} "{self.heavy_atom_basis}"')
96
+ basis_lines.append("end")
97
+
98
+ if blocks:
99
+ blocks = blocks + "\n" + "\n".join(basis_lines)
100
+ else:
101
+ blocks = "\n".join(basis_lines)
102
+
103
+ return blocks if blocks else ""
104
+
105
+ def _print_orca_output_on_error(self):
106
+ """Helper to print ORCA output file content if it exists."""
107
+ if not hasattr(self, 'input_file'): return
108
+
109
+ out_file = os.path.splitext(self.input_file)[0] + ".out"
110
+ if os.path.exists(out_file):
111
+ print(f"\n--- ORCA OUTPUT ({out_file}) ---")
112
+ try:
113
+ with open(out_file, 'r', encoding='utf-8', errors='replace') as f:
114
+ content = f.read()
115
+ print(content[-3000:] if len(content) > 3000 else content)
116
+ except Exception as e:
117
+ print(f"Failed to read output file: {e}")
118
+ print("--- END ORCA OUTPUT ---\n")
119
+ else:
120
+ print(f"\n--- ORCA OUTPUT NOT FOUND ({out_file}) ---\n")
121
+
122
+ def _setup_calculator(self, task_keyword):
123
+ # Force usage of Current Working Directory + "orca.inp"
124
+ cwd = os.getcwd()
125
+ label_path = os.path.join(cwd, 'orca').replace('\\', '/')
126
+ self.input_file = label_path + '.inp'
127
+
128
+ print(f"DEBUG: ASE Label Path : {label_path}")
129
+
130
+ simple_input = f"{self.functional} {self.basis_set} {task_keyword}"
131
+
132
+ profile_obj = None
133
+ if self.orca_path:
134
+ real_exe_path = self._resolve_orca_exe(self.orca_path)
135
+ orca_dir = os.path.dirname(real_exe_path)
136
+ path_env = os.environ.get('PATH', '')
137
+ if orca_dir not in path_env:
138
+ os.environ['PATH'] = orca_dir + os.pathsep + path_env
139
+
140
+ ase_safe_path = real_exe_path.replace('\\', '/')
141
+ profile_obj = OrcaProfile(ase_safe_path)
142
+ print(f"DEBUG: ORCA Executable: {ase_safe_path}")
143
+
144
+ orca_blocks = self._build_orca_blocks()
145
+
146
+ calc = ORCA(
147
+ label=label_path,
148
+ profile=profile_obj,
149
+ charge=int(self.electric_charge_and_multiplicity[0]),
150
+ mult=int(self.electric_charge_and_multiplicity[1]),
151
+ orcasimpleinput=simple_input,
152
+ orcablocks=orca_blocks
153
+ )
154
+
155
+ self.atom_obj.calc = calc
156
+ return self.atom_obj
157
+
12
158
  def run(self):
13
-
14
- from ase.calculators.orca import ORCA
15
- input_dir = os.path.dirname(self.input_file)
16
- self.atom_obj.calc = ORCA(label=input_dir,
17
- profile=self.orca_path,
18
- charge=int(self.electric_charge_and_multiplicity[0]),
19
- mult=int(self.electric_charge_and_multiplicity[1]),
20
- orcasimpleinput=self.functional+' '+self.basis_set)
21
- #orcablocks='%pal nprocs 16 end')
22
- return self.atom_obj
159
+ self._setup_calculator("EnGrad")
160
+ print(f"--- Starting Gradient Calculation (ORCA {self.TARGET_ORCA_VERSION}) ---")
161
+ try:
162
+ forces = self.atom_obj.get_forces()
163
+ potential_energy = self.atom_obj.get_potential_energy()
164
+ print("Gradient calculation completed.")
165
+ return forces, potential_energy
166
+ except subprocess.CalledProcessError as e:
167
+ print(f"CRITICAL: ORCA execution failed with exit code {e.returncode}")
168
+ self._print_orca_output_on_error()
169
+ raise e
170
+ except Exception as e:
171
+ print(f"CRITICAL: An unexpected error occurred: {e}")
172
+ self._print_orca_output_on_error()
173
+ raise e
174
+
175
+ def run_frequency_analysis(self):
176
+ print(f"--- Starting Frequency Calculation (ORCA {self.TARGET_ORCA_VERSION}) ---")
177
+ self._setup_calculator("Freq")
178
+ try:
179
+ self.atom_obj.get_potential_energy()
180
+ print("Frequency calculation completed.")
181
+ # Use self.input_file to construct hess_path instead of relying on calc.label
182
+ hess_path = os.path.splitext(self.input_file)[0] + ".hess"
183
+ return hess_path
184
+ except subprocess.CalledProcessError as e:
185
+ print(f"CRITICAL: ORCA Frequency execution failed with exit code {e.returncode}")
186
+ self._print_orca_output_on_error()
187
+ raise e
188
+ except Exception as e:
189
+ print(f"CRITICAL: An unexpected error occurred during frequency analysis: {e}")
190
+ self._print_orca_output_on_error()
191
+ raise e
192
+
193
+ def get_hessian_matrix(self, hess_file_path=None):
194
+ if hess_file_path is None:
195
+ # Default to orca.hess in the same dir as input_file
196
+ input_dir = os.path.dirname(self.input_file)
197
+ hess_file_path = os.path.join(input_dir, 'orca.hess')
198
+
199
+ if not os.path.exists(hess_file_path):
200
+ raise FileNotFoundError(f"Hessian file not found: {hess_file_path}")
201
+
202
+ print(f"Reading Hessian from: {hess_file_path}")
203
+
204
+ with open(hess_file_path, 'r', encoding='utf-8') as f:
205
+ lines = f.readlines()
206
+
207
+ hessian_matrix = None
208
+ iterator = iter(lines)
209
+ for line in iterator:
210
+ if "$hessian" in line:
211
+ dim_line = next(iterator).strip().split()
212
+ # Parse dimensions: square matrix usually provides just one dimension
213
+ if len(dim_line) == 1:
214
+ n_rows = int(dim_line[0])
215
+ n_cols = n_rows
216
+ else:
217
+ n_rows, n_cols = map(int, dim_line[:2])
218
+
219
+ hessian_matrix = np.zeros((n_rows, n_cols))
220
+ col_pointer = 0
221
+ while col_pointer < n_cols:
222
+ header = next(iterator).strip()
223
+ if not header: break
224
+ col_indices = [int(c) for c in header.split()]
225
+ for r in range(n_rows):
226
+ row_data = next(iterator).strip().split()
227
+ row_idx = int(row_data[0])
228
+ values = [float(x) for x in row_data[1:]]
229
+ for i, val in enumerate(values):
230
+ hessian_matrix[row_idx, col_indices[i]] = val
231
+ col_pointer += len(col_indices)
232
+ break
233
+
234
+ if hessian_matrix is None:
235
+ raise ValueError("Could not find $hessian block in the file.")
236
+ return hessian_matrix
@@ -0,0 +1,314 @@
1
+ import numpy as np
2
+ import os
3
+ import shutil
4
+
5
+ from multioptpy.Utils.calc_tools import Calculationtools
6
+ from multioptpy.Visualization.visualization import Graph
7
+
8
+ class SpringPairMethod:
9
+ """
10
+ Implementation of the Spring Pair Method (SPM) with Adaptive Step Size & Momentum.
11
+ Modified to accept a SINGLE initial structure and auto-generate the second one via perturbation.
12
+ """
13
+ def __init__(self, config):
14
+ self.config = config
15
+
16
+ # --- TUNING PARAMETERS ---
17
+ self.k_spring = 10.0
18
+ self.l_s = max(getattr(self.config, 'L_covergence', 0.1), 0.1)
19
+ self.initial_drift_step = 0.01
20
+ self.climb_step_size = 0.50
21
+ self.drift_limit = 100
22
+ self.momentum = 0.3
23
+ self.MAX_FORCE_THRESHOLD = 0.00100
24
+ self.RMS_FORCE_THRESHOLD = 0.00005
25
+
26
+ # Magnitude of random perturbation to generate Image 2 (Bohr)
27
+ self.perturbation_scale = 0.1
28
+
29
+ def RMS(self, mat):
30
+ return np.sqrt(np.mean(mat**2))
31
+
32
+ def print_info(self, dat, phase):
33
+ print(f"[[{phase} information]]")
34
+ print(f" image_1 image_2")
35
+ print(f"energy (Hartree) : {dat['energy_1']:.8f} {dat['energy_2']:.8f}")
36
+ print(f"gradient RMS : {self.RMS(dat['gradient_1']):.6f} {self.RMS(dat['gradient_2']):.6f}")
37
+
38
+ if "perp_force_1" in dat:
39
+ print(f"perp_force (Drift) : {self.RMS(dat['perp_force_1']):.6f} {self.RMS(dat['perp_force_2']):.6f}")
40
+ print(f"spring_force : {self.RMS(dat['spring_force_1']):.6f} {self.RMS(dat['spring_force_2']):.6f}")
41
+
42
+ print(f"spring_length (Bohr) : {dat['spring_length']:.6f}")
43
+ step_info = dat.get('step_size', 0)
44
+ print(f"DEBUG: k={self.k_spring:.1f}, step={step_info:.6f}")
45
+ print("-" * 80)
46
+ return
47
+
48
+ def get_spring_vectors(self, geom_1, geom_2):
49
+ diff = geom_2 - geom_1
50
+ dist = np.linalg.norm(diff)
51
+ if dist < 1e-10:
52
+ rand_vec = np.random.randn(*diff.shape)
53
+ unit_vec = rand_vec / np.linalg.norm(rand_vec)
54
+ dist = 1e-10
55
+ else:
56
+ unit_vec = diff / dist
57
+ return dist, unit_vec
58
+
59
+ def decompose_gradient(self, gradient, unit_vec):
60
+ grad_flat = gradient.flatten()
61
+ vec_flat = unit_vec.flatten()
62
+ grad_par_mag = np.dot(grad_flat, vec_flat)
63
+ grad_par = grad_par_mag * unit_vec
64
+ grad_perp = gradient - grad_par
65
+ return grad_par, grad_perp
66
+
67
+ def _generate_perturbed_structure(self, geom, scale):
68
+ """Generate a new structure by adding random perturbation to the given geometry."""
69
+ # Generate random noise
70
+ noise = np.random.randn(*geom.shape)
71
+ # Normalize to unit vectors per atom to distribute perturbation evenly
72
+ norms = np.linalg.norm(noise, axis=1, keepdims=True)
73
+ noise = noise / (norms + 1e-10)
74
+ # Scale the noise
75
+ perturbation = noise * scale
76
+ return geom + perturbation
77
+
78
+ def iteration(self, file_directory_1, SP1, element_list, electric_charge_and_multiplicity, FIO1):
79
+ """
80
+ Main SPM Optimization Loop.
81
+ Accepts ONE initial structure. Image 2 is auto-generated.
82
+ """
83
+ G = Graph(self.config.iEIP_FOLDER_DIRECTORY)
84
+ ENERGY_LIST_1, ENERGY_LIST_2 = [], []
85
+ GRAD_LIST_1, GRAD_LIST_2 = [], []
86
+
87
+ # --- Initialize Image 2 from Image 1 ---
88
+ print("### Initializing SPM ###")
89
+ print(f"Base Structure (Image 1): {file_directory_1}")
90
+
91
+ # 1. Get initial geometry of Image 1
92
+ # We perform a dummy single point calculation or just read the geom if possible.
93
+ # Here we run SP1 to get the geometry and energy reliably.
94
+ init_energy, init_grad, init_geom, err = SP1.single_point(
95
+ file_directory_1, element_list, "init_check",
96
+ electric_charge_and_multiplicity, self.config.force_data["xtb"]
97
+ )
98
+ if err:
99
+ print("[Error] Failed to read initial structure.")
100
+ return
101
+
102
+ # 2. Generate Image 2 geometry
103
+ print(f"Generating Image 2 with random perturbation (Scale: {self.perturbation_scale} Bohr)...")
104
+ init_geom_2 = self._generate_perturbed_structure(init_geom, self.perturbation_scale)
105
+
106
+ # Create a directory for Image 2 (internally managed)
107
+ # We can just use the same SP1 object but we need a file path for it.
108
+ # We will generate input files for Image 2 on the fly using FIO1 logic.
109
+ # Note: We reuse SP1 and FIO1 for both images since they share physics/settings.
110
+
111
+ # Initialize loop variables
112
+ geom_num_list_1 = init_geom
113
+ geom_num_list_2 = init_geom_2
114
+
115
+ velocity_1 = np.zeros((len(element_list), 3))
116
+ velocity_2 = np.zeros((len(element_list), 3))
117
+
118
+ current_drift_step = self.initial_drift_step
119
+
120
+ # Variables to store the latest geometry
121
+ new_geom_1 = None
122
+ new_geom_2 = None
123
+
124
+ for cycle in range(0, self.config.microiterlimit):
125
+ if os.path.isfile(self.config.iEIP_FOLDER_DIRECTORY+"end.txt"):
126
+ break
127
+
128
+ print(f"### Cycle {cycle} Start ###")
129
+
130
+ # =========================================================================
131
+ # 1. Drifting Phase
132
+ # =========================================================================
133
+ print(f"--- Drifting Phase (Cycle {cycle}) ---")
134
+
135
+ prev_force_drift_1 = None
136
+ prev_force_drift_2 = None
137
+
138
+ drift_temp_label = f"{cycle}_drift_temp"
139
+
140
+ for d_step in range(self.drift_limit):
141
+ iter_label = drift_temp_label
142
+
143
+ # Make input files for this step
144
+ # Note: file_directory_1/2 here are just strings for the input file path
145
+ # generated by _make_next_input.
146
+ # However, SP1.single_point expects the directory path or file path depending on implementation.
147
+ # Assuming _make_next_input returns the PATH to the input file.
148
+
149
+ input_path_1 = self._make_next_input(FIO1, geom_num_list_1, element_list, electric_charge_and_multiplicity, iter_label + "_img1")
150
+ input_path_2 = self._make_next_input(FIO1, geom_num_list_2, element_list, electric_charge_and_multiplicity, iter_label + "_img2")
151
+
152
+ # 1. QM Calculation
153
+ # We reuse SP1 for both. It's just a calculator.
154
+ energy_1, gradient_1, g1_read, error_flag_1 = SP1.single_point(input_path_1, element_list, iter_label + "_img1", electric_charge_and_multiplicity, self.config.force_data["xtb"])
155
+ energy_2, gradient_2, g2_read, error_flag_2 = SP1.single_point(input_path_2, element_list, iter_label + "_img2", electric_charge_and_multiplicity, self.config.force_data["xtb"])
156
+
157
+ # Update geom from read result to be safe, or stick to internal numpy array
158
+ # Using internal array (geom_num_list_1/2) is safer for consistency unless optimizer changes it.
159
+ # But let's align them just in case output orientation changed.
160
+ # g1_read, g2_read = Calculationtools().kabsch_algorithm(g1_read, g2_read)
161
+ # geom_num_list_1, geom_num_list_2 = g1_read, g2_read
162
+
163
+ # Align current internal geometries
164
+ geom_num_list_1, geom_num_list_2 = Calculationtools().kabsch_algorithm(geom_num_list_1, geom_num_list_2)
165
+
166
+ if error_flag_1 or error_flag_2:
167
+ return
168
+
169
+ # 2. Vector & Force Calculation
170
+ ds, vs = self.get_spring_vectors(geom_num_list_1, geom_num_list_2)
171
+
172
+ _, grad_perp_1 = self.decompose_gradient(gradient_1, vs)
173
+ _, grad_perp_2 = self.decompose_gradient(gradient_2, vs)
174
+
175
+ force_perp_1 = -grad_perp_1
176
+ force_perp_2 = -grad_perp_2
177
+
178
+ spring_force_mag = self.k_spring * (ds - self.l_s)
179
+ spring_force_1 = spring_force_mag * vs
180
+ spring_force_2 = spring_force_mag * (-vs)
181
+
182
+ total_force_1 = force_perp_1 + spring_force_1
183
+ total_force_2 = force_perp_2 + spring_force_2
184
+
185
+ # --- Adaptive Step Logic ---
186
+ if prev_force_drift_1 is not None:
187
+ dot_1 = np.sum(prev_force_drift_1 * total_force_1)
188
+ dot_2 = np.sum(prev_force_drift_2 * total_force_2)
189
+
190
+ if dot_1 < 0 or dot_2 < 0:
191
+ current_drift_step *= 0.5
192
+ velocity_1 *= 0.1
193
+ velocity_2 *= 0.1
194
+ print(f" [Auto-Brake] Oscillation detected at step {d_step}. Reduced drift step to {current_drift_step:.6f}")
195
+ else:
196
+ current_drift_step = min(current_drift_step * 1.05, self.initial_drift_step)
197
+
198
+ prev_force_drift_1 = total_force_1.copy()
199
+ prev_force_drift_2 = total_force_2.copy()
200
+
201
+ # Update Position
202
+ velocity_1 = self.momentum * velocity_1 + current_drift_step * total_force_1
203
+ velocity_2 = self.momentum * velocity_2 + current_drift_step * total_force_2
204
+
205
+ geom_num_list_1 += velocity_1
206
+ geom_num_list_2 += velocity_2
207
+
208
+ # Check Convergence
209
+ drift_metric = max(self.RMS(force_perp_1), self.RMS(force_perp_2))
210
+
211
+ if d_step % 5 == 0:
212
+ info_dat = {
213
+ "energy_1": energy_1, "energy_2": energy_2,
214
+ "gradient_1": gradient_1, "gradient_2": gradient_2,
215
+ "perp_force_1": force_perp_1, "perp_force_2": force_perp_2,
216
+ "spring_force_1": spring_force_1, "spring_force_2": spring_force_2,
217
+ "spring_length": ds,
218
+ "step_size": current_drift_step,
219
+ "convergence_metric": drift_metric
220
+ }
221
+ self.print_info(info_dat, f"Cycle {cycle} - Drifting {d_step}")
222
+
223
+ if drift_metric < self.RMS_FORCE_THRESHOLD:
224
+ print(f" >> Drifting converged at step {d_step}")
225
+ break
226
+
227
+ # =========================================================================
228
+ # 2. Climbing Phase
229
+ # =========================================================================
230
+ print(f"--- Climbing Phase (Cycle {cycle}) ---")
231
+
232
+ iter_label = f"{cycle}_climb"
233
+
234
+ input_path_1 = self._make_next_input(FIO1, geom_num_list_1, element_list, electric_charge_and_multiplicity, iter_label + "_img1")
235
+ input_path_2 = self._make_next_input(FIO1, geom_num_list_2, element_list, electric_charge_and_multiplicity, iter_label + "_img2")
236
+
237
+ energy_1, gradient_1, g1_read, error_flag_1 = SP1.single_point(input_path_1, element_list, iter_label + "_img1", electric_charge_and_multiplicity, self.config.force_data["xtb"])
238
+ energy_2, gradient_2, g2_read, error_flag_2 = SP1.single_point(input_path_2, element_list, iter_label + "_img2", electric_charge_and_multiplicity, self.config.force_data["xtb"])
239
+
240
+ geom_num_list_1, geom_num_list_2 = Calculationtools().kabsch_algorithm(geom_num_list_1, geom_num_list_2)
241
+
242
+ ds, vs = self.get_spring_vectors(geom_num_list_1, geom_num_list_2)
243
+ grad_par_1, _ = self.decompose_gradient(gradient_1, vs)
244
+ grad_par_2, _ = self.decompose_gradient(gradient_2, vs)
245
+
246
+ active_climb_step = self.climb_step_size
247
+
248
+ # Move UP
249
+ geom_num_list_1 += active_climb_step * grad_par_1
250
+ geom_num_list_2 += active_climb_step * grad_par_2
251
+
252
+ # Update 'new_geom' for final output reference
253
+ new_geom_1 = geom_num_list_1.copy()
254
+ new_geom_2 = geom_num_list_2.copy()
255
+
256
+ grad_norm_1 = np.linalg.norm(gradient_1)
257
+ grad_norm_2 = np.linalg.norm(gradient_2)
258
+ global_metric = min(grad_norm_1, grad_norm_2)
259
+
260
+ info_dat = {
261
+ "energy_1": energy_1, "energy_2": energy_2,
262
+ "gradient_1": gradient_1, "gradient_2": gradient_2,
263
+ "par_force_1": grad_par_1, "par_force_2": grad_par_2,
264
+ "spring_length": ds, "convergence_metric": global_metric,
265
+ "step_size": active_climb_step
266
+ }
267
+ self.print_info(info_dat, f"Cycle {cycle} - Climbing")
268
+
269
+ ENERGY_LIST_1.append(energy_1 * self.config.hartree2kcalmol)
270
+ ENERGY_LIST_2.append(energy_2 * self.config.hartree2kcalmol)
271
+ GRAD_LIST_1.append(grad_norm_1)
272
+ GRAD_LIST_2.append(grad_norm_2)
273
+
274
+ if cycle > 5 and global_metric < self.MAX_FORCE_THRESHOLD:
275
+ print("!!! Global Convergence Reached !!!")
276
+ print(f"Saddle point candidate found around cycle {cycle}")
277
+ break
278
+
279
+ # --- END OF OPTIMIZATION LOOP ---
280
+
281
+ # Save the optimized structure
282
+ if new_geom_1 is not None and new_geom_2 is not None:
283
+ avg_geom = (new_geom_1 + new_geom_2) / 2.0
284
+ avg_geom_ang = avg_geom * self.config.bohr2angstroms
285
+
286
+ dir_name = os.path.basename(os.path.normpath(self.config.iEIP_FOLDER_DIRECTORY))
287
+ output_xyz_name = f"{dir_name}_optimized.xyz"
288
+
289
+ try:
290
+ with open(output_xyz_name, "w") as f:
291
+ num_atoms = len(element_list)
292
+ f.write(f"{num_atoms}\n")
293
+ f.write(f"SPM Optimized Saddle Point (Average)\n")
294
+ for i, elem in enumerate(element_list):
295
+ x, y, z = avg_geom_ang[i]
296
+ f.write(f"{elem} {x:.10f} {y:.10f} {z:.10f}\n")
297
+ print(f"\n[Success] Final optimized structure saved to: {output_xyz_name}")
298
+ except Exception as e:
299
+ print(f"\n[Error] Failed to save optimized structure: {e}")
300
+
301
+ return
302
+
303
+ def _make_next_input(self, FIO, geom, element_list, charge_mult, iter_label):
304
+ """
305
+ Creates a PSI4 input file and returns its path.
306
+ """
307
+ geom_tolist = (geom * self.config.bohr2angstroms).tolist()
308
+ for i, elem in enumerate(element_list):
309
+ geom_tolist[i].insert(0, elem)
310
+ geom_tolist.insert(0, charge_mult)
311
+
312
+ # FIO.make_psi4_input_file usually returns the directory or path.
313
+ # Ensure this matches your FIO implementation.
314
+ return FIO.make_psi4_input_file([geom_tolist], iter_label)
multioptpy/ieip.py CHANGED
@@ -15,6 +15,7 @@ from multioptpy.OtherMethod.addf import ADDFlikeMethod
15
15
  from multioptpy.OtherMethod.twopshs import twoPSHSlikeMethod
16
16
  from multioptpy.OtherMethod.elastic_image_pair import ElasticImagePair
17
17
  from multioptpy.OtherMethod.modelfunction import ModelFunctionOptimizer
18
+ from multioptpy.OtherMethod.spring_pair_method import SpringPairMethod
18
19
 
19
20
  class IEIPConfig:
20
21
  """
@@ -174,6 +175,9 @@ class IEIPConfig:
174
175
  self.dimer_trial_angle = getattr(args, 'dimer_trial_angle', np.pi / 32.0)
175
176
  self.dimer_max_iterations = getattr(args, 'dimer_max_iterations', 1000)
176
177
 
178
+ # Add config flag check if needed
179
+ self.use_spm = getattr(args, 'use_spm', False)
180
+
177
181
  # Create output directory
178
182
  os.mkdir(self.iEIP_FOLDER_DIRECTORY)
179
183
 
@@ -205,7 +209,9 @@ class iEIP:
205
209
  self.addf_like_method = ADDFlikeMethod(self.config)
206
210
  self.twoPshs = twoPSHSlikeMethod(self.config)
207
211
  self.dimer_method = DimerMethod(self.config)
208
-
212
+ self.spring_pair_method = SpringPairMethod(self.config)
213
+
214
+
209
215
 
210
216
  def optimize(self):
211
217
  """Load calculation modules based on configuration and run optimization"""
@@ -322,7 +328,13 @@ class iEIP:
322
328
  SP_list[0],
323
329
  self.config.electric_charge_and_multiplicity_list[0],
324
330
  FIO_img_list[0])
325
-
331
+ elif getattr(self.config, 'use_spm', False):
332
+ print("Using Spring Pair Method (SPM)")
333
+ self.spring_pair_method.iteration(
334
+ file_directory_list[0],
335
+ SP_list[0], element_list_list[0],
336
+ self.config.electric_charge_and_multiplicity_list[0],
337
+ FIO_img_list[0])
326
338
  else:
327
339
  self.elastic_image_pair.iteration(
328
340
  file_directory_list[0], file_directory_list[1],
multioptpy/interface.py CHANGED
@@ -5,7 +5,7 @@ import numpy as np
5
5
 
6
6
  """
7
7
  MultiOptPy
8
- Copyright (C) 2023-2025 ss0832
8
+ Copyright (C) 2023-2026 ss0832
9
9
 
10
10
  This program is free software: you can redistribute it and/or modify
11
11
  it under the terms of the GNU General Public License as published by
@@ -133,6 +133,8 @@ def call_ieipparser(parser):
133
133
  parser.add_argument('-dimer_sep','--dimer_separation', help="set dimer separation (default: 0.0001)", type=float, default=0.0001)
134
134
  parser.add_argument('-dimer_trial_angle','--dimer_trial_angle', help="set dimer trial angle (default: pi/32)", type=float, default=np.pi / 32.0)
135
135
  parser.add_argument('-dimer_maxiter','--dimer_max_iterations', help="set max iterations for dimer method (default: 1000)", type=int, default=1000)
136
+ parser.add_argument('-use_spm','--use_spm', help="Use Spring Pair method for searching direction of TS (default: False)", action='store_true'
137
+ )
136
138
  return parser
137
139
 
138
140
  def call_optimizeparser(parser):
@@ -1,21 +1,21 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MultiOptPy
3
- Version: 1.20.4
3
+ Version: 1.20.5
4
4
  Summary: Multifunctional geometry optimization tools for quantum chemical calculations.
5
5
  Author-email: ss0832 <highlighty876@gmail.com>
6
- License: GPLv3
6
+ License-Expression: GPL-3.0-or-later
7
7
  Requires-Python: >=3.12
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
- Requires-Dist: numpy~=2.2.0
10
+ Requires-Dist: numpy>=2.2.0
11
11
  Requires-Dist: scipy>=1.13.0
12
- Requires-Dist: matplotlib~=3.10.0
12
+ Requires-Dist: matplotlib>=3.10.0
13
13
  Requires-Dist: torch~=2.6.0
14
- Requires-Dist: pyscf~=2.9.0
15
- Requires-Dist: tblite~=0.4.0
14
+ Requires-Dist: pyscf>=2.9.0
15
+ Requires-Dist: tblite>=0.4.0
16
16
  Requires-Dist: ase~=3.26.0
17
17
  Requires-Dist: fairchem-core~=2.7.0
18
- Requires-Dist: sympy~=1.13.0
18
+ Requires-Dist: sympy>=1.13.0
19
19
  Dynamic: license-file
20
20
 
21
21
  # MultiOptPy
@@ -29,7 +29,7 @@ Dynamic: license-file
29
29
  [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://buymeacoffee.com/ss0832)
30
30
 
31
31
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/multioptpy?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/multioptpy)
32
- [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.17970774.svg)](https://doi.org/10.5281/zenodo.17970774)
32
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.17973395.svg)](https://doi.org/10.5281/zenodo.17973395)
33
33
 
34
34
  If this tool helped your studies, education, or saved your time, I'd appreciate a coffee!
35
35
  Your support serves as a great encouragement for this personal project and fuels my next journey.
@@ -80,9 +80,9 @@ conda create -n test_mop python=3.12.7
80
80
  conda activate test_mop
81
81
 
82
82
  ## 3. Download and install MultiOptPy:
83
- wget https://github.com/ss0832/MultiOptPy/archive/refs/tags/v1.20.3.zip
84
- unzip v1.20.3.zip
85
- cd MultiOptPy-1.20.3
83
+ wget https://github.com/ss0832/MultiOptPy/archive/refs/tags/v1.20.4.zip
84
+ unzip v1.20.4.zip
85
+ cd MultiOptPy-1.20.4
86
86
  pip install -r requirements.txt
87
87
 
88
88
  ## 4. Copy the test configuration file and run the AutoTS workflow:
@@ -107,10 +107,10 @@ python run_autots.py aldol_rxn.xyz -cfg config_autots_run_xtb_test.json
107
107
  # Installation via pip (Linux)
108
108
  conda create -n <env-name> python=3.12 pip
109
109
  conda activate <env-name>
110
- pip install git+https://github.com/ss0832/MultiOptPy.git@v1.20.3
111
- wget https://github.com/ss0832/MultiOptPy/archive/refs/tags/v1.20.3.zip
112
- unzip v1.20.3.zip
113
- cd MultiOptPy-1.20.3
110
+ pip install git+https://github.com/ss0832/MultiOptPy.git@v1.20.4
111
+ wget https://github.com/ss0832/MultiOptPy/archive/refs/tags/v1.20.4.zip
112
+ unzip v1.20.4.zip
113
+ cd MultiOptPy-1.20.4
114
114
 
115
115
  ## 💻 Command Line Interface (CLI) Functionality (v1.20.2)
116
116
  # The following eight core functionalities are available as direct executable commands in your terminal after installation:
@@ -412,13 +412,13 @@ If you use MultiOptPy in your research, please cite it as follows:
412
412
  month = dec,
413
413
  year = 2025,
414
414
  publisher = {Zenodo},
415
- version = {v1.20.3},
416
- doi = {10.5281/zenodo.17970774},
417
- url = {https://doi.org/10.5281/zenodo.17970774}
415
+ version = {v1.20.4},
416
+ doi = {10.5281/zenodo.17973395},
417
+ url = {https://doi.org/10.5281/zenodo.17973395}
418
418
  }
419
419
  ```
420
420
  ```
421
- ss0832. (2025). MultiOptPy: Multifunctional geometry optimization tools for quantum chemical calculations (v1.20.3). Zenodo. https://doi.org/10.5281/zenodo.17970774
421
+ ss0832. (2025). MultiOptPy: Multifunctional geometry optimization tools for quantum chemical calculations (v1.20.4). Zenodo. https://doi.org/10.5281/zenodo.17973395
422
422
  ```
423
423
 
424
424
  ## Setting Up an Environment for Using NNP(UMA) on Windows 11
@@ -1,15 +1,15 @@
1
1
  multioptpy/__init__.py,sha256=OJ7BAhHT2pWsIdC5wZfhCA5J8zW-odvaym9ySpPZ3Hw,125
2
2
  multioptpy/entrypoints.py,sha256=rizxMJSbFq2aAKpcsPIy1CTIh_ZCGkDpCEEkvMOy6uc,39130
3
3
  multioptpy/fileio.py,sha256=BJVhls9P_GU-ZB_nZgTgUPejru8-lJ1Qi-typHsG0WY,27175
4
- multioptpy/ieip.py,sha256=7zjvxW76k-5-tdgCpW0u4QzQBQtrLkcTYzNauhTX0qA,16182
5
- multioptpy/interface.py,sha256=t0wzrHhMxn4gV65NHoQ4_zbAVd6tCX0U87Z6sjvIXXc,81835
4
+ multioptpy/ieip.py,sha256=XjN3ii1xVclXdSVb6g1gO1Q_7VtOBFR9jK7MjdFZrFM,16786
5
+ multioptpy/interface.py,sha256=Xohjpk1ofcEmDygGg93dRh9Dzha6VjbtcefmcoXqoZE,82013
6
6
  multioptpy/irc.py,sha256=lTb1atfC5_QgW9a7FqQC-e-CXlTEhi1lms-zie_dEhI,20777
7
7
  multioptpy/moleculardynamics.py,sha256=FGA2fulBbNx26gCUSoauaOMoT0dIIddemXEiAoHC6p0,20359
8
8
  multioptpy/neb.py,sha256=eQQsB8HrDOZ4rlvHOUIwfR52QNDvuhvH1am5h6K7Ho0,59339
9
9
  multioptpy/optimization.py,sha256=FBMueS5ChGCZvCHsY7S8hCLf4nrWvjNMcEfWfj8vIdU,101480
10
10
  multioptpy/optimizer.py,sha256=Nm6uDEMiv9u5_CPdhQYqGzwmCqHNzDIdYeYIn9z5EwM,42047
11
11
  multioptpy/Calculator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- multioptpy/Calculator/ase_calculation_tools.py,sha256=gJsqUhv_JVv5_OrHbOD3IvSdfkJuRz4pQ14a_xmkknA,20143
12
+ multioptpy/Calculator/ase_calculation_tools.py,sha256=TxLpUsXM2Cg3U4jUfquZzq6IhGz7FbLj1GO8nWijzzw,20830
13
13
  multioptpy/Calculator/dxtb_calculation_tools.py,sha256=s3LTuypTsfZEIrnkuOQUOCegfh9LCyUG0tI3zzYiliQ,16492
14
14
  multioptpy/Calculator/emt_calculation_tools.py,sha256=aq4I3yM0_A7PFCX2sB5qHfEoij2LP0cqYu_BQkMztMQ,21762
15
15
  multioptpy/Calculator/gpaw_calculation_tools.py,sha256=P2a8YSCzHvgSdvPS_HQo-liLkVBQ1ePr4tooHJm2aL4,8079
@@ -25,10 +25,11 @@ multioptpy/Calculator/ase_tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm
25
25
  multioptpy/Calculator/ase_tools/fairchem.py,sha256=n7sZVEik1iw-gjTXyUx6uGQbJhn9FhhCJXatKQGK8oY,1627
26
26
  multioptpy/Calculator/ase_tools/gamess.py,sha256=OvRD5TGze--x-ZKqLPUAC78UwVlZwLDf-B4VoHrjp9I,904
27
27
  multioptpy/Calculator/ase_tools/gaussian.py,sha256=y4r64zX2o5zORUDt_h6SmnsBQ3wM8WQr7zP1XVmcw74,7829
28
+ multioptpy/Calculator/ase_tools/gxtb_dev.py,sha256=cBWenNu4PYtgQ-kelwmMqJ0FK4WxnCd-IGOA_BxX-y0,1529
28
29
  multioptpy/Calculator/ase_tools/mace.py,sha256=AsyUkTFrOI-kSnDT4d7ehViQfCRT5fG8wdYUTn56RCw,928
29
30
  multioptpy/Calculator/ase_tools/mopac.py,sha256=RUMNikbmVxIvoTi4H8JU3fXY2vBu6BzPxcv0NPd_L-k,723
30
31
  multioptpy/Calculator/ase_tools/nwchem.py,sha256=0NPIaX1_mzkzLh_r9e3WJA6fEiNrksEbXFwbATHkNuw,1162
31
- multioptpy/Calculator/ase_tools/orca.py,sha256=-9D5bqzQzn9IkvbYGC-OJtJKbneGKFKQVBcOhldvfw8,1026
32
+ multioptpy/Calculator/ase_tools/orca.py,sha256=JjTCvW3YT1KwaImkIxyd8FcUgz8BHO5ZFGHqiX3lJKc,10141
32
33
  multioptpy/Calculator/ase_tools/pygfn0.py,sha256=N-Rrtu8wvduPxRSGQMYK9LmQiWCtfCbkBOTXjhHK-gE,1468
33
34
  multioptpy/Constraint/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
35
  multioptpy/Constraint/constraint_condition.py,sha256=wS14fYCzP1V4_d1HLElW3a6fxKfEEX3EjxPvvbRPhZ8,44202
@@ -166,6 +167,7 @@ multioptpy/OtherMethod/dimer.py,sha256=54oqeExIqP_PPBpaZAwo59X11n6k1BgttpYIGKnJ7
166
167
  multioptpy/OtherMethod/elastic_image_pair.py,sha256=YSXjfrmHRRnl5YZ5JVZMLsDSCyfY911Q2F5qp6dj1rQ,33586
167
168
  multioptpy/OtherMethod/modelfunction.py,sha256=GX_YvA8rOzcKeaZcth6L-S0q9wdsTLA07E9sjOaFyRU,26168
168
169
  multioptpy/OtherMethod/newton_traj.py,sha256=gozEqyOCpD4zbYMf3IwPKIMC7v4D7ddXCu1MUz8tzxE,20154
170
+ multioptpy/OtherMethod/spring_pair_method.py,sha256=6YdLCIO-OOt_Mzzf8JFFtWVYAIa1Tsdi1VCxEbbPuto,15577
169
171
  multioptpy/OtherMethod/twopshs.py,sha256=GmBLKTNK-HqFpVmGgwmrtw9-IOf_hPYlUTslx95Sl20,49969
170
172
  multioptpy/PESAnalyzer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
171
173
  multioptpy/PESAnalyzer/calc_irc_curvature.py,sha256=YVZBydXcYLfO0Y8ZYGS7WXokkORfVQaO6zPbXwhQJt4,4986
@@ -241,9 +243,9 @@ multioptpy/Wrapper/ieip_wrapper.py,sha256=H0Fa7x5mzyFu5rnig7GdcZGX9LFclzRxDuREX5
241
243
  multioptpy/Wrapper/md_wrapper.py,sha256=EGQKY8BwBbjWkXxKQGlelzht872OsUp9GmyxPDgWav4,3050
242
244
  multioptpy/Wrapper/neb_wrapper.py,sha256=ZOyHhuSDKA5hABnQB7PYPfbQ9OyaAj9KKBeKu_HLiY4,3254
243
245
  multioptpy/Wrapper/optimize_wrapper.py,sha256=b5DoiSb5Y4YjzYaSfxNa4A5QMn7_UzAvActPOpL78Ok,2667
244
- multioptpy-1.20.4.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
245
- multioptpy-1.20.4.dist-info/METADATA,sha256=dG63R9Y4sXhoGbAjwINFbLarxvbM4eqBR81Y_pkyrPo,16690
246
- multioptpy-1.20.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
247
- multioptpy-1.20.4.dist-info/entry_points.txt,sha256=u2Y0tgYMu7UyZumhLCfoME92lCQXhurAAsixL_B3caw,404
248
- multioptpy-1.20.4.dist-info/top_level.txt,sha256=_MXQNcS1xJbRbGnYUM-F_G-PlLK7wTIkuqx-EvfAhRc,11
249
- multioptpy-1.20.4.dist-info/RECORD,,
246
+ multioptpy-1.20.5.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
247
+ multioptpy-1.20.5.dist-info/METADATA,sha256=FWxTaqF3RW7uxZd5o8HJGtc6kdUI6VRK8RX6cFnF--k,16712
248
+ multioptpy-1.20.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
249
+ multioptpy-1.20.5.dist-info/entry_points.txt,sha256=u2Y0tgYMu7UyZumhLCfoME92lCQXhurAAsixL_B3caw,404
250
+ multioptpy-1.20.5.dist-info/top_level.txt,sha256=_MXQNcS1xJbRbGnYUM-F_G-PlLK7wTIkuqx-EvfAhRc,11
251
+ multioptpy-1.20.5.dist-info/RECORD,,