Source code for CADETProcess.equilibria.buffer_capacity

from collections import defaultdict
import copy

import numpy as np

from CADETProcess import CADETProcessError
from CADETProcess import plotting
from CADETProcess.processModel import ComponentSystem


def preprocessing(reaction_system, buffer, pH=None, components=None):
    buffer = np.array(buffer, ndmin=2)
    buffer_M = 1e-3*buffer

    component_system = copy.deepcopy(reaction_system.component_system)

    indices = component_system.indices
    if components is not None:
        for comp in indices.copy():
            if comp not in components:
                indices.pop(comp)

    try:
        proton_index = indices.pop('H+')
    except ValueError:
        raise CADETProcessError("Could not find proton in component system")

    if pH is None:
        pH = -np.log10(buffer_M[:, proton_index]).reshape((-1))
    else:
        pH = np.asarray(pH, dtype='float64')

    scalar_input = False
    if pH.ndim == 0:
        pH = pH[None]  # Makes x 1D
        scalar_input = True

    pKa = defaultdict(list)
    for r in reaction_system.reactions:
        reaction_indices = np.where(r.stoich)[0]
        for comp, i in indices.items():
            if not all(r_i in i + proton_index for r_i in reaction_indices):
                continue

            pKa[comp].append(-np.log10(r.k_eq*1e-3))

    c_acids_M = {
        comp: buffer_M[:, i]
        for comp, i in indices.items()
        if comp in pKa
    }

    for comp in indices.copy():
        if comp not in pKa:
            indices.pop(comp)

    return pKa, c_acids_M, pH, indices, scalar_input


def c_species_nu(pKa, pH):
    """Compute normalized acid species concentration at given pH.

    Parameters
    ----------
    pKa : list
        List of pKa values.
    pH : float or list of floats.
        pH value

    Returns
    -------
    c_species_nu : np.array
        Normalized acid species concentration.

    """
    pKa = np.array([1.0] + pKa)
    k_eq = 10**(-pKa)
    n = len(k_eq)

    c_H = np.power(10, -pH)
    c_species_nu = np.zeros((n, len(pH)))

    for j in range(n):
        k = np.prod(k_eq[0:j+1])
        c_species_nu[j] = k*c_H**(n-j)

    return c_species_nu


def c_total_nu(pKa, pH):
    """Compute normalized total acid concentration at given pH.

    Parameters
    ----------
    pKa : list
        List of pKa values
    pH : float or list of floats.
        pH value

    Returns
    -------
    c_total_nu : np.array
        Normalized acid species concentration.

    """
    return sum(c_species_nu(pKa, pH))


def z_total_nu(pKa, pH):
    """Compute normalized total charge at given pH.

    Parameters
    ----------
    pKa : list
        List of pKa values
    pH : float or list of floats.
        pH value

    Returns
    -------
    z_total_nu : np.array
        Normalized acid species concentration.

    """
    c = c_species_nu(pKa, pH)

    return np.dot(np.arange(len(c)), c)


def eta(pKa, pH):
    """Compute degree of dissociation at given pH.

    Parameters
    ----------
    pKa : list
        List of pKa values
    pH : float or list of floats.
        pH value

    Returns
    -------
    eta : np.array
        Degree of dissociation.

    """
    return z_total_nu(pKa, pH)/c_total_nu(pKa, pH)


def charge_distribution(reaction_system, pH, components=None):
    """Calculate charge distribution at given pH.

    Parameters
    ----------
    reaction_system : ReactionModel
        Reaction system with deprotonation reactions.
    pH : float or array
        pH value of buffer.
    components : list, optional
        List of components to be considered in buffer capacity calculation.
        If None, all components are considered.

    Returns
    -------
    charge_distribution : np.array
        Degree of protolysis; ratio of the concentration of the species to the
        total concentration.

    """
    buffer = reaction_system.n_comp * [1]
    pKa, c_acids_M, pH, indices, scalar_input = preprocessing(
        reaction_system, buffer, pH, components
    )

    if components is None:
        z_shape = (len(pH), reaction_system.n_comp - 1)
    else:
        n_comp = 0
        for comp in indices.values():
            n_comp += len(comp)
        z_shape = (len(pH), n_comp)

    z = np.zeros(z_shape)

    counter = 0
    for comp, ind in indices.items():
        z_comp = alpha(pKa[comp], pH)
        for j in range(len(ind)):
            z[:, counter] = z_comp[j, :]
            counter += 1

    if scalar_input:
        return np.squeeze(z)

    return z


def cummulative_charge_distribution(reaction_system, pH, components=None):
    """Calculate cummulative charge at given pH.

    Parameters
    ----------
    reaction_system : ReactionModel
        Reaction system with deprotonation reactions.
    buffer : list
        Acid concentrations in mM.
    pH : float or array
        pH value of buffer.
    components : list, optional
        List of components to be considered in buffer capacity calculation.
        If None, all components are considerd.

    Returns
    -------
    cummulative_charge_distribution : np.array
        Degree of dissociation;
    """
    buffer = reaction_system.n_comp * [1]
    pKa, c_acids_M, pH, indices, scalar_input = preprocessing(
        reaction_system, buffer, pH, components
    )

    z_cum = np.zeros((len(pH), len(indices)))

    for i, (comp, ind) in enumerate(indices.items()):
        charges = np.array(reaction_system.component_system.charges)[ind]
        max_charge = max(charges)
        z_cum[:, i] = max_charge - eta(pKa[comp], pH)

    if scalar_input:
        return np.squeeze(z_cum)

    return z_cum


