Skip to content

Apply the 2026 section 68 reduction in Maryland, Maine and New Mexico itemized deductions - #10594

Merged
MaxGhenis merged 5 commits into
mainfrom
claude/section-68-2026-md-me-nm-atit45
Nov 3, 2026
Merged

MaxGhenis merged 5 commits into
mainfrom
claude/section-68-2026-md-me-nm-atit45

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Applies the 2026 federal section 68 reduction (P.L. 119-21, sec. 70111) in Maryland, Maine and New Mexico itemized deductions. Each state starts from federal itemized deductions, but on main all three read the total from before the section 68 reduction, so they overstate deductions for high-income itemizers from 2026. Each fix applies only while gov.irs.deductions.itemized.limitation.obbb.applies is true (from 2026), so earlier years are unchanged. That includes the pre-2018 Pease years, which are out of scope here.

Maryland (us-md-itemized-section-68-2026)

Source. Md. Code, Tax-General § 10-218(b)(1), (b)(3) and (c). (b) allows "the sum of the individual's federal itemized deductions", (b)(1) "limited and reduced as required under the Internal Revenue Code", and (b)(3) "further reduced by the amount claimed as taxes on income paid to a state or political subdivision of a state, after subtracting a pro rata portion of the reduction to itemized deductions required under § 68 of the Internal Revenue Code." (Text from mgaleg.maryland.gov, read 2026-11-02.)

What changed. md_itemized_deductions.py:32-43. From 2026 this PR scales the Maryland base (non-SALT deductions plus capped property taxes) by 1 - R/T, where T is total_itemized_taxable_income_deductions and R is itemized_taxable_income_deductions_reduction. The Maryland phaseout and zero floor follow as before. Taking the income-tax component as I = T - base, the spec's formula T - R - I + R·I/T equals base·(1 - R/T) for every input, negative I included, and gives exactly base at R = 0. The two forms differ only where a zero floor on I binds. T - base equals the income taxes inside the federal SALT deduction only when capped_property_taxes equals the property taxes inside that deduction. Where the phased-down SALT cap binds, Maryland still counts property taxes up to the statutory cap (an existing gap; see follow-ups below), and this PR scales that amount by (1 - R/T) too. No Maryland parameter changed.

Witness and years. 2026, single, $1M AGI, $100,000 of deductions, R = $5,405.41. Main gives $40,000; this PR gives $34,594.59. The spec's SALT-restoration case also gives $34,594.59.

Invariants.

  • With T > 0: md = max(base·(T-R)/T - phaseout, 0).
  • With R > 0 and base > 0, the result is strictly below main's before the phaseout.
  • R = 0 gives exactly main's result.

Maine (us-me-itemized-section-68-2026), confidence medium

Source. 36 M.R.S. § 5125(1), (3), (4), (7) and § 111(1-A). (1) lets "an individual who has claimed itemized deductions from federal adjusted gross income in determining the individual's federal taxable income" claim Maine itemized deductions. (3)(A) reduces the federal sum by "any amount attributable to income taxes or sales and use taxes", and (3)(A-1) increases it by "the amount of property taxes not claimed under the Code, Section 164(a)(1) and (2) as a result of the limitation under the Code, Section 164(b)(6)(B)". § 111(1-A) defines the Code as the IRC "and amendments to that Code as of December 31, 2025", so it includes the section 68 that P.L. 119-21 rewrote. (Text from legislature.maine.gov, read 2026-11-02.)

What changed. me_itemized_deductions_pre_phaseout.py:33-60. From 2026 this PR scales the non-SALT deductions and the medical carve-out by q = F/T, where F is itemized_taxable_income_deductions. Property taxes inside the federal SALT deduction keep their section 68 reduction, while property taxes the SALT cap excluded come back in full under § 5125(3)(A-1). The cap, the medical exception and the later phaseout are unchanged.

Allocation (hub ruling us-me:itemized-section-68-property-tax-allocation-2026). The hub kept the allocation as built: pro rata throughout.

  • Income taxes and medical. The statute doesn't say how a cut to the federal total attaches to "any amount attributable to income taxes" or to medical expenses "included in" it. Maine Revenue Services answered that question once before: its 2016 and 2017 Schedule 2 line 2a worksheets reduced the income-tax subtraction by its pro rata share of the federal Pease reduction. Pease spared medical expenses, but the 2026 section 68 cut reaches every itemized deduction, so the medical carve-out takes its share too.
  • Property taxes. Those inside the federal SALT deduction keep their section 68 reduction, because (3)(A-1) restores only property taxes lost "as a result of" the SALT cap. Those the cap excluded come back in full: section 68(b) applies the cut after the cap, so it never reached them. Maine's 2018–2025 Schedule 2 subtracts all of Schedule A line 5e and adds back lines 5b and 5c in full. That matches the statute only while no section 68 cut applies, so it doesn't decide 2026.
  • Split within SALT. Section 164(b)(6) caps "the aggregate amount of taxes", and neither Maine's text nor its forms order the taxes inside the cap. The PR splits the capped deduction pro rata (salt_deduction · real_estate_taxes / salt). Every filer with a section 68 cut has income above the 37% bracket start, which puts the SALT cap at its $10,000 floor, so this choice moves at most 2/37 × $10,000 = $540.54 of Maine deduction.
  • Reach. Maine's own phase-out removes the whole deduction once Maine AGI reaches $177,250 (single), $265,900 (head of household) or $354,550 (joint) in 2026. So the Maine change moves tax only where Maine AGI sits hundreds of thousands of dollars below federal AGI. The hub will recheck the allocation against Maine's 2026 Schedule 2 when it comes out.

The no-SALT, no-medical witnesses establish the bug independently of these choices.

Witness and years. 2026, $20,000 of non-SALT deductions, R = $1,081.08. Main gives $20,000; this PR gives $18,918.92 (also in me_deductions).

Invariants.

  • With no SALT and no medical deductions: pre_phaseout = min(F, cap).
  • With R = 0 the result equals main's.

New Mexico (us-nm-itemized-raw-total-2026)

Source. NMSA 1978 § 7-2-2(N)(2), Laws 2023, ch. 159, § 1. The excerpt given in the hub spec (not fetched in this session) reads: "Internal Revenue Code, as that section may be amended or renumbered, allowed the taxpayer for the taxpayer's taxable year". The 2025 PIT-1 instructions, line 12, ask for the "allowable federal itemized deductions" (spec excerpt).

What changed.

  • nm_itemized_deductions.py:18-23: the variable now has a formula, which returns itemized_taxable_income_deductions from 2026 and the previous total before.
  • nm_salt_add_back.py:44-49: from 2026, the standard-deduction safeguard compares the allowed total. Main reconstructed the total there instead.
  • Because the safeguard now reads the allowed federal total, it also uses the phased-down federal SALT cap (salt_cap) instead of the statutory cap. This lowers the addback, and NM tax, for 2026+ itemizers whose SALT cap phases down, even when the section 68 reduction is zero (example: single, $550,000 wages, $10,000 property tax, $20,000 income tax: addback $13,900 on main, $10,800 here). The addback numerator still uses the statutory cap (follow-up below).
  • The new section 68 limitation is not allocated to the SALT addback numerator, as the spec directs.

Witness and years. 2026 onward, single, $2M of income, $37,000 of deductions. Main gives $37,000; this PR gives $35,000. A SALT addback case in which the allowed total exceeds the standard deduction by $81.08: main gives $1,000, this PR gives $81.08.

Invariants.

  • nm_itemized_deductions = T - R.
  • 0 ≤ nm_salt_add_back ≤ max(T - R - standard deduction, 0).

Tests

  • The failing YAML tests came first (0750f6d), then the fixes.
  • policyengine_us/tests/test_state_itemized_section_68_2026.py is a Hypothesis property test. It checks the Maryland, Maine and New Mexico identities above with randomly drawn totals and strictly positive reductions, in one simulation per example. It cannot pass trivially: each identity fails on main whenever R > 0, and each reverted fix fails it.
  • Local cost: 57 s and 914 MB peak RSS including model import. These are local measurements on the cloud VM, not CI. The test runs in the Rest "remaining" Python group.

