Skip to content

Commit 240af3d

Browse files
authored
Merge pull request #490 from PolicyEngine/rollback-budget-window-to-477-runtime-upgrades
Restore compact budget-window results
2 parents 37042a9 + 1d4b0b0 commit 240af3d

16 files changed

Lines changed: 298 additions & 829 deletions

changelog_entry.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- bump: patch
2+
changes:
3+
changed:
4+
- Bumped policyengine-core minimum version to 3.23.5 for pandas 3.0 compatibility

projects/policyengine-api-simulation/fixtures/gateway/test_endpoints.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import pytest
44

5-
from tests.fixtures.budget_window_outputs import make_single_year_macro_output
6-
75

86
class MockDict:
97
"""Mock for Modal.Dict to simulate version registry."""
@@ -36,12 +34,7 @@ class MockFunctionCall:
3634

3735
def __init__(self, object_id: str = "mock-job-id-123"):
3836
self.object_id = object_id
39-
self.result = make_single_year_macro_output(
40-
tax_revenue_impact=1000000,
41-
state_tax_revenue_impact=0,
42-
benefit_spending_impact=0,
43-
budgetary_impact=1000000,
44-
)
37+
self.result = {"budget": {"total": 1000000}}
4538
self.error = None
4639
self.running = False
4740
self.__class__.registry[object_id] = self
Lines changed: 37 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
"""Budget-window result validation and aggregation helpers."""
1+
"""Budget-window annual result extraction and aggregation helpers."""
22

33
from __future__ import annotations
44

55
from decimal import Decimal
66
from typing import Any
77

88
from src.modal.gateway.models import (
9+
BudgetWindowAnnualImpact,
910
BudgetWindowResult,
1011
BudgetWindowTotals,
11-
SingleYearMacroOutput,
1212
)
1313

1414
# The UK microsimulation has no state/province fiscal layer, so worker child
@@ -20,10 +20,7 @@
2020
"benefit_spending_impact",
2121
"budgetary_impact",
2222
)
23-
24-
25-
def _is_number(value: Any) -> bool:
26-
return isinstance(value, int | float) and not isinstance(value, bool)
23+
OPTIONAL_BUDGET_KEYS = ("state_tax_revenue_impact",)
2724

2825

2926
def _as_decimal(value: float | int) -> Decimal:
@@ -35,60 +32,46 @@ def _as_decimal(value: float | int) -> Decimal:
3532
return Decimal(str(value))
3633

3734

38-
def validate_single_year_output(
35+
def extract_annual_impact(
3936
*,
4037
simulation_year: str,
4138
child_result: dict[str, Any],
42-
) -> SingleYearMacroOutput:
43-
"""Validate and normalize a child macro result.
44-
45-
UK worker results can omit ``state_tax_revenue_impact`` because there is
46-
no state/province fiscal layer. The canonical output still includes that
47-
field, defaulted to zero, so downstream clients receive one stable shape.
48-
"""
49-
50-
if not isinstance(child_result, dict):
51-
raise ValueError(
52-
"Malformed budget-window child result: expected object for "
53-
f"{simulation_year}"
54-
)
55-
39+
) -> BudgetWindowAnnualImpact:
5640
budget = child_result.get("budget", {})
5741
if not isinstance(budget, dict):
5842
raise ValueError("Malformed budget-window child result: missing budget object")
5943

6044
missing_keys = [
61-
key for key in REQUIRED_BUDGET_KEYS if not _is_number(budget.get(key))
45+
key
46+
for key in REQUIRED_BUDGET_KEYS
47+
if not isinstance(budget.get(key), int | float)
6248
]
6349
if missing_keys:
6450
missing = ", ".join(f"budget.{key}" for key in missing_keys)
6551
raise ValueError(
6652
f"Malformed budget-window child result: missing numeric {missing}"
6753
)
6854

