Fix outcome axis for cross-grid stochastic transitions#321
Merged
Conversation
Shock grid states (Rouwenhorst, Uniform, etc.) are continuous states that should be accepted as float columns in the DataFrame. Previously they were rejected as "unknown columns" because _collect_all_state_names excluded _ShockGrid instances. Split state name collection into required (non-shock states + age) and optional (shock grid states). Shock columns are accepted but not required, since the model draws fresh shock values each period. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shock grid states (Rouwenhorst, Uniform, etc.) are continuous states that should be accepted as float columns in the DataFrame. Previously they were rejected as "unknown columns" because _collect_all_state_names excluded _ShockGrid instances. Split state name collection into required (non-shock states + age) and optional (shock grid states). Shock columns are accepted but not required, since the model draws fresh shock values each period. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The type-assignment loop set discount type for indices 0–7 (low education) but not for indices 8–15 (high education). All high-education agents were incorrectly assigned to the low discount factor type. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fixed_params passed pd.Series raw to functools.partial, causing JAX TypeError during tracing. Runtime params already auto-convert via _maybe_convert_series. Add the same conversion to the fixed_params path using a callback pattern to avoid circular imports between model.py and model_processing.py. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
For per-target stochastic transitions that cross grid sizes (e.g. 3-state health → 2-state health), the outcome axis must use the target regime's grid, not the source's. Without this fix, the converted transition probability array has the wrong last dimension, causing a shape mismatch in jnp.average during Q_and_F evaluation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shock states were made optional in the initial commit, but this is wrong: AR(1) shocks depend on the current value for transitions, and observed persistent shocks (e.g., wage residuals) represent real data. Making them optional would silently fill with NaN. Simplify _collect_state_names to return a single set including all states (shocks and non-shocks alike). This is consistent with validate_initial_conditions in simulate(), which already requires them. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tion - Remove decorative section-separator comment in test file - Remove unused _PROBS_ARRAY constant - Add type annotations to _make_markov_model - Add Returns section to build_regimes_and_template docstring - Add _validate_param_types after Series conversion in fixed_params callback, matching the runtime params validation path Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Member
Author
Code reviewFound 3 issues (only changes relative to PR #308):
pylcm/tests/test_pandas_utils.py Lines 447 to 449 in ee49b73
Lines 656 to 668 in ee49b73
🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
- Update _build_outcome_mapping docstring to describe per-target transition case (target regime's grid for outcome axis) - Add ty: ignore for arr.shape on union type in cross-grid test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…alls The test compared two stochastic simulations without fixing the random seed, causing different MarkovTransition draws on macOS and Windows. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3 tasks
Benchmark comparison (main → HEAD)Comparing
|
- Change convert_series_in_params, array_from_series, and internal helpers to take regimes + ages + regime_names_to_ids instead of model: Model. This breaks the circular import between pandas_utils and model. - Change initial_conditions_from_dataframe to take regimes + regime_names_to_ids instead of model: Model (public API change). - Move _validate_param_types and _check_leaf to model_processing.py. - Remove convert_fixed_params callback from build_regimes_and_template; call convert_series_in_params and _validate_param_types directly. - Remove TYPE_CHECKING import of Model from pandas_utils.py. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…-mapping # Conflicts: # src/lcm/pandas_utils.py
…sion The parameter is needed when fixed_params contain pd.Series indexed by derived categorical outputs (DAG function results like is_married). Without it, convert_series_in_params cannot resolve those indices. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add missing `regimes` param to `initial_conditions_from_dataframe` docstring - Fix "Mapping" -> "Immutable mapping" for `regime_names_to_ids` in `build_regimes_and_template` docstring - Add test exercising fixed_params with pd.Series indexed by a derived categorical (DAG function output not in model grids) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
derived_categoricals describes outcome spaces of DAG function outputs — it belongs on Regime where the functions live. Model-level entries are broadcast to all regimes at init time (convenience sugar). Raises on conflict if a regime already has a different grid for the same key. - Add derived_categoricals field to Regime (Mapping[str, DiscreteGrid]) - Remove from solve() and simulate() signatures - Simplify pandas_utils chain: remove _resolve_categorical_entry, read derived_categoricals from regime directly - Add broadcasting tests (merge, match, conflict, coexistence) - Update docs and AGENTS.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- derived_categoricals accepts both categorical classes and DiscreteGrid; raw classes are normalized to DiscreteGrid in Regime.__post_init__ - Inline _maybe_convert_series and _maybe_convert_dataframe into solve() and simulate() — they were trivial one-liner wrappers - Remove max_compilation_workers and enable_jit from solve() calls (leaked from downstream PRs, not on this branch's solve_brute.solve) - Update docs and error messages to show raw class syntax Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Revert derived_categoricals type to Mapping[str, DiscreteGrid] (raw categorical class acceptance was premature) - Remove InvalidValueFunctionError try/except blocks that leaked from improve/lazy-diagnostics via stash pop - Remove unnecessary dict() conversion in _build_outcome_mapping - Restore DiscreteGrid(...) in docs and tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…teless - Add Model._process_params(): broadcast, convert Series, validate — replaces duplicated 6-line blocks in solve() and simulate() - Extract _apply_fixed_params() from build_regimes_and_template so the parent function has no variable reassignment Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…riable - _build_regimes_and_template_with_fixed_params calls process_regimes and create_params_template internally, so build_regimes_and_template has no variable reassignment - Rename invalid -> invalid_regimes for symmetry with valid_regimes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Consistent parameter order: ages, regimes, regime_names_to_ids across all def sites, call sites, and docstrings in model.py, model_processing.py, and pandas_utils.py. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both _resolve_categoricals and _build_discrete_grid_lookup now iterate regime.states and regime.actions symmetrically. Delete the redundant _build_discrete_action_lookup helper. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rams
`any(v for v in fixed_internal.values())` is False for params like
{"a": 0} or {"a": 0.0}, silently skipping partialling. Remove the
check entirely — _partial_fixed_params_into_regimes handles empty
dicts correctly.
Also rename internal_regimes/params_template to raw_* for clarity.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix enable_jit docstring: "jit" -> "JIT-compile", singular -> plural - Fix array_from_series regime_name arg: mention derived categorical lookup - Move import pytest to top-level in test_static_params.py - Include derived_categoricals in _resolve_categoricals else-branch (regime_name=None); add test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…-mapping # Conflicts: # tests/test_pandas_utils.py
…e-mapping # Conflicts: # src/lcm/pandas_utils.py # tests/test_pandas_utils.py
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mj023
approved these changes
Apr 14, 2026
Collaborator
mj023
left a comment
There was a problem hiding this comment.
Looks correct, nothing from my side.
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.
Summary
_build_outcome_mappingnow uses the target regime's grid for the outcome axis instead of the source regime's gridValueError: Axis must be specified when shapes of a and weights differ) inQ_and_F.py:217during solveRoot cause
_build_outcome_mappingextracted the state name from the qualified function name (next_health__post65→health) but looked it up in the source regime's grids. For cross-grid transitions, the source grid has more categories than the target, producing an array with the wrong last dimension.Test plan
test_convert_series_cross_grid_transition— 3-state source → 2-state target, verifies(n_ages, 3, 2)shapepytest -n 7)🤖 Generated with Claude Code