Skip to content

Repair IRA and employee-plan participation with joint SCF donors - #1203

Draft
MaxGhenis wants to merge 8 commits into
mainfrom
fix/ira-joint-participation
Draft

MaxGhenis wants to merge 8 commits into
mainfrom
fix/ira-joint-participation

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Repair the data assignment that gave virtually every IRA saver an employer-plan contribution. The pinned artifact has 27,261 traditional IRA contributor records; 27,259 also have employer-plan or SE-pension contributions. With the correct §219(g) phase-out in PolicyEngine-US #10032, weighted 2025 gross traditional contributions are $15.400bn and deductions $5.819bn, a $9.581bn phase-out reduction. SOI deductible IRA payments are $13.166590bn (TY2022) and $13.771289bn (TY2023). These are different years and people versus tax returns; no double application of the phase-out was established.

This draft implements option A of d1244. Publication and readiness remain Max's decisions.

Source and method:

  • Federal Reserve SCF 2022 full public microdata and codebook observe the same respondent/spouse's new IRA/Keogh contributions (X6791–X6794) and employee workplace-plan contribution amounts and frequencies. Current SIPP's IRA contribution field is restricted to the main employer/business, making SCF the preferable source for personal IRA saving.
  • Draw a complete weighted donor contribution vector in age/earnings cells, retaining IRA-only, employee-plan-only, and both. Scale one complete vector to CPS RETCB_VAL; preserve observed zero contributions. Replace PUF contribution QRF imputation with complete donor vectors. CPS participation is conditional on reported saving: RETCB_VAL=0 stays zero. Scaling preserves the selected SCF tuple’s zero pattern and account proportions, not its absolute dollars or national IRA totals. Survey omissions remain; the resulting gross IRA levels and deduction could still worsen and require release review.
  • SCF IRA amounts reference 2021, whereas workplace amounts are annualized current 2022 contributions. Positive mixed IRA/Keogh owners are excluded because their pooled amount does not identify contributing account family. Within-IRA traditional/Roth amounts use observed account-balance shares, a declared proxy. Employer Roth amounts retain the existing 15% proxy. Public SCF workplace type 2 pools 401(k), 403(b), thrift and 457 plans; the existing 401(k) leaves represent this broader DC category, and 457-only coverage cannot be distinguished for §219(g). The hot deck approximates weighted source frequencies within age/earnings cells; CPS cell composition and unequal recipient weights affect national shares. Within-couple contribution correlation is not preserved. These limitations must be evaluated during release validation.
  • Inputs are factual contributions only. PolicyEngine-US owns limits, active participation, and deductions. Employer-only allocations and defined-benefit coverage (option A2) are deferred to separate work.

Invariants and tests:

  • Contributions are finite and nonnegative; no account vector is assembled from separate participation draws.
  • IRA-only, employer-plan-only, and both occur in property fixtures; weighted joint shares and contribution totals reconcile with weighted source tuples.
  • Income-compatible draws, zero-weight donor exclusion, deterministic identity-keyed draws, source validation, and CPS bundled-total reconciliation are covered.
  • Round 1 validation: 80 focused tests passed; Ruff lint and test-plan registration checks passed. These are prior-run results.
  • Round 2 local results: 582 passed and 7 setup errors. The six complete engine-free files passed: joint sampling 16; SCF extraction 14; retirement-stage integration 20; same-model validation/report safeguards 24; IRA benchmarks 8; and source-spine blindness 498. The selected multispine physical-dtype regression passed (1 passed, 63 deselected). The requested US lineage file finished with 1 passed and 7 setup errors (exit 1; 1,747 seconds wall time), all from the unchanged 300-second clean-worker import timeout, rather than the producer-count assertion. Files ran one at a time in the foreground. Ruff and CI test-plan registration checks passed. No whole suite, data build or microsimulation was run.
  • Code repairs: the audit helper's np.ndarray arguments let the unchanged fail-closed guard distinguish numeric selectors from source-column reads; its exact runtime import-graph pin is 76. At 5ca8277edf73f77d87ab3f31134abf17e3f8de53, retirement assignment preserves assembly metadata and mass history, emits optional draw_audit separately, and passes that evidence through maintained PoolStageOutput receipts. The selected pool regression passes with the metadata guard unchanged.
  • Replacing the PUF retirement-contribution QRF with joint donor vectors intentionally removes 11 inputs from source:with_us_retirement_contribution_inputs (54→43; graph total 2744→2733). The removed row names are person.@effective:employer_health_coverage, person.@effective:person_tax_unit_link, person.@effective:sex, person.@effective:social_security_income, person.@effective:tax_unit_role, tax_unit.@effective:filing_status, tax_unit.@effective:tax_unit_id, person.social_security_dependents, person.social_security_disability, person.social_security_retirement, and person.social_security_survivors. The invariant is (2733, 92, 227, 35, 0, 213); structural tax-unit identifiers remain enforced by source-wrapper inputs.
  • CI run 38146629082 at 5ca8277edf73f77d87ab3f31134abf17e3f8de53: both US-engine matrices passed all eight lineage tests, including the original producer-count failure, then failed the coverage snapshot (42,184 expected configuration fields; 42,216 observed). Both engine-free matrices likewise reached the stale exact field-usage snapshot after 14,529/14,403 tests passed. The remaining CI blocker requires canonical projection and reviewed snapshot refresh; the PR remains a draft.
  • Prior-round canonical generation was blocked locally. The initial attempt inherited a foreign Python 3.14 PYTHONPATH and hit its 25-minute outer deadline. The corrected isolated .venv/bin/python -u tools/generate_us_bundle_from_constants.py exited 1 after 698 seconds because the clean-worker import of microcosm.build.us_runtime.puf_qrf_worker exceeded the unchanged 300-second timeout. No generated resources changed, no timeout or generated resource was hand-edited, and --check was not run.
  • Round 3 canonical regeneration (2026-10-11): BLOCKED. A fresh locked US environment was set up with uv sync --all-packages --locked --extra us; the documented uv run --no-sync python -u tools/generate_us_bundle_from_constants.py ran with inherited PYTHONPATH cleared and native-library thread limits set; the retry also cleared PYTHONHOME and PYTHONPYCACHEPREFIX. Host load fluctuated from about 20 to nearly 500 during these attempts. The first attempt was stopped after the 45-minute outer deadline with an empty log (exit 130). The single retry reached its 25-minute outer deadline (exit 143; 0 log bytes). The repository generator and its 300-second clean-worker timeout were unchanged. Canonical generation has not completed; generator --check, compiler projection/pin refresh, coverage generation/check, and the requested focused reruns remain outstanding. Round 3 newly run tests: 0.
  • Round 3 read-only field audit explains the observed 42,184→42,216 as net +32 terminal pointers: +9 authored and +23 resolved. This is a net inventory change with removals/replacements and ordered array shifts. Authored source-stage fields add 16 and remove 10 (net +6): added SCF asset/member expected_rows, member, member_sha256, sha256, size_bytes (5); workplace-Roth provenance artifact format, kind, vintage (3); CPS draw donor_table, seed_from_build_config, three age_breaks (5); three PUF age_breaks (3). Removed three fixed account shares (dc_share_of_remainder, se_pension_share, traditional_ira_share) and seven QRF terminals (max_train_samples, n_estimators, predictors/3 through predictors/7). Authored seed ownership adds one net binding (site, owners/0/kind, owners/0/id; +3).
  • Resolved fields add two direct-kernel module strings (microcosm.build.us_runtime.joint_retirement and microcosm.build.us_runtime.scf_retirement; +2), replace the 14-terminal retirement_contributions_training_cap with two 16-terminal CPS/PUF joint-draw definitions (+18), and add the same net three-leaf seed ownership binding (+3). Each joint definition names id, stream, value_source, default, rng_family, rng_version, kernel, seed_material/0, five consumption_order steps, reset_boundary, draw_condition, and derivation; these attest complete-vector sampling and its deterministic consumption order. Pre-regeneration counts are 32,413 authored + 9,803 resolved = 42,216; the branch already has 54 seed sites, 54 owner rows and 113 owner bindings. Canonical regeneration also removes the 11 obsolete producer input rows (2,744→2,733), so final field/count/hash pins must be observed from the maintained compiler after generation.
  • Generation handoff: run tools/generate_us_bundle_from_constants.py, then --check, in a working locked US environment. It emits packages/microcosm-build/src/microcosm/build/us/spec/{battery,bundle,calibration,catalogs,geography,imputation,publication,selection,sources,spine,take_up,vintages}.yaml, packages/microcosm-build/src/microcosm/build/us/country_package.json, and packages/microcosm-build/src/microcosm/build/us/engine_abi.lock.json. Regenerate the retirement input/resource/seed declarations, derive legacy schedule/resource projections through the maintained compiler, and refresh reviewed schedule/payload, graph-node, resource/checkpoint, source-manifest, seed-map, input-count and field-usage pointer/count pins from canonical observations. Preserve the frozen source_stages.json, support_spine.json, and take_up_contract.json assets; canonical generation checks their compatibility projections. Regenerate the existing repository coverage projection with tools/spec_engine_coverage.py, then its --check, retaining operational-field stripping for resource/checkpoint hashes. Session reports/evidence stay untracked.

