mixle.inference.node_report module

Node report protocol – per-subtree diagnostics over a composed distribution tree (workstream D1).

Frame (see the ConditionalJIT track, D1-D6): the estimator tree is an IR. Every node – a leaf distribution or a combinator subtree (Composite/Mixture/Sequence/Conditional/Optional/…) – can report its own residual, its Q-gain (Neal-Hinton free-energy lower-bound improvement), its E/M cost, its update kind, and cheap health receipts. Later track items (freeze/roll-up caching, a block-EM scheduler, leaf hot-swapping, backend re-specialization, a learned controller) all read these reports; none of them may change what a fit computes – only how it is scheduled – so this module never runs its own EM, it only instruments/observes the existing machinery in mixle.inference.em and mixle.inference.estimation.

Design choices (documented here because later D-track items depend on this interface):

  • residual – a Monte-Carlo estimate of this node’s own negative log-density, -mean(log_density(x)) over samples drawn from the node’s own current fit (node.sampler().sample(n)). This is available at EVERY node generically (every ProbabilityDistribution has sampler and log_density) without needing per-combinator data slicing (a leaf under a CompositeDistribution never sees the top-level tuple directly, so an exact real-data residual cannot be computed generically for an arbitrary subtree without reimplementing every combinator’s data-projection rule – out of scope for an S-effort generic dispatcher). For an exponential-family leaf this MC residual converges to the differential/Shannon entropy of the fit; it is a legitimate “how much spread/uncertainty is still in this subtree’s fit” signal, not a real-data fit residual. Callers who want a real-data top-level residual can pass data/enc_data and read the root row, whose residual/Q-gain instead comes from the actual EM objective (see root_em_report()).

  • Q-gainresidual_before - residual_after for the SAME field path across two flat_report_table() calls that bracket one EM update (pass the earlier table as prev_table). This is a genuine before/after delta of the tracked residual, not invented. The Neal-Hinton guarantee (coordinate ascent on one computable free energy F) is proven only for the actual tracked EM objective at the TOP level (see mixle.inference.em’s run_em / mixle.inference.estimation’s optimize, whose delta-gated loop never accepts a decreasing step); per-node Q-gain is a diagnostic decomposition, not individually guaranteed non-negative (a mixture EM step can, e.g., grow one component’s spread while improving the joint objective). root_em_report() verifies the real, provably-monotone quantity.

  • E/M cost – a parameter-count x dataset-size proxy (nobs if supplied, else 1): E-step cost ~ param_count * nobs (every observation is scored against every parameter once), M-step cost ~ param_count for a closed-form/conjugate update, ~ param_count * _GRADIENT_STEPS for a gradient-based estimator, 0 for a frozen/no-op node. Proxy, not wall-clock – documented so D2’s freeze/roll-up cache and D3’s scheduler can decide whether to replace it with a measured cost later.

  • update kind – derived from the node’s own estimator/capabilities: "frozen" (the Neutral capability – a Null distribution/accumulator/encoder), "em" (latent-variable nodes exposing seq_posterior or the LatentStructured capability – the same duck-type mixle.inference.em itself uses for PosteriorTransformEM), "conjugate_closed_form" (the ConjugateUpdatable capability), "gradient" (estimator class name signals a gradient/neural/torch optimizer), else "closed_form" (the common one-shot M-step case).

  • health receipts – reuses the near-degenerate-variance check already used by mixle.inference.precision_plan (sigma2/variance below a floor), adds a generic NaN sweep over the node’s own numeric attributes (-inf is deliberately NOT flagged – it is a legitimate log-space encoding of a zero-probability event, e.g. a categorical’s log_default_value; only NaN is never a legitimate parameter value), and an ill-conditioning check (cond on any square 2D numeric attribute whose name suggests a covariance/precision matrix) – a minimal, honest version where nothing richer already exists in the codebase for a given family.

class NodeReport(field_path, node_type, update_kind, residual, q_gain, e_step_cost, m_step_cost, param_count, health=<factory>)[source]

Bases: object

Per-node diagnostics for one point in a composed distribution tree.

field_path follows the existing EnumerationError path convention used throughout the combinators (e.g. "MixtureDistribution.components[0] -> CompositeDistribution.dists[1]"), so reports compose with the rest of the codebase’s structural-error/debugging vocabulary.

Parameters:
property is_healthy: bool

True iff every boolean health receipt that flags a problem is False.

node_report(dist, *, field_path='root', n_mc=_DEFAULT_MC_SAMPLES, seed=0, nobs=None, prev_residual=None)[source]

Return a NodeReport for a single node (leaf or combinator subtree).

Dispatches generically over the five-piece contract / capability lens – no per-family branching.

Parameters:
  • dist (ProbabilityDistribution)

  • field_path (str)

  • n_mc (int)

  • seed (int)

  • nobs (float | None)

  • prev_residual (float | None)

Return type:

NodeReport

walk_tree(dist, *, path='root')[source]

Return (field_path, node) for every node in the tree, pre-order, deduplicated by identity.

Parameters:
  • dist (ProbabilityDistribution)

  • path (str)

Return type:

list[tuple[str, ProbabilityDistribution]]

flat_report_table(dist, *, nobs=None, prev_table=None, n_mc=_DEFAULT_MC_SAMPLES, seed=0)[source]

Walk a composed tree and return one NodeReport per node, in traversal order.

Satisfies “composed tree -> flat table”: every leaf and every combinator subtree gets exactly one row, keyed by its field_path. Pass the previous call’s return value as prev_table to populate each row’s q_gain as the residual improvement across an intervening EM update.

Parameters:
  • dist (ProbabilityDistribution)

  • nobs (float | None)

  • prev_table (list[NodeReport] | None)

  • n_mc (int)

  • seed (int)

Return type:

list[NodeReport]

root_em_report(enc_data, estimator, model, *, engine=None, max_its=1)[source]

Run max_its real EM steps (via mixle.inference.em.run_em()) and return (new_model, objective_before, objective_after) on the ACTUAL tracked Neal-Hinton objective (observed log-likelihood / MAP / VB, whichever run_em resolves) – the one quantity this module does not approximate. objective_after >= objective_before is the real, provably monotone Q-gain guarantee; NodeReport’s per-node q_gain is a diagnostic decomposition, not individually guaranteed non-negative (see module docstring).

Parameters:
  • enc_data (Any)

  • estimator (Any)

  • model (ProbabilityDistribution)

  • engine (Any | None)

  • max_its (int)

Return type:

tuple[ProbabilityDistribution, float, float]