UK counterpart compute pipeline (managed sims, pe_gap-degrading registry) - #51
UK counterpart compute pipeline (managed sims, pe_gap-degrading registry)#51vahid-ahmadi wants to merge 8 commits into
Conversation
…ure) The UK sibling of compute_counterparts.py (#40), under the same binding contract: managed_microsimulation on the certified populace-uk bundle, machine-wide sim lock, weighted counts and GBP aggregates only (rates derive downstream in one place), microdf auto-weighting with boolean- array subgroup cuts, cross-entity via map_to. Two runs — baseline (calibrated take-up) and fullpart (take-up input overrides forced on, the policyengine_uk_inputs framework in the DB vocabulary) — covering the five ingested UK lanes: DWP take-up benefit-unit caseloads/expenditure (GB-restricted), HBAI poverty counts BHC/AHC by age group, HMRC taxpayer counts + liability aggregates, OBR per-benefit spending lines, UKMOD shared instruments. Engine variable names drift, so every concept resolves through an ordered CANDIDATES registry (first existing wins, recorded in pe_uk_meta.json); an unresolvable concept emits a pe_gap row instead of crashing — the gap stays on the page per the status taxonomy, and the run log lists every unresolved concept for a one-line registry fix. Not executed on this machine (no managed environment or certified bundle); registry-consistency tests pin what is checkable anywhere, and the module imports engine-free (the policyengine import lives inside build_sim). README gains the UK reproducing block. First execution under simlock is the remaining half of #40, along with the country promotion through build_comparison.py. Part of #40 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Import-time sys.argv parsing broke pytest collection ('-q' reached
int()); the module now imports side-effect-free, which the registry
tests rely on.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review of the UK sibling against compute_counterparts.py, the reference implementation, found six ways it could emit wrong numbers under status "ok". None were reachable by the registry tests, and the script has still never been executed, so they were fixed by inspection. 1. Inputs set after a calculate. The fullpart run sized its override array with len(sim.calculate(flag, YEAR)) — caching baseline take-up before the override, which then does nothing. Now sizes from sim.populations[entity].count, as the US pipeline does, and touches no calculate before the last set_input. 2. Hardcoded annual period. Overrides are now set at each flag's own definition_period — all twelve months for monthly variables, with an eternity fallback. UK benefit variables are commonly monthly, so a year-only set_input silently misses the periods the engine reads. 3. No verification the toggle took effect. The run now records flag_means_after per flag and aborts when no override applied, when none reads back as on, or when the fullpart series is identical to baseline across every take-up-sensitive value. A silent no-op made every DWP take-up denominator the baseline caseload — i.e. every take-up rate 100% — while every row still read status "ok". 4. Missing flags skipped silently. flags_missing / flags_failed are recorded and warned, and a run with zero applied overrides is a hard failure rather than a bogus fullpart series. 5. Unweighted count as a poverty denominator. The `count() if hasattr(...) else len()` fallback emitted a ~10^5 record count as a population denominator. The weighted path now requires a real weighted MicroSeries and emits pe_gap otherwise; an unweighted number is never presented as weighted. 6. Silent concept substitution. in_relative_poverty_ahc fell back to the generic in_poverty, which is not an AHC concept. Removed, and the other candidate lists audited: pays_higher_rate no longer lists higher_rate_earned_income_tax (a GBP amount, not a boolean). The invariant — candidate lists hold only genuine synonyms — is stated in the file and enforced by tests. Also: the year is read from argv[1], not argv[2] (there is no argv[2]; `script.py 2026` silently ran 2025), and the engine entry point mirrors the documented binding surface as pe.uk.managed_microsimulation(), marked UNVERIFIED in a comment since no installed engine can confirm it here. Tests extended with what is checkable without the engine: no cross-concept fallbacks, BENEFIT_LINES concepts resolve, argv index, definition-period selection, the fullpart movement assertion, and a denominator test proving an unweighted count can never be emitted as "ok". The module still imports without policyengine installed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Review pass on this file against
Plus: Defect 3 was the worst of them: if all five take-up overrides silently no-op — very possible, since policyengine-uk take-up is parameter/seed-driven and these input variables may not exist — Still unverified until the first real run
Tests: extended |
|
Dual-gate review: NEEDS-FIXES — fix rather than hold (verified independent of #48). Blockers:
Verified sound: nested engine import, map_to usage elsewhere, set_input-only fullpart mutation, simlock docs. |
…ity levels, per-benefit validation, coverage, provenance, weighting 1. FULLPART_OVERRIDES now uses the engine's actual take-up inputs (would_claim_pc/housing_benefit/uc/child_benefit + claims_all_entitled_benefits — the compute campaign's proven fulltakeup set), not guessed takes_up_* names that hit the run's own abort. 2. Entity levels follow the engine: esa_income/jsa -> benunit, winter_fuel_allowance -> household. Geography masks are rebuilt from the person-level country projection with first-person anchoring per group entity (the US first_person_anchor_state rule); a restricted line whose entity has no anchor goes pe_gap — an unrestricted total is never emitted under a GB/NI label (restricted_mask()). 3. Fullpart movement validation is per benefit: each TAKEUP_VALIDATED_PROGRAMS entry must move on its own; components (guarantee/savings credit) are warn-only. UC moving no longer blesses an unchanged PC caseload. 4. Coverage matches advertisement: pension-age HB emitted under the DWP external program id (housing_benefit_pensioners) via a head-SP-age benunit cut, alongside the unrestricted HB line for obr/ukmod; every HBAI numerator cut gets its matching subgroup population denominator; HMRC taxpayer counts + liability emitted per marginal band (basic/higher/additional) through boolean band concepts, and the declared-but-unused total_income / marginal_tax_rate_band candidates are gone. 5. Provenance reads sim.policyengine_bundle first (US precedent), recording which path served (bundle_source) so a fallback never masquerades as the bundle record. 6. weighted_population() identifies a MicroSeries by type and uses MicroSeries.count() (the weighted count) — no .weights access, enforced on the AST by a test. Suite: 158 passed, 4 skipped; ruff format clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
All six blockers fixed in 6d77d5a. Blocker → fix:
Suite: 158 passed / 4 skipped (was 148); Honesty note, same as before: still zero real executions — no managed environment here. The band flag candidates ( |
Review — UK counterpart compute pipelineFaithfully mirrors the US Should address (both cheap, before the first managed run)
NoteThe engine entry ( Reviewed with Claude Code assistance. |
… year param, type-guard the bundle record - fullpart overrides are np.full(n, value, dtype=bool) ndarrays, not Python lists (set_input parity with the US path). - build_sim reads the module-global YEAR everywhere; the year parameter callers never passed is removed rather than left as a silent divergence trap. Signature pinned by test. - The bundle record is read via .items() only when mapping-like; a non-mapping record is stringified with its own bundle_source label instead of raising inside build_sim. Suite: 160 passed, 4 skipped; ruff clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
All three should-address items fixed in b54444c:
Suite: 160 passed / 4 skipped (was 158); |
Re-review — all addressed ✅All three with matching regression tests:
The engine path ( |
DTrim99
left a comment
There was a problem hiding this comment.
Re-reviewed — all three findings fixed with matching regression tests (np.full ndarray, year param dropped, bundle type-guard). Approving.
Re-gate round 2 — the five names are right, but the combined world isn'tThe literal take-up vocabulary is now correct (all five verified as annual benunit variables in pinned 2.89.2), and the earlier entity/provenance/weighting fixes hold. But the dual gate's second pass — this round read the pinned engine source directly — found the full-participation construction itself doesn't survive contact with the engine's mechanics. Five blockers, each with the engine evidence:
Two smaller items: in a real managed environment the focused tests go 31 passed / 2 failed (the fake Holding #76–#80 (the campaign runs) on this — the minimum repair is benefit-specific fullpart construction, claimed-GC / SC-only logic, per-metric direction-aware validation, and an exhaustive source-aware registry where every value lands as a result or a cited gap through the post-#74 DB path. Happy to pair on the HB construction question if useful — it may genuinely be a 🤖 Generated with Claude Code |
1. Housing Benefit leaves the fullpart world as a CITED GAP. In pinned 2.89.2 housing_benefit_eligible requires BOTH ~would_claim_uc (the UC/legacy mutual exclusion, with its own engine regression test) and housing_benefit_reported > 0 — eligibility is receipt-gated, so a take-up world cannot create entitled non-recipients for HB, and forcing the flags on drives HB toward zero rather than all-entitled. Its fullpart rows now emit pe_gap with that reason and an upstream link; the baseline rows are unchanged (HB as a level is fine). HB is out of TAKEUP_VALIDATED_PROGRAMS, because demanding movement would force the wrong-direction number straight back in. As you suggested, this reads as an honest capability gap rather than a failure. 2. Pension Credit components are DWP's concepts, not the engine's. guarantee_credit / savings_credit are entitlement components that never consult would_claim_pc — only the aggregate does — so the raw rows published a wrong-concept value under status "ok", under a savings_credit slug the closed registry does not recognise. They are no longer plain benefit lines: PC_COMPONENT_LINES emits CLAIMED guarantee credit and savings_credit_only, both gated on would_claim_pc, with savings-credit-only additionally requiring guarantee_credit == 0 (the adapter's own definition, adapter.py:88). 3. Validation is per metric and direction-aware. It aggregated the two sensitive metrics and accepted movement in ANY direction, so a frozen PC recipient_count beside a moved spending figure passed as "1/2 moved" and a decrease passed silently. Now every comparable (program, metric) pair must INCREASE — forcing take-up on can only add recipients and spending — a fall is a hard failure, and a program missing one of the two sensitive metrics is a partial pass and also fails. Three tests reproduce the exact probes. 4. Gaps are citable at emission. pe_gap now requires a registered reason from GAP_REASONS, each carrying a rationale and an action link, and an unregistered reason raises. The HBAI loop is exhaustive: it emitted only the total-numerator gap and continued, producing 2 gaps where 12 claims were owed, and since the exporter only surfaces claims that already have a result, the other ten vanished. hbai_gaps() now emits all six cells per housing-cost basis. 5. Rows are source- and unit-aware. Every row carries the external source id and unit concept through ROW_IDENTITY, so DWP benefit_units and UKMOD families stay distinct as the closed registry intends, and an unattributed (program, metric) raises rather than emitting an unattachable value. The HMRC band cuts resolve through the engine's tax_band ENUM first — the three boolean band variables do not exist at the certified pin, so leading with them guaranteed three uncited gaps every run — with the booleans kept as fallbacks. Smaller items: the MicroSeries test helper now builds a real MicroSeries where microdf is importable and the type-identity stand-in only where it is not, so the focused tests hold in the managed environment too (they were 31 passed / 2 failed there). And the output location is decided deliberately: data/pe/, tracked, alongside the US pe_metrics.json — these are run outputs an operator commits and a later build reads, so their first creation is a normal commit rather than something the no-drift gate must be taught about. The ingest that reads them lands with the first real run (#76), which is why every gap is cited here. Suite 294 passed, ruff format clean. Still unexecuted against the managed environment, and knowingly so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HuXJFVme8HRbnke2Ey3Me
|
All five blockers plus the two smaller items addressed in
Smaller items. The Suite 294 passed, ruff format clean. Still unexecuted against the managed environment, and knowingly so — happy to hold #76–#80 until you have re-read this. |
…89.2
Resolved the whole CANDIDATES registry against an installed
policyengine-uk 2.89.2 rather than reasoning about it. Results:
- Exactly three concepts are unresolvable, and they are the three
boolean band flags — confirming the review's read, and confirming
that leading with tax_band is what stops three uncited gaps per run.
- Every BENEFIT_LINES entity matches the engine's own variable entity.
- guarantee_credit, savings_credit and would_claim_pc are all annual
benunit variables, so the PC-component gating maps at the right
entity.
- housing_benefit_eligible reads, verbatim: already_claiming
(housing_benefit_reported > 0) & (social | lha_eligible) &
~would_claim_uc & (capital <= limit). Receipt-gated and
UC-exclusive, exactly as the gate said — the cited gap is right.
One defect that check surfaced: an EnumArray holds integer INDICES, and
decode_to_str() is what turns them into member names. The fallback path
I wrote would have compared the UNDECODED array to BASIC, matching
nothing and silently emptying every band cut. A series that cannot be
decoded is now not used at all — the band falls to its boolean fallback
and then to a cited gap — and the comparison accepts the member name or
its value so a core release that changes which one it decodes to cannot
empty the cut either.
Suite 296 passed, ruff format clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HuXJFVme8HRbnke2Ey3Me
|
Follow-up: I resolved the whole The defect (fixed in What the check confirmed:
Suite 296 passed. Still unexecuted against a managed bundle — but the parts that were reasoned about are now checked. |
Part of #40 — the standing UK compute stage, structured to the US pipeline's binding contract. (Not "Closes": first execution under simlock and the
countrypromotion throughbuild_comparison.pyremain — both need the managed environment, which this machine doesn't have.)What's here
pipeline/compute_uk_counterparts.py— mirrorscompute_counterparts.py:managed_microsimulation(country="uk")on the certified populace-uk bundle under the sim lock; emits weighted counts and GBP aggregates only (data/pe/pe_uk_metrics.json+pe_uk_meta.jsonwith per-variable metadata), so every rate stays derived downstream in one place. Microdf auto-weighting rules honored: boolean-array subgroup cuts,map_tofor cross-entity, no weight-array touching.baseline(calibrated take-up) andfullpart(take-up input-variable overrides forced on — thepolicyengine_uk_inputsframework in the DB's ReformRef vocabulary), giving the entitled-population denominators the DWP take-up comparison needs.tests/test_uk_pipeline_registry.py— registry-consistency tests runnable anywhere; the module imports engine-free (the policyengine import is insidebuild_sim).The honesty mechanism
Engine variable names drift across policyengine-uk versions, so every concept resolves through an ordered
CANDIDATESregistry — first existing variable wins and is recorded inpe_uk_meta.json. A concept with no resolvable variable emits ape_gaprow instead of crashing: the gap stays on the page per the status taxonomy, and the run log lists each unresolved concept for a one-line registry fix. Reviewers should treat the candidate lists as the first thing to correct against the current engine — that's exactly what they're for.Honest status
simlockis owed by whoever holds the managed environment, and itspe_gap/unresolved log becomes the follow-up work list.Pairs with #48 (the claims these counterparts will attach to).
🤖 Generated with Claude Code