Skip to content

ltm: a model dependency inside a frozen dependency's subscript index is lagged twice in the ceteris-paribus partial #1060

Description

@bpowers

Problem

wrap_non_matching_in_previous (src/simlin-engine/src/ltm_augment.rs) freezes a non-live subscripted read whose index is itself a model dependency as PREVIOUS(q[PREVIOUS(ctr, ctr)]). The outer PREVIOUS is a capture of q[PREVIOUS(ctr)] evaluated at t-1, so the read is q_{t-1}[ctr_{t-2}]. The paper's partial (docs/reference/ltm--loops-that-matter.md section 3.1, Delta_x(z) = f(x_current, y_previous) - z_previous) reads every non-isolated input at the previous step, i.e. q_{t-1}[ctr_{t-1}] -- and the anchor z_previous was itself computed with ctr_{t-1}. So whenever ctr moves, the frozen read and the anchor see different elements of q, and the difference is credited to the isolated source.

Where it is pinned today

  • db::ltm_tests::pinned_double_lag_residual_is_not_a_structural_zero pins the residual as values: on the colliding_index_name_model fixture, the pop[nyc] -> share score's boston slot -- an arm whose every occurrence is frozen and which should be inert -- is -1.0588, 0.7297, -1.0286, 0.8182 at steps 2..5. The rustdoc on colliding_index_boston_series records that the semantics question was deferred, and that a crude probe (disabling the whole index pass) takes the fixture to exactly 0 while redding 5 tests -- an upper bound on the narrow fix's cost, not a measurement of it.
  • ltm_augment_zero_slot.rs (reach_of, the BuiltinReach::LagsOneStep arm) recognizes the shape (PREVIOUS(q[PREVIOUS(ctr, ctr)]) "reads q at t-1 indexed by ctr at t-2, where the anchor indexed at t-1") and refuses to classify the arm as PREVIOUS(target). That half is settled and must stay while the lowering is what it is: the arm is NOT a structural zero today precisely because of the double lag (GH ltm: link-score generation materializes one full guard-form arm per element of an arrayed target (quadratic in element count) #977's omission must not claim it).
  • wrap_index_non_matching_in_previous's GH ltm-augment: partial builders PREVIOUS-wrap dimension-name subscript indices (Wildcard and Bare shapes), silently stubbing scores #759 comment already calls reading an index two steps back "semantically wrong for a genuinely-dynamic index".

The clock case was fixed; this is the sibling left in place

Commit f5e0325 ("engine: freeze the clock inside the ceteris-paribus partial", branch ltm-fidelity) found the identical double lag for the clock: z = x + arr[TIME] was lowered to PREVIOUS(arr[PREVIOUS(TIME)]) = arr_{t-1}[TIME_{t-2}], and the twice-lagged clock flipped the link's sign from the second scored step (pinned in tests/integration/ltm_frozen_clock.rs: 0.1667, 0.1379, 0.1213, 0.1118 at t = 2..5 in the once-lagged form). The fix leaves a time-dependent call verbatim inside a subtree the wrap is about to freeze (the frozen flag), because the enclosing capture already lags the whole read once. The wrap_non_matching_in_previous rustdoc says explicitly that a model dependency in that position IS still lagged twice and "does not join" the clock fix -- the A3 review of that commit left it in place as pre-existing.

Fix shape

When the enclosing read is being frozen whole (frozen == true), leave a model-dependency index read verbatim, exactly as f5e0325 does for a time-dependent call: PREVIOUS(q[ctr]) is q_{t-1}[ctr_{t-1}], the paper's value. Keep straight:

  • The static-selector guard is separate and must not move: an_index_naming_the_axis_own_element_stays_a_static_selector pins that q[s1] (an element of the axis) stays unwrapped and qualified.
  • In a LIVE reference's index (frozen == false) the index still takes its own freeze (arr[PREVIOUS(idx)] for the isolated source read through a dynamic index; GH ltm: first-live-step NaN in dynamic-index per-element link scores (uninitialized PREVIOUS(index) capture helper) #975's head-lag pin per_element_dynamic_index_scores_preserve_head_lag). This issue is only about an index inside an already-frozen read.
  • Once the lowering is PREVIOUS(q[ctr]), the zero-slot predicate's LagsOneStep arm may legitimately classify such an arm as PREVIOUS(target). The pinned values in pinned_double_lag_residual_is_not_a_structural_zero will move (possibly to an exact 0 on that fixture, whose arm reads no live occurrence); the test must be re-derived from the new lowering rather than re-captured, and its series[2].abs() > 1.0 soundness assertion is specifically a statement about the OLD lowering.
  • Needs a value test on a model with a genuinely dynamic index (q[ctr], ctr changing every step) where the paper's partial is computed by hand from the recorded series, alongside the existing fixture.

Cost: small (a few lines in the wrap's Expr0::Subscript arm plus re-derived pins).

Components affected

Related

Discovery context

LTM fidelity work of 2026-09-07..09 (branches ltm-fidelity / ltm-fidelity-post); surfaced by the A3 review of commit f5e0325, which fixed the clock case and left this one as pre-existing.

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