From 3be763b7442f97eb21127180c5f99ebbf3e73aec Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 16:36:54 -0400 Subject: [PATCH 01/11] Apply CO OAP payment floor and 3.533-3.534 income rules (WIP) Co-Authored-By: Claude Opus 5.5 --- changelog.d/co-oap-payment-floor.fixed.md | 1 + .../gov/states/co/ssa/oap/grant_standard.yaml | 2 +- .../deeming/child_allocation_share.yaml | 14 +++++ .../co/ssa/oap/income/disregards/earned.yaml | 15 +++++ .../oap/income/disregards/earned_share.yaml | 14 +++++ .../ssa/oap/income/disregards/unearned.yaml | 15 +++++ .../gov/states/co/ssa/oap/payment_floor.yaml | 21 +++++++ .../variables/gov/states/co/ssa/oap/co_oap.py | 23 ++++++-- .../co/ssa/oap/co_oap_countable_income.py | 14 +++++ .../states/co/ssa/oap/co_oap_deemed_income.py | 59 +++++++++++++++++++ .../co/ssa/oap/co_oap_deeming_exempt.py | 34 +++++++++++ .../co/ssa/oap/co_oap_own_countable_income.py | 51 ++++++++++++++++ 12 files changed, 256 insertions(+), 7 deletions(-) create mode 100644 changelog.d/co-oap-payment-floor.fixed.md create mode 100644 policyengine_us/parameters/gov/states/co/ssa/oap/income/deeming/child_allocation_share.yaml create mode 100644 policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/earned.yaml create mode 100644 policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/earned_share.yaml create mode 100644 policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/unearned.yaml create mode 100644 policyengine_us/parameters/gov/states/co/ssa/oap/payment_floor.yaml create mode 100644 policyengine_us/variables/gov/states/co/ssa/oap/co_oap_countable_income.py create mode 100644 policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py create mode 100644 policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deeming_exempt.py create mode 100644 policyengine_us/variables/gov/states/co/ssa/oap/co_oap_own_countable_income.py diff --git a/changelog.d/co-oap-payment-floor.fixed.md b/changelog.d/co-oap-payment-floor.fixed.md new file mode 100644 index 00000000000..5a8c207ae28 --- /dev/null +++ b/changelog.d/co-oap-payment-floor.fixed.md @@ -0,0 +1 @@ +Apply Colorado's $189 Old Age Pension payment floor from January 2026, and count OAP income under 9 CCR 2503-5 3.533 and 3.534 for every client, so clients aged 60 to 64 who are not SSI-eligible no longer receive the full grant regardless of income. diff --git a/policyengine_us/parameters/gov/states/co/ssa/oap/grant_standard.yaml b/policyengine_us/parameters/gov/states/co/ssa/oap/grant_standard.yaml index 22552b1c0da..4274a4a6ced 100644 --- a/policyengine_us/parameters/gov/states/co/ssa/oap/grant_standard.yaml +++ b/policyengine_us/parameters/gov/states/co/ssa/oap/grant_standard.yaml @@ -16,6 +16,6 @@ metadata: - title: ADULT FINANCIAL PROGRAMS | Income Maintenance (Volume 3) | 3.530(A) href: https://sos.state.co.us/CCR/GenerateRulePdf.do?ruleVersionId=11264&fileName=9%20CCR%202503-5#page=57 - title: ADULT FINANCIAL PROGRAMS | Income Maintenance (Volume 3) | 3.530(A) - href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=80 + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=74 - title: Colorado Department of Human Services | OPERATION MEMO href: https://drive.google.com/file/d/1bc_hUAoSzUbyRuMCzcxk2zV7b4RqkwyC/view diff --git a/policyengine_us/parameters/gov/states/co/ssa/oap/income/deeming/child_allocation_share.yaml b/policyengine_us/parameters/gov/states/co/ssa/oap/income/deeming/child_allocation_share.yaml new file mode 100644 index 00000000000..969d1bd29d5 --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/ssa/oap/income/deeming/child_allocation_share.yaml @@ -0,0 +1,14 @@ +description: Colorado deducts this share of the SSI benefit standard for an individual, less the child's own income, for each dependent child in the household when deeming a spouse's unearned income to an Old Age Pension client. + +values: + # OAP rules moved from 9 CCR 2503-3 to 9 CCR 2503-5 on March 2, 2014. + 2014-03-02: 0.5 + +metadata: + unit: /1 + label: Colorado OAP deeming allocation per dependent child as a share of the SSI benefit standard + reference: + - title: "9 CCR 2503-5, Section 3.534(B)(3) #page 78" + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=78 + - title: 9 CCR 2503-5, Section 3.534(B)(3) (effective March 2, 2014) + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=5597#page=48 diff --git a/policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/earned.yaml b/policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/earned.yaml new file mode 100644 index 00000000000..a46db14dcc2 --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/earned.yaml @@ -0,0 +1,15 @@ +description: Colorado deducts this amount from monthly gross earned income when counting income for the Old Age Pension. + +values: + # OAP rules moved from 9 CCR 2503-3 to 9 CCR 2503-5 on March 2, 2014. + 2014-03-02: 65 + +metadata: + unit: currency-USD + period: month + label: Colorado OAP earned income disregard + reference: + - title: "9 CCR 2503-5, Sections 3.533(A)(1)(a), 3.534(A)(1) #page 76-77" + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=76 + - title: 9 CCR 2503-5, Sections 3.533(A)(1)(a), 3.534(A)(1) (effective March 2, 2014) + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=5597#page=47 diff --git a/policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/earned_share.yaml b/policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/earned_share.yaml new file mode 100644 index 00000000000..b7874a274fd --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/earned_share.yaml @@ -0,0 +1,14 @@ +description: Colorado counts this share of monthly earned income, after the earned income disregard, for the Old Age Pension. + +values: + # OAP rules moved from 9 CCR 2503-3 to 9 CCR 2503-5 on March 2, 2014. + 2014-03-02: 0.5 + +metadata: + unit: /1 + label: Colorado OAP countable share of earned income + reference: + - title: "9 CCR 2503-5, Sections 3.533(A)(1)(b), 3.534(A)(2) #page 76-77" + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=76 + - title: 9 CCR 2503-5, Sections 3.533(A)(1)(b), 3.534(A)(2) (effective March 2, 2014) + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=5597#page=47 diff --git a/policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/unearned.yaml b/policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/unearned.yaml new file mode 100644 index 00000000000..ab166d2abaf --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/unearned.yaml @@ -0,0 +1,15 @@ +description: Colorado deducts this amount from monthly unearned income other than SSI when counting income for the Old Age Pension, splitting it equally between spouses. + +values: + # OAP rules moved from 9 CCR 2503-3 to 9 CCR 2503-5 on March 2, 2014. + 2014-03-02: 20 + +metadata: + unit: currency-USD + period: month + label: Colorado OAP unearned income disregard + reference: + - title: "9 CCR 2503-5, Section 3.533(A)(2)(c), (f) #page 77" + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=77 + - title: 9 CCR 2503-5, Section 3.533(A)(2)(c) (effective March 2, 2014) + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=5597#page=47 diff --git a/policyengine_us/parameters/gov/states/co/ssa/oap/payment_floor.yaml b/policyengine_us/parameters/gov/states/co/ssa/oap/payment_floor.yaml new file mode 100644 index 00000000000..7dc0e4ccd0f --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/ssa/oap/payment_floor.yaml @@ -0,0 +1,21 @@ +description: Colorado raises an Old Age Pension grant below this amount to it with a state supplementary payment. + +values: + # Before 2026 Colorado met the SSI pass-along requirement by maintaining + # total expenditures, with irregular lump-sum payments and no payment floor. + 2002-01-01: 0 + 2026-01-01: 189 + +metadata: + unit: currency-USD + period: month + label: Colorado OAP payment floor standard + reference: + - title: "9 CCR 2503-5, Sections 3.530(C), 3.531(D)(2), 3.532(B), 3.533(B), OAP payment floor standard #page 74-77" + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=74 + - title: 9 CCR 2503-5, Section 3.530(C) (emergency rule effective January 1, 2026) + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12318#page=58 + - title: 9 CCR 2503-5, Section 3.531(D) (effective March 2, 2025), MOE payments without a payment floor + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=11861#page=58 + - title: "Colorado State Board of Human Services, Adult Financial Payment Floor and COLA 2026, rulemaking packet 2025-00596 #page 1,4,8" + href: https://www.sos.state.co.us/CCR/Upload/AGORequestEmergency/RegulatoryAnalysisAttachment2025-00596.pdf#page=1 diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py index 9a95709317f..18f4df65c02 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py @@ -5,13 +5,24 @@ class co_oap(Variable): value_type = float entity = Person label = "Colorado Old Age Pension" + unit = USD definition_period = YEAR defined_for = "co_oap_eligible" + # PDF pages 64, 74-77 + reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=64" def formula(person, period, parameters): - income = person("ssi_countable_income", period) - ssi = person("ssi", period) - total_countable_income = ssi + income - p = parameters(period).gov.states.co.ssa.oap - grant_standard = p.grant_standard * MONTHS_IN_YEAR - return max_(0, grant_standard - total_countable_income) + total = 0 + for month in period.get_subperiods(MONTH): + p = parameters(month).gov.states.co.ssa.oap + # 9 CCR 2503-5 3.532(B), 3.533(B): the grant is the grant standard + # less total countable income, SSI included (3.533(A)(2)(d)). + income = person("ssi", month) + person("co_oap_countable_income", month) + grant = max_(0, p.grant_standard - income) + # 3.533 and 3.520.781(A): a client whose total countable income + # equals or exceeds the grant standard is not eligible. + # 3.531(D)(2), 3.532(B), 3.533(B): an eligible client's grant below + # the payment floor standard is raised to it by a state + # supplementary payment, month by month. + total = total + where(grant > 0, max_(grant, p.payment_floor), 0) + return total diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_countable_income.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_countable_income.py new file mode 100644 index 00000000000..46abc1893a7 --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_countable_income.py @@ -0,0 +1,14 @@ +from policyengine_us.model_api import * + + +class co_oap_countable_income(Variable): + value_type = float + entity = Person + label = "Colorado Old Age Pension countable income other than SSI" + unit = USD + definition_period = MONTH + # PDF pages 76-78 + reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=76" + # 9 CCR 2503-5 3.520.781(D)-(F): the client's own countable income + # (3.533) plus income deemed from the spouse (3.534). + adds = ["co_oap_own_countable_income", "co_oap_deemed_income"] diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py new file mode 100644 index 00000000000..a466ecd9de6 --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py @@ -0,0 +1,59 @@ +from policyengine_us.model_api import * + + +class co_oap_deemed_income(Variable): + value_type = float + entity = Person + label = "Colorado Old Age Pension income deemed from a spouse" + unit = USD + definition_period = MONTH + defined_for = StateCode.CO + # PDF pages 77-78 + reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=77" + + def formula(person, period, parameters): + p = parameters(period).gov.states.co.ssa.oap + # 3.534(C): no income is deemed from a spouse who receives Adult + # Financial, SSI or Medicaid with income within the OAP limit. + deemable = ~person("co_oap_deeming_exempt", period) + earned = person("ssi_earned_income", period) * deemable + unearned = person("ssi_unearned_income", period) * deemable + # 3.534(B)(5): court-ordered support obligations of the spouse. Whether + # the income is garnished instead is not modeled. + support = ( + add(person, period, ["child_support_expense", "alimony_expense"]) * deemable + ) + marital_unit = person.marital_unit + spouse_earned = marital_unit.sum(earned) - earned + spouse_unearned = marital_unit.sum(unearned) - unearned + spouse_support = marital_unit.sum(support) - support + + # 3.534(B)(3): for each dependent child in the household, half the SSI + # benefit standard for an individual less the child's own income. + year = period.this_year + dependent_child = person("is_tax_unit_dependent", year) & person( + "is_child", year + ) + child_income = add(person, period, ["ssi_earned_income", "ssi_unearned_income"]) + fbr = parameters(period).gov.ssa.ssi.amount.individual + child_allocation = dependent_child * max_( + 0, fbr * p.income.deeming.child_allocation_share - child_income + ) + child_allocations = person.household.sum(child_allocation) + + # 3.534(A): deduct $65 from the spouse's gross earned income and halve + # the rest. Unlike SSI (20 CFR 416.1163) there is no deeming threshold. + deemed_earned = ( + max_(0, spouse_earned - p.income.disregards.earned) + * p.income.disregards.earned_share + ) + # 3.534(B): deduct the OAP grant standard, child allocations and + # support obligations from the spouse's unearned income. Medical care + # payments for uninsured dependents (3.534(B)(4)) are not modeled. + deemed_unearned = max_( + 0, + spouse_unearned - p.grant_standard - child_allocations - spouse_support, + ) + # Sponsor deeming (3.534(D)) and the institutionalized spouse's MMMNA + # (3.534(C)) are not modeled. + return (deemed_earned + deemed_unearned) * (marital_unit.nb_persons() == 2) diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deeming_exempt.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deeming_exempt.py new file mode 100644 index 00000000000..dc3c64b1be9 --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deeming_exempt.py @@ -0,0 +1,34 @@ +from policyengine_us.model_api import * + + +class co_oap_deeming_exempt(Variable): + value_type = bool + entity = Person + label = "Colorado Old Age Pension spouse income not deemed" + definition_period = MONTH + defined_for = StateCode.CO + # PDF page 78 + reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=78" + + def formula(person, period, parameters): + # 9 CCR 2503-5 3.534(C): the income of a spouse who receives Adult + # Financial grant payments, SSI or Medicaid "and has income no greater + # than the OAP limit" is not deemed to the OAP client. + p = parameters(period).gov.states.co.ssa.oap + ssi = person("ssi", period) + # The rule does not define the OAP limit. We read it as the OAP grant + # standard, against the spouse's own total countable income under + # 3.533, SSI included. + income = person("co_oap_own_countable_income", period) + ssi + within_limit = income <= p.grant_standard + receives_ssi = (ssi > 0) | person("receives_ssi", period) + # Old Age Pension is the Adult Financial program modeled for someone + # who is not on SSI; AND-CS requires SSI. Receipt is judged on the + # spouse's own income: when both spouses are OAP clients within the + # limit, neither one's income is deemed to the other. + year = period.this_year + receives_oap = person("co_oap_eligible", year) & (income < p.grant_standard) + # SNAP counts OAP as income, so Medicaid enrollment is read without + # SNAP's community-engagement pass-through to avoid a cycle. + medicaid = person("medicaid_enrolled_for_ssi_state_supplement", year) + return (receives_ssi | receives_oap | medicaid) & within_limit diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_own_countable_income.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_own_countable_income.py new file mode 100644 index 00000000000..07902c518be --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_own_countable_income.py @@ -0,0 +1,51 @@ +from policyengine_us.model_api import * + + +class co_oap_own_countable_income(Variable): + value_type = float + entity = Person + label = "Colorado Old Age Pension countable income of the client other than SSI" + unit = USD + definition_period = MONTH + defined_for = StateCode.CO + # PDF pages 76-77 + reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=76" + + def formula(person, period, parameters): + # 9 CCR 2503-5 3.533(A) counts the client's own income whether or not + # they are eligible for SSI. The SSI that 3.533(A)(2)(d) adds back is + # added in co_oap. + p = parameters(period).gov.states.co.ssa.oap.income.disregards + # Gross income uses SSI's earned and unearned income sources. + # 3.520.786(K) exempts the unearned income SSI excludes, but limits + # exempt earned income to the items it lists, so SSI's student earned + # income exclusion is not deducted. + earned = person("ssi_earned_income", period) + ssi = person("ssi", period) + receives_ssi = ssi > 0 + # 3.520.79(B)(6): an SSI recipient charged in-kind support and + # maintenance by SSA is charged a matching ISM. A joint couple's ISM is + # one presumed maximum value for the couple, which SSI splits equally + # between the spouses. + ism = person("ssi_in_kind_support_and_maintenance", period) + couple_computation = person("ssi_couple_computation_applies", period) + ism = where(couple_computation, ism / 2, ism) * receives_ssi + unearned = person("ssi_unearned_income", period) + ism + # 3.533(A)(2)(c)(1): a married client's $20 disregard is split equally + # with the spouse, so that no more than $20 applies to the couple. + married = person.marital_unit.nb_persons() == 2 + disregard = where(married, p.unearned / 2, p.unearned) + # 3.533(A)(2)(c): the disregard applies to unearned income other than + # SSI; min_ gives 0 to a client whose only unearned income is SSI + # (3.533(A)(2)(c)(2)). + unearned_disregard = min_(disregard, unearned) + # 3.533(A)(2)(f): when gross unearned income, SSI included, is under + # $20, the rest of the disregard reduces earned income. A client whose + # only unearned income is SSI has no disregard to pass on. + ssi_only = receives_ssi & (unearned == 0) + earned_disregard = where(ssi_only, 0, max_(0, disregard - unearned - ssi)) + # 3.533(A)(1): deduct $65 from gross earned income and halve the rest. + countable_earned = ( + max_(0, earned - p.earned - earned_disregard) * p.earned_share + ) + return unearned - unearned_disregard + countable_earned From 3baff815ed9ab982c20e4214bc1363b2ed0d2243 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 16:52:39 -0400 Subject: [PATCH 02/11] Add OAP YAML, property and calculation-order tests Co-Authored-By: Claude Opus 5.5 --- .../tests/core/test_co_oap_invariants.py | 247 +++++++++++++ ...si_state_supplement_medicaid_dependency.py | 40 +++ .../gov/states/co/ssa/oap/co_oap.yaml | 326 +++++++++++++++++- .../co/ssa/oap/co_oap_deemed_income.yaml | 298 ++++++++++++++++ .../co/ssa/oap/co_oap_deeming_exempt.yaml | 95 +++++ .../ssa/oap/co_oap_own_countable_income.yaml | 196 +++++++++++ 6 files changed, 1194 insertions(+), 8 deletions(-) create mode 100644 policyengine_us/tests/core/test_co_oap_invariants.py create mode 100644 policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deemed_income.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deeming_exempt.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_own_countable_income.yaml diff --git a/policyengine_us/tests/core/test_co_oap_invariants.py b/policyengine_us/tests/core/test_co_oap_invariants.py new file mode 100644 index 00000000000..c5f2ba02cbb --- /dev/null +++ b/policyengine_us/tests/core/test_co_oap_invariants.py @@ -0,0 +1,247 @@ +"""Property tests: Colorado Old Age Pension countable income, deeming and floor. + +9 CCR 2503-5 3.533(A) counts an OAP client's own income; 3.534(A)-(B) deem +part of a spouse's income to the client, except under 3.534(C) when the +spouse receives Adult Financial grant payments, SSI or Medicaid with income +within the OAP limit; 3.533 and 3.520.781(A) make a client whose total +countable income (SSI included) equals or exceeds the grant standard +ineligible; and 3.530(C), 3.532(B), 3.533(B) raise a grant below the $189 +payment floor standard to it. + +Each case is a client aged 60-80 in their own household, with pension, +interest, Social Security and earnings, optionally receiving outside shelter +support, and either single or married to a spouse who has earnings and a +pension. Each married case appears four times: (A) the spouse is enrolled in +Medicaid, (B) the spouse is not, (C) as B with extra spousal earnings, and +(D) as B with the tax-unit roles swapped. For 2026: + +1. Payment: OAP is the grant standard less total countable income when that + is positive, raised to the floor if below it, and zero otherwise; so it is + zero or between the floor and the standard each month. +2. Singles: for a client not on SSI, with no earnings or at least $20 of + unearned income, 3.533(A) counts the same income as SSI's exclusions + (20 CFR 416.1112, 416.1124) applied to the same gross income. For a + client on SSI, 3.533(A) never counts less than SSI does. +3. Exemption: a spouse's Medicaid enrollment can only lower the client's + countable income and raise OAP. +4. Monotonicity: more spousal earnings never lower the client's countable + income, and never raise OAP unless they lower the client's own SSI. That + exception is intended: OAP counts the client's SSI, and a spouse's + earnings lower it under SSI's couple computation or spousal deeming + (e.g. an SSI couple aged 65 with no income: $85/month of spousal + earnings cut each spouse's SSI by $0.02, and OAP rises by as much). +5. Role invariance: the client's countable income and OAP do not depend on + which spouse is the tax-unit head. + +YAML states the worked cases; Python is needed to compare arrangements of +the same household in one simulation and to check against SSI's exclusions. +""" + +import numpy as np +import pytest + +from policyengine_us import Simulation +from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( + _apply_ssi_exclusions, +) + +YEAR = 2026 +TOLERANCE = 0.01 +ARRANGEMENTS = "ABCD" + + +def build_situation(cases): + people, groups = ( + {}, + { + name: {} + for name in ( + "tax_units", + "marital_units", + "spm_units", + "families", + "households", + ) + }, + ) + for i, c in enumerate(cases): + for arrangement in ARRANGEMENTS if c["married"] else "A": + key = f"{arrangement}{i}" + client, spouse = f"client_{key}", f"spouse_{key}" + people[client] = { + "age": {YEAR: c["client_age"]}, + "pension_income": {YEAR: c["client_pension"]}, + "interest_income": {YEAR: c["client_interest"]}, + "social_security_retirement": {YEAR: c["client_social_security"]}, + "employment_income": {YEAR: c["client_earnings"]}, + "ssi_receives_outside_shelter_support": {YEAR: c["shelter_support"]}, + # Set for everyone: an input for some people gives the others + # its default (false), not its formula. + "medicaid_enrolled_for_ssi_state_supplement": {YEAR: True}, + } + members = [client] + if c["married"]: + extra = c["extra_spouse_earnings"] if arrangement == "C" else 0 + people[spouse] = { + "age": {YEAR: c["spouse_age"]}, + "employment_income": {YEAR: c["spouse_earnings"] + extra}, + "pension_income": {YEAR: c["spouse_pension"]}, + "medicaid_enrolled_for_ssi_state_supplement": { + YEAR: arrangement == "A" + }, + } + members = [spouse, client] if arrangement == "D" else [client, spouse] + head, partner = members + people[head]["is_tax_unit_head"] = {YEAR: True} + people[head]["is_tax_unit_spouse"] = {YEAR: False} + people[partner]["is_tax_unit_head"] = {YEAR: False} + people[partner]["is_tax_unit_spouse"] = {YEAR: True} + for name, group in groups.items(): + group[f"{name}_{key}"] = {"members": members} + groups["households"][f"households_{key}"]["state_code"] = {YEAR: "CO"} + return {"people": people, **groups} + + +def calculate(cases): + """Arrays indexed by person name for every person in every arrangement.""" + simulation = Simulation(situation=build_situation(cases)) + names = list(simulation.populations["person"].ids) + results = {} + for variable in ( + "co_oap", + "co_oap_countable_income", + "co_oap_own_countable_income", + "co_oap_eligible", + "ssi", + "ssi_countable_income", + "ssi_earned_income", + "ssi_unearned_income", + ): + values = np.asarray(simulation.calculate(variable, YEAR)) + results[variable] = dict(zip(names, values)) + p = simulation.tax_benefit_system.parameters(f"{YEAR}-01").gov.states.co.ssa.oap + return results, simulation.tax_benefit_system.parameters, p + + +def assert_properties(cases): + results, parameters, p = calculate(cases) + standard, floor = p.grant_standard, p.payment_floor + oap = results["co_oap"] + countable = results["co_oap_countable_income"] + eligible = results["co_oap_eligible"] + ssi = results["ssi"] + for i, c in enumerate(cases): + for arrangement in ARRANGEMENTS if c["married"] else "A": + client = f"client_{arrangement}{i}" + # Monthly amounts: the inputs and the program are constant + # within the year. + total = (ssi[client] + countable[client]) / 12 + paid = oap[client] / 12 + # 1. Payment rule and bounds. + if not eligible[client]: + assert paid == 0, c + elif total < standard - TOLERANCE: + assert paid == pytest.approx(max(standard - total, floor), abs=0.05) + assert floor - TOLERANCE <= paid <= standard + TOLERANCE, c + elif total > standard + TOLERANCE: + assert paid == 0, c + client = f"client_A{i}" + if not c["married"]: + # 2. Singles, against SSI's exclusions on the same gross income. + own = results["co_oap_own_countable_income"][client] + if ssi[client] > 0: + assert own >= results["ssi_countable_income"][client] - TOLERANCE, c + elif not c["shelter_support"]: + other_unearned = ( + c["client_pension"] + + c["client_interest"] + + c["client_social_security"] + ) + if c["client_earnings"] == 0 or other_unearned >= 20 * 12: + expected = _apply_ssi_exclusions( + np.array([results["ssi_earned_income"][client]]), + np.array([results["ssi_unearned_income"][client]]), + parameters, + YEAR, + )[0] + assert own == pytest.approx(expected, abs=TOLERANCE), c + continue + a, b, c_, d = (f"client_{x}{i}" for x in ARRANGEMENTS) + # 3. The Medicaid exemption only lowers countable income. + assert countable[a] <= countable[b] + TOLERANCE, c + assert oap[a] >= oap[b] - TOLERANCE, c + # 4. More spousal earnings never lower countable income, and raise + # OAP only through the client's SSI. + assert countable[c_] >= countable[b] - TOLERANCE, c + if ssi[c_] == pytest.approx(ssi[b], abs=TOLERANCE): + assert oap[c_] <= oap[b] + TOLERANCE, c + # 5. Roles. + assert countable[d] == pytest.approx(countable[b], abs=TOLERANCE), c + assert oap[d] == pytest.approx(oap[b], abs=TOLERANCE), c + + +def test_worked_example(): + """co_oap YAML Case 22, with and without the spouse on Medicaid. The + client, 62, has no income; the spouse, 50, earns $2,000/month. Without + Medicaid, 3.534(A) deems (2,000 - 65) / 2 = 967.50 and the grant of + 1,032 - 967.50 = 64.50 is raised to the $189 floor. With the spouse on + Medicaid, the spouse's own countable income, (2,000 - 65 - 10) / 2 = + 962.50, is within the OAP limit, so nothing is deemed and the client gets + the full 1,032/month.""" + case = { + "client_age": 62, + "client_pension": 0, + "client_interest": 0, + "client_social_security": 0, + "client_earnings": 0, + "shelter_support": False, + "married": True, + "spouse_age": 50, + "spouse_earnings": 24_000, + "spouse_pension": 0, + "extra_spouse_earnings": 1_200, + } + results, _, _ = calculate([case]) + oap = results["co_oap"] + countable = results["co_oap_countable_income"] + assert oap["client_A0"] == pytest.approx(12_384, abs=TOLERANCE) + assert countable["client_A0"] == pytest.approx(0, abs=TOLERANCE) + assert oap["client_B0"] == pytest.approx(2_268, abs=TOLERANCE) + assert countable["client_B0"] == pytest.approx(11_610, abs=TOLERANCE) + assert_properties([case]) + + +try: + import hypothesis + import hypothesis.strategies as st +except ImportError: # pragma: no cover + hypothesis = None + +if hypothesis is not None: + # Each example builds a Simulation; on a loaded runner input generation + # can trip the too_slow health check, which says nothing about the model. + SLOW = [hypothesis.HealthCheck.too_slow] + + def amount(high): + return st.one_of(st.just(0), st.integers(min_value=1, max_value=high)) + + @st.composite + def case(draw): + return { + "client_age": draw(st.integers(min_value=60, max_value=80)), + "client_pension": draw(amount(12_000)), + "client_interest": draw(amount(300)), + "client_social_security": draw(amount(15_000)), + "client_earnings": draw(amount(20_000)), + "shelter_support": draw(st.booleans()), + "married": draw(st.booleans()), + "spouse_age": draw(st.integers(min_value=18, max_value=80)), + "spouse_earnings": draw(amount(30_000)), + "spouse_pension": draw(amount(24_000)), + "extra_spouse_earnings": draw(st.integers(min_value=1, max_value=12_000)), + } + + @hypothesis.settings(max_examples=15, deadline=None, suppress_health_check=SLOW) + @hypothesis.given(st.lists(case(), min_size=1, max_size=8)) + def test_properties(cases): + assert_properties(cases) diff --git a/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py b/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py index a64c80a17a7..290630aae3e 100644 --- a/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py +++ b/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py @@ -57,6 +57,46 @@ def test_ssp_snap_medicaid_calculation_order(year, state, benefit): assert results[0]["medicaid_enrolled"].tolist() == [True, state == "IN"] +def co_oap_couple(year): + """An Old Age Pension client whose spouse is a SNAP workfare participant.""" + people = { + "client": {"age": {year: 62}}, + "spouse": { + "age": {year: 40}, + "employment_income": {year: 5_000}, + "is_snap_workfare_participant": {year: True}, + }, + } + unit = {"unit": {"members": list(people)}} + return { + "people": people, + **{ + entity: unit + for entity in ("tax_units", "marital_units", "spm_units", "families") + }, + "households": {"unit": {"members": list(people), "state_code": {year: "CO"}}}, + } + + +@pytest.mark.parametrize("year", [2027, 2030, 2035]) +def test_co_oap_snap_medicaid_calculation_order(year): + """Colorado OAP reads the spouse's Medicaid enrollment to decide whether the + spouse's income is deemed (9 CCR 2503-5 3.534(C)), and SNAP counts OAP as + income. YAML cannot express repeated calculations in different orders.""" + variables = ("co_oap", "snap", "medicaid_enrolled") + results = [] + for order in (variables, variables[::-1]): + simulation = Simulation(situation=co_oap_couple(year)) + results.append({name: simulation.calculate(name, year) for name in order}) + assert not any( + "ssi_state_supplement_medicaid" in name for name in simulation.branches + ) + for name in variables: + np.testing.assert_array_equal(results[0][name], results[1][name]) + assert results[0]["co_oap"][0] > 0 + assert results[0]["snap"][0] > 0 + + @pytest.mark.parametrize( "variable", ["medicaid_enrolled", "is_medicaid_eligible", "takes_up_medicaid_if_eligible"], diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml index b5fa2f9a340..6e5e2247269 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml @@ -1,35 +1,345 @@ -- name: Countable Income +- name: Case 1, countable income other than SSI reduces the grant. + # 9 CCR 2503-5 3.533(B): 952 x 12 - 1,000 = 10,424. period: 2023 input: co_oap_eligible: true - ssi_countable_income: 1_000 + co_oap_countable_income: 1_000 ssi: 0 output: co_oap: 10_424 -- name: Has some SSI +- name: Case 2, SSI reduces the grant. + # 3.533(A)(2)(d) adds SSI back to countable income: 11,424 - 2,000 = 9,424. period: 2023 input: co_oap_eligible: true - ssi_countable_income: 0 + co_oap_countable_income: 0 ssi: 2_000 output: co_oap: 9_424 -- name: More total income than grant standard +- name: Case 3, total countable income above the grant standard. period: 2023 input: co_oap_eligible: true - ssi_countable_income: 6_000 + co_oap_countable_income: 6_000 ssi: 6_000 output: co_oap: 0 -- name: 2026 grant standard +- name: Case 4, 2026 grant standard. period: 2026 input: co_oap_eligible: true - ssi_countable_income: 0 + co_oap_countable_income: 0 ssi: 0 output: co_oap: 12_384 + +- name: Case 5, the 2026 payment floor raises a grant below $189. + # SSI of $994/month: 1,032 - 994 = 38, raised to the $189 payment floor + # standard (3.530(C)) by a state supplementary payment (3.532(B), 3.533(B)). + # 189 x 12 = 2,268. + period: 2026 + input: + co_oap_eligible: true + co_oap_countable_income: 0 + ssi: 11_928 + output: + co_oap: 2_268 + +- name: Case 6, a grant at the floor is unchanged. + # 1,032 - 843 = 189/month. + period: 2026 + input: + co_oap_eligible: true + co_oap_countable_income: 10_116 + ssi: 0 + output: + co_oap: 2_268 + +- name: Case 7, a grant above the floor is unchanged. + # 1,032 - 800 = 232/month, or 2,784/year. + period: 2026 + input: + co_oap_eligible: true + co_oap_countable_income: 9_600 + ssi: 0 + output: + co_oap: 2_784 + +- name: Case 8, a grant of a fraction of a dollar is raised to the floor. + # 1,032 - 1,031.50 = 0.50/month, raised to 189. + period: 2026 + input: + co_oap_eligible: true + co_oap_countable_income: 12_378 + ssi: 0 + output: + co_oap: 2_268 + +- name: Case 9, income equal to the grant standard gets no floor. + # 3.533: a client whose total countable income equals or exceeds the grant + # standard is denied or discontinued (also 3.520.781(A)), so there is no + # grant to raise. + period: 2026 + input: + co_oap_eligible: true + co_oap_countable_income: 6_000 + ssi: 6_384 + output: + co_oap: 0 + +- name: Case 10, income above the grant standard gets no floor. + period: 2026 + input: + co_oap_eligible: true + co_oap_countable_income: 13_200 + ssi: 0 + output: + co_oap: 0 + +- name: Case 11, there was no payment floor in 2025. + # 1,005 - 1,000 = 5/month, or 60/year. Colorado met the pass-along + # requirement by total expenditures until the 2026 rule. + period: 2025 + input: + co_oap_eligible: true + co_oap_countable_income: 12_000 + ssi: 0 + output: + co_oap: 60 + +- name: Case 12, the floor applies month by month. + # January-June: 1,032 - 1,000 = 32, raised to 189. July-December: income of + # 1,100 exceeds the standard, so nothing is paid. 6 x 189 = 1,134. + period: 2026 + input: + co_oap_eligible: true + ssi: 0 + co_oap_countable_income: + 2026-01: 1_000 + 2026-02: 1_000 + 2026-03: 1_000 + 2026-04: 1_000 + 2026-05: 1_000 + 2026-06: 1_000 + 2026-07: 1_100 + 2026-08: 1_100 + 2026-09: 1_100 + 2026-10: 1_100 + 2026-11: 1_100 + 2026-12: 1_100 + output: + co_oap: 1_134 + +- name: Case 13, an ineligible person gets nothing. + period: 2026 + input: + co_oap_eligible: false + co_oap_countable_income: 0 + ssi: 0 + output: + co_oap: 0 + +- name: Case 14, a 62-year-old's pension reduces OAP. + # Not aged, blind or disabled, so ssi_countable_income is 0. 3.533(A)(2): + # 2,500/month of pension less the $20 disregard is 2,480, above the $1,032 + # standard. + period: 2026 + input: + people: + person: + age: 62 + pension_income: 30_000 + households: + household: + members: [person] + state_code: CO + output: + ssi_countable_income: 0 + co_oap_countable_income: 29_760 + co_oap: 0 + +- name: Case 15, a 62-year-old's earnings reduce OAP. + # 3.533(A)(1), (A)(2)(f): 5,000/month of earnings, no unearned income, so + # the whole $20 disregard reaches earnings: (5,000 - 65 - 20) / 2 = 2,457.50. + period: 2026 + input: + people: + person: + age: 62 + employment_income: 60_000 + households: + household: + members: [person] + state_code: CO + output: + co_oap_countable_income: 29_490 + co_oap: 0 + +- name: Case 16, a 62-year-old with earnings just under the standard gets the floor. + # (2,080 - 65 - 20) / 2 = 997.50; 1,032 - 997.50 = 34.50, raised to 189. + period: 2026 + input: + people: + person: + age: 62 + employment_income: 24_960 + households: + household: + members: [person] + state_code: CO + output: + co_oap_countable_income: 11_970 + co_oap: 2_268 + +- name: Case 17, early Social Security retirement benefits below the floor threshold. + # 900/month less the $20 disregard is 880; 1,032 - 880 = 152, raised to 189. + period: 2026 + input: + people: + person: + age: 62 + social_security_retirement: 10_800 + households: + household: + members: [person] + state_code: CO + output: + co_oap_countable_income: 10_560 + co_oap: 2_268 + +- name: Case 18, early Social Security retirement benefits above the floor threshold. + # 800 - 20 = 780; 1,032 - 780 = 252/month, or 3,024/year. + period: 2026 + input: + people: + person: + age: 62 + social_security_retirement: 9_600 + households: + household: + members: [person] + state_code: CO + output: + co_oap: 3_024 + +- name: Case 19, the same benefits in 2025, before the floor. + # 1,005 - 880 = 125/month, or 1,500/year. + period: 2025 + input: + people: + person: + age: 62 + social_security_retirement: 10_800 + households: + household: + members: [person] + state_code: CO + output: + co_oap: 1_500 + +- name: Case 20, an SSI recipient with no other income gets the floor. + # SSI pays 994/month; 1,032 - 994 = 38, raised to 189. + period: 2026 + input: + people: + person: + age: 70 + households: + household: + members: [person] + state_code: CO + output: + ssi: 11_928 + co_oap_countable_income: 0 + co_oap: 2_268 + +- name: Case 21, each member of an SSI couple is a client. + # SSI pays each spouse 745.50/month (half the $1,491 couple rate). Each is + # exempt from the other's deeming as an SSI recipient (3.534(C)): + # 1,032 - 745.50 = 286.50/month, or 3,438/year, above the floor. + period: 2026 + input: + people: + head: + age: 70 + is_tax_unit_head: true + spouse: + age: 68 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi: [8_946, 8_946] + co_oap_deemed_income: [0, 0] + co_oap: [3_438, 3_438] + +- name: Case 22, a working spouse's deemed earnings leave a grant below the floor. + # The client is 62 with no income. The spouse, 50, earns $2,000/month and + # does not take up Medicaid. 3.534(A): (2,000 - 65) / 2 = 967.50 deemed; + # 1,032 - 967.50 = 64.50, raised to 189. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + employment_income: 24_000 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_deemed_income: [11_610, 0] + co_oap: [2_268, 0] + +- name: Case 23, a spouse's pension above the grant standard is deemed. + # Both are 62. The spouse's $1,500/month pension: own countable income + # 1,500 - 10 = 1,490 exceeds the OAP limit, so it is deemed (3.534(B)): + # 1,500 - 1,032 = 468. The client: 1,032 - 468 = 564/month, or 6,768/year. + # The client has no income and is an OAP client within the limit, so + # nothing is deemed to the spouse, whose own income makes them ineligible. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 62 + pension_income: 18_000 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_deeming_exempt: [true, false] + co_oap_deemed_income: [5_616, 0] + co_oap: [6_768, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deemed_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deemed_income.yaml new file mode 100644 index 00000000000..05419c768d2 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deemed_income.yaml @@ -0,0 +1,298 @@ +- name: Case 1, a working spouse's earnings are deemed with no threshold. + # The client is 62 with no income. The spouse, 50, earns $1,500/month and + # does not take up Medicaid. 9 CCR 2503-5 3.534(A): + # (1,500 - 65) / 2 = 717.50/month, or 8,610/year. The client is an OAP + # client within the limit, so nothing is deemed to the spouse. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + employment_income: 18_000 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_deeming_exempt: [true, false] + co_oap_deemed_income: [8_610, 0] + +- name: Case 2, a spouse on Medicaid with income within the OAP limit has nothing deemed. + # Same couple, with the spouse enrolled in Medicaid. The spouse's own + # countable income, (1,500 - 65 - 10) / 2 = 712.50, is within the $1,032 + # limit, so 3.534(C) deems none of it. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + employment_income: 18_000 + medicaid_enrolled_for_ssi_state_supplement: true + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_own_countable_income: [0, 8_550] + co_oap_deeming_exempt: [true, true] + co_oap_deemed_income: [0, 0] + +- name: Case 3, a spouse's unearned income above the grant standard is deemed. + # The spouse, 50, has a $1,500/month pension. 3.534(B)(2): + # 1,500 - 1,032 = 468/month, or 5,616/year. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + pension_income: 18_000 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_deemed_income: [5_616, 0] + +- name: Case 4, the spouse's court-ordered child support is deducted. + # As Case 3, with $200/month of child support paid. 3.534(B)(5): + # 1,500 - 1,032 - 200 = 268/month, or 3,216/year. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + pension_income: 18_000 + child_support_expense: 2_400 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_deemed_income: [3_216, 0] + +- name: Case 5, an allocation for a dependent child with no income. + # The spouse, 50, has a $2,000/month pension; their child, 10, has no + # income. 3.534(B)(3) deducts half the $994 SSI benefit standard: + # 2,000 - 1,032 - 497 = 471/month, or 5,652/year. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + pension_income: 24_000 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + child: + age: 10 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, spouse, child] + marital_units: + marital_unit: + members: [head, spouse] + child_marital_unit: + members: [child] + households: + household: + members: [head, spouse, child] + state_code: CO + output: + co_oap_deemed_income: [5_652, 0, 0] + +- name: Case 6, the child allocation is reduced by the child's own income. + # As Case 5, with the child receiving $100/month of interest. The + # allocation is 497 - 100 = 397: 2,000 - 1,032 - 397 = 571/month, or + # 6,852/year. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + pension_income: 24_000 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + child: + age: 10 + interest_income: 1_200 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, spouse, child] + marital_units: + marital_unit: + members: [head, spouse] + child_marital_unit: + members: [child] + households: + household: + members: [head, spouse, child] + state_code: CO + output: + co_oap_deemed_income: [6_852, 0, 0] + +- name: Case 7, a spouse on SSI has nothing deemed. + # The spouse, 66, earns $1,000/month. SSI counts (1,000 - 20 - 65) / 2 = + # 457.50 and pays 536.50. The spouse's OAP income, (1,000 - 65) / 2 + + # 536.50 = 1,004, is within the $1,032 limit, so 3.534(C) deems nothing. + # 3.534(A) would otherwise deem (1,000 - 65) / 2 = 467.50. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 66 + employment_income: 12_000 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi: [0, 6_438] + co_oap_deeming_exempt: [true, true] + co_oap_deemed_income: [0, 0] + +- name: Case 8, spousal earnings under $65 deem nothing. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + employment_income: 600 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_deemed_income: [0, 0] + +- name: Case 9, two OAP clients within the limit have nothing deemed to each other. + # Both are 62. The spouse's $600/month pension gives own countable income + # 600 - 10 = 590, within the limit, and the spouse is an OAP client + # (1,032 - 590 = 442/month, or 5,304/year), so 3.534(C) deems nothing to + # the head, who gets the full 1,032/month. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 62 + pension_income: 7_200 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_deeming_exempt: [true, true] + co_oap_deemed_income: [0, 0] + co_oap: [12_384, 5_304] + +- name: Case 10, earned and unearned income are deemed together. + # The spouse, 50, earns $1,000/month and has a $1,200/month pension. + # (1,000 - 65) / 2 + (1,200 - 1,032) = 467.50 + 168 = 635.50/month, or + # 7,626/year. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + employment_income: 12_000 + pension_income: 14_400 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_deemed_income: [7_626, 0] + # The spouse: (1,200 - 10) + (1,000 - 65) / 2 = 1,657.50/month of their own. + co_oap_countable_income: [7_626, 19_890] diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deeming_exempt.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deeming_exempt.yaml new file mode 100644 index 00000000000..b0d62c7fbae --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deeming_exempt.yaml @@ -0,0 +1,95 @@ +- name: Case 1, an SSI recipient within the OAP limit. + # 9 CCR 2503-5 3.534(C): 400 + 600 = 1,000/month is within the $1,032 limit. + period: 2026 + input: + state_code: CO + ssi: 4_800 + co_oap_own_countable_income: 7_200 + co_oap_eligible: false + medicaid_enrolled_for_ssi_state_supplement: false + output: + co_oap_deeming_exempt: true + +- name: Case 2, an SSI recipient above the OAP limit. + # 100 + 1,000 = 1,100/month exceeds the limit. + period: 2026 + input: + state_code: CO + ssi: 1_200 + co_oap_own_countable_income: 12_000 + co_oap_eligible: false + medicaid_enrolled_for_ssi_state_supplement: false + output: + co_oap_deeming_exempt: false + +- name: Case 3, a Medicaid enrollee with income equal to the OAP limit. + # "No greater than the OAP limit" includes income equal to it. + period: 2026 + input: + state_code: CO + ssi: 0 + co_oap_own_countable_income: 12_384 + co_oap_eligible: false + medicaid_enrolled_for_ssi_state_supplement: true + output: + co_oap_deeming_exempt: true + +- name: Case 4, a Medicaid enrollee above the OAP limit. + period: 2026 + input: + state_code: CO + ssi: 0 + co_oap_own_countable_income: 12_396 + co_oap_eligible: false + medicaid_enrolled_for_ssi_state_supplement: true + output: + co_oap_deeming_exempt: false + +- name: Case 5, an OAP client is receiving Adult Financial grant payments. + # Own countable income of 1,031/month leaves a grant of 1, so the spouse + # receives OAP with income within the limit. + period: 2026 + input: + state_code: CO + ssi: 0 + co_oap_own_countable_income: 12_372 + co_oap_eligible: true + medicaid_enrolled_for_ssi_state_supplement: false + output: + co_oap_deeming_exempt: true + +- name: Case 6, an OAP-eligible spouse with income equal to the standard receives no grant. + # 3.533: income equal to the grant standard means denial, so the spouse is + # not receiving Adult Financial grant payments. + period: 2026 + input: + state_code: CO + ssi: 0 + co_oap_own_countable_income: 12_384 + co_oap_eligible: true + medicaid_enrolled_for_ssi_state_supplement: false + output: + co_oap_deeming_exempt: false + +- name: Case 7, a spouse with no assistance has income deemed however low it is. + period: 2026 + input: + state_code: CO + ssi: 0 + co_oap_own_countable_income: 0 + co_oap_eligible: false + medicaid_enrolled_for_ssi_state_supplement: false + output: + co_oap_deeming_exempt: false + +- name: Case 8, reported SSI receipt counts. + period: 2026 + input: + state_code: CO + ssi: 0 + receives_ssi: true + co_oap_own_countable_income: 0 + co_oap_eligible: false + medicaid_enrolled_for_ssi_state_supplement: false + output: + co_oap_deeming_exempt: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_own_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_own_countable_income.yaml new file mode 100644 index 00000000000..59b00258455 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_own_countable_income.yaml @@ -0,0 +1,196 @@ +- name: Case 1, earnings only, with the whole disregard reaching earnings. + # 9 CCR 2503-5 3.533(A)(2)(f): gross unearned income of $0 is under $20, so + # the $20 reduces earnings: (500 - 65 - 20) / 2 = 207.50/month, or + # 2,490/year. At 62 the client is not SSI-eligible, so SSI counts nothing. + period: 2026 + input: + people: + person: + age: 62 + employment_income: 6_000 + households: + household: + members: [person] + state_code: CO + output: + ssi_countable_income: 0 + co_oap_own_countable_income: 2_490 + +- name: Case 2, unearned income less the $20 disregard. + # 3.533(A)(2): 500 - 20 = 480/month, or 5,760/year. + period: 2026 + input: + people: + person: + age: 62 + pension_income: 6_000 + households: + household: + members: [person] + state_code: CO + output: + co_oap_own_countable_income: 5_760 + +- name: Case 3, unearned income under $20 passes the rest of the disregard to earnings. + # $10/month of interest and $500/month of earnings. The disregard covers the + # $10 of interest (3.533(A)(2)(c)) and the other $10 reduces earnings + # (3.533(A)(2)(f)): (500 - 65 - 10) / 2 = 212.50/month, or 2,550/year. + period: 2026 + input: + people: + person: + age: 62 + interest_income: 120 + employment_income: 6_000 + households: + household: + members: [person] + state_code: CO + output: + co_oap_own_countable_income: 2_550 + +- name: Case 4, a married client's half of the disregard reaches earnings. + # 3.533(A)(2)(c)(1) splits the $20 between the spouses, so the client's + # disregard is $10: (500 - 65 - 10) / 2 = 212.50/month. The spouse has no + # income. + period: 2026 + input: + people: + head: + age: 62 + employment_income: 6_000 + is_tax_unit_head: true + spouse: + age: 62 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_own_countable_income: [2_550, 0] + +- name: Case 5, a married client's half of the disregard reduces unearned income. + # 500 - 10 = 490/month, or 5,880/year. + period: 2026 + input: + people: + head: + age: 62 + pension_income: 6_000 + is_tax_unit_head: true + spouse: + age: 62 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_own_countable_income: [5_880, 0] + +- name: Case 6, an SSI-only client with earnings gets no unearned disregard. + # $300/month of earnings. SSI moves its $20 general exclusion to earnings: + # (300 - 20 - 65) / 2 = 107.50, so SSI pays 994 - 107.50 = 886.50. + # 3.533(A)(2)(c)(2) gives a client whose only unearned income is SSI no + # disregard, so none reaches earnings: (300 - 65) / 2 = 117.50/month, or + # 1,410/year. + period: 2026 + input: + people: + person: + age: 70 + employment_income: 3_600 + households: + household: + members: [person] + state_code: CO + output: + ssi: 10_638 + ssi_countable_income: 1_290 + co_oap_own_countable_income: 1_410 + +- name: Case 7, SSI brings gross unearned income above $20, so nothing reaches earnings. + # $5/month of interest and $200/month of earnings. The disregard covers the + # $5 of interest; gross unearned income including SSI is far above $20, so + # nothing reaches earnings (3.533(A)(2)(f)): (200 - 65) / 2 = 67.50/month, + # or 810/year. SSI moves the unused $15 to earnings: + # (200 - 65 - 15) / 2 = 60, or 720/year. + period: 2026 + input: + people: + person: + age: 70 + interest_income: 60 + employment_income: 2_400 + households: + household: + members: [person] + state_code: CO + output: + ssi_countable_income: 720 + co_oap_own_countable_income: 810 + +- name: Case 8, earnings under the $65 disregard count as nothing. + period: 2026 + input: + people: + person: + age: 62 + employment_income: 600 + households: + household: + members: [person] + state_code: CO + output: + co_oap_own_countable_income: 0 + +- name: Case 9, an SSI recipient charged ISM by SSA is charged a matching ISM. + # In their own household with shelter supplied from outside, SSA counts the + # presumed maximum value of 994 / 3 + 20 = 351.33/month and pays + # 994 - 331.33 = 662.67. 3.520.79(B)(6) charges the same ISM, which OAP + # counts as unearned income less the $20 disregard: 331.33/month, or + # 3,976/year. Total countable income 662.67 + 331.33 = 994 leaves a grant + # of 38, raised to the $189 floor. + period: 2026 + absolute_error_margin: 0.1 + input: + people: + person: + age: 70 + ssi_receives_outside_shelter_support: true + households: + household: + members: [person] + state_code: CO + output: + ssi: 7_952 + co_oap_own_countable_income: 3_976 + co_oap: 2_268 + +- name: Case 10, SSI's ISM is not charged to a client who is not on SSI. + # Colorado's own ISM for clients not on SSI (3.520.79(C)) is not modeled. + period: 2026 + input: + people: + person: + age: 62 + ssi_receives_outside_shelter_support: true + households: + household: + members: [person] + state_code: CO + output: + co_oap_own_countable_income: 0 From fda771a72942db8bb813705e3d4414a68b13fc8a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 18:57:36 -0400 Subject: [PATCH 03/11] Scope the monotonicity invariant; cover SSI-only clients with SSI under $20 Co-Authored-By: Claude Opus 5.5 --- policyengine_us/tests/core/test_co_oap_invariants.py | 7 ++++++- .../co/ssa/oap/co_oap_own_countable_income.yaml | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/policyengine_us/tests/core/test_co_oap_invariants.py b/policyengine_us/tests/core/test_co_oap_invariants.py index c5f2ba02cbb..38fc7da2f23 100644 --- a/policyengine_us/tests/core/test_co_oap_invariants.py +++ b/policyengine_us/tests/core/test_co_oap_invariants.py @@ -30,6 +30,10 @@ earnings lower it under SSI's couple computation or spousal deeming (e.g. an SSI couple aged 65 with no income: $85/month of spousal earnings cut each spouse's SSI by $0.02, and OAP rises by as much). + Countable income is checked only while the client's SSI receipt is + unchanged or the client has no shelter support: Colorado's own ISM for + a client not on SSI (3.520.79(C)) is not modeled, so a client whom more + spousal earnings take off SSI also loses SSA's matching ISM. 5. Role invariance: the client's countable income and OAP do not depend on which spouse is the tax-unit head. @@ -172,7 +176,8 @@ def assert_properties(cases): assert oap[a] >= oap[b] - TOLERANCE, c # 4. More spousal earnings never lower countable income, and raise # OAP only through the client's SSI. - assert countable[c_] >= countable[b] - TOLERANCE, c + if (ssi[c_] > 0) == (ssi[b] > 0) or not c["shelter_support"]: + assert countable[c_] >= countable[b] - TOLERANCE, c if ssi[c_] == pytest.approx(ssi[b], abs=TOLERANCE): assert oap[c_] <= oap[b] + TOLERANCE, c # 5. Roles. diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_own_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_own_countable_income.yaml index 59b00258455..c977b09b150 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_own_countable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_own_countable_income.yaml @@ -194,3 +194,15 @@ state_code: CO output: co_oap_own_countable_income: 0 + +- name: Case 11, an SSI-only client with SSI under $20 still passes nothing to earnings. + # SSI of $10/month is the only unearned income, so 3.533(A)(2)(c)(2) gives + # no disregard to pass on, though gross unearned income is under $20: + # (500 - 65) / 2 = 217.50/month, or 2,610/year. + period: 2026 + input: + state_code: CO + ssi: 120 + employment_income: 6_000 + output: + co_oap_own_countable_income: 2_610 From ef440a5e40d48a8ca1edb668e8bb56fd9f32e22b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 20:42:06 -0400 Subject: [PATCH 04/11] Apply the Sol review: cents, cycle-safe Medicaid, exempt income, SSI in deeming, spouse's household - Round the monthly grant to cents so float residue at the standard pays no floor - Read the spouse's Medicaid in a branch where no spouse is exempt for OAP deeming, so the result keeps SNAP's pass-through and does not depend on order - Exempt interest, dividends and child support (3.520.786(K), (N), (P)) - Count SSI in a non-exempt spouse's unearned income and in a child's income - Take the child allocation from the spouse's household; drop the CO mask on spouse-side variables - Scope invariants 2 and 4 to what the rule guarantees Co-Authored-By: Claude Opus 5.5 --- .../deeming/child_allocation_share.yaml | 2 +- .../ssa/oap/income/disregards/unearned.yaml | 2 +- .../co/ssa/oap/income/sources/unearned.yaml | 30 ++++ .../tests/core/test_co_oap_invariants.py | 79 +++++----- ...si_state_supplement_medicaid_dependency.py | 29 ++-- .../gov/states/co/ssa/oap/co_oap.yaml | 47 +++++- .../co/ssa/oap/co_oap_deemed_income.yaml | 146 +++++++++++++++++- .../co/ssa/oap/co_oap_deeming_exempt.yaml | 16 +- .../ssa/oap/co_oap_own_countable_income.yaml | 48 +++++- .../variables/gov/states/co/ssa/oap/co_oap.py | 5 +- .../states/co/ssa/oap/co_oap_deemed_income.py | 43 ++++-- .../co/ssa/oap/co_oap_deeming_exempt.py | 12 +- .../co/ssa/oap/co_oap_medicaid_enrolled.py | 41 +++++ .../co/ssa/oap/co_oap_own_countable_income.py | 26 ++-- 14 files changed, 423 insertions(+), 103 deletions(-) create mode 100644 policyengine_us/parameters/gov/states/co/ssa/oap/income/sources/unearned.yaml create mode 100644 policyengine_us/variables/gov/states/co/ssa/oap/co_oap_medicaid_enrolled.py diff --git a/policyengine_us/parameters/gov/states/co/ssa/oap/income/deeming/child_allocation_share.yaml b/policyengine_us/parameters/gov/states/co/ssa/oap/income/deeming/child_allocation_share.yaml index 969d1bd29d5..5140f1acbf8 100644 --- a/policyengine_us/parameters/gov/states/co/ssa/oap/income/deeming/child_allocation_share.yaml +++ b/policyengine_us/parameters/gov/states/co/ssa/oap/income/deeming/child_allocation_share.yaml @@ -8,7 +8,7 @@ metadata: unit: /1 label: Colorado OAP deeming allocation per dependent child as a share of the SSI benefit standard reference: - - title: "9 CCR 2503-5, Section 3.534(B)(3) #page 78" + - title: 9 CCR 2503-5, Section 3.534(B)(3) href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=78 - title: 9 CCR 2503-5, Section 3.534(B)(3) (effective March 2, 2014) href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=5597#page=48 diff --git a/policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/unearned.yaml b/policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/unearned.yaml index ab166d2abaf..9402fbf5286 100644 --- a/policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/unearned.yaml +++ b/policyengine_us/parameters/gov/states/co/ssa/oap/income/disregards/unearned.yaml @@ -9,7 +9,7 @@ metadata: period: month label: Colorado OAP unearned income disregard reference: - - title: "9 CCR 2503-5, Section 3.533(A)(2)(c), (f) #page 77" + - title: 9 CCR 2503-5, Section 3.533(A)(2)(c), (f) href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=77 - title: 9 CCR 2503-5, Section 3.533(A)(2)(c) (effective March 2, 2014) href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=5597#page=47 diff --git a/policyengine_us/parameters/gov/states/co/ssa/oap/income/sources/unearned.yaml b/policyengine_us/parameters/gov/states/co/ssa/oap/income/sources/unearned.yaml new file mode 100644 index 00000000000..c5a39e917fd --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/ssa/oap/income/sources/unearned.yaml @@ -0,0 +1,30 @@ +description: Colorado counts these income sources as gross unearned income for the Old Age Pension, before disregards. + +values: + # 3.520.785(B) lists countable unearned income. 3.520.786 exempts the + # unearned income SSA excludes (K), which includes interest and dividends + # on countable resources (20 CFR 416.1124(c)(22)), and child support + # received for another person (N) or arrears received for the client (P). + 2014-03-02: + - pension_income + - social_security + - disability_benefits + - veterans_benefits + - survivor_benefits + - workers_compensation + - unemployment_compensation + - retirement_distributions + - gi_cash_assistance + - alimony_income + - financial_assistance + - rental_income + +metadata: + unit: list + period: year + label: Colorado OAP unearned income sources + reference: + - title: "9 CCR 2503-5, Sections 3.520.785, 3.520.786 #page 68-72" + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=68 + - title: 20 CFR 416.1124(c)(22), interest and dividends on countable resources + href: https://www.ecfr.gov/current/title-20/chapter-III/part-416/subpart-K/subject-group-ECFR57d93e39d624237/section-416.1124#p-416.1124(c)(22) diff --git a/policyengine_us/tests/core/test_co_oap_invariants.py b/policyengine_us/tests/core/test_co_oap_invariants.py index 38fc7da2f23..7d8ca126081 100644 --- a/policyengine_us/tests/core/test_co_oap_invariants.py +++ b/policyengine_us/tests/core/test_co_oap_invariants.py @@ -20,20 +20,22 @@ zero or between the floor and the standard each month. 2. Singles: for a client not on SSI, with no earnings or at least $20 of unearned income, 3.533(A) counts the same income as SSI's exclusions - (20 CFR 416.1112, 416.1124) applied to the same gross income. For a - client on SSI, 3.533(A) never counts less than SSI does. + (20 CFR 416.1112, 416.1124) applied to the same gross income, interest + excluded (3.520.786(K)). For a client on SSI with no interest, 3.533(A) + never counts less than SSI does. (PolicyEngine's SSI counts interest, + which 20 CFR 416.1124(c)(22) excludes on countable resources.) 3. Exemption: a spouse's Medicaid enrollment can only lower the client's countable income and raise OAP. -4. Monotonicity: more spousal earnings never lower the client's countable - income, and never raise OAP unless they lower the client's own SSI. That - exception is intended: OAP counts the client's SSI, and a spouse's - earnings lower it under SSI's couple computation or spousal deeming - (e.g. an SSI couple aged 65 with no income: $85/month of spousal - earnings cut each spouse's SSI by $0.02, and OAP rises by as much). - Countable income is checked only while the client's SSI receipt is - unchanged or the client has no shelter support: Colorado's own ISM for - a client not on SSI (3.520.79(C)) is not modeled, so a client whom more - spousal earnings take off SSI also loses SSA's matching ISM. +4. Monotonicity: while both spouses' SSI is unchanged, more spousal + earnings never lower the client's countable income or raise OAP. When + they change SSI, either can move the other way, as intended: OAP counts + the client's SSI (an SSI couple aged 65 with no income: $85/month of + spousal earnings cut each spouse's SSI by $0.02, and OAP rises by as + much); SSI falling under $20 releases the 3.533(A)(2)(f) spillover; a + client taken off SSI loses SSA's matching ISM, because Colorado's own ISM + for clients not on SSI (3.520.79(C)) is not modeled; and a spouse on SSI + above the OAP limit has SSI deemed (3.534(B)(1)), which their earnings + below the $65 disregard can lower. 5. Role invariance: the client's countable income and OAP do not depend on which spouse is the tax-unit head. @@ -81,7 +83,7 @@ def build_situation(cases): "ssi_receives_outside_shelter_support": {YEAR: c["shelter_support"]}, # Set for everyone: an input for some people gives the others # its default (false), not its formula. - "medicaid_enrolled_for_ssi_state_supplement": {YEAR: True}, + "co_oap_medicaid_enrolled": {YEAR: True}, } members = [client] if c["married"]: @@ -90,9 +92,7 @@ def build_situation(cases): "age": {YEAR: c["spouse_age"]}, "employment_income": {YEAR: c["spouse_earnings"] + extra}, "pension_income": {YEAR: c["spouse_pension"]}, - "medicaid_enrolled_for_ssi_state_supplement": { - YEAR: arrangement == "A" - }, + "co_oap_medicaid_enrolled": {YEAR: arrangement == "A"}, } members = [spouse, client] if arrangement == "D" else [client, spouse] head, partner = members @@ -127,8 +127,8 @@ def calculate(cases): return results, simulation.tax_benefit_system.parameters, p -def assert_properties(cases): - results, parameters, p = calculate(cases) +def assert_properties(cases, calculated=None): + results, parameters, p = calculated or calculate(cases) standard, floor = p.grant_standard, p.payment_floor oap = results["co_oap"] countable = results["co_oap_countable_income"] @@ -141,30 +141,32 @@ def assert_properties(cases): # within the year. total = (ssi[client] + countable[client]) / 12 paid = oap[client] / 12 - # 1. Payment rule and bounds. + # 1. Payment rule and bounds, in cents. Within a cent of the + # standard, float32 sums can round either way; YAML pins those. + grant = round(standard - total, 2) if not eligible[client]: assert paid == 0, c - elif total < standard - TOLERANCE: - assert paid == pytest.approx(max(standard - total, floor), abs=0.05) + elif grant >= 0.02: + assert paid == pytest.approx(max(grant, floor), abs=0.05), c assert floor - TOLERANCE <= paid <= standard + TOLERANCE, c - elif total > standard + TOLERANCE: + elif grant <= -0.01: assert paid == 0, c client = f"client_A{i}" if not c["married"]: # 2. Singles, against SSI's exclusions on the same gross income. own = results["co_oap_own_countable_income"][client] if ssi[client] > 0: - assert own >= results["ssi_countable_income"][client] - TOLERANCE, c + if c["client_interest"] == 0: + assert own >= results["ssi_countable_income"][client] - ( + TOLERANCE + ), c elif not c["shelter_support"]: - other_unearned = ( - c["client_pension"] - + c["client_interest"] - + c["client_social_security"] - ) - if c["client_earnings"] == 0 or other_unearned >= 20 * 12: + # Interest is exempt (3.520.786(K)); the other sources count. + unearned = results["ssi_unearned_income"][client] - c["client_interest"] + if c["client_earnings"] == 0 or unearned >= 20 * 12: expected = _apply_ssi_exclusions( np.array([results["ssi_earned_income"][client]]), - np.array([results["ssi_unearned_income"][client]]), + np.array([unearned]), parameters, YEAR, )[0] @@ -174,11 +176,13 @@ def assert_properties(cases): # 3. The Medicaid exemption only lowers countable income. assert countable[a] <= countable[b] + TOLERANCE, c assert oap[a] >= oap[b] - TOLERANCE, c - # 4. More spousal earnings never lower countable income, and raise - # OAP only through the client's SSI. - if (ssi[c_] > 0) == (ssi[b] > 0) or not c["shelter_support"]: + # 4. With the client's SSI unchanged, more spousal earnings never + # lower countable income or raise OAP. + spouse_b, spouse_c = f"spouse_B{i}", f"spouse_C{i}" + if ssi[c_] == pytest.approx(ssi[b], abs=TOLERANCE) and ssi[ + spouse_c + ] == pytest.approx(ssi[spouse_b], abs=TOLERANCE): assert countable[c_] >= countable[b] - TOLERANCE, c - if ssi[c_] == pytest.approx(ssi[b], abs=TOLERANCE): assert oap[c_] <= oap[b] + TOLERANCE, c # 5. Roles. assert countable[d] == pytest.approx(countable[b], abs=TOLERANCE), c @@ -186,7 +190,7 @@ def assert_properties(cases): def test_worked_example(): - """co_oap YAML Case 22, with and without the spouse on Medicaid. The + """co_oap YAML Case 22, adding the spouse on Medicaid. The client, 62, has no income; the spouse, 50, earns $2,000/month. Without Medicaid, 3.534(A) deems (2,000 - 65) / 2 = 967.50 and the grant of 1,032 - 967.50 = 64.50 is raised to the $189 floor. With the spouse on @@ -206,14 +210,15 @@ def test_worked_example(): "spouse_pension": 0, "extra_spouse_earnings": 1_200, } - results, _, _ = calculate([case]) + calculated = calculate([case]) + results = calculated[0] oap = results["co_oap"] countable = results["co_oap_countable_income"] assert oap["client_A0"] == pytest.approx(12_384, abs=TOLERANCE) assert countable["client_A0"] == pytest.approx(0, abs=TOLERANCE) assert oap["client_B0"] == pytest.approx(2_268, abs=TOLERANCE) assert countable["client_B0"] == pytest.approx(11_610, abs=TOLERANCE) - assert_properties([case]) + assert_properties([case], calculated) try: diff --git a/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py b/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py index 290630aae3e..9bb2c3e84e2 100644 --- a/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py +++ b/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py @@ -57,7 +57,7 @@ def test_ssp_snap_medicaid_calculation_order(year, state, benefit): assert results[0]["medicaid_enrolled"].tolist() == [True, state == "IN"] -def co_oap_couple(year): +def co_oap_couple(year, snap=None): """An Old Age Pension client whose spouse is a SNAP workfare participant.""" people = { "client": {"age": {year: 62}}, @@ -68,32 +68,37 @@ def co_oap_couple(year): }, } unit = {"unit": {"members": list(people)}} + spm_units = {"unit": {"members": list(people)}} + if snap is not None: + spm_units["unit"]["snap"] = {year: snap} return { "people": people, - **{ - entity: unit - for entity in ("tax_units", "marital_units", "spm_units", "families") - }, + **{entity: unit for entity in ("tax_units", "marital_units", "families")}, + "spm_units": spm_units, "households": {"unit": {"members": list(people), "state_code": {year: "CO"}}}, } @pytest.mark.parametrize("year", [2027, 2030, 2035]) -def test_co_oap_snap_medicaid_calculation_order(year): +@pytest.mark.parametrize("snap", [None, 12]) +def test_co_oap_snap_medicaid_calculation_order(year, snap): """Colorado OAP reads the spouse's Medicaid enrollment to decide whether the spouse's income is deemed (9 CCR 2503-5 3.534(C)), and SNAP counts OAP as - income. YAML cannot express repeated calculations in different orders.""" + income. The spouse is enrolled through SNAP's pass-through from the + community engagement requirement, with SNAP computed or supplied, and + earns $5,000/year, well within the OAP limit, so nothing is deemed and the + client gets the full 1,032/month in either order. YAML cannot express + repeated calculations in different orders.""" variables = ("co_oap", "snap", "medicaid_enrolled") results = [] for order in (variables, variables[::-1]): - simulation = Simulation(situation=co_oap_couple(year)) + simulation = Simulation(situation=co_oap_couple(year, snap)) results.append({name: simulation.calculate(name, year) for name in order}) - assert not any( - "ssi_state_supplement_medicaid" in name for name in simulation.branches - ) + assert not any("_medicaid_" in name for name in simulation.branches) for name in variables: np.testing.assert_array_equal(results[0][name], results[1][name]) - assert results[0]["co_oap"][0] > 0 + assert results[0]["medicaid_enrolled"].tolist() == [True, True] + assert results[0]["co_oap"][0] == pytest.approx(12_384) assert results[0]["snap"][0] > 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml index 6e5e2247269..9f60ef46ec7 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml @@ -1,22 +1,23 @@ - name: Case 1, countable income other than SSI reduces the grant. - # 9 CCR 2503-5 3.533(B): 952 x 12 - 1,000 = 10,424. + # 9 CCR 2503-5 3.533(B): 952 - 100 = 852/month, or 10,224/year. period: 2023 input: co_oap_eligible: true - co_oap_countable_income: 1_000 + co_oap_countable_income: 1_200 ssi: 0 output: - co_oap: 10_424 + co_oap: 10_224 - name: Case 2, SSI reduces the grant. - # 3.533(A)(2)(d) adds SSI back to countable income: 11,424 - 2,000 = 9,424. + # 3.533(A)(2)(d) adds SSI back to countable income: 952 - 200 = 752/month, + # or 9,024/year. period: 2023 input: co_oap_eligible: true co_oap_countable_income: 0 - ssi: 2_000 + ssi: 2_400 output: - co_oap: 9_424 + co_oap: 9_024 - name: Case 3, total countable income above the grant standard. period: 2023 @@ -343,3 +344,37 @@ co_oap_deeming_exempt: [true, false] co_oap_deemed_income: [5_616, 0] co_oap: [6_768, 0] + +- name: Case 24, income equal to the standard in cents gets no floor. + # $65.40/month of earnings and $1,051.80/month of pension: (65.40 - 65) / 2 + # = 0.20 plus 1,051.80 - 20 = 1,031.80 is exactly 1,032. 3.533: denied. + period: 2026 + input: + people: + person: + age: 62 + employment_income: 784.80 + pension_income: 12_621.60 + households: + household: + members: [person] + state_code: CO + output: + co_oap: 0 + +- name: Case 25, income one cent under the standard gets the floor. + # As Case 24 with $1,051.79/month of pension: total 1,031.99, a grant of + # 0.01 raised to 189. + period: 2026 + input: + people: + person: + age: 62 + employment_income: 784.80 + pension_income: 12_621.48 + households: + household: + members: [person] + state_code: CO + output: + co_oap: 2_268 diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deemed_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deemed_income.yaml index 05419c768d2..ffda677c525 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deemed_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deemed_income.yaml @@ -41,7 +41,7 @@ spouse: age: 50 employment_income: 18_000 - medicaid_enrolled_for_ssi_state_supplement: true + medicaid_enrolled: true is_tax_unit_spouse: true tax_units: tax_unit: @@ -147,7 +147,8 @@ co_oap_deemed_income: [5_652, 0, 0] - name: Case 6, the child allocation is reduced by the child's own income. - # As Case 5, with the child receiving $100/month of interest. The + # As Case 5, with the child receiving $100/month of Social Security + # survivor benefits. The # allocation is 497 - 100 = 397: 2,000 - 1,032 - 397 = 571/month, or # 6,852/year. period: 2026 @@ -163,7 +164,7 @@ is_tax_unit_spouse: true child: age: 10 - interest_income: 1_200 + social_security_survivors: 1_200 is_tax_unit_dependent: true tax_units: tax_unit: @@ -296,3 +297,142 @@ co_oap_deemed_income: [7_626, 0] # The spouse: (1,200 - 10) + (1,000 - 65) / 2 = 1,657.50/month of their own. co_oap_countable_income: [7_626, 19_890] + +- name: Case 11, a child's SSI counts as the child's own income. + # As Case 5, with the child receiving $600/month of SSI. 3.534(B)(3): the + # allocation is max(0, 497 - 600) = 0, so 2,000 - 1,032 = 968/month, or + # 11,616/year, is deemed. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + pension_income: 24_000 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + child: + age: 10 + ssi: 7_200 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, spouse, child] + marital_units: + marital_unit: + members: [head, spouse] + child_marital_unit: + members: [child] + households: + household: + members: [head, spouse, child] + state_code: CO + output: + co_oap_deemed_income: [11_616, 0, 0] + +- name: Case 12, a spouse on SSI above the OAP limit has SSI deemed with the rest. + # Both are 65; the spouse has a $1,000/month pension. SSI computes the + # couple: 1,491 - (1,000 - 20) = 511, or 255.50 each. The spouse's own OAP + # income, 1,000 - 10 + 255.50 = 1,245.50, exceeds the limit, so 3.534(C) + # does not apply. 3.534(B): 1,000 + 255.50 - 1,032 = 223.50/month is + # deemed, or 2,682/year. The client: 1,032 - 255.50 - 223.50 = 553/month, + # or 6,636/year. + period: 2026 + input: + people: + head: + age: 65 + is_tax_unit_head: true + spouse: + age: 65 + pension_income: 12_000 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi: [3_066, 3_066] + co_oap_deeming_exempt: [true, false] + co_oap_deemed_income: [2_682, 0] + co_oap: [6_636, 0] + +- name: Case 13, the child allocation comes from the spouse's household. + # The client, 62, lives alone. The spouse, 50, lives in another Colorado + # household with their child, 10, and has a $2,000/month pension. + # 2,000 - 1,032 - 497 = 471/month is deemed, or 5,652/year. The client: + # 1,032 - 471 = 561/month, or 6,732/year. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + pension_income: 24_000 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + child: + age: 10 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, spouse, child] + marital_units: + marital_unit: + members: [head, spouse] + child_marital_unit: + members: [child] + households: + head_household: + members: [head] + state_code: CO + spouse_household: + members: [spouse, child] + state_code: CO + output: + co_oap_deemed_income: [5_652, 0, 0] + co_oap: [6_732, 0, 0] + +- name: Case 14, a spouse in another state on Medicaid has nothing deemed. + # The client, 62, lives in Colorado. The spouse, 50, lives in New York, + # earns $1,000/month and is enrolled in Medicaid. 3.534(C) sets no + # residence condition: the spouse's own countable income, + # (1,000 - 65 - 10) / 2 = 462.50, is within the limit, so nothing is deemed. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + employment_income: 12_000 + medicaid_enrolled: true + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + head_household: + members: [head] + state_code: CO + spouse_household: + members: [spouse] + state_code: NY + output: + co_oap_deeming_exempt: [true, true] + co_oap_deemed_income: [0, 0] + co_oap: [12_384, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deeming_exempt.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deeming_exempt.yaml index b0d62c7fbae..5cd880a0ad1 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deeming_exempt.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deeming_exempt.yaml @@ -6,7 +6,7 @@ ssi: 4_800 co_oap_own_countable_income: 7_200 co_oap_eligible: false - medicaid_enrolled_for_ssi_state_supplement: false + co_oap_medicaid_enrolled: false output: co_oap_deeming_exempt: true @@ -18,7 +18,7 @@ ssi: 1_200 co_oap_own_countable_income: 12_000 co_oap_eligible: false - medicaid_enrolled_for_ssi_state_supplement: false + co_oap_medicaid_enrolled: false output: co_oap_deeming_exempt: false @@ -30,7 +30,7 @@ ssi: 0 co_oap_own_countable_income: 12_384 co_oap_eligible: false - medicaid_enrolled_for_ssi_state_supplement: true + co_oap_medicaid_enrolled: true output: co_oap_deeming_exempt: true @@ -41,7 +41,7 @@ ssi: 0 co_oap_own_countable_income: 12_396 co_oap_eligible: false - medicaid_enrolled_for_ssi_state_supplement: true + co_oap_medicaid_enrolled: true output: co_oap_deeming_exempt: false @@ -54,7 +54,7 @@ ssi: 0 co_oap_own_countable_income: 12_372 co_oap_eligible: true - medicaid_enrolled_for_ssi_state_supplement: false + co_oap_medicaid_enrolled: false output: co_oap_deeming_exempt: true @@ -67,7 +67,7 @@ ssi: 0 co_oap_own_countable_income: 12_384 co_oap_eligible: true - medicaid_enrolled_for_ssi_state_supplement: false + co_oap_medicaid_enrolled: false output: co_oap_deeming_exempt: false @@ -78,7 +78,7 @@ ssi: 0 co_oap_own_countable_income: 0 co_oap_eligible: false - medicaid_enrolled_for_ssi_state_supplement: false + co_oap_medicaid_enrolled: false output: co_oap_deeming_exempt: false @@ -90,6 +90,6 @@ receives_ssi: true co_oap_own_countable_income: 0 co_oap_eligible: false - medicaid_enrolled_for_ssi_state_supplement: false + co_oap_medicaid_enrolled: false output: co_oap_deeming_exempt: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_own_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_own_countable_income.yaml index c977b09b150..64e80ac9da0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_own_countable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_own_countable_income.yaml @@ -32,15 +32,15 @@ co_oap_own_countable_income: 5_760 - name: Case 3, unearned income under $20 passes the rest of the disregard to earnings. - # $10/month of interest and $500/month of earnings. The disregard covers the - # $10 of interest (3.533(A)(2)(c)) and the other $10 reduces earnings + # $10/month of pension and $500/month of earnings. The disregard covers the + # $10 of pension (3.533(A)(2)(c)) and the other $10 reduces earnings # (3.533(A)(2)(f)): (500 - 65 - 10) / 2 = 212.50/month, or 2,550/year. period: 2026 input: people: person: age: 62 - interest_income: 120 + pension_income: 120 employment_income: 6_000 households: household: @@ -206,3 +206,45 @@ employment_income: 6_000 output: co_oap_own_countable_income: 2_610 + +- name: Case 12, interest and dividends on countable resources are exempt. + # 3.520.786(K) exempts the unearned income SSA excludes, including interest + # and dividends on countable resources (20 CFR 416.1124(c)(22)). + period: 2026 + input: + people: + person: + age: 62 + interest_income: 360 + dividend_income: 240 + households: + household: + members: [person] + state_code: CO + output: + co_oap_own_countable_income: 0 + co_oap: 12_384 + +- name: Case 13, child support received is exempt. + # 3.520.786(N) exempts child support received by the client for another + # person, and (P) arrears received for the client. A 62-year-old caring for + # their child receives $500/month. + period: 2026 + input: + people: + parent: + age: 62 + child_support_received: 6_000 + child: + age: 10 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: CO + output: + co_oap_own_countable_income: [0, 0] + co_oap: [12_384, 0] diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py index 18f4df65c02..994831418c3 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py @@ -18,7 +18,10 @@ def formula(person, period, parameters): # 9 CCR 2503-5 3.532(B), 3.533(B): the grant is the grant standard # less total countable income, SSI included (3.533(A)(2)(d)). income = person("ssi", month) + person("co_oap_countable_income", month) - grant = max_(0, p.grant_standard - income) + # Cents: monthly amounts derived from annual ones carry + # floating-point residue, which must not turn income equal to the + # standard into a positive grant. + grant = max_(0, np.round(p.grant_standard - income, 2)) # 3.533 and 3.520.781(A): a client whose total countable income # equals or exceeds the grant standard is not eligible. # 3.531(D)(2), 3.532(B), 3.533(B): an eligible client's grant below diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py index a466ecd9de6..6521c621690 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py @@ -16,35 +16,47 @@ def formula(person, period, parameters): # 3.534(C): no income is deemed from a spouse who receives Adult # Financial, SSI or Medicaid with income within the OAP limit. deemable = ~person("co_oap_deeming_exempt", period) + ssi = person("ssi", period) earned = person("ssi_earned_income", period) * deemable - unearned = person("ssi_unearned_income", period) * deemable - # 3.534(B)(5): court-ordered support obligations of the spouse. Whether - # the income is garnished instead is not modeled. + # 3.534(B)(1): the total unearned income of the spouse, SSI included + # (3.520.785(B)(1)), less exempt income (3.520.786). + unearned = (add(person, period, p.income.sources.unearned) + ssi) * deemable + # 3.534(B)(5): court-ordered support obligations of the spouse. We + # treat alimony paid as court-ordered. Whether the income is garnished + # instead is not modeled. support = ( add(person, period, ["child_support_expense", "alimony_expense"]) * deemable ) - marital_unit = person.marital_unit - spouse_earned = marital_unit.sum(earned) - earned - spouse_unearned = marital_unit.sum(unearned) - unearned - spouse_support = marital_unit.sum(support) - support - # 3.534(B)(3): for each dependent child in the household, half the SSI - # benefit standard for an individual less the child's own income. + # 3.534(B)(3): for each dependent child in the spouse's household, + # half the SSI benefit standard for an individual less the child's own + # income. year = period.this_year dependent_child = person("is_tax_unit_dependent", year) & person( "is_child", year ) - child_income = add(person, period, ["ssi_earned_income", "ssi_unearned_income"]) + child_income = ( + person("ssi_earned_income", period) + + add(person, period, p.income.sources.unearned) + + ssi + ) fbr = parameters(period).gov.ssa.ssi.amount.individual child_allocation = dependent_child * max_( 0, fbr * p.income.deeming.child_allocation_share - child_income ) - child_allocations = person.household.sum(child_allocation) + # Each person's household total; the spouse's reaches the client + # through the marital unit. + child_allocations = person.household.sum(child_allocation) * deemable + + marital_unit = person.marital_unit + + def spouse(values): + return marital_unit.sum(values) - values # 3.534(A): deduct $65 from the spouse's gross earned income and halve # the rest. Unlike SSI (20 CFR 416.1163) there is no deeming threshold. deemed_earned = ( - max_(0, spouse_earned - p.income.disregards.earned) + max_(0, spouse(earned) - p.income.disregards.earned) * p.income.disregards.earned_share ) # 3.534(B): deduct the OAP grant standard, child allocations and @@ -52,8 +64,11 @@ def formula(person, period, parameters): # payments for uninsured dependents (3.534(B)(4)) are not modeled. deemed_unearned = max_( 0, - spouse_unearned - p.grant_standard - child_allocations - spouse_support, + spouse(unearned) + - p.grant_standard + - spouse(child_allocations) + - spouse(support), ) # Sponsor deeming (3.534(D)) and the institutionalized spouse's MMMNA # (3.534(C)) are not modeled. - return (deemed_earned + deemed_unearned) * (marital_unit.nb_persons() == 2) + return deemed_earned + deemed_unearned diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deeming_exempt.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deeming_exempt.py index dc3c64b1be9..ad111097030 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deeming_exempt.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deeming_exempt.py @@ -6,7 +6,8 @@ class co_oap_deeming_exempt(Variable): entity = Person label = "Colorado Old Age Pension spouse income not deemed" definition_period = MONTH - defined_for = StateCode.CO + # Not restricted to Colorado: 3.534(C) sets no residence condition on the + # spouse. # PDF page 78 reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=78" @@ -25,10 +26,11 @@ def formula(person, period, parameters): # Old Age Pension is the Adult Financial program modeled for someone # who is not on SSI; AND-CS requires SSI. Receipt is judged on the # spouse's own income: when both spouses are OAP clients within the - # limit, neither one's income is deemed to the other. + # limit, neither one's income is deemed to the other. (A reading in + # which neither receives OAP, and each one's income is deemed to the + # other, can also be self-consistent; the model takes the one in which + # both are paid.) year = period.this_year receives_oap = person("co_oap_eligible", year) & (income < p.grant_standard) - # SNAP counts OAP as income, so Medicaid enrollment is read without - # SNAP's community-engagement pass-through to avoid a cycle. - medicaid = person("medicaid_enrolled_for_ssi_state_supplement", year) + medicaid = person("co_oap_medicaid_enrolled", year) return (receives_ssi | receives_oap | medicaid) & within_limit diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_medicaid_enrolled.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_medicaid_enrolled.py new file mode 100644 index 00000000000..94a32c896c8 --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_medicaid_enrolled.py @@ -0,0 +1,41 @@ +from policyengine_us.model_api import * + + +class co_oap_medicaid_enrolled(Variable): + value_type = bool + entity = Person + label = "Medicaid enrollment for the Colorado Old Age Pension deeming exemption" + definition_period = YEAR + reference = ( + # 9 CCR 2503-5 3.534(C) + "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=78", + "https://www.ecfr.gov/current/title-42/section-435.554#p-435.554(c)(7)", + ) + + def formula(person, period, parameters): + p = parameters(period).gov.hhs.medicaid.eligibility.work_requirements + if not p.applies: + return person("medicaid_enrolled", period) + + # SNAP counts OAP as income, and Medicaid's SNAP pass-through from the + # community engagement requirement (42 CFR 435.554(c)(7)) reads SNAP + # receipt, so OAP, a spouse's Medicaid enrollment and SNAP depend on + # each other. Medicaid is computed in a private branch where no + # spouse's Medicaid exempts their income from OAP deeming. That OAP is + # never higher than the final OAP, so SNAP there is never lower, and + # the spouse keeps every Medicaid route, supplied inputs included. + # The result does not depend on calculation order. + simulation = person.simulation + branch_name = f"{simulation.branch_name}_co_oap_medicaid_{period}" + try: + branch = get_override_branch( + simulation, + branch_name, + period, + {"co_oap_medicaid_enrolled": np.zeros(person.count, dtype=bool)}, + ) + return branch.calculate("medicaid_enrolled", period) + finally: + # A branch clones cached arrays; retaining it would duplicate a + # substantial part of the full-population simulation in memory. + simulation.branches.pop(branch_name, None) diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_own_countable_income.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_own_countable_income.py index 07902c518be..aed915fa46d 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_own_countable_income.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_own_countable_income.py @@ -7,34 +7,35 @@ class co_oap_own_countable_income(Variable): label = "Colorado Old Age Pension countable income of the client other than SSI" unit = USD definition_period = MONTH - defined_for = StateCode.CO - # PDF pages 76-77 - reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=76" + # Not restricted to Colorado: a client's spouse may live elsewhere + # (3.534(C)). co_oap itself is defined only for Colorado residents. + # PDF pages 68-73, 76-77 + reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=68" def formula(person, period, parameters): # 9 CCR 2503-5 3.533(A) counts the client's own income whether or not # they are eligible for SSI. The SSI that 3.533(A)(2)(d) adds back is # added in co_oap. - p = parameters(period).gov.states.co.ssa.oap.income.disregards - # Gross income uses SSI's earned and unearned income sources. - # 3.520.786(K) exempts the unearned income SSI excludes, but limits - # exempt earned income to the items it lists, so SSI's student earned - # income exclusion is not deducted. + p = parameters(period).gov.states.co.ssa.oap.income + # 3.520.786 limits exempt earned income to the items it lists, so + # SSI's student earned income exclusion is not deducted. earned = person("ssi_earned_income", period) ssi = person("ssi", period) receives_ssi = ssi > 0 # 3.520.79(B)(6): an SSI recipient charged in-kind support and # maintenance by SSA is charged a matching ISM. A joint couple's ISM is # one presumed maximum value for the couple, which SSI splits equally - # between the spouses. + # between the spouses. ISM through SSA's one-third reduction, and + # Colorado's own ISM for a client not on SSI (3.520.79(C)), are not + # modeled. ism = person("ssi_in_kind_support_and_maintenance", period) couple_computation = person("ssi_couple_computation_applies", period) ism = where(couple_computation, ism / 2, ism) * receives_ssi - unearned = person("ssi_unearned_income", period) + ism + unearned = add(person, period, p.sources.unearned) + ism # 3.533(A)(2)(c)(1): a married client's $20 disregard is split equally # with the spouse, so that no more than $20 applies to the couple. married = person.marital_unit.nb_persons() == 2 - disregard = where(married, p.unearned / 2, p.unearned) + disregard = where(married, p.disregards.unearned / 2, p.disregards.unearned) # 3.533(A)(2)(c): the disregard applies to unearned income other than # SSI; min_ gives 0 to a client whose only unearned income is SSI # (3.533(A)(2)(c)(2)). @@ -46,6 +47,7 @@ def formula(person, period, parameters): earned_disregard = where(ssi_only, 0, max_(0, disregard - unearned - ssi)) # 3.533(A)(1): deduct $65 from gross earned income and halve the rest. countable_earned = ( - max_(0, earned - p.earned - earned_disregard) * p.earned_share + max_(0, earned - p.disregards.earned - earned_disregard) + * p.disregards.earned_share ) return unearned - unearned_disregard + countable_earned From 93cfe1c69998b7b489ac5589c50f351ef56acf26 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 23:11:08 -0400 Subject: [PATCH 05/11] Apply Sol round 2: precision, 3.581(E) cents, reported Medicaid, spouse ISM, rental losses - Judge a positive grant, and OAP receipt in the 3.534(C) test, to a tenth of a cent: float residue is no grant, a half-cent grant is - Drop cents from payments not reduced by SSI (3.581(E)) - Count reported Medicaid coverage; skip the Medicaid branch when no spouse of an OAP-eligible client needs it; co_oap_deeming_exempt is true only for such a spouse - Deem a non-exempt spouse's matching ISM; count a child's own child support in the 3.534(B)(3) allocation; floor each unearned source at zero - Document how the Medicaid branch resolves a household with no self-consistent outcome, and test it in both orders Co-Authored-By: Claude Opus 5.5 --- .../tests/core/test_co_oap_invariants.py | 70 +++-- ...si_state_supplement_medicaid_dependency.py | 46 ++++ .../gov/states/co/ssa/oap/co_oap.yaml | 36 +++ .../co/ssa/oap/co_oap_deemed_income.yaml | 148 ++++++++++- .../co/ssa/oap/co_oap_deeming_exempt.yaml | 244 ++++++++++++++---- .../ssa/oap/co_oap_own_countable_income.yaml | 19 ++ .../gov/states/co/ssa/oap/_precision.py | 4 + .../variables/gov/states/co/ssa/oap/co_oap.py | 17 +- .../states/co/ssa/oap/co_oap_deemed_income.py | 25 +- .../co/ssa/oap/co_oap_deeming_exempt.py | 34 ++- .../co_oap_in_kind_support_and_maintenance.py | 22 ++ .../co/ssa/oap/co_oap_medicaid_enrolled.py | 21 +- .../co/ssa/oap/co_oap_own_countable_income.py | 13 +- .../co/ssa/oap/co_oap_unearned_income.py | 18 ++ 14 files changed, 597 insertions(+), 120 deletions(-) create mode 100644 policyengine_us/variables/gov/states/co/ssa/oap/_precision.py create mode 100644 policyengine_us/variables/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.py create mode 100644 policyengine_us/variables/gov/states/co/ssa/oap/co_oap_unearned_income.py diff --git a/policyengine_us/tests/core/test_co_oap_invariants.py b/policyengine_us/tests/core/test_co_oap_invariants.py index 7d8ca126081..b503cfc27e9 100644 --- a/policyengine_us/tests/core/test_co_oap_invariants.py +++ b/policyengine_us/tests/core/test_co_oap_invariants.py @@ -9,21 +9,24 @@ payment floor standard to it. Each case is a client aged 60-80 in their own household, with pension, -interest, Social Security and earnings, optionally receiving outside shelter -support, and either single or married to a spouse who has earnings and a -pension. Each married case appears four times: (A) the spouse is enrolled in +interest, Social Security, earnings, rental income or loss and child support, +optionally receiving outside shelter support, and either single or married to +a spouse who has earnings and a pension and may receive shelter support. Each married case appears four times: (A) the spouse is enrolled in Medicaid, (B) the spouse is not, (C) as B with extra spousal earnings, and (D) as B with the tax-unit roles swapped. For 2026: 1. Payment: OAP is the grant standard less total countable income when that - is positive, raised to the floor if below it, and zero otherwise; so it is - zero or between the floor and the standard each month. + is positive, raised to the floor if below it, and zero otherwise, with + cents dropped unless SSI reduces the grant (3.581(E)); so it is zero or + between the floor and the standard each month. 2. Singles: for a client not on SSI, with no earnings or at least $20 of unearned income, 3.533(A) counts the same income as SSI's exclusions - (20 CFR 416.1112, 416.1124) applied to the same gross income, interest - excluded (3.520.786(K)). For a client on SSI with no interest, 3.533(A) - never counts less than SSI does. (PolicyEngine's SSI counts interest, - which 20 CFR 416.1124(c)(22) excludes on countable resources.) + (20 CFR 416.1112, 416.1124) applied to the same gross income, with + interest and child support exempt (3.520.786(K), (N)) and a rental loss + counted as zero (3.520.785(B)(20)). For a client on SSI with none of + those, 3.533(A) never counts less than SSI does. (PolicyEngine's SSI + counts interest, which 20 CFR 416.1124(c)(22) excludes on countable + resources: #10079.) 3. Exemption: a spouse's Medicaid enrollment can only lower the client's countable income and raise OAP. 4. Monotonicity: while both spouses' SSI is unchanged, more spousal @@ -80,6 +83,8 @@ def build_situation(cases): "interest_income": {YEAR: c["client_interest"]}, "social_security_retirement": {YEAR: c["client_social_security"]}, "employment_income": {YEAR: c["client_earnings"]}, + "rental_income": {YEAR: c["client_rental"]}, + "child_support_received": {YEAR: c["client_child_support"]}, "ssi_receives_outside_shelter_support": {YEAR: c["shelter_support"]}, # Set for everyone: an input for some people gives the others # its default (false), not its formula. @@ -92,6 +97,9 @@ def build_situation(cases): "age": {YEAR: c["spouse_age"]}, "employment_income": {YEAR: c["spouse_earnings"] + extra}, "pension_income": {YEAR: c["spouse_pension"]}, + "ssi_receives_outside_shelter_support": { + YEAR: c["spouse_shelter_support"] + }, "co_oap_medicaid_enrolled": {YEAR: arrangement == "A"}, } members = [spouse, client] if arrangement == "D" else [client, spouse] @@ -141,32 +149,48 @@ def assert_properties(cases, calculated=None): # within the year. total = (ssi[client] + countable[client]) / 12 paid = oap[client] / 12 - # 1. Payment rule and bounds, in cents. Within a cent of the - # standard, float32 sums can round either way; YAML pins those. - grant = round(standard - total, 2) + # 1. Payment rule and bounds. Payments not reduced by SSI drop + # cents (3.581(E)). Within a fifth of a cent of the standard, + # float32 sums can fall either way; YAML pins those. + grant = standard - total if not eligible[client]: assert paid == 0, c - elif grant >= 0.02: - assert paid == pytest.approx(max(grant, floor), abs=0.05), c + elif grant > 0.002: + expected = max(grant, floor) + if ssi[client] > 0: + assert paid == pytest.approx(expected, abs=0.05), c + else: + assert abs(paid - round(paid)) < 0.01, c + assert expected - 1 < paid <= expected + 0.01, c assert floor - TOLERANCE <= paid <= standard + TOLERANCE, c - elif grant <= -0.01: + elif grant < -0.002: assert paid == 0, c client = f"client_A{i}" if not c["married"]: - # 2. Singles, against SSI's exclusions on the same gross income. + # 2. Singles, against SSI's exclusions on the same gross income: + # interest and child support exempt (3.520.786(K), (N)), a rental + # loss counted as zero (3.520.785(B)(20)). own = results["co_oap_own_countable_income"][client] + plain = ( + c["client_interest"] == 0 + and c["client_child_support"] == 0 + and c["client_rental"] >= 0 + ) if ssi[client] > 0: - if c["client_interest"] == 0: + if plain: assert own >= results["ssi_countable_income"][client] - ( TOLERANCE ), c elif not c["shelter_support"]: - # Interest is exempt (3.520.786(K)); the other sources count. - unearned = results["ssi_unearned_income"][client] - c["client_interest"] + unearned = ( + c["client_pension"] + + c["client_social_security"] + + max(c["client_rental"], 0) + ) if c["client_earnings"] == 0 or unearned >= 20 * 12: expected = _apply_ssi_exclusions( np.array([results["ssi_earned_income"][client]]), - np.array([unearned]), + np.array([float(unearned)]), parameters, YEAR, )[0] @@ -203,11 +227,14 @@ def test_worked_example(): "client_interest": 0, "client_social_security": 0, "client_earnings": 0, + "client_rental": 0, + "client_child_support": 0, "shelter_support": False, "married": True, "spouse_age": 50, "spouse_earnings": 24_000, "spouse_pension": 0, + "spouse_shelter_support": False, "extra_spouse_earnings": 1_200, } calculated = calculate([case]) @@ -243,11 +270,14 @@ def case(draw): "client_interest": draw(amount(300)), "client_social_security": draw(amount(15_000)), "client_earnings": draw(amount(20_000)), + "client_rental": draw(st.integers(min_value=-6_000, max_value=6_000)), + "client_child_support": draw(amount(6_000)), "shelter_support": draw(st.booleans()), "married": draw(st.booleans()), "spouse_age": draw(st.integers(min_value=18, max_value=80)), "spouse_earnings": draw(amount(30_000)), "spouse_pension": draw(amount(24_000)), + "spouse_shelter_support": draw(st.booleans()), "extra_spouse_earnings": draw(st.integers(min_value=1, max_value=12_000)), } diff --git a/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py b/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py index 9bb2c3e84e2..2579b46e4eb 100644 --- a/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py +++ b/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py @@ -102,6 +102,52 @@ def test_co_oap_snap_medicaid_calculation_order(year, snap): assert results[0]["snap"][0] > 0 +def test_co_oap_medicaid_without_a_consistent_outcome(): + """A household with no self-consistent outcome resolves the same way in + either order. The client's spouse, 40, earns $5,000/year and is a SNAP + workfare participant; an unrelated adult in the same SNAP unit has a + $31,250/year pension. With the spouse's income deemed, OAP is lower and + the unit gets SNAP, which enrolls the spouse in Medicaid through the + pass-through; with the spouse exempt, OAP is higher and the unit gets no + SNAP. co_oap_medicaid_enrolled decides enrollment on the SNAP received + while the spouse's income is still deemed, so the spouse is exempt and + the client gets the full 1,032/month.""" + year = 2027 + people = { + "client": {"age": {year: 62}}, + "spouse": { + "age": {year: 40}, + "employment_income": {year: 5_000}, + "is_snap_workfare_participant": {year: True}, + }, + "adult": { + "age": {year: 40}, + "pension_income": {year: 31_250}, + "is_snap_workfare_participant": {year: True}, + }, + } + couple = {"members": ["client", "spouse"]} + single = {"members": ["adult"]} + everyone = {"members": list(people)} + situation = { + "people": people, + **{ + entity: {"couple": couple, "single": single} + for entity in ("tax_units", "marital_units", "families") + }, + "spm_units": {"unit": everyone}, + "households": {"unit": {**everyone, "state_code": {year: "CO"}}}, + } + variables = ("co_oap", "snap", "medicaid_enrolled") + results = [] + for order in (variables, variables[::-1]): + simulation = Simulation(situation=situation) + results.append({name: simulation.calculate(name, year) for name in order}) + for name in variables: + np.testing.assert_array_equal(results[0][name], results[1][name]) + assert results[0]["co_oap"][0] == pytest.approx(12_384) + + @pytest.mark.parametrize( "variable", ["medicaid_enrolled", "is_medicaid_eligible", "takes_up_medicaid_if_eligible"], diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml index 9f60ef46ec7..3f0596c7d47 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml @@ -378,3 +378,39 @@ state_code: CO output: co_oap: 2_268 + +- name: Case 26, a half-cent grant is a grant. + # $65.09/month of earnings and $1,051.95/month of pension: + # (65.09 - 65) / 2 + 1,051.95 - 20 = 1,031.995, below the standard, so the + # grant of 0.005 is raised to the floor. + period: 2026 + input: + people: + person: + age: 62 + employment_income: 781.08 + pension_income: 12_623.40 + households: + household: + members: [person] + state_code: CO + output: + co_oap: 2_268 + +- name: Case 27, payments drop cents when SSI does not reduce the grant. + # $516.50/month of pension: 516.50 - 20 = 496.50; 1,032 - 496.50 = 535.50, + # paid as 535 (3.581(E)), or 6,420/year. Case 21 keeps the cents of a grant + # reduced by SSI. + period: 2026 + input: + people: + person: + age: 62 + pension_income: 6_198 + households: + household: + members: [person] + state_code: CO + output: + co_oap_own_countable_income: 5_958 + co_oap: 6_420 diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deemed_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deemed_income.yaml index ffda677c525..4635e4a91d3 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deemed_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deemed_income.yaml @@ -1,8 +1,8 @@ - name: Case 1, a working spouse's earnings are deemed with no threshold. # The client is 62 with no income. The spouse, 50, earns $1,500/month and # does not take up Medicaid. 9 CCR 2503-5 3.534(A): - # (1,500 - 65) / 2 = 717.50/month, or 8,610/year. The client is an OAP - # client within the limit, so nothing is deemed to the spouse. + # (1,500 - 65) / 2 = 717.50/month, or 8,610/year. The spouse is not + # OAP-eligible, so nothing is deemed the other way. period: 2026 input: people: @@ -25,7 +25,7 @@ members: [head, spouse] state_code: CO output: - co_oap_deeming_exempt: [true, false] + co_oap_deeming_exempt: [false, false] co_oap_deemed_income: [8_610, 0] - name: Case 2, a spouse on Medicaid with income within the OAP limit has nothing deemed. @@ -55,7 +55,7 @@ state_code: CO output: co_oap_own_countable_income: [0, 8_550] - co_oap_deeming_exempt: [true, true] + co_oap_deeming_exempt: [false, true] co_oap_deemed_income: [0, 0] - name: Case 3, a spouse's unearned income above the grant standard is deemed. @@ -433,6 +433,144 @@ members: [spouse] state_code: NY output: - co_oap_deeming_exempt: [true, true] + co_oap_deeming_exempt: [false, true] co_oap_deemed_income: [0, 0] co_oap: [12_384, 0] + +- name: Case 15, a spouse whose income equals the standard receives no OAP and is deemed. + # Both are 62. The spouse earns $2,138.87/month and has $0.13/month of + # pension: (2,138.87 - 65 - 9.87) / 2 = 1,032, so the spouse is denied + # (3.533) and is not receiving Adult Financial payments. 3.534(A) deems + # (2,138.87 - 65) / 2 = 1,036.935/month, or 12,443.22/year, and the client + # is denied too. + period: 2026 + absolute_error_margin: 0.1 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 62 + employment_income: 25_666.44 + pension_income: 1.56 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_deeming_exempt: [true, false] + co_oap_deemed_income: [12_443.22, 0] + co_oap: [0, 0] + +- name: Case 16, reported current Medicaid coverage exempts the spouse. + # 2027, so the Medicaid branch runs. The spouse, 40, earns $5,000/year and + # reports current Medicaid coverage but would not take it up as modeled. + # 3.534(C) asks whether the spouse is receiving Medicaid, so nothing is + # deemed and the client gets the full 1,032/month. + period: 2027 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 40 + employment_income: 5_000 + receives_medicaid: true + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_oap_deeming_exempt: [false, true] + co_oap: [12_384, 0] + +- name: Case 17, a child's own child support reduces the child allocation. + # As Case 5, with the child receiving $900/month of child support. + # 3.520.786 exempts income of the client, spouse and sponsor, not the + # child, so the allocation is max(0, 497 - 900) = 0 and 2,000 - 1,032 = + # 968/month is deemed, or 11,616/year. The client: 1,032 - 968 = 64, + # raised to 189. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + pension_income: 24_000 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + child: + age: 10 + child_support_received: 10_800 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, spouse, child] + marital_units: + marital_unit: + members: [head, spouse] + child_marital_unit: + members: [child] + households: + household: + members: [head, spouse, child] + state_code: CO + output: + co_oap_deemed_income: [11_616, 0, 0] + co_oap: [2_268, 0, 0] + +- name: Case 18, a non-exempt spouse's matching ISM is deemed. + # Both are 65 and receive shelter from outside. SSA charges the couple a + # presumed maximum value of 1,491 / 3 + 20 = 517, or 258.50 each, and with + # the spouse's $950/month pension pays SSI of 1,491 - (950 + 517 - 20) = 44, + # or 22 each. The spouse's own OAP income, 950 + 258.50 - 10 + 22 = + # 1,220.50, exceeds the limit. 3.534(B): 950 + 258.50 + 22 - 1,032 = + # 198.50/month is deemed, or 2,382/year. The client: 1,032 - (258.50 - 10 + # + 22) - 198.50 = 563/month, or 6,756/year; SSI reduces this grant, so + # cents would be kept. + period: 2026 + input: + people: + head: + age: 65 + ssi_receives_outside_shelter_support: true + is_tax_unit_head: true + spouse: + age: 65 + pension_income: 11_400 + ssi_receives_outside_shelter_support: true + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi: [264, 264] + co_oap_deemed_income: [2_382, 0] + co_oap: [6_756, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deeming_exempt.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deeming_exempt.yaml index 5cd880a0ad1..51f56be613d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deeming_exempt.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deeming_exempt.yaml @@ -1,95 +1,229 @@ -- name: Case 1, an SSI recipient within the OAP limit. +- name: Case 1, a spouse on SSI within the OAP limit. # 9 CCR 2503-5 3.534(C): 400 + 600 = 1,000/month is within the $1,032 limit. + # Each case's client is OAP-eligible with no income; the first output is + # the client, who is not the spouse of an OAP-eligible client unless the + # spouse is OAP-eligible too. period: 2026 input: - state_code: CO - ssi: 4_800 - co_oap_own_countable_income: 7_200 - co_oap_eligible: false - co_oap_medicaid_enrolled: false + people: + client: + co_oap_eligible: true + co_oap_own_countable_income: 0 + ssi: 0 + co_oap_medicaid_enrolled: false + spouse: + ssi: 4_800 + co_oap_own_countable_income: 7_200 + co_oap_eligible: false + co_oap_medicaid_enrolled: false + marital_units: + marital_unit: + members: [client, spouse] + households: + household: + members: [client, spouse] + state_code: CO output: - co_oap_deeming_exempt: true + co_oap_deeming_exempt: [false, true] -- name: Case 2, an SSI recipient above the OAP limit. +- name: Case 2, a spouse on SSI above the OAP limit. # 100 + 1,000 = 1,100/month exceeds the limit. period: 2026 input: - state_code: CO - ssi: 1_200 - co_oap_own_countable_income: 12_000 - co_oap_eligible: false - co_oap_medicaid_enrolled: false + people: + client: + co_oap_eligible: true + co_oap_own_countable_income: 0 + ssi: 0 + co_oap_medicaid_enrolled: false + spouse: + ssi: 1_200 + co_oap_own_countable_income: 12_000 + co_oap_eligible: false + co_oap_medicaid_enrolled: false + marital_units: + marital_unit: + members: [client, spouse] + households: + household: + members: [client, spouse] + state_code: CO output: - co_oap_deeming_exempt: false + co_oap_deeming_exempt: [false, false] -- name: Case 3, a Medicaid enrollee with income equal to the OAP limit. +- name: Case 3, a spouse on Medicaid with income equal to the OAP limit. # "No greater than the OAP limit" includes income equal to it. period: 2026 input: - state_code: CO - ssi: 0 - co_oap_own_countable_income: 12_384 - co_oap_eligible: false - co_oap_medicaid_enrolled: true + people: + client: + co_oap_eligible: true + co_oap_own_countable_income: 0 + ssi: 0 + co_oap_medicaid_enrolled: false + spouse: + ssi: 0 + co_oap_own_countable_income: 12_384 + co_oap_eligible: false + co_oap_medicaid_enrolled: true + marital_units: + marital_unit: + members: [client, spouse] + households: + household: + members: [client, spouse] + state_code: CO output: - co_oap_deeming_exempt: true + co_oap_deeming_exempt: [false, true] -- name: Case 4, a Medicaid enrollee above the OAP limit. +- name: Case 4, a spouse on Medicaid above the OAP limit. period: 2026 input: - state_code: CO - ssi: 0 - co_oap_own_countable_income: 12_396 - co_oap_eligible: false - co_oap_medicaid_enrolled: true + people: + client: + co_oap_eligible: true + co_oap_own_countable_income: 0 + ssi: 0 + co_oap_medicaid_enrolled: false + spouse: + ssi: 0 + co_oap_own_countable_income: 12_396 + co_oap_eligible: false + co_oap_medicaid_enrolled: true + marital_units: + marital_unit: + members: [client, spouse] + households: + household: + members: [client, spouse] + state_code: CO output: - co_oap_deeming_exempt: false + co_oap_deeming_exempt: [false, false] -- name: Case 5, an OAP client is receiving Adult Financial grant payments. +- name: Case 5, a spouse who is an OAP client receives Adult Financial grant payments. # Own countable income of 1,031/month leaves a grant of 1, so the spouse - # receives OAP with income within the limit. + # receives OAP with income within the limit. The client, married to an + # OAP-eligible spouse and within the limit, is exempt too. period: 2026 input: - state_code: CO - ssi: 0 - co_oap_own_countable_income: 12_372 - co_oap_eligible: true - co_oap_medicaid_enrolled: false + people: + client: + co_oap_eligible: true + co_oap_own_countable_income: 0 + ssi: 0 + co_oap_medicaid_enrolled: false + spouse: + ssi: 0 + co_oap_own_countable_income: 12_372 + co_oap_eligible: true + co_oap_medicaid_enrolled: false + marital_units: + marital_unit: + members: [client, spouse] + households: + household: + members: [client, spouse] + state_code: CO output: - co_oap_deeming_exempt: true + co_oap_deeming_exempt: [true, true] - name: Case 6, an OAP-eligible spouse with income equal to the standard receives no grant. # 3.533: income equal to the grant standard means denial, so the spouse is # not receiving Adult Financial grant payments. period: 2026 input: - state_code: CO - ssi: 0 - co_oap_own_countable_income: 12_384 - co_oap_eligible: true - co_oap_medicaid_enrolled: false + people: + client: + co_oap_eligible: true + co_oap_own_countable_income: 0 + ssi: 0 + co_oap_medicaid_enrolled: false + spouse: + ssi: 0 + co_oap_own_countable_income: 12_384 + co_oap_eligible: true + co_oap_medicaid_enrolled: false + marital_units: + marital_unit: + members: [client, spouse] + households: + household: + members: [client, spouse] + state_code: CO output: - co_oap_deeming_exempt: false + co_oap_deeming_exempt: [true, false] - name: Case 7, a spouse with no assistance has income deemed however low it is. period: 2026 input: - state_code: CO - ssi: 0 - co_oap_own_countable_income: 0 - co_oap_eligible: false - co_oap_medicaid_enrolled: false + people: + client: + co_oap_eligible: true + co_oap_own_countable_income: 0 + ssi: 0 + co_oap_medicaid_enrolled: false + spouse: + ssi: 0 + co_oap_own_countable_income: 0 + co_oap_eligible: false + co_oap_medicaid_enrolled: false + marital_units: + marital_unit: + members: [client, spouse] + households: + household: + members: [client, spouse] + state_code: CO output: - co_oap_deeming_exempt: false + co_oap_deeming_exempt: [false, false] - name: Case 8, reported SSI receipt counts. period: 2026 input: - state_code: CO - ssi: 0 - receives_ssi: true - co_oap_own_countable_income: 0 - co_oap_eligible: false - co_oap_medicaid_enrolled: false + people: + client: + co_oap_eligible: true + co_oap_own_countable_income: 0 + ssi: 0 + co_oap_medicaid_enrolled: false + spouse: + ssi: 0 + co_oap_own_countable_income: 0 + co_oap_eligible: false + co_oap_medicaid_enrolled: false + receives_ssi: true + marital_units: + marital_unit: + members: [client, spouse] + households: + household: + members: [client, spouse] + state_code: CO output: - co_oap_deeming_exempt: true + co_oap_deeming_exempt: [false, true] + +- name: Case 9, income a hair under the standard from float residue is not a grant. + # Own countable income of 1,031.9999/month, the residue of 1,032 computed + # from annual amounts, gives the OAP-eligible spouse no grant. + period: 2026 + input: + people: + client: + co_oap_eligible: true + co_oap_own_countable_income: 0 + ssi: 0 + co_oap_medicaid_enrolled: false + spouse: + ssi: 0 + co_oap_own_countable_income: 12_383.9988 + co_oap_eligible: true + co_oap_medicaid_enrolled: false + marital_units: + marital_unit: + members: [client, spouse] + households: + household: + members: [client, spouse] + state_code: CO + output: + co_oap_deeming_exempt: [true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_own_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_own_countable_income.yaml index 64e80ac9da0..79beddccd90 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_own_countable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_own_countable_income.yaml @@ -248,3 +248,22 @@ output: co_oap_own_countable_income: [0, 0] co_oap: [12_384, 0] + +- name: Case 14, a rental loss does not offset other income. + # 3.520.785(B)(20): rental income counts to the extent it exceeds expenses. + # $1,000/month of pension less the $20 disregard is 980/month, or + # 11,760/year; the grant of 52 is raised to the floor. + period: 2026 + input: + people: + person: + age: 62 + pension_income: 12_000 + rental_income: -6_000 + households: + household: + members: [person] + state_code: CO + output: + co_oap_own_countable_income: 11_760 + co_oap: 2_268 diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py b/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py new file mode 100644 index 00000000000..848130a22bd --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py @@ -0,0 +1,4 @@ +# Monthly amounts derived from annual float32 inputs carry residue of about +# $0.0001 near $1,000. The rule's own arithmetic produces half cents (earnings +# are halved), so a tenth of a cent separates residue from a real difference. +PRECISION = 0.001 diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py index 994831418c3..d366ddfbe31 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ssa.oap._precision import PRECISION class co_oap(Variable): @@ -8,7 +9,7 @@ class co_oap(Variable): unit = USD definition_period = YEAR defined_for = "co_oap_eligible" - # PDF pages 64, 74-77 + # PDF pages 64, 74-77, 134 reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=64" def formula(person, period, parameters): @@ -17,15 +18,17 @@ def formula(person, period, parameters): p = parameters(month).gov.states.co.ssa.oap # 9 CCR 2503-5 3.532(B), 3.533(B): the grant is the grant standard # less total countable income, SSI included (3.533(A)(2)(d)). - income = person("ssi", month) + person("co_oap_countable_income", month) - # Cents: monthly amounts derived from annual ones carry - # floating-point residue, which must not turn income equal to the - # standard into a positive grant. - grant = max_(0, np.round(p.grant_standard - income, 2)) + ssi = person("ssi", month) + income = ssi + person("co_oap_countable_income", month) + grant = p.grant_standard - income # 3.533 and 3.520.781(A): a client whose total countable income # equals or exceeds the grant standard is not eligible. # 3.531(D)(2), 3.532(B), 3.533(B): an eligible client's grant below # the payment floor standard is raised to it by a state # supplementary payment, month by month. - total = total + where(grant > 0, max_(grant, p.payment_floor), 0) + payment = where(grant > PRECISION, max_(grant, p.payment_floor), 0) + # 3.581(E): payments drop any cents, "except in cases where SSI + # income reduces the grant". + payment = where(ssi > 0, payment, np.floor(payment + PRECISION)) + total = total + payment return total diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py index 6521c621690..1580cb0e697 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py @@ -18,9 +18,17 @@ def formula(person, period, parameters): deemable = ~person("co_oap_deeming_exempt", period) ssi = person("ssi", period) earned = person("ssi_earned_income", period) * deemable - # 3.534(B)(1): the total unearned income of the spouse, SSI included - # (3.520.785(B)(1)), less exempt income (3.520.786). - unearned = (add(person, period, p.income.sources.unearned) + ssi) * deemable + # 3.534(B)(1): the total unearned income of the spouse, SSI and any + # matching ISM included (3.520.785(B)(1), 3.520.79(B)(6)), less exempt + # income (3.520.786). + unearned = ( + add( + person, + period, + ["co_oap_unearned_income", "co_oap_in_kind_support_and_maintenance"], + ) + + ssi + ) * deemable # 3.534(B)(5): court-ordered support obligations of the spouse. We # treat alimony paid as court-ordered. Whether the income is garnished # instead is not modeled. @@ -30,15 +38,16 @@ def formula(person, period, parameters): # 3.534(B)(3): for each dependent child in the spouse's household, # half the SSI benefit standard for an individual less the child's own - # income. + # income. 3.520.786's exemptions apply to the client, spouse and + # sponsor, not the child, so the child's gross income counts, child + # support and SSI included. year = period.this_year dependent_child = person("is_tax_unit_dependent", year) & person( "is_child", year ) - child_income = ( - person("ssi_earned_income", period) - + add(person, period, p.income.sources.unearned) - + ssi + child_income = max_( + 0, + add(person, period, ["ssi_earned_income", "ssi_unearned_income"]) + ssi, ) fbr = parameters(period).gov.ssa.ssi.amount.individual child_allocation = dependent_child * max_( diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deeming_exempt.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deeming_exempt.py index ad111097030..b0b6329ccab 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deeming_exempt.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deeming_exempt.py @@ -1,10 +1,16 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ssa.oap._precision import PRECISION class co_oap_deeming_exempt(Variable): value_type = bool entity = Person - label = "Colorado Old Age Pension spouse income not deemed" + label = "Colorado Old Age Pension client's spouse whose income is not deemed" + documentation = ( + "True for the spouse of an Old Age Pension-eligible client when " + "3.534(C) keeps the spouse's income from being deemed; false for " + "everyone else." + ) definition_period = MONTH # Not restricted to Colorado: 3.534(C) sets no residence condition on the # spouse. @@ -21,16 +27,26 @@ def formula(person, period, parameters): # standard, against the spouse's own total countable income under # 3.533, SSI included. income = person("co_oap_own_countable_income", period) + ssi - within_limit = income <= p.grant_standard + within_limit = income <= p.grant_standard + PRECISION receives_ssi = (ssi > 0) | person("receives_ssi", period) # Old Age Pension is the Adult Financial program modeled for someone # who is not on SSI; AND-CS requires SSI. Receipt is judged on the - # spouse's own income: when both spouses are OAP clients within the - # limit, neither one's income is deemed to the other. (A reading in - # which neither receives OAP, and each one's income is deemed to the - # other, can also be self-consistent; the model takes the one in which - # both are paid.) + # spouse's own income, as co_oap judges a positive grant: when both + # spouses are OAP clients within the limit, neither one's income is + # deemed to the other. (A reading in which neither receives OAP, and + # each one's income is deemed to the other, can also be + # self-consistent; the model takes the one in which both are paid.) year = period.this_year - receives_oap = person("co_oap_eligible", year) & (income < p.grant_standard) + eligible = person("co_oap_eligible", year) + receives_oap = eligible & (p.grant_standard - income > PRECISION) + spouse_of_client = person.marital_unit.sum(eligible) - eligible > 0 + candidate = spouse_of_client & within_limit + exempt = candidate & (receives_ssi | receives_oap) + # Medicaid decides only for a candidate not exempt otherwise. Skipping + # it when no one needs it avoids computing Medicaid enrollment for + # nothing. + needs_medicaid = candidate & ~exempt + if not needs_medicaid.any(): + return exempt medicaid = person("co_oap_medicaid_enrolled", year) - return (receives_ssi | receives_oap | medicaid) & within_limit + return exempt | (needs_medicaid & medicaid) diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.py new file mode 100644 index 00000000000..833a676e43d --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.py @@ -0,0 +1,22 @@ +from policyengine_us.model_api import * + + +class co_oap_in_kind_support_and_maintenance(Variable): + value_type = float + entity = Person + label = "Colorado Old Age Pension in-kind support and maintenance" + unit = USD + definition_period = MONTH + # PDF pages 72-73 + reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=72" + + def formula(person, period, parameters): + # 9 CCR 2503-5 3.520.79(B)(6): an SSI recipient charged in-kind support + # and maintenance by SSA is charged a matching ISM, counted as unearned + # income. A joint couple's ISM is one presumed maximum value for the + # couple, which SSI splits equally between the spouses. ISM through + # SSA's one-third reduction, and Colorado's own ISM for a client not + # on SSI (3.520.79(C)), are not modeled. + ism = person("ssi_in_kind_support_and_maintenance", period) + couple_computation = person("ssi_couple_computation_applies", period) + return where(couple_computation, ism / 2, ism) * (person("ssi", period) > 0) diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_medicaid_enrolled.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_medicaid_enrolled.py index 94a32c896c8..3f51054a54c 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_medicaid_enrolled.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_medicaid_enrolled.py @@ -13,18 +13,27 @@ class co_oap_medicaid_enrolled(Variable): ) def formula(person, period, parameters): + # Reported current coverage counts as receiving Medicaid assistance + # (3.534(C)) whatever projected eligibility and take-up say. + reported = person("receives_medicaid", period) p = parameters(period).gov.hhs.medicaid.eligibility.work_requirements if not p.applies: - return person("medicaid_enrolled", period) + return person("medicaid_enrolled", period) | reported # SNAP counts OAP as income, and Medicaid's SNAP pass-through from the # community engagement requirement (42 CFR 435.554(c)(7)) reads SNAP # receipt, so OAP, a spouse's Medicaid enrollment and SNAP depend on # each other. Medicaid is computed in a private branch where no - # spouse's Medicaid exempts their income from OAP deeming. That OAP is - # never higher than the final OAP, so SNAP there is never lower, and - # the spouse keeps every Medicaid route, supplied inputs included. - # The result does not depend on calculation order. + # spouse's Medicaid exempts their income from OAP deeming: enrollment + # is decided on the SNAP the household receives while the spouse's + # income is still deemed, before the exemption raises OAP. OAP there + # is never higher than the final OAP, so SNAP there is never lower, + # and the spouse keeps every Medicaid route, supplied inputs included. + # Higher OAP can only lower SNAP, so at most one outcome is + # self-consistent, and this finds it when it exists. When none exists + # (the exemption's higher OAP would end the SNAP receipt that gave the + # spouse Medicaid), this treats the spouse as enrolled. The result does + # not depend on calculation order. simulation = person.simulation branch_name = f"{simulation.branch_name}_co_oap_medicaid_{period}" try: @@ -34,7 +43,7 @@ def formula(person, period, parameters): period, {"co_oap_medicaid_enrolled": np.zeros(person.count, dtype=bool)}, ) - return branch.calculate("medicaid_enrolled", period) + return branch.calculate("medicaid_enrolled", period) | reported finally: # A branch clones cached arrays; retaining it would duplicate a # substantial part of the full-population simulation in memory. diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_own_countable_income.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_own_countable_income.py index aed915fa46d..21749b284e9 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_own_countable_income.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_own_countable_income.py @@ -22,16 +22,9 @@ def formula(person, period, parameters): earned = person("ssi_earned_income", period) ssi = person("ssi", period) receives_ssi = ssi > 0 - # 3.520.79(B)(6): an SSI recipient charged in-kind support and - # maintenance by SSA is charged a matching ISM. A joint couple's ISM is - # one presumed maximum value for the couple, which SSI splits equally - # between the spouses. ISM through SSA's one-third reduction, and - # Colorado's own ISM for a client not on SSI (3.520.79(C)), are not - # modeled. - ism = person("ssi_in_kind_support_and_maintenance", period) - couple_computation = person("ssi_couple_computation_applies", period) - ism = where(couple_computation, ism / 2, ism) * receives_ssi - unearned = add(person, period, p.sources.unearned) + ism + unearned = person("co_oap_unearned_income", period) + person( + "co_oap_in_kind_support_and_maintenance", period + ) # 3.533(A)(2)(c)(1): a married client's $20 disregard is split equally # with the spouse, so that no more than $20 applies to the couple. married = person.marital_unit.nb_persons() == 2 diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_unearned_income.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_unearned_income.py new file mode 100644 index 00000000000..8eaa9993f33 --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_unearned_income.py @@ -0,0 +1,18 @@ +from policyengine_us.model_api import * + + +class co_oap_unearned_income(Variable): + value_type = float + entity = Person + label = "Colorado Old Age Pension gross unearned income other than SSI" + unit = USD + definition_period = YEAR + # PDF pages 68-72 + reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=68" + + def formula(person, period, parameters): + sources = parameters(period).gov.states.co.ssa.oap.income.sources.unearned + # 9 CCR 2503-5 3.520.785(B)(20): rental income counts "to the extent + # it exceeds allowable expenses", so a loss does not offset other + # income. No source counts below zero. + return sum(max_(0, person(source, period)) for source in sources) From 38d60680271758e04eda4f975a68262f5ef708ef Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 23:41:44 -0400 Subject: [PATCH 06/11] Apply Sol round 3: seed the Medicaid branch with reported coverage; cents convention; child budget - Seed the branch with reported Medicaid coverage, so a known exemption holds there; state exactly when the one-step rule is self-consistent - Treat monthly amounts as recorded in cents: a quarter cent separates the rule's half-cent grid from float residue and averaging remainders - Count a dependent child by TANF's ages (under 18, or under 19 in secondary school) and floor each of the child's income sources at zero - Precompute arrays before sum() (code-health rule) Co-Authored-By: Claude Opus 5.5 --- .../tests/core/test_co_oap_invariants.py | 8 +-- ...si_state_supplement_medicaid_dependency.py | 53 ++++++++++++++ .../gov/states/co/ssa/oap/co_oap.yaml | 33 +++++++++ .../co/ssa/oap/co_oap_deemed_income.yaml | 72 +++++++++++++++++++ .../gov/states/co/ssa/oap/_precision.py | 12 ++-- .../states/co/ssa/oap/co_oap_deemed_income.py | 27 ++++--- .../co/ssa/oap/co_oap_medicaid_enrolled.py | 26 +++---- .../co/ssa/oap/co_oap_unearned_income.py | 3 +- 8 files changed, 205 insertions(+), 29 deletions(-) diff --git a/policyengine_us/tests/core/test_co_oap_invariants.py b/policyengine_us/tests/core/test_co_oap_invariants.py index b503cfc27e9..6c3067bf736 100644 --- a/policyengine_us/tests/core/test_co_oap_invariants.py +++ b/policyengine_us/tests/core/test_co_oap_invariants.py @@ -150,12 +150,12 @@ def assert_properties(cases, calculated=None): total = (ssi[client] + countable[client]) / 12 paid = oap[client] / 12 # 1. Payment rule and bounds. Payments not reduced by SSI drop - # cents (3.581(E)). Within a fifth of a cent of the standard, - # float32 sums can fall either way; YAML pins those. + # cents (3.581(E)). Within half a cent of the standard, the + # model's quarter-cent precision decides; YAML pins those. grant = standard - total if not eligible[client]: assert paid == 0, c - elif grant > 0.002: + elif grant > 0.005: expected = max(grant, floor) if ssi[client] > 0: assert paid == pytest.approx(expected, abs=0.05), c @@ -163,7 +163,7 @@ def assert_properties(cases, calculated=None): assert abs(paid - round(paid)) < 0.01, c assert expected - 1 < paid <= expected + 0.01, c assert floor - TOLERANCE <= paid <= standard + TOLERANCE, c - elif grant < -0.002: + elif grant < -0.005: assert paid == 0, c client = f"client_A{i}" if not c["married"]: diff --git a/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py b/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py index 2579b46e4eb..dffaf2429b0 100644 --- a/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py +++ b/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py @@ -148,6 +148,59 @@ def test_co_oap_medicaid_without_a_consistent_outcome(): assert results[0]["co_oap"][0] == pytest.approx(12_384) +def test_co_oap_medicaid_branch_keeps_reported_coverage(): + """Reported Medicaid coverage holds inside the branch. Two OAP couples and + an unrelated adult with a $14,000/year pension share one SNAP unit. Spouse + A reports Medicaid coverage, so A's client gets the full 1,032/month. + Spouse B, 40, earns $5,000/year as a SNAP workfare participant. With A's + income exempt the unit gets no SNAP, so B has no route to Medicaid and + B's earnings are deemed: (5,000 / 12 - 65) / 2 = 175.83/month, a grant + of 856.17 paid as 856 (3.581(E)), or 10,272/year.""" + year = 2027 + people = { + "client_a": {"age": {year: 62}}, + "spouse_a": { + "age": {year: 40}, + "employment_income": {year: 5_000}, + "is_snap_workfare_participant": {year: True}, + "receives_medicaid": {year: True}, + "takes_up_medicaid_if_eligible": {year: False}, + }, + "client_b": {"age": {year: 62}}, + "spouse_b": { + "age": {year: 40}, + "employment_income": {year: 5_000}, + "is_snap_workfare_participant": {year: True}, + }, + "adult": { + "age": {year: 40}, + "pension_income": {year: 14_000}, + "is_snap_workfare_participant": {year: True}, + }, + } + units = { + "a": {"members": ["client_a", "spouse_a"]}, + "b": {"members": ["client_b", "spouse_b"]}, + "adult": {"members": ["adult"]}, + } + everyone = {"members": list(people)} + situation = { + "people": people, + **{entity: units for entity in ("tax_units", "marital_units", "families")}, + "spm_units": {"unit": everyone}, + "households": {"unit": {**everyone, "state_code": {year: "CO"}}}, + } + variables = ("co_oap", "snap", "medicaid_enrolled") + results = [] + for order in (variables, variables[::-1]): + simulation = Simulation(situation=situation) + results.append({name: simulation.calculate(name, year) for name in order}) + for name in variables: + np.testing.assert_array_equal(results[0][name], results[1][name]) + assert results[0]["co_oap"][0] == pytest.approx(12_384) + assert results[0]["co_oap"][2] == pytest.approx(10_272) + + @pytest.mark.parametrize( "variable", ["medicaid_enrolled", "is_medicaid_eligible", "takes_up_medicaid_if_eligible"], diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml index 3f0596c7d47..34cbbd8bc90 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml @@ -414,3 +414,36 @@ output: co_oap_own_countable_income: 5_958 co_oap: 6_420 + +- name: Case 28, monthly income is recorded in cents. + # $12,623.99/year of rent averages to $1,051.9992/month (3.520.781(G)), a + # monthly income of $1,052.00 in cents. Less the $20 disregard that is + # 1,032, equal to the standard: denied (3.533). + period: 2026 + input: + people: + person: + age: 62 + rental_income: 12_623.99 + households: + household: + members: [person] + state_code: CO + output: + co_oap: 0 + +- name: Case 29, a grant from income recorded in cents drops its cents. + # $6,192.01/year of rent is $516.00/month in cents: 516 - 20 = 496; + # 1,032 - 496 = 536/month, or 6,432/year. + period: 2026 + input: + people: + person: + age: 62 + rental_income: 6_192.01 + households: + household: + members: [person] + state_code: CO + output: + co_oap: 6_432 diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deemed_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deemed_income.yaml index 4635e4a91d3..470e7e55d0d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deemed_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_deemed_income.yaml @@ -574,3 +574,75 @@ ssi: [264, 264] co_oap_deemed_income: [2_382, 0] co_oap: [6_756, 0] + +- name: Case 19, an 18-year-old in secondary school is a dependent child. + # As Case 5, with the child 18 and in secondary school. The rule does not + # define "dependent child"; TANF's ages (under 19 for a secondary school + # student) apply, so 2,000 - 1,032 - 497 = 471/month is deemed, or + # 5,652/year. The client: 1,032 - 471 = 561/month, or 6,732/year. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + pension_income: 24_000 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + child: + age: 18 + is_in_secondary_school: true + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, spouse, child] + marital_units: + marital_unit: + members: [head, spouse] + child_marital_unit: + members: [child] + households: + household: + members: [head, spouse, child] + state_code: CO + output: + co_oap_deemed_income: [5_652, 0, 0] + co_oap: [6_732, 0, 0] + +- name: Case 20, a child's rental loss does not offset the child's other income. + # As Case 5, with the child receiving $600/month of survivor benefits and a + # $300/month rental loss. The loss counts as zero, so the allocation is + # max(0, 497 - 600) = 0 and 968/month is deemed, or 11,616/year. + period: 2026 + input: + people: + head: + age: 62 + is_tax_unit_head: true + spouse: + age: 50 + pension_income: 24_000 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + child: + age: 10 + social_security_survivors: 7_200 + rental_income: -3_600 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, spouse, child] + marital_units: + marital_unit: + members: [head, spouse] + child_marital_unit: + members: [child] + households: + household: + members: [head, spouse, child] + state_code: CO + output: + co_oap_deemed_income: [11_616, 0, 0] + co_oap: [2_268, 0, 0] diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py b/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py index 848130a22bd..0b929471e2c 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py @@ -1,4 +1,8 @@ -# Monthly amounts derived from annual float32 inputs carry residue of about -# $0.0001 near $1,000. The rule's own arithmetic produces half cents (earnings -# are halved), so a tenth of a cent separates residue from a real difference. -PRECISION = 0.001 +# Colorado records monthly income and grants in dollars and cents +# (9 CCR 2503-5 3.581(E) speaks of cents; 3.533 and 3.534 of $65, $20.00 and +# halves). Amounts the rule produces therefore fall on a half-cent grid: +# earnings are halved. Anything smaller is float32 residue (about $0.0001 +# near $1,000) or the remainder of averaging an annual amount over twelve +# months, which a monthly record in cents does not hold. A quarter cent +# separates the two. +PRECISION = 0.0025 diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py index 1580cb0e697..7b8ab9728f5 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py @@ -38,17 +38,28 @@ def formula(person, period, parameters): # 3.534(B)(3): for each dependent child in the spouse's household, # half the SSI benefit standard for an individual less the child's own - # income. 3.520.786's exemptions apply to the client, spouse and - # sponsor, not the child, so the child's gross income counts, child - # support and SSI included. + # income. The rule does not define "dependent child"; an Adult + # Financial client with one must apply for Colorado Works (3.520.71(F)), + # so we use TANF's ages: under 18, or under 19 in secondary school. + # 3.520.786's exemptions apply to the client, spouse and sponsor, not + # the child, so the child's gross income counts, child support and SSI + # included, with no source below zero. year = period.this_year - dependent_child = person("is_tax_unit_dependent", year) & person( - "is_child", year + tanf_age = parameters(period).gov.hhs.tanf.cash.eligibility.age_limit + age_limit = where( + person("is_in_secondary_school", period), + tanf_age.student, + tanf_age.non_student, ) - child_income = max_( - 0, - add(person, period, ["ssi_earned_income", "ssi_unearned_income"]) + ssi, + dependent_child = person("is_tax_unit_dependent", year) & ( + person("age", year) < age_limit ) + ssi_sources = parameters(period).gov.ssa.ssi.income.sources + child_sources = [ + max_(0, person(source, period)) + for source in ["ssi_earned_income", *ssi_sources.unearned] + ] + child_income = sum(child_sources) + ssi fbr = parameters(period).gov.ssa.ssi.amount.individual child_allocation = dependent_child * max_( 0, fbr * p.income.deeming.child_allocation_share - child_income diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_medicaid_enrolled.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_medicaid_enrolled.py index 3f51054a54c..681f0d5230a 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_medicaid_enrolled.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_medicaid_enrolled.py @@ -23,17 +23,19 @@ def formula(person, period, parameters): # SNAP counts OAP as income, and Medicaid's SNAP pass-through from the # community engagement requirement (42 CFR 435.554(c)(7)) reads SNAP # receipt, so OAP, a spouse's Medicaid enrollment and SNAP depend on - # each other. Medicaid is computed in a private branch where no - # spouse's Medicaid exempts their income from OAP deeming: enrollment - # is decided on the SNAP the household receives while the spouse's - # income is still deemed, before the exemption raises OAP. OAP there - # is never higher than the final OAP, so SNAP there is never lower, - # and the spouse keeps every Medicaid route, supplied inputs included. - # Higher OAP can only lower SNAP, so at most one outcome is - # self-consistent, and this finds it when it exists. When none exists - # (the exemption's higher OAP would end the SNAP receipt that gave the - # spouse Medicaid), this treats the spouse as enrolled. The result does - # not depend on calculation order. + # each other. Medicaid is computed in a private branch where only + # reported coverage exempts a spouse's income from OAP deeming: + # enrollment is decided on the SNAP the household receives while the + # other spouses' income is still deemed, before an exemption raises + # OAP. The spouse keeps every Medicaid route, supplied inputs + # included, and the result does not depend on calculation order. + # In a SNAP unit with one spouse whose exemption turns on this, higher + # OAP can only lower SNAP, so at most one outcome is self-consistent, + # and this finds it when it exists; when none exists (the exemption's + # higher OAP would end the SNAP receipt that gave the spouse Medicaid), + # it treats the spouse as enrolled. With several such spouses in one + # SNAP unit, enrollment is decided with all of their income deemed, + # which need not be self-consistent. simulation = person.simulation branch_name = f"{simulation.branch_name}_co_oap_medicaid_{period}" try: @@ -41,7 +43,7 @@ def formula(person, period, parameters): simulation, branch_name, period, - {"co_oap_medicaid_enrolled": np.zeros(person.count, dtype=bool)}, + {"co_oap_medicaid_enrolled": np.asarray(reported, dtype=bool)}, ) return branch.calculate("medicaid_enrolled", period) | reported finally: diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_unearned_income.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_unearned_income.py index 8eaa9993f33..28888298095 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_unearned_income.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_unearned_income.py @@ -15,4 +15,5 @@ def formula(person, period, parameters): # 9 CCR 2503-5 3.520.785(B)(20): rental income counts "to the extent # it exceeds allowable expenses", so a loss does not offset other # income. No source counts below zero. - return sum(max_(0, person(source, period)) for source in sources) + floored = [max_(0, person(source, period)) for source in sources] + return sum(floored) From 479f0f6f9c1a7ef94d2b7c6718ef76da60d72eb7 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 13 Oct 2026 00:30:58 -0400 Subject: [PATCH 07/11] Apply Sol round 4: take monthly amounts in cents and SSI as SSA pays it - Monthly income from each source, earnings, ISM and support to the nearest cent before budgeting; SSI raised to the next cent as SSA pays it (POMS SI 02001.010). A tenth of a cent now only absorbs float32 residue - Narrow the Medicaid branch's fixed-point statement to a single unresolved exemption in a linked group of households Co-Authored-By: Claude Opus 5.5 --- .../tests/core/test_co_oap_invariants.py | 3 +- .../gov/states/co/ssa/oap/co_oap.yaml | 67 +++++++++++++++++++ .../gov/states/co/ssa/oap/_precision.py | 29 ++++++-- .../variables/gov/states/co/ssa/oap/co_oap.py | 7 +- .../states/co/ssa/oap/co_oap_deemed_income.py | 14 ++-- .../co/ssa/oap/co_oap_deeming_exempt.py | 7 +- .../co_oap_in_kind_support_and_maintenance.py | 4 +- .../co/ssa/oap/co_oap_medicaid_enrolled.py | 15 +++-- .../co/ssa/oap/co_oap_own_countable_income.py | 9 ++- .../co/ssa/oap/co_oap_unearned_income.py | 10 +-- 10 files changed, 136 insertions(+), 29 deletions(-) diff --git a/policyengine_us/tests/core/test_co_oap_invariants.py b/policyengine_us/tests/core/test_co_oap_invariants.py index 6c3067bf736..fb6b5a24e28 100644 --- a/policyengine_us/tests/core/test_co_oap_invariants.py +++ b/policyengine_us/tests/core/test_co_oap_invariants.py @@ -194,7 +194,8 @@ def assert_properties(cases, calculated=None): parameters, YEAR, )[0] - assert own == pytest.approx(expected, abs=TOLERANCE), c + # OAP takes each monthly source in cents; SSI does not. + assert own == pytest.approx(expected, abs=0.3), c continue a, b, c_, d = (f"client_{x}{i}" for x in ARRANGEMENTS) # 3. The Medicaid exemption only lowers countable income. diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml index 34cbbd8bc90..78e513048d7 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml @@ -447,3 +447,70 @@ state_code: CO output: co_oap: 6_432 + +- name: Case 30, a monthly average just under a cent boundary is recorded in cents. + # $12,623.96/year of rent averages to $1,051.9967/month, $1,052.00 in + # cents. Less $20 that equals the standard: denied, as in Case 28. + period: 2026 + input: + people: + person: + age: 62 + rental_income: 12_623.96 + households: + household: + members: [person] + state_code: CO + output: + co_oap: 0 + +- name: Case 31, a monthly average just over a cent boundary is recorded in cents. + # $6,192.04/year of rent averages to $516.0033/month, $516.00 in cents: + # the grant is 536/month, or 6,432/year, as in Case 29. + period: 2026 + input: + people: + person: + age: 62 + rental_income: 6_192.04 + households: + household: + members: [person] + state_code: CO + output: + co_oap: 6_432 + +- name: Case 32, SSI counts as SSA pays it, a fractional cent raised. + # Both are 65. The client earns $1,291/month and the spouse $100.03/month. + # The model computes SSI of $418.9925/month each; SSA raises a fractional + # cent to the next cent (POMS SI 02001.010), paying $419.00. The client's + # only unearned income is SSI, so (1,291 - 65) / 2 = 613, and 613 + 419 = + # 1,032 equals the standard: denied. The spouse is on SSI within the limit + # ((100.03 - 65) / 2 + 419 = 436.515), so nothing is deemed either way; + # the spouse's grant, 1,032 - 436.515 = 595.485, is reduced by SSI and + # keeps its cents: 7,145.82/year. + period: 2026 + absolute_error_margin: 0.1 + input: + people: + head: + age: 65 + employment_income: 15_492 + is_tax_unit_head: true + spouse: + age: 65 + employment_income: 1_200.36 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi: [5_027.91, 5_027.91] + co_oap: [0, 7_145.82] diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py b/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py index 0b929471e2c..f25c3f1e386 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py @@ -1,8 +1,25 @@ +import numpy as np + # Colorado records monthly income and grants in dollars and cents # (9 CCR 2503-5 3.581(E) speaks of cents; 3.533 and 3.534 of $65, $20.00 and -# halves). Amounts the rule produces therefore fall on a half-cent grid: -# earnings are halved. Anything smaller is float32 residue (about $0.0001 -# near $1,000) or the remainder of averaging an annual amount over twelve -# months, which a monthly record in cents does not hold. A quarter cent -# separates the two. -PRECISION = 0.0025 +# halves). The OAP variables therefore take each monthly amount in cents: +# income averaged from an annual amount (3.520.781(G)) to the nearest cent, +# and SSI as SSA pays it. The rule's own arithmetic then stays on a half-cent +# grid (earnings are halved), and anything smaller than a tenth of a cent is +# float32 residue. +PRECISION = 0.001 + + +def cents(amount): + """A monthly amount to the nearest cent.""" + return np.round(np.asarray(amount, dtype=np.float64), 2) + + +def ssi_paid(amount): + """A monthly SSI amount as SSA pays it. + + POMS SI 02001.010: a fractional cent of at least a tenth of a cent raises + the payment to the next higher cent; a smaller one is dropped. + """ + mills = np.floor(np.asarray(amount, dtype=np.float64) * 1_000 + PRECISION) + return np.ceil(mills / 10) / 100 diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py index d366ddfbe31..b128eb0a178 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py @@ -1,5 +1,8 @@ from policyengine_us.model_api import * -from policyengine_us.variables.gov.states.co.ssa.oap._precision import PRECISION +from policyengine_us.variables.gov.states.co.ssa.oap._precision import ( + PRECISION, + ssi_paid, +) class co_oap(Variable): @@ -18,7 +21,7 @@ def formula(person, period, parameters): p = parameters(month).gov.states.co.ssa.oap # 9 CCR 2503-5 3.532(B), 3.533(B): the grant is the grant standard # less total countable income, SSI included (3.533(A)(2)(d)). - ssi = person("ssi", month) + ssi = ssi_paid(person("ssi", month)) income = ssi + person("co_oap_countable_income", month) grant = p.grant_standard - income # 3.533 and 3.520.781(A): a client whose total countable income diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py index 7b8ab9728f5..d8f74ed484e 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deemed_income.py @@ -1,4 +1,8 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ssa.oap._precision import ( + cents, + ssi_paid, +) class co_oap_deemed_income(Variable): @@ -16,8 +20,9 @@ def formula(person, period, parameters): # 3.534(C): no income is deemed from a spouse who receives Adult # Financial, SSI or Medicaid with income within the OAP limit. deemable = ~person("co_oap_deeming_exempt", period) - ssi = person("ssi", period) - earned = person("ssi_earned_income", period) * deemable + # Monthly amounts in cents, SSI as SSA pays it (see _precision.py). + ssi = ssi_paid(person("ssi", period)) + earned = cents(person("ssi_earned_income", period)) * deemable # 3.534(B)(1): the total unearned income of the spouse, SSI and any # matching ISM included (3.520.785(B)(1), 3.520.79(B)(6)), less exempt # income (3.520.786). @@ -33,7 +38,8 @@ def formula(person, period, parameters): # treat alimony paid as court-ordered. Whether the income is garnished # instead is not modeled. support = ( - add(person, period, ["child_support_expense", "alimony_expense"]) * deemable + cents(add(person, period, ["child_support_expense", "alimony_expense"])) + * deemable ) # 3.534(B)(3): for each dependent child in the spouse's household, @@ -56,7 +62,7 @@ def formula(person, period, parameters): ) ssi_sources = parameters(period).gov.ssa.ssi.income.sources child_sources = [ - max_(0, person(source, period)) + cents(max_(0, person(source, period))) for source in ["ssi_earned_income", *ssi_sources.unearned] ] child_income = sum(child_sources) + ssi diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deeming_exempt.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deeming_exempt.py index b0b6329ccab..227011917f2 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deeming_exempt.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_deeming_exempt.py @@ -1,5 +1,8 @@ from policyengine_us.model_api import * -from policyengine_us.variables.gov.states.co.ssa.oap._precision import PRECISION +from policyengine_us.variables.gov.states.co.ssa.oap._precision import ( + PRECISION, + ssi_paid, +) class co_oap_deeming_exempt(Variable): @@ -22,7 +25,7 @@ def formula(person, period, parameters): # Financial grant payments, SSI or Medicaid "and has income no greater # than the OAP limit" is not deemed to the OAP client. p = parameters(period).gov.states.co.ssa.oap - ssi = person("ssi", period) + ssi = ssi_paid(person("ssi", period)) # The rule does not define the OAP limit. We read it as the OAP grant # standard, against the spouse's own total countable income under # 3.533, SSI included. diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.py index 833a676e43d..8bbc4bb9842 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ssa.oap._precision import cents class co_oap_in_kind_support_and_maintenance(Variable): @@ -19,4 +20,5 @@ def formula(person, period, parameters): # on SSI (3.520.79(C)), are not modeled. ism = person("ssi_in_kind_support_and_maintenance", period) couple_computation = person("ssi_couple_computation_applies", period) - return where(couple_computation, ism / 2, ism) * (person("ssi", period) > 0) + ism = where(couple_computation, ism / 2, ism) * (person("ssi", period) > 0) + return cents(ism) diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_medicaid_enrolled.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_medicaid_enrolled.py index 681f0d5230a..fafc5859681 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_medicaid_enrolled.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_medicaid_enrolled.py @@ -29,13 +29,14 @@ def formula(person, period, parameters): # other spouses' income is still deemed, before an exemption raises # OAP. The spouse keeps every Medicaid route, supplied inputs # included, and the result does not depend on calculation order. - # In a SNAP unit with one spouse whose exemption turns on this, higher - # OAP can only lower SNAP, so at most one outcome is self-consistent, - # and this finds it when it exists; when none exists (the exemption's - # higher OAP would end the SNAP receipt that gave the spouse Medicaid), - # it treats the spouse as enrolled. With several such spouses in one - # SNAP unit, enrollment is decided with all of their income deemed, - # which need not be self-consistent. + # Where a single unresolved exemption is all that links a group of + # households through SNAP units and marriages, higher OAP can only + # lower SNAP, so at most one outcome is self-consistent, and this + # finds it when it exists; when none exists (the exemption's higher + # OAP would end the SNAP receipt that gave the spouse Medicaid), it + # treats the spouse as enrolled. With several unresolved exemptions + # linked that way, enrollment is decided with all of their income + # deemed, which need not be self-consistent. simulation = person.simulation branch_name = f"{simulation.branch_name}_co_oap_medicaid_{period}" try: diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_own_countable_income.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_own_countable_income.py index 21749b284e9..538edb6de0b 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_own_countable_income.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_own_countable_income.py @@ -1,4 +1,8 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ssa.oap._precision import ( + cents, + ssi_paid, +) class co_oap_own_countable_income(Variable): @@ -19,8 +23,9 @@ def formula(person, period, parameters): p = parameters(period).gov.states.co.ssa.oap.income # 3.520.786 limits exempt earned income to the items it lists, so # SSI's student earned income exclusion is not deducted. - earned = person("ssi_earned_income", period) - ssi = person("ssi", period) + # Monthly amounts in cents, SSI as SSA pays it (see _precision.py). + earned = cents(person("ssi_earned_income", period)) + ssi = ssi_paid(person("ssi", period)) receives_ssi = ssi > 0 unearned = person("co_oap_unearned_income", period) + person( "co_oap_in_kind_support_and_maintenance", period diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_unearned_income.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_unearned_income.py index 28888298095..a3bfd10badd 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_unearned_income.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_unearned_income.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ssa.oap._precision import cents class co_oap_unearned_income(Variable): @@ -6,7 +7,7 @@ class co_oap_unearned_income(Variable): entity = Person label = "Colorado Old Age Pension gross unearned income other than SSI" unit = USD - definition_period = YEAR + definition_period = MONTH # PDF pages 68-72 reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=68" @@ -14,6 +15,7 @@ def formula(person, period, parameters): sources = parameters(period).gov.states.co.ssa.oap.income.sources.unearned # 9 CCR 2503-5 3.520.785(B)(20): rental income counts "to the extent # it exceeds allowable expenses", so a loss does not offset other - # income. No source counts below zero. - floored = [max_(0, person(source, period)) for source in sources] - return sum(floored) + # income. No source counts below zero. Each monthly amount is taken + # in cents (see _precision.py). + monthly = [cents(max_(0, person(source, period))) for source in sources] + return sum(monthly) From 60d87aac960c46481ce45c40e7d4a81404ec5bb6 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 13 Oct 2026 00:47:54 -0400 Subject: [PATCH 08/11] Apply Sol round 5 follow-ups: half-up cents, payable SSI-reduced grants, mills margin - Round monthly amounts half a cent up, with slack for float residue - Pay a grant reduced by SSI to the cent (3.581(E) keeps its cents) - Fix the unit of ssi_paid's margin; gate matching ISM on SSI as paid - Pin PRECISION with direct-input cases; count SSI as paid in invariant 1 Co-Authored-By: Claude Opus 5.5 --- .../tests/core/test_co_oap_invariants.py | 4 ++- .../gov/states/co/ssa/oap/co_oap.yaml | 26 ++++++++++++++++--- .../gov/states/co/ssa/oap/_precision.py | 21 ++++++++++----- .../variables/gov/states/co/ssa/oap/co_oap.py | 7 +++-- .../co_oap_in_kind_support_and_maintenance.py | 8 ++++-- 5 files changed, 52 insertions(+), 14 deletions(-) diff --git a/policyengine_us/tests/core/test_co_oap_invariants.py b/policyengine_us/tests/core/test_co_oap_invariants.py index fb6b5a24e28..9ce4b22beaf 100644 --- a/policyengine_us/tests/core/test_co_oap_invariants.py +++ b/policyengine_us/tests/core/test_co_oap_invariants.py @@ -50,6 +50,7 @@ import pytest from policyengine_us import Simulation +from policyengine_us.variables.gov.states.co.ssa.oap._precision import ssi_paid from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( _apply_ssi_exclusions, ) @@ -147,7 +148,8 @@ def assert_properties(cases, calculated=None): client = f"client_{arrangement}{i}" # Monthly amounts: the inputs and the program are constant # within the year. - total = (ssi[client] + countable[client]) / 12 + # SSI as SSA pays it, as the model counts it. + total = float(ssi_paid(ssi[client] / 12)) + countable[client] / 12 paid = oap[client] / 12 # 1. Payment rule and bounds. Payments not reduced by SSI drop # cents (3.581(E)). Within half a cent of the standard, the diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml index 78e513048d7..f8aa404c00f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml @@ -487,8 +487,8 @@ # only unearned income is SSI, so (1,291 - 65) / 2 = 613, and 613 + 419 = # 1,032 equals the standard: denied. The spouse is on SSI within the limit # ((100.03 - 65) / 2 + 419 = 436.515), so nothing is deemed either way; - # the spouse's grant, 1,032 - 436.515 = 595.485, is reduced by SSI and - # keeps its cents: 7,145.82/year. + # the spouse's grant, 1,032 - 436.515 = 595.485, is reduced by SSI, so it + # keeps its cents and is paid to the cent: 595.49, or 7,145.88/year. period: 2026 absolute_error_margin: 0.1 input: @@ -513,4 +513,24 @@ state_code: CO output: ssi: [5_027.91, 5_027.91] - co_oap: [0, 7_145.82] + co_oap: [0, 7_145.88] + +- name: Case 33, float residue just under the standard is no grant. + # 12,383.9988 / 12 = 1,031.9999, the residue of an income of 1,032. + period: 2026 + input: + co_oap_eligible: true + co_oap_countable_income: 12_383.9988 + ssi: 0 + output: + co_oap: 0 + +- name: Case 34, a half-cent grant from countable income is a grant. + # 12,383.94 / 12 = 1,031.995 leaves a grant of 0.005, raised to 189. + period: 2026 + input: + co_oap_eligible: true + co_oap_countable_income: 12_383.94 + ssi: 0 + output: + co_oap: 2_268 diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py b/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py index f25c3f1e386..adeff840aca 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py @@ -4,15 +4,22 @@ # (9 CCR 2503-5 3.581(E) speaks of cents; 3.533 and 3.534 of $65, $20.00 and # halves). The OAP variables therefore take each monthly amount in cents: # income averaged from an annual amount (3.520.781(G)) to the nearest cent, -# and SSI as SSA pays it. The rule's own arithmetic then stays on a half-cent -# grid (earnings are halved), and anything smaller than a tenth of a cent is -# float32 residue. +# half a cent rounded up, and SSI as SSA pays it. The rule's own arithmetic +# then stays on a half-cent grid (earnings are halved), and anything smaller +# than a tenth of a cent is float32 residue. PRECISION = 0.001 +# Annual amounts in cents average to multiples of 1/1,200 of a dollar, so an +# average is either exactly half a cent from a whole cent or at least $0.0008 +# away. Float32 residue near $1,000 is about $0.00006, so $0.00005 of slack +# (half a hundredth of a cent) keeps a true half cent from rounding down. +_TIE_SLACK = 0.00005 + def cents(amount): - """A monthly amount to the nearest cent.""" - return np.round(np.asarray(amount, dtype=np.float64), 2) + """A monthly amount to the nearest cent, half a cent rounded up.""" + amount = np.asarray(amount, dtype=np.float64) + return np.floor(amount * 100 + 0.5 + _TIE_SLACK * 100) / 100 def ssi_paid(amount): @@ -21,5 +28,7 @@ def ssi_paid(amount): POMS SI 02001.010: a fractional cent of at least a tenth of a cent raises the payment to the next higher cent; a smaller one is dropped. """ - mills = np.floor(np.asarray(amount, dtype=np.float64) * 1_000 + PRECISION) + amount = np.asarray(amount, dtype=np.float64) + # Whole mills, with half a hundredth of a mill of slack for residue. + mills = np.floor(amount * 1_000 + 0.05) return np.ceil(mills / 10) / 100 diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py index b128eb0a178..a97efb8add0 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap.py @@ -1,6 +1,7 @@ from policyengine_us.model_api import * from policyengine_us.variables.gov.states.co.ssa.oap._precision import ( PRECISION, + cents, ssi_paid, ) @@ -21,6 +22,8 @@ def formula(person, period, parameters): p = parameters(month).gov.states.co.ssa.oap # 9 CCR 2503-5 3.532(B), 3.533(B): the grant is the grant standard # less total countable income, SSI included (3.533(A)(2)(d)). + # PE-US's SSI counts interest that 20 CFR 416.1124(c)(22) + # excludes (#10079); OAP, which adds SSI back, offsets it. ssi = ssi_paid(person("ssi", month)) income = ssi + person("co_oap_countable_income", month) grant = p.grant_standard - income @@ -31,7 +34,7 @@ def formula(person, period, parameters): # supplementary payment, month by month. payment = where(grant > PRECISION, max_(grant, p.payment_floor), 0) # 3.581(E): payments drop any cents, "except in cases where SSI - # income reduces the grant". - payment = where(ssi > 0, payment, np.floor(payment + PRECISION)) + # income reduces the grant"; those are paid to the cent. + payment = where(ssi > 0, cents(payment), np.floor(payment + PRECISION)) total = total + payment return total diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.py index 8bbc4bb9842..076bbaa30a3 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_in_kind_support_and_maintenance.py @@ -1,5 +1,8 @@ from policyengine_us.model_api import * -from policyengine_us.variables.gov.states.co.ssa.oap._precision import cents +from policyengine_us.variables.gov.states.co.ssa.oap._precision import ( + cents, + ssi_paid, +) class co_oap_in_kind_support_and_maintenance(Variable): @@ -20,5 +23,6 @@ def formula(person, period, parameters): # on SSI (3.520.79(C)), are not modeled. ism = person("ssi_in_kind_support_and_maintenance", period) couple_computation = person("ssi_couple_computation_applies", period) - ism = where(couple_computation, ism / 2, ism) * (person("ssi", period) > 0) + receives_ssi = ssi_paid(person("ssi", period)) > 0 + ism = where(couple_computation, ism / 2, ism) * receives_ssi return cents(ism) From 858c0c0c6e85064a3fbd936763e4384ab54521de Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 13 Oct 2026 00:51:00 -0400 Subject: [PATCH 09/11] Allow per-source cent rounding in invariant 2's SSI comparison Co-Authored-By: Claude Opus 5.5 --- policyengine_us/tests/core/test_co_oap_invariants.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/policyengine_us/tests/core/test_co_oap_invariants.py b/policyengine_us/tests/core/test_co_oap_invariants.py index 9ce4b22beaf..d647814670c 100644 --- a/policyengine_us/tests/core/test_co_oap_invariants.py +++ b/policyengine_us/tests/core/test_co_oap_invariants.py @@ -178,11 +178,11 @@ def assert_properties(cases, calculated=None): and c["client_child_support"] == 0 and c["client_rental"] >= 0 ) + # OAP takes each monthly source in cents; SSI does not, so the + # comparisons allow $0.30/year of rounding. if ssi[client] > 0: if plain: - assert own >= results["ssi_countable_income"][client] - ( - TOLERANCE - ), c + assert own >= results["ssi_countable_income"][client] - 0.3, c elif not c["shelter_support"]: unearned = ( c["client_pension"] @@ -196,7 +196,6 @@ def assert_properties(cases, calculated=None): parameters, YEAR, )[0] - # OAP takes each monthly source in cents; SSI does not. assert own == pytest.approx(expected, abs=0.3), c continue a, b, c_, d = (f"client_{x}{i}" for x in ARRANGEMENTS) From ec4d1c06eacb3ffd2871dfc0a8e12d88e4168b55 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 13 Oct 2026 01:01:54 -0400 Subject: [PATCH 10/11] Round a true half cent up despite float32 residue; correct precision comments Co-Authored-By: Claude Opus 5.5 --- .../tests/core/test_co_oap_invariants.py | 2 +- .../baseline/gov/states/co/ssa/oap/co_oap.yaml | 18 ++++++++++++++++++ .../gov/states/co/ssa/oap/_precision.py | 13 ++++++++----- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/policyengine_us/tests/core/test_co_oap_invariants.py b/policyengine_us/tests/core/test_co_oap_invariants.py index d647814670c..acd93f2e6cf 100644 --- a/policyengine_us/tests/core/test_co_oap_invariants.py +++ b/policyengine_us/tests/core/test_co_oap_invariants.py @@ -180,7 +180,7 @@ def assert_properties(cases, calculated=None): ) # OAP takes each monthly source in cents; SSI does not, so the # comparisons allow $0.30/year of rounding. - if ssi[client] > 0: + if ssi_paid(ssi[client] / 12) > 0: if plain: assert own >= results["ssi_countable_income"][client] - 0.3, c elif not c["shelter_support"]: diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml index f8aa404c00f..70071f67479 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml @@ -534,3 +534,21 @@ ssi: 0 output: co_oap: 2_268 + +- name: Case 35, a monthly average of exactly half a cent rounds up. + # $9,000.06/year of rent averages to $750.005/month, $750.01 in cents + # (half a cent rounded up). 750.01 - 20 = 730.01; 1,032 - 730.01 = 301.99, + # paid as 301 (3.581(E)), or 3,612/year. + period: 2026 + input: + people: + person: + age: 62 + rental_income: 9_000.06 + households: + household: + members: [person] + state_code: CO + output: + co_oap_own_countable_income: 8_760.12 + co_oap: 3_612 diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py b/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py index adeff840aca..2dc17acd041 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py @@ -10,10 +10,12 @@ PRECISION = 0.001 # Annual amounts in cents average to multiples of 1/1,200 of a dollar, so an -# average is either exactly half a cent from a whole cent or at least $0.0008 -# away. Float32 residue near $1,000 is about $0.00006, so $0.00005 of slack -# (half a hundredth of a cent) keeps a true half cent from rounding down. -_TIE_SLACK = 0.00005 +# average is either exactly a half cent past a whole cent or at least $0.00083 +# away from one. Storing the annual amount in float32 and dividing it by 12 +# can leave about $0.0001 of residue near $1,000 a month, so $0.0004 of slack, +# about halfway between, keeps a true half cent from rounding down without +# moving any other average across a tie. +_TIE_SLACK = 0.0004 def cents(amount): @@ -29,6 +31,7 @@ def ssi_paid(amount): the payment to the next higher cent; a smaller one is dropped. """ amount = np.asarray(amount, dtype=np.float64) - # Whole mills, with half a hundredth of a mill of slack for residue. + # Whole mills, with half a tenth of a mill ($0.00005) of slack for + # float32 residue. mills = np.floor(amount * 1_000 + 0.05) return np.ceil(mills / 10) / 100 From 8d5abf104845cc22706518c2d0a90e729383f005 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 13 Oct 2026 01:36:26 -0400 Subject: [PATCH 11/11] Pin SSI-reduced cents with a tighter margin; state the slack's size limit Co-Authored-By: Claude Opus 5.5 --- .../policy/baseline/gov/states/co/ssa/oap/co_oap.yaml | 2 +- .../variables/gov/states/co/ssa/oap/_precision.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml index 70071f67479..5afc06b3ef2 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap.yaml @@ -490,7 +490,7 @@ # the spouse's grant, 1,032 - 436.515 = 595.485, is reduced by SSI, so it # keeps its cents and is paid to the cent: 595.49, or 7,145.88/year. period: 2026 - absolute_error_margin: 0.1 + absolute_error_margin: 0.01 input: people: head: diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py b/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py index 2dc17acd041..b784b65c44f 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/_precision.py @@ -11,10 +11,12 @@ # Annual amounts in cents average to multiples of 1/1,200 of a dollar, so an # average is either exactly a half cent past a whole cent or at least $0.00083 -# away from one. Storing the annual amount in float32 and dividing it by 12 -# can leave about $0.0001 of residue near $1,000 a month, so $0.0004 of slack, -# about halfway between, keeps a true half cent from rounding down without -# moving any other average across a tie. +# away from one. Storing the annual amount in float32 and then dividing it by +# 12 in float32 leaves about $0.0001 of residue near $1,000 a month, so $0.0004 +# of slack, about halfway between, keeps a true half cent from rounding down +# without moving any other average across a tie. That holds for a monthly +# amount from one source below about $4,000; above that, float32 cannot +# resolve the 1/1,200 grid, and a tie can round either way. _TIE_SLACK = 0.0004