Skip to content

Commit aaaf66c

Browse files
authored
Add Hypothesis property-based tests for core tHMM invariants (#1010)
* Add Hypothesis property-based tests for core tHMM invariants Covers transition-matrix stochasticity, CellVar.divide, the stationary distribution, weighted Bernoulli/Gamma emission estimators, LineageTree generation, and Viterbi/log-score consistency. One test is a pinned, strict xfail documenting a real bug it found: StateDistribution.logpdf can raise FloatingPointError (NaN from +inf/-inf) when an unobserved (-1) Bernoulli sentinel coincides with an uncensored, exactly-zero Gamma observation under a shape parameter < 1. * Fix FloatingPointError crash in StateDistribution.logpdf Exclude sentinel/negative Bernoulli and Gamma observations from the Gamma and Bernoulli log-likelihood terms up front, instead of only zeroing them out afterwards. Previously an unobserved (-1) Bernoulli value on a row with an uncensored, exactly-zero Gamma observation and a shape parameter < 1 (where the Gamma density diverges to +inf at x=0) could produce a (+inf) + (-inf) = NaN, which crashes under np.seterr(all="raise"). Un-xfails test_gamma_logpdf_never_produces_nan now that the bug it documented is fixed.
1 parent 9fff0e5 commit aaaf66c

4 files changed

Lines changed: 496 additions & 3 deletions

File tree

‎lineage/states/StateDistributionGamma.py‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,27 @@ def logpdf(self, x: np.ndarray) -> np.ndarray:
6262
"""
6363
ll = np.zeros(x.shape[0])
6464

65+
# Negative Bernoulli/Gamma observations are sentinels for "unobserved"
66+
# and must not contribute to the log-likelihood. Excluding them up
67+
# front (rather than only zeroing them out afterwards) avoids ever
68+
# combining a diverging Gamma density (e.g. +inf at x=0 for shape < 1)
69+
# with a -inf sentinel Bernoulli log-likelihood into a NaN.
70+
valid = (x[:, 0] >= 0) & (x[:, 1] >= 0)
71+
6572
# Update uncensored Gamma
66-
ll[x[:, 2] == 1] += sp.gamma.logpdf(x[x[:, 2] == 1, 1], a=self.params[1], scale=self.params[2])
73+
uncensored = valid & (x[:, 2] == 1)
74+
ll[uncensored] += sp.gamma.logpdf(x[uncensored, 1], a=self.params[1], scale=self.params[2])
6775

6876
# Update censored Gamma
69-
ll[x[:, 2] == 0] += sp.gamma.logsf(x[x[:, 2] == 0, 1], a=self.params[1], scale=self.params[2])
77+
censored = valid & (x[:, 2] == 0)
78+
ll[censored] += sp.gamma.logsf(x[censored, 1], a=self.params[1], scale=self.params[2])
7079

7180
# Remove dead cells
7281
ll[x[:, 0] == 0] = 0.0
7382

7483
# Update for observed Bernoulli
75-
ll[np.isfinite(x[:, 0])] += sp.bernoulli.logpmf(x[np.isfinite(x[:, 0]), 0], self.params[0])
84+
observed_bern = valid & np.isfinite(x[:, 0])
85+
ll[observed_bern] += sp.bernoulli.logpmf(x[observed_bern, 0], self.params[0])
7686

7787
# Log likelihood of negative values should be zero
7888
ll[x[:, 1] < 0] = 0.0

0 commit comments

Comments
 (0)