Plates¶
Indexed draws and observations. A
PlateDraw draws one value per
element of a finite set, as sample v : A <- F(...) does in source; a
VectorisedObserve
scores a column of responses at once, as an indexed observe does. The
marginalization helpers integrate a finite latent out of a plate of
scores.
plate
¶
Hierarchical Bayesian modelling primitives.
Adds the operations needed to express models of the kind found in
hierarchical Bayesian regression / latent-class analysis (the
canonical Stan workhorse): finite-domain-indexed draws (plates),
vectorized observations with gather indexing, LKJ priors on
correlation matrices, ordinal monotone splines via cumulative
sum, and generic distribution truncation. Each primitive is
declared with its categorical denotation in :math:\mathbf{Kern};
the runtime is a straight realisation of those denotations.
Categorical foundations
- Plate (indexed draw). Given a finite index set :math:
Aand a parameterized family :math:F : \Theta \to \mathcal{G}(B), a plate draw declares the kernel
.. math::
v : A \to B,\quad v(a) \sim F(\theta(a))
equivalently a single morphism into the function space
:math:\mathbf{1} \to \mathcal{G}(B^A) factoring as the
independent product :math:\prod_{a \in A} F(\theta(a)). Under
the natural isomorphism
:math:\mathbf{Kern}(\mathbf{1}, B^A) \cong \mathbf{Kern}(A, B)
the plate variable IS a :math:\mathbf{Kern}-morphism
:math:A \to B; in the variational backend it is held as a
tensor of shape (|A|, *B.shape) whose prior contribution to
the ELBO is the per-row log-density.
-
Gather. Given a finite fibration :math:
\iota : N \to A(a per-observation grouping assignment) and a plate variable :math:v : A \to B, the gathered morphism is the pullback :math:\iota^* v = v \circ \iota : N \to B. Categorically a textbook reindexing morphism in :math:\mathbf{Kern}; runtime isv[indices]along the leading axis. -
Vectorised observe. A batched observation step
.. math::
\mathcal{S}\llbracket\,\mathsf{observe}\ r[n] \sim
F(\theta[n])\ \mathsf{for}\ n\in N\,\rrbracket :
\Phi \to \mathcal{G}_{\le 1}(\Phi)
has score :math:\prod_{n \in N} p_F(r_{\text{obs}}(n);\,
\theta(n,\phi)). The single-observation form is the
|N| = 1 special case; mass-correctness follows from the
Cho-Jacobs Markov-with-conditioning calculus.
- Marginalise (program-level). Given a previously-drawn
discrete latent :math:
c : \Phi \to \mathcal{G}(C), the marginalisation step is the pushforward through the projection :math:\pi_{\Phi \setminus C} : \Phi \times C \to \Phi:
.. math::
\mathsf{marg}(c) = \mathcal{G}(\pi_{\Phi \setminus C})
\circ \mathcal{S}\llbracket\mathsf{draw}\ c\rrbracket
numerically realised as :math:\log \sum_c \exp(\cdot) over
the :math:C axis of the per-class log-likelihoods.
-
cumsum. The deterministic morphism :math:
\mathrm{cumsum} : \mathrm{Euclidean}(K) \to \mathrm{Euclidean}(K), :math:(x_1, \dots, x_K) \mapsto (x_1, x_1 + x_2, \dots, \sum_i x_i). Used for the standard monotonic-spline parameterization of ordinal coefficients. -
LKJ correlation prior. The Lewandowski-Kurowicka-Joe distribution :math:
\mathrm{LKJ}(K, \eta)on the manifold :math:\mathrm{Corr}_Kof :math:K \times Kcorrelation matrices, parameterized via the Cholesky factor for numerical stability. The accompanyingcholesky_quad_formdeterministic morphism reconstructs a full covariance :math:\Sigma = \mathrm{diag}(s)\, R\, \mathrm{diag}(s)from a correlation :math:Rand a per-component scale :math:s \in \mathrm{PositiveReals}(K). -
Truncated. Given a base family :math:
Fand an interval :math:[a, b], the truncated family :math:F_{|[a, b]}has density :math:p_F(x) / (F_{\text{cdf}}(b) - F_{\text{cdf}}(a))on :math:[a, b]and zero elsewhere. Realised via inverse-CDF sampling when available; rejection otherwise.
References
- Cho, K. and Jacobs, B. (2019). Disintegration and Bayesian inversion via string diagrams. Mathematical Structures in Computer Science 29(7), 938–971. doi:10.1017/S0960129518000488.
- Lewandowski, D., Kurowicka, D. and Joe, H. (2009). Generating random correlation matrices based on vines and extended onion method. Journal of Multivariate Analysis 100(9), 1989–2001. doi:10.1016/j.jmva.2009.04.008.
PlateDraw
¶
PlateDraw(index_size: int, family: ContinuousMorphism, domain: AnySpace | None = None)
Bases: ContinuousMorphism
A finite-domain-indexed draw, as a Kern-morphism A → B.
Concretely: v : A → B ~ F(theta) becomes a tensor of shape
(|A|, *B.shape) whose a-th row is an independent
:math:F(\theta_a)-distributed random variable. The variational
posterior factorizes across rows by default; the prior's ELBO
contribution is :math:\sum_a \log p_F(v_a; \theta_a).
Categorically: by the natural isomorphism
:math:\mathbf{Kern}(\mathbf{1}, B^A) \cong \mathbf{Kern}(A, B),
the plate variable IS a Kern-morphism :math:A \to B. The
PlateDraw is realised as a ContinuousMorphism whose
codomain is the flat product-space of index_size copies of
the per-row family's codomain.
| PARAMETER | DESCRIPTION |
|---|---|
index_size
|
Cardinality :math:
TYPE:
|
family
|
Per-row distribution family.
TYPE:
|
domain
|
The program's input space (broadcast conditioning).
TYPE:
|
Source code in src/quivers/continuous/plate.py
rsample
¶
Reparameterized sample from the plate's prior.
A plate draw is batch-invariant: the latent vector is a
global model quantity shared across every row of an observed
plate. The returned tensor has shape (|A|, *B.shape)
regardless of the program input's leading batch dimension,
with each row drawn independently from the per-row family
F at its declared parameters. x supplies only the
device; it is optional so batch-invariant callers such as
gather need not fabricate one.
This is the standard Pyro / NumPyro semantic: a sample inside
a plate("subj", n_subj) context is one (n_subj,)
vector, not a per-particle (batch, n_subj) replication. The
gather arr[idx] along the plate axis then composes
cleanly with per-row observed-plate axes downstream.
Source code in src/quivers/continuous/plate.py
canonical_latent
¶
Give a clamped plate value its structured (|A|, *B.shape).
A plate latent supplied as a conditioning / observed value may
arrive flattened to (|A| * prod(B.shape),) (the shape a
test point or host-data channel produces when it flattens a
matrix-valued latent). Restoring the structured shape here lets
a gather v[idx] over the plate axis yield (N, *B.shape)
with the per-row event axis intact.
A value whose element count is not exactly |A| * prod(B) is
a genuinely batched latent (a leading parameter-sample axis);
it is returned unchanged so the batched scoring path handles
it.
Source code in src/quivers/continuous/plate.py
log_prob
¶
Prior log-density of the plate sample.
The plate variable v : A -> B scores as the independent
product prod_a p_F(v_a) of the per-row family F; the
contribution is sum_a log p_F(v_a).
Two layouts are accepted. The natural plate latent carries a
leading axis of length |A| (or is a flat vector whose
element count is |A| * prod(event)): its rows are scored
under F and reduced to a scalar, wrapped in a length-1
tensor so the downstream log-joint accumulator sees a uniform
1-d shape it can broadcast against the response plate. A
batched value carries a genuine leading batch axis distinct
from |A|; each batch element is scored independently and a
(batch,) vector of per-element densities is returned.
The per-row event size is read from the value's element count
rather than solely from the family's advertised codomain, so a
scalar family carrying an explicit event axis (over=...)
is scored over the full event without loss.
Source code in src/quivers/continuous/plate.py
gather
¶
Pullback v[indices] along a finite fibration.
indices is a long-tensor of shape (N,) with entries
in [0, |A|); returns a tensor of shape
(N, *B.shape).
Source code in src/quivers/continuous/plate.py
VectorisedObserve
¶
VectorisedObserve(family: ContinuousMorphism, response: Tensor)
Bases: ContinuousMorphism
A batched observation step accumulating per-row log-likelihoods.
Categorically, the batched-likelihood kernel
:math:\Phi \to \mathcal{G}_{\le 1}(\Phi) whose score is
:math:\prod_{n \in N} p_F(r_{\text{obs}}(n);\, \theta(n,\phi)).
Realised as a ContinuousMorphism whose domain is the
parameter-input space (the morphism conditions on θ) and whose
codomain is the per-observation response space — so the
existing MonadicProgram observe-step machinery treats
it as an observed site and threads the score through
log_joint via the usual morph.log_prob(theta, response)
call, with log_prob here summing over the leading index axis.
The observed response tensor is registered as a buffer so the
parent program's optimizer tracks it and the runtime never
has to thread it through observations=....
| PARAMETER | DESCRIPTION |
|---|---|
family
|
The per-observation distribution family.
TYPE:
|
response
|
Observed values
TYPE:
|
Source code in src/quivers/continuous/plate.py
support
property
¶
Inherit the underlying per-row family's support so guides and validators see the correct constraint for the observation.
rsample
¶
Sample the per-observation family at the supplied θ.
x is the θ-tensor (one row per observation index); the
result is the per-observation response sample. Used in
prior-predictive simulation; never called during inference
when the response is observed.
Source code in src/quivers/continuous/plate.py
log_prob
¶
Sum of per-observation log-densities.
y defaults to the registered response buffer; passing a
different value (e.g. a clamped observation) is supported
for fast prior-predictive checks.
Source code in src/quivers/continuous/plate.py
log_likelihood
¶
marginalize_categorical
¶
Program-level marginalisation over a discrete latent class.
Given per-class log-likelihoods of shape (N, K) (one row per
observation, one column per latent-class assignment), returns
the marginalised log-likelihood of shape (N,):
.. math::
\log p(r_n) = \log \sum_{k=1}^{K} p(c_n = k)\, p(r_n | c_n = k).
Realises the program-level marginalize c step as the
pushforward through :math:\pi_{\Phi \setminus C}.
Source code in src/quivers/continuous/plate.py
marginalize_grouped
¶
marginalize_grouped(log_likelihood_per_row_per_class: Tensor | list[Tensor] | tuple[Tensor, ...], group_index: Tensor | tuple[Tensor, ...] | list[Tensor | tuple[Tensor, ...]], log_prior_per_group_per_class: Tensor, num_groups: int | tuple[int, ...], *, reduction: str = 'logsumexp', per_group: bool = False) -> Tensor
Per-group marginalisation over a discrete latent class.
Two surface shapes are accepted, the second a strict generalisation of the first.
Single response axis. Given a per-(response, class)
log-likelihood tensor of shape (N, K) and a fibration
:math:r : \text{Resp} \to G (or a tuple of co-indexed
fibrations into a product grouping plate
:math:G_1 \times \dots \times G_r), return the scalar
.. math::
\sum_{g \in G}\, \rho\!\left[
\log \pi(g, \cdot) + \sum_{n:\, r(n) = g} \ell(n, \cdot)
\right].
Multiple heterogeneous response axes sharing a per-group
class indicator. Given M per-axis log-likelihood tensors
:math:\ell_m \in \mathbb{R}^{N_m \times K} and a parallel
list of fibrations :math:r_m : \text{Resp}_m \to G, return
.. math::
\sum_{g \in G}\, \rho\!\left[
\log \pi(g, \cdot)
+ \sum_{m=1}^{M} \sum_{n:\, r_m(n) = g} \ell_m(n, \cdot)
\right].
Each per-axis log-likelihood is scatter-added into the same
(|G|, K) accumulator before the reduction. Categorically
this is the right Kan extension along the coproduct fibration
:math:\coprod_m r_m : \coprod_m \text{Resp}_m \to G. The
single-axis form is the unary case (M = 1).
The reduction :math:\rho over the class axis is selected by
reduction: logsumexp for the canonical mixture
marginalisation, sum for joint scoring (no
marginalisation), mean for the symmetric average.
| PARAMETER | DESCRIPTION |
|---|---|
log_likelihood_per_row_per_class
|
Single tensor: a per-(response, class) log-likelihood of
shape
TYPE:
|
group_index
|
Single-axis form: a long tensor of shape
TYPE:
|
log_prior_per_group_per_class
|
Per-(group, class) log-prior of shape
TYPE:
|
num_groups
|
Cardinality of the (product) group plate.
TYPE:
|
reduction
|
Per-group reduction over the class axis.
TYPE:
|
per_group
|
Whether to return the reduced per-group aggregates
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Tensor
|
Scalar program-level log-density contribution, or the
per-group aggregates when |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If |
Notes
Edge cases:
K == 1: the log-sum-exp collapses to the body's log-likelihood plus the constant log-prior.- Identity fibration (
group_index = arange(N)andnum_groups == N): each row is its own group, recovering the per-row mixture. - Empty fibre: contributes a multiplicative identity to the
product (
log 1 = 0), so an unused group adds zero per class before the reduction. - Multi-axis with a single entry (
M = 1): exactly equivalent to the single-axis form, modulo unboxing the singleton list.
Source code in src/quivers/continuous/plate.py
614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 | |