diff --git a/changelog.d/fix-mt-separate-medical-dependents.fixed.md b/changelog.d/fix-mt-separate-medical-dependents.fixed.md new file mode 100644 index 00000000000..736e9e0b6ff --- /dev/null +++ b/changelog.d/fix-mt-separate-medical-dependents.fixed.md @@ -0,0 +1 @@ +Count dependents' other medical expenses in Montana's 2021-2023 separate-filing medical deduction, allocating them between spouses filing separately on the same form to minimize the couple's tax; an unmarried filer's column holds all of them. The separate path now chooses between itemizing and the standard deduction by the couple's tax rather than by summed deductions. diff --git a/policyengine_us/parameters/gov/states/mt/tax/income/deductions/itemized/spouse_allocation_rate.yaml b/policyengine_us/parameters/gov/states/mt/tax/income/deductions/itemized/spouse_allocation_rate.yaml index 024fe7224bb..692621782b2 100644 --- a/policyengine_us/parameters/gov/states/mt/tax/income/deductions/itemized/spouse_allocation_rate.yaml +++ b/policyengine_us/parameters/gov/states/mt/tax/income/deductions/itemized/spouse_allocation_rate.yaml @@ -1,13 +1,30 @@ -description: Share of tax unit itemized deductions allocated to each spouse when married filing separately on the same form. This is not a policy parameter but a mathematical representation of "divided equally". +description: Montana itemized deductions paid jointly are split by this share between the spouses' columns when married couples file separately on the same form; this is a modeling convention, not a rate in law. values: - # 0.5 means each spouse gets 50% of joint deductions + # A modeling convention, not a rate in the law. On the same form (status + # 2a), ARM 42.15.524(3)(b) defers to the form, which lets the couple + # allocate payments from accounts both spouses own "in any proportional + # amount"; an equal division is required only on separate forms, absent a + # written agreement (ARM 42.15.524(3)(c)). Readers: + # - mt_non_medical_itemized_deductions_indiv and + # mt_itemized_deductions_for_federal_itemization_indiv split the + # charitable deduction equally by this rate; + # - mt_dependent_medical_expense_head_share uses it as the tie-break and + # starting candidate for the tax-minimizing split of dependents' medical + # bills; + # - mt_child_dependent_care_expense_deduction gives each spouse this share + # of the deduction. There the equal split is the law's rule, not a + # convention: each spouse enters one-half of Form 2441-M line 7. + # The federal rule for separate returns also treats payments from a joint + # account as paid equally. 2021-01-01: 0.5 metadata: label: Montana itemized deductions spouse allocation rate period: year unit: /1 reference: - - title: Montana Administrative Rule 42.15.524 - Itemized Deductions of Married Taxpayers - href: https://rules.mt.gov/gateway/ruleno.asp?RN=42.15.524 - - title: Montana Form 2 Instructions - href: https://revenue.mt.gov/publications/montana-form-2-individual-income-tax-return-forms-and-instructions-includes-form-2ec + - title: Montana Administrative Rule 42.15.524 - Itemized Deductions of Married Taxpayers (archived text, in effect until its repeal effective Jan. 25, 2025) + href: https://web.archive.org/web/20201030190413/http://www.mtrules.org/gateway/ruleno.asp?RN=42.15.524 + - title: 2022 Montana Form 2 Instructions, Allocation of deductions + href: 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=11 + - title: IRS Publication 502, Separate returns + href: https://www.irs.gov/publications/p502 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/cdcc/mt_childcare_itemization_boundary.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/cdcc/mt_childcare_itemization_boundary.yaml index f6f0e87b609..41e3e49579b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/cdcc/mt_childcare_itemization_boundary.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/cdcc/mt_childcare_itemization_boundary.yaml @@ -4,10 +4,11 @@ # Worksheet lines 1-6 (PDF p. 7): # https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7 # Instructions for Form 2 line 15 require both spouses to use the same method -# (PDF p. 16); the model retains the standard deduction when totals tie: +# (PDF p. 16). The couple takes the method with the lower combined tax; the +# summed deductions decide only when the taxes tie: # https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2022_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=16 -- name: Combined-AGI care deduction leaves standard deductions at the itemization boundary +- name: Combined-AGI care deduction at the summed-deduction boundary; itemizing lowers the tax period: 2022 absolute_error_margin: 0.01 input: @@ -40,14 +41,19 @@ # Care = min(3_000, 2_400) - 0.5 * (20_000 + 0 - 18_000) # = 1_400; each spouse gets 700. Itemized total = 4_860 + 700 + 700 # = 6_260. Standard = max(20% * 20_000, 2_260) + 2_260 = 6_260. - # The unchanged tie rule selects standard deductions. Phasing out on - # each spouse's own AGI would incorrectly give 700 + 1_200 care and - # itemized total 6_760, crossing the choice boundary. + # Phasing out on each spouse's own AGI would incorrectly give 700 + + # 1_200 care and itemized total 6_760. mt_child_dependent_care_expense_deduction: [700, 700, 0] mt_itemized_deductions_indiv: [5_560, 700, 0] mt_standard_deduction_indiv: [4_000, 2_260, 0] - mt_tax_unit_itemizes: false - mt_deductions_indiv: [4_000, 2_260, 0] + # The totals tie, but the election goes by tax. The spouse's column has + # no income, so only the head's deduction matters (2,710 exemption, and + # the child's 2,710 in the head's column): itemizing, 20_000 - 2_710 - + # 5_560 - 2_710 = 9_020 gives 176 + 120 x 4% = 180.80; standard, + # 20_000 - 2_710 - 4_000 - 2_710 = 10_580 gives 176 + 1_680 x 4% = + # 243.20. So the couple itemizes. + mt_tax_unit_itemizes: true + mt_deductions_indiv: [5_560, 700, 0] - name: One more dollar of mortgage interest crosses the derived childcare itemization boundary period: 2022 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_and_dental_expenses_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_and_dental_expenses_indiv.yaml new file mode 100644 index 00000000000..253996f6cde --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_and_dental_expenses_indiv.yaml @@ -0,0 +1,127 @@ +- name: With no income, every split gives the same tax, so the tie goes to the equal split + # Two tax units with the same people label the first adult head and then + # spouse; the columns do not depend on the label. The couple has no income, + # so the combined tax is zero at every candidate share and the tie goes to + # the share closest to the equal split. + period: 2021 + absolute_error_margin: 0.01 + input: + people: + a_first: + age: 50 + is_tax_unit_head: true + a_second: + age: 48 + is_tax_unit_spouse: true + other_medical_expenses: 700 + a_child1: + age: 17 + is_tax_unit_dependent: true + other_medical_expenses: 3_000 + a_child2: + age: 12 + is_tax_unit_dependent: true + other_medical_expenses: 1_000 + b_first: + age: 50 + is_tax_unit_spouse: true + b_second: + age: 48 + is_tax_unit_head: true + other_medical_expenses: 700 + b_child1: + age: 17 + is_tax_unit_dependent: true + other_medical_expenses: 3_000 + b_child2: + age: 12 + is_tax_unit_dependent: true + other_medical_expenses: 1_000 + tax_units: + a: + members: [a_first, a_second, a_child1, a_child2] + b: + members: [b_second, b_first, b_child1, b_child2] + households: + a: + members: [a_first, a_second, a_child1, a_child2] + state_code: MT + b: + members: [b_second, b_first, b_child1, b_child2] + state_code: MT + output: + mt_dependent_medical_expense_head_share: [0.5, 0.5] + # The dependents' 4,000 is divided equally: 2,000 to each column. The + # columns hold 2,000 + 2,700 = 4,700, every member's expenses. + mt_medical_and_dental_expenses_indiv: [2_000, 2_700, 0, 0, 2_000, 2_700, 0, 0] + +- name: An unmarried filer's column holds the dependent's expenses + period: 2022 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + other_medical_expenses: 100 + child: + age: 6 + is_tax_unit_dependent: true + other_medical_expenses: 900 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: MT + output: + mt_medical_and_dental_expenses_indiv: [1_000, 0] + +- name: No column amount once Montana ends separate filing on the same form + period: 2024 + input: + people: + parent: + age: 35 + other_medical_expenses: 100 + child: + age: 6 + is_tax_unit_dependent: true + other_medical_expenses: 900 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: MT + output: + mt_married_filing_separately_on_same_return_eligible: [false, false] + mt_medical_and_dental_expenses_indiv: [0, 0] + +- name: A member who is neither spouse nor a tax unit dependent is divided too + # The joint return's line 1 counts every member's other medical expenses, + # so the columns divide every member other than the head and spouse. + period: 2023 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + is_tax_unit_head: true + spouse: + age: 40 + is_tax_unit_spouse: true + relative: + age: 70 + is_tax_unit_dependent: false + other_medical_expenses: 1_000 + tax_units: + tax_unit: + members: [head, spouse, relative] + households: + household: + members: [head, spouse, relative] + state_code: MT + output: + mt_medical_and_dental_expenses_indiv: [500, 500, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_expense_deduction_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_expense_deduction_indiv.yaml index c3cc0f15446..da1d5b324d5 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_expense_deduction_indiv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_expense_deduction_indiv.yaml @@ -170,3 +170,153 @@ # give 0 + 4,000 + 1,000 = 5,000. mt_married_filing_separately_on_same_return_eligible: true mt_medical_expense_deduction_indiv: 1_500 + +- name: A dependent's medical expenses go to the column where they lower the couple's tax most + # Other medical expenses sit on the person cared for, so the child's 6,000 + # is in neither spouse's own input. Two tax units with the same people label + # the earner head and then spouse; each column's amount does not depend on + # the label. The couple may divide the child's bills in any proportion, and + # the model takes the candidate share with the lowest combined tax. + period: 2022 + absolute_error_margin: 0.01 + input: + people: + a_earner: + age: 45 + is_tax_unit_head: true + other_medical_expenses: 1_000 + mt_agi_indiv: 60_000 + a_partner: + age: 45 + is_tax_unit_spouse: true + other_medical_expenses: 500 + mt_agi_indiv: 20_000 + a_child: + age: 10 + is_tax_unit_dependent: true + other_medical_expenses: 6_000 + b_earner: + age: 45 + is_tax_unit_spouse: true + other_medical_expenses: 1_000 + mt_agi_indiv: 60_000 + b_partner: + age: 45 + is_tax_unit_head: true + other_medical_expenses: 500 + mt_agi_indiv: 20_000 + b_child: + age: 10 + is_tax_unit_dependent: true + other_medical_expenses: 6_000 + tax_units: + a: + members: [a_earner, a_partner, a_child] + b: + members: [b_partner, b_earner, b_child] + households: + a: + members: [a_earner, a_partner, a_child] + state_code: MT + b: + members: [b_partner, b_earner, b_child] + state_code: MT + output: + # Floors: earner 7.5% x 60,000 = 4,500; partner 7.5% x 20,000 = 1,500. + # Taxable income before the medical deduction (2022 exemptions of 2,710 + # each; the child's goes to the earner, the larger income): earner + # 60,000 - 5,420 = 54,580, at the 6.75% rate; partner 20,000 - 2,710 = + # 17,290, in the 6% bracket from 15,400 and 5% from 12,000. + # Tax saved by the medical deduction at each share to the earner: + # all to the partner: partner 500 + 6,000 - 1,500 = 5,000, taking + # 17,290 to 12,290: 1,890 x 6% + 3,110 x 5% = 268.90; + # all to the earner: earner 1,000 + 6,000 - 4,500 = 2,500 x 6.75% = + # 168.75; + # equal split: partner 3,500 - 1,500 = 2,000: 1,890 x 6% + 110 x 5% = + # 118.90; earner 4,000 is under the floor; + # the floors' kinks (shares 3,500 / 6,000 and 1 - 1,000 / 6,000): 90.00 + # and 101.25. + # The incomes before the medical deduction differ by more than the + # bills, so the balancing share clips to 1 (all to the earner). + # So all of the child's bills go to the partner's column. + mt_dependent_medical_expense_head_share: [0, 1] + mt_medical_and_dental_expenses_indiv: [1_000, 6_500, 0, 1_000, 6_500, 0] + # Earner: max(0, 1,000 - 4,500) = 0. Partner: 6,500 - 1,500 = 5,000. + # Leaving out the child's expenses would give max(0, 500 - 1,500) = 0. + mt_medical_expense_deduction_indiv: [0, 5_000, 0, 0, 5_000, 0] + +- name: An unmarried filer's column holds every dependent's medical expenses + period: 2023 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_tax_unit_head: true + other_medical_expenses: 500 + mt_agi_indiv: 30_000 + child1: + age: 8 + is_tax_unit_dependent: true + other_medical_expenses: 2_000 + child2: + age: 5 + is_tax_unit_dependent: true + other_medical_expenses: 1_500 + tax_units: + tax_unit: + members: [parent, child1, child2] + mt_agi_joint: 30_000 + households: + household: + members: [parent, child1, child2] + state_code: MT + output: + mt_medical_and_dental_expenses_indiv: [4_000, 0, 0] + # Line 1: (500 + 2,000 + 1,500) - 7.5% x 30,000 = 4,000 - 2,250 = 1,750, + # as on the single-column return. Leaving out the children's expenses + # would give max(0, 500 - 2,250) = 0. + mt_medical_expense_deduction_indiv: [1_750, 0, 0] + mt_medical_expense_deduction_joint: [1_750, 0, 0] + +- name: A reform that allows separate filing in 2024 also allocates a dependent's expenses + period: 2024 + reforms: policyengine_us.tests.policy.baseline.gov.states.mt.tax.income.enable_mt_mfss_2024_reform.enable_mt_mfss_2024_reform + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + is_tax_unit_head: true + health_insurance_premiums: 2_000 + mt_agi_indiv: 40_000 + person2: + age: 45 + is_tax_unit_spouse: true + mt_agi_indiv: 10_000 + person3: + age: 10 + is_tax_unit_dependent: true + other_medical_expenses: 4_000 + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + # Premiums stay under the floor with the other expenses from 2024. + # Floors: head 7.5% x 40,000 = 3,000; spouse 7.5% x 10,000 = 750. 2024 + # separate rates: 4.7% to 20,500 and 5.9% above, so the head's column is + # taxed at 5.9% and the spouse's at 4.7%. Tax saved at each share: + # all to the head: (2,000 + 4,000) - 3,000 = 3,000 x 5.9% = 177.00; + # all to the spouse: 4,000 - 750 = 3,250 x 4.7% = 152.75; + # equal split: 1,000 x 5.9% + 1,250 x 4.7% = 117.75; + # the floors' kinks (shares 1,000 / 4,000 and 1 - 750 / 4,000): + # 2,250 x 4.7% = 105.75 and 2,250 x 5.9% = 132.75. + # The balancing share clips to 1: the incomes differ by more than the + # bills. + mt_dependent_medical_expense_head_share: 1 + mt_medical_and_dental_expenses_indiv: [4_000, 0, 0] + mt_medical_expense_deduction_indiv: [3_000, 0, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/mt_tax_unit_itemizes.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/mt_tax_unit_itemizes.yaml index a9033e68da4..9c45bea3eae 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/mt_tax_unit_itemizes.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/mt_tax_unit_itemizes.yaml @@ -16,7 +16,7 @@ output: mt_tax_unit_itemizes: false -- name: Combined-AGI care phaseout keeps itemized deductions below the standard deduction +- name: Combined-AGI care phaseout keeps itemized deductions below the standard deduction, but itemizing lowers the tax period: 2022 input: people: @@ -47,9 +47,64 @@ # max(min(.2 * 20,000, 5,090), 2,260) = 4,000 and the 2,260 floor. # https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2022_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=35 # Itemized total = 4,650 + 700 + 700 = 6,050 < 4,000 + 2,260 = 6,260. - # Using each spouse's AGI would give the spouse 1,200 and total 6,550, - # incorrectly switching the couple to itemization. + # Using each spouse's AGI would give the spouse 1,200 and total 6,550. mt_itemized_deductions_indiv: [5_350, 700, 0] mt_standard_deduction_indiv: [4_000, 2_260, 0] + # The election goes by the couple's tax, not the sums. The spouse's + # column has no income, so only the head's deduction matters. With the + # 2,710 exemption and the child's 2,710 in the head's column: + # itemizing, 20,000 - 2,710 - 5,350 - 2,710 = 9,230 gives 176 + 330 x 4% + # = 189.20; standard, 20,000 - 2,710 - 4,000 - 2,710 = 10,580 gives + # 176 + 1,680 x 4% = 243.20. So the couple itemizes. + mt_tax_unit_itemizes: true + mt_deductions_indiv: [5_350, 700, 0] + +- name: Spouses filing separately take the standard deduction when itemizing would raise their tax + # Itemized deductions sum to more than the standard deductions, but the + # couple may elect the standard deduction (MCA 2021 15-30-2132(1); spouses + # must both itemize or both take it), so the election goes by tax. The + # child's 10,250 of bills is split equally here by input. + period: 2022 + absolute_error_margin: 0.05 + input: + people: + head: + age: 40 + is_tax_unit_head: true + mt_agi_indiv: 60_000 + mt_non_medical_itemized_deductions_indiv: 3_000 + spouse: + age: 40 + is_tax_unit_spouse: true + mt_agi_indiv: 15_000 + mt_non_medical_itemized_deductions_indiv: 700 + child: + age: 10 + is_tax_unit_dependent: true + other_medical_expenses: 10_250 + tax_units: + tax_unit: + members: [head, spouse, child] + mt_dependent_medical_expense_head_share: 0.5 + households: + household: + members: [head, spouse, child] + state_code: MT + output: + # Medical: head 5,125 - 7.5% x 60,000 = 625; spouse 5,125 - 1,125 = 4,000. + mt_itemized_deductions_indiv: [3_625, 4_700, 0] + # 2022: 20% of AGI, at most 5,090 and at least 2,260. + mt_standard_deduction_indiv: [5_090, 3_000, 0] + # Itemized 8,325 > standard 8,090, but the tax decides. Exemptions are + # 2,710 each, and the child's goes to the head. 2022 rates reach 734 at + # 19,800 and are 6.75% above; 300 at 12,000 and 4% from 8,900 to 12,000; + # 83 at 5,800 and 3% to 8,900. + # Itemizing: head 60,000 - 3,625 - 5,420 = 50,955 gives 734 + 31,155 x + # 6.75% = 2,836.9625; spouse 15,000 - 4,700 - 2,710 = 7,590 gives 83 + + # 1,790 x 3% = 136.70; together 2,973.66. + # Standard: head 60,000 - 5,090 - 5,420 = 49,490 gives 734 + 29,690 x + # 6.75% = 2,738.075; spouse 15,000 - 3,000 - 2,710 = 9,290 gives 176 + + # 390 x 4% = 191.60; together 2,929.675. mt_tax_unit_itemizes: false - mt_deductions_indiv: [4_000, 2_260, 0] + mt_deductions_indiv: [5_090, 3_000, 0] + mt_income_tax_before_refundable_credits_indiv: [2_738.08, 191.60, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/mt_income_tax.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/mt_income_tax.yaml index 62509f8f889..b14e93f80dd 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/mt_income_tax.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/mt_income_tax.yaml @@ -37,3 +37,172 @@ state_code: MT output: mt_income_tax: 4_000 + +- name: A dependent's medical bills lower a couple's tax when spouses file separately on the same form + # TY2022. The head has 60,000 of Montana AGI and 3,000 of other itemized + # deductions, the spouse 15,000 and 700, and their child 10,250 of medical + # bills. The couple may divide the bills in any proportion and elect the + # standard deduction, so the model takes the lowest tax over the candidate + # shares and the two elections. Exemptions are 2,710 each, and the child's + # goes to the head. 2022 rates: 734 at 19,800 and 6.75% above; 300 at + # 12,000 and 5% to 15,400; 176 at 8,900 and 4% to 12,000; 83 at 5,800 and + # 3% to 8,900; 33 at 3,300 and 2% to 5,800; 1% below 3,300. + # Combined tax by share of the bills to the head, itemizing (floors 4,500 + # and 1,125): + # all to the head: medical 5,750 and 0. Head 60,000 - 8,750 - 5,420 = + # 45,830: 734 + 26,030 x 6.75% = 2,491.025. Spouse 15,000 - 700 - + # 2,710 = 11,590: 176 + 2,690 x 4% = 283.60. Total 2,774.625. + # all to the spouse: medical 0 and 9,125. Head 51,580: 2,879.15. Spouse + # 2,465: 24.65. Total 2,903.80. + # equal split: medical 625 and 4,000. Total 2,973.66. + # the head's floor kink (head 4,500, spouse 5,750): 2,879.15 + 117.95 = + # 2,997.10; the spouse's (head 9,125, spouse 1,125): 2,566.9625 + + # 283.60 = 2,850.5625. + # the balancing share: the columns' levels (60,000 - 2,710 - 3,000 + + # 4,500 and 15,000 - 2,710 - 700 + 1,125) differ by 46,075, more than + # the bills, so it clips to 1 (all to the head). + # Standard deductions (5,090 and 3,000): 2,738.075 + 191.60 = 2,929.675. + # The joint return (AGI 75,000, standard deduction 10,180 over itemized + # 10,250 - 5,625 = 4,625, three exemptions): 75,000 - 10,180 - 8,130 = + # 56,690 gives 734 + 36,890 x 6.75% = 3,224.075. + # Before this change the bills were left out and the couple took the + # standard deduction (2,929.675). Splitting them equally with the election + # by deduction sums (8,325 > 8,090) would give 2,973.66. + period: 2022 + absolute_error_margin: 0.05 + input: + people: + head: + age: 40 + is_tax_unit_head: true + mt_agi_indiv: 60_000 + mt_non_medical_itemized_deductions_indiv: 3_000 + spouse: + age: 40 + is_tax_unit_spouse: true + mt_agi_indiv: 15_000 + mt_non_medical_itemized_deductions_indiv: 700 + child: + age: 10 + is_tax_unit_dependent: true + other_medical_expenses: 10_250 + tax_units: + tax_unit: + members: [head, spouse, child] + mt_agi_joint: 75_000 + households: + household: + members: [head, spouse, child] + state_code: MT + output: + mt_dependent_medical_expense_head_share: 1 + mt_medical_and_dental_expenses_indiv: [10_250, 0, 0] + mt_itemized_deductions_indiv: [8_750, 700, 0] + mt_tax_unit_itemizes: true + mt_income_tax_before_refundable_credits_indiv: [2_491.03, 283.60, 0] + mt_income_tax_joint: 3_224.08 + mt_files_separately: true + mt_income_tax: 2_774.63 + +- name: The tax-minimizing split balances the two columns' incomes + # TY2022. Montana AGI is 25,000 for the head and 15,000 for the spouse, with + # no other itemized deductions; their child has 25,000 of medical bills. + # Exemptions are 2,710 each and floors 7.5% of AGI (1,875 and 1,125). + # 2022 rates: 1% to 3,300; 33 at 3,300 and 2% to 5,800; 83 at 5,800 and 3% + # to 8,900; 176 at 8,900 and 4% to 12,000; 300 at 12,000 and 5% to 15,400. + # While both columns are above their floors, the total deduction is fixed + # and each column's income before the child's exemption is its level less + # its share of the bills: head 25,000 - 2,710 + 1,875 = 24,165 - 25,000 s; + # spouse 15,000 - 2,710 + 1,125 = 13,415 - 25,000 (1 - s). They balance at + # s = 0.5 + (24,165 - 13,415) / 50,000 = 0.715: + # head line 1a 17,875, deduction 16,000; 25,000 - 2,710 - 16,000 = 6,290; + # spouse line 1a 7,125, deduction 6,000; 15,000 - 2,710 - 6,000 = 6,290. + # The child's 2,710 is halved: 4,935 each, taxed 33 + 1,635 x 2% = 65.70. + # Total 131.40. + # Other candidates: equal split 178.20 + 9.15 = 187.35; all to the head + # 0 + 203.20 = 203.20; all to the spouse 720.80. Standard deductions (5,000 + # and 3,000): 429 + 191.60 = 620.60. Joint return: 40,000 - 22,000 itemized + # - 8,130 exemptions = 9,870, taxed 176 + 970 x 4% = 214.80. + period: 2022 + absolute_error_margin: 0.05 + input: + people: + head: + age: 40 + is_tax_unit_head: true + mt_agi_indiv: 25_000 + mt_non_medical_itemized_deductions_indiv: 0 + spouse: + age: 40 + is_tax_unit_spouse: true + mt_agi_indiv: 15_000 + mt_non_medical_itemized_deductions_indiv: 0 + child: + age: 10 + is_tax_unit_dependent: true + other_medical_expenses: 25_000 + tax_units: + tax_unit: + members: [head, spouse, child] + mt_agi_joint: 40_000 + households: + household: + members: [head, spouse, child] + state_code: MT + output: + mt_dependent_medical_expense_head_share: 0.715 + mt_medical_and_dental_expenses_indiv: [17_875, 7_125, 0] + mt_medical_expense_deduction_indiv: [16_000, 6_000, 0] + mt_tax_unit_itemizes: true + mt_income_tax_before_refundable_credits_indiv: [65.70, 65.70, 0] + mt_income_tax_joint: 214.80 + mt_files_separately: true + mt_income_tax: 131.40 + +- name: The balancing share counts the head's medical insurance premiums + # TY2022. The case above, with 3,000 of medical insurance premiums on the + # head. The premiums lower the head's level by 3,000: + # head 25,000 - 2,710 - 3,000 + 1,875 = 21,165; spouse 13,415. + # s = 0.5 + (21,165 - 13,415) / 50,000 = 0.5 + 7,750 / 50,000 = 0.655. + # Head line 1a 16,375, deduction 14,500 + 3,000 = 17,500; + # 25,000 - 2,710 - 17,500 = 4,790. + # Spouse line 1a 8,625, deduction 7,500; 15,000 - 2,710 - 7,500 = 4,790. + # The child's 2,710 is halved: 3,435 each, taxed 33 + 135 x 2% = 35.70. + # Leaving the premiums out of the level would keep s at 0.715, giving + # line 20 of 32.90 and 38.60 (71.50 in total). + period: 2022 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + is_tax_unit_head: true + mt_agi_indiv: 25_000 + mt_non_medical_itemized_deductions_indiv: 0 + medical_expense_health_insurance_premiums: 3_000 + spouse: + age: 40 + is_tax_unit_spouse: true + mt_agi_indiv: 15_000 + mt_non_medical_itemized_deductions_indiv: 0 + child: + age: 10 + is_tax_unit_dependent: true + other_medical_expenses: 25_000 + tax_units: + tax_unit: + members: [head, spouse, child] + mt_agi_joint: 40_000 + households: + household: + members: [head, spouse, child] + state_code: MT + output: + mt_dependent_medical_expense_head_share: 0.655 + mt_medical_and_dental_expenses_indiv: [16_375, 8_625, 0] + mt_income_tax_before_refundable_credits_indiv: [35.70, 35.70, 0] + # Joint: 40,000 - 3 x 2,710 - (22,000 + 3,000) = 6,870 taxable, + # tax 33 + 50 + 1,070 x 3% = 115.10. + mt_income_tax_joint: 115.10 + mt_files_separately: true + mt_income_tax: 71.40 diff --git a/policyengine_us/tests/unit/test_mt_dependent_medical_split_invariants.py b/policyengine_us/tests/unit/test_mt_dependent_medical_split_invariants.py new file mode 100644 index 00000000000..eb585c5bae2 --- /dev/null +++ b/policyengine_us/tests/unit/test_mt_dependent_medical_split_invariants.py @@ -0,0 +1,297 @@ +"""Invariants for Montana's tax-minimizing split of dependents' medical bills. + +On a 2021-2023 return where spouses file separately on the same form (status +2a), the couple may divide bills paid from a joint account that are not +attributable to one spouse in any proportion, and may elect the standard +deduction (both spouses or neither). The model treats every dependent's bill +as jointly paid, takes the head's share of those bills +(mt_dependent_medical_expense_head_share) from a closed-form candidate set to +minimize the couple's combined tax before refundable credits (Form 2 line 20 in both +columns), and picks the election (mt_tax_unit_itemizes) by the same tax. + +For each couple in a seeded population of 40 per year: + +1. The chosen tax is at most the tax at each of these shares when itemizing: + all to the head, all to the spouse, the equal split, the shares at which + each column's 7.5% floor starts to bind (computed here from the inputs, + not read from the model), and 0.1 to 0.9. With no capital gain credit the + minimum over all shares is at 0, 1 or where the columns' incomes balance, + so the interior shares catch a chooser that misses the balance point. +2. The chosen tax is at most the tax with the standard deduction in both + columns. +3. The couple's Montana tax (mt_income_tax) is at most its tax before this + change, which counted no dependent bills and itemized when the summed + itemized deductions exceeded the summed standard deductions. +4. Swapping which spouse is labelled head leaves the couple's tax unchanged. +5. More dependent bills never raise the couple's tax. + +Properties 3 to 5 use mt_income_tax in 2022 and 2023. In 2021 they use line +20, because the 2021 income tax rebate is capped at line 20 in each column +and is left out of the choice. + +Why Python rather than YAML: the properties compare generated couples with +their own variants (other shares, the other election, swapped labels, more +bills). The YAML cases pin worked examples. + +Montana AGI and the other itemized deductions are inputs, so the federal +return does not enter. Core treats a formula variable supplied for some tax +units as supplied for all of them, so the forced shares and elections run in +a second simulation in which every tax unit has them. +""" + +import numpy as np +import pytest + +from policyengine_core.periods import period + +from policyengine_us import Simulation +from policyengine_us.tools.mt_separate_return import ( + mt_separate_tax_before_refundable_credits, +) + +# Dollars; the model computes in float32 and incomes reach $150,000. +TOLERANCE = 0.05 +FORM_FLOOR = 0.075 # Itemized Deductions Schedule line 1c +YEARS = [2021, 2022, 2023] +EQUAL_SPLIT = 0.5 + + +def _kinks(couple, pool): + """The head shares at which each column's floor starts to bind.""" + room = [FORM_FLOOR * couple["agi"][i] - couple["own_medical"][i] for i in range(2)] + if pool <= 0: + return EQUAL_SPLIT, EQUAL_SPLIT + head = min(max(room[0] / pool, 0.0), 1.0) + spouse = min(max(1 - room[1] / pool, 0.0), 1.0) + return head, spouse + + +def _situation(entries, year): + """entries: (tag, couple, swapped, bills, unit inputs) per tax unit.""" + people, tax_units, households, marital_units = {}, {}, {}, {} + for tag, couple, swapped, dependent_bills, unit_inputs in entries: + filers = [] + for i in (1, 0) if swapped else (0, 1): + name = f"{tag}_filer{i}" + people[name] = { + "age": {year: 40}, + "is_tax_unit_head": {year: i == (1 if swapped else 0)}, + "is_tax_unit_spouse": {year: i == (0 if swapped else 1)}, + "mt_agi_indiv": {year: couple["agi"][i]}, + "other_medical_expenses": {year: couple["own_medical"][i]}, + "mt_non_medical_itemized_deductions_indiv": { + year: couple["other_itemized"][i] + }, + } + filers.append(name) + members = list(filers) + for j, amount in enumerate(dependent_bills): + name = f"{tag}_child{j}" + people[name] = { + "age": {year: 10}, + "is_tax_unit_dependent": {year: True}, + "other_medical_expenses": {year: amount}, + } + members.append(name) + marital_units[f"{name}_alone"] = {"members": [name]} + marital_units[f"{tag}_couple"] = {"members": filers} + unit = { + "members": members, + "mt_agi_joint": {year: sum(couple["agi"])}, + } + unit.update({k: {year: v} for k, v in unit_inputs.items()}) + tax_units[tag] = unit + households[tag] = {"members": members, "state_code": {year: "MT"}} + return { + "people": people, + "tax_units": tax_units, + "marital_units": marital_units, + "households": households, + } + + +def _taxes(entries, year): + sim = Simulation(situation=_situation(entries, year)) + unit = sim.populations["tax_unit"].members_entity_id + n = len(entries) + + def unit_sum(name): + values = np.asarray(sim.calculate(name, year), dtype=float) + return np.bincount(unit, weights=values, minlength=n) + + out = { + "line_20": unit_sum("mt_income_tax_before_refundable_credits_indiv"), + "itemized": unit_sum("mt_itemized_deductions_indiv"), + "standard": unit_sum("mt_standard_deduction_indiv"), + } + for name in ["mt_income_tax", "mt_dependent_medical_expense_head_share"]: + out[name] = np.asarray(sim.calculate(name, year), dtype=float) + out["couple_tax"] = out["line_20"] if year == 2021 else out["mt_income_tax"] + return out + + +def _check(couples_drawn, year): + n = len(couples_drawn) + # First simulation: the model chooses. Variants: as drawn, labels + # swapped, more bills, and no dependent bills (for the old election). + free = [] + for i, c in enumerate(couples_drawn): + more = [c["dependent_bills"][0] + c["more_bills"]] + c["dependent_bills"][1:] + free += [ + (f"u{i}", c, False, c["dependent_bills"], {}), + (f"s{i}", c, True, c["dependent_bills"], {}), + (f"m{i}", c, False, more, {}), + (f"z{i}", c, False, [0.0] * len(c["dependent_bills"]), {}), + ] + chosen = _taxes(free, year) + pick = lambda k: chosen["couple_tax"][k::4] # noqa: E731 + as_drawn, swapped, more_bills, no_bills = (pick(k) for k in range(4)) + old_itemizes = chosen["itemized"][3::4] > chosen["standard"][3::4] + share = chosen["mt_dependent_medical_expense_head_share"][0::4] + assert ((share >= 0) & (share <= 1)).all() + + # Second simulation: every tax unit has a forced share and election. + forced, labels = [], [] + for i, c in enumerate(couples_drawn): + pool = sum(c["dependent_bills"]) + head_kink, spouse_kink = _kinks(c, pool) + for label, s in [ + ("equal", EQUAL_SPLIT), + ("head", 1.0), + ("spouse", 0.0), + ("head_kink", head_kink), + ("spouse_kink", spouse_kink), + ] + [(f"grid{k}", k / 10) for k in range(1, 10)]: + inputs = { + "mt_dependent_medical_expense_head_share": s, + "mt_tax_unit_itemizes": True, + } + forced.append((f"{label}{i}", c, False, c["dependent_bills"], inputs)) + labels.append(label) + standard = { + "mt_dependent_medical_expense_head_share": EQUAL_SPLIT, + "mt_tax_unit_itemizes": False, + } + forced.append((f"standard{i}", c, False, c["dependent_bills"], standard)) + labels.append("standard") + old = { + "mt_dependent_medical_expense_head_share": EQUAL_SPLIT, + "mt_tax_unit_itemizes": bool(old_itemizes[i]), + } + zero = [0.0] * len(c["dependent_bills"]) + forced.append((f"old{i}", c, False, zero, old)) + labels.append("old") + fixed = _taxes(forced, year) + per = len(labels) // n + labels = np.array(labels[:per]) + fixed_line_20 = fixed["line_20"].reshape(n, per) + fixed_tax = fixed["couple_tax"].reshape(n, per) + + # 1 and 2. The chosen line 20 is at most every candidate's and the + # standard deduction's. + candidates = labels != "old" + assert ( + chosen["line_20"][0::4] <= fixed_line_20[:, candidates].min(axis=1) + TOLERANCE + ).all() + # 3. At most the tax before this change. + assert (as_drawn <= fixed_tax[:, labels == "old"][:, 0] + TOLERANCE).all() + # 4. Label swap. + np.testing.assert_allclose(as_drawn, swapped, atol=TOLERANCE) + # 5. More bills never raise the tax. + assert (more_bills <= as_drawn + TOLERANCE).all() + # The free choice with no dependent bills is the old return's tax or less. + assert (no_bills <= fixed_tax[:, labels == "old"][:, 0] + TOLERANCE).all() + + +SEED = 20261102 + + +@pytest.mark.parametrize("year", YEARS) +def test_seeded_population(year): + rng = np.random.default_rng(SEED + year) + drawn = [] + for _ in range(40): + drawn.append( + { + "agi": [float(rng.integers(0, 150_001)) for _ in range(2)], + "own_medical": [ + float(rng.choice([0, rng.integers(1, 12_001)])) for _ in range(2) + ], + "other_itemized": [ + float(rng.choice([0, rng.integers(1, 12_001)])) for _ in range(2) + ], + "dependent_bills": [ + float(rng.choice([0, rng.integers(1, 40_001)])) + for _ in range(int(rng.integers(1, 3))) + ], + "more_bills": float(rng.integers(1, 10_001)), + } + ) + _check(drawn, year) + + +@pytest.mark.parametrize("year", YEARS) +def test_closed_form_matches_columns(year): + """The closed form the choices compare equals the columns' line 20. + + Couples with wages and long-term capital gains, so the 2021-2023 capital + gain credit (Montana's non-refundable credit) is non-zero, and the federal + return feeds Montana AGI. A closed form that drifts from the column + variables would make the choices minimize a tax the columns don't owe. + """ + rng = np.random.default_rng(SEED + 10 * year) + people, tax_units, households, marital_units = {}, {}, {}, {} + for i in range(30): + members = [] + for role in ("head", "spouse"): + name = f"u{i}_{role}" + people[name] = { + "age": {year: 45}, + "employment_income": {year: float(rng.integers(0, 120_001))}, + "long_term_capital_gains": {year: float(rng.integers(0, 40_001))}, + "other_medical_expenses": { + year: float(rng.choice([0, rng.integers(1, 8_001)])) + }, + } + members.append(name) + marital_units[f"u{i}_couple"] = {"members": list(members)} + child = f"u{i}_child" + people[child] = { + "age": {year: 10}, + "is_tax_unit_dependent": {year: True}, + "other_medical_expenses": {year: float(rng.integers(0, 30_001))}, + } + marital_units[f"u{i}_child_alone"] = {"members": [child]} + members.append(child) + tax_units[f"u{i}"] = {"members": members} + households[f"u{i}"] = {"members": members, "state_code": {year: "MT"}} + situation = { + "people": people, + "tax_units": tax_units, + "marital_units": marital_units, + "households": households, + } + sim = Simulation(situation=situation) + tax_unit = sim.populations["tax_unit"] + unit = tax_unit.members_entity_id + n = tax_unit.count + columns = np.bincount( + unit, + weights=np.asarray( + sim.calculate("mt_income_tax_before_refundable_credits_indiv", year), + dtype=float, + ), + minlength=n, + ) + credits = np.bincount( + unit, + weights=np.asarray(sim.calculate("mt_non_refundable_credits", year), float), + minlength=n, + ) + # The guard is only meaningful if the credit is in play for most couples. + assert (credits > 0).sum() >= n // 2 + deductions = sim.calculate("mt_deductions_indiv", year) + closed_form = mt_separate_tax_before_refundable_credits( + tax_unit, period(year), sim.tax_benefit_system.parameters, deductions + ) + np.testing.assert_allclose(closed_form, columns, atol=0.01) diff --git a/policyengine_us/tests/unit/test_mt_medical_premium_deduction_invariants.py b/policyengine_us/tests/unit/test_mt_medical_premium_deduction_invariants.py index a5a90d8dceb..becf57e178c 100644 --- a/policyengine_us/tests/unit/test_mt_medical_premium_deduction_invariants.py +++ b/policyengine_us/tests/unit/test_mt_medical_premium_deduction_invariants.py @@ -37,9 +37,9 @@ 1. Accounting (2021-2023): the joint deduction is max(0, other expenses - 7.5% x Montana AGI) + lines 2 and 3, held by the - head. The separate variant is max(0, own other expenses - 7.5% x own - Montana AGI) + own lines 2 and 3 for the head and spouse; dependents hold - nothing. + head. The separate variant is max(0, line 1a in the column (property 10) + - 7.5% x own Montana AGI) + own lines 2 and 3 for the head and spouse; + dependents hold nothing. 2. Premium conservation against closed forms. Per-person helpers: line 2 + line 3 = max(0, premiums + long-term care premiums - own deduction), and line 2 = premiums - min(premiums, own @@ -65,6 +65,20 @@ exclusion of all premiums the self-employed deduction took (medical first), and the long-term care premiums still inside gives the same Montana deduction as the computed path. +10. Line 1a on separate returns (2021-2023): other medical expenses sit on + the person cared for, so a dependent's are not clearly attributable to + one spouse. Each spouse's column holds their own other expenses plus + their share of the dependents': the head's share is + mt_dependent_medical_expense_head_share (between 0 and 1, chosen to + minimize the couple's tax; its own invariants are in + test_mt_dependent_medical_split_invariants.py) and the spouse's the rest; + an unmarried head's column holds all of the dependents'. + Conservation: the head's and spouse's columns sum to every member's + other medical expenses, the joint return's line 1 base. Dependents hold + nothing. +11. Differential (2021-2023): for heads of household whose deduction is + recorded per person, the joint and separate variants agree, dependents' + expenses included. """ import numpy as np @@ -300,6 +314,7 @@ def _situation(units, year, mode, unit_inputs=None): PERSON_VARIABLES = [ "other_medical_expenses", + "mt_medical_and_dental_expenses_indiv", "long_term_health_insurance_premiums", "medical_expense_health_insurance_premiums", "self_employed_health_insurance_ald_person", @@ -311,6 +326,7 @@ def _situation(units, year, mode, unit_inputs=None): ] UNIT_VARIABLES = [ "mt_agi_joint", + "mt_dependent_medical_expense_head_share", "self_employed_health_insurance_ald", "mt_premium_deduction_joint", "itemized_medical_expenses", @@ -360,6 +376,7 @@ def _check(units, year, mode): deduction, run["medical_expense_deduction"], atol=TOLERANCE ) assert (indiv == 0).all() + assert (run["mt_medical_and_dental_expenses_indiv"] == 0).all() return None premium = run["medical_expense_health_insurance_premiums"] @@ -409,11 +426,35 @@ def _check(units, year, mode): full = run["mt_premium_deduction_joint"] np.testing.assert_allclose(full, expected_joint_premiums, atol=TOLERANCE) + # 10. Line 1a in each separate column, from the inputs' roles. + unit = run["unit"] + married = _unit_sum(run, role == "spouse") > 0 + head_share = run["mt_dependent_medical_expense_head_share"][unit] + assert ((head_share >= 0) & (head_share <= 1)).all() + spouse_share = 1 - head_share + share = np.where( + married[unit], + np.where( + role == "head", head_share, np.where(role == "spouse", spouse_share, 0) + ), + role == "head", + ) + dependent_other = _unit_sum(run, ~filer * other) + expected_line_1a = filer * other + share * dependent_other[unit] + line_1a = run["mt_medical_and_dental_expenses_indiv"] + np.testing.assert_allclose(line_1a, expected_line_1a, atol=TOLERANCE) + np.testing.assert_allclose( + _unit_sum(run, line_1a), _unit_sum(run, other), atol=TOLERANCE + ) + assert (line_1a[~filer] == 0).all() + # 1. Accounting, joint and separate. floored = np.maximum(_unit_sum(run, other) - FORM_FLOOR * run["mt_agi_joint"], 0) np.testing.assert_allclose(deduction, floored + full, atol=TOLERANCE) expected_indiv = filer * ( - np.maximum(other - FORM_FLOOR * run["mt_agi_indiv"], 0) + line_2 + line_3 + np.maximum(expected_line_1a - FORM_FLOOR * run["mt_agi_indiv"], 0) + + line_2 + + line_3 ) np.testing.assert_allclose(indiv, expected_indiv, atol=TOLERANCE) @@ -459,6 +500,16 @@ def _check(units, year, mode): np.testing.assert_allclose( deduction[single], _unit_sum(run, indiv)[single], atol=TOLERANCE ) + + # 11. So do heads of household, whose columns hold their dependents' + # expenses. + if mode != "unit": + head_of_household = run["filing_status"] == "HEAD_OF_HOUSEHOLD" + np.testing.assert_allclose( + deduction[head_of_household], + _unit_sum(run, indiv)[head_of_household], + atol=TOLERANCE, + ) return run, floored, full @@ -521,6 +572,21 @@ def test_seeded_population(year, mode): assert np.any((full > 0) & (floored > 0)) assert np.any(run["filing_status"] == "SINGLE") assert np.any(run["filing_status"] == "JOINT") + # Properties 10 and 11 bind: dependents' expenses raise the separate + # line 1 of some spouse, some married head and some head of household. + role = run["role"] + filer = role != "dependent" + unit = run["unit"] + agi_floor_indiv = FORM_FLOOR * run["mt_agi_indiv"] + own_line_1 = filer * np.maximum(run["other_medical_expenses"] - agi_floor_indiv, 0) + line_1 = filer * np.maximum( + run["mt_medical_and_dental_expenses_indiv"] - agi_floor_indiv, 0 + ) + raised = line_1 > own_line_1 + 1 + head_of_household = (run["filing_status"] == "HEAD_OF_HOUSEHOLD")[unit] + assert np.any(raised & (role == "spouse")) + assert np.any(raised & (role == "head") & ~head_of_household) + assert np.any(raised & (role == "head") & head_of_household) deduction = run["self_employed_health_insurance_ald_person"] premiums = run["medical_expense_health_insurance_premiums"] long_term_care = run["long_term_health_insurance_premiums"] diff --git a/policyengine_us/tools/mt_separate_return.py b/policyengine_us/tools/mt_separate_return.py new file mode 100644 index 00000000000..00d6a4fe485 --- /dev/null +++ b/policyengine_us/tools/mt_separate_return.py @@ -0,0 +1,199 @@ +"""Montana separate filing on the same form (status 2a), column by column. + +The functions below are the formulas of the status 2a column variables, taking +the amount a step depends on as an argument. The variables call them, and so +do the tax unit's choices that must compare the couple's tax under different +deductions without simulation branches: the split of a dependent's medical +bills (mt_dependent_medical_expense_head_share) and the itemize election +(mt_tax_unit_itemizes). Because both paths run the same code, the tax a choice +compares is the tax the columns then compute. +""" + +import numpy as np + +from policyengine_core.model_api import max_, min_, select, where + + +def mt_medical_deduction(person, period, parameters, line_1a): + """Each column's medical deduction given its line 1a amount. + + While the Itemized Deductions Schedule applies (2021-2023), line 1 floors + the medical and dental expenses other than premiums (line 1a) at 7.5% of + the column's Montana AGI, and lines 2 and 3 add the medical and long-term + care insurance premiums in full. In a later year (a reform allowing + separate filing) the premiums stay under the floor with the other + expenses. Zero for members other than the head and spouse. + """ + p = parameters(period).gov.states.mt.tax.income.deductions.itemized + floor = parameters(period).gov.irs.deductions.itemized.medical.floor + medical_floor = floor * person("mt_agi_indiv", period) + premiums = person("mt_eligible_medical_insurance_premiums", period) + if p.state_specific_deduction_applies: + long_term_care_premiums = person( + "mt_eligible_long_term_care_insurance_premiums", period + ) + deduction = ( + max_(0, line_1a - medical_floor) + premiums + long_term_care_premiums + ) + else: + deduction = max_(0, premiums + line_1a - medical_floor) + return person("is_tax_unit_head_or_spouse", period) * deduction + + +def mt_medical_floor_room(person, period, parameters): + """How much more line 1a each column can take before its floor binds. + + The 7.5% floor less what is already under it in the column: the column's + own other medical expenses, and from 2024 its premiums too. Negative when + the column is already above its floor. + """ + p = parameters(period).gov.states.mt.tax.income.deductions.itemized + floor = parameters(period).gov.irs.deductions.itemized.medical.floor + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + under_floor = head_or_spouse * person("other_medical_expenses", period) + if not p.state_specific_deduction_applies: + under_floor = under_floor + person( + "mt_eligible_medical_insurance_premiums", period + ) + return floor * person("mt_agi_indiv", period) - under_floor + + +def mt_allocate_dependent_exemptions(person, period, pre_dependent_exemption_income): + """Taxable income in each column after the dependent exemptions. + + The dependent exemptions are allocated optimally between the head and + spouse: first to the column with the larger income, up to the difference + between the two, and the rest halved. + """ + dependent_exemptions = person.tax_unit.sum( + person("mt_dependent_exemptions_person", period) + ) + head = person("is_tax_unit_head", period) + spouse = person("is_tax_unit_spouse", period) + head_income = person.tax_unit.sum(pre_dependent_exemption_income * head) + spouse_income = person.tax_unit.sum(pre_dependent_exemption_income * spouse) + # Cap the exemption amount initially at the difference between the head + # and spouse income, and allocate it to the spouse with the larger income. + income_difference = np.abs(head_income - spouse_income) + capped = min_(income_difference, dependent_exemptions) + larger = where(head_income > spouse_income, head, spouse) + # The remaining exemption amount is halved between the head and spouse. + halved_remaining = (dependent_exemptions - capped) / 2 + return max_(pre_dependent_exemption_income - capped * larger - halved_remaining, 0) + + +def _filing_status(person, period): + filing_status = person.tax_unit( + "state_filing_status_if_married_filing_separately_on_same_return", period + ) + return filing_status, filing_status.possible_values + + +def _net_long_term_capital_gain(person, period): + # Montana Form 2 line 2 uses the federal net long-term capital gain + # amount, which is limited by any short-term capital losses. + ltcg = person("long_term_capital_gains", period) + stcg = person("short_term_capital_gains", period) + return max_(min_(ltcg, ltcg + stcg), 0) + + +def mt_regular_tax(person, period, parameters, taxable_income): + """Each column's tax on taxable income from the rate schedule.""" + p = parameters(period).gov.states.mt.tax.income.main + if p.capital_gains.in_effect: + taxable_income = max_( + taxable_income - _net_long_term_capital_gain(person, period), 0 + ) + filing_status, status = _filing_status(person, period) + return select( + [ + filing_status == status.SINGLE, + filing_status == status.HEAD_OF_HOUSEHOLD, + filing_status == status.SEPARATE, + filing_status == status.SURVIVING_SPOUSE, + ], + [ + p.single.calc(taxable_income), + p.head_of_household.calc(taxable_income), + p.separate.calc(taxable_income), + p.surviving_spouse.calc(taxable_income), + ], + ) + + +def mt_capital_gains_threshold(person, period, parameters, taxable_income): + """The part of the lower capital gains rate band left after ordinary income.""" + p = parameters(period).gov.states.mt.tax.income.main.capital_gains + capital_gains = _net_long_term_capital_gain(person, period) + filing_status, status = _filing_status(person, period) + non_qualified_income = max_(taxable_income - capital_gains, 0) + rate_threshold = select( + [ + filing_status == status.SINGLE, + filing_status == status.SEPARATE, + filing_status == status.HEAD_OF_HOUSEHOLD, + filing_status == status.SURVIVING_SPOUSE, + ], + [ + p.rates.single.thresholds[-1], + p.rates.separate.thresholds[-1], + p.rates.head_of_household.thresholds[-1], + p.rates.surviving_spouse.thresholds[-1], + ], + ) + return max_(rate_threshold - non_qualified_income, 0) + + +def mt_capital_gains_tax(person, period, parameters, applicable_threshold): + """Each column's tax on net long-term capital gains (from 2024).""" + p = parameters(period).gov.states.mt.tax.income.main.capital_gains + if not p.in_effect: + return 0 + capital_gains = _net_long_term_capital_gain(person, period) + filing_status, status = _filing_status(person, period) + statuses = [ + filing_status == status.SINGLE, + filing_status == status.SEPARATE, + filing_status == status.SURVIVING_SPOUSE, + filing_status == status.HEAD_OF_HOUSEHOLD, + ] + schedules = [ + p.rates.single, + p.rates.separate, + p.rates.surviving_spouse, + p.rates.head_of_household, + ] + lower_rate = select(statuses, [s.amounts[0] for s in schedules]) + higher_rate = select(statuses, [s.amounts[-1] for s in schedules]) + below = min_(applicable_threshold, capital_gains) + above = max_(capital_gains - applicable_threshold, 0) + return max_(below * lower_rate + above * higher_rate, 0) + + +def mt_separate_tax_before_refundable_credits(tax_unit, period, parameters, deductions): + """The couple's combined status 2a tax before refundable credits. + + deductions is each person's deduction (the head's and spouse's columns; + zero for other members). This follows the column variables from + mt_pre_dependent_exemption_taxable_income_indiv to + mt_income_tax_before_refundable_credits_indiv (Form 2 line 20 in each + column) and sums the columns. Refundable credits do not depend on the + deductions, except the 2021 income tax rebate, which is capped at line + 20 and so is left out of the comparison. + """ + person = tax_unit.members + agi = person("mt_agi_indiv", period) + exemptions = person("mt_personal_exemptions_indiv", period) + pre_dependent_exemption_income = max_(agi - exemptions - deductions, 0) + taxable_income = mt_allocate_dependent_exemptions( + person, period, pre_dependent_exemption_income + ) + regular_tax = mt_regular_tax(person, period, parameters, taxable_income) + threshold = mt_capital_gains_threshold(person, period, parameters, taxable_income) + capital_gains_tax = mt_capital_gains_tax(person, period, parameters, threshold) + non_refundable_credits = person("mt_non_refundable_credits", period) + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + line_20 = head_or_spouse * max_( + regular_tax + capital_gains_tax - non_refundable_credits, 0 + ) + return tax_unit.sum(line_20) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_applicable_threshold_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_applicable_threshold_indiv.py index 1d9edfb6543..7b20059f3c8 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_applicable_threshold_indiv.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_applicable_threshold_indiv.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.tools.mt_separate_return import mt_capital_gains_threshold class mt_capital_gains_tax_applicable_threshold_indiv(Variable): @@ -11,29 +12,5 @@ class mt_capital_gains_tax_applicable_threshold_indiv(Variable): defined_for = "mt_married_filing_separately_on_same_return_eligible" def formula(person, period, parameters): - p = parameters(period).gov.states.mt.tax.income.main.capital_gains - ltcg = person("long_term_capital_gains", period) - stcg = person("short_term_capital_gains", period) - capital_gains = max_(min_(ltcg, ltcg + stcg), 0) taxable_income = person("mt_taxable_income_indiv", period) - filing_status = person.tax_unit( - "state_filing_status_if_married_filing_separately_on_same_return", - period, - ) - status = filing_status.possible_values - non_qualified_income = max_(taxable_income - capital_gains, 0) - rate_threshold = select( - [ - filing_status == status.SINGLE, - filing_status == status.SEPARATE, - filing_status == status.HEAD_OF_HOUSEHOLD, - filing_status == status.SURVIVING_SPOUSE, - ], - [ - p.rates.single.thresholds[-1], - p.rates.separate.thresholds[-1], - p.rates.head_of_household.thresholds[-1], - p.rates.surviving_spouse.thresholds[-1], - ], - ) - return max_(rate_threshold - non_qualified_income, 0) + return mt_capital_gains_threshold(person, period, parameters, taxable_income) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_indiv.py index 9b19609c8c6..1dfb302a59e 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_indiv.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/capital_gains/mt_capital_gains_tax_indiv.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.tools.mt_separate_return import mt_capital_gains_tax class mt_capital_gains_tax_indiv(Variable): @@ -13,60 +14,10 @@ class mt_capital_gains_tax_indiv(Variable): defined_for = "mt_married_filing_separately_on_same_return_eligible" def formula(person, period, parameters): - p = parameters(period).gov.states.mt.tax.income.main.capital_gains - # the tax for capital gains comes into effect after 2024 - if p.in_effect: - ltcg = person("long_term_capital_gains", period) - stcg = person("short_term_capital_gains", period) - net_cg = ltcg + stcg - capital_gains = max_(min_(ltcg, net_cg), 0) - filing_status = person.tax_unit( - "state_filing_status_if_married_filing_separately_on_same_return", - period, - ) - applicable_threshold = person( - "mt_capital_gains_tax_applicable_threshold_indiv", period - ) - status = filing_status.possible_values - - lower_rate = select( - [ - filing_status == status.SINGLE, - filing_status == status.SEPARATE, - filing_status == status.SURVIVING_SPOUSE, - filing_status == status.HEAD_OF_HOUSEHOLD, - ], - [ - p.rates.single.amounts[0], - p.rates.separate.amounts[0], - p.rates.surviving_spouse.amounts[0], - p.rates.head_of_household.amounts[0], - ], - ) - higher_rate = select( - [ - filing_status == status.SINGLE, - filing_status == status.SEPARATE, - filing_status == status.SURVIVING_SPOUSE, - filing_status == status.HEAD_OF_HOUSEHOLD, - ], - [ - p.rates.single.amounts[-1], - p.rates.separate.amounts[-1], - p.rates.surviving_spouse.amounts[-1], - p.rates.head_of_household.amounts[-1], - ], - ) - # Calculate taxes - capital_gains_below_threshold = min_(applicable_threshold, capital_gains) - capital_gains_above_threshold = max_( - capital_gains - applicable_threshold, 0 - ) - - lower_capital_gains_tax = capital_gains_below_threshold * lower_rate - higher_capital_gains_tax = capital_gains_above_threshold * higher_rate - - return max_(lower_capital_gains_tax + higher_capital_gains_tax, 0) - # Only apply tax if capital gains are positive - - return 0 + # The tax on capital gains comes into effect in 2024. + if not parameters(period).gov.states.mt.tax.income.main.capital_gains.in_effect: + return 0 + applicable_threshold = person( + "mt_capital_gains_tax_applicable_threshold_indiv", period + ) + return mt_capital_gains_tax(person, period, parameters, applicable_threshold) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/general/mt_tax_unit_itemizes.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/general/mt_tax_unit_itemizes.py index dd58d10efe0..5bf1e96d0df 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/general/mt_tax_unit_itemizes.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/general/mt_tax_unit_itemizes.py @@ -1,14 +1,55 @@ from policyengine_us.model_api import * +from policyengine_us.tools.mt_separate_return import ( + mt_separate_tax_before_refundable_credits, +) + +# Elections whose taxes differ by no more than this are treated as equal, so +# float32 noise does not decide between them. +TAX_TOLERANCE = 0.01 class mt_tax_unit_itemizes(Variable): value_type = bool entity = TaxUnit label = "Whether the tax unit in Montana itemizes the deductions when married filing separately" + documentation = ( + "The deduction election on the separate (status 2a) path. Spouses " + "filing separately on the same form must both claim the standard " + "deduction or both itemize, and the standard deduction is allowed if " + "the taxpayer elects it, so the couple takes whichever election gives " + "the lower combined tax before refundable credits (Form 2 line 20 in " + "both columns), evaluated in closed form with the column variables' " + "formulas. Itemizing uses the tax-minimizing split of the dependents' " + "medical expenses (mt_dependent_medical_expense_head_share). When the " + "two taxes are equal, the larger combined deduction decides, as it " + "did before. The joint path does not read this variable: a single " + "return takes the larger deduction, which cannot raise its tax. In " + "years without separate filing on the same form, the larger combined " + "deduction decides." + ) + reference = ( + # 2022 Form 2 instructions, PDF page 11: spouses "must either both + # claim the standard deduction, or both itemize their deductions". + "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=11", + # MCA 2021 15-30-2132(1): the standard deduction "is allowed if + # elected by the taxpayer". + "https://mca.legmt.gov/bills/2022/mca/title_0150/chapter_0300/part_0210/section_0320/0150-0300-0210-0320.html", + ) definition_period = YEAR defined_for = StateCode.MT def formula(tax_unit, period, parameters): - itemized_ded = add(tax_unit, period, ["mt_itemized_deductions_indiv"]) - standard_ded = add(tax_unit, period, ["mt_standard_deduction_indiv"]) - return itemized_ded > standard_ded + itemized = tax_unit.members("mt_itemized_deductions_indiv", period) + standard = tax_unit.members("mt_standard_deduction_indiv", period) + larger_itemized = tax_unit.sum(itemized) > tax_unit.sum(standard) + p = parameters(period).gov.states.mt.tax.income + if not p.married_filing_separately_on_same_return_allowed: + return larger_itemized + tax_itemizing = mt_separate_tax_before_refundable_credits( + tax_unit, period, parameters, itemized + ) + tax_standard = mt_separate_tax_before_refundable_credits( + tax_unit, period, parameters, standard + ) + tied = np.abs(tax_itemizing - tax_standard) <= TAX_TOLERANCE + return where(tied, larger_itemized, tax_itemizing < tax_standard) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py index b690c0195d8..8c7e39218b2 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py @@ -24,34 +24,13 @@ class mt_itemized_deductions_indiv(Variable): defined_for = "mt_married_filing_separately_on_same_return_eligible" def formula(person, period, parameters): - p = parameters(period).gov.states.mt.tax.income.deductions.itemized - # Since we only compute the federal charitable deduction at the tax unit level, - # we will split the value between each spouse - charitable_deduction = ( - person.tax_unit("charitable_deduction", period) * p.spouse_allocation_rate - ) + # The medical deduction is kept apart because the split of a + # dependent's medical bills is chosen by comparing the couple's tax + # with every other itemized deduction held fixed + # (mt_dependent_medical_expense_head_share). head_or_spouse = person("is_tax_unit_head_or_spouse", period) - # The interest deduction is the sum of mortagage and investment interest expenses - investment_interest = person("investment_interest_expense", period) - # Montana allows the mortgage interest federal law allows, so interest - # on acquisition debt above the IRC 163(h)(3) caps is not deductible - # (MCA 15-30-2131(1)(a); Form 2 instructions, itemized line 9). - mortgage_interest = person("deductible_mortgage_interest", period) - # Apply the federal phase-out first, then allocate the allowed - # premiums according to each spouse's payments on both residences. - mortgage_insurance = person( - "deductible_mortgage_insurance_premiums_indiv", period - ) - interest_ded = investment_interest + mortgage_interest + mortgage_insurance - other_deductions = add( - person, - period, - [ - "mt_misc_deductions", - "mt_child_dependent_care_expense_deduction", - "mt_medical_expense_deduction_indiv", - "mt_salt_deduction", - "mt_federal_income_tax_deduction_indiv", - ], + medical = person("mt_medical_expense_deduction_indiv", period) + return ( + person("mt_non_medical_itemized_deductions_indiv", period) + + head_or_spouse * medical ) - return head_or_spouse * (interest_ded + charitable_deduction + other_deductions) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_non_medical_itemized_deductions_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_non_medical_itemized_deductions_indiv.py new file mode 100644 index 00000000000..613cd3fc838 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_non_medical_itemized_deductions_indiv.py @@ -0,0 +1,56 @@ +from policyengine_us.model_api import * + + +class mt_non_medical_itemized_deductions_indiv(Variable): + value_type = float + entity = Person + label = "Montana itemized deductions other than medical expenses when married couples are filing separately" + unit = USD + definition_period = YEAR + reference = ( + "https://revenuefiles.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2021_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=33", + # 2021 Form 2 line 9 includes qualified mortgage insurance premiums + # allowed on the federal return, after the federal AGI phase-out. + "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://law.justia.com/codes/montana/2022/title-15/chapter-30/part-21/section-15-30-2131/", + # MT Code § 15-30-2131 (2022) (1) + "https://mca.legmt.gov/bills/2019/mca/title_0150/chapter_0300/part_0210/section_0010/0150-0300-0210-0010.html", + # MCA 15-30-2101: "Internal Revenue Code" means the IRC as amended + "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=38", + # 2023 Form 2 instructions, itemized deductions line 9: home mortgage + # interest "allowed by federal law", limited to the first $750,000 of + # acquisition debt incurred after Dec. 15, 2017 + ) + defined_for = "mt_married_filing_separately_on_same_return_eligible" + + def formula(person, period, parameters): + p = parameters(period).gov.states.mt.tax.income.deductions.itemized + # Since we only compute the federal charitable deduction at the tax unit level, + # we will split the value between each spouse + charitable_deduction = ( + person.tax_unit("charitable_deduction", period) * p.spouse_allocation_rate + ) + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + # The interest deduction is the sum of mortagage and investment interest expenses + investment_interest = person("investment_interest_expense", period) + # Montana allows the mortgage interest federal law allows, so interest + # on acquisition debt above the IRC 163(h)(3) caps is not deductible + # (MCA 15-30-2131(1)(a); Form 2 instructions, itemized line 9). + mortgage_interest = person("deductible_mortgage_interest", period) + # Apply the federal phase-out first, then allocate the allowed + # premiums according to each spouse's payments on both residences. + mortgage_insurance = person( + "deductible_mortgage_insurance_premiums_indiv", period + ) + interest_ded = investment_interest + mortgage_interest + mortgage_insurance + other_deductions = add( + person, + period, + [ + "mt_misc_deductions", + "mt_child_dependent_care_expense_deduction", + "mt_salt_deduction", + "mt_federal_income_tax_deduction_indiv", + ], + ) + return head_or_spouse * (interest_ded + charitable_deduction + other_deductions) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_dependent_medical_expense_head_share.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_dependent_medical_expense_head_share.py new file mode 100644 index 00000000000..4a8b4c51087 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_dependent_medical_expense_head_share.py @@ -0,0 +1,121 @@ +from policyengine_us.model_api import * +from policyengine_us.tools.mt_separate_return import ( + mt_medical_deduction, + mt_medical_floor_room, + mt_separate_tax_before_refundable_credits, +) + +# Two allocations whose taxes differ by no more than this are treated as equal, +# so float32 noise does not decide between them. +TAX_TOLERANCE = 0.01 + + +class mt_dependent_medical_expense_head_share(Variable): + value_type = float + entity = TaxUnit + definition_period = YEAR + label = "Montana head's share of dependents' medical expenses when married couples file separately on the same form" + unit = "/1" + documentation = ( + "The share of the dependents' other medical expenses (Itemized " + "Deductions Schedule line 1a) placed in the head's column when spouses " + "file separately on the same form; the spouse's column takes the " + "rest. For bills paid from accounts both spouses own, the form lets " + "the couple divide them in any proportion, so the model picks the " + "share that gives the lowest combined tax before refundable credits " + "(Form 2 line 20 in both columns), holding every other itemized " + "deduction fixed. It compares four candidates: the equal split " + "(spouse_allocation_rate), all to the head, all to the spouse, and " + "the share at which the two columns' incomes before the dependent " + "exemptions are equal. While both columns are above their 7.5% " + "floors, the total deduction does not depend on the share and the " + "couple's tax is convex in it, lowest where the columns balance. " + "Where a column is below its floor, bills moved into it are wasted " + "and the tax is monotone in the share, so the shares at which a " + "floor starts to bind never beat all to one spouse. The set reaches " + "the minimum unless a column's non-refundable credit (the capital " + "gain credit) holds its line 20 at zero, which can move the minimum " + "away from the balance point. Ties go to the share closest to the " + "equal split, so the result does not depend on which spouse is " + "labelled head. The tax is evaluated in closed form with the column " + "variables' own formulas (policyengine_us.tools.mt_separate_return), " + "not with simulation branches. Unmarried filers and couples without " + "dependents' expenses get the equal split, which has no effect." + ) + reference = ( + # PDF pages 11, 32-33: "Allocation of deductions" and line 1. + "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=11", + # ARM 42.15.524(3)(b): on the same form, a deduction not clearly + # attributable to one spouse "may be divided as provided on the form". + "https://web.archive.org/web/20201030190413/http://www.mtrules.org/gateway/ruleno.asp?RN=42.15.524", + ) + defined_for = StateCode.MT + + def formula(tax_unit, period, parameters): + p = parameters(period).gov.states.mt.tax.income + equal_split = p.deductions.itemized.spouse_allocation_rate + default = np.full(tax_unit.count, equal_split, dtype=float) + if not p.married_filing_separately_on_same_return_allowed: + return default + person = tax_unit.members + head = person("is_tax_unit_head", period) + spouse = person("is_tax_unit_spouse", period) + head_or_spouse = head | spouse + expense = person("other_medical_expenses", period) + pool = tax_unit.sum(expense * ~head_or_spouse) + own_expense = head_or_spouse * expense + other_itemized = person("mt_non_medical_itemized_deductions_indiv", period) + + def couple_tax(head_share): + share = tax_unit.project(head_share) + column_share = where(head, share, where(spouse, 1 - share, 0)) + line_1a = own_expense + column_share * tax_unit.project(pool) + itemized = other_itemized + mt_medical_deduction( + person, period, parameters, line_1a + ) + return mt_separate_tax_before_refundable_credits( + tax_unit, period, parameters, itemized + ) + + # The share at which the two columns' incomes before the dependent + # exemptions are equal. While both columns are above their 7.5% + # floors, each extra dollar of bills is deductible in either column, + # so the total deduction is fixed and the couple's tax is convex in + # the share, lowest where the columns balance. There a column's + # income is level - share x pool, where level is its income with line + # 1a at its floor (room is how far line 1a is below the floor); the + # spouse's uses 1 - share. Where a column is below its floor, bills + # moved into it are wasted, so the tax is monotone there and the + # shares at which a floor starts to bind never beat 0 or 1. + room = mt_medical_floor_room(person, period, parameters) + safe_pool = where(pool > 0, pool, 1) + kink_deduction = mt_medical_deduction( + person, period, parameters, own_expense + room + ) + level = ( + person("mt_agi_indiv", period) + - person("mt_personal_exemptions_indiv", period) + - other_itemized + - kink_deduction + + room + ) + gap = tax_unit.sum(level * head) - tax_unit.sum(level * spouse) + balance = np.clip(0.5 + gap / (2 * safe_pool), 0, 1) + candidates = [ + np.full(tax_unit.count, 0.0), + np.full(tax_unit.count, 1.0), + balance, + ] + best_share = default + best_tax = couple_tax(default) + for candidate in candidates: + tax = couple_tax(candidate) + lower = tax < best_tax - TAX_TOLERANCE + tied_and_closer = (np.abs(tax - best_tax) <= TAX_TOLERANCE) & ( + np.abs(candidate - equal_split) < np.abs(best_share - equal_split) + ) + take = lower | tied_and_closer + best_share = where(take, candidate, best_share) + best_tax = where(take, tax, best_tax) + married = tax_unit("tax_unit_married", period) + return where(married & (pool > 0), best_share, default) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_and_dental_expenses_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_and_dental_expenses_indiv.py new file mode 100644 index 00000000000..c3931ef2fa2 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_and_dental_expenses_indiv.py @@ -0,0 +1,61 @@ +from policyengine_us.model_api import * + + +class mt_medical_and_dental_expenses_indiv(Variable): + value_type = float + entity = Person + definition_period = YEAR + label = "Montana medical and dental expenses in each column when married couples are filing separately" + documentation = ( + "Itemized Deductions Schedule line 1a in a filer's column: medical " + "and dental expenses other than insurance premiums. Other medical " + "expenses are recorded on the person cared for, including amounts " + "other household members paid on that person's behalf, so the " + "inputs do not say which spouse paid a dependent's bills. Each " + "spouse's own expenses stay in that spouse's column. The model treats " + "every dependent's bill as paid from an account both spouses own: " + "for such bills the form lets the couple divide them between the " + "columns in any proportion, whereas a bill paid from one spouse's own " + "funds belongs to that spouse, and the model has no payer data to " + "tell them apart. The head's column takes the share " + "mt_dependent_medical_expense_head_share of every dependent's " + "expenses, chosen to minimize the couple's Montana tax, and the " + "spouse's column the rest. An unmarried filer's column holds every " + "dependent's expenses. The pool is every member other than the head " + "and spouse, so the head's and spouse's columns together hold every " + "member's other medical expenses, as the joint return's line 1 does." + ) + 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", + # 2021 instructions, "Allocation of deductions" (PDF page 10). + "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2021_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=10", + # PDF pages 11, 32-33. Page 11, "Allocation of deductions": payments + # from accounts owned by both spouses for expenses not attributable + # specifically to one spouse "can be allocated to either spouse in + # any proportional amount". Pages 32-33: line 1 instructions. + "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=11", + # 2023 instructions, "Allocation of deductions" (PDF pages 12-13). + "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=12", + # ARM 42.15.524(3)(a)-(b), in effect 2010-2024 (repealed effective + # Jan. 25, 2025), for spouses who filed a joint federal return or + # none: a deduction clearly attributable to one spouse is claimed + # only by that spouse; on the same form, one not clearly attributable + # to one spouse "may be divided as provided on the form". + "https://web.archive.org/web/20201030190413/http://www.mtrules.org/gateway/ruleno.asp?RN=42.15.524", + # MCA 2021 § 15-30-2131(1)(a) + "https://mca.legmt.gov/bills/2022/mca/title_0150/chapter_0300/part_0210/section_0310/0150-0300-0210-0310.html", + ) + unit = USD + defined_for = "mt_married_filing_separately_on_same_return_eligible" + + def formula(person, period, parameters): + expense = person("other_medical_expenses", period) + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + dependent_expense = person.tax_unit.sum(expense * ~head_or_spouse) + married = person.tax_unit("tax_unit_married", period) + head = person("is_tax_unit_head", period) + spouse = person("is_tax_unit_spouse", period) + head_share = person.tax_unit("mt_dependent_medical_expense_head_share", period) + married_share = where(head, head_share, where(spouse, 1 - head_share, 0)) + share = where(married, married_share, head) + return head_or_spouse * expense + share * dependent_expense diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_expense_deduction_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_expense_deduction_indiv.py index 3237224cb20..8f0fd89f9f3 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_expense_deduction_indiv.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_expense_deduction_indiv.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.tools.mt_separate_return import mt_medical_deduction class mt_medical_expense_deduction_indiv(Variable): @@ -21,35 +22,20 @@ class mt_medical_expense_deduction_indiv(Variable): documentation = ( "Each spouse's medical deduction when filing separately on the same " "form. While the Itemized Deductions Schedule applies (2021-2023), " - "line 1 floors the spouse's medical and dental expenses other than " - "premiums at 7.5% of their Montana AGI, and lines 2 and 3 add their " - "medical and long-term care insurance premiums in full, net of their " - "self-employed health insurance deduction. If a reform allows " - "separate filing in a later year, the premiums net of that deduction " - "stay under the floor with the other expenses. Premium inputs use " - "payer attribution: report all premiums paid by a person on that " - "person, including family coverage regardless of the covered " - "beneficiary." + "line 1 floors the medical and dental expenses other than premiums " + "in the spouse's column (mt_medical_and_dental_expenses_indiv: their " + "own expenses and their share of the dependents') at 7.5% of their " + "Montana AGI, and lines 2 and 3 add their medical and long-term care " + "insurance premiums in full, net of their self-employed health " + "insurance deduction. If a reform allows separate filing in a later " + "year, the premiums net of that deduction stay under the floor with " + "the other expenses. Premium inputs use payer attribution: report " + "all premiums paid by a person on that person, including family " + "coverage regardless of the covered beneficiary." ) def formula(person, period, parameters): - p = parameters(period).gov.states.mt.tax.income.deductions.itemized - floor = parameters(period).gov.irs.deductions.itemized.medical.floor - expense = person("other_medical_expenses", period) - # Law does not define Montana AGI as the cap. - # Tax form points to page 1, line 14, which is Montana AGI. - medical_floor = floor * person("mt_agi_indiv", period) - premiums = person("mt_eligible_medical_insurance_premiums", period) - if p.state_specific_deduction_applies: - # Line 1 floors expenses other than premiums; lines 2 and 3 - # deduct the premiums in full. - long_term_care_premiums = person( - "mt_eligible_long_term_care_insurance_premiums", period - ) - deduction = ( - max_(0, expense - medical_floor) + premiums + long_term_care_premiums - ) - else: - deduction = max_(0, premiums + expense - medical_floor) - is_head_or_spouse = person("is_tax_unit_head_or_spouse", period) - return is_head_or_spouse * deduction + line_1a = person("mt_medical_and_dental_expenses_indiv", period) + # Law does not define Montana AGI as the floor's base; the form points + # to page 1, line 14, which is Montana AGI. + return mt_medical_deduction(person, period, parameters, line_1a) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/tax_calculation/mt_regular_income_tax_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/tax_calculation/mt_regular_income_tax_indiv.py index abdebdf6e12..fd94ee08684 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/tax_calculation/mt_regular_income_tax_indiv.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/tax_calculation/mt_regular_income_tax_indiv.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.tools.mt_separate_return import mt_regular_tax class mt_regular_income_tax_indiv(Variable): @@ -10,32 +11,5 @@ class mt_regular_income_tax_indiv(Variable): defined_for = "mt_married_filing_separately_on_same_return_eligible" def formula(person, period, parameters): - p = parameters(period).gov.states.mt.tax.income.main taxable_income = person("mt_taxable_income_indiv", period) - filing_status = person.tax_unit( - "state_filing_status_if_married_filing_separately_on_same_return", - period, - ) - if p.capital_gains.in_effect: - ltcg = person("long_term_capital_gains", period) - stcg = person("short_term_capital_gains", period) - net_cg = ltcg + stcg - # Montana Form 2 line 2 uses the federal net long-term capital gain - # amount, which is limited by any short-term capital losses. - cg_to_subtract = max_(min_(ltcg, net_cg), 0) - taxable_income = max_(taxable_income - cg_to_subtract, 0) - status = filing_status.possible_values - return select( - [ - filing_status == status.SINGLE, - filing_status == status.HEAD_OF_HOUSEHOLD, - filing_status == status.SEPARATE, - filing_status == status.SURVIVING_SPOUSE, - ], - [ - p.single.calc(taxable_income), - p.head_of_household.calc(taxable_income), - p.separate.calc(taxable_income), - p.surviving_spouse.calc(taxable_income), - ], - ) + return mt_regular_tax(person, period, parameters, taxable_income) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/tax_calculation/mt_taxable_income_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/tax_calculation/mt_taxable_income_indiv.py index 4bdc14bf29b..0ec79430286 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/tax_calculation/mt_taxable_income_indiv.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/tax_calculation/mt_taxable_income_indiv.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.tools.mt_separate_return import ( + mt_allocate_dependent_exemptions, +) class mt_taxable_income_indiv(Variable): @@ -17,36 +20,8 @@ def formula(person, period, parameters): pre_dependent_exemption_income = person( "mt_pre_dependent_exemption_taxable_income_indiv", period ) - dependent_exemptions = add( - person.tax_unit, period, ["mt_dependent_exemptions_person"] - ) - # The dependent exemptions are allocated optimally between the head and spouse - head = person("is_tax_unit_head", period) - spouse = person("is_tax_unit_spouse", period) - head_income = person.tax_unit.sum(pre_dependent_exemption_income * head) - spouse_income = person.tax_unit.sum(pre_dependent_exemption_income * spouse) - # Calculate the difference between the head and spouse income - income_difference = np.abs(head_income - spouse_income) - # Cap the exemption amount initially at the difference between the head and spouse income - exemptions_capped_at_income_difference = min_( - income_difference, dependent_exemptions - ) - head_income_exceeds_spouse_income = head_income > spouse_income - # The capped exemptions are allocated to the spouse with the larger income - spouse_with_larger_income = where( - head_income_exceeds_spouse_income, head, spouse - ) - exemptions_capped_at_income_difference_allocated_to_spouse_with_larger_income = ( - exemptions_capped_at_income_difference * spouse_with_larger_income - ) - # The remaining exemption amount is halved and allocated between the head and spouse - remaining_exemptions = ( - dependent_exemptions - exemptions_capped_at_income_difference - ) - halved_remaining_exemptions = remaining_exemptions / 2 - return max_( - pre_dependent_exemption_income - - exemptions_capped_at_income_difference_allocated_to_spouse_with_larger_income - - halved_remaining_exemptions, - 0, + # The dependent exemptions are allocated optimally between the head + # and spouse (mt_allocate_dependent_exemptions). + return mt_allocate_dependent_exemptions( + person, period, pre_dependent_exemption_income )