Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
118 commits
Select commit Hold shift + click to select a range
5e5d529
Let the NBEGM regime declare every choice its structure affords
hmgaudecker Aug 14, 2026
3b2a0b1
Consume pylcm's multi-action discrete envelope
hmgaudecker Aug 15, 2026
9a5fe8a
Solve every living regime with NB-EGM
hmgaudecker Aug 15, 2026
ace111b
Let pylcm's NBEGM probes check the ACA budget
hmgaudecker Aug 16, 2026
4f25818
Declare the liquid Euler margin on the EGM regimes
hmgaudecker Aug 19, 2026
221bf4f
Describe the borrowing constraint's actual solver coverage
hmgaudecker Aug 19, 2026
8466aac
Name the DC-EGM blocker that actually fires
hmgaudecker Aug 19, 2026
a2ad958
FIX Declare the ACA borrowing limit structurally
hmgaudecker Aug 20, 2026
328e371
Migrate ACA benchmark to breakpoint grids
hmgaudecker Aug 21, 2026
f61cff9
ENH Forward NB-EGM workspace controls
hmgaudecker Aug 30, 2026
5d1d479
ENH Allow explicit fp32 ACA runs
hmgaudecker Sep 1, 2026
94977a3
FIX Gate the experimental NB-EGM planner
hmgaudecker Sep 2, 2026
3af83d0
MAINT Migrate the tests to pylcm's SolutionResult solve API
hmgaudecker Sep 3, 2026
b941507
ENH Move ACA execution controls to Model policy
hmgaudecker Sep 8, 2026
735a250
API Remove constructor population hints from model factories
hmgaudecker Sep 12, 2026
4ec7ce0
TST Pin float32 felicity at the leisure floor against a 60-digit refe…
hmgaudecker Sep 25, 2026
4f37ec2
ENH Bend leisure onto a positive floor with a smooth max
hmgaudecker Sep 25, 2026
05c4a43
CI Install pylcm at the commit aca-dev pins
hmgaudecker Sep 25, 2026
3bc89f7
Floor leisure at a small positive value so float32 utility stays fini…
hmgaudecker Sep 26, 2026
fd30823
Declare regime transitions by age with `regime_transitions`
hmgaudecker Sep 28, 2026
bf4c654
Test against pylcm without Regime.active
hmgaudecker Sep 28, 2026
8f6ea5d
Declare the 51-61 entry regimes as the model's starting pairs
hmgaudecker Sep 28, 2026
ad38653
Narrow the admissible starting ages to 51-60
hmgaudecker Sep 28, 2026
57eda22
Test the per-type transformed bequest weight in the dead regime
hmgaudecker Oct 1, 2026
a13aeca
TST Expect the ACA Medicaid expansion to reach the disabled under 65
hmgaudecker Oct 1, 2026
b69fe0c
Extend the ACA Medicaid expansion to the disabled under 65
hmgaudecker Oct 1, 2026
28e4d72
TST Expect employer coverage to survive expansion-only Medicaid eligi…
hmgaudecker Oct 1, 2026
b0b5ed1
Keep employer coverage for households eligible only through the ACA e…
hmgaudecker Oct 1, 2026
3bbcc5a
TST Pin budget, Social Security and pension rules at production values
hmgaudecker Oct 1, 2026
7fd408c
Test health-dependent mortality, terminal age 95 and the sinh asset grid
hmgaudecker Oct 1, 2026
1de2466
Derive the per-type bequest weight from the bequest MPC in the dead r…
hmgaudecker Oct 1, 2026
e7925ad
TST Expect Medicaid to replace non-group cover and pay Medicare premia
hmgaudecker Oct 1, 2026
b9358a6
TEST Pin private premium pricing, couples' cost sharing, DI Medicare …
hmgaudecker Oct 1, 2026
5a5806f
Let Medicaid replace non-group cover and pay Medicare premia
hmgaudecker Oct 1, 2026
b3e852f
FIX Read spousal income by age and own health
hmgaudecker Oct 1, 2026
e9436fc
FIX Credit back only benefits the earnings test withholds
hmgaudecker Oct 1, 2026
1975cea
FIX Cap AIME at the taxable maximum after labor accrual
hmgaudecker Oct 1, 2026
1c06b65
FIX Stop pension accrual at the pension claiming age 71
hmgaudecker Oct 1, 2026
83f2015
Name the non-group premium after Medicaid `medicaid_adjusted_premium`
hmgaudecker Oct 1, 2026
5b56aac
FIX Index and accrue a disabled agent's AIME before the DI continuity…
hmgaudecker Oct 1, 2026
a4eccf2
FIX Freeze AIME once claiming is forced
hmgaudecker Oct 1, 2026
3ae5518
TST Expect a community-rated ACA non-group premium
hmgaudecker Oct 1, 2026
14b312e
FIX Require consumption at or above the household's own floor
hmgaudecker Oct 1, 2026
c84e56b
FIX Add the ACA surtaxes on unearned income and earnings
hmgaudecker Oct 1, 2026
e76ee7a
Community-rate the ACA private non-group premium
hmgaudecker Oct 1, 2026
fb842c5
DOC Describe the ACA plan premium and Medicaid in the non-group regimes
hmgaudecker Oct 1, 2026
117b7bf
FIX Price private cover per the draft; double couples' cost sharing; …
hmgaudecker Oct 1, 2026
0c73bf1
FIX Carry the pension-corrected PIA across a claim-age adjustment in …
hmgaudecker Oct 1, 2026
d9e4fe1
CI Install pylcm at the main commit that aca-dev pins
hmgaudecker Oct 1, 2026
0d4c0af
Merge remote-tracking branch 'origin/fix/smooth-max-leisure-floor' in…
hmgaudecker Oct 1, 2026
6dd9f4f
Merge remote-tracking branch 'origin/fix/smooth-max-leisure-floor' in…
hmgaudecker Oct 1, 2026
93bf0a1
Merge remote-tracking branch 'origin/fix/smooth-max-leisure-floor' in…
hmgaudecker Oct 1, 2026
6252bc6
FIX Drop the withheld fraction from the forced regimes' benchmark params
hmgaudecker Oct 1, 2026
dee781b
Merge remote-tracking branch 'origin/fix/smooth-max-leisure-floor' in…
hmgaudecker Oct 1, 2026
562b8e1
Make mortality depend on health, keep people alive through 95, use a …
hmgaudecker Oct 1, 2026
2c28d15
Merge origin/fix/smooth-max-leisure-floor into fix/draft-processes
hmgaudecker Oct 1, 2026
7b0ac93
TST Build ACA test models without the risk-rated premium parameters
hmgaudecker Oct 1, 2026
3c1c2d8
Merge remote-tracking branch 'origin/fix/draft-budget' into fix/draft…
hmgaudecker Oct 1, 2026
cbcf28d
TST Expect the ACA overlay to charge the ACA surtaxes
hmgaudecker Oct 1, 2026
1bd0a20
Merge origin/fix/draft-prefs into fix/draft-model
hmgaudecker Oct 1, 2026
69923ee
Charge the ACA surtaxes under every ACA variant
hmgaudecker Oct 1, 2026
38c8aee
Merge origin/fix/draft-budget into fix/draft-model
hmgaudecker Oct 1, 2026
c05e03e
MAINT Regenerate the benchmark snapshot from the merged branches
hmgaudecker Oct 1, 2026
78a46ce
FIX Read the disability probability from a dedicated (age, health) pa…
hmgaudecker Oct 1, 2026
2b410bd
TST Share the ACA fixed-params helper with the NBEGM ACA tests
hmgaudecker Oct 1, 2026
eb9a2f7
Merge origin/fix/draft-processes into fix/draft-model
hmgaudecker Oct 1, 2026
77c5c43
TEST Pension wealth stays undivided where nobody survives
hmgaudecker Oct 1, 2026
3155f63
FIX Leave pension wealth undivided where nobody survives
hmgaudecker Oct 1, 2026
50bc566
MAINT Regenerate the benchmark snapshot after the processes merge
hmgaudecker Oct 1, 2026
41bc2f6
Merge remote-tracking branch 'origin/fix/draft-processes' into fix/dr…
hmgaudecker Oct 1, 2026
cf3fe2c
TST Pass categorical Medicaid eligibility to the transitions in the m…
hmgaudecker Oct 1, 2026
8f72005
Merge origin/fix/draft-aca into fix/draft-model
hmgaudecker Oct 1, 2026
e821fa8
MAINT Regenerate the benchmark snapshot after all merges
hmgaudecker Oct 1, 2026
e265120
Merge remote-tracking branch 'origin/fix/draft-aca' into fix/draft-pr…
hmgaudecker Oct 1, 2026
6d26b22
TEST Pin couples' doubled deductible and OOP maximum under the ACA plan
hmgaudecker Oct 1, 2026
b87ac7c
FIX Double couples' deductible and OOP maximum under the ACA plan
hmgaudecker Oct 1, 2026
922b545
MAINT Carry the private-premium and disability inputs in the benchmar…
hmgaudecker Oct 1, 2026
7312f19
Merge origin/fix/draft-premiums into fix/draft-model
hmgaudecker Oct 1, 2026
ecca1c4
MAINT Regenerate the benchmark snapshot after the premiums merge
hmgaudecker Oct 1, 2026
5bda643
TEST Build the ACA execution-config model from ACA-fit params
hmgaudecker Oct 1, 2026
82ac671
MAINT Regenerate the benchmark snapshot with the 950-row premium sample
hmgaudecker Oct 1, 2026
86fa18b
Merge the age-scheduled regime transitions into the draft model
hmgaudecker Oct 2, 2026
db17dcf
TST Read the scheduled disability-Medicare targets through the resolv…
hmgaudecker Oct 2, 2026
aa12061
Type the age stages and starting regimes for the checker
hmgaudecker Oct 2, 2026
aa70e92
MAINT Regenerate the benchmark snapshot with the age-64 disability row
hmgaudecker Oct 2, 2026
c88da0b
FIX Realise medical costs after the period's choices
hmgaudecker Oct 2, 2026
f3f0fb5
TEST Follow the medical-cost shock states into dead and the EGM refusals
hmgaudecker Oct 2, 2026
ce84697
Merge fix/draft-timing into fix/draft-model
hmgaudecker Oct 2, 2026
d8973c4
Port ACA models to explicit graphs and retain medical shock draws
hmgaudecker Oct 2, 2026
a2a16ee
Pin standalone CI to the pylcm draw-state test follow-up
hmgaudecker Oct 2, 2026
196a56a
Pin standalone CI to latest pylcm draw-state branch
hmgaudecker Oct 2, 2026
938c733
Declare regime transitions on Model.edges
hmgaudecker Oct 6, 2026
0a5ea10
TEST Expect the benchmark law arguments under params["edges"]
hmgaudecker Oct 7, 2026
721d774
ENH Route the benchmark law arguments to params["edges"]
hmgaudecker Oct 7, 2026
280e1e7
MAINT Pin CI pylcm to the edges-params API
hmgaudecker Oct 7, 2026
b9915a2
MAINT Read the benchmark law arguments from the snapshot's edges layout
hmgaudecker Oct 7, 2026
2400c0a
MAINT Pin CI pylcm to the fixed edges-params head
hmgaudecker Oct 7, 2026
591da5c
REF Schedule each regime's transition cells by source age
hmgaudecker Oct 8, 2026
5761c52
MAINT Pin CI pylcm to the strict transition-targets head
hmgaudecker Oct 8, 2026
621c150
FIX Drop the risk-rated premium intercept where the ACA plan is commu…
hmgaudecker Oct 8, 2026
e2b17fb
FIX Declare the admissible starts as InitialNodes and read their pair…
hmgaudecker Oct 9, 2026
98ebb47
Test ACA against the precise pylcm API
hmgaudecker Oct 10, 2026
a5e6950
ENH Add a grid flag that folds the transitory medical-cost shock
hmgaudecker Oct 9, 2026
649bed9
ENH Declare the asset law as resources minus the medical bill
hmgaudecker Oct 9, 2026
622ba00
ENH Declare the asset law as lcm.AdditiveShockTransition
hmgaudecker Oct 10, 2026
5b75bee
Enable medical-shock folding in the ACA benchmark
hmgaudecker Oct 11, 2026
ee48d9b
Preserve unfolded controls in legacy model test fixtures
hmgaudecker Oct 11, 2026
b962ebd
Check folded values across every solved model cell
hmgaudecker Oct 11, 2026
89d26dd
State exact ACA cost-sharing parity with a numeric assertion
hmgaudecker Oct 11, 2026
54397e8
Use the unfolded control for lagged medical-shock twins
hmgaudecker Oct 11, 2026
5e69b66
Keep the transition-only fold witness separate from additive folding
hmgaudecker Oct 11, 2026
03a8143
Pass grid points to IrregSpacedGrid as arrays
hmgaudecker Oct 20, 2026
4862f6f
Merge aca-model main into the array-grids compat branch
hmgaudecker Oct 22, 2026
3c237f9
Let simulations restart mid-life from any living regime
hmgaudecker Oct 22, 2026
bfcbbc3
Register policy and solver helpers only where consumed
hmgaudecker Oct 22, 2026
407c737
Merge remote-tracking branch 'origin/feat/restart-initial-nodes' into…
hmgaudecker Oct 23, 2026
ffb2f37
Test against pylcm #604 and offer uncompensated-care targets
hmgaudecker Oct 23, 2026
d0d2ea6
Test the health-cost covariates instead of total health costs as targets
hmgaudecker Oct 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
# install; keep it compatible with the model's graph declarations.
run: >-
pip install "pylcm @
git+https://github.com/OpenSourceEconomics/pylcm.git@eea73a8f0e301e88f02278df3595c797fdba24df"
git+https://github.com/OpenSourceEconomics/pylcm.git@7e99a19badafe20e29a517f69d9ec155bba8e0a0"
- name: Install aca-model with test deps
run: pip install -e . pytest pdbp
- name: Run pytest
Expand Down
8 changes: 6 additions & 2 deletions src/aca_model/aca/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from collections.abc import Mapping
from typing import Any

