Skip to content

Commit 8f5b3b2

Browse files
authored
Add files via upload
- implementation of adaptive NEB method (-aneb, nebmain.py) (J. Chem. Phys. 117, 4651 (2002))
1 parent d1c174f commit 8f5b3b2

2 files changed

Lines changed: 121 additions & 38 deletions

File tree

multioptpy/interface.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ def nebparser(parser):
205205

206206
parser.add_argument("-ewbneb", "--EWBNEB", action='store_true', help='Energy-weighted Nudged elastic band method')
207207
parser.add_argument("-qsm", "--QSM", action='store_true', help='Quadratic String Method (J. Chem. Phys. 124, 054109 (2006))')
208+
parser.add_argument("-aneb", "--ANEB", default=None, nargs="*", help='Adaptic NEB method (ref.: J. Chem. Phys. 117, 4651 (2002)) (Usage: -aneb [interpolation_num (ex. 2)] [frequency (ex. 5)], Default setting is not applying adaptic NEB method.)')
208209

209210
parser.add_argument("-idpp", "--use_image_dependent_pair_potential", action='store_true', help='use image dependent pair potential (IDPP) method (ref. arXiv:1406.1512v1)')
210211
parser.add_argument("-ad", "--align_distances", type=int, default=0, help='distribute images at equal intervals on the reaction coordinate')

multioptpy/neb.py

Lines changed: 120 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
except:
2828
print("You can't use dxtb.")
2929

30-
30+
from scipy.signal import argrelmax
3131

3232
from interface import force_data_parser
3333
from parameter import element_number
@@ -91,6 +91,29 @@ def __init__(self, args):
9191
self.bneb2 = args.BNEB2
9292
self.ewbneb = args.EWBNEB
9393
self.qsm = args.QSM
94+
tmp_aneb = args.ANEB
95+
96+
if tmp_aneb is None:
97+
self.aneb_flag = False
98+
self.aneb_interpolation_num = 0
99+
self.aneb_frequency = 100000000000000000 # approximate infinite number
100+
101+
elif len(tmp_aneb) == 2:
102+
self.aneb_flag = True
103+
self.aneb_interpolation_num = int(tmp_aneb[0])
104+
self.aneb_frequency = int(tmp_aneb[1])
105+
if self.aneb_frequency < 1 or self.aneb_interpolation_num < 1:
106+
print("invalid input (-aneb)")
107+
print("Recommended setting is applied.")
108+
self.aneb_interpolation_num = 1
109+
self.aneb_frequency = 1
110+
else:
111+
self.aneb_flag = False
112+
self.aneb_interpolation_num = 0
113+
self.aneb_frequency = 100000000000000000 # approximate infinite number
114+
print("invalid input (-aneb)")
115+
exit()
116+
94117

95118
# Optimization settings
96119
self.FC_COUNT = args.calc_exact_hess
@@ -338,7 +361,6 @@ def execute(self):
338361
file_directory = self.make_input_files(geometry_list, 0)
339362

340363
# Initialize calculation variables
341-
pre_total_velocity = [[[]]]
342364
force_data = force_data_parser(self.args)
343365

344366
# Check for projection constraints
@@ -366,13 +388,13 @@ def execute(self):
366388
else:
367389
fix_atom_flag = False
368390

369-
# Initialize optimization variables - FIXED: Proper initialization order
391+
# Initialize optimization variables
370392
pre_geom = None
371393
pre_total_force = None
372394
pre_biased_gradient_list = None
373395
pre_total_velocity = []
374396
total_velocity = []
375-
pre_biased_energy_list = None # FIXED: Initialize this variable
397+
pre_biased_energy_list = None
376398

377399
# Check for conflicting optimization methods
378400
if self.config.cg_method and self.config.lbfgs_method:
@@ -381,7 +403,7 @@ def execute(self):
381403

382404
# Setup optimizer
383405
optimizer = self._setup_optimizer()
384-
406+
adaptive_neb_count = 0
385407
# Main NEB iteration loop
386408
for optimize_num in range(self.config.NEB_NUM):
387409
exit_file_detect = os.path.exists(self.config.NEB_FOLDER_DIRECTORY + "end.txt")
@@ -395,30 +417,30 @@ def execute(self):
395417

