Skip to content

Bake per-state ACS-local year files into Stage 12 and record the dataset state runs load - #885

Merged
anth-volk merged 25 commits into
mainfrom
feat/stage12-state-datasets
Oct 22, 2026
Merged

anth-volk merged 25 commits into
mainfrom
feat/stage12-state-datasets

Conversation

@anth-volk

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

Copy link
Copy Markdown
Contributor

Fixes #884

Rebased on main after #879 merged.

Same-Repository Draft PR Check

  • This PR is a draft.
  • This PR is from a branch in PolicyEngine/policyengine-sim-api, not a personal fork.

Summary

Stage 12 per-state datasets

Stage 12 gets its own copy of #879's mechanism. Nothing in it imports the main path's precompute modules.

  1. Content key stage12-sds1 and folder layout. The key is built from the regional dataset's entry in the published Stage 12 bundle, plus the state and filter. Files go in state-datasets/<dataset>/<STATE> under the Stage 12 data folder.

  2. Plan. state_dataset_targets() resolves each state region the way a Stage 12 worker would resolve it. There is one entry per year and state.

  3. Build. build_state_datasets_impl:

    • re-derives each key from the installed bundle, and refuses a state without a row filter;
    • prepares the full-country year file once;
    • refuses data in which a household's district code names a registered district of another state;
    • filters each state with the region's own scoping strategy;
    • copies the version stamp, and checks that .py would reuse the file;
    • uploads the file to the write-once Stage 12 cache.
  4. Precompute run. build_state_datasets runs with 8 CPUs, 96 GiB and a two-hour limit, one call per year and dataset. The run waits for it only before publishing the manifest. For each build it logs the files built and uploaded, the bytes uploaded and the time taken.

  5. Image fetch and worker check. Every entry is checked before anything downloads:

    • unknown entry types are refused;
    • every year must have files for the same set of states;
    • folders and file names must be safe;
    • each file may be listed once, identified by folder plus file name.

    The fetch timeout rises from 1,800 s to 3,600 s, matching the main path. The worker check expects 63 files plus three per state that uses a non-default dataset. The cache release it reports is unchanged.

  6. Manifest. The manifest lists state_dataset entries. A plan with no state files publishes exactly the same manifest as before.

  7. Runtime. Stage 12 state and district calculations read their state's baked file when it exists and .py would reuse it. Otherwise they prepare the dataset as before, including when an upgrade has moved the private .py helper the reuse checks import.

Dataset provenance and data_version

  1. Main path data_version. US state and district runs now report the loaded dataset's revision (the ACS-local revision) as data_version, not the national release. National runs are unchanged.
  2. Published bundle.
    • Stage12CountryBundle.region_datasets names the dataset each region type loads. A templated path such as states/{state_code}.h5 maps to none. A path that names no bundle dataset is refused.
    • region_type_of and Stage12CountryBundle.dataset_for_region read region codes the way the runtime normalises them. The worker and the entry adapter both use them.
    • The bundle digest leaves region_datasets out, so the installed 6.7.5 bundle keeps the digest it has on main.
    • The v2 version manifest is written under active-with-region-datasets. The keys active-with-cache and active keep their current shapes for entry services already running, whose models forbid unknown fields. Loaders read the newest key.
  3. Workers.
    • They accept the dataset their region type loads, and, during the rollout, the default dataset that entry services not yet updated still send.
    • Any other dataset is refused.
    • After resolving the dataset, a worker requires the provenance to name it, by name and URI. The only exception is that rollout case.
  4. Entry adapter. State and district requests send the regional dataset's provenance and population. National and UK requests, and workers whose manifest predates region datasets, are unchanged.

Deploy effects.

  • Stage 12 cache. The bundle digest is unchanged, so existing Stage 12 cache files stay valid. The first deploy builds only the 153 state files (51 states × 3 years) in each environment.
  • Deploy order. It is safe in both directions:
    • workers accept both kinds of provenance;
    • entry services send regional provenance only to workers whose manifest lists region datasets;
    • the digest that entry services send still matches a redeployed worker.
  • Qualification during the rollout. A qualification of a state report whose provenance still names the default dataset (from an entry service not yet updated) reports a data_version difference, because the main path now reports the ACS-local revision. This stops once the entry service is updated.
  • Rollback. Code older than this PR republishes only active-with-cache and active, leaving the active-with-region-datasets copy out of date. Readers and the publisher use that copy only while it equals the active-with-cache manifest once its region datasets are removed. After the older code's publish, entry services stop sending regional provenance, and the next publish keeps the versions the older code added.
    • Rollback window: the older worker is deployed before its publish job runs. Until then, the copy still matches, so updated entry services keep sending regional provenance for state and district runs, and the older worker refuses those Stage 12 comparison runs. Production responses are unaffected. If the older deploy's validation fails, the window lasts until a later publish.
    • To avoid it: delete the active-with-region-datasets key from the simulation-api-v2-version-manifest Modal Dict before deploying the older code. The migration document describes this.

