Skip to content

Commit 00cb68e

Browse files
authored
Add files via upload
Support for o1numhess was implemented in NEB calculations to improve the Hessian approximation.
1 parent ce3ebc9 commit 00cb68e

11 files changed

Lines changed: 2114 additions & 1685 deletions

multioptpy/Calculator/ase_calculation_tools.py

Lines changed: 172 additions & 200 deletions
Large diffs are not rendered by default.

multioptpy/Calculator/dxtb_calculation_tools.py

Lines changed: 238 additions & 196 deletions
Large diffs are not rendered by default.

multioptpy/Calculator/lj_calculation_tools.py

Lines changed: 106 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from multioptpy.Parameters.parameter import UnitValueLib, number_element
99
from multioptpy.fileio import xyz2list
1010
from multioptpy.Visualization.visualization import NEBVisualizer
11+
from multioptpy.ModelHessian.o1numhess import O1NumHessCalculator
1112

1213
class LennardJonesCore:
1314
"""
@@ -42,7 +43,6 @@ def get_parameters(self, atom_symbols):
4243

4344
for i, symbol in enumerate(atom_symbols):
4445

45-
4646
if symbol not in self._param_cache:
4747
if symbol not in self.UFF_PARAMETERS:
4848
raise ValueError(f"Atom symbol '{symbol}' is not supported. "
@@ -122,23 +122,18 @@ def calculate_hessian(self, coords_bohr, atom_symbols):
122122
sub_hessians = term1 + term2
123123

124124
# **Vectorized Hessian Assembly**
125-
# Create meshgrid of indices for all 3x3 sub-blocks
126125
p, q = np.meshgrid(np.arange(3), np.arange(3), indexing='ij')
127126

128-
# Indices for off-diagonal blocks (a, b) and (b, a)
129127
row_indices_ab = (a[:, None, None] * 3 + p).flatten()
130128
col_indices_ab = (b[:, None, None] * 3 + q).flatten()
131129

132-
# Indices for diagonal blocks (a, a) and (b, b)
133130
row_indices_aa = (a[:, None, None] * 3 + p).flatten()
134131
col_indices_aa = (a[:, None, None] * 3 + q).flatten()
135132
row_indices_bb = (b[:, None, None] * 3 + p).flatten()
136133
col_indices_bb = (b[:, None, None] * 3 + q).flatten()
137134

138-
# Flatten the sub-hessian blocks to match the indices
139135
flat_sub_hessians = sub_hessians.flatten()
140136

141-
# Atomically add/subtract the blocks into the Hessian matrix
142137
np.subtract.at(hessian, (row_indices_ab, col_indices_ab), flat_sub_hessians)
143138
np.subtract.at(hessian, (col_indices_ab, row_indices_ab), flat_sub_hessians)
144139
np.add.at(hessian, (row_indices_aa, col_indices_aa), flat_sub_hessians)
@@ -149,6 +144,7 @@ def calculate_hessian(self, coords_bohr, atom_symbols):
149144
class Calculation:
150145
"""
151146
High-level wrapper for Lennard-Jones calculations.
147+
Supports both legacy directory-based execution and direct in-memory execution.
152148
"""
153149
def __init__(self, **kwarg):
154150
UVL = UnitValueLib()
@@ -162,30 +158,68 @@ def __init__(self, **kwarg):
162158
self.gradient = None
163159
self.coordinate = None
164160

165-
def exact_hessian(self, element_list, positions_bohr):
166-
"""Calculates and projects the Hessian."""
161+
def _ensure_atom_symbol(self, element_list):
162+
"""Internal helper to set atom symbols from element list"""
163+
if self.atom_symbol is None:
164+
if element_list is None or len(element_list) == 0:
165+
raise ValueError("Element list is empty.")
166+
167+
symbols = []
168+
for e in element_list:
169+
if isinstance(e, str):
170+
symbols.append(e)
171+
else:
172+
symbols.append(number_element(e))
173+
self.atom_symbol = symbols
174+
175+
def run_calculation(self, positions_ang, element_list, charge_mult=None):
176+
"""
177+
Execute Lennard-Jones calculation for a single geometry.
178+
179+
Args:
180+
positions_ang (np.ndarray): Coordinates in Angstrom.
181+
element_list (list): List of element symbols or numbers.
182+
charge_mult: Unused, kept for interface consistency.
183+
184+
Returns:
185+
tuple: (energy, gradient)
186+
"""
187+
self._ensure_atom_symbol(element_list)
188+
189+
positions_bohr = positions_ang / self.bohr2angstroms
190+
results = self.calculator.calculate_energy_and_gradient(positions_bohr, self.atom_symbol)
191+
192+
return results['energy'], results['gradient']
193+
194+
def calc_exact_hess(self, positions_ang, element_list):
195+
"""
196+
Calculates and projects the Hessian.
197+
"""
198+
self._ensure_atom_symbol(element_list)
199+
positions_bohr = positions_ang / self.bohr2angstroms
200+
167201
results = self.calculator.calculate_hessian(positions_bohr, self.atom_symbol)
168202
exact_hess = results['hessian']
169203

170204
self.Model_hess = Calculationtools().project_out_hess_tr_and_rot_for_coord(
171205
exact_hess, element_list, positions_bohr, display_eigval=False
172206
)
207+
return self.Model_hess
173208

174209
def single_point(self, file_directory, element_list, iter, electric_charge_and_multiplicity, method="", geom_num_list=None):
175210
"""
176-
Executes a Lennard-Jones single point calculation, reading from a file
177-
or using a provided geometry.
211+
Legacy method for directory-based execution.
178212
"""
179213
finish_frag = False
180214
e, g, positions_bohr = None, None, None
181215

182216
try:
183217
os.makedirs(file_directory, exist_ok=True)
184-
except (OSError, TypeError): # TypeError if file_directory is None
218+
except (OSError, TypeError):
185219
pass
186220

187221
if file_directory is None:
188-
file_list = ["dummy"] # To run the loop once for geom_num_list
222+
file_list = ["dummy"]
189223
else:
190224
file_list = sorted(glob.glob(os.path.join(file_directory, "*_[0-9].xyz")))
191225
if not file_list and geom_num_list is None:
@@ -196,35 +230,27 @@ def single_point(self, file_directory, element_list, iter, electric_charge_and_m
196230
positions_angstrom = None
197231
if geom_num_list is None:
198232
positions_angstrom, read_elements, _ = xyz2list(input_file, electric_charge_and_multiplicity)
199-
200-
element_list = read_elements
233+
if element_list is None:
234+
element_list = read_elements
201235
else:
202236
positions_angstrom = geom_num_list
203-
204-
if self.atom_symbol is None:
205-
if element_list is None or len(element_list) == 0:
206-
raise ValueError("Element list is empty. Cannot determine atom symbol.")
207-
first_element = element_list
208-
if type(element_list[0]) is not str:
209-
first_element = []
210-
for i in range(len(element_list)):
211-
first_element.append(number_element(element_list[i]))
212-
213-
self.atom_symbol = first_element
214-
print(f"Atom symbol set to '{self.atom_symbol}' based on the first structure.")
215-
216-
positions_bohr = np.array(positions_angstrom, dtype="float64") / self.bohr2angstroms
217-
218-
results = self.calculator.calculate_energy_and_gradient(positions_bohr, self.atom_symbol)
219-
e = results['energy']
220-
g = results['gradient']
237+
238+
# Ensure numpy array
239+
positions_angstrom = np.array(positions_angstrom, dtype="float64").reshape(-1, 3)
240+
241+
# Execute
242+
e, g = self.run_calculation(positions_angstrom, element_list)
243+
244+
positions_bohr = positions_angstrom / self.bohr2angstroms
221245

222246
if self.FC_COUNT == -1 or isinstance(iter, str):
223247
if self.hessian_flag:
224-
self.exact_hessian(element_list, positions_bohr)
248+
self.calc_exact_hess(positions_angstrom, element_list)
225249
elif iter % self.FC_COUNT == 0 or self.hessian_flag:
226-
self.exact_hessian(element_list, positions_bohr)
250+
self.calc_exact_hess(positions_angstrom, element_list)
227251

252+
# Single point calculation is geometry-independent in loop if geom_num_list is provided
253+
# If reading files, we process only one file here effectively due to return structure
228254
break
229255

230256
except Exception as error:
@@ -260,8 +286,6 @@ class LJEngine(CalculationEngine):
260286
def __init__(self, atom_symbol=None):
261287
super().__init__()
262288
self.atom_symbol = atom_symbol
263-
self.calculator = LennardJonesCore()
264-
self.bohr2angstroms = UnitValueLib().bohr2angstroms
265289

266290
def calculate(self, file_directory, optimize_num, pre_total_velocity, config):
267291
gradient_list, energy_list, geometry_num_list, num_list = [], [], [], []
@@ -273,33 +297,60 @@ def calculate(self, file_directory, optimize_num, pre_total_velocity, config):
273297
print(f"No XYZ files found in directory: {file_directory}")
274298
return np.array([]), np.array([]), np.array([]), pre_total_velocity
275299

300+
# Initialize Calculation Instance
301+
calc_instance = Calculation(
302+
atom_symbol=self.atom_symbol,
303+
FC_COUNT=config.FC_COUNT,
304+
Model_hess=config.model_hessian
305+
)
306+
307+
# Parse Elements from first file
308+
# Used to set atom_symbol once
309+
_, element_list_first, _ = xyz2list(file_list[0], None)
310+
311+
hess_count = 0
312+
276313
for num, input_file in enumerate(file_list):
277314
try:
278-
279315
print(f"Processing file: {input_file}")
316+
317+
# Parse Geometry
280318
positions_angstrom, element_list, _ = xyz2list(input_file, None)
281-
282-
if self.atom_symbol is None:
283-
if element_list is None or len(element_list) == 0:
284-
raise ValueError("Element list from file is empty.")
285-
first_element = element_list
286-
if type(element_list[0]) is not str:
287-
first_element = []
288-
for i in range(len(element_list)):
289-
first_element.append(number_element(element_list[i]))
290-
291-
self.atom_symbol = first_element
292-
print(f"Engine atom symbol set to '{self.atom_symbol}' based on the first file.")
293-
294319
positions_angstrom = np.array(positions_angstrom, dtype='float64').reshape(-1, 3)
295-
positions_bohr = positions_angstrom / self.bohr2angstroms
296320

297-
results = self.calculator.calculate_energy_and_gradient(positions_bohr, self.atom_symbol)
321+
# --- Execute Calculation ---
322+
e, g = calc_instance.run_calculation(positions_angstrom, element_list)
323+
# ---------------------------
324+
325+
energy_list.append(e)
326+
gradient_list.append(g)
298327

299-
energy_list.append(results['energy'])
300-
gradient_list.append(results['gradient'])
328+
# Store in Bohr
329+
positions_bohr = positions_angstrom / calc_instance.bohr2angstroms
301330
geometry_num_list.append(positions_bohr)
302331
num_list.append(num)
332+
333+
# Hessian Calculation
334+
if config.MFC_COUNT != -1 and optimize_num % config.MFC_COUNT == 0 and config.model_hessian.lower() == "o1numhess":
335+
print(f" Calculating O1NumHess for image {num} using {config.model_hessian}...")
336+
o1numhess = O1NumHessCalculator(calc_instance,
337+
element_list,
338+
[0, 1], # Placeholder for charge and multiplicity
339+
method="")
340+
seminumericalhessian = o1numhess.compute_hessian(positions_angstrom)
341+
np.save(os.path.join(config.NEB_FOLDER_DIRECTORY, f"tmp_hessian_{hess_count}.npy"), seminumericalhessian)
342+
hess_count += 1
343+
344+
elif config.FC_COUNT == -1 or isinstance(optimize_num, str):
345+
pass
346+
elif optimize_num % config.FC_COUNT == 0:
347+
print(f" Calculating Exact Hessian (LJ) for image {num}...")
348+
349+
exact_hess = calc_instance.calc_exact_hess(positions_angstrom, element_list)
350+
351+
np.save(os.path.join(config.NEB_FOLDER_DIRECTORY, f"tmp_hessian_{hess_count}.npy"), exact_hess)
352+
hess_count += 1
353+
303354
except Exception as error:
304355
print(f"Error processing {input_file}: {error}")
305356
if optimize_num != 0:
@@ -308,6 +359,7 @@ def calculate(self, file_directory, optimize_num, pre_total_velocity, config):
308359
self._process_visualization(energy_list, gradient_list, num_list, optimize_num, config)
309360
if optimize_num != 0 and len(pre_total_velocity) > 0 and delete_pre_total_velocity:
310361
pre_total_velocity = np.delete(np.array(pre_total_velocity), delete_pre_total_velocity, axis=0)
362+
311363
return (np.array(energy_list, dtype='float64'),
312364
np.array(gradient_list, dtype='float64'),
313365
np.array(geometry_num_list, dtype='float64'),

0 commit comments

Comments
 (0)