diff --git a/arc/imports.py b/arc/imports.py index 70022765ad..83e71adeb9 100644 --- a/arc/imports.py +++ b/arc/imports.py @@ -56,13 +56,30 @@ def resolve_overridden_dependents(settings: dict, local_settings_dict: dict) -> queue_deferred_warning(msg) +def _local_overlays_disabled() -> bool: + """Return True when ~/.arc/{settings,submit,inputs}.py overlays should be skipped. + + The repo's arc/settings/settings.py is always loaded as the baseline. This + guard only controls whether a user's personal ~/.arc/*.py files are layered + on top — we want them ignored under pytest (so tests see the same defaults + CI does) and overridable explicitly via env var. + + Triggered by either pytest being loaded (`'pytest' in sys.modules`, true + from collection onward) or an explicit ARC_IGNORE_LOCAL_SETTINGS=1 env var. + """ + if os.environ.get('ARC_IGNORE_LOCAL_SETTINGS') == '1': + return True + return 'pytest' in sys.modules + + # Common imports where the user can optionally put a modified copy of settings.py or submit.py file under ~/.arc home = os.getenv("HOME") or os.path.expanduser("~") local_arc_path = os.path.join(home, '.arc') +_skip_local = _local_overlays_disabled() local_arc_settings_path = os.path.join(local_arc_path, 'settings.py') settings = {key: val for key, val in vars(arc_settings).items() if '__' not in key} -if os.path.isfile(local_arc_settings_path): +if not _skip_local and os.path.isfile(local_arc_settings_path): local_settings = dict() if local_arc_path not in sys.path: sys.path.insert(1, local_arc_path) @@ -79,7 +96,7 @@ def resolve_overridden_dependents(settings: dict, local_settings_dict: dict) -> if 'global_ess_settings' in local_settings_dict and local_settings_dict['global_ess_settings'] else None local_arc_submit_path = os.path.join(local_arc_path, 'submit.py') -if os.path.isfile(local_arc_submit_path): +if not _skip_local and os.path.isfile(local_arc_submit_path): local_incore_commands, local_pipe_submit, local_submit_scripts = dict(), dict(), dict() if local_arc_path not in sys.path: sys.path.insert(1, local_arc_path) @@ -103,7 +120,7 @@ def resolve_overridden_dependents(settings: dict, local_settings_dict: dict) -> submit_scripts.update(local_submit_scripts) local_arc_inputs_path = os.path.join(local_arc_path, 'inputs.py') -if os.path.isfile(local_arc_inputs_path): +if not _skip_local and os.path.isfile(local_arc_inputs_path): local_input_files = dict() if local_arc_path not in sys.path: sys.path.insert(1, local_arc_path) diff --git a/arc/job/adapters/gaussian.py b/arc/job/adapters/gaussian.py index 33b1e695bf..06b5aea942 100644 --- a/arc/job/adapters/gaussian.py +++ b/arc/job/adapters/gaussian.py @@ -250,8 +250,17 @@ def write_input_file(self) -> None: input_dict['method'] = self.level.method input_dict['multiplicity'] = self.multiplicity input_dict['scan_trsh'] = self.args['keyword']['scan_trsh'] if 'scan_trsh' in self.args['keyword'] else '' - integral_algorithm = 'Acc2E=14' if 'Acc2E=14' in input_dict['trsh'] else 'Acc2E=12' - input_dict['trsh'] = input_dict['trsh'].replace('int=(Acc2E=14)', '') if 'Acc2E=14' in input_dict['trsh'] else input_dict['trsh'] + acc2e_requested = 'Acc2E=14' in input_dict['trsh'] # troubleshooting asked to tighten integrals + integral_algorithm = 'Acc2E=14' if acc2e_requested else 'Acc2E=12' + input_dict['trsh'] = input_dict['trsh'].replace('int=(Acc2E=14)', '') if acc2e_requested else input_dict['trsh'] + # The InaccurateQuadrature remedy escalates to a finer DFT integration grid (recorded as + # 'int=grid=NNNMMM'). Fold that grid into the single integral=() keyword (replacing the + # default ultrafine) rather than emitting a second, conflicting Int keyword - ultrafine is + # (99,590), the remedy grid e.g. 300590 = (300,590) is finer. Sourced from ess_trsh_methods + # so the finer grid persists across retries, and the standalone int=grid= token is dropped. + grid_remedy = next((m for m in (self.ess_trsh_methods or []) if m.startswith('int=grid=')), None) + integration_grid = grid_remedy.split('int=grid=', 1)[1] if grid_remedy else 'ultrafine' + input_dict['trsh'] = re.sub(r'\s*int=grid=\S+', '', input_dict['trsh']) if grid_remedy else input_dict['trsh'] input_dict['xyz'] = [xyz_to_str(xyz) for xyz in self.xyz] if self.run_multi_species else xyz_to_str(self.xyz) if self.level.basis is not None: @@ -267,9 +276,12 @@ def write_input_file(self) -> None: if self.level.method[:2] == 'ro': self.add_to_args(val='use=L506') - elif not('no_xqc' in list(self.args['trsh'].values())) and 'qc' in input_dict['trsh']: + elif 'no_xqc' not in self.ess_trsh_methods \ + and not('no_xqc' in list(self.args['trsh'].values())) and 'qc' in input_dict['trsh']: # xqc will do qc (quadratic convergence) if the job fails w/o it, so use it by default. - # replace qc with xqc if it's not already there + # replace qc with xqc if it's not already there. + # 'no_xqc' in ess_trsh_methods records that the xqc algorithm itself already failed + # (Gaussian l508), so don't upgrade qc to xqc in that case (see arc.job.trsh.trsh_keyword_no_qc). input_dict['trsh'] = input_dict['trsh'].replace('qc', 'xqc') if self.level.method == 'cbs-qb3-paraskevas': @@ -293,7 +305,7 @@ def write_input_file(self) -> None: if self.fine: if self.level.method_type in ['dft', 'composite']: # Note that the Acc2E argument is not available in Gaussian03 - input_dict['fine'] = f'integral=(grid=ultrafine, {integral_algorithm})' + input_dict['fine'] = f'integral=(grid={integration_grid}, {integral_algorithm})' # input_dict['trsh'] may have scf=(...) in it, so we need to add the tight and direct keywords to it scf_start = input_dict['trsh'].find('scf=(') scf_end = input_dict['trsh'].find(')', scf_start) @@ -317,13 +329,13 @@ def write_input_file(self) -> None: else f"opt=({', '.join(key for key in keywords)})" elif self.job_type == 'freq': - input_dict['job_type_2'] = f'freq IOp(7/33=1) scf=(tight, direct) integral=(grid=ultrafine, {integral_algorithm})' + input_dict['job_type_2'] = f'freq IOp(7/33=1) scf=(tight, direct) integral=(grid={integration_grid}, {integral_algorithm})' elif self.job_type == 'optfreq': input_dict['job_type_2'] = 'freq IOp(7/33=1)' elif self.job_type in ['sp', 'conf_sp']: - input_dict['job_type_1'] = f'integral=(grid=ultrafine, {integral_algorithm})' + input_dict['job_type_1'] = f'integral=(grid={integration_grid}, {integral_algorithm})' if input_dict['trsh']: input_dict['trsh'] += ' ' input_dict['trsh'] += 'scf=(tight, direct)' @@ -344,7 +356,7 @@ def write_input_file(self) -> None: ts = 'ts, ' if self.is_ts else '' input_dict['job_type_1'] = f'opt=({ts}modredundant, calcfc, noeigentest, maxStep=5)' \ - f'integral=(grid=ultrafine, {integral_algorithm})' + f'integral=(grid={integration_grid}, {integral_algorithm})' if input_dict['trsh']: input_dict['trsh'] += ' ' input_dict['trsh'] += 'scf=(tight, direct)' @@ -355,7 +367,7 @@ def write_input_file(self) -> None: elif self.job_type == 'irc': if self.fine: # Note that the Acc2E argument is not available in Gaussian03 - input_dict['fine'] = f'integral=(grid=ultrafine, {integral_algorithm})' + input_dict['fine'] = f'integral=(grid={integration_grid}, {integral_algorithm})' # We need to add scf=(direct) to the trsh argument # But we to check if it's already there, and if 'direct' not in input_dict['trsh']: @@ -376,6 +388,16 @@ def write_input_file(self) -> None: input_dict['job_type_1'] = f'irc=(CalcAll, {self.irc_direction}, maxpoints=50, stepsize=7)' + if (acc2e_requested or grid_remedy) and not self.fine and not input_dict['fine'] \ + and self.job_type in ['opt', 'conf_opt', 'optfreq', 'composite', 'irc']: + # Fine jobs fold Acc2E=14 (and any InaccurateQuadrature grid remedy) into integral=(...). + # A non-fine opt/IRC job would otherwise silently drop those troubleshooting requests + # (no integral= in the route), producing a byte-identical resubmit. Emit the integral=() + # keyword so the tightened accuracy and/or finer grid actually take effect (the default + # ultrafine grid stays a fine-only concern - only an explicit grid remedy is emitted here). + grid_part = f'grid={integration_grid}, ' if grid_remedy else '' + input_dict['fine'] = f'integral=({grid_part}{integral_algorithm})' + for constraint_tuple in self.constraints: constraint_type = constraint_type_dict[len(constraint_tuple[0])] constraint_atom_indices = ' '.join([str(atom_index) for atom_index in constraint_tuple[0]]) @@ -387,21 +409,32 @@ def write_input_file(self) -> None: input_dict['job_type_1'] += f' SCRF=({self.level.solvation_method}, Solvent={self.level.solvent})' if self.species[0].number_of_atoms > 1: - if input_dict['job_type_1']: - input_dict['job_type_1'] += ' ' + guess_keyword = '' if 'guess=INDO' in input_dict['trsh']: - input_dict['job_type_1'] += 'guess=INDO' + guess_keyword = 'guess=INDO' input_dict['trsh'] = input_dict['trsh'].replace('guess=INDO', '') - else: - input_dict['job_type_1'] += ' guess=read' if self.checkfile is not None and os.path.isfile(self.checkfile) \ - else ' guess=mix' + elif self.checkfile is not None and os.path.isfile(self.checkfile): + guess_keyword = ' guess=read' + elif any(spc.multiplicity == 1 and spc.number_of_radicals is not None and spc.number_of_radicals > 1 + for spc in self.species): + guess_keyword = ' guess=mix' + if guess_keyword: + if input_dict['job_type_1']: + input_dict['job_type_1'] += ' ' + input_dict['job_type_1'] += guess_keyword # Fix OPT terms_opt = [r'opt=\((.*?)\)', r'opt=(\w+)'] input_dict, parameters_opt = combine_parameters(input_dict, terms_opt) # If 'opt' parameters are found, concatenate and reinsert them if parameters_opt: - # Remove duplicate parameters + # Keep a single force-constant recompute directive (calcall > recalcfc=* > calcfc) so a + # troubleshot opt=() clause never carries conflicting Hessian options - the base route + # always contributes 'calcfc', while the MaxOptCycles ladder may add recalcfc/calcall. + if 'calcall' in parameters_opt: + parameters_opt = [p for p in parameters_opt if p != 'calcfc' and not p.startswith('recalcfc')] + elif any(p.startswith('recalcfc') for p in parameters_opt): + parameters_opt = [p for p in parameters_opt if p != 'calcfc'] combined_opt_params = ','.join(parameters_opt) input_dict['job_type_1'] = f"opt=({combined_opt_params}) {input_dict['job_type_1']}" diff --git a/arc/job/adapters/gaussian_test.py b/arc/job/adapters/gaussian_test.py index 96f65af276..6ab3f7a369 100644 --- a/arc/job/adapters/gaussian_test.py +++ b/arc/job/adapters/gaussian_test.py @@ -8,6 +8,7 @@ import math import os import shutil +import tempfile import unittest from arc.common import ARC_TESTING_PATH @@ -464,7 +465,8 @@ def setUpClass(cls): ) # Gaussian MaxOptCycles error - Part 2 - # Intend to troubleshoot a MaxOptCycles error by adding opt=(RFO) to the input file + # Intend to troubleshoot a MaxOptCycles error by recomputing the Hessian (opt=(recalcfc=5), + # which supersedes the base calcfc) before any step-algorithm flip. job_status = {'keywords': ['MaxOptCycles']} ess_trsh_methods = ['opt=(maxcycle=200)'] output_errors, ess_trsh_methods, remove_checkfile, level_of_theory, software, job_type, fine, trsh_keyword, \ @@ -487,7 +489,8 @@ def setUpClass(cls): ) # Gaussian MaxOptCycles error - Part 3 - # Intend to troubleshoot a MaxOptCycles error by adding opt=(GDIIS) and removing opt=(RFO) to the input file + # With maxcycle+RFO already tried, the next remedy is the Hessian recompute opt=(recalcfc=5) + # (it precedes the DIIS accelerators); RFO is retained as the single step algorithm. job_status = {'keywords': ['MaxOptCycles']} ess_trsh_methods = ['opt=(maxcycle=200)', 'opt=(RFO)'] output_errors, ess_trsh_methods, remove_checkfile, level_of_theory, software, job_type, fine, trsh_keyword, \ @@ -666,7 +669,7 @@ def test_write_input_file(self): %mem=14193mb %NProcShared=8 -#P opt=(calcfc,maxStep=5,modredundant,noeigentest) integral=(grid=ultrafine, Acc2E=12) guess=mix wb97xd/def2tzvp IOp(2/9=2000) scf=(direct,tight) +#P opt=(calcfc,maxStep=5,modredundant,noeigentest) integral=(grid=ultrafine, Acc2E=12) wb97xd/def2tzvp IOp(2/9=2000) scf=(direct,tight) ethanol @@ -796,7 +799,7 @@ def test_gaussian_def2tzvp(self): def test_trsh_write_input_file(self): """Test writing a trsh input file 10. Create an input file for a job with int=(Acc2E=14) included - 11. Create an input file for a job with guess=mix included (removal of Checkfile via ess_trsh_methods) + 11. Create an input file for a job after checkfile removal via ess_trsh_methods (no guess keyword: closed-shell) 12. Create an input file for a job with nosymm included, and also the first pass of SCF error troubleshooting 13. Create an input file for a job with NDamp=30 included, and also the previous pass of SCF error troubleshooting 14. Create an input file for a job with NoDIIS included, and also previous passes of SCF error troubleshooting @@ -808,8 +811,8 @@ def test_trsh_write_input_file(self): 20. Create an input file for a job with L502 error but had already been troubleshooted with L502 error and InaccurateQuadrature 21. Create an input file for a job with L502 error but had already been troubleshooted with L502 error and InaccurateQuadrature 22. Create an input file for a job with MaxOptCycles error - changes maxcycle to 200 from 100 - 23. Create an input file for a job with MaxOptCycles error - Add RFO to the input file - 24. Create an input file for a job with MaxOptCycles error - Add GDIIS and remove RFO from the input file + 23. Create an input file for a job with MaxOptCycles error - recompute the Hessian (recalcfc=5), superseding calcfc + 24. Create an input file for a job with MaxOptCycles error - recalcfc=5 with RFO retained as the step algorithm """ self.job_10.write_input_file() with open(os.path.join(self.job_10.local_path, input_filenames[self.job_10.job_adapter]), 'r') as f: @@ -836,7 +839,7 @@ def test_trsh_write_input_file(self): %mem=14193mb %NProcShared=8 -#P opt=(calcfc,maxcycle=100,maxstep=5,tight) guess=mix wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) scf=(direct,tight) +#P opt=(calcfc,maxcycle=100,maxstep=5,tight) wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) scf=(direct,tight) ethanol @@ -862,7 +865,7 @@ def test_trsh_write_input_file(self): %mem=14193mb %NProcShared=8 -#P opt=(calcfc,maxcycle=100,maxstep=5,tight) guess=mix wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) nosymm scf=(direct,tight,xqc) +#P opt=(calcfc,maxcycle=100,maxstep=5,tight) wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) nosymm scf=(direct,tight,xqc) ethanol @@ -888,7 +891,7 @@ def test_trsh_write_input_file(self): %mem=14193mb %NProcShared=8 -#P opt=(calcfc,maxcycle=100,maxstep=5,tight) guess=mix wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) nosymm scf=(NDamp=30,direct,tight,xqc) +#P opt=(calcfc,maxcycle=100,maxstep=5,tight) wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) nosymm scf=(NDamp=30,direct,tight,xqc) ethanol @@ -914,7 +917,7 @@ def test_trsh_write_input_file(self): %mem=14193mb %NProcShared=8 -#P opt=(calcfc,maxcycle=100,maxstep=5,tight) guess=mix wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) nosymm scf=(NDamp=30,NoDIIS,direct,tight,xqc) +#P opt=(calcfc,maxcycle=100,maxstep=5,tight) wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) nosymm scf=(NDamp=30,NoDIIS,direct,tight,xqc) ethanol @@ -940,7 +943,7 @@ def test_trsh_write_input_file(self): %mem=14193mb %NProcShared=8 -#P opt=(calcfc,cartesian,maxcycle=100,maxstep=5,tight) guess=mix wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) nosymm scf=(NDamp=30,NoDIIS,direct,tight,xqc) +#P opt=(calcfc,cartesian,maxcycle=100,maxstep=5,tight) wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) nosymm scf=(NDamp=30,NoDIIS,direct,tight,xqc) ethanol @@ -994,7 +997,7 @@ def test_trsh_write_input_file(self): %mem=14193mb %NProcShared=8 -#P opt=(calcfc,maxcycle=200,maxstep=5,tight) guess=mix wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) scf=(direct,tight,xqc) +#P opt=(calcfc,maxcycle=200,maxstep=5,tight) wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) scf=(direct,tight,xqc) ethanol @@ -1020,7 +1023,7 @@ def test_trsh_write_input_file(self): %mem=14193mb %NProcShared=8 -#P opt=(calcfc,maxcycle=100,maxstep=5,tight) guess=mix wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) int=grid=300590 scf=(direct,tight) +#P opt=(calcfc,maxcycle=100,maxstep=5,tight) wb97xd integral=(grid=300590, Acc2E=14) IOp(2/9=2000) scf=(direct,tight) ethanol @@ -1046,7 +1049,7 @@ def test_trsh_write_input_file(self): %mem=14193mb %NProcShared=8 -#P opt=(calcfc,maxcycle=100,maxstep=5,tight) guess=mix wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) nosymm scf=(Fermi,NDamp=30,NoDIIS,NoVarAcc,Noincfock,direct,tight,xqc) +#P opt=(calcfc,maxcycle=100,maxstep=5,tight) wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) nosymm scf=(Fermi,NDamp=30,NoDIIS,NoVarAcc,Noincfock,direct,tight,xqc) ethanol @@ -1072,7 +1075,7 @@ def test_trsh_write_input_file(self): %mem=14193mb %NProcShared=8 -#P opt=(calcfc,maxcycle=100,maxstep=5,tight) guess=mix wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) scf=(NDamp=30,NoDIIS,NoVarAcc,direct,tight,xqc) +#P opt=(calcfc,maxcycle=100,maxstep=5,tight) wb97xd integral=(grid=300590, Acc2E=14) IOp(2/9=2000) scf=(NDamp=30,NoDIIS,NoVarAcc,direct,tight,xqc) ethanol @@ -1099,7 +1102,7 @@ def test_trsh_write_input_file(self): %mem=14193mb %NProcShared=8 -#P opt=(calcfc,maxcycle=100,maxstep=5,tight) guess=INDO wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) int=grid=300590 scf=(NDamp=30,NoDIIS,NoVarAcc,direct,tight,xqc) +#P opt=(calcfc,maxcycle=100,maxstep=5,tight) wb97xd integral=(grid=300590, Acc2E=14) IOp(2/9=2000) scf=(NDamp=30,NoDIIS,NoVarAcc,direct,tight,xqc) ethanol @@ -1126,7 +1129,7 @@ def test_trsh_write_input_file(self): %mem=14193mb %NProcShared=8 -#P opt=(calcfc,maxcycle=200,maxstep=5,tight) guess=mix wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) scf=(direct,tight) +#P opt=(calcfc,maxcycle=200,maxstep=5,tight) wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) scf=(direct,tight) ethanol @@ -1153,7 +1156,7 @@ def test_trsh_write_input_file(self): %mem=14193mb %NProcShared=8 -#P opt=(RFO,calcfc,maxcycle=200,maxstep=5,tight) guess=mix wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) scf=(direct,tight) +#P opt=(maxcycle=200,maxstep=5,recalcfc=5,tight) wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) scf=(direct,tight) ethanol @@ -1180,7 +1183,7 @@ def test_trsh_write_input_file(self): %mem=14193mb %NProcShared=8 -#P opt=(GDIIS,calcfc,maxcycle=200,maxstep=5,tight) guess=mix wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) scf=(direct,tight) +#P opt=(RFO,maxcycle=200,maxstep=5,recalcfc=5,tight) wb97xd integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) scf=(direct,tight) ethanol @@ -1260,5 +1263,257 @@ def test_malformed_marker_is_ignored(self): self.assertEqual(get_memory_headroom_fraction(['memory_headroom_notanumber', 'memory_headroom_0.75']), 0.75) +class TestGaussianAdapterNoXqc(unittest.TestCase): + """ + Contains unit tests for the GaussianAdapter qc -> xqc upgrade and its 'no_xqc' opt-out. + + Self-contained (does not depend on the server settings used by TestGaussianAdapter's fixtures). + """ + + def write_input(self, ess_trsh_methods: list) -> str: + """Render a Gaussian input with scf=(qc) requested via trsh args and return its content.""" + project_directory = os.path.join(ARC_TESTING_PATH, 'test_GaussianAdapterNoXqc') + self.addCleanup(shutil.rmtree, project_directory, ignore_errors=True) + job = GaussianAdapter(execution_type='incore', + job_type='opt', + level=Level(method='wb97xd', basis='def2tzvp'), + project='test', + project_directory=project_directory, + species=[ARCSpecies(label='spc1', xyz=['O 0 0 1'], multiplicity=3)], + testing=True, + ess_trsh_methods=ess_trsh_methods, + # the scheduler passes the trsh keywords as a list under args['trsh']['trsh'] + args={'trsh': {'trsh': ['scf=(qc)']}}, + ) + job.write_input_file() + with open(os.path.join(job.local_path, input_filenames[job.job_adapter]), 'r') as f: + return f.read() + + def test_write_input_file_upgrades_qc_to_xqc_by_default(self): + """By default, a requested scf=(qc) is upgraded to scf=(xqc).""" + content = self.write_input(ess_trsh_methods=['scf=(qc)']) + self.assertIn('scf=(xqc)', content) + self.assertNotIn('scf=(qc)', content) + + def test_write_input_file_no_xqc_blocks_qc_upgrade(self): + """Once 'no_xqc' is recorded (Gaussian l508 failed), qc must not be upgraded to xqc.""" + content = self.write_input(ess_trsh_methods=['no_xqc']) + self.assertIn('scf=(qc)', content) + self.assertNotIn('xqc', content) + + +class TestGaussianAdapterAcc2E(unittest.TestCase): + """ + P2: int=(Acc2E=14) must take effect on non-fine opt/IRC jobs (self-contained). + """ + + def render(self, fine, trsh_list, job_type='opt'): + project_directory = os.path.join(ARC_TESTING_PATH, 'test_GaussianAdapterAcc2E') + self.addCleanup(shutil.rmtree, project_directory, ignore_errors=True) + kwargs = dict(execution_type='incore', job_type=job_type, + level=Level(method='wb97xd', basis='def2tzvp'), project='test', + project_directory=project_directory, + species=[ARCSpecies(label='spc1', xyz=['O 0 0 1'], multiplicity=3)], + testing=True, fine=fine, args={'trsh': {'trsh': trsh_list}}) + if job_type == 'irc': + kwargs['irc_direction'] = 'forward' + job = GaussianAdapter(**kwargs) + job.write_input_file() + with open(os.path.join(job.local_path, input_filenames[job.job_adapter]), 'r') as f: + return next(line for line in f if line.startswith('#')) + + def test_non_fine_opt_emits_acc2e(self): + """A non-fine opt with int=(Acc2E=14) in trsh must actually emit the integral setting.""" + route = self.render(fine=False, trsh_list=['int=(Acc2E=14)']) + self.assertIn('integral=(Acc2E=14)', route) + self.assertIn('Acc2E=14', route) + + def test_non_fine_opt_without_acc2e_unchanged(self): + """A normal non-fine opt (no Acc2E trsh) must not gain any integral= setting.""" + route = self.render(fine=False, trsh_list=[]) + self.assertNotIn('integral=', route) + self.assertNotIn('Acc2E', route) + + def test_fine_opt_still_folds_acc2e_into_ultrafine(self): + """A fine opt keeps folding Acc2E=14 into the ultrafine integral grid (unchanged).""" + route = self.render(fine=True, trsh_list=['int=(Acc2E=14)']) + self.assertIn('integral=(grid=ultrafine, Acc2E=14)', route) + + def test_non_fine_irc_emits_acc2e(self): + """A non-fine IRC with int=(Acc2E=14) in trsh must emit the integral setting too.""" + route = self.render(fine=False, trsh_list=['int=(Acc2E=14)'], job_type='irc') + self.assertIn('integral=(Acc2E=14)', route) + + +class TestGaussianAdapterOptLadder(unittest.TestCase): + """ + P3: the opt=() clause the adapter renders from the MaxOptCycles remedy ladder must carry a + single, non-conflicting force-constant directive, and a TS route must never receive GDIIS. + """ + + def render(self, trsh_list, is_ts): + project_directory = os.path.join(ARC_TESTING_PATH, 'test_GaussianAdapterOptLadder') + self.addCleanup(shutil.rmtree, project_directory, ignore_errors=True) + # GaussianAdapter derives self.is_ts from species[0].is_ts. + if is_ts: + spc = ARCSpecies(label='TS0', is_ts=True, + xyz=['O 0.0 0.0 0.0', 'H 0.0 0.0 0.97', 'H 0.94 0.0 -0.24']) + else: + spc = ARCSpecies(label='spc1', xyz=['O 0 0 1'], multiplicity=3) + job = GaussianAdapter(execution_type='incore', job_type='opt', + level=Level(method='wb97xd', basis='def2tzvp'), project='test', + project_directory=project_directory, species=[spc], testing=True, + fine=False, args={'trsh': {'trsh': trsh_list}}) + job.write_input_file() + with open(os.path.join(job.local_path, input_filenames[job.job_adapter]), 'r') as f: + return next(line for line in f if line.startswith('#')) + + def test_recalcfc_supersedes_base_calcfc(self): + """When the ladder adds recalcfc, the base calcfc must be dropped (no conflicting FC opts).""" + route = self.render(['opt=(maxcycle=200)', 'opt=(recalcfc=5)'], is_ts=False) + self.assertIn('recalcfc=5', route) + self.assertNotIn('calcfc,', route.replace('recalcfc', '')) # no standalone calcfc token + + def test_calcall_supersedes_recalcfc_and_calcfc(self): + """calcall is most aggressive: it must drop both recalcfc and calcfc.""" + route = self.render(['opt=(recalcfc=5)', 'opt=(calcall)'], is_ts=False) + self.assertIn('calcall', route) + self.assertNotIn('recalcfc', route) + self.assertNotIn('calcfc', route) + + def test_ts_route_renders_rfo_ladder(self): + """ + A TS opt route built from the (TS-aware) ladder renders with ts + RFO + Hessian recompute, + with the base calcfc superseded by recalcfc. (GDIIS is never produced for a TS - that guard + lives in arc.job.trsh.prioritize_opt_methods, covered by the trsh tests.) + """ + route = self.render(['opt=(maxcycle=200)', 'opt=(recalcfc=5)', 'opt=(RFO)'], is_ts=True) + self.assertIn('ts', route) + self.assertIn('RFO', route) + self.assertIn('recalcfc=5', route) + self.assertNotIn('GDIIS', route) + + +class TestGaussianAdapterGuessMixGating(unittest.TestCase): + """ + Contains unit tests for the guess keyword rendered into the Gaussian route. + + guess=mix is rendered only for a species with multiplicity 1 and more than one radical. + guess=read (checkfile present) and guess=INDO (troubleshooting) take precedence over it. + """ + + OH_XYZ = ['O 0.0 0.0 0.0\nH 0.0 0.0 0.97'] + C2H4_XYZ = ["""C -0.6 0.0 0.0 + C 0.6 0.0 0.0 + H -1.2 0.9 0.0 + H -1.2 -0.9 0.0 + H 1.2 0.9 0.0 + H 1.2 -0.9 0.0"""] + O2_XYZ = ['O 0.0 0.0 0.0\nO 0.0 0.0 1.2'] + TS_XYZ = ['O 0.0 0.0 0.0\nH 0.0 0.0 0.97\nH 0.94 0.0 -0.24'] + TS_DOUBLET_XYZ = ['H 0.0 0.0 0.0\nH 0.0 0.0 0.93\nH 0.0 0.0 1.86'] + + def render(self, species, checkfile=None, args=None, level=None): + """Render the Gaussian route line for ``species`` and return it.""" + project_directory = os.path.join(ARC_TESTING_PATH, 'test_GaussianAdapterGuessMixGating') + self.addCleanup(shutil.rmtree, project_directory, ignore_errors=True) + level = level if level is not None else Level(method='wb97xd', basis='def2tzvp') + job = GaussianAdapter(execution_type='incore', job_type='opt', + level=level, project='test', + project_directory=project_directory, species=[species], testing=True, + checkfile=checkfile, args=args) + job.write_input_file() + with open(os.path.join(job.local_path, input_filenames[job.job_adapter]), 'r') as f: + return next(line for line in f if line.startswith('#')) + + def test_singlet_biradical_gets_guess_mix(self): + """A multiplicity 1 species with two radicals gets guess=mix.""" + route = self.render(ARCSpecies(label='O2_singlet', xyz=self.O2_XYZ, + multiplicity=1, number_of_radicals=2)) + self.assertIn('guess=mix', route) + self.assertIn('uwb97xd', route) + + def test_closed_shell_singlet_has_no_guess_mix(self): + """A multiplicity 1 species with no declared radicals gets no guess keyword.""" + route = self.render(ARCSpecies(label='C2H4', xyz=self.C2H4_XYZ, multiplicity=1)) + self.assertNotIn('guess=', route) + self.assertIn(' wb97xd', route) + + def test_doublet_radical_has_no_guess_mix(self): + """A multiplicity 2 species gets no guess keyword.""" + route = self.render(ARCSpecies(label='OH', xyz=self.OH_XYZ, multiplicity=2)) + self.assertNotIn('guess=', route) + self.assertIn('uwb97xd', route) + + def test_triplet_has_no_guess_mix(self): + """A multiplicity 3 species gets no guess keyword.""" + route = self.render(ARCSpecies(label='O2', xyz=self.O2_XYZ, multiplicity=3)) + self.assertNotIn('guess=', route) + self.assertIn('uwb97xd', route) + + def test_singlet_ts_without_number_of_radicals_has_no_guess_mix(self): + """A multiplicity 1 TS with an undeclared number_of_radicals gets no guess keyword.""" + ts = ARCSpecies(label='TS0', is_ts=True, multiplicity=1, xyz=self.TS_XYZ) + self.assertIsNone(ts.number_of_radicals) + route = self.render(ts) + self.assertNotIn('guess=', route) + self.assertIn('ts', route) + + def test_doublet_ts_has_no_guess_mix(self): + """A multiplicity 2 TS gets no guess keyword. + + Uses the H + H2 abstraction TS (three electrons), since a doublet requires + an odd electron count. + """ + route = self.render(ARCSpecies(label='TS0', is_ts=True, multiplicity=2, xyz=self.TS_DOUBLET_XYZ)) + self.assertNotIn('guess=', route) + self.assertIn('ts', route) + + def test_singlet_biradical_ts_gets_guess_mix(self): + """A multiplicity 1 TS with two declared radicals gets guess=mix.""" + route = self.render(ARCSpecies(label='TS0', is_ts=True, multiplicity=1, + number_of_radicals=2, xyz=self.TS_XYZ)) + self.assertIn('guess=mix', route) + + def test_composite_method_is_gated_by_multiplicity_not_method_type(self): + """At a composite method the guess keyword follows the multiplicity, not is_species_restricted. + + is_species_restricted returns True for every composite job, so it cannot be the gate: + a doublet at CBS-QB3 carries no 'u' prefix yet Gaussian runs UB3LYP for it. + """ + composite = Level(method='cbs-qb3') + doublet_route = self.render(ARCSpecies(label='OH', xyz=self.OH_XYZ, multiplicity=2), + level=composite) + self.assertNotIn('guess=', doublet_route) + self.assertIn('cbs-qb3', doublet_route) + self.assertNotIn('ucbs-qb3', doublet_route) + biradical_route = self.render(ARCSpecies(label='O2_singlet', xyz=self.O2_XYZ, + multiplicity=1, number_of_radicals=2), + level=composite) + self.assertIn('guess=mix', biradical_route) + + def test_checkfile_guess_read_takes_precedence(self): + """With a checkfile present, guess=read is rendered instead of guess=mix.""" + checkfile_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, checkfile_dir, ignore_errors=True) + checkfile = os.path.join(checkfile_dir, 'check.chk') + with open(checkfile, 'w') as f: + f.write('dummy') + for spc in [ARCSpecies(label='O2_singlet', xyz=self.O2_XYZ, multiplicity=1, number_of_radicals=2), + ARCSpecies(label='OH', xyz=self.OH_XYZ, multiplicity=2)]: + route = self.render(spc, checkfile=checkfile) + self.assertIn('guess=read', route) + self.assertNotIn('guess=mix', route) + + def test_trsh_guess_indo_takes_precedence(self): + """guess=INDO requested via troubleshooting is rendered instead of guess=mix.""" + route = self.render(ARCSpecies(label='O2_singlet', xyz=self.O2_XYZ, + multiplicity=1, number_of_radicals=2), + args={'trsh': {'trsh': ['guess=INDO']}}) + self.assertIn('guess=INDO', route) + self.assertNotIn('guess=mix', route) + self.assertNotIn('guess=read', route) + + if __name__ == '__main__': unittest.main(testRunner=unittest.TextTestRunner(verbosity=2)) diff --git a/arc/job/trsh.py b/arc/job/trsh.py index 8a8d248625..04a6245e2a 100644 --- a/arc/job/trsh.py +++ b/arc/job/trsh.py @@ -127,7 +127,7 @@ def determine_ess_status(output_path: str, error = 'There are two blank lines between z-matrix and ' \ 'the variables, expected only one.' elif 'l202.exe' in line: - keywords = ['OptOrientation', 'GL202'] + keywords = ['OptOrientation', 'GL202', 'NoSymm'] error = 'During the optimization process, either the standard ' \ 'orientation or the point group of the molecule has changed.' elif 'l301.exe' in line: @@ -195,8 +195,14 @@ def determine_ess_status(output_path: str, 'specified correctly. Alternatively, a specified atom does not match any ' \ 'standard atomic symbol.' elif 'GL401' in keywords: - keywords.append('BasisSet') - error = 'The projection from the old to the new basis set has failed.' + # A guess=read from a checkpoint built with a different basis fails to + # project onto the new basis. Removing the checkfile drops guess=read so + # the job restarts from a fresh SCF guess with no projection step - this + # is retryable, so classify it as CheckFile (mirroring the checkpoint- + # data-missing cases above), not as a dead-end BasisSet error. + keywords = ['CheckFile'] + error = 'The projection from the old to the new basis set has failed; ' \ + 'removing the checkfile to restart from a fresh SCF guess.' elif 'Erroneous write' in line or 'Write error in NtrExt1' in line: keywords = ['DiskSpace'] error = 'Ran out of disk space.' @@ -867,6 +873,23 @@ def trsh_special_rotor(special_rotor: list, return to_freeze +# Gaussian error classes that resubmission cannot fix: input/template/method-basis problems. +# When one of these is detected we refuse to troubleshoot instead of burning a resubmit on an +# unrelated remedy (historically every Gaussian error picked up a spurious int=(Acc2E=14) step). +# NOTE: this is deliberately conservative - it excludes classes that a resubmit *can* help +# (SCF, opt cycles, internal-coordinate, negative eigenvalues, memory, checkfile, and the +# generic 'Unknown' class, which the scheduler retries on a different node). +# 'ZMat' (L716: z-matrix angle outside 0 < x < 180) is intentionally NOT here: ARC always +# submits Cartesian geometries, so a ZMat error always means the optimizer drove atoms +# collinear - the textbook opt=(cartesian) case, handled by trsh_keyword_cartesian. +# 'BasisSet' is intentionally NOT here either: every genuine dead-end basis error (GL301 +# "atomic number out of range" / "Unrecognized basis set") is already refused by the dedicated +# BasisSet branch in trsh_ess_job, and the one retryable BasisSet case - the GL401 guess=read +# projection failure - is reclassified as 'CheckFile' (see determine_ess_status) and fixed by +# dropping the checkfile. So no BasisSet error reaches this gate. +GAUSSIAN_NON_RETRYABLE_KEYWORDS = ('Syntax', 'InputError', 'MP2', 'Scratch') + + def trsh_ess_job(label: str, level_of_theory: Level | dict | str, server: str, @@ -880,6 +903,7 @@ def trsh_ess_job(label: str, ess_trsh_methods: list, is_h: bool = False, is_monoatomic: bool = False, + is_ts: bool = False, ) -> tuple: """ Troubleshoot issues related to the electronic structure software, such as convergence. @@ -899,6 +923,8 @@ def trsh_ess_job(label: str, 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). + is_ts (bool): Whether the species is a transition state. Makes the opt-cycle remedy + ladder TS-aware (rely on RFO + Hessian recompute, avoid GDIIS/GEDIIS). Todo: - Change server to one that has the same ESS if running out of disk space. @@ -943,6 +969,17 @@ def trsh_ess_job(label: str, logger.info(f'Troubleshooting {job_type} job in {software} for {label} that failed with ' '"Basis set data is not on the checkpoint file" by removing the checkfile.') + elif software == 'gaussian' \ + and any(kw in job_status['keywords'] for kw in GAUSSIAN_NON_RETRYABLE_KEYWORDS): + # Non-retryable input/method error: refuse rather than waste a resubmit (see note above). + non_retryable = next(kw for kw in GAUSSIAN_NON_RETRYABLE_KEYWORDS if kw in job_status['keywords']) + output_errors.append(f'Error: Could not troubleshoot {job_type} for {label}! Gaussian reported a ' + f'non-retryable "{non_retryable}" error that resubmission cannot fix ' + f'({job_status.get("error", "").strip() or "no further detail"}); ') + logger.error(f'Could not troubleshoot {job_type} job in {software} for {label}: non-retryable ' + f'"{non_retryable}" error. {job_status.get("error", "").strip()}') + couldnt_trsh = True + elif software == 'gaussian': trsh_keyword = [] # initialize as a list logger_phrase = f'Troubleshooting {job_type} job in {software} for {label}' @@ -992,7 +1029,7 @@ def trsh_ess_job(label: str, # Troubleshoot by increasing opt max cycles #P opt=(calcfc,maxstep=5,tight,maxcycle=200) guess=mix wb97xd/def2tzvp integral=(grid=ultrafine, Acc2E=14) IOp(2/9=2000) scf=(direct,tight,maxcycle=512) iop(3/33=1) - ess_trsh_methods, trsh_keyword, couldnt_trsh = trsh_keyword_opt_maxcycles(job_status, ess_trsh_methods, trsh_keyword, couldnt_trsh) + ess_trsh_methods, trsh_keyword, couldnt_trsh = trsh_keyword_opt_maxcycles(job_status, ess_trsh_methods, trsh_keyword, couldnt_trsh, is_ts, fine) # print out any words that beging with 'opt=' opt_list = [i for i in ess_trsh_methods if i.startswith('opt=')] if opt_list: @@ -1010,9 +1047,9 @@ def trsh_ess_job(label: str, formatted_string += f', {i}' logger_info.append(formatted_string) - # Remove qc from ess_trsh_methods if 'no_qc' is in the keywords + # Drop the quadratic-convergence SCF remedy (record 'no_xqc') if Gaussian failed in l508 ess_trsh_methods, trsh_keyword, couldnt_trsh = trsh_keyword_no_qc(job_status, ess_trsh_methods, trsh_keyword, couldnt_trsh) - if 'no_qc' in ess_trsh_methods: + if 'no_xqc' in ess_trsh_methods: logger_info.append('removed QC') @@ -1886,9 +1923,16 @@ def trsh_keyword_intaccuracy(ess_trsh_methods, trsh_keyword, couldnt_trsh) -> tu def trsh_keyword_cartesian(job_status, ess_trsh_methods, job_type, trsh_keyword: list, couldnt_trsh: bool) -> tuple[list, list, bool]: """ - Check if the job requires change of cartesian coordinate + Switch the optimization to Cartesian coordinates (opt=(cartesian)). + + Fires for both L103 ('InternalCoordinateError') and L716 ('ZMat', z-matrix angle driven + outside 0 < x < 180 during the optimization). Since ARC always submits Cartesian geometries, + a ZMat error is never a bad input - it is the collinear-angle degeneracy that Cartesian + optimization sidesteps. Tried once (guarded by 'cartesian' not in ess_trsh_methods); if the + error recurs after Cartesian was already attempted, ess_trsh_methods stops changing and + trsh_ess_job() terminates the retry loop via the 'all_attempted' marker. """ - if 'InternalCoordinateError' in job_status['keywords'] \ + if ('InternalCoordinateError' in job_status['keywords'] or 'ZMat' in job_status['keywords']) \ and 'cartesian' not in ess_trsh_methods: ess_trsh_methods.append('cartesian') trsh_keyword.append('opt=(cartesian)') @@ -1906,8 +1950,10 @@ def trsh_keyword_scf(job_status, ess_trsh_methods, trsh_keyword, couldnt_trsh) - Check if the job requires change of scf """ scf_pattern = r"scf=\((.*?)\)" # e.g., scf=(xqc,MaxCycle=1000), will match xqc,MaxCycle=1000 - if 'SCF' in job_status['keywords'] and 'scf=(qc)' not in ess_trsh_methods and 'no_qc' not in ess_trsh_methods: + if 'SCF' in job_status['keywords'] and 'scf=(qc)' not in ess_trsh_methods and 'no_xqc' not in ess_trsh_methods: # try both qc and nosymm + # ('no_xqc' records that the quadratic-convergence remedy already failed in l508 + # and was dropped, so don't re-add it - see trsh_keyword_no_qc()) ess_trsh_methods.append('scf=(qc)') couldnt_trsh = False elif 'SCF' in job_status['keywords'] and 'scf=(NDamp=30)' not in ess_trsh_methods: @@ -1922,8 +1968,12 @@ def trsh_keyword_scf(job_status, ess_trsh_methods, trsh_keyword, couldnt_trsh) - ess_trsh_methods.append('guess=INDO') couldnt_trsh = False trsh_keyword.append('guess=INDO') - # If we have attempted all scf methods above, then we will try last resort methods - if 'SCF' in job_status['keywords'] and 'scf=(qc)' in ess_trsh_methods and 'scf=(NDamp=30)' in ess_trsh_methods and 'scf=(NoDIIS)' in ess_trsh_methods and 'guess=INDO' in ess_trsh_methods \ + # If we have attempted all scf methods above, then we will try last resort methods. + # 'scf=(qc)' counts as attempted either if it is still active or if it was tried and + # dropped after failing in l508 (recorded as 'no_xqc'). + if 'SCF' in job_status['keywords'] \ + and ('scf=(qc)' in ess_trsh_methods or 'no_xqc' in ess_trsh_methods) \ + and 'scf=(NDamp=30)' in ess_trsh_methods and 'scf=(NoDIIS)' in ess_trsh_methods and 'guess=INDO' in ess_trsh_methods \ and 'scf=(Fermi)' not in ess_trsh_methods and 'scf=(Noincfock)' not in ess_trsh_methods and 'scf=(NoVarAcc)' not in ess_trsh_methods: # Uses Fermi broadening to help SCF convergence ess_trsh_methods.append('scf=(Fermi)') @@ -1931,7 +1981,8 @@ def trsh_keyword_scf(job_status, ess_trsh_methods, trsh_keyword, couldnt_trsh) - ess_trsh_methods.append('scf=(Noincfock)') ess_trsh_methods.append('scf=(NoVarAcc)') couldnt_trsh = False - if 'no_qc' in ess_trsh_methods and 'scf=(qc)' in ess_trsh_methods: + if 'no_xqc' in ess_trsh_methods and 'scf=(qc)' in ess_trsh_methods: + # safety net: never keep the qc remedy active once it has been dropped via 'no_xqc' ess_trsh_methods.remove('scf=(qc)') couldnt_trsh = False @@ -1944,7 +1995,8 @@ def trsh_keyword_scf(job_status, ess_trsh_methods, trsh_keyword, couldnt_trsh) - def trsh_keyword_unconverged(job_status, ess_trsh_methods, trsh_keyword, couldnt_trsh, fine) -> tuple[list, list, bool, bool]: """ - Check if the job requires change of scf + Remediate a generic 'Unconverged' (Gaussian l9999) failure by switching to a fine + integration grid for the SCF and the integrals (recorded as 'fine'). This is tried once. """ if 'Unconverged' in job_status['keywords'] and 'fine' not in ess_trsh_methods and not fine: # try a fine grid for SCF and integral @@ -1970,39 +2022,85 @@ def trsh_keyword_nosymm(job_status, ess_trsh_methods, trsh_keyword, couldnt_trsh return ess_trsh_methods, trsh_keyword, couldnt_trsh -def trsh_keyword_opt_maxcycles(job_status, ess_trsh_methods, trsh_keyword, couldnt_trsh) -> tuple[list, list, bool]: +def trsh_keyword_opt_maxcycles(job_status, ess_trsh_methods, trsh_keyword, couldnt_trsh, is_ts=False, fine=False) -> tuple[list, list, bool]: """ - Check if the job requires change of opt(maxcycle=200) + Escalate the remedy for an optimization that hit its cycle limit (MaxOptCycles), one step + per retry. The ladder recomputes the Hessian before flipping the step algorithm: + + 1. opt=(maxcycle=200) - simply allow more cycles (cheapest). + 1b. opt=(cartesian) - fine TS opt ONLY (is_ts and fine): a fine TS opt that dead-ends on + the l9999 "Optimization stopped" redundant-internal-coordinate + oscillation is far more likely to be a coordinate-system pathology + than a step/Hessian-quality problem, so switch to Cartesian + coordinates (which sidestep the collinear/redundant-coordinate + degeneracy) BEFORE spending expensive recalcfc/calcall/RFO cycles. + Tried once (guarded by 'cartesian' not in ess_trsh_methods) and + carried through the rest of the ladder via the opt-route merge. + Gated tightly to the fine TS opt so ground-state (is_ts=False) and + coarse TS opt (fine=False) ladders are unchanged. + 2. opt=(recalcfc=5) - recompute the exact Hessian every 5 steps. Recomputing force + constants is the first-line remedy for a stuck optimization, and + is essential for a TS opt where following the single negative + eigenvalue depends entirely on Hessian quality. + 3. opt=(calcall) - recompute the Hessian at every step (expensive last resort). + 4. opt=(RFO) - rational-function / eigenvector-following step. Correct for both + minima and TS. + 5. opt=(GDIIS) - minimization only. GDIIS is a DIIS step accelerator that can walk + 6. opt=(GEDIIS) a TS opt downhill into a nearby minimum and lose the saddle, so + these are skipped for TS optimizations (is_ts=True). """ opt_pattern = r"opt=\((.*?)\)" - if 'MaxOptCycles' in job_status['keywords'] and 'opt=(maxcycle=200)' not in ess_trsh_methods: - ess_trsh_methods.append('opt=(maxcycle=200)') - trsh_keyword.append('opt=(maxcycle=200)') - couldnt_trsh = False - elif 'MaxOptCycles' in job_status['keywords'] and 'opt=(RFO)' not in ess_trsh_methods: - ess_trsh_methods.append('opt=(RFO)') - trsh_keyword.append('opt=(RFO)') - couldnt_trsh = False - elif 'MaxOptCycles' in job_status['keywords'] and 'opt=(RFO)' in ess_trsh_methods and 'opt=(GDIIS)' not in ess_trsh_methods: - ess_trsh_methods.append('opt=(GDIIS)') - trsh_keyword.append('opt=(GDIIS)') - couldnt_trsh = False - elif 'MaxOptCycles' in job_status['keywords'] and 'opt=(RFO)' in ess_trsh_methods and 'opt=(GDIIS)' in ess_trsh_methods and 'opt=(GEDIIS)' not in ess_trsh_methods: - ess_trsh_methods.append('opt=(GEDIIS)') - trsh_keyword.append('opt=(GEDIIS)') - couldnt_trsh = False - + if 'MaxOptCycles' in job_status['keywords']: + if 'opt=(maxcycle=200)' not in ess_trsh_methods: + ess_trsh_methods.append('opt=(maxcycle=200)') + trsh_keyword.append('opt=(maxcycle=200)') + couldnt_trsh = False + elif is_ts and fine and 'cartesian' not in ess_trsh_methods: + # Fine TS opt oscillation (l9999 "Optimization stopped"): try Cartesian coordinates + # early, before escalating the Hessian ladder. Stored as the bare 'cartesian' marker + # (consistent with trsh_keyword_cartesian) and folded into the opt route by the merge + # block below. + ess_trsh_methods.append('cartesian') + trsh_keyword.append('opt=(cartesian)') + couldnt_trsh = False + elif 'opt=(recalcfc=5)' not in ess_trsh_methods: + ess_trsh_methods.append('opt=(recalcfc=5)') + trsh_keyword.append('opt=(recalcfc=5)') + couldnt_trsh = False + elif 'opt=(calcall)' not in ess_trsh_methods: + ess_trsh_methods.append('opt=(calcall)') + trsh_keyword.append('opt=(calcall)') + couldnt_trsh = False + elif 'opt=(RFO)' not in ess_trsh_methods: + ess_trsh_methods.append('opt=(RFO)') + trsh_keyword.append('opt=(RFO)') + couldnt_trsh = False + elif not is_ts and 'opt=(GDIIS)' not in ess_trsh_methods: + ess_trsh_methods.append('opt=(GDIIS)') + trsh_keyword.append('opt=(GDIIS)') + couldnt_trsh = False + elif not is_ts and 'opt=(GDIIS)' in ess_trsh_methods and 'opt=(GEDIIS)' not in ess_trsh_methods: + ess_trsh_methods.append('opt=(GEDIIS)') + trsh_keyword.append('opt=(GEDIIS)') + couldnt_trsh = False + if any('opt' in keyword for keyword in ess_trsh_methods): opt_list = [match for element in ess_trsh_methods for match in re.findall(opt_pattern, element)] if any(re.search(opt_pattern, element) for element in ess_trsh_methods) else [] + # Fold the bare 'cartesian' marker (see the is_ts+fine branch above) into the merged opt + # route as a leading parameter so it survives alongside the maxcycle/Hessian/algorithm + # keywords rather than being clobbered by the route rewrite below. + if 'cartesian' in ess_trsh_methods and 'cartesian' not in opt_list: + opt_list.insert(0, 'cartesian') + if opt_list: - filtered_methods = prioritize_opt_methods(opt_list) + filtered_methods = prioritize_opt_methods(opt_list, is_ts=is_ts) new_opt_keyword = 'opt=(' + ','.join(filtered_methods) + ')' trsh_keyword = [kw if not kw.startswith('opt') else new_opt_keyword for kw in trsh_keyword] - + return ess_trsh_methods, trsh_keyword, couldnt_trsh @@ -2036,12 +2134,11 @@ def trsh_keyword_inaccurate_quadrature(job_status, ess_trsh_methods, trsh_keywor ess_trsh_methods.append('guess=INDO') trsh_keyword.append('guess=INDO') couldnt_trsh = False - elif 'InaccurateQuadrature' in job_status['keywords'] and 'int=grid=300590' in ess_trsh_methods and 'scf=(NoVarAcc)' in ess_trsh_methods and 'guess=INDO' in ess_trsh_methods and 'int=ultrafine' not in ess_trsh_methods: - # Try all methods above - trsh_keyword.append('int=grid=300590') - trsh_keyword.append('guess=INDO') - # NoVarAcc is not included in trsh_keyword, because it will be in ess_trsh_methods - + # Once int=grid=300590, scf=(NoVarAcc) and guess=INDO have all been tried, the ladder is + # exhausted: ess_trsh_methods stops changing and trsh_ess_job() terminates the retry loop + # via the 'all_attempted' marker. (There is no separate int=ultrafine step - the fine-grid + # remedy is handled by trsh_keyword_unconverged, and ultrafine is the Gaussian 16 default.) + scf_pattern = r"scf=\((.*?)\)" # e.g., scf=(xqc,MaxCycle=1000), will match xqc,MaxCycle=1000 if any('scf' in keyword for keyword in ess_trsh_methods): scf_list = [match for element in ess_trsh_methods for match in re.findall(scf_pattern, element)] if any(re.search(scf_pattern, element) for element in ess_trsh_methods) else [] @@ -2090,30 +2187,48 @@ def trsh_keyword_neg_eigen(job_status, ess_trsh_methods, trsh_keyword, couldnt_t return ess_trsh_methods, trsh_keyword, couldnt_trsh -def prioritize_opt_methods(opt_methods): +def prioritize_opt_methods(opt_methods, is_ts=False): + """ + Reduce an accumulated list of opt=(...) parameters to a self-consistent set: - preferred_order = ['GEDIIS', 'GDIIS', 'RFO'] - selected_method = None - - for method in preferred_order: - if method in opt_methods: - selected_method = method - break - - filtered_methods = [method for method in opt_methods if method not in preferred_order or method == selected_method] + - Keep a single step algorithm. For minimizations prefer GEDIIS > GDIIS > RFO. For a TS + optimization keep only RFO (eigenvector following); GDIIS/GEDIIS are minimization-oriented + DIIS accelerators that can drift off the saddle, so they are dropped. + - Keep a single Hessian-recompute directive, most aggressive wins: calcall > recalcfc=* > calcfc. + """ + all_algorithms = ['GEDIIS', 'GDIIS', 'RFO'] + preferred_order = ['RFO'] if is_ts else all_algorithms + selected_method = next((method for method in preferred_order if method in opt_methods), None) + methods = [method for method in opt_methods if method not in all_algorithms or method == selected_method] + + # Collapse conflicting force-constant recompute directives to the single most aggressive one. + has_calcall = 'calcall' in methods + has_recalcfc = any(method.startswith('recalcfc') for method in methods) - return filtered_methods + def _keep_fc(method: str) -> bool: + if method == 'calcfc': + return not (has_calcall or has_recalcfc) + if method.startswith('recalcfc'): + return not has_calcall + return True + + return [method for method in methods if _keep_fc(method)] def trsh_keyword_no_qc(job_status, ess_trsh_methods, trsh_keyword, couldnt_trsh) -> tuple[list, list, bool]: """ - When a job fails with no qc, there are two possible solutions based upon the error message: - 1. If SCF fails, then try to change the algorithm to LQA. - 2. If SCF fails, then try to change the algorithm to LQA. + Drop the quadratic-convergence SCF remedy after Gaussian failed in link 508. + + A previous retry added 'scf=(qc)' (which the Gaussian adapter upgrades to scf=(xqc)). + If the job then died in l508, the QC/XQC algorithm itself failed to converge, so remove + 'scf=(qc)' from the attempted methods and record 'no_xqc' instead. 'no_xqc' guards + trsh_keyword_scf() from re-adding 'scf=(qc)' (while still counting it as attempted for + the last-resort SCF methods) and tells the Gaussian adapter not to upgrade qc to xqc. """ if 'no_xqc' in job_status['keywords'] and 'scf=(qc)' in ess_trsh_methods: ess_trsh_methods.remove('scf=(qc)') - ess_trsh_methods.append('no_xqc') + if 'no_xqc' not in ess_trsh_methods: + ess_trsh_methods.append('no_xqc') couldnt_trsh = False return ess_trsh_methods, trsh_keyword, couldnt_trsh diff --git a/arc/job/trsh_test.py b/arc/job/trsh_test.py index 251a9742cf..9e14811d41 100644 --- a/arc/job/trsh_test.py +++ b/arc/job/trsh_test.py @@ -99,6 +99,20 @@ def test_determine_ess_status(self): self.assertIn("Error termination via Lnk1e", line) self.assertIn("g09/l401.exe", line) + # A GL401 "projection from the old to the new basis set has failed" error (guess=read from a + # checkpoint built with a different basis) is retryable by dropping the checkfile, so it is + # classified as CheckFile - NOT as a dead-end BasisSet error. + path = os.path.join(self.base_path["gaussian"], "l401_projection.out") + status, keywords, error, line = trsh.determine_ess_status( + output_path=path, species_label="Zr2O4H", job_type="opt" + ) + self.assertEqual(status, "errored") + self.assertEqual(keywords, ["CheckFile"]) + self.assertNotIn("BasisSet", keywords) + self.assertIn("projection from the old to the new basis set", error) + self.assertIn("removing the checkfile", error) + self.assertIn("g09/l401.exe", line) + path = os.path.join(self.base_path["gaussian"], "l9999.out") status, keywords, error, line = trsh.determine_ess_status( output_path=path, species_label="Zr2O4H", job_type="opt" @@ -463,10 +477,13 @@ def test_trsh_ess_job(self): self.assertTrue(all('trsh_attempt' not in e for e in output_errors)) # Gaussian: test 7 - part 2 - # verify troubleshoot attempts counting (consolidated) + # verify troubleshoot attempts counting (consolidated). Full minimization MaxOptCycles + # ladder (maxcycle -> recalcfc -> calcall -> RFO -> GDIIS -> GEDIIS) must be exhausted. job_status = {'keywords': ['MaxOptCycles', 'GL9999']} ess_trsh_methods = ['trsh_attempt', 'int=(Acc2E=14)', 'opt=(maxcycle=200)', + 'trsh_attempt', 'opt=(recalcfc=5)', + 'trsh_attempt', 'opt=(calcall)', 'trsh_attempt', 'opt=(RFO)', 'trsh_attempt', 'opt=(GDIIS)', 'trsh_attempt', 'opt=(GEDIIS)', @@ -477,9 +494,10 @@ def test_trsh_ess_job(self): num_heavy_atoms, cpu_cores, ess_trsh_methods) self.assertTrue(couldnt_trsh) e = output_errors[-1] - self.assertIn('Tried troubleshooting 5 time(s)', e) + self.assertIn('Tried troubleshooting 7 time(s)', e) self.assertNotIn('trsh_attempt', e) - for opt in ("opt=(maxcycle=200)", "opt=(RFO)", "opt=(GDIIS)", "opt=(GEDIIS)"): + for opt in ("opt=(maxcycle=200)", "opt=(recalcfc=5)", "opt=(calcall)", + "opt=(RFO)", "opt=(GDIIS)", "opt=(GEDIIS)"): self.assertIn(opt, e) self.assertIn('all_attempted', e) @@ -544,9 +562,8 @@ def test_trsh_ess_job(self): self.assertFalse(couldnt_trsh) self.assertIn('opt=(maxcycle=200)', ess_trsh_methods) - # Gaussian: test 10 - part 2 - # 'MaxOptCycles', 'GL9999' - # Adding RFO to opt + # Gaussian: test 10 - part 2 (minimization ladder) + # 'MaxOptCycles', 'GL9999' - Hessian recompute (recalcfc) comes before any algorithm flip job_status = {'keywords': ['MaxOptCycles', 'GL9999']} ess_trsh_methods = ['int=(Acc2E=14)', 'opt=(maxcycle=200)'] output_errors, ess_trsh_methods, remove_checkfile, level_of_theory, software, job_type, fine, trsh_keyword, \ @@ -554,49 +571,47 @@ def test_trsh_ess_job(self): job_type, software, fine, memory_gb, num_heavy_atoms, cpu_cores, ess_trsh_methods) self.assertFalse(couldnt_trsh) - self.assertIn('opt=(maxcycle=200)', ess_trsh_methods) - self.assertIn('opt=(RFO)', ess_trsh_methods) - self.assertIn('opt=(maxcycle=200,RFO)', trsh_keyword) - - # Gaussian: test 10 - part 3 - # 'MaxOptCycles', 'GL9999' - # Adding GDIIS to opt - # Removing RFO from opt + self.assertIn('opt=(recalcfc=5)', ess_trsh_methods) + self.assertNotIn('opt=(RFO)', ess_trsh_methods) # algorithm flip must NOT precede Hessian recompute + self.assertIn('opt=(maxcycle=200,recalcfc=5)', trsh_keyword) + + # Gaussian: test 10 - part 3 - calcall (most aggressive Hessian) supersedes recalcfc in the route job_status = {'keywords': ['MaxOptCycles', 'GL9999']} - ess_trsh_methods = ['int=(Acc2E=14)', 'opt=(maxcycle=200)', 'opt=(RFO)'] + ess_trsh_methods = ['int=(Acc2E=14)', 'opt=(maxcycle=200)', 'opt=(recalcfc=5)'] output_errors, ess_trsh_methods, remove_checkfile, level_of_theory, software, job_type, fine, trsh_keyword, \ memory, shift, cpu_cores, couldnt_trsh = 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) self.assertFalse(couldnt_trsh) - self.assertIn('opt=(maxcycle=200)', ess_trsh_methods) - self.assertIn('opt=(RFO)', ess_trsh_methods) - self.assertIn('opt=(GDIIS)', ess_trsh_methods) - self.assertIn('opt=(maxcycle=200,GDIIS)', trsh_keyword) - - # Gaussian: test 10 - part 4 - # 'MaxOptCycles', 'GL9999' - # Adding GEDIIS to opt - # Removing RFO from opt - # Removing GDIIS from opt + self.assertIn('opt=(calcall)', ess_trsh_methods) + self.assertIn('opt=(maxcycle=200,calcall)', trsh_keyword) # recalcfc collapsed into calcall + + # Gaussian: test 10 - part 4 - only now flip to RFO (eigenvector following) job_status = {'keywords': ['MaxOptCycles', 'GL9999']} - ess_trsh_methods = ['int=(Acc2E=14)', 'opt=(maxcycle=200)', 'opt=(RFO)', 'opt=(GDIIS)'] + ess_trsh_methods = ['int=(Acc2E=14)', 'opt=(maxcycle=200)', 'opt=(recalcfc=5)', 'opt=(calcall)'] output_errors, ess_trsh_methods, remove_checkfile, level_of_theory, software, job_type, fine, trsh_keyword, \ memory, shift, cpu_cores, couldnt_trsh = 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) self.assertFalse(couldnt_trsh) - self.assertIn('opt=(maxcycle=200)', ess_trsh_methods) self.assertIn('opt=(RFO)', ess_trsh_methods) + self.assertIn('opt=(maxcycle=200,calcall,RFO)', trsh_keyword) + + # Gaussian: test 10 - part 5 - GDIIS then GEDIIS (minimization only) + job_status = {'keywords': ['MaxOptCycles', 'GL9999']} + ess_trsh_methods = ['int=(Acc2E=14)', 'opt=(maxcycle=200)', 'opt=(recalcfc=5)', 'opt=(calcall)', 'opt=(RFO)'] + output_errors, ess_trsh_methods, remove_checkfile, level_of_theory, software, job_type, fine, trsh_keyword, \ + memory, shift, cpu_cores, couldnt_trsh = 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) + self.assertFalse(couldnt_trsh) self.assertIn('opt=(GDIIS)', ess_trsh_methods) - self.assertIn('opt=(GEDIIS)', ess_trsh_methods) - self.assertIn('opt=(maxcycle=200,GEDIIS)', trsh_keyword) - - # Gaussian: test 10 - part 5 - # 'MaxOptCycles', 'GL9999' - # Final test to ensure that it cannot troubleshoot the job further + self.assertIn('opt=(maxcycle=200,calcall,GDIIS)', trsh_keyword) + + # Gaussian: test 10 - part 6 - final step exhausts the minimization ladder job_status = {'keywords': ['MaxOptCycles', 'GL9999']} - ess_trsh_methods = ['int=(Acc2E=14)', 'opt=(maxcycle=200)', 'opt=(RFO)', 'opt=(GDIIS)', 'opt=(GEDIIS)'] + ess_trsh_methods = ['int=(Acc2E=14)', 'opt=(maxcycle=200)', 'opt=(recalcfc=5)', 'opt=(calcall)', + 'opt=(RFO)', 'opt=(GDIIS)', 'opt=(GEDIIS)'] output_errors, ess_trsh_methods, remove_checkfile, level_of_theory, software, job_type, fine, trsh_keyword, \ memory, shift, cpu_cores, couldnt_trsh = trsh.trsh_ess_job(label, level_of_theory, server, job_status, job_type, software, fine, memory_gb, @@ -834,6 +849,139 @@ def test_trsh_ess_job(self): num_heavy_atoms, cpu_cores, ess_trsh_methods, is_h=True, is_monoatomic=True) + def test_trsh_ess_job_gaussian_non_retryable_refusal(self): + """ + P1: Gaussian input/method errors that a resubmit cannot fix must refuse immediately + (couldnt_trsh=True) without appending int=(Acc2E=14) or any other remedy - previously + every Gaussian error picked up a spurious Acc2E=14 step and burned a resubmit. + """ + def call(job_status): + return trsh.trsh_ess_job('lbl', {'method': 'wb97xd', 'basis': 'def2tzvp'}, None, job_status, + 'opt', 'gaussian', False, 16, 2, 8, []) + non_retryable_cases = [ + (['Syntax'], 'There was a syntax error in the Gaussian input file.'), + (['InputError', 'GL101'], 'The blank line after the coordinate section is missing.'), + (['MP2', 'GL906'], 'The MP2 calculation has failed.'), + (['Scratch'], 'Wrongly specified the scratch directory.'), + ] + for keywords, error in non_retryable_cases: + out = call({'keywords': keywords, 'error': error}) + output_errors, ess_trsh_methods, couldnt_trsh = out[0], out[1], out[11] + self.assertTrue(couldnt_trsh, f'{keywords} should refuse (couldnt_trsh=True)') + self.assertEqual(ess_trsh_methods, [], f'{keywords} must not append any remedy') + self.assertTrue(any('non-retryable' in e for e in output_errors), + f'{keywords} should report a non-retryable error: {output_errors}') + + # ZMat (L716) is NOT non-retryable: it must get the opt=(cartesian) remedy, not a refusal. + zmat = call({'keywords': ['ZMat', 'GL716'], 'error': 'Angle in z-matrix outside the allowed range 0 < x < 180.'}) + self.assertFalse(zmat[11], 'ZMat must be troubleshot, not refused') + self.assertFalse(any('non-retryable' in e for e in zmat[0]), 'ZMat must not be reported non-retryable') + self.assertIn('cartesian', zmat[1]) + + # A genuine dead-end BasisSet error (GL301 "atomic number out of range") is STILL refused - + # via the dedicated BasisSet branch, not the non-retryable gate (so 'BasisSet' no longer + # needs to be in GAUSSIAN_NON_RETRYABLE_KEYWORDS). + self.assertNotIn('BasisSet', trsh.GAUSSIAN_NON_RETRYABLE_KEYWORDS) + bad_basis = call({'keywords': ['GL301', 'BasisSet'], + 'error': 'The basis set 6-311G is not appropriate for the this chemistry.'}) + self.assertTrue(bad_basis[11], 'genuine BasisSet dead-end must still be refused') + self.assertEqual(bad_basis[1], [], 'refused BasisSet must not append any remedy') + + # Legitimate, retryable classes must be UNAFFECTED (still get their remedies): + scf = call({'keywords': ['SCF', 'GL502', 'NoSymm'], 'error': 'Unconverged SCF'}) + self.assertFalse(scf[11]) + self.assertIn('scf=(qc)', scf[1]) + opt = call({'keywords': ['MaxOptCycles', 'GL9999'], 'error': 'steps exceeded'}) + self.assertFalse(opt[11]) + self.assertIn('opt=(maxcycle=200)', opt[1]) + + def test_trsh_ess_job_gaussian_zmat_cartesian_then_terminates(self): + """ + A ZMat (L716) error - the optimizer drove atoms collinear - must be troubleshot with + opt=(cartesian) on the first retry (same remedy L103 uses) and must terminate (not loop) + if it recurs after Cartesian has already been tried. Verified for both opt and conf_opt. + """ + for job_type in ('opt', 'conf_opt'): + ess_trsh_methods = list() + history = list() + for _ in range(5): + out = trsh.trsh_ess_job('lbl', {'method': 'wb97xd', 'basis': 'def2tzvp'}, None, + {'keywords': ['ZMat', 'GL716'], + 'error': 'Angle in z-matrix outside the allowed range 0 < x < 180.'}, + job_type, 'gaussian', False, 16, 2, 8, ess_trsh_methods) + ess_trsh_methods, trsh_keyword, couldnt_trsh = out[1], out[7], out[11] + history.append((list(ess_trsh_methods), list(trsh_keyword), couldnt_trsh)) + if couldnt_trsh or 'all_attempted' in ess_trsh_methods: + break + # First retry: Cartesian recorded and emitted, job is resubmitted (couldnt_trsh False). + self.assertIn('cartesian', history[0][0], f'{job_type}: first ZMat retry must record cartesian') + self.assertIn('opt=(cartesian)', history[0][1], f'{job_type}: first ZMat retry must emit opt=(cartesian)') + self.assertFalse(history[0][2], f'{job_type}: first ZMat retry must resubmit, not give up') + # Second retry (ZMat recurs after cartesian): must terminate, no loop. + self.assertTrue(history[-1][2], f'{job_type}: recurring ZMat after cartesian must terminate') + self.assertIn('all_attempted', history[-1][0], f'{job_type}: must reach all_attempted') + self.assertLessEqual(len(history), 2, f'{job_type}: cartesian tried once then terminate: {history}') + + def test_trsh_ess_job_gaussian_optorientation_nosymm_then_terminates(self): + """ + An l202 "OptOrientation" error (the standard orientation / point group changed mid-opt) is a + symmetry glitch that nosymm cures - the same remedy l101/l103 use - so it must be troubleshot + with nosymm on the first retry, NOT refused as a non-retryable dead-end, and must terminate + (not loop) if it recurs after nosymm has already been applied. + """ + # OptOrientation must no longer be gated as a non-retryable dead-end. + self.assertNotIn('OptOrientation', trsh.GAUSSIAN_NON_RETRYABLE_KEYWORDS) + ess_trsh_methods, history = list(), list() + for _ in range(5): + out = trsh.trsh_ess_job('lbl', {'method': 'wb97xd', 'basis': 'def2tzvp'}, None, + {'keywords': ['OptOrientation', 'GL202', 'NoSymm'], + 'error': 'During the optimization process, either the standard ' + 'orientation or the point group of the molecule has changed.'}, + 'opt', 'gaussian', False, 16, 2, 8, ess_trsh_methods) + output_errors, ess_trsh_methods, trsh_keyword, couldnt_trsh = out[0], out[1], out[7], out[11] + history.append((list(ess_trsh_methods), list(trsh_keyword), couldnt_trsh, + any('non-retryable' in e for e in output_errors))) + if couldnt_trsh or 'all_attempted' in ess_trsh_methods: + break + # First retry: nosymm recorded and emitted exactly once, job resubmitted, NOT refused. + self.assertIn('NoSymm', history[0][0], 'first OptOrientation retry must record NoSymm') + self.assertEqual(history[0][1].count('nosymm'), 1, 'nosymm must be emitted exactly once') + self.assertFalse(history[0][2], 'first OptOrientation retry must resubmit, not give up') + self.assertFalse(history[0][3], 'OptOrientation must not be reported non-retryable') + # Recurrence after nosymm already applied: terminate, no loop. + self.assertTrue(history[-1][2], 'recurring OptOrientation after nosymm must terminate') + self.assertIn('all_attempted', history[-1][0]) + self.assertLessEqual(len(history), 2, f'nosymm tried once then terminate: {history}') + + def test_trsh_ess_job_gaussian_gl401_projection_checkfile_then_terminates(self): + """ + A GL401 "projection from the old to the new basis set has failed" error is reclassified as + CheckFile (see determine_ess_status), so trsh_ess_job must remove the checkfile (drop + guess=read) and resubmit - NOT refuse it as a dead-end BasisSet error - and must terminate + (not loop) if it recurs after the checkfile was already removed. + """ + error = ('The projection from the old to the new basis set has failed; ' + 'removing the checkfile to restart from a fresh SCF guess.') + ess_trsh_methods, history = list(), list() + for _ in range(5): + out = trsh.trsh_ess_job('lbl', {'method': 'wb97xd', 'basis': 'def2tzvp'}, None, + {'keywords': ['CheckFile'], 'error': error}, + 'opt', 'gaussian', False, 16, 2, 8, ess_trsh_methods) + output_errors, ess_trsh_methods, remove_checkfile, couldnt_trsh = out[0], out[1], out[2], out[11] + history.append((list(ess_trsh_methods), remove_checkfile, couldnt_trsh, + any('non-retryable' in e for e in output_errors))) + if couldnt_trsh or 'all_attempted' in ess_trsh_methods: + break + # First pass: checkfile removed, job resubmitted, NOT refused. + self.assertTrue(history[0][1], 'first pass must remove the checkfile') + self.assertIn('checkfile=None', history[0][0]) + self.assertFalse(history[0][2], 'first pass must resubmit, not give up') + self.assertFalse(history[0][3], 'GL401 projection must not be reported non-retryable') + # Recurrence after checkfile already removed: terminate, no loop. + self.assertTrue(history[-1][2], 'recurring GL401 after checkfile removal must terminate') + self.assertIn('all_attempted', history[-1][0]) + self.assertLessEqual(len(history), 2, f'checkfile removed once then terminate: {history}') + def test_trsh_ess_job_terachem_trsh_attempt_only(self): """Isolate the terachem trsh_attempt-only case from Gaussian stateful flow.""" label = 'ethanol' @@ -932,6 +1080,200 @@ def test_trsh_ess_job_gaussian_memory_alternation_terminates(self): break self.assertTrue(couldnt_trsh) + def test_trsh_ess_job_gaussian_scf_qc_progression(self): + """ + Simulate a Gaussian job that persistently fails SCF convergence, feeding the returned + ess_trsh_methods back into trsh_ess_job() as the scheduler does on each retry: + - the job fails in l508 (the scf=(qc)/(xqc) quadratic-convergence link) whenever + 'scf=(qc)' is part of the attempted methods (the adapter upgrades qc to xqc), and + - fails in l502 (plain SCF convergence) otherwise. + + The cycle must progress monotonically: + - 'no_xqc' is recorded exactly once, + - 'scf=(qc)' is never re-added after 'no_xqc' has been recorded (no qc/no_xqc oscillation), + - the rest of the SCF ladder (NDamp, NoDIIS, guess=INDO, and the Fermi/Noincfock/NoVarAcc + last resort) is actually reached before the cycle declares 'all_attempted'. + """ + label = 'ethanol' + level_of_theory = {'method': 'wb97xd', 'basis': 'def2tzvp'} + server = None # server-independent: the SCF cycle under test never consults server settings + job_type = 'opt' + software = 'gaussian' + fine = False + memory_gb = 16 + num_heavy_atoms = 2 + cpu_cores = 8 + scf_status = {'keywords': ['SCF', 'GL502', 'NoSymm'], 'error': 'Unconverged SCF'} + xqc_status = {'keywords': ['no_xqc', 'GL508'], 'error': 'Unconverged'} + ess_trsh_methods = list() + for _ in range(30): + job_status = xqc_status if 'scf=(qc)' in ess_trsh_methods else scf_status + output_errors, ess_trsh_methods, remove_checkfile, level_of_theory, software, job_type, fine, \ + trsh_keyword, memory, shift, cpu_cores, couldnt_trsh = \ + 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) + self.assertLessEqual(ess_trsh_methods.count('no_xqc'), 1, + f'no_xqc was re-appended (oscillation): {ess_trsh_methods}') + if 'no_xqc' in ess_trsh_methods: + self.assertNotIn('scf=(qc)', ess_trsh_methods, + f'scf=(qc) was re-added after no_xqc: {ess_trsh_methods}') + if couldnt_trsh or 'all_attempted' in ess_trsh_methods: + break + else: + self.fail(f'Gaussian SCF troubleshooting cycle did not terminate: {ess_trsh_methods}') + self.assertIn('no_xqc', ess_trsh_methods) + for method in ('scf=(NDamp=30)', 'scf=(NoDIIS)', 'guess=INDO', + 'scf=(Fermi)', 'scf=(Noincfock)', 'scf=(NoVarAcc)'): + self.assertIn(method, ess_trsh_methods, + f'the SCF ladder never reached {method}: {ess_trsh_methods}') + + def _run_gaussian_ladder(self, keywords, error, n=10, is_ts=False, fine=False): + """ + Drive trsh_ess_job() repeatedly for a Gaussian job that keeps failing with the same + (keywords, error), feeding ess_trsh_methods back in as the scheduler does. Returns the + list of (ess_trsh_methods snapshot, couldnt_trsh) tuples, one per retry, stopping once + the cycle gives up or emits 'all_attempted'. + """ + job_status = {'keywords': keywords, 'error': error} + ess_trsh_methods, history = list(), list() + for _ in range(n): + out = trsh.trsh_ess_job('lbl', {'method': 'wb97xd', 'basis': 'def2tzvp'}, None, job_status, + 'opt', 'gaussian', fine, 16, 2, 8, ess_trsh_methods, is_ts=is_ts) + ess_trsh_methods, fine, couldnt_trsh = out[1], out[6], out[11] + history.append((list(ess_trsh_methods), couldnt_trsh)) + if couldnt_trsh or 'all_attempted' in ess_trsh_methods: + break + return history + + @staticmethod + def _distinct_opts(history): + """Distinct opt=(...) remedies in first-seen (escalation) order across a ladder history.""" + distinct = [] + for snap, _ in history: + for m in snap: + if m.startswith('opt=') and m not in distinct: + distinct.append(m) + return distinct + + def test_trsh_ess_job_gaussian_inaccurate_quadrature_ladder(self): + """ + Pin the InaccurateQuadrature (Gaussian l502 CalDSu) remedy ladder so the removal of the + dead 'int=ultrafine' branch in trsh_keyword_inaccurate_quadrature is provably behavior + preserving. Expected escalation, one remedy per retry, then termination: + int=grid=300590 -> scf=(NoVarAcc) -> guess=INDO -> all_attempted. + """ + history = self._run_gaussian_ladder(['InaccurateQuadrature', 'GL502'], 'Inaccurate quadrature in CalDSu') + ess_snapshots = [snap for snap, _ in history] + self.assertEqual(ess_snapshots, [ + ['int=(Acc2E=14)', 'int=grid=300590'], + ['int=(Acc2E=14)', 'int=grid=300590', 'scf=(NoVarAcc)'], + ['int=(Acc2E=14)', 'int=grid=300590', 'scf=(NoVarAcc)', 'guess=INDO'], + ['int=(Acc2E=14)', 'int=grid=300590', 'scf=(NoVarAcc)', 'guess=INDO', 'all_attempted'], + ]) + self.assertTrue(history[-1][1]) # couldnt_trsh is True on the terminal retry + + def test_trsh_ess_job_gaussian_maxoptcycles_ladder_minimization(self): + """ + P3: MaxOptCycles ladder for a minimization (is_ts=False). Recompute the Hessian before any + step-algorithm flip, and keep the DIIS accelerators (legitimate for minima): + maxcycle=200 -> recalcfc=5 -> calcall -> RFO -> GDIIS -> GEDIIS -> all_attempted. + """ + history = self._run_gaussian_ladder(['MaxOptCycles', 'GL9999'], 'Maximum optimization cycles reached.') + self.assertEqual([snap for snap, _ in history][0], ['int=(Acc2E=14)', 'opt=(maxcycle=200)']) + self.assertEqual(self._distinct_opts(history), + ['opt=(maxcycle=200)', 'opt=(recalcfc=5)', 'opt=(calcall)', + 'opt=(RFO)', 'opt=(GDIIS)', 'opt=(GEDIIS)']) + self.assertIn('all_attempted', history[-1][0]) + + def test_trsh_ess_job_gaussian_maxoptcycles_ladder_ts(self): + """ + P3: MaxOptCycles ladder for a TS (is_ts=True). Hessian recompute comes before the algorithm + flip, and GDIIS/GEDIIS (minimization DIIS accelerators that can fall off the saddle) are + NOT applied - the ladder stops at RFO: + maxcycle=200 -> recalcfc=5 -> calcall -> RFO -> all_attempted. + """ + history = self._run_gaussian_ladder(['MaxOptCycles', 'GL9999'], + 'Maximum optimization cycles reached.', is_ts=True) + distinct = self._distinct_opts(history) + self.assertEqual(distinct, ['opt=(maxcycle=200)', 'opt=(recalcfc=5)', 'opt=(calcall)', 'opt=(RFO)']) + self.assertNotIn('opt=(GDIIS)', distinct) + self.assertNotIn('opt=(GEDIIS)', distinct) + # Hessian recompute must precede the RFO algorithm flip: + self.assertLess(distinct.index('opt=(recalcfc=5)'), distinct.index('opt=(RFO)')) + self.assertLess(distinct.index('opt=(calcall)'), distinct.index('opt=(RFO)')) + self.assertIn('all_attempted', history[-1][0]) + + @staticmethod + def _maxoptcycles_trsh(ess_trsh_methods, is_ts, fine): + """One trsh_ess_job() retry for a MaxOptCycles (l9999) Gaussian opt; returns + (ess_trsh_methods, trsh_keyword, couldnt_trsh).""" + job_status = {'keywords': ['MaxOptCycles', 'GL9999'], + 'error': 'Maximum optimization cycles reached.'} + out = trsh.trsh_ess_job('lbl', {'method': 'wb97xd', 'basis': 'def2tzvp'}, None, job_status, + 'opt', 'gaussian', fine, 16, 2, 8, ess_trsh_methods, is_ts=is_ts) + return out[1], out[7], out[11] + + def test_trsh_ess_job_gaussian_maxoptcycles_ladder_ts_fine_cartesian(self): + """ + A FINE TS opt (is_ts=True, fine=True) that dead-ends on the l9999 "Optimization stopped" + MaxOptCycles oscillation must switch to Cartesian coordinates EARLY - after the cheap + maxcycle bump but before the expensive Hessian/algorithm escalation: + maxcycle=200 -> cartesian -> recalcfc=5 -> calcall -> RFO -> all_attempted. + Cartesian is tried exactly once and is carried through (merged into) the rest of the route. + """ + history = self._run_gaussian_ladder(['MaxOptCycles', 'GL9999'], + 'Maximum optimization cycles reached.', + is_ts=True, fine=True) + snapshots = [snap for snap, _ in history] + final_methods = snapshots[-1] + # Cartesian is recorded (bare marker), exactly once, and after the maxcycle bump. + self.assertIn('cartesian', final_methods, + f'cartesian was never tried for the fine TS opt oscillation: {final_methods}') + self.assertEqual(final_methods.count('cartesian'), 1, + f'cartesian must be recorded exactly once: {final_methods}') + self.assertLess(final_methods.index('opt=(maxcycle=200)'), final_methods.index('cartesian')) + self.assertLess(final_methods.index('cartesian'), final_methods.index('opt=(recalcfc=5)')) + # The route keyword emitted on the retry that introduces cartesian merges it with maxcycle. + ess, trsh_keyword, couldnt = self._maxoptcycles_trsh( + ['int=(Acc2E=14)', 'opt=(maxcycle=200)'], is_ts=True, fine=True) + self.assertIn('cartesian', ess) + self.assertFalse(couldnt) + self.assertIn('opt=(cartesian,maxcycle=200)', trsh_keyword) + # The ladder still terminates and GDIIS/GEDIIS stay excluded for a TS. + self.assertIn('all_attempted', final_methods) + self.assertTrue(history[-1][1]) + self.assertFalse(any('GDIIS' in m for m in final_methods)) + + def test_trsh_ess_job_gaussian_maxoptcycles_cartesian_gated(self): + """ + The fine-TS-opt Cartesian remedy must NOT leak into the ground-state opt ladder + (is_ts=False) nor the coarse TS opt ladder (fine=False) - both are unchanged. The + recalcfc Hessian recompute (not cartesian) must be the second rung in those cases. + """ + for is_ts, fine, tag in [(False, True, 'ground-state fine opt'), + (True, False, 'coarse TS opt'), + (False, False, 'ground-state coarse opt')]: + ess, trsh_keyword, couldnt = self._maxoptcycles_trsh( + ['int=(Acc2E=14)', 'opt=(maxcycle=200)'], is_ts=is_ts, fine=fine) + self.assertNotIn('cartesian', ess, f'cartesian must not fire for a {tag}: {ess}') + self.assertNotIn('opt=(cartesian)', trsh_keyword, + f'cartesian route must not appear for a {tag}: {trsh_keyword}') + self.assertIn('opt=(recalcfc=5)', ess, f'{tag} must escalate to recalcfc: {ess}') + + def test_prioritize_opt_methods_ts_vs_min(self): + """P3 unit: TS keeps only RFO and the most aggressive Hessian directive; min keeps GEDIIS.""" + acc = ['maxcycle=200', 'recalcfc=5', 'calcall', 'RFO', 'GDIIS', 'GEDIIS', 'calcfc'] + ts = trsh.prioritize_opt_methods(list(acc), is_ts=True) + self.assertIn('RFO', ts) + self.assertNotIn('GDIIS', ts) + self.assertNotIn('GEDIIS', ts) + self.assertIn('calcall', ts) + self.assertNotIn('recalcfc=5', ts) # calcall (more aggressive) wins + self.assertNotIn('calcfc', ts) + min_ = trsh.prioritize_opt_methods(list(acc), is_ts=False) + self.assertEqual([m for m in min_ if m in ('GEDIIS', 'GDIIS', 'RFO')], ['GEDIIS']) + 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') diff --git a/arc/scheduler.py b/arc/scheduler.py index 3c27829cf5..ecf90f0f72 100644 --- a/arc/scheduler.py +++ b/arc/scheduler.py @@ -3926,6 +3926,7 @@ def troubleshoot_ess(self, job_status=job.job_status[1], is_h=is_h, is_monoatomic=self.species_dict[label].is_monoatomic(), + is_ts=self.species_dict[label].is_ts, job_type=job.job_type, num_heavy_atoms=self.species_dict[label].number_of_heavy_atoms, software=job.job_adapter, diff --git a/arc/testing/trsh/gaussian/l401_projection.out b/arc/testing/trsh/gaussian/l401_projection.out new file mode 100644 index 0000000000..6f90beb66e --- /dev/null +++ b/arc/testing/trsh/gaussian/l401_projection.out @@ -0,0 +1,237 @@ + Entering Gaussian System, Link 0=g09 + Initial command: + /usr/local/g09/l1.exe "/gtmp/calvin.p/scratch/g09/387588.zeus-master/Gau-642.inp" -scrdir="/gtmp/calvin.p/scratch/g09/387588.zeus-master/" + Entering Link 1 = /usr/local/g09/l1.exe PID= 643. + + Copyright (c) 1988,1990,1992,1993,1995,1998,2003,2009,2013, + Gaussian, Inc. All Rights Reserved. + + This is part of the Gaussian(R) 09 program. It is based on + the Gaussian(R) 03 system (copyright 2003, Gaussian, Inc.), + the Gaussian(R) 98 system (copyright 1998, Gaussian, Inc.), + the Gaussian(R) 94 system (copyright 1995, Gaussian, Inc.), + the Gaussian 92(TM) system (copyright 1992, Gaussian, Inc.), + the Gaussian 90(TM) system (copyright 1990, Gaussian, Inc.), + the Gaussian 88(TM) system (copyright 1988, Gaussian, Inc.), + the Gaussian 86(TM) system (copyright 1986, Carnegie Mellon + University), and the Gaussian 82(TM) system (copyright 1983, + Carnegie Mellon University). Gaussian is a federally registered + trademark of Gaussian, Inc. + + This software contains proprietary and confidential information, + including trade secrets, belonging to Gaussian, Inc. + + This software is provided under written license and may be + used, copied, transmitted, or stored only in accord with that + written license. + + The following legend is applicable only to US Government + contracts under FAR: + + RESTRICTED RIGHTS LEGEND + + Use, reproduction and disclosure by the US Government is + subject to restrictions as set forth in subparagraphs (a) + and (c) of the Commercial Computer Software - Restricted + Rights clause in FAR 52.227-19. + + Gaussian, Inc. + 340 Quinnipiac St., Bldg. 40, Wallingford CT 06492 + + + --------------------------------------------------------------- + Warning -- This program may not be used in any manner that + competes with the business of Gaussian, Inc. or will provide + assistance to any competitor of Gaussian, Inc. The licensee + of this program is prohibited from giving any competitor of + Gaussian, Inc. access to this program. By using this program, + the user acknowledges that Gaussian, Inc. is engaged in the + business of creating and licensing software in the field of + computational chemistry and represents and warrants to the + licensee that it is not a competitor of Gaussian, Inc. and that + it will not use this program in any manner prohibited above. + --------------------------------------------------------------- + + + Cite this work as: + Gaussian 09, Revision D.01, + M. J. Frisch, G. W. Trucks, H. B. Schlegel, G. E. Scuseria, + M. A. Robb, J. R. Cheeseman, G. Scalmani, V. Barone, B. Mennucci, + G. A. Petersson, H. Nakatsuji, M. Caricato, X. Li, H. P. Hratchian, + A. F. Izmaylov, J. Bloino, G. Zheng, J. L. Sonnenberg, M. Hada, + M. Ehara, K. Toyota, R. Fukuda, J. Hasegawa, M. Ishida, T. Nakajima, + Y. Honda, O. Kitao, H. Nakai, T. Vreven, J. A. Montgomery, Jr., + J. E. Peralta, F. Ogliaro, M. Bearpark, J. J. Heyd, E. Brothers, + K. N. Kudin, V. N. Staroverov, T. Keith, R. Kobayashi, J. Normand, + K. Raghavachari, A. Rendell, J. C. Burant, S. S. Iyengar, J. Tomasi, + M. Cossi, N. Rega, J. M. Millam, M. Klene, J. E. Knox, J. B. Cross, + V. Bakken, C. Adamo, J. Jaramillo, R. Gomperts, R. E. Stratmann, + O. Yazyev, A. J. Austin, R. Cammi, C. Pomelli, J. W. Ochterski, + R. L. Martin, K. Morokuma, V. G. Zakrzewski, G. A. Voth, + P. Salvador, J. J. Dannenberg, S. Dapprich, A. D. Daniels, + O. Farkas, J. B. Foresman, J. V. Ortiz, J. Cioslowski, + and D. J. Fox, Gaussian, Inc., Wallingford CT, 2013. + + ****************************************** + Gaussian 09: EM64L-G09RevD.01 24-Apr-2013 + 24-Apr-2023 + ****************************************** + %chk=check.chk + %mem=4096mb + %NProcShared=16 + Will use up to 16 processors via shared memory. + ---------------------------------------------------------------------- + #P opt=(calcfc, tight, maxstep=5) guess=read ub2plypd3/def2tzvp scf=(t + ight, direct) integral=(grid=ultrafine, Acc2E=12) IOp(2/9=2000) scf=xq + c + ---------------------------------------------------------------------- + 1/7=10,8=5,10=4,18=20,19=15,38=1/1,3; + 2/9=2000,12=2,17=6,18=5,40=1/2; + 3/5=44,7=101,11=2,16=1,25=1,30=1,71=2,74=-60,75=-5,116=2,140=1/1,2,3; + 4/5=1/1; + 5/5=2,8=3,13=1,32=2,38=6,87=12/2,8; + 8/6=3,8=1,10=1,19=11,30=-1,87=12/1; + 9/15=3,16=-3,87=12/6; + 11/6=1,8=1,15=11,17=12,24=-1,27=1,28=-2,29=300,32=6,42=3,87=12/1,2,10; + 10/6=2,21=1,87=12/2; + 8/6=4,8=1,10=1,19=11,30=-1,87=12/11,4; + 10/5=1,20=4,87=12/2; + 11/12=2,14=11,16=1,17=2,28=-2,42=3,87=12/2,10,12; + 6/7=2,8=2,9=2,10=2/1; + 7/10=1,12=2,25=1,44=2,87=12/1,2,3,16; + 1/7=10,8=5,10=4,18=20,19=15/3(2); + 2/9=2000/2; + 99//99; + 2/9=2000/2; + 3/5=44,7=101,11=2,16=1,25=1,30=1,71=1,74=-60,75=-5,116=2/1,2,3; + 4/5=5,16=3,69=1/1; + 5/5=2,8=3,13=1,32=2,38=5,87=12/2,8; + 8/6=4,10=1,87=12/1; + 9/15=2,16=-3,87=12/6; + 10/5=1,87=12/2; + 7/12=2,87=12/1,2,3,16; + 1/7=10,8=5,18=20,19=15/3(-8); + 2/9=2000/2; + 6/7=2,8=2,9=2,10=2/1; + 99//99; + Leave Link 1 at Mon Apr 24 20:17:17 2023, MaxMem= 536870912 cpu: 0.7 + (Enter /usr/local/g09/l101.exe) + --- + NH2 + --- + Symbolic Z-matrix: + Charge = 0 Multiplicity = 2 + N 0.00024 0.4234 0. + H -0.80505 -0.21124 0. + H 0.80481 -0.21216 0. + + NAtoms= 3 NQM= 3 NQMF= 0 NMMI= 0 NMMIF= 0 + NMic= 0 NMicF= 0. + Isotopes and Nuclear Properties: + (Nuclear quadrupole moments (NQMom) in fm**2, nuclear magnetic moments (NMagM) + in nuclear magnetons) + + Atom 1 2 3 + IAtWgt= 14 1 1 + AtmWgt= 14.0030740 1.0078250 1.0078250 + NucSpn= 2 1 1 + AtZEff= 0.0000000 0.0000000 0.0000000 + NQMom= 2.0440000 0.0000000 0.0000000 + NMagM= 0.4037610 2.7928460 2.7928460 + AtZNuc= 7.0000000 1.0000000 1.0000000 + Leave Link 101 at Mon Apr 24 20:17:17 2023, MaxMem= 536870912 cpu: 1.8 + (Enter /usr/local/g09/l103.exe) + + GradGradGradGradGradGradGradGradGradGradGradGradGradGradGradGradGradGrad + Berny optimization. + Initialization pass. + ---------------------------- + ! Initial Parameters ! + ! (Angstroms and Degrees) ! + -------------------------- -------------------------- + ! Name Definition Value Derivative Info. ! + -------------------------------------------------------------------------------- + ! R1 R(1,2) 1.0253 calculate D2E/DX2 analytically ! + ! R2 R(1,3) 1.0253 calculate D2E/DX2 analytically ! + ! A1 A(2,1,3) 103.4527 calculate D2E/DX2 analytically ! + -------------------------------------------------------------------------------- + Trust Radius=5.00D-02 FncErr=1.00D-07 GrdErr=1.00D-07 + Number of steps in this run= 20 maximum allowed number of steps= 100. + GradGradGradGradGradGradGradGradGradGradGradGradGradGradGradGradGradGrad + + Leave Link 103 at Mon Apr 24 20:17:17 2023, MaxMem= 536870912 cpu: 0.1 + (Enter /usr/local/g09/l202.exe) + Input orientation: + --------------------------------------------------------------------- + Center Atomic Atomic Coordinates (Angstroms) + Number Number Type X Y Z + --------------------------------------------------------------------- + 1 7 0 0.000243 0.423397 0.000000 + 2 1 0 -0.805053 -0.211237 0.000000 + 3 1 0 0.804810 -0.212160 0.000000 + --------------------------------------------------------------------- + Distance matrix (angstroms): + 1 2 3 + 1 N 0.000000 + 2 H 1.025311 0.000000 + 3 H 1.025310 1.609863 0.000000 + Stoichiometry H2N(2) + Framework group CS[SG(H2N)] + Deg. of freedom 3 + Full point group CS NOp 2 + Largest Abelian subgroup CS NOp 2 + Largest concise Abelian subgroup C1 NOp 1 + Standard orientation: + --------------------------------------------------------------------- + Center Atomic Atomic Coordinates (Angstroms) + Number Number Type X Y Z + --------------------------------------------------------------------- + 1 7 0 0.000000 0.141132 0.000000 + 2 1 0 0.804932 -0.493964 0.000000 + 3 1 0 -0.804932 -0.493963 0.000000 + --------------------------------------------------------------------- + Rotational constants (GHZ): 711.0962360 386.9760326 250.6002638 + Leave Link 202 at Mon Apr 24 20:17:17 2023, MaxMem= 536870912 cpu: 0.1 + (Enter /usr/local/g09/l301.exe) + Standard basis: def2TZVP (5D, 7F) + Ernie: Thresh= 0.10000D-02 Tol= 0.10000D-05 Strict=F. + There are 35 symmetry adapted cartesian basis functions of A' symmetry. + There are 13 symmetry adapted cartesian basis functions of A" symmetry. + There are 31 symmetry adapted basis functions of A' symmetry. + There are 12 symmetry adapted basis functions of A" symmetry. + 43 basis functions, 67 primitive gaussians, 48 cartesian basis functions + 5 alpha electrons 4 beta electrons + nuclear repulsion energy 7.5543077488 Hartrees. + IExCor= 419 DFT=T Ex+Corr=B2PLYPD3 ExCW=0 ScaHFX= 0.530000 + ScaDFX= 0.470000 0.470000 0.730000 0.730000 ScalE2= 0.270000 0.270000 + IRadAn= 5 IRanWt= -1 IRanGd= 0 ICorTp=0 IEmpDi=141 + NAtoms= 3 NActive= 3 NUniq= 3 SFac= 1.00D+00 NAtFMM= 60 NAOKFM=F Big=F + Integral buffers will be 131072 words long. + Raffenetti 2 integral format. + Two-electron integral symmetry is turned on. + R6Disp: Grimme-D3(BJ) Dispersion energy= -0.0003067212 Hartrees. + Nuclear repulsion after empirical dispersion term = 7.5540010277 Hartrees. + Leave Link 301 at Mon Apr 24 20:17:17 2023, MaxMem= 536870912 cpu: 0.8 + (Enter /usr/local/g09/l302.exe) + NPDir=0 NMtPBC= 1 NCelOv= 1 NCel= 1 NClECP= 1 NCelD= 1 + NCelK= 1 NCelE2= 1 NClLst= 1 CellRange= 0.0. + One-electron integrals computed using PRISM. + 1 Symmetry operations used in ECPInt. + ECPInt: NShTT= 190 NPrTT= 556 LenC2= 191 LenP2D= 543. + LDataN: DoStor=T MaxTD1= 6 Len= 172 + NBasis= 43 RedAO= T EigKep= 8.62D-03 NBF= 31 12 + NBsUse= 43 1.00D-06 EigRej= -1.00D+00 NBFU= 31 12 + Precomputing XC quadrature grid using + IXCGrd= 4 IRadAn= 5 IRanWt= -1 IRanGd= 0 AccXCQ= 0.00D+00. + Generated NRdTot= 0 NPtTot= 0 NUsed= 0 NTot= 32 + NSgBfM= 48 48 48 48 48 MxSgAt= 3 MxSgA2= 3. + Leave Link 302 at Mon Apr 24 20:17:17 2023, MaxMem= 536870912 cpu: 1.9 + (Enter /usr/local/g09/l303.exe) + DipDrv: MaxL=1. + Leave Link 303 at Mon Apr 24 20:17:17 2023, MaxMem= 536870912 cpu: 0.3 + (Enter /usr/local/g09/l401.exe) + Initial guess from the checkpoint file: "check.chk" + The projection from the old to the new basis set has failed. + Error termination via Lnk1e in /usr/local/g09/l401.exe at Mon Apr 24 20:17:17 2023. + Job cpu time: 0 days 0 hours 0 minutes 6.6 seconds. + File lengths (MBytes): RWF= 5 Int= 0 D2E= 0 Chk= 1 Scr= 1