From d9cb6303dd67ee309758cf7530f9352ddc2a2535 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 16:58:04 -0400 Subject: [PATCH 1/6] Use the separate-filer breakpoint on Form 6251 Part III line 18 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 --- .../amt-part-iii-line-18-separate.fixed.md | 1 + .../amt_tax_including_cg.yaml | 153 +++++++ .../base_tax/amt_base_tax.yaml | 10 + .../alternative_minimum_tax/integration.yaml | 49 ++ .../tests/test_form_6251_part_iii.py | 430 ++++++++++++++++++ .../amt_tax_including_cg.py | 10 +- 6 files changed, 650 insertions(+), 3 deletions(-) create mode 100644 changelog.d/amt-part-iii-line-18-separate.fixed.md create mode 100644 policyengine_us/tests/test_form_6251_part_iii.py diff --git a/changelog.d/amt-part-iii-line-18-separate.fixed.md b/changelog.d/amt-part-iii-line-18-separate.fixed.md new file mode 100644 index 00000000000..09880d3d374 --- /dev/null +++ b/changelog.d/amt-part-iii-line-18-separate.fixed.md @@ -0,0 +1 @@ +Apply the married-filing-separately 28% breakpoint (26 U.S.C. 55(b)(1)(C)) on Form 6251 Part III line 18, so separate filers with qualified dividends or capital gains no longer get the full breakpoint in the capital gains computation of the alternative minimum tax. diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/amt_tax_including_cg.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/amt_tax_including_cg.yaml index 6759e34ef7c..e65b4bc1ae4 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/amt_tax_including_cg.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/amt_tax_including_cg.yaml @@ -156,3 +156,156 @@ # 0%: $89,250 (joint); 15%: $310,750 * 15% = $46,612.50; # 20%: $0. amt_tax_including_cg: 46_612.50 + +# Form 6251 (2025), line 18: "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." 26 U.S.C. 55(b)(1)(C) halves +# the breakpoint 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). Lines 19 and 25 for married filing separately are $48,350 +# and $300,000. +- name: Married filing separately, line 17 above the separate 28% breakpoint + period: 2025 + input: + filing_status: SEPARATE + amt_income_less_exemptions: 1_061_000 + dwks10: 925_000 + dwks13: 925_000 + dwks14: 136_000 + unrecaptured_section_1250_gain: 0 + output: + # Line 12: $1,061,000. Line 13: $925,000 ($25,000 of qualified + # dividends and $900,000 of long-term gain). Line 14: $0. + # Lines 15 and 16: $925,000. Line 17: $1,061,000 - $925,000 = $136,000. + # Line 18: $136,000 > $119,550, so 28% * $136,000 - $2,391 = $35,689. + # (The full $239,100 breakpoint would give 26% * $136,000 = $35,360.) + # Line 19: $48,350. Line 20: $136,000. Line 21: $0. + # Line 22: $925,000. Line 23: $0. Line 24: $925,000. + # Line 25: $300,000. Lines 26 to 28: $0 + $136,000 = $136,000. + # Line 29: $164,000. Line 30: $164,000. Line 31: 15% = $24,600. + # Line 32: $164,000. Line 33: $761,000. Line 34: 20% = $152,200. + # Line 14 is zero, so lines 35 to 37 are skipped. + # Line 38: $35,689 + $24,600 + $152,200 = $212,489. + amt_tax_including_cg: 212_489 + +- name: Married filing separately, line 17 between the separate and full breakpoints + period: 2025 + input: + filing_status: SEPARATE + amt_income_less_exemptions: 250_000 + dwks10: 50_000 + dwks13: 50_000 + dwks14: 200_000 + unrecaptured_section_1250_gain: 0 + output: + # Line 12: $250,000. Lines 13, 15 and 16: $50,000. Line 17: $200,000. + # Line 18: 28% * $200,000 - $2,391 = $53,609 (not 26% * $200,000 = + # $52,000). Line 21: max(0, $48,350 - $200,000) = $0. Line 22: + # $50,000. Line 23: $0. Line 24: $50,000. Line 29: $300,000 - $200,000 + # = $100,000. Line 30: $50,000. Line 31: $7,500. Line 33: $0. + # Line 38: $53,609 + $7,500 = $61,109. + amt_tax_including_cg: 61_109 + +- name: Married filing separately, line 17 exactly at the separate breakpoint + period: 2025 + input: + filing_status: SEPARATE + amt_income_less_exemptions: 169_550 + dwks10: 50_000 + dwks13: 50_000 + dwks14: 119_550 + unrecaptured_section_1250_gain: 0 + output: + # Line 17: $119,550, which is "$119,550 or less", so line 18 is 26% * + # $119,550 = $31,083. Line 29: $300,000 - $119,550 = $180,450. Line 30: + # $50,000. Line 31: $7,500. Line 38: $31,083 + $7,500 = $38,583. + amt_tax_including_cg: 38_583 + +- name: Married filing separately, line 17 one dollar above the separate breakpoint + period: 2025 + absolute_error_margin: 0.01 + input: + filing_status: SEPARATE + amt_income_less_exemptions: 169_551 + dwks10: 50_000 + dwks13: 50_000 + dwks14: 119_551 + unrecaptured_section_1250_gain: 0 + output: + # Line 17: $119,551. Line 18: 28% * $119,551 - $2,391 = $31,083.28 + # (26% would give $31,083.26). Line 31: $7,500. + # Line 38: $31,083.28 + $7,500 = $38,583.28. + amt_tax_including_cg: 38_583.28 + +- name: Married filing separately, line 17 above the full breakpoint + period: 2025 + input: + filing_status: SEPARATE + amt_income_less_exemptions: 400_000 + dwks10: 100_000 + dwks13: 100_000 + dwks14: 300_000 + unrecaptured_section_1250_gain: 0 + output: + # Line 17: $300,000. Line 18: 28% * $300,000 - $2,391 = $81,609 (the + # full breakpoint would subtract $4,782, giving $79,218). + # Line 22: $100,000. Line 29: $300,000 - $300,000 = $0. Line 30: $0. + # Line 32: $0. Line 33: $100,000. Line 34: 20% = $20,000. + # Line 38: $81,609 + $20,000 = $101,609. + amt_tax_including_cg: 101_609 + +- name: Married filing separately, unrecaptured section 1250 gain + period: 2025 + input: + filing_status: SEPARATE + amt_income_less_exemptions: 500_000 + dwks10: 300_000 + dwks13: 200_000 + dwks14: 200_000 + unrecaptured_section_1250_gain: 100_000 + output: + # Line 12: $500,000. Line 13: $200,000. Line 14: $100,000. + # Line 15: min($200,000 + $100,000, $300,000) = $300,000. Line 16: + # $300,000. Line 17: $200,000. Line 18: 28% * $200,000 - $2,391 = + # $53,609. Line 21: $0. Line 22: $200,000. Line 23: $0. Line 24: + # $200,000. Line 29: $300,000 - $200,000 = $100,000. Line 30: $100,000. + # Line 31: $15,000. Line 32: $100,000. Line 33: $100,000. Line 34: + # $20,000. Line 35: $200,000 + $100,000 + $100,000 = $400,000. + # Line 36: $100,000. Line 37: 25% = $25,000. + # Line 38: $53,609 + $15,000 + $20,000 + $25,000 = $113,609. + amt_tax_including_cg: 113_609 + +- name: Single filer with the amounts of the first separate case keeps the full breakpoint + period: 2025 + input: + filing_status: SINGLE + amt_income_less_exemptions: 1_061_000 + dwks10: 925_000 + dwks13: 925_000 + dwks14: 136_000 + unrecaptured_section_1250_gain: 0 + output: + # Line 17: $136,000 <= $239,100, so line 18 is 26% * $136,000 = + # $35,360. Line 19: $48,350; line 21: $0. Line 22: $925,000. + # Line 25: $533,400. Line 28: $136,000. Line 29: $397,400. Line 30: + # $397,400. Line 31: $59,610. Line 32: $397,400. Line 33: $527,600. + # Line 34: $105,520. Line 38: $35,360 + $59,610 + $105,520 = $200,490. + amt_tax_including_cg: 200_490 + +- name: Married filing separately, no preferential income, 2026 + period: 2026 + input: + filing_status: SEPARATE + amt_income_less_exemptions: 136_000 + dwks10: 0 + dwks13: 0 + dwks14: 136_000 + unrecaptured_section_1250_gain: 0 + output: + # Rev. Proc. 2025-32, section 3.10: for 2026 the 28 percent rate applies + # above $122,250 for married individuals filing separate returns + # ($244,500 for all other taxpayers). Line 17 is all of line 12, + # $136,000, so line 18 is 28% * $136,000 - 2% * $122,250 = $38,080 - + # $2,445 = $35,635, the same as line 39. Lines 22 to 37 are zero. + amt_tax_including_cg: 35_635 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/base_tax/amt_base_tax.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/base_tax/amt_base_tax.yaml index 80951a6307b..0d8496e195f 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/base_tax/amt_base_tax.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/base_tax/amt_base_tax.yaml @@ -21,3 +21,13 @@ amt_income_less_exemptions: 200_000 output: amt_base_tax: 52_000 + +- name: Married filing separately above the separate breakpoint + period: 2025 + input: + filing_status: SEPARATE + amt_income_less_exemptions: 136_000 + output: + # Form 6251 (2025) line 7 "All others" (and line 39): $136,000 is more + # than $119,550, so 28% * $136,000 - $2,391 = $35,689. + amt_base_tax: 35_689 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/integration.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/integration.yaml index b765e0a3aef..50790f35b22 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/integration.yaml @@ -24,3 +24,52 @@ alternative_minimum_tax: 0 # Expected income tax from TAXSIM35 income_tax: 85_538 + +# Married filing separately with qualified dividends and long-term gain, so +# Form 6251 Part III applies and line 18 uses the separate breakpoint +# ($119,550 for 2025; 26 U.S.C. 55(b)(1)(C)). Worked by hand from the 2025 +# Form 1040 instructions (Qualified Dividends and Capital Gain Tax Worksheet, +# Tax Computation Worksheet) and the 2025 Form 6251. +- name: Married filing separately with dividends and long-term gain, 2025 + period: 2025 + input: + people: + person: + age: 45 + employment_income: 150_000 + qualified_dividend_income: 25_000 + long_term_capital_gains: 900_000 + tax_units: + tax_unit: + members: [person] + filing_status: SEPARATE + households: + household: + members: [person] + state_code: TX + output: + # Taxable income: $1,075,000 - $15,750 standard deduction = $1,059,250. + # Qualified Dividends and Capital Gain Tax Worksheet: line 4 $925,000; + # line 5 $134,250; line 18 15% * $165,750 = $24,862.50; line 21 20% * + # $759,250 = $151,850; line 22 tax on $134,250 = $25,067; line 25 + # $201,779.50. + taxable_income: 1_059_250 + regular_tax_before_credits: 25_067 + capital_gains_tax: 176_712.5 + # Form 6251: line 1 $1,059,250; line 2a $15,750; line 4 $1,075,000 plus + # 25% * ($1,075,000 - $900,350) = $43,662.50 for married filing + # separately (line 4 instructions) = $1,118,662.50. Line 5: $68,500 - + # 25% * ($1,118,662.50 - $626,350) < 0, so $0. Line 6: $1,118,662.50. + amt_income_less_exemptions: 1_118_662.5 + # Part III: line 12 $1,118,662.50; lines 13, 15, 16 $925,000; line 17 + # $193,662.50; line 18 28% * $193,662.50 - $2,391 = $51,834.50; line 20 + # $134,250; line 21 $0; line 22 $925,000; line 29 $300,000 - $134,250 = + # $165,750; line 31 $24,862.50; line 33 $759,250; line 34 $151,850; + # line 38 $228,547. Line 39: 28% * $1,118,662.50 - $2,391 = + # $310,834.50. Line 40 and line 7: $228,547. + amt_tax_including_cg: 228_547 + amt_base_tax: 310_834.5 + # Line 9 $228,547 less line 10 $201,779.50 = $26,767.50 (26% on line 18 + # would give $25,285.25). + alternative_minimum_tax: 26_767.5 + income_tax_before_credits: 228_547 diff --git a/policyengine_us/tests/test_form_6251_part_iii.py b/policyengine_us/tests/test_form_6251_part_iii.py new file mode 100644 index 00000000000..2b4cee7eb32 --- /dev/null +++ b/policyengine_us/tests/test_form_6251_part_iii.py @@ -0,0 +1,430 @@ +"""Property and differential tests for Form 6251 Part III (26 U.S.C. 55(b)(3)). + +Section 55(b)(3) caps the tentative minimum tax of a taxpayer with net +capital gain at the tax on the taxable excess less the gains, figured "at the +rates and in the same manner as if this paragraph had not been enacted", plus +the 0, 15, 20 and 25 percent amounts on the gains. "In the same manner" +carries section 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." The 2025 Form 6251 applies the same schedule on line 18 +(to line 17, line 12 less the gains) and on line 39 (to line 12): "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." + +Two kinds of test: + +- Differential. Lines 12 to 40 of the 2025 Form 6251 are transcribed below + and compared, household by household and for every filing status, with + `amt_tax_including_cg` (line 38), `amt_base_tax` (line 39) and + `alternative_minimum_tax` (line 11). Line 13 comes from the household's own + dividends and gains (Qualified Dividends and Capital Gain Tax Worksheet, + line 4), not from the model's worksheet variables. +- Properties that hold for every household and year: + - Part III never exceeds line 39 on the same line 12 by more than + rounding. The gains taxed in Part III are at most 25 percent, below the + 26 percent floor of the line 39 schedule, and line 18 taxes the rest on + the same schedule as line 39. + - With no qualified dividends or capital gain, line 17 equals line 12, so + line 18 equals line 39. + - For filers other than married filing separately, line 18 is the AMT + scale applied with no factor, bit for bit: their multiplier is 1. + +Households with 28-percent rate or unrecaptured section 1250 gain go through +the Schedule D Tax Worksheet, which is not transcribed here; they are +checked only against the properties. YAML unit tests cover those lines. +""" + +import numpy as np +import pytest + +from policyengine_us import CountryTaxBenefitSystem, Simulation + +# Hypothesis is a dev extra; skip rather than fail collection without it. +hypothesis = pytest.importorskip("hypothesis") +st = pytest.importorskip("hypothesis.strategies") + +STATUSES = ["SINGLE", "JOINT", "SEPARATE", "HEAD_OF_HOUSEHOLD", "SURVIVING_SPOUSE"] + +# --------------------------------------------------------------------------- +# The 2025 Form 6251, transcribed. +# --------------------------------------------------------------------------- + +# Line 19. +ZERO_RATE_AMOUNT_2025 = { + "SINGLE": 48_350, + "SEPARATE": 48_350, + "JOINT": 96_700, + "SURVIVING_SPOUSE": 96_700, + "HEAD_OF_HOUSEHOLD": 64_750, +} +# Line 25. +FIFTEEN_PERCENT_RATE_AMOUNT_2025 = { + "SINGLE": 533_400, + "SEPARATE": 300_000, + "JOINT": 600_050, + "SURVIVING_SPOUSE": 600_050, + "HEAD_OF_HOUSEHOLD": 566_700, +} + + +def amt_rates_2025(amount, status): + """Form 6251 (2025) lines 18 and 39, and line 7 "All others".""" + if status == "SEPARATE": + return 0.26 * amount if amount <= 119_550 else 0.28 * amount - 2_391 + return 0.26 * amount if amount <= 239_100 else 0.28 * amount - 4_782 + + +def form_6251_part_iii_2025(line_12, line_13, line_20, status): + """2025 Form 6251, lines 12 to 40, with no Schedule D Tax Worksheet. + + With no Schedule D Tax Worksheet, line 14 is zero, line 15 is line 13 + and line 27 is line 20 (line 5 of the capital gains worksheet). + Returns lines 17, 38, 39 and 40. + """ + line_15 = line_13 + line_16 = min(line_12, line_15) + line_17 = line_12 - line_16 + line_18 = amt_rates_2025(line_17, status) + line_21 = max(0, ZERO_RATE_AMOUNT_2025[status] - line_20) + line_22 = min(line_12, line_13) + line_23 = min(line_21, line_22) + line_24 = line_22 - line_23 + line_28 = line_21 + line_20 + line_29 = max(0, FIFTEEN_PERCENT_RATE_AMOUNT_2025[status] - line_28) + line_30 = min(line_24, line_29) + line_31 = 0.15 * line_30 + line_32 = line_23 + line_30 + line_33 = line_22 - line_32 + line_34 = 0.20 * line_33 + line_38 = line_18 + line_31 + line_34 + line_39 = amt_rates_2025(line_12, status) + return line_17, line_38, line_39, min(line_38, line_39) + + +def capital_gains_worksheet_line_4(household): + """Qualified Dividends and Capital Gain Tax Worksheet, lines 2 to 4. + + Line 3 is the smaller of Schedule D lines 15 and 16, or zero if either + is a loss. + """ + long_term = household["long_term_gains"] + net_gain = max(0, min(long_term, long_term + household["short_term_gains"])) + return household["qualified_dividends"] + net_gain + + +# --------------------------------------------------------------------------- +# The model. +# --------------------------------------------------------------------------- + +OUTPUTS = [ + "filing_status", + "taxable_income", + "regular_tax_before_credits", + "capital_gains_tax", + "amt_income_less_exemptions", + "amt_tax_including_cg", + "amt_base_tax", + "alternative_minimum_tax", +] + + +def build_situation(households, year): + people, tax_units, marital_units = {}, {}, {} + groups = {"households": {}, "spm_units": {}, "families": {}} + for i, h in enumerate(households): + head = f"head_{i}" + long_term = h["long_term_gains"] + people[head] = { + "age": {year: 45}, + "employment_income": {year: h["wages"]}, + "qualified_dividend_income": {year: h["qualified_dividends"]}, + "long_term_capital_gains": {year: long_term}, + "short_term_capital_gains": {year: h["short_term_gains"]}, + # Schedule D line 18 is part of the long-term gain. + "long_term_capital_gains_on_collectibles": { + year: max(0, long_term) * h.get("collectibles_share", 0) / 100 + }, + "real_estate_taxes": {year: h.get("real_estate_taxes", 0)}, + } + members = [head] + marital_units[f"marital_unit_{i}"] = {"members": [head]} + if h["status"] == "JOINT": + spouse = f"spouse_{i}" + people[spouse] = {"age": {year: 45}} + members.append(spouse) + marital_units[f"marital_unit_{i}"]["members"].append(spouse) + if h["status"] in ("HEAD_OF_HOUSEHOLD", "SURVIVING_SPOUSE"): + # Old enough to be a dependent without a child tax credit. + child = f"child_{i}" + people[child] = {"age": {year: 17}} + members.append(child) + marital_units[f"marital_unit_{i}_child"] = {"members": [child]} + tax_units[f"tax_unit_{i}"] = { + "members": members, + # Set for every tax unit: an input given to only some of them + # leaves the rest at the default (single). + "filing_status": {year: h["status"]}, + "unrecaptured_section_1250_gain": { + year: max(0, long_term) * h.get("section_1250_share", 0) / 100 + }, + } + groups["households"][f"household_{i}"] = { + "members": members, + "state_code": {year: "TX"}, + } + groups["spm_units"][f"spm_unit_{i}"] = {"members": members} + groups["families"][f"family_{i}"] = {"members": members} + return { + "people": people, + "tax_units": tax_units, + "marital_units": marital_units, + **groups, + } + + +def calculate(households, year): + simulation = Simulation(situation=build_situation(households, year)) + results = {} + for v in OUTPUTS: + values = simulation.calculate(v, year) + results[v] = ( + values.decode_to_str() if v == "filing_status" else np.asarray(values) + ) + return results + + +def tolerance(*amounts): + """Five cents, plus the rounding of single-precision arithmetic. + + The model stores amounts as 32-bit floats, which carry about seven + significant digits. + """ + return 0.05 + 5e-7 * np.max(np.abs(amounts), axis=0) + + +def assert_matches_form_6251(households, law): + """The 2025 model against the transcribed 2025 Form 6251.""" + for i, h in enumerate(households): + status = law["filing_status"][i] + assert status == h["status"], (i, status) + line_12 = float(law["amt_income_less_exemptions"][i]) + line_13 = capital_gains_worksheet_line_4(h) + line_20 = max(0, float(law["taxable_income"][i]) - line_13) + _, line_38, line_39, line_40 = form_6251_part_iii_2025( + line_12, line_13, line_20, status + ) + part_iii = float(law["amt_tax_including_cg"][i]) + base = float(law["amt_base_tax"][i]) + assert part_iii == pytest.approx(line_38, abs=tolerance(line_12)), (i, h) + assert base == pytest.approx(line_39, abs=tolerance(line_12)), (i, h) + # Lines 9 to 11, with no foreign tax credit and no Form 4972. + line_10 = float(law["regular_tax_before_credits"][i]) + float( + law["capital_gains_tax"][i] + ) + amt = max(0, line_40 - line_10) + assert float(law["alternative_minimum_tax"][i]) == pytest.approx( + amt, abs=tolerance(line_12, line_10) + ), (i, h) + + +def assert_properties(households, law): + part_iii = law["amt_tax_including_cg"].astype(float) + base = law["amt_base_tax"].astype(float) + line_12 = law["amt_income_less_exemptions"].astype(float) + # Line 38 never exceeds line 39 on the same line 12. + assert (part_iii <= base + tolerance(line_12)).all() + # No dividends or gains: line 17 is line 12, so line 18 is line 39. + no_gains = np.array( + [ + h["qualified_dividends"] == 0 + and h["long_term_gains"] <= 0 + and h["short_term_gains"] <= 0 + for h in households + ] + ) + assert np.all(np.abs(part_iii - base)[no_gains] <= tolerance(line_12)[no_gains]) + + +# --------------------------------------------------------------------------- +# Line 18 as a schedule. +# --------------------------------------------------------------------------- + +SYSTEM = CountryTaxBenefitSystem() +FILING_STATUS = SYSTEM.variables["filing_status"].possible_values + + +@hypothesis.settings(max_examples=200, deadline=None) +@hypothesis.given( + st.lists( + st.one_of( + st.integers(0, 10_000_000), + st.floats(0, 1e7, allow_nan=False, allow_infinity=False), + ), + min_size=1, + max_size=40, + ), + st.integers(2013, 2035), +) +def test_line_18_schedule(amounts, year): + p = SYSTEM.parameters(f"{year}-01-01").gov.irs.income.amt + line_17 = np.repeat(np.array(amounts, dtype=float), len(STATUSES)) + statuses = np.tile(np.array(STATUSES), len(amounts)) + filing_status = FILING_STATUS.encode(statuses) + line_18 = p.brackets.calc(line_17, factor=p.multiplier[filing_status]) + separate = statuses == "SEPARATE" + # Everyone else: bit for bit the scale with no factor. + assert np.array_equal(line_18[~separate], p.brackets.calc(line_17[~separate])) + # Married filing separately: the breakpoint is halved (55(b)(1)(C)). + breakpoint = p.brackets.thresholds[-1] / 2 + low, high = p.brackets.rates + expected = np.where( + line_17 <= breakpoint, + low * line_17, + high * line_17 - (high - low) * breakpoint, + ) + assert line_18[separate] == pytest.approx(expected[separate], rel=1e-12, abs=1e-6) + # Halving the breakpoint never lowers the tax, and raises it by the + # rate difference on the part of line 17 between the two breakpoints. + full = p.brackets.calc(line_17[separate]) + assert line_18[separate] - full == pytest.approx( + (high - low) * np.clip(line_17[separate] - breakpoint, 0, breakpoint), + rel=1e-9, + abs=1e-6, + ) + + +# --------------------------------------------------------------------------- +# Households. +# --------------------------------------------------------------------------- + +GRID = [ + { + "status": status, + "wages": wages, + "qualified_dividends": dividends, + "long_term_gains": long_term, + "short_term_gains": short_term, + } + for status in STATUSES + for wages in (0, 60_000, 150_000, 260_000, 700_000) + for dividends, long_term, short_term in ( + (0, 0, 0), + (3_000, 0, 0), + (0, 50_000, 0), + (10_000, 40_000, -15_000), + (0, 30_000, 20_000), + (25_000, 900_000, 0), + (0, 1_500_000, 0), + (0, -8_000, 5_000), + ) +] + + +def test_grid_matches_form_6251_2025(): + law = calculate(GRID, 2025) + assert_matches_form_6251(GRID, law) + assert_properties(GRID, law) + # The grid reaches separate filers whose line 17 is above the separate + # breakpoint and below the full one, where Part III sets the AMT. + reached = False + for i, h in enumerate(GRID): + line_12 = float(law["amt_income_less_exemptions"][i]) + line_13 = capital_gains_worksheet_line_4(h) + line_17 = line_12 - min(line_12, line_13) + reached |= ( + h["status"] == "SEPARATE" + and 119_550 < line_17 < 239_100 + and law["amt_tax_including_cg"][i] < law["amt_base_tax"][i] + and law["alternative_minimum_tax"][i] > 0 + ) + assert reached + + +def test_separate_filer_with_line_17_of_136_162_50(): + # 2025, married filing separately: $104,000 of wages, $25,000 of + # qualified dividends and $900,000 of long-term gain. Form 6251 line 4 + # is $1,029,000 plus 25% of the excess over $900,350 ($32,162.50), so + # line 12 is $1,061,162.50 (no exemption is left) and line 17 is + # $136,162.50. That is above $119,550, so line 18 is 28% of it less + # $2,391, $35,734.50; 26% of it would be $35,402.25, $332.25 less. + household = { + "status": "SEPARATE", + "wages": 104_000, + "qualified_dividends": 25_000, + "long_term_gains": 900_000, + "short_term_gains": 0, + } + law = calculate([household], 2025) + line_12 = float(law["amt_income_less_exemptions"][0]) + assert line_12 == 1_061_162.5 + line_17, line_38, line_39, _ = form_6251_part_iii_2025( + line_12, 925_000, float(law["taxable_income"][0]) - 925_000, "SEPARATE" + ) + assert line_17 == 136_162.5 + assert amt_rates_2025(line_17, "SEPARATE") == pytest.approx(35_734.5) + assert line_38 < line_39 + assert_matches_form_6251([household], law) + assert law["alternative_minimum_tax"][0] > 0 + + +household_strategy = st.fixed_dictionaries( + { + "status": st.sampled_from(STATUSES), + "wages": st.integers(0, 1_500_000), + "qualified_dividends": st.one_of(st.just(0), st.integers(1, 400_000)), + "long_term_gains": st.one_of(st.just(0), st.integers(-50_000, 2_000_000)), + "short_term_gains": st.one_of(st.just(0), st.integers(-100_000, 300_000)), + "real_estate_taxes": st.one_of(st.just(0), st.integers(1, 60_000)), + } +) + +# Adds 28-percent rate gain (collectibles) and unrecaptured section 1250 +# gain, as shares of the long-term gain. +household_with_schedule_d_strategy = st.builds( + lambda h, collectibles, section_1250: { + **h, + "collectibles_share": collectibles, + "section_1250_share": section_1250, + }, + household_strategy, + st.one_of(st.just(0), st.integers(1, 100)), + st.one_of(st.just(0), st.integers(1, 100)), +) + +SETTINGS = dict( + max_examples=10, + deadline=None, + suppress_health_check=[ + hypothesis.HealthCheck.too_slow, + hypothesis.HealthCheck.data_too_large, + ], +) + + +@hypothesis.settings(**SETTINGS) +@hypothesis.given(st.lists(household_strategy, min_size=1, max_size=25)) +def test_random_households_match_form_6251_2025(households): + assert_matches_form_6251(households, calculate(households, 2025)) + + +@hypothesis.settings(**SETTINGS) +@hypothesis.given( + st.lists(household_with_schedule_d_strategy, min_size=1, max_size=25), + st.sampled_from([2018, 2022, 2025, 2026, 2030]), +) +def test_random_households_keep_the_properties(households, year): + # Each household with its dividends and gains removed as well, so that + # line 17 is line 12 for some filers of every status. + no_gains = [ + { + **h, + "qualified_dividends": 0, + "long_term_gains": 0, + "short_term_gains": 0, + } + for h in households + ] + households = households + no_gains + assert_properties(households, calculate(households, year)) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/amt_tax_including_cg.py b/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/amt_tax_including_cg.py index b9b6013feac..2205ce58ec1 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/amt_tax_including_cg.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/amt_tax_including_cg.py @@ -30,11 +30,15 @@ def formula(tax_unit, period, parameters): capped_income = min_(capped_capital_gains, reduced_income) # Line 17: Line 12 minus Line 16 (ordinary AMTI). excess_income = max_(0, reduced_income - capped_income) - # Line 18: apply the 26%/28% AMT bracket to Line 17. + # Line 18: apply the 26%/28% AMT bracket to Line 17. The 28% rate + # starts at half the breakpoint for married filing separately + # (26 U.S.C. 55(b)(1)(C)), as on Line 39. p = parameters(period).gov.irs - income_taxes_at_amt_rates = p.income.amt.brackets.calc(excess_income) - # Line 19: 0% LTCG bracket threshold for the filing status. 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] + ) + # Line 19: 0% LTCG bracket threshold for the filing status. cg_bracket = p.capital_gains.thresholds["1"][filing_status] # Line 20: amount from QDCG Worksheet line 5 or Schedule D Tax # Worksheet line 14 (as figured for the regular tax). This is the From bc77b87e7be2e72e0e7eda290441c4db0036b5e8 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 17:39:39 -0400 Subject: [PATCH 2/6] Add hypothesis to the dev extras so CI runs the Part III property tests 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 PolicyEngine/policyengine-us#9761; locked with uv 0.12.13, the version CI uses. Co-Authored-By: Claude Opus 5.5 --- pyproject.toml | 1 + uv.lock | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index ada6b6fa3ca..40ba2d3950f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ dev = [ "setuptools>=80.9.0", "build>=1.2.2.post1", "towncrier>=24.8.0", + "hypothesis>=6.100.0", ] [tool.hatch.build.targets.wheel] diff --git a/uv.lock b/uv.lock index b84171deda0..4f169a24605 100644 --- a/uv.lock +++ b/uv.lock @@ -766,6 +766,94 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/07/3d0c34c345043c6a398a5882e196b2220dc5861adfa18322448b90908f26/huggingface_hub-1.3.4-py3-none-any.whl", hash = "sha256:a0c526e76eb316e96a91e8a1a7a93cf66b0dd210be1a17bd5fc5ae53cba76bfd", size = 536611, upload-time = "2026-01-26T14:05:08.549Z" }, ] +[[package]] +name = "hypothesis" +version = "6.168.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/b7/13118bbc45d6d8b9d04e2de779e2a4ff23145ea39efa692b994fb874ca72/hypothesis-6.168.3.tar.gz", hash = "sha256:a43388f9067678fef6e13bdff325b6cfa6961a590498bb37f7ff31589c83bc75", size = 511022, upload-time = "2026-09-28T05:20:58.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/cd/746a2fc1e5e5ba34f07ea6ee1ad07525a3dde5ff4836db3a7980af49a891/hypothesis-6.168.3-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d20972ca134e652e9928ecd200966a8a2857adc2812c1d74b12a872e5cd503eb", size = 791536, upload-time = "2026-09-28T05:18:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0f/7a158e377b69556c8e12c25c8fd811d0103e54c24a12dab1f3b9819f202e/hypothesis-6.168.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:eafbec09d3e87d13d8242411d1f5868b5e879f1bd6d95e233e9ff80c527bea1c", size = 787313, upload-time = "2026-09-28T05:20:18.122Z" }, + { url = "https://files.pythonhosted.org/packages/45/c6/7df9104ee359e8fcd77791781c47e70794964ca69673665e0de2a6590f4c/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73c5627497968cc62d140e9fde1ea21e12cac7b64dc419fea8286cd34ff1ad4e", size = 1124036, upload-time = "2026-09-28T05:20:04.372Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/8a61715404a73b9a82cb1f976803428d603cdea976a9c30266c72ad6a7ce/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dc56e6dc6eeb0aeb08ac463f847279bde2336c4786faf7ee0af4b93cd031a7", size = 1147875, upload-time = "2026-09-28T05:19:15.495Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3f/2b16e95cc3b9a069afa23830a012aefe377ce4a457a227e5a5026eb827ff/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:753bb501f8560d2e321ed3b62596b4496c56f15668b0a0669231ccc3e6c4e80d", size = 1149489, upload-time = "2026-09-28T05:19:18.886Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e7/d7cf6dc068bb02732b2a6e38a35a0dcb7f2137608198fc79d740f69d197c/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71ab606c472449cb872ef2a7acaec679ee4f651b7f0a477a04ebc85d8933ef8c", size = 1191992, upload-time = "2026-09-28T05:19:21.83Z" }, + { url = "https://files.pythonhosted.org/packages/5f/26/f1e5b25dec15998e8375d722825ec3dae1075cbe7cb1f74221b2312a2e33/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26928956c54748e4dfa587333741ab750246123b93484955646ff316cca27eef", size = 1169911, upload-time = "2026-09-28T05:19:27.996Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/901234a49147e6bf8a5b9e54b775706a223c2b52befea1106ea2aaf15d48/hypothesis-6.168.3-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:0bdfc53c041b61c854fa3761735736b991bc2bddafee45a361cfe4d43027c1ef", size = 1129406, upload-time = "2026-09-28T05:18:42.632Z" }, + { url = "https://files.pythonhosted.org/packages/55/c1/5fc9ff91ec9fba6bb527833386c66812524b5cc75fe7d4a4f9122ee1c420/hypothesis-6.168.3-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:650528e2b1e2a45e95c2df624d4b9364b8ac5a027ba84e1eea2bc5398dd01dcd", size = 1160399, upload-time = "2026-09-28T05:20:23.911Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d2/49ad1ef5c55547c8abfd0fc571b3493806dda530fd5cb3d8263dbad86a72/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:209dc54cdb1b4d6d7020ad8d09e44c49756361b7183adacea4d6f5705085b595", size = 1299906, upload-time = "2026-09-28T05:18:58.417Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5f/f98a26094c4f462c4215807bed0f9a6b5508b27a3c329bcba1bc8adc7ac7/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:af8ca98cd11dc7f9427bb90483abcd9688d4df2e8e63893a30a2423b027ebb11", size = 1425538, upload-time = "2026-09-28T05:20:49.074Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b8/c5b4406e3a6591e51a42f85469351d1d824e0a54b84167aec9b2353759fe/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:f8122cfdd0bc0ba843063effa22ac310bdebdb3a1319bf1e63f14baa119c72ab", size = 1377084, upload-time = "2026-09-28T05:20:06.618Z" }, + { url = "https://files.pythonhosted.org/packages/e0/01/0d84a8ea469d024c15f602799c0b8410fbe03d99fdb0370449b33c3c916f/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4bedbb379eab34f792af7ee9a05aae04e9c08bbb52e0f90f5a2110d4fe4b2fbd", size = 1281162, upload-time = "2026-09-28T05:19:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/22/b5/ea6435038de1a795f005cb603531e8ccc053febc50180f3ab55583c9c60e/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:5a953115b9f5c95133ab2d04efffeec96e5658c3207923dca7285f7db3e6bbef", size = 1300433, upload-time = "2026-09-28T05:18:55.634Z" }, + { url = "https://files.pythonhosted.org/packages/6c/78/fa6c77d9f64b6d592e69e582d83cbce7d7e56897faa4762885a2123b0166/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6173558e676ad25ed1e20507fa4024a0d816dd90f715f77006e5a28f19109026", size = 1336273, upload-time = "2026-09-28T05:19:08.273Z" }, + { url = "https://files.pythonhosted.org/packages/f1/56/4acd0d778bab818c9ea336fcba768bf43bc70db7f84cc373579298df4641/hypothesis-6.168.3-cp310-abi3-win32.whl", hash = "sha256:dc66390fb12d80585aa9222bf538ce8b7aa22cf5d118250647355a1c9e8f62f4", size = 678211, upload-time = "2026-09-28T05:19:23.355Z" }, + { url = "https://files.pythonhosted.org/packages/b0/db/0a02b146ad1c30f9716362f2e1a379dfd6b0f7be03ac1e22c9da24c9e2a9/hypothesis-6.168.3-cp310-abi3-win_amd64.whl", hash = "sha256:92325b276360fe86c5bf71a568c0d53a6d140b0de36dcf029f9164a17803bb24", size = 684906, upload-time = "2026-09-28T05:18:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c0/958deaf726848f96f52250740bf39f13f476b068e22f73e2b358eaa07532/hypothesis-6.168.3-cp310-abi3-win_arm64.whl", hash = "sha256:3cf6f1eeaf41cd8d60cf1f88fde905ca1dd77c906929a507d6ac7f66f2ccba2a", size = 683292, upload-time = "2026-09-28T05:19:57.404Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ab/443208c2988a24eaafbfbd6791b897ad9a8a1dd7f870d5b455d7d15a28ad/hypothesis-6.168.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9029775dc2e25e3e8e596c4306926f0079158353631c7c52c285bbc5c8073c2b", size = 792239, upload-time = "2026-09-28T05:19:59.144Z" }, + { url = "https://files.pythonhosted.org/packages/cf/7b/619c1a76c52de2d3cfd68a2f9876c09464d63097db5bf86a28b62dc37bb1/hypothesis-6.168.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e1d91530cefdb9e0b6467c203eb509c61a11d70480a37db59cb53a2f9913102", size = 788113, upload-time = "2026-09-28T05:19:17.145Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dd/33841e8833105f850214a486d83740ba52041892ca94e687a05829c0ce83/hypothesis-6.168.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0df00cbe8133aa220308d11fa28e3add996cf5102a39b279c1d711015fd9114b", size = 1124241, upload-time = "2026-09-28T05:18:37.589Z" }, + { url = "https://files.pythonhosted.org/packages/4b/43/c84a7900feced6a420e824ab61d316e4f59a6887d991bc143ecc294a09ed/hypothesis-6.168.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd7f75a2e23288ee82ee965a952473d9c5447c2cbc1afc94be09d2402201774e", size = 1170406, upload-time = "2026-09-28T05:19:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/47/5d/6e2a32b9c3020af7a06b00fe446a2c5155ae76ac468c0b68d7188e524002/hypothesis-6.168.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc2b1c37631f2231dd0d077225aa5908fc2bd34a4c3faf475053b0bc2eb24c95", size = 1300339, upload-time = "2026-09-28T05:20:31.968Z" }, + { url = "https://files.pythonhosted.org/packages/f9/78/19f3c9eb9a5c215b9fbde57be18ef7c1c78afa509b460d4e8f86026d741a/hypothesis-6.168.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:73960a58f6efc8cbc57d8ad0647955f7c5b25f3d562e3eaa57afa1c69894f7aa", size = 1336484, upload-time = "2026-09-28T05:19:02.841Z" }, + { url = "https://files.pythonhosted.org/packages/88/bb/d46c4519e989e022667705d8a56e83a09ad4ea50eae07ca4bb0f4708dd23/hypothesis-6.168.3-cp311-cp311-win_amd64.whl", hash = "sha256:3f1122223759acc0fe5301c1ae505d762ee61af91db7b191a791ccdbcd965462", size = 684646, upload-time = "2026-09-28T05:20:00.842Z" }, + { url = "https://files.pythonhosted.org/packages/7a/56/44a17266bcf1add5fba7c3df5c3920396a5e924d917fb0534a6f9e2b9b9a/hypothesis-6.168.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bff12e036b67abc5ded682f724f4d7afaff5223190270a0763695f218c079068", size = 793313, upload-time = "2026-09-28T05:20:22.045Z" }, + { url = "https://files.pythonhosted.org/packages/d8/18/1a25b11e9133cb54c02ee6cf99a61296ad461aabc7aae3399e1d0ff1f095/hypothesis-6.168.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c73c6188056e6dc110260e39d141c69c3d02f23e3ba1d8610c7a8b6f2510ec96", size = 784855, upload-time = "2026-09-28T05:19:34.496Z" }, + { url = "https://files.pythonhosted.org/packages/8b/da/728d3210f0b0089337f37206a100d31ef546d1f99839e709f0a9ca443f8e/hypothesis-6.168.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb1063cb794765097b8c0add598bd34a904f44dc15db81275dd1f0ed1ac567ed", size = 1123043, upload-time = "2026-09-28T05:19:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/53/86/bfb9ec344a8b3ef2fa6f59bb3d30c5b3a7d86d594107670539c34ee53d1b/hypothesis-6.168.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b2d47f5d9060b049039bef0b267e88480f6468f95ee394e514e84a81ccdc5f5", size = 1169121, upload-time = "2026-09-28T05:20:40.512Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e0/5a76dbafbcaf8fff075f09377cddefa17289ef05dbf51878dde88385b966/hypothesis-6.168.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8367f623a98cbf90f33fd2a43b100671b152b5975bc343a578290945f52f7018", size = 1298749, upload-time = "2026-09-28T05:19:32.883Z" }, + { url = "https://files.pythonhosted.org/packages/28/15/0e65af96b353f2c4bdc283c53ed0980e5ac5009afe97ef3b23c77b364664/hypothesis-6.168.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd81241f4cd76fb18d481e87b9688b30a0cb5c30841730513323ba1c7aba1837", size = 1335271, upload-time = "2026-09-28T05:20:54.005Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c5/37eab2155664aa544df321f6d3940b89c6702d79a72fdc98d7b5a2286f8c/hypothesis-6.168.3-cp312-cp312-win_amd64.whl", hash = "sha256:377438de53afb94347d9845b7d90db5905c6d2a8d612108e9938e0f80503bb90", size = 682211, upload-time = "2026-09-28T05:18:51.206Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a2/6787da846d929e52fc3344d803299c45782dbeac287528af08380e984bf8/hypothesis-6.168.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1b230a850de63334c16654a34a2d547e0179d36b9071d4439b3e7237f6d077e7", size = 793254, upload-time = "2026-09-28T05:19:36.294Z" }, + { url = "https://files.pythonhosted.org/packages/89/ba/2893f5ca42501f4d562cba3229c8994c3a3e5fac66ef60c1e0e0b5220a5d/hypothesis-6.168.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d63b0226cd3e0d8bdd97c3384b22a21934ed4d53246c1c93575dada616672499", size = 784699, upload-time = "2026-09-28T05:18:54.194Z" }, + { url = "https://files.pythonhosted.org/packages/b7/aa/7d7349daf75b71f6f35876f8de115e974c5c04d600e86df1b2779b0883ab/hypothesis-6.168.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0369f5df055f96e117ab12e5f249668ff144731ab5280bc7a205fdf81f990b89", size = 1123016, upload-time = "2026-09-28T05:20:36.372Z" }, + { url = "https://files.pythonhosted.org/packages/00/f0/7774e1ea072708ea46cb25c4aeee5f9978b6c271764809069e8a6789858e/hypothesis-6.168.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f076bcd0f77fdcdb7797826c879099573d03a02e228ebe649ea917081b962ac0", size = 1168989, upload-time = "2026-09-28T05:20:08.44Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7d/113992abed9efbd7944e3a496a382da58152dce126fddf6419ff31a0a57d/hypothesis-6.168.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b823ba1fcec8da730f29316d010b06d3f7e0c3828dcf91020e24c55e7d24652a", size = 1298626, upload-time = "2026-09-28T05:19:40.986Z" }, + { url = "https://files.pythonhosted.org/packages/d5/65/3659fa5e733027e5b37a27486e57f4053535e40853d23501c422bb275043/hypothesis-6.168.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dd2849c269d674e4618590f3b48d443bd4c06b5aef3d8d869086c2e6d213d248", size = 1335184, upload-time = "2026-09-28T05:20:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/b4/6c/35aab2221b5ea65125340f236e1a765a9e9f3b28d89a389ea0033fed29f7/hypothesis-6.168.3-cp313-cp313-win_amd64.whl", hash = "sha256:3ef7d26f5789e691401d5f87eafed9bd2763f0dbe47af6d6b66012a509404766", size = 682173, upload-time = "2026-09-28T05:19:12.762Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/27b0cb56ff5d2bc92458bf6fcebdb1f0dc01f57f043a178e9f9d74498169/hypothesis-6.168.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e2d4c68729a13df9af4998d2652cfb5d541c5609b88c306880a5dfb284938ec2", size = 793287, upload-time = "2026-09-28T05:20:38.522Z" }, + { url = "https://files.pythonhosted.org/packages/e8/bd/5342f95c3bc36586777ef8cb8eba87ac7acf0184210327fb0e6d8654e295/hypothesis-6.168.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b345f818083ec99966a43ca4f7b38feb62c6920ce28572bc2bde4948a8b7eaba", size = 784857, upload-time = "2026-09-28T05:20:42.609Z" }, + { url = "https://files.pythonhosted.org/packages/4a/5f/fc774241518a0588d362680a3084275058aab86d6bc02a2c61e1073142d0/hypothesis-6.168.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0608c610fc002978fc5de0f471770d8817e9455cb8649a47983c9f8bccfc1897", size = 1123330, upload-time = "2026-09-28T05:19:29.735Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/131f16775a3dca5f4fe27f0d6ad9a9851600098a54a872d163f6ebf0b664/hypothesis-6.168.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dcf6448b1ecc37f2b23f2d1b3ddfc9ff6b6910614c15a642dc82f419b96037b", size = 1169178, upload-time = "2026-09-28T05:19:53.436Z" }, + { url = "https://files.pythonhosted.org/packages/06/61/c9f5bff8b73321c9fd12f2d69666c6e4861b97b60f5f24fdd6ac293007fd/hypothesis-6.168.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ae4f9f094041dcce5119ebd7bab71062743ab02b6e654d056b370beda78c19e2", size = 1299127, upload-time = "2026-09-28T05:20:14.486Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6d/d4618f8ab12dd76c4d58ea052252759aa2bf0c4e71f4b502ba5d577e0b9f/hypothesis-6.168.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d479985fe73af97badfb72dc6d20c6a353e486a36f6d065f600026e0cf954a86", size = 1335390, upload-time = "2026-09-28T05:20:10.326Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/727161e17cc297df1aeea945de05c532033b66751625bec192bc5b5ff707/hypothesis-6.168.3-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:785e2c45f8c08e274b4bf1ccae97f1a4e09407e9a68e80790cfab1d17a4a45fa", size = 624291, upload-time = "2026-09-28T05:18:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/77/23/2f9b506392a0b8712440bcb781abc5713be9ffac1303b6d10f3669050882/hypothesis-6.168.3-cp314-cp314-win_amd64.whl", hash = "sha256:320920b1e3dae8611eee8a03d063cf2187446f2a17c38cfb8a7fc1466f71eee2", size = 682064, upload-time = "2026-09-28T05:20:16.336Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/efabfd95eb2b69c0c9fa1e1c83c8290aa2715d46baeb5a7764faf09c133a/hypothesis-6.168.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:35380baa981108a7f60c4eab71e46acd8d8f58440520346a1a6aba06dca7e074", size = 791875, upload-time = "2026-09-28T05:19:37.789Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fc/2a0ada1623a9a33048bbf9f00a7c92513398c6b8865cba5efb854262ce30/hypothesis-6.168.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f0aaaed00438fa6673d856aaee12a4a8afbde82a9f3c5ff487cacfb064239afe", size = 783412, upload-time = "2026-09-28T05:18:48.266Z" }, + { url = "https://files.pythonhosted.org/packages/41/31/73c615d37eeb12da0209555612c32ced628d860267c23cfd2691c6207aea/hypothesis-6.168.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27e6df1576bf838e7f484d4ae5cba92114497ca30afb917056bf1ea33e95675", size = 1121662, upload-time = "2026-09-28T05:19:26.225Z" }, + { url = "https://files.pythonhosted.org/packages/92/54/1893344a3b8bbf83fa7c9bb7e96f6836580213854f46671cb749f7b4706c/hypothesis-6.168.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bc85014577982ec6d2e266edc5cd6e7a7d3674c648791ca983c67a52f89a0c4", size = 1167761, upload-time = "2026-09-28T05:18:49.85Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/a61d82968febd25a0f08ffbbfabfaeead66f68fc5fa73f3c9ece1c36fc8f/hypothesis-6.168.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ccfc29505aa1cdcc254cb9cd701fd0811fef5480addd97d4127a00df023410f7", size = 1297309, upload-time = "2026-09-28T05:19:51.711Z" }, + { url = "https://files.pythonhosted.org/packages/bc/01/fe8e4cf6d39d02f4efadaa351427fa13722086cfc54a2da3579741c5177a/hypothesis-6.168.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32d0699566aaa93f9e97a44705d7164386f1e91de78d277f53bada35e78bcc96", size = 1334260, upload-time = "2026-09-28T05:19:00.121Z" }, + { url = "https://files.pythonhosted.org/packages/39/6a/09177ebc62f4778f94379ecade6a51299d3d8c4f4b75ba636bf3a0202364/hypothesis-6.168.3-cp314-cp314t-win_amd64.whl", hash = "sha256:28d88fa174ecbd4ecbd7bb290f06d0db084a3971c3f511ae2830b65b5e25500f", size = 681992, upload-time = "2026-09-28T05:19:48.043Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f6/890bf33d608cd63348d3146a3ca83362fbf09e589c5e5230a00c403070f4/hypothesis-6.168.3-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:61f5782d807b1e6aa5c9beef1054cb2037e7d1add48a64972cd6ee447778781f", size = 791231, upload-time = "2026-09-28T05:18:43.998Z" }, + { url = "https://files.pythonhosted.org/packages/03/dd/fc36b204f8aa7437c676576d9437f0422de46aa8a729e6cd5bbb0224e759/hypothesis-6.168.3-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:7aacf3cf40c7ce8f9e4924d5b57bc0b068beafdfed2347bcf160a28b4cfbba7b", size = 783171, upload-time = "2026-09-28T05:19:20.353Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d2/3cd087d577db67c404991d7723e64d7cd75570cdc82a0f7329aed6f2086c/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01768a03a30dc54df7fe457c0b34016c84d598fa00d5965ededab93ba4eb3408", size = 1121152, upload-time = "2026-09-28T05:18:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/42/6f/a05a68cc66e7a22701c50bf94fb9ddeb36f4f5d0de3cc130066709eda621/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1a8a4ffc6c6e6e577f2bfbcfebf7cffbb310283ba2532c729570a0a752cebaaa", size = 1144069, upload-time = "2026-09-28T05:20:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f4/fdd7a093fb2fb3a0ce24256c92ad5bf67936f4669bd06d5ceae4298150ec/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b987d73eba95183a7e59cca6d1925c588aa4307922d9852cc2b4d282a2ae4128", size = 1146692, upload-time = "2026-09-28T05:20:25.83Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/6dbd4377e935505ae4fc8ee4c7b18c69994c4bbaca015447c470218bdbee/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4569c39bd97d9573e946429ed676f3b55a7c8ed80920addd67d384a47d59b38", size = 1189480, upload-time = "2026-09-28T05:20:46.734Z" }, + { url = "https://files.pythonhosted.org/packages/ba/bf/ff288b496b690000d2686dcfa7f67855e4c5a6dddb464ed8e4880be83bb2/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6042b8707a4b25bbbfe20b110258fb7549a68951e5525608a8ce06091039e9fc", size = 1167109, upload-time = "2026-09-28T05:19:24.758Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d1/3fee2bc29fc747fabbb27e174515592e390809ca465d3bbbabbcfa3235a8/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:f413629de94d38a7a2ad259697a6752526143cb49c2e2d7bdba19381c80693f6", size = 1126823, upload-time = "2026-09-28T05:18:52.636Z" }, + { url = "https://files.pythonhosted.org/packages/58/6a/585294392fa6a6d9719446a042a777ba98ecca265d9e48cbedc10c88df0f/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f071737e4e775bebba07e319eb645a880d1e0186b4d24bad23255e849d86d483", size = 1155773, upload-time = "2026-09-28T05:20:51.268Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4d/fa283ff79996debf1ff08593e01f8b773eb8211b19b3d6d5d491317a65bf/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:f59a3912858d0609c26054aa1c474847c797937f5e0e1c77f0d3f08032e50f09", size = 1296671, upload-time = "2026-09-28T05:18:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/42/05/98f9c2f628da5afabb6c3e8df5d2d299d2d50bf306b14a6e26d3954c4951/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:f09c05a23a8025dd5cad07c2ed299a46778e72d49ff0dd5bf353bcc0f7dc1580", size = 1422001, upload-time = "2026-09-28T05:19:04.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c9/f2109ade29a7ec284d55bca328d784743e8b13e321576fd34ee7e65f99af/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:44ace770bda3a0301739fc5a413c764de790c739df1f1a7218dd049f8594d9f5", size = 1374182, upload-time = "2026-09-28T05:20:27.764Z" }, + { url = "https://files.pythonhosted.org/packages/ce/6d/e05d5f72441564a3bebc71fa155deafd0fd3b015d6014ec8a00edf6e42bb/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:03b131043608f94a2578a2a896a1a72092acb7079815eef5b8513b08447e0e62", size = 1278402, upload-time = "2026-09-28T05:20:56.332Z" }, + { url = "https://files.pythonhosted.org/packages/96/d7/6988a7f1f69c5c530687dce03a32c157e2d878e3a2e32af9269ce016b78a/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:e9784aca26eddfe99b03a0292320db742cc8a74200ef864fdce949f526f973cd", size = 1297770, upload-time = "2026-09-28T05:19:11.387Z" }, + { url = "https://files.pythonhosted.org/packages/a2/28/5bb82b60b836bd2329e1fe01ad94efc2b14dfa806f8e50ed14b795d78770/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:2b52ac363096232bebc2add117e9178d91f4248f4cdb919fd1026b5f86a4bb16", size = 1333977, upload-time = "2026-09-28T05:20:02.483Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7d/d419841b8f65481ea1a50c4ba36f2670d48b8c7a51e4e8586089564cf7f6/hypothesis-6.168.3-cp315-abi3.abi3t-win32.whl", hash = "sha256:d28e3a6b511a74ce37df5274b51f36c2b274fea7365e00b16f0c81e22acd5957", size = 675394, upload-time = "2026-09-28T05:19:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/1de6a2ad100e44817f2e9a8e8ba3eeaf4769621f4f87f2d4966c4971fcc4/hypothesis-6.168.3-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:7b9638789548361a57d984f56619ac694a914c328181d911271618409261ff4a", size = 681699, upload-time = "2026-09-28T05:19:09.957Z" }, + { url = "https://files.pythonhosted.org/packages/c0/79/be3370fd02734d6b1d950183ae9224580d19fa8d356b95348d60f1e3eeb7/hypothesis-6.168.3-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:65d78e4357ec48ed2c67825f06740ee3599be4cfe770a6092bed07108d679ac5", size = 679849, upload-time = "2026-09-28T05:19:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0e/27960db1d45a94497e1681fb9d7b9140b44da4e655ba20f0d82f95d10230/hypothesis-6.168.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:101531b4ccf7fa12965a6b295d10b64c2f8a8fab61e1f9b18c55335d8fb02575", size = 793135, upload-time = "2026-09-28T05:19:14.056Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cb/be08379286c1925fcd651b92ed835f470a83b859b5c484b3431da9d851f5/hypothesis-6.168.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:41e0120814de5c3a6b58d8cb27cb11b8873ab33fba130cd09855a27f5a12a1ca", size = 788995, upload-time = "2026-09-28T05:20:34.164Z" }, + { url = "https://files.pythonhosted.org/packages/38/7b/6d885d139b2ed76e2f4908cf10eae56acc73bf05cbef286357c4559c9a04/hypothesis-6.168.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d120009d145697909f2f0c532f2d568558ddd6990707f8cba6576bfb9770696", size = 1124974, upload-time = "2026-09-28T05:19:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/05/cc/b302ae809b48254830cae57801d0af0b66783df515b5dd56b9f21c8dfa86/hypothesis-6.168.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:818b3d09bba60ef90463e47a4bc87275dba1e8aeb184d3102888440ca7c7837d", size = 1171967, upload-time = "2026-09-28T05:20:12.295Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ab/51031080a6600f5b07adf5f9dfa117345c2adda07a5c24bda523e70bd1e9/hypothesis-6.168.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9e3c36eba80e089ecfa28ec08049723223cb41d3cfe4d9988169cf98440e402b", size = 685641, upload-time = "2026-09-28T05:19:44.609Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -1746,6 +1834,7 @@ dev = [ { name = "build" }, { name = "coverage" }, { name = "furo" }, + { name = "hypothesis" }, { name = "jupyter-book" }, { name = "ruff" }, { name = "setuptools" }, @@ -1757,6 +1846,7 @@ requires-dist = [ { name = "build", marker = "extra == 'dev'", specifier = ">=1.2.2.post1" }, { name = "coverage", marker = "extra == 'dev'", specifier = ">=7.9.2" }, { name = "furo", marker = "extra == 'dev'", specifier = ">=2024.8.6" }, + { name = "hypothesis", marker = "extra == 'dev'", specifier = ">=6.100.0" }, { name = "jupyter-book", marker = "extra == 'dev'", specifier = ">=1.0.4.post1" }, { name = "microdf-python", specifier = ">=1.0.0" }, { name = "pandas", specifier = ">=3.0" }, From 94da0df279155d92d87c662699cae29291c18aa1 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 22:05:47 -0400 Subject: [PATCH 3/6] Read only the AMT parameter subtree in the line 18 schedule test 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 --- policyengine_us/tests/test_form_6251_part_iii.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/policyengine_us/tests/test_form_6251_part_iii.py b/policyengine_us/tests/test_form_6251_part_iii.py index 2b4cee7eb32..977402f47ba 100644 --- a/policyengine_us/tests/test_form_6251_part_iii.py +++ b/policyengine_us/tests/test_form_6251_part_iii.py @@ -268,7 +268,8 @@ def assert_properties(households, law): st.integers(2013, 2035), ) def test_line_18_schedule(amounts, year): - p = SYSTEM.parameters(f"{year}-01-01").gov.irs.income.amt + # The AMT subtree only: the whole tree at an instant is slow to build. + p = SYSTEM.parameters.gov.irs.income.amt(f"{year}-01-01") line_17 = np.repeat(np.array(amounts, dtype=float), len(STATUSES)) statuses = np.tile(np.array(STATUSES), len(amounts)) filing_status = FILING_STATUS.encode(statuses) From d7ccc294f54efe59689a63a81ce1390be96e692b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 22:24:51 -0400 Subject: [PATCH 4/6] Address review notes on the Part III property tests 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 --- .gitignore | 3 +++ policyengine_us/tests/test_form_6251_part_iii.py | 16 +++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index d0c9b065baf..0474dab3b96 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,6 @@ tmpclaude-*-cwd # Claude Code worktrees .claude/worktrees/ .gitnexus/ + +# Hypothesis example database +.hypothesis/ diff --git a/policyengine_us/tests/test_form_6251_part_iii.py b/policyengine_us/tests/test_form_6251_part_iii.py index 977402f47ba..07e5bafa87b 100644 --- a/policyengine_us/tests/test_form_6251_part_iii.py +++ b/policyengine_us/tests/test_form_6251_part_iii.py @@ -39,7 +39,8 @@ import numpy as np import pytest -from policyengine_us import CountryTaxBenefitSystem, Simulation +from policyengine_us import Simulation +from policyengine_us.system import system as SYSTEM # Hypothesis is a dev extra; skip rather than fail collection without it. hypothesis = pytest.importorskip("hypothesis") @@ -251,11 +252,10 @@ def assert_properties(households, law): # Line 18 as a schedule. # --------------------------------------------------------------------------- -SYSTEM = CountryTaxBenefitSystem() FILING_STATUS = SYSTEM.variables["filing_status"].possible_values -@hypothesis.settings(max_examples=200, deadline=None) +@hypothesis.settings(max_examples=200, deadline=None, derandomize=True) @hypothesis.given( st.lists( st.one_of( @@ -275,8 +275,9 @@ def test_line_18_schedule(amounts, year): filing_status = FILING_STATUS.encode(statuses) line_18 = p.brackets.calc(line_17, factor=p.multiplier[filing_status]) separate = statuses == "SEPARATE" - # Everyone else: bit for bit the scale with no factor. - assert np.array_equal(line_18[~separate], p.brackets.calc(line_17[~separate])) + # Everyone else: bit for bit the scale with no factor, on the same array. + full = p.brackets.calc(line_17) + assert np.array_equal(line_18[~separate], full[~separate]) # Married filing separately: the breakpoint is halved (55(b)(1)(C)). breakpoint = p.brackets.thresholds[-1] / 2 low, high = p.brackets.rates @@ -288,8 +289,7 @@ def test_line_18_schedule(amounts, year): assert line_18[separate] == pytest.approx(expected[separate], rel=1e-12, abs=1e-6) # Halving the breakpoint never lowers the tax, and raises it by the # rate difference on the part of line 17 between the two breakpoints. - full = p.brackets.calc(line_17[separate]) - assert line_18[separate] - full == pytest.approx( + assert line_18[separate] - full[separate] == pytest.approx( (high - low) * np.clip(line_17[separate] - breakpoint, 0, breakpoint), rel=1e-9, abs=1e-6, @@ -394,9 +394,11 @@ def test_separate_filer_with_line_17_of_136_162_50(): st.one_of(st.just(0), st.integers(1, 100)), ) +# Fixed examples, so CI on an unrelated pull request draws the same households. SETTINGS = dict( max_examples=10, deadline=None, + derandomize=True, suppress_health_check=[ hypothesis.HealthCheck.too_slow, hypothesis.HealthCheck.data_too_large, From 7c90a984d5f2e6fb8d61578a9b17ff7e5639ee87 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 5 Oct 2026 11:15:54 -0400 Subject: [PATCH 5/6] Compare separate filers' AMT in the section 911(f) worksheet tests 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 --- policyengine_us/tests/test_section_911_tax_stacking.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/policyengine_us/tests/test_section_911_tax_stacking.py b/policyengine_us/tests/test_section_911_tax_stacking.py index c11f6664919..b0b23ca8fe1 100644 --- a/policyengine_us/tests/test_section_911_tax_stacking.py +++ b/policyengine_us/tests/test_section_911_tax_stacking.py @@ -27,7 +27,7 @@ Households live in Texas; some itemize. -Three limits of the model set the scope of these tests, and none comes +Two limits of the model set the scope of these tests, and neither comes from section 911: - `capital_gains_tax` omits the section 1(h)(1) cap at the tax on all @@ -40,9 +40,6 @@ exceeds taxable income, so the households here have no 28-percent rate or unrecaptured section 1250 gain. YAML unit tests cover how the capital gain excess reduces those two amounts. -- `amt_tax_including_cg` applies the 26%/28% breakpoint of other filers to - married-filing-separately filers on Form 6251 line 18, so the AMT of - separate filers with dividends or gains is not compared. """ import numpy as np @@ -413,9 +410,6 @@ def assert_matches_worksheets(households, law): ) assert -tolerance(stacked_income) <= cap_gaps[-1], (i, h) assert cap_gaps[-1] <= 7.5 + tolerance(stacked_income), (i, h) - if status == "SEPARATE" and dividends + gain > 0: - # Form 6251 line 18 (see the module docstring). - continue # Form 6251 lines 9 to 11 with no foreign tax credit. amt = max(0, line_7 - line_6) taxable_excess = float( From 2cd124f323bf7e87377c47c35c6f94e62e81ec18 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 5 Oct 2026 11:35:06 -0400 Subject: [PATCH 6/6] Pin the shrunk line 18 counterexample; derandomize the 911(f) tests 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 --- .../tests/test_form_6251_part_iii.py | 18 ++++++++++++++++++ .../tests/test_section_911_tax_stacking.py | 2 ++ 2 files changed, 20 insertions(+) diff --git a/policyengine_us/tests/test_form_6251_part_iii.py b/policyengine_us/tests/test_form_6251_part_iii.py index 07e5bafa87b..91013165611 100644 --- a/policyengine_us/tests/test_form_6251_part_iii.py +++ b/policyengine_us/tests/test_form_6251_part_iii.py @@ -417,6 +417,24 @@ def test_random_households_match_form_6251_2025(households): st.lists(household_with_schedule_d_strategy, min_size=1, max_size=25), st.sampled_from([2018, 2022, 2025, 2026, 2030]), ) +# The case Hypothesis shrank to against the full-breakpoint line 18: a 2018 +# separate filer with no gains whose line 12 ($95,555) is $5 above the +# separate breakpoint ($95,550), so line 18 fell $0.10 short of line 39. +@hypothesis.example( + households=[ + { + "status": "SEPARATE", + "wages": 150_255, + "qualified_dividends": 0, + "long_term_gains": 0, + "short_term_gains": 0, + "real_estate_taxes": 0, + "collectibles_share": 0, + "section_1250_share": 0, + } + ], + year=2018, +) def test_random_households_keep_the_properties(households, year): # Each household with its dividends and gains removed as well, so that # line 17 is line 12 for some filers of every status. diff --git a/policyengine_us/tests/test_section_911_tax_stacking.py b/policyengine_us/tests/test_section_911_tax_stacking.py index b0b23ca8fe1..f0f22451084 100644 --- a/policyengine_us/tests/test_section_911_tax_stacking.py +++ b/policyengine_us/tests/test_section_911_tax_stacking.py @@ -751,9 +751,11 @@ def get(variable): } ) +# Fixed examples, so CI on an unrelated pull request draws the same households. SETTINGS = dict( max_examples=10, deadline=None, + derandomize=True, suppress_health_check=[ hypothesis.HealthCheck.too_slow, hypothesis.HealthCheck.data_too_large,