Source code for cosapp.utils.distributions.triangular

"""Class defining a triangular distribution."""
import numbers
from typing import Any, Dict, Optional

import numpy
import scipy.stats
import scipy.optimize

from .distribution import Distribution


[docs]class Triangular(Distribution): """A class to define a triangular distribution. Parameters ---------- worst : float The parameter absolute worst value. likely : float The parameter absolute likely value (i.e. corresponding to the peak probability). best : float The parameter absolute best value. pworst : float, optional The worst value probability is the probability that the variable will be lower (if worst<best) or higher (if worst>best) than the worst value; default 0.15 (i.e. 15%). pbest : float, optional The best value probability is the probability that the variable will be higher (if worst<best) or lower (if worst>best) than the best value; default 0.15 (i.e. 15%). """ def __init__( self, worst: float, likely: float, best: float, pworst: Optional[float] = 0.15, pbest: Optional[float] = 0.15, ): self._rv = None # type: scipy.stats.triang # Dummy init self._likely = 0.5 * (best + worst) # type: numbers.Number super().__init__(worst, best, pworst, pbest) self.likely = likely # Trigger likely validation def __json__(self) -> Dict[str, Any]: """Serialize the distribution object. Returns ------- Dict[str, Any] JSONable dictionary describing the distribution. """ base = super().__json__() base.update({"likely": self.likely}) return base @property def likely(self) -> float: """float : The parameter absolute likely value It corresponds to the peak probability. """ return self._likely @likely.setter def likely(self, value: float): params = self._rv.kwds lower = params["loc"] upper = lower + params["scale"] if not (lower <= value <= upper): raise ValueError( f"Likely value not within distribution bounds: {lower} <= {value} <= {upper}." ) self._likely = value self._set_distribution() def _set_distribution(self) -> None: """Set the probability distribution according the parameters.""" if self.pworst + self.pbest > 1.0: raise ValueError( f"Best and worst probabilities are incompatible: {self.__json__()!s}." ) pts = [self.worst, self.best] if self.worst > self.best: ppts = [1 - self.pworst, self.pbest] else: ppts = [self.pworst, 1 - self.pbest] if self._rv is None: x0 = [min(0, self.likely), 2 * abs(self.likely)] else: params = self._rv.kwds # return {"c": #, "loc": #, "scale": #} x0 = [params["loc"], params["scale"]] def make_triang(x): # likely = loc + c * scale if any(numpy.isnan(x)): raise ValueError(f"invalid distribution parameters {x}") c = (self.likely - x[0]) / x[1] return scipy.stats.triang(c=c, loc=x[0], scale=x[1]) def f(x): t = make_triang(x) return t.ppf(ppts) - pts res = scipy.optimize.root(f, x0) if not res.success or any(numpy.isnan(res.x)): raise ValueError( f"Unable to fit triangular distribution on {self.__json__()!s}." ) self._rv = make_triang(res.x)
[docs] def draw(self, quantile: Optional[float] = None) -> float: """Generate a random number. If a quantile is given, generate the perturbation for that quantile. Parameters ---------- quantile : Optional[float], optional Quantile for which the perturbation must be set; default None (i.e. random perturbation) Returns ------- float The random number """ return self._rv.rvs() if quantile is None else self._rv.ppf(quantile)