From 6603895faa0847b7be7073c586870a7fb14bb676 Mon Sep 17 00:00:00 2001 From: Calvin Pieters Date: Fri, 10 Apr 2026 16:44:47 +0300 Subject: [PATCH 1/4] Handle DLPNO methods for monoatomic species DLPNO methods are incompatible with monoatomic species. This change generalizes the previous hydrogen-specific check to all monoatomic species and automatically falls back to the canonical method by stripping the 'dlpno-' prefix. Added max ess trsh counter attempts . --- arc/scheduler.py | 28 ++++++++++++++++++++++------ arc/scheduler_test.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/arc/scheduler.py b/arc/scheduler.py index 6f4d81f39b..df0d7100a0 100644 --- a/arc/scheduler.py +++ b/arc/scheduler.py @@ -67,9 +67,9 @@ logger = get_logger() LOWEST_MAJOR_TS_FREQ, HIGHEST_MAJOR_TS_FREQ, default_job_settings, \ - default_job_types, default_ts_adapters, max_rotor_trsh, rotor_scan_resolution, servers_dict = \ + default_job_types, default_ts_adapters, max_ess_trsh, max_rotor_trsh, rotor_scan_resolution, servers_dict = \ settings['LOWEST_MAJOR_TS_FREQ'], settings['HIGHEST_MAJOR_TS_FREQ'], settings['default_job_settings'], \ - settings['default_job_types'], settings['ts_adapters'], settings['max_rotor_trsh'], \ + settings['default_job_types'], settings['ts_adapters'], settings['max_ess_trsh'], settings['max_rotor_trsh'], \ settings['rotor_scan_resolution'], settings['servers'] @@ -1444,10 +1444,16 @@ def run_sp_job(self, level_of_theory='ccsd/cc-pvdz', job_type='sp') return - mol = self.species_dict[label].mol - if mol is not None and len(mol.atoms) == 1 and mol.atoms[0].element.symbol == 'H' and 'DLPNO' in level.method: - # Run only CCSD for an H atom instead of DLPNO-CCSD(T) / etc. - level = Level(repr='ccsd/vtz', software=level.software, args=level.args) + if self.species_dict[label].is_monoatomic() and 'dlpno' in level.method: + species = self.species_dict[label] + if species.mol.atoms[0].element.symbol in ('H', 'D', 'T'): + logger.info(f'Using HF/{level.basis} for {label} (single electron, no correlation).') + level = Level(method='hf', basis=level.basis, software=level.software, args=level.args) + else: + canonical_method = level.method.replace('dlpno-', '') + logger.info(f'DLPNO methods are incompatible with monoatomic species {label}. ' + f'Using {canonical_method}/{level.basis} instead.') + level = Level(method=canonical_method, basis=level.basis, software=level.software, args=level.args) if self.job_types['sp']: if self.species_dict[label].multi_species: if self.output_multi_spc[self.species_dict[label].multi_species].get('sp', False): @@ -3569,6 +3575,15 @@ def troubleshoot_ess(self, if job.job_adapter == 'gaussian': if self.species_dict[label].checkfile is None: self.species_dict[label].checkfile = job.checkfile + # Guard against infinite troubleshooting loops. + trsh_attempts = job.ess_trsh_methods.count('trsh_attempt') + if trsh_attempts >= max_ess_trsh: + logger.info(f'Could not troubleshoot {job.job_type} for {label}. ' + f'Reached max troubleshooting attempts ({max_ess_trsh}).') + self.output[label]['errors'] += f'Error: ESS troubleshooting attempts exhausted for {label} {job.job_type}; ' + return + job.ess_trsh_methods.append('trsh_attempt') + # Determine if the species is a hydrogen atom (or its isotope). is_h = self.species_dict[label].number_of_atoms == 1 and \ self.species_dict[label].mol.atoms[0].element.symbol in ['H', 'D', 'T'] @@ -3580,6 +3595,7 @@ def troubleshoot_ess(self, server=job.server, job_status=job.job_status[1], is_h=is_h, + is_monoatomic=self.species_dict[label].is_monoatomic(), job_type=job.job_type, num_heavy_atoms=self.species_dict[label].number_of_heavy_atoms, software=job.job_adapter, diff --git a/arc/scheduler_test.py b/arc/scheduler_test.py index 3216a9f254..20e5c62b07 100644 --- a/arc/scheduler_test.py +++ b/arc/scheduler_test.py @@ -757,6 +757,44 @@ def test_add_label_to_unique_species_labels(self): self.assertEqual(unique_label, 'new_species_15_1') self.assertEqual(self.sched2.unique_species_labels, ['methylamine', 'C2H6', 'CtripCO', 'new_species_15', 'new_species_15_0', 'new_species_15_1']) + def test_troubleshoot_ess_max_attempts(self): + """Test that troubleshoot_ess respects the max_ess_trsh limit.""" + label = 'methylamine' + self.sched1.output = dict() + self.sched1.initialize_output_dict() + self.assertEqual(self.sched1.output[label]['errors'], '') + + job = job_factory(job_adapter='gaussian', project='project_test', ess_settings=self.ess_settings, + species=[self.spc1], xyz=self.spc1.get_xyz(), job_type='opt', + level=Level(repr={'method': 'wb97xd', 'basis': 'def2tzvp'}), + project_directory=self.project_directory, job_num=200) + job.ess_trsh_methods = ['trsh_attempt'] * 25 + + self.sched1.troubleshoot_ess(label=label, job=job, + level_of_theory=Level(repr='wb97xd/def2tzvp')) + self.assertIn('ESS troubleshooting attempts exhausted', self.sched1.output[label]['errors']) + + def test_troubleshoot_ess_under_max_attempts(self): + """Test that troubleshoot_ess does not block when under the max_ess_trsh limit.""" + label = 'methylamine' + self.sched1.output = dict() + self.sched1.initialize_output_dict() + + job = job_factory(job_adapter='gaussian', project='project_test', ess_settings=self.ess_settings, + species=[self.spc1], xyz=self.spc1.get_xyz(), job_type='opt', + level=Level(repr={'method': 'wb97xd', 'basis': 'def2tzvp'}), + project_directory=self.project_directory, job_num=201) + job.ess_trsh_methods = ['trsh_attempt'] * 3 + # With only 3 attempts (under max_ess_trsh=25), the guard should NOT fire. + # Verify the error message is NOT set (i.e., the guard did not block). + # We use max_attempts - 1 to test just below the threshold. + job_at_limit = job_factory(job_adapter='gaussian', project='project_test', ess_settings=self.ess_settings, + species=[self.spc1], xyz=self.spc1.get_xyz(), job_type='opt', + level=Level(repr={'method': 'wb97xd', 'basis': 'def2tzvp'}), + project_directory=self.project_directory, job_num=202) + job_at_limit.ess_trsh_methods = ['trsh_attempt'] * 24 + self.assertNotIn('ESS troubleshooting attempts exhausted', self.sched1.output[label]['errors']) + @classmethod def tearDownClass(cls): """ From a2b76567505917af2f4c412ff742285630f56a35 Mon Sep 17 00:00:00 2001 From: Calvin Pieters Date: Fri, 10 Apr 2026 16:45:02 +0300 Subject: [PATCH 2/4] Handle monoatomic species for DLPNO methods in Orca Generalize the H-atom-specific check to all monoatomic species when using DLPNO methods in Orca, as these methods are incompatible with single-atom systems that lack electron pairs to correlate. Added tests for trsh regard monoatomic --- arc/job/trsh.py | 10 ++++++---- arc/job/trsh_test.py | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/arc/job/trsh.py b/arc/job/trsh.py index 89131f9ae4..f1878a7011 100644 --- a/arc/job/trsh.py +++ b/arc/job/trsh.py @@ -838,6 +838,7 @@ def trsh_ess_job(label: str, cpu_cores: int, ess_trsh_methods: list, is_h: bool = False, + is_monoatomic: bool = False, ) -> tuple: """ Troubleshoot issues related to the electronic structure software, such as convergence. @@ -856,6 +857,7 @@ def trsh_ess_job(label: str, cpu_cores (int): The total number of cpu cores requested for a job. ess_trsh_methods (list): The troubleshooting methods tried for this job. is_h (bool): Whether the species is a hydrogen atom (or its isotope). e.g., H, D, T. + is_monoatomic (bool): Whether the species is monoatomic (single atom). Todo: - Change server to one that has the same ESS if running out of disk space. @@ -1016,7 +1018,10 @@ def trsh_ess_job(label: str, couldnt_trsh = True elif 'orca' in software: - if 'Memory' in job_status['keywords']: + if 'dlpno' in level_of_theory.method and (is_monoatomic or is_h): + raise TrshError(f'DLPNO methods are incompatible with monoatomic species {label} in Orca. ' + f'This should have been caught by the Scheduler before job submission.') + elif 'Memory' in job_status['keywords']: # Increase memory allocation. # job_status will be for example # `Error (ORCA_SCF): Not enough memory available! Please increase MaxCore to more than: 289 MB`. @@ -1067,9 +1072,6 @@ def trsh_ess_job(label: str, logger.info(f'Troubleshooting {job_type} job in {software} for {label} using {cpu_cores} cpu cores.') if 'cpu' not in ess_trsh_methods: ess_trsh_methods.append('cpu') - elif 'dlpno' in level_of_theory.method and is_h: - logger.error('DLPNO method is not supported for H atom (or its isotope D or T) in Orca.') - couldnt_trsh = True else: couldnt_trsh = True diff --git a/arc/job/trsh_test.py b/arc/job/trsh_test.py index abc01c850c..d974874e9c 100644 --- a/arc/job/trsh_test.py +++ b/arc/job/trsh_test.py @@ -11,6 +11,7 @@ import arc.job.trsh as trsh from arc.common import ARC_TESTING_PATH +from arc.exceptions import TrshError from arc.imports import settings from arc.parser.parser import parse_1d_scan_energies @@ -775,6 +776,26 @@ def test_trsh_ess_job(self): self.assertIn('cpu', ess_trsh_methods) self.assertEqual(cpu_cores, 10) + # Orca: test 5 + # Test that DLPNO + monoatomic species raises TrshError + label = 'H' + level_of_theory = {'method': 'dlpno-ccsd(T)'} + server = 'server1' + job_type = 'sp' + software = 'orca' + fine = True + memory_gb = 16 + cpu_cores = 12 + num_heavy_atoms = 0 + ess_trsh_methods = [] + job_status = {'keywords': ['MDCI', 'Memory'], + 'error': 'MDCI error in Orca. Assuming memory allocation error.'} + with self.assertRaises(TrshError): + trsh.trsh_ess_job(label, level_of_theory, server, job_status, + job_type, software, fine, memory_gb, + num_heavy_atoms, cpu_cores, ess_trsh_methods, + is_h=True, is_monoatomic=True) + def test_determine_job_log_memory_issues(self): """Test the determine_job_log_memory_issues() function.""" job_log_path_1 = os.path.join(ARC_TESTING_PATH, 'job_log', 'no_issues.log') From adc07a7c340567b4d8df7b9f2a77f2703166d129 Mon Sep 17 00:00:00 2001 From: Calvin Pieters Date: Fri, 10 Apr 2026 19:18:16 +0300 Subject: [PATCH 3/4] Max TRSH ESS counter Added a counter now to how many times ARC will troubleshoot an ESS job. This is set in the settings.py - default is 25 times. --- arc/settings/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/arc/settings/settings.py b/arc/settings/settings.py index 7203ef8a8f..905c398626 100644 --- a/arc/settings/settings.py +++ b/arc/settings/settings.py @@ -272,6 +272,7 @@ inconsistency_ab = 0.3 # maximum allowed inconsistency between consecutive points in the scan given as a fraction # of the maximum scan energy. Default: 30% max_rotor_trsh = 4 # maximum number of times to troubleshoot the same rotor scan +max_ess_trsh = 25 # maximum number of times to troubleshoot the same ESS job (opt, sp, freq, etc.) # Thresholds for identifying significant changes in bond distance, bond angle, # or torsion angle during a rotor scan. For a TS, only 'bond' and 'torsion' are considered. From cceb25e24a4db1e851fedfe6cdeac6dbd8908d90 Mon Sep 17 00:00:00 2001 From: Calvin Pieters Date: Sun, 12 Apr 2026 14:32:22 +0300 Subject: [PATCH 4/4] Fix logging reference in ssh.py Correctly import and use the logging module to set the Paramiko log level in the SSHClient class, replacing an undefined logger reference. --- arc/job/ssh.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/arc/job/ssh.py b/arc/job/ssh.py index 414744aeaf..eab18c9dec 100644 --- a/arc/job/ssh.py +++ b/arc/job/ssh.py @@ -7,6 +7,7 @@ """ import datetime +import logging import os import time from typing import Any, Callable, List, Optional, Tuple, Union @@ -78,7 +79,7 @@ def __init__(self, server: str = '') -> None: self.key = servers[server]['key'] self._sftp = None self._ssh = None - logger.getLogger("paramiko").setLevel(logger.WARNING) + logging.getLogger("paramiko").setLevel(logging.WARNING) def __enter__(self) -> 'SSHClient': self.connect()