MultiOptPy 1.20.2__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.
Files changed (246) hide show
  1. multioptpy/Calculator/__init__.py +0 -0
  2. multioptpy/Calculator/ase_calculation_tools.py +424 -0
  3. multioptpy/Calculator/ase_tools/__init__.py +0 -0
  4. multioptpy/Calculator/ase_tools/fairchem.py +28 -0
  5. multioptpy/Calculator/ase_tools/gamess.py +19 -0
  6. multioptpy/Calculator/ase_tools/gaussian.py +165 -0
  7. multioptpy/Calculator/ase_tools/mace.py +28 -0
  8. multioptpy/Calculator/ase_tools/mopac.py +19 -0
  9. multioptpy/Calculator/ase_tools/nwchem.py +31 -0
  10. multioptpy/Calculator/ase_tools/orca.py +22 -0
  11. multioptpy/Calculator/ase_tools/pygfn0.py +37 -0
  12. multioptpy/Calculator/dxtb_calculation_tools.py +344 -0
  13. multioptpy/Calculator/emt_calculation_tools.py +458 -0
  14. multioptpy/Calculator/gpaw_calculation_tools.py +183 -0
  15. multioptpy/Calculator/lj_calculation_tools.py +314 -0
  16. multioptpy/Calculator/psi4_calculation_tools.py +334 -0
  17. multioptpy/Calculator/pwscf_calculation_tools.py +189 -0
  18. multioptpy/Calculator/pyscf_calculation_tools.py +327 -0
  19. multioptpy/Calculator/sqm1_calculation_tools.py +611 -0
  20. multioptpy/Calculator/sqm2_calculation_tools.py +376 -0
  21. multioptpy/Calculator/tblite_calculation_tools.py +352 -0
  22. multioptpy/Calculator/tersoff_calculation_tools.py +818 -0
  23. multioptpy/Constraint/__init__.py +0 -0
  24. multioptpy/Constraint/constraint_condition.py +834 -0
  25. multioptpy/Coordinate/__init__.py +0 -0
  26. multioptpy/Coordinate/polar_coordinate.py +199 -0
  27. multioptpy/Coordinate/redundant_coordinate.py +638 -0
  28. multioptpy/IRC/__init__.py +0 -0
  29. multioptpy/IRC/converge_criteria.py +28 -0
  30. multioptpy/IRC/dvv.py +544 -0
  31. multioptpy/IRC/euler.py +439 -0
  32. multioptpy/IRC/hpc.py +564 -0
  33. multioptpy/IRC/lqa.py +540 -0
  34. multioptpy/IRC/modekill.py +662 -0
  35. multioptpy/IRC/rk4.py +579 -0
  36. multioptpy/Interpolation/__init__.py +0 -0
  37. multioptpy/Interpolation/adaptive_interpolation.py +283 -0
  38. multioptpy/Interpolation/binomial_interpolation.py +179 -0
  39. multioptpy/Interpolation/geodesic_interpolation.py +785 -0
  40. multioptpy/Interpolation/interpolation.py +156 -0
  41. multioptpy/Interpolation/linear_interpolation.py +473 -0
  42. multioptpy/Interpolation/savitzky_golay_interpolation.py +252 -0
  43. multioptpy/Interpolation/spline_interpolation.py +353 -0
  44. multioptpy/MD/__init__.py +0 -0
  45. multioptpy/MD/thermostat.py +185 -0
  46. multioptpy/MEP/__init__.py +0 -0
  47. multioptpy/MEP/pathopt_bneb_force.py +443 -0
  48. multioptpy/MEP/pathopt_dmf_force.py +448 -0
  49. multioptpy/MEP/pathopt_dneb_force.py +130 -0
  50. multioptpy/MEP/pathopt_ewbneb_force.py +207 -0
  51. multioptpy/MEP/pathopt_gpneb_force.py +512 -0
  52. multioptpy/MEP/pathopt_lup_force.py +113 -0
  53. multioptpy/MEP/pathopt_neb_force.py +225 -0
  54. multioptpy/MEP/pathopt_nesb_force.py +205 -0
  55. multioptpy/MEP/pathopt_om_force.py +153 -0
  56. multioptpy/MEP/pathopt_qsm_force.py +174 -0
  57. multioptpy/MEP/pathopt_qsmv2_force.py +304 -0
  58. multioptpy/ModelFunction/__init__.py +7 -0
  59. multioptpy/ModelFunction/avoiding_model_function.py +29 -0
  60. multioptpy/ModelFunction/binary_image_ts_search_model_function.py +47 -0
  61. multioptpy/ModelFunction/conical_model_function.py +26 -0
  62. multioptpy/ModelFunction/opt_meci.py +50 -0
  63. multioptpy/ModelFunction/opt_mesx.py +47 -0
  64. multioptpy/ModelFunction/opt_mesx_2.py +49 -0
  65. multioptpy/ModelFunction/seam_model_function.py +27 -0
  66. multioptpy/ModelHessian/__init__.py +0 -0
  67. multioptpy/ModelHessian/approx_hessian.py +147 -0
  68. multioptpy/ModelHessian/calc_params.py +227 -0
  69. multioptpy/ModelHessian/fischer.py +236 -0
  70. multioptpy/ModelHessian/fischerd3.py +360 -0
  71. multioptpy/ModelHessian/fischerd4.py +398 -0
  72. multioptpy/ModelHessian/gfn0xtb.py +633 -0
  73. multioptpy/ModelHessian/gfnff.py +709 -0
  74. multioptpy/ModelHessian/lindh.py +165 -0
  75. multioptpy/ModelHessian/lindh2007d2.py +707 -0
  76. multioptpy/ModelHessian/lindh2007d3.py +822 -0
  77. multioptpy/ModelHessian/lindh2007d4.py +1030 -0
  78. multioptpy/ModelHessian/morse.py +106 -0
  79. multioptpy/ModelHessian/schlegel.py +144 -0
  80. multioptpy/ModelHessian/schlegeld3.py +322 -0
  81. multioptpy/ModelHessian/schlegeld4.py +559 -0
  82. multioptpy/ModelHessian/shortrange.py +346 -0
  83. multioptpy/ModelHessian/swartd2.py +496 -0
  84. multioptpy/ModelHessian/swartd3.py +706 -0
  85. multioptpy/ModelHessian/swartd4.py +918 -0
  86. multioptpy/ModelHessian/tshess.py +40 -0
  87. multioptpy/Optimizer/QHAdam.py +61 -0
  88. multioptpy/Optimizer/__init__.py +0 -0
  89. multioptpy/Optimizer/abc_fire.py +83 -0
  90. multioptpy/Optimizer/adabelief.py +58 -0
  91. multioptpy/Optimizer/adabound.py +68 -0
  92. multioptpy/Optimizer/adadelta.py +65 -0
  93. multioptpy/Optimizer/adaderivative.py +56 -0
  94. multioptpy/Optimizer/adadiff.py +68 -0
  95. multioptpy/Optimizer/adafactor.py +70 -0
  96. multioptpy/Optimizer/adam.py +65 -0
  97. multioptpy/Optimizer/adamax.py +62 -0
  98. multioptpy/Optimizer/adamod.py +83 -0
  99. multioptpy/Optimizer/adamw.py +65 -0
  100. multioptpy/Optimizer/adiis.py +523 -0
  101. multioptpy/Optimizer/afire_neb.py +282 -0
  102. multioptpy/Optimizer/block_hessian_update.py +709 -0
  103. multioptpy/Optimizer/c2diis.py +491 -0
  104. multioptpy/Optimizer/component_wise_scaling.py +405 -0
  105. multioptpy/Optimizer/conjugate_gradient.py +82 -0
  106. multioptpy/Optimizer/conjugate_gradient_neb.py +345 -0
  107. multioptpy/Optimizer/coordinate_locking.py +405 -0
  108. multioptpy/Optimizer/dic_rsirfo.py +1015 -0
  109. multioptpy/Optimizer/ediis.py +417 -0
  110. multioptpy/Optimizer/eve.py +76 -0
  111. multioptpy/Optimizer/fastadabelief.py +61 -0
  112. multioptpy/Optimizer/fire.py +77 -0
  113. multioptpy/Optimizer/fire2.py +249 -0
  114. multioptpy/Optimizer/fire_neb.py +92 -0
  115. multioptpy/Optimizer/gan_step.py +486 -0
  116. multioptpy/Optimizer/gdiis.py +609 -0
  117. multioptpy/Optimizer/gediis.py +203 -0
  118. multioptpy/Optimizer/geodesic_step.py +433 -0
  119. multioptpy/Optimizer/gpmin.py +633 -0
  120. multioptpy/Optimizer/gpr_step.py +364 -0
  121. multioptpy/Optimizer/gradientdescent.py +78 -0
  122. multioptpy/Optimizer/gradientdescent_neb.py +52 -0
  123. multioptpy/Optimizer/hessian_update.py +433 -0
  124. multioptpy/Optimizer/hybrid_rfo.py +998 -0
  125. multioptpy/Optimizer/kdiis.py +625 -0
  126. multioptpy/Optimizer/lars.py +21 -0
  127. multioptpy/Optimizer/lbfgs.py +253 -0
  128. multioptpy/Optimizer/lbfgs_neb.py +355 -0
  129. multioptpy/Optimizer/linesearch.py +236 -0
  130. multioptpy/Optimizer/lookahead.py +40 -0
  131. multioptpy/Optimizer/nadam.py +64 -0
  132. multioptpy/Optimizer/newton.py +200 -0
  133. multioptpy/Optimizer/prodigy.py +70 -0
  134. multioptpy/Optimizer/purtubation.py +16 -0
  135. multioptpy/Optimizer/quickmin_neb.py +245 -0
  136. multioptpy/Optimizer/radam.py +75 -0
  137. multioptpy/Optimizer/rfo_neb.py +302 -0
  138. multioptpy/Optimizer/ric_rfo.py +842 -0
  139. multioptpy/Optimizer/rl_step.py +627 -0
  140. multioptpy/Optimizer/rmspropgrave.py +65 -0
  141. multioptpy/Optimizer/rsirfo.py +1647 -0
  142. multioptpy/Optimizer/rsprfo.py +1056 -0
  143. multioptpy/Optimizer/sadam.py +60 -0
  144. multioptpy/Optimizer/samsgrad.py +63 -0
  145. multioptpy/Optimizer/tr_lbfgs.py +678 -0
  146. multioptpy/Optimizer/trim.py +273 -0
  147. multioptpy/Optimizer/trust_radius.py +207 -0
  148. multioptpy/Optimizer/trust_radius_neb.py +121 -0
  149. multioptpy/Optimizer/yogi.py +60 -0
  150. multioptpy/OtherMethod/__init__.py +0 -0
  151. multioptpy/OtherMethod/addf.py +1150 -0
  152. multioptpy/OtherMethod/dimer.py +895 -0
  153. multioptpy/OtherMethod/elastic_image_pair.py +629 -0
  154. multioptpy/OtherMethod/modelfunction.py +456 -0
  155. multioptpy/OtherMethod/newton_traj.py +454 -0
  156. multioptpy/OtherMethod/twopshs.py +1095 -0
  157. multioptpy/PESAnalyzer/__init__.py +0 -0
  158. multioptpy/PESAnalyzer/calc_irc_curvature.py +125 -0
  159. multioptpy/PESAnalyzer/cmds_analysis.py +152 -0
  160. multioptpy/PESAnalyzer/koopman_analysis.py +268 -0
  161. multioptpy/PESAnalyzer/pca_analysis.py +314 -0
  162. multioptpy/Parameters/__init__.py +0 -0
  163. multioptpy/Parameters/atomic_mass.py +20 -0
  164. multioptpy/Parameters/atomic_number.py +22 -0
  165. multioptpy/Parameters/covalent_radii.py +44 -0
  166. multioptpy/Parameters/d2.py +61 -0
  167. multioptpy/Parameters/d3.py +63 -0
  168. multioptpy/Parameters/d4.py +103 -0
  169. multioptpy/Parameters/dreiding.py +34 -0
  170. multioptpy/Parameters/gfn0xtb_param.py +137 -0
  171. multioptpy/Parameters/gfnff_param.py +315 -0
  172. multioptpy/Parameters/gnb.py +104 -0
  173. multioptpy/Parameters/parameter.py +22 -0
  174. multioptpy/Parameters/uff.py +72 -0
  175. multioptpy/Parameters/unit_values.py +20 -0
  176. multioptpy/Potential/AFIR_potential.py +55 -0
  177. multioptpy/Potential/LJ_repulsive_potential.py +345 -0
  178. multioptpy/Potential/__init__.py +0 -0
  179. multioptpy/Potential/anharmonic_keep_potential.py +28 -0
  180. multioptpy/Potential/asym_elllipsoidal_potential.py +718 -0
  181. multioptpy/Potential/electrostatic_potential.py +69 -0
  182. multioptpy/Potential/flux_potential.py +30 -0
  183. multioptpy/Potential/gaussian_potential.py +101 -0
  184. multioptpy/Potential/idpp.py +516 -0
  185. multioptpy/Potential/keep_angle_potential.py +146 -0
  186. multioptpy/Potential/keep_dihedral_angle_potential.py +105 -0
  187. multioptpy/Potential/keep_outofplain_angle_potential.py +70 -0
  188. multioptpy/Potential/keep_potential.py +99 -0
  189. multioptpy/Potential/mechano_force_potential.py +74 -0
  190. multioptpy/Potential/nanoreactor_potential.py +52 -0
  191. multioptpy/Potential/potential.py +896 -0
  192. multioptpy/Potential/spacer_model_potential.py +221 -0
  193. multioptpy/Potential/switching_potential.py +258 -0
  194. multioptpy/Potential/universal_potential.py +34 -0
  195. multioptpy/Potential/value_range_potential.py +36 -0
  196. multioptpy/Potential/void_point_potential.py +25 -0
  197. multioptpy/SQM/__init__.py +0 -0
  198. multioptpy/SQM/sqm1/__init__.py +0 -0
  199. multioptpy/SQM/sqm1/sqm1_core.py +1792 -0
  200. multioptpy/SQM/sqm2/__init__.py +0 -0
  201. multioptpy/SQM/sqm2/calc_tools.py +95 -0
  202. multioptpy/SQM/sqm2/sqm2_basis.py +850 -0
  203. multioptpy/SQM/sqm2/sqm2_bond.py +119 -0
  204. multioptpy/SQM/sqm2/sqm2_core.py +303 -0
  205. multioptpy/SQM/sqm2/sqm2_data.py +1229 -0
  206. multioptpy/SQM/sqm2/sqm2_disp.py +65 -0
  207. multioptpy/SQM/sqm2/sqm2_eeq.py +243 -0
  208. multioptpy/SQM/sqm2/sqm2_overlapint.py +704 -0
  209. multioptpy/SQM/sqm2/sqm2_qm.py +578 -0
  210. multioptpy/SQM/sqm2/sqm2_rep.py +66 -0
  211. multioptpy/SQM/sqm2/sqm2_srb.py +70 -0
  212. multioptpy/Thermo/__init__.py +0 -0
  213. multioptpy/Thermo/normal_mode_analyzer.py +865 -0
  214. multioptpy/Utils/__init__.py +0 -0
  215. multioptpy/Utils/bond_connectivity.py +264 -0
  216. multioptpy/Utils/calc_tools.py +884 -0
  217. multioptpy/Utils/oniom.py +96 -0
  218. multioptpy/Utils/pbc.py +48 -0
  219. multioptpy/Utils/riemann_curvature.py +208 -0
  220. multioptpy/Utils/symmetry_analyzer.py +482 -0
  221. multioptpy/Visualization/__init__.py +0 -0
  222. multioptpy/Visualization/visualization.py +156 -0
  223. multioptpy/WFAnalyzer/MO_analysis.py +104 -0
  224. multioptpy/WFAnalyzer/__init__.py +0 -0
  225. multioptpy/Wrapper/__init__.py +0 -0
  226. multioptpy/Wrapper/autots.py +1239 -0
  227. multioptpy/Wrapper/ieip_wrapper.py +93 -0
  228. multioptpy/Wrapper/md_wrapper.py +92 -0
  229. multioptpy/Wrapper/neb_wrapper.py +94 -0
  230. multioptpy/Wrapper/optimize_wrapper.py +76 -0
  231. multioptpy/__init__.py +5 -0
  232. multioptpy/entrypoints.py +916 -0
  233. multioptpy/fileio.py +660 -0
  234. multioptpy/ieip.py +340 -0
  235. multioptpy/interface.py +1086 -0
  236. multioptpy/irc.py +529 -0
  237. multioptpy/moleculardynamics.py +432 -0
  238. multioptpy/neb.py +1267 -0
  239. multioptpy/optimization.py +1553 -0
  240. multioptpy/optimizer.py +709 -0
  241. multioptpy-1.20.2.dist-info/METADATA +438 -0
  242. multioptpy-1.20.2.dist-info/RECORD +246 -0
  243. multioptpy-1.20.2.dist-info/WHEEL +5 -0
  244. multioptpy-1.20.2.dist-info/entry_points.txt +9 -0
  245. multioptpy-1.20.2.dist-info/licenses/LICENSE +674 -0
  246. multioptpy-1.20.2.dist-info/top_level.txt +1 -0
