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.
- multioptpy/Calculator/__init__.py +0 -0
- multioptpy/Calculator/ase_calculation_tools.py +424 -0
- multioptpy/Calculator/ase_tools/__init__.py +0 -0
- multioptpy/Calculator/ase_tools/fairchem.py +28 -0
- multioptpy/Calculator/ase_tools/gamess.py +19 -0
- multioptpy/Calculator/ase_tools/gaussian.py +165 -0
- multioptpy/Calculator/ase_tools/mace.py +28 -0
- multioptpy/Calculator/ase_tools/mopac.py +19 -0
- multioptpy/Calculator/ase_tools/nwchem.py +31 -0
- multioptpy/Calculator/ase_tools/orca.py +22 -0
- multioptpy/Calculator/ase_tools/pygfn0.py +37 -0
- multioptpy/Calculator/dxtb_calculation_tools.py +344 -0
- multioptpy/Calculator/emt_calculation_tools.py +458 -0
- multioptpy/Calculator/gpaw_calculation_tools.py +183 -0
- multioptpy/Calculator/lj_calculation_tools.py +314 -0
- multioptpy/Calculator/psi4_calculation_tools.py +334 -0
- multioptpy/Calculator/pwscf_calculation_tools.py +189 -0
- multioptpy/Calculator/pyscf_calculation_tools.py +327 -0
- multioptpy/Calculator/sqm1_calculation_tools.py +611 -0
- multioptpy/Calculator/sqm2_calculation_tools.py +376 -0
- multioptpy/Calculator/tblite_calculation_tools.py +352 -0
- multioptpy/Calculator/tersoff_calculation_tools.py +818 -0
- multioptpy/Constraint/__init__.py +0 -0
- multioptpy/Constraint/constraint_condition.py +834 -0
- multioptpy/Coordinate/__init__.py +0 -0
- multioptpy/Coordinate/polar_coordinate.py +199 -0
- multioptpy/Coordinate/redundant_coordinate.py +638 -0
- multioptpy/IRC/__init__.py +0 -0
- multioptpy/IRC/converge_criteria.py +28 -0
- multioptpy/IRC/dvv.py +544 -0
- multioptpy/IRC/euler.py +439 -0
- multioptpy/IRC/hpc.py +564 -0
- multioptpy/IRC/lqa.py +540 -0
- multioptpy/IRC/modekill.py +662 -0
- multioptpy/IRC/rk4.py +579 -0
- multioptpy/Interpolation/__init__.py +0 -0
- multioptpy/Interpolation/adaptive_interpolation.py +283 -0
- multioptpy/Interpolation/binomial_interpolation.py +179 -0
- multioptpy/Interpolation/geodesic_interpolation.py +785 -0
- multioptpy/Interpolation/interpolation.py +156 -0
- multioptpy/Interpolation/linear_interpolation.py +473 -0
- multioptpy/Interpolation/savitzky_golay_interpolation.py +252 -0
- multioptpy/Interpolation/spline_interpolation.py +353 -0
- multioptpy/MD/__init__.py +0 -0
- multioptpy/MD/thermostat.py +185 -0
- multioptpy/MEP/__init__.py +0 -0
- multioptpy/MEP/pathopt_bneb_force.py +443 -0
- multioptpy/MEP/pathopt_dmf_force.py +448 -0
- multioptpy/MEP/pathopt_dneb_force.py +130 -0
- multioptpy/MEP/pathopt_ewbneb_force.py +207 -0
- multioptpy/MEP/pathopt_gpneb_force.py +512 -0
- multioptpy/MEP/pathopt_lup_force.py +113 -0
- multioptpy/MEP/pathopt_neb_force.py +225 -0
- multioptpy/MEP/pathopt_nesb_force.py +205 -0
- multioptpy/MEP/pathopt_om_force.py +153 -0
- multioptpy/MEP/pathopt_qsm_force.py +174 -0
- multioptpy/MEP/pathopt_qsmv2_force.py +304 -0
- multioptpy/ModelFunction/__init__.py +7 -0
- multioptpy/ModelFunction/avoiding_model_function.py +29 -0
- multioptpy/ModelFunction/binary_image_ts_search_model_function.py +47 -0
- multioptpy/ModelFunction/conical_model_function.py +26 -0
- multioptpy/ModelFunction/opt_meci.py +50 -0
- multioptpy/ModelFunction/opt_mesx.py +47 -0
- multioptpy/ModelFunction/opt_mesx_2.py +49 -0
- multioptpy/ModelFunction/seam_model_function.py +27 -0
- multioptpy/ModelHessian/__init__.py +0 -0
- multioptpy/ModelHessian/approx_hessian.py +147 -0
- multioptpy/ModelHessian/calc_params.py +227 -0
- multioptpy/ModelHessian/fischer.py +236 -0
- multioptpy/ModelHessian/fischerd3.py +360 -0
- multioptpy/ModelHessian/fischerd4.py +398 -0
- multioptpy/ModelHessian/gfn0xtb.py +633 -0
- multioptpy/ModelHessian/gfnff.py +709 -0
- multioptpy/ModelHessian/lindh.py +165 -0
- multioptpy/ModelHessian/lindh2007d2.py +707 -0
- multioptpy/ModelHessian/lindh2007d3.py +822 -0
- multioptpy/ModelHessian/lindh2007d4.py +1030 -0
- multioptpy/ModelHessian/morse.py +106 -0
- multioptpy/ModelHessian/schlegel.py +144 -0
- multioptpy/ModelHessian/schlegeld3.py +322 -0
- multioptpy/ModelHessian/schlegeld4.py +559 -0
- multioptpy/ModelHessian/shortrange.py +346 -0
- multioptpy/ModelHessian/swartd2.py +496 -0
- multioptpy/ModelHessian/swartd3.py +706 -0
- multioptpy/ModelHessian/swartd4.py +918 -0
- multioptpy/ModelHessian/tshess.py +40 -0
- multioptpy/Optimizer/QHAdam.py +61 -0
- multioptpy/Optimizer/__init__.py +0 -0
- multioptpy/Optimizer/abc_fire.py +83 -0
- multioptpy/Optimizer/adabelief.py +58 -0
- multioptpy/Optimizer/adabound.py +68 -0
- multioptpy/Optimizer/adadelta.py +65 -0
- multioptpy/Optimizer/adaderivative.py +56 -0
- multioptpy/Optimizer/adadiff.py +68 -0
- multioptpy/Optimizer/adafactor.py +70 -0
- multioptpy/Optimizer/adam.py +65 -0
- multioptpy/Optimizer/adamax.py +62 -0
- multioptpy/Optimizer/adamod.py +83 -0
- multioptpy/Optimizer/adamw.py +65 -0
- multioptpy/Optimizer/adiis.py +523 -0
- multioptpy/Optimizer/afire_neb.py +282 -0
- multioptpy/Optimizer/block_hessian_update.py +709 -0
- multioptpy/Optimizer/c2diis.py +491 -0
- multioptpy/Optimizer/component_wise_scaling.py +405 -0
- multioptpy/Optimizer/conjugate_gradient.py +82 -0
- multioptpy/Optimizer/conjugate_gradient_neb.py +345 -0
- multioptpy/Optimizer/coordinate_locking.py +405 -0
- multioptpy/Optimizer/dic_rsirfo.py +1015 -0
- multioptpy/Optimizer/ediis.py +417 -0
- multioptpy/Optimizer/eve.py +76 -0
- multioptpy/Optimizer/fastadabelief.py +61 -0
- multioptpy/Optimizer/fire.py +77 -0
- multioptpy/Optimizer/fire2.py +249 -0
- multioptpy/Optimizer/fire_neb.py +92 -0
- multioptpy/Optimizer/gan_step.py +486 -0
- multioptpy/Optimizer/gdiis.py +609 -0
- multioptpy/Optimizer/gediis.py +203 -0
- multioptpy/Optimizer/geodesic_step.py +433 -0
- multioptpy/Optimizer/gpmin.py +633 -0
- multioptpy/Optimizer/gpr_step.py +364 -0
- multioptpy/Optimizer/gradientdescent.py +78 -0
- multioptpy/Optimizer/gradientdescent_neb.py +52 -0
- multioptpy/Optimizer/hessian_update.py +433 -0
- multioptpy/Optimizer/hybrid_rfo.py +998 -0
- multioptpy/Optimizer/kdiis.py +625 -0
- multioptpy/Optimizer/lars.py +21 -0
- multioptpy/Optimizer/lbfgs.py +253 -0
- multioptpy/Optimizer/lbfgs_neb.py +355 -0
- multioptpy/Optimizer/linesearch.py +236 -0
- multioptpy/Optimizer/lookahead.py +40 -0
- multioptpy/Optimizer/nadam.py +64 -0
- multioptpy/Optimizer/newton.py +200 -0
- multioptpy/Optimizer/prodigy.py +70 -0
- multioptpy/Optimizer/purtubation.py +16 -0
- multioptpy/Optimizer/quickmin_neb.py +245 -0
- multioptpy/Optimizer/radam.py +75 -0
- multioptpy/Optimizer/rfo_neb.py +302 -0
- multioptpy/Optimizer/ric_rfo.py +842 -0
- multioptpy/Optimizer/rl_step.py +627 -0
- multioptpy/Optimizer/rmspropgrave.py +65 -0
- multioptpy/Optimizer/rsirfo.py +1647 -0
- multioptpy/Optimizer/rsprfo.py +1056 -0
- multioptpy/Optimizer/sadam.py +60 -0
- multioptpy/Optimizer/samsgrad.py +63 -0
- multioptpy/Optimizer/tr_lbfgs.py +678 -0
- multioptpy/Optimizer/trim.py +273 -0
- multioptpy/Optimizer/trust_radius.py +207 -0
- multioptpy/Optimizer/trust_radius_neb.py +121 -0
- multioptpy/Optimizer/yogi.py +60 -0
- multioptpy/OtherMethod/__init__.py +0 -0
- multioptpy/OtherMethod/addf.py +1150 -0
- multioptpy/OtherMethod/dimer.py +895 -0
- multioptpy/OtherMethod/elastic_image_pair.py +629 -0
- multioptpy/OtherMethod/modelfunction.py +456 -0
- multioptpy/OtherMethod/newton_traj.py +454 -0
- multioptpy/OtherMethod/twopshs.py +1095 -0
- multioptpy/PESAnalyzer/__init__.py +0 -0
- multioptpy/PESAnalyzer/calc_irc_curvature.py +125 -0
- multioptpy/PESAnalyzer/cmds_analysis.py +152 -0
- multioptpy/PESAnalyzer/koopman_analysis.py +268 -0
- multioptpy/PESAnalyzer/pca_analysis.py +314 -0
- multioptpy/Parameters/__init__.py +0 -0
- multioptpy/Parameters/atomic_mass.py +20 -0
- multioptpy/Parameters/atomic_number.py +22 -0
- multioptpy/Parameters/covalent_radii.py +44 -0
- multioptpy/Parameters/d2.py +61 -0
- multioptpy/Parameters/d3.py +63 -0
- multioptpy/Parameters/d4.py +103 -0
- multioptpy/Parameters/dreiding.py +34 -0
- multioptpy/Parameters/gfn0xtb_param.py +137 -0
- multioptpy/Parameters/gfnff_param.py +315 -0
- multioptpy/Parameters/gnb.py +104 -0
- multioptpy/Parameters/parameter.py +22 -0
- multioptpy/Parameters/uff.py +72 -0
- multioptpy/Parameters/unit_values.py +20 -0
- multioptpy/Potential/AFIR_potential.py +55 -0
- multioptpy/Potential/LJ_repulsive_potential.py +345 -0
- multioptpy/Potential/__init__.py +0 -0
- multioptpy/Potential/anharmonic_keep_potential.py +28 -0
- multioptpy/Potential/asym_elllipsoidal_potential.py +718 -0
- multioptpy/Potential/electrostatic_potential.py +69 -0
- multioptpy/Potential/flux_potential.py +30 -0
- multioptpy/Potential/gaussian_potential.py +101 -0
- multioptpy/Potential/idpp.py +516 -0
- multioptpy/Potential/keep_angle_potential.py +146 -0
- multioptpy/Potential/keep_dihedral_angle_potential.py +105 -0
- multioptpy/Potential/keep_outofplain_angle_potential.py +70 -0
- multioptpy/Potential/keep_potential.py +99 -0
- multioptpy/Potential/mechano_force_potential.py +74 -0
- multioptpy/Potential/nanoreactor_potential.py +52 -0
- multioptpy/Potential/potential.py +896 -0
- multioptpy/Potential/spacer_model_potential.py +221 -0
- multioptpy/Potential/switching_potential.py +258 -0
- multioptpy/Potential/universal_potential.py +34 -0
- multioptpy/Potential/value_range_potential.py +36 -0
- multioptpy/Potential/void_point_potential.py +25 -0
- multioptpy/SQM/__init__.py +0 -0
- multioptpy/SQM/sqm1/__init__.py +0 -0
- multioptpy/SQM/sqm1/sqm1_core.py +1792 -0
- multioptpy/SQM/sqm2/__init__.py +0 -0
- multioptpy/SQM/sqm2/calc_tools.py +95 -0
- multioptpy/SQM/sqm2/sqm2_basis.py +850 -0
- multioptpy/SQM/sqm2/sqm2_bond.py +119 -0
- multioptpy/SQM/sqm2/sqm2_core.py +303 -0
- multioptpy/SQM/sqm2/sqm2_data.py +1229 -0
- multioptpy/SQM/sqm2/sqm2_disp.py +65 -0
- multioptpy/SQM/sqm2/sqm2_eeq.py +243 -0
- multioptpy/SQM/sqm2/sqm2_overlapint.py +704 -0
- multioptpy/SQM/sqm2/sqm2_qm.py +578 -0
- multioptpy/SQM/sqm2/sqm2_rep.py +66 -0
- multioptpy/SQM/sqm2/sqm2_srb.py +70 -0
- multioptpy/Thermo/__init__.py +0 -0
- multioptpy/Thermo/normal_mode_analyzer.py +865 -0
- multioptpy/Utils/__init__.py +0 -0
- multioptpy/Utils/bond_connectivity.py +264 -0
- multioptpy/Utils/calc_tools.py +884 -0
- multioptpy/Utils/oniom.py +96 -0
- multioptpy/Utils/pbc.py +48 -0
- multioptpy/Utils/riemann_curvature.py +208 -0
- multioptpy/Utils/symmetry_analyzer.py +482 -0
- multioptpy/Visualization/__init__.py +0 -0
- multioptpy/Visualization/visualization.py +156 -0
- multioptpy/WFAnalyzer/MO_analysis.py +104 -0
- multioptpy/WFAnalyzer/__init__.py +0 -0
- multioptpy/Wrapper/__init__.py +0 -0
- multioptpy/Wrapper/autots.py +1239 -0
- multioptpy/Wrapper/ieip_wrapper.py +93 -0
- multioptpy/Wrapper/md_wrapper.py +92 -0
- multioptpy/Wrapper/neb_wrapper.py +94 -0
- multioptpy/Wrapper/optimize_wrapper.py +76 -0
- multioptpy/__init__.py +5 -0
- multioptpy/entrypoints.py +916 -0
- multioptpy/fileio.py +660 -0
- multioptpy/ieip.py +340 -0
- multioptpy/interface.py +1086 -0
- multioptpy/irc.py +529 -0
- multioptpy/moleculardynamics.py +432 -0
- multioptpy/neb.py +1267 -0
- multioptpy/optimization.py +1553 -0
- multioptpy/optimizer.py +709 -0
- multioptpy-1.20.2.dist-info/METADATA +438 -0
- multioptpy-1.20.2.dist-info/RECORD +246 -0
- multioptpy-1.20.2.dist-info/WHEEL +5 -0
- multioptpy-1.20.2.dist-info/entry_points.txt +9 -0
- multioptpy-1.20.2.dist-info/licenses/LICENSE +674 -0
- multioptpy-1.20.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1239 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import numpy as np
|
|
5
|
+
import sys
|
|
6
|
+
import copy
|
|
7
|
+
import traceback
|
|
8
|
+
|
|
9
|
+
# Try importing Matplotlib, but make it optional
|
|
10
|
+
try:
|
|
11
|
+
import matplotlib
|
|
12
|
+
matplotlib.use('Agg') # Use non-interactive backend
|
|
13
|
+
import matplotlib.pyplot as plt
|
|
14
|
+
MATPLOTLIB_AVAILABLE = True
|
|
15
|
+
except ImportError:
|
|
16
|
+
MATPLOTLIB_AVAILABLE = False
|
|
17
|
+
print("Warning: matplotlib not found. Energy profile plots will not be generated.")
|
|
18
|
+
print("Please run 'pip install matplotlib' to enable plotting.")
|
|
19
|
+
|
|
20
|
+
# Use relative imports as this file is inside the 'Wrapper' package
|
|
21
|
+
try:
|
|
22
|
+
from multioptpy.Wrapper.optimize_wrapper import OptimizationJob
|
|
23
|
+
from multioptpy.Wrapper.neb_wrapper import NEBJob
|
|
24
|
+
except ImportError:
|
|
25
|
+
print("Error: Could not import OptimizationJob or NEBJob from relative paths.")
|
|
26
|
+
print("Ensure autots.py and the wrappers are in the same 'Wrapper' directory.")
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AutoTSWorkflow:
|
|
31
|
+
"""
|
|
32
|
+
Manages the 4-step (AFIR -> NEB -> TS -> IRC) automated workflow.
|
|
33
|
+
"""
|
|
34
|
+
def __init__(self, config):
|
|
35
|
+
self.config = config
|
|
36
|
+
self.work_dir = config.get("work_dir", "autots_workflow")
|
|
37
|
+
self.initial_mol_file = config.get("initial_mol_file")
|
|
38
|
+
self.conf_file_source = config.get("software_path_file_source")
|
|
39
|
+
|
|
40
|
+
self.top_n_candidates = config.get("top_n_candidates", 3)
|
|
41
|
+
self.skip_step1 = config.get("skip_step1", False)
|
|
42
|
+
self.run_step4 = config.get("run_step4", False)
|
|
43
|
+
self.skip_to_step4 = config.get("skip_to_step4", False)
|
|
44
|
+
|
|
45
|
+
self.input_base_name = os.path.splitext(os.path.basename(self.initial_mol_file))[0]
|
|
46
|
+
# This will be populated by Step 3
|
|
47
|
+
self.ts_final_files = []
|
|
48
|
+
|
|
49
|
+
def setup_workspace(self):
|
|
50
|
+
"""Prepares the working directory and copies necessary input files."""
|
|
51
|
+
if os.path.exists(self.work_dir):
|
|
52
|
+
print(f"Warning: Working directory '{self.work_dir}' already exists.")
|
|
53
|
+
os.makedirs(self.work_dir, exist_ok=True)
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
abs_initial_path = os.path.abspath(self.initial_mol_file)
|
|
57
|
+
local_mol_name = os.path.basename(self.initial_mol_file)
|
|
58
|
+
shutil.copy(abs_initial_path, os.path.join(self.work_dir, local_mol_name))
|
|
59
|
+
self.initial_mol_file = local_mol_name
|
|
60
|
+
except shutil.Error as e:
|
|
61
|
+
print(f"Warning: Could not copy initial file (may be the same file): {e}")
|
|
62
|
+
except FileNotFoundError:
|
|
63
|
+
print(f"Error: Initial molecule file not found: {self.initial_mol_file}")
|
|
64
|
+
raise
|
|
65
|
+
|
|
66
|
+
if not self.conf_file_source or not os.path.exists(self.conf_file_source):
|
|
67
|
+
raise FileNotFoundError(
|
|
68
|
+
f"Software config file not found at: {self.conf_file_source}"
|
|
69
|
+
)
|
|
70
|
+
try:
|
|
71
|
+
local_conf_name = os.path.basename(self.conf_file_source)
|
|
72
|
+
shutil.copy(self.conf_file_source, os.path.join(self.work_dir, local_conf_name))
|
|
73
|
+
print(f"Copied '{self.conf_file_source}' to '{self.work_dir}'")
|
|
74
|
+
except shutil.Error as e:
|
|
75
|
+
print(f"Warning: Could not copy software_path.conf (may be the same file): {e}")
|
|
76
|
+
|
|
77
|
+
os.chdir(self.work_dir)
|
|
78
|
+
print(f"Changed directory to: {os.getcwd()}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _run_step1_afir_scan(self):
|
|
82
|
+
"""Runs the AFIR scan and copies the resulting trajectory."""
|
|
83
|
+
print("\n--- 1. STARTING STEP 1: AFIR SCAN ---")
|
|
84
|
+
job1_settings = self.config.get("step1_settings", {})
|
|
85
|
+
|
|
86
|
+
if not job1_settings.get("manual_AFIR"):
|
|
87
|
+
raise ValueError("Step 1 settings must contain 'manual_AFIR' (-ma) options.")
|
|
88
|
+
|
|
89
|
+
job = OptimizationJob(input_file=self.initial_mol_file)
|
|
90
|
+
job.set_options(**job1_settings)
|
|
91
|
+
job.run()
|
|
92
|
+
|
|
93
|
+
optimizer_instance = job.get_results()
|
|
94
|
+
if optimizer_instance is None:
|
|
95
|
+
raise RuntimeError("Step 1 failed to produce an optimizer instance.")
|
|
96
|
+
|
|
97
|
+
optimizer_instance.get_result_file_path()
|
|
98
|
+
|
|
99
|
+
source_traj_path = optimizer_instance.traj_file
|
|
100
|
+
if not source_traj_path or not os.path.exists(source_traj_path):
|
|
101
|
+
raise FileNotFoundError(f"Step 1 finished, but 'traj_file' was not found at: {source_traj_path}")
|
|
102
|
+
|
|
103
|
+
local_traj_name = f"{self.input_base_name}_step1_traj.xyz"
|
|
104
|
+
shutil.copy(source_traj_path, local_traj_name)
|
|
105
|
+
|
|
106
|
+
print(f"Copied AFIR trajectory to: {os.path.abspath(local_traj_name)}")
|
|
107
|
+
print("--- STEP 1: AFIR SCAN COMPLETE ---")
|
|
108
|
+
return local_traj_name
|
|
109
|
+
|
|
110
|
+
def _run_step2_neb_optimization(self, afir_traj_path):
|
|
111
|
+
"""Runs NEB, filters candidates by energy, and copies the top N."""
|
|
112
|
+
print("\n--- 2. STARTING STEP 2: NEB OPTIMIZATION ---")
|
|
113
|
+
|
|
114
|
+
job = NEBJob(input_files=[afir_traj_path])
|
|
115
|
+
job2_settings = self.config.get("step2_settings", {})
|
|
116
|
+
job.set_options(**job2_settings)
|
|
117
|
+
job.run()
|
|
118
|
+
|
|
119
|
+
neb_instance = job.get_results()
|
|
120
|
+
if neb_instance is None:
|
|
121
|
+
raise RuntimeError("Step 2 failed to produce an NEB instance.")
|
|
122
|
+
|
|
123
|
+
neb_instance.get_result_file()
|
|
124
|
+
source_ts_paths = neb_instance.ts_guess_file_list
|
|
125
|
+
|
|
126
|
+
if not source_ts_paths:
|
|
127
|
+
print("Step 2 (NEB) did not find any TS candidate files (ts_guess_file_list is empty).")
|
|
128
|
+
return []
|
|
129
|
+
|
|
130
|
+
energy_csv_path = os.path.join(neb_instance.config.NEB_FOLDER_DIRECTORY, "energy_plot.csv")
|
|
131
|
+
selected_paths = self._filter_candidates_by_energy(source_ts_paths, energy_csv_path)
|
|
132
|
+
|
|
133
|
+
refinement_dir = f"{self.input_base_name}_step3_TS_Opt_Inputs"
|
|
134
|
+
os.makedirs(refinement_dir, exist_ok=True)
|
|
135
|
+
local_ts_paths = []
|
|
136
|
+
|
|
137
|
+
print(f"Copying {len(selected_paths)} highest energy candidates for refinement...")
|
|
138
|
+
for i, source_path in enumerate(selected_paths):
|
|
139
|
+
if not os.path.exists(source_path):
|
|
140
|
+
print(f"Warning: Source file not found, skipping: {source_path}")
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
local_guess_name = f"{self.input_base_name}_ts_guess_{i+1}.xyz"
|
|
144
|
+
local_path = os.path.join(refinement_dir, local_guess_name)
|
|
145
|
+
shutil.copy(source_path, local_path)
|
|
146
|
+
print(f" Copied {os.path.basename(source_path)} to {local_path}")
|
|
147
|
+
local_ts_paths.append(local_path)
|
|
148
|
+
|
|
149
|
+
print("--- STEP 2: NEB OPTIMIZATION COMPLETE ---")
|
|
150
|
+
return local_ts_paths
|
|
151
|
+
|
|
152
|
+
def _filter_candidates_by_energy(self, file_paths, energy_csv_path):
|
|
153
|
+
"""
|
|
154
|
+
Parses the energy_plot.csv, correlates it with candidate file paths,
|
|
155
|
+
and returns the paths for the Top N highest energy candidates.
|
|
156
|
+
"""
|
|
157
|
+
print(f"Filtering {len(file_paths)} candidates down to a max of {self.top_n_candidates} by energy...")
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
with open(energy_csv_path, 'r') as f:
|
|
161
|
+
lines = f.readlines()
|
|
162
|
+
if not lines:
|
|
163
|
+
raise ValueError(f"{energy_csv_path} is empty.")
|
|
164
|
+
last_line = lines[-1].strip()
|
|
165
|
+
energies = np.array([float(e) for e in last_line.split(',') if e.strip()])
|
|
166
|
+
except Exception as e:
|
|
167
|
+
print(f"Warning: Could not read or parse energy file '{energy_csv_path}': {e}")
|
|
168
|
+
print("Proceeding with all found candidates (unsorted).")
|
|
169
|
+
return file_paths[:self.top_n_candidates]
|
|
170
|
+
|
|
171
|
+
candidates = []
|
|
172
|
+
for path in file_paths:
|
|
173
|
+
try:
|
|
174
|
+
base_name = os.path.splitext(os.path.basename(path))[0]
|
|
175
|
+
index_str = base_name.split('_')[-1]
|
|
176
|
+
z = int(index_str)
|
|
177
|
+
if z >= len(energies):
|
|
178
|
+
print(f"Warning: Index {z} from file '{path}' is out of bounds for energy list (len {len(energies)}).")
|
|
179
|
+
continue
|
|
180
|
+
candidates.append((energies[z], path))
|
|
181
|
+
except Exception as e:
|
|
182
|
+
print(f"Warning: Could not parse index from '{path}': {e}. Skipping.")
|
|
183
|
+
|
|
184
|
+
candidates.sort(key=lambda x: x[0], reverse=True)
|
|
185
|
+
top_n_list = candidates[:self.top_n_candidates]
|
|
186
|
+
|
|
187
|
+
print(f"Identified {len(candidates)} candidates, selecting top {len(top_n_list)}:")
|
|
188
|
+
for (energy, path) in top_n_list:
|
|
189
|
+
print(f" - Path: {os.path.basename(path)}, Energy: {energy:.6f} Hartree")
|
|
190
|
+
|
|
191
|
+
return [path for energy, path in top_n_list]
|
|
192
|
+
|
|
193
|
+
def _run_step3_ts_refinement(self, local_ts_guess_paths):
|
|
194
|
+
"""Runs saddle_order=1 OptimizationJob on all local candidates."""
|
|
195
|
+
print("\n--- 3. STARTING STEP 3: TS REFINEMENT ---")
|
|
196
|
+
|
|
197
|
+
if not local_ts_guess_paths:
|
|
198
|
+
print("No TS candidates provided. Skipping refinement.")
|
|
199
|
+
return []
|
|
200
|
+
|
|
201
|
+
job3_settings = self.config.get("step3_settings", {})
|
|
202
|
+
final_ts_files = []
|
|
203
|
+
|
|
204
|
+
for i, guess_file_path in enumerate(local_ts_guess_paths):
|
|
205
|
+
print(f"\n--- Running TS refinement for candidate {i+1}/{len(local_ts_guess_paths)} ({guess_file_path}) ---")
|
|
206
|
+
|
|
207
|
+
job = OptimizationJob(input_file=guess_file_path)
|
|
208
|
+
current_job_settings = job3_settings.copy()
|
|
209
|
+
current_job_settings['saddle_order'] = 1
|
|
210
|
+
job.set_options(**current_job_settings)
|
|
211
|
+
job.run()
|
|
212
|
+
|
|
213
|
+
optimizer_instance = job.get_results()
|
|
214
|
+
if optimizer_instance is None:
|
|
215
|
+
print(f"Warning: Refinement for {guess_file_path} failed.")
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
optimizer_instance.get_result_file_path()
|
|
219
|
+
source_final_ts_path = optimizer_instance.optimized_struct_file
|
|
220
|
+
if not source_final_ts_path or not os.path.exists(source_final_ts_path):
|
|
221
|
+
print(f"Warning: Refinement for {guess_file_path} finished, but 'optimized_struct_file' was not found.")
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
if not optimizer_instance.optimized_flag:
|
|
225
|
+
print(f"Warning: Refinement for {guess_file_path} did not converge (optimized_flag=False). Skipping.")
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
local_final_name = f"{self.input_base_name}_ts_final_{i+1}.xyz"
|
|
229
|
+
shutil.copy(source_final_ts_path, local_final_name)
|
|
230
|
+
|
|
231
|
+
print(f"Copied final TS structure to: {os.path.abspath(local_final_name)}")
|
|
232
|
+
final_ts_files.append(local_final_name)
|
|
233
|
+
|
|
234
|
+
print("--- STEP 3: TS REFINEMENT COMPLETE ---")
|
|
235
|
+
return final_ts_files
|
|
236
|
+
|
|
237
|
+
def _run_step4_irc_and_opt(self, ts_final_files):
|
|
238
|
+
"""
|
|
239
|
+
Runs Step 4: IRC calculation, endpoint optimization, and visualization.
|
|
240
|
+
"""
|
|
241
|
+
print("\n--- 4. STARTING STEP 4: IRC & EQ OPTIMIZATION ---")
|
|
242
|
+
|
|
243
|
+
if not ts_final_files:
|
|
244
|
+
print("No TS files provided from Step 3 (or --skip_to_step4). Skipping Step 4.")
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
step4_settings = self.config.get("step4_settings", {})
|
|
248
|
+
if "intrinsic_reaction_coordinates" not in step4_settings:
|
|
249
|
+
raise ValueError("Step 4 requires 'intrinsic_reaction_coordinates' settings in config.json")
|
|
250
|
+
|
|
251
|
+
for i, ts_path in enumerate(ts_final_files):
|
|
252
|
+
ts_index = i + 1
|
|
253
|
+
# If skipping, the base name might be long, so create a short ID
|
|
254
|
+
if self.skip_to_step4:
|
|
255
|
+
ts_name_base = f"{self.input_base_name}_TS_{ts_index}"
|
|
256
|
+
else:
|
|
257
|
+
ts_name_base = f"{self.input_base_name}_ts_final_{ts_index}"
|
|
258
|
+
|
|
259
|
+
print(f"\n--- Running Step 4 for TS Candidate {ts_index} ({ts_path}) ---")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
step4_settings["saddle_order"] = 1
|
|
263
|
+
|
|
264
|
+
# --- 4A: Run IRC ---
|
|
265
|
+
print(f" 4A: Running IRC for {ts_path}...")
|
|
266
|
+
job_irc = OptimizationJob(input_file=ts_path)
|
|
267
|
+
# Pass the full Step 4 settings (including -irc)
|
|
268
|
+
job_irc.set_options(**step4_settings)
|
|
269
|
+
|
|
270
|
+
job_irc.run()
|
|
271
|
+
|
|
272
|
+
irc_instance = job_irc.get_results()
|
|
273
|
+
if irc_instance is None:
|
|
274
|
+
print(f" Warning: IRC job for {ts_path} failed.")
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
# Get TS energy (as per Q2, this is set before IRC runs)
|
|
278
|
+
ts_e = irc_instance.final_energy
|
|
279
|
+
ts_bias_e = irc_instance.final_bias_energy
|
|
280
|
+
|
|
281
|
+
# Get IRC endpoint paths
|
|
282
|
+
endpoint_paths = irc_instance.irc_terminal_struct_paths
|
|
283
|
+
if not endpoint_paths or len(endpoint_paths) != 2:
|
|
284
|
+
print(f" Warning: IRC job for {ts_path} did not return 2 endpoint files. Aborting Step 4 for this candidate.")
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
print(f" IRC found endpoints: {endpoint_paths}")
|
|
288
|
+
|
|
289
|
+
# --- 4B: Run Endpoint Optimization ---
|
|
290
|
+
endpoint_results = []
|
|
291
|
+
for j, end_path in enumerate(endpoint_paths):
|
|
292
|
+
|
|
293
|
+
shutil.copy(end_path, f"{ts_name_base}_IRC_Endpoint_{j+1}.xyz")
|
|
294
|
+
end_path = f"{ts_name_base}_IRC_Endpoint_{j+1}.xyz"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
print(f" 4B: Optimizing endpoint {j+1} from {end_path}...")
|
|
298
|
+
job_opt = OptimizationJob(input_file=end_path)
|
|
299
|
+
|
|
300
|
+
# Prepare settings for endpoint optimization
|
|
301
|
+
opt_settings = copy.deepcopy(step4_settings)
|
|
302
|
+
|
|
303
|
+
# Use the new dedicated opt_method for step 4B
|
|
304
|
+
opt_settings["opt_method"] = opt_settings.get("step4b_opt_method", ["rsirfo_block_fsb"])
|
|
305
|
+
|
|
306
|
+
# Remove IRC flag (as per Q3)
|
|
307
|
+
opt_settings.pop("intrinsic_reaction_coordinates", None)
|
|
308
|
+
|
|
309
|
+
# Set saddle_order=0 (minimization)
|
|
310
|
+
opt_settings['saddle_order'] = 0
|
|
311
|
+
|
|
312
|
+
job_opt.set_options(**opt_settings)
|
|
313
|
+
job_opt.run()
|
|
314
|
+
|
|
315
|
+
opt_instance = job_opt.get_results()
|
|
316
|
+
if opt_instance is None:
|
|
317
|
+
print(f" Warning: Optimization for endpoint {end_path} failed.")
|
|
318
|
+
continue
|
|
319
|
+
|
|
320
|
+
opt_instance.get_result_file_path()
|
|
321
|
+
final_opt_path = opt_instance.optimized_struct_file
|
|
322
|
+
if not final_opt_path or not os.path.exists(final_opt_path):
|
|
323
|
+
print(f" Warning: Optimization for {end_path} finished, but 'optimized_struct_file' was not found.")
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
endpoint_results.append({
|
|
327
|
+
"path": final_opt_path,
|
|
328
|
+
"e": opt_instance.final_energy,
|
|
329
|
+
"bias_e": opt_instance.final_bias_energy,
|
|
330
|
+
"label": f"Endpoint_{j+1}"
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
if not endpoint_results:
|
|
334
|
+
print(f" Warning: Failed to optimize any endpoints for {ts_path}. Aborting result collection.")
|
|
335
|
+
continue
|
|
336
|
+
|
|
337
|
+
if len(endpoint_results) > 2:
|
|
338
|
+
print(f" Warning: More than 2 optimized endpoints found.")
|
|
339
|
+
raise ValueError("More than 2 endpoints found after optimization.")
|
|
340
|
+
|
|
341
|
+
# --- 4C: Collect Results & Visualize ---
|
|
342
|
+
print(f" 4C: Collecting results for TS Candidate {ts_index}...")
|
|
343
|
+
# Prepare result directory
|
|
344
|
+
result_dir = f"{ts_name_base}_Step4_Profile"
|
|
345
|
+
os.makedirs(result_dir, exist_ok=True)
|
|
346
|
+
|
|
347
|
+
e_profile = {
|
|
348
|
+
"TS": {"e": ts_e, "bias_e": ts_bias_e, "path": ts_path},
|
|
349
|
+
}
|
|
350
|
+
if len(endpoint_results) >= 1:
|
|
351
|
+
e_profile["End1"] = endpoint_results[0]
|
|
352
|
+
if len(endpoint_results) >= 2:
|
|
353
|
+
e_profile["End2"] = endpoint_results[1]
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# Create plot (as per Q1)
|
|
357
|
+
plot_path = os.path.join(result_dir, "energy_profile.png")
|
|
358
|
+
self._create_energy_profile_plot(e_profile, plot_path, ts_name_base)
|
|
359
|
+
|
|
360
|
+
# Write text file
|
|
361
|
+
text_path = os.path.join(result_dir, "energy_profile.txt")
|
|
362
|
+
self._write_energy_profile_text(e_profile, text_path, ts_name_base)
|
|
363
|
+
|
|
364
|
+
# Copy final XYZ files
|
|
365
|
+
shutil.copy(ts_path, os.path.join(result_dir, f"{ts_name_base}_ts_final.xyz"))
|
|
366
|
+
if "End1" in e_profile:
|
|
367
|
+
shutil.copy(e_profile["End1"]["path"], os.path.join(result_dir, "endpoint_1_opt.xyz"))
|
|
368
|
+
if "End2" in e_profile:
|
|
369
|
+
shutil.copy(e_profile["End2"]["path"], os.path.join(result_dir, "endpoint_2_opt.xyz"))
|
|
370
|
+
|
|
371
|
+
print(f" Successfully saved profile and structures to: {result_dir}")
|
|
372
|
+
|
|
373
|
+
print("\n--- STEP 4: IRC & EQ OPTIMIZATION COMPLETE ---")
|
|
374
|
+
|
|
375
|
+
def _create_energy_profile_plot(self, e_profile, output_path, title_name):
|
|
376
|
+
"""Generates an energy profile plot using matplotlib."""
|
|
377
|
+
if not MATPLOTLIB_AVAILABLE:
|
|
378
|
+
print(f" Skipping plot generation: matplotlib not installed.")
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
labels = []
|
|
383
|
+
energies = []
|
|
384
|
+
biased_energies = []
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
if "End1" in e_profile:
|
|
388
|
+
labels.append("End1")
|
|
389
|
+
energies.append(e_profile["End1"]["e"])
|
|
390
|
+
biased_energies.append(e_profile["End1"]["bias_e"])
|
|
391
|
+
|
|
392
|
+
labels.append("TS")
|
|
393
|
+
energies.append(e_profile["TS"]["e"])
|
|
394
|
+
biased_energies.append(e_profile["TS"]["bias_e"])
|
|
395
|
+
|
|
396
|
+
if "End2" in e_profile:
|
|
397
|
+
labels.append("End2")
|
|
398
|
+
energies.append(e_profile["End2"]["e"])
|
|
399
|
+
biased_energies.append(e_profile["End2"]["bias_e"])
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
if not energies:
|
|
403
|
+
print(" Warning: No energies found to plot.")
|
|
404
|
+
return
|
|
405
|
+
|
|
406
|
+
# Convert to relative kcal/mol
|
|
407
|
+
min_e = min(energies)
|
|
408
|
+
min_bias_e = min(biased_energies)
|
|
409
|
+
|
|
410
|
+
rel_energies = (np.array(energies) - min_e) * 627.509
|
|
411
|
+
rel_biased_energies = (np.array(biased_energies) - min_bias_e) * 627.509
|
|
412
|
+
|
|
413
|
+
x = list(range(len(labels)))
|
|
414
|
+
plt.figure(figsize=(8, 6))
|
|
415
|
+
|
|
416
|
+
# Plot both energies as requested in Q1
|
|
417
|
+
plt.plot(x, rel_energies, 'o-', c='blue', label='Energy (E_final)')
|
|
418
|
+
plt.plot(x, rel_biased_energies, 'o--', c='red', label='Bias Energy (E_bias_final)')
|
|
419
|
+
|
|
420
|
+
plt.xticks(x, labels)
|
|
421
|
+
plt.ylabel("Relative Energy (kcal/mol)")
|
|
422
|
+
plt.title(f"Reaction Profile for {title_name}")
|
|
423
|
+
plt.legend()
|
|
424
|
+
plt.grid(axis='y', linestyle='--', alpha=0.7)
|
|
425
|
+
plt.savefig(output_path, dpi=300)
|
|
426
|
+
plt.close()
|
|
427
|
+
print(f" Generated energy plot: {output_path}")
|
|
428
|
+
|
|
429
|
+
except Exception as e:
|
|
430
|
+
print(f" Warning: Failed to generate energy plot: {e}")
|
|
431
|
+
|
|
432
|
+
def _write_energy_profile_text(self, e_profile, output_path, title_name):
|
|
433
|
+
"""Writes the final energies to a text file."""
|
|
434
|
+
try:
|
|
435
|
+
with open(output_path, 'w') as f:
|
|
436
|
+
f.write(f"# Energy Profile for {title_name}\n")
|
|
437
|
+
f.write("# All energies in Hartree\n")
|
|
438
|
+
f.write("# -------------------------------------------------------------------\n")
|
|
439
|
+
f.write(f"Structure, File_Path, Final_Energy, Final_Bias_Energy\n")
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
ts_total = e_profile['TS']['bias_e']
|
|
443
|
+
f.write(f"TS, {e_profile['TS']['path']}, {e_profile['TS']['e']:.12f}, {e_profile['TS']['bias_e']:.12f}\n")
|
|
444
|
+
|
|
445
|
+
end1_total = None
|
|
446
|
+
if "End1" in e_profile:
|
|
447
|
+
end1_total = e_profile['End1']['bias_e']
|
|
448
|
+
f.write(f"Endpoint_1, {e_profile['End1']['path']}, {e_profile['End1']['e']:.12f}, {e_profile['End1']['bias_e']:.12f}\n")
|
|
449
|
+
|
|
450
|
+
end2_total = None
|
|
451
|
+
if "End2" in e_profile:
|
|
452
|
+
end2_total = e_profile['End2']['bias_e']
|
|
453
|
+
f.write(f"Endpoint_2, {e_profile['End2']['path']}, {e_profile['End2']['e']:.12f}, {e_profile['End2']['bias_e']:.12f}\n")
|
|
454
|
+
|
|
455
|
+
f.write("# -------------------------------------------------------------------\n")
|
|
456
|
+
# Calculate and write barriers and reaction energies if possible
|
|
457
|
+
if end1_total is not None:
|
|
458
|
+
barrier_1 = (ts_total - end1_total) * 627.509
|
|
459
|
+
f.write(f"Activation Energy (End1 -> TS): {barrier_1: .2f} kcal/mol\n")
|
|
460
|
+
|
|
461
|
+
if end2_total is not None:
|
|
462
|
+
barrier_2 = (ts_total - end2_total) * 627.509
|
|
463
|
+
f.write(f"Activation Energy (End2 -> TS): {barrier_2: .2f} kcal/mol\n")
|
|
464
|
+
|
|
465
|
+
if end1_total is not None and end2_total is not None:
|
|
466
|
+
reaction_e = (end2_total - end1_total) * 627.509
|
|
467
|
+
f.write(f"Reaction Energy (End1 -> End2): {reaction_e: .2f} kcal/mol\n")
|
|
468
|
+
elif end1_total is not None:
|
|
469
|
+
pass
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
print(f" Generated energy text file: {output_path}")
|
|
473
|
+
except Exception as e:
|
|
474
|
+
print(f" Warning: Failed to write energy text file: {e}")
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def run_workflow(self):
|
|
478
|
+
"""Executes the full automated workflow."""
|
|
479
|
+
original_cwd = os.getcwd()
|
|
480
|
+
try:
|
|
481
|
+
if not os.path.exists(self.initial_mol_file):
|
|
482
|
+
raise FileNotFoundError(f"Initial molecule file not found: {self.initial_mol_file}")
|
|
483
|
+
|
|
484
|
+
self.setup_workspace()
|
|
485
|
+
|
|
486
|
+
if self.skip_to_step4:
|
|
487
|
+
# --- Run Step 4 Only ---
|
|
488
|
+
print("\n--- Skipping to Step 4 ---")
|
|
489
|
+
# The input file is the TS file
|
|
490
|
+
self.ts_final_files = [self.initial_mol_file]
|
|
491
|
+
# Ensure base name is from the TS file
|
|
492
|
+
self.input_base_name = os.path.splitext(os.path.basename(self.initial_mol_file))[0]
|
|
493
|
+
|
|
494
|
+
else:
|
|
495
|
+
# --- Run Steps 1-3 ---
|
|
496
|
+
if self.skip_step1:
|
|
497
|
+
print("\n--- 1. STEP 1 (AFIR) SKIPPED ---")
|
|
498
|
+
local_afir_traj = self.initial_mol_file
|
|
499
|
+
else:
|
|
500
|
+
local_afir_traj = self._run_step1_afir_scan()
|
|
501
|
+
|
|
502
|
+
local_ts_paths = self._run_step2_neb_optimization(local_afir_traj)
|
|
503
|
+
|
|
504
|
+
if not local_ts_paths:
|
|
505
|
+
print("Step 2 found 0 candidates. Workflow terminated.")
|
|
506
|
+
print(f"\n --- AUTO-TS WORKFLOW COMPLETED (NO TS FOUND) --- ")
|
|
507
|
+
return
|
|
508
|
+
|
|
509
|
+
self.ts_final_files = self._run_step3_ts_refinement(local_ts_paths)
|
|
510
|
+
|
|
511
|
+
# --- Run Step 4 (if flagged) ---
|
|
512
|
+
if (self.run_step4 or self.skip_to_step4) and self.ts_final_files:
|
|
513
|
+
self._run_step4_irc_and_opt(self.ts_final_files)
|
|
514
|
+
|
|
515
|
+
print(f"\n --- AUTO-TS WORKFLOW COMPLETED SUCCESSFULLY --- ")
|
|
516
|
+
print(f"All results are in: {os.path.realpath(os.getcwd())}")
|
|
517
|
+
|
|
518
|
+
except Exception as e:
|
|
519
|
+
print(f"\n --- AUTO-TS WORKFLOW FAILED --- ")
|
|
520
|
+
print(f"Error: {e}")
|
|
521
|
+
|
|
522
|
+
traceback.print_exc()
|
|
523
|
+
finally:
|
|
524
|
+
os.chdir(original_cwd)
|
|
525
|
+
print(f"Returned to directory: {original_cwd}")
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
class AutoTSWorkflow_v2(AutoTSWorkflow):
|
|
529
|
+
"""
|
|
530
|
+
AutoTSWorkflow (v2) - (FIXED)
|
|
531
|
+
|
|
532
|
+
Manages a dynamic, repeatable, multi-step workflow based on a
|
|
533
|
+
"workflow" block in the configuration file.
|
|
534
|
+
|
|
535
|
+
"""
|
|
536
|
+
|
|
537
|
+
def __init__(self, config):
|
|
538
|
+
"""
|
|
539
|
+
Initializes the v2 workflow.
|
|
540
|
+
"""
|
|
541
|
+
# Call the parent __init__ to load basic settings
|
|
542
|
+
super().__init__(config)
|
|
543
|
+
|
|
544
|
+
# v2-specific attributes
|
|
545
|
+
self.data_cache = {}
|
|
546
|
+
self.workflow_steps = self.config.get("workflow", [])
|
|
547
|
+
|
|
548
|
+
# Validate the workflow configuration immediately
|
|
549
|
+
try:
|
|
550
|
+
self._validate_workflow_config()
|
|
551
|
+
except ValueError as e:
|
|
552
|
+
print(f"Error: Workflow configuration is invalid.")
|
|
553
|
+
print(f"Details: {e}")
|
|
554
|
+
sys.exit(1)
|
|
555
|
+
|
|
556
|
+
def _validate_workflow_config(self):
|
|
557
|
+
"""
|
|
558
|
+
Validates the 'workflow' block in config.json.
|
|
559
|
+
"""
|
|
560
|
+
print("Validating workflow configuration...")
|
|
561
|
+
if not self.workflow_steps:
|
|
562
|
+
print("Warning: 'workflow' block is empty or missing. No steps will be run.")
|
|
563
|
+
return
|
|
564
|
+
|
|
565
|
+
for i, entry in enumerate(self.workflow_steps):
|
|
566
|
+
if "step" not in entry:
|
|
567
|
+
raise ValueError(f"Workflow entry {i} is missing required key 'step'.")
|
|
568
|
+
|
|
569
|
+
step_name = entry["step"]
|
|
570
|
+
if not hasattr(self, f"_run_{step_name}"):
|
|
571
|
+
raise ValueError(f"Workflow entry {i} specifies invalid step: '{step_name}'. No method '_run_{step_name}' found.")
|
|
572
|
+
|
|
573
|
+
repeat = entry.get("repeat", 1)
|
|
574
|
+
if not isinstance(repeat, int) or repeat < 1:
|
|
575
|
+
raise ValueError(f"Workflow entry {i} ({step_name}): 'repeat' must be a positive integer.")
|
|
576
|
+
|
|
577
|
+
repeat_settings = entry.get("repeat_settings", [])
|
|
578
|
+
|
|
579
|
+
if repeat_settings and len(repeat_settings) > repeat:
|
|
580
|
+
raise ValueError(f"Workflow entry {i} ({step_name}): 'repeat_settings' list (len {len(repeat_settings)}) is longer than 'repeat' value ({repeat}).")
|
|
581
|
+
|
|
582
|
+
base_key = entry.get("settings_key", f"{step_name}_settings")
|
|
583
|
+
if base_key not in self.config:
|
|
584
|
+
raise ValueError(f"Workflow entry {i} ({step_name}): Base settings key '{base_key}' (or default) not found in main config.")
|
|
585
|
+
|
|
586
|
+
print("Workflow validation successful.")
|
|
587
|
+
|
|
588
|
+
def run_workflow(self):
|
|
589
|
+
"""
|
|
590
|
+
Overrides the parent 'run_workflow' to call the
|
|
591
|
+
v2 dynamic workflow engine.
|
|
592
|
+
"""
|
|
593
|
+
original_cwd = os.getcwd()
|
|
594
|
+
try:
|
|
595
|
+
if not os.path.exists(self.initial_mol_file):
|
|
596
|
+
if not self.config.get("skip_to_step4"):
|
|
597
|
+
raise FileNotFoundError(f"Initial molecule file not found: {self.initial_mol_file}")
|
|
598
|
+
|
|
599
|
+
self.setup_workspace()
|
|
600
|
+
self.run_dynamic_workflow()
|
|
601
|
+
|
|
602
|
+
print(f"\n --- AUTO-TS WORKFLOW (V2) COMPLETED SUCCESSFULLY --- ")
|
|
603
|
+
print(f"All results are in: {os.path.realpath(os.getcwd())}")
|
|
604
|
+
|
|
605
|
+
except Exception as e:
|
|
606
|
+
print(f"\n --- AUTO-TS WORKFLOW (V2) FAILED --- ")
|
|
607
|
+
print(f"Error: {e}")
|
|
608
|
+
traceback.print_exc()
|
|
609
|
+
finally:
|
|
610
|
+
os.chdir(original_cwd)
|
|
611
|
+
print(f"Returned to directory: {original_cwd}")
|
|
612
|
+
|
|
613
|
+
def _get_settings_for_repeat(self, wf_entry, repeat_index):
|
|
614
|
+
"""
|
|
615
|
+
Gets the correct settings dictionary for a specific repeat.
|
|
616
|
+
"""
|
|
617
|
+
step_name = wf_entry["step"]
|
|
618
|
+
repeat_settings = wf_entry.get("repeat_settings", [])
|
|
619
|
+
|
|
620
|
+
base_settings_key = wf_entry.get("settings_key", f"{step_name}_settings")
|
|
621
|
+
|
|
622
|
+
if base_settings_key not in self.config:
|
|
623
|
+
raise ValueError(f"Failed to find base settings key '{base_settings_key}' in config for {step_name}, repeat {repeat_index+1}.")
|
|
624
|
+
|
|
625
|
+
param_override = {}
|
|
626
|
+
r_setting = None
|
|
627
|
+
|
|
628
|
+
if repeat_index < len(repeat_settings):
|
|
629
|
+
r_setting = repeat_settings[repeat_index]
|
|
630
|
+
elif repeat_settings:
|
|
631
|
+
r_setting = repeat_settings[-1]
|
|
632
|
+
if repeat_index == len(repeat_settings):
|
|
633
|
+
print(f" Info: 'repeat_settings' list (len {len(repeat_settings)}) is shorter than 'repeat' for {step_name}. Re-using last entry for repeat {repeat_index+1} and beyond.")
|
|
634
|
+
|
|
635
|
+
if r_setting:
|
|
636
|
+
param_override = r_setting.get("param_override", {})
|
|
637
|
+
|
|
638
|
+
final_settings = copy.deepcopy(self.config[base_settings_key])
|
|
639
|
+
final_settings.update(param_override)
|
|
640
|
+
|
|
641
|
+
return final_settings
|
|
642
|
+
|
|
643
|
+
def run_dynamic_workflow(self):
|
|
644
|
+
"""
|
|
645
|
+
Executes the dynamic workflow defined in config['workflow'].
|
|
646
|
+
"""
|
|
647
|
+
print("\n--- 🚀 STARTING DYNAMIC WORKFLOW (V2) ---")
|
|
648
|
+
|
|
649
|
+
for entry in self.workflow_steps:
|
|
650
|
+
self.data_cache[entry["step"]] = {"runs": []}
|
|
651
|
+
|
|
652
|
+
for wf_entry in self.workflow_steps:
|
|
653
|
+
if not wf_entry.get("enabled", True):
|
|
654
|
+
print(f"\n--- SKIPPING STEP: {wf_entry['step']} (disabled) ---")
|
|
655
|
+
continue
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
step_name = wf_entry["step"]
|
|
659
|
+
method = getattr(self, f"_run_{step_name}")
|
|
660
|
+
repeat = wf_entry.get("repeat", 1)
|
|
661
|
+
|
|
662
|
+
if step_name == "step4" and self.run_step4 is not True:
|
|
663
|
+
print(f"\n--- SKIPPING STEP: {step_name} (run_step4 flag not set) ---")
|
|
664
|
+
continue
|
|
665
|
+
if step_name == "step1" and self.skip_step1 is True:
|
|
666
|
+
print(f"\n--- SKIPPING STEP: {step_name} (skip_step1 flag set) ---")
|
|
667
|
+
continue
|
|
668
|
+
|
|
669
|
+
if step_name != "step4" and self.skip_to_step4 is True:
|
|
670
|
+
print(f"\n--- SKIPPING STEP: {step_name} (skip_to_step4 flag set) ---")
|
|
671
|
+
continue
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
print(f"\n--- 🏁 EXECUTING STEP: {step_name} (Repeat={repeat}) ---")
|
|
675
|
+
|
|
676
|
+
for i in range(repeat):
|
|
677
|
+
print(f" --- {step_name} | Run {i+1}/{repeat} ---")
|
|
678
|
+
|
|
679
|
+
try:
|
|
680
|
+
settings = self._get_settings_for_repeat(wf_entry, i)
|
|
681
|
+
input_data = self._determine_input_for_run(step_name, i, wf_entry)
|
|
682
|
+
result = method(settings, input_data, run_index=i)
|
|
683
|
+
self.data_cache[step_name]["runs"].append(result)
|
|
684
|
+
print(f" --- {step_name} | Run {i+1}/{repeat} COMPLETED ---")
|
|
685
|
+
|
|
686
|
+
except Exception as e:
|
|
687
|
+
print(f" --- ❌ {step_name} | Run {i+1}/{repeat} FAILED ---")
|
|
688
|
+
print(f" Error: {e}")
|
|
689
|
+
traceback.print_exc()
|
|
690
|
+
print(f" Aborting remaining repeats for {step_name}.")
|
|
691
|
+
break
|
|
692
|
+
|
|
693
|
+
if "runs" in self.data_cache[step_name] and self.data_cache[step_name]["runs"]:
|
|
694
|
+
self._run_post_processing(step_name, wf_entry)
|
|
695
|
+
|
|
696
|
+
print(f"--- ✅ STEP: {step_name} COMPLETE ---")
|
|
697
|
+
|
|
698
|
+
def _determine_input_for_run(self, step_name, run_index, wf_entry):
|
|
699
|
+
"""
|
|
700
|
+
Determines the input for a specific run based on data dependency logic.
|
|
701
|
+
(FIXED: Now uses relative paths and explicit copy for step2 sequential)
|
|
702
|
+
"""
|
|
703
|
+
previous_runs_this_step = self.data_cache[step_name]["runs"]
|
|
704
|
+
|
|
705
|
+
if step_name == "step1":
|
|
706
|
+
if run_index == 0:
|
|
707
|
+
print(f" Info: '{step_name}' run 1 using initial_mol_file.")
|
|
708
|
+
return {"input_file": self.initial_mol_file}
|
|
709
|
+
else:
|
|
710
|
+
if not previous_runs_this_step:
|
|
711
|
+
raise RuntimeError(f"Step 1, run {run_index+1}: Cannot start, previous run (0) failed or produced no output.")
|
|
712
|
+
prev_result = previous_runs_this_step[-1]
|
|
713
|
+
if "final_struct_file" not in prev_result:
|
|
714
|
+
raise RuntimeError(f"Step 1, run {run_index+1}: Previous run did not produce a 'final_struct_file'.")
|
|
715
|
+
print(f" Using previous run's output: {prev_result['final_struct_file']}")
|
|
716
|
+
# This path is already relative (fixed in _run_step1)
|
|
717
|
+
return {"input_file": prev_result['final_struct_file']}
|
|
718
|
+
|
|
719
|
+
elif step_name == "step2":
|
|
720
|
+
mode = wf_entry.get("mode", "sequential")
|
|
721
|
+
|
|
722
|
+
if "step1" not in self.data_cache or "combined_path" not in self.data_cache["step1"]:
|
|
723
|
+
if run_index == 0 or mode == "independent":
|
|
724
|
+
raise RuntimeError(f"Step 2 ({mode}): data_cache[\"step1\"][\"combined_path\"] not found. Did Step 1 run and post-process?")
|
|
725
|
+
|
|
726
|
+
if mode == "sequential":
|
|
727
|
+
if run_index == 0:
|
|
728
|
+
# --- FIX: Explicit copy for Run 1 ---
|
|
729
|
+
source_path = self.data_cache["step1"]["combined_path"]
|
|
730
|
+
new_input_path = f"{self.input_base_name}_step2_run1_init_path.xyz"
|
|
731
|
+
shutil.copy(source_path, new_input_path)
|
|
732
|
+
print(f" Copied '{source_path}' to '{new_input_path}' for Run 1.")
|
|
733
|
+
return {"input_files": [new_input_path]}
|
|
734
|
+
else:
|
|
735
|
+
# --- FIX: Explicit copy for Run 2 and beyond ---
|
|
736
|
+
if not previous_runs_this_step:
|
|
737
|
+
raise RuntimeError(f"Step 2 (sequential), run {run_index+1}: Cannot start, previous run (0) failed.")
|
|
738
|
+
prev_result = previous_runs_this_step[-1]
|
|
739
|
+
|
|
740
|
+
if "final_relaxed_path" not in prev_result or not prev_result["final_relaxed_path"]:
|
|
741
|
+
raise RuntimeError(f"Step 2 (sequential), run {run_index+1}: Previous run produced no 'final_relaxed_path' to refine.")
|
|
742
|
+
|
|
743
|
+
source_path = prev_result["final_relaxed_path"]
|
|
744
|
+
new_input_path = f"{self.input_base_name}_step2_run{run_index+1}_init_path.xyz"
|
|
745
|
+
shutil.copy(source_path, new_input_path)
|
|
746
|
+
print(f" Copied '{source_path}' to '{new_input_path}' for Run {run_index+1}.")
|
|
747
|
+
return {"input_files": [new_input_path]}
|
|
748
|
+
|
|
749
|
+
elif mode == "independent":
|
|
750
|
+
# Independent mode still uses the combined path directly
|
|
751
|
+
return {"input_files": [self.data_cache["step1"]["combined_path"]]}
|
|
752
|
+
else:
|
|
753
|
+
raise ValueError(f"Step 2: Unknown mode '{mode}'. Use 'sequential' or 'independent'.")
|
|
754
|
+
|
|
755
|
+
elif step_name == "step3":
|
|
756
|
+
if "step2" not in self.data_cache or "candidates" not in self.data_cache["step2"]:
|
|
757
|
+
raise RuntimeError("Step 3: data_cache[\"step2\"][\"candidates\"] not found. Did Step 2 run and post-process?")
|
|
758
|
+
return {"input_files": self.data_cache["step2"]["candidates"]}
|
|
759
|
+
|
|
760
|
+
elif step_name == "step4":
|
|
761
|
+
if self.skip_to_step4:
|
|
762
|
+
return {"input_files": [self.initial_mol_file]}
|
|
763
|
+
|
|
764
|
+
if "step3" not in self.data_cache or "ts_final" not in self.data_cache["step3"]:
|
|
765
|
+
raise RuntimeError("Step 4: data_cache[\"step3\"][\"ts_final\"] not found. Did Step 3 run and post-process?")
|
|
766
|
+
|
|
767
|
+
return {"input_files": self.data_cache["step3"]["ts_final"]}
|
|
768
|
+
else:
|
|
769
|
+
print(f"Warning: Input determination logic not implemented for {step_name}. Using 'initial_mol_file'.")
|
|
770
|
+
return {"input_file": self.initial_mol_file}
|
|
771
|
+
|
|
772
|
+
def _run_post_processing(self, step_name, wf_entry):
|
|
773
|
+
"""
|
|
774
|
+
Runs the post-processing logic (merge, select, consolidate).
|
|
775
|
+
"""
|
|
776
|
+
print(f" Post-processing results for {step_name}...")
|
|
777
|
+
runs_list = self.data_cache[step_name]["runs"]
|
|
778
|
+
mode = wf_entry.get("mode", "sequential")
|
|
779
|
+
if not runs_list:
|
|
780
|
+
print(f" No successful runs found for {step_name}. Skipping post-processing.")
|
|
781
|
+
return
|
|
782
|
+
|
|
783
|
+
if step_name == "step1":
|
|
784
|
+
traj_files = [run["traj_file"] for run in runs_list if "traj_file" in run]
|
|
785
|
+
if not traj_files:
|
|
786
|
+
print(" Step 1: No traj_files found in any run. Cannot create combined_path.")
|
|
787
|
+
return
|
|
788
|
+
combined_path = self.merge_paths(traj_files)
|
|
789
|
+
self.data_cache[step_name]["combined_path"] = combined_path
|
|
790
|
+
print(f" Step 1: Created combined path: {combined_path}")
|
|
791
|
+
|
|
792
|
+
elif step_name == "step2":
|
|
793
|
+
if mode == "sequential":
|
|
794
|
+
print(" Step 2 (Sequential mode): Using candidates from the *last* run only for Step 3.")
|
|
795
|
+
if not runs_list:
|
|
796
|
+
print(" Step 2 (Sequential mode): No runs found, candidates list is empty.")
|
|
797
|
+
all_candidates_flat = []
|
|
798
|
+
energy_csvs = []
|
|
799
|
+
else:
|
|
800
|
+
last_run_result = runs_list[-1]
|
|
801
|
+
all_candidates_flat = last_run_result.get("candidates", [])
|
|
802
|
+
energy_csvs = [last_run_result["energy_csv_path"]] if last_run_result.get("energy_csv_path") else []
|
|
803
|
+
|
|
804
|
+
else:# independent
|
|
805
|
+
all_candidates_flat = [
|
|
806
|
+
path for run in runs_list for path in run.get("candidates", [])
|
|
807
|
+
]
|
|
808
|
+
energy_csvs = [
|
|
809
|
+
run["energy_csv_path"] for run in runs_list if run.get("energy_csv_path")
|
|
810
|
+
]
|
|
811
|
+
|
|
812
|
+
if not all_candidates_flat:
|
|
813
|
+
print(" Step 2: No candidates found in any run. Final list is empty.")
|
|
814
|
+
self.data_cache[step_name]["candidates"] = []
|
|
815
|
+
return
|
|
816
|
+
|
|
817
|
+
top_n = self.config.get("top_n_candidates", 3)
|
|
818
|
+
final_candidates = self.select_candidates(all_candidates_flat, energy_csvs, top_n)
|
|
819
|
+
|
|
820
|
+
refinement_dir = f"{self.input_base_name}_step3_TS_Opt_Inputs"
|
|
821
|
+
os.makedirs(refinement_dir, exist_ok=True)
|
|
822
|
+
copied_candidates = []
|
|
823
|
+
for i, source_path in enumerate(final_candidates):
|
|
824
|
+
local_guess_name = f"{self.input_base_name}_ts_guess_{i+1}.xyz"
|
|
825
|
+
local_path = os.path.join(refinement_dir, local_guess_name)
|
|
826
|
+
shutil.copy(source_path, local_path)
|
|
827
|
+
copied_candidates.append(local_path)
|
|
828
|
+
|
|
829
|
+
self.data_cache[step_name]["candidates"] = copied_candidates
|
|
830
|
+
print(f" Step 2: Selected top {len(copied_candidates)} candidates -> {refinement_dir}")
|
|
831
|
+
|
|
832
|
+
elif step_name == "step3":
|
|
833
|
+
final_ts_list = self.consolidate_ts(runs_list)
|
|
834
|
+
self.ts_final_files = final_ts_list
|
|
835
|
+
self.data_cache[step_name]["ts_final"] = final_ts_list
|
|
836
|
+
print(f" Step 3: Consolidated results into {len(final_ts_list)} final TS files.")
|
|
837
|
+
|
|
838
|
+
elif step_name == "step4":
|
|
839
|
+
print(f" Step 4: Post-processing complete (results saved by each run).")
|
|
840
|
+
|
|
841
|
+
# --- 3. Helper Functions (Consolidation Logic) ---
|
|
842
|
+
|
|
843
|
+
def merge_paths(self, traj_files):
|
|
844
|
+
"""
|
|
845
|
+
Merges results from Step 1 (sequential) runs.
|
|
846
|
+
|
|
847
|
+
MODIFIED: This now concatenates all trajectory files from the
|
|
848
|
+
sequential runs (run1_traj.xyz + run2_traj.xyz + ...) into a
|
|
849
|
+
single combined trajectory file.
|
|
850
|
+
"""
|
|
851
|
+
if not traj_files:
|
|
852
|
+
raise ValueError("merge_paths called with no trajectory files.")
|
|
853
|
+
|
|
854
|
+
merged_path = f"{self.input_base_name}_step1_combined_traj.xyz"
|
|
855
|
+
|
|
856
|
+
print(f" (merge_paths): Concatenating {len(traj_files)} trajectory files into '{merged_path}'...")
|
|
857
|
+
|
|
858
|
+
try:
|
|
859
|
+
with open(merged_path, 'wb') as outfile:
|
|
860
|
+
for i, traj_file in enumerate(traj_files):
|
|
861
|
+
if not os.path.exists(traj_file):
|
|
862
|
+
print(f" Warning: Trajectory file not found, skipping: {traj_file}")
|
|
863
|
+
continue
|
|
864
|
+
|
|
865
|
+
print(f" -> Appending file {i+1}/{len(traj_files)}: {traj_file}")
|
|
866
|
+
with open(traj_file, 'rb') as infile:
|
|
867
|
+
shutil.copyfileobj(infile, outfile)
|
|
868
|
+
|
|
869
|
+
except IOError as e:
|
|
870
|
+
print(f" Error during trajectory concatenation: {e}")
|
|
871
|
+
raise RuntimeError(f"Failed to merge trajectory files into {merged_path}")
|
|
872
|
+
|
|
873
|
+
print(f" (merge_paths): Concatenation complete.")
|
|
874
|
+
# --- FIX: Return relative path ---
|
|
875
|
+
return merged_path
|
|
876
|
+
|
|
877
|
+
def select_candidates(self, all_candidates_flat, energy_csvs, top_n):
|
|
878
|
+
"""
|
|
879
|
+
Selects the 'top_n' best candidates from all Step 2 runs.
|
|
880
|
+
"""
|
|
881
|
+
print(f" (select_candidates): Filtering {len(all_candidates_flat)} total candidates down to {top_n}.")
|
|
882
|
+
|
|
883
|
+
if not energy_csvs:
|
|
884
|
+
print(f" Warning: No energy_plot.csv files found. Returning first {top_n} candidates (unsorted).")
|
|
885
|
+
return all_candidates_flat[:top_n]
|
|
886
|
+
|
|
887
|
+
best_csv_path = None
|
|
888
|
+
max_energies = -1
|
|
889
|
+
all_energies = []
|
|
890
|
+
|
|
891
|
+
for csv_path in energy_csvs:
|
|
892
|
+
try:
|
|
893
|
+
with open(csv_path, 'r') as f:
|
|
894
|
+
lines = f.readlines()
|
|
895
|
+
if not lines: continue
|
|
896
|
+
last_line = lines[-1].strip()
|
|
897
|
+
energies = np.array([float(e) for e in last_line.split(',') if e.strip()])
|
|
898
|
+
if len(energies) > max_energies:
|
|
899
|
+
max_energies = len(energies)
|
|
900
|
+
all_energies = energies
|
|
901
|
+
best_csv_path = csv_path
|
|
902
|
+
except Exception:
|
|
903
|
+
continue
|
|
904
|
+
|
|
905
|
+
if best_csv_path is None:
|
|
906
|
+
print(" Warning: Failed to parse any energy_plot.csv file. Returning unsorted candidates.")
|
|
907
|
+
return all_candidates_flat[:top_n]
|
|
908
|
+
|
|
909
|
+
print(f" Using energy reference: {best_csv_path} (found {max_energies} energies)")
|
|
910
|
+
|
|
911
|
+
candidates_with_energy = []
|
|
912
|
+
for path in all_candidates_flat:
|
|
913
|
+
try:
|
|
914
|
+
base_name = os.path.splitext(os.path.basename(path))[0]
|
|
915
|
+
index_str = base_name.split('_')[-1]
|
|
916
|
+
z = int(index_str) # 1-based
|
|
917
|
+
z_idx = z - 1 # 0-based index
|
|
918
|
+
|
|
919
|
+
if z_idx < 0 or z_idx >= len(all_energies):
|
|
920
|
+
print(f" Warning: Index {z} from '{path}' out of bounds for energy list (len {len(all_energies)}). Skipping.")
|
|
921
|
+
continue
|
|
922
|
+
candidates_with_energy.append((all_energies[z_idx], path))
|
|
923
|
+
except Exception as e:
|
|
924
|
+
print(f" Warning: Could not parse index from '{path}': {e}. Skipping.")
|
|
925
|
+
|
|
926
|
+
candidates_with_energy.sort(key=lambda x: x[0], reverse=True)
|
|
927
|
+
top_n_list = candidates_with_energy[:top_n]
|
|
928
|
+
|
|
929
|
+
selected_paths = [path for energy, path in top_n_list]
|
|
930
|
+
for energy, path in top_n_list:
|
|
931
|
+
print(f" - Selected: {os.path.basename(path)} (Energy: {energy:.6f} Hartree)")
|
|
932
|
+
|
|
933
|
+
return selected_paths
|
|
934
|
+
|
|
935
|
+
def consolidate_ts(self, runs_list):
|
|
936
|
+
"""
|
|
937
|
+
Consolidates results from multiple Step 3 runs.
|
|
938
|
+
Adopts the results from the *last* run.
|
|
939
|
+
"""
|
|
940
|
+
if not runs_list:
|
|
941
|
+
return []
|
|
942
|
+
|
|
943
|
+
last_run_results = runs_list[-1]
|
|
944
|
+
final_files = last_run_results.get("optimized_ts_files", [])
|
|
945
|
+
|
|
946
|
+
print(f" (consolidate_ts): Adopting {len(final_files)} TS files from the *last* Step 3 run.")
|
|
947
|
+
|
|
948
|
+
return final_files
|
|
949
|
+
|
|
950
|
+
# --- 4. V2 Adapter Methods for _run_stepX ---
|
|
951
|
+
|
|
952
|
+
def _run_step1(self, settings, input_data, run_index=0):
|
|
953
|
+
"""
|
|
954
|
+
Runs a *single* Step 1 (AFIR) scan.
|
|
955
|
+
(FIXED: Sets WORK_DIR in settings before set_options)
|
|
956
|
+
"""
|
|
957
|
+
input_file = input_data["input_file"]
|
|
958
|
+
print(f" Running Step 1 (AFIR) on: {input_file}")
|
|
959
|
+
|
|
960
|
+
if "manual_AFIR" not in settings:
|
|
961
|
+
raise ValueError(f"Step 1 settings (run {run_index+1}) must contain 'manual_AFIR'.")
|
|
962
|
+
|
|
963
|
+
job = OptimizationJob(input_file=input_file)
|
|
964
|
+
|
|
965
|
+
# **FIX**: Modify settings dict to create a unique WORK_DIR
|
|
966
|
+
base_work_dir = settings.get("WORK_DIR", ".")
|
|
967
|
+
settings["WORK_DIR"] = os.path.join(base_work_dir, f"step1_run_{run_index+1}")
|
|
968
|
+
|
|
969
|
+
job.set_options(**settings)
|
|
970
|
+
job.run()
|
|
971
|
+
|
|
972
|
+
optimizer_instance = job.get_results()
|
|
973
|
+
if optimizer_instance is None:
|
|
974
|
+
raise RuntimeError(f"Step 1 (run {run_index+1}) failed to produce an optimizer instance.")
|
|
975
|
+
|
|
976
|
+
optimizer_instance.get_result_file_path()
|
|
977
|
+
source_traj_path = optimizer_instance.traj_file
|
|
978
|
+
source_final_struct = optimizer_instance.optimized_struct_file
|
|
979
|
+
|
|
980
|
+
if not source_traj_path or not os.path.exists(source_traj_path):
|
|
981
|
+
raise FileNotFoundError(f"Step 1 (run {run_index+1}) 'traj_file' not found at '{source_traj_path}'")
|
|
982
|
+
if not source_final_struct or not os.path.exists(source_final_struct):
|
|
983
|
+
raise FileNotFoundError(f"Step 1 (run {run_index+1}) 'optimized_struct_file' not found at '{source_final_struct}'")
|
|
984
|
+
|
|
985
|
+
local_traj_name = f"{self.input_base_name}_step1_run{run_index+1}_traj.xyz"
|
|
986
|
+
shutil.copy(source_traj_path, local_traj_name)
|
|
987
|
+
local_final_name = f"{self.input_base_name}_step1_run{run_index+1}_final.xyz"
|
|
988
|
+
shutil.copy(source_final_struct, local_final_name)
|
|
989
|
+
|
|
990
|
+
print(f" Step 1 (run {run_index+1}) results saved.")
|
|
991
|
+
# --- FIX: Return relative paths ---
|
|
992
|
+
return {
|
|
993
|
+
"traj_file": local_traj_name,
|
|
994
|
+
"final_struct_file": local_final_name
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
def _run_step2(self, settings, input_data, run_index=0):
|
|
998
|
+
"""
|
|
999
|
+
Runs a *single* Step 2 (NEB) optimization.
|
|
1000
|
+
(FIXED: Sets WORK_DIR in settings)
|
|
1001
|
+
(FIXED: Returns 'last_itr_traj_file_path' as 'final_relaxed_path')
|
|
1002
|
+
"""
|
|
1003
|
+
input_files = input_data["input_files"]
|
|
1004
|
+
if not input_files:
|
|
1005
|
+
raise ValueError(f"Step 2 (run {run_index+1}) received no input files.")
|
|
1006
|
+
print(f" Running Step 2 (NEB) on {len(input_files)} input file(s).")
|
|
1007
|
+
|
|
1008
|
+
job = NEBJob(input_files=input_files)
|
|
1009
|
+
|
|
1010
|
+
# **FIX**: Modify settings dict to create a unique WORK_DIR
|
|
1011
|
+
base_work_dir = settings.get("WORK_DIR", ".")
|
|
1012
|
+
settings["WORK_DIR"] = os.path.join(base_work_dir, f"step2_run_{run_index+1}")
|
|
1013
|
+
|
|
1014
|
+
job.set_options(**settings)
|
|
1015
|
+
job.run()
|
|
1016
|
+
|
|
1017
|
+
neb_instance = job.get_results()
|
|
1018
|
+
if neb_instance is None:
|
|
1019
|
+
raise RuntimeError(f"Step 2 (run {run_index+1}) failed to produce an NEB instance.")
|
|
1020
|
+
|
|
1021
|
+
neb_instance.get_result_file()
|
|
1022
|
+
source_ts_paths = neb_instance.ts_guess_file_list
|
|
1023
|
+
energy_csv_path = os.path.join(neb_instance.config.NEB_FOLDER_DIRECTORY, "energy_plot.csv")
|
|
1024
|
+
|
|
1025
|
+
# **FIX**: Get the final relaxed path based on user's attribute name
|
|
1026
|
+
final_relaxed_path = getattr(neb_instance, 'last_itr_traj_file_path', None)
|
|
1027
|
+
local_final_path = None # Initialize
|
|
1028
|
+
|
|
1029
|
+
if not final_relaxed_path or not os.path.exists(final_relaxed_path):
|
|
1030
|
+
print(f" Warning: 'last_itr_traj_file_path' not found or invalid: '{final_relaxed_path}'. Sequential refinement may fail.")
|
|
1031
|
+
final_relaxed_path = None
|
|
1032
|
+
else:
|
|
1033
|
+
local_final_path_name = f"{self.input_base_name}_step2_run{run_index+1}_final_path.xyz"
|
|
1034
|
+
shutil.copy(final_relaxed_path, local_final_path_name)
|
|
1035
|
+
# --- FIX: Store relative path ---
|
|
1036
|
+
local_final_path = local_final_path_name
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
if not source_ts_paths:
|
|
1040
|
+
print(f" Step 2 (run {run_index+1}) did not find any TS candidates.")
|
|
1041
|
+
return {
|
|
1042
|
+
"candidates": [],
|
|
1043
|
+
"energy_csv_path": None,
|
|
1044
|
+
"final_relaxed_path": local_final_path
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
candidate_dir = f"{self.input_base_name}_step2_run{run_index+1}_candidates"
|
|
1048
|
+
os.makedirs(candidate_dir, exist_ok=True)
|
|
1049
|
+
local_ts_paths = []
|
|
1050
|
+
|
|
1051
|
+
for i, source_path in enumerate(source_ts_paths):
|
|
1052
|
+
if not os.path.exists(source_path):
|
|
1053
|
+
print(f" Warning: Source file not found, skipping: {source_path}")
|
|
1054
|
+
continue
|
|
1055
|
+
local_guess_name = f"{self.input_base_name}_s2_run{run_index+1}_guess_{i+1}.xyz"
|
|
1056
|
+
local_path = os.path.join(candidate_dir, local_guess_name)
|
|
1057
|
+
shutil.copy(source_path, local_path)
|
|
1058
|
+
# --- FIX: Store relative path ---
|
|
1059
|
+
local_ts_paths.append(local_path)
|
|
1060
|
+
|
|
1061
|
+
print(f" Step 2 (run {run_index+1}) found {len(local_ts_paths)} candidates.")
|
|
1062
|
+
return {
|
|
1063
|
+
"candidates": local_ts_paths,
|
|
1064
|
+
# --- FIX: Store relative path ---
|
|
1065
|
+
"energy_csv_path": energy_csv_path if os.path.exists(energy_csv_path) else None,
|
|
1066
|
+
"final_relaxed_path": local_final_path
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
def _run_step3(self, settings, input_data, run_index=0):
|
|
1070
|
+
"""
|
|
1071
|
+
Runs a *single* Step 3 (TS Refinement) pass.
|
|
1072
|
+
(FIXED: Sets WORK_DIR in settings)
|
|
1073
|
+
"""
|
|
1074
|
+
candidate_files = input_data["input_files"]
|
|
1075
|
+
print(f" Running Step 3 (TS Refine) on {len(candidate_files)} candidates (Run {run_index+1}).")
|
|
1076
|
+
|
|
1077
|
+
if not candidate_files:
|
|
1078
|
+
print(" No candidates provided. Skipping refinement.")
|
|
1079
|
+
return {"optimized_ts_files": [], "energies": {}}
|
|
1080
|
+
|
|
1081
|
+
final_ts_files = []
|
|
1082
|
+
final_ts_energies = {}
|
|
1083
|
+
settings['saddle_order'] = 1
|
|
1084
|
+
base_work_dir = settings.get("WORK_DIR", ".") # Get base dir once
|
|
1085
|
+
|
|
1086
|
+
for i, guess_file_path in enumerate(candidate_files):
|
|
1087
|
+
print(f" Refining candidate {i+1}/{len(candidate_files)} ({os.path.basename(guess_file_path)})")
|
|
1088
|
+
|
|
1089
|
+
job = OptimizationJob(input_file=guess_file_path)
|
|
1090
|
+
|
|
1091
|
+
# **FIX**: Modify settings dict for this specific candidate
|
|
1092
|
+
current_settings = copy.deepcopy(settings)
|
|
1093
|
+
cand_basename = os.path.splitext(os.path.basename(guess_file_path))[0]
|
|
1094
|
+
current_settings["WORK_DIR"] = os.path.join(base_work_dir, f"step3_run_{run_index+1}", cand_basename)
|
|
1095
|
+
|
|
1096
|
+
job.set_options(**current_settings)
|
|
1097
|
+
job.run()
|
|
1098
|
+
|
|
1099
|
+
optimizer_instance = job.get_results()
|
|
1100
|
+
if optimizer_instance is None:
|
|
1101
|
+
print(f" Warning: Refinement for {guess_file_path} failed.")
|
|
1102
|
+
continue
|
|
1103
|
+
|
|
1104
|
+
optimizer_instance.get_result_file_path()
|
|
1105
|
+
source_final_ts_path = optimizer_instance.optimized_struct_file
|
|
1106
|
+
if not source_final_ts_path or not os.path.exists(source_final_ts_path):
|
|
1107
|
+
print(f" Warning: Refinement for {guess_file_path} finished, but 'optimized_struct_file' was not found.")
|
|
1108
|
+
continue
|
|
1109
|
+
|
|
1110
|
+
if not optimizer_instance.optimized_flag:
|
|
1111
|
+
print(f"Warning: Refinement for {guess_file_path} did not converge (optimized_flag=False). Skipping.")
|
|
1112
|
+
continue
|
|
1113
|
+
|
|
1114
|
+
local_final_name = f"{self.input_base_name}_s3_run{run_index+1}_ts_final_{i+1}.xyz"
|
|
1115
|
+
shutil.copy(source_final_ts_path, local_final_name)
|
|
1116
|
+
|
|
1117
|
+
# --- FIX: Store relative path ---
|
|
1118
|
+
rel_path = local_final_name
|
|
1119
|
+
final_ts_files.append(rel_path)
|
|
1120
|
+
final_ts_energies[rel_path] = optimizer_instance.final_energy
|
|
1121
|
+
|
|
1122
|
+
print(f" Step 3 (run {run_index+1}) successfully refined {len(final_ts_files)} structures.")
|
|
1123
|
+
return {
|
|
1124
|
+
"optimized_ts_files": final_ts_files,
|
|
1125
|
+
"energies": final_ts_energies
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
def _run_step4(self, settings, input_data, run_index=0):
|
|
1129
|
+
"""
|
|
1130
|
+
Runs a *single* Step 4 (IRC + Opt) pass.
|
|
1131
|
+
(FIXED: Sets WORK_DIR in settings)
|
|
1132
|
+
"""
|
|
1133
|
+
ts_final_files = input_data["input_files"]
|
|
1134
|
+
print(f" Running Step 4 (IRC) on {len(ts_final_files)} TS files (Run {run_index+1}).")
|
|
1135
|
+
|
|
1136
|
+
if not ts_final_files:
|
|
1137
|
+
print(" No TS files provided. Skipping Step 4.")
|
|
1138
|
+
return {"profile_dirs": []}
|
|
1139
|
+
|
|
1140
|
+
if "intrinsic_reaction_coordinates" not in settings:
|
|
1141
|
+
raise ValueError(f"Step 4 (run {run_index+1}) requires 'intrinsic_reaction_coordinates' settings.")
|
|
1142
|
+
|
|
1143
|
+
profile_dirs = []
|
|
1144
|
+
base_work_dir = settings.get("WORK_DIR", ".")
|
|
1145
|
+
|
|
1146
|
+
for i, ts_path in enumerate(ts_final_files):
|
|
1147
|
+
ts_name_base = f"{self.input_base_name}_s4_run{run_index+1}_TS_{i+1}"
|
|
1148
|
+
print(f" Running Step 4 for TS {i+1}/{len(ts_final_files)} ({ts_path})")
|
|
1149
|
+
|
|
1150
|
+
# --- 4A: Run IRC ---
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
relative_ts_path = os.path.relpath(ts_path)
|
|
1154
|
+
job_irc = OptimizationJob(input_file=relative_ts_path)
|
|
1155
|
+
|
|
1156
|
+
irc_settings = copy.deepcopy(settings)
|
|
1157
|
+
irc_settings["saddle_order"] = 1
|
|
1158
|
+
# **FIX**: Set WORK_DIR in settings
|
|
1159
|
+
irc_settings["WORK_DIR"] = os.path.join(base_work_dir, f"step4_run_{run_index+1}", f"ts_{i+1}_irc")
|
|
1160
|
+
job_irc.set_options(**irc_settings)
|
|
1161
|
+
job_irc.run()
|
|
1162
|
+
|
|
1163
|
+
irc_instance = job_irc.get_results()
|
|
1164
|
+
if irc_instance is None:
|
|
1165
|
+
print(f" Warning: IRC job for {ts_path} failed.")
|
|
1166
|
+
continue
|
|
1167
|
+
|
|
1168
|
+
ts_e = irc_instance.final_energy
|
|
1169
|
+
ts_bias_e = irc_instance.final_bias_energy
|
|
1170
|
+
endpoint_paths = irc_instance.irc_terminal_struct_paths
|
|
1171
|
+
|
|
1172
|
+
if not endpoint_paths or len(endpoint_paths) != 2:
|
|
1173
|
+
print(f" Warning: IRC job for {ts_path} did not return 2 endpoint files.")
|
|
1174
|
+
continue
|
|
1175
|
+
|
|
1176
|
+
# --- 4B: Run Endpoint Optimization ---
|
|
1177
|
+
endpoint_results = []
|
|
1178
|
+
for j, end_path in enumerate(endpoint_paths):
|
|
1179
|
+
opt_settings = copy.deepcopy(settings)
|
|
1180
|
+
opt_settings["opt_method"] = opt_settings.get("opt_method", ["rsirfo_block_fsb"])
|
|
1181
|
+
opt_settings.pop("intrinsic_reaction_coordinates", None)
|
|
1182
|
+
opt_settings['saddle_order'] = 0 # Minimization
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
relative_end_path = os.path.relpath(end_path)
|
|
1186
|
+
base_end_path = os.path.basename(relative_end_path)
|
|
1187
|
+
shutil.copy(relative_end_path, base_end_path)
|
|
1188
|
+
job_opt = OptimizationJob(input_file=base_end_path)
|
|
1189
|
+
|
|
1190
|
+
# **FIX**: Set WORK_DIR in settings
|
|
1191
|
+
opt_settings["WORK_DIR"] = os.path.join(base_work_dir, f"step4_run_{run_index+1}", f"ts_{i+1}_endpt_{j+1}")
|
|
1192
|
+
job_opt.set_options(**opt_settings)
|
|
1193
|
+
job_opt.run()
|
|
1194
|
+
|
|
1195
|
+
opt_instance = job_opt.get_results()
|
|
1196
|
+
if opt_instance is None: continue
|
|
1197
|
+
|
|
1198
|
+
opt_instance.get_result_file_path()
|
|
1199
|
+
final_opt_path = opt_instance.optimized_struct_file
|
|
1200
|
+
if not final_opt_path or not os.path.exists(final_opt_path): continue
|
|
1201
|
+
|
|
1202
|
+
endpoint_results.append({
|
|
1203
|
+
"path": final_opt_path,
|
|
1204
|
+
"e": opt_instance.final_energy,
|
|
1205
|
+
"bias_e": opt_instance.final_bias_energy,
|
|
1206
|
+
})
|
|
1207
|
+
|
|
1208
|
+
if not endpoint_results:
|
|
1209
|
+
print(f" Warning: Failed to optimize any endpoints for {ts_path}.")
|
|
1210
|
+
continue
|
|
1211
|
+
|
|
1212
|
+
# --- 4C: Collect Results & Visualize (using parent methods) ---
|
|
1213
|
+
result_dir = f"{ts_name_base}_Step4_Profile"
|
|
1214
|
+
os.makedirs(result_dir, exist_ok=True)
|
|
1215
|
+
|
|
1216
|
+
e_profile = {"TS": {"e": ts_e, "bias_e": ts_bias_e, "path": ts_path}}
|
|
1217
|
+
if len(endpoint_results) >= 1: e_profile["End1"] = endpoint_results[0]
|
|
1218
|
+
if len(endpoint_results) >= 2: e_profile["End2"] = endpoint_results[1]
|
|
1219
|
+
|
|
1220
|
+
plot_path = os.path.join(result_dir, "energy_profile.png")
|
|
1221
|
+
self._create_energy_profile_plot(e_profile, plot_path, ts_name_base)
|
|
1222
|
+
|
|
1223
|
+
text_path = os.path.join(result_dir, "energy_profile.txt")
|
|
1224
|
+
self._write_energy_profile_text(e_profile, text_path, ts_name_base)
|
|
1225
|
+
|
|
1226
|
+
|
|
1227
|
+
shutil.copy(os.path.relpath(ts_path), os.path.join(result_dir, f"{ts_name_base}_ts_final.xyz"))
|
|
1228
|
+
if "End1" in e_profile:
|
|
1229
|
+
shutil.copy(os.path.relpath(e_profile["End1"]["path"]), os.path.join(result_dir, "endpoint_1_opt.xyz"))
|
|
1230
|
+
if "End2" in e_profile:
|
|
1231
|
+
shutil.copy(os.path.relpath(e_profile["End2"]["path"]), os.path.join(result_dir, "endpoint_2_opt.xyz"))
|
|
1232
|
+
|
|
1233
|
+
print(f" Successfully saved profile to: {result_dir}")
|
|
1234
|
+
# --- FIX: Store relative path ---
|
|
1235
|
+
profile_dirs.append(result_dir)
|
|
1236
|
+
|
|
1237
|
+
return {"profile_dirs": profile_dirs}
|
|
1238
|
+
|
|
1239
|
+
|