From 62f6c5cda1f25c4c3e593445a14ef3cbebeb49a4 Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Sun, 21 Jun 2026 20:06:35 +0200 Subject: [PATCH] feat(samples): add "World on a warming planet" capstone A miniature of the Club of Rome's World3 with its pollution sector reframed as a climate channel: four coupled Stocks where Reinforcing capital and population overshoot a finite Resource and the carbon they burn locks in the warming that finishes them. A qualitative tribute to the overshoot-and-collapse shape on the existing four rules, not a port of the equations. --- src/model/samples.ts | 274 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 273 insertions(+), 1 deletion(-) diff --git a/src/model/samples.ts b/src/model/samples.ts index fd89ee5..8dcb4c3 100644 --- a/src/model/samples.ts +++ b/src/model/samples.ts @@ -64,6 +64,21 @@ * threshold the smooth rules can't draw — so here the * ceiling is *declared*, not emergent. * + * And the capstone — every piece at once, at the scale the whole gallery points toward: + * + * 16. World on a warming planet — the Club of Rome's World3 (Meadows et al., *The Limits + * to Growth*) in miniature, its pollution sector reframed + * as a climate channel: four coupled Stocks where two + * Reinforcing engines (capital, population) overshoot a + * finite Resource and the carbon they burn locks in the + * warming that finishes them. The gallery's largest model, + * it composes the whole vocabulary — a Stock→Stock bridge + * ("Epidemic"), logistic-style limits ("Limits to growth"), + * a renewable-resource overshoot ("Overshoot and collapse"), + * and the bathtub's slow sink ("Bathtub with an overflow") + * — into one system. A qualitative tribute to the shape, not + * a port of the equations. + * * These are plain data built from the same tested constructors the store uses * (factory.ts), so every sample is a valid Model by construction. `build()` * mints fresh ids on each call, so loading a sample twice never collides. @@ -1425,6 +1440,256 @@ function bathtubOverflow(): Model { ) } +/** + * World on a warming planet — the gallery's capstone: the Club of Rome's World3 + * (Donella Meadows et al., *The Limits to Growth*, 1972) in miniature, with its + * persistent-pollution sector reframed as the climate channel. It is a *qualitative + * tribute*, not a port: World3 runs on hundreds of equations, table-function + * nonlinearities, and delays; here four Stocks on the gallery's four rules + * reproduce the famous *shape* — overshoot and collapse — without the apparatus. + * + * The four sectors and how they couple: + * - **Capital** is the growth engine. output = factor × Capital × availability; + * a slice is reinvested (investment, an inflow) so Capital → output → investment + * → Capital is **Reinforcing** — the economy compounds. depreciation drains it + * (**Balancing**). + * - **Resources** is the finite planet: a nonrenewable Stock with *no* inflow. + * `availability` ∝ Resources scales output, so as the reserve runs down the + * economy is choked: Resources → availability → output → combustion → Resources, + * one `−` → **Balancing**. This is the World3 limit. + * - **Atmospheric carbon** is the climate channel. The same act that runs the + * economy loads the sky: `combustion` is a single Stock→Stock Flow that drains + * Resources straight *into* carbon — burning the reserve *is* the emission. A + * slow `removal` (a Gap toward preindustrial) is the sink; tuned slow, so the + * carbon **stays** — the bathtub, the lesson that stabilising emissions ≠ + * stabilising concentration. + * - **Population** grows on births (**Reinforcing**) against baseline deaths + * (**Balancing**). `warming` (a Gap reading carbon above the same preindustrial + * baseline — the constant does double duty) drives `heat deaths`, the climate's + * bite on people, and `climate damage`, its bite on Capital. That damage closes + * the long cross-sector loop output → combustion → carbon → warming → climate + * damage → Capital — so the climate is genuine *feedback*, not one-way forcing. + * + * Note what is *absent*: there is no Reinforcing tipping loop. The climate here is + * slow accumulation braked by its sink, not a runaway — faithful to World3's + * pollution sector, and a different model from a carbon-cycle one built to tip. + * The Reinforcing engines are growth (Capital) and births (Population); everything + * the planet pushes back with is Balancing. That asymmetry *is* limits-to-growth. + * + * What emerges (start 0, stop 250, dt 1): the economy booms, then overshoots the + * reserve and collapses — output peaks ≈540 at t≈47, Capital ≈1040 at t≈60, both + * falling to near nothing as Resources deplete 1000 → ~120. The carbon they burned + * climbs to ~1085 ppm and *holds* near 1000 (warming locked at ~4 °C) long after + * the emissions stop. That locked-in heat is what finishes Population: it overshoots + * to ~310 at t≈60 — the *last* sector to peak — then collapses to ~30, a third of + * where it began. Growth first, people last; the limit and the heat together. + */ +function worldOnAWarmingPlanet(): Model { + // Economy upper-left (Resources, Capital and their converters/flows); the carbon + // Stock sits centre on the same baseline as Resources, joined by the combustion + // bridge; `warming` hangs below it and radiates harm out to *both* the economy + // (up-left, climate damage) and Population (right, heat deaths). Population runs + // its own birth/death column on the right. Valves are hand-placed, not at + // midpoints, to keep every Information Link in open space. + + // Resources sector (far left): a finite reserve that only depletes; its grade + // (availability) is what the economy can actually draw on. + const resources = makeStock({ x: -820, y: 60 }, "Resources") + resources.initialValue = 1000 + resources.description = + "The planet's nonrenewable reserve — it has no inflow, so it only ever falls. As it runs down it chokes the economy that lives off it." + const availability = makeConverter({ x: -820, y: -140 }, "availability") + // availability = 0.001 × Resources: a 0…1 grade (starts at 1.0) that scales output. + availability.rule = { kind: "proportional", factor: 0.001 } + availability.description = + "How much of the reserve is still cheap to reach (∝ Resources, ~1.0 at the start, → 0 as it empties). It is the brake the finite planet puts on output." + + // Economy: Capital is the hub — investment fills it (Reinforcing), depreciation + // and climate damage drain it (Balancing). output relays Capital × availability. + const capital = makeStock({ x: -420, y: -40 }, "Capital") + capital.initialValue = 50 + capital.description = + "Industrial capital — the engine that compounds by reinvesting its own output, until the reserve it burns runs short and the heat it raises bites back." + const output = makeConverter({ x: -600, y: -200 }, "output") + // output = Capital × availability (both `+`): the economy's activity, throttled by + // how much reserve is left. It feeds investment, combustion, and (via carbon) warming. + output.rule = { kind: "proportional", factor: 1 } + output.description = + "Industrial output, Capital × availability: more capital makes more, but a depleting reserve scales it down. The relay that ties the economy to the planet." + const investSrc = makeCloud({ x: -680, y: -40 }) + const investment = makeFlow({ x: -550, y: -40 }, "investment", investSrc.id, capital.id) + // investment = 12% of output, reinvested: Capital → output → investment → Capital, + // no `−` → the Reinforcing engine that drives the boom. + investment.rule = { kind: "proportional", factor: 0.12 } + investment.description = + "Output ploughed back into capital, 12% per step. More capital → more output → more investment: the Reinforcing engine of growth." + const deprSink = makeCloud({ x: -240, y: -200 }) + const depreciation = makeFlow({ x: -330, y: -120 }, "depreciation", capital.id, deprSink.id) + // depreciation = 4% of Capital: wear. Below investment while the reserve lasts, + // above it once availability collapses — which is what tips Capital into decline. + depreciation.rule = { kind: "proportional", factor: 0.04 } + depreciation.description = + "Capital wearing out, 4% per step — the Balancing drain that overtakes investment once a depleted reserve starves output." + const dmgSink = makeCloud({ x: -640, y: -260 }) + const climateDamage = makeFlow({ x: -540, y: -160 }, "climate damage", capital.id, dmgSink.id) + // climate damage = 0.004 × Capital × warming: the heat's bite on the economy. It + // closes the long climate→economy loop, so warming is feedback, not just forcing. + climateDamage.rule = { kind: "proportional", factor: 0.004 } + climateDamage.description = + "Capital lost to a hotter world, ∝ Capital × warming. It closes the loop from output through carbon and warming back onto Capital — the climate biting the economy that warmed it." + + // The combustion bridge: a single Stock→Stock Flow draining Resources *into* + // carbon. Burning the reserve is the emission — no Source, no Sink, both ends Stocks. + const carbon = makeStock({ x: 40, y: 60 }, "Atmospheric carbon") + carbon.initialValue = 280 + carbon.unit = "ppm" + carbon.description = + "Carbon in the air (ppm), 280 at the preindustrial start. Combustion fills it fast, the sink empties it slow — so it climbs, then stays. The bathtub." + const combustion = makeFlow({ x: -380, y: 60 }, "combustion", resources.id, carbon.id) + // combustion = 3% of output: the reserve burned each step, drained from Resources + // and added to carbon in one move. Conserved — what leaves the ground enters the sky. + combustion.rule = { kind: "proportional", factor: 0.03 } + combustion.description = + "The reserve burned to run the economy, ∝ output — drained from Resources straight into the air. The same act depletes the planet and loads the sky." + const removalSink = makeCloud({ x: 40, y: -140 }) + const removal = makeFlow({ x: 40, y: -40 }, "removal", carbon.id, removalSink.id) + // removal = 0.001 × (carbon − preindustrial): the natural sink, a Gap toward the + // baseline. Deliberately slow, so carbon barely recedes — the locked-in warming. + removal.rule = { kind: "gap", factor: 0.001 } + removal.description = + "Nature drawing carbon back toward the preindustrial baseline — a slow Gap. So slow that once emitted, the carbon stays for the run: warming you cannot take back." + const preindustrial = makeConverter({ x: -180, y: 240 }, "preindustrial") + preindustrial.rule = { kind: "constant", value: 280 } + preindustrial.description = + "The preindustrial carbon baseline (280 ppm), doing double duty: the target the sink draws toward and the zero from which warming is measured." + + const warming = makeConverter({ x: 300, y: 200 }, "warming") + // warming = 0.005 × (carbon − preindustrial): °C above preindustrial. A Gap reading + // carbon as the level and the baseline as the target; ~4 °C at the carbon peak. + warming.rule = { kind: "gap", factor: 0.005 } + warming.description = + "Warming in °C above preindustrial, ∝ (carbon − baseline) — about 4 °C at the peak. It feeds both the deaths it causes and the capital it destroys." + + // Population: births compound it (Reinforcing), baseline deaths brake it (Balancing), + // and heat deaths add the climate toll that eventually overwhelms the birth engine. + const population = makeStock({ x: 660, y: 60 }, "Population") + population.initialValue = 100 + population.unit = "people" + population.description = + "The people — grows on births, thinned by ordinary deaths, and finally overwhelmed by the heat the economy's carbon locked in. The last sector to peak, and to fall." + const birthSrc = makeCloud({ x: 660, y: -140 }) + const births = makeFlow({ x: 660, y: -40 }, "births", birthSrc.id, population.id) + // births = 5% of Population: the Reinforcing engine, +3%/step net of baseline deaths. + births.rule = { kind: "proportional", factor: 0.05 } + births.description = + "New people, 5% of the population each step — the Reinforcing engine that grows it while the world stays cool enough to bear it." + const deathSink = makeCloud({ x: 520, y: 240 }) + const deaths = makeFlow({ x: 600, y: 160 }, "deaths", population.id, deathSink.id) + // deaths = 2% of Population: ordinary mortality, the Balancing drain births outrun early. + deaths.rule = { kind: "proportional", factor: 0.02 } + deaths.description = + "Ordinary deaths, 2% of the population each step — the Balancing drain the birth engine outpaces, until the heat tips the balance." + const heatSink = makeCloud({ x: 820, y: 240 }) + const heatDeaths = makeFlow({ x: 740, y: 160 }, "heat deaths", population.id, heatSink.id) + // heat deaths = 0.011 × Population × warming: the climate toll. Once warming passes + // ≈2.7 °C this outflow overtakes net births and Population turns from boom to collapse. + heatDeaths.rule = { kind: "proportional", factor: 0.011 } + heatDeaths.description = + "Deaths from a hotter world, ∝ Population × warming. Once warming passes ≈2.7 °C this overtakes net births, and the population collapses with the heat that never lifts." + + return model( + "World on a warming planet", + [ + resources, + availability, + capital, + output, + investSrc, + investment, + deprSink, + depreciation, + dmgSink, + climateDamage, + carbon, + combustion, + removalSink, + removal, + preindustrial, + warming, + population, + birthSrc, + births, + deathSink, + deaths, + heatSink, + heatDeaths, + ], + [ + link(resources, availability, "+", "More reserve left → more of it cheap to reach."), + link(capital, output, "+", "More capital → more output: the level the economy compounds on."), + link( + availability, + output, + "+", + "More availability → more output. With Capital, output = Capital × availability — the planet throttling the economy.", + ), + link( + output, + investment, + "+", + "More output → more reinvested: the + that makes the growth loop Reinforcing.", + ), + link(capital, depreciation, "+", "More capital → more wearing out each step."), + link(capital, climateDamage, "+", "More capital → more of it exposed to a hotter world."), + link( + warming, + climateDamage, + "+", + "More warming → more capital destroyed. This − outflow closes the climate→economy loop: feedback, not forcing.", + ), + link( + output, + combustion, + "+", + "More output → more reserve burned: economic activity is what emits.", + ), + link(carbon, removal, "+", "More carbon above the baseline → faster (but slow) removal."), + link( + preindustrial, + removal, + "-", + "The baseline the sink draws toward (the − target): removal stops once carbon is back to it.", + ), + link(carbon, warming, "+", "More carbon → more warming: carbon is the level in the gap."), + link( + preindustrial, + warming, + "-", + "The baseline warming is measured from (the − target): no carbon above it, no warming.", + ), + link( + population, + births, + "+", + "More people → more births: the + that makes the population loop Reinforcing.", + ), + link(population, deaths, "+", "More people → more ordinary deaths."), + link(population, heatDeaths, "+", "More people → more of them exposed to the heat."), + link( + warming, + heatDeaths, + "+", + "More warming → more heat deaths. With Population this is the climate toll that ends the boom.", + ), + ], + // The economy overshoots the reserve and collapses (output peaks ≈540 @ t≈47, + // Capital ≈1040 @ t≈60 → ~1); Resources deplete 1000 → ~120. The carbon burned + // climbs to ~1085 ppm and holds near 1000 (warming locked at ~4 °C). Population + // overshoots last (~310 @ t≈60) then collapses to ~30 as the locked-in heat bites. + { start: 0, stop: 250, dt: 1 }, + ) +} + /** The gallery, ordered simplest first. */ export const SAMPLES: Sample[] = [ { title: "Bathtub", blurb: "A stock filled and drained — no feedback yet.", build: bathtub }, @@ -1495,7 +1760,14 @@ export const SAMPLES: Sample[] = [ }, { title: "Bathtub with an overflow", - blurb: "A flat-out tap and a spillway: the excess spills to a second Stock and the level holds.", + blurb: + "A flat-out tap and a spillway: the excess spills to a second Stock and the level holds.", build: bathtubOverflow, }, + { + title: "World on a warming planet", + blurb: + "The Club of Rome's World3 in miniature, with a climate channel: growth overshoots a finite planet and the carbon it burns locks in the heat that finishes it.", + build: worldOnAWarmingPlanet, + }, ]