axiom

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

10.00

the value the data-generating process actually used

What the confounded fit believes

18.64

± 3.25 — which puts the truth 2.7 posterior standard deviations away. The model is not uncertain. It is wrong.

The randomized measurement

5.322
5.218 5.427

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.

uncalibrated fit after calibration truth

axiom.calibrate.derive_prior(…) and axiom.calibrate.fit_calibrated(spec, panel, [measurement]) · Laplace backend, 1,000 draws, seed 1

The same three fits, judged two ways: how close the amplitude parameter lands, and whether the model’s own realization of the experiment’s estimand agrees with what the experiment measured.
fitamplitudesd from truth realizes the estimand asz vs measurementverdict
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.

design factor the fraction of the measured contrast attributable to the amplitude parameter at this dose and window 0.5367
implied prior on beta_a, moment-matched to 9.916 ± 0.099 lognormal
route taken the likelihood route records itself in provenance as likelihood, with constraint 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.

plan status downgraded — the estimands differ on window, level not clean
window only 54.7% of the carryover mass lands in the first period, so the steady state is larger by the reciprocal ×1.8269
level individual to cluster, at an intra-cluster correlation of 0.05 — the point estimate scales, and so does its standard error se ×0.5362
resolved the corrections together license a factor of 1.8269 licensed

Experiment, read as the decision’s estimand

38.89

± 0.21 per cluster, after applying the window and level corrections

What that estimand actually is

32.00

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.

ledger: 2 line(s) 1. facet:window carryover_contained [unverified]: 0.54737 of the carryover mass lands within 1 period(s); scaled to the steady-state total by 1.82692 0.5473701838211292 -> 0.9999999999999999 2. facet:level linear_aggregation [unverified]: individual read at cluster level: point factor 1 (additive effect), SE factor 0.53619 1.0 -> 0.5361902647381804
facetassumptionstatecounterfactual 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.

Browse every symbol →

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.

Go to Surface →