From cd7494a28b73438bb2902e2f70e7b65b2089c5a9 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 15:38:31 -0400 Subject: [PATCH 1/6] Guard county-keyed lookups in VT renter credit, MN CCAP, WI Shares, MD CCS defined_for masks a formula's output, but policyengine-core runs the formula over the whole population, so county keys missing from a county-keyed parameter table raise ParameterNotFoundError in multi-household simulations. - vt_renter_credit: mask non-VT and UNKNOWN counties to Addison, and cap the table lookup at household size 8 (new max_household_size parameter), as the Department of Taxes renter credit calculator does for 9+ person households. - mn_ccap_provider_rate, wi_shares_{hourly,monthly}_max_rate, md_ccs_payment_rate: also mask an in-state UNKNOWN county. Co-Authored-By: Claude Opus 5.5 --- changelog.d/fix-county-lookup-guards.fixed.md | 1 + .../credits/renter/max_household_size.yaml | 20 ++++++++++++++++ .../msde/ccs/payment/md_ccs_payment_rate.py | 7 +++++- .../mn/dcyf/ccap/mn_ccap_provider_rate.py | 5 ++-- .../income/credits/renter/vt_renter_credit.py | 23 ++++++++++++------- .../shares/rates/wi_shares_hourly_max_rate.py | 5 ++-- .../rates/wi_shares_monthly_max_rate.py | 5 ++-- 7 files changed, 51 insertions(+), 15 deletions(-) create mode 100644 changelog.d/fix-county-lookup-guards.fixed.md create mode 100644 policyengine_us/parameters/gov/states/vt/tax/income/credits/renter/max_household_size.yaml diff --git a/changelog.d/fix-county-lookup-guards.fixed.md b/changelog.d/fix-county-lookup-guards.fixed.md new file mode 100644 index 00000000000..2bdf7caaf73 --- /dev/null +++ b/changelog.d/fix-county-lookup-guards.fixed.md @@ -0,0 +1 @@ +Stop the Vermont renter credit, Minnesota CCAP provider rate, Wisconsin Shares maximum rates and Maryland CCS payment rate from crashing simulations that include households outside the state, households with an unknown county, or (Vermont) tax units larger than eight people, which now use the eight-person income limits and fair market rent as the Department of Taxes calculator does. diff --git a/policyengine_us/parameters/gov/states/vt/tax/income/credits/renter/max_household_size.yaml b/policyengine_us/parameters/gov/states/vt/tax/income/credits/renter/max_household_size.yaml new file mode 100644 index 00000000000..b60bfdfdef7 --- /dev/null +++ b/policyengine_us/parameters/gov/states/vt/tax/income/credits/renter/max_household_size.yaml @@ -0,0 +1,20 @@ +description: Vermont applies the renter credit income limits and fair market rents for this household size to larger households. +# The income limit and fair market rent tables run through family size 8. The +# Department of Taxes' renter credit calculators (Calculator sheet, cells +# G11-G12) look up IF(Household_Size>8,8,Household_Size) in the income limit +# tables and accept family sizes 1 through 10. Their credit amount lookup (cell +# G13) caps the size at 4, which the fair market rent table already reflects by +# repeating the 4-person value through size 8. +values: + 2021-01-01: 8 +metadata: + unit: person + period: year + label: Vermont renter credit maximum household size + reference: + - title: 2025 Renter Credit Website Calculator (Calculator sheet, cells G11-G13) + href: https://tax.vermont.gov/sites/tax/files/documents/2025%20Renter%20Credit%20Website%20Calculator.xlsx + - title: 2024 Renter Credit Website Calculator (Calculator sheet, cells G11-G13) + href: https://tax.vermont.gov/sites/tax/files/documents/2024%20Renter%20Credit%20Website%20Calculator.xlsx + - title: 2023 Renter Credit Website Calculator (Calculator sheet, cells G11-G13) + href: https://web.archive.org/web/20250526044955/https://tax.vermont.gov/sites/tax/files/documents/2023%20Renter%20Credit%20Website%20Calculator_1.xlsx diff --git a/policyengine_us/variables/gov/states/md/msde/ccs/payment/md_ccs_payment_rate.py b/policyengine_us/variables/gov/states/md/msde/ccs/payment/md_ccs_payment_rate.py index 27c7f18ac00..2cb1e00290a 100644 --- a/policyengine_us/variables/gov/states/md/msde/ccs/payment/md_ccs_payment_rate.py +++ b/policyengine_us/variables/gov/states/md/msde/ccs/payment/md_ccs_payment_rate.py @@ -27,9 +27,14 @@ def formula(person, period, parameters): center_rate = p.formal.licensed_center[region][age_group] * unit_share family_rate = p.formal.licensed_family[region][age_group] * unit_share + # defined_for does not short-circuit vectorized execution, so + # non-Maryland counties are masked before indexing the county-keyed + # informal rate table. The table has no UNKNOWN key either, so a + # Maryland household with an unknown county also falls back to + # Allegany. county = person.household("county_str", period) in_md = person.household("state_code_str", period) == "MD" - safe_county = where(in_md, county, "ALLEGANY_COUNTY_MD") + safe_county = where(in_md & (county != "UNKNOWN"), county, "ALLEGANY_COUNTY_MD") informal_rate = p.informal.rates[safe_county][age_group] * unit_share return select( diff --git a/policyengine_us/variables/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.py b/policyengine_us/variables/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.py index a7117dfac0f..328cadd94b2 100644 --- a/policyengine_us/variables/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.py +++ b/policyengine_us/variables/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.py @@ -29,10 +29,11 @@ def formula(person, period, parameters): # defined_for filters output but does not short-circuit vectorized # execution, so mask on Minnesota before indexing county_str; non-MN # county strings are absent from the rate tables and would crash the - # lookup. + # lookup. The rate tables have no UNKNOWN key either, so a Minnesota + # household with an unknown county also falls back to Hennepin. county = person.household("county_str", period.this_year) in_mn = person.household("state_code_str", period.this_year) == "MN" - county_key = where(in_mn, county, "HENNEPIN_COUNTY_MN") + county_key = where(in_mn & (county != "UNKNOWN"), county, "HENNEPIN_COUNTY_MN") # Centers and license-exempt programs (paid at the center rate per # section 9.9) use the child care center column; family child care diff --git a/policyengine_us/variables/gov/states/vt/tax/income/credits/renter/vt_renter_credit.py b/policyengine_us/variables/gov/states/vt/tax/income/credits/renter/vt_renter_credit.py index c8fd1ec7368..01a5bb60c2e 100644 --- a/policyengine_us/variables/gov/states/vt/tax/income/credits/renter/vt_renter_credit.py +++ b/policyengine_us/variables/gov/states/vt/tax/income/credits/renter/vt_renter_credit.py @@ -17,17 +17,24 @@ class vt_renter_credit(Variable): def formula(tax_unit, period, parameters): p = parameters(period).gov.states.vt.tax.income.credits.renter - # get tax unit size and county. + # The tables run through family size 8; larger households use the + # 8-person values, as in the Department of Taxes' renter credit + # calculator. tax_unit_size = tax_unit("tax_unit_size", period) - county = tax_unit.household("county", period) + size = min_(tax_unit_size, p.max_household_size).astype(int) + # defined_for masks the output but does not stop the formula from + # running over every household, so non-Vermont and unknown counties + # are masked to a Vermont county before indexing the county-keyed + # tables, which would otherwise raise ParameterNotFoundError. + county = tax_unit.household("county_str", period) + in_vt = tax_unit.household("state_code_str", period) == "VT" + safe_county = where(in_vt & (county != "UNKNOWN"), county, "ADDISON_COUNTY_VT") # locate the values by family size and county - full_credit_income_limit = p.income_limit_ami.thirty_percent[tax_unit_size][ - county + full_credit_income_limit = p.income_limit_ami.thirty_percent[size][safe_county] + partial_credit_income_limit = p.income_limit_ami.fifty_percent[size][ + safe_county ] - partial_credit_income_limit = p.income_limit_ami.fifty_percent[tax_unit_size][ - county - ] - fmr = p.fair_market_rent[tax_unit_size][county] + fmr = p.fair_market_rent[size][safe_county] base_credit_amount = fmr * MONTHS_IN_YEAR * p.fmr_rate # Compute what the spreadsheet calls the "percent rebate claimable" diff --git a/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.py b/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.py index 71934ec5349..7ed8a05909b 100644 --- a/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.py +++ b/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.py @@ -23,9 +23,10 @@ def formula(person, period, parameters): county = person.household("county_str", period) # defined_for does not short-circuit vectorized execution, so # non-Wisconsin counties are masked before indexing the county-keyed - # rate tables. + # rate tables. The tables have no UNKNOWN key either, so a Wisconsin + # household with an unknown county also falls back to Adams. in_wi = person.household("state_code_str", period) == "WI" - safe_county = where(in_wi, county, "ADAMS_COUNTY_WI") + safe_county = where(in_wi & (county != "UNKNOWN"), county, "ADAMS_COUNTY_WI") licensed_group_rate = p.licensed_group.hourly[age_group][safe_county] licensed_family_rate = p.licensed_family.hourly[age_group][safe_county] # Certified hourly maximums are 90% of the licensed family hourly diff --git a/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.py b/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.py index 9c049c6cd54..98fbe3b04d1 100644 --- a/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.py +++ b/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.py @@ -26,9 +26,10 @@ def formula(person, period, parameters): county = person.household("county_str", period) # defined_for does not short-circuit vectorized execution, so # non-Wisconsin counties are masked before indexing the county-keyed - # rate tables. + # rate tables. The tables have no UNKNOWN key either, so a Wisconsin + # household with an unknown county also falls back to Adams. in_wi = person.household("state_code_str", period) == "WI" - safe_county = where(in_wi, county, "ADAMS_COUNTY_WI") + safe_county = where(in_wi & (county != "UNKNOWN"), county, "ADAMS_COUNTY_WI") # The published part-time monthly maximum equals the hourly maximum # times the 131 part-time monthly hours (verified for every county), # so it is derived; full-time monthly maximums are independent table From 626c7a926411f5191a8f70f7c09e4057535139f0 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 15:46:13 -0400 Subject: [PATCH 2/6] Add vectorized county-guard and VT 9+ household tests Each case puts an in-state household, an in-state UNKNOWN-county household and (for the crash guards) an out-of-state household in one simulation, so the defined_for mask is not all false and the formula runs over every county. VT test 12 covers tax units of 8, 9 and 12. All six cases raise ParameterNotFoundError on upstream/main and pass with the fix. Co-Authored-By: Claude Opus 5.5 --- .../md/msde/ccs/md_ccs_payment_rate.yaml | 74 ++++++++++ .../mn/dcyf/ccap/mn_ccap_provider_rate.yaml | 41 ++++++ .../income/credits/rent/vt_renter_credit.yaml | 127 ++++++++++++++++++ .../rates/wi_shares_hourly_max_rate.yaml | 41 ++++++ .../rates/wi_shares_monthly_max_rate.yaml | 42 ++++++ 5 files changed, 325 insertions(+) diff --git a/policyengine_us/tests/policy/baseline/gov/states/md/msde/ccs/md_ccs_payment_rate.yaml b/policyengine_us/tests/policy/baseline/gov/states/md/msde/ccs/md_ccs_payment_rate.yaml index b11d1f6c2ad..faea4f52487 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/md/msde/ccs/md_ccs_payment_rate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/md/msde/ccs/md_ccs_payment_rate.yaml @@ -247,3 +247,77 @@ # Negative hours -> 0 units from the bracket -> select default UNIT_1. # Region X, Center, REGULAR (age 5), UNIT_3 base $381/week x 1/3 = $127/week md_ccs_payment_rate: [0, 127] + +- name: Informal provider, unknown-county and out-of-state households in one simulation with a Montgomery County household. + # defined_for masks the output but the formula still runs over every + # household in the simulation. Before the UNKNOWN guard, the Maryland + # household with an UNKNOWN county reached the county-keyed informal rate + # table and raised ParameterNotFoundError. + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 40_000 + is_tax_unit_head_or_spouse: true + person2: + age: 3 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_day: 2 + md_ccs_provider_type: INFORMAL + person3: + age: 30 + employment_income: 40_000 + is_tax_unit_head_or_spouse: true + person4: + age: 3 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_day: 2 + md_ccs_provider_type: INFORMAL + person5: + age: 30 + employment_income: 40_000 + is_tax_unit_head_or_spouse: true + person6: + age: 3 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_day: 2 + md_ccs_provider_type: INFORMAL + tax_units: + tax_unit1: + members: [person1, person2] + tax_unit2: + members: [person3, person4] + tax_unit3: + members: [person5, person6] + spm_units: + spm_unit1: + members: [person1, person2] + spm_unit2: + members: [person3, person4] + spm_unit3: + members: [person5, person6] + households: + household1: + members: [person1, person2] + state_code: MD + county: MONTGOMERY_COUNTY_MD + household2: + members: [person3, person4] + state_code: MD + county: UNKNOWN + household3: + members: [person5, person6] + state_code: VA + county: FAIRFAX_COUNTY_VA + output: + # person2: informal, Montgomery, REGULAR (age 3), UNIT_1 (2 hours): + # $177 / 3 = $59/week. + # person4: UNKNOWN county falls back to Allegany, informal REGULAR + # UNIT_1: $83 / 3 = $27.67/week. + # person6: Virginia; defined_for zeroes the result. + md_ccs_payment_rate: [0, 59, 0, 27.67, 0, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.yaml b/policyengine_us/tests/policy/baseline/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.yaml index df8137bd3e6..d60ca404ca1 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.yaml @@ -241,3 +241,44 @@ county: MILWAUKEE_COUNTY_WI output: mn_ccap_provider_rate: 0 + +- name: Case 13, unknown-county and out-of-state households in one simulation with a Becker County household. + # defined_for masks the output but the formula still runs over every + # household in the simulation. Before the UNKNOWN guard, the Minnesota + # household with an UNKNOWN county reached the county-keyed rate tables and + # raised ParameterNotFoundError. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0 + childcare_hours_per_week: 45 + mn_ccap_provider_type: FAMILY_CHILD_CARE + person2: + age: 0 + childcare_hours_per_week: 45 + mn_ccap_provider_type: FAMILY_CHILD_CARE + person3: + age: 0 + childcare_hours_per_week: 45 + mn_ccap_provider_type: FAMILY_CHILD_CARE + households: + household1: + members: [person1] + state_code: MN + county: BECKER_COUNTY_MN + household2: + members: [person2] + state_code: MN + county: UNKNOWN + household3: + members: [person3] + state_code: WI + county: MILWAUKEE_COUNTY_WI + output: + # person1: Becker family infant weekly 205 -> 205 * 52 / 12 = 888.33. + # person2: UNKNOWN county falls back to Hennepin, family infant weekly + # 250 -> 250 * 52 / 12 = 1,083.33. + # person3: Wisconsin; defined_for zeroes the result. + mn_ccap_provider_rate: [888.33, 1_083.33, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/vt_renter_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/vt_renter_credit.yaml index bb540781e29..ee58676866c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/vt_renter_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/vt_renter_credit.yaml @@ -145,3 +145,130 @@ state_code: VT output: vt_renter_credit: 1_692 + +- name: test 11, out-of-state and unknown-county households in one simulation with a Chittenden County household. + # defined_for masks the output but the formula still runs over every + # household in the simulation. Before the county guard, the New Hampshire + # county and the UNKNOWN county reached the Vermont county-keyed tables and + # raised ParameterNotFoundError. + period: 2025 + input: + people: + person1: + age: 40 + person2: + age: 40 + person3: + age: 40 + tax_units: + tax_unit1: + members: [person1] + tax_unit_size: 2 + vt_renter_credit_income: 49_370 + rent_is_shared_with_another_tax_unit: false + tax_unit2: + members: [person2] + tax_unit_size: 2 + vt_renter_credit_income: 49_370 + rent_is_shared_with_another_tax_unit: false + tax_unit3: + members: [person3] + tax_unit_size: 2 + vt_renter_credit_income: 44_145 + rent_is_shared_with_another_tax_unit: false + spm_units: + spm_unit1: + members: [person1] + housing_assistance: 0 + spm_unit2: + members: [person2] + housing_assistance: 0 + spm_unit3: + members: [person3] + housing_assistance: 0 + households: + household1: + members: [person1] + state_code: VT + county: CHITTENDEN_COUNTY_VT + household2: + members: [person2] + state_code: NH + county: GRAFTON_COUNTY_NH + household3: + members: [person3] + state_code: VT + county: UNKNOWN + output: + # tax_unit1: Chittenden 2025, size 2: full credit limit 31,200, partial + # credit limit 67,540, fair market rent 1,936. + # (67,540 - 49,370) / (67,540 - 31,200) = 0.5; + # 0.5 * 1,936 * 12 * 0.1 = 1,161.6 -> 1,162. + # tax_unit2: New Hampshire; defined_for zeroes the result. + # tax_unit3: UNKNOWN county falls back to Addison 2025, size 2: limits + # 27,900 and 60,390, fair market rent 1,410. + # (60,390 - 44,145) / (60,390 - 27,900) = 0.5; + # 0.5 * 1,410 * 12 * 0.1 = 846. (Chittenden would give 1,496.) + vt_renter_credit: [1_162, 0, 846] + +- name: test 12, tax units of nine or more use the eight-person income limits and fair market rent. + # The income limit and fair market rent tables run through family size 8. + # The Department of Taxes renter credit calculator looks up + # IF(Household_Size>8,8,Household_Size), so larger households use the + # 8-person values. Before the size cap, sizes 9 and 12 raised + # ParameterNotFoundError even with only Vermont households. + period: 2025 + input: + people: + person1: + age: 40 + person2: + age: 40 + person3: + age: 40 + tax_units: + tax_unit1: + members: [person1] + tax_unit_size: 8 + vt_renter_credit_income: 82_780 + rent_is_shared_with_another_tax_unit: false + tax_unit2: + members: [person2] + tax_unit_size: 9 + vt_renter_credit_income: 82_780 + rent_is_shared_with_another_tax_unit: false + tax_unit3: + members: [person3] + tax_unit_size: 12 + vt_renter_credit_income: 82_780 + rent_is_shared_with_another_tax_unit: false + spm_units: + spm_unit1: + members: [person1] + housing_assistance: 0 + spm_unit2: + members: [person2] + housing_assistance: 0 + spm_unit3: + members: [person3] + housing_assistance: 0 + households: + household1: + members: [person1] + state_code: VT + county: CHITTENDEN_COUNTY_VT + household2: + members: [person2] + state_code: VT + county: CHITTENDEN_COUNTY_VT + household3: + members: [person3] + state_code: VT + county: CHITTENDEN_COUNTY_VT + output: + # Chittenden 2025, size 8: full credit limit 54,150, partial credit limit + # 111,410, fair market rent 2,083. + # (111,410 - 82,780) / (111,410 - 54,150) = 0.5; + # 0.5 * 2,083 * 12 * 0.1 = 1,249.8 -> 1,250 for all three tax units. + # (The 7-person limits, 48,650 and 104,650, would give 976.) + vt_renter_credit: [1_250, 1_250, 1_250] diff --git a/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.yaml b/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.yaml index 258a6dec8c6..644e0310298 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.yaml @@ -145,3 +145,44 @@ output: # defined_for wi_shares_eligible_child zeroes the rate. wi_shares_hourly_max_rate: 0 + +- name: Case 9, unknown-county and out-of-state households in one simulation with a Milwaukee County household. + # defined_for masks the output but the formula still runs over every + # household in the simulation. Before the UNKNOWN guard, the Wisconsin + # household with an UNKNOWN county reached the county-keyed rate tables and + # raised ParameterNotFoundError. + period: 2027-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 3 + childcare_hours_per_week: 20 + wi_shares_provider_type: LICENSED_GROUP + person2: + age: 3 + childcare_hours_per_week: 20 + wi_shares_provider_type: LICENSED_GROUP + person3: + age: 3 + childcare_hours_per_week: 20 + wi_shares_provider_type: LICENSED_GROUP + households: + household1: + members: [person1] + state_code: WI + county: MILWAUKEE_COUNTY_WI + household2: + members: [person2] + state_code: WI + county: UNKNOWN + household3: + members: [person3] + state_code: MN + county: HENNEPIN_COUNTY_MN + output: + # person1: Milwaukee licensed group, ages 2-3: 9.72. + # person2: UNKNOWN county falls back to Adams licensed group, ages 2-3: + # 8.75. + # person3: Minnesota; defined_for zeroes the result. + wi_shares_hourly_max_rate: [9.72, 8.75, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.yaml b/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.yaml index efe743cdc17..2de534f3a0d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.yaml @@ -97,3 +97,45 @@ # Licensed family hourly 8.15 x 131 part-time monthly hours = 1,067.65, # not the certified hourly (7.34) x 131. wi_shares_monthly_max_rate: 1_067.65 + +- name: Case 6, unknown-county and out-of-state households in one simulation with a Milwaukee County household. + # defined_for masks the output but the formula still runs over every + # household in the simulation. Before the UNKNOWN guard, the Wisconsin + # household with an UNKNOWN county reached the county-keyed rate tables and + # raised ParameterNotFoundError. + period: 2027-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 3 + childcare_hours_per_week: 15 + wi_shares_provider_type: LICENSED_GROUP + person2: + age: 3 + childcare_hours_per_week: 15 + wi_shares_provider_type: LICENSED_GROUP + person3: + age: 3 + childcare_hours_per_week: 15 + wi_shares_provider_type: LICENSED_GROUP + households: + household1: + members: [person1] + state_code: WI + county: MILWAUKEE_COUNTY_WI + household2: + members: [person2] + state_code: WI + county: UNKNOWN + household3: + members: [person3] + state_code: MN + county: HENNEPIN_COUNTY_MN + output: + # person1: Milwaukee licensed group hourly 9.72 x 131 part-time monthly + # hours = 1,273.32. + # person2: UNKNOWN county falls back to Adams licensed group hourly 8.75 + # x 131 = 1,146.25. + # person3: Minnesota; defined_for zeroes the result. + wi_shares_monthly_max_rate: [1_273.32, 1_146.25, 0] From 8fde198e1478b19ba93784838ed5eb551c7a55fc Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 16:03:35 -0400 Subject: [PATCH 3/6] Note the VT calculator recalculation behind the new test expectations Co-Authored-By: Claude Opus 5.5 --- .../states/vt/tax/income/credits/rent/vt_renter_credit.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/vt_renter_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/vt_renter_credit.yaml index ee58676866c..20d70f3b392 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/vt_renter_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/vt_renter_credit.yaml @@ -209,6 +209,8 @@ # 27,900 and 60,390, fair market rent 1,410. # (60,390 - 44,145) / (60,390 - 27,900) = 0.5; # 0.5 * 1,410 * 12 * 0.1 = 846. (Chittenden would give 1,496.) + # The Department of Taxes 2025 Renter Credit Website Calculator gives + # 1,162 (Chittenden) and 846 (Addison) for these inputs. vt_renter_credit: [1_162, 0, 846] - name: test 12, tax units of nine or more use the eight-person income limits and fair market rent. @@ -271,4 +273,6 @@ # (111,410 - 82,780) / (111,410 - 54,150) = 0.5; # 0.5 * 2,083 * 12 * 0.1 = 1,249.8 -> 1,250 for all three tax units. # (The 7-person limits, 48,650 and 104,650, would give 976.) + # The Department of Taxes 2025 Renter Credit Website Calculator gives + # 1,250 for family sizes 8, 9 and 10 (its maximum input) and 976 for 7. vt_renter_credit: [1_250, 1_250, 1_250] From 4d47b5f22ae6ead238152567ea59ace4d8db0fd5 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 14 Oct 2026 07:02:24 -0400 Subject: [PATCH 4/6] Cite 2021 and 2022 Vermont renter credit size conventions --- .../income/credits/renter/max_household_size.yaml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/policyengine_us/parameters/gov/states/vt/tax/income/credits/renter/max_household_size.yaml b/policyengine_us/parameters/gov/states/vt/tax/income/credits/renter/max_household_size.yaml index b60bfdfdef7..160b2756f93 100644 --- a/policyengine_us/parameters/gov/states/vt/tax/income/credits/renter/max_household_size.yaml +++ b/policyengine_us/parameters/gov/states/vt/tax/income/credits/renter/max_household_size.yaml @@ -1,10 +1,11 @@ description: Vermont applies the renter credit income limits and fair market rents for this household size to larger households. # The income limit and fair market rent tables run through family size 8. The -# Department of Taxes' renter credit calculators (Calculator sheet, cells -# G11-G12) look up IF(Household_Size>8,8,Household_Size) in the income limit -# tables and accept family sizes 1 through 10. Their credit amount lookup (cell -# G13) caps the size at 4, which the fair market rent table already reflects by -# repeating the 4-person value through size 8. +# Department of Taxes' renter credit calculators use Calculator sheet cells +# G10-G12 in 2021-2022 and G11-G13 in 2023-2025. The income limit lookups use +# IF(Household_Size>8,8,Household_Size), and the calculators accept family sizes +# 1 through 10. Their credit amount lookup caps the size at 4, which the fair +# market rent table already reflects by repeating the 4-person value through +# size 8. values: 2021-01-01: 8 metadata: @@ -18,3 +19,7 @@ metadata: href: https://tax.vermont.gov/sites/tax/files/documents/2024%20Renter%20Credit%20Website%20Calculator.xlsx - title: 2023 Renter Credit Website Calculator (Calculator sheet, cells G11-G13) href: https://web.archive.org/web/20250526044955/https://tax.vermont.gov/sites/tax/files/documents/2023%20Renter%20Credit%20Website%20Calculator_1.xlsx + - title: 2022 Renter Credit Website Calculator (Calculator sheet, cells G10-G12) + href: https://tax.vermont.gov/sites/tax/files/documents/2022%20Renter%20Credit%20Website%20Calculator_1.xlsx + - title: 2021 Renter Credit Website Calculator (Calculator sheet, cells G10-G12) + href: https://tax.vermont.gov/sites/tax/files/documents/2021%20Renter%20Credit%20Website%20Calculator_8.xlsx From 68051002b4c2d12f6fd6c22e04836b34830af474 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 14 Oct 2026 07:23:41 -0400 Subject: [PATCH 5/6] Guard county lookups against mismatched states --- changelog.d/fix-county-lookup-guards.fixed.md | 2 +- .../md/msde/ccs/md_ccs_payment_rate.yaml | 53 +++++++++++++++++++ .../mn/dcyf/ccap/mn_ccap_provider_rate.yaml | 28 ++++++++++ .../income/credits/rent/vt_renter_credit.yaml | 45 ++++++++++++++++ .../rates/wi_shares_hourly_max_rate.yaml | 28 ++++++++++ .../rates/wi_shares_monthly_max_rate.yaml | 29 ++++++++++ .../msde/ccs/payment/md_ccs_payment_rate.py | 11 ++-- .../mn/dcyf/ccap/mn_ccap_provider_rate.py | 13 +++-- .../income/credits/renter/vt_renter_credit.py | 13 +++-- .../shares/rates/wi_shares_hourly_max_rate.py | 14 +++-- .../rates/wi_shares_monthly_max_rate.py | 14 +++-- 11 files changed, 226 insertions(+), 24 deletions(-) diff --git a/changelog.d/fix-county-lookup-guards.fixed.md b/changelog.d/fix-county-lookup-guards.fixed.md index 2bdf7caaf73..819dfe761f2 100644 --- a/changelog.d/fix-county-lookup-guards.fixed.md +++ b/changelog.d/fix-county-lookup-guards.fixed.md @@ -1 +1 @@ -Stop the Vermont renter credit, Minnesota CCAP provider rate, Wisconsin Shares maximum rates and Maryland CCS payment rate from crashing simulations that include households outside the state, households with an unknown county, or (Vermont) tax units larger than eight people, which now use the eight-person income limits and fair market rent as the Department of Taxes calculator does. +Stop the Vermont renter credit from crashing on out-of-state households and on tax units over eight people, and all four programs (Vermont renter credit, Minnesota CCAP, Wisconsin Shares and Maryland CCS) from crashing on an in-state household whose county is UNKNOWN or belongs to another state. Vermont tax units larger than eight people use the eight-person income limits and fair market rent as the Department of Taxes calculators do. diff --git a/policyengine_us/tests/policy/baseline/gov/states/md/msde/ccs/md_ccs_payment_rate.yaml b/policyengine_us/tests/policy/baseline/gov/states/md/msde/ccs/md_ccs_payment_rate.yaml index faea4f52487..fdc14f814e4 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/md/msde/ccs/md_ccs_payment_rate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/md/msde/ccs/md_ccs_payment_rate.yaml @@ -321,3 +321,56 @@ # UNIT_1: $83 / 3 = $27.67/week. # person6: Virginia; defined_for zeroes the result. md_ccs_payment_rate: [0, 59, 0, 27.67, 0, 0] + +- name: Maryland household with a Virginia county uses Allegany in a mixed simulation + # Keep a normal Maryland household present so the vectorized formula runs + # for the household whose county belongs to another state. + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 40_000 + is_tax_unit_head_or_spouse: true + person2: + age: 3 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_day: 2 + md_ccs_provider_type: INFORMAL + person3: + age: 30 + employment_income: 40_000 + is_tax_unit_head_or_spouse: true + person4: + age: 3 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_day: 2 + md_ccs_provider_type: INFORMAL + tax_units: + tax_unit1: + members: [person1, person2] + tax_unit2: + members: [person3, person4] + spm_units: + spm_unit1: + members: [person1, person2] + spm_unit2: + members: [person3, person4] + households: + household1: + members: [person1, person2] + state_code: MD + county: MONTGOMERY_COUNTY_MD + household2: + members: [person3, person4] + state_code: MD + county: FAIRFAX_COUNTY_VA + output: + # person2: informal, Montgomery, REGULAR (age 3), UNIT_1 (2 hours): + # $177 / 3 = $59/week. + # person4: the Virginia county falls back to Allegany, informal REGULAR, + # UNIT_1: $83 / 3 = $27.67/week. + md_ccs_payment_rate: [0, 59, 0, 27.67] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.yaml b/policyengine_us/tests/policy/baseline/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.yaml index d60ca404ca1..46c6b66d219 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.yaml @@ -282,3 +282,31 @@ # 250 -> 250 * 52 / 12 = 1,083.33. # person3: Wisconsin; defined_for zeroes the result. mn_ccap_provider_rate: [888.33, 1_083.33, 0] + +- name: Case 14, Minnesota household with a Wisconsin county falls back to Hennepin alongside a Becker County household. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0 + childcare_hours_per_week: 45 + mn_ccap_provider_type: FAMILY_CHILD_CARE + person2: + age: 0 + childcare_hours_per_week: 45 + mn_ccap_provider_type: FAMILY_CHILD_CARE + households: + household1: + members: [person1] + state_code: MN + county: BECKER_COUNTY_MN + household2: + members: [person2] + state_code: MN + county: MILWAUKEE_COUNTY_WI + output: + # person1: Becker family infant weekly 205 * 52 / 12 = 888.33. + # person2: Wisconsin county falls back to Hennepin, family infant weekly + # 250 * 52 / 12 = 1,083.33. + mn_ccap_provider_rate: [888.33, 1_083.33] diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/vt_renter_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/vt_renter_credit.yaml index 120ca110ca0..f13206f8ebf 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/vt_renter_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/vt_renter_credit.yaml @@ -630,3 +630,48 @@ # Full limit 31,050 = HUD FY2021 extremely low-income limit (PE had 31,040). # Full credit: ROUND(2,083 x 12 x 0.1, 0) = ROUND(2,499.6, 0) = 2,500. vt_renter_credit: 2_500 + +- name: Vermont household with a New Hampshire county uses Addison in a mixed simulation + # An in-state household with another state's county must not reach the + # Vermont county-keyed tables, even with a normal Vermont household present. + period: 2025 + input: + people: + person1: + age: 40 + person2: + age: 40 + tax_units: + tax_unit1: + members: [person1] + tax_unit_size: 2 + vt_renter_credit_income: 49_370 + rent_is_shared_with_another_tax_unit: false + tax_unit2: + members: [person2] + tax_unit_size: 2 + vt_renter_credit_income: 44_145 + rent_is_shared_with_another_tax_unit: false + spm_units: + spm_unit1: + members: [person1] + housing_assistance: 0 + spm_unit2: + members: [person2] + housing_assistance: 0 + households: + household1: + members: [person1] + state_code: VT + county: CHITTENDEN_COUNTY_VT + household2: + members: [person2] + state_code: VT + county: GRAFTON_COUNTY_NH + output: + # tax_unit1: Chittenden, size 2: ROUND(1,936 * 12 * 0.1, 0) = 2,323; + # (67,540 - 49,370) * 2,323 / (67,540 - 31,200) = 1,161.5 -> 1,162. + # tax_unit2: the NH county falls back to Addison, size 2: + # ROUND(1,410 * 12 * 0.1, 0) = 1,692; + # (60,390 - 44,145) * 1,692 / (60,390 - 27,900) = 846. + vt_renter_credit: [1_162, 846] diff --git a/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.yaml b/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.yaml index 644e0310298..acab2636d8b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.yaml @@ -186,3 +186,31 @@ # 8.75. # person3: Minnesota; defined_for zeroes the result. wi_shares_hourly_max_rate: [9.72, 8.75, 0] + +- name: Case 10, Wisconsin household with a Minnesota county falls back to Adams alongside a Milwaukee County household. + period: 2027-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 3 + childcare_hours_per_week: 20 + wi_shares_provider_type: LICENSED_GROUP + person2: + age: 3 + childcare_hours_per_week: 20 + wi_shares_provider_type: LICENSED_GROUP + households: + household1: + members: [person1] + state_code: WI + county: MILWAUKEE_COUNTY_WI + household2: + members: [person2] + state_code: WI + county: HENNEPIN_COUNTY_MN + output: + # person1: Milwaukee licensed group, ages 2-3: 9.72 per hour. + # person2: Minnesota county falls back to Adams licensed group, ages 2-3: + # 8.75 per hour. + wi_shares_hourly_max_rate: [9.72, 8.75] diff --git a/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.yaml b/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.yaml index 2de534f3a0d..8097a677ca0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.yaml @@ -139,3 +139,32 @@ # x 131 = 1,146.25. # person3: Minnesota; defined_for zeroes the result. wi_shares_monthly_max_rate: [1_273.32, 1_146.25, 0] + +- name: Case 7, Wisconsin household with a Minnesota county falls back to Adams alongside a Milwaukee County household. + period: 2027-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 3 + childcare_hours_per_week: 15 + wi_shares_provider_type: LICENSED_GROUP + person2: + age: 3 + childcare_hours_per_week: 15 + wi_shares_provider_type: LICENSED_GROUP + households: + household1: + members: [person1] + state_code: WI + county: MILWAUKEE_COUNTY_WI + household2: + members: [person2] + state_code: WI + county: HENNEPIN_COUNTY_MN + output: + # Part-time monthly hours: ceil(30 * 4.348125) = 131. + # person1: Milwaukee licensed group hourly 9.72 * 131 = 1,273.32. + # person2: Minnesota county falls back to Adams licensed group hourly + # 8.75 * 131 = 1,146.25. + wi_shares_monthly_max_rate: [1_273.32, 1_146.25] diff --git a/policyengine_us/variables/gov/states/md/msde/ccs/payment/md_ccs_payment_rate.py b/policyengine_us/variables/gov/states/md/msde/ccs/payment/md_ccs_payment_rate.py index 2cb1e00290a..18c2f68691c 100644 --- a/policyengine_us/variables/gov/states/md/msde/ccs/payment/md_ccs_payment_rate.py +++ b/policyengine_us/variables/gov/states/md/msde/ccs/payment/md_ccs_payment_rate.py @@ -29,12 +29,15 @@ def formula(person, period, parameters): # defined_for does not short-circuit vectorized execution, so # non-Maryland counties are masked before indexing the county-keyed - # informal rate table. The table has no UNKNOWN key either, so a - # Maryland household with an unknown county also falls back to - # Allegany. + # informal rate table. A Maryland household with an unknown county or + # another state's county also falls back to Allegany. county = person.household("county_str", period) in_md = person.household("state_code_str", period) == "MD" - safe_county = where(in_md & (county != "UNKNOWN"), county, "ALLEGANY_COUNTY_MD") + safe_county = where( + in_md & np.char.endswith(county.astype(str), "_MD"), + county, + "ALLEGANY_COUNTY_MD", + ) informal_rate = p.informal.rates[safe_county][age_group] * unit_share return select( diff --git a/policyengine_us/variables/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.py b/policyengine_us/variables/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.py index 328cadd94b2..7ad03ccb1c2 100644 --- a/policyengine_us/variables/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.py +++ b/policyengine_us/variables/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.py @@ -27,13 +27,16 @@ def formula(person, period, parameters): age_group = person("mn_ccap_age_group", period) rate_unit = person("mn_ccap_rate_unit", period) # defined_for filters output but does not short-circuit vectorized - # execution, so mask on Minnesota before indexing county_str; non-MN - # county strings are absent from the rate tables and would crash the - # lookup. The rate tables have no UNKNOWN key either, so a Minnesota - # household with an unknown county also falls back to Hennepin. + # execution, so only Minnesota households with a Minnesota county can + # index the county-keyed tables. Unknown and mismatched-state counties + # fall back to Hennepin along with non-Minnesota households. county = person.household("county_str", period.this_year) in_mn = person.household("state_code_str", period.this_year) == "MN" - county_key = where(in_mn & (county != "UNKNOWN"), county, "HENNEPIN_COUNTY_MN") + county_key = where( + in_mn & np.char.endswith(county.astype(str), "_MN"), + county, + "HENNEPIN_COUNTY_MN", + ) # Centers and license-exempt programs (paid at the center rate per # section 9.9) use the child care center column; family child care diff --git a/policyengine_us/variables/gov/states/vt/tax/income/credits/renter/vt_renter_credit.py b/policyengine_us/variables/gov/states/vt/tax/income/credits/renter/vt_renter_credit.py index 3a2c16c2235..4a83155f55b 100644 --- a/policyengine_us/variables/gov/states/vt/tax/income/credits/renter/vt_renter_credit.py +++ b/policyengine_us/variables/gov/states/vt/tax/income/credits/renter/vt_renter_credit.py @@ -23,12 +23,17 @@ def formula(tax_unit, period, parameters): tax_unit_size = tax_unit("tax_unit_size", period) size = min_(tax_unit_size, p.max_household_size).astype(int) # defined_for masks the output but does not stop the formula from - # running over every household, so non-Vermont and unknown counties - # are masked to a Vermont county before indexing the county-keyed - # tables, which would otherwise raise ParameterNotFoundError. + # running over every household, so non-Vermont households and Vermont + # households with unknown or another state's counties use Addison + # before indexing the county-keyed tables, which would otherwise raise + # ParameterNotFoundError. county = tax_unit.household("county_str", period) in_vt = tax_unit.household("state_code_str", period) == "VT" - safe_county = where(in_vt & (county != "UNKNOWN"), county, "ADDISON_COUNTY_VT") + safe_county = where( + in_vt & np.char.endswith(county.astype(str), "_VT"), + county, + "ADDISON_COUNTY_VT", + ) # locate the values by family size and county full_credit_income_limit = p.income_limit_ami.thirty_percent[size][safe_county] partial_credit_income_limit = p.income_limit_ami.fifty_percent[size][ diff --git a/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.py b/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.py index 7ed8a05909b..659b7845443 100644 --- a/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.py +++ b/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.py @@ -21,12 +21,16 @@ def formula(person, period, parameters): p = parameters(period).gov.states.wi.dcf.shares.rates age_group = person("wi_shares_age_group", period) county = person.household("county_str", period) - # defined_for does not short-circuit vectorized execution, so - # non-Wisconsin counties are masked before indexing the county-keyed - # rate tables. The tables have no UNKNOWN key either, so a Wisconsin - # household with an unknown county also falls back to Adams. + # defined_for does not short-circuit vectorized execution, so only + # Wisconsin households with a Wisconsin county can index the + # county-keyed rate tables. Unknown and mismatched-state counties + # fall back to Adams along with non-Wisconsin households. in_wi = person.household("state_code_str", period) == "WI" - safe_county = where(in_wi & (county != "UNKNOWN"), county, "ADAMS_COUNTY_WI") + safe_county = where( + in_wi & np.char.endswith(county.astype(str), "_WI"), + county, + "ADAMS_COUNTY_WI", + ) licensed_group_rate = p.licensed_group.hourly[age_group][safe_county] licensed_family_rate = p.licensed_family.hourly[age_group][safe_county] # Certified hourly maximums are 90% of the licensed family hourly diff --git a/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.py b/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.py index 98fbe3b04d1..f7a3ca8013e 100644 --- a/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.py +++ b/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.py @@ -24,12 +24,16 @@ def formula(person, period, parameters): p = parameters(period).gov.states.wi.dcf.shares age_group = person("wi_shares_age_group", period) county = person.household("county_str", period) - # defined_for does not short-circuit vectorized execution, so - # non-Wisconsin counties are masked before indexing the county-keyed - # rate tables. The tables have no UNKNOWN key either, so a Wisconsin - # household with an unknown county also falls back to Adams. + # defined_for does not short-circuit vectorized execution, so only + # Wisconsin households with a Wisconsin county can index the + # county-keyed rate tables. Unknown and mismatched-state counties + # fall back to Adams along with non-Wisconsin households. in_wi = person.household("state_code_str", period) == "WI" - safe_county = where(in_wi & (county != "UNKNOWN"), county, "ADAMS_COUNTY_WI") + safe_county = where( + in_wi & np.char.endswith(county.astype(str), "_WI"), + county, + "ADAMS_COUNTY_WI", + ) # The published part-time monthly maximum equals the hourly maximum # times the 131 part-time monthly hours (verified for every county), # so it is derived; full-time monthly maximums are independent table From 7089303db31d1ef4a1408dc83142b46f309ed5f2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 14 Oct 2026 23:10:43 -0400 Subject: [PATCH 6/6] Fix UTF-8 county state suffix guards --- changelog.d/fix-county-lookup-guards.fixed.md | 2 +- .../md/msde/ccs/md_ccs_payment_rate.yaml | 49 +++++++++++++++++++ .../mn/dcyf/ccap/mn_ccap_provider_rate.yaml | 27 ++++++++++ .../income/credits/rent/vt_renter_credit.yaml | 43 ++++++++++++++++ .../rates/wi_shares_hourly_max_rate.yaml | 27 ++++++++++ .../rates/wi_shares_monthly_max_rate.yaml | 27 ++++++++++ .../tools/geography/county_helpers.py | 20 ++++++++ .../msde/ccs/payment/md_ccs_payment_rate.py | 3 +- .../mn/dcyf/ccap/mn_ccap_provider_rate.py | 3 +- .../income/credits/renter/vt_renter_credit.py | 3 +- .../shares/rates/wi_shares_hourly_max_rate.py | 3 +- .../rates/wi_shares_monthly_max_rate.py | 3 +- 12 files changed, 204 insertions(+), 6 deletions(-) diff --git a/changelog.d/fix-county-lookup-guards.fixed.md b/changelog.d/fix-county-lookup-guards.fixed.md index 819dfe761f2..beb428a1ed6 100644 --- a/changelog.d/fix-county-lookup-guards.fixed.md +++ b/changelog.d/fix-county-lookup-guards.fixed.md @@ -1 +1 @@ -Stop the Vermont renter credit from crashing on out-of-state households and on tax units over eight people, and all four programs (Vermont renter credit, Minnesota CCAP, Wisconsin Shares and Maryland CCS) from crashing on an in-state household whose county is UNKNOWN or belongs to another state. Vermont tax units larger than eight people use the eight-person income limits and fair market rent as the Department of Taxes calculators do. +Stop the Vermont renter credit from crashing on out-of-state households and on tax units over eight people, and all four programs (Vermont renter credit, Minnesota CCAP, Wisconsin Shares and Maryland CCS) from crashing on an in-state household whose county is UNKNOWN or belongs to another state, including counties supplied as UTF-8 bytes. Vermont tax units larger than eight people use the eight-person income limits and fair market rent as the Department of Taxes calculators do. diff --git a/policyengine_us/tests/policy/baseline/gov/states/md/msde/ccs/md_ccs_payment_rate.yaml b/policyengine_us/tests/policy/baseline/gov/states/md/msde/ccs/md_ccs_payment_rate.yaml index fdc14f814e4..1454d0f91a4 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/md/msde/ccs/md_ccs_payment_rate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/md/msde/ccs/md_ccs_payment_rate.yaml @@ -374,3 +374,52 @@ # person4: the Virginia county falls back to Allegany, informal REGULAR, # UNIT_1: $83 / 3 = $27.67/week. md_ccs_payment_rate: [0, 59, 0, 27.67] + +- name: UTF-8 byte counties preserve the Maryland rate in a mixed-state simulation + # !!binary preserves bytes, including the UTF-8 Ñ in DOÑA_ANA_COUNTY_NM. + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 40_000 + is_tax_unit_head_or_spouse: true + person2: + age: 3 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_day: 2 + md_ccs_provider_type: INFORMAL + person3: + age: 30 + employment_income: 40_000 + is_tax_unit_head_or_spouse: true + person4: + age: 3 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_day: 2 + md_ccs_provider_type: INFORMAL + tax_units: + tax_unit1: + members: [person1, person2] + tax_unit2: + members: [person3, person4] + spm_units: + spm_unit1: + members: [person1, person2] + spm_unit2: + members: [person3, person4] + households: + household1: + members: [person1, person2] + state_code: MD + county_str: !!binary TU9OVEdPTUVSWV9DT1VOVFlfTUQ= # MONTGOMERY_COUNTY_MD + household2: + members: [person3, person4] + state_code: NM + county_str: !!binary RE/DkUFfQU5BX0NPVU5UWV9OTQ== # DOÑA_ANA_COUNTY_NM + output: + # Montgomery informal regular UNIT_1: $177 / 3 = $59 per week. + md_ccs_payment_rate: [0, 59, 0, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.yaml b/policyengine_us/tests/policy/baseline/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.yaml index 46c6b66d219..8ba83b3d75d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.yaml @@ -310,3 +310,30 @@ # person2: Wisconsin county falls back to Hennepin, family infant weekly # 250 * 52 / 12 = 1,083.33. mn_ccap_provider_rate: [888.33, 1_083.33] + +- name: UTF-8 byte counties preserve the Minnesota rate in a mixed-state simulation + # !!binary preserves bytes, including the UTF-8 Ñ in DOÑA_ANA_COUNTY_NM. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0 + childcare_hours_per_week: 45 + mn_ccap_provider_type: FAMILY_CHILD_CARE + person2: + age: 0 + childcare_hours_per_week: 45 + mn_ccap_provider_type: FAMILY_CHILD_CARE + households: + household1: + members: [person1] + state_code: MN + county_str: !!binary QkVDS0VSX0NPVU5UWV9NTg== # BECKER_COUNTY_MN + household2: + members: [person2] + state_code: NM + county_str: !!binary RE/DkUFfQU5BX0NPVU5UWV9OTQ== # DOÑA_ANA_COUNTY_NM + output: + # Becker family infant weekly $205 * 52 / 12 = $888.33 per month. + mn_ccap_provider_rate: [888.33, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/vt_renter_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/vt_renter_credit.yaml index f13206f8ebf..cc82ce848c0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/vt_renter_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/vt_renter_credit.yaml @@ -675,3 +675,46 @@ # ROUND(1,410 * 12 * 0.1, 0) = 1,692; # (60,390 - 44,145) * 1,692 / (60,390 - 27,900) = 846. vt_renter_credit: [1_162, 846] + +- name: UTF-8 byte counties preserve the Vermont credit in a mixed-state simulation + # !!binary supplies actual bytes through Core's YAML loader. The second + # county is DOÑA_ANA_COUNTY_NM encoded as UTF-8; an ASCII cast raises before + # defined_for can mask the New Mexico household. + period: 2025 + input: + people: + person1: + age: 40 + person2: + age: 40 + tax_units: + tax_unit1: + members: [person1] + tax_unit_size: 2 + vt_renter_credit_income: 49_370 + rent_is_shared_with_another_tax_unit: false + tax_unit2: + members: [person2] + tax_unit_size: 2 + vt_renter_credit_income: 49_370 + rent_is_shared_with_another_tax_unit: false + spm_units: + spm_unit1: + members: [person1] + housing_assistance: 0 + spm_unit2: + members: [person2] + housing_assistance: 0 + households: + household1: + members: [person1] + state_code: VT + county_str: !!binary Q0hJVFRFTkRFTl9DT1VOVFlfVlQ= # CHITTENDEN_COUNTY_VT + household2: + members: [person2] + state_code: NM + county_str: !!binary RE/DkUFfQU5BX0NPVU5UWV9OTQ== # DOÑA_ANA_COUNTY_NM + output: + # Chittenden, size 2: midpoint of the income limits gives half the + # rounded $2,323 base credit, rounded half up to $1,162. + vt_renter_credit: [1_162, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.yaml b/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.yaml index acab2636d8b..a0d9d81a935 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.yaml @@ -214,3 +214,30 @@ # person2: Minnesota county falls back to Adams licensed group, ages 2-3: # 8.75 per hour. wi_shares_hourly_max_rate: [9.72, 8.75] + +- name: UTF-8 byte counties preserve the Wisconsin hourly rate in a mixed-state simulation + # !!binary preserves bytes, including the UTF-8 Ñ in DOÑA_ANA_COUNTY_NM. + period: 2027-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 3 + childcare_hours_per_week: 20 + wi_shares_provider_type: LICENSED_GROUP + person2: + age: 3 + childcare_hours_per_week: 20 + wi_shares_provider_type: LICENSED_GROUP + households: + household1: + members: [person1] + state_code: WI + county_str: !!binary TUlMV0FVS0VFX0NPVU5UWV9XSQ== # MILWAUKEE_COUNTY_WI + household2: + members: [person2] + state_code: NM + county_str: !!binary RE/DkUFfQU5BX0NPVU5UWV9OTQ== # DOÑA_ANA_COUNTY_NM + output: + # Milwaukee licensed group hourly maximum for ages 2-3: $9.72. + wi_shares_hourly_max_rate: [9.72, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.yaml b/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.yaml index 8097a677ca0..6af08ec56aa 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.yaml @@ -168,3 +168,30 @@ # person2: Minnesota county falls back to Adams licensed group hourly # 8.75 * 131 = 1,146.25. wi_shares_monthly_max_rate: [1_273.32, 1_146.25] + +- name: UTF-8 byte counties preserve the Wisconsin monthly rate in a mixed-state simulation + # !!binary preserves bytes, including the UTF-8 Ñ in DOÑA_ANA_COUNTY_NM. + period: 2027-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 3 + childcare_hours_per_week: 15 + wi_shares_provider_type: LICENSED_GROUP + person2: + age: 3 + childcare_hours_per_week: 15 + wi_shares_provider_type: LICENSED_GROUP + households: + household1: + members: [person1] + state_code: WI + county_str: !!binary TUlMV0FVS0VFX0NPVU5UWV9XSQ== # MILWAUKEE_COUNTY_WI + household2: + members: [person2] + state_code: NM + county_str: !!binary RE/DkUFfQU5BX0NPVU5UWV9OTQ== # DOÑA_ANA_COUNTY_NM + output: + # Milwaukee licensed group $9.72 * 131 part-time hours = $1,273.32. + wi_shares_monthly_max_rate: [1_273.32, 0] diff --git a/policyengine_us/tools/geography/county_helpers.py b/policyengine_us/tools/geography/county_helpers.py index 1a269189fbd..4225b45bfaf 100644 --- a/policyengine_us/tools/geography/county_helpers.py +++ b/policyengine_us/tools/geography/county_helpers.py @@ -14,6 +14,26 @@ COUNTY_FIPS_PACKAGE = "policyengine_us.data" +def county_has_state_suffix(county: np.ndarray, state_code: str) -> np.ndarray: + """Match county suffixes without ASCII-decoding UTF-8 byte inputs. + + Core can store bytes inside object arrays, so match each value with a + suffix of the same type and leave the county lookup keys unchanged. + """ + suffix = f"_{state_code}" + byte_suffix = suffix.encode("utf-8") + return np.fromiter( + ( + value.endswith(byte_suffix) + if isinstance(value, bytes) + else str(value).endswith(suffix) + for value in county + ), + dtype=bool, + count=len(county), + ) + + def _read_county_fips_dataset(dataset_file) -> pd.DataFrame: return pd.read_csv( dataset_file, diff --git a/policyengine_us/variables/gov/states/md/msde/ccs/payment/md_ccs_payment_rate.py b/policyengine_us/variables/gov/states/md/msde/ccs/payment/md_ccs_payment_rate.py index 18c2f68691c..8e8232f5642 100644 --- a/policyengine_us/variables/gov/states/md/msde/ccs/payment/md_ccs_payment_rate.py +++ b/policyengine_us/variables/gov/states/md/msde/ccs/payment/md_ccs_payment_rate.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.tools.geography.county_helpers import county_has_state_suffix from policyengine_us.variables.gov.states.md.msde.ccs.md_ccs_provider_type import ( MDCCSProviderType, ) @@ -34,7 +35,7 @@ def formula(person, period, parameters): county = person.household("county_str", period) in_md = person.household("state_code_str", period) == "MD" safe_county = where( - in_md & np.char.endswith(county.astype(str), "_MD"), + in_md & county_has_state_suffix(county, "MD"), county, "ALLEGANY_COUNTY_MD", ) diff --git a/policyengine_us/variables/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.py b/policyengine_us/variables/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.py index 7ad03ccb1c2..18f2d5455db 100644 --- a/policyengine_us/variables/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.py +++ b/policyengine_us/variables/gov/states/mn/dcyf/ccap/mn_ccap_provider_rate.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.tools.geography.county_helpers import county_has_state_suffix from policyengine_us.variables.gov.states.mn.dcyf.ccap.mn_ccap_provider_type import ( MNCCAPProviderType, ) @@ -33,7 +34,7 @@ def formula(person, period, parameters): county = person.household("county_str", period.this_year) in_mn = person.household("state_code_str", period.this_year) == "MN" county_key = where( - in_mn & np.char.endswith(county.astype(str), "_MN"), + in_mn & county_has_state_suffix(county, "MN"), county, "HENNEPIN_COUNTY_MN", ) diff --git a/policyengine_us/variables/gov/states/vt/tax/income/credits/renter/vt_renter_credit.py b/policyengine_us/variables/gov/states/vt/tax/income/credits/renter/vt_renter_credit.py index 4a83155f55b..871e08590e3 100644 --- a/policyengine_us/variables/gov/states/vt/tax/income/credits/renter/vt_renter_credit.py +++ b/policyengine_us/variables/gov/states/vt/tax/income/credits/renter/vt_renter_credit.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.tools.geography.county_helpers import county_has_state_suffix class vt_renter_credit(Variable): @@ -30,7 +31,7 @@ def formula(tax_unit, period, parameters): county = tax_unit.household("county_str", period) in_vt = tax_unit.household("state_code_str", period) == "VT" safe_county = where( - in_vt & np.char.endswith(county.astype(str), "_VT"), + in_vt & county_has_state_suffix(county, "VT"), county, "ADDISON_COUNTY_VT", ) diff --git a/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.py b/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.py index 659b7845443..524135e5777 100644 --- a/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.py +++ b/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_hourly_max_rate.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.tools.geography.county_helpers import county_has_state_suffix from policyengine_us.variables.gov.states.wi.dcf.shares.rates.wi_shares_provider_type import ( WISharesProviderType, ) @@ -27,7 +28,7 @@ def formula(person, period, parameters): # fall back to Adams along with non-Wisconsin households. in_wi = person.household("state_code_str", period) == "WI" safe_county = where( - in_wi & np.char.endswith(county.astype(str), "_WI"), + in_wi & county_has_state_suffix(county, "WI"), county, "ADAMS_COUNTY_WI", ) diff --git a/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.py b/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.py index f7a3ca8013e..e63432a69b3 100644 --- a/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.py +++ b/policyengine_us/variables/gov/states/wi/dcf/shares/rates/wi_shares_monthly_max_rate.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.tools.geography.county_helpers import county_has_state_suffix from policyengine_us.variables.gov.states.wi.dcf.shares.rates.wi_shares_provider_type import ( WISharesProviderType, ) @@ -30,7 +31,7 @@ def formula(person, period, parameters): # fall back to Adams along with non-Wisconsin households. in_wi = person.household("state_code_str", period) == "WI" safe_county = where( - in_wi & np.char.endswith(county.astype(str), "_WI"), + in_wi & county_has_state_suffix(county, "WI"), county, "ADAMS_COUNTY_WI", )