History / release 1 · saved , the current version · released as https://doi.org/10.83371/sh.2610.hs8ev

Cite this release

@software{reaction_diffusion_2026,
  author = {ScienceHub},
  title = {Reaction-diffusion},
  version = {1},
  year = {2026},
  publisher = {ScienceHub},
  doi = {10.83371/sh.2610.hs8ev}
}
ScienceHub (2026). Reaction-diffusion (Version 1) [Computer software]. ScienceHub. https://doi.org/10.83371/sh.2610.hs8ev
cff-version: 1.2.0
message: "If you use this version, cite it as below."
title: "Reaction-diffusion"
type: software
authors:
  - name: "ScienceHub"
version: "1"
date-released: "2026-10-13"
doi: "10.83371/sh.2610.hs8ev"
url: "https://doi.org/10.83371/sh.2610.hs8ev"

The DOI names exactly this release. The project page cites all releases.

# /// script
# requires-python = ">=3.12"
# dependencies = ["marimo>=0.25.1", "numpy", "matplotlib"]
# ///

import marimo

__generated_with = "0.25.1"
app = marimo.App(width="medium")


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        r"""
        # Spots and stripes from two chemicals

        In 1952 Alan Turing proposed that two substances which react and spread
        at different rates can turn a uniform tissue into a pattern: the spots
        of a leopard, the stripes of a zebrafish. Forty years later John
        Pearson showed how rich such patterns can be with a two-line model, the
        Gray-Scott reaction:

        $$\frac{\partial u}{\partial t} = D_u \nabla^2 u - u v^2 + F(1 - u), \qquad
        \frac{\partial v}{\partial t} = D_v \nabla^2 v + u v^2 - (F + k)\, v$$

        $u$ is fed in at rate $F$, $v$ consumes it to make more of itself and is
        removed at rate $F + k$, and $u$ spreads twice as fast as $v$. Small
        changes in $F$ and $k$ give spots that divide, mazes, or coral.
        """
    )
    return


@app.cell
def _():
    import marimo as mo
    import numpy as np
    import matplotlib.pyplot as plt
    return mo, np, plt


@app.cell
def _(mo):
    PRESETS = {
        "Dividing spots": (0.0367, 0.0649),
        "Spots": (0.035, 0.065),
        "Coral": (0.0545, 0.062),
        "Maze": (0.029, 0.057),
        "Worms": (0.078, 0.061),
    }
    preset = mo.ui.dropdown(list(PRESETS), value="Dividing spots", label="Pattern")
    preset
    return PRESETS, preset


@app.cell
def _(PRESETS, mo, preset):
    feed = mo.ui.slider(0.01, 0.09, step=0.0005, value=PRESETS[preset.value][0], label="Feed rate F")
    kill = mo.ui.slider(0.045, 0.07, step=0.0005, value=PRESETS[preset.value][1], label="Removal rate k")
    steps = mo.ui.slider(500, 8000, step=500, value=5000, label="Time steps")
    mo.vstack([feed, kill, steps])
    return feed, kill, steps


@app.cell
def _(np):
    def laplacian(X):
        return np.roll(X, 1, -2) + np.roll(X, -1, -2) + np.roll(X, 1, -1) + np.roll(X, -1, -1) - 4 * X

    def gray_scott(F, k, n=128, steps=5000, Du=0.16, Dv=0.08, seed=1):
        """Explicit Euler, five-point Laplacian, periodic boundaries, unit grid and time step.

        F and k may be arrays: each pair then runs as its own dish, all at once.
        """
        F = np.asarray(F, dtype=float)[..., None, None]
        k = np.asarray(k, dtype=float)[..., None, None]
        shape = np.broadcast_shapes(F.shape, k.shape)[:-2] + (n, n)
        rng = np.random.default_rng(seed)
        U = np.ones(shape)
        V = np.zeros(shape)
        for _ in range(10):
            x, y = rng.integers(0, n - 8, 2)
            U[..., x : x + 8, y : y + 8] = 0.5
            V[..., x : x + 8, y : y + 8] = 0.25
        U += 0.02 * rng.random((n, n))
        V += 0.02 * rng.random((n, n))
        for _ in range(steps):
            Lu, Lv = laplacian(U), laplacian(V)
            uvv = U * V * V
            U += Du * Lu - uvv + F * (1 - U)
            V += Dv * Lv + uvv - (F + k) * V
        return U, V

    return (gray_scott,)


@app.cell
def _(feed, gray_scott, kill, mo, plt, steps):
    with mo.status.spinner(title="Reacting and diffusing"):
        _, V = gray_scott(feed.value, kill.value, steps=steps.value)
    fig, ax = plt.subplots(figsize=(5.5, 5.5))
    ax.imshow(V, cmap="magma", interpolation="bicubic")
    ax.set_axis_off()
    ax.set_title(f"v after {steps.value:,} steps, F = {feed.value:.4f}, k = {kill.value:.4f}")
    fig.tight_layout()
    fig
    return


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        r"""
        ## Where the patterns live

        Pearson mapped which pattern appears for each pair $(F, k)$. Most of the
        plane is dull: either $v$ dies out and the dish stays uniform, or it
        takes over everywhere. Patterns live in a narrow band between the two,
        roughly $0.03 < F < 0.08$ and $0.055 < k < 0.068$. Move a slider a few
        thousandths outside it and the dish goes blank; that sensitivity is
        part of why biologists still argue about which real patterns are
        Turing patterns.
        """
    )
    return


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        r"""
        ---
        **Method and limits.** 128 × 128 periodic grid, explicit Euler with
        unit time step and grid spacing, $D_u = 0.16$, $D_v = 0.08$; ten seeded
        squares of $v$ plus 2 percent noise start the reaction. Patterns depend
        on the seed and the grid size, the regimes do not.
        **References.** A. M. Turing, *Phil. Trans. R. Soc. B* 237 (1952) 37;
        P. Gray and S. K. Scott, *Chem. Eng. Sci.* 38 (1983) 29;
        J. E. Pearson, *Science* 261 (1993) 189.
        Fork it to grow a pattern on a sphere or to add a third species.
        """
    )
    return


if __name__ == "__main__":
    app.run()