From ee0cff605b3a1cdce54e069dd34dabf85513056b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 18:36:08 -0400 Subject: [PATCH 1/8] Count Form 4835 farm rental income in net investment income Form 8960 line 4a takes Schedule 1 line 5, which includes Schedule E line 40 (Form 4835 net farm rental income or loss). Form 4835 is only for a rental activity under the passive activity rules, so it is rent under 26 USC 1411(c)(1)(A)(i). Add farm_rent_income to the NII sources, with worked Form 8960 tests and the dependent invariants. Co-Authored-By: Claude Opus 5.5 --- changelog.d/niit-farm-rent.fixed.md | 1 + .../gov/irs/investment/income/sources.yaml | 21 ++++ ...endent_net_investment_income_invariants.py | 9 +- .../net_investment_income_tax.yaml | 117 ++++++++++++++++++ .../gov/states/mn/tax/income/mn_niit.yaml | 24 ++++ .../income/person/farm/farm_rent_income.py | 11 +- 6 files changed, 179 insertions(+), 4 deletions(-) create mode 100644 changelog.d/niit-farm-rent.fixed.md diff --git a/changelog.d/niit-farm-rent.fixed.md b/changelog.d/niit-farm-rent.fixed.md new file mode 100644 index 00000000000..a349ed72edd --- /dev/null +++ b/changelog.d/niit-farm-rent.fixed.md @@ -0,0 +1 @@ +Count Form 4835 farm rental income (Schedule E line 40) in net investment income, so it reaches the net investment income tax and Minnesota's net investment income tax. diff --git a/policyengine_us/parameters/gov/irs/investment/income/sources.yaml b/policyengine_us/parameters/gov/irs/investment/income/sources.yaml index 9b1f6303618..f6e0e850e26 100644 --- a/policyengine_us/parameters/gov/irs/investment/income/sources.yaml +++ b/policyengine_us/parameters/gov/irs/investment/income/sources.yaml @@ -10,6 +10,17 @@ values: - taxable_interest_income - dividend_income - rental_income + # Form 4835 net farm rental income or loss goes on Schedule E line 40, + # then into line 41 and Schedule 1 line 5, which Form 8960 line 4a takes. + # Form 4835 is only for a rental activity under the passive activity + # rules, and its income isn't subject to self-employment tax, so it is + # rent under § 1411(c)(1)(A)(i). Form 8960 line 4b removes a rental's net + # income or loss only when the rental is nonpassive and rises to a section + # 162 trade or business (for example a real estate professional's rental, + # or property rented to the owner's own nonpassive business). The model + # has no input for that, here or for rental_income, so it counts all of + # both. + - farm_rent_income - passive_partnership_s_corp_income # Income from estates and trusts (Schedule E Part III) keeps its character # in the beneficiary's hands (§§ 652(b), 662(b)). Form 8960 line 4a @@ -49,3 +60,13 @@ metadata: href: https://www.irs.gov/pub/irs-prior/i8960--2024.pdf#page=11 - title: 2025 Instructions for Form 8814, Net Investment Income Tax (NIIT) href: https://www.irs.gov/pub/irs-pdf/i8814.pdf#page=2 + - title: 26 U.S. Code § 1411(c)(2) - Trades and businesses to which tax applies + href: https://www.law.cornell.edu/uscode/text/26/1411#c_2 + - title: 2025 Instructions for Form 8960, passive activity general rules (rental activities) + href: https://www.irs.gov/pub/irs-prior/i8960--2025.pdf#page=4 + - title: 2025 Instructions for Form 8960, lines 4a and 4b + href: https://www.irs.gov/pub/irs-prior/i8960--2025.pdf#page=8 + - title: 2025 Form 4835, Farm Rental Income and Expenses, purpose of form + href: https://www.irs.gov/pub/irs-prior/f4835--2025.pdf#page=2 + - title: 2025 Schedule E (Form 1040), lines 40 and 41 + href: https://www.irs.gov/pub/irs-prior/f1040se--2025.pdf#page=2 diff --git a/policyengine_us/tests/core/test_dependent_net_investment_income_invariants.py b/policyengine_us/tests/core/test_dependent_net_investment_income_invariants.py index f9c54139074..b7b4efb4c5f 100644 --- a/policyengine_us/tests/core/test_dependent_net_investment_income_invariants.py +++ b/policyengine_us/tests/core/test_dependent_net_investment_income_invariants.py @@ -17,9 +17,10 @@ filer's `net_investment_income`, `filer_loss_limited_net_capital_gains`, AGI, NIIT MAGI or `net_investment_income_tax`. 2. `net_investment_income` equals an independent numpy computation over the - head and spouse: their interest, dividends, rents, passive pass-through, - estate and trust income, plus their netted Schedule D gain or loss with - capital gain distributions on line 13, limited under 26 USC 1211(b). + head and spouse: their interest, dividends, rents (including Form 4835 + farm rent), passive pass-through, estate and trust income, plus their + netted Schedule D gain or loss with capital gain distributions on line 13, + limited under 26 USC 1211(b). 3. -loss limit <= `filer_loss_limited_net_capital_gains`, and it equals `loss_limited_net_capital_gains` when the unit has no dependents. 4. For tax units without dependents, `net_investment_income` equals the @@ -43,6 +44,7 @@ # income is drawn as a share of partnership_s_corp_income below. SIGNED_INPUTS = [ "rental_income", + "farm_rent_income", "estate_income", "short_term_capital_gains", "long_term_capital_gains", @@ -70,6 +72,7 @@ "qualified_dividend_income", "non_qualified_dividend_income", "rental_income", + "farm_rent_income", "passive_partnership_s_corp_income", "estate_income", ] diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_tax.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_tax.yaml index d4983683baa..99f9995479f 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_tax.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_tax.yaml @@ -488,3 +488,120 @@ # The default (the full 100,000) would give 1,900. niit_magi: 220_000 net_investment_income_tax: 760 + +- name: Farm rental income counts as net investment income + # 2025 Form 4835 line 32 net farm rental income of 30,000 goes on Schedule + # E line 40, then into line 41 and Schedule 1 line 5. Form 8960 line 4a + # takes Schedule 1 line 5. Form 4835 is only for a rental activity under the + # passive activity rules, so line 4b removes nothing. Line 12 NII 30,000; + # line 13 MAGI 190,000 + 30,000 = 220,000; line 15 220,000 - 200,000 = + # 20,000; line 16 min(30,000, 20,000) = 20,000; line 17 3.8% x 20,000 = 760. + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + employment_income: 190_000 + farm_rent_income: 30_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + adjusted_gross_income: 220_000 + net_investment_income: 30_000 + net_investment_income_tax: 760 + +- name: A farm rental loss nets against other rents on Schedule E line 41 + # Schedule E line 26 rents of 40,000 and a Form 4835 line 34c loss of + # 10,000 (allowed in full against the passive rental income) give line 41 + # and Schedule 1 line 5 of 30,000, so Form 8960 line 4a is 30,000. AGI and + # MAGI 250,000 + 30,000 = 280,000; line 15 80,000; line 16 min(30,000, + # 80,000) = 30,000; line 17 3.8% x 30,000 = 1,140. Leaving the loss out + # would give 3.8% x 40,000 = 1,520. + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + employment_income: 250_000 + rental_income: 40_000 + farm_rent_income: -10_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + adjusted_gross_income: 280_000 + net_investment_income: 30_000 + net_investment_income_tax: 1_140 + +- name: A spouse's farm rental income counts on a joint return + # 2024 Form 8960 line 4a also takes Schedule 1 line 5. Line 12 NII 35,000; + # line 13 MAGI 240,000 + 35,000 = 275,000; line 14 joint threshold + # 250,000; line 15 25,000; line 16 min(35,000, 25,000) = 25,000; line 17 + # 3.8% x 25,000 = 950. + period: 2024 + input: + people: + head: + age: 60 + is_tax_unit_head: true + employment_income: 240_000 + spouse: + age: 58 + is_tax_unit_spouse: true + farm_rent_income: 35_000 + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: TX + output: + filing_status: JOINT + adjusted_gross_income: 275_000 + net_investment_income: 35_000 + net_investment_income_tax: 950 + +- name: A dependent's farm rental income stays off the filer's Form 8960 + # The child's Form 4835 income goes on the child's own return, so the + # parent's line 12 NII is the 5,000 of interest. MAGI 210,000 + 5,000 = + # 215,000; line 15 15,000; line 17 3.8% x min(5,000, 15,000) = 190. + # Counting the child's rent would give 3.8% x 15,000 = 570. + period: 2025 + input: + people: + parent: + age: 50 + is_tax_unit_head: true + employment_income: 210_000 + taxable_interest_income: 5_000 + child: + age: 16 + is_tax_unit_dependent: true + farm_rent_income: 10_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: TX + output: + irs_gross_income: [215_000, 0] + adjusted_gross_income: 215_000 + net_investment_income: 5_000 + net_investment_income_tax: 190 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/mn_niit.yaml b/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/mn_niit.yaml index 277a05bfac5..c0ae2eb6c5c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/mn_niit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/mn_niit.yaml @@ -15,3 +15,27 @@ state_code: MN output: mn_niit: 1_000 # (1_100_000 - 1_000_000) * 0.01 + +- name: Minnesota net investment income includes farm rental income + # Minn. Stat. 290.033(a) uses the section 1411(c) definition, less gain on + # class 2a property, so Form 4835 rent counts. NII 800,000 of interest + + # 300,000 of farm rent = 1,100,000; 1% x (1,100,000 - 1,000,000) = 1,000. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + taxable_interest_income: 800_000 + farm_rent_income: 300_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MN + output: + net_investment_income: 1_100_000 + mn_niit: 1_000 diff --git a/policyengine_us/variables/household/income/person/farm/farm_rent_income.py b/policyengine_us/variables/household/income/person/farm/farm_rent_income.py index dd5d723a63d..b2d9522b40b 100644 --- a/policyengine_us/variables/household/income/person/farm/farm_rent_income.py +++ b/policyengine_us/variables/household/income/person/farm/farm_rent_income.py @@ -5,7 +5,16 @@ class farm_rent_income(Variable): value_type = float entity = Person label = "farm rental income" + documentation = ( + "Net farm rental income or loss from Form 4835: crop or livestock " + "shares a landowner (or sub-lessor) receives from a farm in which " + "they do not materially participate. It is reported on Schedule E " + "line 40 and is not subject to self-employment tax." + ) unit = USD definition_period = YEAR - reference = "https://www.law.cornell.edu/uscode/text/26/1402" + reference = ( + "https://www.law.cornell.edu/uscode/text/26/1402", + "https://www.irs.gov/pub/irs-prior/f4835--2025.pdf#page=2", + ) uprating = "calibration.gov.irs.soi.farm_rent_income" From 32b77d3826f7b8eb426e3f7b86fac96b61f81d19 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 18:45:19 -0400 Subject: [PATCH 2/8] Cite the 2025 Form 8960 instructions as one reference Co-Authored-By: Claude Opus 5.5 --- .../parameters/gov/irs/investment/income/sources.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/policyengine_us/parameters/gov/irs/investment/income/sources.yaml b/policyengine_us/parameters/gov/irs/investment/income/sources.yaml index f6e0e850e26..2c6bf48d19b 100644 --- a/policyengine_us/parameters/gov/irs/investment/income/sources.yaml +++ b/policyengine_us/parameters/gov/irs/investment/income/sources.yaml @@ -62,10 +62,8 @@ metadata: href: https://www.irs.gov/pub/irs-pdf/i8814.pdf#page=2 - title: 26 U.S. Code § 1411(c)(2) - Trades and businesses to which tax applies href: https://www.law.cornell.edu/uscode/text/26/1411#c_2 - - title: 2025 Instructions for Form 8960, passive activity general rules (rental activities) + - title: "2025 Instructions for Form 8960, passive activity rules and lines 4a and 4b #page 4,8" href: https://www.irs.gov/pub/irs-prior/i8960--2025.pdf#page=4 - - title: 2025 Instructions for Form 8960, lines 4a and 4b - href: https://www.irs.gov/pub/irs-prior/i8960--2025.pdf#page=8 - title: 2025 Form 4835, Farm Rental Income and Expenses, purpose of form href: https://www.irs.gov/pub/irs-prior/f4835--2025.pdf#page=2 - title: 2025 Schedule E (Form 1040), lines 40 and 41 From de4c4e13c4f3d42dff691d97f6e34a2de93c0524 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 18:50:27 -0400 Subject: [PATCH 3/8] Update PolicyBench scenario 120 for the farm rental loss in NII Its federal target was the policyengine-us 2.37.2 evidence value. The -2,446.59 Form 4835 loss now reduces net investment income as it already reduced AGI, so NIIT falls by 3.8% x 2,446.59. Co-Authored-By: Claude Opus 5.5 --- .../tests/policy/baseline/gov/irs/ira/policybench_ira.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/ira/policybench_ira.yaml b/policyengine_us/tests/policy/baseline/gov/irs/ira/policybench_ira.yaml index dc32eb9c434..6308ba77e0c 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/ira/policybench_ira.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/ira/policybench_ira.yaml @@ -683,5 +683,9 @@ output: tax_units: tax_unit: - income_tax_before_refundable_credits: 40423.17578125 + # The 2.37.2 evidence value was 40423.17578125. Net investment income + # now includes the -2,446.59 Form 4835 farm rental loss (Schedule E + # line 40), as AGI already did: NII 4,300 -> 1,853.41 with MAGI + # 234,414.20, so NIIT falls by 3.8% x 2,446.59 = 92.97. + income_tax_before_refundable_credits: 40330.20703125 state_income_tax_before_refundable_credits: 11919.0576171875 From 480c69480511a1e46cc3dc257802b819aace9864 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 11:34:24 -0400 Subject: [PATCH 4/8] Limit net investment income losses to what section 461(l) allows Section 1411(c)(1)(B) allows only deductions allowed by chapter 1, and section 461(l) disallows business losses beyond business income plus the threshold. NII added Schedule E rental, Form 4835, estate and passive pass-through losses in full while AGI capped them. Add excess_business_loss (Form 461 line 16) and net_investment_income_excess_business_loss, the part of it that falls on losses NII counts (all of it when they are the only business losses, a proportional share otherwise), and add it back in NII. Co-Authored-By: Claude Opus 5.5 --- .../gov/irs/investment/income/sources.yaml | 12 ++ .../excess_business_loss.yaml | 115 ++++++++++++++ ...nvestment_income_excess_business_loss.yaml | 143 ++++++++++++++++++ .../net_investment_income_tax.yaml | 79 ++++++++++ .../excess_business_loss.py | 26 ++++ .../limited_business_loss.py | 109 ++++++------- ..._investment_income_excess_business_loss.py | 58 +++++++ 7 files changed, 476 insertions(+), 66 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/excess_business_loss.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_excess_business_loss.yaml create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/excess_business_loss.py create mode 100644 policyengine_us/variables/gov/irs/tax/federal_income/net_investment_income_excess_business_loss.py diff --git a/policyengine_us/parameters/gov/irs/investment/income/sources.yaml b/policyengine_us/parameters/gov/irs/investment/income/sources.yaml index 2c6bf48d19b..db2f02d7a7b 100644 --- a/policyengine_us/parameters/gov/irs/investment/income/sources.yaml +++ b/policyengine_us/parameters/gov/irs/investment/income/sources.yaml @@ -36,6 +36,12 @@ values: # (line 13), limited under § 1211(b). Distributions reported without # Schedule D are the same amount. - filer_loss_limited_net_capital_gains + # The losses above count in full, but section 461(l) disallows business + # losses beyond business income plus a threshold, and § 1411(c)(1)(B) + # allows only deductions allowed by chapter 1. This adds back the part of + # the excess business loss that falls on those losses (all of it when they + # are the only business losses; a proportional share otherwise). + - net_investment_income_excess_business_loss metadata: unit: currency-USD label: net investment income sources @@ -68,3 +74,9 @@ metadata: href: https://www.irs.gov/pub/irs-prior/f4835--2025.pdf#page=2 - title: 2025 Schedule E (Form 1040), lines 40 and 41 href: https://www.irs.gov/pub/irs-prior/f1040se--2025.pdf#page=2 + - title: 26 U.S. Code § 1411(c)(1)(B) - Deductions allowed by this subtitle + href: https://www.law.cornell.edu/uscode/text/26/1411#c_1_B + - title: 26 U.S. Code § 461(l) - Limitation on excess business losses of noncorporate taxpayers + href: https://www.law.cornell.edu/uscode/text/26/461#l + - title: Treas. Reg. 1.1411-4(f) - Properly allocable deductions + href: https://www.ecfr.gov/current/title-26/section-1.1411-4#p-1.1411-4(f) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/excess_business_loss.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/excess_business_loss.yaml new file mode 100644 index 00000000000..9006a281c36 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/excess_business_loss.yaml @@ -0,0 +1,115 @@ +# Section 461(l): business losses beyond business income plus the threshold +# are disallowed this year (Form 461 line 16). 2025 thresholds: 313,000, or +# 626,000 on a joint return. + +- name: A loss within the threshold leaves no excess + period: 2025 + input: + people: + person1: + age: 50 + is_tax_unit_head: true + self_employment_income: -100_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + limited_business_loss: 100_000 + excess_business_loss: 0 + +- name: A Form 4835 loss beyond the threshold + # Allowed 313,000 of the 600,000 loss; 287,000 disallowed. + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + farm_rent_income: -600_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + limited_business_loss: 313_000 + excess_business_loss: 287_000 + +- name: Business income raises the limit + # Limit 100,000 + 313,000 = 413,000; 600,000 - 413,000 = 187,000. + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + rental_income: 100_000 + farm_rent_income: -600_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + limited_business_loss: 413_000 + excess_business_loss: 187_000 + +- name: A joint return combines both spouses' losses + # 400,000 + 400,000 = 800,000; 800,000 - 626,000 = 174,000. + period: 2025 + input: + people: + head: + age: 60 + is_tax_unit_head: true + self_employment_income: -400_000 + spouse: + age: 58 + is_tax_unit_spouse: true + farm_rent_income: -400_000 + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: TX + output: + filing_status: JOINT + limited_business_loss: 626_000 + excess_business_loss: 174_000 + +- name: A dependent's loss stays off the filer's Form 461 + # Only the parent's 400,000; 400,000 - 313,000 = 87,000. + period: 2025 + input: + people: + parent: + age: 50 + is_tax_unit_head: true + self_employment_income: -400_000 + child: + age: 16 + is_tax_unit_dependent: true + farm_rent_income: -400_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: TX + output: + limited_business_loss: 313_000 + excess_business_loss: 87_000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_excess_business_loss.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_excess_business_loss.yaml new file mode 100644 index 00000000000..95307442816 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_excess_business_loss.yaml @@ -0,0 +1,143 @@ +# The part of the section 461(l) excess business loss (2025 single threshold +# 313,000) that falls on losses net investment income counts. Net investment +# income adds it back (26 USC 1411(c)(1)(B): deductions allowed by chapter 1). + +- name: All of the excess when net investment income losses are the only losses + # Form 4835 loss 600,000; excess 600,000 - 313,000 = 287,000, all of it on + # the farm rental loss. + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + farm_rent_income: -600_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + excess_business_loss: 287_000 + net_investment_income_excess_business_loss: 287_000 + +- name: None of the excess when no loss is a net investment income loss + period: 2025 + input: + people: + person1: + age: 50 + is_tax_unit_head: true + self_employment_income: -600_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + excess_business_loss: 287_000 + net_investment_income_excess_business_loss: 0 + +- name: A proportional share when both kinds of loss are present + # Schedule C loss 300,000 and Form 4835 loss 300,000; excess 287,000. + # Proportional share: 287,000 x 300,000 / 600,000 = 143,500. (The law does + # not say how to split it; see the variable documentation.) + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + self_employment_income: -300_000 + farm_rent_income: -300_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + excess_business_loss: 287_000 + net_investment_income_excess_business_loss: 143_500 + +- name: Only the passive part of a partnership loss counts + # Partnership/S-corp loss 400,000, of which 100,000 is passive; excess + # 400,000 - 313,000 = 87,000; share 87,000 x 100,000 / 400,000 = 21,750. + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + partnership_s_corp_income: -400_000 + passive_partnership_s_corp_income: -100_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + excess_business_loss: 87_000 + net_investment_income_excess_business_loss: 21_750 + +- name: An estate loss removed by a code H adjustment is not a net investment income loss + # Schedule E Part III loss 600,000 with a +600,000 Schedule K-1 (Form 1041) + # box 14 code H adjustment, so net investment income has no estate loss. + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + estate_income: -600_000 + estate_income_net_investment_income_adjustment: 600_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + excess_business_loss: 287_000 + net_investment_income_excess_business_loss: 0 + +- name: A joint return shares the excess across both spouses' losses + # Head Schedule E rental loss 500,000; spouse partnership loss 400,000, of + # which 200,000 is passive. Losses 900,000; excess 900,000 - 626,000 = + # 274,000; net investment income losses 500,000 + 200,000 = 700,000; share + # 274,000 x 700,000 / 900,000 = 213,111.11. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + head: + age: 60 + is_tax_unit_head: true + rental_income: -500_000 + spouse: + age: 58 + is_tax_unit_spouse: true + partnership_s_corp_income: -400_000 + passive_partnership_s_corp_income: -200_000 + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: TX + output: + filing_status: JOINT + excess_business_loss: 274_000 + net_investment_income_excess_business_loss: 213_111.11 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_tax.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_tax.yaml index 99f9995479f..089a3ff188c 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_tax.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_tax.yaml @@ -605,3 +605,82 @@ adjusted_gross_income: 215_000 net_investment_income: 5_000 net_investment_income_tax: 190 + +- name: A Form 4835 loss reduces net investment income only as far as section 461(l) allows + # Independent review counterexample. Wages 300,000, interest 500,000, Form + # 4835 loss 600,000 (allowed under the at-risk and passive activity rules). + # Section 461(l) allows 313,000 and disallows 287,000. AGI and MAGI + # 300,000 + 500,000 - 313,000 = 487,000. NII 500,000 - 313,000 = 187,000. + # NIIT 3.8% x min(187,000, 487,000 - 200,000) = 7,106. Counting the full + # loss would give NII -100,000 and no tax. + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + employment_income: 300_000 + taxable_interest_income: 500_000 + farm_rent_income: -600_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + adjusted_gross_income: 487_000 + net_investment_income: 187_000 + net_investment_income_tax: 7_106 + +- name: A Schedule E rental loss is limited the same way + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + employment_income: 300_000 + taxable_interest_income: 500_000 + rental_income: -600_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + adjusted_gross_income: 487_000 + net_investment_income: 187_000 + net_investment_income_tax: 7_106 + +- name: A Schedule C loss takes a proportional share of the section 461(l) excess + # Wages 600,000, interest 200,000, Schedule C loss 300,000, Form 4835 loss + # 300,000. Section 461(l) allows 313,000 of the 600,000; AGI and MAGI + # 800,000 - 313,000 = 487,000. Proportional share of the 287,000 excess on + # the farm rental loss: 143,500, so NII 200,000 - 300,000 + 143,500 = + # 43,500. NIIT 3.8% x min(43,500, 287,000) = 1,653. (No rule says how to + # split the excess; see net_investment_income_excess_business_loss.) + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + employment_income: 600_000 + taxable_interest_income: 200_000 + self_employment_income: -300_000 + farm_rent_income: -300_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + adjusted_gross_income: 487_000 + net_investment_income: 43_500 + net_investment_income_tax: 1_653 diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/excess_business_loss.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/excess_business_loss.py new file mode 100644 index 00000000000..bf3a82ac197 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/excess_business_loss.py @@ -0,0 +1,26 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.irs.income.taxable_income.adjusted_gross_income.above_the_line_deductions.limited_business_loss import ( + section_461l_business_income_and_loss, +) + + +class excess_business_loss(Variable): + value_type = float + entity = TaxUnit + label = "Excess business loss disallowed under section 461(l)" + unit = USD + documentation = ( + "The head and spouse's business losses beyond business income plus " + "the section 461(l) threshold (Form 461 line 16): the part of their " + "business losses that limited_business_loss does not deduct this " + "year." + ) + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/461#l", + "https://www.irs.gov/pub/irs-prior/i461--2025.pdf#page=3", + ) + + def formula(tax_unit, period, parameters): + _, business_loss = section_461l_business_income_and_loss(tax_unit, period) + return max_(0, business_loss - tax_unit("limited_business_loss", period)) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/limited_business_loss.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/limited_business_loss.py index 153c74d4982..d48a0a7a485 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/limited_business_loss.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/limited_business_loss.py @@ -1,5 +1,45 @@ from policyengine_us.model_api import * +# Person-level business items that section 461(l) aggregates, in the order +# they are summed: Schedule C, Schedule F, Schedule E rental, Form 4835 farm +# rent, Schedule E estate/trust and partnership/S-corp income. A positive +# amount is business income and a negative amount a business loss. +SECTION_461L_PERSON_ITEMS = [ + "total_self_employment_income", + "farm_operations_income", + "rental_income", + "farm_rent_income", + "estate_income", + "partnership_s_corp_income", +] + + +def section_461l_business_income_and_loss(tax_unit, period): + """ + The head and spouse's business income and business losses under 461(l). + + Returns the two aggregates section 461(l)(3)(A) compares: gross business + income and gains, and business deductions, each summed over the items in + SECTION_461L_PERSON_ITEMS and Form 4797 other net gain. A tax-unit + dependent's business, rental and estate items belong on the dependent's + own return, as in irs_gross_income. They neither add to this return's + losses nor raise its section 461(l) limit. + """ + person = tax_unit.members + not_dependent = ~person("is_tax_unit_dependent", period) + business_income = 0 + business_loss = 0 + for item in SECTION_461L_PERSON_ITEMS: + amount = person(item, period) + business_income = business_income + tax_unit.sum( + not_dependent * max_(0, amount) + ) + business_loss = business_loss + tax_unit.sum(not_dependent * max_(0, -amount)) + other_net_gain = tax_unit("other_net_gain", period) + business_income = business_income + max_(0, other_net_gain) + business_loss = business_loss + max_(0, -other_net_gain) + return business_income, business_loss + class limited_business_loss(Variable): value_type = float @@ -22,75 +62,12 @@ def formula(tax_unit, period, parameters): filing_status = tax_unit("filing_status", period) # Section 461(l) excess business loss limitation threshold_amount = parameters(period).gov.irs.ald.loss.max[filing_status] - person = tax_unit.members - # A tax-unit dependent's business, rental and estate items belong on - # the dependent's own return, as in irs_gross_income. They neither - # add to this return's losses nor raise its Section 461(l) limit. - not_dependent = ~person("is_tax_unit_dependent", period) - # Section 461(l) compares business deductions to business gross # income/gains plus the threshold amount. In this model, positive # business amounts flow into gross income and losses are deducted here, # so allowable business losses are capped at business income plus # the threshold. - indiv_se_income = max_(0, person("total_self_employment_income", period)) - indiv_se_loss = max_(0, -person("total_self_employment_income", period)) - self_employment_income = tax_unit.sum(not_dependent * indiv_se_income) - self_employment_loss = tax_unit.sum(not_dependent * indiv_se_loss) - - # Schedule F farm business income/losses. - indiv_farm_operations_income = max_(0, person("farm_operations_income", period)) - indiv_farm_loss = max_(0, -person("farm_operations_income", period)) - farm_operations_income = tax_unit.sum( - not_dependent * indiv_farm_operations_income - ) - farm_loss = tax_unit.sum(not_dependent * indiv_farm_loss) - - # Schedule E rental, farm-rent, and estate/trust items. - indiv_rental_income = max_(0, person("rental_income", period)) - indiv_rental_loss = max_(0, -person("rental_income", period)) - rental_income = tax_unit.sum(not_dependent * indiv_rental_income) - rental_loss = tax_unit.sum(not_dependent * indiv_rental_loss) - - indiv_farm_rent_income = max_(0, person("farm_rent_income", period)) - indiv_farm_rent_loss = max_(0, -person("farm_rent_income", period)) - farm_rent_income = tax_unit.sum(not_dependent * indiv_farm_rent_income) - farm_rent_loss = tax_unit.sum(not_dependent * indiv_farm_rent_loss) - - indiv_estate_income = max_(0, person("estate_income", period)) - indiv_estate_loss = max_(0, -person("estate_income", period)) - estate_income = tax_unit.sum(not_dependent * indiv_estate_income) - estate_loss = tax_unit.sum(not_dependent * indiv_estate_loss) - - # Partnership/S-corp losses (Schedule E) - indiv_scorp_income = max_(0, person("partnership_s_corp_income", period)) - indiv_scorp_loss = max_(0, -person("partnership_s_corp_income", period)) - partnership_s_corp_income = tax_unit.sum(not_dependent * indiv_scorp_income) - partnership_s_corp_loss = tax_unit.sum(not_dependent * indiv_scorp_loss) - - other_net_gain = tax_unit("other_net_gain", period) - other_business_gain = max_(0, other_net_gain) - other_business_loss = max_(0, -other_net_gain) - - # Total business losses subject to Section 461(l) limitation - total_business_income = ( - self_employment_income - + farm_operations_income - + rental_income - + farm_rent_income - + estate_income - + partnership_s_corp_income - + other_business_gain - ) - total_business_loss = ( - self_employment_loss - + farm_loss - + rental_loss - + farm_rent_loss - + estate_loss - + partnership_s_corp_loss - + other_business_loss + business_income, business_loss = section_461l_business_income_and_loss( + tax_unit, period ) - max_deductible_business_loss = total_business_income + threshold_amount - limited_business_loss = min_(total_business_loss, max_deductible_business_loss) - return limited_business_loss + return min_(business_loss, business_income + threshold_amount) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/net_investment_income_excess_business_loss.py b/policyengine_us/variables/gov/irs/tax/federal_income/net_investment_income_excess_business_loss.py new file mode 100644 index 00000000000..3c2793aa470 --- /dev/null +++ b/policyengine_us/variables/gov/irs/tax/federal_income/net_investment_income_excess_business_loss.py @@ -0,0 +1,58 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.irs.income.taxable_income.adjusted_gross_income.above_the_line_deductions.limited_business_loss import ( + section_461l_business_income_and_loss, +) + + +class net_investment_income_excess_business_loss(Variable): + value_type = float + entity = TaxUnit + label = "Excess business loss attributed to net investment income" + unit = USD + documentation = ( + "The part of the head and spouse's section 461(l) excess business " + "loss that falls on losses net investment income counts: Schedule E " + "rental and Form 4835 farm rental losses, and the parts of estate and " + "trust and partnership/S corporation losses that net investment " + "income includes. Section 1411(c)(1)(B) allows only the deductions " + "allowed by chapter 1, so net investment income adds this back. When " + "the head and spouse also have business losses net investment income " + "does not count, the excess is shared in proportion to the two kinds " + "of loss; no statute, regulation or form instruction says how to " + "split it." + ) + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/1411#c_1_B", + "https://www.law.cornell.edu/uscode/text/26/461#l", + "https://www.ecfr.gov/current/title-26/section-1.1411-4#p-1.1411-4(f)", + ) + + def formula(tax_unit, period, parameters): + person = tax_unit.members + not_dependent = ~person("is_tax_unit_dependent", period) + rental_loss = max_(0, -person("rental_income", period)) + farm_rent_loss = max_(0, -person("farm_rent_income", period)) + # Only the part of each section 461(l) loss that net investment + # income also counts: the estate and trust loss after the Schedule + # K-1 (Form 1041) code H adjustment, and the passive part of the + # partnership/S corporation loss. + estate_loss = min_( + max_(0, -person("estate_net_investment_income", period)), + max_(0, -person("estate_income", period)), + ) + passive_loss = min_( + max_(0, -person("passive_partnership_s_corp_income", period)), + max_(0, -person("partnership_s_corp_income", period)), + ) + investment_loss = tax_unit.sum( + not_dependent * (rental_loss + farm_rent_loss + estate_loss + passive_loss) + ) + _, business_loss = section_461l_business_income_and_loss(tax_unit, period) + investment_share = np.divide( + investment_loss, + business_loss, + out=np.zeros_like(investment_loss), + where=business_loss > 0, + ) + return tax_unit("excess_business_loss", period) * investment_share From 459cbe9c69b7c1a67b09380fe14b0946fb3789d6 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 11:35:26 -0400 Subject: [PATCH 5/8] Check the section 461(l) add-back against an independent reference Co-Authored-By: Claude Opus 5.5 --- ...endent_net_investment_income_invariants.py | 114 ++++++++++++++++-- 1 file changed, 107 insertions(+), 7 deletions(-) diff --git a/policyengine_us/tests/core/test_dependent_net_investment_income_invariants.py b/policyengine_us/tests/core/test_dependent_net_investment_income_invariants.py index b7b4efb4c5f..a96c40717c1 100644 --- a/policyengine_us/tests/core/test_dependent_net_investment_income_invariants.py +++ b/policyengine_us/tests/core/test_dependent_net_investment_income_invariants.py @@ -20,14 +20,19 @@ head and spouse: their interest, dividends, rents (including Form 4835 farm rent), passive pass-through, estate and trust income, plus their netted Schedule D gain or loss with capital gain distributions on line 13, - limited under 26 USC 1211(b). + limited under 26 USC 1211(b), plus the part of their section 461(l) + excess business loss that falls on those losses. 3. -loss limit <= `filer_loss_limited_net_capital_gains`, and it equals `loss_limited_net_capital_gains` when the unit has no dependents. 4. For tax units without dependents, `net_investment_income` equals the all-member formula (person-level sources plus - `loss_limited_net_capital_gains`), so leaving dependents out changes - nothing for filers without dependents. + `loss_limited_net_capital_gains` plus the section 461(l) add-back), so + leaving dependents out changes nothing for filers without dependents. 5. 0 <= NIIT = 3.8% x min(max(0, NII), max(0, MAGI - threshold)). +6. 0 <= `net_investment_income_excess_business_loss` <= min(the section + 461(l) excess business loss, the net investment income losses), with + equality to the excess when those losses are the only business losses. + Section 1411(c)(1)(B) allows only deductions allowed by chapter 1. """ import numpy as np @@ -78,6 +83,8 @@ ] OUTPUTS = [ "net_investment_income", + "excess_business_loss", + "net_investment_income_excess_business_loss", "net_investment_income_tax", "filer_loss_limited_net_capital_gains", "loss_limited_net_capital_gains", @@ -162,6 +169,24 @@ def _draw_units(): "head": {**zero, "long_term_capital_gains": 10_000.0}, "spouse": {**zero, "short_term_capital_gains": -4_000.0}, "dependents": [{**zero, "rental_income": 25_000.0}]}, + # Section 461(l) binds and the farm rental loss is the only loss. + {"kind": "single", "head_wages": 300_000.0, + "head": {**zero, "taxable_interest_income": 500_000.0, + "farm_rent_income": -600_000.0}, + "spouse": None, "dependents": []}, + # Binds on a joint return with a nonpassive partnership loss too. + {"kind": "joint", "head_wages": 400_000.0, + "head": {**zero, "taxable_interest_income": 300_000.0, + "rental_income": -500_000.0}, + "spouse": {**zero, "partnership_s_corp_income": -400_000.0, + "passive_partnership_s_corp_income": -200_000.0}, + "dependents": []}, + # A dependent's large loss would raise the filer's excess. + {"kind": "hoh", "head_wages": 300_000.0, + "head": {**zero, "taxable_interest_income": 400_000.0, + "rental_income": -400_000.0}, + "spouse": None, + "dependents": [{**zero, "farm_rent_income": -500_000.0}]}, ] # fmt: skip return units @@ -259,6 +284,46 @@ def _loss_limit(runs): return runs["irs"].capital_gains.loss_limit[runs["with"]["filing_status"]] +# The drawn inputs that section 461(l) aggregates as business items. +SECTION_461L_INPUTS = [ + "rental_income", + "farm_rent_income", + "estate_income", + "partnership_s_corp_income", +] + + +def _section_461l_reference(runs, units, include): + """Excess business loss and the part on NII losses, from leaf inputs. + + ``include`` selects the members counted (the head and spouse, or all). + The NII share is proportional: NII losses (rental and farm rental losses, + estate losses and the passive part of partnership losses) over all + business losses. + """ + amounts = {name: _by_person(units, name) for name in SECTION_461L_INPUTS} + income = _unit_sum(runs, include * sum(np.maximum(0, v) for v in amounts.values())) + loss = _unit_sum(runs, include * sum(np.maximum(0, -v) for v in amounts.values())) + passive_loss = np.minimum( + np.maximum(0, -_by_person(units, "passive_partnership_s_corp_income")), + np.maximum(0, -amounts["partnership_s_corp_income"]), + ) + investment_loss = _unit_sum( + runs, + include + * ( + np.maximum(0, -amounts["rental_income"]) + + np.maximum(0, -amounts["farm_rent_income"]) + + np.maximum(0, -amounts["estate_income"]) + + passive_loss + ), + ) + threshold = runs["irs"].ald.loss.max[runs["with"]["filing_status"]] + excess = np.maximum(0, loss - (income + threshold)) + share = np.divide(investment_loss, loss, out=np.zeros_like(loss), where=loss > 0) + return excess, excess * share, investment_loss, loss + + def test_dependent_flags_match_the_draw(runs, units): assert np.array_equal( runs["with"]["is_tax_unit_dependent"], _by_person(units, "is_dependent") @@ -294,11 +359,14 @@ def test_net_investment_income_matches_reference(runs, units): schedule_d = _unit_sum( runs, filer * sum(_by_person(units, name) for name in CAPITAL_INPUTS) ) - expected = _unit_sum(runs, filer * person_nii) + np.maximum( - -_loss_limit(runs), schedule_d + _, addback, _, _ = _section_461l_reference(runs, units, filer) + expected = ( + _unit_sum(runs, filer * person_nii) + + np.maximum(-_loss_limit(runs), schedule_d) + + addback ) np.testing.assert_allclose( - runs["with"]["net_investment_income"], expected, atol=TOLERANCE + runs["with"]["net_investment_income"], expected, rtol=1e-6, atol=TOLERANCE ) @@ -317,7 +385,13 @@ def test_filer_capital_gains_within_loss_limit(runs, units): def test_units_without_dependents_match_all_member_formula(runs, units): """Differential check against the all-member formula.""" person_nii = sum(_by_person(units, name) for name in NII_PERSON_INPUTS) - old = _unit_sum(runs, person_nii) + runs["with"]["loss_limited_net_capital_gains"] + everyone = np.ones_like(_by_person(units, "is_dependent"), dtype=bool) + _, addback, _, _ = _section_461l_reference(runs, units, everyone) + old = ( + _unit_sum(runs, person_nii) + + runs["with"]["loss_limited_net_capital_gains"] + + addback + ) no_dependents = np.array([not u["dependents"] for u in units]) np.testing.assert_allclose( runs["with"]["net_investment_income"][no_dependents], @@ -340,3 +414,29 @@ def test_niit_within_statutory_bounds(runs): # Both limbs of section 1411(a)(1) bind somewhere in the population. assert ((nii > 0) & (excess_magi > 0) & (nii < excess_magi)).sum() > 10 assert ((nii > 0) & (excess_magi > 0) & (nii > excess_magi)).sum() > 10 + + +def test_section_461l_addback_bounds(runs, units): + """Property 6, plus a differential check of both new variables.""" + filer = ~_by_person(units, "is_dependent") + excess, addback, investment_loss, loss = _section_461l_reference(runs, units, filer) + model_excess = runs["with"]["excess_business_loss"] + model_addback = runs["with"]["net_investment_income_excess_business_loss"] + np.testing.assert_allclose(model_excess, excess, rtol=1e-6, atol=TOLERANCE) + np.testing.assert_allclose(model_addback, addback, rtol=1e-6, atol=TOLERANCE) + assert np.all(model_addback >= -TOLERANCE) + assert np.all( + model_addback + <= np.minimum(model_excess, investment_loss) * (1 + 1e-6) + TOLERANCE + ) + only_investment_losses = (loss > 0) & np.isclose(investment_loss, loss) + np.testing.assert_allclose( + model_addback[only_investment_losses], + model_excess[only_investment_losses], + rtol=1e-6, + atol=TOLERANCE, + ) + # The edge rows make section 461(l) bind, including with a nonpassive + # partnership loss sharing the excess. + assert (model_excess > 0).sum() >= 3 + assert ((model_addback > 0) & (model_addback < model_excess - 1)).sum() >= 1 From 1def0db5af39dcb34b8e915740d03088e79db6ea Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 11:36:02 -0400 Subject: [PATCH 6/8] Describe the section 461(l) limit in the changelog Co-Authored-By: Claude Opus 5.5 --- changelog.d/niit-farm-rent.fixed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/niit-farm-rent.fixed.md b/changelog.d/niit-farm-rent.fixed.md index a349ed72edd..6825bb85136 100644 --- a/changelog.d/niit-farm-rent.fixed.md +++ b/changelog.d/niit-farm-rent.fixed.md @@ -1 +1 @@ -Count Form 4835 farm rental income (Schedule E line 40) in net investment income, so it reaches the net investment income tax and Minnesota's net investment income tax. +Count Form 4835 farm rental income (Schedule E line 40) in net investment income, and count rental, farm rental, estate and passive pass-through losses in net investment income only as far as section 461(l) allows them, for the net investment income tax and Minnesota's net investment income tax. From 251b87f68f520c9c0232000da442409831f3e876 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 13:35:17 -0400 Subject: [PATCH 7/8] Expect the section 461(l) add-back in the estate income invariants The NII-follows-estate-income property now subtracts the estate losses that section 461(l) disallows, computed independently from the leaf inputs; the two edge rows with a 400,000 estate loss exercise it. Co-Authored-By: Claude Opus 5.5 --- .../core/test_estate_income_invariants.py | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/policyengine_us/tests/core/test_estate_income_invariants.py b/policyengine_us/tests/core/test_estate_income_invariants.py index d17c6959ef1..46053cebb36 100644 --- a/policyengine_us/tests/core/test_estate_income_invariants.py +++ b/policyengine_us/tests/core/test_estate_income_invariants.py @@ -14,7 +14,11 @@ income is excluded from the filer's gross income throughout the model). 2. Tax unit net investment income rises by exactly the sum of estate_income plus the Schedule K-1 (Form 1041) box 14 code H adjustment over non-dependent - members (Form 8960 lines 4a and 7). + members (Form 8960 lines 4a and 7), less the part of an estate loss that + section 461(l) disallows: § 1411(c)(1)(B) allows only deductions allowed by + chapter 1. Estate income is the only business item drawn here, so the whole + excess business loss falls on estate losses that net investment income + counts. 3. When every member's estate income is nonnegative and nothing else produces a loss, AGI rises by exactly the non-dependents' estate income. 4. A dependent's estate income and adjustment, of either sign, leave the @@ -238,9 +242,11 @@ def runs(request, units): ) out[name]["filing_status"] = sim.calculate("filing_status", year) out[name]["person_tax_unit"] = sim.populations["tax_unit"].members_entity_id - out["niit_parameters"] = sim.tax_benefit_system.parameters( - f"{year}-01-01" - ).gov.irs.investment.net_investment_income_tax + year_parameters = sim.tax_benefit_system.parameters(f"{year}-01-01") + out["niit_parameters"] = ( + year_parameters.gov.irs.investment.net_investment_income_tax + ) + out["section_461l_threshold"] = year_parameters.gov.irs.ald.loss.max return out @@ -269,12 +275,40 @@ def test_gross_income_includes_positive_estate_income_of_non_dependents(runs, un np.testing.assert_allclose(delta, expected, atol=TOLERANCE) +def _section_461l_estate_addback(runs, units): + """The estate losses section 461(l) disallows that NII would otherwise take. + + Estate income is the only business item drawn, so the excess business loss + is the head and spouse's estate losses beyond their estate income plus the + threshold. Its share on NII losses is the estate loss net investment income + keeps after the code H adjustment, over the whole estate loss. + """ + estate, adjustment, dependent = _person_inputs(units) + filer = ~dependent + tax_unit = runs["with"]["person_tax_unit"] + + def by_unit(values): + return np.bincount(tax_unit, weights=filer * values, minlength=len(units)) + + income = by_unit(np.maximum(0, estate)) + loss = by_unit(np.maximum(0, -estate)) + investment_loss = by_unit( + np.minimum(np.maximum(0, -(estate + adjustment)), np.maximum(0, -estate)) + ) + threshold = runs["section_461l_threshold"][runs["with"]["filing_status"]] + excess = np.maximum(0, loss - (income + threshold)) + share = np.divide(investment_loss, loss, out=np.zeros_like(loss), where=loss > 0) + return excess * share + + def test_net_investment_income_adds_estate_income_and_code_h(runs, units): estate, adjustment, dependent = _person_inputs(units) per_person = np.where(dependent, 0, estate + adjustment) expected = np.bincount( runs["with"]["person_tax_unit"], weights=per_person, minlength=len(units) - ) + ) + _section_461l_estate_addback(runs, units) + # The two edge rows with a 400,000 estate loss make section 461(l) bind. + assert (_section_461l_estate_addback(runs, units) > 0).sum() >= 2 delta = ( runs["with"]["net_investment_income"] - runs["zero"]["net_investment_income"] ) From 215bfa5beea66b5f0f13a1de94b3d75b27613f1c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 14:36:21 -0400 Subject: [PATCH 8/8] Keep an estate loss in the section 461(l) NII share whatever code H says A Schedule K-1 (Form 1041) box 14 code H adjustment is positive only for section 1291 fund, CFC and QEF distributions that are NII but not taxable income (2025 Form 1041 instructions, Beneficiary reporting), so it is separate line 7 income and never takes a Part III loss out of NII. Count the whole Part III loss as an NII loss, add the review's counterexample (NIIT 29,906), and label the mixed-loss split provisional. Co-Authored-By: Claude Opus 5.5 --- .../gov/irs/investment/income/sources.yaml | 3 +- .../core/test_estate_income_invariants.py | 13 ++----- ...nvestment_income_excess_business_loss.yaml | 16 +++++--- .../net_investment_income_tax.yaml | 38 +++++++++++++++++-- ..._investment_income_excess_business_loss.py | 34 +++++++++-------- 5 files changed, 68 insertions(+), 36 deletions(-) diff --git a/policyengine_us/parameters/gov/irs/investment/income/sources.yaml b/policyengine_us/parameters/gov/irs/investment/income/sources.yaml index db2f02d7a7b..eb007febb5e 100644 --- a/policyengine_us/parameters/gov/irs/investment/income/sources.yaml +++ b/policyengine_us/parameters/gov/irs/investment/income/sources.yaml @@ -40,7 +40,8 @@ values: # losses beyond business income plus a threshold, and § 1411(c)(1)(B) # allows only deductions allowed by chapter 1. This adds back the part of # the excess business loss that falls on those losses (all of it when they - # are the only business losses; a proportional share otherwise). + # are the only business losses; provisionally a proportional share + # otherwise). - net_investment_income_excess_business_loss metadata: unit: currency-USD diff --git a/policyengine_us/tests/core/test_estate_income_invariants.py b/policyengine_us/tests/core/test_estate_income_invariants.py index 46053cebb36..5110fa3b0ab 100644 --- a/policyengine_us/tests/core/test_estate_income_invariants.py +++ b/policyengine_us/tests/core/test_estate_income_invariants.py @@ -280,10 +280,10 @@ def _section_461l_estate_addback(runs, units): Estate income is the only business item drawn, so the excess business loss is the head and spouse's estate losses beyond their estate income plus the - threshold. Its share on NII losses is the estate loss net investment income - keeps after the code H adjustment, over the whole estate loss. + threshold, and all of it falls on net investment income losses: a Part III + loss stays on Form 8960 line 4a whatever the code H adjustment (line 7). """ - estate, adjustment, dependent = _person_inputs(units) + estate, _, dependent = _person_inputs(units) filer = ~dependent tax_unit = runs["with"]["person_tax_unit"] @@ -292,13 +292,8 @@ def by_unit(values): income = by_unit(np.maximum(0, estate)) loss = by_unit(np.maximum(0, -estate)) - investment_loss = by_unit( - np.minimum(np.maximum(0, -(estate + adjustment)), np.maximum(0, -estate)) - ) threshold = runs["section_461l_threshold"][runs["with"]["filing_status"]] - excess = np.maximum(0, loss - (income + threshold)) - share = np.divide(investment_loss, loss, out=np.zeros_like(loss), where=loss > 0) - return excess * share + return np.maximum(0, loss - (income + threshold)) def test_net_investment_income_adds_estate_income_and_code_h(runs, units): diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_excess_business_loss.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_excess_business_loss.yaml index 95307442816..c76685ff1c7 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_excess_business_loss.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_excess_business_loss.yaml @@ -44,8 +44,8 @@ - name: A proportional share when both kinds of loss are present # Schedule C loss 300,000 and Form 4835 loss 300,000; excess 287,000. - # Proportional share: 287,000 x 300,000 / 600,000 = 143,500. (The law does - # not say how to split it; see the variable documentation.) + # Provisional proportional share: 287,000 x 300,000 / 600,000 = 143,500. + # (The law does not say how to split it; see the variable documentation.) period: 2025 input: people: @@ -87,9 +87,13 @@ excess_business_loss: 87_000 net_investment_income_excess_business_loss: 21_750 -- name: An estate loss removed by a code H adjustment is not a net investment income loss - # Schedule E Part III loss 600,000 with a +600,000 Schedule K-1 (Form 1041) - # box 14 code H adjustment, so net investment income has no estate loss. +- name: A positive code H adjustment does not take an estate loss out of net investment income + # Schedule E Part III loss 600,000 (Form 8960 line 4a) and a +600,000 + # Schedule K-1 (Form 1041) box 14 code H adjustment (line 7). Code H is + # positive only for section 1291 fund, CFC and QEF distributions that are + # NII but not taxable income (2025 Form 1041 instructions, Beneficiary + # reporting), so it is separate income and the whole estate loss is still + # a net investment income loss. Excess 600,000 - 313,000 = 287,000. period: 2025 input: people: @@ -107,7 +111,7 @@ state_code: TX output: excess_business_loss: 287_000 - net_investment_income_excess_business_loss: 0 + net_investment_income_excess_business_loss: 287_000 - name: A joint return shares the excess across both spouses' losses # Head Schedule E rental loss 500,000; spouse partnership loss 400,000, of diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_tax.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_tax.yaml index 089a3ff188c..23a25297ea8 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_tax.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_tax.yaml @@ -659,10 +659,10 @@ - name: A Schedule C loss takes a proportional share of the section 461(l) excess # Wages 600,000, interest 200,000, Schedule C loss 300,000, Form 4835 loss # 300,000. Section 461(l) allows 313,000 of the 600,000; AGI and MAGI - # 800,000 - 313,000 = 487,000. Proportional share of the 287,000 excess on - # the farm rental loss: 143,500, so NII 200,000 - 300,000 + 143,500 = - # 43,500. NIIT 3.8% x min(43,500, 287,000) = 1,653. (No rule says how to - # split the excess; see net_investment_income_excess_business_loss.) + # 800,000 - 313,000 = 487,000. Provisional proportional share of the + # 287,000 excess on the farm rental loss: 143,500, so NII 200,000 - 300,000 + # + 143,500 = 43,500. NIIT 3.8% x min(43,500, 287,000) = 1,653. (No rule + # says how to split the excess; see net_investment_income_excess_business_loss.) period: 2025 input: people: @@ -684,3 +684,33 @@ adjusted_gross_income: 487_000 net_investment_income: 43_500 net_investment_income_tax: 1_653 + +- name: A positive code H adjustment leaves the estate loss limited by section 461(l) + # Independent review counterexample. Wages 300,000, interest 500,000, + # Schedule E Part III loss 600,000, and +600,000 code H from a trust's + # NII-only CFC distribution (positive code H can be nothing else). Section + # 461(l) allows 313,000 of the loss: AGI 300,000 + 500,000 - 313,000 = + # 487,000. MAGI adds the positive code H: 1,087,000. NII 500,000 - 313,000 + # + 600,000 = 787,000. NIIT 3.8% x min(787,000, 887,000) = 29,906. + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + employment_income: 300_000 + taxable_interest_income: 500_000 + estate_income: -600_000 + estate_income_net_investment_income_adjustment: 600_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + adjusted_gross_income: 487_000 + niit_magi: 1_087_000 + net_investment_income: 787_000 + net_investment_income_tax: 29_906 diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/net_investment_income_excess_business_loss.py b/policyengine_us/variables/gov/irs/tax/federal_income/net_investment_income_excess_business_loss.py index 3c2793aa470..6bdc110d0a9 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/net_investment_income_excess_business_loss.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/net_investment_income_excess_business_loss.py @@ -12,20 +12,25 @@ class net_investment_income_excess_business_loss(Variable): documentation = ( "The part of the head and spouse's section 461(l) excess business " "loss that falls on losses net investment income counts: Schedule E " - "rental and Form 4835 farm rental losses, and the parts of estate and " - "trust and partnership/S corporation losses that net investment " - "income includes. Section 1411(c)(1)(B) allows only the deductions " - "allowed by chapter 1, so net investment income adds this back. When " - "the head and spouse also have business losses net investment income " - "does not count, the excess is shared in proportion to the two kinds " - "of loss; no statute, regulation or form instruction says how to " - "split it." + "rental, Form 4835 farm rental and Schedule E Part III estate and " + "trust losses, and the passive part of partnership/S corporation " + "losses. Section 1411(c)(1)(B) allows only the deductions allowed by " + "chapter 1, so net investment income adds this back. A Schedule K-1 " + "(Form 1041) box 14 code H adjustment is a separate Form 8960 line 7 " + "item and does not change how much of an estate or trust loss is a " + "net investment income loss. When the head and spouse also have " + "business losses net investment income does not count, the excess is " + "provisionally shared in proportion to the two kinds of loss: no " + "statute, regulation or form instruction says how to split it." ) definition_period = YEAR reference = ( "https://www.law.cornell.edu/uscode/text/26/1411#c_1_B", "https://www.law.cornell.edu/uscode/text/26/461#l", "https://www.ecfr.gov/current/title-26/section-1.1411-4#p-1.1411-4(f)", + # Beneficiary reporting: code H is positive only for section 1291 fund, + # CFC and QEF distributions that are NII but not taxable income. + "https://www.irs.gov/pub/irs-prior/i1041--2025.pdf#page=38", ) def formula(tax_unit, period, parameters): @@ -33,14 +38,10 @@ def formula(tax_unit, period, parameters): not_dependent = ~person("is_tax_unit_dependent", period) rental_loss = max_(0, -person("rental_income", period)) farm_rent_loss = max_(0, -person("farm_rent_income", period)) - # Only the part of each section 461(l) loss that net investment - # income also counts: the estate and trust loss after the Schedule - # K-1 (Form 1041) code H adjustment, and the passive part of the - # partnership/S corporation loss. - estate_loss = min_( - max_(0, -person("estate_net_investment_income", period)), - max_(0, -person("estate_income", period)), - ) + estate_loss = max_(0, -person("estate_income", period)) + # Only the passive part of a partnership/S corporation loss is a net + # investment income loss, and no more than the loss section 461(l) + # counts. passive_loss = min_( max_(0, -person("passive_partnership_s_corp_income", period)), max_(0, -person("partnership_s_corp_income", period)), @@ -49,6 +50,7 @@ def formula(tax_unit, period, parameters): not_dependent * (rental_loss + farm_rent_loss + estate_loss + passive_loss) ) _, business_loss = section_461l_business_income_and_loss(tax_unit, period) + # Provisional proportional split; see the documentation. investment_share = np.divide( investment_loss, business_loss,