Source code for microscopic_gating.gating

r"""
Gating functions for bridge probability modeling.

This module implements gating functions that determine how bridge
probability depends on surface occupancy. The gating function :math:`\mathcal{G}(\phi)`
is the key component linking adsorption to transport properties.

Theoretical Foundation
----------------------
Productive bridge formation requires:

1. Particle site occupied by TC: probability :math:`\theta_P`
2. Network site **unoccupied** (to avoid blocking): probability :math:`1 - \theta_N`
3. Geometric contact within capture volume: factor :math:`\langle\chi\rangle`
4. Successful closure given contact: probability :math:`\kappa_B`

For **symmetric binding** (:math:`K_P \approx K_N \approx K_d`):

.. math::

    \mathcal{G}(\phi) = \theta(\phi)[1-\theta(\phi)]
                      = \frac{\phi/K_d}{(1+\phi/K_d)^2}

For **asymmetric binding** (different affinities):

.. math::

    \mathcal{G}(\phi) = \theta_P(\phi)[1-\theta_N(\phi)]

Key Properties
--------------
**Bell-shaped curve:**

- **Peak position**: :math:`\phi^\ast = K_d` (for symmetric case)
- **Peak value**: :math:`\mathcal{G}_{\max} = 1/4`
- **Low concentration limit**: :math:`\mathcal{G} \sim \phi/K_d` (starvation)
- **High concentration limit**: :math:`\mathcal{G} \sim K_d/\phi` (blocking)

**Physical interpretation:**

- Low :math:`\phi`: Bridge formation limited by "starvation" (insufficient TC)
- High :math:`\phi`: Bridge formation limited by "blocking" (sites saturated)
- Intermediate :math:`\phi`: Optimal balance yields maximum bridging

This non-monotonic behavior is the origin of **re-entrant transport phenomena**
and **dome-shaped phase boundaries** in the model.

Examples
--------
Symmetric gating with Langmuir isotherm:

>>> from microscopic_gating.adsorption import LangmuirIsotherm
>>> isotherm = LangmuirIsotherm(K=1.0)
>>> gating = SymmetricGating(isotherm)
>>> gating.G(1.0)  # Peak at phi = K_d
0.25
>>> gating.phi_star()
1.0

Asymmetric gating (different binding affinities):

>>> isotherm_P = LangmuirIsotherm(K=1.0)  # Particle side
>>> isotherm_N = LangmuirIsotherm(K=2.0)  # Network side
>>> gating = AsymmetricGating(isotherm_P, isotherm_N)
>>> gating.G(1.0)  # doctest: +SKIP
0.1666...

See Also
--------
LangmuirIsotherm : Standard adsorption model
HillIsotherm : Cooperative adsorption
MicroscopicGatingModel : Full model integrating gating with statistics

References
----------
- Eq. (S5): Asymmetric gating (minimal blocking picture)
- Eq. (S6)-(S7): Symmetric gating function
- Eq. (S8): Peak position and maximum value
"""

from __future__ import annotations

from dataclasses import dataclass

import numpy as np

from .types import ArrayLike, Isotherm


[docs]@dataclass(frozen=True) class SymmetricGating: r""" Symmetric gating function :math:`G(\phi) = \theta(1-\theta)`. Implements Eq. (S6)-(S7), assuming :math:`K_P \approx K_N \approx K_d`. This is the standard gating function for symmetric binding scenarios. The gating function produces a bell-shaped curve with maximum at :math:`\phi = K` where :math:`G_{max} = 1/4`. Parameters ---------- isotherm : Isotherm Any isotherm providing :math:`\theta(\phi)`, e.g., :class:`~microscopic_gating.adsorption.LangmuirIsotherm` or :class:`~microscopic_gating.adsorption.HillIsotherm`. See Also -------- AsymmetricGating : Uses :math:`\theta_P(1-\theta_N)` for non-symmetric particle/network binding. Examples -------- >>> from microscopic_gating.adsorption import LangmuirIsotherm >>> isotherm = LangmuirIsotherm(K=1.0) >>> gating = SymmetricGating(isotherm) >>> gating.G(1.0) 0.25 >>> gating.phi_star() 1.0 References ---------- - Eq. (S6)-(S7): Symmetric gating function derivation """ isotherm: Isotherm
[docs] def G(self, phi: ArrayLike) -> ArrayLike: r""" Calculate symmetric gating function. Parameters ---------- phi : ArrayLike Bulk concentration. Returns ------- G : ArrayLike Gating function value :math:`\theta(\phi)[1-\theta(\phi)]`, same shape as `phi`. Values are in [0, 0.25]. Notes ----- The function reaches its maximum of 0.25 when :math:`\theta = 0.5`, which occurs at :math:`\phi = K` for Langmuir/Hill isotherms. """ theta = self.isotherm.theta(phi) return theta * (1.0 - theta)
[docs] def phi_star(self) -> float: r""" Find peak location of the gating function. Returns ------- phi_star : float Concentration at which :math:`G(\phi)` is maximized. For standard Langmuir/Hill forms, :math:`\phi^\\ast = K`. Raises ------ AttributeError If the isotherm does not have a 'K' attribute. Notes ----- For Hill/Langmuir isotherms, :math:`G(\phi)` is maximized at :math:`\phi = K` with maximum value 1/4. """ K = getattr(self.isotherm, "K", None) if K is None: raise AttributeError( "isotherm has no attribute 'K'; phi_star undefined." ) return float(K)
[docs]@dataclass(frozen=True) class AsymmetricGating: r""" Asymmetric gating function: :math:`G(\phi) = \theta_P(\phi)[1-\theta_N(\phi)]`. Implements Eq. (S5) in the minimal blocking picture. This gating function accounts for different binding affinities on the particle versus network sides. Parameters ---------- isotherm_P : Isotherm Particle-side isotherm giving :math:`\theta_P(\phi)`. isotherm_N : Isotherm Network-side isotherm giving :math:`\theta_N(\phi)`. See Also -------- SymmetricGating : Special case where :math:`\theta_P = \theta_N`. Examples -------- >>> from microscopic_gating.adsorption import LangmuirIsotherm >>> isotherm_P = LangmuirIsotherm(K=1.0) >>> isotherm_N = LangmuirIsotherm(K=2.0) >>> gating = AsymmetricGating(isotherm_P, isotherm_N) >>> gating.G(1.0) # doctest: +SKIP 0.1666... References ---------- - Eq. (S5): Asymmetric gating in minimal blocking picture """ isotherm_P: Isotherm isotherm_N: Isotherm
[docs] def G(self, phi: ArrayLike) -> ArrayLike: r""" Calculate asymmetric gating function. Parameters ---------- phi : ArrayLike Bulk concentration. Returns ------- G : ArrayLike Gating function value :math:`\theta_P(\phi)[1-\theta_N(\phi)]`, same shape as `phi`. Notes ----- Unlike symmetric gating, the maximum value and peak location depend on both isotherm parameters. """ theta_P = self.isotherm_P.theta(phi) theta_N = self.isotherm_N.theta(phi) return theta_P * (1.0 - theta_N)