def alpha(pKa, pH):
    """Compute degree of protolysis at given pH.

    Parameters
    ----------
    pKa : list
        List of pKa values
    pH : float or list of floats.
        pH value

    Returns
    -------
    alpha : np.array
        Degree of protolysis.
    """
    return c_species_nu(pKa, pH)/c_total_nu(pKa, pH)


def beta(c_acid, pKa, pH):
    """Compute buffer capacity of acid at given pH.

    Parameters
    ----------
    c_acid : TYPE
        DESCRIPTION.
    pKa : list
        List of pKa values
    pH : float or list of floats.
        pH value

    Returns
    -------
    beta : np.array
        Buffer capacity.
    """
    a = alpha(pKa, pH)
    beta = np.zeros(len(pH),)

    n = c_acid.shape[1]
    for j in range(1, n):
        for i in range(0, j):
            beta += (j-i)**2 * a[j] * a[i]

    beta *= np.log(10) * np.sum(c_acid, axis=1)

    return beta


def beta_water(pH):
    """Compute buffer capacity of water.

    Parameters
    ----------
    pH : float or list of floats.
        pH value

    Returns
    -------
    beta_water
        Buffer capacity of water.
    """
    c_H = 10**(-pH)
    return np.log(10)*(10**(-14)/c_H + c_H)


[docs] def buffer_capacity(reaction_system, buffer, pH=None, components=None): """Calculate buffer capacity at given buffer concentration and pH. Parameters ---------- reaction_system : ReactionModel Reaction system with deprotonation reactions. buffer : list Acid concentrations in mM. pH : float or array, optional pH value of buffer. If None, value is inferred from buffer entry. components : list, optional List of components to be considered in buffer capacity calculation. If None, all components are considerd. Returns ------- buffer_capacity : np.array Buffer capacity in mM for individual acid components. To get overall buffer capacity, component capacities must be summed up. """ pKa, c_acids_M, pH, indices, scalar_input = preprocessing( reaction_system, buffer, pH, components ) buffer_capacity = np.zeros((len(pH), len(c_acids_M)+1)) for i, comp in enumerate(indices): buffer_capacity[:, i] = beta(c_acids_M[comp], pKa[comp], pH) buffer_capacity[:, -1] = beta_water(pH) buffer_capacity *= 1e3 if scalar_input: return np.squeeze(buffer_capacity) return buffer_capacity
def ionic_strength(component_system, buffer): """Compute ionic strength. Parameters ---------- buffer : list Buffer concentrations in mM. component_system : ComponentSystem Component system; must contain charges. Returns ------- i: np.array Ionic strength of buffer """ if not isinstance(component_system, ComponentSystem): raise TypeError("Expected ComponentSystem") if len(buffer) != component_system.n_comp: raise CADETProcessError("Number of components does not match") buffer = np.asarray(buffer, dtype='float64') z = np.asarray(component_system.charges) return 1/2 * np.sum(buffer*z**2) @plotting.create_and_save_figure def plot_buffer_capacity(reaction_system, buffer, pH=None, ax=None): """Plot buffer capacity of reaction system over pH at given concentration. Parameters ---------- reaction_system : MassActionLaw Reaction system with stoichiometric coefficients and reaction rates. buffer : list Buffer concentration in mM. pH : np.array, optional Range of pH to be plotted. ax : Axes Axes to plot on. Returns ------- ax : Axes Axes object with buffer capacity plot. """ if pH is None: pH = np.linspace(0, 14, 101) b = buffer_capacity(reaction_system, buffer, pH) b_total = np.sum(b, axis=1) labels = reaction_system.component_system.names labels.remove('H+') for i in range(reaction_system.component_system.n_components - 1): ax.plot(pH, b[:, i], label=labels[i]) ax.plot(pH, b[:, -1], label='Water') ax.plot(pH, b_total, 'k--', label='Total buffer capacity') layout = plotting.Layout() layout.x_label = '$pH$' layout.y_label = 'buffer capacity / mM' layout.y_lim = (0, 1.1*np.max(b_total)) plotting.set_layout(ax, layout) return ax @plotting.create_and_save_figure def plot_charge_distribution( reaction_system, pH=None, plot_cumulative=False, ax=None): """Plot charge distribution of components over pH. Parameters ---------- reaction_system : MassActionLaw Reaction system with stoichiometric coefficients and reaction rates. pH : np.array, optional Range of pH to be plotted. plot_cumulative : Bool If True, only plot cumulative charge of each acid. ax : Axes Axes to plot on. Returns ------- ax : Axes Axes object with charge distribution plot. """ if pH is None: pH = np.linspace(0, 14, 101) layout = plotting.Layout() if plot_cumulative: c = cummulative_charge_distribution(reaction_system, pH) layout.y_label = 'degree of dissociation' else: c = charge_distribution(reaction_system, pH) layout.y_label = 'degree of protolysis' if plot_cumulative: labels = reaction_system.component_system.names else: labels = reaction_system.component_system.species labels.remove('H+') for i, l in zip(c.T, labels): ax.plot(pH, i, label=l) layout.x_label = '$pH$' layout.y_lim = (1.1*np.min(c), 1.1*np.max(c)) plotting.set_layout(ax, layout) return ax