Skip to content

Commit 82ee38a

Browse files
committed
Restore live conformer jobs by name on restart, not the fossil 'conformer{i}'
The contract: a running conformer job lives in self.running_jobs under the same name the live path emits, '{job_type}_{i}' (e.g. 'conf_opt_0'). Every consumer parses that format -- get_i_from_job_name strips the 'conf_opt'/ 'conf_sp' prefix, and get_completed_incore_jobs routes on it into job_dict[label]['conf_opt'][i]. How it broke: restore_running_jobs emitted the fossil 'conformer{i}' instead. get_i_from_job_name returns None for it, so get_completed_incore_jobs fell into its fallback branch, derived an empty job-type from the underscore-less name ('conformer0'.split('_')[:-1] == []), and died with KeyError: '' on the first scheduling sweep -- crashing every ARC restart that had a live conformer job. The fix emits '{job_type}_{i}' from the same expression the live path uses, rather than a second hard-coded literal that must be kept in sync by hand. The job_dict was already reconstructed correctly (conf_opt keyed by int index), so the name was the only defect. Also corrects get_i_from_job_name's docstring, which still advertised the retired 'conformer12' format. The test drives a restart payload carrying a live conf_opt job through the real restore_running_jobs + get_completed_incore_jobs path: red with KeyError: '' on the unfixed code, green after.
1 parent d285b1e commit 82ee38a

3 files changed

Lines changed: 61 additions & 2 deletions

File tree

arc/checks/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def get_i_from_job_name(job_name: str) -> int | None:
4343
Get the conformer or tsg index from the job name.
4444
4545
Args:
46-
job_name (str): The job name, e.g., 'conformer12' or 'tsg5'.
46+
job_name (str): The job name, e.g., 'conf_opt_12', 'conf_sp_3', or 'tsg5'.
4747
4848
Returns:
4949
int | None: The corresponding conformer or tsg index.

arc/scheduler.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4060,7 +4060,9 @@ def restore_running_jobs(self):
40604060
and ('tsg' not in job_description or job_description['tsg'] is None):
40614061
self.running_jobs[spc_label].append(job_description['job_name'])
40624062
elif 'conformer' in job_description:
4063-
self.running_jobs[spc_label].append(f'conformer{job_description["conformer"]}')
4063+
# Emit the same '{job_type}_{conformer}' name the live path uses (e.g. 'conf_opt_0'),
4064+
# not the fossil 'conformer{i}' that no consumer of running_jobs accepts.
4065+
self.running_jobs[spc_label].append(f'{job_description["job_type"]}_{job_description["conformer"]}')
40644066
elif 'tsg' in job_description:
40654067
self.running_jobs[spc_label].append(f'tsg{job_description["tsg"]}')
40664068
for species in self.species_list:

arc/scheduler_test.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,63 @@ def test_conformers(self):
240240
self.assertEqual(lines[11], '\n')
241241
self.assertEqual(lines[12], 'SMILES: CC\n')
242242

243+
def test_restore_running_jobs_conformer_reconnects(self):
244+
"""Restarting with a live conformer job must reconnect to it, not crash.
245+
246+
Regression for the restore-path job-name contract. During normal operation a running
247+
conformer job is stored in ``running_jobs`` as ``'{job_type}_{i}'`` (e.g. ``'conf_opt_0'``),
248+
and every consumer of ``running_jobs`` parses that format. ``restore_running_jobs`` used to
249+
emit the fossil ``'conformer{i}'`` instead, which ``get_i_from_job_name`` returns ``None``
250+
for; the first scheduling sweep after a restart (``get_completed_incore_jobs``) then fell
251+
into its fallback branch, derived an empty job-type from the underscore-less name, and died
252+
with ``KeyError: ''``. This drives a restart payload carrying a live conformer job through
253+
the real restore + sweep path and asserts the reconnection instead of the crash.
254+
"""
255+
label = 'methylamine'
256+
xyz = """C -0.57422867 -0.01669771 0.01229213
257+
N 0.82084044 0.08279104 -0.37769346
258+
H -1.05737005 -0.84067772 -0.52007494
259+
H -1.10211468 0.90879867 -0.23383011
260+
H -0.66133128 -0.19490562 1.08785111
261+
H 0.88047852 0.26966160 -1.37780789
262+
H 1.27889520 -0.81548721 -0.22940984"""
263+
spc = ARCSpecies(label=label, smiles='CN', xyz=xyz)
264+
sched = Scheduler(project='project_test_restore_conf', ess_settings=self.ess_settings,
265+
species_list=[spc], composite_method=None,
266+
conformer_opt_level=Level(repr=default_levels_of_theory['conformer']),
267+
opt_level=Level(repr=default_levels_of_theory['opt']),
268+
freq_level=Level(repr=default_levels_of_theory['freq']),
269+
sp_level=Level(repr=default_levels_of_theory['sp']),
270+
scan_level=Level(repr=default_levels_of_theory['scan']),
271+
ts_guess_level=Level(repr=default_levels_of_theory['ts_guesses']),
272+
project_directory=self.project_directory, testing=True,
273+
job_types=self.job_types1,
274+
orbitals_level=default_levels_of_theory['orbitals'], adaptive_levels=None)
275+
# A live conformer-opt job, serialized exactly as ARC writes it into the restart file.
276+
conf_job = job_factory(job_adapter='gaussian', project='project_test_restore_conf',
277+
ess_settings=self.ess_settings, species=[spc], xyz=xyz,
278+
job_type='conf_opt', conformer=0,
279+
level=Level(repr={'method': 'wb97xd', 'basis': 'def2svp'}),
280+
project_directory=self.project_directory, job_num=901)
281+
sched.restart_dict = {'running_jobs': {label: [conf_job.as_dict()]}}
282+
sched.running_jobs = dict()
283+
sched.job_dict = dict()
284+
285+
sched.restore_running_jobs()
286+
# The job_dict is reconstructed correctly regardless of the fix: the conformer job is filed
287+
# under 'conf_opt' keyed by its integer index. The name is the only defect.
288+
self.assertIn('conf_opt', sched.job_dict[label])
289+
self.assertIn(0, sched.job_dict[label]['conf_opt'])
290+
291+
# The first scheduling sweep after a restart reproduces the production crash on the unfixed
292+
# code: get_i_from_job_name('conformer0') is None, the fallback derives an empty job-type
293+
# from the underscore-less name, and self.job_dict[label][''] raises KeyError: ''.
294+
sched.get_completed_incore_jobs()
295+
self.assertEqual(sched.completed_incore_jobs, list())
296+
297+
# And the restored name is the live '{job_type}_{i}' format, not the fossil 'conformer{i}'.
298+
self.assertEqual(sched.running_jobs[label], ['conf_opt_0'])
299+
243300
def test_check_negative_freq(self):
244301
"""Test the check_negative_freq() method"""
245302
label = 'C2H6'

0 commit comments

Comments
 (0)