Source code for chemistrykit.solutions.systems.solubility

r"""Solubility equilibria: Ksp, molar solubility, and the common-ion effect.

See Atkins & de Paula, *Physical Chemistry*, 11th ed., Ch. 6.8, or Harris,
*Quantitative Chemical Analysis*, 9th ed., Ch. 8, throughout.

For a sparingly soluble salt :math:`M_pX_q` dissolving as
:math:`M_pX_q(s) \rightleftharpoons pM^{n+} + qX^{m-}`, at molar
solubility :math:`s` (mol/L of the salt that dissolves), :math:`[M^{n+}]
= ps` and :math:`[X^{m-}] = qs`, so

.. math::

    K_{sp} = [M^{n+}]^p[X^{m-}]^q = p^p q^q\, s^{p+q}
"""

from __future__ import annotations

from chemistrykit.solutions.utils.rootfinding import find_positive_root

__all__ = [
    "ksp_from_molar_solubility",
    "molar_solubility_from_ksp",
    "molar_solubility_with_common_ion",
]


[docs] def ksp_from_molar_solubility(s: float, cation_coeff: int, anion_coeff: int) -> float: r"""Compute :math:`K_{sp} = p^pq^q s^{p+q}` from a measured molar solubility. Parameters ---------- s : float Molar solubility of the salt, in mol/L. cation_coeff : int Stoichiometric coefficient of the cation, `p` (e.g. 1 for AgCl, 1 for CaF2's Ca2+, 2 for Ag2CrO4's Ag+). anion_coeff : int Stoichiometric coefficient of the anion, `q` (e.g. 1 for AgCl, 2 for CaF2's F-, 1 for Ag2CrO4's CrO4 2-). Returns ------- float Examples -------- AgCl (1:1 salt) with molar solubility 1.3e-5 mol/L: >>> round(ksp_from_molar_solubility(s=1.3e-5, cation_coeff=1, anion_coeff=1), 12) 1.69e-10 CaF2 (1:2 salt), Ksp = [Ca2+][F-]^2 = s*(2s)^2 = 4s^3: >>> round(ksp_from_molar_solubility(s=2.1e-4, cation_coeff=1, anion_coeff=2), 12) 3.7e-11 """ p, q = cation_coeff, anion_coeff return (p**p) * (q**q) * s ** (p + q)
[docs] def molar_solubility_from_ksp(Ksp: float, cation_coeff: int, anion_coeff: int) -> float: r"""Invert :func:`ksp_from_molar_solubility`: :math:`s = (K_{sp}/(p^pq^q))^{1/(p+q)}`. Parameters ---------- Ksp : float Solubility product. cation_coeff : int Stoichiometric coefficient of the cation, `p`. anion_coeff : int Stoichiometric coefficient of the anion, `q`. Returns ------- float Molar solubility, in mol/L. Examples -------- Round-trips with :func:`ksp_from_molar_solubility`: >>> Ksp = ksp_from_molar_solubility(s=1.3e-5, cation_coeff=1, anion_coeff=1) >>> round(molar_solubility_from_ksp(Ksp, cation_coeff=1, anion_coeff=1), 12) 1.3e-05 """ p, q = cation_coeff, anion_coeff return (Ksp / ((p**p) * (q**q))) ** (1.0 / (p + q))
[docs] def molar_solubility_with_common_ion(Ksp: float, cation_coeff: int, anion_coeff: int, common_ion_conc: float, common_ion: str = "cation") -> float: r"""Molar solubility of :math:`M_pX_q` in the presence of an added common ion. Le Chatelier's principle predicts that adding a common ion (independently, e.g. from a soluble salt sharing that ion) suppresses the target salt's solubility relative to :func:`molar_solubility_from_ksp` (Atkins & de Paula, *Physical Chemistry*, 11th ed., Ch. 6.8b). This solves the exact polynomial .. math:: K_{sp} = (ps + C_0)^p (qs)^q \quad\text{(common cation, added at concentration } C_0\text{)} or the mirror-image equation for a common anion, via :func:`chemistrykit.solutions.utils.rootfinding.find_positive_root` (exact for any p, q -- not just the textbook 1:1-salt quadratic special case). Parameters ---------- Ksp : float Solubility product of the salt whose solubility is being found. cation_coeff : int Stoichiometric coefficient of the cation, `p`. anion_coeff : int Stoichiometric coefficient of the anion, `q`. common_ion_conc : float Concentration of the common ion contributed by an independent (fully dissociated) source, in mol/L. common_ion : {"cation", "anion"} Which ion is shared with the independent source. Returns ------- float Molar solubility of the salt, in mol/L. Examples -------- AgCl (Ksp = 1.8e-10) is much less soluble in 0.10 M NaCl (common Cl- ion) than in pure water: >>> s_pure = molar_solubility_from_ksp(1.8e-10, cation_coeff=1, anion_coeff=1) >>> s_common = molar_solubility_with_common_ion(1.8e-10, cation_coeff=1, anion_coeff=1, common_ion_conc=0.10, common_ion="anion") >>> s_common < s_pure True >>> round(s_common, 12) # dominated by the 0.10 M Cl- already present: s ~= Ksp/0.10 1.8e-09 """ p, q = cation_coeff, anion_coeff def residual(s): if common_ion == "cation": cation = p * s + common_ion_conc anion = q * s elif common_ion == "anion": cation = p * s anion = q * s + common_ion_conc else: raise ValueError("common_ion must be 'cation' or 'anion'") return cation**p * anion**q - Ksp # An upper bound for s: the common-ion-free solubility (adding a # common ion can only suppress solubility further). s_free = molar_solubility_from_ksp(Ksp, p, q) return find_positive_root(residual, lo=1e-20, hi=max(s_free, 1e-10))