Skip to content

Commit d32a2e6

Browse files
authored
Fix ORCA CABS/F12 input generation and keyword duplication (#872)
This pull request improves the handling of complementary auxiliary basis sets (CABS) for F12 calculations in ORCA jobs. It introduces a dedicated `cabs` field for specifying the CABS basis, ensures that F12 methods require a CABS basis, and updates documentation and tests to reflect these changes. The logic for handling monoatomic species with DLPNO methods is also clarified and tested. **Improvements to ORCA F12/CABS handling:** * Added a dedicated `cabs` field to the level specification for F12 calculations, ensuring that CABS is not packed into `auxiliary_basis` and is correctly included in the ORCA input template. [[1]](diffhunk://#diff-60b5d1eeb1dc88db3d353467fdf082daaee0a4d0d330af6e28f19971b4d64ebfR87-R94) [[2]](diffhunk://#diff-a330a0112e60c2872ba1c9bd84f85a963f9edc44a273d883fed5b59c5e8b4a98L91-R101) * Enforced that F12 methods require a CABS basis; an informative error is raised if it is missing. * Updated the input file generation logic to insert the CABS token only when specified. **Test and documentation updates:** * Added tests to verify that F12 jobs with and without a CABS basis behave as expected, and updated existing tests and documentation to use the new `cabs` field. [[1]](diffhunk://#diff-847b38ec557affc7fcb76250b62ea822ded48ecf55c1ee5315ce49b2814bfbefR191-R231) [[2]](diffhunk://#diff-15e1a6128b75ab891a82f553500298b09e0e6c8abf5852df52504d1b94227af5L301-R308) [[3]](diffhunk://#diff-a330a0112e60c2872ba1c9bd84f85a963f9edc44a273d883fed5b59c5e8b4a98L91-R101) [[4]](diffhunk://#diff-a330a0112e60c2872ba1c9bd84f85a963f9edc44a273d883fed5b59c5e8b4a98L121-R128) **DLPNO/monoatomic handling:** * Improved logic for monoatomic species with DLPNO methods: single-electron atoms (e.g., H) now fall back to HF, while heavier monoatomics retain DLPNO. Added tests for this behavior. [[1]](diffhunk://#diff-556ad66547f14f3f9a3915808a05b59151ee8216b817845d8d716577c0aeb5ebL1447-R1455) [[2]](diffhunk://#diff-543ab169dfe340e750c06406160a449f8e1b27f63f3bd9a268a8fafd53e5a9b1L1021-R1022) [[3]](diffhunk://#diff-01ac51eb991fbf67febc6d56918494d7dc92486aec7fe8b44a7c7db75f3c2e84R1008-R1035)
2 parents 70dab25 + c99d254 commit d32a2e6

7 files changed

Lines changed: 114 additions & 35 deletions

File tree

arc/job/adapters/orca.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,14 @@ def _format_orca_basis(basis: str) -> str:
8484
# job_type_2: reserved for Opt + Freq.
8585
# restricted: 'R' = closed-shell SCF, 'U' = spin unrestricted SCF, 'RO' = open-shell spin restricted SCF
8686
# auxiliary_basis: required for DLPNO calculations (speed up calculation)
87+
# cabs: Complementary Auxiliary Basis Set for F12 calculations (e.g., cc-pVTZ-F12-CABS)
8788
# memory: MB per core (must increase as system gets larger)
8889
# cpus: must be less than number of electron pairs, defaults to min(heavy atoms, cpus limit)
8990
# job_options_blocks: input blocks that enable detailed control over program
9091
# job_options_keywords: input keywords that control the job
9192
# method_class: 'HF' for wavefunction methods (hf, mp, cc, dlpno ...). 'KS' for DFT methods.
9293
# options: additional keywords to control job (e.g., TightSCF, NormalPNO ...)
93-
input_template = """!${restricted}${method_class} ${method} ${basis} ${auxiliary_basis} ${keywords}
94+
input_template = """!${restricted}${method_class} ${method} ${basis} ${auxiliary_basis}${cabs} ${keywords}
9495
!${job_type_1}
9596
${job_type_2}
9697
%%maxcore ${memory}
@@ -254,6 +255,12 @@ def write_input_file(self) -> None:
254255
"""
255256
Write the input file to execute the job on the server.
256257
"""
258+
if 'f12' in self.level.method and not self.level.cabs:
259+
raise ValueError(
260+
f"Level '{self.level}' uses an F12 method without a CABS basis. "
261+
f"Set `cabs:` in the level spec (e.g. cc-pVTZ-F12-CABS). "
262+
f"Without it ORCA runs with DimCABS = 0 and returns non-F12 energies."
263+
)
257264
input_dict = dict()
258265
for key in ['block',
259266
'scan',
@@ -264,6 +271,7 @@ def write_input_file(self) -> None:
264271
input_dict[key] = ''
265272
input_dict['auxiliary_basis'] = _format_orca_basis(self.level.auxiliary_basis or '')
266273
input_dict['basis'] = _format_orca_basis(self.level.basis or '')
274+
input_dict['cabs'] = f' {_format_orca_basis(self.level.cabs)}' if self.level.cabs else ''
267275
input_dict['charge'] = self.charge
268276
input_dict['cpus'] = self.cpu_cores
269277
input_dict['label'] = self.species_label
@@ -272,30 +280,28 @@ def write_input_file(self) -> None:
272280
input_dict['multiplicity'] = self.multiplicity
273281
input_dict['xyz'] = xyz_to_str(self.xyz)
274282

275-
scf_convergence = self.args['keyword'].get('scf_convergence', '').lower() or \
276-
orca_default_options_dict['global']['keyword'].get('scf_convergence', '').lower()
277-
if not scf_convergence:
283+
self.args['keyword'].setdefault(
284+
'scf_convergence',
285+
orca_default_options_dict['global']['keyword'].get('scf_convergence', '').lower())
286+
if not self.args['keyword']['scf_convergence']:
278287
raise ValueError('Orca SCF convergence is not specified. Please specify this variable either in '
279288
'settings.py as default or in the input file as additional options.')
280-
self.add_to_args(val=scf_convergence, key1='keyword')
281289

282290
# Orca requires different blocks for wavefunction methods and DFT methods
283291
if self.level.method_type == 'dft':
284292
input_dict['method_class'] = 'KS'
285-
# DFT grid must be the same for both opt and freq
286-
if self.fine:
287-
self.add_to_args(val='defgrid3', key1='keyword')
288-
else:
289-
self.add_to_args(val='defgrid2', key1='keyword')
293+
# DFT grid must be the same for both opt and freq.
294+
# Users can override by setting `dft_grid` in args.keyword (e.g. dft_grid: DEFGRID1).
295+
self.args['keyword'].setdefault('dft_grid', 'defgrid3' if self.fine else 'defgrid2')
290296
elif self.level.method_type == 'wavefunction':
291297
input_dict['method_class'] = 'HF'
292298
if 'dlpno' in self.level.method:
293-
dlpno_threshold = self.args['keyword'].get('dlpno_threshold', '').lower() or \
294-
orca_default_options_dict['global']['keyword'].get('dlpno_threshold', '').lower()
295-
if not dlpno_threshold:
299+
self.args['keyword'].setdefault(
300+
'dlpno_threshold',
301+
orca_default_options_dict['global']['keyword'].get('dlpno_threshold', '').lower())
302+
if not self.args['keyword']['dlpno_threshold']:
296303
raise ValueError('Orca DLPNO threshold is not specified. Please specify this variable either in '
297304
'settings.py as default or in the input file as additional options.')
298-
self.add_to_args(val=dlpno_threshold, key1='keyword')
299305
else:
300306
logger.debug(f'Running {self.level.method_type} {self.level.method} method in Orca.')
301307

arc/job/adapters/orca_test.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,47 @@ def test_write_input_file_with_CPCM_solvation(self):
188188
"""
189189
self.assertEqual(content_3, job_3_expected_input_file)
190190

191+
def test_write_input_file_f12_with_cabs(self):
192+
"""F12 sp_level with a cabs basis emits the CABS token on the ! line."""
193+
job_f12 = OrcaAdapter(execution_type='queue',
194+
job_type='sp',
195+
level=Level(method='DLPNO-CCSD(T)-F12',
196+
basis='cc-pVTZ-F12',
197+
auxiliary_basis='aug-cc-pVTZ/C',
198+
cabs='cc-pVTZ-F12-CABS'),
199+
project='test_f12',
200+
project_directory=os.path.join(ARC_TESTING_PATH, 'test_OrcaAdapter'),
201+
species=[ARCSpecies(label='O_atom', smiles='[O]',
202+
xyz='O 0.0 0.0 0.0')],
203+
testing=True,
204+
)
205+
job_f12.write_input_file()
206+
with open(os.path.join(job_f12.local_path, input_filenames[job_f12.job_adapter]), 'r') as f:
207+
content = f.read()
208+
bang_line = content.splitlines()[0]
209+
self.assertIn('dlpno-ccsd(t)-f12', bang_line)
210+
self.assertIn('cc-pvtz-f12', bang_line)
211+
self.assertIn('aug-cc-pvtz/c', bang_line)
212+
self.assertIn('cc-pvtz-f12-cabs', bang_line)
213+
214+
def test_write_input_file_f12_without_cabs_raises(self):
215+
"""F12 sp_level without a cabs basis raises at input-file generation."""
216+
# _initialize_adapter calls set_files() which calls write_input_file(),
217+
# so the guard fires during OrcaAdapter construction — wrap the whole
218+
# thing in assertRaises.
219+
with self.assertRaises(ValueError):
220+
OrcaAdapter(execution_type='queue',
221+
job_type='sp',
222+
level=Level(method='DLPNO-CCSD(T)-F12',
223+
basis='cc-pVTZ-F12',
224+
auxiliary_basis='aug-cc-pVTZ/C'),
225+
project='test_f12_bad',
226+
project_directory=os.path.join(ARC_TESTING_PATH, 'test_OrcaAdapter'),
227+
species=[ARCSpecies(label='O_atom', smiles='[O]',
228+
xyz='O 0.0 0.0 0.0')],
229+
testing=True,
230+
)
231+
191232
def test_format_orca_method(self):
192233
"""Test ORCA method formatting helper."""
193234
self.assertEqual(_format_orca_method('wb97xd3'), 'wb97x-d3')

arc/job/trsh.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,8 +1018,8 @@ def trsh_ess_job(label: str,
10181018
couldnt_trsh = True
10191019

10201020
elif 'orca' in software:
1021-
if 'dlpno' in level_of_theory.method and (is_monoatomic or is_h):
1022-
raise TrshError(f'DLPNO methods are incompatible with monoatomic species {label} in Orca. '
1021+
if 'dlpno' in level_of_theory.method and is_h:
1022+
raise TrshError(f'DLPNO methods are incompatible with single-electron species {label} in Orca. '
10231023
f'This should have been caught by the Scheduler before job submission.')
10241024
elif 'Memory' in job_status['keywords']:
10251025
# Increase memory allocation.

arc/main_test.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -298,14 +298,14 @@ def test_determine_model_chemistry_for_job_types(self):
298298
freq_level={'method': 'B3LYP/G', 'basis': 'cc-pVDZ(fi/sf/fw)', 'auxiliary_basis': 'def2-svp/C',
299299
'dispersion': 'DEF2-tzvp/c'},
300300
sp_level={'method': 'DLPNO-CCSD(T)-F12', 'basis': 'cc-pVTZ-F12',
301-
'auxiliary_basis': 'aug-cc-pVTZ/C cc-pVTZ-F12-CABS'},
301+
'auxiliary_basis': 'aug-cc-pVTZ/C', 'cabs': 'cc-pVTZ-F12-CABS'},
302302
calc_freq_factor=False, compute_thermo=False)
303303
self.assertEqual(arc9.opt_level.simple(), 'wb97xd/def2tzvp')
304304
self.assertEqual(str(arc9.freq_level), 'b3lyp/g/cc-pvdz(fi/sf/fw), auxiliary_basis: def2-svp/c, '
305305
'dispersion: def2-tzvp/c, software: gaussian')
306306
self.assertEqual(str(arc9.sp_level),
307-
'dlpno-ccsd(t)-f12/cc-pvtz-f12, auxiliary_basis: aug-cc-pvtz/c cc-pvtz-f12-cabs, '
308-
'software: orca')
307+
'dlpno-ccsd(t)-f12/cc-pvtz-f12, auxiliary_basis: aug-cc-pvtz/c, '
308+
'cabs: cc-pvtz-f12-cabs, software: orca')
309309

310310
# Test using default frequency and orbital level for composite job, also forbid rotors job
311311
arc10 = ARC(project='test', composite_method='cbs-qb3', calc_freq_factor=False,

arc/scheduler.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,16 +1444,15 @@ def run_sp_job(self,
14441444
level_of_theory='ccsd/cc-pvdz',
14451445
job_type='sp')
14461446
return
1447-
if self.species_dict[label].is_monoatomic() and 'dlpno' in level.method:
1448-
species = self.species_dict[label]
1449-
if species.mol.atoms[0].element.symbol in ('H', 'D', 'T'):
1450-
logger.info(f'Using HF/{level.basis} for {label} (single electron, no correlation).')
1451-
level = Level(method='hf', basis=level.basis, software=level.software, args=level.args)
1452-
else:
1453-
canonical_method = level.method.replace('dlpno-', '')
1454-
logger.info(f'DLPNO methods are incompatible with monoatomic species {label}. '
1455-
f'Using {canonical_method}/{level.basis} instead.')
1456-
level = Level(method=canonical_method, basis=level.basis, software=level.software, args=level.args)
1447+
if self.species_dict[label].is_monoatomic() and 'dlpno' in level.method \
1448+
and self.species_dict[label].mol.atoms[0].element.symbol in ('H', 'D', 'T'):
1449+
# DLPNO needs electron pairs; fall back to HF for single-electron atoms only.
1450+
# Heavier monoatomics (e.g. [O], [N]) run DLPNO fine in ORCA and are left alone.
1451+
logger.info(f'Using HF/{level.basis} for {label} (single electron, no correlation).')
1452+
level_dict = level.as_dict()
1453+
level_dict.pop('method_type', None) # re-deduce after method change
1454+
level_dict['method'] = 'hf'
1455+
level = Level(repr=level_dict)
14571456
if self.job_types['sp']:
14581457
if self.species_dict[label].multi_species:
14591458
if self.output_multi_spc[self.species_dict[label].multi_species].get('sp', False):

arc/scheduler_test.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,6 +1005,34 @@ def test_switch_ts_rotors_reset(self, mock_run_opt):
10051005
# rotors_dict=None must be preserved — do not re-enable rotor scans.
10061006
self.assertIsNone(sched2.species_dict[ts_label2].rotors_dict)
10071007

1008+
@patch('arc.scheduler.Scheduler.run_job')
1009+
def test_run_sp_monoatomic_dlpno(self, mock_run_job):
1010+
"""Monoatomic H falls back to HF; heavier atoms (O) keep DLPNO intact."""
1011+
dlpno_level = Level(method='DLPNO-CCSD(T)-F12', basis='cc-pVTZ-F12',
1012+
auxiliary_basis='aug-cc-pVTZ/C', cabs='cc-pVTZ-F12-CABS',
1013+
software='orca')
1014+
1015+
for label, smiles in [('H_atom', '[H]'), ('O_atom', '[O]')]:
1016+
self.sched1.species_dict[label] = ARCSpecies(label=label, smiles=smiles)
1017+
self.sched1.job_dict[label] = {}
1018+
self.sched1.output[label] = {'paths': {}, 'job_types': {},
1019+
'errors': '', 'warnings': '', 'conformers': ''}
1020+
1021+
# Single-electron atom → HF fallback, aux/cabs preserved.
1022+
self.sched1.run_sp_job(label='H_atom', level=dlpno_level)
1023+
h_level = mock_run_job.call_args.kwargs['level_of_theory']
1024+
self.assertEqual(h_level.method, 'hf')
1025+
self.assertEqual(h_level.basis, 'cc-pvtz-f12')
1026+
self.assertEqual(h_level.auxiliary_basis, 'aug-cc-pvtz/c')
1027+
self.assertEqual(h_level.cabs, 'cc-pvtz-f12-cabs')
1028+
1029+
# Heavier monoatomic → DLPNO level unchanged.
1030+
mock_run_job.reset_mock()
1031+
self.sched1.run_sp_job(label='O_atom', level=dlpno_level)
1032+
o_level = mock_run_job.call_args.kwargs['level_of_theory']
1033+
self.assertEqual(o_level.method, 'dlpno-ccsd(t)-f12')
1034+
self.assertEqual(o_level.cabs, 'cc-pvtz-f12-cabs')
1035+
10081036
@classmethod
10091037
def tearDownClass(cls):
10101038
"""

docs/source/advanced.rst

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,17 @@ Another example::
8888

8989
sp_level: {'method': 'DLPNO-CCSD(T)-F12',
9090
'basis': 'cc-pVTZ-F12',
91-
'auxiliary_basis': 'aug-cc-pVTZ/C cc-pVTZ-F12-CABS',
91+
'auxiliary_basis': 'aug-cc-pVTZ/C',
92+
'cabs': 'cc-pVTZ-F12-CABS',
9293
'args': {'keyword' :{'opt_convergence': 'TightOpt'}},
9394
'software': 'orca',
9495
}
9596

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

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

120124

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

125130
ARC also supports an additional shortcut argument, ``level_of_theory``,
126131
to simultaneously specify ``opt_level``, ``freq_level``, ``sp_level``, and ``scan_level``.

0 commit comments

Comments
 (0)