From ede70f0deefd5a23f429efd9a4847914015d5d02 Mon Sep 17 00:00:00 2001 From: Hans-Martin von Gaudecker Date: Mon, 11 May 2026 15:40:55 +0200 Subject: [PATCH] consumption_dollars grid: route `max_consumption_dollars` through fixed_params The upper bound of the runtime consumption_dollars action grid used to be a module-level constant in `baseline/regimes/_common.py`, with a TODO to route through `fixed_params` once pylcm could surface grid-level scalars in the params template. pylcm#348 lands that. * `baseline/regimes/_common.py`: declare the grid as `IrregSpacedGrid(..., extra_param_names=("max_consumption_dollars",))` so the template gains a `ScalarFloat` slot per regime alongside `points`. * `consumption_dollars_grid.py`: - Drop the module-level `MAX_CONSUMPTION_DOLLARS` constant. - `_compute_consumption_dollars_points` takes the bound as a parameter. - `inject_consumption_dollars_points` reads it from `model.fixed_params["max_consumption_dollars"]` (alongside `exponent`), then threads it into the gridpoint computation. * `_benchmark_data/benchmark_params.pkl`: regenerated so `fixed_params["max_consumption_dollars"]` is populated from the refreshed aca-data env constants. * CI temporarily pinned to pylcm@feat/runtime-grid-extra-params; revert to @main once pylcm#348/#350 merge. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/main.yml | 4 +-- .../_benchmark_data/benchmark_params.pkl | Bin 68325 -> 68562 bytes src/aca_model/baseline/regimes/_common.py | 11 +----- src/aca_model/consumption_dollars_grid.py | 33 ++++++++++-------- tests/test_consumption_dollars_grid.py | 13 ++++--- 5 files changed, 31 insertions(+), 30 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1148345..8104668 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,10 +26,10 @@ jobs: - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - - name: Install pylcm (feature branch — revert to @main once pylcm#350 merges) + - name: Install pylcm (feature branch — revert to @main once pylcm#348/#350 merge) run: >- pip install "pylcm @ - git+https://github.com/OpenSourceEconomics/pylcm.git@feat/categorical-scalarint" + git+https://github.com/OpenSourceEconomics/pylcm.git@feat/runtime-grid-extra-params" - name: Install aca-model with test deps run: pip install -e . pytest pdbp - name: Run pytest diff --git a/src/aca_model/_benchmark_data/benchmark_params.pkl b/src/aca_model/_benchmark_data/benchmark_params.pkl index 650c3902fc840003a043e1d0a5d24aa41e09120f..f7c505ef8fb94731af080e93df12a095b942dc27 100644 GIT binary patch delta 11859 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 delta 11385 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 Array: """Return log-spaced consumption_dollars gridpoints with both floors pinned. @@ -108,12 +113,12 @@ def _compute_consumption_dollars_points( a feasible action; otherwise sub-ULP drift can flip the `<=` comparison for subjects with very negative cash. The geomspace tail starts at the married floor and runs to - `MAX_CONSUMPTION_DOLLARS` so the two pinned points stay strictly + `max_consumption_dollars` so the two pinned points stay strictly increasing. """ married_dollar_floor = consumption_equiv_floor * jnp.asarray(2.0) ** exponent tail = jnp.geomspace( - married_dollar_floor, MAX_CONSUMPTION_DOLLARS, num=n_points - 1 + married_dollar_floor, max_consumption_dollars, num=n_points - 1 ) pts = jnp.concatenate([consumption_equiv_floor[None], tail]) # `jnp.geomspace` returns `start * r^0` for the first tail element, @@ -129,7 +134,7 @@ def _compute_consumption_dollars_points( msg = ( f"consumption_dollars grid is not strictly increasing at the " f"married-floor kink: pts[1]={float(married_dollar_floor):.6g}, " - f"pts[2]={float(pts[2]):.6g}. Either `MAX_CONSUMPTION_DOLLARS` " + f"pts[2]={float(pts[2]):.6g}. Either `max_consumption_dollars` " f"is too close to the married floor or `n_points` is too small." ) raise ValueError(msg) diff --git a/tests/test_consumption_dollars_grid.py b/tests/test_consumption_dollars_grid.py index 1f42e6f..2d452b0 100644 --- a/tests/test_consumption_dollars_grid.py +++ b/tests/test_consumption_dollars_grid.py @@ -22,20 +22,21 @@ rejects every action for the affected subjects. `_compute_consumption_dollars_points` therefore prepends the singles' -floor as `pts[0]`, runs `geomspace` from the married floor up to -`MAX_CONSUMPTION_DOLLARS` for the rest, and pins the geomspace start -back to the married floor exactly. Test those invariants directly. +floor as `pts[0]`, runs `geomspace` from the married floor up to the +caller-supplied `max_consumption_dollars` for the rest, and pins the +geomspace start back to the married floor exactly. Test those invariants +directly. """ import jax.numpy as jnp import pytest -from aca_model.baseline.regimes._common import MAX_CONSUMPTION_DOLLARS from aca_model.consumption_dollars_grid import _compute_consumption_dollars_points EXPONENT = 0.7 # production value (env_constants["exponent"]) SINGLE_FLOOR = 1597.0921419521899 # production value MARRIED_SCALE = 2.0**EXPONENT +MAX_CONSUMPTION_DOLLARS = 300_000.0 # production value (env_constants) @pytest.mark.parametrize("n_points", [5, 16, 64, 70, 100]) @@ -46,6 +47,7 @@ def test_compute_consumption_dollars_points_first_equals_singles_floor( pts = _compute_consumption_dollars_points( consumption_equiv_floor=jnp.asarray(SINGLE_FLOOR), exponent=jnp.asarray(EXPONENT), + max_consumption_dollars=jnp.asarray(MAX_CONSUMPTION_DOLLARS), n_points=n_points, ) assert float(pts[0]) == SINGLE_FLOOR @@ -59,6 +61,7 @@ def test_compute_consumption_dollars_points_second_equals_married_floor( pts = _compute_consumption_dollars_points( consumption_equiv_floor=jnp.asarray(SINGLE_FLOOR), exponent=jnp.asarray(EXPONENT), + max_consumption_dollars=jnp.asarray(MAX_CONSUMPTION_DOLLARS), n_points=n_points, ) expected = float(jnp.asarray(SINGLE_FLOOR) * jnp.asarray(2.0) ** EXPONENT) @@ -70,6 +73,7 @@ def test_compute_consumption_dollars_points_strictly_increasing() -> None: pts = _compute_consumption_dollars_points( consumption_equiv_floor=jnp.asarray(SINGLE_FLOOR), exponent=jnp.asarray(EXPONENT), + max_consumption_dollars=jnp.asarray(MAX_CONSUMPTION_DOLLARS), n_points=70, ) diffs = jnp.diff(pts) @@ -81,6 +85,7 @@ def test_compute_consumption_dollars_points_last_equals_max() -> None: pts = _compute_consumption_dollars_points( consumption_equiv_floor=jnp.asarray(SINGLE_FLOOR), exponent=jnp.asarray(EXPONENT), + max_consumption_dollars=jnp.asarray(MAX_CONSUMPTION_DOLLARS), n_points=70, ) assert float(pts[-1]) == pytest.approx(MAX_CONSUMPTION_DOLLARS)