Skip to content

Handle zero total rates in capacity methods, and document capacity in the quickstart - #22

Merged
wsdewitt merged 3 commits into
mainfrom
capacity-zero-rates
Oct 18, 2026
Merged

wsdewitt merged 3 commits into
mainfrom
capacity-zero-rates

Conversation

@wsdewitt

@wsdewitt wsdewitt commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #21.

Summary

  • Zero total rates no longer crash. With capacity_method="birth" or "death", evolve divides by the active nodes' total birth or death rate, and Process.waiting_time_rv divides by the rate multiplier. Either can be zero, which raised ZeroDivisionError:
    • evolve now updates a rate multiplier only when the total it divides by is positive. When that total is zero, every active node has rate zero for that event, so the multiplier doesn't matter.
    • waiting_time_rv treats a zero rate multiplier as an infinite waiting time. It still draws from the random number generator, so the random stream is used the same way as before.
  • Tests for every capacity method, which had none.
  • "stop" is added to the type annotation of capacity_method, which left it out.
  • A "Population size limits" section in the quickstart. It explains capacity and each capacity_method, shows the default raising an error when the population exceeds the capacity, and plots the population size over time for "stop", "hard", "birth", and "death". The population sizes come from Tree.slice. The quickstart now imports matplotlib, so its first code cell is tagged nbval-ignore-output, like the other notebooks.

Regression

This was partly a regression in 1.0.0. In 0.6.2, zero totals caused a numpy division by zero when they came from a ConstantProcess, which gave inf with a warning, so the simulation happened to continue. Since 1.0.0, ConstantProcess returns a Python float, so those cases raised. Totals from a DiscreteProcess were Python floats in 0.6.2 too, so those cases also raised before 1.0.0.

Results don't change

I ran each capacity method on 0.6.2 and on this branch, for 10 seeds each, with Python 3.12. The trees are identical for all seven configurations: None (capacity exceeded), "stop", "hard", "birth", "death", and "birth" and "death" with a zero death rate. The cell-cycle model from #21 now runs with either kind of death process, without warnings. The hash over about 140 other seeded simulations is unchanged.

Tests

TestCapacity adds six tests:

  • test_exceeded: with no capacity method, exceeding the capacity raises TreeError and removes the partial simulation.
  • test_stop: "stop" ends early, with exactly capacity survivors at a single end time.
  • test_hard: "hard" ends with at most capacity survivors.
  • test_birth_and_death: "birth" and "death" keep the population between half and twice the capacity.
  • test_zero_total_rate: the cell-cycle model from Capacity methods "birth" and "death" raise ZeroDivisionError when a total rate is zero #21 runs with capacity_method="birth", with a ConstantProcess or a DiscreteProcess death rate.
  • test_zero_rate_multiplier: a zero rate multiplier gives an infinite waiting time.

The last two tests fail without this fix, and all of them pass with it. On Python 3.15, the unit tests, doctests, and all three notebooks (nbval, including from an empty matplotlib cache) pass, along with ruff, docformatter, and pyright. Re-running the quickstart left every existing cell's output unchanged, and the docs build.

🤖 Generated with Claude Code

wsdewitt and others added 2 commits October 18, 2026 00:52
With capacity_method="birth" or "death", evolve divides by the total
birth or death rate of the active nodes, and Process.waiting_time_rv
divides by the rate multiplier. Either can be zero, for example when no
active node is in a state that can divide, or with a zero death rate.
These divisions raised ZeroDivisionError. In 0.6.2 some of these cases
ran by accident, through numpy's division-by-zero semantics, until
ConstantProcess started returning Python floats in 1.0.0.

- evolve updates a rate multiplier only when the total it divides by is
  positive. Otherwise every active node has rate zero for that event,
  so the multiplier doesn't matter.
- waiting_time_rv treats a zero rate multiplier as an infinite waiting
  time.

Every case that ran in 0.6.2 gives the same trees, and seeded results
are unchanged. Also adds tests for each capacity method and adds "stop"
to the type annotation of capacity_method.

Closes #21

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Explains capacity and each capacity_method, and plots the population
size over time for "stop", "hard", "birth", and "death", using
Tree.slice to count the lineages alive at each time. The default
(capacity_method=None) is shown raising an error.

The quickstart now imports matplotlib, so its first code cell is tagged
nbval-ignore-output, like the other notebooks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@wsdewitt wsdewitt changed the title Handle zero total rates in the "birth" and "death" capacity methods Handle zero total rates in capacity methods, and document capacity in the quickstart Oct 18, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@wsdewitt
wsdewitt merged commit f0e10a7 into main Oct 18, 2026
5 checks passed
@wsdewitt
wsdewitt deleted the capacity-zero-rates branch October 18, 2026 08:15
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.

Capacity methods "birth" and "death" raise ZeroDivisionError when a total rate is zero

1 participant