Skip to content

populace-calibrate: representation operator (targets -> calibrated weights) - #3

Merged
MaxGhenis merged 11 commits into
mainfrom
calibrate-kernel
Jun 10, 2026
Merged

populace-calibrate: representation operator (targets -> calibrated weights)#3
MaxGhenis merged 11 commits into
mainfrom
calibrate-kernel

Conversation

@MaxGhenis

Copy link
Copy Markdown
Contributor

The third shard — populace.calibrate, the representation operator (DESIGN.md "populace-calibrate: representation"). Targets become a sparse linear constraint system on a Frame's weight vector; torch Adam over log-weights minimizes the eCPS relative-error loss to produce CALIBRATED weights.

Features (each a behavioral contract test)

  • Hits feasible targets and reduces loss by orders of magnitude.
  • CALIBRATED-kind output weights on the calibrated entity.
  • mass="free" / "conserve" — total moves to fit, or is held exactly.
  • max_weight_ratio — a hard per-record bound (exact on the returned vector), the landmine guard; a test shows it prevents a rare near-zero-weight donor's weight from detonating to hit a tail target.
  • Multi-period(target, period) rows stack over one weight vector ("one weight per trajectory").
  • L0 pruning — hard-concrete gates prune toward a target_records budget (generate-big-then-prune).
  • Uncompilable targets are skipped and reported, never silently dropped.

Grounded in the proven eCPS reweight loss/optimizer. Built largely by an agent (killed mid-test-authoring; work preserved and finished). 23 tests; workspace 234 passed.

🤖 Generated with Claude Code

MaxGhenis and others added 11 commits June 10, 2026 19:32
…e uncommitted work)

Core modules (target/matrix/solve/gates + compat tests) built by the
build agent before it was killed mid-test-authoring. Committing to
preserve; tests + verification to follow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… mode

Complete the killed agent's shard: behavioral contract suites for the
solver (loss reduction + feasible-target hit, CALIBRATED kind, mass
free/conserve, hard weight-ratio bound + landmine prevention,
multi-period one-weight stacking, L0 record-budget pruning), targets
(aggregations, column/callable measure, validation), and the constraint
matrix (shape, multi-period rows, skipped-target reporting).

Two fixes found while testing: (1) make the max_weight_ratio bound exact
on the returned float64 vector (the per-step clamp is float32 and can
overshoot the bound by ~1e-7); (2) --import-mode=importlib at the root so
the shards can share test-file basenames (each ships a test_compat.py).

23 calibrate tests; 234 passed workspace-wide.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…==target-1

Adversarial review found the eCPS +1-in-numerator loss is minimized at
est = target - 1, not target — negligible at $2T magnitudes but fatal
for small targets (a count of 5 converged to 4.0). Drop the +1 from the
numerator (keep it in the denominator regularizer); this is the loss the
docstring always described. Regression test: a count target of 5 now
converges to 5.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…om guard)

Finding 2 (HIGH): the eCPS relative-error loss divides each residual by
(target_value + 1). A target value of exactly -1 -- a raw value=-1, or a mean
target whose value is exactly 1 below the current mean (compiled RHS is
value - current_mean) -- zeroes that denominator. The loss goes NaN, gradients
go NaN, every weight goes NaN, and the only surfaced error was the kernel's
opaque "Weights must be finite; found N NaN", naming neither the target nor the
cause.

build_constraint_matrix now validates the compiled target_vector and raises a
clear ValueError naming the offending target(s) and the cause when any
|target_value + 1| < 1e-8.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Finding 5 (HIGH): build_constraint_matrix built a target's constraint row at a
person-aligned linearization point (via _entity_row's cross-entity collapse) but
called target.offset(frame, w0) with the GROUP weight vector. For a person-entity
mean target on a multi-person frame, offset builds person-length arrays, so
filter_mask * weights broadcast (n_persons,) against (n_households,) and raised
ValueError -- caught and recorded as a SkippedTarget with a raw-numpy reason
("operands could not be broadcast together..."). The target was silently dropped.

Resolve the linearization point once, aligned to the target's own entity
(_linearization_weights), and hand the same vector to both _entity_row and
offset. _entity_row now receives the already-aligned weights instead of
re-deriving them, so the row and its offset share one point.

Adds a multiperson_frame fixture (exercising the previously-unused
persons_per_household machinery) and tests a person sum and a person mean target
across real multi-person households.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ords

Finding 4 (HIGH): with L0 pruning, mass='conserve', and a max_weight_ratio cap,
_project_to_total filled the post-pruning mass deficit by additive
redistribution over ALL records with headroom (cap - weight > 0), INCLUDING the
gate-closed (~0-weight) pruned ones -- resurrecting them. A run that pruned
hundreds of records under free mass returned zero pruned under conserve+cap.

_project_to_total now takes a `pruned` mask (the gate-closed records, by the same
prune threshold the result's n_nonzero uses) and redistributes the deficit only
over surviving records, holding pruned ones at zero. If the survivors lack the
headroom to absorb the freed mass under the cap, it raises a clear ValueError
naming pruning + conserve + cap as jointly infeasible, instead of silently
un-pruning to balance the books. The no-pruning path is unchanged.

Finding 7 (also fixed): max_weight_ratio < 1 with mass='conserve' is infeasible a
priori (sum of caps < input total) -- now rejected in argument validation with a
named error rather than surfacing later as the kernel's opaque mass-conservation
failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ad param)

Finding 3 (HIGH): target_records was dead -- it only appeared in
`if l0_lambda > 0.0 or target_records is not None:`, so the NUMBER never entered
the optimization. Pruning was controlled entirely by l0_lambda, and budget 10 vs
350 at the same lambda produced bitwise-identical weights despite the docstring
promising "prune toward target_records non-zero weights".

Implement honest budget control: when target_records is set, the solver searches
l0_lambda by bisection on log10(lambda) -- the realized non-zero count is monotone
decreasing in the penalty -- so the achieved count tracks the budget. Each
evaluation is a full _optimize reseeded to `seed` (deterministic count-vs-lambda
response); the search keeps the best run seen, stops early within ~5% of the
budget, and is bounded by `budget_iters` optimizations (new param, default 10). A
supplied l0_lambda is the warm start. The result reports the penalty the search
settled on as l0_lambda.

Achieved vs budget on the n=400 fixture (epochs=250): 50->49, 100->100, 150->156,
200->210, 250->238, 300->306; lambda decreases monotonically with the budget.
l0_lambda alone (no target_records) still prunes at a fixed penalty, unchanged.

Updates the solve/init docstrings and README to describe budget control.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…in diagnostics

Two diagnostic-correctness notes from the review (6 and 8), both making the
reported numbers describe the weights actually returned.

Finding 6 (MEDIUM): a `mean` row is linearized about the input weights, so
`A @ w` is the linearized value, not the achieved ratio. After a large mass move
the diagnostic reported a near-zero relative_error and a spurious
within_tolerance=True even when the true achieved mean missed the target and its
tolerance. Target.achieved_value now computes the true aggregate (sum/count
exact, mean as the true ratio sum(measure*filter*w)/sum(filter*w)), and
_build_diagnostics uses it for mean targets -- against the user's declared target
value, not the linearization's shifted RHS. On a 1.5x-mean run the diagnostic now
reports the true ratio (~63k, real -0.8% miss) instead of the linearized ~21k.

Finding 8 (cheap): final_loss returned loss_trajectory[-1], a pre-step,
pre-projection value that does not describe the returned weights (the closing
mass/cap projections move them). CalibrationResult now stores closing_loss -- one
eval-mode eCPS relative-error loss on the returned weights -- and final_loss
returns it. On a conserve+cap run the two differ by ~0.8%.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Budget control (the target_records fix) replaced the old "auto-select a small
default penalty when target_records is given without l0_lambda" behavior, leaving
_DEFAULT_L0_LAMBDA defined but unreferenced. Remove it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…search infeasible probe)

From the verification review of the review-fixes:
- Update the loss formula in 4 doc spots (module/calibrate docstrings,
  __init__, README) to the implemented ((A@w-b)/(b+1))^2 — the +1 was
  dropped from the numerator but the docs still showed it.
- Harden the L0 budget search: an intermediate bisection probe can
  over-prune past the conserve+cap feasible floor and trip the
  infeasibility raise; catch it, steer toward a smaller penalty, and only
  raise (naming all three causes) if no penalty is feasible. A feasible
  budget under conserve+cap no longer crashes mid-search.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sed failure

Both x86-CI failures were knife-edge test designs, not solver bugs — but
chasing them surfaced one real solver gap:

- prune+conserve+cap test: a fixed l0_lambda prunes platform-dependently
  (tens of survivors on arm64, 3 of 400 on x86, which fell below the
  cap-feasibility floor of n/ratio = 4 and tripped the infeasibility
  guard; a cap loose enough to make any count feasible instead lets the
  conserve rescale feed an all-gates-closed collapse). The run now uses
  target_records budget control, which adapts the penalty to a survivor
  count on any platform; the resurrection regression is still caught
  (under the bug every probe returns ~400 nonzero).
- solve: an L0 run whose gates all close returned an all-zero vector and
  died downstream in the kernel's opaque "Weights cannot be all zero".
  calibrate now raises a named error (cause: the penalty; remedies:
  lower l0_lambda or budget control) on the direct path; budget-search
  probes already steer off zero counts. New contract test.
- fit tail-share test: a single predict() round's share noise (~0.0011
  at n=5000) matches the ~0.0015 nearest-snap bias under test, so the
  one-draw comparison was a platform coin flip (passed arm64, failed
  x86). Both shares now average 5 independent predict() rounds, putting
  the bias gap near 3 sigma.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review June 10, 2026 19:13
@MaxGhenis
MaxGhenis merged commit e44df42 into main Jun 10, 2026
2 checks passed
@MaxGhenis
MaxGhenis deleted the calibrate-kernel branch June 10, 2026 19:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant