From 9fc76f979f6ec8d61730930dd354b4bea9669dca Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 17:00:16 -0400 Subject: [PATCH 1/6] Leave tax unit dependents' income out of Alabama AGI Alabama taxes every resident individual on their own income (Ala. Code 40-18-2(a)(1)), and every taxpayer over the filing threshold files a return stating their own items of gross income (40-18-27(a)). Only a husband and wife may combine income on one return (Ala. Admin. Code r. 810-3-27-.01(1)(b)1), and the Form 40 and 40A booklets (2007-2025, p. 5) tell resident dependents to file their own returns. No Alabama statute, rule or form lets a parent report a child's income. al_agi summed every tax unit member's income, so a dependent's wages raised the parent's Alabama AGI, standard deduction phase-out, dependent exemption tier and tax. It now uses tax_unit_non_dep_add for both the gross income sources and the deductions. The deductions list drops the dependents' self-employed health insurance and plan deductions and subtracts the filers' own U.S. obligation interest (us_govt_interest_person). The HB527 contrib reform mirrors the change and leaves a dependent's overtime on the dependent's return. Co-Authored-By: Claude Opus 5.5 --- .../al-agi-exclude-dependents.fixed.md | 1 + .../states/al/tax/income/agi/deductions.yaml | 13 +-- .../al/hb527/al_hb527_overtime_deduction.py | 9 +- .../gov/states/al/tax/income/al_agi.yaml | 107 +++++++++++++++++- .../tax/income/al_us_govt_bond_interest.yaml | 2 +- .../al/hb527/al_hb527_overtime_deduction.yaml | 29 +++++ .../gov/states/al/tax/income/al_agi.py | 26 ++++- 7 files changed, 165 insertions(+), 22 deletions(-) create mode 100644 changelog.d/al-agi-exclude-dependents.fixed.md diff --git a/changelog.d/al-agi-exclude-dependents.fixed.md b/changelog.d/al-agi-exclude-dependents.fixed.md new file mode 100644 index 00000000000..732e2b98393 --- /dev/null +++ b/changelog.d/al-agi-exclude-dependents.fixed.md @@ -0,0 +1 @@ +Leave tax unit dependents' income and deductions out of Alabama adjusted gross income; dependents report them on their own Alabama returns. diff --git a/policyengine_us/parameters/gov/states/al/tax/income/agi/deductions.yaml b/policyengine_us/parameters/gov/states/al/tax/income/agi/deductions.yaml index 03b60f72f2e..38052a3c519 100644 --- a/policyengine_us/parameters/gov/states/al/tax/income/agi/deductions.yaml +++ b/policyengine_us/parameters/gov/states/al/tax/income/agi/deductions.yaml @@ -1,4 +1,7 @@ description: Alabama provides these deductions for the state adjusted gross income. +# Alabama adjusted gross income leaves out a tax unit dependent's income and +# deductions, which belong on the dependent's own return (Ala. Code +# ยงยง 40-18-2(a)(1) and 40-18-27(a); Ala. Admin. Code r. 810-3-27-.01(1)). values: 2021-01-01: # IRA distributions prior to 1982 are not deductible. @@ -6,12 +9,9 @@ values: - alimony_expense # Line 4 - early_withdrawal_penalty # Line 5 - self_employed_health_insurance_ald # Line 7 - # Alabama AGI counts dependents' income, so their deductions too. - - dependents_self_employed_health_insurance_ald - health_savings_account_ald # Line 8 - self_employed_pension_contribution_ald # Line 9 - - dependents_self_employed_pension_contribution_ald - - us_govt_interest + - us_govt_interest_person - taxable_public_pension_income 2023-01-01: # IRA distributions prior to 1982 are not deductible. @@ -19,12 +19,9 @@ values: - alimony_expense # Line 4 - early_withdrawal_penalty # Line 5 - self_employed_health_insurance_ald # Line 7 - # Alabama AGI counts dependents' income, so their deductions too. - - dependents_self_employed_health_insurance_ald - health_savings_account_ald # Line 8 - self_employed_pension_contribution_ald # Line 9 - - dependents_self_employed_pension_contribution_ald - - us_govt_interest + - us_govt_interest_person - al_retirement_exemption #Schedule RS Line 10 - taxable_public_pension_income diff --git a/policyengine_us/reforms/states/al/hb527/al_hb527_overtime_deduction.py b/policyengine_us/reforms/states/al/hb527/al_hb527_overtime_deduction.py index feaa64002e2..0dce861037d 100644 --- a/policyengine_us/reforms/states/al/hb527/al_hb527_overtime_deduction.py +++ b/policyengine_us/reforms/states/al/hb527/al_hb527_overtime_deduction.py @@ -20,7 +20,10 @@ def formula(tax_unit, period, parameters): overtime_income = person("fsla_overtime_premium", period) # Cap at per-taxpayer limit per AL HB527 Section 40-18-15(a)(29) person_capped = min_(overtime_income, p.cap) - return tax_unit.sum(person_capped) + # A dependent's overtime is on the dependent's own return, like the + # rest of their income (see al_agi). + is_dependent = person("is_tax_unit_dependent", period) + return tax_unit.sum(person_capped * ~is_dependent) class al_agi(Variable): value_type = float @@ -39,8 +42,8 @@ def formula(tax_unit, period, parameters): period ).gov.states.al.tax.income.agi.gross_income_sources deductions = parameters(period).gov.states.al.tax.income.agi.deductions - gross_income = add(tax_unit, period, gross_sources) - agi_deductions = add(tax_unit, period, deductions) + gross_income = tax_unit_non_dep_add(tax_unit, period, gross_sources) + agi_deductions = tax_unit_non_dep_add(tax_unit, period, deductions) base_al_agi = gross_income - agi_deductions # If HB527 is in effect, subtract the overtime deduction overtime_deduction = tax_unit("al_hb527_overtime_deduction", period) diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/al_agi.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/al_agi.yaml index d1b72912602..e6242131b16 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/al_agi.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/al_agi.yaml @@ -41,7 +41,7 @@ al_agi: 60_000 al_income_tax: 2_442.75 -- name: A dependent's self-employed health insurance and plan deductions follow their income into Alabama AGI +- name: A dependent's self-employment income and deductions stay on the dependent's own return period: 2025 absolute_error_margin: 0.01 input: @@ -62,9 +62,104 @@ members: [parent, child] state_code: AL output: - # Alabama AGI sums every member's income, so it counts the child's 10,000 - # of Schedule C income and subtracts the child's own self-employed health - # insurance (1,000) and SEP (500) deductions: 50,000 - 1,500. The federal - # AGI leaves all of them on the child's return. + # The child's 10,000 of Schedule C income and their own self-employed + # health insurance (1,000) and SEP (500) deductions all go on the child's + # Alabama return (Ala. Code 40-18-27(a); Form 40 booklet p. 5, + # "Dependent's and Student's Income"), as on the federal return. adjusted_gross_income: 40_000 - al_agi: 48_500 + al_agi: 40_000 + +- name: A dependent's wages stay off the parent's Alabama AGI + period: 2021 + input: + people: + parent: + age: 40 + employment_income: 10_000 + child: + age: 16 + employment_income: 5_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: AL + output: + # The child files their own Alabama return if their income is over the + # filing threshold (2021 Form 40 booklet p. 5). + adjusted_gross_income: 10_000 + al_agi: 10_000 + +- name: A joint return combines the spouses' income but not a dependent's + period: 2025 + input: + people: + head: + age: 40 + employment_income: 30_000 + spouse: + age: 40 + taxable_interest_income: 2_000 + child: + age: 15 + qualified_dividend_income: 3_000 + tax_units: + tax_unit: + members: [head, spouse, child] + households: + household: + members: [head, spouse, child] + state_code: AL + output: + # Ala. Admin. Code r. 810-3-27-.01(1)(b)1: a husband and wife "may elect to + # file a joint return reporting their combined income and deductions". + # 30,000 + 2,000; the child's dividends are on the child's own return. + al_agi: 32_000 + +- name: A dependent's business loss does not reduce the parent's Alabama AGI + period: 2025 + input: + people: + parent: + age: 40 + employment_income: 40_000 + child: + age: 17 + self_employment_income: -5_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: AL + output: + al_agi: 40_000 + +- name: Only the filers' own U.S. government interest is subtracted from Alabama AGI + period: 2025 + input: + people: + parent: + age: 40 + taxable_interest_income: 6_000 + us_govt_interest_person: 1_000 + child: + age: 15 + employment_income: 3_000 + taxable_interest_income: 2_000 + us_govt_interest_person: 2_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: AL + output: + # 6,000 of interest less the parent's 1,000 of exempt U.S. obligation + # interest. The child's wages and interest, and the child's U.S. + # obligation interest, are on the child's own return. + al_agi: 5_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/al_us_govt_bond_interest.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/al_us_govt_bond_interest.yaml index 8e38892af6a..92ca542576c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/al_us_govt_bond_interest.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/al_us_govt_bond_interest.yaml @@ -3,6 +3,6 @@ input: state_code: AL interest_income: 10_000 - us_govt_interest: 5_000 + us_govt_interest_person: 5_000 output: al_agi: 5_000 diff --git a/policyengine_us/tests/policy/contrib/states/al/hb527/al_hb527_overtime_deduction.yaml b/policyengine_us/tests/policy/contrib/states/al/hb527/al_hb527_overtime_deduction.yaml index d917451753a..442114e2f65 100644 --- a/policyengine_us/tests/policy/contrib/states/al/hb527/al_hb527_overtime_deduction.yaml +++ b/policyengine_us/tests/policy/contrib/states/al/hb527/al_hb527_overtime_deduction.yaml @@ -222,3 +222,32 @@ output: # Deduction ($10k) exceeds base AGI ($5k), result should be capped at 0 al_agi: 0 + +- name: AL HB527 - A dependent's overtime and wages stay on the dependent's own return + absolute_error_margin: 0.1 + period: 2025 + reforms: policyengine_us.reforms.states.al.hb527.al_hb527_overtime_deduction.al_hb527_overtime_deduction + input: + gov.contrib.states.al.hb527.in_effect: true + gov.contrib.states.al.hb527.cap: 1_000 + people: + parent: + age: 45 + employment_income: 52_000 + fsla_overtime_premium: 500 + child: + age: 17 + employment_income: 8_000 + fsla_overtime_premium: 800 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_name: AL + output: + # Only the parent's 500 of overtime is deducted on the parent's return. + al_hb527_overtime_deduction: 500 + # 52,000 - 500; the child's 8,000 of wages are on the child's own return. + al_agi: 51_500 diff --git a/policyengine_us/variables/gov/states/al/tax/income/al_agi.py b/policyengine_us/variables/gov/states/al/tax/income/al_agi.py index 23d8bd3f4c5..7e851ca9b85 100644 --- a/policyengine_us/variables/gov/states/al/tax/income/al_agi.py +++ b/policyengine_us/variables/gov/states/al/tax/income/al_agi.py @@ -5,11 +5,29 @@ class al_agi(Variable): value_type = float entity = TaxUnit label = "Alabama adjusted gross income" + documentation = ( + "Alabama adjusted gross income of the head and spouse. A tax unit " + "dependent's income and deductions belong on the dependent's own " + "Alabama return, so they are left out." + ) defined_for = StateCode.AL unit = USD definition_period = YEAR - # The Code of Alabama 1975 - reference = "https://alison.legislature.state.al.us/code-of-alabama" + reference = ( + "https://alison.legislature.state.al.us/code-of-alabama?section=40-18-14", + # Every resident individual is taxed on their own income, and every + # taxpayer over the filing threshold files their own return. + "https://alison.legislature.state.al.us/code-of-alabama?section=40-18-2", + "https://alison.legislature.state.al.us/code-of-alabama?section=40-18-27", + # Ala. Admin. Code r. 810-3-27-.01(1): only a husband and wife may + # combine their income on one return. + "https://admincode.legislature.state.al.us/administrative-code/810-3-27", + # Dependents who meet the filing requirements file their own return. + "https://www.revenue.alabama.gov/wp-content/uploads/2026/01/25f40bk.pdf#page=5", + ) - adds = "gov.states.al.tax.income.agi.gross_income_sources" - subtracts = "gov.states.al.tax.income.agi.deductions" + def formula(tax_unit, period, parameters): + p = parameters(period).gov.states.al.tax.income.agi + gross_income = tax_unit_non_dep_add(tax_unit, period, p.gross_income_sources) + deductions = tax_unit_non_dep_add(tax_unit, period, p.deductions) + return gross_income - deductions From e44d3db4f67bbedef83ebab086e2f838e90bd3d7 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 17:00:16 -0400 Subject: [PATCH 2/6] Keep Alabama's dependent exemption tier through its AGI threshold Ala. Code 40-18-19(a)(9) gives $1,000 per dependent for AGI "equal to or less than" $20,000 (through 2021) or $50,000 (from 2022), and $500 up to and including $100,000. The Form 40 booklet charts read 0 - 50,000, 50,001 - 100,000 and Over 100,000. The bracket lookup counted a threshold as reached at equality, so AGI of exactly $20,000, $50,000 or $100,000 got the lower amount. Use calc(..., right=True) in the variable and in the contrib dependent exemption reform. Adds end-to-end Alabama tax cases for 2021 and 2025 households with a working dependent, computed from the Form 40 booklets. Co-Authored-By: Claude Opus 5.5 --- .../al-dependent-exemption-boundary.fixed.md | 1 + .../al_dependent_exemption_reform.py | 4 +- .../exemptions/al_dependent_exemption.yaml | 59 +++++++++++++++++ .../gov/states/al/tax/income/integration.yaml | 64 +++++++++++++++++++ .../exemptions/al_dependent_exemption.py | 4 +- 5 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 changelog.d/al-dependent-exemption-boundary.fixed.md diff --git a/changelog.d/al-dependent-exemption-boundary.fixed.md b/changelog.d/al-dependent-exemption-boundary.fixed.md new file mode 100644 index 00000000000..4c8473d1c04 --- /dev/null +++ b/changelog.d/al-dependent-exemption-boundary.fixed.md @@ -0,0 +1 @@ +Pay Alabama's ,000 dependent exemption at AGI of exactly ,000 (2021 and before) or ,000 (2022 on), and at exactly ,000, as Ala. Code 40-18-19(a)(9) provides. diff --git a/policyengine_us/reforms/states/al/dependent_exemption/al_dependent_exemption_reform.py b/policyengine_us/reforms/states/al/dependent_exemption/al_dependent_exemption_reform.py index 7891f445fca..42cf2203a87 100644 --- a/policyengine_us/reforms/states/al/dependent_exemption/al_dependent_exemption_reform.py +++ b/policyengine_us/reforms/states/al/dependent_exemption/al_dependent_exemption_reform.py @@ -23,7 +23,9 @@ def formula(tax_unit, period, parameters): else: eligible = is_dependent count = tax_unit.sum(eligible) - per_dependent = where(p.amount < 0, p_base.dependent.calc(al_agi), p.amount) + per_dependent = where( + p.amount < 0, p_base.dependent.calc(al_agi, right=True), p.amount + ) return count * per_dependent class reform(Reform): diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/exemptions/al_dependent_exemption.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/exemptions/al_dependent_exemption.yaml index df18552ce77..1b6e39631e3 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/exemptions/al_dependent_exemption.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/exemptions/al_dependent_exemption.yaml @@ -60,3 +60,62 @@ tax_unit_dependents: 3 output: al_dependent_exemption: 900 + +# Ala. Code 40-18-19(a)(9)b-e: $1,000 for AGI "equal to or less than" $20,000 +# (2007-2021) or $50,000 (2022 on); $500 for AGI "in excess of" that and +# "equal to or less than" $100,000; $300 above. The Form 40 booklet charts read +# 0 - 20,000 / 20,001 - 100,000 (2021, p. 8) and 0 - 50,000 / 50,001 - 100,000 +# / Over 100,000 (2025, p. 9). +- name: 2021 Alabama dependent exemption is $1,000 at exactly $20,000 of AGI + period: 2021 + input: + state_code: AL + al_agi: 20_000 + tax_unit_dependents: 1 + output: + al_dependent_exemption: 1_000 + +- name: 2021 Alabama dependent exemption is $500 just over $20,000 of AGI + period: 2021 + input: + state_code: AL + al_agi: 20_001 + tax_unit_dependents: 1 + output: + al_dependent_exemption: 500 + +- name: 2025 Alabama dependent exemption is $1,000 at exactly $50,000 of AGI + period: 2025 + input: + state_code: AL + al_agi: 50_000 + tax_unit_dependents: 1 + output: + al_dependent_exemption: 1_000 + +- name: 2025 Alabama dependent exemption is $500 just over $50,000 of AGI + period: 2025 + input: + state_code: AL + al_agi: 50_001 + tax_unit_dependents: 1 + output: + al_dependent_exemption: 500 + +- name: 2025 Alabama dependent exemption is $500 at exactly $100,000 of AGI + period: 2025 + input: + state_code: AL + al_agi: 100_000 + tax_unit_dependents: 1 + output: + al_dependent_exemption: 500 + +- name: 2025 Alabama dependent exemption is $300 just over $100,000 of AGI + period: 2025 + input: + state_code: AL + al_agi: 100_001 + tax_unit_dependents: 1 + output: + al_dependent_exemption: 300 diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/integration.yaml index c2c04efc79d..19c8d578a26 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/integration.yaml @@ -25,3 +25,67 @@ output: al_federal_income_tax_deduction: 1_010_474 al_income_tax: 79_786 + +- name: A dependent's wages do not raise the parent's Alabama tax, 2021 + period: 2021 + input: + people: + parent: + age: 40 + employment_income: 20_000 + child: + age: 16 + employment_income: 10_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: AL + output: + # The child's 10,000 of wages go on the child's own return (2021 Form 40 + # booklet p. 5), so the parent's head of family return has AGI of 20,000: + # standard deduction 4,700 (chart, AGI 0-23,499), personal exemption + # 3,000 and dependent exemption 1,000 (AGI not over 20,000). Federal tax + # after refundable credits is zero, so no federal tax deduction. + # Taxable income 11,300; tax 2% x 500 + 4% x 2,500 + 5% x 8,300 = 525. + al_agi: 20_000 + al_standard_deduction: 4_700 + al_personal_exemption: 3_000 + al_dependent_exemption: 1_000 + al_federal_income_tax_deduction: 0 + al_taxable_income: 11_300 + al_income_tax: 525 + +- name: A dependent's wages do not raise the parent's Alabama tax, 2025 + period: 2025 + input: + people: + parent: + age: 40 + employment_income: 24_000 + child: + age: 16 + employment_income: 6_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: AL + output: + # The parent's head of family return has AGI of 24,000 (2025 Form 40 + # booklet p. 5): standard deduction 5,200 (chart, AGI 0-25,999), personal + # exemption 3,000 and dependent exemption 1,000 (AGI not over 50,000). + # Federal tax is offset by the nonrefundable child tax credit, so no + # federal tax deduction. Taxable income 14,800; tax 2% x 500 + 4% x 2,500 + # + 5% x 11,800 = 700. + al_agi: 24_000 + al_standard_deduction: 5_200 + al_personal_exemption: 3_000 + al_dependent_exemption: 1_000 + al_federal_income_tax_deduction: 0 + al_taxable_income: 14_800 + al_income_tax: 700 diff --git a/policyengine_us/variables/gov/states/al/tax/income/exemptions/al_dependent_exemption.py b/policyengine_us/variables/gov/states/al/tax/income/exemptions/al_dependent_exemption.py index de4ef25e57d..16719b1d92b 100644 --- a/policyengine_us/variables/gov/states/al/tax/income/exemptions/al_dependent_exemption.py +++ b/policyengine_us/variables/gov/states/al/tax/income/exemptions/al_dependent_exemption.py @@ -15,5 +15,7 @@ def formula(tax_unit, period, parameters): p = parameters(period).gov.states.al.tax.income.exemptions al_agi = tax_unit("al_agi", period) dependents = tax_unit("tax_unit_dependents", period) - exemption_per_dependent = p.dependent.calc(al_agi) + # The amount drops only once AGI is over a threshold ("equal to or + # less than" $20,000 or $50,000 keeps $1,000). + exemption_per_dependent = p.dependent.calc(al_agi, right=True) return dependents * exemption_per_dependent From b6a268f3c4e8050b6264203a5cfa478f3ef8274a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 17:00:35 -0400 Subject: [PATCH 3/6] Restore the dollar amounts in the dependent exemption changelog fragment Co-Authored-By: Claude Opus 5.5 --- changelog.d/al-dependent-exemption-boundary.fixed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/al-dependent-exemption-boundary.fixed.md b/changelog.d/al-dependent-exemption-boundary.fixed.md index 4c8473d1c04..f07e92969f4 100644 --- a/changelog.d/al-dependent-exemption-boundary.fixed.md +++ b/changelog.d/al-dependent-exemption-boundary.fixed.md @@ -1 +1 @@ -Pay Alabama's ,000 dependent exemption at AGI of exactly ,000 (2021 and before) or ,000 (2022 on), and at exactly ,000, as Ala. Code 40-18-19(a)(9) provides. +Pay Alabama's $1,000 dependent exemption at AGI of exactly $20,000 (2021 and before) or $50,000 (2022 on), and $500 at exactly $100,000, as Ala. Code 40-18-19(a)(9) provides. From d7b9bb988e26d72dbc241adc1593082ee9357bf5 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 17:38:43 -0400 Subject: [PATCH 4/6] Add property tests for dependents in Alabama AGI Hypothesis draws batches of Alabama tax units and runs each twice, with dependents' income and deduction inputs as drawn and zeroed. It checks that dependents never move Alabama AGI, the standard deduction or the exemptions; that Alabama AGI equals a numpy sum of the parameterized items over the head and spouse (the old all-member sum for units without dependents); and that the dependent exemption follows the statutory tiers through each threshold. The test found that Alabama itemized deductions still count a dependent's FICA and self-employment tax; that is #9941, outside this change, so income tax is checked only where itemized and federal tax deductions are unchanged. Reverting al_agi to the all-member sum fails all three tests. Locally: 12.6 s of test time, 1.6 GB peak RSS. Co-Authored-By: Claude Opus 5.5 --- .../core/test_al_agi_dependent_invariants.py | 345 ++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 policyengine_us/tests/core/test_al_agi_dependent_invariants.py diff --git a/policyengine_us/tests/core/test_al_agi_dependent_invariants.py b/policyengine_us/tests/core/test_al_agi_dependent_invariants.py new file mode 100644 index 00000000000..4715b562e27 --- /dev/null +++ b/policyengine_us/tests/core/test_al_agi_dependent_invariants.py @@ -0,0 +1,345 @@ +"""Invariants for tax unit dependents in Alabama adjusted gross income. + +Alabama taxes every resident individual on their own income (Ala. Code +40-18-2(a)(1)), and every taxpayer over the filing threshold files a return +stating their own items of gross income (40-18-27(a)). Only a husband and +wife may combine income on one return (Ala. Admin. Code r. 810-3-27-.01(1)), +and the Form 40 booklet tells resident dependents to file their own returns. +So a tax unit dependent's income and deductions never belong in the filer's +Alabama AGI. + +Hypothesis draws batches of Alabama tax units (single, head of family with +dependents, joint with and without dependents), and a seeded population of +150 units adds breadth. Each batch runs as one vectorized simulation, twice: +with the dependents' income and deduction inputs as drawn and with them set +to zero. For every tax unit: + +1. The dependents' inputs never change Alabama AGI, the standard deduction + or the personal and dependent exemptions. Alabama income tax is unchanged + too wherever the itemized deductions and the federal income tax deduction + are. Two leaks outside AGI remain, and are left out here: Alabama + itemized deductions still count a dependent's FICA and self-employment + tax (PolicyEngine/policyengine-us#9941), and a dependent's dividends or + business income can still change the filer's federal tax through the + federal preferential-rate base and QBI deduction. +2. Differential: Alabama AGI equals an independent numpy sum, over the head + and spouse, of the parameterized gross income sources less the + person-level deductions, less the tax-unit deductions. For units without + dependents this is the previous all-member sum, so they see no change. +3. The dependent exemption per dependent is $1,000 for Alabama AGI up to and + including $50,000, $500 up to and including $100,000 and $300 above + (Ala. Code 40-18-19(a)(9)d-e, 2022 on), so it never rises with AGI. +""" + +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 + +# Person-level inputs behind Alabama's gross income sources and deductions. +DEPENDENT_INPUTS = [ + "employment_income", + "self_employment_income", + "self_employed_health_insurance_premiums", + "self_employed_pension_contributions", + "taxable_interest_income", + "us_govt_interest_person", + "qualified_dividend_income", + "alimony_income", + "alimony_expense", + "taxable_ira_distributions", + "traditional_ira_contributions", + "early_withdrawal_penalty", + "rental_income", + "long_term_capital_gains", + "taxable_public_pension_income", +] +UNIT_OUTPUTS = [ + "al_agi", + "al_standard_deduction", + "al_personal_exemption", + "al_dependent_exemption", + "al_itemized_deductions", + "al_federal_income_tax_deduction", + "al_income_tax", + "tax_unit_dependents", +] +# Outputs that depend on Alabama AGI but not on federal tax or itemized +# deductions. +INVARIANT_OUTPUTS = [ + "al_agi", + "al_standard_deduction", + "al_personal_exemption", + "al_dependent_exemption", +] + +amount = st.integers(1, 40_000).map(float) +small = st.one_of(st.just(0.0), st.integers(1, 5_000).map(float)) + + +def _maybe(draw, strategy): + return draw(strategy) if draw(st.booleans()) else 0.0 + + +@st.composite +def person_amounts(draw): + interest = _maybe(draw, st.integers(1, 5_000).map(float)) + public_pension = _maybe(draw, st.integers(1, 20_000).map(float)) + return { + "employment_income": _maybe(draw, amount), + # Draw some business losses: a dependent's loss must not lower the + # filer's AGI either. + "self_employment_income": _maybe(draw, st.integers(-10_000, 40_000).map(float)), + "self_employed_health_insurance_premiums": draw(small), + "self_employed_pension_contributions": draw(small), + "taxable_interest_income": interest, + # U.S. obligation interest is part of the person's interest income. + "us_govt_interest_person": float(draw(st.integers(0, int(interest)))), + "qualified_dividend_income": draw(small), + "alimony_income": draw(small), + "alimony_expense": draw(small), + "taxable_ira_distributions": draw(small), + "traditional_ira_contributions": draw(small), + "early_withdrawal_penalty": draw(st.integers(0, 300).map(float)), + "rental_income": _maybe(draw, st.integers(-5_000, 20_000).map(float)), + "long_term_capital_gains": draw(small), + # A public pension is both in pension income and subtracted from + # Alabama gross income. + "taxable_public_pension_income": public_pension, + } + + +@st.composite +def tax_units(draw): + kind = draw(st.sampled_from(["single", "hof", "joint", "joint_dependents"])) + n_dependents = draw(st.integers(1, 2)) if kind in ("hof", "joint_dependents") else 0 + return { + "head": {"age": draw(st.integers(25, 75)), **draw(person_amounts())}, + "spouse": ( + {"age": draw(st.integers(25, 75)), **draw(person_amounts())} + if kind.startswith("joint") + else None + ), + "dependents": [ + {"age": draw(st.integers(5, 23)), **draw(person_amounts())} + for _ in range(n_dependents) + ], + } + + +SEED = 20261006 + + +def _seeded_amounts(rng): + def some(low, high, p=0.5): + return float(round(rng.uniform(low, high))) if rng.random() < p else 0.0 + + interest = some(1, 5_000) + public_pension = some(1, 20_000, 0.3) + return { + "employment_income": some(1, 40_000, 0.6), + "self_employment_income": some(-10_000, 40_000, 0.4), + "self_employed_health_insurance_premiums": some(1, 5_000, 0.3), + "self_employed_pension_contributions": some(1, 5_000, 0.3), + "taxable_interest_income": interest, + "us_govt_interest_person": float(round(rng.uniform(0, interest))), + "qualified_dividend_income": some(1, 5_000, 0.3), + "alimony_income": some(1, 5_000, 0.2), + "alimony_expense": some(1, 5_000, 0.2), + "taxable_ira_distributions": some(1, 5_000, 0.3), + "traditional_ira_contributions": some(1, 5_000, 0.3), + "early_withdrawal_penalty": some(0, 300, 0.2), + "rental_income": some(-5_000, 20_000, 0.3), + "long_term_capital_gains": some(1, 5_000, 0.3), + "taxable_public_pension_income": public_pension, + } + + +def _seeded_units(n=150): + rng = np.random.default_rng(SEED) + units = [] + for _ in range(n): + kind = rng.choice(["single", "hof", "joint", "joint_dependents"]) + n_dependents = ( + int(rng.integers(1, 3)) if kind in ("hof", "joint_dependents") else 0 + ) + units.append( + { + "head": {"age": int(rng.integers(25, 76)), **_seeded_amounts(rng)}, + "spouse": ( + {"age": int(rng.integers(25, 76)), **_seeded_amounts(rng)} + if kind.startswith("joint") + else None + ), + "dependents": [ + {"age": int(rng.integers(5, 24)), **_seeded_amounts(rng)} + for _ in range(n_dependents) + ], + } + ) + return units + + +def _situation(units, year, *, zero_dependents): + people, groups = {}, {"tax_units": {}, "households": {}, "marital_units": {}} + for i, u in enumerate(units): + head = f"head_{i}" + people[head] = { + "is_tax_unit_head": True, + "is_tax_unit_spouse": False, + "is_tax_unit_dependent": False, + **u["head"], + } + members, couple = [head], [head] + if u["spouse"] is not None: + spouse = f"spouse_{i}" + people[spouse] = { + "is_tax_unit_head": False, + "is_tax_unit_spouse": True, + "is_tax_unit_dependent": False, + **u["spouse"], + } + members.append(spouse) + couple.append(spouse) + groups["marital_units"][f"couple_{i}"] = {"members": couple} + for j, d in enumerate(u["dependents"]): + child = f"dependent_{i}_{j}" + amounts = dict(d) + if zero_dependents: + amounts.update({name: 0.0 for name in DEPENDENT_INPUTS}) + people[child] = { + "is_full_time_student": d["age"] >= 19, + "is_tax_unit_head": False, + "is_tax_unit_spouse": False, + "is_tax_unit_dependent": True, + **amounts, + } + members.append(child) + groups["marital_units"][f"single_{i}_{j}"] = {"members": [child]} + groups["tax_units"][f"tax_unit_{i}"] = {"members": members} + groups["households"][f"household_{i}"] = { + "members": members, + "state_code": {year: "AL"}, + } + people = { + name: {k: {year: v} for k, v in values.items()} + for name, values in people.items() + } + return {"people": people, **groups} + + +def _agi_items(sim, year): + """Alabama's AGI items split by entity, read from the parameter lists.""" + p = sim.tax_benefit_system.parameters(year).gov.states.al.tax.income.agi + variables = sim.tax_benefit_system.variables + items = {} + for kind, names in ( + ("gross", p.gross_income_sources), + ("deduction", p.deductions), + ): + for name in names: + entity = variables[name].entity.key + items[(kind, name)] = ( + entity, + np.asarray(sim.calculate(name, year), dtype=float), + ) + return items + + +def _run(units, year, *, zero_dependents): + sim = Simulation(situation=_situation(units, year, zero_dependents=zero_dependents)) + out = { + name: np.asarray(sim.calculate(name, year), dtype=float) + for name in UNIT_OUTPUTS + } + out["is_dependent"] = np.asarray( + sim.calculate("is_tax_unit_dependent", year), dtype=bool + ) + out["unit"] = sim.populations["tax_unit"].members_entity_id + out["items"] = _agi_items(sim, year) + return out + + +def _unit_sum(run, values): + return np.bincount(run["unit"], weights=values, minlength=len(run["al_agi"])) + + +def _check(units, year): + run = _run(units, year, zero_dependents=False) + zeroed = _run(units, year, zero_dependents=True) + filer = ~run["is_dependent"] + has_dependent = run["tax_unit_dependents"] > 0 + + # 1. Dependents' items never reach the filer's Alabama return. + for name in INVARIANT_OUTPUTS: + np.testing.assert_allclose( + run[name], zeroed[name], atol=TOLERANCE, err_msg=name + ) + same_other_deductions = np.isclose( + run["al_federal_income_tax_deduction"], + zeroed["al_federal_income_tax_deduction"], + atol=TOLERANCE, + ) & np.isclose( + run["al_itemized_deductions"], + zeroed["al_itemized_deductions"], + atol=TOLERANCE, + ) + np.testing.assert_allclose( + run["al_income_tax"][same_other_deductions], + zeroed["al_income_tax"][same_other_deductions], + atol=TOLERANCE, + ) + + # 2. Differential against numpy sums over the head and spouse. + reference = np.zeros_like(run["al_agi"]) + all_members = np.zeros_like(run["al_agi"]) + for (kind, name), (entity, values) in run["items"].items(): + sign = 1 if kind == "gross" else -1 + if entity == "person": + reference += sign * _unit_sum(run, filer * values) + all_members += sign * _unit_sum(run, values) + else: + assert entity == "tax_unit", name + reference += sign * values + all_members += sign * values + np.testing.assert_allclose(run["al_agi"], reference, atol=TOLERANCE) + np.testing.assert_allclose( + run["al_agi"][~has_dependent], + all_members[~has_dependent], + atol=TOLERANCE, + ) + + # 3. Statutory dependent exemption tiers, "equal to or less than" each + # threshold (Ala. Code 40-18-19(a)(9)d-e). + agi = run["al_agi"] + per_dependent = np.select([agi <= 50_000, agi <= 100_000], [1_000, 500], 300) + np.testing.assert_allclose( + run["al_dependent_exemption"], + run["tax_unit_dependents"] * per_dependent, + atol=TOLERANCE, + ) + + +# 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(tax_units(), min_size=10, max_size=30)) +def test_dependents_stay_off_the_filers_alabama_return(units): + _check(units, 2025) + + +@pytest.mark.parametrize("year", [2025, 2026]) +def test_seeded_population(year): + _check(_seeded_units(), year) From 0b36abc5b8e76a8cad604e0b74cabe6459203273 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 17:50:25 -0400 Subject: [PATCH 5/6] Cite the dependent exemption charts on PDF p. 10 of the 2021 and 2025 booklets Co-Authored-By: Claude Opus 5.5 --- .../al/tax/income/exemptions/al_dependent_exemption.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/exemptions/al_dependent_exemption.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/exemptions/al_dependent_exemption.yaml index 1b6e39631e3..06eebdf5f70 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/exemptions/al_dependent_exemption.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/exemptions/al_dependent_exemption.yaml @@ -64,8 +64,8 @@ # Ala. Code 40-18-19(a)(9)b-e: $1,000 for AGI "equal to or less than" $20,000 # (2007-2021) or $50,000 (2022 on); $500 for AGI "in excess of" that and # "equal to or less than" $100,000; $300 above. The Form 40 booklet charts read -# 0 - 20,000 / 20,001 - 100,000 (2021, p. 8) and 0 - 50,000 / 50,001 - 100,000 -# / Over 100,000 (2025, p. 9). +# 0 - 20,000 / 20,001 - 100,000 (2021) and 0 - 50,000 / 50,001 - 100,000 +# / Over 100,000 (2025), each on PDF p. 10. - name: 2021 Alabama dependent exemption is $1,000 at exactly $20,000 of AGI period: 2021 input: From 6e958210ec472252dab9664ec39c5cd8be2ed311 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 10:09:13 -0400 Subject: [PATCH 6/6] Clarify Alabama integration tax figures use statutory rates --- .../baseline/gov/states/al/tax/income/integration.yaml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/integration.yaml index 19c8d578a26..2f0c519c9a9 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/integration.yaml @@ -49,7 +49,9 @@ # standard deduction 4,700 (chart, AGI 0-23,499), personal exemption # 3,000 and dependent exemption 1,000 (AGI not over 20,000). Federal tax # after refundable credits is zero, so no federal tax deduction. - # Taxable income 11,300; tax 2% x 500 + 4% x 2,500 + 5% x 8,300 = 525. + # Taxable income 11,300; the model's existing statutory-rate calculation + # gives 2% x 500 + 4% x 2,500 + 5% x 8,300 = 525. The Form 40 tax table + # (2021 booklet p. 25) instead gives 528 for this head of family return. al_agi: 20_000 al_standard_deduction: 4_700 al_personal_exemption: 3_000 @@ -80,8 +82,10 @@ # booklet p. 5): standard deduction 5,200 (chart, AGI 0-25,999), personal # exemption 3,000 and dependent exemption 1,000 (AGI not over 50,000). # Federal tax is offset by the nonrefundable child tax credit, so no - # federal tax deduction. Taxable income 14,800; tax 2% x 500 + 4% x 2,500 - # + 5% x 11,800 = 700. + # federal tax deduction. Taxable income 14,800; the model's existing + # statutory-rate calculation gives 2% x 500 + 4% x 2,500 + 5% x 11,800 + # = 700. The Form 40 tax table (2025 booklet p. 25) instead gives 703 + # for this head of family return. al_agi: 24_000 al_standard_deduction: 5_200 al_personal_exemption: 3_000