Skip to content
Merged
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
34 changes: 20 additions & 14 deletions arc/job/adapters/orca.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,14 @@ def _format_orca_basis(basis: str) -> str:
# job_type_2: reserved for Opt + Freq.
# restricted: 'R' = closed-shell SCF, 'U' = spin unrestricted SCF, 'RO' = open-shell spin restricted SCF
# auxiliary_basis: required for DLPNO calculations (speed up calculation)
# cabs: Complementary Auxiliary Basis Set for F12 calculations (e.g., cc-pVTZ-F12-CABS)
# memory: MB per core (must increase as system gets larger)
# cpus: must be less than number of electron pairs, defaults to min(heavy atoms, cpus limit)
# job_options_blocks: input blocks that enable detailed control over program
# 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} ${keywords}
input_template = """!${restricted}${method_class} ${method} ${basis} ${auxiliary_basis}${cabs} ${keywords}
!${job_type_1}
${job_type_2}
%%maxcore ${memory}
Expand Down Expand Up @@ -254,6 +255,12 @@ def write_input_file(self) -> None:
"""
Write the input file to execute the job on the server.
"""
if 'f12' in self.level.method and not self.level.cabs:
raise ValueError(
f"Level '{self.level}' uses an F12 method without a CABS basis. "
f"Set `cabs:` in the level spec (e.g. cc-pVTZ-F12-CABS). "
f"Without it ORCA runs with DimCABS = 0 and returns non-F12 energies."
)
Comment thread
calvinp0 marked this conversation as resolved.
input_dict = dict()
for key in ['block',
'scan',
Expand All @@ -264,6 +271,7 @@ def write_input_file(self) -> None:
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 ''
Comment on lines 258 to +274
input_dict['charge'] = self.charge
input_dict['cpus'] = self.cpu_cores
input_dict['label'] = self.species_label
Expand All @@ -272,30 +280,28 @@ def write_input_file(self) -> None:
input_dict['multiplicity'] = self.multiplicity
input_dict['xyz'] = xyz_to_str(self.xyz)

scf_convergence = self.args['keyword'].get('scf_convergence', '').lower() or \
orca_default_options_dict['global']['keyword'].get('scf_convergence', '').lower()
if not scf_convergence:
self.args['keyword'].setdefault(
'scf_convergence',
orca_default_options_dict['global']['keyword'].get('scf_convergence', '').lower())
if not self.args['keyword']['scf_convergence']:
raise ValueError('Orca SCF convergence is not specified. Please specify this variable either in '
'settings.py as default or in the input file as additional options.')
self.add_to_args(val=scf_convergence, key1='keyword')

# Orca requires different blocks for wavefunction methods and DFT methods
if self.level.method_type == 'dft':
input_dict['method_class'] = 'KS'
# DFT grid must be the same for both opt and freq
if self.fine:
self.add_to_args(val='defgrid3', key1='keyword')
else:
self.add_to_args(val='defgrid2', key1='keyword')
# DFT grid must be the same for both opt and freq.
# Users can override by setting `dft_grid` in args.keyword (e.g. dft_grid: DEFGRID1).
self.args['keyword'].setdefault('dft_grid', 'defgrid3' if self.fine else 'defgrid2')
elif self.level.method_type == 'wavefunction':
input_dict['method_class'] = 'HF'
if 'dlpno' in self.level.method:
dlpno_threshold = self.args['keyword'].get('dlpno_threshold', '').lower() or \
orca_default_options_dict['global']['keyword'].get('dlpno_threshold', '').lower()
if not dlpno_threshold:
self.args['keyword'].setdefault(
'dlpno_threshold',
orca_default_options_dict['global']['keyword'].get('dlpno_threshold', '').lower())
if not self.args['keyword']['dlpno_threshold']:
raise ValueError('Orca DLPNO threshold is not specified. Please specify this variable either in '
'settings.py as default or in the input file as additional options.')
self.add_to_args(val=dlpno_threshold, key1='keyword')
else:
logger.debug(f'Running {self.level.method_type} {self.level.method} method in Orca.')

Expand Down
41 changes: 41 additions & 0 deletions arc/job/adapters/orca_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,47 @@ 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."""
job_f12 = OrcaAdapter(execution_type='queue',
job_type='sp',
level=Level(method='DLPNO-CCSD(T)-F12',
basis='cc-pVTZ-F12',
auxiliary_basis='aug-cc-pVTZ/C',
cabs='cc-pVTZ-F12-CABS'),
project='test_f12',
project_directory=os.path.join(ARC_TESTING_PATH, 'test_OrcaAdapter'),
species=[ARCSpecies(label='O_atom', smiles='[O]',
xyz='O 0.0 0.0 0.0')],
testing=True,
)
job_f12.write_input_file()
with open(os.path.join(job_f12.local_path, input_filenames[job_f12.job_adapter]), 'r') as f:
content = f.read()
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)

def test_write_input_file_f12_without_cabs_raises(self):
"""F12 sp_level without a cabs basis raises at input-file generation."""
# _initialize_adapter calls set_files() which calls write_input_file(),
# so the guard fires during OrcaAdapter construction — wrap the whole
# thing in assertRaises.
with self.assertRaises(ValueError):
OrcaAdapter(execution_type='queue',
job_type='sp',
level=Level(method='DLPNO-CCSD(T)-F12',
basis='cc-pVTZ-F12',
auxiliary_basis='aug-cc-pVTZ/C'),
project='test_f12_bad',
project_directory=os.path.join(ARC_TESTING_PATH, 'test_OrcaAdapter'),
species=[ARCSpecies(label='O_atom', smiles='[O]',
xyz='O 0.0 0.0 0.0')],
testing=True,
)

def test_format_orca_method(self):
"""Test ORCA method formatting helper."""
self.assertEqual(_format_orca_method('wb97xd3'), 'wb97x-d3')
Expand Down
4 changes: 2 additions & 2 deletions arc/job/trsh.py
Original file line number Diff line number Diff line change
Expand Up @@ -1018,8 +1018,8 @@ def trsh_ess_job(label: str,
couldnt_trsh = True

elif 'orca' in software:
if 'dlpno' in level_of_theory.method and (is_monoatomic or is_h):
raise TrshError(f'DLPNO methods are incompatible with monoatomic species {label} in Orca. '
if 'dlpno' in level_of_theory.method and is_h:
raise TrshError(f'DLPNO methods are incompatible with single-electron species {label} in Orca. '
f'This should have been caught by the Scheduler before job submission.')
elif 'Memory' in job_status['keywords']:
# Increase memory allocation.
Expand Down
6 changes: 3 additions & 3 deletions arc/main_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,14 +298,14 @@ def test_determine_model_chemistry_for_job_types(self):
freq_level={'method': 'B3LYP/G', 'basis': 'cc-pVDZ(fi/sf/fw)', 'auxiliary_basis': 'def2-svp/C',
'dispersion': 'DEF2-tzvp/c'},
sp_level={'method': 'DLPNO-CCSD(T)-F12', 'basis': 'cc-pVTZ-F12',
'auxiliary_basis': 'aug-cc-pVTZ/C cc-pVTZ-F12-CABS'},
'auxiliary_basis': 'aug-cc-pVTZ/C', 'cabs': 'cc-pVTZ-F12-CABS'},
calc_freq_factor=False, compute_thermo=False)
self.assertEqual(arc9.opt_level.simple(), 'wb97xd/def2tzvp')
self.assertEqual(str(arc9.freq_level), 'b3lyp/g/cc-pvdz(fi/sf/fw), auxiliary_basis: def2-svp/c, '
'dispersion: def2-tzvp/c, software: gaussian')
self.assertEqual(str(arc9.sp_level),
'dlpno-ccsd(t)-f12/cc-pvtz-f12, auxiliary_basis: aug-cc-pvtz/c cc-pvtz-f12-cabs, '
'software: orca')
'dlpno-ccsd(t)-f12/cc-pvtz-f12, auxiliary_basis: aug-cc-pvtz/c, '
'cabs: cc-pvtz-f12-cabs, software: orca')

