From c159c51f5c4fff095206b41789615cd3a12326bc Mon Sep 17 00:00:00 2001 From: Jonathan Berthias Date: Sat, 26 Sep 2026 15:24:57 +0200 Subject: [PATCH 1/4] Add plain SciPy wrapper factory --- pykelihood/distributions/core.py | 19 +--- pykelihood/distributions/scipy_wrappers.py | 106 +++++++++++++++++++++ tests/test_distribution_core.py | 3 +- tests/test_scipy_wrappers.py | 73 ++++++++++++++ 4 files changed, 182 insertions(+), 19 deletions(-) create mode 100644 pykelihood/distributions/scipy_wrappers.py create mode 100644 tests/test_scipy_wrappers.py diff --git a/pykelihood/distributions/core.py b/pykelihood/distributions/core.py index df01b3e..c26e896 100644 --- a/pykelihood/distributions/core.py +++ b/pykelihood/distributions/core.py @@ -10,12 +10,11 @@ import numpy as np import numpy.typing as npt -from scipy import stats from scipy.stats import rv_continuous from pykelihood.expr import Constant, Expr, Node, PathElem from pykelihood.parameters import Parameter -from pykelihood.state import PositiveTransform, Transform +from pykelihood.state import Transform ParameterInput = Union[Expr, npt.ArrayLike, None] ParameterState = Mapping[Parameter, npt.NDArray[np.float64]] @@ -183,19 +182,3 @@ def ppf( self._scipy_distribution.ppf(q, **self._evaluated_parameters(state)), dtype=np.float64, ) - - -class Normal(ScipyDistribution): - """Normal distribution with free default location and scale parameters.""" - - def __init__( - self, loc: ParameterInput = None, scale: ParameterInput = None - ) -> None: - super().__init__( - stats.norm, - {"loc": loc, "scale": scale}, - defaults={ - "loc": ParameterDefault(0.0), - "scale": ParameterDefault(1.0, PositiveTransform()), - }, - ) diff --git a/pykelihood/distributions/scipy_wrappers.py b/pykelihood/distributions/scipy_wrappers.py new file mode 100644 index 0000000..dbe7989 --- /dev/null +++ b/pykelihood/distributions/scipy_wrappers.py @@ -0,0 +1,106 @@ +"""Opt-in structural wrappers for selected SciPy continuous distributions.""" + +from __future__ import annotations + +import inspect +from typing import Any, ClassVar + +from scipy import stats +from scipy.stats import rv_continuous + +from pykelihood.distributions.core import ( + ParameterDefault, + ParameterInput, + ScipyDistribution, +) +from pykelihood.state import PositiveTransform + + +def _name_from_scipy_dist(scipy_dist: rv_continuous) -> str: + raw_name = type(scipy_dist).__name__.removesuffix("_gen") + return "".join(part.capitalize() for part in raw_name.split("_")) + + +class _WrappedScipyDistribution(ScipyDistribution): + """Shared constructor for generated plain SciPy distributions.""" + + _base_module: ClassVar[rv_continuous] + _shape_names: ClassVar[tuple[str, ...]] + _parameter_names: ClassVar[tuple[str, ...]] + + def __init__(self, *args: ParameterInput, **kwargs: ParameterInput) -> None: + names = self._parameter_names + if len(args) > len(names): + raise TypeError( + f"Expected at most {len(names)} positional parameters, got {len(args)}." + ) + supplied = dict(zip(names, args)) + duplicates = supplied.keys() & kwargs.keys() + if duplicates: + raise TypeError(f"Parameter {min(duplicates)!r} supplied more than once.") + unknown = kwargs.keys() - set(names) + if unknown: + raise TypeError(f"Unexpected distribution parameter: {min(unknown)}") + supplied.update(kwargs) + missing = tuple( + name for name in self._shape_names if supplied.get(name) is None + ) + if missing: + raise TypeError( + "Missing required distribution parameter(s): " + ", ".join(missing) + ) + ordered = {name: supplied.get(name) for name in names} + super().__init__( + self._base_module, + ordered, + defaults={ + "loc": ParameterDefault(0.0), + "scale": ParameterDefault(1.0, PositiveTransform()), + }, + ) + + +def wrap_scipy_distribution( + scipy_dist: rv_continuous, +) -> type[_WrappedScipyDistribution]: + """Create a structural distribution class for one SciPy continuous law. + + Shape parameters follow SciPy's native names and are required. ``loc`` and + ``scale`` are optional free parameters initialized to 0 and 1 respectively. + Literal arguments become constants through :class:`ScipyDistribution`. + The generated constructor accepts arguments in SciPy's order: shape + parameters, then ``loc`` and ``scale``. + """ + shape_names = ( + () + if scipy_dist.shapes is None + else tuple(name.strip() for name in scipy_dist.shapes.split(",")) + ) + parameter_names = (*shape_names, "loc", "scale") + signature_parameters = [ + inspect.Parameter( + name, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + default=inspect.Parameter.empty if name in shape_names else None, + ) + for name in parameter_names + ] + signature = inspect.Signature(signature_parameters) + wrapper_name = _name_from_scipy_dist(scipy_dist) + namespace: dict[str, Any] = { + "_base_module": scipy_dist, + "_shape_names": shape_names, + "_parameter_names": parameter_names, + "__doc__": f"Structural wrapper for ``scipy.stats.{scipy_dist.name}``.", + "__module__": __name__, + "__signature__": signature, + } + return type(wrapper_name, (_WrappedScipyDistribution,), namespace) + + +Norm = wrap_scipy_distribution(stats.norm) +Normal = Norm +Gamma = wrap_scipy_distribution(stats.gamma) +Genextreme = wrap_scipy_distribution(stats.genextreme) + +__all__ = ["Gamma", "Genextreme", "Norm", "Normal", "wrap_scipy_distribution"] diff --git a/tests/test_distribution_core.py b/tests/test_distribution_core.py index 18f77e5..6be164c 100644 --- a/tests/test_distribution_core.py +++ b/tests/test_distribution_core.py @@ -3,7 +3,8 @@ from numpy.testing import assert_allclose from scipy import stats -from pykelihood.distributions.core import Normal, ParameterDefault, ScipyDistribution +from pykelihood.distributions.core import ParameterDefault, ScipyDistribution +from pykelihood.distributions.scipy_wrappers import Normal from pykelihood.effects import linear from pykelihood.expr import Constant from pykelihood.likelihood import negative_log_likelihood diff --git a/tests/test_scipy_wrappers.py b/tests/test_scipy_wrappers.py new file mode 100644 index 0000000..bae84c7 --- /dev/null +++ b/tests/test_scipy_wrappers.py @@ -0,0 +1,73 @@ +import inspect + +import numpy as np +import pytest +from numpy.testing import assert_allclose +from scipy import stats + +from pykelihood.distributions.core import ScipyDistribution +from pykelihood.distributions.scipy_wrappers import Gamma, Genextreme, Norm, Normal +from pykelihood.expr import Constant +from pykelihood.likelihood import negative_log_likelihood +from pykelihood.parameters import Parameter +from pykelihood.parametric import fit_mle +from pykelihood.state import ParameterLayout, PositiveTransform + + +def test_factory_builds_named_classes_with_native_parameters_and_alias() -> None: + assert Norm is Normal + assert Normal.__name__ == "Norm" + assert Gamma.__name__ == "Gamma" + assert Genextreme.__name__ == "Genextreme" + assert tuple(inspect.signature(Gamma).parameters) == ("a", "loc", "scale") + assert tuple(inspect.signature(Genextreme).parameters) == ("c", "loc", "scale") + + +def test_generated_wrapper_requires_shape_and_keeps_scipy_parameterization() -> None: + with pytest.raises(TypeError, match="Missing required.*a"): + Gamma() + + shape = Parameter(init=2.0) + distribution = Gamma(a=shape, loc=1.0, scale=3.0) + + assert isinstance(distribution, ScipyDistribution) + assert distribution.parameters["a"] is shape + assert isinstance(distribution.parameters["loc"], Constant) + assert isinstance(distribution.parameters["scale"], Constant) + assert_allclose(distribution.parameters["loc"].value, 1.0) + assert_allclose(distribution.parameters["scale"].value, 3.0) + assert_allclose( + distribution.pdf([1.0, 2.0]), + stats.gamma.pdf([1.0, 2.0], a=2.0, loc=1.0, scale=3.0), + ) + + +def test_generated_defaults_are_free_parameters_and_fit_uses_the_wrapper() -> None: + distribution = Normal() + location = distribution.parameters["loc"] + scale = distribution.parameters["scale"] + + assert isinstance(location, Parameter) + assert isinstance(scale, Parameter) + assert isinstance(scale.transform, PositiveTransform) + assert ParameterLayout.from_expr(distribution).parameters == (location, scale) + assert_allclose(distribution.pdf(np.array([0.0, 1.0])), stats.norm.pdf([0.0, 1.0])) + + +def test_generated_gamma_fits_its_native_shape_parameter() -> None: + shape = Parameter(init=1.0, transform=PositiveTransform()) + distribution = Gamma(a=shape, loc=0.0, scale=1.0) + data = stats.gamma.ppf(np.linspace(0.1, 0.9, 9), a=2.0) + initial_score = negative_log_likelihood(distribution, data) + + result = fit_mle(distribution, data) + + assert result.model is distribution + assert result.optimize_result.success + assert tuple(result.state) == (shape,) + assert result.state[shape] > 0.0 + assert ( + negative_log_likelihood(distribution, data, state=result.state) < initial_score + ) + assert shape.init is not None + assert_allclose(shape.init, 1.0) From 69308cd6334ea1afd0c0e492ad01a18f451b978e Mon Sep 17 00:00:00 2001 From: Jonathan Berthias Date: Sun, 27 Sep 2026 14:11:20 +0200 Subject: [PATCH 2/4] Strengthen SciPy wrapper contract tests --- tests/test_scipy_wrappers.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/test_scipy_wrappers.py b/tests/test_scipy_wrappers.py index bae84c7..3968295 100644 --- a/tests/test_scipy_wrappers.py +++ b/tests/test_scipy_wrappers.py @@ -19,13 +19,21 @@ def test_factory_builds_named_classes_with_native_parameters_and_alias() -> None assert Normal.__name__ == "Norm" assert Gamma.__name__ == "Gamma" assert Genextreme.__name__ == "Genextreme" + assert tuple(inspect.signature(Normal).parameters) == ("loc", "scale") assert tuple(inspect.signature(Gamma).parameters) == ("a", "loc", "scale") assert tuple(inspect.signature(Genextreme).parameters) == ("c", "loc", "scale") + assert_allclose(Norm(1.0, 2.0).pdf(0.5), stats.norm.pdf(0.5, loc=1.0, scale=2.0)) def test_generated_wrapper_requires_shape_and_keeps_scipy_parameterization() -> None: with pytest.raises(TypeError, match="Missing required.*a"): Gamma() + with pytest.raises(TypeError, match="Unexpected distribution parameter"): + Gamma(a=2.0, typo=1.0) + with pytest.raises(TypeError, match="supplied more than once"): + Gamma(2.0, a=3.0) + with pytest.raises(TypeError, match="at most"): + Gamma(2.0, 0.0, 1.0, 4.0) shape = Parameter(init=2.0) distribution = Gamma(a=shape, loc=1.0, scale=3.0) @@ -42,8 +50,8 @@ def test_generated_wrapper_requires_shape_and_keeps_scipy_parameterization() -> ) -def test_generated_defaults_are_free_parameters_and_fit_uses_the_wrapper() -> None: - distribution = Normal() +def test_generated_defaults_are_free_parameters() -> None: + distribution = Norm() location = distribution.parameters["loc"] scale = distribution.parameters["scale"] @@ -65,7 +73,8 @@ def test_generated_gamma_fits_its_native_shape_parameter() -> None: assert result.model is distribution assert result.optimize_result.success assert tuple(result.state) == (shape,) - assert result.state[shape] > 0.0 + expected_shape = stats.gamma.fit(data, floc=0.0, fscale=1.0)[0] + assert result.state[shape] == pytest.approx(expected_shape, rel=1e-3) assert ( negative_log_likelihood(distribution, data, state=result.state) < initial_score ) From 311df3e2ba07e68bb630dd1ff4e40c71d8e2692c Mon Sep 17 00:00:00 2001 From: Jonathan Berthias Date: Sun, 27 Sep 2026 15:18:59 +0200 Subject: [PATCH 3/4] Use wrapper signatures for argument binding --- pykelihood/distributions/scipy_wrappers.py | 33 ++++------------------ tests/test_scipy_wrappers.py | 8 +++--- 2 files changed, 9 insertions(+), 32 deletions(-) diff --git a/pykelihood/distributions/scipy_wrappers.py b/pykelihood/distributions/scipy_wrappers.py index dbe7989..94c6322 100644 --- a/pykelihood/distributions/scipy_wrappers.py +++ b/pykelihood/distributions/scipy_wrappers.py @@ -17,42 +17,21 @@ def _name_from_scipy_dist(scipy_dist: rv_continuous) -> str: - raw_name = type(scipy_dist).__name__.removesuffix("_gen") - return "".join(part.capitalize() for part in raw_name.split("_")) + return "".join(part.capitalize() for part in scipy_dist.name.split("_")) class _WrappedScipyDistribution(ScipyDistribution): """Shared constructor for generated plain SciPy distributions.""" _base_module: ClassVar[rv_continuous] - _shape_names: ClassVar[tuple[str, ...]] - _parameter_names: ClassVar[tuple[str, ...]] + __signature__: ClassVar[inspect.Signature] def __init__(self, *args: ParameterInput, **kwargs: ParameterInput) -> None: - names = self._parameter_names - if len(args) > len(names): - raise TypeError( - f"Expected at most {len(names)} positional parameters, got {len(args)}." - ) - supplied = dict(zip(names, args)) - duplicates = supplied.keys() & kwargs.keys() - if duplicates: - raise TypeError(f"Parameter {min(duplicates)!r} supplied more than once.") - unknown = kwargs.keys() - set(names) - if unknown: - raise TypeError(f"Unexpected distribution parameter: {min(unknown)}") - supplied.update(kwargs) - missing = tuple( - name for name in self._shape_names if supplied.get(name) is None - ) - if missing: - raise TypeError( - "Missing required distribution parameter(s): " + ", ".join(missing) - ) - ordered = {name: supplied.get(name) for name in names} + bound = self.__signature__.bind(*args, **kwargs) + bound.apply_defaults() super().__init__( self._base_module, - ordered, + bound.arguments, defaults={ "loc": ParameterDefault(0.0), "scale": ParameterDefault(1.0, PositiveTransform()), @@ -89,8 +68,6 @@ def wrap_scipy_distribution( wrapper_name = _name_from_scipy_dist(scipy_dist) namespace: dict[str, Any] = { "_base_module": scipy_dist, - "_shape_names": shape_names, - "_parameter_names": parameter_names, "__doc__": f"Structural wrapper for ``scipy.stats.{scipy_dist.name}``.", "__module__": __name__, "__signature__": signature, diff --git a/tests/test_scipy_wrappers.py b/tests/test_scipy_wrappers.py index 3968295..e0fa277 100644 --- a/tests/test_scipy_wrappers.py +++ b/tests/test_scipy_wrappers.py @@ -26,13 +26,13 @@ def test_factory_builds_named_classes_with_native_parameters_and_alias() -> None def test_generated_wrapper_requires_shape_and_keeps_scipy_parameterization() -> None: - with pytest.raises(TypeError, match="Missing required.*a"): + with pytest.raises(TypeError): Gamma() - with pytest.raises(TypeError, match="Unexpected distribution parameter"): + with pytest.raises(TypeError): Gamma(a=2.0, typo=1.0) - with pytest.raises(TypeError, match="supplied more than once"): + with pytest.raises(TypeError): Gamma(2.0, a=3.0) - with pytest.raises(TypeError, match="at most"): + with pytest.raises(TypeError): Gamma(2.0, 0.0, 1.0, 4.0) shape = Parameter(init=2.0) From 638158d69abc915fdc7ed74a78cb74b6aef3ee06 Mon Sep 17 00:00:00 2001 From: Jonathan Berthias Date: Sun, 27 Sep 2026 22:20:32 +0200 Subject: [PATCH 4/4] Move SciPy implementation out of distribution core --- pykelihood/distributions/core.py | 123 +----------------- pykelihood/distributions/scipy_adapter.py | 138 +++++++++++++++++++++ pykelihood/distributions/scipy_wrappers.py | 7 +- tests/test_distribution_core.py | 2 +- tests/test_scipy_wrappers.py | 2 +- 5 files changed, 143 insertions(+), 129 deletions(-) create mode 100644 pykelihood/distributions/scipy_adapter.py diff --git a/pykelihood/distributions/core.py b/pykelihood/distributions/core.py index c26e896..6280c16 100644 --- a/pykelihood/distributions/core.py +++ b/pykelihood/distributions/core.py @@ -4,31 +4,19 @@ from abc import ABC, abstractmethod from collections.abc import Iterator, Mapping -from dataclasses import dataclass -from types import MappingProxyType from typing import Union import numpy as np import numpy.typing as npt -from scipy.stats import rv_continuous -from pykelihood.expr import Constant, Expr, Node, PathElem +from pykelihood.expr import Expr, Node, PathElem from pykelihood.parameters import Parameter -from pykelihood.state import Transform ParameterInput = Union[Expr, npt.ArrayLike, None] ParameterState = Mapping[Parameter, npt.NDArray[np.float64]] RandomState = Union[int, np.random.Generator, np.random.RandomState, None] -@dataclass(frozen=True) -class ParameterDefault: - """Initial value and optional transform for an omitted parameter.""" - - value: npt.ArrayLike - transform: Transform | None = None - - class Distribution(Node, ABC): """A probability law whose parameter expressions form a graph node.""" @@ -73,112 +61,3 @@ def ppf( self, q: npt.ArrayLike, *, state: ParameterState | None = None ) -> npt.NDArray[np.float64]: raise NotImplementedError - - -class ScipyDistribution(Distribution): - """Continuous SciPy distribution evaluated from expression parameters.""" - - def __init__( - self, - scipy_distribution: rv_continuous, - parameters: Mapping[str, ParameterInput], - *, - defaults: Mapping[str, ParameterDefault] | None = None, - ) -> None: - shape_names = ( - () - if scipy_distribution.shapes is None - else tuple(name.strip() for name in scipy_distribution.shapes.split(",")) - ) - unknown = set(parameters) - set(shape_names) - {"loc", "scale"} - if unknown: - raise TypeError( - f"Unknown distribution parameters: {', '.join(sorted(unknown))}" - ) - for name in shape_names: - if parameters.get(name) is None: - raise TypeError(f"Missing required distribution parameter: {name}") - - parameter_defaults = {} if defaults is None else defaults - resolved: dict[str, Expr] = {} - for name, value in parameters.items(): - if value is None: - if name not in parameter_defaults: - raise TypeError(f"Missing required distribution parameter: {name}") - default = parameter_defaults[name] - resolved[name] = Parameter( - init=default.value, transform=default.transform, name=name - ) - elif isinstance(value, Expr): - resolved[name] = value - else: - resolved[name] = Constant(value) - - self._scipy_distribution = scipy_distribution - self._parameters = MappingProxyType(resolved) - - @property - def parameters(self) -> Mapping[str, Expr]: - return self._parameters - - def _evaluated_parameters( - self, state: ParameterState | None - ) -> dict[str, npt.NDArray[np.float64]]: - parameter_state = {} if state is None else state - return { - name: np.asarray(parameter.eval(parameter_state), dtype=np.float64) - for name, parameter in self.parameters.items() - } - - def rvs( - self, - size: int | tuple[int, ...] | None = None, - *, - state: ParameterState | None = None, - random_state: RandomState = None, - ) -> npt.NDArray[np.float64]: - parameters = self._evaluated_parameters(state) - batch_shape = np.broadcast_shapes( - *(value.shape for value in parameters.values()) - ) - sample_shape = ( - () if size is None else (size,) if isinstance(size, int) else size - ) - return np.asarray( - self._scipy_distribution.rvs( - **parameters, size=sample_shape + batch_shape, random_state=random_state - ), - dtype=np.float64, - ) - - def pdf( - self, x: npt.ArrayLike, *, state: ParameterState | None = None - ) -> npt.NDArray[np.float64]: - return np.asarray( - self._scipy_distribution.pdf(x, **self._evaluated_parameters(state)), - dtype=np.float64, - ) - - def logpdf( - self, x: npt.ArrayLike, *, state: ParameterState | None = None - ) -> npt.NDArray[np.float64]: - return np.asarray( - self._scipy_distribution.logpdf(x, **self._evaluated_parameters(state)), - dtype=np.float64, - ) - - def cdf( - self, x: npt.ArrayLike, *, state: ParameterState | None = None - ) -> npt.NDArray[np.float64]: - return np.asarray( - self._scipy_distribution.cdf(x, **self._evaluated_parameters(state)), - dtype=np.float64, - ) - - def ppf( - self, q: npt.ArrayLike, *, state: ParameterState | None = None - ) -> npt.NDArray[np.float64]: - return np.asarray( - self._scipy_distribution.ppf(q, **self._evaluated_parameters(state)), - dtype=np.float64, - ) diff --git a/pykelihood/distributions/scipy_adapter.py b/pykelihood/distributions/scipy_adapter.py new file mode 100644 index 0000000..3e53ba0 --- /dev/null +++ b/pykelihood/distributions/scipy_adapter.py @@ -0,0 +1,138 @@ +"""SciPy implementation of explicit-state continuous distributions.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from types import MappingProxyType + +import numpy as np +import numpy.typing as npt +from scipy.stats import rv_continuous + +from pykelihood.distributions.core import ( + Distribution, + ParameterInput, + ParameterState, + RandomState, +) +from pykelihood.expr import Constant, Expr +from pykelihood.parameters import Parameter +from pykelihood.state import Transform + + +@dataclass(frozen=True) +class ParameterDefault: + """Initial value and optional transform for an omitted parameter.""" + + value: npt.ArrayLike + transform: Transform | None = None + + +class ScipyDistribution(Distribution): + """Continuous SciPy distribution evaluated from expression parameters.""" + + def __init__( + self, + scipy_distribution: rv_continuous, + parameters: Mapping[str, ParameterInput], + *, + defaults: Mapping[str, ParameterDefault] | None = None, + ) -> None: + shape_names = ( + () + if scipy_distribution.shapes is None + else tuple(name.strip() for name in scipy_distribution.shapes.split(",")) + ) + unknown = set(parameters) - set(shape_names) - {"loc", "scale"} + if unknown: + raise TypeError( + f"Unknown distribution parameters: {', '.join(sorted(unknown))}" + ) + for name in shape_names: + if parameters.get(name) is None: + raise TypeError(f"Missing required distribution parameter: {name}") + + parameter_defaults = {} if defaults is None else defaults + resolved: dict[str, Expr] = {} + for name, value in parameters.items(): + if value is None: + if name not in parameter_defaults: + raise TypeError(f"Missing required distribution parameter: {name}") + default = parameter_defaults[name] + resolved[name] = Parameter( + init=default.value, transform=default.transform, name=name + ) + elif isinstance(value, Expr): + resolved[name] = value + else: + resolved[name] = Constant(value) + + self._scipy_distribution = scipy_distribution + self._parameters = MappingProxyType(resolved) + + @property + def parameters(self) -> Mapping[str, Expr]: + return self._parameters + + def _evaluated_parameters( + self, state: ParameterState | None + ) -> dict[str, npt.NDArray[np.float64]]: + parameter_state = {} if state is None else state + return { + name: np.asarray(parameter.eval(parameter_state), dtype=np.float64) + for name, parameter in self.parameters.items() + } + + def rvs( + self, + size: int | tuple[int, ...] | None = None, + *, + state: ParameterState | None = None, + random_state: RandomState = None, + ) -> npt.NDArray[np.float64]: + parameters = self._evaluated_parameters(state) + batch_shape = np.broadcast_shapes( + *(value.shape for value in parameters.values()) + ) + sample_shape = ( + () if size is None else (size,) if isinstance(size, int) else size + ) + return np.asarray( + self._scipy_distribution.rvs( + **parameters, size=sample_shape + batch_shape, random_state=random_state + ), + dtype=np.float64, + ) + + def pdf( + self, x: npt.ArrayLike, *, state: ParameterState | None = None + ) -> npt.NDArray[np.float64]: + return np.asarray( + self._scipy_distribution.pdf(x, **self._evaluated_parameters(state)), + dtype=np.float64, + ) + + def logpdf( + self, x: npt.ArrayLike, *, state: ParameterState | None = None + ) -> npt.NDArray[np.float64]: + return np.asarray( + self._scipy_distribution.logpdf(x, **self._evaluated_parameters(state)), + dtype=np.float64, + ) + + def cdf( + self, x: npt.ArrayLike, *, state: ParameterState | None = None + ) -> npt.NDArray[np.float64]: + return np.asarray( + self._scipy_distribution.cdf(x, **self._evaluated_parameters(state)), + dtype=np.float64, + ) + + def ppf( + self, q: npt.ArrayLike, *, state: ParameterState | None = None + ) -> npt.NDArray[np.float64]: + return np.asarray( + self._scipy_distribution.ppf(q, **self._evaluated_parameters(state)), + dtype=np.float64, + ) diff --git a/pykelihood/distributions/scipy_wrappers.py b/pykelihood/distributions/scipy_wrappers.py index 94c6322..0d6dda7 100644 --- a/pykelihood/distributions/scipy_wrappers.py +++ b/pykelihood/distributions/scipy_wrappers.py @@ -8,11 +8,8 @@ from scipy import stats from scipy.stats import rv_continuous -from pykelihood.distributions.core import ( - ParameterDefault, - ParameterInput, - ScipyDistribution, -) +from pykelihood.distributions.core import ParameterInput +from pykelihood.distributions.scipy_adapter import ParameterDefault, ScipyDistribution from pykelihood.state import PositiveTransform diff --git a/tests/test_distribution_core.py b/tests/test_distribution_core.py index 6be164c..2e606be 100644 --- a/tests/test_distribution_core.py +++ b/tests/test_distribution_core.py @@ -3,7 +3,7 @@ from numpy.testing import assert_allclose from scipy import stats -from pykelihood.distributions.core import ParameterDefault, ScipyDistribution +from pykelihood.distributions.scipy_adapter import ParameterDefault, ScipyDistribution from pykelihood.distributions.scipy_wrappers import Normal from pykelihood.effects import linear from pykelihood.expr import Constant diff --git a/tests/test_scipy_wrappers.py b/tests/test_scipy_wrappers.py index e0fa277..9507174 100644 --- a/tests/test_scipy_wrappers.py +++ b/tests/test_scipy_wrappers.py @@ -5,7 +5,7 @@ from numpy.testing import assert_allclose from scipy import stats -from pykelihood.distributions.core import ScipyDistribution +from pykelihood.distributions.scipy_adapter import ScipyDistribution from pykelihood.distributions.scipy_wrappers import Gamma, Genextreme, Norm, Normal from pykelihood.expr import Constant from pykelihood.likelihood import negative_log_likelihood