diff --git a/changelog.d/ssi-parent-deeming-household.fixed.md b/changelog.d/ssi-parent-deeming-household.fixed.md new file mode 100644 index 00000000000..1afe51439f1 --- /dev/null +++ b/changelog.d/ssi-parent-deeming-household.fixed.md @@ -0,0 +1 @@ +Deem SSI parental income (20 CFR 416.1165) from the eligible child's co-resident natural, adoptive or step parents and a parent's co-resident spouse, resolved from parent ids or the tax-unit proxy, instead of the child's tax unit. Allocate only for the child's siblings (416.1160(d)), give the couple and individual rates when two natural parents and a stepparent deem (POMS SI 01320.500 B.4), divide among the eligible children who share those parents, and treat children and parents without qualifying immigration status as not eligible for SSI. Stop subtracting the child's student earned income exclusion from the parents' earnings. Mirror this in Medicaid parental deeming, where Missouri deems only natural or adoptive parents and allocates for every child under 18 in the home (DSS Manual 0805.020.15). Replace the per-parent ssi_ineligible_parent_allocation with ssi_parental_deeming_parent_allocation on the child. diff --git a/policyengine_us/tests/core/test_ssi_parental_deeming_invariants.py b/policyengine_us/tests/core/test_ssi_parental_deeming_invariants.py new file mode 100644 index 00000000000..37b7b8272f4 --- /dev/null +++ b/policyengine_us/tests/core/test_ssi_parental_deeming_invariants.py @@ -0,0 +1,743 @@ +"""Invariants of SSI parent-to-child income deeming (20 CFR 416.1165). + +An eligible child's ineligible parents are 'a natural or adoptive parent, or +the spouse ... of a natural or adoptive parent, who lives with you and is not +eligible for SSI benefits' (416.1160(d)). Their income, less an allocation for +each ineligible child ('the natural or adopted child of your parent or of your +parent's spouse ... who lives in the same household with you'), the $20 and +earned income exclusions and the parents' living allowance, is divided among +the parents' eligible children (416.1165(b)-(e)). + +Each Hypothesis example builds a batch of random households: a mother, a +father who may live elsewhere or be absent, a stepparent married to the +mother, an aunt and a grandparent, and children of every relationship (full, +half and step siblings, cousins, unrelated children), some disabled, some +without qualifying immigration status, some living elsewhere, filed in +assorted tax units, with parent ids on everyone, no one or some people. Half +the households are in Missouri, half in the District of Columbia. It checks: + +1. Differential: is_ssi_income_deeming_child, is_ssi_ineligible_parent, + ssi_parental_deeming_child_allocations, + ssi_parental_deeming_parent_allocation and + ssi_unearned_income_deemed_from_ineligible_parent equal an oracle written + here from the regulation and the parent-link contract in + household/demographic/person/_parent_links.py, without the model. With + ids the oracle is the regulation itself; without ids it is the documented + tax-unit proxy. +2. Medicaid: the Medicaid copy equals the oracle's Medicaid variant: a + disabled child need not meet the SSI immigration condition and takes no + allocation when they share the deemed income, and Missouri deems natural + or adoptive parents only and allocates for every minor (DSS Manual + 0805.020.15). In the District, whose $20 disregard and SSI-order + exclusions match SSI's, it equals the SSI variable wherever everyone at + home is a citizen. +3. Conservation: the shares deemed to the eligible children of one set of + ineligible parents add up to those parents' net deemable income. +4. Order: listing each household's people in reverse gives the same values. +5. Monotonicity: more income for every child never lowers deemed income + (allocations shrink, 416.1163(b)(2)); more income for every adult never + lowers it. +6. Bounds: deemed income is never negative or above the parents' gross + income. + +YAML covers the worked cases. This file routes to the Rest/core group. +""" + +import numpy as np +from hypothesis import given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +YEAR = 2026 +INDIVIDUAL = 994 # 2026 Federal benefit rates +COUPLE = 1_491 +MONTHLY_ALLOCATION = COUPLE - INDIVIDUAL # 416.1163(b)(1) +GENERAL_EXCLUSION = 20 # 416.1124(c)(12); DC and MO Medicaid disregards +EARNED_EXCLUSION = 65 # 416.1112(c)(5) +CHILD_AGE = 18 +STUDENT_AGE = 22 +ABSENT_PARENT_ID = 999_999 +HOUSEHOLDS_PER_EXAMPLE = 30 +TOLERANCE = 1e-6 + +INCOME = st.sampled_from((0, 300, 1_200, 3_000)) +ADULT = st.fixed_dictionaries( + { + "earned": INCOME, + "unearned": st.sampled_from((0, 150, 900)), + "disabled": st.sampled_from((False, False, False, True)), + "citizen": st.sampled_from((True, True, True, False)), + } +) +CHILD_KINDS = ( + "full", + "maternal", + "paternal", + "step", + "half_step", + "paternal_step", + "cousin", + "unrelated", +) +CHILD = st.fixed_dictionaries( + { + "kind": st.sampled_from(CHILD_KINDS), + "age": st.integers(0, 20), + "disabled": st.booleans(), + "citizen": st.sampled_from((True, True, True, False)), + "student": st.booleans(), + "unearned": st.sampled_from((0, 150, 600)), + # About one in five lives in another household. + "away": st.sampled_from((False, False, False, False, True)), + "tax_unit": st.sampled_from( + ("family", "father", "stepparent", "aunt", "grandparent", "own") + ), + "has_ids": st.booleans(), + } +) +HOUSEHOLD = st.fixed_dictionaries( + { + "mother": ADULT, + "father": ADULT, + "father_status": st.sampled_from(("married", "unmarried", "away", "absent")), + "stepparent": st.one_of(st.none(), ADULT), + "stepparent_files_separately": st.booleans(), + # A stepparent who has left still has an allocation for his children + # at home (POMS SI 00501.010 B.2.d). + "stepparent_away": st.sampled_from((False, False, True)), + # The wife of a father who is not married to the mother. + "stepmother": st.one_of(st.none(), ADULT), + "aunt": st.one_of(st.none(), ADULT), + "grandparent": st.one_of(st.none(), ADULT), + "children": st.lists(CHILD, min_size=1, max_size=5), + "ids": st.sampled_from(("none", "all", "some")), + "report_own_children": st.booleans(), + } +) + + +def _adult(earned=0, unearned=0, disabled=False, citizen=True): + return dict(earned=earned, unearned=unearned, disabled=disabled, citizen=citizen) + + +def _child(kind, disabled=False, age=8, has_ids=True, tax_unit="family", **facts): + child = dict( + kind=kind, + age=age, + disabled=disabled, + citizen=True, + student=False, + unearned=0, + away=False, + tax_unit=tax_unit, + has_ids=has_ids, + ) + child.update(facts) + return child + + +def _blended(ids, stepmother, father_joins_family=False): + """Two natural parents at home, the mother's husband and maybe the + father's wife: three or four deemors for their child (POMS SI 01320.500 + B.4, Example F.5), and different deemor sets for half siblings.""" + return dict( + mother=_adult(earned=3_000), + father=_adult(earned=3_000, unearned=900), + father_status="unmarried", + stepparent=_adult(earned=1_200), + stepparent_files_separately=False, + stepparent_away=False, + stepmother=_adult(earned=3_000) if stepmother else None, + father_joins_family=father_joins_family, + aunt=None, + grandparent=None, + children=[ + _child("full", disabled=True), + _child("full", disabled=True, age=5), + _child("maternal", disabled=True, age=12), + _child("half_step", age=3, unearned=150), + _child("paternal_step", age=2, tax_unit="father"), + _child("cousin", age=10), + ], + ids=ids, + report_own_children=True, + ) + + +def _absent_stepparent(ids): + """The mother's husband lives elsewhere and files alone; his children + stay with her and her disabled daughter (POMS SI 00501.010 B.2.d).""" + return dict( + mother=_adult(earned=3_000), + father=_adult(), + father_status="absent", + stepparent=_adult(earned=2_000), + stepparent_files_separately=True, + stepparent_away=True, + stepmother=None, + aunt=None, + grandparent=None, + children=[ + _child("maternal", disabled=True), + _child("step", age=9), + _child("step", age=6, tax_unit="stepparent"), + _child("half_step", age=3, unearned=150), + ], + ids=ids, + report_own_children=True, + ) + + +def _mixed_status(): + """Two disabled sisters, one without qualifying immigration status: an + ineligible child for SSI, a sharer of the deemed income for Medicaid.""" + return dict( + mother=_adult(earned=3_000, unearned=900), + father=_adult(), + father_status="absent", + stepparent=None, + stepparent_files_separately=False, + stepparent_away=False, + stepmother=None, + aunt=None, + grandparent=None, + children=[ + _child("maternal", disabled=True), + _child("maternal", disabled=True, age=5, citizen=False), + _child("maternal", age=12), + ], + ids="all", + report_own_children=True, + ) + + +def _eligible_father(): + """An SSI-eligible father lives with the ineligible mother, unmarried: + their son and her son share her income (416.1165(e)(2)), and his + daughter, the first son's sister, is allocated for both.""" + return dict( + mother=_adult(earned=3_000), + father=_adult(disabled=True), + father_status="unmarried", + stepparent=None, + stepparent_files_separately=False, + stepparent_away=False, + stepmother=None, + aunt=None, + grandparent=None, + children=[ + _child("full", disabled=True), + _child("maternal", disabled=True, age=6), + _child("paternal", age=8), + ], + ids="all", + report_own_children=True, + ) + + +# Fixed households in every batch, so the rarer structures are always +# exercised: three and four deemors, with and without ids. +FIXED_HOUSEHOLDS = [ + _blended("all", stepmother=False), + _blended("all", stepmother=True), + _blended("some", stepmother=True), + _blended("none", stepmother=False), + # Without ids the proxy finds three deemors only when the father files + # in the mother's tax unit. + _blended("none", stepmother=False, father_joins_family=True), + # Each of these appears twice so that one copy lands in each state. + _absent_stepparent("all"), + _absent_stepparent("none"), + _absent_stepparent("none"), + _absent_stepparent("all"), + _mixed_status(), + _mixed_status(), + _eligible_father(), + _eligible_father(), +] +BATCH = st.lists( + HOUSEHOLD, min_size=HOUSEHOLDS_PER_EXAMPLE, max_size=HOUSEHOLDS_PER_EXAMPLE +).map(lambda households: households + FIXED_HOUSEHOLDS) + + +def _people(index, household): + """Return each person's facts: true parents, spouse, tax unit, home.""" + + def name(role): + return f"h{index}_{role}" + + status = household["father_status"] + father_present = status != "absent" + married = status == "married" + stepparent = household["stepparent"] if not married else None + stepmother = household["stepmother"] if status == "unmarried" else None + people = [] + + def adult(role, facts, tax_unit, role_in_unit, away=False, spouse=None): + people.append( + dict( + name=name(role), + age=60 if role == "grandparent" else 40, + disabled=facts["disabled"], + citizen=facts["citizen"], + student=False, + earned=facts["earned"], + unearned=facts["unearned"], + parents=(), + away=away, + tax_unit=tax_unit, + role=role_in_unit, + spouse=spouse, + has_ids=False, + ) + ) + + adult( + "mother", + household["mother"], + "family", + "head", + spouse=( + name("father") if married else (name("stepparent") if stepparent else None) + ), + ) + joins = household.get("father_joins_family", False) + if father_present: + adult( + "father", + household["father"], + "family" if married or joins else "father", + "spouse" if married else ("dependent" if joins else "head"), + away=status == "away", + spouse=( + name("mother") + if married + else (name("stepmother") if stepmother else None) + ), + ) + if stepmother: + adult( + "stepmother", + stepmother, + "father", + "spouse", + spouse=name("father"), + ) + if stepparent: + separate = household["stepparent_files_separately"] + adult( + "stepparent", + stepparent, + "stepparent" if separate else "family", + "head" if separate else "spouse", + away=household["stepparent_away"], + spouse=name("mother"), + ) + if household["aunt"]: + adult("aunt", household["aunt"], "aunt", "head") + if household["grandparent"]: + adult("grandparent", household["grandparent"], "grandparent", "head") + present = {person["name"] for person in people} + units = {person["tax_unit"] for person in people} + + def parent(role): + return name(role) if name(role) in present else None + + for position, child in enumerate(household["children"]): + parents = { + "full": (parent("mother"), parent("father")), + "maternal": (parent("mother"),), + "paternal": (parent("father"),), + "step": (parent("stepparent"),), + "half_step": (parent("mother"), parent("stepparent")), + "paternal_step": (parent("father"), parent("stepmother")), + "cousin": (parent("aunt"),), + "unrelated": (None,), + }[child["kind"]] + tax_unit = child["tax_unit"] if child["tax_unit"] in units else "family" + if child["tax_unit"] == "own": + tax_unit = f"child{position}" + people.append( + dict( + name=name(f"child{position}"), + age=child["age"], + disabled=child["disabled"], + citizen=child["citizen"], + student=child["student"], + earned=0, + unearned=child["unearned"], + # None is a parent who lives outside the simulation. + parents=parents, + away=child["away"], + tax_unit=tax_unit, + role="head" if tax_unit == f"child{position}" else "dependent", + spouse=None, + has_ids=child["has_ids"], + ) + ) + # The grandparent is the mother's and the aunt's parent. + if household["grandparent"]: + for person in people: + if person["name"] in (name("mother"), name("aunt")): + person["parents"] = (name("grandparent"),) + mode = household["ids"] + for person in people: + person["has_ids"] = bool(person["parents"]) and ( + mode == "all" or (mode == "some" and person["has_ids"]) + ) + return people + + +def _situation(households, reverse=False, child_rise=0, adult_rise=0): + plurals = ("people", "tax_units", "households", "marital_units", "families") + situation = {plural: {} for plural in plurals + ("spm_units",)} + facts = {} + for index, household in enumerate(households): + people = _people(index, household) + if reverse: + people = people[::-1] + state = "MO" if index % 2 else "DC" + ids = {p["name"]: 1_000 * (index + 1) + n + 1 for n, p in enumerate(people)} + home = { + p["name"]: f"{p['name']}_away" if p["away"] else f"h{index}_home" + for p in people + } + own_children = {} + for person in people: + for parent in person["parents"]: + if parent and home[parent] == home[person["name"]]: + own_children[parent] = own_children.get(parent, 0) + 1 + for person in people: + child = person["age"] < CHILD_AGE + parent_ids = [ + ids.get(parent, ABSENT_PARENT_ID) if parent else ABSENT_PARENT_ID + for parent in person["parents"] + ] + parent_ids = (parent_ids + [0, 0])[:2] if person["has_ids"] else [0, 0] + reported = ( + own_children.get(person["name"], 0) + if household["report_own_children"] + else 0 + ) + rise = child_rise if child else adult_rise + facts[person["name"]] = dict( + person, + household=index, + home=home[person["name"]], + state=state, + tax_unit_key=f"h{index}_{person['tax_unit']}", + reported_children=reported, + person_id=ids[person["name"]], + parent_ids=parent_ids, + earned=person["earned"] + (rise if not child else 0), + unearned=person["unearned"] + rise, + ) + f = facts[person["name"]] + situation["people"][person["name"]] = { + "age": {YEAR: person["age"]}, + "is_ssi_disabled": {YEAR: person["disabled"]}, + "immigration_status": { + YEAR: "CITIZEN" if person["citizen"] else "UNDOCUMENTED" + }, + "ssi_earned_income": {YEAR: 12 * f["earned"]}, + "ssi_unearned_income": {YEAR: 12 * f["unearned"]}, + "is_full_time_college_student": {YEAR: person["student"]}, + "is_in_k12_school": {YEAR: False}, + "person_id": {YEAR: ids[person["name"]]}, + "parent_1_id": {YEAR: parent_ids[0]}, + "parent_2_id": {YEAR: parent_ids[1]}, + "own_children_in_household": {YEAR: reported}, + "is_tax_unit_head": {YEAR: person["role"] == "head"}, + "is_tax_unit_spouse": {YEAR: person["role"] == "spouse"}, + "is_tax_unit_dependent": {YEAR: person["role"] == "dependent"}, + } + couple = person["spouse"] + units = { + "tax_units": f["tax_unit_key"], + "households": home[person["name"]], + "marital_units": ( + "_".join(sorted((person["name"], couple))) + if couple + else f"{person['name']}_mu" + ), + } + units["families"] = units["households"] + units["spm_units"] = units["households"] + for plural, key in units.items(): + group = situation[plural].setdefault(key, {"members": []}) + group["members"].append(person["name"]) + if plural == "households": + group["state_code"] = {YEAR: state} + return situation, facts + + +def _countable(earned, unearned): + """416.1165(d)(1)-(2): the $20 and earned income exclusions, monthly.""" + general = min(GENERAL_EXCLUSION, unearned) + earned_left = max(0, earned - EARNED_EXCLUSION - (GENERAL_EXCLUSION - general)) + return unearned - general + earned_left / 2 + + +def _oracle(facts, medicaid=False): + """Every person's outputs from true relationships and the id contract. + + medicaid gives the Medicaid copy: a disabled child need not meet the SSI + immigration condition, a disabled child who shares the deemed income + takes no allocation, and Missouri households follow DSS Manual + 0805.020.15 (natural or adoptive parents only, an allocation for every + minor). + """ + names = list(facts) + + def co_resident(a, b): + return facts[a]["home"] == facts[b]["home"] + + def spouse(person): + partner = facts[person]["spouse"] + return partner if partner and co_resident(person, partner) else None + + def named_parents(person): + """Parents an id names who live with the person.""" + if not facts[person]["has_ids"]: + return set() + return { + parent + for parent in facts[person]["parents"] + if parent and co_resident(parent, person) + } + + def household_linked(person): + return any( + facts[other]["has_ids"] for other in names if co_resident(other, person) + ) + + def unlinked(person): + linked = sum(person in named_parents(other) for other in names) + own = facts[person]["reported_children"] + return (own > 0 or linked > 0) and (linked == 0 or own > linked) + + def eligible(person): + """The SSI status conditions: aged, blind or disabled and a citizen.""" + return facts[person]["disabled"] and facts[person]["citizen"] + + def deemors(person, with_spouses): + if facts[person]["has_ids"]: + parents = named_parents(person) + if with_spouses: + parents |= {spouse(p) for p in parents if spouse(p)} + return parents + # The tax-unit proxy: every co-resident unlinked parent in the + # person's tax unit, the person included, and each one's spouse. + unit = facts[person]["tax_unit_key"] + found = set() + for member in names: + if facts[member]["tax_unit_key"] != unit or not co_resident(member, person): + continue + partner = spouse(member) + if unlinked(member) or ( + with_spouses and partner is not None and unlinked(partner) + ): + found.add(member) + if with_spouses and partner is not None: + found.add(partner) + return found + + def allocation(child): + person = facts[child] + is_child = person["age"] < CHILD_AGE or ( + person["age"] < STUDENT_AGE and person["student"] + ) + if not is_child or eligible(child): + return 0 + return max(0, MONTHLY_ALLOCATION - person["unearned"]) + + def siblings(person, parents, every_minor): + relatives = parents | {person} + # The parents' spouses who live elsewhere: their children at home + # are still allocated (POMS SI 00501.010 B.2.d). + absent = { + facts[p]["spouse"] + for p in parents + if facts[p]["spouse"] and not co_resident(p, facts[p]["spouse"]) + } + presume = not household_linked(person) or any(map(unlinked, relatives)) + units = {facts[r]["tax_unit_key"] for r in relatives} + # An absent spouse lends only his own tax unit, and in a household + # with links only when he is an unlinked parent. + absent_units = { + facts[a]["tax_unit_key"] + for a in absent + if not household_linked(person) or unlinked(a) + } + found = set() + for other in names: + if other == person or other in parents or not co_resident(other, person): + continue + if every_minor: + counts = facts[other]["age"] < CHILD_AGE + elif facts[other]["has_ids"]: + counts = bool(named_parents(other) & relatives) or bool( + set(facts[other]["parents"]) & absent + ) + else: + unit = facts[other]["tax_unit_key"] + counts = (presume and unit in units) or unit in absent_units + if counts: + found.add(other) + return found + + rows = {} + for person in names: + missouri = medicaid and facts[person]["state"] == "MO" + parents = deemors(person, with_spouses=not missouri) + ineligible = {p for p in parents if p != person and not eligible(p)} + status = facts[person]["disabled"] if medicaid else eligible(person) + applies = facts[person]["age"] < CHILD_AGE and status and bool(ineligible) + rows[person] = dict( + parents=frozenset(parents), + ineligible=ineligible, + applies=applies, + missouri=missouri, + ) + + # 416.1165(e)(2) divides the ineligible parents' income among the + # eligible children who share those parents, and the allocations are for + # every ineligible child of that deeming household. + def pool(person): + return [ + other + for other, r in rows.items() + if r["applies"] + and r["ineligible"] == rows[person]["ineligible"] + and facts[other]["household"] == facts[person]["household"] + ] + + for person, row in rows.items(): + ineligible = row["ineligible"] + if not row["applies"]: + row.update(child_allocations=0, parent_allocation=0, net=0) + continue + allocated = set().union( + *( + siblings(member, rows[member]["parents"], rows[member]["missouri"]) + for member in pool(person) + ) + ) + child_allocations = sum( + allocation(s) + for s in allocated + if s != person and not (medicaid and rows[s]["applies"]) + ) + n = len(ineligible) + parent_allocation = COUPLE * (n // 2) + INDIVIDUAL * (n % 2) + unearned = sum(facts[p]["unearned"] for p in ineligible) + earned = sum(facts[p]["earned"] for p in ineligible) + left = max(0, child_allocations - unearned) + net = max( + 0, + _countable(max(0, earned - left), max(0, unearned - child_allocations)) + - parent_allocation, + ) + row.update( + child_allocations=child_allocations, + parent_allocation=parent_allocation, + net=net, + gross=unearned + earned, + ) + for person, row in rows.items(): + row["deemed"] = row["net"] / len(pool(person)) if row["applies"] else 0 + for person in names: + rows[person]["ineligible_parent"] = any( + r["applies"] and person in r["ineligible"] for r in rows.values() + ) + return rows + + +SSI_VARIABLES = ( + "is_ssi_income_deeming_child", + "is_ssi_ineligible_parent", + "ssi_parental_deeming_child_allocations", + "ssi_parental_deeming_parent_allocation", + "ssi_unearned_income_deemed_from_ineligible_parent", +) +MEDICAID = ( + "medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent" +) + + +def _calculate(situation, variables): + simulation = Simulation(situation=situation) + names = list(situation["people"]) + return { + variable: dict(zip(names, simulation.calculate(variable, YEAR))) + for variable in variables + } + + +def _close(a, b): + return abs(float(a) - float(b)) <= TOLERANCE + + +@settings(max_examples=3, derandomize=True, deadline=None) +@given(BATCH) +def test_parental_deeming_matches_oracle_and_invariants(households): + situation, facts = _situation(households) + results = _calculate(situation, SSI_VARIABLES + (MEDICAID,)) + oracle = _oracle(facts) + medicaid_oracle = _oracle(facts, medicaid=True) + deemed = results["ssi_unearned_income_deemed_from_ineligible_parent"] + + for name, row in oracle.items(): + # 1. Differential against the oracle. + assert results["is_ssi_income_deeming_child"][name] == row["applies"], name + assert results["is_ssi_ineligible_parent"][name] == row["ineligible_parent"], ( + name + ) + assert _close( + results["ssi_parental_deeming_child_allocations"][name], + 12 * row["child_allocations"], + ), name + assert _close( + results["ssi_parental_deeming_parent_allocation"][name], + 12 * row["parent_allocation"], + ), name + assert _close(deemed[name], 12 * row["deemed"]), name + # 2. The Medicaid copy: Missouri's rules in Missouri, SSI's in DC, + # where it equals SSI deeming whenever everyone at home is a citizen. + assert _close(results[MEDICAID][name], 12 * medicaid_oracle[name]["deemed"]), ( + name + ) + home = [n for n in facts if facts[n]["home"] == facts[name]["home"]] + if facts[name]["state"] == "DC" and all(facts[n]["citizen"] for n in home): + assert _close(results[MEDICAID][name], deemed[name]), name + # 6. Bounds. + assert deemed[name] >= 0 + if row["applies"]: + assert deemed[name] <= 12 * row["gross"] + TOLERANCE + + # 3. Conservation: each set of parents' net income is deemed in full. + pools = {} + for name, row in oracle.items(): + if row["applies"]: + key = (facts[name]["household"], frozenset(row["ineligible"])) + pools.setdefault(key, []).append(name) + for members in pools.values(): + total = sum(deemed[name] for name in members) + assert _close(total, 12 * oracle[members[0]]["net"]), members + + # 4. Order of people. + reversed_situation, _ = _situation(households, reverse=True) + reversed_results = _calculate(reversed_situation, SSI_VARIABLES + (MEDICAID,)) + for variable, values in results.items(): + for name, value in values.items(): + assert _close(reversed_results[variable][name], value), (variable, name) + + # 5. More income for children (smaller allocations) or for adults never + # lowers deemed income. + for rise in (dict(child_rise=100), dict(adult_rise=100)): + richer_situation, _ = _situation(households, **rise) + richer = _calculate( + richer_situation, ("ssi_unearned_income_deemed_from_ineligible_parent",) + )["ssi_unearned_income_deemed_from_ineligible_parent"] + assert all(richer[name] >= deemed[name] - TOLERANCE for name in deemed) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.yaml index 985a44b41e4..eeeb9c3a175 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.yaml @@ -85,3 +85,293 @@ # the parents' living allowance (1,491 x 12 = 17,892, the couple SSI # federal benefit rate) -> 19,098, split equally -> 9,549 each. medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: [9_549, 9_549, 0, 0] + +# DSS Manual 0805.020.15 Example #1 (2022 SSI maximums: $841 for one, +# $1,261 for two). Mrs. Boulanger applies for MHDC for Marie; Jane has no +# income. +- name: Case 4, Missouri manual Example 1, the Boulangers. + period: 2022 + absolute_error_margin: 0.01 + input: + people: + mrs_boulanger: + age: 40 + own_children_in_household: 2 + ssi_unearned_income: 441 * 12 + ssi_earned_income: 2_297 * 12 + marie: + age: 12 + is_ssi_disabled: true + ssi_unearned_income: 140 * 12 + jane: + age: 10 + tax_units: + tax_unit: + members: [mrs_boulanger, marie, jane] + marital_units: + mrs_boulanger_alone: + members: [mrs_boulanger] + marie_alone: + members: [marie] + jane_alone: + members: [jane] + households: + household: + members: [mrs_boulanger, marie, jane] + state_code: MO + output: + # $441 - $420 for Jane = $21; - $20 = $1. $2,297 - $65 = $2,232; half = + # $1,116; + $1 = $1,117; less $841 = $276 deemed to Marie. + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: [0, 3_312, 0] + +# DSS Manual 0805.020.15 Example #2 (2022). Cindy has $258 of OASDI; +# Denise has no income. +- name: Case 5, Missouri manual Example 2, the Johnsons. + period: 2022 + absolute_error_margin: 0.01 + input: + people: + mr_johnson: + age: 42 + person_id: 1 + ssi_unearned_income: 452 * 12 + mrs_johnson: + age: 40 + person_id: 2 + ssi_earned_income: 2_382 * 12 + kay: + age: 12 + is_ssi_disabled: true + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + cindy: + age: 10 + person_id: 4 + parent_1_id: 1 + parent_2_id: 2 + ssi_unearned_income: 258 * 12 + denise: + age: 8 + person_id: 5 + parent_1_id: 1 + parent_2_id: 2 + tax_units: + tax_unit: + members: [mr_johnson, mrs_johnson, kay, cindy, denise] + marital_units: + parents: + members: [mr_johnson, mrs_johnson] + kay_alone: + members: [kay] + cindy_alone: + members: [cindy] + denise_alone: + members: [denise] + households: + household: + members: [mr_johnson, mrs_johnson, kay, cindy, denise] + state_code: MO + output: + # Allocations $162 + $420 = $582 exceed the $452 unearned income by $130. + # $2,382 - $130 = $2,252; - $20 - $65 = $2,167; half = $1,083.50, below + # the $1,261 allowance for two parents: nothing is deemed. + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: [0, 0, 0, 0, 0] + +# 0805.020.15: 'Do not deem the income of any other persons in the home, +# including a stepparent.' The child's id names his mother; she is married +# to his stepfather. Federal SSI deeming includes the stepparent +# (20 CFR 416.1160(d)). +- name: Case 6, Missouri deems the mother but not her husband when parent ids are set. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + child: + age: 10 + is_ssi_disabled: true + person_id: 1 + parent_1_id: 2 + parent_2_id: 99 + mother: + age: 35 + person_id: 2 + ssi_earned_income: 48_000 + stepfather: + age: 38 + person_id: 3 + ssi_earned_income: 120_000 + tax_units: + tax_unit: + members: [child, mother, stepfather] + marital_units: + child_alone: + members: [child] + couple: + members: [mother, stepfather] + households: + household: + members: [child, mother, stepfather] + state_code: MO + output: + # Mother only: ($4,000 - $20 - $65) / 2 = $1,957.50, less $994 = + # $963.50 a month. + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: [11_562, 0, 0] + # SSI: ($14,000 - $85) / 2 = $6,957.50, less the $1,491 couple rate = + # $5,466.50 a month. + ssi_unearned_income_deemed_from_ineligible_parent: [65_598, 0, 0] + +- name: Case 7, the same family in the District of Columbia deems both as SSI does. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + child: + age: 10 + is_ssi_disabled: true + person_id: 1 + parent_1_id: 2 + parent_2_id: 99 + mother: + age: 35 + person_id: 2 + ssi_earned_income: 48_000 + stepfather: + age: 38 + person_id: 3 + ssi_earned_income: 120_000 + tax_units: + tax_unit: + members: [child, mother, stepfather] + marital_units: + child_alone: + members: [child] + couple: + members: [mother, stepfather] + households: + household: + members: [child, mother, stepfather] + state_code: DC + output: + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: [65_598, 0, 0] + ssi_unearned_income_deemed_from_ineligible_parent: [65_598, 0, 0] + +# 0805.020.15 step 1 allocates for 'each minor child (under age 18) who is +# living in the home and who is not applying for MA', not only siblings. +# Federal SSI allocates only for the child's siblings (20 CFR 416.1160(d)). +- name: Case 8, Missouri allocates for a cousin under 18 in the home. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + mother: + age: 35 + person_id: 1 + ssi_earned_income: 3_000 * 12 + child: + age: 8 + is_ssi_disabled: true + person_id: 2 + parent_1_id: 1 + cousin: + age: 10 + person_id: 3 + parent_1_id: 50 + tax_units: + tax_unit: + members: [mother, child, cousin] + marital_units: + mother_alone: + members: [mother] + child_alone: + members: [child] + cousin_alone: + members: [cousin] + households: + household: + members: [mother, child, cousin] + state_code: MO + output: + # $3,000 - $497 = $2,503; - $85 = $2,418; half = $1,209; less $994 = + # $215 a month. + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: [0, 2_580, 0] + # SSI: $3,000 - $85 = $2,915; half = $1,457.50; less $994 = $463.50. + ssi_unearned_income_deemed_from_ineligible_parent: [0, 5_562, 0] + +# Medicaid applies its own immigration rules, and states may cover lawfully +# residing children (42 U.S.C. 1396b(v)(4)). A disabled child who is a lawful +# permanent resident without 40 qualifying quarters fails the SSI +# qualified-noncitizen condition, so SSI deems nothing to him, but his +# parent's income still counts toward Medicaid's SSI-methodology budget. +- name: Case 9, a lawful permanent resident child who fails the SSI immigration condition. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + person_id: 1 + ssi_earned_income: 48_000 + child: + age: 10 + is_ssi_disabled: true + immigration_status: LEGAL_PERMANENT_RESIDENT + ssi_qualifying_quarters_earnings: 0 + person_id: 2 + parent_1_id: 1 + tax_units: + tax_unit: + members: [parent, child] + marital_units: + parent_alone: + members: [parent] + child_alone: + members: [child] + households: + household: + members: [parent, child] + state_code: DC + output: + # ($4,000 - $20 - $65) / 2 = $1,957.50, less $994 = $963.50 a month. + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: [0, 11_562] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 0] + +# Deeming counts another person's income (20 CFR 416.1160(a)). Without parent +# ids, the tax-unit proxy keeps a teen mother in her own parent set; she +# must still never deem her own income to herself. +- name: Case 10, a disabled immigrant teen mother does not deem her own income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + father: + age: 45 + own_children_in_household: 1 + ssi_earned_income: 48_000 + kay: + age: 16 + is_ssi_disabled: true + immigration_status: LEGAL_PERMANENT_RESIDENT + ssi_qualifying_quarters_earnings: 0 + own_children_in_household: 1 + ssi_unearned_income: 3_600 + baby: + age: 0 + tax_units: + tax_unit: + members: [father, kay, baby] + marital_units: + father_alone: + members: [father] + kay_alone: + members: [kay] + baby_alone: + members: [baby] + households: + household: + members: [father, kay, baby] + state_code: DC + output: + # $4,000 - $497 for the baby = $3,503; - $85 = $3,418; half = $1,709; + # less $994 for one parent = $715 a month. + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: [0, 8_580, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/ssi_unearned_income_deemed_from_ineligible_parent.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/ssi_unearned_income_deemed_from_ineligible_parent.yaml new file mode 100644 index 00000000000..c843d9273b0 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/ssi_unearned_income_deemed_from_ineligible_parent.yaml @@ -0,0 +1,1370 @@ +# 20 CFR 416.1165 deems the income of an eligible child's ineligible parents +# to the child. An ineligible parent is 'a natural or adoptive parent, or the +# spouse ... of a natural or adoptive parent, who lives with you and is not +# eligible for SSI benefits' (416.1160(d)). The steps (416.1165(a)-(e), POMS +# SI 01320.500 C): +# 1. Deduct an allocation for each ineligible child: 'the natural or adopted +# child of your parent or of your parent's spouse ... who lives in the +# same household with you, and is not eligible for SSI benefits' +# (416.1160(d), 416.1165(b)), from unearned income first, then earned. +# 2. Apply the $20 general exclusion, then $65 and one-half of the rest of +# earned income (416.1165(d)(1)-(2)). +# 3. Subtract the living allowance: the couple rate for two parents, the +# individual rate for one, and both rates for two natural parents and a +# stepparent (416.1165(d)(3), POMS SI 01320.500 B.4). +# 4. Divide equally among the parents' eligible children under 18 in the +# household (416.1165(e)(2)). +# Federal benefit rates: 2025 individual $967, couple $1,450, so the +# ineligible child allocation is $483 a month. + +- name: POMS SI 01320.500 F.1, Bill Sanders, parents with only earned income + # Earnest earns $4,500 in January 2025; Bill has two ineligible siblings. + # $4,500 - 2 x $483 = $3,534; - $85 = $3,449; half = $1,724.50; less the + # $1,450 couple allowance = $274.50 deemed a month. The POMS text then + # calls Bill ineligible, which does not follow from its own figures: $274.50 + # less $20 is $254.50, below the $967 rate. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + earnest: + age: 40 + person_id: 1 + ssi_earned_income: 4_500 * 12 + sarah: + age: 38 + person_id: 2 + bill: + age: 10 + is_ssi_disabled: true + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + first_sibling: + age: 8 + person_id: 4 + parent_1_id: 1 + parent_2_id: 2 + second_sibling: + age: 6 + person_id: 5 + parent_1_id: 1 + parent_2_id: 2 + tax_units: + tax_unit: + members: [earnest, sarah, bill, first_sibling, second_sibling] + marital_units: + parents: + members: [earnest, sarah] + bill_alone: + members: [bill] + first_sibling_alone: + members: [first_sibling] + second_sibling_alone: + members: [second_sibling] + households: + household: + members: [earnest, sarah, bill, first_sibling, second_sibling] + state_code: TX + output: + is_ssi_income_deeming_child: [false, false, true, false, false] + is_ssi_ineligible_parent: [true, true, false, false, false] + ssi_parental_deeming_child_allocations: [0, 0, 11_592, 0, 0] # 2 x $483 x 12 + ssi_parental_deeming_parent_allocation: [0, 0, 17_400, 0, 0] # $1,450 x 12 + ssi_unearned_income_deemed_from_ineligible_parent: [0, 0, 3_294, 0, 0] # $274.50 x 12 + ssi_countable_income: [0, 0, 3_054, 0, 0] # ($274.50 - $20) x 12 + +- name: POMS SI 01320.500 F.2, Nancy Weaver, parents with only unearned income + # $1,000 + $1,124 = $2,124; less $483 for her brother = $1,641; - $20 = + # $1,621; less $1,450 = $171 deemed a month; $151 countable. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + chris: + age: 45 + person_id: 1 + ssi_unearned_income: 1_000 * 12 + deanna: + age: 44 + person_id: 2 + ssi_unearned_income: 1_124 * 12 + nancy: + age: 12 + is_ssi_disabled: true + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + brother: + age: 10 + person_id: 4 + parent_1_id: 1 + parent_2_id: 2 + tax_units: + tax_unit: + members: [chris, deanna, nancy, brother] + marital_units: + parents: + members: [chris, deanna] + nancy_alone: + members: [nancy] + brother_alone: + members: [brother] + households: + household: + members: [chris, deanna, nancy, brother] + state_code: TX + output: + ssi_parental_deeming_child_allocations: [0, 0, 5_796, 0] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 0, 2_052, 0] # $171 x 12 + ssi_countable_income: [0, 0, 1_812, 0] # $151 x 12 + +- name: POMS SI 01320.500 F.3, James and Janet Johnson, two eligible children + # Lucille's $1,295 - $20 = $1,275; less $967 = $308, divided by 2 = $154 + # each; $134 countable each. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + lucille: + age: 35 + person_id: 1 + ssi_unearned_income: 1_295 * 12 + james: + age: 9 + is_ssi_disabled: true + person_id: 2 + parent_1_id: 1 + janet: + age: 7 + is_ssi_disabled: true + person_id: 3 + parent_1_id: 1 + tax_units: + tax_unit: + members: [lucille, james, janet] + marital_units: + lucille_alone: + members: [lucille] + james_alone: + members: [james] + janet_alone: + members: [janet] + households: + household: + members: [lucille, james, janet] + state_code: TX + output: + ssi_parental_deeming_parent_allocation: [0, 11_604, 11_604] # $967 x 12 + ssi_unearned_income_deemed_from_ineligible_parent: [0, 1_848, 1_848] # $154 x 12 + ssi_countable_income: [0, 1_608, 1_608] # $134 x 12 + +- name: A Johnson family where Janet is a disabled child who is not a qualified noncitizen + # Lucille has $2,000 of unearned income. Janet 'is not eligible for SSI + # benefits', so she is an ineligible child (416.1160(d)): she takes a $483 + # allocation and no share of the deemed income (416.1165(e)(2)). $2,000 - + # $483 = $1,517; - $20 = $1,497; less $967 = $530, all deemed to James. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + lucille: + age: 35 + person_id: 1 + ssi_unearned_income: 2_000 * 12 + james: + age: 9 + is_ssi_disabled: true + person_id: 2 + parent_1_id: 1 + janet: + age: 7 + is_ssi_disabled: true + immigration_status: UNDOCUMENTED + person_id: 3 + parent_1_id: 1 + tax_units: + tax_unit: + members: [lucille, james, janet] + marital_units: + lucille_alone: + members: [lucille] + james_alone: + members: [james] + janet_alone: + members: [janet] + households: + household: + members: [lucille, james, janet] + state_code: TX + output: + is_ssi_income_deeming_child: [false, true, false] + is_ssi_ineligible_child: [false, false, true] + ssi_parental_deeming_child_allocations: [0, 5_796, 0] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 6_360, 0] # $530 x 12 + +- name: POMS SI 01320.500 F.5, Tommy Robinson, two natural parents and a stepparent + # Tommy and his parent Willie live in the household of Tommy's other + # parent, Jasmine, her husband Jacob and the Craigs' two children. + # Willie's $1,242 less 2 x $483 = $276; - $20 = $256. The Craigs' $4,474 + # earned - $65 = $4,409; half = $2,204.50. $2,460.50 less the couple rate + # plus the individual rate ($1,450 + $967 = $2,417) = $43.50 deemed a + # month; $23.50 countable. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + willie: + age: 40 + person_id: 1 + ssi_unearned_income: 1_242 * 12 + tommy: + age: 11 + is_ssi_disabled: true + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + jasmine: + age: 38 + person_id: 2 + ssi_earned_income: 2_274 * 12 + jacob: + age: 39 + person_id: 4 + ssi_earned_income: 2_200 * 12 + first_craig_child: + age: 6 + person_id: 5 + parent_1_id: 2 + parent_2_id: 4 + second_craig_child: + age: 4 + person_id: 6 + parent_1_id: 2 + parent_2_id: 4 + tax_units: + robinsons: + members: [willie, tommy] + craigs: + members: [jasmine, jacob, first_craig_child, second_craig_child] + marital_units: + willie_alone: + members: [willie] + tommy_alone: + members: [tommy] + craigs: + members: [jasmine, jacob] + first_craig_child_alone: + members: [first_craig_child] + second_craig_child_alone: + members: [second_craig_child] + households: + household: + members: + [willie, tommy, jasmine, jacob, first_craig_child, second_craig_child] + state_code: TX + output: + is_ssi_ineligible_parent: [true, false, true, true, false, false] + ssi_parental_deeming_child_allocations: [0, 11_592, 0, 0, 0, 0] + ssi_parental_deeming_parent_allocation: [0, 29_004, 0, 0, 0, 0] # ($1,450 + $967) x 12 + ssi_unearned_income_deemed_from_ineligible_parent: [0, 522, 0, 0, 0, 0] # $43.50 x 12 + ssi_countable_income: [0, 282, 0, 0, 0, 0] # $23.50 x 12 + +- name: POMS SI 01320.550 D.3, Henry Walden, in the month his mother died + # Nicolas's $1,368 pension - $20 = $1,348; Theresa's $329 wages - $65 = + # $264, half = $132; $1,480 less $1,450 = $30 deemed; $10 countable. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + nicolas: + age: 50 + person_id: 1 + ssi_unearned_income: 1_368 * 12 + theresa: + age: 48 + person_id: 2 + ssi_earned_income: 329 * 12 + henry: + age: 14 + is_ssi_disabled: true + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + tax_units: + tax_unit: + members: [nicolas, theresa, henry] + marital_units: + parents: + members: [nicolas, theresa] + henry_alone: + members: [henry] + households: + household: + members: [nicolas, theresa, henry] + state_code: TX + output: + ssi_unearned_income_deemed_from_ineligible_parent: [0, 0, 360] # $30 x 12 + ssi_countable_income: [0, 0, 120] # $10 x 12 + +- name: POMS SI 01320.550 D.3, Henry Walden, after his mother's death + # 'Use only the surviving parent's income ... and give one parental + # allocation': $1,378 - $20 = $1,358; less $967 = $391 deemed; $371 + # countable. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + nicolas: + age: 50 + person_id: 1 + ssi_unearned_income: 1_378 * 12 + henry: + age: 14 + is_ssi_disabled: true + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + tax_units: + tax_unit: + members: [nicolas, henry] + marital_units: + nicolas_alone: + members: [nicolas] + henry_alone: + members: [henry] + households: + household: + members: [nicolas, henry] + state_code: TX + output: + ssi_parental_deeming_parent_allocation: [0, 11_604] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 4_692] # $391 x 12 + ssi_countable_income: [0, 4_452] # $371 x 12 + +- name: POMS SI 01320.550 D.7, Ben deems only from his mother Kay before the adoption + # Kay, an unmarried 15-year-old, lives with her parents and her SSI-eligible + # son Ben. Before Kay's parents adopt Ben, they are not his parents, so + # only Kay's income (none) is deemed. Treating the household's tax unit as + # Ben's parents would deem $766 a month of his grandfather's wages. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + grandfather: + age: 45 + person_id: 1 + ssi_earned_income: 5_000 * 12 + grandmother: + age: 43 + person_id: 2 + kay: + age: 15 + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + ben: + age: 0 + is_ssi_disabled: true + person_id: 4 + parent_1_id: 3 + tax_units: + tax_unit: + members: [grandfather, grandmother, kay, ben] + marital_units: + grandparents: + members: [grandfather, grandmother] + kay_alone: + members: [kay] + ben_alone: + members: [ben] + households: + household: + members: [grandfather, grandmother, kay, ben] + state_code: TX + output: + is_ssi_ineligible_parent: [false, false, true, false] + is_ssi_income_deeming_child: [false, false, false, true] + # Kay is Ben's parent, not his sister. + ssi_parental_deeming_child_allocations: [0, 0, 0, 0] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 0, 0, 0] + +- name: POMS SI 01320.550 D.7, Ben after his grandparents adopt him + # 'the amount of income deemed from Kay's parents to Ben is reduced due to + # an ineligible child allocation for another child in the household + # (Kay)': $5,000 - $483 = $4,517; - $85 = $4,432; half = $2,216; less + # $1,450 = $766 deemed a month. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + grandfather: + age: 45 + person_id: 1 + ssi_earned_income: 5_000 * 12 + grandmother: + age: 43 + person_id: 2 + kay: + age: 15 + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + ben: + age: 0 + is_ssi_disabled: true + person_id: 4 + parent_1_id: 1 + parent_2_id: 2 + tax_units: + tax_unit: + members: [grandfather, grandmother, kay, ben] + marital_units: + grandparents: + members: [grandfather, grandmother] + kay_alone: + members: [kay] + ben_alone: + members: [ben] + households: + household: + members: [grandfather, grandmother, kay, ben] + state_code: TX + output: + is_ssi_ineligible_parent: [true, true, false, false] + ssi_parental_deeming_child_allocations: [0, 0, 0, 5_796] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 0, 0, 9_192] # $766 x 12 + +- name: A cousin in the parent's tax unit is not an ineligible child + # Only 'the natural or adopted child of your parent or of your parent's + # spouse' is allocated (416.1160(d)). The cousin's ids name an aunt who + # lives elsewhere. $2,500 - $85 = $2,415; half = $1,207.50; less $967 = + # $240.50 deemed. Allocating for the cousin would leave nothing to deem. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + mother: + age: 35 + person_id: 1 + ssi_earned_income: 2_500 * 12 + child: + age: 8 + is_ssi_disabled: true + person_id: 2 + parent_1_id: 1 + parent_2_id: 99 + cousin: + age: 10 + person_id: 3 + parent_1_id: 50 + tax_units: + tax_unit: + members: [mother, child, cousin] + marital_units: + mother_alone: + members: [mother] + child_alone: + members: [child] + cousin_alone: + members: [cousin] + households: + household: + members: [mother, child, cousin] + state_code: TX + output: + ssi_parental_deeming_child_allocations: [0, 0, 0] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 2_886, 0] # $240.50 x 12 + +- name: A half-sibling claimed by a co-resident grandmother is an ineligible child + # The child lives with his father, his stepmother and their son, whom the + # grandmother claims. Both are ineligible parents (couple rate), and their + # son is allocated wherever he files. $4,000 - $483 = $3,517; - $85 = + # $3,432; half = $1,716; less $1,450 = $266 deemed. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + father: + age: 40 + person_id: 1 + ssi_earned_income: 4_000 * 12 + stepmother: + age: 36 + person_id: 2 + child: + age: 9 + is_ssi_disabled: true + person_id: 3 + parent_1_id: 1 + parent_2_id: 99 + half_brother: + age: 3 + person_id: 4 + parent_1_id: 1 + parent_2_id: 2 + grandmother: + age: 65 + person_id: 5 + tax_units: + parents: + members: [father, stepmother, child] + grandmother: + members: [grandmother, half_brother] + marital_units: + couple: + members: [father, stepmother] + child_alone: + members: [child] + half_brother_alone: + members: [half_brother] + grandmother_alone: + members: [grandmother] + households: + household: + members: [father, stepmother, child, half_brother, grandmother] + state_code: TX + output: + is_ssi_ineligible_parent: [true, true, false, false, false] + ssi_parental_deeming_child_allocations: [0, 0, 5_796, 0, 0] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 0, 3_192, 0, 0] # $266 x 12 + +- name: Without parent ids a stepparent married to the child's parent deems + # 416.1160(d) includes 'the spouse ... of a natural or adoptive parent, who + # lives with you'. The mother reports her child; her husband does not. + # $1,500 + $2,500 = $4,000; - $85 = $3,915; half = $1,957.50; less $1,450 + # = $507.50 deemed. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + mother: + age: 35 + own_children_in_household: 1 + ssi_earned_income: 1_500 * 12 + stepfather: + age: 37 + ssi_earned_income: 2_500 * 12 + child: + age: 9 + is_ssi_disabled: true + tax_units: + tax_unit: + members: [mother, stepfather, child] + marital_units: + couple: + members: [mother, stepfather] + child_alone: + members: [child] + households: + household: + members: [mother, stepfather, child] + state_code: TX + output: + is_ssi_ineligible_parent: [true, true, false] + ssi_parental_deeming_parent_allocation: [0, 0, 17_400] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 0, 6_090] # $507.50 x 12 + +- name: A stepparent does not deem once the natural parent has left the household + # POMS SI 01320.500 A.1: a stepparent's income is deemed 'only when the + # natural or adoptive parent also lives in the household'; SI 01320.550 + # B.8. The child's id names his mother, who lives elsewhere; the + # stepfather reports him as his own child. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + stepfather: + age: 37 + person_id: 1 + own_children_in_household: 1 + ssi_earned_income: 3_000 * 12 + child: + age: 9 + is_ssi_disabled: true + person_id: 2 + parent_1_id: 99 + tax_units: + tax_unit: + members: [stepfather, child] + marital_units: + stepfather_alone: + members: [stepfather] + child_alone: + members: [child] + households: + household: + members: [stepfather, child] + state_code: TX + output: + is_ssi_ineligible_parent: [false, false] + is_ssi_income_deeming_child: [false, false] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 0] + +- name: A parent who claims the child but lives elsewhere does not deem + # 416.1160(d): an ineligible parent 'lives with you'. Without ids, the + # father who claims the child is in her tax unit but another household; + # her mother, who has no income, is the only ineligible parent. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + mother: + age: 33 + own_children_in_household: 1 + father: + age: 35 + own_children_in_household: 1 + ssi_earned_income: 4_000 * 12 + child: + age: 9 + is_ssi_disabled: true + tax_units: + tax_unit: + members: [father, child, mother] + marital_units: + mother_alone: + members: [mother] + father_alone: + members: [father] + child_alone: + members: [child] + households: + home: + members: [mother, child] + state_code: TX + elsewhere: + members: [father] + state_code: TX + output: + is_ssi_ineligible_parent: [true, false, false] + ssi_parental_deeming_parent_allocation: [0, 0, 11_604] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 0, 0] + +- name: A disabled parent who is not a qualified noncitizen is an ineligible parent + # She is not eligible for SSI, so her income is deemed (416.1160(d)). + # $1,500 - $20 = $1,480; less $967 = $513 deemed. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + mother: + age: 40 + is_ssi_disabled: true + immigration_status: UNDOCUMENTED + person_id: 1 + ssi_unearned_income: 1_500 * 12 + child: + age: 9 + is_ssi_disabled: true + person_id: 2 + parent_1_id: 1 + tax_units: + tax_unit: + members: [mother, child] + marital_units: + mother_alone: + members: [mother] + child_alone: + members: [child] + households: + household: + members: [mother, child] + state_code: TX + output: + is_ssi_ineligible_parent: [true, false] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 6_156] # $513 x 12 + +- name: A married 17-year-old is not a child for parental deeming + # POMS SI 01310.115 A.2: an eligible child for deeming 'is not married' + # (416.1856). She and her husband live with her parents, who claim both. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + mother: + age: 45 + person_id: 1 + ssi_earned_income: 5_000 * 12 + father: + age: 46 + person_id: 2 + daughter: + age: 17 + is_ssi_disabled: true + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + husband: + age: 18 + person_id: 4 + tax_units: + tax_unit: + members: [mother, father, daughter, husband] + marital_units: + parents: + members: [mother, father] + couple: + members: [daughter, husband] + households: + household: + members: [mother, father, daughter, husband] + state_code: TX + output: + is_ssi_income_deeming_child: [false, false, false, false] + is_ssi_ineligible_parent: [false, false, false, false] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 0, 0, 0] + +- name: A working student child's exclusion does not reduce the parent's earnings + # The student earned income exclusion applies to the child's own wages + # (416.1112(c)(3)); 416.1161(a) does not exclude it from a parent's income. + # Mother: $3,000 - $85 = $2,915; half = $1,457.50; less $967 = $490.50. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + mother: + age: 40 + person_id: 1 + ssi_earned_income: 3_000 * 12 + child: + age: 16 + is_ssi_disabled: true + meets_ssi_disability_criteria: true + is_in_k12_school: true + person_id: 2 + parent_1_id: 1 + ssi_earned_income: 500 * 12 + tax_units: + tax_unit: + members: [mother, child] + marital_units: + mother_alone: + members: [mother] + child_alone: + members: [child] + households: + household: + members: [mother, child] + state_code: TX + output: + ssi_blind_or_disabled_working_student_exclusion: [0, 6_000] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 5_886] # $490.50 x 12 + +- name: Each family's deemed income is divided among its own eligible children + # Two sisters share a home, each with a disabled son. 416.1165(e)(2) + # divides 'among those eligible children' of the same parents. Ann: $3,000 + # - $85 = $2,915; half = $1,457.50; less $967 = $490.50. Beth: $1,200 - + # $20 = $1,180; less $967 = $213. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + ann: + age: 34 + person_id: 1 + ssi_earned_income: 3_000 * 12 + ann_son: + age: 8 + is_ssi_disabled: true + person_id: 2 + parent_1_id: 1 + beth: + age: 30 + person_id: 3 + ssi_unearned_income: 1_200 * 12 + beth_son: + age: 6 + is_ssi_disabled: true + person_id: 4 + parent_1_id: 3 + tax_units: + ann: + members: [ann, ann_son, beth, beth_son] + marital_units: + ann_alone: + members: [ann] + ann_son_alone: + members: [ann_son] + beth_alone: + members: [beth] + beth_son_alone: + members: [beth_son] + households: + household: + members: [ann, ann_son, beth, beth_son] + state_code: TX + output: + is_ssi_ineligible_parent: [true, false, true, false] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 5_886, 0, 2_556] # $490.50, $213 x 12 + +- name: Without parent ids a disabled teen mother deems from her parents, not herself + # Kay of POMS SI 01320.550 D.7 is herself an eligible child: 16, disabled, + # with her baby and a disabled 12-year-old brother, all claimed by her + # parents. Only her parents deem, and she shares their income with her + # brother (416.1165(e)(2)); her baby is 'your natural child' (416.1160(d)), + # an ineligible child. 2026: $4,000 - $497 = $3,503; - $85 = $3,418; half + # = $1,709; less the $1,491 couple rate = $218, or $109 each. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + father: + age: 45 + own_children_in_household: 2 + ssi_earned_income: 4_000 * 12 + mother: + age: 43 + own_children_in_household: 2 + kay: + age: 16 + is_ssi_disabled: true + own_children_in_household: 1 + ssi_unearned_income: 300 * 12 + brother: + age: 12 + is_ssi_disabled: true + baby: + age: 0 + tax_units: + tax_unit: + members: [father, mother, kay, brother, baby] + marital_units: + parents: + members: [father, mother] + kay_alone: + members: [kay] + brother_alone: + members: [brother] + baby_alone: + members: [baby] + households: + household: + members: [father, mother, kay, brother, baby] + state_code: TX + output: + is_ssi_ineligible_parent: [true, true, false, false, false] + ssi_parental_deeming_child_allocations: [0, 0, 5_964, 5_964, 0] # $497 x 12 + ssi_parental_deeming_parent_allocation: [0, 0, 17_892, 17_892, 0] # $1,491 x 12 + ssi_unearned_income_deemed_from_ineligible_parent: [0, 0, 1_308, 1_308, 0] # $109 x 12 + +- name: Without parent ids a teen mother who deems is not also allocated as an ineligible child + # Ben, disabled, lives with his mother Kay, 15, and her parents, who claim + # both. Without ids, the tax-unit proxy also presumes Kay's parents deem to + # Ben, because they report her as their child (a documented data + # limitation; the D.7 case with ids above has only Kay deem). Kay is one of + # his parents, so she is not his sibling and takes no allocation. 2026: + # $8,000 - $85 = $7,915; half = $3,957.50; less the couple rate plus the + # individual rate for three deemors ($1,491 + $994) = $1,472.50. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + grandfather: + age: 45 + own_children_in_household: 1 + ssi_earned_income: 8_000 * 12 + grandmother: + age: 43 + own_children_in_household: 1 + kay: + age: 15 + own_children_in_household: 1 + ben: + age: 1 + is_ssi_disabled: true + tax_units: + tax_unit: + members: [grandfather, grandmother, kay, ben] + marital_units: + grandparents: + members: [grandfather, grandmother] + kay_alone: + members: [kay] + ben_alone: + members: [ben] + households: + household: + members: [grandfather, grandmother, kay, ben] + state_code: TX + output: + is_ssi_ineligible_parent: [true, true, true, false] + ssi_parental_deeming_child_allocations: [0, 0, 0, 0] + ssi_parental_deeming_parent_allocation: [0, 0, 0, 29_820] # ($1,491 + $994) x 12 + ssi_unearned_income_deemed_from_ineligible_parent: [0, 0, 0, 17_670] # $1,472.50 x 12 + +- name: 416.1165(h) Example 1, Henry, with parent ids + # 1992 rates: $422 individual, $633 couple, so a $211 allocation. $365 - + # $211 = $154; - $20 = $134. $1,165 - $65 = $1,100; half = $550. $684 less + # $633 = $51 deemed; $31 countable. + period: 1992 + absolute_error_margin: 0 + input: + people: + henry: + age: 12 + is_ssi_disabled: true + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + henrys_brother: + age: 12 + person_id: 4 + parent_1_id: 1 + parent_2_id: 2 + henrys_mother: + person_id: 1 + age: 40 + pension_income: 365 * 12 + henrys_father: + person_id: 2 + age: 42 + employment_income: 1_165 * 12 + tax_units: + tax_unit: + members: [henry, henrys_brother, henrys_mother, henrys_father] + marital_units: + parents: + members: [henrys_mother, henrys_father] + henry_alone: + members: [henry] + brother_alone: + members: [henrys_brother] + households: + household: + members: [henry, henrys_brother, henrys_mother, henrys_father] + state_code: TX + output: + ssi_parental_deeming_child_allocations: [2_532, 0, 0, 0] # $211 x 12 + ssi_unearned_income_deemed_from_ineligible_parent: [612, 0, 0, 0] # $51 x 12 + ssi_countable_income: [372, 0, 0, 0] # $31 x 12 + +- name: 416.1165(h) Example 4, Jack, with parent ids + # Allocations of $422 for two brothers exceed the $350 pension by $72, + # which comes off the $1,525 earnings: $1,453; - $85 = $1,368; half = + # $684; less $633 = $51 deemed; $31 countable. + period: 1992 + absolute_error_margin: 0 + input: + people: + jack: + age: 12 + is_ssi_disabled: true + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + jacks_first_brother: + age: 12 + person_id: 4 + parent_1_id: 1 + parent_2_id: 2 + jacks_second_brother: + age: 10 + person_id: 5 + parent_1_id: 1 + parent_2_id: 2 + jacks_mother: + person_id: 1 + age: 40 + pension_income: 350 * 12 + jacks_father: + person_id: 2 + age: 42 + employment_income: 1_525 * 12 + tax_units: + tax_unit: + members: [jack, jacks_first_brother, jacks_second_brother, jacks_mother, jacks_father] + marital_units: + parents: + members: [jacks_mother, jacks_father] + jack_alone: + members: [jack] + first_brother_alone: + members: [jacks_first_brother] + second_brother_alone: + members: [jacks_second_brother] + households: + household: + members: [jack, jacks_first_brother, jacks_second_brother, jacks_mother, jacks_father] + state_code: TX + output: + ssi_parental_deeming_child_allocations: [5_064, 0, 0, 0, 0] # 2 x $211 x 12 + ssi_unearned_income_deemed_from_ineligible_parent: [612, 0, 0, 0, 0] # $51 x 12 + ssi_countable_income: [372, 0, 0, 0, 0] # $31 x 12 + +- name: A stepparent who has left still has an allocation for his child at home + # POMS SI 00501.010 B.2.d: 'If an SSI eligible child's stepparent has + # permanently left the household, we continue to provide the ineligible + # child allocation for each of the stepparent's children remaining in the + # eligible child's household.' The mother's husband lives elsewhere; his son + # stays with her and her disabled daughter. He does not deem (SI 01320.500 + # A.1). $4,000 - $483 = $3,517; - $85 = $3,432; half = $1,716; less $967 = + # $749 deemed. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + mother: + age: 38 + person_id: 1 + ssi_earned_income: 4_000 * 12 + daughter: + age: 10 + is_ssi_disabled: true + person_id: 2 + parent_1_id: 1 + stepson: + age: 12 + person_id: 3 + parent_1_id: 4 + stepfather: + age: 40 + person_id: 4 + tax_units: + mother: + members: [mother, daughter, stepson] + stepfather: + members: [stepfather] + marital_units: + couple: + members: [mother, stepfather] + daughter_alone: + members: [daughter] + stepson_alone: + members: [stepson] + households: + home: + members: [mother, daughter, stepson] + state_code: TX + elsewhere: + members: [stepfather] + state_code: TX + output: + is_ssi_ineligible_parent: [true, false, false, false] + ssi_parental_deeming_child_allocations: [0, 5_796, 0, 0] + ssi_parental_deeming_parent_allocation: [0, 11_604, 0, 0] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 8_988, 0, 0] # $749 x 12 + +- name: An id that repeats the other parent id is not read as an absent stepparent + # The mother's husband lives elsewhere and shares person_id 4 with the aunt + # at home, which the id contract allows (no tax unit spans households). + # The cousin's ids both name the aunt, so the cousin is not the + # stepfather's child and takes no allocation. $4,000 - $85 = $3,915; half + # = $1,957.50; less $967 = $990.50 deemed. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + mother: + age: 38 + person_id: 1 + ssi_earned_income: 4_000 * 12 + daughter: + age: 10 + is_ssi_disabled: true + person_id: 2 + parent_1_id: 1 + aunt: + age: 35 + person_id: 4 + cousin: + age: 9 + person_id: 5 + parent_1_id: 4 + parent_2_id: 4 + stepfather: + age: 40 + person_id: 4 + tax_units: + mother: + members: [mother, daughter] + aunt: + members: [aunt, cousin] + stepfather: + members: [stepfather] + marital_units: + couple: + members: [mother, stepfather] + daughter_alone: + members: [daughter] + aunt_alone: + members: [aunt] + cousin_alone: + members: [cousin] + households: + home: + members: [mother, daughter, aunt, cousin] + state_code: TX + elsewhere: + members: [stepfather] + state_code: TX + output: + ssi_parental_deeming_child_allocations: [0, 0, 0, 0, 0] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 11_886, 0, 0, 0] # $990.50 x 12 + +- name: Without ids an absent stepfather's child in his tax unit is allocated in a linked household + # The daughter's id names her mother. The mother's husband lives elsewhere + # and is a parent; his son, who has no ids, lives with the mother and is in + # his tax unit. The proxy may presume the son is his child because he is a + # parent some of whose children no id names. $4,000 - $483 = $3,517; - + # $85 = $3,432; half = $1,716; less $967 = $749 deemed. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + mother: + age: 38 + person_id: 1 + own_children_in_household: 1 + ssi_earned_income: 4_000 * 12 + daughter: + age: 10 + is_ssi_disabled: true + person_id: 2 + parent_1_id: 1 + stepson: + age: 12 + person_id: 3 + stepfather: + age: 40 + person_id: 4 + is_parent: true + tax_units: + mother: + members: [mother, daughter] + stepfather: + members: [stepfather, stepson] + marital_units: + couple: + members: [mother, stepfather] + daughter_alone: + members: [daughter] + stepson_alone: + members: [stepson] + households: + home: + members: [mother, daughter, stepson] + state_code: TX + elsewhere: + members: [stepfather] + state_code: TX + output: + ssi_parental_deeming_child_allocations: [0, 5_796, 0, 0] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 8_988, 0, 0] # $749 x 12 + +- name: An absent stepfather lends the proxy only his own tax unit + # As above, with an aunt and her son, who has no ids, at home; the mother + # claims the cousin. The stepfather being a parent lets the proxy presume + # his tax unit's child is his, not the cousin in the mother's tax unit + # (416.1160(d) does not reach cousins). $4,000 - $483 = $3,517; - $85 = + # $3,432; half = $1,716; less $967 = $749 deemed. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + mother: + age: 38 + person_id: 1 + own_children_in_household: 1 + ssi_earned_income: 4_000 * 12 + daughter: + age: 10 + is_ssi_disabled: true + person_id: 2 + parent_1_id: 1 + stepson: + age: 12 + person_id: 3 + aunt: + age: 35 + person_id: 5 + cousin: + age: 8 + person_id: 6 + stepfather: + age: 40 + person_id: 4 + is_parent: true + tax_units: + mother: + members: [mother, daughter, cousin] + aunt: + members: [aunt] + stepfather: + members: [stepfather, stepson] + marital_units: + couple: + members: [mother, stepfather] + daughter_alone: + members: [daughter] + stepson_alone: + members: [stepson] + aunt_alone: + members: [aunt] + cousin_alone: + members: [cousin] + households: + home: + members: [mother, daughter, stepson, aunt, cousin] + state_code: TX + elsewhere: + members: [stepfather] + state_code: TX + output: + ssi_parental_deeming_child_allocations: [0, 5_796, 0, 0, 0, 0] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 8_988, 0, 0, 0, 0] # $749 x 12 + +- name: A 17-year-old married to a husband who lives elsewhere is not a child for parental deeming + # 416.1856(b): a child is 'not married'; 416.1806 recognizes the marriage + # wherever the spouses live. Her brother is the only eligible child. + # $4,000 - $85 = $3,915; half = $1,957.50; less $967 = $990.50 deemed. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + mother: + age: 42 + person_id: 1 + ssi_earned_income: 4_000 * 12 + daughter: + age: 17 + is_ssi_disabled: true + person_id: 2 + parent_1_id: 1 + son: + age: 9 + is_ssi_disabled: true + person_id: 3 + parent_1_id: 1 + husband: + age: 19 + person_id: 4 + tax_units: + mother: + members: [mother, daughter, son] + husband: + members: [husband] + marital_units: + mother_alone: + members: [mother] + couple: + members: [daughter, husband] + son_alone: + members: [son] + households: + home: + members: [mother, daughter, son] + state_code: TX + elsewhere: + members: [husband] + state_code: TX + output: + is_ssi_income_deeming_child: [false, false, true, false] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 0, 11_886, 0] # $990.50 x 12 + +- name: Half-siblings with the same ineligible parent share her income when one also lives with an SSI-eligible father + # 416.1165(e)(2) divides 'the parental income to be deemed' among the + # eligible children. The mother is the only ineligible parent of both + # boys; the older boy's father, who lives with them unmarried to her, is + # disabled and eligible, so his income is not deemed (416.1165(g)(1)). + # $3,000 - $85 = $2,915; half = $1,457.50; less $967 = $490.50, or + # $245.25 each. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + mother: + age: 36 + person_id: 1 + ssi_earned_income: 3_000 * 12 + father: + age: 40 + is_ssi_disabled: true + person_id: 2 + older: + age: 10 + is_ssi_disabled: true + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + younger: + age: 6 + is_ssi_disabled: true + person_id: 4 + parent_1_id: 1 + tax_units: + tax_unit: + members: [mother, father, older, younger] + marital_units: + mother_alone: + members: [mother] + father_alone: + members: [father] + older_alone: + members: [older] + younger_alone: + members: [younger] + households: + household: + members: [mother, father, older, younger] + state_code: TX + output: + is_ssi_ineligible_parent: [true, false, false, false] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 0, 2_943, 2_943] # $245.25 x 12 + +- name: Children who share an ineligible parent share one set of allocations + # As above, with the father's 8-year-old daughter, who is not disabled, + # also at home. She is the older boy's sister (416.1160(d)) and a child of + # a parent in the deeming household (POMS SI 01310.140 A), so her + # allocation comes off the mother's income once, before it is divided + # between the boys (SI 01320.500 C, steps 1 and 9). $3,000 - $483 = + # $2,517; - $85 = $2,432; half = $1,216; less $967 = $249, or $124.50 + # each. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + mother: + age: 36 + person_id: 1 + ssi_earned_income: 3_000 * 12 + father: + age: 40 + is_ssi_disabled: true + person_id: 2 + older: + age: 10 + is_ssi_disabled: true + person_id: 3 + parent_1_id: 1 + parent_2_id: 2 + younger: + age: 6 + is_ssi_disabled: true + person_id: 4 + parent_1_id: 1 + half_sister: + age: 8 + person_id: 5 + parent_1_id: 2 + tax_units: + tax_unit: + members: [mother, father, older, younger, half_sister] + marital_units: + mother_alone: + members: [mother] + father_alone: + members: [father] + older_alone: + members: [older] + younger_alone: + members: [younger] + half_sister_alone: + members: [half_sister] + households: + household: + members: [mother, father, older, younger, half_sister] + state_code: TX + output: + ssi_parental_deeming_child_allocations: [0, 0, 5_796, 5_796, 0] + ssi_unearned_income_deemed_from_ineligible_parent: [0, 0, 1_494, 1_494, 0] # $124.50 x 12 diff --git a/policyengine_us/variables/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.py b/policyengine_us/variables/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.py index 2ecd23b455a..9579b1ff3d4 100644 --- a/policyengine_us/variables/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.py +++ b/policyengine_us/variables/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.py @@ -5,6 +5,14 @@ from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( _apply_ssi_exclusions, ) +from policyengine_us.variables.gov.ssa.ssi.eligibility.income.deemed.from_ineligible_parent._ssi_parental_deeming import ( + ssi_ineligible_parents, + ssi_parental_deeming_child, + ssi_parental_deeming_child_allocation_total, + ssi_parental_deeming_children, + ssi_parental_deeming_parent_allocation_total, + ssi_parental_income, +) class medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent( @@ -13,47 +21,63 @@ class medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligibl value_type = float entity = Person label = "Medicaid optional senior or disabled unearned income deemed from ineligible parent" + documentation = ( + "Parental income deemed to a disabled child under the SSI rules of " + "20 CFR 416.1165, with the state's income disregards. The parents " + "and the ineligible children are those of SSI parental deeming, " + "except that a disabled child need not meet the SSI citizenship or " + "qualified-noncitizen condition (Medicaid applies its own immigration " + "rules) and a disabled child who shares the deemed income takes no " + "allocation. Missouri deems only a natural or adoptive parent's " + "income, never a stepparent's, and allocates for every other child " + "under 18 in the home who is not applying (DSS Manual 0805.020.15)." + ) unit = USD definition_period = YEAR - reference = "https://www.law.cornell.edu/cfr/text/20/416.1165" + reference = ( + "https://www.ecfr.gov/current/title-20/section-416.1165", + "https://dssmanuals.mo.gov/mo-healthnet-for-the-aged-blind-and-disabled/0805-000-00/0805-020-00/0805-020-15/", + ) def formula(person, period, parameters): - eligible_child = person("is_ssi_aged_blind_disabled", period) & person( - "is_child", period - ) - ineligible_parent = person("is_ssi_ineligible_parent", period) - tax_unit = person.tax_unit - - prereduction_parental_earned_income = eligible_child * tax_unit.sum( - person("ssi_earned_income", period) * ineligible_parent - ) - blind_disabled_working_student_income = person( - "ssi_blind_or_disabled_working_student_exclusion", period - ) - parental_earned_income = max_( - prereduction_parental_earned_income - blind_disabled_working_student_income, - 0, + state = person.household("state_code_str", period) + missouri = state == "MO" + # Missouri: 'Do not deem the income of any other persons in the home, + # including a stepparent' (0805.020.15). + rows, ineligible, pool = ssi_ineligible_parents( + person, period, with_spouses=~missouri ) - - parental_unearned_income = eligible_child * tax_unit.sum( - person("ssi_unearned_income", period) * ineligible_parent + # Medicaid applies its own immigration rules, so a disabled child + # need not meet the SSI citizenship or qualified-noncitizen condition. + applies = ssi_parental_deeming_child( + person, period, ineligible, ssi_status=False ) - - child_allocations = eligible_child * add( - tax_unit, period, ["ssi_ineligible_child_allocation"] + earned = ssi_parental_income( + person, period, "ssi_earned_income", rows, ineligible ) - parental_allocations = eligible_child * add( - tax_unit, period, ["ssi_ineligible_parent_allocation"] + unearned = ssi_parental_income( + person, period, "ssi_unearned_income", rows, ineligible ) - - parental_unearned_income -= child_allocations - remaining_child_allocations = max_(0, -parental_unearned_income) - parental_unearned_income = max_(0, parental_unearned_income) - parental_earned_income = max_( - 0, parental_earned_income - remaining_child_allocations + # Missouri allocates for 'each child under 18 living in the home who + # is not applying for MA' (0805.020.15 step 1). A disabled child who + # shares the deemed income takes no allocation. + child_allocations = where( + np.any(ineligible, axis=1), + ssi_parental_deeming_child_allocation_total( + person, + period, + rows, + pool, + applies, + every_minor=missouri, + exclude=applies, + ), + 0, ) + remaining_child_allocations = max_(0, child_allocations - unearned) + unearned = max_(0, unearned - child_allocations) + earned = max_(0, earned - remaining_child_allocations) - state = person.household("state_code_str", period) general_disregard = parameters( period ).gov.hhs.medicaid.eligibility.categories.senior_or_disabled.income.disregard.individual[ @@ -62,30 +86,32 @@ def formula(person, period, parameters): # Missouri deems parental income in the SSI order: the $20 # exclusion comes off before the $65 and the half (DSS Manual # § 0805.020.15 steps 4-6), unlike its applicant budget. - net_parental_deemed_income = where( - state == "MO", + net = where( + missouri, _apply_ssi_exclusions( - parental_earned_income, - parental_unearned_income, + earned, + unearned, parameters, period, general_exclusion=general_disregard, ), _apply_medicaid_optional_senior_or_disabled_exclusions( - parental_earned_income, - parental_unearned_income, + earned, + unearned, state, general_disregard, parameters, period, ), ) - net_parental_deemed_income = max_( - 0, net_parental_deemed_income - parental_allocations + # Missouri: 'the SSI maximum for the number of parents in the home + # whose income was used in deeming'. + net = max_( + 0, + net + - ssi_parental_deeming_parent_allocation_total( + parameters, period, np.sum(ineligible, axis=1) + ), ) - - count_eligible_children = tax_unit.sum(eligible_child) - income = np.zeros_like(count_eligible_children, dtype=float) - mask = count_eligible_children > 0 - income[mask] = net_parental_deemed_income[mask] / count_eligible_children[mask] - return income + children = ssi_parental_deeming_children(pool, applies) + return where(applies, net / max_(1, children), 0) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/README.md b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/README.md new file mode 100644 index 00000000000..b2162293450 --- /dev/null +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/README.md @@ -0,0 +1,29 @@ +# SSI parent-to-child deeming + +This module implements Supplemental Security Income (SSI) deeming of an ineligible parent's income to an eligible child under [20 CFR 416.1165](https://www.ecfr.gov/current/title-20/section-416.1165), following POMS [SI 01320.500](https://secure.ssa.gov/poms.nsf/lnx/0501320500). + +## Who deems to whom + +- **Ineligible parents** (416.1160(d)): the child's natural or adoptive parents who live with the child, and the co-resident spouse of each, who are not eligible for SSI. Parent ids (`parent_1_id`, `parent_2_id`) name the parents. Without ids, the unlinked parents in the child's tax unit who live with the child, and their spouses, stand in, as the parent-link contract in `household/demographic/person/_parent_links.py` allows. `resources/deemed/_ssi_parents.py` resolves these people for both income and resource deeming. A stepparent deems only through a spouse who lives in the household (POMS SI 01320.500 A.1), so a child living with a stepparent alone is not deemed income. +- **Eligible child** (`is_ssi_income_deeming_child`; POMS SI 01310.115 A.2, 416.1856): under 18, not married, not the head of a household, aged, blind or disabled and a citizen or qualified noncitizen, and living with at least one ineligible parent. A parent who meets the same status conditions is treated as eligible for SSI, so their income is not deemed (416.1165(g)(1)). +- **Ineligible children** (`ssi_parental_deeming_child_allocations`; 416.1160(d), POMS SI 01310.115 A.3, SI 01310.140 A, SI 00501.010 B.2): the child's own children and the children of any of the child's co-resident parents or parents' spouses who live in the household and are not eligible for SSI, that is, siblings, half-siblings and step-siblings. Cousins and grandchildren are not allocated. A stepparent who has left the household (a spouse in the parent's marital unit who lives elsewhere) keeps the allocation for his children at home (SI 00501.010 B.2.d). Without ids, the members of the tax unit of the child, of one of their parents or of such an absent stepparent stand in, in a household with links only when one of those people is a parent some of whose children no id names. + +## Steps + +1. Total the ineligible parents' earned and unearned income (416.1165(a)). +2. Deduct the ineligible child allocations, `ssi_ineligible_child_allocation` (416.1163(b)), from unearned income first, then from earned income (416.1165(b), Example 4). +3. Apply the $20 general exclusion and the $65 and one-half earned income exclusion (416.1165(d)(1)-(2)). +4. Subtract the living allowance, `ssi_parental_deeming_parent_allocation`: the individual rate for one parent, the couple rate for two, and the couple rate plus the individual rate for two natural parents and a stepparent (416.1165(d)(3), POMS SI 01320.500 B.4). +5. Divide what remains equally among the eligible children who share those parents (416.1165(e)(2)). + +## Medicaid + +`medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent` applies the same steps with each state's income disregards. A disabled child need not meet the SSI citizenship or qualified-noncitizen condition, since Medicaid applies its own immigration rules; a disabled child who shares the deemed income takes no ineligible-child allocation. Missouri deems only natural or adoptive parents, never a stepparent, and allocates for every child under 18 in the home ([DSS Manual 0805.020.15](https://dssmanuals.mo.gov/mo-healthnet-for-the-aged-blind-and-disabled/0805-000-00/0805-020-00/0805-020-15/)). + +## Not modeled + +- 416.1166: an eligible parent living with an ineligible spouse and an eligible child. The ineligible spouse's income is deemed both to the parent, through spousal deeming, and to the child, through these rules. 416.1166 deems to the child only what is left after the parent's benefit reaches zero. +- 416.1165(e)(3): the cap on a child's share at the amount that reduces their benefit to zero, and the redistribution of the rest. +- 416.1165(d) and 416.1161(a)(2)-(3): parents receiving public income-maintenance payments, and income used to compute such payments. +- 416.1165(g)(6) and (i): the $30 medical-facility rate and the home-care waiver. +- Parent ids name natural, adoptive or step parents alike. A stepparent named by an id therefore deems even after the natural parent has left. Name only the natural or adoptive parents and let the spouse test add the stepparent. diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/_ssi_parental_deeming.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/_ssi_parental_deeming.py new file mode 100644 index 00000000000..79fefb28d47 --- /dev/null +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/_ssi_parental_deeming.py @@ -0,0 +1,264 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_parents import ( + ssi_parental_deemors, +) +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_spouse_index, +) +from policyengine_us.variables.household.demographic.person._parent_links import ( + co_resident_parent_indices, + group_member_indices, + has_parent_ids, + household_has_parent_ids, + household_member_indices, + unlinked_parent, +) + + +def ssi_meets_eligibility_status(person, period): + """Aged, blind or disabled, and a citizen or qualified noncitizen. + + Stands in for 'eligible for SSI benefits' when deciding who deems to whom. + The income and resource tests are not read: they depend on deemed income. + """ + immigration_status = person("immigration_status", period) + is_citizen = immigration_status == immigration_status.possible_values.CITIZEN + return person("is_ssi_aged_blind_disabled", period) & ( + is_citizen | person("is_ssi_qualified_noncitizen", period) + ) + + +def ssi_ineligible_parents(person, period, with_spouses=True): + """Return each person's ineligible parents and their deemor pool. + + 416.1160(d): an ineligible parent is 'a natural or adoptive parent, or + the spouse ... of a natural or adoptive parent, who lives with you and is + not eligible for SSI benefits'. ssi_parental_deemors resolves the parents + and spouses from parent ids, or the tax-unit proxy without ids; this drops + those who meet the SSI eligibility status (416.1165(g)(1)). + + Returns rows (the person's parents and parents' spouses, -1 padded), the + mask of rows that are ineligible parents, and a pool id equal for people + with the same ineligible parents. 416.1165(e)(2) divides 'the parental + income to be deemed' among the eligible children, so children whose + ineligible parents are the same share one pool even when one of them + also lives with an SSI-eligible parent. A person is never their own + ineligible parent, although the tax-unit proxy keeps an unlinked parent + in their own set: deeming counts another person's income (416.1160(a)). + """ + rows, _ = ssi_parental_deemors(person, period, with_spouses) + eligible = ssi_meets_eligibility_status(person, period) + own_index = np.arange(person.count) + ineligible = ( + (rows >= 0) & (rows != own_index[:, None]) & ~eligible[np.maximum(rows, 0)] + ) + keys = np.where(ineligible, rows, -1) + keys.sort(axis=1) + _, pool = np.unique(keys, axis=0, return_inverse=True) + return rows, ineligible, pool.reshape(-1) + + +def _spouse_elsewhere(person): + """Each person's spouse who lives in another household, or -1. + + A two-person marital unit whose members live in different households + marks a spouse who lives elsewhere (416.1806 recognizes a marriage + wherever the spouses live). When the simulation has a single marital + unit, which may be the default group of a situation that omits marital + units, no one is treated as such a spouse. + """ + own_index = np.arange(person.count) + couple = (person.marital_unit.nb_persons() == 2) & (person.marital_unit.count > 1) + partner = np.full(person.count, -1, dtype=int) + for member in group_member_indices(person.marital_unit): + partner = np.where( + couple & (member >= 0) & (member != own_index), member, partner + ) + households = person.household.reference_entity.members_entity_id + return np.where( + (partner >= 0) & (households[np.maximum(partner, 0)] != households), + partner, + -1, + ) + + +def ssi_parental_deeming_child(person, period, ineligible, ssi_status=True): + """Whether parental deeming applies to each person as an eligible child. + + POMS SI 01310.115 A.2: 'a natural or adopted child under age 18 who lives + in a household with one or both parents, is not married, and is eligible + for SSI'. 416.1856 also excludes the head of a household. A child is + married when the spouse test of SSI resource deeming finds a co-resident + spouse or when their spouse lives elsewhere. Eligibility is the SSI + status condition: aged, blind or disabled and a citizen or qualified + noncitizen. Without ssi_status, as for Medicaid, which applies its own + immigration rules, being aged, blind or disabled suffices. + """ + status = ( + ssi_meets_eligibility_status(person, period) + if ssi_status + else person("is_ssi_aged_blind_disabled", period) + ) + return ( + person("is_child", period) + & status + & (_ssi_spouse_index(person, period) < 0) + & (_spouse_elsewhere(person) < 0) + & ~person("is_household_head", period) + & np.any(ineligible, axis=1) + ) + + +def _absent_spouses(person, rows): + """The spouse, living elsewhere, of each of a person's parents (rows).""" + absent = _spouse_elsewhere(person) + return np.where(rows >= 0, absent[np.maximum(rows, 0)], -1) + + +def ssi_parental_deeming_child_allocation_total( + person, period, rows, pool, applies, every_minor=False, exclude=None +): + """Sum the ineligible child allocations deducted for each person. + + 416.1165(b) deducts 'an allocation for each ineligible child in the + household'. 416.1160(d): an ineligible child is 'your natural child or + adopted child, or ... the natural or adopted child of your parent or of + your parent's spouse ... who lives in the same household with you'. So + the children count when they are the person's own child or a child of one + of the person's parents or parents' spouses (rows), whether or not that + parent is eligible for SSI, as POMS SI 01310.115 A.3 and SI 01310.140 A + describe. POMS SI 00501.010 B.2.d keeps the children of a stepparent who + has left: 'If an SSI eligible child's stepparent has permanently left the + household, we continue to provide the ineligible child allocation for + each of the stepparent's children remaining in the eligible child's + household', so the spouse of one of the parents who lives elsewhere also + counts. A child with parent ids counts when an id names one of those + people (an id that names no co-resident may name that absent spouse). A + child without ids counts when they are in the tax unit of the person or + of one of their co-resident parents or parents' spouses (in a household + with links, only when one of those is a parent some of whose children no + id names), or in the tax unit of such an absent spouse (in a household + with links, only when that spouse is a parent some of whose children no + id names), as the parent-link contract in _parent_links.py allows. The + parents themselves never count. A child counts for every eligible child + (applies) in a pool when they count for any of them, so the children who + share their parents' income share one set of allocations: 'an allocation + for each ineligible child in the household' (416.1165(b)) of the + deeming household of SI 01310.140 A. every_minor (a bool or a per-person array) instead counts + every co-resident child under 18 other than the person and their + parents, as Missouri's MHDC budget does (DSS Manual 0805.020.15). People + flagged in exclude never count. + """ + every_minor = np.broadcast_to(every_minor, (person.count,)) + pool_count = int(np.max(pool, initial=-1)) + 1 + exclude = ( + np.zeros(person.count, dtype=bool) + if exclude is None + else np.broadcast_to(exclude, (person.count,)) + ) + allocation = person("ssi_ineligible_child_allocation", period) + minor = person("is_child", period) + first, second = co_resident_parent_indices(person, period) + linked = has_parent_ids(person, period) + unlinked = unlinked_parent(person, period) + tax_unit = person.tax_unit.reference_entity.members_entity_id + own_index = np.arange(person.count) + # The person and their parents and parents' spouses. + relatives = np.column_stack((own_index, rows)) + present = relatives >= 0 + safe_relatives = np.maximum(relatives, 0) + relative_tax_units = np.where(present, tax_unit[safe_relatives], -1) + # Spouses of the parents who live elsewhere, by person_id for children + # whose ids name no co-resident, and by tax unit for the proxy. + absent = _absent_spouses(person, rows) + away = absent >= 0 + safe_absent = np.maximum(absent, 0) + no_links = ~household_has_parent_ids(person, period) + may_presume = no_links | np.any(present & unlinked[safe_relatives], axis=1) + # An absent spouse lends only his own tax unit to the proxy, and in a + # household with links only when he is an unlinked parent. + absent_may_presume = away & (no_links[:, None] | unlinked[safe_absent]) + absent_ids = np.where(away, person("person_id", period)[safe_absent], 0) + absent_tax_units = np.where(away, tax_unit[safe_absent], -1) + parent_1 = person("parent_1_id", period) + parent_2 = person("parent_2_id", period) + total = np.zeros(person.count) + for child in household_member_indices(person): + safe = np.maximum(child, 0) + # A second id that repeats the first is no second parent. + unresolved_1 = np.where(first[safe] < 0, parent_1[safe], 0) + unresolved_2 = np.where( + (second[safe] < 0) & (parent_2[safe] != parent_1[safe]), + parent_2[safe], + 0, + ) + named = np.any( + present + & ( + (relatives == first[safe][:, None]) + | (relatives == second[safe][:, None]) + ), + axis=1, + ) | np.any( + away + & ( + ((unresolved_1 != 0)[:, None] & (absent_ids == unresolved_1[:, None])) + | ((unresolved_2 != 0)[:, None] & (absent_ids == unresolved_2[:, None])) + ), + axis=1, + ) + presumed = ( + may_presume + & np.any(present & (relative_tax_units == tax_unit[safe][:, None]), axis=1) + ) | np.any( + absent_may_presume & (absent_tax_units == tax_unit[safe][:, None]), axis=1 + ) + is_parent = np.any(present & (relatives == child[:, None]), axis=1) + counts = ( + (child >= 0) + & (child != own_index) + & ~is_parent + & ~exclude[safe] + & where( + every_minor, + minor[safe], + where(linked[safe], named, presumed), + ) + ) + # The eligible children of one pool share a household, so this + # position holds the same person for all of them. + hit = np.bincount(pool[applies & counts], minlength=pool_count) > 0 + counts = where(applies, hit[pool], counts) + total += where(counts, allocation[safe], 0) + return total + + +def ssi_parental_deeming_parent_allocation_total(parameters, period, parents): + """The living allowance for a child's ineligible parents. + + 416.1165(d)(3): the couple rate 'if both parents live with you', the + individual rate 'if only one parent lives with you'. POMS SI 01320.500 + B.4 adds the couple rate plus the individual rate when both natural or + adoptive parents and a stepparent live with the child (Example F.5); a + couple rate for each pair and an individual rate for one left over + extends that to any number. + """ + p = parameters(period).gov.ssa.ssi.amount + return (p.couple * (parents // 2) + p.individual * (parents % 2)) * MONTHS_IN_YEAR + + +def ssi_parental_deeming_children(pool, applies): + """The number of children deeming applies to in each person's pool. + + 416.1165(e)(2): 'If your parents have more than one eligible child under + age 18 in the household, we divide the parental income to be deemed + equally among those eligible children.' + """ + count = np.bincount(pool[applies], minlength=int(np.max(pool, initial=-1)) + 1) + return count[pool] + + +def ssi_parental_income(person, period, variable, rows, ineligible): + """Total a variable over each person's ineligible parents.""" + values = person(variable, period) + return np.sum(np.where(ineligible, values[np.maximum(rows, 0)], 0), axis=1) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/is_ssi_income_deeming_child.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/is_ssi_income_deeming_child.py new file mode 100644 index 00000000000..2ace300d53c --- /dev/null +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/is_ssi_income_deeming_child.py @@ -0,0 +1,33 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.income.deemed.from_ineligible_parent._ssi_parental_deeming import ( + ssi_ineligible_parents, + ssi_parental_deeming_child, +) + + +class is_ssi_income_deeming_child(Variable): + value_type = bool + entity = Person + label = "Child subject to parental SSI income deeming" + documentation = ( + "An eligible child to whom SSI deems an ineligible parent's income: " + "under 18, not married, not the head of a household, aged, blind or " + "disabled and a citizen or qualified noncitizen, and living with at " + "least one ineligible parent. An ineligible parent is a natural, " + "adoptive or step parent named by the child's parent ids, or the " + "co-resident spouse of one, who does not meet the SSI status " + "conditions; without ids, an unlinked parent in the child's tax unit " + "or the spouse of one stands in." + ) + definition_period = YEAR + reference = ( + "https://www.ecfr.gov/current/title-20/section-416.1165", + "https://www.ecfr.gov/current/title-20/section-416.1160#p-416.1160(d)", + "https://www.ecfr.gov/current/title-20/section-416.1856", + "https://secure.ssa.gov/poms.nsf/lnx/0501310115", + "https://secure.ssa.gov/poms.nsf/lnx/0501320500", + ) + + def formula(person, period, parameters): + _, ineligible, _ = ssi_ineligible_parents(person, period) + return ssi_parental_deeming_child(person, period, ineligible) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/ssi_parental_deeming_child_allocations.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/ssi_parental_deeming_child_allocations.py new file mode 100644 index 00000000000..b4173eea3a2 --- /dev/null +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/ssi_parental_deeming_child_allocations.py @@ -0,0 +1,41 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.income.deemed.from_ineligible_parent._ssi_parental_deeming import ( + ssi_ineligible_parents, + ssi_parental_deeming_child_allocation_total, +) + + +class ssi_parental_deeming_child_allocations(Variable): + value_type = float + entity = Person + label = "SSI ineligible child allocations from an ineligible parent's income" + documentation = ( + "The sum of the ineligible child allocations deducted from the income " + "of this person's ineligible parents before it is deemed to them: one " + "for each ineligible child in their household who is their own child " + "or a child of one of their parents or parents' spouses, that is, " + "their siblings, half-siblings and step-siblings. A child whose " + "parent ids are set counts when an id names this person or one of " + "those parents. A child without ids counts when they belong to the " + "tax unit of this person or of one of those parents; in a household " + "with parent links, only when one of them is a parent some of whose " + "children no id names. Without ids, a cousin or other relative in that " + "tax unit cannot be told apart from a sibling, so supply parent ids." + ) + unit = USD + definition_period = YEAR + defined_for = "is_ssi_income_deeming_child" + reference = ( + "https://www.ecfr.gov/current/title-20/section-416.1165#p-416.1165(b)", + "https://www.ecfr.gov/current/title-20/section-416.1160#p-416.1160(d)", + "https://secure.ssa.gov/poms.nsf/lnx/0501310115", + "https://secure.ssa.gov/poms.nsf/lnx/0501310140", + "https://secure.ssa.gov/poms.nsf/lnx/0501320500", + ) + + def formula(person, period, parameters): + rows, _, pool = ssi_ineligible_parents(person, period) + applies = person("is_ssi_income_deeming_child", period) + return ssi_parental_deeming_child_allocation_total( + person, period, rows, pool, applies + ) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/ssi_parental_deeming_parent_allocation.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/ssi_parental_deeming_parent_allocation.py new file mode 100644 index 00000000000..b9875bf56d6 --- /dev/null +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/ssi_parental_deeming_parent_allocation.py @@ -0,0 +1,31 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.income.deemed.from_ineligible_parent._ssi_parental_deeming import ( + ssi_ineligible_parents, + ssi_parental_deeming_parent_allocation_total, +) + + +class ssi_parental_deeming_parent_allocation(Variable): + value_type = float + entity = Person + label = "SSI living allowance for this person's ineligible parents" + documentation = ( + "The allowance SSI deducts for the needs of this person's ineligible " + "parents before deeming their income: the individual federal benefit " + "rate for one parent, the couple rate for two, and a couple rate for " + "each further pair and an individual rate for one left over, as when " + "both natural parents and a stepparent live with the child." + ) + unit = USD + definition_period = YEAR + defined_for = "is_ssi_income_deeming_child" + reference = ( + "https://www.ecfr.gov/current/title-20/section-416.1165#p-416.1165(d)", + "https://secure.ssa.gov/poms.nsf/lnx/0501320500", + ) + + def formula(person, period, parameters): + _, ineligible, _ = ssi_ineligible_parents(person, period) + return ssi_parental_deeming_parent_allocation_total( + parameters, period, np.sum(ineligible, axis=1) + ) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/ssi_unearned_income_deemed_from_ineligible_parent.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/ssi_unearned_income_deemed_from_ineligible_parent.py index a6d6252e24b..29efdfac11f 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/ssi_unearned_income_deemed_from_ineligible_parent.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent/ssi_unearned_income_deemed_from_ineligible_parent.py @@ -2,71 +2,55 @@ from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( _apply_ssi_exclusions, ) +from policyengine_us.variables.gov.ssa.ssi.eligibility.income.deemed.from_ineligible_parent._ssi_parental_deeming import ( + ssi_ineligible_parents, + ssi_parental_deeming_children, + ssi_parental_income, +) class ssi_unearned_income_deemed_from_ineligible_parent(Variable): value_type = float entity = Person label = "SSI unearned income (deemed from ineligible parent)" + documentation = ( + "The income of this child's ineligible parents that SSI deems to " + "them, after the allocations for ineligible children and parents and " + "the general and earned income exclusions, divided equally among the " + "eligible children who share those parents." + ) unit = USD definition_period = YEAR - reference = "https://www.law.cornell.edu/cfr/text/20/416.1165" + reference = ( + "https://www.ecfr.gov/current/title-20/section-416.1165", + "https://secure.ssa.gov/poms.nsf/lnx/0501320500", + ) def formula(person, period, parameters): - eligible_child = person("is_ssi_aged_blind_disabled", period) & person( - "is_child", period - ) - ineligible_parent = person("is_ssi_ineligible_parent", period) - tax_unit = person.tax_unit - - prereduction_parental_earned_income = eligible_child * tax_unit.sum( - person("ssi_earned_income", period) * ineligible_parent - ) - blind_disabled_working_student_income = person( - "ssi_blind_or_disabled_working_student_exclusion", period - ) - parental_earned_income = max_( - prereduction_parental_earned_income - blind_disabled_working_student_income, + rows, ineligible, pool = ssi_ineligible_parents(person, period) + applies = person("is_ssi_income_deeming_child", period) + # 416.1165(a): the ineligible parents' income. + earned = ssi_parental_income( + person, period, "ssi_earned_income", rows, ineligible + ) + unearned = ssi_parental_income( + person, period, "ssi_unearned_income", rows, ineligible + ) + # 416.1165(b) and 416.1163(b)(3): allocations for ineligible children + # come out of unearned income first, then earned income (416.1165(h), + # Example 4). + child_allocations = person("ssi_parental_deeming_child_allocations", period) + remaining_child_allocations = max_(0, child_allocations - unearned) + unearned = max_(0, unearned - child_allocations) + earned = max_(0, earned - remaining_child_allocations) + # 416.1165(d)(1)-(3): the $20 and earned income exclusions, then the + # living allowance for the parents. + net = max_( 0, + _apply_ssi_exclusions(earned, unearned, parameters, period) + - person("ssi_parental_deeming_parent_allocation", period), ) - - parental_unearned_income = eligible_child * tax_unit.sum( - person("ssi_unearned_income", period) * ineligible_parent - ) - - child_allocations = eligible_child * add( - tax_unit, period, ["ssi_ineligible_child_allocation"] - ) - parental_allocations = eligible_child * add( - tax_unit, period, ["ssi_ineligible_parent_allocation"] - ) - - # To understand this ordering of operations, see Example 4 of - # https://www.law.cornell.edu/cfr/text/20/416.1165#h. - - parental_unearned_income -= child_allocations - remaining_child_allocations = max_(0, -parental_unearned_income) - parental_unearned_income = max_(0, parental_unearned_income) - parental_earned_income = max_( - 0, parental_earned_income - remaining_child_allocations - ) - - net_parental_deemed_income = _apply_ssi_exclusions( - parental_earned_income, - parental_unearned_income, - parameters, - period, - ) - - net_parental_deemed_income = max_( - 0, net_parental_deemed_income - parental_allocations - ) - - count_eligible_children = tax_unit.sum(eligible_child) - # avoid array divide-by-zero warnings by not using where() function - # see the following GitHub issue for more details: - # https://github.com/PolicyEngine/policyengine-us/issues/2494 - income = np.zeros_like(count_eligible_children) - mask = count_eligible_children > 0 - income[mask] = net_parental_deemed_income[mask] / count_eligible_children[mask] - return income + # 416.1165(e)(2): divide equally among the eligible children who share + # these parents. + children = ssi_parental_deeming_children(pool, applies) + return where(applies, net / max_(1, children), 0) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/ssi_ineligible_parent_allocation.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/ssi_ineligible_parent_allocation.py deleted file mode 100644 index 3a5118b4ca8..00000000000 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/deemed/ssi_ineligible_parent_allocation.py +++ /dev/null @@ -1,28 +0,0 @@ -from policyengine_us.model_api import * -from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( - _apply_ssi_exclusions, -) - - -class ssi_ineligible_parent_allocation(Variable): - value_type = float - entity = Person - label = "SSI ineligible parent allocation" - unit = USD - documentation = "The amount of income that SSI does not to SSI claimants." - definition_period = YEAR - reference = "https://www.law.cornell.edu/cfr/text/20/416.1163" - - def formula(person, period, parameters): - ssi = parameters(period).gov.ssa.ssi.amount - ineligible_parent = person("is_ssi_ineligible_parent", period) - count_ineligible_parents = person.tax_unit.sum(ineligible_parent) - return ( - where( - count_ineligible_parents == 2, - ssi.couple / 2, - ssi.individual, - ) - * MONTHS_IN_YEAR - * ineligible_parent - ) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/_ssi_parents.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/_ssi_parents.py new file mode 100644 index 00000000000..3c4bee3cc98 --- /dev/null +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/_ssi_parents.py @@ -0,0 +1,105 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( + _ssi_established_spouse_indices, +) +from policyengine_us.variables.household.demographic.person._parent_links import ( + co_resident_parent_indices, + has_parent_ids, + unlinked_parent, +) + + +def _ssi_unlinked_parental_deemors(person, period, spouse, with_spouses): + """Return tax-unit/household proxy deemor rows and their count. + + The deemors are the person's co-resident tax-unit members who are unlinked + parents or a parent's spouse, and the spouse of each of those, wherever + that spouse files (416.1160(d), 416.1202(b)(1): a parent and the parent's + spouse). Without with_spouses only the unlinked parents count. With no + parent ids, is_parent cannot distinguish a grandparent or an unrelated + parent from the child's parent: this is a data limitation. A person who + is an unlinked parent is in their own set, as in the proxy sets of the + siblings they share a tax unit with, so a teen parent and their brothers + and sisters keep one pool. Income deeming never counts the person as + their own ineligible parent. + """ + unlinked = unlinked_parent(person, period) + has_spouse = spouse >= 0 + parent_or_spouse = unlinked | (has_spouse & unlinked[np.maximum(spouse, 0)]) + group = person.tax_unit + indices = np.arange(person.count) + households = person.household.reference_entity.members_entity_id + tax_units = group.reference_entity.members_entity_id + positions = range( + int(np.max(group.reference_entity.members_position, initial=-1)) + 1 + ) + + def deemors(): + for position in positions: + member = group.value_nth_person(position, indices, default=-1) + safe = np.maximum(member, 0) + selected = ( + (member >= 0) + & (households[safe] == households) + & where(with_spouses, parent_or_spouse[safe], unlinked[safe]) + ) + yield member, selected + # Members of this tax unit are scanned above; add spouses who + # file elsewhere. Each person has at most one spouse. + partner = np.where(selected & with_spouses, spouse[safe], -1) + outside = (partner >= 0) & (tax_units[np.maximum(partner, 0)] != tax_units) + yield partner, outside + + count = np.zeros(person.count, dtype=int) + for _, selected in deemors(): + count += selected + rows = np.full((person.count, max(4, int(np.max(count, initial=0)))), -1) + filled = np.zeros(person.count, dtype=int) + for members, selected in deemors(): + rows[indices[selected], filled[selected]] = members[selected] + filled += selected + return rows + + +def ssi_parental_deemors(person, period, with_spouses=True): + """Return each person's parental deemor rows and a pool id per set. + + 20 CFR 416.1160(d): an ineligible parent is 'a natural or adoptive + parent, or the spouse ... of a natural or adoptive parent, who lives with + you'; 416.1202(b)(1) names the same people for resources. A person's + deemors are their named co-resident natural, adoptive or step parents, + wherever those file, and an established co-resident spouse of each. + Duplicate named or inferred parents count once. Without ids, only + unlinked parents enter the tax-unit proxy, as the parent-link contract + requires. SSI eligibility is not tested here: income deeming drops + eligible parents (416.1165(g)(1)), resource deeming keeps them (POMS SI + 01330.310). with_spouses (a bool or a per-person array) set to False + keeps the named or presumed parents only, without their spouses. + + Returns rows, an int array with one row per person holding their deemors' + row indices sorted ascending and padded with -1 (first), and pool, an int + array equal for people whose deemor sets are equal. SI 01330.200 B and + 416.1165(e)(2) divide among the children of the same parent(s), so these + pools are exact deemor sets rather than tax units, including when linked + and unlinked children share a set. + """ + with_spouses = np.broadcast_to(with_spouses, (person.count,)) + first, second = co_resident_parent_indices(person, period) + spouse = _ssi_established_spouse_indices(person, period, first, second) + rows = _ssi_unlinked_parental_deemors(person, period, spouse, with_spouses) + linked = has_parent_ids(person, period) + if np.any(linked): + first_spouse = np.where((first >= 0) & with_spouses, spouse[first], -1) + second_spouse = np.where((second >= 0) & with_spouses, spouse[second], -1) + named = np.column_stack((first, second, first_spouse, second_spouse)) + named.sort(axis=1) + # Deduplicate before sorting again to give every identical set the + # same key, independently of input parent slot or household order. + named[:, 1:] = np.where(named[:, 1:] == named[:, :-1], -1, named[:, 1:]) + named.sort(axis=1) + rows[linked] = -1 + rows[linked, :4] = named[linked] + # Sorted rows make equal deemor sets equal keys, linked or not. + rows.sort(axis=1) + _, pool = np.unique(rows, axis=0, return_inverse=True) + return rows, pool.reshape(-1) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.py index 2c0a1f658dd..bc9c200fb81 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.py @@ -1,95 +1,19 @@ from policyengine_us.model_api import * -from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( - _ssi_established_spouse_indices, +from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_parents import ( + ssi_parental_deemors, ) -from policyengine_us.variables.household.demographic.person._parent_links import ( - co_resident_parent_indices, - has_parent_ids, - unlinked_parent, -) - - -def _ssi_unlinked_parental_resource_pool(person, resources, parent, spouse): - """Return tax-unit/household proxy resources, count and exact pool keys. - - The deemors are the child's co-resident tax-unit members who are unlinked - parents or a parent's spouse, and the spouse of each of those, wherever - that spouse files (416.1202(b)(1): a parent and the parent's spouse). - With no parent ids, is_parent cannot distinguish a grandparent or an - unrelated parent from the child's parent: this is a data limitation. - Keys hold every deemor's row, sorted, so children share a pool exactly - when they share a deemor set. - """ - parent = parent | ((spouse >= 0) & parent[np.maximum(spouse, 0)]) - group = person.tax_unit - indices = np.arange(person.count) - households = person.household.reference_entity.members_entity_id - tax_units = group.reference_entity.members_entity_id - positions = range( - int(np.max(group.reference_entity.members_position, initial=-1)) + 1 - ) - - def deemors(): - for position in positions: - member = group.value_nth_person(position, indices, default=-1) - safe = np.maximum(member, 0) - selected = (member >= 0) & (households[safe] == households) & parent[safe] - yield member, selected - # Members of this tax unit are scanned above; add spouses who - # file elsewhere. Each person has at most one spouse. - partner = np.where(selected, spouse[safe], -1) - outside = (partner >= 0) & (tax_units[np.maximum(partner, 0)] != tax_units) - yield partner, outside - - count = np.zeros(person.count, dtype=int) - for _, selected in deemors(): - count += selected - keys = np.full((person.count, max(4, int(np.max(count, initial=0)))), -1) - total = np.zeros(person.count) - filled = np.zeros(person.count, dtype=int) - for rows, selected in deemors(): - keys[indices[selected], filled[selected]] = rows[selected] - total += np.where(selected, resources[np.maximum(rows, 0)], 0) - filled += selected - return total, count, keys def _ssi_parental_resource_pool(person, period, resources): - """Resolve relationships first and identify children sharing deemors. + """Total and count each child's parental deemors' resources. - 416.1202(b)(1) includes named co-resident natural, adoptive or step - parents, wherever their tax units, and an established co-resident spouse - of each parent. Duplicate named/inferred parents count once. Without - ids, use only unlinked_parent in the tax-unit proxy, as the parent-link - contract requires. SI 01330.200 B divides the excess among children of - the same parent(s), so allocation groups are exact deemor sets rather - than tax units, including when linked and unlinked children share a set. + The deemors and pools come from ssi_parental_deemors: named co-resident + parents and their spouses, or the tax-unit proxy without ids. """ - first, second = co_resident_parent_indices(person, period) - spouse = _ssi_established_spouse_indices(person, period, first, second) - total, count, keys = _ssi_unlinked_parental_resource_pool( - person, resources, unlinked_parent(person, period), spouse - ) - linked = has_parent_ids(person, period) - if np.any(linked): - first_spouse = np.where(first >= 0, spouse[first], -1) - second_spouse = np.where(second >= 0, spouse[second], -1) - named = np.column_stack((first, second, first_spouse, second_spouse)) - named.sort(axis=1) - # Deduplicate before sorting again to give every identical set the - # same key, independently of input parent slot or household order. - named[:, 1:] = np.where(named[:, 1:] == named[:, :-1], -1, named[:, 1:]) - named.sort(axis=1) - linked_total = np.sum(np.where(named >= 0, resources[named], 0), axis=1) - linked_count = np.sum(named >= 0, axis=1) - total = np.where(linked, linked_total, total) - count = np.where(linked, linked_count, count) - keys[linked] = -1 - keys[linked, :4] = named[linked] - # Sorted rows make equal deemor sets equal keys, linked or not. - keys.sort(axis=1) - _, pool = np.unique(keys, axis=0, return_inverse=True) - return total, count, pool + rows, pool = ssi_parental_deemors(person, period) + present = rows >= 0 + total = np.sum(np.where(present, resources[np.maximum(rows, 0)], 0), axis=1) + return total, np.sum(present, axis=1), pool class ssi_resources_deemed_from_ineligible_parent(Variable): diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/status/is_ssi_ineligible_parent.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/status/is_ssi_ineligible_parent.py index ec194b0e8d4..42d873097c7 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/status/is_ssi_ineligible_parent.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/status/is_ssi_ineligible_parent.py @@ -1,16 +1,27 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.income.deemed.from_ineligible_parent._ssi_parental_deeming import ( + ssi_ineligible_parents, +) class is_ssi_ineligible_parent(Variable): value_type = bool entity = Person label = "Is an SSI-ineligible parent in respect of a child" + documentation = ( + "Whether this person is an ineligible parent of a co-resident child " + "to whom SSI parental income deeming applies: the child's natural, " + "adoptive or step parent, or the co-resident spouse of one, who does " + "not meet the SSI status conditions." + ) definition_period = YEAR + reference = ( + "https://www.ecfr.gov/current/title-20/section-416.1160#p-416.1160(d)", + "https://www.ecfr.gov/current/title-20/section-416.1165", + ) def formula(person, period, parameters): - eligible = person("is_ssi_aged_blind_disabled", period) - child = person("is_child", period) - is_parent = person("is_parent", period) - return ( - ~eligible & ~child & is_parent & (person.tax_unit.sum(eligible & child) > 0) - ) + rows, ineligible, _ = ssi_ineligible_parents(person, period) + applies = person("is_ssi_income_deeming_child", period) + deemors = rows[applies[:, None] & ineligible] + return np.isin(np.arange(person.count), deemors) diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/status/ssi_federal_living_arrangement.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/status/ssi_federal_living_arrangement.py index 5baab0dff67..60a2716b354 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/status/ssi_federal_living_arrangement.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/status/ssi_federal_living_arrangement.py @@ -33,21 +33,12 @@ def formula(person, period, parameters): "ssi_lives_in_medical_treatment_facility", period ) & person("ssi_medicaid_pays_majority_of_care", period) - # 20 CFR § 416.1165: Child under 18 with ineligible parent - # in the tax unit. Under-22 students are excluded because - # is_ssi_ineligible_parent keys off is_child (age < 18); - # extending to under-22 students requires updating the - # deeming framework. - has_ineligible_parent = ( - person.tax_unit.sum( - person.tax_unit.members("is_ssi_ineligible_parent", period) - ) - > 0 - ) - is_child_in_parental = ( - ~is_medical_facility - & person("is_child", period.this_year) - & has_ineligible_parent + # 20 CFR § 416.1165: an eligible child under 18 who lives with an + # ineligible parent, so that parental deeming applies. Deeming ends + # in the month after the child turns 18 (416.1165(g)(7)), so + # under-22 students are not included. + is_child_in_parental = ~is_medical_facility & person( + "is_ssi_income_deeming_child", period.this_year ) # 20 CFR § 416.1131–1133: In another person's household