Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a9a3f8c
Relocate Level into its own arc/level/ package
alongd Apr 22, 2026
67a05b2
Add sp_composite data model under arc/level/
alongd Apr 22, 2026
48e65fe
Wire sp_composite end-to-end: main, Scheduler, species, Arkane
alongd Apr 22, 2026
294d190
Docs: sp_composite user guide, examples, and parseability tests
alongd Apr 22, 2026
26368a6
MRCC route in Molpro
alongd Apr 27, 2026
bae3ec2
Drop misleading "ARC ignores user-specified options" warning
alongd Apr 28, 2026
154e77a
Improve species classification for thermo in processor
alongd May 7, 2026
8dd2665
Treat atoms (e.g., H) correctly in the arkane adapter
alongd May 7, 2026
848dc62
Add zombie-job detection (6h no-output -> kill + resubmit once)
alongd May 7, 2026
38522db
Added MRCC support to trsh
alongd May 9, 2026
b49722c
Improve imports in mapping engine
alongd May 9, 2026
722c80a
Changed molecule error and exception logging into debug
alongd May 9, 2026
42a8532
filter_real_stderr_lines in arkane adapter
alongd May 9, 2026
49783c9
Changed max barrier for rotor threshold from 40 to 60 kJ/mol
alongd May 9, 2026
ee1ccd2
scheduler: mutate species_list in place when deleting IRC species
alongd May 16, 2026
a32dee8
trsh: reduce Orca cpu cores on repeated DLPNO memory error
alongd May 25, 2026
e097fcc
arkane: dedupe species by label in Arkane input
alongd May 30, 2026
d55b3a5
scheduler: don't fail a TS on an unconverged rotor scan
alongd May 30, 2026
a44309f
orca: emit aux basis and CABS in a %basis block
alongd May 30, 2026
51b8d7f
ssh: don't KeyError in list_available_nodes on PBS
alongd May 30, 2026
f0215a9
zombie: distinguish transient stat failures and add per-server grace
alongd Jul 9, 2026
aee01d9
trsh: fix MRCC status classification
alongd Jul 9, 2026
0b19897
arkane: log filtered stderr noise at DEBUG, real errors at INFO
alongd Jul 9, 2026
beeb5bb
sp_composite: support CBS extrapolation as the protocol base
alongd Jul 9, 2026
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 @@ -493,21 +492,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
20 changes: 15 additions & 5 deletions arc/job/adapters/orca.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ def _format_orca_basis(basis: str) -> str:
# job_options_keywords: input keywords that control the job
# method_class: 'HF' for wavefunction methods (hf, mp, cc, dlpno ...). 'KS' for DFT methods.
# options: additional keywords to control job (e.g., TightSCF, NormalPNO ...)
input_template = """!${restricted}${method_class} ${method} ${basis} ${auxiliary_basis}${cabs} ${keywords}
!${job_type_1}
${job_type_2}
input_template = """!${restricted}${method_class} ${method} ${basis} ${keywords}
!${job_type_1}
${job_type_2}${basis_block}
%%maxcore ${memory}
%%pal nprocs ${cpus} end

Expand Down Expand Up @@ -270,9 +270,19 @@ def write_input_file(self) -> None:
'keywords',
]:
input_dict[key] = ''
input_dict['auxiliary_basis'] = _format_orca_basis(self.level.auxiliary_basis or '')
input_dict['basis'] = _format_orca_basis(self.level.basis or '')
input_dict['cabs'] = f' {_format_orca_basis(self.level.cabs)}' if self.level.cabs else ''
# In ORCA, the orbital basis is the only basis allowed on the `!` simple-input line.
# Auxiliary fitting bases (AuxC) and the F12 CABS must be declared inside a %basis block,
# otherwise ORCA raises "UNRECOGNIZED OR DUPLICATED KEYWORD(S) IN SIMPLE INPUT LINE".
basis_block_lines = []
auxiliary_basis = _format_orca_basis(self.level.auxiliary_basis or '')
if auxiliary_basis:
basis_block_lines.append(f'AuxC "{auxiliary_basis}"')
cabs = _format_orca_basis(self.level.cabs) if self.level.cabs else ''
if cabs:
basis_block_lines.append(f'CABS "{cabs}"')
input_dict['basis_block'] = '\n%basis\n' + '\n'.join(basis_block_lines) + '\nend\n' \
if basis_block_lines else ''
input_dict['charge'] = self.charge
input_dict['cpus'] = self.cpu_cores
input_dict['label'] = self.species_label
Expand Down
39 changes: 28 additions & 11 deletions arc/job/adapters/orca_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,12 @@ def test_write_input_file(self):
self.job_1.write_input_file()
with open(os.path.join(self.job_1.local_path, input_filenames[self.job_1.job_adapter]), 'r') as f:
content_1 = f.read()
job_1_expected_input_file = """!uHF dlpno-ccsd(t) def2-tzvp def2-tzvp/c tightscf normalpno
!sp
job_1_expected_input_file = """!uHF dlpno-ccsd(t) def2-tzvp tightscf normalpno
!sp

