Skip to content

Commit 10306f9

Browse files
committed
v2: Additional probability distributions
Add additional probability distributions as required for PEtab-dev/PEtab#595. See #374.
1 parent 87cec8c commit 10306f9

File tree

1 file changed

+199
-16
lines changed

1 file changed

+199
-16
lines changed

petab/v1/distributions.py

Lines changed: 199 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,19 @@
33
from __future__ import annotations
44

55
import abc
6+
from typing import Any
67

78
import numpy as np
8-
from scipy.stats import laplace, norm, uniform
9+
from scipy.stats import (
10+
cauchy,
11+
chi2,
12+
expon,
13+
gamma,
14+
laplace,
15+
norm,
16+
rayleigh,
17+
uniform,
18+
)
919

1020
__all__ = [
1121
"Distribution",
@@ -277,6 +287,21 @@ def _inverse_transform_sample(self, shape) -> np.ndarray | float:
277287
)
278288
return self._ppf_transformed_untruncated(uniform_sample)
279289

290+
def _repr(self, pars: dict[str, Any] = None) -> str:
291+
"""Return a string representation of the distribution."""
292+
pars = ", ".join(f"{k}={v}" for k, v in pars.items()) if pars else ""
293+
294+
if self._logbase is False:
295+
log = ""
296+
elif self._logbase == np.exp(1):
297+
log = ", log=True"
298+
else:
299+
log = f", log={self._logbase}"
300+
301+
trunc = f", trunc={self._trunc}" if self._trunc else ""
302+
303+
return f"{self.__class__.__name__}({pars}{log}{trunc})"
304+
280305

