Skip to content

Assign SNAP take-up from reported receipt and the FNS participation rate#294

Open
daphnehanse11 wants to merge 1 commit into
mainfrom
snap-take-up-stage
Open

Assign SNAP take-up from reported receipt and the FNS participation rate#294
daphnehanse11 wants to merge 1 commit into
mainfrom
snap-take-up-stage

Conversation

@daphnehanse11

Copy link
Copy Markdown
Collaborator

Summary

Closes #243. Second half of the SNAP work-requirements input surface (#248), alongside #293 (hours).

The published dataset ships takes_up_snap_if_eligible constant True (or omits it — same engine default), so PolicyEngine-US pays SNAP to 100% of eligible units. FNS measures participation among the eligible at ~82%. Universal take-up misstates who receives SNAP and overstates the reach of any eligibility-side reform.

This adds a snap_take_up source stage following the #266/#293 template: manifest entry + shared runtime handler + frame transform + release gate + builder wiring.

Semantics (identical to the retired enhanced-CPS pipeline)

  1. Reported recipients always take up — SPM units with positive raw ASEC SPM_SNAPSUB reported receiving SNAP; survey measurement wins unconditionally.
  2. Non-reporters fill to the published rate — seeded draws grant take-up at exactly the fill rate needed for the overall weighted share to land on the manifest's cited FNS rate (0.82). When reporters alone exceed the rate, nothing is forced — the share is emergent.
  3. Eligibility stays the engine's job — the flag is assigned across all units and PE-US intersects it with modeled eligibility, matching the retired pipeline.

Draws are blake2b hashes keyed by stable source identity (source_year/source_household_id/min source_person_id), so support-channel clones of one source unit always agree and reruns are bit-reproducible. The rate lives in the manifest with its FNS citation — a rate without a citation refuses to run.

Healing + gate

  • The frame transform is idempotent on a signal-carrying column and recomputes when the column is constant — the published all-True landmine is repaired, not trusted.
  • Release gate (snap_take_up_signal): column nonconstant, every reporting unit takes up (anchor preserved), weighted share within [0.70, 0.95]. Threaded through release gate failures, calibration diagnostics, and both manifests, mirroring the immigration and hours gates.

Verification

  • 18 new tests (manifest declaration, anchor/fill/overflow semantics, seed stability, clone consistency, citation enforcement, healing path, gate failure modes); full populace-build suite green.
  • End-to-end against the current 233,716-SPM-unit base: take-up share 0.821 (target 0.82), reported share 0.107 (CPS underreporting is exactly why the fill exists), zero reporters denied, gate green.

Follow-up (not in this PR)

State-calibrated take-up (greedy fill against FNS state household counts, the calibrate_binary_assignment machinery) is deliberately deferred: FNS state counts are average-monthly while the flag is annual-ever, and that bridge deserves its own reviewed decision — see the discussion on #292. National-rate + reported-anchor matches the retired pipeline's published behavior.

Merge-order notes

🤖 Generated with Claude Code

MaxGhenis added a commit that referenced this pull request Jul 5, 2026
The two data-seeded take-up programs whose participation rate clears the
provenance bar, assigned by calibrated Bernoulli at the administrative rate:

- TANF (takes_up_tanf_if_eligible, spm_unit): 21.9% (HHS ASPE 24th Welfare
  Indicators Report, 2022, Table 10 Indicator 4).
- EITC (takes_up_eitc, tax_unit): per-child rates (IRS National Taxpayer
  Advocate 2020, TY2016: 0=65%, 1=86%, 2=85%, 3+=82%). Child count is
  approximated from tax-unit member ages under 19; the rate is nearly flat
  above zero children so the coarse count picks the right bin.