Follow-up: once every entry service sends regional provenance, remove the workers' acceptance of the default dataset for state runs.

Testing

  • Test-first. Each commit was written test-first; every new test was run and seen to fail for the expected reason before the code was written.
  • Suites:
    • executor (uv run pytest tests/): 1,753 passed, 22 skipped;
    • entry service: 662 passed;
    • contract library: 274 passed;
    • gateway: 260 passed.
  • Real installed bundle (6.7.5). Tests against it show:
    • 153 planned state files, all from the ACS-local dataset;
    • state and district regions mapped to populace_us_2024_acs_local;
    • matching dataset URIs in the release bundle and the Stage 12 bundle.
  • Code review. Two rounds of review-code, each finding fixed test-first.
    • Round 1:
    • Round 2:
    • Round 3: found no bugs. The rollback window and its avoiding step are documented, and an outdated comment and part of the migration document are corrected.
  • Quality checks. python scripts/run_quality_guards.py passed. ruff check . and ruff format --check with ruff 0.15.22 are clean.
  • Pyright. python -m scripts.rules pyright passes for the executor, the contract library and the entry service, with the executor's errors matching main's baseline (77). The last commit passes isin a list in Stage 12's district-code check. No rules/ file changes.
  • Not run locally: the Modal image smoke, the Docker build and the local integration tests. CI runs them.

🤖 Generated with Claude Code

@anth-volk
anth-volk force-pushed the feat/stage12-state-datasets branch from fca6036 to 416e53d Compare October 21, 2026 20:50
@anth-volk
anth-volk force-pushed the feat/state-datasets branch from 9eb6790 to aaa97c8 Compare October 21, 2026 21:47
@anth-volk
anth-volk force-pushed the feat/stage12-state-datasets branch from 1235a0e to 3a28ef6 Compare October 21, 2026 21:51
Base automatically changed from feat/state-datasets to main October 21, 2026 22:00
anth-volk and others added 25 commits October 22, 2026 17:04
Stage 12 gets its own copy of the per-state dataset identity, without
importing the main path's precompute modules. Stage12StateDatasetCacheIdentity
(schema stage12-sds1) holds a dataset identity's fields for the regional
dataset (its own SHA-256 and artifact revision from the published Stage 12
bundle, the package versions and the bundle manifest digest) plus the state
code and FIPS and the filter variable. Its store path is
state-datasets/<country>/<digest>/<stem>_year_<year>.h5.

stage12_cache/state_datasets.py places each state's files at
<Stage 12 data folder>/state-datasets/<dataset>/<STATE>. The national
dataset and baseline identities are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage 12's precompute gets its own state_dataset_targets, which resolves
every US state region the way a Stage 12 worker resolves a request for it
and keeps the states whose dataset is not the bundle default, with their
row filter and state code. The plan lists one StateDatasetPlanEntry per
precompute year and target state, year-major and by state code, with its
key, store path, file name, image folder and presence in the bucket;
select_work keeps the missing ones. On the installed bundle that is 51
states for each of the three years, all on the ACS-local dataset.

Nothing builds or publishes these entries yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage 12's own build_state_datasets_impl mirrors the main path's: it
re-derives each planned state's key from the installed Stage 12 bundle and
refuses to build on a mismatch, has .py prepare the regional dataset's
full-country year file once, collects garbage and reloads the year, refuses
data in which a household's district code names another state, then for
each state writes the rows its region's own scoping strategy keeps with
.py's save(), copies the year-file stamp, confirms .py's reuse checks accept
the file, and uploads it to the write-once Stage 12 cache, which checks the
stored digest and size. Stage 12 keeps its own copy of the reuse check.

The fake GCS blob gains upload_from_filename, the first test of a file
upload on the Stage 12 side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Stage 12 precompute app gains build_state_datasets (8 CPUs, 96 GiB,
two hours), one call per year, validating plan entries in and dumping
results out. run_precompute starts the state builds with the dataset
builds and waits for them only after the determinism check, before
publishing the manifest, so baselines never wait for them, then reports how
many were built and uploaded and their total size. The determinism record
still covers baselines only.

The manifest does not list the state datasets yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Stage 12 cache manifest model accepts state_dataset entries, with their
folder and state code, in their own model chosen by "type", so dataset and
baseline entries keep their wire shape. The image fetch now checks every
entry before downloading anything: it refuses unknown types, state datasets
that do not cover the same states in every year, unsafe folders and file
names, and two entries for one file (by folder and name, so every state's
same-named file can coexist), and places each state file in its folder.