from lcm import AgeGrid, DiscreteGrid, ExecutionConfig, Model
from lcm import AgeGrid, DiscreteGrid, ExecutionConfig, InitialNodes, Model
from lcm.typing import UserParams

from aca_model.aca import PolicyVariant
Expand All @@ -34,6 +34,7 @@ def create_model(
solver: SolverName = "brute_force",
consumption_dollars_points: tuple[float, ...] | None = None,
execution_config: ExecutionConfig | None = None,
initial_nodes: InitialNodes = INITIAL_NODES,
) -> Model:
"""Create an ACA policy variant model.

Expand Down Expand Up @@ -61,6 +62,9 @@ def create_model(
execution_config: Explicit hardware-local policy forwarded unchanged.
None uses the smallest selected accelerator allocator limit as the
device-memory budget; CPU construction remains unbudgeted.
initial_nodes: Admissible starting `(age, regime)` pairs. The default
admits the estimation sample's entry ages and regimes; pass
`build_restart_initial_nodes` to restart simulations mid-life.

Returns:
pylcm Model.
Expand Down Expand Up @@ -91,7 +95,7 @@ def create_model(

return Model(
regimes=regimes,
initial_nodes=INITIAL_NODES,
initial_nodes=initial_nodes,
ages=ages,
regime_id_class=RegimeId,
description=f"Structural retirement model ({policy.name})",
Expand Down
5 changes: 4 additions & 1 deletion src/aca_model/aca/regimes/_overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from aca_model.aca import health_insurance as aca_hi
from aca_model.aca.health_insurance import PolicyVariant
from aca_model.baseline import health_insurance as baseline_hi
from aca_model.baseline.regimes._common import RegimeSpec
from aca_model.environment import taxes

Expand Down Expand Up @@ -55,6 +56,7 @@ def apply_aca_overrides(
)

if has_medicaid_expansion:
functions["aca_magi"] = baseline_hi.aca_magi
functions["is_medicaid_eligible"] = aca_hi.is_medicaid_eligible

if has_subsidies and spec["his"] == "nongroup" and spec["mc"] == "nomc":
Expand All @@ -64,8 +66,9 @@ def apply_aca_overrides(
# dict, not a DAG function — no entry needed here.
functions["plan_premium"] = aca_hi.community_rated_premium
# The community-rated premium replaces the risk-rated one, so the
# risk-rated premium's zero-profit intercept has no consumer left.
# risk-rated inputs have no consumer left.
functions.pop("private_premium_intercept", None)
functions.pop("predicted_hcc_insurer", None)
functions["hic_premium_subsidy"] = aca_hi.premium_subsidy
# Split at the bracket, which takes finitely many values, so the
# out-of-pocket bill can be averaged per bracket.
Expand Down
8 changes: 6 additions & 2 deletions src/aca_model/baseline/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from collections.abc import Mapping
from typing import Any

from lcm import AgeGrid, DiscreteGrid, ExecutionConfig, Model
from lcm import AgeGrid, DiscreteGrid, ExecutionConfig, InitialNodes, Model
from lcm.typing import UserParams

from aca_model.baseline.regimes import (
Expand All @@ -37,6 +37,7 @@ def create_model(
solver: SolverName = "brute_force",
consumption_dollars_points: tuple[float, ...] | None = None,
execution_config: ExecutionConfig | None = None,
initial_nodes: InitialNodes = INITIAL_NODES,
) -> Model:
"""Create the baseline structural retirement model.

