Skip to content

Make branch-measured marginal tax rates match fresh re-simulation - #9698

Open
MaxGhenis wants to merge 12 commits into
mainfrom
fix-mtr-branch-divergence
Open

MaxGhenis wants to merge 12 commits into
mainfrom
fix-mtr-branch-divergence

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #9697.

Summary

Marginal-rate branches now perturb the inputs that programs actually read, preserve moved wages, hours and capital gains, and clear cached monthly values before applying the increase. The earnings, health-benefit and component rates share the same adult loop; the capital-gains rate raises long-term gains so preferential rates apply. For the reported Ohio parent earning $28,000, the corrected earnings rate is 0.5271.

Without behavioral responses, branch rates match finite differences between fresh simulations. Under dynamic reforms, they match fresh simulations with the parent's earnings, capital-gains and hours responses held at their simulated values. Setting responses to zero only in the raised branch would change behavior between the two sides of the comparison; freezing the same responses preserves the marginal-rate interpretation.

Both simulation wrappers synchronize input_variables after moving inputs and backfilling geography, then rebuild the reform's baseline with those inputs. This also changes exported input names: datasets exported through input_variables retain pre-response variables instead of stale aggregate columns, including long_term_capital_gains.

Round 3 fixes

  • Restore the baseline policy, rebind its holders to that policy, remove reform-only holders and inputs, and restore baseline tracing isolation. The existing UserReform regression must calculate $4,016 of baseline income tax while the reform calculates $0; reform-only inputs must not leak into baseline cache invalidation.
  • Neutralize the hours response alongside earnings and capital-gains responses in behavioral measurement branches. A cold-cache regression enables both labor-supply and capital-gains elasticities and queries the two responses in both orders, preventing capital-gains-first measurement recursion.

Validation

Python tests run one file at a time on macOS using Python 3.13.9 and compatible Core 3.32.15. Local wall times include setup and reflect heavy host contention; they are not Linux CI benchmarks. Python API coverage is required for branch ownership, tracing, cold-cache query order and shared measurements. The new tracing case stays in the existing SPM group; the new combined-response file routes into the existing Rest/core group. No CI runners were added. The failed-probe timings stop at different assertions and are not cost deltas for the new tests.

New head: 37bb63197728f0d481b0782609321872f7bd0a29 (fast-forward from 792e7d4dbb; no merge in this round).

  • Baseline regression before fix: 4 failures at 792e7d4: both wrappers return $0 instead of $4,016; restored policy owns the discarded baseline; reform-only input remains in baseline metadata (479.73 s end to end).
  • Baseline regression after fix: 4 passed, including the new tracing case (534.33 s end to end; 3.46 GiB peak RSS).
  • Both response query orders before/after: CG-first fails with CycleError before the fix (203.38 s; 3.28 GiB). After the fix both isolated query orders and the nonzero order-comparison pass: 3 tests, four one-person simulations (1,414.56 s end to end; 3.51 GiB peak RSS).
  • Single-file checks and runtimes: existing behavioral-response measurement file: 14 passed (860.44 s; 3.73 GiB peak RSS). Five dynamic MTR/baseline comparisons: 5 passed, 7 deselected (2,104.41 s; 5.69 GiB peak RSS). Each file ran in a separate, sequential process; no statewide sweep or population microsimulation was rerun.
  • make format: passed before each commit; formatting and lint pass.
  • gh pr checks on the new head: confirmed on new head 37bb631; CI started and the initial three guard checks are queued (run 37630379186). Full-suite CI is pending, not yet confirmed green.

Historical differential sweep, reported by earlier rounds and not rerun in round 3:

Rate 2.15.16 disagreements Prior PR sweep
Earnings MTR 551 / 816 0 / 816
MTR including health benefits 562 / 816 0 / 816
State MTR 23 / 816 0 / 816
Federal and FICA MTRs 0 / 816 0 / 816
Capital-gains MTR 52 / 102 0 / 102

Impact

pending (hub), after round 3.

Earlier impact batches never ran: the private zsh runner aborted on its read-only status variable. No microsimulations were run in round 3. The baseline correction affects reform comparisons, so the hub must run all eight paired batches on the default dataset, comparing merge base 47ab53e0fb13eb2e7dfe91025f94f6f2c4310c89 against 37bb63197728f0d481b0782609321872f7bd0a29:

