Skip to content

Use the married-filing-separately breakpoint on Form 6251 Part III line 18 - #9780

Merged
MaxGhenis merged 7 commits into
mainfrom
amt-part-iii-line-18-separate
Oct 5, 2026
Merged

MaxGhenis merged 7 commits into
mainfrom
amt-part-iii-line-18-separate

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

What was wrong

amt_tax_including_cg (Form 6251 Part III, line 38) computed line 18 as p.income.amt.brackets.calc(excess_income), so the 28% rate started at the full breakpoint ($239,100 in 2025) for every filing status. For a married individual filing a separate return it starts at half that.

  • 2025 Form 6251, page 2, line 18 (https://www.irs.gov/pub/irs-pdf/f6251.pdf): "If line 17 is $239,100 or less ($119,550 or less if married filing separately), multiply line 17 by 26% (0.26). Otherwise, multiply line 17 by 28% (0.28) and subtract $4,782 ($2,391 if married filing separately) from the result". Line 39 says the same of line 12.
  • 26 U.S.C. 55(b)(1)(C): "In the case of a married individual filing a separate return, subparagraph (A) shall be applied by substituting 50 percent of the dollar amount otherwise applicable under clause (i) and clause (ii) thereof."
  • 26 U.S.C. 55(b)(3)(A): the capped amount is "the amount determined under such first sentence computed at the rates and in the same manner as if this paragraph had not been enacted on the taxable excess reduced by" the gains. Doing it "in the same manner" brings in the (b)(1)(C) substitution.
  • 26 U.S.C. 55(d) sets the exemption amounts (the 50 percent separate-return amounts in (d)(1)(C) and (d)(2)(C)); this PR doesn't change them.

Line 39 (amt_lower_base_tax + amt_higher_base_tax) already used p.multiplier[filing_status]. alternative_minimum_tax takes min(amt_base_tax, amt_tax_including_cg) when Part III is required, so for a separate filer with qualified dividends or long-term gain and line 17 above the separate breakpoint, the AMT came out too low. The shortfall was 2% of the part of line 17 between the two breakpoints, up to $2,391 in 2025. Example: a 2025 separate filer with $104,000 of wages, $25,000 of qualified dividends and $900,000 of long-term gain has line 17 = $136,162.50; line 18 should be $35,734.50, but the model gave $35,402.25, which is $332.25 short.

Fix

filing_status = tax_unit("filing_status", period)
income_taxes_at_amt_rates = p.income.amt.brackets.calc(
    excess_income, factor=p.income.amt.multiplier[filing_status]
)

MarginalRateTaxScale.calc(tax_base, factor) multiplies the thresholds by factor. The default is 1.0, and every status except SEPARATE has a multiplier of exactly 1.0, so for them the computation is the same floating-point operations as before, bit for bit.

Invariants

These hold for every input:

  1. Line 18 and line 39 use the same schedule. With no qualified dividends or capital gain, line 17 equals line 12, so amt_tax_including_cg equals amt_base_tax (within float32 rounding) for every filing status and year.
  2. Part III never exceeds line 39 on the same line 12, beyond rounding. The gains are taxed at 25% or less, below the schedule's 26% floor, and line 17 + line 22 + line 36 ≤ line 12. This holds before and after the fix (the bug only lowered line 18), so it guards structure; it doesn't detect this bug.
  3. Non-separate filers are unchanged bit for bit. For every non-separate status, calc(x, factor=multiplier) is bitwise equal to calc(x).
  4. The size of the change for separate filers is exactly (28% − 26%) × clip(line 17 − T/2, 0, T/2), where T is the full breakpoint.

Tests

  • YAML (amt_tax_including_cg.yaml, base_tax/amt_base_tax.yaml, integration.yaml), with Form 6251 line-by-line comments:
    • 2025 separate filers above, between, exactly at, and $1 over the separate breakpoint, and above the full breakpoint;
    • a separate filer with unrecaptured section 1250 gain (lines 35–37);
    • a single filer with the same amounts, as a contrast;
    • 2026 with Rev. Proc. 2025-32 §3.10's $122,250;
    • a full-chain 2025 separate household: $150,000 wages, $25,000 qualified dividends and $900,000 long-term gain give AMT $26,767.50, where the old formula gave $25,285.25.
    • On upstream/main, 7 of these 10 fail. Three pass on both versions: the at-breakpoint case, the single-filer contrast, and the separate-filer amt_base_tax case (line 39 already used the separate breakpoint).
  • policyengine_us/tests/test_form_6251_part_iii.py (Hypothesis):
    • a transcription of 2025 Form 6251 lines 12–40, compared household by household with amt_tax_including_cg, amt_base_tax and alternative_minimum_tax for all five filing statuses (grid and random households). Line 13 comes from the household's own dividends and gains, not from the model's worksheet variables;
    • invariants 1 and 2 for random households, including 28-percent rate and unrecaptured section 1250 gain, in 2018, 2022, 2025, 2026 and 2030. Each household also runs with a no-gains twin, so invariant 1 covers every status;
    • invariants 3 and 4 on the AMT scale for every year from 2013 to 2035.
    • On upstream/main the differential tests fail. The property test fails too: Hypothesis shrank to a 2018 separate filer with line 12 $5 above the $95,550 breakpoint, where the old formula is $0.10 short.

The Hypothesis file has 5 tests. Three are property or differential tests over generated inputs; two are deterministic (the grid and the $136,162.50 household). All Hypothesis tests run with derandomize=True, so CI on an unrelated PR always draws the same examples. On the pre-fix formula, the three differential tests fail (checked at d9cb630). Before derandomizing, the properties test also caught the bug there, by shrinking to the 2018 example above. That case is now pinned as an @hypothesis.example, so the test still catches the bug with fixed draws; I re-checked that it fails on the pre-fix formula. The 911(f) file's Hypothesis tests are derandomized too, now that they compare separate filers.

Main already has hypothesis>=6.100.0 in the dev extras, so CI runs the new file. This PR no longer changes pyproject.toml or uv.lock.

Microsim

Default dataset (populace_us_2024.h5@populace-us-2024-spm-20260909), policyengine-core 3.32.12. Upstream/main 6b0e773 and this branch were each run in full, one at a time, under a 24 GB watchdog (peak footprint 14.5 GB). I compared 15 outputs per tax unit (Part I–III amounts, the worksheet inputs, regular tax, alternative_minimum_tax, income_tax_before_credits, income_tax, state_income_tax) plus household_net_income.

2025 2026
Tax units (separate filers) 79,729 (891) 79,729 (891)
Non-separate units: every output bitwise identical yes yes
Units whose amt_tax_including_cg changes 40, all separate 41, all separate
Each change = 2% × clip(line 17 − T/2, 0, T/2) all 40 all 41
Of those, Part III required 13 13
Units whose AMT / income tax changes 1 1
Weighted change in alternative_minimum_tax and income_tax +$21,928,438 +$22,583,816
Weighted change in household_net_income −$21,928,438 −$22,583,816
state_income_tax unchanged unchanged

For 12 of the 13 Part III units, the regular tax is above line 40 both before and after the fix, so their AMT stays $0. In the remaining unit, line 17 is above the full breakpoint, so its AMT rises by the maximum amount (2% of the separate breakpoint). That single record carries the whole aggregate, so read the dollar total as one sampled record, not a precise population estimate.

Section 911(f) carve-out removed

#9761 (merged as 94c4453) made test_section_911_tax_stacking.py skip the AMT comparison for separate filers with dividends or gains, because of this bug. This PR merges main and removes that skip and its docstring note (7c90a98), so separate filers are now compared with the transcribed Form 6251 too. Before main was merged, I checked this on a local merge of #9761's head e962c91: with the skip removed, test_grid_matches_the_2025_worksheets fails without this fix and passes with it.

After the merge, at 7c90a98:

  • 127/127 YAML tests in the AMT and FEIE folders pass;
  • test_form_6251_part_iii.py passes, 5/5;
  • test_section_911_tax_stacking.py passes, 10/10.

axiom: TheAxiomFoundation/rulespec-us#1487 queued (us/statutes/26/55 takes the 55(b)(3) amount as a completed input amt_tax_including_capital_gains, so line 18 is not encoded; its Part II breakpoint, amt_twenty_eight_percent_threshold, is already $122,250 for separate filers)

🤖 Generated with Claude Code

Line 18 applied the 26%/28% AMT scale to line 17 with the full breakpoint
($239,100 in 2025) for every filing status. 26 U.S.C. 55(b)(1)(C) halves
the dollar amount for a married individual filing a separate return, and
55(b)(3)(A) figures line 18 "at the rates and in the same manner" as
55(b)(1)(A); the 2025 Form 6251 line 18 reads "$239,100 or less ($119,550
or less if married filing separately) ... subtract $4,782 ($2,391 if
married filing separately)". Lines 7 and 39 (amt_lower_base_tax and
amt_higher_base_tax) already used the multiplier.

