Skip to content

Keller-Segel chemotaxis

The Keller-Segel model of cells that diffuse and climb the gradient of a chemical they secrete.

  • Name: keller_segel
  • Field: physics/active_matter
  • Formalism: ordinary differential equations
  • References: te Vrugt and Wittkowski 2024; Keller and Segel 1970; Horstmann 2003

Factory

@register_model(
    field='physics/active_matter',
    tags=('field', 'chemotaxis', 'aggregation', 'collapse', 'pde'),
    references=('te Vrugt and Wittkowski 2024', 'Keller and Segel 1970', 'Horstmann 2003'),
    defaults={'until': 10.5, 'dt': 0.25},
)
def keller_segel(
    shape: Sequence[int] = (32, 32),
    *,
    length: float = 32.0,
    D: float = 1.0,
    chi: float = 5.0,
    Dc: float = 1.0,
    k: float = 1.0,
    density: float = 1.0,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the Keller-Segel model of cells that diffuse and climb the gradient of a chemical they secrete.

    The cell density follows `rho_t = D laplacian(rho) - chi div(rho grad(c))` and the chemical
    `c_t = Dc laplacian(c) + rho - k c`, on a periodic square. The total number of cells is conserved. The uniform state
    of density `rho_0` is unstable when the chemotactic drive `chi rho_0` exceeds `D (k + Dc q^2)` for the longest
    wave `q = 2 pi / length`. The cells then aggregate into dense spots, which in two dimensions collapse in finite
    time, so a run above the threshold ends before the density blows up.

    Args:
        shape:
            The number of sites along both axes of the periodic lattice.

        length:
            The side of the square.

        D:
            The diffusivity of the cells.

        chi:
            The chemotactic sensitivity.

        Dc:
            The diffusivity of the chemical.

        k:
            The decay rate of the chemical.

        density:
            The mean cell density `rho_0`, perturbed by 1% noise, with the chemical at its uniform value `rho_0 / k`.

        random_state:
            The seed or generator of the noise.

    Returns:
        system:
            The model, with the cell density `colony.rho` and the chemical `colony.c`.

    Examples:
        >>> from skcomplex.models.physics.active_matter import keller_segel
        >>> keller_segel((8, 8), chi=0.5).elements[0].values['chi']
        0.5
    """
    shape = tuple(shape)
    rng = np.random.default_rng(random_state)
    values = {
        'rho': density * (1 + 0.01 * rng.uniform(-1.0, 1.0, shape)),
        'c': np.full(shape, density / k),
        'D': D,
        'chi': chi,
        'Dc': Dc,
        'k': k,
    }
    space = LatticeSpace(shape, spacing=length / shape[0])
    return System(elements=[FieldElement('colony', element=ChemotaxisElement, space=space, values=values)])

Components

class ChemotaxisElement(Element):
    """Site of the Keller-Segel model, cells of density `rho` that climb the gradient of a chemical `c` they secrete.

    The cells diffuse and drift up the chemical, `rho_t = D laplacian(rho) - chi div(rho grad c)`, and the chemical
    diffuses, is secreted by the cells, and decays, `c_t = Dc laplacian(c) + rho - k c`. Above a critical mass the cells
    aggregate and the density blows up in finite time.

    Args:
        label:
            The label of the site or the field.

        visual:
            How the site is drawn, or `None` for `default_visual_`.

        rho:
            The initial density of cells.

        c:
            The initial concentration of the chemical.

        D:
            The diffusion coefficient of the cells.

        chi:
            The chemotactic sensitivity.

        Dc:
            The diffusion coefficient of the chemical.

        k:
            The decay rate of the chemical.

    Attributes:
        default_visual_:
            The visual of the element when it sets none.

    Examples:
        >>> from skcomplex.models.physics.active_matter import ChemotaxisElement
        >>> ChemotaxisElement('site').chi
        5.0
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        rho: Annotated[float, StateSpec()] = 1.0,
        c: Annotated[float, StateSpec()] = 1.0,
        D: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
        chi: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 5.0,
        Dc: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
        k: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.rho = rho
        self.c = c
        self.D = D
        self.chi = chi
        self.Dc = Dc
        self.k = k

    def _get_laws(self, namespace: SimpleNamespace) -> list[BaseLaw]:
        """Return the drift and diffusion of the cells and the balance of the chemical.

        Args:
            namespace:
                The namespace of the symbols the laws can use.

        Returns:
            laws:
                The rates of `rho` and `c`.
        """
        s = namespace
        return [
            RateLaw(s.rho, s.D * laplacian(s.rho) - s.chi * divergence(s.rho * gradient(s.c))),
            RateLaw(s.c, s.Dc * laplacian(s.c) + s.rho - s.k * s.c),
        ]

Equations

colony.rho[i]: diff(rho_colony(t), t) == -chi_colony*(partial(partial(c_colony(t), 1)*rho_colony(t), 1) + partial(partial(c_colony(t), 0)*rho_colony(t), 0)) + D_colony*laplacian(rho_colony(t))
colony.c[i]: diff(c_colony(t), t) == -k_colony*c_colony(t) + Dc_colony*laplacian(c_colony(t)) + rho_colony(t)