Skip to content

Give the additional_tax_bracket reform's unset bracket zero tax instead of NaN - #9831

Merged
MaxGhenis merged 8 commits into
mainfrom
fix-additional-tax-bracket-nan
Oct 6, 2026
Merged

MaxGhenis merged 8 commits into
mainfrom
fix-additional-tax-bracket-nan

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The additional_tax_bracket contrib reform ships brackets 7 and 8 of its schedule (gov.contrib.additional_tax_bracket.bracket) with infinite thresholds for every filing status from 2018 on. Until a user sets bracket 7's threshold, its income_tax_main_rates and regular_tax_before_credits are NaN on main for every household at any income, and so is income_tax. I reproduced this on main for 2025, 2026 and 2027, at taxable incomes of $50,000 and $1 million, with and without long-term gains.

Cause. tax_at_main_rates is the bracket walk shared by the baseline and the reform. It took each bracket's amount as amount_between(x, bottom, top), which is np.clip(x, bottom, top) - bottom. Bracket 8's bottom is bracket 7's infinite threshold, so that amount is inf - inf = NaN.

#9792, merged in the meantime, added amount_taxed_below_rate, the amount behind 26 U.S.C. 1(h)(1)(A)(ii)(I), beside it. That function walks the same brackets with the same clip form, and #9792 made the reform use it for taxable_income_taxed_below_25_percent.

  • With the fix to tax_at_main_rates alone, a bracket 8 rate below 25% with bracket 7 unset still gave NaN for that amount.
  • The NaN then flows through capital_gains_excluded_from_taxable_income (max_/min_ of NaN) into income_tax_main_rates for every household. That last step follows from numpy's NaN semantics; I did not run it as a simulation.

Fix. A new generator, amounts_in_brackets, now produces each bracket's amount for both functions. It keeps the running-maximum clamp from #9084, so brackets never overlap, and computes max(0, min(x, top) - bottom), the form policyengine-core's MarginalRateTaxScale.calc uses.

  • That form is zero for a bracket above an infinite threshold, and equals the clip form bit for bit whenever the bracket's bottom is finite.
  • Sharing the walk keeps the amount taxed below a rate consistent with the tax by construction.

Baseline impact: none

  • The baseline schedule's bracket bottoms are all finite; only its top threshold is infinite. Its tax, and its income taxed below 25 percent, are therefore bit-identical to before.
    • test_baseline_schedule_is_unchanged_in_every_year compares both functions with the old clip form on gov.irs.income.bracket, for every filing status and every year from 2018 to 2035.
    • The schedule properties check the same bitwise equality for any drawn schedule, wherever the clip form gave a number.
  • No microsimulation was run. The contrib reform is off by default (in_effect is false) and has no baseline impact, and the shared walk's baseline outputs are bit-identical.

axiom: n/a: computation fix to a bracket walk for a contrib reform's unset bracket; enacted-law outputs are bit-for-bit unchanged, so rulespec-us has nothing to mirror

Invariants (property and differential tests)

Function properties. These hold for any thresholds, finite or infinite and in any order, for rates in [0, 1] and amounts in [0, $10 million]. Each test ran 300 Hypothesis examples plus explicit ones.

  1. tax_at_main_rates is finite and non-negative.
  2. Differential: it agrees with policyengine-core's MarginalRateTaxScale over the same brackets.
  3. Differential: it is bit-identical to the old clip form wherever that form gave a number. The old form gave NaN exactly where some bracket bottom is infinite.
  4. Identity: adding bracket 8 changes the tax by exactly (r8 − r7) × the income in bracket 8.
    • So the tax is at least the tax with the bracket removed (bracket 8's income taxed at bracket 7's rate) whenever r8 ≥ r7.
    • When r8 < r7 the added bracket cuts the tax. That violation is intended.
  5. With bracket 7's threshold infinite, as shipped, the eight-bracket tax equals the seven-bracket tax bit for bit, whatever bracket 8's rate and threshold.
  6. amount_taxed_below_rate, for any rate limit in [0, 1]:
    • it is finite, and lies between zero and the taxable amount;
    • it is bit for bit the tax at rates of 1 for the brackets below the limit and 0 for the others;
    • it agrees with MarginalRateTaxScale at those rates;
    • it is bit for bit the old clip form wherever that form gave a number.
      An explicit example puts a rate exactly at the limit, as the reform's 2026 bracket 3 rate of 25% is.

