Predator-Prey (Lotka–Volterra, SciPy)

A predator-prey simulation using the Lotka-Volterra equations.

Level:Intermediate

populationecosystemnonlinear-dynamics

  • Stocks:prey, predator
  • Flows:prey_births, predations, predator_deaths
  • Feedback Loops:predation cycle
  • Probes:prey, predator

Dynamic Behavior Patterns

Explore common system behaviors: exponential growth, goal-seeking decay, overshoot-and-collapse, and S-curve saturation.

Explore Dynamic Behavior Patterns
simulation.py

Predator and prey in the Lotka–Volterra model

Here we let SciPy's solve_ivp integrate the predator–prey equations for us. It's a different angle on the classic chase between hungry predators and their food supply.


from tys import probe, progress

Integrate the Lotka–Volterra equations over time.

def simulate(cfg: dict):
    import numpy as np
    from scipy.integrate import solve_ivp

Parameters

    x0        = float(cfg["initial_prey"])
    y0        = float(cfg["initial_predator"])
    alpha     = cfg["alpha"]
    beta      = cfg["beta"]
    delta     = cfg["delta"]
    gamma     = cfg["gamma"]
    sim_time  = float(cfg["sim_time"])
    dt        = float(cfg.get("time_step", 0.01))

    assert all(p > 0 for p in (alpha, beta, delta, gamma)), "Rates must be positive"
    assert dt > 0 and sim_time > 0, "Time parameters must be positive"

Right-hand side of the ODE system

    def lv(_, z):
        x, y = z
        return [
            alpha * x - beta * x * y,        # dx/dt
            delta * x * y - gamma * y        # dy/dt
        ]

Build a t_eval that is inside [0, sim_time]

    n_steps = int(round(sim_time / dt))
    t_eval  = np.linspace(0.0, sim_time, n_steps + 1)

    sol = solve_ivp(
        lv, (0.0, sim_time), [x0, y0],
        t_eval=t_eval, method="RK45", max_step=dt
    )

    prey_series     = np.maximum(sol.y[0], 0.0)   # clip tiny negatives
    predator_series = np.maximum(sol.y[1], 0.0)

    for t, x, y in zip(sol.t, prey_series, predator_series):
        probe("prey",     t, x)
        probe("predator", t, y)

    progress(100)
    return {
        "final_prey":     float(prey_series[-1]),
        "final_predator": float(predator_series[-1]),
    }


def requirements():

SciPy comes with Pyodide, so only declare it as builtin

    return {
        "builtin": ["micropip", "scipy", "pyyaml"]
    }
Default.yaml
initial_prey: 10
initial_predator: 5
alpha: 1.5
beta: 1.0
delta: 1.0
gamma: 3.0
sim_time: 20
time_step: 0.01
Charts (Default)

prey

prey chartCSV
Samples2001 @ 0.00–20.00
Valuesmin 0.22, mean 2.73, median 1.00, max 12.32, σ 3.44

predator

predator chartCSV
Samples2001 @ 0.00–20.00
Valuesmin 0.02, mean 1.58, median 0.21, max 9.32, σ 2.60
Final Results (Default)
MetricValue
final_prey1.99
final_predator0.02
FAQ
How are the predator-prey equations solved?
SciPy's solve_ivp integrates the differential system using RK45 and returns values at each evaluation step.
Why clamp negative populations?
The integrator can introduce tiny negatives, so numpy.maximum ensures prey and predator counts stay non-negative.
How are evaluation times chosen?
t_eval is a linspace from 0 to sim_time with step dt so charts align with the solver output.