From 2228eb730cef5fc821ea593ca18b1a73435c5dc2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 15:02:59 -0400 Subject: [PATCH] Guard WA property tax exemption county lookups against non-WA counties wa_pte_income_eligible and wa_pte_tier index the Washington county-keyed income thresholds with every household's county_str. defined_for masks the output but policyengine-core still runs the formula over the whole population, so any simulation with a household outside Washington (or a Washington household with an UNKNOWN county) raised ParameterNotFoundError in every year. Full-population microsimulations crashed on wa_senior_disabled_property_tax_exemption. Look those households up under Adams County, the county the model assigns a Washington household that gives none, as the MD and IN county-tax formulas already do. Adds vectorized YAML cases that put a Washington household and a South Dakota household in one simulation. Co-Authored-By: Claude Opus 5.5 --- changelog.d/fix-wa-pte-county-lookup.fixed.md | 1 + .../wa_pte_income_eligible.yaml | 63 ++++++++++++++ .../senior_disabled/wa_pte_tier.yaml | 55 ++++++++++++ ...enior_disabled_property_tax_exemption.yaml | 83 +++++++++++++++++++ .../income/wa_pte_income_eligible.py | 8 ++ .../senior_disabled/income/wa_pte_tier.py | 8 ++ 6 files changed, 218 insertions(+) create mode 100644 changelog.d/fix-wa-pte-county-lookup.fixed.md diff --git a/changelog.d/fix-wa-pte-county-lookup.fixed.md b/changelog.d/fix-wa-pte-county-lookup.fixed.md new file mode 100644 index 00000000000..ef0d17828e9 --- /dev/null +++ b/changelog.d/fix-wa-pte-county-lookup.fixed.md @@ -0,0 +1 @@ +Stop the Washington senior and disabled property tax exemption from crashing simulations that include households outside Washington or with an unknown county. diff --git a/policyengine_us/tests/policy/baseline/gov/states/wa/dor/property_tax_exemption/senior_disabled/wa_pte_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/wa/dor/property_tax_exemption/senior_disabled/wa_pte_income_eligible.yaml index bd1f9216f23..3d3ae95080e 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wa/dor/property_tax_exemption/senior_disabled/wa_pte_income_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wa/dor/property_tax_exemption/senior_disabled/wa_pte_income_eligible.yaml @@ -241,3 +241,66 @@ # CDI = 60,000 + 30,000 = 90,000 > King 2024 T3 ($84,000) -> ineligible. # Without the cotenant amount the tax unit would have qualified for Tier 3. wa_pte_income_eligible: false + +- name: Case 12, out-of-state and unknown-county households in one simulation with a King County household. + # defined_for masks the output but the formula still runs over every + # household in the simulation. Before the county guard, the South Dakota + # county and the UNKNOWN county reached the Washington county-keyed + # thresholds and raised ParameterNotFoundError. + period: 2024 + input: + people: + person1: + age: 70 + person2: + age: 70 + person3: + age: 70 + person4: + age: 70 + tax_units: + tax_unit1: + members: [person1] + adjusted_gross_income: 84_000 + tax_unit2: + members: [person2] + adjusted_gross_income: 84_000 + tax_unit3: + members: [person3] + adjusted_gross_income: 41_000 + tax_unit4: + members: [person4] + adjusted_gross_income: 41_001 + spm_units: + spm_unit1: + members: [person1] + spm_unit2: + members: [person2] + spm_unit3: + members: [person3] + spm_unit4: + members: [person4] + households: + household1: + members: [person1] + state_code: WA + county: KING_COUNTY_WA + household2: + members: [person2] + state_code: SD + county: MARSHALL_COUNTY_SD + household3: + members: [person3] + state_code: WA + county: UNKNOWN + household4: + members: [person4] + state_code: WA + county: UNKNOWN + output: + # household1: King 2024 T3 = $84,000; income at threshold -> eligible. + # household2: South Dakota; defined_for zeroes the result. + # household3: UNKNOWN county falls back to Adams 2024 T3 = $41,000; + # income at threshold -> eligible. + # household4: $41,001 > Adams T3 -> ineligible (King T3 would admit it). + wa_pte_income_eligible: [true, false, true, false] diff --git a/policyengine_us/tests/policy/baseline/gov/states/wa/dor/property_tax_exemption/senior_disabled/wa_pte_tier.yaml b/policyengine_us/tests/policy/baseline/gov/states/wa/dor/property_tax_exemption/senior_disabled/wa_pte_tier.yaml index 048dfead7eb..c2c988f1cdc 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wa/dor/property_tax_exemption/senior_disabled/wa_pte_tier.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wa/dor/property_tax_exemption/senior_disabled/wa_pte_tier.yaml @@ -317,3 +317,58 @@ # Completes the Tier 2 boundary triple together with Case 13 (T2-1) and # Case 2 (at T2). wa_pte_tier: 3 + +- name: Case 15, 2027 out-of-state and unknown-county households in one simulation with a King County household. + # defined_for masks the output but the formula still runs over every + # household in the simulation. Before the county guard, the South Dakota + # county and the UNKNOWN county reached the Washington county-keyed + # thresholds and raised ParameterNotFoundError. + period: 2027 + input: + people: + person1: + age: 70 + person2: + age: 70 + person3: + age: 70 + tax_units: + tax_unit1: + members: [person1] + adjusted_gross_income: 83_500 + filing_status: SINGLE + tax_unit2: + members: [person2] + adjusted_gross_income: 83_500 + filing_status: SINGLE + tax_unit3: + members: [person3] + adjusted_gross_income: 52_500 + filing_status: SINGLE + spm_units: + spm_unit1: + members: [person1] + spm_unit2: + members: [person2] + spm_unit3: + members: [person3] + households: + household1: + members: [person1] + state_code: WA + county: KING_COUNTY_WA + household2: + members: [person2] + state_code: SD + county: MARSHALL_COUNTY_SD + household3: + members: [person3] + state_code: WA + county: UNKNOWN + output: + # 2027 CDI = AGI - max(0, $7,500 single standard deduction). + # household1: CDI = 83,500 - 7,500 = 76,000 = King 2027 T1 -> Tier 1. + # household2: South Dakota; defined_for zeroes the result. + # household3: CDI = 52,500 - 7,500 = 45,000. UNKNOWN county falls back + # to Adams 2027: T1 $42,000 < 45,000 <= T2 $49,000 -> Tier 2. + wa_pte_tier: [1, 0, 2] diff --git a/policyengine_us/tests/policy/baseline/gov/states/wa/dor/property_tax_exemption/senior_disabled/wa_senior_disabled_property_tax_exemption.yaml b/policyengine_us/tests/policy/baseline/gov/states/wa/dor/property_tax_exemption/senior_disabled/wa_senior_disabled_property_tax_exemption.yaml index 63ea7dc5418..8b552e04015 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wa/dor/property_tax_exemption/senior_disabled/wa_senior_disabled_property_tax_exemption.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wa/dor/property_tax_exemption/senior_disabled/wa_senior_disabled_property_tax_exemption.yaml @@ -491,3 +491,86 @@ # exemption = tier_3_exempt_share * real_estate_taxes # = 0.45 * 5,000 = 2,250 wa_senior_disabled_property_tax_exemption: 2_250 + +- name: Case 19, 2026 Washington and South Dakota households in one simulation. + # Reproduces the full-population crash: defined_for masks the output but + # the tier lookup still runs over the South Dakota household, whose county + # is not a key of the Washington county thresholds. + period: 2026 + input: + people: + person1: + age: 70 + real_estate_taxes: 6_000 + assessed_property_value: 700_000 + person2: + age: 70 + real_estate_taxes: 6_000 + assessed_property_value: 700_000 + tax_units: + tax_unit1: + members: [person1] + adjusted_gross_income: 40_000 + filing_status: SINGLE + tax_unit2: + members: [person2] + adjusted_gross_income: 40_000 + filing_status: SINGLE + spm_units: + spm_unit1: + members: [person1] + spm_unit2: + members: [person2] + households: + household1: + members: [person1] + state_code: WA + county: KING_COUNTY_WA + household2: + members: [person2] + state_code: SD + county: MARSHALL_COUNTY_SD + output: + # household1 is Case 16's household: 4,320. + # household2 is in South Dakota: 0. + wa_senior_disabled_property_tax_exemption: [4_320, 0] + +- name: Case 20, 2027 Washington and South Dakota households in one simulation. + period: 2027 + input: + people: + person1: + age: 70 + real_estate_taxes: 6_000 + assessed_property_value: 700_000 + person2: + age: 70 + real_estate_taxes: 6_000 + assessed_property_value: 700_000 + tax_units: + tax_unit1: + members: [person1] + adjusted_gross_income: 40_000 + filing_status: SINGLE + tax_unit2: + members: [person2] + adjusted_gross_income: 40_000 + filing_status: SINGLE + spm_units: + spm_unit1: + members: [person1] + spm_unit2: + members: [person2] + households: + household1: + members: [person1] + state_code: WA + county: KING_COUNTY_WA + household2: + members: [person2] + state_code: SD + county: MARSHALL_COUNTY_SD + output: + # household1 is Case 17's household: 5,340. + # household2 is in South Dakota: 0. + wa_senior_disabled_property_tax_exemption: [5_340, 0] diff --git a/policyengine_us/variables/gov/states/wa/dor/property_tax_exemption/senior_disabled/income/wa_pte_income_eligible.py b/policyengine_us/variables/gov/states/wa/dor/property_tax_exemption/senior_disabled/income/wa_pte_income_eligible.py index 32da34f5164..10ab56a6277 100644 --- a/policyengine_us/variables/gov/states/wa/dor/property_tax_exemption/senior_disabled/income/wa_pte_income_eligible.py +++ b/policyengine_us/variables/gov/states/wa/dor/property_tax_exemption/senior_disabled/income/wa_pte_income_eligible.py @@ -15,7 +15,15 @@ class wa_pte_income_eligible(Variable): def formula(tax_unit, period, parameters): income = tax_unit("wa_pte_combined_disposable_income", period) + # defined_for masks the output but does not stop the formula from + # running over every household, and the thresholds are keyed by + # Washington county only. Households outside Washington, or with an + # UNKNOWN county, are looked up under Adams County (the county the + # model assigns a Washington household that gives none) so the lookup + # cannot raise ParameterNotFoundError. county = tax_unit.household("county_str", period) + in_wa = tax_unit.household("state_code_str", period) == "WA" + county = where(in_wa & (county != "UNKNOWN"), county, "ADAMS_COUNTY_WA") p = parameters( period ).gov.states.wa.dor.property_tax_exemption.senior_disabled.income.thresholds diff --git a/policyengine_us/variables/gov/states/wa/dor/property_tax_exemption/senior_disabled/income/wa_pte_tier.py b/policyengine_us/variables/gov/states/wa/dor/property_tax_exemption/senior_disabled/income/wa_pte_tier.py index e08f878f0e3..6c8b4a8d599 100644 --- a/policyengine_us/variables/gov/states/wa/dor/property_tax_exemption/senior_disabled/income/wa_pte_tier.py +++ b/policyengine_us/variables/gov/states/wa/dor/property_tax_exemption/senior_disabled/income/wa_pte_tier.py @@ -15,7 +15,15 @@ class wa_pte_tier(Variable): def formula(tax_unit, period, parameters): income = tax_unit("wa_pte_combined_disposable_income", period) + # defined_for masks the output but does not stop the formula from + # running over every household, and the thresholds are keyed by + # Washington county only. Households outside Washington, or with an + # UNKNOWN county, are looked up under Adams County (the county the + # model assigns a Washington household that gives none) so the lookup + # cannot raise ParameterNotFoundError. county = tax_unit.household("county_str", period) + in_wa = tax_unit.household("state_code_str", period) == "WA" + county = where(in_wa & (county != "UNKNOWN"), county, "ADAMS_COUNTY_WA") p = parameters( period ).gov.states.wa.dor.property_tax_exemption.senior_disabled.income.thresholds