Skip to content

Commit 7d706b3

Browse files
committed
Clear the TS energy cache and checkfile when switching to the next TS guess
`switch_ts()` discards a TS guess and re-optimizes the next one, and it already invalidates every piece of state the new geometry makes meaningless: the chosen guess, the running jobs, the output paths and job_types (via `delete_all_species_jobs()`), the copied `freq.out`, the TS checks dict, and the rotors dict. It did not invalidate the energy data cached on the TS species itself, nor the checkfile pointing at the abandoned guess's wavefunction. All three energy fields are read straight off the species object, which is exactly where `delete_all_species_jobs()` cannot reach: * `e0` decides the E0 check. `compute_rxn_e0()` skips any species that already carries an `e0`, and `ARCReaction.copy_e0_values()` only fills an empty one, so the TS E0 was computed once, for the first guess, and every later guess was judged against it. `e0` is also written to the restart file, so the stale value survived a restart. Measured on a benchmark run, two guesses whose own E0 was 2.51 kJ/mol above the reactants were rejected on the first guess's 121.88 kJ/mol. * `e_elect` decides a second gate by the same mechanism, and this is the strongest reason to clear it. `check_rxn_e_elect()` — the check that runs when E0 cannot be determined — reads `reaction.ts_species.e_elect` directly off the species object, so a retained value silently answered that gate for the new guess too. * `freqs` is the primary source of the reported imaginary frequency. `arc/output.py::_get_ts_imag_freq()` takes the most negative entry of `spc.freqs` and only falls back to the chosen guess's `imaginary_freqs` when `spc.freqs` is empty, so without this clear the run reports the abandoned guess's imaginary frequency for the accepted TS. Caching is correct for the reactants and the products, whose geometries do not change here; it is wrong for the TS, whose geometry changes on every switch. Until the new sp and freq jobs replace them, `e_elect` and `freqs` are also saved to the restart file and reported in output.yml as if they belonged to the new guess. `checkfile` is the same defect class, reached through the route builder rather than through a check. `run_job()` reads the checkfile off the species and hands it to every job it spawns, and `GaussianAdapter` resolves the SCF initial guess for any polyatomic species as ` guess=read` when the checkfile exists and ` guess=mix` when it does not — the fallback is not TS-specific. Left uncleared, the new guess is seeded from the discarded geometry's converged orbitals instead of the symmetry-broken default that a fresh species gets. That branch is right when the checkfile is current; what is wrong is the stale pointer. Deliberately left alone: * `opt_level` — a level of theory, not a geometry-derived quantity. * `external_symmetry` and `optical_isomers` — also sticky, but only ever written onto the scheduler's species by the final Arkane rate/thermo run, after all switching is over. Excluding them is safe only because `compute_rxn_e0()` operates on `reaction.copy()`, which round-trips through `as_dict`/`from_dict` — a genuine deep copy — and merges only `e0` back onto the live reaction; were `copy()` ever to become shallow, this exclusion would become a bug of the same shape as the ones fixed here. * `t1` — a wavefunction diagnostic that no check reads and that output.yml does not carry. * `active` — considered, and deliberately not cleared. It has the identical sticky shape: set only when it is `None`, persisted to the restart file, and consumed by the Orca and Molpro CASSCF route builders. Whether an active space is guess-dependent is a chemistry question rather than a caching one, so it is left for a deliberate decision. The regression test asserts the invalidation of all four fields and, for `e0`, its consequence: that an E0 computed for the new guess is adopted by `copy_e0_values()` rather than masked by the old one.
1 parent 79540b6 commit 7d706b3

2 files changed

Lines changed: 81 additions & 0 deletions

File tree

arc/scheduler.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2989,6 +2989,10 @@ def switch_ts(self, label: str):
29892989
"""
29902990
Try the next optimized TS guess in line if a previous TS guess was found to be wrong.
29912991
2992+
The energy data cached on the TS species (``e0``, ``e_elect`` and ``freqs``) and the
2993+
``checkfile`` are discarded: all four belong to the abandoned guess's geometry and
2994+
wavefunction, and are repopulated by the jobs run for the new guess.
2995+
29922996
Args:
29932997
label (str): The TS species label.
29942998
"""
@@ -2999,6 +3003,10 @@ def switch_ts(self, label: str):
29993003
if os.path.isfile(freq_path):
30003004
os.remove(freq_path)
30013005
self.species_dict[label].populate_ts_checks() # Restart the TS checks dict.
3006+
self.species_dict[label].e0 = None
3007+
self.species_dict[label].e_elect = None
3008+
self.species_dict[label].freqs = None
3009+
self.species_dict[label].checkfile = None
30023010
if self.job_types['rotors'] and self.species_dict[label].rotors_dict is not None:
30033011
# Reset rotors so they are re-determined from the new TS geometry.
30043012
# rotors_dict=None is a sentinel meaning "skip rotor scans"; preserve it.

arc/scheduler_test.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1214,6 +1214,79 @@ def test_switch_ts_rotors_reset(self, mock_run_opt):
12141214
# rotors_dict=None must be preserved — do not re-enable rotor scans.
12151215
self.assertIsNone(sched2.species_dict[ts_label2].rotors_dict)
12161216

