Skip to content

UK student-loans realization gate refuses every spine since #1006: the PLAN_5 top-up shortfall is below one person's weight #1049

Description

@juaristi22

Observed. Since #1006 merged (2026-09-25), every licensed spine built at main's head blocks at the transferred phase on uk_stage_student_loans_realization, worst absolute realization deviation 1.1021725784754537 against the declared maximum 1.0. Seen on the #901 rebase A/B at 8f628b1, on today's spine at 937aca4, and on the #1012 lane's control, reorder, housing and final spines (built under a local relaxation of the threshold to 2.0). Spine-u on 2026-09-23, before #1006, passed with 0.297. The value is identical across the #1012 lane's spines, so the SPI-first reorder did not move it.

What the gate measures. assign_student_loan_plans (uk_runtime/student_loans.py) assigns reported plans from repayments and course start year, then tops up PLAN_5 and PLAN_2 towards the pinned SLC liable stocks for the calibration year (PLAN_2025_STOCKS: PLAN_5 10,000, PLAN_2 8,940,000). The top-up is a Bernoulli draw over the eligible pool at rate = shortfall / eligible_mass with identity-keyed uniforms. The gate (stage_health, check student_loan_plans) compares the realised top-up mass with the expected top-up mass rate * eligible_mass and refuses above a relative deviation of 1.0. It does not compare the final plan count with the stock.

Why PLAN_5 fails, on the licensed spine at 937aca4. Reported PLAN_5 in England already 9,183 of the 10,000 stock, so the shortfall is 817; eligible mass 136,538, so the rate is 0.006; the draw realised 2 rows with combined weight 1,718 against the expected 817, a deviation of +1.10. A shortfall of 817 people is about one survey person's weight, so any integer outcome of the draw is off by around 100% (zero rows −100%, two rows +110%), and the outcome is deterministic because the uniforms are keyed to person ids. The final England count is 10,901 against the 10,000 stock (+9%), which is a perfectly acceptable result the gate does not look at. What #1006 changed is the reported PLAN_5 mass (the band-donor persons), which shrank the shortfall into the one-or-two-rows regime.

What the gate hides on PLAN_2. PLAN_2 passes with deviation 0 only because its rate saturates at 1.0: the shortfall is 7.5M but the eligible pool holds 2.0M, so every eligible person is taken and the increment is deterministic. Its final England count is 3.43M against the 8.94M stock, 62% short, and the gate is silent about it. So the gate reports Bernoulli noise for the small plan and nothing for the large one; neither reading is stock attainment.

Proposed fix (stage and gate, on main).

  • Stage: realise a shortfall deterministically rather than by a Bernoulli draw, for example taking eligible persons in uniform order until the cumulative weight reaches the shortfall (deviation bounded by one person's weight, identity-keyed reproducibility kept); or skip a top-up whose shortfall is below one median person weight and record it in the receipt.
  • Gate: measure the final England count against the stock with a stated tolerance instead of the increment's realisation. That also surfaces the PLAN_2 attainment question, which is a data question about what the SLC liable stock counts (all liable borrowers, including those not repaying) against what the FRS can carry.

Receipts. experiments/901-uk-main-rebase-receipts.md R1 and R7 on #901; the per-plan receipts above come from the stage evidence of the #901 smoke run. The #1014 lane builds its spines under an uncommitted relaxation of the threshold; #901 did the same for its smoke run. Nothing on any branch changes the gate.

Activity

  1. juaristi22 commented on Sep 29, 2026

    @juaristi22
    CollaboratorAuthor

    Fixed in #1045 (María's call to fold it into that PR), commit b14048b8b on the current head, with the licensed receipts in Part F of experiments/1014-cgt-badr-table3-wealth.md there.

    Stage. A plan's shortfall against the SLC liable stock is realised by the identity-keyed greedy walk the gas-connection imposition uses: eligible persons in ascending stable-uniform order (the declared seed and salt), taken when their weight fits inside the remaining shortfall, skipped when it would overshoot, and the skipped person nearest the remainder taken afterwards if that brings the realised mass nearer. Every skipped person outweighed the remainder, so the realised top-up sits within the lightest skipped weight of the shortfall. The receipt records the gap, the skipped rows and the lightest skipped weight, an exhausted pool and the plan's stock attainment; the operations declare the walk and its bound.

    Gate. student_loan_plans now checks what the walk controls: the gap within the lightest skipped weight, and the final England count within a declared 5 percent of the stock. A pool lighter than its shortfall must have been taken whole and is recorded rather than refused; a plan already at or above its stock must have been left as reported; a partial or inconsistent receipt fails closed. The relative-deviation rule on the Bernoulli increment is retired.

    On the licensed spine, built with no relaxation of the threshold: every gate passes. PLAN_5's walk takes 19 rows for 817.1 people against the 817.2 shortfall, so England ends at 9,999.9 against the 10,000 stock (10,901 under the draw, from two rows of 1,718). PLAN_2 is unchanged and receipted as an exhausted pool: 2.04m eligible against a 7.55m shortfall, final count 3.43m, 38 percent of the 8.94m stock. The calibration moves within noise (effective sample size 5,165 against 5,173, CGT liability +3.6 against +3.7 percent). The synthetic smoke build has no eligible persons for either plan and records both pools as exhausted.

    The PLAN_2 attainment stays open as the data question this issue names: what the SLC liable stock counts against what the FRS can carry. The receipt now reports it on every build.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions