mixle.reason.posterior_schema module

Semantic schema for IC-1 posteriors – the missing layer under the frozen Posterior protocol.

mixle.reason.posterior_protocol.Posterior fixes the mechanics of a posterior (.samples / .mean / .cov over a d-vector) but says nothing about what the d axes mean – their names, units, or coordinate space. That gap is a real, observed source of silent bugs: two posteriors can both “satisfy IC-1” while their axes mean different things (the concrete case that motivated this: G2’s invert_source reports (x, y, rate) while G6’s design_monitoring_network expects (x, y, log_rate, onset), and composing them needed a hand-written transform bridge that nothing validated – if a future edit reordered an axis or changed a unit, no error would fire; the numbers would just be wrong).

This module adds, additively and opt-in (it changes no existing posterior), three things:

  • PosteriorSchema / AxisSpec – declare what each latent axis is.

  • adapt() – convert a posterior from one schema’s convention to another’s (reorder, coordinate-transform under an explicit supported probability model, marginalize dropped axes), and raise loudly when a target axis has no source axis to build it from, instead of silently producing a wrong number. This is the general, validated replacement for hand-written bridges.

  • join_independent() – block-diagonally combine independent posteriors (e.g. an (x, y, rate) inversion posterior plus a separately-estimated onset belief with no cross-covariance to it) into one schematized posterior. This is the honest way to append a belief that genuinely isn’t in another posterior’s covariance – distinct from adapt(), which only rearranges information already present.

class AdaptationReceipt(transformation_model, transformed_axes, approximation_error, assumption)[source]

Bases: object

Assumption and numerical status of a coordinate-space adaptation.

Parameters:
  • transformation_model (str)

  • transformed_axes (tuple[str, ...])

  • approximation_error (float)

  • assumption (str)

class AxisSpec(name, unit='', space='linear')[source]

Bases: object

One latent axis’s meaning: the natural quantity name, its unit, and the coordinate space the posterior’s mean/cov are expressed in for this axis ("linear" or "log").

Example: G2 stores the release rate directly -> AxisSpec("rate", "kg/s", "linear"); G6 wants it log-transformed -> AxisSpec("rate", "kg/s", "log"). Same name/unit (it’s the same physical quantity), different space – which is exactly the information adapt() needs, together with an explicit probability model, to build the transform.

Parameters:
class PosteriorSchema(axes)[source]

Bases: object

An ordered list of AxisSpec – the semantic contract for a posterior’s d axes.

Parameters:

axes (tuple[AxisSpec, ...])

validate(mean, cov)[source]

Raise if mean/cov don’t match this schema’s arity – the check that turns a silent convention mismatch into a loud, early error.

Parameters:
Return type:

None

class SchematizedPosterior(mean, cov, schema, prior_dominated=None, adaptation_receipt=None)[source]

Bases: object

A Gaussian posterior (mean + cov) carrying its PosteriorSchema. Satisfies the IC-1 Posterior protocol (.samples / .mean / .cov / .credible_interval / .derived_quantity) so it’s a drop-in wherever a Posterior is expected, and additionally exposes .schema so downstream code can check conventions instead of assuming them.

Parameters:
  • mean (ndarray)

  • cov (ndarray)

  • schema (PosteriorSchema)

  • prior_dominated (bool | None)

  • adaptation_receipt (AdaptationReceipt | None)

adapt(mean, cov, source, target, *, transformation_model=None, prior_dominated=None)[source]

Convert a Gaussian posterior from source convention to target convention.

Matches target axes to source axes by name, applies any supported coordinate change under an explicit probability model, reorders to the target order, and marginalizes (drops) any source axis not in the target. Raises KeyError if a target axis has no source axis of the same name – the case that must never silently produce a wrong number, because the information to build that axis genuinely isn’t present (use join_independent() to append a belief that legitimately comes from elsewhere). Raises ValueError on a unit mismatch for a matched axis – same name, different unit is a real inconsistency, not an adaptation.

This is the general, validated replacement for hand-written convention bridges (e.g. SourcePosterior.to_doe_prior’s rate->log_rate step).

Parameters:
  • mean (ndarray)

  • cov (ndarray)

  • source (PosteriorSchema)

  • target (PosteriorSchema)

  • transformation_model (str | None)

  • prior_dominated (bool | None)

Return type:

SchematizedPosterior

join_independent(*blocks)[source]

Block-diagonally combine independent Gaussian posteriors into one schematized posterior.

Each block is (mean, cov, schema); the result stacks the means and places the covariances on the block diagonal (zero cross-covariance between blocks – the explicit statement that these beliefs are independent, which is exactly the honest way to append an axis like onset that a separate estimate provides and is genuinely uncorrelated with the rest, rather than pretending a single posterior’s covariance contained it). Raises on a duplicate axis name across blocks.

Parameters:

blocks (tuple[ndarray, ndarray, PosteriorSchema])

Return type:

SchematizedPosterior