Source code for landau.phases.asewrapper
import pickle
import numpy as np
from dataclasses import dataclass
from pyiron_snippets.import_alarm import ImportAlarm
with ImportAlarm("ASE is required to use ASE phase wrappers. Install with pip install 'landau[ase]'") as ase_alarm:
try:
from ase.thermochemistry import ThermoChem
except ImportError:
# ASE >=3.28 renamed the base class.
from ase.thermochemistry import BaseThermoChem as ThermoChem
from . import AbstractLinePhase
[docs]
@dataclass(frozen=True)
class AsePhase(AbstractLinePhase):
"""
Phase wrapper for ASE's ThermoChem classes.
Equality and hashing compare ``thermochem`` by its pickled bytes so two
``AsePhase`` instances built from equivalent inputs compare equal even
though ASE's ``ThermoChem`` defaults to identity-based equality.
``atoms_per_formula`` divides the energy returned by ``thermochem`` so the
result is per atom (landau's convention). Use 2 for an ASE ``IdealGasThermo``
built around H₂ or O₂, 3 for CO₂, etc.; the default of 1 is correct
when the ASE object already represents one atom or one per-atom formula unit
(most ``HarmonicThermo`` setups, monatomic ``IdealGasThermo``).
"""
fixed_concentration: float
thermochem: 'ThermoChem'
pressure: float | None = None
atoms_per_formula: int = 1
@ase_alarm
def __post_init__(self, *args, **kwargs):
pass
@property
def line_concentration(self):
return self.fixed_concentration
[docs]
def line_free_energy(self, T):
# ASE ThermoChem subclasses do not uniformly expose both energy methods:
# HarmonicThermo/CrystalThermo only define get_helmholtz_energy, while
# IdealGasThermo only defines get_gibbs_energy. Prefer Helmholtz when
# available (landau has no pressure concept yet); fall back to Gibbs at
# self.pressure, defaulting to 1 atm.
if hasattr(self.thermochem, "get_helmholtz_energy"):
func = np.vectorize(
lambda t: self.thermochem.get_helmholtz_energy(t, verbose=False),
otypes=[float],
)
elif hasattr(self.thermochem, "get_gibbs_energy"):
pressure = self.pressure if self.pressure is not None else 101325.0
func = np.vectorize(
lambda t: self.thermochem.get_gibbs_energy(t, pressure=pressure, verbose=False),
otypes=[float],
)
else:
raise TypeError(
f"{type(self.thermochem).__name__} exposes neither get_helmholtz_energy "
"nor get_gibbs_energy; cannot compute a free energy."
)
res = func(T) / self.atoms_per_formula
if res.ndim == 0:
return res.item()
return res
def _key(self):
return (
self.name,
self.fixed_concentration,
self.pressure,
self.atoms_per_formula,
pickle.dumps(self.thermochem),
)
def __eq__(self, other):
if other.__class__ is not self.__class__:
return NotImplemented
return self._key() == other._key()
def __hash__(self):
return hash(self._key())