281306
class Normal(Distribution):
282307
"""A (log-)normal distribution.
@@ -307,16 +332,7 @@ def __init__(
307332
super().__init__(log=log, trunc=trunc)
308333

309334
def __repr__(self):
310-
if self._logbase is False:
311-
log = ""
312-
if self._logbase == np.exp(1):
313-
log = ", log=True"
314-
else:
315-
log = f", log={self._logbase}"
316-
317-
trunc = f", trunc={self._trunc}" if self._trunc else ""
318-
319-
return f"Normal(loc={self._loc}, scale={self._scale}{log}{trunc})"
335+
return self._repr({"loc": self._loc, "scale": self._scale})
320336

321337
def _sample(self, shape=None) -> np.ndarray | float:
322338
return np.random.normal(loc=self._loc, scale=self._scale, size=shape)
@@ -366,8 +382,7 @@ def __init__(
366382
super().__init__(log=log)
367383

368384
def __repr__(self):
369-
log = f", log={self._logbase}" if self._logbase else ""
370-
return f"Uniform(low={self._low}, high={self._high}{log})"
385+
return self._repr({"low": self._low, "high": self._high})
371386

372387
def _sample(self, shape=None) -> np.ndarray | float:
373388
return np.random.uniform(low=self._low, high=self._high, size=shape)
@@ -411,9 +426,7 @@ def __init__(
411426
super().__init__(log=log, trunc=trunc)
412427

413428
def __repr__(self):
414-
trunc = f", trunc={self._trunc}" if self._trunc else ""
415-
log = f", log={self._logbase}" if self._logbase else ""
416-
return f"Laplace(loc={self._loc}, scale={self._scale}{trunc}{log})"
429+
return self._repr({"loc": self._loc, "scale": self._scale})
417430

418431
def _sample(self, shape=None) -> np.ndarray | float:
419432
return np.random.laplace(loc=self._loc, scale=self._scale, size=shape)
@@ -436,3 +449,173 @@ def loc(self) -> float:
436449
def scale(self) -> float:
437450
"""The scale parameter of the underlying distribution."""
438451
return self._scale
452+
453+
454+
class Cauchy(Distribution):
455+
"""A Cauchy distribution."""
456+
457+
def __init__(
458+
self,
459+
loc: float,
460+
scale: float,
461+
trunc: tuple[float, float] | None = None,
462+
log: bool | float = False,
463+
):
464+
self._loc = loc
465+
self._scale = scale
466+
super().__init__(log=log, trunc=trunc)
467+
468+
def __repr__(self):
469+
return self._repr({"loc": self._loc, "scale": self._scale})
470+
471+
def _pdf_untransformed_untruncated(self, x) -> np.ndarray | float:
472+
return cauchy.pdf(x, loc=self._loc, scale=self._scale)
473+
474+
def _cdf_untransformed_untruncated(self, x) -> np.ndarray | float:
475+
return cauchy.cdf(x, loc=self._loc, scale=self._scale)
476+
477+
def _ppf_untransformed_untruncated(self, q) -> np.ndarray | float:
478+
return cauchy.ppf(q, loc=self._loc, scale=self._scale)
479+
480+
@property
481+
def loc(self) -> float:
482+
"""The location parameter of the underlying distribution."""
483+
return self._loc
484+
485+
@property
486+
def scale(self) -> float:
487+
"""The scale parameter of the underlying distribution."""
488+
return self._scale
489+
490+
491+
class ChiSquare(Distribution):
492+
"""A chi-squared distribution.
493+
494+
:param dof: Degrees of freedom.
495+
"""
496+
497+
def __init__(
498+
self,
499+
dof: int,
500+
trunc: tuple[float, float] | None = None,
501+
):
502+
if not dof.is_integer() or dof < 1:
503+
raise ValueError(
504+
f"`dof' must be a positive integer, but was `{dof}'."
505+
)
506+
507+
self._dof = dof
508+
super().__init__(log=False, trunc=trunc)
509+
510+
def __repr__(self):
511+
return self._repr({"dof": self._dof})
512+
513+
def _pdf_untransformed_untruncated(self, x) -> np.ndarray | float:
514+
return chi2.pdf(x, df=self._dof)
515+
516+
def _cdf_untransformed_untruncated(self, x) -> np.ndarray | float:
517+
return chi2.cdf(x, df=self._dof)
518+
519+
def _ppf_untransformed_untruncated(self, q) -> np.ndarray | float:
520+
return chi2.ppf(q, df=self._dof)
521+
522+
@property
523+
def dof(self) -> int:
524+
"""The degrees of freedom parameter."""
525+
return self._dof
526+
527+
528+
class Exponential(Distribution):
529+
"""
530+
An exponential distribution.
531+
"""
532+
533+
def __init__(
534+
self,
535+
scale: float,
536+
trunc: tuple[float, float] | None = None,
537+
):
538+
self._scale = scale
539+
super().__init__(log=False, trunc=trunc)
540+
541+
def __repr__(self):
542+
return self._repr({"scale": self._scale})
543+
544+
def _pdf_untransformed_untruncated(self, x) -> np.ndarray | float:
545+
return expon.pdf(x, scale=self._scale)
546+
547+
def _cdf_untransformed_untruncated(self, x) -> np.ndarray | float:
548+
return expon.cdf(x, scale=self._scale)
549+
550+
def _ppf_untransformed_untruncated(self, q) -> np.ndarray | float:
551+
return expon.ppf(q, scale=self._scale)
552+
553+
@property
554+
def scale(self) -> float:
555+
"""The scale parameter of the underlying distribution."""
556+
return self._scale
557+
558+
559+
class Gamma(Distribution):
560+
"""A gamma distribution."""
561+
562+
def __init__(
563+
self,
564+
shape: float,
565+
scale: float,
566+
trunc: tuple[float, float] | None = None,
567+
):
568+
self._shape = shape
569+
self._scale = scale
570+
super().__init__(log=False, trunc=trunc)
571+
572+
def __repr__(self):
573+
return self._repr({"shape": self._shape, "scale": self._scale})
574+
575+
def _pdf_untransformed_untruncated(self, x) -> np.ndarray | float:
576+
return gamma.pdf(x, a=self._shape, scale=self._scale)
577+
578+
def _cdf_untransformed_untruncated(self, x) -> np.ndarray | float:
579+
return gamma.cdf(x, a=self._shape, scale=self._scale)
580+
581+
def _ppf_untransformed_untruncated(self, q) -> np.ndarray | float:
582+
return gamma.ppf(q, a=self._shape, scale=self._scale)
583+
584+
@property
585+
def shape(self) -> float:
586+
"""The shape parameter of the underlying distribution."""
587+
return self._shape
588+
589+
@property
590+
def scale(self) -> float:
591+
"""The scale parameter of the underlying distribution."""
592+
return self._scale
593+
594+
595+
class Rayleigh(Distribution):
596+
"""A Rayleigh distribution."""
597+
598+
def __init__(
599+
self,
600+
scale: float,
601+
trunc: tuple[float, float] | None = None,
602+
):
603+
self._scale = scale
604+
super().__init__(log=False, trunc=trunc)
605+
606+
def __repr__(self):
607+
return self._repr({"scale": self._scale})
608+
609+
def _pdf_untransformed_untruncated(self, x) -> np.ndarray | float:
610+
return rayleigh.pdf(x, scale=self._scale)
611+
612+
def _cdf_untransformed_untruncated(self, x) -> np.ndarray | float:
613+
return rayleigh.cdf(x, scale=self._scale)
614+
615+
def _ppf_untransformed_untruncated(self, q) -> np.ndarray | float:
616+
return rayleigh.ppf(q, scale=self._scale)
617+
618+
@property
619+
def scale(self) -> float:
620+
"""The scale parameter of the underlying distribution."""
621+
return self._scale

0 commit comments

Comments
 (0)