69-
normalized = dict(child_result)
70-
normalized_budget = dict(budget)
71-
if "state_tax_revenue_impact" not in normalized_budget:
72-
normalized_budget["state_tax_revenue_impact"] = 0.0
73-
elif not _is_number(normalized_budget["state_tax_revenue_impact"]):
74-
raise ValueError(
75-
"Malformed budget-window child result: missing numeric "
76-
"budget.state_tax_revenue_impact"
77-
)
78-
normalized["budget"] = normalized_budget
79-
80-
try:
81-
return SingleYearMacroOutput.model_validate(normalized)
82-
except Exception as exc:
83-
raise ValueError(
84-
f"Malformed budget-window child result for {simulation_year}: {exc}"
85-
) from exc
55+
tax_revenue_impact = budget["tax_revenue_impact"]
56+
# UK worker results omit the state fiscal layer entirely; coerce to 0.0
57+
# so the parent aggregator can still report federal/state splits with a
58+
# uniform shape across countries.
59+
state_tax_revenue_impact = budget.get("state_tax_revenue_impact")
60+
if not isinstance(state_tax_revenue_impact, int | float):
61+
state_tax_revenue_impact = 0.0
62+
63+
return BudgetWindowAnnualImpact(
64+
year=simulation_year,
65+
taxRevenueImpact=tax_revenue_impact,
66+
federalTaxRevenueImpact=tax_revenue_impact - state_tax_revenue_impact,
67+
stateTaxRevenueImpact=state_tax_revenue_impact,
68+
benefitSpendingImpact=budget["benefit_spending_impact"],
69+
budgetaryImpact=budget["budgetary_impact"],
70+
)
8671

8772

