Skip to content

Calculate a household under several policies in one call with v2 Modal household functions - #903

Draft
anth-volk wants to merge 19 commits into
mainfrom
v2plan/1d-111-household-functions
Draft

anth-volk wants to merge 19 commits into
mainfrom
v2plan/1d-111-household-functions

Conversation

@anth-volk

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

Copy link
Copy Markdown
Contributor

Fixes #902

Summary

Adds Modal functions that calculate one household under one to ten policies in a single call, for the US and the UK, in the versioned v2 Modal application. They reproduce the Public API's household calculation and run in their own image with no datasets and no secrets. Nothing calls them yet: they are deployed but unused until the Simulation Entrypoint gains household routes.

Each commit is one piece:

  1. Household contract (policyengine_simulation_contract.household_execution): a household in the Public API's situation shape with optional axes, kept in the caller's key order (the Public API's order of errors, warnings and values depends on it); one to ten policies; a schema version; the expected PolicyEngine.py bundle; the caller's SPM settings, where leaving them out differs from sending any value (even an empty object); add_missing_variables for /calculate-full. Results carry each policy's household, warnings and SPM receipt, the applied SPM selection and the bundle provenance, or a typed failure.
  2. Manifest schema version 2: each country records whether it is economy-capable and which function calculates its households. The comparison adapter gives the report coordinator only economy-capable countries. Schema version 1 readers keep reading the existing keys (active-with-region-datasets, active-with-cache, active), which the publisher still writes in schema version 1 form. Current readers use the new active-schema-2 document only while the older keys hold its schema version 1 forms, so after a rollback to code released before schema version 2, what that code publishes is read, and the next publish starts from it.
  3. Input preparation (household_inputs): missing variables for /calculate-full, deprecated inputs with the Public API's warnings, unrecognized names with its error types, messages and order, and the requested values.
  4. SPM (household_spm): the selection merged over the bundle's certified defaults with the Public API's codes and messages, and the compact receipt checked against the selection.
  5. Observability stages for the household call, model load, input preparation and each policy's calculation.
  6. Calculation (household_calculation): one simulation per policy, the Public API's reform value conversion, value formatting, null values for values that cannot be calculated (one per axis point, with a warning), and its failure messages. Country models load once per container.
  7. Modal functions calculate_household_us and calculate_household_uk in a household image: the CA store, the locked packages and the source, without the bundle's datasets or the Stage 12 cache. No secrets. 2 CPUs, 4 GiB reserved and 16 GiB maximum memory, 900-second timeout, no retries, at most 20 containers.
  8. Release validation: each deployed household function must calculate a one-adult, current-law household with the deployed bundle before the manifest records its household_callable.
  9. Pre-merge image check: a v2 image smoke builds the coordinator, UK worker and household images, and the US worker image without its Stage 12 cache layer (only a deployment can fetch it), imports the entry points in each, and calculates the validation household in the household image.

Changes made after three structured review rounds (the second found no bugs; the third, after rebasing onto the region dataset manifest keys, found one minor issue that main already has, now documented):

  • Uncertifiable SPM receipts are worded and checked in the Public API's order. The shared contract receipt model keeps its wording; the household code translates its year and metro-area messages.
  • spm carries the caller's settings exactly as sent, and the function returns the Public API's SPM codes for settings sent for another country, an explicit null, or malformed settings.
  • The household SPM code loads its forecast without the main path's SPM module.
  • Tests that a publish after code released before schema version 2 (as a deployed revert would run) starts from what that code wrote. (When this branch was rebased onto the region dataset manifest keys from main, the schema version 2 reader took the same approach as those keys: a newer document is read only while the older keys match it.)
  • The comparison backend sends the economy-capable country's simulation callable as a typed value.
  • The country model loads in its own span before the SPM selection is resolved.
  • More real-model tests: a chosen US SPM selection, and the validation household for both countries.
  • Manifest tests store older publishers' documents with only the fields those publishers wrote, and the migration document states what a rollback to older code costs once current code publishes again.

Every known difference from the Public API is listed in the household_calculation module documentation: undefined numbers are the string "NaN" and dates are HTTP date text; the default household year is the UTC year; an age given as one value under calculate-full; an explicit null axes; a failure under any one policy fails the whole request; the request routes' error wording; and requests the contract rejects.

Tests

  • Real-model tests run in the executor's PR test job: US baseline and reform in one call, US axes, US calculate-full (about 90 seconds on an idle machine), a chosen US SPM selection, the release validation household for both countries, UK axes, calculate-full, reform values given as text, unrecognized inputs, deprecated inputs, SPM refusal and an invalid household.
  • Stand-in model tests cover the failure paths, SPM handling and value formatting.
  • Declaration tests for the Modal functions and the household image, release validation, manifest publication, and the image smoke and its script.
  • Locally, on the current head (rebased on main): the executor (1847 passed), entrypoint (665), contract (331), gateway, observability and persistence suites; Ruff format and lint, and Pyright for each changed package, on every commit; the quality guards.
  • GitHub Actions: before the rebase onto the region dataset manifest keys, every check passed on commit bcedd0d4, including the PR image smoke job, which built every v2 image and calculated the validation household in the household image for the US and the UK. After the rebase, the PR image smoke job passed again on 7455b240; the other runs on the rebased commits (7455b240, 08f13016) were cancelled at the user's request, so the local runs above are the evidence for them, and CI needs to run again before merge.

Environment variables and Modal secrets

Value Read by Provider How it reaches the code
Modal secrets Household functions and household image build None secrets=[]; the image downloads nothing.
MODAL_ENVIRONMENT, MODAL_TASK_ID, MODAL_REGION Household functions' telemetry identity Modal Set by Modal in every container.
MODAL_IDENTITY_TOKEN Household functions' telemetry export (workload identity federation) Modal Set by Modal in every container. Not verified: that the workload identity provider's conditions accept tokens from the new function names. A refused token loses telemetry only; calculations are unaffected.
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 OBSERVABILITY_OTLP_ENDPOINT), POLICYENGINE_OTEL_GOOGLE_AUDIENCE (from OBSERVABILITY_OTLP_GOOGLE_AUDIENCE) Household image environment GitHub repository variables (all eight exist) Unchanged wiring: the env: of the "Deploy the additional versioned v2 Modal application" step in simulation-deploy.reusable.yml, baked into the image by modal_image_environment() at modal deploy, as for the other v2 images.
MODAL_TOKEN_ID, MODAL_TOKEN_SECRET Release validation calling the household functions; the image smoke GitHub repository and organization secrets Unchanged: the existing env: of the v2 validation step and of the image smoke step.
PE_UK_PRIVATE_HF_READ_TOKEN Image smoke: the UK dataset layer of the coordinator and UK worker images GitHub organization secret Unchanged: the image smoke step's env:; modal-image-smoke.sh stores it in the staging Modal secret pe-uk-private-hf-read-token before the smokes run. The household image does not use it.

