Repository navigation
Conversation
…ataset rule Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rea dataset (failing) A simulation run the simulation service starts on its own may later be named by a local-area report, which reads the area columns. Today the plan copies them only for constituency and local-authority regions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-area dataset The output plan copies UK_AREA_DATASET_COLUMNS for such a simulation too, by C1 · 23's rule (calculates_on_local_area_dataset), whether it is planned for a report or alone, so its run's file holds what a local-area report reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the local-area dataset Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # projects/policyengine-simulation-executor/tests/test_stage12_output_planning.py
Under the shared-report keys, a report whose baseline and reform have the same policy and dynamic names one simulation, and one run of it, on both sides. The contract refuses that today: ReportRunIds requires three different runs, and ReportExecutionInput different execution IDs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ReportRunIds requires only that the report run differ from its simulation runs, and names_one_simulation_run says when both sides are one run. ReportExecutionInput lets the two inputs share an execution ID when they are the same simulation, policy included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (failing) Its run's poverty and inequality rows must be one set, as for any run, not one per side with the same keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Its poverty and inequality rows come from the baseline side only, so the completion inserts no two rows with one key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sides (failing) It must start one worker for that run, or read its stored output once, and aggregate the one output as both sides. Today it starts a worker per role, the second of which finds the run held, and reads a stored file twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The coordinator starts the baseline's worker only, or reads the stored output once, and gives the reform the same output described for its role; the artifact both sides name is read and decoded once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n the aggregation Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (failing) Per C1 · 24's answer 5: the coordinator waits for a run another report run's worker, or an earlier attempt's worker, holds, never cancelling or releasing it, and reads it from its stored file once it succeeds. It fails such a run only when its holder's call ended without an outcome, and only while the run still records that call. A failing, cancelled or pre-empted coordinator leaves its workers and unstarted runs as they are. The existing tests of the earlier rules change with them: a pre-empted coordinator no longer cancels its workers, a restart no longer releases their runs, and a failed report run no longer fails its other runs. The release-based restart tests are replaced by ones that wait. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iling) A coordinator waiting for a run another call holds fails it, once that call ended without an outcome, only while the run still records that call and is unfinished. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing) A worker whose start Modal refuses fails the report run and leaves its run pending with no call, for another report run naming it to start. No coordinator releases a run any more, so the release write's tests go, and a missing run raises for the new conditional failure instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…for a report Per C1 · 24's answer 5 (2026-11-14): - The coordinator starts a worker on each run pending with no call, and waits for a run another call holds (shared_simulation_runs.WaitedRun): it probes that call, and once it has ended reads the run again. A run that succeeded is read from its stored file and checked as a reused run is; one that failed fails the report run with its own failure; one still unfinished under the ended call is failed while it records that call (fail_simulation_run_held_by, modal_call_ended). - A pre-empted coordinator cancels no worker, and its restart waits for its earlier workers. A failed or cancelled coordinator fails its report run only; its workers finish, and a refused start leaves its run pending. A worker's run is failed only for its own failure. - release_simulation_run goes: nothing releases a run any more. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…se test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Per C1 · 24's answer 4: a simulation's stored plan holds every column that doesn't depend on the other side, the area and US budget columns, the labour-supply responses only when its own policy turns them on, and never the cliff columns; US runs always copy the two inputs .py's labour-supply output reads. Each worker is given its own simulation's stored plan, and a side that stores no responses is read with zeros. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Per C1 · 24's answer 4 (2026-11-14): a simulation run's file depends on its simulation alone. resolve_simulation_output_plan, now each run's stored plan, holds every column that doesn't depend on the other side, the area and US budget columns, the labour-supply responses only when the simulation's own policy turns them on, the two US inputs .py's labour-supply output reads from either side, and never the cliff columns, so US national runs keep their region-group split. The report-run coordinator gives each worker its simulation's stored plan and keeps the report's own plan for the aggregation. Each artifact is checked against its own simulation's plan, a reused or waited-for file against what the report reads from its side (side_read_plan), and a side storing no responses the report reads is read with zeros. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shared simulation runs never store the cliff columns, and cliffs come back in a later stage as a calculation of each simulation run (the user, 2026-11-14). Until then such a report run fails before any worker starts, with cliff_impacts_unavailable, and touches none of its simulation runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A report run with include_cliffs true fails before any run is read or started, with cliff_impacts_unavailable, which no reuse refusal matches, so no simulation run is marked as refused. Cliffs return in a later stage as a calculation of each simulation run (the user, 2026-11-14). A reuse test that sent include_cliffs only to check that options reach the SPM resolver now sends the SPM selection alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 1, bug 1: a failed probe of a held call, or of the coordinator's own worker, was taken as the call ending, failing a healthy shared run. It must count only once Modal reports the call ended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 1, bug 1: Modal's client raises for a lost connection too, so a failed probe of a held call, or of the coordinator's own worker, failed a healthy shared run. WaitedRun and the new ProbedWorkerCall now ask Modal for the call's state, as the stuck-run sweep does (modal_call_state, a CoordinatorDependencies seam), and keep waiting, within the wait window, while Modal reports the call running or can't say. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ailing) Review round 1, bug 2: a run another call held is read without the check a reused run gets, that both sides name one catalog dataset and region. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 1, bug 2: as for a reused run, a run another call held must name the catalog dataset and region of the report run's other simulation run, or the report run fails with reused_simulation_run_mismatch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With shared simulation runs, new report runs may keep naming, and waiting for, a run whose call Modal never ends, so the sweep's 24-hour age limit must list and fail a simulation run whatever report runs name it. Its other passes keep leaving such a run to their coordinators. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With shared simulation runs, new report runs may keep joining, and waiting for, a run whose call Modal never ends, so the exclusion of runs an unfinished report run names would keep the 24-hour age limit from ever failing it. list_active_runs and fail_stuck_run take regardless_of_report_runs, which only the age limit passes; the sweep's other passes still leave such a run to its coordinators. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Docstrings of the worker, the reuse module, the child wait, the store's completion and failure writes, and the Stage 12 doc no longer describe releasing a run or stopping unfinished workers for a report's sake. - CoordinationCore.stop_children, which nothing calls any more, goes. - recorded_run_failure builds a failed run's detail for both the coordinator's prepared runs and its waited-for runs, with one simulation_run_failed code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ling) Review round 1: since the contract accepts one simulation run on both sides of a report run, a comparison-path report with one execution ID would claim its record and fail later on its child records' keys. It must be refused before anything is written, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only a report run may name one simulation run on both sides; the comparison path keys its child records by execution ID, so it refuses such an input before writing anything, as the contract did before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…shared Review round 1's missing cases, each passing against the code already there: a waited-for run whose call ends leaving it to another call is waited for under that call; one left pending with no call gets this coordinator's own worker; and one run on both sides that another call holds is waited for and read once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Per the user (2026-11-14), C1 · 26 covers the household function too. Like the coordinator, it must wait for a side another call holds, after the sides it can claim, using the run once it succeeds and failing it only once its call ended without an outcome; fail only the runs it holds; and calculate a run named on both sides once, which the household contract must accept when both sides are one calculation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Per the user (2026-11-14), C1 · 26 covers the household function too: - A side whose run another call holds is waited for once the sides this call can claim are done (wait_for_held_run with a WaitedRun, within HOUSEHOLD_SIDE_WAIT_SECONDS of the call's start): used once it succeeds, its recorded failure is the side's, and it is failed only once Modal reports its call ended. A side still held when the window closes fails the report run and is left to its call. - A run named on both sides is calculated once; the household contract accepts one run on both sides when both are one calculation, and the simulation service's builder test now checks two policies are refused. - A failing call fails only the runs it holds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e them (failing) A report run's worker looks its current-law US baseline up in the precomputed cache under the digest of the plan its simulation alone decides, but the precompute plans its baselines as one side of a current-law report, so the digests differ and every lookup misses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each precomputed side is planned with resolve_simulation_output_plan, so a report run's current-law US national baseline finds its precomputed files again. A comparison-path call still plans its baseline with its report's plan, so its baseline now calculates all 20 region groups; the first deployment computes the 60 baseline files under their new identities. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A budget window's later year names simulation runs that stay pending with no call until the earlier years end, which can take longer than the age limit; the age limit, which ignores the report runs naming a run, fails them although no call has held them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The age limit fails a simulation run whatever report runs name it only when the run records a call, which may never end while new report runs keep joining it. A run no call has claimed, such as a budget window's later year waiting for the earlier years, waits for the report runs naming it, as it did before shared runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ling) A failed probe of a held run's call asks Modal for the call's state even when the run's row already says the run finished. Also covers what was untested: Modal unable to say a call's state keeps the wait going, an ended call's unfinished run is failed under that call, and an older stored file lacking a column the report reads is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A probe of the holding call that raises now reads the run first: a run that finished, or moved to another call, is taken from its row, and only one still unfinished under the probed call asks Modal for the call's state. The module docstring no longer says the stuck-run sweep never judges a run an unfinished report run names, which its age limit does. The report's own plan is now required where stored files are checked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A forced re-run's simulation runs are extra runs no other report run joins (C1 · 24, answer 3), so its coordinator finds both pending with no call, starts a worker on each and waits for no other call, with no marker telling it the run was forced. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A household side's run may be held by another household function, which records the run and goes on to its other side and its own waits. The wait probes that call until it ends, so a side whose run already succeeded, or failed, waits out the whole window, and a failed run then fails the report run as still running under another call rather than with its recorded failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The call holding a household side's run is another household function, which goes on to its other side and its own waits once it has recorded the run. The household wait now reads the run before each probe (WaitedRun.read_run_first) and takes a run that succeeded, failed, was left unstarted or moved to another call at once, so household report runs whose sides overlap no longer wait out their windows, and a failed run fails the report run with its recorded failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The household function claims and calculates a waited-for side left pending with no call, waits under a later call for a side moved to it, waits once for one held run on both sides, and counts its own calculation in its wait window. A failed probe of a run moved to another call follows it without asking Modal, and a waited-for file lacking a column the report reads is refused. All pass against the code already there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reason of a side still held when the window closes says the run is still running "after 780 seconds", but the window counts from the call's start, so the wait itself was shorter. A side whose run succeeds on the window's last probe still succeeds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the wait window closes, the household function reads the side's run again: one that succeeded or failed meanwhile is taken as it is, and one still held says it was still running when the window closed, counted from the call's start. One helper now answers for every side whose run another call finished, which removes a branch nothing reached. The household functions' Modal timeout moves to stage12_timeouts, where the wait window is derived from it, and the stale docstrings and README text on the household function's failures are corrected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four household tests still expected a refused or mismatched payload to fail the report run's simulation runs; since the household function fails only the runs it holds, they expect the report run alone to fail, and run_household_report's docstring says so. New tests cover a side whose run fails on the wait window's last probe, and a wait that reads the run first finding it moved or unstarted without a probe. The household finishing time's comment says no write leaves a held run pending with no call today, and the coordinator test of fresh runs is named for the state it reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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 #1071
Same-Repository Draft PR Check
PolicyEngine/policyengine-sim-api, not a personal fork.Changelog
changelog.d/c1-26-shared-simulation-runs.changed.md, whose type is breaking, added, changed, fixed or removed.Summary
Stage 12's v2 functions can now share simulation runs between report runs, as the Public API's simulation service is to do for identical requests. This follows the migration's shared-run decisions: answer 4 (each run stores what its own simulation decides), answer 5 (the coordinator waits for a run another call holds), answer 8 (household reports are keyed too, so the household function gets the same treatment), interim A (report runs asking for cliffs are refused for now; cliffs come back in a later stage as a calculation of each simulation run), and answer 3 with interim B (forced re-runs, and for now re-runs without a key, calculate extra runs nobody joins; a test covers this, and it needs no code).
Stacked on #1068, the local-area report profile; its commits up to 45ffb33 are merged in.
Waiting instead of cancelling (
stage12_runtime.shared_simulation_runs,report_run_coordination):fail_simulation_run_held_by,modal_call_ended), so every report run waiting for it stops.modal_call_state), because Modal's client also raises for a lost connection while the call goes on.release_simulation_runis removed.The household function (
household_report_recording):HOUSEHOLD_SIDE_WAIT_SECONDS, derived from its 900-second Modal timeout less two minutes to finish). The holding call is another household function, which goes on after recording the run, so the wait reads the run before each probe and takes a finished run at once. A side still held when the window closes fails the report run withhousehold_simulation_run_unavailable, and the run is left to its call.One simulation run on both sides:
What a simulation run stores (
output_planning.resolve_simulation_output_plan):reused_simulation_output_incompatibleotherwise.cliff_impacts_unavailable, and touches none of its simulation runs.Precomputed US baselines are planned the same way, so a report run's current-law US national baseline finds them. The first deployment computes the 60 baseline files again under their new cache identities. A comparison-path call (a shadow comparison with a production job, or the operator-only
POST /internal/stage12/reports) still plans its baseline with its report's plan, so its US national baseline no longer reads the precomputed files and calculates all 20 region groups. This costs compute, not correctness.Stuck-run sweep:
For the simulation service (C1 · 27): it must not create shared pending runs, that is, name a run another report run created and that hasn't succeeded, until this is deployed to the default worker version's Modal application. Until then, a report run naming a run another worker holds would cancel that worker, and a failing report run would fail runs others wait for. Joining a run that already succeeded is safe today.
Deployment and environment:
v2_app.pychanges only where the household functions' 900-second timeout comes from: it now importsHOUSEHOLD_FUNCTION_TIMEOUT_SECONDSfromstage12_timeouts, which it already imported from.modal_call_state) and restore calls by ID with the Modal client inside their containers, as the stuck-run sweep and the coordinator's restore already do. This is not yet verified in a deployed container.Known limits:
Testing
🤖 Generated with Claude Code