Canonical target-granular forms, per-target regime transitions, model-level broadcast#379
Merged
hmgaudecker merged 7 commits intoJun 11, 2026
Conversation
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.
for more information, see https://pre-commit.ci
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>
Benchmark comparison (main → HEAD)Comparing
|
hmgaudecker
commented
Jun 11, 2026
hmgaudecker
left a comment
Member
Author
There was a problem hiding this comment.
Review start, continuing later.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)replacesNoneinstate_transitions: an ordinary identity law — legal bare, insidePhasedsides, and in per-target cells — whose argument must match its dict key (checked, including insidePhased/per-target leaves).Noneis rejected with a migration hint.EffectiveUserRegime: the regime as the model runs it — model-levelderived_categoricalsmerged, defaultHinjected, completeness validated (utility entry, state-transition coverage, state/action overlap, distributed-grid rules) in its constructor. A bareRegimevalidates only local value-shape properties; completeness relocates to model build, the seam that lets model-level slots satisfy it later (commit 3).model.user_regimesexposes the effective form.canonicalize_regimes: the model-level canonicalization stage. Every phase slice'sstate_transitionsvalue becomes the canonicalMapping[RegimeName, law]over exactly the reachable targets carrying the state — bare laws broadcast, per-target dicts pass through,fixed_transitiondesugars into grid-annotated identities. Reachability is resolved once, here;_extract_phase_transitionsbecomes a pure transpose and_classify_transitionsis deleted. Rule: the params template reads the user (effective) spec, the engine reads the canonical spec.Behavior-preserving except two sanctioned breaks: the
Nonespelling dies loudly, and completeness errors raise atModel(...)instead ofRegime(...).Commit 2 — per-target regime transitions
MarkovTransition) keep meaning "every regime reachable"; terminal staystransition=None;transition={}errors; cells must beMarkovTransition-wrapped (plain callables rejected as "not yet supported").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_targetsbecomesget_period_targets.to_<target>_next_regime;Phasedgranular 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).Nonemasks the model entry uniformly across slots; masking a state also drops its broadcast law; an unbound mask errors (with thefixed_transitionhint forstate_transitions). Broadcast laws are not merged into terminal regimes.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_variablesreports the result;validate_model_inputsexempts broadcast names from the unused-variable check.distributed=Trueis legal only on model-level states; a sharded state pruned from a non-terminal regime errors (unshard, or make the regime use it).batch_sizestays per-declaration.Verification
feat/canonical-broadcast-grammar(one mirror commit per pylcm commit):pref_typeviafixed_transition; granular regime transitions declaring each regime's true target set; shared functions/constraint/states/laws broadcast fromModel(...)withdeadmasking what the bequest DAG does not read — 236 tests green.🤖 Generated with Claude Code