diff --git a/changelog.d/fix-dwks-regular-tax-lines.fixed.md b/changelog.d/fix-dwks-regular-tax-lines.fixed.md new file mode 100644 index 00000000000..751247ecad3 --- /dev/null +++ b/changelog.d/fix-dwks-regular-tax-lines.fixed.md @@ -0,0 +1 @@ +Cap the regular tax at the tax on all taxable income at the ordinary rates (26 U.S.C. 1(h)(1), Schedule D Tax Worksheet line 47), and make regular_tax_before_credits, which Form 6251 line 10 uses, the regular tax the model charges (income_tax_main_rates plus capital_gains_tax) instead of a second worksheet computation. regular_tax_before_credits now includes the capital gains tax. diff --git a/policyengine_us/parameters/gov/irs/capital_gains/thresholds.yaml b/policyengine_us/parameters/gov/irs/capital_gains/thresholds.yaml index 2e2c3824377..10404e10bb5 100644 --- a/policyengine_us/parameters/gov/irs/capital_gains/thresholds.yaml +++ b/policyengine_us/parameters/gov/irs/capital_gains/thresholds.yaml @@ -16,7 +16,7 @@ metadata: - title: 26 U.S. Code § 1(j)(5)(B) - Capital gains rate thresholds href: https://www.law.cornell.edu/uscode/text/26/1#j_5_B - title: 2026 IRS data release - href: https://www.irs.gov/pub/irs-drop/rp-25-32.pdf#page=12 + href: https://www.irs.gov/pub/irs-drop/rp-25-32.pdf#page=13 - title: 2025 IRS data release href: https://www.irs.gov/pub/irs-drop/rp-24-40.pdf#page=7 - title: 2024 IRS data release diff --git a/policyengine_us/parameters/gov/irs/credits/ctc/amount/base.yaml b/policyengine_us/parameters/gov/irs/credits/ctc/amount/base.yaml index 2a5f73e2b72..69de4bd838f 100644 --- a/policyengine_us/parameters/gov/irs/credits/ctc/amount/base.yaml +++ b/policyengine_us/parameters/gov/irs/credits/ctc/amount/base.yaml @@ -27,7 +27,7 @@ brackets: - title: 26 U.S. Code § 24(i)(2) - Inflation adjustment for credit amount href: https://www.law.cornell.edu/uscode/text/26/24#i_2 - title: 2026 IRS data release - href: https://www.irs.gov/pub/irs-drop/rp-25-32.pdf#page=13 + href: https://www.irs.gov/pub/irs-drop/rp-25-32.pdf#page=14 # IRC §24(i)(2) adjusts the $2,200 statutory base using 2024 as reference year. # Current implementation uses incremental uprating from latest value, which # produces correct results but differs from statutory base-year calculation. diff --git a/policyengine_us/parameters/gov/irs/credits/ctc/refundable/individual_max.yaml b/policyengine_us/parameters/gov/irs/credits/ctc/refundable/individual_max.yaml index 0d5efe4a16b..5d118287b6d 100644 --- a/policyengine_us/parameters/gov/irs/credits/ctc/refundable/individual_max.yaml +++ b/policyengine_us/parameters/gov/irs/credits/ctc/refundable/individual_max.yaml @@ -31,7 +31,7 @@ metadata: - title: 26 U.S. Code § 24(i)(1) - Inflation adjustment for refundable amount href: https://www.law.cornell.edu/uscode/text/26/24#i_1 - title: 2026 IRS data release - href: https://www.irs.gov/pub/irs-drop/rp-25-32.pdf#page=13 + href: https://www.irs.gov/pub/irs-drop/rp-25-32.pdf#page=14 - title: 2025 IRS data release href: https://www.irs.gov/pub/irs-drop/rp-24-40.pdf#page=8 - title: 2024 IRS data release diff --git a/policyengine_us/parameters/gov/states/me/tax/income/deductions/standard/aged_or_blind.yaml b/policyengine_us/parameters/gov/states/me/tax/income/deductions/standard/aged_or_blind.yaml index 41b88f928d6..7ba3defe3b5 100644 --- a/policyengine_us/parameters/gov/states/me/tax/income/deductions/standard/aged_or_blind.yaml +++ b/policyengine_us/parameters/gov/states/me/tax/income/deductions/standard/aged_or_blind.yaml @@ -18,7 +18,7 @@ metadata: href: https://www.mainelegislature.org/legis/statutes/36/title36sec5124-C.html - title: 36 M.R.S. Sec. 5124-C(1-B)(B) and (1-C)(B) (Maine additional standard deduction equals the IRC Section 63(c)(3) amount) href: https://legislature.maine.gov/statutes/36/title36sec5124-C.html - - title: IRS Rev. Proc. 2025-32, Section 3.14(3) (2026 federal additional standard deduction for the aged or the blind) + - title: IRS Rev. Proc. 2025-32, Section 4.14(3) (2026 federal additional standard deduction for the aged or the blind) href: https://www.irs.gov/pub/irs-drop/rp-25-32.pdf#page=18 SINGLE: 2018-01-01: 1_600 diff --git a/policyengine_us/reforms/additional_tax_bracket/additional_tax_bracket_reform.py b/policyengine_us/reforms/additional_tax_bracket/additional_tax_bracket_reform.py index 13fad485592..8cd60d4f0ee 100644 --- a/policyengine_us/reforms/additional_tax_bracket/additional_tax_bracket_reform.py +++ b/policyengine_us/reforms/additional_tax_bracket/additional_tax_bracket_reform.py @@ -5,80 +5,9 @@ amount_taxed_below_rate, tax_at_main_rates, ) -from policyengine_us.variables.gov.irs.tax.federal_income.foreign_earned_income_exclusion.schedule_d_tax_worksheet_after_capital_gain_excess import ( - schedule_d_tax_worksheet_after_capital_gain_excess, -) def create_additional_tax_bracket() -> Reform: - class regular_tax_before_credits(Variable): - value_type = float - entity = TaxUnit - definition_period = YEAR - label = "Regular tax before credits" - documentation = "Regular tax on regular taxable income before credits" - unit = USD - - def formula(tax_unit, period, parameters): - p = parameters(period).gov.irs - filing_status = tax_unit("filing_status", period) - # As in the baseline formula, a Form 2555 filer stacks taxable - # income on the excluded amount (26 U.S.C. 911(f)). - dwks1 = tax_unit("taxable_income_plus_section_911_exclusion", period) - worksheet = schedule_d_tax_worksheet_after_capital_gain_excess( - tax_unit, period, tax_unit("taxable_income", period) - ) - - dwks16 = min_(p.capital_gains.thresholds["1"][filing_status], dwks1) - dwks17 = min_(tax_unit("dwks14", period), dwks16) - dwks20 = dwks16 - dwks17 - lowest_rate_tax = p.capital_gains.rates["1"] * dwks20 - # Break in worksheet lines - dwks13 = worksheet.line_13 - dwks21 = min_(dwks1, dwks13) - dwks22 = dwks20 - dwks23 = max_(0, dwks21 - dwks22) - dwks25 = min_(p.capital_gains.thresholds["2"][filing_status], dwks1) - dwks19 = tax_unit("dwks19", period) - dwks26 = min_(dwks19, dwks20) - dwks27 = max_(0, dwks25 - dwks26) - dwks28 = min_(dwks23, dwks27) - dwks29 = p.capital_gains.rates["2"] * dwks28 - dwks30 = dwks22 + dwks28 - dwks31 = dwks21 - dwks30 - dwks32 = p.capital_gains.rates["3"] * dwks31 - # Break in worksheet lines - dwks33 = min_(worksheet.line_9, worksheet.unrecaptured_section_1250_gain) - dwks10 = worksheet.line_10 - dwks34 = dwks10 + dwks19 - dwks36 = max_(0, dwks34 - dwks1) - dwks37 = max_(0, dwks33 - dwks36) - - p = parameters(period).gov.irs.income - - dwks38 = p.amt.capital_gains.capital_gain_excess_tax_rate * dwks37 - # Break in worksheet lines - dwks39 = dwks19 + dwks20 + dwks28 + dwks31 + dwks37 - dwks40 = dwks1 - dwks39 - dwks41 = p.amt.brackets.rates[-1] * dwks40 - - # Compute regular tax using bracket rates and thresholds - # The shared schedule clamps inverted brackets as in the - # baseline formulas (#9084). - reg_taxinc = max_(0, dwks19) - bracket = parameters(period).gov.contrib.additional_tax_bracket.bracket - reg_tax = tax_at_main_rates(reg_taxinc, filing_status, bracket) - - # Return to worksheet lines - dwks42 = reg_tax - dwks43 = dwks29 + dwks32 + dwks38 + dwks41 + dwks42 + lowest_rate_tax - excluded = max_(0, tax_unit("foreign_earned_income_exclusion", period)) - tax_on_excluded = tax_at_main_rates(excluded, filing_status, bracket) - dwks43 = where(excluded > 0, max_(0, dwks43 - tax_on_excluded), dwks43) - dwks44 = tax_unit("income_tax_main_rates", period) - dwks45 = min_(dwks43, dwks44) - return where(tax_unit("has_qdiv_or_ltcg", period), dwks45, dwks44) - class income_tax_main_rates(Variable): value_type = float entity = TaxUnit @@ -110,6 +39,28 @@ def formula(tax_unit, period, parameters): tax_on_excluded = tax_at_main_rates(excluded, filing_status, bracket) return where(excluded > 0, max_(0, tax - tax_on_excluded), tax) + class tax_on_taxable_income_at_main_rates(Variable): + value_type = float + entity = TaxUnit + definition_period = YEAR + label = "Tax on all taxable income at the main rates" + reference = "https://www.law.cornell.edu/uscode/text/26/1#h_1" + unit = USD + + def formula(tax_unit, period, parameters): + # Schedule D Tax Worksheet line 46 on the reform's rate schedule, + # as in the baseline formula on the baseline schedule. Section + # 1(h)(1) caps the regular tax at it (capital_gains_tax). + taxable_income = tax_unit( + "taxable_income_plus_section_911_exclusion", period + ) + bracket = parameters(period).gov.contrib.additional_tax_bracket.bracket + filing_status = tax_unit("filing_status", period) + tax = tax_at_main_rates(max_(0, taxable_income), filing_status, bracket) + excluded = max_(0, tax_unit("foreign_earned_income_exclusion", period)) + tax_on_excluded = tax_at_main_rates(excluded, filing_status, bracket) + return where(excluded > 0, max_(0, tax - tax_on_excluded), tax) + class taxable_income_taxed_below_25_percent(Variable): value_type = float entity = TaxUnit @@ -135,7 +86,7 @@ def formula(tax_unit, period, parameters): class reform(Reform): def apply(self): self.update_variable(income_tax_main_rates) - self.update_variable(regular_tax_before_credits) + self.update_variable(tax_on_taxable_income_at_main_rates) self.update_variable(taxable_income_taxed_below_25_percent) return reform diff --git a/policyengine_us/reforms/biden/budget_2025/capital_gains_tax_increase.py b/policyengine_us/reforms/biden/budget_2025/capital_gains_tax_increase.py index f0233f9e3cd..c87177265ba 100644 --- a/policyengine_us/reforms/biden/budget_2025/capital_gains_tax_increase.py +++ b/policyengine_us/reforms/biden/budget_2025/capital_gains_tax_increase.py @@ -3,6 +3,7 @@ section_911_net_capital_gain_other_than_dividends, ) from policyengine_us.variables.gov.irs.tax.federal_income.capital_gains.capital_gains_tax import ( + limit_to_tax_at_main_rates, rate_gain_taxed_at_28_percent, ) @@ -144,7 +145,13 @@ def formula(tax_unit, period, parameters): adjusted_net_cg, taxable_unrecaptured_gain, ) - return main_cg_tax + unrecaptured_gain_tax + remaining_cg_tax + # As in the baseline formula, section 1(h)(1) keeps the regular + # tax within the tax on all taxable income at the main rates. + return limit_to_tax_at_main_rates( + tax_unit, + period, + main_cg_tax + unrecaptured_gain_tax + remaining_cg_tax, + ) class reform(Reform): def apply(self): diff --git a/policyengine_us/reforms/harris/capital_gains/harris_capital_gains.py b/policyengine_us/reforms/harris/capital_gains/harris_capital_gains.py index fdbc86ebcba..49c106c3d69 100644 --- a/policyengine_us/reforms/harris/capital_gains/harris_capital_gains.py +++ b/policyengine_us/reforms/harris/capital_gains/harris_capital_gains.py @@ -3,6 +3,7 @@ section_911_net_capital_gain_other_than_dividends, ) from policyengine_us.variables.gov.irs.tax.federal_income.capital_gains.capital_gains_tax import ( + limit_to_tax_at_main_rates, rate_gain_taxed_at_28_percent, ) @@ -134,7 +135,13 @@ def formula(tax_unit, period, parameters): taxable_unrecaptured_gain, ) - return main_cg_tax + unrecaptured_gain_tax + remaining_cg_tax + # As in the baseline formula, section 1(h)(1) keeps the regular + # tax within the tax on all taxable income at the main rates. + return limit_to_tax_at_main_rates( + tax_unit, + period, + main_cg_tax + unrecaptured_gain_tax + remaining_cg_tax, + ) class reform(Reform): def apply(self): diff --git a/policyengine_us/tests/core/test_me_standard_deduction_federal_conformity.py b/policyengine_us/tests/core/test_me_standard_deduction_federal_conformity.py index c739c1b0523..1fb402c0372 100644 --- a/policyengine_us/tests/core/test_me_standard_deduction_federal_conformity.py +++ b/policyengine_us/tests/core/test_me_standard_deduction_federal_conformity.py @@ -109,7 +109,7 @@ def test_me_standard_deduction_does_not_follow_federal_in_2026(): def test_me_standard_deduction_applies_the_federal_dependent_cap_in_2027(): - # IRC Section 63(c)(5), as adjusted by Rev. Proc. 2025-32 section .14(2): + # IRC Section 63(c)(5), as adjusted by Rev. Proc. 2025-32 section 4.14(2): # an individual who is a dependent of another taxpayer gets at most the # greater of the dependent floor and earned income plus a fixed addition, # capped at the ordinary basic standard deduction. Maine follows the federal diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/alternative_minimum_tax.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/alternative_minimum_tax.yaml index e39db40df01..ba2904afa58 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/alternative_minimum_tax.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/alternative_minimum_tax.yaml @@ -99,7 +99,6 @@ foreign_tax_credit: 0 regular_tax_before_credits: 11_000 form_4972_lumpsum_distributions: 0 - capital_gains_tax: 0 output: alternative_minimum_tax: 0 @@ -112,7 +111,6 @@ foreign_tax_credit: 0 regular_tax_before_credits: 11_000 form_4972_lumpsum_distributions: 0 - capital_gains_tax: 0 output: alternative_minimum_tax: 1_000 @@ -168,7 +166,6 @@ foreign_tax_credit: 0 regular_tax_before_credits: 30_000 form_4972_lumpsum_distributions: 0 - capital_gains_tax: 0 output: # Line 3 = 280,000. Line 4 = 280,000 x 28% - 4,782 = 73,618. Line 5 = # 130,000 x 26% = 33,800. Line 6 = 39,818, which goes on Form 6251 line 7. @@ -187,7 +184,6 @@ foreign_tax_credit: 0 regular_tax_before_credits: 30_000 form_4972_lumpsum_distributions: 0 - capital_gains_tax: 0 output: # Form 6251 line 7 = 150,000 x 26% = 39,000. alternative_minimum_tax: 9_000 @@ -203,7 +199,6 @@ foreign_tax_credit: 0 regular_tax_before_credits: 0 form_4972_lumpsum_distributions: 0 - capital_gains_tax: 0 output: # Line 3 = 180,000. Line 4 = 180,000 x 28% - 2,391 = 48,009. Line 5 = # 130,000 x 28% - 2,391 = 34,009. Line 6 = 14,000. @@ -219,9 +214,9 @@ amt_part_iii_required: true amt_tax_including_cg: 57_785 foreign_tax_credit: 0 - regular_tax_before_credits: 0 + # Form 6251 line 10: the regular tax, all of it capital gains tax. + regular_tax_before_credits: 42_637.50 form_4972_lumpsum_distributions: 0 - capital_gains_tax: 42_637.50 output: # Form 6251 line 39 = 311,900 x 28% - 4,782 = 82,550, line 38 = 57,785, # line 40 = 57,785. Worksheet line 5 = 100,000 x 26% = 26,000, line 6 = @@ -238,6 +233,5 @@ foreign_tax_credit: 0 regular_tax_before_credits: 0 form_4972_lumpsum_distributions: 0 - capital_gains_tax: 0 output: alternative_minimum_tax: 0 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 fd894743583..b89b41f7e71 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 @@ -327,7 +327,7 @@ dwks19: 136_000 unrecaptured_section_1250_gain: 0 output: - # Rev. Proc. 2025-32, section 3.10: for 2026 the 28 percent rate applies + # Rev. Proc. 2025-32, section 4.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 - 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 23a7ed89800..ff44250ad9e 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 @@ -54,8 +54,9 @@ # $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 + income_tax_main_rates: 25_067 capital_gains_tax: 176_712.5 + regular_tax_before_credits: 201_779.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 - diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/regular_tax_before_credits.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/regular_tax_before_credits.yaml index eea12a96d21..32e6afcea1c 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/regular_tax_before_credits.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/regular_tax_before_credits.yaml @@ -31,7 +31,346 @@ output: # Line 1 = 134,250 and the gains are cut to 34,250, so the ordinary # income is the 100,000 excluded and its tax cancels against line 5 of - # the Foreign Earned Income Tax Worksheet. The 5,137.50 of capital gains - # tax is counted separately in capital_gains_tax. - regular_tax_before_credits: 0 + # the Foreign Earned Income Tax Worksheet. The regular tax is the + # 5,137.50 of capital gains tax. + income_tax_main_rates: 0 capital_gains_tax: 5_137.50 + regular_tax_before_credits: 5_137.50 + +# Tax units from the default dataset (populace_us_2024 at +# populace-us-2024-spm-20260909), with their 2025 inputs as the +# microsimulation uprates them. In a 2025 run before #9781, these were among +# the 18 tax units whose regular_tax_before_credits fell below +# income_tax_main_rates, which understated Form 6251 line 10. All 18 elect +# under Form 4952 line 4g. net_capital_gain applied the election but worksheet +# line 10 (dwks10) did not, so worksheet line 21 (dwks19) was too small. A +# second Schedule D Tax Worksheet, which took worksheet line 28 as the smaller +# of lines 21 and 22 instead of their sum, then taxed line 23 at 15 percent +# instead of 20. Form 6251 line 10 used the smaller of that worksheet's tax and +# income_tax_main_rates. Line 10 is now the regular tax the model charges, +# income_tax_main_rates plus capital_gains_tax. +- name: Dataset tax unit 1101624, a Form 4952 election against long-term gain + absolute_error_margin: 1 + period: 2025 + input: + people: + person1: + age: 60 + long_term_capital_gains: 110_314.59 + short_term_capital_gains: 15_101.07 + qualified_dividend_income: 917.84 + person2: + age: 58 + long_term_capital_gains: 110_314.59 + short_term_capital_gains: 15_101.07 + qualified_dividend_income: 917.84 + investment_income_elected_form_4952: 141_871.53 + tax_units: + tax_unit: + members: [person1, person2] + filing_status: JOINT + taxable_income: 3_437_082.75 + amt_income_less_exemptions: 3_447_082.75 + households: + household: + members: [person1, person2] + state_code: TX + output: + # Schedule D Tax Worksheet: line 7 220,629.18; line 8 141,871.53; line 9 + # 78,757.65; line 6 1,835.68; line 10 80,593.33, the net capital gain. + net_capital_gain: 80_593.33 + dwks10: 80_593.33 + # Line 21 = line 18 = 3,437,082.75 - 80,593.33. + dwks19: 3_356_489.42 + # Line 44: 37% x 3,356,489.42 - 75,937.50. Line 34: 20% x 80,593.33. + income_tax_main_rates: 1_165_963.59 + capital_gains_tax: 16_118.67 + # Line 47 = line 45 = 1,182,082.26 (line 46: 1,195,768.12). + regular_tax_before_credits: 1_182_082.26 + # Form 6251: line 17 3,366,489.42; line 18 937,835.04; line 33 80,593.33 + # (20%: 16,118.67); line 38 953,953.71, below line 10. + amt_tax_including_cg: 953_953.71 + alternative_minimum_tax: 0 + +- name: Dataset tax unit 1004930, an election larger than the gains and dividends + absolute_error_margin: 0.5 + period: 2025 + input: + people: + person1: + age: 85 + investment_income_elected_form_4952: 130_193.69 + person2: + age: 85 + long_term_capital_gains: 80_880.30 + short_term_capital_gains: 10_988.80 + qualified_dividend_income: 2_748.49 + tax_units: + tax_unit: + members: [person1, person2] + filing_status: JOINT + taxable_income: 186_864.59 + amt_income_less_exemptions: 84_564.59 + households: + household: + members: [person1, person2] + state_code: TX + output: + # Line 8 takes all 80,880.30 of the gain and line 5 the other 49,313.39 + # from the 2,748.49 of dividends, so lines 6, 9 and 10 are zero. + net_capital_gain: 0 + dwks10: 0 + dwks19: 186_864.59 + # 11,157 + 22% x (186,864.59 - 96,950). + income_tax_main_rates: 30_938.21 + capital_gains_tax: 0 + regular_tax_before_credits: 30_938.21 + # Form 6251 line 39: 26% x 84,564.59 = 21,986.79, below line 10. + alternative_minimum_tax: 0 + +- name: Dataset tax unit 1156248, an election and unrecaptured section 1250 gain + absolute_error_margin: 0.5 + period: 2025 + input: + people: + person1: + age: 36 + long_term_capital_gains: 11_073.12 + short_term_capital_gains: 1_512.26 + qualified_dividend_income: 763.81 + investment_income_elected_form_4952: 6_819.79 + person2: + age: 36 + long_term_capital_gains: 171.63 + short_term_capital_gains: 23.44 + qualified_dividend_income: 50.92 + tax_units: + tax_unit: + members: [person1, person2] + filing_status: JOINT + taxable_income: 535_956.88 + amt_income_less_exemptions: 430_456.88 + unrecaptured_section_1250_gain: 85.57 + households: + household: + members: [person1, person2] + state_code: TX + output: + # Schedule D Tax Worksheet: line 9 4,424.96; line 10 5,239.69; line 12 + # 85.57; line 13 5,154.12; line 14 530,802.76; line 18 530,717.19; line + # 19 394,600; line 21 530,717.19. + net_capital_gain: 5_239.69 + dwks10: 5_239.69 + dwks19: 530_717.19 + # Line 44: 35% x 530,717.19 - 60,905.50. Line 30 5,154.12 at 15% + # (773.12) and line 39 85.57 at 25% (21.39). + income_tax_main_rates: 124_845.52 + capital_gains_tax: 794.51 + # Line 47 = line 45 = 125,640.03 (line 46: 126,679.41). + regular_tax_before_credits: 125_640.03 + # Form 6251: line 18 114,278.81; line 31 773.12; line 37 21.39. + amt_tax_including_cg: 115_073.32 + alternative_minimum_tax: 0 + +- name: Dataset tax unit 1032460, an election that spills into qualified dividends + absolute_error_margin: 0.05 + period: 2025 + input: + people: + person1: + age: 76 + person2: + age: 76 + long_term_capital_gains: 1_109.12 + short_term_capital_gains: 153.57 + qualified_dividend_income: 2_804.45 + investment_income_elected_form_4952: 2_681.56 + tax_units: + tax_unit: + members: [person1, person2] + filing_status: JOINT + taxable_income: 11_777.39 + households: + household: + members: [person1, person2] + state_code: TX + output: + # Line 8 takes the 1,109.12 of gain and line 5 the other 1,572.44 from + # the dividends: line 6 1,232.01, line 9 0, line 10 1,232.01. + net_capital_gain: 1_232.01 + dwks10: 1_232.01 + dwks19: 10_545.38 + # 10% x 10,545.38; the dividends are taxed at 0 percent. + income_tax_main_rates: 1_054.54 + capital_gains_tax: 0 + regular_tax_before_credits: 1_054.54 + +# 26 U.S.C. 1(h)(1): with a net capital gain, "the tax imposed by this section +# for such taxable year shall not exceed the sum of" subparagraphs (A) to (F). +# The 2025 Schedule D Tax Worksheet takes "the smaller of line 45 or line 46" +# on line 47, and the Qualified Dividends and Capital Gain Tax Worksheet "the +# smaller of line 23 or line 24" on line 25. The two differ where gain taxed at +# 15 percent falls in the 12 percent bracket: in 2025 the 0 percent rate +# amount ends $125 ($250 joint, $100 head of household) below the top of that +# bracket, and in 2026 $950 ($1,900 joint, $1,250 head of household). These +# cases put that much qualified dividend income on top of ordinary income that +# fills the 0 percent rate amount. Amounts are taxed at the rates, as the Tax +# Computation Worksheet does; the Tax Table rounds within $50 rows. Line +# numbers are the Schedule D Tax Worksheet's, which gives the same tax. +- name: 2025 single, line 45 exceeds line 46 by $3.75 + absolute_error_margin: 0.01 + period: 2025 + input: + filing_status: SINGLE + taxable_income: 48_475 + qualified_dividend_income: 125 + output: + # Line 21 = 48,350; tax 1,192.50 + 12% x 36,425 = 5,563.50. + income_tax_main_rates: 5_563.50 + # Line 46: tax on 48,475 = 1,192.50 + 12% x 36,550 = 5,578.50. + tax_on_taxable_income_at_main_rates: 5_578.50 + # Line 30 = 125 at 15% = 18.75, so line 45 = 5,582.25. Line 47 = 5,578.50, + # which leaves 15.00 for the gains. + capital_gains_tax: 15 + regular_tax_before_credits: 5_578.50 + +- name: 2025 joint, line 45 exceeds line 46 by $7.50 + absolute_error_margin: 0.01 + period: 2025 + input: + filing_status: JOINT + taxable_income: 96_950 + qualified_dividend_income: 250 + output: + # Line 44: 2,385 + 12% x (96,700 - 23,850) = 11,127. Line 46: 2,385 + 12% + # x 73,100 = 11,157. Line 45: 11,127 + 15% x 250 = 11,164.50. + income_tax_main_rates: 11_127 + tax_on_taxable_income_at_main_rates: 11_157 + capital_gains_tax: 30 + regular_tax_before_credits: 11_157 + +- name: 2026 single, line 45 exceeds line 46 by $28.50 + absolute_error_margin: 0.01 + period: 2026 + input: + filing_status: SINGLE + taxable_income: 50_400 + qualified_dividend_income: 950 + output: + # Rev. Proc. 2025-32, sections 4.01 (Table 3) and 4.03: the 12 percent + # bracket ends at 50,400 and the 0 percent rate amount at 49,450. + # Line 44: 1,240 + 12% x 37,050 = 5,686. Line 46: 5,800. Line 45: 5,686 + # + 15% x 950 = 5,828.50. + income_tax_main_rates: 5_686 + tax_on_taxable_income_at_main_rates: 5_800 + capital_gains_tax: 114 + regular_tax_before_credits: 5_800 + +- name: 2026 head of household, line 45 exceeds line 46 by $37.50 + absolute_error_margin: 0.01 + period: 2026 + input: + filing_status: HEAD_OF_HOUSEHOLD + taxable_income: 67_450 + qualified_dividend_income: 1_250 + output: + # Rev. Proc. 2025-32, Table 2 and section 4.03: 12 percent to 67,450, 0 + # percent rate amount 66,200. Line 44: 1,770 + 12% x 48,500 = 7,590. + # Line 46: 7,740. Line 45: 7,590 + 15% x 1,250 = 7,777.50. + income_tax_main_rates: 7_590 + tax_on_taxable_income_at_main_rates: 7_740 + capital_gains_tax: 150 + regular_tax_before_credits: 7_740 + +- name: 2026 joint, line 45 exceeds line 46 by $57 + absolute_error_margin: 0.01 + period: 2026 + input: + filing_status: JOINT + taxable_income: 100_800 + qualified_dividend_income: 1_900 + output: + # Rev. Proc. 2025-32, Table 1 and section 4.03: 12 percent to 100,800, 0 + # percent rate amount 98,900. Line 44: 2,480 + 12% x 74,100 = 11,372. + # Line 46: 11,600. Line 45: 11,372 + 15% x 1,900 = 11,657. + income_tax_main_rates: 11_372 + tax_on_taxable_income_at_main_rates: 11_600 + capital_gains_tax: 228 + regular_tax_before_credits: 11_600 + +- name: 2026 separate, long-term gain, line 45 exceeds line 46 by $28.50 + absolute_error_margin: 0.01 + period: 2026 + input: + filing_status: SEPARATE + taxable_income: 50_400 + long_term_capital_gains: 950 + output: + # Rev. Proc. 2025-32, Table 4 and section 4.03, as for single filers. + income_tax_main_rates: 5_686 + tax_on_taxable_income_at_main_rates: 5_800 + capital_gains_tax: 114 + regular_tax_before_credits: 5_800 + +- name: 2026 single, gain into the 22 percent bracket leaves line 45 below line 46 + absolute_error_margin: 0.01 + period: 2026 + input: + filing_status: SINGLE + taxable_income: 60_000 + qualified_dividend_income: 10_000 + output: + # Line 44: 1,240 + 12% x 37,600 = 5,752. Line 45: 5,752 + 15% x 10,000 + # = 7,252. Line 46: 5,800 + 22% x 9,600 = 7,912. + income_tax_main_rates: 5_752 + tax_on_taxable_income_at_main_rates: 7_912 + capital_gains_tax: 1_500 + regular_tax_before_credits: 7_252 + +- name: 2025 single, more gain than the overlap leaves line 45 below line 46 + absolute_error_margin: 0.01 + period: 2025 + input: + filing_status: SINGLE + taxable_income: 49_350 + qualified_dividend_income: 1_000 + output: + # Line 45: 5,563.50 + 15% x 1,000 = 5,713.50. Line 46: 5,578.50 + 22% x + # 875 = 5,771. Line 47 is line 45. + income_tax_main_rates: 5_563.50 + tax_on_taxable_income_at_main_rates: 5_771 + capital_gains_tax: 150 + regular_tax_before_credits: 5_713.50 + +- name: 2025 single, gain partly in the 0 percent rate amount leaves line 45 below line 46 + absolute_error_margin: 0.01 + period: 2025 + input: + filing_status: SINGLE + taxable_income: 48_475 + qualified_dividend_income: 475 + output: + # Line 21 = 48,000 (tax 5,521.50); line 22 = 350 at 0%; line 30 = 125 at + # 15% (18.75). Line 45 = 5,540.25; line 46 = 5,578.50. + income_tax_main_rates: 5_521.50 + tax_on_taxable_income_at_main_rates: 5_578.50 + capital_gains_tax: 18.75 + regular_tax_before_credits: 5_540.25 + +- name: 2025 single Form 2555 filer, line 47 less the tax on the excluded amount + absolute_error_margin: 0.01 + period: 2025 + input: + filing_status: SINGLE + taxable_income: 8_475 + qualified_dividend_income: 125 + foreign_earned_income_exclusion: 40_000 + output: + # Foreign Earned Income Tax Worksheet: line 3 = 48,475; line 4 = Schedule + # D Tax Worksheet line 47, the tax on 48,475 (5,578.50), as in the first + # 2025 case; line 5 = tax on 40,000 = 1,192.50 + 12% x 28,075 = 4,561.50; + # line 6 = 1,017. + income_tax_main_rates: 1_002 + tax_on_taxable_income_at_main_rates: 1_017 + capital_gains_tax: 15 + regular_tax_before_credits: 1_017 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/before_credits/tax_on_taxable_income_at_main_rates.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/before_credits/tax_on_taxable_income_at_main_rates.yaml new file mode 100644 index 00000000000..6de0024d4db --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/before_credits/tax_on_taxable_income_at_main_rates.yaml @@ -0,0 +1,57 @@ +- name: Without dividends or gains, the tax at the main rates + absolute_error_margin: 0.01 + period: 2025 + input: + filing_status: SINGLE + taxable_income: 60_000 + output: + # 2025: 5,578.50 + 22% x (60,000 - 48,475). + tax_on_taxable_income_at_main_rates: 8_114 + income_tax_main_rates: 8_114 + +- name: Long-term gain is taxed at the main rates too + absolute_error_margin: 0.01 + period: 2025 + input: + filing_status: SINGLE + taxable_income: 60_000 + long_term_capital_gains: 20_000 + output: + # Schedule D Tax Worksheet line 46: the tax on all of line 1. Line 44 + # taxes only line 21, 40,000: 1,192.50 + 12% x 28,075 = 4,561.50. + tax_on_taxable_income_at_main_rates: 8_114 + income_tax_main_rates: 4_561.50 + +- name: Married filing jointly, 2026 + absolute_error_margin: 0.01 + period: 2026 + input: + filing_status: JOINT + taxable_income: 250_000 + qualified_dividend_income: 50_000 + output: + # Rev. Proc. 2025-32, section 4.01, Table 1: 35,932 + 24% x (250,000 - + # 211,400). + tax_on_taxable_income_at_main_rates: 45_196 + +- name: A Form 2555 filer subtracts the tax on the excluded amount + absolute_error_margin: 0.01 + period: 2025 + input: + filing_status: HEAD_OF_HOUSEHOLD + taxable_income: 36_375 + long_term_capital_gains: 10_000 + foreign_earned_income_exclusion: 40_000 + output: + # Foreign Earned Income Tax Worksheet: line 3 = 76,375, taxed at + # 9,977.50; line 5, the tax on 40,000, is 4,460. + tax_on_taxable_income_at_main_rates: 5_517.50 + +- name: No taxable income + period: 2025 + input: + filing_status: SINGLE + taxable_income: 0 + long_term_capital_gains: 5_000 + output: + tax_on_taxable_income_at_main_rates: 0 diff --git a/policyengine_us/tests/policy/contrib/additional_tax_bracket/additional_tax_bracket_reform.yaml b/policyengine_us/tests/policy/contrib/additional_tax_bracket/additional_tax_bracket_reform.yaml index 14319c011a4..369dc5f9c36 100644 --- a/policyengine_us/tests/policy/contrib/additional_tax_bracket/additional_tax_bracket_reform.yaml +++ b/policyengine_us/tests/policy/contrib/additional_tax_bracket/additional_tax_bracket_reform.yaml @@ -17,6 +17,8 @@ output: regular_tax_before_credits: 328_110 income_tax_main_rates: 328_110 + # No gains: line 46 is the tax at the main rates on the same schedule. + tax_on_taxable_income_at_main_rates: 328_110 - name: Additional tax bracket is not applied. period: 2027 @@ -36,6 +38,8 @@ output: regular_tax_before_credits: 290_114 income_tax_main_rates: 290_114 + # No gains: line 46 is the tax at the main rates on the same schedule. + tax_on_taxable_income_at_main_rates: 290_114 - name: Additional tax bracket is applied, taxable income is below the threshold. period: 2027 @@ -56,6 +60,8 @@ output: regular_tax_before_credits: 166_046 income_tax_main_rates: 166_046 + # No gains: line 46 is the tax at the main rates on the same schedule. + tax_on_taxable_income_at_main_rates: 166_046 - name: Additional tax bracket is not applied, taxable income is below the threshold. period: 2027 @@ -75,6 +81,8 @@ output: regular_tax_before_credits: 145_893 income_tax_main_rates: 145_893 + # No gains: line 46 is the tax at the main rates on the same schedule. + tax_on_taxable_income_at_main_rates: 145_893 # With its default parameters the reform's added bracket is unset: brackets 7 # and 8 both have a threshold of infinity for every filing status and year. diff --git a/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py b/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py index 43b7ad96afb..90bfa90f8cc 100644 --- a/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py +++ b/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py @@ -45,13 +45,15 @@ - with its first seven brackets set to current law and the added bracket unset, it reproduces the baseline; - for any thresholds of brackets 7 and 8, finite or infinite, the reform's - ``income_tax_main_rates``, ``regular_tax_before_credits`` and - ``taxable_income_taxed_below_25_percent`` are finite, and its two taxes are - at least their values with the bracket removed when r8 >= r7. The - households have no 28 percent rate gain or unrecaptured section 1250 gain, - so the amount taxed below 25 percent does not change which of their income - is taxed at the regular rates (``dwks19`` and - ``capital_gains_excluded_from_taxable_income``). + ``income_tax_main_rates``, ``tax_on_taxable_income_at_main_rates``, + ``regular_tax_before_credits`` and ``taxable_income_taxed_below_25_percent`` + are finite, and its three taxes are at least their values with the bracket + removed when r8 >= r7. The households have no 28 percent rate gain or + unrecaptured section 1250 gain, so the amount taxed below 25 percent does + not change which of their income is taxed at the regular rates (``dwks19`` + and ``capital_gains_excluded_from_taxable_income``). The regular tax is + ``income_tax_main_rates`` plus ``capital_gains_tax``, which section 1(h)(1) + limits to ``tax_on_taxable_income_at_main_rates``, so it rises with both. No simulation here sets a foreign earned income exclusion, so the reform's subtraction of the tax on the excluded amount is covered only through the @@ -89,8 +91,18 @@ N_BRACKETS = 8 BRACKET = "gov.contrib.additional_tax_bracket.bracket" YEAR = 2026 -REFORM_TAXES = ["income_tax_main_rates", "regular_tax_before_credits"] # Every variable the reform replaces. +REPLACED_VARIABLES = [ + "income_tax_main_rates", + "tax_on_taxable_income_at_main_rates", + "taxable_income_taxed_below_25_percent", +] +# The reform's taxes: the two it replaces, and the regular tax built on them. +REFORM_TAXES = [ + "income_tax_main_rates", + "tax_on_taxable_income_at_main_rates", + "regular_tax_before_credits", +] REFORM_VARIABLES = REFORM_TAXES + ["taxable_income_taxed_below_25_percent"] DOWNSTREAM_VARIABLES = REFORM_VARIABLES + ["income_tax_before_credits", "income_tax"] # Simulation variables are float32, about seven significant digits, so @@ -385,8 +397,8 @@ def test_baseline_schedule_is_unchanged_in_every_year(): # --------------------------------------------------------------------------- ORDINARY_INCOMES = [0, 30_000, 250_000, 700_000, 3_000_000] -# (long-term capital gains, qualified dividends): none, and enough to take -# the Schedule D Tax Worksheet path in regular_tax_before_credits. +# (long-term capital gains, qualified dividends): none, and enough for a +# capital gains tax in regular_tax_before_credits. PREFERENTIAL_INCOME = [(0, 0), (150_000, 20_000)] @@ -427,7 +439,7 @@ def default_system_state(): bracket = baseline_system.parameters( f"{YEAR}-01-01" ).gov.contrib.additional_tax_bracket.bracket - variables = [baseline_system.variables[name] for name in REFORM_VARIABLES] + variables = [baseline_system.variables[name] for name in REPLACED_VARIABLES] values = [float(bracket.rates[str(i)]) for i in range(1, N_BRACKETS + 1)] + [ float(bracket.thresholds[str(i)][status]) for i in range(1, N_BRACKETS + 1) diff --git a/policyengine_us/tests/test_form_4952_section_911_interaction.py b/policyengine_us/tests/test_form_4952_section_911_interaction.py index 912637a8972..57bcad7642d 100644 --- a/policyengine_us/tests/test_form_4952_section_911_interaction.py +++ b/policyengine_us/tests/test_form_4952_section_911_interaction.py @@ -595,10 +595,12 @@ def assert_matches_2025_forms(households, law): stacked_income = taxable_income + h["exclusion"] slack = tolerance(stacked_income) regular_tax = float(law["regular_tax"][i]) - assert regular_tax == pytest.approx(line_6_feitw, abs=slack), (i, h) - # The line 47 cap, which the model's section 1(h) formulas omit, - # binds by at most the rate overlap. - cap_gap = regular_tax - line_6_capped + assert float(law["regular_tax_before_credits"][i]) == pytest.approx( + regular_tax, abs=slack + ), (i, h) + assert regular_tax == pytest.approx(line_6_capped, abs=slack), (i, h) + # The line 47 cap binds by at most the rate overlap. + cap_gap = line_6_feitw - line_6_capped assert -slack <= cap_gap <= law["rate_overlap"][i] + slack, (i, h) gain = section_1222_gain(h) if status == "SEPARATE" and h["dividends"] + gain > 0: @@ -613,7 +615,7 @@ def assert_matches_2025_forms(households, law): ordinary_income, status, ) - amt = max(0, line_7 - line_6_feitw) + amt = max(0, line_7 - line_6_capped) assert float(law["alternative_minimum_tax"][i]) == pytest.approx( amt, abs=tolerance(stacked_income, taxable_excess) ), (i, h) diff --git a/policyengine_us/tests/test_form_6251_part_iii.py b/policyengine_us/tests/test_form_6251_part_iii.py index eb25558d626..1126bdcf26c 100644 --- a/policyengine_us/tests/test_form_6251_part_iii.py +++ b/policyengine_us/tests/test_form_6251_part_iii.py @@ -35,10 +35,11 @@ the Schedule D Tax Worksheet. Its 2025 lines 1 to 47 are transcribed below too, and for those households the model is compared with: -- line 47, the regular tax (`income_tax_main_rates` plus - `capital_gains_tax`), which taxes at the regular rates the greater of line - 18 or line 20 (line 21), where line 19 is the taxable income taxed at a - rate below 25 percent (26 U.S.C. 1(h)(1)(A)(ii)(I)); +- line 47, the regular tax (`regular_tax_before_credits`, which is + `income_tax_main_rates` plus `capital_gains_tax`), which taxes at the + regular rates the greater of line 18 or line 20 (line 21), where line 19 is + the taxable income taxed at a rate below 25 percent (26 U.S.C. + 1(h)(1)(A)(ii)(I)), and is never more than line 46; - Form 6251 lines 38 to 40 and 11, with line 14 from Schedule D line 19, line 15 capped by worksheet line 10, line 27 from worksheet line 21 and lines 35 to 37 taxing the rest of the unrecaptured gain at 25 percent. @@ -303,6 +304,7 @@ def capital_gains_worksheet_line_4(household): "dwks14", "dwks19", "income_tax_main_rates", + "tax_on_taxable_income_at_main_rates", "regular_tax_before_credits", "capital_gains_tax", "amt_income_less_exemptions", @@ -401,10 +403,9 @@ def assert_matches_form_6251(households, law): 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] - ) + # Lines 9 to 11, with no foreign tax credit and no Form 4972. Line 10 + # is the regular tax, gains included. + line_10 = float(law["regular_tax_before_credits"][i]) amt = max(0, line_40 - line_10) assert float(law["alternative_minimum_tax"][i]) == pytest.approx( amt, abs=tolerance(line_12, line_10) @@ -445,6 +446,9 @@ def assert_matches_schedule_d_tax_worksheet(households, law): regular_tax = float(law["income_tax_main_rates"][i]) + float( law["capital_gains_tax"][i] ) + assert float(law["regular_tax_before_credits"][i]) == pytest.approx( + regular_tax, abs=tolerance(line_1) + ), (i, h) if line_1 <= 0: assert regular_tax == 0, (i, h) continue @@ -458,10 +462,9 @@ def assert_matches_schedule_d_tax_worksheet(households, law): assert float(law[model_line][i]) == pytest.approx( worksheet[worksheet_line], abs=tolerance(line_1) ), (i, h, model_line) - # Line 45. The model does not take the smaller of lines 45 and 46, - # which differ only when a little gain taxed at 15 percent sits where - # the regular rate is 12 percent. - assert regular_tax == pytest.approx(worksheet[45], abs=tolerance(line_1)), ( + # Line 47, the smaller of lines 45 and 46. They differ when a little + # gain taxed at 15 percent sits where the regular rate is 12 percent. + assert regular_tax == pytest.approx(worksheet[47], abs=tolerance(line_1)), ( i, h, worksheet, @@ -483,9 +486,9 @@ def assert_matches_schedule_d_tax_worksheet(households, law): assert float(law["amt_base_tax"][i]) == pytest.approx( line_39, abs=tolerance(line_12) ), (i, h) - # Line 11, with line 10 the worksheet's regular tax (line 45, as + # Line 11, with line 10 the worksheet's regular tax (line 47, as # above) and no foreign tax credit. - amt = max(0, line_40 - worksheet[45]) + amt = max(0, line_40 - worksheet[47]) assert float(law["alternative_minimum_tax"][i]) == pytest.approx( amt, abs=tolerance(line_12, line_1) ), (i, h) @@ -514,6 +517,11 @@ def assert_schedule_d_properties(households, law): ] ) assert np.array_equal(line_21[plain], line_14[plain]) + # Line 47 is never more than line 46, the tax on all taxable income at the + # regular rates (26 U.S.C. 1(h)(1), "shall not exceed"). + regular_tax = law["regular_tax_before_credits"].astype(float) + line_46 = law["tax_on_taxable_income_at_main_rates"].astype(float) + assert (regular_tax <= line_46 + tol).all() # --------------------------------------------------------------------------- diff --git a/policyengine_us/tests/test_negative_main_rates_inverted_brackets.py b/policyengine_us/tests/test_negative_main_rates_inverted_brackets.py index 995a588fc11..6af6c56020d 100644 --- a/policyengine_us/tests/test_negative_main_rates_inverted_brackets.py +++ b/policyengine_us/tests/test_negative_main_rates_inverted_brackets.py @@ -11,14 +11,16 @@ above bracket 6 top $305,875), worth a flat −$82,486 per married-filing-separately filer under TCJA-expiration counterfactuals. -Three code sites run the schedule and all must clamp together: -`income_tax_main_rates`, the AMT regular-tax worksheet -(`regular_tax_before_credits`, taken when the unit has qualified -dividends or long-term gains), and the `additional_tax_bracket` contrib -reform's copies of both. Clamping only the first diverges the AMT -comparator from the corrected main-rates tax and manufactures phantom -AMT for any affected filer with $1 of preferential income. They now -share one loop, `tax_at_main_rates`, which carries the clamp. +Every code site that runs the schedule must clamp: `income_tax_main_rates`, +`tax_on_taxable_income_at_main_rates` (the tax on all taxable income, +which caps the regular tax under section 1(h)(1)), and the +`additional_tax_bracket` contrib reform's copies of both. The AMT used to +compare against a second worksheet copy of the schedule, and clamping only +`income_tax_main_rates` then manufactured phantom AMT for any affected +filer with $1 of preferential income; Form 6251 line 10 is now +`regular_tax_before_credits`, the main-rates tax plus the capital gains +tax. The sites share one loop, `tax_at_main_rates`, which carries the +clamp. Note the fixture detail that makes these tests real: situation values must be period-keyed (`{"2026": ...}`). An undated `filing_status` @@ -86,6 +88,7 @@ def test_zero_income_filer_owes_zero_under_inverted_brackets(inverted_bracket_sy tax_benefit_system=inverted_bracket_system, situation=separate_filer(0) ) assert sim.calculate("income_tax_main_rates", 2026)[0] == 0 + assert sim.calculate("tax_on_taxable_income_at_main_rates", 2026)[0] == 0 def test_low_income_filer_unaffected_by_inversion_above_their_income( @@ -130,11 +133,11 @@ def test_main_rates_never_negative_across_incomes(inverted_bracket_system): def test_amt_comparator_stays_consistent_with_preferential_income( inverted_bracket_system, ): - # One dollar of qualified dividends routes the AMT regular-tax - # comparison through the duplicate worksheet schedule. If that copy - # is not clamped identically, the corrected main-rates tax minus a - # still-corrupted comparator manufactures tens of thousands of - # dollars of phantom AMT. + # One dollar of qualified dividends brings in the capital gains tax + # and its section 1(h)(1) cap at the tax on all taxable income. If + # either schedule were not clamped identically, Form 6251 line 10 + # would diverge from the corrected main-rates tax and manufacture tens + # of thousands of dollars of phantom AMT. sim = Simulation( tax_benefit_system=inverted_bracket_system, situation=separate_filer(300_000, qualified_dividends=1), @@ -142,9 +145,11 @@ def test_amt_comparator_stays_consistent_with_preferential_income( amt = sim.calculate("alternative_minimum_tax", 2026)[0] main = sim.calculate("income_tax_main_rates", 2026)[0] before_credits = sim.calculate("income_tax_before_credits", 2026)[0] + regular = sim.calculate("regular_tax_before_credits", 2026)[0] assert amt == 0 assert main > 0 assert abs(before_credits - main) < 2 # only the $1 dividend's tax + assert before_credits == pytest.approx(regular) def test_mixed_statuses_only_the_separate_row_responds(inverted_bracket_system): diff --git a/policyengine_us/tests/test_regular_tax_before_credits.py b/policyengine_us/tests/test_regular_tax_before_credits.py new file mode 100644 index 00000000000..bc542f86f0b --- /dev/null +++ b/policyengine_us/tests/test_regular_tax_before_credits.py @@ -0,0 +1,753 @@ +"""Property and differential tests for the regular tax on Form 6251 line 10. + +26 U.S.C. 1(h)(1): "If a taxpayer has a net capital gain for any taxable +year, the tax imposed by this section for such taxable year shall not exceed +the sum of" the amounts in subparagraphs (A) to (F). The 2025 Schedule D Tax +Worksheet adds those amounts on line 45, figures the tax on all of line 1 at +the regular rates on line 46, and enters "the smaller of line 45 or line 46" +on line 47, the tax on Form 1040 line 16. + +The model figures the regular tax once, by the section 1(h) formulas: +`income_tax_main_rates` taxes subparagraph (A)'s amount at the regular +rates, and `capital_gains_tax` adds subparagraphs (B) to (F), but no more +than the rest of `tax_on_taxable_income_at_main_rates` (line 46). +`regular_tax_before_credits` is the sum, and Form 6251 line 10 uses it. + +Two kinds of test: + +- Differential. Lines 1 to 47 of the 2025 Schedule D Tax Worksheet (2025 + Instructions for Schedule D, pages 15 and 16) are transcribed below line by + line, with every "skip" instruction, the Form 4952 line 4g election (lines + 3 to 9, with line 4 from Form 4952 lines 4d and 4e), capital gain + distributions (Schedule D line 13), short-term losses, and Schedule D lines + 18 and 19. The tax at the regular rates is the 2025 Tax Computation + Worksheet, extended below $100,000 at the same rates as the model does + (the Tax Table rounds within $50 rows). For 2026 the same worksheet runs on + Rev. Proc. 2025-32's amounts: the rate tables of section 4.01 and the + maximum zero and 15 percent rate amounts of section 4.03. Every household + is compared line by line with the model. Schedule D lines 18 and 19 are + the 28 percent rate gain and unrecaptured section 1250 gain as entered, as + the model takes them. +- Properties that hold for every household and year, from the model alone: + 1. 0 <= regular tax <= line 46, the tax on all taxable income at the + regular rates ("shall not exceed"). + 2. The preferential computation never exceeds the ordinary one: + 0 <= capital_gains_tax <= line 46 - income_tax_main_rates, so the + main-rates tax is never more than the regular tax. + 3. Identities: regular_tax_before_credits is income_tax_main_rates plus + capital_gains_tax; income_tax_before_credits is the regular tax plus + the AMT; and the AMT is Form 6251 line 9 less line 10 (the regular tax), + if more than zero. + 4. Without qualified dividends or net capital gain, the regular tax is + line 46. + 5. Worksheet lines: line 10 is the net capital gain, line 13 the adjusted + net capital gain, line 18 <= line 21 <= line 14, and the main-rates tax + is the tax on line 21. + 6. More wages, long-term gain or qualified dividends never lower the + regular tax. + 7. A household's results do not depend on the other households in its + batch or their order. + And, for every year from 2018 and every filing status, the line 46 limit + binds exactly where the 0 percent rate amount ends below the top of the + 12 percent bracket. +""" + +from collections import namedtuple + +import numpy as np +import pytest + +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") +st = pytest.importorskip("hypothesis.strategies") + +STATUSES = ["SINGLE", "JOINT", "SEPARATE", "HEAD_OF_HOUSEHOLD", "SURVIVING_SPOUSE"] +INF = float("inf") + +# --------------------------------------------------------------------------- +# Dollar amounts. +# --------------------------------------------------------------------------- + +Amounts = namedtuple( + "Amounts", + [ + "zero_rate", # worksheet line 15 + "below_25_percent", # worksheet line 19, the top of the 24% bracket + "fifteen_rate", # worksheet line 26 + "brackets", # (top, rate) rows of the regular rate schedule + ], +) + + +def _rows(*tops): + """The 10 to 37 percent schedule with the given bracket tops.""" + rates = [0.10, 0.12, 0.22, 0.24, 0.32, 0.35, 0.37] + return list(zip(list(tops) + [INF], rates)) + + +# 2025: Schedule D Tax Worksheet lines 15, 19 and 26 as printed; the bracket +# tops are the rows of the 2025 Tax Computation Worksheet (2025 Instructions +# for Form 1040, line 16), whose subtraction amounts imply the 10 and 12 +# percent tops (single: 22% x 48,475 - 5,086 = 5,578.50 = 10% x 11,925 + +# 12% x 36,550). +_UNMARRIED_2025 = (11_925, 48_475, 103_350, 197_300, 250_525) +AMOUNTS = { + 2025: Amounts( + zero_rate={ + "SINGLE": 48_350, + "SEPARATE": 48_350, + "JOINT": 96_700, + "SURVIVING_SPOUSE": 96_700, + "HEAD_OF_HOUSEHOLD": 64_750, + }, + below_25_percent={ + "SINGLE": 197_300, + "SEPARATE": 197_300, + "JOINT": 394_600, + "SURVIVING_SPOUSE": 394_600, + "HEAD_OF_HOUSEHOLD": 197_300, + }, + fifteen_rate={ + "SINGLE": 533_400, + "SEPARATE": 300_000, + "JOINT": 600_050, + "SURVIVING_SPOUSE": 600_050, + "HEAD_OF_HOUSEHOLD": 566_700, + }, + brackets={ + "SINGLE": _rows(*_UNMARRIED_2025, 626_350), + "SEPARATE": _rows(*_UNMARRIED_2025, 375_800), + "JOINT": _rows(23_850, 96_950, 206_700, 394_600, 501_050, 751_600), + "SURVIVING_SPOUSE": _rows( + 23_850, 96_950, 206_700, 394_600, 501_050, 751_600 + ), + "HEAD_OF_HOUSEHOLD": _rows( + 17_000, 64_850, 103_350, 197_300, 250_500, 626_350 + ), + }, + ), + # 2026: Rev. Proc. 2025-32, section 4.01 (Tables 1 to 4) and section 4.03. + 2026: Amounts( + zero_rate={ + "SINGLE": 49_450, + "SEPARATE": 49_450, + "JOINT": 98_900, + "SURVIVING_SPOUSE": 98_900, + "HEAD_OF_HOUSEHOLD": 66_200, + }, + below_25_percent={ + "SINGLE": 201_775, + "SEPARATE": 201_775, + "JOINT": 403_550, + "SURVIVING_SPOUSE": 403_550, + "HEAD_OF_HOUSEHOLD": 201_750, + }, + fifteen_rate={ + "SINGLE": 545_500, + "SEPARATE": 306_850, + "JOINT": 613_700, + "SURVIVING_SPOUSE": 613_700, + "HEAD_OF_HOUSEHOLD": 579_600, + }, + brackets={ + "SINGLE": _rows(12_400, 50_400, 105_700, 201_775, 256_225, 640_600), + "SEPARATE": _rows(12_400, 50_400, 105_700, 201_775, 256_225, 384_350), + "JOINT": _rows(24_800, 100_800, 211_400, 403_550, 512_450, 768_700), + "SURVIVING_SPOUSE": _rows( + 24_800, 100_800, 211_400, 403_550, 512_450, 768_700 + ), + "HEAD_OF_HOUSEHOLD": _rows( + 17_700, 67_450, 105_700, 201_750, 256_200, 640_600 + ), + }, + ), +} + + +def regular_rate_tax(amount, status, year): + """The tax on an amount at the regular rates.""" + tax, bottom = 0.0, 0.0 + for top, rate in AMOUNTS[year].brackets[status]: + if amount <= bottom: + break + tax += rate * (min(amount, top) - bottom) + bottom = top + return tax + + +def test_rate_schedules_match_the_printed_amounts(): + """The rows reproduce the printed tax amounts: the 2025 Tax Computation + Worksheet's subtraction amounts and Rev. Proc. 2025-32's bases.""" + # 2025, rate x amount - subtraction at the top of each row. + for status, row_top, rate, subtraction in [ + ("SINGLE", 197_300, 0.24, 7_153), + ("SINGLE", 626_350, 0.35, 30_452.75), + ("SEPARATE", 375_800, 0.35, 30_452.75), + ("JOINT", 751_600, 0.35, 60_905.50), + ("HEAD_OF_HOUSEHOLD", 250_500, 0.32, 24_676), + ]: + assert regular_rate_tax(row_top, status, 2025) == pytest.approx( + rate * row_top - subtraction + ) + # 2026, the base at the bottom of a row. + for status, row_bottom, base in [ + ("SINGLE", 640_600, 192_979.25), + ("SEPARATE", 384_350, 103_291.75), + ("JOINT", 768_700, 206_583.50), + ("HEAD_OF_HOUSEHOLD", 640_600, 191_171), + ]: + assert regular_rate_tax(row_bottom, status, 2026) == pytest.approx(base) + + +# --------------------------------------------------------------------------- +# The 2025 Schedule D Tax Worksheet, transcribed. +# --------------------------------------------------------------------------- + + +def schedule_d_tax_worksheet(h, line_1, status, year): + """Lines 1 to 47. Returns a dict of line amounts, with line 47 the tax.""" + a = AMOUNTS[year] + schedule_d_15 = h["long_term"] + h["distributions"] + schedule_d_16 = schedule_d_15 + h["short_term"] + schedule_d_18 = h["collectibles"] + schedule_d_19 = h["section_1250"] + line = {1: line_1} + line[46] = regular_rate_tax(line_1, status, year) + # "Exception: Don't use the Qualified Dividends and Capital Gain Tax + # Worksheet or this worksheet to figure your tax if: Line 15 or line 16 + # of Schedule D is zero or less and you have no qualified dividends ...; + # or Form 1040 ... line 15, is zero or less." + if (min(schedule_d_15, schedule_d_16) <= 0 and h["dividends"] == 0) or ( + line_1 <= 0 + ): + line[47] = line[46] + return line + line[2] = h["dividends"] + line[3] = h["election"] + # Form 4952 line 4d, "the excess, if any, of your total gains over your + # total losses", and line 4e, the smaller of line 4d or "the excess, if + # any, of your net long-term capital gain over your net short-term + # capital loss", with every capital asset held for investment. + line_4d = max(0, schedule_d_16) + line[4] = min(line_4d, max(0, schedule_d_15 - max(0, -h["short_term"]))) + line[5] = max(0, line[3] - line[4]) + line[6] = max(0, line[2] - line[5]) + line[7] = min(schedule_d_15, schedule_d_16) + line[8] = min(line[3], line[4]) + line[9] = max(0, line[7] - line[8]) + line[10] = line[6] + line[9] + line[11] = schedule_d_18 + schedule_d_19 + line[12] = min(line[9], line[11]) + line[13] = line[10] - line[12] + line[14] = max(0, line[1] - line[13]) + line[15] = a.zero_rate[status] + line[16] = min(line[1], line[15]) + line[17] = min(line[14], line[16]) + line[18] = max(0, line[1] - line[10]) + line[19] = min(line[1], a.below_25_percent[status]) + line[20] = min(line[14], line[19]) + line[21] = max(line[18], line[20]) + line[22] = line[16] - line[17] + for i in range(23, 44): + line[i] = 0 + # "If lines 1 and 16 are the same, skip lines 23 through 43." + if line[1] != line[16]: + line[23] = min(line[1], line[13]) + line[24] = line[22] + line[25] = max(0, line[23] - line[24]) + line[26] = a.fifteen_rate[status] + line[27] = min(line[1], line[26]) + line[28] = line[21] + line[22] + line[29] = max(0, line[27] - line[28]) + line[30] = min(line[25], line[29]) + line[31] = 0.15 * line[30] + line[32] = line[24] + line[30] + # "If lines 1 and 32 are the same, skip lines 33 through 43." + if line[1] != line[32]: + line[33] = line[23] - line[32] + line[34] = 0.20 * line[33] + # "If Schedule D, line 19, is zero or blank, skip lines 35 + # through 40." + if schedule_d_19 > 0: + line[35] = min(line[9], schedule_d_19) + line[36] = line[10] + line[21] + line[37] = line[1] + line[38] = max(0, line[36] - line[37]) + line[39] = max(0, line[35] - line[38]) + line[40] = 0.25 * line[39] + # "If Schedule D, line 18, is zero or blank, skip lines 41 + # through 43." + if schedule_d_18 > 0: + line[41] = line[21] + line[22] + line[30] + line[33] + line[39] + line[42] = line[1] - line[41] + line[43] = 0.28 * line[42] + line[44] = regular_rate_tax(line[21], status, year) + line[45] = line[31] + line[34] + line[40] + line[43] + line[44] + line[47] = min(line[45], line[46]) + return line + + +def assert_worksheet_identities(line): + """Identities of the transcription itself.""" + if 2 not in line: + return + # Lines 21, 22, 30, 33, 39 and 42 split line 1 among the regular rates + # and the 0, 15, 20, 25 and 28 percent rates. Where the worksheet skips + # lines, the lines it skips would be zero. + split = line[21] + line[22] + line[30] + line[33] + line[39] + line[42] + assert split == pytest.approx(line[1], abs=0.01), line + assert line[45] >= line[44] >= 0 + assert line[47] == min(line[45], line[46]) + assert line[18] <= line[21] <= max(line[14], line[18]) + + +# --------------------------------------------------------------------------- +# The model. +# --------------------------------------------------------------------------- + +OUTPUTS = [ + "filing_status", + "taxable_income", + "has_qdiv_or_ltcg", + "net_capital_gain", + "adjusted_net_capital_gain", + "capital_gains_excluded_from_taxable_income", + "dwks10", + "dwks13", + "dwks14", + "dwks19", + "income_tax_main_rates", + "capital_gains_tax", + "tax_on_taxable_income_at_main_rates", + "regular_tax_before_credits", + "amt_part_iii_required", + "amt_base_tax", + "amt_tax_including_cg", + "alternative_minimum_tax", + "income_tax_before_credits", +] + + +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}" + people[head] = { + "age": {year: 45}, + "employment_income": {year: h["wages"]}, + "qualified_dividend_income": {year: h["dividends"]}, + "long_term_capital_gains": {year: h["long_term"]}, + "short_term_capital_gains": {year: h["short_term"]}, + "non_sch_d_capital_gains": {year: h["distributions"]}, + "investment_income_elected_form_4952": {year: h["election"]}, + # Schedule D line 18 is part of the long-term gain. + "long_term_capital_gains_on_collectibles": {year: h["collectibles"]}, + } + 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: h["section_1250"]}, + } + 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) + + +# --------------------------------------------------------------------------- +# Checks. +# --------------------------------------------------------------------------- + + +def assert_matches_worksheet(households, law, year): + """The model against the transcribed worksheet, household by household.""" + for i, h in enumerate(households): + status = law["filing_status"][i] + assert status == h["status"], (i, status) + line_1 = float(law["taxable_income"][i]) + line = schedule_d_tax_worksheet(h, line_1, status, year) + assert_worksheet_identities(line) + slack = tolerance(line_1) + context = (i, h, line) + assert float(law["tax_on_taxable_income_at_main_rates"][i]) == pytest.approx( + line[46], abs=slack + ), context + assert float(law["regular_tax_before_credits"][i]) == pytest.approx( + line[47], abs=slack + ), context + if 2 in line: + assert float(law["income_tax_main_rates"][i]) == pytest.approx( + line[44], abs=slack + ), context + assert float(law["capital_gains_tax"][i]) == pytest.approx( + line[47] - line[44], abs=slack + ), context + for model_line, worksheet_line in ( + ("dwks10", 10), + ("dwks13", 13), + ("dwks14", 14), + ("dwks19", 21), + ): + assert float(law[model_line][i]) == pytest.approx( + line[worksheet_line], abs=slack + ), (model_line, context) + + +def assert_properties(households, law): + """Properties 1 to 5, for every household.""" + line_1 = law["taxable_income"].astype(float) + tol = tolerance(line_1) + main = law["income_tax_main_rates"].astype(float) + gains_tax = law["capital_gains_tax"].astype(float) + regular = law["regular_tax_before_credits"].astype(float) + line_46 = law["tax_on_taxable_income_at_main_rates"].astype(float) + amt = law["alternative_minimum_tax"].astype(float) + # 1. "Shall not exceed": 0 <= regular tax <= line 46. + assert (regular >= 0).all() + assert (regular <= line_46 + tol).all() + # 2. The preferential computation never exceeds the ordinary one. + assert (gains_tax >= 0).all() + assert (gains_tax <= line_46 - main + tol).all() + assert (main <= regular + tol).all() + # 3. Identities, to single-precision rounding (the model adds 32-bit + # floats: near $384,000 they are 3 cents apart). + assert (np.abs(regular - (main + gains_tax)) <= tol).all() + before_credits = law["income_tax_before_credits"].astype(float) + assert (np.abs(before_credits - (regular + amt)) <= tol).all() + # Form 6251 lines 9 to 11, with no foreign tax credit or Form 4972. + line_9 = np.where( + law["amt_part_iii_required"], + np.minimum(law["amt_base_tax"], law["amt_tax_including_cg"]), + law["amt_base_tax"], + ).astype(float) + assert (np.abs(amt - np.maximum(0, line_9 - regular)) <= tol).all() + # 4. No dividends or gain: the regular tax is line 46, bit for bit. + no_gain = law["net_capital_gain"] == 0 + assert np.array_equal(regular[no_gain], line_46[no_gain]) + # 5. Worksheet lines. + has_gains = law["has_qdiv_or_ltcg"].astype(bool) + assert np.allclose( + law["dwks10"], law["net_capital_gain"], rtol=0, atol=0.01 + 1e-6 * line_1 + ) + assert np.allclose( + law["dwks13"][has_gains], + law["adjusted_net_capital_gain"][has_gains], + rtol=0, + atol=(0.01 + 1e-6 * line_1)[has_gains], + ) + line_14 = law["dwks14"].astype(float) + line_21 = law["dwks19"].astype(float) + line_18 = np.where(has_gains, np.maximum(0, line_1 - law["dwks10"]), 0) + assert (line_18 <= line_21 + tol).all() + assert (line_21 <= line_14 + tol).all() + # income_tax_main_rates taxes line 21, the amount section 1(h)(1)(A) + # taxes at the regular rates. + regular_rate_amount = np.maximum( + 0, line_1 - law["capital_gains_excluded_from_taxable_income"] + ) + assert np.allclose( + line_21[has_gains], regular_rate_amount[has_gains], rtol=0, atol=tol[has_gains] + ) + + +# --------------------------------------------------------------------------- +# Households. +# --------------------------------------------------------------------------- + +DOLLARS = 2_000_000 + + +@st.composite +def households(draw): + long_term = draw(st.one_of(st.just(0), st.integers(-50_000, DOLLARS))) + short_term = draw(st.one_of(st.just(0), st.integers(-100_000, 300_000))) + distributions = draw(st.one_of(st.just(0), st.integers(1, 100_000))) + dividends = draw(st.one_of(st.just(0), st.integers(1, 400_000))) + gain = max(0, min(long_term, long_term + short_term) + distributions) + # Schedule D lines 18 and 19 as shares of the long-term gain. + collectibles = max(0, long_term) * draw(st.sampled_from([0, 0, 10, 50, 100])) // 100 + section_1250 = max(0, long_term) * draw(st.sampled_from([0, 0, 20, 60, 100])) // 100 + return { + "status": draw(st.sampled_from(STATUSES)), + "wages": draw(st.one_of(st.just(0), st.integers(1, 1_500_000))), + "dividends": dividends, + "long_term": long_term, + "short_term": short_term, + "distributions": distributions, + "election": draw( + st.one_of( + st.just(0), + st.just(0), + st.integers(1, DOLLARS), + st.sampled_from([gain, gain + dividends]), + ) + ), + "collectibles": collectibles, + "section_1250": section_1250, + } + + +def household(status, wages=0, dividends=0, long_term=0, **rest): + return { + "status": status, + "wages": wages, + "dividends": dividends, + "long_term": long_term, + "short_term": rest.get("short_term", 0), + "distributions": rest.get("distributions", 0), + "election": rest.get("election", 0), + "collectibles": rest.get("collectibles", 0), + "section_1250": rest.get("section_1250", 0), + } + + +# Wages that put taxable income, with the standard deduction, near the lines +# where the limit binds: the 0 percent rate amount, the top of the 12 percent +# bracket and the top of the 24 percent bracket. +GRID = [ + household(status, wages=wages, dividends=dividends, long_term=long_term, **rest) + for status in STATUSES + for wages in (0, 40_000, 64_000, 110_000, 250_000, 450_000, 900_000) + for dividends, long_term, rest in ( + (0, 0, {}), + (125, 0, {}), + (250, 0, {}), + (2_000, 0, {}), + (0, 30_000, {"short_term": -10_000}), + (5_000, 80_000, {"election": 20_000}), + (0, 200_000, {"collectibles": 50_000, "section_1250": 60_000}), + (10_000, 600_000, {"distributions": 4_000, "section_1250": 100_000}), + ) +] + + +# Standard deductions (2025: Rev. Proc. 2025-32, section 3.01; 2026: section +# 4.14(1)). The surviving spouse here has a dependent child. +STANDARD_DEDUCTION = { + 2025: {"SINGLE": 15_750, "SEPARATE": 15_750, "JOINT": 31_500}, + 2026: {"SINGLE": 16_100, "SEPARATE": 16_100, "JOINT": 32_200}, +} +for _year, _head in ((2025, 23_625), (2026, 24_150)): + STANDARD_DEDUCTION[_year]["HEAD_OF_HOUSEHOLD"] = _head + STANDARD_DEDUCTION[_year]["SURVIVING_SPOUSE"] = STANDARD_DEDUCTION[_year]["JOINT"] + + +def boundary_households(year): + """Ordinary taxable income equal to the 0 percent rate amount, and + qualified dividends up to the top of the 12 percent bracket, then a dollar + less and a dollar more: line 45 exceeds line 46 by 3 percent of the + dividends inside the band.""" + out = [] + for status in STATUSES: + zero_rate = AMOUNTS[year].zero_rate[status] + top_of_12 = AMOUNTS[year].brackets[status][1][0] + wages = zero_rate + STANDARD_DEDUCTION[year][status] + for dividends in (top_of_12 - zero_rate + d for d in (-1, 0, 1)): + out.append(household(status, wages=wages, dividends=dividends)) + return out + + +def test_grid_matches_worksheet_2025_and_2026(): + for year in (2025, 2026): + households = GRID + boundary_households(year) + law = calculate(households, year) + assert_matches_worksheet(households, law, year) + assert_properties(households, law) + lines = [ + schedule_d_tax_worksheet( + h, float(law["taxable_income"][i]), h["status"], year + ) + for i, h in enumerate(households) + ] + excess = np.array( + [line[45] - line[46] if 45 in line else 0.0 for line in lines] + ) + # Every boundary household's line 45 exceeds its line 46, by 3 + # percent of the dividends that fit in the band. + boundary = excess[len(GRID) :].reshape(len(STATUSES), 3) + bands = np.array( + [ + AMOUNTS[year].brackets[status][1][0] - AMOUNTS[year].zero_rate[status] + for status in STATUSES + ] + ) + expected = 0.03 * np.stack([bands - 1, bands, bands], axis=1) + # A dollar above the band is taxed at 22 percent on line 46 and 15 on + # line 45, which takes 7 cents off the excess. + expected[:, 2] -= 0.07 + assert boundary == pytest.approx(expected, abs=0.01), year + + +def test_line_45_above_line_46_by_hand(): + # 2026, single: 49,450 of ordinary taxable income and 950 of qualified + # dividends. Line 44, the tax on 49,450, is 1,240 + 12% x 37,050 = + # 5,686; line 30 is 950 (15%: 142.50), so line 45 is 5,828.50. Line 46, + # the tax on 50,400, is 5,800 (Rev. Proc. 2025-32, Table 3). + h = household("SINGLE", wages=50_400 + 16_100 - 950, dividends=950) + law = calculate([h], 2026) + assert law["taxable_income"][0] == 50_400 + line = schedule_d_tax_worksheet(h, 50_400, "SINGLE", 2026) + assert (line[44], line[45], line[46], line[47]) == pytest.approx( + (5_686, 5_828.50, 5_800, 5_800) + ) + assert law["income_tax_main_rates"][0] == pytest.approx(5_686) + assert law["capital_gains_tax"][0] == pytest.approx(114) + assert law["regular_tax_before_credits"][0] == pytest.approx(5_800) + assert_matches_worksheet([h], law, 2026) + + +def test_limit_binds_at_the_top_of_the_12_percent_bracket_every_year(): + """From 2018, for every filing status: taxable income at the top of the + 12 percent bracket, with qualified dividends from the 0 percent rate amount + up. The dividends are taxed at 15 percent on line 45 and 12 percent on + line 46, so the regular tax is line 46, and the capital gains tax is 12 + percent of the dividends.""" + for year in range(2018, 2036): + p = SYSTEM.parameters(f"{year}-01-01").gov.irs + situation = {"people": {}, "tax_units": {}, "households": {}} + bands = [] + for i, status in enumerate(STATUSES): + top_of_12 = float(p.income.bracket.thresholds["2"][status]) + zero_rate = float(p.capital_gains.thresholds["1"][status]) + band = max(0.0, top_of_12 - zero_rate) + bands.append(band) + situation["people"][f"person_{i}"] = { + "age": {year: 45}, + "qualified_dividend_income": {year: band}, + } + situation["tax_units"][f"tax_unit_{i}"] = { + "members": [f"person_{i}"], + "filing_status": {year: status}, + "taxable_income": {year: top_of_12}, + } + situation["households"][f"household_{i}"] = { + "members": [f"person_{i}"], + "state_code": {year: "TX"}, + } + sim = Simulation(situation=situation) + bands = np.array(bands) + ordinary_rate = p.income.bracket.rates["2"] + preferential_rate = p.capital_gains.rates["2"] + assert preferential_rate > ordinary_rate, year + regular = sim.calculate("regular_tax_before_credits", year) + line_46 = sim.calculate("tax_on_taxable_income_at_main_rates", year) + gains_tax = sim.calculate("capital_gains_tax", year) + assert np.allclose(regular, line_46, rtol=0, atol=0.01), year + assert np.allclose(gains_tax, ordinary_rate * bands, rtol=0, atol=0.01), year + # The band is $100 to $250 wide from 2018 to 2025 and $950 to $1,900 + # in 2026 (Rev. Proc. 2025-32), the published years. + if year <= 2025: + assert ((bands >= 100) & (bands <= 250)).all(), (year, bands) + elif year == 2026: + assert ((bands >= 950) & (bands <= 1_900)).all(), (year, bands) + + +# 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, + ], +) + + +@hypothesis.settings(**SETTINGS) +@hypothesis.given( + st.lists(households(), min_size=1, max_size=25), st.sampled_from([2025, 2026]) +) +def test_random_households_match_worksheet(batch, year): + law = calculate(batch, year) + assert_matches_worksheet(batch, law, year) + assert_properties(batch, law) + + +@hypothesis.settings(**SETTINGS) +@hypothesis.given( + st.lists(households(), min_size=1, max_size=15), + st.sampled_from([2018, 2021, 2024, 2025, 2026, 2027, 2030, 2035]), + st.integers(1, 50_000), +) +# Capital gain distributions with a Schedule D net loss. Before #9788, gross +# income added the distributions outside the Schedule D loss limit, while +# net_capital_gain nets them on Schedule D line 13, so $1 more long-term gain +# raised net capital gain but not taxable income and cut this 2018 single +# filer's regular tax by $0.12. +@hypothesis.example( + [ + household("SINGLE"), + household("SINGLE", short_term=-40_273, distributions=50_000), + ], + 2018, + 1, +) +def test_random_households_keep_the_properties(batch, year, more): + """Properties 1 to 7 in years without a transcription.""" + count = len(batch) + variants = ( + batch + + [{**h, "wages": h["wages"] + more} for h in batch] + + [{**h, "long_term": max(0, h["long_term"]) + more} for h in batch] + + [{**h, "dividends": h["dividends"] + more} for h in batch] + ) + law = calculate(variants, year) + assert_properties(variants, law) + regular = law["regular_tax_before_credits"].astype(float).reshape(4, count) + tol = tolerance(law["taxable_income"].astype(float)).reshape(4, count) + # 6. More wages, long-term gain or dividends never lower the regular tax. + # (Raising a long-term loss to a gain is also more income.) + for k in (1, 2, 3): + assert (regular[k] >= regular[0] - tol[k]).all(), k + # 7. The same households in reverse order give the same results. + reverse = calculate(batch[::-1], year) + for v in OUTPUTS: + assert np.array_equal(reverse[v][::-1], law[v][:count]), v diff --git a/policyengine_us/tests/test_section_911_tax_stacking.py b/policyengine_us/tests/test_section_911_tax_stacking.py index 536f1f8dd0d..379db143da5 100644 --- a/policyengine_us/tests/test_section_911_tax_stacking.py +++ b/policyengine_us/tests/test_section_911_tax_stacking.py @@ -27,19 +27,17 @@ Households live in Texas; some itemize. -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 - taxable income at ordinary rates (worksheet line 25, "the smaller of line - 23 or line 24"). The cap binds only where the 15% rate applies inside the - 12% bracket, a band of $100 to $250 of income. The model is therefore - compared with worksheet line 23, and checked to be within $7.50 of line - 25. -- `capital_gains_tax` taxes 28-percent rate gain in full even when it - 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. +The model takes the section 1(h)(1) cap at the tax on all taxable income +at ordinary rates (worksheet line 25, "the smaller of line 23 or line 24"), +so it is compared with line 25. The cap binds only where the 15% rate +applies inside the 12% bracket, a band of $100 to $250 of income in 2025, +so line 25 is checked to be within $7.50 of line 23. + +One limit of the model sets the scope of these tests, and it does not come +from section 911: `capital_gains_tax` taxes 28-percent rate gain in full +even when it 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. """ import numpy as np @@ -167,9 +165,10 @@ def foreign_earned_income_tax_worksheet( ): """2025 Form 1040 instructions, page 37. - Returns line 6 figured with line 23 of the capital gains worksheet (what - the model computes), line 6 figured with its line 25 (what the form - says), and line 5 of the capital gains worksheet, which Form 6251 needs. + Returns line 6 figured with line 23 of the capital gains worksheet (the + tax before the section 1(h)(1) cap), line 6 figured with its line 25 + (what the form says and the model computes), and line 5 of the capital + gains worksheet, which Form 6251 needs. A filer with no exclusion gets the ordinary line 16 tax. """ line_1 = taxable_income @@ -394,12 +393,14 @@ def assert_matches_worksheets(households, law): ) regular_tax = float(law["regular_tax"][i]) stacked_income = taxable_income + h["exclusion"] - assert regular_tax == pytest.approx(line_6, abs=tolerance(stacked_income)), ( - i, - h, - ) - # The form's line 25 cap, which the model's section 1(h) formulas omit. - cap_gaps.append(regular_tax - line_6_capped) + assert float(law["regular_tax_before_credits"][i]) == pytest.approx( + regular_tax, abs=tolerance(stacked_income) + ), (i, h) + assert regular_tax == pytest.approx( + line_6_capped, abs=tolerance(stacked_income) + ), (i, h) + # The form's line 25 cap takes at most $7.50 off line 23. + cap_gaps.append(line_6 - line_6_capped) line_7 = form_6251_line_7( float(law["amt_income_less_exemptions"][i]), h["exclusion"], @@ -411,7 +412,7 @@ 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) # Form 6251 lines 9 to 11 with no foreign tax credit. - amt = max(0, line_7 - line_6) + amt = max(0, line_7 - line_6_capped) taxable_excess = float( law["amt_income_less_exemptions_plus_section_911_exclusion"][i] ) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/alternative_minimum_tax.py b/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/alternative_minimum_tax.py index 5de3a171f33..a53e7d4c84f 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/alternative_minimum_tax.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/alternative_minimum_tax.py @@ -53,12 +53,13 @@ def formula(tax_unit, period, parameters): foreign_tax_credit = tax_unit("foreign_tax_credit_potential", period) # Line 9 reduced_tax = total_amt_tax - foreign_tax_credit - # Line 10 contains regular tax before credits, lump sum distributions, and capital gains tax + # Line 10: Form 1040 line 16 less any tax from Form 4972, less the + # foreign tax credit. Line 16 is the regular tax including the + # capital gains tax, the same amount income_tax_before_credits adds. regular_tax_before_credits = tax_unit("regular_tax_before_credits", period) lump_sum_distributions = tax_unit("form_4972_lumpsum_distributions", period) - capital_gains_tax = tax_unit("capital_gains_tax", period) - tax_before_credits = regular_tax_before_credits + capital_gains_tax reduced_tax_before_credits = max_( - 0, tax_before_credits - foreign_tax_credit - lump_sum_distributions + 0, + regular_tax_before_credits - foreign_tax_credit - lump_sum_distributions, ) return max_(0, reduced_tax - reduced_tax_before_credits) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/regular_tax_before_credits.py b/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/regular_tax_before_credits.py index bfce22dbb4f..9ee0c8f4475 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/regular_tax_before_credits.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/regular_tax_before_credits.py @@ -1,10 +1,4 @@ from policyengine_us.model_api import * -from policyengine_us.variables.gov.irs.tax.federal_income.before_credits.tax_at_main_rates import ( - tax_at_main_rates, -) -from policyengine_us.variables.gov.irs.tax.federal_income.foreign_earned_income_exclusion.schedule_d_tax_worksheet_after_capital_gain_excess import ( - schedule_d_tax_worksheet_after_capital_gain_excess, -) class regular_tax_before_credits(Variable): @@ -12,68 +6,29 @@ class regular_tax_before_credits(Variable): entity = TaxUnit definition_period = YEAR label = "Regular tax before credits" - documentation = "Regular tax on regular taxable income before credits" + documentation = ( + "The regular tax on all taxable income, qualified dividends and " + "capital gains included, before credits: the tax at the main rates " + "plus the capital gains tax, which income_tax_before_credits also " + "adds. Schedule D Tax Worksheet line 47 (or Qualified Dividends and " + "Capital Gain Tax Worksheet line 25, or the Tax Table or Tax " + "Computation Worksheet without dividends or gains); for a Form 2555 " + "filer, line 6 of the Foreign Earned Income Tax Worksheet. Form 6251 " + "line 10 starts from it." + ) unit = USD - - def formula(tax_unit, period, parameters): - p = parameters(period).gov.irs - filing_status = tax_unit("filing_status", period) - # A Form 2555 filer enters line 3 of the Foreign Earned Income Tax - # Worksheet on line 1 and refigures the capital gain lines for any - # capital gain excess (26 U.S.C. 911(f)). - dwks1 = tax_unit("taxable_income_plus_section_911_exclusion", period) - worksheet = schedule_d_tax_worksheet_after_capital_gain_excess( - tax_unit, period, tax_unit("taxable_income", period) - ) - - dwks16 = min_(p.capital_gains.thresholds["1"][filing_status], dwks1) - dwks17 = min_(tax_unit("dwks14", period), dwks16) - dwks20 = dwks16 - dwks17 - lowest_rate_tax = p.capital_gains.rates["1"] * dwks20 - # Break in worksheet lines - dwks13 = worksheet.line_13 - dwks21 = min_(dwks1, dwks13) - dwks22 = dwks20 - dwks23 = max_(0, dwks21 - dwks22) - dwks25 = min_(p.capital_gains.thresholds["2"][filing_status], dwks1) - dwks19 = tax_unit("dwks19", period) - dwks26 = min_(dwks19, dwks20) - dwks27 = max_(0, dwks25 - dwks26) - dwks28 = min_(dwks23, dwks27) - dwks29 = p.capital_gains.rates["2"] * dwks28 - dwks30 = dwks22 + dwks28 - dwks31 = dwks21 - dwks30 - dwks32 = p.capital_gains.rates["3"] * dwks31 - # Break in worksheet lines - dwks33 = min_(worksheet.line_9, worksheet.unrecaptured_section_1250_gain) - dwks10 = worksheet.line_10 - dwks34 = dwks10 + dwks19 - dwks36 = max_(0, dwks34 - dwks1) - dwks37 = max_(0, dwks33 - dwks36) - - dwks38 = p.income.amt.capital_gains.capital_gain_excess_tax_rate * dwks37 - # Break in worksheet lines - dwks39 = dwks19 + dwks20 + dwks28 + dwks31 + dwks37 - dwks40 = dwks1 - dwks39 - dwks41 = p.income.amt.brackets.rates[-1] * dwks40 - - # Compute regular tax using bracket rates and thresholds - # The shared schedule clamps inverted brackets as in - # income_tax_main_rates (#9084), or the AMT comparator diverges from - # the main-rates tax and manufactures phantom AMT for qdiv/LTCG - # filers. - reg_taxinc = max_(0, dwks19) - reg_tax = tax_at_main_rates(reg_taxinc, filing_status, p.income.bracket) - - # Return to worksheet lines - dwks42 = reg_tax - dwks43 = dwks29 + dwks32 + dwks38 + dwks41 + dwks42 + lowest_rate_tax - # Foreign Earned Income Tax Worksheet, lines 4 to 6: the worksheet - # tax on line 3 less the tax on the excluded amount alone. - # income_tax_main_rates is already net of it. - excluded = max_(0, tax_unit("foreign_earned_income_exclusion", period)) - tax_on_excluded = tax_at_main_rates(excluded, filing_status, p.income.bracket) - dwks43 = where(excluded > 0, max_(0, dwks43 - tax_on_excluded), dwks43) - dwks44 = tax_unit("income_tax_main_rates", period) - dwks45 = min_(dwks43, dwks44) - return where(tax_unit("has_qdiv_or_ltcg", period), dwks45, dwks44) + reference = [ + dict( + title="26 U.S. Code § 1(h)(1)", + href="https://www.law.cornell.edu/uscode/text/26/1#h_1", + ), + dict( + title="2025 Instructions for Schedule D (Form 1040), Schedule D Tax Worksheet, line 47", + href="https://www.irs.gov/pub/irs-prior/i1040sd--2025.pdf#page=16", + ), + dict( + title="2025 Form 6251, line 10", + href="https://www.irs.gov/pub/irs-prior/f6251--2025.pdf#page=1", + ), + ] + adds = ["income_tax_main_rates", "capital_gains_tax"] diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/before_credits/tax_on_taxable_income_at_main_rates.py b/policyengine_us/variables/gov/irs/tax/federal_income/before_credits/tax_on_taxable_income_at_main_rates.py new file mode 100644 index 00000000000..4d9ebdb2cb5 --- /dev/null +++ b/policyengine_us/variables/gov/irs/tax/federal_income/before_credits/tax_on_taxable_income_at_main_rates.py @@ -0,0 +1,48 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.irs.tax.federal_income.before_credits.tax_at_main_rates import ( + tax_at_main_rates, +) + + +class tax_on_taxable_income_at_main_rates(Variable): + value_type = float + entity = TaxUnit + definition_period = YEAR + label = "Tax on all taxable income at the main rates" + documentation = ( + "The tax on all taxable income, capital gains and qualified dividends " + "included, at the ordinary rate schedule of 26 U.S.C. 1: Schedule D " + "Tax Worksheet line 46 and Qualified Dividends and Capital Gain Tax " + "Worksheet line 24. Section 1(h)(1) caps the regular tax at this " + "amount. For a taxpayer excluding foreign earned income, taxable " + "income includes the excluded amount and the tax on the excluded " + "amount alone is subtracted, as in income_tax_main_rates." + ) + unit = USD + reference = [ + dict( + title="26 U.S. Code § 1(h)(1)", + href="https://www.law.cornell.edu/uscode/text/26/1#h_1", + ), + dict( + title="26 U.S. Code § 911(f)(1)(A)", + href="https://www.law.cornell.edu/uscode/text/26/911#f_1_A", + ), + dict( + title="2025 Instructions for Schedule D (Form 1040), Schedule D Tax Worksheet, lines 46 and 47", + href="https://www.irs.gov/pub/irs-prior/i1040sd--2025.pdf#page=16", + ), + ] + + def formula(tax_unit, period, parameters): + # Worksheet line 1: taxable income, or for a Form 2555 filer line 3 + # of the Foreign Earned Income Tax Worksheet. + taxable_income = tax_unit("taxable_income_plus_section_911_exclusion", period) + bracket = parameters(period).gov.irs.income.bracket + filing_status = tax_unit("filing_status", period) + tax = tax_at_main_rates(max_(0, taxable_income), filing_status, bracket) + # 26 U.S.C. 911(f)(1)(A): less the tax on the excluded amount alone + # (Foreign Earned Income Tax Worksheet lines 5 and 6). + excluded = max_(0, tax_unit("foreign_earned_income_exclusion", period)) + tax_on_excluded = tax_at_main_rates(excluded, filing_status, bracket) + return where(excluded > 0, max_(0, tax - tax_on_excluded), tax) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/capital_gains_tax.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/capital_gains_tax.py index 9bef17774d5..10db9bbb169 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/capital_gains_tax.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/capital_gains_tax.py @@ -38,6 +38,28 @@ def rate_gain_taxed_at_28_percent( ) +def limit_to_tax_at_main_rates(tax_unit, period, capital_gains_tax): + """The capital gains tax that 26 U.S.C. 1(h)(1) allows. + + With a net capital gain, "the tax imposed by this section for such + taxable year shall not exceed the sum of" the amounts in subparagraphs + (A) to (F), so the regular tax is the smaller of that sum and the tax on + all taxable income at the main rates (Schedule D Tax Worksheet line 47, + "the smaller of line 45 or line 46"; Qualified Dividends and Capital Gain + Tax Worksheet line 25). income_tax_main_rates is subparagraph (A), so the + tax on the gains is at most the rest of the tax at the main rates. The + limit binds where gain taxed at 15 percent sits in the 12 percent bracket, + which happens when the 0 percent rate amount is below the top of that + bracket. + """ + tax_at_main_rates_on_the_rest = max_( + 0, + tax_unit("tax_on_taxable_income_at_main_rates", period) + - tax_unit("income_tax_main_rates", period), + ) + return min_(capital_gains_tax, tax_at_main_rates_on_the_rest) + + class capital_gains_tax(Variable): value_type = float entity = TaxUnit @@ -53,6 +75,10 @@ class capital_gains_tax(Variable): title="26 U.S. Code § 911(f)", href="https://www.law.cornell.edu/uscode/text/26/911#f", ), + dict( + title="2025 Instructions for Schedule D (Form 1040), Schedule D Tax Worksheet, line 47", + href="https://www.irs.gov/pub/irs-prior/i1040sd--2025.pdf#page=16", + ), ] def formula(tax_unit, period, parameters): @@ -138,4 +164,8 @@ def formula(tax_unit, period, parameters): taxable_unrecaptured_gain, ) - return main_cg_tax + unrecaptured_gain_tax + remaining_cg_tax + return limit_to_tax_at_main_rates( + tax_unit, + period, + main_cg_tax + unrecaptured_gain_tax + remaining_cg_tax, + ) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_capital_gain_excess.py b/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_capital_gain_excess.py index 1cc7a77763a..e2f420d8c84 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_capital_gain_excess.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_capital_gain_excess.py @@ -13,11 +13,11 @@ class section_911_capital_gain_excess(Variable): "taxable income plus the excluded amount, the capital gains are " "reduced by this excess, so that the excluded amount is never taxed " "at the capital gains rates. The section 1(h) formulas use this " - "amount; the Schedule D Tax Worksheet lines (dwks14, dwks19, " - "regular_tax_before_credits) measure the excess from worksheet line " - "10, as the worksheet does. On a return the two are the same amount, " - "and in the model dwks10 equals net_capital_gain for the same inputs, " - "apart from single-precision rounding." + "amount; the Schedule D Tax Worksheet lines (dwks14 and dwks19) " + "measure the excess from worksheet line 10, as the worksheet does. On " + "a return the two are the same amount, and in the model dwks10 equals " + "net_capital_gain for the same inputs, apart from single-precision " + "rounding." ) definition_period = YEAR reference = [