88from multioptpy .Parameters .parameter import UnitValueLib , number_element
99from multioptpy .fileio import xyz2list
1010from multioptpy .Visualization .visualization import NEBVisualizer
11+ from multioptpy .ModelHessian .o1numhess import O1NumHessCalculator
1112
1213class 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):
149144class 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