Skip to content

Commit a6bd112

Browse files
committed
Invalidate cached properties in delay distribution classes upon parameter updates and add a new DoublePoissonDelay class for discrete delays. Update method signatures for clarity.
1 parent ac52394 commit a6bd112

4 files changed

Lines changed: 105 additions & 7 deletions

File tree

predicators/approaches/pp_param_learning_approach.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
from collections import defaultdict
33
from typing import Dict, List, Optional, Sequence, Set, Tuple
4+
from pprint import pformat
45

56
import numpy as np
67
from gym.spaces import Box
@@ -142,9 +143,9 @@ def _learn_process_parameters(self,
142143
progress_bar = tqdm(desc="Optim. params.", unit="iter")
143144

144145
# 2. Define objective and optimize
145-
def objective(params):
146+
def objective(params) -> float:
146147
"""Objective function for scipy.optimize.minimize to minimize.
147-
148+
ˍ
148149
It does some preparation and then calls the -ELBO function.
149150
"""
150151
nonlocal best_elbo
@@ -191,6 +192,10 @@ def objective(params):
191192
},
192193
method="L-BFGS-B") # terminate in 19464iter
193194
progress_bar.close()
195+
# Display the learned processes
196+
logging.debug("Learned processes:")
197+
for proc in self._processes:
198+
logging.debug(pformat(proc))
194199
logging.info(f"Best likelihood bound: {-result.fun}")
195200
breakpoint()
196201

predicators/structs.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2460,6 +2460,11 @@ def filter_predicates(self, kept: Collection[Predicate]) -> CausalProcess:
24602460
def _set_parameters(self, parameters: Sequence[float]) -> None:
24612461
self.strength = parameters[0]
24622462
self.delay_distribution.set_parameters(parameters[1:])
2463+
# Invalidate cached properties
2464+
if '_str' in self.__dict__:
2465+
del self.__dict__['_str']
2466+
if '_hash' in self.__dict__:
2467+
del self.__dict__['_hash']
24632468

24642469
def delay_probability(self, delay: int) -> float:
24652470
return self.delay_distribution.probability(delay)

predicators/utils.py

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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:
43534363
class 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)
44074495
def get_git_commit_hash() -> str:

scripts/configs/mara_bench.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ APPROACHES:
1414
ARGS:
1515
- "load_data"
1616
FLAGS:
17-
boil_use_constant_delay: True
17+
# boil_use_constant_delay: True
1818
demonstrator: "oracle_process_planning"
1919
# This following is needed to generate a successful demo trajectory, or
2020
# else the demo collection will fail with an option plan exhausted error.

0 commit comments

Comments
 (0)