From f0d207c4d5202384933430e57436a2dc956add31 Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Sat, 20 Jun 2026 14:03:43 +0200 Subject: [PATCH] feat(model): make limits-to-growth a runnable two-flow logistic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recast the S-curve as a Reinforcing inflow plus a crowding-driven die-off that grows with Yeast², using only the existing proportional rule. Yeast now climbs 20 → ~1000 as a true sigmoid, and the detector still classifies it R + B — the balancing loop stays visible, which a single "logistic" rule would have hidden. Drops the carrying-capacity converter (a faithful one needs a divide rule). --- src/model/samples.ts | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/src/model/samples.ts b/src/model/samples.ts index 8c9136a..4321c97 100644 --- a/src/model/samples.ts +++ b/src/model/samples.ts @@ -15,9 +15,9 @@ * Beyond that primer, three classic models go a step further — each adds one * structure the first four never show, so they read as a second tier: * - * 5. Limits to growth — two loops (R and B) fighting over a single Flow, plus a - * constant Converter (carrying capacity) that feeds a loop - * without being part of it. + * 5. Limits to growth — a Reinforcing inflow and a Balancing outflow on one + * Stock, with a Converter (crowding) relaying the density + * that brakes growth: the S-curve. * 6. Predator and prey — two coupled Stocks whose interlocking loops oscillate. * 7. Epidemic — a chain of Stocks joined by Stock→Stock Flows: no clouds. * @@ -192,30 +192,41 @@ function population(): Model { } /** - * Limits to growth — the S-curve, and the first model where two loops fight over - * one Flow. Yeast multiplies the more there is of it (Yeast → [+] → growth: a - * Reinforcing loop), but the fuller the vat the more crowding holds growth back - * (Yeast → [+] → crowding → [−] → growth: a Balancing loop). Carrying capacity is - * a *constant* Converter — no inputs — that sets how soon crowding bites; it feeds - * the balancing loop without sitting on any cycle. + * Limits to growth — the S-curve, where a Reinforcing engine meets a Balancing + * brake. Yeast multiplies the more there is of it (Yeast → [+] → growth: a + * Reinforcing inflow), but crowding rises with the population (Yeast → [+] → + * crowding) and drives a die-off that grows with the *square* of the Yeast + * (Yeast, crowding → [+] → die-off → drains Yeast: a Balancing outflow). Growth + * wins early, the die-off wins late, so Yeast settles where they balance (≈1000) + * — the classic sigmoid, with *both* loops visible to the detector. (A named + * "carrying capacity" would want a divide rule we don't have yet; here the ceiling + * falls out of the growth and die-off rates.) */ function limitsToGrowth(): Model { const source = makeCloud({ x: -280, y: 0 }) const yeast = makeStock({ x: 40, y: 0 }, "Yeast") + yeast.initialValue = 20 const growth = makeFlow(midpoint(source.position, yeast.position), "growth", source.id, yeast.id) - // crowding rides above the pipe; carrying capacity stacks above the Source on the - // left, so the `capacity → crowding` link is a clean horizontal hop along the top. - const capacity = makeConverter({ x: -280, y: -160 }, "carrying capacity") - const crowding = makeConverter({ x: -40, y: -160 }, "crowding") + // growth = 30% of Yeast (its `+` input): the Reinforcing engine. + growth.rule = { kind: "proportional", factor: 0.3 } + const sink = makeCloud({ x: 360, y: 0 }) + const dieOff = makeFlow(midpoint(yeast.position, sink.position), "die-off", yeast.id, sink.id) + // die-off = factor × Yeast × crowding. With crowding ∝ Yeast it scales as Yeast², + // so the Balancing drain overtakes the linear growth and Yeast plateaus. + dieOff.rule = { kind: "proportional", factor: 0.0003 } + // crowding ≈ the population density (proportional to Yeast), what drives the die-off. + const crowding = makeConverter({ x: 200, y: -160 }, "crowding") + crowding.rule = { kind: "proportional", factor: 1 } return model( "Limits to growth", - [source, yeast, growth, crowding, capacity], + [source, yeast, growth, sink, dieOff, crowding], [ link(yeast, growth, "+"), link(yeast, crowding, "+"), - link(crowding, growth, "-"), - link(capacity, crowding, "-"), + link(yeast, dieOff, "+"), + link(crowding, dieOff, "+"), ], + { start: 0, stop: 40, dt: 1 }, ) }