396418
# Calculate energy and gradients
397419
energy_list, gradient_list, geometry_num_list, pre_total_velocity = \
398-
self.calculation_engine.calculate(file_directory, optimize_num,
420+
self.calculation_engine.calculate(file_directory, adaptive_neb_count,
399421
pre_total_velocity, self.config)
400422

401-
if optimize_num == 0:
423+
if adaptive_neb_count == 0:
402424
init_geometry_num_list = geometry_num_list
403425

404426
# Apply bias potential - FIXED: Check if hessian files exist before using them
405427
biased_energy_list, biased_gradient_list = self._apply_bias_potential(
406428
energy_list, gradient_list, geometry_num_list, element_list, force_data, optimize_num)
407429

408-
# FIXED: Initialize pre_biased_energy_list on first iteration
409-
if optimize_num == 0:
430+
# Initialize pre_biased_energy_list on first iteration
431+
if adaptive_neb_count == 0:
410432
pre_biased_energy_list = copy.copy(biased_energy_list)
411433

412434
# Calculate model hessian if needed
413-
if (self.config.FC_COUNT == -1 and self.config.model_hessian and
414-
optimize_num % self.config.MFC_COUNT == 0):
435+
if (self.config.FC_COUNT == -1 and self.config.model_hessian and
436+
adaptive_neb_count % self.config.MFC_COUNT == 0):
415437
for i in range(len(geometry_num_list)):
416438
hessian = ApproxHessian().main(geometry_num_list[i], element_list,
417439
gradient_list[i], approx_hess_type=self.config.model_hessian)
418440
np.save(self.config.NEB_FOLDER_DIRECTORY + "tmp_hessian_" + str(i) + ".npy", hessian)
419441

420442
# Initialize projection constraints
421-
if projection_constraint_flag and optimize_num == 0:
443+
if projection_constraint_flag and adaptive_neb_count == 0:
422444
PC_list = []
423445
for i in range(len(energy_list)):
424446
PC_list.append(ProjectOutConstrain(
@@ -435,7 +457,7 @@ def execute(self):
435457

436458
# Calculate forces
437459
total_force = STRING_FORCE_CALC.calc_force(
438-
geometry_num_list, biased_energy_list, biased_gradient_list, optimize_num, element_list)
460+
geometry_num_list, biased_energy_list, biased_gradient_list, adaptive_neb_count, element_list)
439461

440462
# Calculate analysis metrics
441463
cos_list, tot_force_rms_list, tot_force_max_list, bias_force_rms_list, path_length_list = \
@@ -450,7 +472,7 @@ def execute(self):
450472
# Perform optimization step
451473
new_geometry = self._perform_optimization_step(
452474
optimizer, geometry_num_list, total_force, biased_gradient_list,
453-
pre_geom, pre_biased_gradient_list, optimize_num, biased_energy_list,
475+
pre_geom, pre_biased_gradient_list, adaptive_neb_count, biased_energy_list,
454476
pre_biased_energy_list, pre_total_velocity, total_velocity,
455477
cos_list, pre_geom, STRING_FORCE_CALC)
456478

@@ -465,41 +487,95 @@ def execute(self):
465487
projection_constraint_flag, PC_list if projection_constraint_flag else None)
466488

467489
# Align geometries if needed
468-
if self.config.align_distances >= 1 and optimize_num % self.config.align_distances == 0 and optimize_num > 0:
469-
print("Aligning geometries...")
470-
tmp_new_geometry = distribute_geometry(np.array(new_geometry))
471-
for k in range(len(new_geometry)):
472-
new_geometry[k] = copy.copy(tmp_new_geometry[k])
473-
474-
if self.config.align_distances_spline >= 1 and optimize_num % self.config.align_distances_spline == 0 and optimize_num > 0:
475-
print("Aligning geometries using spline...")
476-
tmp_new_geometry = distribute_geometry_spline(np.array(new_geometry))
477-
for k in range(len(new_geometry)):
478-
new_geometry[k] = copy.copy(tmp_new_geometry[k])
490+
new_geometry = self._align_geometries(new_geometry, optimize_num)
479491

480492
# Save analysis files
481493
tmp_instance_fileio = FileIO(file_directory + "/", "dummy.txt")
482494
tmp_instance_fileio.argrelextrema_txt_save(biased_energy_list, "approx_TS_node", "max")
483495
tmp_instance_fileio.argrelextrema_txt_save(biased_energy_list, "approx_EQ_node", "min")
484496
tmp_instance_fileio.argrelextrema_txt_save(bias_force_rms_list, "local_min_bias_grad_node", "min")
485497

486-
# Prepare for next iteration - FIXED: Proper variable assignment order
487-
pre_geom = copy.copy(geometry_num_list)
488-
geometry_list = self.print_geometry_list(new_geometry, element_list, electric_charge_and_multiplicity)
489-
file_directory = self.make_input_files(geometry_list, optimize_num + 1)
490-
pre_total_force = copy.copy(total_force)
491-
pre_biased_gradient_list = copy.copy(biased_gradient_list)
492-
pre_total_velocity = copy.copy(total_velocity)
493-
pre_biased_energy_list = copy.copy(biased_energy_list)
494-
495-
# Save energy data
496-
with open(self.config.NEB_FOLDER_DIRECTORY + "energy_plot.csv", "a") as f:
497-
f.write(",".join(list(map(str, biased_energy_list.tolist()))) + "\n")
498+
# Prepare for next iteration
499+
if adaptive_neb_count % self.config.aneb_frequency == 0 and adaptive_neb_count > 0 and self.config.aneb_flag:
500+
pre_geom = None
501+
pre_total_force = None
502+
pre_biased_gradient_list = None
503+
pre_total_velocity = []
504+
total_velocity = []
505+
pre_biased_energy_list = None
506+
new_geometry = self._exec_adaptive_neb(new_geometry, biased_energy_list)
507+
geometry_list = self.print_geometry_list(new_geometry, element_list, electric_charge_and_multiplicity)
508+
file_directory = self.make_input_files(geometry_list, optimize_num + 1)
509+
adaptive_neb_count = 0
510+
511+
else:
512+
pre_geom = copy.copy(geometry_num_list)
513+
geometry_list = self.print_geometry_list(new_geometry, element_list, electric_charge_and_multiplicity)
514+
file_directory = self.make_input_files(geometry_list, optimize_num + 1)
515+
pre_total_force = copy.copy(total_force)
516+
pre_biased_gradient_list = copy.copy(biased_gradient_list)
517+
pre_total_velocity = copy.copy(total_velocity)
518+
pre_biased_energy_list = copy.copy(biased_energy_list)
519+
adaptive_neb_count += 1
498520

521+
499522
self.make_traj_file(file_directory)
500523
print("Complete...")
501524
return
502-
525+
526+
def _exec_adaptive_neb(self, new_geometry, energy_list):
527+
"""Execute the adaptive NEB algorithm (private method)"""
528+
# ref.: P. Maragakis, S. A. Andreev, Y. Brumer, D. R. Reichman, E. Kaxiras, J. Chem. Phys. 117, 4651 (2002)
529+
ene_max_val_indices = argrelmax(energy_list)[0]
530+
print("Using Adaptive NEB method...")
531+
if len(ene_max_val_indices) == 0:
532+
print("Maxima not found.")
533+
return new_geometry
534+
if self.config.aneb_interpolation_num < 1:
535+
print("Interpolation number is 0.")
536+
return new_geometry
537+
538+
adaptive_neb_applied_new_geometry = []
539+
for i in range(len(new_geometry)):
540+
541+
if i in ene_max_val_indices:
542+
delta_geom_minus = new_geometry[i] - new_geometry[i-1]
543+
delta_geom_plus = new_geometry[i+1] - new_geometry[i]
544+
545+
for j in range(self.config.aneb_interpolation_num):
546+
alpha = (j + 1) / (self.config.aneb_interpolation_num + 1)
547+
new_point = new_geometry[i-1] + alpha * delta_geom_minus
548+
adaptive_neb_applied_new_geometry.append(new_point)
549+
550+
adaptive_neb_applied_new_geometry.append(new_geometry[i])
551+
552+
for j in range(self.config.aneb_interpolation_num):
553+
alpha = (j + 1) / (self.config.aneb_interpolation_num + 1)
554+
new_point = new_geometry[i] + alpha * delta_geom_plus
555+
adaptive_neb_applied_new_geometry.append(new_point)
556+
557+
else:
558+
adaptive_neb_applied_new_geometry.append(new_geometry[i])
559+
560+
adaptive_neb_applied_new_geometry = np.array(adaptive_neb_applied_new_geometry, dtype="float64")
561+
print("Interpolated nodes: ", ene_max_val_indices)
562+
print("The number of interpolated nodes: ", len(ene_max_val_indices)*2)
563+
return adaptive_neb_applied_new_geometry
564+
565+
def _align_geometries(self, new_geometry, optimize_num):
566+
"""Align geometries if needed (private method)"""
567+
if self.config.align_distances >= 1 and optimize_num % self.config.align_distances == 0 and optimize_num > 0:
568+
print("Aligning geometries...")
569+
tmp_new_geometry = distribute_geometry(np.array(new_geometry))
570+
for k in range(len(new_geometry)):
571+
new_geometry[k] = copy.copy(tmp_new_geometry[k])
572+
if self.config.align_distances_spline >= 1 and optimize_num % self.config.align_distances_spline == 0 and optimize_num > 0:
573+
print("Aligning geometries using spline...")
574+
tmp_new_geometry = distribute_geometry_spline(np.array(new_geometry))
575+
for k in range(len(new_geometry)):
576+
new_geometry[k] = copy.copy(tmp_new_geometry[k])
577+
return new_geometry
578+
503579
def _setup_force_calculation(self):
504580
"""Setup force calculation method"""
505581
if self.config.om:
@@ -640,6 +716,12 @@ def _save_analysis_data(self, cos_list, tot_force_rms_list, tot_force_max_list,
640716
# Save perpendicular gradient MAX data
641717
with open(self.config.NEB_FOLDER_DIRECTORY + "perp_max_gradient.csv", "a") as f:
642718
f.write(",".join(list(map(str, tot_force_max_list))) + "\n")
719+
720+
# Save energy data
721+
with open(self.config.NEB_FOLDER_DIRECTORY + "energy_plot.csv", "a") as f:
722+
f.write(",".join(list(map(str, biased_energy_list.tolist()))) + "\n")
723+
724+
643725

644726
def _perform_optimization_step(self, optimizer, geometry_num_list, total_force,
645727
biased_gradient_list, pre_geom, pre_biased_gradient_list,

0 commit comments

Comments
 (0)