Batch 2025 2026
Base Required Required
Static tax reform Required Required
Labor-supply response Required Required
Capital-gains response Required Required

Use the existing top-rate reform (47%), CBO labor-supply elasticities (income −0.05, substitution 0.25), and capital-gains elasticity −0.62. Report person cliff counts/shares, mean cliff gap, mean MTR overall and among TANF recipients, static budget/poverty/distribution comparisons, and dynamic budget effects relative to the static reform within each version. Unreformed static budget, poverty and distribution outputs do not read marginal rates and are expected to be unchanged; population confirmation remains pending. Reform-versus-baseline comparisons, cliff measures and dynamic results require the hub runs after the baseline correction.

Remaining review points

Round 4 fixes

Head: a729eecd02f9121f0a0a2712268d44e2af7fe952. The redo verified the two existing fast-forward commits, bc17da2fdfae1824170225571e7a925449594f66 (P1) and a729eecd02f9121f0a0a2712268d44e2af7fe952 (P2), and recovered their completed CI results. No merge, history rewrite, or additional source changes were needed in the redo.

  • P1: refresh only the four Ohio 2026 household-net-income snapshots to $23,294.93, $24,175.43, $41,073.76 and $41,546.66 after main Give North Carolina reduced-price school meal students the free tier #9756. The one school-age child's annual meal subsidy falls from $1,142.23816777 to $1,119.60. Removing January CPI uprating reduces it by $26.23816777; correcting paid lunch from $0.46 to $0.44 adds $3.60. This $22.63816777 decrease flows through free school meals, household benefits and net income equally in both earnings pairs. MTR expectations remain 0.1195 and 0.5271.
  • P2: register successfully stored post-construction set_input calls through the shared US simulation mixin. Each branch receives a separate input list; non-input calculated caches still clear, and neutralized or expired writes do not become inputs. Raised branches retain their measuring branch's explicit identification and other inputs, except the perturbed earnings/gains. The fix also reaches health-inclusive rates and both baseline/reform behavioral measurement arms.

Recovered failing evidence at reviewed head 275b87b040a39dfc3c0c246c6947622447d0256b, on macOS with Python 3.14.7 and Core 3.32.15:

  • Ohio YAML: 4 stale income assertions failed, 8 tests passed (113.64s pytest elapsed).
  • Texas $80,000 parent with children aged 4 and 8 and branch-local has_itin=[False, True, True]: federal MTR was -4.28 instead of 0.12, restoring $4,400 CTC in the raised branch. Health-inclusive MTR was -12.481421 instead of 0.1965. The generated input-preservation invariant also failed because raised has_itin was absent: 3 failed, 12 deselected (372.56s).
  • The retained combined-response regression failed because the baseline earnings measurement's raised branch lost identification: 1 failed, 3 deselected (6,113.17s pytest elapsed). A superseded full-policy behavioral case also failed with baseline MTR -4.2035 instead of 0.1965 (2,425.77s); it is supporting evidence, not additional retained CI coverage.

All four requested files passed individually and sequentially in CI run 37687411397, selective job 113055230778, associated with PR head a729eecd. GitHub tested merge commit 2dffe27e73ac39bc8b9e7238dcef529187b9a114, which includes six newer main changes. All round-4 changed files and the four requested test files are identical between that tree and the literal PR head; those incoming policy changes do not affect the new branch-input regressions. Linux used Python 3.14.7, pytest 8.4.2 and Core 3.32.21. These are completed CI runs, not fresh local full-file runs in the redo.

Requested file Result Pytest elapsed, including fixtures Command start to pytest summary
test_marginal_rate_branches.py 15 passed 636.93s 675.39s
test_combined_behavioral_responses.py 4 passed 249.37s 296.53s
test_spm_system.py 74 passed 378.07s 417.17s
household marginal_tax_rate.yaml 12 passed 76.33s 118.30s

Total: 105 passed across the four files. The Rest Python job explicitly names the federal, health, property and combined measurement regressions as passing. The generated invariant covers boolean, enum, string and money inputs; monthly, annual and eternal periods; nested branches; cache recomputation; and caller/sibling isolation. The combined regression observes real raised branches in all four baseline/reform earnings/capital-gains paths. Each selective file reports one plugin assertion-rewrite warning. All 37 checks, including lint, partner contracts and the full SPM/microsimulation CI job, are successful for this PR head.

