"""The ORCA keyword model: what vibe-sketch knows how to say in ORCA.
The ORCA writer translates a program-neutral job specification through these
tables and nothing else. A method, basis set, auxiliary basis set,
approximation, dispersion correction or solvent that is not here is refused
with the reason (AGENTS.md, ground rule 5); extending what ORCA jobs can do
means adding a row here, with a test.
Spellings follow the ORCA 6 manual. Names are matched without regard to case,
and a few common spellings map to ORCA's (``M06-2X`` to ``M062X``,
``D3(BJ)`` to ``D3BJ``).
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from typing import Final, Literal
__all__ = [
"APPROXIMATIONS",
"Method",
"approximation_keyword",
"aux_basis_kind",
"basis_family",
"dispersion_keyword",
"known_methods",
"method_for",
"solvent_name",
]
MethodClass = Literal["hf", "dft", "double-hybrid", "composite", "mp2", "coupled-cluster"]
[docs]
@dataclass(frozen=True)
class Method:
"""One ORCA method keyword and what it can do."""
keyword: str
kind: MethodClass
#: The method carries its own dispersion correction (D3, D4 or VV10).
has_dispersion: bool = False
#: ORCA has analytic second derivatives for it, so ``Freq`` works as written.
analytic_hessian: bool = True
def _methods() -> dict[str, Method]:
table: list[Method] = [Method("HF", "hf")]
for keyword in (
"BP86", "BLYP", "PBE", "revPBE", "TPSS", "SCAN", "r2SCAN",
"B3LYP", "B3PW91", "PBE0", "TPSSh", "TPSS0", "M06", "M062X", "BHandHLYP",
"X3LYP", "CAM-B3LYP", "wB97X", "LC-BLYP", "r2SCAN0", "PW6B95",
): # fmt: skip
table.append(Method(keyword, "dft"))
for keyword in ("B97-D3", "wB97X-D3", "wB97X-D4", "wB97M-D4"):
table.append(Method(keyword, "dft", has_dispersion=True))
for keyword in ("wB97X-V", "wB97M-V"): # VV10 nonlocal correlation
table.append(Method(keyword, "dft", has_dispersion=True, analytic_hessian=False))
for keyword in ("B2PLYP", "DSD-PBEP86", "PWPB95"):
table.append(Method(keyword, "double-hybrid", analytic_hessian=False))
for keyword in ("HF-3c", "PBEh-3c", "B97-3c", "r2SCAN-3c", "wB97X-3c"):
table.append(Method(keyword, "composite", has_dispersion=True))
for keyword in ("MP2", "RI-MP2"):
table.append(Method(keyword, "mp2", analytic_hessian=False))
for keyword in ("CCSD", "CCSD(T)", "DLPNO-CCSD", "DLPNO-CCSD(T)"):
table.append(Method(keyword, "coupled-cluster", analytic_hessian=False))
known = {method.keyword.lower(): method for method in table}
known["m06-2x"] = known["m062x"]
return known
_METHODS: Final = _methods()
[docs]
def method_for(name: str) -> Method | None:
"""The ORCA method for ``name``, or ``None`` if vibe-sketch does not know it."""
return _METHODS.get(name.lower())
[docs]
def known_methods() -> list[str]:
"""Every method keyword in the table, for refusals and listings."""
return sorted({method.keyword for method in _METHODS.values()}, key=str.lower)
#: Basis-set families by pattern; the canonical spelling is rebuilt from the match.
_BASIS_FAMILIES: Final[list[tuple[str, re.Pattern[str]]]] = [
("def2", re.compile(r"^(ma-)?def2-(m?SVPD?|TZVPP?D?|QZVPP?D?)$", re.IGNORECASE)),
("pople", re.compile(
r"^(STO-3G|3-21G|6-31(1)?\+{0,2}G" # 6-31G, 6-311++G ...
r"(\*{1,2}|\(\d?[df]{1,2}(,\d?[pd]{1,2})?\))?)$", # *, **, (d,p), (2df,2pd)
re.IGNORECASE,
)),
("dunning", re.compile(r"^(aug-)?cc-p(w?C)?V[DTQ56]Z$", re.IGNORECASE)),
("jensen", re.compile(r"^(aug-)?pc(seg|Sseg|J)?-[0-4]$", re.IGNORECASE)),
("x2c", re.compile(r"^x2c-(SVPall|TZVPall|TZVPPall|QZVPall)(-s)?$", re.IGNORECASE)),
] # fmt: skip
[docs]
def basis_family(name: str) -> str | None:
"""The family of a basis set ORCA knows by this name, or ``None``."""
for family, pattern in _BASIS_FAMILIES:
if pattern.match(name):
return family
return None
_AUX_PATTERNS: Final[list[tuple[str, re.Pattern[str]]]] = [
("J", re.compile(r"^(def2|SARC|x2c)/J$", re.IGNORECASE)),
("JK", re.compile(r"^(def2|(aug-)?cc-pV[DTQ5]Z)/JK$", re.IGNORECASE)),
("C", re.compile(r"^((def2-(SVP|TZVPP?|QZVPP))|(aug-)?cc-pV[DTQ5]Z)/C$", re.IGNORECASE)),
("auto", re.compile(r"^AutoAux$", re.IGNORECASE)),
]
[docs]
def aux_basis_kind(name: str) -> str | None:
"""``J``, ``JK``, ``C`` or ``auto`` for an auxiliary basis set ORCA knows, else ``None``."""
for kind, pattern in _AUX_PATTERNS:
if pattern.match(name):
return kind
return None
#: RI approximations, and the auxiliary basis kinds each can use.
APPROXIMATIONS: Final = {
"RIJCOSX": ("J", "auto"),
"RIJK": ("JK", "auto"),
"RIJONX": ("J", "auto"),
}
_APPROXIMATION_ALIASES: Final = {"ri-j": "RIJONX", "rij": "RIJONX"}
[docs]
def approximation_keyword(name: str) -> str | None:
"""ORCA's keyword for an RI approximation, or ``None``."""
lowered = name.lower()
for keyword in APPROXIMATIONS:
if keyword.lower() == lowered:
return keyword
return _APPROXIMATION_ALIASES.get(lowered)
_DISPERSION: Final = {
"d2": "D2",
"d3": "D3ZERO",
"d3zero": "D3ZERO",
"d3(0)": "D3ZERO",
"d3bj": "D3BJ",
"d3(bj)": "D3BJ",
"d4": "D4",
}
[docs]
def dispersion_keyword(name: str) -> str | None:
"""ORCA's keyword for a dispersion correction, or ``None``."""
return _DISPERSION.get(name.lower())
#: Solvents ORCA's CPCM and SMD both know, by the name vibe-sketch accepts.
_SOLVENTS: Final = {
"water": "water",
"h2o": "water",
"methanol": "methanol",
"ethanol": "ethanol",
"acetonitrile": "acetonitrile",
"mecn": "acetonitrile",
"acetone": "acetone",
"dmso": "DMSO",
"dimethylsulfoxide": "DMSO",
"dmf": "DMF",
"n,n-dimethylformamide": "DMF",
"thf": "THF",
"tetrahydrofuran": "THF",
"dichloromethane": "CH2Cl2",
"ch2cl2": "CH2Cl2",
"dcm": "CH2Cl2",
"chloroform": "chloroform",
"chcl3": "chloroform",
"toluene": "toluene",
"benzene": "benzene",
"hexane": "hexane",
"n-hexane": "hexane",
"cyclohexane": "cyclohexane",
"diethylether": "diethylether",
"diethyl ether": "diethylether",
"pyridine": "pyridine",
"ammonia": "ammonia",
"octanol": "octanol",
"ethyl acetate": "ethylacetate",
"ethylacetate": "ethylacetate",
"carbon tetrachloride": "CCl4",
"ccl4": "CCl4",
"1,4-dioxane": "dioxane",
"dioxane": "dioxane",
}
[docs]
def solvent_name(name: str) -> str | None:
"""ORCA's name for a solvent, or ``None``."""
return _SOLVENTS.get(name.lower().strip())