mixle.reason.core module

Reasoning front door for fusing modality evidence into a belief.

reason(prior, evidence) folds a sequence of linear-Gaussian observations into a belief state by exact Kalman assimilation, tracking how many nats of uncertainty each modality removed. The returned ReasonedAnswer is the posterior belief plus the tools a scientific answer needs: credible intervals, per-modality attribution, and an epistemic/aleatoric split of any prediction.

class Latent[source]

Bases: object

Factories for the shared latent prior used at the start of assimilation.

static gaussian(mean, cov)[source]

A Gaussian prior N(mean, cov) over the latent.

Parameters:
Return type:

GaussianBelief

static vector(dim, *, mean=0.0, var=1.0)[source]

An isotropic Gaussian prior over a dim-vector latent: N(mean*1, var*I).

Raises:

ValueError – if dim is not an exact positive integer (MXR-080-0274) – a fractional dim used to be silently truncated with int() rather than rejected.

Parameters:
Return type:

GaussianBelief

static mechanistic(A, steps, *, x0_mean=None, x0_cov=None, process_cov=None)[source]

Return a linear-dynamics prior over z_0 .. z_{steps-1}.

The trajectory follows z_{t+1} = A z_t + w_t with w_t ~ N(0, Q). The returned belief is the joint Gaussian over the stacked trajectory (steps * d,). Because the states are coupled, evidence at one time can inform other times through the dynamics, so fusing observations via reason() performs exact Kalman smoothing for this linear-Gaussian model.

Parameters:
  • A (Any) – (d, d) linear state-transition operator (one discrete step).

  • steps (int) – number of time steps T in the trajectory.

  • x0_mean (Any) – mean of z_0 (default zeros).

  • x0_cov (Any) – covariance of z_0 (default identity).

  • process_cov (Any) – process-noise covariance Q (default zeros – deterministic dynamics).

Raises:

ValueError – if steps is not an exact positive integer; if x0_mean doesn’t have shape (d,) or isn’t finite; or if x0_cov / process_cov isn’t a finite, symmetric, positive-semidefinite (d, d) covariance (MXR-080-0274) – previously steps was silently truncated with int() and the covariances were never validated at all, so a malformed one either propagated silently into the joint prior or surfaced only as a confusing low-level error far from its actual cause.

Return type:

GaussianBelief

class LinearGaussianEvidence(H, y, R, name='')[source]

Bases: object

One modality’s evidence about the latent z: y = H z + noise, noise ~ N(0, R).

H is the (possibly linearized) forward operator mapping the latent to this modality’s measurement space, y the observed data, R its noise covariance (matrix, diagonal, or scalar). Application forward models (e.g. mixle_pde geophysics operators) produce these.

Parameters:
Evidence

Short alias – Evidence(H, y, R, name).

class NonlinearEvidence(h, y, R, jacobian=None, iterations=2, name='')[source]

Bases: object

One modality’s evidence through a nonlinear forward model.

Assimilated by (iterated) extended-Kalman linearization: at the current belief mean m the forward is replaced by its tangent h(z) ~ h(m) + J(m)(z - m) and the exact linear update runs on that tangent; with iterations > 1 the linearization point is refined at the updated mean and the update repeats from the pre-update belief, which matters when the prior mean is far from the truth. jacobian is analytic when you have it; otherwise a central finite difference is used. This is a Gaussian approximation around the linearization point; for strongly multimodal posteriors it reports one mode’s belief, not the full mixture.

Parameters:
block_selector(step, n_blocks, block_dim, within=None)[source]

An observation matrix that reads time-block step of a stacked trajectory latent.

For a latent built by Latent.mechanistic() (shape (n_blocks * block_dim,)), returns the H selecting block step – use it to build LinearGaussianEvidence for an observation at that time. within optionally reads only part of the block (a (k, block_dim) local readout); by default the whole block is read (identity).

Raises:

ValueError – if n_blocks / block_dim is not an exact positive integer; if step is not an exact integer or falls outside [0, n_blocks); or if within doesn’t have block_dim columns (MXR-080-0274). step used to accept Python-style negative indexing by accident (silently selecting a DIFFERENT, in-range block with no signal that anything unusual happened) while a step at or beyond n_blocks fell through to a confusing low-level “could not broadcast” error instead of a clear one.

Parameters:
Return type:

ndarray

class ReasonedAnswer(belief, prior, trace, *, full_prior=None, evidence=None, query_idx=None)[source]

Bases: object

A posterior belief about a query, with the UQ a scientific answer needs.

Beyond mean / interval / entropy (delegated to the belief), it exposes attribution() – the nats of uncertainty each modality removed – and predict(), which splits a prediction’s uncertainty into epistemic (from latent uncertainty) and aleatoric (observation noise) via the law of total variance.

Parameters:
  • belief (BeliefState)

  • prior (BeliefState)

  • trace (list[tuple[str, BeliefState]])

  • full_prior (BeliefState | None)

  • evidence (list[Any] | None)

  • query_idx (Any)

property mean: ndarray

Return posterior mean from the underlying belief.

cov()[source]

Return posterior covariance from the underlying belief.

Return type:

ndarray

sd()[source]

Return posterior standard deviations from the underlying belief.

Return type:

ndarray

entropy()[source]

Return posterior entropy from the underlying belief.

Return type:

float

interval(level=0.9)[source]

Per-coordinate central credible interval at level (an (d, 2) array of [lo, hi]).

Parameters:

level (float)

Return type:

ndarray

information_gain()[source]

Total nats of uncertainty the evidence removed from the prior (H[prior] - H[posterior]).