%basis
AuxC "def2-tzvp/c"
end

%maxcore 1792
%pal nprocs 8 end
Expand All @@ -130,8 +134,12 @@ def test_write_input_file_with_SMD_solvation(self):
self.job_2.write_input_file()
with open(os.path.join(self.job_2.local_path, input_filenames[self.job_2.job_adapter]), 'r') as f:
content_2 = f.read()
job_2_expected_input_file = """!uHF dlpno-ccsd(t) def2-tzvp def2-tzvp/c tightscf normalpno
!sp
job_2_expected_input_file = """!uHF dlpno-ccsd(t) def2-tzvp tightscf normalpno
!sp

%basis
AuxC "def2-tzvp/c"
end

%maxcore 1792
%pal nprocs 8 end
Expand Down Expand Up @@ -163,8 +171,12 @@ def test_write_input_file_with_CPCM_solvation(self):
self.job_3.write_input_file()
with open(os.path.join(self.job_3.local_path, input_filenames[self.job_3.job_adapter]), 'r') as f:
content_3 = f.read()
job_3_expected_input_file = """!uHF dlpno-ccsd(t) def2-tzvp def2-tzvp/c tightscf normalpno
!sp
job_3_expected_input_file = """!uHF dlpno-ccsd(t) def2-tzvp tightscf normalpno
!sp

%basis
AuxC "def2-tzvp/c"
end

%maxcore 1792
%pal nprocs 8 end
Expand All @@ -189,7 +201,7 @@ def test_write_input_file_with_CPCM_solvation(self):
self.assertEqual(content_3, job_3_expected_input_file)

def test_write_input_file_f12_with_cabs(self):
"""F12 sp_level with a cabs basis emits the CABS token on the ! line."""
"""F12 sp_level with a cabs basis emits the AuxC and CABS tokens in a %basis block."""
job_f12 = OrcaAdapter(execution_type='queue',
job_type='sp',
level=Level(method='DLPNO-CCSD(T)-F12',
Expand All @@ -208,8 +220,13 @@ def test_write_input_file_f12_with_cabs(self):
bang_line = content.splitlines()[0]
self.assertIn('dlpno-ccsd(t)-f12', bang_line)
self.assertIn('cc-pvtz-f12', bang_line)
self.assertIn('aug-cc-pvtz/c', bang_line)
self.assertIn('cc-pvtz-f12-cabs', bang_line)
# Aux and CABS must NOT be on the ! line (ORCA rejects them there), only the orbital basis.
self.assertNotIn('aug-cc-pvtz/c', bang_line)
self.assertNotIn('cabs', bang_line.lower())
# They must appear inside a %basis block instead.
self.assertIn('%basis', content)
self.assertIn('AuxC "aug-cc-pvtz/c"', content)
self.assertIn('CABS "cc-pvtz-f12-cabs"', content)

def test_write_input_file_f12_without_cabs_raises(self):
"""F12 sp_level without a cabs basis raises at input-file generation."""
Expand Down Expand Up @@ -255,8 +272,8 @@ def test_write_input_file_mrci(self):
self.job_4.write_input_file()
with open(os.path.join(self.job_4.local_path, input_filenames[self.job_4.job_adapter]), 'r') as f:
content_4 = f.read()
job_4_expected_input_file = """!uHF aug-cc-pvtz tightscf
!sp
job_4_expected_input_file = """!uHF aug-cc-pvtz tightscf
!sp

%maxcore 1792
%pal nprocs 8 end
Expand Down
Loading
Loading