Skip to content

Distributions

SciPy-shaped distributions. Each generator is used directly (norm.rvs(key, loc, scale)) or frozen with fixed parameters (norm(loc=..., scale=...)).

Base classes

probjax.stats.rv_generic

Bases: ABC

Generic random variable class for common functionality.

Source code in probjax/stats/base.py
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
class rv_generic(ABC):
    """Generic random variable class for common functionality."""

    name: ClassVar[Optional[str]] = None
    parameters: ClassVar[Mapping[str, stats_constraints.Constraint]] = {}
    parameter_aliases: ClassVar[Mapping[str, str]] = {}
    extra_frozen_kwds: ClassVar[frozenset[str]] = frozenset()

    def __init__(self, name: Optional[str] = None):
        if name is not None:
            type(self).name = name

    def __call__(self, *args: Any, **kwds: Any) -> "rv_frozen":
        """Call the distribution with the given arguments."""
        return self.freeze(*args, **kwds)

    @staticmethod
    def _format_frozen_class_name(name: Optional[str], fallback: str) -> str:
        """Create a stable class name for generated frozen distributions."""
        raw_name = name or fallback
        parts = "".join(ch if ch.isalnum() else "_" for ch in str(raw_name)).split("_")
        camel = "".join(part.capitalize() for part in parts if part)
        return f"Frozen{camel or 'Distribution'}"

    def _get_or_create_frozen_class(
        self, base_frozen_cls: type["rv_frozen"]
    ) -> type["rv_frozen"]:
        """Get a cached frozen class for this distribution instance."""
        cache = getattr(self, "_frozen_cls_cache", None)
        if cache is not None and issubclass(cache, base_frozen_cls):
            return cast(type["rv_frozen"], cache)

        class_name = self._format_frozen_class_name(
            getattr(self, "name", None), self.__class__.__name__
        )
        frozen_cls = cast(
            type["rv_frozen"],
            FrozenDistributionMeta(
                class_name,
                (base_frozen_cls,),
                {},
                dist=self,
            ),
        )
        self._frozen_cls_cache = frozen_cls
        return frozen_cls

    @staticmethod
    def _distribution_display_name(dist: Any) -> str:
        return (
            getattr(dist, "name", None)
            or getattr(dist, "__name__", None)
            or dist.__class__.__name__
        )

    @classmethod
    def _bind_frozen_args_for_dist(
        cls, dist: Any, args: tuple[Any, ...], kwds: Mapping[str, Any]
    ) -> _FrozenArgs:
        """Validate and bind frozen distribution arguments."""
        parameters = tuple(getattr(dist, "parameters", {}).keys())
        extra_kwds = set(getattr(dist, "extra_frozen_kwds", ()))
        kwds_dict = dict(kwds)
        dist_name = cls._distribution_display_name(dist)

        if len(args) > len(parameters):
            raise TypeError(
                f"{dist_name} expected at most {len(parameters)} positional "
                f"arguments, got {len(args)}."
            )

        positional_names = set(parameters[: len(args)])
        duplicate_names = sorted(positional_names.intersection(kwds_dict))
        if duplicate_names:
            names = ", ".join(repr(name) for name in duplicate_names)
            raise TypeError(f"{dist_name} got multiple values for argument {names}.")

        valid_kwds = set(parameters).union(extra_kwds)
        unexpected = sorted(set(kwds_dict).difference(valid_kwds))
        if unexpected:
            names = ", ".join(repr(name) for name in unexpected)
            raise TypeError(f"{dist_name} got unexpected keyword argument {names}.")

        parameter_values = {
            name: value for name, value in zip(parameters, args, strict=False)
        }
        parameter_values.update({
            name: kwds_dict[name] for name in parameters if name in kwds_dict
        })
        return _FrozenArgs(args=args, kwds=kwds_dict, parameter_values=parameter_values)

    def _freeze_as(
        self, base_frozen_cls: type["rv_frozen"], *args: Any, **kwds: Any
    ) -> "rv_frozen":
        """Freeze a distribution into the requested frozen base class."""
        frozen_cls = self._get_or_create_frozen_class(base_frozen_cls)
        return frozen_cls(self, *args, **kwds)

    @classmethod
    def _parse_args(
        cls, *args: Any, **kwds: Any
    ) -> tuple[tuple[Any, ...], dict[str, Any]]:
        """Parse arguments for the distribution."""
        # Move important kwds to args
        params = dict(cls.parameters)
        for param_name in params:
            if param_name in kwds:
                args = args + (kwds.pop(param_name),)
        return args, kwds

    def freeze(self, *args: Any, **kwds: Any) -> "rv_frozen":
        """Freeze the distribution for the given arguments."""
        return self._freeze_as(rv_frozen, *args, **kwds)

    def from_params(
        self, params: Optional[Mapping[str, Any]] = None, **kwds: Any
    ) -> "rv_frozen":
        """Create a frozen distribution from name-keyed parameters."""
        values = dict(params or {})
        values.update(kwds)
        args = []
        for name in self.parameters:
            if name not in values:
                break
            args.append(values.pop(name))
        return self.freeze(*args, **values)

    @classmethod
    def params_to_unconstrained(cls, params: Mapping[str, Any]) -> dict[str, Any]:
        """Map constrained parameters to an optimization-friendly pytree."""
        from probjax.stats.constraint_registry import biject_to

        unconstrained = {}
        for name, value in params.items():
            constraint = cls.parameters.get(name)
            if isinstance(constraint, stats_constraints.Distribution):
                unconstrained[name] = jax.tree_util.tree_map(
                    lambda component: DistributionParams(
                        component.dist, component.unconstrained_params
                    ),
                    value,
                    is_leaf=lambda component: isinstance(component, rv_frozen),
                )
                continue
            if not isinstance(constraint, stats_constraints.Constraint):
                unconstrained[name] = value
                continue
            try:
                unconstrained[name] = biject_to(constraint).inv(value)
            except NotImplementedError:
                unconstrained[name] = value
        return unconstrained

    @classmethod
    def params_from_unconstrained(cls, params: Mapping[str, Any]) -> dict[str, Any]:
        """Map unconstrained parameters back to their declared supports."""
        from probjax.stats.constraint_registry import biject_to

        constrained = {}
        for name, value in params.items():
            constraint = cls.parameters.get(name)
            if isinstance(constraint, stats_constraints.Distribution):
                constrained[name] = jax.tree_util.tree_map(
                    lambda component: component.constrain(),
                    value,
                    is_leaf=lambda component: isinstance(component, DistributionParams),
                )
                continue
            if not isinstance(constraint, stats_constraints.Constraint):
                constrained[name] = value
                continue
            try:
                constrained[name] = biject_to(constraint)(value)
            except NotImplementedError:
                constrained[name] = value
        return constrained

    def rvs(
        self,
        rng: RngKey,
        *args: Any,
        shape: Tuple[int, ...] = (),
        name: Optional[str] = None,
        **kwargs: Any,
    ) -> Array:
        """Random variates of given shape.

        Calls through the `rv_p` primitive so traced execution records a random
        variable site while eager execution remains a direct sample.
        """
        from probjax.core.custom_primitives.random_variable import rv_p

        return rv_p.bind(
            rng,
            *args,
            shape=shape,
            dist=self,
            name=name,
            rvs_fn=type(self)._rvs_impl,
            logpdf_fn=type(self).logpdf,
            kwds=kwargs,
        )

    @classmethod
    @abstractmethod
    def support(cls, *args, **kwds) -> stats_constraints.Constraint:
        """Support of the distribution."""
        ...

    @classmethod
    @abstractmethod
    def _rvs_impl(
        cls, rng: RngKey, *args: Any, shape: Tuple[int, ...] = (), **kwargs: Any
    ) -> Array:
        """Implementation for random variate sampling.

        Parameters
        ----------
        rng : jax.random.PRNGKey
            The random key used for sampling
        *args : array_like
            Shape parameters for the distribution
        shape : tuple of ints, optional
            The shape of the samples to draw
        name : str, optional
            Optional site name used when tracing probabilistic programs.
            If omitted, a unique name is generated from the distribution name.
        **kwargs : dict, optional
            Additional parameters (loc, scale, etc.)

        Returns
        -------
        rvs : ndarray or scalar
            Random variates of given shape
        """
        ...

    @classmethod
    def mean(cls, *args: Any, **kwds: Any) -> ArrayLike:
        """Mean of the distribution."""
        raise NotImplementedError("Mean is not implemented for this distribution.")

    @classmethod
    def mode(cls, *args: Any, **kwds: Any) -> ArrayLike:
        """Mode of the distribution."""
        raise NotImplementedError("Mode is not implemented for this distribution.")

    @classmethod
    def var(cls, *args: Any, **kwds: Any) -> ArrayLike:
        """Variance of the distribution."""
        raise NotImplementedError("Variance is not implemented for this distribution.")

    @classmethod
    def std(cls, *args: Any, **kwds: Any) -> ArrayLike:
        """Standard deviation of the distribution."""
        return jnp.sqrt(cls.var(*args, **kwds))

    @classmethod
    def cdf(cls, *args: Any, **kwds: Any) -> ArrayLike:
        """Cumulative distribution function of the RV."""
        raise NotImplementedError("CDF is not implemented for this distribution.")

    @classmethod
    @abstractmethod
    def logpdf(cls, x: ArrayLike, *args: Any, **kwds: Any) -> Array:
        """Log probability of the distribution at the given value."""
        ...

    @classmethod
    def logcdf(cls, x: ArrayLike, *args: Any, **kwds: Any) -> Array:
        """Log of the cumulative distribution function at x of the given RV."""
        return jnp.log(cls.cdf(x, *args, **kwds))

    @classmethod
    def sf(cls, x: ArrayLike, *args: Any, **kwds: Any) -> Array:
        """Survival function (1 - cdf) at x of the given RV."""
        cdf_val = jnp.asarray(cls.cdf(x, *args, **kwds))
        return jnp.ones_like(cdf_val) - cdf_val

    @classmethod
    def logsf(cls, x: ArrayLike, *args: Any, **kwds: Any) -> Array:
        """Log of the survival function at x of the given RV."""
        return jnp.log(cls.sf(x, *args, **kwds))

    @classmethod
    def ppf(cls, *args: Any, **kwds: Any) -> ArrayLike:
        """Percent point function (inverse of cdf) of the RV."""
        raise NotImplementedError("PPF is not implemented for this distribution.")

    @classmethod
    def isf(cls, q: ArrayLike, *args: Any, **kwds: Any) -> ArrayLike:
        """Inverse survival function (1 - ppf) of the RV."""
        q = jnp.asarray(q)
        return cls.ppf(1.0 - q, *args, **kwds)

    @classmethod
    def entropy(cls, *args: Any, **kwds: Any) -> ArrayLike:
        """Entropy of the RV."""
        raise NotImplementedError("Entropy is not implemented for this distribution.")

    @classmethod
    def median(cls, *args: Any, **kwds: Any) -> ArrayLike:
        """Median of the distribution."""
        args, kwds = cls._parse_args(*args, **kwds)
        return cls.ppf(0.5, *args, **kwds)

    @classmethod
    def interval(
        cls, alpha: ArrayLike, *args: Any, **kwds: Any
    ) -> tuple[ArrayLike, ArrayLike]:
        """Confidence interval with equal areas around the median."""
        args, kwds = cls._parse_args(*args, **kwds)
        alpha = jnp.asarray(alpha)
        q1 = (1.0 - alpha) / 2
        q2 = (1.0 + alpha) / 2
        a = cls.ppf(q1, *args, **kwds)
        b = cls.ppf(q2, *args, **kwds)
        return a, b

    @classmethod
    def moment(cls, n: int, *args: Any, **kwds: Any) -> ArrayLike:
        """n-th non-central moment of the distribution.

        Parameters
        ----------
        n : int
            Order of the moment
        *args : array_like
            Shape parameters for the distribution
        **kwds : dict, optional
            Additional parameters (loc, scale, etc.)

        Returns
        -------
        moment : float or ndarray
            n-th non-central moment
        """
        raise NotImplementedError("Moment is not implemented for this distribution.")

    @classmethod
    def skew(cls, *args: Any, **kwds: Any) -> ArrayLike:
        """Skewness of the distribution.

        Parameters
        ----------
        *args : array_like
            Shape parameters for the distribution
        **kwds : dict, optional
            Additional parameters (loc, scale, etc.)

        Returns
        -------
        skew : float or ndarray
            Skewness of the distribution
        """
        raise NotImplementedError("Skewness is not implemented for this distribution.")

    @classmethod
    def kurtosis(cls, *args: Any, **kwds: Any) -> ArrayLike:
        """Kurtosis of the distribution.

        Parameters
        ----------
        *args : array_like
            Shape parameters for the distribution
        **kwds : dict, optional
            Additional parameters (loc, scale, etc.)

        Returns
        -------
        kurtosis : float or ndarray
            Kurtosis of the distribution (Fisher's definition, kurtosis - 3)
        """
        raise NotImplementedError("Kurtosis is not implemented for this distribution.")

    @classmethod
    def fit(cls, data: ArrayLike, **kwds: Any) -> tuple[Array, ...]:
        """Maximum likelihood estimation of distribution parameters.

        Parameters
        ----------
        data : array_like
            Data to fit the distribution to
        **kwds : dict, optional
            Additional parameters for the optimization

        Returns
        -------
        params : tuple
            The fitted parameters of the distribution
        """
        raise NotImplementedError("Not implemented for this distribution.")

    @classmethod
    def fit_params(cls, data: ArrayLike, **kwds: Any) -> dict[str, Any]:
        """Fit and return parameters keyed by their declared names."""
        fitted = cls.fit(data, **kwds)
        values = fitted if isinstance(fitted, tuple) else (fitted,)
        return dict(zip(cls.parameters, values, strict=False))

    @classmethod
    def _fit_mle(cls, data: ArrayLike, **kwds: Any) -> tuple[Array, ...]:
        """Fit by optimizing the likelihood in unconstrained parameter space."""
        from jax.scipy.optimize import minimize

        init_params = {}
        for name in cls.parameters:
            if name not in kwds:
                raise ValueError(f"Provide an initial value for parameter {name!r}.")
            init_params[name] = jnp.asarray(kwds.pop(name))

        initial, unravel = ravel_pytree(cls.params_to_unconstrained(init_params))

        def objective(flat_params):
            params = cls.params_from_unconstrained(unravel(flat_params))
            return -jnp.sum(cls.logpdf(data, **params))

        result = minimize(objective, initial, method="BFGS", **kwds)
        if not bool(jnp.all(jnp.isfinite(result.x))):
            message = getattr(result, "message", "unknown error")
            raise ValueError(f"Optimization failed: {message}")

        fitted = cls.params_from_unconstrained(unravel(result.x))
        return tuple(fitted[name] for name in cls.parameters)

freeze

freeze(*args, **kwds)

Freeze the distribution for the given arguments.

Source code in probjax/stats/base.py
def freeze(self, *args: Any, **kwds: Any) -> "rv_frozen":
    """Freeze the distribution for the given arguments."""
    return self._freeze_as(rv_frozen, *args, **kwds)

from_params

from_params(params=None, **kwds)

Create a frozen distribution from name-keyed parameters.

Source code in probjax/stats/base.py
def from_params(
    self, params: Optional[Mapping[str, Any]] = None, **kwds: Any
) -> "rv_frozen":
    """Create a frozen distribution from name-keyed parameters."""
    values = dict(params or {})
    values.update(kwds)
    args = []
    for name in self.parameters:
        if name not in values:
            break
        args.append(values.pop(name))
    return self.freeze(*args, **values)

params_to_unconstrained classmethod

params_to_unconstrained(params)

Map constrained parameters to an optimization-friendly pytree.

Source code in probjax/stats/base.py
@classmethod
def params_to_unconstrained(cls, params: Mapping[str, Any]) -> dict[str, Any]:
    """Map constrained parameters to an optimization-friendly pytree."""
    from probjax.stats.constraint_registry import biject_to

    unconstrained = {}
    for name, value in params.items():
        constraint = cls.parameters.get(name)
        if isinstance(constraint, stats_constraints.Distribution):
            unconstrained[name] = jax.tree_util.tree_map(
                lambda component: DistributionParams(
                    component.dist, component.unconstrained_params
                ),
                value,
                is_leaf=lambda component: isinstance(component, rv_frozen),
            )
            continue
        if not isinstance(constraint, stats_constraints.Constraint):
            unconstrained[name] = value
            continue
        try:
            unconstrained[name] = biject_to(constraint).inv(value)
        except NotImplementedError:
            unconstrained[name] = value
    return unconstrained

params_from_unconstrained classmethod

params_from_unconstrained(params)

Map unconstrained parameters back to their declared supports.

Source code in probjax/stats/base.py
@classmethod
def params_from_unconstrained(cls, params: Mapping[str, Any]) -> dict[str, Any]:
    """Map unconstrained parameters back to their declared supports."""
    from probjax.stats.constraint_registry import biject_to

    constrained = {}
    for name, value in params.items():
        constraint = cls.parameters.get(name)
        if isinstance(constraint, stats_constraints.Distribution):
            constrained[name] = jax.tree_util.tree_map(
                lambda component: component.constrain(),
                value,
                is_leaf=lambda component: isinstance(component, DistributionParams),
            )
            continue
        if not isinstance(constraint, stats_constraints.Constraint):
            constrained[name] = value
            continue
        try:
            constrained[name] = biject_to(constraint)(value)
        except NotImplementedError:
            constrained[name] = value
    return constrained

rvs

rvs(rng, *args, shape=(), name=None, **kwargs)

Random variates of given shape.

Calls through the rv_p primitive so traced execution records a random variable site while eager execution remains a direct sample.

Source code in probjax/stats/base.py
def rvs(
    self,
    rng: RngKey,
    *args: Any,
    shape: Tuple[int, ...] = (),
    name: Optional[str] = None,
    **kwargs: Any,
) -> Array:
    """Random variates of given shape.

    Calls through the `rv_p` primitive so traced execution records a random
    variable site while eager execution remains a direct sample.
    """
    from probjax.core.custom_primitives.random_variable import rv_p

    return rv_p.bind(
        rng,
        *args,
        shape=shape,
        dist=self,
        name=name,
        rvs_fn=type(self)._rvs_impl,
        logpdf_fn=type(self).logpdf,
        kwds=kwargs,
    )

support abstractmethod classmethod

support(*args, **kwds)

Support of the distribution.

Source code in probjax/stats/base.py
@classmethod
@abstractmethod
def support(cls, *args, **kwds) -> stats_constraints.Constraint:
    """Support of the distribution."""
    ...

mean classmethod

mean(*args, **kwds)

Mean of the distribution.

Source code in probjax/stats/base.py
@classmethod
def mean(cls, *args: Any, **kwds: Any) -> ArrayLike:
    """Mean of the distribution."""
    raise NotImplementedError("Mean is not implemented for this distribution.")

mode classmethod

mode(*args, **kwds)

Mode of the distribution.

Source code in probjax/stats/base.py
@classmethod
def mode(cls, *args: Any, **kwds: Any) -> ArrayLike:
    """Mode of the distribution."""
    raise NotImplementedError("Mode is not implemented for this distribution.")

var classmethod

var(*args, **kwds)

Variance of the distribution.

Source code in probjax/stats/base.py
@classmethod
def var(cls, *args: Any, **kwds: Any) -> ArrayLike:
    """Variance of the distribution."""
    raise NotImplementedError("Variance is not implemented for this distribution.")

std classmethod

std(*args, **kwds)

Standard deviation of the distribution.