The scale is now applied with the filing-status multiplier as its
threshold factor. Filers other than married filing separately have a
multiplier of 1, which is the scale's default factor, so their line 18 is
unchanged bit for bit.

Tests: YAML cases with Form 6251 line comments (2025 and 2026, both sides
of the separate breakpoint, unrecaptured section 1250 gain, a full-chain
household), and Hypothesis property and differential tests against a
transcription of 2025 Form 6251 lines 12 to 40.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The new test file skips itself through pytest.importorskip when hypothesis
is missing, and main's dev extras did not include it. Same line as
#9761; locked with uv 0.12.13, the version CI
uses.

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

codecov Bot commented Oct 3, 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 (2cd124f).
⚠️ Report is 194 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #9780   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            4         1    -3     
  Lines           76        42   -34     
  Branches         2         0    -2     
=========================================
- Hits            76        42   -34     
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.

MaxGhenis and others added 5 commits October 3, 2026 22:05
Building the whole parameter tree at each of the 23 years made the test
take 195 s in CI; the gov.irs.income.amt subtree takes well under a second.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reuse the shared tax-benefit system instead of building a second one,
compare the bitwise line 18 check over the same array, derandomize the
Hypothesis tests so unrelated pull requests draw the same examples, and
ignore the .hypothesis/ example database.

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