The reform in simulations. The grid has 50 tax units: 5 filing statuses × 5 ordinary taxable incomes × with or without $170,000 of long-term gains and qualified dividends.

  1. With default parameters, five variables are finite in 2025 and 2026: income_tax_main_rates, regular_tax_before_credits, taxable_income_taxed_below_25_percent, income_tax_before_credits and income_tax.

  2. Differential: with brackets 1 to 7 set to current law and the added bracket unset, the reform reproduces the baseline for those five variables.

  3. For any thresholds of brackets 7 and 8 and r8 ≥ r7:

    • the reform's three variables are finite;
    • its two taxes are at least their values with the bracket removed.

    Thresholds are set per filing status, so one simulation covers five shapes: both infinite; finite bracket 7 with bracket 8 unbounded; bracket 8 below bracket 7; a zero bracket 7 threshold; and bracket 7 unbounded above a finite bracket 8. Three explicit and three generated examples run.

    Removing the bracket can move bracket 8 across the 25% limit, which changes the amount taxed below 25%. These households have no 28%-rate or unrecaptured section 1250 gain, so that amount changes neither dwks19 nor capital_gains_excluded_from_taxable_income for them, and the inequality concerns only the schedule.

No simulation sets a foreign earned income exclusion. The reform's subtraction of the tax on the excluded amount is covered only through invariants 1 to 6, not end to end.

Tests

  • New YAML cases in contrib/additional_tax_bracket:
    • 2026 joint, $1 million taxable income, under the reform's own 2026 schedule: $324,821, computed by hand in the file.
    • 2026 single with no taxable income: $0.
    • 2025 joint, $1 million: $294,062.50, from the 2025 joint rate table (Rev. Proc. 2024-40, section 2.01, Table 1: $202,154.50 plus 37% of the excess over $751,600).
  • On main, before the fix:
  • On the fix (local runs at af420d4; later commits change only tests, comments and the changelog, and the folder was re-run at the head as noted):
    • contrib/additional_tax_bracket: 18 passed (9 YAML cases, 9 Python tests).
    • Related tests: 266 passed. These are the IRS alternative_minimum_tax, before_credits, capital_gains and foreign_earned_income_exclusion folders, non_sch_d_capital_gains.yaml, the contrib watca, Biden capital_gains_tax_increase and Harris capital_gains YAML files, test_negative_main_rates_inverted_brackets.py, test_form_6251_part_iii.py and test_form_4952_section_911_interaction.py.
    • tests/code_health: 136 passed.

The new Python file runs in two CI steps: make test-policy-contrib-python, and the contrib YAML batch, because test_batched.py hands policyengine-core test the whole subdirectory. Other contrib folders with Python tests, such as contrib/local/ny, already run that way. That is why the simulation property is kept to six examples.

The new YAML cases reuse the reform combo of #9792's case, so they add no cached system. The file's combo weight is 2.25, within the 5.0 budget.

Memory.

  • At a42a678, CI's YAML batch for this folder peaked at 10,719 MB (run 37389800915, job 112033537450). A main-based PR run whose folder holds only the YAML file peaked at 6,578 MB (run 37388221916).
  • The tests had built a module-level reformed system and cloned it for each simulation with parameter changes.
  • From 80f291b, each reformed simulation instead applies the reform to a clone of the already loaded default system, and the tests collect each simulation before building the next.
  • Locally (macOS, Python 3.14t), the folder's peak resident set fell from 14.5 GB to 9.45 GB for the two changes together, with the same 18 tests passing.
  • Every simulation call also asserts that the shared default system's reform variables and schedule values are unchanged afterwards. So the reform and parameter changes reach only the clone.

