feat(model): make limits-to-growth a runnable two-flow logistic
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).
This commit is contained in:
@@ -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 },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user