multioptpy/neb.py ADDED
@@ -0,0 +1,1267 @@
1
+ import os
2
+ import numpy as np
3
+ import sys
4
+ import glob
5
+ import datetime
6
+ import copy
7
+ import re
8
+
9
+ try:
10
+ import psi4
11
+ except:
12
+ psi4 = None
13
+
14
+
15
+ try:
16
+ import pyscf
17
+ from pyscf import tdscf
18
+ from pyscf.hessian import thermo
19
+ except:
20
+ pass
21
+
22
+ try:
23
+ import dxtb
24
+ dxtb.timer.disable()
25
+ except:
26
+ pass
27
+
28
+ from scipy.signal import argrelmax
29
+
30
+ from multioptpy.interface import force_data_parser
31
+ from multioptpy.Parameters.parameter import element_number
32
+ from multioptpy.Potential.potential import BiasPotentialCalculation
33
+ from multioptpy.MEP.pathopt_bneb_force import CaluculationBNEB, CaluculationBNEB2, CaluculationBNEB3
34
+ from multioptpy.MEP.pathopt_dneb_force import CaluculationDNEB
35
+ from multioptpy.MEP.pathopt_dmf_force import CaluculationDMF
36
+ from multioptpy.MEP.pathopt_nesb_force import CaluculationNESB
37
+ from multioptpy.MEP.pathopt_lup_force import CaluculationLUP
38
+ from multioptpy.MEP.pathopt_om_force import CaluculationOM
39
+ from multioptpy.MEP.pathopt_ewbneb_force import CaluculationEWBNEB
40
+ from multioptpy.MEP.pathopt_qsm_force import CaluculationQSM
41
+ from multioptpy.MEP.pathopt_qsmv2_force import CaluculationQSMv2
42
+ from multioptpy.Utils.calc_tools import Calculationtools
43
+ from multioptpy.Potential.idpp import IDPP, CFB_ENM
44
+ from multioptpy.Constraint.constraint_condition import ProjectOutConstrain
45
+ from multioptpy.fileio import xyz2list, traj2list, FileIO
46
+ from multioptpy.Optimizer import lbfgs_neb
47
+ from multioptpy.Optimizer import conjugate_gradient_neb
48
+ from multioptpy.Optimizer import trust_radius_neb
49
+ from multioptpy.Optimizer.fire_neb import FIREOptimizer
50
+ from multioptpy.Optimizer.rfo_neb import RFOOptimizer, RFOQSMOptimizer
51
+ from multioptpy.Optimizer.gradientdescent_neb import SteepestDescentOptimizer
52
+ from multioptpy.ModelHessian.approx_hessian import ApproxHessian
53
+ from multioptpy.Visualization.visualization import NEBVisualizer
54
+ from multioptpy.Calculator.tblite_calculation_tools import TBLiteEngine
55
+ from multioptpy.Calculator.pyscf_calculation_tools import PySCFEngine
56
+ from multioptpy.Calculator.psi4_calculation_tools import Psi4Engine
57
+ from multioptpy.Calculator.dxtb_calculation_tools import DXTBEngine
58
+ from multioptpy.Calculator.ase_calculation_tools import ASEEngine
59
+ from multioptpy.Calculator.sqm1_calculation_tools import SQM1Engine
60
+ from multioptpy.Calculator.sqm2_calculation_tools import SQM2Engine
61
+ from multioptpy.Calculator.lj_calculation_tools import LJEngine
62
+ from multioptpy.Calculator.emt_calculation_tools import EMTEngine
63
+ from multioptpy.Calculator.tersoff_calculation_tools import TersoffEngine
64
+ from multioptpy.Utils.calc_tools import apply_climbing_image, calc_path_length_list
65
+ from multioptpy.Interpolation.geodesic_interpolation import distribute_geometry_geodesic
66
+ from multioptpy.Interpolation.binomial_interpolation import bernstein_interpolation, distribute_geometry_by_length_bernstein, distribute_geometry_by_energy_bernstein
67
+ from multioptpy.Interpolation.spline_interpolation import spline_interpolation, distribute_geometry_spline, distribute_geometry_by_length_spline
68
+ from multioptpy.Interpolation.linear_interpolation import distribute_geometry, distribute_geometry_by_length, distribute_geometry_by_energy, distribute_geometry_by_predicted_energy
69
+ from multioptpy.Interpolation.savitzky_golay_interpolation import savitzky_golay_interpolation, distribute_geometry_by_length_savgol
70
+ from multioptpy.Interpolation.adaptive_interpolation import adaptive_geometry_energy_interpolation
71
+
72
+
73
+
74
+ class NEBConfig:
75
+ """Configuration management class for NEB calculations"""
76
+
77
+ def __init__(self, args):
78
+ # Basic calculation settings
79
+ self.functional = args.functional
80
+ self.basisset = args.basisset
81
+ self.BASIS_SET = args.basisset
82
+ self.basic_set_and_function = args.functional + "/" + args.basisset
83
+ self.FUNCTIONAL = args.functional
84
+
85
+ # Solvent model settings
86
+ self.cpcm_solv_model = args.cpcm_solv_model
87
+ self.alpb_solv_model = args.alpb_solv_model
88
+
89
+ # Computational settings
90
+ self.N_THREAD = args.N_THREAD
91
+ self.SET_MEMORY = args.SET_MEMORY
92
+ self.pyscf = args.pyscf
93
+ self.usextb = args.usextb
94
+ self.usedxtb = args.usedxtb
95
+ self.sqm1 = args.sqm1
96
+ self.sqm2 = args.sqm2
97
+
98
+ # NEB specific settings
99
+ self.NEB_NUM = args.NSTEP
100
+ self.partition = args.partition
101
+ self.APPLY_CI_NEB = args.apply_CI_NEB
102
+ self.om = args.OM
103
+ self.lup = args.LUP
104
+ self.dneb = args.DNEB
105
+ self.nesb = args.NESB
106
+ self.bneb = args.BNEB
107
+ self.bneb2 = args.BNEB2
108
+ self.ewbneb = args.EWBNEB
109
+ self.dmf = args.DMF
110
+ self.qsm = args.QSM
111
+ self.qsmv2 = args.QSMv2
112
+ tmp_aneb = args.ANEB
113
+
114
+ if tmp_aneb is None:
115
+ self.aneb_flag = False
116
+ self.aneb_interpolation_num = 0
117
+ self.aneb_frequency = 100000000000000000 # approximate infinite number
118
+
119
+ elif len(tmp_aneb) == 2:
120
+ self.aneb_flag = True
121
+ self.aneb_interpolation_num = int(tmp_aneb[0])
122
+ self.aneb_frequency = int(tmp_aneb[1])
123
+ if self.aneb_frequency < 1 or self.aneb_interpolation_num < 1:
124
+ print("invalid input (-aneb)")
125
+ print("Recommended setting is applied.")
126
+ self.aneb_interpolation_num = 1
127
+ self.aneb_frequency = 1
128
+ else:
129
+ self.aneb_flag = False
130
+ self.aneb_interpolation_num = 0
131
+ self.aneb_frequency = 100000000000000000 # approximate infinite number
132
+ print("invalid input (-aneb)")
133
+ exit()
134
+
135
+
136
+ # Optimization settings
137
+ self.FC_COUNT = args.calc_exact_hess
138
+ self.MFC_COUNT = int(args.calc_model_hess)
139
+ self.model_hessian = args.use_model_hessian
140
+ self.climbing_image_start = args.climbing_image[0]
141
+ self.climbing_image_interval = args.climbing_image[1]
142
+ self.sd = args.steepest_descent
143
+ self.cg_method = args.conjugate_gradient
144
+ self.lbfgs_method = args.memory_limited_BFGS
145
+
146
+ # Flags
147
+ self.IDPP_flag = args.use_image_dependent_pair_potential
148
+ self.CFB_ENM_flag = args.use_correlated_flat_bottom_elastic_network_model
149
+ self.align_distances = args.align_distances
150
+ self.align_distances_energy = args.align_distances_energy
151
+ self.align_distances_energy_predicted = args.align_distances_energy_predicted
152
+ self.align_distances_spline = args.align_distances_spline
153
+ self.align_distances_spline_ver2 = args.align_distances_spline_ver2
154
+ self.align_distances_geodesic = args.align_distances_geodesic
155
+ self.align_distances_bernstein = args.align_distances_bernstein
156
+ self.align_distances_bernstein_energy = args.align_distances_bernstein_energy
157
+ self.align_distances_adaptive_energy = args.align_distances_adaptive_energy
158
+
159
+
160
+
161
+ tmp_align_savgol_list = args.align_distances_savgol.split(",")
162
+
163
+ if len(tmp_align_savgol_list) != 3:
164
+ print("invalid input (-adsg)")
165
+ exit()
166
+ else:
167
+ self.align_distances_savgol = int(tmp_align_savgol_list[0])
168
+ self.align_distances_savgol_window = int(tmp_align_savgol_list[1])
169
+ self.align_distances_savgol_poly = int(tmp_align_savgol_list[2])
170
+
171
+
172
+ self.node_distance_spline = args.node_distance_spline
173
+ self.node_distance_bernstein = args.node_distance_bernstein
174
+ if args.node_distance_savgol is None:
175
+ self.node_distance_savgol = None
176
+ self.node_distance_savgol_window = 0
177
+ self.node_distance_savgol_poly = 0
178
+ else:
179
+ tmp_node_savgol = args.node_distance_savgol.split(",")
180
+ if len(tmp_node_savgol) != 3:
181
+ print("invalid input (-node_distance_savgol)")
182
+ exit()
183
+ self.node_distance_savgol = float(tmp_node_savgol[0])
184
+ self.node_distance_savgol_window = int(tmp_node_savgol[1])
185
+ self.node_distance_savgol_poly = int(tmp_node_savgol[2])
186
+
187
+ self.excited_state = args.excited_state
188
+ self.unrestrict = args.unrestrict
189
+ self.save_pict = args.save_pict
190
+ self.apply_convergence_criteria = args.apply_convergence_criteria
191
+ self.node_distance = args.node_distance
192
+ self.not_ts_optimization = args.not_ts_optimization
193
+
194
+ # Electronic state settings
195
+ self.electronic_charge = args.electronic_charge
196
+ self.spin_multiplicity = args.spin_multiplicity
197
+
198
+ # Constants
199
+ self.bohr2angstroms = 0.52917721067
200
+ self.hartree2kcalmol = 627.509
201
+
202
+ # Additional settings
203
+ self.dft_grid = int(args.dft_grid)
204
+ self.spring_constant_k = 0.01
205
+ self.force_const_for_cineb = 0.01
206
+ self.othersoft = args.othersoft
207
+ self.software_path_file = args.software_path_file
208
+ self.ratio_of_rfo_step = args.ratio_of_rfo_step
209
+
210
+ # FIRE method parameters
211
+ self.FIRE_dt = 0.1
212
+ self.dt = 0.5
213
+ self.a = 0.10
214
+ self.n_reset = 0
215
+ self.FIRE_N_accelerate = 5
216
+ self.FIRE_f_inc = 1.10
217
+ self.FIRE_f_accelerate = 0.99
218
+ self.FIRE_f_decelerate = 0.5
219
+ self.FIRE_a_start = 0.1
220
+ self.FIRE_dt_max = 1.0
221
+
222
+ # Initialize derived settings
223
+ self.set_sub_basisset(args)
224
+ self.set_fixed_edges(args)
225
+
226
+ # Input file and directory settings
227
+ self.init_input = args.JOB
228
+ self.NEB_FOLDER_DIRECTORY = self.make_neb_work_directory(args.JOB)
229
+
230
+ def set_sub_basisset(self, args):
231
+ """Set up basis set configuration"""
232
+ if len(args.sub_basisset) % 2 != 0:
233
+ print("invalid input (-sub_bs)")
234
+ sys.exit(0)
235
+
236
+ if args.pyscf:
237
+ self.SUB_BASIS_SET = {}
238
+ if len(args.sub_basisset) > 0:
239
+ self.SUB_BASIS_SET["default"] = str(args.basisset)
240
+ for j in range(int(len(args.sub_basisset)/2)):
241
+ self.SUB_BASIS_SET[args.sub_basisset[2*j]] = args.sub_basisset[2*j+1]
242
+ print("Basis Sets defined by User are detected.")
243
+ print(self.SUB_BASIS_SET)
244
+ else:
245
+ self.SUB_BASIS_SET = {"default": args.basisset}
246
+ else:
247
+ self.SUB_BASIS_SET = args.basisset
248
+ if len(args.sub_basisset) > 0:
249
+ self.SUB_BASIS_SET += "\nassign " + str(args.basisset) + "\n"
250
+ for j in range(int(len(args.sub_basisset)/2)):
251
+ self.SUB_BASIS_SET += "assign " + args.sub_basisset[2*j] + " " + args.sub_basisset[2*j+1] + "\n"
252
+ print("Basis Sets defined by User are detected.")
253
+ print(self.SUB_BASIS_SET)
254
+
255
+ # ECP settings
256
+ if len(args.effective_core_potential) % 2 != 0:
257
+ print("invalid input (-ecp)")
258
+ sys.exit(0)
259
+
260
+ if args.pyscf:
261
+ self.ECP = {}
262
+ if len(args.effective_core_potential) > 0:
263
+ for j in range(int(len(args.effective_core_potential)/2)):
264
+ self.ECP[args.effective_core_potential[2*j]] = args.effective_core_potential[2*j+1]
265
+ else:
266
+ self.ECP = ""
267
+
268
+ def set_fixed_edges(self, args):
269
+ """Set up edge fixing configuration"""
270
+ if args.fixedges <= 0:
271
+ self.fix_init_edge = False
272
+ self.fix_end_edge = False
273
+ elif args.fixedges == 1:
274
+ self.fix_init_edge = True
275
+ self.fix_end_edge = False
276
+ elif args.fixedges == 2:
277
+ self.fix_init_edge = False
278
+ self.fix_end_edge = True
279
+ else:
280
+ self.fix_init_edge = True
281
+ self.fix_end_edge = True
282
+
283
+ def make_neb_work_directory(self, input_file):
284
+ """Create NEB working directory path"""
285
+ if os.path.splitext(input_file)[1] == ".xyz":
286
+ tmp_name = os.path.splitext(input_file)[0]
287
+ else:
288
+ tmp_name = input_file
289
+
290
+ timestamp = str(datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S_%f")[:-2])
291
+ if self.othersoft != "None":
292
+ return tmp_name + "_NEB_" + self.othersoft + "_" + timestamp + "/"
293
+ elif self.sqm2:
294
+ return tmp_name + "_NEB_SQM2_" + timestamp + "/"
295
+
296
+ elif self.sqm1:
297
+ return tmp_name + "_NEB_SQM1_" + timestamp + "/"
298
+
299
+ elif self.usextb == "None" and self.usedxtb == "None":
300
+ return tmp_name + "_NEB_" + self.basic_set_and_function.replace("/", "_") + "_" + timestamp + "/"
301
+ else:
302
+ if self.usextb != "None":
303
+ return tmp_name + "_NEB_" + self.usextb + "_" + timestamp + "/"
304
+ else:
305
+ return tmp_name + "_NEB_" + self.usedxtb + "_" + timestamp + "/"
306
+
307
+
308
+ class CalculationEngineFactory:
309
+ """Factory class for creating calculation engines"""
310
+
311
+ @staticmethod
312
+ def create_engine(config):
313
+ """Create appropriate calculation engine based on configuration"""
314
+ if config.othersoft != "None":
315
+ if config.othersoft.lower() == "lj":
316
+ print("Use Lennard-Jones cluster potential.")
317
+ return LJEngine()
318
+ elif config.othersoft.lower() == "emt":
319
+ print("Use EMT cluster potential.")
320
+ return EMTEngine()
321
+ elif config.othersoft.lower() == "tersoff":
322
+ print("Use Tersoff cluster potential.")
323
+ return TersoffEngine()
324
+ else:
325
+ return ASEEngine(software_path_file=config.software_path_file)
326
+ elif config.sqm2:
327
+ return SQM2Engine()
328
+ elif config.sqm1:
329
+ return SQM1Engine()
330
+ elif config.usextb != "None":
331
+ return TBLiteEngine()
332
+ elif config.usedxtb != "None":
333
+ return DXTBEngine()
334
+ elif config.pyscf:
335
+ return PySCFEngine()
336
+ else:
337
+ return Psi4Engine()
338
+
339
+
340
+ class OptimizationFactory:
341
+ """Factory class for creating optimization algorithms"""
342
+
343
+ @staticmethod
344
+ def create_optimizer(method, config):
345
+ """Create appropriate optimizer based on method and configuration"""
346
+ if method == "fire":
347
+ return FIREOptimizer(config)
348
+ elif method == "steepest_descent":
349
+ return SteepestDescentOptimizer(config)
350
+ elif method == "rfo" and (config.qsm or config.qsmv2):
351
+ return RFOQSMOptimizer(config)
352
+ elif method == "rfo":
353
+ tmp_opt = RFOOptimizer(config)
354
+ if config.not_ts_optimization:
355
+ print("Applying NEB without TS optimization.")
356
+ tmp_opt.set_apply_ts_opt(False)
357
+ return tmp_opt
358
+ elif method == "lbfgs":
359
+ tr_neb = trust_radius_neb.TR_NEB(
360
+ NEB_FOLDER_DIRECTORY=config.NEB_FOLDER_DIRECTORY,
361
+ fix_init_edge=config.fix_init_edge,
362
+ fix_end_edge=config.fix_end_edge,
363
+ apply_convergence_criteria=config.apply_convergence_criteria
364
+ )
365
+ return lbfgs_neb.LBFGS_NEB(TR_NEB=tr_neb)
366
+ elif method == "conjugate_gradient":
367
+ tr_neb = trust_radius_neb.TR_NEB(
368
+ NEB_FOLDER_DIRECTORY=config.NEB_FOLDER_DIRECTORY,
369
+ fix_init_edge=config.fix_init_edge,
370
+ fix_end_edge=config.fix_end_edge,
371
+ apply_convergence_criteria=config.apply_convergence_criteria
372
+ )
373
+ return conjugate_gradient_neb.ConjugateGradientNEB(TR_NEB=tr_neb, cg_method=config.cg_method)
374
+ else:
375
+ raise ValueError(f"Unsupported optimization method: {method}")
376
+
377
+
378
+ class NEB:
379
+ """Main NEB (Nudged Elastic Band) calculation class (Refactored version)"""
380
+
381
+ def __init__(self, args):
382
+ # Store original args for backward compatibility
383
+ self.args = args
384
+
385
+
386
+ def set_job(self, job):
387
+ self.args.JOB = job
388
+
389
+ def run(self):
390
+ if type(self.args.INPUT) is str:
391
+ START_FILE_LIST = [self.args.INPUT]
392
+ else:
393
+ START_FILE_LIST = self.args.INPUT #
394
+
395
+ job_file_list = []
396
+
397
+ for job_file in START_FILE_LIST:
398
+ print()
399
+ if "*" in job_file:
400
+ result_list = glob.glob(job_file)
401
+ job_file_list = job_file_list + result_list
402
+ else:
403
+ job_file_list = job_file_list + [job_file]
404
+
405
+ for job in job_file_list:
406
+ print("********************************")
407
+ print(job)
408
+ print("********************************")
409
+ if not os.path.exists(job):
410
+ print(f"{job} does not exist (neither as a file nor a directory).")
411
+ continue
412
+ self.set_job(job)
413
+
414
+ # Initialize configuration
415
+ self.config = NEBConfig(self.args)
416
+
417
+ # Create calculation engine
418
+ self.calculation_engine = CalculationEngineFactory.create_engine(self.config)
419
+
420
+ # Initialize visualizer if needed
421
+ if self.config.save_pict:
422
+ self.visualizer = NEBVisualizer(self.config)
423
+
424
+ # Create working directory
425
+ os.mkdir(self.config.NEB_FOLDER_DIRECTORY)
426
+
427
+ # Set element list (will be initialized in run method)
428
+ self.element_list = None
429
+ self.execute()
430
+
431
+ def execute(self):
432
+ """Execute NEB calculation"""
433
+ # Load and prepare geometries
434
+ geometry_list, element_list, electric_charge_and_multiplicity = self.make_geometry_list(
435
+ self.config.init_input, self.config.partition)
436
+ self.element_list = element_list
437
+ self.config.element_list = element_list # Add to config for optimizer access
438
+
439
+ # Create initial input files
440
+ file_directory = self.make_input_files(geometry_list, 0)
441
+
442
+ # Initialize calculation variables
443
+ force_data = force_data_parser(self.args)
444
+
445
+ # Check for projection constraints
446
+ if len(force_data["projection_constraint_condition_list"]) > 0:
447
+ projection_constraint_flag = True
448
+ else:
449
+ projection_constraint_flag = False
450
+
451
+ # Get element number list
452
+ element_number_list = []
453
+ for elem in element_list:
454
+ element_number_list.append(element_number(elem))
455
+ element_number_list = np.array(element_number_list, dtype="int")
456
+
457
+ # Save input configuration
458
+ with open(self.config.NEB_FOLDER_DIRECTORY + "input.txt", "w") as f:
459
+ f.write(str(vars(self.args)))
460
+
461
+ # Setup force calculation method
462
+ STRING_FORCE_CALC = self._setup_force_calculation()
463
+
464
+ # Check for fixed atoms
465
+ if len(force_data["fix_atoms"]) > 0:
466
+ fix_atom_flag = True
467
+ else:
468
+ fix_atom_flag = False
469
+
470
+ # Initialize optimization variables
471
+ pre_geom = None
472
+ pre_total_force = None
473
+ pre_biased_gradient_list = None
474
+ pre_total_velocity = []
475
+ total_velocity = []
476
+ pre_biased_energy_list = None
477
+
478
+ # Check for conflicting optimization methods
479
+ if self.config.cg_method and self.config.lbfgs_method:
480
+ print("You can not use CG and LBFGS at the same time.")
481
+ exit()
482
+
483
+ # Setup optimizer
484
+ optimizer = self._setup_optimizer()
485
+ adaptive_neb_count = 0
486
+ # Main NEB iteration loop
487
+ for optimize_num in range(self.config.NEB_NUM):
488
+ exit_file_detect = os.path.exists(self.config.NEB_FOLDER_DIRECTORY + "end.txt")
489
+ if exit_file_detect:
490
+ if psi4:
491
+ psi4.core.clean()
492
+ break
493
+
494
+ print(f"\n Path Relaxation Method: ITR. {optimize_num} \n")
495
+ self.make_traj_file(file_directory)
496
+
497
+ # Calculate energy and gradients
498
+ energy_list, gradient_list, geometry_num_list, pre_total_velocity = \
499
+ self.calculation_engine.calculate(file_directory, adaptive_neb_count,
500
+ pre_total_velocity, self.config)
501
+
502
+ if adaptive_neb_count == 0:
503
+ init_geometry_num_list = geometry_num_list
504
+
505
+ # Apply bias potential - FIXED: Check if hessian files exist before using them
506
+ biased_energy_list, biased_gradient_list = self._apply_bias_potential(
507
+ energy_list, gradient_list, geometry_num_list, element_list, force_data, optimize_num)
508
+
509
+ # Initialize pre_biased_energy_list on first iteration
510
+ if adaptive_neb_count == 0:
511
+ pre_biased_energy_list = copy.copy(biased_energy_list)
512
+
513
+ # Calculate model hessian if needed
514
+ if (self.config.FC_COUNT == -1 and self.config.model_hessian and
515
+ adaptive_neb_count % self.config.MFC_COUNT == 0):
516
+ for i in range(len(geometry_num_list)):
517
+ hessian = ApproxHessian().main(geometry_num_list[i], element_list,
518
+ gradient_list[i], approx_hess_type=self.config.model_hessian)
519
+ np.save(self.config.NEB_FOLDER_DIRECTORY + "tmp_hessian_" + str(i) + ".npy", hessian)
520
+
521
+ # Initialize projection constraints
522
+ if projection_constraint_flag and adaptive_neb_count == 0:
523
+ PC_list = []
524
+ for i in range(len(energy_list)):
525
+ PC_list.append(ProjectOutConstrain(
526
+ force_data["projection_constraint_condition_list"],
527
+ force_data["projection_constraint_atoms"],
528
+ force_data["projection_constraint_constant"]))
529
+ PC_list[i].initialize(geometry_num_list[i])
530
+
531
+ # Apply projection constraints
532
+ if projection_constraint_flag:
533
+ for i in range(len(energy_list)):
534
+ biased_gradient_list[i] = copy.copy(PC_list[i].calc_project_out_grad(
535
+ geometry_num_list[i], biased_gradient_list[i]))
536
+
537
+ # Calculate forces
538
+ total_force = STRING_FORCE_CALC.calc_force(
539
+ geometry_num_list, biased_energy_list, biased_gradient_list, adaptive_neb_count, element_list)
540
+
541
+ # Calculate analysis metrics
542
+ cos_list, tot_force_rms_list, tot_force_max_list, bias_force_rms_list, path_length_list = \
543
+ self._calculate_analysis_metrics(total_force, biased_gradient_list, geometry_num_list)
544
+
545
+ # Save analysis data and create plots
546
+ self._save_analysis_data(cos_list, tot_force_rms_list, tot_force_max_list,
547
+ bias_force_rms_list, file_directory, optimize_num, path_length_list, biased_energy_list)
548
+
549
+ total_velocity = self.force2velocity(total_force, element_list)
550
+
551
+ # Perform optimization step
552
+ new_geometry = self._perform_optimization_step(
553
+ optimizer, geometry_num_list, total_force, biased_gradient_list,
554
+ pre_geom, pre_biased_gradient_list, adaptive_neb_count, biased_energy_list,
555
+ pre_biased_energy_list, pre_total_velocity, total_velocity,
556
+ cos_list, pre_geom, STRING_FORCE_CALC)
557
+
558
+ # Apply climbing image if needed
559
+ if (optimize_num > self.config.climbing_image_start and
560
+ (optimize_num - self.config.climbing_image_start) % self.config.climbing_image_interval == 0):
561
+ new_geometry = apply_climbing_image(new_geometry, biased_energy_list, element_list)
562
+
563
+ # Apply constraints
564
+ new_geometry = self._apply_constraints(
565
+ new_geometry, fix_atom_flag, force_data, init_geometry_num_list,
566
+ projection_constraint_flag, PC_list if projection_constraint_flag else None)
567
+
568
+ # Align geometries if needed
569
+ new_geometry = self._align_geometries(new_geometry, optimize_num, biased_gradient_list, biased_energy_list)
570
+
571
+ # Save analysis files
572
+ tmp_instance_fileio = FileIO(file_directory + "/", "dummy.txt")
573
+ tmp_instance_fileio.argrelextrema_txt_save(biased_energy_list, "approx_TS_node", "max")
574
+ tmp_instance_fileio.argrelextrema_txt_save(biased_energy_list, "approx_EQ_node", "min")
575
+ tmp_instance_fileio.argrelextrema_txt_save(bias_force_rms_list, "local_min_bias_grad_node", "min")
576
+
577
+ # Prepare for next iteration
578
+ if adaptive_neb_count % self.config.aneb_frequency == 0 and adaptive_neb_count > 0 and self.config.aneb_flag:
579
+ pre_geom = None
580
+ pre_total_force = None
581
+ pre_biased_gradient_list = None
582
+ pre_total_velocity = []
583
+ total_velocity = []
584
+ pre_biased_energy_list = None
585
+ new_geometry = self._exec_adaptive_neb(new_geometry, biased_energy_list)
586
+ geometry_list = self.print_geometry_list(new_geometry, element_list, electric_charge_and_multiplicity)
587
+ file_directory = self.make_input_files(geometry_list, optimize_num + 1)
588
+ adaptive_neb_count = 0
589
+
590
+ else:
591
+ pre_geom = copy.copy(geometry_num_list)
592
+ geometry_list = self.print_geometry_list(new_geometry, element_list, electric_charge_and_multiplicity)
593
+ file_directory = self.make_input_files(geometry_list, optimize_num + 1)
594
+ pre_total_force = copy.copy(total_force)
595
+ pre_biased_gradient_list = copy.copy(biased_gradient_list)
596
+ pre_total_velocity = copy.copy(total_velocity)
597
+ pre_biased_energy_list = copy.copy(biased_energy_list)
598
+ adaptive_neb_count += 1
599
+
600
+
601
+ self.make_traj_file(file_directory)
602
+
603
+ self.get_result_file()
604
+ print("Complete...")
605
+ return
606
+
607
+ def _exec_adaptive_neb(self, new_geometry, energy_list):
608
+ """Execute the adaptive NEB algorithm (private method)"""
609
+ # ref.: P. Maragakis, S. A. Andreev, Y. Brumer, D. R. Reichman, E. Kaxiras, J. Chem. Phys. 117, 4651 (2002)
610
+ ene_max_val_indices = argrelmax(energy_list)[0]
611
+ print("Using Adaptive NEB method...")
612
+ if len(ene_max_val_indices) == 0:
613
+ print("Maxima not found.")
614
+ return new_geometry
615
+ if self.config.aneb_interpolation_num < 1:
616
+ print("Interpolation number is 0.")
617
+ return new_geometry
618
+
619
+ adaptive_neb_applied_new_geometry = []
620
+ for i in range(len(new_geometry)):
621
+
622
+ if i in ene_max_val_indices:
623
+ delta_geom_minus = new_geometry[i] - new_geometry[i-1]
624
+ delta_geom_plus = new_geometry[i+1] - new_geometry[i]
625
+
626
+ for j in range(self.config.aneb_interpolation_num):
627
+ alpha = (j + 1) / (self.config.aneb_interpolation_num + 1)
628
+ new_point = new_geometry[i-1] + alpha * delta_geom_minus
629
+ adaptive_neb_applied_new_geometry.append(new_point)
630
+
631
+ adaptive_neb_applied_new_geometry.append(new_geometry[i])
632
+
633
+ for j in range(self.config.aneb_interpolation_num):
634
+ alpha = (j + 1) / (self.config.aneb_interpolation_num + 1)
635
+ new_point = new_geometry[i] + alpha * delta_geom_plus
636
+ adaptive_neb_applied_new_geometry.append(new_point)
637
+
638
+ else:
639
+ adaptive_neb_applied_new_geometry.append(new_geometry[i])
640
+
641
+ adaptive_neb_applied_new_geometry = np.array(adaptive_neb_applied_new_geometry, dtype="float64")
642
+ print("Interpolated nodes: ", ene_max_val_indices)
643
+ print("The number of interpolated nodes: ", len(ene_max_val_indices) * 2 * self.config.aneb_interpolation_num)
644
+ return adaptive_neb_applied_new_geometry
645
+
646
+ def _align_geometries(self, new_geometry, optimize_num, gradient_list=None, energy_list=None):
647
+ """Align geometries if needed based on configuration (private method)."""
648
+
649
+ # Early exit if optimization hasn't started or is at step 0
650
+ if optimize_num <= 0:
651
+ return new_geometry
652
+
653
+ n_nodes = len(new_geometry)
654
+
655
+ # Helper function to update the geometry list in-place
656
+ def update_geometry_in_place(result_geometry):
657
+ for k in range(n_nodes):
658
+ new_geometry[k] = copy.copy(result_geometry[k])
659
+
660
+ # Define alignment strategies
661
+ # Format: (config_attribute_name, function_to_call, log_message, kwargs_generator_lambda)
662
+ alignment_strategies = [
663
+ (
664
+ 'align_distances',
665
+ distribute_geometry,
666
+ "Aligning geometries...",
667
+ lambda: {}
668
+ ),
669
+ (
670
+ 'align_distances_energy',
671
+ distribute_geometry_by_energy,
672
+ "Aligning geometries (energy-weighted)...",
673
+ # Fetch smoothing parameter from config, default to 0.1 if not present
674
+ lambda: {
675
+ 'energy_list': energy_list,
676
+ 'gradient_list': gradient_list,
677
+ 'smoothing': getattr(self.config, 'align_distances_energy_smoothing', 0.02)
678
+ }
679
+ ),
680
+ (
681
+ 'align_distances_bernstein',
682
+ bernstein_interpolation,
683
+ "Aligning geometries using Bernstein interpolation...",
684
+ lambda: {'n_points': n_nodes}
685
+ ),
686
+ (
687
+ 'align_distances_bernstein_energy',
688
+ distribute_geometry_by_energy_bernstein,
689
+ "Aligning geometries using energy-weighted Bernstein interpolation...",
690
+ # Fetch smoothing parameter from config, default to 0.1 if not present
691
+ lambda: {
692
+ 'energy_list': energy_list,
693
+ 'gradient_list': gradient_list,
694
+ 'smoothing': getattr(self.config, 'align_distances_bernstein_energy_smoothing', 0.5)
695
+ }
696
+ ),
697
+ (
698
+ 'align_distances_spline',
699
+ distribute_geometry_spline,
700
+ "Aligning geometries using spline...",
701
+ lambda: {}
702
+ ),
703
+ (
704
+ 'align_distances_spline_ver2',
705
+ spline_interpolation,
706
+ "Aligning geometries using spline ver2...",
707
+ lambda: {'n_points': n_nodes}
708
+ ),
709
+ (
710
+ 'align_distances_savgol',
711
+ savitzky_golay_interpolation,
712
+ "Aligning geometries using Savitzky-Golay filter...",
713
+ lambda: {
714
+ 'number_of_nodes': n_nodes,
715
+ 'window_length': getattr(self.config, 'align_distances_savgol_window', None),
716
+ 'polyorder': getattr(self.config, 'align_distances_savgol_poly', None)
717
+ }
718
+ ),
719
+ (
720
+ 'align_distances_geodesic',
721
+ distribute_geometry_geodesic,
722
+ "Aligning geometries using geodesic interpolation...",
723
+ lambda: {}
724
+ ),
725
+
726
+
727
+ (
728
+ 'align_distances_adaptive_energy',
729
+ adaptive_geometry_energy_interpolation,
730
+ "Aligning geometries (Adaptive Geometry + Energy)...",
731
+ lambda: {'energy_list': energy_list, 'gradient_list': gradient_list, 'angle_threshold_deg': 15.0}
732
+ ),
733
+ (
734
+ 'align_distances_energy_predicted',
735
+ distribute_geometry_by_predicted_energy,
736
+ "Aligning geometries using energy-weighted predicted interpolation...",
737
+ lambda: {'energy_list': energy_list, 'gradient_list': gradient_list}
738
+ ),
739
+
740
+ ]
741
+
742
+ # Iterate through strategies and execute if the interval matches
743
+ for config_attr, func, message, kwargs_gen in alignment_strategies:
744
+ # Get the interval from config, default to 0 if attribute is missing
745
+ interval = getattr(self.config, config_attr, 0)
746
+
747
+ if interval >= 1 and optimize_num % interval == 0:
748
+ print(message)
749
+
750
+ # Execute the function with the generated keyword arguments
751
+ # The first argument is always assumed to be the geometry array
752
+ tmp_new_geometry = func(np.array(new_geometry), **kwargs_gen())
753
+
754
+ # Update the result
755
+ update_geometry_in_place(tmp_new_geometry)
756
+
757
+ return new_geometry
758
+
759
+
760
+ def _setup_force_calculation(self):
761
+ """Setup force calculation method"""
762
+ if self.config.om:
763
+ return CaluculationOM(self.config.APPLY_CI_NEB)
764
+ elif self.config.lup:
765
+ return CaluculationLUP(self.config.APPLY_CI_NEB)
766
+ elif self.config.dneb:
767
+ return CaluculationDNEB(self.config.APPLY_CI_NEB)
768
+ elif self.config.nesb:
769
+ return CaluculationNESB(self.config.APPLY_CI_NEB)
770
+ elif self.config.bneb:
771
+ return CaluculationBNEB2(self.config.APPLY_CI_NEB)
772
+ elif self.config.bneb2:
773
+ return CaluculationBNEB3(self.config.APPLY_CI_NEB)
774
+ elif self.config.ewbneb:
775
+ return CaluculationEWBNEB(self.config.APPLY_CI_NEB)
776
+ elif self.config.qsm:
777
+ return CaluculationQSM(self.config.APPLY_CI_NEB)
778
+ elif self.config.qsmv2:
779
+ return CaluculationQSMv2(self.config.APPLY_CI_NEB)
780
+ elif self.config.dmf:
781
+ return CaluculationDMF(self.config.APPLY_CI_NEB)
782
+ else:
783
+ return CaluculationBNEB(self.config.APPLY_CI_NEB)
784
+
785
+ def _setup_optimizer(self):
786
+ """Setup optimization algorithm"""
787
+ # Determine optimization method based on configuration
788
+ if self.config.FC_COUNT != -1 or (self.config.MFC_COUNT != -1 and self.config.model_hessian):
789
+ return OptimizationFactory.create_optimizer("rfo", self.config)
790
+ elif self.config.lbfgs_method:
791
+ return OptimizationFactory.create_optimizer("lbfgs", self.config)
792
+ elif self.config.cg_method:
793
+ return OptimizationFactory.create_optimizer("conjugate_gradient", self.config)
794
+ else:
795
+ return OptimizationFactory.create_optimizer("fire", self.config)
796
+
797
+ def _apply_bias_potential(self, energy_list, gradient_list, geometry_num_list, element_list, force_data, optimize_num):
798
+ """Apply bias potential to energies and gradients - FIXED: Check hessian file existence"""
799
+ biased_energy_list = []
800
+ biased_gradient_list = []
801
+
802
+ for i in range(len(energy_list)):
803
+ _, B_e, B_g, B_hess = BiasPotentialCalculation(
804
+ self.config.NEB_FOLDER_DIRECTORY).main(
805
+ energy_list[i], gradient_list[i], geometry_num_list[i],
806
+ element_list, force_data)
807
+
808
+ # FIXED: Only load hessian files if they exist and are needed
809
+ if self.config.FC_COUNT > 0 or (self.config.MFC_COUNT > 0 and self.config.model_hessian):
810
+ hessian_file = self.config.NEB_FOLDER_DIRECTORY + "tmp_hessian_" + str(i) + ".npy"
811
+ if os.path.exists(hessian_file):
812
+ hess = np.load(hessian_file)
813
+ np.save(hessian_file, B_hess + hess)
814
+ else:
815
+ # If hessian file doesn't exist yet, just save the bias hessian
816
+ # This can happen on the first iteration when exact hessians haven't been calculated yet
817
+ if not np.allclose(B_hess, 0): # Only save if bias hessian is non-zero
818
+ np.save(hessian_file, B_hess)
819
+
820
+ biased_energy_list.append(B_e)
821
+ biased_gradient_list.append(B_g)
822
+
823
+ return np.array(biased_energy_list, dtype="float64"), np.array(biased_gradient_list, dtype="float64")
824
+
825
+ def _calculate_analysis_metrics(self, total_force, biased_gradient_list, geometry_num_list):
826
+ """Calculate analysis metrics for monitoring convergence"""
827
+ cos_list = []
828
+ tot_force_rms_list = []
829
+ tot_force_max_list = []
830
+ bias_force_rms_list = []
831
+
832
+ for i in range(len(total_force)):
833
+ # Calculate cosine between total force and biased gradient
834
+ total_force_norm = np.linalg.norm(total_force[i])
835
+ biased_grad_norm = np.linalg.norm(biased_gradient_list[i])
836
+
837
+ if total_force_norm > 1e-10 and biased_grad_norm > 1e-10:
838
+ cos = np.sum(total_force[i] * biased_gradient_list[i]) / (total_force_norm * biased_grad_norm)
839
+ else:
840
+ cos = 0.0
841
+ cos_list.append(cos)
842
+
843
+ tot_force_rms = np.sqrt(np.mean(total_force[i]**2))
844
+ tot_force_rms_list.append(tot_force_rms)
845
+
846
+ tot_force_max = np.max(np.abs(total_force[i]))
847
+ tot_force_max_list.append(tot_force_max)
848
+
849
+ bias_force_rms = np.sqrt(np.mean(biased_gradient_list[i]**2))
850
+ bias_force_rms_list.append(bias_force_rms)
851
+
852
+ path_length_list = calc_path_length_list(geometry_num_list)
853
+
854
+ return cos_list, tot_force_rms_list, tot_force_max_list, bias_force_rms_list, path_length_list
855
+
856
+ def _save_analysis_data(self, cos_list, tot_force_rms_list, tot_force_max_list,
857
+ bias_force_rms_list, file_directory, optimize_num, path_length_list, biased_energy_list):
858
+ """Save analysis data and create plots"""
859
+ # Save path length data
860
+ with open(self.config.NEB_FOLDER_DIRECTORY + "path_length.csv", "a") as f:
861
+ f.write(",".join(list(map(str, path_length_list))) + "\n")
862
+
863
+ # Create energy vs path length plot
864
+ if self.config.save_pict:
865
+ self.visualizer.simple_scatter_plot(path_length_list, biased_energy_list, "", optimize_num, "Path length (ang.)", "Energy (Hartree)", "BE_PL")
866
+
867
+
868
+ # Save bias force RMS data
869
+ with open(self.config.NEB_FOLDER_DIRECTORY + "bias_force_rms.csv", "a") as f:
870
+ f.write(",".join(list(map(str, bias_force_rms_list))) + "\n")
871
+
872
+ # Create bias force RMS vs path length plot
873
+ if self.config.save_pict:
874
+ self.visualizer.simple_scatter_plot(path_length_list, bias_force_rms_list, "", optimize_num, "Path length (ang.)", "Bias force RMS (Hartree)", "BFRMS_PL")
875
+
876
+ # Create orthogonality plot
877
+ if self.config.save_pict:
878
+ self.visualizer.plot_orthogonality([x for x in range(len(cos_list))], cos_list, optimize_num)
879
+
880
+
881
+ # Save orthogonality data
882
+ with open(self.config.NEB_FOLDER_DIRECTORY + "orthogonality.csv", "a") as f:
883
+ f.write(",".join(list(map(str, cos_list))) + "\n")
884
+
885
+ # Create perpendicular gradient RMS plot
886
+ if self.config.save_pict:
887
+ self.visualizer.plot_perpendicular_gradient(
888
+ [x for x in range(len(tot_force_rms_list))][1:-1],
889
+ tot_force_rms_list[1:-1], optimize_num, "rms")
890
+
891
+ # Save perpendicular gradient RMS data
892
+ with open(self.config.NEB_FOLDER_DIRECTORY + "perp_rms_gradient.csv", "a") as f:
893
+ f.write(",".join(list(map(str, tot_force_rms_list))) + "\n")
894
+
895
+ # Create perpendicular gradient MAX plot
896
+ if self.config.save_pict:
897
+ self.visualizer.plot_perpendicular_gradient(
898
+ [x for x in range(len(tot_force_max_list))],
899
+ tot_force_max_list, optimize_num, "max")
900
+
901
+ # Save perpendicular gradient MAX data
902
+ with open(self.config.NEB_FOLDER_DIRECTORY + "perp_max_gradient.csv", "a") as f:
903
+ f.write(",".join(list(map(str, tot_force_max_list))) + "\n")
904
+
905
+ # Save energy data
906
+ with open(self.config.NEB_FOLDER_DIRECTORY + "energy_plot.csv", "a") as f:
907
+ f.write(",".join(list(map(str, biased_energy_list.tolist()))) + "\n")
908
+
909
+
910
+
911
+ def _perform_optimization_step(self, optimizer, geometry_num_list, total_force,
912
+ biased_gradient_list, pre_geom, pre_biased_gradient_list,
913
+ optimize_num, biased_energy_list, pre_biased_energy_list,
914
+ pre_total_velocity, total_velocity, cos_list,
915
+ pre_geom_param, STRING_FORCE_CALC):
916
+ """Perform optimization step based on the selected method"""
917
+ if isinstance(optimizer, RFOOptimizer):
918
+ # RFO optimization
919
+ return optimizer.optimize(
920
+ geometry_num_list, biased_gradient_list, pre_geom, pre_biased_gradient_list,
921
+ optimize_num, biased_energy_list, pre_biased_energy_list,
922
+ pre_total_velocity, total_velocity, cos_list, pre_geom_param, STRING_FORCE_CALC)
923
+ elif isinstance(optimizer, RFOQSMOptimizer):
924
+ # RFOQSM optimization
925
+ return optimizer.optimize(
926
+ geometry_num_list, biased_gradient_list, pre_geom, pre_biased_gradient_list,
927
+ optimize_num, biased_energy_list, pre_biased_energy_list,
928
+ pre_total_velocity, total_velocity, cos_list, pre_geom_param, STRING_FORCE_CALC)
929
+ elif isinstance(optimizer, FIREOptimizer):
930
+ # FIRE optimization
931
+ if optimize_num < self.config.sd:
932
+ return optimizer.optimize(
933
+ geometry_num_list, total_force, pre_total_velocity, optimize_num,
934
+ total_velocity, cos_list, biased_energy_list, pre_biased_energy_list, pre_geom_param)
935
+ else:
936
+ # Switch to steepest descent
937
+ sd_optimizer = SteepestDescentOptimizer(self.config)
938
+ return sd_optimizer.optimize(geometry_num_list, total_force)
939
+ elif isinstance(optimizer, SteepestDescentOptimizer):
940
+ return optimizer.optimize(geometry_num_list, total_force)
941
+ elif hasattr(optimizer, 'LBFGS_NEB_calc'):
942
+ # LBFGS optimization
943
+ return optimizer.LBFGS_NEB_calc(
944
+ geometry_num_list, total_force, pre_total_velocity, optimize_num,
945
+ total_velocity, cos_list, biased_energy_list, pre_biased_energy_list, pre_geom_param)
946
+ elif hasattr(optimizer, 'CG_NEB_calc'):
947
+ # Conjugate gradient optimization
948
+ return optimizer.CG_NEB_calc(
949
+ geometry_num_list, total_force, pre_total_velocity, optimize_num,
950
+ total_velocity, cos_list, biased_energy_list, pre_biased_energy_list, pre_geom_param)
951
+ else:
952
+ # Default to FIRE
953
+ fire_optimizer = FIREOptimizer(self.config)
954
+ return fire_optimizer.optimize(
955
+ geometry_num_list, total_force, pre_total_velocity, optimize_num,
956
+ total_velocity, cos_list, biased_energy_list, pre_biased_energy_list, pre_geom_param)
957
+
958
+ def _apply_constraints(self, new_geometry, fix_atom_flag, force_data,
959
+ init_geometry_num_list, projection_constraint_flag, PC_list):
960
+ """Apply various constraints to the new geometry"""
961
+
962
+ # Apply fixing edge node
963
+ if self.config.fix_init_edge:
964
+ new_geometry[0] = init_geometry_num_list[0] * self.config.bohr2angstroms
965
+ if self.config.fix_end_edge:
966
+ new_geometry[-1] = init_geometry_num_list[-1] * self.config.bohr2angstroms
967
+
968
+
969
+ # Apply fixed atoms constraint
970
+ if fix_atom_flag:
971
+ for k in range(len(new_geometry)):
972
+ for j in force_data["fix_atoms"]:
973
+ new_geometry[k][j-1] = copy.copy(init_geometry_num_list[k][j-1] * self.config.bohr2angstroms)
974
+
975
+
976
+ # Apply projection constraints
977
+ if projection_constraint_flag:
978
+ for x in range(len(new_geometry)):
979
+ tmp_new_geometry = new_geometry[x] / self.config.bohr2angstroms
980
+ tmp_new_geometry = PC_list[x].adjust_init_coord(tmp_new_geometry) * self.config.bohr2angstroms
981
+ new_geometry[x] = copy.copy(tmp_new_geometry)
982
+
983
+ # Apply Kabsch alignment if no fixed atoms
984
+ if not fix_atom_flag:
985
+ for k in range(len(new_geometry)-1):
986
+ tmp_new_geometry, _ = Calculationtools().kabsch_algorithm(new_geometry[k], new_geometry[k+1])
987
+ new_geometry[k] = copy.copy(tmp_new_geometry)
988
+
989
+ return new_geometry
990
+
991
+ def make_geometry_list(self, init_input, partition_function):
992
+ """Create geometry list from input files"""
993
+ if os.path.splitext(init_input)[1] == ".xyz":
994
+ self.config.init_input = os.path.splitext(init_input)[0]
995
+ xyz_flag = True
996
+ else:
997
+ xyz_flag = False
998
+
999
+ start_file_list = sum([sorted(glob.glob(os.path.join(init_input, f"*_" + "[0-9]" * i + ".xyz")))
1000
+ for i in range(1, 7)], [])
1001
+
1002
+ loaded_geometry_list = []
1003
+
1004
+ if xyz_flag:
1005
+ geometry_list, elements, electric_charge_and_multiplicity = traj2list(
1006
+ init_input, [self.config.electronic_charge, self.config.spin_multiplicity])
1007
+
1008
+ element_list = elements[0]
1009
+
1010
+ for i in range(len(geometry_list)):
1011
+ loaded_geometry_list.append([electric_charge_and_multiplicity] +
1012
+ [[element_list[num]] + list(map(str, geometry))
1013
+ for num, geometry in enumerate(geometry_list[i])])
1014
+ else:
1015
+ for start_file in start_file_list:
1016
+ tmp_geometry_list, element_list, electric_charge_and_multiplicity = xyz2list(
1017
+ start_file, [self.config.electronic_charge, self.config.spin_multiplicity])
1018
+ tmp_data = [electric_charge_and_multiplicity]
1019
+
1020
+ for i in range(len(tmp_geometry_list)):
1021
+ tmp_data.append([element_list[i]] + list(map(str, tmp_geometry_list[i])))
1022
+ loaded_geometry_list.append(tmp_data)
1023
+
1024
+ electric_charge_and_multiplicity = loaded_geometry_list[0][0]
1025
+ element_list = [row[0] for row in loaded_geometry_list[0][1:]]
1026
+
1027
+ loaded_geometry_num_list = [[list(map(float, row[1:4])) for row in geometry[1:]]
1028
+ for geometry in loaded_geometry_list]
1029
+
1030
+ geometry_list = [loaded_geometry_list[0]]
1031
+
1032
+ tmp_data = []
1033
+
1034
+ for k in range(len(loaded_geometry_list) - 1):
1035
+ delta_num_geom = (np.array(loaded_geometry_num_list[k + 1], dtype="float64") -
1036
+ np.array(loaded_geometry_num_list[k], dtype="float64")) / (partition_function + 1)
1037
+
1038
+ for i in range(partition_function + 1):
1039
+ frame_geom = np.array(loaded_geometry_num_list[k], dtype="float64") + delta_num_geom * i
1040
+ tmp_data.append(frame_geom)
1041
+ tmp_data.append(np.array(loaded_geometry_num_list[-1], dtype="float64"))
1042
+ tmp_data = np.array(tmp_data, dtype="float64")
1043
+
1044
+ # Apply IDPP if requested
1045
+ if self.config.IDPP_flag:
1046
+ IDPP_obj = IDPP()
1047
+ tmp_data = IDPP_obj.opt_path(tmp_data, element_list)
1048
+
1049
+ # Apply CFB_ENM if requested
1050
+ if self.config.CFB_ENM_flag:
1051
+ CFB_ENM_obj = CFB_ENM()
1052
+ tmp_data = CFB_ENM_obj.opt_path(tmp_data, element_list)
1053
+
1054
+ # Align distances if requested
1055
+ if self.config.align_distances > 0:
1056
+ tmp_data = distribute_geometry(tmp_data)
1057
+
1058
+ if self.config.align_distances_bernstein > 0:
1059
+ tmp_data = bernstein_interpolation(tmp_data, len(tmp_data))
1060
+
1061
+
1062
+ if self.config.align_distances_spline_ver2 > 0:
1063
+ tmp_data = spline_interpolation(tmp_data, len(tmp_data))
1064
+
1065
+ if self.config.align_distances_savgol > 0:
1066
+ tmp_data = savitzky_golay_interpolation(tmp_data, len(tmp_data), window_length=self.config.align_distances_savgol_window, polyorder=self.config.align_distances_savgol_poly)
1067
+
1068
+ if self.config.align_distances_geodesic > 0:
1069
+ tmp_data = distribute_geometry_geodesic(tmp_data)
1070
+
1071
+ # Apply node distance constraint if specified
1072
+ if self.config.node_distance is not None:
1073
+ tmp_data = distribute_geometry_by_length(tmp_data, self.config.node_distance)
1074
+
1075
+ if self.config.node_distance_spline is not None:
1076
+ tmp_data = distribute_geometry_by_length_spline(tmp_data, self.config.node_distance_spline)
1077
+
1078
+ if self.config.node_distance_bernstein is not None:
1079
+ tmp_data = distribute_geometry_by_length_bernstein(tmp_data, self.config.node_distance_bernstein)
1080
+
1081
+ if self.config.node_distance_savgol is not None:
1082
+ tmp_data = distribute_geometry_by_length_savgol(tmp_data, self.config.node_distance_savgol, window_length=self.config.node_distance_savgol_window, polyorder=self.config.node_distance_savgol_poly)
1083
+
1084
+ for data in tmp_data:
1085
+ geometry_list.append([electric_charge_and_multiplicity] +
1086
+ [[element_list[num]] + list(map(str, geometry))
1087
+ for num, geometry in enumerate(data)])
1088
+
1089
+ print("\n Geometries are loaded. \n")
1090
+ return geometry_list, element_list, electric_charge_and_multiplicity
1091
+
1092
+ def print_geometry_list(self, new_geometry, element_list, electric_charge_and_multiplicity):
1093
+ """Convert geometry array back to list format"""
1094
+ new_geometry = new_geometry.tolist()
1095
+ geometry_list = []
1096
+ for geometries in new_geometry:
1097
+ new_data = [electric_charge_and_multiplicity]
1098
+ for num, geometry in enumerate(geometries):
1099
+ geometry_data = list(map(str, geometry))
1100
+ geometry_data = [element_list[num]] + geometry_data
1101
+ new_data.append(geometry_data)
1102
+
1103
+ geometry_list.append(new_data)
1104
+ return geometry_list
1105
+
1106
+ def make_input_files(self, geometry_list, optimize_num):
1107
+ """Create input files for calculations"""
1108
+ file_directory = self.config.NEB_FOLDER_DIRECTORY + "path_ITR_" + str(optimize_num) + "_" + str(self.config.init_input)
1109
+ try:
1110
+ os.mkdir(file_directory)
1111
+ except:
1112
+ pass
1113
+ tmp_cs = [self.config.electronic_charge, self.config.spin_multiplicity]
1114
+ float_pattern = r"([+-]?(?:\d+(?:\.\d+)?)(?:[eE][+-]?\d+)?)"
1115
+
1116
+ for y, geometry in enumerate(geometry_list):
1117
+ tmp_geometry = []
1118
+ for geom in geometry:
1119
+ if len(geom) == 4 and re.match(r"[A-Za-z]+", str(geom[0])) \
1120
+ and all(re.match(float_pattern, str(x)) for x in geom[1:]):
1121
+ tmp_geometry.append(geom)
1122
+
1123
+ if len(geom) == 2 and re.match(r"-*\d+", str(geom[0])) and re.match(r"-*\d+", str(geom[1])):
1124
+ tmp_cs = geom
1125
+
1126
+ with open(file_directory + "/" + self.config.init_input + "_" + str(y) + ".xyz", "w") as w:
1127
+ w.write(str(len(tmp_geometry)) + "\n")
1128
+ w.write(str(tmp_cs[0]) + " " + str(tmp_cs[1]) + "\n")
1129
+ for rows in tmp_geometry:
1130
+ w.write(f"{rows[0]:2} {float(rows[1]):>17.12f} {float(rows[2]):>17.12f} {float(rows[3]):>17.12f}\n")
1131
+ return file_directory
1132
+
1133
+ def make_traj_file(self, file_directory):
1134
+ """Create trajectory file from current geometries"""
1135
+ print("\nprocessing geometry collecting ...\n")
1136
+ file_list = sum([sorted(glob.glob(os.path.join(file_directory, f"*_" + "[0-9]" * i + ".xyz")))
1137
+ for i in range(1, 7)], [])
1138
+
1139
+ for m, file in enumerate(file_list):
1140
+ tmp_geometry_list, element_list, _ = xyz2list(file, None)
1141
+ atom_num = len(tmp_geometry_list)
1142
+ with open(file_directory + "/" + self.config.init_input + "_path.xyz", "a") as w:
1143
+ w.write(str(atom_num) + "\n")
1144
+ w.write("Frame " + str(m) + "\n")
1145
+ for i in range(len(tmp_geometry_list)):
1146
+ w.write(f"{element_list[i]:2} {float(tmp_geometry_list[i][0]):17.12f} {float(tmp_geometry_list[i][1]):17.12f} {float(tmp_geometry_list[i][2]):17.12f}\n")
1147
+ print("\ncollecting geometries was complete...\n")
1148
+ return
1149
+
1150
+ def force2velocity(self, gradient_list, element_list):
1151
+ """Convert force to velocity"""
1152
+ velocity_list = gradient_list
1153
+ return np.array(velocity_list, dtype="float64")
1154
+
1155
+ def get_result_file(self):
1156
+ """
1157
+ Gets the absolute file paths for the geometry files corresponding to
1158
+ energy maxima (TS candidates) from the final iteration results.
1159
+ """
1160
+ self.ts_guess_file_list = []
1161
+
1162
+ # 1. Get the last energy list from energy_plot.csv
1163
+ energy_file_path = os.path.join(self.config.NEB_FOLDER_DIRECTORY, "energy_plot.csv")
1164
+
1165
+ if not os.path.exists(energy_file_path):
1166
+ print(f"Error: Energy log file not found: {energy_file_path}")
1167
+ return
1168
+
1169
+ try:
1170
+ with open(energy_file_path, 'r') as f:
1171
+ lines = f.readlines()
1172
+ if not lines:
1173
+ print(f"Error: {energy_file_path} is empty.")
1174
+ return
1175
+ # Get the last line
1176
+ last_line = lines[-1].strip()
1177
+
1178
+ # Load the energy list from CSV as a numpy array
1179
+ biased_energy_list = np.array([float(e) for e in last_line.split(',') if e.strip()])
1180
+
1181
+ except Exception as e:
1182
+ print(f"Error: Failed to read {energy_file_path}: {e}")
1183
+ return
1184
+
1185
+ if len(biased_energy_list) == 0:
1186
+ print("Error: No energy data found in the last line of energy_plot.csv.")
1187
+ return
1188
+
1189
+ # 2. Identify the indices (Z) of energy maxima
1190
+ ts_indices = np.array([], dtype=int)
1191
+ if len(biased_energy_list) > 2:
1192
+ # Find local maxima, excluding endpoints (0 and -1)
1193
+ ts_indices = argrelmax(biased_energy_list[1:-1])[0] + 1
1194
+
1195
+ # If no local maxima are found (e.g., monotonic path),
1196
+ # set the highest energy point (excluding endpoints) as the TS candidate.
1197
+ if len(ts_indices) == 0:
1198
+ print("No local maxima found. Setting the highest energy point (excluding endpoints) as the TS candidate.")
1199
+ ts_indices = np.array([np.argmax(biased_energy_list[1:-1]) + 1])
1200
+
1201
+ elif len(biased_energy_list) > 0:
1202
+ print("Warning: Path has 2 or fewer images. Cannot identify a non-endpoint TS.")
1203
+
1204
+ else:
1205
+ return # Energy list is empty (already caught above, but just in case)
1206
+
1207
+ #print(f"Final energy profile (kcal/mol, relative to start): {[(e - biased_energy_list[0]) * self.config.hartree2kcalmol for e in biased_energy_list]}")
1208
+ print(f"TS candidate indices: {ts_indices}")
1209
+
1210
+ if len(ts_indices) == 0:
1211
+ print("No TS candidates were found.")
1212
+ return
1213
+
1214
+ # 3. Identify the last iteration directory (path_ITR_YYY_XXX)
1215
+ # Find the directory with the largest YYY
1216
+ itr_dirs = glob.glob(os.path.join(self.config.NEB_FOLDER_DIRECTORY, "path_ITR_*"))
1217
+ if not itr_dirs:
1218
+ print(f"Error: Iteration directories not found in {self.config.NEB_FOLDER_DIRECTORY}.")
1219
+ return
1220
+
1221
+ last_itr_num = -1
1222
+ last_itr_dir = ""
1223
+
1224
+ # Pattern: path_ITR_(number)_(init_input_name)
1225
+ # Escape special characters in init_input with re.escape and confirm it matches the end ($)
1226
+ base_name = re.escape(self.config.init_input)
1227
+ pattern = re.compile(r"path_ITR_(\d+)_" + base_name + r"$")
1228
+
1229
+ for d in itr_dirs:
1230
+ dir_name = os.path.basename(d) # Get the directory name itself
1231
+ match = pattern.match(dir_name)
1232
+ if match:
1233
+ try:
1234
+ itr_num = int(match.group(1))
1235
+ if itr_num > last_itr_num:
1236
+ last_itr_num = itr_num
1237
+ last_itr_dir = d
1238
+ except ValueError:
1239
+ continue
1240
+
1241
+ if not last_itr_dir:
1242
+ print(f"Error: No valid last iteration directory ('path_ITR_*_{self.config.init_input}') found.")
1243
+ return
1244
+
1245
+ print(f"Last iteration directory: {last_itr_dir}")
1246
+
1247
+ # 4. Assemble the file paths and add to the list
1248
+ for z in ts_indices:
1249
+ # File format: XXX_Z.xyz (e.g., input_name_5.xyz)
1250
+ file_name = f"{self.config.init_input}_{z}.xyz"
1251
+ file_path = os.path.join(last_itr_dir, file_name)
1252
+
1253
+ if os.path.exists(file_path):
1254
+ # Get the absolute path, as requested
1255
+ abs_path = os.path.abspath(file_path)
1256
+ self.ts_guess_file_list.append(abs_path)
1257
+ print(f"Found TS candidate file: {abs_path}")
1258
+ else:
1259
+ print(f"Warning: Expected file not found: {file_path}")
1260
+
1261
+ # 5. Get traj file path
1262
+ last_itr_traj_file_path = os.path.join(last_itr_dir, f"{self.config.init_input}_path.xyz")
1263
+ self.last_itr_traj_file_path = last_itr_traj_file_path
1264
+
1265
+ return
1266
+
1267
+