Follow-ups (not in this PR)

  • Statutory vs phased-down SALT cap. Maryland's capped_property_taxes and New Mexico's nm_salt_add_back numerator both cap SALT at the statutory amount rather than the phased-down salt_cap. Both behave the same on main.
  • New Mexico's addback after the section 68 cut. NMSA 7-2-2(N)(2) subtracts "the amount of state and local income and sales taxes included in the taxpayer's itemized deductions". This PR leaves the cut out of that numerator, as the spec directed.
  • The 2013–2017 Pease years. Maryland's (b)(3) and Maine's 2016–2017 Schedule 2 both apply the Pease reduction, and all three states start from the federal allowed total, which then carried it. This PR leaves those years as on main.

Extra files

  • The property test file above.
  • changelog.d/section-68-2026-md-me-nm.fixed.md.

impact: we estimate that Maryland income tax rises $12.5 million in 2026 on 9 records of the default dataset (14,415 weighted tax units). Those households also pay $6.1 million more county income tax. New Mexico income tax rises $80,101 on net: it rises on 2 records and falls on 6 whose SALT addback falls. Household net income falls $18.7 million in all. Maine's itemized deduction moves on 5 records with a combined weight of 0.03, and no Maine tax moves. Two records carry all but $41 of the Maryland change, so these totals carry wide sampling error. Federal income tax and benefits don't move.
axiom: TheAxiomFoundation/rulespec-us#1999 queued (Maryland); TheAxiomFoundation/rulespec-us#2000 queued (Maine); TheAxiomFoundation/rulespec-us#2001 queued (New Mexico)

🤖 Generated with Claude Code

Each state starts from federal itemized deductions, which from 2026 the
P.L. 119-21 section 68 reduction limits; main uses the pre-reduction total.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6JzbnbD1RtbxQx1ds6
Each state starts from federal itemized deductions allowed after section 68
(Md. Tax-General 10-218(b), 36 M.R.S. 5125(1), NMSA 7-2-2(N)(2)), but main
used the pre-reduction total, overstating deductions for high-income
itemizers from 2026.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6JzbnbD1RtbxQx1ds6
YAML cases fix single points; this checks the MD, ME and NM identities
over random totals and positive reductions in one simulation per example.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6JzbnbD1RtbxQx1ds6
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: 6409fc53feab366a21f5f04ba8c981291091d76a (draft; CI skipped because the PR is a draft: the latest Pull request run at this head is completed / skipped).

Items

us-md-itemized-section-68-2026: FIXED.

  • Reproduced on origin/main (0ea8894): 3 of the 5 new 2026 cases failed. The 2 control cases, R = 0 and T = 0, passed.
  • Fix at md_itemized_deductions.py:32-43: base · (1 - R/T), then the Maryland phaseout.
  • Tests at md_itemized_deductions.yaml:133-220.

us-me-itemized-section-68-2026: FIXED, confidence medium.

  • Reproduced: all 4 new cases failed on main.
  • Fix at me_itemized_deductions_pre_phaseout.py:33-60.
  • Tests at me_itemized_deductions_pre_phaseout.yaml:106-190. The spec's two cases also assert me_deductions.

us-nm-itemized-raw-total-2026: FIXED.

  • Reproduced: the $2M case in nm_itemized_deductions.yaml failed, and the new addback case in nm_salt_add_back.yaml failed. The R = 0 control passed.
  • Fixes at nm_itemized_deductions.py:18-23 and nm_salt_add_back.py:44-49.
  • Tests at nm_itemized_deductions.yaml:49-87 and nm_salt_add_back.yaml:66-83.

Tests run (one file per process, after the fix)

File Result
md_itemized_deductions.yaml 14 passed
integration_md.yaml (covers the reader md_deductions, which has no test file of its own) 7 passed
me_itemized_deductions_pre_phaseout.yaml 9 passed
me_deductions.yaml (reader) 4 passed
nm_itemized_deductions.yaml 5 passed
nm_salt_add_back.yaml 6 passed
nm_deductions.yaml (reader) 3 passed
nm_taxable_income.yaml (reader of nm_salt_add_back) 3 passed
state_itemized_deductions.yaml (reader, via the parameter list) 17 passed
test_state_itemized_section_68_2026.py (Hypothesis, 15 examples of 1-6 units per state) 1 passed: 57 s wall including import, 914 MB peak RSS; local VM, not CI

The readers come from a grep over .py and .yaml files:

  • md_itemized_deductions: md_deductions, state_itemized_deductions.yaml
  • me_itemized_deductions_pre_phaseout: me_deductions, the same list
  • nm_itemized_deductions: nm_deductions, the same list
  • nm_salt_add_back: nm_taxable_income

No full suites and no microsimulations were run.

Hand computations

  • MD: 100,000 - 5,405.41 - 60,000 = 34,594.59. Restoration case: 110,000 - 5,945.95 - 10,000 + 540.54 - 60,000 = 34,594.59. Property case: 49,000 · (1 - 3,700/74,000) = 46,550, less the 15,000 phaseout = 31,550.
  • ME: 20,000 · 35/37 = 18,918.92, and 30,000 · 35/37 = 28,378.38. Component case: (40,000 - 10,000 + 4,000) · 35/37 + 10,000 · 35/37 = 41,621.62. Excluded-property case: 17,000 · 26/27 + 10,000 · 26/27 + 2,000 = 28,000.
  • NM: 37,000 - 2,000 = 35,000. Addback: 17,000 - 918.92 - 16,000 = 81.08.
  • The federal rate parameter is 0.05405405, not exactly 2/37. The resulting error is under $0.001 at these amounts, inside the 0.01 margin.

Mutants

Each mutant restores one file from origin/main, runs in its own process, and is reverted afterwards; git diff was clean after each.

Mutant YAML Property test
MD 3 failed failed
ME 4 failed failed
NM itemized 1 failed failed
NM addback 1 failed failed

Judgment calls (please review)

  1. MD formula form. I implemented base · (1 - R/T) rather than the literal T - R - I + R·I/T with I = T - base. They agree whenever I ≥ 0. The literal form, with I clamped at 0, would also change R = 0 results when capped property taxes exceed the phased-down federal SALT deduction. That is a pre-existing SALT-cap mismatch outside this item.
  2. Gate. Each fix is gated on the federal limitation.obbb.applies parameter, which turns on in 2026. Pre-2018 Pease years are deliberately untouched, as the NM spec says the historical Pease issue is not classified here.
  3. ME allocation. On top of the backlog's deduced q = F/T allocation, I split property taxes inside the federal SALT deduction pro rata within SALT. That part keeps its section 68 reduction, and the part the cap excluded is added back in full, following the spec's line "do not restore property deductions lost because of section68". This split is my own deduction and is unverified against any 2026 Maine form.
  4. Axiom line. I wrote axiom: needed. The item asked for axiom: pending: hub files parity, but the brief's general rules forbid that line. Please set the line you want.
  5. Branch name. The PR is on claude/section-68-2026-md-me-nm-atit45, the branch this session was configured to push to, not section-68-2026-md-me-nm.
  6. Sources. No source was fetched in this session. Every quote in the PR body comes from the spec excerpt fields. The statutory and agency text behind the fixes is unverified here.

Partner tests

Nothing under tests/policy/baseline/partners/ changed. No partner test references MD, ME or NM (grep for the state codes and names returned nothing).

Proposed follow-ups (not opened)

  • Use the phased-down SALT cap in MD capped_property_taxes and NM nm_salt_add_back. Both read the statutory cap parameter rather than salt_cap, which the 2025+ MAGI phase-down lowers. High-income filers can therefore keep more property tax or SALT than federally deducted.
  • Decide the pre-2018 Pease treatment in MD, ME and NM. Each statute starts from federally allowed deductions, which would include the old section 68 reduction for 2013-2017. This PR leaves those years as they were.

Generated by Claude Code

@MaxGhenis
MaxGhenis marked this pull request as ready for review November 2, 2026 15:57
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review
VERDICT: APPROVE
6409fc5
METHODOLOGY_CHOICE: yes

The head has not moved past 6409fc53fe. The PR is no longer a draft. The hand-off says CI was skipped, but that is out of date: Pull request run 41476763190 at this head finished success, with all 34 jobs success and none cancelled or skipped. The PR merges cleanly into current main (e736b11), and no file it touches has changed on main since its base (0ea8894).

I found no wrong result in the code. The two P2 findings below are inaccurate or missing statements in the PR body. They should be fixed before landing, because the hub's impact reading depends on them. Maine's property-tax allocation is a methodology choice the hub should rule on (P3-1).

Findings

P2-1. NM: the safeguard change moves 2026 results even when R = 0, and the body doesn't say so

