From 75b9d64b5ea59097ad5ea81c43952b303e0e51b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 23:23:14 +0000 Subject: [PATCH 01/12] Round summed resources to cents before exact-limit asset tests Balances are stored as float32, so three accounts that add to exactly a limit in cents can sum a fraction of a cent above it (178.57 + 1,547.79 + 273.64 = 2,000.0001) and fail an "at or below" test, or below it and pass a "below" test. Add a shared round_to_cents helper and apply it where SSI, SNAP, Medicaid, the Medicare Savings Programs, BBCE and 47 state programs compare summed resources with a dollar limit. Each site has a YAML witness at the limit that fails on main, and a cent-over (or cent-under) case. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6HXUrFaqEeR9DeqWcq --- changelog.d/round-summed-limits.fixed.md | 1 + .../is_medically_needy_for_medicaid.yaml | 35 +++++ .../eligibility/msp_asset_eligible.yaml | 27 ++++ .../meets_tanf_non_cash_asset_test.yaml | 23 ++++ ...sources_deemed_from_ineligible_parent.yaml | 36 ++++++ .../resources/meets_ssi_resource_test.yaml | 25 ++++ .../dpa/atap/ak_atap_resources_eligible.yaml | 25 ++++ .../ar/dhs/tea/ar_tea_resources_eligible.yaml | 25 ++++ .../hhs/tanf/az_tanf_resources_eligible.yaml | 25 ++++ .../ca_capi_resource_eligible.yaml | 29 +++++ .../states/co/ssa/oap/co_oap_eligible.yaml | 29 +++++ .../eligibility/ct_ssp_resource_eligible.yaml | 29 +++++ .../ct_tfa_resources_eligible.yaml | 25 ++++ .../dc_tanf_resources_eligible.yaml | 25 ++++ .../de_tanf_resources_eligible.yaml | 25 ++++ .../dcf/tanf/fl_tca_resources_eligible.yaml | 25 ++++ .../ga_tanf_resources_eligible.yaml | 25 ++++ .../ia/dhs/fip/ia_fip_resources_eligible.yaml | 25 ++++ .../states/ia/hhs/ssa/ia_ssa_eligible.yaml | 28 ++++ .../id/tafi/id_tafi_resources_eligible.yaml | 25 ++++ .../resources/in_tanf_resources_eligible.yaml | 25 ++++ .../dcf/tanf/ks_tanf_resources_eligible.yaml | 25 ++++ .../dcbs/ktap/ky_ktap_resource_eligible.yaml | 25 ++++ .../dhhs/tanf/me_tanf_resources_eligible.yaml | 25 ++++ .../mi_fip_resources_eligible.yaml | 25 ++++ .../dcyf/mfip/mn_mfip_resources_eligible.yaml | 25 ++++ .../eligibility/mn_msa_resource_eligible.yaml | 29 +++++ .../dss/tanf/mo_tanf_resources_eligible.yaml | 25 ++++ .../dhs/tanf/ms_tanf_resources_eligible.yaml | 25 ++++ .../eligibility/ms_hmw_resource_eligible.yaml | 29 +++++ .../mt_tanf_resources_eligible.yaml | 25 ++++ .../dhs/tanf/nd_tanf_resources_eligible.yaml | 25 ++++ .../dhhs/adc/ne_adc_resources_eligible.yaml | 25 ++++ .../dhhs/fanf/nh_fanf_resources_eligible.yaml | 25 ++++ .../states/nh/dhhs/ssp/nh_ssp_eligible.yaml | 28 ++++ .../wfnj/nj_wfnj_resources_eligible.yaml | 25 ++++ .../nm_works_resources_eligible.yaml | 25 ++++ .../nv_tanf_resources_eligible.yaml | 25 ++++ .../otda/tanf/ny_tanf_resources_eligible.yaml | 25 ++++ .../dhs/tanf/ok_tanf_resources_eligible.yaml | 25 ++++ .../odhs/tanf/or_tanf_resources_eligible.yaml | 25 ++++ .../pa_tanf_resources_eligible.yaml | 25 ++++ .../dhs/works/ri_works_resource_eligible.yaml | 25 ++++ .../sc/tanf/sc_tanf_resources_eligible.yaml | 25 ++++ .../sd_tanf_resources_eligible.yaml | 25 ++++ .../eligibility/tn_ff_resources_eligible.yaml | 25 ++++ .../tx_tanf_resources_eligible.yaml | 25 ++++ .../ut_fep_resources_eligible.yaml | 25 ++++ .../vt_reach_up_resources_eligible.yaml | 25 ++++ .../dshs/tanf/wa_tanf_resources_eligible.yaml | 25 ++++ .../works/wi_works_resources_eligible.yaml | 25 ++++ .../works/wv_works_resources_eligible.yaml | 25 ++++ .../power/wy_power_resources_eligible.yaml | 25 ++++ .../eligibility/meets_snap_asset_test.yaml | 23 ++++ .../unit/test_round_to_cents_properties.py | 121 ++++++++++++++++++ policyengine_us/tools/general.py | 14 ++ ...ional_senior_or_disabled_asset_eligible.py | 2 +- .../is_medically_needy_for_medicaid.py | 3 +- .../eligibility/msp_asset_eligible.py | 11 +- .../meets_tanf_non_cash_asset_test.py | 3 +- ...resources_deemed_from_ineligible_parent.py | 7 +- .../resources/meets_ssi_resource_test.py | 11 +- .../eligibility/ak_atap_resources_eligible.py | 3 +- .../eligibility/ar_tea_resources_eligible.py | 3 +- .../eligibility/az_tanf_resources_eligible.py | 3 +- .../eligibility/ca_capi_resource_eligible.py | 3 +- .../gov/states/co/ssa/oap/co_oap_eligible.py | 3 +- .../eligibility/ct_ssp_resource_eligible.py | 3 +- .../eligibility/ct_tfa_resources_eligible.py | 3 +- .../eligibility/dc_tanf_resources_eligible.py | 5 +- .../eligibility/de_tanf_resources_eligible.py | 3 +- .../eligibility/fl_tca_resources_eligible.py | 3 +- .../eligibility/ga_tanf_resources_eligible.py | 3 +- .../eligibility/ia_fip_resources_eligible.py | 3 +- .../gov/states/ia/hhs/ssa/ia_ssa_eligible.py | 2 + .../eligibility/id_tafi_resources_eligible.py | 3 +- .../eligibility/in_tanf_resources_eligible.py | 3 +- .../eligibility/ks_tanf_resources_eligible.py | 3 +- .../eligibility/ky_ktap_resource_eligible.py | 3 +- .../eligibility/me_tanf_resources_eligible.py | 5 +- .../eligibility/mi_fip_resources_eligible.py | 3 +- .../eligibility/mn_mfip_resources_eligible.py | 3 +- .../eligibility/mn_msa_resource_eligible.py | 3 +- .../eligibility/mo_tanf_resources_eligible.py | 3 +- .../eligibility/ms_tanf_resources_eligible.py | 5 +- .../eligibility/ms_hmw_resource_eligible.py | 3 +- .../eligibility/mt_tanf_resources_eligible.py | 5 +- .../eligibility/nd_tanf_resources_eligible.py | 3 +- .../eligibility/ne_adc_resources_eligible.py | 3 +- .../eligibility/nh_fanf_resources_eligible.py | 3 +- .../gov/states/nh/dhhs/ssp/nh_ssp_eligible.py | 2 + .../eligibility/nj_wfnj_resources_eligible.py | 3 +- .../nm_works_resources_eligible.py | 5 +- .../eligibility/nv_tanf_resources_eligible.py | 3 +- .../eligibility/ny_tanf_resources_eligible.py | 3 +- .../eligibility/ok_tanf_resources_eligible.py | 3 +- .../eligibility/or_tanf_resources_eligible.py | 5 +- .../eligibility/pa_tanf_resources_eligible.py | 6 +- .../eligibility/ri_works_resource_eligible.py | 3 +- .../eligibility/sc_tanf_resources_eligible.py | 3 +- .../eligibility/sd_tanf_resources_eligible.py | 3 +- .../eligibility/tn_ff_resources_eligible.py | 5 +- .../eligibility/tx_tanf_resources_eligible.py | 5 +- .../eligibility/ut_fep_resources_eligible.py | 3 +- .../vt_reach_up_resources_eligible.py | 3 +- .../dshs/tanf/wa_tanf_resources_eligible.py | 5 +- .../wi_works_resources_eligible.py | 3 +- .../wv_works_resources_eligible.py | 5 +- .../wy_power_resources_eligible.py | 3 +- .../snap/eligibility/meets_snap_asset_test.py | 3 +- 110 files changed, 1648 insertions(+), 60 deletions(-) create mode 100644 changelog.d/round-summed-limits.fixed.md create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/ssa/ia_ssa_eligible.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/ssp/nh_ssp_eligible.yaml create mode 100644 policyengine_us/tests/unit/test_round_to_cents_properties.py diff --git a/changelog.d/round-summed-limits.fixed.md b/changelog.d/round-summed-limits.fixed.md new file mode 100644 index 00000000000..548cceea1c7 --- /dev/null +++ b/changelog.d/round-summed-limits.fixed.md @@ -0,0 +1 @@ +Round summed resources to whole cents before SSI, SNAP, Medicaid, Medicare Savings Program and state asset tests compare them with their dollar limits, so balances that add to exactly a limit pass or fail as the total does. diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.yaml index a3934f084f7..7257fdd5de9 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.yaml @@ -940,3 +940,38 @@ # 2027 still reads the 195,000 couple limit; 31,000 first applies in 2028. is_in_medicaid_medically_needy_category: [true, true] is_medically_needy_for_medicaid: [true, true] + +# Arkansas' 2,000 individual asset limit: 178.57 + 1,547.79 + 273.64 = +# 2,000.00. The float32 balances add to 2,000.0001 unless rounded to cents. +- name: Three accounts that total exactly the Arkansas limit pass + period: 2026 + input: + is_disabled: true + meets_ssi_disability_criteria: true + is_in_medicaid_medically_needy_category: true + is_adult_for_medicaid: false + is_optional_senior_or_disabled_for_medicaid: false + ssi_countable_income: 0 + bank_account_assets: 178.57 + stock_assets: 1_547.79 + bond_assets: 273.64 + state_code: AR + output: + is_medically_needy_for_medicaid: true + +# 178.57 + 1,547.79 + 273.65 = 2,000.01, a cent over the limit. +- name: Three accounts a cent over the Arkansas limit fail + period: 2026 + input: + is_disabled: true + meets_ssi_disability_criteria: true + is_in_medicaid_medically_needy_category: true + is_adult_for_medicaid: false + is_optional_senior_or_disabled_for_medicaid: false + ssi_countable_income: 0 + bank_account_assets: 178.57 + stock_assets: 1_547.79 + bond_assets: 273.65 + state_code: AR + output: + is_medically_needy_for_medicaid: false diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/eligibility/msp_asset_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/eligibility/msp_asset_eligible.yaml index feaba98d662..e932d51a68d 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/eligibility/msp_asset_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/eligibility/msp_asset_eligible.yaml @@ -437,3 +437,30 @@ # July 2027; resources may not exceed it. msp_asset_eligible: 2027-07: [true, true] + +# 42 U.S.C. 1396d(p)(1)(C): 2,885.40 + 5,798.41 + 976.19 = 9,660.00, at the +# 2025 individual limit. The float32 balances add to 9,660.001 unless rounded. +- name: Three accounts that total exactly the individual limit pass + period: 2025-01 + input: + age: 67 + bank_account_assets: 2_885.40 + stock_assets: 5_798.41 + bond_assets: 976.19 + spm_unit_is_married: false + state_code: TX + output: + msp_asset_eligible: true + +# 2,885.40 + 5,798.41 + 976.20 = 9,660.01, a cent over the limit. +- name: Three accounts a cent over the individual limit fail + period: 2025-01 + input: + age: 67 + bank_account_assets: 2_885.40 + stock_assets: 5_798.41 + bond_assets: 976.20 + spm_unit_is_married: false + state_code: TX + output: + msp_asset_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/tanf/non_cash/meets_tanf_non_cash_asset_test.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/tanf/non_cash/meets_tanf_non_cash_asset_test.yaml index 9a37b0b65a0..998283aefdb 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/tanf/non_cash/meets_tanf_non_cash_asset_test.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/tanf/non_cash/meets_tanf_non_cash_asset_test.yaml @@ -106,3 +106,26 @@ household_vehicles_value: 18_000 output: meets_tanf_non_cash_asset_test: true + +# Idaho's 5,000 BBCE asset limit: 3,247.08 + 1,316.70 + 436.22 = 5,000.00. +# The float32 balances add to 5,000.0005 unless rounded to cents. +- name: Three accounts that total exactly the Idaho limit pass + period: 2026 + input: + state_code_str: ID + bank_account_assets: 3_247.08 + stock_assets: 1_316.70 + bond_assets: 436.22 + output: + meets_tanf_non_cash_asset_test: true + +# 3,247.08 + 1,316.70 + 436.23 = 5,000.01, a cent over the limit. +- name: Three accounts a cent over the Idaho limit fail + period: 2026 + input: + state_code_str: ID + bank_account_assets: 3_247.08 + stock_assets: 1_316.70 + bond_assets: 436.23 + output: + meets_tanf_non_cash_asset_test: false diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml index 7d42e07cc67..660a4855fb3 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml @@ -397,6 +397,42 @@ ssi_resources_deemed_from_ineligible_parent: [0, 500, 0, 500, 0, 500] meets_ssi_resource_test: [false, true, false, true, false, false] +# 20 CFR 416.1202(b)(1), 416.1205: the child's 575.52 + 570.57 + 353.91 = +# 1,500.00 plus parental excess 2,500 - 2,000 = 500 is 2,000.00, at the +# individual limit. The child's float32 balances add to 1,500.0001 unless +# rounded to cents. +- name: Child accounts plus parental excess that total exactly the limit pass + period: 2026-01 + input: + people: + parent: + age: 40 + is_ssi_aged_blind_disabled: false + is_tax_unit_head: true + is_parent: true + bank_account_assets: 2_500 + child: + age: 16 + is_ssi_aged_blind_disabled: true + is_tax_unit_dependent: true + bank_account_assets: 575.52 + stock_assets: 570.57 + bond_assets: 353.91 + tax_units: + tax_unit: + members: [parent, child] + marital_units: + parent_unit: + members: [parent] + child_unit: + members: [child] + households: + household: + members: [parent, child] + output: + ssi_resources_deemed_from_ineligible_parent: [0, 500] + meets_ssi_resource_test: [false, true] + # POMS SI 01330.340 White example: parent pool1200 <=couple limit3000; child deemed0. - name: POMS White eligible parent leaves no excess resources to deem period: 2024-01 diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/meets_ssi_resource_test.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/meets_ssi_resource_test.yaml index 111d5625edf..208c36cb1f0 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/meets_ssi_resource_test.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/meets_ssi_resource_test.yaml @@ -326,3 +326,28 @@ ssi_resources_deemed_from_ineligible_parent: [0, 0, 0, 0, 0, 0, 0, 0, 0] ssi_resources_deemed_from_ineligible_spouse: [3001, 0, 0, 0, 0, 0, 0, 0, 0] meets_ssi_resource_test: [true, true, true, true, true, true, true, true, true] + +# 20 CFR 416.1205: 178.57 + 1,547.79 + 273.64 = 2,000.00, at the individual +# limit. The three float32 balances add to 2,000.0001 unless rounded to cents. +- name: Three accounts that total exactly the individual limit pass + period: 2026-01 + input: + age: 70 + is_ssi_aged_blind_disabled: true + bank_account_assets: 178.57 + stock_assets: 1_547.79 + bond_assets: 273.64 + output: + meets_ssi_resource_test: true + +# 178.57 + 1,547.79 + 273.65 = 2,000.01, a cent over the limit. +- name: Three accounts a cent over the individual limit fail + period: 2026-01 + input: + age: 70 + is_ssi_aged_blind_disabled: true + bank_account_assets: 178.57 + stock_assets: 1_547.79 + bond_assets: 273.65 + output: + meets_ssi_resource_test: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ak/dpa/atap/ak_atap_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ak/dpa/atap/ak_atap_resources_eligible.yaml index 7c1b7871e76..cdf490fab39 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ak/dpa/atap/ak_atap_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ak/dpa/atap/ak_atap_resources_eligible.yaml @@ -188,3 +188,28 @@ # Elderly, limit is $3,000 # $3,000 = $3,000 = eligible ak_atap_resources_eligible: true + +# 1,157.81 + 285.17 + 557.02 = 2,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 2,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.02 + state_code: AK + output: + ak_atap_resources_eligible: true + +# 1,157.81 + 285.17 + 557.03 = 2,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.03 + state_code: AK + output: + ak_atap_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/dhs/tea/ar_tea_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/dhs/tea/ar_tea_resources_eligible.yaml index 3ff73c08f4c..35116efc03b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/dhs/tea/ar_tea_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/dhs/tea/ar_tea_resources_eligible.yaml @@ -95,3 +95,28 @@ output: ar_tea_resources_eligible: false # 10,000 > 3,000 -> ineligible + +# 798.28 + 2,187.62 + 14.10 = 3,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 3,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 798.28 + stock_assets: 2_187.62 + bond_assets: 14.10 + state_code: AR + output: + ar_tea_resources_eligible: true + +# 798.28 + 2,187.62 + 14.11 = 3,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 798.28 + stock_assets: 2_187.62 + bond_assets: 14.11 + state_code: AR + output: + ar_tea_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/az/hhs/tanf/az_tanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/az/hhs/tanf/az_tanf_resources_eligible.yaml index 253c6e15d87..13ad274e098 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/az/hhs/tanf/az_tanf_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/az/hhs/tanf/az_tanf_resources_eligible.yaml @@ -100,3 +100,28 @@ output: # $5,000 > $2,000 limit az_tanf_resources_eligible: false + +# 1,157.81 + 285.17 + 557.02 = 2,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 2,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.02 + state_code: AZ + output: + az_tanf_resources_eligible: true + +# 1,157.81 + 285.17 + 557.03 = 2,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.03 + state_code: AZ + output: + az_tanf_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/cdss/capi/eligibility/ca_capi_resource_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/cdss/capi/eligibility/ca_capi_resource_eligible.yaml index e21c6962b06..2e7d8d9d241 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/cdss/capi/eligibility/ca_capi_resource_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/cdss/capi/eligibility/ca_capi_resource_eligible.yaml @@ -33,3 +33,32 @@ state_code: CA output: ca_capi_resource_eligible: false + +# 1,157.81 + 285.17 + 557.02 = 2,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 2,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025 + input: + age: 70 + is_ssi_aged_blind_disabled: true + is_ssi_disabled: true + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.02 + state_code: CA + output: + ca_capi_resource_eligible: true + +# 1,157.81 + 285.17 + 557.03 = 2,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025 + input: + age: 70 + is_ssi_aged_blind_disabled: true + is_ssi_disabled: true + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.03 + state_code: CA + output: + ca_capi_resource_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_eligible.yaml index 88b99f6d860..af164364611 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/oap/co_oap_eligible.yaml @@ -47,3 +47,32 @@ state_code: CO output: co_oap_eligible: true + +# 1,157.81 + 285.17 + 557.02 = 2,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 2,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025 + input: + age: 70 + is_ssi_aged_blind_disabled: true + is_ssi_disabled: true + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.02 + state_code: CO + output: + co_oap_eligible: true + +# 1,157.81 + 285.17 + 557.03 = 2,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025 + input: + age: 70 + is_ssi_aged_blind_disabled: true + is_ssi_disabled: true + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.03 + state_code: CO + output: + co_oap_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ct/dss/ssp/eligibility/ct_ssp_resource_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ct/dss/ssp/eligibility/ct_ssp_resource_eligible.yaml index f1c4a0a51dc..f88d486d645 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ct/dss/ssp/eligibility/ct_ssp_resource_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ct/dss/ssp/eligibility/ct_ssp_resource_eligible.yaml @@ -204,3 +204,32 @@ output: # Married households use the $2,400 couple limit; $2,500 is ineligible. ct_ssp_resource_eligible: [false, false] + +# 1,079.68 + 511.39 + 8.93 = 1,600.00, at the limit. +# The float32 balances add to a fraction of a cent above 1,600 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 70 + is_ssi_aged_blind_disabled: true + is_ssi_disabled: true + bank_account_assets: 1_079.68 + stock_assets: 511.39 + bond_assets: 8.93 + state_code: CT + output: + ct_ssp_resource_eligible: true + +# 1,079.68 + 511.39 + 8.94 = 1,600.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 70 + is_ssi_aged_blind_disabled: true + is_ssi_disabled: true + bank_account_assets: 1_079.68 + stock_assets: 511.39 + bond_assets: 8.94 + state_code: CT + output: + ct_ssp_resource_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ct/dss/tfa/eligibility/ct_tfa_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ct/dss/tfa/eligibility/ct_tfa_resources_eligible.yaml index 9fbf36fe64d..65e8bf5fdc3 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ct/dss/tfa/eligibility/ct_tfa_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ct/dss/tfa/eligibility/ct_tfa_resources_eligible.yaml @@ -123,3 +123,28 @@ output: # $5,000 > $3,000 old limit, ineligible before 10/1/2023 ct_tfa_resources_eligible: false + +# 4,489.49 + 1,282.41 + 228.10 = 6,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 6,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 4_489.49 + stock_assets: 1_282.41 + bond_assets: 228.10 + state_code: CT + output: + ct_tfa_resources_eligible: true + +# 4,489.49 + 1,282.41 + 228.11 = 6,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 4_489.49 + stock_assets: 1_282.41 + bond_assets: 228.11 + state_code: CT + output: + ct_tfa_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/dc/dhs/tanf/eligibility/dc_tanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/dc/dhs/tanf/eligibility/dc_tanf_resources_eligible.yaml index f316a3c6abd..004e26a22b4 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/dc/dhs/tanf/eligibility/dc_tanf_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/dc/dhs/tanf/eligibility/dc_tanf_resources_eligible.yaml @@ -35,3 +35,28 @@ bank_account_assets: 2_001 output: dc_tanf_resources_eligible: false + +# 798.28 + 2,187.62 + 14.10 = 3,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 3,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 798.28 + stock_assets: 2_187.62 + bond_assets: 14.10 + state_code: DC + output: + dc_tanf_resources_eligible: true + +# 798.28 + 2,187.62 + 14.11 = 3,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 798.28 + stock_assets: 2_187.62 + bond_assets: 14.11 + state_code: DC + output: + dc_tanf_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/de/dhss/tanf/eligibility/de_tanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/de/dhss/tanf/eligibility/de_tanf_resources_eligible.yaml index 42366652f8c..8a0d4c517f6 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/de/dhss/tanf/eligibility/de_tanf_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/de/dhss/tanf/eligibility/de_tanf_resources_eligible.yaml @@ -72,3 +72,28 @@ output: # $10,001 > $10,000 limit → ineligible de_tanf_resources_eligible: false + +# 8,688.73 + 9.74 + 1,301.53 = 10,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 10,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 8_688.73 + stock_assets: 9.74 + bond_assets: 1_301.53 + state_code: DE + output: + de_tanf_resources_eligible: true + +# 8,688.73 + 9.74 + 1,301.54 = 10,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 8_688.73 + stock_assets: 9.74 + bond_assets: 1_301.54 + state_code: DE + output: + de_tanf_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/fl/dcf/tanf/fl_tca_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/fl/dcf/tanf/fl_tca_resources_eligible.yaml index f8b813e1729..b281102d1c2 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/fl/dcf/tanf/fl_tca_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/fl/dcf/tanf/fl_tca_resources_eligible.yaml @@ -69,3 +69,28 @@ state_code: FL output: fl_tca_resources_eligible: false + +# 1,157.81 + 285.17 + 557.02 = 2,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 2,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.02 + state_code: FL + output: + fl_tca_resources_eligible: true + +# 1,157.81 + 285.17 + 557.03 = 2,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.03 + state_code: FL + output: + fl_tca_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ga/dfcs/tanf/eligibility/ga_tanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ga/dfcs/tanf/eligibility/ga_tanf_resources_eligible.yaml index 21f0ca1b77e..da1ffd0b19d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ga/dfcs/tanf/eligibility/ga_tanf_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ga/dfcs/tanf/eligibility/ga_tanf_resources_eligible.yaml @@ -29,3 +29,28 @@ spm_unit_cash_assets: 999 output: ga_tanf_resources_eligible: true + +# 650.46 + 299.64 + 49.90 = 1,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 1,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 650.46 + stock_assets: 299.64 + bond_assets: 49.90 + state_code: GA + output: + ga_tanf_resources_eligible: true + +# 650.46 + 299.64 + 49.91 = 1,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 650.46 + stock_assets: 299.64 + bond_assets: 49.91 + state_code: GA + output: + ga_tanf_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_resources_eligible.yaml index 5aab0ffe678..3e665e4b9cf 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_resources_eligible.yaml @@ -97,3 +97,28 @@ output: # At limit should be eligible (<=) ia_fip_resources_eligible: true + +# 1,157.81 + 285.17 + 557.02 = 2,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 2,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.02 + state_code: IA + output: + ia_fip_resources_eligible: true + +# 1,157.81 + 285.17 + 557.03 = 2,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.03 + state_code: IA + output: + ia_fip_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/ssa/ia_ssa_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/ssa/ia_ssa_eligible.yaml new file mode 100644 index 00000000000..48895eb269d --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/ssa/ia_ssa_eligible.yaml @@ -0,0 +1,28 @@ +# 1,157.81 + 285.17 + 557.02 = 2,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 2,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 70 + is_ssi_aged_blind_disabled: true + is_ssi_disabled: true + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.02 + state_code: IA + output: + ia_ssa_eligible: true + +# 1,157.81 + 285.17 + 557.03 = 2,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 70 + is_ssi_aged_blind_disabled: true + is_ssi_disabled: true + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.03 + state_code: IA + output: + ia_ssa_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/id/tafi/id_tafi_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/id/tafi/id_tafi_resources_eligible.yaml index 451c82e2971..7bdf37bdcaa 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/id/tafi/id_tafi_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/id/tafi/id_tafi_resources_eligible.yaml @@ -101,3 +101,28 @@ output: id_tafi_resources_eligible: false # $100,000 > $5,000 -> ineligible + +# 4,211.74 + 769.20 + 19.06 = 5,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 5,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 4_211.74 + stock_assets: 769.20 + bond_assets: 19.06 + state_code: ID + output: + id_tafi_resources_eligible: true + +# 4,211.74 + 769.20 + 19.07 = 5,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 4_211.74 + stock_assets: 769.20 + bond_assets: 19.07 + state_code: ID + output: + id_tafi_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/in/fssa/tanf/resources/in_tanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/in/fssa/tanf/resources/in_tanf_resources_eligible.yaml index 0e5da21a6d4..3f2a4f46298 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/in/fssa/tanf/resources/in_tanf_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/in/fssa/tanf/resources/in_tanf_resources_eligible.yaml @@ -58,3 +58,28 @@ output: in_tanf_resources_eligible: true # 0 <= 1_000 = true + +# 650.46 + 299.64 + 49.90 = 1,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 1,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 650.46 + stock_assets: 299.64 + bond_assets: 49.90 + state_code: IN + output: + in_tanf_resources_eligible: true + +# 650.46 + 299.64 + 49.91 = 1,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 650.46 + stock_assets: 299.64 + bond_assets: 49.91 + state_code: IN + output: + in_tanf_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ks/dcf/tanf/ks_tanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ks/dcf/tanf/ks_tanf_resources_eligible.yaml index 6c8dd12e410..3952e174c96 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ks/dcf/tanf/ks_tanf_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ks/dcf/tanf/ks_tanf_resources_eligible.yaml @@ -40,3 +40,28 @@ spm_unit_cash_assets: 10_000 output: ks_tanf_resources_eligible: false + +# 798.28 + 2,187.62 + 14.10 = 3,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 3,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 798.28 + stock_assets: 2_187.62 + bond_assets: 14.10 + state_code: KS + output: + ks_tanf_resources_eligible: true + +# 798.28 + 2,187.62 + 14.11 = 3,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 798.28 + stock_assets: 2_187.62 + bond_assets: 14.11 + state_code: KS + output: + ks_tanf_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ky/dcbs/ktap/ky_ktap_resource_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ky/dcbs/ktap/ky_ktap_resource_eligible.yaml index fd1658a596d..c0e3229af88 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ky/dcbs/ktap/ky_ktap_resource_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ky/dcbs/ktap/ky_ktap_resource_eligible.yaml @@ -102,3 +102,28 @@ output: # Not in Kentucky ky_ktap_resource_eligible: false + +# 8,688.73 + 9.74 + 1,301.53 = 10,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 10,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 8_688.73 + stock_assets: 9.74 + bond_assets: 1_301.53 + state_code: KY + output: + ky_ktap_resource_eligible: true + +# 8,688.73 + 9.74 + 1,301.54 = 10,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 8_688.73 + stock_assets: 9.74 + bond_assets: 1_301.54 + state_code: KY + output: + ky_ktap_resource_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/me/dhhs/tanf/me_tanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/me/dhhs/tanf/me_tanf_resources_eligible.yaml index 3024313a3a5..3574d25a15a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/me/dhhs/tanf/me_tanf_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/me/dhhs/tanf/me_tanf_resources_eligible.yaml @@ -27,3 +27,28 @@ spm_unit_cash_assets: 0 output: me_tanf_resources_eligible: true + +# 8,688.73 + 9.74 + 1,301.53 = 10,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 10,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 8_688.73 + stock_assets: 9.74 + bond_assets: 1_301.53 + state_code: ME + output: + me_tanf_resources_eligible: true + +# 8,688.73 + 9.74 + 1,301.54 = 10,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 8_688.73 + stock_assets: 9.74 + bond_assets: 1_301.54 + state_code: ME + output: + me_tanf_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/mi/mdhhs/fip/eligibility/mi_fip_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/mi/mdhhs/fip/eligibility/mi_fip_resources_eligible.yaml index 9c15f884ddd..500406f49c5 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mi/mdhhs/fip/eligibility/mi_fip_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mi/mdhhs/fip/eligibility/mi_fip_resources_eligible.yaml @@ -45,3 +45,28 @@ spm_unit_cash_assets: 10_000 output: mi_fip_resources_eligible: 0 + +# 8,612.82 + 6,310.95 + 76.23 = 15,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 15,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 8_612.82 + stock_assets: 6_310.95 + bond_assets: 76.23 + state_code: MI + output: + mi_fip_resources_eligible: true + +# 8,612.82 + 6,310.95 + 76.24 = 15,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 8_612.82 + stock_assets: 6_310.95 + bond_assets: 76.24 + state_code: MI + output: + mi_fip_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/mn/dcyf/mfip/mn_mfip_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/mn/dcyf/mfip/mn_mfip_resources_eligible.yaml index 10a5e6fc792..301006eec8e 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mn/dcyf/mfip/mn_mfip_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mn/dcyf/mfip/mn_mfip_resources_eligible.yaml @@ -82,3 +82,28 @@ # Limit: $10,000 # Eligible: 10,001 <= 10,000 = false mn_mfip_resources_eligible: false + +# 8,688.73 + 9.74 + 1,301.53 = 10,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 10,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 8_688.73 + stock_assets: 9.74 + bond_assets: 1_301.53 + state_code: MN + output: + mn_mfip_resources_eligible: true + +# 8,688.73 + 9.74 + 1,301.54 = 10,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 8_688.73 + stock_assets: 9.74 + bond_assets: 1_301.54 + state_code: MN + output: + mn_mfip_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/mn/dhs/msa/eligibility/mn_msa_resource_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/mn/dhs/msa/eligibility/mn_msa_resource_eligible.yaml index e51576f874a..ddc7e1bec8c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mn/dhs/msa/eligibility/mn_msa_resource_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mn/dhs/msa/eligibility/mn_msa_resource_eligible.yaml @@ -332,3 +332,32 @@ state_code: MN output: mn_msa_resource_eligible: true + +# 8,688.73 + 9.74 + 1,301.53 = 10,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 10,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 70 + is_ssi_aged_blind_disabled: true + is_ssi_disabled: true + bank_account_assets: 8_688.73 + stock_assets: 9.74 + bond_assets: 1_301.53 + state_code: MN + output: + mn_msa_resource_eligible: true + +# 8,688.73 + 9.74 + 1,301.54 = 10,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 70 + is_ssi_aged_blind_disabled: true + is_ssi_disabled: true + bank_account_assets: 8_688.73 + stock_assets: 9.74 + bond_assets: 1_301.54 + state_code: MN + output: + mn_msa_resource_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_resources_eligible.yaml index 6abfee83c58..50616a884c9 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_resources_eligible.yaml @@ -21,3 +21,28 @@ spm_unit_cash_assets: 1_001 output: mo_tanf_resources_eligible: false + +# 650.46 + 299.64 + 49.90 = 1,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 1,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 650.46 + stock_assets: 299.64 + bond_assets: 49.90 + state_code: MO + output: + mo_tanf_resources_eligible: true + +# 650.46 + 299.64 + 49.91 = 1,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 650.46 + stock_assets: 299.64 + bond_assets: 49.91 + state_code: MO + output: + mo_tanf_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ms/dhs/tanf/ms_tanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ms/dhs/tanf/ms_tanf_resources_eligible.yaml index f6c851d2cae..33e366dd5bb 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ms/dhs/tanf/ms_tanf_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ms/dhs/tanf/ms_tanf_resources_eligible.yaml @@ -64,3 +64,28 @@ output: # Not in MS, returns false (defined_for = MS) ms_tanf_resources_eligible: false + +# 1,157.81 + 285.17 + 557.02 = 2,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 2,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.02 + state_code: MS + output: + ms_tanf_resources_eligible: true + +# 1,157.81 + 285.17 + 557.03 = 2,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.03 + state_code: MS + output: + ms_tanf_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ms/dom/hmw/eligibility/ms_hmw_resource_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ms/dom/hmw/eligibility/ms_hmw_resource_eligible.yaml index 6bcc9a817e5..d1d0a3b36f5 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ms/dom/hmw/eligibility/ms_hmw_resource_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ms/dom/hmw/eligibility/ms_hmw_resource_eligible.yaml @@ -109,3 +109,32 @@ members: [person2] output: ms_hmw_resource_eligible: [true, true] + +# 1,843.69 + 2,145.15 + 11.16 = 4,000.00, which the limit excludes. +# The float32 balances add to a fraction of a cent below 4,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit fail + period: 2025 + input: + age: 70 + is_ssi_aged_blind_disabled: true + is_ssi_disabled: true + bank_account_assets: 1_843.69 + stock_assets: 2_145.15 + bond_assets: 11.16 + state_code: MS + output: + ms_hmw_resource_eligible: false + +# 1,843.69 + 2,145.15 + 11.15 = 3,999.99, a cent under the limit. +- name: Three accounts a cent under the resource limit pass + period: 2025 + input: + age: 70 + is_ssi_aged_blind_disabled: true + is_ssi_disabled: true + bank_account_assets: 1_843.69 + stock_assets: 2_145.15 + bond_assets: 11.15 + state_code: MS + output: + ms_hmw_resource_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/dhs/tanf/eligibility/mt_tanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/dhs/tanf/eligibility/mt_tanf_resources_eligible.yaml index f883908a09f..8413dc1b1c0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/dhs/tanf/eligibility/mt_tanf_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/dhs/tanf/eligibility/mt_tanf_resources_eligible.yaml @@ -21,3 +21,28 @@ bank_account_assets: 0 output: mt_tanf_resources_eligible: true + +# 798.28 + 2,187.62 + 14.10 = 3,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 3,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 798.28 + stock_assets: 2_187.62 + bond_assets: 14.10 + state_code: MT + output: + mt_tanf_resources_eligible: true + +# 798.28 + 2,187.62 + 14.11 = 3,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 798.28 + stock_assets: 2_187.62 + bond_assets: 14.11 + state_code: MT + output: + mt_tanf_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf_resources_eligible.yaml index 08bf4c5395d..8ed8999c375 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf_resources_eligible.yaml @@ -237,3 +237,28 @@ # Limit: $6,000 + $25 * (10-2) = $6,000 + $200 = $6,200 # $6,200 <= $6,200 = true nd_tanf_resources_eligible: true + +# 798.28 + 2,187.62 + 14.10 = 3,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 3,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 798.28 + stock_assets: 2_187.62 + bond_assets: 14.10 + state_code: ND + output: + nd_tanf_resources_eligible: true + +# 798.28 + 2,187.62 + 14.11 = 3,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 798.28 + stock_assets: 2_187.62 + bond_assets: 14.11 + state_code: ND + output: + nd_tanf_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ne/dhhs/adc/ne_adc_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ne/dhhs/adc/ne_adc_resources_eligible.yaml index 596d450fe45..28a326e8432 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ne/dhhs/adc/ne_adc_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ne/dhhs/adc/ne_adc_resources_eligible.yaml @@ -84,3 +84,28 @@ spm_unit_size: 2 ne_adc_resources_eligible: false # Resources ($6,001) > Limit ($6,000) -> false + +# 2,286.12 + 404.84 + 1,309.04 = 4,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 4,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025 + input: + age: 30 + bank_account_assets: 2_286.12 + stock_assets: 404.84 + bond_assets: 1_309.04 + state_code: NE + output: + ne_adc_resources_eligible: true + +# 2,286.12 + 404.84 + 1,309.05 = 4,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025 + input: + age: 30 + bank_account_assets: 2_286.12 + stock_assets: 404.84 + bond_assets: 1_309.05 + state_code: NE + output: + ne_adc_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/fanf/nh_fanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/fanf/nh_fanf_resources_eligible.yaml index a64ded75848..8b2eb46310e 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/fanf/nh_fanf_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/fanf/nh_fanf_resources_eligible.yaml @@ -118,3 +118,28 @@ output: # $1,999 < $2,000 limit = eligible nh_fanf_resources_eligible: true + +# 719.22 + 208.18 + 72.60 = 1,000.00, which the limit excludes. +# The float32 balances add to a fraction of a cent below 1,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit fail + period: 2025 + input: + age: 30 + bank_account_assets: 719.22 + stock_assets: 208.18 + bond_assets: 72.60 + state_code: NH + output: + nh_fanf_resources_eligible: false + +# 719.22 + 208.18 + 72.59 = 999.99, a cent under the limit. +- name: Three accounts a cent under the resource limit pass + period: 2025 + input: + age: 30 + bank_account_assets: 719.22 + stock_assets: 208.18 + bond_assets: 72.59 + state_code: NH + output: + nh_fanf_resources_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/ssp/nh_ssp_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/ssp/nh_ssp_eligible.yaml new file mode 100644 index 00000000000..894d144a8c8 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nh/dhhs/ssp/nh_ssp_eligible.yaml @@ -0,0 +1,28 @@ +# 285.92 + 1,181.93 + 32.15 = 1,500.00, at the limit. +# The float32 balances add to a fraction of a cent above 1,500 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 70 + is_ssi_aged_blind_disabled: true + is_ssi_disabled: true + bank_account_assets: 285.92 + stock_assets: 1_181.93 + bond_assets: 32.15 + state_code: NH + output: + nh_ssp_eligible: true + +# 285.92 + 1,181.93 + 32.16 = 1,500.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 70 + is_ssi_aged_blind_disabled: true + is_ssi_disabled: true + bank_account_assets: 285.92 + stock_assets: 1_181.93 + bond_assets: 32.16 + state_code: NH + output: + nh_ssp_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/nj/njdhs/wfnj/nj_wfnj_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nj/njdhs/wfnj/nj_wfnj_resources_eligible.yaml index 6fc296c5bdf..992dcf63659 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nj/njdhs/wfnj/nj_wfnj_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nj/njdhs/wfnj/nj_wfnj_resources_eligible.yaml @@ -21,3 +21,28 @@ spm_unit_cash_assets: 0 output: nj_wfnj_resources_eligible: true + +# 1,157.81 + 285.17 + 557.02 = 2,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 2,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.02 + state_code: NJ + output: + nj_wfnj_resources_eligible: true + +# 1,157.81 + 285.17 + 557.03 = 2,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.03 + state_code: NJ + output: + nj_wfnj_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/nm_works/eligibility/nm_works_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/nm_works/eligibility/nm_works_resources_eligible.yaml index 13fb0f6fbcd..fcee32c4d3b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/nm_works/eligibility/nm_works_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/nm_works/eligibility/nm_works_resources_eligible.yaml @@ -36,3 +36,28 @@ # Liquid resources alone can fail the NM Works test even before adding any # future modeled non-liquid countable resources. nm_works_resources_eligible: false + +# 285.92 + 1,181.93 + 32.15 = 1,500.00, at the limit. +# The float32 balances add to a fraction of a cent above 1,500 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 285.92 + stock_assets: 1_181.93 + bond_assets: 32.15 + state_code: NM + output: + nm_works_resources_eligible: true + +# 285.92 + 1,181.93 + 32.16 = 1,500.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 285.92 + stock_assets: 1_181.93 + bond_assets: 32.16 + state_code: NM + output: + nm_works_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/nv/dwss/tanf/eligibility/nv_tanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nv/dwss/tanf/eligibility/nv_tanf_resources_eligible.yaml index 9f571c0547d..a8b4ad3b553 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nv/dwss/tanf/eligibility/nv_tanf_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nv/dwss/tanf/eligibility/nv_tanf_resources_eligible.yaml @@ -24,3 +24,28 @@ spm_unit_cash_assets: 10_001 output: nv_tanf_resources_eligible: false + +# 8,688.73 + 9.74 + 1,301.53 = 10,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 10,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 8_688.73 + stock_assets: 9.74 + bond_assets: 1_301.53 + state_code: NV + output: + nv_tanf_resources_eligible: true + +# 8,688.73 + 9.74 + 1,301.54 = 10,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 8_688.73 + stock_assets: 9.74 + bond_assets: 1_301.54 + state_code: NV + output: + nv_tanf_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/otda/tanf/ny_tanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/otda/tanf/ny_tanf_resources_eligible.yaml index 5fdaef1d68a..19757c115f5 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ny/otda/tanf/ny_tanf_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/otda/tanf/ny_tanf_resources_eligible.yaml @@ -171,3 +171,28 @@ # Pre-reform: disability did NOT qualify for higher limit # Uses $2,000 limit, $2,500 > $2,000 → ineligible ny_tanf_resources_eligible: false + +# 2,105.87 + 384.60 + 9.53 = 2,500.00, at the limit. +# The float32 balances add to a fraction of a cent above 2,500 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 2_105.87 + stock_assets: 384.60 + bond_assets: 9.53 + state_code: NY + output: + ny_tanf_resources_eligible: true + +# 2,105.87 + 384.60 + 9.54 = 2,500.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 2_105.87 + stock_assets: 384.60 + bond_assets: 9.54 + state_code: NY + output: + ny_tanf_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ok/dhs/tanf/ok_tanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ok/dhs/tanf/ok_tanf_resources_eligible.yaml index 38583a1f602..cac5d84846c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ok/dhs/tanf/ok_tanf_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ok/dhs/tanf/ok_tanf_resources_eligible.yaml @@ -77,3 +77,28 @@ state_code: OK output: ok_tanf_resources_eligible: true + +# 650.46 + 299.64 + 49.90 = 1,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 1,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 650.46 + stock_assets: 299.64 + bond_assets: 49.90 + state_code: OK + output: + ok_tanf_resources_eligible: true + +# 650.46 + 299.64 + 49.91 = 1,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 650.46 + stock_assets: 299.64 + bond_assets: 49.91 + state_code: OK + output: + ok_tanf_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/or/odhs/tanf/or_tanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/or/odhs/tanf/or_tanf_resources_eligible.yaml index 1186d1002bf..98ca066c457 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/or/odhs/tanf/or_tanf_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/or/odhs/tanf/or_tanf_resources_eligible.yaml @@ -35,3 +35,28 @@ spm_unit_cash_assets: 10_001 output: or_tanf_resources_eligible: false + +# 8,688.73 + 9.74 + 1,301.53 = 10,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 10,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 8_688.73 + stock_assets: 9.74 + bond_assets: 1_301.53 + state_code: OR + output: + or_tanf_resources_eligible: true + +# 8,688.73 + 9.74 + 1,301.54 = 10,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 8_688.73 + stock_assets: 9.74 + bond_assets: 1_301.54 + state_code: OR + output: + or_tanf_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/pa/dhs/tanf/eligibility/pa_tanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/pa/dhs/tanf/eligibility/pa_tanf_resources_eligible.yaml index a959a2e9858..db07aa1209f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/pa/dhs/tanf/eligibility/pa_tanf_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/pa/dhs/tanf/eligibility/pa_tanf_resources_eligible.yaml @@ -45,3 +45,28 @@ state_code: PA output: pa_tanf_resources_eligible: false + +# 650.46 + 299.64 + 49.90 = 1,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 1,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 650.46 + stock_assets: 299.64 + bond_assets: 49.90 + state_code: PA + output: + pa_tanf_resources_eligible: true + +# 650.46 + 299.64 + 49.91 = 1,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 650.46 + stock_assets: 299.64 + bond_assets: 49.91 + state_code: PA + output: + pa_tanf_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ri/dhs/works/ri_works_resource_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ri/dhs/works/ri_works_resource_eligible.yaml index 6c572074ff6..ba661907dce 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ri/dhs/works/ri_works_resource_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ri/dhs/works/ri_works_resource_eligible.yaml @@ -64,3 +64,28 @@ # Limit: $5,000 # $4,999 <= $5,000 = true ri_works_resource_eligible: true + +# 4,211.74 + 769.20 + 19.06 = 5,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 5,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 4_211.74 + stock_assets: 769.20 + bond_assets: 19.06 + state_code: RI + output: + ri_works_resource_eligible: true + +# 4,211.74 + 769.20 + 19.07 = 5,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 4_211.74 + stock_assets: 769.20 + bond_assets: 19.07 + state_code: RI + output: + ri_works_resource_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_resources_eligible.yaml index ef036df8b90..09d3821e098 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_resources_eligible.yaml @@ -34,3 +34,28 @@ output: sc_tanf_resources_eligible: true # 0 <= 2,500 -> eligible + +# 2,105.87 + 384.60 + 9.53 = 2,500.00, at the limit. +# The float32 balances add to a fraction of a cent above 2,500 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 2_105.87 + stock_assets: 384.60 + bond_assets: 9.53 + state_code: SC + output: + sc_tanf_resources_eligible: true + +# 2,105.87 + 384.60 + 9.54 = 2,500.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 2_105.87 + stock_assets: 384.60 + bond_assets: 9.54 + state_code: SC + output: + sc_tanf_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/tanf/eligibility/sd_tanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/tanf/eligibility/sd_tanf_resources_eligible.yaml index e26e7dedeb8..dfdcecd592d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/tanf/eligibility/sd_tanf_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/tanf/eligibility/sd_tanf_resources_eligible.yaml @@ -102,3 +102,28 @@ # Limit: $2,000 # Eligible: $1,999 <= $2,000 = true sd_tanf_resources_eligible: true + +# 1,157.81 + 285.17 + 557.02 = 2,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 2,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.02 + state_code: SD + output: + sd_tanf_resources_eligible: true + +# 1,157.81 + 285.17 + 557.03 = 2,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.03 + state_code: SD + output: + sd_tanf_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/tn/dhs/ff/eligibility/tn_ff_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/tn/dhs/ff/eligibility/tn_ff_resources_eligible.yaml index a8bd6fe0df9..77d310f3adc 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/tn/dhs/ff/eligibility/tn_ff_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/tn/dhs/ff/eligibility/tn_ff_resources_eligible.yaml @@ -114,3 +114,28 @@ output: # Countable: $3,000 liquid assets + ($6,600 - $4,600 vehicle exclusion) = $5,000 tn_ff_resources_eligible: false + +# 1,157.81 + 285.17 + 557.02 = 2,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 2,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.02 + state_code: TN + output: + tn_ff_resources_eligible: true + +# 1,157.81 + 285.17 + 557.03 = 2,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.03 + state_code: TN + output: + tn_ff_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/tx/tanf/eligibility/tx_tanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/tx/tanf/eligibility/tx_tanf_resources_eligible.yaml index c3a2135753c..8b11f01a5f2 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/tx/tanf/eligibility/tx_tanf_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/tx/tanf/eligibility/tx_tanf_resources_eligible.yaml @@ -42,3 +42,28 @@ output: tx_tanf_resources_eligible: true # Resources (0) <= limit (1,000) → Pass + +# 650.46 + 299.64 + 49.90 = 1,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 1,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 650.46 + stock_assets: 299.64 + bond_assets: 49.90 + state_code: TX + output: + tx_tanf_resources_eligible: true + +# 650.46 + 299.64 + 49.91 = 1,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 650.46 + stock_assets: 299.64 + bond_assets: 49.91 + state_code: TX + output: + tx_tanf_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ut/dwf/fep/eligibility/ut_fep_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ut/dwf/fep/eligibility/ut_fep_resources_eligible.yaml index f8905ef04be..746a2074200 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ut/dwf/fep/eligibility/ut_fep_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ut/dwf/fep/eligibility/ut_fep_resources_eligible.yaml @@ -45,3 +45,28 @@ output: # Resources = 5,000 > 2,000 - ineligible ut_fep_resources_eligible: false + +# 1,157.81 + 285.17 + 557.02 = 2,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 2,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.02 + state_code: UT + output: + ut_fep_resources_eligible: true + +# 1,157.81 + 285.17 + 557.03 = 2,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.03 + state_code: UT + output: + ut_fep_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/dcf/reach_up/vt_reach_up_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/vt/dcf/reach_up/vt_reach_up_resources_eligible.yaml index 1072da1660a..3e503e86178 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/vt/dcf/reach_up/vt_reach_up_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/dcf/reach_up/vt_reach_up_resources_eligible.yaml @@ -73,3 +73,28 @@ output: # Assets: $9,001 > $9,000 limit vt_reach_up_resources_eligible: false + +# 617.28 + 8,353.98 + 28.74 = 9,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 9,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 617.28 + stock_assets: 8_353.98 + bond_assets: 28.74 + state_code: VT + output: + vt_reach_up_resources_eligible: true + +# 617.28 + 8,353.98 + 28.75 = 9,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 617.28 + stock_assets: 8_353.98 + bond_assets: 28.75 + state_code: VT + output: + vt_reach_up_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/wa/dshs/tanf/wa_tanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/wa/dshs/tanf/wa_tanf_resources_eligible.yaml index ee925dfa47f..c14319af143 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wa/dshs/tanf/wa_tanf_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wa/dshs/tanf/wa_tanf_resources_eligible.yaml @@ -50,3 +50,28 @@ bank_account_assets: 1_001 output: wa_tanf_resources_eligible: false + +# 8,978.98 + 2,564.81 + 456.21 = 12,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 12,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025 + input: + age: 30 + bank_account_assets: 8_978.98 + stock_assets: 2_564.81 + bond_assets: 456.21 + state_code: WA + output: + wa_tanf_resources_eligible: true + +# 8,978.98 + 2,564.81 + 456.22 = 12,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025 + input: + age: 30 + bank_account_assets: 8_978.98 + stock_assets: 2_564.81 + bond_assets: 456.22 + state_code: WA + output: + wa_tanf_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/works/wi_works_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/works/wi_works_resources_eligible.yaml index 62907a62354..ee465405121 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/works/wi_works_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/works/wi_works_resources_eligible.yaml @@ -36,3 +36,28 @@ state_code: CA output: wi_works_resources_eligible: false + +# 2,105.87 + 384.60 + 9.53 = 2,500.00, at the limit. +# The float32 balances add to a fraction of a cent above 2,500 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025 + input: + age: 30 + bank_account_assets: 2_105.87 + stock_assets: 384.60 + bond_assets: 9.53 + state_code: WI + output: + wi_works_resources_eligible: true + +# 2,105.87 + 384.60 + 9.54 = 2,500.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025 + input: + age: 30 + bank_account_assets: 2_105.87 + stock_assets: 384.60 + bond_assets: 9.54 + state_code: WI + output: + wi_works_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/wv/dhhr/works/wv_works_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/wv/dhhr/works/wv_works_resources_eligible.yaml index 64cb29352e1..16e4e33fab5 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wv/dhhr/works/wv_works_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wv/dhhr/works/wv_works_resources_eligible.yaml @@ -50,3 +50,28 @@ spm_unit_cash_assets: 10_000 output: wv_works_resources_eligible: false + +# 1,157.81 + 285.17 + 557.02 = 2,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 2,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.02 + state_code: WV + output: + wv_works_resources_eligible: true + +# 1,157.81 + 285.17 + 557.03 = 2,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.03 + state_code: WV + output: + wv_works_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/wy/dfs/power/wy_power_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/wy/dfs/power/wy_power_resources_eligible.yaml index e1aeccfbe8d..0f12cd002d9 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wy/dfs/power/wy_power_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wy/dfs/power/wy_power_resources_eligible.yaml @@ -34,3 +34,28 @@ spm_unit_cash_assets: 5_001 output: wy_power_resources_eligible: false + +# 4,211.74 + 769.20 + 19.06 = 5,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 5,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 4_211.74 + stock_assets: 769.20 + bond_assets: 19.06 + state_code: WY + output: + wy_power_resources_eligible: true + +# 4,211.74 + 769.20 + 19.07 = 5,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 4_211.74 + stock_assets: 769.20 + bond_assets: 19.07 + state_code: WY + output: + wy_power_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/usda/snap/eligibility/meets_snap_asset_test.yaml b/policyengine_us/tests/policy/baseline/gov/usda/snap/eligibility/meets_snap_asset_test.yaml index 58a3ceb5d15..2367a19f859 100644 --- a/policyengine_us/tests/policy/baseline/gov/usda/snap/eligibility/meets_snap_asset_test.yaml +++ b/policyengine_us/tests/policy/baseline/gov/usda/snap/eligibility/meets_snap_asset_test.yaml @@ -78,3 +78,26 @@ # $4,000 is between the $3,000 standard and $4,500 elderly/disabled # limits, and the eligible elderly member confers the higher limit. meets_snap_asset_test: true + +# 7 U.S.C. 2014(g)(1): 2,607.87 + 29.87 + 362.26 = 3,000.00, at the FY2025 +# standard limit. The float32 balances add to 3,000.0002 unless rounded. +- name: Three accounts that total exactly the standard limit pass + period: 2025-01 + input: + age: 30 + bank_account_assets: 2_607.87 + stock_assets: 29.87 + bond_assets: 362.26 + output: + meets_snap_asset_test: true + +# 2,607.87 + 29.87 + 362.27 = 3,000.01, a cent over the limit. +- name: Three accounts a cent over the standard limit fail + period: 2025-01 + input: + age: 30 + bank_account_assets: 2_607.87 + stock_assets: 29.87 + bond_assets: 362.27 + output: + meets_snap_asset_test: false diff --git a/policyengine_us/tests/unit/test_round_to_cents_properties.py b/policyengine_us/tests/unit/test_round_to_cents_properties.py new file mode 100644 index 00000000000..415fd6d7fb5 --- /dev/null +++ b/policyengine_us/tests/unit/test_round_to_cents_properties.py @@ -0,0 +1,121 @@ +"""Hypothesis checks that splitting a total into cents doesn't change a limit test. + +Account balances are stored as float32, so three balances that add to a limit +in cents can sum a fraction of a cent above or below it. Asset tests round the +sum to cents before comparing it with the limit (``round_to_cents``). + +The first property checks the helper alone, without a model, on both ways +the model sums inputs: float32 addition within one entity, and a float64 sum +cast back to float32 when person inputs are added up to a group entity. + +The second property runs the model. Each example is one situation of +single-person households. Each household splits a total at, a cent below or +a cent above the SSI and SNAP limits among bank, stock and bond assets, and +its result must match the result for the same total held in one account. +Hypothesis is a dev extra, so this module skips without it. +""" + +import numpy as np +import pytest + +from policyengine_us import Simulation +from policyengine_us.tools.general import round_to_cents + +hypothesis = pytest.importorskip("hypothesis") +st = pytest.importorskip("hypothesis.strategies") + +ACCOUNTS = ("bank_account_assets", "stock_assets", "bond_assets") + +# Three float32 parts and two float32 additions each err by at most half a +# unit in the last place of the total. Below 2**14 dollars that unit is at +# most 2**-10, so the sum stays within 5 / 2**11 < 0.005 of the total. +MAX_CENTS = 2**14 * 100 - 1 + + +@st.composite +def cent_split(draw, total_cents): + """Three whole-cent parts that add to total_cents.""" + cuts = sorted(draw(st.integers(0, total_cents)) for _ in range(2)) + return (cuts[0], cuts[1] - cuts[0], total_cents - cuts[1]) + + +@st.composite +def total_and_split(draw): + total = draw(st.integers(0, MAX_CENTS)) + return total, draw(cent_split(total)) + + +@hypothesis.settings(max_examples=2_000, deadline=None) +@hypothesis.given(total_and_split()) +def test_rounded_sum_of_float32_parts_is_the_total(case): + total, parts = case + values = [np.float32(part / 100) for part in parts] + # Float32 addition, as a formula adding one entity's own inputs. + float32_sum = values[0] + values[1] + values[2] + # A float64 sum stored as float32, as an aggregate to a group entity. + float64_sum = np.float32(sum(float(value) for value in values)) + for amount in (float32_sum, float64_sum): + assert round_to_cents(amount) == total / 100 + + +YEAR = 2026 +# 20 CFR 416.1205: the individual limit is 2,000. Everyone is 70, so SNAP +# applies its FY2026 limit for households with an elderly member, 4,500 +# (7 U.S.C. 2014(g)(1)). +LIMITS = {"meets_ssi_resource_test": 2_000, "meets_snap_asset_test": 4_500} + + +@st.composite +def household(draw): + test = draw(st.sampled_from(sorted(LIMITS))) + total = LIMITS[test] * 100 + draw(st.sampled_from([-1, 0, 1])) + return test, draw(cent_split(total)) + + +def build_situation(households): + people = {} + groups = {} + for i, (_, parts) in enumerate(households): + for split in (False, True): + name = f"h{i}_{'split' if split else 'whole'}" + amounts = parts if split else (sum(parts), 0, 0) + person = { + "age": {YEAR: 70}, + "is_ssi_aged_blind_disabled": {YEAR: True}, + } + for account, cents in zip(ACCOUNTS, amounts): + person[account] = {YEAR: cents / 100} + people[name] = person + groups[name] = {"members": [name]} + return { + "people": people, + **{ + entity: groups + for entity in ( + "tax_units", + "spm_units", + "families", + "marital_units", + "households", + ) + }, + } + + +@hypothesis.settings( + max_examples=10, + deadline=None, + suppress_health_check=[ + hypothesis.HealthCheck.too_slow, + hypothesis.HealthCheck.data_too_large, + ], +) +@hypothesis.given(st.lists(household(), min_size=1, max_size=20)) +def test_split_accounts_match_one_account(households): + simulation = Simulation(situation=build_situation(households)) + period = f"{YEAR}-01" + results = {test: np.asarray(simulation.calculate(test, period)) for test in LIMITS} + # People alternate whole, split, so each person's group holds only them. + for i, (test, parts) in enumerate(households): + whole, split = results[test][2 * i], results[test][2 * i + 1] + assert split == whole, (test, parts) diff --git a/policyengine_us/tools/general.py b/policyengine_us/tools/general.py index 7afc7860fd7..5c031eb37ec 100644 --- a/policyengine_us/tools/general.py +++ b/policyengine_us/tools/general.py @@ -14,6 +14,20 @@ USD = "currency-USD" +def round_to_cents(amount): + """Round a dollar amount to whole cents, as a float64 array. + + Inputs are stored as float32, which holds most cent amounts only + approximately. A sum of several of them can land a fraction of a cent + above or below the whole-cent total they represent: three balances + entered as 178.57, 1,547.79 and 273.64 add to 2,000.0001 in float32. + Rounding the sum before an exact-limit comparison compares the total the + inputs represent, so a split of a total among accounts or people passes + or fails the limit as the total does. + """ + return np.round(np.asarray(amount, dtype=np.float64), 2) + + def has_input_for_period(simulation, variable, period): """Whether the visible array for this exact period was set as an input. diff --git a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/is_optional_senior_or_disabled_asset_eligible.py b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/is_optional_senior_or_disabled_asset_eligible.py index b4745bda5ad..c09be4d4c19 100644 --- a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/is_optional_senior_or_disabled_asset_eligible.py +++ b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/is_optional_senior_or_disabled_asset_eligible.py @@ -19,7 +19,7 @@ def formula(person, period, parameters): # the individual or the married couple, not the tax filing unit. personal_assets = person("ssi_countable_resources", period) # $ # Inputs are stored as float32, so compare whole cents with the limit. - assets = np.round(person.marital_unit.sum(personal_assets), 2) + assets = round_to_cents(person.marital_unit.sum(personal_assets)) is_couple = person.marital_unit.nb_persons() == 2 state = person.household("state_code_str", period) diff --git a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.py b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.py index 633c16213f9..54fa24bdece 100644 --- a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.py +++ b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.py @@ -57,7 +57,8 @@ def formula(person, period, parameters): income = marital_unit.sum( personal_income - medical_expenses - parental_expense_share ) - assets = marital_unit.sum(personal_assets) + # Inputs are stored as float32, so compare whole cents with the limit. + assets = round_to_cents(marital_unit.sum(personal_assets)) ma = parameters(period).gov.hhs.medicaid mn = ma.eligibility.categories.medically_needy is_joint = marital_unit.nb_persons() == 2 diff --git a/policyengine_us/variables/gov/hhs/medicare/savings_programs/eligibility/msp_asset_eligible.py b/policyengine_us/variables/gov/hhs/medicare/savings_programs/eligibility/msp_asset_eligible.py index 98455cb905b..c0fa8859dd2 100644 --- a/policyengine_us/variables/gov/hhs/medicare/savings_programs/eligibility/msp_asset_eligible.py +++ b/policyengine_us/variables/gov/hhs/medicare/savings_programs/eligibility/msp_asset_eligible.py @@ -24,10 +24,13 @@ def formula(person, period, parameters): # If asset test doesn't apply, everyone is asset-eligible personal_resources = person("ssi_countable_resources", period.this_year) married = person.spm_unit("spm_unit_is_married", period.this_year) - countable_resources = where( - married, - person.marital_unit.sum(personal_resources), - personal_resources, + # Inputs are stored as float32, so compare whole cents with the limit. + countable_resources = round_to_cents( + where( + married, + person.marital_unit.sum(personal_resources), + personal_resources, + ) ) # States may set a higher limit than the federal one, never a lower one. state_limit = p.asset.state_limit diff --git a/policyengine_us/variables/gov/hhs/tanf/non_cash/meets_tanf_non_cash_asset_test.py b/policyengine_us/variables/gov/hhs/tanf/non_cash/meets_tanf_non_cash_asset_test.py index 7cfb1a7e15c..bd326fec4e4 100644 --- a/policyengine_us/variables/gov/hhs/tanf/non_cash/meets_tanf_non_cash_asset_test.py +++ b/policyengine_us/variables/gov/hhs/tanf/non_cash/meets_tanf_non_cash_asset_test.py @@ -24,6 +24,7 @@ def formula(spm_unit, period, parameters): 0, ) tx_vehicle_value = max_(vehicle_value - vehicle_exemption, 0) - assets = assets + where(state == "TX", tx_vehicle_value, 0) + # Inputs are stored as float32, so compare whole cents with the limit. + assets = round_to_cents(assets + where(state == "TX", tx_vehicle_value, 0)) asset_limit = limits.asset_limit[state] return assets <= asset_limit diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.py index 2c0a1f658dd..0b9fe1dec7c 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.py @@ -120,11 +120,12 @@ def formula(person, period, parameters): # 'only to the extent that those resources exceed the resource limits # described in § 416.1205': use the reformable individual/couple limits. allowance = where(parent_count > 1, p.couple, p.individual) - excess = max_(0, parental_resources - allowance) + # Inputs are stored as float32, so compare whole cents with the limits. + excess = max_(0, round_to_cents(parental_resources) - allowance) # Do not discard ABD parents. SI 01330.310 B-C says zero if parent(s) # meets resource eligibility, usual deeming if not. The Ryder example # (.340) has an ABD mother failing the couple limit: 3100-3000=100. - own_resources = person("ssi_countable_resources", period) + own_resources = round_to_cents(person("ssi_countable_resources", period)) active = person("is_ssi_resource_deeming_child", period) & ( own_resources <= p.individual ) @@ -150,7 +151,7 @@ def formula(person, period, parameters): count = np.bincount(pool[active & income_eligible], minlength=pool_count) share = excess / max_(1, count[pool]) deemed = where(active, share, deemed) - remaining = active & (own_resources + share <= p.individual) + remaining = active & (round_to_cents(own_resources + share) <= p.individual) if np.array_equal(active, remaining): break active = remaining diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/meets_ssi_resource_test.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/meets_ssi_resource_test.py index 8a593952788..6c789f8c52d 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/meets_ssi_resource_test.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/meets_ssi_resource_test.py @@ -25,10 +25,13 @@ def formula(person, period, parameters): "ssi_resources_deemed_from_ineligible_parent", ], ) - countable_resources = where( - joint_claim, - person.marital_unit.sum(personal_resources), - personal_resources + deemed_resources, + # Inputs are stored as float32, so compare whole cents with the limit. + countable_resources = round_to_cents( + where( + joint_claim, + person.marital_unit.sum(personal_resources), + personal_resources + deemed_resources, + ) ) # 416.1205(a): an individual living with an ineligible spouse counts # 'including the resources of the spouse' against the spouse limit. diff --git a/policyengine_us/variables/gov/states/ak/dpa/atap/eligibility/ak_atap_resources_eligible.py b/policyengine_us/variables/gov/states/ak/dpa/atap/eligibility/ak_atap_resources_eligible.py index 34b4fd788d9..b352a28ccb2 100644 --- a/policyengine_us/variables/gov/states/ak/dpa/atap/eligibility/ak_atap_resources_eligible.py +++ b/policyengine_us/variables/gov/states/ak/dpa/atap/eligibility/ak_atap_resources_eligible.py @@ -14,7 +14,8 @@ class ak_atap_resources_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.ak.dpa.atap.resource_limit - resources = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + resources = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) # Higher limit if household has elderly member (age 60+) person = spm_unit.members diff --git a/policyengine_us/variables/gov/states/ar/dhs/tea/eligibility/ar_tea_resources_eligible.py b/policyengine_us/variables/gov/states/ar/dhs/tea/eligibility/ar_tea_resources_eligible.py index a9dd2f42819..3d6ecda781d 100644 --- a/policyengine_us/variables/gov/states/ar/dhs/tea/eligibility/ar_tea_resources_eligible.py +++ b/policyengine_us/variables/gov/states/ar/dhs/tea/eligibility/ar_tea_resources_eligible.py @@ -15,5 +15,6 @@ def formula(spm_unit, period, parameters): # Per 208.00.13 Ark. Code R. Section 001, Section 3.4 p = parameters(period).gov.states.ar.dhs.tea.resources # Use federal asset calculation for SPM unit - resources = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + resources = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) return resources <= p.limit diff --git a/policyengine_us/variables/gov/states/az/hhs/tanf/eligibility/az_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/az/hhs/tanf/eligibility/az_tanf_resources_eligible.py index 4cdea6416b5..d7b36d6b804 100644 --- a/policyengine_us/variables/gov/states/az/hhs/tanf/eligibility/az_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/az/hhs/tanf/eligibility/az_tanf_resources_eligible.py @@ -13,5 +13,6 @@ class az_tanf_resources_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.az.hhs.tanf.resources - resources = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + resources = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) return resources <= p.limit diff --git a/policyengine_us/variables/gov/states/ca/cdss/capi/eligibility/ca_capi_resource_eligible.py b/policyengine_us/variables/gov/states/ca/cdss/capi/eligibility/ca_capi_resource_eligible.py index 89ba309b6b7..af80f3980bc 100644 --- a/policyengine_us/variables/gov/states/ca/cdss/capi/eligibility/ca_capi_resource_eligible.py +++ b/policyengine_us/variables/gov/states/ca/cdss/capi/eligibility/ca_capi_resource_eligible.py @@ -10,7 +10,8 @@ class ca_capi_resource_eligible(Variable): reference = "https://www.cdss.ca.gov/Portals/9/CAPI/CAPI_Regulations-Accessible.pdf" def formula(spm_unit, period, parameters): - resources = spm_unit("ca_capi_resources", period) + # Inputs are stored as float32, so compare whole cents with the limit. + resources = round_to_cents(spm_unit("ca_capi_resources", period)) p = parameters(period).gov.states.ca.cdss.capi.resources married = spm_unit("spm_unit_is_married", period) resource_limit = where(married, p.limit.couple, p.limit.single) diff --git a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_eligible.py b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_eligible.py index 5d9e562ce4c..9eb4dbcb247 100644 --- a/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_eligible.py +++ b/policyengine_us/variables/gov/states/co/ssa/oap/co_oap_eligible.py @@ -9,7 +9,8 @@ class co_oap_eligible(Variable): defined_for = StateCode.CO def formula(person, period, parameters): - assets = person("ssi_countable_resources", period) + # Inputs are stored as float32, so compare whole cents with the limit. + assets = round_to_cents(person("ssi_countable_resources", period)) joint_claim = person("ssi_claim_is_joint", period) p = parameters(period).gov.states.co.ssa.oap asset_limit = where(joint_claim, p.resources.couple, p.resources.single) diff --git a/policyengine_us/variables/gov/states/ct/dss/ssp/eligibility/ct_ssp_resource_eligible.py b/policyengine_us/variables/gov/states/ct/dss/ssp/eligibility/ct_ssp_resource_eligible.py index f4e5cf330f0..97ad8dfea5b 100644 --- a/policyengine_us/variables/gov/states/ct/dss/ssp/eligibility/ct_ssp_resource_eligible.py +++ b/policyengine_us/variables/gov/states/ct/dss/ssp/eligibility/ct_ssp_resource_eligible.py @@ -35,4 +35,5 @@ def formula(person, period, parameters): p.asset_limit.couple, p.asset_limit.individual, ) - return resources <= limit + # Inputs are stored as float32, so compare whole cents with the limit. + return round_to_cents(resources) <= limit diff --git a/policyengine_us/variables/gov/states/ct/dss/tfa/eligibility/ct_tfa_resources_eligible.py b/policyengine_us/variables/gov/states/ct/dss/tfa/eligibility/ct_tfa_resources_eligible.py index 5b6de4508de..379336c8115 100644 --- a/policyengine_us/variables/gov/states/ct/dss/tfa/eligibility/ct_tfa_resources_eligible.py +++ b/policyengine_us/variables/gov/states/ct/dss/tfa/eligibility/ct_tfa_resources_eligible.py @@ -13,5 +13,6 @@ class ct_tfa_resources_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.ct.dss.tfa.resource - assets = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + assets = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) return assets <= p.limit diff --git a/policyengine_us/variables/gov/states/dc/dhs/tanf/eligibility/dc_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/dc/dhs/tanf/eligibility/dc_tanf_resources_eligible.py index d69a7d04eaf..37b07a2aa2c 100644 --- a/policyengine_us/variables/gov/states/dc/dhs/tanf/eligibility/dc_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/dc/dhs/tanf/eligibility/dc_tanf_resources_eligible.py @@ -24,5 +24,8 @@ def formula(spm_unit, period, parameters): p.higher.amount, p.lower.amount, ) - countable_resources = spm_unit("dc_tanf_countable_resources", period) + # Inputs are stored as float32, so compare whole cents with the limit. + countable_resources = round_to_cents( + spm_unit("dc_tanf_countable_resources", period) + ) return countable_resources <= resource_limit diff --git a/policyengine_us/variables/gov/states/de/dhss/tanf/eligibility/de_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/de/dhss/tanf/eligibility/de_tanf_resources_eligible.py index 97c1189e3b9..692ba24d48a 100644 --- a/policyengine_us/variables/gov/states/de/dhss/tanf/eligibility/de_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/de/dhss/tanf/eligibility/de_tanf_resources_eligible.py @@ -12,6 +12,7 @@ class de_tanf_resources_eligible(Variable): def formula(spm_unit, period, parameters): # Per DSSM 4002: Resource limit is $10,000 p = parameters(period).gov.states.de.dhss.tanf - resources = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + resources = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) return resources <= p.resource_limit diff --git a/policyengine_us/variables/gov/states/fl/dcf/tanf/eligibility/fl_tca_resources_eligible.py b/policyengine_us/variables/gov/states/fl/dcf/tanf/eligibility/fl_tca_resources_eligible.py index 1be15ff8877..58cbc2d93e7 100644 --- a/policyengine_us/variables/gov/states/fl/dcf/tanf/eligibility/fl_tca_resources_eligible.py +++ b/policyengine_us/variables/gov/states/fl/dcf/tanf/eligibility/fl_tca_resources_eligible.py @@ -15,5 +15,6 @@ class fl_tca_resources_eligible(Variable): def formula(spm_unit, period, parameters): # Per Florida DCF: Countable assets must be <= $2,000 p = parameters(period).gov.states.fl.dcf.tanf.resources - assets = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + assets = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) return assets <= p.asset_limit diff --git a/policyengine_us/variables/gov/states/ga/dfcs/tanf/eligibility/ga_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/ga/dfcs/tanf/eligibility/ga_tanf_resources_eligible.py index a15e7b7049c..5d7e01e7b23 100644 --- a/policyengine_us/variables/gov/states/ga/dfcs/tanf/eligibility/ga_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/ga/dfcs/tanf/eligibility/ga_tanf_resources_eligible.py @@ -18,5 +18,6 @@ def formula(spm_unit, period, parameters): # In a more complete implementation, this would account for # excluded resources (primary residence, household goods, etc.) # and vehicle value limits - cash_assets = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + cash_assets = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) return cash_assets <= p.limit diff --git a/policyengine_us/variables/gov/states/ia/dhs/fip/eligibility/ia_fip_resources_eligible.py b/policyengine_us/variables/gov/states/ia/dhs/fip/eligibility/ia_fip_resources_eligible.py index 5db0c542a5b..fb075e93bb7 100644 --- a/policyengine_us/variables/gov/states/ia/dhs/fip/eligibility/ia_fip_resources_eligible.py +++ b/policyengine_us/variables/gov/states/ia/dhs/fip/eligibility/ia_fip_resources_eligible.py @@ -19,5 +19,6 @@ def formula(spm_unit, period, parameters): p.recipient_limit, p.applicant_limit, ) - resources = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + resources = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) return resources <= limit diff --git a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_eligible.py b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_eligible.py index 9bdb207b59b..c04e6fad062 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_eligible.py +++ b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_eligible.py @@ -44,6 +44,8 @@ def formula(person, period, parameters): ) resource = where(couple, person.marital_unit.sum(resource), resource) resource = resource + person("ia_ssa_resources_deemed_from_parents", period) + # Inputs are stored as float32, so compare whole cents with the limit. + resource = round_to_cents(resource) # 6-B Resources prints "$2,000 for an individual" and "$3,000 for a # married couple living together" under 20 CFR 416.1202; these equal # the SSI limits of 20 CFR 416.1205, so the federal parameters are used diff --git a/policyengine_us/variables/gov/states/id/tafi/eligibility/id_tafi_resources_eligible.py b/policyengine_us/variables/gov/states/id/tafi/eligibility/id_tafi_resources_eligible.py index f348121d4c0..41902b707e1 100644 --- a/policyengine_us/variables/gov/states/id/tafi/eligibility/id_tafi_resources_eligible.py +++ b/policyengine_us/variables/gov/states/id/tafi/eligibility/id_tafi_resources_eligible.py @@ -12,5 +12,6 @@ class id_tafi_resources_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.id.tafi.resources # Per IDAPA 16.03.08.200: Resources must not exceed $5,000 - resources = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + resources = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) return resources <= p.limit diff --git a/policyengine_us/variables/gov/states/in/fssa/tanf/eligibility/in_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/in/fssa/tanf/eligibility/in_tanf_resources_eligible.py index bf033a68739..621ec6ead8e 100644 --- a/policyengine_us/variables/gov/states/in/fssa/tanf/eligibility/in_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/in/fssa/tanf/eligibility/in_tanf_resources_eligible.py @@ -14,7 +14,8 @@ class in_tanf_resources_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states["in"].fssa.tanf.resources.limit - countable = spm_unit("in_tanf_countable_resources", period) + # Inputs are stored as float32, so compare whole cents with the limit. + countable = round_to_cents(spm_unit("in_tanf_countable_resources", period)) is_enrolled = spm_unit("is_tanf_enrolled", period) limit = where( diff --git a/policyengine_us/variables/gov/states/ks/dcf/tanf/eligibility/ks_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/ks/dcf/tanf/eligibility/ks_tanf_resources_eligible.py index 8fcc8858c32..87b2e17eaf5 100644 --- a/policyengine_us/variables/gov/states/ks/dcf/tanf/eligibility/ks_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/ks/dcf/tanf/eligibility/ks_tanf_resources_eligible.py @@ -15,5 +15,6 @@ class ks_tanf_resources_eligible(Variable): def formula(spm_unit, period, parameters): # Per KEESM 5110: Resource limit is $3,000 p = parameters(period).gov.states.ks.dcf.tanf.resource_limit - resources = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + resources = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) return resources <= p.amount diff --git a/policyengine_us/variables/gov/states/ky/dcbs/ktap/eligibility/ky_ktap_resource_eligible.py b/policyengine_us/variables/gov/states/ky/dcbs/ktap/eligibility/ky_ktap_resource_eligible.py index e73597c2678..6dc029862af 100644 --- a/policyengine_us/variables/gov/states/ky/dcbs/ktap/eligibility/ky_ktap_resource_eligible.py +++ b/policyengine_us/variables/gov/states/ky/dcbs/ktap/eligibility/ky_ktap_resource_eligible.py @@ -11,5 +11,6 @@ class ky_ktap_resource_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.ky.dcbs.ktap.eligibility - resources = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + resources = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) return resources <= p.resource_limit diff --git a/policyengine_us/variables/gov/states/me/dhhs/tanf/eligibility/me_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/me/dhhs/tanf/eligibility/me_tanf_resources_eligible.py index 64f0dd1b647..651e4499b58 100644 --- a/policyengine_us/variables/gov/states/me/dhhs/tanf/eligibility/me_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/me/dhhs/tanf/eligibility/me_tanf_resources_eligible.py @@ -17,5 +17,8 @@ def formula(spm_unit, period, parameters): # Resource limit is $10,000 per family # One vehicle per licensed driver is exempt p = parameters(period).gov.states.me.dhhs.tanf - countable_resources = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + countable_resources = round_to_cents( + spm_unit("spm_unit_cash_assets", period.this_year) + ) return countable_resources <= p.resource_limit diff --git a/policyengine_us/variables/gov/states/mi/mdhhs/fip/eligibility/mi_fip_resources_eligible.py b/policyengine_us/variables/gov/states/mi/mdhhs/fip/eligibility/mi_fip_resources_eligible.py index f779f24d077..622cc1efd5a 100644 --- a/policyengine_us/variables/gov/states/mi/mdhhs/fip/eligibility/mi_fip_resources_eligible.py +++ b/policyengine_us/variables/gov/states/mi/mdhhs/fip/eligibility/mi_fip_resources_eligible.py @@ -17,6 +17,7 @@ def formula(spm_unit, period, parameters): # Count liquid financial assets explicitly. BEM 400 excludes several # non-liquid resources that are not yet modeled separately here. - assets = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + assets = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) return assets <= p.resources.limit diff --git a/policyengine_us/variables/gov/states/mn/dcyf/mfip/eligibility/mn_mfip_resources_eligible.py b/policyengine_us/variables/gov/states/mn/dcyf/mfip/eligibility/mn_mfip_resources_eligible.py index df1826a132e..b23c3a1da1a 100644 --- a/policyengine_us/variables/gov/states/mn/dcyf/mfip/eligibility/mn_mfip_resources_eligible.py +++ b/policyengine_us/variables/gov/states/mn/dcyf/mfip/eligibility/mn_mfip_resources_eligible.py @@ -13,5 +13,6 @@ def formula(spm_unit, period, parameters): # Per MN Stat. 256P.02, Subd. 2: # Assets must not exceed $10,000. p = parameters(period).gov.states.mn.dcyf.mfip.resources - resources = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + resources = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) return resources <= p.limit diff --git a/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_resource_eligible.py b/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_resource_eligible.py index dcbab010738..98379d99009 100644 --- a/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_resource_eligible.py +++ b/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_resource_eligible.py @@ -25,5 +25,6 @@ def formula(person, period, parameters): p = parameters(period).gov.states.mn.dhs.msa.eligibility receives_ssi = (person("ssi", period) > 0) | person("receives_ssi", period) countable_resources = person("ssi_countable_resources", period.this_year) - unit_resources = person.marital_unit.sum(countable_resources) + # Inputs are stored as float32, so compare whole cents with the limit. + unit_resources = round_to_cents(person.marital_unit.sum(countable_resources)) return receives_ssi | (unit_resources <= p.asset_limit.non_ssi_track) diff --git a/policyengine_us/variables/gov/states/mo/dss/tanf/eligibility/mo_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/mo/dss/tanf/eligibility/mo_tanf_resources_eligible.py index 29a469b49b0..9c0cee65722 100644 --- a/policyengine_us/variables/gov/states/mo/dss/tanf/eligibility/mo_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/mo/dss/tanf/eligibility/mo_tanf_resources_eligible.py @@ -20,5 +20,6 @@ def formula(spm_unit, period, parameters): # pact"); IEP participation is not observable, so the $1,000 limit # applies to all units. p = parameters(period).gov.states.mo.dss.tanf.resource_limit - resources = spm_unit("mo_tanf_countable_resources", period) + # Inputs are stored as float32, so compare whole cents with the limit. + resources = round_to_cents(spm_unit("mo_tanf_countable_resources", period)) return resources <= p.amount diff --git a/policyengine_us/variables/gov/states/ms/dhs/tanf/eligibility/ms_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/ms/dhs/tanf/eligibility/ms_tanf_resources_eligible.py index 337e0289112..d469c4e8bbb 100644 --- a/policyengine_us/variables/gov/states/ms/dhs/tanf/eligibility/ms_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/ms/dhs/tanf/eligibility/ms_tanf_resources_eligible.py @@ -15,5 +15,8 @@ class ms_tanf_resources_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.ms.dhs.tanf.resources - household_assets = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + household_assets = round_to_cents( + spm_unit("spm_unit_cash_assets", period.this_year) + ) return household_assets <= p.limit diff --git a/policyengine_us/variables/gov/states/ms/dom/hmw/eligibility/ms_hmw_resource_eligible.py b/policyengine_us/variables/gov/states/ms/dom/hmw/eligibility/ms_hmw_resource_eligible.py index 8cf9b04ed93..edb83ae7be6 100644 --- a/policyengine_us/variables/gov/states/ms/dom/hmw/eligibility/ms_hmw_resource_eligible.py +++ b/policyengine_us/variables/gov/states/ms/dom/hmw/eligibility/ms_hmw_resource_eligible.py @@ -23,4 +23,5 @@ def formula(person, period, parameters): personal_resources, ) p = parameters(period).gov.states.ms.dom.hmw.resources.limit - return resources < where(couple, p.couple, p.individual) + # Inputs are stored as float32, so compare whole cents with the limit. + return round_to_cents(resources) < where(couple, p.couple, p.individual) diff --git a/policyengine_us/variables/gov/states/mt/dhs/tanf/eligibility/mt_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/mt/dhs/tanf/eligibility/mt_tanf_resources_eligible.py index d05fd7d9d16..cb4dcd924f6 100644 --- a/policyengine_us/variables/gov/states/mt/dhs/tanf/eligibility/mt_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/mt/dhs/tanf/eligibility/mt_tanf_resources_eligible.py @@ -11,5 +11,8 @@ class mt_tanf_resources_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.mt.dhs.tanf.resource_limit - countable_resources = spm_unit("mt_tanf_countable_resources", period) + # Inputs are stored as float32, so compare whole cents with the limit. + countable_resources = round_to_cents( + spm_unit("mt_tanf_countable_resources", period) + ) return countable_resources <= p.amount diff --git a/policyengine_us/variables/gov/states/nd/dhs/tanf/eligibility/nd_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/nd/dhs/tanf/eligibility/nd_tanf_resources_eligible.py index cba07aa9691..d8630342090 100644 --- a/policyengine_us/variables/gov/states/nd/dhs/tanf/eligibility/nd_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/nd/dhs/tanf/eligibility/nd_tanf_resources_eligible.py @@ -11,7 +11,8 @@ class nd_tanf_resources_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.nd.dhs.tanf.resources.limit - resources = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + resources = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) unit_size = spm_unit("spm_unit_size", period.this_year) capped_size = min_(unit_size, 2) base_limit = p.base[capped_size] diff --git a/policyengine_us/variables/gov/states/ne/dhhs/adc/eligibility/ne_adc_resources_eligible.py b/policyengine_us/variables/gov/states/ne/dhhs/adc/eligibility/ne_adc_resources_eligible.py index d2a5813e246..e57f9d19eca 100644 --- a/policyengine_us/variables/gov/states/ne/dhhs/adc/eligibility/ne_adc_resources_eligible.py +++ b/policyengine_us/variables/gov/states/ne/dhhs/adc/eligibility/ne_adc_resources_eligible.py @@ -15,7 +15,8 @@ class ne_adc_resources_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.ne.dhhs.adc # Count explicitly modeled liquid financial assets. - countable_resources = spm_unit("spm_unit_cash_assets", period) + # Inputs are stored as float32, so compare whole cents with the limit. + countable_resources = round_to_cents(spm_unit("spm_unit_cash_assets", period)) size = spm_unit("spm_unit_size", period) # Resource limits: $4,000 for 1 person, $6,000 for 2+ persons limit = p.resources.limit.calc(size) diff --git a/policyengine_us/variables/gov/states/nh/dhhs/fanf/eligibility/nh_fanf_resources_eligible.py b/policyengine_us/variables/gov/states/nh/dhhs/fanf/eligibility/nh_fanf_resources_eligible.py index 4494d90ee00..b586b9bacea 100644 --- a/policyengine_us/variables/gov/states/nh/dhhs/fanf/eligibility/nh_fanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/nh/dhhs/fanf/eligibility/nh_fanf_resources_eligible.py @@ -19,7 +19,8 @@ def formula(spm_unit, period, parameters): # NOTE: One vehicle per adult is excluded regardless of value. # Resource limits differ for applicants ($1,000) vs recipients ($5,000 as of 07/01/22) p = parameters(period).gov.states.nh.dhhs.fanf.resources - resources = spm_unit("spm_unit_cash_assets", period) + # Inputs are stored as float32, so compare whole cents with the limit. + resources = round_to_cents(spm_unit("spm_unit_cash_assets", period)) # He-W 656.06(a)(1) also treats an applicant who received FANF in any # one of the previous 6 months as a recipient. The inputs have no # 6-month look-back, so current enrollment stands in for it. diff --git a/policyengine_us/variables/gov/states/nh/dhhs/ssp/nh_ssp_eligible.py b/policyengine_us/variables/gov/states/nh/dhhs/ssp/nh_ssp_eligible.py index 4ba8532fe3f..b931c4de3d6 100644 --- a/policyengine_us/variables/gov/states/nh/dhhs/ssp/nh_ssp_eligible.py +++ b/policyengine_us/variables/gov/states/nh/dhhs/ssp/nh_ssp_eligible.py @@ -42,6 +42,8 @@ def formula(person, period, parameters): resources = resources + essential * max_( essential_resources, spouse_value(essential_resources, spouse) ) + # Inputs are stored as float32, so compare whole cents with the limit. + resources = round_to_cents(resources) # He-W 616.02(a): qualified aliens under 8 USC 1641(b) are eligible once # the waiting period since entry has elapsed, which also covers entry # before August 22, 1996. AAM 305.11 exempts the listed statuses, as diff --git a/policyengine_us/variables/gov/states/nj/njdhs/wfnj/eligibility/nj_wfnj_resources_eligible.py b/policyengine_us/variables/gov/states/nj/njdhs/wfnj/eligibility/nj_wfnj_resources_eligible.py index 52729d916fb..0933ef6c5f7 100644 --- a/policyengine_us/variables/gov/states/nj/njdhs/wfnj/eligibility/nj_wfnj_resources_eligible.py +++ b/policyengine_us/variables/gov/states/nj/njdhs/wfnj/eligibility/nj_wfnj_resources_eligible.py @@ -11,5 +11,6 @@ class nj_wfnj_resources_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.nj.njdhs.wfnj.resources - resources = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + resources = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) return resources <= p.limit diff --git a/policyengine_us/variables/gov/states/nm/hca/nm_works/eligibility/nm_works_resources_eligible.py b/policyengine_us/variables/gov/states/nm/hca/nm_works/eligibility/nm_works_resources_eligible.py index ea5843c25d3..e41f1d96fad 100644 --- a/policyengine_us/variables/gov/states/nm/hca/nm_works/eligibility/nm_works_resources_eligible.py +++ b/policyengine_us/variables/gov/states/nm/hca/nm_works/eligibility/nm_works_resources_eligible.py @@ -11,7 +11,10 @@ class nm_works_resources_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.nm.hca.nm_works.resources.limit - liquid_resources = spm_unit("nm_works_countable_liquid_resources", period) + # Inputs are stored as float32, so compare whole cents with the limit. + liquid_resources = round_to_cents( + spm_unit("nm_works_countable_liquid_resources", period) + ) non_liquid_resources = spm_unit( "nm_works_countable_non_liquid_resources", period ) diff --git a/policyengine_us/variables/gov/states/nv/dwss/tanf/eligibility/nv_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/nv/dwss/tanf/eligibility/nv_tanf_resources_eligible.py index aa7c78bfff2..6c6f08ff330 100644 --- a/policyengine_us/variables/gov/states/nv/dwss/tanf/eligibility/nv_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/nv/dwss/tanf/eligibility/nv_tanf_resources_eligible.py @@ -14,5 +14,6 @@ def formula(spm_unit, period, parameters): # Nevada excludes the home and up to two vehicles, so use liquid # financial assets explicitly until those vehicle exclusions are # modeled separately. - assets = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + assets = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) return assets <= p.amount diff --git a/policyengine_us/variables/gov/states/ny/otda/tanf/eligibility/ny_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/ny/otda/tanf/eligibility/ny_tanf_resources_eligible.py index 8a5ee8a8fd0..56c2e4af153 100644 --- a/policyengine_us/variables/gov/states/ny/otda/tanf/eligibility/ny_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/ny/otda/tanf/eligibility/ny_tanf_resources_eligible.py @@ -34,5 +34,6 @@ def formula(spm_unit, period, parameters): p.resource_limit.higher.amount, p.resource_limit.lower.amount, ) - resources = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + resources = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) return resources <= resource_limit diff --git a/policyengine_us/variables/gov/states/ok/dhs/tanf/eligibility/ok_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/ok/dhs/tanf/eligibility/ok_tanf_resources_eligible.py index 5b29e7bdfb2..0952aef98e3 100644 --- a/policyengine_us/variables/gov/states/ok/dhs/tanf/eligibility/ok_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/ok/dhs/tanf/eligibility/ok_tanf_resources_eligible.py @@ -14,5 +14,6 @@ class ok_tanf_resources_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.ok.dhs.tanf.resources # Per OAC 340:10-3-5: Maximum countable reserve is $1,000 - resources = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + resources = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) return resources <= p.limit diff --git a/policyengine_us/variables/gov/states/or/odhs/tanf/eligibility/or_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/or/odhs/tanf/eligibility/or_tanf_resources_eligible.py index 58f51d3a206..be74e18548a 100644 --- a/policyengine_us/variables/gov/states/or/odhs/tanf/eligibility/or_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/or/odhs/tanf/eligibility/or_tanf_resources_eligible.py @@ -15,5 +15,8 @@ class or_tanf_resources_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states["or"].odhs.tanf.resources.limit # Count explicitly modeled liquid financial assets. - countable_resources = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + countable_resources = round_to_cents( + spm_unit("spm_unit_cash_assets", period.this_year) + ) return countable_resources <= p.amount diff --git a/policyengine_us/variables/gov/states/pa/dhs/tanf/eligibility/pa_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/pa/dhs/tanf/eligibility/pa_tanf_resources_eligible.py index fb86153e9de..61a88c49e66 100644 --- a/policyengine_us/variables/gov/states/pa/dhs/tanf/eligibility/pa_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/pa/dhs/tanf/eligibility/pa_tanf_resources_eligible.py @@ -12,6 +12,10 @@ class pa_tanf_resources_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.pa.dhs.tanf - household_assets = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + + household_assets = round_to_cents( + spm_unit("spm_unit_cash_assets", period.this_year) + ) return household_assets <= p.resource_limit diff --git a/policyengine_us/variables/gov/states/ri/dhs/works/eligibility/ri_works_resource_eligible.py b/policyengine_us/variables/gov/states/ri/dhs/works/eligibility/ri_works_resource_eligible.py index 94ed47b2733..bb5033b466b 100644 --- a/policyengine_us/variables/gov/states/ri/dhs/works/eligibility/ri_works_resource_eligible.py +++ b/policyengine_us/variables/gov/states/ri/dhs/works/eligibility/ri_works_resource_eligible.py @@ -14,5 +14,6 @@ class ri_works_resource_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.ri.dhs.works - resources = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + resources = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) return resources <= p.resource_limit diff --git a/policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_resources_eligible.py index 1e6c3f92bac..83f72782efc 100644 --- a/policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_resources_eligible.py @@ -11,5 +11,6 @@ class sc_tanf_resources_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.sc.tanf.resources - assets = spm_unit("sc_tanf_countable_resources", period) + # Inputs are stored as float32, so compare whole cents with the limit. + assets = round_to_cents(spm_unit("sc_tanf_countable_resources", period)) return assets <= p.limit diff --git a/policyengine_us/variables/gov/states/sd/dss/tanf/eligibility/sd_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/sd/dss/tanf/eligibility/sd_tanf_resources_eligible.py index 4aa8067f568..2a0feb13035 100644 --- a/policyengine_us/variables/gov/states/sd/dss/tanf/eligibility/sd_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/sd/dss/tanf/eligibility/sd_tanf_resources_eligible.py @@ -11,5 +11,6 @@ class sd_tanf_resources_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.sd.dss.tanf.resources - resources = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + resources = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) return resources <= p.limit diff --git a/policyengine_us/variables/gov/states/tn/dhs/ff/eligibility/tn_ff_resources_eligible.py b/policyengine_us/variables/gov/states/tn/dhs/ff/eligibility/tn_ff_resources_eligible.py index 023b4003ece..aba97c3046c 100644 --- a/policyengine_us/variables/gov/states/tn/dhs/ff/eligibility/tn_ff_resources_eligible.py +++ b/policyengine_us/variables/gov/states/tn/dhs/ff/eligibility/tn_ff_resources_eligible.py @@ -10,6 +10,9 @@ class tn_ff_resources_eligible(Variable): defined_for = StateCode.TN def formula(spm_unit, period, parameters): - countable_resources = spm_unit("tn_ff_countable_resources", period) + # Inputs are stored as float32, so compare whole cents with the limit. + countable_resources = round_to_cents( + spm_unit("tn_ff_countable_resources", period) + ) p = parameters(period).gov.states.tn.dhs.ff.resources return countable_resources <= p.limit diff --git a/policyengine_us/variables/gov/states/tx/tanf/eligibility/tx_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/tx/tanf/eligibility/tx_tanf_resources_eligible.py index 65248230bf3..e438cc96097 100644 --- a/policyengine_us/variables/gov/states/tx/tanf/eligibility/tx_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/tx/tanf/eligibility/tx_tanf_resources_eligible.py @@ -13,7 +13,10 @@ class tx_tanf_resources_eligible(Variable): defined_for = StateCode.TX def formula(spm_unit, period, parameters): - countable_resources = spm_unit("tx_tanf_countable_resources", period) + # Inputs are stored as float32, so compare whole cents with the limit. + countable_resources = round_to_cents( + spm_unit("tx_tanf_countable_resources", period) + ) p = parameters(period).gov.states.tx.tanf.resources return countable_resources <= p.resource_limit diff --git a/policyengine_us/variables/gov/states/ut/dwf/fep/eligibility/ut_fep_resources_eligible.py b/policyengine_us/variables/gov/states/ut/dwf/fep/eligibility/ut_fep_resources_eligible.py index d4404c00f8e..d08d1e7a029 100644 --- a/policyengine_us/variables/gov/states/ut/dwf/fep/eligibility/ut_fep_resources_eligible.py +++ b/policyengine_us/variables/gov/states/ut/dwf/fep/eligibility/ut_fep_resources_eligible.py @@ -15,5 +15,6 @@ def formula(spm_unit, period, parameters): p = parameters(period).gov.states.ut.dwf.fep.resources.limit # Count liquid financial assets explicitly; other excluded resources # are not yet modeled separately here. - assets = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + assets = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) return assets <= p.amount diff --git a/policyengine_us/variables/gov/states/vt/dcf/reach_up/eligibility/vt_reach_up_resources_eligible.py b/policyengine_us/variables/gov/states/vt/dcf/reach_up/eligibility/vt_reach_up_resources_eligible.py index b0f625dc3f8..5dacf52526e 100644 --- a/policyengine_us/variables/gov/states/vt/dcf/reach_up/eligibility/vt_reach_up_resources_eligible.py +++ b/policyengine_us/variables/gov/states/vt/dcf/reach_up/eligibility/vt_reach_up_resources_eligible.py @@ -14,5 +14,6 @@ def formula(spm_unit, period, parameters): p = parameters(period).gov.states.vt.dcf.reach_up.resources # NOTE: Many exclusions exist (vehicles, retirement accounts, etc.) # that are not separately tracked in PolicyEngine - assets = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + assets = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) return assets <= p.limit diff --git a/policyengine_us/variables/gov/states/wa/dshs/tanf/wa_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/wa/dshs/tanf/wa_tanf_resources_eligible.py index 984fd596db5..ab86366a75e 100644 --- a/policyengine_us/variables/gov/states/wa/dshs/tanf/wa_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/wa/dshs/tanf/wa_tanf_resources_eligible.py @@ -11,5 +11,8 @@ class wa_tanf_resources_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.wa.dshs.tanf - countable_resources = spm_unit("wa_tanf_countable_resources", period) + # Inputs are stored as float32, so compare whole cents with the limit. + countable_resources = round_to_cents( + spm_unit("wa_tanf_countable_resources", period) + ) return countable_resources <= p.resource_limit diff --git a/policyengine_us/variables/gov/states/wi/dcf/works/eligibility/wi_works_resources_eligible.py b/policyengine_us/variables/gov/states/wi/dcf/works/eligibility/wi_works_resources_eligible.py index 7871b8455c5..14c8e8ce701 100644 --- a/policyengine_us/variables/gov/states/wi/dcf/works/eligibility/wi_works_resources_eligible.py +++ b/policyengine_us/variables/gov/states/wi/dcf/works/eligibility/wi_works_resources_eligible.py @@ -13,6 +13,7 @@ class wi_works_resources_eligible(Variable): defined_for = StateCode.WI def formula(spm_unit, period, parameters): - countable = spm_unit("wi_works_countable_resources", period) + # Inputs are stored as float32, so compare whole cents with the limit. + countable = round_to_cents(spm_unit("wi_works_countable_resources", period)) p = parameters(period).gov.states.wi.dcf.works.asset return countable <= p.limit diff --git a/policyengine_us/variables/gov/states/wv/dhhr/works/eligibility/wv_works_resources_eligible.py b/policyengine_us/variables/gov/states/wv/dhhr/works/eligibility/wv_works_resources_eligible.py index d22bd9bdfa5..8ffbd787b1f 100644 --- a/policyengine_us/variables/gov/states/wv/dhhr/works/eligibility/wv_works_resources_eligible.py +++ b/policyengine_us/variables/gov/states/wv/dhhr/works/eligibility/wv_works_resources_eligible.py @@ -12,5 +12,8 @@ class wv_works_resources_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.wv.dhhr.works.resources # Count explicitly modeled liquid financial assets. - countable_resources = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + countable_resources = round_to_cents( + spm_unit("spm_unit_cash_assets", period.this_year) + ) return countable_resources <= p.limit diff --git a/policyengine_us/variables/gov/states/wy/dfs/power/eligibility/wy_power_resources_eligible.py b/policyengine_us/variables/gov/states/wy/dfs/power/eligibility/wy_power_resources_eligible.py index 792b0606f84..6e5c7c395a2 100644 --- a/policyengine_us/variables/gov/states/wy/dfs/power/eligibility/wy_power_resources_eligible.py +++ b/policyengine_us/variables/gov/states/wy/dfs/power/eligibility/wy_power_resources_eligible.py @@ -14,5 +14,6 @@ class wy_power_resources_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.wy.dfs.power.resources - resources = spm_unit("spm_unit_cash_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + resources = round_to_cents(spm_unit("spm_unit_cash_assets", period.this_year)) return resources <= p.limit diff --git a/policyengine_us/variables/gov/usda/snap/eligibility/meets_snap_asset_test.py b/policyengine_us/variables/gov/usda/snap/eligibility/meets_snap_asset_test.py index a7b89c647c5..3c03accc5a0 100644 --- a/policyengine_us/variables/gov/usda/snap/eligibility/meets_snap_asset_test.py +++ b/policyengine_us/variables/gov/usda/snap/eligibility/meets_snap_asset_test.py @@ -26,7 +26,8 @@ def formula(spm_unit, period, parameters): # their share at the moment. has_elderly_or_disabled = spm_unit("has_snap_elderly_disabled_member", period) asset_test = parameters(period).gov.usda.snap.asset_test - assets = spm_unit("snap_assets", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + assets = round_to_cents(spm_unit("snap_assets", period.this_year)) asset_limit = where( has_elderly_or_disabled, asset_test.limit.elderly_disabled, From d4760909d9f764d4ff4ec5a37e3e5f53b9f982a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 23:43:01 +0000 Subject: [PATCH 02/12] Round CalWORKs and Illinois AABD resources to cents, and move the medically needy witness to Hawaii The Arkansas case counted the unit as an SSI recipient, which bars the medically needy pathway, so it couldn't show the asset boundary. Hawaii has the same 2,000 limit and reaches it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6HXUrFaqEeR9DeqWcq --- .../is_medically_needy_for_medicaid.yaml | 10 +++---- .../ca_tanf_resources_eligible.yaml | 25 ++++++++++++++++ .../asset/il_aabd_asset_value_eligible.yaml | 29 +++++++++++++++++++ .../eligibility/ca_tanf_resources_eligible.py | 3 +- .../asset/il_aabd_asset_value_eligible.py | 3 +- 5 files changed, 63 insertions(+), 7 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.yaml index 7257fdd5de9..c3191a38bf0 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.yaml @@ -941,9 +941,9 @@ is_in_medicaid_medically_needy_category: [true, true] is_medically_needy_for_medicaid: [true, true] -# Arkansas' 2,000 individual asset limit: 178.57 + 1,547.79 + 273.64 = +# Hawaii's 2,000 individual asset limit: 178.57 + 1,547.79 + 273.64 = # 2,000.00. The float32 balances add to 2,000.0001 unless rounded to cents. -- name: Three accounts that total exactly the Arkansas limit pass +- name: Three accounts that total exactly the Hawaii limit pass period: 2026 input: is_disabled: true @@ -955,12 +955,12 @@ bank_account_assets: 178.57 stock_assets: 1_547.79 bond_assets: 273.64 - state_code: AR + state_code: HI output: is_medically_needy_for_medicaid: true # 178.57 + 1,547.79 + 273.65 = 2,000.01, a cent over the limit. -- name: Three accounts a cent over the Arkansas limit fail +- name: Three accounts a cent over the Hawaii limit fail period: 2026 input: is_disabled: true @@ -972,6 +972,6 @@ bank_account_assets: 178.57 stock_assets: 1_547.79 bond_assets: 273.65 - state_code: AR + state_code: HI output: is_medically_needy_for_medicaid: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/cdss/tanf/cash/eligibility/ca_tanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/cdss/tanf/cash/eligibility/ca_tanf_resources_eligible.yaml index 56a3befc19b..741e139b403 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/cdss/tanf/cash/eligibility/ca_tanf_resources_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/cdss/tanf/cash/eligibility/ca_tanf_resources_eligible.yaml @@ -143,3 +143,28 @@ # Resources: 14,000 + 501 = 14,501 exceeds the lower resource limit # but stays within the higher limit for households with an elderly member ca_tanf_resources_eligible: true + +# 8,441.48 + 8.48 + 3,687.04 = 12,137.00, at the limit. +# The float32 balances add to a fraction of a cent above 12,137 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025 + input: + age: 30 + bank_account_assets: 8_441.48 + stock_assets: 8.48 + bond_assets: 3_687.04 + state_code: CA + output: + ca_tanf_resources_eligible: true + +# 8,441.48 + 8.48 + 3,687.05 = 12,137.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025 + input: + age: 30 + bank_account_assets: 8_441.48 + stock_assets: 8.48 + bond_assets: 3_687.05 + state_code: CA + output: + ca_tanf_resources_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/il/dhs/aabd/asset/il_aabd_asset_value_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/il/dhs/aabd/asset/il_aabd_asset_value_eligible.yaml index e3f21170387..3a60f1a78e6 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/il/dhs/aabd/asset/il_aabd_asset_value_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/il/dhs/aabd/asset/il_aabd_asset_value_eligible.yaml @@ -33,3 +33,32 @@ state_code: IL output: il_aabd_asset_value_eligible: true + +# 1,157.81 + 285.17 + 557.02 = 2,000.00, at the limit. +# The float32 balances add to a fraction of a cent above 2,000 unless rounded to cents. +- name: Three accounts that total exactly the resource limit pass + period: 2025-01 + input: + age: 70 + is_ssi_aged_blind_disabled: true + is_ssi_disabled: true + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.02 + state_code: IL + output: + il_aabd_asset_value_eligible: true + +# 1,157.81 + 285.17 + 557.03 = 2,000.01, a cent over the limit. +- name: Three accounts a cent over the resource limit fail + period: 2025-01 + input: + age: 70 + is_ssi_aged_blind_disabled: true + is_ssi_disabled: true + bank_account_assets: 1_157.81 + stock_assets: 285.17 + bond_assets: 557.03 + state_code: IL + output: + il_aabd_asset_value_eligible: false diff --git a/policyengine_us/variables/gov/states/ca/cdss/tanf/cash/eligibility/ca_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/ca/cdss/tanf/cash/eligibility/ca_tanf_resources_eligible.py index 3d48ac8ead1..346d9c99fed 100644 --- a/policyengine_us/variables/gov/states/ca/cdss/tanf/cash/eligibility/ca_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/ca/cdss/tanf/cash/eligibility/ca_tanf_resources_eligible.py @@ -10,6 +10,7 @@ class ca_tanf_resources_eligible(Variable): reference = "https://my.dpss.lacounty.gov/public/en/home/epolicy/program/calworks/property/property.html" def formula(spm_unit, period, parameters): - resources = spm_unit("ca_tanf_resources", period) + # Inputs are stored as float32, so compare whole cents with the limit. + resources = round_to_cents(spm_unit("ca_tanf_resources", period)) limit = spm_unit("ca_tanf_resources_limit", period) return resources <= limit diff --git a/policyengine_us/variables/gov/states/il/dhs/aabd/asset/il_aabd_asset_value_eligible.py b/policyengine_us/variables/gov/states/il/dhs/aabd/asset/il_aabd_asset_value_eligible.py index 70eeaf73810..2912c634701 100644 --- a/policyengine_us/variables/gov/states/il/dhs/aabd/asset/il_aabd_asset_value_eligible.py +++ b/policyengine_us/variables/gov/states/il/dhs/aabd/asset/il_aabd_asset_value_eligible.py @@ -16,7 +16,8 @@ class il_aabd_asset_value_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.il.dhs.aabd.asset.disregard size = spm_unit("spm_unit_size", period) - countable_asset = spm_unit("il_aabd_countable_assets", period) + # Inputs are stored as float32, so compare whole cents with the limit. + countable_asset = round_to_cents(spm_unit("il_aabd_countable_assets", period)) capped_size = min_(size, 2) reduced_unit_size = max_(size - 2, 0) p1 = p.base[capped_size] From 56f56d1c657a6f45f29fe98c99be88c286b89ab1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 00:20:56 +0000 Subject: [PATCH 03/12] Pin float32-drifting splits in the BBCE witness and the property test snap_assets sums person inputs to the SPM unit in float64 before storing float32, so the Idaho witness needs a split that drifts on that path too. Hypothesis favours edge splits that never drift; pinned examples make the model property fail without the rounding. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6HXUrFaqEeR9DeqWcq --- .../non_cash/meets_tanf_non_cash_asset_test.yaml | 16 ++++++++-------- .../tests/unit/test_round_to_cents_properties.py | 9 +++++++++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/tanf/non_cash/meets_tanf_non_cash_asset_test.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/tanf/non_cash/meets_tanf_non_cash_asset_test.yaml index 998283aefdb..fc4e1b64f1a 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/tanf/non_cash/meets_tanf_non_cash_asset_test.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/tanf/non_cash/meets_tanf_non_cash_asset_test.yaml @@ -107,25 +107,25 @@ output: meets_tanf_non_cash_asset_test: true -# Idaho's 5,000 BBCE asset limit: 3,247.08 + 1,316.70 + 436.22 = 5,000.00. +# Idaho's 5,000 BBCE asset limit: 4,338.99 + 609.03 + 51.98 = 5,000.00. # The float32 balances add to 5,000.0005 unless rounded to cents. - name: Three accounts that total exactly the Idaho limit pass period: 2026 input: state_code_str: ID - bank_account_assets: 3_247.08 - stock_assets: 1_316.70 - bond_assets: 436.22 + bank_account_assets: 4_338.99 + stock_assets: 609.03 + bond_assets: 51.98 output: meets_tanf_non_cash_asset_test: true -# 3,247.08 + 1,316.70 + 436.23 = 5,000.01, a cent over the limit. +# 4,338.99 + 609.03 + 51.99 = 5,000.01, a cent over the limit. - name: Three accounts a cent over the Idaho limit fail period: 2026 input: state_code_str: ID - bank_account_assets: 3_247.08 - stock_assets: 1_316.70 - bond_assets: 436.23 + bank_account_assets: 4_338.99 + stock_assets: 609.03 + bond_assets: 51.99 output: meets_tanf_non_cash_asset_test: false diff --git a/policyengine_us/tests/unit/test_round_to_cents_properties.py b/policyengine_us/tests/unit/test_round_to_cents_properties.py index 415fd6d7fb5..5a25c329a1a 100644 --- a/policyengine_us/tests/unit/test_round_to_cents_properties.py +++ b/policyengine_us/tests/unit/test_round_to_cents_properties.py @@ -111,6 +111,15 @@ def build_situation(households): ], ) @hypothesis.given(st.lists(household(), min_size=1, max_size=20)) +# Hypothesis favours splits at the edges, which don't drift, so these pin +# splits whose float32 sums land above the limit: 2,000.0001 for SSI and +# 4,500.0005 for SNAP. +@hypothesis.example( + [ + ("meets_ssi_resource_test", (17_857, 154_779, 27_364)), + ("meets_snap_asset_test", (418_399, 3_831, 27_770)), + ] +) def test_split_accounts_match_one_account(households): simulation = Simulation(situation=build_situation(households)) period = f"{YEAR}-01" From 220543dd31c96f70721ef8e954626d068682efec Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 04:41:29 +0000 Subject: [PATCH 04/12] Keep the SSI parental deemed share exact; round only float32 sums Rounding own resources plus the share to cents turned a child's true 2,000.0033 into 2,000.00 and passed them, though main fails them. That is a rule about how a share counts, not float noise. POMS SI 01330.200 B says to equally divide the deemed resources among the children, and 20 CFR 416.1202(b)(1) sets no rounding rule, so the share now stays unrounded. The rounding stays on the sums of float32 balances the comparisons reach: the deemors' total before the parental limit comes off, a child's own balances, a couple's balances on a joint claim, and a person's own plus an ineligible spouse's. New YAML pins the review's three-child witness (all fail, as on main), a whole-cent share at the limit that main fails, a cent-over companion for the deemed-parent case, and at-limit and cent-over pairs for the spousal and joint paths. Co-Authored-By: Claude Opus 5.5 --- ...sources_deemed_from_ineligible_parent.yaml | 137 ++++++++++++++++++ .../resources/meets_ssi_resource_test.yaml | 131 +++++++++++++++++ ...resources_deemed_from_ineligible_parent.py | 13 +- .../resources/meets_ssi_resource_test.py | 26 ++-- 4 files changed, 290 insertions(+), 17 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml index 660a4855fb3..03173f032ad 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml @@ -433,6 +433,143 @@ ssi_resources_deemed_from_ineligible_parent: [0, 500] meets_ssi_resource_test: [false, true] +# The child's 575.52 + 570.57 + 353.92 = 1,500.01 plus the same 500 is +# 2,000.01, a cent over the individual limit. +- name: Child accounts plus parental excess a cent over the limit fail + period: 2026-01 + input: + people: + parent: + age: 40 + is_ssi_aged_blind_disabled: false + is_tax_unit_head: true + is_parent: true + bank_account_assets: 2_500 + child: + age: 16 + is_ssi_aged_blind_disabled: true + is_tax_unit_dependent: true + bank_account_assets: 575.52 + stock_assets: 570.57 + bond_assets: 353.92 + tax_units: + tax_unit: + members: [parent, child] + marital_units: + parent_unit: + members: [parent] + child_unit: + members: [child] + households: + household: + members: [parent, child] + output: + ssi_resources_deemed_from_ineligible_parent: [0, 500] + meets_ssi_resource_test: [false, false] + +# POMS SI 01330.200 B: 'equally divide the value of the deemed resources +# among those children'; 20 CFR 416.1202(b)(1) sets no rounding rule for a +# child's share. The parent's 2,100 is 100 over the individual limit, so +# each of three children is deemed 100 / 3 = 33.3333. Each child holds +# 1,966.67 in one account, so the total of 2,000.0033 is over the limit. +- name: A share of parental excess counts to the fraction of a cent + period: 2026-01 + input: + people: + parent: + age: 40 + is_ssi_aged_blind_disabled: false + is_tax_unit_head: true + is_parent: true + bank_account_assets: 2_100 + child1: + age: 8 + is_ssi_aged_blind_disabled: true + is_tax_unit_dependent: true + bank_account_assets: 1_966.67 + child2: + age: 10 + is_ssi_aged_blind_disabled: true + is_tax_unit_dependent: true + bank_account_assets: 1_966.67 + child3: + age: 12 + is_ssi_aged_blind_disabled: true + is_tax_unit_dependent: true + bank_account_assets: 1_966.67 + tax_units: + tax_unit: + members: [parent, child1, child2, child3] + marital_units: + parent_unit: + members: [parent] + child1_unit: + members: [child1] + child2_unit: + members: [child2] + child3_unit: + members: [child3] + households: + household: + members: [parent, child1, child2, child3] + output: + ssi_resources_deemed_from_ineligible_parent: [0, 33.3333, 33.3333, 33.3333] + meets_ssi_resource_test: [false, false, false, false] + +# The parent's 2,100 is 100 over the individual limit, which divides among +# four children as 25 each. The first child's 178.57 + 1,522.79 + 273.64 = +# 1,975.00 plus 25 is 2,000.00, at the limit. That child's float32 balances +# add to 1,975.0001 unless rounded to cents. +- name: Child accounts plus a share of parental excess that total exactly the limit pass + period: 2026-01 + input: + people: + parent: + age: 40 + is_ssi_aged_blind_disabled: false + is_tax_unit_head: true + is_parent: true + bank_account_assets: 2_100 + child1: + age: 8 + is_ssi_aged_blind_disabled: true + is_tax_unit_dependent: true + bank_account_assets: 178.57 + stock_assets: 1_522.79 + bond_assets: 273.64 + child2: + age: 10 + is_ssi_aged_blind_disabled: true + is_tax_unit_dependent: true + child3: + age: 12 + is_ssi_aged_blind_disabled: true + is_tax_unit_dependent: true + child4: + age: 14 + is_ssi_aged_blind_disabled: true + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [parent, child1, child2, child3, child4] + marital_units: + parent_unit: + members: [parent] + child1_unit: + members: [child1] + child2_unit: + members: [child2] + child3_unit: + members: [child3] + child4_unit: + members: [child4] + households: + household: + members: [parent, child1, child2, child3, child4] + output: + ssi_resources_deemed_from_ineligible_parent: [0, 25, 25, 25, 25] + meets_ssi_resource_test: [false, true, true, true, true] + # POMS SI 01330.340 White example: parent pool1200 <=couple limit3000; child deemed0. - name: POMS White eligible parent leaves no excess resources to deem period: 2024-01 diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/meets_ssi_resource_test.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/meets_ssi_resource_test.yaml index 208c36cb1f0..99b9b0ac73a 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/meets_ssi_resource_test.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/meets_ssi_resource_test.yaml @@ -351,3 +351,134 @@ bond_assets: 273.65 output: meets_ssi_resource_test: false + +# 20 CFR 416.1202(a), 416.1205: the claimant's and the ineligible spouse's +# 575.52 + 570.57 + 353.91 = 1,500.00 each add to 3,000.00, at the couple +# limit. The float32 balances add to 3,000.0002 unless rounded to cents. +- name: Spousal accounts that total exactly the couple limit pass + period: 2026-01 + input: + people: + claimant: + age: 40 + is_ssi_aged_blind_disabled: true + is_tax_unit_head: true + bank_account_assets: 575.52 + stock_assets: 570.57 + bond_assets: 353.91 + spouse: + age: 40 + is_ssi_aged_blind_disabled: false + is_tax_unit_spouse: true + bank_account_assets: 575.52 + stock_assets: 570.57 + bond_assets: 353.91 + tax_units: + family: + members: [claimant, spouse] + marital_units: + family_couple_0: + members: [claimant, spouse] + households: + family: + members: [claimant, spouse] + output: + ssi_claim_is_joint: [false, false] + meets_ssi_resource_test: [true, true] + +# The spouse's 575.52 + 570.57 + 353.92 = 1,500.01 makes 3,000.01, a cent +# over the couple limit. The spouse, who isn't aged, blind or disabled, is +# tested alone against the individual limit and passes. +- name: Spousal accounts a cent over the couple limit fail + period: 2026-01 + input: + people: + claimant: + age: 40 + is_ssi_aged_blind_disabled: true + is_tax_unit_head: true + bank_account_assets: 575.52 + stock_assets: 570.57 + bond_assets: 353.91 + spouse: + age: 40 + is_ssi_aged_blind_disabled: false + is_tax_unit_spouse: true + bank_account_assets: 575.52 + stock_assets: 570.57 + bond_assets: 353.92 + tax_units: + family: + members: [claimant, spouse] + marital_units: + family_couple_0: + members: [claimant, spouse] + households: + family: + members: [claimant, spouse] + output: + ssi_claim_is_joint: [false, false] + meets_ssi_resource_test: [false, true] + +# 20 CFR 416.1205: a jointly eligible couple's 1,500.00 in one account and +# 575.52 + 570.57 + 353.91 = 1,500.00 add to 3,000.00, at the couple limit. +# The float32 balances add to 3,000.0001 unless rounded to cents. +- name: Joint claim accounts that total exactly the couple limit pass + period: 2026-01 + input: + people: + claimant: + age: 40 + is_ssi_aged_blind_disabled: true + is_tax_unit_head: true + bank_account_assets: 1_500 + spouse: + age: 40 + is_ssi_aged_blind_disabled: true + is_tax_unit_spouse: true + bank_account_assets: 575.52 + stock_assets: 570.57 + bond_assets: 353.91 + tax_units: + family: + members: [claimant, spouse] + marital_units: + family_couple_0: + members: [claimant, spouse] + households: + family: + members: [claimant, spouse] + output: + ssi_claim_is_joint: [true, true] + meets_ssi_resource_test: [true, true] + +# The spouse's 575.52 + 570.57 + 353.92 = 1,500.01 makes 3,000.01, a cent +# over the couple limit. +- name: Joint claim accounts a cent over the couple limit fail + period: 2026-01 + input: + people: + claimant: + age: 40 + is_ssi_aged_blind_disabled: true + is_tax_unit_head: true + bank_account_assets: 1_500 + spouse: + age: 40 + is_ssi_aged_blind_disabled: true + is_tax_unit_spouse: true + bank_account_assets: 575.52 + stock_assets: 570.57 + bond_assets: 353.92 + tax_units: + family: + members: [claimant, spouse] + marital_units: + family_couple_0: + members: [claimant, spouse] + households: + family: + members: [claimant, spouse] + output: + ssi_claim_is_joint: [true, true] + meets_ssi_resource_test: [false, false] diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.py index 0b9fe1dec7c..28334d22c0a 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.py @@ -120,11 +120,14 @@ def formula(person, period, parameters): # 'only to the extent that those resources exceed the resource limits # described in § 416.1205': use the reformable individual/couple limits. allowance = where(parent_count > 1, p.couple, p.individual) - # Inputs are stored as float32, so compare whole cents with the limits. + # Balances are float32 inputs. Each deemor's are added together, then + # the deemors' totals, so the sum can land a fraction of a cent off + # the total it represents: round it to cents before the limit comes off. excess = max_(0, round_to_cents(parental_resources) - allowance) # Do not discard ABD parents. SI 01330.310 B-C says zero if parent(s) # meets resource eligibility, usual deeming if not. The Ryder example # (.340) has an ABD mother failing the couple limit: 3100-3000=100. + # The child's own float32 balances are added together: round to cents. own_resources = round_to_cents(person("ssi_countable_resources", period)) active = person("is_ssi_resource_deeming_child", period) & ( own_resources <= p.individual @@ -149,9 +152,15 @@ def formula(person, period, parameters): pool_count = int(np.max(pool, initial=-1)) + 1 for _ in range(rounds): count = np.bincount(pool[active & income_eligible], minlength=pool_count) + # The share is a division, not a sum of balances, so it stays + # exact: SI 01330.200 B says to 'equally divide the value of the + # deemed resources among those children', and 416.1202(b)(1) sets + # no rule for rounding a child's share. + # https://secure.ssa.gov/poms.nsf/lnx/0501330200 + # https://www.law.cornell.edu/cfr/text/20/416.1202 share = excess / max_(1, count[pool]) deemed = where(active, share, deemed) - remaining = active & (round_to_cents(own_resources + share) <= p.individual) + remaining = active & (own_resources + share <= p.individual) if np.array_equal(active, remaining): break active = remaining diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/meets_ssi_resource_test.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/meets_ssi_resource_test.py index 6c789f8c52d..e206d99cd66 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/meets_ssi_resource_test.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/meets_ssi_resource_test.py @@ -17,21 +17,17 @@ def formula(person, period, parameters): joint_claim = person("ssi_claim_is_joint", period) personal_resources = person("ssi_countable_resources", period) spousal_deeming = person("is_ssi_spousal_resource_deeming_applies", period) - deemed_resources = add( - person, - period, - [ - "ssi_resources_deemed_from_ineligible_spouse", - "ssi_resources_deemed_from_ineligible_parent", - ], - ) - # Inputs are stored as float32, so compare whole cents with the limit. - countable_resources = round_to_cents( - where( - joint_claim, - person.marital_unit.sum(personal_resources), - personal_resources + deemed_resources, - ) + spouse_resources = person("ssi_resources_deemed_from_ineligible_spouse", period) + parent_resources = person("ssi_resources_deemed_from_ineligible_parent", period) + # Balances are float32 inputs, so a sum of them can land a fraction of + # a cent off the total it represents. Round each such sum to cents: a + # couple's balances, or a person's own plus a spouse's. The parental + # share is an equal division with no rounding rule (SI 01330.200 B, + # 416.1202(b)(1)), so it is added unrounded. + countable_resources = where( + joint_claim, + round_to_cents(person.marital_unit.sum(personal_resources)), + round_to_cents(personal_resources + spouse_resources) + parent_resources, ) # 416.1205(a): an individual living with an ineligible spouse counts # 'including the resources of the spouse' against the spouse limit. From 365613ef887108da57acf050eced500fe677dd8e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 04:41:29 +0000 Subject: [PATCH 05/12] Round Maryland PAA and Virginia Auxiliary Grant resources to cents Both compare ssi_countable_resources, a float32 sum of bank, stock and bond balances, with the $2,000 SSI limit, so three balances that add to exactly $2,000.00 failed as $2,000.0001. Each fix gets an at-limit case and a cent-over companion. Co-Authored-By: Claude Opus 5.5 --- .../md/dhs/fia/paa/md_paa_eligible.yaml | 42 +++++++++++++++++++ .../gov/states/va/dars/ag/va_ag_eligible.yaml | 35 ++++++++++++++++ .../states/md/dhs/fia/paa/md_paa_eligible.py | 5 ++- .../gov/states/va/dars/ag/va_ag_eligible.py | 5 ++- 4 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/states/va/dars/ag/va_ag_eligible.yaml diff --git a/policyengine_us/tests/policy/baseline/gov/states/md/dhs/fia/paa/md_paa_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/md/dhs/fia/paa/md_paa_eligible.yaml index e139b19db84..827caa644a9 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/md/dhs/fia/paa/md_paa_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/md/dhs/fia/paa/md_paa_eligible.yaml @@ -386,3 +386,45 @@ state_code: MD output: md_paa_eligible: true + +# PAA Manual §500's $2,000 limit: 178.57 + 1,547.79 + 273.64 = 2,000.00, at +# the limit. The three float32 balances add to 2,000.0001 unless rounded to +# cents. +- name: Case 19, three accounts that total exactly the resource limit are eligible. + period: 2026-01 + input: + people: + person1: + age: 70 + is_disabled: true + ssi: 800 + bank_account_assets: 178.57 + stock_assets: 1_547.79 + bond_assets: 273.64 + md_paa_living_arrangement: CARE_HOME_LEVEL_B + households: + household: + members: [person1] + state_code: MD + output: + md_paa_eligible: true + +# 178.57 + 1,547.79 + 273.65 = 2,000.01, a cent over the limit. +- name: Case 20, three accounts a cent over the resource limit are ineligible. + period: 2026-01 + input: + people: + person1: + age: 70 + is_disabled: true + ssi: 800 + bank_account_assets: 178.57 + stock_assets: 1_547.79 + bond_assets: 273.65 + md_paa_living_arrangement: CARE_HOME_LEVEL_B + households: + household: + members: [person1] + state_code: MD + output: + md_paa_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/va/dars/ag/va_ag_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/va/dars/ag/va_ag_eligible.yaml new file mode 100644 index 00000000000..fb98c10be34 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/va/dars/ag/va_ag_eligible.yaml @@ -0,0 +1,35 @@ +# An applicant who doesn't receive SSI is tested against the SSI resource +# limits. 178.57 + 1,547.79 + 273.64 = 2,000.00, at the $2,000 individual +# limit. The three float32 balances add to 2,000.0001 unless rounded to cents. +- name: Three accounts that total exactly the individual resource limit are eligible + period: 2026-01 + input: + state_code: VA + age: 70 + is_disabled: true + is_ssi_aged_blind_disabled: true + va_ag_certified: true + ssi: 0 + social_security: 30_000 + bank_account_assets: 178.57 + stock_assets: 1_547.79 + bond_assets: 273.64 + output: + va_ag_eligible: true + +# 178.57 + 1,547.79 + 273.65 = 2,000.01, a cent over the limit. +- name: Three accounts a cent over the individual resource limit are ineligible + period: 2026-01 + input: + state_code: VA + age: 70 + is_disabled: true + is_ssi_aged_blind_disabled: true + va_ag_certified: true + ssi: 0 + social_security: 30_000 + bank_account_assets: 178.57 + stock_assets: 1_547.79 + bond_assets: 273.65 + output: + va_ag_eligible: false diff --git a/policyengine_us/variables/gov/states/md/dhs/fia/paa/md_paa_eligible.py b/policyengine_us/variables/gov/states/md/dhs/fia/paa/md_paa_eligible.py index 5d71e550170..2559d8f9f62 100644 --- a/policyengine_us/variables/gov/states/md/dhs/fia/paa/md_paa_eligible.py +++ b/policyengine_us/variables/gov/states/md/dhs/fia/paa/md_paa_eligible.py @@ -61,7 +61,10 @@ def formula(person, period, parameters): ssi_resource_limit = parameters( period ).gov.ssa.ssi.eligibility.resources.limit.individual - ssi_resources = person("ssi_countable_resources", period.this_year) + # Inputs are stored as float32, so compare whole cents with the limit. + ssi_resources = round_to_cents( + person("ssi_countable_resources", period.this_year) + ) resource_eligible = ssi_resources <= ssi_resource_limit living_arrangement = person("md_paa_living_arrangement", period) in_facility = living_arrangement != living_arrangement.possible_values.NONE diff --git a/policyengine_us/variables/gov/states/va/dars/ag/va_ag_eligible.py b/policyengine_us/variables/gov/states/va/dars/ag/va_ag_eligible.py index acc268b6f8e..7a1f7ed8d4d 100644 --- a/policyengine_us/variables/gov/states/va/dars/ag/va_ag_eligible.py +++ b/policyengine_us/variables/gov/states/va/dars/ag/va_ag_eligible.py @@ -18,7 +18,10 @@ def formula(person, period, parameters): person.marital_unit.sum(person("va_ag_is_joint_initial_month", period)) == 2 ) resources = person("ssi_countable_resources", period.this_year) - resources = where(joint, person.marital_unit.sum(resources), resources) + # Inputs are stored as float32, so compare whole cents with the limit. + resources = round_to_cents( + where(joint, person.marital_unit.sum(resources), resources) + ) limits = parameters(period).gov.ssa.ssi.eligibility.resources.limit receives_ssi = (person("ssi", period) > 0) | person("receives_ssi", period) resource_eligible = receives_ssi | ( From 4d6efb0473bc972c3294a5905e258913606d8e37 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 04:41:29 +0000 Subject: [PATCH 06/12] Correct the exactness bound and drop the model property test Below 2**15 dollars a float32 half unit is at most 2**-10, so five half units stay under half a cent: the helper's guarantee holds below $32,768, not $16,384, and the property now draws totals up to $32,767.99. The model property built a Simulation per example, and only its pinned example caught anything; the YAML at-limit cases cover that, so it goes. Also remove a stray blank line in the Pennsylvania TANF formula. Co-Authored-By: Claude Opus 5.5 --- .../unit/test_round_to_cents_properties.py | 97 ++----------------- .../eligibility/pa_tanf_resources_eligible.py | 1 - 2 files changed, 9 insertions(+), 89 deletions(-) diff --git a/policyengine_us/tests/unit/test_round_to_cents_properties.py b/policyengine_us/tests/unit/test_round_to_cents_properties.py index 5a25c329a1a..8dfc05b2b56 100644 --- a/policyengine_us/tests/unit/test_round_to_cents_properties.py +++ b/policyengine_us/tests/unit/test_round_to_cents_properties.py @@ -1,35 +1,28 @@ -"""Hypothesis checks that splitting a total into cents doesn't change a limit test. +"""Hypothesis check that splitting a total into cents doesn't change a limit test. Account balances are stored as float32, so three balances that add to a limit in cents can sum a fraction of a cent above or below it. Asset tests round the sum to cents before comparing it with the limit (``round_to_cents``). -The first property checks the helper alone, without a model, on both ways -the model sums inputs: float32 addition within one entity, and a float64 sum -cast back to float32 when person inputs are added up to a group entity. - -The second property runs the model. Each example is one situation of -single-person households. Each household splits a total at, a cent below or -a cent above the SSI and SNAP limits among bank, stock and bond assets, and -its result must match the result for the same total held in one account. -Hypothesis is a dev extra, so this module skips without it. +The property checks the helper alone, without a model, on both ways the model +sums inputs: float32 addition within one entity, and a float64 sum cast back +to float32 when person inputs are added up to a group entity. The YAML cases +at each fixed limit check the model. Hypothesis is a dev extra, so this module +skips without it. """ import numpy as np import pytest -from policyengine_us import Simulation from policyengine_us.tools.general import round_to_cents hypothesis = pytest.importorskip("hypothesis") st = pytest.importorskip("hypothesis.strategies") -ACCOUNTS = ("bank_account_assets", "stock_assets", "bond_assets") - # Three float32 parts and two float32 additions each err by at most half a -# unit in the last place of the total. Below 2**14 dollars that unit is at -# most 2**-10, so the sum stays within 5 / 2**11 < 0.005 of the total. -MAX_CENTS = 2**14 * 100 - 1 +# unit in the last place of the total. Below 2**15 dollars a half unit is at +# most 2**-10, so the sum stays within 5 / 2**10 < 0.005 of the total. +MAX_CENTS = 2**15 * 100 - 1 @st.composite @@ -56,75 +49,3 @@ def test_rounded_sum_of_float32_parts_is_the_total(case): float64_sum = np.float32(sum(float(value) for value in values)) for amount in (float32_sum, float64_sum): assert round_to_cents(amount) == total / 100 - - -YEAR = 2026 -# 20 CFR 416.1205: the individual limit is 2,000. Everyone is 70, so SNAP -# applies its FY2026 limit for households with an elderly member, 4,500 -# (7 U.S.C. 2014(g)(1)). -LIMITS = {"meets_ssi_resource_test": 2_000, "meets_snap_asset_test": 4_500} - - -@st.composite -def household(draw): - test = draw(st.sampled_from(sorted(LIMITS))) - total = LIMITS[test] * 100 + draw(st.sampled_from([-1, 0, 1])) - return test, draw(cent_split(total)) - - -def build_situation(households): - people = {} - groups = {} - for i, (_, parts) in enumerate(households): - for split in (False, True): - name = f"h{i}_{'split' if split else 'whole'}" - amounts = parts if split else (sum(parts), 0, 0) - person = { - "age": {YEAR: 70}, - "is_ssi_aged_blind_disabled": {YEAR: True}, - } - for account, cents in zip(ACCOUNTS, amounts): - person[account] = {YEAR: cents / 100} - people[name] = person - groups[name] = {"members": [name]} - return { - "people": people, - **{ - entity: groups - for entity in ( - "tax_units", - "spm_units", - "families", - "marital_units", - "households", - ) - }, - } - - -@hypothesis.settings( - max_examples=10, - deadline=None, - suppress_health_check=[ - hypothesis.HealthCheck.too_slow, - hypothesis.HealthCheck.data_too_large, - ], -) -@hypothesis.given(st.lists(household(), min_size=1, max_size=20)) -# Hypothesis favours splits at the edges, which don't drift, so these pin -# splits whose float32 sums land above the limit: 2,000.0001 for SSI and -# 4,500.0005 for SNAP. -@hypothesis.example( - [ - ("meets_ssi_resource_test", (17_857, 154_779, 27_364)), - ("meets_snap_asset_test", (418_399, 3_831, 27_770)), - ] -) -def test_split_accounts_match_one_account(households): - simulation = Simulation(situation=build_situation(households)) - period = f"{YEAR}-01" - results = {test: np.asarray(simulation.calculate(test, period)) for test in LIMITS} - # People alternate whole, split, so each person's group holds only them. - for i, (test, parts) in enumerate(households): - whole, split = results[test][2 * i], results[test][2 * i + 1] - assert split == whole, (test, parts) diff --git a/policyengine_us/variables/gov/states/pa/dhs/tanf/eligibility/pa_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/pa/dhs/tanf/eligibility/pa_tanf_resources_eligible.py index 61a88c49e66..bda666ab06f 100644 --- a/policyengine_us/variables/gov/states/pa/dhs/tanf/eligibility/pa_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/pa/dhs/tanf/eligibility/pa_tanf_resources_eligible.py @@ -13,7 +13,6 @@ def formula(spm_unit, period, parameters): p = parameters(period).gov.states.pa.dhs.tanf # Inputs are stored as float32, so compare whole cents with the limit. - household_assets = round_to_cents( spm_unit("spm_unit_cash_assets", period.this_year) ) From b7773a86b4e0c3c9d8b519c1e937cfc1a4c4070c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 04:49:49 +0000 Subject: [PATCH 07/12] Pin the exact share in the deemed-parent division loop Rounding own resources plus the share inside the loop alone keeps the review's three-child witness unchanged, since all three children stay or leave together. This case lets one child leave the division on the exact share, so the other two divide the excess as 50 each; rounding the loop's total would keep all three at 33.33. Main gives the same result as this branch. Co-Authored-By: Claude Opus 5.5 --- ...sources_deemed_from_ineligible_parent.yaml | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml index 03173f032ad..3e2b151e86b 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml @@ -516,6 +516,53 @@ ssi_resources_deemed_from_ineligible_parent: [0, 33.3333, 33.3333, 33.3333] meets_ssi_resource_test: [false, false, false, false] +# SI 01330.200 B: when a child is ineligible, 'divide the value of the deemed +# resources among the remaining eligible children'. The first child's +# 1,966.67 plus the exact share of 33.3333 is over the limit, so that child +# leaves the division and keeps the share last tested; the other two +# children, who hold nothing, divide the 100 as 50 each. +- name: A child whom an unrounded share takes over the limit leaves the division + period: 2026-01 + input: + people: + parent: + age: 40 + is_ssi_aged_blind_disabled: false + is_tax_unit_head: true + is_parent: true + bank_account_assets: 2_100 + child1: + age: 8 + is_ssi_aged_blind_disabled: true + is_tax_unit_dependent: true + bank_account_assets: 1_966.67 + child2: + age: 10 + is_ssi_aged_blind_disabled: true + is_tax_unit_dependent: true + child3: + age: 12 + is_ssi_aged_blind_disabled: true + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [parent, child1, child2, child3] + marital_units: + parent_unit: + members: [parent] + child1_unit: + members: [child1] + child2_unit: + members: [child2] + child3_unit: + members: [child3] + households: + household: + members: [parent, child1, child2, child3] + output: + ssi_resources_deemed_from_ineligible_parent: [0, 33.3333, 50, 50] + meets_ssi_resource_test: [false, false, true, true] + # The parent's 2,100 is 100 over the individual limit, which divides among # four children as 25 each. The first child's 178.57 + 1,522.79 + 273.64 = # 1,975.00 plus 25 is 2,000.00, at the limit. That child's float32 balances From 0e3f0a468380d52975a1556791e1c0defd22c81e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 05:15:27 +0000 Subject: [PATCH 08/12] Keep parental shares exact in the medically needy and Iowa SSA tests Both add a parental share to a child's resources and then rounded the total to cents, which absorbed a fraction-of-a-cent share the same way the SSI test did. Medically needy adds SSI's own share; Iowa divides the excess as SSI does. Round only the summed balances and add the share unrounded, as the SSI resource test now does. The medically needy witness is a disabled child whose own income is too high for SSI, so they don't count in the division but are deemed the share their SSI-eligible siblings divide: 1,966.67 plus 33.3333 fails, as on main, and a whole-cent share at the limit passes, which main fails. Iowa's computed share doesn't give that witness: a child whom the share takes over the limit leaves the division and is deemed the remaining children's larger share, or the whole excess if none remain. So its witness enters a certified share. Co-Authored-By: Claude Opus 5.5 --- .../is_medically_needy_for_medicaid.yaml | 143 ++++++++++++++++++ .../states/ia/hhs/ssa/ia_ssa_eligible.yaml | 68 +++++++++ .../is_medically_needy_for_medicaid.py | 10 +- .../gov/states/ia/hhs/ssa/ia_ssa_eligible.py | 10 +- 4 files changed, 225 insertions(+), 6 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.yaml index c3191a38bf0..ca07572b347 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.yaml @@ -975,3 +975,146 @@ state_code: HI output: is_medically_needy_for_medicaid: false + + +# A child's resources include the SSI parental share (Cases 5 and 6). The +# first child's own income is too high for SSI, as in Case 5, so they don't +# count in the division but are still deemed the share the three SSI-eligible +# siblings divide (SI 01330.200 B, which sets no rounding rule for a share). +# The parent's 2,100 is 100 over the SSI individual allowance, so the share +# is 100 / 3 = 33.3333. The first child's 1,966.67 in one account plus the +# share is 2,000.0033, over Massachusetts's 2,000 limit. The siblings receive +# SSI, so they qualify through that pathway instead; the parent's 80,000 of +# wages keeps the children out of the MAGI child groups. +- name: Case 37, a fractional parental share counts against a disabled child's asset limit. + period: 2026 + input: + people: + parent: + age: 40 + is_tax_unit_head: true + is_parent: true + employment_income_before_lsr: 80_000 + bank_account_assets: 2_100 + child1: + age: 8 + is_tax_unit_dependent: true + is_ssi_disabled: true + ssi_countable_income: 21_358 + medicaid_medically_needy_medical_expenses: 16_000 + is_optional_senior_or_disabled_for_medicaid: false + bank_account_assets: 1_966.67 + child2: + age: 10 + is_tax_unit_dependent: true + is_ssi_disabled: true + ssi_countable_income: 0 + is_optional_senior_or_disabled_for_medicaid: false + child3: + age: 12 + is_tax_unit_dependent: true + is_ssi_disabled: true + ssi_countable_income: 0 + is_optional_senior_or_disabled_for_medicaid: false + child4: + age: 14 + is_tax_unit_dependent: true + is_ssi_disabled: true + ssi_countable_income: 0 + is_optional_senior_or_disabled_for_medicaid: false + tax_units: + tax_unit: + members: [parent, child1, child2, child3, child4] + marital_units: + parent_unit: + members: [parent] + child1_unit: + members: [child1] + child2_unit: + members: [child2] + child3_unit: + members: [child3] + child4_unit: + members: [child4] + households: + household: + members: [parent, child1, child2, child3, child4] + state_code: MA + output: + ssi_resources_deemed_from_ineligible_parent: + 2026-01: [0, 33.3333, 33.3333, 33.3333, 33.3333] + is_medically_needy_for_medicaid: [false, false, false, false, false] + +# With four SSI-eligible siblings the parent's 100 of excess divides as 25 +# each. The first child's 178.57 + 1,522.79 + 273.64 = 1,975.00 plus 25 is +# 2,000.00, at the limit, and income 21,358 - 16,000 = 5,358 <= 6,264 as in +# Case 5. That child's float32 balances add to 1,975.0001 unless rounded to +# cents. +- name: Case 38, a disabled child's accounts plus a whole-cent parental share at the asset limit qualify. + period: 2026 + input: + people: + parent: + age: 40 + is_tax_unit_head: true + is_parent: true + employment_income_before_lsr: 80_000 + bank_account_assets: 2_100 + child1: + age: 8 + is_tax_unit_dependent: true + is_ssi_disabled: true + ssi_countable_income: 21_358 + medicaid_medically_needy_medical_expenses: 16_000 + is_optional_senior_or_disabled_for_medicaid: false + bank_account_assets: 178.57 + stock_assets: 1_522.79 + bond_assets: 273.64 + child2: + age: 10 + is_tax_unit_dependent: true + is_ssi_disabled: true + ssi_countable_income: 0 + is_optional_senior_or_disabled_for_medicaid: false + child3: + age: 12 + is_tax_unit_dependent: true + is_ssi_disabled: true + ssi_countable_income: 0 + is_optional_senior_or_disabled_for_medicaid: false + child4: + age: 14 + is_tax_unit_dependent: true + is_ssi_disabled: true + ssi_countable_income: 0 + is_optional_senior_or_disabled_for_medicaid: false + child5: + age: 16 + is_tax_unit_dependent: true + is_ssi_disabled: true + ssi_countable_income: 0 + is_optional_senior_or_disabled_for_medicaid: false + tax_units: + tax_unit: + members: [parent, child1, child2, child3, child4, child5] + marital_units: + parent_unit: + members: [parent] + child1_unit: + members: [child1] + child2_unit: + members: [child2] + child3_unit: + members: [child3] + child4_unit: + members: [child4] + child5_unit: + members: [child5] + households: + household: + members: [parent, child1, child2, child3, child4, child5] + state_code: MA + output: + ssi_resources_deemed_from_ineligible_parent: + 2026-01: [0, 25, 25, 25, 25, 25] + is_medically_needy_for_medicaid: [false, true, false, false, false, false] diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/ssa/ia_ssa_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/ssa/ia_ssa_eligible.yaml index 48895eb269d..3c5c7e5b0b7 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/ssa/ia_ssa_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/ssa/ia_ssa_eligible.yaml @@ -26,3 +26,71 @@ state_code: IA output: ia_ssa_eligible: false + +# ia_ssa_resources_deemed_from_parents divides the parental excess among the +# children as SSI does, and a certified share can be entered instead. A share +# of 33.3333 (100 divided among three children) plus the child's 1,966.67 in +# one account is 2,000.0033, over the limit: the share isn't rounded to +# cents, as SSI sets no rounding rule for it (SI 01330.200 B). +- name: A fractional share of parental excess counts against the resource limit + period: 2026-01 + input: + people: + parent: + age: 40 + child: + age: 10 + bank_account_assets: 1_966.67 + ia_ssa_resources_deemed_from_parents: 33.3333 + ia_ssa_category: IN_HOME_CARE + ia_ssa_ihhrc_monthly_cost: 400 + is_disabled: true + meets_ssi_disability_criteria: true + marital_units: + parent_unit: + members: [parent] + child_unit: + members: [child] + tax_units: + family: + members: [parent, child] + households: + home: + members: [parent, child] + state_code: IA + output: + ia_ssa_eligible: [false, false] + +# A share of 25 (100 divided among four children) plus the child's 178.57 + +# 1,522.79 + 273.64 = 1,975.00 is 2,000.00, at the limit. The child's float32 +# balances add to 1,975.0001 unless rounded to cents. +- name: Three accounts plus a whole-cent share of parental excess at the limit pass + period: 2026-01 + input: + people: + parent: + age: 40 + child: + age: 10 + bank_account_assets: 178.57 + stock_assets: 1_522.79 + bond_assets: 273.64 + ia_ssa_resources_deemed_from_parents: 25 + ia_ssa_category: IN_HOME_CARE + ia_ssa_ihhrc_monthly_cost: 400 + is_disabled: true + meets_ssi_disability_criteria: true + marital_units: + parent_unit: + members: [parent] + child_unit: + members: [child] + tax_units: + family: + members: [parent, child] + households: + home: + members: [parent, child] + state_code: IA + output: + ia_ssa_eligible: [false, true] diff --git a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.py b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.py index 54fa24bdece..ea217c5be14 100644 --- a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.py +++ b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.py @@ -33,7 +33,8 @@ def formula(person, period, parameters): # includes their deemed income. SSI's allocation gates apply: a child # whose own resources exceed the SSI individual limit, or who fails # SSI's immigration test, is deemed nothing. - personal_assets = person("ssi_countable_resources", period) + person( + personal_assets = person("ssi_countable_resources", period) + deemed_assets = person( "ssi_resources_deemed_from_ineligible_parent", period.first_month ) # SSI financial responsibility rules (42 CFR 435.602): the unit is @@ -57,8 +58,11 @@ def formula(person, period, parameters): income = marital_unit.sum( personal_income - medical_expenses - parental_expense_share ) - # Inputs are stored as float32, so compare whole cents with the limit. - assets = round_to_cents(marital_unit.sum(personal_assets)) + # Balances are float32 inputs, so round their sum to cents. The SSI + # parental share is an equal division with no rounding rule (SI + # 01330.200 B, 416.1202(b)(1)), so it is added unrounded. + own_assets = round_to_cents(marital_unit.sum(personal_assets)) + assets = own_assets + marital_unit.sum(deemed_assets) ma = parameters(period).gov.hhs.medicaid mn = ma.eligibility.categories.medically_needy is_joint = marital_unit.nb_persons() == 2 diff --git a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_eligible.py b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_eligible.py index c04e6fad062..56bdd0255ee 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_eligible.py +++ b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_eligible.py @@ -43,9 +43,13 @@ def formula(person, period, parameters): community | person("ia_ssa_rcf_couple_budget", period) ) resource = where(couple, person.marital_unit.sum(resource), resource) - resource = resource + person("ia_ssa_resources_deemed_from_parents", period) - # Inputs are stored as float32, so compare whole cents with the limit. - resource = round_to_cents(resource) + # Balances are float32 inputs, so round their sum to cents. The + # parental share divides the excess as SSI does, and SSI sets no rule + # for rounding a share (SI 01330.200 B, 416.1202(b)(1)), so it is + # added unrounded. + resource = round_to_cents(resource) + person( + "ia_ssa_resources_deemed_from_parents", period + ) # 6-B Resources prints "$2,000 for an individual" and "$3,000 for a # married couple living together" under 20 CFR 416.1202; these equal # the SSI limits of 20 CFR 416.1205, so the federal parameters are used From 7a72caaa57a508ce07591cfc06c4cd96741991af Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 05:54:30 +0000 Subject: [PATCH 09/12] Compare own resources plus an exact share in whole cents Keeping the parental share unrounded left two float errors that decide totals at the limit. Float64 addition put 1,983.90 + 48.30 / 3 at 2,000.0000000000002, which removed an at-limit child from the division and raised the siblings' shares. And core stores the share as float32: a share of 100.01 reads back as 100.010002, so a child with 1,899.99 failed a $2,000 limit that main and the previous head passed. The deeming loops now test divisor x own + excess <= divisor x limit in whole cents, and store each share as the largest float32 at or below it, so own resources plus the share read back stay within any limit the exact total meets. The share is still never rounded to cents. Iowa's loop also rounds the deemors' total and the child's own balances, which it summed in float32 without rounding. New cases: a whole-cent share at the limit (SSI, medically needy and Iowa), 16.10 shares that keep an at-limit child in the division (SSI and Iowa), a parent's three-account split at the Iowa allowance, and an Iowa child whose three accounts plus the share total the limit. Co-Authored-By: Claude Opus 5.5 --- .../is_medically_needy_for_medicaid.yaml | 38 +++ ...sources_deemed_from_ineligible_parent.yaml | 82 +++++ .../ia_ssa_resources_deemed_from_parents.yaml | 299 ++++++++++++++++++ policyengine_us/tools/general.py | 34 ++ ...resources_deemed_from_ineligible_parent.py | 14 +- .../ia_ssa_resources_deemed_from_parents.py | 17 +- 6 files changed, 475 insertions(+), 9 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.yaml index ca07572b347..7331422ee87 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/is_medically_needy_for_medicaid.yaml @@ -1118,3 +1118,41 @@ ssi_resources_deemed_from_ineligible_parent: 2026-01: [0, 25, 25, 25, 25, 25] is_medically_needy_for_medicaid: [false, true, false, false, false, false] + +# The parent's 2,100.01 is 100.01 over the SSI individual allowance, all +# deemed to the only child, whose own income is too high for SSI. The +# child's 1,899.99 plus 100.01 is exactly 2,000.00, at the limit. +- name: Case 39, a disabled child's whole-cent parental share that reaches the asset limit exactly qualifies. + period: 2026 + input: + people: + parent: + age: 40 + is_tax_unit_head: true + is_parent: true + employment_income_before_lsr: 80_000 + bank_account_assets: 2_100.01 + child1: + age: 8 + is_tax_unit_dependent: true + is_ssi_disabled: true + ssi_countable_income: 21_358 + medicaid_medically_needy_medical_expenses: 16_000 + is_optional_senior_or_disabled_for_medicaid: false + bank_account_assets: 1_899.99 + tax_units: + tax_unit: + members: [parent, child1] + marital_units: + parent_unit: + members: [parent] + child1_unit: + members: [child1] + households: + household: + members: [parent, child1] + state_code: MA + output: + ssi_resources_deemed_from_ineligible_parent: + 2026-01: [0, 100.01] + is_medically_needy_for_medicaid: [false, true] diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml index 3e2b151e86b..347927b3a52 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml @@ -617,6 +617,88 @@ ssi_resources_deemed_from_ineligible_parent: [0, 25, 25, 25, 25] meets_ssi_resource_test: [false, true, true, true, true] +# 20 CFR 416.1202(b)(1): the parent's 2,100.01 is 100.01 over the +# individual limit, all deemed to the only child, whose 1,899.99 brings the +# total to exactly 2,000.00. Stored as the nearest float32, the 100.01 share +# would read back as 100.010002 and push that total over the limit. +- name: A whole-cent parental share that brings a child exactly to the limit passes + period: 2026-01 + input: + people: + parent: + age: 40 + is_ssi_aged_blind_disabled: false + is_tax_unit_head: true + is_parent: true + bank_account_assets: 2_100.01 + child: + age: 16 + is_ssi_aged_blind_disabled: true + is_tax_unit_dependent: true + bank_account_assets: 1_899.99 + tax_units: + tax_unit: + members: [parent, child] + marital_units: + parent_unit: + members: [parent] + child_unit: + members: [child] + households: + household: + members: [parent, child] + output: + ssi_resources_deemed_from_ineligible_parent: [0, 100.01] + meets_ssi_resource_test: [false, true] + +# SI 01330.200 B: the parent's 2,048.30 is 48.30 over the individual limit, +# which divides among three children as 16.10 each. The first child's +# 1,983.90 plus 16.10 is exactly 2,000.00, so all three stay in the division +# and pass. In float64, 1,983.90 + 48.30 / 3 is 2,000.0000000000002, which +# would remove the first child and raise the others' shares. +- name: A child whom a share reaches exactly to the limit stays in the division + period: 2026-01 + input: + people: + parent: + age: 40 + is_ssi_aged_blind_disabled: false + is_tax_unit_head: true + is_parent: true + bank_account_assets: 2_048.30 + child1: + age: 8 + is_ssi_aged_blind_disabled: true + is_tax_unit_dependent: true + bank_account_assets: 1_983.90 + child2: + age: 10 + is_ssi_aged_blind_disabled: true + is_tax_unit_dependent: true + bank_account_assets: 1_980 + child3: + age: 12 + is_ssi_aged_blind_disabled: true + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [parent, child1, child2, child3] + marital_units: + parent_unit: + members: [parent] + child1_unit: + members: [child1] + child2_unit: + members: [child2] + child3_unit: + members: [child3] + households: + household: + members: [parent, child1, child2, child3] + output: + ssi_resources_deemed_from_ineligible_parent: [0, 16.10, 16.10, 16.10] + meets_ssi_resource_test: [false, true, true, true] + # POMS SI 01330.340 White example: parent pool1200 <=couple limit3000; child deemed0. - name: POMS White eligible parent leaves no excess resources to deem period: 2024-01 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/ssa/ia_ssa_resources_deemed_from_parents.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/ssa/ia_ssa_resources_deemed_from_parents.yaml index a4329d445fe..48504beceeb 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/ssa/ia_ssa_resources_deemed_from_parents.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/ssa/ia_ssa_resources_deemed_from_parents.yaml @@ -422,3 +422,302 @@ ia_ssa_resources_deemed_from_parents: [0, 200] ia_ssa_eligible: [false, true] ia_ssa: [0, 400] + +# The parent's 1,805.81 + 244.12 + 50.07 = 2,100.00 is 100 over the one-parent +# allowance; the float32 balances add to 2,100.0002 unless rounded to cents. +# The child's 1,900 plus 100 is exactly the 2,000 limit. +- name: Case 8, a parent's three accounts at the allowance plus 100 deem exactly 100. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: + 2026: 40 + person_id: + 2026: 1 + bank_account_assets: + 2026: 1_805.81 + stock_assets: + 2026: 244.12 + bond_assets: + 2026: 50.07 + person2: + age: + 2026: 10 + person_id: + 2026: 2 + parent_1_id: + 2026: 1 + bank_account_assets: + 2026: 1_900 + ia_ssa_category: IN_HOME_CARE + ia_ssa_ihhrc_monthly_cost: 400 + is_disabled: + 2026: true + meets_ssi_disability_criteria: + 2026: true + marital_units: + person1_unit: + members: [person1] + person2_unit: + members: [person2] + families: + family: + members: [person1, person2] + tax_units: + family: + members: [person1, person2] + spm_units: + unit: + members: [person1, person2] + households: + home: + members: [person1, person2] + state_code: + 2026: IA + output: + ia_ssa_resources_deemed_from_parents: [0, 100] + ia_ssa_eligible: [false, true] + +# The parent's 2,100.01 is 100.01 over the allowance, all deemed to the only +# child, whose 1,899.99 brings the total to exactly 2,000.00. Stored as the +# nearest float32, the share would read back as 100.010002. +- name: Case 9, a whole-cent share that brings a child exactly to the limit leaves them eligible. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: + 2026: 40 + person_id: + 2026: 1 + bank_account_assets: + 2026: 2_100.01 + person2: + age: + 2026: 10 + person_id: + 2026: 2 + parent_1_id: + 2026: 1 + bank_account_assets: + 2026: 1_899.99 + ia_ssa_category: IN_HOME_CARE + ia_ssa_ihhrc_monthly_cost: 400 + is_disabled: + 2026: true + meets_ssi_disability_criteria: + 2026: true + marital_units: + person1_unit: + members: [person1] + person2_unit: + members: [person2] + families: + family: + members: [person1, person2] + tax_units: + family: + members: [person1, person2] + spm_units: + unit: + members: [person1, person2] + households: + home: + members: [person1, person2] + state_code: + 2026: IA + output: + ia_ssa_resources_deemed_from_parents: [0, 100.01] + ia_ssa_eligible: [false, true] + +# The parent's 100 of excess divides among four children as 25 each. The +# first child's 178.57 + 1,522.79 + 273.64 = 1,975.00 plus 25 is 2,000.00, at +# the limit; the float32 balances add to 1,975.0001 unless rounded to cents, +# which would leave the child out and deem every child 33.33. +- name: Case 10, a child whose three accounts and share total exactly the limit stays in the division. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: + 2026: 40 + person_id: + 2026: 1 + bank_account_assets: + 2026: 2_100 + person2: + age: + 2026: 8 + person_id: + 2026: 2 + parent_1_id: + 2026: 1 + bank_account_assets: + 2026: 178.57 + stock_assets: + 2026: 1_522.79 + bond_assets: + 2026: 273.64 + ia_ssa_category: IN_HOME_CARE + ia_ssa_ihhrc_monthly_cost: 400 + is_disabled: + 2026: true + meets_ssi_disability_criteria: + 2026: true + person3: + age: + 2026: 10 + person_id: + 2026: 3 + parent_1_id: + 2026: 1 + ia_ssa_category: IN_HOME_CARE + ia_ssa_ihhrc_monthly_cost: 400 + is_disabled: + 2026: true + meets_ssi_disability_criteria: + 2026: true + person4: + age: + 2026: 12 + person_id: + 2026: 4 + parent_1_id: + 2026: 1 + ia_ssa_category: IN_HOME_CARE + ia_ssa_ihhrc_monthly_cost: 400 + is_disabled: + 2026: true + meets_ssi_disability_criteria: + 2026: true + person5: + age: + 2026: 14 + person_id: + 2026: 5 + parent_1_id: + 2026: 1 + ia_ssa_category: IN_HOME_CARE + ia_ssa_ihhrc_monthly_cost: 400 + is_disabled: + 2026: true + meets_ssi_disability_criteria: + 2026: true + marital_units: + person1_unit: + members: [person1] + person2_unit: + members: [person2] + person3_unit: + members: [person3] + person4_unit: + members: [person4] + person5_unit: + members: [person5] + families: + family: + members: [person1, person2, person3, person4, person5] + tax_units: + family: + members: [person1, person2, person3, person4, person5] + spm_units: + unit: + members: [person1, person2, person3, person4, person5] + households: + home: + members: [person1, person2, person3, person4, person5] + state_code: + 2026: IA + output: + ia_ssa_resources_deemed_from_parents: [0, 25, 25, 25, 25] + ia_ssa_eligible: [false, true, true, true, true] + +# The parent's 48.30 of excess divides among three children as 16.10 each. +# The first child's 1,983.90 plus 16.10 is exactly 2,000.00. In float64, +# 1,983.90 + 48.30 / 3 is 2,000.0000000000002, which would leave the first +# child out of the division. +- name: Case 11, a child whom a share reaches exactly to the limit stays in the division. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: + 2026: 40 + person_id: + 2026: 1 + bank_account_assets: + 2026: 2_048.30 + person2: + age: + 2026: 8 + person_id: + 2026: 2 + parent_1_id: + 2026: 1 + bank_account_assets: + 2026: 1_983.90 + ia_ssa_category: IN_HOME_CARE + ia_ssa_ihhrc_monthly_cost: 400 + is_disabled: + 2026: true + meets_ssi_disability_criteria: + 2026: true + person3: + age: + 2026: 10 + person_id: + 2026: 3 + parent_1_id: + 2026: 1 + bank_account_assets: + 2026: 1_980 + ia_ssa_category: IN_HOME_CARE + ia_ssa_ihhrc_monthly_cost: 400 + is_disabled: + 2026: true + meets_ssi_disability_criteria: + 2026: true + person4: + age: + 2026: 12 + person_id: + 2026: 4 + parent_1_id: + 2026: 1 + ia_ssa_category: IN_HOME_CARE + ia_ssa_ihhrc_monthly_cost: 400 + is_disabled: + 2026: true + meets_ssi_disability_criteria: + 2026: true + marital_units: + person1_unit: + members: [person1] + person2_unit: + members: [person2] + person3_unit: + members: [person3] + person4_unit: + members: [person4] + families: + family: + members: [person1, person2, person3, person4] + tax_units: + family: + members: [person1, person2, person3, person4] + spm_units: + unit: + members: [person1, person2, person3, person4] + households: + home: + members: [person1, person2, person3, person4] + state_code: + 2026: IA + output: + ia_ssa_resources_deemed_from_parents: [0, 16.10, 16.10, 16.10] + ia_ssa_eligible: [false, true, true, true] diff --git a/policyengine_us/tools/general.py b/policyengine_us/tools/general.py index 5c031eb37ec..d29c1ae921a 100644 --- a/policyengine_us/tools/general.py +++ b/policyengine_us/tools/general.py @@ -28,6 +28,40 @@ def round_to_cents(amount): return np.round(np.asarray(amount, dtype=np.float64), 2) +def meets_limit_with_share(own, excess, divisor, limit): + """Whether own + excess / divisor <= limit, computed exactly. + + own, excess and limit are whole-cent amounts (round summed balances + first); the share excess / divisor need not be. Float64 addition can put + a total at the limit a hair above it (1,983.90 + 48.30 / 3 gives + 2,000.0000000000002), so compare whole cents instead: + divisor x own + excess <= divisor x limit. + """ + own_cents = np.rint(np.asarray(own, dtype=np.float64) * 100).astype(np.int64) + excess_cents = np.rint(np.asarray(excess, dtype=np.float64) * 100).astype(np.int64) + limit_cents = np.rint(np.asarray(limit, dtype=np.float64) * 100).astype(np.int64) + divisor = np.asarray(divisor).astype(np.int64) + return excess_cents <= divisor * (limit_cents - own_cents) + + +def float32_at_most(amount): + """The largest float32 at or below each amount. + + Float variables are stored as float32, and the nearest float32 can lie + above the amount: a share of 100.01 is stored as 100.010002, so own + resources of 1,899.99 plus that share exceed a 2,000 limit that the + exact total meets. Storing the float32 at or below the amount never + fails a total that meets the limit. A total over a whole-cent limit + exceeds it by at least a cent divided by the share's divisor, which is + more than float32's step for any share below 2**11 dollars and divisor + below 80, so there the comparison stays exact both ways. + """ + amount = np.asarray(amount, dtype=np.float64) + nearest = amount.astype(np.float32) + below = np.nextafter(nearest, np.float32(-np.inf)) + return np.where(nearest.astype(np.float64) > amount, below, nearest) + + def has_input_for_period(simulation, variable, period): """Whether the visible array for this exact period was set as an input. diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.py index 28334d22c0a..c146f90f560 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.py @@ -152,16 +152,22 @@ def formula(person, period, parameters): pool_count = int(np.max(pool, initial=-1)) + 1 for _ in range(rounds): count = np.bincount(pool[active & income_eligible], minlength=pool_count) + divisor = max_(1, count[pool]) # The share is a division, not a sum of balances, so it stays # exact: SI 01330.200 B says to 'equally divide the value of the # deemed resources among those children', and 416.1202(b)(1) sets - # no rule for rounding a child's share. + # no rule for rounding a child's share. Own resources and the + # excess are whole cents, so test own + share in whole cents. # https://secure.ssa.gov/poms.nsf/lnx/0501330200 # https://www.law.cornell.edu/cfr/text/20/416.1202 - share = excess / max_(1, count[pool]) + share = excess / divisor deemed = where(active, share, deemed) - remaining = active & (own_resources + share <= p.individual) + remaining = active & meets_limit_with_share( + own_resources, excess, divisor, p.individual + ) if np.array_equal(active, remaining): break active = remaining - return deemed + # Stored as float32, a share must not round up: own resources plus + # the share read back would then exceed a limit the exact total meets. + return float32_at_most(deemed) diff --git a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_resources_deemed_from_parents.py b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_resources_deemed_from_parents.py index 7ffa7cf73c7..312f25dec08 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_resources_deemed_from_parents.py +++ b/policyengine_us/variables/gov/states/ia/hhs/ssa/ia_ssa_resources_deemed_from_parents.py @@ -53,21 +53,28 @@ def formula(person, period, parameters): person, period, resources ) allowance = where(parent_count > 1, limit.couple, limit.individual) - excess = max_(parental_resources - allowance, 0) + # Balances are float32 inputs, so round the deemors' total and the + # child's own balances to cents, as SSI's deeming does. The share + # stays exact, so own + share is tested in whole cents. + excess = max_(round_to_cents(parental_resources) - allowance, 0) child = person("is_ssi_resource_deeming_child", period) ihhrc = person("ia_ssa_nonfinancial_eligible", period) & ( person("ia_ssa_ihhrc_amount", period) > 0 ) - own = person("ssi_countable_resources", period.this_year) + own = round_to_cents(person("ssi_countable_resources", period.this_year)) eligible = child & (receives_ssi | ihhrc) & (own <= limit.individual) pool_count = int(np.max(pool, initial=-1)) + 1 share = excess # Each round removes at least one child or leaves the result stable. for _ in range(int(np.max(person.household.nb_persons(), initial=0)) + 1): count = np.bincount(pool[eligible], minlength=pool_count) - share = excess / max_(count[pool], 1) - remaining = eligible & (own + share <= limit.individual) + divisor = max_(count[pool], 1) + share = excess / divisor + remaining = eligible & meets_limit_with_share( + own, excess, divisor, limit.individual + ) if np.array_equal(remaining, eligible): break eligible = remaining - return child * share + # Stored as float32, a share must not round up past the exact total. + return float32_at_most(child * share) From 26d99e56a36f5bedd3f900d12dd8d99a395269be Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 05:58:06 +0000 Subject: [PATCH 10/12] Say in the changelog that a parental deeming share is compared exactly Co-Authored-By: Claude Opus 5.5 --- changelog.d/round-summed-limits.fixed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/round-summed-limits.fixed.md b/changelog.d/round-summed-limits.fixed.md index 548cceea1c7..a78a2e3c648 100644 --- a/changelog.d/round-summed-limits.fixed.md +++ b/changelog.d/round-summed-limits.fixed.md @@ -1 +1 @@ -Round summed resources to whole cents before SSI, SNAP, Medicaid, Medicare Savings Program and state asset tests compare them with their dollar limits, so balances that add to exactly a limit pass or fail as the total does. +Round summed resources to whole cents before SSI, SNAP, Medicaid, Medicare Savings Program and state asset tests compare them with their dollar limits, so balances that add to exactly a limit pass or fail as the total does. A child's share of parental resources stays unrounded and is compared exactly. From cc8622208bc221cb82bfea8788d84a2918f7c7e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 06:15:13 +0000 Subject: [PATCH 11/12] Pin the rounding of a child's own balances in the SSI deeming loop The whole-cent comparison converts own resources to cents itself, so no test caught dropping the loop's own-resource rounding. The loop still needs it to decide which children are deemed at all: a child whose balances total exactly $2,000 would add to 2,000.0001, be left out of the deeming, and pass on their own resources. This case expects the child to be deemed the parent's 100 of excess and fail. Co-Authored-By: Claude Opus 5.5 --- ...sources_deemed_from_ineligible_parent.yaml | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml index 347927b3a52..6755de7315b 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml @@ -699,6 +699,43 @@ ssi_resources_deemed_from_ineligible_parent: [0, 16.10, 16.10, 16.10] meets_ssi_resource_test: [false, true, true, true] +# 20 CFR 416.1202(b)(1): the child's own 178.57 + 1,547.79 + 273.64 = +# 2,000.00 is at the individual limit, so the child is deemed the parent's +# 100 of excess and fails at 2,100.00. The float32 balances add to +# 2,000.0001 unless rounded to cents, which would leave the child out of the +# deeming and pass them on their own 2,000.00. +- name: A child whose own accounts total exactly the limit is still deemed the parental excess + period: 2026-01 + input: + people: + parent: + age: 40 + is_ssi_aged_blind_disabled: false + is_tax_unit_head: true + is_parent: true + bank_account_assets: 2_100 + child: + age: 16 + is_ssi_aged_blind_disabled: true + is_tax_unit_dependent: true + bank_account_assets: 178.57 + stock_assets: 1_547.79 + bond_assets: 273.64 + tax_units: + tax_unit: + members: [parent, child] + marital_units: + parent_unit: + members: [parent] + child_unit: + members: [child] + households: + household: + members: [parent, child] + output: + ssi_resources_deemed_from_ineligible_parent: [0, 100] + meets_ssi_resource_test: [false, false] + # POMS SI 01330.340 White example: parent pool1200 <=couple limit3000; child deemed0. - name: POMS White eligible parent leaves no excess resources to deem period: 2024-01 From 122d8fd961580dc5995c79b2f3e3a936f6ab56a3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 3 Nov 2026 17:46:29 -0500 Subject: [PATCH 12/12] Correct the float64 example in the exact-share comments 1,983.90 + 48.30 / 3 is exactly 2,000.0 in float64. The drift comes from forming the excess as 2,048.30 - 2,000, which gives 48.30000000000018; 1,983.90 plus a third of that gives 2,000.0000000000002. Comments only. Co-Authored-By: Claude Opus 5.5 --- .../deemed/ssi_resources_deemed_from_ineligible_parent.yaml | 5 +++-- .../ia/hhs/ssa/ia_ssa_resources_deemed_from_parents.yaml | 5 +++-- policyengine_us/tools/general.py | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml index 6755de7315b..515fd731e6b 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/resources/deemed/ssi_resources_deemed_from_ineligible_parent.yaml @@ -654,8 +654,9 @@ # SI 01330.200 B: the parent's 2,048.30 is 48.30 over the individual limit, # which divides among three children as 16.10 each. The first child's # 1,983.90 plus 16.10 is exactly 2,000.00, so all three stay in the division -# and pass. In float64, 1,983.90 + 48.30 / 3 is 2,000.0000000000002, which -# would remove the first child and raise the others' shares. +# and pass. In float64, 2,048.30 - 2,000 gives an excess of 48.30000000000018, +# and 1,983.90 plus a third of it gives 2,000.0000000000002, which would +# remove the first child and raise the others' shares. - name: A child whom a share reaches exactly to the limit stays in the division period: 2026-01 input: diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/ssa/ia_ssa_resources_deemed_from_parents.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/ssa/ia_ssa_resources_deemed_from_parents.yaml index 48504beceeb..f998bf24a8e 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/ssa/ia_ssa_resources_deemed_from_parents.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/ssa/ia_ssa_resources_deemed_from_parents.yaml @@ -638,8 +638,9 @@ # The parent's 48.30 of excess divides among three children as 16.10 each. # The first child's 1,983.90 plus 16.10 is exactly 2,000.00. In float64, -# 1,983.90 + 48.30 / 3 is 2,000.0000000000002, which would leave the first -# child out of the division. +# 2,048.30 - 2,000 gives an excess of 48.30000000000018, and 1,983.90 plus a +# third of it gives 2,000.0000000000002, which would leave the first child +# out of the division. - name: Case 11, a child whom a share reaches exactly to the limit stays in the division. period: 2026-01 absolute_error_margin: 0.01 diff --git a/policyengine_us/tools/general.py b/policyengine_us/tools/general.py index d29c1ae921a..90d9ad38984 100644 --- a/policyengine_us/tools/general.py +++ b/policyengine_us/tools/general.py @@ -32,8 +32,9 @@ def meets_limit_with_share(own, excess, divisor, limit): """Whether own + excess / divisor <= limit, computed exactly. own, excess and limit are whole-cent amounts (round summed balances - first); the share excess / divisor need not be. Float64 addition can put - a total at the limit a hair above it (1,983.90 + 48.30 / 3 gives + first); the share excess / divisor need not be. Float64 arithmetic can + put a total at the limit a hair above it (2,048.30 - 2,000 gives an + excess of 48.30000000000018, and 1,983.90 plus a third of it gives 2,000.0000000000002), so compare whole cents instead: divisor x own + excess <= divisor x limit. """