Skip to content

Commit 70dab25

Browse files
authored
Fix switch_ts resetting rotors convergence flag and stale rotors_dict (#870)
delete_all_species_jobs blanket-set all output job_types to False, including rotors and bde which are initialised to True by initialize_output_dict. For species with no torsional modes (e.g. cyclic TS from THF), no scan jobs are ever spawned, so rotors stays False and check_all_done incorrectly marks the TS as unconverged — even when opt, freq, sp, and IRC all passed. Additionally, switch_ts did not reset rotors_dict, so determine_rotors was never re-called for the new TS geometry and stale scan results from the previous guess carried over. Changes: - Preserve the True default for rotors/bde in delete_all_species_jobs, matching initialize_output_dict. - Reset rotors_dict and number_of_rotors in switch_ts when job_types['rotors'] is enabled, so the new geometry gets fresh rotor detection.
2 parents 05099fc + 0942b37 commit 70dab25

2 files changed

Lines changed: 110 additions & 1 deletion

File tree

arc/scheduler.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2773,6 +2773,11 @@ def switch_ts(self, label: str):
27732773
if os.path.isfile(freq_path):
27742774
os.remove(freq_path)
27752775
self.species_dict[label].populate_ts_checks() # Restart the TS checks dict.
2776+
if self.job_types['rotors'] and self.species_dict[label].rotors_dict is not None:
2777+
# Reset rotors so they are re-determined from the new TS geometry.
2778+
# rotors_dict=None is a sentinel meaning "skip rotor scans"; preserve it.
2779+
self.species_dict[label].rotors_dict = {}
2780+
self.species_dict[label].number_of_rotors = 0
27762781
if not self.species_dict[label].ts_guesses_exhausted and self.species_dict[label].chosen_ts is not None:
27772782
logger.info(f'Optimizing species {label} again using a different TS guess: '
27782783
f'conformer {self.species_dict[label].chosen_ts}')
@@ -3728,7 +3733,13 @@ def delete_all_species_jobs(self, label: str):
37283733
self.running_jobs[label] = list()
37293734
self.output[label]['paths'] = {key: '' if key != 'irc' else list() for key in self.output[label]['paths'].keys()}
37303735
for job_type in self.output[label]['job_types']:
3731-
self.output[label]['job_types'][job_type] = False
3736+
# rotors and bde are initialised to True (see initialize_output_dict) because
3737+
# species with no torsional modes / no BDE targets should not be blocked from
3738+
# convergence. Preserve that default when resetting job state.
3739+
if job_type in ['rotors', 'bde']:
3740+
self.output[label]['job_types'][job_type] = True
3741+
else:
3742+
self.output[label]['job_types'][job_type] = False
37323743
self.output[label]['convergence'] = None
37333744
self._pending_pipe_sp.discard(label)
37343745
self._pending_pipe_freq.discard(label)

arc/scheduler_test.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,104 @@ def test_switch_ts_cleanup(self, mock_run_opt):
907907
self.assertIsNone(sched.species_dict[ts_label].ts_checks['NMD'])
908908
self.assertIsNone(sched.species_dict[ts_label].ts_checks['E0'])
909909

910+
# Verify rotors convergence flag preserved as True (not blanket-reset to False).
911+
self.assertTrue(sched.output[ts_label]['job_types']['rotors'])
912+
913+
@patch('arc.scheduler.Scheduler.run_opt_job')
914+
def test_switch_ts_rotors_reset(self, mock_run_opt):
915+
"""Test that switch_ts resets rotors_dict when rotors are enabled, and preserves the None sentinel."""
916+
ts_xyz = str_to_xyz("""N 0.91779059 0.51946178 0.00000000
917+
H 1.81402049 1.03819414 0.00000000
918+
H 0.00000000 0.00000000 0.00000000
919+
H 0.91779059 1.22790192 0.72426890""")
920+
921+
ts_spc = ARCSpecies(label='TS_rot', is_ts=True, xyz=ts_xyz, multiplicity=1, charge=0,
922+
compute_thermo=False)
923+
ts_spc.ts_guesses = [
924+
TSGuess(index=0, method='heuristics', success=True, energy=100.0, xyz=ts_xyz,
925+
execution_time='0:00:01'),
926+
TSGuess(index=1, method='heuristics', success=True, energy=110.0, xyz=ts_xyz,
927+
execution_time='0:00:01'),
928+
]
929+
ts_spc.ts_guesses[0].opt_xyz = ts_xyz
930+
ts_spc.ts_guesses[0].imaginary_freqs = [-500.0]
931+
ts_spc.ts_guesses[1].opt_xyz = ts_xyz
932+
ts_spc.ts_guesses[1].imaginary_freqs = [-400.0]
933+
ts_spc.chosen_ts = 0
934+
ts_spc.chosen_ts_list = [0]
935+
ts_spc.ts_guesses_exhausted = False
936+
# Simulate stale rotors from previous guess.
937+
ts_spc.rotors_dict = {0: {'pivots': [1, 2], 'scan_path': '', 'success': True}}
938+
ts_spc.number_of_rotors = 1
939+
940+
project_directory = os.path.join(ARC_PATH, 'Projects',
941+
'arc_project_for_testing_delete_after_usage5')
942+
self.addCleanup(shutil.rmtree, project_directory, ignore_errors=True)
943+
sched = Scheduler(project='test_switch_ts_rot', ess_settings=self.ess_settings,
944+
species_list=[ts_spc],
945+
opt_level=Level(repr=default_levels_of_theory['opt']),
946+
freq_level=Level(repr=default_levels_of_theory['freq']),
947+
sp_level=Level(repr=default_levels_of_theory['sp']),
948+
ts_guess_level=Level(repr=default_levels_of_theory['ts_guesses']),
949+
project_directory=project_directory,
950+
testing=True,
951+
job_types=self.job_types2, # rotors=True
952+
)
953+
954+
ts_label = 'TS_rot'
955+
sched.output[ts_label]['job_types']['opt'] = True
956+
sched.output[ts_label]['job_types']['freq'] = True
957+
sched.job_dict[ts_label] = {'opt': {}, 'freq': {}, 'sp': {}}
958+
sched.running_jobs[ts_label] = []
959+
960+
sched.switch_ts(ts_label)
961+
962+
# rotors_dict should be reset so determine_rotors re-runs for the new geometry.
963+
self.assertEqual(sched.species_dict[ts_label].rotors_dict, {})
964+
self.assertEqual(sched.species_dict[ts_label].number_of_rotors, 0)
965+
966+
# Now test that rotors_dict=None sentinel is preserved (species marked to skip rotors).
967+
ts_spc2 = ARCSpecies(label='TS_norot', is_ts=True, xyz=ts_xyz, multiplicity=1, charge=0,
968+
compute_thermo=False)
969+
ts_spc2.ts_guesses = [
970+
TSGuess(index=0, method='heuristics', success=True, energy=100.0, xyz=ts_xyz,
971+
execution_time='0:00:01'),
972+
TSGuess(index=1, method='heuristics', success=True, energy=110.0, xyz=ts_xyz,
973+
execution_time='0:00:01'),
974+
]
975+
ts_spc2.ts_guesses[0].opt_xyz = ts_xyz
976+
ts_spc2.ts_guesses[0].imaginary_freqs = [-500.0]
977+
ts_spc2.ts_guesses[1].opt_xyz = ts_xyz
978+
ts_spc2.ts_guesses[1].imaginary_freqs = [-400.0]
979+
ts_spc2.chosen_ts = 0
980+
ts_spc2.chosen_ts_list = [0]
981+
ts_spc2.ts_guesses_exhausted = False
982+
ts_spc2.rotors_dict = None # Sentinel: skip rotor scans.
983+
984+
project_directory2 = os.path.join(ARC_PATH, 'Projects',
985+
'arc_project_for_testing_delete_after_usage6')
986+
self.addCleanup(shutil.rmtree, project_directory2, ignore_errors=True)
987+
sched2 = Scheduler(project='test_switch_ts_norot', ess_settings=self.ess_settings,
988+
species_list=[ts_spc2],
989+
opt_level=Level(repr=default_levels_of_theory['opt']),
990+
freq_level=Level(repr=default_levels_of_theory['freq']),
991+
sp_level=Level(repr=default_levels_of_theory['sp']),
992+
ts_guess_level=Level(repr=default_levels_of_theory['ts_guesses']),
993+
project_directory=project_directory2,
994+
testing=True,
995+
job_types=self.job_types2, # rotors=True
996+
)
997+
998+
ts_label2 = 'TS_norot'
999+
sched2.output[ts_label2]['job_types']['opt'] = True
1000+
sched2.job_dict[ts_label2] = {'opt': {}, 'freq': {}, 'sp': {}}
1001+
sched2.running_jobs[ts_label2] = []
1002+
1003+
sched2.switch_ts(ts_label2)
1004+
1005+
# rotors_dict=None must be preserved — do not re-enable rotor scans.
1006+
self.assertIsNone(sched2.species_dict[ts_label2].rotors_dict)
1007+
9101008
@classmethod
9111009
def tearDownClass(cls):
9121010
"""

0 commit comments

Comments
 (0)