Skip to content

Commit b52e7c4

Browse files
committed
add log_prob methods to ConstantDelay and DoublePoissonDelay for vectorized log probability lookups.
1 parent 6a1df57 commit b52e7c4

3 files changed

Lines changed: 98 additions & 20 deletions

File tree

predicators/approaches/pp_param_learning_approach.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -333,11 +333,34 @@ def elbo_torch(
333333
for starts, gp in zip(start_times, ground_processes):
334334
if len(starts) > 0:
335335
s0 = starts[0]
336-
for t in range(s0 + 1, num_time_steps):
337-
delay_prob = gp.delay_distribution.probability(t - s0)
338-
if delay_prob > 1e-9:
339-
ll = ll + guide[gp][t] * torch.log(
340-
torch.tensor(delay_prob))
336+
# Relevant time steps for this gp's delay calculation: s0+1 to
337+
# num_time_steps-1
338+
# These correspond to delay values: 1 to num_time_steps-1-s0
339+
# Check if there are any time steps for delay
340+
if s0 + 1 < num_time_steps:
341+
# Create a tensor of delay values that occurred
342+
delay_values = torch.arange(1,
343+
num_time_steps - s0,
344+
dtype=torch.long)
345+
# Corresponding time indices for guide probabilities
346+
t_indices_for_guide = torch.arange(s0 + 1,
347+
num_time_steps,
348+
dtype=torch.long)
349+
350+
# Get log prob for all possible delay values at once
351+
all_delay_log_probs = gp.delay_distribution.log_prob(
352+
delay_values)
353+
354+
# Get the slice of guide prob relevant to these time steps
355+
guide_slice = guide[gp][t_indices_for_guide]
356+
357+
# Mask for valid log prob (not -inf)
358+
valid_mask = ~torch.isneginf(all_delay_log_probs)
359+
360+
# Add to ll only for terms where log probability is valid
361+
if valid_mask.any():
362+
ll = ll + torch.sum(guide_slice[valid_mask] *
363+
all_delay_log_probs[valid_mask])
341364

342365
# -----------------------------------------------------------------
343366
# 3. Entropy of the variational distributions

predicators/ground_truth_models/boil/processes.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from predicators.structs import CausalProcess, EndogenousProcess, \
99
ExogenousProcess, LiftedAtom, ParameterizedOption, Predicate, Type, \
1010
Variable
11-
from predicators.utils import CMPDelay, ConstantDelay, GaussianDelay, \
12-
null_sampler, DoublePoissonDelay
11+
from predicators.utils import CMPDelay, ConstantDelay, DoublePoissonDelay, \
12+
GaussianDelay, null_sampler
1313

1414

1515
class PyBulletBoilGroundTruthProcessFactory(GroundTruthProcessFactory):
@@ -413,7 +413,8 @@ def get_processes(
413413
elif CFG.boil_use_normal_delay:
414414
delay_distribution = GaussianDelay(mean=1, std=0.2, rng=rng)
415415
elif CFG.boil_use_cmp_delay:
416-
delay_distribution = CMPDelay(1, 1) # Assumed from other mean=1 cases
416+
delay_distribution = CMPDelay(
417+
1, 1) # Assumed from other mean=1 cases
417418
else:
418419
delay_distribution = DoublePoissonDelay(mu=1, phi=50)
419420
declare_complete_process = EndogenousProcess(
@@ -452,7 +453,8 @@ def get_processes(
452453
elif CFG.boil_use_cmp_delay:
453454
delay_distribution = CMPDelay(100, 2.9)
454455
else:
455-
delay_distribution = DoublePoissonDelay(mu=4, phi=50) # mu=4 from ConstantDelay(4)
456+
delay_distribution = DoublePoissonDelay(
457+
mu=4, phi=50) # mu=4 from ConstantDelay(4)
456458
fill_jug_process = ExogenousProcess("FillJug", parameters,
457459
condition_at_start,
458460
condition_overall, set(),
@@ -572,7 +574,8 @@ def get_processes(
572574
elif CFG.boil_use_normal_delay:
573575
delay_distribution = GaussianDelay(mean=3, std=0.2, rng=rng)
574576
elif CFG.boil_use_cmp_delay:
575-
delay_distribution = CMPDelay(55, 3) # Assumed from other mean=3 cases
577+
delay_distribution = CMPDelay(
578+
55, 3) # Assumed from other mean=3 cases
576579
else:
577580
delay_distribution = DoublePoissonDelay(mu=3, phi=50)
578581
human_happy_process = ExogenousProcess("HumanHappy", parameters,

predicators/utils.py

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4311,6 +4311,10 @@ class ConstantDelay(DelayDistribution):
43114311

43124312
def __init__(self, delay: int):
43134313
self.delay = delay
4314+
self._log_pmf_value = torch.tensor(0.0,
4315+
dtype=torch.get_default_dtype())
4316+
self._neg_inf = torch.tensor(float('-inf'),
4317+
dtype=torch.get_default_dtype())
43144318

43154319
def sample(self):
43164320
return self.delay
@@ -4326,9 +4330,24 @@ def set_parameters(self, parameters):
43264330
def probability(self, k: int) -> float:
43274331
return 1.0 if k == self.delay else 0.0
43284332

4333+
def log_prob(self, k: Union[int, torch.Tensor]) -> torch.Tensor:
4334+
"""Fast vectorized lookup of log P(delay = k)."""
4335+
if not isinstance(k, torch.Tensor):
4336+
k_tensor = torch.tensor(k, dtype=torch.long)
4337+
else:
4338+
k_tensor = k.long()
4339+
4340+
# Initialize with -inf
4341+
log_probs = torch.full_like(k_tensor,
4342+
self._neg_inf,
4343+
dtype=torch.get_default_dtype())
4344+
# Set log(1.0) = 0.0 where k == self.delay
4345+
log_probs[k_tensor == self.delay] = self._log_pmf_value
4346+
return log_probs
4347+
43294348
@cached_property
43304349
def _str(self) -> str:
4331-
return f"ConstantDelay({self.delay})"
4350+
return f"ConstantDelay({self.delay:.4f})"
43324351

43334352

43344353
class GaussianDelay(DelayDistribution):
@@ -4438,15 +4457,22 @@ def __init__(self, mu: float, phi: float, max_k: int = 50):
44384457
def _update_cache(self) -> None:
44394458
ks = torch.arange(self._max_k, dtype=torch.get_default_dtype())
44404459
log_fact = gammaln(ks + 1)
4441-
log_C = -0.5 * (torch.log(torch.tensor(2 * torch.pi)) +
4442-
torch.log(self.phi) + torch.log(self.mu))
4443-
log_p = (log_C + self.phi * (ks * torch.log(self.mu) - log_fact) -
4444-
self.phi * self.mu)
4445-
log_p -= torch.max(log_p)
4446-
pmf = torch.exp(log_p)
4447-
pmf /= pmf.sum()
4448-
self._pmf = pmf
4449-
self._cdf = torch.cumsum(pmf, dim=0)
4460+
log_C = -0.5 * (
4461+
torch.log(torch.tensor(2 * torch.pi, dtype=self.mu.dtype))
4462+
+ # Ensure tensor has same dtype
4463+
torch.log(self.phi) + torch.log(self.mu))
4464+
# Calculate unnormalized log probabilities
4465+
log_p_unnormalized = (log_C + self.phi *
4466+
(ks * torch.log(self.mu) - log_fact) -
4467+
self.phi * self.mu)
4468+
4469+
# Calculate normalized log PMF using logsumexp for numerical stability
4470+
log_normalization_constant = torch.logsumexp(log_p_unnormalized, dim=0)
4471+
self._log_pmf = log_p_unnormalized - log_normalization_constant
4472+
4473+
# Calculate PMF from the normalized log PMF
4474+
self._pmf = torch.exp(self._log_pmf)
4475+
self._cdf = torch.cumsum(self._pmf, dim=0)
44504476

44514477
def set_parameters(self, parameters: Sequence[float]) -> None:
44524478
"""Update μ and φ, then rebuild the cache."""
@@ -4465,6 +4491,32 @@ def probability(self, k: int) -> float:
44654491
return float(self._pmf[k])
44664492
return 0.0
44674493

4494+
def log_prob(self, k: Union[int, torch.Tensor]) -> torch.Tensor:
4495+
"""Fast vectorized lookup of log P(delay = k)."""
4496+
if not isinstance(k, torch.Tensor):
4497+
k_tensor = torch.tensor(
4498+
k, dtype=torch.long) # Use long for potential indexing
4499+
else:
4500+
k_tensor = k.long() # Ensure long type for indexing
4501+
4502+
original_shape = k_tensor.shape
4503+
k_flat = k_tensor.flatten()
4504+
4505+
# Initialize log_probs with -inf, matching the default dtype
4506+
log_probs_flat = torch.full_like(k_flat,
4507+
float('-inf'),
4508+
dtype=torch.get_default_dtype())
4509+
4510+
assert self._log_pmf is not None, "Cache not updated, _log_pmf is None."
4511+
4512+
valid_mask = (k_flat >= 0) & (k_flat < self._max_k)
4513+
valid_indices_in_k = k_flat[valid_mask]
4514+
4515+
if valid_indices_in_k.numel() > 0:
4516+
log_probs_flat[valid_mask] = self._log_pmf[valid_indices_in_k]
4517+
4518+
return log_probs_flat.reshape(original_shape)
4519+
44684520
def sample(self) -> int:
44694521
u = torch.rand(1).item()
44704522
return int(torch.searchsorted(self._cdf, torch.tensor(u)))

0 commit comments

Comments
 (0)