The worker check expects the 63 national files plus one file per year for
each state the installed bundle maps to a non-default dataset, and requires
every listed state file on disk; the release it reports keeps today's
fields. No manifest lists state datasets yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build_manifest appends a state_dataset entry, with its folder, state code
and the stored digest and size, for every planned state file after the
datasets and baselines. A plan without state datasets publishes the
manifest it always did. The Stage 12 image fetch places each state file in
its folder, and the worker check expects them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage 12's worker asks its own copy of the baked-folder lookup, in
stage12_cache/state_datasets.py, for the Stage 12 image folder of the
calculation's state before loading a non-default dataset, and passes it to
_load_dataset. The lookup returns the folder only when the image holds that
state's year file for the year and .py's reuse checks accept it, so
ensure_datasets never rebuilds the regional dataset inside a baked folder;
otherwise the calculation prepares the dataset as before. District
calculations read their state's file and keep their district filter.
National, region-group and UK calculations are unchanged.

The selection-contract test now uses real RegionResolution and
DatasetSelection values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main-path US state and district runs load a non-default dataset (the
ACS-local dataset since .py 6.7), but their results named the default
dataset's release as data_version, because the runtime passed no resolved
data version and the year file carries none. The runtime now passes the
revision the .py release manifest gives that dataset
(populace-us-2024-buildo-acs-local-... for 6.7.5). National runs and every
run on the default dataset report the release's data version as before.

Stage 12 takes data_version from its dataset provenance; once state runs
record ACS-local there, both paths report the same value for them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage12CountryBundle gains region_datasets: for each region type in .py's
region_datasets, the identity of the bundle dataset its path names, or None
for a per-region file pattern such as states/{state_code}.h5; a path that
names no bundle dataset is refused. Bundles published before the field
have None. On the installed 6.7.5 bundle, state and congressional_district
regions load populace_us_2024_acs_local and national regions
populace_us_2024.

Entry services already running forbid unknown fields in the manifest they
read, so the v2 version manifest now goes under three keys:
active-with-region-datasets holds it all, active-with-cache keeps its
current shape without region datasets, and active keeps its shape without
cache metadata either. Loaders read the newest key a publish has written,
and a first publish keeps the versions stored under the older keys.

Adding the field changes the Stage 12 bundle digest, which keys every
Stage 12 cache file, so the first deploy rebuilds the Stage 12 cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Stage 12 worker now accepts a simulation whose provenance names the
dataset the installed bundle gives its region type (state and
congressional_district regions: the ACS-local dataset), with that dataset's
URI and revision, as well as the default dataset. The default stays
accepted for state and district runs because entry services released
before regional provenance send it until they are updated; any other
dataset, and a regional dataset for a national run, are refused as before.

After resolving the region's dataset, the worker requires the provenance to
name the dataset it loads, by name and URI, so a comparison record can no
longer name one dataset while the calculation used another. The one
accepted difference is default provenance on a run that loads a regional
dataset, from those older entry services.

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

The entry adapter now records, for a state or congressional-district
request, the dataset the worker's published bundle names for that region
type (the ACS-local dataset): its identity, URI and revision in the dataset
and bundle provenance, and its URI and SHA-256 as the population artifact.
A bare two-letter US region counts as a state, as the gateway reads it. The
entry service keeps its own copy of the worker's region-type reading.

National and UK requests, and requests to workers whose manifest predates
region datasets, still name the default dataset, and the request's data
value is still checked against the default. Workers accept regional
provenance since the previous commit, so they never refuse what this sends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adding region_datasets to the bundle changed bundle_digest for every
bundle. A deploy replaces the running v2 application, whose name depends
only on the .py version, before the new version manifest is published, and
entry services send the digest they read from that manifest; until the
publish, every comparison run would have been refused with "installed
bundle digest differs", and the Stage 12 cache, keyed by the digest, would
have been rebuilt. region_datasets comes from the same .py bundle whose
version the digest already covers, so the digest now leaves it out. The
installed 6.7.5 bundle keeps the digest it had before this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Stage 12 worker and the entry adapter each kept a copy of the
region-type reading, and neither ignored surrounding whitespace or upper
case as the runtime's region normalisation does: for a region of State/UT
the entry sent default provenance while the worker loaded the ACS-local
dataset, hidden only by the temporary acceptance of the default. The
contract library now has one region_type_of, matching the runtime's
reading, and Stage12CountryBundle.dataset_for_region, which both use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The US worker image's Stage 12 cache fetch kept its 1,800 s limit while
gaining a file per state for each precomputed year, downloaded and hashed
one after another on two CPUs. It now allows 3,600 s, as the main path's
fetch does for the same files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage 12's copy of the per-state build gets the fixes made to the main
path's:
- it refuses only district codes that name a registered district of a
  different state, so an unassigned code such as 0 no longer stops the
  precompute and the deploy waiting on it;
