Skip to content
The Lumenary

Math laboratory

Lotka-Volterra: conserved quantity, Euler versus RK4, cycle averages

Integrates the Lotka-Volterra predator-prey equations with Euler's method and with RK4 and monitors the conserved quantity V: Euler drifts outward, RK4 conserves V closely. Measures the period of the cycle and the average prey and predator populations over one period, which match the equilibrium (c/d, a/b). Floating-point numerical evidence.

Back to the labNumerical approximation
Lotka-Volterra: conserved quantity, Euler versus RK4, cycle averagesPythonNumerical approximationComputationally tested
"""Lotka-Volterra cycles: what a conserved quantity says about numerical methods.

Prey x and predators y obey x' = a x - b x y, y' = d x y - c y. The quantity
V = d x - c ln x + b y - a ln y is exactly conserved, so true orbits are closed.
We integrate with Euler's method and with classical Runge-Kutta (RK4) and
watch V: Euler's orbit spirals outward (V grows steadily), while RK4 keeps V
constant to many digits. With RK4 we also measure the period and check that
the average prey population over one cycle equals the equilibrium value c/d.
"""

import math

A, B, C, D = 1.0, 0.5, 0.75, 0.25  # prey growth, predation, predator death, conversion

def rhs(x, y):
    """Lotka-Volterra vector field."""
    return A * x - B * x * y, D * x * y - C * y

def conserved(x, y):
    """V = d x - c ln x + b y - a ln y."""
    return D * x - C * math.log(x) + B * y - A * math.log(y)

def euler_step(x, y, h):
    """One explicit Euler step."""
    fx, fy = rhs(x, y)
    return x + h * fx, y + h * fy

def rk4_step(x, y, h):
    """One classical Runge-Kutta step."""
    k1 = rhs(x, y)
    k2 = rhs(x + h / 2 * k1[0], y + h / 2 * k1[1])
    k3 = rhs(x + h / 2 * k2[0], y + h / 2 * k2[1])
    k4 = rhs(x + h * k3[0], y + h * k3[1])
    return (x + h / 6 * (k1[0] + 2 * k2[0] + 2 * k3[0] + k4[0]),
            y + h / 6 * (k1[1] + 2 * k2[1] + 2 * k3[1] + k4[1]))

def run(step, x, y, h, n):
    """Positions after 0, 1, ..., n steps."""
    path = [(x, y)]
    for _ in range(n):
        x, y = step(x, y, h)
        path.append((x, y))
    return path

def period_and_averages(x0, y0, h=0.001, t_max=40.0):
    """Period (time between successive upward crossings of y = a/b by the
    prey-rich branch) and the averages of x and y over that period, with RK4."""
    x, y = x0, y0
    crossings, t = [], 0.0
    sums, sx, sy = [], 0.0, 0.0
    while t < t_max and len(crossings) < 3:
        nx, ny = rk4_step(x, y, h)
        sx += h * (x + nx) / 2
        sy += h * (y + ny) / 2
        if y < A / B <= ny and nx > C / D:
            crossings.append(t + h)
            sums.append((sx, sy))
        x, y, t = nx, ny, t + h
    period = crossings[2] - crossings[1]
    avg_x = (sums[2][0] - sums[1][0]) / period
    avg_y = (sums[2][1] - sums[1][1]) / period
    return period, avg_x, avg_y

def main():
    x0, y0, h, n = 4.0, 1.0, 0.01, 3000
    v0 = conserved(x0, y0)
    eul = run(euler_step, x0, y0, h, n)
    rk = run(rk4_step, x0, y0, h, n)
    print(f"a = {A}, b = {B}, c = {C}, d = {D}; equilibrium (c/d, a/b) ="
          f" ({C / D:g}, {A / B:g})")
    print(f"Start (x, y) = ({x0}, {y0}), V = {v0:.6f}; step h = {h}")
    print("    t     V (Euler)      V (RK4)     max prey (Euler, RK4)")
    for k in [0, 500, 1000, 2000, 3000]:
        me = max(p[0] for p in eul[:k + 1])
        mr = max(p[0] for p in rk[:k + 1])
        print(f"{k * h:>5.0f} {conserved(*eul[k]):>12.6f} {conserved(*rk[k]):>12.6f}"
              f"   {me:>8.3f} {mr:>8.3f}")
    print("Euler's orbit spirals outward; RK4 stays on the closed level curve.")
    print()
    period, ax, ay = period_and_averages(x0, y0)
    print(f"RK4 period of the cycle: {period:.3f}"
          f" (small-oscillation value 2 pi/sqrt(ac) = {2 * math.pi / math.sqrt(A * C):.3f})")
    print(f"Average prey over a cycle {ax:.4f} (equilibrium c/d = {C / D:.4f})")
    print(f"Average predators         {ay:.4f} (equilibrium a/b = {A / B:.4f})")
    print()
    print("Epistemic status: floating-point integration with fixed steps. The")
    print("conservation of V and the cycle averages are theorems (V and the")
    print("prey average are proved in Lean); the numbers show how well each")
    print("method respects them.")

if __name__ == "__main__":
    main()
Runs entirely in your browser, in an isolated worker. Nothing is sent to a server.

Used in