Source code for chemistrykit.surface.systems.dubinin

r"""Polanyi's adsorption potential and the Dubinin-Radushkevich isotherm.

M. Polanyi, *Verh. Dtsch. Phys. Ges.* 16 (1914), 1012; M. M. Dubinin and
L. V. Radushkevich, *Proc. Acad. Sci. USSR, Phys. Chem. Sect.* 55 (1947),
331. Polanyi described adsorption by the *adsorption potential*

.. math::

    A = RT\ln\frac{P_0}{P},

the isothermal work of compressing vapor from its equilibrium pressure
:math:`P` to saturation :math:`P_0`, and postulated that the filled
adsorption volume :math:`W` depends on :math:`A` alone, through a
temperature-independent *characteristic curve* :math:`W(A)`. Dubinin and
Radushkevich gave that curve an explicit form for micropore filling in
activated carbons,

.. math::

    W = W_0\exp\!\left[-\left(\frac{A}{E}\right)^2\right],

with :math:`W_0` the limiting micropore volume and :math:`E` a
characteristic energy (Gregg & Sing, *Adsorption, Surface Area and
Porosity*, 2nd ed., Ch. 4). Because :math:`\ln W` is linear in
:math:`A^2`, :func:`fit_dubinin_radushkevich` recovers :math:`(W_0, E)`
by the same shared least-squares routine as every other linearization in
this package.
"""

from __future__ import annotations

from dataclasses import dataclass

import numpy as np

from chemistrykit.constants import R
from chemistrykit.surface.core.base_system import AdsorptionIsotherm
from chemistrykit.surface.utils.regression import linear_fit

__all__ = [
    "polanyi_potential",
    "dubinin_radushkevich_loading",
    "DubininRadushkevichIsotherm",
    "DubininRadushkevichFit",
    "fit_dubinin_radushkevich",
]


[docs] def polanyi_potential(P, P0: float, T: float, R_gas: float = R): r"""Polanyi adsorption potential :math:`A = RT\ln(P_0/P)`, in J/mol. Pressures above `P0` (bulk condensation) are clipped to :math:`A=0`. Parameters ---------- P : float or array-like of float Equilibrium pressure (same units as `P0`). P0 : float Saturation vapor pressure at temperature `T`. T : float Absolute temperature, in K. R_gas : float, default :data:`chemistrykit.constants.R` Returns ------- float or ndarray Examples -------- At saturation the potential vanishes; at :math:`P=P_0/e` it equals :math:`RT`: >>> polanyi_potential(P=1.0, P0=1.0, T=77.0) 0.0 >>> round(polanyi_potential(P=np.exp(-1.0), P0=1.0, T=100.0, R_gas=8.0), 10) 800.0 """ P = np.asarray(P, dtype=np.float64) result = R_gas * T * np.log(P0 / P) result = np.maximum(result, 0.0) return float(result) if result.ndim == 0 else result
[docs] def dubinin_radushkevich_loading(P, W0: float, E: float, P0: float, T: float, R_gas: float = R): r"""Dubinin-Radushkevich micropore filling :math:`W = W_0\exp[-(A/E)^2]`. Parameters ---------- P : float or array-like of float Equilibrium pressure. W0 : float Limiting micropore volume (or loading) at :math:`P=P_0`. E : float Characteristic energy, in J/mol. P0 : float Saturation vapor pressure at `T`. T : float Absolute temperature, in K. R_gas : float, default :data:`chemistrykit.constants.R` Returns ------- float or ndarray Examples -------- When the adsorption potential equals the characteristic energy (:math:`A=E`), the pores are filled to exactly :math:`W_0/e`: >>> T, E = 300.0, 10.0e3 >>> P = 1.0 * np.exp(-E / (8.31446261815324 * T)) >>> round(float(dubinin_radushkevich_loading(P, W0=2.0, E=E, P0=1.0, T=T) * np.e), 8) 2.0 """ A = np.asarray(polanyi_potential(P, P0, T, R_gas), dtype=np.float64) result = W0 * np.exp(-((A / E) ** 2)) return float(result) if result.ndim == 0 else result
[docs] class DubininRadushkevichIsotherm(AdsorptionIsotherm): r"""The Dubinin-Radushkevich isotherm at a fixed temperature. Parameters ---------- W0 : float Limiting micropore volume (saturation loading). E : float Characteristic energy, in J/mol. P0 : float Saturation vapor pressure at `T`. T : float Absolute temperature, in K. Examples -------- >>> iso = DubininRadushkevichIsotherm(W0=0.5, E=12e3, P0=1.0, T=300.0) >>> round(float(iso.fractional_coverage(1.0)), 10) 1.0 """ def __init__(self, W0: float, E: float, P0: float, T: float): if W0 <= 0 or E <= 0 or P0 <= 0 or T <= 0: raise ValueError("W0, E, P0 and T must all be positive") self.W0 = float(W0) self.E = float(E) self.P0 = float(P0) self.T = float(T) self.saturation_loading = self.W0
[docs] def loading(self, P): return dubinin_radushkevich_loading(P, self.W0, self.E, self.P0, self.T)
[docs] @dataclass class DubininRadushkevichFit: """Result of fitting loading-vs-pressure data to the Dubinin-Radushkevich isotherm.""" W0: float """float: Fitted limiting micropore volume.""" E: float """float: Fitted characteristic energy, in J/mol.""" r_squared: float """float: Coefficient of determination of the linearized (``ln W`` vs ``A^2``) fit."""
[docs] def fit_dubinin_radushkevich(P, W, P0: float, T: float, R_gas: float = R) -> DubininRadushkevichFit: r"""Fit data to the Dubinin-Radushkevich isotherm via :math:`\ln W = \ln W_0 - A^2/E^2`. Parameters ---------- P : array-like of float Equilibrium pressures, all below `P0`. W : array-like of float Corresponding adsorbed volumes (all positive). P0 : float Saturation vapor pressure at `T`. T : float Absolute temperature, in K. R_gas : float, default :data:`chemistrykit.constants.R` Returns ------- DubininRadushkevichFit Examples -------- >>> P = np.array([1e-4, 1e-3, 1e-2, 0.05, 0.1, 0.3]) >>> W = dubinin_radushkevich_loading(P, W0=0.4, E=9.0e3, P0=1.0, T=77.0) >>> fit = fit_dubinin_radushkevich(P, W, P0=1.0, T=77.0) >>> round(fit.W0, 6), round(fit.E, 3) (0.4, 9000.0) """ A = np.asarray(polanyi_potential(P, P0, T, R_gas), dtype=np.float64) W = np.asarray(W, dtype=np.float64) fit = linear_fit(A**2, np.log(W)) return DubininRadushkevichFit(W0=float(np.exp(fit.intercept)), E=float(1.0 / np.sqrt(-fit.slope)), r_squared=fit.r_squared)