diff --git a/changelog.d/fix-mt-taxsim-state-agi.fixed.md b/changelog.d/fix-mt-taxsim-state-agi.fixed.md new file mode 100644 index 00000000000..949470eb3ca --- /dev/null +++ b/changelog.d/fix-mt-taxsim-state-agi.fixed.md @@ -0,0 +1 @@ +Report Montana's state adjusted gross income (taxsim_state_agi) as the Montana AGI of the filing path used through 2023, pooled on joint returns, and federal AGI from 2024, instead of 0 from 2024. diff --git a/policyengine_us/parameters/gov/states/household/state_agis.yaml b/policyengine_us/parameters/gov/states/household/state_agis.yaml index fec49cd3ba6..f5bd5f4c837 100644 --- a/policyengine_us/parameters/gov/states/household/state_agis.yaml +++ b/policyengine_us/parameters/gov/states/household/state_agis.yaml @@ -25,7 +25,7 @@ values: # Minnesota adopts federal AGI - mo_adjusted_gross_income # Missouri - ms_agi # Mississippi - - mt_agi_indiv # Montana + - mt_agi # Montana; federal AGI from 2024 (states_using_federal_agi) # North Carolina adopts federal AGI # North Dakota adopts federal AGI - ne_agi # Nebraska diff --git a/policyengine_us/parameters/gov/states/household/states_using_federal_agi.yaml b/policyengine_us/parameters/gov/states/household/states_using_federal_agi.yaml new file mode 100644 index 00000000000..c78e5797e10 --- /dev/null +++ b/policyengine_us/parameters/gov/states/household/states_using_federal_agi.yaml @@ -0,0 +1,35 @@ +description: List of USPS state codes whose state adjusted gross income, as reported in TAXSIM's state AGI output, is federal adjusted gross income. +metadata: + unit: list + period: year + label: States reporting federal AGI as state AGI + reference: + - title: 2022 Montana Form 2, page 1, line 14 - Montana adjusted gross income (federal adjusted gross income plus Montana additions minus Montana subtractions) + href: https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=1 + - title: 2024 Montana Form 2 Instructions, page i - Montana taxable income is now calculated from federal taxable income + 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=5 + - title: 2024 Montana Form 2 Instructions, page 7 - Line 1, federal adjusted gross income + 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: + # Carry forward the seven states from taxsim_state_agi's former + # FEDERAL_AGI_STATES constant, based on the comments in state_agis.yaml. + # The references above support Montana's addition from 2024. + 0000-01-01: + - CO + - MI + - MN + - NC + - ND + - NM + - SC + # From 2024 Montana Form 2 starts from federal adjusted gross income (line 1) + # and federal taxable income, and has no Montana adjusted gross income line. + 2024-01-01: + - CO + - MI + - MN + - MT + - NC + - ND + - NM + - SC diff --git a/policyengine_us/tests/core/test_mt_state_agi_invariants.py b/policyengine_us/tests/core/test_mt_state_agi_invariants.py new file mode 100644 index 00000000000..330b4e02af5 --- /dev/null +++ b/policyengine_us/tests/core/test_mt_state_agi_invariants.py @@ -0,0 +1,270 @@ +"""Invariants of Montana's state adjusted gross income as TAXSIM reports it. + +Montana Form 2 for 2021-2023 reports Montana adjusted gross income on page 1, +line 14: federal adjusted gross income (line 11) plus Montana additions +(line 12) minus Montana subtractions (line 13). A joint return fills one +column for the couple; under filing status 2a (married filing separately on +the same form) each spouse fills their own column. From 2024 Form 2 starts +from federal adjusted gross income (line 1) and federal taxable income, and +has no Montana adjusted gross income line, so the state AGI that TAXSIM +reports is federal adjusted gross income. TAXSIM (taxsimtest build 2025Aug23) +reports these values rounded to whole dollars: 80,000 for a 2022 couple +with 100,000 of wages and a 20,000 Schedule C loss, 121,200 (Montana AGI) +for a 2023 filer aged 70, and +100,000 (federal AGI) for a 2024 filer aged 70 whose Montana taxable income +nets a 5,500 subtraction. + +A seeded grid of single filers and couples (ages 40 and 70, wages, +self-employment gains and losses, pensions, Social Security and interest) +runs as one simulation per year from 2021 to 2027, and Hypothesis draws +further batches. For every tax unit: + +1. Filing path: `mt_agi` is the sum of each spouse's `mt_agi_indiv` when the + couple files separately on the same form, and `mt_agi_joint` otherwise. +2. Reporting: `taxsim_state_agi`, and its alias `state_agi`, equal `mt_agi` + through 2023 and federal adjusted gross income from 2024. +3. Pooling: through 2023, a unit on the joint path with non-negative federal + AGI, no Montana additions or subtractions and no Social Security reports + federal AGI, so one spouse's loss offsets the other spouse's income. +4. Bounds: `mt_agi` is never negative, and a unit outside Montana has no + `mt_agi`. + +The non-negative bound is inherited from PE's existing `mt_agi_joint` and +`mt_agi_indiv`, not a TAXSIM requirement. An independent review probe for a +2023 single filer aged 40 with a 20,000 Schedule C loss and no other income +returned federal AGI -20,000 and TAXSIM v32 -19,999.99; PE's Montana AGI is +zero. Property 3 excludes negative federal AGI rather than endorsing that +existing discrepancy. Properties 1, 2 and 4 still exercise those units. + +Guards check that the grid reaches both filing paths, a joint-path couple +whose pooled AGI differs from the sum of the spouses' floored AGIs, and, from +2024, a filer whose Montana base differs from federal AGI, so properties 1-3 +are never vacuous. +""" + +from itertools import product + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +TOLERANCE = 0.01 # dollars +YEARS = range(2021, 2028) +# Restated from Form 2, independently of +# gov.states.household.states_using_federal_agi. +FIRST_FEDERAL_AGI_YEAR = 2024 + +PERSON_INPUTS = ( + "age", + "employment_income", + "self_employment_income", + "taxable_private_pension_income", + "social_security_retirement", + "taxable_interest_income", +) +TAX_UNIT_OUTPUTS = ( + "mt_agi", + "mt_agi_joint", + "mt_files_separately", + "taxsim_state_agi", + "state_agi", + "adjusted_gross_income", +) +PERSON_OUTPUTS = ( + "mt_agi_indiv", + "mt_additions", + "mt_subtractions", + "social_security", +) + + +def _person(age, wages=0, self_employment=0, pension=0, social_security=0, interest=0): + return dict( + age=age, + employment_income=float(wages), + self_employment_income=float(self_employment), + taxable_private_pension_income=float(pension), + social_security_retirement=float(social_security), + taxable_interest_income=float(interest), + ) + + +def _seeded_units(): + """Single filers and couples in Montana, plus one Idaho couple.""" + units = [] + for age, head_wages, spouse in product( + (40, 70), + (0, 30_000, 100_000), + (None, (0, -20_000), (0, 15_000), (60_000, 0), (60_000, -20_000)), + ): + # Retirees also get the income Montana adjusts: pensions, Social + # Security and interest. + retiree = dict(pension=10_000, social_security=12_000, interest=3_000) + extra = retiree if age == 70 else {} + head = _person(age, wages=head_wages, **extra) + if spouse is None: + units.append(("MT", head, None)) + continue + spouse_wages, spouse_self_employment = spouse + units.append( + ( + "MT", + head, + _person(age, spouse_wages, spouse_self_employment, **extra), + ) + ) + units.append(("ID", _person(40, 100_000), _person(40, 0, -20_000))) + return units + + +def _situation(units, year): + period = str(year) + people, tax_units, families, spm_units, marital_units, households = ( + {}, + {}, + {}, + {}, + {}, + {}, + ) + for i, (state, head, spouse) in enumerate(units): + members = [] + for role, inputs in (("head", head), ("spouse", spouse)): + if inputs is None: + continue + name = f"{role}_{i}" + people[name] = {k: {period: inputs[k]} for k in PERSON_INPUTS} + members.append(name) + tax_units[f"tax_unit_{i}"] = {"members": members} + families[f"family_{i}"] = {"members": members} + spm_units[f"spm_unit_{i}"] = {"members": members} + marital_units[f"marital_unit_{i}"] = {"members": members} + households[f"household_{i}"] = { + "members": members, + "state_code": {period: state}, + } + return dict( + people=people, + tax_units=tax_units, + families=families, + spm_units=spm_units, + marital_units=marital_units, + households=households, + ) + + +def _run(units, year): + simulation = Simulation(situation=_situation(units, year)) + run = {name: simulation.calculate(name, year) for name in TAX_UNIT_OUTPUTS} + for name in PERSON_OUTPUTS: + run[name] = simulation.map_result( + simulation.calculate(name, year), "person", "tax_unit" + ) + run["in_mt"] = np.array([state == "MT" for state, _, _ in units]) + return run + + +def _check(units, year): + """Assert properties 1-4 for every unit and return the run.""" + run = _run(units, year) + in_mt = run["in_mt"] + separate = run["mt_files_separately"] + mt_agi = run["mt_agi"] + + # 1. Filing path. + expected_mt_agi = np.where(separate, run["mt_agi_indiv"], run["mt_agi_joint"]) + np.testing.assert_allclose(mt_agi[in_mt], expected_mt_agi[in_mt], atol=TOLERANCE) + if year >= FIRST_FEDERAL_AGI_YEAR: + assert not separate.any() + assert (run["mt_agi_indiv"] == 0).all() + + # 2. Reporting. + reported = run["taxsim_state_agi"] + if year >= FIRST_FEDERAL_AGI_YEAR: + expected_reported = run["adjusted_gross_income"] + else: + expected_reported = mt_agi + np.testing.assert_allclose( + reported[in_mt], expected_reported[in_mt], atol=TOLERANCE + ) + np.testing.assert_allclose(run["state_agi"], reported, atol=TOLERANCE) + + # 3. Pooling. + unadjusted = ( + (run["mt_additions"] == 0) + & (run["mt_subtractions"] == 0) + & (run["social_security"] == 0) + ) + pooled = in_mt & ~separate & unadjusted & (run["adjusted_gross_income"] >= 0) + if year < FIRST_FEDERAL_AGI_YEAR: + np.testing.assert_allclose( + reported[pooled], + run["adjusted_gross_income"][pooled], + atol=TOLERANCE, + ) + + # 4. Inherited PE bounds, including negative federal AGI. + assert (mt_agi >= 0).all() + assert (mt_agi[~in_mt] == 0).all() + return run + + +@pytest.mark.parametrize("year", YEARS) +def test_seeded_grid(year): + run = _check(_seeded_units(), year) + in_mt = run["in_mt"] + separate = run["mt_files_separately"] + assert (in_mt & (run["adjusted_gross_income"] < 0)).any() + if year < FIRST_FEDERAL_AGI_YEAR: + # Both filing paths are reached. + assert (in_mt & separate).any() + assert (in_mt & ~separate).any() + # Pooling matters for some joint-path couple: summing each spouse's + # floored Montana AGI, as taxsim_state_agi used to, overstates it. + floored_sum = run["mt_agi_indiv"] + joint_path = ( + in_mt + & ~separate + & (run["adjusted_gross_income"] >= 0) + & (run["mt_additions"] == 0) + & (run["mt_subtractions"] == 0) + & (run["social_security"] == 0) + ) + assert (floored_sum[joint_path] > run["mt_agi"][joint_path] + 1).any() + else: + # Federal AGI is not just the Montana base under another name. + assert ( + np.abs(run["mt_agi_joint"] - run["adjusted_gross_income"])[in_mt] > 1 + ).any() + + +amount = st.integers(0, 150_000) +gain_or_loss = st.integers(-40_000, 60_000) +people = st.builds( + _person, + age=st.sampled_from([25, 45, 64, 65, 80]), + wages=amount, + self_employment=gain_or_loss, + pension=st.sampled_from([0, 12_000]), + social_security=st.sampled_from([0, 18_000]), + interest=st.sampled_from([0, 2_500]), +) +units = st.tuples(st.sampled_from(["MT", "MT", "MT", "ID"]), people, st.none() | people) + +# A batch's cost is mostly per-variable overhead, so each example is a large +# batch and there are few examples. +SETTINGS = dict( + max_examples=5, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + + +@settings(**SETTINGS) +@given(st.lists(units, min_size=10, max_size=30), st.sampled_from(list(YEARS))) +def test_drawn_batches(batch, year): + _check(batch, year) diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/mt_agi.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/mt_agi.yaml new file mode 100644 index 00000000000..1dbe1b3f827 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/mt_agi.yaml @@ -0,0 +1,100 @@ +# Montana adjusted gross income of the filing path used (Form 2, page 1, +# line 14 for 2021-2023): the joint Montana AGI, or the sum of each spouse's +# Montana AGI when the couple files separately on the same form. + +- name: Separate filing on the same form adds each spouse's Montana AGI + period: 2022 + input: + people: + head: + mt_agi_indiv: 60_000 + spouse: + mt_agi_indiv: 30_000 + tax_units: + tax_unit: + members: [head, spouse] + mt_agi_joint: 85_000 + mt_files_separately: true + households: + household: + members: [head, spouse] + state_code: MT + output: + mt_agi: 90_000 + +- name: Joint filing takes the joint Montana AGI + period: 2022 + input: + people: + head: + mt_agi_indiv: 60_000 + spouse: + mt_agi_indiv: 30_000 + tax_units: + tax_unit: + members: [head, spouse] + mt_agi_joint: 85_000 + mt_files_separately: false + households: + household: + members: [head, spouse] + state_code: MT + output: + mt_agi: 85_000 + +- name: Joint return pools a spouse's loss with the other spouse's wages + period: 2022 + input: + people: + head: + age: 40 + employment_income: 100_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: + mt_files_separately: false + mt_agi: 80_000 + +- name: From 2024 every return takes the joint Montana base + period: 2024 + input: + people: + head: + age: 70 + employment_income: 100_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: MT + output: + mt_files_separately: false + mt_agi_joint: 94_500 + mt_agi: 94_500 + +- name: Zero outside Montana + period: 2022 + input: + people: + head: + age: 40 + employment_income: 100_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: ID + output: + mt_agi: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/tax/income/taxsim_state_agi.yaml b/policyengine_us/tests/policy/baseline/gov/states/tax/income/taxsim_state_agi.yaml new file mode 100644 index 00000000000..1cbbf97b167 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/tax/income/taxsim_state_agi.yaml @@ -0,0 +1,194 @@ +# Montana. Through 2023 the state AGI is Form 2, page 1, line 14 (Montana +# adjusted gross income) of the filing path used: one column on a joint +# return, a column per spouse under status 2a. From 2024 Form 2 starts from +# federal adjusted gross income (line 1) and has no Montana AGI line, so the +# state AGI is federal AGI. Where noted, the expected value is what TAXSIM +# (taxsimtest build 2025Aug23) reports as state AGI (v32) for the same inputs, +# rounded to whole dollars (the raw probe values include a one-cent offset). + +- name: Montana 2022 joint return pools a spouse's loss with the other spouse's wages + # TAXSIM v32: 80,000. + period: 2022 + input: + people: + head: + age: 40 + employment_income: 100_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: + mt_files_separately: false + mt_agi_joint: 80_000 + taxsim_state_agi: 80_000 + state_agi: 80_000 + +- name: Montana 2023 joint return pools a spouse's loss with the other spouse's wages + # TAXSIM v32: 80,000. + period: 2023 + input: + people: + head: + age: 40 + employment_income: 100_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: + mt_files_separately: false + taxsim_state_agi: 80_000 + +- name: Montana 2023 state AGI is Montana AGI, after the senior interest exemption + # TAXSIM v32: 121,200. Federal AGI is 122,000: wages 70,000, pension + # 30,000, interest 5,000 and 17,000 of the 20,000 of Social Security. + period: 2023 + input: + people: + head: + age: 70 + employment_income: 70_000 + taxable_private_pension_income: 30_000 + social_security_retirement: 20_000 + taxable_interest_income: 5_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: MT + output: + adjusted_gross_income: 122_000 + taxsim_state_agi: 121_200 + +- name: Montana 2022 separate filing on the same form adds each spouse's Montana AGI + period: 2022 + input: + people: + head: + mt_agi_indiv: 60_000 + spouse: + mt_agi_indiv: 30_000 + tax_units: + tax_unit: + members: [head, spouse] + mt_agi_joint: 85_000 + mt_files_separately: true + households: + household: + members: [head, spouse] + state_code: MT + output: + taxsim_state_agi: 90_000 + +- name: Montana 2022 joint filing reports the joint Montana AGI + period: 2022 + input: + people: + head: + mt_agi_indiv: 60_000 + spouse: + mt_agi_indiv: 30_000 + tax_units: + tax_unit: + members: [head, spouse] + mt_agi_joint: 85_000 + mt_files_separately: false + households: + household: + members: [head, spouse] + state_code: MT + output: + taxsim_state_agi: 85_000 + +- name: Montana 2024 state AGI is federal AGI + # TAXSIM v32: 100,000. + period: 2024 + input: + people: + head: + age: 40 + employment_income: 100_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: MT + output: + taxsim_state_agi: 100_000 + state_agi: 100_000 + +- name: Montana 2024 state AGI is federal AGI, not federal AGI less the senior subtraction + # TAXSIM v32: 100,000, while TAXSIM's Montana taxable income (77,950) nets + # the 5,500 subtraction for a taxpayer aged 65 or older. + period: 2024 + input: + people: + head: + age: 70 + employment_income: 100_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: MT + output: + adjusted_gross_income: 100_000 + mt_agi: 94_500 + taxsim_state_agi: 100_000 + +- name: Montana 2024 joint return reports the couple's federal AGI + # TAXSIM v32: 80,000. + period: 2024 + input: + people: + head: + age: 40 + employment_income: 100_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: + taxsim_state_agi: 80_000 + +- name: Montana 2026 state AGI is federal AGI + period: 2026 + input: + people: + head: + age: 40 + employment_income: 100_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: MT + output: + taxsim_state_agi: 100_000 diff --git a/policyengine_us/tests/policy/baseline/gov/tax/income/test_legacy_state_umbrellas.py b/policyengine_us/tests/policy/baseline/gov/tax/income/test_legacy_state_umbrellas.py index aecab18704f..f6ece880e00 100644 --- a/policyengine_us/tests/policy/baseline/gov/tax/income/test_legacy_state_umbrellas.py +++ b/policyengine_us/tests/policy/baseline/gov/tax/income/test_legacy_state_umbrellas.py @@ -82,8 +82,7 @@ def make_tax_unit_situation( } -def calculate_sum(situation: dict, year: int, variables: list[str]) -> float: - simulation = Simulation(situation=situation) +def calculate_sum(simulation: Simulation, year: int, variables: list[str]) -> float: total = 0.0 for variable in variables: @@ -106,6 +105,11 @@ def configured_component_vars(parameter_name: str, year: int) -> list[str]: ["ok_agi"], make_tax_unit_situation(year=2023, state="OK", wages=40_000.0), ), + ( + "taxsim_state_agi", + ["mt_agi"], + make_tax_unit_situation(year=2023, state="MT", wages=40_000.0), + ), ( "taxsim_state_taxable_income", ["ok_taxable_income"], @@ -141,8 +145,37 @@ def configured_component_vars(parameter_name: str, year: int) -> list[str]: def test_legacy_state_umbrellas_match_state_specific_components( legacy_var, state_specific_vars, situation ): - expected = calculate_sum(situation, 2023, state_specific_vars) - actual = calculate_sum(situation, 2023, [legacy_var]) + simulation = Simulation(situation=situation) + expected = calculate_sum(simulation, 2023, state_specific_vars) + actual = calculate_sum(simulation, 2023, [legacy_var]) + assert actual == pytest.approx(expected, abs=0.01) + + +@pytest.mark.parametrize("year", [2021, 2022, 2023, 2024, 2025, 2026]) +def test_mt_state_agi_is_montana_agi_then_federal_agi(year): + # Through 2023 Montana's state AGI is Form 2 line 14, Montana AGI. From + # 2024 Form 2 starts from federal AGI (line 1), so Montana joins the + # states that report federal AGI. A filer aged 70 has a Montana + # subtraction from 2024, so the two measures differ. + federal_agi_states = configured_component_vars("states_using_federal_agi", year) + assert ("MT" in federal_agi_states) == (year >= 2024) + state_agis = configured_component_vars("state_agis", year) + assert "mt_agi" in state_agis + assert "mt_agi_indiv" not in state_agis + situation = make_tax_unit_situation( + year=year, state="MT", primary_age=70, wages=40_000.0 + ) + simulation = Simulation(situation=situation) + if year >= 2024: + expected_vars = ["adjusted_gross_income"] + assert calculate_sum(simulation, year, ["mt_agi"]) < calculate_sum( + simulation, year, expected_vars + ) + else: + expected_vars = ["mt_agi"] + expected = calculate_sum(simulation, year, expected_vars) + assert expected > 0 + actual = calculate_sum(simulation, year, ["taxsim_state_agi"]) assert actual == pytest.approx(expected, abs=0.01) @@ -154,10 +187,11 @@ def test_state_ctc_matches_configured_component_sum(): wages=20_050.0, dependent_ages=(9, 7), ) + simulation = Simulation(situation=situation) expected = calculate_sum( - situation, 2023, configured_component_vars("state_ctcs", 2023) + simulation, 2023, configured_component_vars("state_ctcs", 2023) ) - actual = calculate_sum(situation, 2023, ["taxsim_state_ctc"]) + actual = calculate_sum(simulation, 2023, ["taxsim_state_ctc"]) assert expected > 0 assert actual == pytest.approx(expected, abs=0.01) @@ -173,12 +207,15 @@ def test_mn_combined_credit_is_state_eitc_not_state_ctc(year): wages=20_050.0, dependent_ages=(9, 7), ) - combined = calculate_sum(situation, year, ["mn_child_and_working_families_credits"]) + simulation = Simulation(situation=situation) + combined = calculate_sum( + simulation, year, ["mn_child_and_working_families_credits"] + ) assert combined > 0 - assert calculate_sum(situation, year, ["taxsim_state_eitc"]) == pytest.approx( + assert calculate_sum(simulation, year, ["taxsim_state_eitc"]) == pytest.approx( combined, abs=0.01 ) - assert calculate_sum(situation, year, ["taxsim_state_ctc"]) == 0 + assert calculate_sum(simulation, year, ["taxsim_state_ctc"]) == 0 def test_state_property_tax_credit_matches_configured_component_sum(): @@ -189,12 +226,13 @@ def test_state_property_tax_credit_matches_configured_component_sum(): wages=10_000.0, rent=12_000.0, ) + simulation = Simulation(situation=situation) expected = calculate_sum( - situation, + simulation, 2023, configured_component_vars("state_property_tax_credits", 2023), ) - actual = calculate_sum(situation, 2023, ["taxsim_state_property_tax_credit"]) + actual = calculate_sum(simulation, 2023, ["taxsim_state_property_tax_credit"]) assert actual == pytest.approx(expected, abs=0.01) @@ -220,6 +258,7 @@ def test_state_property_tax_credit_matches_configured_component_sum(): def test_decomposed_credit_components_preserve_combined_credit_total( component_vars, combined_var, situation ): - combined_total = calculate_sum(situation, 2023, [combined_var]) - decomposed_total = calculate_sum(situation, 2023, component_vars) + simulation = Simulation(situation=situation) + combined_total = calculate_sum(simulation, 2023, [combined_var]) + decomposed_total = calculate_sum(simulation, 2023, component_vars) assert decomposed_total == pytest.approx(combined_total, abs=0.01) diff --git a/policyengine_us/variables/gov/states/tax/income/_generate_state_mfs_variables.py b/policyengine_us/variables/gov/states/tax/income/_generate_state_mfs_variables.py index 5689a485855..1f5d0c83ce5 100644 --- a/policyengine_us/variables/gov/states/tax/income/_generate_state_mfs_variables.py +++ b/policyengine_us/variables/gov/states/tax/income/_generate_state_mfs_variables.py @@ -19,7 +19,7 @@ # Skip variables that already exist or aren't needed if ( # Skip existing AGI variables - (state_code in ["ky", "ms", "mt"] and var_code == "agi") + (state_code in ["ky", "ms"] and var_code == "agi") or # Skip all DC variables except taxable income (state_code == "dc" and var_code != "taxable_income") @@ -119,6 +119,9 @@ "mt_taxable_income_indiv", "mt_taxable_income_joint", ), + # 2021-2023 Form 2 line 14: one column on a joint return, + # a column per spouse under status 2a. + "agi": ("mt_agi_indiv", "mt_agi_joint"), }, }[state_code][var_code] diff --git a/policyengine_us/variables/gov/states/tax/income/taxsim_state_agi.py b/policyengine_us/variables/gov/states/tax/income/taxsim_state_agi.py index dcd31c2df2d..9c9d46d866c 100644 --- a/policyengine_us/variables/gov/states/tax/income/taxsim_state_agi.py +++ b/policyengine_us/variables/gov/states/tax/income/taxsim_state_agi.py @@ -5,21 +5,21 @@ class taxsim_state_agi(Variable): value_type = float entity = TaxUnit label = "State adjusted gross income" + documentation = ( + "State adjusted gross income as TAXSIM reports it in its state AGI " + "output. States listed in gov.states.household.states_using_federal_agi " + "report federal adjusted gross income; other states report the state " + "AGI variables in gov.states.household.state_agis, except Arkansas " + "and Delaware, which report the larger of their separate and joint AGIs." + ) unit = USD definition_period = YEAR def formula(tax_unit, period, parameters): - # States that adopt the federal AGI - # Based on comments in state_agis.yaml - FEDERAL_AGI_STATES = [ - "CO", # Colorado - "MI", # Michigan - "MN", # Minnesota - "NC", # North Carolina - "ND", # North Dakota - "NM", # New Mexico - "SC", # South Carolina - ] + # States that report federal AGI as state AGI + federal_agi_states = parameters( + period + ).gov.states.household.states_using_federal_agi # Get the current state state_code = tax_unit.household("state_code_str", period) @@ -47,7 +47,7 @@ def formula(tax_unit, period, parameters): state_specific_base = where(is_state, max_agi, state_specific_base) # Check if the state adopts federal AGI - uses_federal = np.isin(state_code, FEDERAL_AGI_STATES) + uses_federal = np.isin(state_code, federal_agi_states) # Get federal AGI federal_agi = tax_unit("adjusted_gross_income", period)