No new variable or secret is introduced.

Before merging

  • A full CI run on the current head, including the PR image smoke job (runs on the rebased commits were cancelled at the user's request).
  • Review.
  • Merging deploys to staging and then production. Each deployment's v2 validation now calls both household functions, and a failure stops that deployment as a failed worker validation already does.
  • An operator check of the workload identity provider's conditions for the new function names (telemetry only; not required for the calculations).
  • Merge coordination only: other open changes to the v2 Modal application or the v2 manifest rebase onto this one.

🤖 Generated with Claude Code

anth-volk and others added 17 commits October 22, 2026 21:17
The v2 household functions take one household in the Public API's situation
shape, with optional axes, and one to ten policies, so a baseline and its
reform are calculated in one call. The request names the PolicyEngine.py
bundle it expects, whether to add missing variables (as the Public API's
/calculate-full route does), and an optional SPM selection whose omission is
kept distinct from an empty choice.

A household keeps its caller's key order through validation and transport,
at the top level and in each group, because the Public API walks a household
in that order and the order of its input errors, warnings and values follows
it. The result carries each policy's calculated household, warnings and SPM
receipt, the applied SPM selection and the bundle provenance; a failure
carries a typed error whose messages and input errors match the Public API's
error responses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Schema version 2 of the v2 version manifest records, for each country a
worker application serves, whether it is economy-capable (it names a
single-simulation worker) and which function calculates its households, and
the validation receipt names an invocation for each of those functions. The
Simulation Entrypoint's comparison adapter now accepts only a country the
manifest marks as economy-capable, so the report coordinator never receives
a report for a country that only calculates households.

Deployed and rollback revisions read schema version 1 and reject unknown
fields, so the publisher writes schema version 2 under a new key and keeps
writing the schema version 1 forms (with region datasets, without them, and
without cache releases) under the existing keys whenever schema version 1
can describe the manifest. Current readers use the schema version 2 document
while the older keys hold its schema version 1 forms, or, for a manifest
schema version 1 cannot describe, unless the older keys hold a later
validation; otherwise they read the older keys as before, as economy-capable
countries without household functions. Code released before schema version 2
that publishes after a rollback therefore leaves no stale document behind,
and the next publish starts from what it wrote. The publisher records every
bundle country as economy-capable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The v2 household functions prepare a household exactly as the Public API's
household calculation does before calculating it: /calculate-full adds every
missing yearly, monthly and eternal variable for the year of the first age
entered for "you"; deprecated inputs are dropped, including axes that vary
them, with the Public API's warning text; unknown entity groups, variables,
variables on the wrong entity, axis variables and policy parameters are
reported in the order the household was sent, with the Public API's error
types and messages; and the requested values are the null values, or with
axes every value of a known variable, in document order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A US household's SPM selection is merged over the installed bundle's
certified defaults with the Public API's rules and error codes: settings for
any other country, or for a US model bundle without canonical SPM, are
unsupported; a model that can run canonical SPM without a certified
configuration fails closed; a forecast hash the bundle lacks is an invalid
setting from a request but an unavailable configuration from a saved
household; and an unknown scenario, county vintage or area is reported with
the Public API's code and message.

After each calculation, the country model's SPM receipt is reduced to the
compact public receipt and checked against the resolved selection. SPM errors
that end a calculation are recognized through wrapped causes, as the Public
API recognizes them. The executor's SPM module now exposes the certified
forecast it already loads once per process.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A v2 household function call is measured as one operation that covers the
whole call, with nested spans for loading the country model, preparing the
household, and calculating it under each policy. The stage plan lists those
names so the household functions open spans only from registered stages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
calculate_household runs one household request: it checks the request names
the installed bundle, resolves the SPM selection, prepares the household once,
then builds a simulation and calculates the requested values for each policy
in order. It returns each policy's calculated household, warnings and SPM
receipt with the applied SPM selection and bundle provenance, or a typed
failure.

Each step follows the Public API's household calculation: reform values are
converted to each parameter's value type, values are written as its country
module writes them, a value that cannot be calculated is null (one null per
point, with a warning, when the household has axes), and an SPM error ends the
calculation only for a chosen measurement or a configuration the build cannot
certify. Failures carry the Public API's messages: an invalid household
payload, unrecognized inputs, an SPM error, or an error calculating the
household under a policy. Country models are loaded once per process.

The tests calculate real US and UK households, including US households with
axes and with every missing variable added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… app

Each function takes a household request, calculates it with the executor's
household calculation and returns the result or typed failure as JSON. It
continues the caller's trace through the keyword-only observability context
and measures the call as one v2 household operation.

The functions run in their own image: the CA store, the locked packages and
the source, without the bundle's datasets or the Stage 12 cache, so neither
the image build nor the functions use a secret. Each reserves 2 CPUs and
4 GiB, may use up to 16 GiB for large households with axes, times out after
900 seconds, is not retried, and runs at most 20 containers. The other v2
images keep their layers and order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nifest

After the country validation functions, release validation calls each
deployed household function with a one-adult, current-law household and
requires a successful calculation with the deployed bundle, reported by that
function, with a number for the household's net income. The release record
keeps each household invocation ID, and the manifest publisher records a
country's household_callable only when the record shows its function was
validated. A household function that fails validation stops the release, as a
failed country validation already does.

The validation household and its check live in the executor package, so the
pre-merge image check can calculate the same household inside the household
image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The image check now also runs a v2 image smoke. It builds the report
coordinator, UK worker and household images the v2 application deploys, and
the US worker image without its last layer, the Stage 12 cache, which only a
deployment can fetch: the deployment's precompute writes the cache, and the
image build reads it with the deployment's Cloud Storage credential. That
layer adds data files, not Python packages.

In each report image the smoke imports the v2 application module, as Modal
does before it runs a function, and the modules the coordinator, workers and
validation functions import while handling a request. In the household image
it imports the household entry points and calculates the release validation
household for each country, which shows the image calculates without
datasets. The smoke functions use no secret.

Modal builds the smoke's images together. The job's timeout rises from 45 to
90 minutes, because after a relock both the executor smoke and the v2 smoke
download certified datasets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A household's SPM receipt that the installed country model reports in a form
that cannot be certified now fails with the Public API's reason, checked in
its order: the forecast hash, scenario and geography kind are all required
before they are compared with the selection, then the years mapping, the
runtime versions and the forecast ID. The contract's receipt model words its
year and metro-area errors as the Public API's model does, and a receipt that
is not an object, or whose years are not a mapping, is reported the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s codes

The request's spm field now carries the caller's SPM settings as any JSON
value, and the household function checks them as the Public API's routes
do: settings for a country other than the US are unsupported, an explicit
null for the US is invalid, and malformed settings are invalid with the
Public API's message, before the bundle's certification is read. A metro
selection needs an area and any other kind accepts none, judged on the
settings as sent, as the Public API's model judges them. Before, a malformed
selection or an explicit null failed the request's validation instead of
returning a typed SPM failure.

The request's documentation now says which parts of a saved household's
calculation match the Public API's stored-household route (the calculated
household, warnings, SPM receipt and SPM codes) and which do not (two error
messages, which that route words with the household and policy IDs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The household SPM code now loads the certified forecast with its own
per-process loader instead of a wrapper added to the main path's SPM
module, which returns to its state on main. The household path and the main
path share no SPM code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The household calculation module's documentation now lists each known
difference from the Public API's household calculation, which were described
in several places or not at all: how undefined numbers and dates are written,
the UTC default year, an age given as one value under calculate-full, an
explicit null axes, checking every policy's parameters first, the request
routes' error wording, and requests the contract rejects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A review asked whether a publish after code released before schema version 2
(as a deployed revert would run) could start from a stale schema version 2
document and drop what that code wrote. The schema version 2 reader already
uses the document only while it is current; these tests pin that down for a
publish: it keeps an application the older code redeployed without household
functions, it takes the default from what the older code wrote, and older
keys that a household-only schema version 2 publish left alone change nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A v2 worker version now returns the simulation callable of an
economy-capable country, and raises LookupError for any other country. The
Simulation Entrypoint's comparison backend stores that callable on the
prepared report and sends it to the coordinator, instead of looking up the
country's worker by matching and sending a value that schema version 2
allows to be null. The adapter already skips a country that is not
economy-capable, so a prepared report always has one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… models

Two real-model tests join the PR checks: a US household without a county
leaves the SPM threshold uncalculated under the inherited county selection,
fails with SPM_GEOGRAPHY_REQUIRED when the selection is chosen, and is
calculated with a national receipt when the national kind is chosen; and the
installed US and UK models calculate the household that release validation
and the image check send.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A US household calculation resolved its SPM selection before the model-load
span opened, and that resolution imports the US model, so a cold container's
model load was measured in none of the household stages. The country model
now loads first, in its own span, and the SPM selection is resolved while the
household is prepared, as the Public API resolves it while preparing a
calculation. A test checks both, and that a saved household's settings are
resolved as saved. The module's list of differences from the Public API now
says that a failure under any one policy returns one failure for the whole
request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk
anth-volk force-pushed the v2plan/1d-111-household-functions branch from bcedd0d to 7455b24 Compare October 22, 2026 17:41
anth-volk and others added 2 commits October 22, 2026 22:04
…eholds

The contract's SPM receipt model is shared with the main path, so it returns
to its wording on main. The household SPM code now translates that model's
year and metro-area errors into the Public API's wording when it reports a
receipt it cannot certify, so household messages still match the Public API
and nothing the main path runs changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… limits

The tests' older-publisher documents now carry only the fields those
publishers wrote: no region datasets under the keys that predate them. Two
tests join them: the fixtures hold only released fields, and a publish
refuses an unreadable schema version 2 document.

The migration document now says what a rollback to code released before
schema version 2 costs once current code publishes again: the manifest names
no household functions for versions published before the rollback until each
is published again, and it loses what only the schema version 2 document held.
It also says that current readers do not read a publish by code released
before cache releases, so a rollback that far needs the newer keys deleted
first; the region dataset readers on main already behave this way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

Calculate a household under several policies in one call on Modal (v2 household functions)

1 participant