1217+
@patch('arc.scheduler.Scheduler.run_opt_job')
1218+
def test_switch_ts_clears_stale_ts_energy(self, mock_run_opt):
1219+
"""Test that switch_ts discards the energy data computed for the TS guess being abandoned."""
1220+
ts_xyz = str_to_xyz("""N 0.91779059 0.51946178 0.00000000
1221+
H 1.81402049 1.03819414 0.00000000
1222+
H 0.00000000 0.00000000 0.00000000
1223+
H 0.91779059 1.22790192 0.72426890""")
1224+
1225+
ts_spc = ARCSpecies(label='TS_e0', is_ts=True, xyz=ts_xyz, multiplicity=1, charge=0,
1226+
compute_thermo=False)
1227+
ts_spc.ts_guesses = [
1228+
TSGuess(index=0, method='heuristics', success=True, energy=100.0, xyz=ts_xyz,
1229+
execution_time='0:00:01'),
1230+
TSGuess(index=1, method='heuristics', success=True, energy=110.0, xyz=ts_xyz,
1231+
execution_time='0:00:01'),
1232+
]
1233+
ts_spc.ts_guesses[0].opt_xyz = ts_xyz
1234+
ts_spc.ts_guesses[0].imaginary_freqs = [-798.8]
1235+
ts_spc.ts_guesses[1].opt_xyz = ts_xyz
1236+
ts_spc.ts_guesses[1].imaginary_freqs = [-784.0]
1237+
ts_spc.chosen_ts = 0
1238+
ts_spc.chosen_ts_list = [0]
1239+
ts_spc.ts_guesses_exhausted = False
1240+
# Energy data computed for guess 0, all of it specific to that geometry.
1241+
ts_spc.e0 = 121.88
1242+
ts_spc.e_elect = -148340.0
1243+
ts_spc.freqs = [-798.8, 1042.3, 1626.7, 1642.1, 3396.4, 3512.9]
1244+
1245+
project_directory = os.path.join(ARC_PATH, 'Projects',
1246+
'arc_project_for_testing_delete_after_usage22')
1247+
self.addCleanup(shutil.rmtree, project_directory, ignore_errors=True)
1248+
checkfile_path = os.path.join(project_directory, 'calcs', 'TSs', 'TS_e0', 'opt_a0',
1249+
'check.chk')
1250+
ts_spc.checkfile = checkfile_path
1251+
sched = Scheduler(project='test_switch_ts_e0', ess_settings=self.ess_settings,
1252+
species_list=[ts_spc],
1253+
opt_level=Level(repr=default_levels_of_theory['opt']),
1254+
freq_level=Level(repr=default_levels_of_theory['freq']),
1255+
sp_level=Level(repr=default_levels_of_theory['sp']),
1256+
ts_guess_level=Level(repr=default_levels_of_theory['ts_guesses']),
1257+
project_directory=project_directory,
1258+
testing=True,
1259+
job_types=self.job_types1,
1260+
)
1261+
1262+
ts_label = 'TS_e0'
1263+
sched.output[ts_label]['job_types']['opt'] = True
1264+
sched.output[ts_label]['job_types']['freq'] = True
1265+
sched.output[ts_label]['job_types']['sp'] = True
1266+
sched.job_dict[ts_label] = {'opt': {}, 'freq': {}, 'sp': {}}
1267+
sched.running_jobs[ts_label] = []
1268+
1269+
sched.switch_ts(ts_label)
1270+
1271+
self.assertEqual(sched.species_dict[ts_label].chosen_ts, 1)
1272+
self.assertIsNone(sched.species_dict[ts_label].e0)
1273+
self.assertIsNone(sched.species_dict[ts_label].e_elect)
1274+
self.assertIsNone(sched.species_dict[ts_label].freqs)
1275+
self.assertIsNone(sched.species_dict[ts_label].checkfile)
1276+
1277+
# The consequence: an E0 computed for the new guess must be adopted, not masked by the
1278+
# value belonging to guess 0. ``compute_rxn_e0`` returns a copy of the reaction that
1279+
# ``copy_e0_values`` merges back, and it only fills an E0 that is empty.
1280+
rxn = ARCReaction(label='NH2 + H <=> NH3',
1281+
r_species=[ARCSpecies(label='NH2', smiles='[NH2]'),
1282+
ARCSpecies(label='H', smiles='[H]')],
1283+
p_species=[ARCSpecies(label='NH3', smiles='N')])
1284+
rxn.ts_species = sched.species_dict[ts_label]
1285+
rxn_copy = rxn.copy()
1286+
rxn_copy.ts_species.e0 = 245.0
1287+
rxn.copy_e0_values(rxn_copy)
1288+
self.assertEqual(rxn.ts_species.e0, 245.0)
1289+
12171290
def setup_ts_scheduler_for_freq_check(self, project, chosen_ts, chosen_ts_list=None):
12181291
"""
12191292
Set up a Scheduler with a single TS species whose TSGuess ``index`` (identity) and

0 commit comments

Comments
 (0)