Pillar 03 · axiom.calibrate
Fold the experiment in
You have an observational model fitted to everything you have ever collected, and it is confounded. You also have one clean randomized result, measured on a narrow slice. Neither is what you need on its own. Calibration is the operation that makes the second correct the first — and the part that usually goes unrecorded is what that correction assumed.
The setup
A model that is confidently wrong
4 units over 24 periods, with a saturating dose–response and geometric carryover. Something unobserved drives both the dose and the outcome — the correlation between them is 0.96 — so the fitted amplitude comes out far too large, and the posterior is tight enough to be confident about it.
True amplitude
the value the data-generating process actually used
What the confounded fit believes
± 3.25 — which puts the truth 2.7 posterior standard deviations away. The model is not uncertain. It is wrong.
The randomized measurement
randomized_contrast · source rct-2026Q1 · target estimand 4760098b3929…
The measurement knows what it measured
It is not a loose number with a standard error. It carries the estimand it is an estimate of — dose, reference, window, population, level — hashed. That is what lets axiom check later whether the experiment and the decision are even about the same quantity. On this page they are not, and that turns out to matter.
Two routes
Through the prior, or through the likelihood
axiom offers both, because they are not the same operation and they do not agree. The prior route converts the measurement into a prior on the amplitude and refits. The likelihood route adds the measurement to the log density as a constraint, so it competes with the observational data instead of preceding it.
Fitted amplitude under each route
Both routes start from the same confounded fit and pull it toward the truth by different amounts.
axiom.calibrate.derive_prior(…) and axiom.calibrate.fit_calibrated(spec, panel, [measurement]) · Laplace backend, 1,000 draws, seed 1
| fit | amplitude | sd from truth | realizes the estimand as | z vs measurement | verdict |
|---|---|---|---|---|---|
| uncalibrated | 18.637 | +2.7 | 9.782 | -4.29 | disagrees |
| prior route | 9.953 | -0.5 | 7.318 | -2.82 | disagrees |
| likelihood route | 15.059 | +1.9 | 5.468 | -0.64 | agrees |
The two routes disagree, and axiom does not hide it
The prior route recovers the amplitude almost exactly (9.95 against a truth of 10) but its realization of the estimand still disagrees with the measurement at z = -2.82. The likelihood route is the reverse: its amplitude is still high at 15.06, but it agrees with the experiment at z = -0.64. Constraining a parameter and constraining a prediction are different requests, and which one you want depends on what you intend to do with the model. axiom implements both and reports the disagreement rather than picking for you.
What the prior route derived
Turning a measurement of a contrast into a prior on a kernel parameter is not division. It goes through the design factor — the share of the contrast that the amplitude is responsible for at the tested dose — and every step writes its assumption down.
first_period_lift@rct-2026Q1
provenance
The hard part
The experiment measured one thing. You need another.
The experiment measured a first-period lift for an individual. The decision needs a steady-state per-period lift for a cluster. Those are different numbers. axiom compares the two estimands facet by facet, names the window, level facets that differ, and refuses to call the transfer clean.
Experiment, read as the decision’s estimand
± 0.21 per cluster, after applying the window and level corrections
What that estimand actually is
the simulation’s truth. The transfer lands above it, and the ledger below names the assumption responsible.
The transfer overshoots — and the ledger says which assumption did it
Reading the experiment through the transfer gives 38.89 per cluster. The truth is 32.00. The calibrated model’s own forward pass gives 38.27, so both routes overshoot in the same direction and by a similar amount.
That is not a bug being hidden — it is the point of
keeping a ledger. The window correction assumes the accumulation is
linear in the carryover mass. With a saturating dose–response it is not, so
scaling a first-period reading up to steady state overstates it. The assumption is
recorded as carryover_contained in state unverified, which
is exactly the line a reviewer should challenge first.
The ledger, in full
Every correction applied to move a number from one estimand to another leaves a line: which facet it touched, what it assumed, whether that assumption has been verified, and the factor it contributed.
| facet | assumption | state | counterfactual | value |
|---|---|---|---|---|
| window | carryover_contained | unverified | 0.54737 | 1.00000 |
| level | linear_aggregation | unverified | 1.00000 | 0.53619 |
axiom also checks the ledger for completeness against the plan: the facets covered are {'level': 1, 'window': 1}, and the check returns identified. A transfer that quietly skipped a differing facet would fail that check rather than producing a number.
What is in axiom.calibrate
31 public symbols. Measurements and their estimands, both calibration routes, agreement tests, prior derivation with the design factor, the correction algebra for each facet, and the ledger that records all of it.
Next: the shape of the response
Calibration corrected the amplitude. The surface is what that amplitude is an amplitude of — the dose–response curve, its optimum, and where the next probe should go.