Review

Two independent Opus reviews.

  • First review: requested changes. All were made in dd18085:
    • suppress Hypothesis's too_slow health check, after a loaded host stalled one draw for 180 seconds;
    • make the simulation property cheaper;
    • make the docstring accurate about foreign earned income;
    • add notes on the stale 2026 schedule.
  • Second review, after the merge and the shared walk: approved. It checked the refactor against the merge and main versions in a 6,000-case differential, with 0 bit mismatches wherever the old form was defined. It also showed the new property kills a <=-for-< mutant, a mutant without the clamp, and the clip-form version. Its wording nits are made in a42a678.

Not changed here (follow-ups, filed as #9832)

  • Stale 2026 schedule. The reform's parameter file sets its own schedule from 2026: rates of 10, 15, 25, 28, 33, 35 and 39.6%, with thresholds marked "CBO forecast". Current law's 2026 rates are 10, 12, 22, 24, 32, 35 and 37%.
    • Its 2026 married-filing-separately thresholds are non-monotone: bracket 5's 541,550 is above bracket 6's 305,875.
    • So turning the reform on for 2026 or later changes brackets 1 to 7 as well as adding bracket 8.
    • The new 2026 YAML case pins that schedule and will move when the schedule is refreshed.
  • Finite bracket 8 threshold. A finite threshold for bracket 8 leaves income above it untaxed, because the walk taxes nothing above the last threshold.

🤖 Generated with Claude Code

MaxGhenis and others added 5 commits October 5, 2026 18:05
With its shipped parameters, brackets 7 and 8 of the reform's schedule
have infinite thresholds, and its income_tax_main_rates and
regular_tax_before_credits are NaN for every household. Three YAML cases
pin the finite tax the reform should give (2026 joint and zero-income
filers, and the 2025 joint rate table from Rev. Proc. 2024-40, section
2.01). Property tests check tax_at_main_rates for any thresholds,
finite or infinite, against policyengine-core's MarginalRateTaxScale and
against the old clip form wherever it was defined. A reform-level
property checks that, for any thresholds of brackets 7 and 8, the
reform's taxes are finite and at least the taxes with the bracket
removed.

On main, the three YAML cases and seven of the eight Python tests fail
with NaN. The check that the baseline schedule is unchanged passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tax_at_main_rates took each bracket's amount as amount_between's
clip(x, bottom, top) - bottom. Above an infinite threshold, a bracket runs
from infinity to infinity and that is inf - inf = NaN, so the
additional_tax_bracket reform, whose brackets 7 and 8 ship with infinite
thresholds, gave NaN for every household. The loop now takes
max(0, min(x, top) - bottom), as policyengine-core's
MarginalRateTaxScale.calc does. That is zero for such a bracket and equals
the clip form bit for bit whenever the bottom is finite, so the baseline
schedule is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Suppress Hypothesis's too_slow and data_too_large health checks, as the
  repository's other Hypothesis tests do: on a loaded machine one draw
  stalled for 180 seconds and failed the reform-level property.
- Each reform-level example runs two simulations on clones of the
  reform's system, and the file runs in both the contrib YAML step and the
  contrib Python step. Set brackets 7 and 8 per filing status so that one
  simulation covers five threshold shapes, and run three explicit and
  three generated examples instead of four and twelve.
- Say that no simulation sets a foreign earned income exclusion, so the
  reform's subtraction of the tax on the excluded amount is covered only
  through the schedule properties.
- Note that the reform's own 2026 schedule is not current law, and that
  the fix also covers an infinite threshold below the baseline's top
  bracket.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both sides appended cases to additional_tax_bracket_reform.yaml; keep both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#9792 added amount_taxed_below_rate beside tax_at_main_rates. It walks the
same clamped brackets but takes each bracket's amount with
amount_between's clip form, so a bracket above an infinite threshold
whose rate is below the limit still gave inf - inf = NaN: for the
additional_tax_bracket reform, a bracket 8 rate under 25 percent with
bracket 7 unset. Both functions now take each bracket's amount from
amounts_in_brackets, which holds the #9084 clamp and the
max(0, min(x, top) - bottom) form, so the amount below a rate stays
consistent with the tax by construction.

The tests check amount_taxed_below_rate for any thresholds and any rate
limit: finite, between zero and the amount, bit for bit the tax at rates
of 1 below the limit and 0 otherwise, close to MarginalRateTaxScale at
those rates, and bit for bit the old clip form wherever that form gave a
number, including on the baseline schedule for every year from 2018 to
2035. The simulation tests also check the reform's
taxable_income_taxed_below_25_percent.

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

codecov Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (909176a) to head (1e30280).
⚠️ Report is 217 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #9831   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            4         1    -3     
  Lines           76        19   -57     
  Branches         2         4    +2     
=========================================
- Hits            76        19   -57     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

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.

- Add an explicit example where a bracket's rate equals the 25 percent
  limit, as the reform's 2026 bracket 3 does, so a rate equal to the
  limit stays outside the amount taxed below it whatever examples
  Hypothesis generates.
- Say that amount_taxed_below_rate took the clip form only for brackets
  taxed below its limit, and that the brackets' pieces sum to at most the
  taxable amount.
- Reword the changelog: the bracket adds nothing to the tax and to the
  income taxed below 25 percent; the total amount is not zeroed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 2 commits October 5, 2026 21:06
The contrib YAML batch for this folder, which also collects the Python
tests, peaked at 10,719 MB on CI at a42a678, against 6,578 MB for the
same folder on a recent run from main (run 37388221916). The tests built a
module-level reformed system and cloned it for every simulation.
Instead, apply the reform to a clone of the already loaded default system
for each simulation, and collect each simulation before building the
next. Locally (macOS, Python 3.14t) the folder's peak resident set fell
from 14.5 GB to 9.45 GB, with the same 18 tests passing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each reformed simulation in these tests applies the reform and its
parameter changes to a clone of the loaded default system. After every
simulation, assert that the default system still holds its own
income_tax_main_rates, regular_tax_before_credits and
taxable_income_taxed_below_25_percent, and its 2026 values of the
reform's schedule, so a leak into the shared system fails here rather
than in a later test. The folder passes locally: 18 tests.

Correction to 80f291b's message: run 37388221916 is a pull-request
run on a main-based branch (nc-reduced-price-school-meals) whose folder
holds only the YAML file, not a run on main; and the old tests cloned
their module-level reformed system only for the simulations with
parameter changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis merged commit 50464ee into main Oct 6, 2026
37 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Merged by the US + core hub on its merge gates. Audit:

  • Head: 1e302805cdb1db73a50b8df71d4beb7db1e7626b.
  • Independent review (Opus 5.5, Subfleet; files in ~/reviews/us-hub/atb-nan/):
    • Reviews 1 and 2 requested changes, all of which were addressed in dd18085a25, af420d432d and a42a67842e.
    • Review 3 approved the code and tests at 80f291bd79.
    • Review 4 approved the 80f291bd79..1e302805cd delta. That delta is test-only: one file, +24 lines, adding a check that reformed simulations leave the default system unchanged. Review 4 has no blocking findings.
  • Tests: the reviewer asked for a run of the changed test file at this head. CI's other-shard-1 job runs the contrib test folder, and every check passed at this head before merging.
  • Impact: the contrib additional_tax_bracket reform is off by default. A 6,000-case differential showed the shared bracket walk's baseline outputs are bit-identical to main, so there is no baseline microsimulation change.
  • Follow-ups: additional_tax_bracket reform: from 2026 its brackets 1-7 are not current law, and a finite bracket 8 threshold untaxes income above it #9832 covers the reform's post-2025 bracket schedule. The stray root PROGRESS.md on main and the double run of contrib Python tests are noted in the build report.
  • Decisions: none.

@MaxGhenis
MaxGhenis deleted the fix-additional-tax-bracket-nan branch October 6, 2026 02:51
MaxGhenis added a commit that referenced this pull request Oct 6, 2026
Brings in #9831 (amounts_in_brackets, the shared bracket walk that
tax_at_main_rates and amount_taxed_below_rate use) and #9788 (capital gain
distributions netted inside the capital loss limit). No textual conflicts:
the additional_tax_bracket reform's tax_on_taxable_income_at_main_rates
override calls tax_at_main_rates, so it takes #9831's walk.

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