- a target without a row filter is refused before anything is written, and
  the target's strategy is typed as an optional RowFilterStrategy;
- builds are grouped by year and dataset, since one build prepares one
  dataset's year file;
- each result records how long the year's build had run, and the log
  reports per build the files built and uploaded, the bytes actually
  uploaded, and the build time.

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

The Stage 12 worker foundation document said the cache manifest held all 63
files and described two manifest keys. It now describes the per-state
datasets the precompute builds and the image places in state folders, the
third manifest key that carries region datasets, the dataset provenance
the entry service and worker now agree on, and why the bundle digest
leaves region datasets out.

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

The cache release a US worker reports is now built by
_cache_release_summary, and a test shows it still validates against
Stage12CacheRelease, which pins the national counts and forbids other
fields, when the installed manifest also lists state datasets. Another test
shows UK regions keep requiring the default dataset when the bundle names
dataset per region type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Code released before region datasets publishes only the active-with-cache
and active keys, so after a rollback the active-with-region-datasets copy
is left out of date. Entry services from this change would have kept
reading it, sending ACS-local provenance to the older workers, which refuse
it, and the next publish would have dropped the worker versions the older
code added. Readers and the publisher now use that copy only when, with its
region datasets removed, it equals the manifest under active-with-cache;
otherwise they read active-with-cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
region_type_of restates the runtime's region normalisation, so an executor
test now runs both on the same codes (US and UK, bare codes, mixed case,
surrounding spaces, prefixed codes). It found one difference: a bare "uk"
requested as a US region was read as a state; another country's code now
names no region type, as at run time.

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

When two bundle datasets have the same file path, region_datasets took the
identity first in sorted order, so an alias listed for the default dataset's
file would have become the national region's dataset and workers would
then refuse national runs naming the default. The default dataset now owns
its path; other collisions still take the first identity in sorted order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A test only showed that the digest ignores region_datasets, so the next
field added to the bundle could change every digest unnoticed, refusing
comparison runs during a deploy and rebuilding the Stage 12 cache. The
digest of a fixed bundle is now pinned to the value the code before
region datasets gives it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage 12's copy of the baked-folder lookup now logs a warning and returns
no folder when .py's year-file reuse checks, which import a private .py
helper, cannot be imported after an upgrade, so state and district
calculations prepare their dataset instead of failing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage 12's copy of the in-container checks now refuses a state target
without a row filter before the full-country year file is prepared, and
returns each state's row filter. The orchestration docstring says builds
run one call per year and dataset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Stage 12 worker foundation document said current readers read the
newest key a publish has written. It now gives the actual rule (the region
dataset copy only while it matches active-with-cache once region datasets
are removed) and the one window it leaves on a rollback to code from before
region datasets: until the older code publishes, entry services keep
sending regional provenance, which the older worker refuses, failing Stage
12 comparison runs for state and district requests but no production
response. The rollback step that avoids it is to delete the
active-with-region-datasets key before deploying the older code. A comment
in publish_v2_manifest now gives the right reason readers never combine
parts of two documents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pyright reads `Series.isin` as taking a sequence, mapping, Series or
DataFrame, so it refused the set of registered district codes. The
check now passes them sorted, which keeps the same matches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk
anth-volk force-pushed the feat/stage12-state-datasets branch from 2da4cdc to 6b23b26 Compare October 22, 2026 13:13
@anth-volk
anth-volk marked this pull request as ready for review October 22, 2026 15:04
@anth-volk
anth-volk merged commit 1f59a56 into main Oct 22, 2026
22 checks passed
@anth-volk
anth-volk deleted the feat/stage12-state-datasets branch October 22, 2026 15:06
anth-volk added a commit that referenced this pull request Oct 22, 2026
Brings in main up to 1f59a56 (#885 and the rules, Pyright and
locked-install work before it). Git merged every file without conflicts,
including changelog_entry.yaml, which keeps both sides' entries.

Checked after the merge: the contract (274 passed), gateway (261 passed)
and entry (662 passed) suites and the quality guards pass. Two known
failures remain, both fixed by the next commit:

- The executor suite has 2 failures out of 1789, the two routed-national
  tests in test_simulation_output_builder.py. main's run now passes
  data_folder= to _load_dataset (for the baked state folders), and the
  tests' fake does not take that keyword:
  TypeError: ... got an unexpected keyword argument 'data_folder'.
- The executor's Pyright check reports five reportPossiblyUnbound
  errors in _uk_area_simulations, which opens its spans with
  runtime.span(...) instead of scopes.span(runtime, ...).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

Bake per-state ACS-local year files into Stage 12 and record the dataset state runs load

1 participant