diff --git a/src/components/Inspector.vue b/src/components/Inspector.vue index b439625..7ad6650 100644 --- a/src/components/Inspector.vue +++ b/src/components/Inspector.vue @@ -115,6 +115,8 @@ const RULE_HINT: Record = { constant: "A fixed number — no inputs.", proportional: "rate = factor × its “+” inputs.", gap: "rate = factor × (level − target): the “+” input is the level, the “−” the target.", + overflow: + "rate = max(0, factor × (level − threshold)): spills only once the “+” level passes the “−” threshold.", } @@ -167,6 +169,7 @@ const RULE_HINT: Record = { + diff --git a/src/model/io.ts b/src/model/io.ts index fada8c8..8778db8 100644 --- a/src/model/io.ts +++ b/src/model/io.ts @@ -55,7 +55,8 @@ function isPolarity(value: unknown): value is Polarity { function isRule(value: unknown): value is Rule { if (!isObject(value)) return false if (value.kind === "constant") return isFiniteNumber(value.value) - if (value.kind === "proportional" || value.kind === "gap") return isFiniteNumber(value.factor) + if (value.kind === "proportional" || value.kind === "gap" || value.kind === "overflow") + return isFiniteNumber(value.factor) return false } diff --git a/src/model/simulation.ts b/src/model/simulation.ts index e54948b..5d3660f 100644 --- a/src/model/simulation.ts +++ b/src/model/simulation.ts @@ -107,6 +107,19 @@ function evalRule(rule: Rule, links: InformationLink[], valueOf: (id: string) => rule.factor * ((level ? valueOf(level.source) : 0) - (target ? valueOf(target.source) : 0)) ) } + case "overflow": { + // max(0, factor × (level − threshold)): a one-sided gap. The `+` input is the + // level, the `−` the threshold; it stays shut until the level passes it, so an + // overflow Flow spills only the excess. Clamping at 0 is what stops it running + // backwards below the threshold — gap can't, by design (it's bidirectional). + const level = links.find((link) => link.polarity === "+") + const threshold = links.find((link) => link.polarity === "-") + return Math.max( + 0, + rule.factor * + ((level ? valueOf(level.source) : 0) - (threshold ? valueOf(threshold.source) : 0)), + ) + } } } diff --git a/src/model/types.ts b/src/model/types.ts index 00af745..23981aa 100644 --- a/src/model/types.ts +++ b/src/model/types.ts @@ -42,11 +42,15 @@ export interface Position { * (→ exponential growth/decay) * - `gap` — `factor × (level − target)`, where the `+` input is the * level and the `−` input the target. (→ goal-seeking) + * - `overflow` — `max(0, factor × (level − threshold))`: a one-sided `gap` + * that only fires once the `+` level exceeds the `−` + * threshold. (→ a spillway / hard ceiling) */ export type Rule = | { kind: "constant"; value: number } | { kind: "proportional"; factor: number } | { kind: "gap"; factor: number } + | { kind: "overflow"; factor: number } /** * The run parameters for a simulation: integrate from `start` to `stop` in steps