Skip to content

Add v2 Modal household functions that calculate one household under several policies - #842

Closed
anth-volk wants to merge 5 commits into
apiv2/1c-household-contractsfrom
apiv2/1c-household-functions
Closed

anth-volk wants to merge 5 commits into
apiv2/1c-household-contractsfrom
apiv2/1c-household-functions

Conversation

@anth-volk

@anth-volk anth-volk commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #840

Summary

Second of two stacked pull requests for the household part of the API v2 migration (Step 1, group 1C), on top of #841. It adds a household calculation that reproduces the Public API's in-process household calculation, and one v2 Modal household function per country in the PolicyEngine.py bundle. Nothing calls the new functions yet; the entrypoint household submit and poll routes come in group 2B.

Household calculation (policyengine_simulation_executor)

household_calculation.calculate_household runs one HouseholdExecutionRequest: it calculates the household under each policy in order, so a baseline and its reform share one invocation, and returns a HouseholdExecutionResult or a typed HouseholdExecutionFailure. It ports the Public API's household_calculation_service.py, country.py and spm.py behavior:

  • household_inputs.py: country-model metadata from the tax-benefit system (variables with the Public API's default values, entities and role plurals, gov parameters); the household year and the calculate-full addition of missing yearly, monthly and eternal variables; dropping deprecated inputs with the same warnings; unrecognized household, axis and policy inputs with the same types, paths and messages; the list of values to calculate with and without axes.
  • household_calculation.py: simulation construction and reform value conversion; serialization of requested values (enum names, infinities as "Infinity", one value per axis point); a value that cannot be calculated becomes null, with the Public API's warning when axes are used; per-policy warnings (deprecated inputs, then calculation warnings). Failure codes and messages follow the Public API's error responses, in its order: bundle mismatch, SPM selection, unrecognized inputs, unparseable household (Invalid household payload: ...), other errors (Error calculating household under policy: ...).
  • household_spm.py: the SPM selection merged over the bundle's certified defaults with the Public API's codes (including SPM_SETTINGS_UNSUPPORTED outside the US and the saved-household artifact case), the rule that a chosen measurement fails on a missing SPM input while an inherited one leaves the value null, and the compact SPM receipt checked against the resolved selection.
  • Country models load once per container and are reused while it stays warm.
  • New v2_household stage plan in policyengine_simulation_observability.stages (execution, country model load, input normalization, one calculation span per policy).

Two deliberate differences from the Public API, both for inputs the Public API answers with an HTTP 500 or with invalid JSON: an undefined number is returned as "NaN" (the contract carries only finite numbers), and a you age entered without periods counts as no age when choosing the default year. Date values are returned as ISO strings.

Modal functions this deploys

Added to the existing versioned v2 application policyengine-simulation-v2-py{version} (the version comes from the installed PolicyEngine.py bundle), in both the staging and production Modal environments, by the existing deploy_stage12_v2 job:

Function Image CPU Memory Timeout Retries Max containers Secrets
calculate_household_us household image 2.0 4,096 MiB reserved, 16,384 MiB limit 900 s 0 20 none
calculate_household_uk household image 2.0 4,096 MiB reserved, 16,384 MiB limit 900 s 0 20 none
  • The functions are generated from the bundle's countries, so a country added to the bundle gets one without code changes to the app.
  • The household image is the frozen modal-simulation-image dependency group plus source, with no datasets and no Stage 12 cache. Household calculations read no private data, Cloud Storage or database, so the functions get no Modal secrets. The existing images keep the same layers.
  • Sizing: the 16 GiB limit equals a whole Public API Cloud Run instance, for large households calculated with every variable at many axis points; the timeout is three times the Public API's 300-second request timeout. Containers scale to zero; 2B-2's staging check measures latency and memory.
  • Deployment validation (validate_v2_worker_release) now calls each household function with a one-adult, current-law household and requires a successful calculation with this deployment's bundle; the release record keeps the invocation IDs. A failing household function therefore fails the v2 deployment, which stops the manifest publication and, as any v2 worker validation failure does, the Cloud Run promotion of that release. This adds one cold start and one calculation per country to the deployment.
  • The manifest publisher records household_callable for a country only when the release record shows its household function was validated.
  • docs/migration/stage-12-modal-worker-foundation.md describes the household functions and their release validation.

Configuration and environment variables

No variable or secret is added. The values involved, and how they reach the functions:

  • Deploy step (deploy_stage12_v2, "Deploy the additional versioned v2 Modal application"), unchanged: MODAL_TOKEN_ID and MODAL_TOKEN_SECRET (GitHub environment secrets), OBSERVABILITY_SERVICE_NAMESPACE, OBSERVABILITY_TRACE_PROJECT_ID, OBSERVABILITY_LOGGING_PROJECT_ID, OBSERVABILITY_LOG_NAME, OBSERVABILITY_GOOGLE_WORKLOAD_IDENTITY_PROVIDER, OBSERVABILITY_GOOGLE_SERVICE_ACCOUNT_EMAIL, OTEL_EXPORTER_OTLP_ENDPOINT (from vars.OBSERVABILITY_OTLP_ENDPOINT) and POLICYENGINE_OTEL_GOOGLE_AUDIENCE (from vars.OBSERVABILITY_OTLP_GOOGLE_AUDIENCE), all GitHub environment variables passed through the step's env: block. modal_image_environment() bakes the observability values into the household image at build time, as it does for the existing images.
  • Validation step, unchanged: MODAL_TOKEN_ID and MODAL_TOKEN_SECRET (GitHub environment secrets, step env:).
  • Runtime in the household containers: MODAL_ENVIRONMENT, MODAL_TASK_ID and MODAL_REGION are set by Modal; telemetry export uses Modal's container identity token (MODAL_IDENTITY_TOKEN) with the baked workload identity settings.
  • Unverified: that Modal provides MODAL_IDENTITY_TOKEN to functions with no attached secrets, and that the Google workload identity provider's attribute conditions accept tokens from the new function names. If either does not hold, telemetry export from the household functions fails; by sim.observability.failures-never-break-requests that does not change a calculation result.

No operator step is needed before merging.

Coordination

Group 1B edits the report coordinator in the same Modal app in parallel. This pull request changes build_v2_image (an optional dataset layer) and appends the household functions at the end of src/modal/v2_app.py; it does not change coordinate_report. A textual conflict is possible if 1B edits build_v2_image or also appends to that file.

Tests

  • New: tests/test_household_inputs.py, tests/test_household_spm.py, tests/test_household_calculation.py (fake country models for failure paths, and the installed US and UK models for a baseline and reform in one request, axes, calculate-full, unrecognized inputs, deprecated inputs, SPM for the UK, and an invalid household).
  • Updated: tests/test_stage12_modal_app.py (functions generated for every bundle country, sizing, no secrets, image without datasets, observability context and runtime shutdown, wrong country refused), tests/test_validate_v2_worker_release.py, tests/test_update_v2_version_manifest.py, contract and stage registry tests.
  • projects/policyengine-simulation-executor: uv run pytest tests → 1341 passed, 22 skipped (including a test that unrecognized inputs are listed in the order the household was sent, as the Public API lists them).
  • libs/policyengine-simulation-contract → 236 passed; libs/policyengine-simulation-observability → 34 passed.
  • Registering the v2 app locally with the real Modal SDK (no deploy) lists calculate_household_us and calculate_household_uk as global, non-serialized functions resolvable by module attribute.
  • ruff check . and ruff format --check projects/*/src libs/*/src (ruff 0.15.22) passed.
  • Not run locally: the Docker build, local integration tests and a real Modal deployment. Per the stacked-PR rule, CI runs on this pull request once Add household calculation contracts and record country capabilities in v2 manifest schema version 2 #841 merges.

🤖 Generated with Claude Code

@anth-volk
anth-volk added this pull request to stack #843 October 19, 2026 22:02
@anth-volk anth-volk changed the title apiv2/1c household functions Add v2 Modal household functions that calculate one household under several policies Oct 19, 2026
anth-volk and others added 5 commits October 20, 2026 17:20
Add the input handling the v2 household functions need, reproducing the
Public API's in-process household calculation so v2 results can be compared
with it field by field:

- country-model metadata read from a tax-benefit system (variables with the
  Public API's default values, entities and role plurals, gov parameters);
- the household year and the calculate-full addition of missing yearly,
  monthly and eternal variables;
- dropping deprecated inputs, with the same warnings;
- unrecognized household, axis and policy inputs, with the same types,
  paths and messages;
- the list of values to calculate, with and without axes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the calculation the v2 household functions run. calculate_household
takes one HouseholdExecutionRequest and calculates the household under each
requested policy in order, so a baseline and its reform share one
invocation, returning per-policy results or a typed failure.

It reproduces the Public API's in-process household calculation: the same
simulation construction and reform value conversion, requested values,
value serialization (infinities as "Infinity"), values that cannot be
calculated (null, with a warning when axes are used), warnings, and SPM
selection, error codes and compact receipt. Failure codes and messages
match the Public API's error responses: bundle mismatch, SPM selection,
unrecognized inputs, unparseable household, and any other calculation
error.

Also:

- register the v2_household stage plan for the household function's spans;
- add HouseholdBundleProvenance.from_bundle and household_callable_name to
  the contract;
- expose the executor's certified SPM forecast loader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The v2 Modal application now defines calculate_household_{country} for each
country in the installed PolicyEngine.py bundle (today calculate_household_us
and calculate_household_uk). Each takes one HouseholdExecutionRequest and
returns the per-policy result or typed failure from calculate_household.

- The household image holds the frozen simulation dependency group and no
  datasets or cache, and the functions get no Modal secrets: household
  calculations read no private data, Cloud Storage or database.
- Sizing: 2 CPUs, 4 GiB reserved with a 16 GiB limit for large households
  calculated with every variable at many axis points, a 900-second
  timeout, no retries and at most 20 containers.
- Deployment validation calls each household function with a one-adult
  current-law household and requires a successful calculation with this
  deployment's bundle; the release record keeps the invocation IDs.
- The manifest publisher records a country's household function only when
  the release record shows it was validated.

Nothing calls the household functions yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A household with an unknown person variable and an unknown axis variable,
sent with `axes` last, must list the person's error first and join the
message in that order, as the Public API does. The test failed before the
contract serialized households in the caller's key order (the fix in the
pull request below) and passes with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Describe the household functions in the v2 worker document: one per bundle
country, their image without datasets, no secrets, their limits, and that
deployment validation calculates a household with each one. A failed
household validation stops manifest publication and the Cloud Run
promotion, as any v2 worker validation failure does.

Documentation only: no test can fail first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk
anth-volk force-pushed the apiv2/1c-household-functions branch from 4d79ea2 to a4b6872 Compare October 20, 2026 13:24
@anth-volk anth-volk closed this Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v2 Modal household functions for every country in the PolicyEngine.py bundle

1 participant