Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -354,10 +354,12 @@ def reform_validation_payload(
budget effect rather than failing the build. OBBBA rows are special because
policyengine-us already carries OBBBA in its baseline, while JCX-35-25
scores enactment relative to a pre-OBBBA present-law baseline. For those
rows, each spec's ``parameter_changes`` remains the counterfactual repeal
patch, but scoring first merges all OBBBA repeal patches into a pre-OBBBA
baseline and then adds the row's provision back. The shape matches the
calibration-diagnostics dashboard's reform_validation reader.
rows, each spec's ``parameter_changes`` is the counterfactual repeal patch;
scoring merges all repeals into a pre-OBBBA baseline, then enacts the
provisions one at a time in JCX order and scores each *stacked* — the
incremental effect given the lines above it — so the per-line effects sum to
the bill total, matching JCT (see ``stacked_obbba_effects``). The shape
matches the calibration-diagnostics dashboard's reform_validation reader.
"""
estimates = in_sample_estimates or {}
targets = in_sample_targets or {}
Expand All @@ -366,6 +368,7 @@ def reform_validation_payload(
parameter_reform_sims: dict[str, Any] = {}
obbba_specs = tuple(spec for spec in specs if _is_obbba_spec(spec))
obbba_pre_baseline_changes = _merged_parameter_changes(obbba_specs)
obbba_stacked: dict[str, tuple[float, float, float]] | None = None

def parameter_changes_key(changes: dict[str, Any]) -> str:
return json.dumps(changes, sort_keys=True, separators=(",", ":"))
Expand All @@ -386,9 +389,60 @@ def simulation_for_parameter_changes(changes: dict[str, Any]) -> Any:
parameter_reform_sims[key] = simulate(reform) # type: ignore[misc]
return parameter_reform_sims[key]

def obbba_component_effect(
def stacked_obbba_effects() -> dict[str, tuple[float, float, float]]:
"""Score the OBBBA provisions *stacked* in their JCX-35-25 order.

JCT presents each provision's budget effect incrementally — given the
provisions above it in the document — so the line items sum to the
bill's total. We mirror that: starting from the pre-OBBBA baseline (all
provisions reverted), enact the provisions one at a time in ``specs``
order, and score each as the change in its budget measure from enacting
it on top of the lines already enacted. The per-line effects then
telescope to the true total OBBBA effect, rather than each being
measured in isolation against pre-OBBBA law (which ignores the
interactions between provisions, e.g. the standard deduction and the
personal-exemption repeal).

All OBBBA specs must share one (budget_measure, period) for the
cumulative baseline to be coherent; a mixed group falls back to
isolated scoring.
"""
if simulate is None or not obbba_specs:
return {}
measures = {(spec.budget_measure, spec.period) for spec in obbba_specs}
if len(measures) != 1:
return {
spec.id: _isolated_obbba_effect(spec) for spec in obbba_specs
}
measure, period = next(iter(measures))
# state 0: pre-OBBBA (every provision reverted).
prev_total = _weighted_total(
simulation_for_parameter_changes(obbba_pre_baseline_changes),
measure,
period,
)
enacted: set[str] = set()
effects: dict[str, tuple[float, float, float]] = {}
for spec in obbba_specs:
# Enacting a provision means dropping its repeal from the baseline.
enacted |= set((spec.parameter_changes or {}).keys())
state_changes = {
path: change
for path, change in obbba_pre_baseline_changes.items()
if path not in enacted
}
cur_total = _weighted_total(
simulation_for_parameter_changes(state_changes), measure, period
)
effects[spec.id] = (cur_total - prev_total, prev_total, cur_total)
prev_total = cur_total
return effects

def _isolated_obbba_effect(
spec: ReformValidationSpec,
) -> tuple[float | None, float | None, float | None]:
# One provision enacted alone on the pre-OBBBA baseline. Only used as a
# fallback when the OBBBA group spans multiple measures/periods.
if simulate is None:
return None, None, None
component_paths = set((spec.parameter_changes or {}).keys())
Expand All @@ -406,10 +460,13 @@ def obbba_component_effect(
def simulated_effect(
spec: ReformValidationSpec,
) -> tuple[float | None, float | None, float | None]:
nonlocal obbba_stacked
if simulate is None:
return None, None, None
if _is_obbba_spec(spec):
return obbba_component_effect(spec)
if obbba_stacked is None:
obbba_stacked = stacked_obbba_effects()
return obbba_stacked.get(spec.id, (None, None, None))
base = baseline_total(spec.budget_measure, spec.period)
reform_total = _weighted_total(
simulate(spec.build_reform()), spec.budget_measure, spec.period
Expand Down
26 changes: 16 additions & 10 deletions packages/populace-build/tests/test_reform_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def simulate(reform):
assert payload["reforms"][0]["populace"]["budget_effect"] == pytest.approx(-33e9)


def test_obbba_components_score_against_pre_obbba_baseline(monkeypatch):
def test_obbba_components_score_stacked_in_jcx_order(monkeypatch):
specs = (
ReformValidationSpec(
id="obbba_a",
Expand Down Expand Up @@ -192,25 +192,31 @@ def test_obbba_components_score_against_pre_obbba_baseline(monkeypatch):
)

def simulate(reform):
# Reform keys are the provisions still turned off. The full pre-OBBBA
# baseline has both patches applied. Component A is scored with only B
# still off, and component B with only A still off.
# Reform keys are the provisions still repealed. Pre-OBBBA repeals both;
# the provisions are then enacted one at a time in order: A (only B still
# repealed), then B (nothing repealed → reform is None).
totals = {
frozenset({"gov.example.a", "gov.example.b"}): 1_000.0,
frozenset({"gov.example.b"}): 900.0,
frozenset({"gov.example.a"}): 1_060.0,
None: 950.0,
frozenset({"gov.example.a", "gov.example.b"}): 1_000.0, # pre-OBBBA
frozenset({"gov.example.b"}): 900.0, # A enacted
None: 960.0, # A and B enacted
}
return _FakeSim({"income_tax": totals[reform]})

payload = reform_validation_payload(specs, period=2026, simulate=simulate)
rows = {row["id"]: row for row in payload["reforms"]}
# A is scored against pre-OBBBA; B is scored against the post-A state, not
# against pre-OBBBA — that stacking is the whole point.
assert rows["obbba_a"]["populace"]["baseline_total"] == pytest.approx(1_000.0)
assert rows["obbba_a"]["populace"]["reform_total"] == pytest.approx(900.0)
assert rows["obbba_a"]["populace"]["budget_effect"] == pytest.approx(-100.0)
assert rows["obbba_b"]["populace"]["baseline_total"] == pytest.approx(1_000.0)
assert rows["obbba_b"]["populace"]["reform_total"] == pytest.approx(1_060.0)
assert rows["obbba_b"]["populace"]["baseline_total"] == pytest.approx(900.0)
assert rows["obbba_b"]["populace"]["reform_total"] == pytest.approx(960.0)
assert rows["obbba_b"]["populace"]["budget_effect"] == pytest.approx(60.0)
# Stacked line effects telescope to the true total OBBBA effect.
total = sum(
rows[i]["populace"]["budget_effect"] for i in ("obbba_a", "obbba_b")
)
assert total == pytest.approx(960.0 - 1_000.0)


def test_shipped_obbba_config_is_out_of_sample_counterfactual():
Expand Down
Loading