Skip to content

Commit 37510c9

Browse files
committed
Update
1 parent 6e1da8e commit 37510c9

4 files changed

Lines changed: 138 additions & 27 deletions

File tree

arc/statmech/arkane.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -712,18 +712,9 @@ def _section_contains_key(file_path: str, section_start: str, section_end: str,
712712

713713
def _get_qm_corrections_files() -> List[str]:
714714
"""
715-
Return quantum corrections data.py paths, preferring ARC-local data.
716-
717-
Preference order:
718-
1) ARC-local database/data directories (if present)
719-
2) RMG-database
715+
Return quantum corrections data.py paths from the RMG database.
720716
"""
721717
candidates = [
722-
os.path.join(ARC_PATH, 'arc', 'database', 'input', 'quantum_corrections', 'data.py'),
723-
os.path.join(ARC_PATH, 'arc', 'database', 'quantum_corrections', 'data.py'),
724-
os.path.join(ARC_PATH, 'arc', 'data', 'input', 'quantum_corrections', 'data.py'),
725-
os.path.join(ARC_PATH, 'arc', 'data', 'quantum_corrections', 'data.py'),
726-
os.path.join(ARC_PATH, 'data', 'quantum_corrections.py'),
727718
os.path.join(RMG_DB_PATH, 'input', 'quantum_corrections', 'data.py'),
728719
]
729720
return [path for path in candidates if os.path.isfile(path)]
@@ -809,7 +800,7 @@ def _iter_level_keys_from_section(file_path: str,
809800
def _available_years_for_level(level: "Level",
810801
file_path: str,
811802
section_start: str,
812-
section_end: str) -> list[Optional[int]]:
803+
section_end: str) -> List[Optional[int]]:
813804
"""
814805
Return a sorted list of available year suffixes for a given Level in a section.
815806
"""
@@ -850,7 +841,7 @@ def _available_years_for_level(level: "Level",
850841
return sorted(years, key=lambda y: (-1 if y is None else y))
851842

852843

853-
def _format_years(years: list[Optional[int]]) -> str:
844+
def _format_years(years: List[Optional[int]]) -> str:
854845
"""
855846
Format a list of years for logging.
856847
"""
@@ -875,7 +866,14 @@ def _find_best_level_key_for_sp_level(level: "Level",
875866

876867
target_method_norm = _normalize_method(level.method)
877868
target_base, method_year = _split_method_year(target_method_norm)
878-
target_year = getattr(level, 'year', None) if getattr(level, 'year', None) is not None else method_year
869+
explicit_year = getattr(level, 'year', None)
870+
if explicit_year is not None and method_year is not None and explicit_year != method_year:
871+
raise InputError(
872+
f"Conflicting year specifications for level '{level}': "
873+
f"explicit year={explicit_year}, method suffix year={method_year}. "
874+
"Please remove the year suffix from the method name or update the 'year' attribute to match."
875+
)
876+
target_year = explicit_year if explicit_year is not None else method_year
879877
target_basis_norm = _normalize_basis(level.basis)
880878
target_software = level.software.lower() if level.software else None
881879

@@ -957,14 +955,16 @@ def get_arkane_model_chemistry(sp_level: 'Level',
957955
"""
958956
Get Arkane model chemistry string with database validation.
959957
960-
Reads quantum_corrections/data.py as plain text (prefers ARC-local overrides,
961-
then RMG-database), searches for
958+
Reads quantum_corrections/data.py as plain text, searches for
962959
LevelOfTheory(...) keys, and matches:
963960
- method: ignoring hyphens and optional 4-digit year suffix
964961
- basis: ignoring hyphens and spaces
965962
966-
If multiple entries only differ by year, the one with the *latest* year
967-
is chosen (year=0 if no year in that entry).
963+
When a year is explicitly specified in the Level, only entries with that exact
964+
year are matched. If no year is specified and an entry without a year exists,
965+
that entry is used. Only when no year is specified and no no-year entry exists,
966+
if multiple entries differ only by year, the one with the *latest* year is
967+
chosen (treating entries with no year as year=0).
968968
969969
Args:
970970
sp_level (Level): Level of theory for energy.
@@ -1005,7 +1005,8 @@ def get_arkane_model_chemistry(sp_level: 'Level',
10051005
f"available years: {_format_years(years)}. "
10061006
f"Specify a year to select a matching entry."
10071007
)
1008-
return _level_to_str(sp_level)
1008+
# No matching AEC level in Arkane DB for this composite method
1009+
return None
10091010
return best_energy
10101011

10111012
# ---- Case 1: User supplied explicit frequency scale factor ----
@@ -1098,8 +1099,7 @@ def check_arkane_bacs(sp_level: 'Level',
10981099
"""
10991100
Check that Arkane has AECs and BACs for the given sp level of theory.
11001101
1101-
Uses plain-text parsing of quantum_corrections/data.py (prefers ARC-local overrides,
1102-
then RMG-database), matching LevelOfTheory
1102+
Uses plain-text parsing of quantum_corrections/data.py, matching LevelOfTheory
11031103
keys by:
11041104
- method base (ignore hyphens + optional year)
11051105
- basis (normalized)

arc/statmech/arkane_test.py

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import os
99
import shutil
10+
import tempfile
1011
import unittest
1112

1213
from arc.common import ARC_PATH, ARC_TESTING_PATH
@@ -15,8 +16,19 @@
1516
from arc.species import ARCSpecies
1617
from arc.statmech.adapter import StatmechEnum
1718
from arc.statmech.arkane import ArkaneAdapter
18-
from arc.statmech.arkane import _level_to_str, _section_contains_key, get_arkane_model_chemistry
19-
from arc.imports import settings
19+
from arc.statmech.arkane import (
20+
_available_years_for_level,
21+
_find_best_level_key_for_sp_level,
22+
_get_qm_corrections_files,
23+
_level_to_str,
24+
_normalize_basis,
25+
_normalize_method,
26+
_parse_lot_params,
27+
_section_contains_key,
28+
_split_method_year,
29+
get_arkane_model_chemistry,
30+
)
31+
from unittest.mock import patch
2032

2133

2234
class TestEnumerationClasses(unittest.TestCase):
@@ -180,6 +192,105 @@ def test_get_arkane_model_chemistry_latest_year(self):
180192
freq_scale_factor=1.0)
181193
self.assertEqual(model_chemistry, "LevelOfTheory(method='cbsqb3',software='gaussian')")
182194

195+
def test_level_helpers(self):
196+
"""Test helper functions for method/basis/year parsing."""
197+
self.assertEqual(_normalize_method("DLPNO-CCSD(T)-F12"), "dlpnoccsd(t)f12")
198+
self.assertEqual(_normalize_method("dlpnoccsd(t)f122023"), "dlpnoccsd(t)f122023")
199+
200+
base, year = _split_method_year("dlpnoccsd(t)f122023")
201+
self.assertEqual(base, "dlpnoccsd(t)f12")
202+
self.assertEqual(year, 2023)
203+
base, year = _split_method_year("dlpnoccsd(t)f12")
204+
self.assertEqual(base, "dlpnoccsd(t)f12")
205+
self.assertIsNone(year)
206+
207+
self.assertEqual(_normalize_basis("cc-pVTZ-F12"), "ccpvtzf12")
208+
self.assertEqual(_normalize_basis("ccpvtz f12"), "ccpvtzf12")
209+
210+
params = _parse_lot_params(
211+
"LevelOfTheory(method='dlpnoccsd(t)f122023',basis='ccpvtzf12',software='orca')"
212+
)
213+
self.assertEqual(params["method"], "dlpnoccsd(t)f122023")
214+
self.assertEqual(params["basis"], "ccpvtzf12")
215+
self.assertEqual(params["software"], "orca")
216+
217+
def test_level_key_selection(self):
218+
"""Test matching of LevelOfTheory keys by year and no-year preference."""
219+
section = '\n'.join([
220+
'atom_energies = {',
221+
" \"LevelOfTheory(method='cbsqb3',software='gaussian')\": {},",
222+
" \"LevelOfTheory(method='cbsqb32023',software='gaussian')\": {},",
223+
"}",
224+
"pbac = {",
225+
])
226+
with tempfile.NamedTemporaryFile(mode="w+", delete=False) as f:
227+
f.write(section)
228+
path = f.name
229+
try:
230+
level = Level(method="CBS-QB3", software="gaussian")
231+
best = _find_best_level_key_for_sp_level(level, path, "atom_energies = {", "pbac = {")
232+
self.assertEqual(best, "LevelOfTheory(method='cbsqb3',software='gaussian')")
233+
234+
level_year = Level(method="CBS-QB3", software="gaussian", year=2023)
235+
best_year = _find_best_level_key_for_sp_level(level_year, path, "atom_energies = {", "pbac = {")
236+
self.assertEqual(best_year, "LevelOfTheory(method='cbsqb32023',software='gaussian')")
237+
238+
years = _available_years_for_level(level, path, "atom_energies = {", "pbac = {")
239+
self.assertEqual(years, [None, 2023])
240+
finally:
241+
os.remove(path)
242+
243+
def test_conflicting_year_spec(self):
244+
"""Test conflicting year in method suffix vs explicit year."""
245+
section = '\n'.join([
246+
'atom_energies = {',
247+
" \"LevelOfTheory(method='b97d32023',software='gaussian')\": {},",
248+
"}",
249+
"pbac = {",
250+
])
251+
with tempfile.NamedTemporaryFile(mode="w+", delete=False) as f:
252+
f.write(section)
253+
path = f.name
254+
try:
255+
level = Level(method="b97d32023", software="gaussian", year=2022)
256+
with self.assertRaises(InputError):
257+
_find_best_level_key_for_sp_level(level, path, "atom_energies = {", "pbac = {")
258+
finally:
259+
os.remove(path)
260+
261+
def test_qm_corrections_file_path(self):
262+
"""Test quantum corrections files are read from the RMG database path."""
263+
with tempfile.TemporaryDirectory() as rmg_root:
264+
rmg_qc = os.path.join(rmg_root, 'input', 'quantum_corrections', 'data.py')
265+
os.makedirs(os.path.dirname(rmg_qc), exist_ok=True)
266+
with open(rmg_qc, 'w') as f:
267+
f.write('# rmg qc\n')
268+
269+
with patch('arc.statmech.arkane.RMG_DB_PATH', rmg_root):
270+
paths = _get_qm_corrections_files()
271+
self.assertTrue(paths)
272+
self.assertEqual(paths[0], rmg_qc)
273+
274+
def test_get_arkane_model_chemistry_from_qm_file(self):
275+
"""Test reading LevelOfTheory keys from a quantum corrections file."""
276+
section = '\n'.join([
277+
'atom_energies = {',
278+
" \"LevelOfTheory(method='cbsqb3',software='gaussian')\": {},",
279+
"}",
280+
"pbac = {",
281+
])
282+
with tempfile.NamedTemporaryFile(mode="w+", delete=False) as f:
283+
f.write(section)
284+
path = f.name
285+
try:
286+
with patch('arc.statmech.arkane._get_qm_corrections_files', return_value=[path]):
287+
model_chemistry = get_arkane_model_chemistry(
288+
sp_level=Level(method='CBS-QB3'),
289+
freq_scale_factor=1.0,
290+
)
291+
self.assertEqual(model_chemistry, "LevelOfTheory(method='cbsqb3',software='gaussian')")
292+
finally:
293+
os.remove(path)
183294
def test_generate_arkane_input(self):
184295
"""Test generating Arkane input"""
185296
statmech_dir = os.path.join(ARC_TESTING_PATH, 'arkane_input_tests_delete')

docs/source/advanced.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,9 @@ is equivalent to::
154154
sp_level = {'method': 'wb97xd', 'basis': 'def2svp'}
155155

156156
Note: Year suffixes in the method (e.g., ``wb97xd32023``) are meant for Arkane database matching
157-
and are not valid QC methods. Do not include year suffixes in ``level_of_theory``; instead, set
158-
``arkane_level_of_theory`` with a ``year`` value if you need a specific correction year.
157+
and are not valid QC methods. Do not include year suffixes in ``level_of_theory``; instead, specify a
158+
``year`` key on ``sp_level`` if you need a specific correction year. ``arkane_level_of_theory`` can still
159+
be used to explicitly override Arkane behavior.
159160

160161
Note: If ``level_of_theory`` does not contain any deliminator (neither ``//`` nor ``\/``), it is interpreted as a
161162
composite method.

docs/source/examples.rst

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,8 @@ To specify a composite method, simply define something like::
8686

8787
Note: Do not include year suffixes in ``level_of_theory`` (e.g., ``wb97xd32023``). Year suffixes are
8888
for Arkane database matching only and are not valid QC methods. If you need a specific correction year,
89-
set ``arkane_level_of_theory`` with a ``year`` value instead.
90-
If ``year`` is omitted, ARC will prefer the no-year Arkane entry for that method/basis; if none exists,
91-
ARC will fall back to the latest available year in the Arkane database.
89+
specify a ``year`` key on ``sp_level``. If ``year`` is omitted, ARC will prefer the no-year Arkane entry for
90+
that method/basis; if none exists, ARC will fall back to the latest available year in the Arkane database.
9291

9392
Note that for composite methods the ``freq_level`` and ``scan_level`` may have different
9493
default values than for non-composite methods (defined in settings.py). Note: an independent

0 commit comments

Comments
 (0)