New-case selective call times were 1.08s (federal), 2.16s (health), 2.99s (property) and 35.93s (combined measurement preservation), with 5.67s shared fixture setup for the identification cases. No per-file peak RSS is available. Existing CI resource artifacts report 9,351,764 kB for the 724-test Rest/core group and 5,361,316 kB for the 113-test SPM group; these are whole-group measurements. Local before timings reflect host contention and a different Core version, so they are not Linux before/after cost benchmarks. New API/branch tests remain in Rest/core; the unchanged SPM file stays in its existing CI group. No CI jobs or runner budgets were added. Repository dependency and lock files were not changed. Prior make format runs and CI lint both passed.

Additional local check at literal head a729eecd, using the same Python 3.14.7 / Core 3.32.15 environment as the recovered failures: the federal, health and generated input-preservation regressions all passed (3 passed, 12 deselected, 4 existing numerical divide warnings). Pytest reported 3,474.20s including collection/setup; end-to-end foreground invocation was 3,955.40s, with 45.46s user CPU, 12.57s system CPU and 1.14 GiB peak RSS. Shared fixture setup was 95.99s; case calls were 5.73s, 10.81s and 11.51s. These macOS measurements reflect severe host contention and are not Linux CI cost comparisons. The process finished normally; no interruption or additional local test process was needed. The four full-file results and behavioral regression passing evidence above come from the completed CI runs.

Impact: P1 is test-only. The P2 source audit finds no expected numeric change in the hub's recorded eight default-dataset batches (2025/2026 base, static top-rate, CBO labor supply and capital-gains response). Construction-time inputs remain equivalent; internal measurement setters copy already preserved primitives or unchanged zero defaults, and sibling input lists stay isolated. Market-income/tax-first queries start shared measurements at the top-level simulation before policy-choice branches. The running impact at 37bb631977 therefore remains applicable to P2. This conclusion is scoped to those recorded paths; direct cold dynamic queries inside policy override branches can now preserve their explicitly set overrides and change accordingly. Main's school-meal parameter update is already in the merged head and is separate from the test-only P1 refresh. No population microsimulations were run in this redo. Hub impact remains pending; CI for this PR head is green.

Merge with main

Merged main f9b55fc4dd05f1bed921a4dd06cb8c81f6ceb2c7 into a729eecd02f9121f0a0a2712268d44e2af7fe952 using merge commit 2c1029deeb176d69a5a081f6233d7cddcb6d2d22, followed by period-specific input fix 3f807d71ca409e421f86d6c56a2beee280171933. No rebase or history rewrite. This section records validation at the merged head; earlier head and CI references remain historical.

Conflict resolutions:

  • policyengine_us/system.py: keep the shared pre-response input migration, synchronized input registry, and post-migration baseline rebuild; retain main's delete_input_values cleanup of obsolete replay keys in both simulation wrappers.
  • policyengine_us/variables/household/marginal_tax_rate_helpers.py: keep the shared earnings/component perturbation and held measured responses; retain Fix IRA deduction phase-outs and compensation limits #10032's user_set_variables and use it inside the shared helper to preserve supplied IRA amounts while clearing formula-internal SSA holder writes.
  • policyengine_us/variables/household/marginal_tax_rate.py: retain marginal_earnings_change, with Fix IRA deduction phase-outs and compensation limits #10032 input preservation applied by the shared helper.
  • policyengine_us/variables/household/marginal_tax_rate_including_health_benefits.py: retain the shared health-inclusive earnings path with the same input preservation.
  • policyengine_us/variables/gov/simulation/capital_gains_responses.py: retain main's response forms/validation and the PR's existing perturbation call. The capital-gains MTR class and loop are unchanged from a729eecd02.

The auto-merged behavioral measurement code keeps main's private baseline-policy measurement branches and cache isolation alongside this PR's hours-response neutralization. The migrated-input regression now expects the deliberately rebuilt baseline to hold pre-response keys; it still checks source/baseline privacy and explicit override retention. d1021's reuse rule is preserved: MTR and perturbation branches hold behavioral responses at the household's measured values.

A merge regression exposed a cross-period interaction with #10032: setting a Roth IRA contribution for 2027 could preserve a calculated 2026 contribution in the raised branch. On locked Core 3.32.27, the new case failed with $500 in the raised branch versus $1,000 in a fresh reference. The shared perturbation helper now applies existing drop_inherited_values after its variable-name filter, preserving exact supplied branch/period keys while clearing other-period calculated caches. The regression checks both IRA allocations, the explicit next-year $2,000 contribution, and parent isolation. It reuses the existing read-only fixture; no full-model setup or CI runner was added. The capital-gains loop waits for #10058.

