diff --git a/changelog.d/surviving-spouse-gross-income-test.fixed.md b/changelog.d/surviving-spouse-gross-income-test.fixed.md new file mode 100644 index 00000000000..dee3be0c8fd --- /dev/null +++ b/changelog.d/surviving-spouse-gross-income-test.fixed.md @@ -0,0 +1 @@ +Stop applying the qualifying relative gross income test to surviving spouse filing status, which IRC 2(a)(1)(B) disregards, and stop counting a dependent whose parent ids or relationship flags show they are not the filer's son, daughter or stepchild. diff --git a/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/filing_status.yaml b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/filing_status.yaml index f993c6326da..2c3f50fec24 100644 --- a/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/filing_status.yaml +++ b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/filing_status.yaml @@ -251,3 +251,112 @@ output: # Each parent has 5,100 of taxable benefits, not below 4,400. filing_status: SINGLE + +- name: Widowed parent of a 29-year-old dependent with $6,238 of income files as surviving spouse. + period: 2025 + input: + people: + head: + age: 67 + is_surviving_spouse: true + is_tax_unit_head: true + adult_child: + age: 29 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + # IRC 2(a)(1)(B) disregards the IRC 152(d)(1)(B) gross income test, + # so income above the $5,200 limit does not matter. + employment_income: 6_238 + tax_units: + tax_unit: + members: [head, adult_child] + households: + household: + members: [head, adult_child] + output: + filing_status: SURVIVING_SPOUSE + basic_standard_deduction: 31_500 + +- name: Widowed filer with a dependent parent files as head of household, not surviving spouse. + period: 2025 + input: + people: + head: + age: 67 + is_surviving_spouse: true + is_tax_unit_head: true + parent: + age: 88 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_parent_of_filer_or_spouse: true + tax_units: + tax_unit: + members: [head, parent] + households: + household: + members: [head, parent] + output: + filing_status: HEAD_OF_HOUSEHOLD + +- name: Widowed grandparent of a dependent grandchild files as head of household, not surviving spouse. + period: 2025 + input: + people: + head: + person_id: 1 + age: 60 + is_tax_unit_head: true + is_surviving_spouse: true + mother: + person_id: 2 + parent_1_id: 1 + age: 35 + is_tax_unit_head: true + is_tax_unit_spouse: false + grandchild: + person_id: 3 + parent_1_id: 2 + age: 8 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + tax_units: + head_unit: + members: [head, grandchild] + mother_unit: + members: [mother] + households: + household: + members: [head, mother, grandchild] + output: + filing_status: [HEAD_OF_HOUSEHOLD, SINGLE] + +- name: Widow whose own parent id is set still files as surviving spouse with a dependent child without ids. + period: 2025 + input: + people: + head: + person_id: 1 + parent_1_id: 90 + age: 67 + is_surviving_spouse: true + is_tax_unit_head: true + adult_child: + person_id: 2 + age: 29 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + employment_income: 3_000 + tax_units: + tax_unit: + members: [head, adult_child] + households: + household: + members: [head, adult_child] + output: + filing_status: SURVIVING_SPOUSE + basic_standard_deduction: 31_500 diff --git a/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/surviving_spouse_eligible.yaml b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/surviving_spouse_eligible.yaml index c84c6758396..9f0ef280682 100644 --- a/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/surviving_spouse_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/surviving_spouse_eligible.yaml @@ -2,7 +2,7 @@ period: 2022 input: people: - head: + head: is_surviving_spouse: true tax_units: tax_unit: @@ -14,14 +14,679 @@ period: 2022 input: people: - head: + head: is_tax_unit_head: true is_surviving_spouse: true - child: {} + child: + age: 10 tax_units: tax_unit: members: [head, child] - tax_unit_child_dependents: 1 tax_unit_married: false output: surviving_spouse_eligible: true + +# IRC 2(a)(1)(B) determines the son or daughter's dependency "without regard +# to subsections (b)(1), (b)(2), and (d)(1)(B)" of section 152. The 2025 Form +# 1040 instructions (page 15) apply this as a child the filer "could claim as a +# dependent except that ... the child had gross income of $5,200 or more". +- name: Adult dependent child with gross income above the exemption amount still qualifies. + period: 2025 + input: + people: + head: + age: 67 + is_tax_unit_head: true + is_surviving_spouse: true + adult_child: + age: 29 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + # Above the $5,200 2025 qualifying relative gross income limit. + employment_income: 6_238 + tax_units: + tax_unit: + members: [head, adult_child] + output: + dependent_gross_income: [0, 6_238] + is_qualifying_relative_dependent: [false, false] + surviving_spouse_eligible: true + +- name: Traced household, adult dependent child with $6,238 of disability benefits qualifies. + period: 2025 + input: + people: + head: + age: 67 + is_tax_unit_head: true + is_surviving_spouse: true + adult_child: + age: 29 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + disability_benefits: 6_238 + tax_units: + tax_unit: + members: [head, adult_child] + output: + surviving_spouse_eligible: true + +- name: Adult dependent child with gross income at the exemption amount qualifies. + period: 2025 + input: + people: + head: + age: 67 + is_tax_unit_head: true + is_surviving_spouse: true + adult_child: + age: 29 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + employment_income: 5_200 + tax_units: + tax_unit: + members: [head, adult_child] + output: + surviving_spouse_eligible: true + +- name: Adult dependent child with gross income below the exemption amount qualifies. + period: 2025 + input: + people: + head: + age: 67 + is_tax_unit_head: true + is_surviving_spouse: true + adult_child: + age: 29 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + employment_income: 3_000 + tax_units: + tax_unit: + members: [head, adult_child] + output: + is_qualifying_relative_dependent: [false, true] + surviving_spouse_eligible: true + +- name: Permanently disabled adult child with high income qualifies. + period: 2025 + input: + people: + head: + age: 67 + is_tax_unit_head: true + is_surviving_spouse: true + adult_child: + age: 40 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_permanently_and_totally_disabled: true + employment_income: 20_000 + tax_units: + tax_unit: + members: [head, adult_child] + output: + surviving_spouse_eligible: true + +# IRC 2(a)(1)(B)(i) requires a son, stepson, daughter or stepdaughter. A +# dependent parent is a qualifying relative under IRC 152(d)(2)(C), but not a +# child, so the filer does not qualify. +- name: Dependent parent of the filer with income below the exemption amount does not qualify. + period: 2025 + input: + people: + head: + age: 67 + is_tax_unit_head: true + is_surviving_spouse: true + parent: + age: 88 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_parent_of_filer_or_spouse: true + tax_units: + tax_unit: + members: [head, parent] + output: + is_qualifying_relative_dependent: [false, true] + surviving_spouse_eligible: false + +- name: Dependent grandparent of the filer does not qualify. + period: 2025 + input: + people: + head: + age: 50 + is_tax_unit_head: true + is_surviving_spouse: true + grandparent: + age: 92 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_grandparent_of_filer_or_spouse: true + tax_units: + tax_unit: + members: [head, grandparent] + output: + surviving_spouse_eligible: false + +- name: Unrelated dependent member of the household does not qualify. + period: 2025 + input: + people: + head: + age: 50 + is_tax_unit_head: true + is_surviving_spouse: true + household_member: + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_related_to_head_or_spouse: false + tax_units: + tax_unit: + members: [head, household_member] + output: + surviving_spouse_eligible: false + +- name: Dependent parent plus an adult child with high income qualifies through the child. + period: 2025 + input: + people: + head: + age: 67 + is_tax_unit_head: true + is_surviving_spouse: true + parent: + age: 88 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_parent_of_filer_or_spouse: true + adult_child: + age: 29 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + employment_income: 30_000 + tax_units: + tax_unit: + members: [head, parent, adult_child] + output: + surviving_spouse_eligible: true + +- name: Remarried surviving spouse with a dependent child does not qualify. + period: 2025 + input: + people: + head: + age: 67 + is_tax_unit_head: true + is_surviving_spouse: true + spouse: + age: 65 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 10 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, spouse, child] + output: + surviving_spouse_eligible: false + +- name: Filer who is not a surviving spouse does not qualify with an adult child. + period: 2025 + input: + people: + head: + age: 67 + is_tax_unit_head: true + is_surviving_spouse: false + adult_child: + age: 29 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + employment_income: 6_238 + tax_units: + tax_unit: + members: [head, adult_child] + output: + surviving_spouse_eligible: false + +# Parent ids (natural, adoptive or step) identify the filer's own children. +- name: Adult dependent whose parent id names the widowed head qualifies regardless of income. + period: 2025 + input: + people: + head: + person_id: 1 + age: 67 + is_tax_unit_head: true + is_surviving_spouse: true + adult_child: + person_id: 2 + parent_1_id: 1 + age: 29 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + employment_income: 30_000 + tax_units: + tax_unit: + members: [head, adult_child] + households: + household: + members: [head, adult_child] + output: + surviving_spouse_eligible: true + +- name: Dependent grandchild whose parent id names another household member does not qualify. + period: 2025 + input: + people: + head: + person_id: 1 + age: 60 + is_tax_unit_head: true + is_surviving_spouse: true + mother: + person_id: 2 + parent_1_id: 1 + age: 35 + is_tax_unit_head: true + is_tax_unit_spouse: false + grandchild: + person_id: 3 + parent_1_id: 2 + age: 8 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + tax_units: + head_unit: + members: [head, grandchild] + mother_unit: + members: [mother] + households: + household: + members: [head, mother, grandchild] + output: + surviving_spouse_eligible: [false, false] + +- name: Dependent parent without parent ids in a household with ids does not qualify. + period: 2025 + input: + people: + head: + person_id: 1 + # The head's own parent id names the dependent mother. + parent_1_id: 2 + age: 67 + is_tax_unit_head: true + is_surviving_spouse: true + mother: + person_id: 2 + age: 90 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + taxable_pension_income: 20_000 + tax_units: + tax_unit: + members: [head, mother] + households: + household: + members: [head, mother] + output: + surviving_spouse_eligible: false + +# A parent id of 0 is unknown, not evidence that the filer is not the parent. +- name: Head's own parent id does not stop a dependent child without ids from qualifying. + period: 2025 + input: + people: + head: + person_id: 1 + # Names the head's absent parent. + parent_1_id: 90 + age: 67 + is_tax_unit_head: true + is_surviving_spouse: true + adult_child: + person_id: 2 + age: 29 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + employment_income: 3_000 + tax_units: + tax_unit: + members: [head, adult_child] + households: + household: + members: [head, adult_child] + output: + surviving_spouse_eligible: true + +- name: Housemate's parent id in another tax unit does not stop a dependent child without ids from qualifying. + period: 2025 + input: + people: + head: + person_id: 1 + age: 67 + is_tax_unit_head: true + is_surviving_spouse: true + adult_child: + person_id: 2 + age: 29 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + employment_income: 3_000 + housemate: + person_id: 3 + parent_1_id: 90 + age: 40 + is_tax_unit_head: true + is_tax_unit_spouse: false + tax_units: + head_unit: + members: [head, adult_child] + housemate_unit: + members: [housemate] + households: + household: + members: [head, adult_child, housemate] + output: + surviving_spouse_eligible: [true, false] + +- name: Dependent child naming only the deceased parent, with the other id unknown, qualifies. + period: 2025 + input: + people: + head: + person_id: 1 + age: 67 + is_tax_unit_head: true + is_surviving_spouse: true + adult_child: + person_id: 2 + # The deceased spouse, who resolves to no household member. + parent_1_id: 91 + age: 29 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + employment_income: 30_000 + tax_units: + tax_unit: + members: [head, adult_child] + households: + household: + members: [head, adult_child] + output: + surviving_spouse_eligible: true + +- name: Dependent child whose second parent id names the head qualifies. + period: 2025 + input: + people: + head: + person_id: 1 + age: 67 + is_tax_unit_head: true + is_surviving_spouse: true + adult_child: + person_id: 2 + parent_1_id: 91 + parent_2_id: 1 + age: 29 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + employment_income: 30_000 + tax_units: + tax_unit: + members: [head, adult_child] + households: + household: + members: [head, adult_child] + output: + surviving_spouse_eligible: true + +# Two parent ids can name both biological parents and leave out a step +# parent, so naming two other parents does not rule a dependent out. +- name: Stepchild whose two parent ids name absent biological parents qualifies. + period: 2025 + input: + people: + head: + person_id: 1 + age: 67 + is_tax_unit_head: true + is_surviving_spouse: true + stepchild: + person_id: 2 + # The deceased spouse and the child's other biological parent. + parent_1_id: 91 + parent_2_id: 92 + age: 15 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, stepchild] + households: + household: + members: [head, stepchild] + output: + surviving_spouse_eligible: true + +- name: Daughter naming only her co-resident mother, with the other id unknown, qualifies her widowed father. + period: 2025 + input: + people: + head: + person_id: 1 + age: 45 + is_tax_unit_head: true + is_surviving_spouse: true + daughter: + person_id: 2 + parent_1_id: 3 + age: 15 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + mother: + person_id: 3 + age: 40 + is_tax_unit_head: true + is_tax_unit_spouse: false + tax_units: + head_unit: + members: [head, daughter] + mother_unit: + members: [mother] + households: + household: + members: [head, daughter, mother] + output: + surviving_spouse_eligible: [true, false] + +- name: Dependent nephew whose parent shares a parent id with the head does not qualify. + period: 2025 + input: + people: + head: + person_id: 1 + # The head's absent mother. + parent_1_id: 90 + age: 50 + is_tax_unit_head: true + is_surviving_spouse: true + nephew: + person_id: 2 + parent_1_id: 3 + age: 15 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + sister: + person_id: 3 + parent_1_id: 90 + age: 45 + is_tax_unit_head: true + is_tax_unit_spouse: false + tax_units: + head_unit: + members: [head, nephew] + sister_unit: + members: [sister] + households: + household: + members: [head, nephew, sister] + output: + surviving_spouse_eligible: [false, false] + +- name: Dependent sibling who shares an absent parent id with the head does not qualify. + period: 2025 + input: + people: + head: + person_id: 1 + parent_1_id: 90 + age: 45 + is_tax_unit_head: true + is_surviving_spouse: true + sibling: + person_id: 2 + parent_1_id: 90 + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, sibling] + households: + household: + members: [head, sibling] + output: + surviving_spouse_eligible: false + +- name: Dependent sibling who shares a co-resident parent with the head does not qualify. + period: 2025 + input: + people: + head: + person_id: 1 + parent_1_id: 3 + age: 45 + is_tax_unit_head: true + is_surviving_spouse: true + sibling: + person_id: 2 + parent_1_id: 3 + age: 30 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + mother: + person_id: 3 + age: 70 + is_tax_unit_head: true + is_tax_unit_spouse: false + tax_units: + head_unit: + members: [head, sibling] + mother_unit: + members: [mother] + households: + household: + members: [head, sibling, mother] + output: + surviving_spouse_eligible: [false, false] + +- name: Dependent parent and grandparent named through the head's own parent ids do not qualify. + period: 2025 + input: + people: + head: + person_id: 1 + parent_1_id: 2 + age: 50 + is_tax_unit_head: true + is_surviving_spouse: true + mother: + person_id: 2 + parent_1_id: 3 + age: 75 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + grandmother: + person_id: 3 + age: 95 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, mother, grandmother] + households: + household: + members: [head, mother, grandmother] + output: + surviving_spouse_eligible: false + +- name: Dependent grandmother linked through a mother in another tax unit does not qualify. + period: 2025 + input: + people: + head: + person_id: 1 + parent_1_id: 2 + age: 45 + is_tax_unit_head: true + is_surviving_spouse: true + mother: + person_id: 2 + parent_1_id: 3 + age: 75 + is_tax_unit_head: true + is_tax_unit_spouse: false + grandmother: + person_id: 3 + age: 95 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + taxable_pension_income: 20_000 + tax_units: + head_unit: + members: [head, grandmother] + mother_unit: + members: [mother] + households: + household: + members: [head, mother, grandmother] + output: + surviving_spouse_eligible: [false, false] diff --git a/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/test_surviving_spouse_eligible_properties.py b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/test_surviving_spouse_eligible_properties.py new file mode 100644 index 00000000000..71cb8f97c72 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/household/demographic/tax_unit/test_surviving_spouse_eligible_properties.py @@ -0,0 +1,258 @@ +"""Exhaustive property checks for surviving spouse filing status. + +The relationship examples live in surviving_spouse_eligible.yaml and +filing_status.yaml. These tests check invariants over every combination on +the grid below, which named YAML cases cannot state: eligibility never +changes with the dependent's income (units grouped by everything but +income), matches a reference rule written from the grid's relationship +labels, and differs from the previous rule (read from +tax_unit_child_dependents) only for children who fail the gross income test. +Every combination is its own tax unit in one vectorized simulation on the +shared model, so mixed populations are exercised too. + +IRC 2(a)(1)(B) requires a dependent son, stepson, daughter or stepdaughter, +with dependency determined without regard to IRC 152(b)(1), (b)(2) and +(d)(1)(B), so the qualifying relative gross income test does not apply. +""" + +import itertools + +import numpy as np +import pytest + +from policyengine_us import Simulation +from policyengine_us.system import system + +PERIOD = 2025 +EXEMPTION_AMOUNT = system.parameters(PERIOD).gov.irs.income.exemption.amount + +SURVIVING_SPOUSE = [True, False] +MARRIED = [False, True] +# Under 19 (qualifying child by age), an adult child, an elderly relative. +AGES = [10, 29, 88] +INCOMES = [0, 3_000, EXEMPTION_AMOUNT - 1, EXEMPTION_AMOUNT, 6_238, 50_000] +DISABLED = [False, True] +# How the dependent's relationship to the head is described, and whether it +# makes the dependent the head's child. A parent id of 0 is unknown. "Other" +# is a relative who lives in the household and files in their own tax unit. +RELATIONSHIPS = { + # No parent ids and no relationship flags. + "unmarked": True, + # Relationship flags without parent ids. + "parent": False, + "grandparent": False, + "unrelated": False, + # The dependent's first id names the head. + "names_head": True, + # The dependent's first id names an absent parent; the second is unknown. + "names_absent_parent_only": True, + # Two absent parents named: the head can still be a step parent. + "names_two_absent_parents": True, + # The dependent names the other, who has no ids: the head can still be + # the second parent. + "names_other": True, + # The head's own id names an absent parent; the dependent has no ids. + "head_has_ids": True, + # The head's own id names the dependent. + "named_as_head_parent": False, + # The head and the dependent name the same absent parent. + "shares_parent_with_head": False, + # The dependent names the other, whose id names the head. + "parent_names_head": False, + # The dependent names the other, who shares an absent parent with the head. + "parent_shares_parent_with_head": False, +} +NEEDS_OTHER = {"names_other", "parent_names_head", "parent_shares_parent_with_head"} + +UNITS = [ + dict( + surviving=surviving, + married=married, + age=age, + income=income, + disabled=disabled, + relationship=relationship, + ) + for surviving, married, age, income, disabled, relationship in itertools.product( + SURVIVING_SPOUSE, MARRIED, AGES, INCOMES, DISABLED, RELATIONSHIPS + ) +] + [ + dict(surviving=surviving, married=married, relationship=None) + for surviving in SURVIVING_SPOUSE + for married in MARRIED +] +# Ids that name no one in the simulation. +ABSENT_ID = 10_000_000 + + +def _situation(): + year = str(PERIOD) + people, tax_units, marital_units, households = {}, {}, {}, {} + for i, unit in enumerate(UNITS): + head, spouse, dependent, other = (f"{r}_{i}" for r in ("h", "s", "d", "o")) + head_id, spouse_id, dependent_id, other_id = (4 * i + k for k in (1, 2, 3, 4)) + absent_id, second_absent_id = ABSENT_ID + 2 * i, ABSENT_ID + 2 * i + 1 + members = [head] + people[head] = { + "person_id": {year: head_id}, + "age": {year: 67}, + "is_tax_unit_head": {year: True}, + "is_tax_unit_spouse": {year: False}, + "is_surviving_spouse": {year: unit["surviving"]}, + } + if unit["married"]: + members.append(spouse) + people[spouse] = { + "person_id": {year: spouse_id}, + "age": {year: 65}, + "is_tax_unit_head": {year: False}, + "is_tax_unit_spouse": {year: True}, + } + marital_units[f"m_{i}"] = {"members": members[:]} + tax_units[f"t_{i}"] = {"members": members} + households[f"g_{i}"] = {"members": members[:]} + relationship = unit["relationship"] + if relationship is None: + continue + members.append(dependent) + households[f"g_{i}"]["members"].append(dependent) + parent_ids = { + "names_head": (head_id, 0), + "names_absent_parent_only": (absent_id, 0), + "names_two_absent_parents": (absent_id, second_absent_id), + "shares_parent_with_head": (absent_id, 0), + }.get(relationship, (other_id, 0) if relationship in NEEDS_OTHER else (0, 0)) + people[dependent] = { + "person_id": {year: dependent_id}, + "parent_1_id": {year: parent_ids[0]}, + "parent_2_id": {year: parent_ids[1]}, + "age": {year: unit["age"]}, + "is_tax_unit_head": {year: False}, + "is_tax_unit_spouse": {year: False}, + "is_tax_unit_dependent": {year: True}, + "employment_income": {year: unit["income"]}, + "is_permanently_and_totally_disabled": {year: unit["disabled"]}, + "is_parent_of_filer_or_spouse": {year: relationship == "parent"}, + "is_grandparent_of_filer_or_spouse": {year: relationship == "grandparent"}, + "is_related_to_head_or_spouse": {year: relationship != "unrelated"}, + } + marital_units[f"dm_{i}"] = {"members": [dependent]} + head_parent_id = { + "head_has_ids": absent_id, + "named_as_head_parent": dependent_id, + "shares_parent_with_head": absent_id, + "parent_shares_parent_with_head": absent_id, + }.get(relationship) + if head_parent_id is not None: + people[head]["parent_1_id"] = {year: head_parent_id} + if relationship in NEEDS_OTHER: + other_parent_id = { + "parent_names_head": head_id, + "parent_shares_parent_with_head": absent_id, + }.get(relationship, 0) + people[other] = { + "person_id": {year: other_id}, + "parent_1_id": {year: other_parent_id}, + "age": {year: 40}, + "is_tax_unit_head": {year: True}, + "is_tax_unit_spouse": {year: False}, + } + marital_units[f"om_{i}"] = {"members": [other]} + tax_units[f"ot_{i}"] = {"members": [other]} + households[f"g_{i}"]["members"].append(other) + + def copy(groups): + return {name: {"members": g["members"][:]} for name, g in groups.items()} + + return { + "people": people, + "tax_units": tax_units, + "marital_units": marital_units, + "spm_units": copy(households), + "families": copy(households), + "households": households, + } + + +def _field(name, default): + return np.array([unit.get(name, default) for unit in UNITS]) + + +@pytest.fixture(scope="module") +def grid(): + sim = Simulation(situation=_situation()) + position = {name: i for i, name in enumerate(sim.populations["tax_unit"].ids)} + order = [position[f"t_{i}"] for i in range(len(UNITS))] + + def calc(variable): + return np.asarray(sim.calculate(variable, PERIOD))[order] + + return { + "eligible": calc("surviving_spouse_eligible"), + "hoh": calc("head_of_household_eligible"), + "filing_status": sim.calculate("filing_status", PERIOD).decode_to_str()[order], + "child_dependents": calc("tax_unit_child_dependents"), + "surviving": _field("surviving", False), + "married": _field("married", False), + "has_dependent": np.array([u["relationship"] is not None for u in UNITS]), + "is_child": np.array( + [RELATIONSHIPS.get(u["relationship"], False) for u in UNITS] + ), + "age": _field("age", -1), + "income": _field("income", -1), + "disabled": _field("disabled", False), + } + + +def test_matches_reference_rule(grid): + # A surviving spouse head, not remarried, with a dependent child. + expected = grid["surviving"] & ~grid["married"] & grid["is_child"] + assert len(grid["eligible"]) == len(UNITS) + np.testing.assert_array_equal(grid["eligible"], expected) + + +def test_dependent_income_never_changes_eligibility(grid): + groups = {} + for i, unit in enumerate(UNITS): + if unit["relationship"] is not None: + key = tuple(value for name, value in unit.items() if name != "income") + groups.setdefault(key, set()).add(bool(grid["eligible"][i])) + assert len(groups) * len(INCOMES) == grid["has_dependent"].sum() + assert all(len(values) == 1 for values in groups.values()) + + +def test_non_child_dependents_never_qualify(grid): + not_child = grid["has_dependent"] & ~grid["is_child"] + assert not_child.any() + assert not grid["eligible"][not_child].any() + + +def test_eligibility_requires_unmarried_surviving_spouse_with_dependent(grid): + eligible = grid["eligible"] + assert eligible.any() + assert (~grid["has_dependent"]).sum() == len(SURVIVING_SPOUSE) * len(MARRIED) + assert grid["surviving"][eligible].all() + assert not grid["married"][eligible].any() + assert grid["has_dependent"][eligible].all() + + +def test_filing_status_follows_eligibility(grid): + eligible = grid["eligible"] + np.testing.assert_array_equal(grid["filing_status"] == "SURVIVING_SPOUSE", eligible) + assert not (eligible & grid["hoh"]).any() + + +def test_only_change_for_children_is_dropping_income_test(grid): + # Differential against the previous rule, which counted child dependents + # under is_child_dependent and so applied the gross income test. + previous = grid["surviving"] & ~grid["married"] & (grid["child_dependents"] > 0) + is_child = grid["is_child"] + # Every child unit the previous rule accepted stays eligible. + assert grid["eligible"][previous & is_child].all() + # Units added are adult, non-disabled children whose income fails the + # qualifying relative gross income test. + added = grid["eligible"] & ~previous + assert added.any() + assert (grid["age"][added] >= 19).all() + assert not grid["disabled"][added].any() + assert (grid["income"][added] >= EXEMPTION_AMOUNT).all() diff --git a/policyengine_us/variables/household/demographic/tax_unit/surviving_spouse_eligible.py b/policyengine_us/variables/household/demographic/tax_unit/surviving_spouse_eligible.py index 01ef59677b2..abf7e783058 100644 --- a/policyengine_us/variables/household/demographic/tax_unit/surviving_spouse_eligible.py +++ b/policyengine_us/variables/household/demographic/tax_unit/surviving_spouse_eligible.py @@ -1,4 +1,8 @@ from policyengine_us.model_api import * +from policyengine_us.variables.household.demographic.person._parent_links import ( + co_resident_parent_indices, + tax_unit_parent_indices, +) class surviving_spouse_eligible(Variable): @@ -6,7 +10,29 @@ class surviving_spouse_eligible(Variable): entity = TaxUnit definition_period = YEAR label = "Qualifies for surviving spouse filing status" - reference = "https://www.law.cornell.edu/uscode/text/26/2#a" + documentation = ( + "IRC 2(a)(1)(B) requires a dependent son, stepson, daughter or " + "stepdaughter, with dependency determined without regard to IRC " + "152(b)(1), (b)(2) and (d)(1)(B). The qualifying relative gross " + "income test of 152(d)(1)(B) does not apply, so an adult child with " + "gross income at or above the exemption amount still qualifies the " + "filer. A dependent is the head's child when their parent_1_id or " + "parent_2_id (natural, adoptive or step parents) names the tax unit " + "head. Because a parent id of 0 is unknown and a person can have more " + "parents than the two ids hold, naming other parents does not rule a " + "dependent out. The ids rule a dependent out only when they show " + "another relationship to the head: the head's parent or grandparent, " + "a sibling (a shared parent id), a grandchild (the dependent's parent " + "names the head) or a niece or nephew (the dependent's parent shares " + "a parent id with the head). Otherwise a dependent counts as the " + "head's child unless marked as a parent or grandparent of the filer " + "or as unrelated to the filer. Without such ids the model cannot tell " + "a child from a sibling, niece, nephew or grandchild." + ) + reference = ( + "https://www.law.cornell.edu/uscode/text/26/2#a_1_B", + "https://www.irs.gov/pub/irs-pdf/i1040gi.pdf#page=15", + ) def formula(tax_unit, period, parameters): # The surviving spouse filing status should only apply to surviving spouse heads @@ -15,6 +41,72 @@ def formula(tax_unit, period, parameters): is_head = person("is_tax_unit_head", period) is_surviving_spouse = person("is_surviving_spouse", period) surviving_spouse_head = tax_unit.any(is_head & is_surviving_spouse) - has_child_dependents = tax_unit("tax_unit_child_dependents", period) > 0 + # Without the 152(d)(1)(B) gross income test, the qualifying child, + # disabled dependent and qualifying relative paths of + # is_child_dependent together cover every tax unit dependent. + is_dependent = person("is_tax_unit_dependent", period) + + # IRC 2(a)(1)(B)(i) limits the dependent to a son, stepson, daughter + # or stepdaughter of the filer. Each person's parents, as rows: both + # id slots resolved among household members and among tax unit + # members, -1 when unresolved. + rows = np.arange(person.count) + parent_rows = [ + *co_resident_parent_indices(person, period), + *tax_unit_parent_indices(person, period), + ] + parent_ids = [person("parent_1_id", period), person("parent_2_id", period)] + + def parents_of(of): + safe = np.maximum(of, 0) + return [where(of >= 0, parent[safe], -1) for parent in parent_rows] + + def ids_of(of): + safe = np.maximum(of, 0) + return [where(of >= 0, ids[safe], 0) for ids in parent_ids] + + def shares_id(ids, other_ids): + shared = np.zeros(person.count, dtype=bool) + for a in ids: + for b in other_ids: + shared |= (a != 0) & (a == b) + return shared + + # Row of the head of each person's tax unit. + unit = person.tax_unit.reference_entity.members_entity_id + head_by_unit = np.full(tax_unit.count, -1) + head_by_unit[unit[is_head]] = rows[is_head] + head = head_by_unit[unit] + head_parents = parents_of(head) + head_ids = ids_of(head) + + names_head = np.zeros(person.count, dtype=bool) + is_head_grandchild = np.zeros(person.count, dtype=bool) + is_head_niece_or_nephew = np.zeros(person.count, dtype=bool) + for parent in parent_rows: + names_head |= (head >= 0) & (parent == head) + for grandparent in parents_of(parent): + is_head_grandchild |= (head >= 0) & (grandparent == head) + is_head_niece_or_nephew |= (parent >= 0) & shares_id( + ids_of(parent), head_ids + ) + is_head_ancestor = np.zeros(person.count, dtype=bool) + for parent in head_parents: + is_head_ancestor |= parent == rows + for grandparent in parents_of(parent): + is_head_ancestor |= grandparent == rows + is_head_sibling = shares_id(parent_ids, head_ids) + + not_child = ( + is_head_ancestor + | is_head_sibling + | is_head_grandchild + | is_head_niece_or_nephew + | person("is_parent_of_filer_or_spouse", period) + | person("is_grandparent_of_filer_or_spouse", period) + | ~person("is_related_to_head_or_spouse", period) + ) + is_child = names_head | ~not_child + has_qualifying_child = tax_unit.any(is_dependent & is_child) married = tax_unit("tax_unit_married", period) - return surviving_spouse_head & has_child_dependents & ~married + return surviving_spouse_head & has_qualifying_child & ~married