Skip to content

Canonical target-granular forms, per-target regime transitions, model-level broadcast#379

Merged
hmgaudecker merged 7 commits into
feat/phase-variant-statesfrom
feat/canonical-broadcast-grammar
Jun 11, 2026
Merged

Canonical target-granular forms, per-target regime transitions, model-level broadcast#379
hmgaudecker merged 7 commits into
feat/phase-variant-statesfrom
feat/canonical-broadcast-grammar

Conversation

@hmgaudecker

@hmgaudecker hmgaudecker commented Jun 10, 2026

Copy link
Copy Markdown
Member

Stacked on #373. Pushes "canonical = granular" to its fixed point: one PR, three commits.

Commit 1 — the Spine

The engine reads one uniform, target-granular representation of every law of motion; the user pipeline gains an explicit effective-regime layer; fixed states get a first-class spelling.

  • lcm.fixed_transition(state_name) replaces None in state_transitions: an ordinary identity law — legal bare, inside Phased sides, and in per-target cells — whose argument must match its dict key (checked, including inside Phased/per-target leaves). None is rejected with a migration hint.
  • EffectiveUserRegime: the regime as the model runs it — model-level derived_categoricals merged, default H injected, completeness validated (utility entry, state-transition coverage, state/action overlap, distributed-grid rules) in its constructor. A bare Regime validates only local value-shape properties; completeness relocates to model build, the seam that lets model-level slots satisfy it later (commit 3). model.user_regimes exposes the effective form.
  • canonicalize_regimes: the model-level canonicalization stage. Every phase slice's state_transitions value becomes the canonical Mapping[RegimeName, law] over exactly the reachable targets carrying the state — bare laws broadcast, per-target dicts pass through, fixed_transition desugars into grid-annotated identities. Reachability is resolved once, here; _extract_phase_transitions becomes a pure transpose and _classify_transitions is deleted. Rule: the params template reads the user (effective) spec, the engine reads the canonical spec.

Behavior-preserving except two sanctioned breaks: the None spelling dies loudly, and completeness errors raise at Model(...) instead of Regime(...).

Commit 2 — per-target regime transitions

transition={"retired": MarkovTransition(prob_retired), "dead": MarkovTransition(prob_dead)}
  • The key set declares reachability: omitted regimes are structurally unreachable. Coarse forms (bare callable / MarkovTransition) keep meaning "every regime reachable"; terminal stays transition=None; transition={} errors; cells must be MarkovTransition-wrapped (plain callables rejected as "not yet supported").
  • Reachability now has a single source of truth — the regime transition — resolved in canonicalize_regimes. The coverage-based reachability inference and the incomplete-target runtime subsystem (_validate_no_reachable_incomplete_targets, the validate_V hint) are deleted; get_complete_targets becomes get_period_targets.
  • Cell params surface in the template under to_<target>_next_regime; Phased granular transitions require matching forms and identical key sets across phases. The simulation membership draw intersects active targets with the declared key set.

Commit 3 — model-level broadcast + pruning + sharding

Model(functions=..., constraints=..., states=..., state_transitions=..., actions=...), each slot accepting exactly what the regime-level slot accepts (incl. Phased, processes, per-target dicts, fixed_transition).

  • Exactly-one-level rule (params-style): a name lives at model level or regime level, never both — ambiguity errors. A regime-level None masks the model entry uniformly across slots; masking a state also drops its broadcast law; an unbound mask errors (with the fixed_transition hint for state_transitions). Broadcast laws are not merged into terminal regimes.
  • DAG pruning: broadcast states and actions survive per regime only where a root computation (utility, H, constraints, derived categoricals, the regime transition, or a law toward a reachable target that keeps the state) transitively reads them — a cross-regime least fixed point, computed per phase slice, pruned only when dead in both phases. Regime-level declarations are never pruned. model.pruned_variables reports the result; validate_model_inputs exempts broadcast names from the unused-variable check.
  • Sharding: distributed=True is legal only on model-level states; a sharded state pruned from a non-terminal regime errors (unshard, or make the regime use it). batch_size stays per-declaration.

