diff --git a/preliz/__init__.py b/preliz/__init__.py index d089553c..c4c67eb6 100644 --- a/preliz/__init__.py +++ b/preliz/__init__.py @@ -11,6 +11,7 @@ from preliz.distributions import * from preliz.distributions.plot import plot +from preliz.distributions.catalog import catalog from preliz.predictive import * from preliz.ppls import * from preliz.unidimensional import * diff --git a/preliz/distributions/__init__.py b/preliz/distributions/__init__.py index 7138cd42..ca303a44 100644 --- a/preliz/distributions/__init__.py +++ b/preliz/distributions/__init__.py @@ -101,13 +101,11 @@ all_continuous_multivariate = [Dirichlet, MvNormal] +all_modifiers = [Mixture, Truncated, Censored, Hurdle] __all__ = ( # noqa: PLE0604 [s.__name__ for s in all_continuous] + [s.__name__ for s in all_discrete] + [s.__name__ for s in all_continuous_multivariate] - + [Mixture.__name__] - + [Truncated.__name__] - + [Censored.__name__] - + [Hurdle.__name__] + + [s.__name__ for s in all_modifiers] ) diff --git a/preliz/distributions/asymmetric_laplace.py b/preliz/distributions/asymmetric_laplace.py index b9b37eae..099f7677 100644 --- a/preliz/distributions/asymmetric_laplace.py +++ b/preliz/distributions/asymmetric_laplace.py @@ -62,6 +62,8 @@ class AsymmetricLaplace(Continuous): Symmetry parameter (0 < q < 1). """ + parametrizations = [("kappa", "mu", "b"), ("q", "mu", "b")] + def __init__(self, kappa=None, mu=None, b=None, q=None): super().__init__() self.support = (-pt.inf, pt.inf) diff --git a/preliz/distributions/bernoulli.py b/preliz/distributions/bernoulli.py index 087f4209..967f6bf3 100644 --- a/preliz/distributions/bernoulli.py +++ b/preliz/distributions/bernoulli.py @@ -47,6 +47,8 @@ class Bernoulli(Discrete): Alternative log odds for the probability of success. """ + parametrizations = [("p",), ("logit_p",)] + def __init__(self, p=None, logit_p=None): super().__init__() self.support = (0, 1) diff --git a/preliz/distributions/beta.py b/preliz/distributions/beta.py index 2e24f248..1cc20433 100644 --- a/preliz/distributions/beta.py +++ b/preliz/distributions/beta.py @@ -70,6 +70,8 @@ class Beta(Continuous): concentration > 0 """ + parametrizations = [("alpha", "beta"), ("mu", "sigma"), ("mu", "nu")] + def __init__(self, alpha=None, beta=None, mu=None, sigma=None, nu=None): super().__init__() self.support = (0, 1) diff --git a/preliz/distributions/catalog.py b/preliz/distributions/catalog.py new file mode 100644 index 00000000..0e338c42 --- /dev/null +++ b/preliz/distributions/catalog.py @@ -0,0 +1,403 @@ +"""Distribution catalog for exploring and querying PreliZ distributions.""" + +from sys import modules + +import numpy as np + +from preliz.distributions import ( + all_continuous, + all_continuous_multivariate, + all_discrete, + all_modifiers, +) +from preliz.distributions.distributions import _format_support + +_GROUPS = { + "continuous": all_continuous, + "discrete": all_discrete, + "continuous_multivariate": all_continuous_multivariate, + "unbounded": [ + "AsymmetricLaplace", + "Cauchy", + "ExGaussian", + "Gumbel", + "Laplace", + "Logistic", + "Moyal", + "Normal", + "SkewNormal", + "SkewStudentT", + "StudentT", + ], + "positive": [ + "ChiSquared", + "Exponential", + "Gamma", + "HalfCauchy", + "HalfNormal", + "HalfStudentT", + "InverseGamma", + "LogLogistic", + "LogNormal", + "Pareto", + "Rice", + "Wald", + "Weibull", + ], + "bounded": [ + "Beta", + "BetaScaled", + "Kumaraswamy", + "LogitNormal", + "Triangular", + "TruncatedNormal", + "Uniform", + "VonMises", + ], + "non_negative_continuous": [ + "ChiSquared", + "Exponential", + "Gamma", + "HalfCauchy", + "HalfNormal", + "HalfStudentT", + "InverseGamma", + "LogLogistic", + "LogitNormal", + "LogNormal", + "Pareto", + "Rice", + "ScaledInverseChiSquared", + "Wald", + "Weibull", + ], + "non_negative_discrete": [ + "Bernoulli", + "Binomial", + "DiscreteWeibull", + "Geometric", + "NegativeBinomial", + "Poisson", + "ZeroInflatedBinomial", + "ZeroInflatedNegativeBinomial", + "ZeroInflatedPoisson", + ], + "bounded_discrete": [ + "BetaBinomial", + "Binomial", + "Categorical", + "DiscreteUniform", + "Hypergeometric", + "ZeroInflatedBinomial", + ], + "multivariate": [ + "Dirichlet", + "MultivariateNormal", + ], + "symmetric": [ + "Beta", + "BetaScaled", + "Cauchy", + "DiscreteUniform", + "Laplace", + "Logistic", + "MultivariateNormal", + "Normal", + "StudentT", + "Uniform", + "VonMises", + ], + "asymmetric": [ + "AsymmetricLaplace", + "DiscreteWeibull", + "ExGaussian", + "Exponential", + "Gamma", + "Geometric", + "Gumbel", + "InverseGamma", + "Kumaraswamy", + "LogLogistic", + "LogitNormal", + "Moyal", + "Pareto", + "Rice", + "ScaledInverseChiSquared", + "SkewNormal", + "SkewStudentT", + "Wald", + "Weibull", + ], + "heavy_tailed": [ + "Cauchy", + "HalfCauchy", + "HalfStudentT", + "InverseGamma", + "LogLogistic", + "LogNormal", + "Pareto", + "SkewStudentT", + "StudentT", + ], + "light_tailed": [ + "AsymmetricLaplace", + "ChiSquared", + "ExGaussian", + "Exponential", + "Gamma", + "HalfNormal", + "Laplace", + "Logistic", + "Moyal", + "Normal", + "Rice", + "SkewNormal", + "Triangular", + "TruncatedNormal", + "Wald", + "Weibull", + ], + "zero_inflated": [ + "ZeroInflatedBinomial", + "ZeroInflatedNegativeBinomial", + "ZeroInflatedPoisson", + ], + "extreme_value": [ + "Gumbel", + "LogLogistic", + ], + "circular": [ + "VonMises", + ], + "binary": [ + "Bernoulli", + "Binomial", + ], + "count": [ + "DiscreteWeibull", + "Geometric", + "NegativeBinomial", + "Poisson", + "ZeroInflatedNegativeBinomial", + "ZeroInflatedPoisson", + ], +} + + +def _get_dist_class(name): + return getattr(modules["preliz.distributions"], name) + + +class DistributionCatalog: + """Registry for accessing PreliZ distributions. + + Provides methods to list, filter, and inspect distributions. + + Examples + -------- + List all distributions:: + + >>> pz.catalog + + Get instances by category:: + + >>> pz.catalog.get("continuous") + >>> pz.catalog.get("positive") + + Get distribution names:: + + >>> pz.catalog.get(output="names") + >>> pz.catalog.get("discrete", output="names") + + Get info about a specific distribution:: + + >>> pz.catalog.info("Gamma") + + Filter distributions by properties:: + + >>> pz.catalog.find(kind="continuous", num_params=2) + """ + + def __repr__(self): + continuous_names = [d.__name__ for d in all_continuous] + discrete_names = [d.__name__ for d in all_discrete] + multivariate_names = [d.__name__ for d in all_continuous_multivariate] + modifiers = [d.__name__ for d in all_modifiers] + + lines = ["PreliZ Distributions", "=" * 50] + lines.append(f"Continuous ({len(continuous_names)}):") + lines.append(" " + ", ".join(continuous_names)) + lines.append(f"\nDiscrete ({len(discrete_names)}):") + lines.append(" " + ", ".join(discrete_names)) + lines.append(f"\nMultivariate ({len(multivariate_names)}):") + lines.append(" " + ", ".join(multivariate_names)) + lines.append(f"\nModifiers ({len(modifiers)}):") + lines.append(" " + ", ".join(modifiers)) + return "\n".join(lines) + + def _repr_html_(self): + continuous_names = [d.__name__ for d in all_continuous] + discrete_names = [d.__name__ for d in all_discrete] + multivariate_names = [d.__name__ for d in all_continuous_multivariate] + modifiers = [d.__name__ for d in all_modifiers] + + html = ["
"] + html.append("PreliZ Distributions

") + html.append(f"Continuous ({len(continuous_names)}): ") + html.append(", ".join(continuous_names) + "

") + html.append(f"Discrete ({len(discrete_names)}): ") + html.append(", ".join(discrete_names) + "

") + html.append(f"Multivariate ({len(multivariate_names)}): ") + html.append(", ".join(multivariate_names) + "

") + html.append(f"Modifiers ({len(modifiers)}): ") + html.append(", ".join(modifiers)) + html.append("
") + + return "".join(html) + + def get(self, category="continuous", output="instances"): + """Return a list of uninitialized PreliZ distribution instances by category. + + Parameters + ---------- + category : str + Category of distributions to return. One of: + - ``"continuous"``: All univariate continuous distributions. + - ``"discrete"``: All discrete distributions. + - ``"continuous_multivariate"``: All continuous multivariate distributions. + - ``"positive"``: Continuous distributions on the positive reals. + - ``"unbounded"``: Continuous distributions on the full real line. + - ``"bounded"``: Continuous distributions on a finite interval. + - ``"non_negative"``: All non-negative distributions. + - ``"non_negative_continuous"``: Continuous distributions on [0, inf). + - ``"non_negative_discrete"``: Discrete distributions on {0, 1, 2, ...}. + - ``"bounded_discrete"``: Discrete distributions on a finite interval. + - ``"multivariate"``: All multivariate distributions. + - ``"symmetric"``: Distributions that are symmetric. + - ``"asymmetric"``: Distributions with skewed shapes. + - ``"heavy_tailed"``: Distributions with slowly decaying tails. + - ``"light_tailed"``: Distributions with quickly decaying tails. + - ``"zero_inflated"``: Discrete distributions with extra zeros. + - ``"extreme_value"``: Distributions for extreme events. + - ``"circular"``: Distributions on a circular domain. + - ``"binary"``: Distributions for binary outcomes. + - ``"count"``: Discrete distributions for count data. + + output : str + Whether to return distribution instances ("instances") or names ("names"). + Defaults to "instances". + + Returns + ------- + list of PreliZ distribution instances + """ + if output not in ["instances", "names"]: + raise ValueError("Invalid value for 'output'. Must be 'instances' or 'names'.") + + if output == "instances": + group = _GROUPS.get(category) + if group is None: + raise ValueError( + f"Unknown category '{category}'. " + f"Must be one of: {', '.join(repr(k) for k in _GROUPS)}" + ) + if category in ("continuous", "discrete", "continuous_multivariate"): + return [d() for d in group] + return [_get_dist_class(name)() for name in group] + + else: + group = _GROUPS.get(category) + if group is None: + raise ValueError( + f"Unknown category '{category}'. " + f"Must be one of: {', '.join(repr(k) for k in _GROUPS)}" + ) + if category in ("continuous", "discrete", "continuous_multivariate"): + return [d.__name__ for d in group] + return list(group) + + def info(self, name): + """Return metadata about a distribution. + + Parameters + ---------- + name : str + Name of the distribution (e.g., "Gamma", "Normal"). + + Returns + ------- + dict + Dictionary with keys: name, kind, param_names, params_support, support, + parametrizations. + """ + dist_cls = _get_dist_class(name) + dist = dist_cls() + result = { + "name": name, + "kind": dist.kind, + "param_names": dist.param_names, + "params_support": _format_support(dist.params_support), + "support": _format_support(dist.support), + } + parametrizations = getattr(dist_cls, "parametrizations", None) + if parametrizations is not None: + result["parametrizations"] = parametrizations + return result + + def find(self, kind=None, num_params=None, support=None): + """Find distributions matching given criteria. + + Parameters + ---------- + kind : str, optional + Filter by kind: "continuous", "discrete". + num_params : int, optional + Filter by number of parameters. + support : str, optional + Filter by support type: "positive", "bounded", "unbounded", + "non_negative". + + Returns + ------- + list of PreliZ distribution instances + """ + results = [] + + if kind == "continuous": + candidates = all_continuous + elif kind == "discrete": + candidates = all_discrete + else: + candidates = all_continuous + all_discrete + + for dist_cls in candidates: + dist = dist_cls() + + if num_params is not None and len(dist.param_names) != num_params: + continue + + if support is not None: + if not self._matches_support(dist, support): + continue + + results.append(dist) + + return results + + @staticmethod + def _matches_support(dist, support_type): + lower, upper = dist.support + if lower is None or upper is None: + return False + if support_type == "positive": + return lower >= 0 and upper == np.inf + elif support_type == "bounded": + return lower != -np.inf and upper != np.inf + elif support_type == "unbounded": + return lower == -np.inf and upper == np.inf + elif support_type == "non_negative": + return lower >= 0 + return True + + +catalog = DistributionCatalog() diff --git a/preliz/distributions/categorical.py b/preliz/distributions/categorical.py index 2529bcb0..0ae6b90f 100644 --- a/preliz/distributions/categorical.py +++ b/preliz/distributions/categorical.py @@ -37,8 +37,11 @@ class Categorical(Discrete): Alternative log odds for the probability of success. """ + parametrizations = [("p",), ("logit_p",)] + def __init__(self, p=None, logit_p=None): super().__init__() + self.support = (None, None) self._parametrization(p, logit_p) def _parametrization(self, p=None, logit_p=None): diff --git a/preliz/distributions/distributions.py b/preliz/distributions/distributions.py index baae5aab..ccc807e0 100644 --- a/preliz/distributions/distributions.py +++ b/preliz/distributions/distributions.py @@ -71,10 +71,47 @@ def __repr__(self): def _repr_html_(self): name = self._get_name() + if self.is_frozen: desc = self._get_description() - return f"{name}({desc})" - return f"{name}" + summary = f"{name}({desc})" + else: + summary = f"{name}" + + td_label = "style='text-align:right;padding-right:10px'" + rows = [] + rows.append(f"Kind{self.kind}") + + support = _format_support(self.support) + if isinstance(support, tuple): + support_str = f"({support[0]}, {support[1]})" + else: + support_str = str(support) + rows.append(f"Support{support_str}") + + if self.is_frozen: + try: + rows.append(f"Mean{self.mean():.2g}") + except Exception: + pass + try: + rows.append(f"Std{self.std():.2g}") + except Exception: + pass + + parametrizations = getattr(self.__class__, "parametrizations", None) + if parametrizations: + params_str = ", ".join(f"({', '.join(p)})" for p in parametrizations) + rows.append( + f"Parametrizations{params_str}" + ) + + html = f"
{summary}" + html += "" + html += "".join(rows) + html += "
" + + return html @property def params_dict(self): @@ -83,6 +120,36 @@ def params_dict(self): else: return None + def info(self): + """Return metadata about this distribution. + + Returns + ------- + dict + Dictionary with keys: name, kind, param_names, params, params_support, + support, is_frozen, parametrizations. + """ + result = { + "name": self._get_name(), + "kind": self.kind, + "param_names": self.param_names, + "params_support": _format_support(self.params_support), + "support": _format_support(self.support), + } + if self.is_frozen: + result["params"] = self.params + + parametrizations = getattr(self.__class__, "parametrizations", None) + if parametrizations is not None: + result["parametrizations"] = parametrizations + if self.is_frozen: + result["parametrization_values"] = { + params: tuple(getattr(self, name) for name in params) + for params in parametrizations + } + + return result + def summary(self, mass=None, interval=None, fmt=".2f"): """ Namedtuple with the mean, median, sd, and lower and upper bounds. @@ -1122,3 +1189,19 @@ def _discrete_xvals(lower_ep, upper_ep, n_points): x_vals = np.linspace(lower_ep, upper_ep + 1, n_points, dtype=int) return x_vals + + +def _format_support_value(value): + if value == np.inf: + return "inf" + if value == -np.inf: + return "-inf" + if isinstance(value, float | np.floating) and 0 < abs(value) < 1e-10: + return 0 + return value + + +def _format_support(values): + if isinstance(values, tuple | list): + return tuple(_format_support(v) for v in values) + return _format_support_value(values) diff --git a/preliz/distributions/exponential.py b/preliz/distributions/exponential.py index 1c4ea164..27cf356c 100644 --- a/preliz/distributions/exponential.py +++ b/preliz/distributions/exponential.py @@ -48,6 +48,8 @@ class Exponential(Continuous): Scale (scale > 0). """ + parametrizations = [("lam",), ("scale",)] + def __init__(self, lam=None, scale=None): super().__init__() self.support = (0, np.inf) diff --git a/preliz/distributions/gamma.py b/preliz/distributions/gamma.py index 7fd10f20..67ad7092 100644 --- a/preliz/distributions/gamma.py +++ b/preliz/distributions/gamma.py @@ -65,6 +65,8 @@ class Gamma(Continuous): """ + parametrizations = [("alpha", "beta"), ("mu", "sigma")] + def __init__(self, alpha=None, beta=None, mu=None, sigma=None): super().__init__() self.support = (0, np.inf) diff --git a/preliz/distributions/halfnormal.py b/preliz/distributions/halfnormal.py index db7aab25..86fb6da8 100644 --- a/preliz/distributions/halfnormal.py +++ b/preliz/distributions/halfnormal.py @@ -56,6 +56,8 @@ class HalfNormal(Continuous): Precision :math:`\tau` (``tau`` > 0). """ + parametrizations = [("sigma",), ("tau",)] + def __init__(self, sigma=None, tau=None): super().__init__() self.support = (0, np.inf) diff --git a/preliz/distributions/halfstudentt.py b/preliz/distributions/halfstudentt.py index 8b42665a..fcf797dd 100644 --- a/preliz/distributions/halfstudentt.py +++ b/preliz/distributions/halfstudentt.py @@ -72,6 +72,8 @@ class HalfStudentT(Continuous): Scale parameter (lam > 0). Converges to the precision as nu increases. """ + parametrizations = [("nu", "sigma"), ("nu", "lam")] + def __init__(self, nu=None, sigma=None, lam=None): super().__init__() self.support = (0, np.inf) diff --git a/preliz/distributions/inversegamma.py b/preliz/distributions/inversegamma.py index 93a7f2a0..fb64bae2 100644 --- a/preliz/distributions/inversegamma.py +++ b/preliz/distributions/inversegamma.py @@ -63,6 +63,8 @@ class InverseGamma(Continuous): Standard deviation (sigma > 0) """ + parametrizations = [("alpha", "beta"), ("mu", "sigma")] + def __init__(self, alpha=None, beta=None, mu=None, sigma=None): super().__init__() self.support = (0, np.inf) diff --git a/preliz/distributions/logitnormal.py b/preliz/distributions/logitnormal.py index 519ddd64..981cdee9 100644 --- a/preliz/distributions/logitnormal.py +++ b/preliz/distributions/logitnormal.py @@ -55,6 +55,8 @@ class LogitNormal(Continuous): Scale parameter (tau > 0). """ + parametrizations = [("mu", "sigma"), ("mu", "tau")] + def __init__(self, mu=None, sigma=None, tau=None): super().__init__() self.support = (0, 1) diff --git a/preliz/distributions/negativebinomial.py b/preliz/distributions/negativebinomial.py index 413ff374..14337500 100644 --- a/preliz/distributions/negativebinomial.py +++ b/preliz/distributions/negativebinomial.py @@ -72,6 +72,8 @@ class NegativeBinomial(Discrete): Number of target success trials (n > 0) """ + parametrizations = [("mu", "alpha"), ("p", "n")] + def __init__(self, mu=None, alpha=None, p=None, n=None): super().__init__() self.support = (0, np.inf) diff --git a/preliz/distributions/normal.py b/preliz/distributions/normal.py index 365250e0..b86ef1ab 100644 --- a/preliz/distributions/normal.py +++ b/preliz/distributions/normal.py @@ -61,6 +61,8 @@ class Normal(Continuous): Precision (tau > 0). """ + parametrizations = [("mu", "sigma"), ("mu", "tau")] + def __init__(self, mu=None, sigma=None, tau=None): super().__init__() self.support = (-pt.inf, pt.inf) diff --git a/preliz/distributions/studentt.py b/preliz/distributions/studentt.py index d29a31a3..9ea81a88 100644 --- a/preliz/distributions/studentt.py +++ b/preliz/distributions/studentt.py @@ -67,6 +67,8 @@ class StudentT(Continuous): Scale parameter (lam > 0). Converges to the precision as nu increases. """ + parametrizations = [("nu", "mu", "sigma"), ("nu", "mu", "lam")] + def __init__(self, nu=None, mu=None, sigma=None, lam=None): super().__init__() self.support = (-np.inf, np.inf) diff --git a/preliz/distributions/wald.py b/preliz/distributions/wald.py index 5153e3bd..db858738 100644 --- a/preliz/distributions/wald.py +++ b/preliz/distributions/wald.py @@ -56,6 +56,8 @@ class Wald(Continuous): Shape parameter (phi > 0). """ + parametrizations = [("mu", "lam"), ("mu", "phi"), ("lam", "phi")] + def __init__(self, mu=None, lam=None, phi=None): super().__init__() self.support = (0, np.inf) diff --git a/preliz/tests/test_catalog.py b/preliz/tests/test_catalog.py new file mode 100644 index 00000000..f6aadafe --- /dev/null +++ b/preliz/tests/test_catalog.py @@ -0,0 +1,157 @@ +import pytest + +from preliz.distributions.catalog import catalog + + +class TestCatalogGet: + def test_get_continuous_instances(self): + dists = catalog.get("continuous") + assert len(dists) > 0 + assert all(hasattr(d, "pdf") for d in dists) + assert all(hasattr(d, "kind") for d in dists) + + def test_get_discrete_instances(self): + dists = catalog.get("discrete") + assert len(dists) > 0 + assert all(hasattr(d, "pmf") or hasattr(d, "pdf") for d in dists) + + def test_get_continuous_names(self): + names = catalog.get("continuous", output="names") + assert len(names) > 0 + assert all(isinstance(n, str) for n in names) + assert "Normal" in names + assert "Gamma" in names + + def test_get_discrete_names(self): + names = catalog.get("discrete", output="names") + assert len(names) > 0 + assert "Poisson" in names + assert "Binomial" in names + + def test_get_by_support_category(self): + positive = catalog.get("positive") + assert len(positive) > 0 + + bounded = catalog.get("bounded") + assert len(bounded) > 0 + + unbounded = catalog.get("unbounded") + assert len(unbounded) > 0 + + def test_get_invalid_category(self): + with pytest.raises(ValueError, match="Unknown category"): + catalog.get("invalid_category") + + def test_get_invalid_output(self): + with pytest.raises(ValueError, match="Invalid value for 'output'"): + catalog.get("continuous", output="invalid") + + +class TestCatalogInfo: + def test_info_returns_dict(self): + info = catalog.info("Gamma") + assert isinstance(info, dict) + + def test_info_has_required_keys(self): + info = catalog.info("Gamma") + assert "name" in info + assert "kind" in info + assert "param_names" in info + assert "params_support" in info + assert "support" in info + + def test_info_gamma(self): + info = catalog.info("Gamma") + assert info["name"] == "Gamma" + assert info["kind"] == "continuous" + assert info["param_names"] == ("alpha", "beta") + assert "parametrizations" in info + assert ("alpha", "beta") in info["parametrizations"] + assert ("mu", "sigma") in info["parametrizations"] + + def test_info_normal(self): + info = catalog.info("Normal") + assert info["name"] == "Normal" + assert info["kind"] == "continuous" + assert "parametrizations" in info + + def test_info_poisson(self): + info = catalog.info("Poisson") + assert info["name"] == "Poisson" + assert info["kind"] == "discrete" + assert info["param_names"] == ("mu",) + + def test_info_support_formatting(self): + info = catalog.info("Gamma") + assert info["support"] == (0, "inf") + assert info["params_support"] == ((0, "inf"), (0, "inf")) + + info = catalog.info("Normal") + assert info["support"] == ("-inf", "inf") + + def test_info_invalid_name(self): + with pytest.raises(AttributeError): + catalog.info("NonExistentDistribution") + + +class TestCatalogFind: + def test_find_by_kind_continuous(self): + dists = catalog.find(kind="continuous") + assert len(dists) > 0 + assert all(d.kind == "continuous" for d in dists) + + def test_find_by_kind_discrete(self): + dists = catalog.find(kind="discrete") + assert len(dists) > 0 + assert all(d.kind == "discrete" for d in dists) + + def test_find_by_num_params(self): + dists = catalog.find(num_params=1) + assert len(dists) > 0 + assert all(len(d.param_names) == 1 for d in dists) + + dists = catalog.find(num_params=2) + assert len(dists) > 0 + assert all(len(d.param_names) == 2 for d in dists) + + def test_find_by_support_positive(self): + dists = catalog.find(support="positive") + assert len(dists) > 0 + for d in dists: + lower, upper = d.support + assert lower >= 0 + assert upper == float("inf") + + def test_find_by_support_bounded(self): + dists = catalog.find(support="bounded") + assert len(dists) > 0 + for d in dists: + lower, upper = d.support + assert lower != float("-inf") + assert upper != float("inf") + + def test_find_combined_filters(self): + dists = catalog.find(kind="continuous", num_params=2) + assert len(dists) > 0 + assert all(d.kind == "continuous" for d in dists) + assert all(len(d.param_names) == 2 for d in dists) + + def test_find_no_filters(self): + dists = catalog.find() + assert len(dists) > 0 + + +class TestCatalogRepr: + def test_repr_contains_distributions(self): + repr_str = repr(catalog) + assert "PreliZ Distributions" in repr_str + assert "Continuous" in repr_str + assert "Discrete" in repr_str + assert "Normal" in repr_str + assert "Poisson" in repr_str + + def test_repr_html_contains_distributions(self): + html = catalog._repr_html_() + assert "