Source code for mbe_step.thermo
# -*- coding: utf-8 -*-
"""Energy offsets onto the DfE0 scale from SEAMM's thermochemistry database.
A configuration's labels are on the high level's absolute energy scale (the
VASP cell's pseudopotential reference is replaced by the high-level monomers').
The training sets use the energy of formation at 0 K instead, DfE0 = E -
sum(atomic reference energies) + sum(atomic heats of formation at 0 K), with the
atomic references at the same level. For a molecule the difference DfE0 - E is a
constant, so it enters as an offset per molecule type, exactly the number the
code steps' own DfE0 uses (seamm_thermochemistry.formation_energy, anchored at
0 K).
"""
from collections import Counter
from seamm_mbe.units import EV_TO_KJ_PER_MOL
#: The parameter value that asks for the offsets from the database
AUTOMATIC = "DfE0 (from the thermochemistry database)"
[docs]
def is_automatic(text):
"""Whether the 'energy offsets' parameter asks for DfE0 offsets."""
return (text or "").strip().lower() in (AUTOMATIC.lower(), "dfe0")
[docs]
def reference(model_chemistry, provider=None):
"""(code, method, settings) under which the database holds the atomic
references for a model chemistry. A program can say with a
``thermochemistry_reference(model_chemistry)`` hook on its step; otherwise
the code is the step's name in lower case, the method the model
chemistry's (in its model-chemistry spelling, as the reference runs were
tagged) and the settings its basis, without a ``bse:`` prefix."""
hook = getattr(provider, "thermochemistry_reference", None)
if hook is not None:
return hook(model_chemistry)
code = str(model_chemistry.get("step") or model_chemistry.get("owner")).lower()
method = model_chemistry.get("method") or ""
basis = model_chemistry.get("basis") or ""
if basis.lower().startswith("bse:"):
basis = basis[4:]
return code, method, basis
[docs]
def compositions(systems):
"""{type name: Counter of element symbols} for the molecule types of
seamm_mbe Systems."""
result = {}
for system in systems:
for molecule in system.molecules:
if molecule.type not in result:
result[molecule.type] = Counter(
system.symbols[i] for i in molecule.atoms
)
return result
[docs]
def dfe0_offsets(model_chemistry, systems, provider=None):
"""The offsets DfE0 - E in eV per molecule, by type, at a model chemistry.
Raises
------
ValueError
If seamm_thermochemistry is not installed or has no atomic references
for the elements at this level.
"""
try:
from seamm_thermochemistry import ThermoDB, formation_energy
except ImportError:
raise ValueError(
"The energy offsets onto the DfE0 scale need seamm_thermochemistry, "
"which is not installed. Set 'energy offsets' to 'none', or give them."
) from None
code, method, settings = reference(model_chemistry, provider)
level = model_chemistry.get("level", f"{code}:{method}/{settings}")
offsets = {}
try:
database = ThermoDB(read_only=True)
except (FileNotFoundError, OSError) as e:
raise ValueError(
"The energy offsets onto the DfE0 scale need the thermochemistry "
f"database, which is not installed here ({e}). Install it with "
"seamm-thermochemistry's installer, or set 'energy offsets' to 'none' "
"or give them."
) from None
with database as db:
for name, composition in sorted(compositions(systems).items()):
try:
kj = formation_energy(
dict(composition),
0.0,
db,
code,
method,
settings=settings,
anchor=True,
anchor_at_0K=True,
units="kJ/mol",
)
except Exception as e:
raise ValueError(
f"No DfE0 energy offset for {name} at {level}: the "
f"thermochemistry database has no atomic references for it "
f"({type(e).__name__}: {e}). Set 'energy offsets' to 'none' "
"for absolute energies, or give the offsets as 'type value'."
) from None
offsets[name] = kj / EV_TO_KJ_PER_MOL
return offsets