Skip to content

Commit 843e94c

Browse files
committed
Fix SC CCAP copay for large families
1 parent 8c19cd1 commit 843e94c

File tree

2 files changed

+70
-24
lines changed

2 files changed

+70
-24
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
description: South Carolina sets this maximum family size with positive copay tiers under the Child Care Scholarship Program fee scale.
2+
values:
3+
2024-10-01: 14
4+
5+
metadata:
6+
unit: person
7+
period: year
8+
label: South Carolina CCAP copay maximum paid-tier family size
9+
reference:
10+
- title: SC CCAP Fee Scale 2024-2025, Appendix 2
11+
href: https://www.scchildcare.org/media/ubhdm1at/1-13-2025_policy-manual.pdf#page=182
12+
- title: SC CCAP Fee Scale 2025-2026
13+
href: https://www.scchildcare.org/media/ih2mrjw5/fee-scale-2025-2026.pdf#page=1

policyengine_us/variables/gov/states/sc/dss/ccap/sc_ccap_copay.py renamed to policyengine_us/variables/gov/states/sc/dss/ccap/copay/sc_ccap_copay.py

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from policyengine_us.model_api import *
2+
from policyengine_us.variables.gov.hhs.hhs_smi import smi
3+
from policyengine_us.variables.gov.hhs.tax_unit_fpg import fpg
24

35

46
class sc_ccap_copay(Variable):
@@ -15,22 +17,16 @@ class sc_ccap_copay(Variable):
1517
)
1618

1719
def formula(spm_unit, period, parameters):
18-
from policyengine_us.variables.gov.hhs.hhs_smi import smi
19-
2020
p = parameters(period).gov.states.sc.dss.ccap.copay
2121
monthly_income = spm_unit("sc_ccap_countable_income", period)
2222
tier_count = len(p.smi_tier_ratios.thresholds)
2323

24-
# Cap family size at max_family_size for threshold computation.
25-
# Pre-2024: 16 (all sizes have paid tiers).
26-
# Post-2024: 14 (sizes 15-16 are $0-only).
2724
size = spm_unit("spm_unit_size", period.this_year)
2825
max_size = int(p.max_family_size)
29-
capped_size = min_(size, max_size)
30-
above_max = size > max_size
26+
fee_scale_size = min_(size, max_size)
3127

3228
state = spm_unit.household("state_code_str", period)
33-
monthly_smi = smi(capped_size, state, period, parameters) / MONTHS_IN_YEAR
29+
state_group = spm_unit.household("state_group_str", period)
3430

3531
# Family-level copay exemptions (Section 3.4.2, p.108).
3632
# Head Start copay waiver is per-child, handled below.
@@ -46,34 +42,71 @@ def formula(spm_unit, period, parameters):
4642
# Pre-2024-10-01: tiers at fixed SMI ratios (45/55/65/75% mark
4743
# boundaries between 5 tiers).
4844
# Post-2024-10-01: equal-width bands from 150% FPL to 85% SMI.
45+
tier = np.zeros_like(monthly_income, dtype=int)
46+
below_fpl_threshold = np.zeros_like(monthly_income, dtype=bool)
47+
zero_only_row = np.zeros_like(monthly_income, dtype=bool)
4948
if p.fpg_exempt_in_effect:
50-
p_fpg = parameters(period).gov.hhs.fpg
51-
state_group = spm_unit.household("state_group_str", period)
52-
capped_fpg = (
53-
p_fpg.first_person[state_group]
54-
+ p_fpg.additional_person[state_group] * (capped_size - 1)
55-
) / MONTHS_IN_YEAR
56-
lower = np.floor(capped_fpg * p.fpg_exempt_rate + 0.5)
57-
below_fpl_threshold = monthly_income <= lower
49+
max_paid_size = int(p.max_paid_family_size)
50+
zero_only_row = fee_scale_size > max_paid_size
51+
52+
# Rows 15-16 are $0-only on the published fee scale. For rows with
53+
# paid tiers, fall back to the largest published paid-tier size
54+
# whose derived 150% FPL and 85% SMI bounds still form a
55+
# nonnegative band.
56+
paid_band_size = np.ones_like(size, dtype=int)
57+
for candidate in range(1, max_paid_size + 1):
58+
lower_candidate = np.floor(
59+
fpg(candidate, state_group, period, parameters)
60+
/ MONTHS_IN_YEAR
61+
* p.fpg_exempt_rate
62+
+ 0.5
63+
)
64+
upper_candidate = np.floor(
65+
smi(candidate, state, period, parameters)
66+
/ MONTHS_IN_YEAR
67+
* p.smi_tier_ratios.calc(tier_count)
68+
+ 0.5
69+
)
70+
valid_candidate = (fee_scale_size >= candidate) & (
71+
lower_candidate <= upper_candidate
72+
)
73+
paid_band_size = where(
74+
valid_candidate,
75+
candidate,
76+
paid_band_size,
77+
)
78+
79+
monthly_fpg = (
80+
fpg(paid_band_size, state_group, period, parameters) / MONTHS_IN_YEAR
81+
)
82+
monthly_smi = (
83+
smi(paid_band_size, state, period, parameters) / MONTHS_IN_YEAR
84+
)
85+
lower = np.floor(monthly_fpg * p.fpg_exempt_rate + 0.5)
5886
upper = np.floor(monthly_smi * p.smi_tier_ratios.calc(tier_count) + 0.5)
5987
band_width = np.ceil((upper - lower) / tier_count)
60-
tier = np.zeros_like(monthly_income, dtype=int)
88+
below_fpl_threshold = (~zero_only_row) & (monthly_income <= lower)
6189
for i in range(1, tier_count):
6290
threshold = lower + band_width * i
63-
tier = tier + (monthly_income > threshold).astype(int)
91+
tier = tier + ((~zero_only_row) & (monthly_income > threshold)).astype(
92+
int
93+
)
6494
else:
65-
below_fpl_threshold = False
66-
tier = np.zeros_like(monthly_income, dtype=int)
95+
monthly_smi = (
96+
smi(fee_scale_size, state, period, parameters) / MONTHS_IN_YEAR
97+
)
6798
for i in range(1, tier_count):
6899
ratio = p.smi_tier_ratios.calc(i)
69100
threshold = np.floor(monthly_smi * ratio + 0.5)
70101
tier = tier + (monthly_income > threshold).astype(int)
71102

72-
exempt = (
73-
protective | is_tanf | below_fpl_threshold | has_disabled_child | above_max
74-
)
103+
exempt = protective | is_tanf | below_fpl_threshold | has_disabled_child
75104

76-
weekly_copay_per_child = p.weekly_amounts.calc(tier + 1)
105+
weekly_copay_per_child = where(
106+
zero_only_row,
107+
0,
108+
p.weekly_amounts.calc(tier + 1),
109+
)
77110

78111
# Head Start children have no copay (Section 2.15); only count
79112
# non-Head-Start eligible children for the copay calculation.

0 commit comments

Comments
 (0)