diff --git a/arc/level.py b/arc/level.py index 50d2062d2a..76ff606f55 100644 --- a/arc/level.py +++ b/arc/level.py @@ -39,6 +39,7 @@ class Level(object): args (Dict[Dict[str, str]], optional): Additional arguments provided to the software. Different than the ``args`` in ``LevelOfTheory``. compatible_ess (list, optional): Entries are names of compatible ESS. Not in ``LevelOfTheory``. + year (int, optional): Optional 4-digit year suffix for differentiating methods such as b97d3/b97d32023. """ def __init__(self, @@ -56,6 +57,7 @@ def __init__(self, solvent: Optional[str] = None, solvation_scheme_level: Optional['Level'] = None, args: Optional[Union[Dict[str, str], Iterable, str]] = None, + year: Optional[int] = None, ): self.repr = repr self.method = method @@ -88,6 +90,12 @@ def __init__(self, 'Both solvation method and solvent must be defined together, or both must be None. ' f'Got solvation method = "{self.solvation_method}", solvent = "{self.solvent}".' ) + if year is not None: + self.year = int(year) + if self.year < 1000 or self.year > 9999: + raise ValueError(f'year must be a 4-digit integer (1000-9999), got {self.year}.') + else: + self.year = None self.args = args or {'keyword': dict(), 'block': dict()} if self.repr is not None: @@ -122,6 +130,8 @@ def __str__(self) -> str: str_ = self.method if self.basis is not None: str_ += f'/{self.basis}' + if self.year is not None: + str_ += f', year: {self.year}' if self.auxiliary_basis is not None: str_ += f', auxiliary_basis: {self.auxiliary_basis}' if self.dispersion is not None: @@ -165,6 +175,8 @@ def simple(self) -> str: str_ = self.method if self.basis is not None: str_ += f'/{self.basis}' + if self.year is not None: + str_ += f' ({self.year})' return str_ def as_dict(self) -> dict: @@ -196,7 +208,8 @@ def build(self): 'solvation_method': None, 'solvent': None, 'solvation_scheme_level': None, - 'args': None} + 'args': None, + 'year': None} allowed_keys = list(level_dict.keys()) if isinstance(self.repr, str): diff --git a/arc/level_test.py b/arc/level_test.py index 8a39542db6..32fd965c13 100644 --- a/arc/level_test.py +++ b/arc/level_test.py @@ -119,6 +119,13 @@ def test_build(self): "dlpno-ccsd(t)/def2-tzvp, auxiliary_basis: def2-tzvp/c, solvation_method: smd, " "solvent: water, solvation_scheme_level: 'apfd/def2-tzvp, software: gaussian', software: orca") + def test_year_validation(self): + """Test year validation for Level""" + with self.assertRaises(ValueError): + Level(method='b97d3', basis='def2tzvp', year=23) + level = Level(method='b97d3', basis='def2tzvp', year=2023) + self.assertEqual(level.year, 2023) + def test_ess_methods_yml(self): """Test reading the ess_methods.yml file""" ess_methods = read_yaml_file(path=os.path.join(ARC_PATH, 'data', 'ess_methods.yml')) diff --git a/arc/statmech/arkane.py b/arc/statmech/arkane.py index 15b9eeed11..317336c16e 100644 --- a/arc/statmech/arkane.py +++ b/arc/statmech/arkane.py @@ -6,7 +6,7 @@ import re import shutil from abc import ABC -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, List, Optional, Tuple from mako.template import Template @@ -52,7 +52,8 @@ species('${spc['label']}', '${spc['path']}'${spc['pdep_data'] if 'pdep_data' in spc else ''}, structure=SMILES('${spc['smiles']}'), spinMultiplicity=${spc['multiplicity']}) % else: -species('${spc['label']}', '${spc['path']}'${spc['pdep_data'] if 'pdep_data' in spc else ''}) +species('${spc['label']}', '${spc['path']}'${spc['pdep_data'] if 'pdep_data' in spc else ''}, + spinMultiplicity=${spc['multiplicity']}) % endif % endfor @@ -326,8 +327,7 @@ def render_arkane_input_template(self, species_list.append({'label': spc.label, 'path': spc.yml_path or os.path.join(statmech_dir, 'species', f'{spc.label}.py'), 'smiles': spc.mol.copy(deep=True).to_smiles() if not spc.is_ts else '', - 'multiplicity': spc.multiplicity, - }) + 'multiplicity': spc.multiplicity}) ts_list = [{'label': rxn.ts_species.label, 'path': rxn.ts_species.yml_path or os.path.join(statmech_dir, 'TSs', f'{rxn.ts_species.label}.py')} for rxn in self.reactions] if self.reactions else list() @@ -520,6 +520,13 @@ def run_arkane(statmech_dir: str) -> None: shell_script = rf'''bash -lc 'set -euo pipefail cd "{statmech_dir}" +# Limit BLAS thread counts so OpenBLAS/MKL cannot explode into 48 threads +export OPENBLAS_NUM_THREADS=1 +export MKL_NUM_THREADS=1 +export OMP_NUM_THREADS=1 +export NUMEXPR_NUM_THREADS=1 +export MKL_DYNAMIC=FALSE + if command -v micromamba >/dev/null 2>&1; then micromamba run -n {env_name} {arkane_cmd} elif command -v conda >/dev/null 2>&1 || command -v mamba >/dev/null 2>&1; then @@ -673,6 +680,199 @@ def _section_contains_key(file_path: str, section_start: str, section_end: str, return False +def _normalize_method(method: str) -> str: + """ + Normalize method names for comparison: + - lowercase + - remove all hyphens + + Examples: + "DLPNO-CCSD(T)-F12" -> "dlpnoccsd(t)f12" + "dlpnoccsd(t)f122023" -> "dlpnoccsd(t)f122023" + """ + return method.lower().replace('-', '') + + +def _split_method_year(method_norm: str) -> "Tuple[str, Optional[int]]": + """ + Split a normalized method into (base_method, year). + + Examples: + "dlpnoccsd(t)f122023" -> ("dlpnoccsd(t)f12", 2023) + "dlpnoccsd(t)f12" -> ("dlpnoccsd(t)f12", None) + """ + m = re.match(r"^(.*?)(\d{4})$", method_norm) + if not m: + return method_norm, None + base, year_str = m.groups() + return base, int(year_str) + + +def _normalize_basis(basis: Optional[str]) -> Optional[str]: + """ + Normalize basis names for comparison: + - lowercase + - remove hyphens and spaces + + Examples: + "cc-pVTZ-F12" -> "ccpvtzf12" + "ccpvtzf12" -> "ccpvtzf12" + """ + if basis is None: + return None + return basis.replace('-', '').replace(' ', '').lower() + + +def _parse_lot_params(lot_str: str) -> dict: + """ + Parse method, basis, and software from a LevelOfTheory(...) string. + + Example lot_str: + "LevelOfTheory(method='dlpnoccsd(t)f122023',basis='ccpvtzf12',software='orca')" + """ + params = {'method': None, 'basis': None, 'software': None} + for key in params.keys(): + m = re.search(rf"{key}='([^']+)'", lot_str) + if m: + params[key] = m.group(1) + return params + + +def _iter_level_keys_from_section(file_path: str, + section_start: str, + section_end: str) -> list[str]: + """ + Return all LevelOfTheory(...) key strings that appear as dictionary keys + in a given section of data.py. + + These look like: + "LevelOfTheory(method='...',basis='...',software='...')" : { ... } + """ + section = _extract_section(file_path, section_start, section_end) + if section is None: + return [] + + # Match things like: "LevelOfTheory(...)" : { ... } + pattern = r'"(LevelOfTheory\([^"]*\))"\s*:' + return re.findall(pattern, section, flags=re.DOTALL) + + +def _available_years_for_level(level: "Level", + file_path: str, + section_start: str, + section_end: str) -> list[Optional[int]]: + """ + Return a sorted list of available year suffixes for a given Level in a section. + """ + if level is None or level.method is None: + return [] + + target_method_norm = _normalize_method(level.method) + target_base, _ = _split_method_year(target_method_norm) + target_basis_norm = _normalize_basis(level.basis) + target_software = level.software.lower() if level.software else None + + years = set() + for lot_str in _iter_level_keys_from_section(file_path, section_start, section_end): + params = _parse_lot_params(lot_str) + cand_method = params.get('method') + cand_basis = params.get('basis') + cand_sw = params.get('software') + + if cand_method is None: + continue + + cand_method_norm = _normalize_method(cand_method) + cand_base, cand_year = _split_method_year(cand_method_norm) + + if cand_base != target_base: + continue + if target_basis_norm is not None: + cand_basis_norm = _normalize_basis(cand_basis) + if cand_basis_norm != target_basis_norm: + continue + if target_software is not None and cand_sw is not None: + if cand_sw.lower() != target_software: + continue + + years.add(cand_year) + + # Sort with None first to represent "no year suffix" + return sorted(years, key=lambda y: (-1 if y is None else y)) + + +def _format_years(years: list[Optional[int]]) -> str: + """ + Format a list of years for logging. + """ + if not years: + return "none" + return ", ".join("none" if y is None else str(y) for y in years) + + +def _find_best_level_key_for_sp_level(level: "Level", + file_path: str, + section_start: str, + section_end: str) -> Optional[str]: + """ + Given an ARC Level and a data.py section, find the LevelOfTheory(...) key string + that best matches the level's method/basis, allowing: + - hyphen-insensitive comparison + - an optional 4-digit year suffix in Arkane's method + and choose the *no-year* entry when no year is specified. + """ + if level is None or level.method is None: + return None + + target_method_norm = _normalize_method(level.method) + target_base, method_year = _split_method_year(target_method_norm) + target_year = getattr(level, 'year', None) if getattr(level, 'year', None) is not None else method_year + target_basis_norm = _normalize_basis(level.basis) + target_software = level.software.lower() if level.software else None + + best_key = None + + for lot_str in _iter_level_keys_from_section(file_path, section_start, section_end): + params = _parse_lot_params(lot_str) + cand_method = params.get('method') + cand_basis = params.get('basis') + cand_sw = params.get('software') + + if cand_method is None: + continue + + cand_method_norm = _normalize_method(cand_method) + cand_base, cand_year = _split_method_year(cand_method_norm) + + # method base must match + if cand_base != target_base: + continue + + # basis must match (normalized), if we have one + if target_basis_norm is not None: + cand_basis_norm = _normalize_basis(cand_basis) + if cand_basis_norm != target_basis_norm: + continue + + # if user specified software, prefer matching software; + # but don't *require* it to exist in data.py + if target_software is not None and cand_sw is not None: + if cand_sw.lower() != target_software: + continue + + if target_year is not None: + if cand_year != target_year: + continue + best_key = lot_str + break + else: + if cand_year is None: + best_key = lot_str + break + + return best_key + + def _level_to_str(level: 'Level') -> str: """ Convert Level to Arkane's LevelOfTheory string representation. @@ -683,12 +883,16 @@ def _level_to_str(level: 'Level') -> str: Returns: str: LevelOfTheory string representation. """ - parts = [f"method='{level.method}'"] + method = _normalize_method(level.method) + if getattr(level, 'year', None) is not None and not method.endswith(str(level.year)): + method = f"{method}{level.year}" + + parts = [f"method='{method}'"] if level.basis: - parts.append(f"basis='{level.basis}'") + parts.append(f"basis='{_normalize_basis(level.basis)}'") if level.software: - parts.append(f"software='{level.software}'") - return f"LevelOfTheory({','.join(parts)})".replace('-','') + parts.append(f"software='{level.software.lower()}'") + return f"LevelOfTheory({','.join(parts)})".replace('-', '') def get_arkane_model_chemistry(sp_level: 'Level', @@ -698,6 +902,14 @@ def get_arkane_model_chemistry(sp_level: 'Level', """ Get Arkane model chemistry string with database validation. + Reads RMG's quantum_corrections/data.py as plain text, searches for + LevelOfTheory(...) keys, and matches: + - method: ignoring hyphens and optional 4-digit year suffix + - basis: ignoring hyphens and spaces + + If multiple entries only differ by year, the one with the *latest* year + is chosen (year=0 if no year in that entry). + Args: sp_level (Level): Level of theory for energy. freq_level (Optional[Level]): Level of theory for frequencies. @@ -706,9 +918,6 @@ def get_arkane_model_chemistry(sp_level: 'Level', Returns: Optional[str]: Arkane-compatible model chemistry string. """ - if sp_level.method_type == 'composite': - return f"LevelOfTheory(method='{sp_level.method}',software='gaussian')" - qm_corr_file = os.path.join(RMG_DB_PATH, 'input', 'quantum_corrections', 'data.py') atom_energies_start = "atom_energies = {" @@ -716,40 +925,99 @@ def get_arkane_model_chemistry(sp_level: 'Level', freq_dict_start = "freq_dict = {" freq_dict_end = "}" - sp_repr = _level_to_str(sp_level) - quoted_sp_repr = f'"{sp_repr}"' - + if sp_level.method_type == 'composite': + # Composite Gaussian methods: prefer best year match from DB, fall back to normalized LevelOfTheory. + best_energy = _find_best_level_key_for_sp_level( + sp_level, qm_corr_file, atom_energies_start, atom_energies_end + ) + if best_energy is None: + years = _available_years_for_level(sp_level, qm_corr_file, atom_energies_start, atom_energies_end) + if getattr(sp_level, 'year', None) is not None: + logger.warning( + f"No Arkane AEC entry found for year {sp_level.year} at {sp_level.simple()}; " + f"available years: {_format_years(years)}" + ) + elif years: + logger.warning( + f"No Arkane AEC entry found for {sp_level.simple()} without a year; " + f"available years: {_format_years(years)}. " + f"Specify a year to select a matching entry." + ) + return _level_to_str(sp_level) + return best_energy + + # ---- Case 1: User supplied explicit frequency scale factor ---- + # We only need an energy level (AEC entry in atom_energies) if freq_scale_factor is not None: - found = _section_contains_key(file_path=qm_corr_file, - section_start=atom_energies_start, - section_end=atom_energies_end, - target=quoted_sp_repr) - if not found: + best_energy = _find_best_level_key_for_sp_level( + sp_level, qm_corr_file, atom_energies_start, atom_energies_end + ) + if best_energy is None: + years = _available_years_for_level(sp_level, qm_corr_file, atom_energies_start, atom_energies_end) + if getattr(sp_level, 'year', None) is not None: + logger.warning( + f"No Arkane AEC entry found for year {sp_level.year} at {sp_level.simple()}; " + f"available years: {_format_years(years)}" + ) + elif years: + logger.warning( + f"No Arkane AEC entry found for {sp_level.simple()} without a year; " + f"available years: {_format_years(years)}. " + f"Specify a year to select a matching entry." + ) + # No matching AEC level in Arkane DB return None - return sp_repr + # modelChemistry = LevelOfTheory(...) + return best_energy + # ---- Case 2: CompositeLevelOfTheory (separate freq and energy levels) ---- if freq_level is None: raise ValueError("freq_level required when freq_scale_factor isn't provided") - freq_repr = _level_to_str(freq_level) - quoted_freq_repr = f'"{freq_repr}"' - - found_sp = _section_contains_key(file_path=qm_corr_file, - section_start=atom_energies_start, - section_end=atom_energies_end, - target=quoted_sp_repr) - found_freq = _section_contains_key(file_path=qm_corr_file, - section_start=freq_dict_start, - section_end=freq_dict_end, - target=quoted_freq_repr) + best_energy = _find_best_level_key_for_sp_level( + sp_level, qm_corr_file, atom_energies_start, atom_energies_end + ) + best_freq = _find_best_level_key_for_sp_level( + freq_level, qm_corr_file, freq_dict_start, freq_dict_end + ) - if not found_sp or not found_freq: + if best_energy is None or best_freq is None: + if best_energy is None: + years = _available_years_for_level(sp_level, qm_corr_file, atom_energies_start, atom_energies_end) + if getattr(sp_level, 'year', None) is not None: + logger.warning( + f"No Arkane AEC entry found for year {sp_level.year} at {sp_level.simple()}; " + f"available years: {_format_years(years)}" + ) + elif years: + logger.warning( + f"No Arkane AEC entry found for {sp_level.simple()} without a year; " + f"available years: {_format_years(years)}. " + f"Specify a year to select a matching entry." + ) + if best_freq is None: + years = _available_years_for_level(freq_level, qm_corr_file, freq_dict_start, freq_dict_end) + if getattr(freq_level, 'year', None) is not None: + logger.warning( + f"No Arkane frequency correction entry found for year {freq_level.year} at {freq_level.simple()}; " + f"available years: {_format_years(years)}" + ) + elif years: + logger.warning( + f"No Arkane frequency correction entry found for {freq_level.simple()} without a year; " + f"available years: {_format_years(years)}. " + f"Specify a year to select a matching entry." + ) + # If either is missing, cannot construct a valid composite model chemistry return None - return (f"CompositeLevelOfTheory(\n" - f" freq={freq_repr},\n" - f" energy={sp_repr}\n" - f")") + # These strings are LevelOfTheory(...) expressions usable directly in Arkane input + return ( + "CompositeLevelOfTheory(\n" + f" freq={best_freq},\n" + f" energy={best_energy}\n" + ")" + ) def check_arkane_bacs(sp_level: 'Level', @@ -759,13 +1027,11 @@ def check_arkane_bacs(sp_level: 'Level', """ Check that Arkane has AECs and BACs for the given sp level of theory. - Args: - sp_level (Level): Level of theory for energy. - bac_type (str): Type of bond additivity correction ('p' for Petersson, 'm' for Melius) - raise_error (bool): Whether to raise an error if AECs or BACs are missing. - - Returns: - bool: True if both AECs and BACs are available, False otherwise. + Uses plain-text parsing of quantum_corrections/data.py, matching LevelOfTheory + keys by: + - method base (ignore hyphens + optional year) + - basis (normalized) + and picking the latest year where multiple exist. """ qm_corr_file = os.path.join(RMG_DB_PATH, 'input', 'quantum_corrections', 'data.py') @@ -778,24 +1044,39 @@ def check_arkane_bacs(sp_level: 'Level', bac_section_start = "pbac = {" bac_section_end = "mbac = {" - sp_repr = _level_to_str(sp_level) - quoted_sp_repr = f'"{sp_repr}"' - - has_aec = _section_contains_key( - file_path=qm_corr_file, - section_start=atom_energies_start, - section_end=atom_energies_end, - target=quoted_sp_repr, + best_aec_key = _find_best_level_key_for_sp_level( + sp_level, qm_corr_file, atom_energies_start, atom_energies_end ) - has_bac = _section_contains_key( - file_path=qm_corr_file, - section_start=bac_section_start, - section_end=bac_section_end, - target=quoted_sp_repr, + best_bac_key = _find_best_level_key_for_sp_level( + sp_level, qm_corr_file, bac_section_start, bac_section_end ) + + has_aec = best_aec_key is not None + has_bac = best_bac_key is not None has_encorr = bool(has_aec and has_bac) + + # For logging, prefer the matched key; fall back to the naive LevelOfTheory string + repr_level = best_aec_key if best_aec_key is not None else _level_to_str(sp_level) + if not has_encorr: - mssg = f"Arkane does not have the required energy corrections for {sp_repr} (AEC: {has_aec}, BAC: {has_bac})" + year_note = "" + aec_years = _available_years_for_level(sp_level, qm_corr_file, atom_energies_start, atom_energies_end) + bac_years = _available_years_for_level(sp_level, qm_corr_file, bac_section_start, bac_section_end) + if getattr(sp_level, 'year', None) is not None: + year_note = ( + f" Available AEC years: {_format_years(aec_years)}; " + f"available BAC years: {_format_years(bac_years)}." + ) + elif aec_years or bac_years: + year_note = ( + f" Available AEC years: {_format_years(aec_years)}; " + f"available BAC years: {_format_years(bac_years)}. " + f"Specify a year to select a matching entry." + ) + mssg = ( + f"Arkane does not have the required energy corrections for {repr_level} " + f"(AEC: {has_aec}, BAC: {has_bac}).{year_note}" + ) if raise_error: raise ValueError(mssg) else: @@ -803,6 +1084,7 @@ def check_arkane_bacs(sp_level: 'Level', return has_encorr + def parse_species_thermo(species, output_content: str) -> None: """Parse thermodynamic data for a single species.""" # Parse E0 diff --git a/arc/statmech/arkane_test.py b/arc/statmech/arkane_test.py index 3e7bac2f22..52e2ac16b5 100644 --- a/arc/statmech/arkane_test.py +++ b/arc/statmech/arkane_test.py @@ -120,6 +120,8 @@ def test_level_to_str(self): "LevelOfTheory(method='b3lyp',basis='631g(d)',software='gaussian')") self.assertEqual(_level_to_str(Level(method='CCSD(T)-F12', basis='cc-pVTZ-F12')), "LevelOfTheory(method='ccsd(t)f12',basis='ccpvtzf12',software='molpro')") + self.assertEqual(_level_to_str(Level(method='b97d3', basis='def2tzvp', software='gaussian', year=2023)), + "LevelOfTheory(method='b97d32023',basis='def2tzvp',software='gaussian')") def test_section_contains_key(self): """Test the _section_contains_key function""" @@ -144,7 +146,21 @@ def test_get_arkane_model_chemistry(self): "LevelOfTheory(method='ccsd(t)f12',basis='ccpvtzf12',software='molpro')") self.assertEqual(get_arkane_model_chemistry(sp_level=Level(method='CBS-QB3'), freq_scale_factor=1.0), - "LevelOfTheory(method='cbs-qb3',software='gaussian')") + "LevelOfTheory(method='cbsqb32023',software='gaussian')") + + def test_get_arkane_model_chemistry_year_not_found(self): + """Test warnings when a requested year is not found in the Arkane database.""" + level = Level(method='b97d3', basis='def2tzvp', software='gaussian', year=2099) + with self.assertLogs('arc', level='WARNING') as cm: + model_chemistry = get_arkane_model_chemistry(sp_level=level, freq_scale_factor=1.0) + self.assertIsNone(model_chemistry) + self.assertTrue(any('available years' in msg for msg in cm.output)) + + def test_get_arkane_model_chemistry_latest_year(self): + """Test selecting the latest available year when no year is specified.""" + model_chemistry = get_arkane_model_chemistry(sp_level=Level(method='CBS-QB3'), + freq_scale_factor=1.0) + self.assertEqual(model_chemistry, "LevelOfTheory(method='cbsqb32023',software='gaussian')") def test_generate_arkane_input(self): """Test generating Arkane input""" diff --git a/docs/source/advanced.rst b/docs/source/advanced.rst index c9276cbd3e..1dfc14a8d8 100644 --- a/docs/source/advanced.rst +++ b/docs/source/advanced.rst @@ -95,9 +95,23 @@ Another example:: 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. +You can also provide a 4-digit ``year`` to distinguish method variants (e.g., ``b97d3`` vs ``b97d32023``):: + sp_level: {'method': 'b97d3', + 'basis': 'def2tzvp', + 'year': 2023} -THe following are examples for **equivalent** definitions:: +Equivalent ``input.yml`` form:: + + sp_level: + method: b97d3 + basis: def2tzvp + year: 2023 + +If ``year`` is omitted, ARC will match the newest available entry for that method/basis in the Arkane database. + + +The following are examples for **equivalent** definitions:: opt_level = 'apfd/def2tzvp' opt_level = {'method': 'apfd', 'basis': 'def2tzvp'} @@ -138,6 +152,10 @@ is equivalent to:: scan_level = {'method': 'wb97xd', 'basis': 'def2svp'} sp_level = {'method': 'wb97xd', 'basis': 'def2svp'} +Note: Year suffixes in the method (e.g., ``wb97xd32023``) are meant for Arkane database matching +and are not valid QC methods. Do not include year suffixes in ``level_of_theory``; instead, set +``arkane_level_of_theory`` with a ``year`` value if you need a specific correction year. + Note: If ``level_of_theory`` does not contain any deliminator (neither ``//`` nor ``\/``), it is interpreted as a composite method. diff --git a/docs/source/examples.rst b/docs/source/examples.rst index 7c9b245cf4..f5ef9af56b 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -84,6 +84,10 @@ To specify a composite method, simply define something like:: level_of_theory: CBS-QB3 +Note: Do not include year suffixes in ``level_of_theory`` (e.g., ``wb97xd32023``). Year suffixes are +for Arkane database matching only and are not valid QC methods. If you need a specific correction year, +set ``arkane_level_of_theory`` with a ``year`` value instead. + Note that for composite methods the ``freq_level`` and ``scan_level`` may have different default values than for non-composite methods (defined in settings.py). Note: an independent frequencies calculation job is automatically executed after a composite job just so that the Hamiltonian will