From 7e55aa530768596c8996741ade101f47e7fba8fc Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:19:23 +0400 Subject: [PATCH 1/6] Build congressional districts for state runs only Nationwide reports in the app now take congressional districts from a separate run of each state and DC on that state's dataset, so a district shows the same figures in a nationwide report as in its state's report. National runs no longer need to compute them. _should_build_us_congressional_district_impact now allows only state/* regions, in any case. National runs (no region, or us) and region-group runs return congressional_district_impact null. Segmented national children run as region groups, and the segmented national reduce and Stage 12 aggregation build their output with the national region, so all of them follow the same check. District and place runs still compute none. The contract field is already nullable. Tests, seen failing before the change: - test_region_group_resolution.py: state/ca and STATE/CA are allowed; None, us and region_group/... are refused - test_simulation_output_builder.py: the output builder skips the district calculation for None, us and a region group, and still runs it for state/ut Co-Authored-By: Claude Opus 5.5 --- changelog_entry.yaml | 1 + .../segmented_national_reduce.py | 5 ++- .../simulation_output_geographic.py | 23 ++++++---- .../tests/test_region_group_resolution.py | 23 +++++----- .../tests/test_simulation_output_builder.py | 45 +++++++++++++++++++ 5 files changed, 76 insertions(+), 21 deletions(-) diff --git a/changelog_entry.yaml b/changelog_entry.yaml index 2e68ba388..40f353b66 100644 --- a/changelog_entry.yaml +++ b/changelog_entry.yaml @@ -16,6 +16,7 @@ - Ran Stage 12 US national simulations as 20 region-group calls each, and built an independent, immutable Stage 12 cache of three annual US datasets and 60 current-law baselines before each deployment, publishing it only after a recorded determinism check and recording its release in the v2 version manifest under a new key, while the existing key keeps a copy that earlier readers accept - Made the budget refuse a household count outside its country's plausible range (above 200 million in the US or 60 million in the UK, negative or not finite), raising ImplausibleEntityCountError instead of publishing a count like #767's changed: + - Made only US state runs compute congressional district impacts; national runs (no region, or us) and region-group runs, and so segmented national runs, their reduce and Stage 12 aggregation, now return congressional_district_impact null, because nationwide reports take each district from a separate run of its state on that state's dataset, which matches the state's own report - Moved the worker's credential setup, request year and policy parsing, and region resolution out of simulation_runtime.py (1,111 lines, now 651) into gcp_credentials.py, request_policy.py and region_resolution.py; simulation_runtime.py keeps dataset selection and loading, simulation building, local-area breakdowns and the run itself, which tests replace by name there, and re-exports the moved names, so every caller still imports them from simulation_runtime; the shared helpers _parse_year, _normalise_policy, _compile_interval_policy, _runtime_policy (now wrapper_policy), _resolve_region, _resolve_dataset_reference and _require_local_area_breakdown are now public, and the credential messages still log under the simulation_runtime logger; the 3,048-line output builder test file is split into seven test files and a module of shared fakes, all named test_simulation_output_*.py so the weighted-frame rule still checks them - Split the artifact precompute (precompute.py, 931 lines) and the Stage 12 cache precompute (stage12_cache/precompute.py, 844 lines) each into planning, dataset-building and baseline modules, with the manifest and run_precompute left in the original module, which re-exports every public name, so the Modal precompute apps and their tests import from the same paths; the cache precompute's _scoped_planned_simulation and _baseline_identity are now public, and the worker validation imports scoped_planned_simulation from its new module; the 1,835-line precompute test file is split into four, and the cache precompute tests patch each name in the module whose code reads it - Split the 4,709-line Stage 12 runtime test file into thirteen test files by behaviour (dataset loading, provenance, the single worker and its failures, aggregates, the coordinator and its failures, runtime store retries, failure records, cancellation, child state errors and the wait window) and three support modules for the shared builders, fake stores and fake child calls, whose helpers are now public; the other Stage 12 tests import them from those modules, every test keeps its name and body, and two parametrized cases are named after the renamed bundle helper diff --git a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/segmented_national_reduce.py b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/segmented_national_reduce.py index 26ba85cac..4d059e96f 100644 --- a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/segmented_national_reduce.py +++ b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/segmented_national_reduce.py @@ -142,8 +142,9 @@ def build_national_output( """Reduce the children's microdata into the full national macro output dict. Runs the existing ``SimulationOutputBuilder`` over stand-in simulations - with ``resolved_region_code=country`` so the national congressional - district table populates. No model recompute occurs. + with ``resolved_region_code=country``, as for any national run, so the + output has no congressional districts: nationwide reports take those from + separate state runs. No model recompute occurs. """ country = inputs.country country_module = inputs.country_module diff --git a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation_output_geographic.py b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation_output_geographic.py index 4ce4a49b8..c1e315eb3 100644 --- a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation_output_geographic.py +++ b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation_output_geographic.py @@ -223,12 +223,16 @@ def _integer_record_value(record: Mapping[str, Any], key: str) -> int | None: def _should_build_us_congressional_district_impact(region_code: str | None) -> bool: - if region_code is None: - return True - normalized_region_code = region_code.lower() - return normalized_region_code == "us" or normalized_region_code.startswith( - ("state/", "region_group/") - ) + """Return whether a US run for this region computes congressional districts. + + Only state runs do, such as ``state/ca`` in any case. National runs (no + region, or ``us``) and region-group runs compute none, so neither do + segmented national runs, their reduce or Stage 12 aggregation: a + nationwide report takes its districts from a separate run of each state + on that state's dataset, which gives each district the same figures as + its state's own report. District and place runs compute none either. + """ + return region_code is not None and region_code.lower().startswith("state/") def build_congressional_district_impact( @@ -238,10 +242,11 @@ def build_congressional_district_impact( *, region_code: str | None = None, ) -> CongressionalDistrictImpactOutput | None: - """Compute US congressional district impacts for national, state or group runs. + """Compute US congressional district impacts for a state run. - None for other countries or regions, and when the calculation fails (the - failure is logged; SPM selection errors still propagate). + None for other countries and other regions, including national and + region-group runs, and when the calculation fails (the failure is logged; + SPM selection errors still propagate). """ if country != "us": return None diff --git a/projects/policyengine-simulation-executor/tests/test_region_group_resolution.py b/projects/policyengine-simulation-executor/tests/test_region_group_resolution.py index 3c366b068..01d3830b4 100644 --- a/projects/policyengine-simulation-executor/tests/test_region_group_resolution.py +++ b/projects/policyengine-simulation-executor/tests/test_region_group_resolution.py @@ -1,4 +1,4 @@ -"""Unit tests for region-group resolution (B2) and the CD-impact gate (B3).""" +"""Unit tests for region-group resolution (B2) and the district-impact regions (B3).""" from unittest.mock import MagicMock @@ -11,7 +11,7 @@ from policyengine_simulation_executor import region_resolution from policyengine_simulation_executor import simulation_runtime as sr from policyengine_simulation_executor.simulation_output_geographic import ( - _should_build_us_congressional_district_impact as gate, + _should_build_us_congressional_district_impact as should_build_districts, ) @@ -119,13 +119,16 @@ def test__resolve_region_dispatches_to_group(self, monkeypatch): assert isinstance(res.scoping_strategy, RegionGroupStrategy) -class TestCongressionalDistrictGate: - def test__allows_national_state_and_group(self): - assert gate(None) is True - assert gate("us") is True - assert gate("state/ca") is True - assert gate("region_group/state/hi+state/ia") is True +class TestCongressionalDistrictRegions: + def test__allows_states(self): + assert should_build_districts("state/ca") is True + assert should_build_districts("STATE/CA") is True + + def test__rejects_national_and_group(self): + assert should_build_districts(None) is False + assert should_build_districts("us") is False + assert should_build_districts("region_group/state/hi+state/ia") is False def test__rejects_district_and_place(self): - assert gate("congressional_district/CA-01") is False - assert gate("place/CA-12345") is False + assert should_build_districts("congressional_district/CA-01") is False + assert should_build_districts("place/CA-12345") is False diff --git a/projects/policyengine-simulation-executor/tests/test_simulation_output_builder.py b/projects/policyengine-simulation-executor/tests/test_simulation_output_builder.py index 7166450fb..b75e5e92a 100644 --- a/projects/policyengine-simulation-executor/tests/test_simulation_output_builder.py +++ b/projects/policyengine-simulation-executor/tests/test_simulation_output_builder.py @@ -199,6 +199,51 @@ def fake_build_congressional_district_impact( assert calls == [("us", baseline, reform, "state/ut")] +@pytest.fixture +def district_calculations(monkeypatch): + """Record each call to policyengine.py's district calculation, in its place.""" + calculations = [] + + def record_district_calculation(baseline, reform): + calculations.append((baseline, reform)) + return SimpleNamespace(district_results=[]) + + monkeypatch.setattr( + "policyengine.outputs.congressional_district_impact." + "compute_us_congressional_district_impacts", + record_district_calculation, + ) + return calculations + + +@pytest.mark.parametrize( + "region_code", + [None, "us", "region_group/state/hi+state/ia"], +) +def test_builder_skips_districts_for_national_and_region_group_runs( + district_calculations, region_code +): + baseline, reform = macro_baseline_reform() + builder = make_output_builder( + "us", baseline, reform, BuilderOptions(resolved_region_code=region_code) + ) + + assert builder._build_congressional_district_impact() is None + assert district_calculations == [] + + +def test_builder_builds_districts_for_state_runs(district_calculations): + baseline, reform = macro_baseline_reform() + builder = make_output_builder( + "us", baseline, reform, BuilderOptions(resolved_region_code="state/ut") + ) + + assert builder._build_congressional_district_impact() == ( + CongressionalDistrictImpactOutput(districts=[]) + ) + assert district_calculations == [(baseline, reform)] + + def test_builder_records_output_timings_without_real_calculation(monkeypatch): baseline = object() reform = object() From 994c7e31289b9988608a872722fd440d15d90389 Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:20:16 +0400 Subject: [PATCH 2/6] Expect no congressional districts on a US national integration result National runs now return congressional_district_impact null, so the beta check in test_calculate_default_model would fail against them. A new helper, assert_no_congressional_district_results, checks that the field is present and null, and test_calculate_default_model uses it in place of the at-large district check. The state checks still expect UT- districts. _AT_LARGE_DISTRICT_IDS and assert_congressional_district_results' expected_district_ids option served only the national check, so they go. Tests, seen failing before the helper existed: local tests that the helper accepts a null field, in a dict or a generated client object, and refuses an empty district list, a listed district, a generated client object with districts, and a result without the field. Co-Authored-By: Claude Opus 5.5 --- changelog_entry.yaml | 2 +- .../tests/simulation/test_calculate.py | 63 ++++++++++++++----- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/changelog_entry.yaml b/changelog_entry.yaml index 40f353b66..2daf91fab 100644 --- a/changelog_entry.yaml +++ b/changelog_entry.yaml @@ -16,7 +16,7 @@ - Ran Stage 12 US national simulations as 20 region-group calls each, and built an independent, immutable Stage 12 cache of three annual US datasets and 60 current-law baselines before each deployment, publishing it only after a recorded determinism check and recording its release in the v2 version manifest under a new key, while the existing key keeps a copy that earlier readers accept - Made the budget refuse a household count outside its country's plausible range (above 200 million in the US or 60 million in the UK, negative or not finite), raising ImplausibleEntityCountError instead of publishing a count like #767's changed: - - Made only US state runs compute congressional district impacts; national runs (no region, or us) and region-group runs, and so segmented national runs, their reduce and Stage 12 aggregation, now return congressional_district_impact null, because nationwide reports take each district from a separate run of its state on that state's dataset, which matches the state's own report + - Made only US state runs compute congressional district impacts; national runs (no region, or us) and region-group runs, and so segmented national runs, their reduce and Stage 12 aggregation, now return congressional_district_impact null, because nationwide reports take each district from a separate run of its state on that state's dataset, which matches the state's own report; the beta integration check of a US national result now expects congressional_district_impact null, and the state checks still expect the state's districts - Moved the worker's credential setup, request year and policy parsing, and region resolution out of simulation_runtime.py (1,111 lines, now 651) into gcp_credentials.py, request_policy.py and region_resolution.py; simulation_runtime.py keeps dataset selection and loading, simulation building, local-area breakdowns and the run itself, which tests replace by name there, and re-exports the moved names, so every caller still imports them from simulation_runtime; the shared helpers _parse_year, _normalise_policy, _compile_interval_policy, _runtime_policy (now wrapper_policy), _resolve_region, _resolve_dataset_reference and _require_local_area_breakdown are now public, and the credential messages still log under the simulation_runtime logger; the 3,048-line output builder test file is split into seven test files and a module of shared fakes, all named test_simulation_output_*.py so the weighted-frame rule still checks them - Split the artifact precompute (precompute.py, 931 lines) and the Stage 12 cache precompute (stage12_cache/precompute.py, 844 lines) each into planning, dataset-building and baseline modules, with the manifest and run_precompute left in the original module, which re-exports every public name, so the Modal precompute apps and their tests import from the same paths; the cache precompute's _scoped_planned_simulation and _baseline_identity are now public, and the worker validation imports scoped_planned_simulation from its new module; the 1,835-line precompute test file is split into four, and the cache precompute tests patch each name in the module whose code reads it - Split the 4,709-line Stage 12 runtime test file into thirteen test files by behaviour (dataset loading, provenance, the single worker and its failures, aggregates, the coordinator and its failures, runtime store retries, failure records, cancellation, child state errors and the wait window) and three support modules for the shared builders, fake stores and fake child calls, whose helpers are now public; the other Stage 12 tests import them from those modules, every test keeps its name and body, and two parametrized cases are named after the renamed bundle helper diff --git a/projects/policyengine-apis-integ/tests/simulation/test_calculate.py b/projects/policyengine-apis-integ/tests/simulation/test_calculate.py index 105d7bcb6..16b89ff0f 100644 --- a/projects/policyengine-apis-integ/tests/simulation/test_calculate.py +++ b/projects/policyengine-apis-integ/tests/simulation/test_calculate.py @@ -34,7 +34,6 @@ "no_change_percentage", "population", } -_AT_LARGE_DISTRICT_IDS = {"AK-01", "DC-01", "DE-01", "ND-01", "SD-01", "VT-01", "WY-01"} # From the generated client, so a stage added to the schema is accepted here. _JOB_STAGES = frozenset(stage.value for stage in JobStage) @@ -196,7 +195,6 @@ def assert_congressional_district_results( economy_result: object, *, expected_district_prefix: str | None = None, - expected_district_ids: set[str] | None = None, ) -> None: economy_result = _as_mapping(economy_result, label="economy result") assert "congressional_district_impact" in economy_result, ( @@ -237,13 +235,22 @@ def assert_congressional_district_results( f"got {district_ids}" ) - if expected_district_ids is not None: - district_ids = set(_district_ids(district_results)) - missing = expected_district_ids - district_ids - assert not missing, ( - f"Missing expected district IDs {sorted(missing)} " - f"from result IDs: {sorted(district_ids)}" - ) + +def assert_no_congressional_district_results(economy_result: object) -> None: + """Check a US national result has congressional_district_impact set to null. + + Only state runs compute districts: nationwide reports take them from a + separate run of each state. + """ + economy_result = _as_mapping(economy_result, label="economy result") + assert "congressional_district_impact" in economy_result, ( + f"Missing 'congressional_district_impact' in result: {economy_result.keys()}" + ) + district_impact = economy_result["congressional_district_impact"] + assert district_impact is None, ( + "Expected no congressional_district_impact in a US national result, " + f"got {district_impact!r}" + ) _UK_AREA_RESULT_KEYS = ("constituency_impact", "local_authority_impact") @@ -323,6 +330,34 @@ def test_result_assertions_accept_generated_client_objects() -> None: assert_congressional_district_results(result, expected_district_prefix="UT-") +def test_national_us_assertions_accept_a_result_without_districts() -> None: + assert_no_congressional_district_results( + _GeneratedClientObject({"budget": {}, "congressional_district_impact": None}) + ) + assert_no_congressional_district_results({"congressional_district_impact": None}) + + +@pytest.mark.parametrize( + "district_impact", + [ + {"districts": []}, + {"districts": [{"district": "UT-01"}]}, + _GeneratedClientObject({"districts": [{"district": "UT-01"}]}), + ], + ids=["no-districts-listed", "districts-listed", "generated-client-object"], +) +def test_national_us_assertions_reject_district_results(district_impact) -> None: + with pytest.raises(AssertionError, match="congressional_district_impact"): + assert_no_congressional_district_results( + {"congressional_district_impact": district_impact} + ) + + +def test_national_us_assertions_reject_a_result_without_the_district_field() -> None: + with pytest.raises(AssertionError, match="congressional_district_impact"): + assert_no_congressional_district_results({"budget": {}}) + + _BREAKDOWN_CONSTITUENCY = { "constituency_code": "E14001063", "constituency_name": "Aldershot", @@ -613,9 +648,10 @@ def test_calculate_default_model( ): """A simulation on the default model version returns economic impact data. - Given a simulation request with default model version + Given a US national simulation request with default model version When the simulation is submitted and polled to completion - Then the result contains expected economic impact data. + Then the result contains expected economic impact data and no + congressional districts, which only state runs compute. """ # Given request = SimulationRequest.from_dict( @@ -649,10 +685,7 @@ def test_calculate_default_model( economy_result = result.result assert_economy_result_sections(economy_result) - assert_congressional_district_results( - economy_result, - expected_district_ids=_AT_LARGE_DISTRICT_IDS, - ) + assert_no_congressional_district_results(economy_result) @pytest.mark.beta_only From fa4e9781ff6d3d4d9e8befc362467611a8f86f46 Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:35:07 +0400 Subject: [PATCH 3/6] Say which Stage 12 reports still compute congressional districts The docstring of _should_build_us_congressional_district_impact and the changelog item said Stage 12 aggregation computes no districts. Stage 12 aggregation passes on the report's own region, so it computes none for a national report, but a report for one state still computes that state's districts. Both now say so. Documentation only. Co-Authored-By: Claude Opus 5.5 --- changelog_entry.yaml | 2 +- .../simulation_output_geographic.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/changelog_entry.yaml b/changelog_entry.yaml index 2daf91fab..da1438fec 100644 --- a/changelog_entry.yaml +++ b/changelog_entry.yaml @@ -16,7 +16,7 @@ - Ran Stage 12 US national simulations as 20 region-group calls each, and built an independent, immutable Stage 12 cache of three annual US datasets and 60 current-law baselines before each deployment, publishing it only after a recorded determinism check and recording its release in the v2 version manifest under a new key, while the existing key keeps a copy that earlier readers accept - Made the budget refuse a household count outside its country's plausible range (above 200 million in the US or 60 million in the UK, negative or not finite), raising ImplausibleEntityCountError instead of publishing a count like #767's changed: - - Made only US state runs compute congressional district impacts; national runs (no region, or us) and region-group runs, and so segmented national runs, their reduce and Stage 12 aggregation, now return congressional_district_impact null, because nationwide reports take each district from a separate run of its state on that state's dataset, which matches the state's own report; the beta integration check of a US national result now expects congressional_district_impact null, and the state checks still expect the state's districts + - Made only US state runs compute congressional district impacts; national runs (no region, or us) and region-group runs, and so segmented national runs, their reduce and Stage 12 aggregation of a national report, now return congressional_district_impact null (Stage 12 aggregation of a state report still computes that state's districts), because nationwide reports take each district from a separate run of its state on that state's dataset, which matches the state's own report; the beta integration check of a US national result now expects congressional_district_impact null, and the state checks still expect the state's districts - Moved the worker's credential setup, request year and policy parsing, and region resolution out of simulation_runtime.py (1,111 lines, now 651) into gcp_credentials.py, request_policy.py and region_resolution.py; simulation_runtime.py keeps dataset selection and loading, simulation building, local-area breakdowns and the run itself, which tests replace by name there, and re-exports the moved names, so every caller still imports them from simulation_runtime; the shared helpers _parse_year, _normalise_policy, _compile_interval_policy, _runtime_policy (now wrapper_policy), _resolve_region, _resolve_dataset_reference and _require_local_area_breakdown are now public, and the credential messages still log under the simulation_runtime logger; the 3,048-line output builder test file is split into seven test files and a module of shared fakes, all named test_simulation_output_*.py so the weighted-frame rule still checks them - Split the artifact precompute (precompute.py, 931 lines) and the Stage 12 cache precompute (stage12_cache/precompute.py, 844 lines) each into planning, dataset-building and baseline modules, with the manifest and run_precompute left in the original module, which re-exports every public name, so the Modal precompute apps and their tests import from the same paths; the cache precompute's _scoped_planned_simulation and _baseline_identity are now public, and the worker validation imports scoped_planned_simulation from its new module; the 1,835-line precompute test file is split into four, and the cache precompute tests patch each name in the module whose code reads it - Split the 4,709-line Stage 12 runtime test file into thirteen test files by behaviour (dataset loading, provenance, the single worker and its failures, aggregates, the coordinator and its failures, runtime store retries, failure records, cancellation, child state errors and the wait window) and three support modules for the shared builders, fake stores and fake child calls, whose helpers are now public; the other Stage 12 tests import them from those modules, every test keeps its name and body, and two parametrized cases are named after the renamed bundle helper diff --git a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation_output_geographic.py b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation_output_geographic.py index c1e315eb3..9b6d1d437 100644 --- a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation_output_geographic.py +++ b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation_output_geographic.py @@ -227,10 +227,12 @@ def _should_build_us_congressional_district_impact(region_code: str | None) -> b Only state runs do, such as ``state/ca`` in any case. National runs (no region, or ``us``) and region-group runs compute none, so neither do - segmented national runs, their reduce or Stage 12 aggregation: a - nationwide report takes its districts from a separate run of each state - on that state's dataset, which gives each district the same figures as - its state's own report. District and place runs compute none either. + segmented national runs, their reduce or Stage 12 aggregation of a + national report: a nationwide report takes its districts from a separate + run of each state on that state's dataset, which gives each district the + same figures as its state's own report. Stage 12 aggregation of a state + report still computes that state's districts. District and place runs + compute none. """ return region_code is not None and region_code.lower().startswith("state/") From 9b03b340d307281aa32650752cbc59bf615416b8 Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Mon, 2 Nov 2026 18:04:11 +0400 Subject: [PATCH 4/6] Accept a US national result with no district field in the beta check The gateway sends job-status responses with response_model_exclude_none, so once national runs return congressional_district_impact None the deployed result has no such field. assert_no_congressional_district_results first required the field to be present, so the beta check of a US national result would have failed after deploy and stopped the production deploy. It now reads the field with .get(...), as assert_no_uk_area_results does, and its local test passes results without the field; that test failed before the helper changed. The test that refused a result without the field is removed. The changelog fragment now says the field is null in the output and left out of the job-status response, and the district check's docstring says Stage 12 aggregation matches only a state/... spelling, because it passes the region as the request sent it. Co-Authored-By: Claude Opus 5.5 --- changelog.d/954.changed.md | 2 +- .../tests/simulation/test_calculate.py | 20 +++++++++---------- .../simulation_output_geographic.py | 5 +++-- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/changelog.d/954.changed.md b/changelog.d/954.changed.md index 4abdddb3d..ad5a9a9fd 100644 --- a/changelog.d/954.changed.md +++ b/changelog.d/954.changed.md @@ -1 +1 @@ -Made only US state runs compute congressional district impacts; national runs (no region, or `us`) and region-group runs, and so segmented national runs, their reduce and Stage 12 aggregation of a national report, now return `congressional_district_impact` null (Stage 12 aggregation of a state report still computes that state's districts), because nationwide reports take each district from a separate run of its state on that state's dataset, which matches the state's own report; the beta integration check of a US national result now expects `congressional_district_impact` null, and the state checks still expect the state's districts. +Made only US state runs compute congressional district impacts; national runs (no region, or `us`) and region-group runs, and so segmented national runs, their reduce and Stage 12 aggregation of a national report, now compute none: `congressional_district_impact` is null in their output, and the gateway's job-status response leaves that null field out (Stage 12 aggregation of a state report spelled `state/...` still computes that state's districts), because nationwide reports take each district from a separate run of its state on that state's dataset, which matches the state's own report; the beta integration check of a US national result now expects no `congressional_district_impact`, whether absent or null, and the state checks still expect the state's districts. diff --git a/projects/policyengine-apis-integ/tests/simulation/test_calculate.py b/projects/policyengine-apis-integ/tests/simulation/test_calculate.py index 16b89ff0f..79a75d592 100644 --- a/projects/policyengine-apis-integ/tests/simulation/test_calculate.py +++ b/projects/policyengine-apis-integ/tests/simulation/test_calculate.py @@ -237,16 +237,15 @@ def assert_congressional_district_results( def assert_no_congressional_district_results(economy_result: object) -> None: - """Check a US national result has congressional_district_impact set to null. + """Check a US national result has no congressional_district_impact. Only state runs compute districts: nationwide reports take them from a - separate run of each state. + separate run of each state. The field is null in the run's output, and the + gateway's job-status response leaves null fields out, so a deployed result + has no such field. """ economy_result = _as_mapping(economy_result, label="economy result") - assert "congressional_district_impact" in economy_result, ( - f"Missing 'congressional_district_impact' in result: {economy_result.keys()}" - ) - district_impact = economy_result["congressional_district_impact"] + district_impact = economy_result.get("congressional_district_impact") assert district_impact is None, ( "Expected no congressional_district_impact in a US national result, " f"got {district_impact!r}" @@ -335,6 +334,10 @@ def test_national_us_assertions_accept_a_result_without_districts() -> None: _GeneratedClientObject({"budget": {}, "congressional_district_impact": None}) ) assert_no_congressional_district_results({"congressional_district_impact": None}) + # The gateway's job-status response leaves null fields out, so a deployed + # national result has no district field at all. + assert_no_congressional_district_results({"budget": {}}) + assert_no_congressional_district_results(_GeneratedClientObject({"budget": {}})) @pytest.mark.parametrize( @@ -353,11 +356,6 @@ def test_national_us_assertions_reject_district_results(district_impact) -> None ) -def test_national_us_assertions_reject_a_result_without_the_district_field() -> None: - with pytest.raises(AssertionError, match="congressional_district_impact"): - assert_no_congressional_district_results({"budget": {}}) - - _BREAKDOWN_CONSTITUENCY = { "constituency_code": "E14001063", "constituency_name": "Aldershot", diff --git a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation_output_geographic.py b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation_output_geographic.py index 9b6d1d437..7f76ec7d3 100644 --- a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation_output_geographic.py +++ b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation_output_geographic.py @@ -231,8 +231,9 @@ def _should_build_us_congressional_district_impact(region_code: str | None) -> b national report: a nationwide report takes its districts from a separate run of each state on that state's dataset, which gives each district the same figures as its state's own report. Stage 12 aggregation of a state - report still computes that state's districts. District and place runs - compute none. + report still computes that state's districts when the request spells the + region ``state/...``: that path passes the region as sent, so a bare code + such as ``ut`` computes none there. District and place runs compute none. """ return region_code is not None and region_code.lower().startswith("state/") From 23aa3b5a55a40e87577ca27f00ddf1d7d1f47bfe Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Mon, 2 Nov 2026 18:22:11 +0400 Subject: [PATCH 5/6] Check an at-large state's one district in the beta tests Removing the national check also removed the only deployed check of the seven at-large states, whose districts nationwide reports now take from each state's own run. The two Utah beta tests sent the same request, so the pinned-version one now runs state/vt and expects exactly VT-01, through a restored expected_district_ids option that matches a state's district IDs exactly. The option's local tests failed first, because the option did not exist. The beta suite keeps its number of tests, so BETA_PYTEST_WORKERS is unchanged. Co-Authored-By: Claude Opus 5.5 --- .../tests/simulation/test_calculate.py | 60 +++++++++++++++++-- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/projects/policyengine-apis-integ/tests/simulation/test_calculate.py b/projects/policyengine-apis-integ/tests/simulation/test_calculate.py index 79a75d592..85fe4f9d7 100644 --- a/projects/policyengine-apis-integ/tests/simulation/test_calculate.py +++ b/projects/policyengine-apis-integ/tests/simulation/test_calculate.py @@ -195,6 +195,7 @@ def assert_congressional_district_results( economy_result: object, *, expected_district_prefix: str | None = None, + expected_district_ids: set[str] | None = None, ) -> None: economy_result = _as_mapping(economy_result, label="economy result") assert "congressional_district_impact" in economy_result, ( @@ -235,6 +236,13 @@ def assert_congressional_district_results( f"got {district_ids}" ) + if expected_district_ids is not None: + district_ids = _district_ids(district_results) + assert set(district_ids) == expected_district_ids, ( + f"Expected district IDs {sorted(expected_district_ids)}, " + f"got {sorted(district_ids)}" + ) + def assert_no_congressional_district_results(economy_result: object) -> None: """Check a US national result has no congressional_district_impact. @@ -329,6 +337,47 @@ def test_result_assertions_accept_generated_client_objects() -> None: assert_congressional_district_results(result, expected_district_prefix="UT-") +def _state_result_with_districts(*district_ids: str) -> dict: + return { + "congressional_district_impact": { + "districts": [ + { + "district": district_id, + "average_household_income_change": 0.0, + "relative_household_income_change": 0.0, + "winner_percentage": 0.0, + "loser_percentage": 0.0, + "no_change_percentage": 1.0, + "population": 1.0, + } + for district_id in district_ids + ] + } + } + + +def test_state_assertions_accept_an_at_large_state_with_its_one_district() -> None: + assert_congressional_district_results( + _state_result_with_districts("VT-01"), + expected_district_prefix="VT-", + expected_district_ids={"VT-01"}, + ) + + +@pytest.mark.parametrize( + "district_ids", + [("VT-00",), ("VT-01", "VT-02")], + ids=["another-id", "an-extra-district"], +) +def test_state_assertions_reject_other_district_ids(district_ids) -> None: + with pytest.raises(AssertionError, match="district IDs"): + assert_congressional_district_results( + _state_result_with_districts(*district_ids), + expected_district_prefix="VT-", + expected_district_ids={"VT-01"}, + ) + + def test_national_us_assertions_accept_a_result_without_districts() -> None: assert_no_congressional_district_results( _GeneratedClientObject({"budget": {}, "congressional_district_impact": None}) @@ -750,16 +799,18 @@ def test_calculate_specific_model( ): """A simulation pinned to a specific US model version returns impact data. - Given a simulation request with a specific model version + Given a simulation request with a specific model version, for Vermont When the simulation is submitted and polled to completion - Then the result contains expected economic impact data. + Then the result contains expected economic impact data, and Vermont's one + at-large district: nationwide reports take each at-large district from its + state's own run, as here. """ # Given request = SimulationRequest.from_dict( { "country": "us", "version": us_model_version, - "region": "state/ut", + "region": "state/vt", "scope": "macro", "reform": { "gov.irs.credits.ctc.refundable.fully_refundable": { @@ -794,7 +845,8 @@ def test_calculate_specific_model( assert_economy_result_sections(economy_result) assert_congressional_district_results( economy_result, - expected_district_prefix="UT-", + expected_district_prefix="VT-", + expected_district_ids={"VT-01"}, ) From 86ec11deaf99833a198e8aadd67bc2e807fbc888 Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Mon, 2 Nov 2026 18:38:06 +0400 Subject: [PATCH 6/6] Fail the exact district check when a district is listed twice expected_district_ids compared sets, so a state result listing VT-01 twice passed. It now compares the sorted lists; the new repeated-district case failed first. Co-Authored-By: Claude Opus 5.5 --- .../tests/simulation/test_calculate.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/projects/policyengine-apis-integ/tests/simulation/test_calculate.py b/projects/policyengine-apis-integ/tests/simulation/test_calculate.py index 85fe4f9d7..d61607a3b 100644 --- a/projects/policyengine-apis-integ/tests/simulation/test_calculate.py +++ b/projects/policyengine-apis-integ/tests/simulation/test_calculate.py @@ -238,7 +238,8 @@ def assert_congressional_district_results( if expected_district_ids is not None: district_ids = _district_ids(district_results) - assert set(district_ids) == expected_district_ids, ( + # Sorted lists, not sets, so a district listed twice fails too. + assert sorted(district_ids) == sorted(expected_district_ids), ( f"Expected district IDs {sorted(expected_district_ids)}, " f"got {sorted(district_ids)}" ) @@ -366,8 +367,8 @@ def test_state_assertions_accept_an_at_large_state_with_its_one_district() -> No @pytest.mark.parametrize( "district_ids", - [("VT-00",), ("VT-01", "VT-02")], - ids=["another-id", "an-extra-district"], + [("VT-00",), ("VT-01", "VT-02"), ("VT-01", "VT-01")], + ids=["another-id", "an-extra-district", "a-repeated-district"], ) def test_state_assertions_reject_other_district_ids(district_ids) -> None: with pytest.raises(AssertionError, match="district IDs"):