Predator-Prey App — ChatGPT - Conversation Transcript
Uploaded a file: MODEL_pred_prey.pdf
Could you explain this paper briefly for an ecology PhD with limited maths ability
Here’s a concise, ecology-focused overview of the paper “Chaotic dynamics in a simple predator–prey model with discrete delay” by Guihong Fan and Gail Wolkowicz (2021) — written for a biological scientist with limited maths background:
Core idea
The authors study how adding a time delay—representing the period between prey capture and its conversion into predator biomass—can transform a simple, normally stable predator–prey system into one showing oscillations, bistability, and even chaos.
1. The base model (without delay)
They start from a Gause-type predator–prey model where:
- Prey grow logistically (limited by carrying capacity $K$).
- Predators consume prey according to a simple linear “mass-action” rate (Holling type I functional response).
Without delay, the system always settles to a stable equilibrium:
- Either predators die out and prey reach carrying capacity.
- Or both coexist at constant densities.
- No oscillations occur — it’s globally stable.
2. Introducing a time delay
To model processing or digestion time, they add a discrete delay $\tau$ :
$$ \begin{aligned} \dot{x}(t) &= r x(t)\!\left(1 - \frac{x(t)}{K}\right) - m x(t) y(t) \\ \dot{y}(t) &= -s y(t) + Y e^{-s\tau} m x(t - \tau) y(t - \tau) \end{aligned} $$
where $e^{-s\tau}$ accounts for predators that die before finishing processing prey.
This delay doesn’t change the basic structure but introduces memory: predator growth depends on past prey availability.
3. What happens as the delay increases
When $\tau = 0$: system stable.
As $\tau$ grows:
- The equilibrium becomes unstable through a Hopf bifurcation, giving rise to sustained cycles.
- With further increases, the cycles undergo:
- Saddle-node bifurcations (two cycles coexist — bistability).
- Period-doubling cascades (oscillations repeating every 2, 4, 8 … cycles).
- Finally, chaotic fluctuations similar to those seen in the Mackey–Glass equation (a well-known model of physiological chaos).
When the delay becomes very long, predators go extinct because they can’t reproduce fast enough to offset mortality.
4. Example behaviour (Fig. 2–10)
Using parameter values typical of scaled ecological systems ($r = 1,\ s = 0.02,\ Y = 0.6$):
- Two Hopf bifurcations occur at delays $\tau≈1.9$ and $\tau≈108$.
- Between these, the model shows:
- Stable oscillations → bistability (two distinct cycles).
- Period-doubling → chaos → back to periodicity.
- Strange attractor with sensitive dependence on initial conditions (the hallmark of chaos).
5. Ecological interpretation
- Delay = processing or maturation time.
Longer delays (e.g., due to lower temperatures) can destabilize predator–prey dynamics. - Moderate delay → large oscillations; populations risk stochastic extinction if they dip too low.
- Long delay → predators go extinct deterministically.
- Short delay → stable coexistence.
- Temperature effects: since digestion/maturation speeds up with warmth, climate change can shift populations between stable, cyclic, or chaotic regimes.
6. Broader implications
- Even the simplest predator–prey model can show complex and unpredictable dynamics when biologically realistic time lags are included.
- Ignoring delays can therefore mislead population predictions or conservation decisions.
- The pattern of transitions—steady → cyclic → chaotic → steady—may also occur in more realistic food-web or stage-structured models.
In short
A time delay in predator response—representing processing or maturation time—can turn a stable ecological system into one showing oscillations, bistability, and chaos.
The longer the delay, the more complex the dynamics, until predators eventually go extinct.
Would you like me to also extract the main equations and parameter meanings (in plain language) as a follow-up table? That would make it easier to connect the math to the biology.
Yes please. Can you stick to the unscaled equations as these are more intuitive. And use the d/d format rather dot format as Im not so familiar with that. Can you tabulate the parameters and provide a term by term explanation of the equations, but please keep it simple
Here’s a clear, biology-oriented breakdown of the unscaled predator–prey model used in the paper, with the equations written in the more familiar $\frac{d}{dt}$ form, followed by a simple term-by-term explanation and a table of parameters.
Unscaled model equations
$$ \begin{aligned} \frac{dx(t)}{dt} &= r\,x(t)\!\left(1 - \frac{x(t)}{K}\right) - m\,x(t)\,y(t) \\ \frac{dy(t)}{dt} &= -s\,y(t) + Y\,e^{-s\tau}\,m\,x(t-\tau)\,y(t-\tau) \end{aligned} $$
where
- $x(t)$ = prey population at time $t$
- $y(t)$ = predator population at time $t$
- $\tau$ = time delay (e.g., digestion or maturation time)
Explanation of each term
| Equation part | Meaning (biological interpretation) |
|---|---|
| $\frac{dx}{dt} = r\,x(1 - \frac{x}{K})$ | Logistic prey growth: prey increase at rate $r$, but level off near carrying capacity $K$. |
| $-m\,x\,y$ | Predation term: prey are eaten in proportion to both prey and predator abundance (simple “mass action” assumption). |
| $\frac{dy}{dt} = -s\,y$ | Predator death term: predators die naturally at rate $s$ in the absence of food. |
| $+ Y\,e^{-s\tau}\,m\,x(t-\tau)\,y(t-\tau)$ | Delayed predator growth: new predators are produced from prey eaten $\tau$ time units ago; only a fraction $e^{-s\tau}$ survive the delay period. |
Parameter table
| Symbol | Description | Units | Typical ecological meaning |
|---|---|---|---|
| $r$ | Intrinsic growth rate of prey | 1/time | How fast the prey would grow without predators |
| $K$ | Carrying capacity of prey | population | Max sustainable prey population |
| $m$ | Attack or encounter rate | 1/(predator·time) | How effectively predators catch prey |
| $s$ | Predator death rate | 1/time | Natural loss rate of predators (starvation, mortality) |
| $Y$ | Conversion efficiency | dimensionless | How efficiently consumed prey biomass is converted into predator biomass |
| $\tau$ | Time delay | time | Time between capturing prey and producing new predators (e.g., digestion or maturation) |
| $e^{-s\tau}$ | Survival factor | dimensionless | Fraction of predators surviving the delay interval |
Plain-language summary
- The prey equation says prey grow logistically but are reduced by encounters with predators.
- The predator equation says predators die continually, but gain biomass based on prey captured a time $\tau$ ago, adjusted by how many predators survive that time.
- When $\tau = 0$, both populations approach a steady equilibrium.
- When $\tau$ increases, the delayed feedback can destabilize the system, causing oscillations and potentially chaotic swings in population size.
Would you like me to rewrite the whole predator equation in plain words — like a short sentence version that reads more like a biological rule than a formula?
the final term for the prey is quite complex, can you explain it more simply, including the sub terms
Absolutely — let’s unpack that delayed predator growth term in plain, step-by-step ecological language.
The term
$$ +\, Y\, e^{-s\tau}\, m\, x(t-\tau)\, y(t-\tau) $$
This is the gain term in the predator equation:
$$ \frac{dy}{dt} = -s\,y(t) + \textcolor{blue}{Y\, e^{-s\tau}\, m\, x(t-\tau)\, y(t-\tau)}. $$
It represents how new predator biomass (or individuals) is produced from prey eaten in the past.
Breaking it into sub-parts
| Sub-term | Meaning | Biological interpretation |
|---|---|---|
| $x(t-\tau)$ | Prey abundance τ time units ago | How many prey were available when predators were feeding in the past. |
| $y(t-\tau)$ | Predator abundance τ time units ago | How many predators were hunting back then. |
| $m\,x(t-\tau)\,y(t-\tau)$ | Predation rate τ time units ago | The rate at which prey were being captured and consumed then (simple mass-action assumption). |
| $e^{-s\tau}$ | Survival fraction of predators | The proportion of those predators that survived the time delay (τ). Some die before digesting prey or producing offspring. If $s$ is high or τ is long, fewer survive. |
| $Y$ | Yield or conversion efficiency | How efficiently consumed prey biomass is turned into predator biomass (energy gain → reproduction). |
| Altogether | $Y\, e^{-s\tau}\, m\, x(t-\tau)\, y(t-\tau)$ | The rate of new predator production now, based on how many prey were eaten τ time units ago and how many of those predators survived to benefit from that meal. |
Putting it simply
- At time $t - \tau$, predators ate prey at rate $m\,x(t-\tau)\,y(t-\tau)$.
- It takes τ units of time for that food to be converted into predator growth.
- Some predators die before then → only a fraction $e^{-s\tau}$ survive.
- Each surviving predator gains biomass with efficiency $Y$.
- So, at time $t$, the net predator growth rate from those earlier meals is $$ Y\,e^{-s\tau}\,m\,x(t-\tau)\,y(t-\tau). $$
Would you like me to rewrite the whole predator equation in plain words — like a short sentence version that reads more like a biological rule than a formula?
yep can you write out word equations for both predator and prey
Sure — here are simple word equations that capture the meaning of both the prey and predator dynamics in the model, using everyday ecological language rather than symbols.
Prey equation
$$ \frac{dx}{dt} = r\,x(t)\!\left(1 - \frac{x(t)}{K}\right) - m\,x(t)\,y(t) $$
Word equation:
Change in prey = natural growth of prey (limited by carrying capacity) − losses to predation.
In words:
- The prey population increases through natural reproduction, but this growth slows as the population approaches the environment’s carrying capacity.
- At the same time, prey are removed when they are eaten by predators, at a rate that depends on how many prey and predators are present.
Predator equation
$$ \frac{dy}{dt} = -s\,y(t) + Y\,e^{-s\tau}\,m\,x(t-\tau)\,y(t-\tau) $$
Word equation:
Change in predators = natural predator deaths + growth from prey consumed earlier (after a delay).
In words:
- The predator population declines naturally as individuals die at a constant rate.
- It increases when earlier feeding events (from τ time units ago) result in the production of new predator biomass or offspring.
- Only a fraction of predators survive the delay period, and the energy gained from the prey is converted into new predator growth with some efficiency.
Together:
Prey grow logistically but are eaten by predators.
Predators die naturally but increase by converting previously captured prey into new predator biomass after a delay.
Would you like me to include a simple feedback diagram (arrows showing how prey growth, predation, delay, and mortality interact)? It can make the system’s structure even clearer.
Id like to turn this into a web app in a self contained html/JS page. Something that would display predator and prey distributions for different parameter values in real time. We could have sliders for the various parameters (including length of simulation and time step). Id like to use a simple numerical method like forward difference if possible so it's easy to explain the methodology. Does this make sense? Do you need any additional information from me?
That makes perfect sense. Here’s a single, self-contained HTML file that runs the unscaled delayed predator–prey model with simple forward-Euler (forward difference), interactive sliders (including duration and time step), real-time plotting, pause/reset, and CSV export.
Copy everything below into a file like pred_prey_delay.html and open it in your browser.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Predator–Prey with Delay (Forward Euler)</title>
<style>
:root { --fg:#0f172a; --muted:#475569; --accent:#2563eb; --bg:#f8fafc; }
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif;
margin:0; color:var(--fg); background:var(--bg); }
header { padding:16px 20px; border-bottom:1px solid #e5e7eb; background:white; position:sticky; top:0; z-index:5;}
header h1 { margin:0; font-size:18px; }
main { display:grid; grid-template-columns: 360px 1fr; gap:16px; padding:16px; }
@media (max-width: 980px) { main { grid-template-columns:1fr; } }
.panel { background:white; border:1px solid #e5e7eb; border-radius:12px; padding:14px; }
.row { display:flex; align-items:center; gap:10px; margin:10px 0; }
.row label { width:125px; font-size:13px; color:var(--muted); }
input[type="range"] { width:100%; }
.val { min-width:72px; text-align:right; font-variant-numeric: tabular-nums; }
.btns { display:flex; gap:8px; flex-wrap:wrap; margin-top:10px; }
button { border:1px solid #d1d5db; background:#fff; padding:8px 12px; border-radius:10px; cursor:pointer; }
button.primary { background:var(--accent); color:white; border-color:var(--accent); }
canvas { width:100%; height:360px; border-radius:12px; background:white; border:1px solid #e5e7eb; }
.small { font-size:12px; color:var(--muted); line-height:1.35; }
.plots { display:grid; grid-template-columns:1fr; gap:12px; }
.legend { display:flex; gap:12px; align-items:center; margin:8px 0 0; }
.dot { width:10px; height:10px; border-radius:10px; display:inline-block; }
.dot.prey { background:#16a34a; } /* green */
.dot.pred { background:#ef4444; } /* red */
.dot.phase { background:#0ea5e9; } /* cyan */
.split { display:grid; grid-template-columns: 1fr 1fr; gap:12px; }
.code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
</style>
</head>
<body>
<header>
<h1>Predator–Prey with Discrete Delay — Forward Euler (unscaled model)</h1>
</header>
<main>
<!-- Controls -->
<section class="panel" id="controls">
<h3 style="margin:0 0 8px 0;">Parameters</h3>
<div class="row"><label for="r">Prey growth r</label>
<input id="r" type="range" min="0" max="3" step="0.01" value="1.0"/>
<div class="val" id="r_val">1.00</div>
</div>
<div class="row"><label for="K">Carrying capacity K</label>
<input id="K" type="range" min="0.1" max="10" step="0.1" value="1.0"/>
<div class="val" id="K_val">1.0</div>
</div>
<div class="row"><label for="m">Attack rate m</label>
<input id="m" type="range" min="0" max="5" step="0.01" value="1.0"/>
<div class="val" id="m_val">1.00</div>
</div>
<div class="row"><label for="s">Predator death s</label>
<input id="s" type="range" min="0" max="1" step="0.001" value="0.02"/>
<div class="val" id="s_val">0.020</div>
</div>
<div class="row"><label for="Y">Conversion Y</label>
<input id="Y" type="range" min="0" max="2" step="0.01" value="0.6"/>
<div class="val" id="Y_val">0.60</div>
</div>
<div class="row"><label for="tau">Delay τ (time)</label>
<input id="tau" type="range" min="0" max="200" step="0.1" value="0"/>
<div class="val" id="tau_val">0.0</div>
</div>
<h3 style="margin:14px 0 8px 0;">Initial conditions & numerics</h3>
<div class="row"><label for="x0">Prey x(≤0)</label>
<input id="x0" type="range" min="0" max="2" step="0.01" value="0.1"/>
<div class="val" id="x0_val">0.10</div>
</div>
<div class="row"><label for="y0">Predator y(≤0)</label>
<input id="y0" type="range" min="0" max="2" step="0.01" value="0.1"/>
<div class="val" id="y0_val">0.10</div>
</div>
<div class="row"><label for="dt">Time step Δt</label>
<input id="dt" type="range" min="0.0005" max="0.5" step="0.0005" value="0.01"/>
<div class="val" id="dt_val">0.0100</div>
</div>
<div class="row"><label for="T">Duration T</label>
<input id="T" type="range" min="50" max="5000" step="10" value="1200"/>
<div class="val" id="T_val">1200</div>
</div>
<div class="btns">
<button id="start" class="primary">Start</button>
<button id="pause">Pause</button>
<button id="reset">Reset</button>
<button id="export">Export CSV</button>
<button id="clear">Clear Plots</button>
</div>
<p class="small" style="margin-top:10px;">
<strong>Numerics:</strong> Forward Euler updates:
<span class="code">x<sub>n+1</sub> = x<sub>n</sub> + Δt · [r x<sub>n</sub>(1 − x<sub>n</sub>/K) − m x<sub>n</sub> y<sub>n</sub>]</span>,
<span class="code">y<sub>n+1</sub> = y<sub>n</sub> + Δt · [−s y<sub>n</sub> + Y e<sup>−sτ</sup> m x<sub>n−d</sub> y<sub>n−d</sub>]</span>,
with delay index <span class="code">d = round(τ/Δt)</span>. History for <span class="code">t ≤ 0</span> set to
<span class="code">x = x₀</span>, <span class="code">y = y₀</span>. Values are floored at 0.
</p>
<p class="small">
Tip: If things blow up or oscillate too wildly numerically, reduce Δt.
</p>
</section>
<!-- Plots -->
<section class="plots">
<div class="panel">
<h3 style="margin:0 0 8px 0;">Time series</h3>
<canvas id="ts"></canvas>
<div class="legend">
<span class="dot prey"></span> Prey x(t)
<span class="dot pred"></span> Predator y(t)
</div>
</div>
<div class="panel">
<h3 style="margin:0 0 8px 0;">Phase plane (x vs y)</h3>
<canvas id="phase"></canvas>
<div class="legend">
<span class="dot phase"></span> Trajectory
</div>
</div>
</section>
</main>
<script>
(() => {
// ---- DOM helpers ----
const el = id => document.getElementById(id);
const inputs = ["r","K","m","s","Y","tau","x0","y0","dt","T"];
const state = {};
function syncVal(id) {
const v = parseFloat(el(id).value);
state[id] = v;
el(id + "_val").textContent = (id==="K"||id==="T") ? v.toFixed(0)
: (id==="s" ? v.toFixed(3)
: (id==="dt" ? v.toFixed(4)
: v.toFixed(2)));
if (id==="tau") { // show one decimal for tau
el(id + "_val").textContent = v.toFixed(1);
}
}
inputs.forEach(id => {
syncVal(id);
el(id).addEventListener("input", () => {
syncVal(id);
if (id==="dt" || id==="tau") recomputeDelaySteps();
});
});
// ---- Simulation buffers ----
let t = 0, running = false, rafId = null;
let xs = [], ys = [], ts = [];
let delaySteps = 0;
let lastDrawIdx = 0;
// canvases and contexts
const tsCanvas = el("ts"), phCanvas = el("phase");
const tsCtx = tsCanvas.getContext("2d");
const phCtx = phCanvas.getContext("2d");
function resizeCanvases() {
const setSize = (cv) => { const rect = cv.getBoundingClientRect(); cv.width = Math.max(600, rect.width*devicePixelRatio|0); cv.height = Math.max(250, rect.height*devicePixelRatio|0); };
setSize(tsCanvas); setSize(phCanvas);
drawAll(true);
}
window.addEventListener("resize", resizeCanvases, {passive:true});
resizeCanvases();
function recomputeDelaySteps() {
delaySteps = Math.max(0, Math.round(state.tau / state.dt));
}
recomputeDelaySteps();
// ---- Initialization ----
function initialize() {
t = 0;
xs = []; ys = []; ts = [];
// prefill history for t <= 0 with (x0,y0)
for (let i = 0; i < delaySteps; i++) {
xs.push(state.x0);
ys.push(state.y0);
ts.push(-(delaySteps - i) * state.dt);
}
// seed current
xs.push(state.x0); ys.push(state.y0); ts.push(0);
lastDrawIdx = 0;
drawAll(true);
}
initialize();
// ---- Model step (Forward Euler) ----
function stepOnce() {
const n = xs.length - 1;
const x = xs[n], y = ys[n];
const dt = state.dt;
const r = state.r, K = state.K, m = state.m, s = state.s, Y = state.Y, tau = state.tau;
// delay term indices
const d = delaySteps;
const idxDelay = Math.max(0, n - d);
const xd = xs[idxDelay], yd = ys[idxDelay];
// derivatives (unscaled model)
const dx = r * x * (1 - x / K) - m * x * y;
const dy = -s * y + Y * Math.exp(-s * tau) * m * xd * yd;
const xn = Math.max(0, x + dt * dx);
const yn = Math.max(0, y + dt * dy);
xs.push(xn);
ys.push(yn);
t += dt;
ts.push(t);
}
// ---- Animation loop ----
function loop() {
if (!running) return;
// simulate a small block per frame for speed (keeps ~real-time)
const blockSteps = 200; // compute up to 200 Euler steps per frame
let k = 0;
const T = state.T;
while (k < blockSteps && t < T) {
stepOnce();
k++;
}
drawAll(false);
if (t >= T) {
running = false;
rafId = null;
return;
}
rafId = requestAnimationFrame(loop);
}
// ---- Plotting helpers ----
function drawAll(full) {
drawTimeSeries(full);
drawPhase(full);
}
function autoscale(arr, pad=0.08) {
let min = Infinity, max = -Infinity;
for (let i=0;i<arr.length;i++) { const v=arr[i]; if (!Number.isFinite(v)) continue; if (v<min) min=v; if (v>max) max=v; }
if (!Number.isFinite(min) || !Number.isFinite(max)) { min=0; max=1; }
if (Math.abs(max-min) < 1e-6) { max = min + 1; }
const span = max-min;
return { min: Math.max(0, min - pad*span), max: max + pad*span };
}
function drawTimeSeries(full) {
const ctx = tsCtx, W = tsCanvas.width, H = tsCanvas.height;
ctx.clearRect(0,0,W,H);
// axes
ctx.strokeStyle = "#e5e7eb"; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(40,10); ctx.lineTo(40,H-30); ctx.lineTo(W-10,H-30); ctx.stroke();
// scale
const tmin = 0, tmax = Math.max(state.T, ts[ts.length-1]||state.T);
const xScale = v => 40 + (W-50) * (v - tmin) / (tmax - tmin + 1e-12);
const yRange = autoscale([...xs, ...ys]);
const yScale = v => (H-30) - (H-40) * (v - yRange.min) / (yRange.max - yRange.min + 1e-12);
// grid lines (few)
ctx.fillStyle = "#94a3b8"; ctx.font = "12px system-ui, sans-serif";
ctx.fillText("t", W-24, H-10);
ctx.save(); ctx.translate(12, 16); ctx.rotate(-Math.PI/2); ctx.fillText("population", 0, 0); ctx.restore();
// prey
ctx.strokeStyle = "#16a34a"; ctx.lineWidth = 1.5; ctx.beginPath();
for (let i=0;i<ts.length;i++){
const px = xScale(Math.max(0, ts[i]));
const py = yScale(xs[i]);
if (i===0) ctx.moveTo(px,py); else ctx.lineTo(px,py);
}
ctx.stroke();
// predator
ctx.strokeStyle = "#ef4444"; ctx.lineWidth = 1.5; ctx.beginPath();
for (let i=0;i<ts.length;i++){
const px = xScale(Math.max(0, ts[i]));
const py = yScale(ys[i]);
if (i===0) ctx.moveTo(px,py); else ctx.lineTo(px,py);
}
ctx.stroke();
// labels
ctx.fillStyle = "#16a34a"; ctx.fillText("x(t)", 48, 18);
ctx.fillStyle = "#ef4444"; ctx.fillText("y(t)", 84, 18);
}
function drawPhase(full) {
const ctx = phCtx, W = phCanvas.width, H = phCanvas.height;
ctx.clearRect(0,0,W,H);
// axes
ctx.strokeStyle = "#e5e7eb"; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(40,10); ctx.lineTo(40,H-30); ctx.lineTo(W-10,H-30); ctx.stroke();
const xRange = autoscale(xs);
const yRange = autoscale(ys);
const xScale = v => 40 + (W-50) * (v - xRange.min) / (xRange.max - xRange.min + 1e-12);
const yScale = v => (H-30) - (H-40) * (v - yRange.min) / (yRange.max - yRange.min + 1e-12);
// path
ctx.strokeStyle = "#0ea5e9"; ctx.lineWidth = 1.4; ctx.beginPath();
for (let i=0;i<xs.length;i++){
const px = xScale(xs[i]);
const py = yScale(ys[i]);
if (i===0) ctx.moveTo(px,py); else ctx.lineTo(px,py);
}
ctx.stroke();
// labels
ctx.fillStyle = "#94a3b8"; ctx.font = "12px system-ui, sans-serif";
ctx.fillText("prey x", 50, 18);
ctx.save(); ctx.translate(12, 60); ctx.rotate(-Math.PI/2); ctx.fillText("predator y", 0, 0); ctx.restore();
}
// ---- Buttons ----
el("start").addEventListener("click", () => {
if (!running) {
running = true;
if (!rafId) rafId = requestAnimationFrame(loop);
}
});
el("pause").addEventListener("click", () => {
running = false;
if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
});
el("reset").addEventListener("click", () => {
running = false;
if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
recomputeDelaySteps();
initialize();
});
el("clear").addEventListener("click", () => {
running = false;
if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
xs = []; ys = []; ts = []; t = 0; drawAll(true);
});
el("export").addEventListener("click", () => {
let csv = "t,x,y\n";
for (let i=0;i<ts.length;i++) {
csv += `${ts[i]},${xs[i]},${ys[i]}\n`;
}
const blob = new Blob([csv], {type:"text/csv"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = "pred_prey_delay.csv";
document.body.appendChild(a); a.click();
setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 0);
});
// If any parameter slider changes while running, keep going with new values.
inputs.forEach(id => el(id).addEventListener("change", () => {
// when tau or dt changes, delaySteps may change; preserve arrays sensibly by padding/trimming history
if (id==="dt" || id==="tau") {
const oldD = delaySteps;
recomputeDelaySteps();
const newD = delaySteps;
const deficit = newD - oldD;
if (deficit > 0) {
// need more history: pad using current earliest known value
const xpad = xs.length ? xs[0] : state.x0;
const ypad = ys.length ? ys[0] : state.y0;
const t0 = ts.length ? ts[0] : 0;
for (let i=0;i<deficit;i++) {
xs.unshift(xpad);
ys.unshift(ypad);
ts.unshift(t0 - (deficit - i) * state.dt);
}
} else if (deficit < 0) {
// can drop extra history (but keep at least one point)
xs = xs.slice(-deficit);
ys = ys.slice(-deficit);
ts = ts.slice(-deficit);
}
}
}));
})();
</script>
</body>
</html>
Notes on what’s happening
- Equations (unscaled):
dx/dt = r·x·(1 − x/K) − m·x·ydy/dt = −s·y + Y·e^(−s·τ)·m·x(t−τ)·y(t−τ)
- Numerics: Forward Euler (forward difference). Delay handled by keeping a history buffer and reading the state from
round(τ/Δt)steps back. - Initial history: For all times ≤ 0, the app uses constant values
x0,y0. You can change those with sliders. - Stability tip: If the plot “explodes” due to numerical error, reduce Δt (time step). Forward Euler is simple but not very stable at large Δt.
If you want extras (e.g., show the coexistence equilibrium or add a second plot for the delayed product $x(t-τ)y(t-τ)$), I can wire those in.
rather than showing the timeseries evolve can we just show the full timeseies and recalculate that each time a parameter is changes. So we wouldnt need a start, clear and reset button
Absolutely—here’s a single, self-contained HTML page that recomputes and plots the full time series instantly whenever you change a parameter. There are no start/reset buttons; moving any slider triggers a fresh simulation and redraw. It uses the unscaled delayed predator–prey model and a simple forward Euler scheme.
Copy this into a file like pred_prey_delay_static.html and open it in your browser.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Predator–Prey with Delay — Full Recompute (Forward Euler)</title>
<style>
:root { --fg:#0f172a; --muted:#475569; --accent:#2563eb; --bg:#f8fafc; }
* { box-sizing: border-box; }
body { margin:0; color:var(--fg); background:var(--bg);
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif; }
header { padding:16px 20px; border-bottom:1px solid #e5e7eb; background:#fff; position:sticky; top:0; z-index:2; }
header h1 { margin:0; font-size:18px; }
main { display:grid; grid-template-columns:360px 1fr; gap:16px; padding:16px; }
@media (max-width: 980px) { main { grid-template-columns:1fr; } }
.panel { background:#fff; border:1px solid #e5e7eb; border-radius:12px; padding:14px; }
.row { display:flex; align-items:center; gap:10px; margin:10px 0; }
.row label { width:150px; font-size:13px; color:var(--muted); }
input[type="range"] { width:100%; }
.val { min-width:72px; text-align:right; font-variant-numeric: tabular-nums; }
canvas { width:100%; height:360px; border-radius:12px; background:#fff; border:1px solid #e5e7eb; }
.legend { display:flex; gap:12px; align-items:center; margin:8px 0 0; font-size:13px; color:var(--muted); }
.dot { width:10px; height:10px; border-radius:50%; display:inline-block; }
.dot.prey { background:#16a34a; }
.dot.pred { background:#ef4444; }
.dot.phase { background:#0ea5e9; }
.small { font-size:12px; color:var(--muted); line-height:1.35; }
.btns { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
button { border:1px solid #d1d5db; background:#fff; padding:8px 12px; border-radius:10px; cursor:pointer; }
</style>
</head>
<body>
<header>
<h1>Predator–Prey with Discrete Delay — Full Time Series (Forward Euler)</h1>
</header>
<main>
<!-- Controls -->
<section class="panel">
<h3 style="margin:0 0 8px 0;">Parameters</h3>
<div class="row"><label for="r">Prey growth r</label>
<input id="r" type="range" min="0" max="3" step="0.01" value="1.0"/>
<div class="val" id="r_val">1.00</div>
</div>
<div class="row"><label for="K">Carrying capacity K</label>
<input id="K" type="range" min="0.1" max="10" step="0.1" value="1.0"/>
<div class="val" id="K_val">1.0</div>
</div>
<div class="row"><label for="m">Attack rate m</label>
<input id="m" type="range" min="0" max="5" step="0.01" value="1.0"/>
<div class="val" id="m_val">1.00</div>
</div>
<div class="row"><label for="s">Predator death s</label>
<input id="s" type="range" min="0" max="1" step="0.001" value="0.02"/>
<div class="val" id="s_val">0.020</div>
</div>
<div class="row"><label for="Y">Conversion Y</label>
<input id="Y" type="range" min="0" max="2" step="0.01" value="0.6"/>
<div class="val" id="Y_val">0.60</div>
</div>
<div class="row"><label for="tau">Delay τ (time)</label>
<input id="tau" type="range" min="0" max="200" step="0.1" value="0"/>
<div class="val" id="tau_val">0.0</div>
</div>
<h3 style="margin:14px 0 8px 0;">Initial conditions & numerics</h3>
<div class="row"><label for="x0">Prey x(≤0)</label>
<input id="x0" type="range" min="0" max="2" step="0.01" value="0.1"/>
<div class="val" id="x0_val">0.10</div>
</div>
<div class="row"><label for="y0">Predator y(≤0)</label>
<input id="y0" type="range" min="0" max="2" step="0.01" value="0.1"/>
<div class="val" id="y0_val">0.10</div>
</div>
<div class="row"><label for="dt">Time step Δt</label>
<input id="dt" type="range" min="0.0005" max="0.5" step="0.0005" value="0.01"/>
<div class="val" id="dt_val">0.0100</div>
</div>
<div class="row"><label for="T">Duration T</label>
<input id="T" type="range" min="50" max="5000" step="10" value="1200"/>
<div class="val" id="T_val">1200</div>
</div>
<div class="btns">
<button id="export">Export CSV</button>
</div>
<p class="small" style="margin-top:10px;">
<strong>Model (unscaled):</strong>
<span>dx/dt = r·x·(1 − x/K) − m·x·y</span>,
<span>dy/dt = −s·y + Y·e<sup>−sτ</sup>·m·x(t−τ)·y(t−τ)</span>.
Forward Euler; delay index <em>d = round(τ/Δt)</em>. History for <em>t ≤ 0</em>: x = x₀, y = y₀.
</p>
<p class="small">If curves look numerically unstable, reduce Δt.</p>
</section>
<!-- Plots -->
<section class="panel">
<h3 style="margin:0 0 8px 0;">Time series</h3>
<canvas id="ts"></canvas>
<div class="legend">
<span class="dot prey"></span> Prey x(t)
<span class="dot pred"></span> Predator y(t)
</div>
</section>
<section class="panel">
<h3 style="margin:0 0 8px 0;">Phase plane (x vs y)</h3>
<canvas id="phase"></canvas>
<div class="legend"><span class="dot phase"></span> Trajectory</div>
</section>
</main>
<script>
(() => {
// --- Elements & state ---
const el = id => document.getElementById(id);
const sliders = ["r","K","m","s","Y","tau","x0","y0","dt","T"];
const state = {};
let ts = [], xs = [], ys = [];
// canvases
const tsCanvas = el("ts"), tsCtx = tsCanvas.getContext("2d");
const phCanvas = el("phase"), phCtx = phCanvas.getContext("2d");
function resizeCanvases() {
const setSize = (cv) => {
const rect = cv.getBoundingClientRect();
cv.width = Math.max(600, Math.round(rect.width * devicePixelRatio));
cv.height = Math.max(250, Math.round(rect.height * devicePixelRatio));
};
setSize(tsCanvas); setSize(phCanvas);
drawAll();
}
window.addEventListener("resize", resizeCanvases, {passive:true});
function syncVal(id) {
const v = parseFloat(el(id).value);
state[id] = v;
const target = el(id + "_val");
if (!target) return;
let text;
if (id==="K" || id==="T") text = v.toFixed(0);
else if (id==="s") text = v.toFixed(3);
else if (id==="dt") text = v.toFixed(4);
else if (id==="tau") text = v.toFixed(1);
else text = v.toFixed(2);
target.textContent = text;
}
sliders.forEach(id => {
syncVal(id);
el(id).addEventListener("input", () => { syncVal(id); recompute(); });
});
// --- Simulation (full recompute) ---
function recompute() {
const r=state.r, K=state.K, m=state.m, s=state.s, Y=state.Y, tau=state.tau;
const x0=state.x0, y0=state.y0, dt=state.dt, T=state.T;
// guardrails
const N = Math.max(2, Math.floor(T / dt) + 1);
const d = Math.max(0, Math.round(tau / dt));
xs = new Array(N + d);
ys = new Array(N + d);
ts = new Array(N + d);
// fill history for t ≤ 0
for (let i=0; i<d; i++) {
xs[i] = x0; ys[i] = y0; ts[i] = (i - d) * dt; // negative times
}
// set initial current point at t=0
xs[d] = x0; ys[d] = y0; ts[d] = 0;
// integrate forward
for (let n = d; n < d + N - 1; n++) {
const x = xs[n], y = ys[n];
const xd = xs[n - d], yd = ys[n - d]; // delayed
const dx = r * x * (1 - x / K) - m * x * y;
const dy = -s * y + Y * Math.exp(-s * tau) * m * xd * yd;
const xn = Math.max(0, x + dt * dx);
const yn = Math.max(0, y + dt * dy);
xs[n+1] = xn; ys[n+1] = yn; ts[n+1] = ts[n] + dt;
}
// trim off the negative-time history for plotting/export
if (d > 0) {
xs = xs.slice(d); ys = ys.slice(d); ts = ts.slice(d);
}
drawAll();
}
// --- Plotting ---
function autoscale(arr, pad=0.08) {
let min = Infinity, max = -Infinity;
for (let v of arr) { if (Number.isFinite(v)) { if (v<min) min=v; if (v>max) max=v; } }
if (!Number.isFinite(min) || !Number.isFinite(max)) { min=0; max=1; }
if (Math.abs(max-min) < 1e-12) { max = min + 1; }
const span = max - min;
return { min: Math.max(0, min - pad*span), max: max + pad*span };
}
function drawAll() { drawTimeSeries(); drawPhase(); }
function drawTimeSeries() {
const ctx = tsCtx, W = tsCanvas.width, H = tsCanvas.height;
ctx.clearRect(0,0,W,H);
// axes
ctx.strokeStyle = "#e5e7eb"; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(40,10); ctx.lineTo(40,H-30); ctx.lineTo(W-10,H-30); ctx.stroke();
const tmin = 0, tmax = ts.length ? ts[ts.length-1] : state.T;
const xRange = autoscale([...xs, ...ys]);
const sx = v => 40 + (W-50) * (v - tmin) / (tmax - tmin + 1e-12);
const sy = v => (H-30) - (H-40) * (v - xRange.min) / (xRange.max - xRange.min + 1e-12);
// labels
ctx.fillStyle = "#94a3b8"; ctx.font = "12px system-ui, sans-serif";
ctx.fillText("t", W-24, H-10);
ctx.save(); ctx.translate(12, 18); ctx.rotate(-Math.PI/2); ctx.fillText("population", 0, 0); ctx.restore();
// prey
ctx.strokeStyle = "#16a34a"; ctx.lineWidth = 1.5; ctx.beginPath();
for (let i=0;i<xs.length;i++){ const px=sx(ts[i]); const py=sy(xs[i]); if (i===0) ctx.moveTo(px,py); else ctx.lineTo(px,py); }
ctx.stroke();
// predator
ctx.strokeStyle = "#ef4444"; ctx.lineWidth = 1.5; ctx.beginPath();
for (let i=0;i<ys.length;i++){ const px=sx(ts[i]); const py=sy(ys[i]); if (i===0) ctx.moveTo(px,py); else ctx.lineTo(px,py); }
ctx.stroke();
// series tags
ctx.fillStyle = "#16a34a"; ctx.fillText("x(t)", 50, 18);
ctx.fillStyle = "#ef4444"; ctx.fillText("y(t)", 86, 18);
}
function drawPhase() {
const ctx = phCtx, W = phCanvas.width, H = phCanvas.height;
ctx.clearRect(0,0,W,H);
// axes
ctx.strokeStyle = "#e5e7eb"; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(40,10); ctx.lineTo(40,H-30); ctx.lineTo(W-10,H-30); ctx.stroke();
const xr = autoscale(xs), yr = autoscale(ys);
const sx = v => 40 + (W-50) * (v - xr.min) / (xr.max - xr.min + 1e-12);
const sy = v => (H-30) - (H-40) * (v - yr.min) / (yr.max - yr.min + 1e-12);
ctx.strokeStyle = "#0ea5e9"; ctx.lineWidth = 1.4; ctx.beginPath();
for (let i=0;i<xs.length;i++){ const px=sx(xs[i]); const py=sy(ys[i]); if (i===0) ctx.moveTo(px,py); else ctx.lineTo(px,py); }
ctx.stroke();
ctx.fillStyle = "#94a3b8"; ctx.font = "12px system-ui, sans-serif";
ctx.fillText("prey x", 50, 18);
ctx.save(); ctx.translate(12, 60); ctx.rotate(-Math.PI/2); ctx.fillText("predator y", 0, 0); ctx.restore();
}
// --- Export ---
el("export").addEventListener("click", () => {
let csv = "t,x,y\n";
for (let i=0;i<ts.length;i++) csv += `${ts[i]},${xs[i]},${ys[i]}\n`;
const blob = new Blob([csv], {type:"text/csv"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = "pred_prey_delay.csv";
document.body.appendChild(a); a.click();
setTimeout(()=>{ URL.revokeObjectURL(url); a.remove(); },0);
});
// initial layout + compute
resizeCanvases();
recompute();
})();
</script>
</body>
</html>
What changed vs. the previous version
- No animation loop; the full series is computed immediately on every slider move.
- Removed start/pause/reset/clear; kept CSV export for convenience.
- Delay handled by an integer index
d = round(τ/Δt)into the history buffer. - Initial history for all $t \le 0$: $x=x_0$, $y=y_0$.
- Plots: time series and phase plane update instantly.
If you’d like, I can add optional overlays (e.g., the coexistence equilibrium when it exists) or a toggle to show $x(t-\tau)\,y(t-\tau)$ as a third curve.
Great. 1. Can we have ? icons next to each paramter that opens up help on what that parameter does 2. can we include a drop down text box that includes the rendered model equations with the associated word equations and explanations of each term 3. can we include a second drop down text box that very simply explains how the equations are solved ising the forward discretisation. The explanation should be for someone with little or no knowledhe of how to solve these equatons (so model (unscaled) text avn then be removed).4. finally tave a think about the overall layout of the app to improve it e.g. readable axis labels, layout of phase plot, adding bit of colour, better app title - anything to improve the app look
Love it. I’ve rebuilt the app to add per-parameter help, two collapsible “explainers”, and a tidier UI (clearer axes, nicer colors, better title, balanced phase plot). It still recomputes the full time series instantly on any change and keeps everything in one HTML file.
Save as pred_prey_delay_explorer.html and open in your browser.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Predator–Prey with Processing Delay — Interactive Explorer</title>
<!-- MathJax (for rendered equations in the explainers) -->
<script>
window.MathJax = { tex: {inlineMath: [['\\(','\\)'], ['$', '$']]} };
</script>
<script id="MathJax-script" async
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<style>
:root{
--fg:#0f172a; --muted:#475569; --accent:#2563eb; --bg:#f8fafc;
--grid:#e5e7eb; --panel:#ffffff; --prey:#16a34a; --pred:#ef4444; --phase:#0ea5e9;
}
*{ box-sizing: border-box; }
body{ margin:0; color:var(--fg); background:var(--bg);
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif; }
header{ padding:18px 20px; border-bottom:1px solid var(--grid); background:var(--panel); position:sticky; top:0; z-index:2; }
header h1{ margin:0; font-size:20px; line-height:1.2; }
header p{ margin:4px 0 0; color:var(--muted); font-size:13px; }
main{ display:grid; grid-template-columns:380px 1fr; gap:16px; padding:16px; }
@media (max-width: 1040px){ main{ grid-template-columns:1fr; } }
.panel{ background:var(--panel); border:1px solid var(--grid); border-radius:12px; padding:14px; }
.panel h3{ margin:0 0 8px 0; font-size:16px; }
.row{ display:grid; grid-template-columns: 150px 1fr 70px 24px; gap:10px; align-items:center; margin:10px 0; }
.row label{ font-size:13px; color:var(--muted); }
input[type="range"]{ width:100%; }
.val{ text-align:right; font-variant-numeric: tabular-nums; color:#0b1324; }
.help-btn{
width:22px; height:22px; border-radius:50%;
border:1px solid #cbd5e1; background:#fff; cursor:pointer; padding:0; font-weight:700;
color:#334155; line-height:20px;
}
.help-btn:focus{ outline:2px solid var(--accent); outline-offset:2px; }
.help-pop{
position:relative;
}
.help-pop .bubble{
display:none; position:absolute; right:0; top:28px; width:min(320px, 80vw);
background:#fff; border:1px solid var(--grid); border-radius:10px; padding:10px 12px; box-shadow:0 8px 24px rgba(2,6,23,.06);
font-size:12.5px; color:#0b1324; z-index:10;
}
.help-pop .bubble p{ margin:6px 0; }
.help-pop.open .bubble{ display:block; }
.legend{ display:flex; gap:12px; align-items:center; margin-top:8px; color:var(--muted); font-size:13px; }
.dot{ width:10px; height:10px; border-radius:50%; display:inline-block; }
.dot.prey{ background:var(--prey); }
.dot.pred{ background:var(--pred); }
.dot.phase{ background:var(--phase); }
.small{ font-size:12px; color:var(--muted); line-height:1.4; }
.explainers details{ border:1px solid var(--grid); border-radius:10px; padding:10px 12px; background:#fff; }
.explainers details summary{ cursor:pointer; list-style:none; }
.explainers details summary::-webkit-details-marker{ display:none; }
.explainers details + details{ margin-top:8px; }
.explainers .content{ margin-top:8px; font-size:14px; color:#0b1324; }
.explainers .content p{ margin:8px 0; }
.explainers code{ background:#f1f5f9; padding:0 6px; border-radius:6px; }
canvas{ width:100%; height:380px; border-radius:12px; background:#fff; border:1px solid var(--grid); }
.plot-title{ margin:0 0 4px 0; font-size:15px; color:#0b1324; }
.grid-label{ fill:#64748b; font: 12px system-ui, sans-serif; }
.plots{ display:grid; grid-template-columns:1fr; gap:16px; }
.export-row{ display:flex; gap:8px; justify-content:flex-start; margin-top:8px; }
button.export{ border:1px solid #cbd5e1; background:#fff; padding:8px 12px; border-radius:10px; cursor:pointer; }
</style>
</head>
<body>
<header>
<h1>Predator–Prey with Processing Delay — Interactive Explorer</h1>
<p>Explore how digestion/maturation delay shapes dynamics. Move any slider to recompute the full trajectory instantly.</p>
</header>
<main>
<!-- Controls -->
<section class="panel">
<h3>Parameters</h3>
<!-- r -->
<div class="row">
<label for="r">Prey growth r</label>
<input id="r" type="range" min="0" max="3" step="0.01" value="1.00"/>
<div class="val" id="r_val">1.00</div>
<div class="help-pop">
<button class="help-btn" aria-label="Help for r">?</button>
<div class="bubble">
<strong>r — intrinsic prey growth rate</strong>
<p>How fast prey would grow without predators, per unit time.</p>
<p>Higher <em>r</em> → faster rebound of the prey population.</p>
</div>
</div>
</div>
<!-- K -->
<div class="row">
<label for="K">Carrying capacity K</label>
<input id="K" type="range" min="0.1" max="10" step="0.1" value="1.0"/>
<div class="val" id="K_val">1</div>
<div class="help-pop">
<button class="help-btn" aria-label="Help for K">?</button>
<div class="bubble">
<strong>K — prey carrying capacity</strong>
<p>Upper bound set by resources; prey logistic growth slows as it nears <em>K</em>.</p>
</div>
</div>
</div>
<!-- m -->
<div class="row">
<label for="m">Attack rate m</label>
<input id="m" type="range" min="0" max="5" step="0.01" value="1.0"/>
<div class="val" id="m_val">1.00</div>
<div class="help-pop">
<button class="help-btn" aria-label="Help for m">?</button>
<div class="bubble">
<strong>m — encounter/attack rate</strong>
<p>Rate at which predators meet & capture prey (Holling type I).</p>
<p>Higher <em>m</em> → stronger predation pressure.</p>
</div>
</div>
</div>
<!-- s -->
<div class="row">
<label for="s">Predator death s</label>
<input id="s" type="range" min="0" max="1" step="0.001" value="0.02"/>
<div class="val" id="s_val">0.020</div>
<div class="help-pop">
<button class="help-btn" aria-label="Help for s">?</button>
<div class="bubble">
<strong>s — predator mortality rate</strong>
<p>Background per-time death rate. Larger <em>s</em> reduces predator persistence unless compensated by food.</p>
</div>
</div>
</div>
<!-- Y -->
<div class="row">
<label for="Y">Conversion Y</label>
<input id="Y" type="range" min="0" max="2" step="0.01" value="0.60"/>
<div class="val" id="Y_val">0.60</div>
<div class="help-pop">
<button class="help-btn" aria-label="Help for Y">?</button>
<div class="bubble">
<strong>Y — conversion efficiency</strong>
<p>How efficiently eaten prey biomass becomes predator biomass.</p>
<p>Higher <em>Y</em> boosts predator growth from each capture.</p>
</div>
</div>
</div>
<!-- tau -->
<div class="row">
<label for="tau">Delay τ</label>
<input id="tau" type="range" min="0" max="200" step="0.1" value="0.0"/>
<div class="val" id="tau_val">0.0</div>
<div class="help-pop">
<button class="help-btn" aria-label="Help for tau">?</button>
<div class="bubble">
<strong>τ — processing/maturation delay</strong>
<p>Time between prey capture and its contribution to predator growth.</p>
<p>Only a fraction <em>e<sup>−sτ</sup></em> of predators survive through the delay.</p>
</div>
</div>
</div>
<h3 style="margin-top:12px;">Initial conditions & numerics</h3>
<!-- x0 -->
<div class="row">
<label for="x0">Initial prey x (t≤0)</label>
<input id="x0" type="range" min="0" max="2" step="0.01" value="0.10"/>
<div class="val" id="x0_val">0.10</div>
<div class="help-pop">
<button class="help-btn" aria-label="Help for x0">?</button>
<div class="bubble">
<strong>Initial prey level</strong>
<p>Used for the whole pre-history (t ≤ 0) and the start at t = 0.</p>
</div>
</div>
</div>
<!-- y0 -->
<div class="row">
<label for="y0">Initial pred. y (t≤0)</label>
<input id="y0" type="range" min="0" max="2" step="0.01" value="0.10"/>
<div class="val" id="y0_val">0.10</div>
<div class="help-pop">
<button class="help-btn" aria-label="Help for y0">?</button>
<div class="bubble">
<strong>Initial predator level</strong>
<p>Used for the whole pre-history (t ≤ 0) and the start at t = 0.</p>
</div>
</div>
</div>
<!-- dt -->
<div class="row">
<label for="dt">Time step Δt</label>
<input id="dt" type="range" min="0.0005" max="0.5" step="0.0005" value="0.0100"/>
<div class="val" id="dt_val">0.0100</div>
<div class="help-pop">
<button class="help-btn" aria-label="Help for dt">?</button>
<div class="bubble">
<strong>Numerical time step</strong>
<p>Smaller Δt = more stable & accurate (but slower). If plots “blow up,” reduce Δt.</p>
</div>
</div>
</div>
<!-- T -->
<div class="row">
<label for="T">Duration T</label>
<input id="T" type="range" min="50" max="5000" step="10" value="1200"/>
<div class="val" id="T_val">1200</div>
<div class="help-pop">
<button class="help-btn" aria-label="Help for T">?</button>
<div class="bubble">
<strong>Total simulated time</strong>
<p>How long to compute the trajectory. Longer T shows more long-term behavior.</p>
</div>
</div>
</div>
<div class="export-row">
<button class="export" id="export">Export CSV</button>
</div>
<div class="explainers" style="margin-top:14px;">
<details>
<summary><strong>Model equations & plain-language meaning</strong></summary>
<div class="content">
<p><em>Unscaled predator–prey with discrete delay</em>:</p>
<p>
\[
\frac{dx(t)}{dt} = r\,x(t)\!\left(1 - \frac{x(t)}{K}\right) - m\,x(t)\,y(t)
\]
<small><br><strong>Prey rule (words):</strong> natural logistic growth limited by \(K\) minus losses to predation.</small>
</p>
<p>
\[
\frac{dy(t)}{dt} = -s\,y(t) + Y\,e^{-s\tau}\,m\,x(t-\tau)\,y(t-\tau)
\]
<small><br><strong>Predator rule (words):</strong> natural deaths plus growth from prey eaten \(\tau\) time units ago; only a fraction \(e^{-s\tau}\) of predators survive that delay, and \(Y\) converts prey to predator biomass.</small>
</p>
<p><strong>Predator gain term:</strong> \(Y\) (efficiency) × \(e^{-s\tau}\) (survival through delay) × \(m\,x(t-\tau)\,y(t-\tau)\) (past predation rate).</p>
</div>
</details>
<details>
<summary><strong>How this is solved (forward differences)</strong></summary>
<div class="content">
<p><em>Idea:</em> slice time into small steps of size \(\Delta t\). Change per step ≈ derivative × \(\Delta t\).</p>
<ol>
<li>For all \(t\le 0\), set \(x=x_0\), \(y=y_0\) (gives a “history” to look back to).</li>
<li>Turn the delay into a look-back index \(d=\mathrm{round}(\tau/\Delta t)\).</li>
<li>Euler updates:
\[
x_{n+1} = x_n + \Delta t\,[\,r\,x_n(1 - x_n/K) - m\,x_n\,y_n\,],\quad
y_{n+1} = y_n + \Delta t\,[\,-s\,y_n + Y e^{-s\tau} m x_{n-d} y_{n-d}\,].
\]
</li>
<li>If anything goes numerically wild, try a smaller \(\Delta t\).</li>
</ol>
</div>
</details>
</div>
</section>
<!-- Plots -->
<section class="panel">
<div class="plot-title">
<span>Time series</span>
</div>
<canvas id="ts"></canvas>
<div class="legend">
<span class="dot prey"></span> Prey \(x(t)\)
<span class="dot pred"></span> Predator \(y(t)\)
</div>
</section>
<section class="panel">
<div class="plot-title">
<span>Phase plane: predator vs prey</span>
<div class="inline-help" id="phase-help">
<button type="button">? about this plot</button>
<div class="bubble">
<p><strong>How to read:</strong> Each point is \((x(t), y(t))\). The curve shows how the system moves through prey–predator space over time.</p>
<ul>
<li><em>Stable equilibrium:</em> the path spirals in and settles at a point.</li>
<li><em>Oscillations (limit cycle):</em> the path closes on a loop and keeps circling.</li>
<li><em>Chaotic regime:</em> the path never repeats; it fills out a fuzzy band or tangled set.</li>
</ul>
<p>As the delay \(\tau\) increases from 0 (with other values fixed as in the presets), the system typically goes from a stable point → periodic loops → complex/chaotic bands, then can return to stability for very large \(\tau\).</p>
</div>
</div>
</div>
<canvas id="phase" aria-label="Phase plot, predator vs prey"></canvas>
<div class="legend"><span class="dot phase"></span> Trajectory</div>
</section>
</main>
<script>
(() => {
const el = id => document.getElementById(id);
const sliders = ["r","K","m","s","Y","tau","x0","y0","dt","T"];
const state = {};
let ts = [], xs = [], ys = [];
// Help bubbles
document.addEventListener("click", (e) => {
const btn = e.target.closest(".help-btn");
document.querySelectorAll(".help-pop").forEach(p => {
if (btn && p.contains(btn)) p.classList.toggle("open");
else p.classList.remove("open");
});
});
const phaseHelp = document.getElementById("phase-help");
phaseHelp.querySelector("button").addEventListener("click", (e)=>{ phaseHelp.classList.toggle("open"); });
document.addEventListener("click", (e)=>{ if (!phaseHelp.contains(e.target)) phaseHelp.classList.remove("open"); });
// Canvas sizing
const tsCanvas = el("ts"), tsCtx = tsCanvas.getContext("2d");
const phCanvas = el("phase"), phCtx = phCanvas.getContext("2d");
function resizeCanvases() {
const setSize = (cv, minW=720, minH=340) => {
const rect = cv.getBoundingClientRect();
cv.width = Math.max(minW, Math.round(rect.width * devicePixelRatio));
cv.height = Math.max(minH, Math.round(rect.height * devicePixelRatio));
};
setSize(tsCanvas, 760, 360); setSize(phCanvas, 760, 360);
drawAll();
}
window.addEventListener("resize", resizeCanvases, {passive:true});
// Sliders
function syncVal(id){
const v = parseFloat(el(id).value); state[id] = v;
const out = el(id+"_val");
if (out){
let txt = v;
if (id==="K" || id==="T") txt = v.toFixed(0);
else if (id==="s") txt = v.toFixed(3);
else if (id==="dt") txt = v.toFixed(4);
else if (id==="tau") txt = v.toFixed(1);
else txt = v.toFixed(2);
out.textContent = txt;
}
}
sliders.forEach(id => {
syncVal(id);
el(id).addEventListener("input", () => { syncVal(id); recompute(); });
});
// Presets
el("preset-stable").addEventListener("click",()=>{ setParams({r:1, K:1, m:1, s:0.02, Y:0.6, tau:0.5, x0:0.1, y0:0.1, dt:0.01, T:1200}); });
el("preset-osc").addEventListener("click",()=>{ setParams({r:1, K:1, m:1, s:0.02, Y:0.6, tau:85, x0:0.1, y0:0.1, dt:0.01, T:1200}); });
el("preset-chaos").addEventListener("click",()=>{ setParams({r:1, K:1, m:1, s:0.02, Y:0.6, tau:90, x0:0.1, y0:0.1, dt:0.01, T:1200}); });
function setParams(map){
for (const k in map){
if (el(k)) el(k).value = map[k];
state[k] = map[k];
const out = el(k+"_val"); if (out) {
let v = map[k];
if (k==="K" || k==="T") v = (+v).toFixed(0);
else if (k==="s") v = (+v).toFixed(3);
else if (k==="dt") v = (+v).toFixed(4);
else if (k==="tau") v = (+v).toFixed(1);
else v = (+v).toFixed(2);
out.textContent = v;
}
}
recompute();
}
// Simulation
function recompute(){
const r=state.r, K=state.K, m=state.m, s=state.s, Y=state.Y, tau=state.tau;
const x0=state.x0, y0=state.y0, dt=state.dt, T=state.T;
const N = Math.max(2, Math.floor(T/dt) + 1);
const d = Math.max(0, Math.round(tau/dt));
xs = new Array(N + d); ys = new Array(N + d); ts = new Array(N + d);
for (let i=0; i<d; i++){ xs[i] = x0; ys[i] = y0; ts[i] = (i - d) * dt; }
xs[d] = x0; ys[d] = y0; ts[d] = 0;
const gainFactor = Y * Math.exp(-s * tau) * m;
for (let n=d; n<d+N-1; n++){
const x = xs[n], y = ys[n];
const xd = xs[n - d], yd = ys[n - d];
const dx = r * x * (1 - x / K) - m * x * y;
const dy = -s * y + gainFactor * xd * yd;
xs[n+1] = Math.max(0, x + dt * dx);
ys[n+1] = Math.max(0, y + dt * dy);
ts[n+1] = ts[n] + dt;
}
if (d>0){ xs = xs.slice(d); ys = ys.slice(d); ts = ts.slice(d); }
drawAll();
}
// Plotting
function autoscale(arr, pad=0.12){
let min = Infinity, max = -Infinity;
for (let v of arr){ if (Number.isFinite(v)){ if (v<min) min=v; if (v>max) max=v; } }
if (!Number.isFinite(min) || !Number.isFinite(max)){ min=0; max=1; }
if (Math.abs(max-min) < 1e-9){ max = min + 1; }
const span = max-min;
return { min: Math.max(0, min - pad*span), max: max + pad*span };
}
function ticks(min, max, approx=6){
const span = max - min;
const raw = span / Math.max(1, approx);
const mag = Math.pow(10, Math.floor(Math.log10(raw)));
const norm = raw / mag;
let step = 1*mag;
if (norm > 5) step = 10*mag;
else if (norm > 2) step = 5*mag;
else if (norm > 1) step = 2*mag;
const start = Math.ceil(min / step) * step;
const arr = [];
for (let v = start; v <= max + 1e-9; v += step) arr.push(v);
return arr;
}
function formatTick(v){
const a = Math.abs(v);
if (a >= 1000 || (a < 1e-3 && a > 0)) return v.toExponential(0);
if (a < 1) return v.toFixed(2);
if (a < 10) return v.toFixed(1);
return v.toFixed(0);
}
function drawAxes(ctx, W, H, x0, y0, x1, y1, xTicks, yTicks, xLabel, yLabel){
const left = 60, right = 22, top = 20, bottom = 48;
ctx.clearRect(0,0,W,H);
ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue("--grid").trim();
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(left, top); ctx.lineTo(left, H-bottom); ctx.lineTo(W-right, H-bottom); ctx.stroke();
const sx = v => left + (W - left - right) * (v - x0) / (x1 - x0 + 1e-12);
const sy = v => (H - bottom) - (H - top - bottom) * (v - y0) / (y1 - y0 + 1e-12);
ctx.fillStyle = "#5b6771"; ctx.font = "13px system-ui, sans-serif";
ctx.textAlign = "center"; ctx.textBaseline = "top";
xTicks.forEach(t => {
const x = sx(t);
ctx.beginPath(); ctx.moveTo(x, H-bottom); ctx.lineTo(x, H-bottom+5); ctx.stroke();
ctx.fillText(formatTick(t), x, H-bottom+10);
});
ctx.textAlign = "right"; ctx.textBaseline = "middle";
yTicks.forEach(t => {
const y = sy(t);
ctx.beginPath(); ctx.moveTo(left-5, y); ctx.lineTo(left, y); ctx.stroke();
ctx.fillText(formatTick(t), left-8, y);
});
ctx.textAlign = "center"; ctx.textBaseline = "bottom";
ctx.fillText(xLabel, left + (W-left-right)/2, H - 8);
ctx.save(); ctx.translate(18, H/2); ctx.rotate(-Math.PI/2);
ctx.textAlign = "center"; ctx.textBaseline = "top"; ctx.fillText(yLabel, 0, 0);
ctx.restore();
return { sx, sy };
}
function drawTimeSeries(){
const ctx = tsCtx, W = tsCanvas.width, H = tsCanvas.height;
const tmin = 0, tmax = ts.length ? ts[ts.length-1] : state.T;
const yr = autoscale([...xs, ...ys]);
const xTicks = ticks(tmin, tmax, 7);
const yTicks = ticks(yr.min, yr.max, 6);
const { sx, sy } = drawAxes(ctx, W, H, tmin, yr.min, tmax, yr.max, xTicks, yTicks, "time", "population");
ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue("--prey").trim();
ctx.lineWidth = 1.8; ctx.beginPath();
for (let i=0;i<xs.length;i++){ const x=sx(ts[i]), y=sy(xs[i]); if (i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); }
ctx.stroke();
ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue("--pred").trim();
ctx.lineWidth = 1.8; ctx.beginPath();
for (let i=0;i<ys.length;i++){ const x=sx(ts[i]), y=sy(ys[i]); if (i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); }
ctx.stroke();
}
function drawPhase(){
const ctx = phCtx, W = phCanvas.width, H = phCanvas.height;
const xr = autoscale(xs), yr = autoscale(ys);
const span = Math.max(xr.max - xr.min, yr.max - yr.min);
const xmid = (xr.max + xr.min)/2, ymid = (yr.max + yr.min)/2;
const x0 = Math.max(0, xmid - span/2), x1 = xmid + span/2;
const y0 = Math.max(0, ymid - span/2), y1 = ymid + span/2;
const xTicks = ticks(x0, x1, 6);
const yTicks = ticks(y0, y1, 6);
const { sx, sy } = drawAxes(ctx, W, H, x0, y0, x1, y1, xTicks, yTicks, "prey x", "predator y");
ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue("--phase").trim();
ctx.lineWidth = 1.7; ctx.beginPath();
for (let i=0;i<xs.length;i++){ const x=sx(xs[i]); const y=sy(ys[i]); if (i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); }
ctx.stroke();
}
function drawAll(){ drawTimeSeries(); drawPhase(); }
// Export
el("export").addEventListener("click", () => {
let csv = "t,x,y\n";
for (let i=0;i<ts.length;i++) csv += `${ts[i]},${xs[i]},${ys[i]}\n`;
const blob = new Blob([csv], {type:"text/csv"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = "pred_prey_delay.csv";
document.body.appendChild(a); a.click();
setTimeout(()=>{ URL.revokeObjectURL(url); a.remove(); },0);
});
// Initial boot
resizeCanvases();
recompute();
})();
</script>
</body>
</html>
Preset choices (from the paper)
- Stable: with $r=1,K=1,m=1,Y=0.6,s=0.02$, the coexistence equilibrium is asymptotically stable at $\tau = 0$ (and again for large $\tau$ after the second Hopf).
- Oscillatory: same parameters with $\tau$ between the two Hopf points (e.g. $\tau\approx 85$) gives sustained periodic orbits; the paper’s time-series examples include $\tau=82,85,86.3,86.8$.
- Chaotic: $\tau=90$ shows a strange attractor and chaotic return map.
If you want any other palettes (e.g., a dark mode toggle) or overlays (like marking the coexistence equilibrium when it exists), I can wire those in too.