Source code for mathematicskit.probability.systems.branching
r"""Galton-Watson branching processes: extinction probability and simulation.
Each individual independently has :math:`j` children with probability
:math:`p_j`. With probability generating function
:math:`G(s) = \sum_j p_j s^j`, the probability :math:`q_n` that the
family is extinct by generation :math:`n` obeys :math:`q_{n+1} = G(q_n)`,
:math:`q_0 = 0`, and increases to the smallest root of :math:`G(q) = q`
in :math:`[0, 1]`. That root is 1 exactly when the mean offspring
:math:`m = G'(1) \leq 1` (unless :math:`p_1 = 1`). No scipy routine
covers branching processes, so the fixed-point iteration is written out
here; the simulation draws offspring counts with
:meth:`numpy.random.Generator.multinomial`. See Athreya & Ney,
*Branching Processes* (1972), Ch. I.
"""
from __future__ import annotations
import numpy as np
from mathematicskit.constants import DEFAULT_ATOL, DEFAULT_MAX_ITER
from mathematicskit.probability.core.base import BranchingProcessResult
__all__ = ["galton_watson_extinction_probability", "galton_watson_simulate"]
def _check_pmf(offspring_pmf) -> np.ndarray:
p = np.asarray(offspring_pmf, dtype=np.float64)
if p.ndim != 1 or np.any(p < 0) or not np.isclose(p.sum(), 1.0):
raise ValueError("offspring_pmf must be a 1D array of non-negative probabilities summing to 1.")
return p
[docs]
def galton_watson_extinction_probability(offspring_pmf, tol: float = DEFAULT_ATOL, max_iter: int = DEFAULT_MAX_ITER) -> float:
r"""Probability that a Galton-Watson family started by one individual eventually dies out.
Iterates :math:`q_{n+1} = G(q_n)` from :math:`q_0 = 0`; each iterate
is the probability of extinction by generation :math:`n`. In the
subcritical and critical cases (:math:`m \leq 1`) the answer is 1 by
the Galton-Watson theorem and is returned directly, since the
critical iteration converges only like :math:`1/n`.
Parameters
----------
offspring_pmf : array-like of float
``offspring_pmf[j]`` is the probability of exactly ``j`` children.
tol : float
Stop when successive iterates differ by less than ``tol``.
max_iter : int
Returns
-------
float
The extinction probability :math:`q`.
Examples
--------
>>> # G(s) = 1/4 + s/4 + s^2/2, whose smallest fixed point is 1/2.
>>> round(galton_watson_extinction_probability([0.25, 0.25, 0.5]), 8)
0.5
"""
p = _check_pmf(offspring_pmf)
mean = float(np.dot(np.arange(p.size), p))
if p.size > 1 and np.isclose(p[1], 1.0):
return 0.0
if mean <= 1.0:
return 1.0
q = 0.0
for _ in range(max_iter):
q_next = float(np.polynomial.polynomial.polyval(q, p))
if abs(q_next - q) < tol:
return q_next
q = q_next
return q
[docs]
def galton_watson_simulate(offspring_pmf, n_generations: int, n_runs: int = 1000, founders: int = 1, seed: int = 0) -> BranchingProcessResult:
r"""Simulate independent Galton-Watson processes.
The children of the :math:`Z_n` individuals in generation :math:`n`
are counted with one multinomial draw of size :math:`Z_n` over the
offspring classes, so :math:`Z_{n+1} = \sum_j j\,N_j`.
Parameters
----------
offspring_pmf : array-like of float
n_generations : int
n_runs : int
Number of independent families.
founders : int
Population of generation 0.
seed : int
Returns
-------
BranchingProcessResult
Examples
--------
>>> result = galton_watson_simulate([0.25, 0.25, 0.5], n_generations=30, n_runs=4000, seed=0)
>>> bool(abs(result.extinct_fraction - 0.5) < 0.03)
True
"""
p = _check_pmf(offspring_pmf)
rng = np.random.default_rng(seed)
j = np.arange(p.size)
sizes = np.zeros((n_runs, n_generations + 1), dtype=np.int64)
sizes[:, 0] = founders
for n in range(n_generations):
counts = rng.multinomial(sizes[:, n], p)
sizes[:, n + 1] = counts @ j
return BranchingProcessResult(generation_sizes=sizes, extinct_fraction=float(np.mean(sizes[:, -1] == 0)))