axiom

Pillar 04 · axiom.surface · axiom.diagnose

Map the dose–response

Effects saturate. Doubling a dose rarely doubles a response, and a linear model asked to describe a saturating world will get the optimum wrong in a predictable direction. The surface layer is where axiom keeps the actual shape — and keeps exactly one implementation of it, so the likelihood, the design maths, and the optimizer cannot drift apart.

The experiment

12 arms, chosen rather than guessed

Two treatments, each with a Hill kernel. The arms come from a rotatable central composite design over the dose box — a classical response-surface layout that spends its runs where curvature is estimable, instead of spreading them evenly and learning the shape badly everywhere.

Response to treatment a, with the truth overlaid

Blue is the posterior mean with its 90% interval; dashed grey is the curve the simulation actually used.

posterior mean truth

axiom.surface.response_band(fit, "a", mass=0.9) — every posterior draw pushed through forward, then summarised

Response to treatment b

A different reference dose and a different shape parameter, from the same fit.

posterior mean truth

axiom.surface.response_band(fit, "b", mass=0.9) · 12 arms, noise sd 0.5, Laplace backend, seed 0

Parameter recovery

The simulation knows the truth, so recovery is checkable rather than assertable. Every ported estimator in axiom ships with a recovery test like this one in the same commit.

Posterior mean and 90% highest-density interval against the parameter the data-generating process actually used.
parametertruthposterior meansd 90% HDIcontains truth
alpha 2.00 2.470.26 [2.02, 2.87] no
beta_a 10.00 12.171.44 [9.80, 14.47] yes
k_a 50.00 61.398.46 [47.68, 74.89] yes
s_a 2.00 1.630.20 [1.30, 1.96] no
beta_b 6.00 6.480.92 [4.97, 7.93] yes
k_b 12.00 19.803.24 [14.52, 24.98] no
s_b 1.50 1.640.29 [1.13, 2.07] yes
sigma 0.35 0.190.04 [0.12, 0.25] no

Only 4 of 8 intervals contain the truth — and that column is computed by the build, not typed in, so it cannot flatter the fit. This is not a failure to report quietly: it is the honest consequence of asking 12 arms to pin down seven structural parameters that trade off against one another. The curves above still track the truth closely across the dose range, because the combinations the data constrain are well determined even where the individual parameters are not. The next section is axiom saying so out loud.

The check that earns its keep

A good fit is not the same as an identified one

The curves above track the truth and the posterior predictive checks all pass. That is not sufficient. Several parameters are trading off against each other so strongly that the data cannot separate them — the fit is fine as a prediction and unreliable as an explanation, and those two uses call for different decisions.

condition no. of the posterior correlation structure — anything in the hundreds means a ridge, not a peak 1,043
ridge pairs beta_a ↔ k_a at ρ = +0.967 — amplitude against half-saturation, the classic saturating-curve trade-off ρ > 0.9
beta_a ↔ s_a at ρ = -0.931 ρ > 0.9
beta_b ↔ s_b at ρ = -0.901 ρ > 0.9
verdict weak-identification report failed

Posterior predictive checks all pass

Every summary statistic axiom checks sits inside its predictive interval, and the list of extreme statistics is empty. If model checking stopped at goodness of fit, this model would be signed off. weak_identification is the check that catches what a residual plot cannot.

Posterior predictive check. Every statistic sits inside its predictive interval; the list of extreme statistics is empty.
statisticobservedintervalp extreme
mean 10.110 [9.98, 10.30] 0.76no
sd 3.114 [3.00, 3.28] 0.92no
min 4.379 [3.97, 4.89] 0.94no
max 14.463 [14.32, 15.01] 0.32no
unit_sd 3.114 [3.00, 3.28] 0.92no

Acting on it

Where the optimum is, and what a budget buys

Given a fixed total dose to spend across both treatments, axiom finds the split that maximises the expected outcome — pushing the posterior through the same forward the likelihood used, not a linearised stand-in.

The response surface over both treatments

Expected outcome at every dose pair. The marked point is the allocation axiom chooses for a budget of 60.

axiom.surface.forward(doses, theta) over a 36×36 grid · allocation from axiom.surface.allocate(surface, posterior, budget=60)

chosen split a = 37.8, b = 22.2 — from the fitted posterior, which is all you would have in real work converged
oracle split a = 42.1, b = 17.9 — what you would choose knowing the true parameters 10.022
regret the outcome actually lost by acting on the fit instead of the truth 0.0917

Regret is the number that matters

The fitted allocation puts 37.8 on a where the oracle would put 42.1 — a visible gap in the parameters. The outcome cost of that gap is 0.0917 against an achievable 10.02. Near an optimum the surface is flat, so being somewhat wrong about where the peak is costs far less than being wrong about the shape. That is worth knowing before spending more on precision you will not use.

The budget frontier

Re-solving the allocation across budgets gives the frontier and, from its slope, the shadow price — what the next unit of budget is worth. It falls monotonically, which is saturation showing up where a decision-maker can actually use it.

Marginal value of the next unit of budget

Shadow price at each budget on the frontier.

axiom.surface.frontier(surface, theta, budgets=…, bounds=…) then .shadow_prices() · seven budgets, warm-started

Rule five, in the wild

When the answer does not exist, you get a refusal

Asking for the slope of the response at zero dose looks like a reasonable request. For a Hill curve with a shape parameter below one it is not: the slope at zero is infinite. Here is what axiom does with that.

Unsupported — returned, not raised, and not fudged

marginal_band(result, 'b', doses=0..40)

→ Unsupported: “marginal of 'b' is not finite in 396 of 96000 (draw, unit, period) cells; a kernel with shape s < 1 has infinite slope at zero dose”

It names the count and the cause. It does not return inf, it does not silently drop the offending draws, and it does not raise an exception that a caller might catch broadly and lose. The marginal curves on this page are computed on a grid that starts one step above zero — which is a choice the caller makes, having been told why.

What is in axiom.surface

88 public symbols. Seven response kernels including Hill, logistic, power, splines and a Gaussian process; four carryover families; nuisance terms for trend, seasonality and events; classical designs; the optimizer, frontier and steepest ascent; and the one forward they all go through.

Browse every symbol →

Next: everything you have ever measured

One study gives you one surface. A programme gives you dozens of studies of varying quality, which have to be pooled before they mean anything together.

Go to Pool →