Repository navigation
Declare transitions by age, fold the transitory medical shock, move execution controls to Model policy - #21
Merged
Merged
Conversation
`build_actions` and the nongroup builder narrowed the M1 regime for NBEGM, dropping `buy_private` and `labor_supply` as actions and fixing their former outputs to constants. That made the solved model a different model from the one brute force solves: the household lost its coverage and hours choices. The regime now declares whichever choices its structure affords under every solver. pylcm's ride-along discrete envelope is written over a single action's grid and refuses a regime declaring several, so model build under NBEGM raises until that arity widens. The refusal is the honest outcome — a solver that cannot carry a choice refuses the regime rather than being handed a narrower one. Tests split accordingly: the regime-level and solver-config assertions stay green, model-build assertions become strict xfails naming the arity, and one green test pins the refusal itself so the xfails flip when the arity widens. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_018yHhuFzqsDw1MhdB1i2Ljm
The M1 regime declares both buy_private and labor_supply, and pylcm's envelope now branches over the Cartesian product of a regime's discrete action grids, so the eight strict xfails pinned to the old single-action refusal all XPASS. Removes them and converts the refusal test into the capability it replaced: the model builds with both actions live. 25 passed where 15 had failed. Every one of the 15 was a stale expectation -- including the five policy-variant builds, which are reported as FAILED rather than XPASS under strict=True and so read like real build failures until the XPASS(strict) marker is inspected. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_018yHhuFzqsDw1MhdB1i2Ljm
`solver="nbegm"` attached the solver to one regime and left the other 17 on brute force, so the model's stated solver was not the one that produced most of its result. Every living regime now gets the NBEGM config, and the retiree and tied builders carry the savings-form budget (`resources`, `savings`) that the solver's contract reads -- without it those regimes cannot be built at all. The build-time affinity and interval-constancy probes cannot run on the added regimes: they differentiate the budget on scalar inputs and `assets_and_income.capital_income` declares `rate_of_return: ScalarFloat`, which the probe's one-element array violates. The probes fall back to `assume_declared` and warn, so NB-EGM's exactness precondition is asserted rather than checked there, and the solve needs validating against an independent reference. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_018yHhuFzqsDw1MhdB1i2Ljm
The probes run on the first solve against the model's complete parameter vector, so the tax tables and threshold schedules the budget reads are the model's own rather than synthesized stand-ins. Affinity and constancy are checked rather than asserted.
pylcm takes the liquid roles from the regime: a `ConsumptionSavingsRegime` names the liquid state, the action paid from resources, the resources node and the post-decision state, and the solver carries numerical configuration only. `ACA_LIQUID_MARGIN` states that once for the three living-regime builders, which now route through `build_alive_regime`; a brute-force regime has no `resources` or `savings` node to name and stays a plain `Regime`. The role assertions move with the declaration: they read `regime.liquid`, which is public API, rather than the bound solver's attributes.
Both callers of `build_model_constraints` still said DC-EGM gets no broadcast constraint. It is declared under every solver: the EGM solve enforces the limit through the savings grid's lower bound, but forward simulation re-decides consumption by an argmax over the consumption grid and needs the explicit feasibility mask. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_018yHhuFzqsDw1MhdB1i2Ljm
The xfail reason described the assets-law chain, but the build stops earlier: pylcm's DC-EGM refuses the broadcast borrowing constraint for reading continuous variables. Both blockers are now listed in the order they fire. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_018yHhuFzqsDw1MhdB1i2Ljm
Bumps the github-actions group with 1 update in the / directory: [actions/setup-python](https://github.com/actions/setup-python). Updates `actions/setup-python` from 6 to 7 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](actions/setup-python@v6...v7) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] <[email protected]>
pylcm's `Model.solve` returns a `SolutionResult` and `Model.simulate` takes `solution=` in place of `period_to_regime_to_V_arr=`. The value mapping these tests read is `SolutionResult.values`; passing `None` for the old keyword meant "solve inside simulate", which is the new default. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01Y7QCx9tkGD3TqwNw681QwD
Forward ExecutionConfig through baseline, ACA and benchmark factories. Derive omitted accelerator budgets from selected JAX allocator limits and reject retired GridConfig execution fields. Preserve economic grids and numerical solver settings. Validation against pylcm f6d41d54: exact ASV CPU preflight, focused compatibility tests, Ruff, ty and repository hooks pass. DC-EGM construction still exposes an upstream continuation-template placement failure on three CPU devices.
…rence Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
Leisure is s*logaddexp(F/s, available/s) with F = 1e-5 * time_endowment, so float32 felicity and its consumption derivative stay finite when work costs exceed the endowment, while cells away from the floor are unchanged to ~1e-8. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
…te (#18) Co-authored-by: Claude Opus 5.5 <[email protected]>
Stage transitions use `ByAge` schedules over `AgeRange` constants from the config, the dead regime is terminal with `regime_transitions=None`, and a coverage test checks every (age, regime) node. CI installs the matching pylcm. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
Both factories declare INITIAL_REGIMES (ages 51-61 x the five *_inelig_canwork regimes) instead of forwarding an optional root mapping. Declarations use the keyword-only AgeRange/ByAge/MarkovTransition signatures. simulate_with_dense_index drops initial-condition rows older than the last admissible start age and raises on any kept row outside model.initial_nodes. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
INITIAL_REGIMES spans ages start_age..last_start_age (51-60, the baseline estimation sample) in the five *_inelig_canwork regimes, 50 pairs. Starts at age 61 or later are dropped by select_admissible_starts and rejected at admission. The solved domain stays 181 nodes: age-61 entry regimes are reached from age 60. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
The expansion track tests MAGI only, for every household under 65, regardless of health or assets, as the draft's post-ACA eligibility rule states. The aged keep the categorical track with its asset test. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
…bility Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
…xpansion Retiree and tied HIS transitions, and the pension imputation's target HIS, read categorical (SSI) Medicaid eligibility rather than the two-track eligibility. The draft assumes the ACA does not affect employer-provided insurance; an expansion-eligible retiree or tied household keeps its employer coverage, with Medicaid paying on top of its OOP. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
Tests for the PIA carried across a claim-age adjustment, the earnings-test credit, age x health spousal income, disabled AIME accrual, frozen AIME at forced-claim ages, the taxable-maximum cap after accrual, the end of pension accrual at 71, the household's own consumption floor and the ACA surtaxes. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
Each living regime's per-target probability cells move from `Regime(regime_transitions=...)` into `Transition(targets=..., law=...)` under `Model(edges=...)`; `dead` has no outgoing edges and is terminal. Terminal checks read `model.graph.laws[name].terminal`. Edges, targets and probability cells are unchanged. Standalone CI installs the pylcm revision that carries the graph-only transition API. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
The regime-transition law reads `survival_probs` and `prob_disabled_next`; pylcm resolves an edge-declared callable's parameters under `params["edges"][source]`, never under the source regime. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
`get_benchmark_params` moves each source regime's `survival_probs` and `prob_disabled_next` from `fixed_params[source]` to `fixed_params["edges"][source]`, where pylcm resolves the parameters of the regime-transition law declared in `Model(edges=...)`. The law reads the same per-source Series as before; the snapshot file is unchanged. The NB-EGM template walk skips the `edges` root, whose entries are source regimes rather than regime functions. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
The snapshot stores `survival_probs` and `prob_disabled_next` under `fixed_params["edges"][source]`, as `aca_slurm.assemble_fixed_params` writes them, so `get_benchmark_params` returns the fixed params as stored. Regenerated with aca-dev `scripts/regen_benchmark_params.py` against aca-data 652f5c6b, aca-slurm 93472b9 and pylcm db04e088; every other leaf equals the previous snapshot. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
pylcm derives a per-target law's targets from its cases and refuses supplied targets that name fewer ages than the law. Each living regime's law is a `ByAge` schedule whose cases name exactly the destinations its source ages reach, so the edges declare no `targets` of their own. The parameter paths are unchanged: a schedule adds no level below the source. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
…nity-rated The ACA overlay replaces the non-group premium with the community-rated plan premium, but left the risk-rated premium's zero-profit intercept in the regime. Nothing reads it any more, yet its inputs (markup, minimum, predicted-cost loading and the insurer-cost sample) stayed required parameters of the ACA model, which assembles none of them. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
…s from the graph pylcm now publishes `Model.initial_nodes` as an `InitialNodes` declaration and the expanded `(age, regime)` pairs on `Model.graph.initial_nodes`. The entry ages are declared with `InitialNodes(by_age=...)`, and admissible-start filtering reads the pairs from the graph. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
`GridConfig.fold_hcc_transitory` declares the transitory shock `fold=True`. Nothing a period computes reads last year's transitory shock, so pylcm then draws it on the transition only: living regimes keep the lagged persistent shock, which prices the private premium, and lose the transitory axis. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
`GridConfig.subtract_hcc_bill` declares the asset law toward every target as `lcm.SubtractedBill`: assets before the out-of-pocket bill minus `oop_costs`, conditioned on marital status, health, Medicaid eligibility, the private-cover choice and the ACA cost-sharing bracket. With `fold_hcc_transitory`, pylcm then averages each target's value over the transitory medical-cost shock once per period instead of at every source point. The ACA cost-sharing scale is split at its income bracket, which takes finitely many values, so the bill can be conditioned on it; the scale is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
The shock is `negative_oop_costs`, the out-of-pocket bill negated, added to the assets before the bill. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
…-actions-696b649f44' into combine/open-prs
…GM law - The ACA nongroup regimes take the cost-sharing scale from the bracket, so the identity asserts name `cost_sharing_scale_of_bracket`. - The EGM model-creation tests build from the benchmark grid config without `subtract_hcc_bill`, which needs the brute-force asset law. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
pylcm folds a shock state only under grid search. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW
This was referenced Oct 22, 2026
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.
Regime transitions
ByAgeschedules. The ages areAgeRangeconstants taken from the config.Regimefield isregime_transitions. The dead regime is terminal (regime_transitions=None).Medical costs
fold_hcc_transitory, removes the IID transitory medical-cost shock from the state space. The shock is then drawn only on the transition.subtract_hcc_bill, every living regime's asset law becomes resources before the bill plus the out-of-pocket bill as an additive shock (lcm.AdditiveShockTransition).Execution and solvers
Modelpolicy: a newexecution.pywithexecution_config_for_devices. The model factories no longer take population hints.SolutionResultsolve API, and the benchmark uses breakpoint grids.CI
actions/setup-pythonis updated from 6 to 7.Supersedes #16, #17, #19 and #20.
https://claude.ai/code/session_01Hs8mfXPYyEa24KaAp6BwCW