policyengine_us/variables/gov/states/nm/tax/income/deductions/nm_salt_add_back.py:44-49. On main, the safeguard rebuilt the federal total as itemized_deductions_less_salt + min(statutory cap, salt). From 2026 it now reads itemized_taxable_income_deductions, which carries the phased-down salt_cap from the 2025+ MAGI phase-down. So the addback changes for any 2026 NM itemizer whose SALT cap phases down, even when the section 68 reduction is zero. The body describes this only as "compares the allowed total". The hand-off lists "NM nm_salt_add_back reads the statutory cap" as an untouched follow-up, but this PR already fixes the safeguard half of it.

Witness: full chain, 2026, NM, single, wages $550,000, real estate taxes $10,000, state income tax $20,000, no other deductions. I ran the same script against main (via PYTHONPATH on a worktree) and against this head:

main PR
itemized_taxable_income_deductions_reduction 0 0
salt_cap / salt_deduction 26,900 26,900
nm_salt_add_back 13,900 10,800
nm_income_tax 29,041.00 28,858.10
household_net_income 346,540.75 346,723.66

The PR's value is the right direction: $26,900 allowed − $16,100 standard deduction = $10,800. Main used $30,000 − $16,100 = $13,900. Still, this moves baseline in the opposite direction from the section 68 fix (it lowers tax), and it reaches filers below the 37% bracket. Add to the NM "What changed" list:

  • Because the safeguard now reads the allowed federal total, it also uses the phased-down federal SALT cap (salt_cap) instead of the statutory cap. This lowers the addback, and NM tax, for 2026+ itemizers whose SALT cap phases down, even when the section 68 reduction is zero (example: single, $550,000 wages, $10,000 property tax, $20,000 income tax: addback $13,900 on main, $10,800 here). The addback numerator still uses the statutory cap (follow-up below).

P2-2. MD: the body's account of judgment call 1 is wrong as written

Body, Maryland "What changed": "when capped property taxes exceed the federal SALT deduction (so T - base < 0), the spec's literal formula would also change results with R = 0." With I = T − base, T − R − I + R·I/T = (T − I)(1 − R/T) = base·(1 − R/T) for every input, negative I included. At R = 0 it gives exactly base. The two forms differ only if I is clamped at zero. The hand-off mentions the clamp but the body doesn't. There is no witness where the unclamped literal formula and the code differ. Check on the full-chain case from my run (2026, MD, single, wages $1M, charity $100k, property tax $30k): T = 105,000, R = 5,675.68, base = 125,000, so I = −20,000. Then T − R − I + R·I/T = 105,000 − 5,675.68 − (−20,000) + 5,675.68 × (−20,000)/105,000 = 105,000 − 5,675.68 + 20,000 − 1,081.08 = 118,243.24, which is 125,000 × (1 − 5,675.68/105,000) = 118,243.25 to the cent. The code returns 58,243.25 after the $60,000 phaseout.

The actual gap is that T − base equals the income taxes inside the federal SALT deduction only when capped_property_taxes equals the property taxes inside that deduction. That fails whenever the phased-down salt_cap binds (same run: salt_deduction 10,000, capped_property_taxes 30,000). Replace the sentence with:

The two forms are algebraically identical when I = T − base. They differ only if I is floored at zero. T − base is the income-tax share of the federal SALT deduction only when capped_property_taxes equals the property taxes inside that deduction. Where the phased-down SALT cap binds, Maryland still counts property taxes up to the statutory cap (a pre-existing follow-up), and this PR scales those by (1 − R/T) too.

P3-1. ME: the property-tax split is one reading among several (methodology choice)

me_itemized_deductions_pre_phaseout.py:46-60 treats the property taxes inside the federal SALT deduction as salt_deduction × real_estate_taxes / salt (pro rata within SALT). That share keeps its section 68 reduction, and the rest is added back in full. I couldn't reach 36 M.R.S. § 5125 or any 2026 Maine guidance (see below), so I can't confirm this. Two other readings look defensible to me:

  • (a) ordering instead of pro rata: income taxes fill the cap first, so the cap excludes property taxes first;
  • (b) restoring the section 68 share on deducted property taxes as well, as Maryland's (b)(3) does for income taxes. The spec rejects this ("do not restore property deductions lost because of section68"), but no fetched source settles it.

Mutant A below shows the YAML pins the chosen reading. Suggested register entry for the hub to rule on: us-me:itemized-section-68-property-tax-allocation-2026, covering how § 5125(3)(A-1) "excluded" property taxes and the section 68 reduction split among SALT components. The medical carve-out at q = F/T (line 45) is the backlog's own deduction, flagged in the body, and also unverified.

P3-2. No test pins the gate in the 2013-2017 Pease years

Mutant B: changing md_itemized_deductions.py:32 from limitation.obbb.applies to limitation.applies turns the scaling on in 2013-2017. It survives md_itemized_deductions.yaml (14 passed) and integration_md.yaml (7 passed). The gate itself is right (see "Checked and sound"), but nothing protects it. Optional: add one 2017 case per state with a positive Pease reduction that expects main's value, for example in md_itemized_deductions.yaml:

- name: Maryland ignores the pre-2018 section 68 reduction (out of scope here).
  period: 2017
  input:
    state_code: MD
    filing_status: SINGLE
    adjusted_gross_income: 150_000
    total_itemized_taxable_income_deductions: 40_000
    itemized_deductions_less_salt: 30_000
    itemized_taxable_income_deductions_reduction: 1_000
    capped_property_taxes: 4_000
  output:
    md_itemized_deductions: 34_000

P3-3. The axiom: line needs the hub's choice

The body ends axiom: needed. CLAUDE.md reserves that for external contributors. The hub should set its own line. The impact: pending (hub) line is present.

Verification

Defect at main. I copied the PR's four YAML files and ran them against current main (e736b11), one file per process: MD 3 failed / 11 passed, ME 4 failed / 5 passed, NM itemized 1 failed / 4 passed, NM addback 1 failed / 5 passed. That matches the hand-off. The full-chain witness (2026, single, $1M wages, $100k charity) gives:

  • MD: md_itemized_deductions 35,000 → 29,864.87, md_income_tax 56,197.50 → 56,518.45;
  • NM: nm_itemized_deductions 95,000 → 89,864.87, nm_income_tax 50,753.00 → 51,055.97;
  • ME: unchanged, because Maine's phaseout zeroes me_deductions at that income.

Hand values, computed before reading the producing code (rate 2/37):

  • MD 1: 100,000 × 35/37 = 94,594.59, less (1,000,000 − 200,000) × 7.5% = 60,000, gives 34,594.59. MD 2: R = 110,000 × 2/37 = 5,945.95, restoration 5,945.95 × 10,000/110,000 = 540.54, so 110,000 − 5,945.95 − 10,000 + 540.54 − 60,000 = 34,594.59. MD property case: 49,000 × (1 − 3,700/74,000) = 46,550, less 15,000, gives 31,550. All three match.
  • ME: 20,000 × 35/37 = 18,918.92 and 30,000 × 35/37 = 28,378.38. Component case: 37,837.84 − 9,459.46 + (4,000 − 4,000 × 2/37 = 3,783.78) = 32,162.16, below the 2026 cap of 37,100 (from the run), plus 9,459.46 = 41,621.62. Excluded-property case: 17,000 × 26/27 + (12,000 − 10,000/27) = 16,370.37 + 11,629.63 = 28,000. All match.
  • NM: taxable income 2,000,000 is above the $640,600 threshold by more than 37,000, so R = 37,000 × 0.05405405 = 1,999.9999, leaving 35,000.00. Addback case: 17,000 × 2/37 = 918.92; 16,081.08 − 16,000 = 81.08, which is below the $10,000 income-tax SALT, so the addback is 81.08. Both match.

Mutants. Each ran in its own process, with the file restored afterwards and git status clean after each.

Mutant YAML Property test
MD file reverted to base 3 failed (md_itemized_deductions.yaml) failed
ME file reverted 4 failed (me_itemized_deductions_pre_phaseout.yaml) failed (assert_allclose mismatch)
NM itemized reverted 1 failed (nm_itemized_deductions.yaml) failed
NM addback reverted 1 failed (nm_salt_add_back.yaml) failed
A (mine): ME property term (1 - allowed_share) → 0 2 failed (the component and excluded-property cases) passed (it has no property taxes)
B (mine): MD gate → limitation.applies survived (P3-2) not run