Source code in probjax/stats/base.py
@classmethod
def std(cls, *args: Any, **kwds: Any) -> ArrayLike:
    """Standard deviation of the distribution."""
    return jnp.sqrt(cls.var(*args, **kwds))

cdf classmethod

cdf(*args, **kwds)

Cumulative distribution function of the RV.

Source code in probjax/stats/base.py
@classmethod
def cdf(cls, *args: Any, **kwds: Any) -> ArrayLike:
    """Cumulative distribution function of the RV."""
    raise NotImplementedError("CDF is not implemented for this distribution.")

logpdf abstractmethod classmethod

logpdf(x, *args, **kwds)

Log probability of the distribution at the given value.

Source code in probjax/stats/base.py
@classmethod
@abstractmethod
def logpdf(cls, x: ArrayLike, *args: Any, **kwds: Any) -> Array:
    """Log probability of the distribution at the given value."""
    ...

logcdf classmethod

logcdf(x, *args, **kwds)

Log of the cumulative distribution function at x of the given RV.

Source code in probjax/stats/base.py
@classmethod
def logcdf(cls, x: ArrayLike, *args: Any, **kwds: Any) -> Array:
    """Log of the cumulative distribution function at x of the given RV."""
    return jnp.log(cls.cdf(x, *args, **kwds))

sf classmethod

sf(x, *args, **kwds)

Survival function (1 - cdf) at x of the given RV.

Source code in probjax/stats/base.py
@classmethod
def sf(cls, x: ArrayLike, *args: Any, **kwds: Any) -> Array:
    """Survival function (1 - cdf) at x of the given RV."""
    cdf_val = jnp.asarray(cls.cdf(x, *args, **kwds))
    return jnp.ones_like(cdf_val) - cdf_val

logsf classmethod

logsf(x, *args, **kwds)

Log of the survival function at x of the given RV.

Source code in probjax/stats/base.py
@classmethod
def logsf(cls, x: ArrayLike, *args: Any, **kwds: Any) -> Array:
    """Log of the survival function at x of the given RV."""
    return jnp.log(cls.sf(x, *args, **kwds))

ppf classmethod

ppf(*args, **kwds)

Percent point function (inverse of cdf) of the RV.

Source code in probjax/stats/base.py
@classmethod
def ppf(cls, *args: Any, **kwds: Any) -> ArrayLike:
    """Percent point function (inverse of cdf) of the RV."""
    raise NotImplementedError("PPF is not implemented for this distribution.")

isf classmethod

isf(q, *args, **kwds)

Inverse survival function (1 - ppf) of the RV.

Source code in probjax/stats/base.py
@classmethod
def isf(cls, q: ArrayLike, *args: Any, **kwds: Any) -> ArrayLike:
    """Inverse survival function (1 - ppf) of the RV."""
    q = jnp.asarray(q)
    return cls.ppf(1.0 - q, *args, **kwds)

entropy classmethod

entropy(*args, **kwds)

Entropy of the RV.

Source code in probjax/stats/base.py
@classmethod
def entropy(cls, *args: Any, **kwds: Any) -> ArrayLike:
    """Entropy of the RV."""
    raise NotImplementedError("Entropy is not implemented for this distribution.")

median classmethod

median(*args, **kwds)

Median of the distribution.

Source code in probjax/stats/base.py
@classmethod
def median(cls, *args: Any, **kwds: Any) -> ArrayLike:
    """Median of the distribution."""
    args, kwds = cls._parse_args(*args, **kwds)
    return cls.ppf(0.5, *args, **kwds)

interval classmethod

interval(alpha, *args, **kwds)

Confidence interval with equal areas around the median.

Source code in probjax/stats/base.py
@classmethod
def interval(
    cls, alpha: ArrayLike, *args: Any, **kwds: Any
) -> tuple[ArrayLike, ArrayLike]:
    """Confidence interval with equal areas around the median."""
    args, kwds = cls._parse_args(*args, **kwds)
    alpha = jnp.asarray(alpha)
    q1 = (1.0 - alpha) / 2
    q2 = (1.0 + alpha) / 2
    a = cls.ppf(q1, *args, **kwds)
    b = cls.ppf(q2, *args, **kwds)
    return a, b

moment classmethod

moment(n, *args, **kwds)

n-th non-central moment of the distribution.

Parameters

n : int Order of the moment args : array_like Shape parameters for the distribution *kwds : dict, optional Additional parameters (loc, scale, etc.)

Returns

moment : float or ndarray n-th non-central moment

Source code in probjax/stats/base.py
@classmethod
def moment(cls, n: int, *args: Any, **kwds: Any) -> ArrayLike:
    """n-th non-central moment of the distribution.

    Parameters
    ----------
    n : int
        Order of the moment
    *args : array_like
        Shape parameters for the distribution
    **kwds : dict, optional
        Additional parameters (loc, scale, etc.)

    Returns
    -------
    moment : float or ndarray
        n-th non-central moment
    """
    raise NotImplementedError("Moment is not implemented for this distribution.")

skew classmethod

skew(*args, **kwds)

Skewness of the distribution.

Parameters

args : array_like Shape parameters for the distribution *kwds : dict, optional Additional parameters (loc, scale, etc.)

Returns

skew : float or ndarray Skewness of the distribution

Source code in probjax/stats/base.py
@classmethod
def skew(cls, *args: Any, **kwds: Any) -> ArrayLike:
    """Skewness of the distribution.

    Parameters
    ----------
    *args : array_like
        Shape parameters for the distribution
    **kwds : dict, optional
        Additional parameters (loc, scale, etc.)

    Returns
    -------
    skew : float or ndarray
        Skewness of the distribution
    """
    raise NotImplementedError("Skewness is not implemented for this distribution.")

kurtosis classmethod

kurtosis(*args, **kwds)

Kurtosis of the distribution.

Parameters

args : array_like Shape parameters for the distribution *kwds : dict, optional Additional parameters (loc, scale, etc.)

Returns

