Skip to content

Commit 73d661c

Browse files
authored
Arkane matching method name (#814)
When ARC hands off to Arkane for thermochemistry, it needs to look up atom energy corrections (AEC), bond additivity corrections (BAC), and frequency scale factors from the RMG-database quantum_corrections/data.py. That file uses RMG's own naming convention — all lowercase, no hyphens, and sometimes year-suffixed variants like b97d32023 alongside b97d3. Previously, ARC did exact string matching against these entries. If a user wrote CCSD(T)-F12/cc-pVTZ-F12 and the database had ccsd(t)f12/ccpvtzf12, it wouldn't match. Users had to know RMG's internal naming to get corrections applied. ## Changes 1. `Level.year` attribute Adds an optional year (4-digit int) to the Level class. Validated on construction, included in __str__, simple(), as_dict(), and build() for serialization/restart support. 2. Fuzzy matching engine - _normalize_method: lowercase, strip hyphens → DLPNO-CCSD(T)-F12 becomes dlpnoccsd(t)f12 - _normalize_basis: lowercase, strip hyphens and spaces → cc-pVTZ-F12 becomes ccpvtzf12 - _split_method_year: extracts trailing 4-digit year → b97d32023 → (b97d3, 2023) - _parse_lot_params: regex-extracts method/basis/software from LevelOfTheory(...) strings - _find_best_level_key_for_sp_level: core matcher — iterates all LevelOfTheory(...) keys in a data.py section, normalizes both sides, and selects by year priority: - Explicit year → exact match only - No year specified + no-year entry exists → prefer it - No year specified + only year-suffixed entries → pick latest - _find_best_across_files: searches multiple data.py locations in priority order - _warn_no_match / _all_available_years: actionable warnings listing what years are available - _extract_section: now supports section_end=None (read to EOF), fixing the freq_dict section which is last in the file 3. `arkane_level_of_theory` changes in `input.yml` The year is specified on arkane_level_of_theory, the Level object dedicated to Arkane lookups: ```yml arkane_level_of_theory: method: b97d3 basis: def2tzvp year: 2023 ``` Most users won't need year at all — the fuzzy matching handles the common case automatically. 4. Doc updates Documents the year key on arkane_level_of_theory, warns against putting year suffixes in level_of_theory, explains the fallback behavior.
2 parents 6112a7d + 0db4191 commit 73d661c

11 files changed

Lines changed: 721 additions & 124 deletions

File tree

arc/job/adapters/xtb_adapter.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,9 @@ def opt_ts(self):
328328
'futurewarning:',
329329
'userwarning:',
330330
'deprecationwarning:',
331-
'warnings.warn('
331+
'warnings.warn(',
332+
'pjrt_executable.cc',
333+
'cpu_aot_loader.cc',
332334
]
333335
real_errors = []
334336
for line in stderr:

arc/level.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class Level(object):
3939
args (Dict[Dict[str, str]], optional): Additional arguments provided to the software.
4040
Different than the ``args`` in ``LevelOfTheory``.
4141
compatible_ess (list, optional): Entries are names of compatible ESS. Not in ``LevelOfTheory``.
42+
year (int, optional): Optional 4-digit year suffix for differentiating methods such as b97d3/b97d32023.
4243
"""
4344

4445
def __init__(self,
@@ -56,6 +57,7 @@ def __init__(self,
5657
solvent: Optional[str] = None,
5758
solvation_scheme_level: Optional['Level'] = None,
5859
args: Optional[Union[Dict[str, str], Iterable, str]] = None,
60+
year: Optional[int] = None,
5961
):
6062
self.repr = repr
6163
self.method = method
@@ -88,6 +90,12 @@ def __init__(self,
8890
'Both solvation method and solvent must be defined together, or both must be None. '
8991
f'Got solvation method = "{self.solvation_method}", solvent = "{self.solvent}".'
9092
)
93+
if year is not None:
94+
self.year = int(year)
95+
if self.year < 1000 or self.year > 9999:
96+
raise ValueError(f'year must be a 4-digit integer (1000-9999), got {self.year}.')
97+
else:
98+
self.year = None
9199
self.args = args or {'keyword': dict(), 'block': dict()}
92100

93101
if self.repr is not None:
@@ -122,6 +130,8 @@ def __str__(self) -> str:
122130
str_ = self.method
123131
if self.basis is not None:
124132
str_ += f'/{self.basis}'
133+
if self.year is not None:
134+
str_ += f', year: {self.year}'
125135
if self.auxiliary_basis is not None:
126136
str_ += f', auxiliary_basis: {self.auxiliary_basis}'
127137
if self.dispersion is not None:
@@ -165,6 +175,8 @@ def simple(self) -> str:
165175
str_ = self.method
166176
if self.basis is not None:
167177
str_ += f'/{self.basis}'
178+
if self.year is not None:
179+
str_ += f' ({self.year})'
168180
return str_
169181

170182
def as_dict(self) -> dict:
@@ -196,7 +208,8 @@ def build(self):
196208
'solvation_method': None,
197209
'solvent': None,
198210
'solvation_scheme_level': None,
199-
'args': None}
211+
'args': None,
212+
'year': None}
200213
allowed_keys = list(level_dict.keys())
201214

202215
if isinstance(self.repr, str):
@@ -304,6 +317,20 @@ def lower(self):
304317

305318
self.args = args
306319

320+
def warn_if_year_set(self, attr_name: str):
321+
"""
322+
Warn if ``year`` is set on this Level. The ``year`` attribute only affects Arkane
323+
database matching and is ignored on non-``arkane_level_of_theory`` levels.
324+
325+
Args:
326+
attr_name (str): The name of the attribute this Level is assigned to (for the warning message).
327+
"""
328+
if self.year is not None:
329+
logger.warning(
330+
f'The "year" attribute on {attr_name} ({self.simple()}) has no effect. '
331+
f'Year is only used for Arkane database matching via arkane_level_of_theory.'
332+
)
333+
307334
def deduce_method_type(self):
308335
"""
309336
Determine the model chemistry type:

arc/level_test.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,27 @@ def test_build(self):
119119
"dlpno-ccsd(t)/def2-tzvp, auxiliary_basis: def2-tzvp/c, solvation_method: smd, "
120120
"solvent: water, solvation_scheme_level: 'apfd/def2-tzvp, software: gaussian', software: orca")
121121

122+
def test_year_validation(self):
123+
"""Test year validation for Level"""
124+
with self.assertRaises(ValueError):
125+
Level(method='b97d3', basis='def2tzvp', year=23)
126+
level = Level(method='b97d3', basis='def2tzvp', year=2023)
127+
self.assertEqual(level.year, 2023)
128+
129+
def test_warn_if_year_set(self):
130+
"""Test that warn_if_year_set logs a warning when year is set."""
131+
level_with_year = Level(method='b97d3', basis='def2tzvp', year=2023)
132+
with self.assertLogs('arc', level='WARNING') as cm:
133+
level_with_year.warn_if_year_set('sp_level')
134+
self.assertEqual(len(cm.output), 1)
135+
self.assertIn('sp_level', cm.output[0])
136+
self.assertIn('has no effect', cm.output[0])
137+
138+
level_no_year = Level(method='b97d3', basis='def2tzvp')
139+
with self.assertRaises(AssertionError):
140+
with self.assertLogs('arc', level='WARNING'):
141+
level_no_year.warn_if_year_set('opt_level')
142+
122143
def test_ess_methods_yml(self):
123144
"""Test reading the ess_methods.yml file"""
124145
ess_methods = read_yaml_file(path=os.path.join(ARC_PATH, 'data', 'ess_methods.yml'))

arc/main.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ def __init__(self,
406406
self.job_types['opt'] = True # Run the optimizations, self.fine_only will make sure that they are fine.
407407

408408
self.set_levels_of_theory() # All level of theories should be Level types after this call.
409+
self._warn_year_on_non_arkane_levels()
409410
if self.thermo_adapter == 'arkane':
410411
self.check_arkane_level_of_theory()
411412

@@ -1155,6 +1156,17 @@ def process_level_of_theory(self):
11551156

11561157
self.level_of_theory = '' # Reset the level_of_theory argument to avoid conflicts upon restarting ARC.
11571158

1159+
def _warn_year_on_non_arkane_levels(self):
1160+
"""
1161+
Warn if ``year`` was specified on any Level other than ``arkane_level_of_theory``.
1162+
The ``year`` attribute only affects Arkane database matching and is ignored elsewhere.
1163+
"""
1164+
for attr_name in ('sp_level', 'opt_level', 'freq_level', 'scan_level', 'irc_level',
1165+
'conformer_opt_level', 'conformer_sp_level', 'orbitals_level'):
1166+
level = getattr(self, attr_name, None)
1167+
if isinstance(level, Level):
1168+
level.warn_if_year_set(attr_name)
1169+
11581170
def check_arkane_level_of_theory(self):
11591171
"""
11601172
Check that the level of theory has AEC in Arkane.

arc/main_test.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,24 @@ def test_determine_model_chemistry_for_job_types(self):
340340
self.assertEqual(arc14.freq_level.simple(), 'pm6')
341341
self.assertEqual(arc14.sp_level.simple(), 'amber')
342342

343+
# Test explicit year in arkane_level_of_theory dictionary
344+
arc15 = ARC(project='test',
345+
sp_level='wb97xd/def2tzvp',
346+
opt_level='wb97xd/def2tzvp',
347+
arkane_level_of_theory={'method': 'wb97xd', 'basis': 'def2tzvp', 'year': 2023},
348+
bac_type=None,
349+
calc_freq_factor=False, compute_thermo=False)
350+
self.assertEqual(arc15.arkane_level_of_theory.year, 2023)
351+
352+
# Test warning when year is specified on sp_level instead of arkane_level_of_theory
353+
arc16 = ARC(project='test',
354+
sp_level={'method': 'wb97xd', 'basis': 'def2tzvp', 'year': 2023},
355+
opt_level='wb97xd/def2tzvp',
356+
calc_freq_factor=False, compute_thermo=False)
357+
with open(os.path.join(arc16.project_directory, 'arc.log'), 'r') as f:
358+
log_content = f.read()
359+
self.assertIn('"year" attribute on sp_level', log_content)
360+
343361
def test_determine_unique_species_labels(self):
344362
"""Test the determine_unique_species_labels method"""
345363
spc0 = ARCSpecies(label='spc0', smiles='CC', compute_thermo=False)

0 commit comments

Comments
 (0)