Skip to content

Add Hypothesis property-based tests for core tHMM invariants - #1010

Merged
aarmey merged 4 commits into
masterfrom
worktree-wobbly-riding-origami
Sep 12, 2026
Merged

aarmey merged 4 commits into
masterfrom
worktree-wobbly-riding-origami

Conversation

@aarmey

@aarmey aarmey commented Sep 12, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds hypothesis as a dev dependency and a new lineage/tests/test_properties.py with 13 property-based tests covering invariants of the tHMM machinery that the existing example-based tests don't exercise across a range of inputs:

    • CellVar.divide: daughter states are always valid, parent/gen bookkeeping is correct, and results are reproducible for a given RNG seed, for any row-stochastic transition matrix.
    • BaumWelch.calculate_stationary: the returned vector is always a valid probability distribution and a genuine fixed point (w @ T == w) of any stochastic T.
    • stateCommon.bern_estimator: the weighted Bernoulli MAP estimate always stays in [0, 1].
    • StateDistributionGamma.StateDistribution.logpdf/dist: negative "unobserved" sentinel rows always contribute exactly zero log-likelihood, and a distribution's distance to itself is zero.
    • LineageTree.rand_init: for any (pi, T) and requested size, the produced tree is a valid full binary tree (every non-leaf has exactly 2 children) with valid state indices, and simulation is reproducible for a given seed.
    • Viterbi/tHMM.log_score: decoded state sequences are always valid and always score at least as well as an arbitrary random assignment of the same lineage.
  • One test (test_gamma_logpdf_never_produces_nan) is a strict, pinned xfail that documents a real bug Hypothesis found: StateDistribution.logpdf can raise FloatingPointError instead of returning a finite log-likelihood. This happens because lineage/__init__.py sets np.seterr(all="raise"), and an "unobserved" Bernoulli sentinel (-1) on a row with an uncensored, exactly-zero Gamma observation under a shape parameter < 1 (where the Gamma density diverges to +inf at x=0) produces a (+inf) + (-inf) = NaN inside logpdf, which the current code only zeroes out after the NaN has already been created. This is left as a documented xfail rather than fixed here, since correcting the ordering of the masking logic touches numerically sensitive code that's out of scope for a test-only PR.

Test plan

  • uv run pytest lineage/tests/test_properties.py -v — 12 passed, 1 xfailed (repeated 3x to confirm no flakiness)
  • uv run pytest lineage/tests -q — full existing suite still green: 66 passed, 1 xfailed
  • uv run ruff check / ruff format --check on the new file
  • uv run ty check lineage/tests/test_properties.py

aarmey and others added 4 commits September 12, 2026 08:03
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.
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.
@aarmey
aarmey merged commit aaaf66c into master Sep 12, 2026
1 of 2 checks passed
@aarmey
aarmey deleted the worktree-wobbly-riding-origami branch September 12, 2026 15:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant