From e58f36ed3abd81270d0354f84829284254e055bd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 15:03:39 +0000 Subject: [PATCH 1/5] Apply New York's pre-TCJA mortgage debt limits from 2018 N.Y. Tax Law 615(a) computes New York itemized deductions as they existed before P.L. 115-97, but ny_itemized_deductions_max added the federal interest_deduction, so post-2017 loans got the federal $750,000 limit and no home equity debt. A new ny_mortgage_interest_deduction applies the preserved $1,000,000 / $100,000 limits (halved for separate returns) with per-home fair market value room, and ny_interest_deduction substitutes it for the federal mortgage component. New inputs default to values that add no home equity debt. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6J5LH2sLC5oX6pUHnC --- .../ny-pre-tcja-mortgage-interest.fixed.md | 1 + .../interest/home_acquisition_debt_limit.yaml | 29 +++ .../interest/home_equity_debt_limit.yaml | 29 +++ .../test_ny_mortgage_debt_limit_invariants.py | 227 ++++++++++++++++++ .../ny_mortgage_interest_deduction.yaml | 208 ++++++++++++++++ .../itemized/ny_interest_deduction.py | 25 ++ .../itemized/ny_itemized_deductions_max.py | 5 +- .../ny_mortgage_interest_deduction.py | 182 ++++++++++++++ .../itemized/ny_mortgage_interest_inputs.py | 80 ++++++ .../ny_itemized_deductions_phase_out.py | 2 +- 10 files changed, 786 insertions(+), 2 deletions(-) create mode 100644 changelog.d/ny-pre-tcja-mortgage-interest.fixed.md create mode 100644 policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_acquisition_debt_limit.yaml create mode 100644 policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_equity_debt_limit.yaml create mode 100644 policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml create mode 100644 policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.py create mode 100644 policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py create mode 100644 policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py diff --git a/changelog.d/ny-pre-tcja-mortgage-interest.fixed.md b/changelog.d/ny-pre-tcja-mortgage-interest.fixed.md new file mode 100644 index 00000000000..b9181cd44d1 --- /dev/null +++ b/changelog.d/ny-pre-tcja-mortgage-interest.fixed.md @@ -0,0 +1 @@ +Apply New York's pre-TCJA mortgage debt limits ($1,000,000 acquisition and $100,000 home equity debt) to its itemized interest deduction from 2018, with new home value and non-acquisition debt inputs. diff --git a/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_acquisition_debt_limit.yaml b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_acquisition_debt_limit.yaml new file mode 100644 index 00000000000..02e2f769e7e --- /dev/null +++ b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_acquisition_debt_limit.yaml @@ -0,0 +1,29 @@ +description: New York limits the home acquisition debt on which mortgage interest is deductible to this amount, reduced by any grandfathered debt, based on filing status. +SINGLE: + 2018-01-01: 1_000_000 +JOINT: + 2018-01-01: 1_000_000 +SEPARATE: + 2018-01-01: 500_000 +HEAD_OF_HOUSEHOLD: + 2018-01-01: 1_000_000 +SURVIVING_SPOUSE: + 2018-01-01: 1_000_000 +metadata: + propagate_metadata_to_children: true + breakdown: filing_status + unit: currency-USD + period: year + label: New York home acquisition debt limit + reference: + # N.Y. Tax Law 615(a) computes New York itemized deductions as they + # existed immediately before P.L. 115-97, so the IRC 163(h)(3)(B)(ii) + # limit applies rather than the IRC 163(h)(3)(F) limit for later debt. + - title: New York Tax Law 615(a) + href: https://www.nysenate.gov/legislation/laws/TAX/615 + - title: "L.2018, chapter 59 (S7509C), Part JJ, sections 3 and 7 #page 42-43" + href: https://legislation.nysenate.gov/pdf/bills/2017/S7509C#page=42 + - title: 26 U.S. Code 163(h)(3)(B)(ii) (2017 edition) + href: https://www.govinfo.gov/content/pkg/USCODE-2017-title26/html/USCODE-2017-title26-subtitleA-chap1-subchapB-partVI-sec163.htm + - title: New York State Department of Taxation and Finance, Itemized deductions (2025), Interest you paid + href: https://www.tax.ny.gov/pit/file/itemized-deductions-2025.htm#interest diff --git a/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_equity_debt_limit.yaml b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_equity_debt_limit.yaml new file mode 100644 index 00000000000..edf2e20e399 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_equity_debt_limit.yaml @@ -0,0 +1,29 @@ +description: New York limits the home equity debt on which mortgage interest is deductible to this amount, based on filing status. +SINGLE: + 2018-01-01: 100_000 +JOINT: + 2018-01-01: 100_000 +SEPARATE: + 2018-01-01: 50_000 +HEAD_OF_HOUSEHOLD: + 2018-01-01: 100_000 +SURVIVING_SPOUSE: + 2018-01-01: 100_000 +metadata: + propagate_metadata_to_children: true + breakdown: filing_status + unit: currency-USD + period: year + label: New York home equity debt limit + reference: + # N.Y. Tax Law 615(a) computes New York itemized deductions as they + # existed immediately before P.L. 115-97, so the IRC 163(h)(3)(C)(ii) + # limit applies and IRC 163(h)(3)(F) does not suspend it. + - title: New York Tax Law 615(a) + href: https://www.nysenate.gov/legislation/laws/TAX/615 + - title: "L.2018, chapter 59 (S7509C), Part JJ, sections 3 and 7 #page 42-43" + href: https://legislation.nysenate.gov/pdf/bills/2017/S7509C#page=42 + - title: 26 U.S. Code 163(h)(3)(C)(ii) (2017 edition) + href: https://www.govinfo.gov/content/pkg/USCODE-2017-title26/html/USCODE-2017-title26-subtitleA-chap1-subchapB-partVI-sec163.htm + - title: "2017 IRS Publication 936, Home equity debt limit and Table 1 #page 10-11" + href: https://www.irs.gov/pub/irs-prior/p936--2017.pdf#page=10 diff --git a/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py new file mode 100644 index 00000000000..8f7b21d845a --- /dev/null +++ b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py @@ -0,0 +1,227 @@ +"""Invariants for New York's pre-TCJA mortgage debt limits. + +N.Y. Tax Law 615(a) computes New York itemized deductions as they existed +immediately before P.L. 115-97, so from 2018 New York keeps the IRC +163(h)(3)(B)-(D) limits: grandfathered debt, incurred on or before October +13, 1987, has no dollar limit and reduces the $1,000,000 acquisition-debt +limit ($500,000 married filing separately); acquisition debt above that limit +and non-acquisition debt count as home equity debt up to $100,000 ($50,000), +and on each home only up to its fair market value less the debt on it +treated as acquisition or grandfathered debt. + +Hypothesis draws two homes, each with an acquisition balance and vintage, +non-acquisition debt and a fair market value, around every cutoff and limit, +and checks _ny_qualified_loan_limit: + +1. Differential: it equals an independent restatement in the statute's + order, which places the acquisition limit on each home by a separate loop. +2. Bounds: grandfathered debt <= New York limit <= total debt, and the New + York limit is at least PE's federal limit for the same acquisition loans. +3. Defaults: with no fair market value and no non-acquisition debt (the + input defaults), no debt counts as home equity debt. +4. Order: swapping the two homes never changes the limit. +5. Monotone: a higher fair market value never lowers the limit. + +And for _ny_allowed_interest: with no non-acquisition debt the result lies +between the federal deduction and the interest paid, and equals the federal +deduction when federal law disallows nothing. + +The limits are read from the New York and federal parameters for every +filing status in 2018, 2025 and 2026. The properties cannot pass trivially: +the differential compares against a separately written per-home calculation, +and the default and bound checks fail if the fair market value test, the +home equity limit or the acquisition limit is dropped or reversed. + +These are for-all properties of the helpers, which YAML cannot state. The +policy outcomes are checked in ny_mortgage_interest_deduction.yaml. +""" + +import numpy as np +from hypothesis import given, settings +from hypothesis import strategies as st + +from policyengine_us.system import system +from policyengine_us.variables.gov.states.ny.tax.income.taxable_income.deductions.itemized.ny_mortgage_interest_deduction import ( + _ny_allowed_interest, + _ny_deductible_share, + _ny_qualified_loan_limit, +) +from policyengine_us.variables.household.expense.tax_unit.mortgage_interest_structure import ( + _debt_by_vintage, + _qualified_loan_limit, +) + +GRANDFATHERED_YEAR = 1987 +PRE_TCJA_YEAR = 2017 +FILING_STATUSES = ( + "SINGLE", + "JOINT", + "SEPARATE", + "HEAD_OF_HOUSEHOLD", + "SURVIVING_SPOUSE", +) +TAX_YEARS = (2018, 2025, 2026) + + +def parameter_limits(year, filing_status): + p = system.parameters(f"{year}-01-01") + ny = p.gov.states.ny.tax.income.deductions.itemized.interest + federal = p.gov.irs.deductions.itemized.interest.mortgage + return ( + ( + float(ny.home_acquisition_debt_limit[filing_status]), + float(ny.home_equity_debt_limit[filing_status]), + ), + ( + float(federal.pre_tcja_cap[filing_status]), + float(federal.cap[filing_status]), + ), + ) + + +LIMITS = { + (year, filing_status): parameter_limits(year, filing_status) + for year in TAX_YEARS + for filing_status in FILING_STATUSES +} +TOLERANCE = 0.01 + +YEARS = (0, 1960, 1987, 1988, 2017, 2018, 2025) +BOUNDARY_AMOUNTS = ( + 0, + 1, + 50_000, + 100_000, + 375_000, + 500_000, + 550_000, + 750_000, + 1_000_000, + 1_100_000, +) +amounts = st.one_of( + st.sampled_from(BOUNDARY_AMOUNTS), + st.integers(0, 3_000_000), +).map(float) +# (acquisition balance, origination year, non-acquisition debt, fair market value) +homes = st.tuples(amounts, st.sampled_from(YEARS), amounts, amounts) + + +def grandfathered(year): + return 0 < year <= GRANDFATHERED_YEAR + + +def model_limit(pair, limits): + grandfathered_debts = [ + np.array([balance if grandfathered(year) else 0.0]) + for balance, year, _, _ in pair + ] + other_debts = [ + np.array([0.0 if grandfathered(year) else balance]) + for balance, year, _, _ in pair + ] + return float( + _ny_qualified_loan_limit( + grandfathered_debts, + other_debts, + [np.array([home[2]]) for home in pair], + [np.array([home[3]]) for home in pair], + *limits, + )[0] + ) + + +def statute(pair, limits): + """IRC 163(h)(3)(B)(ii), (C) and (D) before P.L. 115-97, home by home.""" + acquisition_limit, home_equity_limit = limits + old = sum(balance for balance, year, _, _ in pair if grandfathered(year)) + other = sum(balance for balance, year, _, _ in pair if not grandfathered(year)) + # (D)(ii): grandfathered debt reduces the $1,000,000 limit. + other_allowed = min(other, max(0.0, acquisition_limit - old)) + home_equity = 0.0 + for balance, year, non_acquisition, fair_market_value in pair: + if grandfathered(year): + on_home, excess = balance, 0.0 + else: + on_home = other_allowed * balance / other if other > 0 else 0.0 + excess = balance - on_home + # (C)(i): no more than fair market value less acquisition debt. + room = max(0.0, fair_market_value - on_home) + home_equity += min(excess + non_acquisition, room) + # (C)(ii): the aggregate home equity limit. + return old + other_allowed + min(home_equity, home_equity_limit) + + +def federal_limit(pair, limits): + debt = _debt_by_vintage( + [np.array([home[0]]) for home in pair], + [np.array([home[1]]) for home in pair], + GRANDFATHERED_YEAR, + PRE_TCJA_YEAR, + ) + return float(_qualified_loan_limit(*debt, *limits)[0]) + + +SETTINGS = dict(max_examples=400, deadline=None, derandomize=True) + + +def test_new_york_limits_are_the_statutory_amounts(): + for (year, filing_status), (ny_limits, _) in LIMITS.items(): + expected = ( + (500_000, 50_000) if filing_status == "SEPARATE" else (1_000_000, 100_000) + ) + assert ny_limits == expected, (year, filing_status) + + +@settings(**SETTINGS) +@given(first=homes, second=homes, status=st.sampled_from(sorted(LIMITS))) +def test_new_york_qualified_loan_limit_properties(first, second, status): + ny_limits, federal_limits = LIMITS[status] + pair = [first, second] + limit = model_limit(pair, ny_limits) + old = sum(home[0] for home in pair if grandfathered(home[1])) + total = sum(home[0] + home[2] for home in pair) + + # 1. Differential. + assert abs(limit - statute(pair, ny_limits)) <= TOLERANCE + # 2. Bounds. + assert old - TOLERANCE <= limit <= total + TOLERANCE + assert limit >= federal_limit(pair, federal_limits) - TOLERANCE + # 3. Defaults: no fair market value and no non-acquisition debt. + defaults = [(home[0], home[1], 0.0, 0.0) for home in pair] + acquisition = sum(home[0] for home in pair) + assert ( + abs(model_limit(defaults, ny_limits) - min(max(old, ny_limits[0]), acquisition)) + <= TOLERANCE + ) + # 4. Order. + assert abs(limit - model_limit([second, first], ny_limits)) <= TOLERANCE + # 5. Monotone in fair market value. + richer = [(home[0], home[1], home[2], home[3] + 250_000) for home in pair] + assert model_limit(richer, ny_limits) >= limit - TOLERANCE + + +@settings(**SETTINGS) +@given( + deductible=amounts, + non_deductible=amounts, + limit=amounts, + debt=amounts, +) +def test_new_york_allowed_interest_without_non_acquisition_debt( + deductible, non_deductible, limit, debt +): + share = _ny_deductible_share(np.array([min(limit, debt)]), np.array([debt])) + allowed = float( + _ny_allowed_interest( + np.array([deductible]), + np.array([non_deductible]), + np.array([0.0]), + share, + )[0] + ) + assert 0 <= share[0] <= 1 + assert deductible - TOLERANCE <= allowed + assert allowed <= deductible + non_deductible + TOLERANCE + if non_deductible == 0: + assert allowed == deductible diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml new file mode 100644 index 00000000000..779833a3c44 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml @@ -0,0 +1,208 @@ +# N.Y. Tax Law 615(a) computes New York itemized deductions as they existed +# immediately before P.L. 115-97, so from 2018 New York keeps the pre-TCJA +# mortgage limits of IRC 163(h)(3)(B)-(D). IRS Publication 936 (2017), +# Table 1 (PDF page 11): +# G = grandfathered debt, 1987 or earlier; A = other acquisition debt +# line 6 = min(max(G, $1,000,000), G + A) +# line 7 = min(home equity debt, $100,000), where home equity debt is the +# acquisition debt above line 6 plus non-acquisition debt, limited +# on each home to its fair market value less the debt on it +# counted in line 6 +# line 8 = line 6 + line 7 +# line 11 = line 8 / total debt, rounded to three places +# line 12 = interest x line 11 +# Limits are halved for married filing separately. PolicyEngine's federal +# deduction uses the $750,000 limit for debt from 2018 or later, no home +# equity debt and the exact ratio. + +- name: New York keeps the $1,000,000 acquisition-debt limit + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: SINGLE + home_mortgage_interest: 36_000 + first_home_mortgage_balance: 900_000 + first_home_mortgage_origination_year: 2020 + ny_first_home_fair_market_value: 1_200_000 + ny_first_home_non_acquisition_debt: 0 + output: + # Federal: $36,000 x $750,000 / $900,000 = $30,000. + deductible_mortgage_interest_tax_unit: 30_000 + # New York: the $1,000,000 limit covers all $900,000. Main allowed the + # federal $30,000. + ny_mortgage_interest_deduction: 36_000 + ny_interest_deduction: 36_000 + ny_itemized_deductions_max: 36_000 + +- name: Acquisition debt above the limit counts as home equity debt + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: SINGLE + home_mortgage_interest: 40_000 + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2020 + ny_first_home_fair_market_value: 2_000_000 + ny_first_home_non_acquisition_debt: 0 + output: + # Federal: $40,000 x $750,000 / $1,500,000 = $20,000. + deductible_mortgage_interest_tax_unit: 20_000 + # New York: ($1,000,000 + $100,000) / $1,500,000 = .733; $40,000 x .733. + ny_mortgage_interest_deduction: 29_320 + +- name: Separate returns halve both limits + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: SEPARATE + home_mortgage_interest: 20_000 + first_home_mortgage_balance: 600_000 + first_home_mortgage_origination_year: 2020 + ny_first_home_fair_market_value: 800_000 + ny_first_home_non_acquisition_debt: 0 + output: + # Federal: $20,000 x $375,000 / $600,000 = $12,500. + deductible_mortgage_interest_tax_unit: 12_500 + # New York: ($500,000 + $50,000) / $600,000 = .917; $20,000 x .917. + ny_mortgage_interest_deduction: 18_340 + +- name: Without a fair market value no debt counts as home equity debt + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: SINGLE + home_mortgage_interest: 40_000 + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2020 + output: + deductible_mortgage_interest_tax_unit: 20_000 + # New York: $1,000,000 / $1,500,000 = .667; $40,000 x .667. + ny_mortgage_interest_deduction: 26_680 + +- name: Fair market value limits home equity debt + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: SINGLE + home_mortgage_interest: 40_000 + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2020 + ny_first_home_fair_market_value: 1_040_000 + output: + # Room for home equity debt: $1,040,000 - $1,000,000 = $40,000. + # New York: $1,040,000 / $1,500,000 = .693; $40,000 x .693. + ny_mortgage_interest_deduction: 27_720 + +- name: Non-acquisition debt counts as home equity debt up to the limit + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: JOINT + home_mortgage_interest: 35_000 + first_home_mortgage_balance: 500_000 + first_home_mortgage_origination_year: 2015 + ny_first_home_fair_market_value: 1_000_000 + ny_first_home_non_acquisition_debt: 200_000 + output: + # PE's federal calculation does not see the non-acquisition debt. + deductible_mortgage_interest_tax_unit: 35_000 + # New York: ($500,000 + $100,000) / $700,000 = .857; $35,000 x .857. + ny_mortgage_interest_deduction: 29_995 + +- name: Grandfathered debt reduces the acquisition-debt limit + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: JOINT + home_mortgage_interest: 50_000 + first_home_mortgage_balance: 400_000 + first_home_mortgage_origination_year: 1986 + second_home_mortgage_balance: 800_000 + second_home_mortgage_origination_year: 2020 + ny_first_home_fair_market_value: 500_000 + ny_second_home_fair_market_value: 900_000 + output: + # Federal: line 6 = $400,000 of grandfathered debt; the later debt + # raises the limit only to $750,000; $50,000 x $750,000 / $1,200,000. + deductible_mortgage_interest_tax_unit: 31_250 + # New York: line 6 = max($400,000, $1,000,000) = $1,000,000, of which + # $600,000 is the second home's; its other $200,000 is home equity debt, + # within its room of $900,000 - $600,000 = $300,000 and limited to + # $100,000. ($1,000,000 + $100,000) / $1,200,000 = .917; $50,000 x .917. + ny_mortgage_interest_deduction: 45_850 + +- name: Each home's equity room limits its own excess debt + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: JOINT + home_mortgage_interest: 60_000 + first_home_mortgage_balance: 900_000 + first_home_mortgage_origination_year: 2020 + second_home_mortgage_balance: 600_000 + second_home_mortgage_origination_year: 2021 + ny_first_home_fair_market_value: 3_000_000 + ny_second_home_fair_market_value: 400_000 + output: + # Line 6 = $1,000,000, shared 9:6: $600,000 on the first home and + # $400,000 on the second. The first home's other $300,000 fits in its + # room; the second home has no room, so its other $200,000 does not + # count. Line 7 = min($300,000, $100,000). + # ($1,000,000 + $100,000) / $1,500,000 = .733; $60,000 x .733. + ny_mortgage_interest_deduction: 43_980 + +- name: A directly supplied deduction passes through when nothing is disallowed + period: 2025 + input: + state_code: NY + deductible_mortgage_interest: 12_000 + output: + ny_mortgage_interest_deduction: 12_000 + +- name: Before 2018 New York uses the federal amount + period: 2017 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: SINGLE + home_mortgage_interest: 40_000 + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2010 + ny_first_home_fair_market_value: 2_000_000 + output: + # Federal: $40,000 x $1,000,000 / $1,500,000. + deductible_mortgage_interest_tax_unit: 26_666.67 + ny_mortgage_interest_deduction: 26_666.67 + +- name: Rounding cannot take New York below the federal amount + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: SINGLE + home_mortgage_interest: 60_000 + first_home_mortgage_balance: 1_200_000 + first_home_mortgage_origination_year: 2015 + output: + # Federal: $60,000 x $1,000,000 / $1,200,000 = $50,000. The worksheet + # gives $60,000 x .833 = $49,980; PE keeps the federal $50,000. + deductible_mortgage_interest_tax_unit: 50_000 + ny_mortgage_interest_deduction: 50_000 + +- name: Mortgage interest outside New York is zero + period: 2025 + input: + state_code: NJ + home_mortgage_interest: 36_000 + first_home_mortgage_balance: 900_000 + first_home_mortgage_origination_year: 2020 + output: + ny_mortgage_interest_deduction: 0 diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.py new file mode 100644 index 00000000000..a30c7ce2626 --- /dev/null +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.py @@ -0,0 +1,25 @@ +from policyengine_us.model_api import * + + +class ny_interest_deduction(Variable): + value_type = float + entity = TaxUnit + label = "New York interest deduction" + unit = USD + definition_period = YEAR + documentation = ( + "Interest New York allows as an itemized deduction: the federal " + "interest_deduction components, with New York's pre-TCJA home " + "mortgage interest in place of the federal home mortgage interest." + ) + reference = ( + "https://www.nysenate.gov/legislation/laws/TAX/615", + "https://www.tax.ny.gov/pit/file/itemized-deductions-2025.htm#interest", + ) + defined_for = StateCode.NY + + adds = [ + "ny_mortgage_interest_deduction", + "non_mortgage_interest", + "deductible_mortgage_insurance_premiums", + ] diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemized_deductions_max.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemized_deductions_max.py index c217b06ddea..0b23e8bd25f 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemized_deductions_max.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemized_deductions_max.py @@ -21,12 +21,15 @@ class ny_itemized_deductions_max(Variable): - Medical expenses deductible above 10% of AGI (not the 7.5% floor) - Miscellaneous deductions still allowed with 2% AGI floor - Casualty losses not limited to federally declared disasters + - Mortgage interest under the pre-TCJA acquisition and home equity + debt limits (ny_interest_deduction) - NY-specific college tuition deduction addition per ยง 615(d) """ adds = [ # Federal deductions that don't differ from pre-TCJA "charitable_deduction", - "interest_deduction", + # Federal interest components with NY's pre-TCJA mortgage interest + "ny_interest_deduction", # NY-specific pre-TCJA versions "ny_medical_expense_deduction", "ny_salt_deduction", diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py new file mode 100644 index 00000000000..925176dde51 --- /dev/null +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py @@ -0,0 +1,182 @@ +from policyengine_us.model_api import * + + +def _ny_qualified_loan_limit( + grandfathered_debts, + other_debts, + non_acquisition_debts, + fair_market_values, + home_acquisition_debt_limit, + home_equity_debt_limit, +): + """Pre-TCJA IRC 163(h)(3)(B)-(D), which N.Y. Tax Law 615(a) preserves. + + Each argument except the limits is a list with one array per home. + + Grandfathered debt, incurred on or before October 13, 1987, has no dollar + limit but reduces the acquisition-debt limit for other acquisition debt + (2017 Publication 936, Table 1, lines 1-6). Acquisition debt above that + limit, and debt that was never acquisition debt, may qualify as home + equity debt (Figure A, note 3). On each home, home equity debt cannot + exceed the home's fair market value less the debt on it treated as + acquisition or grandfathered debt (IRC 163(h)(3)(C)(i)); across homes it + cannot exceed the home equity debt limit (line 7). + + The limit on other acquisition debt is shared among homes in proportion + to each home's other acquisition debt; the statute's aggregate limit does + not say how to place it on a particular home. + """ + grandfathered_debt = sum(grandfathered_debts) + other_debt = sum(other_debts) + acquisition_debt = grandfathered_debt + other_debt + # Lines 3-6: debt treated as acquisition or grandfathered debt. + line_6 = min_( + max_(grandfathered_debt, home_acquisition_debt_limit), acquisition_debt + ) + other_debt_treated = line_6 - grandfathered_debt + home_equity_debt = 0 + for grandfathered, other, non_acquisition, fair_market_value in zip( + grandfathered_debts, other_debts, non_acquisition_debts, fair_market_values + ): + other_share = np.divide( + other, + other_debt, + out=np.zeros_like(other_debt), + where=other_debt > 0, + ) + treated = grandfathered + other_debt_treated * other_share + excess_acquisition = other - other_debt_treated * other_share + equity_room = max_(fair_market_value - treated, 0) + home_equity_debt = home_equity_debt + min_( + excess_acquisition + non_acquisition, equity_room + ) + # Line 7. + line_7 = min_(home_equity_debt, home_equity_debt_limit) + # Line 8. + return line_6 + line_7 + + +def _ny_deductible_share(qualified_loan_limit, total_debt): + """2017 Publication 936, Table 1, line 11. + + The worksheet divides the qualified loan limit by total debt, rounded to + three places; PE rounds half up. Interest is treated as within the + limits, a share of one, when no balance is reported, as in the federal + calculation. The ratio is computed in double precision so that single + precision cannot move a ratio across a rounding boundary. + """ + limit = np.asarray(qualified_loan_limit, dtype=np.float64) + debt = np.asarray(total_debt, dtype=np.float64) + ratio = np.divide(limit, debt, out=np.ones_like(debt), where=debt > 0) + return np.floor(ratio * 1_000 + 0.5) / 1_000 + + +def _ny_allowed_interest( + deductible, non_deductible, non_acquisition_debt, deductible_share +): + """Worksheet lines 10 and 12, with PE's federal pass-through and floor. + + Interest paid (line 10) is the federally deductible and non-deductible + parts together, the raw interest rather than the federally allowed + amount. When federal law disallows none of the interest and no + non-acquisition debt is reported, New York's limits are at least as + large as the federal ones, so the federal amount passes through; this + keeps a deduction supplied directly. Otherwise the worksheet result + applies, floored at the federal deduction when no non-acquisition debt + is reported so that rounding to three places cannot take New York below + PE's unrounded federal amount. The federal calculation does not see + non-acquisition debt, so no floor applies when it is reported. + """ + worksheet = (deductible + non_deductible) * deductible_share + has_non_acquisition_debt = non_acquisition_debt > 0 + floor = where(has_non_acquisition_debt, 0, deductible) + uses_worksheet = (non_deductible > 0) | has_non_acquisition_debt + return where(uses_worksheet, max_(worksheet, floor), deductible) + + +class ny_mortgage_interest_deduction(Variable): + value_type = float + entity = TaxUnit + label = "New York home mortgage interest deduction" + unit = USD + definition_period = YEAR + documentation = ( + "Home mortgage interest New York allows. N.Y. Tax Law 615(a) computes " + "New York itemized deductions as they existed immediately before " + "P.L. 115-97, so from 2018 New York keeps interest on up to " + "$1,000,000 of home acquisition debt plus $100,000 of home equity " + "debt ($500,000 and $50,000 for married filing separately), rather " + "than the federal $750,000 limit for debt incurred after December 15, " + "2017 and the federal suspension of home equity interest. Before 2018 " + "New York uses the federal amount." + ) + reference = ( + "https://www.nysenate.gov/legislation/laws/TAX/615", + # L.2018, chapter 59, Part JJ, sections 3 and 7. PDF pages 42-43. + "https://legislation.nysenate.gov/pdf/bills/2017/S7509C#page=42", + "https://www.tax.ny.gov/pit/file/itemized-deductions-2025.htm#interest", + # 26 U.S.C. 163(h)(3)(B)-(D), 2017 edition. + "https://www.govinfo.gov/content/pkg/USCODE-2017-title26/html/USCODE-2017-title26-subtitleA-chap1-subchapB-partVI-sec163.htm", + # 2017 Publication 936: debt definitions, home equity debt limit, + # Table 1 and grandfathered debt. + # PDF pages 3, 10-11, 13 + "https://www.irs.gov/pub/irs-prior/p936--2017.pdf#page=3", + ) + defined_for = StateCode.NY + + def formula(tax_unit, period, parameters): + # Before 2018, New York's itemized deductions follow the federal ones. + return add(tax_unit, period, ["deductible_mortgage_interest"]) + + def formula_2018(tax_unit, period, parameters): + balances = [ + tax_unit("first_home_mortgage_balance", period).astype(np.float64), + tax_unit("second_home_mortgage_balance", period).astype(np.float64), + ] + origination_years = [ + tax_unit("first_home_mortgage_origination_year", period), + tax_unit("second_home_mortgage_origination_year", period), + ] + non_acquisition_debts = [ + tax_unit("ny_first_home_non_acquisition_debt", period).astype(np.float64), + tax_unit("ny_second_home_non_acquisition_debt", period).astype(np.float64), + ] + fair_market_values = [ + tax_unit("ny_first_home_fair_market_value", period).astype(np.float64), + tax_unit("ny_second_home_fair_market_value", period).astype(np.float64), + ] + p_irs = parameters(period).gov.irs.deductions.itemized.interest.mortgage + # The shared origination-year convention: a loan from 1987 or earlier + # is grandfathered debt, and 0 means unknown, not grandfathered. + grandfathered_debts = [ + where( + (year > 0) & (year <= p_irs.grandfathered_origination_year), + balance, + 0, + ) + for balance, year in zip(balances, origination_years) + ] + other_debts = [ + balance - grandfathered + for balance, grandfathered in zip(balances, grandfathered_debts) + ] + filing_status = tax_unit("filing_status", period) + p = parameters(period).gov.states.ny.tax.income.deductions.itemized.interest + qualified_loan_limit = _ny_qualified_loan_limit( + grandfathered_debts, + other_debts, + non_acquisition_debts, + fair_market_values, + p.home_acquisition_debt_limit[filing_status], + p.home_equity_debt_limit[filing_status], + ) + non_acquisition_debt = sum(non_acquisition_debts) + # Line 9: total average balances. + total_debt = sum(balances) + non_acquisition_debt + deductible_share = _ny_deductible_share(qualified_loan_limit, total_debt) + return _ny_allowed_interest( + add(tax_unit, period, ["deductible_mortgage_interest"]), + add(tax_unit, period, ["non_deductible_mortgage_interest"]), + non_acquisition_debt, + deductible_share, + ) diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py new file mode 100644 index 00000000000..5e9d7442f0c --- /dev/null +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py @@ -0,0 +1,80 @@ +from policyengine_us.model_api import * + +_FAIR_MARKET_VALUE_DEFAULT = ( + "Leave at 0 if unknown. A value of 0 gives the home no room for home " + "equity debt, so New York then allows no home equity debt on it and the " + "deduction rests on acquisition and grandfathered debt alone." +) + +_NON_ACQUISITION_DEBT_DEFAULT = ( + "Include the interest on this debt in the home mortgage interest inputs. " + "Defaults to 0, which leaves New York's calculation to the acquisition " + "mortgage balances." +) + + +class ny_first_home_fair_market_value(Variable): + value_type = float + entity = TaxUnit + label = "New York first home fair market value" + unit = USD + definition_period = YEAR + default_value = 0 + defined_for = StateCode.NY + documentation = ( + "Fair market value of the main home, which limits the home equity " + "debt New York allows on it under pre-TCJA IRC 163(h)(3)(C)(i). " + + _FAIR_MARKET_VALUE_DEFAULT + ) + reference = "https://www.nysenate.gov/legislation/laws/TAX/615" + + +class ny_second_home_fair_market_value(Variable): + value_type = float + entity = TaxUnit + label = "New York second home fair market value" + unit = USD + definition_period = YEAR + default_value = 0 + defined_for = StateCode.NY + documentation = ( + "Fair market value of the second home, which limits the home equity " + "debt New York allows on it under pre-TCJA IRC 163(h)(3)(C)(i). " + + _FAIR_MARKET_VALUE_DEFAULT + ) + reference = "https://www.nysenate.gov/legislation/laws/TAX/615" + + +class ny_first_home_non_acquisition_debt(Variable): + value_type = float + entity = TaxUnit + label = "New York first home non-acquisition debt" + unit = USD + definition_period = YEAR + default_value = 0 + defined_for = StateCode.NY + documentation = ( + "Average balance of debt secured by the main home that is not home " + "acquisition debt (not used to buy, build or substantially improve " + "it), such as a home equity loan spent on other things. Pre-TCJA IRC " + "163(h)(3)(C) treats it as home equity debt for New York. " + + _NON_ACQUISITION_DEBT_DEFAULT + ) + reference = "https://www.nysenate.gov/legislation/laws/TAX/615" + + +class ny_second_home_non_acquisition_debt(Variable): + value_type = float + entity = TaxUnit + label = "New York second home non-acquisition debt" + unit = USD + definition_period = YEAR + default_value = 0 + defined_for = StateCode.NY + documentation = ( + "Average balance of debt secured by the second home that is not home " + "acquisition debt (not used to buy, build or substantially improve " + "it). Pre-TCJA IRC 163(h)(3)(C) treats it as home equity debt for " + "New York. " + _NON_ACQUISITION_DEBT_DEFAULT + ) + reference = "https://www.nysenate.gov/legislation/laws/TAX/615" diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/phase_out/ny_itemized_deductions_phase_out.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/phase_out/ny_itemized_deductions_phase_out.py index db906ce1a2f..3a311801b19 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/phase_out/ny_itemized_deductions_phase_out.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/phase_out/ny_itemized_deductions_phase_out.py @@ -45,7 +45,7 @@ class ny_itemized_deductions_phase_out(Variable): L3 = L1 - L2 (worksheet line 3) is therefore the mortgage interest, real estate taxes, charitable gifts, and 2%-floor miscellaneous deductions - exactly the 68(c) base subject to the limitation. Because investment - interest appears in both L1 (inside interest_deduction) and L2, it cancels + interest appears in both L1 (inside ny_interest_deduction) and L2, it cancels out of L3, as intended. """ From 4b893cfcb9df1d8c150f0aa33f9f4c29d4df38db Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 15:15:11 +0000 Subject: [PATCH 2/5] Keep directly supplied interest totals in New York's interest deduction Building ny_interest_deduction from its components dropped an interest total supplied as deductible_interest_expense, which ny_itemized_deductions_max.yaml uses. Start from interest_deduction and swap only the federal mortgage component for New York's. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6J5LH2sLC5oX6pUHnC --- .../itemized/ny_interest_deduction.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.py index a30c7ce2626..24ba2108855 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.py @@ -9,8 +9,8 @@ class ny_interest_deduction(Variable): definition_period = YEAR documentation = ( "Interest New York allows as an itemized deduction: the federal " - "interest_deduction components, with New York's pre-TCJA home " - "mortgage interest in place of the federal home mortgage interest." + "interest_deduction with New York's pre-TCJA home mortgage interest " + "in place of the federal home mortgage interest." ) reference = ( "https://www.nysenate.gov/legislation/laws/TAX/615", @@ -18,8 +18,13 @@ class ny_interest_deduction(Variable): ) defined_for = StateCode.NY - adds = [ - "ny_mortgage_interest_deduction", - "non_mortgage_interest", - "deductible_mortgage_insurance_premiums", - ] + def formula(tax_unit, period, parameters): + # Starting from interest_deduction keeps an interest total supplied + # directly (deductible_interest_expense); only the federal mortgage + # component is swapped for New York's. + federal_interest = tax_unit("interest_deduction", period) + federal_mortgage_interest = add( + tax_unit, period, ["deductible_mortgage_interest"] + ) + ny_mortgage_interest = tax_unit("ny_mortgage_interest_deduction", period) + return federal_interest - federal_mortgage_interest + ny_mortgage_interest From 7538a504f8a6f6ec021150e4891b8b75c37cd9b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 20:52:45 +0000 Subject: [PATCH 3/5] Treat an unknown home value as no limit and let New York use the larger allocation share Round 1 review of the New York pre-TCJA mortgage interest calculation: - A fair market value of 0 now means unknown, as the Hawaii calculation and the origination-year inputs treat it, so home equity debt on a home without a value is capped only by the $100,000 ($50,000 separate) limit instead of losing its interest. - The deductible share is the larger of the 2017 Publication 936 worksheet's three-place share and the unrounded ratio of Treas. Reg. 1.163-10T(d); CCA 201201017 allows any reasonable method. The federal floor, which followed no source, goes. The pass-through stays so a directly supplied deduction is not limited twice. - The non-acquisition debt inputs now say that PE's federal calculation does not disallow that interest after 2017. - YAML expectations, two new cases and the Hypothesis properties follow. Co-Authored-By: Claude Opus 5.5 --- .../test_ny_mortgage_debt_limit_invariants.py | 98 ++++++++++++++----- .../ny_mortgage_interest_deduction.yaml | 84 ++++++++++++---- .../ny_mortgage_interest_deduction.py | 56 ++++++----- .../itemized/ny_mortgage_interest_inputs.py | 8 +- 4 files changed, 179 insertions(+), 67 deletions(-) diff --git a/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py index 8f7b21d845a..4d03bf23f14 100644 --- a/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py +++ b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py @@ -7,7 +7,9 @@ limit ($500,000 married filing separately); acquisition debt above that limit and non-acquisition debt count as home equity debt up to $100,000 ($50,000), and on each home only up to its fair market value less the debt on it -treated as acquisition or grandfathered debt. +treated as acquisition or grandfathered debt. A fair market value of 0 means +unknown: the home counts as worth at least the debt on it, so only the dollar +limit applies. Hypothesis draws two homes, each with an acquisition balance and vintage, non-acquisition debt and a fair market value, around every cutoff and limit, @@ -15,16 +17,29 @@ 1. Differential: it equals an independent restatement in the statute's order, which places the acquisition limit on each home by a separate loop. + The restatement shares the acquisition limit between homes the same way + (in proportion to each home's other acquisition debt), so it cannot catch + a wrong sharing rule. 2. Bounds: grandfathered debt <= New York limit <= total debt, and the New York limit is at least PE's federal limit for the same acquisition loans. 3. Defaults: with no fair market value and no non-acquisition debt (the - input defaults), no debt counts as home equity debt. + input defaults), the limit is line 6 plus the acquisition debt above it, + up to the home equity debt limit. 4. Order: swapping the two homes never changes the limit. -5. Monotone: a higher fair market value never lowers the limit. +5. Monotone: a higher positive fair market value never lowers the limit. +6. Unknown value: setting every fair market value to 0 (unknown) never + lowers the limit. -And for _ny_allowed_interest: with no non-acquisition debt the result lies -between the federal deduction and the interest paid, and equals the federal -deduction when federal law disallows nothing. +For _ny_allowed_interest, interest on two homes with no non-acquisition debt +is split into the federally deductible and non-deductible parts by PE's +federal share for the same loans. The New York result lies between the +federal deduction and the interest paid, and equals the federal deduction +when federal law disallows nothing. The federal bound follows from the +limits, not from a floor. + +For _ny_deductible_share: the share is at least the unrounded ratio and at +least the ratio rounded half up to three places, at most one, and exceeds the +unrounded ratio by no more than .0005. The limits are read from the New York and federal parameters for every filing status in 2018, 2025 and 2026. The properties cannot pass trivially: @@ -36,6 +51,9 @@ policy outcomes are checked in ny_mortgage_interest_deduction.yaml. """ +import math +from fractions import Fraction + import numpy as np from hypothesis import given, settings from hypothesis import strategies as st @@ -145,8 +163,11 @@ def statute(pair, limits): else: on_home = other_allowed * balance / other if other > 0 else 0.0 excess = balance - on_home - # (C)(i): no more than fair market value less acquisition debt. - room = max(0.0, fair_market_value - on_home) + # (C)(i): no more than fair market value less acquisition debt. A + # value of 0 means unknown and sets no such limit. + room = ( + max(0.0, fair_market_value - on_home) if fair_market_value > 0 else math.inf + ) home_equity += min(excess + non_acquisition, room) # (C)(ii): the aggregate home equity limit. return old + other_allowed + min(home_equity, home_equity_limit) @@ -188,30 +209,47 @@ def test_new_york_qualified_loan_limit_properties(first, second, status): assert old - TOLERANCE <= limit <= total + TOLERANCE assert limit >= federal_limit(pair, federal_limits) - TOLERANCE # 3. Defaults: no fair market value and no non-acquisition debt. + acquisition_limit, home_equity_limit = ny_limits defaults = [(home[0], home[1], 0.0, 0.0) for home in pair] acquisition = sum(home[0] for home in pair) - assert ( - abs(model_limit(defaults, ny_limits) - min(max(old, ny_limits[0]), acquisition)) - <= TOLERANCE - ) + line_6 = min(max(old, acquisition_limit), acquisition) + expected = line_6 + min(acquisition - line_6, home_equity_limit) + assert abs(model_limit(defaults, ny_limits) - expected) <= TOLERANCE # 4. Order. assert abs(limit - model_limit([second, first], ny_limits)) <= TOLERANCE - # 5. Monotone in fair market value. - richer = [(home[0], home[1], home[2], home[3] + 250_000) for home in pair] + # 5. Monotone in a positive fair market value. + richer = [ + (home[0], home[1], home[2], home[3] + 250_000 if home[3] > 0 else 0.0) + for home in pair + ] assert model_limit(richer, ny_limits) >= limit - TOLERANCE + # 6. An unknown value (0) never lowers the limit. + unknown = [(home[0], home[1], home[2], 0.0) for home in pair] + assert model_limit(unknown, ny_limits) >= limit - TOLERANCE @settings(**SETTINGS) @given( - deductible=amounts, - non_deductible=amounts, - limit=amounts, - debt=amounts, + interest=amounts, + first=homes, + second=homes, + status=st.sampled_from(sorted(LIMITS)), ) def test_new_york_allowed_interest_without_non_acquisition_debt( - deductible, non_deductible, limit, debt + interest, first, second, status ): - share = _ny_deductible_share(np.array([min(limit, debt)]), np.array([debt])) + ny_limits, federal_limits = LIMITS[status] + pair = [(home[0], home[1], 0.0, home[3]) for home in (first, second)] + acquisition = sum(home[0] for home in pair) + if acquisition > 0: + federal_share = min(1.0, federal_limit(pair, federal_limits) / acquisition) + else: + federal_share = 1.0 + deductible = interest * federal_share + non_deductible = interest - deductible + share = _ny_deductible_share( + np.array([model_limit(pair, ny_limits)]), np.array([acquisition]) + ) allowed = float( _ny_allowed_interest( np.array([deductible]), @@ -220,8 +258,22 @@ def test_new_york_allowed_interest_without_non_acquisition_debt( share, )[0] ) - assert 0 <= share[0] <= 1 - assert deductible - TOLERANCE <= allowed - assert allowed <= deductible + non_deductible + TOLERANCE + assert deductible - TOLERANCE <= allowed <= interest + TOLERANCE if non_deductible == 0: assert allowed == deductible + + +@settings(**SETTINGS) +@given(limit=amounts, debt=amounts) +def test_new_york_deductible_share_is_the_larger_method(limit, debt): + debt = max(debt, 1.0) + limit = min(limit, debt) + share = float(_ny_deductible_share(np.array([limit]), np.array([debt]))[0]) + ratio = limit / debt + # Rounded half up to three places, computed exactly. + exact = Fraction(int(limit), int(debt)) + rounded = math.floor(exact * 1_000 + Fraction(1, 2)) / 1_000 + assert share >= ratio - 1e-12 + assert share >= rounded - 1e-12 + assert share <= 1 + 1e-12 + assert share - ratio <= 0.0005 + 1e-12 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml index 779833a3c44..c718feb93b1 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml @@ -6,10 +6,12 @@ # line 6 = min(max(G, $1,000,000), G + A) # line 7 = min(home equity debt, $100,000), where home equity debt is the # acquisition debt above line 6 plus non-acquisition debt, limited -# on each home to its fair market value less the debt on it -# counted in line 6 +# on each home to its room: its fair market value less the debt on +# it counted in line 6 (a value of 0 means unknown and sets no +# limit) # line 8 = line 6 + line 7 -# line 11 = line 8 / total debt, rounded to three places +# line 11 = the larger of line 8 / total debt rounded to three places and +# the unrounded ratio # line 12 = interest x line 11 # Limits are halved for married filing separately. PolicyEngine's federal # deduction uses the $750,000 limit for debt from 2018 or later, no home @@ -49,8 +51,9 @@ output: # Federal: $40,000 x $750,000 / $1,500,000 = $20,000. deductible_mortgage_interest_tax_unit: 20_000 - # New York: ($1,000,000 + $100,000) / $1,500,000 = .733; $40,000 x .733. - ny_mortgage_interest_deduction: 29_320 + # New York: ($1,000,000 + $100,000) / $1,500,000 = .7333. The worksheet's + # .733 rounds down, so the unrounded share applies: $40,000 x 11/15. + ny_mortgage_interest_deduction: 29_333.33 - name: Separate returns halve both limits period: 2025 @@ -66,10 +69,11 @@ output: # Federal: $20,000 x $375,000 / $600,000 = $12,500. deductible_mortgage_interest_tax_unit: 12_500 - # New York: ($500,000 + $50,000) / $600,000 = .917; $20,000 x .917. + # New York: ($500,000 + $50,000) / $600,000 = .9167. The worksheet's .917 + # is the larger share: $20,000 x .917. ny_mortgage_interest_deduction: 18_340 -- name: Without a fair market value no debt counts as home equity debt +- name: Without a fair market value, only the dollar limit caps home equity debt period: 2025 absolute_error_margin: 0.01 input: @@ -80,8 +84,10 @@ first_home_mortgage_origination_year: 2020 output: deductible_mortgage_interest_tax_unit: 20_000 - # New York: $1,000,000 / $1,500,000 = .667; $40,000 x .667. - ny_mortgage_interest_deduction: 26_680 + # New York: a value of 0 means unknown, so the home has no room limit. + # Line 7 = min($500,000, $100,000). ($1,000,000 + $100,000) / $1,500,000 + # = .7333 (worksheet .733); $40,000 x 11/15. + ny_mortgage_interest_deduction: 29_333.33 - name: Fair market value limits home equity debt period: 2025 @@ -95,8 +101,9 @@ ny_first_home_fair_market_value: 1_040_000 output: # Room for home equity debt: $1,040,000 - $1,000,000 = $40,000. - # New York: $1,040,000 / $1,500,000 = .693; $40,000 x .693. - ny_mortgage_interest_deduction: 27_720 + # New York: $1,040,000 / $1,500,000 = .6933 (worksheet .693); + # $40,000 x 1.04 / 1.5. + ny_mortgage_interest_deduction: 27_733.33 - name: Non-acquisition debt counts as home equity debt up to the limit period: 2025 @@ -112,8 +119,9 @@ output: # PE's federal calculation does not see the non-acquisition debt. deductible_mortgage_interest_tax_unit: 35_000 - # New York: ($500,000 + $100,000) / $700,000 = .857; $35,000 x .857. - ny_mortgage_interest_deduction: 29_995 + # New York: ($500,000 + $100,000) / $700,000 = .8571 (worksheet .857); + # $35,000 x 6/7. + ny_mortgage_interest_deduction: 30_000 - name: Grandfathered debt reduces the acquisition-debt limit period: 2025 @@ -135,7 +143,8 @@ # New York: line 6 = max($400,000, $1,000,000) = $1,000,000, of which # $600,000 is the second home's; its other $200,000 is home equity debt, # within its room of $900,000 - $600,000 = $300,000 and limited to - # $100,000. ($1,000,000 + $100,000) / $1,200,000 = .917; $50,000 x .917. + # $100,000. ($1,000,000 + $100,000) / $1,200,000 = .9167. The worksheet's + # .917 is the larger share: $50,000 x .917. ny_mortgage_interest_deduction: 45_850 - name: Each home's equity room limits its own excess debt @@ -156,8 +165,9 @@ # $400,000 on the second. The first home's other $300,000 fits in its # room; the second home has no room, so its other $200,000 does not # count. Line 7 = min($300,000, $100,000). - # ($1,000,000 + $100,000) / $1,500,000 = .733; $60,000 x .733. - ny_mortgage_interest_deduction: 43_980 + # ($1,000,000 + $100,000) / $1,500,000 = .7333 (worksheet .733); + # $60,000 x 11/15. + ny_mortgage_interest_deduction: 44_000 - name: A directly supplied deduction passes through when nothing is disallowed period: 2025 @@ -167,6 +177,39 @@ output: ny_mortgage_interest_deduction: 12_000 +- name: A directly supplied deduction passes through even with balances + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + deductible_mortgage_interest: 12_000 + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2020 + output: + # With no home_mortgage_interest, non_deductible_mortgage_interest_tax_unit + # reads only home_mortgage_interest_tax_unit and the points, so it is 0 + # and the supplied $12,000 passes through. Without the pass-through, the + # worksheet would limit it again: $12,000 x 11/15 = $8,800. + ny_mortgage_interest_deduction: 12_000 + +- name: Without a fair market value, home equity debt keeps its interest + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: SINGLE + home_mortgage_interest: 20_000 + first_home_mortgage_balance: 300_000 + first_home_mortgage_origination_year: 2020 + ny_first_home_non_acquisition_debt: 50_000 + output: + # PE's federal calculation does not see the $50,000. + deductible_mortgage_interest_tax_unit: 20_000 + # New York: line 8 = $300,000 + min($50,000, $100,000) = $350,000, equal + # to line 9, so the share is 1. A value of 0 once gave this home no room + # and $17,140. + ny_mortgage_interest_deduction: 20_000 + - name: Before 2018 New York uses the federal amount period: 2017 absolute_error_margin: 0.01 @@ -182,7 +225,7 @@ deductible_mortgage_interest_tax_unit: 26_666.67 ny_mortgage_interest_deduction: 26_666.67 -- name: Rounding cannot take New York below the federal amount +- name: When the worksheet rounds down, New York uses the unrounded share period: 2025 absolute_error_margin: 0.01 input: @@ -191,9 +234,12 @@ home_mortgage_interest: 60_000 first_home_mortgage_balance: 1_200_000 first_home_mortgage_origination_year: 2015 + ny_first_home_fair_market_value: 1_000_000 output: - # Federal: $60,000 x $1,000,000 / $1,200,000 = $50,000. The worksheet - # gives $60,000 x .833 = $49,980; PE keeps the federal $50,000. + # Federal: $60,000 x $1,000,000 / $1,200,000 = $50,000. + # New York: room = $1,000,000 - $1,000,000 = $0, so line 7 is 0; + # $1,000,000 / $1,200,000 = .8333. The worksheet's .833 gives $49,980, + # the unrounded share $50,000, which applies. deductible_mortgage_interest_tax_unit: 50_000 ny_mortgage_interest_deduction: 50_000 diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py index 925176dde51..abcbbe015ec 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py @@ -19,7 +19,8 @@ def _ny_qualified_loan_limit( limit, and debt that was never acquisition debt, may qualify as home equity debt (Figure A, note 3). On each home, home equity debt cannot exceed the home's fair market value less the debt on it treated as - acquisition or grandfathered debt (IRC 163(h)(3)(C)(i)); across homes it + acquisition or grandfathered debt (IRC 163(h)(3)(C)(i)); a value of 0 + means unknown and sets no such limit. Across homes it cannot exceed the home equity debt limit (line 7). The limit on other acquisition debt is shared among homes in proportion @@ -46,7 +47,11 @@ def _ny_qualified_loan_limit( ) treated = grandfathered + other_debt_treated * other_share excess_acquisition = other - other_debt_treated * other_share - equity_room = max_(fair_market_value - treated, 0) + # 0 means unknown: treat the home as worth at least the debt on it, + # as the Hawaii calculation does. + equity_room = where( + fair_market_value > 0, max_(fair_market_value - treated, 0), np.inf + ) home_equity_debt = home_equity_debt + min_( excess_acquisition + non_acquisition, equity_room ) @@ -57,41 +62,42 @@ def _ny_qualified_loan_limit( def _ny_deductible_share(qualified_loan_limit, total_debt): - """2017 Publication 936, Table 1, line 11. + """2017 Publication 936, Table 1, line 11, or the unrounded ratio. - The worksheet divides the qualified loan limit by total debt, rounded to - three places; PE rounds half up. Interest is treated as within the - limits, a share of one, when no balance is reported, as in the federal - calculation. The ratio is computed in double precision so that single - precision cannot move a ratio across a rounding boundary. + Until regulations issue, the House report on OBRA 1987 allows any + reasonable method of allocating interest on debt above the limits. The + IRS accepts the worksheet, which rounds the ratio to three places, and + the regulation's simplified method, which uses the unrounded ratio + (Treas. Reg. 1.163-10T(d); CCA 201201017). A taxpayer can use either, so + the larger share applies; PE rounds half up. The share is one when the + household reports no balance, as in the federal calculation. PE computes the + ratio in double precision so that single precision cannot move it + across a rounding boundary. """ limit = np.asarray(qualified_loan_limit, dtype=np.float64) debt = np.asarray(total_debt, dtype=np.float64) ratio = np.divide(limit, debt, out=np.ones_like(debt), where=debt > 0) - return np.floor(ratio * 1_000 + 0.5) / 1_000 + worksheet = np.floor(ratio * 1_000 + 0.5) / 1_000 + return np.maximum(worksheet, ratio) def _ny_allowed_interest( deductible, non_deductible, non_acquisition_debt, deductible_share ): - """Worksheet lines 10 and 12, with PE's federal pass-through and floor. + """Worksheet lines 10 and 12, with PE's federal pass-through. Interest paid (line 10) is the federally deductible and non-deductible parts together, the raw interest rather than the federally allowed - amount. When federal law disallows none of the interest and no - non-acquisition debt is reported, New York's limits are at least as - large as the federal ones, so the federal amount passes through; this - keeps a deduction supplied directly. Otherwise the worksheet result - applies, floored at the federal deduction when no non-acquisition debt - is reported so that rounding to three places cannot take New York below - PE's unrounded federal amount. The federal calculation does not see - non-acquisition debt, so no floor applies when it is reported. + amount. When federal law disallows none of the interest and the + household reports no non-acquisition debt, the federal amount passes + through, which keeps a deduction supplied directly. Otherwise line 12 + applies. New York's limits never fall below the federal ones for the + same loans, so without non-acquisition debt the result never falls + below PE's federal amount. """ worksheet = (deductible + non_deductible) * deductible_share - has_non_acquisition_debt = non_acquisition_debt > 0 - floor = where(has_non_acquisition_debt, 0, deductible) - uses_worksheet = (non_deductible > 0) | has_non_acquisition_debt - return where(uses_worksheet, max_(worksheet, floor), deductible) + uses_worksheet = (non_deductible > 0) | (non_acquisition_debt > 0) + return where(uses_worksheet, worksheet, deductible) class ny_mortgage_interest_deduction(Variable): @@ -121,6 +127,12 @@ class ny_mortgage_interest_deduction(Variable): # Table 1 and grandfathered debt. # PDF pages 3, 10-11, 13 "https://www.irs.gov/pub/irs-prior/p936--2017.pdf#page=3", + # Treasury Regulation 1.163-10T(d) and (g): the unrounded simplified + # method and the taxpayer's choice of method. PDF pages 3-4, 6. + "https://www.govinfo.gov/content/pkg/CFR-2025-title26-vol3/pdf/CFR-2025-title26-vol3-sec1-163-10T.pdf#page=3", + # CCA 201201017 (nonprecedential): any reasonable allocation method. + # PDF pages 4-5. + "https://www.irs.gov/pub/irs-wd/1201017.pdf#page=4", ) defined_for = StateCode.NY diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py index 5e9d7442f0c..aace4efd0f1 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py @@ -1,13 +1,15 @@ from policyengine_us.model_api import * _FAIR_MARKET_VALUE_DEFAULT = ( - "Leave at 0 if unknown. A value of 0 gives the home no room for home " - "equity debt, so New York then allows no home equity debt on it and the " - "deduction rests on acquisition and grandfathered debt alone." + "Leave at 0 if unknown. New York then counts the home as worth at least " + "the debt on it, so only the $100,000 ($50,000 separate) home equity debt " + "limit applies, as in PolicyEngine's Hawaii calculation." ) _NON_ACQUISITION_DEBT_DEFAULT = ( "Include the interest on this debt in the home mortgage interest inputs. " + "PolicyEngine's federal calculation reads only the acquisition mortgage " + "balances, so it does not disallow that interest federally after 2017. " "Defaults to 0, which leaves New York's calculation to the acquisition " "mortgage balances." ) From 169fe4fd39fc37ae6d4e62a42bed776b4c330df8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 05:35:52 +0000 Subject: [PATCH 4/5] Round an exact tie in New York's worksheet share up, and tighten the text An exact tie such as 1,001,000 / 2,000,000 = .5005 comes to 500.49999999999994 after x 1,000 in binary, so the floor rounded it down to .500 and New York used the unrounded .5005 instead of the worksheet's .501. Twelve ratios from .5005 to .5115 did this. Rounding the scaled ratio to nine places before the floor makes the tie round up, as the docstring says. A YAML case and a Hypothesis example now pin the tie. Also from the round 2 review: - A YAML case for non-acquisition debt on the second home, which no test read before. - Property 3 now asserts that the Hawaii helper gives the same default limit, which the input documentation cites. - The input documentation credits the unknown-value rule to PolicyEngine, and says the federal calculation applies its share to home equity interest when the balances exceed the federal limit. - Active voice, a clearer subject and corrected Publication 936 pages in the docstrings and comments. - The Hawaii docstring and the rounding candidate in the source-conflict backlog now account for New York's home value input and its larger-share rule. Co-Authored-By: Claude Opus 5.5 --- .../test_ny_mortgage_debt_limit_invariants.py | 31 ++++++++----- .../ny_mortgage_interest_deduction.yaml | 43 +++++++++++++++++-- .../hi_mortgage_interest_deduction.py | 4 +- .../ny_mortgage_interest_deduction.py | 15 ++++--- .../itemized/ny_mortgage_interest_inputs.py | 11 ++--- source_conflicts/candidates.yaml | 7 ++- 6 files changed, 82 insertions(+), 29 deletions(-) diff --git a/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py index 4d03bf23f14..5530d861791 100644 --- a/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py +++ b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py @@ -24,15 +24,16 @@ York limit is at least PE's federal limit for the same acquisition loans. 3. Defaults: with no fair market value and no non-acquisition debt (the input defaults), the limit is line 6 plus the acquisition debt above it, - up to the home equity debt limit. + up to the home equity debt limit, which is also what the Hawaii helper + gives. 4. Order: swapping the two homes never changes the limit. 5. Monotone: a higher positive fair market value never lowers the limit. 6. Unknown value: setting every fair market value to 0 (unknown) never lowers the limit. -For _ny_allowed_interest, interest on two homes with no non-acquisition debt -is split into the federally deductible and non-deductible parts by PE's -federal share for the same loans. The New York result lies between the +For _ny_allowed_interest, PE's federal share for the same loans splits +interest on two homes with no non-acquisition debt into the federally +deductible and non-deductible parts. The New York result lies between the federal deduction and the interest paid, and equals the federal deduction when federal law disallows nothing. The federal bound follows from the limits, not from a floor. @@ -41,11 +42,11 @@ least the ratio rounded half up to three places, at most one, and exceeds the unrounded ratio by no more than .0005. -The limits are read from the New York and federal parameters for every -filing status in 2018, 2025 and 2026. The properties cannot pass trivially: -the differential compares against a separately written per-home calculation, -and the default and bound checks fail if the fair market value test, the -home equity limit or the acquisition limit is dropped or reversed. +The test reads the limits from the New York and federal parameters for +every filing status in 2018, 2025 and 2026. The properties cannot pass +trivially: the differential compares against a separately written per-home +calculation, and the default and bound checks fail if the fair market value +test, the home equity limit or the acquisition limit is dropped or reversed. These are for-all properties of the helpers, which YAML cannot state. The policy outcomes are checked in ny_mortgage_interest_deduction.yaml. @@ -55,10 +56,13 @@ from fractions import Fraction import numpy as np -from hypothesis import given, settings +from hypothesis import example, given, settings from hypothesis import strategies as st from policyengine_us.system import system +from policyengine_us.variables.gov.states.hi.tax.income.deductions.itemized.hi_mortgage_interest_deduction import ( + _hi_qualified_loan_limit, +) from policyengine_us.variables.gov.states.ny.tax.income.taxable_income.deductions.itemized.ny_mortgage_interest_deduction import ( _ny_allowed_interest, _ny_deductible_share, @@ -215,6 +219,12 @@ def test_new_york_qualified_loan_limit_properties(first, second, status): line_6 = min(max(old, acquisition_limit), acquisition) expected = line_6 + min(acquisition - line_6, home_equity_limit) assert abs(model_limit(defaults, ny_limits) - expected) <= TOLERANCE + hawaii = float( + _hi_qualified_loan_limit( + np.array([old]), np.array([acquisition - old]), *ny_limits + )[0] + ) + assert abs(expected - hawaii) <= TOLERANCE # 4. Order. assert abs(limit - model_limit([second, first], ny_limits)) <= TOLERANCE # 5. Monotone in a positive fair market value. @@ -265,6 +275,7 @@ def test_new_york_allowed_interest_without_non_acquisition_debt( @settings(**SETTINGS) @given(limit=amounts, debt=amounts) +@example(limit=1_001.0, debt=2_000.0) def test_new_york_deductible_share_is_the_larger_method(limit, debt): debt = max(debt, 1.0) limit = min(limit, debt) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml index c718feb93b1..5ebb83c3d0d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml @@ -13,7 +13,7 @@ # line 11 = the larger of line 8 / total debt rounded to three places and # the unrounded ratio # line 12 = interest x line 11 -# Limits are halved for married filing separately. PolicyEngine's federal +# Married filing separately halves the limits. PolicyEngine's federal # deduction uses the $750,000 limit for debt from 2018 or later, no home # equity debt and the exact ratio. @@ -123,6 +123,23 @@ # $35,000 x 6/7. ny_mortgage_interest_deduction: 30_000 +- name: Non-acquisition debt on the second home counts as home equity debt + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: JOINT + home_mortgage_interest: 30_000 + first_home_mortgage_balance: 400_000 + first_home_mortgage_origination_year: 2015 + ny_second_home_non_acquisition_debt: 200_000 + output: + # PE's federal calculation does not see the non-acquisition debt. + deductible_mortgage_interest_tax_unit: 30_000 + # New York: ($400,000 + $100,000) / $600,000 = .8333 (worksheet .833); + # $30,000 x 5/6. + ny_mortgage_interest_deduction: 25_000 + - name: Grandfathered debt reduces the acquisition-debt limit period: 2025 absolute_error_margin: 0.01 @@ -187,9 +204,10 @@ first_home_mortgage_origination_year: 2020 output: # With no home_mortgage_interest, non_deductible_mortgage_interest_tax_unit - # reads only home_mortgage_interest_tax_unit and the points, so it is 0 - # and the supplied $12,000 passes through. Without the pass-through, the - # worksheet would limit it again: $12,000 x 11/15 = $8,800. + # is 0: it takes home_mortgage_interest_tax_unit plus the points, less + # deductible_mortgage_interest_tax_unit, and does not read the supplied + # person-level amount. So the supplied $12,000 passes through. Without the + # pass-through, the worksheet would limit it again: $12,000 x 11/15 = $8,800. ny_mortgage_interest_deduction: 12_000 - name: Without a fair market value, home equity debt keeps its interest @@ -243,6 +261,23 @@ deductible_mortgage_interest_tax_unit: 50_000 ny_mortgage_interest_deduction: 50_000 +- name: An exact tie in the worksheet's share rounds up + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: SINGLE + home_mortgage_interest: 80_000 + first_home_mortgage_balance: 2_000_000 + first_home_mortgage_origination_year: 2020 + ny_first_home_fair_market_value: 1_001_000 + output: + # Federal: $80,000 x $750,000 / $2,000,000 = $30,000. + deductible_mortgage_interest_tax_unit: 30_000 + # New York: room = $1,001,000 - $1,000,000 = $1,000. $1,001,000 / + # $2,000,000 = .5005, an exact tie, so the worksheet's share is .501. + ny_mortgage_interest_deduction: 40_080 + - name: Mortgage interest outside New York is zero period: 2025 input: diff --git a/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_mortgage_interest_deduction.py b/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_mortgage_interest_deduction.py index 67a8036904a..7c2e0662d13 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_mortgage_interest_deduction.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_mortgage_interest_deduction.py @@ -19,8 +19,8 @@ def _hi_qualified_loan_limit( Debt above the acquisition-debt limit may qualify as home equity debt, up to the home equity debt limit (line 7; Publication 936 (2017), Figure A, note 3). The home equity debt limit is also capped by each home's fair - market value less its acquisition debt; PolicyEngine has no home value - input, so every home is treated as worth at least its mortgages. + market value less its acquisition debt; PolicyEngine has no Hawaii home + value input, so it counts every home as worth at least its mortgages. """ total_debt = grandfathered_debt + other_debt # Lines 3-6. diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py index abcbbe015ec..a61b4eb00ae 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py @@ -20,10 +20,10 @@ def _ny_qualified_loan_limit( equity debt (Figure A, note 3). On each home, home equity debt cannot exceed the home's fair market value less the debt on it treated as acquisition or grandfathered debt (IRC 163(h)(3)(C)(i)); a value of 0 - means unknown and sets no such limit. Across homes it + means unknown and sets no such limit. Across homes, home equity debt cannot exceed the home equity debt limit (line 7). - The limit on other acquisition debt is shared among homes in proportion + PE shares the limit on other acquisition debt among homes in proportion to each home's other acquisition debt; the statute's aggregate limit does not say how to place it on a particular home. """ @@ -77,7 +77,9 @@ def _ny_deductible_share(qualified_loan_limit, total_debt): limit = np.asarray(qualified_loan_limit, dtype=np.float64) debt = np.asarray(total_debt, dtype=np.float64) ratio = np.divide(limit, debt, out=np.ones_like(debt), where=debt > 0) - worksheet = np.floor(ratio * 1_000 + 0.5) / 1_000 + # Round the scaled ratio first. In binary, 1,001,000 / 2,000,000 x 1,000 + # comes to 500.49999999999994, so the floor alone rounds this exact tie down. + worksheet = np.floor(np.round(ratio * 1_000, 9) + 0.5) / 1_000 return np.maximum(worksheet, ratio) @@ -123,9 +125,10 @@ class ny_mortgage_interest_deduction(Variable): "https://www.tax.ny.gov/pit/file/itemized-deductions-2025.htm#interest", # 26 U.S.C. 163(h)(3)(B)-(D), 2017 edition. "https://www.govinfo.gov/content/pkg/USCODE-2017-title26/html/USCODE-2017-title26-subtitleA-chap1-subchapB-partVI-sec163.htm", - # 2017 Publication 936: debt definitions, home equity debt limit, - # Table 1 and grandfathered debt. - # PDF pages 3, 10-11, 13 + # 2017 Publication 936: Figure A note 3, home acquisition debt, home + # equity debt and its limit, grandfathered debt, and Table 1 with its + # line 7 instructions. + # PDF pages 3, 9-11, 13 "https://www.irs.gov/pub/irs-prior/p936--2017.pdf#page=3", # Treasury Regulation 1.163-10T(d) and (g): the unrounded simplified # method and the taxpayer's choice of method. PDF pages 3-4, 6. diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py index aace4efd0f1..6334a21a858 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py @@ -1,17 +1,18 @@ from policyengine_us.model_api import * _FAIR_MARKET_VALUE_DEFAULT = ( - "Leave at 0 if unknown. New York then counts the home as worth at least " + "Leave at 0 if unknown. PolicyEngine then counts the home as worth at least " "the debt on it, so only the $100,000 ($50,000 separate) home equity debt " - "limit applies, as in PolicyEngine's Hawaii calculation." + "limit applies, as in its Hawaii calculation." ) _NON_ACQUISITION_DEBT_DEFAULT = ( "Include the interest on this debt in the home mortgage interest inputs. " "PolicyEngine's federal calculation reads only the acquisition mortgage " - "balances, so it does not disallow that interest federally after 2017. " - "Defaults to 0, which leaves New York's calculation to the acquisition " - "mortgage balances." + "balances, so after 2017 it treats that interest like interest on those " + "balances: fully deductible when they fit the federal limit, and deductible " + "at the same share when they don't. Defaults to 0, which leaves New York's " + "calculation to the acquisition mortgage balances." ) diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 025bb5dd42b..104d42c38aa 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -10464,15 +10464,18 @@ candidates: - policyengine_us/variables/gov/states/al/tax/income/deductions/itemized/al_investment_interest.py - policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_before_limitation.py - policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_mortgage_interest_deduction.py + - policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py prs: - https://github.com/PolicyEngine/policyengine-us/pull/10274 + - https://github.com/PolicyEngine/policyengine-us/pull/10596 state: unreviewed - note: 'PE-US follows two conventions, so follows stays unknown until one is picked. The federal deduction + note: 'PE-US follows three conventions, so follows stays unknown until one is picked. The federal deduction (deductible_mortgage_interest_tax_unit) uses the exact ratio, and Alabama and North Carolina read that federal amount. Hawaii (hi_mortgage_interest_deduction) follows the 2017 worksheet as a gap-fill, rounding line 11 to three places, and floors the result at the federal deduction, a PE safeguard that binds only when the rounded share falls below the exact federal share. Settling the federal reading (rulespec-us#1413) and using it for - both would make that floor unnecessary.' + both would make that floor unnecessary. New York (ny_mortgage_interest_deduction) allows the larger of the + three-place share and the exact ratio.' - id: us:niit-excess-business-loss-addback program: income-tax years: From 53f8806db4aa11d7bc1ce0400301607051297ba4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 06:12:17 +0000 Subject: [PATCH 5/5] Make two sentences from the previous commit say what they mean The property 3 docstring's "which is also what the Hawaii helper gives" could attach to the home equity debt limit rather than the whole default limit, so it is now a sentence of its own. The rounding candidate's note said "using it for both" after listing three conventions; it now says Hawaii, whose floor the sentence is about. Co-Authored-By: Claude Opus 5.5 --- .../tests/core/test_ny_mortgage_debt_limit_invariants.py | 4 ++-- source_conflicts/candidates.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py index 5530d861791..b3a1e2a20c3 100644 --- a/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py +++ b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py @@ -24,8 +24,8 @@ York limit is at least PE's federal limit for the same acquisition loans. 3. Defaults: with no fair market value and no non-acquisition debt (the input defaults), the limit is line 6 plus the acquisition debt above it, - up to the home equity debt limit, which is also what the Hawaii helper - gives. + up to the home equity debt limit. The Hawaii helper gives the same + limit. 4. Order: swapping the two homes never changes the limit. 5. Monotone: a higher positive fair market value never lowers the limit. 6. Unknown value: setting every fair market value to 0 (unknown) never diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 104d42c38aa..74d0591305a 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -10474,8 +10474,8 @@ candidates: amount. Hawaii (hi_mortgage_interest_deduction) follows the 2017 worksheet as a gap-fill, rounding line 11 to three places, and floors the result at the federal deduction, a PE safeguard that binds only when the rounded share falls below the exact federal share. Settling the federal reading (rulespec-us#1413) and using it for - both would make that floor unnecessary. New York (ny_mortgage_interest_deduction) allows the larger of the - three-place share and the exact ratio.' + Hawaii too would make that floor unnecessary. New York (ny_mortgage_interest_deduction) allows the larger of + the three-place share and the exact ratio.' - id: us:niit-excess-business-loss-addback program: income-tax years: