ferrotorch_distributions/lib.rs
1//! Probability distributions for ferrotorch.
2//!
3//! This crate provides differentiable probability distributions following the
4//! PyTorch `torch.distributions` API. Each distribution supports:
5//!
6//! - **`sample`** — draw samples (no gradient)
7//! - **`rsample`** — reparameterized sampling (gradient flows through samples)
8//! - **`log_prob`** — compute log-probability of a value
9//! - **`entropy`** — compute the distribution's entropy
10//!
11//! # Distributions
12//!
13//! | Distribution | Parameters | Reparameterized |
14//! |-------------|-----------|-----------------|
15//! | [`Normal`] | `loc`, `scale` | Yes |
16//! | [`Uniform`] | `low`, `high` | Yes |
17//! | [`Bernoulli`] | `probs` | No (discrete) |
18//! | [`Binomial`] | `total_count`, `probs` | No (discrete) |
19//! | [`ContinuousBernoulli`] | `probs` | Yes (continuous on [0,1]) |
20//! | [`Categorical`] | `probs` | No (discrete) |
21//! | [`Geometric`] | `probs` | No (discrete) |
22//! | [`Beta`] | `concentration1`, `concentration0` | Yes |
23//! | [`Gamma`] | `concentration`, `rate` | Yes |
24//! | [`Exponential`] | `rate` | Yes |
25//! | [`Laplace`] | `loc`, `scale` | Yes |
26//! | [`Cauchy`] | `loc`, `scale` | Yes |
27//! | [`Gumbel`] | `loc`, `scale` | Yes |
28//! | [`HalfNormal`] | `scale` | Yes |
29//! | [`LogNormal`] | `loc`, `scale` | Yes |
30//! | [`Poisson`] | `rate` | No (discrete) |
31//! | [`StudentT`] | `df`, `loc`, `scale` | Yes |
32//! | [`MultivariateNormal`] | `loc`, `scale_tril` | Yes |
33//! | [`LowRankMultivariateNormal`] | `loc`, `cov_factor`, `cov_diag` | Yes |
34//! | [`Dirichlet`] | `concentration` | Yes |
35//! | [`Multinomial`] | `total_count`, `probs` | No (discrete) |
36//! | [`Independent`] | base distribution + `reinterpreted_batch_ndims` | inherits |
37//! | [`MixtureSameFamily`] | mixing `Categorical` + components | No |
38//! | [`OneHotCategorical`] | `probs` | No (discrete) |
39//! | [`RelaxedBernoulli`] | `temperature`, `probs` | Yes (Concrete relaxation) |
40//! | [`RelaxedOneHotCategorical`] | `temperature`, `probs` | Yes (Concrete relaxation) |
41//! | [`ExpRelaxedCategorical`] | `temperature`, `probs` | Yes (log-simplex Concrete relaxation) |
42//! | [`Pareto`] | `scale`, `alpha` | No (rsample not yet implemented) |
43//! | [`Kumaraswamy`] | `concentration1`, `concentration0` | No (rsample not yet implemented) |
44//! | [`VonMises`] | `loc`, `concentration` | No (rejection sampling) |
45//! | [`Weibull`] | `scale`, `concentration` | No (rsample not yet implemented) |
46//!
47//! # Infrastructure
48//!
49//! - [`constraints`] — constraint objects for parameter and support validation
50//! - [`transforms`] — bijective transforms with log-det-Jacobian computation
51//! - [`kl`] — analytical KL divergence for same-family distribution pairs
52//! - [`TransformedDistribution`](transforms::TransformedDistribution) — apply
53//! bijective transforms to a base distribution
54//!
55//! ## REQ status (per `.design/ferrotorch-distributions/lib.md`)
56//!
57//! Full evidence rows (impl + non-test production consumer + upstream cites)
58//! live in the design doc; this synopsis is a one-line summary per REQ.
59//!
60//! | REQ | Status | Evidence |
61//! |---|---|---|
62//! | REQ-1 (`Distribution<T>` trait: 4 required methods) | SHIPPED | `pub trait Distribution<T: Float>: Send + Sync` with `sample`/`rsample`/`log_prob`/`entropy` in `lib.rs` mirroring `torch/distributions/distribution.py:167-255`; consumers: `impl Distribution<T> for Normal<T>` in `normal.rs`, plus 25 other concrete `impl Distribution` sites across the crate |
63//! | REQ-2 (default-implemented property methods) | SHIPPED | `batch_shape`/`cdf`/`icdf`/`mean`/`mode`/`variance`/`stddev` defaults in `pub trait Distribution` mirroring `torch/distributions/distribution.py:108-165`; consumers: `fn Independent::batch_shape` in `independent.rs` overrides the default; `fn TransformedDistribution::entropy` in `transforms.rs` invokes `self.base.mean()?` |
64//! | REQ-3 (module tree + `pub use` re-exports) | SHIPPED | mod declarations + `pub use bernoulli::Bernoulli` through `pub use weibull::Weibull` block in `lib.rs` mirroring `torch/distributions/__init__.py:74-119`; consumers: `tests/conformance_distributions_*` use the re-exports; downstream crates import via `ferrotorch_distributions::{Normal, Bernoulli, ...}` |
65//! | REQ-4 (`<T: Float>` generic parametrisation) | SHIPPED | `pub trait Distribution<T: Float>` with explicit `T: Float` on every method (R-DEV-7: monomorphise per-dtype); consumers: every concrete `pub struct Normal<T: Float>` / `Gamma<T: Float>` etc. with `impl<T: Float> Distribution<T>` — f32 and f64 both exercised by `*_f64` tests per family |
66//! | REQ-5 (full PyTorch `Distribution` surface) | SHIPPED | `support` / `arg_constraints` / `has_rsample` / `has_enumerate_support` / `event_shape` / `expand` / `enumerate_support` / `perplexity` defaults landed on `pub trait Distribution` in `lib.rs` mirroring `torch/distributions/distribution.py:25-348`; consumers: `fn Normal::support` / `fn Normal::arg_constraints` in `normal.rs`, `fn Bernoulli::support` / `fn Bernoulli::enumerate_support` in `bernoulli.rs`, plus `fn Uniform::support` in `uniform.rs`, `fn Exponential::support` in `exponential.rs`, `fn Gamma::support` in `gamma.rs`, `fn Categorical::support` in `categorical.rs`; `Distribution::perplexity` default `exp(self.entropy()?)` consumed by every concrete distribution via the default impl |
67
68mod bernoulli;
69mod beta;
70mod binomial;
71mod categorical;
72mod cauchy;
73pub mod constraints;
74mod continuous_bernoulli;
75mod dirichlet;
76pub mod exp_family;
77mod exponential;
78pub(crate) mod fallback;
79mod gamma;
80mod geometric;
81mod gumbel;
82mod half_normal;
83mod independent;
84pub mod kl;
85mod kumaraswamy;
86mod laplace;
87mod lognormal;
88mod low_rank_multivariate_normal;
89mod mixture_same_family;
90mod multinomial;
91mod multivariate_normal;
92mod normal;
93mod one_hot_categorical;
94mod pareto;
95mod poisson;
96mod relaxed_bernoulli;
97mod relaxed_one_hot_categorical;
98pub(crate) mod special_fns;
99mod student_t;
100pub mod transforms;
101mod uniform;
102mod von_mises;
103mod weibull;
104
105pub use bernoulli::Bernoulli;
106pub use beta::Beta;
107pub use binomial::Binomial;
108pub use categorical::Categorical;
109pub use cauchy::Cauchy;
110pub use continuous_bernoulli::ContinuousBernoulli;
111pub use dirichlet::Dirichlet;
112pub use exponential::Exponential;
113pub use gamma::Gamma;
114pub use geometric::Geometric;
115pub use gumbel::Gumbel;
116pub use half_normal::HalfNormal;
117pub use independent::Independent;
118pub use kumaraswamy::Kumaraswamy;
119pub use laplace::Laplace;
120pub use lognormal::LogNormal;
121pub use low_rank_multivariate_normal::LowRankMultivariateNormal;
122pub use mixture_same_family::MixtureSameFamily;
123pub use multinomial::Multinomial;
124pub use multivariate_normal::MultivariateNormal;
125pub use normal::Normal;
126pub use one_hot_categorical::{OneHotCategorical, OneHotCategoricalStraightThrough};
127pub use pareto::Pareto;
128pub use poisson::Poisson;
129pub use relaxed_bernoulli::RelaxedBernoulli;
130pub use relaxed_one_hot_categorical::{ExpRelaxedCategorical, RelaxedOneHotCategorical};
131pub use student_t::StudentT;
132pub use transforms::{
133 AbsTransform, AffineTransform, CatTransform, ComposeTransform, CorrCholeskyTransform,
134 CumulativeDistributionTransform, ExpTransform, IndependentTransform, LowerCholeskyTransform,
135 PowerTransform, ReshapeTransform, SigmoidTransform, SoftmaxTransform, SoftplusTransform,
136 StackTransform, StickBreakingTransform, TanhTransform, Transform, TransformedDistribution,
137};
138pub use uniform::Uniform;
139pub use von_mises::VonMises;
140pub use weibull::Weibull;
141
142pub use exp_family::kl_expfamily_expfamily;
143
144use std::any::Any;
145use std::collections::HashMap;
146use std::fmt::Debug;
147
148use ferrotorch_core::dtype::Float;
149use ferrotorch_core::error::{FerrotorchError, FerrotorchResult};
150use ferrotorch_core::tensor::Tensor;
151
152// ---------------------------------------------------------------------------
153// AsDistAny — vtable-resident `&dyn Any` downcast hook (#1374 KL recursion)
154// ---------------------------------------------------------------------------
155
156/// Upcast a `&dyn Distribution<T>` (or any `'static` value) to `&dyn Any`.
157///
158/// `kl_dispatch` (in `kl.rs`) matches concrete `(P, Q)` pairs by
159/// `Any::downcast_ref`, which a *sized* `&P: Distribution<T> + 'static`
160/// coerces into for free. But the recursion-based KL pairs
161/// (`Independent-Independent`, `TransformedDistribution-TransformedDistribution`,
162/// mirroring `torch/distributions/kl.py:944-949,496-502`) must re-dispatch on
163/// a *type-erased* base distribution — a `&dyn Distribution<T>` — which has
164/// already lost the `Any` vtable entry. Making `AsDistAny` a supertrait of
165/// [`Distribution`] puts `as_dist_any` *in the `Distribution` vtable*, so
166/// every `&dyn Distribution<T>` can recover its concrete `&dyn Any` for the
167/// downcast chain. The blanket impl below covers every `'static` type, so no
168/// per-distribution boilerplate is needed.
169pub trait AsDistAny {
170 /// Recover the value as `&dyn Any` for concrete-type downcasting.
171 fn as_dist_any(&self) -> &dyn Any;
172}
173
174impl<U: 'static> AsDistAny for U {
175 fn as_dist_any(&self) -> &dyn Any {
176 self
177 }
178}
179
180/// Which recursion-based KL registration a distribution participates in.
181///
182/// Mirrors the two `@register_kl` registrations that recurse into a base
183/// distribution rather than reading concrete parameters:
184/// `_kl_independent_independent` (`torch/distributions/kl.py:944-949`) and
185/// `_kl_transformed_transformed` (`torch/distributions/kl.py:496-502`).
186#[derive(Debug, Clone)]
187pub enum KlRecurseKind {
188 /// [`Independent`]: `KL = _sum_rightmost(KL(p.base, q.base), n)` when the
189 /// two share `reinterpreted_batch_ndims = n`, else `NotImplementedError`.
190 /// Mirrors `torch/distributions/kl.py:944-949`.
191 Independent {
192 /// `reinterpreted_batch_ndims` — the number of rightmost dims of the
193 /// base-dist KL result to sum out.
194 reinterpreted_batch_ndims: usize,
195 },
196 /// [`TransformedDistribution`]: `KL = KL(p.base, q.base)` when the two
197 /// have equal transform chains AND equal event shapes, else
198 /// `NotImplementedError`. Mirrors `torch/distributions/kl.py:496-502`.
199 Transformed {
200 /// Structural fingerprint of the transform chain — equal iff the two
201 /// chains are equal under each `Transform`'s `__eq__` semantics
202 /// (`torch/distributions/transforms.py` per-transform `__eq__`).
203 transform_fingerprint: Vec<String>,
204 /// The distribution's `event_shape` (the `p.event_shape !=
205 /// q.event_shape` guard at `torch/distributions/kl.py:500-501`).
206 event_shape: Vec<usize>,
207 },
208}
209
210/// Recursion descriptor returned by [`Distribution::kl_recurse`].
211///
212/// Carries the type-erased base distribution to re-dispatch on plus the
213/// per-kind metadata the KL recursion needs. `None` from `kl_recurse` (the
214/// trait default) means the distribution participates in KL only via concrete
215/// closed-form arms, not recursion.
216pub struct KlRecurseInfo<'a, T: Float> {
217 /// The base distribution to re-run `kl_divergence` on.
218 pub base: &'a dyn Distribution<T>,
219 /// Which recursion registration this distribution participates in.
220 pub kind: KlRecurseKind,
221}
222
223// ---------------------------------------------------------------------------
224// Dyn-safe constraint object surface (REQ-5: support / arg_constraints)
225// ---------------------------------------------------------------------------
226
227/// Object-safe constraint descriptor exposed by `Distribution::support` and
228/// `Distribution::arg_constraints`.
229///
230/// The full [`constraints::Constraint`] trait carries a generic
231/// `check<T: Float>` method (R-DEV-7 monomorphisation) which forbids
232/// trait-object use. `DistConstraint` exposes only the dtype-independent
233/// metadata callers need to introspect a distribution's support:
234/// human-readable name, discrete-or-continuous flag, and the number of
235/// rightmost dims that together form an event.
236///
237/// Mirrors the subset of `torch.distributions.constraints.Constraint` that
238/// is interrogable without a concrete tensor argument — see
239/// `torch/distributions/constraints.py:80-106` (`Constraint.is_discrete`,
240/// `Constraint.event_dim`).
241pub trait DistConstraint: Send + Sync + Debug {
242 /// Human-readable constraint name (e.g. `"Real"`, `"UnitInterval"`).
243 fn name(&self) -> &'static str;
244
245 /// Whether the constrained domain is discrete (`true`) or continuous
246 /// (`false`). Defaults to `false`.
247 fn is_discrete(&self) -> bool {
248 false
249 }
250
251 /// Number of rightmost dimensions that together form a single event.
252 /// Defaults to `0` (univariate).
253 fn event_dim(&self) -> usize {
254 0
255 }
256}
257
258/// Blanket impl: every type that satisfies the non-generic surface of
259/// [`constraints::Constraint`] *and* is `Debug + 'static` is a
260/// [`DistConstraint`]. The blanket pulls `name`/`is_discrete`/`event_dim`
261/// straight off the source trait — `check<T>` is intentionally *not* on
262/// `DistConstraint` because it would re-introduce the generic method that
263/// breaks dyn-compatibility.
264impl<C> DistConstraint for C
265where
266 C: constraints::Constraint + Debug + 'static,
267{
268 fn name(&self) -> &'static str {
269 <Self as constraints::Constraint>::name(self)
270 }
271 fn is_discrete(&self) -> bool {
272 <Self as constraints::Constraint>::is_discrete(self)
273 }
274 fn event_dim(&self) -> usize {
275 <Self as constraints::Constraint>::event_dim(self)
276 }
277}
278
279/// A probability distribution over tensors.
280///
281/// This trait mirrors PyTorch's `torch.distributions.Distribution` base class.
282/// Implementations define how to sample, compute log-probabilities, and
283/// measure entropy.
284///
285/// # Type parameter
286///
287/// `T` must implement [`Float`] — currently `f32` or `f64`.
288///
289/// # `sample` vs `rsample`
290///
291/// - [`sample`](Distribution::sample) draws samples with no gradient. Use for
292/// discrete distributions or when gradients through sampling are not needed.
293/// - [`rsample`](Distribution::rsample) draws reparameterized samples. The
294/// result has `requires_grad = true` and gradients flow back through the
295/// sampling operation via the reparameterization trick. This is essential
296/// for variational inference (VAE, etc.).
297///
298/// Distributions that cannot be reparameterized (e.g., [`Bernoulli`],
299/// [`Categorical`]) return an error from `rsample`.
300pub trait Distribution<T: Float>: Send + Sync + AsDistAny {
301 /// Draw samples from the distribution.
302 ///
303 /// The returned tensor has the given `shape` and `requires_grad = false`.
304 fn sample(&self, shape: &[usize]) -> FerrotorchResult<Tensor<T>>;
305
306 /// Draw reparameterized samples from the distribution.
307 ///
308 /// The returned tensor has `requires_grad = true` and gradients flow
309 /// through the sampling operation back to the distribution parameters.
310 ///
311 /// Returns an error for distributions that cannot be reparameterized.
312 fn rsample(&self, shape: &[usize]) -> FerrotorchResult<Tensor<T>>;
313
314 /// Compute the log-probability of `value` under the distribution.
315 ///
316 /// Returns a tensor with the same shape as `value`.
317 fn log_prob(&self, value: &Tensor<T>) -> FerrotorchResult<Tensor<T>>;
318
319 /// Compute the entropy of the distribution.
320 ///
321 /// Returns a scalar tensor (or a tensor matching the batch shape of the
322 /// distribution parameters).
323 fn entropy(&self) -> FerrotorchResult<Tensor<T>>;
324
325 // -----------------------------------------------------------------------
326 // Distribution properties (#585) — default implementations return
327 // NotImplementedOnCuda-style errors. Concrete distributions override
328 // what they can express in closed form.
329 // -----------------------------------------------------------------------
330
331 /// The batch shape of the distribution — the shape of parameter tensors
332 /// (excluding event dims). Default returns an empty vec (scalar batch).
333 ///
334 /// Distributions with batched parameters (e.g. `Normal` with `loc` of
335 /// shape `[B]`) override this to return `vec![B]`. Used by `Independent`
336 /// to forward the correct sample shape to the base distribution.
337 fn batch_shape(&self) -> Vec<usize> {
338 vec![]
339 }
340
341 /// Cumulative distribution function: `P(X <= value)`. Default returns an
342 /// `InvalidArgument` error for distributions without a closed-form CDF.
343 fn cdf(&self, _value: &Tensor<T>) -> FerrotorchResult<Tensor<T>> {
344 Err(FerrotorchError::InvalidArgument {
345 message: "cdf not implemented for this distribution".into(),
346 })
347 }
348
349 /// Inverse CDF (quantile function): the value `x` such that
350 /// `P(X <= x) = q`. Default returns an `InvalidArgument` error.
351 fn icdf(&self, _q: &Tensor<T>) -> FerrotorchResult<Tensor<T>> {
352 Err(FerrotorchError::InvalidArgument {
353 message: "icdf not implemented for this distribution".into(),
354 })
355 }
356
357 /// Distribution mean. Default returns an `InvalidArgument` error.
358 fn mean(&self) -> FerrotorchResult<Tensor<T>> {
359 Err(FerrotorchError::InvalidArgument {
360 message: "mean not implemented for this distribution".into(),
361 })
362 }
363
364 /// Distribution mode. Default returns an `InvalidArgument` error.
365 fn mode(&self) -> FerrotorchResult<Tensor<T>> {
366 Err(FerrotorchError::InvalidArgument {
367 message: "mode not implemented for this distribution".into(),
368 })
369 }
370
371 /// Distribution variance. Default returns an `InvalidArgument` error.
372 fn variance(&self) -> FerrotorchResult<Tensor<T>> {
373 Err(FerrotorchError::InvalidArgument {
374 message: "variance not implemented for this distribution".into(),
375 })
376 }
377
378 /// Distribution standard deviation. Default: `sqrt(variance)`.
379 fn stddev(&self) -> FerrotorchResult<Tensor<T>> {
380 let v = self.variance()?;
381 let data = v.data_vec()?;
382 let out: Vec<T> = data.iter().map(|x| x.sqrt()).collect();
383 Tensor::from_storage(
384 ferrotorch_core::storage::TensorStorage::cpu(out),
385 v.shape().to_vec(),
386 false,
387 )
388 }
389
390 // -----------------------------------------------------------------------
391 // Full PyTorch `Distribution` surface (#1376) — defaults return either
392 // a structured `InvalidArgument` (for methods PyTorch raises
393 // `NotImplementedError` on) or a sensible fallback (e.g. `perplexity =
394 // exp(entropy)`). Concrete distributions override only what they can
395 // express. Mirrors `torch/distributions/distribution.py:25-348`.
396 // -----------------------------------------------------------------------
397
398 /// Shape of a single sample (without batching). Default returns an
399 /// empty vec, matching `torch/distributions/distribution.py:114-119`
400 /// (`event_shape = torch.Size()` for univariate distributions).
401 fn event_shape(&self) -> Vec<usize> {
402 vec![]
403 }
404
405 /// Whether the distribution implements reparameterized sampling.
406 ///
407 /// Default: `false`. Continuous distributions with a closed-form
408 /// reparameterization (Normal, Uniform, Exponential, Gamma, Beta,
409 /// Laplace, Cauchy, …) override to return `true`. Mirrors the
410 /// class-level `has_rsample = False` flag at
411 /// `torch/distributions/distribution.py:25`.
412 fn has_rsample(&self) -> bool {
413 false
414 }
415
416 /// Whether the distribution implements `enumerate_support`.
417 ///
418 /// Default: `false`. Finite discrete distributions (Bernoulli,
419 /// Categorical, OneHotCategorical) override to return `true`.
420 /// Mirrors `torch/distributions/distribution.py:26`
421 /// (`has_enumerate_support = False`).
422 fn has_enumerate_support(&self) -> bool {
423 false
424 }
425
426 /// The support of the distribution as a [`DistConstraint`] object
427 /// (e.g. `Real`, `UnitInterval`, `Positive`).
428 ///
429 /// Default returns `None`. Concrete distributions override to advertise
430 /// their support. Mirrors
431 /// `torch/distributions/distribution.py:131-138` (the `support`
432 /// property), where PyTorch raises `NotImplementedError` if a subclass
433 /// has not declared support.
434 fn support(&self) -> Option<Box<dyn DistConstraint>> {
435 None
436 }
437
438 /// The argument constraints map: parameter-name → [`DistConstraint`].
439 ///
440 /// Default returns an empty map. Concrete distributions override to
441 /// advertise the constraint each constructor argument must satisfy.
442 /// Mirrors `torch/distributions/distribution.py:121-129` (the
443 /// `arg_constraints` property).
444 fn arg_constraints(&self) -> HashMap<&'static str, Box<dyn DistConstraint>> {
445 HashMap::new()
446 }
447
448 /// Return a new distribution with batch dims expanded to `batch_shape`.
449 ///
450 /// Default returns `InvalidArgument`. Concrete distributions override
451 /// by constructing a new instance whose parameters have been broadcast
452 /// to the target shape (no allocation copy in PyTorch via
453 /// `Tensor::expand`; ferrotorch CPU path materialises the broadcast for
454 /// simplicity). Mirrors
455 /// `torch/distributions/distribution.py:86-105` (`expand`).
456 fn expand(&self, _batch_shape: &[usize]) -> FerrotorchResult<Box<dyn Distribution<T>>> {
457 Err(FerrotorchError::InvalidArgument {
458 message: "expand not implemented for this distribution".into(),
459 })
460 }
461
462 /// Enumerate all values supported by a discrete distribution.
463 ///
464 /// Default returns `InvalidArgument`. Finite discrete distributions
465 /// (Bernoulli, Categorical, OneHotCategorical) override. Mirrors
466 /// `torch/distributions/distribution.py:224-246`
467 /// (`enumerate_support`).
468 ///
469 /// When `expand` is `true`, the result is broadcast across the
470 /// distribution's `batch_shape`; when `false`, the trailing batch
471 /// dimensions are kept as singletons.
472 fn enumerate_support(&self, _expand: bool) -> FerrotorchResult<Tensor<T>> {
473 Err(FerrotorchError::InvalidArgument {
474 message: "enumerate_support not implemented for this distribution".into(),
475 })
476 }
477
478 /// Perplexity: `exp(entropy)`. Default body invokes `self.entropy()?`
479 /// and exponentiates element-wise. Mirrors
480 /// `torch/distributions/distribution.py:257-264`.
481 fn perplexity(&self) -> FerrotorchResult<Tensor<T>> {
482 let h = self.entropy()?;
483 let data = h.data_vec()?;
484 let out: Vec<T> = data.iter().map(|x| x.exp()).collect();
485 Tensor::from_storage(
486 ferrotorch_core::storage::TensorStorage::cpu(out),
487 h.shape().to_vec(),
488 false,
489 )
490 }
491
492 /// Recursion descriptor for the KL pairs that re-dispatch into a base
493 /// distribution instead of reading concrete parameters.
494 ///
495 /// Default returns `None` (the distribution participates in KL only via
496 /// concrete closed-form arms). [`Independent`] and
497 /// [`TransformedDistribution`] override this to expose their type-erased
498 /// base + the per-kind guard metadata, which lets `kl_divergence` mirror
499 /// `_kl_independent_independent` (`torch/distributions/kl.py:944-949`) and
500 /// `_kl_transformed_transformed` (`torch/distributions/kl.py:496-502`)
501 /// without the dispatcher needing the concrete generic base type `D`
502 /// (which `Any::downcast_ref` cannot recover for `Independent<T, D>`).
503 fn kl_recurse(&self) -> Option<KlRecurseInfo<'_, T>> {
504 None
505 }
506}
507
508// ---------------------------------------------------------------------------
509// ExponentialFamily trait (#1404, #1407)
510// ---------------------------------------------------------------------------
511
512/// Marker trait for distributions in the exponential family.
513///
514/// An exponential-family density has the canonical form
515/// `p(x; θ) = exp(<t(x), η(θ)> − F(η) + k(x))` where:
516/// - `η(θ)` are the **natural parameters** (returned by [`natural_params`](ExponentialFamily::natural_params))
517/// - `F(η)` is the **log-normalizer** (returned by [`log_normalizer`](ExponentialFamily::log_normalizer))
518/// - `k(x)` is the carrier measure (its expectation is
519/// [`mean_carrier_measure`](ExponentialFamily::mean_carrier_measure))
520///
521/// Mirrors `torch.distributions.ExponentialFamily` (`torch/distributions/exp_family.py:11-66`).
522/// Used by KL-divergence machinery and analytic entropy reasoning.
523pub trait ExponentialFamily<T: Float>: Distribution<T> {
524 /// The natural parameters `η(θ)` as a flat list of `Tensor<T>`.
525 /// Mirrors `_natural_params` (`exp_family.py:32-38`).
526 fn natural_params(&self) -> FerrotorchResult<Vec<Tensor<T>>>;
527
528 /// The log-normalizer `F(η)` evaluated at the given natural-parameter
529 /// tuple. The argument is the same shape/order as
530 /// [`natural_params`](Self::natural_params) returns. Mirrors
531 /// `_log_normalizer(*natural_params)` (`exp_family.py:40-45`).
532 fn log_normalizer(&self, natural_params: &[Tensor<T>]) -> FerrotorchResult<Tensor<T>>;
533
534 /// The mean parameters `∇F(η)` — the gradient of the log-normalizer
535 /// evaluated at this distribution's own natural parameters, which equals
536 /// the expected sufficient statistics `E[t(X)]`.
537 ///
538 /// PyTorch obtains this gradient via reverse-mode autograd through
539 /// `_log_normalizer` (`torch/distributions/exp_family.py:62`
540 /// `torch.autograd.grad(lg_normal.sum(), nparams, create_graph=True)` and
541 /// `torch/distributions/kl.py:292` likewise). ferrotorch instead provides
542 /// the gradient in **closed form** per family (R-DEV-7: the analytic
543 /// gradient is cleaner and avoids building an autograd graph through the
544 /// host-side `_log_normalizer`). The returned `Vec` is index-aligned with
545 /// [`natural_params`](Self::natural_params): `mean_params()[i]` is
546 /// `∂F/∂η_i` at this distribution's `η`.
547 ///
548 /// Consumed by [`kl_expfamily_expfamily`](crate::exp_family::kl_expfamily_expfamily)
549 /// as the `∇A(η_p)` term of the Bregman-divergence KL formula
550 /// (`torch/distributions/kl.py:282-297`).
551 fn mean_params(&self) -> FerrotorchResult<Vec<Tensor<T>>>;
552
553 /// The expected carrier measure `E[k(X)]`. Returns 0 for most
554 /// continuous families. Mirrors `_mean_carrier_measure`
555 /// (`exp_family.py:47-53`).
556 fn mean_carrier_measure(&self) -> FerrotorchResult<T> {
557 Ok(<T as num_traits::Zero>::zero())
558 }
559}