Expand Down Expand Up @@ -73,6 +74,9 @@ def create_model(
execution_config: Explicit hardware-local policy forwarded unchanged.
None uses the smallest selected accelerator allocator limit as the
device-memory budget; CPU construction remains unbudgeted.
initial_nodes: Admissible starting `(age, regime)` pairs. The default
admits the estimation sample's entry ages and regimes; pass
`build_restart_initial_nodes` to restart simulations mid-life.

Returns:
A pylcm Model with 19 regimes (18 non-terminal + dead) spanning
Expand Down Expand Up @@ -102,7 +106,7 @@ def create_model(

return Model(
regimes=regimes,
initial_nodes=INITIAL_NODES,
initial_nodes=initial_nodes,
ages=ages,
regime_id_class=RegimeId,
description="Baseline structural retirement model (pre-ACA)",
Expand Down
2 changes: 2 additions & 0 deletions src/aca_model/baseline/regimes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
build_model_functions,
build_model_state_transitions,
build_model_states,
build_restart_initial_nodes,
)
from aca_model.baseline.regimes._dcegm import build_dcegm_solver
from aca_model.baseline.regimes._nbegm import build_nbegm_solver
Expand All @@ -46,6 +47,7 @@
"build_all_regimes",
"build_model_slots",
"build_regime",
"build_restart_initial_nodes",
"nongroup",
"retiree",
"tied",
Expand Down
32 changes: 23 additions & 9 deletions src/aca_model/baseline/regimes/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ def build_grids(
consumption_dollars=(
IrregSpacedGrid(n_points=grid_config.n_consumption_dollars_gridpoints)
if consumption_dollars_points is None
else IrregSpacedGrid(points=consumption_dollars_points)
else IrregSpacedGrid(points=np.asarray(consumption_dollars_points))
),
wage_res=wage_res,
hcc_persistent=hcc_persistent,
Expand Down Expand Up @@ -378,7 +378,7 @@ def build_assets_grid(*, floor: float, n_points: int) -> IrregSpacedGrid:
)
negative[0] = floor
positive[-1] = _ASSETS_TOP
return IrregSpacedGrid(points=np.concatenate([negative, positive]).tolist())
return IrregSpacedGrid(points=np.concatenate([negative, positive]))


def get_hcc_persistent_shock(*, grid_config: GridConfig) -> RouwenhorstAR1Process:
Expand Down Expand Up @@ -526,6 +526,24 @@ def transition_ages(spec: RegimeSpec) -> tuple[int, ...]:
raise ValueError(f"Unknown regime spec: {spec}")


def build_restart_initial_nodes(*, ages: Iterable[int]) -> InitialNodes:
"""Admit every living regime active at each of `ages` as a start.

A simulation restarted from the states another simulation reached at some
age starts in whichever living regime that simulation had arrived at.
"""
return InitialNodes(
by_age={
age: tuple(
name
for name, spec in REGIME_SPECS.items()
if age in transition_ages(spec)
)
for age in ages
}
)


def next_model_age(period: Period) -> FloatND:
"""Advance by one grid position, never by an assumed number of years.

Expand Down Expand Up @@ -845,12 +863,10 @@ def build_model_functions(*, solver: SolverName = "brute_force") -> dict:
# assets adjustment, both of which scale by the marginal income tax rate.
functions["marginal_tax_rate"] = taxes.marginal_rate

# HIC premium
functions["predicted_hcc_insurer"] = health_insurance.hcc_insurer_predicted

# Transfers
functions["consumption_dollars_floor"] = assets_and_income.consumption_dollars_floor
functions["transfers"] = assets_and_income.transfers
if solver == "brute_force":
functions["transfers"] = assets_and_income.transfers
functions["consumption_equiv"] = preferences.consumption_equiv

return functions
Expand Down Expand Up @@ -999,10 +1015,8 @@ def build_common_functions(spec: RegimeSpec, *, subtract_bill: bool = False) ->
if spec["mc"] == "oamc"
else health_insurance.is_disabled_from_health
)
# MAGI for the ACA Medicaid expansion track; pruned in the baseline DAG.
functions["aca_magi"] = health_insurance.aca_magi

# Swapped per policy variant by the ACA overlay, hence regime-level
functions["predicted_hcc_insurer"] = health_insurance.hcc_insurer_predicted
functions["is_medicaid_eligible"] = health_insurance.is_medicaid_eligible
functions["after_tax_income"] = taxes.after_tax_income
if subtract_bill:
Expand Down
5 changes: 4 additions & 1 deletion src/aca_model/baseline/regimes/_dcegm.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
the one piece that needs `lcm.solvers`.
"""

import numpy as np
from lcm import IrregSpacedGrid
from lcm.solvers import DCEGM

Expand All @@ -31,7 +32,9 @@ def build_dcegm_solver(grids: Grids) -> DCEGM:
assets_points = grids.assets.to_jax()
savings_stop = float(assets_points[-1]) - float(assets_points[0])
savings_grid = IrregSpacedGrid(
points=tuple(savings_stop * (i / (n_points - 1)) ** 3 for i in range(n_points)),
points=np.array(
[savings_stop * (i / (n_points - 1)) ** 3 for i in range(n_points)]
),
)
return DCEGM(savings_grid=savings_grid)

Expand Down
5 changes: 4 additions & 1 deletion src/aca_model/baseline/regimes/_nbegm.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Device placement and compiled program widths belong to the model execution policy.
"""

import numpy as np
from lcm import IrregSpacedGrid
from lcm.solvers import NBEGM

Expand All @@ -17,7 +18,9 @@ def build_nbegm_solver(grids: Grids) -> NBEGM:
assets_points = grids.assets.to_jax()
savings_stop = float(assets_points[-1]) - float(assets_points[0])
savings_grid = IrregSpacedGrid(
points=tuple(savings_stop * (i / (n_points - 1)) ** 3 for i in range(n_points)),
points=np.array(
[savings_stop * (i / (n_points - 1)) ** 3 for i in range(n_points)]
),
)
return NBEGM(
savings_grid=savings_grid,
Expand Down
8 changes: 6 additions & 2 deletions src/aca_model/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@
import jax.numpy as jnp
import numpy as np
from jax import Array
from lcm import DiscreteGrid, ExecutionConfig, Model
from lcm import DiscreteGrid, ExecutionConfig, InitialNodes, Model

from aca_model.agent.health import GoodHealth
from aca_model.agent.labor_market import IsMarried
from aca_model.agent.preferences import BenchmarkPrefType
from aca_model.baseline.health_insurance import HealthInsuranceState
from aca_model.baseline.model import create_model
from aca_model.baseline.regimes import ENTRY_REGIMES
from aca_model.baseline.regimes import ENTRY_REGIMES, INITIAL_NODES
from aca_model.config import BENCHMARK_GRID_CONFIG, GridConfig
from aca_model.consumption_dollars_grid import (
compute_consumption_dollars_points,
Expand Down Expand Up @@ -68,6 +68,7 @@ def create_benchmark_model(
pref_type_grid: DiscreteGrid,
grid_config: GridConfig = BENCHMARK_GRID_CONFIG,
execution_config: ExecutionConfig | None = None,
initial_nodes: InitialNodes = INITIAL_NODES,
) -> Model:
"""Create the aca baseline with `BENCHMARK_GRID_CONFIG` and frozen fixed_params.

Expand All @@ -80,6 +81,8 @@ def create_benchmark_model(
execution_config: Explicit hardware-local policy forwarded unchanged.
None uses the smallest selected accelerator allocator limit as the
device-memory budget; CPU construction remains unbudgeted.
initial_nodes: Admissible starting `(age, regime)` pairs, forwarded
to `create_model`.
"""
fixed_params, wage_params, _ = get_benchmark_params(model=None)
return create_model(
Expand All @@ -89,6 +92,7 @@ def create_benchmark_model(
derived_categoricals=_DERIVED_CATEGORICALS,
pref_type_grid=pref_type_grid,
execution_config=execution_config,
initial_nodes=initial_nodes,
)


Expand Down
15 changes: 15 additions & 0 deletions tests/test_aca_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,21 @@ def test_aca_non_group_premium_is_community_rated(
assert _aca_premium(policy, spousal_income) == pytest.approx(expected)


@pytest.mark.parametrize("policy", list(PolicyVariant))
def test_aca_non_group_risk_prediction_is_registered_only_without_reform(
policy: PolicyVariant,
) -> None:
"""The community-rated premium does not register a risk prediction node."""
functions = {**build_model_functions(), **_build_functions(REGIME_SPECS[REGIME])}
apply_aca_overrides(functions, REGIME_SPECS[REGIME], policy)
expected = (
health_insurance.hcc_insurer_predicted
if policy == PolicyVariant.ACA_ONLY_MEDICAID_EXPANSION
else None
)
assert functions.get("predicted_hcc_insurer") is expected


RISK_RATED_PREMIUM_PARAMS = frozenset(
{
"premium_markup",
Expand Down
19 changes: 19 additions & 0 deletions tests/test_benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import numpy as np
import pytest
from _lcm.simulation.additional_targets import _collect_all_available_targets
from lcm import (
DiscreteGrid,
GridBreakpoint,
Expand Down Expand Up @@ -112,6 +113,24 @@ def test_benchmark_panel_exposes_hic_premium_and_wage_targets() -> None:
assert (defined_wage > 0).all()


def test_benchmark_model_offers_uncompensated_care_targets() -> None:
"""Transfers, the floor and the health-cost covariates can be added to the panel.

Uncompensated care is the part of `transfers` above
`consumption_dollars_floor`. Total health costs depend on the next period's
shocks, so they are rebuilt from `is_married`, `good_health` and the shock
states rather than offered as a target.
"""
model = create_benchmark_model(pref_type_grid=DiscreteGrid(BenchmarkPrefType))
available = _collect_all_available_targets(model._regimes) # noqa: SLF001
assert {
"transfers",
"consumption_dollars_floor",
"is_married",
"good_health",
} <= available


@pytest.mark.long_running
def test_benchmark_simulate_obeys_borrowing_constraint() -> None:
"""`consumption_dollars <= max(cash_on_hand, floor)` holds for every alive row.
Expand Down
34 changes: 34 additions & 0 deletions tests/test_model_creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@
from aca_model.aca import health_insurance as aca_hi
from aca_model.aca.health_insurance import PolicyVariant
from aca_model.aca.regimes import build_all_regimes as _build_aca_regimes
from aca_model.agent import assets_and_income
from aca_model.agent.preferences import BenchmarkPrefType
from aca_model.baseline import health_insurance as baseline_hi
from aca_model.baseline.regimes import REGIME_SPECS, RegimeId
from aca_model.baseline.regimes import build_regime as _build_regime
from aca_model.baseline.regimes._common import (
SolverName,
build_grids,
build_model_functions,
build_model_state_transitions,
build_model_states,
)
Expand Down Expand Up @@ -290,6 +294,36 @@ def test_aca_model_creates_successfully() -> None:
assert model.n_periods == 46


@pytest.mark.parametrize("name", REGIME_SPECS)
def test_baseline_regime_omits_expansion_magi(name: str) -> None:
"""Baseline eligibility does not register the ACA expansion income node."""
assert "aca_magi" not in build_regime(name).functions


@pytest.mark.parametrize("policy", list(PolicyVariant))
def test_aca_regime_registers_magi_only_for_expansion(policy: PolicyVariant) -> None:
"""The expansion eligibility consumer owns its MAGI income node."""
has_expansion = policy in (
PolicyVariant.ACA,
PolicyVariant.ACA_NO_MANDATE,
PolicyVariant.ACA_ONLY_MEDICAID_EXPANSION,
)
expected = baseline_hi.aca_magi if has_expansion else None
regimes = build_aca_regimes(policy)
assert {
name: regimes[name].functions.get("aca_magi") for name in REGIME_SPECS
} == dict.fromkeys(REGIME_SPECS, expected)


@pytest.mark.parametrize("solver", ["brute_force", "dcegm", "nbegm"])
def test_model_registers_transfers_only_for_brute_asset_laws(
solver: SolverName,
) -> None:
"""Savings-form asset laws already include transfers through resources."""
expected = assets_and_income.transfers if solver == "brute_force" else None
assert build_model_functions(solver=solver).get("transfers") is expected


def test_aca_nongroup_inelig_has_real_functions() -> None:
"""Nongroup+nomc regimes get real ACA functions under ACA policy."""
regimes = build_aca_regimes(PolicyVariant.ACA)
Expand Down
Loading
Loading