bayesmithDocumentation0.10.0 source

bayesmith.exact.discrete

Source-derived signatures and contracts. See the reference index for coverage and conventions.

Module contract
Exact marginalisation of declared discrete latents, by enumeration.

:attr:`~bayesmith.graph.nodes.Probabilistic.support` has carried
``Discrete(n)`` since P1, and its docstring said plainly that nothing read it:
it was "recorded here so the declaration exists from the start". This module is
what reads it, which is what makes the declaration load-bearing rather than
decorative.

**What this computes.** For a graph whose latents are some discrete sites and
some others, the marginal ``log sum_z exp(log_joint(graph, {..., z}))``, summing
over every joint assignment of the declared discrete sites and conditioning on
whatever else the caller supplies. Exact, not approximate: no sampling, no
bound, and no dependence on a random key.

**Why only the exact half.** ``numpyro`` already ships
``DiscreteHMCGibbs`` and ``MixedHMC`` for sampling discrete sites, and the
migration spec is explicit that P4 need only write the exact-marginalisation
side. It could not have been borrowed either way: numpyro's enumeration route
goes through ``numpyro.contrib.funsor``, and ``funsor`` is not a dependency
here -- measured, it does not import in this venv.

**The cost is exponential and is therefore stated, not hidden.** A site with
``n`` states over ``c`` coordinates contributes ``n ** c`` assignments, and the
sites multiply. That is the price of an exact answer rather than a defect, and
the only dishonest thing to do with it is to begin the sum and hope: a caller
who sees a refusal naming 16777216 can raise the budget deliberately, while one
watching a hung process cannot tell it from a bug. So :func:`enumeration_states`
is computable before any arithmetic runs and :data:`ENUMERATION_BUDGET` bounds
it.

**What this does NOT do, and where the next piece goes.** A plated discrete
site is counted honestly at ``n ** plate_size`` and will therefore hit the
budget quickly -- for a mixture assignment per data point, almost immediately.
That is the correct cost *for this algorithm* and the wrong algorithm for that
model: conditionally independent assignments factorise, so the sum is
``size * n`` terms rather than ``n ** size``, and a chain of assignments is
``T * n**2`` by forward-backward rather than ``n ** T``. Both are structural
specialisations of exactly the kind this package dispatches on, and both are
P4's remaining half. Enumeration is the general fallback they must agree with,
and is deliberately built first so that they have an oracle to be checked
against.

ENUMERATION_BUDGET

Defined in bayesmith.exact.discrete, line 66.

ENUMERATION_BUDGET: int = 65536

discrete_latents

Defined in bayesmith.exact.discrete, line 77.

discrete_latents(graph: Graph) -> tuple[str, ...]
The latent nodes declaring :class:`~bayesmith.graph.nodes.Discrete`.

In graph order, which is topological, so the enumeration below is
reproducible rather than set-ordered.

Three exclusions, each of which would be a different wrong answer:

* **Observed nodes.** They carry data. Summing a node over states it does
  not occupy replaces the likelihood with something else entirely.
* **Continuous latents.** Conditioned on, not summed.
* **Latents with no declared support.** ``None`` is not a claim of
  continuity and not a claim of discreteness -- the field's own docstring
  says it must be ineligible for any support-specific method. So such a
  node is not enumerated, and, having no value either, it is refused by
  the graph's existing check, which names it.

enumeration_states

Defined in bayesmith.exact.discrete, line 114.

enumeration_states(graph: Graph) -> int
How many joint assignments enumeration would visit. Exact, and cheap.

One when there are no discrete latents -- the empty product. That is not a
quibble: a sum over zero assignments returns ``-inf``, which is a silent
and entirely wrong answer for a graph that simply has nothing to
marginalise.

marginal_log_likelihood

Defined in bayesmith.exact.discrete, line 182.

marginal_log_likelihood(graph: Graph, values: Mapping[str, Any] | None=None, *, budget: int=ENUMERATION_BUDGET) -> jax.Array
``log sum_z exp log_joint``, summing every declared discrete latent out.

Args:
    graph: the graph. Its discrete latents are summed over; every other
        latent must have a value in ``values``, exactly as ``log_joint``
        requires, and is conditioned on rather than marginalised.
    values: values for the latents that are NOT being marginalised.
    budget: most assignments to visit -- see :data:`ENUMERATION_BUDGET`.

Raises:
    ValueError: if the assignment count exceeds ``budget``. Raised before
        any density is evaluated, so the cost is a refusal rather than a
        wait.
    GraphError: from ``log_joint``, if a latent that is not being
        marginalised has no value. Deliberately not re-raised here: the
        graph's own message already names the node and lists the latents,
        and a second wording would be a second thing to keep true.

posterior_marginals

Defined in bayesmith.exact.discrete, line 210.

posterior_marginals(graph: Graph, values: Mapping[str, Any] | None=None, *, budget: int=ENUMERATION_BUDGET) -> dict[str, jax.Array]
Per-site posterior probabilities over the states, exactly.

``{site: probabilities}`` where each array has the site's plate shape with
the ``n`` states as its LAST axis, so ``result["z"][..., k]`` is
``P(z = k | data)`` and the axis a caller reduces over is the one numpy
conventions put last.

Computed from the same normalised terms as
:func:`marginal_log_likelihood`, so the two cannot disagree about the
normaliser -- the shape of defect this package records as B1, where two
halves of one Gaussian disagreed about which covariance they described.

Type a concept or API name.