Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
84ab88b
Relocate Level into its own arc/level/ package
alongd Apr 22, 2026
a2be46f
Add sp_composite data model under arc/level/
alongd Apr 22, 2026
f11444c
Wire sp_composite end-to-end: main, Scheduler, species, Arkane
alongd Apr 22, 2026
c261d6c
Docs: sp_composite user guide, examples, and parseability tests
alongd Apr 22, 2026
1322663
MRCC route in Molpro
alongd Apr 27, 2026
aad1aa7
Drop misleading "ARC ignores user-specified options" warning
alongd Apr 28, 2026
1408f29
Improve species classification for thermo in processor
alongd May 7, 2026
7dbed62
Treat atoms (e.g., H) correctly in the arkane adapter
alongd May 7, 2026
5aa65db
Add zombie-job detection (6h no-output -> kill + resubmit once)
alongd May 7, 2026
d37cf21
Added MRCC support to trsh
alongd May 9, 2026
037f15d
Improve imports in mapping engine
alongd May 9, 2026
b030034
Changed molecule error and exception logging into debug
alongd May 9, 2026
5f3f5f1
filter_real_stderr_lines in arkane adapter
alongd May 9, 2026
f739412
Changed max barrier for rotor threshold from 40 to 60 kJ/mol
alongd May 9, 2026
81ed883
scheduler: mutate species_list in place when deleting IRC species
alongd May 16, 2026
d7e6a58
trsh: reduce Orca cpu cores on repeated DLPNO memory error
alongd May 25, 2026
e8f9a21
arkane: dedupe species by label in Arkane input
alongd May 30, 2026
365ca1d
scheduler: don't fail a TS on an unconverged rotor scan
alongd May 30, 2026
fa1917c
orca: emit aux basis and CABS in a %basis block
alongd May 30, 2026
9312a8b
ssh: don't KeyError in list_available_nodes on PBS
alongd May 30, 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