Files
meadows/docs/adr/0004-rate-rules-not-formulas.md
2026-06-22 23:01:33 +02:00

5.7 KiB
Raw Permalink Blame History

Behaviour comes from rules over Information Links, not free-form formulas

Part of meadows · see DESIGN.md.

Numeric simulation (phase 2) makes a Model alive: Stocks accumulate over time, Flows and Converters recompute each instant. Two coupled decisions shape how a Model carries the numbers, and both trade expressive power for valid by construction — the right trade for a tool that exists to popularise systems thinking, not to compete with Vensim.

Decision

1. The Information Link is the declared dependency. A Flow's or Converter's inputs are exactly the elements that link into it. There is no separate "equation references" namespace to keep in sync — the wiring you draw is the wiring the simulator reads. The same signed graph the loop detector walks (ADR-0001) is the graph the simulator integrates, so the loops you see classified R/B are the loops you run. They can never disagree.

2. A Flow/Converter computes from a small fixed vocabulary of Rules, not a typed-in formula. Each instantaneous element picks one rule and a plain number or two — never an expression:

Rule Value Reads (via Information Links) Emergent behaviour
Constant a fixed number nothing linear Stock change
Proportional factor × (its + inputs) the +-polarity inputs exponential growth / decay
Gap factor × (level target) the + input is level, is target goal-seeking / asymptotic
Overflow max(0, factor × (level threshold)) the + input is level, is threshold a spillway / hard ceiling

The famous curves are compositions of these over the structure — a logistic S-curve is Proportional growth meeting a Gap-driven ceiling (limits-to-growth); goal-seeking decay is a lone Gap (coffee cooling). The user sets up a local rule; the global shape emerges. That emergence is the lesson.

Overflow is the one declared limit. Constant/Proportional/Gap are smooth and their ceilings emerge (the S-curve is two of them meeting); Overflow instead is a threshold — max(0, …) that stays shut until a level crosses it, then spills the excess down an outflow (a bathtub brimming onto the floor). It earns a rule of its own because Gap cannot stand in: Gap is signed and bidirectional (coffee re-warms if it drops below the room), so a Gap outflow runs backwards below its target, and that clamp can't be applied to Gap globally without breaking the goal-seekers. So Overflow is a deliberate exception to "the shape emerges" — a hard ceiling you declare, for the real limit that is a wall, not a slope (and the rate-side sibling of the non-negative-Stock floor the integrator already enforces).

Polarity does double duty. The +/ already captured for loop classification (ADR-0001) also selects each operand's role: Proportional reads its + inputs; Gap reads its + input as the level and its input as the target, and Overflow the same with its input as the threshold. One gesture, two payoffs — no new per-link data.

Considered Options

  • Free-form expression strings (birth rate = Population × fertility) — maximally expressive, and what equation?: string originally anticipated. Rejected: needs a parser + a sandbox (never eval/new Function), invites broken-formula and name-resolution errors (auto-names contain spaces), and lets a learner paint a curve instead of discovering it from structure.
  • Pick the output curve (label a Stock "exponential" / "logarithmic") — rejected: it is the answer, not the cause, and it breaks the moment feedback decides the shape. "Logarithmic" in particular has no honest local rule; what people mean by it is asymptotic approach — which is the Gap rule.
  • Rules over Information Links (chosen) — no parser, valid by construction, and it teaches structure → behaviour.

Consequences

  • The domain types gain a Rule union on Flow/Converter (replacing the unused equation?: string), an optional initialValue on Stock, and an optional SimSpec (start / stop / dt) on the Model. All optional and additive, so existing saved Models still load (F8); they are simply not simulatable until equipped.
  • Algebraic loops are an error. A cycle in the wiring is legitimate feedback iff it passes through a Stock — the Stock supplies last-step state and so breaks the within-step dependency. A cycle among only Flows/Converters has no Stock to break it: the simulator cannot order it and rejects it. The simulator reuses the cycle machinery to detect this; it is a new sim-readiness check, distinct from structural validity (validation.ts).
  • A reader seeing { kind: "gap", factor } on a Flow and wondering where its operands come from should look here: they are the Flow's inbound Information Links, picked by Polarity.
  • The vocabulary starts deliberately small (Constant / Proportional / Gap — enough for linear, exponential, and goal-seeking, and for the coffee and savings samples). Growing it is additive: a new kind in the union plus a case in the evaluator — as Overflow later bore out (one union member, one evaluator case, plus matching touch-ups to the rule validator and the inspector). Multi-input products (e.g. Population × fertility) are a later increment, not a phase-2 blocker.