with_us_take_up_inputs seeds every program the take-up contract marks `seed`
(and only those), keying draws on stable source identity so support-channel
clones agree and reruns are bit-reproducible (the SNAP #294 keying). A frame
already carrying a non-constant column passes through untouched; a missing or
constant column is recomputed, so the published all-True landmine is healed.
No reported-receipt column is threaded through the base spine, so assignment is
calibrated Bernoulli rather than the SNAP reported-receipt anchor.

us_take_up_summary and us_take_up_signal_gate expose the per-program
participation-vs-administrative surface: weighted share, target rate, source,
and a plausibility band, failing on a missing/constant column or an
out-of-band share.

Validated at n=8000: TANF share 0.210 (target 0.219); EITC per-bin
0.648/0.866/0.842/0.832 (targets 0.65/0.86/0.85/0.82); EITC overall 0.797
(IRS ~0.78). 17 seeding tests plus the gate failure modes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Jul 5, 2026
… take-up contract (#312) (#315)

* Add engine-asserted take-up contract inventory (#312 step 1)

The blocking first deliverable: a checked-in table classifying every
policyengine-us take-up flag, asserted against the installed engine.

- PolicyEngineUSEngine.take_up_variables() / take_up_contract() derive, from
  engine metadata, each takes_up_* flag's entity, default, whether the engine
  computes it (formula / adds / start-date formula), its consumers, and thus an
  engine_class (model_simulated / data_seeded / dead). Same metadata-derivation
  doctrine as the #301 formula-owned guard.
- us/take_up_contract.json records the engine facts plus the curated populace
  treatment (seed / rate_unsourced / model_simulated / out_of_scope /
  near_universal) with each rate's administrative provenance.
- assert_take_up_contract_current() fails the build when the table drifts from
  the pinned engine (new/renamed flag, a flag gaining a formula, a changed
  default); assert_take_up_treatments_consistent() catches treatments that
  contradict the engine class. The loader enforces the provenance rule: a
  program marked seed must carry a sourced administrative rate.

Finding against pinned policyengine-us 1.752.2: all 13 take-up flags are
data_seeded (default True, no formula, all consumed); there are no *_seed
variables and no model-simulated take-up. The chip_take_up_seed / aca_take_up_seed
model-side migration the issue references has not landed in the pinned engine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Seed TANF and EITC take-up flags from administrative rates (#312 step 2)

The two data-seeded take-up programs whose participation rate clears the
provenance bar, assigned by calibrated Bernoulli at the administrative rate:

- TANF (takes_up_tanf_if_eligible, spm_unit): 21.9% (HHS ASPE 24th Welfare
  Indicators Report, 2022, Table 10 Indicator 4).
- EITC (takes_up_eitc, tax_unit): per-child rates (IRS National Taxpayer
  Advocate 2020, TY2016: 0=65%, 1=86%, 2=85%, 3+=82%). Child count is
  approximated from tax-unit member ages under 19; the rate is nearly flat
  above zero children so the coarse count picks the right bin.

with_us_take_up_inputs seeds every program the take-up contract marks `seed`
(and only those), keying draws on stable source identity so support-channel
clones agree and reruns are bit-reproducible (the SNAP #294 keying). A frame
already carrying a non-constant column passes through untouched; a missing or
constant column is recomputed, so the published all-True landmine is healed.
No reported-receipt column is threaded through the base spine, so assignment is
calibrated Bernoulli rather than the SNAP reported-receipt anchor.

us_take_up_summary and us_take_up_signal_gate expose the per-program
participation-vs-administrative surface: weighted share, target rate, source,
and a plausibility band, failing on a missing/constant column or an
out-of-band share.

Validated at n=8000: TANF share 0.210 (target 0.219); EITC per-bin
0.648/0.866/0.842/0.832 (targets 0.65/0.86/0.85/0.82); EITC overall 0.797
(IRS ~0.78). 17 seeding tests plus the gate failure modes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* WIP checkpoint: agent stopped mid-task; lead salvaged state

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* WIP checkpoint: agent killed (session limit / stop); lead salvaged

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Register take-up contract resource; fake take-up stages in the builder test; diagnostics test class

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Sort imports (ruff --fix)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Add a snap_take_up source stage (manifest entry, runtime handler, frame
transform, release gate, builder wiring) following the immigration and
hours templates. Reported SPM_SNAPSUB recipients always take up;
non-reporters fill via seeded stable-identity draws to the cited FNS
0.82 participation rate. Heals the published all-True landmine.
Closes #243; second half of #248 alongside #293.
@daphnehanse11

Copy link
Copy Markdown
Collaborator Author

Rebased onto current main (8b1795a, was 14 behind) and squashed the two commits, mirroring the #293 rebase.

Conflict resolutions were the same 11 additive anchor points as #293 — main's TANF/EITC take-up (#315) and eCPS-parity (#316) wiring interleaved with this PR's SNAP wiring. In main() the TANF/EITC seeding + gate now run first, then SNAP take-up + gate; snap_take_up_gate threads through _release_gate_failures/_build_manifests alongside degenerate_input_gate/ecps_parity_gate.

Intended changes beyond conflict resolution:

  • Dropped the stale takes_up_snap_if_eligible reviewed exclusion from US_DEGENERATE_INPUT_REVIEWED_EXCLUSIONS — the merge-order follow-up this PR's description flagged (Gate releases on input columns stuck at the engine default #286 merged first; this stage makes the column non-degenerate, and the cannot-rot gate fails on a listed column that carries signal).
  • Reconciled the Seed TANF and EITC take-up from administrative rates; engine-asserted take-up contract (#312) #315 take-up contract inventory: SNAP stays populace_treatment: "out_of_scope" (flipping it to seed would make with_us_take_up_inputs double-seed it), but the scope_owner/notes now say the stage has landed rather than "in-flight", and clarify that out_of_scope means out of scope for the Bernoulli module, not unseeded. Same doc fix in take_up.py's module docstring.
  • One incidental ruff format hunk in take_up.py (pre-existing three-line ternary collapsed to one).

Verified the rebased net diff against the original with interdiff — no other semantic drift. Full populace-build suite green locally; ruff check + format clean.

Note for merge order with #293: both PRs insert at the same manifest/builder anchor points; whichever merges second gets the same-shape trivial conflict both descriptions anticipate.

🤖 Generated with Claude Code

@daphnehanse11

Copy link
Copy Markdown
Collaborator Author

Heads-up from the medicaid take-up workstream (#334/#337): once #337 lands, parity_gate fails on stale known-gap exemptions — any register entry whose layer the candidate now populates. This branch's SNAP stage will make takes_up_snap_if_eligible exactly that: the entry still sits in ecps_parity_known_gaps.json with reason "Populace does not yet produce it", so the first release build off a rebased #294 will fail the parity gate until that entry is deleted. When rebasing, (1) remove the takes_up_snap_if_eligible entry from the register, and (2) note the take-up contract landed in #315 marks SNAP out_of_scope pointing at this PR — the contract entry's treatment/notes should be updated to reflect the stage actually landing (same pattern #334 used for Medicaid: the stage owns the flag, the contract records how). Also expect small rebase friction in us_runtime/__init__.py and the builder wiring where #334 added its stage alongside where this PR adds SNAP's.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Carry SNAP reported amounts and take-up inputs through Populace US outputs

1 participant