Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions arc/job/adapters/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import sys
import re

from pprint import pformat
from typing import TYPE_CHECKING

from arc.common import get_logger
Expand Down Expand Up @@ -470,21 +469,26 @@ def set_job_args(args: dict | None,
"""
Set the job args considering args from ``level`` and from ``trsh``.

The caller (e.g. :meth:`arc.scheduler.Scheduler.run_job`) is expected to
have already merged any ``level.args`` content into ``args`` before calling
this function — ``run_job`` does so via ``args.update(level.args)``. When
the caller passes empty ``args`` and the level supplies ``args``, we fall
back to ``level.args`` for convenience.

Args:
args (dict): The job specific arguments.
args (dict): The job-specific arguments.
level (Level): The level of theory.
job_name (str): The job name.

Returns:
dict: The initialized job specific arguments.
dict: The initialized job-specific arguments, guaranteed to carry the
``'keyword'``, ``'block'``, and ``'trsh'`` buckets (each a dict).
"""
# Ignore user-specified additional job arguments when troubleshooting.
if args is not None and args and any(val for val in args.values()) \
and level is not None and level.args and any(val for val in level.args.values()):
logger.warning(f'When troubleshooting {job_name}, ARC ignores the following user-specified options:\n'
f'{pformat(level.args)}')
elif not args and level is not None:
# Convenience fallback: empty (or None) caller-args inherits level.args.
if not args and level is not None and level.args is not None:
args = level.args
if args is None:
args = dict()
for key in ['keyword', 'block', 'trsh']:
if key not in args.keys():
args[key] = dict()
Expand Down
24 changes: 24 additions & 0 deletions arc/job/adapters/common_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
This module contains unit tests of the arc.job.adapters.common module
"""

import logging
import os
import shutil
import unittest
Expand Down Expand Up @@ -166,6 +167,29 @@ def test_set_job_args(self):
args = common.set_job_args(args={'keyword': 'k1'}, level=Level(repr='CBS-QB3'), job_name='j1')
self.assertEqual(args, {'keyword':'k1', 'block': dict(), 'trsh': dict()})

def test_set_job_args_no_spurious_warning_when_level_has_args(self):
"""Regression: the previous "ARC ignores user-specified options" warning
fired on every first-run job whose level carried args, because
``run_job`` had already merged ``level.args`` into ``args`` before
calling — nothing was actually being ignored. The warning should now
be silent on a normal first-run path."""
merged_args = {'keyword': {'core': 'core,0,0,0,0,0,0,0,0;'}, 'block': {}}
level_with_args = Level(method='ccsd(t)', basis='cc-pCVTZ',
args=merged_args)
with self.assertNoLogs(logger='arc', level=logging.WARNING):
result = common.set_job_args(args=merged_args,
level=level_with_args, job_name='j_first_run')
# Args content is preserved (not dropped).
self.assertEqual(result['keyword'], {'core': 'core,0,0,0,0,0,0,0,0;'})
self.assertEqual(result['trsh'], {}) # bucket added by guarantee

def test_set_job_args_args_none_preserves_level_args(self):
"""When the caller passes None, fall back to level.args (legacy convenience)."""
level = Level(method='ccsd(t)', basis='cc-pVTZ',
args={'keyword': {'general': 'foo'}, 'block': {}})
result = common.set_job_args(args=None, level=level, job_name='j1')
self.assertEqual(result['keyword'], {'general': 'foo'})

def test_which(self):
"""Test the which() function"""
ans = common.which(command='python', return_bool=True, raise_error=False)
Expand Down
43 changes: 42 additions & 1 deletion arc/job/adapters/molpro.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@
settings['default_job_settings'], settings['global_ess_settings'], settings['input_filenames'], \
settings['output_filenames'], settings['servers'], settings['submit_filenames']

# Methods that native Molpro does not support but its MRCC plugin does.
# When the level's method matches one of these (case-insensitive), the adapter
# emits a ``{mrcc,method=...}`` plugin call instead of a bare directive that
# Molpro's input parser would reject with "Unknown command or directive".
# Compared against the lowercased ``Level.method``.
MRCC_ROUTED_METHODS = frozenset({
'ccsdt',
'ccsdt(q)',
'ccsdtq',
'ccsdtq(p)',
'ccsdtqp',
})


input_template = """***,${label}
memory,Total=${memory},m;

Expand All @@ -47,7 +61,7 @@
${cabs}
int;

{hf;${shift}
{${hf_method};${shift}
maxit,999;
wf,spin=${spin},charge=${charge};
}
Expand Down Expand Up @@ -229,10 +243,37 @@ def write_input_file(self) -> None:
input_dict['spin'] = self.multiplicity - 1
input_dict['xyz'] = xyz_to_str(self.xyz)
input_dict['orbitals'] = '\ngprint,orbitals;\n'
input_dict['hf_method'] = 'hf' # default; overridden below for open-shell MRCC

if not is_restricted(self):
input_dict['restricted'] = 'u'

if self.level.method in MRCC_ROUTED_METHODS:
# Restriction is implicit from the preceding {hf;...} block; the
# MRCC plugin call does not accept a 'u'/'r' prefix.
input_dict['method'] = '{mrcc,method=' + self.level.method.upper() + '}'
input_dict['restricted'] = ''
if not is_restricted(self):
# Open-shell wavefunction + MRCC's approximate-CC family
# (CCSDT(Q), CCSDTQ(P), and the perturbative-(T) variants)
# refuses standard ROHF orbitals:
# "Approximate CC methods are not implemented for standard
# ROHF orbitals! Use semicanonical orbitals!"
# Solution: use UHF instead of (RO)HF as the SCF reference.
# UHF orbitals are semicanonical by construction (alpha and
# beta Fock matrices are separately diagonal) and live at the
# default record 2100.2, which MRCC reads. MRCC then reports
# ``Type=UHF/CANONICAL`` and accepts.
#
# An earlier attempt at this fix prepended ``{uccsd}`` to the
# MRCC call. {uccsd} does run UCCSD on top of ROHF, but the
# post-UCCSD canonical orbitals go to a separate record while
# the default 2100.2 still holds the original ROHF orbitals —
# MRCC reads 2100.2 by default and complained. Switching the
# SCF reference to UHF avoids this orbital-record bookkeeping
# entirely.
input_dict['hf_method'] = 'uhf'

# Job type specific options
if self.job_type in ['opt', 'optfreq', 'conf_opt']:
keywords = ['optg', 'root=2', 'method=qsd', 'readhess', "savexyz='geometry.xyz'"] if self.is_ts \
Expand Down
119 changes: 119 additions & 0 deletions arc/job/adapters/molpro_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,24 @@ def setUpClass(cls):
'closed': [1, 0, 0, 0, 0, 0, 0, 0]})],
testing=True,
)
cls.job_mrcc_ccsdt = MolproAdapter(execution_type='queue',
job_type='sp',
level=Level(method='CCSDT', basis='cc-pVDZ'),
project='test',
project_directory=os.path.join(ARC_TESTING_PATH,
'test_MolproAdapter_mrcc_ccsdt'),
species=[ARCSpecies(label='spc1', xyz=['O 0 0 1'], multiplicity=3)],
testing=True,
)
cls.job_mrcc_ccsdtq = MolproAdapter(execution_type='queue',
job_type='sp',
level=Level(method='CCSDT(Q)', basis='cc-pVDZ'),
project='test',
project_directory=os.path.join(ARC_TESTING_PATH,
'test_MolproAdapter_mrcc_ccsdtq'),
species=[ARCSpecies(label='spc1', xyz=['O 0 0 1'], multiplicity=1)],
testing=True,
)

