Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- [design/conveyors.md](design/conveyors.md) -- XMILE conveyor support: complete specification of syntax, per-DT simulation semantics, leakage/initialization formulas, spread inputs, arrays, and engine integration
- [design/engine-performance.md](design/engine-performance.md) -- Engine compile/simulate profile (C-LEARN), how to measure a change (the instruction, LTM, artifact, sweep and dump channels), implemented optimizations, and remaining proposals
- [design/ltm--loops-that-matter.md](design/ltm--loops-that-matter.md) -- LTM implementation design: data structures, synthetic variables, module handling, array/element-level support, and post-simulation loop discovery (candidate generation, retention against the loop universe, ranking and the coverage-aware cap)
- [design/ltm-always-on.md](design/ltm-always-on.md) -- Proposed always-on LTM constraints: faithful counterfactuals, immutable run analysis, completeness and numerical validity, cheaper instrumentation and storage, bounded discovery, and acceptance evidence
- [design/mdl-parser.md](design/mdl-parser.md) -- Vensim MDL parser design history and implementation notes
- [design/queues.md](design/queues.md) -- XMILE queue support: complete specification of queue stocks, FIFO discipline, conveyor/queue coupling, overflow flows, and engine integration
- [design/vdf.md](design/vdf.md) -- VDF binary format specification and parser design
Expand Down
123 changes: 69 additions & 54 deletions docs/design-plans/2026-09-04-link-scores-from-fragments.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/design-plans/2026-09-07-ltm-single-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Drafted from the Definition of Done; to be validated with the owner before an im

- **LTM (Loops That Matter)**: The feedback-loop dominance method Simlin implements (Schoenberg, Eberlein et al.). It quantifies, at each timestep, how much of a model's observed behavior each feedback loop accounts for. Full write-up in `docs/reference/ltm--loops-that-matter.md`.
- **Link score**: A dimensionless per-timestep measure of one causal link's contribution to the change in its target variable, carrying both a magnitude and a sign. Computed by re-evaluating the target's equation with every input except the one under test held at its previous value.
- **Loop score**: The product of the link scores around a closed loop. Its sign is the loop's polarity; its magnitude is the "force" the loop exerts on the stocks it touches. A loop alone on its stocks always scores exactly plus or minus 1.
- **Loop score**: The product of the link scores around a closed loop. Its sign is the loop's polarity. An isolated active loop with no changing exogenous inputs has the unit-magnitude cancellation described in the papers; a raw loop score need not have unit magnitude under exogenous forcing. Its partition-relative score is plus or minus 1 when it is the only nonzero member of the partition, regardless of its raw magnitude. At equilibrium, the scores are zero.
- **Relative loop score**: A loop score normalized by the sum of absolute loop scores of all loops in its cycle partition. Lands in [-1, 1], and the absolute values within a partition sum to 1. This is what consumers plot, because raw scores diverge toward infinity near dominance shifts where competing loops cancel.
- **Polarity (reinforcing / balancing / undetermined)**: An even number of negative links makes a loop reinforcing; an odd number makes it balancing; a link of unknown sign makes it undetermined. Static polarity is derived from the equation AST at compile time; runtime polarity is classified from the sign of the recorded score series, with a confidence value. This plan deletes static polarity.
- **Cycle partition**: A group of stocks connected to each other through feedback, computed as a strongly connected component of the stock-to-stock reachability graph. Relative scores are only meaningful within a partition.
Expand Down
34 changes: 20 additions & 14 deletions docs/design/ltm--loops-that-matter.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,19 +311,25 @@ For a link from `x` to `z` where `z = f(x, y, ...)`:
modules: the `eqn` field holds the original text like `SMTH1(x, 5)` while
the AST holds the expanded form like `$⁚s⁚0⁚smth1·output`).
2. Compute the dependency set from the AST via `identifier_set()`.
3. Build the ceteris-paribus partial equation using `build_partial_equation()`,
which parses the equation into an `Expr0` AST, recursively walks the tree
wrapping variable references in `PREVIOUS()` for all dependencies except `x`
(`wrap_non_matching_in_previous`), and prints the result back to equation text. This
3. Build the ceteris-paribus partial equation using `build_partial_equation_shaped()`,
which recursively walks the `Expr0` tree wrapping variable references in
`PREVIOUS()` for all dependencies except `x` (`wrap_non_matching_in_previous`),
and prints the result back to equation text. This
AST-based approach avoids the pitfalls of text-based replacement (e.g.,
replacing `x` inside `x_rate`, or corrupting function names like `MAX`).
4. The link score is:
```
if (TIME = INITIAL_TIME) then 0
else if ((z - PREVIOUS(z)) = 0) OR ((x - PREVIOUS(x)) = 0) then 0
else ABS(SAFEDIV((partial_eq - PREVIOUS(z)), (z - PREVIOUS(z)), 0))
* SIGN(SAFEDIV((partial_eq - PREVIOUS(z)), (x - PREVIOUS(x)), 0))
if (TIME <= INITIAL_TIME) then 0
else if (ABS((z - PREVIOUS(z))) <= 0) OR (ABS((x - PREVIOUS(x))) <= 0) then 0
else SAFEDIV(((partial_eq) - PREVIOUS(z)), ABS((z - PREVIOUS(z))), 0)
* SIGN((x - PREVIOUS(x)))
```
`|N/Δz| * sign(N/Δx)` is written `SAFEDIV(N, |Δz|, 0) * sign(Δx)`, so the
partial appears once. Every generator (this form, the flow-to-stock and
element-reducer scores, and the black-box module transfer) wraps its score
in one guard, `link_score_guard`. Both of its comparisons are exact:
equation `=` is approximate, and an approximate zero-change or first-step
test would make a score depend on the model's units or time scale.