Validation:

  • Official Form 5498/SOI accumulation workbooks independently rechecked: TY2022 traditional 5,900,726 taxpayers / $25.807585bn; Roth 9,170,792 / $30.584013bn. TY2023 traditional 5,873,053 / $26.901749bn; Roth 9,488,414 / $32.809393bn. These correct the research report's unchecked Table 5/6 totals and coordinates (B7/C7, not B8/C8).
  • Sources: 2022 traditional, 2022 Roth, 2023 traditional, 2023 Roth; deductions 2022, deductions 2023. Workbook amounts are thousands of dollars.
  • Verified final SCF donor extract: 10,395 positive person-implicates (590 excluded); weighted IRA-only 10.429M, employer/SE-only 40.215M, both 6.491M. Joint saver shares 18.254% / 70.385% / 11.361%; IRA contributor employee/SE overlap 38.362%. Donor traditional IRA 6.827M / $31.526bn; Roth 12.508M / $43.602bn. These source/proxy totals are not rebuilt-population validation.
  • Rebuilt population contributors, amounts, active-participant share, and model deduction after the repair remain unmeasured. Unit/source checks cannot establish the resulting national deduction.

Data release needs:

Run a fresh configured Route A build with a fresh RUN_ROOT (old accepted stages/checkpoints can bypass changed retirement logic): ROUTE_A_ENV=<settings-file> bash tools/route_a/route_a.sh --resolve, then ROUTE_A_ENV=<settings-file> ~/reviews/us-hub/scripts/heavy_run.sh ira-participation-release bash tools/route_a/route_a.sh with the same operator settings. The raw-source base stage alone is documented at roughly 46 minutes / 72GB, beyond this sandbox job. Use a reviewed PolicyEngine-US version containing #10032; the existing lock predates the phase-out fix. Validate before/after with the same model, gross counts/amounts against accumulation tables, deductions separately against SOI payments, and the share of traditional IRA contributors who are active participants. The new tools/validate_us_ira_participation.py runs this same-engine before/after scoring through the maintained household-batched scorer and records hashes, gross contributor counts/amounts, traditional-contributor active share, and filer-restricted parent tax-unit deductions separately from dependent own-return deductions. It requires unaged base-2024 H5 inputs and refuses stored capped contributions, active-participation results or deductions that could override the model. Run it under the heavy lock after the build: ~/reviews/us-hub/scripts/heavy_run.sh ira-participation-validation uv run --no-sync python tools/validate_us_ira_participation.py --before-h5 <incumbent.h5> --after-h5 <candidate.h5> --year 2025 --out <untracked-evidence/ira-validation.json>. Recalibrate/review the broad JCT IRA anchor and general AGI/tax constraints. Do not publish or promote from this PR.

This branch has not been deployed

No deployments
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.

1 participant