def test_set_cpu_and_mem(self):
"""Test assigning number of cpu's and memory"""
Expand Down Expand Up @@ -441,6 +459,107 @@ def test_write_mrci_input_file(self):
"""
self.assertEqual(content_7, job_7_expected_input_file)

def test_write_input_file_mrcc_routing(self):
"""Methods unsupported by native Molpro but supported by MRCC are routed through the MRCC plugin.

For an open-shell wavefunction, the SCF reference is switched from
``{hf;...}`` (which gives Molpro's ROHF for open-shell) to
``{uhf;...}``. MRCC's approximate-CC family (``CCSDT(Q)``,
``CCSDTQ(P)``, and the perturbative-``(T)`` variants) refuses
standard ROHF orbitals with the error::

Approximate CC methods are not implemented for standard ROHF orbitals!
Use semicanonical orbitals!

UHF orbitals are semicanonical by construction (alpha and beta Fock
matrices are separately diagonal), saved to the default record 2100.2
which MRCC reads — MRCC then reports ``Type=UHF/CANONICAL`` and runs
the requested approximate-CC method.
"""
self.job_mrcc_ccsdt.cpu_cores = 48
self.job_mrcc_ccsdt.set_input_file_memory()
self.job_mrcc_ccsdt.write_input_file()
with open(os.path.join(self.job_mrcc_ccsdt.local_path,
input_filenames[self.job_mrcc_ccsdt.job_adapter]), 'r') as f:
content_ccsdt = f.read()
# spc1 has multiplicity=3 (open-shell triplet) — UHF reference expected.
expected_ccsdt = """***,spc1
memory,Total=438,m;

