Repository navigation
Conversation
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 Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9698 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 4 2 -2
Lines 76 111 +35
Branches 2 7 +5
=========================================
+ Hits 76 111 +35
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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>
This was referenced Oct 10, 2026
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #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_variablesafter moving inputs and backfilling geography, then rebuild the reform's baseline with those inputs. This also changes exported input names: datasets exported throughinput_variablesretain pre-response variables instead of stale aggregate columns, includinglong_term_capital_gains.Round 3 fixes
UserReformregression must calculate $4,016 of baseline income tax while the reform calculates $0; reform-only inputs must not leak into baseline cache invalidation.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 from792e7d4dbb; no merge in this round).make format: passed before each commit; formatting and lint pass.gh pr checkson 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:
Impact
pending (hub), after round 3.
Earlier impact batches never ran: the private zsh runner aborted on its read-only
statusvariable. 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 base47ab53e0fb13eb2e7dfe91025f94f6f2c4310c89against37bb63197728f0d481b0782609321872f7bd0a29: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
axiom: n/a: simulation infrastructure; no statutory provision changes.
Round 4 fixes
Head:
a729eecd02f9121f0a0a2712268d44e2af7fe952. The redo verified the two existing fast-forward commits,bc17da2fdfae1824170225571e7a925449594f66(P1) anda729eecd02f9121f0a0a2712268d44e2af7fe952(P2), and recovered their completed CI results. No merge, history rewrite, or additional source changes were needed in the redo.set_inputcalls 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: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 raisedhas_itinwas absent: 3 failed, 12 deselected (372.56s).All four requested files passed individually and sequentially in CI run 37687411397, selective job 113055230778, associated with PR head
a729eecd. GitHub tested merge commit2dffe27e73ac39bc8b9e7238dcef529187b9a114, 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.test_marginal_rate_branches.pytest_combined_behavioral_responses.pytest_spm_system.pymarginal_tax_rate.yamlTotal: 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 formatruns 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
37bb631977therefore 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.