88-
def sum_single_year_outputs(
89-
*,
90-
outputs_by_year: dict[str, SingleYearMacroOutput],
91-
years: list[str],
73+
def sum_annual_impacts(
74+
annual_impacts: list[BudgetWindowAnnualImpact],
9275
) -> BudgetWindowTotals:
9376
"""Sum per-year impacts using Decimal accumulators.
9477
@@ -110,21 +93,18 @@ def sum_single_year_outputs(
11093
"budgetaryImpact": Decimal(0),
11194
}
11295

113-
for year in years:
114-
output = outputs_by_year[year]
115-
budget = output.model_dump(mode="json")["budget"]
116-
tax_revenue_impact = budget["tax_revenue_impact"]
117-
state_tax_revenue_impact = budget.get("state_tax_revenue_impact")
118-
119-
totals["taxRevenueImpact"] += _as_decimal(tax_revenue_impact)
96+
for annual_impact in annual_impacts:
97+
totals["taxRevenueImpact"] += _as_decimal(annual_impact.taxRevenueImpact)
12098
totals["federalTaxRevenueImpact"] += _as_decimal(
121-
tax_revenue_impact - state_tax_revenue_impact
99+
annual_impact.federalTaxRevenueImpact
100+
)
101+
totals["stateTaxRevenueImpact"] += _as_decimal(
102+
annual_impact.stateTaxRevenueImpact
122103
)
123-
totals["stateTaxRevenueImpact"] += _as_decimal(state_tax_revenue_impact)
124104
totals["benefitSpendingImpact"] += _as_decimal(
125-
budget["benefit_spending_impact"]
105+
annual_impact.benefitSpendingImpact
126106
)
127-
totals["budgetaryImpact"] += _as_decimal(budget["budgetary_impact"])
107+
totals["budgetaryImpact"] += _as_decimal(annual_impact.budgetaryImpact)
128108

129109
return BudgetWindowTotals(**{key: float(value) for key, value in totals.items()})
130110

@@ -133,25 +113,12 @@ def build_budget_window_result(
133113
*,
134114
start_year: str,
135115
window_size: int,
136-
outputs_by_year: dict[str, SingleYearMacroOutput],
116+
annual_impacts: list[BudgetWindowAnnualImpact],
137117
) -> BudgetWindowResult:
138-
years = [str(int(start_year) + offset) for offset in range(window_size)]
139-
missing_years = [year for year in years if year not in outputs_by_year]
140-
if missing_years:
141-
raise ValueError(
142-
"Cannot build budget-window result: missing outputs for "
143-
+ ", ".join(missing_years)
144-
)
145-
146-
ordered_outputs = {year: outputs_by_year[year] for year in years}
147118
return BudgetWindowResult(
148119
startYear=start_year,
149120
endYear=str(int(start_year) + window_size - 1),
150121
windowSize=window_size,
151-
years=years,
152-
outputsByYear=ordered_outputs,
153-
totals=sum_single_year_outputs(
154-
outputs_by_year=ordered_outputs,
155-
years=years,
156-
),
122+
annualImpacts=annual_impacts,
123+
totals=sum_annual_impacts(annual_impacts),
157124
)

projects/policyengine-api-simulation/src/modal/budget_window_scheduler.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
)
1515
from src.modal.budget_window_results import (
1616
build_budget_window_result,
17-
validate_single_year_output,
17+
extract_annual_impact,
1818
)
1919
from src.modal.budget_window_state import (
2020
build_batch_status_response,
@@ -167,7 +167,7 @@ def poll_running_children_once(self) -> bool:
167167
return False
168168

169169
try:
170-
single_year_output = validate_single_year_output(
170+
annual_impact = extract_annual_impact(
171171
simulation_year=simulation_year,
172172
child_result=child_result,
173173
)
@@ -189,7 +189,7 @@ def poll_running_children_once(self) -> bool:
189189
mark_child_completed(
190190
self.state,
191191
year=simulation_year,
192-
single_year_output=single_year_output,
192+
annual_impact=annual_impact,
193193
)
194194
put_batch_job_state(self.state)
195195
progress_made = True
@@ -222,15 +222,15 @@ def fail_batch_for_child_error(
222222
put_batch_job_state(self.state)
223223

224224
def complete_batch(self) -> dict[str, Any]:
225-
outputs_by_year = {
226-
simulation_year: self.state.partial_outputs_by_year[simulation_year]
225+
annual_impacts = [
226+
self.state.partial_annual_impacts[simulation_year]
227227
for simulation_year in self.state.years
228-
if simulation_year in self.state.partial_outputs_by_year
229-
}
228+
if simulation_year in self.state.partial_annual_impacts
229+
]
230230
result = build_budget_window_result(
231231
start_year=self.state.start_year,
232232
window_size=self.state.window_size,
233-
outputs_by_year=outputs_by_year,
233+
annual_impacts=annual_impacts,
234234
)
235235
mark_batch_complete(self.state, result=result)
236236
put_batch_job_state(self.state)

projects/policyengine-api-simulation/src/modal/budget_window_state.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99

1010
from src.modal.gateway.models import (
1111
BatchChildJobStatus,
12+
BudgetWindowAnnualImpact,
1213
BudgetWindowBatchRequest,
1314
BudgetWindowBatchState,
1415
BudgetWindowBatchStatusResponse,
1516
BudgetWindowResult,
1617
PolicyEngineBundle,
17-
SingleYearMacroOutput,
1818
)
1919

2020
logger = logging.getLogger(__name__)
@@ -79,7 +79,7 @@ def create_initial_batch_state(
7979
completed_years=[],
8080
failed_years=[],
8181
child_jobs={},
82-
partial_outputs_by_year={},
82+
partial_annual_impacts={},
8383
result=None,
8484
error=None,
8585
created_at=now,
@@ -166,7 +166,7 @@ def mark_child_completed(
166166
state: BudgetWindowBatchState,
167167
*,
168168
year: str,
169-
single_year_output: SingleYearMacroOutput,
169+
annual_impact: BudgetWindowAnnualImpact,
170170
) -> BudgetWindowBatchState:
171171
if year in state.running_years:
172172
state.running_years.remove(year)
@@ -178,7 +178,7 @@ def mark_child_completed(
178178
job_id=child.job_id,
179179
status="complete",
180180
)
181-
state.partial_outputs_by_year[year] = single_year_output
181+
state.partial_annual_impacts[year] = annual_impact
182182
return _touch(state)
183183

184184

0 commit comments

Comments
 (0)