Skip to content

ltm: one causal link read through two reference sites is scored as two split partials (isolated loop scores 0.667, not 1) #1061

Description

@bpowers

Problem

When a target reads the same source element through two reference sites, the reference-site IR (db/ltm_ir.rs::collect_all_reference_sites, one RefShape per site) scores each site's ceteris-paribus partial separately, and the element graph carries parallel edges (or a direct edge plus an agg-routed path). The de-subscripted scalar model -- the correctness standard for arrayed LTM (docs/reference/ltm--loops-that-matter.md section 15.4) -- has ONE link whose partial holds every read of that source live. Splitting the partial breaks the isolated-loop invariant (reference 4.1: "a loop that is the only loop acting on its stocks always has loop score +/-1", the property Appendix B rests on) and, for reducers, doubles the loop list.

D1: Bare plus FixedIndex of the same element

Oracle corpus fixed_broadcast: Region = {nyc, boston, la}; pop[Region] stock (100, 200, 300) with inflow growth; growth[r] = pop * 0.02 + pop[nyc] * spill; spill = 0.01. For element nyc the scalar partial is 0.03 dpop / 0.03 dpop = 1, and pop_nyc -> growth_nyc -> pop_nyc is the only loop on that stock, so its raw score must be +1. Simlin emits

  • $⁚ltm⁚link_score⁚pop→growth[nyc] = 0.02/0.03 = 2/3 (the FixedIndex read frozen), and
  • $⁚ltm⁚link_score⁚pop[nyc]→growth = 1/3 (the Bare read frozen).

Johnson sees one adjacency, the loop takes the Bare name (parse_link_offsets' Bare-first rule), and the isolated loop reports raw 0.6667 at every step:

edge BAD  pop_nyc -> growth_nyc: scalar [1, 1, 1, 1] vs direct $⁚ltm⁚link_score⁚pop→growth[0] [0.666667, 0.666667, 0.666667, 0.666667]
loop RAW  BAD r1[0] ~ r3 growth_nyc -> pop_nyc: arrayed [0.666667, ...] vs scalar [1, 1, 1, 1]

D2: a direct read plus a read inside a hoisted reducer

Corpus mean_loop: growth[r] = pop[r] * 0.02 * (1 - MEAN(pop[*]) / 1000). Element nyc, t = 1: scalar link 1.2837; Simlin's direct edge pop→growth[nyc] = 1.3405 plus the agg path pop[nyc]→$⁚ltm⁚agg⁚0 * $⁚ltm⁚agg⁚0→growth[nyc] = -0.0559 (sum 1.2846, off by the second-order cross term). The arrayed path then reports TWO loops for the one scalar loop growth_nyc -> pop_nyc (raw 1.35 and -0.057; relative 0.78 and -0.03 against a scalar relative of 0.35). Same on min_loop, max_loop, stddev_loop, share_sum, scalar_cofactor_inline, two_d and migration_matrix (every edge SPLIT row in the oracle reports). For nonlinear reducers the two halves also do not multiply to the scalar link: on migration_matrix (a 3x3 flow matrix through a reducer) the median raw loop deviation is 0.5%, the 90th percentile 12% and the maximum 4.2, because splitting a ratio's numerator and denominator between the direct and agg edges yields opposite-signed loops of 5-10x the true magnitude. The chain rule through an agg (reference 4.2) is exact only for a linear consumer with no second read (cross_agg: all 12 element links equal the product of the halves to 1e-9).

Why it matters

Silent wrong numbers on the dominance surface for shapes a modeler writes without trying (a broadcast term, a share-of-mean factor). The isolated-loop invariant is the reference's own sanity check and it fails on a three-variable model; the doubled loops and the mis-split relative scores move dominance verdicts (epic #488's no-silent-wrong-numbers invariant).

The Bare-vs-FixedIndex per-shape split is deliberate (docs/tech-debt.md entry 26: "The Bare-vs-FixedIndex per-shape split survives") and was introduced to fix the opposite failure -- a partial that left every read live, including reads of OTHER elements. The correct partial holds every read of the SAME source element live and every other read frozen; the per-shape split freezes some reads of the isolated element, which is the error here.

Fix routes

(a) Textual, medium cost: when the IR finds several sites for one (from[e], to[f]) element edge, generate one partial holding all of them live and emit ONE edge; agg-routed sites join the direct partial, and the agg node remains a hop only for edges with no direct read. Acceptance: fixed_broadcast raw loop = 1; the edge rows of mean_loop, min_loop, stddev_loop; residual second-order differences only on reducer-chain edges that keep an agg hop.

(b) The fragments plan's per-slot re-evaluation (docs/design-plans/2026-09-04-link-scores-from-fragments.md): perturb one input slot and re-evaluate the target slot, so every read of that slot is live by construction and RefShape / agg hoisting are needed only for the element graph, never for scoring. Subsumes (a) and deletes most of the textual partial builders.

The oracle

The de-subscripting oracle that finds this class (desub.py / oracle.py, with the corpus in gen_corpus.py, in the audit's scratchpad) is promoted to an integration harness by the single-mode plan (docs/design-plans/2026-09-07-ltm-single-mode.md, Phase 5: tests/integration/ltm_desubscript_oracle.rs, AC7.2). That harness is the acceptance test for either route.

Components affected

  • src/simlin-engine/src/db/ltm_ir.rs -- collect_all_reference_sites, ClassifiedSite (one site per read; needs a per-edge grouping)
  • src/simlin-engine/src/db/analysis.rs -- RefShape, emit_edges_for_reference (parallel element edges)
  • src/simlin-engine/src/db/ltm/link_scores.rs -- per-shape emission (emit_link_scores_for_edge), the agg-routed leg
  • src/simlin-engine/src/ltm_augment.rs -- build_partial_equation_shaped (holds only the matching shape live)
  • src/simlin-engine/src/db/ltm/loops.rs -- the Bare-first name rule that picks which split edge a loop reads

Related

Discovery context

LTM fidelity audit of 2026-09-07..09 (branches ltm-fidelity / ltm-fidelity-post), audit report B (de-subscripting oracle), finding D. Pre-existing; not a regression of that branch's commits (05e4dae, 3afd637, f5e0325, 8cbcf0c, 6a90255, 95f848f, e5319fb, e85914f).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    engineIssues with the rust-based simulation engineltmLoops that Matter (LTM) analysis subsystem

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions