feat(simulation): add the overflow rule, a one-sided gap for hard ceilings
A fourth Rule kind: overflow = max(0, factor × (level − threshold)). It stays shut until its `+` level passes its `−` threshold, so an outflow on it spills only the excess — a spillway / hard ceiling the smooth rules can't draw. Gap can't stand in for it: it's signed and bidirectional (coffee cooling relies on that), so below the threshold a gap outflow runs backwards. Clamping at zero is the whole point, and it can't be applied globally without breaking gap. This is the additive vocabulary growth ADR-0004 anticipates — a new kind in the union plus a case in the evaluator — alongside touch-ups to the rule validator and the inspector.
This commit is contained in:
@@ -115,6 +115,8 @@ const RULE_HINT: Record<Rule["kind"], string> = {
|
|||||||
constant: "A fixed number — no inputs.",
|
constant: "A fixed number — no inputs.",
|
||||||
proportional: "rate = factor × its “+” inputs.",
|
proportional: "rate = factor × its “+” inputs.",
|
||||||
gap: "rate = factor × (level − target): the “+” input is the level, the “−” the target.",
|
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.",
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -167,6 +169,7 @@ const RULE_HINT: Record<Rule["kind"], string> = {
|
|||||||
<option value="constant">Constant</option>
|
<option value="constant">Constant</option>
|
||||||
<option value="proportional">Proportional</option>
|
<option value="proportional">Proportional</option>
|
||||||
<option value="gap">Gap</option>
|
<option value="gap">Gap</option>
|
||||||
|
<option value="overflow">Overflow</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ function isPolarity(value: unknown): value is Polarity {
|
|||||||
function isRule(value: unknown): value is Rule {
|
function isRule(value: unknown): value is Rule {
|
||||||
if (!isObject(value)) return false
|
if (!isObject(value)) return false
|
||||||
if (value.kind === "constant") return isFiniteNumber(value.value)
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
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)),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,11 +42,15 @@ export interface Position {
|
|||||||
* (→ exponential growth/decay)
|
* (→ exponential growth/decay)
|
||||||
* - `gap` — `factor × (level − target)`, where the `+` input is the
|
* - `gap` — `factor × (level − target)`, where the `+` input is the
|
||||||
* level and the `−` input the target. (→ goal-seeking)
|
* 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 =
|
export type Rule =
|
||||||
| { kind: "constant"; value: number }
|
| { kind: "constant"; value: number }
|
||||||
| { kind: "proportional"; factor: number }
|
| { kind: "proportional"; factor: number }
|
||||||
| { kind: "gap"; factor: number }
|
| { kind: "gap"; factor: number }
|
||||||
|
| { kind: "overflow"; factor: number }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The run parameters for a simulation: integrate from `start` to `stop` in steps
|
* The run parameters for a simulation: integrate from `start` to `stop` in steps
|
||||||
|
|||||||
Reference in New Issue
Block a user