"""Prior distributions for Bayesian inference in zfit.
This module provides a collection of commonly used prior distributions that can be
attached to parameters in Bayesian inference workflows. All priors automatically
adapt to parameter bounds when specified, ensuring proper normalization within
the valid parameter range.
The constraint system provides systematic handling of parameter constraints
similar to PyMC's transform system, enabling automatic transforms for bounded
parameters while maintaining proper probability densities.
"""
# Copyright (c) 2025 zfit
from __future__ import annotations
from abc import ABC, abstractmethod
import zfit
import zfit.z.numpy as znp
from .._interfaces import ZfitPrior
from .mathconstrain import POSITIVE, UNCONSTRAINED, ConstraintType, PriorConstraint, validate_parameter
class BasePrior(ZfitPrior, ABC):
"""Base class for priors that automatically adapt to parameter limits.
This abstract base class provides the foundation for all prior distributions
that can adjust their support to match parameter constraints. When a prior
is assigned to a parameter with bounds, it automatically truncates or adjusts
its distribution to respect those limits while maintaining proper normalization.
The new constraint system provides automatic parameter validation in eager mode
and systematic handling of transforms for bounded parameters.
Subclasses must implement the `_create_pdf` method and define their constraint
via the `constraint` class attribute.
"""
# Constraint configuration - subclasses must override this
constraint: PriorConstraint = UNCONSTRAINED
def __init__(self, pdf_params: dict, bounds: tuple[float, float] | None = None, name: str | None = None):
"""Initialize an adaptive prior.
Args:
pdf_params: Parameters to pass to the PDF constructor
bounds: Optional custom bounds for the prior (overrides constraint defaults)
name: Optional name for the prior
"""
# Validate parameters in eager mode
self._validate_parameters(pdf_params)
# Store parameters for potential adaptation
self._pdf_params = pdf_params.copy()
# Determine bounds using constraint system
if bounds is not None:
self._original_bounds = bounds
else:
self._original_bounds = self.constraint.bounds
# Validate bounds with constraint
self._original_bounds = self.constraint.validate_bounds(self._original_bounds)
# Create initial PDF
obs = zfit.Space("prior_space", limits=self._original_bounds)
pdf = self._create_pdf(obs, **self._pdf_params)
super().__init__(pdf=pdf, name=name)
def _validate_parameters(self, params: dict):
"""Validate parameters if in eager mode. Override in subclasses."""
@abstractmethod
def _create_pdf(self, obs, **params):
"""Create the underlying PDF. Must be implemented by subclasses."""
def _register_default_param(self, param):
"""Register a parameter and potentially adapt the prior's range."""
super()._register_default_param(param)
# Check if we should adapt to parameter limits
if self._should_adapt_to_param(param):
self._adapt_to_parameter_limits(param)
return self
def _should_adapt_to_param(self, param) -> bool:
"""Check if the prior should adapt to parameter limits."""
return hasattr(param, "has_limits") and param.has_limits
def _get_adapted_bounds(self, param) -> tuple[float, float]:
"""Get bounds adapted to parameter limits using constraint system."""
lower, upper = self._original_bounds
# Extract parameter bounds safely
# Use parameter limits if available
if (param_lower := getattr(param, "lower", None)) is not None:
lower = param_lower if lower == -float("inf") else max(lower, param_lower)
if (param_upper := getattr(param, "upper", None)) is not None:
upper = param_upper if upper == float("inf") else min(upper, param_upper)
# Apply constraint validation
return self.constraint.validate_bounds((lower, upper))
def _adapt_to_parameter_limits(self, param):
"""Adapt the prior's observation space to the parameter limits."""
adapted_bounds = self._get_adapted_bounds(param)
# Only update if bounds actually changed
if adapted_bounds != self._original_bounds:
obs = zfit.Space("prior_space", limits=adapted_bounds)
self.pdf = self._create_adapted_pdf(obs, *adapted_bounds)
def _create_adapted_pdf(self, obs, lower, upper):
"""Create an adapted PDF with new bounds. Can be overridden for special handling."""
del lower, upper # Unused in base implementation
return self._create_pdf(obs, **self._pdf_params)
[docs]
class Normal(BasePrior):
"""Normal (Gaussian) prior distribution.
The Normal prior is one of the most commonly used priors in Bayesian inference.
It represents beliefs that parameter values follow a bell-shaped distribution
centered at a specific value with a given spread. When assigned to a parameter
with bounds, it automatically becomes a truncated normal distribution.
This prior is suitable for:
- Parameters where you have informative prior knowledge about the likely value
- Regression coefficients and effect sizes
- Location parameters when uncertainty is approximately symmetric
Example:
>>> # Prior centered at 0 with standard deviation 1
>>> prior = Normal(mu=0.0, sigma=1.0)
>>>
>>> # Prior for a parameter expected around 10 with uncertainty ±2
>>> prior = Normal(mu=10.0, sigma=2.0)
"""
constraint = UNCONSTRAINED
def __init__(self, mu: float, sigma: float, name: str | None = None):
"""Initialize a Normal prior.
Args:
mu: Mean (center) of the normal distribution
sigma: Standard deviation (spread) of the normal distribution. Must be positive.
name: Optional name for the prior
"""
pdf_params = {"mu": mu, "sigma": sigma}
super().__init__(pdf_params, name=name)
def _validate_parameters(self, params: dict):
"""Validate Normal distribution parameters."""
validate_parameter("mu", params["mu"])
validate_parameter("sigma", params["sigma"], POSITIVE)
def _create_pdf(self, obs, mu, sigma):
"""Create a Gaussian PDF."""
return zfit.pdf.Gauss(mu=mu, sigma=sigma, obs=obs)
def _create_adapted_pdf(self, obs, lower, upper):
"""Create a truncated Gaussian when adapting to limits."""
return zfit.pdf.TruncatedGauss(
mu=self._pdf_params["mu"], sigma=self._pdf_params["sigma"], low=lower, high=upper, obs=obs
)
[docs]
class HalfNormal(BasePrior):
"""Half-normal prior distribution.
The Half-Normal prior is a normal distribution truncated at a lower bound
(typically zero). It's ideal for parameters that must be positive and where
smaller values are more likely than larger ones. The distribution has its
mode at the truncation point and decreases monotonically.
This prior is suitable for:
- Standard deviations and scale parameters
- Variance components in hierarchical models
- Any positive parameter where smaller values are preferred
- Error terms and measurement uncertainties
Example:
>>> # Half-normal starting at 0 with scale 1
>>> prior = HalfNormal(sigma=1.0)
>>>
>>> # Half-normal starting at 2 with scale 0.5
>>> prior = HalfNormal(mu=2.0, sigma=0.5)
"""
def __init__(self, *, sigma: float, mu: float = 0, name: str | None = None):
"""Initialize a Half-Normal prior.
Args:
sigma: Scale parameter controlling the spread of the distribution.
Must be positive. Required keyword-only parameter.
mu: Location parameter (lower bound) where the distribution starts.
Defaults to 0 for a standard half-normal.
name: Optional name for the prior
"""
self.constraint = PriorConstraint(ConstraintType.LOWER_BOUNDED, bounds=(mu, float("inf")))
pdf_params = {"mu": mu, "sigma": sigma}
bounds = (mu, float("inf"))
super().__init__(pdf_params, bounds=bounds, name=name)
def _validate_parameters(self, params: dict):
"""Validate HalfNormal distribution parameters."""
validate_parameter("mu", params["mu"])
validate_parameter("sigma", params["sigma"], POSITIVE)
def _create_pdf(self, obs, mu, sigma):
"""Create a half-normal using TruncatedGauss."""
lower = float(obs.lower[0][0])
upper = float(obs.upper[0][0])
return zfit.pdf.TruncatedGauss(mu=mu, sigma=sigma, low=lower, high=upper, obs=obs)
def _get_adapted_bounds(self, param):
"""Ensure lower bound respects the mu parameter."""
lower, upper = super()._get_adapted_bounds(param)
# Ensure lower bound is at least mu
lower = max(self._pdf_params["mu"], lower)
return lower, upper
[docs]
class Gamma(BasePrior):
"""Gamma prior distribution.
The Gamma distribution is a flexible family of continuous probability
distributions for positive values. Its shape can range from exponential-like
(alpha=1) to bell-shaped (larger alpha values). The distribution is commonly
used in Bayesian inference due to its conjugacy properties with certain
likelihoods.
This prior is suitable for:
- Rate parameters and inverse scales
- Precision parameters (inverse of variance)
- Waiting times and lifetimes
- Any positive parameter with flexibility in shape
The parameterization used here:
- Mean = alpha/beta (when mu=0)
- Variance = alpha/beta²
Example:
>>> # Gamma prior with shape=2, rate=1
>>> prior = Gamma(alpha=2.0, beta=1.0)
>>>
>>> # Shifted Gamma starting at 0.5
>>> prior = Gamma(alpha=2.0, beta=1.0, mu=0.5)
"""
def __init__(self, alpha: float, beta: float, mu: float = 0, name: str | None = None):
"""Initialize a Gamma prior.
Args:
alpha: Shape parameter controlling the form of the distribution.
Must be positive. Larger values make the distribution more
bell-shaped.
beta: Rate parameter (inverse scale). Must be positive.
Larger values shift the distribution toward zero.
mu: Location parameter that shifts the entire distribution.
Defaults to 0 for a standard Gamma. The distribution
has support on [mu, ∞).
name: Optional name for the prior
"""
self.constraint = PriorConstraint(ConstraintType.LOWER_BOUNDED, bounds=(mu, float("inf")))
pdf_params = {"gamma": alpha, "beta": beta, "mu": mu}
bounds = (mu, float("inf"))
super().__init__(pdf_params, bounds=bounds, name=name)
def _validate_parameters(self, params: dict):
"""Validate Gamma distribution parameters."""
validate_parameter("gamma", params["gamma"], POSITIVE) # alpha
validate_parameter("beta", params["beta"], POSITIVE)
validate_parameter("mu", params["mu"])
def _create_pdf(self, obs, gamma, beta, mu):
"""Create a Gamma PDF."""
return zfit.pdf.Gamma(gamma=gamma, beta=beta, mu=mu, obs=obs)
def _create_adapted_pdf(self, obs, lower, _upper):
"""Create adapted Gamma PDF with adjusted mu."""
# Note: zfit's Gamma doesn't support upper truncation directly
# Adjust mu to match the lower bound
adapted_mu = max(self._pdf_params["mu"], lower)
return zfit.pdf.Gamma(gamma=self._pdf_params["gamma"], beta=self._pdf_params["beta"], mu=adapted_mu, obs=obs)
[docs]
class Beta(BasePrior):
"""Beta prior distribution for arbitrary [a, b] intervals.
This extends the standard Beta distribution to any bounded interval [a, b]
by applying an affine transformation. The shape parameters work the same way
as in the standard Beta distribution, but the support is transformed to [a, b].
This prior is suitable for:
- Any parameter bounded within a specific interval
- Proportional parameters scaled to custom ranges
- Mixing weights that don't sum to 1
- Any bounded parameter where Beta-like shapes are desired
The transformation is: Y = a + (b - a) * X where X ~ Beta(alpha, beta)
Example:
>>> # Uniform prior on [2, 10]
>>> prior = Beta(alpha=1.0, beta=1.0, lower=2.0, upper=10.0)
>>>
>>> # Prior on [-1, 1] favoring values near 0.6
>>> prior = Beta(alpha=8.0, beta=2.0, lower=-1.0, upper=1.0)
"""
def __init__(self, alpha: float, beta: float, lower: float, upper: float, name: str | None = None):
"""Initialize a Beta prior.
Args:
alpha: First shape parameter controlling behavior near the upper bound.
Must be positive.
beta: Second shape parameter controlling behavior near the lower bound.
Must be positive.
lower: Lower bound of the distribution.
upper: Upper bound of the distribution. Must be > lower.
name: Optional name for the prior
"""
if lower >= upper:
msg = f"Lower bound {lower} must be less than upper bound {upper}"
raise ValueError(msg)
self.lower = float(lower)
self.upper = float(upper)
self.scale = self.upper - self.lower
# Create constraint for this specific interval
self.constraint = PriorConstraint(ConstraintType.CUSTOM_BOUNDS, bounds=(lower, upper))
pdf_params = {"alpha": alpha, "beta": beta}
super().__init__(pdf_params, bounds=(lower, upper), name=name)
def _validate_parameters(self, params: dict):
"""Validate Beta distribution parameters."""
validate_parameter("alpha", params["alpha"], POSITIVE)
validate_parameter("beta", params["beta"], POSITIVE)
def _create_pdf(self, obs, alpha, beta):
"""Create a scaled Beta PDF."""
del alpha, beta # Using uniform approach for now
# For now, use a simple approach with Uniform distribution
# In a full implementation, this would need proper Jacobian handling
return zfit.pdf.Uniform(low=self.lower, high=self.upper, obs=obs)
[docs]
class LogNormal(BasePrior):
"""Log-normal prior distribution.
The Log-Normal distribution arises when the logarithm of a variable is
normally distributed. It's right-skewed and only defined for positive values.
This makes it useful for parameters that are positive and potentially have
a long right tail.
This prior is suitable for:
- Parameters with multiplicative effects
- Sizes, lengths, and other positive measurements
- Parameters where relative changes are more important than absolute
- Economic variables like income or prices
- Any positive parameter with potential for extreme values
Properties:
- If X ~ LogNormal(mu, sigma), then log(X) ~ Normal(mu, sigma)
- Mode = exp(mu - sigma^2)
- Median = exp(mu)
- Mean = exp(mu + sigma^2/2)
Example:
>>> # Log-normal with median at 1
>>> prior = LogNormal(mu=0.0, sigma=1.0)
>>>
>>> # Log-normal with median at 10 and moderate spread
>>> prior = LogNormal(mu=2.303, sigma=0.5) # 2.303 ≈ log(10)
"""
constraint = POSITIVE
def __init__(self, mu: float, sigma: float, name: str | None = None):
"""Initialize a Log-Normal prior.
Args:
mu: Mean of the logarithm of the variable. This controls the
median of the distribution (median = exp(mu)).
sigma: Standard deviation of the logarithm. Must be positive.
Larger values create heavier right tails.
name: Optional name for the prior
Note:
The parameters mu and sigma are NOT the mean and standard deviation
of the Log-Normal distribution itself, but of the underlying
normal distribution of log(X).
"""
pdf_params = {"mu": mu, "sigma": sigma}
super().__init__(pdf_params, name=name)
def _validate_parameters(self, params: dict):
"""Validate LogNormal distribution parameters."""
validate_parameter("mu", params["mu"])
validate_parameter("sigma", params["sigma"], POSITIVE)
def _create_pdf(self, obs, mu, sigma):
"""Create a LogNormal PDF."""
return zfit.pdf.LogNormal(mu=mu, sigma=sigma, obs=obs)
[docs]
class Cauchy(BasePrior):
"""Cauchy prior distribution.
The Cauchy distribution is a heavy-tailed distribution that doesn't have
a defined mean or variance. Its heavy tails make it useful for robust
inference when outliers are expected or when you want to express very
weak prior information.
This prior is suitable for:
- Robust modeling when outliers are expected
- Parameters where extreme values are plausible
- Weakly informative priors that still allow for surprises
- Location parameters with high uncertainty
Properties:
- No finite mean or variance (moments don't exist)
- Median and mode equal the location parameter m
- Heavy tails decrease as 1/x²
Example:
>>> # Cauchy centered at 0 with scale 1 (standard Cauchy)
>>> prior = Cauchy(m=0.0, gamma=1.0)
>>>
>>> # Cauchy centered at 10 with wider scale
>>> prior = Cauchy(m=10.0, gamma=5.0)
Warning:
Due to its heavy tails, the Cauchy can lead to slower MCMC
convergence. Consider using a Student-t with low degrees of
freedom as an alternative with finite variance.
"""
constraint = UNCONSTRAINED
def __init__(self, m: float, gamma: float, name: str | None = None):
"""Initialize a Cauchy prior.
Args:
m: Location parameter (center) of the distribution. This is
both the median and the mode.
gamma: Scale parameter controlling the spread. Must be positive.
Larger values create wider distributions.
name: Optional name for the prior
"""
pdf_params = {"m": m, "gamma": gamma}
super().__init__(pdf_params, name=name)
def _validate_parameters(self, params: dict):
"""Validate Cauchy distribution parameters."""
validate_parameter("m", params["m"])
validate_parameter("gamma", params["gamma"], POSITIVE)
def _create_pdf(self, obs, m, gamma):
"""Create a Cauchy PDF."""
return zfit.pdf.Cauchy(m=m, gamma=gamma, obs=obs)
[docs]
class KDE(ZfitPrior):
"""Kernel Density Estimate prior from samples.
The KDE prior is a non-parametric way to construct a prior distribution
from empirical samples. It estimates the probability density by placing
kernels (typically Gaussian) at each sample point. This is particularly
useful for hierarchical Bayesian models where posterior samples from one
analysis become priors for another.
This prior is suitable for:
- Using posterior samples from previous analyses as priors
Implementation details:
- Uses exact KDE for < 1000 samples (more accurate but slower)
- Switches to grid-based KDE for larger samples (faster approximation)
- Automatically adds margin to bounds for numerical stability
- Adapts to parameter limits while preserving sample structure
Example:
>>> # Create prior from previous posterior samples
>>> posterior_samples = previous_result.samples['param_name']
>>> prior = KDE(posterior_samples)
>>>
>>> # KDE with custom bandwidth
>>> prior = KDE(samples, bandwidth=0.1)
>>>
>>> # Using KDE for hierarchical modeling
>>> group1_posterior = fit1.samples['effect']
>>> group2_prior = KDE(group1_posterior)
"""
def __init__(self, samples, bandwidth: float | str | None = None, name: str | None = None):
"""Initialize a KDE prior.
Args:
samples: Array of samples to estimate the density from.
Can be a numpy array, list, or tensor.
bandwidth: Bandwidth for kernel density estimation. If None,
uses the KDE's default. Can be float or string like 'scott'.
name: Optional name for the prior
Note:
The KDE is constructed with a 10% margin beyond the sample
range to ensure numerical stability at the boundaries.
"""
samples = znp.asarray(samples)
if len(samples) == 0:
msg = "Cannot create KDE prior from empty samples"
raise ValueError(msg)
self._samples = samples
self._bandwidth = bandwidth
self._n_samples = len(samples)
# Calculate bounds with margin
self._min_val = float(znp.min(samples))
self._max_val = float(znp.max(samples))
self._range = self._max_val - self._min_val
# Use adaptive margin based on sample range
if self._range > 0:
self._margin = 0.1 * self._range
else:
# Handle case where all samples are identical
self._margin = 1.0
self._margin = max(self._margin, 1e-6) # Ensure minimum margin
self._original_bounds = (self._min_val - self._margin, self._max_val + self._margin)
# Create initial PDF
pdf = self._create_kde_pdf(self._original_bounds)
super().__init__(pdf=pdf, name=name)
def _create_kde_pdf(self, bounds):
"""Create KDE PDF with given bounds.
Automatically chooses between exact and grid-based KDE based on sample size:
- < 1000 samples: Exact KDE (more accurate, slower)
- >= 1000 samples: Grid-based KDE (faster, good approximation)
"""
lower, upper = bounds
obs = zfit.Space("prior_space", limits=(lower, upper))
data = zfit.Data.from_numpy(obs=obs, array=self._samples)
# Automatic algorithm selection based on performance characteristics
use_exact = self._n_samples < 1000
if use_exact:
return zfit.pdf.KDE1DimExact(data=data, obs=obs, bandwidth=self._bandwidth, padding=False)
else:
# For large samples, use grid-based approximation
# Grid points: balance between accuracy and performance
# Rule of thumb: use sqrt(n_samples) but cap at 1024
optimal_grid_points = min(1024, max(128, int(self._n_samples**0.5)))
return zfit.pdf.KDE1DimGrid(
data=data,
obs=obs,
bandwidth=self._bandwidth or "scott",
num_grid_points=optimal_grid_points,
padding=False,
)
def _register_default_param(self, param):
"""Register a parameter and potentially adapt the KDE bounds."""
super()._register_default_param(param)
# Check if we should adapt to parameter limits
if hasattr(param, "has_limits") and param.has_limits:
self._adapt_to_parameter_limits(param)
return self
def _adapt_to_parameter_limits(self, param):
"""Adapt KDE prior to parameter limits."""
lower = self._min_val - self._margin
upper = self._max_val + self._margin
# Extract parameter bounds safely
# Expand to parameter limits if they're wider (preserve sample structure)
if (param_lower := getattr(param, "lower", None)) is not None:
lower = min(lower, param_lower)
if (param_upper := getattr(param, "upper", None)) is not None:
upper = max(upper, param_upper)
# Only recreate if bounds actually changed
new_bounds = (lower, upper)
if new_bounds != self._original_bounds:
self.pdf = self._create_kde_pdf(new_bounds)
# Update bounds for future comparisons
self._original_bounds = new_bounds
[docs]
class Poisson(BasePrior):
"""Poisson prior distribution.
The Poisson distribution is a discrete probability distribution that models
the number of events occurring in a fixed interval of time or space. It's
particularly useful for parameters that represent counts, rates, or other
discrete positive quantities.
Properties:
- Support: Non-negative integers {0, 1, 2, ...}
- Mean = Variance = λ (rate parameter)
- Mode = floor(λ) if λ is not an integer, otherwise λ and λ-1
Example:
>>> # Prior for a count parameter expecting ~3 events
>>> prior = Poisson(lam=3.0)
>>>
>>> # Prior for rare events
>>> prior = Poisson(lam=0.5)
"""
constraint = POSITIVE
def __init__(self, lam: float, name: str | None = None):
"""Initialize a Poisson prior.
Args:
lam: Rate parameter (expected number of events). Must be positive.
This is both the mean and variance of the distribution.
name: Optional name for the prior
"""
pdf_params = {"lam": lam}
super().__init__(pdf_params, name=name)
def _validate_parameters(self, params: dict):
"""Validate Poisson distribution parameters."""
validate_parameter("lam", params["lam"], POSITIVE)
def _create_pdf(self, obs, lam):
"""Create a Poisson PDF."""
return zfit.pdf.Poisson(lam=lam, obs=obs)
def _get_adapted_bounds(self, param):
"""Ensure bounds are appropriate for discrete distribution."""
lower, upper = super()._get_adapted_bounds(param)
# Poisson is non-negative
lower = max(0, lower)
return lower, upper
[docs]
class Exponential(BasePrior):
"""Exponential prior distribution.
The Exponential distribution is a memoryless continuous distribution often
used to model waiting times, lifetimes, or inter-arrival times. It's the
continuous analog of the geometric distribution and has a constant hazard rate.
This prior is suitable for:
- Rate parameters and inverse time scales
- Waiting times and inter-arrival intervals
- Lifetimes and survival analysis
- Any parameter representing decay or hazard rates
- Regularization when expecting small positive values
Properties:
- Support: Non-negative real numbers [0, ∞)
- Mean = 1/λ, Variance = 1/λ²
- Mode = 0 (exponential decay from maximum at 0)
- Memoryless property: P(X > s+t | X > s) = P(X > t)
Example:
>>> # Prior for a rate parameter with expected value 1/2 = 0.5
>>> prior = Exponential(lam=2.0)
>>>
>>> # Prior for a decay constant
>>> prior = Exponential(lam=1.0) # Mean = 1
"""
constraint = POSITIVE
def __init__(self, lam: float, name: str | None = None):
"""Initialize an Exponential prior.
Args:
lam: Rate parameter (inverse of the mean). Must be positive.
Higher values concentrate probability near zero.
name: Optional name for the prior
"""
pdf_params = {"lam": lam}
super().__init__(pdf_params, name=name)
def _validate_parameters(self, params: dict):
"""Validate Exponential distribution parameters."""
validate_parameter("lam", params["lam"], POSITIVE)
def _create_pdf(self, obs, lam):
"""Create an Exponential PDF."""
return zfit.pdf.Exponential(lam=lam, obs=obs)
[docs]
class StudentT(BasePrior):
"""Student's t-distribution prior.
The Student's t-distribution is a heavy-tailed distribution that approaches
the normal distribution as degrees of freedom increase. It's useful when
you want robustness against outliers while maintaining finite variance
(unlike the Cauchy distribution).
This prior is suitable for:
- Robust inference with outlier resistance
- Parameters where extreme values are possible but not as likely as in Cauchy
- Alternative to Normal when heavier tails are desired
- Location parameters with moderate uncertainty
- Regression coefficients in robust modeling
Properties:
- Support: All real numbers (-∞, ∞)
- Mean = μ (for ndof > 1), undefined for ndof ≤ 1
- Variance = σ²·ndof/(ndof-2) (for ndof > 2)
- Approaches Normal(mu, sigma) as ndof → ∞
- Heavier tails than normal for small ndof
Example:
>>> # Heavy-tailed prior (like Cauchy but with finite variance)
>>> prior = StudentT(ndof=3, mu=0.0, sigma=1.0)
>>>
>>> # Moderately robust prior
>>> prior = StudentT(ndof=10, mu=5.0, sigma=2.0)
"""
constraint = UNCONSTRAINED
def __init__(self, ndof: float, mu: float, sigma: float, name: str | None = None):
"""Initialize a Student's t prior.
Args:
ndof: Degrees of freedom parameter controlling tail heaviness.
Must be positive. Lower values give heavier tails.
As ndof → ∞, approaches Normal(mu, sigma).
mu: Location parameter (center) of the distribution.
sigma: Scale parameter controlling spread. Must be positive.
name: Optional name for the prior
"""
pdf_params = {"ndof": ndof, "mu": mu, "sigma": sigma}
super().__init__(pdf_params, name=name)
def _validate_parameters(self, params: dict):
"""Validate StudentT distribution parameters."""
validate_parameter("ndof", params["ndof"], POSITIVE)
validate_parameter("mu", params["mu"])
validate_parameter("sigma", params["sigma"], POSITIVE)
def _create_pdf(self, obs, ndof, mu, sigma):
"""Create a Student's t PDF."""
return zfit.pdf.StudentT(ndof=ndof, mu=mu, sigma=sigma, obs=obs)
def _create_adapted_pdf(self, obs, lower, upper):
"""Create adapted Student's t PDF with truncation if needed."""
del lower, upper # Unused in base implementation
# Note: zfit's StudentT doesn't support direct truncation
# For now, we'll use the regular StudentT and let the parameter bounds handle it
return zfit.pdf.StudentT(
ndof=self._pdf_params["ndof"], mu=self._pdf_params["mu"], sigma=self._pdf_params["sigma"], obs=obs
)