Checked and sound

  • Federal rule at this head. itemized_taxable_income_deductions_reduction.py computes R = obbb.rate × min(T, taxable_income_before_itemized_deductions − 37% bracket threshold) when limitation.applies and obbb.applies. T is total_itemized_taxable_income_deductions, the sum of gov.irs.deductions.itemized_deductions, which includes the phased-down salt_deduction. itemized_taxable_income_deductions = max(0, T − R). The rate is 0.05405405, not exactly 2/37 (pre-existing; under $0.001 at these amounts). So in the hand-off, T is the pre-section-68 federal total and R the 2026 reduction. On whether each state starts from deductions allowed after § 68, I checked only the spec excerpts, not the statutes (see below).
  • Gate. limitation/obbb/applies.yaml is false from 2009 and true from 2026-01-01. The full-chain run (MD, ME, NM; single, $1M wages, $100k charity, $30k property tax) gives byte-identical outputs on main and this head for 2015, 2017 and 2025. The run shows positive federal Pease reductions in 2015 and 2017 that the states still ignore. 2026 and 2027 move as expected.
  • Equivalence (judgment call 1). Shown algebraically and numerically in P2-2.
  • Tests at this head, one file per process: md_itemized_deductions 14 passed; me_itemized_deductions_pre_phaseout 9; nm_itemized_deductions 5; nm_salt_add_back 6; readers integration_md 7, me_deductions 4, nm_deductions 3, nm_taxable_income 3, state_itemized_deductions 17. The property test passed: 9.6 s of test time, 44 s wall including environment start and model import, 916 MB peak child RSS. These are local VM numbers, not CI. hypothesis comes from the dev extra: a plain uv sync fails to collect the file, but CI and uv sync --extra dev are fine.
  • Can the property pass trivially? No: the four reverted-file mutants all fail it. It draws R ≥ 1 and T ≥ 1,000, so it never reaches R = 0 or T = 0. The YAML covers those (the MD R = 0 and T = 0 cases, the NM R = 0 case). The MD check restates the implemented form base·q, but its input I equals T − base exactly, so it equals the statutory form here. MD AGI is fixed at $100,000, so the property never reaches Maryland's phaseout.
  • New tests vs claims. The MD, ME and NM cases assert the exact claimed values to $0.01, not weaker bounds. The ME component and excluded-property cases pin the allocation reading (mutant A).
  • Readers. md_itemized_deductions → md_deductions → md_taxable_income → MD income tax. me_itemized_deductions_pre_phaseout → me_deductions → me_taxable_income. nm_itemized_deductions → nm_deductions → nm_taxable_income, and nm_salt_add_back → nm_taxable_income → nm_income_tax_before_non_refundable_credits. All three are also in the state_itemized_deductions parameter list. Each chain reaches state income tax and household_net_income, both of which move in the witnesses above. At baseline:
    • MD moves for 2026+ itemizers with R > 0;
    • ME moves for 2026+ itemizers with R > 0 when the cap or phaseout doesn't absorb the change;
    • NM moves for those with R > 0, and through P2-1 for those whose SALT cap phases down.
  • Follow-ups. Both claims hold, and both are identical on main:
    • MD capped_property_taxes reads the statutory cap: 2026, MD, $1M wages, $30k property tax gives salt_deduction 10,000 but capped_property_taxes 30,000. This PR doesn't make it worse: it scales that overstated base down by (1 − R/T), so md_itemized_deductions goes from 65,000 on main to 58,243.25.
    • NM numerator: 2026, NM, wages $550,000, charity $50,000, property tax $10,000, income tax $20,000 gives salt_deduction 26,900, nm_salt_add_back 20,000 (= 30,000 × 20,000/30,000). The pro rata income-tax share of the deducted 26,900 would be 17,933.33. This PR leaves the numerator line unchanged.
    • A third follow-up: the 2013-2017 Pease non-application, confirmed by the 2015/2017 runs above.
  • Body, otherwise. These match the code: the line ranges cited, the MD and ME "R = 0 gives main's result" invariants (the MD R = 0 YAML case gives 34,000 as on main), the NM bounds, failing tests first (0750f6d adds only the four YAML files), and the extra files listed.
  • References. Variable references follow the repo convention: one entry per source, a # PDF pages 94-95 comment above the href, and no page list in the href.
  • Scope. The files list touches nothing under .github/ or policyengine_us/tests/policy/baseline/partners/, and the Household API Partners job passed. An open-PR search for each of the four changed variable names returns only Apply the 2026 section 68 reduction in Maryland, Maine and New Mexico itemized deductions #10594.
  • Population. Beyond the common set, the impact pair needs md_itemized_deductions, md_deductions, me_itemized_deductions_pre_phaseout, me_deductions, nm_itemized_deductions, nm_salt_add_back, nm_deductions, itemized_taxable_income_deductions_reduction and salt_cap, plus each state's income tax. Expected 2026 direction:
    • MD: total income tax rises;
    • ME: rises slightly, possibly near zero, because Maine's phaseout removes deductions at most incomes where R > 0;
    • NM: section 68 raises tax for R > 0 filers, and P2-1 lowers it for phased-cap filers. The net is most likely an increase, but the hub should report both pieces.

What I couldn't check

  • Every controlling text. The VM's egress proxy refused uscode.house.gov, govinfo.gov, mgaleg.maryland.gov, legislature.maine.gov and nmlegis.gov (CONNECT 403 / DNS refusal). I could fetch none of 26 U.S.C. 68 as amended by P.L. 119-21 § 70111, Md. Code Tax-General § 10-218, 36 M.R.S. § 5125, or NMSA § 7-2-2 and the NM addback provision. A web search returned only secondary summaries, which I don't rely on. So I could not confirm:

    • whether amended § 68 has any SALT-specific or component rule that would change R's base;
    • the wording of MD (b)(3) and its restriction to income taxes (which bears on sales-tax electors, a pre-existing issue);
    • the text of ME § 5125(3)(A-1);
    • whether each state statute starts from deductions allowed after § 68.

    The PR's quotes all come from the hub's spec excerpts, and I treat them as unverified too.

  • Population impact: no microsimulation was run, per the brief.

  • The CI timing and memory reports for the Python shard that now holds the property test.


Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Hub impact pair, progress

The base side of the impact pair for this PR finished. The head side runs next, and the result comment follows it.

  • base 0ea88946c6584d59057d1dac23bdd8a85cb6ea6a
  • Year 2026, 8 household chunks, Microsimulation.default_dataset.
  • Wall time: 10:02.81 for the whole process (h:mm:ss), 570.7 s inside calculate_chunked.
  • Maximum resident set size: 3,802,932 kB (3.63 GiB). Exit status 0.
Variable Weighted sum
md_itemized_deductions 23,633,476,785.28
md_deductions 30,732,223,727.41
me_itemized_deductions_pre_phaseout 4,551,828,204.95
me_deductions 16,787,410,277.65
nm_itemized_deductions 8,805,151,058.54
nm_salt_add_back 660,548,696.79
nm_deductions 29,301,203,896.83
itemized_taxable_income_deductions_reduction 10,599,253,278.96
salt_cap 6,513,011,228,769.60
md_income_tax 15,156,844,198.63
me_income_tax 2,798,402,934.94
nm_income_tax 2,257,269,570.33
income_tax 2,595,105,852,713.51
state_income_tax 578,044,599,346.72
household_net_income 17,161,633,654,134.80
household_benefits 2,059,999,418,114.87
weighted sums as JSON
{
 "household_benefits": 2059999418114.8687,
 "household_net_income": 17161633654134.797,
 "income_tax": 2595105852713.508,
 "itemized_taxable_income_deductions_reduction": 10599253278.9609,
 "md_deductions": 30732223727.406742,
 "md_income_tax": 15156844198.633507,
 "md_itemized_deductions": 23633476785.28398,
 "me_deductions": 16787410277.648087,
 "me_income_tax": 2798402934.942308,
 "me_itemized_deductions_pre_phaseout": 4551828204.946152,
 "nm_deductions": 29301203896.830112,
 "nm_income_tax": 2257269570.331682,
 "nm_itemized_deductions": 8805151058.535755,
 "nm_salt_add_back": 660548696.7935939,
 "salt_cap": 6513011228769.599,
 "state_income_tax": 578044599346.724
}

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review
IMPACT PAIR: differs
head 6409fc5
base 0ea8894

10 of the 16 variables move in 2026, all through Maryland, Maine and New Mexico tax units. md_income_tax rises $12,503,376 and nm_income_tax rises $80,101. Together they equal the $12,583,477 rise in state_income_tax. household_net_income falls $18,721,850, which also carries Maryland county tax (see "By state"). No federal variable moves, and the Maine change reaches no Maine tax.

These totals rest on very few records. Two Maryland records carry all but $41 of the Maryland tax change, two New Mexico records net within $19 of the New Mexico one, and Maine's five moving records weigh 0.03 together. So the dollar totals carry wide sampling error.

Setup

  • Dataset: hf://datasets/policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260909 (Microsimulation.default_dataset, identical in both trees).
  • Local file SHA-256: 6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84. DEFAULT_DATASET_SHA256: 6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84. They match.
  • Year 2026, 8 household chunks on both sides.
  • Head: unchanged from the brief. Base: git merge-base origin/main <head>, with main at 6fa4eaf6d33caaf74e3b76885f9210cae72c53bf. It equals the PR's base commit.
  • The tag resolved to Hugging Face revision 9a814a3b3b53c0ecd6e1737b6ec862c31300ef6f, where populace_us_2024.h5 is 826,917,837 bytes. The download ran before the base side, so neither timing includes it. The hash ran before the base side and again after it.
  • git diff <base> <head> -- pyproject.toml uv.lock is empty, so one uv sync in the head worktree (CPython 3.14.6, policyengine-core 3.33.2) served both sides. Each side's summary.json shows it loaded policyengine_us from its own worktree.
  • Microsimulation accepted dataset_end_year=2026 on both sides.
  • Helper: policyengine_us/tools/calculate_chunked.py from Add household-chunked microsimulation calculations #10561 at c545c47877c70b367a6fab2322037e453364997e (SHA-256 ad5f6f69db71116242f2935e64fe0c50b0ad7b711d5f6ed11ce861daaca297db). Its docstring says the result is exact for household-local calculations, and its population-aggregate guard raised nothing on either side. pair_side.py and compare.py are the brief's scripts, unchanged.
  • Before the runs, an import of each tree found all 16 variables on both sides, on the same entity: household_net_income and household_benefits on household, the other 14 on tax_unit.

Results

Sums are weighted dollars, using each entity's simulation weight. household_net_income and household_benefits are household variables, and the other 14 are tax-unit variables. Weighted rows sum the weights of the rows whose value rose or fell by more than $0.000001.

Variable Base sum Head sum Change Rows that differ Weighted rows up Weighted rows down Largest absolute difference
md_itemized_deductions 23,633,476,785 23,388,822,375 -244,654,410.00 10 0.00 29,441.33 17,291.09
md_deductions 30,732,223,727 30,535,874,165 -196,349,562.54 9 0.00 14,414.78 17,291.09
me_itemized_deductions_pre_phaseout 4,551,828,205 4,551,828,187 -18.21 5 0.00 0.03 1,418.71
me_deductions 16,787,410,278 16,787,410,278 0.00 0 0.00 0.00 0.00
nm_itemized_deductions 8,805,151,059 8,793,109,978 -12,041,080.24 15 0.00 2,034.00 11,240.02
nm_salt_add_back 660,548,697 649,875,516 -10,673,180.47 32 10,739.98 3,054.00 29,254.46
nm_deductions 29,301,203,897 29,289,173,193 -12,030,704.26 3 0.00 2,014.81 11,240.02
itemized_taxable_income_deductions_reduction 10,599,253,279 10,599,253,279 0.00 0 0.00 0.00 0.00
salt_cap 6,513,011,228,770 6,513,011,228,770 0.00 0 0.00 0.00 0.00
md_income_tax 15,156,844,199 15,169,347,575 12,503,375.91 9 14,414.78 0.00 1,123.91
me_income_tax 2,798,402,935 2,798,402,935 0.00 0 0.00 0.00 0.00
nm_income_tax 2,257,269,570 2,257,349,671 80,100.92 8 2,014.81 3,053.96 1,726.02
income_tax 2,595,105,852,714 2,595,105,852,714 0.00 0 0.00 0.00 0.00
state_income_tax 578,044,599,347 578,057,182,824 12,583,476.83 17 16,429.59 3,053.96 1,726.02
household_net_income 17,161,633,654,135 17,161,614,932,285 -18,721,849.86 17 3,053.96 16,429.59 1,726.00
household_benefits 2,059,999,418,115 2,059,999,418,115 0.00 0 0.00 0.00 0.00

10 of 16 variables move: md_itemized_deductions, md_deductions, me_itemized_deductions_pre_phaseout, nm_itemized_deductions, nm_salt_add_back, nm_deductions, md_income_tax, nm_income_tax, state_income_tax, household_net_income.
6 don't move on any row: me_deductions, itemized_taxable_income_deductions_reduction, salt_cap, me_income_tax, income_tax, household_benefits.

In nm_salt_add_back, 26 of the 32 rows that differ move by $0.003 or less. They make up all 10,739.98 weighted rows up, 0.04 of the 3,054.00 weighted rows down and +$7.25 of the change. No tax moves on them. The other 6 rows all fall, by $2,874 to $29,254.

By state

These notes come from the two sides' pickles, the dataset's person and household tables, and the formulas each note names. by_state.py below reproduces every number.

Maryland

  • Maryland holds 40 records with a section 68 reduction (itemized_taxable_income_deductions_reduction > 0), weight 55,095. md_itemized_deductions falls on 10 of them (weight 29,441). The other 30 (weight 25,654) have md_itemized_deductions of 0 on both sides.
  • md_deductions and md_income_tax move on 9 of the 10 (weight 14,415). The tenth (weight 15,027) has itemized deductions below the $6,850 Maryland standard deduction on both sides ($5,436 on base, $2,221 on head). md_deductions gives a federal itemizer the larger of the two and anyone else the standard deduction, so it stays at $6,850 either way.
  • md_income_tax rises by $4.90 to $1,123.91 per record. Two records (weights 9,058 and 5,357) carry all but $41 of the $12,503,376.

New Mexico

  • New Mexico holds 15 records with a section 68 reduction (weight 2,034), and nm_itemized_deductions falls on all 15. One record (weight 2,015) carries 99.9% of the change.
  • nm_deductions falls on 3 of them (weight 2,015). nm_deductions uses nm_itemized_deductions only for federal itemizers, so the other 12 keep the standard deduction on both sides.
  • nm_salt_add_back falls by more than a cent on 6 records (weight 3,054). All 6 have a phased-down salt_cap, below the statutory cap, and 5 of them have no section 68 reduction. The addback can't exceed federal itemized deductions minus the standard deduction. From 2026 the head takes that total from itemized_taxable_income_deductions, which carries the phased-down cap (nm_salt_add_back.py:44-49 at head). main rebuilt it as itemized_deductions_less_salt plus SALT capped at the statutory cap (lines 33-34 and 41-42 on main). The PR body describes this effect. One record (weight 3,054) carries 99.997% of the change.
  • The 26 sub-cent rows all have no section 68 reduction and the unphased $40,400 cap. For them the head's total and main's rebuild add the same amounts at different precision. The head reads the stored float32 itemized_taxable_income_deductions, which on these rows equals total_itemized_taxable_income_deductions (it adds salt_deduction third), and subtracts the standard deduction in float32. main added the claimed SALT last and subtracted in float64, because its claimed SALT is float64: the tax-unit sum of the person-level real_estate_taxes and the statutory cap lookup both return float64. The stored float32 results differ by at most $0.003.
  • nm_income_tax rises on 2 records (weight 2,015, +$709,812) and falls on the 6 whose addback falls (weight 3,054, -$629,711). Two records net +$80,119 against the +$80,101 total: one (weight 2,015) adds $709,811 and another (weight 3,054) takes away $629,692. The other six net -$19.

Maine

  • Maine holds 23 records with a section 68 reduction (weight 1,171). me_itemized_deductions_pre_phaseout falls on 5 of them, whose weights sum to 0.03. The other 18 have me_itemized_deductions_pre_phaseout of $0 or $37,100 on both sides.
  • me_deductions is 0 on both sides for all 23. It multiplies the deduction by one minus the phase-out percentage, so the phase-out takes the whole deduction there. Neither me_deductions nor me_income_tax moves.

Households

  • household_net_income moves on 17 households: the 9 that hold the Maryland records whose md_income_tax moves and the 8 that hold the New Mexico records whose nm_income_tax moves, linked through the dataset's person table.
  • Each New Mexico household's change equals minus its nm_income_tax change. The largest gap is $0.08, which is float32 rounding.
  • The Maryland households lose $18,641,586 while their md_income_tax rises $12,503,376. The other $6,138,210 matches Maryland county tax. In 8 of the 9 households the gap, divided by the md_deductions change, matches the 2026 rate in gov.local.md.flat_rate for the household's county_fips to within 0.002 percentage points: 2.95% in Washington County, 3.06% in Harford County, and 3.20% in Montgomery, Prince George's, Baltimore County and Baltimore City. The ninth, a Harford household whose md_deductions fell only $78, gives 3.00% against 3.06%, within float32 rounding. md_flat_rate_county_tax taxes md_taxable_income, and household_tax_before_refundable_credits adds it through local_income_tax_before_refundable_credits, outside state_income_tax.

Federal and downstream

  • No federal variable moves. Federal SALT reads state_withheld_income_tax, which Maryland and New Mexico estimate from AGI (md_withheld_income_tax, nm_withheld_income_tax), not from the state taxes the PR changes. local_income_tax, the other income-tax input to federal SALT, leaves out Maryland county tax, which reaches SALT only through the AGI-based md_withheld_income_tax.
  • The PR changes four formulas: md_itemized_deductions, me_itemized_deductions_pre_phaseout, nm_itemized_deductions and nm_salt_add_back. Every other variable that moves sits downstream of them: in Maryland through md_deductions and md_taxable_income, in New Mexico through nm_deductions and nm_taxable_income, which also reads nm_salt_add_back.

Resources

Side Wall time, whole process Helper call and pickle writes Maximum resident set size Exit status
base 10:02.81 570.7 s 3,802,932 kB (3.63 GiB) 0
head 9:56.45 566.8 s 3,801,112 kB (3.63 GiB) 0

The sides ran one at a time, each detached under nohup setsid, with only a once-a-minute ps sampler beside it. Neither came near 14 GB, so the 16-chunk rerun rule didn't apply. Each side took about 10 minutes, not the 70 the brief expected.

Dropped variables

None. Every variable exists on both sides on the same entity, Microsimulation accepted dataset_end_year, and the helper's guard rejected none.

Commands

The session ran these commands from the root of its existing clone of PolicyEngine/policyengine-us, in place of the brief's git clone ... pe:

git fetch origin main pull/10594/head:pr-10594 pull/10561/head:pr-10561
HEAD_SHA=$(git rev-parse pr-10594); BASE_SHA=$(git merge-base origin/main $HEAD_SHA); echo $HEAD_SHA $BASE_SHA
git worktree add --detach ../head $HEAD_SHA
git worktree add --detach ../base $BASE_SHA
git show pr-10561:policyengine_us/tools/calculate_chunked.py > ../calculate_chunked.py
git diff $BASE_SHA $HEAD_SHA -- pyproject.toml uv.lock     # empty
git diff $BASE_SHA $HEAD_SHA -- policyengine_us/system.py  # empty, so DEFAULT_DATASET and DEFAULT_DATASET_SHA256 match
(cd ../head && uv sync)
# Cache the default dataset before the timed runs, then hash the file it resolves to.
PYTHONPATH=$PWD/../head ../head/.venv/bin/python -c "from policyengine_us.system import _resolve_dataset_path, DEFAULT_DATASET; print(_resolve_dataset_path(DEFAULT_DATASET))"
sha256sum "$(readlink -f <printed path>)"
apt-get install -y time || (apt-get update && apt-get install -y time)   # the VM had no /usr/bin/time
mkdir -p ../out
H=$PWD/../calculate_chunked.py; PY=$PWD/../head/.venv/bin/python
export OMP_NUM_THREADS=4 OPENBLAS_NUM_THREADS=4 MKL_NUM_THREADS=4
PYTHONPATH=$PWD/../base /usr/bin/time -v $PY ../pair_side.py $H 8 2026 ../out/base > ../out/base.log 2> ../out/base.err
PYTHONPATH=$PWD/../head /usr/bin/time -v $PY ../pair_side.py $H 8 2026 ../out/head > ../out/head.log 2> ../out/head.err
$PY ../compare.py ../out/base ../out/head ../out/compare.json

The by-state notes come from this read-only script, run as $PY -I by_state.py ../out <dataset file> ../head/policyengine_us/parameters/gov/local/md/flat_rate.yaml:

by_state.py
"""Read-only checks behind the by-state notes.

python -I by_state.py <out_dir> <dataset.h5> <head>/policyengine_us/parameters/gov/local/md/flat_rate.yaml
"""
import json
import sys

import numpy as np
import pandas as pd
import yaml

out, dataset, flat_rate_yaml = sys.argv[1], sys.argv[2], sys.argv[3]
b = pd.read_pickle(f"{out}/base/tax_unit.pkl")
h = pd.read_pickle(f"{out}/head/tax_unit.pkl")
assert np.array_equal(b.tax_unit_id, h.tax_unit_id) and np.array_equal(b.weight, h.weight)
w = b.weight.to_numpy(dtype=float)
R = b.itemized_taxable_income_deductions_reduction.to_numpy(dtype=float)
cap = b.salt_cap.to_numpy(dtype=float)
ids = b.tax_unit_id.to_numpy()


def diff(name, frame_b=b, frame_h=h):
    return frame_h[name].to_numpy(dtype=float) - frame_b[name].to_numpy(dtype=float)


def summary(mask, d):
    return {
        "records": int(mask.sum()),
        "weight": float(w[mask].sum()),
        "weighted_change": float((d[mask] * w[mask]).sum()),
        "min_abs": float(np.abs(d[mask]).min()) if mask.any() else 0.0,
        "max_abs": float(np.abs(d[mask]).max()) if mask.any() else 0.0,
        "all_R_positive": bool((R[mask] > 0).all()),
    }


def top(mask, d, n=3):
    order = np.argsort(-np.abs(d * w * mask))[:n]
    total = float((d[mask] * w[mask]).sum())
    return [
        {"id": int(ids[i]), "weight": float(w[i]), "diff": float(d[i]),
         "weighted": float(d[i] * w[i]), "share": float(d[i] * w[i] / total)}
        for i in order if mask[i]
    ]


facts = {"federal_R_positive": {"records": int((R > 0).sum()), "weight": float(w[R > 0].sum())}}
moved = lambda d: np.abs(d) > 1e-6

# Maryland
d_itm, d_ded, d_tax = diff("md_itemized_deductions"), diff("md_deductions"), diff("md_income_tax")
facts["md"] = {
    "itemized": summary(moved(d_itm), d_itm),
    "deductions": summary(moved(d_ded), d_ded),
    "tax": summary(moved(d_tax), d_tax),
    "tax_top": top(moved(d_tax), d_tax, 2),
    "itemized_top": top(moved(d_itm), d_itm, 3),
    "itemized_not_deductions": [
        {"id": int(ids[i]), "weight": float(w[i]),
         "md_itemized_base": float(b.md_itemized_deductions.iloc[i]),
         "md_itemized_head": float(h.md_itemized_deductions.iloc[i]),
         "md_deductions_both": [float(b.md_deductions.iloc[i]), float(h.md_deductions.iloc[i])]}
        for i in np.flatnonzero(moved(d_itm) & ~moved(d_ded))
    ],
    "tax_rows_equal_deduction_rows": bool(np.array_equal(moved(d_tax), moved(d_ded))),
}

# Maine
d_pre, d_med, d_mtax = diff("me_itemized_deductions_pre_phaseout"), diff("me_deductions"), diff("me_income_tax")
m = moved(d_pre)
facts["me"] = {
    "pre_phaseout": summary(m, d_pre),
    "me_deductions_on_those_rows": sorted({float(x) for x in b.me_deductions[m]} | {float(x) for x in h.me_deductions[m]}),
    "me_deductions_moved": int(moved(d_med).sum()),
    "me_income_tax_moved": int(moved(d_mtax).sum()),
}

# New Mexico
d_nitm, d_nded, d_add, d_ntax = (diff(n) for n in ("nm_itemized_deductions", "nm_deductions", "nm_salt_add_back", "nm_income_tax"))
big, small = np.abs(d_add) > 0.01, moved(d_add) & (np.abs(d_add) <= 0.01)
facts["nm"] = {
    "itemized": summary(moved(d_nitm), d_nitm),
    "itemized_top": top(moved(d_nitm), d_nitm, 2),
    "deductions": summary(moved(d_nded), d_nded),
    "deductions_rows_subset_of_itemized_rows": bool((moved(d_nded) <= moved(d_nitm)).all()),
    "addback_over_1c": {**summary(big, d_add),
                        "all_down": bool((d_add[big] < 0).all()),
                        "R_zero": int((R[big] == 0).sum()),
                        "salt_caps": sorted(float(x) for x in cap[big]),
                        "top": top(big, d_add, 1)},
    "addback_sub_1c": {"records": int(small.sum()), "max_abs": float(np.abs(d_add[small]).max()),
                       "weight_up": float(w[small & (d_add > 0)].sum()),
                       "weight_down": float(w[small & (d_add < 0)].sum()),
                       "weighted_change": float((d_add[small] * w[small]).sum()),
                       "all_R_zero": bool((R[small] == 0).all()),
                       "salt_caps": sorted({float(x) for x in cap[small]}),
                       "nm_income_tax_moved_on_them": int((small & moved(d_ntax)).sum())},
    "tax_up": summary(moved(d_ntax) & (d_ntax > 0), d_ntax),
    "tax_down": summary(moved(d_ntax) & (d_ntax < 0), d_ntax),
    "tax_down_rows_equal_addback_over_1c_rows": bool(np.array_equal(moved(d_ntax) & (d_ntax < 0), big)),
    "tax_top": top(moved(d_ntax), d_ntax, 2),
}

# Households: link tax units to households through the dataset's person table.
with pd.HDFStore(dataset, mode="r") as store:
    person = store["/person"][["person_household_id", "person_tax_unit_id"]]
    places = store["/household"][["household_id", "state_fips", "county_fips"]].set_index("household_id")
link = person.drop_duplicates("person_tax_unit_id").set_index("person_tax_unit_id").person_household_id
tu = pd.DataFrame({"household_id": link.reindex(ids).to_numpy(),
                   "d_md": d_tax, "d_nm": d_ntax, "d_state": diff("state_income_tax"), "d_fed": diff("income_tax"),
                   "d_md_ded": d_ded})
assert tu.household_id.notna().all()
per_household = tu.groupby("household_id").sum()

# Records with a section 68 reduction, by state (FIPS 24 MD, 23 ME, 35 NM).
state = places.state_fips.reindex(tu.household_id).to_numpy()
by_state_R = {}
for code, fips, first in (("MD", 24, d_itm), ("ME", 23, d_pre), ("NM", 35, d_nitm)):
    in_state, has_R = state == fips, R > 0
    rest = in_state & has_R & ~moved(first)
    by_state_R[code] = {
        "R_positive_records": int((in_state & has_R).sum()),
        "R_positive_weight": float(w[in_state & has_R].sum()),
        "moved_records": int((in_state & has_R & moved(first)).sum()),
        "moved_outside_state_or_without_R": int((moved(first) & ~(in_state & has_R)).sum()),
        "rest_records": int(rest.sum()),
        "rest_weight": float(w[rest].sum()),
    }
md_rest = (state == 24) & (R > 0) & ~moved(d_itm)
by_state_R["MD"]["rest_md_itemized_values"] = sorted({float(x) for x in b.md_itemized_deductions[md_rest]} | {float(x) for x in h.md_itemized_deductions[md_rest]})
me_R = (state == 23) & (R > 0)
by_state_R["ME"]["me_deductions_values_all_R"] = sorted({float(x) for x in b.me_deductions[me_R]} | {float(x) for x in h.me_deductions[me_R]})
me_rest = me_R & ~moved(d_pre)
by_state_R["ME"]["rest_pre_phaseout_values"] = sorted({round(float(x), 2) for x in b.me_itemized_deductions_pre_phaseout[me_rest]} | {round(float(x), 2) for x in h.me_itemized_deductions_pre_phaseout[me_rest]})
by_state_R["ME"]["me_deductions_positive_with_R"] = int((me_R & ((b.me_deductions > 0) | (h.me_deductions > 0)).to_numpy()).sum())
facts["section_68_by_state"] = by_state_R
hb, hh = pd.read_pickle(f"{out}/base/household.pkl"), pd.read_pickle(f"{out}/head/household.pkl")
hw = hb.weight.to_numpy(dtype=float)
hd = pd.DataFrame({"weight": hw, "d_hni": diff("household_net_income", hb, hh)}, index=hb.household_id)
hd = hd.join(per_household, how="left").fillna(0.0)
hm = hd[np.abs(hd.d_hni) > 1e-6]
md_h, nm_h = hm[np.abs(hm.d_md) > 1e-6], hm[np.abs(hm.d_nm) > 1e-6]
facts["households"] = {
    "moved": int(len(hm)),
    "md": int(len(md_h)), "nm": int(len(nm_h)), "both_or_neither": int(len(hm) - len(md_h) - len(nm_h)),
    "md_weighted_d_hni": float((md_h.d_hni * md_h.weight).sum()),
    "md_weighted_d_tax": float((md_h.d_md * md_h.weight).sum()),
    "md_weighted_gap": float(((md_h.d_hni + md_h.d_md) * md_h.weight).sum()),
    "nm_max_abs_gap": float(np.abs(nm_h.d_hni + nm_h.d_nm).max()),
    "nm_weighted_gap": float(((nm_h.d_hni + nm_h.d_nm) * nm_h.weight).sum()),
    "any_federal_change": bool((np.abs(hd.d_fed) > 1e-6).any()),
}

# Maryland county tax: per household, (change in net income + change in MD tax)
# divided by the change in md_deductions, against the county's flat rate.
FIPS_KEYS = {24005: "BALTIMORE_COUNTY_MD", 24025: "HARFORD_COUNTY_MD", 24031: "MONTGOMERY_COUNTY_MD",
             24033: "PRINCE_GEORGE_S_COUNTY_MD", 24043: "WASHINGTON_COUNTY_MD", 24510: "BALTIMORE_CITY_MD"}
rates = yaml.safe_load(open(flat_rate_yaml))


def rate_2026(key):
    values = rates[key]
    return float(values[max(k for k in values if str(k) <= "2026-01-01")])


county_rows = []
for household, row in md_h.iterrows():
    fips = int(places.county_fips.loc[household])
    county_rows.append({
        "household_id": int(household), "weight": float(row.weight), "county_fips": fips,
        "d_hni": float(row.d_hni), "d_md_tax": float(row.d_md), "d_md_deductions": float(row.d_md_ded),
        "gap_over_d_md_deductions": float((row.d_hni + row.d_md) / row.d_md_ded),
        "county_rate_2026": rate_2026(FIPS_KEYS[fips]) if fips in FIPS_KEYS else None,
    })
