Source code for ionize.Ion.mobility

from __future__ import division
from math import copysign, sqrt
import warnings
import numpy as np

from ..constants import pitts, reference_temperature, kelvin, faraday, \
    elementary_charge, lpm3, gpkg, onsager_fuoss

def mobility(self, pH=None, ionic_strength=None, temperature=None):
    """Return the effective mobility of the ion in m^2/V/s.

    If a context solution is available, mobility uses the full Onsager-Fuoss
    correction to mobility. Otherwise, the Robinson-Stokes model is used.

    :param pH
    :param ionic_strength
    :param temperature
    """
    pH, ionic_strength, temperature = \
        self._resolve_context(pH, ionic_strength, temperature)

    ionization_fraction = self.ionization_fraction(pH, ionic_strength,
                                                   temperature)
    actual_mobility = self.actual_mobility(ionic_strength, temperature)

    effective_mobility = np.sum(ionization_fraction * actual_mobility)

    return effective_mobility


def actual_mobility(self, ionic_strength=None, temperature=None):
    """Return the mobility for each charge state.

    Uses the Onsager-Fuoss correction if a context solution is available,
    otherwise uses the Robinson-Stokes mobility.

    :param ionic_strength
    :param temperature
    """
    try:
        return self.onsager_fuoss_mobility()
    except AttributeError:
        # warnings.warn("Insufficient information for Onsager-Fuoss "
        #               "correction to mobility. Returning the Robinson-"
        #               "Stokes approximation.")
        return self.robinson_stokes_mobility(ionic_strength, temperature)


def absolute_mobility(self, temperature=None):
    """Return the mobility of each charge state at infinite dilution.

    :param temperature
    """
    _, _, temperature = \
        self._resolve_context(None, None, temperature)

    if self._nightingale_function:
        absolute_mobility = \
            (self._nightingale_function(temperature).tolist() *
             10.35e-11 / self._solvent.viscosity(temperature) *
             self.valence)

        if not (self.nightingale_data['min'] <
                temperature <
                self.nightingale_data['max']):
            warnings.warn('Temperature outside range'
                          'for nightingale data.')
    else:
        absolute_mobility =\
            (self._solvent.viscosity(self.reference_temperature) /
             self._solvent.viscosity(temperature)*self.reference_mobility)

    return absolute_mobility


def robinson_stokes_mobility(self, ionic_strength=None, temperature=None):
    """Return the Robinson-Stokes correction to fully ionized mobility.

    This correction is appropriate if a generic ionic strength is known,
    but the specific ions in solution are unknown.
    """
    _, ionic_strength, temperature = \
        self._resolve_context(None, ionic_strength, temperature)

    dielectric = self._solvent.dielectric(temperature)
    viscosity = self._solvent.viscosity(temperature)

    alpha = (5.799e5 * abs(self.valence) /
             (kelvin(temperature) * dielectric)**(3./2.)
             )
    beta = (3.022588e-9 * abs(self.valence) / viscosity /
            (kelvin(temperature) * dielectric)**(1./2.))

    mobility = self.absolute_mobility(temperature)
    mobility -= (alpha * mobility +
                 beta * np.sign(self.valence)) * \
        (sqrt(2 * ionic_strength) / (1. + pitts * sqrt(2 *ionic_strength)))

    return mobility


def onsager_fuoss_mobility(self):
    """Return the Onsager-Fuoss corrected mobility of each charge state."""
    _, ionic_strength, temperature = \
        self._resolve_context(None, None, None)

    dielectric = self._solvent.dielectric(temperature)
    viscosity = self._solvent.viscosity(temperature)
    interaction = _interaction(self, self.context())

    alpha = (1.98074e6 * abs(self.valence) * interaction /
             (kelvin(temperature) * dielectric)**(3./2.)
             )
    beta = (3.022588e-9 * abs(self.valence) / viscosity /
            (kelvin(temperature) * dielectric)**(1./2.))

    mobility = self.absolute_mobility()
    mobility -= (alpha * mobility +
                 beta * np.sign(self.valence)) * \
        (sqrt(2 * ionic_strength) / (1. + pitts * sqrt(2 *ionic_strength)))

    return mobility


def _interaction(ion, solution):
    ions = [ion_ for ion_ in solution.ions
            if solution.concentration(ion_) > 0] + \
           [solution._hydroxide,  solution._hydronium]

    if ion not in ions:
        ions = ions + [ion]
    ion_index = ions.index(ion)
    if ion_index != 0:
        start_index = len(np.concatenate([ion_.valence
                                          for ion_ in ions[:ion_index]]))
    else:
        start_index = 0
    end_index = start_index + len(ion.valence)

    omega = np.concatenate([ion.absolute_mobility() / ion.valence
                            for ion in ions]) / faraday

    if np.any(omega == 0.):
        raise RuntimeError('Onsager-Fuoss approximation '
                           'diverges for non-mobile ions. ')

    valences = np.concatenate([ion.valence for ion in ions])
    concentrations = np.concatenate([solution.concentration(ion) *
                                     ion.ionization_fraction(solution.pH)
                                     for ion in ions])

    potential = concentrations * valences**2. / (2. * solution.ionic_strength)

    h = potential * omega / (omega + omega[:, np.newaxis])
    d = np.diag(np.sum(h, 1))
    B = 2 * (h + d) - np.identity(len(omega))

    r = np.zeros([len(omega), 6])
    r[:, 0] = (valences - (np.sum(valences * potential) /
                           np.sum(potential / omega)) / omega)

    for i in range(1, 6):
        r[:, i] = np.dot(B, r[:, i-1])

    factor = np.dot(onsager_fuoss, np.transpose(r))

    return factor[start_index:end_index]