From b1b2561fb1c3fd568b28311a86bdfede0dca8a61 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 16:32:00 -0400 Subject: [PATCH 1/6] Replay A-COLA stage 1 (Registration 18, d658) onto master Replays 9de858d0eaf14df7affd5d4e810568a6d41584e9 ("Build the A-COLA trust fund test, stage 1 (Registration 18, d658)"), whose parent 75cd3524 was 510 commits behind master, onto origin/master 2195258c with `git cherry-pick 9de858d0`. Conflicts and how they were resolved: - tests/tier_counts.json and tests/README-tiers.md: kept master's versions. Stage 1's counts were taken on its old base. The tiers are recounted at the end of this branch, after stage 2 adds its tests. Until then the tier-manifest check fails on this commit alone. The other 24 files apply unchanged. They are byte-identical to 9de858d0, and the stage-2 builder found no bug in them. Co-Authored-By: Claude Opus 5.5 --- .../tr2026_v_c1_vintage1/capture_manifest.txt | 2 + .../tr2026_v_c1_vintage1/capture_record.json | 30 + .../tr2026_v_c1_vintage1/cola_timing_note.txt | 1 + .../timing_note_record.json | 12 + .../trustees2026_v_c1_table.html | 2605 +++++++++++++++++ docs/design/acola_registration18.md | 241 ++ docs/design/acola_spec_only_nulls.json | 272 ++ scripts/build_acola_spec_only_nulls.py | 31 + scripts/capture_tr2026_v_c1.py | 323 ++ scripts/run_acola_registered.py | 269 ++ src/populace_dynamics/acola_track/__init__.py | 1 + src/populace_dynamics/acola_track/config.py | 330 +++ src/populace_dynamics/acola_track/nulls.py | 456 +++ src/populace_dynamics/acola_track/runner.py | 447 +++ .../acola_track/statistics.py | 601 ++++ tests/acola_track/__init__.py | 1 + tests/acola_track/test_capture.py | 64 + tests/acola_track/test_config.py | 182 ++ tests/acola_track/test_nulls.py | 188 ++ .../acola_track/test_registered_run_script.py | 202 ++ tests/acola_track/test_runner.py | 411 +++ tests/acola_track/test_statistics.py | 770 +++++ tests/acola_track/test_timing_properties.py | 215 ++ tests/acola_track/test_unchanged.py | 41 + 24 files changed, 7695 insertions(+) create mode 100644 data/external/snapshots/tr2026_v_c1_vintage1/capture_manifest.txt create mode 100644 data/external/snapshots/tr2026_v_c1_vintage1/capture_record.json create mode 100644 data/external/snapshots/tr2026_v_c1_vintage1/cola_timing_note.txt create mode 100644 data/external/snapshots/tr2026_v_c1_vintage1/timing_note_record.json create mode 100644 data/external/snapshots/tr2026_v_c1_vintage1/trustees2026_v_c1_table.html create mode 100644 docs/design/acola_registration18.md create mode 100644 docs/design/acola_spec_only_nulls.json create mode 100644 scripts/build_acola_spec_only_nulls.py create mode 100644 scripts/capture_tr2026_v_c1.py create mode 100644 scripts/run_acola_registered.py create mode 100644 src/populace_dynamics/acola_track/__init__.py create mode 100644 src/populace_dynamics/acola_track/config.py create mode 100644 src/populace_dynamics/acola_track/nulls.py create mode 100644 src/populace_dynamics/acola_track/runner.py create mode 100644 src/populace_dynamics/acola_track/statistics.py create mode 100644 tests/acola_track/__init__.py create mode 100644 tests/acola_track/test_capture.py create mode 100644 tests/acola_track/test_config.py create mode 100644 tests/acola_track/test_nulls.py create mode 100644 tests/acola_track/test_registered_run_script.py create mode 100644 tests/acola_track/test_runner.py create mode 100644 tests/acola_track/test_statistics.py create mode 100644 tests/acola_track/test_timing_properties.py create mode 100644 tests/acola_track/test_unchanged.py diff --git a/data/external/snapshots/tr2026_v_c1_vintage1/capture_manifest.txt b/data/external/snapshots/tr2026_v_c1_vintage1/capture_manifest.txt new file mode 100644 index 000000000..80d14dcdb --- /dev/null +++ b/data/external/snapshots/tr2026_v_c1_vintage1/capture_manifest.txt @@ -0,0 +1,2 @@ +2026-10-01T15:01:44Z 4d29f2b85d67b995eea25f0bdfb46b3755671c082d75a9855680836d4b80c46a 438787 trustees2026_v_c1_table.html +2026-10-01T15:49:46Z f63274d3ef3b06c1647326490945ed05c01421466f9b0cca891bd1d0a511341b 107 cola_timing_note.txt diff --git a/data/external/snapshots/tr2026_v_c1_vintage1/capture_record.json b/data/external/snapshots/tr2026_v_c1_vintage1/capture_record.json new file mode 100644 index 000000000..f3a9b48eb --- /dev/null +++ b/data/external/snapshots/tr2026_v_c1_vintage1/capture_record.json @@ -0,0 +1,30 @@ +{ + "schema_version": "tr2026_v_c1_capture.v1", + "table": "Table V.C1", + "caption": "Table V.C1.\u2014Cost-of-Living Benefit Increases, Average Wage Index, Contribution and Benefit Bases, and Retirement Earnings Test Exempt Amounts, 1975-2035", + "source_url": "https://www.ssa.gov/oact/TR/2026/V_C_prog.html", + "located_from": { + "list_of_tables_url": "https://www.ssa.gov/oact/TR/2026/X1_trLOT.html", + "href": "V_C_prog.html#1047210", + "how": "the 2026 Report's table of contents (https://www.ssa.gov/oact/TR/2026/trTOC.html) links no table; it links the List of Tables, whose one entry naming V.C1 links this anchor on the section V.C chapter page" + }, + "retrieved_utc": "2026-10-01T15:01:44Z", + "fetch_mode": "browser-h2", + "fetch_method": "curl -sS -L; on a status other than 200, curl --http2 --compressed with Chrome 129 request headers (gzip, deflate); the decoded body is kept in memory only", + "http_status": 200, + "page": { + "stored": false, + "why_not_stored": "Table V.C1 has no page of its own; the chapter page holds 26 tables and the chapter prose, and an A-COLA builder may read no Trustees Report page or table that prints OASDI cost, cost rates, income rates or balances (EV/RESTRICTED-FILES.md, 2026-10-01 02:40)", + "sha256": "15cf3836ac11fcb0706075c3cdc8cfaf88a87c1dffa75cd277699a9d57c98f0b", + "bytes": 2749014, + "note": "the page's bytes vary between requests outside the tables, so a later fetch may not reproduce this digest" + }, + "fragment": { + "file": "trustees2026_v_c1_table.html", + "sha256": "4d29f2b85d67b995eea25f0bdfb46b3755671c082d75a9855680836d4b80c46a", + "bytes": 438787, + "byte_offset_start": 15514, + "byte_offset_end": 454301, + "definition": "page[byte_offset_start:byte_offset_end], the outermost element whose
equals the caption above (whitespace and no-break spaces normalized for the match)" + } +} diff --git a/data/external/snapshots/tr2026_v_c1_vintage1/cola_timing_note.txt b/data/external/snapshots/tr2026_v_c1_vintage1/cola_timing_note.txt new file mode 100644 index 000000000..f8f71f866 --- /dev/null +++ b/data/external/snapshots/tr2026_v_c1_vintage1/cola_timing_note.txt @@ -0,0 +1 @@ +a Effective with benefits payable for June in each year 1975-82, and for December in each year after 1982. diff --git a/data/external/snapshots/tr2026_v_c1_vintage1/timing_note_record.json b/data/external/snapshots/tr2026_v_c1_vintage1/timing_note_record.json new file mode 100644 index 000000000..0ca1bda5d --- /dev/null +++ b/data/external/snapshots/tr2026_v_c1_vintage1/timing_note_record.json @@ -0,0 +1,12 @@ +{ + "source_url": "https://www.ssa.gov/oact/TR/2026/V_C_prog.html", + "anchor": "12", + "linked_from": "Cost-of-living benefit increase, footnote a", + "retrieved_utc": "2026-10-01T15:49:46Z", + "fetch_mode": "browser-h2", + "sha256": "f63274d3ef3b06c1647326490945ed05c01421466f9b0cca891bd1d0a511341b", + "bytes": 107, + "source_page_sha256": "6634c30c98482e0ae75035511e0c04e5092e50b48d8656eddbba2aa1ed4d284c", + "page_stored": false, + "text": "a Effective with benefits payable for June in each year 1975-82, and for December in each year after 1982." +} diff --git a/data/external/snapshots/tr2026_v_c1_vintage1/trustees2026_v_c1_table.html b/data/external/snapshots/tr2026_v_c1_vintage1/trustees2026_v_c1_table.html new file mode 100644 index 000000000..76faedc2c --- /dev/null +++ b/data/external/snapshots/tr2026_v_c1_vintage1/trustees2026_v_c1_table.html @@ -0,0 +1,2605 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
Cost-of-living
benefit
increasea
(percent)
+
+
Average
wage index (AWI)  b
+
+

Contribution
and benefit
base c
+
+ +
+ + + + +
Under
NRAd
+
+
At NRAe
+
+ +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + +
f 2.5
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ +
+ + + + +
g2.8
+
+ + + + +
g$176,100
+
+
g$23,400
+
+
g$62,160
+
+ + + + + + + + + + +
g184,500
+
+
g24,480
+
+
g65,160
+
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ +
+ + + + +
g2.8
+
+ + + + +
g176,100
+
+
g23,400
+
+
g62,160
+
+ + + + + + + + + + +
g184,500
+
+
g24,480
+
+
g65,160
+
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ +
+ + + + +
g2.8
+
+ + + + +
g176,100
+
+
g23,400
+
+
g62,160
+
+ + + + + + + + + + +
g184,500
+
+
g24,480
+
+
g65,160
+
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/docs/design/acola_registration18.md b/docs/design/acola_registration18.md new file mode 100644 index 000000000..084c98d69 --- /dev/null +++ b/docs/design/acola_registration18.md @@ -0,0 +1,241 @@ +**Registration 18: trust fund test A-COLA. The 2030 cost effect of OACT provision A1 ("Starting December 2027, reduce the annual COLA by 1 percentage point") against OACT and PWBM, with MINT5 as a third comparator. One-shot, report-only, blind by procedure, not gated.** + +This comment registers the test before any build code for it exists or merges. Max authorized it on 2026-09-30 (decision d658, ruled 12:38): "Yes: register today a report-only blind test of OACT provision A1 (COLA -1pt from Dec 2027), 2030 cost effect, vs OACT and PWBM, with MINT5 as third comparator on the COLA row; report our ratio of means and a median beside MINT's median; FRA-68 rows secondary later. Build + independent review + post the #42 registration today." The artifact publishes regardless of outcome. + +This is the first of two registration comments. This one fixes the rows, the statistics, the comparators and the rules. After the build merges, a second comment posts the registered commit's full SHA, and the run waits for that comment. + +OACT is SSA's Office of the Chief Actuary. PWBM is the Penn Wharton Budget Model. MINT is SSA's Modeling Income in the Near Term model, and MINT5 is its fifth version. COLA means cost-of-living adjustment. FRA means full retirement age (OACT's texts say NRA, normal retirement age). OASI and DI are the Old-Age and Survivors Insurance and Disability Insurance trust funds; OASDI is both together. TR means the Social Security Trustees Report. PIA means primary insurance amount; AIME means average indexed monthly earnings. PSID is the Panel Study of Income Dynamics. SD means standard deviation. `EV` is the evidence folder. `DM` is the design memo `EV/trust-fund-comparisons-20260929/design.md` (SHA-256 `cbea98315857a6ae923c5ce1712c16f137cb45ff424dddd5bcfa8552527cefd1`). `LOC` is the locator file `EV/trust-fund-registration-20260930/locators.md` (SHA-256 `a5db50bcc7194820370def68022e96df283142eb3d187b2606a67eba30b5f3bc`). `S` is `EV/trust-fund-comparisons-20260929/sources`. Code citations refer to `origin/master` at `dd222b9af926c773a0f92732e2d08be507793378`, with paths under `src/populace_dynamics/` unless a path says otherwise. + +**What the test checks.** It checks the model's near-term cost mechanics on a closed PSID cohort: which 2030 beneficiaries carry how many reduced COLAs, weighted by their benefit dollars. It does not estimate a 75-year actuarial balance or a depletion date for any reform (DM:443). Arithmetic on the provision text alone gets most of the answer (DM:287). A match is therefore weak evidence, and the spec-only null below appears beside every comparison. + +### Rows and effective dates + +| Sub-row | Reform | First reduced COLA | Model fields | Year | Comparators | +|---|---|---|---|---|---| +| A-COLA (cost) | OACT A1, 2026 TR: "Starting December 2027, reduce the annual COLA by 1 percentage point" (DM:250; LOC 2.1) | the increase determined in 2027, effective December 2027, first paid January 2028 | `first_reduced_determination_year = 2027`, `annual_reduction = 0.01` (`COLAReform`, `scenario_benefits.py:394-407`; December timing per 42 USC 415(i)(2)(A)(ii), `scenario_benefits.py:20-24`) | calendar 2030 | OACT, PWBM | +| A-COLA/MINT5 (benefits by age) | MINT5's one-point COLA cut, from the start its index lists as "Start 2011" (LOC §1) | the increase determined in 2011, effective December 2011 (alternative M1: determined in 2010) | the same fields, with 2011 (M1: 2010) | calendar 2030 | MINT5 | + +- **Payment timing.** Under the model's annual convention, the amount paid in year t reflects the increases determined through t − 1 (`scenario_benefits.py:66-71`). Calendar-2030 payments therefore carry the reduced increases determined in 2027, 2028 and 2029 on the cost row. On the MINT5 sub-row they carry those determined in 2011 through 2029 (2010 through 2029 on M1). +- **MINT5 runs its own reform.** MINT5 scored a one-point cut from 2011, not A1, so the MINT5 sub-row runs MINT's start date and compares like with like. The MINT5 sub-row is not a comparison of A1 with MINT. +- **FRA-68 rows come later.** OACT C1.1 ("Starting with those age 62 in 2027, increase NRA 1 month every 2 years until NRA reaches 68", run098) and C1.2 ("Increase NRA 2 months per year for those age 62 in 2027-2032", run099) are not registered here (DM:251-252). A separate, later comment on this issue registers them before any FRA build code merges. That comment will name their PWBM and MINT comparators, any CBO row, and one reading of C1.1's "1 month every 2 years" for the 2027–2030 cohorts (DM:256). This comment fixes nothing about the FRA rows. Their sources still go on the restricted list now (LOC 5.1). +- **Excluded comparators.** CBO has no one-point COLA option (DM:265), so CBO does not enter this registration. Cato is not a comparator (Max, noted on d658). + +### Benefit mechanism + +- **Primary: mechanism L.** L is Track A v1's legacy calculators (`cola_track_a`), and it is the only mechanism with committed real-data runs (DM:244). At `dd222b9`, `runs/` holds the exercise-1 and exercise-3 artifacts (`runs/replication_urban2010_cola_v1.json`, `runs/replication_urban2010_fra68_v1.json`) and no Track A v2 artifact. In Track A v2's terms, L computes DI levels with `legacy_fixed_35` (`track_a_v2/benefits.py:86-94`). Every AIME divides by 35 years (`cola_track_a/benefits.py:68-78`). The DI level is the disclosed oracle approximation of ruling d074 (`cola_track_a/config.py:279`). +- **Secondary: mechanism D.** D is v2's statutory DI computation (`statutory_415_b_2`, `track_a_v2/benefits.py:90-94`), with every other field of the primary cost row. D runs once Track A v2's own real-data run artifact is committed to master. It runs at a commit that a further comment on this issue registers before D runs, and the same no-drop rule covers it. If D runs after the primary comparison is posted, its output carries the label "registered before the comparator was public, run after it". + +### Population, draws and conventions (both sub-rows unless stated) + +- **Population.** R0's population: the PSID 2011 anchor wave, opening year 2010, born 1980 or earlier, weighted by the 2011-wave weight (`cola_track_a/config.py:128-167`). The cohort is closed. No births are drawn, everyone aged 50 or older in 2030 was alive in 2010 (`cola_track_a/adapters.py:19-22`), and no post-2010 earnings are drawn (`cola_track_a/adapters.py:25-27`). +- **Draws.** K = 20, with root seeds 5200 + k (`cola_track_a/config.py:259, 401`). Within a draw, the baseline and the reform are computed from the same projected state, with fixed paths (`cola_track_a/benefits.py:3-6`). +- **Exposure clock.** The eligibility clock: the year the worker attains 62, the year the period of disability began, or the year of death before eligibility. Auxiliaries follow the worker's clock (`cola_track_a/config.py:215`; `scenario_benefits.py:25-37, 205-222`). +- **Benefit period.** Calendar-year payments on the annual scale, 12 times the monthly amount (`cola_track_a/config.py:203, 216`; `cola_track_a/benefits.py:13-14`). +- **Components.** R0's five: retired worker, disabled worker, spouse, aged widow(er) and disabled widow(er) (`estimates/cola_age_profile.py:271-278`). +- **Baseline COLA path.** The realized series runs through 2007. TR 2008 intermediate rates apply from 2008: Table V.C1 through 2017, then the ultimate CPI. The path extends to 2030 (`cola_track_a/runner.py:231-258`; `cola_track_a/config.py:265-268, 278, 380-385`). The path stops at 2030, so 2030 is the only year this test can read (DM:242). +- **Negative reformed increases.** The `REFUSE` policy stays (`scenario_benefits.py:238-244, 407`). A refusal is reported on this issue. +- **Opening stock.** The benefit basis is frozen at the opening year (d075; `cola_track_a/config.py:245-247`). +- **Labels.** Every output carries "PSID-seeded closed cohort", "Python oracle (not Axiom)" and "fixed-path mechanical incidence" (`cola_track_a/config.py:64-69`), plus "report-only". + +### Metric for the cost row + +For each draw and scenario s ∈ {base, reform}: + +T_s = Σ_{i ∈ S_s} w_i · b_{s,i} + +P = 100 × (T_reform − T_base) / T_base + +- **S_s** is every person alive in the 2030 projected state with a positive benefit in scenario s (the `positive_benefit` rule, `estimates/cola_age_profile.py:214-220`), at every age in the cohort. +- **b** is annual calendar-2030 payments summed over R0's five components. +- **w** is the person's R0 weight. +- **Equivalence.** P is the ratio of weighted totals. It uses the arithmetic of v2's `ratio_of_all_alive_weighted_totals` (`track_a_v2/estimands.py:107-133`), applied to a single all-ages cell. The new config computes P in its own module. +- **Reported value.** The run reports the mean of P over the 20 draws and the sample SD over draws (ddof = 1). It also reports the Monte Carlo floor, computed by the A7 convention (`estimates/cola_age_profile.py:96-120, 1651-1677`). For each seed 0–4, the opening-wave family units split into two disjoint halves. The reported value is recomputed on each half, and the floor summarizes |side a − side b| (mean, SD, minimum, maximum, count). An undefined floor is reported as undefined, never as zero. +- **Ratio of means.** Beside P, the run reports the weighted ratio of scenario means (`ratio_of_scenario_means`, `estimates/cola_age_profile.py:203-206`) on the same all-ages cell (d658: "report our ratio of means"). The two statistics coincide when S_base and S_reform are the same set. The run reports the count and weight of each set, so any difference shows. +- **OASI and DI split.** + - P_DI covers the disabled-worker component. P_OASI covers the other four: retired worker, spouse, aged widow(er) and disabled widow(er). + - Each is 100 × (ΔT) / T_base within its group, and the run reports each group's share of T_base. Per-component reform totals are new code (DM:240). + - The component rule places a spouse's benefit that rests on an entitled, unconverted disabled worker in OASI, although DI pays it in law (`cola_track_a/benefits.py:52-57`). The run reports the weighted count and the dollars of such spouse benefits, so the size of that misassignment shows. + - A comparator's split enters the comparison only where its sealed file prints 2030 OASI and DI cost separately. The lane records whether it does, without values. + +### Metric for the MINT5 sub-row + +- **Population.** MINT's Table 1 population is current-law beneficiaries aged 62 and older in 2030 (LOC 2.3). Ours is the members of S_base aged 62 or older in 2030. Age is 2030 − birth year, the code's default rule (`estimates/cola_age_profile.py:251-252, 440`). +- **Bands.** MINT's: Total (62 and older), 62–69, 70–79, 80–89 and 90+. The run re-tabulates the person-level rows into these bands. +- **Statistics.** The run computes each of the following per band and per draw, then reports the mean over the 20 draws, the draw SD and the floor, as for P. + 1. **Ratio of means** (d658): `ratio_of_scenario_means` on the band. + 2. **Median of individual percent changes.** + - Population: the persons in S_alt within the band, meaning people who receive a benefit in both scenarios with b_base > 0 (`estimates/cola_age_profile.py:232-234`). Each person's benefit is the sum of their components, which matches MINT's "individual benefits". + - Change: x_i = 100 × (b_reform,i / b_base,i − 1). + - Median: sort x ascending and let W = Σ w_i. The weighted median is the smallest x_(j) whose cumulative weight reaches W/2. If the cumulative weight at x_(j) equals W/2 exactly, the median is the midpoint of x_(j) and x_(j+1). + 3. **Difference of medians.** 100 × (m_reform / m_base − 1), where m_s is the weighted median of b_{s,i} over S_s within the band, by the same rule. +- **Which median is compared.** Before the seal closes, the lane records from MINT's column heads and notes, without values, whether "Median Difference vs Scheduled" is a median of individual differences or a difference of medians (LOC 2.3). The matching statistic is the headline comparison, and the other is reported beside it. If the notes do not settle the question, the run compares both and neither is the headline. The ratio of means appears beside either one. Because the run computes the median directly, this comment registers no mean-to-median bias direction. +- **Memo items.** + - Beside MINT's "% of Population Affected": our weighted share of the band's baseline beneficiaries whose benefit falls. + - Our weighted median baseline and reform annual benefits. These are not compared with MINT's dollar levels, because the dollar and benefit-period bases differ. + +### Registered rows + +Each alternative changes one field from its primary. The one shot computes every row, and every row is published. + +| ID | Sub-row | Field changed | Value | +|---|---|---|---| +| C0 | cost | none | primary, as above | +| C1 | cost | benefit period | the December 2030 monthly amount × 12 (`BenefitPeriod.DECEMBER`, which includes the increase effective December 2030; `scenario_benefits.py:247-255`) | +| C0-D | cost | mechanism | D (secondary; runs later, as above) | +| M0 | MINT5 | none | primary, with the first reduced increase determined in 2011 | +| M1 | MINT5 | first reduced increase | determined in 2010 | +| M2 | MINT5 | benefit period | the December 2030 monthly amount × 12 | +| M3 | MINT5 | exposure clock | entitlement (R2's field; LOC 2.3 (e)) | +| M4 | MINT5 | age rule | completed age at the start of 2030, 2030 − birth year − 1 (`estimates/cola_age_profile.py:247-257`) | + +- **Why C1.** C0 counts the increases determined through 2029, and C1 counts those through 2030. So C0 and C1 bracket either way a comparator might time its 2030 cost. The lane records whether each comparator counts December 2030 benefits at the December 2030 increase. +- **Why M1.** The index's "Start 2011" could mean the increase effective December 2011 (M0) or the first payments in 2011 (M1). This comment fixes M0 as primary now. The lane records its reading from MINT's notes, and a mismatch is reported, never used to swap rows. + +### Spec-only null + +N = 100 × [ Π_{k = 2027}^{2029} (1 + c_k − 0.01) / (1 + c_k) − 1 ] + +- **Inputs.** c_k is the TR 2026 intermediate COLA effective in December of year k. The null applies it to every 2030 beneficiary, as if everyone had become eligible before 2028. The memo's arithmetic puts N near a 3 percent cut (DM:287). +- **Locator for c_k.** + - Primary: the 2026 Trustees Report's Table V.C1, intermediate assumptions, the increases effective December 2027, 2028 and 2029. That table number is the one TR 2008's COLA path comes from (`cola_track_a/runner.py:253-256`). Its 2026 counterpart has not been checked this session. + - Fallback: if the table does not print those years, the builder uses the 2026 Report's intermediate annual CPI assumption for the same years (42 USC 415(i) ties the COLA to the CPI) and says so. + - The builder reads only that table, not the Report's cost tables. +- **N_model (memo item).** The same formula with the model's own TR 2008 rates for 2027–2029 (`cola_track_a/runner.py:231-258`). P − N_model is the model's composition content: the share of 2030 dollars held by people who became eligible in 2028–2030, and the DI and survivor COLA clocks. N − N_model is the COLA-vintage delta. +- **N_MINT (the MINT5 sub-row's null).** The same formula over k = 2011 through 2029 (M1: 2010 through 2029), with the model's TR 2008 rates, which are also MINT5's vintage. +- **When the nulls are posted.** All three nulls are arithmetic on the specification alone. The build computes them, and the second registration comment posts their values before the run. +- **What is reported.** For each comparator C, the run reports P − C and N − C, as intervals when C is an interval, plus P − N and P − N_model. The MINT5 sub-row reports the same with N_MINT. +- **Reading (DM:287).** Agreement would show that the engine's composition and COLA mechanics are consistent with the comparator's. It would not validate long-run dynamics. A miss larger than the null's would be informative, as either a bug or a composition error. + +### Year + +Calendar 2030 only (DM:242). + +### Comparators: locators, precision, interval rules and unit conversions + +**OACT A1, 2026 TR basis** (LOC 2.1) +- **Page.** `https://www.ssa.gov/oact/solvency/provisions/tables/table_run113.html` (PDF twin: `.../tables/table_run113.pdf`), in the current `provisions/` folder, where category A is on the 2026 TR basis. The comparator is not `provisions_tr2025/tables/table_run113.html`. In the 2025 folder, run113 is B1.3, because run numbers collide across folders (DM:197). +- **Footer vintage (required).** "Based on Intermediate Assumptions of the 2026 Trustees Report". If the footer differs at fetch time, the lane stops and reports. It does not substitute another page. The sealed copy is the comparator. +- **Text to match.** "Starting December 2027, reduce the annual COLA by 1 percentage point." +- **Cells.** Table 1, the single-year table, calendar-year row 2030. From the "Proposal" column, the cost rate p. From the "Change from current law" column, the cost rate d. The lane records the header strings verbatim. The proposal income rate and the change in income rate are memo items. +- **Unit.** Percent of current-law taxable payroll. +- **Published precision.** Two decimals. The format was checked on run115, a page of the same family (DM:268). The lane confirms the format on run113 without reading values. +- **Conversion.** The current-law cost rate is CL = p − d, and C_OACT = 100 × d / (p − d) (DM:261). Payroll cancels. +- **Interval rule.** p and d each carry ±0.005, so the derived CL carries ±0.01. The C interval runs from the minimum to the maximum of 100 · d / (p − d) over the four corners (p̂ ± 0.005, d̂ ± 0.005). The corners suffice because, while p − d > 0, C is monotone in d and, for fixed d, monotone in p. +- **Precision gate (DM:268).** If any OACT column that enters the statistic is smaller than 0.10 in magnitude, PWBM becomes primary and OACT report-only. Under d658 both are report-only already. The lane still applies the gate mechanically and records which branch applied. + +**PWBM `cola_offset_cut_1pp`** (LOC 2.2) +- **Pinned folder.** `https://pwbm-website-data.s3.us-east-1.amazonaws.com/social-security/2026-09-20-marikop-upload/` (DM:197). +- **Slug.** `cola_offset_cut_1pp` is A1: "Starting December 2027, reduce the annual COLA by 1 percentage point". Its source is table_run113 on the 2026 TR basis (`S/pwbm-social-security.md:30-31`, as LOC records). +- **File.** `{folder}/cola_offset_cut_1pp/law_change/law_change_brief/charts/cost_and_income_rate.csv`. On 2026-09-30, a HEAD request returned HTTP 200 with `Last-Modified: Tue, 22 Sep 2026 20:43:15 GMT`. The seal pins the bytes it fetches. A later change to the object is reported, not substituted. +- **Header.** `year,Baseline income rate,Baseline cost rate,A1 income rate,A1 cost rate`, preceded by a UTF-8 byte-order mark. +- **Cells.** Row `year` = 2030. B = `Baseline cost rate` and R = `A1 cost rate`. Both income-rate columns are memo items. +- **Unit.** Percent of PWBM's own baseline taxable payroll. +- **Conversion.** C_PWBM = 100 × (R − B) / B (DM:262). +- **Published precision.** Full precision: 15 decimals, checked on a different slug's file (DM:268). The lane counts the stored decimals of B and R in the 2030 row without printing them. +- **Interval rule.** If both stored values carry six or more decimals, C is a point value. Otherwise B and R each carry ± half their last stored unit, and C runs over the four corners, as for OACT. +- **Also recorded (specifications, no values).** The lane records the "Divergence(s) from the policy scored by SSA" text in `law_change_brief.md` and PWBM's definition of cost. The brief's Table 2 is restricted. + +**MINT5, archived Office of Retirement Policy table** (LOC 2.3) +- **Original URL.** `http://ssa.gov/retirementpolicy/projections/colas/reduce-COLA-by-1-point-2030.html`. The live tree now returns 404. +- **Archive capture.** `https://web.archive.org/web/20111030080836id_/http://ssa.gov/retirementpolicy/projections/colas/reduce-COLA-by-1-point-2030.html`, CDX digest `HQABASP6M6XNNB4FM6R4PC3JDJLHEJJ3`. +- **Twins in the same capture set.** `...-2030.xlsx` at `web/20111030094220id_/`, digest `4M3YURYZAVDWU6KTJ34FT5GMAIBIMOX3`; `...-2030.pdf` at `web/20111030094225id_/`, digest `XOEMLRJGQZWRMM5XBX5QNHMZG7PREHOG`. +- **Text to match.** The source line "SSA's Office of Retirement Policy, using MINT 5/ TR 08 Assumptions/ SSMBA 3.1. Run: 08/12/2011" and the title "Reduce the Cost of Living Adjustment by 1 percentage point, 2030". +- **Cells.** Table 1, "Overall individual benefits 2030". The rows are Total, 62-69, 70-79, 80-89 and 90+. The column is "Median Difference vs Scheduled", "%" sub-column. Memo items: its "$" sub-column, "Median Monthly Benefit" (Scheduled and Option, with Payable as a reference), "% of Population Affected", and Table 2, "Affected individual benefits". +- **Unit and conversion.** Percent against scheduled benefits. Ours is computed in the same unit, so no conversion applies. +- **Published precision.** Not yet recorded, because the research lane masked every numeral. Without printing values, the lane records the displayed decimals of the HTML "%" column and the stored decimals in the XLSX twin. +- **Interval rule.** If the XLSX stores more decimals than the HTML displays, and the stored value rounds to the displayed one, the XLSX value is the registered point, ± half its last stored unit. Otherwise the HTML display is registered, ± half its last displayed unit. +- **Vintage.** TR 2008 intermediate, the same as the model's. MINT5's table is static. + +**Comparator lane.** A fresh validation-only lane fetches and seals the comparators. It is neither the research lane nor the memo's reviewer or reviser. +- Two transcribers work independently, and their results are reconciled. +- The lane seals them in `EV/acola-comparator-seal-20260930.json`. The seal holds the SHA-256 of every fetched file, the retrieval dates, the OACT footer string, the PWBM folder and `Last-Modified`, and the MINT capture timestamp and digest (LOC 3). +- It runs a values scan on anything builders will read (DM:195). +- The seal opens only after the run artifact merges. + +### Adjustments + +No adjustment changes any number. Each difference below is named on the output with its source. + +- **Cost definition (dilution).** The TR 2026 glossary defines cost as scheduled benefits plus administrative expenses, the financial interchange with the Railroad Retirement program and vocational rehabilitation payments (DM:269). Administrative expenses do not change with the reform, so the comparators' percent is slightly diluted relative to a benefits-only P. The Railroad Retirement interchange may move with a benefit change, and the sources do not say in which direction. The lane records PWBM's cost definition, and where a source is silent, its dilution is registered as unsigned. +- **Closed cohort versus full population (context only).** The model has nobody under 50 in 2030 and no post-2010 immigrants (DM:270). Every beneficiary type gets the reduced COLAs, so the missing groups dilute the COLA row little (DM:271). The output sets the model's weighted 2030 beneficiary count beside the all-ages 2030 OASDI count in the committed TR 2026 snapshot `data/external/snapshots/ssa_level_anchors_vintage1/trustees2026_lr4b4.html` (DM:270), as context only. The row is not scaled; the scaling by the share of benefit dollars going to people 50 and over applies only to the FRA rows (DM:272). +- **Scheduled versus payable.** 2030 comes before depletion in both TR 2026 and PWBM's baseline (DM:273). Every comparison is on scheduled benefits, and MINT's column is "vs Scheduled". +- **Baseline vintage.** The model uses TR 2008 paths. OACT uses TR 2026, PWBM its own baseline, and MINT5 TR 2008 (DM:274; LOC 2.3). The percent metric removes most level differences, because a one-point cut scales benefits by about (1 + c − 0.01)/(1 + c) per reduced COLA and so depends only weakly on the COLA c (DM:274). The vintage is named as a delta, and N − N_model measures its COLA-rate part. +- **DI and auxiliaries.** Children are absent. Disabled widow(er)s appear only in the opening stock and are not projected (DM:275; the projected components are listed in `cola_track_a/benefits.py:38-62`). This is minor for the COLA row. +- **COLA timing.** C0 and C1 bracket the comparators' timing, as described under "Why C1". No adjustment applies. +- **Taxation of benefits.** The test compares cost only, not annual balance, which nets the change in income. OACT's and PWBM's income-rate changes are recorded as memo items; they feed Option B (DM:276). + +### Pass criteria + +The test is report-only. No pass band exists, and nobody set one (d658; DM §5, question 2). Track A carries no numerical acceptance threshold (d074), and `TrackAConfig` refuses an acceptance rule (`cola_track_a/config.py:386-390`). The new config carries no acceptance rule either. No band may be added after this comment. For each comparator, the output reports the model's value (mean, draw SD and floor), the comparator's interval, the gap interval and the null's gap. + +### Predicted bias directions, registered in advance + +From the memo (DM:278-285), the items that apply to the COLA row: +- **No earnings after 2010.** Cohorts reaching 62 in 2027–2030 have AIMEs built on careers that stop at ages 42–45, so their PIAs are understated and their share of 2030 benefit dollars is too small. The 2028–2030 cohorts carry fewer reduced COLAs (the 2027 cohort carries all three), so this pushes the model's COLA effect slightly away from zero (DM:279). +- **Net.** The COLA row is primary. The FRA biases, which mostly push away from zero, belong to the later FRA registration (DM:285). + +Added by this drafter, deduced from code read this session and labelled as deductions: +- **Claiming vintage.** Claiming snaps to the 2008 claiming table (`cola_track_a/config.py:272-274`). If early claiming has fallen since 2008, which this session did not check, the model has too many beneficiaries aged 62–64 in 2030. On the eligibility clock, those people became eligible in 2028–2030 and carry at most two reduced increases in 2030 payments. This pushes the COLA effect slightly toward zero. +- **COLA-rate vintage.** The per-COLA factor 1 − 0.01/(1 + c) rises with c. So if the model's TR 2008 rates exceed TR 2026's for 2027–2029, the model's cut is slightly smaller in magnitude, and the reverse also holds. The sign of N − N_model, posted before the run, fixes this direction. +- **Timing.** If a comparator counts December 2030 at the reduced December 2030 increase, C0 lies slightly closer to zero than that comparator, and C1 bounds the gap. +- **MINT5 sub-row (M0–M4).** Added after the independent review, deduced the same way. The claiming-vintage item applies: in 2030, people aged 62–64 carry at most two reduced increases under M0 and M1, so too many early claimants pull the 62–69 band and the total toward zero. The COLA-rate-vintage item does not apply, because MINT5 and the model both run on TR 2008. The no-earnings item moves the ratio of means mainly through benefit-dollar weights, and moves the median of individual percent changes little. + +This comment registers no candid forecast; d658 does not call for one. The nulls, posted before the run, are the stated expectation. + +### Rules + +- **One shot.** The run happens once, at the registered commit. There is no tuning and no rerun. Any code or specification change after the second comment needs a new registration. +- **Re-execution.** One disclosed re-execution is allowed, and only for an external infrastructure failure before any value is scored. It is reported here before relaunch. +- **No-drop rule.** The one run computes every row, alternative, statistic, memo item and null listed in this comment, and all of them are published, including any undefined value with its reason. Nothing is dropped. The secondary D row runs later under the same rule, and the FRA rows will run under their own registration's. +- **Publishes regardless.** The artifact merges by its own PR, a second review-and-CI cycle, before the seal opens (DM:215). A validation-only lane opens the seal, and the orchestrator verifies the result and posts it on this issue whatever it shows (DM:216). +- **Topology.** The run executes under launchd, detached from the desktop app, as Registrations 13–17 did. If a guard refuses the run, the refusal is reported here, as Registration 14's was. + +### Build, and what the second registration comment posts + +The build follows DM:289-302: +- **Separate config.** A separate config module holds its own registered-row set with the rows above. `REGISTERED_ROWS` (`cola_track_a/config.py:225-241`), the exercise-1 and exercise-3 constants and `_check_reform` (`fra68_track/reform.py:377-411`) stay unchanged. Track A v2 imports `REGISTERED_ROWS` (`track_a_v2/matrix.py:8`) and the v1 and FRA runners (`track_a_v2/runner.py:20-31`), and `scripts/track_a_dry_run.py:386-395` checks every registered row against the A1 block. +- **Entry script.** A thin copy of `scripts/run_track_a_registered.py` keeps its guards. The registration pointer must be a comment URL on this issue, and the run refuses unless `HEAD` equals `--registered-commit` on a clean tree (`scripts/run_track_a_registered.py:178-192`). The pointer is this comment's URL. +- **New code.** The aggregate statistic with per-component reform totals, the OASI and DI split, the MINT-band statistics, the three nulls, and a `compare.py` for the comparison lane. +- **Start-year check.** Today's rows start their reduced increases in 2009 or 2010 (`scenario_benefits.py:180`; `cola_track_a/config.py:229-230`). The build checks that nothing downstream assumes a first reduced increase at or before the 2010 opening year (DM:297). +- **Review.** Tests use invented data only. An independent review precedes the merge (d658). The build coordinates with the Track A v2 and U2 lanes before touching shared modules (DM:302). + +The second registration comment, posted after the merge and before the run, gives: +- the registered commit's full 40-hex SHA and the exact run command; +- the SHA-256 of the committed specification file that restates this comment, with a statement that the file matches it; +- N, N_model, N_MINT and M1's null, with the TR 2026 file, table and row that c_k comes from, and that file's SHA-256; +- the comparator seal's SHA-256 and the lane's value-free records: OACT's header strings and footer, the stored decimals of PWBM's values, MINT's median type, age convention and decimals, MINT's start-date reading, each comparator's December 2030 timing, and whether any comparator prints an OASI and DI split. + +### Blindness caveat + +The blindness is procedural, not informational (DM:218). +- **Public tables.** OACT, PWBM and MINT publish these tables, and model-built code may carry training priors. +- **Mitigation.** The registered runs have no free parameters once registered, and single-year 2030 cost changes are unlikely to be memorized. +- **Limit 1: MINT5 is close to exercise 1.** The MINT5 sub-row's reform is close to exercise 1's R1 row: a first reduced increase determined in 2010, with 2030 benefits by age. That row's model result and its DYNASIM comparator have been public on this issue since 2026-09-23. The sub-row therefore tests the statistic and population definitions more than fresh mechanics. +- **Limit 2: DM prints run115 numbers.** Lines 268, 343 and 475 print numbers derived from OACT run115 (provision A3, chained CPI from December 2027); line 268 also prints CBO 2015's current-law 2030 outlays (LOC 5.3(a)). Scaled by A3's stated average COLA reduction, line 343 or 475 gives a near-linear proxy for A1's 2030 cost change. Builders do not read those lines. The restricted list adds DM. Builders work from this comment and their brief, and may follow its DM citations only in a copy with lines 268, 343 and 475 redacted, after a values scan. + +### Disclosure: who has seen what + +- **The research lane** behind the comparator report disclosed exposure to OACT's removed provision A7 row, CRFB's FRA-68 "gap closed" share, some CBO 2015 Option 25 and 33 sentences, and the Trustees Report's "size needed" cut (DM:198). It does not build. +- **The memo's reviewer** declared opening OACT's category pages and PWBM's exercise-slug descriptions (DM:198). It does not build. +- **The memo's reviser** read, with values, the full OACT run115 table, CBO 2015's current-law and Option 29 2030 cells, and the 2030–31 rows of PWBM's `oasdi_rate_13_4` CSV (a payroll-tax provision, whose rows may carry PWBM's baseline 2030 cost rate), as DM:9 records. It does not build. +- **The locator step** (three Claude Code workflow agents, Opus 5.5, 2026-09-30; the third wrote LOC) will not build or forecast. The second agent read DM in full, lines 9, 268, 343 and 475 included, so it saw OACT's current-law 2030 cost rate and A3's 2030 income-rate and cost-rate changes. The first and third printed DM lines 9, 268 and 475 in full and saw line 343 with decimals masked. The first agent's mask hid only the digits next to each decimal point. All three read the `S/` notes on OACT, PWBM, MINT, CBO and CRFB, and sections of the other-models note. The third also read Cato's `commands.txt` with decimals masked. The second agent downloaded the archived MINT5 2030 HTML and XLSX and PWBM's A1 cost-rate CSV, and printed only their labels, structure and stored-decimal counts. The third made the HEAD request and the 512-byte header read described in LOC lines 8–14. +- **This registration's drafter** (a Claude Code subagent, Opus 5.5, 2026-09-30) will not build or forecast. + - **What it read:** + - DM's heading list and lines 180–219 (the common protocol), 234–328 (Option A) and 433–471 (sections 4 and 5). It did not read lines 1–179, 220–233, 329–432 or 472–479, so it did not see lines 9, 343 or 475. + - LOC in full. + - `EV/RESTRICTED-FILES.md` (SHA-256 `241a6855…`) in full. + - The repository files cited above, at `dd222b9`. + - On this issue, it read: + - the first line of each of the last 90 comments; + - Registration 13 in full, including its forecast table for exercise 1; + - the headings and bold-led lines of Registrations 14–17; + - Registration 17's opening paragraphs, rules and drafter disclosure. + + It opened no result comment. + - The d658 record in the decision ledger. + - File-name listings of the trust-fund folders and of the repository tree. + - **What it did not open:** any OACT, PWBM, MINT, CBO or CRFB page or file; any `S/` note; the other-models note; any TR 2026 page or snapshot; any comparator lane or seal; any run artifact. + - **Exposure:** it has seen no A1 value from any comparator. Reading DM line 268 in full, it saw OACT's current-law 2030 OASDI cost rate, which the memo derives from run115 as proposal minus change, and a precision figure computed from it. That rate is a current-law baseline, not an A1 or A3 change. Current law is common to every 2026-TR provision page, so within rounding it equals the denominator p − d of C_OACT. The same line gave it CBO 2015's approximate current-law 2030 outlays as a share of GDP, a baseline for the later FRA rows. It has not seen p or d for run113, A3's 2030 changes (DM:343, 475), or any comparator's numerator. Its only network requests were GitHub API reads of this issue. +- **Review and revision lanes** (Claude Code subagents, Opus 5.5, 2026-09-30) will not build or forecast. A first round-1 review attempt printed DM line 268 in full, so it saw OACT's current-law 2030 cost rate and CBO 2015's current-law 2030 outlays. The round-1 reviewer read lines 9, 268, 343 and 475 with every digit masked. The lane that drafted the round-1 fixes read line 475 with digits masked. The round-2 reviewer saw no DM value. +- **Builders** are fresh lanes that read `RESTRICTED-FILES.md` first and open nothing it lists. The list gains this test's sources, their earlier vintages and proxies, the FRA-row sources and the lane's output paths before any builder starts (DM:199-212). +- **The orchestrating session** (Claude Code, Opus 5.5) will not build or forecast. On 2026-09-29 at 14:34 UTC, a minute after the memo's last edit, it printed DM lines 12–19, 75–93 and 433–480 in full. That range includes line 475, so it saw A3's (run115) 2030 cost-rate change, a near-linear proxy for A1's 2030 cost change. A context compaction later that day removed the read from its working record; its session transcript shows it. On 2026-09-30 it read DM's heading list and lines 180–330, with every line cut at 400 characters. On line 268 that view ends before the current-law cost rate and shows only the precision rules (±0.005 on OACT's printed columns, ±0.01 on the derived rate). It did not read DM lines 9 or 343, and it opened no comparator page or file. On 2026-09-29 it coordinated the comparator research lane and worked from that lane's notes (the other-models note and specs-only notes under `S/`), so it cannot rule out having seen values there. Any builder brief it writes passes the values scan (DM:195) before a builder reads it. diff --git a/docs/design/acola_spec_only_nulls.json b/docs/design/acola_spec_only_nulls.json new file mode 100644 index 000000000..b04864b66 --- /dev/null +++ b/docs/design/acola_spec_only_nulls.json @@ -0,0 +1,272 @@ +{ + "schema_version": "populace_dynamics.acola_track.spec_nulls.v1", + "annual_reduction": "0.01", + "inputs": { + "TR2026": { + "table": "Table V.C1", + "column": "Cost-of-living benefit increasea (percent)", + "cells": { + "2027": { + "as_published": "2.4", + "percent": "2.4", + "rate": "0.024", + "row_header_path": [ + "Intermediate:", + "2027" + ], + "column_header_path": [ + "Cost-of-living benefit increasea (percent)" + ] + }, + "2028": { + "as_published": "2.4", + "percent": "2.4", + "rate": "0.024", + "row_header_path": [ + "Intermediate:", + "2028" + ], + "column_header_path": [ + "Cost-of-living benefit increasea (percent)" + ] + }, + "2029": { + "as_published": "2.4", + "percent": "2.4", + "rate": "0.024", + "row_header_path": [ + "Intermediate:", + "2029" + ], + "column_header_path": [ + "Cost-of-living benefit increasea (percent)" + ] + } + }, + "source_url": "https://www.ssa.gov/oact/TR/2026/V_C_prog.html", + "file": "data/external/snapshots/tr2026_v_c1_vintage1/trustees2026_v_c1_table.html", + "sha256": "4d29f2b85d67b995eea25f0bdfb46b3755671c082d75a9855680836d4b80c46a", + "capture_manifest_sha256": "fd271e0067c70737ac36e3a442c96d0b7e4650aaa14398b8383ac6e24e853abb", + "retrieved_utc": "2026-10-01T15:01:44Z", + "located_from": { + "list_of_tables_url": "https://www.ssa.gov/oact/TR/2026/X1_trLOT.html", + "href": "V_C_prog.html#1047210", + "how": "the 2026 Report's table of contents (https://www.ssa.gov/oact/TR/2026/trTOC.html) links no table; it links the List of Tables, whose one entry naming V.C1 links this anchor on the section V.C chapter page" + }, + "timing_footnote": { + "text": "a Effective with benefits payable for June in each year 1975-82, and for December in each year after 1982.", + "file": "data/external/snapshots/tr2026_v_c1_vintage1/cola_timing_note.txt", + "sha256": "f63274d3ef3b06c1647326490945ed05c01421466f9b0cca891bd1d0a511341b", + "retrieved_utc": "2026-10-01T15:49:46Z", + "anchor": "12" + }, + "fallback_used": false, + "column_choice": "Printed COLA column, matching TR2008 V.C1; determination year k is the increase effective December k." + }, + "model_TR2008": { + "file": "data/external/tr2008/tr2008_report.json", + "sha256": "c16161f1ee99d94d97648da078d686325fb05a0d44653b76bc609158e63f6d62", + "rates": { + "2010": "0.028", + "2011": "0.028", + "2012": "0.028", + "2013": "0.028", + "2014": "0.028", + "2015": "0.028", + "2016": "0.028", + "2017": "0.028", + "2018": "0.028", + "2019": "0.028", + "2020": "0.028", + "2021": "0.028", + "2022": "0.028", + "2023": "0.028", + "2024": "0.028", + "2025": "0.028", + "2026": "0.028", + "2027": "0.028", + "2028": "0.028", + "2029": "0.028" + }, + "source_tags": { + "2010": "tr2008_v_c1_projected", + "2011": "tr2008_v_c1_projected", + "2012": "tr2008_v_c1_projected", + "2013": "tr2008_v_c1_projected", + "2014": "tr2008_v_c1_projected", + "2015": "tr2008_v_c1_projected", + "2016": "tr2008_v_c1_projected", + "2017": "tr2008_v_c1_projected", + "2018": "derived_ultimate_cpi", + "2019": "derived_ultimate_cpi", + "2020": "derived_ultimate_cpi", + "2021": "derived_ultimate_cpi", + "2022": "derived_ultimate_cpi", + "2023": "derived_ultimate_cpi", + "2024": "derived_ultimate_cpi", + "2025": "derived_ultimate_cpi", + "2026": "derived_ultimate_cpi", + "2027": "derived_ultimate_cpi", + "2028": "derived_ultimate_cpi", + "2029": "derived_ultimate_cpi" + }, + "basis": "TR2008 V.C1 printed COLAs through 2017; Table II.C1 ultimate annual-average CPI from 2018, matching the runtime model path." + } + }, + "december_null_registered": false, + "december_note": "C1 and M2 include December2030; the registered nulls stop at determination year2029. No December null is computed.", + "N": { + "value": -2.901170402765274, + "value_decimal": "-2.9011704027652740478515625", + "exact": { + "numerator": "-97347125", + "denominator": "33554432" + }, + "years": [ + 2027, + 2028, + 2029 + ], + "factor_count": 3, + "rates": { + "2027": "0.024", + "2028": "0.024", + "2029": "0.024" + }, + "source": "TR2026" + }, + "N_model": { + "value": -2.889991972119744, + "value_decimal": "-2.8899919721197439019598290221155817992219312710473", + "exact": { + "numerator": "-98112875", + "denominator": "33949186" + }, + "years": [ + 2027, + 2028, + 2029 + ], + "factor_count": 3, + "rates": { + "2027": "0.028", + "2028": "0.028", + "2029": "0.028" + }, + "source": "model_TR2008" + }, + "N_MINT": { + "value": -16.95019759725955, + "value_decimal": "-16.950197597259551155011310281830073286124621211173", + "exact": { + "numerator": "-13658846831907464659256660086044965534729868780939875", + "denominator": "805822277500515946132634136398784083302867716407296" + }, + "years": [ + 2011, + 2012, + 2013, + 2014, + 2015, + 2016, + 2017, + 2018, + 2019, + 2020, + 2021, + 2022, + 2023, + 2024, + 2025, + 2026, + 2027, + 2028, + 2029 + ], + "factor_count": 19, + "rates": { + "2011": "0.028", + "2012": "0.028", + "2013": "0.028", + "2014": "0.028", + "2015": "0.028", + "2016": "0.028", + "2017": "0.028", + "2018": "0.028", + "2019": "0.028", + "2020": "0.028", + "2021": "0.028", + "2022": "0.028", + "2023": "0.028", + "2024": "0.028", + "2025": "0.028", + "2026": "0.028", + "2027": "0.028", + "2028": "0.028", + "2029": "0.028" + }, + "source": "model_TR2008" + }, + "N_M1": { + "value": -17.758075052539127, + "value_decimal": "-17.758075052539127505643495979477640666609790265539", + "exact": { + "numerator": "-7355264176191157484627957051996279498828937067702044375", + "denominator": "414192650635265196312173946108975018817674006233350144" + }, + "years": [ + 2010, + 2011, + 2012, + 2013, + 2014, + 2015, + 2016, + 2017, + 2018, + 2019, + 2020, + 2021, + 2022, + 2023, + 2024, + 2025, + 2026, + 2027, + 2028, + 2029 + ], + "factor_count": 20, + "rates": { + "2010": "0.028", + "2011": "0.028", + "2012": "0.028", + "2013": "0.028", + "2014": "0.028", + "2015": "0.028", + "2016": "0.028", + "2017": "0.028", + "2018": "0.028", + "2019": "0.028", + "2020": "0.028", + "2021": "0.028", + "2022": "0.028", + "2023": "0.028", + "2024": "0.028", + "2025": "0.028", + "2026": "0.028", + "2027": "0.028", + "2028": "0.028", + "2029": "0.028" + }, + "source": "model_TR2008" + }, + "N_minus_N_model": { + "value": -0.011178430645530146, + "value_decimal": "-0.011178430645530145891733477884418200778068728952735", + "exact": { + "numerator": "-6366930339125", + "denominator": "569572826546176" + } + } +} diff --git a/scripts/build_acola_spec_only_nulls.py b/scripts/build_acola_spec_only_nulls.py new file mode 100644 index 000000000..0954929b0 --- /dev/null +++ b/scripts/build_acola_spec_only_nulls.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +"""Write Registration 18's spec-only nulls from pinned, approved inputs.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "src")) + +from populace_dynamics.acola_track.nulls import ( # noqa: E402 + NULLS_PATH, + build_nulls, +) + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, default=NULLS_PATH) + args = parser.parse_args(argv) + rendered = json.dumps(build_nulls(), indent=2, allow_nan=False) + "\n" + args.output.write_text(rendered, encoding="utf-8") + print(args.output) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/capture_tr2026_v_c1.py b/scripts/capture_tr2026_v_c1.py new file mode 100644 index 000000000..f1bcb956d --- /dev/null +++ b/scripts/capture_tr2026_v_c1.py @@ -0,0 +1,323 @@ +"""Capture TR 2026 Table V.C1 as a byte-exact table fragment (A-COLA). + +Registration 18 (trust fund test A-COLA, decision d658) takes the +spec-only null's COLAs c_k from "the 2026 Trustees Report's Table V.C1, +intermediate assumptions, the increases effective December 2027, 2028 +and 2029". The Report publishes Table V.C1 on no page of its own: its +List of Tables (``X1_trLOT.html``, linked from the table of contents as +"LIST OF TABLES") links the entry "V.C1 Cost-of-Living Benefit +Increases, ..." to ``V_C_prog.html#1047210``, the chapter page of +section V.C, which holds 26 tables and the chapter's prose. A builder +of A-COLA may read no Trustees Report page or table that prints OASDI +cost, cost rates, income rates or balances, so this script does not +store the chapter page. It fetches the page once, keeps it in memory, +cuts out the one ```` element whose caption is Table V.C1's, and +writes: + +* the fragment, byte for byte as served (``trustees2026_v_c1_table.html``); +* ``capture_manifest.txt``, one line in the vintage-1 grammar + `` `` for the fragment; +* ``capture_record.json``: the URL, the retrieval time, the fetch mode, + the HTTP status, the SHA-256 and size of the whole page as served, and + the fragment's byte offsets in it, so the fragment can be located again + in any later fetch whose table bytes are unchanged. + +The fetch follows ``EV/pia-captures-20260929/_tools/capture.sh``: plain +curl first, then full Chrome request headers over HTTP/2 (gzip and +deflate only), storing the decoded body. Nothing else on the page is +printed or stored. The script refuses to overwrite an existing file. + +Usage:: + + python scripts/capture_tr2026_v_c1.py [--output-dir DIR] +""" + +from __future__ import annotations + +import argparse +import datetime +import hashlib +import json +import subprocess +from html.parser import HTMLParser +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SOURCE_URL = "https://www.ssa.gov/oact/TR/2026/V_C_prog.html" +#: The anchor the 2026 List of Tables gives for Table V.C1. +LIST_OF_TABLES_URL = "https://www.ssa.gov/oact/TR/2026/X1_trLOT.html" +LIST_OF_TABLES_HREF = "V_C_prog.html#1047210" +CAPTION = ( + "Table V.C1.—Cost-of-Living Benefit Increases, Average Wage " + "Index, Contribution and Benefit Bases, and Retirement Earnings Test " + "Exempt Amounts, 1975-2035" +) +DEFAULT_OUTPUT_DIR = ( + ROOT / "data" / "external" / "snapshots" / "tr2026_v_c1_vintage1" +) +FRAGMENT_NAME = "trustees2026_v_c1_table.html" +MANIFEST_NAME = "capture_manifest.txt" +RECORD_NAME = "capture_record.json" +TIMING_NOTE_NAME = "cola_timing_note.txt" +_BROWSER_HEADERS = ( + "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9," + "image/avif,image/webp,image/apng,*/*;q=0.8," + "application/signed-exchange;v=b3;q=0.7", + "Accept-Language: en-US,en;q=0.9", + "Accept-Encoding: gzip, deflate", + "Cache-Control: no-cache", + "Pragma: no-cache", + 'Sec-Ch-Ua: "Google Chrome";v="129", "Not=A?Brand";v="8", ' + '"Chromium";v="129"', + "Sec-Ch-Ua-Mobile: ?0", + 'Sec-Ch-Ua-Platform: "macOS"', + "Sec-Fetch-Dest: document", + "Sec-Fetch-Mode: navigate", + "Sec-Fetch-Site: none", + "Sec-Fetch-User: ?1", + "Upgrade-Insecure-Requests: 1", +) + + +def _fetch(mode: str) -> tuple[str, bytes, str]: + """One curl request; returns (HTTP status, decoded body, UTC time).""" + + command = ["curl", "-sS", "-L", "--max-time", "180"] + if mode == "browser-h2": + command += ["--http2", "--compressed"] + for header in _BROWSER_HEADERS: + command += ["-H", header] + command += ["-w", "\n%{http_code}", SOURCE_URL] + retrieved = datetime.datetime.now(datetime.timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) + out = subprocess.run(command, capture_output=True, check=False) + body, _, status = out.stdout.rpartition(b"\n") + return status.decode().strip(), body, retrieved + + +class _TableSpans(HTMLParser): + """Character spans of every outermost ``
`` and its caption.""" + + def __init__(self, text: str) -> None: + super().__init__(convert_charrefs=True) + self._line_starts = [0] + for index, char in enumerate(text): + if char == "\n": + self._line_starts.append(index + 1) + self._text = text + self._depth = 0 + self._start: int | None = None + self._caption: list[str] | None = None + self._captions: list[str] = [] + self.tables: list[tuple[int, int, str]] = [] + + def _offset(self) -> int: + line, column = self.getpos() + return self._line_starts[line - 1] + column + + def handle_starttag(self, tag, attrs): # noqa: ANN001 + if tag == "table": + self._depth += 1 + if self._depth == 1: + self._start = self._offset() + self._captions = [] + elif tag == "caption" and self._depth == 1: + self._caption = [] + + def handle_data(self, data): # noqa: ANN001 + if self._caption is not None: + self._caption.append(data) + + def handle_endtag(self, tag): # noqa: ANN001 + if tag == "caption" and self._caption is not None: + self._captions.append( + " ".join("".join(self._caption).replace("\xa0", " ").split()) + ) + self._caption = None + elif tag == "table": + self._depth -= 1 + if self._depth == 0 and self._start is not None: + end = self._text.index(">", self._offset()) + 1 + caption = self._captions[0] if self._captions else "" + self.tables.append((self._start, end, caption)) + self._start = None + + +def table_fragment(page: bytes) -> tuple[bytes, int, int]: + """The V.C1 ``
`` element's exact bytes and its byte offsets.""" + + text = page.decode("utf-8") + parser = _TableSpans(text) + parser.feed(text) + parser.close() + matches = [span for span in parser.tables if span[2] == CAPTION] + if len(matches) != 1: + raise ValueError( + f"expected exactly one table captioned {CAPTION!r}; found " + f"{len(matches)}" + ) + start, end, _ = matches[0] + if not text[start:end].lower().startswith(" tag") + if not text[start:end].lower().endswith("
"): + raise ValueError("the V.C1 span does not end at
") + byte_start = len(text[:start].encode("utf-8")) + byte_end = len(text[:end].encode("utf-8")) + fragment = page[byte_start:byte_end] + if fragment != text[start:end].encode("utf-8"): + raise ValueError("the fragment's bytes differ from its decoded text") + return fragment, byte_start, byte_end + + +class _COLATimingNote(HTMLParser): + """Read only V.C1 footnote a, between its two named anchors.""" + + def __init__(self) -> None: + super().__init__(convert_charrefs=True) + self.active = False + self.finished = False + self.parts: list[str] = [] + self.matches = 0 + + def handle_starttag(self, tag, attrs): + if tag == "a": + name = dict(attrs).get("name") + if name == "12": + self.active = True + self.matches += 1 + elif name == "13" and self.active: + self.active = False + self.finished = True + + def handle_endtag(self, tag): + if self.active and tag in {"div", "p", "br"}: + self.parts.append(" ") + + def handle_data(self, data): + if self.active: + self.parts.append(data) + + +def cola_timing_note(page: bytes) -> str: + """The timing note linked as footnote a from V.C1's COLA header. + + The note is outside the table element. No adjacent note or chapter + text is returned. The registered input includes December timing. + """ + parser = _COLATimingNote() + parser.feed(page.decode("utf-8")) + note = " ".join("".join(parser.parts).replace("\xa0", " ").split()) + if parser.matches != 1 or not parser.finished: + raise ValueError( + "expected one V.C1 COLA footnote between anchors 12 and 13" + ) + if "December" not in note or "1982" not in note: + raise ValueError("V.C1 COLA footnote does not confirm December timing") + return note + + +def _write_new(path: Path, data: bytes) -> None: + with path.open("xb") as handle: + handle.write(data) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR) + args = parser.parse_args(argv) + for name in (FRAGMENT_NAME, TIMING_NOTE_NAME, MANIFEST_NAME, RECORD_NAME): + if (args.output_dir / name).exists(): + raise FileExistsError(args.output_dir / name) + status, body, retrieved = _fetch("plain") + mode = "plain" + if status != "200": + status, body, retrieved = _fetch("browser-h2") + mode = "browser-h2" + if status != "200" or not body: + raise SystemExit(f"fetch failed: HTTP {status} ({mode})") + fragment, start, end = table_fragment(body) + timing_note = (cola_timing_note(body) + "\n").encode() + timing_sha = hashlib.sha256(timing_note).hexdigest() + fragment_sha = hashlib.sha256(fragment).hexdigest() + args.output_dir.mkdir(parents=True, exist_ok=True) + _write_new(args.output_dir / FRAGMENT_NAME, fragment) + _write_new(args.output_dir / TIMING_NOTE_NAME, timing_note) + _write_new( + args.output_dir / MANIFEST_NAME, + ( + f"{retrieved} {fragment_sha} {len(fragment)} {FRAGMENT_NAME}\n" + f"{retrieved} {timing_sha} {len(timing_note)} {TIMING_NOTE_NAME}\n" + ).encode(), + ) + record = { + "schema_version": "tr2026_v_c1_capture.v1", + "table": "Table V.C1", + "caption": CAPTION, + "source_url": SOURCE_URL, + "located_from": { + "list_of_tables_url": LIST_OF_TABLES_URL, + "href": LIST_OF_TABLES_HREF, + "how": ( + "the 2026 Report's table of contents " + "(https://www.ssa.gov/oact/TR/2026/trTOC.html) links no " + "table; it links the List of Tables, whose one entry naming " + "V.C1 links this anchor on the section V.C chapter page" + ), + }, + "retrieved_utc": retrieved, + "fetch_mode": mode, + "fetch_method": ( + "curl -sS -L; on a status other than 200, curl --http2 " + "--compressed with Chrome 129 request headers (gzip, deflate); " + "the decoded body is kept in memory only" + ), + "http_status": int(status), + "timing_footnote": { + "file": TIMING_NOTE_NAME, + "sha256": timing_sha, + "text": timing_note.decode().strip(), + "anchor": "12", + "extraction": "V.C1 COLA note only; whitespace normalized", + }, + "page": { + "stored": False, + "why_not_stored": ( + "Table V.C1 has no page of its own; the chapter page holds " + "26 tables and the chapter prose, and an A-COLA builder may " + "read no Trustees Report page or table that prints OASDI " + "cost, cost rates, income rates or balances " + "(EV/RESTRICTED-FILES.md, 2026-10-01 02:40)" + ), + "sha256": hashlib.sha256(body).hexdigest(), + "bytes": len(body), + "note": ( + "the page's bytes vary between requests outside the " + "tables, so a later fetch may not reproduce this digest" + ), + }, + "fragment": { + "file": FRAGMENT_NAME, + "sha256": fragment_sha, + "bytes": len(fragment), + "byte_offset_start": start, + "byte_offset_end": end, + "definition": ( + "page[byte_offset_start:byte_offset_end], the outermost " + " element whose ') + groups.extend( + f"" + for year in (2027, 2028, 2029) + ) + return ( + f"
equals the caption above " + "(whitespace and no-break spaces normalized for the match)" + ), + }, + } + _write_new( + args.output_dir / RECORD_NAME, + (json.dumps(record, indent=2) + "\n").encode(), + ) + print(args.output_dir / FRAGMENT_NAME) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_acola_registered.py b/scripts/run_acola_registered.py new file mode 100644 index 000000000..554e5f84d --- /dev/null +++ b/scripts/run_acola_registered.py @@ -0,0 +1,269 @@ +"""Registration 18: guarded one-shot A-COLA run; report-only.""" + +from __future__ import annotations + +import argparse +import datetime +import hashlib +import importlib.util +import json +import re +import subprocess +import sys +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT / "src") not in sys.path: + sys.path.insert(0, str(ROOT / "src")) + +from populace_dynamics.acola_track.config import ( # noqa: E402 + REGISTRATION_POINTER, + SPECIFICATION_PATH, + ACOLAConfig, + check_specification, +) +from populace_dynamics.acola_track.runner import run_acola # noqa: E402 +from populace_dynamics.cohorts import psid2010 # noqa: E402 +from populace_dynamics.cola_track_a import ( # noqa: E402 + TrackAInputs, + prepare_track_a_cohort, +) +from populace_dynamics.cola_track_a.mortality import ( # noqa: E402 + load_tr2008_mortality, +) +from populace_dynamics.cola_track_a.runner import ( # noqa: E402 + A1_SPECIFICATION_PATH, + load_claiming_pmf, + tr2008_baseline_cola, + tr2008_ssa_parameters, +) +from populace_dynamics.cola_track_a.statutory import ( # noqa: E402 + CAPTURE_PATH, + CAPTURE_SHA256, +) +from populace_dynamics.data import tr2008 # noqa: E402 +from populace_dynamics.engine.di_entitlement_rates import ( # noqa: E402 + load_di_entitlement_rates, +) +from populace_dynamics.estimates.parameters import ( # noqa: E402 + load_cola_history, +) +from populace_dynamics.ss.params import load_ssa_parameters # noqa: E402 + +REGISTERED_HEADER = ( + "REGISTERED ONE-SHOT RUN - A-COLA, Registration 18, 2030. " + "Python oracle benefits: not Axiom. Report-only. " + "Publishes regardless of outcome." +) +DEFAULT_OUTPUT = ROOT / "runs" / "trust_fund_acola_2030_v1.json" +_POINTER_PATTERN = re.compile( + r"https://github\.com/PolicyEngine/microcosm-dynamics/issues/42" + r"#issuecomment-[0-9]+" +) + + +def _git(*args: str) -> str: + return subprocess.run( + ["git", "-C", str(ROOT), *args], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + + +def _sha256(path: Path) -> str: + return hashlib.sha256(Path(path).read_bytes()).hexdigest() + + +def _script_module(name: str) -> Any: + spec = importlib.util.spec_from_file_location( + "_acola_" + name, ROOT / "scripts" / (name + ".py") + ) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def preflight( + *, + registration_pointer: str, + registered_commit: str, + output: Path, + git: Any = _git, + specification_path: Path = SPECIFICATION_PATH, + config: ACOLAConfig | None = None, +) -> dict[str, str]: + """Read git and the committed specification; load no population data.""" + + if not _POINTER_PATTERN.fullmatch(registration_pointer): + raise ValueError( + "the registration pointer must be an issue #42 comment URL" + ) + if registration_pointer != REGISTRATION_POINTER: + raise ValueError( + "the pointer must be Registration 18's exact comment URL" + ) + if not re.fullmatch(r"[0-9a-f]{40}", registered_commit): + raise ValueError("--registered-commit must be a full 40-hex SHA") + head = git("rev-parse", "HEAD") + if head != registered_commit: + raise ValueError( + f"HEAD {head} is not the registered commit {registered_commit}" + ) + if git("status", "--porcelain") != "": + raise ValueError("the working tree must be clean for a registered run") + configuration = config or ACOLAConfig() + configuration.check_runnable() + configuration.check_registered() + binding = check_specification(specification_path) + output = Path(output) + if output.resolve() != DEFAULT_OUTPUT.resolve(): + raise ValueError("output must be the fixed registered artifact path") + if output.exists() or output.with_suffix(".env.json").exists(): + raise ValueError( + f"{output} already exists: the registered run is one-shot" + ) + return {"head": head, "specification_sha256": binding["sha256"]} + + +def _write_new(path: Path, text: str) -> None: + """Create a file exclusively, retaining the template's one-shot guard.""" + + with Path(path).open("x", encoding="utf-8") as handle: + handle.write(text) + + +def write_artifact( + output: Path, artifact: dict[str, Any], environment: dict[str, Any] +) -> None: + """Serialize the complete result and its hash-bound environment sidecar.""" + + content = json.dumps(artifact, indent=2, allow_nan=False) + "\n" + sidecar = ( + json.dumps( + { + "artifact": output.name, + "artifact_sha256": hashlib.sha256( + content.encode() + ).hexdigest(), + "environment": environment, + }, + indent=2, + allow_nan=False, + ) + + "\n" + ) + output.parent.mkdir(parents=True, exist_ok=True) + _write_new(output, content) + _write_new(output.with_suffix(".env.json"), sidecar) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--registration-pointer", required=True) + parser.add_argument("--registered-commit", required=True) + parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) + args = parser.parse_args(argv) + started = datetime.datetime.now(datetime.timezone.utc).isoformat() + config = ACOLAConfig() + state = preflight( + registration_pointer=args.registration_pointer, + registered_commit=args.registered_commit, + output=args.output, + config=config, + ) + track_config = config.track_a_config() + claiming_pmf = load_claiming_pmf() + raw = psid2010.load_psid2010_inputs(anchor_wave=2011) + a3 = psid2010.build_psid2010_cohort( + raw, psid2010.Psid2010CohortSpec(anchor_wave=2011) + ) + cohort = prepare_track_a_cohort( + a3, data_provenance="registered_real", config=track_config + ) + base_params = load_ssa_parameters() + params = tr2008_ssa_parameters( + base_params, alternative=track_config.tr2008_alternative + ) + environment = _script_module("run_track_a_registered")._environment( + ssa_parameters_revision=base_params.pe_us_revision + ) + realized = load_cola_history() + baseline = tr2008_baseline_cola( + realized, + first_year=track_config.tr2008_first_rate_year, + last_year=track_config.reference_year, + alternative=track_config.tr2008_alternative, + ) + rate_check = _script_module("track_a_dry_run")._spec_rate_check(baseline) + di_rates = load_di_entitlement_rates(track_config.di_spec) + mortality = load_tr2008_mortality( + range(2011, track_config.reference_year + 1), + alternative=track_config.tr2008_alternative, + base_year=track_config.mortality_base_year, + ) + provenance = { + "ssa_parameters": params.pe_us_revision, + "statutory_capture": { + "path": str(CAPTURE_PATH.relative_to(ROOT)), + "sha256": CAPTURE_SHA256, + }, + "tr2008_file_sha256": dict(tr2008.FILE_SHA256), + "di_rates": { + key: value + for key, value in di_rates.provenance.items() + if key.endswith("sha256") + }, + "cola_history_sha256": realized.provenance["sha256"], + "claiming_reference_sha256": _sha256(psid2010.CLAIMING_REFERENCE_PATH), + "a1_specification_sha256": _sha256(A1_SPECIFICATION_PATH), + "acola_specification_sha256": state["specification_sha256"], + "mortality": dict(mortality.provenance), + "psid_inputs": {"2011": dict(raw.provenance)}, + } + result = run_acola( + TrackAInputs( + cohort=cohort, + params=params, + baseline=baseline, + di_rates=di_rates, + population_mortality=mortality, + claiming_pmf=claiming_pmf, + provenance=provenance, + ), + config=config, + registration_pointer=args.registration_pointer, + progress=lambda message: print(message, file=sys.stderr), + check_committed_parameters=True, + ) + artifact = { + "header": REGISTERED_HEADER, + **result, + "checks": {"a1_rate_path": rate_check, "acola_binding": state}, + "run": { + "started": started, + "finished": datetime.datetime.now( + datetime.timezone.utc + ).isoformat(), + "registration_pointer": args.registration_pointer, + "registered_commit": args.registered_commit, + "git_head": state["head"], + "git_clean": True, + "command": " ".join( + [ + "python", + "scripts/run_acola_registered.py", + *(sys.argv[1:] if argv is None else argv), + ] + ), + }, + } + write_artifact(args.output, artifact, environment) + print(args.output) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/populace_dynamics/acola_track/__init__.py b/src/populace_dynamics/acola_track/__init__.py new file mode 100644 index 000000000..eda3daa70 --- /dev/null +++ b/src/populace_dynamics/acola_track/__init__.py @@ -0,0 +1 @@ +"""Registration 18's report-only, fixed-path A-COLA model side.""" diff --git a/src/populace_dynamics/acola_track/config.py b/src/populace_dynamics/acola_track/config.py new file mode 100644 index 000000000..b6a45aea1 --- /dev/null +++ b/src/populace_dynamics/acola_track/config.py @@ -0,0 +1,330 @@ +"""Registration 18's independent rows and projection configuration.""" + +from __future__ import annotations + +import hashlib +import math +from dataclasses import dataclass, field, fields, replace +from pathlib import Path +from typing import Any + +from populace_dynamics.cola_track_a.config import ( + OPENING_STOCK_BASIS, + POPULATIONS, + TRACK_A_LABELS, + AuxiliaryEntitlementClock, + LevelPolicy, + RegisteredRow, + SpouseEntitlementRule, + SurvivorEntitlementRule, + TrackAConfig, + rulings_departures, +) +from populace_dynamics.engine.di_entitlement_rates import DIEntitlementSpec +from populace_dynamics.estimates.cola_age_profile import PRIMARY_COMPONENTS +from populace_dynamics.scenario_benefits import ( + BenefitPeriod, + ExposureClock, + NegativeRatePolicy, +) + +ROOT = Path(__file__).resolve().parents[3] +REGISTRATION_POINTER = ( + "https://github.com/PolicyEngine/microcosm-dynamics/issues/42" + "#issuecomment-5919983573" +) +SPECIFICATION_PATH = ROOT / "docs/design/acola_registration18.md" +SPECIFICATION_SHA256 = ( + "02287d54f12285c8dffcb94ba7dae9cbe3cc34ba338c602a5ad4afe14d598f20" +) +LABELS = (*TRACK_A_LABELS, "report-only") +AUTHORIZATION = { + "decision": "d658", + "ruling": ( + "Yes: register today a report-only blind test of OACT provision A1 " + "(COLA -1pt from Dec 2027), 2030 cost effect, vs OACT and PWBM, " + "with MINT5 as third comparator on the COLA row; report our ratio " + "of means and a median beside MINT's median; FRA-68 rows secondary " + "later. Build + independent review + post the #42 registration today." + ), + "carried_conventions": ( + "Registered by Registration 18, citing cola_track_a/config.py: " + "d074 claim class, COLA horizon and disclosed oracle DI levels; " + "d075 opening-stock basis." + ), +} + + +@dataclass(frozen=True) +class ACOLARow: + """A row built outside the legacy registry; D remains deferred.""" + + row_id: str + sub_row: str + field_changed: str | None + first_reduced_determination_year: int + exposure_clock: ExposureClock = ExposureClock.ELIGIBILITY + benefit_period: BenefitPeriod = BenefitPeriod.CALENDAR_YEAR + mechanism: str = "L" + age_rule: str = "reference_year_minus_birth_year" + components: tuple[str, ...] = PRIMARY_COMPONENTS + anchor_wave: int = 2011 + + @property + def di_computation(self) -> str: + return ( + "statutory_415_b_2" if self.mechanism == "D" else "legacy_fixed_35" + ) + + @property + def statistics(self) -> tuple[str, ...]: + if self.sub_row == "cost": + return ("P", "ratio_of_scenario_means", "P_OASI", "P_DI") + return ( + "ratio_of_scenario_means", + "median_individual_pct_change", + "difference_of_weighted_medians_pct", + "baseline_falling_share", + "median_benefit_base", + "median_benefit_reform", + ) + + @property + def population(self): + return POPULATIONS[self.anchor_wave] + + def track_row(self) -> RegisteredRow: + """Supply only the legacy benefit interface, without its registry.""" + return RegisteredRow( + row_id=self.row_id, + field_changed=self.field_changed, + first_reduced_determination_year=( + self.first_reduced_determination_year + ), + exposure_clock=self.exposure_clock, + benefit_period=self.benefit_period, + components=self.components, + headline_statistic=( + "ratio_of_all_alive_weighted_totals" + if self.sub_row == "cost" + else "ratio_of_scenario_means" + ), + anchor_wave=self.anchor_wave, + ) + + def as_dict(self) -> dict[str, Any]: + return { + "row_id": self.row_id, + "sub_row": self.sub_row, + "field_changed": self.field_changed, + "first_reduced_determination_year": ( + self.first_reduced_determination_year + ), + "exposure_clock": self.exposure_clock.value, + "benefit_period": self.benefit_period.value, + "benefit_scale": "annual_12_times_monthly", + "mechanism": self.mechanism, + "di_computation": self.di_computation, + "age_rule": self.age_rule, + "components": list(self.components), + "statistics": list(self.statistics), + "population": self.population.as_dict(), + "negative_rate_policy": NegativeRatePolicy.REFUSE.value, + } + + +_C0 = ACOLARow("C0", "cost", None, 2027) +_M0 = ACOLARow("M0", "mint5", None, 2011) +REGISTERED_ROWS = { + "C0": _C0, + "C1": replace( + _C0, + row_id="C1", + field_changed="benefit_period", + benefit_period=BenefitPeriod.DECEMBER, + ), + "C0-D": replace( + _C0, row_id="C0-D", field_changed="mechanism", mechanism="D" + ), + "M0": _M0, + "M1": replace( + _M0, + row_id="M1", + field_changed="first_reduced_increase", + first_reduced_determination_year=2010, + ), + "M2": replace( + _M0, + row_id="M2", + field_changed="benefit_period", + benefit_period=BenefitPeriod.DECEMBER, + ), + "M3": replace( + _M0, + row_id="M3", + field_changed="exposure_clock", + exposure_clock=ExposureClock.ENTITLEMENT, + ), + "M4": replace( + _M0, + row_id="M4", + field_changed="age_rule", + age_rule="reference_year_minus_birth_year_minus_one", + ), +} +RUNNABLE_ROWS = tuple(k for k in REGISTERED_ROWS if k != "C0-D") +DEFERRED_ROWS = { + "C0-D": { + "status": "deferred", + "row": REGISTERED_ROWS["C0-D"].as_dict(), + "reason": ( + "Runs once Track A v2's own real-data run artifact is committed " + "to master, at a commit that a further issue #42 comment " + "registers before D runs. If run after the primary comparison, " + "label: registered before the comparator was public, run after it." + ), + } +} + + +def check_specification(path: Path = SPECIFICATION_PATH) -> dict[str, Any]: + """Bind the verbatim posted specification, without a ratification test.""" + digest = hashlib.sha256(Path(path).read_bytes()).hexdigest() + if digest != SPECIFICATION_SHA256: + raise ValueError("A-COLA specification SHA-256 differs from its pin") + return { + "path": "docs/design/acola_registration18.md", + "sha256": digest, + "registration_pointer": REGISTRATION_POINTER, + "authorization": AUTHORIZATION, + } + + +@dataclass(frozen=True) +class ACOLAConfig: + """Compose R0's projection conventions; never pass new ids to Track A.""" + + reference_year: int = 2030 + draw_indices: tuple[int, ...] = tuple(range(20)) + floor_seeds: tuple[int, ...] = (0, 1, 2, 3, 4) + rows: tuple[str, ...] = RUNNABLE_ROWS + annual_reduction: float = 0.01 + tr2008_alternative: str = "intermediate" + tr2008_first_rate_year: int = 2008 + mortality_base_year: int = 2004 + claim_table_max_year: int = 2008 + di_spec: DIEntitlementSpec = field(default_factory=DIEntitlementSpec) + claim_class: str = "track_a_reported_not_gated_psid_oracle" + oracle_cola_horizon_extension_to_2030: bool = True + di_benefit_level: LevelPolicy = LevelPolicy.DISCLOSED_ORACLE_APPROXIMATION + acceptance_rule: str | None = None + opening_stock_basis: str = OPENING_STOCK_BASIS + preeligibility_death_level: LevelPolicy = ( + LevelPolicy.DISCLOSED_ORACLE_APPROXIMATION + ) + auxiliary_entitlement_clock: AuxiliaryEntitlementClock = ( + AuxiliaryEntitlementClock.AUXILIARY_OWN + ) + survivor_entitlement_rule: SurvivorEntitlementRule = ( + SurvivorEntitlementRule.EARLIEST_ELIGIBILITY + ) + spouse_entitlement_rule: SpouseEntitlementRule = ( + SpouseEntitlementRule.OWN_CLAIM_NOT_BEFORE_WORKER + ) + opening_aged_widow_min_age: int = 60 + opening_stock_dime_floor: bool = False + + def __post_init__(self) -> None: + object.__setattr__(self, "rows", tuple(self.rows)) + if not self.rows or len(set(self.rows)) != len(self.rows): + raise ValueError("rows must be non-empty and unique") + if "C0-D" in self.rows: + raise ValueError("C0-D is registered but deferred") + if set(self.rows) - set(RUNNABLE_ROWS): + raise ValueError("unknown A-COLA row") + if self.reference_year != 2030: + raise ValueError("Registration 18 reads calendar 2030 only") + if ( + not math.isfinite(self.annual_reduction) + or self.annual_reduction < 0 + ): + raise ValueError( + "annual_reduction must be finite and non-negative" + ) + for name in ("draw_indices", "floor_seeds"): + indices = tuple(getattr(self, name)) + if ( + not indices + or any(type(k) is not int or k < 0 for k in indices) + or len(set(indices)) != len(indices) + ): + raise ValueError( + f"{name} must be unique non-negative integers" + ) + object.__setattr__(self, name, tuple(sorted(indices))) + # Track A performs the shared enum and opening-basis validation. + track = self.track_a_config() + for name in ( + "di_benefit_level", + "preeligibility_death_level", + "auxiliary_entitlement_clock", + "survivor_entitlement_rule", + "spouse_entitlement_rule", + ): + object.__setattr__(self, name, getattr(track, name)) + + @property + def anchor_waves(self) -> tuple[int, ...]: + return (2011,) + + @property + def start_years(self) -> dict[int, int]: + return {2011: 2010} + + def rows_for_wave(self, anchor_wave: int) -> tuple[str, ...]: + return self.rows if anchor_wave == 2011 else () + + def track_a_config(self) -> TrackAConfig: + values = { + f.name: getattr(self, f.name) + for f in fields(TrackAConfig) + if f.name != "rows" + } + return TrackAConfig(rows=("R0",), **values) + + def check_runnable(self) -> None: + if self.acceptance_rule is not None: + raise ValueError( + "A-COLA is report-only; no acceptance rule exists" + ) + self.track_a_config().check_runnable() + + def check_registered(self) -> None: + """Invented sensitivities never change the registered one shot.""" + self.check_runnable() + expected = ACOLAConfig() + mismatches = [ + f.name + for f in fields(self) + if getattr(self, f.name) != getattr(expected, f.name) + ] + if mismatches: + raise ValueError( + "Registration 18 settings differ: " + ", ".join(mismatches) + ) + departures = rulings_departures(self.track_a_config()) + if departures: + raise ValueError(f"carried Track A rulings differ: {departures}") + + def as_dict(self) -> dict[str, Any]: + values = self.track_a_config().as_dict() + values.update( + rows=list(self.rows), + registered_rows={ + k: row.as_dict() for k, row in REGISTERED_ROWS.items() + }, + negative_rate_policy=NegativeRatePolicy.REFUSE.value, + labels=list(LABELS), + authorization=AUTHORIZATION, + ) + return values diff --git a/src/populace_dynamics/acola_track/nulls.py b/src/populace_dynamics/acola_track/nulls.py new file mode 100644 index 000000000..15ef9b149 --- /dev/null +++ b/src/populace_dynamics/acola_track/nulls.py @@ -0,0 +1,456 @@ +"""Exact spec-only arithmetic and narrow Trustees baseline extractions. + +No real cohort is read. The context parser returns only the registered +2030 OASDI beneficiary count, with its source labels and provenance. +""" + +from __future__ import annotations + +import hashlib +import json +import math +import re +from collections.abc import Iterable +from dataclasses import dataclass +from decimal import Decimal, localcontext +from fractions import Fraction +from html.parser import HTMLParser +from pathlib import Path +from typing import Any + +from populace_dynamics.acola_track.config import ROOT +from populace_dynamics.data import tr2008 + +SNAPSHOT_DIR = ROOT / "data/external/snapshots/tr2026_v_c1_vintage1" +VC1_PATH = SNAPSHOT_DIR / "trustees2026_v_c1_table.html" +VC1_SHA256 = "4d29f2b85d67b995eea25f0bdfb46b3755671c082d75a9855680836d4b80c46a" +VC1_MANIFEST_SHA256 = ( + "fd271e0067c70737ac36e3a442c96d0b7e4650aaa14398b8383ac6e24e853abb" +) +TIMING_NOTE_SHA256 = ( + "f63274d3ef3b06c1647326490945ed05c01421466f9b0cca891bd1d0a511341b" +) +TIMING_NOTE = ( + "a Effective with benefits payable for June in each year 1975-82, " + "and for December in each year after 1982." +) +VC1_CAPTION = ( + "Table V.C1.—Cost-of-Living Benefit Increases, Average Wage Index, " + "Contribution and Benefit Bases, and Retirement Earnings Test " + "Exempt Amounts, 1975-2035" +) +VC1_COLUMN = "Cost-of-living benefit increasea (percent)" +VC1_SOURCE_URL = "https://www.ssa.gov/oact/TR/2026/V_C_prog.html" +CONTEXT_PATH = ( + ROOT + / "data/external/snapshots/ssa_level_anchors_vintage1" + / "trustees2026_lr4b4.html" +) +CONTEXT_SHA256 = ( + "40435030d154e29eb49a4e411b78253f504049eddff6e149d7e33033fb139458" +) +CONTEXT_CAPTION = ( + "Table IV.B4.—Covered Workers and Beneficiaries, Calendar Years 1945-2100" +) +CONTEXT_COLUMN = ("Beneficiaries b (in thousands)", "OASDI c") +NULLS_PATH = ROOT / "docs/design/acola_spec_only_nulls.json" + + +def _sha(raw: bytes) -> str: + return hashlib.sha256(raw).hexdigest() + + +def _verified(path: Path, expected: str) -> bytes: + raw = path.read_bytes() + if _sha(raw) != expected: + raise ValueError(f"source SHA-256 differs from pin: {path.name}") + return raw + + +def _text(chunks: list[str]) -> str: + return " ".join("".join(chunks).replace("\xa0", " ").split()) + + +@dataclass +class _Cell: + text: str + tag: str + colspan: int + rowspan: int + + +class _Tables(HTMLParser): + """Resolve literal table labels and spans without printing other cells.""" + + def __init__(self): + super().__init__(convert_charrefs=True) + self.tables: list[dict] = [] + self.stack: list[dict] = [] + + def handle_starttag(self, tag, attrs): + if tag == "table": + self.stack.append( + {"caption": None, "rows": [], "row": None, "cell": None} + ) + elif self.stack: + table = self.stack[-1] + if tag == "caption": + table["caption_chunks"] = [] + elif tag == "tr": + table["row"] = [] + elif tag in {"th", "td"} and table["row"] is not None: + table["cell"] = [] + table["tag"] = tag + table["attrs"] = dict(attrs) + elif tag == "br" and table["cell"] is not None: + table["cell"].append(" ") + + def handle_data(self, data): + if self.stack: + table = self.stack[-1] + if "caption_chunks" in table: + table["caption_chunks"].append(data) + if table["cell"] is not None: + table["cell"].append(data) + + def handle_endtag(self, tag): + if not self.stack: + return + table = self.stack[-1] + if tag == "caption" and "caption_chunks" in table: + table["caption"] = _text(table.pop("caption_chunks")) + elif tag in {"th", "td"} and table["cell"] is not None: + spans = [] + for name in ("colspan", "rowspan"): + value = table["attrs"].get(name) or "1" + if not re.fullmatch(r"[1-9]\d*", value): + raise ValueError("invalid HTML table span") + spans.append(int(value)) + table["row"].append( + _Cell(_text(table["cell"]), table["tag"], *spans) + ) + table["cell"] = None + elif tag == "tr" and table["row"] is not None: + table["rows"].append(table["row"]) + table["row"] = None + elif tag == "table": + self.tables.append(self.stack.pop()) + + +def _table(raw: bytes, caption: str) -> dict: + parser = _Tables() + parser.feed(raw.decode("utf-8")) + parser.close() + matches = [t for t in parser.tables if t["caption"] == caption] + if len(matches) != 1: + raise ValueError(f"expected exactly one literal table {caption!r}") + return matches[0] + + +def _grid(rows: list[list[_Cell]]) -> list[list[_Cell | None]]: + occupied = {} + width = 0 + for r, cells in enumerate(rows): + col = 0 + for cell in cells: + while (r, col) in occupied: + col += 1 + for dr in range(cell.rowspan): + for dc in range(cell.colspan): + key = (r + dr, col + dc) + if key in occupied: + raise ValueError("overlapping HTML spans") + occupied[key] = cell + col += cell.colspan + width = max(width, col) + return [ + [occupied.get((r, c)) for c in range(width)] for r in range(len(rows)) + ] + + +def _headers(rows: list[list[_Cell]]) -> tuple[tuple[str, ...], ...]: + paths = [] + grid = _grid(rows) + for column in range(len(grid[0])): + parts = [] + previous = None + for row in grid: + cell = row[column] + if cell is not None and cell is not previous and cell.text: + parts.append(cell.text) + previous = cell + paths.append(tuple(parts)) + return tuple(paths) + + +def _group_rows(table: dict, group: str, years: Iterable[int]): + wanted = {str(y) for y in years} + output = {} + current = None + for source, expanded in zip( + table["rows"], _grid(table["rows"]), strict=True + ): + nonempty = [c for c in source if c.text] + if len(nonempty) == 1 and nonempty[0].text.endswith(":"): + current = nonempty[0].text + if current != group: + continue + labels = [c.text for c in source if c.text in wanted] + if not labels: + continue + if len(labels) != 1 or labels[0] in output: + raise ValueError("ambiguous registered row label") + output[labels[0]] = expanded + if set(output) != wanted: + raise ValueError("registered Intermediate years missing from table") + return output + + +def extract_tr2026_rates(raw: bytes) -> dict[str, Any]: + """Read V.C1's COLA column only, over the three registered years.""" + table = _table(raw, VC1_CAPTION) + paths = _headers(table["rows"][:2]) + expected = (VC1_COLUMN,) + if paths.count(expected) != 1: + raise ValueError("V.C1 COLA column header differs") + column = paths.index(expected) + rows = _group_rows(table, "Intermediate:", (2027, 2028, 2029)) + cells = {} + for year, row in rows.items(): + cell = row[column] + if cell is None: + raise ValueError("registered COLA cell missing") + literal = cell.text + percent = Decimal(literal) + if not percent.is_finite(): + raise ValueError("nonfinite COLA input") + cells[year] = { + "as_published": literal, + "percent": str(percent), + "rate": str(percent / 100), + "row_header_path": ["Intermediate:", year], + "column_header_path": list(expected), + } + return {"table": "Table V.C1", "column": VC1_COLUMN, "cells": cells} + + +def tr2026_inputs() -> dict[str, Any]: + _verified(SNAPSHOT_DIR / "capture_manifest.txt", VC1_MANIFEST_SHA256) + raw = _verified(VC1_PATH, VC1_SHA256) + capture = json.loads((SNAPSHOT_DIR / "capture_record.json").read_bytes()) + if ( + capture["source_url"] != VC1_SOURCE_URL + or capture["caption"] != VC1_CAPTION + or capture["fragment"]["sha256"] != VC1_SHA256 + or capture["fragment"]["bytes"] != len(raw) + ): + raise ValueError( + "TR2026 capture record does not identify pinned table" + ) + timing_raw = _verified( + SNAPSHOT_DIR / "cola_timing_note.txt", TIMING_NOTE_SHA256 + ) + if timing_raw.decode("utf-8").strip() != TIMING_NOTE: + raise ValueError( + "V.C1 timing footnote differs from registered mapping" + ) + timing_record = json.loads( + (SNAPSHOT_DIR / "timing_note_record.json").read_bytes() + ) + if ( + timing_record["sha256"] != TIMING_NOTE_SHA256 + or timing_record["bytes"] != len(timing_raw) + or timing_record["source_url"] != VC1_SOURCE_URL + or timing_record["text"] != TIMING_NOTE + ): + raise ValueError("V.C1 timing footnote record differs") + return { + **extract_tr2026_rates(raw), + "source_url": VC1_SOURCE_URL, + "file": str(VC1_PATH.relative_to(ROOT)), + "sha256": VC1_SHA256, + "capture_manifest_sha256": VC1_MANIFEST_SHA256, + "retrieved_utc": capture["retrieved_utc"], + "located_from": capture["located_from"], + "timing_footnote": { + "text": TIMING_NOTE, + "file": str( + (SNAPSHOT_DIR / "cola_timing_note.txt").relative_to(ROOT) + ), + "sha256": TIMING_NOTE_SHA256, + "retrieved_utc": timing_record["retrieved_utc"], + "anchor": timing_record["anchor"], + }, + "fallback_used": False, + "column_choice": ( + "Printed COLA column, matching TR2008 V.C1; determination " + "year k is the increase effective December k." + ), + } + + +def _fraction(value: Any) -> Fraction: + if isinstance(value, float): + if not math.isfinite(value): + raise ValueError("nonfinite rate") + return Fraction(str(value)) + return Fraction(value) + + +def spec_null( + rates: Iterable[Any], annual_reduction: Any = "0.01" +) -> Fraction: + """Exact percent effect 100 * (product of registered factors - 1).""" + reduction = _fraction(annual_reduction) + if reduction < 0: + raise ValueError("annual reduction must be non-negative") + product = Fraction(1) + for value in rates: + rate = _fraction(value) + if rate <= -1 or rate - reduction < 0: + raise ValueError("REFUSE: reformed increase is negative") + product *= (1 + rate - reduction) / (1 + rate) + return 100 * (product - 1) + + +def _exact_record(value: Fraction) -> dict[str, Any]: + with localcontext() as context: + context.prec = 50 + decimal = Decimal(value.numerator) / Decimal(value.denominator) + return { + "value": float(value), + "value_decimal": str(decimal), + "exact": { + "numerator": str(value.numerator), + "denominator": str(value.denominator), + }, + } + + +def model_inputs() -> dict[str, Any]: + entries = tr2008.cola_path(2010, 2029, alternative="intermediate") + return { + "file": "data/external/tr2008/tr2008_report.json", + "sha256": _sha( + (ROOT / "data/external/tr2008/tr2008_report.json").read_bytes() + ), + "rates": { + str(e.determination_year): str(Decimal(str(e.percent)) / 100) + for e in entries + }, + "source_tags": {str(e.determination_year): e.source for e in entries}, + "basis": ( + "TR2008 V.C1 printed COLAs through 2017; Table II.C1 ultimate " + "annual-average CPI from 2018, matching the runtime model path." + ), + } + + +def build_nulls(*, annual_reduction: Any = "0.01", baseline=None) -> dict: + inputs = tr2026_inputs() + model = model_inputs() + rates = dict(model["rates"]) + if baseline is not None: + rates = { + str(y): str(baseline.rate_for_determination_year(y)) + for y in range(2010, 2030) + } + model = { + **model, + "rates": rates, + "runtime_matches_committed_tr2008": all( + _fraction(rates[y]) == _fraction(model["rates"][y]) + for y in rates + ), + } + specifications = { + "N": (range(2027, 2030), inputs, "TR2026"), + "N_model": (range(2027, 2030), model, "model_TR2008"), + "N_MINT": (range(2011, 2030), model, "model_TR2008"), + "N_M1": (range(2010, 2030), model, "model_TR2008"), + } + output = { + "schema_version": "populace_dynamics.acola_track.spec_nulls.v1", + "annual_reduction": str(annual_reduction), + "inputs": {"TR2026": inputs, "model_TR2008": model}, + "december_null_registered": False, + "december_note": ( + "C1 and M2 include December2030; the registered nulls stop " + "at determination year2029. No December null is computed." + ), + } + exact = {} + for name, (years, source, source_key) in specifications.items(): + years = list(years) + values = { + str(y): ( + source["cells"][str(y)]["rate"] + if source_key == "TR2026" + else rates[str(y)] + ) + for y in years + } + exact[name] = spec_null(values.values(), annual_reduction) + output[name] = { + **_exact_record(exact[name]), + "years": years, + "factor_count": len(years), + "rates": values, + "source": source_key, + } + output["N_minus_N_model"] = _exact_record(exact["N"] - exact["N_model"]) + return output + + +def nulls_for_run(baseline, annual_reduction: Any = "0.01") -> dict: + """Recompute using the runtime baseline; retain a pre-run source record.""" + committed = json.loads(NULLS_PATH.read_bytes()) + expected = build_nulls() + if committed != expected: + raise ValueError("committed spec-only nulls differ from their inputs") + result = build_nulls(annual_reduction=annual_reduction, baseline=baseline) + result["committed_file"] = str(NULLS_PATH.relative_to(ROOT)) + result["committed_file_sha256"] = _sha(NULLS_PATH.read_bytes()) + return result + + +def extract_context_count(raw: bytes) -> dict[str, Any]: + """Extract exactly one beneficiary count; return no other source values.""" + table = _table(raw, CONTEXT_CAPTION) + # The committed table has three header rows, ending before Historical. + header_rows = [] + for row in table["rows"]: + if any(c.text == "Historical data:" for c in row): + break + header_rows.append(row) + paths = _headers(header_rows) + if paths.count(CONTEXT_COLUMN) != 1: + raise ValueError("IV.B4 OASDI beneficiary column header differs") + selected = _group_rows(table, "Intermediate:", (2030,))["2030"] + cell = selected[paths.index(CONTEXT_COLUMN)] + if cell is None or not re.fullmatch(r"\d[\d,]*", cell.text): + raise ValueError("IV.B4 OASDI beneficiary count is not an integer") + thousands = int(cell.text.replace(",", "")) + return { + "beneficiary_count": thousands * 1000, + "published_count_thousands": thousands, + "as_published": cell.text, + "table": "Table IV.B4", + "row_header_path": ["Intermediate:", "2030"], + "column_header_path": list(CONTEXT_COLUMN), + "published_unit": "thousand_persons", + "stored_unit": "persons", + "scale_multiplier": 1000, + "context_only": True, + "scaling_applied_to_model": False, + } + + +def context_beneficiary_count() -> dict[str, Any]: + raw = _verified(CONTEXT_PATH, CONTEXT_SHA256) + return { + **extract_context_count(raw), + "source_url": "https://www.ssa.gov/oact/TR/2026/lr4b4.html", + "file": str(CONTEXT_PATH.relative_to(ROOT)), + "sha256": CONTEXT_SHA256, + } diff --git a/src/populace_dynamics/acola_track/runner.py b/src/populace_dynamics/acola_track/runner.py new file mode 100644 index 000000000..df3623119 --- /dev/null +++ b/src/populace_dynamics/acola_track/runner.py @@ -0,0 +1,447 @@ +"""Registration 18's model side, using the unchanged mechanism L.""" + +from __future__ import annotations + +import copy +import math +from collections import Counter, defaultdict +from collections.abc import Callable +from pathlib import Path +from typing import Any + +import pandas as pd + +from populace_dynamics import scenario_benefits as sb +from populace_dynamics.cola_track_a import runner as legacy +from populace_dynamics.cola_track_a.adapters import claiming_schedule +from populace_dynamics.cola_track_a.benefits import ( + BenefitContext, + StateLookups, + reference_benefit_rows, +) +from populace_dynamics.cola_track_a.config import ( + builder_defaults, + max_rulings, + rulings_departures, +) +from populace_dynamics.engine.di_entitlement import ( + fra_schedule_from_parameters, +) +from populace_dynamics.estimates.cola_age_profile import REGISTERED_REAL + +from .config import ( + AUTHORIZATION, + DEFERRED_ROWS, + REGISTERED_ROWS, + REGISTRATION_POINTER, + SPECIFICATION_PATH, + ACOLAConfig, + check_specification, +) +from .nulls import context_beneficiary_count, nulls_for_run +from .statistics import ( + summarize_cost, + summarize_mint, +) + +SCHEMA_VERSION = "populace_dynamics.acola_track.run.v1" +MEMBERSHIP_NOTE = ( + "Under mechanism L S_base and S_reform are equal by construction: " + "the legacy emitter retains positive baseline benefits, refuses a " + "nonpositive reform total, and sets both beneficiary flags true. " + "Equal counts are not evidence of independently equal membership." +) +GAPS = ( + { + "key": "statutory_rounding", + "description": ( + "Worker PIA paths floor to dimes after increases and again " + "after the claim-age factor. The common eligibility AIME " + "uses dollar rounding. A worker's cut bound uses a " + "benefit-level-dependent allowance for propagated dime " + "losses and the final dime floor, not a fixed percentage " + "tolerance. Dual entitlement has a separate structural " + "exception. No rounding adjustment is applied to results." + ), + "source": ( + "scenario_benefits.increased_pia_path, monthly_benefit_path; " + "ss.benefits.aime; ss.statutory_aime" + ), + "adjustment_applied": False, + }, + { + "key": "dual_entitlement_bounds_and_monotonicity", + "intended_violation": True, + "description": ( + "Mechanism L subtracts the full own increased PIA from the " + "spouse excess while an early own retirement benefit carries " + "a smaller claim-age factor. If the spouse excess begins " + "later and carries fewer reduced COLAs, its increase can " + "exceed the loss of the own benefit. Individual and aggregate " + "benefits can therefore rise under a cut, violating the " + "intended full-null-to-zero bounds and monotonicity. This is " + "a structural effect, so no fixed rounding tolerance is " + "asserted. The shared registered arithmetic is unchanged." + ), + "source": ( + "cola_track_a/benefits.py:_spouse_excess; " + "scenario_benefits.py:spouse_excess_path; " + "ss/benefits.py:spousal_benefit" + ), + }, + { + "key": "reform_only_auxiliary_excess", + "description": ( + "Mechanism L drops spouse and widow excesses whose baseline " + "is zero even when the reform excess is positive. This " + "pushes P away from zero; the registered arithmetic is kept." + ), + "source": "cola_track_a/benefits.py:_spouse_excess,_widow_excess", + "counter": None, + "undefined_reason": ( + "The public row emitter exposes neither the dropped excess " + "nor a counter; counting would require copying private " + "benefit arithmetic or editing a shared module." + ), + }, + { + "key": "annual_convention_omissions", + "description": ( + "The annual convention omits family maximums, earnings " + "tests, recomputation and payment-month timing." + ), + "source": "scenario_benefits.py module annual-convention note", + }, + { + "key": "closed_cohort", + "description": ( + "The R0 population is closed, has no children or post-2010 " + "immigrants and draws no post-2010 earnings. Disabled " + "widow(er)s enter only in the opening stock. The context " + "beneficiary count does not scale the row." + ), + "source": "Registration 18 Population and Adjustments", + }, + { + "key": "benefits_only_cost", + "description": ( + "P covers scheduled benefits only. Comparator cost " + "definitions and their administrative-expense and Railroad " + "Retirement interchange flags are reported by the comparison " + "lane; an unstated flag gives unsigned dilution. Income-rate " + "changes are comparator memo items, not this cost statistic." + ), + "source": "Registration 18 Adjustments", + }, + { + "key": "baseline_vintage", + "description": ( + "The model uses TR 2008 paths; OACT uses TR 2026, PWBM its " + "own baseline and MINT5 TR 2008. N minus N_model records the " + "COLA-rate vintage delta without adjusting any result." + ), + "source": "Registration 18 Adjustments", + }, + { + "key": "december_null", + "description": ( + "C1 and M2 include the increase effective December 2030. " + "The registered nulls stop at determination year 2029; " + "no December null is registered or computed." + ), + "source": "Builder brief ruling 11", + }, +) + + +def _check_floor(inputs: legacy.TrackAInputs, config: ACOLAConfig) -> dict: + """Require positive reduced increases through the December horizon.""" + + minima = {} + for row_id in config.rows: + row = REGISTERED_ROWS[row_id] + reform = sb.COLAReform( + annual_reduction=config.annual_reduction, + first_reduced_determination_year=( + row.first_reduced_determination_year + ), + ) + minimum = sb.minimum_reformed_rate( + inputs.baseline, + reform, + through_determination_year=config.reference_year, + ) + if not minimum > 0: + raise ValueError( + f"{row_id}: reduced rate {minimum!r}; REFUSE requires " + "the preflight's strictly positive floor through 2030" + ) + minima[row_id] = minimum + return minima + + +def spouse_worker_class(person_id: int, lookups: StateLookups) -> str: + """Classify the living 2030 spouse link, including opening spouses.""" + + state = lookups.final.loc[person_id] + spouse = state.get("spouse_person_id") + if pd.isna(spouse) or not lookups.alive(int(spouse)): + return "unclassified" + worker = lookups.final.loc[int(spouse)] + entitled = worker.get("di_entitled", False) + if not pd.isna(entitled) and bool(entitled): + if pd.isna(worker.get("di_conversion_year")): + return "entitled_unconverted_di_worker" + return "another_worker" + + +def _shifted_summary(summary: dict, null: dict) -> dict: + shifted = copy.deepcopy(summary) + shifted["mean"] = ( + None if summary["mean"] is None else summary["mean"] - null["value"] + ) + shifted["per_draw"] = [ + None if value is None else value - null["value"] + for value in summary["per_draw"] + ] + shifted["null_value"] = null["value"] + shifted["unit"] = "percentage points" + return shifted + + +def _add_null_gaps(summary: dict, row: Any, nulls: dict) -> None: + """Shift percent statistics only; levels and shares remain memo items.""" + + if row.sub_row == "cost": + summary["null_gaps"] = { + "P_minus_N": _shifted_summary( + summary["statistics"]["P"], nulls["N"] + ), + "P_minus_N_model": _shifted_summary( + summary["statistics"]["P"], nulls["N_model"] + ), + } + else: + null_key = "N_M1" if row.row_id == "M1" else "N_MINT" + summary["registered_null"] = null_key + for band in summary["bands"].values(): + band["null_gaps"] = { + statistic + + "_minus_" + + null_key: _shifted_summary( + band["statistics"][statistic], nulls[null_key] + ) + for statistic in ( + "ratio_of_scenario_means", + "median_individual_pct_change", + "difference_of_weighted_medians_pct", + ) + } + + +def run_acola( + inputs: legacy.TrackAInputs, + *, + config: ACOLAConfig | None = None, + registration_pointer: str | None = None, + progress: Callable[[str], None] | None = None, + check_committed_parameters: bool = False, + specification_path: Path = SPECIFICATION_PATH, +) -> dict[str, Any]: + """Project each draw once, then publish every mechanism-L row.""" + + config = config or ACOLAConfig() + config.check_runnable() + track_config = config.track_a_config() + cohorts = legacy._population_cohorts(inputs, track_config) + data_provenance = next(iter(cohorts.values())).data_provenance + labels = tuple(next(iter(cohorts.values())).labels) + real = data_provenance == REGISTERED_REAL + if real: + if registration_pointer != REGISTRATION_POINTER: + raise ValueError( + "a registered_real run needs Registration 18's pointer" + ) + config.check_registered() + departures = rulings_departures(track_config) + if departures: + raise ValueError( + f"registered run departs from Track A rulings: {departures}" + ) + specification = check_specification(specification_path) + minima = _check_floor(inputs, config) + consistency = legacy._parameter_consistency( + inputs, + track_config, + cohorts, + compare_committed_values=real or check_committed_parameters, + ) + if (real or check_committed_parameters) and not consistency["consistent"]: + failed = sorted( + name + for name, check in consistency["checks"].items() + if not check["consistent"] + ) + raise ValueError( + f"registered parameters differ from committed inputs: {failed}" + ) + legacy._check_source_provenance(cohorts, data_provenance) + legacy._check_output_labels(labels, data_provenance) + nulls = nulls_for_run( + inputs.baseline, annual_reduction=config.annual_reduction + ) + context_count = context_beneficiary_count() + schedule = claiming_schedule( + inputs.claiming_pmf, max_table_year=track_config.claim_table_max_year + ) + fra_schedule = fra_schedule_from_parameters(inputs.params) + records = {key: [] for key in config.rows} + counters = {key: Counter() for key in config.rows} + refusals: dict[str, list[dict]] = defaultdict(list) + weights: dict[int, set[float]] = defaultdict(set) + draws = {} + for wave, cohort in cohorts.items(): + results, draws[str(wave)] = legacy._project_population( + cohort, + inputs, + track_config, + schedule=schedule, + fra_schedule=fra_schedule, + progress=progress, + ) + if set(results) != set(config.draw_indices): + raise ValueError("projection omitted a configured draw") + context = BenefitContext( + cohort, inputs.params, inputs.baseline, track_config + ) + pia_cache: dict = {} + for draw in config.draw_indices: + result = results[draw] + lookups = StateLookups(result, config.reference_year) + for person in lookups.final.itertuples(index=False): + weights[int(person.person_id)].add(float(person.weight)) + for row_id in config.rows: + row = REGISTERED_ROWS[row_id] + if progress is not None: + progress( + f"anchor wave {wave}, draw {draw}, row {row_id}: benefits" + ) + try: + emitted, row_counters = reference_benefit_rows( + result, + draw=draw, + row=row.track_row(), + context=context, + pia_cache=pia_cache, + lookups=lookups, + ) + except ValueError as error: + refusals[row_id].append( + {"draw": draw, "reason": str(error)} + ) + continue + for record in emitted: + record["spouse_worker_class"] = spouse_worker_class( + int(record["person_id"]), lookups + ) + records[row_id].extend(emitted) + counters[row_id].update(row_counters) + rows = {} + output_labels = [*labels, "report-only"] + for row_id in config.rows: + row = REGISTERED_ROWS[row_id] + options = { + "draw_indices": config.draw_indices, + "floor_seeds": config.floor_seeds, + "certified_draw_indices": config.draw_indices, + "refused_draws": { + entry["draw"]: entry["reason"] for entry in refusals[row_id] + }, + } + if row.sub_row == "cost": + summary = summarize_cost(records[row_id], **options) + else: + summary = summarize_mint( + records[row_id], + reference_year=config.reference_year, + age_rule=row.age_rule, + **options, + ) + status = "refused" if refusals[row_id] else "computed" + _add_null_gaps(summary, row, nulls) + rows[row_id] = { + "status": status, + "labels": output_labels, + "row": row.as_dict(), + "benefit_counters": dict(sorted(counters[row_id].items())), + "refusals": refusals[row_id], + "membership_note": MEMBERSHIP_NOTE, + **summary, + } + cost_cells = rows.get("C0", {}).get("per_draw", []) + model_counts = [ + { + "draw": cell["draw"], + "weighted_count": cell.get("weight_base"), + "undefined_reason": cell.get("undefined_reasons", {}).get( + "weight_base" + ), + } + for cell in cost_cells + ] + complete_counts = [cell["weighted_count"] for cell in model_counts] + model_mean = ( + math.fsum(complete_counts) / len(complete_counts) + if complete_counts + and all(value is not None for value in complete_counts) + else None + ) + return { + "schema_version": SCHEMA_VERSION, + "publishes_regardless": True, + "comparator_seal_opened_before_commit": False, + "data_provenance": data_provenance, + "registration_pointer": registration_pointer, + "labels": output_labels, + "config": config.as_dict(), + "specification": specification, + "authorization": copy.deepcopy(AUTHORIZATION), + "carried_track_a_rulings": max_rulings(track_config), + "carried_track_a_builder_defaults": builder_defaults(track_config), + "deferred_rows": copy.deepcopy(DEFERRED_ROWS), + "reduced_rate_minimum_by_row": minima, + "parameter_consistency": consistency, + "cohorts": { + str(wave): { + **dict(cohort.diagnostics), + "source_provenance": dict(cohort.source_provenance), + } + for wave, cohort in cohorts.items() + }, + "scheduled_entrants": 0, + "population_mortality": legacy._mortality_record( + inputs.population_mortality + ), + "ssa_parameters_revision": inputs.params.pe_us_revision, + "draws": draws, + "rows": rows, + "nulls": nulls, + "context_beneficiary_count": { + "context_only": True, + "scaling_applied": False, + "model_per_draw": model_counts, + "model_mean": model_mean, + "undefined_reason": ( + None if model_mean is not None else "C0 unavailable" + ), + "trustees": context_count, + }, + "projection_weights": { + "n_persons_weight_varies_across_draws": sum( + len(values) > 1 for values in weights.values() + ), + "population": "all persons alive in at least one 2030 draw", + }, + "gaps": copy.deepcopy(list(GAPS)), + "inputs_provenance": dict(inputs.provenance), + } diff --git a/src/populace_dynamics/acola_track/statistics.py b/src/populace_dynamics/acola_track/statistics.py new file mode 100644 index 000000000..7a377fd04 --- /dev/null +++ b/src/populace_dynamics/acola_track/statistics.py @@ -0,0 +1,601 @@ +"""Registration 18 statistics on invented or registered person-level rows. + +The A7 normalizer, recipient masks, scenario means, draw summaries and +family-unit splitter are reused unchanged. Its public tabulator is bypassed +so it cannot publish an unregistered mean of individual ratios. The median +uses exact binary-float weights and the registered midpoint rule. +""" + +from __future__ import annotations + +import math +from collections.abc import Iterable, Mapping, Sequence +from dataclasses import replace +from fractions import Fraction +from typing import Any + +import numpy as np +import pandas as pd + +from populace_dynamics.estimates import cola_age_profile as a7 + +P = "P" +P_OASI = "P_OASI" +P_DI = "P_DI" +RATIO_OF_SCENARIO_MEANS = a7.RATIO_OF_SCENARIO_MEANS +MEDIAN_INDIVIDUAL_PCT_CHANGE = "median_individual_pct_change" +DIFFERENCE_OF_WEIGHTED_MEDIANS_PCT = "difference_of_weighted_medians_pct" +BASELINE_FALLING_SHARE = "baseline_falling_share" +MEDIAN_BENEFIT_BASE = "median_benefit_base" +MEDIAN_BENEFIT_REFORM = "median_benefit_reform" +COST_STATISTICS = (P, P_OASI, P_DI, RATIO_OF_SCENARIO_MEANS) +MINT_STATISTICS = ( + RATIO_OF_SCENARIO_MEANS, + MEDIAN_INDIVIDUAL_PCT_CHANGE, + DIFFERENCE_OF_WEIGHTED_MEDIANS_PCT, + BASELINE_FALLING_SHARE, + MEDIAN_BENEFIT_BASE, + MEDIAN_BENEFIT_REFORM, +) +MINT_BANDS = ( + ("Total", 62, None), + ("62-69", 62, 69), + ("70-79", 70, 79), + ("80-89", 80, 89), + ("90+", 90, None), +) +SPOUSE_WORKER_CLASSES = ( + "entitled_unconverted_di_worker", + "another_worker", + "unclassified", +) +OASI_COMPONENTS = tuple( + c for c in a7.PRIMARY_COMPONENTS if c != "disabled_worker" +) +INTENDED_BOUND_VIOLATIONS = ( + { + "mechanism": "L", + "cause": "dual-entitlement spouse excess with different COLA clocks", + "behavior": ( + "A claimant's own early retirement benefit and a later spouse " + "excess have different age factors. The excess subtracts the " + "full own PIA. Reducing that PIA can raise the combined benefit " + "when the own clock carries more reduced increases than the " + "linked worker's clock. Individual percent changes and P can " + "exceed zero; larger cuts or earlier starts can raise them." + ), + "tolerance": ( + "This is a structural exception, not a rounding tolerance. " + "No universal [N_full, 0] or monotonic cut bound is asserted." + ), + "source": ( + "scenario_benefits.spouse_excess_path; " + "ss.benefits.spousal_benefit; cola_track_a.benefits._spouse_excess" + ), + "adjustment_applied": False, + }, +) + + +def weighted_median( + values: Sequence[float], + weights: Sequence[float], + person_ids: Sequence[int | str] | None = None, +) -> float | None: + """Exact-W/2 midpoint median, excluding zero-weight observations. + + Fraction(float(weight)) preserves the supplied binary float exactly. + Equal values sort stably by person id. An empty positive-weight sample + returns None; callers report the reason and zero-weight exclusions. + """ + if len(values) != len(weights): + raise a7.ColaTabulationError("values and weights differ in length") + ids = list(range(len(values))) if person_ids is None else list(person_ids) + if len(ids) != len(values): + raise a7.ColaTabulationError("person_ids differ in length") + if len({type(pid) for pid in ids}) > 1: + raise a7.ColaTabulationError("person_ids must have one key type") + atoms = [] + for value, weight, pid in zip(values, weights, ids, strict=True): + value = a7._finite(float(value), "a median value") + weight = a7._finite(float(weight), "a median weight") + if weight < 0: + raise a7.ColaTabulationError("median weights must be nonnegative") + if weight > 0: + atoms.append((value, pid, Fraction(weight))) + if not atoms: + return None + atoms.sort(key=lambda atom: (atom[0], atom[1])) + total = sum((atom[2] for atom in atoms), Fraction(0)) + cumulative = Fraction(0) + for index, (value, _, weight) in enumerate(atoms): + cumulative += weight + if 2 * cumulative == total: + # All remaining weights are positive, so a next atom exists. + return a7._finite( + float((Fraction(value) + Fraction(atoms[index + 1][0])) / 2), + "the midpoint weighted median", + ) + if 2 * cumulative > total: + return value + raise AssertionError("positive weights must cross the median") + + +def _empty_rows() -> a7._Rows: + """An emitted row can have no beneficiaries in any configured draw.""" + ints = np.array([], dtype=np.int64) + floats = np.array([], dtype=np.float64) + booleans = np.array([], dtype=bool) + return a7._Rows( + draw=ints, + person_id=np.array([], dtype=object), + family_unit_id=np.array([], dtype=object), + weight=floats, + birth_year=ints, + age=ints, + group=ints, + selected_base=floats, + selected_reform=floats, + recipient_base=booleans, + recipient_reform=booleans, + flagged_zero_base=0, + flagged_zero_reform=0, + n_persons_weight_varies=0, + extra_columns=(), + ) + + +def _prepare( + records, + draw_indices, + floor_seeds, + reference_year, + age_rule, + certified_draw_indices=None, +): + config = a7.ColaAgeProfileConfig( + reference_year=reference_year, + age_rule=age_rule, + age_groups=(a7.AgeGroup("all", 0, None),), + draw_indices=tuple(draw_indices), + floor_seeds=tuple(floor_seeds), + allow_membership_difference=True, + membership_basis=a7.SCENARIO_SPECIFIC, + ) + records, _ = a7._records(records) + normalize_config = config + if certified_draw_indices is not None: + certified = tuple(sorted(certified_draw_indices)) + if certified != config.draw_indices: + raise a7.ColaTabulationError( + "certified projection draw inventory differs from configured draws" + ) + present = tuple( + sorted({a7._int_value(r["draw"], "draw") for r in records}) + ) + if not set(present) <= set(certified): + raise a7.ColaTabulationError("rows contain an uncertified draw") + if present: + normalize_config = replace(config, draw_indices=present) + rows = ( + a7._normalize(records, normalize_config) if records else _empty_rows() + ) + if np.any(rows.group < 0): + raise a7.ColaTabulationError("a person lies outside the all-ages cell") + # A7 validates ids before they are sorted. Every output and summation + # thereafter sees the same draw/person order, independent of input order. + order = sorted( + range(rows.n), key=lambda i: (int(rows.draw[i]), rows.person_id[i]) + ) + records = [records[i] for i in order] + arrays = { + name: getattr(rows, name)[order] + for name in ( + "draw", + "person_id", + "family_unit_id", + "weight", + "birth_year", + "age", + "group", + "selected_base", + "selected_reform", + "recipient_base", + "recipient_reform", + ) + } + return records, replace(rows, **arrays), config + + +def _scenario_cell(rows, selected, draw): + cell = a7._cell( + rows, + selected, + a7._membership_masks(rows, a7.SCENARIO_SPECIFIC), + draw, + ) + # A7 computes this internally; it is not registered for A-COLA. + cell.pop(a7.MEAN_OF_INDIVIDUAL_RATIOS) + cell["undefined_reasons"].pop(a7.MEAN_OF_INDIVIDUAL_RATIOS, None) + intermediate = {"undefined_reasons": {}} + for scenario in ("base", "reform"): + key = f"mean_benefit_{scenario}" + intermediate[key] = cell.pop(key) + if intermediate[key] is None: + intermediate["undefined_reasons"][ + key + ] = f"empty_{scenario}_membership" + cell["not_registered"] = intermediate + return cell + + +def _percent(total_base, total_reform, key, reasons): + if total_base <= 0: + reasons[key] = "nonpositive_baseline_total" + return None + # Match v2's arithmetic order, rather than subtracting totals first. + return a7._finite(100 * (total_reform / total_base - 1), key) + + +def _component_matrix(records): + amounts = np.zeros((len(records), len(a7.PRIMARY_COMPONENTS), 2)) + for i, record in enumerate(records): + parsed = a7._parse_components(record["benefit_components"], i) + for j, component in enumerate(a7.PRIMARY_COMPONENTS): + if component in parsed: + amounts[i, j] = parsed[component] + return amounts + + +def _cost_cell(rows, components, spouse_classes, selected, draw): + cell = _scenario_cell(rows, selected, draw) + totals = { + c: { + scenario: a7._fsum( + rows.weight[selected] * components[selected, j, s] + ) + for s, scenario in enumerate(("base", "reform")) + } + for j, c in enumerate(a7.PRIMARY_COMPONENTS) + } + total_base = a7._fsum(rows.weight[selected] * rows.selected_base[selected]) + total_reform = a7._fsum( + rows.weight[selected] * rows.selected_reform[selected] + ) + reasons = cell["undefined_reasons"] + cell[P] = _percent(total_base, total_reform, P, reasons) + group_shares = {} + for label, chosen, key in ( + ("OASI", OASI_COMPONENTS, P_OASI), + ("DI", ("disabled_worker",), P_DI), + ): + base = math.fsum(totals[c]["base"] for c in chosen) + reform = math.fsum(totals[c]["reform"] for c in chosen) + cell[key] = _percent(base, reform, key, reasons) + group_shares[label] = base / total_base if total_base > 0 else None + if total_base <= 0: + reasons[f"group_share_{label}"] = "nonpositive_baseline_total" + spouse = components[:, a7.PRIMARY_COMPONENTS.index("spouse"), :] + spouse_memo = {} + for label in SPOUSE_WORKER_CLASSES: + memo = {} + for s, scenario in enumerate(("base", "reform")): + mask = selected & (spouse_classes == label) & (spouse[:, s] > 0) + memo[f"n_{scenario}"] = int(np.count_nonzero(mask)) + memo[f"weighted_count_{scenario}"] = a7._fsum(rows.weight[mask]) + memo[f"dollars_{scenario}"] = a7._fsum( + rows.weight[mask] * spouse[mask, s] + ) + spouse_memo[label] = memo + cell.update( + T_base=total_base, + T_reform=total_reform, + component_totals=totals, + group_shares=group_shares, + spouse_worker_classes=spouse_memo, + ) + return cell + + +def _mint_cell(rows, selected, draw): + cell = _scenario_cell(rows, selected, draw) + base, reform, alt = ( + selected & mask + for mask in a7._membership_masks(rows, a7.SCENARIO_SPECIFIC) + ) + reasons = cell["undefined_reasons"] + for key, mask, values, reason in ( + ( + MEDIAN_BENEFIT_BASE, + base, + rows.selected_base, + "empty_positive_weight_baseline_membership", + ), + ( + MEDIAN_BENEFIT_REFORM, + reform, + rows.selected_reform, + "empty_positive_weight_reform_membership", + ), + ): + cell[key] = weighted_median( + values[mask], rows.weight[mask], rows.person_id[mask] + ) + if cell[key] is None: + reasons[key] = reason + changes = 100 * (rows.selected_reform[alt] / rows.selected_base[alt] - 1) + cell[MEDIAN_INDIVIDUAL_PCT_CHANGE] = weighted_median( + changes, rows.weight[alt], rows.person_id[alt] + ) + if cell[MEDIAN_INDIVIDUAL_PCT_CHANGE] is None: + reasons[MEDIAN_INDIVIDUAL_PCT_CHANGE] = ( + "empty_positive_weight_alternative_membership" + ) + mb, mr = cell[MEDIAN_BENEFIT_BASE], cell[MEDIAN_BENEFIT_REFORM] + if mb is None or mr is None: + cell[DIFFERENCE_OF_WEIGHTED_MEDIANS_PCT] = None + reasons[DIFFERENCE_OF_WEIGHTED_MEDIANS_PCT] = ( + "baseline_or_reform_median_undefined" + ) + else: + cell[DIFFERENCE_OF_WEIGHTED_MEDIANS_PCT] = _percent( + mb, mr, DIFFERENCE_OF_WEIGHTED_MEDIANS_PCT, reasons + ) + falls = base & (rows.selected_reform < rows.selected_base) + falling_weight = a7._fsum(rows.weight[falls]) + if cell["weight_base"] > 0: + cell[BASELINE_FALLING_SHARE] = falling_weight / cell["weight_base"] + else: + cell[BASELINE_FALLING_SHARE] = None + reasons[BASELINE_FALLING_SHARE] = "empty_baseline_membership" + cell["n_baseline_falling"] = int(np.count_nonzero(falls)) + cell["weight_baseline_falling"] = falling_weight + cell["median_zero_weight_exclusions"] = { + key: int(np.count_nonzero(mask & (rows.weight == 0))) + for key, mask in (("base", base), ("reform", reform), ("alt", alt)) + } + return cell + + +def _summary(cells, statistic): + summary = a7._draw_summary(cells, statistic) + summary["sd"] = summary["sample_sd"] + summary["undefined_reason"] = ( + None if summary["defined"] else "one_or_more_undefined_draws" + ) + summary["undefined_reasons"] = {} + if summary["sample_sd"] is None: + summary["undefined_reasons"]["sample_sd"] = ( + summary["undefined_reason"] or "fewer_than_two_draws" + ) + summary["undefined_reasons"]["sd"] = summary["undefined_reasons"][ + "sample_sd" + ] + return summary + + +def _refused_cell(template, reason): + """Keep the complete memo shape without calling unknown amounts zero.""" + reasons = {} + + def unknown(value, path): + if isinstance(value, dict): + return { + key: unknown(nested, f"{path}.{key}" if path else key) + for key, nested in value.items() + } + if isinstance(value, int | float) or value is None: + reasons[path] = reason + return None + return value + + cell = { + key: unknown(value, key) + for key, value in template.items() + if key not in ("draw", "undefined_reasons") + } + cell["draw"] = template["draw"] + cell["undefined_reasons"] = reasons + intermediate = cell.get("not_registered") + if intermediate is not None: + intermediate["undefined_reasons"] = { + key: reason + for key, value in intermediate.items() + if key != "undefined_reasons" and value is None + } + cell["status"] = "refused" + cell["refusal_reason"] = reason + return cell + + +def _summarize(rows, config, bands, statistics, cell_fn, refused_draws=None): + refused = dict(refused_draws or {}) + if not set(refused) <= set(config.draw_indices): + raise a7.ColaTabulationError( + "refused_draws contain an unconfigured draw" + ) + if any( + not isinstance(reason, str) or not reason + for reason in refused.values() + ): + raise a7.ColaTabulationError( + "every refused draw needs a nonempty reason" + ) + subsets = { + label: (rows.age >= lower) + & (np.ones(rows.n, dtype=bool) if upper is None else rows.age <= upper) + for label, lower, upper in bands + } + + def evaluate(selected, draw): + if draw in refused: + empty_cell = cell_fn(np.zeros(rows.n, dtype=bool), draw) + return _refused_cell(empty_cell, refused[draw]) + return cell_fn(selected, draw) + + def profile(subset): + return { + label: [ + evaluate(subset & mask & (rows.draw == draw), draw) + for draw in config.draw_indices + ] + for label, mask in subsets.items() + } + + full = profile(np.ones(rows.n, dtype=bool)) + output = { + label: { + "per_draw": cells, + "statistics": {key: _summary(cells, key) for key in statistics}, + "floor_per_seed": [], + } + for label, cells in full.items() + } + unit_frame = pd.DataFrame({a7.FAMILY_UNIT: rows.family_unit_id.tolist()}) + for seed in config.floor_seeds: + in_a = np.zeros(rows.n, dtype=bool) + if rows.n: + side, _ = a7.split_panel_by_person( + unit_frame, a7.FAMILY_UNIT, fraction=0.5, seed=seed + ) + in_a[side.index.to_numpy()] = True + side_a, side_b = profile(in_a), profile(~in_a) + for label in output: + a = {key: _summary(side_a[label], key) for key in statistics} + b = {key: _summary(side_b[label], key) for key in statistics} + gaps, reasons = {}, {} + for key in statistics: + ma, mb = a[key]["mean"], b[key]["mean"] + gaps[key] = None if ma is None or mb is None else abs(ma - mb) + if gaps[key] is None: + reasons[key] = "one_or_both_half_sample_means_undefined" + output[label]["floor_per_seed"].append( + { + "seed": seed, + "side_a": a, + "side_b": b, + "absolute_gaps": gaps, + "undefined_reasons": reasons, + } + ) + for band in output.values(): + for key in statistics: + usable, dropped = [], [] + for seed in band["floor_per_seed"]: + gap = seed["absolute_gaps"][key] + if gap is None: + dropped.append(seed["seed"]) + else: + usable.append(gap) + band["statistics"][key]["floor"] = { + **a7._floor_summary(usable), + "dropped_seeds": dropped, + } + return output + + +def _input_summary(rows): + return { + "n_rows": rows.n, + "n_persons": len(set(rows.person_id)), + "n_family_units": len(set(rows.family_unit_id)), + "n_persons_weight_varies_across_draws": rows.n_persons_weight_varies, + "extra_columns_ignored_by_a7": list(rows.extra_columns), + "zero_weight_rows": int(np.count_nonzero(rows.weight == 0)), + "membership_basis": a7.SCENARIO_SPECIFIC, + "recipient_rule": a7.POSITIVE_BENEFIT, + "median_weight_arithmetic": "exact Fraction of supplied float", + "median_tie_sort": "value ascending, then person_id ascending", + "median_zero_weight_rule": "exclude and report counts", + "floor_split_population": "entire normalized row table", + "floor_split_rule": "Bernoulli(0.5), sorted opening family ids", + } + + +def summarize_cost( + records: pd.DataFrame | Iterable[Mapping[str, Any]], + *, + draw_indices: Sequence[int], + floor_seeds: Sequence[int] = a7.DEFAULT_FLOOR_SEEDS, + certified_draw_indices: Sequence[int] | None = None, + refused_draws: Mapping[int, str] | None = None, +) -> dict[str, Any]: + """All-ages totals, split, spouse memo, draw summaries and A7 floors.""" + records, rows, config = _prepare( + records, + draw_indices, + floor_seeds, + 2030, + "reference_year_minus_birth_year", + certified_draw_indices, + ) + components = _component_matrix(records) + spouse_classes = np.array( + [r.get("spouse_worker_class", "unclassified") for r in records], + dtype=object, + ) + if any(c not in SPOUSE_WORKER_CLASSES for c in spouse_classes): + raise a7.ColaTabulationError("unknown spouse_worker_class") + output = _summarize( + rows, + config, + (("all", 0, None),), + COST_STATISTICS, + lambda selected, draw: _cost_cell( + rows, components, spouse_classes, selected, draw + ), + refused_draws, + )["all"] + output["input_summary"] = _input_summary(rows) + output["certified_projection_draws"] = ( + list(config.draw_indices) + if certified_draw_indices is not None + else None + ) + output["statistic_additions"] = [P_OASI, P_DI, RATIO_OF_SCENARIO_MEANS] + output["intended_bound_violations"] = [ + dict(item) for item in INTENDED_BOUND_VIOLATIONS + ] + return output + + +def summarize_mint( + records: pd.DataFrame | Iterable[Mapping[str, Any]], + *, + draw_indices: Sequence[int], + floor_seeds: Sequence[int] = a7.DEFAULT_FLOOR_SEEDS, + reference_year: int = 2030, + age_rule: str = "reference_year_minus_birth_year", + certified_draw_indices: Sequence[int] | None = None, + refused_draws: Mapping[int, str] | None = None, +) -> dict[str, Any]: + """All five MINT bands; memo statistics receive draw SDs and floors.""" + _, rows, config = _prepare( + records, + draw_indices, + floor_seeds, + reference_year, + age_rule, + certified_draw_indices, + ) + return { + "bands": _summarize( + rows, + config, + MINT_BANDS, + MINT_STATISTICS, + lambda selected, draw: _mint_cell(rows, selected, draw), + refused_draws, + ), + "input_summary": _input_summary(rows), + "certified_projection_draws": ( + list(config.draw_indices) + if certified_draw_indices is not None + else None + ), + "falling_share_units": "fraction of baseline beneficiary weight", + "benefit_level_units": "annual, 12 times the row monthly amount", + "intended_bound_violations": [ + dict(item) for item in INTENDED_BOUND_VIOLATIONS + ], + } diff --git a/tests/acola_track/__init__.py b/tests/acola_track/__init__.py new file mode 100644 index 000000000..7ae06c5a5 --- /dev/null +++ b/tests/acola_track/__init__.py @@ -0,0 +1 @@ +"""Invented-data tests of Registration 18's A-COLA build.""" diff --git a/tests/acola_track/test_capture.py b/tests/acola_track/test_capture.py new file mode 100644 index 000000000..1ae9c03dd --- /dev/null +++ b/tests/acola_track/test_capture.py @@ -0,0 +1,64 @@ +"""The approved table and its note are isolated from invented page text.""" + +import importlib.util +from pathlib import Path + +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +ROOT = Path(__file__).resolve().parents[2] +spec = importlib.util.spec_from_file_location( + "acola_capture", ROOT / "scripts/capture_tr2026_v_c1.py" +) +capture = importlib.util.module_from_spec(spec) +spec.loader.exec_module(capture) + + +@given(st.text(alphabet="abc XYZé—\n", max_size=100)) +@settings(deadline=None) +def test_fragment_keeps_exact_bytes_and_excludes_other_table_text(prefix): + fragment = ( + '
' + + capture.CAPTION + + "
Invented
" + ).encode() + before = (prefix + "
Other
").encode() + after = b"
Other
hidden
" + result, start, end = capture.table_fragment(before + fragment + after) + assert result == fragment + assert start == len(before) + assert end == len(before) + len(fragment) + + +@pytest.mark.parametrize("count", [0, 2]) +def test_caption_must_match_exactly_once(count): + fragment = ( + "
" + capture.CAPTION + "
" + ).encode() + with pytest.raises(ValueError, match="exactly one table"): + capture.table_fragment(fragment * count) + + +def test_timing_note_does_not_include_the_next_note_or_chapter(): + page = ( + b'
Unrelated
a
' + b"
Increases are effective in December after 1982.
" + b'
bAnother note
Unrelated
' + ) + assert capture.cola_timing_note(page) == ( + "a Increases are effective in December after 1982." + ) + + +def test_capture_refuses_existing_snapshot_before_fetching( + tmp_path, monkeypatch +): + (tmp_path / capture.FRAGMENT_NAME).write_text("invented") + + def forbidden(*args): + pytest.fail("existing snapshot must refuse without a fetch") + + monkeypatch.setattr(capture, "_fetch", forbidden) + with pytest.raises(FileExistsError): + capture.main(["--output-dir", str(tmp_path)]) diff --git a/tests/acola_track/test_config.py b/tests/acola_track/test_config.py new file mode 100644 index 000000000..449a202e4 --- /dev/null +++ b/tests/acola_track/test_config.py @@ -0,0 +1,182 @@ +"""Registration 18 row definitions, composition and specification binding.""" + +from dataclasses import fields, replace +from pathlib import Path +from tempfile import TemporaryDirectory + +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +from populace_dynamics.acola_track.config import ( + DEFERRED_ROWS, + LABELS, + REGISTERED_ROWS, + ROOT, + RUNNABLE_ROWS, + SPECIFICATION_PATH, + SPECIFICATION_SHA256, + ACOLAConfig, + check_specification, +) +from populace_dynamics.cola_track_a.config import ( + REGISTERED_ROWS as LEGACY_ROWS, +) +from populace_dynamics.cola_track_a.config import ( + TrackAConfig, + rulings_departures, +) +from populace_dynamics.scenario_benefits import BenefitPeriod, ExposureClock +from populace_dynamics.ss.statutory_aime import ComputationYears + + +def test_all_registered_rows_have_the_registered_fields(): + assert list(REGISTERED_ROWS) == [ + "C0", + "C1", + "C0-D", + "M0", + "M1", + "M2", + "M3", + "M4", + ] + assert ACOLAConfig().rows == RUNNABLE_ROWS + assert len(RUNNABLE_ROWS) == 7 + for row in REGISTERED_ROWS.values(): + assert row.anchor_wave == 2011 + assert row.track_row().first_reduced_determination_year == ( + row.first_reduced_determination_year + ) + assert row.track_row().exposure_clock is row.exposure_clock + assert row.track_row().benefit_period is row.benefit_period + assert row.as_dict()["benefit_scale"] == "annual_12_times_monthly" + assert REGISTERED_ROWS["C0"].first_reduced_determination_year == 2027 + assert REGISTERED_ROWS["M0"].first_reduced_determination_year == 2011 + assert REGISTERED_ROWS["M1"].first_reduced_determination_year == 2010 + assert REGISTERED_ROWS["M3"].exposure_clock is ExposureClock.ENTITLEMENT + assert REGISTERED_ROWS["M4"].age_rule.endswith("_minus_one") + + +@pytest.mark.parametrize( + "alternative,primary,changed", + [ + ("C1", "C0", "benefit_period"), + ("C0-D", "C0", "mechanism"), + ("M1", "M0", "first_reduced_determination_year"), + ("M2", "M0", "benefit_period"), + ("M3", "M0", "exposure_clock"), + ("M4", "M0", "age_rule"), + ], +) +def test_each_alternative_changes_exactly_one_registered_field( + alternative, primary, changed +): + a, p = REGISTERED_ROWS[alternative], REGISTERED_ROWS[primary] + differences = [ + f.name + for f in fields(a) + if f.name not in {"row_id", "field_changed"} + and getattr(a, f.name) != getattr(p, f.name) + ] + assert differences == [changed] + + +def test_deferred_D_has_a_full_spec_and_cannot_run(): + assert DEFERRED_ROWS["C0-D"]["status"] == "deferred" + assert "further issue #42 comment" in DEFERRED_ROWS["C0-D"]["reason"] + assert REGISTERED_ROWS["C0-D"].di_computation == ( + ComputationYears.STATUTORY.value + ) + assert REGISTERED_ROWS["C0"].di_computation == ( + ComputationYears.LEGACY_FIXED_35.value + ) + with pytest.raises(ValueError, match="deferred"): + ACOLAConfig(rows=("C0-D",)) + + +def test_projection_composes_R0_without_extending_its_registry(): + config = ACOLAConfig() + track = config.track_a_config() + assert track == TrackAConfig(rows=("R0",)) + assert track.anchor_waves == (2011,) + assert config.start_years == {2011: 2010} + assert config.rows_for_wave(2009) == () + assert list(LEGACY_ROWS) == ["R0", "R1", "R2", "R3", "R4", "R5", "R6"] + assert rulings_departures(track) == [] + config.check_runnable() + config.check_registered() + assert LABELS[-1] == "report-only" + + +@given(st.lists(st.integers(0, 100), min_size=1, max_size=20, unique=True)) +@settings(deadline=None) +def test_unique_draws_sort_without_changing_the_seed_indices(draws): + config = ACOLAConfig(draw_indices=tuple(draws)) + assert config.draw_indices == tuple(sorted(draws)) + assert config.track_a_config().draw_indices == config.draw_indices + + +@pytest.mark.parametrize( + "kwargs,message", + [ + ({"rows": ()}, "non-empty"), + ({"rows": ("C0", "C0")}, "unique"), + ({"rows": ("unknown",)}, "unknown"), + ({"draw_indices": ()}, "draw_indices"), + ({"draw_indices": (0, 0)}, "draw_indices"), + ({"draw_indices": (-1,)}, "draw_indices"), + ({"draw_indices": (0.5,)}, "draw_indices"), + ({"floor_seeds": (1, 1)}, "floor_seeds"), + ({"reference_year": 2031}, "2030 only"), + ({"annual_reduction": -0.01}, "non-negative"), + ({"annual_reduction": float("nan")}, "finite"), + ({"opening_stock_basis": "rolling"}, "not implemented"), + ], +) +def test_invalid_config_refuses(kwargs, message): + with pytest.raises(ValueError, match=message): + ACOLAConfig(**kwargs) + + +@pytest.mark.parametrize( + "kwargs", + [ + {"rows": ("C0",)}, + {"draw_indices": (0, 1)}, + {"floor_seeds": (0, 1)}, + {"annual_reduction": 0}, + {"claim_table_max_year": 2007}, + {"opening_stock_dime_floor": True}, + {"auxiliary_entitlement_clock": "worker"}, + {"di_benefit_level": "exclude"}, + ], +) +def test_invented_sensitivities_cannot_change_the_registered_run(kwargs): + config = ACOLAConfig(**kwargs) + config.check_runnable() + with pytest.raises(ValueError, match="settings differ"): + config.check_registered() + + +def test_an_acceptance_rule_is_refused(): + config = ACOLAConfig(acceptance_rule="any band") + with pytest.raises(ValueError, match="no acceptance rule"): + config.check_runnable() + + +def test_specification_hash_is_the_verbatim_posted_body_pin(): + assert check_specification()["sha256"] == SPECIFICATION_SHA256 + with TemporaryDirectory(dir=ROOT, prefix=".acola-spec-test-") as directory: + changed = Path(directory) / "registration.md" + changed.write_bytes(SPECIFICATION_PATH.read_bytes() + b"changed\n") + with pytest.raises(ValueError, match="SHA-256"): + check_specification(changed) + + +def test_december_rows_do_not_change_the_registration_reference_year(): + assert REGISTERED_ROWS["C1"].benefit_period is BenefitPeriod.DECEMBER + assert REGISTERED_ROWS["M2"].benefit_period is BenefitPeriod.DECEMBER + config = replace(ACOLAConfig(), annual_reduction=0) + assert config.track_a_config().annual_reduction == 0 + assert config.reference_year == 2030 diff --git a/tests/acola_track/test_nulls.py b/tests/acola_track/test_nulls.py new file mode 100644 index 000000000..b01c1dfd8 --- /dev/null +++ b/tests/acola_track/test_nulls.py @@ -0,0 +1,188 @@ +"""Exact null arithmetic and narrow approved baseline input extraction.""" + +import json +from decimal import Decimal +from fractions import Fraction +from types import SimpleNamespace + +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +from populace_dynamics.acola_track.nulls import ( + CONTEXT_CAPTION, + NULLS_PATH, + VC1_CAPTION, + VC1_COLUMN, + build_nulls, + context_beneficiary_count, + extract_context_count, + extract_tr2026_rates, + nulls_for_run, + spec_null, + tr2026_inputs, +) + + +@given( + st.lists(st.integers(10, 1000), min_size=1, max_size=25), + st.integers(1, 10), +) +@settings(deadline=None) +def test_null_is_exact_product_with_factors_less_than_one(rates, reduction): + c = [Fraction(x, 1000) for x in rates] + r = Fraction(reduction, 1000) + product = Fraction(1) + for rate in c: + factor = (1 + rate - r) / (1 + rate) + assert 0 < factor < 1 + product *= factor + assert spec_null(c, r) == 100 * (product - 1) + assert -100 < spec_null(c, r) < 0 + + +@given(st.lists(st.integers(0, 1000), min_size=0, max_size=25)) +@settings(deadline=None) +def test_zero_reform_is_exactly_zero(rates): + assert spec_null([Fraction(x, 1000) for x in rates], 0) == 0 + + +@given(st.integers(10, 1000), st.integers(1, 20)) +@settings(deadline=None) +def test_more_reduced_increases_strictly_lowers_the_null(rate, count): + rates = [Fraction(rate, 1000)] * count + assert spec_null(rates + [rates[0]]) < spec_null(rates) + + +@given(st.integers(10, 1000), st.integers(1, 9)) +@settings(deadline=None) +def test_larger_reduction_lowers_null_and_larger_cola_raises_it(rate, r): + c = Fraction(rate, 1000) + reduction = Fraction(r, 1000) + assert spec_null([c], reduction + Fraction(1, 1000)) < spec_null( + [c], reduction + ) + assert spec_null([c + Fraction(1, 1000)], reduction) > spec_null( + [c], reduction + ) + + +@pytest.mark.parametrize( + "rates,reduction", [([-1], 0.01), ([0], 0.01), ([0.03], -0.01)] +) +def test_invalid_or_refused_null_inputs(rates, reduction): + with pytest.raises(ValueError): + spec_null(rates, reduction) + + +def _cola_table(): + groups = [] + for group, rate in [("Intermediate:", "2.7"), ("Low-cost:", "9.9")]: + groups.append(f'
{group}
{year}{rate}
" + '' + f'' + + "".join(groups) + + "
{VC1_CAPTION}
Calendar year{VC1_COLUMN}
" + ).encode() + + +def test_literal_labels_select_only_registered_years_and_intermediate_cola(): + result = extract_tr2026_rates(_cola_table()) + assert list(result["cells"]) == ["2027", "2028", "2029"] + assert all(c["rate"] == "0.027" for c in result["cells"].values()) + assert result["cells"]["2027"]["row_header_path"] == [ + "Intermediate:", + "2027", + ] + + +@pytest.mark.parametrize( + "before,after", + [ + ("Intermediate:", "Wrong assumption:"), + (VC1_CAPTION, "Wrong table"), + (VC1_COLUMN, "Wrong column"), + ("2029", "2039"), + ], +) +def test_cola_extractor_refuses_changed_source_identity(before, after): + with pytest.raises(ValueError): + extract_tr2026_rates( + _cola_table().replace(before.encode(), after.encode()) + ) + + +def test_context_extractor_returns_only_one_count_and_no_other_values(): + raw = ( + f"" + '' + '' + "" + '' + "" + '' + "
{CONTEXT_CAPTION}
Calendar yearBeneficiaries b (in thousands)
DIOASDI c
Historical data:
2030111222
Intermediate:
203033312,345
" + ).encode() + result = extract_context_count(raw) + assert result["beneficiary_count"] == 12_345_000 + assert result["as_published"] == "12,345" + assert result["scaling_applied_to_model"] is False + assert not any(str(x) in json.dumps(result) for x in (111, 222, 333)) + + +def test_committed_nulls_reproduce_exactly_from_pinned_inputs(): + assert json.loads(NULLS_PATH.read_bytes()) == build_nulls() + result = build_nulls() + assert result["N"]["factor_count"] == 3 + assert result["N_model"]["factor_count"] == 3 + assert result["N_MINT"]["factor_count"] == 19 + assert result["N_M1"]["factor_count"] == 20 + assert result["N_M1"]["value"] < result["N_MINT"]["value"] + assert result["N_MINT"]["value"] < result["N_model"]["value"] < 0 + assert result["december_null_registered"] is False + for name in ("N", "N_model", "N_MINT", "N_M1"): + exact = result[name]["exact"] + rational = Fraction(int(exact["numerator"]), int(exact["denominator"])) + assert float(rational) == result[name]["value"] + assert float(Decimal(result[name]["value_decimal"])) == float(rational) + json.dumps(result, allow_nan=False) + + +def test_build_zero_reform_nulls_are_all_exactly_zero(): + result = build_nulls(annual_reduction=0) + for name in ("N", "N_model", "N_MINT", "N_M1", "N_minus_N_model"): + assert result[name]["value"] == 0 + assert result[name]["exact"] == {"numerator": "0", "denominator": "1"} + + +def test_runtime_nulls_follow_the_same_invented_baseline_as_benefits(): + baseline = SimpleNamespace(rate_for_determination_year=lambda year: 0.04) + result = nulls_for_run(baseline) + expected = spec_null([Fraction(4, 100)] * 19) + assert result["N_MINT"]["value"] == float(expected) + assert ( + result["inputs"]["model_TR2008"]["runtime_matches_committed_tr2008"] + is False + ) + assert result["N"]["value"] == build_nulls()["N"]["value"] + assert len(result["committed_file_sha256"]) == 64 + zero = nulls_for_run(baseline, annual_reduction=0) + assert all( + zero[key]["value"] == 0 for key in ("N", "N_model", "N_MINT", "N_M1") + ) + + +def test_approved_context_snapshot_has_a_pinned_single_cell_provenance(): + context = context_beneficiary_count() + assert context["row_header_path"] == ["Intermediate:", "2030"] + assert context["column_header_path"][-1] == "OASDI c" + assert context["beneficiary_count"] > 0 + inputs = tr2026_inputs() + assert inputs["fallback_used"] is False + assert inputs["table"] == "Table V.C1" diff --git a/tests/acola_track/test_registered_run_script.py b/tests/acola_track/test_registered_run_script.py new file mode 100644 index 000000000..efa80a6b8 --- /dev/null +++ b/tests/acola_track/test_registered_run_script.py @@ -0,0 +1,202 @@ +"""A-COLA preflight and exclusive output tests; no population read.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + +import pytest + +from populace_dynamics.acola_track.config import ( + REGISTRATION_POINTER, + SPECIFICATION_PATH, + SPECIFICATION_SHA256, + ACOLAConfig, +) + +ROOT = Path(__file__).resolve().parents[2] +COMMIT = "a" * 40 + + +def _script(default_output=None): + spec = importlib.util.spec_from_file_location( + "_acola_registered_entry", ROOT / "scripts" / "run_acola_registered.py" + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + if default_output is not None: + module.DEFAULT_OUTPUT = default_output + return module + + +def _git(head=COMMIT, porcelain=""): + def fake(*args): + if args == ("rev-parse", "HEAD"): + return head + if args == ("status", "--porcelain"): + return porcelain + raise AssertionError(args) + + return fake + + +def test_committed_specification_and_registered_state_pass( + tmp_path, monkeypatch +): + script = _script(tmp_path / "artifact.json") + + def no_data(*args, **kwargs): + pytest.fail("preflight read population data") + + monkeypatch.setattr(script.psid2010, "load_psid2010_inputs", no_data) + monkeypatch.setattr(script, "run_acola", no_data) + state = script.preflight( + registration_pointer=REGISTRATION_POINTER, + registered_commit=COMMIT, + output=tmp_path / "artifact.json", + git=_git(), + ) + assert state == { + "head": COMMIT, + "specification_sha256": SPECIFICATION_SHA256, + } + + +@pytest.mark.parametrize( + ("change", "match"), + [ + ({"registration_pointer": "https://example.org/x"}, "issue #42"), + ( + { + "registration_pointer": "https://github.com/PolicyEngine/microcosm-dynamics/issues/420#issuecomment-1" + }, + "issue #42", + ), + ( + { + "registration_pointer": "https://github.com/PolicyEngine/microcosm-dynamics/issues/42" + }, + "issue #42", + ), + ( + { + "registration_pointer": "https://github.com/PolicyEngine/microcosm-dynamics/issues/42#issuecomment-1" + }, + "exact comment URL", + ), + ({"registered_commit": "a" * 7}, "40-hex"), + ({"registered_commit": "z" * 40}, "40-hex"), + ({"git": _git(head="b" * 40)}, "not the registered commit"), + ({"git": _git(porcelain=" M src/file.py")}, "clean"), + ({"git": _git(porcelain="?? untracked.log")}, "clean"), + ({"config": ACOLAConfig(draw_indices=(0,))}, "Registration 18"), + ({"config": ACOLAConfig(rows=("C0",))}, "Registration 18"), + ({"config": ACOLAConfig(annual_reduction=0)}, "Registration 18"), + ({"config": ACOLAConfig(floor_seeds=(1, 2))}, "Registration 18"), + ], +) +def test_preflight_refusal_matrix(tmp_path, change, match): + values = { + "registration_pointer": REGISTRATION_POINTER, + "registered_commit": COMMIT, + "output": tmp_path / "artifact.json", + "git": _git(), + } + values.update(change) + with pytest.raises(ValueError, match=match): + _script(values["output"]).preflight(**values) + + +def test_changed_specification_is_refused(tmp_path): + changed = tmp_path / "spec.md" + changed.write_bytes( + SPECIFICATION_PATH.read_bytes() + b"\nINVENTED CHANGE\n" + ) + with pytest.raises(ValueError, match="SHA-256|sha256|binding"): + _script(tmp_path / "artifact.json").preflight( + registration_pointer=REGISTRATION_POINTER, + registered_commit=COMMIT, + output=tmp_path / "artifact.json", + git=_git(), + specification_path=changed, + ) + + +@pytest.mark.parametrize("sidecar", [False, True]) +def test_existing_artifact_or_sidecar_is_one_shot_refusal(tmp_path, sidecar): + output = tmp_path / "artifact.json" + occupied = output.with_suffix(".env.json") if sidecar else output + occupied.write_text("INVENTED existing file") + with pytest.raises(ValueError, match="one-shot"): + _script(tmp_path / "artifact.json").preflight( + registration_pointer=REGISTRATION_POINTER, + registered_commit=COMMIT, + output=output, + git=_git(), + ) + + +def test_write_new_never_overwrites(tmp_path): + output = tmp_path / "exclusive.json" + script = _script() + script._write_new(output, "first") + with pytest.raises(FileExistsError): + script._write_new(output, "second") + assert output.read_text() == "first" + + +@pytest.mark.parametrize("field", ["artifact", "environment"]) +def test_nonfinite_value_is_rejected_before_either_file_is_written( + tmp_path, field +): + output = tmp_path / "nonfinite.json" + artifact = {"invented": 1} + environment = {"python": "INVENTED"} + (artifact if field == "artifact" else environment)["undefined"] = float( + "nan" + ) + with pytest.raises(ValueError, match="JSON compliant"): + _script().write_artifact(output, artifact, environment) + assert not output.exists() + assert not output.with_suffix(".env.json").exists() + + +def test_undefined_values_and_reasons_serialize(tmp_path): + output = tmp_path / "undefined.json" + artifact = {"value": None, "undefined_reason": "INVENTED empty band"} + _script().write_artifact(output, artifact, {"python": "INVENTED"}) + assert json.loads(output.read_text()) == artifact + + +def test_default_output_is_the_registered_artifact(): + assert ( + _script().DEFAULT_OUTPUT + == ROOT / "runs" / "trust_fund_acola_2030_v1.json" + ) + + +def test_main_requires_registration_arguments(): + with pytest.raises(SystemExit): + _script().main([]) + + +def test_sidecar_uses_the_existing_environment_resolver(): + environment = ( + _script() + ._script_module("run_track_a_registered") + ._environment(ssa_parameters_revision="INVENTED", packages={}) + ) + assert environment["populace_dynamics_source"] == "src/populace_dynamics" + assert environment["policyengine_us_parameters"]["revision"] == "INVENTED" + + +def test_preflight_refuses_an_alternate_artifact_path(tmp_path): + script = _script(tmp_path / "registered.json") + with pytest.raises(ValueError, match="fixed registered artifact"): + script.preflight( + registration_pointer=REGISTRATION_POINTER, + registered_commit=COMMIT, + output=tmp_path / "alternate.json", + git=_git(), + ) diff --git a/tests/acola_track/test_runner.py b/tests/acola_track/test_runner.py new file mode 100644 index 000000000..603a7f4b8 --- /dev/null +++ b/tests/acola_track/test_runner.py @@ -0,0 +1,411 @@ +"""Registration 18's whole runner, using invented populations only.""" + +from __future__ import annotations + +import hashlib +import importlib.util +import json +from dataclasses import replace +from functools import lru_cache +from pathlib import Path +from types import SimpleNamespace + +import pandas as pd +import pytest + +from populace_dynamics.acola_track import runner +from populace_dynamics.acola_track.config import ( + REGISTRATION_POINTER, + RUNNABLE_ROWS, + ACOLAConfig, +) +from populace_dynamics.cohorts import psid2010 +from populace_dynamics.cola_track_a import ( + TrackAInputs, + invented, + prepare_track_a_cohort, +) +from populace_dynamics.cola_track_a.opening import track_a_cohort_sha256 +from tests.cola_track_a.test_assembly import ( + invented_cola, + invented_di_rates, + invented_mortality, + invented_params, +) + +ROOT = Path(__file__).resolve().parents[2] +CONFIG = ACOLAConfig(draw_indices=(0, 1)) + + +@lru_cache(maxsize=1) +def _cohort(): + return prepare_track_a_cohort( + psid2010.build_psid2010_cohort( + invented.invented_psid2010_inputs(seed=7) + ), + data_provenance="invented", + config=CONFIG.track_a_config(), + ) + + +def _inputs(**changes): + values = { + "cohort": _cohort(), + "params": invented_params(), + "baseline": invented_cola(), + "di_rates": invented_di_rates(), + "population_mortality": invented_mortality(), + "claiming_pmf": invented.invented_claiming_pmf(), + "provenance": {"invented_inputs": True}, + } + values.update(changes) + return TrackAInputs(**values) + + +@pytest.fixture(scope="module") +def whole_result(): + return runner.run_acola(_inputs()) + + +def test_whole_invented_run_has_every_row_memo_null_and_label(whole_result): + assert set(whole_result["rows"]) == set(RUNNABLE_ROWS) + assert whole_result["deferred_rows"]["C0-D"]["status"] == "deferred" + assert whole_result["deferred_rows"]["C0-D"]["row"]["mechanism"] == "D" + assert whole_result["labels"][-1] == "report-only" + assert whole_result["publishes_regardless"] is True + assert whole_result["comparator_seal_opened_before_commit"] is False + assert set(whole_result["nulls"]) >= {"N", "N_model", "N_MINT", "N_M1"} + assert whole_result["nulls"]["december_null_registered"] is False + assert ( + whole_result["projection_weights"][ + "n_persons_weight_varies_across_draws" + ] + == 0 + ) + for row_id, row in whole_result["rows"].items(): + assert row["status"] == "computed" + assert row["labels"] == whole_result["labels"] + assert "equal by construction" in row["membership_note"] + if row_id.startswith("C"): + assert row["statistics"]["P"]["mean"] <= 0 + assert len(row["per_draw"]) == 20 + assert set(row["null_gaps"]) == {"P_minus_N", "P_minus_N_model"} + for cell in row["per_draw"]: + assert cell["n_base"] == cell["n_reform"] + assert cell["weight_base"] == cell["weight_reform"] + assert set(cell["spouse_worker_classes"]) == { + "entitled_unconverted_di_worker", + "another_worker", + "unclassified", + } + else: + assert set(row["bands"]) == { + "Total", + "62-69", + "70-79", + "80-89", + "90+", + } + assert row["registered_null"] == ( + "N_M1" if row_id == "M1" else "N_MINT" + ) + for band in row["bands"].values(): + assert len(band["per_draw"]) == 20 + assert len(band["floor_per_seed"]) == 5 + assert len(band["null_gaps"]) == 3 + context = whole_result["context_beneficiary_count"] + assert context["context_only"] is True + assert context["scaling_applied"] is False + assert context["model_mean"] == pytest.approx( + sum( + cell["weight_base"] + for cell in whole_result["rows"]["C0"]["per_draw"] + ) + / 20 + ) + assert context["trustees"]["beneficiary_count"] > 0 + + +def test_invented_whole_artifact_and_sidecar_only_in_temporary_directory( + tmp_path, whole_result +): + spec = importlib.util.spec_from_file_location( + "_acola_entry_dry", ROOT / "scripts" / "run_acola_registered.py" + ) + script = importlib.util.module_from_spec(spec) + spec.loader.exec_module(script) + artifact = { + "header": "INVENTED DATA - NOT A COMPARISON", + **whole_result, + "checks": {"invented_dry_run": True}, + "run": { + "started": "INVENTED", + "finished": "INVENTED", + "registration_pointer": REGISTRATION_POINTER, + "registered_commit": "a" * 40, + "git_head": "a" * 40, + "git_clean": True, + "command": "INVENTED DATA - pytest temporary-directory dry run", + }, + } + output = tmp_path / "invented.json" + script.write_artifact(output, artifact, {"python": "INVENTED"}) + assert json.loads(output.read_text()) == artifact + sidecar = json.loads(output.with_suffix(".env.json").read_text()) + assert sidecar["artifact"] == output.name + assert ( + sidecar["artifact_sha256"] + == hashlib.sha256(output.read_bytes()).hexdigest() + ) + assert sidecar["environment"] == {"python": "INVENTED"} + for row in artifact["rows"].values(): + if "bands" in row: + for band in row["bands"].values(): + for summary in band["statistics"].values(): + if summary["mean"] is None: + assert summary["undefined_reason"] + if not summary["floor"]["defined"]: + assert summary["floor"]["undefined_reason"] + + +def test_projection_once_per_draw_with_shared_lookups_and_cache(monkeypatch): + project = runner.legacy._project_population + emitter = runner.reference_benefit_rows + calls = [] + projects = [] + + def project_spy(*args, **kwargs): + projects.append(kwargs) + return project(*args, **kwargs) + + def emit_spy(*args, **kwargs): + calls.append( + (kwargs["draw"], id(kwargs["lookups"]), id(kwargs["pia_cache"])) + ) + return emitter(*args, **kwargs) + + monkeypatch.setattr(runner.legacy, "_project_population", project_spy) + monkeypatch.setattr(runner, "reference_benefit_rows", emit_spy) + runner.run_acola(_inputs(), config=CONFIG) + assert len(projects) == 1 + assert len(calls) == 2 * len(RUNNABLE_ROWS) + assert len({call[2] for call in calls}) == 1 + for draw in CONFIG.draw_indices: + assert len({call[1] for call in calls if call[0] == draw}) == 1 + + +def test_same_draw_seed_is_deterministic(whole_result): + # Recompute seed 5200 independently of the other nineteen draws. + repeated = runner.run_acola( + _inputs(), config=ACOLAConfig(draw_indices=(0,)) + ) + for row_id, row in repeated["rows"].items(): + if "bands" in row: + for band, cell in row["bands"].items(): + assert ( + cell["per_draw"][0] + == whole_result["rows"][row_id]["bands"][band]["per_draw"][ + 0 + ] + ) + else: + assert ( + row["per_draw"][0] + == whole_result["rows"][row_id]["per_draw"][0] + ) + + +@pytest.mark.parametrize( + ("spouse", "linked", "expected"), + [ + (pd.NA, None, "unclassified"), + (2, None, "unclassified"), + ( + 2, + {"di_entitled": True, "di_conversion_year": pd.NA}, + "entitled_unconverted_di_worker", + ), + ( + 2, + {"di_entitled": True, "di_conversion_year": 2029}, + "another_worker", + ), + ( + 2, + {"di_entitled": False, "di_conversion_year": pd.NA}, + "another_worker", + ), + ], +) +def test_spouse_classification_uses_living_link_2030_di_state( + spouse, linked, expected +): + states = [{"person_id": 1, "spouse_person_id": spouse}] + if linked is not None: + states.append({"person_id": 2, **linked}) + final = pd.DataFrame(states).set_index("person_id", drop=False) + lookups = SimpleNamespace( + final=final, alive=lambda pid: pid in final.index + ) + assert runner.spouse_worker_class(1, lookups) == expected + + +def test_refused_row_is_published_with_every_null_statistic(monkeypatch): + emitter = runner.reference_benefit_rows + + def refused(*args, **kwargs): + if kwargs["row"].row_id == "C0" and kwargs["draw"] == 1: + raise ValueError("INVENTED refusal") + return emitter(*args, **kwargs) + + monkeypatch.setattr(runner, "reference_benefit_rows", refused) + result = runner.run_acola(_inputs(), config=CONFIG) + row = result["rows"]["C0"] + assert row["status"] == "refused" + assert len(row["refusals"]) == 1 + successful, failed = row["per_draw"] + assert successful["draw"] == 0 + assert successful["P"] < 0 + assert successful["T_base"] > 0 + assert failed["draw"] == 1 + assert failed["T_base"] is None + assert failed["weight_base"] is None + for totals in failed["component_totals"].values(): + assert totals == {"base": None, "reform": None} + for memo in failed["spouse_worker_classes"].values(): + assert all(value is None for value in memo.values()) + assert failed["undefined_reasons"]["T_base"] + for summary in row["statistics"].values(): + assert summary["mean"] is None + assert summary["undefined_reason"] + assert summary["floor"]["mean"] is None + assert summary["floor"]["undefined_reason"] + assert result["rows"]["M0"]["status"] == "computed" + assert result["context_beneficiary_count"]["model_mean"] is None + json.dumps(result, allow_nan=False) + + +def test_completed_draw_with_no_recipients_is_explicitly_undefined( + monkeypatch, +): + emitter = runner.reference_benefit_rows + + def empty(*args, **kwargs): + if kwargs["row"].row_id == "C0" and kwargs["draw"] == 0: + return [], {} + return emitter(*args, **kwargs) + + monkeypatch.setattr(runner, "reference_benefit_rows", empty) + result = runner.run_acola(_inputs(), config=CONFIG) + row = result["rows"]["C0"] + assert row["status"] == "computed" + first, second = row["per_draw"] + assert first["T_base"] == 0 + assert first["n_base"] == 0 + assert first["P"] is None + assert first["undefined_reasons"]["P"] + assert second["P"] < 0 + assert row["statistics"]["P"]["mean"] is None + assert row["statistics"]["P"]["undefined_reason"] + json.dumps(result, allow_nan=False) + + +def test_projection_cannot_silently_omit_a_draw(monkeypatch): + monkeypatch.setattr( + runner.legacy, + "_project_population", + lambda *args, **kwargs: ({0: None}, {}), + ) + with pytest.raises(ValueError, match="omitted a configured draw"): + runner.run_acola(_inputs(), config=CONFIG) + + +def test_reduced_rate_floor_refuses_before_projection(monkeypatch): + baseline = replace( + invented_cola(), + by_determination_year={year: 0.005 for year in range(1979, 2032)}, + ) + monkeypatch.setattr( + runner.legacy, + "_project_population", + lambda *args, **kwargs: pytest.fail("projection ran"), + ) + with pytest.raises(ValueError, match="REFUSE"): + runner.run_acola(_inputs(baseline=baseline), config=CONFIG) + + +def test_registered_pointer_is_required_before_any_real_statistic(): + cohort = replace(_cohort(), data_provenance="registered_real") + with pytest.raises(ValueError, match="Registration 18's pointer"): + runner.run_acola(_inputs(cohort=cohort)) + + +def test_registered_real_rejects_partial_run_before_projection(): + cohort = replace(_cohort(), data_provenance="registered_real") + with pytest.raises(ValueError, match="Registration 18"): + runner.run_acola( + _inputs(cohort=cohort), + config=CONFIG, + registration_pointer=REGISTRATION_POINTER, + ) + + +def test_registered_real_requires_committed_parameter_values(monkeypatch): + cohort = replace(_cohort(), data_provenance="registered_real") + monkeypatch.setattr( + runner.legacy, + "_project_population", + lambda *args, **kwargs: pytest.fail("projection ran"), + ) + with pytest.raises(ValueError, match="committed inputs"): + runner.run_acola( + _inputs(cohort=cohort), registration_pointer=REGISTRATION_POINTER + ) + + +@pytest.mark.parametrize("change", ["source", "seal", "labels"]) +def test_source_seal_and_labels_are_checked_before_projection( + monkeypatch, change +): + cohort = _cohort() + if change == "source": + cohort = replace(cohort, source_provenance={"kind": "caller_frames"}) + match = "source provenance" + elif change == "seal": + cohort = replace(cohort, diagnostics={"changed": True}) + match = "does not match the seal" + else: + cohort = replace(cohort, labels=("WRONG",)) + object.__setattr__(cohort, "seal", track_a_cohort_sha256(cohort)) + match = "must carry the labels" + monkeypatch.setattr( + runner.legacy, + "_project_population", + lambda *args, **kwargs: pytest.fail("projection ran"), + ) + with pytest.raises(ValueError, match=match): + runner.run_acola(_inputs(cohort=cohort), config=CONFIG) + + +def test_shifted_summary_preserves_undefined_sd_reason(): + summary = { + "mean": 0.0, + "sd": None, + "sample_sd": None, + "per_draw": [0.0], + "undefined_reason": None, + "undefined_reasons": {"sd": "fewer_than_two_draws"}, + "floor": { + "defined": False, + "mean": None, + "undefined_reason": "no usable seed", + }, + } + shifted = runner._shifted_summary(summary, {"value": -3.0}) + assert shifted["mean"] == 3.0 + assert shifted["per_draw"] == [3.0] + assert shifted["sd"] is None + assert shifted["undefined_reasons"]["sd"] == "fewer_than_two_draws" + assert shifted["floor"] == summary["floor"] + assert summary["mean"] == 0.0 + json.dumps(shifted, allow_nan=False) diff --git a/tests/acola_track/test_statistics.py b/tests/acola_track/test_statistics.py new file mode 100644 index 000000000..fcf4c8414 --- /dev/null +++ b/tests/acola_track/test_statistics.py @@ -0,0 +1,770 @@ +"""Invented-data tests of Registration 18 estimands and the A7 floor.""" + +import json +import math +import statistics +from copy import deepcopy +from fractions import Fraction + +import numpy as np +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +from populace_dynamics import scenario_benefits as sb +from populace_dynamics.acola_track import statistics as ac +from populace_dynamics.estimates import cola_age_profile as a7 +from populace_dynamics.ss import benefits as ss_benefits +from populace_dynamics.track_a_v2 import estimands as v2 +from populace_dynamics.track_a_v2.matrix import UNION_STATISTIC +from tests.cola_track_a.test_assembly import invented_params + + +def record( + person_id=1, + *, + draw=0, + age=65, + weight=1.0, + base=100.0, + reform=90.0, + component="retired_worker", + family=None, + spouse_class="unclassified", +): + return { + "draw": draw, + "person_id": person_id, + "family_unit_id": person_id if family is None else family, + "weight": float(weight), + "birth_year": 2030 - age, + "beneficiary_base": base > 0, + "beneficiary_reform": reform > 0, + "benefit_base": float(base), + "benefit_reform": float(reform), + "benefit_components": { + component: {"base": float(base), "reform": float(reform)} + }, + "spouse_worker_class": spouse_class, + } + + +def panel(): + return [ + record( + i + 1, + draw=draw, + age=age, + weight=i + 1, + base=100 * (i + 1), + reform=100 * (i + 1) * (0.9 - i * 0.1), + component=component, + spouse_class="entitled_unconverted_di_worker", + ) + for draw in (0, 1) + for i, (age, component) in enumerate( + zip((65, 75, 85, 95, 60), a7.PRIMARY_COMPONENTS, strict=True) + ) + ] + + +def normalized(records, draws=(0, 1)): + config = a7.ColaAgeProfileConfig( + age_groups=(a7.AgeGroup("all", 0, None),), + draw_indices=draws, + allow_membership_difference=True, + ) + return a7._normalize(records, config), config + + +@pytest.mark.parametrize( + "values,weights,expected", + [ + ([0, 10], [1, 1], 5), + ([0, 10, 20], [1, 2, 1], 10), + ([0, 10, 20], [1, 0, 1], 10), + ([10, 0, 20], [0, 1, 1], 10), + ([10, 20, 30], [0, 0, 0], None), + ([], [], None), + ([-10, -2, 0], [1, 1, 1], -2), + ], +) +def test_registered_exact_weighted_median(values, weights, expected): + assert ac.weighted_median(values, weights) == expected + + +def test_near_half_weight_uses_exact_binary_fraction(): + above = math.nextafter(1.0, math.inf) + below = math.nextafter(1.0, 0.0) + assert ac.weighted_median([0, 10], [above, 1]) == 0 + assert ac.weighted_median([0, 10], [below, 1]) == 10 + # Decimal-looking floats are interpreted as their supplied binary values. + assert Fraction(0.1) + Fraction(0.2) != Fraction(0.3) + assert ac.weighted_median([0, 1, 2], [0.1, 0.2, 0.3]) == 1 + + +def test_midpoint_does_not_overflow_or_lose_subnormal_values(): + assert ac.weighted_median([1e308, 1e308], [1, 1]) == 1e308 + smallest = math.ulp(0.0) + expected = float((Fraction(smallest) + Fraction(2 * smallest)) / 2) + assert ac.weighted_median([smallest, 2 * smallest], [1, 1]) == expected + + +@pytest.mark.parametrize( + "values,weights,ids", + [ + ([1], [-1], None), + ([math.inf], [1], None), + ([1], [math.nan], None), + ([1], [], None), + ([1], [1], []), + ([1, 2], [1, 1], [1, "a"]), + ], +) +def test_median_refuses_invalid_input(values, weights, ids): + with pytest.raises(a7.ColaTabulationError): + ac.weighted_median(values, weights, ids) + + +@given(st.lists(st.integers(-10000, 10000), min_size=1, max_size=31)) +@settings(deadline=None) +def test_equal_positive_weights_match_ordinary_median(values): + assert ac.weighted_median(values, [1] * len(values)) == statistics.median( + values + ) + + +@given( + st.lists( + st.tuples(st.integers(-10000, 10000), st.integers(1, 100)), + min_size=1, + max_size=30, + ), + st.integers(1, 100), +) +@settings(deadline=None) +def test_median_bounds_permutation_and_weight_scale(atoms, scale): + values, weights = zip(*atoms, strict=True) + ids = list(range(len(values))) + median = ac.weighted_median(values, weights, ids) + assert min(values) <= median <= max(values) + assert median == ac.weighted_median(values[::-1], weights[::-1], ids[::-1]) + assert median == ac.weighted_median(values, [w * scale for w in weights]) + + +def test_component_accounting_and_spouse_three_classes(): + records = panel() + cost = ac.summarize_cost(records, draw_indices=(0, 1)) + cell = cost["per_draw"][0] + assert cell["T_base"] == 5500 + assert cell["T_reform"] == pytest.approx(3250) + assert cell[ac.P] == pytest.approx(100 * (3250 / 5500 - 1)) + for scenario in ("base", "reform"): + assert sum( + totals[scenario] for totals in cell["component_totals"].values() + ) == pytest.approx(cell[f"T_{scenario}"]) + assert sum(cell["group_shares"].values()) == pytest.approx(1) + assert cell[ac.P] == pytest.approx( + cell["group_shares"]["OASI"] * cell[ac.P_OASI] + + cell["group_shares"]["DI"] * cell[ac.P_DI] + ) + assert cell["n_base"] == cell["n_reform"] == 5 + assert cell["weight_base"] == cell["weight_reform"] == 15 + classes = cell["spouse_worker_classes"] + assert ( + classes["entitled_unconverted_di_worker"]["weighted_count_base"] == 3 + ) + assert classes["entitled_unconverted_di_worker"]["dollars_base"] == 900 + assert classes["entitled_unconverted_di_worker"]["dollars_reform"] == 630 + assert classes["another_worker"]["n_base"] == 0 + assert classes["unclassified"]["n_base"] == 0 + assert cost["statistic_additions"] == [ + ac.P_OASI, + ac.P_DI, + ac.RATIO_OF_SCENARIO_MEANS, + ] + + +def test_spouse_memo_counts_positive_component_by_scenario(): + rows = [ + record( + i, + component="spouse", + base=50, + reform=(0 if i == 1 else 40), + spouse_class=label, + weight=i, + ) + for i, label in enumerate(ac.SPOUSE_WORKER_CLASSES, 1) + ] + result = ac.summarize_cost(rows, draw_indices=(0,)) + classes = result["per_draw"][0]["spouse_worker_classes"] + assert classes[ac.SPOUSE_WORKER_CLASSES[0]]["n_reform"] == 0 + assert classes[ac.SPOUSE_WORKER_CLASSES[1]]["weighted_count_base"] == 2 + assert classes["unclassified"]["dollars_reform"] == 120 + + +def test_three_mint_estimands_are_distinct_and_memos_have_floors(): + rows = [ + record(i + 1, base=base, reform=reform) + for i, (base, reform) in enumerate(((100, 50), (200, 190), (300, 200))) + ] + total = ac.summarize_mint(rows, draw_indices=(0,))["bands"]["Total"] + cell = total["per_draw"][0] + assert cell[ac.RATIO_OF_SCENARIO_MEANS] == pytest.approx( + 100 * (440 / 600 - 1) + ) + assert cell[ac.MEDIAN_INDIVIDUAL_PCT_CHANGE] == pytest.approx(-100 / 3) + assert cell[ac.DIFFERENCE_OF_WEIGHTED_MEDIANS_PCT] == pytest.approx(-5) + assert cell[ac.MEDIAN_BENEFIT_BASE] == 200 + assert cell[ac.MEDIAN_BENEFIT_REFORM] == 190 + assert cell[ac.BASELINE_FALLING_SHARE] == 1 + for key in ac.MINT_STATISTICS: + summary = total["statistics"][key] + assert "floor" in summary + assert summary["sd"] is None + assert summary["undefined_reasons"]["sd"] == "fewer_than_two_draws" + + +def test_mint_uses_scenario_specific_sets_and_intersection_for_changes(): + rows = [ + record(1, base=100, reform=90, weight=2), + record(2, base=400, reform=0, weight=3), + record(3, base=0, reform=500, weight=4), + ] + cell = ac.summarize_mint(rows, draw_indices=(0,))["bands"]["Total"][ + "per_draw" + ][0] + assert cell["n_base"] == cell["n_reform"] == 2 + assert cell["weight_base"] == 5 + assert cell["weight_reform"] == 6 + assert cell["n_alternative"] == 1 + assert cell[ac.MEDIAN_INDIVIDUAL_PCT_CHANGE] == pytest.approx(-10) + assert cell[ac.RATIO_OF_SCENARIO_MEANS] == pytest.approx( + 100 * ((2180 / 6) / (1400 / 5) - 1) + ) + assert cell[ac.BASELINE_FALLING_SHARE] == 1 + cost = ac.summarize_cost(rows, draw_indices=(0,))["per_draw"][0] + assert cost[ac.P] != pytest.approx(cost[ac.RATIO_OF_SCENARIO_MEANS]) + + +def test_band_boundaries_and_m4_age_offset(): + ages = (61, 62, 69, 70, 79, 80, 89, 90) + rows = [record(i, age=age) for i, age in enumerate(ages)] + mint = ac.summarize_mint(rows, draw_indices=(0,))["bands"] + assert [ + mint[label]["per_draw"][0]["n_base"] for label, _, _ in ac.MINT_BANDS + ] == [7, 2, 2, 2, 1] + m4 = ac.summarize_mint( + rows, + draw_indices=(0,), + age_rule="reference_year_minus_birth_year_minus_one", + )["bands"] + assert [ + m4[label]["per_draw"][0]["n_base"] for label, _, _ in ac.MINT_BANDS + ] == [6, 2, 2, 2, 0] + + +def test_zero_weights_are_excluded_and_counted_for_each_median(): + rows = [record(1, weight=0, base=100, reform=50), record(2, weight=2)] + cell = ac.summarize_mint(rows, draw_indices=(0,))["bands"]["Total"][ + "per_draw" + ][0] + assert cell["median_zero_weight_exclusions"] == { + "base": 1, + "reform": 1, + "alt": 1, + } + assert cell[ac.MEDIAN_INDIVIDUAL_PCT_CHANGE] == pytest.approx(-10) + assert cell["n_base"] == 2 + assert cell["weight_base"] == 2 + + +def test_draw_sample_sd_and_weight_variation_are_reported(): + rows = [ + record(1, draw=0, reform=90), + record(1, draw=1, reform=80, weight=2), + ] + result = ac.summarize_cost(rows, draw_indices=(0, 1)) + summary = result["statistics"][ac.P] + assert summary["mean"] == pytest.approx(-15) + assert summary["sample_sd"] == pytest.approx(math.sqrt(50)) + assert summary["sd"] == summary["sample_sd"] + assert result["input_summary"]["n_persons_weight_varies_across_draws"] == 1 + + +def test_zero_reform_has_exact_zero_changes_share_and_floors(): + rows = panel() + for row in rows: + row["benefit_reform"] = row["benefit_base"] + for component in row["benefit_components"].values(): + component["reform"] = component["base"] + cost = ac.summarize_cost(rows, draw_indices=(0, 1)) + assert all(cell[ac.P] == 0 for cell in cost["per_draw"]) + mint = ac.summarize_mint(rows, draw_indices=(0, 1)) + change_statistics = ac.MINT_STATISTICS[:4] + for band in mint["bands"].values(): + for cell in band["per_draw"]: + assert all(cell[key] == 0 for key in change_statistics) + for key in change_statistics: + summary = band["statistics"][key] + assert summary["mean"] == summary["sample_sd"] == 0 + if summary["floor"]["defined"]: + assert summary["floor"]["mean"] == 0 + # Median benefit levels are memo levels, and remain positive at zero cut. + assert ( + mint["bands"]["Total"]["statistics"][ac.MEDIAN_BENEFIT_BASE]["mean"] + > 0 + ) + + +def test_empty_band_and_undefined_floor_serialize_with_reasons(): + result = ac.summarize_mint([record()], draw_indices=(0,)) + band = result["bands"]["90+"] + for key in ac.MINT_STATISTICS: + assert band["per_draw"][0][key] is None + assert band["per_draw"][0]["undefined_reasons"][key] + assert band["statistics"][key]["floor"]["defined"] is False + assert band["statistics"][key]["floor"]["mean"] is None + json.dumps(result, allow_nan=False) + + +@pytest.mark.parametrize("summarizer", [ac.summarize_cost, ac.summarize_mint]) +def test_entirely_empty_table_publishes_every_draw_as_undefined(summarizer): + result = summarizer([], draw_indices=(0, 1)) + bands = result.get("bands", {"all": result}) + for band in bands.values(): + assert [c["draw"] for c in band["per_draw"]] == [0, 1] + for summary in band["statistics"].values(): + assert summary["mean"] is None + assert summary["undefined_draws"] + assert summary["floor"]["undefined_reason"] == "no usable seed" + json.dumps(result, allow_nan=False) + + +def test_missing_draw_in_nonempty_input_is_refused(): + with pytest.raises(a7.ColaTabulationError, match="draws present"): + ac.summarize_cost([record()], draw_indices=(0, 1)) + + +@pytest.mark.parametrize("summarizer", [ac.summarize_cost, ac.summarize_mint]) +def test_certified_empty_recipient_draw_preserves_valid_draw(summarizer): + result = summarizer( + [record()], draw_indices=(0, 1), certified_draw_indices=(0, 1) + ) + band = result.get("bands", {"Total": result})["Total"] + key = ( + ac.P if summarizer is ac.summarize_cost else ac.RATIO_OF_SCENARIO_MEANS + ) + assert band["per_draw"][0][key] == pytest.approx(-10) + assert band["per_draw"][1][key] is None + assert band["per_draw"][1]["n_base"] == 0 + assert band["statistics"][key]["n_defined_draws"] == 1 + assert band["statistics"][key]["mean"] is None + assert result["certified_projection_draws"] == [0, 1] + + +@pytest.mark.parametrize("summarizer", [ac.summarize_cost, ac.summarize_mint]) +def test_refused_draw_preserves_valid_data_and_marks_every_memo(summarizer): + result = summarizer( + [record()], + draw_indices=(0, 1), + certified_draw_indices=(0, 1), + refused_draws={1: "REFUSE: negative reformed increase"}, + ) + band = result.get("bands", {"Total": result})["Total"] + key = ( + ac.P if summarizer is ac.summarize_cost else ac.RATIO_OF_SCENARIO_MEANS + ) + assert band["per_draw"][0][key] == pytest.approx(-10) + refused = band["per_draw"][1] + assert refused["status"] == "refused" + assert refused["n_base"] is None + assert refused["weight_base"] is None + assert refused["undefined_reasons"][key] == refused["refusal_reason"] + assert refused["not_registered"]["mean_benefit_base"] is None + assert ( + refused["not_registered"]["undefined_reasons"]["mean_benefit_base"] + == refused["refusal_reason"] + ) + if summarizer is ac.summarize_cost: + assert refused["T_base"] is None + assert refused["component_totals"]["spouse"]["base"] is None + assert refused["undefined_reasons"]["component_totals.spouse.base"] + assert ( + refused["spouse_worker_classes"]["unclassified"]["n_base"] is None + ) + else: + assert refused["median_zero_weight_exclusions"]["alt"] is None + assert refused["undefined_reasons"][ + "median_zero_weight_exclusions.alt" + ] + summary = band["statistics"][key] + assert summary["n_defined_draws"] == 1 + assert summary["per_draw"][0] == pytest.approx(-10) + assert summary["per_draw"][1] is None + assert summary["floor"]["n_seeds"] == 0 + for seed in band["floor_per_seed"]: + assert seed["absolute_gaps"][key] is None + assert seed["undefined_reasons"][key] + undefined = { + item["draw"]: item["reason"] + for item in seed["side_a"][key]["undefined_draws"] + } + assert undefined[1] == refused["refusal_reason"] + json.dumps(result, allow_nan=False) + + +@pytest.mark.parametrize( + "certified,records,refused", + [ + ((0,), [record()], None), + ((0, 1, 1), [record()], None), + ((0, 1), [record(draw=2)], None), + ((0, 1), [record()], {2: "error"}), + ((0, 1), [record()], {1: ""}), + ], +) +def test_certification_and_refusal_inventory_are_checked( + certified, records, refused +): + with pytest.raises(a7.ColaTabulationError): + ac.summarize_cost( + records, + draw_indices=(0, 1), + certified_draw_indices=certified, + refused_draws=refused, + ) + + +def test_undefined_group_is_null_with_reason_and_zero_share(): + cell = ac.summarize_cost([record()], draw_indices=(0,))["per_draw"][0] + assert cell[ac.P_DI] is None + assert cell["undefined_reasons"][ac.P_DI] == "nonpositive_baseline_total" + assert cell["group_shares"]["DI"] == 0 + + +def test_unregistered_mean_of_individual_ratios_never_appears(): + for result in ( + ac.summarize_cost(panel(), draw_indices=(0, 1)), + ac.summarize_mint(panel(), draw_indices=(0, 1)), + ): + assert a7.MEAN_OF_INDIVIDUAL_RATIOS not in json.dumps(result) + bands = result.get("bands", {"all": result}) + for band in bands.values(): + for cell in band["per_draw"]: + assert "mean_benefit_base" not in cell + assert "mean_benefit_base" in cell["not_registered"] + if cell["not_registered"]["mean_benefit_base"] is None: + assert cell["not_registered"]["undefined_reasons"][ + "mean_benefit_base" + ] + + +def test_cost_and_mint_order_independent_including_every_floor(): + rows = panel() + for summarizer in (ac.summarize_cost, ac.summarize_mint): + assert summarizer(rows, draw_indices=(0, 1)) == summarizer( + rows[::-1], draw_indices=(0, 1) + ) + + +def test_ratio_means_and_union_differential_against_existing_functions(): + records = panel() + rows, config = normalized(records) + masks = a7._membership_masks(rows, config.membership_basis) + cost = ac.summarize_cost(records, draw_indices=(0, 1)) + for cell in cost["per_draw"]: + selected = rows.draw == cell["draw"] + expected = v2._cell(rows, selected, masks, cell["draw"]) + assert cell[ac.P] == expected[UNION_STATISTIC] + assert ( + cell[ac.RATIO_OF_SCENARIO_MEANS] + == expected[a7.RATIO_OF_SCENARIO_MEANS] + ) + + +def test_cost_floor_matches_v2_and_ratio_means_floor_matches_a7(): + records = panel() + rows, config = normalized(records) + result = ac.summarize_cost(records, draw_indices=(0, 1)) + _, floors = v2._floors(rows, config, UNION_STATISTIC) + assert result["statistics"][ac.P]["floor"] == floors["all"] + _, a7_floors = a7._floors(rows, config) + assert ( + result["statistics"][ac.RATIO_OF_SCENARIO_MEANS]["floor"] + == a7_floors["all"][a7.RATIO_OF_SCENARIO_MEANS] + ) + + +def test_mint_floor_splits_full_table_including_people_below_62(): + # Excluding the first family would shift all RNG draws on the remaining + # sorted families. Every MINT statistic must use the full row table. + records = [ + record(i, age=(50 if i == 0 else 65), reform=100 - i) for i in range(8) + ] + rows, config = normalized(records, (0,)) + result = ac.summarize_mint(records, draw_indices=(0,))["bands"]["Total"] + frame = a7.pd.DataFrame({a7.FAMILY_UNIT: rows.family_unit_id.tolist()}) + masks = a7._membership_masks(rows, config.membership_basis) + for seed in result["floor_per_seed"]: + left, _ = a7.split_panel_by_person( + frame, a7.FAMILY_UNIT, seed=seed["seed"] + ) + in_a = np.zeros(rows.n, dtype=bool) + in_a[left.index.to_numpy()] = True + for key, selected in (("side_a", in_a), ("side_b", ~in_a)): + selected &= rows.age >= 62 + expected = a7._cell(rows, selected, masks, 0) + assert ( + seed[key][ac.RATIO_OF_SCENARIO_MEANS]["mean"] + == expected[a7.RATIO_OF_SCENARIO_MEANS] + ) + + +def test_floor_with_only_one_usable_seed_is_undefined(): + result = ac.summarize_cost( + [record(i, reform=90 - i) for i in range(10)], + draw_indices=(0,), + floor_seeds=(0,), + ) + floor = result["statistics"][ac.P]["floor"] + assert floor["n_seeds"] == 1 + assert floor["defined"] is False + assert floor["undefined_reason"] == "fewer than 2 usable seeds" + assert floor["mean"] is None + + +@given( + st.lists( + st.tuples( + st.integers(1, 10000), st.integers(1, 10000), st.integers(1, 100) + ), + min_size=1, + max_size=20, + ), + st.integers(0, 1000), +) +@settings(max_examples=50, deadline=None) +def test_accounting_identity_and_cut_bound_on_invented_components(atoms, cut): + factor = 1 - cut / 1000 + records = [] + for pid, (oasi, di, weight) in enumerate(atoms): + row = record( + pid, weight=weight, base=oasi + di, reform=(oasi + di) * factor + ) + row["benefit_components"] = { + "retired_worker": {"base": oasi, "reform": oasi * factor}, + "disabled_worker": {"base": di, "reform": di * factor}, + } + records.append(row) + # Floors are separately differential-tested; two seeds keeps this property fast. + result = ac.summarize_cost(records, draw_indices=(0,), floor_seeds=(0, 1)) + assert result == ac.summarize_cost( + records[::-1], draw_indices=(0,), floor_seeds=(0, 1) + ) + cell = result["per_draw"][0] + assert -cut / 10 - 1e-12 <= cell[ac.P] <= 1e-12 + assert cell[ac.P] == pytest.approx( + cell["group_shares"]["OASI"] * cell[ac.P_OASI] + + cell["group_shares"]["DI"] * cell[ac.P_DI], + abs=1e-11, + ) + if factor > 0: + assert cell[ac.P] == pytest.approx( + cell[ac.RATIO_OF_SCENARIO_MEANS], abs=1e-11 + ) + assert 0 <= sum(cell["group_shares"].values()) <= 1.000000000001 + + +@given( + st.lists( + st.tuples( + st.integers(1, 10000), + st.integers(0, 1000), + st.integers(0, 100), + st.integers(60, 100), + ), + min_size=1, + max_size=10, + ) +) +@settings(max_examples=40, deadline=None) +def test_mint_order_falling_share_and_individual_cut_bounds(atoms): + records = [ + record( + pid, base=base, reform=base * factor / 1000, weight=weight, age=age + ) + for pid, (base, factor, weight, age) in enumerate(atoms) + ] + kwargs = {"draw_indices": (0,), "floor_seeds": (0, 1)} + result = ac.summarize_mint(records, **kwargs) + assert result == ac.summarize_mint(records[::-1], **kwargs) + for band in result["bands"].values(): + cell = band["per_draw"][0] + falling = cell[ac.BASELINE_FALLING_SHARE] + if falling is not None: + assert 0 <= falling <= 1 + change = cell[ac.MEDIAN_INDIVIDUAL_PCT_CHANGE] + if change is not None: + assert -100 <= change <= 0 + json.dumps(result, allow_nan=False) + + +@given(st.integers(0, 999), st.integers(0, 999)) +@settings(max_examples=30, deadline=None) +def test_larger_cut_weakly_lowers_p_and_every_individual_change(a, b): + low, high = sorted((a, b)) + smaller, larger = panel(), panel() + for records, cut in ((smaller, low), (larger, high)): + for row in records: + row["benefit_reform"] = row["benefit_base"] * (1 - cut / 1000) + for values in row["benefit_components"].values(): + values["reform"] = values["base"] * (1 - cut / 1000) + results = [ + ac.summarize_cost(r, draw_indices=(0, 1), floor_seeds=(0, 1)) + for r in (smaller, larger) + ] + assert ( + results[1]["statistics"][ac.P]["mean"] + <= results[0]["statistics"][ac.P]["mean"] + ) + for a_row, b_row in zip(smaller, larger, strict=True): + assert b_row["benefit_reform"] <= a_row["benefit_reform"] + + +def test_component_membership_can_disappear_without_refusing_cost_split(): + row = record(base=150, reform=100) + row["benefit_components"] = { + "disabled_worker": {"base": 100, "reform": 100}, + "aged_widow": {"base": 50, "reform": 0}, + } + result = ac.summarize_cost([row], draw_indices=(0,)) + cell = result["per_draw"][0] + assert cell[ac.P_OASI] == -100 + assert cell[ac.P_DI] == 0 + assert cell[ac.P] == pytest.approx(-100 / 3) + + +def test_a7_validation_is_preserved_for_duplicates_and_component_sum(): + row = record() + with pytest.raises(a7.ColaTabulationError, match="duplicate"): + ac.summarize_cost([row, row], draw_indices=(0,)) + wrong = deepcopy(row) + wrong["benefit_base"] += 1 + with pytest.raises(a7.ColaTabulationError, match="sum"): + ac.summarize_mint([wrong], draw_indices=(0,)) + + +def late_spouse_records(reduction, first_year=2027): + """Two invented living spouses; the later excess has no age reduction. + + The claimant claims own retirement at 62 in 2025 and spouse excess at + 67 in 2030. The linked worker first claims at 62 in 2030. These are + valid eligibility and entitlement clocks; all paths retain dime floors. + """ + params = invented_params() + baseline = sb.COLASeries( + by_determination_year={year: 0.028 for year in range(1979, 2031)}, + provenance={"invented": True}, + ) + reform = sb.COLAReform( + annual_reduction=reduction, + first_reduced_determination_year=first_year, + ) + own_clock = sb.WorkerClock.at_age_62(1963, entitlement_year=2025) + linked_clock = sb.WorkerClock.at_age_62(1968, entitlement_year=2030) + own_factor = 1 - ss_benefits.early_reduction( + params.fra_months(1963) - 62 * 12, params + ) + own = sb.OwnBenefit(100, own_factor, own_clock) + own_path = sb.worker_scenario_paths( + eligibility_pia=own.eligibility_pia, + claim_age_factor=own.claim_age_factor, + clock=own.clock, + baseline=baseline, + reform=reform, + ) + linked_path = sb.worker_scenario_paths( + eligibility_pia=260, + claim_age_factor=own_factor, + clock=linked_clock, + baseline=baseline, + reform=reform, + ) + spouse_path = sb.spouse_scenario_paths( + worker_eligibility_pia=260, + worker_clock=linked_clock, + own=own, + months_early=max(0, params.fra_months(1963) - 67 * 12), + entitlement_year=2030, + params=params, + baseline=baseline, + reform=reform, + ) + base_own = own_path.baseline_monthly_by_payment_year[2030] + reform_own = own_path.reform_monthly_by_payment_year[2030] + base_spouse = spouse_path.baseline_monthly_by_payment_year[2030] + reform_spouse = spouse_path.reform_monthly_by_payment_year[2030] + claimant = record( + 1, + family=1, + age=67, + base=12 * (base_own + base_spouse), + reform=12 * (reform_own + reform_spouse), + spouse_class="another_worker", + ) + claimant["benefit_components"] = { + "retired_worker": {"base": 12 * base_own, "reform": 12 * reform_own}, + "spouse": {"base": 12 * base_spouse, "reform": 12 * reform_spouse}, + } + linked = record( + 2, + family=1, + age=62, + base=12 * linked_path.baseline_monthly_by_payment_year[2030], + reform=12 * linked_path.reform_monthly_by_payment_year[2030], + ) + assert own_path.reduced_increases_by_payment_year[2030] >= 3 + assert linked_path.reduced_increases_by_payment_year[2030] == 0 + assert base_spouse > 0 # the legacy baseline-only excess gate keeps it + return [claimant, linked] + + +def test_valid_dual_entitlement_can_raise_benefits_under_registered_cut(): + records = late_spouse_records(0.01) + claimant, linked = records + assert claimant["benefit_reform"] > claimant["benefit_base"] + assert linked["benefit_reform"] == linked["benefit_base"] + result = ac.summarize_cost(records, draw_indices=(0,)) + assert result["per_draw"][0][ac.P] > 0 + assert result["intended_bound_violations"] == list( + ac.INTENDED_BOUND_VIOLATIONS + ) + mint = ac.summarize_mint(records, draw_indices=(0,)) + assert ( + mint["bands"]["Total"]["per_draw"][0][ac.MEDIAN_INDIVIDUAL_PCT_CHANGE] + > 0 + ) + assert mint["intended_bound_violations"] == list( + ac.INTENDED_BOUND_VIOLATIONS + ) + earlier = ac.summarize_cost( + late_spouse_records(0.01, 2026), draw_indices=(0,) + ) + assert earlier["per_draw"][0][ac.P] > result["per_draw"][0][ac.P] + json.dumps(result, allow_nan=False) + + +@given(st.integers(3, 20)) +@settings(max_examples=20, deadline=None) +def test_dual_entitlement_bound_violation_is_structural_under_dime_floors( + mills, +): + smaller = late_spouse_records(mills / 1000) + larger = late_spouse_records((mills + 1) / 1000) + assert smaller[0]["benefit_reform"] > smaller[0]["benefit_base"] + assert larger[0]["benefit_reform"] >= smaller[0]["benefit_reform"] + for rows in (smaller, larger): + result = ac.summarize_cost(rows, draw_indices=(0,), floor_seeds=(0, 1)) + assert result["per_draw"][0][ac.P] > 0 diff --git a/tests/acola_track/test_timing_properties.py b/tests/acola_track/test_timing_properties.py new file mode 100644 index 000000000..19ad805b8 --- /dev/null +++ b/tests/acola_track/test_timing_properties.py @@ -0,0 +1,215 @@ +"""Invented people pin future start years and rounding-aware bounds.""" + +import math +from dataclasses import replace + +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +from populace_dynamics import scenario_benefits as sb +from populace_dynamics.acola_track.config import REGISTERED_ROWS, ACOLAConfig +from populace_dynamics.cola_track_a.benefits import ( + BenefitContext, + reference_benefit_rows, +) +from populace_dynamics.engine.rng import ( + ProjectionModule, + ProjectionRNGRegistry, +) +from populace_dynamics.estimates.parameters import COLASeries +from populace_dynamics.ss import benefits as ss_benefits +from tests.cola_track_a.test_assembly import ( + _career, + _handmade_cohort, + _state, + _static, + invented_cola, + invented_params, +) + + +@pytest.mark.parametrize("first", [2010, 2011, 2027]) +@pytest.mark.parametrize( + "eligible", [2008, 2010, 2011, 2027, 2028, 2029, 2030] +) +@pytest.mark.parametrize("period", list(sb.BenefitPeriod)) +def test_future_start_year_reaches_the_actual_legacy_emitter( + first, eligible, period +): + birth = eligible - 62 + cohort, result = _handmade_cohort( + [_static(1, birth)], + {1: _career(40_000.0, birth)}, + [ + _state( + 1, + birth, + 2030, + claimed=True, + claim_year=eligible, + claim_age=62, + marital_status="single", + ) + ], + [_state(1, birth, 2010, marital_status="single")], + ) + row = replace( + REGISTERED_ROWS["C0"], + first_reduced_determination_year=first, + benefit_period=period, + ) + rows, _ = reference_benefit_rows( + result, + draw=0, + row=row.track_row(), + context=BenefitContext( + cohort, + invented_params(), + invented_cola(), + ACOLAConfig(draw_indices=(0,)).track_a_config(), + ), + ) + payment = sb.payment_year_for_reference(2030, period) + assert len(rows) == 1 + assert rows[0]["reduced_increases"] == payment - max(first, eligible) + if ( + first == 2027 + and eligible == 2030 + and period is sb.BenefitPeriod.CALENDAR_YEAR + ): + assert rows[0]["benefit_base"] == rows[0]["benefit_reform"] + + +@given( + eligible=st.integers(2000, 2030), + first=st.sampled_from([2010, 2011, 2027]), + december=st.booleans(), + amount=st.integers(100, 10_000), + reduction_bp=st.integers(0, 150), +) +@settings(max_examples=100, deadline=None) +def test_cut_bounds_monotonicity_and_zero_reform_on_rounded_paths( + eligible, + first, + december, + amount, + reduction_bp, +): + # Uniform invented rates deliberately differ from the model's rates. + c = 0.031 + reduction = reduction_bp / 10_000 + end = 2030 + int(december) + baseline = COLASeries( + {year: c for year in range(1979, 2031)}, {"invented": True} + ) + clock = sb.WorkerClock.at_age_62(eligible - 62) + + def path(r, start): + rates = sb.scenario_rates( + baseline, + sb.COLAReform( + annual_reduction=r, first_reduced_determination_year=start + ), + clock=clock, + ) + return sb.increased_pia_path( + eligibility_pia=float(amount), + eligibility_year=eligible, + cola=rates, + horizon_year=end, + )[end] + + base = path(0, first) + reform = path(reduction, first) + change = 100 * (reform / base - 1) + factors = end - min(first, end) + full_null = 100 * ((1 + c - reduction) / (1 + c)) ** factors - 100 + # Each dime floor loses <0.1 monthly dollars. Bound the propagated + # losses, relative to this person's actual baseline, rather than + # asserting a universal percent tolerance at arbitrarily low levels. + rounding_tolerance_pp = ( + 100 + * 0.1 + * math.fsum((1 + c) ** j for j in range(end - eligible + 1)) + / base + ) + assert full_null - rounding_tolerance_pp - 1e-10 <= change <= 0 + assert ( + path(reduction, 2010) <= path(reduction, 2011) <= path(reduction, 2027) + ) + assert path(min(reduction + 0.001, 0.02), first) <= reform + if reduction == 0: + assert reform == base + assert change == 0 + + # The payable worker path also floors after the claim-age factor. + # Dollar rounding in the common eligibility AIME cannot introduce a + # scenario difference; retain the actual path's final dime loss here. + params = invented_params() + birth = eligible - 62 + factor = 1 - ss_benefits.early_reduction( + params.fra_months(birth) - 62 * 12, params + ) + entitled_clock = sb.WorkerClock.at_age_62(birth, entitlement_year=eligible) + + def worker_path(r, start): + paths = sb.worker_scenario_paths( + eligibility_pia=float(amount), + claim_age_factor=factor, + clock=entitled_clock, + baseline=baseline, + reform=sb.COLAReform( + annual_reduction=r, first_reduced_determination_year=start + ), + horizon_year=end, + ) + return paths.reform_monthly_by_payment_year[end] + + worker_base = worker_path(0, first) + worker_reform = worker_path(reduction, first) + worker_change = 100 * (worker_reform / worker_base - 1) + worker_tolerance_pp = ( + 100 + * 0.1 + * ( + factor * math.fsum((1 + c) ** j for j in range(end - eligible + 1)) + + 1 + ) + / worker_base + ) + assert full_null - worker_tolerance_pp - 1e-10 <= worker_change <= 0 + assert ( + worker_path(reduction, 2010) + <= worker_path(reduction, 2011) + <= worker_path(reduction, 2027) + ) + assert worker_path(min(reduction + 0.001, 0.02), first) <= worker_reform + if reduction == 0: + assert worker_reform == worker_base + assert worker_change == 0 + + +@given( + draw=st.integers(0, 19), + period=st.integers(1, 20), + module=st.sampled_from(list(ProjectionModule)), +) +@settings(max_examples=40, deadline=None) +def test_registered_draw_root_and_module_stream_are_deterministic( + draw, period, module +): + import numpy as np + + first = ProjectionRNGRegistry(draw, 20) + second = ProjectionRNGRegistry(draw, 20) + assert first.draw_seed == second.draw_seed == 5200 + draw + expected = first.generator(period, module).random(20) + # Query another stream to show calls cannot consume the tested stream. + first.generator(0, module).random(100) + np.testing.assert_array_equal( + first.generator(period, module).random(20), expected + ) + np.testing.assert_array_equal( + second.generator(period, module).random(20), expected + ) diff --git a/tests/acola_track/test_unchanged.py b/tests/acola_track/test_unchanged.py new file mode 100644 index 000000000..81dbf6a31 --- /dev/null +++ b/tests/acola_track/test_unchanged.py @@ -0,0 +1,41 @@ +"""Keep the legacy registries and every registered consumer unchanged.""" + +import hashlib +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +# origin/master at the assigned checkpoint, 75cd35245f1a0c12e7a23b048a7d126c74ad8ecb. +PINS = { + "src/populace_dynamics/cola_track_a/config.py": "8c72a3164fc8b185f51f8f888b1f6678a801cd665929cdd0575fa10fb877dfae", + "src/populace_dynamics/fra68_track/config.py": "2bb435e02025f5264991956935e5a33a1179d3b1d331de4b24bbe6c5e4bee141", + "src/populace_dynamics/fra68_track/reform.py": "47a35589fbff9022b13bc37b388febf0788a2c3fc03a252cee1895b5d0187a29", + "src/populace_dynamics/scenario_benefits.py": "dffe28fdac66eb849f387ed3a481b6aee85d470946951a755196761c70cc4cbf", + "src/populace_dynamics/track_a_v2/matrix.py": "ffd878d257f35ce551db589411d81a52ac7f121a0648edaf57f69f7393a331e8", + "src/populace_dynamics/track_a_v2/runner.py": "5df9d33b8a443cca2c31c5150ec930bc7b6862e3c4b28b19584fe1d596283cde", + "scripts/track_a_dry_run.py": "2bb935dd9d7e2ac9dc194dcc41e050b91e3894d056c3076cb6e4414927d45a4a", +} + + +@pytest.mark.parametrize("path, digest", PINS.items()) +def test_legacy_registries_constants_and_consumers_are_byte_unchanged( + path, digest +): + assert hashlib.sha256((ROOT / path).read_bytes()).hexdigest() == digest + + +def test_the_legacy_matrix_and_registered_rate_check_still_work(): + import importlib.util + + from populace_dynamics.cola_track_a.runner import tr2008_baseline_cola + from populace_dynamics.estimates.parameters import load_cola_history + from populace_dynamics.track_a_v2.matrix import MATRIX + + assert len(MATRIX) == 68 + spec = importlib.util.spec_from_file_location( + "acola_legacy_dry", ROOT / "scripts/track_a_dry_run.py" + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + module._spec_rate_check(tr2008_baseline_cola(load_cola_history())) From 75ef143a6bb24a7a28f41cd9be28ecd7cd4f91b4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 24 Oct 2026 10:51:42 -0400 Subject: [PATCH 2/6] A-COLA stage 2: the Registration 18 comparison script Adds scripts/compare_acola.py, a standard-library program for the validation lane after the seal opens. It fetches nothing, and it writes decimals as strings. It converts comparator records in the cleared brief's section-4 schema and the stage-1 dry-run artifacts into the registered comparisons. tests/acola_track/test_compare.py exercises it on wholly invented source values. Built by a Subfleet build lane (GPT-6.1 Sol, job 20261024-073819-mgf-f0-acola-stage2) under the cleared builder brief and its 2026-10-24 addendum. That lane's sandbox could not write .git, so the lane session committed its working tree in steps. Co-Authored-By: Claude Opus 5.5 --- scripts/compare_acola.py | 730 ++++++++++++++++++++++++++++++ tests/acola_track/test_compare.py | 717 +++++++++++++++++++++++++++++ 2 files changed, 1447 insertions(+) create mode 100644 scripts/compare_acola.py create mode 100644 tests/acola_track/test_compare.py diff --git a/scripts/compare_acola.py b/scripts/compare_acola.py new file mode 100644 index 000000000..c4940ea90 --- /dev/null +++ b/scripts/compare_acola.py @@ -0,0 +1,730 @@ +#!/usr/bin/env python3 +"""Registration 18 comparison, for the validation lane after seal opening. + +This standard-library program fetches nothing. Decimal values in its JSON +output are strings, preserving the computed digits without a float conversion. +The only float conversion implements the explicitly registered MINT reading S. +""" + +from __future__ import annotations + +import argparse +import hashlib +import itertools +import json +import math +import re +from decimal import ROUND_HALF_EVEN, ROUND_HALF_UP, Decimal, localcontext +from pathlib import Path + +PRECISION = 50 +BANDS = ("Total", "62-69", "70-79", "80-89", "90+") +MINT_STATISTICS = ( + "median_individual_pct_change", + "difference_of_weighted_medians_pct", + "ratio_of_scenario_means", +) +OACT_FOOTER = "Based on Intermediate Assumptions of the 2026 Trustees Report" + + +def _decimal(value): + if value is None: + return None + if isinstance(value, bool): + raise ValueError("a boolean is not a decimal") + parsed = Decimal(str(value)) + if not parsed.is_finite(): + raise ValueError("nonfinite decimal") + return parsed + + +def _decimals(value): + if value is None: + return None + parsed = int(value) + if parsed < 0 or Decimal(str(value)) != parsed: + raise ValueError("decimal count must be a nonnegative integer") + return parsed + + +def _half_unit(decimals): + return Decimal(5).scaleb(-decimals - 1) + + +def _undefined(reason): + return { + "point": None, + "lower": None, + "upper": None, + "undefined_reason": reason, + } + + +def _interval(point, lower=None, upper=None): + return { + "point": point, + "lower": point if lower is None else lower, + "upper": point if upper is None else upper, + "undefined_reason": None, + } + + +def subtract_interval(left, right): + """Return [a,b] - [c,d] = [a-d,b-c], retaining undefined reasons.""" + if left["lower"] is None or right["lower"] is None: + reasons = [ + item["undefined_reason"] + for item in (left, right) + if item["lower"] is None + ] + return _undefined("; ".join(reasons)) + with localcontext() as context: + context.prec = PRECISION + return _interval( + left["point"] - right["point"], + left["lower"] - right["upper"], + left["upper"] - right["lower"], + ) + + +def _corners(first, second, first_decimals, second_decimals, function): + with localcontext() as context: + context.prec = PRECISION + corners = [ + function(a, b) + for a, b in itertools.product( + ( + first - _half_unit(first_decimals), + first + _half_unit(first_decimals), + ), + ( + second - _half_unit(second_decimals), + second + _half_unit(second_decimals), + ), + ) + ] + return _interval(function(first, second), min(corners), max(corners)) + + +def _oact_formula(proposal, change): + if proposal - change <= 0: + raise ValueError("OACT p - d must be positive at every corner") + return Decimal(100) * change / (proposal - change) + + +def oact_comparison(record): + """Compute C_OACT and its registered printed-precision interval.""" + record = record or {} + year = record.get("row_label_verbatim") + if year is not None and str(year).strip() != "2030": + raise ValueError("OACT row must be 2030") + columns = { + column["key"]: column + for column in (record.get("columns") or []) + if column is not None + } + proposal = columns.get("proposal_cost_rate") or {} + change = columns.get("change_cost_rate") or {} + p, d = (_decimal(item.get("value")) for item in (proposal, change)) + pd, dd = (_decimals(item.get("decimals")) for item in (proposal, change)) + gate = ( + None + if p is None or d is None + else ( + "PWBM_primary_OACT_report_only" + if min(p.copy_abs(), d.copy_abs()) < Decimal("0.10") + else "OACT_primary_PWBM_secondary" + ) + ) + value = ( + _undefined("missing OACT cost value or printed decimals") + if None in (p, d, pd, dd) + else _corners(p, d, pd, dd, _oact_formula) + ) + with localcontext() as context: + context.prec = PRECISION + current_law = None if p is None or d is None else p - d + return { + "value": value, + "branch": "four_corners", + "current_law_cost_rate": current_law, + "precision_gate": gate, + "report_only": True, + "income_rate_memo": { + key: columns.get(key) + for key in ("proposal_income_rate", "change_income_rate") + }, + } + + +def _pwbm_formula(baseline, reform): + if baseline <= 0: + raise ValueError("PWBM B must be positive at every corner") + return Decimal(100) * (reform - baseline) / baseline + + +def pwbm_comparison(record): + """Use the six-decimal point rule, otherwise all four corners.""" + record = record or {} + row = record.get("row_2030") or {} + if row.get("year") is not None and str(row["year"]) != "2030": + raise ValueError("PWBM row must be 2030") + decimals = record.get("stored_decimals") or {} + keys = ("Baseline cost rate", "A1 cost rate") + b, r = (_decimal(row.get(key)) for key in keys) + bd, rd = (_decimals(decimals.get(key)) for key in keys) + if None in (b, r, bd, rd): + value, branch = ( + _undefined("missing PWBM cost value or stored decimals"), + None, + ) + elif min(bd, rd) >= 6: + with localcontext() as context: + context.prec = PRECISION + value, branch = _interval(_pwbm_formula(b, r)), "point" + else: + value = _corners(b, r, bd, rd, _pwbm_formula) + branch = "four_corners" + return { + "value": value, + "branch": branch, + "report_only": True, + "income_rate_memo": { + key: row.get(key) + for key in ("Baseline income rate", "A1 income rate") + }, + } + + +def _percent_format(number_format): + """Excel's quoted and escaped percent signs are literals.""" + unquoted = re.sub(r'"[^\"]*"', "", number_format) + return "%" in re.sub(r"\\.", "", unquoted) + + +def mint_interval(cell, reading="S"): + """Apply reading S or sensitivity L and identify both rounding modes.""" + cell = cell or {} + html = _decimal(cell.get("html_value")) + html_decimals = _decimals(cell.get("html_decimals")) + if html is None or html_decimals is None: + return { + "value": _undefined("missing MINT HTML value or decimals"), + "reading": reading, + "branch": None, + "rounding_modes_agree": [], + "matching_modes": [], + } + if reading == "S": + raw, number_format = cell.get("xlsx_raw"), cell.get( + "xlsx_number_format" + ) + if raw is None or number_format is None: + stored, stored_decimals = None, None + else: + double = float(raw) + if not math.isfinite(double): + raise ValueError("nonfinite stored MINT double") + stored = Decimal(repr(double)) + if _percent_format(number_format): + stored = stored.scaleb(2) + stored_decimals = max(0, -stored.as_tuple().exponent) + elif reading == "L": + stored = _decimal(cell.get("xlsx_value_pct")) + stored_decimals = _decimals(cell.get("xlsx_decimals_pct")) + else: + raise ValueError("MINT reading must be S or L") + with localcontext() as context: + context.prec = PRECISION + quantum = Decimal(1).scaleb(-html_decimals) + modes = ( + [] + if stored is None + else [ + name + for name, mode in ( + ("half_up", ROUND_HALF_UP), + ("half_even", ROUND_HALF_EVEN), + ) + if stored.quantize(quantum, rounding=mode) == html + ] + ) + use_stored = ( + stored is not None + and stored_decimals is not None + and stored_decimals > html_decimals + and bool(modes) + ) + point, decimals = ( + (stored, stored_decimals) if use_stored else (html, html_decimals) + ) + half = _half_unit(decimals) + return { + "value": _interval(point, point - half, point + half), + "reading": reading, + "branch": "xlsx" if use_stored else "html", + "rounding_modes_agree": modes, + "matching_modes": modes, + "stored_value_pct": stored, + "stored_decimals_pct": stored_decimals, + "missing_xlsx": stored is None or stored_decimals is None, + } + + +def _summary_interval(summary, missing_reason): + mean = _decimal(summary.get("mean")) + if mean is None: + return _undefined(summary.get("undefined_reason") or missing_reason) + return _interval(mean) + + +def _null_interval(nulls, key): + record = nulls.get(key) or {} + exact = record.get("exact") or {} + numerator = _decimal(exact.get("numerator")) + denominator = _decimal(exact.get("denominator")) + if numerator is not None and denominator is not None: + if denominator == 0: + raise ValueError(f"zero denominator for exact null {key}") + with localcontext() as context: + context.prec = PRECISION + value = numerator / denominator + elif record.get("value_decimal") is not None: + value = _decimal(record["value_decimal"]) + else: + value = _decimal(record.get("value")) + return ( + _undefined(record.get("undefined_reason") or f"missing null {key}") + if value is None + else _interval(value) + ) + + +def _comparison(summary, comparator, nulls, null_key, model_null_key): + summary = summary or { + "mean": None, + "sample_sd": None, + "floor": None, + "undefined_reason": "missing model statistic", + } + model = _summary_interval(summary, "undefined model statistic") + null = _null_interval(nulls, null_key) + model_null = _null_interval(nulls, model_null_key) + return { + "model": summary, + "comparator": comparator, + "null_key": null_key, + "model_null_key": model_null_key, + "model_minus_comparator": subtract_interval(model, comparator), + "null_minus_comparator": subtract_interval(null, comparator), + "model_minus_null": subtract_interval(model, null), + "model_minus_model_null": subtract_interval(model, model_null), + } + + +def _split(record, flag): + printed = record.get(flag) + if printed is False: + return { + "printed": False, + "comparison": None, + "note": "not printed by the comparator", + } + return { + "printed": printed, + "comparison": None, + "undefined_reason": ( + "numeric split schema unspecified" + if printed + else "split availability not stated" + ), + "note": "carried fields are retained in comparator_record", + } + + +def _flags(record): + flags = { + key: record.get(key) + for key in ( + "cost_includes_admin_expenses", + "cost_includes_railroad_interchange", + ) + } + admin, railroad = flags.values() + return { + **flags, + "dilution": ( + "unsigned" + if railroad != "no" or admin in (None, "not_stated") + else ( + "toward zero" + if admin == "yes" + else "none from these cost items" + ) + ), + "administrative_expenses_effect": { + "yes": "dilutes toward zero", + "no": "excluded", + }.get(admin, "not stated"), + "railroad_interchange_effect": { + "yes": "direction unknown", + "no": "excluded", + }.get(railroad, "not stated"), + "december_2030_timing": record.get("december_2030_timing"), + } + + +def _registration_checks(record, keys): + return { + key: { + "matches": record.get(key), + "undefined_reason": ( + "source lacks registration-match flag" + if record.get(key) is None + else None + ), + } + for key in keys + } + + +def build_comparison(artifact, comparator): + """Consume the stage-1 artifact shape without dropping undefined rows.""" + if comparator.get("schema_version") != "acola-comparator-v1": + raise ValueError("expected acola-comparator-v1") + oact, pwbm, mint = ( + comparator.get(key) or {} for key in ("oact", "pwbm", "mint5") + ) + footer = oact.get("footer_verbatim") + if footer is not None and footer.rstrip(".").strip() != OACT_FOOTER: + raise ValueError("OACT footer does not match registration") + for key in ( + "footer_matches_registration", + "provision_text_matches_registration", + ): + if oact.get(key) is False: + raise ValueError(f"OACT {key} is false") + nulls, rows = artifact.get("nulls") or {}, artifact.get("rows") or {} + cost = {} + for name, record, conversion in ( + ("OACT", oact, oact_comparison(oact)), + ("PWBM", pwbm, pwbm_comparison(pwbm)), + ): + cost[name] = { + "conversion": conversion, + "comparator_record": record, + "registration_checks": _registration_checks( + record, + ( + ( + "footer_matches_registration", + "provision_text_matches_registration", + ) + if name == "OACT" + else ("last_modified_matches_registration",) + ), + ), + "flags": _flags(record), + "oasi_di_split": _split(record, "prints_oasi_di_split_2030"), + "rows": { + row: { + **_comparison( + (rows.get(row) or {}).get("statistics", {}).get("P"), + conversion["value"], + nulls, + "N", + "N_model", + ), + "all_model_statistics": (rows.get(row) or {}).get( + "statistics" + ), + "model_row_record": rows.get(row), + } + for row in ("C0", "C1") + }, + } + median_type = mint.get("median_type") + headline = { + "median_of_individual_differences": MINT_STATISTICS[0], + "difference_of_medians": MINT_STATISTICS[1], + }.get(median_type) + mint_rows = {} + source_bands = (mint.get("table1") or {}).get("rows") or {} + for row_id in ("M0", "M1", "M2", "M3", "M4"): + null_key = "N_M1" if row_id == "M1" else "N_MINT" + model_row = rows.get(row_id) or {} + band_results = {} + for band in BANDS: + source_band = source_bands.get(band) or {} + cell = source_band.get("median_diff_pct") + registered, literal = mint_interval(cell), mint_interval(cell, "L") + model_band = (model_row.get("bands") or {}).get(band) or {} + statistics = model_band.get("statistics") or {} + band_results[band] = { + "registered_reading_S": registered, + "sensitivity_reading_L": literal, + "headline_statistic": headline, + "statistics": { + statistic: { + **_comparison( + statistics.get(statistic), + registered["value"], + nulls, + null_key, + null_key, + ), + "role": ( + "headline" if statistic == headline else "beside" + ), + "compared_as_median": statistic == headline + or ( + headline is None + and statistic in MINT_STATISTICS[:2] + ), + "sensitivity_reading_L": _comparison( + statistics.get(statistic), + literal["value"], + nulls, + null_key, + null_key, + ), + } + for statistic in MINT_STATISTICS + }, + "model_memo_items": { + key: statistics.get(key) + for key in ( + "baseline_falling_share", + "median_benefit_base", + "median_benefit_reform", + ) + }, + "comparator_memo_items": { + key: value + for key, value in source_band.items() + if key != "median_diff_pct" + }, + "dollar_levels_compared": False, + "model_band_record": model_band, + } + mint_rows[row_id] = { + "bands": band_results, + "model_row_record": model_row, + } + return { + "schema_version": "acola-comparison-v1", + "report_only": True, + "decimal_precision": PRECISION, + "decimal_encoding": "JSON strings", + "rounding_interpretation": "XLSX branch if either named mode agrees", + "cost": cost, + "mint5": { + "comparator_record": mint, + "headline_statistic": headline, + "median_type": median_type, + "start_date_reading": mint.get("start_date_reading"), + "december_2030_timing": mint.get("december_2030_timing"), + "oasi_di_split": _split(mint, "prints_oasi_di_split"), + "table2_memo": mint.get("table2"), + "rows": mint_rows, + }, + "nulls": nulls, + "N_minus_N_model": subtract_interval( + _null_interval(nulls, "N"), _null_interval(nulls, "N_model") + ), + "deferred_rows": artifact.get("deferred_rows"), + "model_artifact": artifact, + "comparator_record": comparator, + } + + +def _load(path): + return json.loads( + Path(path).read_text(encoding="utf-8-sig"), + parse_float=Decimal, + parse_constant=lambda value: (_ for _ in ()).throw( + ValueError(f"nonfinite JSON number: {value}") + ), + ) + + +def _sha256(content): + return hashlib.sha256(content).hexdigest() + + +def verify_seal(seal_path, comparator_path, evidence_root=None): + """Hash every listed file before parsing any comparator or artifact.""" + seal_path, comparator_path = ( + Path(seal_path), + Path(comparator_path).resolve(), + ) + root = Path(evidence_root or seal_path.parent).resolve() + seal_bytes = seal_path.read_bytes() + seal = json.loads(seal_bytes) + files = seal.get("files") + if not isinstance(files, dict) or not files: + raise ValueError("seal has no file inventory") + failures, verified = [], set() + for relative, expected in files.items(): + path = (root / relative).resolve() + if Path(relative).is_absolute() or not path.is_relative_to(root): + failures.append(f"invalid seal path: {relative}") + continue + try: + content = path.read_bytes() + except OSError: + failures.append(f"missing sealed file: {relative}") + continue + if _sha256(content) != expected.get("sha256") or len( + content + ) != expected.get("bytes"): + failures.append(f"seal mismatch: {relative}") + verified.add(path) + if comparator_path not in verified: + failures.append("comparator record is not listed in seal") + if failures: + raise ValueError("; ".join(failures)) + return { + "seal_sha256": _sha256(seal_bytes), + "verified_files": len(verified), + } + + +def compare_files( + artifact_path, comparator_path, seal_path, evidence_root=None +): + verification = verify_seal(seal_path, comparator_path, evidence_root) + artifact_path = Path(artifact_path) + sidecar_path = artifact_path.with_suffix(".env.json") + sidecar = _load(sidecar_path) + content = artifact_path.read_bytes() + if sidecar.get("artifact_sha256") != _sha256(content): + raise ValueError("artifact SHA-256 differs from .env.json sidecar") + if sidecar.get("artifact") != artifact_path.name: + raise ValueError("sidecar names another artifact") + result = build_comparison(_load(artifact_path), _load(comparator_path)) + result["verification"] = { + **verification, + "artifact_sha256": _sha256(content), + "sidecar_sha256": _sha256(sidecar_path.read_bytes()), + } + return result + + +def _json_default(value): + if isinstance(value, Decimal): + return str(value) + raise TypeError(f"cannot serialize {type(value).__name__}") + + +def json_result(result): + return ( + json.dumps(result, indent=2, default=_json_default, allow_nan=False) + + "\n" + ) + + +def _display(value): + if isinstance(value, dict) and "lower" in value: + if value["lower"] is None: + return "undefined: " + value["undefined_reason"] + return f'[{value["lower"]}, {value["upper"]}]' + return json.dumps( + value, default=_json_default, ensure_ascii=False + ).replace("|", "\\|") + + +def markdown_result(result): + """Include all digits, uncertainty, null reasons, flags and source memos.""" + verification = result.get("verification") or {} + lines = [ + "# Registration 18 comparison (report-only)", + "", + f'Seal SHA-256: `{verification.get("seal_sha256")}`', + "", + "Decimal arithmetic uses 50 significant digits; decimal JSON values are strings.", + "", + "| Row | Comparator | Band/statistic | Mean | Draw SD | Floor | Comparator interval | Model − comparator | Null − comparator | Model − null | Model − model null |", + "|---|---|---|---|---|---|---|---|---|---|---|", + ] + + def append(row, source, label, comparison): + model = comparison["model"] + values = [ + row, + source, + label, + model.get("mean"), + model.get("sample_sd"), + model.get("floor"), + comparison["comparator"], + comparison["model_minus_comparator"], + comparison["null_minus_comparator"], + comparison["model_minus_null"], + comparison["model_minus_model_null"], + ] + lines.append( + "| " + " | ".join(_display(item) for item in values) + " |" + ) + + for name, source in result["cost"].items(): + for row, comparison in source["rows"].items(): + append(row, name, "P", comparison) + for row, record in result["mint5"]["rows"].items(): + for band, cell in record["bands"].items(): + for statistic, comparison in cell["statistics"].items(): + append( + row, + "MINT5 S", + f"{band}: {statistic} ({comparison['role']})", + comparison, + ) + append( + row, + "MINT5 L sensitivity", + f"{band}: {statistic}", + comparison["sensitivity_reading_L"], + ) + lines += [ + "", + "All carried records, memo items, timing and cost-definition flags,", + "rounding branches, deferred rows and undefined reasons follow.", + "Dollar levels are memo items; no dollar-level comparison is made.", + "", + "```json", + json_result(result).rstrip(), + "```", + "", + ] + return "\n".join(lines) + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--artifact", required=True, type=Path) + parser.add_argument("--comparator", required=True, type=Path) + parser.add_argument("--seal", required=True, type=Path) + parser.add_argument("--evidence-root", type=Path) + parser.add_argument("--output-json", required=True, type=Path) + parser.add_argument("--output-markdown", required=True, type=Path) + args = parser.parse_args(argv) + result = compare_files( + args.artifact, args.comparator, args.seal, args.evidence_root + ) + inputs = { + args.artifact.resolve(), + args.artifact.with_suffix(".env.json").resolve(), + args.comparator.resolve(), + args.seal.resolve(), + } + outputs = {args.output_json.resolve(), args.output_markdown.resolve()} + if inputs & outputs or len(outputs) != 2: + raise ValueError("comparison outputs must be distinct from inputs") + args.output_json.write_text(json_result(result), encoding="utf-8") + args.output_markdown.write_text(markdown_result(result), encoding="utf-8") + print(f'Seal SHA-256: {result["verification"]["seal_sha256"]}') + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/acola_track/test_compare.py b/tests/acola_track/test_compare.py new file mode 100644 index 000000000..3e6a96f44 --- /dev/null +++ b/tests/acola_track/test_compare.py @@ -0,0 +1,717 @@ +"""Registration 18 comparison rules, using wholly invented source values.""" + +from __future__ import annotations + +import ast +import hashlib +import importlib.util +import json +from copy import deepcopy +from decimal import Decimal, localcontext +from pathlib import Path + +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +ROOT = Path(__file__).resolve().parents[2] +SPEC = importlib.util.spec_from_file_location( + "_compare_acola", ROOT / "scripts" / "compare_acola.py" +) +compare = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(compare) +D = Decimal + + +def invented_cell(**changes): + cell = { + "html_display": "-7.3", + "html_value": "-7.3", + "html_decimals": 1, + "xlsx_cell": "D7", + "xlsx_raw": "-0.073200000000000001", + "xlsx_number_format": "0.0%", + "xlsx_value_pct": "-7.3200000000000001", + "xlsx_decimals_pct": 16, + "pdf_display": "-7.3", + } + cell.update(changes) + return cell + + +def invented_comparator(): + """Schema-only fixture; these values describe no published comparator.""" + timing = { + "reading": "not_stated", + "evidence_verbatim": "INVENTED timing note", + "evidence_source": "invented.txt", + } + return { + "schema_version": "acola-comparator-v1", + "producer": "invented", + "sources": {}, + "oact": { + "footer_verbatim": compare.OACT_FOOTER + ".", + "footer_matches_registration": True, + "provision_text_matches_registration": True, + "row_label_verbatim": "2030", + "columns": [ + { + "key": key, + "header_verbatim": "INVENTED " + key, + "display": value, + "value": value, + "decimals": 2, + } + for key, value in ( + ("proposal_cost_rate", "21.75"), + ("change_cost_rate", "-0.75"), + ("proposal_income_rate", "18.25"), + ("change_income_rate", "-0.25"), + ) + ], + "prints_oasi_di_split_2030": False, + "cost_includes_admin_expenses": "yes", + "cost_includes_railroad_interchange": "yes", + "december_2030_timing": timing, + }, + "pwbm": { + "last_modified": "INVENTED", + "last_modified_matches_registration": True, + "row_2030": { + "year": "2030", + "Baseline income rate": "14.000000", + "Baseline cost rate": "25.000000", + "A1 income rate": "13.500000", + "A1 cost rate": "24.000000", + }, + "stored_decimals": { + key: 6 + for key in ( + "Baseline income rate", + "Baseline cost rate", + "A1 income rate", + "A1 cost rate", + ) + }, + "prints_oasi_di_split_2030": False, + "cost_includes_admin_expenses": "not_stated", + "cost_includes_railroad_interchange": "not_stated", + "december_2030_timing": timing, + }, + "mint5": { + "table1": { + "rows": { + band: { + "median_diff_pct": invented_cell(), + "median_diff_dollars": None, + "median_monthly_scheduled": {"html_value": "1600"}, + "median_monthly_option": {"html_value": "1500"}, + "median_monthly_payable": None, + "pct_population_affected": {"html_value": "75"}, + } + for band in compare.BANDS + } + }, + "table2": {"rows": {"Total": {"invented_memo": None}}}, + "median_type": "median_of_individual_differences", + "start_date_reading": "unsettled", + "december_2030_timing": timing, + "prints_oasi_di_split": False, + }, + } + + +def invented_summary(mean=-5): + return { + "mean": mean, + "sample_sd": 0.125, + "per_draw": [mean, mean], + "floor": { + "mean": None, + "defined": False, + "undefined_reason": "INVENTED empty half", + }, + "undefined_reason": None if mean is not None else "empty band", + } + + +def invented_artifact(): + summary = invented_summary() + mint_stats = {stat: deepcopy(summary) for stat in compare.MINT_STATISTICS} + mint_stats.update( + { + key: invented_summary(1000) + for key in ( + "baseline_falling_share", + "median_benefit_base", + "median_benefit_reform", + ) + } + ) + rows = { + row: {"statistics": {"P": deepcopy(summary)}} for row in ("C0", "C1") + } + rows.update( + { + row: { + "bands": { + band: {"statistics": deepcopy(mint_stats)} + for band in compare.BANDS + } + } + for row in ("M0", "M1", "M2", "M3", "M4") + } + ) + return { + "schema_version": "acola-track-v1", + "rows": rows, + "nulls": { + key: {"value": value} + for key, value in ( + ("N", -3), + ("N_model", -2), + ("N_MINT", -17), + ("N_M1", -18), + ) + }, + "deferred_rows": { + "C0-D": {"status": "deferred", "reason": "INVENTED"} + }, + } + + +def write_inputs(tmp_path, artifact=None): + artifact_path, record_path = ( + tmp_path / "invented.json", + tmp_path / "reconciled.json", + ) + artifact_path.write_text(json.dumps(artifact or invented_artifact())) + sidecar = { + "artifact": artifact_path.name, + "artifact_sha256": hashlib.sha256( + artifact_path.read_bytes() + ).hexdigest(), + } + artifact_path.with_suffix(".env.json").write_text(json.dumps(sidecar)) + record_path.write_text(json.dumps(invented_comparator())) + extra = tmp_path / "invented.txt" + extra.write_text("INVENTED source bytes") + files = { + path.name: { + "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), + "bytes": path.stat().st_size, + } + for path in (record_path, extra) + } + seal = tmp_path / "seal.json" + seal.write_text(json.dumps({"files": files})) + return artifact_path, record_path, seal + + +def test_oact_conversion_uses_proposal_minus_change_and_decimal_corners(): + result = compare.oact_comparison(invented_comparator()["oact"]) + with localcontext() as context: + context.prec = compare.PRECISION + assert result["value"]["point"] == D(100) * D("-0.75") / D("22.50") + assert result["current_law_cost_rate"] == D("22.50") + assert ( + result["value"]["lower"] + < result["value"]["point"] + < result["value"]["upper"] + ) + assert result["precision_gate"] == "OACT_primary_PWBM_secondary" + assert result["income_rate_memo"]["change_income_rate"]["value"] == "-0.25" + + +@given( + p=st.integers(1, 10000), d=st.integers(-1000, 0), places=st.integers(1, 5) +) +@settings(max_examples=60, deadline=None) +def test_oact_corners_contain_point_and_monotonicity(p, d, places): + record = invented_comparator()["oact"] + record["columns"][0].update(value=str(p), decimals=places) + record["columns"][1].update(value=str(d), decimals=places) + interval = compare.oact_comparison(record)["value"] + assert interval["lower"] <= interval["point"] <= interval["upper"] + with localcontext() as context: + context.prec = compare.PRECISION + proposal, change, delta = D(p), D(d), D("0.001") + base = compare._oact_formula(proposal, change) + assert compare._oact_formula(proposal, change + delta) >= base + assert compare._oact_formula(proposal + delta, change) >= base + + +@pytest.mark.parametrize( + "value,expected", + [ + ("-0.09", "PWBM_primary_OACT_report_only"), + ("-0.10", "OACT_primary_PWBM_secondary"), + ], +) +def test_oact_precision_gate_boundary(value, expected): + record = invented_comparator()["oact"] + record["columns"][1]["value"] = value + assert compare.oact_comparison(record)["precision_gate"] == expected + record["columns"][1]["value"] = "-0.09999999999999999999999999999999999" + assert ( + compare.oact_comparison(record)["precision_gate"] + == "PWBM_primary_OACT_report_only" + ) + + +def test_oact_decimals_are_not_hard_coded_and_every_corner_positive(): + record = invented_comparator()["oact"] + record["columns"][0]["decimals"] = 0 + wider = compare.oact_comparison(record)["value"] + original = compare.oact_comparison(invented_comparator()["oact"])["value"] + assert wider["lower"] < original["lower"] + assert wider["upper"] > original["upper"] + record["columns"][0].update(value="0.01", decimals=2) + record["columns"][1].update(value="0", decimals=0) + with pytest.raises(ValueError, match="every corner"): + compare.oact_comparison(record) + + +def test_oact_wrong_year_refuses(): + record = invented_comparator()["oact"] + record["row_label_verbatim"] = "2031" + with pytest.raises(ValueError, match="2030"): + compare.oact_comparison(record) + + +@pytest.mark.parametrize( + "bd,rd,branch", + [ + (6, 6, "point"), + (15, 15, "point"), + (5, 6, "four_corners"), + (6, 5, "four_corners"), + ], +) +def test_pwbm_decimal_branches_and_point_containment(bd, rd, branch): + record = invented_comparator()["pwbm"] + record["stored_decimals"].update( + {"Baseline cost rate": bd, "A1 cost rate": rd} + ) + result = compare.pwbm_comparison(record) + value = result["value"] + assert result["branch"] == branch + assert value["point"] == -4 + assert value["lower"] <= value["point"] <= value["upper"] + assert (value["lower"] == value["upper"]) == (branch == "point") + + +@given(b=st.integers(1, 10000), r=st.integers(0, 10000)) +@settings(max_examples=50, deadline=None) +def test_pwbm_corner_interval_contains_point(b, r): + record = invented_comparator()["pwbm"] + record["row_2030"].update( + {"Baseline cost rate": str(b), "A1 cost rate": str(r)} + ) + record["stored_decimals"].update( + {"Baseline cost rate": 2, "A1 cost rate": 2} + ) + value = compare.pwbm_comparison(record)["value"] + assert value["lower"] <= value["point"] <= value["upper"] + + +def test_pwbm_refuses_nonpositive_corner_and_wrong_year(): + record = invented_comparator()["pwbm"] + record["row_2030"]["Baseline cost rate"] = "0.001" + record["stored_decimals"]["Baseline cost rate"] = 0 + with pytest.raises(ValueError, match="every corner"): + compare.pwbm_comparison(record) + record["row_2030"]["year"] = "2029" + with pytest.raises(ValueError, match="2030"): + compare.pwbm_comparison(record) + + +def test_mint_shortest_reading_S_and_literal_sensitivity_L(): + shortest = compare.mint_interval(invented_cell()) + literal = compare.mint_interval(invented_cell(), "L") + assert shortest["branch"] == literal["branch"] == "xlsx" + assert shortest["value"]["point"] == D("-7.32") + assert shortest["stored_decimals_pct"] == 2 + assert literal["value"]["point"] == D("-7.3200000000000001") + assert literal["stored_decimals_pct"] == 16 + assert shortest["matching_modes"] == ["half_up", "half_even"] + assert shortest["value"]["lower"] == D("-7.325") + assert shortest["value"]["upper"] == D("-7.315") + + +@pytest.mark.parametrize( + "raw,html,expected_modes", + [("-0.0725", "-7.3", ["half_up"]), ("-0.0725", "-7.2", ["half_even"])], +) +def test_negative_tie_discloses_agreeing_modes(raw, html, expected_modes): + result = compare.mint_interval( + invented_cell(xlsx_raw=raw, html_value=html) + ) + assert result["branch"] == "xlsx" + assert result["matching_modes"] == expected_modes + + +@pytest.mark.parametrize( + "changes,point,branch", + [ + ({"xlsx_raw": "-0.073"}, D("-7.3"), "html"), + ({"xlsx_raw": "-0.0853"}, D("-7.3"), "html"), + ({"xlsx_raw": None}, D("-7.3"), "html"), + ({"xlsx_number_format": None}, D("-7.3"), "html"), + ( + {"xlsx_raw": "-7.32", "xlsx_number_format": "0.00"}, + D("-7.32"), + "xlsx", + ), + ( + {"xlsx_raw": "-7.32", "xlsx_number_format": '0.00"%"'}, + D("-7.32"), + "xlsx", + ), + ( + {"xlsx_raw": "-7.32", "xlsx_number_format": "0.00\\%"}, + D("-7.32"), + "xlsx", + ), + ({"xlsx_raw": "-7.32e-2"}, D("-7.32"), "xlsx"), + ], +) +def test_mint_interval_branches_and_formats(changes, point, branch): + result = compare.mint_interval(invented_cell(**changes)) + assert result["branch"] == branch + assert result["value"]["point"] == point + + +def test_literal_branch_uses_recorded_count_and_fallback(): + cell = invented_cell(xlsx_value_pct="-7.35", xlsx_decimals_pct=2) + assert compare.mint_interval(cell, "L")["branch"] == "html" + cell["xlsx_value_pct"] = None + assert compare.mint_interval(cell, "L")["missing_xlsx"] is True + + +def test_interval_subtraction_and_null_propagation(): + left = compare._interval(D(2), D(1), D(3)) + right = compare._interval(D(6), D(4), D(8)) + assert compare.subtract_interval(left, right) == compare._interval( + D(-4), D(-7), D(-1) + ) + assert ( + compare.subtract_interval(left, compare._undefined("missing"))[ + "undefined_reason" + ] + == "missing" + ) + + +def test_null_uses_exact_rational_then_decimal_before_float_approximation(): + record = { + "exact": {"numerator": "-1", "denominator": "3"}, + "value_decimal": "-0.3333", + "value": -0.33, + } + with localcontext() as context: + context.prec = compare.PRECISION + expected = -D(1) / D(3) + assert compare._null_interval({"N": record}, "N")["point"] == expected + record.pop("exact") + assert compare._null_interval({"N": record}, "N")["point"] == D("-0.3333") + record.pop("value_decimal") + assert compare._null_interval({"N": record}, "N")["point"] == D("-0.33") + + +def test_real_spec_only_null_records_keep_exact_precision(): + from populace_dynamics.acola_track.nulls import build_nulls + + nulls = build_nulls() + for key in ("N", "N_model", "N_MINT", "N_M1"): + record = nulls[key] + with localcontext() as context: + context.prec = compare.PRECISION + exact = D(record["exact"]["numerator"]) / D( + record["exact"]["denominator"] + ) + assert compare._null_interval(nulls, key)["point"] == exact + + +@pytest.mark.parametrize( + "median_type,headline", + [ + ("median_of_individual_differences", "median_individual_pct_change"), + ("difference_of_medians", "difference_of_weighted_medians_pct"), + ("unsettled", None), + (None, None), + ], +) +def test_headline_follows_record_and_all_rows_bands_statistics_survive( + median_type, headline +): + record = invented_comparator() + record["mint5"]["median_type"] = median_type + result = compare.build_comparison(invented_artifact(), record) + assert result["mint5"]["headline_statistic"] == headline + for row in ("M0", "M1", "M2", "M3", "M4"): + bands = result["mint5"]["rows"][row]["bands"] + assert set(bands) == set(compare.BANDS) + for cell in bands.values(): + assert set(cell["statistics"]) == set(compare.MINT_STATISTICS) + assert cell["headline_statistic"] == headline + assert cell["dollar_levels_compared"] is False + null_key = "N_M1" if row == "M1" else "N_MINT" + value = cell["statistics"][compare.MINT_STATISTICS[0]] + assert value["null_key"] == null_key + assert value["model_minus_null"]["point"] == ( + 13 if row == "M1" else 12 + ) + for source in result["cost"].values(): + assert set(source["rows"]) == {"C0", "C1"} + for cell in source["rows"].values(): + assert cell["model_minus_null"]["point"] == -2 + assert cell["model_minus_model_null"]["point"] == -3 + + +def test_null_fields_are_reported_never_guessed_and_empty_band_serializes(): + artifact, record = invented_artifact(), invented_comparator() + artifact["rows"]["M0"]["bands"]["90+"]["statistics"][ + compare.MINT_STATISTICS[0] + ] = invented_summary(None) + record["oact"]["columns"][0]["value"] = None + record["pwbm"]["stored_decimals"]["A1 cost rate"] = None + record["mint5"]["table1"]["rows"]["90+"]["median_diff_pct"] = None + record["mint5"]["start_date_reading"] = None + result = compare.build_comparison(artifact, record) + assert result["cost"]["OACT"]["conversion"]["value"]["point"] is None + assert result["cost"]["PWBM"]["conversion"]["value"]["point"] is None + cell = result["mint5"]["rows"]["M0"]["bands"]["90+"] + assert cell["registered_reading_S"]["value"]["point"] is None + assert cell["statistics"][compare.MINT_STATISTICS[0]][ + "model_minus_comparator" + ]["undefined_reason"] + serialized = compare.json_result(result) + assert "empty band" in serialized + assert "INVENTED empty half" in serialized + assert "NaN" not in serialized + + +def test_flags_timing_memos_and_split_are_read_from_records(): + record = invented_comparator() + record["mint5"]["start_date_reading"] = "M1" + record["oact"]["december_2030_timing"][ + "reading" + ] = "excludes_dec2030_increase" + result = compare.build_comparison(invented_artifact(), record) + assert result["cost"]["PWBM"]["flags"]["dilution"] == "unsigned" + assert ( + result["cost"]["OACT"]["flags"]["december_2030_timing"]["reading"] + == "excludes_dec2030_increase" + ) + assert result["mint5"]["start_date_reading"] == "M1" + assert result["mint5"]["table2_memo"] == record["mint5"]["table2"] + assert ( + result["cost"]["OACT"]["oasi_di_split"]["note"] + == "not printed by the comparator" + ) + record["oact"]["prints_oasi_di_split_2030"] = True + record["oact"]["invented_split_fields"] = {"retained": "123"} + result = compare.build_comparison(invented_artifact(), record) + assert ( + result["cost"]["OACT"]["oasi_di_split"]["undefined_reason"] + == "numeric split schema unspecified" + ) + assert result["cost"]["OACT"]["comparator_record"][ + "invented_split_fields" + ] == {"retained": "123"} + + +@pytest.mark.parametrize( + "admin,railroad,dilution", + [ + ("no", "no", "none from these cost items"), + ("yes", "no", "toward zero"), + ("no", "yes", "unsigned"), + ("yes", "not_stated", "unsigned"), + ], +) +def test_dilution_claims_follow_each_flag(admin, railroad, dilution): + flags = compare._flags( + { + "cost_includes_admin_expenses": admin, + "cost_includes_railroad_interchange": railroad, + } + ) + assert flags["dilution"] == dilution + assert (flags["administrative_expenses_effect"] == "excluded") == ( + admin == "no" + ) + if railroad == "no": + assert flags["railroad_interchange_effect"] == "excluded" + + +def test_missing_registration_flags_have_explicit_unknown_status(): + record = invented_comparator() + record["oact"]["footer_matches_registration"] = None + record["oact"]["provision_text_matches_registration"] = None + result = compare.build_comparison(invented_artifact(), record) + for check in result["cost"]["OACT"]["registration_checks"].values(): + assert check["matches"] is None + assert check["undefined_reason"] + + +@pytest.mark.parametrize( + "key", + ["footer_matches_registration", "provision_text_matches_registration"], +) +def test_oact_registration_mismatch_refuses(key): + record = invented_comparator() + record["oact"][key] = False + with pytest.raises(ValueError, match=key): + compare.build_comparison(invented_artifact(), record) + + +def test_wrong_oact_footer_refuses_without_substitution(): + record = invented_comparator() + record["oact"]["footer_verbatim"] = "INVENTED wrong vintage" + with pytest.raises(ValueError, match="footer"): + compare.build_comparison(invented_artifact(), record) + + +@pytest.mark.parametrize( + "mutation", ["hash", "bytes", "missing", "unsealed_comparator"] +) +def test_seal_checks_every_entry_and_precedes_any_artifact_read( + tmp_path, monkeypatch, mutation +): + artifact, record, seal = write_inputs(tmp_path) + inventory = json.loads(seal.read_text()) + if mutation == "hash": + (tmp_path / "invented.txt").write_text("TAMPERED") + elif mutation == "bytes": + inventory["files"]["invented.txt"]["bytes"] += 1 + elif mutation == "missing": + (tmp_path / "invented.txt").unlink() + else: + inventory["files"].pop("reconciled.json") + seal.write_text(json.dumps(inventory)) + artifact.unlink() + monkeypatch.setattr( + compare, + "_load", + lambda path: pytest.fail("parsed values before seal verification"), + ) + with pytest.raises(ValueError, match="seal|sealed"): + compare.compare_files(artifact, record, seal) + + +def test_seal_reports_all_failures_and_refuses_path_escape(tmp_path): + _, record, seal = write_inputs(tmp_path) + inventory = json.loads(seal.read_text()) + inventory["files"].update( + { + "../outside": {"sha256": "x", "bytes": 0}, + "missing": {"sha256": "x", "bytes": 0}, + } + ) + seal.write_text(json.dumps(inventory)) + with pytest.raises(ValueError) as error: + compare.verify_seal(seal, record) + assert "invalid seal path" in str(error.value) + assert "missing sealed file" in str(error.value) + + +@pytest.mark.parametrize( + "change", ["artifact", "sidecar_hash", "sidecar_filename"] +) +def test_artifact_sidecar_is_verified(tmp_path, change): + artifact, record, seal = write_inputs(tmp_path) + sidecar = artifact.with_suffix(".env.json") + data = json.loads(sidecar.read_text()) + if change == "artifact": + artifact.write_text("{}") + elif change == "sidecar_hash": + data["artifact_sha256"] = "0" * 64 + else: + data["artifact"] = "another.json" + sidecar.write_text(json.dumps(data)) + with pytest.raises(ValueError, match="sidecar"): + compare.compare_files(artifact, record, seal) + + +def test_cli_writes_exact_json_markdown_and_prints_seal_hash(tmp_path, capsys): + artifact, record, seal = write_inputs(tmp_path) + out_json, out_md = tmp_path / "result.json", tmp_path / "result.md" + assert ( + compare.main( + [ + "--artifact", + str(artifact), + "--comparator", + str(record), + "--seal", + str(seal), + "--output-json", + str(out_json), + "--output-markdown", + str(out_md), + ] + ) + == 0 + ) + seal_hash = hashlib.sha256(seal.read_bytes()).hexdigest() + assert seal_hash in capsys.readouterr().out + result = json.loads(out_json.read_text()) + assert result["verification"]["seal_sha256"] == seal_hash + assert result["verification"]["verified_files"] == 2 + markdown = out_md.read_text() + assert "| Row | Comparator |" in markdown + assert "MINT5 L sensitivity" in markdown + assert '"sample_sd": "0.125"' in markdown + assert "not printed by the comparator" in markdown + assert '"value": "-7.3200000000000001"' not in markdown + assert "-7.3200000000000001" in markdown + assert "verdict" not in result and "pass_band" not in result + + +def test_load_keeps_decimals_bom_and_refuses_nonfinite(tmp_path): + path = tmp_path / "number.json" + path.write_text('\ufeff{"value": 1.2345678901234567890123456789}') + assert compare._load(path)["value"] == D("1.2345678901234567890123456789") + path.write_text('{"value": NaN}') + with pytest.raises(ValueError, match="nonfinite"): + compare._load(path) + + +def test_script_has_only_standard_library_dependencies(): + import sys + + tree = ast.parse((ROOT / "scripts" / "compare_acola.py").read_text()) + for node in ast.walk(tree): + if isinstance(node, ast.Import): + assert all( + alias.name.split(".")[0] in sys.stdlib_module_names + for alias in node.names + ) + elif isinstance(node, ast.ImportFrom): + assert node.module.split(".")[0] in sys.stdlib_module_names + + +def test_consumes_actual_stage1_invented_dry_artifact_layout(tmp_path): + from populace_dynamics.acola_track.runner import run_acola + from tests.acola_track.test_runner import CONFIG, _inputs + + artifact = run_acola(_inputs(), config=CONFIG) + paths = write_inputs(tmp_path, artifact) + result = compare.compare_files(*paths) + assert result["deferred_rows"]["C0-D"]["status"] == "deferred" + assert set(result["mint5"]["rows"]) == {"M0", "M1", "M2", "M3", "M4"} + source = artifact["rows"]["C0"]["statistics"]["P"] + model = result["cost"]["OACT"]["rows"]["C0"]["model"] + assert model["mean"] == D(str(source["mean"])) + assert model["sample_sd"] == D(str(source["sample_sd"])) + assert model["floor"]["defined"] == source["floor"]["defined"] + assert ( + result["mint5"]["rows"]["M1"]["bands"]["Total"]["statistics"][ + compare.MINT_STATISTICS[0] + ]["null_key"] + == "N_M1" + ) + compare.json_result(result) From d17d1d00115c96f5fe378d8cbd4fb8d2c9703540 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 24 Oct 2026 10:51:42 -0400 Subject: [PATCH 3/6] A-COLA stage 2: mechanism-L equivalence with the registered commit scripts/acola_mechanism_l_equivalence.py runs mechanism L for rows C0, C1 and M0-M4 on one fixed, seeded invented population: 69 people per row, 483 row-person records. It writes canonical JSON. The golden file tests/acola_track/mechanism_l_dd222b9.json (SHA-256 07c59d69...87de) was produced at dd222b9af926c773a0f92732e2d08be507793378 in a temporary git worktree (.equiv-dd222b9), with the script's SHA-256 5d7eef14...7a27. Imports were checked to resolve to the worktree's src. The builder's earlier git-archive export of the same commit gave the identical file; the provenance records both. The test recomputes at HEAD and asserts bit-identical equality. No REG18_MECHANISM_DRIFT. Built by a Subfleet build lane (GPT-6.1 Sol, job 20261024-073819-mgf-f0-acola-stage2) under the cleared builder brief and its 2026-10-24 addendum. That lane's sandbox could not write .git, so the lane session committed its working tree in steps. Co-Authored-By: Claude Opus 5.5 --- scripts/acola_mechanism_l_equivalence.py | 351 + tests/acola_track/mechanism_l_dd222b9.json | 6715 +++++++++++++++++ .../mechanism_l_dd222b9.provenance.json | 291 + .../test_mechanism_l_equivalence.py | 112 + 4 files changed, 7469 insertions(+) create mode 100644 scripts/acola_mechanism_l_equivalence.py create mode 100644 tests/acola_track/mechanism_l_dd222b9.json create mode 100644 tests/acola_track/mechanism_l_dd222b9.provenance.json create mode 100644 tests/acola_track/test_mechanism_l_equivalence.py diff --git a/scripts/acola_mechanism_l_equivalence.py b/scripts/acola_mechanism_l_equivalence.py new file mode 100644 index 000000000..5c816bded --- /dev/null +++ b/scripts/acola_mechanism_l_equivalence.py @@ -0,0 +1,351 @@ +#!/usr/bin/env python3 +"""Canonical mechanism-L benefits on seeded, entirely invented people. + +Run this same script against the registered source and the build source. +Only legacy interfaces already present at the registration commit are used; +the A-COLA package is deliberately unnecessary in the historical checkout. +The fixture is a fixed projection state, not a new demographic simulation. +""" + +from __future__ import annotations + +import argparse +import json +import random +import sys +from pathlib import Path + +SEED = 20261024 +REGISTERED_COMMIT = "dd222b9af926c773a0f92732e2d08be507793378" +ROW_IDS = ("C0", "C1", "M0", "M1", "M2", "M3", "M4") + + +def registered_rows(): + """Spell out Registration 18 using the common legacy row interface.""" + from populace_dynamics.cola_track_a.config import RegisteredRow + from populace_dynamics.scenario_benefits import ( + BenefitPeriod, + ExposureClock, + ) + + components = ( + "retired_worker", + "disabled_worker", + "spouse", + "aged_widow", + "disabled_widow", + ) + rows = [] + for row_id in ROW_IDS: + first = 2027 if row_id.startswith("C") else 2011 + if row_id == "M1": + first = 2010 + rows.append( + RegisteredRow( + row_id=row_id, + field_changed={ + "C1": "benefit_period", + "M1": "first_reduced_increase", + "M2": "benefit_period", + "M3": "exposure_clock", + "M4": "age_rule", + }.get(row_id), + first_reduced_determination_year=first, + exposure_clock=( + ExposureClock.ENTITLEMENT + if row_id == "M3" + else ExposureClock.ELIGIBILITY + ), + benefit_period=( + BenefitPeriod.DECEMBER + if row_id in ("C1", "M2") + else BenefitPeriod.CALENDAR_YEAR + ), + components=components, + headline_statistic=( + "ratio_of_all_alive_weighted_totals" + if row_id.startswith("C") + else "ratio_of_scenario_means" + ), + ) + ) + return tuple(rows) + + +def invented_parameters(): + """Invented monetary/rate inputs; the FRA shape spans the cap gap.""" + from populace_dynamics.ss.params import SSAParameters + + return SSAParameters( + nawi={y: 2800.0 * 1.04 ** (y - 1951) for y in range(1951, 2061)}, + wage_base={1937: 3000.0, 1975: 14100.0, 1990: 51300.0}, + pia_factors=(0.9, 0.32, 0.15), + fra_months_by_birth_year=[ + (1900, 780), + *((y, 780 + 2 * (y - 1937)) for y in range(1938, 1943)), + (1943, 792), + (1955, 794), + (1956, 796), + (1957, 798), + (1958, 800), + (1959, 802), + (1960, 804), + ], + early_monthly_rates=(5 / 900, 5 / 1200), + early_first_bracket_months=36, + delayed_credit_by_birth_year=[(1900, 0.08)], + max_delayed_months=48, + pe_us_revision="INVENTED mechanism-L equivalence fixture", + ) + + +def invented_population(): + """Fixed seeded careers and states, including all 1917–1942 cohorts.""" + import pandas as pd + + from populace_dynamics.cola_track_a.config import INVENTED_COHORT_LABEL + from populace_dynamics.cola_track_a.opening import ( + OpeningStockRecord, + TrackACohort, + ) + from populace_dynamics.engine.loop import ProjectionResult + + rng = random.Random(SEED) + persons, initial, final, departed = [], [], [], [] + careers, opening = {}, {} + + def add(birth, level, *, claim_age=70, **changes): + pid = len(persons) + 1 + weight = float(rng.randint(100, 500)) + person = { + "person_id": pid, + "family_unit_id": (pid + 1) // 2, + "weight": weight, + "sex": "female" if pid % 2 else "male", + "birth_year": birth, + "age_opening": 2010 - birth, + "opening_status": "none", + "ss_opening": 0, + "marital_status_opening": "married", + "ss_receipt_opening": False, + } + for column in ( + "opening_claim_year", + "opening_claim_year_upper_bound", + "opening_claim_age", + "spouse_person_id", + "late_spouse_person_id", + "late_spouse_death_year", + "widowhood_year", + "linked_spouse_birth_year", + ): + person[column] = pd.NA + persons.append(person) + careers[pid] = { + y: float(level + rng.randint(0, 1000)) + for y in range(max(1951, birth + 22), 2011) + } + state = { + "person_id": pid, + "year": 2030, + "age": 2030 - birth, + "sex": person["sex"], + "birth_year": birth, + "weight": weight, + "di_entitled": False, + "di_award_year": pd.NA, + "di_conversion_year": pd.NA, + "di_recovery_year": pd.NA, + "claimed": True, + "claim_age": claim_age, + "claim_year": birth + claim_age, + "marital_status": "married", + "spouse_person_id": pd.NA, + "widowhood_year": pd.NA, + "late_spouse_person_id": pd.NA, + "widowed_in_projection": False, + } + state.update(changes) + final.append(state) + initial.append( + { + **state, + "year": 2010, + "age": 2010 - birth, + "claimed": state["claim_year"] <= 2010, + } + ) + return pid + + # Low and high earners in every affected older cohort; each low + # earner has both an own worker benefit and a positive spouse excess. + for birth in (*range(1917, 1943), 1965, 1966, 1967, 1968): + claim_age = 70 if birth < 1943 else 62 + low = add(birth, 2000, claim_age=claim_age) + high = add(birth, 80000, claim_age=claim_age) + final[low - 1]["spouse_person_id"] = high + final[high - 1]["spouse_person_id"] = low + initial[low - 1]["spouse_person_id"] = high + initial[high - 1]["spouse_person_id"] = low + + # Unconverted and converted DI records, including a spouse on DI. + disabled = add( + 1975, + 30000, + di_entitled=True, + di_award_year=2008, + claimed=False, + ) + spouse = add(1967, 2000, claim_age=62, spouse_person_id=disabled) + final[disabled - 1]["spouse_person_id"] = spouse + add( + 1960, + 25000, + di_award_year=2000, + di_conversion_year=2027, + ) + + # A projected widow rests on a deceased worker's late-claim record. + deceased = add(1958, 70000, claim_age=68) + departed.append({**final.pop(), "year": 2027, "age": 69}) + add( + 1960, + 2000, + claim_age=64, + marital_status="widowed", + widowhood_year=2028, + late_spouse_person_id=deceased, + widowed_in_projection=True, + ) + + # Intact frozen opening stock covers all remaining component paths. + for birth, component in ( + (1935, "retired_worker"), + (1975, "disabled_worker"), + (1940, "spouse"), + (1942, "aged_widow"), + (1975, "disabled_widow"), + ): + pid = add(birth, 10000) + status = "survivor" if "widow" in component else component + persons[pid - 1]["opening_status"] = status + persons[pid - 1]["ss_receipt_opening"] = True + opening[pid] = OpeningStockRecord( + person_id=pid, + status=status, + component=component, + observed_annual_amount=float(rng.randint(500, 2000) * 12), + clock_year=2008, + clock_rule="INVENTED fixed opening clock", + entitlement_year=2009, + entitlement_clamped=False, + ) + if component == "disabled_worker": + final[-1]["di_entitled"] = True + final[-1]["di_award_year"] = 2008 + + static = pd.DataFrame(persons) + initial_frame = pd.DataFrame(initial) + return ( + TrackACohort( + persons=static, + careers=careers, + initial_slice=initial_frame, + opening=opening, + data_provenance="invented", + labels=(INVENTED_COHORT_LABEL,), + diagnostics={"seed": SEED}, + ), + ProjectionResult( + slices=( + initial_frame, + pd.DataFrame(departed), + pd.DataFrame(final), + ), + traces=(), + draw_index=0, + ), + ) + + +def compute_equivalence(): + """Return per-person scenario/component amounts for every runnable row.""" + from populace_dynamics.cola_track_a.benefits import ( + BenefitContext, + StateLookups, + reference_benefit_rows, + ) + from populace_dynamics.cola_track_a.config import TrackAConfig + from populace_dynamics.estimates.parameters import COLASeries + + cohort, result = invented_population() + context = BenefitContext( + cohort=cohort, + params=invented_parameters(), + baseline=COLASeries( + {y: 0.02 + 0.002 * (y % 5) for y in range(1979, 2031)}, + {"invented": True}, + ), + config=TrackAConfig(rows=("R0",), draw_indices=(0,)), + ) + lookups = StateLookups(result, 2030) + cache, output = {}, {} + for row in registered_rows(): + benefits, _ = reference_benefit_rows( + result, + draw=0, + row=row, + context=context, + pia_cache=cache, + lookups=lookups, + ) + output[row.row_id] = { + str(person["person_id"]): { + "birth_year": person["birth_year"], + "basis": person["basis"], + "base": person["benefit_base"], + "reform": person["benefit_reform"], + "components": person["benefit_components"], + } + for person in benefits + } + return { + "schema_version": "acola-mechanism-l-equivalence-v1", + "data": "INVENTED DATA ONLY", + "seed": SEED, + "draw": 0, + "reference_year": 2030, + "rows": output, + } + + +def canonical_json(value): + return json.dumps(value, sort_keys=True, indent=2, allow_nan=False) + "\n" + + +def differing_people(expected, actual): + """Name changed row/person pairs without hiding missing membership.""" + differences = [] + for row in sorted(set(expected["rows"]) | set(actual["rows"])): + before = expected["rows"].get(row, {}) + after = actual["rows"].get(row, {}) + for person in sorted(set(before) | set(after), key=int): + if before.get(person) != after.get(person): + differences.append(f"{row}/person-{person}") + return differences + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("repository_root", type=Path) + parser.add_argument("--output", required=True, type=Path) + args = parser.parse_args(argv) + source = args.repository_root.resolve() / "src" + if not (source / "populace_dynamics/cola_track_a/benefits.py").is_file(): + parser.error("repository root has no legacy mechanism-L source") + sys.path.insert(0, str(source)) + args.output.write_text(canonical_json(compute_equivalence())) + + +if __name__ == "__main__": + main() diff --git a/tests/acola_track/mechanism_l_dd222b9.json b/tests/acola_track/mechanism_l_dd222b9.json new file mode 100644 index 000000000..16f519f43 --- /dev/null +++ b/tests/acola_track/mechanism_l_dd222b9.json @@ -0,0 +1,6715 @@ +{ + "data": "INVENTED DATA ONLY", + "draw": 0, + "reference_year": 2030, + "rows": { + "C0": { + "1": { + "base": 17727.600000000002, + "basis": "projected", + "birth_year": 1917, + "components": { + "retired_worker": { + "base": 11239.2, + "reform": 10915.2 + }, + "spouse": { + "base": 6488.400000000001, + "reform": 6300.0 + } + }, + "reform": 17215.199999999997 + }, + "10": { + "base": 38676.0, + "basis": "projected", + "birth_year": 1921, + "components": { + "retired_worker": { + "base": 38676.0, + "reform": 37556.399999999994 + } + }, + "reform": 37556.399999999994 + }, + "11": { + "base": 17415.6, + "basis": "projected", + "birth_year": 1922, + "components": { + "retired_worker": { + "base": 11844.0, + "reform": 11500.8 + }, + "spouse": { + "base": 5571.6, + "reform": 5410.799999999999 + } + }, + "reform": 16911.6 + }, + "12": { + "base": 38398.8, + "basis": "projected", + "birth_year": 1922, + "components": { + "retired_worker": { + "base": 38398.8, + "reform": 37288.8 + } + }, + "reform": 37288.8 + }, + "13": { + "base": 17372.4, + "basis": "projected", + "birth_year": 1923, + "components": { + "retired_worker": { + "base": 12145.2, + "reform": 11793.599999999999 + }, + "spouse": { + "base": 5227.200000000001, + "reform": 5074.799999999999 + } + }, + "reform": 16868.399999999998 + }, + "14": { + "base": 38091.600000000006, + "basis": "projected", + "birth_year": 1923, + "components": { + "retired_worker": { + "base": 38091.600000000006, + "reform": 36987.600000000006 + } + }, + "reform": 36987.600000000006 + }, + "15": { + "base": 17438.399999999998, + "basis": "projected", + "birth_year": 1924, + "components": { + "retired_worker": { + "base": 12381.599999999999, + "reform": 12025.2 + }, + "spouse": { + "base": 5056.799999999999, + "reform": 4909.200000000001 + } + }, + "reform": 16934.4 + }, + "16": { + "base": 38115.600000000006, + "basis": "projected", + "birth_year": 1924, + "components": { + "retired_worker": { + "base": 38115.600000000006, + "reform": 37011.600000000006 + } + }, + "reform": 37011.600000000006 + }, + "17": { + "base": 17407.199999999997, + "basis": "projected", + "birth_year": 1925, + "components": { + "retired_worker": { + "base": 12348.0, + "reform": 11990.400000000001 + }, + "spouse": { + "base": 5059.200000000001, + "reform": 4912.799999999999 + } + }, + "reform": 16903.199999999997 + }, + "18": { + "base": 38055.600000000006, + "basis": "projected", + "birth_year": 1925, + "components": { + "retired_worker": { + "base": 38055.600000000006, + "reform": 36952.8 + } + }, + "reform": 36952.8 + }, + "19": { + "base": 17433.600000000002, + "basis": "projected", + "birth_year": 1926, + "components": { + "retired_worker": { + "base": 12610.800000000001, + "reform": 12247.2 + }, + "spouse": { + "base": 4822.799999999999, + "reform": 4682.4 + } + }, + "reform": 16929.6 + }, + "2": { + "base": 39610.8, + "basis": "projected", + "birth_year": 1917, + "components": { + "retired_worker": { + "base": 39610.8, + "reform": 38463.600000000006 + } + }, + "reform": 38463.600000000006 + }, + "20": { + "base": 37957.2, + "basis": "projected", + "birth_year": 1926, + "components": { + "retired_worker": { + "base": 37957.2, + "reform": 36858.0 + } + }, + "reform": 36858.0 + }, + "21": { + "base": 17437.199999999997, + "basis": "projected", + "birth_year": 1927, + "components": { + "retired_worker": { + "base": 12853.199999999999, + "reform": 12480.0 + }, + "spouse": { + "base": 4584.0, + "reform": 4452.0 + } + }, + "reform": 16932.0 + }, + "22": { + "base": 37810.8, + "basis": "projected", + "birth_year": 1927, + "components": { + "retired_worker": { + "base": 37810.8, + "reform": 36715.2 + } + }, + "reform": 36715.2 + }, + "23": { + "base": 17392.800000000003, + "basis": "projected", + "birth_year": 1928, + "components": { + "retired_worker": { + "base": 12974.400000000001, + "reform": 12598.800000000001 + }, + "spouse": { + "base": 4418.4, + "reform": 4290.0 + } + }, + "reform": 16888.800000000003 + }, + "24": { + "base": 37615.2, + "basis": "projected", + "birth_year": 1928, + "components": { + "retired_worker": { + "base": 37615.2, + "reform": 36524.399999999994 + } + }, + "reform": 36524.399999999994 + }, + "25": { + "base": 17530.800000000003, + "basis": "projected", + "birth_year": 1929, + "components": { + "retired_worker": { + "base": 13332.0, + "reform": 12945.599999999999 + }, + "spouse": { + "base": 4198.799999999999, + "reform": 4077.6000000000004 + } + }, + "reform": 17023.199999999997 + }, + "26": { + "base": 37749.600000000006, + "basis": "projected", + "birth_year": 1929, + "components": { + "retired_worker": { + "base": 37749.600000000006, + "reform": 36656.399999999994 + } + }, + "reform": 36656.399999999994 + }, + "27": { + "base": 17530.800000000003, + "basis": "projected", + "birth_year": 1930, + "components": { + "retired_worker": { + "base": 13218.0, + "reform": 12836.400000000001 + }, + "spouse": { + "base": 4312.799999999999, + "reform": 4188.0 + } + }, + "reform": 17024.4 + }, + "28": { + "base": 37824.0, + "basis": "projected", + "birth_year": 1930, + "components": { + "retired_worker": { + "base": 37824.0, + "reform": 36729.600000000006 + } + }, + "reform": 36729.600000000006 + }, + "29": { + "base": 17524.800000000003, + "basis": "projected", + "birth_year": 1931, + "components": { + "retired_worker": { + "base": 13144.800000000001, + "reform": 12763.199999999999 + }, + "spouse": { + "base": 4380.0, + "reform": 4254.0 + } + }, + "reform": 17017.199999999997 + }, + "3": { + "base": 17586.0, + "basis": "projected", + "birth_year": 1918, + "components": { + "retired_worker": { + "base": 11367.599999999999, + "reform": 11040.0 + }, + "spouse": { + "base": 6218.400000000001, + "reform": 6037.200000000001 + } + }, + "reform": 17077.199999999997 + }, + "30": { + "base": 37855.2, + "basis": "projected", + "birth_year": 1931, + "components": { + "retired_worker": { + "base": 37855.2, + "reform": 36759.600000000006 + } + }, + "reform": 36759.600000000006 + }, + "31": { + "base": 17535.6, + "basis": "projected", + "birth_year": 1932, + "components": { + "retired_worker": { + "base": 13207.199999999999, + "reform": 12825.599999999999 + }, + "spouse": { + "base": 4328.4, + "reform": 4203.6 + } + }, + "reform": 17029.199999999997 + }, + "32": { + "base": 37843.2, + "basis": "projected", + "birth_year": 1932, + "components": { + "retired_worker": { + "base": 37843.2, + "reform": 36748.8 + } + }, + "reform": 36748.8 + }, + "33": { + "base": 17419.199999999997, + "basis": "projected", + "birth_year": 1933, + "components": { + "retired_worker": { + "base": 12774.0, + "reform": 12405.599999999999 + }, + "spouse": { + "base": 4645.200000000001, + "reform": 4509.6 + } + }, + "reform": 16915.199999999997 + }, + "34": { + "base": 37813.2, + "basis": "projected", + "birth_year": 1933, + "components": { + "retired_worker": { + "base": 37813.2, + "reform": 36716.399999999994 + } + }, + "reform": 36716.399999999994 + }, + "35": { + "base": 17576.4, + "basis": "projected", + "birth_year": 1934, + "components": { + "retired_worker": { + "base": 12944.400000000001, + "reform": 12568.800000000001 + }, + "spouse": { + "base": 4632.0, + "reform": 4498.799999999999 + } + }, + "reform": 17067.600000000002 + }, + "36": { + "base": 38118.0, + "basis": "projected", + "birth_year": 1934, + "components": { + "retired_worker": { + "base": 38118.0, + "reform": 37014.0 + } + }, + "reform": 37014.0 + }, + "37": { + "base": 17744.399999999998, + "basis": "projected", + "birth_year": 1935, + "components": { + "retired_worker": { + "base": 13213.199999999999, + "reform": 12831.599999999999 + }, + "spouse": { + "base": 4531.200000000001, + "reform": 4399.200000000001 + } + }, + "reform": 17230.800000000003 + }, + "38": { + "base": 38389.2, + "basis": "projected", + "birth_year": 1935, + "components": { + "retired_worker": { + "base": 38389.2, + "reform": 37279.2 + } + }, + "reform": 37279.2 + }, + "39": { + "base": 17781.600000000002, + "basis": "projected", + "birth_year": 1936, + "components": { + "retired_worker": { + "base": 13030.800000000001, + "reform": 12654.0 + }, + "spouse": { + "base": 4750.799999999999, + "reform": 4614.0 + } + }, + "reform": 17268.0 + }, + "4": { + "base": 39152.399999999994, + "basis": "projected", + "birth_year": 1918, + "components": { + "retired_worker": { + "base": 39152.399999999994, + "reform": 38018.399999999994 + } + }, + "reform": 38018.399999999994 + }, + "40": { + "base": 38606.399999999994, + "basis": "projected", + "birth_year": 1936, + "components": { + "retired_worker": { + "base": 38606.399999999994, + "reform": 37489.2 + } + }, + "reform": 37489.2 + }, + "41": { + "base": 17851.199999999997, + "basis": "projected", + "birth_year": 1937, + "components": { + "retired_worker": { + "base": 13032.0, + "reform": 12655.199999999999 + }, + "spouse": { + "base": 4819.200000000001, + "reform": 4678.799999999999 + } + }, + "reform": 17334.0 + }, + "42": { + "base": 38790.0, + "basis": "projected", + "birth_year": 1937, + "components": { + "retired_worker": { + "base": 38790.0, + "reform": 37665.600000000006 + } + }, + "reform": 37665.600000000006 + }, + "43": { + "base": 17845.199999999997, + "basis": "projected", + "birth_year": 1938, + "components": { + "retired_worker": { + "base": 12769.199999999999, + "reform": 12400.800000000001 + }, + "spouse": { + "base": 5076.0, + "reform": 4928.4 + } + }, + "reform": 17329.2 + }, + "44": { + "base": 38942.399999999994, + "basis": "projected", + "birth_year": 1938, + "components": { + "retired_worker": { + "base": 38942.399999999994, + "reform": 37814.399999999994 + } + }, + "reform": 37814.399999999994 + }, + "45": { + "base": 18085.2, + "basis": "projected", + "birth_year": 1939, + "components": { + "retired_worker": { + "base": 12976.800000000001, + "reform": 12601.199999999999 + }, + "spouse": { + "base": 5108.4, + "reform": 4960.799999999999 + } + }, + "reform": 17562.0 + }, + "46": { + "base": 39442.8, + "basis": "projected", + "birth_year": 1939, + "components": { + "retired_worker": { + "base": 39442.8, + "reform": 38300.399999999994 + } + }, + "reform": 38300.399999999994 + }, + "47": { + "base": 18242.4, + "basis": "projected", + "birth_year": 1940, + "components": { + "retired_worker": { + "base": 12932.400000000001, + "reform": 12556.800000000001 + }, + "spouse": { + "base": 5310.0, + "reform": 5157.6 + } + }, + "reform": 17714.4 + }, + "48": { + "base": 39885.600000000006, + "basis": "projected", + "birth_year": 1940, + "components": { + "retired_worker": { + "base": 39885.600000000006, + "reform": 38732.399999999994 + } + }, + "reform": 38732.399999999994 + }, + "49": { + "base": 18422.4, + "basis": "projected", + "birth_year": 1941, + "components": { + "retired_worker": { + "base": 13060.800000000001, + "reform": 12680.400000000001 + }, + "spouse": { + "base": 5361.6, + "reform": 5206.799999999999 + } + }, + "reform": 17887.199999999997 + }, + "5": { + "base": 17588.4, + "basis": "projected", + "birth_year": 1919, + "components": { + "retired_worker": { + "base": 11530.8, + "reform": 11198.400000000001 + }, + "spouse": { + "base": 6057.6, + "reform": 5882.4 + } + }, + "reform": 17080.800000000003 + }, + "50": { + "base": 40276.8, + "basis": "projected", + "birth_year": 1941, + "components": { + "retired_worker": { + "base": 40276.8, + "reform": 39110.399999999994 + } + }, + "reform": 39110.399999999994 + }, + "51": { + "base": 18520.800000000003, + "basis": "projected", + "birth_year": 1942, + "components": { + "retired_worker": { + "base": 12902.400000000001, + "reform": 12529.199999999999 + }, + "spouse": { + "base": 5618.4, + "reform": 5455.200000000001 + } + }, + "reform": 17984.399999999998 + }, + "52": { + "base": 40638.0, + "basis": "projected", + "birth_year": 1942, + "components": { + "retired_worker": { + "base": 40638.0, + "reform": 39462.0 + } + }, + "reform": 39462.0 + }, + "53": { + "base": 12949.199999999999, + "basis": "projected", + "birth_year": 1965, + "components": { + "retired_worker": { + "base": 3379.2000000000003, + "reform": 3280.7999999999997 + }, + "spouse": { + "base": 9570.0, + "reform": 9292.8 + } + }, + "reform": 12573.599999999999 + }, + "54": { + "base": 27372.0, + "basis": "projected", + "birth_year": 1965, + "components": { + "retired_worker": { + "base": 27372.0, + "reform": 26580.0 + } + }, + "reform": 26580.0 + }, + "55": { + "base": 13039.199999999999, + "basis": "projected", + "birth_year": 1966, + "components": { + "retired_worker": { + "base": 3219.6000000000004, + "reform": 3158.3999999999996 + }, + "spouse": { + "base": 9819.599999999999, + "reform": 9628.8 + } + }, + "reform": 12787.199999999999 + }, + "56": { + "base": 27591.600000000002, + "basis": "projected", + "birth_year": 1966, + "components": { + "retired_worker": { + "base": 27591.600000000002, + "reform": 27057.600000000002 + } + }, + "reform": 27057.600000000002 + }, + "57": { + "base": 13112.400000000001, + "basis": "projected", + "birth_year": 1967, + "components": { + "retired_worker": { + "base": 3092.3999999999996, + "reform": 3062.3999999999996 + }, + "spouse": { + "base": 10020.0, + "reform": 9922.8 + } + }, + "reform": 12985.199999999999 + }, + "58": { + "base": 27769.199999999997, + "basis": "projected", + "birth_year": 1967, + "components": { + "retired_worker": { + "base": 27769.199999999997, + "reform": 27498.0 + } + }, + "reform": 27498.0 + }, + "59": { + "base": 13095.599999999999, + "basis": "projected", + "birth_year": 1968, + "components": { + "retired_worker": { + "base": 3024.0, + "reform": 3024.0 + }, + "spouse": { + "base": 10071.599999999999, + "reform": 10071.599999999999 + } + }, + "reform": 13095.599999999999 + }, + "6": { + "base": 39056.399999999994, + "basis": "projected", + "birth_year": 1919, + "components": { + "retired_worker": { + "base": 39056.399999999994, + "reform": 37927.2 + } + }, + "reform": 37927.2 + }, + "60": { + "base": 27741.600000000002, + "basis": "projected", + "birth_year": 1968, + "components": { + "retired_worker": { + "base": 27741.600000000002, + "reform": 27741.600000000002 + } + }, + "reform": 27741.600000000002 + }, + "61": { + "base": 13213.199999999999, + "basis": "projected", + "birth_year": 1975, + "components": { + "disabled_worker": { + "base": 13213.199999999999, + "reform": 12831.599999999999 + } + }, + "reform": 12831.599999999999 + }, + "62": { + "base": 4516.799999999999, + "basis": "projected", + "birth_year": 1967, + "components": { + "retired_worker": { + "base": 3116.3999999999996, + "reform": 3085.2000000000003 + }, + "spouse": { + "base": 1400.4, + "reform": 1304.4 + } + }, + "reform": 4389.6 + }, + "63": { + "base": 15170.400000000001, + "basis": "projected", + "birth_year": 1960, + "components": { + "retired_worker": { + "base": 15170.400000000001, + "reform": 14731.199999999999 + } + }, + "reform": 14731.199999999999 + }, + "65": { + "base": 40938.0, + "basis": "projected", + "birth_year": 1960, + "components": { + "aged_widow": { + "base": 36267.600000000006, + "reform": 35217.600000000006 + }, + "retired_worker": { + "base": 4670.4, + "reform": 4536.0 + } + }, + "reform": 39753.600000000006 + }, + "66": { + "base": 13574.37690890607, + "basis": "opening_stock", + "birth_year": 1935, + "components": { + "retired_worker": { + "base": 13574.37690890607, + "reform": 13181.320278393594 + } + }, + "reform": 13181.320278393594 + }, + "67": { + "base": 19011.84038662128, + "basis": "opening_stock", + "birth_year": 1975, + "components": { + "disabled_worker": { + "base": 19011.84038662128, + "reform": 18461.337776272838 + } + }, + "reform": 18461.337776272838 + }, + "68": { + "base": 21634.16319856905, + "basis": "opening_stock", + "birth_year": 1940, + "components": { + "spouse": { + "base": 21634.16319856905, + "reform": 21007.729193689782 + } + }, + "reform": 21007.729193689782 + }, + "69": { + "base": 17565.70648297362, + "basis": "opening_stock", + "birth_year": 1942, + "components": { + "aged_widow": { + "base": 17565.70648297362, + "reform": 17057.07780343261 + } + }, + "reform": 17057.07780343261 + }, + "7": { + "base": 17528.4, + "basis": "projected", + "birth_year": 1920, + "components": { + "retired_worker": { + "base": 11574.0, + "reform": 11236.8 + }, + "spouse": { + "base": 5954.4, + "reform": 5784.0 + } + }, + "reform": 17020.800000000003 + }, + "70": { + "base": 15560.40080324886, + "basis": "opening_stock", + "birth_year": 1975, + "components": { + "disabled_widow": { + "base": 15560.40080324886, + "reform": 15109.837307760834 + } + }, + "reform": 15109.837307760834 + }, + "8": { + "base": 38869.2, + "basis": "projected", + "birth_year": 1920, + "components": { + "retired_worker": { + "base": 38869.2, + "reform": 37744.8 + } + }, + "reform": 37744.8 + }, + "9": { + "base": 17515.199999999997, + "basis": "projected", + "birth_year": 1921, + "components": { + "retired_worker": { + "base": 11822.400000000001, + "reform": 11480.400000000001 + }, + "spouse": { + "base": 5692.799999999999, + "reform": 5528.4 + } + }, + "reform": 17008.800000000003 + } + }, + "C1": { + "1": { + "base": 18081.600000000002, + "basis": "projected", + "birth_year": 1917, + "components": { + "retired_worker": { + "base": 11462.400000000001, + "reform": 11022.0 + }, + "spouse": { + "base": 6619.200000000001, + "reform": 6363.599999999999 + } + }, + "reform": 17385.6 + }, + "10": { + "base": 39448.8, + "basis": "projected", + "birth_year": 1921, + "components": { + "retired_worker": { + "base": 39448.8, + "reform": 37932.0 + } + }, + "reform": 37932.0 + }, + "11": { + "base": 17763.600000000002, + "basis": "projected", + "birth_year": 1922, + "components": { + "retired_worker": { + "base": 12080.400000000001, + "reform": 11614.8 + }, + "spouse": { + "base": 5683.200000000001, + "reform": 5466.0 + } + }, + "reform": 17080.800000000003 + }, + "12": { + "base": 39165.600000000006, + "basis": "projected", + "birth_year": 1922, + "components": { + "retired_worker": { + "base": 39165.600000000006, + "reform": 37660.8 + } + }, + "reform": 37660.8 + }, + "13": { + "base": 17719.199999999997, + "basis": "projected", + "birth_year": 1923, + "components": { + "retired_worker": { + "base": 12387.599999999999, + "reform": 11911.2 + }, + "spouse": { + "base": 5331.6, + "reform": 5126.4 + } + }, + "reform": 17037.6 + }, + "14": { + "base": 38851.2, + "basis": "projected", + "birth_year": 1923, + "components": { + "retired_worker": { + "base": 38851.2, + "reform": 37356.0 + } + }, + "reform": 37356.0 + }, + "15": { + "base": 17787.600000000002, + "basis": "projected", + "birth_year": 1924, + "components": { + "retired_worker": { + "base": 12628.800000000001, + "reform": 12144.0 + }, + "spouse": { + "base": 5158.799999999999, + "reform": 4958.4 + } + }, + "reform": 17102.4 + }, + "16": { + "base": 38877.600000000006, + "basis": "projected", + "birth_year": 1924, + "components": { + "retired_worker": { + "base": 38877.600000000006, + "reform": 37380.0 + } + }, + "reform": 37380.0 + }, + "17": { + "base": 17755.199999999997, + "basis": "projected", + "birth_year": 1925, + "components": { + "retired_worker": { + "base": 12594.0, + "reform": 12109.2 + }, + "spouse": { + "base": 5161.200000000001, + "reform": 4963.200000000001 + } + }, + "reform": 17072.4 + }, + "18": { + "base": 38815.2, + "basis": "projected", + "birth_year": 1925, + "components": { + "retired_worker": { + "base": 38815.2, + "reform": 37321.2 + } + }, + "reform": 37321.2 + }, + "19": { + "base": 17782.800000000003, + "basis": "projected", + "birth_year": 1926, + "components": { + "retired_worker": { + "base": 12862.800000000001, + "reform": 12368.400000000001 + }, + "spouse": { + "base": 4920.0, + "reform": 4729.200000000001 + } + }, + "reform": 17097.600000000002 + }, + "2": { + "base": 40402.8, + "basis": "projected", + "birth_year": 1917, + "components": { + "retired_worker": { + "base": 40402.8, + "reform": 38847.600000000006 + } + }, + "reform": 38847.600000000006 + }, + "20": { + "base": 38715.600000000006, + "basis": "projected", + "birth_year": 1926, + "components": { + "retired_worker": { + "base": 38715.600000000006, + "reform": 37225.2 + } + }, + "reform": 37225.2 + }, + "21": { + "base": 17786.4, + "basis": "projected", + "birth_year": 1927, + "components": { + "retired_worker": { + "base": 13110.0, + "reform": 12603.599999999999 + }, + "spouse": { + "base": 4676.4, + "reform": 4497.6 + } + }, + "reform": 17101.199999999997 + }, + "22": { + "base": 38566.8, + "basis": "projected", + "birth_year": 1927, + "components": { + "retired_worker": { + "base": 38566.8, + "reform": 37081.2 + } + }, + "reform": 37081.2 + }, + "23": { + "base": 17739.600000000002, + "basis": "projected", + "birth_year": 1928, + "components": { + "retired_worker": { + "base": 13232.400000000001, + "reform": 12723.599999999999 + }, + "spouse": { + "base": 4507.200000000001, + "reform": 4333.200000000001 + } + }, + "reform": 17056.800000000003 + }, + "24": { + "base": 38365.2, + "basis": "projected", + "birth_year": 1928, + "components": { + "retired_worker": { + "base": 38365.2, + "reform": 36889.2 + } + }, + "reform": 36889.2 + }, + "25": { + "base": 17881.199999999997, + "basis": "projected", + "birth_year": 1929, + "components": { + "retired_worker": { + "base": 13598.400000000001, + "reform": 13074.0 + }, + "spouse": { + "base": 4282.799999999999, + "reform": 4118.4 + } + }, + "reform": 17192.4 + }, + "26": { + "base": 38503.2, + "basis": "projected", + "birth_year": 1929, + "components": { + "retired_worker": { + "base": 38503.2, + "reform": 37022.399999999994 + } + }, + "reform": 37022.399999999994 + }, + "27": { + "base": 17881.2, + "basis": "projected", + "birth_year": 1930, + "components": { + "retired_worker": { + "base": 13480.800000000001, + "reform": 12964.800000000001 + }, + "spouse": { + "base": 4400.4, + "reform": 4228.799999999999 + } + }, + "reform": 17193.600000000002 + }, + "28": { + "base": 38578.8, + "basis": "projected", + "birth_year": 1930, + "components": { + "retired_worker": { + "base": 38578.8, + "reform": 37095.600000000006 + } + }, + "reform": 37095.600000000006 + }, + "29": { + "base": 17875.199999999997, + "basis": "projected", + "birth_year": 1931, + "components": { + "retired_worker": { + "base": 13406.400000000001, + "reform": 12890.400000000001 + }, + "spouse": { + "base": 4468.799999999999, + "reform": 4297.200000000001 + } + }, + "reform": 17187.600000000002 + }, + "3": { + "base": 17937.600000000002, + "basis": "projected", + "birth_year": 1918, + "components": { + "retired_worker": { + "base": 11594.400000000001, + "reform": 11149.2 + }, + "spouse": { + "base": 6343.200000000001, + "reform": 6098.4 + } + }, + "reform": 17247.6 + }, + "30": { + "base": 38611.2, + "basis": "projected", + "birth_year": 1931, + "components": { + "retired_worker": { + "base": 38611.2, + "reform": 37126.8 + } + }, + "reform": 37126.8 + }, + "31": { + "base": 17886.0, + "basis": "projected", + "birth_year": 1932, + "components": { + "retired_worker": { + "base": 13470.0, + "reform": 12951.599999999999 + }, + "spouse": { + "base": 4416.0, + "reform": 4246.799999999999 + } + }, + "reform": 17198.399999999998 + }, + "32": { + "base": 38598.0, + "basis": "projected", + "birth_year": 1932, + "components": { + "retired_worker": { + "base": 38598.0, + "reform": 37116.0 + } + }, + "reform": 37116.0 + }, + "33": { + "base": 17767.199999999997, + "basis": "projected", + "birth_year": 1933, + "components": { + "retired_worker": { + "base": 13029.599999999999, + "reform": 12529.199999999999 + }, + "spouse": { + "base": 4737.6, + "reform": 4554.0 + } + }, + "reform": 17083.199999999997 + }, + "34": { + "base": 38568.0, + "basis": "projected", + "birth_year": 1933, + "components": { + "retired_worker": { + "base": 38568.0, + "reform": 37082.399999999994 + } + }, + "reform": 37082.399999999994 + }, + "35": { + "base": 17928.0, + "basis": "projected", + "birth_year": 1934, + "components": { + "retired_worker": { + "base": 13202.400000000001, + "reform": 12693.599999999999 + }, + "spouse": { + "base": 4725.6, + "reform": 4543.200000000001 + } + }, + "reform": 17236.800000000003 + }, + "36": { + "base": 38880.0, + "basis": "projected", + "birth_year": 1934, + "components": { + "retired_worker": { + "base": 38880.0, + "reform": 37383.600000000006 + } + }, + "reform": 37383.600000000006 + }, + "37": { + "base": 18098.4, + "basis": "projected", + "birth_year": 1935, + "components": { + "retired_worker": { + "base": 13476.0, + "reform": 12960.0 + }, + "spouse": { + "base": 4622.4, + "reform": 4443.6 + } + }, + "reform": 17403.6 + }, + "38": { + "base": 39156.0, + "basis": "projected", + "birth_year": 1935, + "components": { + "retired_worker": { + "base": 39156.0, + "reform": 37651.2 + } + }, + "reform": 37651.2 + }, + "39": { + "base": 18138.0, + "basis": "projected", + "birth_year": 1936, + "components": { + "retired_worker": { + "base": 13291.199999999999, + "reform": 12778.800000000001 + }, + "spouse": { + "base": 4846.799999999999, + "reform": 4660.799999999999 + } + }, + "reform": 17439.600000000002 + }, + "4": { + "base": 39934.8, + "basis": "projected", + "birth_year": 1918, + "components": { + "retired_worker": { + "base": 39934.8, + "reform": 38398.8 + } + }, + "reform": 38398.8 + }, + "40": { + "base": 39378.0, + "basis": "projected", + "birth_year": 1936, + "components": { + "retired_worker": { + "base": 39378.0, + "reform": 37863.600000000006 + } + }, + "reform": 37863.600000000006 + }, + "41": { + "base": 18208.800000000003, + "basis": "projected", + "birth_year": 1937, + "components": { + "retired_worker": { + "base": 13292.400000000001, + "reform": 12781.199999999999 + }, + "spouse": { + "base": 4916.4, + "reform": 4726.799999999999 + } + }, + "reform": 17508.0 + }, + "42": { + "base": 39564.0, + "basis": "projected", + "birth_year": 1937, + "components": { + "retired_worker": { + "base": 39564.0, + "reform": 38041.2 + } + }, + "reform": 38041.2 + }, + "43": { + "base": 18202.800000000003, + "basis": "projected", + "birth_year": 1938, + "components": { + "retired_worker": { + "base": 13024.800000000001, + "reform": 12524.400000000001 + }, + "spouse": { + "base": 5178.0, + "reform": 4977.6 + } + }, + "reform": 17502.0 + }, + "44": { + "base": 39720.0, + "basis": "projected", + "birth_year": 1938, + "components": { + "retired_worker": { + "base": 39720.0, + "reform": 38191.2 + } + }, + "reform": 38191.2 + }, + "45": { + "base": 18446.4, + "basis": "projected", + "birth_year": 1939, + "components": { + "retired_worker": { + "base": 13234.800000000001, + "reform": 12727.199999999999 + }, + "spouse": { + "base": 5211.6, + "reform": 5010.0 + } + }, + "reform": 17737.199999999997 + }, + "46": { + "base": 40231.2, + "basis": "projected", + "birth_year": 1939, + "components": { + "retired_worker": { + "base": 40231.2, + "reform": 38682.0 + } + }, + "reform": 38682.0 + }, + "47": { + "base": 18607.199999999997, + "basis": "projected", + "birth_year": 1940, + "components": { + "retired_worker": { + "base": 13190.400000000001, + "reform": 12682.800000000001 + }, + "spouse": { + "base": 5416.799999999999, + "reform": 5209.200000000001 + } + }, + "reform": 17892.0 + }, + "48": { + "base": 40682.399999999994, + "basis": "projected", + "birth_year": 1940, + "components": { + "retired_worker": { + "base": 40682.399999999994, + "reform": 39120.0 + } + }, + "reform": 39120.0 + }, + "49": { + "base": 18790.8, + "basis": "projected", + "birth_year": 1941, + "components": { + "retired_worker": { + "base": 13321.199999999999, + "reform": 12807.599999999999 + }, + "spouse": { + "base": 5469.6, + "reform": 5258.4 + } + }, + "reform": 18066.0 + }, + "5": { + "base": 17941.199999999997, + "basis": "projected", + "birth_year": 1919, + "components": { + "retired_worker": { + "base": 11761.2, + "reform": 11308.8 + }, + "spouse": { + "base": 6180.0, + "reform": 5941.200000000001 + } + }, + "reform": 17250.0 + }, + "50": { + "base": 41082.0, + "basis": "projected", + "birth_year": 1941, + "components": { + "retired_worker": { + "base": 41082.0, + "reform": 39499.2 + } + }, + "reform": 39499.2 + }, + "51": { + "base": 18890.399999999998, + "basis": "projected", + "birth_year": 1942, + "components": { + "retired_worker": { + "base": 13159.199999999999, + "reform": 12654.0 + }, + "spouse": { + "base": 5731.200000000001, + "reform": 5510.4 + } + }, + "reform": 18164.4 + }, + "52": { + "base": 41451.600000000006, + "basis": "projected", + "birth_year": 1942, + "components": { + "retired_worker": { + "base": 41451.600000000006, + "reform": 39855.600000000006 + } + }, + "reform": 39855.600000000006 + }, + "53": { + "base": 13208.400000000001, + "basis": "projected", + "birth_year": 1965, + "components": { + "retired_worker": { + "base": 3446.3999999999996, + "reform": 3314.3999999999996 + }, + "spouse": { + "base": 9762.0, + "reform": 9385.2 + } + }, + "reform": 12699.599999999999 + }, + "54": { + "base": 27919.199999999997, + "basis": "projected", + "birth_year": 1965, + "components": { + "retired_worker": { + "base": 27919.199999999997, + "reform": 26845.199999999997 + } + }, + "reform": 26845.199999999997 + }, + "55": { + "base": 13300.800000000001, + "basis": "projected", + "birth_year": 1966, + "components": { + "retired_worker": { + "base": 3284.3999999999996, + "reform": 3188.3999999999996 + }, + "spouse": { + "base": 10016.400000000001, + "reform": 9726.0 + } + }, + "reform": 12914.400000000001 + }, + "56": { + "base": 28142.399999999998, + "basis": "projected", + "birth_year": 1966, + "components": { + "retired_worker": { + "base": 28142.399999999998, + "reform": 27327.600000000002 + } + }, + "reform": 27327.600000000002 + }, + "57": { + "base": 13375.199999999999, + "basis": "projected", + "birth_year": 1967, + "components": { + "retired_worker": { + "base": 3153.6000000000004, + "reform": 3092.3999999999996 + }, + "spouse": { + "base": 10221.599999999999, + "reform": 10022.400000000001 + } + }, + "reform": 13114.800000000001 + }, + "58": { + "base": 28324.800000000003, + "basis": "projected", + "birth_year": 1967, + "components": { + "retired_worker": { + "base": 28324.800000000003, + "reform": 27772.800000000003 + } + }, + "reform": 27772.800000000003 + }, + "59": { + "base": 13357.199999999999, + "basis": "projected", + "birth_year": 1968, + "components": { + "retired_worker": { + "base": 3084.0, + "reform": 3054.0 + }, + "spouse": { + "base": 10273.2, + "reform": 10172.400000000001 + } + }, + "reform": 13226.400000000001 + }, + "6": { + "base": 39837.600000000006, + "basis": "projected", + "birth_year": 1919, + "components": { + "retired_worker": { + "base": 39837.600000000006, + "reform": 38305.2 + } + }, + "reform": 38305.2 + }, + "60": { + "base": 28296.0, + "basis": "projected", + "birth_year": 1968, + "components": { + "retired_worker": { + "base": 28296.0, + "reform": 28018.800000000003 + } + }, + "reform": 28018.800000000003 + }, + "61": { + "base": 13477.199999999999, + "basis": "projected", + "birth_year": 1975, + "components": { + "disabled_worker": { + "base": 13477.199999999999, + "reform": 12958.800000000001 + } + }, + "reform": 12958.800000000001 + }, + "62": { + "base": 4605.6, + "basis": "projected", + "birth_year": 1967, + "components": { + "retired_worker": { + "base": 3177.6000000000004, + "reform": 3115.2000000000003 + }, + "spouse": { + "base": 1428.0, + "reform": 1317.6 + } + }, + "reform": 4432.8 + }, + "63": { + "base": 15472.800000000001, + "basis": "projected", + "birth_year": 1960, + "components": { + "retired_worker": { + "base": 15472.800000000001, + "reform": 14877.599999999999 + } + }, + "reform": 14877.599999999999 + }, + "65": { + "base": 41755.2, + "basis": "projected", + "birth_year": 1960, + "components": { + "aged_widow": { + "base": 36991.2, + "reform": 35568.0 + }, + "retired_worker": { + "base": 4764.0, + "reform": 4581.6 + } + }, + "reform": 40149.600000000006 + }, + "66": { + "base": 13845.86444708419, + "basis": "opening_stock", + "birth_year": 1935, + "components": { + "retired_worker": { + "base": 13845.86444708419, + "reform": 13313.13348117753 + } + }, + "reform": 13313.13348117753 + }, + "67": { + "base": 19392.077194353707, + "basis": "opening_stock", + "birth_year": 1975, + "components": { + "disabled_worker": { + "base": 19392.077194353707, + "reform": 18645.951154035567 + } + }, + "reform": 18645.951154035567 + }, + "68": { + "base": 22066.84646254043, + "basis": "opening_stock", + "birth_year": 1940, + "components": { + "spouse": { + "base": 22066.84646254043, + "reform": 21217.80648562668 + } + }, + "reform": 21217.80648562668 + }, + "69": { + "base": 17917.020612633092, + "basis": "opening_stock", + "birth_year": 1942, + "components": { + "aged_widow": { + "base": 17917.020612633092, + "reform": 17227.648581466936 + } + }, + "reform": 17227.648581466936 + }, + "7": { + "base": 17877.600000000002, + "basis": "projected", + "birth_year": 1920, + "components": { + "retired_worker": { + "base": 11804.400000000001, + "reform": 11347.2 + }, + "spouse": { + "base": 6073.200000000001, + "reform": 5842.799999999999 + } + }, + "reform": 17190.0 + }, + "70": { + "base": 15871.608819313838, + "basis": "opening_stock", + "birth_year": 1975, + "components": { + "disabled_widow": { + "base": 15871.608819313838, + "reform": 15260.935680838444 + } + }, + "reform": 15260.935680838444 + }, + "8": { + "base": 39645.600000000006, + "basis": "projected", + "birth_year": 1920, + "components": { + "retired_worker": { + "base": 39645.600000000006, + "reform": 38121.600000000006 + } + }, + "reform": 38121.600000000006 + }, + "9": { + "base": 17865.6, + "basis": "projected", + "birth_year": 1921, + "components": { + "retired_worker": { + "base": 12058.8, + "reform": 11594.400000000001 + }, + "spouse": { + "base": 5806.799999999999, + "reform": 5583.6 + } + }, + "reform": 17178.0 + } + }, + "M0": { + "1": { + "base": 17727.600000000002, + "basis": "projected", + "birth_year": 1917, + "components": { + "retired_worker": { + "base": 11239.2, + "reform": 9328.8 + }, + "spouse": { + "base": 6488.400000000001, + "reform": 5382.0 + } + }, + "reform": 14710.800000000001 + }, + "10": { + "base": 38676.0, + "basis": "projected", + "birth_year": 1921, + "components": { + "retired_worker": { + "base": 38676.0, + "reform": 32096.399999999998 + } + }, + "reform": 32096.399999999998 + }, + "11": { + "base": 17415.6, + "basis": "projected", + "birth_year": 1922, + "components": { + "retired_worker": { + "base": 11844.0, + "reform": 9834.0 + }, + "spouse": { + "base": 5571.6, + "reform": 4620.0 + } + }, + "reform": 14454.0 + }, + "12": { + "base": 38398.8, + "basis": "projected", + "birth_year": 1922, + "components": { + "retired_worker": { + "base": 38398.8, + "reform": 31866.0 + } + }, + "reform": 31866.0 + }, + "13": { + "base": 17372.4, + "basis": "projected", + "birth_year": 1923, + "components": { + "retired_worker": { + "base": 12145.2, + "reform": 10084.8 + }, + "spouse": { + "base": 5227.200000000001, + "reform": 4333.200000000001 + } + }, + "reform": 14418.0 + }, + "14": { + "base": 38091.600000000006, + "basis": "projected", + "birth_year": 1923, + "components": { + "retired_worker": { + "base": 38091.600000000006, + "reform": 31611.600000000002 + } + }, + "reform": 31611.600000000002 + }, + "15": { + "base": 17438.399999999998, + "basis": "projected", + "birth_year": 1924, + "components": { + "retired_worker": { + "base": 12381.599999999999, + "reform": 10272.0 + }, + "spouse": { + "base": 5056.799999999999, + "reform": 4198.799999999999 + } + }, + "reform": 14470.800000000001 + }, + "16": { + "base": 38115.600000000006, + "basis": "projected", + "birth_year": 1924, + "components": { + "retired_worker": { + "base": 38115.600000000006, + "reform": 31628.399999999998 + } + }, + "reform": 31628.399999999998 + }, + "17": { + "base": 17407.199999999997, + "basis": "projected", + "birth_year": 1925, + "components": { + "retired_worker": { + "base": 12348.0, + "reform": 10251.599999999999 + }, + "spouse": { + "base": 5059.200000000001, + "reform": 4196.4 + } + }, + "reform": 14448.0 + }, + "18": { + "base": 38055.600000000006, + "basis": "projected", + "birth_year": 1925, + "components": { + "retired_worker": { + "base": 38055.600000000006, + "reform": 31581.600000000002 + } + }, + "reform": 31581.600000000002 + }, + "19": { + "base": 17433.600000000002, + "basis": "projected", + "birth_year": 1926, + "components": { + "retired_worker": { + "base": 12610.800000000001, + "reform": 10465.2 + }, + "spouse": { + "base": 4822.799999999999, + "reform": 4003.2000000000003 + } + }, + "reform": 14468.400000000001 + }, + "2": { + "base": 39610.8, + "basis": "projected", + "birth_year": 1917, + "components": { + "retired_worker": { + "base": 39610.8, + "reform": 32869.2 + } + }, + "reform": 32869.2 + }, + "20": { + "base": 37957.2, + "basis": "projected", + "birth_year": 1926, + "components": { + "retired_worker": { + "base": 37957.2, + "reform": 31498.800000000003 + } + }, + "reform": 31498.800000000003 + }, + "21": { + "base": 17437.199999999997, + "basis": "projected", + "birth_year": 1927, + "components": { + "retired_worker": { + "base": 12853.199999999999, + "reform": 10664.400000000001 + }, + "spouse": { + "base": 4584.0, + "reform": 3804.0 + } + }, + "reform": 14468.400000000001 + }, + "22": { + "base": 37810.8, + "basis": "projected", + "birth_year": 1927, + "components": { + "retired_worker": { + "base": 37810.8, + "reform": 31371.600000000002 + } + }, + "reform": 31371.600000000002 + }, + "23": { + "base": 17392.800000000003, + "basis": "projected", + "birth_year": 1928, + "components": { + "retired_worker": { + "base": 12974.400000000001, + "reform": 10766.400000000001 + }, + "spouse": { + "base": 4418.4, + "reform": 3669.6000000000004 + } + }, + "reform": 14436.0 + }, + "24": { + "base": 37615.2, + "basis": "projected", + "birth_year": 1928, + "components": { + "retired_worker": { + "base": 37615.2, + "reform": 31221.600000000002 + } + }, + "reform": 31221.600000000002 + }, + "25": { + "base": 17530.800000000003, + "basis": "projected", + "birth_year": 1929, + "components": { + "retired_worker": { + "base": 13332.0, + "reform": 11064.0 + }, + "spouse": { + "base": 4198.799999999999, + "reform": 3484.7999999999997 + } + }, + "reform": 14548.800000000001 + }, + "26": { + "base": 37749.600000000006, + "basis": "projected", + "birth_year": 1929, + "components": { + "retired_worker": { + "base": 37749.600000000006, + "reform": 31327.199999999997 + } + }, + "reform": 31327.199999999997 + }, + "27": { + "base": 17530.800000000003, + "basis": "projected", + "birth_year": 1930, + "components": { + "retired_worker": { + "base": 13218.0, + "reform": 10972.8 + }, + "spouse": { + "base": 4312.799999999999, + "reform": 3578.3999999999996 + } + }, + "reform": 14551.199999999999 + }, + "28": { + "base": 37824.0, + "basis": "projected", + "birth_year": 1930, + "components": { + "retired_worker": { + "base": 37824.0, + "reform": 31394.399999999998 + } + }, + "reform": 31394.399999999998 + }, + "29": { + "base": 17524.800000000003, + "basis": "projected", + "birth_year": 1931, + "components": { + "retired_worker": { + "base": 13144.800000000001, + "reform": 10902.0 + }, + "spouse": { + "base": 4380.0, + "reform": 3642.0 + } + }, + "reform": 14544.0 + }, + "3": { + "base": 17586.0, + "basis": "projected", + "birth_year": 1918, + "components": { + "retired_worker": { + "base": 11367.599999999999, + "reform": 9432.0 + }, + "spouse": { + "base": 6218.400000000001, + "reform": 5160.0 + } + }, + "reform": 14592.0 + }, + "30": { + "base": 37855.2, + "basis": "projected", + "birth_year": 1931, + "components": { + "retired_worker": { + "base": 37855.2, + "reform": 31419.600000000002 + } + }, + "reform": 31419.600000000002 + }, + "31": { + "base": 17535.6, + "basis": "projected", + "birth_year": 1932, + "components": { + "retired_worker": { + "base": 13207.199999999999, + "reform": 10964.400000000001 + }, + "spouse": { + "base": 4328.4, + "reform": 3591.6000000000004 + } + }, + "reform": 14556.0 + }, + "32": { + "base": 37843.2, + "basis": "projected", + "birth_year": 1932, + "components": { + "retired_worker": { + "base": 37843.2, + "reform": 31410.0 + } + }, + "reform": 31410.0 + }, + "33": { + "base": 17419.199999999997, + "basis": "projected", + "birth_year": 1933, + "components": { + "retired_worker": { + "base": 12774.0, + "reform": 10603.2 + }, + "spouse": { + "base": 4645.200000000001, + "reform": 3850.7999999999997 + } + }, + "reform": 14454.0 + }, + "34": { + "base": 37813.2, + "basis": "projected", + "birth_year": 1933, + "components": { + "retired_worker": { + "base": 37813.2, + "reform": 31374.0 + } + }, + "reform": 31374.0 + }, + "35": { + "base": 17576.4, + "basis": "projected", + "birth_year": 1934, + "components": { + "retired_worker": { + "base": 12944.400000000001, + "reform": 10737.599999999999 + }, + "spouse": { + "base": 4632.0, + "reform": 3847.2000000000003 + } + }, + "reform": 14584.800000000001 + }, + "36": { + "base": 38118.0, + "basis": "projected", + "birth_year": 1934, + "components": { + "retired_worker": { + "base": 38118.0, + "reform": 31632.0 + } + }, + "reform": 31632.0 + }, + "37": { + "base": 17744.399999999998, + "basis": "projected", + "birth_year": 1935, + "components": { + "retired_worker": { + "base": 13213.199999999999, + "reform": 10966.8 + }, + "spouse": { + "base": 4531.200000000001, + "reform": 3758.3999999999996 + } + }, + "reform": 14725.199999999999 + }, + "38": { + "base": 38389.2, + "basis": "projected", + "birth_year": 1935, + "components": { + "retired_worker": { + "base": 38389.2, + "reform": 31858.800000000003 + } + }, + "reform": 31858.800000000003 + }, + "39": { + "base": 17781.600000000002, + "basis": "projected", + "birth_year": 1936, + "components": { + "retired_worker": { + "base": 13030.800000000001, + "reform": 10812.0 + }, + "spouse": { + "base": 4750.799999999999, + "reform": 3945.6000000000004 + } + }, + "reform": 14757.599999999999 + }, + "4": { + "base": 39152.399999999994, + "basis": "projected", + "birth_year": 1918, + "components": { + "retired_worker": { + "base": 39152.399999999994, + "reform": 32488.800000000003 + } + }, + "reform": 32488.800000000003 + }, + "40": { + "base": 38606.399999999994, + "basis": "projected", + "birth_year": 1936, + "components": { + "retired_worker": { + "base": 38606.399999999994, + "reform": 32040.0 + } + }, + "reform": 32040.0 + }, + "41": { + "base": 17851.199999999997, + "basis": "projected", + "birth_year": 1937, + "components": { + "retired_worker": { + "base": 13032.0, + "reform": 10813.2 + }, + "spouse": { + "base": 4819.200000000001, + "reform": 3999.6000000000004 + } + }, + "reform": 14812.800000000001 + }, + "42": { + "base": 38790.0, + "basis": "projected", + "birth_year": 1937, + "components": { + "retired_worker": { + "base": 38790.0, + "reform": 32187.600000000002 + } + }, + "reform": 32187.600000000002 + }, + "43": { + "base": 17845.199999999997, + "basis": "projected", + "birth_year": 1938, + "components": { + "retired_worker": { + "base": 12769.199999999999, + "reform": 10598.400000000001 + }, + "spouse": { + "base": 5076.0, + "reform": 4213.200000000001 + } + }, + "reform": 14811.600000000002 + }, + "44": { + "base": 38942.399999999994, + "basis": "projected", + "birth_year": 1938, + "components": { + "retired_worker": { + "base": 38942.399999999994, + "reform": 32319.600000000002 + } + }, + "reform": 32319.600000000002 + }, + "45": { + "base": 18085.2, + "basis": "projected", + "birth_year": 1939, + "components": { + "retired_worker": { + "base": 12976.800000000001, + "reform": 10768.8 + }, + "spouse": { + "base": 5108.4, + "reform": 4239.6 + } + }, + "reform": 15008.400000000001 + }, + "46": { + "base": 39442.8, + "basis": "projected", + "birth_year": 1939, + "components": { + "retired_worker": { + "base": 39442.8, + "reform": 32732.399999999998 + } + }, + "reform": 32732.399999999998 + }, + "47": { + "base": 18242.4, + "basis": "projected", + "birth_year": 1940, + "components": { + "retired_worker": { + "base": 12932.400000000001, + "reform": 10731.599999999999 + }, + "spouse": { + "base": 5310.0, + "reform": 4406.4 + } + }, + "reform": 15138.0 + }, + "48": { + "base": 39885.600000000006, + "basis": "projected", + "birth_year": 1940, + "components": { + "retired_worker": { + "base": 39885.600000000006, + "reform": 33097.2 + } + }, + "reform": 33097.2 + }, + "49": { + "base": 18422.4, + "basis": "projected", + "birth_year": 1941, + "components": { + "retired_worker": { + "base": 13060.800000000001, + "reform": 10837.2 + }, + "spouse": { + "base": 5361.6, + "reform": 4450.799999999999 + } + }, + "reform": 15288.0 + }, + "5": { + "base": 17588.4, + "basis": "projected", + "birth_year": 1919, + "components": { + "retired_worker": { + "base": 11530.8, + "reform": 9571.2 + }, + "spouse": { + "base": 6057.6, + "reform": 5025.6 + } + }, + "reform": 14596.800000000001 + }, + "50": { + "base": 40276.8, + "basis": "projected", + "birth_year": 1941, + "components": { + "retired_worker": { + "base": 40276.8, + "reform": 33426.0 + } + }, + "reform": 33426.0 + }, + "51": { + "base": 18520.800000000003, + "basis": "projected", + "birth_year": 1942, + "components": { + "retired_worker": { + "base": 12902.400000000001, + "reform": 10710.0 + }, + "spouse": { + "base": 5618.4, + "reform": 4660.799999999999 + } + }, + "reform": 15370.800000000001 + }, + "52": { + "base": 40638.0, + "basis": "projected", + "birth_year": 1942, + "components": { + "retired_worker": { + "base": 40638.0, + "reform": 33726.0 + } + }, + "reform": 33726.0 + }, + "53": { + "base": 12949.199999999999, + "basis": "projected", + "birth_year": 1965, + "components": { + "retired_worker": { + "base": 3379.2000000000003, + "reform": 3280.7999999999997 + }, + "spouse": { + "base": 9570.0, + "reform": 9292.8 + } + }, + "reform": 12573.599999999999 + }, + "54": { + "base": 27372.0, + "basis": "projected", + "birth_year": 1965, + "components": { + "retired_worker": { + "base": 27372.0, + "reform": 26580.0 + } + }, + "reform": 26580.0 + }, + "55": { + "base": 13039.199999999999, + "basis": "projected", + "birth_year": 1966, + "components": { + "retired_worker": { + "base": 3219.6000000000004, + "reform": 3158.3999999999996 + }, + "spouse": { + "base": 9819.599999999999, + "reform": 9628.8 + } + }, + "reform": 12787.199999999999 + }, + "56": { + "base": 27591.600000000002, + "basis": "projected", + "birth_year": 1966, + "components": { + "retired_worker": { + "base": 27591.600000000002, + "reform": 27057.600000000002 + } + }, + "reform": 27057.600000000002 + }, + "57": { + "base": 13112.400000000001, + "basis": "projected", + "birth_year": 1967, + "components": { + "retired_worker": { + "base": 3092.3999999999996, + "reform": 3062.3999999999996 + }, + "spouse": { + "base": 10020.0, + "reform": 9922.8 + } + }, + "reform": 12985.199999999999 + }, + "58": { + "base": 27769.199999999997, + "basis": "projected", + "birth_year": 1967, + "components": { + "retired_worker": { + "base": 27769.199999999997, + "reform": 27498.0 + } + }, + "reform": 27498.0 + }, + "59": { + "base": 13095.599999999999, + "basis": "projected", + "birth_year": 1968, + "components": { + "retired_worker": { + "base": 3024.0, + "reform": 3024.0 + }, + "spouse": { + "base": 10071.599999999999, + "reform": 10071.599999999999 + } + }, + "reform": 13095.599999999999 + }, + "6": { + "base": 39056.399999999994, + "basis": "projected", + "birth_year": 1919, + "components": { + "retired_worker": { + "base": 39056.399999999994, + "reform": 32410.800000000003 + } + }, + "reform": 32410.800000000003 + }, + "60": { + "base": 27741.600000000002, + "basis": "projected", + "birth_year": 1968, + "components": { + "retired_worker": { + "base": 27741.600000000002, + "reform": 27741.600000000002 + } + }, + "reform": 27741.600000000002 + }, + "61": { + "base": 13213.199999999999, + "basis": "projected", + "birth_year": 1975, + "components": { + "disabled_worker": { + "base": 13213.199999999999, + "reform": 10963.2 + } + }, + "reform": 10963.2 + }, + "62": { + "base": 4516.799999999999, + "basis": "projected", + "birth_year": 1967, + "components": { + "retired_worker": { + "base": 3116.3999999999996, + "reform": 3085.2000000000003 + }, + "spouse": { + "base": 1400.4, + "reform": 697.2 + } + }, + "reform": 3782.4000000000005 + }, + "63": { + "base": 15170.400000000001, + "basis": "projected", + "birth_year": 1960, + "components": { + "retired_worker": { + "base": 15170.400000000001, + "reform": 12590.400000000001 + } + }, + "reform": 12590.400000000001 + }, + "65": { + "base": 40938.0, + "basis": "projected", + "birth_year": 1960, + "components": { + "aged_widow": { + "base": 36267.600000000006, + "reform": 32796.0 + }, + "retired_worker": { + "base": 4670.4, + "reform": 4317.6 + } + }, + "reform": 37113.600000000006 + }, + "66": { + "base": 13574.37690890607, + "basis": "opening_stock", + "birth_year": 1935, + "components": { + "retired_worker": { + "base": 13574.37690890607, + "reform": 11265.695592540982 + } + }, + "reform": 11265.695592540982 + }, + "67": { + "base": 19011.84038662128, + "basis": "opening_stock", + "birth_year": 1975, + "components": { + "disabled_worker": { + "base": 19011.84038662128, + "reform": 15778.374792962226 + } + }, + "reform": 15778.374792962226 + }, + "68": { + "base": 21634.16319856905, + "basis": "opening_stock", + "birth_year": 1940, + "components": { + "spouse": { + "base": 21634.16319856905, + "reform": 17954.70235061218 + } + }, + "reform": 17954.70235061218 + }, + "69": { + "base": 17565.70648297362, + "basis": "opening_stock", + "birth_year": 1942, + "components": { + "aged_widow": { + "base": 17565.70648297362, + "reform": 14578.194154552326 + } + }, + "reform": 14578.194154552326 + }, + "7": { + "base": 17528.4, + "basis": "projected", + "birth_year": 1920, + "components": { + "retired_worker": { + "base": 11574.0, + "reform": 9600.0 + }, + "spouse": { + "base": 5954.4, + "reform": 4944.0 + } + }, + "reform": 14544.0 + }, + "70": { + "base": 15560.40080324886, + "basis": "opening_stock", + "birth_year": 1975, + "components": { + "disabled_widow": { + "base": 15560.40080324886, + "reform": 12913.943669290587 + } + }, + "reform": 12913.943669290587 + }, + "8": { + "base": 38869.2, + "basis": "projected", + "birth_year": 1920, + "components": { + "retired_worker": { + "base": 38869.2, + "reform": 32254.800000000003 + } + }, + "reform": 32254.800000000003 + }, + "9": { + "base": 17515.199999999997, + "basis": "projected", + "birth_year": 1921, + "components": { + "retired_worker": { + "base": 11822.400000000001, + "reform": 9812.400000000001 + }, + "spouse": { + "base": 5692.799999999999, + "reform": 4723.200000000001 + } + }, + "reform": 14535.600000000002 + } + }, + "M1": { + "1": { + "base": 17727.600000000002, + "basis": "projected", + "birth_year": 1917, + "components": { + "retired_worker": { + "base": 11239.2, + "reform": 9234.0 + }, + "spouse": { + "base": 6488.400000000001, + "reform": 5331.6 + } + }, + "reform": 14565.599999999999 + }, + "10": { + "base": 38676.0, + "basis": "projected", + "birth_year": 1921, + "components": { + "retired_worker": { + "base": 38676.0, + "reform": 31780.800000000003 + } + }, + "reform": 31780.800000000003 + }, + "11": { + "base": 17415.6, + "basis": "projected", + "birth_year": 1922, + "components": { + "retired_worker": { + "base": 11844.0, + "reform": 9733.2 + }, + "spouse": { + "base": 5571.6, + "reform": 4579.200000000001 + } + }, + "reform": 14312.400000000001 + }, + "12": { + "base": 38398.8, + "basis": "projected", + "birth_year": 1922, + "components": { + "retired_worker": { + "base": 38398.8, + "reform": 31556.399999999998 + } + }, + "reform": 31556.399999999998 + }, + "13": { + "base": 17372.4, + "basis": "projected", + "birth_year": 1923, + "components": { + "retired_worker": { + "base": 12145.2, + "reform": 9980.400000000001 + }, + "spouse": { + "base": 5227.200000000001, + "reform": 4296.0 + } + }, + "reform": 14276.400000000001 + }, + "14": { + "base": 38091.600000000006, + "basis": "projected", + "birth_year": 1923, + "components": { + "retired_worker": { + "base": 38091.600000000006, + "reform": 31304.399999999998 + } + }, + "reform": 31304.399999999998 + }, + "15": { + "base": 17438.399999999998, + "basis": "projected", + "birth_year": 1924, + "components": { + "retired_worker": { + "base": 12381.599999999999, + "reform": 10168.8 + }, + "spouse": { + "base": 5056.799999999999, + "reform": 4159.200000000001 + } + }, + "reform": 14328.0 + }, + "16": { + "base": 38115.600000000006, + "basis": "projected", + "birth_year": 1924, + "components": { + "retired_worker": { + "base": 38115.600000000006, + "reform": 31318.800000000003 + } + }, + "reform": 31318.800000000003 + }, + "17": { + "base": 17407.199999999997, + "basis": "projected", + "birth_year": 1925, + "components": { + "retired_worker": { + "base": 12348.0, + "reform": 10146.0 + }, + "spouse": { + "base": 5059.200000000001, + "reform": 4158.0 + } + }, + "reform": 14304.0 + }, + "18": { + "base": 38055.600000000006, + "basis": "projected", + "birth_year": 1925, + "components": { + "retired_worker": { + "base": 38055.600000000006, + "reform": 31270.800000000003 + } + }, + "reform": 31270.800000000003 + }, + "19": { + "base": 17433.600000000002, + "basis": "projected", + "birth_year": 1926, + "components": { + "retired_worker": { + "base": 12610.800000000001, + "reform": 10370.400000000001 + }, + "spouse": { + "base": 4822.799999999999, + "reform": 3957.6000000000004 + } + }, + "reform": 14328.0 + }, + "2": { + "base": 39610.8, + "basis": "projected", + "birth_year": 1917, + "components": { + "retired_worker": { + "base": 39610.8, + "reform": 32544.0 + } + }, + "reform": 32544.0 + }, + "20": { + "base": 37957.2, + "basis": "projected", + "birth_year": 1926, + "components": { + "retired_worker": { + "base": 37957.2, + "reform": 31188.0 + } + }, + "reform": 31188.0 + }, + "21": { + "base": 17437.199999999997, + "basis": "projected", + "birth_year": 1927, + "components": { + "retired_worker": { + "base": 12853.199999999999, + "reform": 10561.2 + }, + "spouse": { + "base": 4584.0, + "reform": 3766.7999999999997 + } + }, + "reform": 14328.0 + }, + "22": { + "base": 37810.8, + "basis": "projected", + "birth_year": 1927, + "components": { + "retired_worker": { + "base": 37810.8, + "reform": 31069.199999999997 + } + }, + "reform": 31069.199999999997 + }, + "23": { + "base": 17392.800000000003, + "basis": "projected", + "birth_year": 1928, + "components": { + "retired_worker": { + "base": 12974.400000000001, + "reform": 10660.8 + }, + "spouse": { + "base": 4418.4, + "reform": 3632.3999999999996 + } + }, + "reform": 14293.199999999999 + }, + "24": { + "base": 37615.2, + "basis": "projected", + "birth_year": 1928, + "components": { + "retired_worker": { + "base": 37615.2, + "reform": 30913.199999999997 + } + }, + "reform": 30913.199999999997 + }, + "25": { + "base": 17530.800000000003, + "basis": "projected", + "birth_year": 1929, + "components": { + "retired_worker": { + "base": 13332.0, + "reform": 10960.8 + }, + "spouse": { + "base": 4198.799999999999, + "reform": 3446.3999999999996 + } + }, + "reform": 14407.199999999999 + }, + "26": { + "base": 37749.600000000006, + "basis": "projected", + "birth_year": 1929, + "components": { + "retired_worker": { + "base": 37749.600000000006, + "reform": 31020.0 + } + }, + "reform": 31020.0 + }, + "27": { + "base": 17530.800000000003, + "basis": "projected", + "birth_year": 1930, + "components": { + "retired_worker": { + "base": 13218.0, + "reform": 10863.599999999999 + }, + "spouse": { + "base": 4312.799999999999, + "reform": 3544.7999999999997 + } + }, + "reform": 14408.399999999998 + }, + "28": { + "base": 37824.0, + "basis": "projected", + "birth_year": 1930, + "components": { + "retired_worker": { + "base": 37824.0, + "reform": 31088.399999999998 + } + }, + "reform": 31088.399999999998 + }, + "29": { + "base": 17524.800000000003, + "basis": "projected", + "birth_year": 1931, + "components": { + "retired_worker": { + "base": 13144.800000000001, + "reform": 10796.400000000001 + }, + "spouse": { + "base": 4380.0, + "reform": 3603.6000000000004 + } + }, + "reform": 14400.0 + }, + "3": { + "base": 17586.0, + "basis": "projected", + "birth_year": 1918, + "components": { + "retired_worker": { + "base": 11367.599999999999, + "reform": 9343.2 + }, + "spouse": { + "base": 6218.400000000001, + "reform": 5107.200000000001 + } + }, + "reform": 14450.400000000001 + }, + "30": { + "base": 37855.2, + "basis": "projected", + "birth_year": 1931, + "components": { + "retired_worker": { + "base": 37855.2, + "reform": 31107.600000000002 + } + }, + "reform": 31107.600000000002 + }, + "31": { + "base": 17535.6, + "basis": "projected", + "birth_year": 1932, + "components": { + "retired_worker": { + "base": 13207.199999999999, + "reform": 10850.400000000001 + }, + "spouse": { + "base": 4328.4, + "reform": 3560.3999999999996 + } + }, + "reform": 14410.800000000001 + }, + "32": { + "base": 37843.2, + "basis": "projected", + "birth_year": 1932, + "components": { + "retired_worker": { + "base": 37843.2, + "reform": 31099.199999999997 + } + }, + "reform": 31099.199999999997 + }, + "33": { + "base": 17419.199999999997, + "basis": "projected", + "birth_year": 1933, + "components": { + "retired_worker": { + "base": 12774.0, + "reform": 10496.400000000001 + }, + "spouse": { + "base": 4645.200000000001, + "reform": 3817.2000000000003 + } + }, + "reform": 14313.600000000002 + }, + "34": { + "base": 37813.2, + "basis": "projected", + "birth_year": 1933, + "components": { + "retired_worker": { + "base": 37813.2, + "reform": 31071.600000000002 + } + }, + "reform": 31071.600000000002 + }, + "35": { + "base": 17576.4, + "basis": "projected", + "birth_year": 1934, + "components": { + "retired_worker": { + "base": 12944.400000000001, + "reform": 10629.599999999999 + }, + "spouse": { + "base": 4632.0, + "reform": 3811.2000000000003 + } + }, + "reform": 14440.800000000001 + }, + "36": { + "base": 38118.0, + "basis": "projected", + "birth_year": 1934, + "components": { + "retired_worker": { + "base": 38118.0, + "reform": 31321.199999999997 + } + }, + "reform": 31321.199999999997 + }, + "37": { + "base": 17744.399999999998, + "basis": "projected", + "birth_year": 1935, + "components": { + "retired_worker": { + "base": 13213.199999999999, + "reform": 10856.400000000001 + }, + "spouse": { + "base": 4531.200000000001, + "reform": 3726.0 + } + }, + "reform": 14582.400000000001 + }, + "38": { + "base": 38389.2, + "basis": "projected", + "birth_year": 1935, + "components": { + "retired_worker": { + "base": 38389.2, + "reform": 31549.199999999997 + } + }, + "reform": 31549.199999999997 + }, + "39": { + "base": 17781.600000000002, + "basis": "projected", + "birth_year": 1936, + "components": { + "retired_worker": { + "base": 13030.800000000001, + "reform": 10705.2 + }, + "spouse": { + "base": 4750.799999999999, + "reform": 3906.0 + } + }, + "reform": 14611.199999999999 + }, + "4": { + "base": 39152.399999999994, + "basis": "projected", + "birth_year": 1918, + "components": { + "retired_worker": { + "base": 39152.399999999994, + "reform": 32172.0 + } + }, + "reform": 32172.0 + }, + "40": { + "base": 38606.399999999994, + "basis": "projected", + "birth_year": 1936, + "components": { + "retired_worker": { + "base": 38606.399999999994, + "reform": 31725.600000000002 + } + }, + "reform": 31725.600000000002 + }, + "41": { + "base": 17851.199999999997, + "basis": "projected", + "birth_year": 1937, + "components": { + "retired_worker": { + "base": 13032.0, + "reform": 10708.8 + }, + "spouse": { + "base": 4819.200000000001, + "reform": 3957.6000000000004 + } + }, + "reform": 14666.400000000001 + }, + "42": { + "base": 38790.0, + "basis": "projected", + "birth_year": 1937, + "components": { + "retired_worker": { + "base": 38790.0, + "reform": 31868.399999999998 + } + }, + "reform": 31868.399999999998 + }, + "43": { + "base": 17845.199999999997, + "basis": "projected", + "birth_year": 1938, + "components": { + "retired_worker": { + "base": 12769.199999999999, + "reform": 10491.599999999999 + }, + "spouse": { + "base": 5076.0, + "reform": 4172.4 + } + }, + "reform": 14664.0 + }, + "44": { + "base": 38942.399999999994, + "basis": "projected", + "birth_year": 1938, + "components": { + "retired_worker": { + "base": 38942.399999999994, + "reform": 32000.399999999998 + } + }, + "reform": 32000.399999999998 + }, + "45": { + "base": 18085.2, + "basis": "projected", + "birth_year": 1939, + "components": { + "retired_worker": { + "base": 12976.800000000001, + "reform": 10664.400000000001 + }, + "spouse": { + "base": 5108.4, + "reform": 4196.4 + } + }, + "reform": 14860.800000000001 + }, + "46": { + "base": 39442.8, + "basis": "projected", + "birth_year": 1939, + "components": { + "retired_worker": { + "base": 39442.8, + "reform": 32409.600000000002 + } + }, + "reform": 32409.600000000002 + }, + "47": { + "base": 18242.4, + "basis": "projected", + "birth_year": 1940, + "components": { + "retired_worker": { + "base": 12932.400000000001, + "reform": 10623.599999999999 + }, + "spouse": { + "base": 5310.0, + "reform": 4366.799999999999 + } + }, + "reform": 14990.399999999998 + }, + "48": { + "base": 39885.600000000006, + "basis": "projected", + "birth_year": 1940, + "components": { + "retired_worker": { + "base": 39885.600000000006, + "reform": 32776.8 + } + }, + "reform": 32776.8 + }, + "49": { + "base": 18422.4, + "basis": "projected", + "birth_year": 1941, + "components": { + "retired_worker": { + "base": 13060.800000000001, + "reform": 10732.8 + }, + "spouse": { + "base": 5361.6, + "reform": 4405.200000000001 + } + }, + "reform": 15138.0 + }, + "5": { + "base": 17588.4, + "basis": "projected", + "birth_year": 1919, + "components": { + "retired_worker": { + "base": 11530.8, + "reform": 9476.400000000001 + }, + "spouse": { + "base": 6057.6, + "reform": 4977.6 + } + }, + "reform": 14454.0 + }, + "50": { + "base": 40276.8, + "basis": "projected", + "birth_year": 1941, + "components": { + "retired_worker": { + "base": 40276.8, + "reform": 33097.2 + } + }, + "reform": 33097.2 + }, + "51": { + "base": 18520.800000000003, + "basis": "projected", + "birth_year": 1942, + "components": { + "retired_worker": { + "base": 12902.400000000001, + "reform": 10603.2 + }, + "spouse": { + "base": 5618.4, + "reform": 4616.4 + } + }, + "reform": 15219.599999999999 + }, + "52": { + "base": 40638.0, + "basis": "projected", + "birth_year": 1942, + "components": { + "retired_worker": { + "base": 40638.0, + "reform": 33394.8 + } + }, + "reform": 33394.8 + }, + "53": { + "base": 12949.199999999999, + "basis": "projected", + "birth_year": 1965, + "components": { + "retired_worker": { + "base": 3379.2000000000003, + "reform": 3280.7999999999997 + }, + "spouse": { + "base": 9570.0, + "reform": 9292.8 + } + }, + "reform": 12573.599999999999 + }, + "54": { + "base": 27372.0, + "basis": "projected", + "birth_year": 1965, + "components": { + "retired_worker": { + "base": 27372.0, + "reform": 26580.0 + } + }, + "reform": 26580.0 + }, + "55": { + "base": 13039.199999999999, + "basis": "projected", + "birth_year": 1966, + "components": { + "retired_worker": { + "base": 3219.6000000000004, + "reform": 3158.3999999999996 + }, + "spouse": { + "base": 9819.599999999999, + "reform": 9628.8 + } + }, + "reform": 12787.199999999999 + }, + "56": { + "base": 27591.600000000002, + "basis": "projected", + "birth_year": 1966, + "components": { + "retired_worker": { + "base": 27591.600000000002, + "reform": 27057.600000000002 + } + }, + "reform": 27057.600000000002 + }, + "57": { + "base": 13112.400000000001, + "basis": "projected", + "birth_year": 1967, + "components": { + "retired_worker": { + "base": 3092.3999999999996, + "reform": 3062.3999999999996 + }, + "spouse": { + "base": 10020.0, + "reform": 9922.8 + } + }, + "reform": 12985.199999999999 + }, + "58": { + "base": 27769.199999999997, + "basis": "projected", + "birth_year": 1967, + "components": { + "retired_worker": { + "base": 27769.199999999997, + "reform": 27498.0 + } + }, + "reform": 27498.0 + }, + "59": { + "base": 13095.599999999999, + "basis": "projected", + "birth_year": 1968, + "components": { + "retired_worker": { + "base": 3024.0, + "reform": 3024.0 + }, + "spouse": { + "base": 10071.599999999999, + "reform": 10071.599999999999 + } + }, + "reform": 13095.599999999999 + }, + "6": { + "base": 39056.399999999994, + "basis": "projected", + "birth_year": 1919, + "components": { + "retired_worker": { + "base": 39056.399999999994, + "reform": 32096.399999999998 + } + }, + "reform": 32096.399999999998 + }, + "60": { + "base": 27741.600000000002, + "basis": "projected", + "birth_year": 1968, + "components": { + "retired_worker": { + "base": 27741.600000000002, + "reform": 27741.600000000002 + } + }, + "reform": 27741.600000000002 + }, + "61": { + "base": 13213.199999999999, + "basis": "projected", + "birth_year": 1975, + "components": { + "disabled_worker": { + "base": 13213.199999999999, + "reform": 10855.2 + } + }, + "reform": 10855.2 + }, + "62": { + "base": 4516.799999999999, + "basis": "projected", + "birth_year": 1967, + "components": { + "retired_worker": { + "base": 3116.3999999999996, + "reform": 3085.2000000000003 + }, + "spouse": { + "base": 1400.4, + "reform": 662.4000000000001 + } + }, + "reform": 3747.6000000000004 + }, + "63": { + "base": 15170.400000000001, + "basis": "projected", + "birth_year": 1960, + "components": { + "retired_worker": { + "base": 15170.400000000001, + "reform": 12464.400000000001 + } + }, + "reform": 12464.400000000001 + }, + "65": { + "base": 40938.0, + "basis": "projected", + "birth_year": 1960, + "components": { + "aged_widow": { + "base": 36267.600000000006, + "reform": 32796.0 + }, + "retired_worker": { + "base": 4670.4, + "reform": 4317.6 + } + }, + "reform": 37113.600000000006 + }, + "66": { + "base": 13574.37690890607, + "basis": "opening_stock", + "birth_year": 1935, + "components": { + "retired_worker": { + "base": 13574.37690890607, + "reform": 11155.247596535677 + } + }, + "reform": 11155.247596535677 + }, + "67": { + "base": 19011.84038662128, + "basis": "opening_stock", + "birth_year": 1975, + "components": { + "disabled_worker": { + "base": 19011.84038662128, + "reform": 15623.68484401162 + } + }, + "reform": 15623.68484401162 + }, + "68": { + "base": 21634.16319856905, + "basis": "opening_stock", + "birth_year": 1940, + "components": { + "spouse": { + "base": 21634.16319856905, + "reform": 17778.67585697873 + } + }, + "reform": 17778.67585697873 + }, + "69": { + "base": 17565.70648297362, + "basis": "opening_stock", + "birth_year": 1942, + "components": { + "aged_widow": { + "base": 17565.70648297362, + "reform": 14435.270682448867 + } + }, + "reform": 14435.270682448867 + }, + "7": { + "base": 17528.4, + "basis": "projected", + "birth_year": 1920, + "components": { + "retired_worker": { + "base": 11574.0, + "reform": 9507.599999999999 + }, + "spouse": { + "base": 5954.4, + "reform": 4894.799999999999 + } + }, + "reform": 14402.399999999998 + }, + "70": { + "base": 15560.40080324886, + "basis": "opening_stock", + "birth_year": 1975, + "components": { + "disabled_widow": { + "base": 15560.40080324886, + "reform": 12787.336378415188 + } + }, + "reform": 12787.336378415188 + }, + "8": { + "base": 38869.2, + "basis": "projected", + "birth_year": 1920, + "components": { + "retired_worker": { + "base": 38869.2, + "reform": 31940.399999999998 + } + }, + "reform": 31940.399999999998 + }, + "9": { + "base": 17515.199999999997, + "basis": "projected", + "birth_year": 1921, + "components": { + "retired_worker": { + "base": 11822.400000000001, + "reform": 9714.0 + }, + "spouse": { + "base": 5692.799999999999, + "reform": 4678.799999999999 + } + }, + "reform": 14392.800000000001 + } + }, + "M2": { + "1": { + "base": 18081.600000000002, + "basis": "projected", + "birth_year": 1917, + "components": { + "retired_worker": { + "base": 11462.400000000001, + "reform": 9421.2 + }, + "spouse": { + "base": 6619.200000000001, + "reform": 5437.200000000001 + } + }, + "reform": 14858.400000000001 + }, + "10": { + "base": 39448.8, + "basis": "projected", + "birth_year": 1921, + "components": { + "retired_worker": { + "base": 39448.8, + "reform": 32415.600000000002 + } + }, + "reform": 32415.600000000002 + }, + "11": { + "base": 17763.600000000002, + "basis": "projected", + "birth_year": 1922, + "components": { + "retired_worker": { + "base": 12080.400000000001, + "reform": 9932.400000000001 + }, + "spouse": { + "base": 5683.200000000001, + "reform": 4665.6 + } + }, + "reform": 14598.0 + }, + "12": { + "base": 39165.600000000006, + "basis": "projected", + "birth_year": 1922, + "components": { + "retired_worker": { + "base": 39165.600000000006, + "reform": 32185.199999999997 + } + }, + "reform": 32185.199999999997 + }, + "13": { + "base": 17719.199999999997, + "basis": "projected", + "birth_year": 1923, + "components": { + "retired_worker": { + "base": 12387.599999999999, + "reform": 10184.400000000001 + }, + "spouse": { + "base": 5331.6, + "reform": 4377.6 + } + }, + "reform": 14562.0 + }, + "14": { + "base": 38851.2, + "basis": "projected", + "birth_year": 1923, + "components": { + "retired_worker": { + "base": 38851.2, + "reform": 31926.0 + } + }, + "reform": 31926.0 + }, + "15": { + "base": 17787.600000000002, + "basis": "projected", + "birth_year": 1924, + "components": { + "retired_worker": { + "base": 12628.800000000001, + "reform": 10372.8 + }, + "spouse": { + "base": 5158.799999999999, + "reform": 4240.799999999999 + } + }, + "reform": 14613.599999999999 + }, + "16": { + "base": 38877.600000000006, + "basis": "projected", + "birth_year": 1924, + "components": { + "retired_worker": { + "base": 38877.600000000006, + "reform": 31944.0 + } + }, + "reform": 31944.0 + }, + "17": { + "base": 17755.199999999997, + "basis": "projected", + "birth_year": 1925, + "components": { + "retired_worker": { + "base": 12594.0, + "reform": 10352.400000000001 + }, + "spouse": { + "base": 5161.200000000001, + "reform": 4238.4 + } + }, + "reform": 14590.800000000001 + }, + "18": { + "base": 38815.2, + "basis": "projected", + "birth_year": 1925, + "components": { + "retired_worker": { + "base": 38815.2, + "reform": 31896.0 + } + }, + "reform": 31896.0 + }, + "19": { + "base": 17782.800000000003, + "basis": "projected", + "birth_year": 1926, + "components": { + "retired_worker": { + "base": 12862.800000000001, + "reform": 10569.599999999999 + }, + "spouse": { + "base": 4920.0, + "reform": 4042.7999999999997 + } + }, + "reform": 14612.399999999998 + }, + "2": { + "base": 40402.8, + "basis": "projected", + "birth_year": 1917, + "components": { + "retired_worker": { + "base": 40402.8, + "reform": 33196.8 + } + }, + "reform": 33196.8 + }, + "20": { + "base": 38715.600000000006, + "basis": "projected", + "birth_year": 1926, + "components": { + "retired_worker": { + "base": 38715.600000000006, + "reform": 31812.0 + } + }, + "reform": 31812.0 + }, + "21": { + "base": 17786.4, + "basis": "projected", + "birth_year": 1927, + "components": { + "retired_worker": { + "base": 13110.0, + "reform": 10771.2 + }, + "spouse": { + "base": 4676.4, + "reform": 3842.3999999999996 + } + }, + "reform": 14613.599999999999 + }, + "22": { + "base": 38566.8, + "basis": "projected", + "birth_year": 1927, + "components": { + "retired_worker": { + "base": 38566.8, + "reform": 31686.0 + } + }, + "reform": 31686.0 + }, + "23": { + "base": 17739.600000000002, + "basis": "projected", + "birth_year": 1928, + "components": { + "retired_worker": { + "base": 13232.400000000001, + "reform": 10872.0 + }, + "spouse": { + "base": 4507.200000000001, + "reform": 3708.0 + } + }, + "reform": 14580.0 + }, + "24": { + "base": 38365.2, + "basis": "projected", + "birth_year": 1928, + "components": { + "retired_worker": { + "base": 38365.2, + "reform": 31533.600000000002 + } + }, + "reform": 31533.600000000002 + }, + "25": { + "base": 17881.199999999997, + "basis": "projected", + "birth_year": 1929, + "components": { + "retired_worker": { + "base": 13598.400000000001, + "reform": 11173.2 + }, + "spouse": { + "base": 4282.799999999999, + "reform": 3519.6000000000004 + } + }, + "reform": 14692.800000000001 + }, + "26": { + "base": 38503.2, + "basis": "projected", + "birth_year": 1929, + "components": { + "retired_worker": { + "base": 38503.2, + "reform": 31640.399999999998 + } + }, + "reform": 31640.399999999998 + }, + "27": { + "base": 17881.2, + "basis": "projected", + "birth_year": 1930, + "components": { + "retired_worker": { + "base": 13480.800000000001, + "reform": 11083.2 + }, + "spouse": { + "base": 4400.4, + "reform": 3614.3999999999996 + } + }, + "reform": 14697.599999999999 + }, + "28": { + "base": 38578.8, + "basis": "projected", + "birth_year": 1930, + "components": { + "retired_worker": { + "base": 38578.8, + "reform": 31707.600000000002 + } + }, + "reform": 31707.600000000002 + }, + "29": { + "base": 17875.199999999997, + "basis": "projected", + "birth_year": 1931, + "components": { + "retired_worker": { + "base": 13406.400000000001, + "reform": 11010.0 + }, + "spouse": { + "base": 4468.799999999999, + "reform": 3679.2000000000003 + } + }, + "reform": 14689.199999999999 + }, + "3": { + "base": 17937.600000000002, + "basis": "projected", + "birth_year": 1918, + "components": { + "retired_worker": { + "base": 11594.400000000001, + "reform": 9525.599999999999 + }, + "spouse": { + "base": 6343.200000000001, + "reform": 5212.799999999999 + } + }, + "reform": 14738.399999999998 + }, + "30": { + "base": 38611.2, + "basis": "projected", + "birth_year": 1931, + "components": { + "retired_worker": { + "base": 38611.2, + "reform": 31732.800000000003 + } + }, + "reform": 31732.800000000003 + }, + "31": { + "base": 17886.0, + "basis": "projected", + "birth_year": 1932, + "components": { + "retired_worker": { + "base": 13470.0, + "reform": 11073.599999999999 + }, + "spouse": { + "base": 4416.0, + "reform": 3627.6000000000004 + } + }, + "reform": 14701.199999999999 + }, + "32": { + "base": 38598.0, + "basis": "projected", + "birth_year": 1932, + "components": { + "retired_worker": { + "base": 38598.0, + "reform": 31723.199999999997 + } + }, + "reform": 31723.199999999997 + }, + "33": { + "base": 17767.199999999997, + "basis": "projected", + "birth_year": 1933, + "components": { + "retired_worker": { + "base": 13029.599999999999, + "reform": 10707.599999999999 + }, + "spouse": { + "base": 4737.6, + "reform": 3890.3999999999996 + } + }, + "reform": 14598.0 + }, + "34": { + "base": 38568.0, + "basis": "projected", + "birth_year": 1933, + "components": { + "retired_worker": { + "base": 38568.0, + "reform": 31687.199999999997 + } + }, + "reform": 31687.199999999997 + }, + "35": { + "base": 17928.0, + "basis": "projected", + "birth_year": 1934, + "components": { + "retired_worker": { + "base": 13202.400000000001, + "reform": 10843.2 + }, + "spouse": { + "base": 4725.6, + "reform": 3885.6000000000004 + } + }, + "reform": 14728.800000000001 + }, + "36": { + "base": 38880.0, + "basis": "projected", + "birth_year": 1934, + "components": { + "retired_worker": { + "base": 38880.0, + "reform": 31947.600000000002 + } + }, + "reform": 31947.600000000002 + }, + "37": { + "base": 18098.4, + "basis": "projected", + "birth_year": 1935, + "components": { + "retired_worker": { + "base": 13476.0, + "reform": 11076.0 + }, + "spouse": { + "base": 4622.4, + "reform": 3796.7999999999997 + } + }, + "reform": 14872.800000000001 + }, + "38": { + "base": 39156.0, + "basis": "projected", + "birth_year": 1935, + "components": { + "retired_worker": { + "base": 39156.0, + "reform": 32176.800000000003 + } + }, + "reform": 32176.800000000003 + }, + "39": { + "base": 18138.0, + "basis": "projected", + "birth_year": 1936, + "components": { + "retired_worker": { + "base": 13291.199999999999, + "reform": 10920.0 + }, + "spouse": { + "base": 4846.799999999999, + "reform": 3985.2000000000003 + } + }, + "reform": 14905.199999999999 + }, + "4": { + "base": 39934.8, + "basis": "projected", + "birth_year": 1918, + "components": { + "retired_worker": { + "base": 39934.8, + "reform": 32814.0 + } + }, + "reform": 32814.0 + }, + "40": { + "base": 39378.0, + "basis": "projected", + "birth_year": 1936, + "components": { + "retired_worker": { + "base": 39378.0, + "reform": 32360.399999999998 + } + }, + "reform": 32360.399999999998 + }, + "41": { + "base": 18208.800000000003, + "basis": "projected", + "birth_year": 1937, + "components": { + "retired_worker": { + "base": 13292.400000000001, + "reform": 10921.2 + }, + "spouse": { + "base": 4916.4, + "reform": 4040.3999999999996 + } + }, + "reform": 14961.599999999999 + }, + "42": { + "base": 39564.0, + "basis": "projected", + "birth_year": 1937, + "components": { + "retired_worker": { + "base": 39564.0, + "reform": 32509.199999999997 + } + }, + "reform": 32509.199999999997 + }, + "43": { + "base": 18202.800000000003, + "basis": "projected", + "birth_year": 1938, + "components": { + "retired_worker": { + "base": 13024.800000000001, + "reform": 10702.8 + }, + "spouse": { + "base": 5178.0, + "reform": 4256.4 + } + }, + "reform": 14959.199999999999 + }, + "44": { + "base": 39720.0, + "basis": "projected", + "birth_year": 1938, + "components": { + "retired_worker": { + "base": 39720.0, + "reform": 32642.399999999998 + } + }, + "reform": 32642.399999999998 + }, + "45": { + "base": 18446.4, + "basis": "projected", + "birth_year": 1939, + "components": { + "retired_worker": { + "base": 13234.800000000001, + "reform": 10875.599999999999 + }, + "spouse": { + "base": 5211.6, + "reform": 4282.799999999999 + } + }, + "reform": 15158.399999999998 + }, + "46": { + "base": 40231.2, + "basis": "projected", + "birth_year": 1939, + "components": { + "retired_worker": { + "base": 40231.2, + "reform": 33058.8 + } + }, + "reform": 33058.8 + }, + "47": { + "base": 18607.199999999997, + "basis": "projected", + "birth_year": 1940, + "components": { + "retired_worker": { + "base": 13190.400000000001, + "reform": 10837.2 + }, + "spouse": { + "base": 5416.799999999999, + "reform": 4450.799999999999 + } + }, + "reform": 15288.0 + }, + "48": { + "base": 40682.399999999994, + "basis": "projected", + "birth_year": 1940, + "components": { + "retired_worker": { + "base": 40682.399999999994, + "reform": 33426.0 + } + }, + "reform": 33426.0 + }, + "49": { + "base": 18790.8, + "basis": "projected", + "birth_year": 1941, + "components": { + "retired_worker": { + "base": 13321.199999999999, + "reform": 10945.2 + }, + "spouse": { + "base": 5469.6, + "reform": 4496.4 + } + }, + "reform": 15441.599999999999 + }, + "5": { + "base": 17941.199999999997, + "basis": "projected", + "birth_year": 1919, + "components": { + "retired_worker": { + "base": 11761.2, + "reform": 9666.0 + }, + "spouse": { + "base": 6180.0, + "reform": 5076.0 + } + }, + "reform": 14742.0 + }, + "50": { + "base": 41082.0, + "basis": "projected", + "birth_year": 1941, + "components": { + "retired_worker": { + "base": 41082.0, + "reform": 33760.8 + } + }, + "reform": 33760.8 + }, + "51": { + "base": 18890.399999999998, + "basis": "projected", + "birth_year": 1942, + "components": { + "retired_worker": { + "base": 13159.199999999999, + "reform": 10816.8 + }, + "spouse": { + "base": 5731.200000000001, + "reform": 4707.6 + } + }, + "reform": 15524.400000000001 + }, + "52": { + "base": 41451.600000000006, + "basis": "projected", + "birth_year": 1942, + "components": { + "retired_worker": { + "base": 41451.600000000006, + "reform": 34062.0 + } + }, + "reform": 34062.0 + }, + "53": { + "base": 13208.400000000001, + "basis": "projected", + "birth_year": 1965, + "components": { + "retired_worker": { + "base": 3446.3999999999996, + "reform": 3314.3999999999996 + }, + "spouse": { + "base": 9762.0, + "reform": 9385.2 + } + }, + "reform": 12699.599999999999 + }, + "54": { + "base": 27919.199999999997, + "basis": "projected", + "birth_year": 1965, + "components": { + "retired_worker": { + "base": 27919.199999999997, + "reform": 26845.199999999997 + } + }, + "reform": 26845.199999999997 + }, + "55": { + "base": 13300.800000000001, + "basis": "projected", + "birth_year": 1966, + "components": { + "retired_worker": { + "base": 3284.3999999999996, + "reform": 3188.3999999999996 + }, + "spouse": { + "base": 10016.400000000001, + "reform": 9726.0 + } + }, + "reform": 12914.400000000001 + }, + "56": { + "base": 28142.399999999998, + "basis": "projected", + "birth_year": 1966, + "components": { + "retired_worker": { + "base": 28142.399999999998, + "reform": 27327.600000000002 + } + }, + "reform": 27327.600000000002 + }, + "57": { + "base": 13375.199999999999, + "basis": "projected", + "birth_year": 1967, + "components": { + "retired_worker": { + "base": 3153.6000000000004, + "reform": 3092.3999999999996 + }, + "spouse": { + "base": 10221.599999999999, + "reform": 10022.400000000001 + } + }, + "reform": 13114.800000000001 + }, + "58": { + "base": 28324.800000000003, + "basis": "projected", + "birth_year": 1967, + "components": { + "retired_worker": { + "base": 28324.800000000003, + "reform": 27772.800000000003 + } + }, + "reform": 27772.800000000003 + }, + "59": { + "base": 13357.199999999999, + "basis": "projected", + "birth_year": 1968, + "components": { + "retired_worker": { + "base": 3084.0, + "reform": 3054.0 + }, + "spouse": { + "base": 10273.2, + "reform": 10172.400000000001 + } + }, + "reform": 13226.400000000001 + }, + "6": { + "base": 39837.600000000006, + "basis": "projected", + "birth_year": 1919, + "components": { + "retired_worker": { + "base": 39837.600000000006, + "reform": 32734.800000000003 + } + }, + "reform": 32734.800000000003 + }, + "60": { + "base": 28296.0, + "basis": "projected", + "birth_year": 1968, + "components": { + "retired_worker": { + "base": 28296.0, + "reform": 28018.800000000003 + } + }, + "reform": 28018.800000000003 + }, + "61": { + "base": 13477.199999999999, + "basis": "projected", + "birth_year": 1975, + "components": { + "disabled_worker": { + "base": 13477.199999999999, + "reform": 11072.400000000001 + } + }, + "reform": 11072.400000000001 + }, + "62": { + "base": 4605.6, + "basis": "projected", + "birth_year": 1967, + "components": { + "retired_worker": { + "base": 3177.6000000000004, + "reform": 3115.2000000000003 + }, + "spouse": { + "base": 1428.0, + "reform": 704.4000000000001 + } + }, + "reform": 3819.6000000000004 + }, + "63": { + "base": 15472.800000000001, + "basis": "projected", + "birth_year": 1960, + "components": { + "retired_worker": { + "base": 15472.800000000001, + "reform": 12715.199999999999 + } + }, + "reform": 12715.199999999999 + }, + "65": { + "base": 41755.2, + "basis": "projected", + "birth_year": 1960, + "components": { + "aged_widow": { + "base": 36991.2, + "reform": 33123.6 + }, + "retired_worker": { + "base": 4764.0, + "reform": 4359.6 + } + }, + "reform": 37483.2 + }, + "66": { + "base": 13845.86444708419, + "basis": "opening_stock", + "birth_year": 1935, + "components": { + "retired_worker": { + "base": 13845.86444708419, + "reform": 11378.352548466391 + } + }, + "reform": 11378.352548466391 + }, + "67": { + "base": 19392.077194353707, + "basis": "opening_stock", + "birth_year": 1975, + "components": { + "disabled_worker": { + "base": 19392.077194353707, + "reform": 15936.158540891847 + } + }, + "reform": 15936.158540891847 + }, + "68": { + "base": 22066.84646254043, + "basis": "opening_stock", + "birth_year": 1940, + "components": { + "spouse": { + "base": 22066.84646254043, + "reform": 18134.249374118303 + } + }, + "reform": 18134.249374118303 + }, + "69": { + "base": 17917.020612633092, + "basis": "opening_stock", + "birth_year": 1942, + "components": { + "aged_widow": { + "base": 17917.020612633092, + "reform": 14723.97609609785 + } + }, + "reform": 14723.97609609785 + }, + "7": { + "base": 17877.600000000002, + "basis": "projected", + "birth_year": 1920, + "components": { + "retired_worker": { + "base": 11804.400000000001, + "reform": 9694.8 + }, + "spouse": { + "base": 6073.200000000001, + "reform": 4994.4 + } + }, + "reform": 14689.199999999999 + }, + "70": { + "base": 15871.608819313838, + "basis": "opening_stock", + "birth_year": 1975, + "components": { + "disabled_widow": { + "base": 15871.608819313838, + "reform": 13043.083105983493 + } + }, + "reform": 13043.083105983493 + }, + "8": { + "base": 39645.600000000006, + "basis": "projected", + "birth_year": 1920, + "components": { + "retired_worker": { + "base": 39645.600000000006, + "reform": 32576.399999999998 + } + }, + "reform": 32576.399999999998 + }, + "9": { + "base": 17865.6, + "basis": "projected", + "birth_year": 1921, + "components": { + "retired_worker": { + "base": 12058.8, + "reform": 9908.400000000001 + }, + "spouse": { + "base": 5806.799999999999, + "reform": 4771.200000000001 + } + }, + "reform": 14679.600000000002 + } + }, + "M3": { + "1": { + "base": 17727.600000000002, + "basis": "projected", + "birth_year": 1917, + "components": { + "retired_worker": { + "base": 11239.2, + "reform": 9328.8 + }, + "spouse": { + "base": 6488.400000000001, + "reform": 5382.0 + } + }, + "reform": 14710.800000000001 + }, + "10": { + "base": 38676.0, + "basis": "projected", + "birth_year": 1921, + "components": { + "retired_worker": { + "base": 38676.0, + "reform": 32096.399999999998 + } + }, + "reform": 32096.399999999998 + }, + "11": { + "base": 17415.6, + "basis": "projected", + "birth_year": 1922, + "components": { + "retired_worker": { + "base": 11844.0, + "reform": 9834.0 + }, + "spouse": { + "base": 5571.6, + "reform": 4620.0 + } + }, + "reform": 14454.0 + }, + "12": { + "base": 38398.8, + "basis": "projected", + "birth_year": 1922, + "components": { + "retired_worker": { + "base": 38398.8, + "reform": 31866.0 + } + }, + "reform": 31866.0 + }, + "13": { + "base": 17372.4, + "basis": "projected", + "birth_year": 1923, + "components": { + "retired_worker": { + "base": 12145.2, + "reform": 10084.8 + }, + "spouse": { + "base": 5227.200000000001, + "reform": 4333.200000000001 + } + }, + "reform": 14418.0 + }, + "14": { + "base": 38091.600000000006, + "basis": "projected", + "birth_year": 1923, + "components": { + "retired_worker": { + "base": 38091.600000000006, + "reform": 31611.600000000002 + } + }, + "reform": 31611.600000000002 + }, + "15": { + "base": 17438.399999999998, + "basis": "projected", + "birth_year": 1924, + "components": { + "retired_worker": { + "base": 12381.599999999999, + "reform": 10272.0 + }, + "spouse": { + "base": 5056.799999999999, + "reform": 4198.799999999999 + } + }, + "reform": 14470.800000000001 + }, + "16": { + "base": 38115.600000000006, + "basis": "projected", + "birth_year": 1924, + "components": { + "retired_worker": { + "base": 38115.600000000006, + "reform": 31628.399999999998 + } + }, + "reform": 31628.399999999998 + }, + "17": { + "base": 17407.199999999997, + "basis": "projected", + "birth_year": 1925, + "components": { + "retired_worker": { + "base": 12348.0, + "reform": 10251.599999999999 + }, + "spouse": { + "base": 5059.200000000001, + "reform": 4196.4 + } + }, + "reform": 14448.0 + }, + "18": { + "base": 38055.600000000006, + "basis": "projected", + "birth_year": 1925, + "components": { + "retired_worker": { + "base": 38055.600000000006, + "reform": 31581.600000000002 + } + }, + "reform": 31581.600000000002 + }, + "19": { + "base": 17433.600000000002, + "basis": "projected", + "birth_year": 1926, + "components": { + "retired_worker": { + "base": 12610.800000000001, + "reform": 10465.2 + }, + "spouse": { + "base": 4822.799999999999, + "reform": 4003.2000000000003 + } + }, + "reform": 14468.400000000001 + }, + "2": { + "base": 39610.8, + "basis": "projected", + "birth_year": 1917, + "components": { + "retired_worker": { + "base": 39610.8, + "reform": 32869.2 + } + }, + "reform": 32869.2 + }, + "20": { + "base": 37957.2, + "basis": "projected", + "birth_year": 1926, + "components": { + "retired_worker": { + "base": 37957.2, + "reform": 31498.800000000003 + } + }, + "reform": 31498.800000000003 + }, + "21": { + "base": 17437.199999999997, + "basis": "projected", + "birth_year": 1927, + "components": { + "retired_worker": { + "base": 12853.199999999999, + "reform": 10664.400000000001 + }, + "spouse": { + "base": 4584.0, + "reform": 3804.0 + } + }, + "reform": 14468.400000000001 + }, + "22": { + "base": 37810.8, + "basis": "projected", + "birth_year": 1927, + "components": { + "retired_worker": { + "base": 37810.8, + "reform": 31371.600000000002 + } + }, + "reform": 31371.600000000002 + }, + "23": { + "base": 17392.800000000003, + "basis": "projected", + "birth_year": 1928, + "components": { + "retired_worker": { + "base": 12974.400000000001, + "reform": 10766.400000000001 + }, + "spouse": { + "base": 4418.4, + "reform": 3669.6000000000004 + } + }, + "reform": 14436.0 + }, + "24": { + "base": 37615.2, + "basis": "projected", + "birth_year": 1928, + "components": { + "retired_worker": { + "base": 37615.2, + "reform": 31221.600000000002 + } + }, + "reform": 31221.600000000002 + }, + "25": { + "base": 17530.800000000003, + "basis": "projected", + "birth_year": 1929, + "components": { + "retired_worker": { + "base": 13332.0, + "reform": 11064.0 + }, + "spouse": { + "base": 4198.799999999999, + "reform": 3484.7999999999997 + } + }, + "reform": 14548.800000000001 + }, + "26": { + "base": 37749.600000000006, + "basis": "projected", + "birth_year": 1929, + "components": { + "retired_worker": { + "base": 37749.600000000006, + "reform": 31327.199999999997 + } + }, + "reform": 31327.199999999997 + }, + "27": { + "base": 17530.800000000003, + "basis": "projected", + "birth_year": 1930, + "components": { + "retired_worker": { + "base": 13218.0, + "reform": 10972.8 + }, + "spouse": { + "base": 4312.799999999999, + "reform": 3578.3999999999996 + } + }, + "reform": 14551.199999999999 + }, + "28": { + "base": 37824.0, + "basis": "projected", + "birth_year": 1930, + "components": { + "retired_worker": { + "base": 37824.0, + "reform": 31394.399999999998 + } + }, + "reform": 31394.399999999998 + }, + "29": { + "base": 17524.800000000003, + "basis": "projected", + "birth_year": 1931, + "components": { + "retired_worker": { + "base": 13144.800000000001, + "reform": 10902.0 + }, + "spouse": { + "base": 4380.0, + "reform": 3642.0 + } + }, + "reform": 14544.0 + }, + "3": { + "base": 17586.0, + "basis": "projected", + "birth_year": 1918, + "components": { + "retired_worker": { + "base": 11367.599999999999, + "reform": 9432.0 + }, + "spouse": { + "base": 6218.400000000001, + "reform": 5160.0 + } + }, + "reform": 14592.0 + }, + "30": { + "base": 37855.2, + "basis": "projected", + "birth_year": 1931, + "components": { + "retired_worker": { + "base": 37855.2, + "reform": 31419.600000000002 + } + }, + "reform": 31419.600000000002 + }, + "31": { + "base": 17535.6, + "basis": "projected", + "birth_year": 1932, + "components": { + "retired_worker": { + "base": 13207.199999999999, + "reform": 10964.400000000001 + }, + "spouse": { + "base": 4328.4, + "reform": 3591.6000000000004 + } + }, + "reform": 14556.0 + }, + "32": { + "base": 37843.2, + "basis": "projected", + "birth_year": 1932, + "components": { + "retired_worker": { + "base": 37843.2, + "reform": 31410.0 + } + }, + "reform": 31410.0 + }, + "33": { + "base": 17419.199999999997, + "basis": "projected", + "birth_year": 1933, + "components": { + "retired_worker": { + "base": 12774.0, + "reform": 10603.2 + }, + "spouse": { + "base": 4645.200000000001, + "reform": 3850.7999999999997 + } + }, + "reform": 14454.0 + }, + "34": { + "base": 37813.2, + "basis": "projected", + "birth_year": 1933, + "components": { + "retired_worker": { + "base": 37813.2, + "reform": 31374.0 + } + }, + "reform": 31374.0 + }, + "35": { + "base": 17576.4, + "basis": "projected", + "birth_year": 1934, + "components": { + "retired_worker": { + "base": 12944.400000000001, + "reform": 10737.599999999999 + }, + "spouse": { + "base": 4632.0, + "reform": 3847.2000000000003 + } + }, + "reform": 14584.800000000001 + }, + "36": { + "base": 38118.0, + "basis": "projected", + "birth_year": 1934, + "components": { + "retired_worker": { + "base": 38118.0, + "reform": 31632.0 + } + }, + "reform": 31632.0 + }, + "37": { + "base": 17744.399999999998, + "basis": "projected", + "birth_year": 1935, + "components": { + "retired_worker": { + "base": 13213.199999999999, + "reform": 10966.8 + }, + "spouse": { + "base": 4531.200000000001, + "reform": 3758.3999999999996 + } + }, + "reform": 14725.199999999999 + }, + "38": { + "base": 38389.2, + "basis": "projected", + "birth_year": 1935, + "components": { + "retired_worker": { + "base": 38389.2, + "reform": 31858.800000000003 + } + }, + "reform": 31858.800000000003 + }, + "39": { + "base": 17781.600000000002, + "basis": "projected", + "birth_year": 1936, + "components": { + "retired_worker": { + "base": 13030.800000000001, + "reform": 10812.0 + }, + "spouse": { + "base": 4750.799999999999, + "reform": 3945.6000000000004 + } + }, + "reform": 14757.599999999999 + }, + "4": { + "base": 39152.399999999994, + "basis": "projected", + "birth_year": 1918, + "components": { + "retired_worker": { + "base": 39152.399999999994, + "reform": 32488.800000000003 + } + }, + "reform": 32488.800000000003 + }, + "40": { + "base": 38606.399999999994, + "basis": "projected", + "birth_year": 1936, + "components": { + "retired_worker": { + "base": 38606.399999999994, + "reform": 32040.0 + } + }, + "reform": 32040.0 + }, + "41": { + "base": 17851.199999999997, + "basis": "projected", + "birth_year": 1937, + "components": { + "retired_worker": { + "base": 13032.0, + "reform": 10813.2 + }, + "spouse": { + "base": 4819.200000000001, + "reform": 3999.6000000000004 + } + }, + "reform": 14812.800000000001 + }, + "42": { + "base": 38790.0, + "basis": "projected", + "birth_year": 1937, + "components": { + "retired_worker": { + "base": 38790.0, + "reform": 32187.600000000002 + } + }, + "reform": 32187.600000000002 + }, + "43": { + "base": 17845.199999999997, + "basis": "projected", + "birth_year": 1938, + "components": { + "retired_worker": { + "base": 12769.199999999999, + "reform": 10598.400000000001 + }, + "spouse": { + "base": 5076.0, + "reform": 4213.200000000001 + } + }, + "reform": 14811.600000000002 + }, + "44": { + "base": 38942.399999999994, + "basis": "projected", + "birth_year": 1938, + "components": { + "retired_worker": { + "base": 38942.399999999994, + "reform": 32319.600000000002 + } + }, + "reform": 32319.600000000002 + }, + "45": { + "base": 18085.2, + "basis": "projected", + "birth_year": 1939, + "components": { + "retired_worker": { + "base": 12976.800000000001, + "reform": 10768.8 + }, + "spouse": { + "base": 5108.4, + "reform": 4239.6 + } + }, + "reform": 15008.400000000001 + }, + "46": { + "base": 39442.8, + "basis": "projected", + "birth_year": 1939, + "components": { + "retired_worker": { + "base": 39442.8, + "reform": 32732.399999999998 + } + }, + "reform": 32732.399999999998 + }, + "47": { + "base": 18242.4, + "basis": "projected", + "birth_year": 1940, + "components": { + "retired_worker": { + "base": 12932.400000000001, + "reform": 10731.599999999999 + }, + "spouse": { + "base": 5310.0, + "reform": 4406.4 + } + }, + "reform": 15138.0 + }, + "48": { + "base": 39885.600000000006, + "basis": "projected", + "birth_year": 1940, + "components": { + "retired_worker": { + "base": 39885.600000000006, + "reform": 33097.2 + } + }, + "reform": 33097.2 + }, + "49": { + "base": 18422.4, + "basis": "projected", + "birth_year": 1941, + "components": { + "retired_worker": { + "base": 13060.800000000001, + "reform": 10837.2 + }, + "spouse": { + "base": 5361.6, + "reform": 4450.799999999999 + } + }, + "reform": 15288.0 + }, + "5": { + "base": 17588.4, + "basis": "projected", + "birth_year": 1919, + "components": { + "retired_worker": { + "base": 11530.8, + "reform": 9571.2 + }, + "spouse": { + "base": 6057.6, + "reform": 5025.6 + } + }, + "reform": 14596.800000000001 + }, + "50": { + "base": 40276.8, + "basis": "projected", + "birth_year": 1941, + "components": { + "retired_worker": { + "base": 40276.8, + "reform": 33426.0 + } + }, + "reform": 33426.0 + }, + "51": { + "base": 18520.800000000003, + "basis": "projected", + "birth_year": 1942, + "components": { + "retired_worker": { + "base": 12902.400000000001, + "reform": 10816.8 + }, + "spouse": { + "base": 5618.4, + "reform": 4706.4 + } + }, + "reform": 15523.199999999999 + }, + "52": { + "base": 40638.0, + "basis": "projected", + "birth_year": 1942, + "components": { + "retired_worker": { + "base": 40638.0, + "reform": 34059.600000000006 + } + }, + "reform": 34059.600000000006 + }, + "53": { + "base": 12949.199999999999, + "basis": "projected", + "birth_year": 1965, + "components": { + "retired_worker": { + "base": 3379.2000000000003, + "reform": 3280.7999999999997 + }, + "spouse": { + "base": 9570.0, + "reform": 9292.8 + } + }, + "reform": 12573.599999999999 + }, + "54": { + "base": 27372.0, + "basis": "projected", + "birth_year": 1965, + "components": { + "retired_worker": { + "base": 27372.0, + "reform": 26580.0 + } + }, + "reform": 26580.0 + }, + "55": { + "base": 13039.199999999999, + "basis": "projected", + "birth_year": 1966, + "components": { + "retired_worker": { + "base": 3219.6000000000004, + "reform": 3158.3999999999996 + }, + "spouse": { + "base": 9819.599999999999, + "reform": 9628.8 + } + }, + "reform": 12787.199999999999 + }, + "56": { + "base": 27591.600000000002, + "basis": "projected", + "birth_year": 1966, + "components": { + "retired_worker": { + "base": 27591.600000000002, + "reform": 27057.600000000002 + } + }, + "reform": 27057.600000000002 + }, + "57": { + "base": 13112.400000000001, + "basis": "projected", + "birth_year": 1967, + "components": { + "retired_worker": { + "base": 3092.3999999999996, + "reform": 3062.3999999999996 + }, + "spouse": { + "base": 10020.0, + "reform": 9922.8 + } + }, + "reform": 12985.199999999999 + }, + "58": { + "base": 27769.199999999997, + "basis": "projected", + "birth_year": 1967, + "components": { + "retired_worker": { + "base": 27769.199999999997, + "reform": 27498.0 + } + }, + "reform": 27498.0 + }, + "59": { + "base": 13095.599999999999, + "basis": "projected", + "birth_year": 1968, + "components": { + "retired_worker": { + "base": 3024.0, + "reform": 3024.0 + }, + "spouse": { + "base": 10071.599999999999, + "reform": 10071.599999999999 + } + }, + "reform": 13095.599999999999 + }, + "6": { + "base": 39056.399999999994, + "basis": "projected", + "birth_year": 1919, + "components": { + "retired_worker": { + "base": 39056.399999999994, + "reform": 32410.800000000003 + } + }, + "reform": 32410.800000000003 + }, + "60": { + "base": 27741.600000000002, + "basis": "projected", + "birth_year": 1968, + "components": { + "retired_worker": { + "base": 27741.600000000002, + "reform": 27741.600000000002 + } + }, + "reform": 27741.600000000002 + }, + "61": { + "base": 13213.199999999999, + "basis": "projected", + "birth_year": 1975, + "components": { + "disabled_worker": { + "base": 13213.199999999999, + "reform": 10963.2 + } + }, + "reform": 10963.2 + }, + "62": { + "base": 4516.799999999999, + "basis": "projected", + "birth_year": 1967, + "components": { + "retired_worker": { + "base": 3116.3999999999996, + "reform": 3085.2000000000003 + }, + "spouse": { + "base": 1400.4, + "reform": 1387.1999999999998 + } + }, + "reform": 4472.400000000001 + }, + "63": { + "base": 15170.400000000001, + "basis": "projected", + "birth_year": 1960, + "components": { + "retired_worker": { + "base": 15170.400000000001, + "reform": 12590.400000000001 + } + }, + "reform": 12590.400000000001 + }, + "65": { + "base": 40938.0, + "basis": "projected", + "birth_year": 1960, + "components": { + "aged_widow": { + "base": 36267.600000000006, + "reform": 35742.0 + }, + "retired_worker": { + "base": 4670.4, + "reform": 4402.799999999999 + } + }, + "reform": 40144.8 + }, + "66": { + "base": 13574.37690890607, + "basis": "opening_stock", + "birth_year": 1935, + "components": { + "retired_worker": { + "base": 13574.37690890607, + "reform": 11265.695592540982 + } + }, + "reform": 11265.695592540982 + }, + "67": { + "base": 19011.84038662128, + "basis": "opening_stock", + "birth_year": 1975, + "components": { + "disabled_worker": { + "base": 19011.84038662128, + "reform": 15778.374792962226 + } + }, + "reform": 15778.374792962226 + }, + "68": { + "base": 21634.16319856905, + "basis": "opening_stock", + "birth_year": 1940, + "components": { + "spouse": { + "base": 21634.16319856905, + "reform": 17954.70235061218 + } + }, + "reform": 17954.70235061218 + }, + "69": { + "base": 17565.70648297362, + "basis": "opening_stock", + "birth_year": 1942, + "components": { + "aged_widow": { + "base": 17565.70648297362, + "reform": 14578.194154552326 + } + }, + "reform": 14578.194154552326 + }, + "7": { + "base": 17528.4, + "basis": "projected", + "birth_year": 1920, + "components": { + "retired_worker": { + "base": 11574.0, + "reform": 9600.0 + }, + "spouse": { + "base": 5954.4, + "reform": 4944.0 + } + }, + "reform": 14544.0 + }, + "70": { + "base": 15560.40080324886, + "basis": "opening_stock", + "birth_year": 1975, + "components": { + "disabled_widow": { + "base": 15560.40080324886, + "reform": 12913.943669290587 + } + }, + "reform": 12913.943669290587 + }, + "8": { + "base": 38869.2, + "basis": "projected", + "birth_year": 1920, + "components": { + "retired_worker": { + "base": 38869.2, + "reform": 32254.800000000003 + } + }, + "reform": 32254.800000000003 + }, + "9": { + "base": 17515.199999999997, + "basis": "projected", + "birth_year": 1921, + "components": { + "retired_worker": { + "base": 11822.400000000001, + "reform": 9812.400000000001 + }, + "spouse": { + "base": 5692.799999999999, + "reform": 4723.200000000001 + } + }, + "reform": 14535.600000000002 + } + }, + "M4": { + "1": { + "base": 17727.600000000002, + "basis": "projected", + "birth_year": 1917, + "components": { + "retired_worker": { + "base": 11239.2, + "reform": 9328.8 + }, + "spouse": { + "base": 6488.400000000001, + "reform": 5382.0 + } + }, + "reform": 14710.800000000001 + }, + "10": { + "base": 38676.0, + "basis": "projected", + "birth_year": 1921, + "components": { + "retired_worker": { + "base": 38676.0, + "reform": 32096.399999999998 + } + }, + "reform": 32096.399999999998 + }, + "11": { + "base": 17415.6, + "basis": "projected", + "birth_year": 1922, + "components": { + "retired_worker": { + "base": 11844.0, + "reform": 9834.0 + }, + "spouse": { + "base": 5571.6, + "reform": 4620.0 + } + }, + "reform": 14454.0 + }, + "12": { + "base": 38398.8, + "basis": "projected", + "birth_year": 1922, + "components": { + "retired_worker": { + "base": 38398.8, + "reform": 31866.0 + } + }, + "reform": 31866.0 + }, + "13": { + "base": 17372.4, + "basis": "projected", + "birth_year": 1923, + "components": { + "retired_worker": { + "base": 12145.2, + "reform": 10084.8 + }, + "spouse": { + "base": 5227.200000000001, + "reform": 4333.200000000001 + } + }, + "reform": 14418.0 + }, + "14": { + "base": 38091.600000000006, + "basis": "projected", + "birth_year": 1923, + "components": { + "retired_worker": { + "base": 38091.600000000006, + "reform": 31611.600000000002 + } + }, + "reform": 31611.600000000002 + }, + "15": { + "base": 17438.399999999998, + "basis": "projected", + "birth_year": 1924, + "components": { + "retired_worker": { + "base": 12381.599999999999, + "reform": 10272.0 + }, + "spouse": { + "base": 5056.799999999999, + "reform": 4198.799999999999 + } + }, + "reform": 14470.800000000001 + }, + "16": { + "base": 38115.600000000006, + "basis": "projected", + "birth_year": 1924, + "components": { + "retired_worker": { + "base": 38115.600000000006, + "reform": 31628.399999999998 + } + }, + "reform": 31628.399999999998 + }, + "17": { + "base": 17407.199999999997, + "basis": "projected", + "birth_year": 1925, + "components": { + "retired_worker": { + "base": 12348.0, + "reform": 10251.599999999999 + }, + "spouse": { + "base": 5059.200000000001, + "reform": 4196.4 + } + }, + "reform": 14448.0 + }, + "18": { + "base": 38055.600000000006, + "basis": "projected", + "birth_year": 1925, + "components": { + "retired_worker": { + "base": 38055.600000000006, + "reform": 31581.600000000002 + } + }, + "reform": 31581.600000000002 + }, + "19": { + "base": 17433.600000000002, + "basis": "projected", + "birth_year": 1926, + "components": { + "retired_worker": { + "base": 12610.800000000001, + "reform": 10465.2 + }, + "spouse": { + "base": 4822.799999999999, + "reform": 4003.2000000000003 + } + }, + "reform": 14468.400000000001 + }, + "2": { + "base": 39610.8, + "basis": "projected", + "birth_year": 1917, + "components": { + "retired_worker": { + "base": 39610.8, + "reform": 32869.2 + } + }, + "reform": 32869.2 + }, + "20": { + "base": 37957.2, + "basis": "projected", + "birth_year": 1926, + "components": { + "retired_worker": { + "base": 37957.2, + "reform": 31498.800000000003 + } + }, + "reform": 31498.800000000003 + }, + "21": { + "base": 17437.199999999997, + "basis": "projected", + "birth_year": 1927, + "components": { + "retired_worker": { + "base": 12853.199999999999, + "reform": 10664.400000000001 + }, + "spouse": { + "base": 4584.0, + "reform": 3804.0 + } + }, + "reform": 14468.400000000001 + }, + "22": { + "base": 37810.8, + "basis": "projected", + "birth_year": 1927, + "components": { + "retired_worker": { + "base": 37810.8, + "reform": 31371.600000000002 + } + }, + "reform": 31371.600000000002 + }, + "23": { + "base": 17392.800000000003, + "basis": "projected", + "birth_year": 1928, + "components": { + "retired_worker": { + "base": 12974.400000000001, + "reform": 10766.400000000001 + }, + "spouse": { + "base": 4418.4, + "reform": 3669.6000000000004 + } + }, + "reform": 14436.0 + }, + "24": { + "base": 37615.2, + "basis": "projected", + "birth_year": 1928, + "components": { + "retired_worker": { + "base": 37615.2, + "reform": 31221.600000000002 + } + }, + "reform": 31221.600000000002 + }, + "25": { + "base": 17530.800000000003, + "basis": "projected", + "birth_year": 1929, + "components": { + "retired_worker": { + "base": 13332.0, + "reform": 11064.0 + }, + "spouse": { + "base": 4198.799999999999, + "reform": 3484.7999999999997 + } + }, + "reform": 14548.800000000001 + }, + "26": { + "base": 37749.600000000006, + "basis": "projected", + "birth_year": 1929, + "components": { + "retired_worker": { + "base": 37749.600000000006, + "reform": 31327.199999999997 + } + }, + "reform": 31327.199999999997 + }, + "27": { + "base": 17530.800000000003, + "basis": "projected", + "birth_year": 1930, + "components": { + "retired_worker": { + "base": 13218.0, + "reform": 10972.8 + }, + "spouse": { + "base": 4312.799999999999, + "reform": 3578.3999999999996 + } + }, + "reform": 14551.199999999999 + }, + "28": { + "base": 37824.0, + "basis": "projected", + "birth_year": 1930, + "components": { + "retired_worker": { + "base": 37824.0, + "reform": 31394.399999999998 + } + }, + "reform": 31394.399999999998 + }, + "29": { + "base": 17524.800000000003, + "basis": "projected", + "birth_year": 1931, + "components": { + "retired_worker": { + "base": 13144.800000000001, + "reform": 10902.0 + }, + "spouse": { + "base": 4380.0, + "reform": 3642.0 + } + }, + "reform": 14544.0 + }, + "3": { + "base": 17586.0, + "basis": "projected", + "birth_year": 1918, + "components": { + "retired_worker": { + "base": 11367.599999999999, + "reform": 9432.0 + }, + "spouse": { + "base": 6218.400000000001, + "reform": 5160.0 + } + }, + "reform": 14592.0 + }, + "30": { + "base": 37855.2, + "basis": "projected", + "birth_year": 1931, + "components": { + "retired_worker": { + "base": 37855.2, + "reform": 31419.600000000002 + } + }, + "reform": 31419.600000000002 + }, + "31": { + "base": 17535.6, + "basis": "projected", + "birth_year": 1932, + "components": { + "retired_worker": { + "base": 13207.199999999999, + "reform": 10964.400000000001 + }, + "spouse": { + "base": 4328.4, + "reform": 3591.6000000000004 + } + }, + "reform": 14556.0 + }, + "32": { + "base": 37843.2, + "basis": "projected", + "birth_year": 1932, + "components": { + "retired_worker": { + "base": 37843.2, + "reform": 31410.0 + } + }, + "reform": 31410.0 + }, + "33": { + "base": 17419.199999999997, + "basis": "projected", + "birth_year": 1933, + "components": { + "retired_worker": { + "base": 12774.0, + "reform": 10603.2 + }, + "spouse": { + "base": 4645.200000000001, + "reform": 3850.7999999999997 + } + }, + "reform": 14454.0 + }, + "34": { + "base": 37813.2, + "basis": "projected", + "birth_year": 1933, + "components": { + "retired_worker": { + "base": 37813.2, + "reform": 31374.0 + } + }, + "reform": 31374.0 + }, + "35": { + "base": 17576.4, + "basis": "projected", + "birth_year": 1934, + "components": { + "retired_worker": { + "base": 12944.400000000001, + "reform": 10737.599999999999 + }, + "spouse": { + "base": 4632.0, + "reform": 3847.2000000000003 + } + }, + "reform": 14584.800000000001 + }, + "36": { + "base": 38118.0, + "basis": "projected", + "birth_year": 1934, + "components": { + "retired_worker": { + "base": 38118.0, + "reform": 31632.0 + } + }, + "reform": 31632.0 + }, + "37": { + "base": 17744.399999999998, + "basis": "projected", + "birth_year": 1935, + "components": { + "retired_worker": { + "base": 13213.199999999999, + "reform": 10966.8 + }, + "spouse": { + "base": 4531.200000000001, + "reform": 3758.3999999999996 + } + }, + "reform": 14725.199999999999 + }, + "38": { + "base": 38389.2, + "basis": "projected", + "birth_year": 1935, + "components": { + "retired_worker": { + "base": 38389.2, + "reform": 31858.800000000003 + } + }, + "reform": 31858.800000000003 + }, + "39": { + "base": 17781.600000000002, + "basis": "projected", + "birth_year": 1936, + "components": { + "retired_worker": { + "base": 13030.800000000001, + "reform": 10812.0 + }, + "spouse": { + "base": 4750.799999999999, + "reform": 3945.6000000000004 + } + }, + "reform": 14757.599999999999 + }, + "4": { + "base": 39152.399999999994, + "basis": "projected", + "birth_year": 1918, + "components": { + "retired_worker": { + "base": 39152.399999999994, + "reform": 32488.800000000003 + } + }, + "reform": 32488.800000000003 + }, + "40": { + "base": 38606.399999999994, + "basis": "projected", + "birth_year": 1936, + "components": { + "retired_worker": { + "base": 38606.399999999994, + "reform": 32040.0 + } + }, + "reform": 32040.0 + }, + "41": { + "base": 17851.199999999997, + "basis": "projected", + "birth_year": 1937, + "components": { + "retired_worker": { + "base": 13032.0, + "reform": 10813.2 + }, + "spouse": { + "base": 4819.200000000001, + "reform": 3999.6000000000004 + } + }, + "reform": 14812.800000000001 + }, + "42": { + "base": 38790.0, + "basis": "projected", + "birth_year": 1937, + "components": { + "retired_worker": { + "base": 38790.0, + "reform": 32187.600000000002 + } + }, + "reform": 32187.600000000002 + }, + "43": { + "base": 17845.199999999997, + "basis": "projected", + "birth_year": 1938, + "components": { + "retired_worker": { + "base": 12769.199999999999, + "reform": 10598.400000000001 + }, + "spouse": { + "base": 5076.0, + "reform": 4213.200000000001 + } + }, + "reform": 14811.600000000002 + }, + "44": { + "base": 38942.399999999994, + "basis": "projected", + "birth_year": 1938, + "components": { + "retired_worker": { + "base": 38942.399999999994, + "reform": 32319.600000000002 + } + }, + "reform": 32319.600000000002 + }, + "45": { + "base": 18085.2, + "basis": "projected", + "birth_year": 1939, + "components": { + "retired_worker": { + "base": 12976.800000000001, + "reform": 10768.8 + }, + "spouse": { + "base": 5108.4, + "reform": 4239.6 + } + }, + "reform": 15008.400000000001 + }, + "46": { + "base": 39442.8, + "basis": "projected", + "birth_year": 1939, + "components": { + "retired_worker": { + "base": 39442.8, + "reform": 32732.399999999998 + } + }, + "reform": 32732.399999999998 + }, + "47": { + "base": 18242.4, + "basis": "projected", + "birth_year": 1940, + "components": { + "retired_worker": { + "base": 12932.400000000001, + "reform": 10731.599999999999 + }, + "spouse": { + "base": 5310.0, + "reform": 4406.4 + } + }, + "reform": 15138.0 + }, + "48": { + "base": 39885.600000000006, + "basis": "projected", + "birth_year": 1940, + "components": { + "retired_worker": { + "base": 39885.600000000006, + "reform": 33097.2 + } + }, + "reform": 33097.2 + }, + "49": { + "base": 18422.4, + "basis": "projected", + "birth_year": 1941, + "components": { + "retired_worker": { + "base": 13060.800000000001, + "reform": 10837.2 + }, + "spouse": { + "base": 5361.6, + "reform": 4450.799999999999 + } + }, + "reform": 15288.0 + }, + "5": { + "base": 17588.4, + "basis": "projected", + "birth_year": 1919, + "components": { + "retired_worker": { + "base": 11530.8, + "reform": 9571.2 + }, + "spouse": { + "base": 6057.6, + "reform": 5025.6 + } + }, + "reform": 14596.800000000001 + }, + "50": { + "base": 40276.8, + "basis": "projected", + "birth_year": 1941, + "components": { + "retired_worker": { + "base": 40276.8, + "reform": 33426.0 + } + }, + "reform": 33426.0 + }, + "51": { + "base": 18520.800000000003, + "basis": "projected", + "birth_year": 1942, + "components": { + "retired_worker": { + "base": 12902.400000000001, + "reform": 10710.0 + }, + "spouse": { + "base": 5618.4, + "reform": 4660.799999999999 + } + }, + "reform": 15370.800000000001 + }, + "52": { + "base": 40638.0, + "basis": "projected", + "birth_year": 1942, + "components": { + "retired_worker": { + "base": 40638.0, + "reform": 33726.0 + } + }, + "reform": 33726.0 + }, + "53": { + "base": 12949.199999999999, + "basis": "projected", + "birth_year": 1965, + "components": { + "retired_worker": { + "base": 3379.2000000000003, + "reform": 3280.7999999999997 + }, + "spouse": { + "base": 9570.0, + "reform": 9292.8 + } + }, + "reform": 12573.599999999999 + }, + "54": { + "base": 27372.0, + "basis": "projected", + "birth_year": 1965, + "components": { + "retired_worker": { + "base": 27372.0, + "reform": 26580.0 + } + }, + "reform": 26580.0 + }, + "55": { + "base": 13039.199999999999, + "basis": "projected", + "birth_year": 1966, + "components": { + "retired_worker": { + "base": 3219.6000000000004, + "reform": 3158.3999999999996 + }, + "spouse": { + "base": 9819.599999999999, + "reform": 9628.8 + } + }, + "reform": 12787.199999999999 + }, + "56": { + "base": 27591.600000000002, + "basis": "projected", + "birth_year": 1966, + "components": { + "retired_worker": { + "base": 27591.600000000002, + "reform": 27057.600000000002 + } + }, + "reform": 27057.600000000002 + }, + "57": { + "base": 13112.400000000001, + "basis": "projected", + "birth_year": 1967, + "components": { + "retired_worker": { + "base": 3092.3999999999996, + "reform": 3062.3999999999996 + }, + "spouse": { + "base": 10020.0, + "reform": 9922.8 + } + }, + "reform": 12985.199999999999 + }, + "58": { + "base": 27769.199999999997, + "basis": "projected", + "birth_year": 1967, + "components": { + "retired_worker": { + "base": 27769.199999999997, + "reform": 27498.0 + } + }, + "reform": 27498.0 + }, + "59": { + "base": 13095.599999999999, + "basis": "projected", + "birth_year": 1968, + "components": { + "retired_worker": { + "base": 3024.0, + "reform": 3024.0 + }, + "spouse": { + "base": 10071.599999999999, + "reform": 10071.599999999999 + } + }, + "reform": 13095.599999999999 + }, + "6": { + "base": 39056.399999999994, + "basis": "projected", + "birth_year": 1919, + "components": { + "retired_worker": { + "base": 39056.399999999994, + "reform": 32410.800000000003 + } + }, + "reform": 32410.800000000003 + }, + "60": { + "base": 27741.600000000002, + "basis": "projected", + "birth_year": 1968, + "components": { + "retired_worker": { + "base": 27741.600000000002, + "reform": 27741.600000000002 + } + }, + "reform": 27741.600000000002 + }, + "61": { + "base": 13213.199999999999, + "basis": "projected", + "birth_year": 1975, + "components": { + "disabled_worker": { + "base": 13213.199999999999, + "reform": 10963.2 + } + }, + "reform": 10963.2 + }, + "62": { + "base": 4516.799999999999, + "basis": "projected", + "birth_year": 1967, + "components": { + "retired_worker": { + "base": 3116.3999999999996, + "reform": 3085.2000000000003 + }, + "spouse": { + "base": 1400.4, + "reform": 697.2 + } + }, + "reform": 3782.4000000000005 + }, + "63": { + "base": 15170.400000000001, + "basis": "projected", + "birth_year": 1960, + "components": { + "retired_worker": { + "base": 15170.400000000001, + "reform": 12590.400000000001 + } + }, + "reform": 12590.400000000001 + }, + "65": { + "base": 40938.0, + "basis": "projected", + "birth_year": 1960, + "components": { + "aged_widow": { + "base": 36267.600000000006, + "reform": 32796.0 + }, + "retired_worker": { + "base": 4670.4, + "reform": 4317.6 + } + }, + "reform": 37113.600000000006 + }, + "66": { + "base": 13574.37690890607, + "basis": "opening_stock", + "birth_year": 1935, + "components": { + "retired_worker": { + "base": 13574.37690890607, + "reform": 11265.695592540982 + } + }, + "reform": 11265.695592540982 + }, + "67": { + "base": 19011.84038662128, + "basis": "opening_stock", + "birth_year": 1975, + "components": { + "disabled_worker": { + "base": 19011.84038662128, + "reform": 15778.374792962226 + } + }, + "reform": 15778.374792962226 + }, + "68": { + "base": 21634.16319856905, + "basis": "opening_stock", + "birth_year": 1940, + "components": { + "spouse": { + "base": 21634.16319856905, + "reform": 17954.70235061218 + } + }, + "reform": 17954.70235061218 + }, + "69": { + "base": 17565.70648297362, + "basis": "opening_stock", + "birth_year": 1942, + "components": { + "aged_widow": { + "base": 17565.70648297362, + "reform": 14578.194154552326 + } + }, + "reform": 14578.194154552326 + }, + "7": { + "base": 17528.4, + "basis": "projected", + "birth_year": 1920, + "components": { + "retired_worker": { + "base": 11574.0, + "reform": 9600.0 + }, + "spouse": { + "base": 5954.4, + "reform": 4944.0 + } + }, + "reform": 14544.0 + }, + "70": { + "base": 15560.40080324886, + "basis": "opening_stock", + "birth_year": 1975, + "components": { + "disabled_widow": { + "base": 15560.40080324886, + "reform": 12913.943669290587 + } + }, + "reform": 12913.943669290587 + }, + "8": { + "base": 38869.2, + "basis": "projected", + "birth_year": 1920, + "components": { + "retired_worker": { + "base": 38869.2, + "reform": 32254.800000000003 + } + }, + "reform": 32254.800000000003 + }, + "9": { + "base": 17515.199999999997, + "basis": "projected", + "birth_year": 1921, + "components": { + "retired_worker": { + "base": 11822.400000000001, + "reform": 9812.400000000001 + }, + "spouse": { + "base": 5692.799999999999, + "reform": 4723.200000000001 + } + }, + "reform": 14535.600000000002 + } + } + }, + "schema_version": "acola-mechanism-l-equivalence-v1", + "seed": 20261024 +} diff --git a/tests/acola_track/mechanism_l_dd222b9.provenance.json b/tests/acola_track/mechanism_l_dd222b9.provenance.json new file mode 100644 index 000000000..076814822 --- /dev/null +++ b/tests/acola_track/mechanism_l_dd222b9.provenance.json @@ -0,0 +1,291 @@ +{ + "current_command": ".venv/bin/python scripts/acola_mechanism_l_equivalence.py . --output .equivalence-current.json", + "export_crosscheck": { + "export_command": [ + "git", + "archive", + "dd222b9af926c773a0f92732e2d08be507793378", + "--", + "src/populace_dynamics/__init__.py", + "src/populace_dynamics/artifacts.py", + "src/populace_dynamics/claiming.py", + "src/populace_dynamics/cohorts/__init__.py", + "src/populace_dynamics/cohorts/psid2010.py", + "src/populace_dynamics/cola_track_a/__init__.py", + "src/populace_dynamics/cola_track_a/adapters.py", + "src/populace_dynamics/cola_track_a/benefits.py", + "src/populace_dynamics/cola_track_a/config.py", + "src/populace_dynamics/cola_track_a/invented.py", + "src/populace_dynamics/cola_track_a/mortality.py", + "src/populace_dynamics/cola_track_a/opening.py", + "src/populace_dynamics/cola_track_a/runner.py", + "src/populace_dynamics/cola_track_a/statutory.py", + "src/populace_dynamics/contract.py", + "src/populace_dynamics/data/__init__.py", + "src/populace_dynamics/data/asec_firm_size.py", + "src/populace_dynamics/data/births.py", + "src/populace_dynamics/data/couple_earnings.py", + "src/populace_dynamics/data/cps_tenure.py", + "src/populace_dynamics/data/deaths.py", + "src/populace_dynamics/data/disability.py", + "src/populace_dynamics/data/family.py", + "src/populace_dynamics/data/household_composition.py", + "src/populace_dynamics/data/marriage.py", + "src/populace_dynamics/data/panels.py", + "src/populace_dynamics/data/psid.py", + "src/populace_dynamics/data/relmap.py", + "src/populace_dynamics/data/social_security_income.py", + "src/populace_dynamics/data/tr2008.py", + "src/populace_dynamics/data/transitions.py", + "src/populace_dynamics/engine/__init__.py", + "src/populace_dynamics/engine/assembly.py", + "src/populace_dynamics/engine/candidates.py", + "src/populace_dynamics/engine/claiming.py", + "src/populace_dynamics/engine/composition.py", + "src/populace_dynamics/engine/di_entitlement.py", + "src/populace_dynamics/engine/di_entitlement_rates.py", + "src/populace_dynamics/engine/disability.py", + "src/populace_dynamics/engine/earnings_domain.py", + "src/populace_dynamics/engine/entrant_schedule.py", + "src/populace_dynamics/engine/forward_earnings.py", + "src/populace_dynamics/engine/loop.py", + "src/populace_dynamics/engine/marital.py", + "src/populace_dynamics/engine/panel_builders.py", + "src/populace_dynamics/engine/refit.py", + "src/populace_dynamics/engine/rng.py", + "src/populace_dynamics/engine/steps.py", + "src/populace_dynamics/engine/support.py", + "src/populace_dynamics/estimates/__init__.py", + "src/populace_dynamics/estimates/career.py", + "src/populace_dynamics/estimates/cola_age_profile.py", + "src/populace_dynamics/estimates/first_report.py", + "src/populace_dynamics/estimates/ledgers.py", + "src/populace_dynamics/estimates/parameters.py", + "src/populace_dynamics/estimates/preparation.py", + "src/populace_dynamics/estimates/publication.py", + "src/populace_dynamics/estimates/runner.py", + "src/populace_dynamics/evaluation.py", + "src/populace_dynamics/firms/__init__.py", + "src/populace_dynamics/firms/banding.py", + "src/populace_dynamics/harness/__init__.py", + "src/populace_dynamics/harness/holdout.py", + "src/populace_dynamics/harness/m6_candidate3_runner.py", + "src/populace_dynamics/harness/m6_cells.py", + "src/populace_dynamics/harness/m6_inputs.py", + "src/populace_dynamics/harness/m6_population.py", + "src/populace_dynamics/harness/m6_preflight.py", + "src/populace_dynamics/harness/m6_projection.py", + "src/populace_dynamics/harness/m6_reporting.py", + "src/populace_dynamics/harness/m6_runner.py", + "src/populace_dynamics/harness/m6_scoring.py", + "src/populace_dynamics/harness/metrics.py", + "src/populace_dynamics/harness/moments.py", + "src/populace_dynamics/harness/panel.py", + "src/populace_dynamics/harness/views.py", + "src/populace_dynamics/models/__init__.py", + "src/populace_dynamics/models/couple_formation_sim_v1.py", + "src/populace_dynamics/models/couple_formation_sim_v2.py", + "src/populace_dynamics/models/disability_hazard_sim.py", + "src/populace_dynamics/models/family_transitions/__init__.py", + "src/populace_dynamics/models/family_transitions/common.py", + "src/populace_dynamics/models/family_transitions/components/__init__.py", + "src/populace_dynamics/models/family_transitions/components/divorce.py", + "src/populace_dynamics/models/family_transitions/components/fertility.py", + "src/populace_dynamics/models/family_transitions/components/first_marriage.py", + "src/populace_dynamics/models/family_transitions/components/first_marriage_support_aware.py", + "src/populace_dynamics/models/family_transitions/components/initial_states.py", + "src/populace_dynamics/models/family_transitions/components/remarriage.py", + "src/populace_dynamics/models/family_transitions/components/spousal_age_gap.py", + "src/populace_dynamics/models/family_transitions/components/widowhood.py", + "src/populace_dynamics/models/family_transitions/fitted.py", + "src/populace_dynamics/models/family_transitions/registry.py", + "src/populace_dynamics/models/family_transitions/simulator.py", + "src/populace_dynamics/models/household_composition/__init__.py", + "src/populace_dynamics/models/household_composition/base_simulator.py", + "src/populace_dynamics/models/household_composition/common.py", + "src/populace_dynamics/models/household_composition/components/__init__.py", + "src/populace_dynamics/models/household_composition/components/child_attribution.py", + "src/populace_dynamics/models/household_composition/components/cohabitation_overlay.py", + "src/populace_dynamics/models/household_composition/components/fertility_core_lift.py", + "src/populace_dynamics/models/household_composition/components/household_size.py", + "src/populace_dynamics/models/household_composition/components/legal_spouse_residual.py", + "src/populace_dynamics/models/household_composition/components/marital_core_adapter.py", + "src/populace_dynamics/models/household_composition/components/multigenerational_occupancy.py", + "src/populace_dynamics/models/household_composition/components/nonfamily_bridge.py", + "src/populace_dynamics/models/household_composition/components/parental_home_exit.py", + "src/populace_dynamics/models/household_composition/components/skip_generation_state.py", + "src/populace_dynamics/models/household_composition/data.py", + "src/populace_dynamics/models/household_composition/fitted.py", + "src/populace_dynamics/models/household_composition/registry.py", + "src/populace_dynamics/models/household_composition/simulator.py", + "src/populace_dynamics/scenario_benefits.py", + "src/populace_dynamics/ss/__init__.py", + "src/populace_dynamics/ss/benefits.py", + "src/populace_dynamics/ss/params.py", + "src/populace_dynamics/ss/statutory_aime.py" + ], + "historical_command": "PYTHONPATH=$PWD/.equiv-dd222b9-export/src .venv/bin/python scripts/acola_mechanism_l_equivalence.py .equiv-dd222b9-export --output tests/acola_track/mechanism_l_dd222b9.json", + "method": "historical_source_export_worktree_creation_denied", + "procedure_limitations": [ + "The requested temporary git worktree could not be created because repository metadata is read-only in this sandbox.", + "The fallback exported only the actual imported legacy source dependency closure with git archive at the registered SHA.", + "This establishes historical-source equivalence to the working source; a build-commit pin cannot be supplied until the workspace changes are committed." + ], + "result": "bit-identical canonical JSON", + "worktree_error": "fatal: could not create leading directories of '.git/worktrees/-equiv-dd222b9': Operation not permitted" + }, + "golden_sha256": "07c59d69544503f87b43e8be5df57f01d773f597aa52b8f4027f40f7f9cc87de", + "historical_command": "PYTHONPATH=$PWD/.equiv-dd222b9/src .venv/bin/python scripts/acola_mechanism_l_equivalence.py .equiv-dd222b9 --output .equiv-worktree-dd222b9.json", + "historical_source_files": { + "src/populace_dynamics/__init__.py": "87ad051ce4d9812cafe636e1524c288a5d24bdb0710fdaee30f178a44ba210be", + "src/populace_dynamics/artifacts.py": "d95dd7a66315ff7780d4bec08b8ac91637f5bf98ec458edbf5afb28aa9492c9c", + "src/populace_dynamics/claiming.py": "8cfe25b746eeb0148270b03fb92ab455c4fa698041a80980477f7b9cb9231149", + "src/populace_dynamics/cohorts/__init__.py": "90d3a4bce424c04ea4b6baa4870abff6794c5d998651786346c6e6af2f0f0265", + "src/populace_dynamics/cohorts/psid2010.py": "bc8981e1a29973d63d32ed1331f70aeab60b7efec7ecaa4fba95dbe0f7cae357", + "src/populace_dynamics/cola_track_a/__init__.py": "a45a6451845ad042f6556043fc7c117ffefe0baad216538838878b407b1176e2", + "src/populace_dynamics/cola_track_a/adapters.py": "9860a44b79e9a4e4d2f2097315ef5d6717d7a6bd3392c807af12132299865167", + "src/populace_dynamics/cola_track_a/benefits.py": "fd346c154dbf7927020f31e8e1564769bae5343e7fd2ef95b1e6fdcaca142df9", + "src/populace_dynamics/cola_track_a/config.py": "8c72a3164fc8b185f51f8f888b1f6678a801cd665929cdd0575fa10fb877dfae", + "src/populace_dynamics/cola_track_a/invented.py": "8dfc5f9b5bb0ab0b7b58dc558f28da8d1bfb01fe2400605f9d539c2e2e86ff62", + "src/populace_dynamics/cola_track_a/mortality.py": "61158d7721bf0fde614ea96b82b9f8c4d282aa3b8185071eec168284376c89d8", + "src/populace_dynamics/cola_track_a/opening.py": "4b62e5034dea888d032a9fd65b57eec865011ab630f5fe56753ba6ece9562bef", + "src/populace_dynamics/cola_track_a/runner.py": "b6b275c1562ae48464ef6a25ebfa599820f275f8c51c34445d424ad5e49c2039", + "src/populace_dynamics/cola_track_a/statutory.py": "7ab9f416a40455be40aff1d1bb6b311535aff6212f71a3954287eb69087c2652", + "src/populace_dynamics/contract.py": "481ff781c51e0e056fa2dc630d291020524a91c62f07cf5e0c243ef6ec325ed4", + "src/populace_dynamics/data/__init__.py": "0e8998bc22416fe40779c3a001ff9f01bb12e02310b718750ff3c607ef11dc8d", + "src/populace_dynamics/data/asec_firm_size.py": "9b69b59630063baf2f20463e4b6876746025d30cb576e0cf8af97651506bca97", + "src/populace_dynamics/data/births.py": "88b29f72c49dc879634a8589705baab06eb157850a539c2c0639e5992d3442e2", + "src/populace_dynamics/data/couple_earnings.py": "a53518bcf0f0c6df5d4f7945917b5ba88016cb896b670a4985a271afb12a56e2", + "src/populace_dynamics/data/cps_tenure.py": "512b6d25db75042c2877d870c7b5169e2b745967cb0fa8a7a075c3485bcee994", + "src/populace_dynamics/data/deaths.py": "b687cea94c61bcd9c526df631b905bb8376df09d1bcbac065679025f2791c1ec", + "src/populace_dynamics/data/disability.py": "06e71bd4411dd6856f9a9406ebdaa83385a085c65fac67000f94a4e2815dc093", + "src/populace_dynamics/data/family.py": "6908023ed654f73ffcaa7892b7be81ad9a714b678f341305478dc430d09491a2", + "src/populace_dynamics/data/household_composition.py": "9aa34c9999335d05a0dfb3cb4901cbd478d3c09b4975c4ed3d1a4a5774823c47", + "src/populace_dynamics/data/marriage.py": "1d445d7d06b8cef966249d979962df1ed028541d427396627131a742a0562984", + "src/populace_dynamics/data/panels.py": "e0efe96b95aee9b0b0584c816784abcfdc506a8d16485de90fb1d80bdddcd79d", + "src/populace_dynamics/data/psid.py": "b0983390f84833f1b1436f60a066a829700f6149193aa712808b87d53867e29f", + "src/populace_dynamics/data/relmap.py": "d6ea2c5785cdb0f770c7f571cc5054ea1aaac81836042068b8398e81c906ccf7", + "src/populace_dynamics/data/social_security_income.py": "c156b73396756aae1c79305f9459896f8b7ac37f4f48d017e68e386aedfbde8f", + "src/populace_dynamics/data/tr2008.py": "c793f1d4840f3e61403b169b52d477810f35f81b033d53a13e90e1305f25bc64", + "src/populace_dynamics/data/transitions.py": "b574b9434845421ff6c33047aacb2b4a565cbf97e7bc2f85ba921ac5de2c5337", + "src/populace_dynamics/engine/__init__.py": "ffa9a75dd7101f0cb34639c717af83202e30eeba2cf78dd2443668a0f6f29bd3", + "src/populace_dynamics/engine/assembly.py": "2c5b603d39bc86b0a716a57859d438c21cfb007a04c051ce5865b8b60d05a9c2", + "src/populace_dynamics/engine/candidates.py": "c8ffa5b861bade166e32c5475a25e225ba7eac67a09280f9e649e902aadb45bd", + "src/populace_dynamics/engine/claiming.py": "e87109243567c81aa4c39bd9bd422496036a2e2ab0c3fb9adc92c5cb365107d3", + "src/populace_dynamics/engine/composition.py": "1086c2fd7564debd9836de1039f92787f9208046814670b7e0f5d5b336f8f6bb", + "src/populace_dynamics/engine/di_entitlement.py": "07c5ba9c3f63148774e0942e42c1fec3939c41974f0bf411471a6acf2e052407", + "src/populace_dynamics/engine/di_entitlement_rates.py": "6000ba7b7039e2359920d62fd7f4c4948d729917a39ef7ad8c28d9f3da3c9ba3", + "src/populace_dynamics/engine/disability.py": "f665238890c4f596eda628bdd084343d31b0caddbe0f81830311d4b9958ed65e", + "src/populace_dynamics/engine/earnings_domain.py": "465744c483d6b7ed556b5ff1d71ed615e060e7411ed083fa21515ff9f36d5179", + "src/populace_dynamics/engine/entrant_schedule.py": "23011162e0aae8bda0bf9d5cf5d04f5286c1a4b90928993b12f3fa113e2bc6e4", + "src/populace_dynamics/engine/forward_earnings.py": "094aa5f3aab43470b054d602756091e467e6b4414d818384d0bb329f78f7a6bf", + "src/populace_dynamics/engine/loop.py": "936c11438f466f33d85e6033ae1d63f358a964f61942821ccbb5ec4aa10e639d", + "src/populace_dynamics/engine/marital.py": "82247d03907231f384e11edb939653d5e45b7785987c81c54538fb0b92cdcbf7", + "src/populace_dynamics/engine/panel_builders.py": "347810bb2073eb46379c1e95741af416eb3b46543864861b8c13408f2eefe3bb", + "src/populace_dynamics/engine/refit.py": "9601a0cfe68b783d38dfd82f7058b0d74415005385abc406efa6d0993a10d014", + "src/populace_dynamics/engine/rng.py": "5695c8e1d82bbde4ff3c81011524c453d9626be0aa500c560b462c73d1d3c747", + "src/populace_dynamics/engine/steps.py": "eedbf0d54b00e078c81c973a2b550c65648ea4941b755cfc85c4bc2421e5dfd0", + "src/populace_dynamics/engine/support.py": "1cb0d887a41ce7eafded507d6bf9b6656cf4830709f8f273b92b4bd87b68c46a", + "src/populace_dynamics/estimates/__init__.py": "b71edb4e0139a646dfbf4c0a7c901bd484fe548507b75e19e705be1a889f3d91", + "src/populace_dynamics/estimates/career.py": "19b243f6d712700c5d5cdbf40ce461532c5770fab72f27f39bee410b46d09701", + "src/populace_dynamics/estimates/cola_age_profile.py": "fd07e43f02778e7ac9682c3fed4ae61aca936dfcdb37ffdf1863a37db814278b", + "src/populace_dynamics/estimates/first_report.py": "16fb6b00a4bf706c22b53ffcfa18be7b6be91e2c3383bcc0860060b0a994a8da", + "src/populace_dynamics/estimates/ledgers.py": "7cae0554809535f9e9e7fca464f451196748c2561d4e21165203933e17e140d7", + "src/populace_dynamics/estimates/parameters.py": "daeec7542b53d65bb255b291f9449998fadb684d8833f67dac97b2be8f3d8406", + "src/populace_dynamics/estimates/preparation.py": "57d90bc0ceaaffb809e52c3028283fcabfc32d7f415a45c1b638171b9343f633", + "src/populace_dynamics/estimates/publication.py": "4de4e6969209202d7fbabcd16f207bec01a09eba7e6170824f76a7b6872d34b7", + "src/populace_dynamics/estimates/runner.py": "20d3a79a2e3edd1b392243c75b334b90e8960b3ebf24b660b91df14939773e59", + "src/populace_dynamics/evaluation.py": "575fee998754cfa0f35e86c058f23516eddfa7dcf5b9c3eafefac5a2c81f598e", + "src/populace_dynamics/firms/__init__.py": "514bab3711ed6c8d04907e90ceb993bdeef2b38c3ba4c13131d3f87f98c8bf7b", + "src/populace_dynamics/firms/banding.py": "1a58507f339edc48d16fac582c7e10d048cdb6288317315a6b8ce7555be8c833", + "src/populace_dynamics/harness/__init__.py": "bab6e9f39b5d688cbee8b813f072fcd617c9e1b3aff2a20903daa7ea6db7aa91", + "src/populace_dynamics/harness/holdout.py": "2371f1f48f9e1d59dfa86e48b7457e61ad66be73c1ec5e4a13f11e6aca15b264", + "src/populace_dynamics/harness/m6_candidate3_runner.py": "8b5fbef77374780a8493817ef4603c5fc849bbad6d02b166acfdc6a3f4221b1c", + "src/populace_dynamics/harness/m6_cells.py": "faff61537aacccdf9b9ec335167e7d5f4178bcb2a73e0d6639141dea2f4af282", + "src/populace_dynamics/harness/m6_inputs.py": "b7cf775ce9e418378c6dd09174c2b3a1101fb79089392011c3ddbf8a7e8171b9", + "src/populace_dynamics/harness/m6_population.py": "c5bef28afde23c27737b8d1855e4647d439b62065c52b3b08dac1b7daf878eef", + "src/populace_dynamics/harness/m6_preflight.py": "1bd8057be07e356d4b2b3d0ac5601567d3c11ecbbd74292611f6cbc891e0a001", + "src/populace_dynamics/harness/m6_projection.py": "49ff7ccc5e7e9d59e7b7e324e08e81c23e7ee433ca7b14fc5c9e71013658e569", + "src/populace_dynamics/harness/m6_reporting.py": "b00f77aa734143d21b30e047f75c21bff91c26b68af35fc7863f23cea6e087d9", + "src/populace_dynamics/harness/m6_runner.py": "01cd9d7fa3c28a296f3cfef180f1567a6b5908d02878bbd979fe2bc55a3ddb86", + "src/populace_dynamics/harness/m6_scoring.py": "a3f4f2a5c8a2938bde5fb0c924667c85157cc0bd9da1e8057b3389a71bf17a7a", + "src/populace_dynamics/harness/metrics.py": "5a46ce77339717d9665fe5542452a8c2c89c75c66f6fd7437cf55440290677bb", + "src/populace_dynamics/harness/moments.py": "66344f4e4778e1f9e12c63997a8999435eb9559e4582095668a34723b07a7d2a", + "src/populace_dynamics/harness/panel.py": "fa1833a183a4578100c82e318d0a5c1348a30202c9711a576cedcc306b4c7610", + "src/populace_dynamics/harness/views.py": "01b75eac55dfa146e3d9eff646742e950564100c6210364096cfcb430219bccb", + "src/populace_dynamics/models/__init__.py": "14d311b15eef2dd652900e1a1427fea1a7c01634420c9fdb2efa7484fd9e354a", + "src/populace_dynamics/models/couple_formation_sim_v1.py": "5e2f266cb56f02e64fe9d0b12393e97480944c91f03eabb82020bccdee1b9b11", + "src/populace_dynamics/models/couple_formation_sim_v2.py": "74dd8dcd362fbb7e6ccea434e83a758c48eb1e7cf123b770c88e36d4d8784e58", + "src/populace_dynamics/models/disability_hazard_sim.py": "49fb0b0761b76c9f742455d3b912a38be603e9b0abd748bcd4a647c5bb4a92a6", + "src/populace_dynamics/models/family_transitions/__init__.py": "da5aa098fb8b317f1c3a9553251578f4d5b47b47952e8488146592acb004a9d0", + "src/populace_dynamics/models/family_transitions/common.py": "059da70e80538b2adf5105685a50ecb0be2a717677ec95bb68b277f64e337f95", + "src/populace_dynamics/models/family_transitions/components/__init__.py": "a956fd1cce980167cebb2905ed4fa3e4ed7cfb5d735d96db4e59bc232bd4f76f", + "src/populace_dynamics/models/family_transitions/components/divorce.py": "36e42a899ca6564722d01a235af63d8c9083f8ded754cf4a3fadf46e695417f4", + "src/populace_dynamics/models/family_transitions/components/fertility.py": "04d68d9f19edddb932f42267be98a75e8f080c985480974d0a14a6d9640cb7e8", + "src/populace_dynamics/models/family_transitions/components/first_marriage.py": "3b90492eb32e779061afb4d771e40d1d0990c0fa4ddfc912bd6a61187efeabb1", + "src/populace_dynamics/models/family_transitions/components/first_marriage_support_aware.py": "5bff8cc9fc2d9a57686a2adb99e11f2683017605ba21db9e61d21bd42b137b2a", + "src/populace_dynamics/models/family_transitions/components/initial_states.py": "6cbaf9f968e6c5f99701ebe1bf7f44e5092afecae3211f101cb39e07ee001f53", + "src/populace_dynamics/models/family_transitions/components/remarriage.py": "704b1f5f552ae06e861e6f3f950a85be7d61fb8d6e93cdf09f61467d641f2da7", + "src/populace_dynamics/models/family_transitions/components/spousal_age_gap.py": "c5f935300e497f66343ca3e6e8475cdf7b7d16498ac5b98d86403a55a861327c", + "src/populace_dynamics/models/family_transitions/components/widowhood.py": "3986df5300bb9b1a2cf0502552f3260ab6156c2431d2563c6061be12cc8f0a99", + "src/populace_dynamics/models/family_transitions/fitted.py": "0404bda0d0b4b723688acd38f11676b8e1f0f3a7e350501197150b89aa75d8bc", + "src/populace_dynamics/models/family_transitions/registry.py": "fb3d438192479cfc115109e861450fc75377e5bafaeb4c672a998438f008fd6b", + "src/populace_dynamics/models/family_transitions/simulator.py": "519c583adc82f96af3c37be7cdad0d6a4a34f564e51af2360d9c354759ee58fa", + "src/populace_dynamics/models/household_composition/__init__.py": "884c54ffbf919b4f4adfcd48fb708fd462e8a45229e6a7bf3429a6e86c92a2af", + "src/populace_dynamics/models/household_composition/base_simulator.py": "a54ca4c1195a42d4e7c2a60cd9dea5490dc2fd06705badc1d65034cf87e2a80e", + "src/populace_dynamics/models/household_composition/common.py": "72f2bfdadb728709ba846581ecf6933f7e9b5e41f07bf980e879d0bdfe0cc85b", + "src/populace_dynamics/models/household_composition/components/__init__.py": "4b70557ff80aa912e50c615ae9d88c9efc496933fb78b059a4d0477a3567961b", + "src/populace_dynamics/models/household_composition/components/child_attribution.py": "2b986e550b2c30970061ec0bb867926c82ff6f3f893c1107c44dd730c1b7cc0b", + "src/populace_dynamics/models/household_composition/components/cohabitation_overlay.py": "29a4e31124dd82481031f7cd616cf5d3698822e41d1168dce9d8da974a03d4b1", + "src/populace_dynamics/models/household_composition/components/fertility_core_lift.py": "329c6a5df82c9d478229e7f6685327af36ebab72af4f35deb89bda39a5266539", + "src/populace_dynamics/models/household_composition/components/household_size.py": "989f5e7f273d8c01ccfc3528c26388cb272c6bc8290169ec526a701991996eaa", + "src/populace_dynamics/models/household_composition/components/legal_spouse_residual.py": "e2c69b5a0cc4889fdb13f7e7b19d180c6fbb3893c693d2acf2770866b9aeb4ef", + "src/populace_dynamics/models/household_composition/components/marital_core_adapter.py": "f37f165ecf4e533b6f9eb0abb38a05b5fc81e9922cd5fb189f59f7d6e9964c52", + "src/populace_dynamics/models/household_composition/components/multigenerational_occupancy.py": "a4df92a6a3e2f6215ed9295dda3ff244294422408845f87ef9780be6db351837", + "src/populace_dynamics/models/household_composition/components/nonfamily_bridge.py": "6b99c421d92213dba02d4725bcf1d7cd318bab5314da66f614a5f4f2edc9fc83", + "src/populace_dynamics/models/household_composition/components/parental_home_exit.py": "64d5cd87a89e2c8ec04ba5338f4ed7f3cbb1072d0062ed5178d03411b84cb2e8", + "src/populace_dynamics/models/household_composition/components/skip_generation_state.py": "e8580534e1508afe088cc8b6e479daad95800eb6f80beecfdde474559af53339", + "src/populace_dynamics/models/household_composition/data.py": "079c16e5edff9eb19f4fad61b48515a96f44564b458d7ce25ee5dfecf644e8fa", + "src/populace_dynamics/models/household_composition/fitted.py": "a1cbb579fd5bdd28bdac2175727ded889508887d761dde04cd415f6fd01045b4", + "src/populace_dynamics/models/household_composition/registry.py": "e73e6dc03aee2df8256070078cdb4af2b9aa0e2209e0f5e87c898cc2d14e9467", + "src/populace_dynamics/models/household_composition/simulator.py": "cbea6bb582698dbe1f18991ac86795fedfaa1e39c42dac89bca8780155277a12", + "src/populace_dynamics/scenario_benefits.py": "dffe28fdac66eb849f387ed3a481b6aee85d470946951a755196761c70cc4cbf", + "src/populace_dynamics/ss/__init__.py": "9014fcf3dc4c7d25025cf96bf970c6226f697fc1f8ad8cb37011fa2d1962447e", + "src/populace_dynamics/ss/benefits.py": "2e5e28996624651144b8d8a640caa17a95f0e16769260999b21f214e285b0846", + "src/populace_dynamics/ss/params.py": "9b49894b56eaab32daa62032a139ecb33a1d11f657b1523d3bebadebc2387efd", + "src/populace_dynamics/ss/statutory_aime.py": "2566c6ff7baae9a5f8a181d9c17f37cff96f6871cc79334fb0ccaee3a2d11c63" + }, + "historical_worktree_created": true, + "method": "git_worktree_at_registered_commit", + "persons_per_row": { + "C0": 69, + "C1": 69, + "M0": 69, + "M1": 69, + "M2": 69, + "M3": 69, + "M4": 69 + }, + "registered_commit": "dd222b9af926c773a0f92732e2d08be507793378", + "result": "bit-identical canonical JSON", + "rows": [ + "C0", + "C1", + "M0", + "M1", + "M2", + "M3", + "M4" + ], + "script_sha256": "5d7eef14e1cd4556e7a6d4d28f3b43f3cb4c442a68ffa6569724b5a831ea7a27", + "worktree_command": "git worktree add --detach .equiv-dd222b9 dd222b9af926c773a0f92732e2d08be507793378", + "worktree_run": { + "date": "2026-10-24", + "head_source_commit": "b1b2561fb1c3fd568b28311a86bdfede0dca8a61", + "head_source_note": "Stage 2's commits add no file under src/, so the HEAD-side run used this commit's src/ tree.", + "identical_to_golden": true, + "imports_resolved_to": ".equiv-dd222b9/src (checked for cola_track_a/benefits.py and claiming.py)", + "output_sha256": "07c59d69544503f87b43e8be5df57f01d773f597aa52b8f4027f40f7f9cc87de", + "run_by": "the lane session, after the builder's sandbox denied the worktree" + } +} diff --git a/tests/acola_track/test_mechanism_l_equivalence.py b/tests/acola_track/test_mechanism_l_equivalence.py new file mode 100644 index 000000000..8e773d9e7 --- /dev/null +++ b/tests/acola_track/test_mechanism_l_equivalence.py @@ -0,0 +1,112 @@ +"""Registration 18's seeded invented mechanism-L regression evidence.""" + +from __future__ import annotations + +import hashlib +import importlib.util +import json +import subprocess +import sys +from functools import lru_cache +from pathlib import Path + +from populace_dynamics.acola_track.config import REGISTERED_ROWS + +ROOT = Path(__file__).resolve().parents[2] +SCRIPT = ROOT / "scripts/acola_mechanism_l_equivalence.py" +GOLDEN = Path(__file__).with_name("mechanism_l_dd222b9.json") +PROVENANCE = GOLDEN.with_suffix(".provenance.json") + + +@lru_cache(maxsize=1) +def equivalence_script(): + spec = importlib.util.spec_from_file_location("_acola_equivalence", SCRIPT) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_mechanism_l_is_bit_identical_to_registration_commit(tmp_path): + output = tmp_path / "head.json" + subprocess.run( + [ + sys.executable, + str(SCRIPT), + str(ROOT), + "--output", + str(output), + ], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ) + actual, expected = output.read_bytes(), GOLDEN.read_bytes() + if actual != expected: + differences = equivalence_script().differing_people( + json.loads(expected), json.loads(actual) + ) + raise AssertionError( + "REG18_MECHANISM_DRIFT: " + + (", ".join(differences) or "canonical JSON metadata/encoding") + ) + + +def test_historical_golden_pins_source_script_and_output(): + provenance = json.loads(PROVENANCE.read_text()) + assert provenance["registered_commit"] == ( + "dd222b9af926c773a0f92732e2d08be507793378" + ) + assert ( + provenance["script_sha256"] + == hashlib.sha256(SCRIPT.read_bytes()).hexdigest() + ) + assert ( + provenance["golden_sha256"] + == hashlib.sha256(GOLDEN.read_bytes()).hexdigest() + ) + assert provenance["historical_command"] + assert provenance["historical_source_files"] + + +def test_historical_interface_rows_match_new_registry(): + for row in equivalence_script().registered_rows(): + assert row == REGISTERED_ROWS[row.row_id].track_row() + + +def test_invented_population_exercises_old_dual_entitlement_and_components(): + value = json.loads(GOLDEN.read_text()) + assert tuple(value["rows"]) == equivalence_script().ROW_IDS + assert value["seed"] == 20261024 + for people in value["rows"].values(): + assert {person["birth_year"] for person in people.values()} >= set( + range(1917, 1943) + ) + components = set() + for person in people.values(): + components.update(person["components"]) + if ( + 1917 <= person["birth_year"] <= 1942 + and person["basis"] == "projected" + ): + assert person["components"]["retired_worker"]["base"] > 0 + assert components == { + "retired_worker", + "disabled_worker", + "spouse", + "aged_widow", + "disabled_widow", + } + dual = [ + p + for p in people.values() + if 1917 <= p["birth_year"] <= 1942 + and "spouse" in p["components"] + and p["basis"] == "projected" + ] + assert {p["birth_year"] for p in dual} == set(range(1917, 1943)) + assert value["rows"]["M0"] == value["rows"]["M4"] + assert value["rows"]["C0"] != value["rows"]["C1"] + assert value["rows"]["M0"] != value["rows"]["M1"] + assert value["rows"]["M0"] != value["rows"]["M2"] + assert value["rows"]["M0"] != value["rows"]["M3"] From 1b4923b3bec1426c646cbc40d5aaee6f6f4b3de0 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 24 Oct 2026 10:51:42 -0400 Subject: [PATCH 4/6] A-COLA stage 2: cross-check the TR 2026 COLA capture Checks that stage 1's Table V.C1 capture and master's data/tr2026 accessor give the same intermediate COLA increases effective December 2027, 2028 and 2029. Only the COLA rows are read from the accessor. Built by a Subfleet build lane (GPT-6.1 Sol, job 20261024-073819-mgf-f0-acola-stage2) under the cleared builder brief and its 2026-10-24 addendum. That lane's sandbox could not write .git, so the lane session committed its working tree in steps. Co-Authored-By: Claude Opus 5.5 --- tests/acola_track/test_tr2026_crosscheck.py | 22 +++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/acola_track/test_tr2026_crosscheck.py diff --git a/tests/acola_track/test_tr2026_crosscheck.py b/tests/acola_track/test_tr2026_crosscheck.py new file mode 100644 index 000000000..98452af14 --- /dev/null +++ b/tests/acola_track/test_tr2026_crosscheck.py @@ -0,0 +1,22 @@ +"""Cross-check only the registered December COLAs through the accessor.""" + +from decimal import Decimal + +from populace_dynamics.acola_track.nulls import tr2026_inputs +from populace_dynamics.data import tr2026 + + +def test_registered_cola_capture_matches_master_accessor(): + captured = tr2026_inputs()["cells"] + entries = tr2026.cola_path(2027, 2029) + assert tuple(entry.determination_year for entry in entries) == ( + 2027, + 2028, + 2029, + ) + for entry in entries: + assert entry.effective_month == "December" + assert entry.source == "tr2026_v_c1_projected" + assert Decimal(str(entry.percent)) == Decimal( + captured[str(entry.determination_year)]["percent"] + ) From e422ad55c0bf95d986090dcc7ed5754ccb1f7a43 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 24 Oct 2026 10:51:42 -0400 Subject: [PATCH 5/6] A-COLA stage 2: delayed-credit disclosure and the design note Invented late claimants born 1917-1942 pin mechanism L's legacy 48-month delayed-credit cap (claiming.benefit_factor, reached from cola_track_a/benefits.py:345). The cap is disclosed before the run; see docs/errata/2026-10-11-delayed-credit-window.md and docs/design/d1408_delayed_credit_diagnostic.md. docs/design/acola_trust_fund_implementation.md maps the implementation and holds the section "Disclosures for the second registration comment". Built by a Subfleet build lane (GPT-6.1 Sol, job 20261024-073819-mgf-f0-acola-stage2) under the cleared builder brief and its 2026-10-24 addendum. That lane's sandbox could not write .git, so the lane session committed its working tree in steps. Co-Authored-By: Claude Opus 5.5 --- .../design/acola_trust_fund_implementation.md | 261 ++++++++++++++++++ .../test_delayed_credit_disclosure.py | 100 +++++++ 2 files changed, 361 insertions(+) create mode 100644 docs/design/acola_trust_fund_implementation.md create mode 100644 tests/acola_track/test_delayed_credit_disclosure.py diff --git a/docs/design/acola_trust_fund_implementation.md b/docs/design/acola_trust_fund_implementation.md new file mode 100644 index 000000000..4006ba087 --- /dev/null +++ b/docs/design/acola_trust_fund_implementation.md @@ -0,0 +1,261 @@ +# Registration 18: A-COLA implementation + +This is the microsimulation build for decision d658 and milestone F0 of +the mortality, groups and financing design (#558, approved revision +`2c19f5b6`, decision d1922). Registration 18 is the specification: +. +`acola_registration18.md` preserves its posted bytes, SHA-256 +`02287d54f12285c8dffcb94ba7dae9cbe3cc34ba338c602a5ad4afe14d598f20`. +The binding check records d658's authorization rather than applying A7's +ratification-wording test. This build authorizes no population run. + +The local build instructions are the cleared +`EV/acola-builder-brief-cleared-20260930.md` and +`EV/acola-build-20260930/stage2-brief-20261024.md`. EV denotes +`/Users/maxghenis/microcosm-launch-evidence/dynasim-parity-20260909`. +PSID use follows the project's data-access record. Tests use invented data. + +## Implementation map + +All package paths below are under `src/populace_dynamics/acola_track/`; +test paths are under `tests/acola_track/`. + +| Registration element | Implementation | Tests | +| --- | --- | --- | +| Independent registry, R0 population, five components, labels, report-only status | `config.py`: `ACOLARow`, `REGISTERED_ROWS`, `ACOLAConfig` | `test_config.py`, `test_unchanged.py` | +| C0: 2027, eligibility clock, calendar payments, L | `config.py`, `runner.py`: `run_acola` | `test_config.py`, `test_timing_properties.py`, `test_runner.py` | +| C1: December 2030 monthly amount times 12 | `config.py`: C1 | `test_timing_properties.py` | +| M0: 2011; M1: 2010; M2: December; M3: entitlement; M4: completed start-year age | `config.py`: M0–M4 | `test_config.py`, `test_timing_properties.py`, `test_runner.py` | +| C0-D: mechanism D, deferred | `config.py`: `DEFERRED_ROWS`; `runner.py` artifact | `test_config.py`, `test_runner.py` | +| Twenty draws, seeds 5200+k, one shared projected state per draw | `runner.py`: unchanged Track A projection helpers and `reference_benefit_rows` | `test_runner.py` | +| All-ages T_base/T_reform and P; component totals; OASI/DI shares and P values; memberships | `statistics.py`: `summarize_cost`, `_cost_cell` | `test_statistics.py` | +| Spouse-on-DI weighted counts and annual dollars, both scenarios and three classes | `runner.py`: `spouse_worker_class`; `statistics.py`: `_cost_cell` | `test_runner.py`, `test_statistics.py` | +| MINT Total 62+, 62–69, 70–79, 80–89, 90+; all three estimands and three memo statistics | `statistics.py`: `summarize_mint`, `_mint_cell`, `weighted_median` | `test_statistics.py` | +| Draw means, sample SD (ddof=1), five family-half floors | `statistics.py`: `_summary`, `_summarize` | `test_statistics.py` | +| N, N_model, N_MINT and M1 null, provenance and model-null gaps | `nulls.py`, `runner.py`: `_add_null_gaps`; `scripts/build_acola_spec_only_nulls.py`; `acola_spec_only_nulls.json` | `test_nulls.py`, `test_runner.py` | +| Unscaled 2030 beneficiary-count context | `nulls.py`: `extract_context_count`; `runner.py` | `test_nulls.py`, `test_runner.py` | +| REFUSE, no-drop refusals and null serialization | `runner.py`: `_check_floor`, refusal records; `statistics.py`: `_refused_cell` | `test_runner.py`, `test_statistics.py` | +| Exact pointer, SHA, HEAD, clean tree, specification and one-shot output guards; artifact and environment hash sidecar | `scripts/run_acola_registered.py`: `preflight`, `write_artifact` | `test_registered_run_script.py`, `test_runner.py` | +| OACT/PWBM conversion, precision branches and four-corner intervals; MINT S/L readings, headline choice, gaps and metadata; seal and sidecar verification; JSON and Markdown | `scripts/compare_acola.py` | `test_compare.py` | +| Historical mechanism-L equivalence and delayed-credit disclosure | `scripts/acola_mechanism_l_equivalence.py`, historical golden and provenance | `test_mechanism_l_equivalence.py`, `test_delayed_credit_disclosure.py` | +| Independent TR 2026 COLA cross-check | `nulls.py`: capture extractor; `data.tr2026.cola_path(2027, 2029)` | `test_tr2026_crosscheck.py` | + +The runner composes `TrackAConfig(rows=("R0",))` to select the population +and projection, and constructs public `RegisteredRow` instances outside +the legacy registry. It never calls `run_track_a` with A-COLA row IDs. +`cola_track_a`, `scenario_benefits.py`, the A7 tabulator, Track A v2, +FRA 68, the oracle, claiming and the shared engine remain unchanged. +New source files are enumerated individually in the birth-evidence +post-review exclusions and its exact-tuple and reachability tests. + +## Mechanism preservation evidence + +The requested `dd222b9..origin/master` check at master `2195258c` +found only two added files among the mechanism paths: +`engine/forward_earnings_s1_v1.py` (297 lines) and +`ss/statutory_delayed_credit.py` (125 lines). No existing file changed. +The A-COLA build edits none of those paths. + +`scripts/acola_mechanism_l_equivalence.py` uses only legacy interfaces +present at both revisions. Its seed 20261024 supplies fixed invented +careers and projected states, including every birth cohort 1917–1942 +claiming after FRA, dually entitled spouses, DI, conversion, survivors, +opening components and recent eligibility boundaries. All seven runnable +rows have 69 beneficiaries, giving 483 row/person records with baseline, +reform and component amounts. Their canonical JSON is bit-identical +between the registered source and current working source. The golden +`tests/acola_track/mechanism_l_dd222b9.json` has SHA-256 +`07c59d69544503f87b43e8be5df57f01d773f597aa52b8f4027f40f7f9cc87de`. +Its adjacent provenance file pins the script, all imported historical +source files and exact commands. Any future difference raises +`REG18_MECHANISM_DRIFT` with row/person identifiers. + +The build environment denied writes to `.git`, including creation of the +required historical worktree. The historical execution therefore used a +clearly labelled source export of 117 actually imported source files at +`dd222b9af926c773a0f92732e2d08be507793378`, excluding all data, runs and +Track B source. The export was removed after comparison. This establishes +working-source equality; the required worktree procedure and a comparison +at the final committed build SHA remain release prerequisites. The golden +was not regenerated from the current source to resolve a difference. + +## Arithmetic and invariants + +P follows v2's operation order, `100 * (T_reform / T_base - 1)`. +Component accounting and the OASI/DI decomposition are checked per draw: +component totals sum to T_s; T_base equals its OASI and DI parts; and P +equals the baseline-share-weighted P_OASI/P_DI where both denominators +are positive. A zero component denominator makes that component's P +undefined, rather than zero. The decomposition is not asserted for +unweighted averages of draw-specific shares. Differential tests compare +P with v2's all-alive weighted-total cell, means with A7, and floors with +A7's convention. Equal scenario memberships imply identity of P and the +ratio of means, subject to floating arithmetic tolerance. + +The weighted median uses the midpoint at exactly half the weight, with +exact `Fraction` representations of supplied binary-float weights. +Property tests cover permutation invariance, ordinary medians with equal +weights, range bounds, exact and near-half ties, and zero-weight handling. +Floor splits use the whole emitted normalized row table, with each opening +family assigned by the unchanged Bernoulli(0.5) splitter for seeds 0–4. +Every statistic and age band of a row therefore uses the same split. +Each side computes the mean across draws before its absolute gap is +taken. Fewer than two usable seeds gives a null floor and reason. + +Zero reform makes all change statistics, falling shares and nulls zero; +benefit-level medians remain positive and equal. Seed determinism, +permutation invariance, shared draw states and the exact legacy source +pins have tests. Empty bands and refused draws serialize with +`allow_nan=False`, null values and reasons; valid draw results remain +visible when another draw refuses. No missing configured projection draw +can silently become an empty completed draw. + +For an ordinary worker, dime flooring at each increase and after the +claim factor requires a benefit-level-dependent allowance for accumulated +rounding losses. Tests derive the allowance from monthly dime losses, +the number of increases and the actual baseline benefit. Common AIME +dollar rounding does not create a scenario difference. Intact opening +benefits are not dime-floored by default. Dual entitlement creates a +separate structural exception: a cut to the claimant's own full PIA can +increase the spouse excess by more than the loss in the age-reduced own +benefit. Thus individual benefits and P can rise under a cut; intended +bounds and monotonicity have an explicit tested exception, without a +universal rounding tolerance. The registered arithmetic is preserved. + +The first-year tests exercise the actual legacy emitter with determination +years 2010, 2011 and 2027, both payment periods, and eligibility in 2008, +2010, 2011 and 2027–2030. In particular C0 carries 3, 2, 1 and 0 reduced +increases for eligibility in 2027, 2028, 2029 and 2030; C1 carries one +additional increase. C1 and M2 read payment year 2031, which is covered by +the baseline's determination-year 2030 rate. Registered model rates are +2.8% in every reduced year, leaving a strictly positive 1.8% reform rate. +The floor guard is stricter than REFUSE (strict positivity rather than +nonnegativity), and REFUSE cannot intervene for the registered rate path. + +Comparison arithmetic uses `Decimal` for printed and stored decimal +strings. Four-corner tests check containment, monotonicity and positive +denominators; interval subtraction is `[a,b]-[c,d]=[a-d,b-c]`. Seal files +are checked before comparison, and the artifact is checked against its +sidecar. No acceptance threshold, pass band or verdict is computed. + +## Disclosures for the second registration comment + +The following choices and retained limitations must be disclosed before +the population run, together with the registered commit and exact command. + +1. Scenario means and benefit-level medians use the scenario-specific + positive-benefit sets S_base and S_reform. Individual percent-change + medians use their intersection with positive baseline benefits S_alt. + A benefit present in only one scenario enters only that scenario's + mean or level median; the falling share uses baseline recipients. + Its output unit is a fraction of baseline beneficiary weight. +2. Spouse amounts, including opening-stock amounts, are classified by + the living 2030 worker in `spouse_person_id`: entitled, unconverted DI; + another worker; or unclassified if no living link exists. Counts and + annual dollars are reported in both scenarios. Spouse remains an OASI + component even when its linked worker is on DI (ruling 4). +3. The legacy emitter drops a spouse or widow excess with zero baseline + excess even if its reform excess is positive. This pushes P away from + zero. No public counter exposes dropped cases; the artifact reports + that counting is unavailable without copying private arithmetic or + changing a shared module. The arithmetic is unchanged (ruling 5). +4. Under L the two positive-benefit sets are equal by construction: the + emitter retains positive baseline benefits, refuses nonpositive reform + totals, and sets both flags true. Equal counts are not evidence of + independently equal membership (ruling 6). +5. Weighted medians exclude zero weights and report the exclusions; + comparisons to W/2 use exact fractions of the supplied floats. Sorting + is by value, then person ID. An exact W/2 tie takes the midpoint with + the next positive-weight observation (ruling 7). +6. A7's public tabulator is bypassed. Intermediate mean-benefit values + not registered as statistics are under `not_registered`; no + unregistered mean of individual ratios is published (ruling 8). +7. The binding check pins the verbatim specification's SHA-256 and d658's + authorization; it makes no ratification claim (ruling 9). The entry + script pins the first comment's exact URL. The launcher records the + second comment and enforces waiting for it (ruling 10). +8. C1 and M2 include the December 2030 increase, whereas the registered + nulls stop at determination year 2029. No December-specific null is + computed (ruling 11). +9. Weight variation is reported across draws over everyone alive in at + least one 2030 state. This diagnostic is not limited to emitted + beneficiaries (ruling 12). +10. C0-D remains registered but deferred. It runs after Track A v2's own + real-data artifact is committed to master, at a commit registered by + a further issue #42 comment. If run after publication of the primary + comparison, it is labelled "registered before the comparator was + public, run after it" (ruling 13). +11. Coordination checks list open PRs before work and at close-out. + Only exclusion and tier bookkeeping shared files change; protected + benefit modules are not edited (ruling 14). +12. The annual convention omits family maximums, earnings tests, + recomputation and payment-month timing. The closed population omits + children, later immigrants and post-2010 earnings; disabled widow(er)s + enter only as opening stock. Benefit-only cost, baseline vintage, + timing and auxiliary limitations remain named gaps. No adjustment or + scaling is applied (ruling 15). +13. Undefined bands, floors and refused-draw values are null with reasons. + A refusal preserves valid per-draw results but makes a summary that + requires every draw undefined. Empty completed draws have zero + totals and undefined ratios. JSON forbids NaN (ruling 16). +14. The zero-reform invariant concerns changes and falling shares; + baseline/reform median benefit levels remain equal and positive. + P uses `100*(T_reform/T_base-1)`, matching v2's operation order. + Floors split every emitted row, including beneficiaries under 62. +15. Dual-entitlement spouse arithmetic can raise individual and aggregate + benefits under a cut and violate the intended bounds and monotonicity. + Worker-path bounds instead allow propagated dime losses plus the + final claim-factor dime floor, scaled by that person's benefit level. + No universal tolerance is asserted for dual entitlement. +16. MINT reading S uses the shortest decimal representation of the stored + double, shifted two places for a percent format. Both half-up and + half-even rounding are checked against the HTML precision, and the + matching modes are reported. If either mode matches and the stored + value has more decimals, the XLSX branch applies; otherwise HTML + applies. Literal reading L is a sensitivity only. The sealed record + supplies median type, start-date interpretation, timing and flags; + the build never substitutes constants for them. Unknown fields are + reported as unknown. The v1 schema defines no numeric split extension: + if a record asserts an OASI/DI split, its raw fields are retained with + an explicit schema limitation rather than guessed field mappings. +17. Mechanism L reaches `claiming.benefit_factor` at + `src/populace_dynamics/cola_track_a/benefits.py:345`. Its call to the + sealed oracle retains the legacy 48-month delayed-credit cap for + births 1917–1942, including late-entitled linked worker records. + R0's population can retain these people through 2030. The statutory + window is longer for those cohorts. Registration 18 keeps L and + discloses the cap rather than changing the registered mechanism. + See `docs/errata/2026-10-11-delayed-credit-window.md` and + `docs/design/d1408_delayed_credit_diagnostic.md`, decision d1408. + The new tests use invented people only; no d1408 script is run. +18. N uses the Table V.C1 COLA column, intermediate rows 2027, 2028 and + 2029, each 2.4%, effective December of its determination year. Source: + . + File: `data/external/snapshots/tr2026_v_c1_vintage1/trustees2026_v_c1_table.html`. + SHA-256: `4d29f2b85d67b995eea25f0bdfb46b3755671c082d75a9855680836d4b80c46a`. + Column: `Cost-of-living benefit increasea (percent)`. + The preserved timing footnote says December after 1982. The capture + is the exact table fragment from the chapter, with a separately + preserved timing note; the rest of the chapter is not stored. + The URL was located through the Report's List of Tables. No CPI + fallback is used. `data.tr2026.cola_path(2027,2029)` is checked against + these three captured COLAs without printing any other accessor value. +19. N_model uses the model's TR 2008 ultimate annual-average CPI, 2.8% + in 2027–2029. N_MINT uses TR 2008 V.C1 through 2017 and ultimate CPI + thereafter, 2.8% in every 2011–2029 year; M1 adds 2010 at 2.8%. + N minus N_model therefore compares printed TR 2026 COLAs against + the model's derived TR 2008 ultimate-CPI continuation. + +The pre-run nulls in percent units are N = -2.9011704027652740478515625, +N_model = -2.8899919721197439019598290221155817992219312710473, +N_MINT = -16.950197597259551155011310281830073286124621211173 and +M1 = -17.758075052539127505643495979477640666609790265539. +Their exact rational values, factor counts (3, 3, 19, 20), input rates, +source files and hashes are in `acola_spec_only_nulls.json`. + +## Review and release + +The build is WIP until independent review and the coordinator's tier-count +slot. Do not merge. The coordinator lands this after independent review +and the tier-count slot. The run artifact later receives its own review +and CI cycle before any comparator seal is opened. diff --git a/tests/acola_track/test_delayed_credit_disclosure.py b/tests/acola_track/test_delayed_credit_disclosure.py new file mode 100644 index 000000000..4488d2937 --- /dev/null +++ b/tests/acola_track/test_delayed_credit_disclosure.py @@ -0,0 +1,100 @@ +"""Invented late claimants pin the legacy cap disclosed before the run.""" + +from collections import Counter +from dataclasses import fields, replace + +import pytest + +from populace_dynamics import claiming +from populace_dynamics.cola_track_a.benefits import ( + BenefitContext, + StateLookups, + _Calculator, + reference_benefit_rows, +) +from populace_dynamics.cola_track_a.config import TrackAConfig +from populace_dynamics.estimates.parameters import COLASeries +from populace_dynamics.ss.params import SSAParameters +from tests.acola_track.test_mechanism_l_equivalence import ( + equivalence_script, +) + + +@pytest.fixture(scope="module") +def disclosure_case(): + script = equivalence_script() + params = script.invented_parameters() + cohort, result = script.invented_population() + context = BenefitContext( + cohort, + params, + COLASeries({y: 0.028 for y in range(1979, 2031)}, {"invented": True}), + TrackAConfig(rows=("R0",), draw_indices=(0,)), + ) + row = script.registered_rows()[0] + lookups = StateLookups(result, 2030) + longer_context = replace( + context, params=replace(params, max_delayed_months=60) + ) + base_rows, _ = reference_benefit_rows( + result, draw=0, row=row, context=context + ) + longer_rows, _ = reference_benefit_rows( + result, draw=0, row=row, context=longer_context + ) + return ( + context, + longer_context, + row, + lookups, + {p["person_id"]: p for p in base_rows}, + {p["person_id"]: p for p in longer_rows}, + ) + + +@pytest.mark.parametrize("birth", range(1917, 1943)) +def test_mechanism_l_uses_legacy_48_month_cap_for_old_late_claimants( + birth, disclosure_case +): + context, longer_context, row, lookups, base_rows, longer_rows = ( + disclosure_case + ) + params = context.params + calculator = _Calculator(context, row, lookups, Counter(), {}) + pid = 2 * (birth - 1917) + 1 + state = lookups.final.loc[pid] + assert state["claim_year"] - birth == 70 + assert 840 - params.fra_months(birth) > 48 + legacy_factor = 1 + 4 * params.delayed_credit_annual_rate(birth) + record = calculator.worker_record(pid, state) + assert record.claim_age_factor == legacy_factor + assert record.claim_age_factor == claiming.benefit_factor( + 840, birth, params + ) + assert record.claim_age_factor == claiming.benefit_factor( + params.fra_months(birth) + 48, birth, params + ) + + # Change only this invented parameter bundle to expose the lost + # months, without replacing any function used by mechanism L. + longer_window = longer_context.params + assert claiming.benefit_factor(840, birth, longer_window) > legacy_factor + uncapped_calculator = _Calculator( + longer_context, + row, + lookups, + Counter(), + {}, + ) + assert ( + uncapped_calculator.worker_record(pid, state).claim_age_factor + > record.claim_age_factor + ) + base, longer = base_rows[pid], longer_rows[pid] + for scenario in ("base", "reform"): + assert ( + base["benefit_components"]["retired_worker"][scenario] + < longer["benefit_components"]["retired_worker"][scenario] + ) + defaults = {field.name: field.default for field in fields(SSAParameters)} + assert defaults["max_delayed_months"] == params.max_delayed_months == 48 From f78efbc652e57d0715865ef96c18f6a7c1edc87b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 24 Oct 2026 10:51:42 -0400 Subject: [PATCH 6/6] A-COLA: exclusion tuple and tier counts Adds stage 1's five acola_track sources to POST_REVIEW_SOURCE_EXCLUSIONS and its exact-tuple test, keeping master's existing entries. Recounts the tiers with recount-tiers.py and the empty PSID root: unit 18,088 -> 18,351, artifact 4,253 -> 4,278, total 24,619 -> 24,907. The tier-manifest check passes on full collection. Built by a Subfleet build lane (GPT-6.1 Sol, job 20261024-073819-mgf-f0-acola-stage2) under the cleared builder brief and its 2026-10-24 addendum. That lane's sandbox could not write .git, so the lane session committed its working tree in steps. Co-Authored-By: Claude Opus 5.5 --- scripts/first_estimates_birth_evidence.py | 6 ++++++ tests/README-tiers.md | 6 +++--- tests/estimates/test_birth_evidence_artifact.py | 14 ++++++++++++++ tests/tier_counts.json | 4 ++-- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/scripts/first_estimates_birth_evidence.py b/scripts/first_estimates_birth_evidence.py index 83a0ca1fb..abbbcb2fe 100644 --- a/scripts/first_estimates_birth_evidence.py +++ b/scripts/first_estimates_birth_evidence.py @@ -409,6 +409,12 @@ Path("src/populace_dynamics/group_breakdowns/fra68.py"), Path("src/populace_dynamics/group_breakdowns/uniform_cut.py"), Path("src/populace_dynamics/group_breakdowns/min_benefit.py"), + # Registration 18's opt-in COLA layer is outside the historical reducer. + Path("src/populace_dynamics/acola_track/__init__.py"), + Path("src/populace_dynamics/acola_track/config.py"), + Path("src/populace_dynamics/acola_track/nulls.py"), + Path("src/populace_dynamics/acola_track/runner.py"), + Path("src/populace_dynamics/acola_track/statistics.py"), ) POST_REVIEW_SHARED_SOURCE_BLOBS = { Path( diff --git a/tests/README-tiers.md b/tests/README-tiers.md index e2715c124..38cf65c2a 100644 --- a/tests/README-tiers.md +++ b/tests/README-tiers.md @@ -39,9 +39,9 @@ pytest --collect-only -q -m oracle_policyengine | tail -1 | Tier | Tests at HEAD | |---|---:| -| `unit` | 18,088 | -| `artifact` | 4,253 | +| `unit` | 18,351 | +| `artifact` | 4,278 | | `integration_psid` | 1,450 | | `reproduction_legacy` | 520 | | `oracle_policyengine` | 308 | -| **Total** | **24,619** | +| **Total** | **24,907** | diff --git a/tests/estimates/test_birth_evidence_artifact.py b/tests/estimates/test_birth_evidence_artifact.py index 3a0a2086d..082d38a6e 100644 --- a/tests/estimates/test_birth_evidence_artifact.py +++ b/tests/estimates/test_birth_evidence_artifact.py @@ -253,6 +253,11 @@ def test_post_review_sources_are_outside_historical_reducer_identity(): Path("src/populace_dynamics/group_breakdowns/fra68.py"), Path("src/populace_dynamics/group_breakdowns/uniform_cut.py"), Path("src/populace_dynamics/group_breakdowns/min_benefit.py"), + Path("src/populace_dynamics/acola_track/__init__.py"), + Path("src/populace_dynamics/acola_track/config.py"), + Path("src/populace_dynamics/acola_track/nulls.py"), + Path("src/populace_dynamics/acola_track/runner.py"), + Path("src/populace_dynamics/acola_track/statistics.py"), ) assert reducer.POST_REVIEW_SHARED_SOURCE_BLOBS == { Path( @@ -623,6 +628,15 @@ def test_post_review_exclusions_are_unreachable_from_birth_evidence(): "the opt-in Track A assembly became reachable from the " f"birth-evidence reducer: {sorted(track_a_modules & reachable)}" ) + acola_modules = {"populace_dynamics.acola_track"} | { + f"populace_dynamics.acola_track.{name}" + for name in ("config", "nulls", "runner", "statistics") + } + assert acola_modules.issubset(module_paths) + assert acola_modules.isdisjoint(reachable), ( + "the opt-in Registration 18 assembly became reachable from the " + f"birth-evidence reducer: {sorted(acola_modules & reachable)}" + ) fra68_modules = {"populace_dynamics.fra68_track"} | { f"populace_dynamics.fra68_track.{name}" for name in ("benefits", "config", "reform", "runner") diff --git a/tests/tier_counts.json b/tests/tier_counts.json index 0303dc6ab..5860fe5a4 100644 --- a/tests/tier_counts.json +++ b/tests/tier_counts.json @@ -1,8 +1,8 @@ { "schema_version": 1, "counts": { - "unit": 18088, - "artifact": 4253, + "unit": 18351, + "artifact": 4278, "integration_psid": 1450, "reproduction_legacy": 520, "oracle_policyengine": 308