Repository navigation
Draw UK QRF stages from uniforms keyed on each record's id, and tie pension contributions to pay - #1332
Conversation
|
@juaristi22 thanks, this is my review at The design is right. Keying each QRF draw on the record's own id, salted by stage and target, is the correct fix for the positional stream. The keys hold up: blake2b gives 64 bits, the salts differ by stage, target and quantile or sign, and high-value copies get their own offset ids, so they draw independently as before. The property tests do catch the old behaviour. One thing blocks merge: CI is red on engine-us, because one US spec hash pin was not updated. Before #1333 merges I would also like three things: the 2.134.5 invariance check, a clear statement that this PR redraws everything once, and an annual allowance check against HMRC, now that employer contributions follow pay. I also have one question about the SPI second forest, where clones of one FRS person now share uniforms. What I ran
Findings
What looks right
🤖 Generated with Claude Code |
The US bundle's semantic hash covers microcosm.fit.qrf's attested bytes, which fit_draw_next's caller-owned uniforms change, as the seed protocol re-pin records. The engine-workflow tier pins the hash apart, and the four --check tools do not reach it (Vahid's review of #1332, finding 1). The value is the one the regenerated F0 coverage report carries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stable_identity_uniforms formats each id into its key, so the integer 1001 and the float 1001.0 draw differently, and a merge that turned an id column into floats would silently redraw every record. A repeated id would give two records the same uniforms. integer_record_keys refuses both, and every keyed UK stage (was_wealth, lcfs_consumption, etb_services, uc_reporter_redraw and salary_sacrifice) passes its ids through it (Vahid's review of #1332, nit 11). The changelog fragment now says that the switch redraws these stages once for every record (finding 4), and salary_sacrifice's guard names its dependency: no stage between the pay caps and this one may lower pay (nit 10). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage 2 keyed its uniforms on person_source_id, the FRS twin's id. The SPI channel and the band donors copy one FRS person more than once, so those copies shared every stage-2 uniform and drew comonotone values, where the positional stream had drawn them apart. Stage 1 shares the twin's identity on purpose and counts it; stage 2 now keys on the row's own person_id, which is just as stable (a copy's id is an offset of its source's), and refuses ids that are not unique integers (Vahid's review of #1332, finding 2). The bridge test checks the stage-2 keys, and the locality test now also checks a stage-2 draw: dropping a recipient or reversing the recipients moves no other row's draw. Keying stage 2 on the twin's id fails them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only the WAS, LCFS and ETB tests ran the shared locality check. Salary sacrifice now runs it too: reversing the people, changing one and adding another moves no other person's uniform, and a positional stream fails it. The UC reporter redraw checks that each redrawn unit's uniforms are the ones keyed on its own benefit unit id, which no row order can move (Vahid's review of #1332, finding 8). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The FRS spine caps each adult's net-pay deduction at their usual earnings. A deduction beside no recorded pay may stand for pay the adult tab left blank, not for a contribution above pay, so the stage evidence now counts the two cases apart (above_recorded_pay and beside_no_recorded_pay), each with banded support and amounts withheld below ten source households (Vahid's review of #1332, finding 7). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in's rate The symmetric rule carried the FRS twin's scheme record onto the SPI row: a member twin's rate applied to the row's own pay even where the forest drew no contribution, and a twin outside any scheme cleared the forest's draw. But the record describes the twin's job at the twin's pay, and the row's pay is redrawn, often far from the twin's. On the arms it put members at low drawn pay, where few people are members, so participation by pay band moved away from the FRS profile that the forest reproduces (Vahid's review of #1332, finding 6). Now the forest's draw decides membership, since it reads the row's own drawn pay. A member whose twin has an ASHE rate takes that rate times its own pay, which still scales the amount to pay above the FRS's range; any other member keeps the forest's amount, bounded by ASHE's top rate. Nothing is cleared any more, so the receipt drops employer_none_outside_twin_scheme. The rule test and the SPI stage test fail under both earlier rules (María's ruling of 21 October). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@vahid-ahmadi thanks. Pushed
🤖 Generated with Claude Code |
|
@juaristi22 thanks, this is my second review, at Nothing in the delta blocks this PR once it is rebased on #1329. CI is green at head, both engine-us jobs included. The stage-2 keying is fixed and the integer-key guard is a good addition. Three things carry to #1333: the annual allowance figures (5), the first build of the final employer rule (6), and the release-level invariance check (3). pe-uk 2.142.0, released at 13:04 today, carries #2536, so all three can now come from one #1333 arm. One correction to your reply on item 2, and one follow-up I would like filed now (item 7). What I ran
Round-1 items
New findings
Merge orderThe order is #1329, then #1332, then #1333:
What looks right
🤖 Generated with Claude Code |
fit_draw_next takes quantiles and sign_uniforms (both or neither), one in [0, 1) per recipient row, and then draws as predict_from_uniforms does, leaving the chain's draw stream where it was. Keyed on stable record ids, a row's draw then depends on its own uniforms and features only: adding, dropping or reordering other recipients moves no row's draw. Without them the step consumes the positional stream as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Six UK stages drew from a positional stream: was_wealth, the lcfs_consumption chain, etb_services, uc_reporter_redraw, the SPI stage-2 fill and salary_sacrifice. A positional stream gives a row the uniform of its position, so any change in the rows before it, or in the frame's length, redraws it. On #1308's arms an engine bump moved a few households' net income; was_wealth, which reads it, redrew them; the high-value support copies then changed the frame's length, and every later imputation redrew nearly every household, including households whose own inputs had not moved. Each draw now reads stable_identity_uniforms keyed on the household, benefit unit or person id (person_source_id in the SPI fill, as stage 1 does) and a salt naming the stage and target. Copy ids are offsets of their source's, so the keys hold across engines. Chains keep their order, and every stage keeps its declared seed. A shared check (test_support/microcosm_build/uk_keyed_draws.py) reverses the recipients, changes one and adds another, and requires every other recipient to draw what it drew before; the was_wealth, lcfs_consumption and etb_services tests run it, and fail with the positional stream back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
policyengine-uk relieves an employee's contributions through the net pay arrangement, which cannot take more than the pay they come out of, and from 2.134.5 it counts every contribution, the employer's included, as pension input for the annual allowance. Two readings broke that link: - the FRS records each job's pension deduction (JOB.DEDUC1) apart from the adult's usual earnings (ADULT.INEARNS), so a deduction could exceed the pay beside it; - an SPI-channel row redraws its pay from the tape, while the stage-2 forest draws its contributions. The forest reads the drawn pay, but it draws whether the row has an employer contribution apart from the twin's scheme record, and it can only draw amounts that FRS rows in the same leaf received, so it cannot scale them to pay above the FRS's range. uk_runtime/pension_coherence.py caps employee contributions at pay, on FRS rows as frs_spine reads them and on SPI rows after stage 2. An SPI row takes its employer contribution as #1069 sets an FRS row's: its FRS twin's scheme record times its own pay. A member twin carries the rate its ASHE draw gave it, and a twin with pay outside any employer scheme carries none, so the row's membership is the twin's both ways (María's ruling of 20 October). Only a row whose twin has no pay, and so no scheme record, keeps the forest's draw, bounded by ASHE's top rate times its pay. Personal contributions stay as recorded: the engine limits their relief itself. Each step records a receipt in the stage evidence: support banded on distinct source households, and weighted amounts withheld as "<10" where one to nine households carry them. salary_sacrifice now refuses an employee contribution above the pay that remains, which the conversion can no longer produce; its tests keep their inputs inside that rule. New tests pin the cap, the rule both ways, the receipts and the SPI stage's wiring; the one-sided rule and an uncapped FRS deduction both fail them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage 2 of the SPI income stage draws from uniforms keyed on person_source_id too (42fb9f6), so the engine test that captured stage 1's query also saw stage 2's call and failed on its targets. The test now records each stage's query by the targets it draws. It also checks that stage 2 is queried over the whole age domain and that every row draws from uniforms keyed on its own identity under the stage's seed, which no test covered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…chain step fit_draw_next now takes caller-owned uniforms (c5276aa), and microcosm.fit.qrf is one of the kernel modules the seed protocol attests, so the protocol's content digest and the compiled seed stream map digest legitimately move, as they did when predict_from_uniforms landed (1734b9e). Nothing in the spec engine or the US spec changes, and main still matches the old digests. The committed F0 coverage report is regenerated from the re-pinned inventory, and tools/spec_engine_coverage.py --check passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The US bundle's semantic hash covers microcosm.fit.qrf's attested bytes, which fit_draw_next's caller-owned uniforms change, as the seed protocol re-pin records. The engine-workflow tier pins the hash apart, and the four --check tools do not reach it (Vahid's review of #1332, finding 1). The value is the one the regenerated F0 coverage report carries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stable_identity_uniforms formats each id into its key, so the integer 1001 and the float 1001.0 draw differently, and a merge that turned an id column into floats would silently redraw every record. A repeated id would give two records the same uniforms. integer_record_keys refuses both, and every keyed UK stage (was_wealth, lcfs_consumption, etb_services, uc_reporter_redraw and salary_sacrifice) passes its ids through it (Vahid's review of #1332, nit 11). The changelog fragment now says that the switch redraws these stages once for every record (finding 4), and salary_sacrifice's guard names its dependency: no stage between the pay caps and this one may lower pay (nit 10). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage 2 keyed its uniforms on person_source_id, the FRS twin's id. The SPI channel and the band donors copy one FRS person more than once, so those copies shared every stage-2 uniform and drew comonotone values, where the positional stream had drawn them apart. Stage 1 shares the twin's identity on purpose and counts it; stage 2 now keys on the row's own person_id, which is just as stable (a copy's id is an offset of its source's), and refuses ids that are not unique integers (Vahid's review of #1332, finding 2). The bridge test checks the stage-2 keys, and the locality test now also checks a stage-2 draw: dropping a recipient or reversing the recipients moves no other row's draw. Keying stage 2 on the twin's id fails them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only the WAS, LCFS and ETB tests ran the shared locality check. Salary sacrifice now runs it too: reversing the people, changing one and adding another moves no other person's uniform, and a positional stream fails it. The UC reporter redraw checks that each redrawn unit's uniforms are the ones keyed on its own benefit unit id, which no row order can move (Vahid's review of #1332, finding 8). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The FRS spine caps each adult's net-pay deduction at their usual earnings. A deduction beside no recorded pay may stand for pay the adult tab left blank, not for a contribution above pay, so the stage evidence now counts the two cases apart (above_recorded_pay and beside_no_recorded_pay), each with banded support and amounts withheld below ten source households (Vahid's review of #1332, finding 7). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in's rate The symmetric rule carried the FRS twin's scheme record onto the SPI row: a member twin's rate applied to the row's own pay even where the forest drew no contribution, and a twin outside any scheme cleared the forest's draw. But the record describes the twin's job at the twin's pay, and the row's pay is redrawn, often far from the twin's. On the arms it put members at low drawn pay, where few people are members, so participation by pay band moved away from the FRS profile that the forest reproduces (Vahid's review of #1332, finding 6). Now the forest's draw decides membership, since it reads the row's own drawn pay. A member whose twin has an ASHE rate takes that rate times its own pay, which still scales the amount to pay above the FRS's range; any other member keeps the forest's amount, bounded by ASHE's top rate. Nothing is cleared any more, so the receipt drops employer_none_outside_twin_scheme. The rule test and the SPI stage test fail under both earlier rules (María's ruling of 21 October). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The keyed draws change the hermetic H2 fixture's CGT asset-type facts and property purchase targets, and main (#1329, #1295) moved the same two files, so they are regenerated on the rebased tree with tools/graph_uk_spine_fixture.py; --check then passes. They are re-derived synthetic aggregates with no record-level data. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
c11ac14 to
1599324
Compare
Main moved the seed protocol, the compiled seed map and the bundle spec_sha256 (microcosm#104, #1124, #1301, #1332); this branch moved the source-manifest, resource-semantics, tail and full-checkpoint digests. The conflicts keep both sides' changes; the merged tree's identity values are re-derived on Modal in the next commit, and the F0 attestation is regenerated there. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Reran build_1073_scf_education_debt_receipt.py on Modal at 2a6e65a (run 20261022T153020Z-2a6e65a73), after the merge with main brought in #1332's QRF change. Every fit figure is unchanged; only the receipt's commit, run and date move. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Prepares the next policyengine-uk bump (#1095). #1308's arm on policyengine-uk 2.134.5 failed the weight-ratio gate. The engine change moved few households, but the build redrew most of the spine. This PR makes each stage's draws move only for the records whose own inputs move, and ties pension contributions to the pay they come from. ORR's rail support row is already held out of the fit on main, by #1329.
Rebased onto main 211483f, after #1329 and #1295. This PR's own rail commit is dropped: #1329 holds the same row out under its own entry.
The bump itself is the stacked PR that follows. It merges after this PR and after the policyengine-uk release that carries the fixes of PolicyEngine/policyengine-uk#2523.
Why the weight ratio failed on 2.134.5
RegimeGatedQRFdrew each target's quantile and sign uniforms from one positional stream (rng.random(n)per target), so a row's draw depended on its place in the frame, not on the row itself.household_net_incomefor some households.was_wealthreads it and redrew their wealth.high_value_home_supportthen copied a different set of households, and the frame's length changed.The record-level evidence stays local.
Summary
Keyed draws
fit_draw_next(microcosm-fit) takes caller-ownedquantilesandsign_uniforms, both or neither, so a chain step can draw from keyed uniforms. Thefit.qrfparity pins and the spec-engine golden re-derive.microcosm.fit.qrfis a kernel module the seed protocol attests, so the US inventory's seed protocol and compiled seed map digests re-pin, with the F0 coverage report, as they did forpredict_from_uniformsin 1734b9e.RegimeGatedQRFnow draws fromstable_identity_uniforms, keyed on the record's own id and salted by stage and target:was_wealth, the LCFS consumption chain andetb_services, on the household id;uc_reporter_redraw, on the benefit unit id;person_source_idunder the stage's own seed;salary_sacrifice, on the person id.test_support/microcosm_build/uk_keyed_draws.pychecks the property on three stages. It reverses the recipients, changes one and adds one, and no other draw may move. With the positional stream restored, all three tests fail. The SPI bridge test also checks the second forest's keys.Pension contributions that cohere with pay
salary_sacrificerefuses employee contributions above the pay that remains.Measurement
Two arms ran on this machine: national role, local staging only, feed abf0921.
C0, the control: main a90eec3, policyengine-uk 2.134.4.
C1: this PR at 4198bdd, before the review fixes. Its employer rule took the twin's scheme record both ways; the rule now leaves membership to the forest (c11ac14), and Lock policyengine-uk 2.145.0, follow its bus fares and bind thin target rows #1333's arm measures the final code.
Both arms pass all seven terminal gates. On C1, the weight-ratio gate passes with much more room than on C0.
The fit is slightly better: the final loss falls a little, and a few more rows sit within 10% of their targets.
24 rows move by more than three points, nearly all toward their targets: income tax by region, self-employment counts, the State Pension bands and population by region among them. One row moves just outside the 10% line, and four move inside it.
The support floor admits one Universal Credit payment band row and drops another, as the draws move.
ORR's rail support is measured, not bound (on C1 by this PR's own entry, since replaced by UK pre-Budget data: measure stored values at their period, draw Carer's Allowance claims, and the #1278 items (#1324, #1319, #1278) #1329's).
The two fragile HMRC rows stay bound, and less of their fitted weight sits on households near the solver's cap. Across the frame, the share of weight near the cap falls by more than half.
Build time on a quiet machine is about the same: 631 seconds of stage time against 607.
The pension receipts sit in each stage's evidence: support as a band, and weighted amounts withheld where fewer than ten source households carry them.
Tests
tools/ci_test_plan.py verifyand the four--checktools pass, and the H2 fixture regenerates byte for byte at 4198bdd.tools/spec_engine_coverage.py --checkpasses at 1258b13.--checktools, the spec-engine coverage check and the H2 fixture check pass.🤖 Generated with Claude Code