Fix SNAP expected contribution rounding per 7 CFR 273.10(e) - #9318
Conversation
Round net income to the nearest dollar (1-49 cents down, 50-99 cents up, 7 CFR 273.10(e)(1)(ii)(A)) and round 30 percent of net income up to the next higher dollar (7 CFR 273.10(e)(2)(ii)(A)(1)), replacing the prior floor-then-multiply formula. Add boundary tests against the FNS Basis of Issuance table and categorical-eligibility ceiling tests. Fixes #9312 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9318 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 3 1 -2
Lines 65 14 -51
=========================================
- Hits 65 14 -51
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The 31 SNAP pins in the analytics-coverage partner tests shift by $1-$11 per year under the legally correct rounding (net income to the nearest dollar, 30 percent rounded up to the next dollar). Approved by Ziming. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…g fix The 33 signature-replay SNAP pins (masked in the first CI run by the edge-case failures) shift under the corrected rounding, and the edge-case comments now show the ceil-of-30-percent arithmetic that produces the updated pins. Approved by Ziming. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The last partner layer (masked while earlier directories failed): five SNAP pins shift under the corrected rounding; 2025 totals become whole dollars. Verified locally with the full partner folder (630 passed). Approved by Ziming. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…into fix/snap-expected-contribution-rounding
Program Review — SNAP expected-contribution rounding (7 CFR 273.10(e))PR #9318 "Fix SNAP expected contribution rounding per 7 CFR 273.10(e)". Single code change: The fix replaces net_income = np.floor(snap_net_income + 0.5) # net income half-up (273.10(e)(1)(ii)(A))
contribution = np.round(net_income * 0.3, 2) # cents guard against float noise
return np.ceil(contribution) # 30% rounded up (273.10(e)(2)(ii)(A)(1))Verdict: the rounding is correct and well-audited against the CFR and the FNS FY2026 issuance table. The one item that must be resolved before merge is a process gate — confirming partner sign-off on the 12 partner-contract test files — not a correctness defect. Source Documents
Critical (Must Fix)None. All computed values are verified correct against the CFR and the FNS table. (The partner-sign-off item below is a mandatory pre-merge process gate, not a correctness defect — see Should Address #1.) Should Address
Suggestions
Value / Rule Audit — PASS
Validation Summary
Review Severity: COMMENTThe fix is correct and thoroughly audited — rounding matches the CFR verbatim, boundary rows match the FNS FY2026 table, and every partner pin is arithmetically verified. Two items to close before merge, neither a correctness defect: (1) the maintainer must confirm partner sign-off/notice for the 12 partner-contract files is on record (process gate per CLAUDE.md), and (2) add one even-dollar Reviewed with Claude Code assistance. |
Review response: add net $6.50 and $10.50 cases that pin half-up rounding against round-half-to-even (the $3.50 boundary cannot distinguish them), cite 7 CFR 273.10(e)(2)(ii)(A)(2) for the allotment down-round equivalence, and deduplicate the Cornell CFR reference link. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Response to the program review (commit 809c4d8): Should Address 1 (partner sign-off): The maintainer (@hua7450) explicitly authorized the partner-contract test edits in this PR during review of the rounding fix ("yes, you can fix the partner folder"), after the three-question gate was raised. All 12 partner files were edited under that authorization; every pin was verified locally (full partner folder run: 630 passed). Should Address 2 (half-up test): Added — with one correction to the suggested cases. Net $2.50 and $4.50 do not discriminate half-up from round-half-to-even: at $2.50, half-up→3 gives EC ceil(0.90)=1 while half-to-even→2 gives EC ceil(0.60)=1 — identical output (same at $4.50: EC 2 either way). The tie-break is only observable where the two candidate integers straddle an expected-contribution band boundary (ceil(0.3n) increments) with the lower integer even. Added the two smallest such cases to
Both verified locally (file passes 15/15). Should Address 3a (anchor Should Address 3b ((e)(2)(ii)(B)): No file in this diff cites (e)(2)(ii)(B). The allotment down-round equivalence comment now cites 7 CFR 273.10(e)(2)(ii)(A)(2) (verified verbatim: "round the allotment down to the nearest lower dollar") alongside 7 USC 2017(a). 🤖 Generated with Claude Code |
PavelMakarchuk
left a comment
There was a problem hiding this comment.
PR 9318 review — Fix SNAP expected contribution rounding per 7 CFR 273.10(e)
PR: #9318 — "Fix SNAP expected contribution rounding per 7 CFR 273.10(e)" by hua7450 (fixes #9312)
Reviewed head SHA: 809c4d8e3e8157f830bfc4e6f5c91019de5dedbf
Merge base: 350ecc766e4467dc05093d197a3404b53e0c9e4d
Mode: full (6 review roles + 6 Phase-5 verifications, including one 600 DPI visual confirmation)
Recommended severity: REQUEST_CHANGES — 1 CRITICAL, 13 SHOULD ADDRESS, 15 SUGGESTION
Source Documents
| Document | Role | Result |
|---|---|---|
pr9318-review-context.md |
scoping | 21 files: 1 variable .py, 1 changelog, 6 baseline SNAP test YAMLs (2 new), 13 partner contract YAMLs. No parameter files changed. CI 32/32 pass. |
pr9318-review-regulatory.md |
program-reviewer | 0 CRITICAL, 3 SHOULD, 6 SUGGESTION |
pr9318-review-references.md |
reference-checker | 0 CRITICAL, 4 SHOULD, 2 SUGGESTION |
pr9318-review-code.md |
code-validator | 0 CRITICAL, 4 SHOULD, 6 SUGGESTION |
pr9318-review-tests.md |
edge-case-checker | 1 CRITICAL, 7 SHOULD, 7 SUGGESTION |
pr9318-review-pdf-issuance.md |
PDF audit (FNS Basis of Issuance) | 15/15 test cases MATCH the official table |
pr9318-review-pdf-rounding.md |
PDF audit (rounding method + FY2026 params) | 42/43 values MATCH, 1 mismatch |
pr9318-review-pdf-manifest.md |
PDF sourcing | 3 PDFs obtained; official FNS-hosted Basis-of-Issuance copy unobtainable (403/CAPTCHA), MO DSS mirror verified as the FY2026 edition |
pr9318-review-verification-queue.md |
Phase-4 triage | 5 items routed to Phase 5 |
pr9318-review-codepath-1.md |
Phase 5C | CONFIRMED — Hawaii minimum allotment |
pr9318-review-codepath-2.md |
Phase 5C | CONFIRMED — stale integration.yaml pins |
pr9318-review-codepath-3.md |
Phase 5C | CONFIRMED — stale snap.yaml:61 pin |
pr9318-review-codepath-4.md |
Phase 5C | REJECTED — California pin is correct (see Investigated and cleared) |
pr9318-review-codepath-5.md |
Phase 5C | CONFIRMED — the float guard is load-bearing |
pr9318-review-mismatch-1.md |
Phase 5D (600 DPI) | CONFIRMED MISMATCH — Hawaii minimum allotment |
pr9318-review-run-state.md |
run metrics | BEHIND=17, AHEAD=6 |
Note on the two-stage verification rule. A PDF value mismatch may be reported only if it passed
both code-path (5C) and visual (5D) verification. Exactly one item qualifies: the Hawaii
minimum allotment (codepath-1 CONFIRMED, mismatch-1 CONFIRMED at 600 DPI). Codepath-2, -3 and -5
are test-pin and dtype findings with no disputed PDF page, so 5D visual verification is not
applicable to them — they were settled empirically by running the model against the PR head, not by
skipping a required step.
Branch Status
BEHIND = 17 commits, AHEAD = 6 commits relative to main.
The branch is 17 commits stale. A rebase onto upstream/main is recommended before merge —
principally so the partner contract files (which other PRs also touch) are re-run against current
main rather than a 17-commit-old baseline, and so CI's green result reflects the merge state.
Staleness did not affect any finding in this report. Every Phase-5 verification was executed
against the PR-head snapshot (809c4d8e3e) with the snapshot's policyengine_us on sys.path[0],
and each verifier independently confirmed the loaded package resolved to the snapshot rather than
the editable install. Informational only.
Headline
The rounding-method determination — the heart of the regulatory verdict
7 CFR 273.10(e)(2)(ii)(A) permits two methods, and this PR implements method (A)(1) exactly.
It is not a hybrid and not a third method.
- (A)(1): round the 30%-of-net-income contribution up to the next higher dollar →
A = M − ceil(0.3·N). This is what the PR computes. - (A)(2): don't round the 30% at all; round the resulting allotment down →
A = floor(M − 0.3·N). - For integer
M,floor(M − x) = M − ceil(x), and 7 CFR 273.10(e)(4)(ii) guarantees the maximum
allotment is an integer for published fiscal-year values. The two methods therefore produce the
same integer for every input, and both are identical to the 7 USC 2017(a) instruction "rounded
to the nearest lower whole dollar." The implementation is simultaneously compliant with all three
citations. - The net-income step is likewise correct: 7 CFR 273.10(e)(1)(ii)(A) requires 1–49¢ down and 50–99¢
up, which is half-up, notnp.round's banker's rounding.np.floor(x + 0.5)implements this
faithfully, including at exactly.50.
Independent corroboration from the official FNS table. The Basis of Coupon/EBT Issuance table
(48 States & DC, October 1 2025) encodes allotment = max_allotment − ceil(0.30 × N). Round-up was
confirmed on boundary rows across household sizes 1, 2, 3, 5, 7 and 10, in each of which the
"truncate" prediction is a dollar higher than what the table prints (size 1 band 1-3 → 297 not
298; size 2 band 14-16 → 541 not 542; size 3 band 51-53 → 769 not 770; size 5 band 4-6 → 1181
not 1182; size 7 band 7-10 → 1568 not 1569; size 10 band 11-13 → 2221 not 2222). The
categorical evidence is that the table's first band is the singleton 0 - 0: under truncation
0.30 × 3 = $0.90 truncates to 0, so the opening band would necessarily be 0 - 3 and no 0 - 0
singleton could exist. It exists on every column.
All 15 cases in the new issuance test matched the official table exactly — zero differences of
any size. Note the new file snap_normal_allotment_basis_of_issuance.yaml contains 15 cases,
not the 13 the diff summary suggested (YAML entries at lines 8, 38, 68, 98, 128, 158, 188, 218,
248, 278, 308, 338, 369, 404, 436); all 15 were audited.
Direction of the fix is right. The old floor(net) * 0.3 truncated net income downward even at
50–99¢ and applied no rounding at all to the 30%. It understated the expected contribution,
overstated the allotment, and produced allotments in cents that appear nowhere in the FNS table —
mismatching 21 of 30 checked net-income values in the FY2026 3-person column.
Measured household impact
Verified over a grid of net incomes $0–$2,000 crossed with cent values {0, .01, .25, .49, .50, .51,
.75, .99}:
- Expected contribution weakly increases; SNAP allotment weakly decreases. Monotone — no
household's benefit can rise. - $0 to $1.20 per month ($0–$14.40/year), mean ≈ $0.60/month among households with positive net
income; 95.0% of the grid moves. The $1.20 ceiling = up to +$0.30 from the net-income half-up
correction plus up to just under +$1.00 from theceil. - No eligibility flips.
is_snap_eligibleis unchanged in every partner file in the diff; only
benefit amounts move. - Unaffected: $0-net-income households (max allotment either way), net incomes that are exact
multiples of $10 with no cents, and 1–2 person households already floored at the minimum.
Critical
C1 — Two baseline test pins still assert the removed formula, hidden behind wide error margins
Confirmed by Phase-5 model runs (codepath-2 and codepath-3), not by inspection.
Frame this correctly: the PR's new values are the right ones. The defect is not that the model
is wrong — 47 / 611 follow directly from 7 CFR 273.10(e)(1)(ii)(A) and (e)(2)(ii)(A)(1), which
is exactly what the PR set out to implement. The defect is that these integration pins were not
updated alongside the 20 other baseline pins that were, leaving stale expectations in the tree
that only survive because a wide absolute_error_margin swallows the entire effect of the change.
CI being 32/32 green is not evidence these cases are current.
(a) policyengine_us/tests/policy/baseline/gov/usda/snap/integration.yaml — case "Family of 3,
CBPP Example", period: 2022-01
| Line | Pinned | Model output at PR head | Δ | Margin | Masked? |
|---|---|---|---|---|---|
integration.yaml:93 |
snap_expected_contribution: 154 * 0.3 → 46.20 |
47.00 | 0.80 | 5 (:54) |
yes |
integration.yaml:100 |
snap_normal_allotment: 612 |
611.00 | 1.00 | 5 (:54) |
yes |
snap_net_income here is a computed output, not an input, so the case does exercise the changed
formula. This is the only file under gov/usda/snap/ that reaches snap_expected_contribution
through a formula and was not touched by the PR — the sweep skipped it. The file is absent from the
21-file diff (the integration.yaml the PR does touch is the different file at
.../snap/income/ineligible_members/integration.yaml).
One correction to the original reviewers' framing, from codepath-2: line 93 does not encode the
old formula's output. The model's net income here is 153.59998, so the old formula returned
floor(153.59998) * 0.3 = 45.90, not 46.20. Line 93 is CBPP's own hand arithmetic on the article's
rounded $154, and it was already $0.30 off from the model before this PR — swallowed by the same
margin. This strengthens the finding: the ±5 margin has been hiding drift in both directions, and
the pin was never an assertion of model behaviour. The mechanical bound also holds generally — this
PR can move a single month by at most ~$1.10, so no SNAP case carrying a ±5 margin can ever
detect it.
(b) policyengine_us/tests/policy/baseline/gov/usda/snap/snap.yaml:61 — case "North Carolina
2025, yearly integration test"
Pinned snap: 71.75; model at PR head returns exactly 72.0 (float32, exact). Δ = 0.25 against
absolute_error_margin: 0.3 at snap.yaml:44 — masked. Month-by-month trace: Jan–Sep ineligible
($0), Oct–Dec eligible at the FY2026 minimum allotment $24 → 3 × $24 = $72. Every month is a whole
dollar; there is no $0.25 anywhere in the calculation.
This one is pre-existing — PR 9318 does not touch line 61 (its three hunks in this file start at
old line 88), and the pre-PR formula also yields $72 here because the expected contribution (~$550)
dwarfs the $298 max allotment and the result floors to the minimum either way. The pin drifted twice
under the margin: once when the FY2026 actual ($298) replaced a projection, and again when merged
commit cf165464a6 added statutory rounding to snap_min_allotment. Worth fixing as a drive-by
since the PR already edits this file.
Suggested fix (not applied — this review is read-only):
integration.yaml:93→47;integration.yaml:100→611. Leave the verbatim CBPP comment
block at lines 46–99 untouched (line 46 declares it verbatim, and CBPP's "about $46 / about $612"
is the source's rounding). Add a short comment noting the ~$1 divergence from CBPP is the
expected consequence of the regulatory rounding rule. Tightening the ±5 margin below ~1 would
additionally require re-deriving lines 79 (750.6) and 90 (153.59998).snap.yaml:61→72, and tightensnap.yaml:44from0.3to0.01.
Severity note for transparency: the edge-case-checker rated this CRITICAL; the code-validator
rated the same finding SHOULD-ADDRESS (SA-1), and codepath-2's verifier concurred with
SHOULD-ADDRESS/HIGH on the grounds that nothing ships broken and no user-facing number is wrong.
Per the merge-at-highest-priority rule it is reported here as CRITICAL. The maintainer may
reasonably treat it as a two-value blocking-but-trivial fix rather than a design problem.
Partner Contract Tests — maintainer decision required (A1)
13 of the 21 changed files are API partner contract tests, carrying ~59 changed expected-value
lines plus ~82 rewritten arithmetic comment lines:
| Family | Files | Changed value lines |
|---|---|---|
partners/amplifi/2025.yaml, 2026.yaml |
2 | 1 + 3 |
partners/impactica/2025.yaml |
1 | 1 |
partners/analytics_coverage/edge_cases/federal/nutrition/snap/ — federal, ca, ks, ma, nc, co, il, wa |
8 | 17, 4, 3, 2, 2, 1, 1, 1 |
partners/analytics_coverage/signatures/ca.yaml, federal.yaml |
2 | 23 + 10 |
Per this repository's CLAUDE.md, files under policyengine_us/tests/policy/baseline/partners/**
are API partner contract tests, not ordinary snapshots — changed expected outputs constitute a
partner-facing API change, and the three-question gate (are you sure you want to edit this file /
have you notified a team member / have you notified the API partner) must be run with the user
before any edit.
The PR body claims Ziming's approval and offers the API partner's own rounding report as partner
notice. This review cannot verify either claim. Nothing in the diff, the CI record, or any
fetchable artifact corroborates that the approval exists or that the three-question gate was run
before the 13 files were edited. All three code-facing roles flagged it and all three explicitly
declined to judge it. This is something the maintainer must consciously accept before merge, not
something the review has cleared.
Corroboration that does exist (all mechanical, none of it a substitute for the approval):
- Structural integrity — code-validator. All 13 files are SAME-KEYS: in every file the
multiset of YAML keys on added lines is identical to that on removed lines. No key added, removed
or renamed; no case added or deleted; noperiodorabsolute_error_marginaltered; no input
block touched. Edits are confined to expected-output scalars and (in the 8 edge-case files) the
arithmetic comments. - Direction — code-validator. Across the entire diff, baseline and partner, all 83 changed
numeric expectations are thesnapkey and all 83 are weakly decreasing. Zero increases, zero
non-snapnumeric changes. Consistent withEC_new >= EC_oldalways. - No orphaned downstream pins. These files pin no aggregate depending on
snap
(household_net_income,spm_unit_benefits,spm_unit_net_incomeappear nowhere under
tests/policy/baseline/partners/), so no companion value should have moved and didn't. - Arithmetic — edge-case-checker. Hand-derived 22 of ~59 changed partner values (every value
whose rewritten comment carries full arithmetic), across all three families. Every one is
consistent with the new formula. Worked example (federal aged-SSI): Jan–Sep net 785 →
ceil(235.50)= 236 → 298 − 236 = 62 × 9 = 558; Oct–Dec net 777.62 → 778 →ceil(233.40)= 234 →
308.52 − 234 = 74.52 × 3 = 223.57; total 781.57 ✓. - Cents fingerprint — all 59 values. Under the new formula a full-year 2026 total must equal
9 × integer + 3 × (FY2027 max allotment − integer), so the cents are determined entirely by
unit size: 1-person.57, 2-person.84, 3-person.17, 4-person.31, period-2025.00.
All 59 changed values conform. The old values had arbitrary cents (.91 .41 .51 .01 .26 .44 .10);
the new ones collapse to exactly these five endings — a strong signal the fix was applied
uniformly and no pin was hand-edited to a wrong value. No inconsistency found. - Magnitudes — regulatory role. All observed partner deltas are annual, all negative, none
exceeding $14.40: impactica 2025 −$10.50; amplifi 2025 −$8.10; amplifi 2026 −$9.00 / −$3.60 /
−$2.09; signatures/ca −$13.20 (= 12 × $1.10) and −$6.60; signatures/federal −$9.00. No
eligibility flips in any partner file.
Should Address
A1 — Partner contract approval and notice are unverifiable
See the dedicated section above. policyengine_us/tests/policy/baseline/partners/** — 13 files,
~59 changed expected values. Maintainer gate, not a review clearance.
A2 — The code comment's equivalence claim is false in PolicyEngine's own projected years
policyengine_us/variables/gov/usda/snap/snap_expected_contribution.py:25-28
The comment states that rounding the 30% up is "equivalent to rounding the allotment down to the
nearest lower whole dollar (7 CFR 273.10(e)(2)(ii)(A)(2))". That holds only when
snap_max_allotment is a whole dollar. It is not in projected years:
parameters/gov/usda/snap/max_allotment.yaml carries uprating: gov.usda.snap.uprating, so
post-COLA months get fractional maxima. The PR's own diff demonstrates it — pins of 812.72,
565.28, 1029.10, 308.52 produce benefits like 156.72, 188.72, 74.52.
Consequence: option (1) yields a $156.72 monthly allotment where option (2) would yield
floor(812.72 − 654.9) = 157. No state issues an allotment in cents under either CFR branch, and
7 USC 2017(a) requires a whole-dollar allotment. This PR correctly fixes the contribution rounding
but leaves the allotment non-integer in projected years.
Resolution (either is defensible; both are outside this PR's minimal scope but should be filed
rather than dropped): round the uprated snap_max_allotment to a whole dollar (FNS publishes whole
dollars, so the projection should too), or apply option (2) by flooring snap_normal_allotment.
At minimum, qualify the comment so it does not assert an equivalence the model can violate.
(Merged: regulatory SHOULD 1 + references R4.)
A3 — Net-income rounding lives in one consumer, not at the source
snap_expected_contribution.py:24 vs
policyengine_us/variables/gov/usda/snap/eligibility/meets_snap_net_income_test.py:17 and
policyengine_us/variables/gov/usda/snap/income/snap_net_income_fpg_ratio.py:13
7 CFR 273.10(e)(2)(i)(A) compares "their net income, as calculated in paragraph (e)(1)" — i.e.
the rounded figure — to the net income standard. After this PR, snap_net_income is rounded
inside snap_expected_contribution but meets_snap_net_income_test still compares the raw,
cents-bearing value to the ceiling'd limit. The same regulatory quantity now has two different
values depending on which consumer reads it, and this PR introduces the split.
Numeric impact is small (an eligibility flip needs net income in the half-open band (L, L + 0.50)
for whole-dollar limit L), but the inconsistency is real. Cleaner: round at the source — make
snap_net_income return the rounded figure, or add a snap_rounded_net_income variable citing
(e)(1)(ii)(A) and have all three consumers read it. That fixes snap_net_income_fpg_ratio for free
and is the DRY placement for the floor(x + 0.5) expression. If deferred, file it.
(Merged: regulatory SHOULD 2 + code S-2.)
A4 — The reference tuple cites only one of the two governing paragraphs
snap_expected_contribution.py:11-15
reference = (
"https://www.law.cornell.edu/uscode/text/7/2017#a",
# 7 CFR 273.10(e)(1)(ii)(A) and (e)(2)(ii)(A)(1).
"https://www.law.cornell.edu/cfr/text/7/273.10#e_2_ii_A_1",
)The comment names two provisions but only one href is supplied, anchored on (e)(2)(ii)(A)(1).
A reader clicking it lands on the 30%-round-up paragraph and never sees the net-income half-up rule
that np.floor(net + 0.5) on line 24 implements. The comment's placement directly above a URL
covering only the second provision compounds it. Suggested:
reference = (
# 7 USC 2017(a): allotment = thrifty food plan less 30% of income.
"https://www.law.cornell.edu/uscode/text/7/2017#a",
# 7 CFR 273.10(e)(1)(ii)(A): net income rounded 1-49 cents down, 50-99 cents up.
"https://www.law.cornell.edu/cfr/text/7/273.10#e_1_ii_A",
# 7 CFR 273.10(e)(2)(ii)(A)(1): 30% of net income rounded up to the next higher dollar.
"https://www.law.cornell.edu/cfr/text/7/273.10#e_2_ii_A_1",
)The #e_1_ii_A anchor form matches established repo usage (#d_2_ii_A in
parameters/gov/usda/wic/income/sources.yaml; #c_5, #a_1 elsewhere).
Related, same theme: three test comments cite bare (e)(2)(ii)(A) without the terminal (1) —
tests/policy/baseline/gov/usda/snap/snap_normal_allotment_basis_of_issuance.yaml:4 and
tests/policy/baseline/gov/usda/snap/snap_categorical_eligibility_ceiling.yaml:~266. This matters
more than usual because (A)(2) prescribes the opposite mechanic; citing bare (A) points at a
fork rather than the branch taken. Append (1).
(Merged: regulatory SHOULD 3 + references R1, R5, R6 + code S-3.)
A5 — Both cited provisions are state options, presented as unconditional federal mandates
snap_expected_contribution.py:24 and :29
Verified verbatim from GPO govinfo XML, corroborated by Cornell LII:
(e)(1)(ii)intro: "the State agency shall use one of the following two procedures" — (A)
is 1-49-down/50-99-up; (B) is "apply the rounding procedure … in effect for the State's TANF
program", which may include the cents.(e)(2)(ii)(A)intro: "the State agency shall round in one of the following ways" — (1)
round the 30% up; (2) don't round the 30% and round the allotment down.
The code comments read as though (A) and (A)(1) are the uniform federal rule. The citations do
corroborate the arithmetic implemented, so this is not a wrong citation — but it overstates the
source. Because (A)(1) ≡ (A)(2), the allotment election is harmless; the (e)(1)(ii)(A) vs (B)
election is not a no-op — a state that includes cents in income calculations would carry them
through the whole deduction chain, and no parameter models that today.
Minimum fix: add one clause to each comment, e.g. "…, the option PolicyEngine applies for all
states; states may instead elect (e)(1)(ii)(B) (TANF rounding)." Longer term, consider a
gov.usda.snap.rounding.net_income parameter with a state breakdown, mirroring how other SNAP state
options (BBCE; DC/MD/NJ minimum allotment) are handled. Option (A) is the right default — it is what
the FNS issuance table assumes.
(Merged: references R2 + regulatory SUGGESTION 5 + pdf-rounding caveat.)
A6 — The FNS issuance table is cited via a mutable state-agency mirror
policyengine_us/tests/policy/baseline/gov/usda/snap/snap_normal_allotment_basis_of_issuance.yaml:3
# https://dssmanuals.mo.gov/wp-content/uploads/2022/09/snap-basis-issuance-table.pdf#page=1The document is the right table — three independent confirmations: the printed header on every
page reads "Food & Nutrition Service / Basis of Coupon / EBT Issuance / 48 States & DC / October
1, 2025" with an 8/14/2025 revision stamp and "Reduction Amt: 30%"; its 0 - 0 row reads
298/546/785/994/1183/1421/1571/1789/2007/2225, identical to the official FNS FY2026 maximum
allotments; and parameters/gov/usda/snap/max_allotment.yaml already carries those same values at
2025-10-01 citing https://www.fns.usda.gov/snap/allotment/cola/fy26. #page=1 is correct
(offset 0) and all eight cited bands were confirmed on the rendered page image.
Two problems remain:
- Authority. A federal USDA-FNS value cited to a state agency's website. Whether FNS
publishes the full basis-of-issuance table onfns.usda.govis unresolved —fns.usda.gov
returns HTTP 403 to automated fetches and every search engine served a CAPTCHA. Needs a human
with a browser. If no official copy exists, the mirror is acceptable under the "no government
source exists" carve-out, but the comment should say so. - Mutability — the real risk. The MO URL is replaced in place each October. Around 2026-10-01
it will silently become the FY2027 table while the test header still claims "October 1, 2025"
and "$785" — the reference will then actively contradict the test. Pin it to a Wayback capture
(https://web.archive.org/web/20251101194701/https://dssmanuals.mo.gov/...#page=1, confirmed
available) and add the official FNS COLA URL alongside it.
(Merged: references R3 + regulatory SUGGESTION 8 + pdf-issuance advisory.)
A7 — The float guard is load-bearing; document it as such and retract the suggestion to remove it
snap_expected_contribution.py:31 — contribution = np.round(net_income * expected_food_contribution, 2)
Three roles disagreed; codepath-5 settled it empirically. The code-validator (S-1) and
edge-case-checker (S1) were right; the regulatory role's SUGGESTION 4 — that the guard is an inert
no-op — used the wrong dtype and its recommendation to remove the guard is hereby retracted.
policyengine-core maps value_type = float → numpy.float32
(policyengine_core/variables/config.py:24). Both snap_net_income and
snap_expected_contribution are float, the rate 0.3 is a Python float, and under both NEP-50
and legacy casting a float32 array times a Python float scalar stays float32. Confirmed by
introspection of the built system and by a live simulation: snap_net_income and
snap_expected_contribution both report dtype: float32.
Scan of ceil(round(floor(x+0.5) * 0.3, 2)) (guarded) vs ceil(floor(x+0.5) * 0.3) (unguarded):
| dtype | inputs | divergences |
|---|---|---|
| float32 (the model's real dtype) | integer net incomes 0..20000 | 677 (3.38%) |
| float32 | integer net incomes 0..2000 (realistic SNAP range) | 81 (4.05%) |
| float64 | same ranges | 0 |
Concrete diverging example, end to end in a real simulation using the PR's own new test household
(MO, 3 persons, tanf: 3108, housing_cost: 0, 2026-01 → net income $50):
GUARDED net=[50.] contrib=[15.] allotment=[770.] snap=[770.]
UNGUARDED net=[50.] contrib=[16.] allotment=[769.] snap=[769.]
float32(50) * 0.3 = 15.00000095367431640625, so an unguarded ceil returns 16. Removing the
guard would systematically overstate the contribution — and understate the allotment — by exactly
$1 for roughly 3–4% of households (those whose rounded net income is one of the ~34% of multiples of
$10 carrying upward float32 noise). The error is one-directional and always against the
household. There are zero cases where the guard rounds down a legitimate round-up: because
net_income is an integer after floor(x + 0.5), net_income * 0.3 has at most one decimal digit
in exact arithmetic, so round(..., 2) reproduces the exact value and can never mask a true
fractional remainder. The guard is mathematically exact, not a fudge.
Actions:
- Keep the guard. The existing comment already states the reason correctly and sufficiently.
- The only test in the entire SNAP suite that would catch its removal is the net-$50 case at
snap_normal_allotment_basis_of_issuance.yaml:278(Δ = 1.0 > margin 0.1). Every other case — net
0/3/4/6/7/10/11/13/14/51/3.49/3.50/6.50/10.50, the twosnap_expected_contribution.yamlcases,
bothsnap_categorical_eligibility_ceiling.yamlcases,integration.yaml— is non-diverging
(float32(10 * 0.3) == 3.0exactly). Add a comment on that case marking it as the float32
regression guard, so a future cleanup does not delete both the guard and its only coverage. - Assert
snap_expected_contributionexplicitly in the net-$10 and net-$50 cases (currently only
the downstream allotment is pinned, and the comments say "30% = 3.00 exactly", which reads as a
trivial case rather than the regression guard it is).
(Merged: tests S1 + code S-1 + regulatory SUGGESTION 4 [retracted] + codepath-5 CONFIRMED.)
A8 — No baseline case pins the rounding across the October-1 fiscal-year boundary
All 15 new cases and all 12 ineligible_members cases use period: 2026-01. The Oct–Dec months —
where an integer expected contribution is subtracted from a fractional, uprated max allotment
(the exact configuration behind A2) — are pinned only in the 13 partner contract files, which
reviewers must not edit, and in one work-requirements case. A whole-year period with month-keyed
output is the right tool. Suggested addition to
policyengine_us/tests/policy/baseline/gov/usda/snap/snap_normal_allotment_basis_of_issuance.yaml
(3-person MO, tanf: 2_640/yr = $220/month, housing_cost: 0, period: 2026):
snap_expected_contribution:
2026-09: 4 # net = 220 - 209 = 11 -> ceil(3.30) = 4
2026-10: 2 # net = 220 - 216.38 = 3.62 -> rounds to 4 -> ceil(1.20) = 2(tests S2.)
A9 — The smallest non-zero contribution boundary (net $0.50) is untested
The matrix covers net $0 (EC 0, full $785) and net $3 (EC 1) but not the point where the contribution
first becomes non-zero: net $0.49 → EC 0 → $785; net $0.50 → rounds to 1 → EC 1 → $784. This case
also discriminates the new rule from the old (floor(0.50) × 0.3 = 0 → $785) and is the cleanest
single pin of the half-up-vs-banker's-rounding fix, which is the subtlest part of the change.
Add to snap_normal_allotment_basis_of_issuance.yaml (3-person MO, tanf: 2_514).
(Merged: tests S3 + pdf-rounding MISSING-FROM-REPO 5.)
A10 — The minimum-allotment interaction with the new rounding is untested at its boundary
snap_normal_allotment = max_(min_allotment, max − EC).
policyengine_us/tests/policy/baseline/gov/usda/snap/snap_categorical_eligibility_ceiling.yaml
covers only the far side (max − EC deeply negative → $24 for 2p, $0 for 3p). The interesting boundary
— where the $1 shift this PR introduces is exactly what pushes the allotment below the $24 floor —
is untested. For a 2-person FY2026 unit:
| target | net income | EC = ceil(0.3n) | 546 − EC | result |
|---|---|---|---|---|
| just above floor | 1,736 | 521 | 25 | snap 25 |
| exactly at floor | 1,740 | 522 | 24 | snap 24 |
| just below floor | 1,745 | 524 | 22 | snap 24 (min binds) |
Under the old formula the middle row gave 546 − 522.00 = 24.00 and the third 546 − 523.50 = 22.50, so a case at net 1,740 pins that the ceil does not accidentally push a household off the
floor. (tests S4.)
A11 — Two partner pins now sit 0.0002 from a rounding tie
policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/il.yaml
documents net = 540.4998, and .../snap/federal.yaml (LUA case) documents net = 686.4998. Both
are two hundredths of a cent below the .5 half-up boundary. Any future FPG/SUA/LUA uprating that
nudges these by +$0.0002 flips the net dollar, the contribution, and the annual pin by $1–$3 — and
these are partner contract values, so a silent flip is partner-visible. Do not edit these files.
The fragility is worth the maintainer's awareness, and it independently argues for the baseline-side
.5 coverage in A8/A9 so the rounding rule is pinned somewhere robust rather than only at knife-edge
households. (tests S6.)
A12 — The new test files depend on three unasserted parameter facts
The whole 15-case band matrix in snap_normal_allotment_basis_of_issuance.yaml is constructed as
net = tanf/12 − 209. It silently assumes (a) the FY2026 1–3-person standard deduction is exactly
$209, (b) Missouri applies no standard utility allowance with housing_cost: 0, and (c) TANF is the
sole income. If any of those changes, all 15 cases shift together and the failure message points at
allotments rather than at the cause. Assert snap_standard_deduction: 209 and
snap_excess_shelter_expense_deduction: 0 in at least the first case. (tests S7.)
A13 — Test-file convention deviations in the two new files
- Case naming. All 18 new cases lack the directory's
Case N, …prefix and trailing period —
snap_normal_allotment_basis_of_issuance.yaml:8,38,68,98,128,158,188,218,248,278,308,338,369,404,436
andsnap_categorical_eligibility_ceiling.yaml:7,41,79. Siblings do follow it
(income/ineligible_members/integration.yaml:6). The descriptive names are genuinely informative,
soCase N, 3-person household, net income $0 … receives $785.beats discarding them. - Person keys.
parent/child1/child2at
snap_normal_allotment_basis_of_issuance.yaml:13,15,17(repeated per case) and
snap_categorical_eligibility_ceiling.yaml:12,16,46,50,52; the local norm isperson1/
person2/person3(income/ineligible_members/integration.yaml:11,16,18). - Underscore formatting. Within one file,
tanf:values are written without underscores at
snap_normal_allotment_basis_of_issuance.yaml:22,52,82,112,142,172,202,232,262,292,322(2508,
2544, …) but with them at:352,383,418,450(2_549.88,2_550,2_586,2_634). Repo style
is2_508for values ≥ 1,000. - File naming. Neither new file mirrors a variable name;
snap_normal_allotment.yamland
meets_snap_categorical_eligibility.yamlalready exist. Consider appending to those, or moving
these under a descriptive folder asintegration.yaml, to keep the "test path mirrors variable
path" convention intact.
(Merged: code SA-2, SA-3, SA-4 + tests G1 + regulatory SUGGESTION 9b.)
Suggestions
S1 — 7 CFR 273.10(e)(2)(iii) zero-benefit denial for 3+ person units is not modeled
snap_categorical_eligibility_ceiling.yaml case 2 honestly documents this ("PE does not convert a
zero-benefit 3+ unit into a denial") and pins is_snap_eligible: true with snap: 0. Benefit
dollars are unaffected, but downstream consumers of is_snap_eligible (categorical-eligibility
chains for school meals, Lifeline, etc.) see a household the reg would have denied. The reg does
give the state a choice between denial (A) and certify-and-suspend (B), so eligible-with-zero is
defensible — but pinning an acknowledged departure without an issue link makes it read as settled.
Add a # TODO(#NNNN) or link a tracking issue. Same for the (e)(2)(ii)(B) initial-month $10 floor,
which an annual model cannot express. Pre-existing, out of scope, worth filing.
(regulatory SUGGESTION 6 + tests G7.)
S2 — Changelog fragment carries a leading bullet the repo convention omits
changelog.d/fix-snap-expected-contribution-rounding.fixed.md begins with - Round SNAP …. Every
merged fragment in this repo starts with bare prose and towncrier's markdown template supplies the
- , so as written this renders - - Round SNAP … in CHANGELOG.md. Drop the leading - . (The
fragment is otherwise correct: top-level location, .fixed type, stem matching the branch, and both
CFR subsections cited.)
S3 — Stale float-margin and comment in the variable's own test file
policyengine_us/tests/policy/baseline/gov/usda/snap/snap_expected_contribution.yaml:3,34 keep
absolute_error_margin: 0.01 # Floating point issue. The expected outputs are now exact whole-dollar
multiples (250 * 12, 129 * 12), so both the margin and the stale comment can go, sharpening the
pin.
S4 — A partner comment's arithmetic is off by a cent (comment only, do not edit)
policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ca.yaml
new comment reads "Annual = 9 × 127.00 + 3 × 156.72 = 1,613.17"; that arithmetic gives 1,613.16. The
asserted value 1613.17 is the true float result — the comment's rounding of the Oct–Dec monthly
figure is what is imprecise. Cosmetic, and it lives in a partner contract file, so it is recorded
rather than proposed as an edit.
S5 — The float32 guard degrades at extreme magnitudes (no benefit impact)
At net income ≈ $1e8, float32(N * 0.3) is 30000002.0, which round(·, 2) cannot clean (spacing
exceeds $0.005). Immaterial: snap_expected_contribution has no defined_for so it is computed for
every SPM unit, but it feeds only snap_normal_allotment, which is defined_for = "is_snap_eligible"
and floors at the minimum allotment. Recorded for completeness. (code S-4.)
S6 — Most new issuance cases do not assert the changed variable
snap_normal_allotment_basis_of_issuance.yaml asserts snap_expected_contribution at only lines
365, 396, 432, 464; the other eleven cases assert snap_normal_allotment / snap alone. A future
regression in the rounding surfaces as a downstream failure rather than pointing at the changed
variable. (code S-5; overlaps A7 action 3.)
S7 — absolute_error_margin: 0.1 on currency — consistent locally, looser than the written guideline
snap_normal_allotment_basis_of_issuance.yaml:10 (every case) and
snap_categorical_eligibility_ceiling.yaml:9,43,81 use 0.1; the guideline for currency is 0.01.
This matches the existing SNAP directory convention exactly
(income/ineligible_members/integration.yaml:8,34,62,…) and 0.1 is still an order of magnitude
tighter than the $1-scale effects under test. No change recommended — recorded only so the
deviation from the written guideline is on file. (code S-6.)
S8 — The guard assumes a rate with at most two decimals
snap_expected_contribution.py:31. At rate 0.3 the guard can never over-correct, because 3N/10 has
exactly one decimal digit and its nearest non-integer neighbours sit $0.10 away — twenty times the
$0.005 rounding radius. The residual risk is a parametric reform setting a rate with more than
two decimals: round-then-ceil then diverges from an exact ceil whenever the true product lands in
(k, k+0.005]. Concrete: rate 0.2001, net income $25 → exact ceil(5.0025) = 6, shipped code
gives 5. If that matters, np.ceil(x - 1e-6) is rate-agnostic; otherwise a one-line comment noting
the guard assumes a cents-precision rate is enough. (code S-1 residual.)
S9 — Test-coverage expansion opportunities
None of these are errors; they are untested surface.
- Only household size 3 is exercised by the 15 new issuance cases. The table gives ten
independent size columns on page 1; a size-indexing bug would not be caught. Highest-value
additions: size 1 (0 - 0→ 298,1 - 3→ 297 — the single most discriminating round-up row in
the document) and size 10 (0 - 0→ 2225,11 - 13→ 2221, which also exercises the
additional-person extrapolation for sizes 9–10 that is not an explicit parameter row). - Only the extreme low end of the income range: net $0–$51, the first 17 of ~1,668 bands. Nothing
tests magnitudes where float error is larger — e.g. size 7 net 4991 → 73, size 10 net 5004 → 723
(both on page 45). - Band ends tested at one end only: bands
1-3and14-16. Adding net $1 (tanf: 2_532, EC 1 →
$784) and net $16 (tanf: 2_700, EC 5 → $780) completes the symmetry the other bands have. - The variable's own test file is thin:
snap_expected_contribution.yamlholds 2 cases, both
period: 2022, both 4-person. A few cases feedingsnap_net_incomedirectly as an input and
assertingsnap_expected_contributionalone would isolate the rule (0 → 0; 0.49 → 0; 0.50 → 1;
10 → 3; 428.50 → 129; 1,172.50 → 352). - Only one state (MO) and no non-contiguous region. The table covers 48 States & DC and MO is in
scope, but AK/HI/GU/VI go untested — see also S11.
(Merged: tests G2/G3/G4/G5 + pdf-issuance MISSING-FROM-REPO 1–4.)
S10 — Booleans asserted under a case-level margin
Both new files set absolute_error_margin: 0.1 at case level while asserting booleans
(is_snap_eligible, meets_snap_gross_income_test, meets_snap_categorical_eligibility). Since
0.1 < 1 the boolean assertions remain functional — not a defect — but the convention is no margin
on booleans; splitting them into a marginless case would match the reference. (tests G6.)
S11 — Store the FNS-published minimum allotments rather than deriving them
This is the durable fix for the confirmed Hawaii mismatch below. The FY2026 COLA memo publishes an
explicit seven-region minimum-allotment row; the repo derives the value from the rounded one-person
maximum allotment instead. Consider an FY-dated min_allotment/amount.yaml broken down by
snap_region (sourced to the COLA memo #page=5, exactly as max_allotment already is), or store
the unrounded one-person TFP cost as the derivation base. A Hawaii-only override would paper over the
mechanism and need re-checking every COLA year. Add YAML tests for at least one non-contiguous
region — policyengine_us/tests/policy/baseline/gov/usda/snap/snap_min_allotment.yaml has ten cases
and all ten are CONTIGUOUS_US (CA, NJ, DC, MD), which is why the divergence is unasserted.
Out of scope for this PR.
S12 — Guam and the U.S. Virgin Islands have no poverty guideline
policyengine_us/parameters/gov/hhs/fpg.yaml sets first_person and additional_person to 0 for
GU, PR and VI, but the FY2026 COLA memo groups Guam and the Virgin Islands with the 48
states and D.C. for income eligibility. Any GU/VI SPM unit routed through snap_fpg.py gets a $0
gross and net income standard and fails both tests — while the repo carries full GU/VI max-allotment
and standard-deduction columns. Territory support is half-present. Out of this PR's scope; a
coherence gap in the FY2026 parameter set.
S13 — FY2026 parameter references cite landing pages instead of the PDF with a page anchor
The FY2026 entries in parameters/gov/usda/snap/max_allotment.yaml,
income/deductions/standard.yaml, income/deductions/excess_shelter_expense/cap.yaml and
.../homeless/deduction.yaml all cite https://www.fns.usda.gov/snap/allotment/cola/fy26 (a landing
page), while the FY2016–FY2024 entries cite the PDF with #page=1 / #page=2. Per the parameters
convention, these should carry anchors: max allotments #page=4/#page=5, standard deductions
#page=6, shelter cap #page=6, homeless deduction #page=6, minimum allotments #page=5, asset
limits #page=7.
S14 — expected_contribution.yaml metadata is incomplete
policyengine_us/parameters/gov/usda/snap/expected_contribution.yaml has unit: /1 and a
reference, but no label and no period — both required by the parameter convention. It also
cites only 7 USC 2017(a); now that the variable's rounding rests on 7 CFR 273.10(e)(2)(ii)(A), the
parameter should carry that cite too. (The variable was correctly updated with both cites by this
PR.)
S15 — Document the upstream invariant that makes negative net income unreachable
snap_expected_contribution.py:24. Half-up rounding is asymmetric on negatives
(floor(-3.5 + 0.5) = -3), and a negative contribution would push the allotment above the maximum.
This is unreachable only because snap_net_income.py:16 returns max_(0, gross - deductions) — an
invariant that lives in a different file. Worth a one-line comment in the variable noting the
reliance. Relatedly, (e)(1)(ii)(A) says round each income and allotment calculation — the standard
deduction, earned-income deduction and shelter computations too — while the model rounds only at the
30% step. Pre-existing, not introduced here, and it affects none of the new cases (all monthly inputs
are exact), but it can produce ±$1 drift elsewhere.
Investigated and cleared
The California ineligible_members pin (225) — codepath-4 REJECTED, no defect
policyengine_us/tests/policy/baseline/gov/usda/snap/income/ineligible_members/integration.yaml:206-231,
Case 8 ("California delays OBBBA so refugees remain eligible in January 2026", period: 2026-01).
This was the single changed baseline pin the edge-case-checker could not close by hand, because it
depends on a CalWORKs grant entering SNAP unearned income — itself a multi-step state computation.
Phase-5 ran the model against the PR head and closed it. The model returns exactly 225.0; the
PR updated the pin correctly.
| Step | Value |
|---|---|
| gross income | 2,000 earned + 475 CalWORKs unearned = 2,475 |
| deductions | 400 earned-income (20%) + 209 standard = 609 |
| net income | 2,475 − 609 = 1,866 |
| expected contribution | ceil(round(floor(1,866 + 0.5) × 0.30, 2)) = ceil(559.80) = 560 |
| max allotment (3-person, FY2026) | 785 |
snap_normal_allotment |
785 − 560 = 225 ✓ new pin |
The CalWORKs term: ca_tanf = (3/3) × min(max(1,175 − 700, 0), 1,175) = $475/month, flowing
straight into snap_unearned_income. Controlled comparison against the pre-PR tree reproduces the
old pin to float32 precision (225.19995 vs pinned 225.2), and the new tree reproduces 225
exactly — the 0.2 shift is entirely the new ceil (559.80 → 560). The margin also discriminates:
absolute_error_margin: 0.1 would fail on the old value (Δ = 0.19995 > 0.1), so this pin
genuinely guards the change. Not a defect; reported here only so the decomposition is on record.
Method note surfaced by this verifier and worth propagating: a script file run from another
directory silently imports the installed package instead of the snapshot. Verifications must run
with the snapshot on sys.path[0] (cd <snapshot> && python3 -c …).
PDF Audit Summary
| Category | Count | Detail |
|---|---|---|
| Confirmed correct | 57 | 15/15 new issuance-test cases matched the FNS Basis of Coupon/EBT Issuance table exactly (48 States & DC, Oct 1 2025, all on #page=01, offset 0); 42/43 FY2026 parameter values matched the official FNS tables — 63 max-allotment cells, 30 standard-deduction cells (incl. the easy-to-miss VI size-3 = $185 and HI size-5 = $300 irregularities), 5 shelter-cap values, the $198.99 homeless deduction, 6 of 7 derived minimum allotments, all 27 income-eligibility-standard cells, and both asset limits |
| Mismatches | 1 | Hawaii FY2026 minimum allotment: model $40 vs FNS $41 (Δ = $1.00/month). Passed both required stages — codepath-1 CONFIRMED (5C) and mismatch-1 CONFIRMED at 600 DPI (5D). See below. |
| Mismatches rejected | 1 | The California ineligible_members Case 8 pin (225) — codepath-4 REJECTED. Investigated and cleared above; it is a model-pin item, not a disputed PDF page value. |
| Unmodeled | 4 | 7 CFR 273.10(e)(2)(iii) zero-benefit denial for 3+ person units; (e)(2)(ii)(B) $10 initial-month floor and proration generally; the published 165%-of-poverty table for elderly/disabled separate households (COLA memo #page=3) has no counterpart in the SNAP parameter tree; GU/VI poverty guidelines are 0 while GU/VI allotment columns exist |
| Pre-existing | 1 | The Hawaii minimum allotment — outside this PR's diff (see below) |
The one confirmed mismatch — Hawaii FY2026 minimum allotment
Repo $40 vs FNS $41, source: FNS SNAP — Fiscal Year 2026 Cost-of-Living Adjustments
#page=5, table "MINIMUM SNAP ALLOTMENTS, OCTOBER 1, 2025 TO SEPTEMBER 30, 2026", Hawaii column.
Rendered at 600 DPI (5100 × 6600 px), cropped, and additionally upscaled 2× on the Hawaii cell to
rule out a glyph misread: it reads $41 — a dollar sign, a 4, and a 1. The full row as rendered
is 48 States & DC $24 / Guam $35 / USVI $31 / AK Urban $31 / AK Rural 1 $39 / AK Rural 2 $48 /
Hawaii $41, so no transposition is possible. The PDF text layer agrees exactly, and the memo's
own narrative independently states the 48-states figure of $24.
It is pre-existing — outside this PR's diff. grep -c "parameters/" pr9318-review-diff.txt → 0.
PR 9318 changes exactly one source file (snap_expected_contribution.py) plus test YAML and a
changelog fragment. snap_min_allotment.py, max_allotment.yaml and the whole min_allotment/
directory are untouched. The PR's rounding change cannot interact: the derived minimum does not
depend on the expected contribution, it merely floors the result. The two min_allotment hits inside
the diff are new test assertions only (snap_min_allotment: 24 for a Missouri/CONTIGUOUS_US unit and
0 for a 3-person unit) and both are correct against FNS.
It is neither a wrong stored constant nor a rounding-convention difference — it is the wrong
derivation base on a derived value. There is no stored Hawaii constant;
policyengine_us/variables/gov/usda/snap/snap_min_allotment.py:18-28 computes
np.round(0.08 × max_allotment.main.HI.1) = np.round(0.08 × 506) = np.round(40.48) = 40.
Rounding convention is decisively ruled out: the repo's np.round matches FNS in six of seven
regions (24/31/39/48/35/31), and ceil would break Alaska Rural 1 (40 vs 39) and Guam (36 vs 35),
while half-up is identical to np.round at 40.48 since no tie is involved. What differs is the
base:
- 7 CFR 273.10(e)(2)(ii)(C): the minimum is 8% of the maximum allotment for a household of
one — what the repo implements and what its comment cites. - 7 USC 2017(a): the minimum is 8% of the cost of the thrifty food plan for a 1-member
household — what FNS actually computes from. - 7 CFR 273.10(e)(4)(ii): the maximum allotment is itself the TFP cost rounded to the nearest
lower dollar.
So the published maximum is a downward-rounded proxy for the statutory base. With floor(T) = 506,
T ∈ [506, 507) and 0.08T ∈ [40.48, 40.56); FNS publishing 41 forces T ≥ 506.25. The proxy
understates by exactly $1 iff 0.08 × max ends in .48, i.e. max ≡ 6 (mod 25), and never
overstates. FY2026 residues: 298→23, 385→10, 491→16, 598→23, 439→14, 383→8, 506→6 — Hawaii is the
only FY2026 region in the failure class. Historic HI size-1 values give residues 22, 13, 2, 17,
6, so FY2026 is the first year the residue hits 6: a newly-manifesting defect produced by correct
FY2026 data, not a long-standing wrong number.
Reachable and payable, confirmed by a live model run (HI, 2-person, categorically eligible):
snap_min_allotment [40.] → snap_normal_allotment [40.] → snap [40.]. Blast radius: Hawaii only,
1–2 person households whose computed allotment falls below the floor, effective 2025-10-01,
$1/month each. Not a blocker for this PR — file it (remediation in S11).
Validation Summary
| Role | CRITICAL | SHOULD | SUGGESTION | Verdict |
|---|---|---|---|---|
| regulatory (program-reviewer) | 0 | 3 | 6 | Formula change correct; model becomes more compliant with 7 CFR 273.10(e) |
| references (reference-checker) | 0 | 4 | 2 | Both CFR designators verified verbatim against two independent sources; no wrong-subparagraph finding, rules not swapped |
| code (code-validator) | 0 | 4 | 6 | Vectorized, float-safe, ruff format clean; 10 of 10 clean categories PASS |
| tests (edge-case-checker) | 1 | 7 | 7 | Every changed baseline pin re-derived by hand and correct, except the stale ones in C1 |
| PDF — issuance table | — | — | — | 15/15 MATCH, zero differences of any size |
| PDF — rounding + FY2026 params | — | — | — | 42/43 MATCH, 1 mismatch (Hawaii, pre-existing) |
| Phase 5C/5D verification | — | — | — | 5 CONFIRMED, 1 REJECTED |
| Consolidated | 1 | 13 | 15 | REQUEST_CHANGES |
Checks that passed cleanly and are worth recording:
- No reinvented variable. The diff adds no new variable — it modifies exactly one and otherwise
touches test YAML and a changelog fragment. - Order of operations correct.
snap_net_incomealready implements 7 CFR 273.10(e)(1)(i)(A)–(I),
and the new code inserts the (e)(1)(ii) rounding at exactly the right point — after net income is
final, before the 30% multiply. - Vectorization.
np.floor,np.round,np.ceilare ufuncs, elementwise and dtype-preserving.
No Pythonif, noand/or/not, no.item(), no scalar conditional on an array. - Hard-coded values.
0.5and2are numeric-method constants, not policy values, matching 13
repo precedents for the half-up idiom. The only policy value (0.3) stays parameterised in
gov.usda.snap.expected_contribution. - Entity levels and periods. SPMUnit/MONTH reading SPMUnit/MONTH at
period; all 20 case periods
are validYYYY/YYYY-01forms; no invented input variables anywhere in the new files. - Changelog. Correct name, correct top-level location, correct
.fixedtype (patch bump), both
CFR subsections cited. Only the leading bullet (S2) is off. - Float-boundary safety. Half-up verified at 3.49→3, 3.50→4, 6.50→7, 10.50→11, 428.50→429;
negatives unreachable via the upstreammax_(0, …). - Formatting.
uv run ruff format --checkreports "1 file already formatted"; no line exceeds 88
characters.
Review Severity
REQUEST_CHANGES
One CRITICAL is present: two baseline test pins (gov/usda/snap/integration.yaml:93,100 and
gov/usda/snap/snap.yaml:61) assert values the model no longer produces, surviving only because
absolute_error_margin: 5 and 0.3 respectively swallow the drift. Both were confirmed by running
the model against the PR head.
This is not a verdict on the substance of the change, which is correct. The formula implements
7 CFR 273.10(e)(2)(ii)(A)(1) exactly, matches the official FNS issuance table on all 15 new cases and
on boundary rows across six household sizes, and makes the model strictly more compliant than the
code it replaces. The blocking item is a two-value test-hygiene omission plus one process gate. If
the maintainer resolves the partner-approval question (A1) and accepts the two pin corrections, this
is otherwise a well-evidenced, well-tested fix.
Next Steps
Before merge (blocking):
- C1 — set
policyengine_us/tests/policy/baseline/gov/usda/snap/integration.yaml:93to47and
:100to611, leaving the verbatim CBPP comment block (lines 46–99) untouched; and set
policyengine_us/tests/policy/baseline/gov/usda/snap/snap.yaml:61to72with
snap.yaml:44tightened from0.3to0.01. - A1 — the maintainer must consciously accept the partner contract change: confirm Ziming's
approval exists, confirm the API partner was notified, and confirm theCLAUDE.mdthree-question
gate was run before the 13 files undertests/policy/baseline/partners/**were edited. The review
corroborated structure, direction and arithmetic on those files but cannot verify the approval
or the notice. - Rebase onto
upstream/main(BEHIND=17) so CI's green result reflects the merge state.
Before merge (cheap and recommended):
- A2 — at minimum, qualify the equivalence comment at
snap_expected_contribution.py:25-28so
it does not assert an equivalence the uprated projected years violate. - A4 — add the
#e_1_ii_Aanchor as a thirdreferenceentry, one comment per URL; append the
terminal(1)to the three bare(e)(2)(ii)(A)test comments. - A5 — add one clause to each rounding comment noting (A)/(A)(1) are the state options
PolicyEngine applies nationwide. - A7 — add a comment on the net-$50 case at
snap_normal_allotment_basis_of_issuance.yaml:278marking it as the only float32 regression
guard in the SNAP suite, and assertsnap_expected_contributionexplicitly in the net-$10 and
net-$50 cases. Do not remove thenp.round(..., 2)guard — the earlier suggestion to do so is
retracted. - A6 — pin the issuance-table citation to a Wayback capture and add the official FNS COLA URL
alongside the Missouri mirror. - S2 — drop the leading
-from the changelog fragment.
File as follow-up issues (out of scope for this PR):
- Hawaii FY2026 minimum allotment $40 vs $41 — store the FNS-published per-region minimums, or the
unrounded one-person TFP cost, as the derivation base (S11); add non-contiguous-region tests
tosnap_min_allotment.yaml. - Round the uprated
snap_max_allotmentto a whole dollar, or floorsnap_normal_allotment, so
projected-year allotments are integers (A2). - Hoist the (e)(1)(ii)(A) net-income rounding to the source so all three consumers agree (A3).
- Test coverage: FY-boundary case (A8), net-$0.50 boundary (A9), minimum-allotment boundary
(A10), unasserted parameter facts (A12), and the breadth items in S9. - Parameter hygiene:
expected_contribution.yamllabel/period(S14), FY2026 reference page
anchors (S13), GU/VI poverty guidelines (S12), (e)(2)(iii) denial tracking issue (S1). - Convention cleanups in the two new test files (A13).
Monitor: the two partner pins sitting 0.0002 from a rounding tie (A11) — il.yaml at
net = 540.4998 and federal.yaml LUA at net = 686.4998. A future FPG/SUA/LUA uprating of
+$0.0002 will flip them by $1–$3, partner-visibly.
- Move the 7 CFR 273.10(e)(1)(ii)(A) half-up rounding into snap_net_income so the eligibility test, FPG ratio, and expected contribution all read the same rounded figure; the expected contribution keeps only its 273.10(e)(2)(ii)(A)(1) round-up. - Compare BBCE net income against the same rounded-up whole-dollar standard as the federal net test in meets_tanf_non_cash_net_income_test; the previous raw ratio comparison denied households sitting exactly at the published standard once net income is rounded. - Update the stale CBPP integration pins (47 / 611) and the North Carolina yearly pin (72) hidden behind wide error margins, and tighten those margins. - Cite each rounding provision in its own variable, mark the net-$50 case as the float32 cents-guard regression test, pin the issuance table to a Wayback capture, note the state-option elections, and drop the changelog fragment's leading bullet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…into fix/snap-expected-contribution-rounding
|
Response to the review (commit 949546a, merged with main at 22597f4): C1 (blocking): Both stale pins fixed. A1 (partner gate): On record from the maintainer (@hua7450): the partner-contract edits were explicitly approved, and the API partner is informed — the partner's own rounding report is in fact what surfaced this issue and motivated this PR, so the changes here are the partner's requested behavior. A3 (rounding at the source) — taken into this PR rather than deferred. A2 / A5: The equivalence comment is now qualified ("for whole-dollar maximum allotments…"), and both rounding comments note that (e)(1)(ii)(A) and (e)(2)(ii)(A)(1) are the state options PolicyEngine applies for all states (states may instead elect TANF rounding under (e)(1)(ii)(B)). A4: Each provision is now cited in the variable that implements it — with one correction to the suggested fix: the A6: The issuance-table citation is pinned to the Wayback capture of the Missouri DSS mirror (with a note that the live URL is replaced each October) plus the official FNS FY26 COLA URL. A7: The S2 / S3: Changelog leading bullet dropped (and the sentence updated for the source-level rounding); both stale Rebase: Merged with current Deferred per the review's own triage (to be filed as follow-up issues): Hawaii FY2026 minimum allotment derivation base ($40 vs $41), whole-dollar allotments in uprated projected years, the (e)(2)(iii) zero-benefit denial tracking issue, GU/VI poverty guidelines, the additional coverage cases (A8–A10, A12, S9), and the test-file convention cleanups (A13). 🤖 Generated with Claude Code |
Summary
Fixes the rounding in
snap_expected_contribution, which previously computedfloor(net_income) * 0.3with no rounding of the product.Fixes #9312
Legal basis
np.floor(x + 0.5)rather thannp.round's half-to-even).np.ceilso float32 noise (e.g.50 * 0.3 = 15.0000009) cannot push an exact dollar amount up to the next dollar.Impact
Benefits shift weakly downward only (
EC_new >= EC_oldalways): at most ~$1.10/month (up to ~$0.99 from rounding 30% up, plus up to ~$0.30 when net income rounds upward), i.e. $1–$13 per year on annual totals.Tests
snap_normal_allotment_basis_of_issuance.yaml(13 cases): 3-person allotments at net-income boundaries validated against the FNS Basis of Coupon/EBT Issuance table (Oct 1, 2025), including the net income $3.49 vs $3.50 half-up boundary and the exact-multiple $50/$51 rows.snap_categorical_eligibility_ceiling.yaml(3 cases): categorically eligible units above the issuance-table maximum (2-person gets the minimum allotment, 3-person gets $0 but stays flagged eligible per 7 CFR 273.10(e)(2)(iii) not converting to denial in the model).230.40 → 230) — artifacts of the missing rounding.Partner contract tests (updated with approval)
64 SNAP pins under
tests/policy/baseline/partners/analytics_coverage/shift by $1–$13/year under the corrected rounding and were updated with Ziming's approval:round net half-up → 30% rounded up), e.g.EC 337.20 → ceil(1,124 × 0.3) = 338.The change responds directly to the API partner's own rounding report, which serves as partner notice.
Test plan
🤖 Generated with Claude Code