# Conflicts:
#	policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/amt_tax_including_cg.yaml
#	policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/base_tax/amt_base_tax.yaml
The worksheet comparison skipped the AMT of married-filing-separately
filers with dividends or gains because Form 6251 line 18 used the full
breakpoint. With line 18 fixed, those filers match the transcribed Form
6251 too, so the skip and its docstring note go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Derandomizing the Part III tests meant the properties test no longer drew
the 2018 separate filer $5 above the $95,550 breakpoint that caught the
full-breakpoint line 18. Pin it as an explicit example (it fails on the
pre-fix formula). The 911(f) worksheet tests now compare separate filers'
AMT too, so give them fixed examples as well.

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

Copy link
Copy Markdown
Contributor Author

Hand-off to the US + core hub. Head 2cd124f323bf7e87377c47c35c6f94e62e81ec18.

Reviews (all independent Subfleet runs, all APPROVE, no blocking findings; files in ~/reviews/pe-us-amt-line18-mfs-2026-10-03/):

  • r1 and r2, Opus, at 94da0df: the full change (law, YAML values recomputed by hand, Hypothesis file, CI collection). review-r1.md, review-r2.md.
  • r3, Opus, delta at 7c90a98: the merge of main after Tax Form 2555 filers' income at stacked rates (26 U.S.C. 911(f)) #9761, the YAML conflict resolution, and removal of the 911(f) carve-out. review-r3-delta.md.
  • r4, Sonnet, delta at 2cd124f: the pinned Hypothesis counterexample and derandomizing the 911(f) tests. review-r4-delta.md.

Passed locally at the current head:

  • 127/127 YAML tests in the AMT and FEIE folders.
  • test_form_6251_part_iii.py 5/5 and test_section_911_tax_stacking.py 10/10.
  • The pinned example fails on the pre-fix formula, as it should.

Pending: CI on 2cd124f (run 37334027809, queued). The earlier "Full Suite - Rest" failure predates main's #9782/#9787 fixes.

Depends on: nothing. #9761 is merged and its carve-out is removed here. #9781 (Form 4952 election, Schedule D lines 3–10) also touches the dwks/Part III inputs, so whichever lands second may need a re-merge.

Hold / decision: none. This is a bug fix to the law, and its aggregate impact (+$21.9M in 2025, +$22.6M in 2026, from one record) is stated in the PR body.

Follow-ups owed (not merge conditions):

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Landed by the US + core hub, after session 37df6349 handed it off, at head 2cd124f323bf7e87377c47c35c6f94e62e81ec18. Squash merge, --admin --match-head-commit; the only block was the required approving review, which no one else can give on Max's PRs.

Gates:

  • Reviews: independent Opus reviews approved r1 and r2 (94da0df279, the full change) and r3 (7c90a984d5, the merge of main after Tax Form 2555 filers' income at stacked rates (26 U.S.C. 911(f)) #9761 and removal of the 911(f) carve-out). r4 (Sonnet) approved the last delta to 2cd124f323. The hub confirmed that delta is test-only: +18 lines in test_form_6251_part_iii.py and +2 in test_section_911_tax_stacking.py.
  • Combined with main: main took Apply the Form 4952 election in Schedule D Tax Worksheet lines 3-10 #9781 (Form 4952, Schedule D lines 3–10) after this PR's CI started. The hub ran the affected tests on the exact tree GitHub's merge produces, main 18cc357b86 plus this PR:
    • the alternative_minimum_tax, capital_gains and foreign_earned_income_exclusion YAML folders: 159 passed;
    • test_form_6251_part_iii.py, test_section_911_tax_stacking.py and test_form_4952_election_worksheet.py: 19 passed, Hypothesis included.
  • CI: gh pr checks exits 0, and every check run on 2cd124f323 succeeded.
  • State: MERGEABLE against current main, with no file overlap with main's changes since its base. Not a draft; no CHANGES_REQUESTED review.
  • Impact: real microsimulation in the body, +$21.9M (2025) and +$22.6M (2026), from one record. This is a correction to the law (55(b)(1)(C) breakpoint for separate filers).

#9792 is stacked on this PR; the hub merges main into it next. Follow-ups noted in the hand-off: rulespec-us#1487, and chip task_f9763819 (line 27, which #9792 addresses).

MaxGhenis added a commit that referenced this pull request Oct 5, 2026
…nded

#9780 (which this branch was stacked on) squash-merged, so this merge takes main
and re-applies only this PR's own changes (2cd124f..eb4e9c0). Test-file
conflicts with the squashed #9780 are resolved by taking main plus this PR's
diff, which applies with identical +/- lines.

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