Validation at 3f807d71ca409e421f86d6c56a2beee280171933: 369 passed across 37 files, each run individually, sequentially, in the foreground with OMP_NUM_THREADS=4 OPENBLAS_NUM_THREADS=4 MKL_NUM_THREADS=4 VECLIB_MAXIMUM_THREADS=4 NUMEXPR_NUM_THREADS=4 OMP_WAIT_POLICY=PASSIVE. This covers every PR-added/changed test file, all marginal-rate files, branch/perturbation and behavioral measurement coverage, and every test file changed by #10032 (plus the two later IRA variable files). Python 3.13.9 used the prescribed shared executable and a workspace-local copy of lockfile-pinned Core 3.32.27; dependency and lock files match merged main. make format passes. No population microsimulations were run.

Test file (under policyengine_us/tests/) Passed Wall seconds
core/test_behavioral_response_measurement_copies.py 16 720.55
core/test_combined_behavioral_responses.py 4 153.72
core/test_spm_system.py 77 391.30
core/test_marginal_rate_branches.py 16 341.43
core/test_marginal_tax_rate_coverage.py 13 203.24
core/test_behavioral_response_measurements.py 16 540.27
core/test_override_branches.py 23 66.24
core/test_has_tin_branch_inputs.py 6 21.20
policy/baseline/household/federal_marginal_tax_rate.yaml 5 59.26
policy/baseline/household/fica_marginal_tax_rate.yaml 5 56.30
policy/baseline/household/marginal_tax_rate.yaml 12 86.10
policy/baseline/household/marginal_tax_rate_computed.yaml 2 41.35
policy/baseline/household/marginal_tax_rate_including_health_benefits.yaml 5 41.58
policy/baseline/household/state_marginal_tax_rate.yaml 5 47.10
core/test_dependent_above_the_line_deductions_invariants.py 9 45.01
core/test_tax_unit_non_dep_add_projector.py 1 12.92
policy/baseline/gov/irs/credits/retirement_saving/savers_credit_person.yaml 16 37.93
policy/baseline/gov/irs/credits/retirement_saving/savers_credit_qualified_contributions.yaml 3 27.72
policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/social_security/dependent_taxable_social_security.yaml 5 26.79
policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/social_security/dependent_taxable_ss_magi.yaml 6 27.30
policy/baseline/gov/irs/income/taxable_income/deductions/qbid/estate_income_would_be_qualified.yaml 6 39.15
policy/baseline/gov/irs/ira/dependent_traditional_ira_deduction.yaml 5 29.88
policy/baseline/gov/irs/ira/ira_active_participant.yaml 14 28.68
policy/baseline/gov/irs/ira/ira_phase_out.yaml 15 26.06
policy/baseline/gov/irs/ira/policybench_ira.yaml 8 30.54
policy/baseline/gov/states/ca/tax/income/deductions/itemized/ca_itemized_deductions_pre_limitation.yaml 13 40.10
policy/baseline/gov/states/mi/tax/income/household_resources_adjustments.yaml 1 26.33
policy/baseline/gov/states/mn/tax/property/homestead_credit_refund/mn_homestead_credit_refund_household_income.yaml 1 35.39
policy/baseline/gov/territories/pr/tax/income/taxable_income/deductions/retirement/pr_retirement_deduction.yaml 4 36.60
policy/baseline/household/expense/retirement/ira_compensation.yaml 12 44.80
policy/baseline/household/expense/retirement/ira_contribution_limit.yaml 8 32.81
policy/baseline/household/expense/retirement/self_employed_pension_contribution_limit.yaml 5 46.20
test_ira_formula_differential.py 6 33.14
test_mi_household_resources_adjustments.py 4 75.41
policy/baseline/gov/irs/ira/ira_219g_magi.yaml 2 61.50
policy/baseline/gov/irs/ira/ira_219g_taxable_social_security.yaml 2 56.73
unit/test_ira_invariants.py 18 72.38

The new regression's isolated before probe failed in 1.09s pytest time / 24.04s including startup; the fixed probe passed in 0.58s / 12.63s, with 0.03s setup and 0.46s case time. These probes stop at different assertions and are not a before/after performance benchmark. Per-case peak RSS is unavailable (/usr/bin/time -l cannot read the sandbox's kernel clock data). The existing Rest/core CI group and runner budget are unchanged. An additional pre-fix merge check on shared Core 3.33.2 passed all 16 behavioral-copy tests (877.20s pytest / 920.17s wall); the table above is the complete final-head validation on locked Core.

The hub reruns base and static impacts at the merged head; dynamic (LSR+CG) runs need a memory window that the hub schedules separately.

Review r5: responses

The cross-year aggregate-input P2 is fixed. user_set_variables(sim, period) now checks visible (variable, branch, period) input keys, with eternal inputs valid in every period. The shared perturbation helper uses that measured-period registry to choose aggregate versus pre-response targets. The IRA fallback passes through the same period filter. Cache cleanup intentionally starts with all supplied variable names, then drop_inherited_values retains only exact supplied keys, preserving the 2027 input while clearing calculated 2026 caches. The no-period API remains compatible with the IRMAA reference helper. d1021 still applies: measured behavioral responses remain fixed.

Fetched PR head 3f807d71ca409e421f86d6c56a2beee280171933 and main f9b55fc4dd05f1bed921a4dd06cb8c81f6ceb2c7; git merge gh/main reported already up to date. No conflict resolutions or history rewrites were needed.

The regression uses OHIO_WORKS_FIRST_PARENT with a branch-local aggregate input, checks both future-year and measured-year inputs, compares against independent fresh references, and verifies source/branch isolation and the supplied 2027 value.

Supplied aggregate period Before rate / raised TANF After rate / raised TANF Fresh rate / raised TANF
2027, measuring 2026 -0.233500004 / $5,974.60 0.119498074 / $5,474.60 0.119498047 / $5,474.60
2026, measuring 2026 -0.233500004 / $5,974.60 -0.233500004 / $5,974.60 -0.233500000 / $5,974.60

The unchanged new regression produced 1 failed, 1 passed before and 2 passed after on Python 3.13.9 / locked Core 3.32.27. It needs Python coverage because YAML cannot exercise post-construction branch inputs. The fixture builds three independent household models once and returns one read-only source plus scalar references. Local before/after elapsed times, including startup, were 38.93s / 30.56s (pytest 9.84s / 6.33s). The passing fixture setup took 2.29s and calls 0.61s / 0.63s. Since the failing run stops at an earlier assertion, these figures are execution evidence rather than a performance delta. Peak RSS is unavailable. The tests remain in the existing Rest/core group; no CI jobs or runner budgets changed.

Validation at 23b7f445dbe8164377b7996c886654b382798780: 371 passed across 39 files. Each file ran in a separate foreground process, sequentially, using the prescribed shared Python 3.13.9 interpreter and workspace-local cached Core 3.32.27 source. Every process set OMP_NUM_THREADS=4 OPENBLAS_NUM_THREADS=4 MKL_NUM_THREADS=4 VECLIB_MAXIMUM_THREADS=4 NUMEXPR_NUM_THREADS=4 OMP_WAIT_POLICY=PASSIVE. All MTR files, branch/perturbation files, every test file changed by #10032, and the existing cross-period IRA regression passed. The additional labor-supply guard and IRMAA helper compatibility files passed too. 26 Microsimulation cases were deselected (2 in the MTR branch file, 24 in SPM); no Microsimulation cases ran. make format and git diff --check pass. No dependency, lockfile, Makefile or workflow changes. New-head CI and population impacts are left to the hub.

Paths below are relative to policyengine_us/tests/; wall times include interpreter startup and cleanup, and are local macOS measurements.

File Passed Deselected Wall seconds
core/test_marginal_rate_branches.py 16 2 326.88
core/test_marginal_tax_rate_coverage.py 13 0 147.49
core/test_override_branches.py 23 0 68.80
core/test_has_tin_branch_inputs.py 6 0 25.16
core/test_behavioral_response_measurement_copies.py 16 0 738.22
core/test_combined_behavioral_responses.py 4 0 487.83
core/test_behavioral_response_measurements.py 16 0 125.54
core/test_spm_system.py 53 24 203.05
policy/baseline/household/federal_marginal_tax_rate.yaml 5 0 53.25
policy/baseline/household/fica_marginal_tax_rate.yaml 5 0 46.44
policy/baseline/household/marginal_tax_rate.yaml 12 0 86.45
policy/baseline/household/marginal_tax_rate_computed.yaml 2 0 35.35
policy/baseline/household/marginal_tax_rate_including_health_benefits.yaml 5 0 54.03
policy/baseline/household/state_marginal_tax_rate.yaml 5 0 53.00
core/test_dependent_above_the_line_deductions_invariants.py 9 0 54.16
core/test_tax_unit_non_dep_add_projector.py 1 0 23.22
policy/baseline/gov/irs/credits/retirement_saving/savers_credit_person.yaml 16 0 31.33
policy/baseline/gov/irs/credits/retirement_saving/savers_credit_qualified_contributions.yaml 3 0 39.87
policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/social_security/dependent_taxable_social_security.yaml 5 0 30.47
policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/social_security/dependent_taxable_ss_magi.yaml 6 0 38.00
policy/baseline/gov/irs/income/taxable_income/deductions/qbid/estate_income_would_be_qualified.yaml 6 0 45.86
policy/baseline/gov/irs/ira/dependent_traditional_ira_deduction.yaml 5 0 30.41
policy/baseline/gov/irs/ira/ira_active_participant.yaml 14 0 34.16
policy/baseline/gov/irs/ira/ira_phase_out.yaml 15 0 28.57
policy/baseline/gov/irs/ira/policybench_ira.yaml 8 0 35.88
policy/baseline/gov/states/ca/tax/income/deductions/itemized/ca_itemized_deductions_pre_limitation.yaml 13 0 45.76
policy/baseline/gov/states/mi/tax/income/household_resources_adjustments.yaml 1 0 31.17
policy/baseline/gov/states/mn/tax/property/homestead_credit_refund/mn_homestead_credit_refund_household_income.yaml 1 0 36.98
policy/baseline/gov/territories/pr/tax/income/taxable_income/deductions/retirement/pr_retirement_deduction.yaml 4 0 30.85
policy/baseline/household/expense/retirement/ira_compensation.yaml 12 0 31.44
policy/baseline/household/expense/retirement/ira_contribution_limit.yaml 8 0 27.79
policy/baseline/household/expense/retirement/self_employed_pension_contribution_limit.yaml 5 0 31.55
test_ira_formula_differential.py 6 0 10.63
test_mi_household_resources_adjustments.py 4 0 34.38
unit/test_ira_invariants.py 18 0 42.14
policy/baseline/gov/irs/ira/ira_219g_magi.yaml 2 0 45.38
policy/baseline/gov/irs/ira/ira_219g_taxable_social_security.yaml 2 0 40.95
core/test_labor_supply_behavioral_response_guard.py 3 0 10.74
policy/baseline/gov/hhs/medicare/test_irmaa_lookback_and_switch.py 23 0 88.52

axiom: n/a: simulation infrastructure; no statutory provision changes.

marginal_tax_rate and its relatives measure a rate by raising one adult's
earnings in a branch that keeps only the simulation's listed inputs. Two
defects made the branch disagree with a fresh simulation of the same
household:

- Simulation moves employment_income, self-employment income, weekly hours
  and long-term gains onto pre-response variables, and backfills state_code
  from state_code_str, after policyengine-core records input_variables.
  Only Microsimulation patched the list, so household branches erased the
  moved inputs. An Ohio parent earning $28,000 then drew $7,475 of Ohio
  Works First in the branch and reported a rate of -4.70; fresh
  simulations give 0.527.
- Branches raised the employment_income aggregate, which programs that
  read pre-response income (TANF's earned income) never see, and left the
  base's cached monthly values in place. Microsimulation rates therefore
  missed TANF phase-outs.

The moves and the input list now live in shared helpers used by both
wrappers, and one branch builder raises the variable that holds each input
for every branch-measured rate. marginal_tax_rate_on_capital_gains raised
the capital_gains aggregate, which taxed the increase at ordinary rates; it
now raises long-term gains.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.24%. Comparing base (909176a) to head (23b7f44).
⚠️ Report is 533 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff             @@
##              main    #9698      +/-   ##
===========================================
- Coverage   100.00%   97.24%   -2.76%     
===========================================
  Files            4        2       -2     
  Lines           76      145      +69     
  Branches         2        7       +5     
===========================================
+ Hits            76      141      +65     
- Misses           0        4       +4     
Flag Coverage Δ
unittests 97.24% <100.00%> (-2.76%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

MaxGhenis and others added 5 commits October 6, 2026 03:01
Conflict: tests/policy/baseline/household/marginal_tax_rate.yaml. Both
sides appended cases at the end of the file. Kept main's comment fix and
its three-roommate case from #9653, then this PR's six Ohio base/alt/rate
cases. The rate variables' docstrings from #9653 merged cleanly next to
this PR's formula changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… moving inputs

Rates measured in a reformed simulation with labor supply or capital gains
elasticities now hold the responses at their simulated values. Since the
branches raise pre-response inputs (employment_income_before_lsr,
long_term_capital_gains_before_response), recomputing the responses there
scaled them with the raise: a Texas worker's $1,000 raise became $971 of
wages (rate 0.3166, fresh 0.2965), and a 20% capital gains rate read 0.3307.
create_perturbed_branch, which every earnings and capital gains rate uses,
now sets the three earnings responses, the capital gains response and the
weekly hours response to their values in the simulation; neutralized ones,
as in the behavioral response measurement branches, stay zero.

Core branches a reformed simulation's baseline while constructing it, before
the wrappers move inputs. That branch kept employment_income and
weekly_hours_worked as inputs with empty pre-response variables and the stale
input_variables, so its TANF counted no earnings and the baseline rate that
labor supply responses compare against erased pre-response wages: an Ohio
Works First parent got a -$416 response to a top-rate reform that does not
touch them. Both wrappers now branch the baseline again after the moves, as
core's subsample does.

Adds tests for both (invariants 5 and 6) and corrects the #9653 comment on
the component helper's adult loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 8, 2026
…9994)

* Bind a subsampled reform's rebuilt baseline arm to baseline policy

Core's subsample rebuilds the baseline arm as a branch of the reform and
then hands it the replaced arm's policy. Before core 3.32.21 it does not
rebind the branch's holders, so the baseline read the reform's neutralized
income_tax ($0 instead of $4,016), and the arm kept the replaced arm as its
own baseline. No core version points the policy back at the new arm or
primes its private parameter-tracing root, so a traced baseline arm wrote
its tracer onto the shared parameter tree (core 3.32.8).

Clear the arm's baseline, rebind its holders and isolate its tracing, as
construction does. Adds a regression test for both wrappers and their
clones and a Hypothesis property that subsampling never changes which
policy a holder belongs to.

Found in review of #9698 (P3-1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drop holders a baseline arm cannot use, at construction and after subsampling

A reform's baseline arm is a branch of the reform, so its holders were
built for the reform's variables. Rebinding one to the baseline's variable
kept the holder's population and storage, so a reform that moved a variable
to another entity, or into or out of ETERNITY, left the baseline arm with
storage its own variable cannot use (core 3.32.8; core 3.32.21 drops these
holders itself). Drop them with their recorded inputs, as core 3.32.21's
binder does, on both baseline-arm paths. apply_reform keeps a simulation's
own inputs, as core's does. No shipped reform changes a variable's entity
or ETERNITY-ness.

Tests now also check that each holder suits its variable, and the property
test's structural reform redefines is_household_head as yearly. The
regression test reforms the supplied system instead of rebuilding policy
from source.

Addresses review r1 of #9994.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 11, 2026
…'s branch

Review r1 of #10058 found two ways the branch lost an input:

- A capital_gains total set through set_input after construction is not
  among the construction inputs the branch keeps, so the branch dropped it
  and rebuilt the total from long-term gains: 73.4 and 231.9 instead of
  0.188. The branch now raises the parent's total whenever set_input stored
  one for the period, at construction or later (tools.period_branch.has_input).
- A variable given for one period kept every period's array, so a 2026
  value calculated before branching survived when the input was for 2025
  (adjusted gross income for 2025 measured 0.000). The branch now drops what
  was calculated for periods the input was not given for
  (drop_inherited_values limited to those variables).

Also neutralize the hours response in the behavioral response measurement
branches, as #9698 does: a capital-gains-first request with labor supply
elasticities otherwise re-entered the measurements through hours and raised
a CycleError. In labor-first order those branches already returned zero for
it through the labor supply response's re-entry guard.

Tests: a late-set total (Python), an input for another year and New York's
$25 million threshold, which the scaled rise spreads over $24,985 (YAML).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Branch-measured marginal tax rates disagree with fresh re-simulation

1 participant