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..beb428a1ed6 --- /dev/null +++ b/changelog.d/fix-county-lookup-guards.fixed.md @@ -0,0 +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, 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/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..160b2756f93 --- /dev/null +++ b/policyengine_us/parameters/gov/states/vt/tax/income/credits/renter/max_household_size.yaml @@ -0,0 +1,25 @@ +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 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: + 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 + - 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 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..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 @@ -247,3 +247,179 @@ # 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] + +- 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] + +- 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 df8137bd3e6..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 @@ -241,3 +241,99 @@ 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] + +- 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] + +- 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 6a063373d9a..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 @@ -146,6 +146,140 @@ 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; + # Base: ROUND(1,936 * 12 * 0.1, 0) = 2,323; + # 0.5 * 2,323 = 1,161.5 -> 1,162 (half up). + # 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; + # Base: ROUND(1,410 * 12 * 0.1, 0) = 1,692; + # 0.5 * 1,692 = 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. + # 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; + # Base: ROUND(2,083 * 12 * 0.1, 0) = 2,500; + # 0.5 * 2,500 = 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] + # Franklin and Grand Isle use the state-average fair market rent (32 V.S.A. # 6061(7)(C)). Expected values are the department's Renter Credit Website # Calculators (2022, 2023, 2025), recalculated in LibreOffice for each household: @@ -496,3 +630,91 @@ # 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] + +- 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 258a6dec8c6..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 @@ -145,3 +145,99 @@ 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] + +- 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] + +- 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 efe743cdc17..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 @@ -97,3 +97,101 @@ # 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] + +- 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] + +- 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 27c7f18ac00..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, ) @@ -27,9 +28,17 @@ 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. 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, "ALLEGANY_COUNTY_MD") + safe_county = where( + in_md & county_has_state_suffix(county, "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 a7117dfac0f..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, ) @@ -27,12 +28,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. + # 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, "HENNEPIN_COUNTY_MN") + county_key = where( + in_mn & county_has_state_suffix(county, "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 6284d0a2187..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): @@ -17,17 +18,29 @@ 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 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_has_state_suffix(county, "VT"), + 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 - ] - partial_credit_income_limit = p.income_limit_ami.fifty_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 ] - fmr = p.fair_market_rent[tax_unit_size][county] + fmr = p.fair_market_rent[size][safe_county] # The department rounds the credit amount to whole dollars, half up, # before applying the income phase-out: its 2021 calculator computes # ROUND(FMR * 12 * 10%, 0), and its published tables are whole dollars. 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..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, ) @@ -21,11 +22,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. + # 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, "ADAMS_COUNTY_WI") + safe_county = where( + in_wi & county_has_state_suffix(county, "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 9c049c6cd54..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, ) @@ -24,11 +25,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. + # 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, "ADAMS_COUNTY_WI") + safe_county = where( + in_wi & county_has_state_suffix(county, "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