From 7fb88f0abfdc22d0f4990845ba23553dde0d4d62 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 11:06:28 -0400 Subject: [PATCH 1/2] Floor Montana's joint-return medical and standard deductions on the return's Montana AGI The 2021-2023 Form 2 Itemized Deductions Schedule (line 1b) and Standard Deduction Worksheet (line 1) take Montana AGI from page 1, line 14. Single, head of household and joint returns report that line in one column, so a joint return pools both spouses' income before flooring at zero. Both joint-path variables instead summed each spouse's mt_agi_indiv, which floors each spouse at zero first, so a spouse's loss never offset the other's income. They now use mt_agi_joint. From 2024 mt_agi_indiv is switched off (Montana repealed filing separately on the same form), so the joint medical floor fell to zero and the full medical expense was deducted. From 2024 Form 2 line 2 takes federal itemized deductions (Worksheet A), so the variable now carries the federal medical expense deduction (7.5% of federal AGI). A new parameter, gov.states.mt.tax.income.deductions.itemized.state_specific_deduction_applies, marks the switch. Co-Authored-By: Claude Opus 5.5 --- .../fix-mt-medical-floor-joint-agi.fixed.md | 1 + .../state_specific_deduction_applies.yaml | 16 ++ .../mt_medical_expense_deduction_joint.yaml | 115 ++++++++- .../standard/mt_standard_deduction_joint.yaml | 45 +++- ...test_mt_joint_agi_deductions_properties.py | 230 ++++++++++++++++++ .../mt_medical_expense_deduction_joint.py | 24 +- .../standard/mt_standard_deduction_joint.py | 4 +- 7 files changed, 408 insertions(+), 27 deletions(-) create mode 100644 changelog.d/fix-mt-medical-floor-joint-agi.fixed.md create mode 100644 policyengine_us/parameters/gov/states/mt/tax/income/deductions/itemized/state_specific_deduction_applies.yaml create mode 100644 policyengine_us/tests/unit/test_mt_joint_agi_deductions_properties.py diff --git a/changelog.d/fix-mt-medical-floor-joint-agi.fixed.md b/changelog.d/fix-mt-medical-floor-joint-agi.fixed.md new file mode 100644 index 00000000000..abd7a2db8d4 --- /dev/null +++ b/changelog.d/fix-mt-medical-floor-joint-agi.fixed.md @@ -0,0 +1 @@ +Floor Montana's 2021-2023 medical expense deduction and standard deduction on a joint return's own Montana AGI (Form 2, page 1, line 14), so one spouse's loss offsets the other's income, instead of on the sum of each spouse's AGI floored at zero. From 2024, take the federal medical expense deduction (7.5% of federal AGI), because Form 2 line 2 uses federal itemized deductions; the Montana floor had fallen to zero from 2024, so the full medical expense was deducted. diff --git a/policyengine_us/parameters/gov/states/mt/tax/income/deductions/itemized/state_specific_deduction_applies.yaml b/policyengine_us/parameters/gov/states/mt/tax/income/deductions/itemized/state_specific_deduction_applies.yaml new file mode 100644 index 00000000000..8b6d63360ab --- /dev/null +++ b/policyengine_us/parameters/gov/states/mt/tax/income/deductions/itemized/state_specific_deduction_applies.yaml @@ -0,0 +1,16 @@ +description: Montana computes itemized deductions on its own Itemized Deductions Schedule, in lieu of adjusted federal itemized deductions, if this is true. +metadata: + label: Montana state specific itemized deductions apply + period: year + unit: bool + reference: + - title: Montana Individual Income Tax Return (Form 2) 2023 | Itemized Deductions Schedule + href: https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2023_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7 + - title: 2024 Montana Form 2 Instructions | Discontinued Worksheets and Schedules + href: https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=8 + - title: 2024 Montana Form 2 Instructions | Line 2 and Worksheet A, Adjusted Federal Itemized Deductions + href: https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=15 + +values: + 2021-01-01: true + 2024-01-01: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_expense_deduction_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_expense_deduction_joint.yaml index df9e02675bc..f683598aa05 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_expense_deduction_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_expense_deduction_joint.yaml @@ -1,4 +1,4 @@ -- name: Amount only distributed to the head +- name: Amount only distributed to the head period: 2021 absolute_error_margin: 1 input: @@ -6,16 +6,19 @@ person1: is_tax_unit_head: true other_medical_expenses: 1_000 - mt_agi_indiv: 400 person2: is_tax_unit_head: false other_medical_expenses: 1_000 - mt_agi_indiv: 600 + tax_units: + tax_unit: + members: [person1, person2] + mt_agi_joint: 1_000 households: household: members: [person1, person2] state_code: MT output: + # 2,000 - 7.5% x 1,000 = 1,925 mt_medical_expense_deduction_joint: [1_925, 0] - name: No income @@ -26,11 +29,13 @@ person1: is_tax_unit_head: true other_medical_expenses: 1_000 - mt_agi_indiv: 0 person2: is_tax_unit_head: false other_medical_expenses: 1_000 - mt_agi_indiv: 0 + tax_units: + tax_unit: + members: [person1, person2] + mt_agi_joint: 0 households: household: members: [person1, person2] @@ -46,14 +51,110 @@ person1: is_tax_unit_head: true other_medical_expenses: 0 - mt_agi_indiv: 1_000 person2: is_tax_unit_head: false other_medical_expenses: 0 - mt_agi_indiv: 0 + tax_units: + tax_unit: + members: [person1, person2] + mt_agi_joint: 1_000 households: household: members: [person1, person2] state_code: MT output: mt_medical_expense_deduction_joint: [0, 0] + +- name: Spouse's loss reduces the joint return's Montana AGI before the floor + period: 2022 + absolute_error_margin: 1 + input: + people: + head: + age: 40 + employment_income: 100_000 + other_medical_expenses: 10_000 + spouse: + age: 40 + self_employment_income: -20_000 + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: MT + output: + # Form 2 page 1, line 14: 100,000 - 20,000 = 80,000, reported in one column. + mt_agi_joint: 80_000 + # Itemized Deductions Schedule line 1: 10,000 - 7.5% x 80,000 = 4,000. + # Flooring each spouse's Montana AGI at zero first would give + # 10,000 - 7.5% x 90,000 = 3,250. + mt_medical_expense_deduction_joint: [4_000, 0] + +- name: Last year of the Montana schedule floors on Montana AGI, not the federal deduction + period: 2023 + absolute_error_margin: 1 + input: + people: + person1: + is_tax_unit_head: true + other_medical_expenses: 10_000 + tax_units: + tax_unit: + members: [person1] + mt_agi_joint: 40_000 + medical_expense_deduction: 9_999 + households: + household: + members: [person1] + state_code: MT + output: + # 10,000 - 7.5% x 40,000 = 7,000 + mt_medical_expense_deduction_joint: 7_000 + +- name: From 2024 the federal medical expense deduction carries through + period: 2024 + absolute_error_margin: 1 + input: + people: + person1: + is_tax_unit_head: true + other_medical_expenses: 10_000 + tax_units: + tax_unit: + members: [person1] + mt_agi_joint: 50_000 + medical_expense_deduction: 2_500 + households: + household: + members: [person1] + state_code: MT + output: + mt_medical_expense_deduction_joint: 2_500 + +- name: From 2024 the floor uses federal AGI, not Montana AGI + period: 2024 + absolute_error_margin: 1 + input: + people: + person1: + age: 70 + medicare_enrolled: false + employment_income: 100_000 + other_medical_expenses: 10_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + # The $5,500 subtraction for taxpayers 65 and older reduces Montana AGI + # but not federal AGI. + adjusted_gross_income: 100_000 + mt_agi_joint: 94_500 + # Form 2 line 2, Worksheet A, takes federal Schedule A: + # 10,000 - 7.5% x 100,000 = 2,500. + mt_medical_expense_deduction_joint: 2_500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/standard/mt_standard_deduction_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/standard/mt_standard_deduction_joint.yaml index fc733856faf..68ba1b89870 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/standard/mt_standard_deduction_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/standard/mt_standard_deduction_joint.yaml @@ -2,7 +2,7 @@ period: 2021 input: state_code: MT - mt_agi_indiv: 12_950 + mt_agi_joint: 12_950 filing_status: SINGLE output: mt_standard_deduction_joint: 2_590 @@ -11,7 +11,7 @@ period: 2021 input: state_code: MT - mt_agi_indiv: 8_000 + mt_agi_joint: 8_000 filing_status: SURVIVING_SPOUSE output: mt_standard_deduction_joint: 2_140 @@ -20,7 +20,7 @@ period: 2021 input: state_code: MT - mt_agi_indiv: 30_000 + mt_agi_joint: 30_000 filing_status: SEPARATE output: mt_standard_deduction_joint: 4_830 @@ -29,7 +29,7 @@ period: 2022 input: state_code: MT - mt_agi_indiv: 25_180 + mt_agi_joint: 25_180 filing_status: HEAD_OF_HOUSEHOLD output: mt_standard_deduction_joint: 5_036 @@ -38,7 +38,7 @@ period: 2022 input: state_code: MT - mt_agi_indiv: 15_000 + mt_agi_joint: 15_000 filing_status: JOINT output: mt_standard_deduction_joint: 4_520 @@ -50,17 +50,15 @@ people: person1: is_tax_unit_head: true - mt_agi_indiv: 5_000 person2: is_tax_unit_head: false - mt_agi_indiv: 100_000 person3: is_tax_unit_head: false - mt_agi_indiv: 0 tax_units: tax_unit: members: [person1, person2, person3] filing_status: JOINT + mt_agi_joint: 105_000 households: household: members: [person1, person2, person3] @@ -75,17 +73,15 @@ people: person1: is_tax_unit_head: true - mt_agi_indiv: 20_000 person2: is_tax_unit_head: false - mt_agi_indiv: 10_000 person3: is_tax_unit_head: false - mt_agi_indiv: 0 tax_units: tax_unit: members: [person1, person2, person3] filing_status: JOINT + mt_agi_joint: 30_000 households: household: members: [person1, person2, person3] @@ -97,7 +93,32 @@ period: 2024 input: state_code: MT - mt_agi_indiv: 12_950 + mt_agi_joint: 12_950 filing_status: JOINT output: mt_standard_deduction_joint: 29_200 + +- name: Spouse's loss reduces the joint return's Montana AGI + period: 2022 + absolute_error_margin: 1 + input: + people: + head: + age: 40 + employment_income: 50_000 + spouse: + age: 40 + self_employment_income: -10_000 + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: MT + output: + # Form 2 page 1, line 14: 50,000 - 10,000 = 40,000, reported in one column. + mt_agi_joint: 40_000 + # Standard Deduction Worksheet: 20% x 40,000 = 8,000, between the $4,520 + # minimum and the $10,180 maximum for a joint return. + mt_standard_deduction_joint: [8_000, 0] diff --git a/policyengine_us/tests/unit/test_mt_joint_agi_deductions_properties.py b/policyengine_us/tests/unit/test_mt_joint_agi_deductions_properties.py new file mode 100644 index 00000000000..035f36895bf --- /dev/null +++ b/policyengine_us/tests/unit/test_mt_joint_agi_deductions_properties.py @@ -0,0 +1,230 @@ +"""Properties of Montana's AGI-based deductions on a joint (single-column) return. + +From 2021 to 2023 the Form 2 Itemized Deductions Schedule (line 1b) and the +Standard Deduction Worksheet (line 1) both take Montana AGI from Form 2 page 1, +line 14. Single, head of household and joint returns report that line in one +column, so a joint return pools both spouses' income before Montana AGI is +floored at zero. From 2024 Form 2 line 2 takes federal itemized deductions +(Worksheet A), so the medical floor applies to federal AGI. + +Each test evaluates every point of an input grid in one vectorized simulation, +so the properties hold for all grid inputs rather than for a few examples: + +- 2021-2023: the medical deduction is max(0, expenses - 7.5% of the return's + Montana AGI), and the standard deduction is 20% of that AGI held between the + filing status minimum and maximum; +- moving an income item from one spouse to the other leaves both deductions + unchanged, because they depend on the return's pooled Montana AGI only; +- each deduction is held once, by the head; +- the medical deduction lies between zero and the medical expenses; +- for a single filer the joint and separate variants agree; +- from 2024 the medical deduction is the federal medical expense deduction, + including where Montana AGI differs from federal AGI. +""" + +import itertools + +import numpy as np +import pytest + +from policyengine_us import Simulation + +MEDICAL_FLOOR = 0.075 # Itemized Deductions Schedule line 1c +STANDARD_RATE = 0.2 # Standard Deduction Worksheet line 2 + +HEAD_WAGES = [0, 30_000, 60_000, 100_000] +SPOUSE_SELF_EMPLOYMENT = [-50_000, -20_000, 0, 40_000] +MEDICAL = [0, 2_000, 10_000, 30_000] +# Where the spouse's self-employment income is recorded: on the spouse, or +# moved to the head. The return's income is the same either way. +SPLITS = ["spouse", "head"] +CHILD_MEDICAL = [None, 1_500] # no child, or a dependent child's expenses +COUPLE_GRID = list( + itertools.product( + HEAD_WAGES, SPOUSE_SELF_EMPLOYMENT, MEDICAL, SPLITS, CHILD_MEDICAL + ) +) +SINGLE_GRID = list( + itertools.product([0, 20_000, 100_000], [-10_000, 0], MEDICAL, [40, 70]) +) +STATE_YEARS = [2021, 2022, 2023] +FEDERAL_YEARS = [2024, 2025] +YEARS = STATE_YEARS + FEDERAL_YEARS + + +def every_year(value): + return {year: value for year in YEARS} + + +@pytest.fixture(scope="module") +def grid(): + """One simulation holding every couple and single-filer grid point.""" + people, tax_units, households = {}, {}, {} + unit_of_person, role = [], [] + for i, (wages, se, medical, split, child) in enumerate(COUPLE_GRID): + head, spouse = f"c{i}_head", f"c{i}_spouse" + people[head] = { + "age": every_year(40), + "employment_income": every_year(wages), + "self_employment_income": every_year(se if split == "head" else 0), + "other_medical_expenses": every_year(medical), + } + people[spouse] = { + "age": every_year(40), + "self_employment_income": every_year(se if split == "spouse" else 0), + } + members = [head, spouse] + if child is not None: + name = f"c{i}_child" + people[name] = { + "age": every_year(10), + "is_tax_unit_dependent": every_year(True), + "other_medical_expenses": every_year(child), + } + members.append(name) + unit_of_person += [i] * len(members) + role += ["head", "spouse", "dependent"][: len(members)] + tax_units[f"c{i}"] = {"members": members} + households[f"c{i}"] = {"members": members, "state_code": every_year("MT")} + for j, (wages, se, medical, age) in enumerate(SINGLE_GRID): + name = f"s{j}" + people[name] = { + "age": every_year(age), + "employment_income": every_year(wages), + "self_employment_income": every_year(se), + "other_medical_expenses": every_year(medical), + } + unit_of_person.append(len(COUPLE_GRID) + j) + role.append("head") + tax_units[name] = {"members": [name]} + households[name] = {"members": [name], "state_code": every_year("MT")} + sim = Simulation( + situation={"people": people, "tax_units": tax_units, "households": households} + ) + return sim, np.array(unit_of_person), np.array(role) + + +N_COUPLES = len(COUPLE_GRID) +COUPLES = slice(0, N_COUPLES) +SINGLES = slice(N_COUPLES, None) + + +def per_unit(sim, variable, year, unit): + """Sum a person-level variable to its tax unit, in grid order.""" + return np.bincount(unit, weights=sim.calculate(variable, year)) + + +def montana_agi_reference(sim, year): + # The grid has no Montana additions or subtractions before 2024, so page 1 + # line 14 is the return's federal AGI, floored at zero like PE's. + return np.maximum(sim.calculate("adjusted_gross_income", year), 0) + + +def twin_pairs(): + """Index pairs of couples that differ only in which spouse has the item.""" + index = {point: i for i, point in enumerate(COUPLE_GRID)} + return np.array( + [ + (index[(w, se, m, "spouse", c)], index[(w, se, m, "head", c)]) + for w, se, m, split, c in COUPLE_GRID + if split == "spouse" + ] + ).T + + +@pytest.mark.parametrize("year", STATE_YEARS) +def test_premises(grid, year): + sim, unit, _ = grid + filing_status = sim.calculate("filing_status", year).decode_to_str() + assert set(filing_status[COUPLES]) == {"JOINT"} + assert set(filing_status[SINGLES]) == {"SINGLE"} + np.testing.assert_allclose( + sim.calculate("mt_agi_joint", year), montana_agi_reference(sim, year) + ) + # Moving the item between spouses does not change the return's AGI... + spouse_side, head_side = twin_pairs() + agi = sim.calculate("adjusted_gross_income", year) + np.testing.assert_allclose(agi[spouse_side], agi[head_side]) + # ...but it does change the sum of each spouse's AGI floored at zero, so + # the grid reaches the cases where pooling matters. + summed_individual = per_unit(sim, "mt_agi_indiv", year, unit) + assert np.any(summed_individual[COUPLES] > agi[COUPLES] + 1) + + +@pytest.mark.parametrize("year", STATE_YEARS) +def test_medical_floor_uses_the_return_montana_agi(grid, year): + sim, unit, role = grid + expenses = sim.calculate("itemized_medical_expenses", year) + expected = np.maximum( + expenses - MEDICAL_FLOOR * montana_agi_reference(sim, year), 0 + ) + deduction = per_unit(sim, "mt_medical_expense_deduction_joint", year, unit) + np.testing.assert_allclose(deduction, expected, atol=0.01) + held = sim.calculate("mt_medical_expense_deduction_joint", year) + assert np.all(held[role != "head"] == 0) + assert np.all(deduction >= 0) + assert np.all(deduction <= expenses + 0.01) + spouse_side, head_side = twin_pairs() + np.testing.assert_allclose(deduction[spouse_side], deduction[head_side]) + assert deduction.max() > 0 + # Adding each spouse's Montana AGI after flooring it at zero would change + # the deduction somewhere on the grid. + summed_individual = per_unit(sim, "mt_agi_indiv", year, unit) + unpooled = np.maximum(expenses - MEDICAL_FLOOR * summed_individual, 0) + assert np.any(np.abs(unpooled - expected) > 1) + + +@pytest.mark.parametrize("year", STATE_YEARS) +def test_standard_deduction_uses_the_return_montana_agi(grid, year): + sim, unit, role = grid + p = sim.tax_benefit_system.parameters( + f"{year}-01-01" + ).gov.states.mt.tax.income.deductions.standard + filing_status = sim.calculate("filing_status", year).decode_to_str() + minimum = p.floor[filing_status] + maximum = p.cap[filing_status] + expected = np.clip( + STANDARD_RATE * montana_agi_reference(sim, year), minimum, maximum + ) + deduction = per_unit(sim, "mt_standard_deduction_joint", year, unit) + np.testing.assert_allclose(deduction, expected, atol=0.01) + held = sim.calculate("mt_standard_deduction_joint", year) + assert np.all(held[role != "head"] == 0) + spouse_side, head_side = twin_pairs() + np.testing.assert_allclose(deduction[spouse_side], deduction[head_side]) + # Some returns fall between the minimum and maximum. + assert np.any((deduction > minimum + 1) & (deduction < maximum - 1)) + # Adding each spouse's Montana AGI after flooring it at zero would change + # the deduction somewhere on the grid. + summed_individual = per_unit(sim, "mt_agi_indiv", year, unit) + unpooled = np.clip(STANDARD_RATE * summed_individual, minimum, maximum) + assert np.any(np.abs(unpooled - expected) > 1) + + +@pytest.mark.parametrize("year", STATE_YEARS) +def test_single_filer_joint_and_separate_variants_agree(grid, year): + sim, unit, _ = grid + for joint, separate in [ + ("mt_medical_expense_deduction_joint", "mt_medical_expense_deduction_indiv"), + ("mt_standard_deduction_joint", "mt_standard_deduction_indiv"), + ]: + np.testing.assert_allclose( + per_unit(sim, joint, year, unit)[SINGLES], + per_unit(sim, separate, year, unit)[SINGLES], + atol=0.01, + ) + + +@pytest.mark.parametrize("year", FEDERAL_YEARS) +def test_from_2024_the_federal_medical_deduction_carries_through(grid, year): + sim, unit, role = grid + federal = sim.calculate("medical_expense_deduction", year) + deduction = per_unit(sim, "mt_medical_expense_deduction_joint", year, unit) + np.testing.assert_allclose(deduction, federal, atol=0.01) + held = sim.calculate("mt_medical_expense_deduction_joint", year) + assert np.all(held[role != "head"] == 0) + # The grid includes returns whose Montana AGI differs from federal AGI + # (the subtraction for taxpayers 65 and older) and that deduct expenses. + montana_agi = sim.calculate("mt_agi_joint", year) + federal_agi = np.maximum(sim.calculate("adjusted_gross_income", year), 0) + assert np.any((montana_agi < federal_agi - 1) & (deduction > 0)) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_expense_deduction_joint.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_expense_deduction_joint.py index 28d8b020611..182f474d45d 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_expense_deduction_joint.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_expense_deduction_joint.py @@ -8,17 +8,27 @@ class mt_medical_expense_deduction_joint(Variable): label = "Montana medical expense deduction when married couples are filing jointly" reference = ( "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7", + "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2022_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=32", + "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=15", "https://law.justia.com/codes/montana/2022/title-15/chapter-30/part-21/section-15-30-2131/", - # MT Code § 15-30-2131 (2022) (1)(g)(i) + # MT Code § 15-30-2131 (2022) (1)(a) ) unit = USD defined_for = StateCode.MT def formula(person, period, parameters): - expense = person.tax_unit("itemized_medical_expenses", period) - p = parameters(period).gov.irs.deductions.itemized.medical - # Law does not define Montana AGI as the cap. - # Tax form points to page 1, line 14, which is Montana AGI. - medical_floor = p.floor * add(person.tax_unit, period, ["mt_agi_indiv"]) + p = parameters(period).gov.states.mt.tax.income.deductions.itemized + if p.state_specific_deduction_applies: + expense = person.tax_unit("itemized_medical_expenses", period) + floor = parameters(period).gov.irs.deductions.itemized.medical.floor + # Itemized Deductions Schedule line 1b takes Montana AGI from + # page 1, line 14. A joint return reports that line in a single + # column, so the floor applies to the return's pooled Montana AGI. + montana_agi = person.tax_unit("mt_agi_joint", period) + deduction = max_(0, expense - floor * montana_agi) + else: + # From 2024, Form 2 line 2 takes federal itemized deductions + # (Worksheet A), so the federal floor on federal AGI applies. + deduction = person.tax_unit("medical_expense_deduction", period) is_head = person("is_tax_unit_head", period) - return is_head * max_(0, expense - medical_floor) + return is_head * deduction diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/standard/mt_standard_deduction_joint.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/standard/mt_standard_deduction_joint.py index 9f5279e5caa..c471e6c9ac1 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/standard/mt_standard_deduction_joint.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/standard/mt_standard_deduction_joint.py @@ -17,7 +17,9 @@ def formula(person, period, parameters): if p.state_specific_deduction_applies: # ── Pre-2024 MT-specific calculation ─────────────────────────────── - agi = add(person.tax_unit, period, ["mt_agi_indiv"]) + # Standard Deduction Worksheet line 1 takes Montana AGI from + # page 1, line 14, which pools both spouses on a joint return. + agi = person.tax_unit("mt_agi_joint", period) floor = p.floor[filing_status] cap = p.cap[filing_status] uncapped = p.rate * agi From dc5b0f2bd44485326ba7630e6765ac0e50ca88bc Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 15:02:58 -0400 Subject: [PATCH 2/2] test(mt): cover joint deductions through final tax --- .../joint_agi_deductions_integration.yaml | 406 ++++++++++++++++++ ...test_mt_joint_agi_deductions_properties.py | 186 +++++++- 2 files changed, 585 insertions(+), 7 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/joint_agi_deductions_integration.yaml diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/joint_agi_deductions_integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/joint_agi_deductions_integration.yaml new file mode 100644 index 00000000000..8181f5f68f9 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/joint_agi_deductions_integration.yaml @@ -0,0 +1,406 @@ +# Return-level deduction propagation, with nonmedical deductions and credits zero. +# Sources: 2022 Form 2 p. 1 (exemptions), p. 7 (deductions), p. 8 (tax table): +# https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7 +# https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=1 +# https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=8 +# The marginal tax schedule retains main's cent precision (offset 602.50); +# the printed 2022 tax table rounds its offset to 603. This PR preserves it. +# 2024 instructions p. 15, Worksheet A (adjusted federal deductions): +# https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=15 +# 2024 ordinary tax rates, instructions p. 22 (printed p. 14): +# https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=22 +# 2024 joint federal standard deduction, IRS instructions p. 6: +# https://www.irs.gov/pub/irs-prior/i1040gi--2024.pdf#page=6 +# 2024 federal Schedule A p. 1, lines 1-4 (7.5% medical floor): +# https://www.irs.gov/pub/irs-prior/f1040sa--2024.pdf#page=1 +# 2024 Form 2 p. 2 (ordinary income and capital-gain threshold/rates): +# https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2024_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=2 + +- name: 2022 pooled medical deduction at separate tax selection boundary 2812 + period: 2022 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + adjusted_gross_income_person: 90_000 + other_medical_expenses: 29_980 + mt_withheld_income_tax: 0 + mt_non_refundable_credits: 0 + mt_refundable_credits: 0 + # Controlled alternative tax isolates the filing selection boundary. + # This is not a derived separate-return household tax expectation. + mt_income_tax_indiv: 2812 + spouse: + age: 40 + adjusted_gross_income_person: -10_000 + mt_withheld_income_tax: 0 + mt_non_refundable_credits: 0 + mt_refundable_credits: 0 + mt_income_tax_indiv: 0 + tax_units: + tax_unit: + members: [head, spouse] + adjusted_gross_income: 80_000 + income_tax_before_refundable_credits: 0 + households: + household: + members: [head, spouse] + state_code: MT + output: + mt_agi_joint: 80_000 + # 29,980 - 7.5% * (90,000 - 10,000) = 23,980. + # The old unpooled floor gave 23,230 and joint tax 2,863.625. + mt_medical_expense_deduction_joint: [23_980, 0] + mt_itemized_deductions_joint: [23_980, 0] + mt_itemized_deductions_for_federal_itemization_joint: [23_980, 0] + # max(joint 23,980, separate 29,980 - 7.5% * 90,000) = 23,980. + state_itemized_deductions: 23_980 + # 20% * 80,000 = 16,000, capped at the joint maximum 10,180. + state_standard_deduction: 10_180 + # 80,000 - 23,980 - 2 * 2,710 exemptions = 50,600. + mt_taxable_income_joint: [50_600, 0] + # 734 + 6.75% * (50,600 - 19,800) = 2,813. + mt_regular_income_tax_joint: 2_813 + mt_income_tax_joint: 2_813 + # Separate filing requires strictly lower tax; ties retain joint filing. + mt_files_separately: true + mt_income_tax: 2812 + +- name: 2022 pooled medical deduction at separate tax selection boundary 2813 + period: 2022 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + adjusted_gross_income_person: 90_000 + other_medical_expenses: 29_980 + mt_withheld_income_tax: 0 + mt_non_refundable_credits: 0 + mt_refundable_credits: 0 + # Controlled alternative tax isolates the filing selection boundary. + # This is not a derived separate-return household tax expectation. + mt_income_tax_indiv: 2813 + spouse: + age: 40 + adjusted_gross_income_person: -10_000 + mt_withheld_income_tax: 0 + mt_non_refundable_credits: 0 + mt_refundable_credits: 0 + mt_income_tax_indiv: 0 + tax_units: + tax_unit: + members: [head, spouse] + adjusted_gross_income: 80_000 + income_tax_before_refundable_credits: 0 + households: + household: + members: [head, spouse] + state_code: MT + output: + mt_agi_joint: 80_000 + # 29,980 - 7.5% * (90,000 - 10,000) = 23,980. + # The old unpooled floor gave 23,230 and joint tax 2,863.625. + mt_medical_expense_deduction_joint: [23_980, 0] + mt_itemized_deductions_joint: [23_980, 0] + mt_itemized_deductions_for_federal_itemization_joint: [23_980, 0] + # max(joint 23,980, separate 29,980 - 7.5% * 90,000) = 23,980. + state_itemized_deductions: 23_980 + # 20% * 80,000 = 16,000, capped at the joint maximum 10,180. + state_standard_deduction: 10_180 + # 80,000 - 23,980 - 2 * 2,710 exemptions = 50,600. + mt_taxable_income_joint: [50_600, 0] + # 734 + 6.75% * (50,600 - 19,800) = 2,813. + mt_regular_income_tax_joint: 2_813 + mt_income_tax_joint: 2_813 + # Separate filing requires strictly lower tax; ties retain joint filing. + mt_files_separately: false + mt_income_tax: 2813 + +- name: 2022 pooled medical deduction at separate tax selection boundary 2814 + period: 2022 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + adjusted_gross_income_person: 90_000 + other_medical_expenses: 29_980 + mt_withheld_income_tax: 0 + mt_non_refundable_credits: 0 + mt_refundable_credits: 0 + # Controlled alternative tax isolates the filing selection boundary. + # This is not a derived separate-return household tax expectation. + mt_income_tax_indiv: 2814 + spouse: + age: 40 + adjusted_gross_income_person: -10_000 + mt_withheld_income_tax: 0 + mt_non_refundable_credits: 0 + mt_refundable_credits: 0 + mt_income_tax_indiv: 0 + tax_units: + tax_unit: + members: [head, spouse] + adjusted_gross_income: 80_000 + income_tax_before_refundable_credits: 0 + households: + household: + members: [head, spouse] + state_code: MT + output: + mt_agi_joint: 80_000 + # 29,980 - 7.5% * (90,000 - 10,000) = 23,980. + # The old unpooled floor gave 23,230 and joint tax 2,863.625. + mt_medical_expense_deduction_joint: [23_980, 0] + mt_itemized_deductions_joint: [23_980, 0] + mt_itemized_deductions_for_federal_itemization_joint: [23_980, 0] + # max(joint 23,980, separate 29,980 - 7.5% * 90,000) = 23,980. + state_itemized_deductions: 23_980 + # 20% * 80,000 = 16,000, capped at the joint maximum 10,180. + state_standard_deduction: 10_180 + # 80,000 - 23,980 - 2 * 2,710 exemptions = 50,600. + mt_taxable_income_joint: [50_600, 0] + # 734 + 6.75% * (50,600 - 19,800) = 2,813. + mt_regular_income_tax_joint: 2_813 + mt_income_tax_joint: 2_813 + # Separate filing requires strictly lower tax; ties retain joint filing. + mt_files_separately: false + mt_income_tax: 2813 + +- name: 2022 pooled standard deduction propagates to state deductions and final tax + period: 2022 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 50_000 + mt_withheld_income_tax: 0 + mt_non_refundable_credits: 0 + mt_refundable_credits: 0 + spouse: + age: 40 + self_employment_income: -10_000 + mt_withheld_income_tax: 0 + mt_non_refundable_credits: 0 + mt_refundable_credits: 0 + tax_units: + tax_unit: + members: [head, spouse] + income_tax_before_refundable_credits: 0 + households: + household: + members: [head, spouse] + state_code: MT + output: + adjusted_gross_income: 40_000 + mt_agi_joint: 40_000 + # 20% * (50,000 - 10,000) = 8,000, within [4,520, 10,180]. + mt_standard_deduction_joint: [8_000, 0] + # max(joint 8,000, separate 5,090 + 2,260) = 8,000. + state_standard_deduction: 8_000 + state_itemized_deductions: 0 + # 40,000 - 8,000 - 2 * 2,710 = 26,580. + mt_taxable_income_joint: [26_580, 0] + # 734 + 6.75% * (26,580 - 19,800) = 1,191.65. + mt_income_tax_joint: 1_191.65 + mt_files_separately: false + mt_income_tax: 1_191.65 + +- name: 2024 medical floor propagates through capital gains with wages 70100 and expenses 35000 + period: 2024 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 70100 + long_term_capital_gains: 20_000 + other_medical_expenses: 35000 + mt_withheld_income_tax: 0 + mt_non_refundable_credits: 0 + mt_refundable_credits: 0 + spouse: + age: 40 + mt_withheld_income_tax: 0 + mt_non_refundable_credits: 0 + mt_refundable_credits: 0 + tax_units: + tax_unit: + members: [head, spouse] + income_tax_before_refundable_credits: 0 + households: + household: + members: [head, spouse] + state_code: MT + output: + adjusted_gross_income: 90100 + # 35000 - 7.5% * 90100 = 28242.5 (federal AGI). + medical_expense_deduction: 28242.5 + mt_medical_expense_deduction_joint: [28242.5, 0] + mt_itemized_deductions_joint: [28242.5, 0] + mt_itemized_deductions_for_federal_itemization_joint: [28242.5, 0] + state_itemized_deductions: 28242.5 + state_standard_deduction: 29_200 + # 90100 - max(28242.5, 29,200) = 60900; no exemptions from 2024. + mt_taxable_income_joint: [60900, 0] + # Ordinary income = 60900 - 20,000 = 40900; + # remaining lower-rate room = max(41,000 - 40900, 0) = 100. + mt_capital_gains_tax_applicable_threshold_joint: 100 + # 3% * min(100, 20,000) + 4.1% * (20,000 - min(100, 20,000)). + mt_capital_gains_tax_joint: 818.9 + # 4.7% * min(40900, 41,000) + 5.9% * max(40900 - 41,000, 0). + mt_regular_income_tax_joint: 1922.3 + # 1922.3 + 818.9 = 2741.2. + mt_income_tax_joint: 2741.2 + mt_files_separately: false + mt_income_tax: 2741.2 + +- name: 2024 medical floor propagates through capital gains with wages 70200 and expenses 35000 + period: 2024 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 70200 + long_term_capital_gains: 20_000 + other_medical_expenses: 35000 + mt_withheld_income_tax: 0 + mt_non_refundable_credits: 0 + mt_refundable_credits: 0 + spouse: + age: 40 + mt_withheld_income_tax: 0 + mt_non_refundable_credits: 0 + mt_refundable_credits: 0 + tax_units: + tax_unit: + members: [head, spouse] + income_tax_before_refundable_credits: 0 + households: + household: + members: [head, spouse] + state_code: MT + output: + adjusted_gross_income: 90200 + # 35000 - 7.5% * 90200 = 28235 (federal AGI). + medical_expense_deduction: 28235 + mt_medical_expense_deduction_joint: [28235, 0] + mt_itemized_deductions_joint: [28235, 0] + mt_itemized_deductions_for_federal_itemization_joint: [28235, 0] + state_itemized_deductions: 28235 + state_standard_deduction: 29_200 + # 90200 - max(28235, 29,200) = 61000; no exemptions from 2024. + mt_taxable_income_joint: [61000, 0] + # Ordinary income = 61000 - 20,000 = 41000; + # remaining lower-rate room = max(41,000 - 41000, 0) = 0. + mt_capital_gains_tax_applicable_threshold_joint: 0 + # 3% * min(0, 20,000) + 4.1% * (20,000 - min(0, 20,000)). + mt_capital_gains_tax_joint: 820 + # 4.7% * min(41000, 41,000) + 5.9% * max(41000 - 41,000, 0). + mt_regular_income_tax_joint: 1927 + # 1927 + 820 = 2747. + mt_income_tax_joint: 2747 + mt_files_separately: false + mt_income_tax: 2747 + +- name: 2024 medical floor propagates through capital gains with wages 70300 and expenses 35000 + period: 2024 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 70300 + long_term_capital_gains: 20_000 + other_medical_expenses: 35000 + mt_withheld_income_tax: 0 + mt_non_refundable_credits: 0 + mt_refundable_credits: 0 + spouse: + age: 40 + mt_withheld_income_tax: 0 + mt_non_refundable_credits: 0 + mt_refundable_credits: 0 + tax_units: + tax_unit: + members: [head, spouse] + income_tax_before_refundable_credits: 0 + households: + household: + members: [head, spouse] + state_code: MT + output: + adjusted_gross_income: 90300 + # 35000 - 7.5% * 90300 = 28227.5 (federal AGI). + medical_expense_deduction: 28227.5 + mt_medical_expense_deduction_joint: [28227.5, 0] + mt_itemized_deductions_joint: [28227.5, 0] + mt_itemized_deductions_for_federal_itemization_joint: [28227.5, 0] + state_itemized_deductions: 28227.5 + state_standard_deduction: 29_200 + # 90300 - max(28227.5, 29,200) = 61100; no exemptions from 2024. + mt_taxable_income_joint: [61100, 0] + # Ordinary income = 61100 - 20,000 = 41100; + # remaining lower-rate room = max(41,000 - 41100, 0) = 0. + mt_capital_gains_tax_applicable_threshold_joint: 0 + # 3% * min(0, 20,000) + 4.1% * (20,000 - min(0, 20,000)). + mt_capital_gains_tax_joint: 820 + # 4.7% * min(41100, 41,000) + 5.9% * max(41100 - 41,000, 0). + mt_regular_income_tax_joint: 1932.9 + # 1932.9 + 820 = 2752.9. + mt_income_tax_joint: 2752.9 + mt_files_separately: false + mt_income_tax: 2752.9 + +- name: 2024 medical floor propagates through capital gains with wages 60000 and expenses 40000 + period: 2024 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 60000 + long_term_capital_gains: 20_000 + other_medical_expenses: 40000 + mt_withheld_income_tax: 0 + mt_non_refundable_credits: 0 + mt_refundable_credits: 0 + spouse: + age: 40 + mt_withheld_income_tax: 0 + mt_non_refundable_credits: 0 + mt_refundable_credits: 0 + tax_units: + tax_unit: + members: [head, spouse] + income_tax_before_refundable_credits: 0 + households: + household: + members: [head, spouse] + state_code: MT + output: + adjusted_gross_income: 80000 + # 40000 - 7.5% * 80000 = 34000 (federal AGI). + medical_expense_deduction: 34000 + mt_medical_expense_deduction_joint: [34000, 0] + mt_itemized_deductions_joint: [34000, 0] + mt_itemized_deductions_for_federal_itemization_joint: [34000, 0] + state_itemized_deductions: 34000 + state_standard_deduction: 29_200 + # 80000 - max(34000, 29,200) = 46000; no exemptions from 2024. + mt_taxable_income_joint: [46000, 0] + # Ordinary income = 46000 - 20,000 = 26000; + # remaining lower-rate room = max(41,000 - 26000, 0) = 15000. + mt_capital_gains_tax_applicable_threshold_joint: 15000 + # 3% * min(15000, 20,000) + 4.1% * (20,000 - min(15000, 20,000)). + mt_capital_gains_tax_joint: 655 + # 4.7% * min(26000, 41,000) + 5.9% * max(26000 - 41,000, 0). + mt_regular_income_tax_joint: 1222 + # 1222 + 655 = 1877. + mt_income_tax_joint: 1877 + mt_files_separately: false + mt_income_tax: 1877 diff --git a/policyengine_us/tests/unit/test_mt_joint_agi_deductions_properties.py b/policyengine_us/tests/unit/test_mt_joint_agi_deductions_properties.py index 035f36895bf..aa925d3fb6d 100644 --- a/policyengine_us/tests/unit/test_mt_joint_agi_deductions_properties.py +++ b/policyengine_us/tests/unit/test_mt_joint_agi_deductions_properties.py @@ -3,29 +3,38 @@ From 2021 to 2023 the Form 2 Itemized Deductions Schedule (line 1b) and the Standard Deduction Worksheet (line 1) both take Montana AGI from Form 2 page 1, line 14. Single, head of household and joint returns report that line in one -column, so a joint return pools both spouses' income before Montana AGI is -floored at zero. From 2024 Form 2 line 2 takes federal itemized deductions -(Worksheet A), so the medical floor applies to federal AGI. +column, so a joint return pools both spouses' income. The model retains its +existing convention of flooring that pooled Montana AGI at zero. From 2024 +Form 2 line 2 takes federal itemized deductions (Worksheet A), so the medical +floor applies to federal AGI. -Each test evaluates every point of an input grid in one vectorized simulation, -so the properties hold for all grid inputs rather than for a few examples: +The deterministic tests evaluate every point of an input grid in one vectorized +simulation; Hypothesis adds generated batches that check the same invariants: - 2021-2023: the medical deduction is max(0, expenses - 7.5% of the return's Montana AGI), and the standard deduction is 20% of that AGI held between the filing status minimum and maximum; -- moving an income item from one spouse to the other leaves both deductions - unchanged, because they depend on the return's pooled Montana AGI only; +- moving an income item between spouses preserves both deductions when the + return's applicable AGI and medical expenses are held fixed; - each deduction is held once, by the head; - the medical deduction lies between zero and the medical expenses; - for a single filer the joint and separate variants agree; - from 2024 the medical deduction is the federal medical expense deduction, including where Montana AGI differs from federal AGI. + +Hypothesis also generates arbitrary couples over 2021-2026 to check these +invariants beyond the deterministic grid, with witnesses for losses, dependent +expenses, the standard-deduction interior, and senior Montana subtractions. +Sources: 2022 Form 2, PDF p. 7, and 2024 instructions, PDF p. 15: +https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7 +https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=15 """ import itertools import numpy as np import pytest +from hypothesis import given, settings, strategies as st from policyengine_us import Simulation @@ -228,3 +237,166 @@ def test_from_2024_the_federal_medical_deduction_carries_through(grid, year): montana_agi = sim.calculate("mt_agi_joint", year) federal_agi = np.maximum(sim.calculate("adjusted_gross_income", year), 0) assert np.any((montana_agi < federal_agi - 1) & (deduction > 0)) + + +GENERATED_YEARS = list(range(2021, 2027)) +# Keep wages plus positive self-employment below the 2021 Social Security +# wage base. Moving SE earnings to the wage earner would otherwise change +# the SE-tax deduction, changing federal AGI for a reason unrelated to MT. +GENERATED_COUPLES = st.lists( + st.tuples( + st.integers(min_value=0, max_value=100_000), + st.integers(min_value=-40_000, max_value=40_000), + st.integers(min_value=0, max_value=50_000), + st.one_of(st.none(), st.integers(min_value=0, max_value=10_000)), + st.sampled_from([40, 70]), + st.sampled_from([40, 70]), + ), + min_size=1, + max_size=6, +) + + +def generated_couple_simulation(points): + """Batch arbitrary situations with both placements of each income item.""" + people, tax_units, households = {}, {}, {} + unit_of_person = [] + + def all_years(value): + return {year: value for year in GENERATED_YEARS} + + for point_index, (wages, se, medical, child, head_age, spouse_age) in enumerate( + points + ): + for split_index, split in enumerate(["spouse", "head"]): + unit = 2 * point_index + split_index + head, spouse = f"h{unit}", f"s{unit}" + people[head] = { + # Keep medical expenses fixed when moving income between seniors; + # modeled Medicare premiums/MSP would otherwise change the premise. + "medicare_enrolled": all_years(False), + "age": all_years(head_age), + "employment_income": all_years(wages), + "self_employment_income": all_years(se if split == "head" else 0), + "other_medical_expenses": all_years(medical), + } + people[spouse] = { + "medicare_enrolled": all_years(False), + "age": all_years(spouse_age), + "self_employment_income": all_years(se if split == "spouse" else 0), + } + members = [head, spouse] + if child is not None: + name = f"c{unit}" + people[name] = { + "age": all_years(10), + "is_tax_unit_dependent": all_years(True), + "other_medical_expenses": all_years(child), + } + members.append(name) + unit_of_person.extend([unit] * len(members)) + tax_units[f"t{unit}"] = {"members": members} + households[f"hh{unit}"] = { + "members": members, + "state_code": all_years("MT"), + } + simulation = Simulation( + situation={"people": people, "tax_units": tax_units, "households": households} + ) + return simulation, np.array(unit_of_person) + + +@settings(max_examples=8, deadline=None) +@given(points=GENERATED_COUPLES) +def test_generated_joint_deduction_invariants(points): + # Fixed witnesses in every randomized batch prevent vacuous passes and + # retain the inherited zero-clipping convention. Their pre-2024 medical + # arithmetic is (10,000 + 1,500) - 7.5% * (100,000 - 20,000) = 5,500; + # 10,000 - 7.5% * max(-20,000, 0) = 10,000. The standard witness is + # 20% * (50,000 - 10,000) = 8,000, inside the 2022 joint bounds. + witnesses = [ + (100_000, -20_000, 10_000, 1_500, 70, 40), + (0, -20_000, 10_000, None, 40, 40), + (50_000, -10_000, 0, None, 40, 40), + # The older second adult is the model's head, despite input order. + (0, 0, 0, None, 40, 70), + ] + sim, unit = generated_couple_simulation(points + witnesses) + for year in GENERATED_YEARS: + federal_agi = sim.calculate("adjusted_gross_income", year) + np.testing.assert_allclose(federal_agi[::2], federal_agi[1::2], atol=0.02) + assert set(sim.calculate("filing_status", year).decode_to_str()) == {"JOINT"} + # Head selection follows the oldest eligible adult, not input order. + is_head = sim.calculate("is_tax_unit_head", year) + assert np.all(np.bincount(unit, weights=is_head) == 1) + # There is no Social Security income in these situations. The + # signed federal person amounts, state additions, and subtractions + # are therefore pooled before applying the existing zero floor. + pooled_agi = np.maximum( + per_unit(sim, "adjusted_gross_income_person", year, unit) + + per_unit(sim, "mt_additions", year, unit) + - per_unit(sim, "mt_subtractions", year, unit), + 0, + ) + montana_agi = sim.calculate("mt_agi_joint", year) + np.testing.assert_allclose(montana_agi, pooled_agi, atol=0.02) + np.testing.assert_allclose(montana_agi[::2], montana_agi[1::2], atol=0.02) + + expenses = sim.calculate("itemized_medical_expenses", year) + # Dependent medical expenses are part of the return's expenses. + # Medicare premiums are disabled to hold expenses fixed across twins. + np.testing.assert_allclose( + expenses, per_unit(sim, "other_medical_expenses", year, unit), atol=0.02 + ) + np.testing.assert_allclose(expenses[::2], expenses[1::2], atol=0.02) + medical = per_unit(sim, "mt_medical_expense_deduction_joint", year, unit) + standard = per_unit(sim, "mt_standard_deduction_joint", year, unit) + for variable, values in [ + ("mt_medical_expense_deduction_joint", medical), + ("mt_standard_deduction_joint", standard), + ]: + # Exactly the head carries the amount; no spouse or dependent + # can duplicate the return's deduction. + assert np.all(sim.calculate(variable, year)[~is_head] == 0) + np.testing.assert_allclose(values[::2], values[1::2], atol=0.02) + assert np.all(medical >= 0) + assert np.all(medical <= expenses + 0.02) + + if year < 2024: + np.testing.assert_allclose( + montana_agi, np.maximum(federal_agi, 0), atol=0.02 + ) + np.testing.assert_allclose( + medical, + np.maximum(expenses - MEDICAL_FLOOR * montana_agi, 0), + atol=0.02, + ) + p = sim.tax_benefit_system.parameters( + f"{year}-01-01" + ).gov.states.mt.tax.income.deductions.standard + filing_status = sim.calculate("filing_status", year).decode_to_str() + minimum, maximum = p.floor[filing_status], p.cap[filing_status] + np.testing.assert_allclose( + standard, + np.clip(STANDARD_RATE * montana_agi, minimum, maximum), + atol=0.02, + ) + assert np.all(standard >= minimum - 0.02) + assert np.all(standard <= maximum + 0.02) + summed_individual = per_unit(sim, "mt_agi_indiv", year, unit) + assert np.any(summed_individual > montana_agi + 1) + else: + np.testing.assert_allclose( + medical, sim.calculate("medical_expense_deduction", year), atol=0.02 + ) + np.testing.assert_allclose( + medical, + np.maximum(expenses - MEDICAL_FLOOR * np.maximum(federal_agi, 0), 0), + atol=0.02, + ) + np.testing.assert_allclose( + standard, sim.calculate("standard_deduction", year), atol=0.02 + ) + # Every batch includes a senior with positive expenses and + # enough income for the MT subtraction to change the AGI. + assert np.any((montana_agi < federal_agi - 1) & (medical > 0))