# Test using default frequency and orbital level for composite job, also forbid rotors job
arc10 = ARC(project='test', composite_method='cbs-qb3', calc_freq_factor=False,
Expand Down
19 changes: 9 additions & 10 deletions arc/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1444,16 +1444,15 @@ def run_sp_job(self,
level_of_theory='ccsd/cc-pvdz',
job_type='sp')
return
if self.species_dict[label].is_monoatomic() and 'dlpno' in level.method:
species = self.species_dict[label]
if species.mol.atoms[0].element.symbol in ('H', 'D', 'T'):
logger.info(f'Using HF/{level.basis} for {label} (single electron, no correlation).')
level = Level(method='hf', basis=level.basis, software=level.software, args=level.args)
else:
canonical_method = level.method.replace('dlpno-', '')
logger.info(f'DLPNO methods are incompatible with monoatomic species {label}. '
f'Using {canonical_method}/{level.basis} instead.')
level = Level(method=canonical_method, basis=level.basis, software=level.software, args=level.args)
if self.species_dict[label].is_monoatomic() and 'dlpno' in level.method \
and self.species_dict[label].mol.atoms[0].element.symbol in ('H', 'D', 'T'):
# DLPNO needs electron pairs; fall back to HF for single-electron atoms only.
# Heavier monoatomics (e.g. [O], [N]) run DLPNO fine in ORCA and are left alone.
logger.info(f'Using HF/{level.basis} for {label} (single electron, no correlation).')
level_dict = level.as_dict()
level_dict.pop('method_type', None) # re-deduce after method change
level_dict['method'] = 'hf'
level = Level(repr=level_dict)
Comment thread
calvinp0 marked this conversation as resolved.
if self.job_types['sp']:
if self.species_dict[label].multi_species:
if self.output_multi_spc[self.species_dict[label].multi_species].get('sp', False):
Expand Down
28 changes: 28 additions & 0 deletions arc/scheduler_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,34 @@ def test_switch_ts_rotors_reset(self, mock_run_opt):
# rotors_dict=None must be preserved — do not re-enable rotor scans.
self.assertIsNone(sched2.species_dict[ts_label2].rotors_dict)

@patch('arc.scheduler.Scheduler.run_job')
def test_run_sp_monoatomic_dlpno(self, mock_run_job):
"""Monoatomic H falls back to HF; heavier atoms (O) keep DLPNO intact."""
dlpno_level = Level(method='DLPNO-CCSD(T)-F12', basis='cc-pVTZ-F12',
auxiliary_basis='aug-cc-pVTZ/C', cabs='cc-pVTZ-F12-CABS',
software='orca')

for label, smiles in [('H_atom', '[H]'), ('O_atom', '[O]')]:
self.sched1.species_dict[label] = ARCSpecies(label=label, smiles=smiles)
self.sched1.job_dict[label] = {}
self.sched1.output[label] = {'paths': {}, 'job_types': {},
'errors': '', 'warnings': '', 'conformers': ''}

# Single-electron atom → HF fallback, aux/cabs preserved.
self.sched1.run_sp_job(label='H_atom', level=dlpno_level)
h_level = mock_run_job.call_args.kwargs['level_of_theory']
self.assertEqual(h_level.method, 'hf')
self.assertEqual(h_level.basis, 'cc-pvtz-f12')
self.assertEqual(h_level.auxiliary_basis, 'aug-cc-pvtz/c')
self.assertEqual(h_level.cabs, 'cc-pvtz-f12-cabs')

# Heavier monoatomic → DLPNO level unchanged.
mock_run_job.reset_mock()
self.sched1.run_sp_job(label='O_atom', level=dlpno_level)
o_level = mock_run_job.call_args.kwargs['level_of_theory']
self.assertEqual(o_level.method, 'dlpno-ccsd(t)-f12')
self.assertEqual(o_level.cabs, 'cc-pvtz-f12-cabs')

@classmethod
def tearDownClass(cls):
"""
Expand Down
17 changes: 11 additions & 6 deletions docs/source/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,17 @@ Another example::

sp_level: {'method': 'DLPNO-CCSD(T)-F12',
'basis': 'cc-pVTZ-F12',
'auxiliary_basis': 'aug-cc-pVTZ/C cc-pVTZ-F12-CABS',
'auxiliary_basis': 'aug-cc-pVTZ/C',
'cabs': 'cc-pVTZ-F12-CABS',
'args': {'keyword' :{'opt_convergence': 'TightOpt'}},
'software': 'orca',
}

specifies ``DLPNO-CCSD(T)-F12/cc-pVTZ-F12`` model chemistry along with two auxiliary basis sets,
``aug-cc-pVTZ/C`` and ``cc-pVTZ-F12-CABS``, with ``TightOpt`` for a single point energy calculation.
specifies ``DLPNO-CCSD(T)-F12/cc-pVTZ-F12`` model chemistry along with an
auxiliary basis ``aug-cc-pVTZ/C`` and a complementary auxiliary basis (CABS)
``cc-pVTZ-F12-CABS``, with ``TightOpt`` for a single point energy calculation.
The ``cabs`` argument is the single source of truth for F12 complementary
auxiliary basis sets; do not pack the CABS token into ``auxiliary_basis``.
You can also provide a 4-digit ``year`` on ``arkane_level_of_theory`` to distinguish method variants
in the Arkane database (e.g., ``b97d3`` vs ``b97d32023``)::

Expand All @@ -118,9 +122,10 @@ The following are examples for **equivalent** definitions::
conformer_opt_level = {'method': 'PM6'}


Note that the ``cabs`` and ``solvation_scheme_level`` arguments currently have no effect
and will be implemented in future versions. The ``software`` argument is automatically determined
unless specified by the user.
Note that the ``solvation_scheme_level`` argument currently has no effect and
will be implemented in future versions. The ``cabs`` argument is consumed by
the ORCA and Molpro adapters for F12 calculations; it is ignored by other ESS.
The ``software`` argument is automatically determined unless specified by the user.

ARC also supports an additional shortcut argument, ``level_of_theory``,
to simultaneously specify ``opt_level``, ``freq_level``, ``sp_level``, and ``scan_level``.
Expand Down
Loading