Verification

  • pylcm: full suite green serially after each commit (1134 passed); prek + ty clean.
  • aca-model migrated on feat/canonical-broadcast-grammar (one mirror commit per pylcm commit): pref_type via fixed_transition; granular regime transitions declaring each regime's true target set; shared functions/constraint/states/laws broadcast from Model(...) with dead masking what the bequest DAG does not read — 236 tests green.
  • GPU regression (seeded panel diff on an A100, expectation bit-identical) to follow before merge.

🤖 Generated with Claude Code

hmgaudecker and others added 2 commits June 10, 2026 17:34
The Spine of the canonical-broadcast-grammar work: the engine now reads one
uniform, target-granular representation of every law of motion, produced by
a model-level canonicalization stage; the user pipeline gains an explicit
effective-regime layer; and fixed states get a first-class spelling.

- `lcm.fixed_transition(state_name)` replaces `None` in `state_transitions`:
  an ordinary identity law (legal bare, inside `Phased` sides, and in
  per-target cells) whose argument must match its dict key. `None` is
  rejected with a migration hint. `_IdentityTransition` moves to the leaf
  module `_lcm/identity_transition.py`.
- `EffectiveUserRegime` (`_lcm/regime_building/effective.py`): the regime as
  the model runs it — model-level `derived_categoricals` merged, default `H`
  injected, completeness validated (utility entry, state-transition
  coverage, state/action overlap, distributed-grid rules). A bare `Regime`
  now validates only local value-shape properties at construction;
  completeness checks relocate to model build, where model-level slots can
  still satisfy them. `model.user_regimes` exposes the effective form; the
  params template reads it (template keys mirror the user's coarseness).
- `canonicalize_regimes` (`_lcm/regime_building/canonicalize.py`): rewrites
  every phase slice's `state_transitions` into the canonical
  `Mapping[RegimeName, law]` form over exactly the reachable targets
  carrying the state — bare laws broadcast, per-target dicts pass through,
  `fixed_transition` entries desugar into grid-annotated identities.
  Reachability is resolved once, here; `_extract_phase_transitions` becomes
  a pure transpose and `_classify_transitions` is gone. Rule established:
  the params template reads the user (effective) spec, the engine reads the
  canonical spec (`_extract_param_key` consults the template).

Behavior-preserving except the two sanctioned breaks above (the `None`
spelling and the relocated error sites). 1118 tests green.
@read-the-docs-community

read-the-docs-community Bot commented Jun 10, 2026

Copy link
Copy Markdown

hmgaudecker and others added 5 commits June 11, 2026 00:06
The granular regime-transition spelling:

    transition={"retired": MarkovTransition(prob_retired),
                "dead": MarkovTransition(prob_dead)}

Each cell returns that target's transition probability; the key set IS the
regime's reachability declaration — omitted targets are structurally
unreachable. The coarse forms stay valid and reach every regime.

- Grammar: cells must be `MarkovTransition`-wrapped (deterministic granular
  rejected as not yet supported); `transition={}` errors (terminality stays
  `transition=None`); `Phased` composes outermost with matching forms and
  identical key sets across phases.
- Reachability has a single source of truth — the regime transition.
  `canonicalize_regimes` validates per-target state laws against it: they
  must cover every reachable target carrying the state and may name neither
  unreachable nor unknown targets, with the error pointing to the granular
  spelling as the narrowing tool. The coverage-based reachability inference
  is gone.
- The incomplete-target runtime subsystem
  (`_validate_no_reachable_incomplete_targets`, the stochastic-coverage
  filter in `get_complete_targets`) is deleted: `get_period_targets` keeps
  only the active-next-period filter over the canonical bundles, and the
  realized membership draw reads the declared targets' probability dict.
- Engine: granular cells flow as `to_<target>_next_regime`-parameterized
  functions and are assembled into the same probs-dict contract the coarse
  wrapper produces (`_assemble_granular_regime_transition_probs`); the params
  template keys each cell under `to_<target>_next_regime`.
`Model(functions=..., constraints=..., states=..., state_transitions=...,
actions=...)` merges each entry into every regime, replacing hand-broadcast
duplication in downstream models.

- Exactly-one-level rule (the params-ambiguity precedent): a name is defined
  at model level or regime level, never both; same-key collisions error even
  for identical values. A regime-level `None` masks the model entry — in
  every slot, uniformly; masking a state drops its broadcast law; an unbound
  mask errors (for state_transitions, with a `fixed_transition` pointer).
  Broadcast laws are not merged into terminal regimes (they are inert there).
- DAG pruning: broadcast states and actions are weeded per regime by
  reachability — kept only where a root computation (utility, H,
  constraints, derived categoricals, the regime transition, or a law of
  motion toward a reachable target that keeps the state) transitively reads
  them, computed per phase slice as a cross-regime least fixed point and
  pruned only when dead in both phases. Regime-level declarations are never
  pruned. `model.pruned_variables` records the result; `Model.__repr__`
  mentions it when nonempty. The unused-variable check exempts broadcast
  names (pruning already weeded them; a retained one may be used only
  through a handover law).
- Sharding: `distributed=True` is legal only on model-level states (one
  declaration keeps every regime's device layout consistent); a sharded
  state pruned from a non-terminal regime is an error.
…' into feat/canonical-broadcast-grammar

# Conflicts:
#	src/_lcm/regime_building/canonicalize.py
An explicit field-name tuple silently drops any newly added Regime field
from the effective regime (the field falls back to its class default);
iterating dataclasses.fields keeps the copy complete by construction. The
two fields with merge logic (functions, derived_categoricals) stay special.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
aca-model main still declares fixed states with the removed `None`
state-transition spelling, so the GPU benchmark's model construction
fails under this PR. Flip back to branch = "main" once both sides
have merged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Benchmark comparison (main → HEAD)

Comparing 64cf042c (main) → 6bf5ebad (HEAD)

Benchmark Statistic before after Ratio Alert
aca-baseline execution time 15.286 s 15.601 s 1.02
peak GPU mem 581 MB 581 MB 1.00
compilation time 311.45 s 345.26 s 1.11
peak CPU mem 7.83 GB 12.76 GB 1.63
aca-baseline-debug execution time 72.232 s 76.391 s 1.06
peak GPU mem 581 MB 581 MB 1.00
compilation time 404.14 s 443.54 s 1.10
peak CPU mem 7.63 GB 7.71 GB 1.01
Mahler-Yum execution time 4.868 s 4.780 s 0.98
peak GPU mem 520 MB 520 MB 1.00
compilation time 11.92 s 12.03 s 1.01
peak CPU mem 1.60 GB 1.60 GB 1.00
Precautionary Savings - Solve execution time 27.6 ms 26.8 ms 0.97
peak GPU mem 8 MB 8 MB 1.00
compilation time 1.52 s 1.55 s 1.02
peak CPU mem 1.15 GB 1.16 GB 1.00
Precautionary Savings - Simulate execution time 98.4 ms 111.4 ms 1.13
peak GPU mem 162 MB 162 MB 1.00
compilation time 3.83 s 3.76 s 0.98
peak CPU mem 1.35 GB 1.34 GB 1.00
Precautionary Savings - Solve & Simulate execution time 124.7 ms 121.0 ms 0.97
peak GPU mem 566 MB 566 MB 1.00
compilation time 5.07 s 5.04 s 1.00
peak CPU mem 1.32 GB 1.32 GB 1.00
Precautionary Savings - Solve & Simulate (irreg) execution time 229.2 ms 228.9 ms 1.00
peak GPU mem 2.18 GB 2.18 GB 1.00
compilation time 5.24 s 5.29 s 1.01
peak CPU mem 1.37 GB 1.38 GB 1.01

@hmgaudecker hmgaudecker left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review start, continuing later.

@hmgaudecker hmgaudecker merged commit 6bf5eba into feat/phase-variant-states Jun 11, 2026
11 checks passed
@hmgaudecker hmgaudecker deleted the feat/canonical-broadcast-grammar branch June 11, 2026 18:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant