Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
628ef23
Count capital gains once in Arizona property tax credit income
MaxGhenis Oct 3, 2026
7b5013b
Count dependents' income in Arizona property tax credit income
MaxGhenis Oct 3, 2026
6c814d5
Apply Arizona's $1,500 member capital loss limit and allow a negative…
MaxGhenis Oct 3, 2026
0450a28
Cite the line J note where it appears: 2023-2025 instructions, page 5
MaxGhenis Oct 3, 2026
7491794
Combine members' income before any floor in Arizona property tax cred…
MaxGhenis Oct 3, 2026
00c0547
Merge remote-tracking branch 'upstream/main' into az-ptc-line-d-fold
MaxGhenis Oct 4, 2026
c47e4ae
Merge #9770 (Arizona line D per-member loss limit) into the capital g…
MaxGhenis Oct 6, 2026
7d69e82
Swap only the capital amounts federal AGI holds for Arizona line D
MaxGhenis Oct 6, 2026
21cb189
Merge remote-tracking branch 'upstream/main' into az-ptc-line-d-fold
MaxGhenis Oct 6, 2026
8d16289
Add back only loss_ald's capital part in Arizona household income
MaxGhenis Oct 6, 2026
f4fda7a
Merge main (#9788 capital loss netting) into the Arizona property tax…
MaxGhenis Oct 6, 2026
f2f60fe
Merge remote-tracking branch 'upstream/main' into az-ptc-line-d-fold
MaxGhenis Oct 6, 2026
2ea9d6b
Add the dependent IRA household now that #9801 is on main
MaxGhenis Oct 6, 2026
8e1e519
Leave the prior-year long-term carryover out of Arizona line D
MaxGhenis Oct 6, 2026
9ed88bc
Merge main into Arizona property tax credit repair (#9762)
MaxGhenis Oct 8, 2026
4e36f89
Cover dependent Arizona capital-gain changes (#9762)
MaxGhenis Oct 8, 2026
20d335f
Merge latest main into Arizona property tax credit repair (#9762)
MaxGhenis Oct 8, 2026
57eceac
Clarify Arizona capital input convention and consolidate release notes
MaxGhenis Oct 8, 2026
a05c931
Test bounded whole-dollar parity of federal loss extraction
MaxGhenis Oct 9, 2026
c6ad076
Test bounded whole-dollar parity of federal loss extraction
MaxGhenis Oct 9, 2026
3603ddc
Pin d1107 terminating-trust carryover treatment for Arizona credit
MaxGhenis Oct 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fix-az-ptc-capital-gains.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Arizona property tax credit household income counts capital gains and qualified dividends once, includes dependents' income and losses, and limits each member's net capital loss (including capital gain distributions) to $1,500 instead of federal AGI's $3,000 per-return limit. Household income can be negative, with zero used only for the credit schedules. Short-term capital inputs must exclude prior-year carryovers.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
description: Arizona limits each household member's net capital loss to this amount when computing household income for the property tax credit.
values:
2021-01-01: 1_500
metadata:
unit: currency-USD
period: year
label: Arizona property tax credit capital loss limit per household member
reference:
- title: A.A.C. R15-2C-502(C)(3). Property Tax Credit
href: https://www.law.cornell.edu/regulations/arizona/Ariz-Admin-Code-SS-R15-2C-502
- title: ITR 12-1. Income Tax Ruling - Property Tax Credit Household Income, item (7)
href: https://azdor.gov/sites/default/files/2023-03/RULINGS_INDV_2012_itr12-1.pdf#page=2
- title: 2025 Form 140PTC Instructions, Part 1 line D
href: https://azdor.gov/sites/default/files/document/FORMS_INDIVIDUAL_2025_140PTCi.pdf#page=4
- title: 2024 Form 140PTC Instructions, Part 1 line D
href: https://azdor.gov/sites/default/files/document/FORMS_INDIVIDUAL_2024_140PTCi.pdf#page=4
- title: 2023 Form 140PTC Instructions, Part 1 line D
href: https://azdor.gov/sites/default/files/2023-12/FORMS_INDIVIDUAL_2023_140PTCi.pdf#page=4
- title: 2022 Form 140PTC Instructions, Part 1 line D
href: https://azdor.gov/sites/default/files/2023-03/FORMS_INDIVIDUAL_2022_140PTCi-2D.pdf#page=4
- title: 2021 Form 140PTC Instructions, Part 1 line D
href: https://azdor.gov/sites/default/files/2023-03/FORMS_INDIVIDUAL_2021_140PTCi-2D.pdf#page=4
Original file line number Diff line number Diff line change
@@ -1,30 +1,60 @@
description: Arizona accounts for the following income sources when computing household income for the property tax credit.
values:
2021-01-01:
# Item (7): Capital gains - excluded gains not in Federal AGI
- capital_gains_excluded_from_taxable_income
# Item (2): All interest income including tax-exempt
- tax_exempt_interest_income
# Item (9): Pension income - tax-exempt portions not in Federal AGI
# (taxable pension is already in Federal AGI via az_property_tax_credit_agi)
- tax_exempt_public_pension_income
- tax_exempt_private_pension_income
# Federal AGI minus Social Security (per ARS 43-1072(I) exclusion)
# Federal AGI minus Social Security (per ARS 43-1072(I) exclusion).
# Item (7) capital gains and losses (Form 140PTC Part 1 line D) are counted
# here, once, for members who are not dependents: federal AGI's net capital
# gain and capital loss deduction (limited to $3,000 per return) are
# replaced by each member's combined gains and losses, with a net loss
# limited to $1,500 for each member (A.A.C. R15-2C-502(C)(3)). The total can
# be negative; az_property_tax_credit uses zero for the schedules.
# ARS 43-1072(H)(6)(b) also adds "the amount of capital gains excluded from
# adjusted gross income"; the model has no gain excluded from federal AGI
# (such as a section 121 home sale or section 1202 small business stock
# exclusion), so nothing is added for it. Do not add
# capital_gains_excluded_from_taxable_income: it is the part of federal
# taxable income taxed at the capital gains rates (at zero taxable income,
# the adjusted net capital gain), which federal AGI already holds.
- az_property_tax_credit_agi
# Federal AGI leaves out dependents' income; Arizona counts every household
# member's income whether or not the member is a dependent
# (A.A.C. R15-2C-502(A)(2), (B)). This includes each dependent's line D
# amount, with the same $1,500 loss limit.
- az_property_tax_credit_dependent_income
metadata:
unit: list
period: year
label: Arizona property tax credit income sources
reference:
- title: ARS 43-1072. Property tax credit - Income definition
href: https://www.azleg.gov/ars/43/01072.htm
- title: A.A.C. R15-2C-502. Property Tax Credit
href: https://www.law.cornell.edu/regulations/arizona/Ariz-Admin-Code-SS-R15-2C-502
- title: ITR 12-1. Income Tax Ruling - Property Tax Credit Household Income
href: https://azdor.gov/sites/default/files/2023-03/RULINGS_INDV_2012_itr12-1.pdf
href: https://azdor.gov/sites/default/files/2023-03/RULINGS_INDV_2012_itr12-1.pdf#page=2
- title: 2025 Form 140PTC Instructions, Part 1 lines A to I
href: https://azdor.gov/sites/default/files/document/FORMS_INDIVIDUAL_2025_140PTCi.pdf#page=4
- title: 2025 Property Tax Refund (Credit) Claim Form 140PTC, Part 1
href: https://azdor.gov/sites/default/files/document/FORMS_INDIVIDUAL_2025_140PTC_f.pdf#page=2
- title: 2024 Form 140PTC Instructions, Part 1 lines A to I
href: https://azdor.gov/sites/default/files/document/FORMS_INDIVIDUAL_2024_140PTCi.pdf#page=4
- title: 2024 Property Tax Refund (Credit) Claim Form 140PTC
href: https://azdor.gov/forms/tax-credits-forms/property-tax-refund-credit-claim-form-fillable
- title: 2023 Form 140PTC Instructions, Part 1 lines A to I
href: https://azdor.gov/sites/default/files/2023-12/FORMS_INDIVIDUAL_2023_140PTCi.pdf#page=4
- title: 2023 Property Tax Refund (Credit) Claim Form 140PTC
href: https://azdor.gov/forms/tax-credits-forms/property-tax-refund-credit-claim-form-fillable
- title: 2022 Form 140PTC Instructions, Part 1 lines A to I
href: https://azdor.gov/sites/default/files/2023-03/FORMS_INDIVIDUAL_2022_140PTCi-2D.pdf#page=4
- title: 2022 Property Tax Refund (Credit) Claim Form 140PTC
href: https://azdor.gov/sites/default/files/2023-03/FORMS_INDIVIDUAL_2022_140PTC.pdf
- title: 2021 Form 140PTC Instructions, Part 1 lines A to I
href: https://azdor.gov/sites/default/files/2023-03/FORMS_INDIVIDUAL_2021_140PTCi-2D.pdf#page=4
- title: 2021 Property Tax Refund (Credit) Claim Form 140PTC
href: https://azdor.gov/sites/default/files/2023-03/FORMS_INDIVIDUAL_2021_140PTC.pdf
292 changes: 292 additions & 0 deletions policyengine_us/tests/core/test_loss_ald_extraction_parity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
"""Bounded whole-dollar compatibility of the extracted business-loss component.

YAML covers the policy examples. This differential test covers Core's float32
storage and ``adds`` accumulation, which YAML cannot compare with the former
single-formula calculation. The independent reference applies sections 461(l)
and 1211(b) to leaf inputs and casts the combined deduction only once, as the
pre-extraction formula did.

All intermediate totals stay below 2**24, the exact-integer range of float32.
This is a bounded guarantee, not parity for fractional dollars or larger totals.
Use production classes in one small Core system, without building or copying a
country model per case; package import constructs its global model once. This
module belongs to the existing rest-python core CI group.
"""

from pathlib import Path

import numpy as np
import pytest
import yaml
from policyengine_core.parameters import ParameterNode
from policyengine_core.parameters.config import Loader
from policyengine_core.simulations import Simulation
from policyengine_core.taxbenefitsystems import TaxBenefitSystem

import policyengine_us
from policyengine_us.entities import Person, TaxUnit
from policyengine_us.variables.gov.irs.income.taxable_income.adjusted_gross_income.above_the_line_deductions.capital_losses_allowed_against_gains import (
capital_losses_allowed_against_gains,
)
from policyengine_us.variables.gov.irs.income.taxable_income.adjusted_gross_income.above_the_line_deductions.limited_business_loss import (
limited_business_loss,
)
from policyengine_us.variables.gov.irs.income.taxable_income.adjusted_gross_income.above_the_line_deductions.limited_capital_loss import (
limited_capital_loss,
)
from policyengine_us.variables.gov.irs.income.taxable_income.adjusted_gross_income.above_the_line_deductions.loss_ald import (
loss_ald,
)
from policyengine_us.variables.household.demographic.tax_unit.filing_status import (
FilingStatus,
filing_status,
)
from policyengine_us.variables.household.demographic.tax_unit.is_tax_unit_dependent import (
is_tax_unit_dependent,
)
from policyengine_us.variables.household.income.person.capital_gains.capital_gains import (
capital_gains,
)
from policyengine_us.variables.household.income.person.capital_gains.capital_losses import (
capital_losses,
)
from policyengine_us.variables.household.income.person.capital_gains.long_term_capital_gains import (
long_term_capital_gains,
)
from policyengine_us.variables.household.income.person.capital_gains.non_sch_d_capital_gains import (
non_sch_d_capital_gains,
)
from policyengine_us.variables.household.income.person.capital_gains.other_net_gain import (
other_net_gain,
)
from policyengine_us.variables.household.income.person.capital_gains.short_term_capital_gains import (
short_term_capital_gains,
)
from policyengine_us.variables.household.income.person.estate.estate_income import (
estate_income,
)
from policyengine_us.variables.household.income.person.farm.farm_operations_income import (
farm_operations_income,
)
from policyengine_us.variables.household.income.person.farm.farm_rent_income import (
farm_rent_income,
)
from policyengine_us.variables.household.income.person.misc.rental_income import (
rental_income,
)
from policyengine_us.variables.household.income.person.self_employment.partnership_income import (
partnership_income,
)
from policyengine_us.variables.household.income.person.self_employment.partnership_s_corp_income import (
partnership_s_corp_income,
)
from policyengine_us.variables.household.income.person.self_employment.s_corp_income import (
s_corp_income,
)
from policyengine_us.variables.household.income.person.self_employment.total_self_employment_income import (
total_self_employment_income,
)
from policyengine_us.variables.input.self_employment_income import (
self_employment_income,
)
from policyengine_us.variables.input.sstb_self_employment_income import (
sstb_self_employment_income,
)

INPUT_CLASSES = (
self_employment_income,
sstb_self_employment_income,
farm_operations_income,
rental_income,
farm_rent_income,
estate_income,
partnership_income,
s_corp_income,
short_term_capital_gains,
long_term_capital_gains,
non_sch_d_capital_gains,
)
INPUTS = tuple(variable.__name__ for variable in INPUT_CLASSES)
ZERO = dict.fromkeys(INPUTS, 0)
STATUSES = tuple(status.name for status in FilingStatus)
EXACT_INTEGER_LIMIT = 2**24


@pytest.fixture(scope="module")
def system():
result = TaxBenefitSystem(entities=[Person, TaxUnit])
result.add_variables(
*INPUT_CLASSES,
filing_status,
is_tax_unit_dependent,
other_net_gain,
total_self_employment_income,
partnership_s_corp_income,
capital_gains,
capital_losses,
limited_business_loss,
capital_losses_allowed_against_gains,
limited_capital_loss,
loss_ald,
)
parameter_root = Path(policyengine_us.__file__).parent / "parameters"

def load(relative_path):
return yaml.load((parameter_root / relative_path).read_text(), Loader=Loader)

# Read only the published thresholds and gross-income source list needed
# by these formulas; no full parameter-tree construction or copying.
result.parameters = ParameterNode(
"",
data={
"gov": {
"irs": {
"ald": {
"loss": {
"max": load("gov/irs/ald/loss/max.yaml"),
"capital": {
"max": load("gov/irs/ald/loss/capital/max.yaml")
},
}
},
"gross_income": {
"sources": load("gov/irs/gross_income/sources.yaml")
},
}
}
},
)
return result


def _draw_units(year, thresholds, capital_caps):
rng = np.random.default_rng(9762 + year)
units = []
for i in range(120):
members = []
for dependent in [False] + ([False] if i % 2 else []) + [True] * (i % 3):
amounts = {name: int(rng.integers(-100_000, 100_001)) for name in INPUTS}
amounts["non_sch_d_capital_gains"] = abs(amounts["non_sch_d_capital_gains"])
members.append((amounts, dependent))
units.append(
{
"status": STATUSES[i % len(STATUSES)],
"other": int(rng.integers(-100_000, 100_001)),
"members": members,
}
)
for status in STATUSES:
for offset in (-1, 0, 1):
# Below/at/above business and capital limits, including MFS.
# Opposite-signed short/long-term sources net before the
# distribution offsets the resulting loss.
head = {
**ZERO,
"farm_rent_income": 12_345,
"rental_income": -(thresholds[status] + 12_345 + 17 + offset),
"short_term_capital_gains": -(
2_000 + capital_caps[status] + offset + 700
),
"long_term_capital_gains": 700,
"non_sch_d_capital_gains": 2_000,
}
dependent = {
**ZERO,
"estate_income": -800_000,
"partnership_income": 400_000,
"short_term_capital_gains": -400_000,
"long_term_capital_gains": 100_000,
"non_sch_d_capital_gains": 200_000,
}
units.append(
{
"status": status,
"other": 17,
"members": [(head, False), (dependent, True)],
}
)
return units


def _pre_extraction_reference(unit, threshold, capital_cap):
"""Independent scalar reference, retaining the former one final cast."""
business_income = max(0, unit["other"])
business_losses = max(0, -unit["other"])
capital_losses = gains_in_gross_income = 0
for person, dependent in unit["members"]:
if dependent:
continue
business_sources = (
person["self_employment_income"] + person["sstb_self_employment_income"],
person["farm_operations_income"],
person["rental_income"],
person["farm_rent_income"],
person["estate_income"],
person["partnership_income"] + person["s_corp_income"],
)
business_income += sum(max(0, value) for value in business_sources)
business_losses += sum(max(0, -value) for value in business_sources)
net_capital = (
person["short_term_capital_gains"] + person["long_term_capital_gains"]
)
capital_losses += max(0, -net_capital)
gains_in_gross_income += max(0, net_capital) + max(
0, person["non_sch_d_capital_gains"]
)
business = min(business_losses, business_income + threshold)
against_gains = min(capital_losses, gains_in_gross_income)
net_capital_loss = min(capital_cap, capital_losses - against_gains)
combined = business + against_gains + net_capital_loss
# Check every total, not only individual leaf inputs: an intermediate
# sum outside this domain can round before the final combined deduction.
assert (
max(
business_income + threshold,
business_losses,
capital_losses,
gains_in_gross_income,
combined,
)
< EXACT_INTEGER_LIMIT
)
return np.float32(combined)


@pytest.mark.parametrize("year", [2021, 2025, 2026])
def test_whole_dollar_loss_ald_matches_pre_extraction_formula(system, year):
"""Dependent masking, status limits and source offsets preserve exact totals."""
p = system.parameters(f"{year}-01-01").gov.irs.ald.loss
thresholds = {status: p.max[status] for status in STATUSES}
capital_caps = {status: p.capital.max[status] for status in STATUSES}
units = _draw_units(year, thresholds, capital_caps)
people, tax_units = {}, {}
for i, unit in enumerate(units):
members = []
for j, (amounts, dependent) in enumerate(unit["members"]):
name = f"u{i}_p{j}"
members.append(name)
people[name] = {
"is_tax_unit_dependent": {year: dependent},
**{name: {year: value} for name, value in amounts.items()},
}
tax_units[f"t{i}"] = {
"members": members,
"filing_status": {year: unit["status"]},
"other_net_gain": {year: unit["other"]},
}
simulation = Simulation(
tax_benefit_system=system,
situation={"people": people, "tax_units": tax_units},
)
expected = np.array(
[
_pre_extraction_reference(
unit, thresholds[unit["status"]], capital_caps[unit["status"]]
)
for unit in units
],
dtype=np.float32,
)
actual = simulation.calculate("loss_ald", year)
assert actual.dtype == np.float32
np.testing.assert_array_equal(actual, expected)
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
# Total loss_ald: 610,000 + 3,000 = 613,000
# Gross income: 82,813 + 20 (interest) = 82,833
# AGI: 82,833 - 613,000 = -530,167
limited_business_loss: 610_000
limited_capital_loss: 3_000
loss_ald: 613_000
adjusted_gross_income: -530_167

Expand Down
Loading
Loading