@@ -4316,6 +4316,11 @@ def sample(self):
43164316
43174317 def set_parameters (self , parameters ):
43184318 self .delay = parameters [0 ]
4319+ # Invalidate cached properties
4320+ if '_str' in self .__dict__ :
4321+ del self .__dict__ ['_str' ]
4322+ if '_hash' in self .__dict__ :
4323+ del self .__dict__ ['_hash' ]
43194324
43204325 def probability (self , k : int ) -> float :
43214326 return 1.0 if k == self .delay else 0.0
@@ -4341,6 +4346,11 @@ def sample(self):
43414346 def set_parameters (self , parameters ):
43424347 self .mean = parameters [0 ]
43434348 self .std = parameters [1 ]
4349+ # Invalidate cached properties
4350+ if '_str' in self .__dict__ :
4351+ del self .__dict__ ['_str' ]
4352+ if '_hash' in self .__dict__ :
4353+ del self .__dict__ ['_hash' ]
43444354
43454355 def probability (self , k : int ) -> float :
43464356 return norm .pdf (k , self .mean , self .std )
@@ -4353,14 +4363,14 @@ def _str(self) -> str:
43534363class CMPDelay (DelayDistribution ):
43544364 """Conway-Maxwell-Poisson (CMP) distribution for delays."""
43554365
4356- def __init__ (self , lam : float , nu : float , rng : np .random .Generator ):
4366+ def __init__ (self , lam : float , nu : float , rng : np .random .Generator ) -> None :
43574367 self .lam = lam
43584368 self .nu = nu
43594369 self .rng = rng
43604370 self ._max_k = 100
43614371 self ._update_cache ()
43624372
4363- def _update_cache (self ):
4373+ def _update_cache (self ) -> None :
43644374 """Precompute and cache PMF and CDF."""
43654375 # Calculate log factorial values once
43664376 log_factorials = np .array ([gammaln (n + 1 ) for n in range (self ._max_k )])
@@ -4381,10 +4391,15 @@ def _update_cache(self):
43814391 self ._pmf = masses / np .sum (masses )
43824392 self ._cdf = np .cumsum (self ._pmf )
43834393
4384- def set_parameters (self , parameters ) :
4394+ def set_parameters (self , parameters : Sequence [ float ]) -> None :
43854395 self .lam = parameters [0 ]
43864396 self .nu = parameters [1 ]
43874397 self ._update_cache ()
4398+ # Invalidate cached properties
4399+ if '_str' in self .__dict__ :
4400+ del self .__dict__ ['_str' ]
4401+ if '_hash' in self .__dict__ :
4402+ del self .__dict__ ['_hash' ]
43884403
43894404 def probability (self , k : int ) -> float :
43904405 """Return the probability of delay k."""
@@ -4396,12 +4411,85 @@ def probability(self, k: int) -> float:
43964411 def _str (self ) -> str :
43974412 return f"CMPDelay({ self .lam } , { self .nu } )"
43984413
4399- def sample (self ):
4414+ def sample (self ) -> None :
44004415 """Sample from the CMP distribution using cached CDF."""
44014416 u = self .rng .random ()
44024417 idx = np .searchsorted (self ._cdf , u )
44034418 return idx
44044419
4420+ class DoublePoissonDelay (DelayDistribution ):
4421+ """
4422+ Double-Poisson distribution for discrete delays.
4423+
4424+ Parameters
4425+ ----------
4426+ mu : float
4427+ Mean-like parameter ( > 0 ).
4428+ phi : float
4429+ Dispersion parameter ( > 0 ).
4430+ • phi < 1 → over-dispersion
4431+ • phi = 1 → Poisson
4432+ • phi > 1 → under-dispersion
4433+ rng : np.random.Generator
4434+ Numpy random generator used by `sample()`.
4435+ max_k : int, optional
4436+ Cache PMF/CDF up to this value (default 100).
4437+ Increase if your delays can be very large.
4438+ """
4439+
4440+ def __init__ (self ,
4441+ mu : float ,
4442+ phi : float ,
4443+ rng : np .random .Generator ,
4444+ max_k : int = 50 ):
4445+ self .mu = mu
4446+ self .phi = phi
4447+ self .rng = rng
4448+ self ._max_k = max_k
4449+ self ._update_cache ()
4450+
4451+ # ------------------------------------------------------------------ #
4452+ # Internals
4453+ # ------------------------------------------------------------------ #
4454+ def _update_cache (self ) -> None :
4455+ """Vectorised pre-computation of PMF and CDF."""
4456+ ks = np .arange (self ._max_k ) # 0, 1, …, max_k-1
4457+ log_fact = gammaln (ks + 1 ) # log(k!)
4458+ # Saddle-point normaliser C(μ, φ) ≈ (2π φ μ)^(-1/2)
4459+ log_C = - 0.5 * (np .log (2 * np .pi ) + np .log (self .phi ) + np .log (self .mu ))
4460+
4461+ # log P(Y=k)
4462+ log_p = (log_C
4463+ + self .phi * (ks * np .log (self .mu ) - log_fact )
4464+ - self .phi * self .mu )
4465+
4466+ # Stabilise → exponentiate → renormalise
4467+ log_p -= log_p .max () # avoid overflow
4468+ pmf = np .exp (log_p )
4469+ pmf /= pmf .sum ()
4470+
4471+ self ._pmf = pmf
4472+ self ._cdf = np .cumsum (pmf )
4473+
4474+ def set_parameters (self , parameters : Sequence [float ]) -> None :
4475+ """Update μ and φ, then rebuild the cache."""
4476+ self .mu , self .phi = parameters
4477+ self ._update_cache ()
4478+
4479+ def probability (self , k : int ) -> float :
4480+ """Fast O(1) lookup of P(delay = k)."""
4481+ if 0 <= k < self ._max_k :
4482+ return float (self ._pmf [k ])
4483+ return 0.0
4484+
4485+ def sample (self ) -> int :
4486+ """Inverse-CDF sampling using cached CDF (O(log max_k))."""
4487+ u = self .rng .random ()
4488+ return int (np .searchsorted (self ._cdf , u ))
4489+
4490+ @cached_property
4491+ def _str (self ) -> str :
4492+ return f"DoublePoissonDelay({ self .mu } , { self .phi } )"
44054493
44064494@functools .lru_cache (maxsize = None )
44074495def get_git_commit_hash () -> str :
0 commit comments