kurtosis : float or ndarray Kurtosis of the distribution (Fisher's definition, kurtosis - 3)

Source code in probjax/stats/base.py
@classmethod
def kurtosis(cls, *args: Any, **kwds: Any) -> ArrayLike:
    """Kurtosis of the distribution.

    Parameters
    ----------
    *args : array_like
        Shape parameters for the distribution
    **kwds : dict, optional
        Additional parameters (loc, scale, etc.)

    Returns
    -------
    kurtosis : float or ndarray
        Kurtosis of the distribution (Fisher's definition, kurtosis - 3)
    """
    raise NotImplementedError("Kurtosis is not implemented for this distribution.")

fit classmethod

fit(data, **kwds)

Maximum likelihood estimation of distribution parameters.

Parameters

data : array_like Data to fit the distribution to **kwds : dict, optional Additional parameters for the optimization

Returns

params : tuple The fitted parameters of the distribution

Source code in probjax/stats/base.py
@classmethod
def fit(cls, data: ArrayLike, **kwds: Any) -> tuple[Array, ...]:
    """Maximum likelihood estimation of distribution parameters.

    Parameters
    ----------
    data : array_like
        Data to fit the distribution to
    **kwds : dict, optional
        Additional parameters for the optimization

    Returns
    -------
    params : tuple
        The fitted parameters of the distribution
    """
    raise NotImplementedError("Not implemented for this distribution.")

fit_params classmethod

fit_params(data, **kwds)

Fit and return parameters keyed by their declared names.

Source code in probjax/stats/base.py
@classmethod
def fit_params(cls, data: ArrayLike, **kwds: Any) -> dict[str, Any]:
    """Fit and return parameters keyed by their declared names."""
    fitted = cls.fit(data, **kwds)
    values = fitted if isinstance(fitted, tuple) else (fitted,)
    return dict(zip(cls.parameters, values, strict=False))

probjax.stats.rv_continuous

Bases: rv_generic

Base class for continuous random variables.

Source code in probjax/stats/base.py
class rv_continuous(rv_generic):
    """Base class for continuous random variables."""

    def freeze(self, *args: Any, **kwds: Any) -> "rv_continuous_frozen":
        """Freeze the distribution for the given arguments."""
        return cast(
            "rv_continuous_frozen",
            self._freeze_as(rv_continuous_frozen, *args, **kwds),
        )

    @classmethod
    def pdf(cls, x: ArrayLike, *args: Any, **kwds: Any) -> Array:
        """Probability density function at x of the given RV."""
        args, kwds = cls._parse_args(*args, **kwds)
        return jnp.exp(cls.logpdf(x, *args, **kwds))

    @classmethod
    def logpdf(cls, x: ArrayLike, *args: Any, **kwds: Any) -> Array:
        """Log of the probability density function at x of the given RV."""
        raise NotImplementedError("Logpdf is not implemented for this distribution.")

    @classmethod
    def fit(cls, data: ArrayLike, **kwds: Any) -> tuple[Array, ...]:
        """Maximum likelihood estimation in unconstrained parameter space."""
        return cls._fit_mle(data, **kwds)

freeze

freeze(*args, **kwds)

Freeze the distribution for the given arguments.

Source code in probjax/stats/base.py
def freeze(self, *args: Any, **kwds: Any) -> "rv_continuous_frozen":
    """Freeze the distribution for the given arguments."""
    return cast(
        "rv_continuous_frozen",
        self._freeze_as(rv_continuous_frozen, *args, **kwds),
    )

pdf classmethod

pdf(x, *args, **kwds)

Probability density function at x of the given RV.

Source code in probjax/stats/base.py
@classmethod
def pdf(cls, x: ArrayLike, *args: Any, **kwds: Any) -> Array:
    """Probability density function at x of the given RV."""
    args, kwds = cls._parse_args(*args, **kwds)
    return jnp.exp(cls.logpdf(x, *args, **kwds))

logpdf classmethod

logpdf(x, *args, **kwds)

Log of the probability density function at x of the given RV.

Source code in probjax/stats/base.py
@classmethod
def logpdf(cls, x: ArrayLike, *args: Any, **kwds: Any) -> Array:
    """Log of the probability density function at x of the given RV."""
    raise NotImplementedError("Logpdf is not implemented for this distribution.")

fit classmethod

fit(data, **kwds)

Maximum likelihood estimation in unconstrained parameter space.

Source code in probjax/stats/base.py
@classmethod
def fit(cls, data: ArrayLike, **kwds: Any) -> tuple[Array, ...]:
    """Maximum likelihood estimation in unconstrained parameter space."""
    return cls._fit_mle(data, **kwds)

probjax.stats.rv_discrete

Bases: rv_generic

Base class for discrete random variables.

Source code in probjax/stats/base.py
class rv_discrete(rv_generic):
    """Base class for discrete random variables."""

    def freeze(self, *args: Any, **kwds: Any) -> "rv_discrete_frozen":
        """Freeze the distribution for the given arguments."""
        return cast(
            "rv_discrete_frozen",
            self._freeze_as(rv_discrete_frozen, *args, **kwds),
        )

    @classmethod
    @abstractmethod
    def pmf(cls, k: ArrayLike, *args: Any, **kwds: Any) -> Array:
        """Probability mass function at k of the given RV."""
        ...

    @classmethod
    def logpmf(cls, k: ArrayLike, *args: Any, **kwds: Any) -> Array:
        """Log of the probability mass function at k of the given RV."""
        return jnp.log(cls.pmf(k, *args, **kwds))

    @classmethod
    def logpdf(cls, x: ArrayLike, *args: Any, **kwds: Any) -> Array:
        """Log of the probability density function at x of the given RV."""
        return cls.logpmf(x, *args, **kwds)

    @classmethod
    def pdf(cls, x: ArrayLike, *args: Any, **kwds: Any) -> Array:
        """Probability density function at x of the given RV."""
        return jnp.exp(cls.logpdf(x, *args, **kwds))

freeze

freeze(*args, **kwds)

Freeze the distribution for the given arguments.

Source code in probjax/stats/base.py
def freeze(self, *args: Any, **kwds: Any) -> "rv_discrete_frozen":
    """Freeze the distribution for the given arguments."""
    return cast(
        "rv_discrete_frozen",
        self._freeze_as(rv_discrete_frozen, *args, **kwds),
    )

pmf abstractmethod classmethod

pmf(k, *args, **kwds)

Probability mass function at k of the given RV.

Source code in probjax/stats/base.py
@classmethod
@abstractmethod
def pmf(cls, k: ArrayLike, *args: Any, **kwds: Any) -> Array:
    """Probability mass function at k of the given RV."""
    ...

logpmf classmethod

logpmf(k, *args, **kwds)

Log of the probability mass function at k of the given RV.

Source code in probjax/stats/base.py
@classmethod
def logpmf(cls, k: ArrayLike, *args: Any, **kwds: Any) -> Array:
    """Log of the probability mass function at k of the given RV."""
    return jnp.log(cls.pmf(k, *args, **kwds))

logpdf classmethod

logpdf(x, *args, **kwds)

Log of the probability density function at x of the given RV.

Source code in probjax/stats/base.py
@classmethod
def logpdf(cls, x: ArrayLike, *args: Any, **kwds: Any) -> Array:
    """Log of the probability density function at x of the given RV."""
    return cls.logpmf(x, *args, **kwds)

pdf classmethod

pdf(x, *args, **kwds)

Probability density function at x of the given RV.

Source code in probjax/stats/base.py
@classmethod
def pdf(cls, x: ArrayLike, *args: Any, **kwds: Any) -> Array:
    """Probability density function at x of the given RV."""
    return jnp.exp(cls.logpdf(x, *args, **kwds))

probjax.stats.rv_multivariate

Bases: rv_continuous

Base class for multivariate continuous random variables.

Source code in probjax/stats/base.py
class rv_multivariate(rv_continuous):
    """Base class for multivariate continuous random variables."""

    multivariate: ClassVar[bool] = True

    def freeze(self, *args: Any, **kwds: Any) -> "rv_continuous_frozen":
        """Freeze the multivariate distribution for the given arguments."""
        frozen_cls = cast(type["rv_frozen"], globals()["rv_multivariate_frozen"])
        return cast(
            "rv_continuous_frozen",
            self._freeze_as(frozen_cls, *args, **kwds),
        )

    @classmethod
    @abstractmethod
    def _multivariate_batch_event_shape(
        cls, *args: Any, **kwds: Any
    ) -> tuple[Tuple[int, ...], Tuple[int, ...]]:
        """Infer batch and event shape for multivariate distributions."""
        ...

freeze

freeze(*args, **kwds)

Freeze the multivariate distribution for the given arguments.

Source code in probjax/stats/base.py
def freeze(self, *args: Any, **kwds: Any) -> "rv_continuous_frozen":
    """Freeze the multivariate distribution for the given arguments."""
    frozen_cls = cast(type["rv_frozen"], globals()["rv_multivariate_frozen"])
    return cast(
        "rv_continuous_frozen",
        self._freeze_as(frozen_cls, *args, **kwds),
    )

probjax.stats.rv_frozen

Bases: DistributionAPI

Source code in probjax/stats/base.py
class rv_frozen(DistributionAPI, metaclass=FrozenDistributionMeta):
    def __init__(
        self,
        dist,
        *args,
        **kwds,
    ):
        frozen_args = rv_generic._bind_frozen_args_for_dist(dist, tuple(args), kwds)
        self.args = frozen_args.args
        self.kwds = frozen_args.kwds
        self.dist = dist

        self._parameter_values = dict(frozen_args.parameter_values)
        self._parameter_aliases = dict(getattr(self.dist, "parameter_aliases", {}))
        self._call_kwds = self._build_call_kwargs()

        self._batch_shape, self._event_shape = self._compute_batch_and_event_shape(
            *self.args, **self.kwds
        )

        for param_name, value in self._parameter_values.items():
            if hasattr(type(self), param_name):
                continue
            object.__setattr__(self, param_name, value)

        super().__init__()

    def _build_call_kwargs(self) -> dict[str, Any]:
        """Build canonical kwargs for calling distribution methods."""
        call_kwds = dict(self.kwds)
        parameters = tuple(getattr(self.dist, "parameters", {}).keys())
        for idx, name in enumerate(parameters):
            if idx < len(self.args) and name not in call_kwds:
                call_kwds[name] = self.args[idx]
        return call_kwds

    def _call_dist(self, method_name: str, *args: Any, **kwds: Any) -> Any:
        """Call a distribution method using canonical frozen parameters."""
        method = getattr(self.dist, method_name)
        call_kwds = dict(self._call_kwds)
        call_kwds.update(kwds)
        return method(*args, **call_kwds)

    def _bind_rvs(
        self,
        rng: RngKey,
        shape: Tuple[int, ...],
        name: Optional[str] = None,
        **kwargs: Any,
    ) -> Array:
        from probjax.core.custom_primitives.random_variable import rv_p

        call_kwds = dict(self._call_kwds)
        call_kwds.update(kwargs)

        rvs_fn = getattr(self.dist, "_rvs_impl", None)
        if rvs_fn is None:
            rvs_fn = self.dist.rvs

        logpdf_fn = getattr(self.dist, "logpdf", None)
        if logpdf_fn is None:
            logpdf_fn = getattr(type(self.dist), "logpdf", None)

        return rv_p.bind(
            rng,
            shape=shape,
            dist=self.dist,
            name=name,
            rvs_fn=rvs_fn,
            logpdf_fn=logpdf_fn,
            kwds=call_kwds,
        )

    def _compute_batch_and_event_shape(
        self, *args: Any, **kwds: Any
    ) -> tuple[Tuple[int, ...], Tuple[int, ...]]:
        """Compute the batch and event shape of the distribution."""
        # Get shapes from the distribution arguments
        batch_shapes = []

        num_params = len(self.dist.parameters)

        # Check args for shapes
        for arg in args:
            if isinstance(arg, (jnp.ndarray, np.ndarray)):
                batch_shapes.append(arg.shape)

        # Check kwargs for shapes
        for v in kwds.values():
            if isinstance(v, (jnp.ndarray, np.ndarray)):
                batch_shapes.append(v.shape)

        if len(batch_shapes) > num_params:
            raise ValueError(
                "Too many args/kwargs provided for distribution "
                f"{self.dist.__class__.__name__}."
                f"Expected {self.dist.parameters} shapes, got {len(batch_shapes)}."
            )

        # Compute final shapes assuming rv is univariate
        if len(batch_shapes) > 0:
            batch_shape_raw = jnp.broadcast_shapes(*batch_shapes)
        else:
            batch_shape_raw = ()

        batch_shape: Tuple[int, ...] = tuple(int(dim) for dim in batch_shape_raw)
        event_shape: Tuple[int, ...] = tuple()

        return batch_shape, event_shape

    @property
    def batch_shape(self) -> Tuple[int, ...]:
        return self._batch_shape

    @property
    def event_shape(self) -> Tuple[int, ...]:
        return self._event_shape

    @property
    def params(self) -> dict[str, Any]:
        """Name-keyed constrained parameters."""
        return dict(self._parameter_values)

    @property
    def unconstrained_params(self) -> dict[str, Any]:
        """Name-keyed parameters mapped through the constraint registry."""
        return self.dist.params_to_unconstrained(self.params)

    def __getattr__(self, name: str) -> Any:
        """Expose frozen parameters as attributes for SciPy-like ergonomics."""
        if name in self._parameter_values:
            return self._parameter_values[name]
        alias = self._parameter_aliases.get(name)
        if alias is not None and alias in self._parameter_values:
            return self._parameter_values[alias]
        raise AttributeError(f"{type(self).__name__!s} has no attribute {name!r}")

    def _format_arg(self, arg):
        """Format argument for string representation, showing shapes for arrays."""
        # Check if arg is a JAX array
        if isinstance(arg, (jnp.ndarray, np.ndarray)):
            return f"array(shape={arg.shape}, dtype={arg.dtype})"
        # For other types, just use normal string representation
        return str(arg)

    def __str__(self):
        """String representation with array shapes instead of content."""
        args_str = ', '.join(self._format_arg(arg) for arg in self.args)
        kwargs_str = ', '.join(
            f'{k}={self._format_arg(v)}' for k, v in self.kwds.items()
        )

        if args_str and kwargs_str:
            params_str = f"{args_str}, {kwargs_str}"
        elif args_str:
            params_str = args_str
        elif kwargs_str:
            params_str = kwargs_str
        else:
            params_str = ""

        # Get name from class or distribution
        name = (
            getattr(self, 'name', None)
            or getattr(self.dist, 'name', None)
            or self.dist.__class__.__name__
        )
        return f"{name}({params_str})"

    def __repr__(self):
        """Representation for debugging."""
        return self.__str__()

    def tree_flatten(self):
        """Return a flattened representation for JAX pytree."""
        # Flatten the distribution
        children = (self.args, self.kwds)
        # Auxiliary data that can be useful for reconstruction
        aux_data = {'dist': self.dist}
        return (children, aux_data)

    @classmethod
    def tree_unflatten(cls, aux_data, children):
        """Reconstruct an instance from flattened representation."""
        args, kwds = children
        dist = aux_data['dist']
        # Create and return a new frozen instance
        instance = cls(dist, *args, **kwds)
        return instance

    def cdf(self, x: ArrayLike):
        """Cumulative distribution function (i.e. P(X <= x)).

        Parameters
        ----------
        x : array_like
            Points at which to evaluate the cumulative distribution function.

        Returns
        -------
        cdf : ndarray or scalar
            Cumulative distribution function evaluated at x
        """
        return self._call_dist("cdf", x)

    def logcdf(self, x: ArrayLike):
        """Log of the cumulative distribution function (i.e. log(P(X <= x))).

        Parameters
        ----------
        x : array_like
            Points at which to evaluate the cumulative distribution function.

        Returns
        -------
        logcdf : ndarray or scalar
            Log of the cumulative distribution function evaluated at x
        """
        return self._call_dist("logcdf", x)

    def ppf(self, q: ArrayLike):
        """Percent point function (inverse of cdf).

        Parameters
        ----------
        q : array_like
            Probability at which to evaluate the inverse cumulative distribution
            function.

        Returns
        -------
        ppf : ndarray or scalar
            Percent point function evaluated at q
        """
        return self._call_dist("ppf", q)

    def isf(self, q: ArrayLike):
        """Inverse survival function (1 - ppf) of the frozen distribution."""
        return self._call_dist("isf", q)

    def rvs(
        self,
        rng: RngKey,
        shape: Tuple[int, ...] = (),
        name: Optional[str] = None,
        **kwargs,
    ):
        """Random variates of the frozen distribution.

        Parameters
        ----------
        rng : jax.random.PRNGKey
            The random key used for sampling
        shape : tuple of ints, optional
            The shape of the samples to draw. Default is ().
        name : str, optional
            Optional site name used when tracing probabilistic programs.

        Returns
        -------
        rvs : ndarray or scalar
            Random variates of given shape
        """
        return self._bind_rvs(rng, shape=shape, name=name, **kwargs)

    def sf(self, x: ArrayLike):
        """Survival function (1 - cdf)."""
        return self._call_dist("sf", x)

    def logsf(self, x: ArrayLike):
        """Log of the survival function (1 - cdf)."""
        return self._call_dist("logsf", x)

    def stats(self, moments: str = 'mv'):
        """Returns mean, variance, skew, or kurtosis of the frozen distribution.

        Parameters
        ----------
        moments : str, optional
            Which moments to compute: 'mv' (default), 'v', 's', 'k'.

        Returns
        -------
        stats : ndarray or scalar
            Mean, variance, skew, or kurtosis of the distribution
        """
        return self._call_dist("stats", moments=moments)

    def median(self):
        """Median of the distribution."""
        return self._call_dist("median")

    def mean(self):
        """Mean of the distribution."""
        return self._call_dist("mean")

    def var(self):
        """Variance of the distribution."""
        return self._call_dist("var")

    def std(self):
        """Standard deviation of the distribution."""
        return self._call_dist("std")

    def moment(self, order: Optional[int] = None):
        """Non-central moment of the distribution."""
        return self._call_dist("moment", order)

    def entropy(self):
        """Entropy of the distribution."""
        return self._call_dist("entropy")

    def interval(self, confidence: Optional[ArrayLike] = None):
        """Confidence interval with equal areas around the median of the distribution.

        Parameters
        ----------
        confidence : array_like, optional
            Confidence level for the interval. Default is 0.95.

        Returns
        """
        return self._call_dist("interval", confidence)

    def support(self):
        """Support of the frozen distribution."""
        return self._call_dist("support")

params property

params

Name-keyed constrained parameters.

unconstrained_params property

unconstrained_params

Name-keyed parameters mapped through the constraint registry.

tree_flatten

tree_flatten()

Return a flattened representation for JAX pytree.

Source code in probjax/stats/base.py
def tree_flatten(self):
    """Return a flattened representation for JAX pytree."""
    # Flatten the distribution
    children = (self.args, self.kwds)
    # Auxiliary data that can be useful for reconstruction
    aux_data = {'dist': self.dist}
    return (children, aux_data)

tree_unflatten classmethod

tree_unflatten(aux_data, children)

Reconstruct an instance from flattened representation.

Source code in probjax/stats/base.py
@classmethod
def tree_unflatten(cls, aux_data, children):
    """Reconstruct an instance from flattened representation."""
    args, kwds = children
    dist = aux_data['dist']
    # Create and return a new frozen instance
    instance = cls(dist, *args, **kwds)
    return instance

cdf

cdf(x)

Cumulative distribution function (i.e. P(X <= x)).

Parameters

x : array_like Points at which to evaluate the cumulative distribution function.

Returns

cdf : ndarray or scalar Cumulative distribution function evaluated at x

Source code in probjax/stats/base.py
def cdf(self, x: ArrayLike):
    """Cumulative distribution function (i.e. P(X <= x)).

    Parameters
    ----------
    x : array_like
        Points at which to evaluate the cumulative distribution function.

    Returns
    -------
    cdf : ndarray or scalar
        Cumulative distribution function evaluated at x
    """
    return self._call_dist("cdf", x)

logcdf

logcdf(x)

Log of the cumulative distribution function (i.e. log(P(X <= x))).

Parameters

x : array_like Points at which to evaluate the cumulative distribution function.

Returns

logcdf : ndarray or scalar Log of the cumulative distribution function evaluated at x

Source code in probjax/stats/base.py
def logcdf(self, x: ArrayLike):
    """Log of the cumulative distribution function (i.e. log(P(X <= x))).

    Parameters
    ----------
    x : array_like
        Points at which to evaluate the cumulative distribution function.

    Returns
    -------
    logcdf : ndarray or scalar
        Log of the cumulative distribution function evaluated at x
    """
    return self._call_dist("logcdf", x)

ppf

ppf(q)

Percent point function (inverse of cdf).

Parameters

q : array_like Probability at which to evaluate the inverse cumulative distribution function.

Returns

ppf : ndarray or scalar Percent point function evaluated at q

Source code in probjax/stats/base.py
def ppf(self, q: ArrayLike):
    """Percent point function (inverse of cdf).

    Parameters
    ----------
    q : array_like
        Probability at which to evaluate the inverse cumulative distribution
        function.

    Returns
    -------
    ppf : ndarray or scalar
        Percent point function evaluated at q
    """
    return self._call_dist("ppf", q)

isf

isf(q)

Inverse survival function (1 - ppf) of the frozen distribution.

Source code in probjax/stats/base.py
def isf(self, q: ArrayLike):
    """Inverse survival function (1 - ppf) of the frozen distribution."""
    return self._call_dist("isf", q)

rvs

rvs(rng, shape=(), name=None, **kwargs)

Random variates of the frozen distribution.

Parameters

rng : jax.random.PRNGKey The random key used for sampling shape : tuple of ints, optional The shape of the samples to draw. Default is (). name : str, optional Optional site name used when tracing probabilistic programs.

Returns

rvs : ndarray or scalar Random variates of given shape

Source code in probjax/stats/base.py
def rvs(
    self,
    rng: RngKey,
    shape: Tuple[int, ...] = (),
    name: Optional[str] = None,
    **kwargs,
):
    """Random variates of the frozen distribution.

    Parameters
    ----------
    rng : jax.random.PRNGKey
        The random key used for sampling
    shape : tuple of ints, optional
        The shape of the samples to draw. Default is ().
    name : str, optional
        Optional site name used when tracing probabilistic programs.

    Returns
    -------
    rvs : ndarray or scalar
        Random variates of given shape
    """
    return self._bind_rvs(rng, shape=shape, name=name, **kwargs)

sf

sf(x)

Survival function (1 - cdf).

Source code in probjax/stats/base.py
def sf(self, x: ArrayLike):
    """Survival function (1 - cdf)."""
    return self._call_dist("sf", x)

logsf

logsf(x)

Log of the survival function (1 - cdf).

Source code in probjax/stats/base.py
def logsf(self, x: ArrayLike):
    """Log of the survival function (1 - cdf)."""
    return self._call_dist("logsf", x)

stats

stats(moments='mv')

Returns mean, variance, skew, or kurtosis of the frozen distribution.

Parameters

moments : str, optional Which moments to compute: 'mv' (default), 'v', 's', 'k'.

Returns

stats : ndarray or scalar Mean, variance, skew, or kurtosis of the distribution

Source code in probjax/stats/base.py
def stats(self, moments: str = 'mv'):
    """Returns mean, variance, skew, or kurtosis of the frozen distribution.

    Parameters
    ----------
    moments : str, optional
        Which moments to compute: 'mv' (default), 'v', 's', 'k'.

    Returns
    -------
    stats : ndarray or scalar
        Mean, variance, skew, or kurtosis of the distribution
    """
    return self._call_dist("stats", moments=moments)

median

median()

Median of the distribution.

Source code in probjax/stats/base.py
def median(self):
    """Median of the distribution."""
    return self._call_dist("median")

mean

mean()

Mean of the distribution.

Source code in probjax/stats/base.py
def mean(self):
    """Mean of the distribution."""
    return self._call_dist("mean")

var

var()

Variance of the distribution.

Source code in probjax/stats/base.py
def var(self):
    """Variance of the distribution."""
    return self._call_dist("var")

std

std()

Standard deviation of the distribution.

Source code in probjax/stats/base.py
def std(self):
    """Standard deviation of the distribution."""
    return self._call_dist("std")

moment

moment(order=None)

Non-central moment of the distribution.

Source code in probjax/stats/base.py
def moment(self, order: Optional[int] = None):
    """Non-central moment of the distribution."""
    return self._call_dist("moment", order)

entropy

entropy()

Entropy of the distribution.

Source code in probjax/stats/base.py
def entropy(self):
    """Entropy of the distribution."""
    return self._call_dist("entropy")

interval

interval(confidence=None)

Confidence interval with equal areas around the median of the distribution.

Parameters

confidence : array_like, optional Confidence level for the interval. Default is 0.95.

Returns

Source code in probjax/stats/base.py
def interval(self, confidence: Optional[ArrayLike] = None):
    """Confidence interval with equal areas around the median of the distribution.

    Parameters
    ----------
    confidence : array_like, optional
        Confidence level for the interval. Default is 0.95.

    Returns
    """
    return self._call_dist("interval", confidence)

support

support()

Support of the frozen distribution.

Source code in probjax/stats/base.py
def support(self):
    """Support of the frozen distribution."""
    return self._call_dist("support")

probjax.stats.rv_exponential_family

Bases: rv_generic

Base class for exponential family random variables.

Source code in probjax/stats/base.py
class rv_exponential_family(rv_generic):
    """Base class for exponential family random variables."""

    @classmethod
    def natural_parameters(cls, *args: Any, **kwds: Any) -> Array:
        """Natural parameters of the distribution."""
        raise NotImplementedError(
            "Natural parameters are not implemented for this distribution."
        )

    @classmethod
    def sufficient_statistics(cls, x: ArrayLike, *args: Any, **kwds: Any) -> Array:
        """Sufficient statistics of the distribution."""
        raise NotImplementedError(
            "Sufficient statistics are not implemented for this distribution."
        )

    @classmethod
    def log_partition(cls, *args: Any, **kwds: Any) -> Array:
        """Log partition function of the distribution."""
        raise NotImplementedError(
            "Log partition function is not implemented for this distribution."
        )

    @classmethod
    def fit(cls, data: ArrayLike, **kwds: Any) -> tuple[Array, ...]:
        """Maximum likelihood estimation in unconstrained parameter space."""
        return cls._fit_mle(data, **kwds)

natural_parameters classmethod

natural_parameters(*args, **kwds)

Natural parameters of the distribution.

Source code in probjax/stats/base.py
@classmethod
def natural_parameters(cls, *args: Any, **kwds: Any) -> Array:
    """Natural parameters of the distribution."""
    raise NotImplementedError(
        "Natural parameters are not implemented for this distribution."
    )

sufficient_statistics classmethod

sufficient_statistics(x, *args, **kwds)

Sufficient statistics of the distribution.

Source code in probjax/stats/base.py
@classmethod
def sufficient_statistics(cls, x: ArrayLike, *args: Any, **kwds: Any) -> Array:
    """Sufficient statistics of the distribution."""
    raise NotImplementedError(
        "Sufficient statistics are not implemented for this distribution."
    )

log_partition classmethod

log_partition(*args, **kwds)

Log partition function of the distribution.

Source code in probjax/stats/base.py
@classmethod
def log_partition(cls, *args: Any, **kwds: Any) -> Array:
    """Log partition function of the distribution."""
    raise NotImplementedError(
        "Log partition function is not implemented for this distribution."
    )

fit classmethod

fit(data, **kwds)

Maximum likelihood estimation in unconstrained parameter space.

Source code in probjax/stats/base.py
@classmethod
def fit(cls, data: ArrayLike, **kwds: Any) -> tuple[Array, ...]:
    """Maximum likelihood estimation in unconstrained parameter space."""
    return cls._fit_mle(data, **kwds)

probjax.stats.rv_spherical

Bases: rv_multivariate

Base class for spherical distributions on the unit sphere.

Source code in probjax/stats/base.py
class rv_spherical(rv_multivariate):
    """Base class for spherical distributions on the unit sphere."""

    def freeze(self, *args: Any, **kwds: Any) -> "rv_continuous_frozen":
        """Freeze the spherical distribution for the given arguments."""
        frozen_cls = cast(type["rv_frozen"], globals()["rv_spherical_frozen"])
        return cast(
            "rv_continuous_frozen",
            self._freeze_as(frozen_cls, *args, **kwds),
        )

    @classmethod
    @abstractmethod
    def mean_direction_vector(cls, *args: Any, **kwds: Any) -> ArrayLike:
        """Representative principal direction of the spherical distribution."""
        ...

    @classmethod
    @abstractmethod
    def mean_direction_dyad(cls, *args: Any, **kwds: Any) -> Array:
        """Expected dyadic product :math:`E[XX^T]` for spherical random vectors."""
        ...

    @classmethod
    def dispersion(cls, *args: Any, **kwds: Any) -> Array:
        """Dispersion matrix defined as :math:`E[XX^T] - I/d`."""
        dyad = jnp.asarray(cls.mean_direction_dyad(*args, **kwds))
        dim = dyad.shape[-1]
        identity = jnp.eye(dim, dtype=dyad.dtype) / jnp.asarray(dim, dtype=dyad.dtype)
        identity = jnp.broadcast_to(identity, dyad.shape)
        return dyad - identity

    @classmethod
    def axial_dispersion(cls, *args: Any, **kwds: Any) -> Array:
        """Dispersion along the principal axis :math:`1 - mu^T E[XX^T] mu`."""
        mean_vec = jnp.asarray(cls.mean_direction_vector(*args, **kwds))
        dyad = jnp.asarray(cls.mean_direction_dyad(*args, **kwds))
        axial_moment = jnp.einsum("...i,...ij,...j->...", mean_vec, dyad, mean_vec)
        one = jnp.asarray(1.0, dtype=axial_moment.dtype)
        zero = jnp.asarray(0.0, dtype=axial_moment.dtype)
        return jnp.maximum(zero, one - axial_moment)

freeze

freeze(*args, **kwds)

Freeze the spherical distribution for the given arguments.

Source code in probjax/stats/base.py
def freeze(self, *args: Any, **kwds: Any) -> "rv_continuous_frozen":
    """Freeze the spherical distribution for the given arguments."""
    frozen_cls = cast(type["rv_frozen"], globals()["rv_spherical_frozen"])
    return cast(
        "rv_continuous_frozen",
        self._freeze_as(frozen_cls, *args, **kwds),
    )

mean_direction_vector abstractmethod classmethod

mean_direction_vector(*args, **kwds)

Representative principal direction of the spherical distribution.

Source code in probjax/stats/base.py
@classmethod
@abstractmethod
def mean_direction_vector(cls, *args: Any, **kwds: Any) -> ArrayLike:
    """Representative principal direction of the spherical distribution."""
    ...

mean_direction_dyad abstractmethod classmethod

mean_direction_dyad(*args, **kwds)

Expected dyadic product :math:E[XX^T] for spherical random vectors.

Source code in probjax/stats/base.py
@classmethod
@abstractmethod
def mean_direction_dyad(cls, *args: Any, **kwds: Any) -> Array:
    """Expected dyadic product :math:`E[XX^T]` for spherical random vectors."""
    ...

dispersion classmethod

dispersion(*args, **kwds)

Dispersion matrix defined as :math:E[XX^T] - I/d.

Source code in probjax/stats/base.py
@classmethod
def dispersion(cls, *args: Any, **kwds: Any) -> Array:
    """Dispersion matrix defined as :math:`E[XX^T] - I/d`."""
    dyad = jnp.asarray(cls.mean_direction_dyad(*args, **kwds))
    dim = dyad.shape[-1]
    identity = jnp.eye(dim, dtype=dyad.dtype) / jnp.asarray(dim, dtype=dyad.dtype)
    identity = jnp.broadcast_to(identity, dyad.shape)
    return dyad - identity

axial_dispersion classmethod

axial_dispersion(*args, **kwds)

Dispersion along the principal axis :math:1 - mu^T E[XX^T] mu.

Source code in probjax/stats/base.py
@classmethod
def axial_dispersion(cls, *args: Any, **kwds: Any) -> Array:
    """Dispersion along the principal axis :math:`1 - mu^T E[XX^T] mu`."""
    mean_vec = jnp.asarray(cls.mean_direction_vector(*args, **kwds))
    dyad = jnp.asarray(cls.mean_direction_dyad(*args, **kwds))
    axial_moment = jnp.einsum("...i,...ij,...j->...", mean_vec, dyad, mean_vec)
    one = jnp.asarray(1.0, dtype=axial_moment.dtype)
    zero = jnp.asarray(0.0, dtype=axial_moment.dtype)
    return jnp.maximum(zero, one - axial_moment)

Continuous

probjax.stats.norm module-attribute

norm = norm_gen(name='norm')

probjax.stats.gamma module-attribute

gamma = gamma_gen(name='gamma')

probjax.stats.beta module-attribute

beta = beta_gen(name='beta')

probjax.stats.expon module-attribute

expon = expon_gen(name='expon')

probjax.stats.laplace module-attribute

laplace = laplace_gen(name='laplace')

probjax.stats.logistic module-attribute

logistic = logistic_gen(name='logistic')

probjax.stats.uniform module-attribute

uniform = uniform_gen(name='uniform')

probjax.stats.cauchy module-attribute

cauchy = cauchy_gen(name='cauchy')

probjax.stats.chi2 module-attribute

chi2 = chi2_gen(name='chi2')

probjax.stats.t module-attribute

t = t_gen(name='t')

probjax.stats.pareto module-attribute

pareto = pareto_gen(name='pareto')

probjax.stats.genpareto module-attribute

genpareto = genpareto_gen(name='genpareto')

probjax.stats.gennorm module-attribute

gennorm = gennorm_gen(name='gennorm')

probjax.stats.skewnorm module-attribute

skewnorm = skewnorm_gen(name='skewnorm')

probjax.stats.truncnorm module-attribute

truncnorm = truncnorm_gen(name='truncnorm')

Flexible univariate families

Parameterised densities intended as conditional heads for autoregressive models, or as flexible marginals in their own right.

probjax.stats.mixture_kernel module-attribute

mixture_kernel = mixture_kernel_gen(name='mixture_kernel')

probjax.stats.logistic_mixture_kernel module-attribute

logistic_mixture_kernel = logistic_mixture_kernel_gen(name='logistic_mixture_kernel')

probjax.stats.histogram module-attribute

histogram = histogram_gen(name='histogram')

probjax.stats.tailed_histogram module-attribute

tailed_histogram = tailed_histogram_gen(name='tailed_histogram')

probjax.stats.spline_normal module-attribute

spline_normal = spline_normal_gen(name='spline_normal')

Multivariate and directional

probjax.stats.multivariate_normal module-attribute

multivariate_normal = multivariate_normal_gen(name='multivariate_normal')

probjax.stats.dirichlet module-attribute

dirichlet = dirichlet_gen(name='dirichlet')

probjax.stats.vonmises module-attribute

vonmises = vonmises_gen(name='vonmises')

probjax.stats.watson module-attribute

watson = watson_gen(name='watson')

probjax.stats.bingham module-attribute

bingham = bingham_gen(name='bingham')

probjax.stats.wrapcauchy module-attribute

wrapcauchy = wrapcauchy_gen(name='wrapcauchy')

Discrete

probjax.stats.bernoulli module-attribute

bernoulli = bernoulli_gen(name='bernoulli')

probjax.stats.binomial module-attribute

binomial = binomial_gen(name='binomial')

probjax.stats.categorical module-attribute

categorical = categorical_gen(name='categorical')

probjax.stats.poisson module-attribute

poisson = poisson_gen(name='poisson')

probjax.stats.geometric module-attribute

geometric = geometric_gen(name='geometric')

probjax.stats.dirac module-attribute

dirac = dirac_gen(name='dirac')

probjax.stats.empirical

Bases: rv_discrete

An Empirical distribution that puts probability mass on observed data points.

Source code in probjax/stats/discrete/empirical.py
class empirical(rv_discrete):
    """An Empirical distribution that puts probability mass on observed data points."""

    parameters = {
        "values": None,  # No constraints on values
        "weights": simplex,  # Optional weights must sum to 1
    }

    def __init__(self, name: Optional[str] = None):
        super().__init__(name=name)

    @classmethod
    def _parse_args(cls, values, weights=None, **kwds):
        """Parse arguments for the Empirical distribution."""
        if weights is None:
            weights = jnp.ones(values.shape[0]) / values.shape[0]
        return (values, weights), kwds

    @classmethod
    def _get_support(cls, values, weights=None, **kwds):
        """Get the support of the Empirical distribution."""
        return (jnp.min(values), jnp.max(values))

    @classmethod
    def support(cls, values, weights=None, **kwds):
        """Get the support of the Empirical distribution."""
        return cls._get_support(values, weights, **kwds)

    @classmethod
    def _get_batch_shape(cls, values, weights=None, **kwds):
        """Get the batch shape of the Empirical distribution."""
        return values.shape[1:]

    @classmethod
    def _get_event_shape(cls, values, weights=None, **kwds):
        """Get the event shape of the Empirical distribution."""
        return ()

    @classmethod
    def pmf(cls, x: ArrayLike, values, weights=None, **kwds):
        """Probability mass function of the Empirical distribution."""
        x = jnp.asarray(x)
        if weights is None:
            weights = jnp.ones(values.shape[0]) / values.shape[0]
        return jnp.sum(weights * (x == values), axis=0)

    @classmethod
    def logpmf(cls, x: ArrayLike, values, weights=None, **kwds):
        """Log probability mass function of the Empirical distribution."""
        x = jnp.asarray(x)
        return jnp.log(cls.pmf(x, values, weights, **kwds))

    @classmethod
    def cdf(cls, x: ArrayLike, values, weights=None, **kwds):
        """Cumulative distribution function of the Empirical distribution."""
        x = jnp.asarray(x)
        if weights is None:
            weights = jnp.ones(values.shape[0]) / values.shape[0]
        return jnp.sum(weights * (values <= x), axis=0)

    @classmethod
    def ppf(cls, q: ArrayLike, values, weights=None, **kwds):
        """Percent point function of the Empirical distribution."""
        q = jnp.asarray(q)
        if weights is None:
            weights = jnp.ones(values.shape[0]) / values.shape[0]
        sorted_idx = jnp.argsort(values)
        sorted_values = values[sorted_idx]
        sorted_weights = weights[sorted_idx]
        cdf = jnp.cumsum(sorted_weights)
        return jnp.interp(q, cdf, sorted_values)

    @classmethod
    def _rvs_impl(
        cls,
        rng: RngKey,
        values=None,
        weights=None,
        shape: Tuple[int, ...] = (),
        **kwargs,
    ):
        """Random variates of the Empirical distribution."""
        if weights is None:
            weights = jnp.ones(values.shape[0]) / values.shape[0]
        idx = random.categorical(rng, weights, shape)
        return values[idx]

    @classmethod
    def mean(cls, values, weights=None, **kwds):
        """Mean of the Empirical distribution."""
        if weights is None:
            weights = jnp.ones(values.shape[0]) / values.shape[0]
        return jnp.sum(weights * values, axis=0)

    @classmethod
    def var(cls, values, weights=None, **kwds):
        """Variance of the Empirical distribution."""
        if weights is None:
            weights = jnp.ones(values.shape[0]) / values.shape[0]
        mean = cls.mean(values, weights, **kwds)
        return jnp.sum(weights * (values - mean) ** 2, axis=0)

    @classmethod
    def entropy(cls, values, weights=None, **kwds):
        """Entropy of the Empirical distribution."""
        if weights is None:
            weights = jnp.ones(values.shape[0]) / values.shape[0]
        return -jnp.sum(weights * jnp.log(weights))

    @classmethod
    def mode(cls, values, weights=None, **kwds):
        """Mode of the Empirical distribution."""
        if weights is None:
            weights = jnp.ones(values.shape[0]) / values.shape[0]
        return values[jnp.argmax(weights)]

    def freeze(self, values, weights=None, **kwds):
        """Freeze the Empirical distribution with the given parameters."""
        return empirical_frozen(self, values=values, weights=weights, **kwds)

support classmethod

support(values, weights=None, **kwds)

Get the support of the Empirical distribution.

Source code in probjax/stats/discrete/empirical.py
@classmethod
def support(cls, values, weights=None, **kwds):
    """Get the support of the Empirical distribution."""
    return cls._get_support(values, weights, **kwds)

pmf classmethod

pmf(x, values, weights=None, **kwds)

Probability mass function of the Empirical distribution.

Source code in probjax/stats/discrete/empirical.py
@classmethod
def pmf(cls, x: ArrayLike, values, weights=None, **kwds):
    """Probability mass function of the Empirical distribution."""
    x = jnp.asarray(x)
    if weights is None:
        weights = jnp.ones(values.shape[0]) / values.shape[0]
    return jnp.sum(weights * (x == values), axis=0)

logpmf classmethod

logpmf(x, values, weights=None, **kwds)

Log probability mass function of the Empirical distribution.

Source code in probjax/stats/discrete/empirical.py
@classmethod
def logpmf(cls, x: ArrayLike, values, weights=None, **kwds):
    """Log probability mass function of the Empirical distribution."""
    x = jnp.asarray(x)
    return jnp.log(cls.pmf(x, values, weights, **kwds))

cdf classmethod

cdf(x, values, weights=None, **kwds)

Cumulative distribution function of the Empirical distribution.

Source code in probjax/stats/discrete/empirical.py
@classmethod
def cdf(cls, x: ArrayLike, values, weights=None, **kwds):
    """Cumulative distribution function of the Empirical distribution."""
    x = jnp.asarray(x)
    if weights is None:
        weights = jnp.ones(values.shape[0]) / values.shape[0]
    return jnp.sum(weights * (values <= x), axis=0)

ppf classmethod

ppf(q, values, weights=None, **kwds)

Percent point function of the Empirical distribution.

Source code in probjax/stats/discrete/empirical.py
@classmethod
def ppf(cls, q: ArrayLike, values, weights=None, **kwds):
    """Percent point function of the Empirical distribution."""
    q = jnp.asarray(q)
    if weights is None:
        weights = jnp.ones(values.shape[0]) / values.shape[0]
    sorted_idx = jnp.argsort(values)
    sorted_values = values[sorted_idx]
    sorted_weights = weights[sorted_idx]
    cdf = jnp.cumsum(sorted_weights)
    return jnp.interp(q, cdf, sorted_values)

mean classmethod

mean(values, weights=None, **kwds)

Mean of the Empirical distribution.

Source code in probjax/stats/discrete/empirical.py
@classmethod
def mean(cls, values, weights=None, **kwds):
    """Mean of the Empirical distribution."""
    if weights is None:
        weights = jnp.ones(values.shape[0]) / values.shape[0]
    return jnp.sum(weights * values, axis=0)

var classmethod

var(values, weights=None, **kwds)

Variance of the Empirical distribution.

Source code in probjax/stats/discrete/empirical.py
@classmethod
def var(cls, values, weights=None, **kwds):
    """Variance of the Empirical distribution."""
    if weights is None:
        weights = jnp.ones(values.shape[0]) / values.shape[0]
    mean = cls.mean(values, weights, **kwds)
    return jnp.sum(weights * (values - mean) ** 2, axis=0)

entropy classmethod

entropy(values, weights=None, **kwds)

Entropy of the Empirical distribution.

Source code in probjax/stats/discrete/empirical.py
@classmethod
def entropy(cls, values, weights=None, **kwds):
    """Entropy of the Empirical distribution."""
    if weights is None:
        weights = jnp.ones(values.shape[0]) / values.shape[0]
    return -jnp.sum(weights * jnp.log(weights))

mode classmethod

mode(values, weights=None, **kwds)

Mode of the Empirical distribution.

Source code in probjax/stats/discrete/empirical.py
@classmethod
def mode(cls, values, weights=None, **kwds):
    """Mode of the Empirical distribution."""
    if weights is None:
        weights = jnp.ones(values.shape[0]) / values.shape[0]
    return values[jnp.argmax(weights)]

freeze

freeze(values, weights=None, **kwds)

Freeze the Empirical distribution with the given parameters.

Source code in probjax/stats/discrete/empirical.py
def freeze(self, values, weights=None, **kwds):
    """Freeze the Empirical distribution with the given parameters."""
    return empirical_frozen(self, values=values, weights=weights, **kwds)

Higher-order

probjax.stats.transformed

Transformed Distribution (:mod:probjax.stats.transformed)

This module implements transformed distributions that apply a bijective transformation to a base distribution.

transformed_frozen

Bases: rv_continuous_frozen

Frozen transformed distribution with base-shape metadata.

Source code in probjax/stats/transformed.py
class transformed_frozen(rv_continuous_frozen):
    """Frozen transformed distribution with base-shape metadata."""

    def __init__(self, dist, base_dist, bijector, **kwds):
        super().__init__(dist, base_dist=base_dist, bijector=bijector, **kwds)

    def _compute_batch_and_event_shape(self, base_dist, bijector, **kwds):
        del bijector, kwds
        batch_shape = tuple(int(dim) for dim in base_dist.batch_shape)
        event_shape = tuple(int(dim) for dim in base_dist.event_shape)
        return batch_shape, event_shape

transformed_gen

Bases: rv_continuous

A transformed distribution that applies a bijective transformation to a base distribution.

Source code in probjax/stats/transformed.py
class transformed_gen(rv_continuous):
    """A transformed distribution that applies a bijective transformation to a base distribution."""

    parameters = {
        "base_dist": distribution,
        "bijector": callable,  # type: ignore[dict-item]
    }
    extra_frozen_kwds = frozenset({"inverse_and_logdet"})

    def __init__(self, name: Optional[str] = None):
        super().__init__(name=name)

    @classmethod
    def _parse_args(cls, base_dist, bijector, **kwds):
        """Parse arguments for the transformed distribution."""
        return (base_dist, bijector), kwds

    def freeze(self, base_dist, bijector, **kwargs):
        """Freeze the transformed distribution with the given parameters."""
        return transformed_frozen(
            self, base_dist=base_dist, bijector=bijector, **kwargs
        )

    @classmethod
    def support(cls, base_dist, bijector, **kwds):
        """Support of the transformed distribution."""
        return real

    @classmethod
    @functools.cache
    def _get_vmapped_bijector(cls, bijector):
        """Get a single-axis vmapped bijector (per-shard under a mesh)."""
        return batch_shard(jax.vmap(bijector))

    @classmethod
    def _get_inverse_and_logdet(cls, bijector):
        """Build inverse+logabsdet function for a bijector.

        Deliberately NOT cached on the bijector object: for module-backed
        bijectors (e.g. a trained flow's nnx transformation) the inverse
        jaxpr bakes the current weights in as constants, so a cache keyed by
        object identity would keep serving stale weights after in-place
        training. Under ``jax.jit`` the tracing cost is paid once per
        compilation anyway.
        """
        return inverse_and_logabsdet(bijector)

    @classmethod
    def _get_vmapped_inverse_and_logdet(cls, bijector):
        """Get a single-axis vmapped inverse+logabsdet function.

        Wrapped in :func:`batch_shard`: under an active mesh the inverse runs
        per-shard on local batches (avoids GSPMD rematerialization in Auto
        mode; required for Explicit-axes meshes, where sharded scans inside
        flow inverses are unsupported).
        """
        return batch_shard(jax.vmap(cls._get_inverse_and_logdet(bijector)))

    @classmethod
    def _get_vmapped_inverse_and_logdet_with_override(
        cls, bijector, inverse_and_logdet_fn=None
    ):
        """Get vmapped inverse+logabsdet, optionally using a custom function.

        Falls back to the bijector's own ``inverse_and_logdet`` method when
        present (see :class:`probjax.stats.bijective.protocols.InvertibleTransformProtocol`),
        skipping jaxpr auto-inversion.
        """
        if inverse_and_logdet_fn is None:
            inverse_and_logdet_fn = getattr(bijector, "inverse_and_logdet", None)
        if inverse_and_logdet_fn is None:
            return None
        return batch_shard(jax.vmap(inverse_and_logdet_fn))

    @staticmethod
    def _flatten_by_event_shape(x: ArrayLike, event_shape: Tuple[int, ...]):
        """Flatten all leading dimensions into one axis while preserving event dims."""
        x_arr = jnp.asarray(x)
        event_shape = tuple(event_shape)

        if event_shape:
            event_ndim = len(event_shape)
            if x_arr.ndim < event_ndim:
                raise ValueError(
                    "Input has fewer dimensions than the distribution event shape."
                )
            trailing_shape = tuple(x_arr.shape[-event_ndim:])
            if trailing_shape != event_shape:
                raise ValueError(
                    "Trailing dimensions of the input must match the distribution event shape."
                )
            leading_shape = tuple(x_arr.shape[:-event_ndim])
            x_flat = jnp.reshape(x_arr, (-1,) + event_shape)
        else:
            leading_shape = tuple(x_arr.shape)
            x_flat = jnp.reshape(x_arr, (-1,))

        return x_arr, x_flat, leading_shape

    @staticmethod
    def _unflatten_by_event_shape(x_flat, leading_shape: Tuple[int, ...], event_shape):
        """Restore flattened values back to leading and event dimensions."""
        event_shape = tuple(event_shape)
        if event_shape:
            return jnp.reshape(x_flat, leading_shape + event_shape)
        return jnp.reshape(x_flat, leading_shape)

    @staticmethod
    def _ensure_univariate_event(event_shape: Tuple[int, ...]):
        """Restrict operations that only support univariate events."""
        if event_shape not in ((), (1,)):
            raise NotImplementedError(
                "This method currently supports only univariate transformed distributions."
            )

    @classmethod
    def logpdf(cls, x: ArrayLike, base_dist, bijector, inverse_and_logdet=None, **kwds):
        """Log probability density function of the transformed distribution."""
        event_shape = tuple(base_dist.event_shape)
        _, x_flat, leading_shape = cls._flatten_by_event_shape(x, event_shape)

        vmapped_inverse_and_logdet = cls._get_vmapped_inverse_and_logdet_with_override(
            bijector, inverse_and_logdet
        )
        if vmapped_inverse_and_logdet is None:
            vmapped_inverse_and_logdet = cls._get_vmapped_inverse_and_logdet(bijector)

        inv_flat, log_det_flat = vmapped_inverse_and_logdet(x_flat)
        inv_value = cls._unflatten_by_event_shape(inv_flat, leading_shape, event_shape)
        log_det = jnp.reshape(
            log_det_flat, leading_shape + tuple(log_det_flat.shape[1:])
        )
        return base_dist.logpdf(inv_value) + log_det

    @classmethod
    def pdf(cls, x: ArrayLike, base_dist, bijector, **kwds):
        """Probability density function of the transformed distribution."""
        return jnp.exp(cls.logpdf(x, base_dist, bijector, **kwds))

    @classmethod
    def cdf(cls, x: ArrayLike, base_dist, bijector, inverse_and_logdet=None, **kwds):
        """Cumulative distribution function of the transformed distribution."""
        event_shape = tuple(base_dist.event_shape)
        cls._ensure_univariate_event(event_shape)
        _, x_flat, leading_shape = cls._flatten_by_event_shape(x, event_shape)

        vmapped_inverse_and_logdet = cls._get_vmapped_inverse_and_logdet_with_override(
            bijector, inverse_and_logdet
        )
        if vmapped_inverse_and_logdet is None:
            vmapped_inverse_and_logdet = cls._get_vmapped_inverse_and_logdet(bijector)

        inv_flat, _ = vmapped_inverse_and_logdet(x_flat)
        inv_value = cls._unflatten_by_event_shape(inv_flat, leading_shape, event_shape)
        return base_dist.cdf(inv_value)

    @classmethod
    def ppf(cls, q: ArrayLike, base_dist, bijector, inverse_and_logdet=None, **kwds):
        """Percent point function of the transformed distribution."""
        del inverse_and_logdet, kwds
        event_shape = tuple(base_dist.event_shape)
        cls._ensure_univariate_event(event_shape)

        base_ppf = base_dist.ppf(q)
        _, base_ppf_flat, leading_shape = cls._flatten_by_event_shape(
            base_ppf, event_shape
        )
        vmapped_bijector = cls._get_vmapped_bijector(bijector)
        transformed_flat = vmapped_bijector(base_ppf_flat)
        return cls._unflatten_by_event_shape(
            transformed_flat, leading_shape, event_shape
        )

    @classmethod
    def _rvs_impl(
        cls,
        rng: RngKey,
        base_dist=None,
        bijector=None,
        shape: Tuple[int, ...] = (),
        **kwargs,
    ):
        """Random variates of the transformed distribution."""
        if base_dist is None or bijector is None:
            raise ValueError("Both base_dist and bijector must be provided.")
        samples = base_dist.rvs(rng, shape=shape)
        event_shape = tuple(base_dist.event_shape)
        _, samples_flat, leading_shape = cls._flatten_by_event_shape(
            samples, event_shape
        )
        vmapped_bijector = cls._get_vmapped_bijector(bijector)
        transformed_flat = vmapped_bijector(samples_flat)
        return cls._unflatten_by_event_shape(
            transformed_flat, leading_shape, event_shape
        )

    @classmethod
    def mean(cls, base_dist, bijector, **kwds):
        """Mean of the transformed distribution."""
        raise NotImplementedError("Mean not implemented for transformed distribution")

    @classmethod
    def var(cls, base_dist, bijector, **kwds):
        """Variance of the transformed distribution."""
        raise NotImplementedError(
            "Variance not implemented for transformed distribution"
        )

    @classmethod
    def entropy(cls, base_dist, bijector, inverse_and_logdet=None, **kwds):
        """Entropy of the transformed distribution."""
        raise NotImplementedError(
            "Entropy not implemented for transformed distribution"
        )

    @classmethod
    def mode(cls, base_dist, bijector, **kwds):
        """Mode of the transformed distribution."""
        raise NotImplementedError("Mode not implemented for transformed distribution")

freeze

freeze(base_dist, bijector, **kwargs)

Freeze the transformed distribution with the given parameters.

Source code in probjax/stats/transformed.py
def freeze(self, base_dist, bijector, **kwargs):
    """Freeze the transformed distribution with the given parameters."""
    return transformed_frozen(
        self, base_dist=base_dist, bijector=bijector, **kwargs
    )

support classmethod

support(base_dist, bijector, **kwds)

Support of the transformed distribution.

Source code in probjax/stats/transformed.py
@classmethod
def support(cls, base_dist, bijector, **kwds):
    """Support of the transformed distribution."""
    return real

logpdf classmethod

logpdf(x, base_dist, bijector, inverse_and_logdet=None, **kwds)

Log probability density function of the transformed distribution.

Source code in probjax/stats/transformed.py
@classmethod
def logpdf(cls, x: ArrayLike, base_dist, bijector, inverse_and_logdet=None, **kwds):
    """Log probability density function of the transformed distribution."""
    event_shape = tuple(base_dist.event_shape)
    _, x_flat, leading_shape = cls._flatten_by_event_shape(x, event_shape)

    vmapped_inverse_and_logdet = cls._get_vmapped_inverse_and_logdet_with_override(
        bijector, inverse_and_logdet
    )
    if vmapped_inverse_and_logdet is None:
        vmapped_inverse_and_logdet = cls._get_vmapped_inverse_and_logdet(bijector)

    inv_flat, log_det_flat = vmapped_inverse_and_logdet(x_flat)
    inv_value = cls._unflatten_by_event_shape(inv_flat, leading_shape, event_shape)
    log_det = jnp.reshape(
        log_det_flat, leading_shape + tuple(log_det_flat.shape[1:])
    )
    return base_dist.logpdf(inv_value) + log_det

pdf classmethod

pdf(x, base_dist, bijector, **kwds)

Probability density function of the transformed distribution.

Source code in probjax/stats/transformed.py
@classmethod
def pdf(cls, x: ArrayLike, base_dist, bijector, **kwds):
    """Probability density function of the transformed distribution."""
    return jnp.exp(cls.logpdf(x, base_dist, bijector, **kwds))

cdf classmethod

cdf(x, base_dist, bijector, inverse_and_logdet=None, **kwds)

Cumulative distribution function of the transformed distribution.

Source code in probjax/stats/transformed.py
@classmethod
def cdf(cls, x: ArrayLike, base_dist, bijector, inverse_and_logdet=None, **kwds):
    """Cumulative distribution function of the transformed distribution."""
    event_shape = tuple(base_dist.event_shape)
    cls._ensure_univariate_event(event_shape)
    _, x_flat, leading_shape = cls._flatten_by_event_shape(x, event_shape)

    vmapped_inverse_and_logdet = cls._get_vmapped_inverse_and_logdet_with_override(
        bijector, inverse_and_logdet
    )
    if vmapped_inverse_and_logdet is None:
        vmapped_inverse_and_logdet = cls._get_vmapped_inverse_and_logdet(bijector)

    inv_flat, _ = vmapped_inverse_and_logdet(x_flat)
    inv_value = cls._unflatten_by_event_shape(inv_flat, leading_shape, event_shape)
    return base_dist.cdf(inv_value)

ppf classmethod

ppf(q, base_dist, bijector, inverse_and_logdet=None, **kwds)

Percent point function of the transformed distribution.

Source code in probjax/stats/transformed.py
@classmethod
def ppf(cls, q: ArrayLike, base_dist, bijector, inverse_and_logdet=None, **kwds):
    """Percent point function of the transformed distribution."""
    del inverse_and_logdet, kwds
    event_shape = tuple(base_dist.event_shape)
    cls._ensure_univariate_event(event_shape)

    base_ppf = base_dist.ppf(q)
    _, base_ppf_flat, leading_shape = cls._flatten_by_event_shape(
        base_ppf, event_shape
    )
    vmapped_bijector = cls._get_vmapped_bijector(bijector)
    transformed_flat = vmapped_bijector(base_ppf_flat)
    return cls._unflatten_by_event_shape(
        transformed_flat, leading_shape, event_shape
    )

mean classmethod

mean(base_dist, bijector, **kwds)

Mean of the transformed distribution.

Source code in probjax/stats/transformed.py
@classmethod
def mean(cls, base_dist, bijector, **kwds):
    """Mean of the transformed distribution."""
    raise NotImplementedError("Mean not implemented for transformed distribution")

var classmethod

var(base_dist, bijector, **kwds)

Variance of the transformed distribution.

Source code in probjax/stats/transformed.py
@classmethod
def var(cls, base_dist, bijector, **kwds):
    """Variance of the transformed distribution."""
    raise NotImplementedError(
        "Variance not implemented for transformed distribution"
    )

entropy classmethod

entropy(base_dist, bijector, inverse_and_logdet=None, **kwds)

Entropy of the transformed distribution.

Source code in probjax/stats/transformed.py
@classmethod
def entropy(cls, base_dist, bijector, inverse_and_logdet=None, **kwds):
    """Entropy of the transformed distribution."""
    raise NotImplementedError(
        "Entropy not implemented for transformed distribution"
    )

mode classmethod

mode(base_dist, bijector, **kwds)

Mode of the transformed distribution.

Source code in probjax/stats/transformed.py
@classmethod
def mode(cls, base_dist, bijector, **kwds):
    """Mode of the transformed distribution."""
    raise NotImplementedError("Mode not implemented for transformed distribution")

probjax.stats.mixture

Mixture Distribution (:mod:probjax.stats.mixture)

This module implements mixture distributions that combine multiple component distributions with mixing probabilities.

mixture_frozen

Bases: rv_continuous_frozen, rv_discrete_frozen

Frozen mixture distribution.

Source code in probjax/stats/mixture.py
class mixture_frozen(rv_continuous_frozen, rv_discrete_frozen):
    """Frozen mixture distribution."""

    def __init__(self, dist, mixing_probs, components, **kwds):
        super().__init__(dist, mixing_probs=mixing_probs, components=components, **kwds)

    def _compute_batch_and_event_shape(self, mixing_probs, components, **kwds):
        """Compute the batch and event shape of the distribution."""
        batch_shape1 = mixing_probs.shape[:-1]
        num_components = mixing_probs.shape[-1]
        assert len(components) == num_components, (
            "Number of components must match number of mixing probabilities"
        )
        event_shape = components[0].event_shape
        assert all(comp.event_shape == event_shape for comp in components), (
            "All components must have the same event shape"
        )
        batch_shape2 = components[0].batch_shape
        assert all(comp.batch_shape == batch_shape2 for comp in components), (
            "All components must have the same batch shape"
        )
        batch_shape = jnp.broadcast_shapes(batch_shape1, batch_shape2)
        return batch_shape, event_shape

mixture_gen

Bases: rv_generic

A mixture distribution that combines multiple component distributions.

Source code in probjax/stats/mixture.py
class mixture_gen(rv_generic):
    """A mixture distribution that combines multiple component distributions."""

    parameters = {
        "mixing_probs": simplex,
        "components": distribution,
    }

    def __init__(self, name: Optional[str] = None):
        super().__init__(name=name)

    def __call__(self, mixing_probs, components, **kwargs) -> rv_frozen:
        """Create a frozen mixture distribution."""
        return self.freeze(mixing_probs=mixing_probs, components=components, **kwargs)

    def freeze(self, mixing_probs, components, **kwargs):
        """Freeze the mixture distribution with the given parameters."""
        return mixture_frozen(
            self, mixing_probs=mixing_probs, components=components, **kwargs
        )

    @classmethod
    def support(cls, mixing_probs, components, **kwds):
        """Get the support of the mixture distribution."""
        supports = [comp.support() for comp in components]
        if all(isinstance(s, tuple) and len(s) == 2 for s in supports):
            return (min(s[0] for s in supports), max(s[1] for s in supports))
        return tuple(
            set().union(*[s if isinstance(s, tuple) else (s,) for s in supports])
        )

    @classmethod
    def logpdf(cls, x: ArrayLike, mixing_probs, components, **kwds):
        """Log probability density function of the mixture distribution."""
        x = jnp.asarray(x)
        log_pdfs = jnp.stack([comp.logpdf(x) for comp in components], axis=-1)
        return jax.scipy.special.logsumexp(jnp.log(mixing_probs) + log_pdfs, axis=-1)

    @classmethod
    def cdf(cls, x: ArrayLike, mixing_probs, components, **kwds):
        """Cumulative distribution function of the mixture distribution."""
        x = jnp.asarray(x)
        cdfs = jnp.stack([comp.cdf(x) for comp in components], axis=-1)
        return jnp.sum(mixing_probs * cdfs, axis=-1)

    @classmethod
    def ppf(cls, q: ArrayLike, mixing_probs, components, **kwds):
        """Percent point function of the mixture distribution."""
        q = jnp.asarray(q)
        x0 = jnp.mean([comp.ppf(q) for comp in components], axis=0)
        return jax.scipy.optimize.root(
            lambda x: cls.cdf(x, mixing_probs, components) - q,
            x0,
        ).x

    @classmethod
    def _rvs_impl(
        cls,
        rng: RngKey,
        mixing_probs=None,
        components=None,
        shape: Tuple[int, ...] = (),
        **kwargs,
    ):
        """Random variates of the mixture distribution."""
        key_sample, key_cluster_membership = random.split(rng, 2)

        # Sample from all components at once
        component_samples = jnp.stack(
            [comp.rvs(key_sample, shape=shape) for comp in components], axis=-1
        )

        # Sample cluster membership
        cluster_membership = random.categorical(
            key_cluster_membership,
            mixing_probs,
            shape=shape,
        )
        while cluster_membership.ndim < component_samples.ndim:
            cluster_membership = jnp.expand_dims(cluster_membership, axis=-1)
        # Select samples based on cluster membership
        samples = jnp.take_along_axis(component_samples, cluster_membership, axis=-1)

        return jnp.squeeze(samples, axis=-1)

    @classmethod
    def mean(cls, mixing_probs, components, **kwds):
        """Mean of the mixture distribution."""
        means = jnp.stack([jnp.asarray(comp.mean()) for comp in components], axis=-1)
        return jnp.sum(mixing_probs * means, axis=-1)

    @classmethod
    def var(cls, mixing_probs, components, **kwds):
        """Variance of the mixture distribution."""
        means = jnp.stack([jnp.asarray(comp.mean()) for comp in components], axis=-1)
        vars = jnp.stack([jnp.asarray(comp.var()) for comp in components], axis=-1)
        mean = jnp.sum(mixing_probs * means, axis=-1)
        return jnp.sum(mixing_probs * (vars + (means - mean[..., None]) ** 2), axis=-1)

    @classmethod
    def mode(cls, mixing_probs, components, **kwds):
        """Mode of the mixture distribution (supports univariate mixtures)."""
        if not components:
            raise ValueError("At least one component is required to compute the mode.")

        event_shape = components[0].event_shape
        if event_shape not in ((), (1,)):
            raise NotImplementedError(
                "mixture.mode currently supports only univariate mixtures."
            )

        # Restrict implementation to mixtures of univariate Normal components for now.
        if not (
            _NORM_GEN and all(isinstance(comp.dist, _NORM_GEN) for comp in components)
        ):
            raise NotImplementedError(
                "Mode computation currently implemented only for univariate "
                "Normal mixtures."
            )

        dtype = mixing_probs.dtype

        def log_prob(x):
            return cls.logpdf(x, mixing_probs, components, **kwds)

        candidates = []
        for comp in components:
            try:
                comp_mode = jnp.asarray(comp.mode())
                candidates.append(comp_mode.reshape(()))
            except NotImplementedError:
                try:
                    comp_mean = jnp.asarray(comp.mean())
                    candidates.append(comp_mean.reshape(()))
                except NotImplementedError:
                    pass

        try:
            mixture_mean = jnp.asarray(
                cls.mean(mixing_probs, components, **kwds)
            ).reshape(())
            candidates.append(mixture_mean)
        except NotImplementedError:
            pass

        if not candidates:
            raise NotImplementedError(
                "Unable to construct candidate modes for the mixture."
            )

        candidates_arr = jnp.stack([jnp.asarray(c, dtype=dtype) for c in candidates])
        candidate_logp = log_prob(candidates_arr)
        best_idx = jnp.argmax(candidate_logp)
        best_candidate = candidates_arr[best_idx]

        # Discrete mixtures: return the best candidate directly.
        if hasattr(components[0], "pmf") or hasattr(components[0], "logpmf"):
            return best_candidate

        # Attempt a simple grid search around the mixture mean if variance is finite.
        try:
            mixture_var = jnp.asarray(
                cls.var(mixing_probs, components, **kwds)
            ).reshape(())
        except NotImplementedError:
            mixture_var = jnp.asarray(jnp.nan, dtype=dtype)

        finite_var = jnp.isfinite(mixture_var) & (mixture_var > 0)
        std = jnp.sqrt(jnp.maximum(mixture_var, jnp.asarray(1e-12, dtype=dtype)))
        span = jnp.asarray(5.0, dtype=dtype) * std
        all_points = jnp.concatenate([candidates_arr, jnp.array([best_candidate])])
        low = jnp.min(all_points)
        high = jnp.max(all_points)
        try:
            mean_val = jnp.asarray(
                cls.mean(mixing_probs, components, **kwds)
            ).reshape(())
        except NotImplementedError:
            mean_val = best_candidate

        if bool(finite_var):
            low = jnp.minimum(low, mean_val - span)
            high = jnp.maximum(high, mean_val + span)
        else:
            width = jnp.maximum(high - low, jnp.asarray(1.0, dtype=dtype))
            low = low - 0.5 * width
            high = high + 0.5 * width

        if not jnp.isfinite(low):
            low = best_candidate - jnp.asarray(5.0, dtype=dtype)
        if not jnp.isfinite(high):
            high = best_candidate + jnp.asarray(5.0, dtype=dtype)

        if high <= low:
            high = low + jnp.asarray(1.0, dtype=dtype)

        grid = jnp.linspace(low, high, num=512, dtype=dtype)
        grid_logp = log_prob(grid)
        grid_best_idx = jnp.argmax(grid_logp)
        grid_best = grid[grid_best_idx]

        # Local refinement around the best grid point.
        step = (high - low) / jnp.asarray(511.0, dtype=dtype)
        left = jnp.maximum(low, grid_best - 3 * step)
        right = jnp.minimum(high, grid_best + 3 * step)
        fine_grid = jnp.linspace(left, right, num=256, dtype=dtype)
        fine_logp = log_prob(fine_grid)
        fine_best = fine_grid[jnp.argmax(fine_logp)]

        return fine_best

    @classmethod
    def entropy(cls, mixing_probs, components, **kwds):
        """Entropy of the mixture distribution."""
        raise NotImplementedError("Entropy not implemented for mixture distribution")

    @classmethod
    def fit(
        cls,
        x: ArrayLike,
        components: Sequence[rv_frozen],
        mixing_probs_init: Optional[ArrayLike] = None,
        max_iter: int = 100,
        tol: float = 1e-4,
        rng_key: Optional[RngKey] = None,
    ):
        """Fit a finite mixture model with analytic weighted M-steps."""
        if not components:
            raise ValueError("mixture.fit requires at least one component.")
        if not all(isinstance(comp, rv_frozen) for comp in components):
            raise TypeError(
                "mixture.fit expects frozen component distributions "
                "(e.g. ``norm(loc, scale)``)."
            )

        del rng_key

        data = jnp.asarray(x)
        event_shape = components[0].event_shape
        if any(comp.event_shape != event_shape for comp in components[1:]):
            raise ValueError("All components must share the same event shape.")
        if any(comp.batch_shape for comp in components):
            raise NotImplementedError(
                "mixture.fit does not currently support batched component parameters."
            )

        if event_shape:
            if data.ndim < len(event_shape):
                raise ValueError(
                    "Observations must have enough trailing dimensions to match the "
                    "component event shape."
                )
            if tuple(data.shape[-len(event_shape) :]) != event_shape:
                raise ValueError(
                    "Trailing dimensions of the observations must match the component "
                    "event shape."
                )
            data = jnp.reshape(data, (-1,) + event_shape)
        else:
            data = jnp.reshape(jnp.asarray(data), (-1,))

        if data.shape[0] == 0:
            raise ValueError("mixture.fit requires at least one observation.")

        numeric_dtype = jnp.result_type(data.dtype, jnp.float32)
        n_components = len(components)

        if mixing_probs_init is None:
            mixing_probs = jnp.full(
                (n_components,), 1.0 / n_components, dtype=numeric_dtype
            )
        else:
            mixing_probs = jnp.asarray(mixing_probs_init, dtype=numeric_dtype)
            if mixing_probs.shape != (n_components,):
                raise ValueError("mixing_probs_init must have shape (n_components,)")
            mixing_probs = jnp.clip(mixing_probs, 1e-12)
            mixing_probs = mixing_probs / jnp.sum(mixing_probs)

        component_dists = tuple(comp.dist for comp in components)
        component_params = tuple(comp.params for comp in components)
        tol_value = jnp.asarray(tol, dtype=numeric_dtype)
        data = data.astype(numeric_dtype)

        def em_step(carry, _):
            mixing_curr, params_curr, previous_ll, done = carry

            def update(state):
                mixing_state, params_state, previous_ll_state = state
                log_pdfs = jnp.stack(
                    [
                        dist.logpdf(data, **params)
                        for dist, params in zip(
                            component_dists, params_state, strict=False
                        )
                    ],
                    axis=1,
                )
                log_weights = jnp.log(jnp.clip(mixing_state, 1e-12)) + log_pdfs
                log_norm = jax.scipy.special.logsumexp(
                    log_weights, axis=1, keepdims=True
                )
                responsibilities = jnp.exp(log_weights - log_norm)
                component_weights = jnp.sum(responsibilities, axis=0)
                mixing_next = jnp.clip(
                    component_weights / jnp.sum(component_weights), 1e-12
                )
                mixing_next /= jnp.sum(mixing_next)

                params_next = []
                for index, (dist, params) in enumerate(
                    zip(component_dists, params_state, strict=False)
                ):
                    weights = responsibilities[:, index]
                    total_weight = component_weights[index]
                    normalized_weights = weights / jnp.maximum(total_weight, 1e-12)

                    def fit_component(
                        _, dist=dist, normalized_weights=normalized_weights
                    ):
                        try:
                            return dist.fit_params(data, weights=normalized_weights)
                        except TypeError:
                            return dist.fit_params(data)

                    params_next.append(
                        lax.cond(
                            total_weight > 1e-10,
                            fit_component,
                            lambda _, params=params: params,
                            operand=None,
                        )
                    )

                log_likelihood = jnp.mean(log_norm)
                difference = jnp.where(
                    jnp.isfinite(previous_ll_state),
                    jnp.abs(log_likelihood - previous_ll_state),
                    jnp.asarray(jnp.inf, dtype=numeric_dtype),
                )
                return (
                    mixing_next,
                    tuple(params_next),
                    log_likelihood,
                    difference <= tol_value,
                )

            return lax.cond(
                done,
                lambda state: (*state, jnp.asarray(True)),
                update,
                (mixing_curr, params_curr, previous_ll),
            ), None

        initial = (
            mixing_probs,
            component_params,
            jnp.asarray(-jnp.inf, dtype=numeric_dtype),
            jnp.asarray(False),
        )
        final, _ = lax.scan(em_step, initial, xs=None, length=int(max_iter))
        mixing_probs_final, params_final, _, _ = final
        fitted_components = [
            dist.from_params(params)
            for dist, params in zip(component_dists, params_final, strict=False)
        ]
        return mixing_probs_final, fitted_components

freeze

freeze(mixing_probs, components, **kwargs)

Freeze the mixture distribution with the given parameters.

Source code in probjax/stats/mixture.py
def freeze(self, mixing_probs, components, **kwargs):
    """Freeze the mixture distribution with the given parameters."""
    return mixture_frozen(
        self, mixing_probs=mixing_probs, components=components, **kwargs
    )

support classmethod

support(mixing_probs, components, **kwds)

Get the support of the mixture distribution.

Source code in probjax/stats/mixture.py
@classmethod
def support(cls, mixing_probs, components, **kwds):
    """Get the support of the mixture distribution."""
    supports = [comp.support() for comp in components]
    if all(isinstance(s, tuple) and len(s) == 2 for s in supports):
        return (min(s[0] for s in supports), max(s[1] for s in supports))
    return tuple(
        set().union(*[s if isinstance(s, tuple) else (s,) for s in supports])
    )

logpdf classmethod

logpdf(x, mixing_probs, components, **kwds)

Log probability density function of the mixture distribution.

Source code in probjax/stats/mixture.py
@classmethod
def logpdf(cls, x: ArrayLike, mixing_probs, components, **kwds):
    """Log probability density function of the mixture distribution."""
    x = jnp.asarray(x)
    log_pdfs = jnp.stack([comp.logpdf(x) for comp in components], axis=-1)
    return jax.scipy.special.logsumexp(jnp.log(mixing_probs) + log_pdfs, axis=-1)

cdf classmethod

cdf(x, mixing_probs, components, **kwds)

Cumulative distribution function of the mixture distribution.

Source code in probjax/stats/mixture.py
@classmethod
def cdf(cls, x: ArrayLike, mixing_probs, components, **kwds):
    """Cumulative distribution function of the mixture distribution."""
    x = jnp.asarray(x)
    cdfs = jnp.stack([comp.cdf(x) for comp in components], axis=-1)
    return jnp.sum(mixing_probs * cdfs, axis=-1)

ppf classmethod

ppf(q, mixing_probs, components, **kwds)

Percent point function of the mixture distribution.

Source code in probjax/stats/mixture.py
@classmethod
def ppf(cls, q: ArrayLike, mixing_probs, components, **kwds):
    """Percent point function of the mixture distribution."""
    q = jnp.asarray(q)
    x0 = jnp.mean([comp.ppf(q) for comp in components], axis=0)
    return jax.scipy.optimize.root(
        lambda x: cls.cdf(x, mixing_probs, components) - q,
        x0,
    ).x

mean classmethod

mean(mixing_probs, components, **kwds)

Mean of the mixture distribution.

Source code in probjax/stats/mixture.py
@classmethod
def mean(cls, mixing_probs, components, **kwds):
    """Mean of the mixture distribution."""
    means = jnp.stack([jnp.asarray(comp.mean()) for comp in components], axis=-1)
    return jnp.sum(mixing_probs * means, axis=-1)

var classmethod

var(mixing_probs, components, **kwds)

Variance of the mixture distribution.

Source code in probjax/stats/mixture.py
@classmethod
def var(cls, mixing_probs, components, **kwds):
    """Variance of the mixture distribution."""
    means = jnp.stack([jnp.asarray(comp.mean()) for comp in components], axis=-1)
    vars = jnp.stack([jnp.asarray(comp.var()) for comp in components], axis=-1)
    mean = jnp.sum(mixing_probs * means, axis=-1)
    return jnp.sum(mixing_probs * (vars + (means - mean[..., None]) ** 2), axis=-1)

mode classmethod

mode(mixing_probs, components, **kwds)

Mode of the mixture distribution (supports univariate mixtures).

Source code in probjax/stats/mixture.py
@classmethod
def mode(cls, mixing_probs, components, **kwds):
    """Mode of the mixture distribution (supports univariate mixtures)."""
    if not components:
        raise ValueError("At least one component is required to compute the mode.")

    event_shape = components[0].event_shape
    if event_shape not in ((), (1,)):
        raise NotImplementedError(
            "mixture.mode currently supports only univariate mixtures."
        )

    # Restrict implementation to mixtures of univariate Normal components for now.
    if not (
        _NORM_GEN and all(isinstance(comp.dist, _NORM_GEN) for comp in components)
    ):
        raise NotImplementedError(
            "Mode computation currently implemented only for univariate "
            "Normal mixtures."
        )

    dtype = mixing_probs.dtype

    def log_prob(x):
        return cls.logpdf(x, mixing_probs, components, **kwds)

    candidates = []
    for comp in components:
        try:
            comp_mode = jnp.asarray(comp.mode())
            candidates.append(comp_mode.reshape(()))
        except NotImplementedError:
            try:
                comp_mean = jnp.asarray(comp.mean())
                candidates.append(comp_mean.reshape(()))
            except NotImplementedError:
                pass

    try:
        mixture_mean = jnp.asarray(
            cls.mean(mixing_probs, components, **kwds)
        ).reshape(())
        candidates.append(mixture_mean)
    except NotImplementedError:
        pass

    if not candidates:
        raise NotImplementedError(
            "Unable to construct candidate modes for the mixture."
        )

    candidates_arr = jnp.stack([jnp.asarray(c, dtype=dtype) for c in candidates])
    candidate_logp = log_prob(candidates_arr)
    best_idx = jnp.argmax(candidate_logp)
    best_candidate = candidates_arr[best_idx]

    # Discrete mixtures: return the best candidate directly.
    if hasattr(components[0], "pmf") or hasattr(components[0], "logpmf"):
        return best_candidate

    # Attempt a simple grid search around the mixture mean if variance is finite.
    try:
        mixture_var = jnp.asarray(
            cls.var(mixing_probs, components, **kwds)
        ).reshape(())
    except NotImplementedError:
        mixture_var = jnp.asarray(jnp.nan, dtype=dtype)

    finite_var = jnp.isfinite(mixture_var) & (mixture_var > 0)
    std = jnp.sqrt(jnp.maximum(mixture_var, jnp.asarray(1e-12, dtype=dtype)))
    span = jnp.asarray(5.0, dtype=dtype) * std
    all_points = jnp.concatenate([candidates_arr, jnp.array([best_candidate])])
    low = jnp.min(all_points)
    high = jnp.max(all_points)
    try:
        mean_val = jnp.asarray(
            cls.mean(mixing_probs, components, **kwds)
        ).reshape(())
    except NotImplementedError:
        mean_val = best_candidate

    if bool(finite_var):
        low = jnp.minimum(low, mean_val - span)
        high = jnp.maximum(high, mean_val + span)
    else:
        width = jnp.maximum(high - low, jnp.asarray(1.0, dtype=dtype))
        low = low - 0.5 * width
        high = high + 0.5 * width

    if not jnp.isfinite(low):
        low = best_candidate - jnp.asarray(5.0, dtype=dtype)
    if not jnp.isfinite(high):
        high = best_candidate + jnp.asarray(5.0, dtype=dtype)

    if high <= low:
        high = low + jnp.asarray(1.0, dtype=dtype)

    grid = jnp.linspace(low, high, num=512, dtype=dtype)
    grid_logp = log_prob(grid)
    grid_best_idx = jnp.argmax(grid_logp)
    grid_best = grid[grid_best_idx]

    # Local refinement around the best grid point.
    step = (high - low) / jnp.asarray(511.0, dtype=dtype)
    left = jnp.maximum(low, grid_best - 3 * step)
    right = jnp.minimum(high, grid_best + 3 * step)
    fine_grid = jnp.linspace(left, right, num=256, dtype=dtype)
    fine_logp = log_prob(fine_grid)
    fine_best = fine_grid[jnp.argmax(fine_logp)]

    return fine_best

entropy classmethod

entropy(mixing_probs, components, **kwds)

Entropy of the mixture distribution.

Source code in probjax/stats/mixture.py
@classmethod
def entropy(cls, mixing_probs, components, **kwds):
    """Entropy of the mixture distribution."""
    raise NotImplementedError("Entropy not implemented for mixture distribution")

fit classmethod

fit(x, components, mixing_probs_init=None, max_iter=100, tol=0.0001, rng_key=None)

Fit a finite mixture model with analytic weighted M-steps.

Source code in probjax/stats/mixture.py
@classmethod
def fit(
    cls,
    x: ArrayLike,
    components: Sequence[rv_frozen],
    mixing_probs_init: Optional[ArrayLike] = None,
    max_iter: int = 100,
    tol: float = 1e-4,
    rng_key: Optional[RngKey] = None,
):
    """Fit a finite mixture model with analytic weighted M-steps."""
    if not components:
        raise ValueError("mixture.fit requires at least one component.")
    if not all(isinstance(comp, rv_frozen) for comp in components):
        raise TypeError(
            "mixture.fit expects frozen component distributions "
            "(e.g. ``norm(loc, scale)``)."
        )

    del rng_key

    data = jnp.asarray(x)
    event_shape = components[0].event_shape
    if any(comp.event_shape != event_shape for comp in components[1:]):
        raise ValueError("All components must share the same event shape.")
    if any(comp.batch_shape for comp in components):
        raise NotImplementedError(
            "mixture.fit does not currently support batched component parameters."
        )

    if event_shape:
        if data.ndim < len(event_shape):
            raise ValueError(
                "Observations must have enough trailing dimensions to match the "
                "component event shape."
            )
        if tuple(data.shape[-len(event_shape) :]) != event_shape:
            raise ValueError(
                "Trailing dimensions of the observations must match the component "
                "event shape."
            )
        data = jnp.reshape(data, (-1,) + event_shape)
    else:
        data = jnp.reshape(jnp.asarray(data), (-1,))

    if data.shape[0] == 0:
        raise ValueError("mixture.fit requires at least one observation.")

    numeric_dtype = jnp.result_type(data.dtype, jnp.float32)
    n_components = len(components)

    if mixing_probs_init is None:
        mixing_probs = jnp.full(
            (n_components,), 1.0 / n_components, dtype=numeric_dtype
        )
    else:
        mixing_probs = jnp.asarray(mixing_probs_init, dtype=numeric_dtype)
        if mixing_probs.shape != (n_components,):
            raise ValueError("mixing_probs_init must have shape (n_components,)")
        mixing_probs = jnp.clip(mixing_probs, 1e-12)
        mixing_probs = mixing_probs / jnp.sum(mixing_probs)

    component_dists = tuple(comp.dist for comp in components)
    component_params = tuple(comp.params for comp in components)
    tol_value = jnp.asarray(tol, dtype=numeric_dtype)
    data = data.astype(numeric_dtype)

    def em_step(carry, _):
        mixing_curr, params_curr, previous_ll, done = carry

        def update(state):
            mixing_state, params_state, previous_ll_state = state
            log_pdfs = jnp.stack(
                [
                    dist.logpdf(data, **params)
                    for dist, params in zip(
                        component_dists, params_state, strict=False
                    )
                ],
                axis=1,
            )
            log_weights = jnp.log(jnp.clip(mixing_state, 1e-12)) + log_pdfs
            log_norm = jax.scipy.special.logsumexp(
                log_weights, axis=1, keepdims=True
            )
            responsibilities = jnp.exp(log_weights - log_norm)
            component_weights = jnp.sum(responsibilities, axis=0)
            mixing_next = jnp.clip(
                component_weights / jnp.sum(component_weights), 1e-12
            )
            mixing_next /= jnp.sum(mixing_next)

            params_next = []
            for index, (dist, params) in enumerate(
                zip(component_dists, params_state, strict=False)
            ):
                weights = responsibilities[:, index]
                total_weight = component_weights[index]
                normalized_weights = weights / jnp.maximum(total_weight, 1e-12)

                def fit_component(
                    _, dist=dist, normalized_weights=normalized_weights
                ):
                    try:
                        return dist.fit_params(data, weights=normalized_weights)
                    except TypeError:
                        return dist.fit_params(data)

                params_next.append(
                    lax.cond(
                        total_weight > 1e-10,
                        fit_component,
                        lambda _, params=params: params,
                        operand=None,
                    )
                )

            log_likelihood = jnp.mean(log_norm)
            difference = jnp.where(
                jnp.isfinite(previous_ll_state),
                jnp.abs(log_likelihood - previous_ll_state),
                jnp.asarray(jnp.inf, dtype=numeric_dtype),
            )
            return (
                mixing_next,
                tuple(params_next),
                log_likelihood,
                difference <= tol_value,
            )

        return lax.cond(
            done,
            lambda state: (*state, jnp.asarray(True)),
            update,
            (mixing_curr, params_curr, previous_ll),
        ), None

    initial = (
        mixing_probs,
        component_params,
        jnp.asarray(-jnp.inf, dtype=numeric_dtype),
        jnp.asarray(False),
    )
    final, _ = lax.scan(em_step, initial, xs=None, length=int(max_iter))
    mixing_probs_final, params_final, _, _ = final
    fitted_components = [
        dist.from_params(params)
        for dist, params in zip(component_dists, params_final, strict=False)
    ]
    return mixing_probs_final, fitted_components

probjax.stats.indep

Independent Distribution (:mod:probjax.stats.indep)

This module contains the Independent distribution, which treats a distribution as a batch of independent distributions.

rv_frozen_indep

Bases: rv_continuous_frozen

Frozen independent distribution.

Source code in probjax/stats/indep.py
class rv_frozen_indep(rv_continuous_frozen):
    """Frozen independent distribution."""

    def __init__(self, dist, base_dists, reinterpreted_batch_ndims, **kwargs):
        super().__init__(dist, base_dists, reinterpreted_batch_ndims, **kwargs)
        batch_shape, event_shape, split_dims, split_indices = determine_shapes(
            base_dists, reinterpreted_batch_ndims
        )
        self._batch_shape = batch_shape
        self._event_shape = event_shape
        self.split_dims = split_dims
        self.split_indices = split_indices

    def _compute_batch_and_event_shape(
        self, base_dists, reinterpreted_batch_ndims, **kwargs
    ):
        batch_shape, event_shape, _, _ = determine_shapes(
            base_dists, reinterpreted_batch_ndims
        )
        return batch_shape, event_shape

indep_gen

Bases: rv_generic

Independent random variable.

Creates an independent distribution by treating the provided distribution as a batch of independent distributions.

Parameters

*base_dists : rv_continuous_frozen Base distribution(s) to make independent. reinterpreted_batch_ndims : int, optional The number of batch dimensions that should be considered as event dimensions. Default is 1.

Source code in probjax/stats/indep.py
class indep_gen(rv_generic):
    """Independent random variable.

    Creates an independent distribution by treating the provided distribution as
    a batch of independent distributions.

    Parameters
    ----------
    *base_dists : rv_continuous_frozen
        Base distribution(s) to make independent.
    reinterpreted_batch_ndims : int, optional
        The number of batch dimensions that should be considered as event dimensions.
        Default is 1.
    """

    parameters = {
        "base_dists": distribution,
        "reinterpreted_batch_ndims": non_negative_integer,
    }

    def __init__(self, name: Optional[str] = None):
        super().__init__(name=name)

    def __call__(self, *base_dists, reinterpreted_batch_ndims=1, **kwargs):
        """Create a frozen independent distribution."""
        return self.freeze(
            base_dists=base_dists,
            reinterpreted_batch_ndims=reinterpreted_batch_ndims,
            **kwargs,
        )

    def freeze(self, base_dists, reinterpreted_batch_ndims=1, **kwargs):
        """Freeze the independent distribution with the given parameters."""
        return rv_frozen_indep(
            self,
            base_dists=base_dists,
            reinterpreted_batch_ndims=reinterpreted_batch_ndims,
            **kwargs,
        )

    @classmethod
    def support(cls, base_dists, reinterpreted_batch_ndims=1, **kwargs):
        """Support of the independent distribution."""
        if len(base_dists) == 1:
            return base_dists[0].support()
        else:
            return tuple(d.support() for d in base_dists)

    @classmethod
    def pdf(cls, x, base_dists, reinterpreted_batch_ndims=1, **kwargs):
        """Probability density function of the independent distribution."""
        return jnp.exp(cls.logpdf(x, base_dists, reinterpreted_batch_ndims, **kwargs))

    @classmethod
    def logpdf(cls, x, base_dists, reinterpreted_batch_ndims=1, **kwargs):
        """Log of the probability density function of the independent distribution."""
        batch_shape, event_shape, split_dims, split_indices = determine_shapes(
            base_dists, reinterpreted_batch_ndims
        )

        # Split the input along the last dimension
        split_value = jnp.split(x, split_indices, axis=-1)

        # Compute logpdf for each base distribution
        logpdf = sum(d.logpdf(v) for d, v in zip(base_dists, split_value, strict=False))
        # Sum up to be of shape reinterpreted_batch_ndins
        for _ in range(reinterpreted_batch_ndims):
            logpdf = jnp.sum(logpdf, axis=-1)
        return logpdf

    @classmethod
    def cdf(cls, x, base_dists, reinterpreted_batch_ndims=1, **kwargs):
        """Cumulative distribution function of the independent distribution."""
        batch_shape, event_shape, split_dims, split_indices = determine_shapes(
            base_dists, reinterpreted_batch_ndims
        )

        # Split the input along the last dimension
        split_value = jnp.split(x, split_indices, axis=-1)

        # Compute CDF for each base distribution
        cdf = jnp.prod([
            d.cdf(v) for d, v in zip(base_dists, split_value, strict=False)
        ])

        # Product up to be of shape reinterpreted_batch_ndims
        for _ in range(reinterpreted_batch_ndims):
            cdf = jnp.prod(cdf, axis=-1)
        return cdf

    @classmethod
    def _rvs_impl(
        cls,
        rng: RngKey,
        base_dists=None,
        reinterpreted_batch_ndims=1,
        shape: Tuple[int, ...] = (),
        **kwargs,
    ):
        """Random variates of the independent distribution."""
        batch_shape, event_shape, split_dims, split_indices = determine_shapes(
            base_dists, reinterpreted_batch_ndims
        )
        keys = random.split(rng, len(base_dists))

        # Generate samples for each base distribution
        samples = jnp.concatenate(
            [
                d.dist._rvs_impl(k, shape=shape, **d._call_kwds)
                for k, d in zip(keys, base_dists, strict=False)
            ],
            axis=-1,
        )
        return samples

    @classmethod
    def mean(cls, base_dists, reinterpreted_batch_ndims=1, **kwargs):
        """Mean of the independent distribution."""
        batch_shape, event_shape, split_dims, split_indices = determine_shapes(
            base_dists, reinterpreted_batch_ndims
        )
        if len(base_dists) == 1:
            return base_dists[0].mean(*kwargs)
        else:
            means = jnp.stack([d.mean(*kwargs) for d in base_dists], axis=-1)
            return means.reshape(batch_shape + event_shape)

    @classmethod
    def var(cls, base_dists, reinterpreted_batch_ndims=1, **kwargs):
        """Variance of the independent distribution."""
        batch_shape, event_shape, split_dims, split_indices = determine_shapes(
            base_dists, reinterpreted_batch_ndims
        )
        if len(base_dists) == 1:
            return base_dists[0].var(*kwargs)
        else:
            variances = jnp.stack([d.var(*kwargs) for d in base_dists], axis=-1)
            return variances.reshape(batch_shape + event_shape)

    @classmethod
    def entropy(cls, base_dists, reinterpreted_batch_ndims=1, **kwargs):
        """Entropy of the independent distribution."""
        batch_shape, event_shape, split_dims, split_indices = determine_shapes(
            base_dists, reinterpreted_batch_ndims
        )
        if len(base_dists) == 1:
            return base_dists[0].entropy(*kwargs)
        else:
            entropies = jnp.stack([d.entropy(*kwargs) for d in base_dists], axis=-1)
            return entropies.reshape(batch_shape + event_shape)

    @classmethod
    def mode(cls, base_dists, reinterpreted_batch_ndims=1, **kwargs):
        """Mode of the independent distribution."""
        batch_shape, event_shape, split_dims, split_indices = determine_shapes(
            base_dists, reinterpreted_batch_ndims
        )
        if len(base_dists) == 1:
            return base_dists[0].mode(*kwargs)
        else:
            modes = jnp.stack([d.mode(*kwargs) for d in base_dists], axis=-1)
            return modes.reshape(batch_shape + event_shape)

    @classmethod
    def fit(cls, data, base_dists, reinterpreted_batch_ndims=1, **kwargs):
        """Fit the independent distribution to data.

        Parameters
        ----------
        data : ArrayLike
            The data to fit the distribution to.
        base_dists : Sequence[rv_continuous_frozen]
            The base distributions to fit.
        reinterpreted_batch_ndims : int, optional
            The number of batch dimensions that should be considered as event
            dimensions.
            Default is 1.
        **kwargs
            Additional keyword arguments passed to each base distribution's fit method.

        Returns
        -------
        Sequence[rv_continuous_frozen]
            The fitted base distributions.
        """
        batch_shape, event_shape, split_dims, split_indices = determine_shapes(
            base_dists, reinterpreted_batch_ndims
        )

        # Split the data along the last dimension
        split_data = jnp.split(data, split_indices, axis=-1)

        # Fit each base distribution
        fitted_dists = [
            d.fit(dat, **kwargs) for d, dat in zip(base_dists, split_data, strict=False)
        ]

        return fitted_dists

freeze

freeze(base_dists, reinterpreted_batch_ndims=1, **kwargs)

Freeze the independent distribution with the given parameters.

Source code in probjax/stats/indep.py
def freeze(self, base_dists, reinterpreted_batch_ndims=1, **kwargs):
    """Freeze the independent distribution with the given parameters."""
    return rv_frozen_indep(
        self,
        base_dists=base_dists,
        reinterpreted_batch_ndims=reinterpreted_batch_ndims,
        **kwargs,
    )

support classmethod

support(base_dists, reinterpreted_batch_ndims=1, **kwargs)

Support of the independent distribution.

Source code in probjax/stats/indep.py
@classmethod
def support(cls, base_dists, reinterpreted_batch_ndims=1, **kwargs):
    """Support of the independent distribution."""
    if len(base_dists) == 1:
        return base_dists[0].support()
    else:
        return tuple(d.support() for d in base_dists)

pdf classmethod

pdf(x, base_dists, reinterpreted_batch_ndims=1, **kwargs)

Probability density function of the independent distribution.

Source code in probjax/stats/indep.py
@classmethod
def pdf(cls, x, base_dists, reinterpreted_batch_ndims=1, **kwargs):
    """Probability density function of the independent distribution."""
    return jnp.exp(cls.logpdf(x, base_dists, reinterpreted_batch_ndims, **kwargs))

logpdf classmethod

logpdf(x, base_dists, reinterpreted_batch_ndims=1, **kwargs)

Log of the probability density function of the independent distribution.

Source code in probjax/stats/indep.py
@classmethod
def logpdf(cls, x, base_dists, reinterpreted_batch_ndims=1, **kwargs):
    """Log of the probability density function of the independent distribution."""
    batch_shape, event_shape, split_dims, split_indices = determine_shapes(
        base_dists, reinterpreted_batch_ndims
    )

    # Split the input along the last dimension
    split_value = jnp.split(x, split_indices, axis=-1)

    # Compute logpdf for each base distribution
    logpdf = sum(d.logpdf(v) for d, v in zip(base_dists, split_value, strict=False))
    # Sum up to be of shape reinterpreted_batch_ndins
    for _ in range(reinterpreted_batch_ndims):
        logpdf = jnp.sum(logpdf, axis=-1)
    return logpdf

cdf classmethod

cdf(x, base_dists, reinterpreted_batch_ndims=1, **kwargs)

Cumulative distribution function of the independent distribution.

Source code in probjax/stats/indep.py
@classmethod
def cdf(cls, x, base_dists, reinterpreted_batch_ndims=1, **kwargs):
    """Cumulative distribution function of the independent distribution."""
    batch_shape, event_shape, split_dims, split_indices = determine_shapes(
        base_dists, reinterpreted_batch_ndims
    )

    # Split the input along the last dimension
    split_value = jnp.split(x, split_indices, axis=-1)

    # Compute CDF for each base distribution
    cdf = jnp.prod([
        d.cdf(v) for d, v in zip(base_dists, split_value, strict=False)
    ])

    # Product up to be of shape reinterpreted_batch_ndims
    for _ in range(reinterpreted_batch_ndims):
        cdf = jnp.prod(cdf, axis=-1)
    return cdf

mean classmethod

mean(base_dists, reinterpreted_batch_ndims=1, **kwargs)

Mean of the independent distribution.

Source code in probjax/stats/indep.py
@classmethod
def mean(cls, base_dists, reinterpreted_batch_ndims=1, **kwargs):
    """Mean of the independent distribution."""
    batch_shape, event_shape, split_dims, split_indices = determine_shapes(
        base_dists, reinterpreted_batch_ndims
    )
    if len(base_dists) == 1:
        return base_dists[0].mean(*kwargs)
    else:
        means = jnp.stack([d.mean(*kwargs) for d in base_dists], axis=-1)
        return means.reshape(batch_shape + event_shape)

var classmethod

var(base_dists, reinterpreted_batch_ndims=1, **kwargs)

Variance of the independent distribution.

Source code in probjax/stats/indep.py
@classmethod
def var(cls, base_dists, reinterpreted_batch_ndims=1, **kwargs):
    """Variance of the independent distribution."""
    batch_shape, event_shape, split_dims, split_indices = determine_shapes(
        base_dists, reinterpreted_batch_ndims
    )
    if len(base_dists) == 1:
        return base_dists[0].var(*kwargs)
    else:
        variances = jnp.stack([d.var(*kwargs) for d in base_dists], axis=-1)
        return variances.reshape(batch_shape + event_shape)

entropy classmethod

entropy(base_dists, reinterpreted_batch_ndims=1, **kwargs)

Entropy of the independent distribution.

Source code in probjax/stats/indep.py
@classmethod
def entropy(cls, base_dists, reinterpreted_batch_ndims=1, **kwargs):
    """Entropy of the independent distribution."""
    batch_shape, event_shape, split_dims, split_indices = determine_shapes(
        base_dists, reinterpreted_batch_ndims
    )
    if len(base_dists) == 1:
        return base_dists[0].entropy(*kwargs)
    else:
        entropies = jnp.stack([d.entropy(*kwargs) for d in base_dists], axis=-1)
        return entropies.reshape(batch_shape + event_shape)

mode classmethod

mode(base_dists, reinterpreted_batch_ndims=1, **kwargs)

Mode of the independent distribution.

Source code in probjax/stats/indep.py
@classmethod
def mode(cls, base_dists, reinterpreted_batch_ndims=1, **kwargs):
    """Mode of the independent distribution."""
    batch_shape, event_shape, split_dims, split_indices = determine_shapes(
        base_dists, reinterpreted_batch_ndims
    )
    if len(base_dists) == 1:
        return base_dists[0].mode(*kwargs)
    else:
        modes = jnp.stack([d.mode(*kwargs) for d in base_dists], axis=-1)
        return modes.reshape(batch_shape + event_shape)

fit classmethod

fit(data, base_dists, reinterpreted_batch_ndims=1, **kwargs)

Fit the independent distribution to data.

Parameters

data : ArrayLike The data to fit the distribution to. base_dists : Sequence[rv_continuous_frozen] The base distributions to fit. reinterpreted_batch_ndims : int, optional The number of batch dimensions that should be considered as event dimensions. Default is 1. **kwargs Additional keyword arguments passed to each base distribution's fit method.

Returns

Sequence[rv_continuous_frozen] The fitted base distributions.

Source code in probjax/stats/indep.py
@classmethod
def fit(cls, data, base_dists, reinterpreted_batch_ndims=1, **kwargs):
    """Fit the independent distribution to data.

    Parameters
    ----------
    data : ArrayLike
        The data to fit the distribution to.
    base_dists : Sequence[rv_continuous_frozen]
        The base distributions to fit.
    reinterpreted_batch_ndims : int, optional
        The number of batch dimensions that should be considered as event
        dimensions.
        Default is 1.
    **kwargs
        Additional keyword arguments passed to each base distribution's fit method.

    Returns
    -------
    Sequence[rv_continuous_frozen]
        The fitted base distributions.
    """
    batch_shape, event_shape, split_dims, split_indices = determine_shapes(
        base_dists, reinterpreted_batch_ndims
    )

    # Split the data along the last dimension
    split_data = jnp.split(data, split_indices, axis=-1)

    # Fit each base distribution
    fitted_dists = [
        d.fit(dat, **kwargs) for d, dat in zip(base_dists, split_data, strict=False)
    ]

    return fitted_dists

determine_shapes

determine_shapes(base_dist, reinterpreted_batch_ndims)

Helper function to determine shapes for Independent distribution.

Source code in probjax/stats/indep.py
def determine_shapes(
    base_dist: Union[rv_continuous_frozen, Sequence[rv_continuous_frozen]],
    reinterpreted_batch_ndims: int,
) -> Tuple[Tuple[int, ...], Tuple[int, ...], Tuple[int, ...], Tuple[int, ...]]:
    """Helper function to determine shapes for Independent distribution."""
    if isinstance(base_dist, rv_continuous_frozen):
        # Single distribution case
        base_dist = [base_dist]

    # Extract batch shapes and event shapes from the list of base distributions
    batch_shapes = [b.batch_shape for b in base_dist]
    event_shapes = [b.event_shape for b in base_dist]

    batch_ndims = [len(b) for b in batch_shapes]
    event_ndims = [len(e) for e in event_shapes]

    assert reinterpreted_batch_ndims >= 0, (
        "reinterpreted_batch_ndims must be non-negative."
    )
    assert all([b == batch_ndims[0] for b in batch_ndims]), (
        "Batch dimensions must be equal for all base distributions."
    )
    assert all([e == event_ndims[0] for e in event_ndims]), (
        "Event dimensions must be equal for all base distributions."
    )

    # Reinterpret batch dimensions as event dimensions where applicable.
    new_event_shapes = []
    new_batch_shapes = []

    for b_shape, e_shape in zip(batch_shapes, event_shapes, strict=False):
        if len(b_shape) > 0:
            # Reinterpret batch dimensions as event dimensions
            new_event_shape = b_shape + e_shape
            new_batch_shape = ()
        else:
            new_event_shape = e_shape
            new_batch_shape = b_shape

        new_event_shapes.append(new_event_shape)
        new_batch_shapes.append(new_batch_shape)

    # Concatenate event shapes for multiple distributions
    if len(new_event_shapes) > 1:
        # Sum the first dimension of each event shape
        first_dims = [e[0] if len(e) > 0 else 0 for e in new_event_shapes]
        first_dim_sum = sum(first_dims)

        # Take the rest of the dimensions from the first event shape
        other_dims = new_event_shapes[0][1:] if len(new_event_shapes[0]) > 0 else ()

        # Combine into final event shape
        event_shape = (first_dim_sum,) + other_dims
    else:
        event_shape = new_event_shapes[0]

    # Batch shape is empty since we've reinterpreted all batch dimensions
    batch_shape = ()

    # For splitting: use the first dimension of each event shape
    split_dims = [e[0] if len(e) > 0 else 1 for e in new_event_shapes]
    # Only need split points between distributions
    split_indices = [sum(split_dims[: i + 1]) for i in range(len(split_dims) - 1)]

    return (
        tuple(batch_shape),
        tuple(event_shape),
        tuple(split_dims),
        tuple(split_indices),
    )

Fitting

probjax.stats.fit

Gradient-based fitting (:mod:probjax.stats.fit)

A framework-agnostic training loop: :func:fit minimizes any loss_fn(params, rng, batch) over a params pytree with optax, where batch is either one batch -- a bare array, or a dict such as {"data": x, "context": c} whose leaves share the leading example axis -- or an iterable of batches, for data that does not fit in memory:

params, losses = fit(loss_fn, params, key, {"data": x}) # whole array params, losses = fit(loss_fn, params, key, my_dataloader) # streamed

Nothing here assumes a particular NN library. The loop is a single jax.lax.scan: it compiles once no matter how many steps are requested, and runs end to end without returning to Python -- a streamed batch arrives through an ordered io_callback, and on_step reports progress the same way.

Module-backed models (the families in :mod:probjax.nn.generative) get the convenient model.fit(rng, data) via :class:FitMixin, which lazily builds the pure loss_fn + params from the module once per instance:

flow = maf(2, 5, rngs=nnx.Rngs(0)) losses = flow.fit(jax.random.key(0), samples) flow.logpdf(samples) # trained in place

This is the object-layer counterpart of the scipy-style classmethod rv_generic.fit (closed-form / optimizer MLE for parametric families).

FitMixin

Adds model.fit(rng, data, ...) for modules with a loss method.

Source code in probjax/stats/fit.py
class FitMixin:
    """Adds ``model.fit(rng, data, ...)`` for modules with a ``loss`` method."""

    def _default_fit_kwargs(self) -> dict:
        """Model-family defaults for :func:`fit`, overridable per subclass.

        Anything the caller passes explicitly wins, so this only shifts the
        starting point for a family whose loss landscape is known to want
        something other than plain constant-rate Adam.
        """
        return {}

    def fit(
        self,
        rng: RngKey,
        data: ArrayLike,
        *,
        context: Optional[ArrayLike] = None,
        weights: Optional[ArrayLike] = None,
        **fit_kwargs,
    ) -> Array:
        """Train this model in place; returns per-step losses."""
        from flax import nnx

        fit_kwargs = {**self._default_fit_kwargs(), **fit_kwargs}
        loss_fn = _pure_loss_fn(self)
        params = nnx.state(self, nnx.Param)
        if is_batch_stream(data):
            if weights is not None:
                raise ValueError(
                    "weights cannot be passed alongside an iterable data source; "
                    "include them in each batch dict instead."
                )
            if context is not None:
                raise ValueError(
                    "context cannot be passed alongside an iterable data "
                    "source; yield (data, context) pairs or batch dicts from "
                    "the iterable instead."
                )
            batch = _BatchAdapter(data)
        else:
            batch = {"data": data}
            if context is not None:
                batch["context"] = context
            if weights is not None:
                batch["weights"] = weights
        params, losses = fit(loss_fn, params, rng, batch, **fit_kwargs)
        nnx.update(self, params)
        return losses

fit

fit(rng, data, *, context=None, weights=None, **fit_kwargs)

Train this model in place; returns per-step losses.

Source code in probjax/stats/fit.py
def fit(
    self,
    rng: RngKey,
    data: ArrayLike,
    *,
    context: Optional[ArrayLike] = None,
    weights: Optional[ArrayLike] = None,
    **fit_kwargs,
) -> Array:
    """Train this model in place; returns per-step losses."""
    from flax import nnx

    fit_kwargs = {**self._default_fit_kwargs(), **fit_kwargs}
    loss_fn = _pure_loss_fn(self)
    params = nnx.state(self, nnx.Param)
    if is_batch_stream(data):
        if weights is not None:
            raise ValueError(
                "weights cannot be passed alongside an iterable data source; "
                "include them in each batch dict instead."
            )
        if context is not None:
            raise ValueError(
                "context cannot be passed alongside an iterable data "
                "source; yield (data, context) pairs or batch dicts from "
                "the iterable instead."
            )
        batch = _BatchAdapter(data)
    else:
        batch = {"data": data}
        if context is not None:
            batch["context"] = context
        if weights is not None:
            batch["weights"] = weights
    params, losses = fit(loss_fn, params, rng, batch, **fit_kwargs)
    nnx.update(self, params)
    return losses

is_batch_stream

is_batch_stream(data)

Whether data is an iterable of batches rather than one batch pytree.

Both readings are pytrees, so nothing about the structure separates a list of batches from one batch made of several arrays. Rather than guess from shapes -- which fails silently and in whichever direction the guess went -- the rule is fixed and stated:

  • a bare array or a dict is one batch;
  • a list or tuple is a sequence of batches;
  • anything else with __iter__ or __next__ (a generator, a DataLoader) is a stream of batches.

So a single batch that groups several arrays must be a dict -- {"data": x, "context": c} -- not a tuple.

Source code in probjax/stats/fit.py
def is_batch_stream(data) -> bool:
    """Whether ``data`` is an iterable of batches rather than one batch pytree.

    Both readings are pytrees, so nothing about the *structure* separates a
    list of batches from one batch made of several arrays. Rather than guess
    from shapes -- which fails silently and in whichever direction the guess
    went -- the rule is fixed and stated:

    * a **bare array** or a **dict** is one batch;
    * a **list** or **tuple** is a sequence of batches;
    * anything else with ``__iter__`` or ``__next__`` (a generator, a
      ``DataLoader``) is a stream of batches.

    So a single batch that groups several arrays must be a dict --
    ``{"data": x, "context": c}`` -- not a tuple.
    """
    if hasattr(data, "shape") or isinstance(data, (dict, int, float, complex)):
        return False
    return hasattr(data, "__iter__") or hasattr(data, "__next__")

take_batches

take_batches(source, count)

The first count batches of source, restarting it if it is short.

Exposed for callers that must see some data before training starts -- fitting a standardising transform, say -- without giving up the ability to train on the same iterable afterwards.

Source code in probjax/stats/fit.py
def take_batches(source, count: int) -> list:
    """The first ``count`` batches of ``source``, restarting it if it is short.

    Exposed for callers that must see some data before training starts --
    fitting a standardising transform, say -- without giving up the ability to
    train on the same iterable afterwards.
    """
    return _BatchStream(source).take(count)

fit

fit(loss_fn, params, rng, batch, *, num_steps='auto', batch_size='auto', learning_rate=0.001, schedule='constant', clip_norm=10.0, optimizer=None, on_step=None, log_every=1)

Minimize loss_fn over params with minibatch gradient descent.

Parameters:

Name Type Description Default
loss_fn

loss_fn(params, rng, batch) -> scalar. Must be a stable function object across calls to benefit from the cached jitted step (avoid rebuilding it per call).

required
params

Pytree of trainable parameters.

required
rng RngKey

PRNG key consumed for minibatching and the per-step loss.

required
batch object

Either one batch -- a bare array, or a dict of arrays such as {"data": x, "context": c} whose leaves share the leading example axis -- or an iterable of batches: a list, a tuple, a generator, a DataLoader. Note that a list or tuple is always read as a sequence of batches, so a single batch grouping several arrays must be a dict. With an iterable, minibatching is the loader's job: every batch must have the same shapes and dtypes as the first (one compiled step serves them all), and a finite iterable is restarted as many times as num_steps requires.

required
num_steps int | Literal['auto']

Number of gradient steps, or "auto" (the default) to scale with the dataset: enough steps for a fixed number of passes over it, clamped to [1000, 20000]. For an iterable with a __len__, the dataset size is taken as len(batch) * batch_examples; without one there is nothing to scale from and "auto" means 1000.

'auto'
batch_size int | None | Literal['auto']

Minibatch size, or "auto" (the default) for min(num_examples, 512). None still means the full dataset every step, which was the previous default and stops being viable as the dataset grows. Must not be set for an iterable batch.

'auto'
learning_rate float

Adam learning rate, used when optimizer is None.

0.001
schedule Schedule

"constant" or "warmup_cosine" (5% warmup, cosine decay to learning_rate / 1000). Ignored when optimizer is given.

'constant'
clip_norm Optional[float]

Global gradient-norm clip; None disables. Ignored when optimizer is given.

10.0
optimizer

Optional optax.GradientTransformation. Supplying it takes full control, bypassing learning_rate, schedule and clip_norm.

None
on_step

Optional on_step(step, loss) -> bool | None called on the host every log_every steps. Returning False stops training early. Parameters are deliberately not passed: the callback runs inside the compiled loop, so handing it the tree would copy every parameter back to the host on each call.

None
log_every int

Cadence for on_step. Ignored when on_step is None.

1

Returns:

Type Description
object

(trained_params, losses). losses has shape (num_steps,),

Array

or is truncated at the stopping step if on_step asked to stop.

Warns:

Type Description
RuntimeWarning

if any step produced a non-finite loss. The parameters are returned as-is rather than repaired -- once a NaN gradient has been applied the run is dead, and silently continuing would hide it.

Note

The loop is a single jax.lax.scan, so it compiles once regardless of num_steps and runs without returning to Python. Two consequences: losses arrive only when the run finishes rather than step by step (use on_step to watch it live), and a diverged run still executes its remaining iterations.

Source code in probjax/stats/fit.py
def fit(
    loss_fn,
    params,
    rng: RngKey,
    batch: object,
    *,
    num_steps: "int | Literal['auto']" = "auto",
    batch_size: "int | None | Literal['auto']" = "auto",
    learning_rate: float = 1e-3,
    schedule: Schedule = "constant",
    clip_norm: Optional[float] = 10.0,
    optimizer=None,
    on_step=None,
    log_every: int = 1,
) -> Tuple[object, Array]:
    """Minimize ``loss_fn`` over ``params`` with minibatch gradient descent.

    Args:
        loss_fn: ``loss_fn(params, rng, batch) -> scalar``. Must be a stable
            function object across calls to benefit from the cached jitted
            step (avoid rebuilding it per call).
        params: Pytree of trainable parameters.
        rng: PRNG key consumed for minibatching and the per-step loss.
        batch: Either one batch -- a bare array, or a dict of arrays such as
            ``{"data": x, "context": c}`` whose leaves share the leading
            example axis -- or an **iterable of batches**: a list, a tuple, a
            generator, a ``DataLoader``. Note that a list or tuple is always
            read as a sequence of batches, so a single batch grouping several
            arrays must be a dict. With an iterable, minibatching is the
            loader's job: every batch must have the same shapes and dtypes as
            the first (one compiled step serves them all), and a finite
            iterable is restarted as many times as ``num_steps`` requires.
        num_steps: Number of gradient steps, or ``"auto"`` (the default) to
            scale with the dataset: enough steps for a fixed number of passes
            over it, clamped to [1000, 20000]. For an iterable with a
            ``__len__``, the dataset size is taken as
            ``len(batch) * batch_examples``; without one there is nothing to
            scale from and ``"auto"`` means 1000.
        batch_size: Minibatch size, or ``"auto"`` (the default) for
            ``min(num_examples, 512)``. ``None`` still means the full dataset
            every step, which was the previous default and stops being viable
            as the dataset grows. Must not be set for an iterable ``batch``.
        learning_rate: Adam learning rate, used when ``optimizer`` is None.
        schedule: ``"constant"`` or ``"warmup_cosine"`` (5% warmup, cosine decay
            to ``learning_rate / 1000``). Ignored when ``optimizer`` is given.
        clip_norm: Global gradient-norm clip; ``None`` disables. Ignored when
            ``optimizer`` is given.
        optimizer: Optional ``optax.GradientTransformation``. Supplying it takes
            full control, bypassing ``learning_rate``, ``schedule`` and
            ``clip_norm``.
        on_step: Optional ``on_step(step, loss) -> bool | None`` called on the
            host every ``log_every`` steps. Returning ``False`` stops training
            early. Parameters are deliberately not passed: the callback runs
            inside the compiled loop, so handing it the tree would copy every
            parameter back to the host on each call.
        log_every: Cadence for ``on_step``. Ignored when ``on_step`` is None.

    Returns:
        ``(trained_params, losses)``. ``losses`` has shape ``(num_steps,)``,
        or is truncated at the stopping step if ``on_step`` asked to stop.

    Warns:
        RuntimeWarning: if any step produced a non-finite loss. The parameters
            are returned as-is rather than repaired -- once a NaN gradient has
            been applied the run is dead, and silently continuing would hide it.

    Note:
        The loop is a single ``jax.lax.scan``, so it compiles once regardless of
        ``num_steps`` and runs without returning to Python. Two consequences:
        losses arrive only when the run finishes rather than step by step (use
        ``on_step`` to watch it live), and a diverged run still executes its
        remaining iterations.
    """
    if log_every < 1:
        raise ValueError(f"log_every must be at least 1; got {log_every}.")

    if is_batch_stream(batch):
        if isinstance(batch_size, int):
            raise ValueError(
                "batch_size cannot be set when batch is an iterable: the "
                "iterable decides its own batch size. Pass an array pytree "
                "instead, or drop batch_size."
            )
        stream = _BatchStream(batch, "batch")
        first = _as_device_batch(stream.peek())
        spec = _batch_spec(first)
        leaves = jax.tree.leaves(first)
        if not leaves:
            raise ValueError("batch must contain at least one array leaf.")
        # Only the *stream* length tells us the dataset size; a bare iterator
        # has no such information and "auto" falls back to the floor.
        per_batch = leaves[0].shape[0]
        source_len = _maybe_len(batch)
        num_steps = _resolve_num_steps(
            num_steps,
            per_batch * source_len if source_len else per_batch,
            per_batch,
        )
        fetch = _stream_fetch(stream, spec)
    else:
        batch = _as_device_batch(batch)
        leaves = jax.tree.leaves(batch)
        if not leaves:
            raise ValueError("batch must contain at least one array leaf.")
        num_examples = leaves[0].shape[0]
        batch_size = _resolve_batch_size(batch_size, num_examples)
        num_steps = _resolve_num_steps(num_steps, num_examples, batch_size)
        fetch = _array_fetch(batch, batch_size, num_examples)

    if optimizer is not None:
        tx = optimizer
    else:
        tx = _build_optimizer(learning_rate, num_steps, schedule, clip_norm)
    opt_state = tx.init(params)

    body = _make_step(loss_fn, tx, fetch, on_step, log_every)
    init = (params, opt_state, rng, jnp.asarray(False), jnp.asarray(0, jnp.int32))
    try:
        # Not wrapped in jit: the scan is one XLA computation either way, and
        # jitting here would key the cache on a closure rebuilt every call.
        (params, _, _, stopped, _), losses = jax.lax.scan(
            body, init, None, length=num_steps
        )
    except Exception:
        # A bad batch fails inside the callback, where JAX wraps it in a
        # JaxRuntimeError over a traceback through the whole scan machinery.
        # The stream's own error is the one the user can act on.
        error = getattr(fetch, "stream_state", {}).get("error")
        if error is not None:
            raise error from None
        raise

    if on_step is not None and bool(stopped):
        # Steps after the stop ran as no-ops and reported NaN; drop them rather
        # than hand back losses that look like divergence.
        ran = int(jnp.sum(jnp.asarray(~jnp.isnan(losses), jnp.int32)))
        losses = losses[:ran]

    finite = jnp.isfinite(losses)
    if not bool(jnp.all(finite)):
        first = int(jnp.argmin(finite))
        warnings.warn(
            f"Training loss became non-finite at step {first} of "
            f"{losses.shape[0]}; the returned parameters are unusable. Lower "
            "the learning rate, tighten clip_norm, or check the model for an "
            "unbounded transform.",
            RuntimeWarning,
            stacklevel=2,
        )
    return params, losses

probjax.stats.FitMixin

Adds model.fit(rng, data, ...) for modules with a loss method.

Source code in probjax/stats/fit.py
class FitMixin:
    """Adds ``model.fit(rng, data, ...)`` for modules with a ``loss`` method."""

    def _default_fit_kwargs(self) -> dict:
        """Model-family defaults for :func:`fit`, overridable per subclass.

        Anything the caller passes explicitly wins, so this only shifts the
        starting point for a family whose loss landscape is known to want
        something other than plain constant-rate Adam.
        """
        return {}

    def fit(
        self,
        rng: RngKey,
        data: ArrayLike,
        *,
        context: Optional[ArrayLike] = None,
        weights: Optional[ArrayLike] = None,
        **fit_kwargs,
    ) -> Array:
        """Train this model in place; returns per-step losses."""
        from flax import nnx

        fit_kwargs = {**self._default_fit_kwargs(), **fit_kwargs}
        loss_fn = _pure_loss_fn(self)
        params = nnx.state(self, nnx.Param)
        if is_batch_stream(data):
            if weights is not None:
                raise ValueError(
                    "weights cannot be passed alongside an iterable data source; "
                    "include them in each batch dict instead."
                )
            if context is not None:
                raise ValueError(
                    "context cannot be passed alongside an iterable data "
                    "source; yield (data, context) pairs or batch dicts from "
                    "the iterable instead."
                )
            batch = _BatchAdapter(data)
        else:
            batch = {"data": data}
            if context is not None:
                batch["context"] = context
            if weights is not None:
                batch["weights"] = weights
        params, losses = fit(loss_fn, params, rng, batch, **fit_kwargs)
        nnx.update(self, params)
        return losses

fit

fit(rng, data, *, context=None, weights=None, **fit_kwargs)

Train this model in place; returns per-step losses.

Source code in probjax/stats/fit.py
def fit(
    self,
    rng: RngKey,
    data: ArrayLike,
    *,
    context: Optional[ArrayLike] = None,
    weights: Optional[ArrayLike] = None,
    **fit_kwargs,
) -> Array:
    """Train this model in place; returns per-step losses."""
    from flax import nnx

    fit_kwargs = {**self._default_fit_kwargs(), **fit_kwargs}
    loss_fn = _pure_loss_fn(self)
    params = nnx.state(self, nnx.Param)
    if is_batch_stream(data):
        if weights is not None:
            raise ValueError(
                "weights cannot be passed alongside an iterable data source; "
                "include them in each batch dict instead."
            )
        if context is not None:
            raise ValueError(
                "context cannot be passed alongside an iterable data "
                "source; yield (data, context) pairs or batch dicts from "
                "the iterable instead."
            )
        batch = _BatchAdapter(data)
    else:
        batch = {"data": data}
        if context is not None:
            batch["context"] = context
        if weights is not None:
            batch["weights"] = weights
    params, losses = fit(loss_fn, params, rng, batch, **fit_kwargs)
    nnx.update(self, params)
    return losses

probjax.stats.is_batch_stream

is_batch_stream(data)

Whether data is an iterable of batches rather than one batch pytree.

Both readings are pytrees, so nothing about the structure separates a list of batches from one batch made of several arrays. Rather than guess from shapes -- which fails silently and in whichever direction the guess went -- the rule is fixed and stated:

  • a bare array or a dict is one batch;
  • a list or tuple is a sequence of batches;
  • anything else with __iter__ or __next__ (a generator, a DataLoader) is a stream of batches.

So a single batch that groups several arrays must be a dict -- {"data": x, "context": c} -- not a tuple.

Source code in probjax/stats/fit.py
def is_batch_stream(data) -> bool:
    """Whether ``data`` is an iterable of batches rather than one batch pytree.

    Both readings are pytrees, so nothing about the *structure* separates a
    list of batches from one batch made of several arrays. Rather than guess
    from shapes -- which fails silently and in whichever direction the guess
    went -- the rule is fixed and stated:

    * a **bare array** or a **dict** is one batch;
    * a **list** or **tuple** is a sequence of batches;
    * anything else with ``__iter__`` or ``__next__`` (a generator, a
      ``DataLoader``) is a stream of batches.

    So a single batch that groups several arrays must be a dict --
    ``{"data": x, "context": c}`` -- not a tuple.
    """
    if hasattr(data, "shape") or isinstance(data, (dict, int, float, complex)):
        return False
    return hasattr(data, "__iter__") or hasattr(data, "__next__")

probjax.stats.take_batches

take_batches(source, count)

The first count batches of source, restarting it if it is short.

Exposed for callers that must see some data before training starts -- fitting a standardising transform, say -- without giving up the ability to train on the same iterable afterwards.

Source code in probjax/stats/fit.py
def take_batches(source, count: int) -> list:
    """The first ``count`` batches of ``source``, restarting it if it is short.

    Exposed for callers that must see some data before training starts --
    fitting a standardising transform, say -- without giving up the ability to
    train on the same iterable afterwards.
    """
    return _BatchStream(source).take(count)

Transform protocols

Used to build higher-order distributions and normalizing flows.

probjax.stats.TransformedDistribution

Bases: DistributionAPI

Push a base :class:DistributionAPI through a transform.

Sampling applies the forward transform to base samples; logpdf uses the change-of-variables formula with the transform's inverse (explicit or auto-derived).

Parameters:

Name Type Description Default
base DistributionAPI

Base distribution (anything satisfying DistributionAPI — scipy-style frozen distributions, learned-model distribution views, or another TransformedDistribution).

required
transform

Forward callable, optionally satisfying :class:InvertibleTransformProtocol.

required
event_shape Optional[Tuple[int, ...]]

Override when the transform changes the event shape; defaults to the base distribution's event shape.

None
Source code in probjax/stats/bijective/protocols.py
class TransformedDistribution(DistributionAPI):
    """Push a base :class:`DistributionAPI` through a transform.

    Sampling applies the forward transform to base samples; ``logpdf`` uses
    the change-of-variables formula with the transform's inverse (explicit
    or auto-derived).

    Args:
        base: Base distribution (anything satisfying ``DistributionAPI`` —
            scipy-style frozen distributions, learned-model distribution
            views, or another ``TransformedDistribution``).
        transform: Forward callable, optionally satisfying
            :class:`InvertibleTransformProtocol`.
        event_shape: Override when the transform changes the event shape;
            defaults to the base distribution's event shape.
    """

    def __init__(
        self,
        base: DistributionAPI,
        transform,
        *,
        event_shape: Optional[Tuple[int, ...]] = None,
    ):
        self.base = base
        self.transform = transform
        self._invertible = ensure_invertible(transform)
        self._event_shape = (
            tuple(event_shape) if event_shape is not None else tuple(base.event_shape)
        )

    @property
    def batch_shape(self) -> Tuple[int, ...]:
        return tuple(self.base.batch_shape)

    @property
    def event_shape(self) -> Tuple[int, ...]:
        return self._event_shape

    def _flatten(self, x):
        """Flatten leading dims to a single vmap axis, keeping event dims."""
        x = jnp.asarray(x)
        event_ndim = len(self._event_shape)
        if event_ndim:
            if tuple(x.shape[-event_ndim:]) != self._event_shape:
                raise ValueError(
                    "Trailing dimensions of the input must match the event shape."
                )
            leading_shape = tuple(x.shape[:-event_ndim])
        else:
            leading_shape = tuple(x.shape)
        return jnp.reshape(x, (-1,) + self._event_shape), leading_shape

    def rvs(
        self,
        rng: RngKey,
        shape: Tuple[int, ...] = (),
        name: Optional[str] = None,
        **kwargs,
    ) -> Array:
        samples = self.base.rvs(rng, shape=shape, **kwargs)
        base_event_ndim = len(tuple(self.base.event_shape))
        flat = jnp.reshape(
            samples, (-1,) + tuple(samples.shape[samples.ndim - base_event_ndim :])
        )
        leading_shape = tuple(samples.shape[: samples.ndim - base_event_ndim])
        transformed_flat = batch_shard(jax.vmap(self.transform))(flat)
        return jnp.reshape(transformed_flat, leading_shape + self._event_shape)

    def logpdf(self, x: ArrayLike) -> Array:
        x_flat, leading_shape = self._flatten(x)
        inv_flat, logdet_flat = batch_shard(
            jax.vmap(self._invertible.inverse_and_logdet)
        )(x_flat)
        inv = jnp.reshape(inv_flat, leading_shape + tuple(self.base.event_shape))
        logdet = jnp.reshape(logdet_flat, leading_shape)
        return self.base.logpdf(inv) + logdet

probjax.stats.forward_and_logdet

forward_and_logdet(transform, x)

Compute y = T(x) and log |det dT/dx| at x.

Uses transform.forward_and_logdet if the transform provides it; otherwise evaluates the (possibly auto-derived) inverse log-determinant at y and negates it.

Source code in probjax/stats/bijective/protocols.py
def forward_and_logdet(transform, x: ArrayLike) -> Tuple[Array, Array]:
    """Compute ``y = T(x)`` and ``log |det dT/dx|`` at ``x``.

    Uses ``transform.forward_and_logdet`` if the transform provides it;
    otherwise evaluates the (possibly auto-derived) inverse log-determinant
    at ``y`` and negates it.
    """
    explicit = getattr(transform, "forward_and_logdet", None)
    if explicit is not None:
        return explicit(x)
    y = transform(x)
    _, logdet_inv = ensure_invertible(transform).inverse_and_logdet(y)
    return y, -logdet_inv

probjax.stats.ensure_invertible

ensure_invertible(transform)

Return transform if it satisfies :class:InvertibleTransformProtocol, otherwise wrap the forward callable with an auto-derived inverse.

Source code in probjax/stats/bijective/protocols.py
def ensure_invertible(transform) -> InvertibleTransformProtocol:
    """Return ``transform`` if it satisfies :class:`InvertibleTransformProtocol`,
    otherwise wrap the forward callable with an auto-derived inverse."""
    if isinstance(transform, InvertibleTransformProtocol):
        return transform
    return _AutoInvertedTransform(transform)