- test_additional_tax_bracket_unset_bracket.py (#9831) listed
  regular_tax_before_credits among the variables the reform replaces. On
  this branch the reform replaces tax_on_taxable_income_at_main_rates
  instead, and regular_tax_before_credits is income_tax_main_rates plus
  capital_gains_tax. The default-system leak check now covers the three
  replaced variables, and the finite and monotone checks cover the reform's
  three taxes: income_tax_main_rates, tax_on_taxable_income_at_main_rates
  and regular_tax_before_credits.
- test_regular_tax_before_credits.py set aside households with capital gain
  distributions and a Schedule D net loss in the monotonicity property.
  #9788 nets the distributions inside the capital loss limit in AGI, so the
  property now holds for every household. The exclusion is gone, and the
  minimized counterexample (2018 single, $50,000 of distributions, a
  $40,273 short-term loss, $1 more long-term gain) is an explicit example;
  it failed before #9788 (the regular tax fell by $0.12) and passes now.

Both files pass after the merge: 9 and 6 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 6, 2026
…1)) and use it on Form 6251 line 10 (#9852)

* Cap the regular tax at the tax on all taxable income and use it on Form 6251 line 10

26 U.S.C. 1(h)(1) says the tax "shall not exceed" the sum of the
subparagraph (A) to (F) amounts, and Schedule D Tax Worksheet line 47
takes the smaller of line 45 and line 46 (the tax on all of line 1).
capital_gains_tax now stops at tax_on_taxable_income_at_main_rates (new,
line 46) less income_tax_main_rates. The cap binds where gain taxed at
15 percent falls in the 12 percent bracket: up to $3.75 single and $7.50
joint in 2025, and $28.50 and $57 in 2026. The Biden and Harris capital
gains reforms apply the same limit, and the additional_tax_bracket
reform figures line 46 on its own schedule.

regular_tax_before_credits was a second Schedule D Tax Worksheet
(2017 numbering) whose result was min(worksheet tax including the gains,
income_tax_main_rates excluding them). Form 6251 line 10 then added
capital_gains_tax. Whenever the worksheet path fell below the main-rates
path, line 10 understated the regular tax: before #9781 that happened for
18 dataset tax units in 2025 ($328M weighted), because dwks10 ignored
the Form 4952 line 4g election and because the worksheet took line 28 as
min(line 21, line 22) instead of their sum. regular_tax_before_credits
is now income_tax_main_rates plus capital_gains_tax, the regular tax
income_tax_before_credits charges, and Form 6251 line 10 uses it alone.

Tests: real-unit regression cases for tax units 1101624, 1004930,
1156248 and 1032460; line 45/46 boundary cases for 2025 and 2026
(single, joint, head of household, Form 2555); the existing worksheet
property tests now compare with line 47 and Foreign Earned Income Tax
Worksheet line 25 exactly.

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

* Cite Rev. Proc. 2025-32 section 4 for 2026 amounts

Section 3 of Rev. Proc. 2025-32 modifies 2025 items (only .01, the
standard deduction, and .02, section 179); the 2026 adjusted items are
section 4. The 2026 AMT breakpoints are section 4.10 (PDF page 16) and
the 2026 additional standard deduction for the aged or blind is section
4.14(3) (PDF page 18). Also give the dependent standard deduction
citation its section number (4.14(2)).

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

* Add property and differential tests for the regular tax and the line 47 limit

test_regular_tax_before_credits.py transcribes the 2025 Schedule D Tax
Worksheet, lines 1 to 47, with every skip instruction, the Form 4952
election, capital gain distributions, short-term losses and Schedule D
lines 18 and 19. The regular rates come from the 2025 Tax Computation
Worksheet rows. 2026 runs on Rev. Proc. 2025-32's tables and capital gains
amounts. The model matches lines 10, 13, 14, 21, 44, 46 and 47 for a grid,
households at the 0 percent amount, and Hypothesis draws.

Properties, 2018 to 2035:
- 0 <= regular tax <= line 46;
- 0 <= capital_gains_tax <= line 46 - income_tax_main_rates;
- regular_tax_before_credits = main rates + capital gains tax;
- income_tax_before_credits = regular + AMT;
- the AMT is line 9 less line 10;
- without net capital gain, the regular tax is line 46;
- the worksheet lines (dwks10, dwks13, line 18 <= 21 <= 14);
- the tax is monotone in wages, gains and dividends;
- results are independent of batch order.

The limit binds at the top of the 12 percent bracket in every year and
filing status, with the published band widths for 2018 to 2026.

The monotonicity check sets aside households with capital gain
distributions and a Schedule D net loss, a known defect it found. Gross
income adds non_sch_d_capital_gains outside the loss limit, while
net_capital_gain nets them (a 2018 single filer's tax falls $0.12 with $1
more gain). No dataset tax unit has both. It is left for a follow-up.

Also adds 2026 joint, separate and non-binding line 45/46 YAML cases,
from the same Rev. Proc. 2025-32 figures as the Axiom companion tests.

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

* Align the merged tests with main: #9831's reform variables and #9788's netting

- test_additional_tax_bracket_unset_bracket.py (#9831) listed
  regular_tax_before_credits among the variables the reform replaces. On
  this branch the reform replaces tax_on_taxable_income_at_main_rates
  instead, and regular_tax_before_credits is income_tax_main_rates plus
  capital_gains_tax. The default-system leak check now covers the three
  replaced variables, and the finite and monotone checks cover the reform's
  three taxes: income_tax_main_rates, tax_on_taxable_income_at_main_rates
  and regular_tax_before_credits.
- test_regular_tax_before_credits.py set aside households with capital gain
  distributions and a Schedule D net loss in the monotonicity property.
  #9788 nets the distributions inside the capital loss limit in AGI, so the
  property now holds for every household. The exclusion is gone, and the
  minimized counterexample (2018 single, $50,000 of distributions, a
  $40,273 short-term loss, $1 more long-term gain) is an explicit example;
  it failed before #9788 (the regular tax fell by $0.12) and passes now.

Both files pass after the merge: 9 and 6 tests.

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

* Say in the changelog that regular_tax_before_credits now includes the capital gains tax

PolicyEngine/salt-amt-calculator's API reads regular_tax_before_credits as
its regular tax, so the meaning change belongs in the release note.

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

* Point three Rev. Proc. 2025-32 links at the pages with their 2026 amounts

Three links pointed into the wrong section of Rev. Proc. 2025-32:

- the capital gains rate thresholds linked page 12, which is section 4.02
  (kiddie tax). Section 4.03 and its 2026 amounts (49,450, 98,900, 66,200,
  545,500, 613,700, 579,600 and 306,850) are on page 13;
- the child tax credit amount ($2,200) and refundable maximum ($1,700)
  linked page 13, which holds sections 4.03 and 4.04. Section 4.05 and its
  amounts are on page 14.

The other parameter links to the Rev. Proc. land on the page that has the
2026 amounts or on the page where their section starts.

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 7, 2026
…worksheets (#9893)

* Cap the regular tax at the tax on all taxable income and use it on Form 6251 line 10

26 U.S.C. 1(h)(1) says the tax "shall not exceed" the sum of the
subparagraph (A) to (F) amounts, and Schedule D Tax Worksheet line 47
takes the smaller of line 45 and line 46 (the tax on all of line 1).
capital_gains_tax now stops at tax_on_taxable_income_at_main_rates (new,
line 46) less income_tax_main_rates. The cap binds where gain taxed at
15 percent falls in the 12 percent bracket: up to $3.75 single and $7.50
joint in 2025, and $28.50 and $57 in 2026. The Biden and Harris capital
gains reforms apply the same limit, and the additional_tax_bracket
reform figures line 46 on its own schedule.

regular_tax_before_credits was a second Schedule D Tax Worksheet
(2017 numbering) whose result was min(worksheet tax including the gains,
income_tax_main_rates excluding them). Form 6251 line 10 then added
capital_gains_tax. Whenever the worksheet path fell below the main-rates
path, line 10 understated the regular tax: before #9781 that happened for
18 dataset tax units in 2025 ($328M weighted), because dwks10 ignored
the Form 4952 line 4g election and because the worksheet took line 28 as
min(line 21, line 22) instead of their sum. regular_tax_before_credits
is now income_tax_main_rates plus capital_gains_tax, the regular tax
income_tax_before_credits charges, and Form 6251 line 10 uses it alone.

Tests: real-unit regression cases for tax units 1101624, 1004930,
1156248 and 1032460; line 45/46 boundary cases for 2025 and 2026
(single, joint, head of household, Form 2555); the existing worksheet
property tests now compare with line 47 and Foreign Earned Income Tax
Worksheet line 25 exactly.

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

* Cite Rev. Proc. 2025-32 section 4 for 2026 amounts

Section 3 of Rev. Proc. 2025-32 modifies 2025 items (only .01, the
standard deduction, and .02, section 179); the 2026 adjusted items are
section 4. The 2026 AMT breakpoints are section 4.10 (PDF page 16) and
the 2026 additional standard deduction for the aged or blind is section
4.14(3) (PDF page 18). Also give the dependent standard deduction
citation its section number (4.14(2)).

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

* Add property and differential tests for the regular tax and the line 47 limit

test_regular_tax_before_credits.py transcribes the 2025 Schedule D Tax
Worksheet, lines 1 to 47, with every skip instruction, the Form 4952
election, capital gain distributions, short-term losses and Schedule D
lines 18 and 19. The regular rates come from the 2025 Tax Computation
Worksheet rows. 2026 runs on Rev. Proc. 2025-32's tables and capital gains
amounts. The model matches lines 10, 13, 14, 21, 44, 46 and 47 for a grid,
households at the 0 percent amount, and Hypothesis draws.

Properties, 2018 to 2035:
- 0 <= regular tax <= line 46;
- 0 <= capital_gains_tax <= line 46 - income_tax_main_rates;
- regular_tax_before_credits = main rates + capital gains tax;
- income_tax_before_credits = regular + AMT;
- the AMT is line 9 less line 10;
- without net capital gain, the regular tax is line 46;
- the worksheet lines (dwks10, dwks13, line 18 <= 21 <= 14);
- the tax is monotone in wages, gains and dividends;
- results are independent of batch order.

The limit binds at the top of the 12 percent bracket in every year and
filing status, with the published band widths for 2018 to 2026.

The monotonicity check sets aside households with capital gain
distributions and a Schedule D net loss, a known defect it found. Gross
income adds non_sch_d_capital_gains outside the loss limit, while
net_capital_gain nets them (a 2018 single filer's tax falls $0.12 with $1
more gain). No dataset tax unit has both. It is left for a follow-up.

Also adds 2026 joint, separate and non-binding line 45/46 YAML cases,
from the same Rev. Proc. 2025-32 figures as the Axiom companion tests.

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

* Align the merged tests with main: #9831's reform variables and #9788's netting

- test_additional_tax_bracket_unset_bracket.py (#9831) listed
  regular_tax_before_credits among the variables the reform replaces. On
  this branch the reform replaces tax_on_taxable_income_at_main_rates
  instead, and regular_tax_before_credits is income_tax_main_rates plus
  capital_gains_tax. The default-system leak check now covers the three
  replaced variables, and the finite and monotone checks cover the reform's
  three taxes: income_tax_main_rates, tax_on_taxable_income_at_main_rates
  and regular_tax_before_credits.
- test_regular_tax_before_credits.py set aside households with capital gain
  distributions and a Schedule D net loss in the monotonicity property.
  #9788 nets the distributions inside the capital loss limit in AGI, so the
  property now holds for every household. The exclusion is gone, and the
  minimized counterexample (2018 single, $50,000 of distributions, a
  $40,273 short-term loss, $1 more long-term gain) is an explicit example;
  it failed before #9788 (the regular tax fell by $0.12) and passes now.

Both files pass after the merge: 9 and 6 tests.

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

* Say in the changelog that regular_tax_before_credits now includes the capital gains tax

PolicyEngine/salt-amt-calculator's API reads regular_tax_before_credits as
its regular tax, so the meaning change belongs in the release note.

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

* Point three Rev. Proc. 2025-32 links at the pages with their 2026 amounts

Three links pointed into the wrong section of Rev. Proc. 2025-32:

- the capital gains rate thresholds linked page 12, which is section 4.02
  (kiddie tax). Section 4.03 and its 2026 amounts (49,450, 98,900, 66,200,
  545,500, 613,700, 579,600 and 306,850) are on page 13;
- the child tax credit amount ($2,200) and refundable maximum ($1,700)
  linked page 13, which holds sections 4.03 and 4.04. Section 4.05 and its
  amounts are on page 14.

The other parameter links to the Rev. Proc. land on the page that has the
2026 amounts or on the page where their section starts.

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

* Test 28-percent rate gain with a Form 4952 election against the 2025 worksheets

- rate_gain_with_form_4952_election.yaml: the reproducer (no taxable income;
  taxable income below the gain; half and all of a collectibles gain elected;
  no election) and an election above the 24% bracket that leaves 28% on line
  9 only, with Schedule D Tax Worksheet lines.
- test_section_1h_regular_tax.py: for every household and year (2018 to 2030)
  with elections and both gains, the regular tax is at most line 46 figured
  from the rate parameters, exactly zero with no taxable income, and not
  negative; and electing more lowers it by at most the 12/15 rate overlap
  plus 3% of the smaller of the extra amount elected and the unrecaptured
  section 1250 gain.
  Pins the intended exception where an election moves gain from 28 to 25
  percent (section 1(h)(1)(E)(ii) measures its excess with net capital gain,
  which section 1(h)(2) reduces): $68,874 to $67,374 on $50,000 elected. A
  2025 differential compares Form 6251 Part III and line 11 after an election
  with nonzero Schedule D lines 18 and 19.
- test_form_6251_part_iii.py: the Schedule D Tax Worksheet transcription takes
  amounts, Form 4952 lines 4g and 4e, and the Foreign Earned Income Tax
  Worksheet footnote's four modifications.
- test_section_911_tax_stacking.py and test_form_4952_section_911_interaction.py:
  households with 28-percent rate and unrecaptured section 1250 gain are now
  compared with the forms (through the Schedule D Tax Worksheet and Form 6251
  Part III with the AMT capital gain excess), including rows that reach
  modification 4, and kept in the monotonicity properties, with the corrected
  election bound in place of #9800's overlap-only bound.
- Harris, Biden 2025 budget and additional tax bracket reforms: a case each
  where the line 47 limit binds.

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

* Tighten reach assertions and fix a stale docstring (hard-tier review)

- test_section_1h_regular_tax.py: the grid reaches 28% tax after an election
  only if some elected household has a positive Schedule D Tax Worksheet line
  42; a positive capital gains tax could be all at 25%.
- test_section_911_tax_stacking.py, test_form_4952_section_911_interaction.py:
  floor the unrecaptured section 1250 gain after modification 4 at zero, so a
  household whose line 9 the excess exhausts does not count as reaching it.
- test_form_6251_part_iii.py: the transcription, not the model, leaves out the
  short-term loss netting (#9841 added it to the model).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant