From b78aa0dd36ec0e69da5b25c9eeba09e9ebc8fbd1 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 23:29:44 -0400 Subject: [PATCH 01/12] Follow the adopted CA Premium Subsidy design at the 150% FPL boundary Gov. Code 100800(c) delegates the state premium subsidy design to the Covered California Board. The adopted 2026 Program Design runs its bands "100% up to 150%" (0%) and "Above 150% up to 165%"; the 2026 Policy Explainer labels the upper band "150% - At or Below 165%". The model now puts a fraction equal to a threshold in the lower band (searchsorted side="left"), keeping the existing whole-percentage FPL truncation. Co-Authored-By: Claude Opus 5.5 --- .../statute-first-ca-premium-subsidy.fixed.md | 1 + .../applicable_percentage/threshold.yaml | 10 +++--- ...premium_subsidy_applicable_percentage.yaml | 32 ++++++++++++++++--- .../ca/hbex/premium_subsidy/integration.yaml | 13 ++++---- ...a_premium_subsidy_applicable_percentage.py | 12 +++++-- 5 files changed, 51 insertions(+), 17 deletions(-) create mode 100644 changelog.d/statute-first-ca-premium-subsidy.fixed.md diff --git a/changelog.d/statute-first-ca-premium-subsidy.fixed.md b/changelog.d/statute-first-ca-premium-subsidy.fixed.md new file mode 100644 index 00000000000..8ddc57f72f9 --- /dev/null +++ b/changelog.d/statute-first-ca-premium-subsidy.fixed.md @@ -0,0 +1 @@ +Follow California's adopted Premium Subsidy Program Design at its band boundaries: exactly 150% of FPL is in the 0% band, not the 3.19% band the Policy Explainer shows. diff --git a/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/threshold.yaml b/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/threshold.yaml index 2196012efae..f0ba3db0176 100644 --- a/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/threshold.yaml +++ b/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/threshold.yaml @@ -1,9 +1,11 @@ description: California sets the federal poverty line thresholds for the applicable percentage brackets under the California Premium Subsidy program. values: - # 150% is untiered in the Program Design prose but sits in the 3.19% band: - # the Explainer labels the band "150% - At or Below 165%", and under - # 26 CFR 1.36B-3(g)(2) FPL-percentage truncation to 2 decimals collapses - # [150%, 151%) to 1.50, so 150% takes the 150-165% bracket rate. + # The adopted Program Design runs its bands "100% up to 150%" (0%) and + # "Above 150% up to 165%", so a fraction equal to a threshold takes the + # lower band: 150% pays 0%. The Policy Explainer labels the upper band + # "150% - At or Below 165%"; the model follows the adopted design. With + # the FPL fraction truncated to whole percentages (aca_magi_fraction, + # Form 8962 line 5), incomes from 150% to under 151% of FPL count as 150%. 2026-01-01: - 1 - 1.5 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml index b71a61c042f..1ac24c948f9 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml @@ -36,9 +36,11 @@ output: ca_premium_subsidy_applicable_percentage: 0 -- name: Case 2, exactly 150% FPL -> 3.19% (start of the 150-165% ramp). - # searchsorted([1.0,1.5,1.65], 1.50, "right") = 2, -1 -> bracket 1 [1.5,1.65]. - # position (1.50-1.50)/0.15 = 0 -> initial rate 0.0319. +- name: Case 2, exactly 150% FPL -> 0% (the adopted design's "up to 150%" band). + # The 2026 Program Design's bands are "100% up to 150%" (0%) and "Above 150% + # up to 165%"; the Policy Explainer's "150% - At or Below 165%" would give + # 3.19%. The model follows the adopted design. + # searchsorted([1.0,1.5,1.65], 1.50, "left") = 1, -1 -> bracket 0 [1.0,1.5]. period: 2026 absolute_error_margin: 0.0001 input: @@ -55,7 +57,7 @@ members: [person1] state_code: CA output: - ca_premium_subsidy_applicable_percentage: 0.0319 + ca_premium_subsidy_applicable_percentage: 0 - name: Case 3, exactly 165% FPL -> 3.91% (top of the ramp). # searchsorted([1.0,1.5,1.65], 1.65, "right") = 3, -1 = 2, clipped to bracket 1. @@ -343,3 +345,25 @@ state_code: CA output: ca_premium_subsidy_applicable_percentage: 0.050517 # model computes 0.05051714 + +- name: Case 2b, 151% FPL starts the 150-165% ramp. + # searchsorted([1.0,1.5,1.65], 1.51, "left") = 2, -1 -> bracket 1 [1.5,1.65]. + # position (1.51-1.50)/0.15 = 0.0667 -> 0.0319 + 0.0667 x (0.0391 - 0.0319) + # = 0.03238. + period: 2026 + absolute_error_margin: 0.0001 + input: + people: + person1: + age: 40 + is_aca_ptc_eligible: true + tax_units: + tax_unit: + members: [person1] + aca_magi_fraction: 1.51 + households: + household: + members: [person1] + state_code: CA + output: + ca_premium_subsidy_applicable_percentage: 0.03238 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/integration.yaml index 8a5c9e53751..cb799eed844 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/integration.yaml @@ -11,7 +11,7 @@ # asserts the intermediate values so a reviewer can follow the arithmetic: # eligible = in_effect & any(is_aca_ptc_eligible) & 1.0 <= magi_frac <= 1.65 # pct = CA schedule keyed on aca_magi_fraction -# (100-150%: 0%; 150-165%: 3.19% -> 3.91% linear) +# (100-150%: 0%; above 150-165%: 3.19% -> 3.91% linear) # amount = max(0, slcsp_annual - aca_ptc - pct * max(aca_magi, 0)) # result = where(eligible, amount, 0) @@ -43,9 +43,10 @@ ca_premium_subsidy_applicable_percentage: 0 ca_premium_subsidy: 2_200 -- name: Scenario 2, single age 40 exactly at 150% FPL (knot, pct jumps to 3.19%). - # magi_frac 1.50 -> bracket [1.5,1.65] start -> pct 0.0319. income 22_500. - # amount = max(0, 6_000 - 3_000 - 0.0319*22_500) = 6_000 - 3_000 - 717.75 = 2_282.25. +- name: Scenario 2, single age 40 exactly at 150% FPL (top of the 0% band). + # The adopted 2026 Program Design puts 150% in its "up to 150%" band, so + # pct 0 (the Policy Explainer's band would give 3.19% and $2,282.25). + # amount = max(0, 6_000 - 3_000 - 0*22_500) = 3_000. period: 2026 absolute_error_margin: 0.01 input: @@ -67,8 +68,8 @@ state_code: CA output: ca_premium_subsidy_eligible: true - ca_premium_subsidy_applicable_percentage: 0.0319 - ca_premium_subsidy: 2_282.25 + ca_premium_subsidy_applicable_percentage: 0 + ca_premium_subsidy: 3_000 - name: Scenario 3, single age 55 at 157.5% FPL (midpoint of the 150-165% ramp). # magi_frac 1.575 -> pct 0.0319 + 0.5*0.0072 = 0.0355. income 23_625. diff --git a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.py b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.py index b32cc5a600a..a3750314eb2 100644 --- a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.py +++ b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.py @@ -24,13 +24,19 @@ def formula(tax_unit, period, parameters): initial_rates = np.array(p.initial) final_rates = np.array(p.final) - # Find which bracket each tax unit falls into. searchsorted returns - # the index where magi_frac would be inserted; subtract 1 to get the + # Find which bracket each tax unit falls into. The adopted Program + # Design (Gov. Code 100800(c) delegates the design to the Board) runs + # its bands "up to" each threshold and "Above" the previous one, so a + # fraction equal to a threshold belongs to the lower band: exactly + # 150% FPL pays 0% in 2026. The Policy Explainer's "150% - At or Below + # 165%" band puts 150% in the upper band; the model follows the + # adopted design. searchsorted with side="left" returns the index of + # the first threshold at or above magi_frac; subtract 1 to get the # bracket index, clamped to the valid range. Rate arrays are one # shorter than thresholds (federal/NM convention), so bracket_idx is # always a valid left edge and bracket_idx + 1 a valid right edge. bracket_idx = clip( - np.searchsorted(thresholds, magi_frac, side="right") - 1, + np.searchsorted(thresholds, magi_frac, side="left") - 1, 0, len(initial_rates) - 1, ) From 0cfff85764e677e712f9c347a8723783cc13ece8 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 23:39:13 -0400 Subject: [PATCH 02/12] Tidy CA premium subsidy tests and pin the FPL truncation (review r1) Review r1 P3s: searchsorted comments now say side="left"; Case 13 explains the continuous rate at 165% in 2027; Case 2b tests 1.505 instead of duplicating Case 7; a new case derives 150.49% from wages and shows the truncated fraction pays 0%; the threshold comment notes the truncation is the federal form's gap-fill; the eligibility test header matches the formula's FPL-band description. Co-Authored-By: Claude Opus 5.5 --- .../applicable_percentage/threshold.yaml | 2 + ...premium_subsidy_applicable_percentage.yaml | 53 +++++++++++++------ .../ca_premium_subsidy_eligible.yaml | 4 +- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/threshold.yaml b/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/threshold.yaml index f0ba3db0176..089869ea49a 100644 --- a/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/threshold.yaml +++ b/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/threshold.yaml @@ -6,6 +6,8 @@ values: # "150% - At or Below 165%"; the model follows the adopted design. With # the FPL fraction truncated to whole percentages (aca_magi_fraction, # Form 8962 line 5), incomes from 150% to under 151% of FPL count as 150%. + # The design is silent on rounding; the truncation is the federal form's + # gap-fill. 2026-01-01: - 1 - 1.5 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml index 1ac24c948f9..3cf053e7ade 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml @@ -16,7 +16,7 @@ # ---- Knot points ---- - name: Case 1, exactly 100% FPL -> 0.00% (start of the flat 100-150% band). - # searchsorted([1.0,1.5,1.65], 1.00, "right") = 1, -1 -> bracket 0 [1.0,1.5]. + # searchsorted([1.0,1.5,1.65], 1.00, "left") = 0, -1 = -1, clipped to bracket 0 [1.0,1.5]. # initial 0, final 0 -> 0 regardless of position. period: 2026 absolute_error_margin: 0.0001 @@ -60,7 +60,7 @@ ca_premium_subsidy_applicable_percentage: 0 - name: Case 3, exactly 165% FPL -> 3.91% (top of the ramp). - # searchsorted([1.0,1.5,1.65], 1.65, "right") = 3, -1 = 2, clipped to bracket 1. + # searchsorted([1.0,1.5,1.65], 1.65, "left") = 2, -1 = 1 -> bracket 1 at position 1. # bracket_start 1.5, bracket_end 1.65, position (1.65-1.5)/0.15 = 1 -> final 0.0391. period: 2026 absolute_error_margin: 0.0001 @@ -188,7 +188,7 @@ ca_premium_subsidy_applicable_percentage: 0 - name: Case 9, below the 100% floor (0.99) clamps to the first bracket -> 0.00%. - # searchsorted([1.0,1.5,1.65], 0.99, "right") = 0, -1 = -1, clipped to bracket 0. + # searchsorted([1.0,1.5,1.65], 0.99, "left") = 0, -1 = -1, clipped to bracket 0. # bracket 0 [1.0,1.5]: position (0.99-1.0)/0.5 = -0.02 clipped to 0; initial 0 -> 0. # The eligible gate (fpl_floor 1.0) separately zeroes the subsidy here. period: 2026 @@ -210,7 +210,7 @@ ca_premium_subsidy_applicable_percentage: 0 - name: Case 10, above the 165% cap (1.66) clamps to the top bracket rate -> 3.91%. - # searchsorted([1.0,1.5,1.65], 1.66, "right") = 3, -1 = 2, clipped to bracket 1. + # searchsorted([1.0,1.5,1.65], 1.66, "left") = 3, -1 = 2, clipped to bracket 1. # position (1.66-1.5)/0.15 = 1.067 clipped to 1 -> final 0.0391. The eligible # gate (fpl_limit 1.65) separately zeroes the subsidy above the band, so this # confirms the schedule variable itself does not extrapolate past 3.91%. @@ -233,7 +233,7 @@ ca_premium_subsidy_applicable_percentage: 0.0391 - name: Case 11, negative FPL fraction clamps to the first bracket -> 0.00%. - # searchsorted([1.0,1.5,1.65], -0.5, "right") = 0, -1 = -1, clipped to bracket 0. + # searchsorted([1.0,1.5,1.65], -0.5, "left") = 0, -1 = -1, clipped to bracket 0. # position (-0.5-1.0)/0.5 = -3 clipped to 0; initial 0 -> 0. Guards against a # negative aca_magi_fraction producing a spurious rate. period: 2026 @@ -281,10 +281,11 @@ # ---- PY2027 new 165-200% FPL tier ---- -- name: Case 13, PY2027 exactly 165% FPL -> 3.41% (start of the new 165-200% tier). - # PY2027 thresholds [1,1.5,1.65,2.0]. searchsorted(..., 1.65, "right") = 3, - # -1 = 2 -> bracket 2 [1.65,2.0], position 0 -> initial rate 0.0341. The 165% - # knot now takes the NEW tier's initial rate (2026 had 0.0391 at this point). +- name: Case 13, PY2027 exactly 165% FPL -> 3.41% from either adjacent tier. + # PY2027 thresholds [1,1.5,1.65,2.0]. searchsorted(..., 1.65, "left") = 2, + # -1 = 1 -> bracket 1 [1.5,1.65] at position 1 -> its final rate 0.0341, + # which equals the 165-200% tier's initial rate, so the rate is continuous + # at 165% (2026 had 0.0391 at this point). period: 2027 absolute_error_margin: 0.0001 input: @@ -346,10 +347,11 @@ output: ca_premium_subsidy_applicable_percentage: 0.050517 # model computes 0.05051714 -- name: Case 2b, 151% FPL starts the 150-165% ramp. - # searchsorted([1.0,1.5,1.65], 1.51, "left") = 2, -1 -> bracket 1 [1.5,1.65]. - # position (1.51-1.50)/0.15 = 0.0667 -> 0.0319 + 0.0667 x (0.0391 - 0.0319) - # = 0.03238. +- name: Case 2b, a supplied fraction of 1.505 interpolates on the fraction given. + # searchsorted([1.0,1.5,1.65], 1.505, "left") = 2, -1 -> bracket 1 [1.5,1.65]. + # position (1.505-1.50)/0.15 = 0.0333 -> 0.0319 + 0.0333 x (0.0391 - 0.0319) + # = 0.03214. (A fraction computed from income is truncated to whole + # percentages first; see the next case.) period: 2026 absolute_error_margin: 0.0001 input: @@ -360,10 +362,31 @@ tax_units: tax_unit: members: [person1] - aca_magi_fraction: 1.51 + aca_magi_fraction: 1.505 households: household: members: [person1] state_code: CA output: - ca_premium_subsidy_applicable_percentage: 0.03238 + ca_premium_subsidy_applicable_percentage: 0.03214 + +- name: Case 2c, income at 150.5% of FPL truncates to 150% and pays 0%. + # The Program Design is silent on rounding; aca_magi_fraction drops the + # fraction of a percentage point, as Form 8962 Worksheet 2 does ("drop any + # numbers after the decimal point"), a gap-fill. 2025 FPG for one person is + # $15,650; 1.505 x 15,650 = 23,553.25, so $23,553 of wages is 150.49% -> 1.50. + period: 2026 + absolute_error_margin: 0.0001 + input: + people: + person1: + age: 40 + employment_income: 23_553 + is_aca_ptc_eligible: true + households: + household: + members: [person1] + state_code: CA + output: + aca_magi_fraction: 1.50 + ca_premium_subsidy_applicable_percentage: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.yaml index c2f5969cf23..85d82596a5a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.yaml @@ -2,8 +2,8 @@ # Eligible when: the program is in effect (PY2026-2027), the household is in # California, at least one tax-unit member is eligible for the federal ACA # premium tax credit (which embeds on-Marketplace enrollment, the -# married-filing-separately exclusion, and the federal required-contribution -# income test), the tax unit is an applicable return filer, and ACA MAGI is at +# married-filing-separately exclusion, and the federal FPL-band income test), +# the tax unit is an applicable return filer, and ACA MAGI is at # or above the 100% FPL floor and at or below the FPL limit (1.65 in PY2026, # 1.99 in PY2027). is_aca_ptc_eligible and aca_magi_fraction are supplied # directly so the gate logic is tested in isolation from the premium-data lookup. From a5d51e415c3b5fcde630d965a2ca67b6c3f692ae Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 23:42:20 -0400 Subject: [PATCH 03/12] Pin truncation, not rounding, in the CA 150% income case (review r2) Co-Authored-By: Claude Opus 5.5 --- .../ca_premium_subsidy_applicable_percentage.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml index 3cf053e7ade..11a51dc1522 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml @@ -370,18 +370,19 @@ output: ca_premium_subsidy_applicable_percentage: 0.03214 -- name: Case 2c, income at 150.5% of FPL truncates to 150% and pays 0%. +- name: Case 2c, income at 150.6% of FPL truncates to 150% and pays 0%. # The Program Design is silent on rounding; aca_magi_fraction drops the # fraction of a percentage point, as Form 8962 Worksheet 2 does ("drop any # numbers after the decimal point"), a gap-fill. 2025 FPG for one person is - # $15,650; 1.505 x 15,650 = 23,553.25, so $23,553 of wages is 150.49% -> 1.50. + # $15,650; $23,569 / $15,650 = 1.506, which truncates to 1.50 (rate 0) but + # would round to 1.51 (rate 0.03238). period: 2026 absolute_error_margin: 0.0001 input: people: person1: age: 40 - employment_income: 23_553 + employment_income: 23_569 is_aca_ptc_eligible: true households: household: From f8fedc081af38c3c259e7886aa420879839c6b84 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 12:40:59 -0400 Subject: [PATCH 04/12] Select California premium subsidy bands from exact income --- .../applicable_percentage/threshold.yaml | 13 +- .../premium_subsidy/ca_premium_subsidy.yaml | 15 +- ...premium_subsidy_applicable_percentage.yaml | 273 +++++++++++++++++- .../ca/hbex/premium_subsidy/integration.yaml | 33 ++- ...a_premium_subsidy_applicable_percentage.py | 87 ++++++ ...a_premium_subsidy_applicable_percentage.py | 30 +- 6 files changed, 407 insertions(+), 44 deletions(-) create mode 100644 policyengine_us/tests/unit/test_ca_premium_subsidy_applicable_percentage.py diff --git a/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/threshold.yaml b/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/threshold.yaml index 089869ea49a..63e30cf9e77 100644 --- a/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/threshold.yaml +++ b/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/threshold.yaml @@ -1,13 +1,14 @@ description: California sets the federal poverty line thresholds for the applicable percentage brackets under the California Premium Subsidy program. values: # The adopted Program Design runs its bands "100% up to 150%" (0%) and - # "Above 150% up to 165%", so a fraction equal to a threshold takes the + # "Above 150% up to 165%", so an exact ratio equal to a threshold takes the # lower band: 150% pays 0%. The Policy Explainer labels the upper band - # "150% - At or Below 165%"; the model follows the adopted design. With - # the FPL fraction truncated to whole percentages (aca_magi_fraction, - # Form 8962 line 5), incomes from 150% to under 151% of FPL count as 150%. - # The design is silent on rounding; the truncation is the federal form's - # gap-fill. + # "150% - At or Below 165%"; the model follows the adopted design. + # Band selection uses exact nonnegative ACA MAGI divided by prior-year FPG; + # only the rate lookup uses truncated aca_magi_fraction. The Designs leave + # rounding unspecified. FTB 3849 Table 1-2 (2020/2021) tests exact income + # for "above" boundaries before truncating for lookup; Form 8962 Worksheet 2 + # line 4 likewise checks "more than" before truncation. 2026-01-01: - 1 - 1.5 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml index 889da75d6d7..37b03c4bf7d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml @@ -141,7 +141,9 @@ # ---- Negative-MAGI clamp ---- - name: Case 6, negative ACA MAGI is clamped to zero (does not inflate subsidy). - # magi_frac 1.575 -> pct 0.0355; aca_magi -10_000 clamps to 0 so the -pct*income + # Inject pct 0.0355 to isolate the amount formula's clamp: exact negative + # MAGI would otherwise select the 0% band, hiding a missing amount clamp. + # aca_magi -10_000 clamps to 0 so the -pct*income # term is 0 rather than +355. amount = max(0, 6_000 - 2_000 - 0.0355*0) = 4_000. # Without the clamp it would be 6_000 - 2_000 - 0.0355*(-10_000) = 4_355. period: 2026 @@ -156,6 +158,7 @@ members: [person1] aca_magi: -10_000 aca_magi_fraction: 1.575 + ca_premium_subsidy_applicable_percentage: 0.0355 slcsp: 6_000 aca_ptc: 2_000 tax_unit_is_filer: true @@ -476,7 +479,8 @@ # add(is_aca_ptc_eligible) > 0, which fires on any eligible member, so the tax # unit is eligible. The amount uses tax-unit slcsp/aca_ptc (not per-member), so # the asymmetry does NOT halve or zero the payout. magi_frac 1.575 -> pct 0.0355. - # income 30_000. amount = max(0, 8_000 - 3_000 - 0.0355*30_000) = 5_000 - 1_065 = 3_935. + # Pin prior-year FPG to 20_000: income 31_500 / 20_000 = 1.575. + # amount = max(0, 8_000 - 3_000 - 0.0355*31_500) = 5_000 - 1_118.25 = 3_881.75. # Confirms the add()>0 gate + defined_for + person->tax_unit aggregation produce # the full subsidy rather than a per-eligible-member proration. period: 2026 @@ -492,7 +496,9 @@ tax_units: tax_unit: members: [person1, person2] - aca_magi: 30_000 + tax_unit_fpg: + 2025: 20_000 + aca_magi: 31_500 aca_magi_fraction: 1.575 slcsp: 8_000 aca_ptc: 3_000 @@ -503,4 +509,5 @@ state_code: CA output: ca_premium_subsidy_eligible: true - ca_premium_subsidy: 3_935 + ca_premium_subsidy_applicable_percentage: 0.0355 + ca_premium_subsidy: 3_881.75 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml index 11a51dc1522..d478d144905 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml @@ -6,12 +6,16 @@ # final: [0, 0.0391] # Bands (initial -> final over the FPL band): # 100-150% FPL: 0% (flat) -# 150-165% FPL: 3.19% -> 3.91% (interpolated linearly) -# The percentage depends only on aca_magi_fraction; every expected value is -# hand-computed inline. The variable itself has no income-band cap (out-of-band +# above 150-165% FPL: 3.19% -> 3.91% (interpolated linearly) +# The exact nonnegative MAGI / prior-year FPG ratio selects the band; the +# truncated aca_magi_fraction determines the rate within it. Supplied-fraction +# cases normalize prior-year FPG to $10,000 and supply consistent MAGI to +# isolate interpolation, including fractional percentages that a derived +# aca_magi_fraction would truncate. Every expected value is hand-computed +# inline. The variable itself has no income-band cap (out-of-band # clamping to the nearest bracket rate is fine; the ca_premium_subsidy_eligible -# gate zeroes the subsidy outside 100-165% FPL), so this file checks the raw -# interpolation, including clamps at 0.99 and 1.66. +# gate separately determines eligibility). This file +# checks raw interpolation, including clamps at 0.99 and 1.66. # ---- Knot points ---- @@ -28,6 +32,9 @@ tax_units: tax_unit: members: [person1] + aca_magi: 10_000 + tax_unit_fpg: + 2025: 10_000 aca_magi_fraction: 1.00 households: household: @@ -51,6 +58,9 @@ tax_units: tax_unit: members: [person1] + aca_magi: 15_000 + tax_unit_fpg: + 2025: 10_000 aca_magi_fraction: 1.50 households: household: @@ -72,6 +82,9 @@ tax_units: tax_unit: members: [person1] + aca_magi: 16_500 + tax_unit_fpg: + 2025: 10_000 aca_magi_fraction: 1.65 households: household: @@ -94,6 +107,9 @@ tax_units: tax_unit: members: [person1] + aca_magi: 12_500 + tax_unit_fpg: + 2025: 10_000 aca_magi_fraction: 1.25 households: household: @@ -115,6 +131,9 @@ tax_units: tax_unit: members: [person1] + aca_magi: 15_750 + tax_unit_fpg: + 2025: 10_000 aca_magi_fraction: 1.575 households: household: @@ -136,6 +155,9 @@ tax_units: tax_unit: members: [person1] + aca_magi: 16_000 + tax_unit_fpg: + 2025: 10_000 aca_magi_fraction: 1.60 households: household: @@ -159,6 +181,9 @@ tax_units: tax_unit: members: [person1] + aca_magi: 15_100 + tax_unit_fpg: + 2025: 10_000 aca_magi_fraction: 1.51 households: household: @@ -179,6 +204,9 @@ tax_units: tax_unit: members: [person1] + aca_magi: 14_900 + tax_unit_fpg: + 2025: 10_000 aca_magi_fraction: 1.49 households: household: @@ -201,6 +229,9 @@ tax_units: tax_unit: members: [person1] + aca_magi: 9_900 + tax_unit_fpg: + 2025: 10_000 aca_magi_fraction: 0.99 households: household: @@ -224,6 +255,9 @@ tax_units: tax_unit: members: [person1] + aca_magi: 16_600 + tax_unit_fpg: + 2025: 10_000 aca_magi_fraction: 1.66 households: household: @@ -233,7 +267,8 @@ ca_premium_subsidy_applicable_percentage: 0.0391 - name: Case 11, negative FPL fraction clamps to the first bracket -> 0.00%. - # searchsorted([1.0,1.5,1.65], -0.5, "left") = 0, -1 = -1, clipped to bracket 0. + # Negative MAGI is floored at 0 for the exact ratio; searchsorted(..., 0, + # "left") = 0, -1 = -1, clipped to bracket 0. # position (-0.5-1.0)/0.5 = -3 clipped to 0; initial 0 -> 0. Guards against a # negative aca_magi_fraction producing a spurious rate. period: 2026 @@ -246,6 +281,9 @@ tax_units: tax_unit: members: [person1] + aca_magi: -5_000 + tax_unit_fpg: + 2025: 10_000 aca_magi_fraction: -0.50 households: household: @@ -271,6 +309,9 @@ tax_units: tax_unit: members: [person1] + aca_magi: 15_750 + tax_unit_fpg: + 2026: 10_000 aca_magi_fraction: 1.575 households: household: @@ -296,6 +337,9 @@ tax_units: tax_unit: members: [person1] + aca_magi: 16_500 + tax_unit_fpg: + 2026: 10_000 aca_magi_fraction: 1.65 households: household: @@ -317,6 +361,9 @@ tax_units: tax_unit: members: [person1] + aca_magi: 18_250 + tax_unit_fpg: + 2026: 10_000 aca_magi_fraction: 1.825 households: household: @@ -339,6 +386,9 @@ tax_units: tax_unit: members: [person1] + aca_magi: 19_900 + tax_unit_fpg: + 2026: 10_000 aca_magi_fraction: 1.99 households: household: @@ -362,6 +412,9 @@ tax_units: tax_unit: members: [person1] + aca_magi: 15_050 + tax_unit_fpg: + 2025: 10_000 aca_magi_fraction: 1.505 households: household: @@ -370,19 +423,43 @@ output: ca_premium_subsidy_applicable_percentage: 0.03214 -- name: Case 2c, income at 150.6% of FPL truncates to 150% and pays 0%. - # The Program Design is silent on rounding; aca_magi_fraction drops the - # fraction of a percentage point, as Form 8962 Worksheet 2 does ("drop any - # numbers after the decimal point"), a gap-fill. 2025 FPG for one person is - # $15,650; $23,569 / $15,650 = 1.506, which truncates to 1.50 (rate 0) but - # would round to 1.51 (rate 0.03238). +# ---- Derived fractions around the exact 150% and truncated 151% boundaries ---- + +- name: Case 2c, income at 150.6% of FPL pays 3.19% despite truncating to 150%. + # The adopted Design (d)(4) puts exact income above 150% in the paying band. + # FTB 3849 (2020/2021), Table 1-2, decides "above" on the exact percentage + # before truncating for the rate lookup. $23,569 / 2025 one-person FPG + # $15,650 = 150.6006% selects the upper band; the truncated 1.50 fraction + # gives its starting rate 0.0319, for a $751.85 annual contribution. period: 2026 - absolute_error_margin: 0.0001 + absolute_error_margin: 0.000001 + input: + people: + person1: + age: 40 + employment_income: + 2026: 23_569 + is_aca_ptc_eligible: true + households: + household: + members: [person1] + state_code: CA + output: + aca_magi_fraction: 1.50 + ca_premium_subsidy_applicable_percentage: 0.0319 + +- name: Case 2d, PY2026 raw income $23,475, exactly 150% remains in the 0% band. + # $23,475 / 2025 one-person FPG $15,650 = 150.0000%. + # Exact income selects the band; the derived fraction 1.50 determines + # the rate 0, for an annual contribution of $0.00. + period: 2026 + absolute_error_margin: 0.000001 input: people: person1: age: 40 - employment_income: 23_569 + employment_income: + 2026: 23_475 is_aca_ptc_eligible: true households: household: @@ -391,3 +468,171 @@ output: aca_magi_fraction: 1.50 ca_premium_subsidy_applicable_percentage: 0 + +- name: Case 2e, PY2026 raw income $23,477, just above 150% enters the paying band. + # $23,477 / 2025 one-person FPG $15,650 = 150.0128%. + # Exact income selects the band; the derived fraction 1.50 determines + # the rate 0.0319, for an annual contribution of $748.92. + period: 2026 + absolute_error_margin: 0.000001 + input: + people: + person1: + age: 40 + employment_income: + 2026: 23_477 + is_aca_ptc_eligible: true + households: + household: + members: [person1] + state_code: CA + output: + aca_magi_fraction: 1.50 + ca_premium_subsidy_applicable_percentage: 0.0319 + +- name: Case 2f, PY2026 raw income $23,630, just below 151% keeps the starting rate. + # $23,630 / 2025 one-person FPG $15,650 = 150.9904%. + # Exact income selects the band; the derived fraction 1.50 determines + # the rate 0.0319, for an annual contribution of $753.80. + period: 2026 + absolute_error_margin: 0.000001 + input: + people: + person1: + age: 40 + employment_income: + 2026: 23_630 + is_aca_ptc_eligible: true + households: + household: + members: [person1] + state_code: CA + output: + aca_magi_fraction: 1.50 + ca_premium_subsidy_applicable_percentage: 0.0319 + +- name: Case 2g, PY2026 raw income $23,632, just above 151% uses the next truncated percentage. + # $23,632 / 2025 one-person FPG $15,650 = 151.0032%. + # Exact income selects the band; the derived fraction 1.51 determines + # the rate 0.03238, for an annual contribution of $765.20. + period: 2026 + absolute_error_margin: 0.000001 + input: + people: + person1: + age: 40 + employment_income: + 2026: 23_632 + is_aca_ptc_eligible: true + households: + household: + members: [person1] + state_code: CA + output: + aca_magi_fraction: 1.51 + ca_premium_subsidy_applicable_percentage: 0.03238 + +- name: Case 16a, PY2027 raw income $23,940, exactly 150% remains in the 0% band. + # $23,940 / 2026 one-person FPG $15,960 = 150.0000%. + # Exact income selects the band; the derived fraction 1.50 determines + # the rate 0, for an annual contribution of $0.00. + period: 2027 + absolute_error_margin: 0.000001 + input: + people: + person1: + age: 40 + employment_income: + 2027: 23_940 + is_aca_ptc_eligible: true + households: + household: + members: [person1] + state_code: CA + output: + aca_magi_fraction: 1.50 + ca_premium_subsidy_applicable_percentage: 0 + +- name: Case 16b, PY2027 raw income $23,942, just above 150% enters the paying band. + # $23,942 / 2026 one-person FPG $15,960 = 150.0125%. + # Exact income selects the band; the derived fraction 1.50 determines + # the rate 0.0269, for an annual contribution of $644.04. + period: 2027 + absolute_error_margin: 0.000001 + input: + people: + person1: + age: 40 + employment_income: + 2027: 23_942 + is_aca_ptc_eligible: true + households: + household: + members: [person1] + state_code: CA + output: + aca_magi_fraction: 1.50 + ca_premium_subsidy_applicable_percentage: 0.0269 + +- name: Case 16c, PY2027 raw income $24,036, 150.6% pays the starting rate despite truncating to 150%. + # $24,036 / 2026 one-person FPG $15,960 = 150.6015%. + # Exact income selects the band; the derived fraction 1.50 determines + # the rate 0.0269, for an annual contribution of $646.57. + period: 2027 + absolute_error_margin: 0.000001 + input: + people: + person1: + age: 40 + employment_income: + 2027: 24_036 + is_aca_ptc_eligible: true + households: + household: + members: [person1] + state_code: CA + output: + aca_magi_fraction: 1.50 + ca_premium_subsidy_applicable_percentage: 0.0269 + +- name: Case 16d, PY2027 raw income $24,099, just below 151% keeps the starting rate. + # $24,099 / 2026 one-person FPG $15,960 = 150.9962%. + # Exact income selects the band; the derived fraction 1.50 determines + # the rate 0.0269, for an annual contribution of $648.26. + period: 2027 + absolute_error_margin: 0.000001 + input: + people: + person1: + age: 40 + employment_income: + 2027: 24_099 + is_aca_ptc_eligible: true + households: + household: + members: [person1] + state_code: CA + output: + aca_magi_fraction: 1.50 + ca_premium_subsidy_applicable_percentage: 0.0269 + +- name: Case 16e, PY2027 raw income $24,100, just above 151% uses the next truncated percentage. + # $24,100 / 2026 one-person FPG $15,960 = 151.0025%. + # Exact income selects the band; the derived fraction 1.51 determines + # the rate 0.02738, for an annual contribution of $659.86. + period: 2027 + absolute_error_margin: 0.000001 + input: + people: + person1: + age: 40 + employment_income: + 2027: 24_100 + is_aca_ptc_eligible: true + households: + household: + members: [person1] + state_code: CA + output: + aca_magi_fraction: 1.51 + ca_premium_subsidy_applicable_percentage: 0.02738 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/integration.yaml index cb799eed844..de51d8869c8 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/integration.yaml @@ -7,10 +7,13 @@ # # To keep expected values exactly hand-verifiable and independent of the # rating-area premium lookup, aca_magi, aca_magi_fraction, slcsp (annual), -# aca_ptc and is_aca_ptc_eligible are supplied directly, and each scenario -# asserts the intermediate values so a reviewer can follow the arithmetic: +# aca_ptc and is_aca_ptc_eligible are supplied directly. Prior-year FPG is +# pinned to a synthetic $15,000 for the supplied single-person scenarios; +# Scenario 4 uses the actual 2025 four-person $32,150. Each asserts intermediate +# values so a reviewer can follow the arithmetic: # eligible = in_effect & any(is_aca_ptc_eligible) & 1.0 <= magi_frac <= 1.65 -# pct = CA schedule keyed on aca_magi_fraction +# pct = exact nonnegative MAGI / prior-year FPG selects the CA band; +# aca_magi_fraction determines the rate within that band # (100-150%: 0%; above 150-165%: 3.19% -> 3.91% linear) # amount = max(0, slcsp_annual - aca_ptc - pct * max(aca_magi, 0)) # result = where(eligible, amount, 0) @@ -29,6 +32,8 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2025: 15_000 aca_magi: 18_750 aca_magi_fraction: 1.25 slcsp: 6_000 @@ -57,6 +62,8 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2025: 15_000 aca_magi: 22_500 aca_magi_fraction: 1.50 slcsp: 6_000 @@ -84,6 +91,8 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2025: 15_000 aca_magi: 23_625 aca_magi_fraction: 1.575 slcsp: 7_500 @@ -99,8 +108,10 @@ ca_premium_subsidy: 3_461.31 - name: Scenario 4, family of four at 165% FPL (top of the ramp, pct 3.91%). - # ages 45/43/15/13; magi_frac 1.65 -> pct 0.0391. income 51_500. - # amount = max(0, 18_000 - 9_000 - 0.0391*51_500) = 18_000 - 9_000 - 2_013.65 = 6_986.35. + # ages 45/43/15/13; magi_frac 1.65 -> pct 0.0391. income 53_047.50 + # equals 165% of 2025 four-person FPG $32,150. + # amount = max(0, 18_000 - 9_000 - 0.0391*53_047.50) + # = 18_000 - 9_000 - 2_074.15725 = 6_925.84. period: 2026 absolute_error_margin: 0.01 input: @@ -120,7 +131,9 @@ tax_units: tax_unit: members: [person1, person2, person3, person4] - aca_magi: 51_500 + tax_unit_fpg: + 2025: 32_150 + aca_magi: 53_047.50 aca_magi_fraction: 1.65 slcsp: 18_000 aca_ptc: 9_000 @@ -132,7 +145,7 @@ output: ca_premium_subsidy_eligible: true ca_premium_subsidy_applicable_percentage: 0.0391 - ca_premium_subsidy: 6_986.35 + ca_premium_subsidy: 6_925.84 - name: Scenario 5, single age 40 at 160% FPL where APTC covers the residual -> $0. # magi_frac 1.60 -> pct 0.0319 + (2/3)*0.0072 = 0.0367. income 24_000. @@ -149,6 +162,8 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2025: 15_000 aca_magi: 24_000 aca_magi_fraction: 1.60 slcsp: 3_000 @@ -176,6 +191,8 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2025: 15_000 aca_magi: 21_000 aca_magi_fraction: 1.40 slcsp: 6_000 @@ -202,6 +219,8 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2024: 15_000 aca_magi: 21_000 aca_magi_fraction: 1.40 slcsp: 6_000 diff --git a/policyengine_us/tests/unit/test_ca_premium_subsidy_applicable_percentage.py b/policyengine_us/tests/unit/test_ca_premium_subsidy_applicable_percentage.py new file mode 100644 index 00000000000..8b424af08da --- /dev/null +++ b/policyengine_us/tests/unit/test_ca_premium_subsidy_applicable_percentage.py @@ -0,0 +1,87 @@ +"""Properties of the adopted 2026/2027 Design (d)(4) contribution bands. + +FTB 3849 Table 1-2 distinguishes exact band eligibility from the truncated +percentage used for the rate lookup. The physical-income properties use +that truncation. Continuity tests supply untruncated lookup fractions to +isolate the interpolation: the actual lookup has whole-percentage steps, +whereas the underlying bands join continuously everywhere except 150%. +""" + +import numpy as np +import pytest +from hypothesis import example, given, settings +from hypothesis import strategies as st +from policyengine_core.periods import period + +from policyengine_us.system import system + +FPG = 100_000 +VARIABLE = "ca_premium_subsidy_applicable_percentage" + + +def _rates(year, incomes, *, truncate=True): + """Call the registered formula on vectorized inputs without rebuilding US.""" + calculation_period = period(year) + incomes = np.asarray(incomes, dtype=float) + fractions = np.maximum(incomes, 0) / FPG + if truncate: + fractions = np.floor(100 * np.maximum(incomes, 0) / FPG) / 100 + inputs = { + "aca_magi": incomes, + "aca_magi_fraction": fractions, + "tax_unit_fpg": np.full(incomes.shape, FPG), + } + + def tax_unit(name, input_period): + expected_period = ( + calculation_period.last_year + if name == "tax_unit_fpg" + else calculation_period + ) + assert input_period == expected_period + return inputs[name] + + formula = system.variables[VARIABLE].get_formula(calculation_period) + return formula(tax_unit, calculation_period, system.parameters) + + +@pytest.mark.parametrize("year", [2026, 2027]) +@settings(max_examples=100, deadline=None, derandomize=True) +@given(incomes=st.lists(st.integers(-100_000, 600_000), min_size=2, max_size=40)) +@example(incomes=[149_999, 150_000, 150_001, 150_999, 151_000]) +def test_rate_never_decreases_with_exact_income(year, incomes): + rates = _rates(year, sorted(incomes)) + assert np.all(np.diff(rates) >= -1e-12) + + +@pytest.mark.parametrize("year", [2026, 2027]) +@settings(max_examples=100, deadline=None, derandomize=True) +@given(income=st.integers(-100_000, 150_000)) +@example(income=150_000) +def test_rate_is_zero_at_or_below_exactly_150_percent(year, income): + assert _rates(year, [income])[0] == 0 + + +@pytest.mark.parametrize("year, starting_rate", [(2026, 0.0319), (2027, 0.0269)]) +@settings(max_examples=100, deadline=None, derandomize=True) +@given(income=st.integers(150_001, 150_999)) +def test_exact_income_above_150_uses_upper_band_truncated_starting_rate( + year, starting_rate, income +): + assert _rates(year, [income])[0] == pytest.approx(starting_rate, rel=0, abs=1e-12) + + +@pytest.mark.parametrize( + "year, boundary", + [(2026, 1), (2026, 1.65), (2027, 1), (2027, 1.65), (2027, 2)], +) +@settings(max_examples=100, deadline=None, derandomize=True) +@given(epsilon=st.floats(min_value=1e-7, max_value=1e-4)) +def test_interpolated_bands_are_continuous_except_at_150_percent( + year, boundary, epsilon +): + incomes = FPG * np.array([boundary - epsilon, boundary, boundary + epsilon]) + rates = _rates(year, incomes, truncate=False) + # Each adopted band's slope is below 0.05, so two-sided variation is + # at most 0.1 * epsilon. An additional band-boundary jump violates this. + assert np.ptp(rates) <= 0.1 * epsilon + 1e-10 diff --git a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.py b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.py index a3750314eb2..49d66771dff 100644 --- a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.py +++ b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.py @@ -13,30 +13,32 @@ class ca_premium_subsidy_applicable_percentage(Variable): # benchmark second lowest cost silver plan, bracket-interpolated # linearly within each federal poverty line band. "https://board.coveredca.com/meetings/2025/July%2028,%202025/CoveredCA_2026_Premium_Subsidy_Program_Design_Final.pdf#page=2", - "https://hbex.coveredca.com/stakeholders/PDFs/2026-02_StatePremiumSub_PolicyExplainer-Final.pdf#page=2", + "https://board.coveredca.com/meetings/2026/May%2021,%202026/CoveredCA_2027_Premium_Subsidy_Program_Design_V4_05-19-26.pdf#page=2", + "https://www.ftb.ca.gov/forms/2020/2020-3849-instructions.pdf#page=6", + "https://www.ftb.ca.gov/forms/2021/2021-3849-instructions.pdf#page=6", + "https://www.irs.gov/pub/irs-pdf/i8962.pdf#page=9", ) def formula(tax_unit, period, parameters): magi_frac = tax_unit("aca_magi_fraction", period) + magi = max_(tax_unit("aca_magi", period), 0) + exact_magi_frac = magi / tax_unit("tax_unit_fpg", period.last_year) p = parameters(period).gov.states.ca.hbex.premium_subsidy.applicable_percentage thresholds = np.array(p.threshold) initial_rates = np.array(p.initial) final_rates = np.array(p.final) - # Find which bracket each tax unit falls into. The adopted Program - # Design (Gov. Code 100800(c) delegates the design to the Board) runs - # its bands "up to" each threshold and "Above" the previous one, so a - # fraction equal to a threshold belongs to the lower band: exactly - # 150% FPL pays 0% in 2026. The Policy Explainer's "150% - At or Below - # 165%" band puts 150% in the upper band; the model follows the - # adopted design. searchsorted with side="left" returns the index of - # the first threshold at or above magi_frac; subtract 1 to get the - # bracket index, clamped to the valid range. Rate arrays are one - # shorter than thresholds (federal/NM convention), so bracket_idx is - # always a valid left edge and bracket_idx + 1 a valid right edge. + # The Board-adopted Designs (d)(4), operative under Gov. Code + # 100800(c), put exactly 150% in the 0% band and income above 150% + # in the paying band. Use the exact ratio to select the band. + # For rounding, predecessor FTB 3849 Table 1-2 (2020/2021) tests + # "above" on exact income before truncating for the rate lookup. + # Form 8962 Worksheet 2 line 4 likewise checks "more than" before + # truncation. side="left" keeps an exact threshold in the lower + # band; clipping keeps both bracket endpoints in the rate arrays. bracket_idx = clip( - np.searchsorted(thresholds, magi_frac, side="left") - 1, + np.searchsorted(thresholds, exact_magi_frac, side="left") - 1, 0, len(initial_rates) - 1, ) @@ -44,6 +46,8 @@ def formula(tax_unit, period, parameters): bracket_start = thresholds[bracket_idx] bracket_end = thresholds[bracket_idx + 1] + # Retain the truncated fraction within the band: 150.6% uses the + # 150% lookup row of the upper band (3.19% in 2026, 2.69% in 2027). position = clip( (magi_frac - bracket_start) / (bracket_end - bracket_start), 0, 1 ) From 38efe457a9f8aef4b0231a9182f4359e03d378bb Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 12:41:18 -0400 Subject: [PATCH 05/12] Document exact California subsidy boundaries and lookup rounding --- changelog.d/statute-first-ca-premium-subsidy.fixed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/statute-first-ca-premium-subsidy.fixed.md b/changelog.d/statute-first-ca-premium-subsidy.fixed.md index 8ddc57f72f9..d1a583bcac8 100644 --- a/changelog.d/statute-first-ca-premium-subsidy.fixed.md +++ b/changelog.d/statute-first-ca-premium-subsidy.fixed.md @@ -1 +1 @@ -Follow California's adopted Premium Subsidy Program Design at its band boundaries: exactly 150% of FPL is in the 0% band, not the 3.19% band the Policy Explainer shows. +Select California Premium Subsidy bands from exact nonnegative ACA MAGI divided by prior-year FPL under the adopted [2026](https://board.coveredca.com/meetings/2025/July%2028,%202025/CoveredCA_2026_Premium_Subsidy_Program_Design_Final.pdf#page=2) and [2027 Designs, (d)(4)](https://board.coveredca.com/meetings/2026/May%2021,%202026/CoveredCA_2027_Premium_Subsidy_Program_Design_V4_05-19-26.pdf#page=2): exactly 150% pays 0%, while income above 150% enters the 3.19% or 2.69% band. Retain the truncated fraction for the rate within that band, following [FTB 3849 Table 1-2 (2020)](https://www.ftb.ca.gov/forms/2020/2020-3849-instructions.pdf#page=6) and [2021](https://www.ftb.ca.gov/forms/2021/2021-3849-instructions.pdf#page=6); [Form 8962 Worksheet 2 line 4](https://www.irs.gov/pub/irs-pdf/i8962.pdf#page=9) likewise tests "more than" on the exact amount before truncating. From 7769acf60fa02d223a065968d9d61b99c676a77b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 13:29:40 -0400 Subject: [PATCH 06/12] Decide CA Premium Subsidy eligibility on the exact FPL ratio ca_premium_subsidy_eligible compared the truncated aca_magi_fraction with <= 1.65, so every PY2026 income from 165.003% to 165.994% of FPL passed an "at or below 165 percent" test (Program Design (d)(7)). Compare the exact ratio of nonnegative ACA MAGI to prior-year FPG instead, as FTB 3849 Table 1-2 does for the predecessor program's band edges. The PY2027 limit was 1.99 on the truncated fraction, which equals "under 200 percent" only under truncation. Express it as 2.0 with a new fpl_limit_inclusive parameter (true in PY2026, false in PY2027), the same pattern as NM's benchmark_gross_up.boundary_inclusive. Tests: boundary cases from derived income at $25,822.50 / $25,823 / $25,978 / $25,979 (PY2026) and $26,335 / $31,919 / $31,920 (PY2027); supplied cases now set aca_magi and prior-year FPG instead of the fraction; a Hypothesis differential test against an exact-rational reading of (d)(7). Co-Authored-By: Claude Opus 5.5 --- ...a-premium-subsidy-165-exact-ratio.fixed.md | 1 + .../ca/hbex/premium_subsidy/fpl_limit.yaml | 6 +- .../premium_subsidy/fpl_limit_inclusive.yaml | 18 + .../premium_subsidy/ca_premium_subsidy.yaml | 26 +- .../ca_premium_subsidy_eligible.yaml | 314 ++++++++++++++++-- ..._ca_premium_subsidy_eligible_properties.py | 130 ++++++++ .../ca_premium_subsidy_eligible.py | 21 +- 7 files changed, 469 insertions(+), 47 deletions(-) create mode 100644 changelog.d/ca-premium-subsidy-165-exact-ratio.fixed.md create mode 100644 policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/fpl_limit_inclusive.yaml create mode 100644 policyengine_us/tests/unit/test_ca_premium_subsidy_eligible_properties.py diff --git a/changelog.d/ca-premium-subsidy-165-exact-ratio.fixed.md b/changelog.d/ca-premium-subsidy-165-exact-ratio.fixed.md new file mode 100644 index 00000000000..18681ed6ca7 --- /dev/null +++ b/changelog.d/ca-premium-subsidy-165-exact-ratio.fixed.md @@ -0,0 +1 @@ +Decide California Premium Subsidy eligibility on the exact ratio of nonnegative ACA MAGI to prior-year FPL instead of the truncated `aca_magi_fraction`, so PY2026 incomes from 165.003% to 165.994% of FPL are no longer eligible ([2026 Program Design, (d)(7)](https://board.coveredca.com/meetings/2025/July%2028,%202025/CoveredCA_2026_Premium_Subsidy_Program_Design_Final.pdf#page=2): "at or below 165 percent"). The PY2027 limit becomes a strict 200% ([2027 Program Design V4, (d)(7)](https://board.coveredca.com/meetings/2026/May%2021,%202026/CoveredCA_2027_Premium_Subsidy_Program_Design_V4_05-19-26.pdf#page=2): "under 200 percent"), which leaves PY2027 eligibility unchanged. diff --git a/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/fpl_limit.yaml b/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/fpl_limit.yaml index d50b1ad09c0..170e2d96028 100644 --- a/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/fpl_limit.yaml +++ b/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/fpl_limit.yaml @@ -1,9 +1,11 @@ description: California limits income eligibility to this share of the federal poverty line under the California Premium Subsidy program. values: + # PY2026 Program Design (d)(7): "at or below 165 percent" (inclusive). 2026-01-01: 1.65 # PY2027 Program Design V4 (adopted Board Res. 2026-22) extends eligibility to - # "under 200%" FPL; with 2-decimal FPL-percentage truncation, <= 1.99 == "<200%". - 2027-01-01: 1.99 + # "under 200 percent" (strict; see fpl_limit_inclusive). The limit applies + # to the exact income-to-FPL ratio, not the truncated aca_magi_fraction. + 2027-01-01: 2.0 metadata: unit: /1 diff --git a/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/fpl_limit_inclusive.yaml b/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/fpl_limit_inclusive.yaml new file mode 100644 index 00000000000..655524ba46e --- /dev/null +++ b/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/fpl_limit_inclusive.yaml @@ -0,0 +1,18 @@ +description: California uses this rule to treat the income eligibility limit as inclusive under the California Premium Subsidy program. +values: + # PY2026 Program Design (d)(7): "at or below 165 percent". + 2026-01-01: true + # PY2027 Program Design V4 (d)(7): "under 200 percent". + 2027-01-01: false + +metadata: + unit: bool + period: year + label: California Premium Subsidy income limit inclusive + reference: + - title: 2026 California Premium Subsidy Program Design, subdiv. (d)(7) + href: https://board.coveredca.com/meetings/2025/July%2028,%202025/CoveredCA_2026_Premium_Subsidy_Program_Design_Final.pdf#page=2 + - title: 2027 California Premium Subsidy Program Design V4, subdiv. (d)(7) + href: https://board.coveredca.com/meetings/2026/May%2021,%202026/CoveredCA_2027_Premium_Subsidy_Program_Design_V4_05-19-26.pdf#page=2 + - title: Covered California Board Resolution 2026-22 + href: https://board.coveredca.com/meetings/2026/June%2018,%202026/2026-22.pdf diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml index 889da75d6d7..b52a008c547 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml @@ -90,8 +90,9 @@ ca_premium_subsidy: 3_112.50 - name: Case 4, exactly 165% FPL positive subsidy (pct 3.91%, top of the ramp). - # pct = 0.0391 (final rate). income 26_000. - # amount = max(0, 7_000 - 2_500 - 0.0391*26_000) = 7_000 - 2_500 - 1_016.60 = 3_483.40. + # pct = 0.0391 (final rate). income 25_822.50 = 165% of the 2025 one-person + # FPG $15,650, the highest income "at or below 165 percent". + # amount = max(0, 7_000 - 2_500 - 0.0391*25_822.50) = 7_000 - 2_500 - 1_009.66 = 3_490.34. period: 2026 absolute_error_margin: 0.01 input: @@ -102,7 +103,7 @@ tax_units: tax_unit: members: [person1] - aca_magi: 26_000 + aca_magi: 25_822.50 aca_magi_fraction: 1.65 slcsp: 7_000 aca_ptc: 2_500 @@ -112,7 +113,8 @@ members: [person1] state_code: CA output: - ca_premium_subsidy: 3_483.40 + ca_premium_subsidy_eligible: true + ca_premium_subsidy: 3_490.34 - name: Case 5, 160% FPL with expected contribution exceeding the gap -> $0 floor. # pct = 0.0319 + (2/3)*0.0072 = 0.0367. income 24_000 -> 0.0367*24_000 = 880.80. @@ -159,6 +161,9 @@ slcsp: 6_000 aca_ptc: 2_000 tax_unit_is_filer: true + # Negative MAGI is below the 100% floor on the exact ratio, so inject + # eligibility to reach the amount formula's clamp. + ca_premium_subsidy_eligible: true households: household: members: [person1] @@ -232,6 +237,7 @@ aca_magi_fraction: 1.66 slcsp: 6_000 aca_ptc: 2_000 + tax_unit_is_filer: true households: household: members: [person1] @@ -266,9 +272,10 @@ # ---- Floor edge with a positive amount ---- - name: Case 11, exactly at the 100% FPL floor (1.0) with a positive subsidy. - # magi_frac 1.0 -> pct 0 (flat 100-150% band). Eligible: 1.0 >= fpl_floor 1.0, - # <= fpl_limit 1.65, in effect, APTC-eligible. income 15_000. - # amount = max(0, 6_000 - 3_500 - 0*15_000) = 2_500. Confirms the floor edge is + # magi_frac 1.0 -> pct 0 (flat 100-150% band). Eligible: income 15_650 is + # exactly the 2025 one-person FPG, so 1.0 >= fpl_floor 1.0, <= fpl_limit 1.65, + # in effect, APTC-eligible. + # amount = max(0, 6_000 - 3_500 - 0*15_650) = 2_500. Confirms the floor edge is # eligible AND pays the full post-APTC gap (pct is 0 at the bottom of the band). period: 2026 absolute_error_margin: 0.01 @@ -280,7 +287,7 @@ tax_units: tax_unit: members: [person1] - aca_magi: 15_000 + aca_magi: 15_650 aca_magi_fraction: 1.00 slcsp: 6_000 aca_ptc: 3_500 @@ -290,6 +297,7 @@ members: [person1] state_code: CA output: + ca_premium_subsidy_eligible: true ca_premium_subsidy: 2_500 # ---- Mid-band interaction: APTC and pct both active, residual positive ---- @@ -372,6 +380,7 @@ aca_magi_fraction: 1.66 slcsp: 6_000 aca_ptc: 2_000 + tax_unit_is_filer: true households: household: members: [person1] @@ -400,6 +409,7 @@ aca_magi_fraction: 0.99 slcsp: 6_000 aca_ptc: 2_000 + tax_unit_is_filer: true households: household: members: [person1] diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.yaml index c2f5969cf23..769e1323623 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.yaml @@ -5,14 +5,20 @@ # married-filing-separately exclusion, and the federal required-contribution # income test), the tax unit is an applicable return filer, and ACA MAGI is at # or above the 100% FPL floor and at or below the FPL limit (1.65 in PY2026, -# 1.99 in PY2027). is_aca_ptc_eligible and aca_magi_fraction are supplied -# directly so the gate logic is tested in isolation from the premium-data lookup. -# fpl_floor = 1.0, fpl_limit = 1.65 (PY2026) +# under 2.00 in PY2027; see fpl_limit_inclusive). Program Design (d)(7) applies +# these limits to the exact ratio of nonnegative ACA MAGI to prior-year FPG, +# not to the truncated aca_magi_fraction (FTB 3849 Table 1-2 likewise decides +# band edges on the exact percentage). Supplied-income cases pin prior-year FPG +# to $10,000 and set aca_magi to the target ratio; is_aca_ptc_eligible is +# supplied so the gate logic is tested in isolation from the premium-data +# lookup. Income-derived cases use the actual prior-year one-person FPG +# ($15,650 for PY2026, $15,960 for PY2027). +# fpl_floor = 1.0; fpl_limit = 1.65 inclusive (PY2026), 2.0 exclusive (PY2027) # ---- Income-band boundaries ---- - name: Case 1, just below the 100% FPL floor (0.99) is ineligible. - # aca_magi_fraction 0.99 < fpl_floor 1.0 -> income test fails. + # Exact ratio 9_900 / 10_000 = 0.99 < fpl_floor 1.0 -> income test fails. period: 2026 input: people: @@ -22,7 +28,10 @@ tax_units: tax_unit: members: [person1] - aca_magi_fraction: 0.99 + aca_magi: 9_900 + tax_unit_fpg: + 2025: 10_000 + tax_unit_is_filer: true households: household: members: [person1] @@ -31,7 +40,7 @@ ca_premium_subsidy_eligible: false - name: Case 2, exactly at the 100% FPL floor (1.0) is eligible. - # aca_magi_fraction 1.0 >= fpl_floor 1.0 and <= fpl_limit 1.65. + # Exact ratio 1.0 >= fpl_floor 1.0 and <= fpl_limit 1.65. period: 2026 input: people: @@ -41,7 +50,9 @@ tax_units: tax_unit: members: [person1] - aca_magi_fraction: 1.00 + aca_magi: 10_000 + tax_unit_fpg: + 2025: 10_000 tax_unit_is_filer: true households: household: @@ -60,7 +71,9 @@ tax_units: tax_unit: members: [person1] - aca_magi_fraction: 1.40 + aca_magi: 14_000 + tax_unit_fpg: + 2025: 10_000 tax_unit_is_filer: true households: household: @@ -70,7 +83,7 @@ ca_premium_subsidy_eligible: true - name: Case 4, exactly at the 165% FPL limit (1.65) is eligible. - # aca_magi_fraction 1.65 <= fpl_limit 1.65 (at the limit, not above). + # Exact ratio 1.65 <= fpl_limit 1.65 (PY2026: "at or below 165 percent"). period: 2026 input: people: @@ -80,7 +93,9 @@ tax_units: tax_unit: members: [person1] - aca_magi_fraction: 1.65 + aca_magi: 16_500 + tax_unit_fpg: + 2025: 10_000 tax_unit_is_filer: true households: household: @@ -90,22 +105,122 @@ ca_premium_subsidy_eligible: true - name: Case 5, just above the 165% FPL limit (1.66) is ineligible. - # aca_magi_fraction 1.66 > fpl_limit 1.65 -> above the band (federal APTC only). + # Exact ratio 1.66 > fpl_limit 1.65 -> above the band (federal APTC only). + period: 2026 + input: + people: + person1: + age: 40 + is_aca_ptc_eligible: true + tax_units: + tax_unit: + members: [person1] + aca_magi: 16_600 + tax_unit_fpg: + 2025: 10_000 + tax_unit_is_filer: true + households: + household: + members: [person1] + state_code: CA + output: + ca_premium_subsidy_eligible: false + +# ---- PY2026 165% limit from derived income (exact ratio, not truncation) ---- + +- name: Case 4b, PY2026 income of exactly 165% of FPL is eligible. + # $25,822.50 / 2025 one-person FPG $15,650 = 165.000%, which is "at or below + # 165 percent" (Program Design (d)(7)). period: 2026 + absolute_error_margin: 0.0001 input: people: person1: age: 40 + employment_income: 25_822.50 is_aca_ptc_eligible: true tax_units: tax_unit: members: [person1] - aca_magi_fraction: 1.66 + tax_unit_is_filer: true + households: + household: + members: [person1] + state_code: CA + output: + aca_magi: 25_822.50 + aca_magi_fraction: 1.65 + ca_premium_subsidy_eligible: true + +- name: Case 5b, PY2026 income of 165.003% of FPL is ineligible though it truncates to 1.65. + # $25,823 / $15,650 = 165.0032%. aca_magi_fraction truncates to 1.65, but the + # limit applies to the exact ratio, which is above 165 percent. + period: 2026 + absolute_error_margin: 0.0001 + input: + people: + person1: + age: 40 + employment_income: 25_823 + is_aca_ptc_eligible: true + tax_units: + tax_unit: + members: [person1] + tax_unit_is_filer: true households: household: members: [person1] state_code: CA output: + aca_magi: 25_823 + aca_magi_fraction: 1.65 + ca_premium_subsidy_eligible: false + +- name: Case 5c, PY2026 income of 165.994% of FPL is ineligible though it truncates to 1.65. + # $25,978 / $15,650 = 165.9936%, the highest whole-dollar income that + # truncates to 1.65. Before the exact-ratio gate this case was eligible. + period: 2026 + absolute_error_margin: 0.0001 + input: + people: + person1: + age: 40 + employment_income: 25_978 + is_aca_ptc_eligible: true + tax_units: + tax_unit: + members: [person1] + tax_unit_is_filer: true + households: + household: + members: [person1] + state_code: CA + output: + aca_magi: 25_978 + aca_magi_fraction: 1.65 + ca_premium_subsidy_eligible: false + +- name: Case 5d, PY2026 income of 166.000% of FPL is ineligible. + # $25,979 / $15,650 = 166.0000%; the truncated fraction is 1.66 as well. + period: 2026 + absolute_error_margin: 0.0001 + input: + people: + person1: + age: 40 + employment_income: 25_979 + is_aca_ptc_eligible: true + tax_units: + tax_unit: + members: [person1] + tax_unit_is_filer: true + households: + household: + members: [person1] + state_code: CA + output: + aca_magi: 25_979 + aca_magi_fraction: 1.66 ca_premium_subsidy_eligible: false # ---- Federal APTC gate ---- @@ -122,7 +237,10 @@ tax_units: tax_unit: members: [person1] - aca_magi_fraction: 1.40 + aca_magi: 14_000 + tax_unit_fpg: + 2025: 10_000 + tax_unit_is_filer: true households: household: members: [person1] @@ -144,7 +262,9 @@ tax_units: tax_unit: members: [person1, person2] - aca_magi_fraction: 1.40 + aca_magi: 14_000 + tax_unit_fpg: + 2025: 10_000 tax_unit_is_filer: true households: household: @@ -166,7 +286,10 @@ tax_units: tax_unit: members: [person1] - aca_magi_fraction: 1.40 + aca_magi: 14_000 + tax_unit_fpg: + 2025: 10_000 + tax_unit_is_filer: true households: household: members: [person1] @@ -187,7 +310,10 @@ tax_units: tax_unit: members: [person1] - aca_magi_fraction: 1.40 + aca_magi: 14_000 + tax_unit_fpg: + 2024: 10_000 + tax_unit_is_filer: true households: household: members: [person1] @@ -206,7 +332,10 @@ tax_units: tax_unit: members: [person1] - aca_magi_fraction: 1.40 + aca_magi: 14_000 + tax_unit_fpg: + 2027: 10_000 + tax_unit_is_filer: true households: household: members: [person1] @@ -225,7 +354,9 @@ tax_units: tax_unit: members: [person1] - aca_magi_fraction: 1.40 + aca_magi: 14_000 + tax_unit_fpg: + 2026: 10_000 tax_unit_is_filer: true households: household: @@ -236,8 +367,8 @@ # ---- Zero-income edge ---- -- name: Case 12, zero-income household (aca_magi_fraction 0) is ineligible. - # aca_magi_fraction 0 < fpl_floor 1.0 -> income test fails. A tax unit with no +- name: Case 12, zero-income household (0% FPL) is ineligible. + # Exact ratio 0 < fpl_floor 1.0 -> income test fails. A tax unit with no # income (0% FPL) sits below the CA 100% floor and receives federal APTC only, # even with an APTC-eligible member. Confirms the fraction-0 edge is on the # ineligible side of the floor rather than a spurious pass-through. @@ -250,7 +381,10 @@ tax_units: tax_unit: members: [person1] - aca_magi_fraction: 0 + aca_magi: 0 + tax_unit_fpg: + 2025: 10_000 + tax_unit_is_filer: true households: household: members: [person1] @@ -261,7 +395,7 @@ # ---- Applicable return filer gate (Program Design (d)(7)) ---- - name: Case 13, non-filer in-band CA unit is ineligible. - # aca_magi_fraction 1.40 in [1.0,1.65] and APTC-eligible, but the tax unit is + # Exact ratio 1.40 in [1.0,1.65] and APTC-eligible, but the tax unit is # not an applicable return filer, so ca_premium_subsidy_eligible is false. # GC 100810(a)(6) requires filing a California return; a non-filer forms no # claiming unit. @@ -274,7 +408,9 @@ tax_units: tax_unit: members: [person1] - aca_magi_fraction: 1.40 + aca_magi: 14_000 + tax_unit_fpg: + 2025: 10_000 tax_unit_is_filer: false # Nor does it take up the federal credit, so it files no return to claim it. takes_up_aca_if_eligible: false @@ -297,7 +433,9 @@ tax_units: tax_unit: members: [person1] - aca_magi_fraction: 1.40 + aca_magi: 14_000 + tax_unit_fpg: + 2025: 10_000 tax_unit_is_filer: true households: household: @@ -326,7 +464,9 @@ tax_unit: members: [person1, person2] filing_status: SEPARATE - aca_magi_fraction: 1.40 + aca_magi: 14_000 + tax_unit_fpg: + 2025: 10_000 tax_unit_is_filer: true households: household: @@ -351,7 +491,57 @@ tax_units: tax_unit: members: [person1] - aca_magi_fraction: 1.40 + aca_magi: 14_000 + tax_unit_fpg: + 2025: 10_000 + tax_unit_is_filer: true + households: + household: + members: [person1] + state_code: CA + output: + ca_premium_subsidy_eligible: false + +# ---- PY2027 FPL-limit edge (under 200%) ---- + +- name: Case 17, PY2027 at 199% FPL (1.99) is eligible (limit under 200%). + # PY2027 Program Design (d)(7): "under 200 percent". 1.99 < 2.0 -> in-band. + # Confirms the widened PY2027 band admits units up to 199% FPL. + period: 2027 + input: + people: + person1: + age: 40 + is_aca_ptc_eligible: true + tax_units: + tax_unit: + members: [person1] + aca_magi: 19_900 + tax_unit_fpg: + 2026: 10_000 + tax_unit_is_filer: true + households: + household: + members: [person1] + state_code: CA + output: + ca_premium_subsidy_eligible: true + +- name: Case 18, PY2027 at exactly 200% FPL (2.00) is ineligible (strict limit). + # PY2027 fpl_limit 2.0 with fpl_limit_inclusive false: 2.00 is not under 2.0. + # Pairs with Case 17 to bracket the PY2027 upper edge. + period: 2027 + input: + people: + person1: + age: 40 + is_aca_ptc_eligible: true + tax_units: + tax_unit: + members: [person1] + aca_magi: 20_000 + tax_unit_fpg: + 2026: 10_000 tax_unit_is_filer: true households: household: @@ -360,50 +550,104 @@ output: ca_premium_subsidy_eligible: false -# ---- PY2027 FPL-limit edge (limit raised to 1.99) ---- +- name: Case 17a, PY2027 at 199.5% FPL (1.995) is eligible. + # Exact ratio 19_950 / 10_000 = 1.995 < 2.0. A limit of 1.99 on the exact + # ratio would wrongly exclude incomes between 199% and 200% of FPL. + period: 2027 + input: + people: + person1: + age: 40 + is_aca_ptc_eligible: true + tax_units: + tax_unit: + members: [person1] + aca_magi: 19_950 + tax_unit_fpg: + 2026: 10_000 + tax_unit_is_filer: true + households: + household: + members: [person1] + state_code: CA + output: + ca_premium_subsidy_eligible: true -- name: Case 17, PY2027 at 199% FPL (1.99) is eligible (limit raised to 1.99). - # PY2027 fpl_limit 1.99: 1.99 <= 1.99 -> in-band. Confirms the widened - # PY2027 band admits units up to 199% FPL. +# ---- PY2027 200% limit from derived income ---- + +- name: Case 17b, PY2027 income of 199.994% of FPL is eligible. + # $31,919 / 2026 one-person FPG $15,960 = 199.9937%, which is "under 200 + # percent" (2027 Program Design (d)(7)). period: 2027 + absolute_error_margin: 0.0001 input: people: person1: age: 40 + employment_income: 31_919 is_aca_ptc_eligible: true tax_units: tax_unit: members: [person1] - aca_magi_fraction: 1.99 tax_unit_is_filer: true households: household: members: [person1] state_code: CA output: + aca_magi: 31_919 + aca_magi_fraction: 1.99 ca_premium_subsidy_eligible: true -- name: Case 18, PY2027 at 200% FPL (2.00) is ineligible (just above the 1.99 limit). - # PY2027 fpl_limit 1.99: 2.00 > 1.99 -> above the band. Pairs with Case 17 to - # bracket the PY2027 upper edge. +- name: Case 18b, PY2027 income of exactly 200% of FPL is ineligible. + # $31,920 / $15,960 = 200.000%, which is not "under 200 percent". period: 2027 + absolute_error_margin: 0.0001 input: people: person1: age: 40 + employment_income: 31_920 is_aca_ptc_eligible: true tax_units: tax_unit: members: [person1] - aca_magi_fraction: 2.00 tax_unit_is_filer: true households: household: members: [person1] state_code: CA output: + aca_magi: 31_920 + aca_magi_fraction: 2.00 ca_premium_subsidy_eligible: false +- name: Case 18c, PY2027 income of 165.006% of FPL is eligible (165% is the PY2026 limit only). + # $26,335 / $15,960 = 165.0063%. Ineligible under the PY2026 limit, but + # inside the PY2027 band, which runs to under 200%. + period: 2027 + absolute_error_margin: 0.0001 + input: + people: + person1: + age: 40 + employment_income: 26_335 + is_aca_ptc_eligible: true + tax_units: + tax_unit: + members: [person1] + tax_unit_is_filer: true + households: + household: + members: [person1] + state_code: CA + output: + aca_magi: 26_335 + aca_magi_fraction: 1.65 + ca_premium_subsidy_eligible: true + +# ---- Filing via federal credit take-up ---- + - name: Case 13b, an in-band CA unit that takes up the federal credit files and is eligible. # Case 13 with take-up true and a positive federal credit ($6,000 benchmark # less 4.19% of $22,000 = $5,078.20): a tax unit that takes up the credit diff --git a/policyengine_us/tests/unit/test_ca_premium_subsidy_eligible_properties.py b/policyengine_us/tests/unit/test_ca_premium_subsidy_eligible_properties.py new file mode 100644 index 00000000000..4988ed6b746 --- /dev/null +++ b/policyengine_us/tests/unit/test_ca_premium_subsidy_eligible_properties.py @@ -0,0 +1,130 @@ +"""Properties of the California Premium Subsidy income band. + +Program Design (d)(7) defines an applicable return filer by household income +"at or above 100 percent and at or below 165 percent of the federal poverty +level" in PY2026, and "at or above 100 percent and under 200 percent" in +PY2027. The bounds below are transcribed from those Designs, not read from +the parameters, and are evaluated in exact rational arithmetic. + +The model divides nonnegative ACA MAGI by the prior-year poverty guideline. +It must not use aca_magi_fraction for the band: that fraction truncates to +whole percentages, so every income from 165.00% to 165.99% of the guideline +becomes 1.65 and would pass an "at or below 165 percent" test. FTB 3849 +Table 1-2, for the predecessor California program, likewise truncates for +the rate lookup but decides band edges on the exact percentage. + +- The parameters match the Designs' bounds and inclusivity. +- With every non-income condition met, eligibility equals the exact reading + of (d)(7) for any income and guideline. Incomes are whole dollars, and some + guidelines are multiples of $20, so 165% and 200% of the guideline are + themselves whole dollars and the inclusive and strict edges are hit + exactly. +""" + +from fractions import Fraction + +import numpy as np +import pytest +from hypothesis import example, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +# Program Design (d)(7), by plan year: (floor, limit, limit is inclusive). +DESIGN_BANDS = { + # 2026 Design: "at or above 100 percent and at or below 165 percent". + 2026: (Fraction(1), Fraction(165, 100), True), + # 2027 Design V4: "at or above 100 percent and under 200 percent". + 2027: (Fraction(1), Fraction(2), False), +} +EDGES = (Fraction(1), Fraction(165, 100), Fraction(2)) +TAX_UNITS = 64 + + +def design_eligible(year, income, guideline): + floor, limit, inclusive = DESIGN_BANDS[year] + ratio = Fraction(max(income, 0), guideline) + below_limit = ratio <= limit if inclusive else ratio < limit + return ratio >= floor and below_limit + + +@pytest.fixture(scope="module") +def simulations(): + """One simulation per plan year, with every non-income condition met.""" + sims = {} + for year in DESIGN_BANDS: + sim = Simulation( + situation={ + "people": {f"p{i}": {"age": {year: 40}} for i in range(TAX_UNITS)}, + "tax_units": { + f"t{i}": {"members": [f"p{i}"]} for i in range(TAX_UNITS) + }, + "households": { + f"h{i}": {"members": [f"p{i}"], "state_code": {year: "CA"}} + for i in range(TAX_UNITS) + }, + } + ) + sim.set_input("is_aca_ptc_eligible", year, np.ones(TAX_UNITS, dtype=bool)) + sim.set_input("files_return_for_aca_ptc", year, np.ones(TAX_UNITS, dtype=bool)) + sims[year] = sim + return sims + + +def model_eligible(sim, year, incomes, guidelines): + sim.set_input("aca_magi", year, np.asarray(incomes, dtype=float)) + sim.set_input("tax_unit_fpg", year - 1, np.asarray(guidelines, dtype=float)) + sim.delete_arrays("ca_premium_subsidy_eligible", year) + return sim.calculate("ca_premium_subsidy_eligible", year) + + +@st.composite +def tax_unit_incomes(draw): + """A whole-dollar income and guideline, often within $2 of a band edge.""" + guideline = draw( + st.one_of( + st.integers(10_000, 80_000), + st.integers(500, 4_000).map(lambda k: 20 * k), + ) + ) + edge = draw(st.sampled_from(EDGES)) + near_edge = int(edge * guideline) + draw(st.integers(-2, 2)) + income = draw(st.one_of(st.just(near_edge), st.integers(-50_000, 3 * guideline))) + return income, guideline + + +@pytest.mark.parametrize("year", sorted(DESIGN_BANDS)) +def test_parameters_match_program_design(year, simulations): + p = ( + simulations[year] + .tax_benefit_system.parameters(f"{year}-01-01") + .gov.states.ca.hbex.premium_subsidy + ) + floor, limit, inclusive = DESIGN_BANDS[year] + assert p.in_effect + assert Fraction(str(p.fpl_floor)) == floor + assert Fraction(str(p.fpl_limit)) == limit + assert bool(p.fpl_limit_inclusive) is inclusive + + +@pytest.mark.parametrize("year", sorted(DESIGN_BANDS)) +@settings(max_examples=60, deadline=None, derandomize=True) +@given(units=st.lists(tax_unit_incomes(), min_size=1, max_size=TAX_UNITS)) +# PY2026, 2025 one-person guideline $15,650: 165.000%, 165.003%, 165.994% +# and 166.000%; truncation maps the middle two to 1.65. +@example(units=[(25_822, 15_650), (25_823, 15_650), (25_978, 15_650), (25_979, 15_650)]) +# A $20-multiple guideline puts 100%, 165% and 200% on whole dollars. +@example(units=[(16_000, 16_000), (26_400, 16_000), (26_401, 16_000), (32_000, 16_000)]) +def test_eligibility_matches_exact_program_design_band(year, simulations, units): + padded = (units * TAX_UNITS)[:TAX_UNITS] + incomes, guidelines = zip(*padded) + expected = [ + design_eligible(year, income, guideline) for income, guideline in padded + ] + actual = model_eligible(simulations[year], year, incomes, guidelines) + mismatches = [ + (income, guideline, bool(a)) + for income, guideline, a, e in zip(incomes, guidelines, actual, expected) + if bool(a) != e + ] + assert not mismatches diff --git a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py index eb31d0a2ee2..aede8e91064 100644 --- a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py +++ b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py @@ -12,6 +12,11 @@ class ca_premium_subsidy_eligible(Variable): # return filer definition at Program Design (d)(7). "https://board.coveredca.com/meetings/2025/July%2028,%202025/CoveredCA_2026_Premium_Subsidy_Program_Design_Final.pdf#page=1", "https://board.coveredca.com/meetings/2025/July%2028,%202025/CoveredCA_2026_Premium_Subsidy_Program_Design_Final.pdf#page=2", + # 2027 Program Design V4, (d)(7): "under 200 percent". + "https://board.coveredca.com/meetings/2026/May%2021,%202026/CoveredCA_2027_Premium_Subsidy_Program_Design_V4_05-19-26.pdf#page=2", + # FTB 3849 Table 1-2 (predecessor program): exact percentage decides + # the band edge; the truncated percentage only drives the rate lookup. + "https://www.ftb.ca.gov/forms/2020/2020-3849-instructions.pdf#page=6", # GC Title 25 Section 100805 authorizes the subsidy and disclaims the # federal income requirements; Section 100810 defines eligibility. "https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=GOV§ionNum=100805.", @@ -35,8 +40,20 @@ def formula(tax_unit, period, parameters): # California band. The unmodeled 26 CFR 1.36B-2(b)(2)(ii)-(v) MFS # exceptions (abandoned-spouse, domestic-abuse relief) are not applied. aptc_eligible = add(tax_unit, period, ["is_aca_ptc_eligible"]) > 0 - magi_frac = tax_unit("aca_magi_fraction", period) - income_eligible = (magi_frac >= p.fpl_floor) & (magi_frac <= p.fpl_limit) + # Program Design (d)(7) bounds household income "at or above 100 + # percent" and, in PY2026, "at or below 165 percent" (PY2027: "under + # 200 percent") of the FPL. Test the exact ratio, not the truncated + # aca_magi_fraction: truncation maps 165.00%-165.99% to 1.65, which + # would admit incomes above 165%. FTB 3849 Table 1-2 (2020) likewise + # decides band edges on the exact percentage. + magi = max_(tax_unit("aca_magi", period), 0) + exact_magi_frac = magi / tax_unit("tax_unit_fpg", period.last_year) + below_limit = where( + p.fpl_limit_inclusive, + exact_magi_frac <= p.fpl_limit, + exact_magi_frac < p.fpl_limit, + ) + income_eligible = (exact_magi_frac >= p.fpl_floor) & below_limit # An applicable return filer (Program Design (d)(7); GC # Section 100810(a)(6)) must file a California return, so the tax unit # must be a filer. This gate replaces reliance on the federal 100% FPL From 85f63684e40eaef75c4336274fbad214970bd63a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 13:35:27 -0400 Subject: [PATCH 07/12] Add 100% floor cases from derived income; merge duplicate reference Cases 1b and 2b test the PY2026 floor at $15,649 (99.994%, ineligible) and $15,650 (exactly 100%, eligible) of the 2025 one-person FPG, so the PE tests mirror the companion cases added to rulespec-us#1832. The two 2026 Program Design references differed only by #page=, so they become one entry per the repo's reference rules. Co-Authored-By: Claude Opus 5.5 --- .../ca_premium_subsidy_eligible.yaml | 49 ++++++++++++++++++- .../ca_premium_subsidy_eligible.py | 2 +- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.yaml index 769e1323623..f683a143464 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.yaml @@ -126,7 +126,54 @@ output: ca_premium_subsidy_eligible: false -# ---- PY2026 165% limit from derived income (exact ratio, not truncation) ---- +# ---- PY2026 limits from derived income (exact ratio, not truncation) ---- + +- name: Case 1b, PY2026 income of 99.994% of FPL is ineligible. + # $15,649 / 2025 one-person FPG $15,650 = 99.9936%, below "at or above 100 + # percent" (Program Design (d)(7)). + period: 2026 + absolute_error_margin: 0.0001 + input: + people: + person1: + age: 40 + employment_income: 15_649 + is_aca_ptc_eligible: true + tax_units: + tax_unit: + members: [person1] + tax_unit_is_filer: true + households: + household: + members: [person1] + state_code: CA + output: + aca_magi: 15_649 + aca_magi_fraction: 0.99 + ca_premium_subsidy_eligible: false + +- name: Case 2b, PY2026 income of exactly 100% of FPL is eligible. + # $15,650 / $15,650 = 100.000%, "at or above 100 percent". + period: 2026 + absolute_error_margin: 0.0001 + input: + people: + person1: + age: 40 + employment_income: 15_650 + is_aca_ptc_eligible: true + tax_units: + tax_unit: + members: [person1] + tax_unit_is_filer: true + households: + household: + members: [person1] + state_code: CA + output: + aca_magi: 15_650 + aca_magi_fraction: 1.00 + ca_premium_subsidy_eligible: true - name: Case 4b, PY2026 income of exactly 165% of FPL is eligible. # $25,822.50 / 2025 one-person FPG $15,650 = 165.000%, which is "at or below diff --git a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py index aede8e91064..f4ca328b4b3 100644 --- a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py +++ b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py @@ -10,8 +10,8 @@ class ca_premium_subsidy_eligible(Variable): reference = ( # 2026 Program Design (Final): eligibility bands and the applicable # return filer definition at Program Design (d)(7). + # PDF pages 1-2 "https://board.coveredca.com/meetings/2025/July%2028,%202025/CoveredCA_2026_Premium_Subsidy_Program_Design_Final.pdf#page=1", - "https://board.coveredca.com/meetings/2025/July%2028,%202025/CoveredCA_2026_Premium_Subsidy_Program_Design_Final.pdf#page=2", # 2027 Program Design V4, (d)(7): "under 200 percent". "https://board.coveredca.com/meetings/2026/May%2021,%202026/CoveredCA_2027_Premium_Subsidy_Program_Design_V4_05-19-26.pdf#page=2", # FTB 3849 Table 1-2 (predecessor program): exact percentage decides From d9929e416a369d4ce860b90e68598fd3277f990f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 19:26:59 -0400 Subject: [PATCH 08/12] Divide in float64; make the property test cache-safe The model stores MAGI and FPG as float32. A float32 quotient can round a ratio a fraction of a cent above 165% of a fractional guideline down to 1.65 (e.g. $18,711.599609375 over $11,340.36328125), so the gate now divides in float64. On float32 inputs that classifies every ratio exactly against 1.00, 1.65 and 2.00. The property test built one simulation per year and reused it, deleting only the output array, so derived intermediates could stay cached across Hypothesis examples. It now builds a fresh simulation per example. Its reference reads (d)(7) on the stored float32 values, the generator adds fractional guidelines and incomes a few float32 steps from each edge, and explicit examples cover the float32 counterexamples and a 72-person household at exactly 166% that main's float32 truncation misrounds to 165.99%. Co-Authored-By: Claude Opus 5.5 --- ..._ca_premium_subsidy_eligible_properties.py | 136 ++++++++++-------- .../ca_premium_subsidy_eligible.py | 5 +- 2 files changed, 84 insertions(+), 57 deletions(-) diff --git a/policyengine_us/tests/unit/test_ca_premium_subsidy_eligible_properties.py b/policyengine_us/tests/unit/test_ca_premium_subsidy_eligible_properties.py index 4988ed6b746..85950e9f25e 100644 --- a/policyengine_us/tests/unit/test_ca_premium_subsidy_eligible_properties.py +++ b/policyengine_us/tests/unit/test_ca_premium_subsidy_eligible_properties.py @@ -13,12 +13,19 @@ Table 1-2, for the predecessor California program, likewise truncates for the rate lookup but decides band edges on the exact percentage. +The model stores MAGI and the guideline as float32 and divides them in +float64, so the reference reads (d)(7) exactly on the stored float32 values. + - The parameters match the Designs' bounds and inclusivity. - With every non-income condition met, eligibility equals the exact reading - of (d)(7) for any income and guideline. Incomes are whole dollars, and some - guidelines are multiples of $20, so 165% and 200% of the guideline are - themselves whole dollars and the inclusive and strict edges are hit - exactly. + of (d)(7) for any stored income and guideline. The examples mix three kinds + of income: whole dollars near each edge (some guidelines are multiples of + $20, which put 165% and 200% on whole dollars); a few float32 steps either + side of each edge; and incomes across the range. Some guidelines are + fractional, like the projected ones. + +Each example builds a fresh simulation, so nothing computed for one example +is cached into the next. """ from fractions import Fraction @@ -29,6 +36,7 @@ from hypothesis import strategies as st from policyengine_us import Simulation +from policyengine_us.system import system # Program Design (d)(7), by plan year: (floor, limit, limit is inclusive). DESIGN_BANDS = { @@ -38,68 +46,73 @@ 2027: (Fraction(1), Fraction(2), False), } EDGES = (Fraction(1), Fraction(165, 100), Fraction(2)) -TAX_UNITS = 64 +MAX_TAX_UNITS = 128 + + +def stored(value): + """The exact value of a number after float32 storage.""" + return Fraction(float(np.float32(value))) def design_eligible(year, income, guideline): floor, limit, inclusive = DESIGN_BANDS[year] - ratio = Fraction(max(income, 0), guideline) + ratio = max(stored(income), 0) / stored(guideline) below_limit = ratio <= limit if inclusive else ratio < limit return ratio >= floor and below_limit -@pytest.fixture(scope="module") -def simulations(): - """One simulation per plan year, with every non-income condition met.""" - sims = {} - for year in DESIGN_BANDS: - sim = Simulation( - situation={ - "people": {f"p{i}": {"age": {year: 40}} for i in range(TAX_UNITS)}, - "tax_units": { - f"t{i}": {"members": [f"p{i}"]} for i in range(TAX_UNITS) - }, - "households": { - f"h{i}": {"members": [f"p{i}"], "state_code": {year: "CA"}} - for i in range(TAX_UNITS) - }, - } - ) - sim.set_input("is_aca_ptc_eligible", year, np.ones(TAX_UNITS, dtype=bool)) - sim.set_input("files_return_for_aca_ptc", year, np.ones(TAX_UNITS, dtype=bool)) - sims[year] = sim - return sims - - -def model_eligible(sim, year, incomes, guidelines): +def model_eligible(year, incomes, guidelines): + """Eligibility from a fresh simulation with every non-income condition met.""" + n = len(incomes) + sim = Simulation( + situation={ + "people": {f"p{i}": {"age": {year: 40}} for i in range(n)}, + "tax_units": {f"t{i}": {"members": [f"p{i}"]} for i in range(n)}, + "households": { + f"h{i}": {"members": [f"p{i}"], "state_code": {year: "CA"}} + for i in range(n) + }, + } + ) + sim.set_input("is_aca_ptc_eligible", year, np.ones(n, dtype=bool)) + sim.set_input("files_return_for_aca_ptc", year, np.ones(n, dtype=bool)) sim.set_input("aca_magi", year, np.asarray(incomes, dtype=float)) sim.set_input("tax_unit_fpg", year - 1, np.asarray(guidelines, dtype=float)) - sim.delete_arrays("ca_premium_subsidy_eligible", year) return sim.calculate("ca_premium_subsidy_eligible", year) +def float32_steps(value, steps): + """The float32 value `steps` representable numbers away from `value`.""" + x = np.float32(value) + toward = np.float32(np.inf if steps > 0 else -np.inf) + for _ in range(abs(steps)): + x = np.nextafter(x, toward) + return float(x) + + @st.composite def tax_unit_incomes(draw): - """A whole-dollar income and guideline, often within $2 of a band edge.""" guideline = draw( st.one_of( - st.integers(10_000, 80_000), - st.integers(500, 4_000).map(lambda k: 20 * k), + st.integers(10_000, 80_000).map(float), + st.integers(500, 4_000).map(lambda k: 20.0 * k), + st.floats(10_000, 500_000, width=32), ) ) - edge = draw(st.sampled_from(EDGES)) - near_edge = int(edge * guideline) + draw(st.integers(-2, 2)) - income = draw(st.one_of(st.just(near_edge), st.integers(-50_000, 3 * guideline))) + edge_income = draw(st.sampled_from(EDGES)) * stored(guideline) + kind = draw(st.sampled_from(["whole_dollars", "float32_steps", "any"])) + if kind == "whole_dollars": + income = float(int(edge_income) + draw(st.integers(-2, 2))) + elif kind == "float32_steps": + income = float32_steps(float(edge_income), draw(st.integers(-3, 3))) + else: + income = draw(st.floats(-50_000, float(np.float32(3 * guideline)), width=32)) return income, guideline @pytest.mark.parametrize("year", sorted(DESIGN_BANDS)) -def test_parameters_match_program_design(year, simulations): - p = ( - simulations[year] - .tax_benefit_system.parameters(f"{year}-01-01") - .gov.states.ca.hbex.premium_subsidy - ) +def test_parameters_match_program_design(year): + p = system.parameters(f"{year}-01-01").gov.states.ca.hbex.premium_subsidy floor, limit, inclusive = DESIGN_BANDS[year] assert p.in_effect assert Fraction(str(p.fpl_floor)) == floor @@ -108,23 +121,34 @@ def test_parameters_match_program_design(year, simulations): @pytest.mark.parametrize("year", sorted(DESIGN_BANDS)) -@settings(max_examples=60, deadline=None, derandomize=True) -@given(units=st.lists(tax_unit_incomes(), min_size=1, max_size=TAX_UNITS)) -# PY2026, 2025 one-person guideline $15,650: 165.000%, 165.003%, 165.994% -# and 166.000%; truncation maps the middle two to 1.65. -@example(units=[(25_822, 15_650), (25_823, 15_650), (25_978, 15_650), (25_979, 15_650)]) +@settings(max_examples=30, deadline=None, derandomize=True) +@given(units=st.lists(tax_unit_incomes(), min_size=1, max_size=MAX_TAX_UNITS)) +# PY2026, 2025 one-person guideline $15,650: 164.997%, 165.000%, 165.003%, +# 165.994% and 166.000%; truncation maps 165.000% to 165.994% to 1.65. +@example( + units=[ + (25_822, 15_650), + (25_822.5, 15_650), + (25_823, 15_650), + (25_978, 15_650), + (25_979, 15_650), + ] +) # A $20-multiple guideline puts 100%, 165% and 200% on whole dollars. @example(units=[(16_000, 16_000), (26_400, 16_000), (26_401, 16_000), (32_000, 16_000)]) -def test_eligibility_matches_exact_program_design_band(year, simulations, units): - padded = (units * TAX_UNITS)[:TAX_UNITS] - incomes, guidelines = zip(*padded) - expected = [ - design_eligible(year, income, guideline) for income, guideline in padded - ] - actual = model_eligible(simulations[year], year, incomes, guidelines) +# The 2025 guideline for 72 people, $406,150: $674,209 is exactly 166%, which +# float32 truncation in aca_magi_fraction misrounds to 165.99%. +@example(units=[(674_209, 406_150), (812_300, 406_150)]) +# Stored float32 incomes a fraction of a cent above 165% of a fractional +# guideline: a float32 quotient rounds both ratios down to 1.65. +@example(units=[(18_711.599609375, 11_340.36328125), (37_907.703125, 22_974.365234375)]) +def test_eligibility_matches_exact_program_design_band(year, units): + incomes, guidelines = zip(*units) + expected = [design_eligible(year, i, g) for i, g in units] + actual = model_eligible(year, incomes, guidelines) mismatches = [ (income, guideline, bool(a)) - for income, guideline, a, e in zip(incomes, guidelines, actual, expected) + for (income, guideline), a, e in zip(units, actual, expected) if bool(a) != e ] assert not mismatches diff --git a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py index f4ca328b4b3..02275f2918b 100644 --- a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py +++ b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py @@ -47,7 +47,10 @@ def formula(tax_unit, period, parameters): # would admit incomes above 165%. FTB 3849 Table 1-2 (2020) likewise # decides band edges on the exact percentage. magi = max_(tax_unit("aca_magi", period), 0) - exact_magi_frac = magi / tax_unit("tax_unit_fpg", period.last_year) + fpg = tax_unit("tax_unit_fpg", period.last_year) + # Divide in float64: a float32 quotient can round a ratio just above + # a limit down onto it. + exact_magi_frac = magi.astype(np.float64) / fpg below_limit = where( p.fpl_limit_inclusive, exact_magi_frac <= p.fpl_limit, From cc1af12cd1e9ae88ee8e02e273313ea49d65b45f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 23:07:54 -0400 Subject: [PATCH 09/12] Address California premium subsidy reviews and record the 150 percent source conflict --- .../statute-first-ca-premium-subsidy.fixed.md | 2 +- .../code_health/test_source_conflicts.py | 1 + .../premium_subsidy/ca_premium_subsidy.yaml | 46 +++++++++- ...premium_subsidy_applicable_percentage.yaml | 84 ------------------- ...a_premium_subsidy_applicable_percentage.py | 10 +-- ...a_premium_subsidy_applicable_percentage.py | 11 ++- source_conflicts/candidates.yaml | 45 ++++++++++ 7 files changed, 103 insertions(+), 96 deletions(-) diff --git a/changelog.d/statute-first-ca-premium-subsidy.fixed.md b/changelog.d/statute-first-ca-premium-subsidy.fixed.md index d1a583bcac8..edcb4be4da5 100644 --- a/changelog.d/statute-first-ca-premium-subsidy.fixed.md +++ b/changelog.d/statute-first-ca-premium-subsidy.fixed.md @@ -1 +1 @@ -Select California Premium Subsidy bands from exact nonnegative ACA MAGI divided by prior-year FPL under the adopted [2026](https://board.coveredca.com/meetings/2025/July%2028,%202025/CoveredCA_2026_Premium_Subsidy_Program_Design_Final.pdf#page=2) and [2027 Designs, (d)(4)](https://board.coveredca.com/meetings/2026/May%2021,%202026/CoveredCA_2027_Premium_Subsidy_Program_Design_V4_05-19-26.pdf#page=2): exactly 150% pays 0%, while income above 150% enters the 3.19% or 2.69% band. Retain the truncated fraction for the rate within that band, following [FTB 3849 Table 1-2 (2020)](https://www.ftb.ca.gov/forms/2020/2020-3849-instructions.pdf#page=6) and [2021](https://www.ftb.ca.gov/forms/2021/2021-3849-instructions.pdf#page=6); [Form 8962 Worksheet 2 line 4](https://www.irs.gov/pub/irs-pdf/i8962.pdf#page=9) likewise tests "more than" on the exact amount before truncating. +Select California Premium Subsidy bands from exact nonnegative ACA MAGI divided by prior-year FPL under the adopted [2026 Program Design, (d)(4)](https://board.coveredca.com/meetings/2025/July%2028,%202025/CoveredCA_2026_Premium_Subsidy_Program_Design_Final.pdf#page=2): exactly 150% changes from a 3.19% contribution to 0%, while income above 150% enters the 3.19% band. Retain the truncated fraction for the rate within that band, following [FTB 3849 Table 1-2 (2020)](https://www.ftb.ca.gov/forms/2020/2020-3849-instructions.pdf#page=6) and [2021](https://www.ftb.ca.gov/forms/2021/2021-3849-instructions.pdf#page=6); [2025 Form 8962 Worksheet 2 line 4](https://www.irs.gov/pub/irs-prior/i8962--2025.pdf#page=9) likewise tests "more than" on the exact amount before truncating. diff --git a/policyengine_us/tests/code_health/test_source_conflicts.py b/policyengine_us/tests/code_health/test_source_conflicts.py index 901b7b37215..ea7b9fbdddd 100644 --- a/policyengine_us/tests/code_health/test_source_conflicts.py +++ b/policyengine_us/tests/code_health/test_source_conflicts.py @@ -152,6 +152,7 @@ "policyengine_us/variables/gov/local/ca/cc/ga/eligibility/ca_cc_general_assistance_eligible.py": 1, "policyengine_us/variables/gov/local/tax/sales/local_sales_tax.py": 1, "policyengine_us/variables/gov/states/ar/ade/oec/sra/rates/ar_sra_daily_copay.py": 1, + "policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.py": 1, "policyengine_us/variables/gov/states/ct/tax/income/rebate/ct_child_tax_rebate.py": 1, "policyengine_us/variables/gov/states/fl/commerce/liheap/eligibility/fl_liheap_eligible.py": 1, "policyengine_us/variables/gov/states/fl/commerce/liheap/eligibility/fl_liheap_income_limit.py": 1, diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml index 37b03c4bf7d..da255575843 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml @@ -9,6 +9,9 @@ # figure (spread across 12 months and re-summed by add()), matching the NM # convention. aca_magi, aca_magi_fraction, slcsp, aca_ptc and is_aca_ptc_eligible # are supplied directly so each expectation is exactly hand-verifiable. +# Prior-year tax_unit_fpg is pinned so supplied MAGI and FPL fractions agree. +# The negative-MAGI clamp case supplies eligibility and a positive rate to +# isolate the amount formula; its income fraction is correctly zero. # CA schedule: threshold [1.0, 1.5, 1.65], initial [0, 0.0319], final [0, 0.0391] # ---- 100-150% FPL band (applicable percentage 0%) ---- @@ -27,6 +30,8 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2025: 15_000 aca_magi: 21_000 aca_magi_fraction: 1.40 slcsp: 6_000 @@ -50,6 +55,8 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2025: 15_000 aca_magi: 21_000 aca_magi_fraction: 1.40 slcsp: 6_000 @@ -77,6 +84,8 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2025: 15_873.015873015873 aca_magi: 25_000 aca_magi_fraction: 1.575 slcsp: 6_000 @@ -102,6 +111,8 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2025: 15_757.575757575758 aca_magi: 26_000 aca_magi_fraction: 1.65 slcsp: 7_000 @@ -127,6 +138,8 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2025: 15_000 aca_magi: 24_000 aca_magi_fraction: 1.60 slcsp: 3_000 @@ -141,8 +154,10 @@ # ---- Negative-MAGI clamp ---- - name: Case 6, negative ACA MAGI is clamped to zero (does not inflate subsidy). - # Inject pct 0.0355 to isolate the amount formula's clamp: exact negative - # MAGI would otherwise select the 0% band, hiding a missing amount clamp. + # Supply eligibility and pct 0.0355 to isolate the amount clamp: + # exact negative MAGI would otherwise be ineligible and select the 0% band, + # hiding a missing amount clamp. The income fraction is zero because + # aca_magi_fraction floors negative MAGI at zero before dividing by FPG. # aca_magi -10_000 clamps to 0 so the -pct*income # term is 0 rather than +355. amount = max(0, 6_000 - 2_000 - 0.0355*0) = 4_000. # Without the clamp it would be 6_000 - 2_000 - 0.0355*(-10_000) = 4_355. @@ -156,8 +171,11 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2025: 10_000 aca_magi: -10_000 - aca_magi_fraction: 1.575 + aca_magi_fraction: 0 + ca_premium_subsidy_eligible: true ca_premium_subsidy_applicable_percentage: 0.0355 slcsp: 6_000 aca_ptc: 2_000 @@ -183,6 +201,8 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2025: 15_873.015873015873 aca_magi: 25_000 aca_magi_fraction: 1.575 slcsp: 6_000 @@ -207,6 +227,8 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2024: 15_873.015873015873 aca_magi: 25_000 aca_magi_fraction: 1.575 slcsp: 6_000 @@ -231,6 +253,8 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2025: 16_265.060240963856 aca_magi: 27_000 aca_magi_fraction: 1.66 slcsp: 6_000 @@ -255,6 +279,8 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2025: 15_000 aca_magi: 21_000 aca_magi_fraction: 1.40 slcsp: 0 @@ -283,6 +309,8 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2025: 15_000 aca_magi: 15_000 aca_magi_fraction: 1.00 slcsp: 6_000 @@ -313,6 +341,8 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2025: 15_000 aca_magi: 24_000 aca_magi_fraction: 1.60 slcsp: 6_000 @@ -341,6 +371,8 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2027: 15_873.015873015873 aca_magi: 25_000 aca_magi_fraction: 1.575 slcsp: 6_000 @@ -371,6 +403,8 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2025: 16_265.060240963856 aca_magi: 27_000 aca_magi_fraction: 1.66 slcsp: 6_000 @@ -399,6 +433,8 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2025: 15_000 aca_magi: 14_850 aca_magi_fraction: 0.99 slcsp: 6_000 @@ -430,6 +466,8 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2026: 15_873.015873015873 aca_magi: 25_000 aca_magi_fraction: 1.575 slcsp: 6_000 @@ -461,6 +499,8 @@ tax_units: tax_unit: members: [person1] + tax_unit_fpg: + 2025: 15_873.015873015873 aca_magi: 25_000 aca_magi_fraction: 1.575 slcsp: 6_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml index d478d144905..a5b10f361ab 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml @@ -552,87 +552,3 @@ output: aca_magi_fraction: 1.50 ca_premium_subsidy_applicable_percentage: 0 - -- name: Case 16b, PY2027 raw income $23,942, just above 150% enters the paying band. - # $23,942 / 2026 one-person FPG $15,960 = 150.0125%. - # Exact income selects the band; the derived fraction 1.50 determines - # the rate 0.0269, for an annual contribution of $644.04. - period: 2027 - absolute_error_margin: 0.000001 - input: - people: - person1: - age: 40 - employment_income: - 2027: 23_942 - is_aca_ptc_eligible: true - households: - household: - members: [person1] - state_code: CA - output: - aca_magi_fraction: 1.50 - ca_premium_subsidy_applicable_percentage: 0.0269 - -- name: Case 16c, PY2027 raw income $24,036, 150.6% pays the starting rate despite truncating to 150%. - # $24,036 / 2026 one-person FPG $15,960 = 150.6015%. - # Exact income selects the band; the derived fraction 1.50 determines - # the rate 0.0269, for an annual contribution of $646.57. - period: 2027 - absolute_error_margin: 0.000001 - input: - people: - person1: - age: 40 - employment_income: - 2027: 24_036 - is_aca_ptc_eligible: true - households: - household: - members: [person1] - state_code: CA - output: - aca_magi_fraction: 1.50 - ca_premium_subsidy_applicable_percentage: 0.0269 - -- name: Case 16d, PY2027 raw income $24,099, just below 151% keeps the starting rate. - # $24,099 / 2026 one-person FPG $15,960 = 150.9962%. - # Exact income selects the band; the derived fraction 1.50 determines - # the rate 0.0269, for an annual contribution of $648.26. - period: 2027 - absolute_error_margin: 0.000001 - input: - people: - person1: - age: 40 - employment_income: - 2027: 24_099 - is_aca_ptc_eligible: true - households: - household: - members: [person1] - state_code: CA - output: - aca_magi_fraction: 1.50 - ca_premium_subsidy_applicable_percentage: 0.0269 - -- name: Case 16e, PY2027 raw income $24,100, just above 151% uses the next truncated percentage. - # $24,100 / 2026 one-person FPG $15,960 = 151.0025%. - # Exact income selects the band; the derived fraction 1.51 determines - # the rate 0.02738, for an annual contribution of $659.86. - period: 2027 - absolute_error_margin: 0.000001 - input: - people: - person1: - age: 40 - employment_income: - 2027: 24_100 - is_aca_ptc_eligible: true - households: - household: - members: [person1] - state_code: CA - output: - aca_magi_fraction: 1.51 - ca_premium_subsidy_applicable_percentage: 0.02738 diff --git a/policyengine_us/tests/unit/test_ca_premium_subsidy_applicable_percentage.py b/policyengine_us/tests/unit/test_ca_premium_subsidy_applicable_percentage.py index 8b424af08da..4aecce7c251 100644 --- a/policyengine_us/tests/unit/test_ca_premium_subsidy_applicable_percentage.py +++ b/policyengine_us/tests/unit/test_ca_premium_subsidy_applicable_percentage.py @@ -1,4 +1,4 @@ -"""Properties of the adopted 2026/2027 Design (d)(4) contribution bands. +"""Properties of the adopted 2026 Design (d)(4) contribution bands. FTB 3849 Table 1-2 distinguishes exact band eligibility from the truncated percentage used for the rate lookup. The physical-income properties use @@ -45,7 +45,7 @@ def tax_unit(name, input_period): return formula(tax_unit, calculation_period, system.parameters) -@pytest.mark.parametrize("year", [2026, 2027]) +@pytest.mark.parametrize("year", [2026]) @settings(max_examples=100, deadline=None, derandomize=True) @given(incomes=st.lists(st.integers(-100_000, 600_000), min_size=2, max_size=40)) @example(incomes=[149_999, 150_000, 150_001, 150_999, 151_000]) @@ -54,7 +54,7 @@ def test_rate_never_decreases_with_exact_income(year, incomes): assert np.all(np.diff(rates) >= -1e-12) -@pytest.mark.parametrize("year", [2026, 2027]) +@pytest.mark.parametrize("year", [2026]) @settings(max_examples=100, deadline=None, derandomize=True) @given(income=st.integers(-100_000, 150_000)) @example(income=150_000) @@ -62,7 +62,7 @@ def test_rate_is_zero_at_or_below_exactly_150_percent(year, income): assert _rates(year, [income])[0] == 0 -@pytest.mark.parametrize("year, starting_rate", [(2026, 0.0319), (2027, 0.0269)]) +@pytest.mark.parametrize("year, starting_rate", [(2026, 0.0319)]) @settings(max_examples=100, deadline=None, derandomize=True) @given(income=st.integers(150_001, 150_999)) def test_exact_income_above_150_uses_upper_band_truncated_starting_rate( @@ -73,7 +73,7 @@ def test_exact_income_above_150_uses_upper_band_truncated_starting_rate( @pytest.mark.parametrize( "year, boundary", - [(2026, 1), (2026, 1.65), (2027, 1), (2027, 1.65), (2027, 2)], + [(2026, 1), (2026, 1.65)], ) @settings(max_examples=100, deadline=None, derandomize=True) @given(epsilon=st.floats(min_value=1e-7, max_value=1e-4)) diff --git a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.py b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.py index 49d66771dff..014fc7bc446 100644 --- a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.py +++ b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.py @@ -13,10 +13,11 @@ class ca_premium_subsidy_applicable_percentage(Variable): # benchmark second lowest cost silver plan, bracket-interpolated # linearly within each federal poverty line band. "https://board.coveredca.com/meetings/2025/July%2028,%202025/CoveredCA_2026_Premium_Subsidy_Program_Design_Final.pdf#page=2", - "https://board.coveredca.com/meetings/2026/May%2021,%202026/CoveredCA_2027_Premium_Subsidy_Program_Design_V4_05-19-26.pdf#page=2", + "https://hbex.coveredca.com/data-research/Final-2027-California-Premium-Subsidy-Program-Design.pdf#page=2", + "https://hbex.coveredca.com/toolkit/webinars-briefings/downloads/OE27_Enroller_Kickoff.pdf#page=40", "https://www.ftb.ca.gov/forms/2020/2020-3849-instructions.pdf#page=6", "https://www.ftb.ca.gov/forms/2021/2021-3849-instructions.pdf#page=6", - "https://www.irs.gov/pub/irs-pdf/i8962.pdf#page=9", + "https://www.irs.gov/pub/irs-prior/i8962--2025.pdf#page=9", ) def formula(tax_unit, period, parameters): @@ -37,6 +38,10 @@ def formula(tax_unit, period, parameters): # Form 8962 Worksheet 2 line 4 likewise checks "more than" before # truncation. side="left" keeps an exact threshold in the lower # band; clipping keeps both bracket endpoints in the rate arrays. + # The OE27 enroller example conflicts with the adopted Designs at 150%: + # it charges 2.80%, but (d)(4) starts the paying band "Above 150%". + # Follow the adopted 0% boundary; the citations above document both. + # Candidate: us-ca:premium-subsidy-150-fpl-boundary until registered. bracket_idx = clip( np.searchsorted(thresholds, exact_magi_frac, side="left") - 1, 0, @@ -47,7 +52,7 @@ def formula(tax_unit, period, parameters): bracket_end = thresholds[bracket_idx + 1] # Retain the truncated fraction within the band: 150.6% uses the - # 150% lookup row of the upper band (3.19% in 2026, 2.69% in 2027). + # 150% lookup row of the upper band (3.19% in 2026). position = clip( (magi_frac - bracket_start) / (bracket_end - bracket_start), 0, 1 ) diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 8338e796c45..b51cb11f0bc 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -750,6 +750,51 @@ candidates: state: ruled note: 'The October 2026 statute-first ruling retains the bar; obtain the statutory URL and verify the instruction flow''s joint-return effect. Code not yet on main: ca_eitc_eligible.' +- id: us-ca:premium-subsidy-150-fpl-boundary + program: california-premium-subsidy + years: + from: 2026 + type: program + note: Covered California coverage year beginning January 1. + question: Does exactly 150% FPL remain in the zero-contribution band under the adopted Program Design or enter + the paying band illustrated by Covered California's guidance and enroller example? + sources: + - kind: statute + citation: California Government Code 100800(c), Board-adopted program design + url: https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=GOV§ionNum=100800. + - kind: policy + citation: Final 2026 California Premium Subsidy Program Design, (d)(4), PDF page 2 + url: https://board.coveredca.com/meetings/2025/July%2028,%202025/CoveredCA_2026_Premium_Subsidy_Program_Design_Final.pdf#page=2 + - kind: policy + citation: Final 2027 California Premium Subsidy Program Design, (d)(4), PDF page 2 + url: https://hbex.coveredca.com/data-research/Final-2027-California-Premium-Subsidy-Program-Design.pdf#page=2 + - kind: guidance + citation: 2026 California State Premium Subsidy Policy Explainer, Table 1, PDF page 2 + url: https://hbex.coveredca.com/stakeholders/PDFs/2026-02_StatePremiumSub_PolicyExplainer-Final.pdf#page=2 + - kind: guidance + citation: 2027 Premium Subsidy Insight, Table 1, PDF page 2 + url: https://hbex.coveredca.com/stakeholders/PDFs/2026-03_StatePremiumSub2027_Insight_Final.pdf#page=2 + - kind: agency-practice + citation: OE 2027 Enroller Kick-off, exactly 150% FPL example, PDF pages 39-40 + url: https://hbex.coveredca.com/toolkit/webinars-briefings/downloads/OE27_Enroller_Kickoff.pdf#page=40 + readings: + statute: The Board-adopted Designs place income up to 150% in the 0% band and start the paying band above + 150%, excluding exactly 150% from that band. + other: The guidance labels the paying band from 150%, and the OE27 example explicitly charges 2.80% at + exactly 150% FPL. + example: The OE27 single-person example has $23,940 income at exactly 150% FPL and a 2.80% contribution; + the adopted Design's boundary instead gives a 0% contribution. + pe_us: + follows: statute + code: + - policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/threshold.yaml + - policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.py + prs: + - https://github.com/PolicyEngine/policyengine-us/pull/10301 + state: ruled + note: The adopted rule's explicit above-150% wording governs the conflicting training example. Recheck if + Covered California amends the Design or clarifies the boundary. PR 10301 changes the boundary; PR 10305 + handles the final 2027 schedule and eligibility limits separately. - id: us-ca:renter-credit-2025-joint-income-limit program: income-tax years: From 1238297ebf32dfbbc1f79d73b5f75a737650cd5a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 19:30:48 -0400 Subject: [PATCH 10/12] Move PY2027 to the Final 2027 California Premium Subsidy Program Design The Final 2027 Program Design replaces the V4 schedule this model carried: (d)(4) has one paying band, "Above 150% up to 200%", from 2.8% to 5.28% (V4: 2.69%-3.41% to 165%, then 3.41%-5.1%), and (d)(7) makes household income "at or below 200 percent" eligible (V4: "under 200 percent"). - Thresholds [1, 1.5, 2.0], initial [0, 0.028], final [0, 0.0528]. - Both years' limits are inclusive, so drop fpl_limit_inclusive and test the exact ratio with <=. - The OE27 enroller deck's summary slide says "up to under 200% FPL" while its own 200% example applies 5.28%; follow (d)(7) and record candidate us-ca:premium-subsidy-200-fpl-limit. - Rate, amount and eligibility cases move to the Final Design, including the deck's $31,920 example (5.28%, $478.80 a year) and incomes that truncate to 2.00 but exceed 200%; the property tests read 2027 as inclusive and #10301's rate properties now cover 2027. Co-Authored-By: Claude Opus 5.5 --- ...a-premium-subsidy-165-exact-ratio.fixed.md | 2 +- .../applicable_percentage/final.yaml | 14 +-- .../applicable_percentage/initial.yaml | 16 ++- .../applicable_percentage/threshold.yaml | 11 +- .../ca/hbex/premium_subsidy/fpl_limit.yaml | 23 ++-- .../premium_subsidy/fpl_limit_inclusive.yaml | 18 --- .../ca/hbex/premium_subsidy/in_effect.yaml | 20 ++-- .../code_health/test_source_conflicts.py | 1 + .../premium_subsidy/ca_premium_subsidy.yaml | 55 +++++++-- ...premium_subsidy_applicable_percentage.yaml | 109 ++++++++++++++---- .../ca_premium_subsidy_eligible.yaml | 100 +++++++++++++--- ...a_premium_subsidy_applicable_percentage.py | 11 +- ..._ca_premium_subsidy_eligible_properties.py | 45 +++++--- .../ca_premium_subsidy_eligible.py | 15 +-- source_conflicts/candidates.yaml | 40 +++++++ 15 files changed, 344 insertions(+), 136 deletions(-) delete mode 100644 policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/fpl_limit_inclusive.yaml diff --git a/changelog.d/ca-premium-subsidy-165-exact-ratio.fixed.md b/changelog.d/ca-premium-subsidy-165-exact-ratio.fixed.md index 18681ed6ca7..aad77321e4d 100644 --- a/changelog.d/ca-premium-subsidy-165-exact-ratio.fixed.md +++ b/changelog.d/ca-premium-subsidy-165-exact-ratio.fixed.md @@ -1 +1 @@ -Decide California Premium Subsidy eligibility on the exact ratio of nonnegative ACA MAGI to prior-year FPL instead of the truncated `aca_magi_fraction`, so PY2026 incomes from 165.003% to 165.994% of FPL are no longer eligible ([2026 Program Design, (d)(7)](https://board.coveredca.com/meetings/2025/July%2028,%202025/CoveredCA_2026_Premium_Subsidy_Program_Design_Final.pdf#page=2): "at or below 165 percent"). The PY2027 limit becomes a strict 200% ([2027 Program Design V4, (d)(7)](https://board.coveredca.com/meetings/2026/May%2021,%202026/CoveredCA_2027_Premium_Subsidy_Program_Design_V4_05-19-26.pdf#page=2): "under 200 percent"), which leaves PY2027 eligibility unchanged. +Decide California Premium Subsidy eligibility on the exact ratio of nonnegative ACA MAGI to prior-year FPL instead of the truncated `aca_magi_fraction`, so PY2026 incomes from 165.003% to 165.994% of FPL are no longer eligible ([2026 Program Design, (d)(7)](https://board.coveredca.com/meetings/2025/July%2028,%202025/CoveredCA_2026_Premium_Subsidy_Program_Design_Final.pdf#page=2): "at or below 165 percent"). Move PY2027 from the superseded Program Design V4 to the [Final 2027 Program Design, (d)(4) and (d)(7)](https://hbex.coveredca.com/data-research/Final-2027-California-Premium-Subsidy-Program-Design.pdf#page=2): 0% up to 150% FPL, 2.8% rising to 5.28% above 150% up to 200% (replacing V4's 2.69%-3.41% and 3.41%-5.1% bands), and eligibility "at or below 200 percent", so households at exactly 200% FPL now qualify. diff --git a/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/final.yaml b/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/final.yaml index 76f17788fdf..e7ebb6e31fa 100644 --- a/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/final.yaml +++ b/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/final.yaml @@ -4,12 +4,10 @@ values: 2026-01-01: - 0 # 100-150%: flat 0% - 0.0391 # 150-165%: ends 3.91% - # PY2027 Program Design V4 (adopted Board Res. 2026-22). - # Bands: 100-150% FPL, 150-165% FPL, 165-200% FPL. + # Final 2027 Program Design. Bands: 100-150% FPL, 150-200% FPL. 2027-01-01: - 0 # 100-150%: flat 0% - - 0.0341 # 150-165%: ends 3.41% - - 0.051 # 165-200%: ends 5.1% + - 0.0528 # 150-200%: ends 5.28% metadata: unit: /1 @@ -20,7 +18,7 @@ metadata: href: https://board.coveredca.com/meetings/2025/July%2028,%202025/CoveredCA_2026_Premium_Subsidy_Program_Design_Final.pdf#page=2 - title: 2026 California State Premium Subsidy Policy Explainer, Table 1 href: https://hbex.coveredca.com/stakeholders/PDFs/2026-02_StatePremiumSub_PolicyExplainer-Final.pdf#page=2 - - title: 2027 California Premium Subsidy Program Design V4, subdiv. (d)(4) - href: https://board.coveredca.com/meetings/2026/May%2021,%202026/CoveredCA_2027_Premium_Subsidy_Program_Design_V4_05-19-26.pdf#page=2 - - title: Covered California Board Resolution 2026-22 - href: https://board.coveredca.com/meetings/2026/June%2018,%202026/2026-22.pdf + - title: Final 2027 California Premium Subsidy Program Design, subdiv. (d)(4) + href: https://hbex.coveredca.com/data-research/Final-2027-California-Premium-Subsidy-Program-Design.pdf#page=2 + - title: 2027 California State Premium Subsidy Program Insight, Table 1 + href: https://hbex.coveredca.com/stakeholders/PDFs/2026-03_StatePremiumSub2027_Insight_Final.pdf#page=2 diff --git a/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/initial.yaml b/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/initial.yaml index 5496e66f5ef..a1fafd8c2cd 100644 --- a/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/initial.yaml +++ b/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/initial.yaml @@ -4,12 +4,10 @@ values: 2026-01-01: - 0 # 100-150%: flat 0% - 0.0319 # 150-165%: starts 3.19% - # PY2027 Program Design V4 (adopted Board Res. 2026-22). - # Bands: 100-150% FPL, 150-165% FPL, 165-200% FPL. + # Final 2027 Program Design. Bands: 100-150% FPL, 150-200% FPL. 2027-01-01: - - 0 # 100-150%: flat 0% - - 0.0269 # 150-165%: starts 2.69% - - 0.0341 # 165-200%: starts 3.41% + - 0 # 100-150%: flat 0% + - 0.028 # 150-200%: starts 2.8% metadata: unit: /1 @@ -20,7 +18,7 @@ metadata: href: https://board.coveredca.com/meetings/2025/July%2028,%202025/CoveredCA_2026_Premium_Subsidy_Program_Design_Final.pdf#page=2 - title: 2026 California State Premium Subsidy Policy Explainer, Table 1 href: https://hbex.coveredca.com/stakeholders/PDFs/2026-02_StatePremiumSub_PolicyExplainer-Final.pdf#page=2 - - title: 2027 California Premium Subsidy Program Design V4, subdiv. (d)(4) - href: https://board.coveredca.com/meetings/2026/May%2021,%202026/CoveredCA_2027_Premium_Subsidy_Program_Design_V4_05-19-26.pdf#page=2 - - title: Covered California Board Resolution 2026-22 - href: https://board.coveredca.com/meetings/2026/June%2018,%202026/2026-22.pdf + - title: Final 2027 California Premium Subsidy Program Design, subdiv. (d)(4) + href: https://hbex.coveredca.com/data-research/Final-2027-California-Premium-Subsidy-Program-Design.pdf#page=2 + - title: 2027 California State Premium Subsidy Program Insight, Table 1 + href: https://hbex.coveredca.com/stakeholders/PDFs/2026-03_StatePremiumSub2027_Insight_Final.pdf#page=2 diff --git a/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/threshold.yaml b/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/threshold.yaml index 63e30cf9e77..3af7d80bda5 100644 --- a/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/threshold.yaml +++ b/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/applicable_percentage/threshold.yaml @@ -13,12 +13,11 @@ values: - 1 - 1.5 - 1.65 - # PY2027 Program Design V4 (adopted Board Res. 2026-22) widens the top band - # to under 200% FPL, adding a 165-200% bracket. + # The Final 2027 Program Design has two bands, "100% up to 150%" (0%) and + # "Above 150% up to 200%" (2.8% to 5.28%), with no knot at 165%. 2027-01-01: - 1 - 1.5 - - 1.65 - 2.0 metadata: @@ -30,7 +29,5 @@ metadata: href: https://board.coveredca.com/meetings/2025/July%2028,%202025/CoveredCA_2026_Premium_Subsidy_Program_Design_Final.pdf#page=2 - title: 2026 California State Premium Subsidy Policy Explainer, Table 1 href: https://hbex.coveredca.com/stakeholders/PDFs/2026-02_StatePremiumSub_PolicyExplainer-Final.pdf#page=2 - - title: 2027 California Premium Subsidy Program Design V4, subdiv. (d)(4) - href: https://board.coveredca.com/meetings/2026/May%2021,%202026/CoveredCA_2027_Premium_Subsidy_Program_Design_V4_05-19-26.pdf#page=2 - - title: Covered California Board Resolution 2026-22 - href: https://board.coveredca.com/meetings/2026/June%2018,%202026/2026-22.pdf + - title: Final 2027 California Premium Subsidy Program Design, subdiv. (d)(4) and (d)(7) + href: https://hbex.coveredca.com/data-research/Final-2027-California-Premium-Subsidy-Program-Design.pdf#page=2 diff --git a/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/fpl_limit.yaml b/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/fpl_limit.yaml index 170e2d96028..baf2ec7dfec 100644 --- a/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/fpl_limit.yaml +++ b/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/fpl_limit.yaml @@ -1,10 +1,15 @@ description: California limits income eligibility to this share of the federal poverty line under the California Premium Subsidy program. values: - # PY2026 Program Design (d)(7): "at or below 165 percent" (inclusive). + # Each limit is inclusive and applies to the exact income-to-FPL ratio, not + # the truncated aca_magi_fraction. + # PY2026 Program Design (d)(7): "at or below 165 percent". 2026-01-01: 1.65 - # PY2027 Program Design V4 (adopted Board Res. 2026-22) extends eligibility to - # "under 200 percent" (strict; see fpl_limit_inclusive). The limit applies - # to the exact income-to-FPL ratio, not the truncated aca_magi_fraction. + # Final 2027 Program Design (d)(7): "at or below 200 percent". + # The OE27 enroller deck conflicts with (d)(7) at 200%: its summary slide says + # the subsidy "applies only to enrollees up to under 200% FPL", while its own + # worked example at exactly 200% FPL applies the 5.28% rate. Follow the + # adopted Design. + # Candidate: us-ca:premium-subsidy-200-fpl-limit until registered. 2027-01-01: 2.0 metadata: @@ -16,7 +21,9 @@ metadata: href: https://board.coveredca.com/meetings/2025/July%2028,%202025/CoveredCA_2026_Premium_Subsidy_Program_Design_Final.pdf#page=2 - title: 2026 California State Premium Subsidy Policy Explainer, eligibility href: https://hbex.coveredca.com/stakeholders/PDFs/2026-02_StatePremiumSub_PolicyExplainer-Final.pdf#page=1 - - title: 2027 California Premium Subsidy Program Design V4, subdiv. (d)(7) - href: https://board.coveredca.com/meetings/2026/May%2021,%202026/CoveredCA_2027_Premium_Subsidy_Program_Design_V4_05-19-26.pdf#page=2 - - title: Covered California Board Resolution 2026-22 - href: https://board.coveredca.com/meetings/2026/June%2018,%202026/2026-22.pdf + - title: Final 2027 California Premium Subsidy Program Design, subdiv. (d)(7) + href: https://hbex.coveredca.com/data-research/Final-2027-California-Premium-Subsidy-Program-Design.pdf#page=2 + - title: 2027 California State Premium Subsidy Program Insight, eligibility + href: https://hbex.coveredca.com/stakeholders/PDFs/2026-03_StatePremiumSub2027_Insight_Final.pdf#page=1 + - title: "Covered California OE 2027 Enroller Kick-off, 200% FPL example #page 37-40" + href: https://hbex.coveredca.com/toolkit/webinars-briefings/downloads/OE27_Enroller_Kickoff.pdf#page=37 diff --git a/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/fpl_limit_inclusive.yaml b/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/fpl_limit_inclusive.yaml deleted file mode 100644 index 655524ba46e..00000000000 --- a/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/fpl_limit_inclusive.yaml +++ /dev/null @@ -1,18 +0,0 @@ -description: California uses this rule to treat the income eligibility limit as inclusive under the California Premium Subsidy program. -values: - # PY2026 Program Design (d)(7): "at or below 165 percent". - 2026-01-01: true - # PY2027 Program Design V4 (d)(7): "under 200 percent". - 2027-01-01: false - -metadata: - unit: bool - period: year - label: California Premium Subsidy income limit inclusive - reference: - - title: 2026 California Premium Subsidy Program Design, subdiv. (d)(7) - href: https://board.coveredca.com/meetings/2025/July%2028,%202025/CoveredCA_2026_Premium_Subsidy_Program_Design_Final.pdf#page=2 - - title: 2027 California Premium Subsidy Program Design V4, subdiv. (d)(7) - href: https://board.coveredca.com/meetings/2026/May%2021,%202026/CoveredCA_2027_Premium_Subsidy_Program_Design_V4_05-19-26.pdf#page=2 - - title: Covered California Board Resolution 2026-22 - href: https://board.coveredca.com/meetings/2026/June%2018,%202026/2026-22.pdf diff --git a/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/in_effect.yaml b/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/in_effect.yaml index 8602e85fce9..594aae370db 100644 --- a/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/in_effect.yaml +++ b/policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/in_effect.yaml @@ -1,13 +1,15 @@ description: California provides state premium subsidies when this is true under the California Premium Subsidy program. values: # The provision runs from PY2026, matching the New Mexico and Maryland approach - # (ending 2028-01-01). PY2026 and PY2027 both have board-adopted, verified - # program designs: the PY2027 Program Design V4 (adopted Board Resolution - # 2026-22, June 18 2026; funded by the 2026 Budget Act $300M HCARF) changes the - # schedule, so PY2027 is NOT a carry-forward of PY2026 (see the applicable - # percentage and fpl_limit files). Section 100800(c) has the Exchange adopt the - # design annually upon appropriation and Section 100825 creates no entitlement, - # so the provision ends 2028-01-01 absent a verified PY2028 design. + # (ending 2028-01-01). PY2026 and PY2027 both have adopted, verified program + # designs: the Final 2027 Program Design (adopted under item 4800-101-3381 of + # the Budget Act of 2026; the 2027 Insight reports $300M appropriated from + # the Health Care Affordability Reserve Fund) changes the schedule, so PY2027 + # is NOT a carry-forward of PY2026 (see the applicable percentage and + # fpl_limit files). + # Section 100800(c) has the Exchange adopt the design annually upon + # appropriation and Section 100825 creates no entitlement, so the provision + # ends 2028-01-01 absent a verified PY2028 design. # # The Program Design's enhanced-PTC trigger ("state subsidy applies if the # federal enhanced PTC is not extended by Sept 30 2025") fired, so the residual @@ -25,6 +27,10 @@ metadata: reference: - title: 2026 California Premium Subsidy Program Design, preamble href: https://board.coveredca.com/meetings/2025/July%2028,%202025/CoveredCA_2026_Premium_Subsidy_Program_Design_Final.pdf#page=1 + - title: Final 2027 California Premium Subsidy Program Design, preamble + href: https://hbex.coveredca.com/data-research/Final-2027-California-Premium-Subsidy-Program-Design.pdf#page=1 + - title: 2027 California State Premium Subsidy Program Insight, background + href: https://hbex.coveredca.com/stakeholders/PDFs/2026-03_StatePremiumSub2027_Insight_Final.pdf#page=1 - title: CA Gov. Code Section 100800(c) (Exchange adopts annual program design upon appropriation) href: https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=GOV§ionNum=100800. - title: CA Gov. Code Section 100825 diff --git a/policyengine_us/tests/code_health/test_source_conflicts.py b/policyengine_us/tests/code_health/test_source_conflicts.py index 051616009eb..e0d7f090687 100644 --- a/policyengine_us/tests/code_health/test_source_conflicts.py +++ b/policyengine_us/tests/code_health/test_source_conflicts.py @@ -125,6 +125,7 @@ "policyengine_us/parameters/gov/ssa/ssi/income/ism/food_counts.yaml": 1, "policyengine_us/parameters/gov/states/ar/ade/oec/sra/rates/no_copay_smi_threshold.yaml": 1, "policyengine_us/parameters/gov/states/ca/cpuc/income_sources.yaml": 1, + "policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/fpl_limit.yaml": 1, "policyengine_us/parameters/gov/states/co/doi/premium_assistance/amount/additional_member.yaml": 1, "policyengine_us/parameters/gov/states/co/doi/premium_assistance/amount/first_member.yaml": 1, "policyengine_us/parameters/gov/states/fl/commerce/liheap/payment/base_amount.yaml": 1, diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml index a734d018bf7..4e1f22bc0c6 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml @@ -13,6 +13,7 @@ # The negative-MAGI clamp case supplies eligibility and a positive rate to # isolate the amount formula; its income fraction is correctly zero. # CA schedule: threshold [1.0, 1.5, 1.65], initial [0, 0.0319], final [0, 0.0391] +# PY2027 (Final Design): threshold [1.0, 1.5, 2.0], initial [0, 0.028], final [0, 0.0528] # ---- 100-150% FPL band (applicable percentage 0%) ---- @@ -454,15 +455,16 @@ ca_premium_subsidy_applicable_percentage: 0 ca_premium_subsidy: 0 -# ---- PY2027 carry-forward (positive amount) ---- +# ---- PY2027 Final Program Design (positive amount) ---- - name: Case 16, PY2027 pays a positive subsidy at 157.5% FPL (revised schedule). - # in_effect is true through PY2027 and the schedule CHANGED (Board Res. 2026-22): - # the 150-165% band is now initial 0.0269 -> final 0.0341, so - # magi_frac 1.575 -> pct 0.0269 + 0.5*(0.0341-0.0269) = 0.0269 + 0.0036 = 0.0305. - # income 25_000. amount = max(0, 6_000 - 2_000 - 0.0305*25_000) - # = 6_000 - 2_000 - 762.50 = 3_237.50 (up from the 2026 value of 3_112.50 at the - # same FPL because the PY2027 applicable percentage in this band is lower). + # in_effect is true through PY2027 and the Final 2027 Program Design (d)(4) + # changes the schedule: one paying band "Above 150% up to 200%", initial + # 0.028 -> final 0.0528. magi_frac 1.575 -> position (1.575-1.5)/0.5 = 0.15 + # -> pct 0.028 + 0.15*0.0248 = 0.03172. income 25_000. + # amount = max(0, 6_000 - 2_000 - 0.03172*25_000) = 6_000 - 2_000 - 793 = 3_207 + # (up from the 2026 value of 3_112.50 at the same FPL because the PY2027 + # applicable percentage at this point is lower). period: 2027 absolute_error_margin: 0.01 input: @@ -485,7 +487,7 @@ members: [person1] state_code: CA output: - ca_premium_subsidy: 3_237.50 + ca_premium_subsidy: 3_207 # ---- Floor from the SLCSP == APTC direction ---- @@ -558,3 +560,40 @@ ca_premium_subsidy_eligible: true ca_premium_subsidy_applicable_percentage: 0.0355 ca_premium_subsidy: 3_881.75 + +# ---- PY2027 upper limit (Covered California's worked example) ---- + +- name: Case 19, PY2027 exactly 200% FPL pays the 1.5-point gap (OE27 example). + # The OE27 enroller deck (PDF pages 37-38) works a single 40-year-old in + # Sacramento County at $31,920, exactly 200% of the 2026 one-person FPG + # $15,960: benchmark $688 a month, federal required contribution 6.78%, + # state rate 5.28% (6.78% - 1.5%), state subsidy about $40 a month. + # slcsp = 688*12 = 8_256. aca_ptc = 8_256 - 0.0678*31_920 = 8_256 - 2_164.176 + # = 6_091.824. Eligible: 2.0 is "at or below 200 percent" (Design (d)(7)). + # amount = max(0, 8_256 - 6_091.824 - 0.0528*31_920) + # = 2_164.176 - 1_685.376 = 478.80 = 0.015*31_920, or $39.90 a month. + period: 2027 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + is_aca_ptc_eligible: true + tax_units: + tax_unit: + members: [person1] + tax_unit_fpg: + 2026: 15_960 + aca_magi: 31_920 + aca_magi_fraction: 2.00 + slcsp: 8_256 + aca_ptc: 6_091.824 + tax_unit_is_filer: true + households: + household: + members: [person1] + state_code: CA + output: + ca_premium_subsidy_eligible: true + ca_premium_subsidy_applicable_percentage: 0.0528 + ca_premium_subsidy: 478.80 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml index a5b10f361ab..22f9e017b04 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.yaml @@ -7,6 +7,8 @@ # Bands (initial -> final over the FPL band): # 100-150% FPL: 0% (flat) # above 150-165% FPL: 3.19% -> 3.91% (interpolated linearly) +# PY2027 (Final 2027 Program Design (d)(4)): threshold [1.0, 1.5, 2.0], +# initial [0, 0.028], final [0, 0.0528]; above 150-200% FPL: 2.8% -> 5.28%. # The exact nonnegative MAGI / prior-year FPG ratio selects the band; the # truncated aca_magi_fraction determines the rate within it. Supplied-fraction # cases normalize prior-year FPG to $10,000 and supply consistent MAGI to @@ -292,13 +294,12 @@ output: ca_premium_subsidy_applicable_percentage: 0 -# ---- Carry-forward ---- +# ---- PY2027 Final Program Design ---- -- name: Case 12, PY2027 schedule at 157.5% FPL -> 3.05% (revised 150-165% band). - # PY2027 Program Design V4 (Board Res. 2026-22) changes the 150-165% band to - # initial 0.0269 -> final 0.0341. bracket 1 [1.5,1.65]: - # position (1.575-1.5)/0.15 = 0.5 -> 0.0269 + 0.5*(0.0341-0.0269) - # = 0.0269 + 0.5*0.0072 = 0.0269 + 0.0036 = 0.0305. +- name: Case 12, PY2027 157.5% FPL -> 3.172%. + # Final 2027 Program Design (d)(4): "Above 150% up to 200%", 2.8% -> 5.28%. + # bracket 1 [1.5,2.0]: position (1.575-1.5)/0.5 = 0.15 + # -> 0.028 + 0.15*(0.0528-0.028) = 0.028 + 0.15*0.0248 = 0.028 + 0.00372 = 0.03172. period: 2027 absolute_error_margin: 0.0001 input: @@ -318,15 +319,12 @@ members: [person1] state_code: CA output: - ca_premium_subsidy_applicable_percentage: 0.0305 + ca_premium_subsidy_applicable_percentage: 0.03172 -# ---- PY2027 new 165-200% FPL tier ---- - -- name: Case 13, PY2027 exactly 165% FPL -> 3.41% from either adjacent tier. - # PY2027 thresholds [1,1.5,1.65,2.0]. searchsorted(..., 1.65, "left") = 2, - # -1 = 1 -> bracket 1 [1.5,1.65] at position 1 -> its final rate 0.0341, - # which equals the 165-200% tier's initial rate, so the rate is continuous - # at 165% (2026 had 0.0391 at this point). +- name: Case 13, PY2027 exactly 165% FPL -> 3.544% (165% is not a PY2027 knot). + # PY2027 thresholds [1,1.5,2.0]. searchsorted(..., 1.65, "left") = 2, -1 = 1 + # -> bracket 1 [1.5,2.0]: position (1.65-1.5)/0.5 = 0.3 + # -> 0.028 + 0.3*0.0248 = 0.028 + 0.00744 = 0.03544. period: 2027 absolute_error_margin: 0.0001 input: @@ -346,11 +344,11 @@ members: [person1] state_code: CA output: - ca_premium_subsidy_applicable_percentage: 0.0341 + ca_premium_subsidy_applicable_percentage: 0.03544 -- name: Case 14, PY2027 182.5% FPL (midpoint of the new 165-200% tier) -> 4.255%. - # bracket 2 [1.65,2.0]: position (1.825-1.65)/0.35 = 0.5. - # 0.0341 + 0.5*(0.051-0.0341) = 0.0341 + 0.5*0.0169 = 0.0341 + 0.00845 = 0.04255. +- name: Case 14, PY2027 182.5% FPL -> 4.412%. + # bracket 1 [1.5,2.0]: position (1.825-1.5)/0.5 = 0.65 + # -> 0.028 + 0.65*0.0248 = 0.028 + 0.01612 = 0.04412. period: 2027 absolute_error_margin: 0.0001 input: @@ -370,12 +368,11 @@ members: [person1] state_code: CA output: - ca_premium_subsidy_applicable_percentage: 0.04255 + ca_premium_subsidy_applicable_percentage: 0.04412 -- name: Case 15, PY2027 199% FPL (top of the eligible band) -> ~5.05%. - # bracket 2 [1.65,2.0]: position (1.99-1.65)/0.35 = 0.34/0.35 = 0.971428... - # 0.0341 + 0.971428*(0.051-0.0341) = 0.0341 + 0.971428*0.0169 = 0.0341 + - # 0.016417 = 0.050517. Derived (non-round position); confirm against the model. +- name: Case 15, PY2027 199% FPL -> 5.2304%. + # bracket 1 [1.5,2.0]: position (1.99-1.5)/0.5 = 0.98 + # -> 0.028 + 0.98*0.0248 = 0.028 + 0.024304 = 0.052304. period: 2027 absolute_error_margin: 0.0001 input: @@ -395,7 +392,31 @@ members: [person1] state_code: CA output: - ca_premium_subsidy_applicable_percentage: 0.050517 # model computes 0.05051714 + ca_premium_subsidy_applicable_percentage: 0.052304 + +- name: Case 15a, PY2027 exactly 200% FPL -> 5.28% (top of the band). + # searchsorted([1,1.5,2.0], 2.0, "left") = 2, -1 = 1 -> bracket 1 [1.5,2.0] + # at position 1 -> the final rate 0.0528. + period: 2027 + absolute_error_margin: 0.0001 + input: + people: + person1: + age: 40 + is_aca_ptc_eligible: true + tax_units: + tax_unit: + members: [person1] + aca_magi: 20_000 + tax_unit_fpg: + 2026: 10_000 + aca_magi_fraction: 2.00 + households: + household: + members: [person1] + state_code: CA + output: + ca_premium_subsidy_applicable_percentage: 0.0528 - name: Case 2b, a supplied fraction of 1.505 interpolates on the fraction given. # searchsorted([1.0,1.5,1.65], 1.505, "left") = 2, -1 -> bracket 1 [1.5,1.65]. @@ -552,3 +573,43 @@ output: aca_magi_fraction: 1.50 ca_premium_subsidy_applicable_percentage: 0 + +- name: Case 16b, PY2027 raw income $23,942, just above 150% enters the 2.8% band. + # $23,942 / 2026 one-person FPG $15,960 = 150.0125%, "Above 150%". The + # truncated fraction 1.50 sits at the start of the band: rate 0.028. + period: 2027 + absolute_error_margin: 0.000001 + input: + people: + person1: + age: 40 + employment_income: + 2027: 23_942 + is_aca_ptc_eligible: true + households: + household: + members: [person1] + state_code: CA + output: + aca_magi_fraction: 1.50 + ca_premium_subsidy_applicable_percentage: 0.028 + +- name: Case 16c, PY2027 raw income $31,920, exactly 200% pays 5.28%. + # $31,920 / $15,960 = 200.000%. The OE27 enroller deck's 200% FPL example + # applies "5.28% (6.78% - 1.5%)" (PDF page 37). + period: 2027 + absolute_error_margin: 0.000001 + input: + people: + person1: + age: 40 + employment_income: + 2027: 31_920 + is_aca_ptc_eligible: true + households: + household: + members: [person1] + state_code: CA + output: + aca_magi_fraction: 2.00 + ca_premium_subsidy_applicable_percentage: 0.0528 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.yaml index 3ca437fafad..34d90029643 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.yaml @@ -5,7 +5,7 @@ # married-filing-separately exclusion, and the federal FPL-band income test), # the tax unit is an applicable return filer, and ACA MAGI is at # or above the 100% FPL floor and at or below the FPL limit (1.65 in PY2026, -# under 2.00 in PY2027; see fpl_limit_inclusive). Program Design (d)(7) applies +# 2.00 in PY2027). Program Design (d)(7) applies # these limits to the exact ratio of nonnegative ACA MAGI to prior-year FPG, # not to the truncated aca_magi_fraction (FTB 3849 Table 1-2 likewise decides # band edges on the exact percentage). Supplied-income cases pin prior-year FPG @@ -13,7 +13,7 @@ # supplied so the gate logic is tested in isolation from the premium-data # lookup. Income-derived cases use the actual prior-year one-person FPG # ($15,650 for PY2026, $15,960 for PY2027). -# fpl_floor = 1.0; fpl_limit = 1.65 inclusive (PY2026), 2.0 exclusive (PY2027) +# fpl_floor = 1.0; fpl_limit = 1.65 (PY2026), 2.0 (PY2027), both inclusive # ---- Income-band boundaries ---- @@ -549,11 +549,11 @@ output: ca_premium_subsidy_eligible: false -# ---- PY2027 FPL-limit edge (under 200%) ---- +# ---- PY2027 FPL-limit edge (at or below 200%) ---- -- name: Case 17, PY2027 at 199% FPL (1.99) is eligible (limit under 200%). - # PY2027 Program Design (d)(7): "under 200 percent". 1.99 < 2.0 -> in-band. - # Confirms the widened PY2027 band admits units up to 199% FPL. +- name: Case 17, PY2027 at 199% FPL (1.99) is eligible. + # Final 2027 Program Design (d)(7): "at or below 200 percent". 1.99 <= 2.0. + # Confirms the widened PY2027 band admits units between 165% and 200% FPL. period: 2027 input: people: @@ -574,9 +574,9 @@ output: ca_premium_subsidy_eligible: true -- name: Case 18, PY2027 at exactly 200% FPL (2.00) is ineligible (strict limit). - # PY2027 fpl_limit 2.0 with fpl_limit_inclusive false: 2.00 is not under 2.0. - # Pairs with Case 17 to bracket the PY2027 upper edge. +- name: Case 18, PY2027 at exactly 200% FPL (2.00) is eligible (inclusive limit). + # Exact ratio 20_000 / 10_000 = 2.0, which is "at or below 200 percent". + # Pairs with Case 18a to bracket the PY2027 upper edge. period: 2027 input: people: @@ -594,11 +594,34 @@ household: members: [person1] state_code: CA + output: + ca_premium_subsidy_eligible: true + +- name: Case 18a, PY2027 at 200.5% FPL (2.005) is ineligible. + # Exact ratio 20_050 / 10_000 = 2.005 > 2.0. A limit tested on the truncated + # fraction (2.00) would wrongly admit it. + period: 2027 + input: + people: + person1: + age: 40 + is_aca_ptc_eligible: true + tax_units: + tax_unit: + members: [person1] + aca_magi: 20_050 + tax_unit_fpg: + 2026: 10_000 + tax_unit_is_filer: true + households: + household: + members: [person1] + state_code: CA output: ca_premium_subsidy_eligible: false - name: Case 17a, PY2027 at 199.5% FPL (1.995) is eligible. - # Exact ratio 19_950 / 10_000 = 1.995 < 2.0. A limit of 1.99 on the exact + # Exact ratio 19_950 / 10_000 = 1.995 <= 2.0. A limit of 1.99 on the exact # ratio would wrongly exclude incomes between 199% and 200% of FPL. period: 2027 input: @@ -623,8 +646,7 @@ # ---- PY2027 200% limit from derived income ---- - name: Case 17b, PY2027 income of 199.994% of FPL is eligible. - # $31,919 / 2026 one-person FPG $15,960 = 199.9937%, which is "under 200 - # percent" (2027 Program Design (d)(7)). + # $31,919 / 2026 one-person FPG $15,960 = 199.9937%. period: 2027 absolute_error_margin: 0.0001 input: @@ -646,8 +668,10 @@ aca_magi_fraction: 1.99 ca_premium_subsidy_eligible: true -- name: Case 18b, PY2027 income of exactly 200% of FPL is ineligible. - # $31,920 / $15,960 = 200.000%, which is not "under 200 percent". +- name: Case 18b, PY2027 income of exactly 200% of FPL is eligible. + # $31,920 / $15,960 = 200.000%, "at or below 200 percent" (Final 2027 + # Design (d)(7)). This is the OE27 enroller deck's 200% FPL example, which + # applies the 5.28% state rate (PDF pages 37-40). period: 2027 absolute_error_margin: 0.0001 input: @@ -667,11 +691,57 @@ output: aca_magi: 31_920 aca_magi_fraction: 2.00 + ca_premium_subsidy_eligible: true + +- name: Case 18d, PY2027 income of 200.006% of FPL is ineligible though it truncates to 2.00. + # $31,921 / $15,960 = 200.0063%. + period: 2027 + absolute_error_margin: 0.0001 + input: + people: + person1: + age: 40 + employment_income: 31_921 + is_aca_ptc_eligible: true + tax_units: + tax_unit: + members: [person1] + tax_unit_is_filer: true + households: + household: + members: [person1] + state_code: CA + output: + aca_magi: 31_921 + aca_magi_fraction: 2.00 + ca_premium_subsidy_eligible: false + +- name: Case 18e, PY2027 income of 200.996% of FPL is ineligible though it truncates to 2.00. + # $32,079 / $15,960 = 200.9962%; 201% of FPL is $32,079.60. + period: 2027 + absolute_error_margin: 0.0001 + input: + people: + person1: + age: 40 + employment_income: 32_079 + is_aca_ptc_eligible: true + tax_units: + tax_unit: + members: [person1] + tax_unit_is_filer: true + households: + household: + members: [person1] + state_code: CA + output: + aca_magi: 32_079 + aca_magi_fraction: 2.00 ca_premium_subsidy_eligible: false - name: Case 18c, PY2027 income of 165.006% of FPL is eligible (165% is the PY2026 limit only). # $26,335 / $15,960 = 165.0063%. Ineligible under the PY2026 limit, but - # inside the PY2027 band, which runs to under 200%. + # inside the PY2027 band, which runs to 200%. period: 2027 absolute_error_margin: 0.0001 input: diff --git a/policyengine_us/tests/unit/test_ca_premium_subsidy_applicable_percentage.py b/policyengine_us/tests/unit/test_ca_premium_subsidy_applicable_percentage.py index 4aecce7c251..2b620d60b66 100644 --- a/policyengine_us/tests/unit/test_ca_premium_subsidy_applicable_percentage.py +++ b/policyengine_us/tests/unit/test_ca_premium_subsidy_applicable_percentage.py @@ -1,4 +1,4 @@ -"""Properties of the adopted 2026 Design (d)(4) contribution bands. +"""Properties of the adopted 2026 and Final 2027 Design (d)(4) contribution bands. FTB 3849 Table 1-2 distinguishes exact band eligibility from the truncated percentage used for the rate lookup. The physical-income properties use @@ -45,16 +45,17 @@ def tax_unit(name, input_period): return formula(tax_unit, calculation_period, system.parameters) -@pytest.mark.parametrize("year", [2026]) +@pytest.mark.parametrize("year", [2026, 2027]) @settings(max_examples=100, deadline=None, derandomize=True) @given(incomes=st.lists(st.integers(-100_000, 600_000), min_size=2, max_size=40)) @example(incomes=[149_999, 150_000, 150_001, 150_999, 151_000]) +@example(incomes=[199_999, 200_000, 200_001, 200_999, 201_000]) def test_rate_never_decreases_with_exact_income(year, incomes): rates = _rates(year, sorted(incomes)) assert np.all(np.diff(rates) >= -1e-12) -@pytest.mark.parametrize("year", [2026]) +@pytest.mark.parametrize("year", [2026, 2027]) @settings(max_examples=100, deadline=None, derandomize=True) @given(income=st.integers(-100_000, 150_000)) @example(income=150_000) @@ -62,7 +63,7 @@ def test_rate_is_zero_at_or_below_exactly_150_percent(year, income): assert _rates(year, [income])[0] == 0 -@pytest.mark.parametrize("year, starting_rate", [(2026, 0.0319)]) +@pytest.mark.parametrize("year, starting_rate", [(2026, 0.0319), (2027, 0.028)]) @settings(max_examples=100, deadline=None, derandomize=True) @given(income=st.integers(150_001, 150_999)) def test_exact_income_above_150_uses_upper_band_truncated_starting_rate( @@ -73,7 +74,7 @@ def test_exact_income_above_150_uses_upper_band_truncated_starting_rate( @pytest.mark.parametrize( "year, boundary", - [(2026, 1), (2026, 1.65)], + [(2026, 1), (2026, 1.65), (2027, 1), (2027, 2)], ) @settings(max_examples=100, deadline=None, derandomize=True) @given(epsilon=st.floats(min_value=1e-7, max_value=1e-4)) diff --git a/policyengine_us/tests/unit/test_ca_premium_subsidy_eligible_properties.py b/policyengine_us/tests/unit/test_ca_premium_subsidy_eligible_properties.py index 85950e9f25e..91fa8d624b8 100644 --- a/policyengine_us/tests/unit/test_ca_premium_subsidy_eligible_properties.py +++ b/policyengine_us/tests/unit/test_ca_premium_subsidy_eligible_properties.py @@ -2,21 +2,23 @@ Program Design (d)(7) defines an applicable return filer by household income "at or above 100 percent and at or below 165 percent of the federal poverty -level" in PY2026, and "at or above 100 percent and under 200 percent" in -PY2027. The bounds below are transcribed from those Designs, not read from -the parameters, and are evaluated in exact rational arithmetic. +level" in PY2026, and "at or above 100 percent and at or below 200 percent" +in the Final 2027 Design. The bounds below are transcribed from those +Designs, not read from the parameters, and are evaluated in exact rational +arithmetic. The model divides nonnegative ACA MAGI by the prior-year poverty guideline. It must not use aca_magi_fraction for the band: that fraction truncates to whole percentages, so every income from 165.00% to 165.99% of the guideline -becomes 1.65 and would pass an "at or below 165 percent" test. FTB 3849 -Table 1-2, for the predecessor California program, likewise truncates for -the rate lookup but decides band edges on the exact percentage. +becomes 1.65 and would pass an "at or below 165 percent" test (in PY2027, +200.00% to 200.99% becomes 2.00). FTB 3849 Table 1-2, for the predecessor +California program, likewise truncates for the rate lookup but decides band +edges on the exact percentage. The model stores MAGI and the guideline as float32 and divides them in float64, so the reference reads (d)(7) exactly on the stored float32 values. -- The parameters match the Designs' bounds and inclusivity. +- The parameters match the Designs' bounds. - With every non-income condition met, eligibility equals the exact reading of (d)(7) for any stored income and guideline. The examples mix three kinds of income: whole dollars near each edge (some guidelines are multiples of @@ -38,12 +40,12 @@ from policyengine_us import Simulation from policyengine_us.system import system -# Program Design (d)(7), by plan year: (floor, limit, limit is inclusive). +# Program Design (d)(7), by plan year: the floor and the limit, both inclusive. DESIGN_BANDS = { # 2026 Design: "at or above 100 percent and at or below 165 percent". - 2026: (Fraction(1), Fraction(165, 100), True), - # 2027 Design V4: "at or above 100 percent and under 200 percent". - 2027: (Fraction(1), Fraction(2), False), + 2026: (Fraction(1), Fraction(165, 100)), + # Final 2027 Design: "at or above 100 percent and at or below 200 percent". + 2027: (Fraction(1), Fraction(2)), } EDGES = (Fraction(1), Fraction(165, 100), Fraction(2)) MAX_TAX_UNITS = 128 @@ -55,10 +57,9 @@ def stored(value): def design_eligible(year, income, guideline): - floor, limit, inclusive = DESIGN_BANDS[year] + floor, limit = DESIGN_BANDS[year] ratio = max(stored(income), 0) / stored(guideline) - below_limit = ratio <= limit if inclusive else ratio < limit - return ratio >= floor and below_limit + return floor <= ratio <= limit def model_eligible(year, incomes, guidelines): @@ -113,11 +114,10 @@ def tax_unit_incomes(draw): @pytest.mark.parametrize("year", sorted(DESIGN_BANDS)) def test_parameters_match_program_design(year): p = system.parameters(f"{year}-01-01").gov.states.ca.hbex.premium_subsidy - floor, limit, inclusive = DESIGN_BANDS[year] + floor, limit = DESIGN_BANDS[year] assert p.in_effect assert Fraction(str(p.fpl_floor)) == floor assert Fraction(str(p.fpl_limit)) == limit - assert bool(p.fpl_limit_inclusive) is inclusive @pytest.mark.parametrize("year", sorted(DESIGN_BANDS)) @@ -135,7 +135,18 @@ def test_parameters_match_program_design(year): ] ) # A $20-multiple guideline puts 100%, 165% and 200% on whole dollars. -@example(units=[(16_000, 16_000), (26_400, 16_000), (26_401, 16_000), (32_000, 16_000)]) +@example( + units=[ + (16_000, 16_000), + (26_400, 16_000), + (26_401, 16_000), + (32_000, 16_000), + (32_001, 16_000), + ] +) +# PY2027, 2026 one-person guideline $15,960: 199.994%, 200.000%, 200.006% and +# 200.996%; truncation maps 200.000% to 200.996% to 2.00. +@example(units=[(31_919, 15_960), (31_920, 15_960), (31_921, 15_960), (32_079, 15_960)]) # The 2025 guideline for 72 people, $406,150: $674,209 is exactly 166%, which # float32 truncation in aca_magi_fraction misrounds to 165.99%. @example(units=[(674_209, 406_150), (812_300, 406_150)]) diff --git a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py index 02275f2918b..654f8e9dad3 100644 --- a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py +++ b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py @@ -12,8 +12,8 @@ class ca_premium_subsidy_eligible(Variable): # return filer definition at Program Design (d)(7). # PDF pages 1-2 "https://board.coveredca.com/meetings/2025/July%2028,%202025/CoveredCA_2026_Premium_Subsidy_Program_Design_Final.pdf#page=1", - # 2027 Program Design V4, (d)(7): "under 200 percent". - "https://board.coveredca.com/meetings/2026/May%2021,%202026/CoveredCA_2027_Premium_Subsidy_Program_Design_V4_05-19-26.pdf#page=2", + # Final 2027 Program Design, (d)(7): "at or below 200 percent". + "https://hbex.coveredca.com/data-research/Final-2027-California-Premium-Subsidy-Program-Design.pdf#page=2", # FTB 3849 Table 1-2 (predecessor program): exact percentage decides # the band edge; the truncated percentage only drives the rate lookup. "https://www.ftb.ca.gov/forms/2020/2020-3849-instructions.pdf#page=6", @@ -41,8 +41,8 @@ def formula(tax_unit, period, parameters): # exceptions (abandoned-spouse, domestic-abuse relief) are not applied. aptc_eligible = add(tax_unit, period, ["is_aca_ptc_eligible"]) > 0 # Program Design (d)(7) bounds household income "at or above 100 - # percent" and, in PY2026, "at or below 165 percent" (PY2027: "under - # 200 percent") of the FPL. Test the exact ratio, not the truncated + # percent" and "at or below" 165 percent (PY2026) or 200 percent + # (PY2027) of the FPL. Test the exact ratio, not the truncated # aca_magi_fraction: truncation maps 165.00%-165.99% to 1.65, which # would admit incomes above 165%. FTB 3849 Table 1-2 (2020) likewise # decides band edges on the exact percentage. @@ -51,12 +51,9 @@ def formula(tax_unit, period, parameters): # Divide in float64: a float32 quotient can round a ratio just above # a limit down onto it. exact_magi_frac = magi.astype(np.float64) / fpg - below_limit = where( - p.fpl_limit_inclusive, - exact_magi_frac <= p.fpl_limit, - exact_magi_frac < p.fpl_limit, + income_eligible = (exact_magi_frac >= p.fpl_floor) & ( + exact_magi_frac <= p.fpl_limit ) - income_eligible = (exact_magi_frac >= p.fpl_floor) & below_limit # An applicable return filer (Program Design (d)(7); GC # Section 100810(a)(6)) must file a California return, so the tax unit # must be a filer. This gate replaces reliance on the federal 100% FPL diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 2a745c362f3..de421b1544c 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -795,6 +795,46 @@ candidates: note: The adopted rule's explicit above-150% wording governs the conflicting training example. Recheck if Covered California amends the Design or clarifies the boundary. PR 10301 changes the boundary; PR 10305 handles the final 2027 schedule and eligibility limits separately. +- id: us-ca:premium-subsidy-200-fpl-limit + program: california-premium-subsidy + years: + from: 2027 + type: program + note: Covered California coverage year beginning January 1. + question: Is a household at exactly 200% FPL eligible for the 2027 California Premium Subsidy, as the adopted + Program Design's "at or below 200 percent" reads, or excluded, as the OE27 enroller deck's summary slide reads? + sources: + - kind: statute + citation: California Government Code 100800(c), Exchange-adopted program design + url: https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=GOV§ionNum=100800. + - kind: policy + citation: Final 2027 California Premium Subsidy Program Design, (d)(7), PDF page 2 + url: https://hbex.coveredca.com/data-research/Final-2027-California-Premium-Subsidy-Program-Design.pdf#page=2 + - kind: guidance + citation: 2027 California State Premium Subsidy Program Insight, eligibility and Table 1, PDF pages 1-2 + url: https://hbex.coveredca.com/stakeholders/PDFs/2026-03_StatePremiumSub2027_Insight_Final.pdf#page=1 + - kind: agency-practice + citation: OE 2027 Enroller Kick-off, 200% FPL example and summary slide, PDF pages 37-40 + url: https://hbex.coveredca.com/toolkit/webinars-briefings/downloads/OE27_Enroller_Kickoff.pdf#page=37 + readings: + statute: Design (d)(7) defines an applicable return filer by household income at or above 100 percent and + at or below 200 percent of the FPL, so exactly 200% is eligible; the Insight agrees. + other: The OE27 summary slide says the 2027 subsidy applies only to enrollees up to under 200% FPL, which + would exclude exactly 200%. + example: A single filer with $31,920 income, exactly 200% of the 2026 one-person FPG of $15,960, is eligible + and pays 5.28% of income under the Design; the deck's own worked example on PDF pages 37 and 39-40 applies + the same 5.28%, while its summary slide on PDF page 38 would exclude the household. + pe_us: + follows: statute + code: + - policyengine_us/parameters/gov/states/ca/hbex/premium_subsidy/fpl_limit.yaml + - policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_eligible.py + prs: + - https://github.com/PolicyEngine/policyengine-us/pull/10305 + state: ruled + note: The adopted Design's explicit at-or-below wording governs the training deck's summary slide. Earlier + PE-US code followed the superseded Program Design V4, which read under 200 percent. Recheck if Covered + California amends the Design or clarifies the limit. - id: us-ca:renter-credit-2025-joint-income-limit program: income-tax years: From 93371bc7e82a24a210995c5f3c4b87775a263233 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 24 Oct 2026 10:41:44 -0400 Subject: [PATCH 11/12] Divide the CA Premium Subsidy band ratio in float64 The model stores aca_magi and tax_unit_fpg as float32. Their float32 quotient can round an exact ratio just above 150% of a fractional FPG down onto 1.5, which side="left" then puts in the 0% band. Divide in float64, as the eligibility gate already does, and add a float32 regression test for both plan years. Whole-dollar FPGs pick the same band either way. Amount Case 19 supplies aca_ptc at the official Rev. Proc. 2026-26 credit. Say why in the case: the federal parameters still carry the 2026 applicable percentages into 2027, so a derived credit would use 6.60% instead of 6.78% at 200% FPL. Co-Authored-By: Claude Opus 5.5 --- .../premium_subsidy/ca_premium_subsidy.yaml | 4 ++++ ...a_premium_subsidy_applicable_percentage.py | 22 ++++++++++++++----- ...a_premium_subsidy_applicable_percentage.py | 6 ++++- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml index 4e1f22bc0c6..6686f84de0a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml @@ -572,6 +572,10 @@ # = 6_091.824. Eligible: 2.0 is "at or below 200 percent" (Design (d)(7)). # amount = max(0, 8_256 - 6_091.824 - 0.0528*31_920) # = 2_164.176 - 1_685.376 = 478.80 = 0.015*31_920, or $39.90 a month. + # aca_ptc is supplied rather than derived: the federal parameters still carry + # the 2026 table (6.60% at 200%) into 2027, while Rev. Proc. 2026-26 sets + # 6.78%. A derived credit would be $6,149.28, and the state subsidy $421.34. + # Derive it here once the 2027 federal table is in. period: 2027 absolute_error_margin: 0.01 input: diff --git a/policyengine_us/tests/unit/test_ca_premium_subsidy_applicable_percentage.py b/policyengine_us/tests/unit/test_ca_premium_subsidy_applicable_percentage.py index 2b620d60b66..bae4c46633d 100644 --- a/policyengine_us/tests/unit/test_ca_premium_subsidy_applicable_percentage.py +++ b/policyengine_us/tests/unit/test_ca_premium_subsidy_applicable_percentage.py @@ -19,17 +19,18 @@ VARIABLE = "ca_premium_subsidy_applicable_percentage" -def _rates(year, incomes, *, truncate=True): +def _rates(year, incomes, *, truncate=True, fpg=FPG, dtype=float): """Call the registered formula on vectorized inputs without rebuilding US.""" calculation_period = period(year) - incomes = np.asarray(incomes, dtype=float) - fractions = np.maximum(incomes, 0) / FPG + incomes = np.asarray(incomes, dtype=dtype) + fpgs = np.full(incomes.shape, fpg, dtype=dtype) + fractions = np.maximum(incomes, 0) / fpgs if truncate: - fractions = np.floor(100 * np.maximum(incomes, 0) / FPG) / 100 + fractions = np.floor(100 * np.maximum(incomes, 0) / fpgs) / 100 inputs = { "aca_magi": incomes, "aca_magi_fraction": fractions, - "tax_unit_fpg": np.full(incomes.shape, FPG), + "tax_unit_fpg": fpgs, } def tax_unit(name, input_period): @@ -72,6 +73,17 @@ def test_exact_income_above_150_uses_upper_band_truncated_starting_rate( assert _rates(year, [income])[0] == pytest.approx(starting_rate, rel=0, abs=1e-12) +@pytest.mark.parametrize("year, starting_rate", [(2026, 0.0319), (2027, 0.028)]) +def test_float32_ratio_just_above_150_uses_upper_band(year, starting_rate): + # The model stores income and FPG as float32. This pair's exact ratio is + # 1.50000006, but its float32 quotient rounds down to exactly 1.5. + income, fpg = np.float32(24_600.00390625), np.float32(16_400.001953125) + assert float(income) / float(fpg) > 1.5 + assert income / fpg == np.float32(1.5) + rate = _rates(year, [income], fpg=fpg, dtype=np.float32)[0] + assert rate == pytest.approx(starting_rate, rel=0, abs=1e-7) + + @pytest.mark.parametrize( "year, boundary", [(2026, 1), (2026, 1.65), (2027, 1), (2027, 2)], diff --git a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.py b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.py index 014fc7bc446..713730c4f55 100644 --- a/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.py +++ b/policyengine_us/variables/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy_applicable_percentage.py @@ -23,7 +23,11 @@ class ca_premium_subsidy_applicable_percentage(Variable): def formula(tax_unit, period, parameters): magi_frac = tax_unit("aca_magi_fraction", period) magi = max_(tax_unit("aca_magi", period), 0) - exact_magi_frac = magi / tax_unit("tax_unit_fpg", period.last_year) + # Divide in float64: a float32 quotient can round a ratio just above + # 150% down onto it, which side="left" would put in the 0% band. + exact_magi_frac = magi.astype(np.float64) / tax_unit( + "tax_unit_fpg", period.last_year + ) p = parameters(period).gov.states.ca.hbex.premium_subsidy.applicable_percentage thresholds = np.array(p.threshold) From 32004fd2227ea6a1af95bfa7a824992c0c030d0f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 25 Oct 2026 22:21:00 -0700 Subject: [PATCH 12/12] Derive Case 19's federal credit from the 2027 table Main now carries the federal 2027 applicable percentage table (#10443, Rev. Proc. 2026-26), which gives 6.78% at exactly 200% FPL. Case 19 no longer supplies aca_ptc; it derives it and pins the federal credit, 8,256 - 0.0678 * 31,920 = 6,091.824, as an output. The state subsidy stays 478.80. Co-Authored-By: Claude Opus 5.5 --- .../ca/hbex/premium_subsidy/ca_premium_subsidy.yaml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml index 6686f84de0a..e9fbfbaa428 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/hbex/premium_subsidy/ca_premium_subsidy.yaml @@ -572,10 +572,9 @@ # = 6_091.824. Eligible: 2.0 is "at or below 200 percent" (Design (d)(7)). # amount = max(0, 8_256 - 6_091.824 - 0.0528*31_920) # = 2_164.176 - 1_685.376 = 478.80 = 0.015*31_920, or $39.90 a month. - # aca_ptc is supplied rather than derived: the federal parameters still carry - # the 2026 table (6.60% at 200%) into 2027, while Rev. Proc. 2026-26 sets - # 6.78%. A derived credit would be $6,149.28, and the state subsidy $421.34. - # Derive it here once the 2027 federal table is in. + # aca_ptc is derived from the federal 2027 applicable percentage table + # (Rev. Proc. 2026-26, PolicyEngine/policyengine-us#10443), which gives 6.78% + # at exactly 200% FPL. The output pins that federal credit. period: 2027 absolute_error_margin: 0.01 input: @@ -591,13 +590,13 @@ aca_magi: 31_920 aca_magi_fraction: 2.00 slcsp: 8_256 - aca_ptc: 6_091.824 tax_unit_is_filer: true households: household: members: [person1] state_code: CA output: + aca_ptc: 6_091.824 ca_premium_subsidy_eligible: true ca_premium_subsidy_applicable_percentage: 0.0528 ca_premium_subsidy: 478.80