Skip to content
Open
1 change: 1 addition & 0 deletions changelog.d/niit-farm-rent.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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.
32 changes: 32 additions & 0 deletions policyengine_us/parameters/gov/irs/investment/income/sources.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,6 +36,13 @@ 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; provisionally a proportional share
# otherwise).
- net_investment_income_excess_business_loss
metadata:
unit: currency-USD
label: net investment income sources
Expand All @@ -49,3 +67,17 @@ 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 rules and lines 4a and 4b #page 4,8"
href: https://www.irs.gov/pub/irs-prior/i8960--2025.pdf#page=4
- 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
- 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)
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,22 @@
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), 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
Expand All @@ -43,6 +49,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",
Expand Down Expand Up @@ -70,11 +77,14 @@
"qualified_dividend_income",
"non_qualified_dividend_income",
"rental_income",
"farm_rent_income",
"passive_partnership_s_corp_income",
"estate_income",
]
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",
Expand Down Expand Up @@ -159,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

Expand Down Expand Up @@ -256,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")
Expand Down Expand Up @@ -291,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
)


Expand All @@ -314,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],
Expand All @@ -337,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
39 changes: 34 additions & 5 deletions policyengine_us/tests/core/test_estate_income_invariants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -269,12 +275,35 @@ 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, 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, _, 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))
threshold = runs["section_461l_threshold"][runs["with"]["filing_status"]]
return np.maximum(0, loss - (income + threshold))


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"]
)
Expand Down
Loading
Loading