Canonical params: the target is a genuine tree level#384
Open
hmgaudecker wants to merge 44 commits into
Open
Conversation
A phase-variant container for a quantity that is a derived DAG function in the solve phase but a seeded, evolved state in the simulate phase. Mirrors SolveSimulateFunctionPair; exported from the top-level lcm package. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tion A SolveSimulateStatePair in Regime.states is treated as a derived function under the state's name: validators accept it, get_all_functions and the regime processing inject its solve variant, the params template covers it, and the variables/grids builders exclude it from the solve state grid. Solving a model with pension wealth as a state pair yields the same value function as imputing it via an ordinary derived function. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two xfail tests pin the Milestone B behavior: in simulation a SolveSimulateStatePair is seeded from the initial conditions and evolved by its own transition (true value, not the AIME imputation), while the action choice still uses the imputed value (decide on imputed, account on true). Wiring the realized next-state path to read the carried true state will turn these green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A SolveSimulateStatePair is imputed from other states during solve and carried as a true, seeded state during simulate — "decide on imputed, account on true". Decision, feasibility, and regime transitions read the imputed value; the realized next-state and dataframe outputs read the carried true value. - engine.Regime gains simulate_only_grids plus simulate_state_names / simulate_grids views; empty for pair-free regimes, so those collapse to the solve sets and behave identically. - Regime processing registers pair.transition as next_<name> for the simulate phase only; the realized next_state drops the pair's solve function so the name resolves to the carried leaf instead of the imputation. - Seeding, initial-conditions validation (true seed required), and result metadata use the simulate state set, so the pair is seeded and surfaced as a column; the params template exposes the transition's params under next_<name>. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…compile Three gaps surfaced once a state pair carries a parameterized transition and a function pair lives in a regime's `functions`: - the function-output/grid-indexing validator now unwraps SolveSimulateFunctionPair entries into their solve/simulate variants instead of passing the pair to the AST scanner (which requires a Callable) - get_all_functions exposes a state pair's transition under `next_<name>`, so Series-parameter conversion resolves its parameters against the same name the params template uses - the AOT simulate lower-args seed the pair's simulate-only states, which are carried by the compiled next_state program but absent from the solve state space Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The page framed distributed=True as communication-free *only* on never-transitioning axes, with transitioning axes 'swamping' the compute. A transitioning discrete axis in fact shards at near-full speed: its cross-shard probability mass contracts via an output-sized all-reduce, not the V-array-sized all-gather that makes continuous-axis sharding the rejected case. Refine the one-line model, the knobs table, the section heading, the mechanism paragraph, the worked example, and the checklist to match, and add the measured transitioning-shard data point. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A target regime that shares nothing but a carried state pair with its source (retirement keeps only pension wealth) received no transition entry at all: - `_get_reachable_targets` counted the pair toward the target's state needs, but a pair never has a `next_<name>` entry among the ordinary transitions, so the subset check excluded every pair-carrying target not explicitly named in a per-target dict. - `_extract_transitions_from_regime` drops targets with an empty ordinary-transition dict, and the pair augmentation only visited existing entries — so the pair's `next_<name>` was never registered and the simulation silently froze the carried value on the crossing. Fix both layers: pair states count as self-covered for reachability, and the augmentation registers a fresh `next_<name>`-only entry for every reachable pair-carrying target absent from the ordinary set. Solve-phase transitions are untouched — in solve the pair is a derived function in the target regime, not a handed-over state. A regime whose transition reads no per-subject state or action (the pair-only target's own onward transition) produces one unbatched probability distribution; `draw_key_from_dict` now broadcasts it across the subjects' keys instead of vmapping over the wrong axis. The two processing-seam tests and the end-to-end handover test are the coverage guard; a runtime checker would duplicate the registration logic it guards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A malformed `SolveSimulateStatePair` previously passed `Regime(...)` untouched and only failed deep inside processing with an opaque error. `_state_pair_field_errors` now enforces the pair contract loudly: - `solve` must be callable (the solve-phase imputation), - `transition` must be a plain deterministic callable — `MarkovTransition` is not supported for state pairs, - `grid` must be an LCM grid, without `batch_size` or `distributed` (the grid is the simulate-phase domain of a carried per-subject value, not a solve axis those knobs apply to), - a terminal regime cannot carry a pair (no next period to carry it into; rejecting it also keeps the auto-registered `next_<name>` out of regimes that must not have transitions). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A `SolveSimulateStatePair` wrapping a `DiscreteGrid` was invisible to the pandas helpers, which iterate `regime.states` raw: a discrete pair column in an initial-conditions DataFrame failed label-to-code conversion, and the pair was absent from the categorical lookup used for code-to-label mapping. `_state_grids_unwrapping_pairs` replaces each pair by its inner grid at all three discovery sites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The test double assembled its own function mapping, which had drifted from `Regime.get_all_functions` (it dropped a state pair's `next_<name>` transition). The mock now normalizes its loose fields (`None`-valued states, missing regime transition) and delegates to the real method, so the key set can never drift again; a regression test pins the equality. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase-dependent regime data previously lived in unmarked fields (`grids`, `variables` — the solve sets) with simulate views bolted on as properties, so a call site reading the unmarked name silently inherited solve semantics. Two review findings were exactly that pattern: the initial-conditions feasibility check iterated the solve state set against simulate-phase functions, and the to_dataframe code-to-label mapping looped the solve grids so a discrete pair's column had codes but no labels. `Regime` now carries only phase-invariant data (name, terminal, active_periods, regime_params_template, stochastic_state_transitions, resolved_fixed_params) plus two frozen phase objects: - `solution: SolutionPhase` — solve variables and grids (pairs absent; productmap order), the compiled solve function sets, and `state_action_space()`. - `simulation: SimulationPhase` — carried variables (solve states plus pairs, appended — NOT a productmap order), grids including each pair's domain, `pair_state_names`/`pair_grids`, and the compiled simulate function sets. Every read names its phase in the access path, so a wrong-phase read is visible in the diff line itself. The unmarked accessors and the `SolveFunctions`/`SimulateFunctions` containers are deleted with no deprecation; old pickled results are not loadable (all persisted artifacts are cheaply recreatable). Behavior-preserving: the V-array axis-order regression test pins the solve dispatch, and the to_dataframe column order is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`SimulationPhase.functions` carried each state pair's solve imputation, so simulate-phase consumers silently read the imputed value: a `to_dataframe` additional target computed from the AIME imputation instead of the carried wealth, and the initial-conditions feasibility check validated constraints against the imputation while the seeded true value could be infeasible. The published simulate function set now drops the pair imputations (the names are leaves fed with carried values), and the feasibility check iterates the simulate state set so seeded pair values reach the constraints. Only the decision functions — Q_and_F/argmax and the regime-transition probabilities — keep the imputation: the agent decides on the value the solved policy was computed for. The escape hatch for users wanting the imputed value as a simulate output (declare `solve` under a second name in `functions`) is documented on `SolveSimulateStatePair`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase becomes a broadcast dimension of the regime spec: a bare slot value applies to both phases, Phased(solve=..., simulate=...) gives each phase its own variant. normalize_regime_phases expands every slot into per-phase RegimePhaseSpec slices (solution / simulation), applies the per-slot grammar, and aggregates violations into a single RegimeInitializationError raised at Regime construction. Grammar highlights: - functions and state_transitions accept Phased; constraints, actions, and derived_categoricals reject it with an explanation (a phase-variant feasible set or action menu would let the simulated argmax range over choices V was never computed for). - states accept Phased(solve=callable, simulate=Grid) - the carried state, generalizing SolveSimulateStatePair with the law of motion in the regular state_transitions slot; all other matrix cells reject. - transition accepts Phased with matching stochasticity; None variants reject (terminality is phase-invariant). Phased is outermost-only. - A SolveSimulateFunctionPair in constraints now rejects loudly at construction (previously crashed opaquely inside dags). The normalizer also desugars the two legacy pair containers, making it the single phase-resolution site; get_all_functions, the params template, and _process_regime_core resolve Phased like the legacy pairs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
process_regimes normalizes each user regime once and threads the per-phase spec into both phase builders: - Nested transitions come from one extraction (_extract_phase_transitions) run per phase slice against that phase's state sets. The simulate slice holds every carried state and its law of motion as an ordinary simple transition, so carried-only hand-over targets fall out of the uniform reachability rule and the pair-specific augmentation (_augment_nested_transitions_with_state_pairs) is deleted. - _process_regime_core receives phase-resolved functions, constraints, and state transitions instead of resolving pair containers itself; the simulate decision pool is the simulation slice plus each carried state's solve-phase imputation (decide on imputed), and the published pool strips the imputations as before. - SimulationPhase.pair_state_names becomes carried_only_state_names and pair_grids becomes carried_grids; variables.state_pair_grids becomes carried_state_grids and covers Phased declarations. Behavior-preserving for the existing suite; carried states declared via Phased(solve=..., simulate=Grid) now flow through the same engine path as the legacy SolveSimulateStatePair. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A `Phased(solve=..., simulate=...)` entry in `state_transitions` gives an ordinary grid state different laws per phase (e.g. an approximate law during backward induction, the exact law in simulation): - The simulate-phase next_state is always built from the simulation slice's transitions, so the realized panel follows the simulate law while the policy was solved (and the decision is made) under the solve law. Q_and_F keeps the solve transitions in both phases - the continuation valuation belongs to the solved V. - The params template unions both variants' parameters under `next_<state>` (same rule as phase-variant functions). - A stochastic (`MarkovTransition`) variant inside a `Phased` law is rejected with "not yet supported" - per-phase stochasticity needs the validation-metadata path to become phase-aware first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`transition=Phased(solve=plan, simulate=realized)` gives each phase its own regime transition: the solve variant prices the continuation (used by Q_and_F in both phases - V was solved under it), the simulate variant drives the realized per-subject draw. Variants must have matching stochasticity; the params template unions both variants' parameters under `next_regime`. Default change for carried states: the realized draw is now built against the published pair-free pool, so under a bare broadcast (`f == Phased(solve=f, simulate=f)`) it reads each carried state's TRUE carried value rather than the solve-phase imputation. The decision (argmax over the solved policy) still reads the imputation. Decide-on-imputed for the draw stays expressible by declaring the imputation under a second name in `functions`. Plumbing: the simulate crtp accepts the carried states (extended grids in the accepting-all wrapper); the membership step feeds current-period carried values from the state carrier - the draw runs before the carrier advances - and the AOT lower-args seed them like next_state's. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Phased grammar subsumes both containers (names never shipped in a
release):
- functions={"f": Phased(solve=..., simulate=...)} replaces
SolveSimulateFunctionPair.
- states={"s": Phased(solve=impute, simulate=Grid)} plus a regular
state_transitions law replaces SolveSimulateStatePair.
Removed with them: the pair-specific validators (the slot grammar in
the normalizer covers their cases), the pair branches in the params
template, collect_state_transitions, variables, and pandas label/code
discovery, and the "pair" vocabulary in engine docstrings (now
"carried states"). Tests migrate to the Phased spellings;
test_solve_simulate_state_pair.py becomes test_carried_states.py with
the validation cases living in test_phased.py.
Docs: beta_delta switches to Phased; AGENTS.md documents the phase
grammar, the normalizer boundary, and the noun-vs-verb naming rule
(solution/simulation for artifacts, solve/simulate for acts and
variant selection).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The notebook explained the phase split in terms of the deleted pair container; the prose now introduces phase as a broadcast dimension and names the solve/simulate variants where each phase is discussed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
normalize_regime_phases (the entry point) leads, followed by its return type PhasedRegimeSpec, then the RegimePhaseSpec component; private helpers stay below. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
terminal becomes a property derived from the solution slice (terminality is phase-invariant by the slot grammar, so a stored flag was a redundant degree of freedom). The unconsumed actions / derived_categoricals copies are dropped - those phase-invariant slots stay on the user regime, and their model-level broadcast is unchanged in this PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…stages Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`finalize_regimes` (src/_lcm/regime_building/finalize.py) merges model-level derived categoricals, injects the default H into non-terminal regimes, and validates completeness via `regime.replace(...)` — so `model.user_regimes` holds plain `lcm.regime.Regime` instances. Internal signatures mark the post-merge form with the erased alias `FinalizedUserRegime`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`SolutionPhase` and `SimulationPhase` publish `state_names`, `action_names`, and `discrete_state_names` as direct properties; the `Variables` bundle behind them is the private `_variables` field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…_params Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- transitions.ipynb: three regime-transition forms (bare callable / MarkovTransition / per-target dict with key-set reachability), fixed_transition for fixed states, and a worked two-regime "Cross-Regime Transitions" example covering reachability narrowing, the coverage rule, entering laws, dropped states, and cross-grid laws with fixed_transition inside per-target cells. - regimes.ipynb: anatomy table without the None spelling; fixed-state example via fixed_transition. - defining_models.md: "Model-Level Regime Slots" section (exactly-one-level rule, None masking, DAG pruning, model.pruned_variables, sharding rules). - NEW explanations/phase_grammar.ipynb: the phase-broadcast grammar with a runnable carried-state model; registered in the toc and index. - architecture.md: finalize-at-model-build pipeline description and the regime_building module map (broadcast, finalize, phases, canonicalize). - CHANGES.md: entry for the new interfaces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
After `canonicalize_regimes`, every non-terminal regime's transition is a `Mapping[RegimeName, cell]` (terminal stays `None`): user per-target dicts pass through, and coarse forms (bare callable or `MarkovTransition`) map every regime to one shared `_CoarseTransitionCell`, so the engine keeps evaluating the underlying object once and indexing per target — cells are views on one evaluation, never per-cell re-evaluations. The per-phase transpose becomes homogeneous: each target's bundle carries its `next_regime` cell next to its `next_<state>` laws, killing the `RegimeName | TransitionFunctionName` union and the top-level `"next_regime"` special key. The engine dispatch (coarse fast path via `_wrap_regime_transition_probs` vs granular cell assembly) now derives from the cell type of the canonical mapping. The params template keeps reading the user spec, so template keys and validation messages are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- `_grid_mapping_errors` / `_callable_mapping_errors` take keyword-only arguments (project convention for multi-argument functions). - `RegimePhaseSpec.stochastic_regime_transition` docstring covers the per-target dict case the flag actually encodes. - `_model_slot_value_errors` docstring describes the `None`-only check it performs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The params template mirrors the user's coarseness with per-target transition params as a nested branch `template[regime][target][func][param]` — the target is a regime name, not a mangled `to_<target>_…` prefix — so param qnames (`regime__target__func__param`) parallel engine function qnames. Broadcast laws keep their single coarse `next_<state>` key. - `create_regime_params_template` nests `<func>__<target>` collection entries under the target and rejects target/function top-level collisions - `broadcast_to_template` resolves 4-part qnames at four levels: exact / coarse-function (drops the target) / regime / model - `_extract_param_key` / `_rename_params_to_qnames` walk the nested branch; granular `next_regime` cells key their params per target - `convert_series_in_params` maps `target__func__param` directly onto the engine's `func__target` function key; the `to_`-prefix resolver is deleted - `_remove_fixed_params_from_template` and `get_params_template` recurse, so fixed-param trimming and the readable template handle arbitrary depth - `UserParams` / `UserFacingParamsTemplate` / `RegimeParamsTemplate` admit the target level Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d categoricals Canonical flat params key every state-transition-law param per target (`<target>__<law>__<param>`), matching the engine's target-prefixed function qnames: - the engine binds transition-law params under the function qname uniformly; for coarse laws the param names are read off the coarse template branch (`names_key` on `_rename_params_to_qnames`) - `materialize_granular_transition_params` expands a coarse user value to one entry per reachable carrying target, every target sharing the same leaf object (`Regime.granular_param_expansions` records the prefixes per regime, across both phases); fixed params materialize before partialling - `cast_params_to_canonical_dtypes` casts once per distinct input object, so values broadcast into several slots stay one shared leaf - the DAG builders use the engine's binding vocabulary (`_engine_flat_param_names`) instead of the template names - the transition validator reads any target's shared binding for coarse stochastic laws - a coarse regime transition stays a single shared evaluation: it keeps its one `next_regime` entry and rejects per-target params; missing and unknown keys are reported together - model-level `derived_categoricals` follow the exactly-one-level rule: a name declared at both levels is an ambiguity error, also when the grids match - docs: "Per-target parameters" section in the transitions guide, CHANGES.md entry, `to_<target>_…` spelling removed everywhere Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Check out this pull request on See visual diffs & provide feedback on Jupyter Notebooks. Powered by ReviewNB |
Benchmark comparison (main → HEAD)Comparing
|
Three changes, all bit-identical (same operations, cached or hoisted): - the per-subject regime draw is a module-level jitted function (`_draw_random_regime_ids`), traced once per shape instead of rebuilding and re-tracing an un-jitted vmap on every period-regime call - `vmapped_unravel_index` (optimal-action lookup) is jitted with the grid shape static, removing the second per-call vmap re-trace in the inner loop - the params-completed state-action space is built once per regime per solve/simulate call instead of once per period-regime iteration — runtime grid completion (e.g. process gridpoint computation) rides on it and was rerunning identically every period On the Precautionary-Savings simulate benchmark workload this removes ~40% of host-side function calls; the draw re-trace was the source of the simulate-runtime regression flagged by the GPU benchmark. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…g-beliefs example
- rename `target_name` / bare `target` variables to `target_regime_name`
across `regime_building/` and `simulation/`; prose spells the granular
form `{target_regime: ...}` / `Mapping[target_regime, law]`
- the unbound-mask error drops its `fixed_transition` migration hint
- docs: coarse/granular vocabulary defined where the transition forms are
introduced; carried states defined as a term in the phase-grammar page;
exactly-one-level rule, `None`-masking, and the cross-regime fixed-point
pruning spelled out in defining_models.md; `FinalizedUserRegime`
described in plain words (an alias the type checker treats as plain
`Regime` — pure signature documentation)
- new phase-grammar section "Wrong beliefs: different transitions in solve
and simulate" with the `dags.rename_arguments` pattern (per-side renames
split a param across phases; a shared name stays one shared parameter),
pinned by three tests with analytic income paths
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three markdown cells held their source as a single newline-joined string instead of a JSON array of lines, which renders fine but makes the cell diff unreadable. Split them into per-line arrays (rendered text unchanged). House convention: one JSON array element per line. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hmgaudecker
commented
Jun 13, 2026
- Rename _NUM_PARTS_PER_TARGET_PARAM to _NUM_PARTS_PER_TARGET_SPECIFIC_PARAM in params/processing.py and add a comment explaining the qname part counts. - Docstring: "Target level" to "Target-regime level", target_0 to target_regime_0, and "the targets" to "the target regimes" — "target" alone was ambiguous. - transitions.ipynb: "target gets what" to "target regime gets what", "target-blind" to "target-regime blind". - Revert the target_name to target_regime_name rename in the mechanical _add_raw_transition helper (it paired a verbose key name with a terse target_value); the validated stochastic path keeps target_regime_name. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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; #374 is retargeted onto this branch.
Continues the house pattern into the params system. Coarse vs. granular: a granular spelling names each target regime explicitly, a coarse spelling is target-blind and means "the same thing toward every (reachable) target"; the canonical form the engine stores is always granular, coarse user spellings are sugar that broadcasts into it. #373 applied this to state and regime transitions — this PR applies it to parameters: the target regime is a genuine level of the params tree, and canonical (engine-side) params always store the target-granular form.
Template & user spellings
Per-target transition params nest under the target regime —
template[regime][target_regime][func][param]— replacing the mangledto_<target>_…keys. Param qnames parallel engine function qnames.UserParamsresolve at four levels, most to least specific:{regime: {target_regime: {func: {param: v}}}}: a value for that target's law only{regime: {func: {param: v}}}: one value broadcasts over the law's targets{regime: {param: v}}{param: v}Exactly one level per param; multi-level specs are ambiguity errors. Missing and unknown keys are reported together.
Canonical storage
<target_regime>__<law>__<param>), matching the engine's target-prefixed function qnames. A coarse user value materializes one entry per reachable carrying target, all targets sharing one leaf object (cast_params_to_canonical_dtypescasts once per distinct input object) — bit-identical arithmetic, no per-target copies, identity-based dedup downstream.next_regimeentry, per-target params rejected.Unification rounded out
derived_categoricalsfollow the exactly-one-level rule of the other model-level slots: a name declared at model level and regime level is an ambiguity error, also when the grids match (the silent merge-if-equal is gone).Performance
unravel_indexare jitted at module level instead of re-tracing an un-jitted vmap on every period-regime call, and the params-completed state-action space is built once per regime per solve/simulate call instead of every period (runtime grid completion — e.g. process gridpoint recursions — rode on it). Bit-identical results; ~40 % fewer host-side function calls on the Precautionary-Savings simulate workload. The draw re-trace was the source of the simulate-runtime alert on The regime grammar: phase broadcast, canonical target-granular forms, model-level slots #373's benchmark run.Docs
to_<target>_…spelling removed everywhere.Verified: full CPU suite green, ty clean, prek clean; aca-model suite green against this tip with no downstream changes needed.
🤖 Generated with Claude Code