From f1a94d5f2d863ccf9142b13804ddc45c3db31a5b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 14 May 2026 08:58:37 -0400 Subject: [PATCH 1/8] Fix asset stock period handling --- changelog.d/snap-assets-stock.fixed.md | 1 + .../gov/hhs/tanf/non_cash/asset_limit.yaml | 2 +- .../tanf/non_cash/tx_vehicle_exemption.yaml | 14 ++++++ .../meets_tanf_non_cash_asset_test.yaml | 47 ++++++++++++++++--- .../il_aabd_countable_vehicle_value.yaml | 2 +- .../usda/snap/eligibility/snap_assets.yaml | 14 ++++++ .../pell_grant/head/pell_grant_head_assets.py | 1 + .../pell_grant/pell_grant_countable_assets.py | 1 + .../meets_tanf_non_cash_asset_test.py | 3 ++ .../gov/local/tax/assessed_property_value.py | 1 + .../gov/states/dc/dhs/ccsp/dc_ccsp_assets.py | 1 + .../aabd/asset/il_aabd_countable_assets.py | 1 + .../asset/il_aabd_countable_vehicle_value.py | 5 +- .../hbwd/asset/il_hbwd_countable_assets.py | 1 + .../tcap/eaedc/ma_eaedc_countable_assets.py | 1 + .../gov/usda/snap/eligibility/snap_assets.py | 1 + .../household/assets/bank_account_assets.py | 1 + .../variables/household/assets/bond_assets.py | 1 + .../assets/household_business_assets_debt.py | 1 + .../household_business_assets_equity.py | 1 + .../assets/household_business_assets_value.py | 1 + .../household_other_real_estate_debt.py | 1 + .../household_other_real_estate_equity.py | 1 + .../household_other_real_estate_value.py | 1 + .../assets/household_rental_property_debt.py | 1 + .../household_rental_property_equity.py | 1 + .../assets/household_rental_property_value.py | 1 + .../assets/household_vehicles_debt.py | 1 + .../assets/household_vehicles_equity.py | 1 + .../variables/household/assets/net_worth.py | 1 + .../household/assets/spm_unit_assets.py | 1 + .../household/assets/spm_unit_cash_assets.py | 1 + .../household/assets/stock_assets.py | 1 + .../household/household_vehicle_value.py | 1 + .../expense/tax/taxable_estate_value.py | 1 + .../person/general/personal_property.py | 1 + ...stb_unadjusted_basis_qualified_property.py | 1 + .../unadjusted_basis_qualified_property.py | 1 + 38 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 changelog.d/snap-assets-stock.fixed.md create mode 100644 policyengine_us/parameters/gov/hhs/tanf/non_cash/tx_vehicle_exemption.yaml diff --git a/changelog.d/snap-assets-stock.fixed.md b/changelog.d/snap-assets-stock.fixed.md new file mode 100644 index 00000000000..49d506eeaee --- /dev/null +++ b/changelog.d/snap-assets-stock.fixed.md @@ -0,0 +1 @@ +Fix asset stock variables being divided across months in SNAP categorical eligibility calculations. diff --git a/policyengine_us/parameters/gov/hhs/tanf/non_cash/asset_limit.yaml b/policyengine_us/parameters/gov/hhs/tanf/non_cash/asset_limit.yaml index 92a6c4439f3..d0f578a214a 100644 --- a/policyengine_us/parameters/gov/hhs/tanf/non_cash/asset_limit.yaml +++ b/policyengine_us/parameters/gov/hhs/tanf/non_cash/asset_limit.yaml @@ -100,7 +100,7 @@ RI: # * SC: # * 2015-10-01: .inf TX: - 2015-10-01: 5_000 # Excludes 1 vehicle up to $15,000 & includes excess vehicle value (not modeled). + 2015-10-01: 5_000 # Excludes 1 vehicle up to $15,000 & includes excess vehicle value. UT: 2015-10-01: -.inf # Non-BBCE. VT: diff --git a/policyengine_us/parameters/gov/hhs/tanf/non_cash/tx_vehicle_exemption.yaml b/policyengine_us/parameters/gov/hhs/tanf/non_cash/tx_vehicle_exemption.yaml new file mode 100644 index 00000000000..8aa2ae7c076 --- /dev/null +++ b/policyengine_us/parameters/gov/hhs/tanf/non_cash/tx_vehicle_exemption.yaml @@ -0,0 +1,14 @@ +description: Texas excludes up to this value for one vehicle from the TANF non-cash asset test used for SNAP broad-based categorical eligibility. + +values: + 2015-10-01: 15_000 + +metadata: + unit: currency-USD + period: year + label: Texas TANF non-cash vehicle exemption for SNAP BBCE + reference: + - title: USDA Broad-based Categorical Eligibility + href: https://www.fns.usda.gov/snap/broad-based-categorical-eligibility + - title: USDA ERS SNAP Policy Database (bbce_asset/bbce_a_amt columns, Jan 1996 - Dec 2020) + href: https://www.ers.usda.gov/data-products/snap-policy-data-sets 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 1ca2b5f96ea..1bb406ae2ae 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 @@ -3,19 +3,19 @@ output: meets_tanf_non_cash_asset_test: true -- name: Texas asset test is $5,000, monthly assets below limit +- name: Texas asset test is $5,000, asset stock below limit period: 2022 input: state_code_str: TX - snap_assets: 16_000 # $1,333.33 monthly + snap_assets: 4_000 output: meets_tanf_non_cash_asset_test: true -- name: Texas asset test is $5,000, monthly assets above limit +- name: Texas asset test is $5,000, asset stock above limit period: 2025 input: state_code_str: TX - snap_assets: 61_200 # $5,100 monthly + snap_assets: 5_100 output: meets_tanf_non_cash_asset_test: false @@ -31,7 +31,7 @@ period: 2024 input: state_code_str: IN - snap_assets: 48_000 # $4,000 monthly + snap_assets: 4_000 output: meets_tanf_non_cash_asset_test: true @@ -39,6 +39,41 @@ period: 2024 input: state_code_str: IN - snap_assets: 72_000 # $6,000 monthly + snap_assets: 6_000 output: meets_tanf_non_cash_asset_test: false + +- name: Texas asset test uses annual asset stock in monthly formula + period: 2026 + input: + state_code_str: TX + snap_assets: 15_000 + output: + meets_tanf_non_cash_asset_test: false + +- name: Texas BBCE ignores one vehicle up to $15,000 + period: 2026 + input: + state_code_str: TX + snap_assets: 4_000 + household_vehicles_value: 15_000 + output: + meets_tanf_non_cash_asset_test: true + +- name: Texas BBCE counts excess vehicle value above $15,000 + period: 2026 + input: + state_code_str: TX + snap_assets: 4_000 + household_vehicles_value: 18_000 + output: + meets_tanf_non_cash_asset_test: false + +- name: Non-Texas BBCE asset tests do not use the Texas vehicle rule + period: 2026 + input: + state_code_str: IN + snap_assets: 4_000 + household_vehicles_value: 18_000 + output: + meets_tanf_non_cash_asset_test: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/il/dhs/aabd/asset/il_aabd_countable_vehicle_value.yaml b/policyengine_us/tests/policy/baseline/gov/states/il/dhs/aabd/asset/il_aabd_countable_vehicle_value.yaml index 1d302b03d7a..93562513223 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/il/dhs/aabd/asset/il_aabd_countable_vehicle_value.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/il/dhs/aabd/asset/il_aabd_countable_vehicle_value.yaml @@ -51,4 +51,4 @@ spm_unit_size: 2 state_code: IL output: - il_aabd_countable_vehicle_value: 4_000*12 + il_aabd_countable_vehicle_value: 4_000 diff --git a/policyengine_us/tests/policy/baseline/gov/usda/snap/eligibility/snap_assets.yaml b/policyengine_us/tests/policy/baseline/gov/usda/snap/eligibility/snap_assets.yaml index 28de081931b..970d2191db8 100644 --- a/policyengine_us/tests/policy/baseline/gov/usda/snap/eligibility/snap_assets.yaml +++ b/policyengine_us/tests/policy/baseline/gov/usda/snap/eligibility/snap_assets.yaml @@ -57,3 +57,17 @@ members: [person1, person2] output: snap_assets: 2_000 + +- name: Monthly SNAP asset requests preserve annual asset stocks + period: 2024-01 + input: + people: + person: + bank_account_assets: 1_200 + stock_assets: 600 + bond_assets: 300 + spm_units: + spm_unit: + members: [person] + output: + snap_assets: 2_100 diff --git a/policyengine_us/variables/gov/ed/pell_grant/head/pell_grant_head_assets.py b/policyengine_us/variables/gov/ed/pell_grant/head/pell_grant_head_assets.py index 29c846299e9..6d73cb680af 100644 --- a/policyengine_us/variables/gov/ed/pell_grant/head/pell_grant_head_assets.py +++ b/policyengine_us/variables/gov/ed/pell_grant/head/pell_grant_head_assets.py @@ -5,6 +5,7 @@ class pell_grant_head_assets(Variable): value_type = float entity = TaxUnit unit = USD + quantity_type = STOCK label = "Pell Grant head assets" definition_period = YEAR diff --git a/policyengine_us/variables/gov/ed/pell_grant/pell_grant_countable_assets.py b/policyengine_us/variables/gov/ed/pell_grant/pell_grant_countable_assets.py index f813b615fc0..f1b81b37973 100644 --- a/policyengine_us/variables/gov/ed/pell_grant/pell_grant_countable_assets.py +++ b/policyengine_us/variables/gov/ed/pell_grant/pell_grant_countable_assets.py @@ -6,4 +6,5 @@ class pell_grant_countable_assets(Variable): entity = Person label = "Pell Grant countable assets" unit = USD + quantity_type = STOCK definition_period = YEAR 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 9da59638adb..03e47f40c60 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 @@ -12,5 +12,8 @@ def formula(spm_unit, period, parameters): assets = spm_unit("snap_assets", period) state = spm_unit.household("state_code_str", period) limits = parameters(period).gov.hhs.tanf.non_cash + vehicle_value = spm_unit.household("household_vehicles_value", period) + tx_vehicle_value = max_(vehicle_value - limits.tx_vehicle_exemption, 0) + 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/local/tax/assessed_property_value.py b/policyengine_us/variables/gov/local/tax/assessed_property_value.py index eeb27c65cf3..46842490ae3 100644 --- a/policyengine_us/variables/gov/local/tax/assessed_property_value.py +++ b/policyengine_us/variables/gov/local/tax/assessed_property_value.py @@ -6,5 +6,6 @@ class assessed_property_value(Variable): entity = Person label = "Assessed property value" unit = USD + quantity_type = STOCK documentation = "Total assessed value of property owned by this person." definition_period = YEAR diff --git a/policyengine_us/variables/gov/states/dc/dhs/ccsp/dc_ccsp_assets.py b/policyengine_us/variables/gov/states/dc/dhs/ccsp/dc_ccsp_assets.py index ce590f718e2..67322eff039 100644 --- a/policyengine_us/variables/gov/states/dc/dhs/ccsp/dc_ccsp_assets.py +++ b/policyengine_us/variables/gov/states/dc/dhs/ccsp/dc_ccsp_assets.py @@ -6,6 +6,7 @@ class dc_ccsp_assets(Variable): entity = SPMUnit label = "DC Child Care Subsidy Program (CCSP) asset" definition_period = MONTH + quantity_type = STOCK reference = "https://osse.dc.gov/sites/default/files/dc/sites/osse/publication/attachments/DC%20Child%20Care%20Subsidy%20Program%20Policy%20Manual.pdf#page=12" defined_for = StateCode.DC diff --git a/policyengine_us/variables/gov/states/il/dhs/aabd/asset/il_aabd_countable_assets.py b/policyengine_us/variables/gov/states/il/dhs/aabd/asset/il_aabd_countable_assets.py index 8f91c9e5ad5..cb62aaa3c29 100644 --- a/policyengine_us/variables/gov/states/il/dhs/aabd/asset/il_aabd_countable_assets.py +++ b/policyengine_us/variables/gov/states/il/dhs/aabd/asset/il_aabd_countable_assets.py @@ -5,6 +5,7 @@ class il_aabd_countable_assets(Variable): value_type = float entity = SPMUnit definition_period = MONTH + quantity_type = STOCK label = "Illinois Aid to the Aged, Blind or Disabled (AABD) countable assets" reference = ( "https://www.law.cornell.edu/regulations/illinois/Ill-Admin-Code-tit-89-SS-113.140", diff --git a/policyengine_us/variables/gov/states/il/dhs/aabd/asset/il_aabd_countable_vehicle_value.py b/policyengine_us/variables/gov/states/il/dhs/aabd/asset/il_aabd_countable_vehicle_value.py index bb0c810307f..425c6b14488 100644 --- a/policyengine_us/variables/gov/states/il/dhs/aabd/asset/il_aabd_countable_vehicle_value.py +++ b/policyengine_us/variables/gov/states/il/dhs/aabd/asset/il_aabd_countable_vehicle_value.py @@ -5,6 +5,7 @@ class il_aabd_countable_vehicle_value(Variable): value_type = float entity = SPMUnit definition_period = MONTH + quantity_type = STOCK label = ( "Illinois Aid to the Aged, Blind or Disabled (AABD) countable vehicles value" ) @@ -16,9 +17,7 @@ class il_aabd_countable_vehicle_value(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.il.dhs.aabd.asset.vehicle_exemption vehicle_count = spm_unit.household("household_vehicles_owned", period) - total_vehicle_value = ( - spm_unit.household("household_vehicles_value", period) * MONTHS_IN_YEAR - ) + total_vehicle_value = spm_unit.household("household_vehicles_value", period) avg_vehicle_value = np.zeros_like(vehicle_count) mask = vehicle_count != 0 avg_vehicle_value[mask] = total_vehicle_value[mask] / vehicle_count[mask] diff --git a/policyengine_us/variables/gov/states/il/hfs/hbwd/asset/il_hbwd_countable_assets.py b/policyengine_us/variables/gov/states/il/hfs/hbwd/asset/il_hbwd_countable_assets.py index a8dbb6c5c12..69f333d3c31 100644 --- a/policyengine_us/variables/gov/states/il/hfs/hbwd/asset/il_hbwd_countable_assets.py +++ b/policyengine_us/variables/gov/states/il/hfs/hbwd/asset/il_hbwd_countable_assets.py @@ -7,6 +7,7 @@ class il_hbwd_countable_assets(Variable): entity = Person label = "Illinois Health Benefits for Workers with Disabilities countable assets" definition_period = MONTH + quantity_type = STOCK reference = ( "https://www.law.cornell.edu/regulations/illinois/Ill-Admin-Code-tit-89-SS-120.381", "https://www.law.cornell.edu/regulations/illinois/Ill-Admin-Code-tit-89-SS-120.510", diff --git a/policyengine_us/variables/gov/states/ma/dta/tcap/eaedc/ma_eaedc_countable_assets.py b/policyengine_us/variables/gov/states/ma/dta/tcap/eaedc/ma_eaedc_countable_assets.py index b91ab904ccf..087865786d2 100644 --- a/policyengine_us/variables/gov/states/ma/dta/tcap/eaedc/ma_eaedc_countable_assets.py +++ b/policyengine_us/variables/gov/states/ma/dta/tcap/eaedc/ma_eaedc_countable_assets.py @@ -7,6 +7,7 @@ class ma_eaedc_countable_assets(Variable): label = "Massachusetts EAEDC countable assets" unit = USD definition_period = MONTH + quantity_type = STOCK defined_for = StateCode.MA adds = "gov.states.ma.dta.tcap.eaedc.assets.sources" diff --git a/policyengine_us/variables/gov/usda/snap/eligibility/snap_assets.py b/policyengine_us/variables/gov/usda/snap/eligibility/snap_assets.py index 2b94bcd0b96..fa907d0f662 100644 --- a/policyengine_us/variables/gov/usda/snap/eligibility/snap_assets.py +++ b/policyengine_us/variables/gov/usda/snap/eligibility/snap_assets.py @@ -16,6 +16,7 @@ class snap_assets(Variable): ) label = "SNAP assets" unit = USD + quantity_type = STOCK reference = ( "https://www.law.cornell.edu/uscode/text/7/2014#g", "https://www.law.cornell.edu/cfr/text/7/273.8", diff --git a/policyengine_us/variables/household/assets/bank_account_assets.py b/policyengine_us/variables/household/assets/bank_account_assets.py index 1dbfdbc151e..9d96085b69c 100644 --- a/policyengine_us/variables/household/assets/bank_account_assets.py +++ b/policyengine_us/variables/household/assets/bank_account_assets.py @@ -10,6 +10,7 @@ class bank_account_assets(Variable): "Imputed from SIPP TVAL_BANK." ) unit = USD + quantity_type = STOCK definition_period = YEAR uprating = "gov.bls.cpi.cpi_u" reference = "https://secure.ssa.gov/poms.nsf/lnx/0501140200" diff --git a/policyengine_us/variables/household/assets/bond_assets.py b/policyengine_us/variables/household/assets/bond_assets.py index 4d62b21fc5b..7644bbc5d0f 100644 --- a/policyengine_us/variables/household/assets/bond_assets.py +++ b/policyengine_us/variables/household/assets/bond_assets.py @@ -9,6 +9,7 @@ class bond_assets(Variable): "Value of bonds and government securities. Imputed from SIPP TVAL_BOND." ) unit = USD + quantity_type = STOCK definition_period = YEAR uprating = "gov.bls.cpi.cpi_u" reference = ( diff --git a/policyengine_us/variables/household/assets/household_business_assets_debt.py b/policyengine_us/variables/household/assets/household_business_assets_debt.py index f3949179fe2..a38cea91ec5 100644 --- a/policyengine_us/variables/household/assets/household_business_assets_debt.py +++ b/policyengine_us/variables/household/assets/household_business_assets_debt.py @@ -7,5 +7,6 @@ class household_business_assets_debt(Variable): label = "Business asset debt" documentation = "Debt secured by household business assets." unit = USD + quantity_type = STOCK definition_period = YEAR uprating = "gov.bls.cpi.cpi_u" diff --git a/policyengine_us/variables/household/assets/household_business_assets_equity.py b/policyengine_us/variables/household/assets/household_business_assets_equity.py index 2e87ae835cf..068155709db 100644 --- a/policyengine_us/variables/household/assets/household_business_assets_equity.py +++ b/policyengine_us/variables/household/assets/household_business_assets_equity.py @@ -7,5 +7,6 @@ class household_business_assets_equity(Variable): label = "Business asset equity" documentation = "Net equity in business assets held by the household." unit = USD + quantity_type = STOCK definition_period = YEAR uprating = "gov.bls.cpi.cpi_u" diff --git a/policyengine_us/variables/household/assets/household_business_assets_value.py b/policyengine_us/variables/household/assets/household_business_assets_value.py index 77f6851f29b..59c4c5c3d7d 100644 --- a/policyengine_us/variables/household/assets/household_business_assets_value.py +++ b/policyengine_us/variables/household/assets/household_business_assets_value.py @@ -7,5 +7,6 @@ class household_business_assets_value(Variable): label = "Business asset value" documentation = "Value of business assets held by the household." unit = USD + quantity_type = STOCK definition_period = YEAR uprating = "gov.bls.cpi.cpi_u" diff --git a/policyengine_us/variables/household/assets/household_other_real_estate_debt.py b/policyengine_us/variables/household/assets/household_other_real_estate_debt.py index b16294b6527..b13392bff53 100644 --- a/policyengine_us/variables/household/assets/household_other_real_estate_debt.py +++ b/policyengine_us/variables/household/assets/household_other_real_estate_debt.py @@ -7,5 +7,6 @@ class household_other_real_estate_debt(Variable): label = "Other real estate debt" documentation = "Debt secured by non-homestead real estate held by the household." unit = USD + quantity_type = STOCK definition_period = YEAR uprating = "gov.bls.cpi.cpi_u" diff --git a/policyengine_us/variables/household/assets/household_other_real_estate_equity.py b/policyengine_us/variables/household/assets/household_other_real_estate_equity.py index eb3244abb15..9f9efbc1062 100644 --- a/policyengine_us/variables/household/assets/household_other_real_estate_equity.py +++ b/policyengine_us/variables/household/assets/household_other_real_estate_equity.py @@ -7,5 +7,6 @@ class household_other_real_estate_equity(Variable): label = "Other real estate equity" documentation = "Net equity in non-homestead real estate held by the household." unit = USD + quantity_type = STOCK definition_period = YEAR uprating = "gov.bls.cpi.cpi_u" diff --git a/policyengine_us/variables/household/assets/household_other_real_estate_value.py b/policyengine_us/variables/household/assets/household_other_real_estate_value.py index eec6fc50fb5..faceb72fb5f 100644 --- a/policyengine_us/variables/household/assets/household_other_real_estate_value.py +++ b/policyengine_us/variables/household/assets/household_other_real_estate_value.py @@ -7,5 +7,6 @@ class household_other_real_estate_value(Variable): label = "Other real estate value" documentation = "Value of non-homestead real estate held by the household." unit = USD + quantity_type = STOCK definition_period = YEAR uprating = "gov.bls.cpi.cpi_u" diff --git a/policyengine_us/variables/household/assets/household_rental_property_debt.py b/policyengine_us/variables/household/assets/household_rental_property_debt.py index a969f752b8f..0730fa4d8d3 100644 --- a/policyengine_us/variables/household/assets/household_rental_property_debt.py +++ b/policyengine_us/variables/household/assets/household_rental_property_debt.py @@ -7,5 +7,6 @@ class household_rental_property_debt(Variable): label = "Rental property debt" documentation = "Debt secured by household rental property assets." unit = USD + quantity_type = STOCK definition_period = YEAR uprating = "gov.bls.cpi.cpi_u" diff --git a/policyengine_us/variables/household/assets/household_rental_property_equity.py b/policyengine_us/variables/household/assets/household_rental_property_equity.py index 7229e0b89e8..7d76890b365 100644 --- a/policyengine_us/variables/household/assets/household_rental_property_equity.py +++ b/policyengine_us/variables/household/assets/household_rental_property_equity.py @@ -7,5 +7,6 @@ class household_rental_property_equity(Variable): label = "Rental property equity" documentation = "Net equity in household rental property assets." unit = USD + quantity_type = STOCK definition_period = YEAR uprating = "gov.bls.cpi.cpi_u" diff --git a/policyengine_us/variables/household/assets/household_rental_property_value.py b/policyengine_us/variables/household/assets/household_rental_property_value.py index 9db93b237e7..a1b95f726be 100644 --- a/policyengine_us/variables/household/assets/household_rental_property_value.py +++ b/policyengine_us/variables/household/assets/household_rental_property_value.py @@ -7,5 +7,6 @@ class household_rental_property_value(Variable): label = "Rental property value" documentation = "Value of rental property assets held by the household." unit = USD + quantity_type = STOCK definition_period = YEAR uprating = "gov.bls.cpi.cpi_u" diff --git a/policyengine_us/variables/household/assets/household_vehicles_debt.py b/policyengine_us/variables/household/assets/household_vehicles_debt.py index 47121cd4883..c3ee49e53f0 100644 --- a/policyengine_us/variables/household/assets/household_vehicles_debt.py +++ b/policyengine_us/variables/household/assets/household_vehicles_debt.py @@ -7,5 +7,6 @@ class household_vehicles_debt(Variable): label = "Vehicle debt" documentation = "Outstanding debt secured by household vehicles." unit = USD + quantity_type = STOCK definition_period = YEAR uprating = "gov.bls.cpi.cpi_u" diff --git a/policyengine_us/variables/household/assets/household_vehicles_equity.py b/policyengine_us/variables/household/assets/household_vehicles_equity.py index 5494b42162f..c3226ccb086 100644 --- a/policyengine_us/variables/household/assets/household_vehicles_equity.py +++ b/policyengine_us/variables/household/assets/household_vehicles_equity.py @@ -7,5 +7,6 @@ class household_vehicles_equity(Variable): label = "Vehicle equity" documentation = "Net equity in household vehicles after secured vehicle debt." unit = USD + quantity_type = STOCK definition_period = YEAR uprating = "gov.bls.cpi.cpi_u" diff --git a/policyengine_us/variables/household/assets/net_worth.py b/policyengine_us/variables/household/assets/net_worth.py index 87407e27fc1..7eeb44becde 100644 --- a/policyengine_us/variables/household/assets/net_worth.py +++ b/policyengine_us/variables/household/assets/net_worth.py @@ -6,5 +6,6 @@ class net_worth(Variable): entity = Household label = "net worth" unit = USD + quantity_type = STOCK definition_period = YEAR uprating = "gov.bls.cpi.cpi_u" diff --git a/policyengine_us/variables/household/assets/spm_unit_assets.py b/policyengine_us/variables/household/assets/spm_unit_assets.py index 259394b2731..696d9571015 100644 --- a/policyengine_us/variables/household/assets/spm_unit_assets.py +++ b/policyengine_us/variables/household/assets/spm_unit_assets.py @@ -7,3 +7,4 @@ class spm_unit_assets(Variable): label = "SPM unit assets" definition_period = YEAR unit = USD + quantity_type = STOCK diff --git a/policyengine_us/variables/household/assets/spm_unit_cash_assets.py b/policyengine_us/variables/household/assets/spm_unit_cash_assets.py index fc143cd39e2..d7674c8fc41 100644 --- a/policyengine_us/variables/household/assets/spm_unit_cash_assets.py +++ b/policyengine_us/variables/household/assets/spm_unit_cash_assets.py @@ -7,5 +7,6 @@ class spm_unit_cash_assets(Variable): label = "SPM unit cash assets" definition_period = YEAR unit = USD + quantity_type = STOCK adds = ["bank_account_assets", "stock_assets", "bond_assets"] diff --git a/policyengine_us/variables/household/assets/stock_assets.py b/policyengine_us/variables/household/assets/stock_assets.py index 943e50b252c..1f96b09b357 100644 --- a/policyengine_us/variables/household/assets/stock_assets.py +++ b/policyengine_us/variables/household/assets/stock_assets.py @@ -7,6 +7,7 @@ class stock_assets(Variable): label = "Stock assets" documentation = "Value of stocks and mutual funds. Imputed from SIPP TVAL_STMF." unit = USD + quantity_type = STOCK definition_period = YEAR uprating = "gov.bls.cpi.cpi_u" reference = "https://secure.ssa.gov/poms.nsf/lnx/0501140220" diff --git a/policyengine_us/variables/household/demographic/household/household_vehicle_value.py b/policyengine_us/variables/household/demographic/household/household_vehicle_value.py index c8ca8e0ff9c..9de11cd9c5f 100644 --- a/policyengine_us/variables/household/demographic/household/household_vehicle_value.py +++ b/policyengine_us/variables/household/demographic/household/household_vehicle_value.py @@ -6,3 +6,4 @@ class household_vehicles_value(Variable): entity = Household label = "Value of vehicles owned" definition_period = YEAR + quantity_type = STOCK diff --git a/policyengine_us/variables/household/expense/tax/taxable_estate_value.py b/policyengine_us/variables/household/expense/tax/taxable_estate_value.py index ff124eef8d6..b3137dd7b01 100644 --- a/policyengine_us/variables/household/expense/tax/taxable_estate_value.py +++ b/policyengine_us/variables/household/expense/tax/taxable_estate_value.py @@ -6,5 +6,6 @@ class taxable_estate_value(Variable): entity = Person label = "Taxable estate value" unit = USD + quantity_type = STOCK definition_period = YEAR reference = "https://www.law.cornell.edu/uscode/text/26/2001#b_1" diff --git a/policyengine_us/variables/household/income/person/general/personal_property.py b/policyengine_us/variables/household/income/person/general/personal_property.py index 5f914eb97c3..2482c04985e 100644 --- a/policyengine_us/variables/household/income/person/general/personal_property.py +++ b/policyengine_us/variables/household/income/person/general/personal_property.py @@ -6,4 +6,5 @@ class personal_property(Variable): entity = Person label = "Personal property value" unit = USD + quantity_type = STOCK definition_period = YEAR diff --git a/policyengine_us/variables/household/income/person/self_employment/sstb_unadjusted_basis_qualified_property.py b/policyengine_us/variables/household/income/person/self_employment/sstb_unadjusted_basis_qualified_property.py index 0995258b96c..689d52e1f21 100644 --- a/policyengine_us/variables/household/income/person/self_employment/sstb_unadjusted_basis_qualified_property.py +++ b/policyengine_us/variables/household/income/person/self_employment/sstb_unadjusted_basis_qualified_property.py @@ -6,6 +6,7 @@ class sstb_unadjusted_basis_qualified_property(Variable): entity = Person label = "SSTB allocable UBIA of qualified property" unit = USD + quantity_type = STOCK documentation = ( "Portion of unadjusted_basis_qualified_property allocable to " "specified service trades or businesses for section 199A. Used to " diff --git a/policyengine_us/variables/household/income/person/self_employment/unadjusted_basis_qualified_property.py b/policyengine_us/variables/household/income/person/self_employment/unadjusted_basis_qualified_property.py index d80cd26c07f..54f59f7c94e 100644 --- a/policyengine_us/variables/household/income/person/self_employment/unadjusted_basis_qualified_property.py +++ b/policyengine_us/variables/household/income/person/self_employment/unadjusted_basis_qualified_property.py @@ -6,6 +6,7 @@ class unadjusted_basis_qualified_property(Variable): entity = Person label = "Unadjusted basis for qualified property" unit = USD + quantity_type = STOCK documentation = "Share of unadjusted basis upon acquisition of all property held by qualified pass-through businesses." definition_period = YEAR reference = "https://www.law.cornell.edu/uscode/text/26/199A#b_6" From c225f7f027acd5f6f1e05ec7a48a1570073d4ec0 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 14 May 2026 11:41:30 -0400 Subject: [PATCH 2/8] Periodize Texas SNAP vehicle exemption --- .../hhs/tanf/non_cash/tx_vehicle_exemption.yaml | 5 +++++ .../non_cash/meets_tanf_non_cash_asset_test.yaml | 15 ++++++++++++--- uv.lock | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/policyengine_us/parameters/gov/hhs/tanf/non_cash/tx_vehicle_exemption.yaml b/policyengine_us/parameters/gov/hhs/tanf/non_cash/tx_vehicle_exemption.yaml index 8aa2ae7c076..15385a0b99c 100644 --- a/policyengine_us/parameters/gov/hhs/tanf/non_cash/tx_vehicle_exemption.yaml +++ b/policyengine_us/parameters/gov/hhs/tanf/non_cash/tx_vehicle_exemption.yaml @@ -2,12 +2,17 @@ description: Texas excludes up to this value for one vehicle from the TANF non-c values: 2015-10-01: 15_000 + 2023-09-01: 22_500 metadata: unit: currency-USD period: year label: Texas TANF non-cash vehicle exemption for SNAP BBCE reference: + - title: 88(R) HB 1287 - Enrolled version + href: https://capitol.texas.gov/tlodocs/88R/billtext/html/HB01287F.htm + - title: Texas Works Handbook 24-3, A-1210 General Policy + href: https://www.hhs.texas.gov/sites/default/files/documents/twh_24-3.pdf - title: USDA Broad-based Categorical Eligibility href: https://www.fns.usda.gov/snap/broad-based-categorical-eligibility - title: USDA ERS SNAP Policy Database (bbce_asset/bbce_a_amt columns, Jan 1996 - Dec 2020) 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 1bb406ae2ae..371bb8f606e 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 @@ -51,17 +51,26 @@ output: meets_tanf_non_cash_asset_test: false -- name: Texas BBCE ignores one vehicle up to $15,000 +- name: Texas BBCE ignores one vehicle up to $22,500 from September 2023 period: 2026 input: state_code_str: TX snap_assets: 4_000 - household_vehicles_value: 15_000 + household_vehicles_value: 22_500 output: meets_tanf_non_cash_asset_test: true -- name: Texas BBCE counts excess vehicle value above $15,000 +- name: Texas BBCE counts excess vehicle value above $22,500 from September 2023 period: 2026 + input: + state_code_str: TX + snap_assets: 4_000 + household_vehicles_value: 24_000 + output: + meets_tanf_non_cash_asset_test: false + +- name: Texas BBCE counted excess vehicle value above $15,000 before September 2023 + period: 2022 input: state_code_str: TX snap_assets: 4_000 diff --git a/uv.lock b/uv.lock index afa0dbe9a95..76042ff3b95 100644 --- a/uv.lock +++ b/uv.lock @@ -2974,7 +2974,7 @@ wheels = [ [[package]] name = "policyengine-us" -version = "1.690.0" +version = "1.691.7" source = { editable = "." } dependencies = [ { name = "microdf-python" }, From eb76f42bbe145467efb09a57147c1201c28c60e2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 14 May 2026 13:08:05 -0400 Subject: [PATCH 3/8] Fix vectorized boolean reductions --- .../tests/core/test_vectorized_reductions.py | 64 +++++++++++++++++++ ...is_in_medicaid_medically_needy_category.py | 2 +- .../variables/gov/usda/is_usda_disabled.py | 2 +- 3 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 policyengine_us/tests/core/test_vectorized_reductions.py diff --git a/policyengine_us/tests/core/test_vectorized_reductions.py b/policyengine_us/tests/core/test_vectorized_reductions.py new file mode 100644 index 00000000000..15b99d23c5e --- /dev/null +++ b/policyengine_us/tests/core/test_vectorized_reductions.py @@ -0,0 +1,64 @@ +from policyengine_us import Simulation + + +def _two_person_two_household_situation( + person_a: dict, + person_b: dict, + *, + state: str = "CA", +) -> dict: + return { + "people": { + "a": person_a, + "b": person_b, + }, + "tax_units": { + "tax_unit_a": {"members": ["a"]}, + "tax_unit_b": {"members": ["b"]}, + }, + "spm_units": { + "spm_unit_a": {"members": ["a"]}, + "spm_unit_b": {"members": ["b"]}, + }, + "families": { + "family_a": {"members": ["a"]}, + "family_b": {"members": ["b"]}, + }, + "households": { + "household_a": {"members": ["a"], "state_code": {"2026": state}}, + "household_b": {"members": ["b"], "state_code": {"2026": state}}, + }, + } + + +def test_is_usda_disabled_reduces_per_person_not_across_simulation(): + simulation = Simulation( + situation=_two_person_two_household_situation( + person_a={"age": {"2026": 30}}, + person_b={ + "age": {"2026": 40}, + "social_security_disability": {"2026": 100}, + }, + ) + ) + + assert simulation.calculate("is_usda_disabled", 2026).tolist() == [ + False, + True, + ] + + +def test_medicaid_medically_needy_category_reduces_per_person(): + simulation = Simulation( + situation=_two_person_two_household_situation( + person_a={"age": {"2026": 30}}, + person_b={"age": {"2026": 70}}, + ) + ) + + assert simulation.calculate( + "is_in_medicaid_medically_needy_category", 2026 + ).tolist() == [ + False, + True, + ] diff --git a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/is_in_medicaid_medically_needy_category.py b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/is_in_medicaid_medically_needy_category.py index 36af6d20266..7c0ef4771d2 100644 --- a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/is_in_medicaid_medically_needy_category.py +++ b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/is_in_medicaid_medically_needy_category.py @@ -35,7 +35,7 @@ def formula(person, period, parameters): is_pregnant, is_senior, ] - return np.any( + return np.logical_or.reduce( [ person_in_category & category_is_covered for person_in_category, category_is_covered in zip( diff --git a/policyengine_us/variables/gov/usda/is_usda_disabled.py b/policyengine_us/variables/gov/usda/is_usda_disabled.py index 5111822214e..eaf15235460 100644 --- a/policyengine_us/variables/gov/usda/is_usda_disabled.py +++ b/policyengine_us/variables/gov/usda/is_usda_disabled.py @@ -10,4 +10,4 @@ class is_usda_disabled(Variable): def formula(person, period, parameters): programs = parameters(period).gov.usda.disabled_programs - return np.any([person(program, period) for program in programs]) + return np.logical_or.reduce([person(program, period) for program in programs]) From c1925becc98d3fe5d8d558d9aefcfd88b926f125 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 14 May 2026 13:28:28 -0400 Subject: [PATCH 4/8] Guard vectorized numpy reductions --- .../tests/core/test_vectorized_reductions.py | 58 +++++++++++++++++++ .../irs_gross_income/has_qdiv_or_ltcg.py | 3 +- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/policyengine_us/tests/core/test_vectorized_reductions.py b/policyengine_us/tests/core/test_vectorized_reductions.py index 15b99d23c5e..c19d9fac9af 100644 --- a/policyengine_us/tests/core/test_vectorized_reductions.py +++ b/policyengine_us/tests/core/test_vectorized_reductions.py @@ -1,3 +1,6 @@ +import ast +from pathlib import Path + from policyengine_us import Simulation @@ -62,3 +65,58 @@ def test_medicaid_medically_needy_category_reduces_per_person(): False, True, ] + + +def test_has_qdiv_or_ltcg_reduces_per_tax_unit_not_across_simulation(): + simulation = Simulation( + situation=_two_person_two_household_situation( + person_a={"age": {"2026": 30}}, + person_b={ + "age": {"2026": 40}, + "qualified_dividend_income": {"2026": 100}, + }, + ) + ) + + assert simulation.calculate("has_qdiv_or_ltcg", 2026).tolist() == [ + False, + True, + ] + + +def test_numpy_any_all_outputs_specify_axis_or_stay_in_control_flow(): + variables_dir = Path(__file__).parents[2] / "variables" + offenders = [] + + for path in variables_dir.rglob("*.py"): + tree = ast.parse(path.read_text()) + parents = {} + for parent in ast.walk(tree): + for child in ast.iter_child_nodes(parent): + parents[child] = parent + + for node in ast.walk(tree): + if not ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and node.func.attr in {"any", "all"} + and isinstance(node.func.value, ast.Name) + and node.func.value.id == "np" + ): + continue + + if any(keyword.arg == "axis" for keyword in node.keywords): + continue + + parent = parents.get(node) + in_control_flow_test = False + while parent is not None: + if isinstance(parent, (ast.If, ast.While)): + in_control_flow_test = node in ast.walk(parent.test) + break + parent = parents.get(parent) + + if not in_control_flow_test: + offenders.append(f"{path.relative_to(variables_dir)}:{node.lineno}") + + assert offenders == [] diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/has_qdiv_or_ltcg.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/has_qdiv_or_ltcg.py index 4fa9ec37e4d..2db8bf45bfc 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/has_qdiv_or_ltcg.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/has_qdiv_or_ltcg.py @@ -21,5 +21,6 @@ def formula(tax_unit, period, parameters): [ add(tax_unit, period, [income_source]) > 0 for income_source in INCOME_SOURCES - ] + ], + axis=0, ) From 59d405ea7e117ed26654eb6efb77004bd6572452 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 14 May 2026 13:40:27 -0400 Subject: [PATCH 5/8] Fix additional vectorization edge cases --- .../tx_additional_vehicle_exemption.yaml | 15 +++++++++++++ .../tests/core/test_vectorized_reductions.py | 19 +++++++++++++++++ .../meets_tanf_non_cash_asset_test.yaml | 20 ++++++++++++++++++ .../co/ccap/entry/co_ccap_fpg_eligible.yaml | 10 +++++++++ .../payment/il_aabd_utility_allowance.yaml | 11 ++++++++++ .../demographic/geographic/county/county.yaml | 8 +++++++ .../meets_tanf_non_cash_asset_test.py | 14 +++++++++++-- .../co/ccap/entry/co_ccap_fpg_eligible.py | 16 +++++++------- .../payment/utility/il_utility_allowance.py | 2 +- .../demographic/geographic/county/county.py | 21 +++++++++++++++---- 10 files changed, 120 insertions(+), 16 deletions(-) create mode 100644 policyengine_us/parameters/gov/hhs/tanf/non_cash/tx_additional_vehicle_exemption.yaml diff --git a/policyengine_us/parameters/gov/hhs/tanf/non_cash/tx_additional_vehicle_exemption.yaml b/policyengine_us/parameters/gov/hhs/tanf/non_cash/tx_additional_vehicle_exemption.yaml new file mode 100644 index 00000000000..23460fefb5e --- /dev/null +++ b/policyengine_us/parameters/gov/hhs/tanf/non_cash/tx_additional_vehicle_exemption.yaml @@ -0,0 +1,15 @@ +description: Texas excludes up to this value for each additional vehicle after the first vehicle from the TANF non-cash asset test used for SNAP broad-based categorical eligibility. + +values: + 2015-10-01: 0 + 2023-09-01: 8_700 + +metadata: + unit: currency-USD + period: year + label: Texas TANF non-cash additional vehicle exemption for SNAP BBCE + reference: + - title: 88(R) HB 1287 - Enrolled version + href: https://capitol.texas.gov/tlodocs/88R/billtext/html/HB01287F.htm + - title: Texas Works Handbook 24-3, A-1210 General Policy + href: https://www.hhs.texas.gov/sites/default/files/documents/twh_24-3.pdf diff --git a/policyengine_us/tests/core/test_vectorized_reductions.py b/policyengine_us/tests/core/test_vectorized_reductions.py index c19d9fac9af..daa2ef448e6 100644 --- a/policyengine_us/tests/core/test_vectorized_reductions.py +++ b/policyengine_us/tests/core/test_vectorized_reductions.py @@ -120,3 +120,22 @@ def test_numpy_any_all_outputs_specify_axis_or_stay_in_control_flow(): offenders.append(f"{path.relative_to(variables_dir)}:{node.lineno}") assert offenders == [] + + +def test_tanf_non_cash_asset_test_does_not_mutate_snap_assets(): + simulation = Simulation( + situation=_two_person_two_household_situation( + person_a={"age": {"2026": 30}}, + person_b={"age": {"2026": 40}}, + state="TX", + ) + ) + simulation.set_input("snap_assets", 2026, [4_000, 4_000]) + simulation.set_input("household_vehicles_value", 2026, [24_000, 0]) + + simulation.calculate("meets_tanf_non_cash_asset_test", "2026-01") + + assert simulation.calculate("snap_assets", "2026-01").tolist() == [ + 4_000, + 4_000, + ] 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 371bb8f606e..9a37b0b65a0 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 @@ -69,6 +69,26 @@ output: meets_tanf_non_cash_asset_test: false +- name: Texas BBCE excludes first and additional vehicles from September 2023 + period: 2026 + input: + state_code_str: TX + snap_assets: 4_000 + household_vehicles_value: 31_200 + household_vehicles_owned: 2 + output: + meets_tanf_non_cash_asset_test: true + +- name: Texas BBCE counts excess above first and additional vehicle exemptions + period: 2026 + input: + state_code_str: TX + snap_assets: 4_000 + household_vehicles_value: 32_300 + household_vehicles_owned: 2 + output: + meets_tanf_non_cash_asset_test: false + - name: Texas BBCE counted excess vehicle value above $15,000 before September 2023 period: 2022 input: diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/entry/co_ccap_fpg_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/entry/co_ccap_fpg_eligible.yaml index cea84cdfad1..eab6ece1bb6 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ccap/entry/co_ccap_fpg_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ccap/entry/co_ccap_fpg_eligible.yaml @@ -27,3 +27,13 @@ spm_unit_fpg: 12 output: co_ccap_fpg_eligible: false + +- name: Unknown county fallback does not overwrite valid county rows + period: 2023-01 + input: + state_code_str: [CO, CO] + co_ccap_countable_income: [2.3, 0] + county_str: [BACA_COUNTY_CO, UNKNOWN] + spm_unit_fpg: [12, 12] + output: + co_ccap_fpg_eligible: [false, true] diff --git a/policyengine_us/tests/policy/baseline/gov/states/il/dhs/aabd/payment/il_aabd_utility_allowance.yaml b/policyengine_us/tests/policy/baseline/gov/states/il/dhs/aabd/payment/il_aabd_utility_allowance.yaml index 64d7bb0b153..86b59c6ca2e 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/il/dhs/aabd/payment/il_aabd_utility_allowance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/il/dhs/aabd/payment/il_aabd_utility_allowance.yaml @@ -29,3 +29,14 @@ state_code: IL output: il_aabd_utility_allowance: 18.45 # 9.45 + 9 + +- name: Vectorized utility allowances use elementwise caps + period: 2022-01 + input: + spm_unit_size: [1, 6] + county_str: [COOK_COUNTY_IL, BOND_COUNTY_IL] + water_expense: [60, 0] + coal_expense: [0, 144] + state_code: [IL, IL] + output: + il_aabd_utility_allowance: [3.8, 12] diff --git a/policyengine_us/tests/policy/baseline/household/demographic/geographic/county/county.yaml b/policyengine_us/tests/policy/baseline/household/demographic/geographic/county/county.yaml index 79320157a2c..f2c5ad21f72 100644 --- a/policyengine_us/tests/policy/baseline/household/demographic/geographic/county/county.yaml +++ b/policyengine_us/tests/policy/baseline/household/demographic/geographic/county/county.yaml @@ -40,6 +40,14 @@ output: county: [NASSAU_COUNTY_NY, LOS_ANGELES_COUNTY_CA, WAYNE_COUNTY_MI, CLARK_COUNTY_NV] +- name: Mixed known and missing county FIPS preserves known rows + period: 2025 + input: + county_fips: ["36059", ""] + state_code: [NY, CA] + output: + county: [NASSAU_COUNTY_NY, ALAMEDA_COUNTY_CA] + - name: Only have the state code CA as input, return the first county of that state. period: 2025 input: 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 03e47f40c60..7cfb1a7e15c 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 @@ -13,7 +13,17 @@ def formula(spm_unit, period, parameters): state = spm_unit.household("state_code_str", period) limits = parameters(period).gov.hhs.tanf.non_cash vehicle_value = spm_unit.household("household_vehicles_value", period) - tx_vehicle_value = max_(vehicle_value - limits.tx_vehicle_exemption, 0) - assets += where(state == "TX", tx_vehicle_value, 0) + vehicles_owned = spm_unit.household("household_vehicles_owned", period) + # Preserve the historical one-vehicle assumption when vehicle value is + # provided without a vehicle count. + vehicle_count = max_(vehicles_owned, vehicle_value > 0) + vehicle_exemption = where( + vehicle_count > 0, + limits.tx_vehicle_exemption + + max_(vehicle_count - 1, 0) * limits.tx_additional_vehicle_exemption, + 0, + ) + tx_vehicle_value = max_(vehicle_value - vehicle_exemption, 0) + assets = 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/states/co/ccap/entry/co_ccap_fpg_eligible.py b/policyengine_us/variables/gov/states/co/ccap/entry/co_ccap_fpg_eligible.py index 53720e97d3c..f75c0ea4b1d 100644 --- a/policyengine_us/variables/gov/states/co/ccap/entry/co_ccap_fpg_eligible.py +++ b/policyengine_us/variables/gov/states/co/ccap/entry/co_ccap_fpg_eligible.py @@ -26,16 +26,14 @@ def formula(spm_unit, period, parameters): county = household("county_str", period.this_year) fpg_rate = np.zeros_like(county, dtype=float) mask = state_eligible - - # Current fix for counties not in the dataset. - try: - p.entry.fpg_rate[county[mask]] - except: - county = np.array( - ["DENVER_COUNTY_CO"] * len(county), - ) if mask.any(): - fpg_rate[mask] = p.entry.fpg_rate[county[mask]] + valid_counties = np.array(list(p.entry.fpg_rate._children)) + lookup_county = np.where( + np.isin(county, valid_counties), + county, + "DENVER_COUNTY_CO", + ) + fpg_rate[mask] = p.entry.fpg_rate[lookup_county[mask]] fpg = spm_unit("spm_unit_fpg", period) fpg_limit = np.round(fpg * fpg_rate, 2) meets_income_limit = monthly_gross_income < fpg_limit diff --git a/policyengine_us/variables/gov/states/il/dhs/aabd/payment/utility/il_utility_allowance.py b/policyengine_us/variables/gov/states/il/dhs/aabd/payment/utility/il_utility_allowance.py index aad587c120f..bc26862854c 100644 --- a/policyengine_us/variables/gov/states/il/dhs/aabd/payment/utility/il_utility_allowance.py +++ b/policyengine_us/variables/gov/states/il/dhs/aabd/payment/utility/il_utility_allowance.py @@ -22,7 +22,7 @@ def formula(spm_unit, period, parameters): [ where( spm_unit(expense, period) > 0, - min( + min_( spm_unit(expense, period), p.utility[expense.replace("_expense", "")][area][capped_size], ), diff --git a/policyengine_us/variables/household/demographic/geographic/county/county.py b/policyengine_us/variables/household/demographic/geographic/county/county.py index 3e09ad55eae..7832812dd1f 100644 --- a/policyengine_us/variables/household/demographic/geographic/county/county.py +++ b/policyengine_us/variables/household/demographic/geographic/county/county.py @@ -35,13 +35,26 @@ def formula(household, period, parameters): # First look if county FIPS is provided; if so, map to county name county_fips: "pd.Series[str]" | None = household("county_fips", period) - if not simulation.is_over_dataset and county_fips.all(): + if not simulation.is_over_dataset: COUNTY_FIPS_DATASET: "pd.DataFrame" = load_county_fips_dataset() # Decode FIPS codes county_fips_codes = COUNTY_FIPS_DATASET.set_index("county_fips") - county_name = county_fips_codes.loc[county_fips, "county_name"] - state_code = county_fips_codes.loc[county_fips, "state"] - return map_county_string_to_enum(county_name, state_code) + result = household("first_county_in_state", period) + county_fips = np.asarray(county_fips).astype(str) + known_fips = county_fips != "" + valid_fips = known_fips & np.isin(county_fips, county_fips_codes.index) + if valid_fips.any(): + county_name = county_fips_codes.loc[ + county_fips[valid_fips], + "county_name", + ] + state_code = county_fips_codes.loc[county_fips[valid_fips], "state"] + result = np.array(result, copy=True) + result[valid_fips] = map_county_string_to_enum( + county_name, + state_code, + ).to_numpy() + return result return household("first_county_in_state", period) From 7e6337471b5e17b0f64dba54a6accbe943897c03 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 14 May 2026 14:24:21 -0400 Subject: [PATCH 6/8] Cover vectorized edge case fixes --- .../tests/core/test_vectorized_reductions.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/policyengine_us/tests/core/test_vectorized_reductions.py b/policyengine_us/tests/core/test_vectorized_reductions.py index daa2ef448e6..fd3b12e4859 100644 --- a/policyengine_us/tests/core/test_vectorized_reductions.py +++ b/policyengine_us/tests/core/test_vectorized_reductions.py @@ -1,6 +1,8 @@ import ast from pathlib import Path +import pytest + from policyengine_us import Simulation @@ -139,3 +141,76 @@ def test_tanf_non_cash_asset_test_does_not_mutate_snap_assets(): 4_000, 4_000, ] + + +def test_tanf_non_cash_asset_test_applies_texas_additional_vehicle_exemption(): + simulation = Simulation( + situation=_two_person_two_household_situation( + person_a={"age": {"2026": 30}}, + person_b={"age": {"2026": 40}}, + state="TX", + ) + ) + simulation.set_input("snap_assets", 2026, [4_000, 4_000]) + simulation.set_input("household_vehicles_value", 2026, [31_200, 32_300]) + simulation.set_input("household_vehicles_owned", 2026, [2, 2]) + + assert simulation.calculate( + "meets_tanf_non_cash_asset_test", "2026-01" + ).tolist() == [ + True, + False, + ] + + +def test_county_mixed_known_and_missing_fips_preserves_known_rows(): + simulation = Simulation( + situation=_two_person_two_household_situation( + person_a={"age": {"2025": 30}}, + person_b={"age": {"2025": 40}}, + ) + ) + simulation.set_input("state_code", 2025, ["NY", "CA"]) + simulation.set_input("county_fips", 2025, ["36059", ""]) + + assert simulation.calculate("county_str", 2025).tolist() == [ + "NASSAU_COUNTY_NY", + "ALAMEDA_COUNTY_CA", + ] + + +def test_il_aabd_utility_allowance_uses_elementwise_caps(): + simulation = Simulation( + situation=_two_person_two_household_situation( + person_a={"age": {"2022": 30}}, + person_b={"age": {"2022": 40}}, + state="IL", + ) + ) + simulation.set_input("state_code", 2022, ["IL", "IL"]) + simulation.set_input("county_str", 2022, ["COOK_COUNTY_IL", "BOND_COUNTY_IL"]) + simulation.set_input("water_expense", 2022, [60, 0]) + simulation.set_input("coal_expense", 2022, [0, 144]) + + assert simulation.calculate( + "il_aabd_utility_allowance", "2022-01" + ).tolist() == pytest.approx([3.8, 11.1]) + + +def test_co_ccap_unknown_county_fallback_is_row_specific(): + simulation = Simulation( + situation=_two_person_two_household_situation( + person_a={"age": {"2023": 30}}, + person_b={"age": {"2023": 40}}, + state="CO", + ) + ) + simulation.set_input("state_code", 2023, ["CO", "CO"]) + simulation.set_input("county_str", 2023, ["BACA_COUNTY_CO", "UNKNOWN"]) + simulation.set_input("co_ccap_countable_income", 2023, [30 * 12, 0]) + simulation.set_input("spm_unit_fpg", 2023, [12 * 12, 12 * 12]) + + assert simulation.calculate("co_ccap_fpg_eligible", "2023-01").tolist() == [ + False, + True, + ] From 10f4a2599c65d5edf9b0eeb24810379c642cf656 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 14 May 2026 14:27:46 -0400 Subject: [PATCH 7/8] Cover missing county FIPS fallback --- .../tests/core/test_vectorized_reductions.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/policyengine_us/tests/core/test_vectorized_reductions.py b/policyengine_us/tests/core/test_vectorized_reductions.py index fd3b12e4859..4be7221029a 100644 --- a/policyengine_us/tests/core/test_vectorized_reductions.py +++ b/policyengine_us/tests/core/test_vectorized_reductions.py @@ -179,6 +179,22 @@ def test_county_mixed_known_and_missing_fips_preserves_known_rows(): ] +def test_county_all_missing_fips_uses_state_fallback(): + simulation = Simulation( + situation=_two_person_two_household_situation( + person_a={"age": {"2025": 30}}, + person_b={"age": {"2025": 40}}, + ) + ) + simulation.set_input("state_code", 2025, ["NY", "CA"]) + simulation.set_input("county_fips", 2025, ["", ""]) + + assert simulation.calculate("county_str", 2025).tolist() == [ + "ALBANY_COUNTY_NY", + "ALAMEDA_COUNTY_CA", + ] + + def test_il_aabd_utility_allowance_uses_elementwise_caps(): simulation = Simulation( situation=_two_person_two_household_situation( From efc0af46e903663a3b9c268f01d06d6c6ba5defc Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 14 May 2026 14:37:49 -0400 Subject: [PATCH 8/8] Simplify county fallback control flow --- .../demographic/geographic/county/county.py | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/policyengine_us/variables/household/demographic/geographic/county/county.py b/policyengine_us/variables/household/demographic/geographic/county/county.py index 7832812dd1f..e75c4ced0e3 100644 --- a/policyengine_us/variables/household/demographic/geographic/county/county.py +++ b/policyengine_us/variables/household/demographic/geographic/county/county.py @@ -31,30 +31,28 @@ def formula(household, period, parameters): if len(known_periods) > 0: last_known_period = sorted(known_periods)[-1] return holder.get_array(last_known_period) + return household("first_county_in_state", period) # First look if county FIPS is provided; if so, map to county name county_fips: "pd.Series[str]" | None = household("county_fips", period) - if not simulation.is_over_dataset: - COUNTY_FIPS_DATASET: "pd.DataFrame" = load_county_fips_dataset() - - # Decode FIPS codes - county_fips_codes = COUNTY_FIPS_DATASET.set_index("county_fips") - result = household("first_county_in_state", period) - county_fips = np.asarray(county_fips).astype(str) - known_fips = county_fips != "" - valid_fips = known_fips & np.isin(county_fips, county_fips_codes.index) - if valid_fips.any(): - county_name = county_fips_codes.loc[ - county_fips[valid_fips], - "county_name", - ] - state_code = county_fips_codes.loc[county_fips[valid_fips], "state"] - result = np.array(result, copy=True) - result[valid_fips] = map_county_string_to_enum( - county_name, - state_code, - ).to_numpy() - return result - - return household("first_county_in_state", period) + COUNTY_FIPS_DATASET: "pd.DataFrame" = load_county_fips_dataset() + + # Decode FIPS codes + county_fips_codes = COUNTY_FIPS_DATASET.set_index("county_fips") + result = household("first_county_in_state", period) + county_fips = np.asarray(county_fips).astype(str) + known_fips = county_fips != "" + valid_fips = known_fips & np.isin(county_fips, county_fips_codes.index) + if valid_fips.any(): + county_name = county_fips_codes.loc[ + county_fips[valid_fips], + "county_name", + ] + state_code = county_fips_codes.loc[county_fips[valid_fips], "state"] + result = np.array(result, copy=True) + result[valid_fips] = map_county_string_to_enum( + county_name, + state_code, + ).to_numpy() + return result