facts["md_county"] = sorted(county_rows, key=lambda r: r["county_fips"])
print(json.dumps(facts, indent=1))
compare.json
{
 "household_net_income": {
  "entity": "household",
  "base_sum": 17161633654134.797,
  "head_sum": 17161614932284.938,
  "change": -18721849.859375,
  "records_changed": 17,
  "weighted_changed": 19483.549516650208,
  "weighted_up": 3053.961045373755,
  "weighted_down": 16429.588471276453,
  "largest_abs": 1726.0,
  "rows": [
   {
    "id": 1044603,
    "base": 447266.875,
    "head": 448992.875
   },
   {
    "id": 1019711,
    "base": 1193200.5,
    "head": 1191523.25
   },
   {
    "id": 1020204,
    "base": 1184917.0,
    "head": 1183440.625
   },
   {
    "id": 1019684,
    "base": 957511.25,
    "head": 956445.125
   },
   {
    "id": 1044295,
    "base": 631847.1875,
    "head": 632834.4375
   }
  ]
 },
 "household_benefits": {
  "entity": "household",
  "base_sum": 2059999418114.8687,
  "head_sum": 2059999418114.8687,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "md_itemized_deductions": {
  "entity": "tax_unit",
  "base_sum": 23633476785.28398,
  "head_sum": 23388822375.288548,
  "change": -244654409.9954338,
  "records_changed": 10,
  "weighted_changed": 29441.329996718676,
  "weighted_up": 0.0,
  "weighted_down": 29441.329996718676,
  "largest_abs": 17291.09375,
  "rows": [
   {
    "id": 1025837,
    "base": 191181.671875,
    "head": 173890.578125
   },
   {
    "id": 1026192,
    "base": 159868.40625,
    "head": 144648.34375
   },
   {
    "id": 1025806,
    "base": 131547.375,
    "head": 120096.125
   },
   {
    "id": 1101936,
    "base": 42690.3515625,
    "head": 35975.03125
   },
   {
    "id": 1102460,
    "base": 58243.49609375,
    "head": 52286.62890625
   }
  ]
 },
 "md_deductions": {
  "entity": "tax_unit",
  "base_sum": 30732223727.406742,
  "head_sum": 30535874164.868927,
  "change": -196349562.5378151,
  "records_changed": 9,
  "weighted_changed": 14414.778238906176,
  "weighted_up": 0.0,
  "weighted_down": 14414.778238906176,
  "largest_abs": 17291.09375,
  "rows": [
   {
    "id": 1025837,
    "base": 191181.671875,
    "head": 173890.578125
   },
   {
    "id": 1026192,
    "base": 159868.40625,
    "head": 144648.34375
   },
   {
    "id": 1025806,
    "base": 131547.375,
    "head": 120096.125
   },
   {
    "id": 1101936,
    "base": 42690.3515625,
    "head": 35975.03125
   },
   {
    "id": 1102460,
    "base": 58243.49609375,
    "head": 52286.62890625
   }
  ]
 },
 "me_itemized_deductions_pre_phaseout": {
  "entity": "tax_unit",
  "base_sum": 4551828204.946152,
  "head_sum": 4551828186.738266,
  "change": -18.2078857421875,
  "records_changed": 5,
  "weighted_changed": 0.02885818830691278,
  "weighted_up": 0.0,
  "weighted_down": 0.02885818830691278,
  "largest_abs": 1418.71484375,
  "rows": [
   {
    "id": 1000163,
    "base": 36512.3359375,
    "head": 35093.62109375
   },
   {
    "id": 1075142,
    "base": 20559.984375,
    "head": 19689.9609375
   },
   {
    "id": 1000364,
    "base": 2602.8994140625,
    "head": 2462.2021484375
   },
   {
    "id": 1074793,
    "base": 10606.58203125,
    "head": 10537.017578125
   },
   {
    "id": 1000277,
    "base": 688.5829467773438,
    "head": 683.6682739257812
   }
  ]
 },
 "me_deductions": {
  "entity": "tax_unit",
  "base_sum": 16787410277.648087,
  "head_sum": 16787410277.648087,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "nm_itemized_deductions": {
  "entity": "tax_unit",
  "base_sum": 8805151058.535755,
  "head_sum": 8793109978.29378,
  "change": -12041080.241975784,
  "records_changed": 15,
  "weighted_changed": 2034.0039223907806,
  "weighted_up": 0.0,
  "weighted_down": 2034.0039223907806,
  "largest_abs": 11240.015625,
  "rows": [
   {
    "id": 1058575,
    "base": 207940.375,
    "head": 196700.359375
   },
   {
    "id": 1058500,
    "base": 110465.984375,
    "head": 104494.8515625
   },
   {
    "id": 1058371,
    "base": 37485.25390625,
    "head": 35459.0234375
   },
   {
    "id": 1214044,
    "base": 18944.63671875,
    "head": 17920.6015625
   },
   {
    "id": 1058854,
    "base": 10000.0,
    "head": 9459.458984375
   }
  ]
 },
 "nm_salt_add_back": {
  "entity": "tax_unit",
  "base_sum": 660548696.7935939,
  "head_sum": 649875516.3280382,
  "change": -10673180.465555668,
  "records_changed": 32,
  "weighted_changed": 13793.982002581062,
  "weighted_up": 10739.984198251623,
  "weighted_down": 3053.9978043294395,
  "largest_abs": 29254.455078125,
  "rows": [
   {
    "id": 1057795,
    "base": 31979.166015625,
    "head": 2724.7109375
   },
   {
    "id": 1058371,
    "base": 38118.0625,
    "head": 19359.0234375
   },
   {
    "id": 1135947,
    "base": 22083.484375,
    "head": 6967.556640625
   },
   {
    "id": 1135738,
    "base": 21422.79296875,
    "head": 15915.875
   },
   {
    "id": 1213136,
    "base": 28620.40234375,
    "head": 25125.625
   }
  ]
 },
 "nm_deductions": {
  "entity": "tax_unit",
  "base_sum": 29301203896.830112,
  "head_sum": 29289173192.56698,
  "change": -12030704.263134003,
  "records_changed": 3,
  "weighted_changed": 2014.8111841565697,
  "weighted_up": 0.0,
  "weighted_down": 2014.8111841565697,
  "largest_abs": 11240.015625,
  "rows": [
   {
    "id": 1058575,
    "base": 211940.375,
    "head": 200700.359375
   },
   {
    "id": 1058500,
    "base": 110465.984375,
    "head": 104494.8515625
   },
   {
    "id": 1058371,
    "base": 39985.25390625,
    "head": 37959.0234375
   }
  ]
 },
 "itemized_taxable_income_deductions_reduction": {
  "entity": "tax_unit",
  "base_sum": 10599253278.9609,
  "head_sum": 10599253278.9609,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "salt_cap": {
  "entity": "tax_unit",
  "base_sum": 6513011228769.599,
  "head_sum": 6513011228769.599,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "md_income_tax": {
  "entity": "tax_unit",
  "base_sum": 15156844198.633507,
  "head_sum": 15169347574.545631,
  "change": 12503375.912124634,
  "records_changed": 9,
  "weighted_changed": 14414.778238906176,
  "weighted_up": 14414.778238906176,
  "weighted_down": 0.0,
  "largest_abs": 1123.9140625,
  "rows": [
   {
    "id": 1025837,
    "base": 83650.2265625,
    "head": 84774.140625
   },
   {
    "id": 1026192,
    "base": 97117.0,
    "head": 98106.296875
   },
   {
    "id": 1025806,
    "base": 75335.8828125,
    "head": 76051.5859375
   },
   {
    "id": 1101936,
    "base": 71413.6484375,
    "head": 71850.1484375
   },
   {
    "id": 1026326,
    "base": 72448.5234375,
    "head": 72834.09375
   }
  ]
 },
 "me_income_tax": {
  "entity": "tax_unit",
  "base_sum": 2798402934.942308,
  "head_sum": 2798402934.942308,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "nm_income_tax": {
  "entity": "tax_unit",
  "base_sum": 2257269570.331682,
  "head_sum": 2257349671.2469015,
  "change": 80100.91521930695,
  "records_changed": 8,
  "weighted_changed": 5068.771277744032,
  "weighted_up": 2014.810232370277,
  "weighted_down": 3053.961045373755,
  "largest_abs": 1726.015625,
  "rows": [
   {
    "id": 1057795,
    "base": 34923.45703125,
    "head": 33197.44140625
   },
   {
    "id": 1058371,
    "base": 56983.65234375,
    "head": 55996.41015625
   },
   {
    "id": 1135947,
    "base": 30356.154296875,
    "head": 29464.314453125
   },
   {
    "id": 1058575,
    "base": 54533.76171875,
    "head": 55196.921875
   },
   {
    "id": 1058500,
    "base": 97034.1484375,
    "head": 97386.4453125
   }
  ]
 },
 "income_tax": {
  "entity": "tax_unit",
  "base_sum": 2595105852713.508,
  "head_sum": 2595105852713.508,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "state_income_tax": {
  "entity": "tax_unit",
  "base_sum": 578044599346.724,
  "head_sum": 578057182823.5513,
  "change": 12583476.827270508,
  "records_changed": 17,
  "weighted_changed": 19483.549516650208,
  "weighted_up": 16429.588471276453,
  "weighted_down": 3053.961045373755,
  "largest_abs": 1726.015625,
  "rows": [
   {
    "id": 1057795,
    "base": 34923.45703125,
    "head": 33197.44140625
   },
   {
    "id": 1025837,
    "base": 83650.2265625,
    "head": 84774.140625
   },
   {
    "id": 1026192,
    "base": 97117.0,
    "head": 98106.296875
   },
   {
    "id": 1058371,
    "base": 56983.65234375,
    "head": 55996.41015625
   },
   {
    "id": 1135947,
    "base": 30356.154296875,
    "head": 29464.314453125
   }
  ]
 }
}

Generated by Claude Code

@MaxGhenis
MaxGhenis merged commit eae5f04 into main Nov 3, 2026
35 checks passed
@MaxGhenis
MaxGhenis deleted the claude/section-68-2026-md-me-nm-atit45 branch November 3, 2026 07:16
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

US + core hub merge audit, us#10594 at adf72458598fbe61ed6a3c69b81a3f529bfd80c3 (squash): Apply the 2026 section 68 reduction in Maryland, Maine and New Mexico itemized deductions.

MaxGhenis pushed a commit that referenced this pull request Nov 3, 2026
Brings in main since the November 2 merge, including #10594's state
section 68 changes, which share no file or line with this branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6GA1FXMB3qVPzGgvWQ
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.

2 participants