Both entropies are always taken from this answer’s OWN coordinate space – the full prior for an unqueried answer, or that prior’s matching marginal after marginal() – so querying a coordinate the evidence never touched correctly reports zero gain (MXR-080-0272), rather than a full-state prior entropy mixed against a lower-dimensional posterior marginal’s.

Return type:

float

attribution(*, normalize=False, method='sequential', **kwargs)[source]

Per-modality information gain in nats – which modality sharpened the belief, and by how much.

Two allocation rules are available, and they generally disagree for REDUNDANT sources (two items that both constrain overlapping coordinates) – pick deliberately, not by default (MXR-080-0275):

  • method="sequential" (default): each item’s nats are H[belief before it] - H[belief after it], in the order evidence was given to reason(). Cheap (one pass over the fold trace) and always sums exactly to information_gain(), but for redundant sources it is a CONDITIONAL, fold-order-dependent credit split, not an intrinsic measure of modality importance: whichever redundant source is folded first is credited with uncertainty a different order would have credited to another source. The FINAL POSTERIOR does not depend on order for linear-Gaussian evidence (sequential exact Kalman assimilation commutes – see reason()); this per-source SPLIT of it still does. Treat the result as “credit under the order evidence happened to be listed in,” not as “how important is this modality.”

  • method="shapley": the order-INVARIANT Shapley value – each source’s nats averaged over (up to) every permutation of fold order, satisfying the same efficiency property (values sum to information_gain()) without depending on which order the caller listed evidence in. Exact for up to _SHAPLEY_EXACT_MAX_SOURCES sources (O(n!) re-folds of the full evidence sequence); beyond that it averages n_permutations uniformly random permutations instead (O(n_permutations * n) re-folds), a standard permutation-sampling Monte Carlo estimator whose standard error shrinks as O(1 / sqrt(n_permutations)). Pass n_permutations= / rng= to control the sampling estimate. Each re-fold repeats the full (possibly nonlinear, possibly expensive) assimilation from the original prior, so this costs O(n) to O(n!) times a single reason() call – fine for the small modality counts this module is used with, not intended for hundreds of sources.

With normalize=True, values are the fraction of the total gain (they then sum to ~1), computed after the chosen allocation rule.

Parameters:
Return type:

dict[str, float]

predict(H, R=0.0)[source]

Split the uncertainty of a new prediction y* = H z + noise(R) (law of total variance).

epistemic = diag(H P Hᵀ) (from the latent’s remaining uncertainty, reducible by more data) and aleatoric = diag(R) (irreducible observation noise). Exact for the Gaussian belief.

Parameters:
  • H (Any) – (k, d) observation operator, where d is this belief’s own latent dimension. Its shape must match exactly – it is never reshaped from a looser guess, since a wrong-width H can have a total element count that happens to divide evenly by d and so silently describe a DIFFERENT (still valid-looking) operator instead.

  • R (Any) – the prediction’s noise covariance: a scalar (shared by all k outputs), a length-k vector of per-output variances, or a (k, k) covariance matrix (symmetrized, and required finite and positive semi-definite).

Raises:

ValueError – if H’s width doesn’t match this belief’s latent dimension, or R is the wrong shape, non-finite, or not a valid (non-negative / positive semi-definite) noise covariance (MXR-080-0273).

Return type:

UncertaintyDecomposition

marginal(indices)[source]

Restrict the answer to a subset of latent coordinates (query a specific variable).

The prior and every fold step’s before/after belief are re-marginalized to the SAME coordinates as the posterior (MXR-080-0272), so information_gain() and the default attribution() on the result are computed entirely within the queried subspace – a coordinate the evidence never touched now correctly reports zero gain instead of leaking in the full-state prior entropy. indices are local to THIS answer’s own coordinates (so repeated/nested marginal() calls compose correctly); the original full-space prior and evidence are carried through unchanged so attribution(method="shapley") keeps working after marginalizing.

Parameters:

indices (Any)

Return type:

ReasonedAnswer

reason(prior, evidence, *, query=None)[source]

Fuse evidence into prior by exact Kalman assimilation; return the queried posterior.

Parameters:
  • prior (Any) – the latent’s prior belief (GaussianBelief; build one with Latent).

  • evidence (Any) –

    a sequence of LinearGaussianEvidence and/or NonlinearEvidence – one per modality / observation. Nonlinear items assimilate by iterated-EKF linearization (a Gaussian approximation; see NonlinearEvidence). They are folded in one at a time, and the nats each removes are recorded for ReasonedAnswer.attribution().

    Order independence holds only when every item is LinearGaussianEvidence: sequential exact Kalman assimilation of linear-Gaussian observations commutes, so folding them in one at a time then equals conditioning on all of them at once, in any order. That guarantee does NOT extend to NonlinearEvidence: each nonlinear item linearizes at the CURRENT belief mean, which itself depends on whatever was already folded in before it. Mixing nonlinear evidence with anything else – other nonlinear evidence included – therefore makes both the posterior and the per-source ReasonedAnswer.attribution() depend on the order evidence is given in (a RuntimeWarning is raised when this applies); pass evidence in a fixed, deliberate order rather than relying on permutation invariance.

    The POSTERIOR’s order independence does not extend to attribution()’s default credit split, even for pure LinearGaussianEvidence (MXR-080-0275): two sources that both constrain overlapping coordinates (“redundant” evidence) split their combined information gain differently depending on fold order, because the default allocation is sequential (whichever redundant source is folded first is credited with uncertainty a different order would credit to the other). Use attribution(method="shapley") for an order-invariant split when that matters more than the cost of recomputing it.

  • query (Any) – optional latent coordinate indices to restrict the answer to.

Returns:

A ReasonedAnswer – the posterior belief plus attribution and prediction UQ.

Return type:

ReasonedAnswer