### Flow-to-Stock Links

Expand All @@ -343,9 +349,9 @@ outflow -- so the emitted equation is the standard guard form with that
numerator:

```
if (TIME = INITIAL_TIME) then 0
else if ((net - PREVIOUS(net)) = 0) OR ((flow - PREVIOUS(flow)) = 0) then 0
else SAFEDIV(+/-(flow - PREVIOUS(flow)), ABS(net - PREVIOUS(net)), 0) * SIGN(flow - PREVIOUS(flow))
if (TIME <= INITIAL_TIME) then 0
else if (ABS((net - PREVIOUS(net))) <= 0) OR (ABS((flow - PREVIOUS(flow))) <= 0) then 0
else SAFEDIV(+/-(flow - PREVIOUS(flow)), ABS((net - PREVIOUS(net))), 0) * SIGN((flow - PREVIOUS(flow)))
```

which evaluates to `sign * |Δflow / Δnet|`: the 2023 paper's Eq. 3 (its
Expand All @@ -357,7 +363,7 @@ Polarity is structural: inflows +1, outflows -1. Both deltas are read over
all describe one interval, the score is the same whether a stock's flows are
written separately or as one net flow, and no `dt` appears (an isolated loop
scores exactly `+/-1` at every `dt`, `tests/integration/ltm_dt_invariance.rs`).
Like every other score it is 0 at `TIME = INITIAL_TIME` and defined from the
Like every other score it is 0 at `TIME <= INITIAL_TIME` and defined from the
first step after the start. The net aux is LTM machinery, not a causal node:
the causal graph keeps its `flow -> stock` edges, and the aux appears in no
loop and no link.
Expand Down Expand Up @@ -975,7 +981,7 @@ edge:
they cannot disagree about which cycles exist: a value is active when it is
finite and nonzero, or infinite (a divergent link is real signal); only NaN and
an exact zero are inactive. Step 0 is excluded from union membership and masked
out of every activity test: every link-score equation's `TIME = INITIAL_TIME`
out of every activity test: every link-score equation's `TIME <= INITIAL_TIME`
guard arm is emitted as the literal constant `0`, so a cycle "active" only there
is not a scorable loop. Self-edges are dropped at build time -- an elementary
cycle never repeats a node, so a self-edge can neither be nor extend one, and a
Expand Down Expand Up @@ -2150,7 +2156,7 @@ cases remain deliberate carve-outs:
5. **PREVIOUS is intrinsic**: The `PREVIOUS()` function used in link score
equations is compiled as an intrinsic two-argument builtin. Unary syntax is
desugared to `PREVIOUS(x, 0)`. LTM first-timestep behavior is handled
explicitly with `TIME = INITIAL_TIME`.
explicitly with `TIME <= INITIAL_TIME`.

6. **Relative loop score formula and timing**: The implementation computes
`loop_score / sum_of_abs_scores` with explicit division-by-zero protection
Expand Down
Loading
Loading