geometry={angstrom;
O 0.00000000 0.00000000 1.00000000}

gprint,orbitals;

basis=cc-pvdz



int;

{uhf;
maxit,999;
wf,spin=2,charge=0;
}

{mrcc,method=CCSDT}



---;

"""
self.assertEqual(content_ccsdt, expected_ccsdt)
# Sanity: the bare directive Molpro rejects must NOT appear on its own line.
self.assertNotIn('\nccsdt;\n', content_ccsdt)
self.assertNotIn('\nuccsdt;\n', content_ccsdt)
# An earlier (insufficient) fix used `{uccsd}` between HF and MRCC —
# this contract has been replaced with UHF, so {uccsd} must NOT appear.
self.assertNotIn('{uccsd}', content_ccsdt)
# UHF must replace HF as the only SCF reference (no {hf;...} block).
self.assertNotIn('{hf;', content_ccsdt)
self.assertIn('{uhf;', content_ccsdt)

self.job_mrcc_ccsdtq.cpu_cores = 48
self.job_mrcc_ccsdtq.set_input_file_memory()
self.job_mrcc_ccsdtq.write_input_file()
with open(os.path.join(self.job_mrcc_ccsdtq.local_path,
input_filenames[self.job_mrcc_ccsdtq.job_adapter]), 'r') as f:
content_ccsdtq = f.read()
expected_ccsdtq = """***,spc1
memory,Total=438,m;

geometry={angstrom;
O 0.00000000 0.00000000 1.00000000}

gprint,orbitals;

basis=cc-pvdz



int;

{hf;
maxit,999;
wf,spin=0,charge=0;
}

{mrcc,method=CCSDT(Q)}



---;

