feat(model): equip the sample gallery with runnable values
Give 10 of 11 samples initial values, rules, and a run window so they simulate on load: Bathtub (linear), Savings/Escalation/Fixes-that-fail (Reinforcing), Coffee (Balancing), Population, Epidemic (SIR), Tragedy (overshoot/collapse), Drift (eroding goal), Predator-prey (damped). Limits to growth is left diagram-only — its S-curve needs a saturating rule the vocabulary lacks.
This commit is contained in:
@@ -44,6 +44,7 @@ import {
|
|||||||
MODEL_VERSION,
|
MODEL_VERSION,
|
||||||
type ModelNode,
|
type ModelNode,
|
||||||
type Polarity,
|
type Polarity,
|
||||||
|
type SimSpec,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
|
|
||||||
/** A loadable example: a title and one-line blurb for the menu, plus a builder. */
|
/** A loadable example: a title and one-line blurb for the menu, plus a builder. */
|
||||||
@@ -58,8 +59,13 @@ function link(source: ModelNode, target: ModelNode, polarity: Polarity): Informa
|
|||||||
return { id: newId("link"), source: source.id, target: target.id, polarity }
|
return { id: newId("link"), source: source.id, target: target.id, polarity }
|
||||||
}
|
}
|
||||||
|
|
||||||
function model(name: string, nodes: ModelNode[], infoLinks: InformationLink[]): Model {
|
function model(
|
||||||
return { version: MODEL_VERSION, id: newId("model"), name, nodes, infoLinks }
|
name: string,
|
||||||
|
nodes: ModelNode[],
|
||||||
|
infoLinks: InformationLink[],
|
||||||
|
sim?: SimSpec,
|
||||||
|
): Model {
|
||||||
|
return { version: MODEL_VERSION, id: newId("model"), name, nodes, infoLinks, sim }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,6 +76,7 @@ function model(name: string, nodes: ModelNode[], infoLinks: InformationLink[]):
|
|||||||
function bathtub(): Model {
|
function bathtub(): Model {
|
||||||
const source = makeCloud({ x: -280, y: 0 })
|
const source = makeCloud({ x: -280, y: 0 })
|
||||||
const water = makeStock({ x: 0, y: 0 }, "Water")
|
const water = makeStock({ x: 0, y: 0 }, "Water")
|
||||||
|
water.initialValue = 20
|
||||||
const sink = makeCloud({ x: 280, y: 0 })
|
const sink = makeCloud({ x: 280, y: 0 })
|
||||||
const filling = makeFlow(
|
const filling = makeFlow(
|
||||||
midpoint(source.position, water.position),
|
midpoint(source.position, water.position),
|
||||||
@@ -78,7 +85,15 @@ function bathtub(): Model {
|
|||||||
water.id,
|
water.id,
|
||||||
)
|
)
|
||||||
const emptying = makeFlow(midpoint(water.position, sink.position), "emptying", water.id, sink.id)
|
const emptying = makeFlow(midpoint(water.position, sink.position), "emptying", water.id, sink.id)
|
||||||
return model("Bathtub", [source, water, sink, filling, emptying], [])
|
// No Information Links, so each rate is a plain Constant. A faster inflow than
|
||||||
|
// outflow means Water rises in a straight line — accumulation with no feedback.
|
||||||
|
filling.rule = { kind: "constant", value: 5 }
|
||||||
|
emptying.rule = { kind: "constant", value: 3 }
|
||||||
|
return model("Bathtub", [source, water, sink, filling, emptying], [], {
|
||||||
|
start: 0,
|
||||||
|
stop: 40,
|
||||||
|
dt: 1,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,13 +107,21 @@ function savings(): Model {
|
|||||||
// visible Reinforcing loop instead of overlapping the inflow pipe.
|
// visible Reinforcing loop instead of overlapping the inflow pipe.
|
||||||
const source = makeCloud({ x: -240, y: -80 })
|
const source = makeCloud({ x: -240, y: -80 })
|
||||||
const balance = makeStock({ x: 120, y: 40 }, "Balance")
|
const balance = makeStock({ x: 120, y: 40 }, "Balance")
|
||||||
|
balance.initialValue = 1000
|
||||||
const interest = makeFlow(
|
const interest = makeFlow(
|
||||||
midpoint(source.position, balance.position),
|
midpoint(source.position, balance.position),
|
||||||
"interest",
|
"interest",
|
||||||
source.id,
|
source.id,
|
||||||
balance.id,
|
balance.id,
|
||||||
)
|
)
|
||||||
return model("Savings account", [source, balance, interest], [link(balance, interest, "+")])
|
// interest = 5% × Balance (the `+` link). A Stock feeding its own inflow → the
|
||||||
|
// Reinforcing loop runs as exponential growth.
|
||||||
|
interest.rule = { kind: "proportional", factor: 0.05 }
|
||||||
|
return model("Savings account", [source, balance, interest], [link(balance, interest, "+")], {
|
||||||
|
start: 0,
|
||||||
|
stop: 40,
|
||||||
|
dt: 1,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,13 +132,19 @@ function savings(): Model {
|
|||||||
*/
|
*/
|
||||||
function coffee(): Model {
|
function coffee(): Model {
|
||||||
const coffee = makeStock({ x: -200, y: 0 }, "Coffee")
|
const coffee = makeStock({ x: -200, y: 0 }, "Coffee")
|
||||||
|
coffee.initialValue = 90
|
||||||
const sink = makeCloud({ x: 200, y: 0 })
|
const sink = makeCloud({ x: 200, y: 0 })
|
||||||
const cooling = makeFlow(midpoint(coffee.position, sink.position), "cooling", coffee.id, sink.id)
|
const cooling = makeFlow(midpoint(coffee.position, sink.position), "cooling", coffee.id, sink.id)
|
||||||
|
// cooling = 0.1 × (Coffee − room): the `+` input is the level, the `−` the target.
|
||||||
|
// An outflow closing the gap to room temperature → the Balancing loop settles there.
|
||||||
|
cooling.rule = { kind: "gap", factor: 0.1 }
|
||||||
const room = makeConverter({ x: 0, y: -160 }, "room temperature")
|
const room = makeConverter({ x: 0, y: -160 }, "room temperature")
|
||||||
|
room.rule = { kind: "constant", value: 20 }
|
||||||
return model(
|
return model(
|
||||||
"Coffee cooling",
|
"Coffee cooling",
|
||||||
[coffee, sink, cooling, room],
|
[coffee, sink, cooling, room],
|
||||||
[link(coffee, cooling, "+"), link(room, cooling, "-")],
|
[link(coffee, cooling, "+"), link(room, cooling, "-")],
|
||||||
|
{ start: 0, stop: 60, dt: 1 },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,11 +161,23 @@ function population(): Model {
|
|||||||
// Valves are placed by hand, not at the midpoint, to hold the steps.
|
// Valves are placed by hand, not at the midpoint, to hold the steps.
|
||||||
const source = makeCloud({ x: -360, y: -240 })
|
const source = makeCloud({ x: -360, y: -240 })
|
||||||
const fertility = makeConverter({ x: -360, y: -40 }, "fertility")
|
const fertility = makeConverter({ x: -360, y: -40 }, "fertility")
|
||||||
|
fertility.rule = { kind: "constant", value: 0.03 }
|
||||||
const lifeExpectancy = makeConverter({ x: -360, y: 240 }, "life expectancy")
|
const lifeExpectancy = makeConverter({ x: -360, y: 240 }, "life expectancy")
|
||||||
|
// Wired into deaths for the Balancing loop's structure, but not yet read by the
|
||||||
|
// rate: a faithful "deaths = Population ÷ life expectancy" needs a divide rule we
|
||||||
|
// don't have, so deaths uses a flat mortality rate below. (See the gallery notes.)
|
||||||
|
lifeExpectancy.rule = { kind: "constant", value: 70 }
|
||||||
const people = makeStock({ x: 0, y: 0 }, "Population")
|
const people = makeStock({ x: 0, y: 0 }, "Population")
|
||||||
|
people.initialValue = 100
|
||||||
const births = makeFlow({ x: -160, y: -160 }, "births", source.id, people.id)
|
const births = makeFlow({ x: -160, y: -160 }, "births", source.id, people.id)
|
||||||
|
// births = fertility × Population (both `+` inputs): more people and higher
|
||||||
|
// fertility, more births — the Reinforcing engine.
|
||||||
|
births.rule = { kind: "proportional", factor: 1 }
|
||||||
const sink = makeCloud({ x: 360, y: 240 })
|
const sink = makeCloud({ x: 360, y: 240 })
|
||||||
const deaths = makeFlow({ x: 160, y: 160 }, "deaths", people.id, sink.id)
|
const deaths = makeFlow({ x: 160, y: 160 }, "deaths", people.id, sink.id)
|
||||||
|
// deaths = 2% of Population each step (its `+` input) — the Balancing drain. With
|
||||||
|
// births at 3%, the Reinforcing loop wins and the population grows exponentially.
|
||||||
|
deaths.rule = { kind: "proportional", factor: 0.02 }
|
||||||
return model(
|
return model(
|
||||||
"Population",
|
"Population",
|
||||||
[source, people, sink, births, deaths, fertility, lifeExpectancy],
|
[source, people, sink, births, deaths, fertility, lifeExpectancy],
|
||||||
@@ -146,6 +187,7 @@ function population(): Model {
|
|||||||
link(people, deaths, "+"),
|
link(people, deaths, "+"),
|
||||||
link(lifeExpectancy, deaths, "-"),
|
link(lifeExpectancy, deaths, "-"),
|
||||||
],
|
],
|
||||||
|
{ start: 0, stop: 100, dt: 1 },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +234,7 @@ function predatorPrey(): Model {
|
|||||||
// cross-stock loop traces a circuit through the open centre.
|
// cross-stock loop traces a circuit through the open centre.
|
||||||
const preySource = makeCloud({ x: -480, y: -140 })
|
const preySource = makeCloud({ x: -480, y: -140 })
|
||||||
const rabbits = makeStock({ x: -80, y: -140 }, "Rabbits")
|
const rabbits = makeStock({ x: -80, y: -140 }, "Rabbits")
|
||||||
|
rabbits.initialValue = 100
|
||||||
const preySink = makeCloud({ x: 320, y: -140 })
|
const preySink = makeCloud({ x: 320, y: -140 })
|
||||||
const rabbitBirths = makeFlow(
|
const rabbitBirths = makeFlow(
|
||||||
midpoint(preySource.position, rabbits.position),
|
midpoint(preySource.position, rabbits.position),
|
||||||
@@ -199,14 +242,19 @@ function predatorPrey(): Model {
|
|||||||
preySource.id,
|
preySource.id,
|
||||||
rabbits.id,
|
rabbits.id,
|
||||||
)
|
)
|
||||||
|
// rabbits breed in proportion to themselves (Reinforcing) …
|
||||||
|
rabbitBirths.rule = { kind: "proportional", factor: 0.08 }
|
||||||
const predation = makeFlow(
|
const predation = makeFlow(
|
||||||
midpoint(rabbits.position, preySink.position),
|
midpoint(rabbits.position, preySink.position),
|
||||||
"predation",
|
"predation",
|
||||||
rabbits.id,
|
rabbits.id,
|
||||||
preySink.id,
|
preySink.id,
|
||||||
)
|
)
|
||||||
|
// … and are thinned by predation = rabbits × foxes (both `+`): the coupling term.
|
||||||
|
predation.rule = { kind: "proportional", factor: 0.004 }
|
||||||
const foxSource = makeCloud({ x: -480, y: 140 })
|
const foxSource = makeCloud({ x: -480, y: 140 })
|
||||||
const foxes = makeStock({ x: -80, y: 140 }, "Foxes")
|
const foxes = makeStock({ x: -80, y: 140 }, "Foxes")
|
||||||
|
foxes.initialValue = 20
|
||||||
const foxSink = makeCloud({ x: 320, y: 140 })
|
const foxSink = makeCloud({ x: 320, y: 140 })
|
||||||
const foxBirths = makeFlow(
|
const foxBirths = makeFlow(
|
||||||
midpoint(foxSource.position, foxes.position),
|
midpoint(foxSource.position, foxes.position),
|
||||||
@@ -214,12 +262,17 @@ function predatorPrey(): Model {
|
|||||||
foxSource.id,
|
foxSource.id,
|
||||||
foxes.id,
|
foxes.id,
|
||||||
)
|
)
|
||||||
|
// foxes are born in proportion to the rabbits available to eat …
|
||||||
|
foxBirths.rule = { kind: "proportional", factor: 0.02 }
|
||||||
const foxDeaths = makeFlow(
|
const foxDeaths = makeFlow(
|
||||||
midpoint(foxes.position, foxSink.position),
|
midpoint(foxes.position, foxSink.position),
|
||||||
"fox deaths",
|
"fox deaths",
|
||||||
foxes.id,
|
foxes.id,
|
||||||
foxSink.id,
|
foxSink.id,
|
||||||
)
|
)
|
||||||
|
// … and die off on their own. The lag around the loop makes the two populations
|
||||||
|
// chase each other. (Forward Euler damps the orbit — see the gallery notes.)
|
||||||
|
foxDeaths.rule = { kind: "proportional", factor: 0.2 }
|
||||||
return model(
|
return model(
|
||||||
"Predator and prey",
|
"Predator and prey",
|
||||||
[
|
[
|
||||||
@@ -241,6 +294,7 @@ function predatorPrey(): Model {
|
|||||||
link(rabbits, foxBirths, "+"),
|
link(rabbits, foxBirths, "+"),
|
||||||
link(foxes, foxDeaths, "+"),
|
link(foxes, foxDeaths, "+"),
|
||||||
],
|
],
|
||||||
|
{ start: 0, stop: 120, dt: 0.25 },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,21 +309,32 @@ function predatorPrey(): Model {
|
|||||||
*/
|
*/
|
||||||
function epidemic(): Model {
|
function epidemic(): Model {
|
||||||
const susceptible = makeStock({ x: -280, y: 0 }, "Susceptible")
|
const susceptible = makeStock({ x: -280, y: 0 }, "Susceptible")
|
||||||
|
susceptible.initialValue = 990
|
||||||
const infected = makeStock({ x: 0, y: 0 }, "Infected")
|
const infected = makeStock({ x: 0, y: 0 }, "Infected")
|
||||||
|
infected.initialValue = 10
|
||||||
const recovered = makeStock({ x: 280, y: 0 }, "Recovered")
|
const recovered = makeStock({ x: 280, y: 0 }, "Recovered")
|
||||||
|
recovered.initialValue = 0
|
||||||
const infection = makeFlow(
|
const infection = makeFlow(
|
||||||
midpoint(susceptible.position, infected.position),
|
midpoint(susceptible.position, infected.position),
|
||||||
"infection",
|
"infection",
|
||||||
susceptible.id,
|
susceptible.id,
|
||||||
infected.id,
|
infected.id,
|
||||||
)
|
)
|
||||||
|
// infection = infectivity × Susceptible × Infected (proportional reads all three
|
||||||
|
// `+` inputs): the more carriers and the more susceptibles, the faster it spreads.
|
||||||
|
// The non-negative-stock floor keeps Susceptible from being over-drained.
|
||||||
|
infection.rule = { kind: "proportional", factor: 1 }
|
||||||
const recovery = makeFlow(
|
const recovery = makeFlow(
|
||||||
midpoint(infected.position, recovered.position),
|
midpoint(infected.position, recovered.position),
|
||||||
"recovery",
|
"recovery",
|
||||||
infected.id,
|
infected.id,
|
||||||
recovered.id,
|
recovered.id,
|
||||||
)
|
)
|
||||||
|
// recovery = 15% of the Infected each step (its one `+` input).
|
||||||
|
recovery.rule = { kind: "proportional", factor: 0.15 }
|
||||||
const infectivity = makeConverter({ x: -140, y: -160 }, "infectivity")
|
const infectivity = makeConverter({ x: -140, y: -160 }, "infectivity")
|
||||||
|
// Small, so infectivity × S × I stays a sane rate (R0 = infectivity·S₀/γ ≈ 2.6).
|
||||||
|
infectivity.rule = { kind: "constant", value: 0.0004 }
|
||||||
return model(
|
return model(
|
||||||
"Epidemic",
|
"Epidemic",
|
||||||
[susceptible, infected, recovered, infection, recovery, infectivity],
|
[susceptible, infected, recovered, infection, recovery, infectivity],
|
||||||
@@ -279,6 +344,7 @@ function epidemic(): Model {
|
|||||||
link(infected, recovery, "+"),
|
link(infected, recovery, "+"),
|
||||||
link(infectivity, infection, "+"),
|
link(infectivity, infection, "+"),
|
||||||
],
|
],
|
||||||
|
{ start: 0, stop: 60, dt: 1 },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,17 +361,22 @@ function epidemic(): Model {
|
|||||||
*/
|
*/
|
||||||
function tragedyOfTheCommons(): Model {
|
function tragedyOfTheCommons(): Model {
|
||||||
const pasture = makeStock({ x: 0, y: 0 }, "Pasture")
|
const pasture = makeStock({ x: 0, y: 0 }, "Pasture")
|
||||||
|
pasture.initialValue = 1000
|
||||||
// Two symmetric herds: cattle enter from a Source on the outside, grass leaves
|
// Two symmetric herds: cattle enter from a Source on the outside, grass leaves
|
||||||
// the Pasture downward to a Sink. The two `Pasture → growth` links are the weak
|
// the Pasture downward to a Sink. The two `Pasture → growth` links are the weak
|
||||||
// brake the trap overruns.
|
// brake the trap overruns.
|
||||||
const sourceA = makeCloud({ x: -640, y: 0 })
|
const sourceA = makeCloud({ x: -640, y: 0 })
|
||||||
const herdA = makeStock({ x: -360, y: 0 }, "Herd A")
|
const herdA = makeStock({ x: -360, y: 0 }, "Herd A")
|
||||||
|
herdA.initialValue = 10
|
||||||
const growthA = makeFlow(
|
const growthA = makeFlow(
|
||||||
midpoint(sourceA.position, herdA.position),
|
midpoint(sourceA.position, herdA.position),
|
||||||
"growth A",
|
"growth A",
|
||||||
sourceA.id,
|
sourceA.id,
|
||||||
herdA.id,
|
herdA.id,
|
||||||
)
|
)
|
||||||
|
// growth = herd × Pasture (both `+`): each herd grows the more cattle it has and
|
||||||
|
// the more grass is left — a Reinforcing loop, braked only by the shared Pasture.
|
||||||
|
growthA.rule = { kind: "proportional", factor: 0.0003 }
|
||||||
const sinkA = makeCloud({ x: -200, y: 240 })
|
const sinkA = makeCloud({ x: -200, y: 240 })
|
||||||
const grazingA = makeFlow(
|
const grazingA = makeFlow(
|
||||||
midpoint(pasture.position, sinkA.position),
|
midpoint(pasture.position, sinkA.position),
|
||||||
@@ -313,14 +384,19 @@ function tragedyOfTheCommons(): Model {
|
|||||||
pasture.id,
|
pasture.id,
|
||||||
sinkA.id,
|
sinkA.id,
|
||||||
)
|
)
|
||||||
|
// grazing = 6% of the herd, drained from the *shared* Pasture (which never
|
||||||
|
// regrows here): two appetites racing one stock down to bare dirt.
|
||||||
|
grazingA.rule = { kind: "proportional", factor: 0.06 }
|
||||||
const sourceB = makeCloud({ x: 640, y: 0 })
|
const sourceB = makeCloud({ x: 640, y: 0 })
|
||||||
const herdB = makeStock({ x: 360, y: 0 }, "Herd B")
|
const herdB = makeStock({ x: 360, y: 0 }, "Herd B")
|
||||||
|
herdB.initialValue = 10
|
||||||
const growthB = makeFlow(
|
const growthB = makeFlow(
|
||||||
midpoint(sourceB.position, herdB.position),
|
midpoint(sourceB.position, herdB.position),
|
||||||
"growth B",
|
"growth B",
|
||||||
sourceB.id,
|
sourceB.id,
|
||||||
herdB.id,
|
herdB.id,
|
||||||
)
|
)
|
||||||
|
growthB.rule = { kind: "proportional", factor: 0.0003 }
|
||||||
const sinkB = makeCloud({ x: 200, y: 240 })
|
const sinkB = makeCloud({ x: 200, y: 240 })
|
||||||
const grazingB = makeFlow(
|
const grazingB = makeFlow(
|
||||||
midpoint(pasture.position, sinkB.position),
|
midpoint(pasture.position, sinkB.position),
|
||||||
@@ -328,6 +404,7 @@ function tragedyOfTheCommons(): Model {
|
|||||||
pasture.id,
|
pasture.id,
|
||||||
sinkB.id,
|
sinkB.id,
|
||||||
)
|
)
|
||||||
|
grazingB.rule = { kind: "proportional", factor: 0.06 }
|
||||||
return model(
|
return model(
|
||||||
"Tragedy of the commons",
|
"Tragedy of the commons",
|
||||||
[pasture, sourceA, herdA, growthA, sinkA, grazingA, sourceB, herdB, growthB, sinkB, grazingB],
|
[pasture, sourceA, herdA, growthA, sinkA, grazingA, sourceB, herdB, growthB, sinkB, grazingB],
|
||||||
@@ -339,6 +416,7 @@ function tragedyOfTheCommons(): Model {
|
|||||||
link(herdB, grazingB, "+"),
|
link(herdB, grazingB, "+"),
|
||||||
link(pasture, growthB, "+"),
|
link(pasture, growthB, "+"),
|
||||||
],
|
],
|
||||||
|
{ start: 0, stop: 60, dt: 1 },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,24 +435,31 @@ function escalation(): Model {
|
|||||||
// cross there, where the R badge lands, so the whole loop reads at a glance.
|
// cross there, where the R badge lands, so the whole loop reads at a glance.
|
||||||
const blueSource = makeCloud({ x: -560, y: -120 })
|
const blueSource = makeCloud({ x: -560, y: -120 })
|
||||||
const blueArsenal = makeStock({ x: 280, y: -120 }, "Blue arsenal")
|
const blueArsenal = makeStock({ x: 280, y: -120 }, "Blue arsenal")
|
||||||
|
blueArsenal.initialValue = 10
|
||||||
const blueBuildup = makeFlow(
|
const blueBuildup = makeFlow(
|
||||||
midpoint(blueSource.position, blueArsenal.position),
|
midpoint(blueSource.position, blueArsenal.position),
|
||||||
"Blue buildup",
|
"Blue buildup",
|
||||||
blueSource.id,
|
blueSource.id,
|
||||||
blueArsenal.id,
|
blueArsenal.id,
|
||||||
)
|
)
|
||||||
|
// Each side builds in proportion to the other's arsenal (its one `+` input), so
|
||||||
|
// the two feed each other: a Reinforcing loop with no brake → unbounded growth.
|
||||||
|
blueBuildup.rule = { kind: "proportional", factor: 0.1 }
|
||||||
const redSource = makeCloud({ x: -560, y: 120 })
|
const redSource = makeCloud({ x: -560, y: 120 })
|
||||||
const redArsenal = makeStock({ x: 280, y: 120 }, "Red arsenal")
|
const redArsenal = makeStock({ x: 280, y: 120 }, "Red arsenal")
|
||||||
|
redArsenal.initialValue = 12
|
||||||
const redBuildup = makeFlow(
|
const redBuildup = makeFlow(
|
||||||
midpoint(redSource.position, redArsenal.position),
|
midpoint(redSource.position, redArsenal.position),
|
||||||
"Red buildup",
|
"Red buildup",
|
||||||
redSource.id,
|
redSource.id,
|
||||||
redArsenal.id,
|
redArsenal.id,
|
||||||
)
|
)
|
||||||
|
redBuildup.rule = { kind: "proportional", factor: 0.1 }
|
||||||
return model(
|
return model(
|
||||||
"Escalation",
|
"Escalation",
|
||||||
[blueSource, blueArsenal, blueBuildup, redSource, redArsenal, redBuildup],
|
[blueSource, blueArsenal, blueBuildup, redSource, redArsenal, redBuildup],
|
||||||
[link(blueArsenal, redBuildup, "+"), link(redArsenal, blueBuildup, "+")],
|
[link(blueArsenal, redBuildup, "+"), link(redArsenal, blueBuildup, "+")],
|
||||||
|
{ start: 0, stop: 40, dt: 1 },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,13 +481,22 @@ function fixesThatFail(): Model {
|
|||||||
// the diagonal back to Congestion. Placed by hand, not midpoint, to hold the column.
|
// the diagonal back to Congestion. Placed by hand, not midpoint, to hold the column.
|
||||||
const source = makeCloud({ x: -420, y: 120 })
|
const source = makeCloud({ x: -420, y: 120 })
|
||||||
const congestion = makeStock({ x: 300, y: 120 }, "Congestion")
|
const congestion = makeStock({ x: 300, y: 120 }, "Congestion")
|
||||||
|
congestion.initialValue = 50
|
||||||
const driving = makeFlow({ x: -120, y: 120 }, "driving", source.id, congestion.id)
|
const driving = makeFlow({ x: -120, y: 120 }, "driving", source.id, congestion.id)
|
||||||
|
// driving = 1.5 × road building (its `+` input): every new road induces *more*
|
||||||
|
// traffic than it cleared — the side effect that refills the symptom.
|
||||||
|
driving.rule = { kind: "proportional", factor: 1.5 }
|
||||||
const sink = makeCloud({ x: 300, y: -160 })
|
const sink = makeCloud({ x: 300, y: -160 })
|
||||||
const roadBuilding = makeFlow({ x: -120, y: -160 }, "road building", congestion.id, sink.id)
|
const roadBuilding = makeFlow({ x: -120, y: -160 }, "road building", congestion.id, sink.id)
|
||||||
|
// road building = 40% of Congestion (its `+` input), draining it: the Balancing
|
||||||
|
// fix. But induced driving outweighs it, so the Reinforcing loop wins and
|
||||||
|
// Congestion climbs anyway — you can't build your way out of traffic.
|
||||||
|
roadBuilding.rule = { kind: "proportional", factor: 0.4 }
|
||||||
return model(
|
return model(
|
||||||
"Fixes that fail",
|
"Fixes that fail",
|
||||||
[source, congestion, driving, sink, roadBuilding],
|
[source, congestion, driving, sink, roadBuilding],
|
||||||
[link(congestion, roadBuilding, "+"), link(roadBuilding, driving, "+")],
|
[link(congestion, roadBuilding, "+"), link(roadBuilding, driving, "+")],
|
||||||
|
{ start: 0, stop: 30, dt: 1 },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,13 +516,18 @@ function driftToLowPerformance(): Model {
|
|||||||
// Reinforcing spiral cross in the open centre, where the R badge lands.
|
// Reinforcing spiral cross in the open centre, where the R badge lands.
|
||||||
const source = makeCloud({ x: -560, y: 120 })
|
const source = makeCloud({ x: -560, y: 120 })
|
||||||
const performance = makeStock({ x: -160, y: 120 }, "Performance")
|
const performance = makeStock({ x: -160, y: 120 }, "Performance")
|
||||||
|
performance.initialValue = 40
|
||||||
const improvement = makeFlow(
|
const improvement = makeFlow(
|
||||||
midpoint(source.position, performance.position),
|
midpoint(source.position, performance.position),
|
||||||
"improvement",
|
"improvement",
|
||||||
source.id,
|
source.id,
|
||||||
performance.id,
|
performance.id,
|
||||||
)
|
)
|
||||||
|
// improvement closes the gap upward: 10% of (Standard − Performance), pulling
|
||||||
|
// Performance toward the Standard.
|
||||||
|
improvement.rule = { kind: "gap", factor: 0.1 }
|
||||||
const standard = makeStock({ x: 160, y: -120 }, "Standard")
|
const standard = makeStock({ x: 160, y: -120 }, "Standard")
|
||||||
|
standard.initialValue = 80
|
||||||
const sink = makeCloud({ x: 560, y: -120 })
|
const sink = makeCloud({ x: 560, y: -120 })
|
||||||
const slippage = makeFlow(
|
const slippage = makeFlow(
|
||||||
midpoint(standard.position, sink.position),
|
midpoint(standard.position, sink.position),
|
||||||
@@ -436,6 +535,10 @@ function driftToLowPerformance(): Model {
|
|||||||
standard.id,
|
standard.id,
|
||||||
sink.id,
|
sink.id,
|
||||||
)
|
)
|
||||||
|
// slippage erodes the *same* gap from the other side: the Standard drifts down
|
||||||
|
// toward actual Performance. Both gaps close, so they meet — the Standard has
|
||||||
|
// sagged from 80 to the middle, the eroding-goal trap.
|
||||||
|
slippage.rule = { kind: "gap", factor: 0.1 }
|
||||||
return model(
|
return model(
|
||||||
"Drift to low performance",
|
"Drift to low performance",
|
||||||
[source, performance, improvement, standard, sink, slippage],
|
[source, performance, improvement, standard, sink, slippage],
|
||||||
@@ -445,6 +548,7 @@ function driftToLowPerformance(): Model {
|
|||||||
link(standard, slippage, "+"),
|
link(standard, slippage, "+"),
|
||||||
link(performance, slippage, "-"),
|
link(performance, slippage, "-"),
|
||||||
],
|
],
|
||||||
|
{ start: 0, stop: 60, dt: 1 },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user