Source code for atomistics.workflows.evcurve.thermo

from copy import copy
from typing import Optional

import numpy as np
import scipy.constants


class ThermoBulk:
    """
    Class should provide all tools to compute bulk thermodynamic quantities. Central quantity is the Free Energy F(V,T).
    ToDo: Make it a (light weight) pyiron object (introduce a new tool rather than job object).
    """

    eV_to_J_per_mol = scipy.constants.electron_volt * scipy.constants.Avogadro
    kB = 1 / scipy.constants.physical_constants["Boltzmann constant in eV/K"][0]

    def __init__(self):
        self._volumes: Optional[np.ndarray] = None
        self._temperatures: Optional[np.ndarray] = None
        self._energies: Optional[np.ndarray] = None
        self._entropy: Optional[np.ndarray] = None
        self._pressure: Optional[np.ndarray] = None
        self._num_atoms: Optional[int] = None

        self._fit_order = 3

[docs] def copy(self) -> "ThermoBulk": """ Returns: A copy of the ThermoBulk object. """ cls = self.__class__ result = cls.__new__(cls) result.__init__() result.__dict__["_volumes"] = copy(self._volumes) result.__dict__["_temperatures"] = copy(self._temperatures) result.__dict__["_energies"] = copy(self._energies) result.__dict__["_fit_order"] = self._fit_order return result
def _reset_energy(self): """ Reset the energy array. """ if self._volumes is not None and self._temperatures is not None: self._energies = np.zeros((len(self._temperatures), len(self._volumes))) @property def num_atoms(self) -> int: """ Get the number of atoms. Returns: The number of atoms. """ if self._num_atoms is None: return 1 # normalize per cell if number of atoms unknown return self._num_atoms @num_atoms.setter def num_atoms(self, num: int): """ Set the number of atoms. Args: num: The number of atoms. """ self._num_atoms = num @property def _coeff(self) -> np.ndarray: """ Get the coefficients of the polynomial fit. Returns: The coefficients of the polynomial fit. """ return np.polyfit(self._volumes, self._energies.T, deg=self._fit_order) @property def temperatures(self) -> np.ndarray: """ Get the temperatures. Returns: The temperatures. """ return self._temperatures @property def _d_temp(self) -> float: """ Get the temperature step size. Returns: The temperature step size. """ return self.temperatures[1] - self.temperatures[0] @property def _d_vol(self) -> float: """ Get the volume step size. Returns: The volume step size. """ return self.volumes[1] - self.volumes[0] @temperatures.setter def temperatures(self, temp_lst: np.ndarray): """ Set the temperatures. Args: temp_lst: The temperatures. """ if not hasattr(temp_lst, "__len__"): raise ValueError("Requires list as input parameter") len_temp = -1 if self._temperatures is not None: len_temp = len(self._temperatures) self._temperatures = np.array(temp_lst) if len(temp_lst) != len_temp: self._reset_energy() @property def volumes(self) -> np.ndarray: """ Get the volumes. Returns: The volumes. """ return self._volumes @volumes.setter def volumes(self, volume_lst: np.ndarray): """ Set the volumes. Args: volume_lst: The volumes. """ if not hasattr(volume_lst, "__len__"): raise ValueError("Requires list as input parameter") len_vol = -1 if self._volumes is not None: len_vol = len(self._volumes) self._volumes = np.array(volume_lst) if len(volume_lst) != len_vol: self._reset_energy() @property def entropy(self) -> np.ndarray: """ Get the entropy. Returns: The entropy. """ if self._entropy is None: self._compute_thermo() return self._entropy @property def pressure(self) -> np.ndarray: """ Get the pressure. Returns: The pressure. """ if self._pressure is None: self._compute_thermo() return self._pressure @property def energies(self) -> np.ndarray: """ Get the energies. Returns: The energies. """ return self._energies @energies.setter def energies(self, erg_lst: np.ndarray): """ Set the energies. Args: erg_lst: The energies. """ if np.ndim(erg_lst) == 2: self._energies = erg_lst elif np.ndim(erg_lst) == 1: if len(erg_lst) == len(self.volumes): self._energies = np.tile(erg_lst, (len(self.temperatures), 1)) else: raise ValueError() else: self._energies = ( np.ones((len(self.volumes), len(self.temperatures))) * erg_lst )
[docs] def set_temperatures( self, temperature_min: float = 0.0, temperature_max: float = 1500.0, temperature_steps: float = 50.0, ): """ Set the temperatures. Args: temperature_min: The minimum temperature. temperature_max: The maximum temperature. temperature_steps: The number of temperature steps. """ self.temperatures = np.linspace( temperature_min, temperature_max, temperature_steps )
[docs] def set_volumes( self, volume_min: float, volume_max: float = None, volume_steps: int = 10 ): """ Set the volumes. Args: volume_min: The minimum volume. volume_max: The maximum volume. volume_steps: The number of volume steps. """ if volume_max is None: volume_max = 1.1 * volume_min self.volumes = np.linspace(volume_min, volume_max, volume_steps)
[docs] def meshgrid(self) -> np.ndarray: """ Create a meshgrid of volumes and temperatures. Returns: The meshgrid of volumes and temperatures. """ return np.meshgrid(self.volumes, self.temperatures)
[docs] def get_minimum_energy_path(self, pressure: np.ndarray = None) -> np.ndarray: """ Get the minimum energy path. Args: pressure: The pressure. Returns: The minimum energy path. """ if pressure is not None: raise NotImplementedError() v_min_lst = [] for c in self._coeff.T: v_min = np.roots(np.polyder(c, 1)) p_der2 = np.polyder(c, 2) p_val2 = np.polyval(p_der2, v_min) v_m_lst = v_min[p_val2 > 0] if len(v_m_lst) > 0: v_min_lst.append(v_m_lst[0]) else: v_min_lst.append(np.nan) return np.array(v_min_lst)
[docs] def get_free_energy( self, vol: np.ndarray, pressure: np.ndarray = None ) -> np.ndarray: """ Get the free energy. Args: vol: The volume. pressure: The pressure. Returns: The free energy. """ if not pressure: return np.polyval(self._coeff, vol) else: raise NotImplementedError()
[docs] def interpolate_volume(self, volumes: np.ndarray, fit_order: int = None): """ Interpolate the volumes. Args: volumes: The volumes. fit_order: The order of the polynomial fit. Returns: The interpolated volume. """ if fit_order is not None: self._fit_order = fit_order new = self.copy() new.volumes = volumes new.energies = np.array([np.polyval(self._coeff, v) for v in volumes]).T return new
def _compute_thermo(self): """ Compute the thermodynamic quantities. """ self._entropy, self._pressure = np.gradient( -self.energies, self._d_temp, self._d_vol )
[docs] def get_free_energy_p(self) -> np.ndarray: """ Get the free energy at the minimum energy path. Returns: The free energy at the minimum energy path. """ coeff = np.polyfit(self._volumes, self.energies.T, deg=self._fit_order) return np.polyval(coeff, self.get_minimum_energy_path())
[docs] def get_entropy_p(self) -> np.ndarray: """ Get the entropy at the minimum energy path. Returns: The entropy at the minimum energy path. """ s_coeff = np.polyfit(self._volumes, self.entropy.T, deg=self._fit_order) return np.polyval(s_coeff, self.get_minimum_energy_path())
[docs] def get_entropy_v(self) -> np.ndarray: """ Get the entropy at constant volume. Returns: The entropy at constant volume. """ eq_volume = self.volumes[0] s_coeff = np.polyfit(self.volumes, self.entropy.T, deg=self._fit_order) const_v = eq_volume * np.ones(len(s_coeff.T)) return np.polyval(s_coeff, const_v)
[docs] def plot_free_energy(self): """ Plot the free energy. """ try: import pylab as plt except ImportError: import matplotlib.pyplot as plt plt.plot(self.temperatures, self.get_free_energy_p() / self.num_atoms) plt.xlabel("Temperature [K]") plt.ylabel("Free energy [eV]")
[docs] def plot_entropy(self): """ Plot the entropy. """ try: import pylab as plt except ImportError: import matplotlib.pyplot as plt plt.plot( self.temperatures, self.eV_to_J_per_mol / self.num_atoms * self.get_entropy_p(), label="S$_p$", ) plt.plot( self.temperatures, self.eV_to_J_per_mol / self.num_atoms * self.get_entropy_v(), label="S$_V$", ) plt.legend() plt.xlabel("Temperature [K]") plt.ylabel("Entropy [J K$^{-1}$ mol-atoms$^{-1}$]")
[docs] def plot_heat_capacity(self, to_kB: bool = True): """ Plot the heat capacity. Args: to_kB: Convert the heat capacity to kB units. """ try: import pylab as plt except ImportError: import matplotlib.pyplot as plt if to_kB: units = self.kB / self.num_atoms plt.ylabel("Heat capacity [kB]") else: units = self.eV_to_J_per_mol plt.ylabel("Heat capacity [J K$^{-1}$ mol-atoms$^{-1}$]") temps = self.temperatures[:-2] c_p = temps * np.gradient(self.get_entropy_p(), self._d_temp)[:-2] c_v = temps * np.gradient(self.get_entropy_v(), self._d_temp)[:-2] plt.plot(temps, units * c_p, label="c$_p$") plt.plot(temps, units * c_v, label="c$_v$") plt.legend(loc="lower right") plt.xlabel("Temperature [K]")
[docs] def contour_pressure(self) -> None: """ Plot the contour of pressure. """ try: import pylab as plt except ImportError: import matplotlib.pyplot as plt x, y = self.meshgrid() p_coeff = np.polyfit(self.volumes, self.pressure.T, deg=self._fit_order) p_grid = np.array([np.polyval(p_coeff, v) for v in self._volumes]).T plt.contourf(x, y, p_grid) plt.plot(self.get_minimum_energy_path(), self.temperatures) plt.xlabel("Volume [$\AA^3$]") plt.ylabel("Temperature [K]")
[docs] def contour_entropy(self) -> None: """ Plot the contour of entropy. """ try: import pylab as plt except ImportError: import matplotlib.pyplot as plt s_coeff = np.polyfit(self.volumes, self.entropy.T, deg=self._fit_order) s_grid = np.array([np.polyval(s_coeff, v) for v in self.volumes]).T x, y = self.meshgrid() plt.contourf(x, y, s_grid) plt.plot(self.get_minimum_energy_path(), self.temperatures) plt.xlabel("Volume [$\AA^3$]") plt.ylabel("Temperature [K]")
[docs] def plot_contourf(self, ax=None, show_min_erg_path=False): """ Plot the contourf of energies. Args: ax: The matplotlib axes object. show_min_erg_path: Whether to show the minimum energy path. Returns: The matplotlib axes object. """ try: import pylab as plt except ImportError: import matplotlib.pyplot as plt x, y = self.meshgrid() if ax is None: fig, ax = plt.subplots(1, 1) ax.contourf(x, y, self.energies) if show_min_erg_path: plt.plot(self.get_minimum_energy_path(), self.temperatures, "w--") plt.xlabel("Volume [$\AA^3$]") plt.ylabel("Temperature [K]") return ax
[docs] def plot_min_energy_path(self, *args, ax=None, **qwargs): """ Plot the minimum energy path. Args: *args: Additional arguments to pass to the plot function. ax: The matplotlib axes object. **qwargs: Additional keyword arguments to pass to the plot function. Returns: The matplotlib axes object. """ try: import pylab as plt except ImportError: import matplotlib.pyplot as plt if ax is None: fig, ax = plt.subplots(1, 1) ax.xlabel("Volume [$\AA^3$]") ax.ylabel("Temperature [K]") ax.plot(self.get_minimum_energy_path(), self.temperatures, *args, **qwargs) return ax
def get_thermo_bulk_model(temperatures: np.ndarray, debye_model) -> ThermoBulk: """ Get the thermo bulk model. Args: temperatures: The temperatures. debye_model: The Debye model. Returns: The thermo bulk model. """ thermo = ThermoBulk() thermo.temperatures = temperatures thermo.volumes = debye_model.volume thermo.energies = ( debye_model.interpolate() + debye_model.energy_vib(T=thermo.temperatures, low_T_limit=True).T ) return thermo