"""
self.assertEqual(content_ccsdtq, expected_ccsdtq)
self.assertNotIn('\nccsdt(q);\n', content_ccsdtq)
# spc1 here has multiplicity=1 (closed-shell) — RHF gives canonical
# orbitals MRCC accepts directly. No UHF/UCCSD pre-step needed.
self.assertNotIn('{uccsd}', content_ccsdtq)
self.assertNotIn('{uhf;', content_ccsdtq)
self.assertIn('{hf;', content_ccsdtq)

def test_set_files(self):
"""Test setting files"""
job_1_files_to_upload = [{'file_name': 'submit.sub',
Expand Down
44 changes: 37 additions & 7 deletions arc/job/trsh.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,10 +393,41 @@ def determine_ess_status(output_path: str,
return 'errored', keywords, error, line

elif software == 'molpro':
# MRCC ROHF-incompatibility check BEFORE the generic reverse scan
# because the underlying cause ("Use semicanonical orbitals!")
# appears earlier in the file than the downstream "Fatal error in
# mrcc." line — reverse iteration would otherwise classify the
# latter (generic) before the former (specific). Fix in the
# adapter prepends ``{uccsd}`` to generate semicanonical orbitals;
# this keyword surfaces the diagnostic for any legacy run that
# hits it.
joined = '\n'.join(lines) if isinstance(lines, list) else str(lines)
if 'standard ROHF orbitals' in joined or 'Use semicanonical orbitals' in joined:
rohf_line = next(
(ln for ln in lines if 'standard ROHF orbitals' in ln
or 'Use semicanonical orbitals' in ln),
'',
)
return ('errored', ['MRCCRequiresSemicanonical'],
'MRCC requires semicanonical orbitals; ROHF orbitals '
'are not supported for approximate CC.',
rohf_line)
for line in reverse_lines:
if 'molpro calculation terminated' in line.lower() \
or 'variable memory released' in line.lower():
return 'done', list(), '', ''
elif 'Fatal error in xmrcc' in line or 'Fatal error in mrcc' in line:
# MRCC bailed for a tiny system where the requested CC
# excitation rank exceeds the determinant space (e.g.
# atomic H or H2 at CCSDT(Q)). The composite framework
# should short-circuit a δ-term high leg with this
# keyword to the corresponding low-leg energy (δ = 0,
# which is correct for a degenerate-method case).
keywords = ['MRCCDegenerateSystem']
error = ('MRCC xmrcc fatal — the requested CC excitation '
'rank exceeds the determinant space for this '
'system (degenerate / too few electrons).')
break
elif 'No convergence' in line and '?No convergence in rhfpr' not in line:
keywords = ['Unconverged']
error = 'Unconverged'
Expand Down Expand Up @@ -1684,13 +1715,12 @@ def scan_quality_check(label: str,
logger.warning(message)
return invalidate, invalidation_reason, message, actions
else:
logger.warning(f'The maximal barrier for rotor {pivots} of {label} is '
f'{(np.max(energies) - np.min(energies)):.2f} kJ/mol, which is higher than the set threshold '
f'of {maximum_barrier} kJ/mol. Since this mode when treated as torsion has {num_wells}, '
f'this mode is not invalidated: treating it as a vibrational mode will be less accurate than '
f'the hindered rotor treatment, since the entropy contribution from the population of '
f'this species at the higher wells will not be taken into account. NOT invalidating this '
f'torsional mode.')
barrier_kJmol = np.max(energies) - np.min(energies)
logger.warning(f'Rotor {pivots} of {label}: barrier {barrier_kJmol:.2f} kJ/mol '
f'exceeds the {maximum_barrier} kJ/mol threshold, but the mode has '
f'{num_wells} wells. Keeping the hindered-rotor treatment — '
f'demoting to a harmonic vibration would miss the entropic '
f'contribution from the upper well(s).')

if preserve_params is not None:
success = True
Expand Down
26 changes: 26 additions & 0 deletions arc/job/trsh_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,32 @@ def test_determine_ess_status(self):
self.assertEqual(error, "Unrecognized basis set 6-311G**")
self.assertIn(" ? Basis library exhausted", line) # line includes '\n'

# Molpro + MRCC: degenerate small system (e.g. atomic H, H2 at CCSDT(Q)).
# MRCC's xmrcc bails because there's no determinant space at the
# requested excitation rank. Trsh must classify this so the framework
# knows to short-circuit the sub-job (delta = 0) instead of cycling
# the generic ladder (shift / vdz / memory).
path = os.path.join(self.base_path["molpro"], "mrcc_xmrcc_fatal.out")
status, keywords, error, line = trsh.determine_ess_status(
output_path=path, species_label="H", job_type="sp"
)
self.assertEqual(status, "errored")
self.assertEqual(keywords, ["MRCCDegenerateSystem"])
self.assertIn("xmrcc", error.lower())
self.assertIn("Fatal error in xmrcc", line)

# Molpro + MRCC: ROHF orbitals incompatible with approximate CC methods
# (open-shell radicals). Trsh classifies and the adapter's UCCSD
# prefix should prevent this from happening on new runs; the keyword
# is the diagnostic for any legacy runs that don't have the prefix.
path = os.path.join(self.base_path["molpro"], "mrcc_rohf_unsupported.out")
status, keywords, error, line = trsh.determine_ess_status(
output_path=path, species_label="OH", job_type="sp"
)
self.assertEqual(status, "errored")
self.assertEqual(keywords, ["MRCCRequiresSemicanonical"])
self.assertIn("semicanonical", error.lower())

# Orca

# test detection of a successful job
Expand Down
Loading