Skip to content

Commit fd1fa76

Browse files
authored
Add LogUniform distribution (#465)
Add an explicit LogUniform distribution class. The interpretation of the distribution parameter is different from the existing `Uniform(a, b, log=True)`. In PEtab v2, X ~ LogUniform(a, b) <=> ln(X) ~ Uniform(ln(a), ln(b)). However, in PEtab v1, a `parameterScaleUniform` prior for a parameterScale=log parameter is interpreted as ln(X) ~ Uniform(a, b).
1 parent a165108 commit fd1fa76

File tree

3 files changed

+69
-2
lines changed

3 files changed

+69
-2
lines changed

petab/v1/distributions.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"Normal",
3737
"Rayleigh",
3838
"Uniform",
39+
"LogUniform",
3940
]
4041

4142

@@ -382,6 +383,10 @@ class Uniform(Distribution):
382383
If ``False``, no transformation is applied.
383384
If a transformation is applied, the lower and upper bounds are the
384385
lower and upper bounds of the underlying uniform distribution.
386+
Note that this differs from the usual definition of a log-uniform
387+
distribution, where the logarithm of the variable is uniformly
388+
distributed between the logarithms of the bounds (see also
389+
:class:`LogUniform`).
385390
"""
386391

387392
def __init__(
@@ -411,6 +416,45 @@ def _ppf_untransformed_untruncated(self, q) -> np.ndarray | float:
411416
return uniform.ppf(q, loc=self._low, scale=self._high - self._low)
412417

413418

419+
class LogUniform(Distribution):
420+
"""A log-uniform or reciprocal distribution.
421+
422+
A random variable is log-uniformly distributed between ``low`` and ``high``
423+
if its logarithm is uniformly distributed between ``log(low)`` and
424+
``log(high)``.
425+
426+
:param low: The lower bound of the distribution.
427+
:param high: The upper bound of the distribution.
428+
:param trunc: The truncation limits of the distribution.
429+
"""
430+
431+
def __init__(
432+
self,
433+
low: float,
434+
high: float,
435+
trunc: tuple[float, float] | None = None,
436+
):
437+
self._logbase = np.exp(1)
438+
self._low = self._log(low)
439+
self._high = self._log(high)
440+
super().__init__(log=self._logbase, trunc=trunc)
441+
442+
def __repr__(self):
443+
return self._repr({"low": self._low, "high": self._high})
444+
445+
def _sample(self, shape=None) -> np.ndarray | float:
446+
return np.random.uniform(low=self._low, high=self._high, size=shape)
447+
448+
def _pdf_untransformed_untruncated(self, x) -> np.ndarray | float:
449+
return uniform.pdf(x, loc=self._low, scale=self._high - self._low)
450+
451+
def _cdf_untransformed_untruncated(self, x) -> np.ndarray | float:
452+
return uniform.cdf(x, loc=self._low, scale=self._high - self._low)
453+
454+
def _ppf_untransformed_untruncated(self, q) -> np.ndarray | float:
455+
return uniform.ppf(q, loc=self._low, scale=self._high - self._low)
456+
457+
414458
class Laplace(Distribution):
415459
"""A (log-)Laplace distribution.
416460

petab/v2/core.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ class PriorDistribution(str, Enum):
201201
PriorDistribution.LAPLACE: Laplace,
202202
PriorDistribution.LOG_LAPLACE: Laplace,
203203
PriorDistribution.LOG_NORMAL: Normal,
204-
PriorDistribution.LOG_UNIFORM: Uniform,
204+
PriorDistribution.LOG_UNIFORM: LogUniform,
205205
PriorDistribution.NORMAL: Normal,
206206
PriorDistribution.RAYLEIGH: Rayleigh,
207207
PriorDistribution.UNIFORM: Uniform,
@@ -1060,7 +1060,12 @@ def prior_dist(self) -> Distribution:
10601060
# `Uniform.__init__` does not accept the `trunc` parameter
10611061
low = max(self.prior_parameters[0], self.lb)
10621062
high = min(self.prior_parameters[1], self.ub)
1063-
return cls(low, high, log=log)
1063+
return cls(low, high)
1064+
1065+
if cls == LogUniform:
1066+
# Mind the different interpretation of distribution parameters for
1067+
# Uniform(..., log=True) and LogUniform!!
1068+
return cls(*self.prior_parameters, trunc=[self.lb, self.ub])
10641069

10651070
return cls(*self.prior_parameters, log=log, trunc=[self.lb, self.ub])
10661071

tests/v1/test_distributions.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sys
2+
from math import exp
23

34
import numpy as np
45
import pytest
@@ -115,3 +116,20 @@ def cdf(x):
115116
assert_allclose(
116117
distribution.pdf(sample), reference_pdf, rtol=1e-10, atol=1e-14
117118
)
119+
120+
121+
def test_log_uniform():
122+
"""Test Uniform(a, b, log=True) vs LogUniform(a, b)."""
123+
# support between exp(1) and exp(2)
124+
dist = Uniform(1, 2, log=True)
125+
assert dist.pdf(exp(0)) == 0
126+
assert dist.pdf(exp(1)) > 0
127+
assert dist.pdf(exp(2)) > 0
128+
assert dist.pdf(exp(3)) == 0
129+
130+
# support between 1 and 2
131+
dist = LogUniform(1, 2)
132+
assert dist.pdf(0) == 0
133+
assert dist.pdf(1) > 0
134+
assert dist.pdf(2) > 0
135+
assert dist.pdf(3) == 0

0 commit comments

Comments
 (0)