Repository navigation
Bake per-state ACS-local year files into the main path's worker image - #879
Merged
Merged
Conversation
Merged
2 tasks done
RegionResolution gains state_code: the two-letter code of the US state a state or congressional-district region lies in, taken from the .py region. National, UK and region-group resolutions leave it None. The baked per-state datasets added next are looked up by this code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_load_dataset takes an optional data_folder that replaces the folder the dataset selection would otherwise use (the image's data folder for the default dataset, a temporary folder for any other). Callers are unchanged; the main path and Stage 12 will each pass the folder of a baked per-state dataset when one exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A per-state dataset is a regional dataset's year file filtered to one state's households. Its content key (schema sds1) covers the regional dataset's name, revision-pinned URI and certified SHA-256, the year, the state code and FIPS, the filter variable, the model and .py versions and the SPM selection. Its store path is state-datasets/<country>/<digest>/<stem>_year_<year>.h5, keeping the basename ensure_datasets resolves. state_datasets.py gives each state's folder under the image data folder, state-datasets/<dataset>/<STATE>, so the manifest builder and the runtime lookup agree on where a state's files live. The default dataset's key and identity are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
state_dataset_targets resolves every US state region exactly as a request for it is resolved and keeps the states whose dataset is not the bundle default, with their row filter (state_fips) 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, runtime filename, image folder and store presence; select_work keeps the missing ones. A bundle that sends states back to the default dataset plans none. Nothing builds or publishes these entries yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build_state_datasets_impl re-derives each planned state's key in its own container and refuses to build when the planner disagreed, then has .py's ensure_datasets prepare the regional dataset's full-country year file once, collects garbage, and reloads that one year. It refuses data in which a household's congressional district code names another state, since district requests read their state's file. For each state it keeps the rows the state region's own scoping strategy keeps, writes them with .py's save(), copies the year-file stamp that save() drops, confirms that .py's reuse checks (stamp and renames record, then the legacy SPM record) accept the file, and uploads it write-once. Weights are unchanged. The tests write a three-household year file with .py's own save, stamp and SPM record, and show that .py's real ensure_datasets reuses a state file without rebuilding it. The identity stubs now import .py's US dataset module before patching the manifest, so a stubbed test cannot bind the stub into it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The image's artifact fetch now puts an artifact with a "folder" at <data folder>/<folder>/<filename>, so per-state datasets, which all share the year file's basename, land in their own state folders. Artifacts without a folder still go straight into the data folder. Before anything downloads, it refuses absolute folders, folders with "." or ".." parts, empty parts or backslashes, filenames that are not bare names, and two entries for the same file. The fetch layer's timeout rises from 900 s to 3,600 s for the several GB of per-state datasets. No manifest lists such artifacts yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The precompute app gains build_state_datasets (8 CPUs, 96 GiB, two hours), one container per year, validating plan entries in and dumping build results out like the other wrappers. run_precompute spawns the state builds with the dataset builds and waits for them only before publishing the manifest, so baseline computation never waits for them, then reports how many state datasets were built and uploaded and their total size. The manifest does not list the state datasets yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build_manifest appends a state_dataset entry for every planned state file, with its folder and state code, after the datasets and baselines. The new entries use their own StateDatasetArtifact model, chosen by "type", so a plan without per-state datasets publishes byte-for-byte the manifest it always did (the pinned wire-shape test is unchanged). The deploy-time manifest validation accepts the new entries and hands them to the fetch layer, which places each in its state's folder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before loading a non-default dataset, the main path's runtime asks state_datasets.baked_state_data_folder for the image folder of the request's state (a district passes its state's code). It returns the folder only when the image holds that state's year file for the requested year and .py's reuse checks (stamp and renames record, legacy SPM record) accept it; a file .py would rebuild is skipped, so ensure_datasets never rebuilds the full regional dataset inside a baked folder. _load_dataset then reads from that folder, and the district filter still applies at run time. Otherwise the request prepares the regional dataset on demand, as before. Runs whose region loads a non-default dataset record dataset_source as baked_state or on_demand. National and default-dataset runs are unchanged, and Stage 12 still loads without a folder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The per-state build refused the whole year whenever any household's congressional_district_geoid did not start with its own state FIPS, including codes that name no district, such as an unassigned 0. Such a household matches no district request on either path, so it cannot change a result, yet it would have stopped the precompute and the deploy waiting on it. The build now refuses only codes that name a registered district of a different state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
run_precompute grouped state entries by year only, while one build prepares a single dataset's year file and refuses entries for two. A bundle sending different states to different datasets would have failed every year's build; builds are now grouped by year and dataset. Each state build result records how long the year's build had run when the file was uploaded, and the precompute log now reports, per build, how many files were built and uploaded, the bytes actually uploaded (files already stored no longer count), and the build time, for tuning the two-hour limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A district request is now followed from region resolution, with the real dataset selection and baked-folder lookup over a stamped Utah file, to the folder _load_dataset is given; the old test only repeated a state lookup. Each state file's rows are now compared with .py's filter applied to the full source frames, instead of with the file filtered again, which would not have noticed another state's rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
StateDatasetTarget.scoping_strategy is typed as an optional RowFilterStrategy instead of Any, and a build whose target has none raises a clear error before writing anything, instead of failing on None. A comment in artifact_keys now says that per-state datasets are fetched into their state's folder rather than straight into the data folder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The baked-folder lookup runs .py's reuse checks, one of which imports a private .py helper. If a .py upgrade moved it, every state and district request would have failed with ImportError while loading its dataset. The lookup now logs a warning and returns no folder, so the request prepares its dataset on demand as it would without a baked file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The check for a state target without a row filter ran inside the per-state loop, after the full-country year file was prepared and earlier states were uploaded. _checked_state_targets now refuses it with the other in-container checks, before anything is prepared, and returns each state's row filter. Docstrings now say builds run one container per year and dataset. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main now runs Pyright on every package (#864). Against the executor's baseline, this branch added seven errors: - The run's new reads of `dataset_selection`, `region_resolution` and `simulation_params` were reported as possibly unbound, because `runtime.span(...)` is typed as possibly suppressing exceptions. The run's spans now open with `scopes.span(runtime, ...)`, as docs/engineering/skills/type-checking.md says. `_build_simulation` is annotated with the `StageReportingSimulation` it returns, which the progress callback needs. - `build_state_datasets` couldn't be passed as a `RemoteFunction`, because Pyright doesn't resolve `modal` in src/modal to the Modal package. The precompute app casts its six Modal functions to that protocol. - The manifest's artifact list is built with `ManifestEntry` elements, and the district-code check passes `isin` a sorted list. 54 baseline entries no longer occur, so the baseline drops from 178 to 124 entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
force-pushed
the
feat/state-datasets
branch
from
October 21, 2026 21:47
9eb6790 to
aaa97c8
Compare
anth-volk
marked this pull request as ready for review
October 21, 2026 22:00
anth-volk
added a commit
that referenced
this pull request
Oct 21, 2026
Without extraPaths, Pyright adds the executor's src directory as an import root, so `import modal` in the Modal app modules under src/modal resolved to that directory, and every modal attribute was unknown. At runtime src is never an import root: the package is installed from src, and the app modules import each other as src.modal.<module> from the project directory, which Pyright still resolves. `extraPaths = []` in the executor's [tool.pyright] turns the src root off. Only Pyright reads it; nothing about how Modal loads the apps changes. That removes 42 errors and exposes 4 the shadowing hid. The previous two commits fixed 3 of them (the routing store and the batch call id). This one fixes the fourth: the segmented national child spawn opens its span with scopes.span, so Pyright no longer reports the method as missing a return. The precompute app already casts its six Modal functions to RemoteFunction (#879), so passing them to PrecomputeFunctions reports nothing. Its comment said Pyright doesn't resolve `modal` there; it now says why the casts are still needed: each modal.Function's `remote` and `spawn` take that function's own parameters, which Pyright doesn't accept for RemoteFunction's `*args: Any`. The executor's baseline drops from 119 entries to 77. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 21, 2026
#879 already opens the annual run's eight request, dataset, policy and simulation-build spans in simulation_runtime.py with scopes.span, and annotates _build_simulation as returning the StageReportingSimulation that _report_calculation_start needs. This commit opens the ninth span, credential setup in setup_gcp_credentials, the same way, and documents why _build_simulation returns a StageReportingSimulation in both branches (ArtifactBaselineSimulation subclasses it). Neither change alters what a run does, and the executor's baseline stays at 77 entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 21, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #878
Same-Repository Draft PR Check
PolicyEngine/policyengine-sim-api, not a personal fork.Summary
US state and congressional-district requests no longer download the 9.8 GB ACS-local dataset, or prepare it for every state, on each new worker. The precompute prepares one year file per state for 2025–2027, and the image includes them. Each commit is a separate change:
RegionResolution.state_code. Resolving a state or district region now reports the two-letter code of its state. National, UK and region-group resolutions report none. This is a shared helper, and Stage 12 is unaffected._load_dataset(data_folder=...). Callers can choose the folder a year file is read from. If they don't, the folder is chosen as before. This is a shared helper.sds1and folder layout.state-datasets/us/<digest>/<stem>_year_<year>.h5.state-datasets/<dataset>/<STATE>.state_dataset_targets()resolves every state region the way a request for it would be resolved. It keeps only the states whose dataset is not the default. The plan lists one entry per year and state, in a fixed order. A bundle that maps states back to the default dataset plans none.build_state_datasets_impl:save();save()drops, and confirms .py's reuse checks accept the file;folderis placed in that folder. Absolute or unsafe folders and filenames are refused before anything downloads, as are repeated files. The fetch timeout rises from 900 s to 3,600 s.build_state_datasetsruns with 8 CPUs, 96 GiB and a two-hour limit, one container per year and dataset. It starts alongside the dataset builds. The run waits for it only before publishing the manifest, so baselines never wait. For each build it logs the files built and uploaded, the bytes uploaded and the time taken.state_datasetentries, each withfolderandstate_code. They use their own model, chosen bytype. A plan with no per-state files publishes byte for byte the same manifest as before.dataset_sourceasbaked_stateoron_demand.Each state file holds exactly the rows the run-time state filter would keep from the full-country file, and carries the same stamp and records. Results are therefore unchanged.
First deploy. The precompute builds 153 per-state files (51 states × 3 years), each from a 9.8 GB source, and the image copies in several GB more.
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.
Real .py year files. The per-state build tests write a three-household year file with .py's own
save(), version stamp and SPM record. They show that .py's realensure_datasetsreuses a state file without rebuilding it. Re-applying the state filter keeps the same rows, and weights are unchanged.Executor suite.
uv run pytest tests/inprojects/policyengine-simulation-executor: 1,640 passed, 22 skipped.Code review. The
review-codereview found no bugs. Its points are addressed in the last four commits, each test-first:A second round found no bugs. Its points are addressed in the last two commits:
Quality checks.
python scripts/run_quality_guards.py: passed.Pyright.
python -m scripts.rules pyright(pinned 1.1.414) passes for the executor. Main started running Pyright (Run Pyright in CI for every package, in its own locked environment #864) after this branch was written, and the last commit fixes the seven errors the branch added:scopes.span(runtime, ...), asdocs/engineering/skills/type-checking.mdsays, and_build_simulationis annotated with theStageReportingSimulationit returns;RemoteFunction;This also clears 54 existing errors, so the executor baseline drops from 178 to 124 entries. That changes
rules/, which needs code-owner approval.Ruff 0.15.22 (the version CI pins):
ruff check .andruff format --check projects/*/src libs/*/srcare clean.Not run locally: the Modal image smoke, the Docker build and the local integration tests. CI runs them.
Planned after the beta deploy:
dataset_source=baked_statewithdataset_loadin seconds;🤖 Generated with Claude Code