API reference¶
The Python API has one canonical path per name, reached through each level's package —
there are no top-level re-exports. A run always starts from a simulate_* entry point and
returns a *Result object that carries the true history behind the dataset.
from zombi2 import species, genomes
from zombi2.rates import scope, modifiers
result = species.simulate_species_tree(birth=1.0, death=0.3, n_extant=20, seed=1)
The reference below is generated from the source docstrings, level by level, in the same order as the user guide.
Species trees¶
zombi2.species.simulate_species_tree ¶
simulate_species_tree(birth, death=0.0, *, n_extant=None, total_time=None, mass_extinctions=None, sampling=1.0, fossils=0.0, seed=None, progress=False, max_lineages=100000) -> SpeciesResult
Grow a forward birth-death tree.
birth and death are rate specs (a number, a scope wrapper, or a product
with modifiers); the default scope is per lineage (each lineage speciates/dies at
the base rate, so the tree grows exponentially). Yule = death=0.
Stop at exactly n_extant living lineages, or at total_time — give exactly
one. n_extant is conditioned on survival: a birth-death tree can die out, so we
restart (advancing the same generator) until one reaches n_extant. total_time is not
conditioned. Deterministic given seed.
mass_extinctions is a list of (time, fraction_lost) pulses — e.g. [(3.0, 0.75)] culls
75% of the lineages alive at time 3.0 (time runs forward from the origin, t=0). It is a point-in-time
intervention on the process (not a rate) placed on the timeline, so it needs a fixed end:
give total_time (not n_extant), with each time strictly inside (0, total_time).
sampling (ρ, default 1.0) is incomplete extant sampling: each survivor is observed with
probability ρ, the rest relabelled unsampled. It prunes the extant tree to the sampled
survivors (the unsampled ones remain only in the complete tree). n_extant still stops at that
many survivors; sampling then thins what you observe, so result.n_extant can be smaller.
fossils is a recovery rate along the branches: each branch of length L yields
Poisson(fossils × L) fossils, returned as result.fossils = (lineage, time) pairs. A
side output — the fossil's lineage is not removed and does not enter the extant tree.
Source code in zombi2/species/__init__.py
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 | |
zombi2.species.SpeciesResult
dataclass
¶
SpeciesResult(complete_tree: Tree, events: list[Event], seed: int | None, fossils: list[tuple[int, float]] = list())
What simulate_species_tree returns: the complete_tree (with the dead) and the derived
extant_tree (the observed survivors), the events log (the recorded true history), the
seed, and any fossils. (The record= memory dial lands with the data-heavy levels.)
n_extant
property
¶
The number of observed survivors — the extant tips. Under sampling < 1 this is
the sampled subset (the rest are unsampled), so it matches the extant tree's tip count.
extant_tree
cached
property
¶
The survivors' tree — the complete tree pruned to extant lineages with the
unifurcations suppressed (dated, bifurcating). None if nothing survived.
write ¶
Write outputs to directory, each file prefixed species_; outputs selects which
(default = all applicable): "complete" → species_complete.nwk, "extant" →
species_extant.nwk (if any survived), "events" → species_events.tsv (the
always-recorded true history), "fossils" → species_fossils.tsv (if any recovered),
"fates" → species_fates.tsv (each tip's resolved fate).
species_fates.tsv is the tip-fate table: one lineage<TAB>fate row per tip, with fate
one of extant / extinct / unsampled. Fate is resolved once, at the end of the run,
on the same stable n<id> that keys every other file, so it never renames anything — it is
a materialised view of information the run already holds. It exists because the .nwk records
only branch lengths, from which a reader cannot tell an extinct tip from a survivor that sits at
the present; this table says so directly, so a downstream level can build the extant set from
fate rather than guessing from tip depth.
Source code in zombi2/species/__init__.py
zombi2.species.Tree
dataclass
¶
The complete tree: every lineage that ever lived, keyed by id, rooted at root.
leaves ¶
extant ¶
extinct ¶
unsampled ¶
Survivors not observed under incomplete sampling — kept in the complete tree (told
apart by their fate) but pruned from the extant tree.
to_newick ¶
Serialise to Newick (matching tree.to_newick() elsewhere in the codebase). Each
branch length is end_time - birth_time and every node — leaves and internals — is named
n<id>.
The root carries a branch length like any other node: its stem, the time from the origin
to the first split. A forward birth–death run starts from one lineage, so that stem is real
simulated time in which events happen, and writing )n0; would silently discard it — for a
tree whose crown comes late, a large fraction of its history. It is emitted as )n0:<stem>;
and :func:read_newick reads it back.
Source code in zombi2/tree.py
zombi2.species.Node
dataclass
¶
Node(id: int, parent: int | None, birth_time: float, end_time: float | None = None, children: tuple[int, int] | None = None, fate: str = 'alive')
One lineage segment: born at birth_time, ended at end_time by a split, a
death, or reaching the present. A split has two children; a leaf has none.
zombi2.species.Event
dataclass
¶
A recorded event in the true history: a speciation (with its two children) or an extinction.
zombi2.species.prune ¶
Prune the complete tree to a kept set (matching prune(tree, keep=...) in the codebase):
drop the pruned subtrees and suppress the unifurcations they leave behind, giving a dated,
bifurcating tree. Branch lengths merge across suppressed nodes; None if nothing is kept.
keep="extant" (default) keeps the survivors — the extant tree. ("sampled", the
fossil/serially-sampled tree, arrives with the sampling and fossils slices; it raises for now.)
Source code in zombi2/tree.py
Genomes¶
The genome level has three resolutions — family ⊂ ordered ⊂ nucleotide — one entry point each.
zombi2.genomes.simulate_genomes_family ¶
simulate_genomes_family(tree, *, duplication=0.0, transfer=0.0, loss=0.0, origination=0.0, transfer_to='uniform', replacement=False, self_transfer=False, initial_families=100, family_names=None, family_speed=None, max_family_size=10.0, seed=None, parallel=False, stream_to=None, outputs=None, progress=False) -> 'FamilyGenomesResult | StreamedRun'
Evolve a multiset of gene families along a species tree by duplication, transfer, loss, and origination.
tree is the complete species tree (a :class:~zombi2.species_tree.Tree, or a
:class:~zombi2.species_tree.SpeciesResult whose complete_tree is used). Genomes evolve on
every lineage, extant and extinct alike, so the true gene-tree history is complete and a
transfer can arrive "from the dead"; the observed genomes are the extant tips.
Rates (each a scope(base) × modifiers spec): duplication/transfer/loss default
per copy, origination per lineage. When a transfer fires it moves a copy from a
uniformly-chosen donor copy to a recipient lineage alive at that instant, chosen by
transfer_to — "uniform" (any other contemporaneous lineage), "distance" /
Distance(decay=) (closer relatives likelier), Clades({...}, Between({...})) (weighted by
the donor's and recipient's named clade, so transfer can run between two clades — see below),
or mod.DrivenBy(source, mapping) (weighted by another level; see below). replacement=True
overwrites a homologous
copy in the recipient (additive fallback if it has none); self_transfer=True lets a lineage
donate to itself. The root starts with initial_families families of one copy each, recorded
as originations at the crown. family_names=["toxin", …] additionally declares named families —
each gets a normal (integer) family id, but its name is remembered in result.family_names so
you can track a specific family (result.has_family(node, "toxin")); this is the handle a joint
DrivenBy("genomes:toxin", …) reads. Deterministic given seed.
Conditioning (a trait drives a rate). Any of the four rates may be driven by another level —
loss = 0.25 * mod.DrivenBy("habitat.tsv", {"aquatic": 3.0, "terrestrial": 1.0}) scales each
lineage's loss by the habitat on that branch, read from a driver file grown first
(traits.simulate_discrete(...).write(dir, outputs=("driver",))). A driven rate is then
per-lineage: it is summed over the living lineages (each with its own copy count and driver
value), the affected lineage is drawn weighted by its rate, and the Gillespie steps at every
mid-branch switch of the driver (SPEC §2). For transfer the affected
lineage is the donor, so a driven transfer says how often a lineage donates.
Conditioning (a trait drives who receives). transfer_to = mod.DrivenBy(source, mapping) is
the other half, and a different model: the mapping's numbers are per-candidate weights, not
rate multipliers, so they leave the total amount of transfer alone and only redistribute it
(SPEC §5, the choice slot). Candidate lineage k gets weight mapping(driver value on k now)
and receives with probability w_k / Σw — five candidates at weight 1 and five at weight 2 send
two thirds of transfers to the weight-2 group. Weight 0 means "cannot receive"; when every
candidate weighs 0 the transfer does not happen (see :func:_do_transfer). The two couplings are
independent and may be used together or apart.
parallel opts into a separate engine that evolves the families concurrently, one per worker
process — the families are independent (a transfer roams a copy across lineages but never mixes two
families), so it enumerates every family's origination first and then evolves each on its own. It is
worker-count invariant (each family draws from its own stream spawned off seed) but gives a
different-though-equally-valid draw than the serial default for a given seed. False (default)
runs the serial loop; True uses every core; an int sets the worker count. A driven rate
or transfer_to (DrivenBy) has no per-family engine yet — the run announces this and falls
back to the serial loop. The gain is real but modest (a merge over the whole event log stays
serial), so a handful of workers is the sweet spot; unlike the sequences level it does not scale far.
Because it spawns processes, a calling script must guard its entry with if __name__ ==
"__main__": (the zombi2 CLI already does).
stream_to=DIR takes the same engine to the many-families regime: each family is written straight
to disk as it finishes — no whole-run merge, no run held in memory (a run that fills gigabytes in
memory streams in tens of megabytes) — and a light :class:~zombi2.genomes.StreamedRun handle comes
back instead of a FamilyGenomesResult. outputs= picks which files, exactly as
:meth:FamilyGenomesResult.write takes them (default: all of them). It is the per-family engine, so a
driven rate raises here rather than falling back (that would defeat the point), and outputs
without stream_to is an error.
Source code in zombi2/genomes/family.py
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 | |
zombi2.genomes.simulate_genomes_ordered ¶
simulate_genomes_ordered(tree, *, duplication=0.0, transfer=0.0, loss=0.0, origination=0.0, inversion=0.0, transposition=0.0, translocation=0.0, chromosomes=1, topology='circular', fission=0.0, fusion=0.0, chromosome_origination=0.0, chromosome_loss=0.0, duplication_extent=None, loss_extent=None, transfer_extent=None, inversion_extent=None, transposition_extent=None, translocation_extent=None, inversion_probability=0.0, transfer_to='uniform', replacement=False, self_transfer=False, initial_families=100, family_names=None, family_speed=None, max_family_size=10.0, seed=None, progress=False) -> OrderedGenomesResult
Evolve ordered genomes — genes with a position and an orientation, on chromosomes — along a species tree, by the D/T/L/O core plus segmental rearrangements and the chromosome tier.
Every gene-level event acts on an extent — a run of consecutive genes (the ZOMBI1 model):
duplication copies the run in tandem, loss removes it, transfer sends it to a
contemporaneous recipient as a block, inversion reverses it (flipping strands), transposition
relocates it elsewhere on the same chromosome, and translocation moves it to a different
chromosome. The run's length is drawn per event from <event>_extent (a distribution,
default Geometric(mean=1) — usually a single gene; dial the mean up for larger blocks).
origination is the exception: a family is born once, a single gene, no extent.
transposition and translocation land the moved block inverted with probability
inversion_probability.
Where a run stops is set by the chromosome's topology. A run goes rightwards from the gene
it starts at. On a "circular" chromosome there are no ends, so a run that reaches the last
gene continues from the first, and only the whole chromosome bounds it; on a "linear" one the
run stops at the last gene. So on a circular chromosome every gene is covered by segmental events
at the same rate, and the nominal mean extent is the realised one.
Scopes follow the cross-level grammar, which counts an event per the thing it acts on: the
gene-level events — duplication/transfer/loss and the rearrangements
inversion/transposition/translocation — are per copy, since each acts on a run of
genes that starts at one of them; the chromosome tier fission/fusion/chromosome_loss
is per chromosome; and the two events that make something from nothing,
origination/chromosome_origination, are per lineage. The
run starts with chromosomes chromosomes of the given topology, across which the
initial_families founding genes are dealt round-robin; family_names=["toxin", …] additionally
declares named families (remembered in result.family_names for result.has_family(node,
"toxin")), as in the family core; transfer_to / replacement / self_transfer behave
as in the family core.
The chromosome tier changes chromosome number: fission (split), fusion (merge — the
reticulation), chromosome_origination (a de-novo replicon), chromosome_loss (a whole
chromosome and its genes die; never the genome's last). Chromosomes carry identity — re-minted at
every event that reshapes them — so chromosome_events is the true reticulating chromosome
genealogy, rooted at the initial and de-novo originations. Deterministic given seed.
Source code in zombi2/genomes/ordered.py
787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 | |
zombi2.genomes.simulate_genomes_nucleotide ¶
simulate_genomes_nucleotide(tree, *, inversion=0.0, inversion_extent=50.0, translocation=0.0, translocation_extent=50.0, transposition=0.0, transposition_extent=50.0, inversion_probability=0.0, loss=0.0, loss_extent=50.0, duplication=0.0, duplication_extent=50.0, transfer=0.0, transfer_extent=50.0, transfer_to='uniform', self_transfer=False, origination=0.0, origination_extent=50.0, fission=0.0, fusion=0.0, chromosome_origination=0.0, chromosome_loss=0.0, chromosomes=1, root_length=1000, topology='circular', genes=0, gene_length=100, gff=None, fasta=None, trim_overlaps=False, seed=None, progress=False) -> NucleotideGenomesResult
Evolve a nucleotide genome along a species tree by inversion, translocation, transposition,
loss, duplication, transfer, origination, and the number-changing chromosome tier.
The run starts from a karyotype — chromosomes replicons, each its own source: an int
N gives N equal replicons of root_length/topology, or pass a list of (length,
topology) for heterogeneous sizes and shapes. Each lineage inherits a copy of its parent's
karyotype at speciation, with every chromosome re-minted (the chromosome network), and evolves:
inversion(per lineage) reverses a geometric-length (meaninversion_extent) arc of a length-weighted chromosome.translocation(per lineage) moves a geometric-length (meantranslocation_extent) arc to a different chromosome;transposition(per lineage, meantransposition_extent) moves one within its chromosome. Both land inverted with probabilityinversion_probability, keep source coordinates, and are rearrangements, not edges.loss(per lineage) deletes a geometric-length (meanloss_extent) arc — an ancestry-changing event (a death), recorded inevents. Never empties a chromosome.duplication(per lineage) copies a geometric-length (meanduplication_extent) arc in tandem — an ancestry-changing birth, recorded inevents.transfer(per lineage) copies a geometric-length (meantransfer_extent) arc into a contemporaneous recipient (transfer_to:"uniform"or"distance"/ a :class:Distance;self_transferallows the donor itself) — a horizontal birth, additive (the donor keeps its copy). This is what needs the global timeline.origination(per lineage) lays down a new gene on a fresh source (geometric length, meanorigination_extent) — a birth of a wholly new family, indivisible from birth.fission(per chromosome) splits a chromosome in two (a bifurcation);fusion(per chromosome) merges two chromosomes of the same topology (the reticulation).chromosome_origination(per lineage) adds a de-novo circular replicon (a plasmid, a network root) carrying one new gene;chromosome_loss(per chromosome) kills a whole chromosome (its material dies as a loss; never the last one) — a network leaf. All record a chromosome-network edge.
A chromosome never exists without a gene. A replicon is born with one, and any event that would strip a chromosome of its last gene — a loss, a translocation carrying it away, a fission splitting off a geneless half — simply does not happen. (Vacuous when no genes are declared.)
The engine runs a global-timeline Gillespie: all lineages alive at once evolve along one clock
(every segmental event is per lineage — the rate says how often a lineage does it, the extent how
much it touches, so a bigger genome does not get proportionally more events; the chromosome tier is
per chromosome), so a transfer
couples two contemporaries. With loss, the strong invariant weakens: every node carries a subset
of the initial sequence (each ancestral position at most once, monotonically down every path);
origination further adds fresh sources beyond the root. Deterministic given seed. (Transfer is
always additive.)
Source code in zombi2/genomes/nucleotide.py
1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 | |
zombi2.genomes.FamilyGenomesResult
dataclass
¶
FamilyGenomesResult(complete_tree: Tree, genomes: dict[int, tuple[GeneCopy, ...]], events: list[Event], seed: int | None, family_names: dict[str, int] = dict(), initial_genome: tuple[GeneCopy, ...] = ())
What simulate_genomes_family returns: the complete_tree it ran on, the final
genomes at every node (extant and extinct), the events log (the compact source of
truth), and the seed. The observed genomes are the extant tips —
{n.id: genomes[n.id] for n in complete_tree.extant()}. The phyletic profiles are derived
from those tips on access, and write materialises the chosen outputs to disk.
profiles
cached
property
¶
The phyletic profiles — each gene family's copy count in each extant species — derived
from the observed genomes (the classic comparative-genomics matrix). See :mod:.profiles.
gene_trees
cached
property
¶
{family id: GeneTree} — each family's true genealogy inside the complete tree,
derived from the event log. Each GeneTree exposes .complete and .extant. See
:mod:.gene_trees.
family_counts ¶
A multiset view of one node's genome: family id → copy count.
has_family ¶
Whether the named family name (declared via family_names=) is present — has ≥ 1 copy — in
the genome at node_id. The presence signal a joint DrivenBy("genomes:<name>", …) reads.
Source code in zombi2/genomes/family.py
write ¶
Materialise chosen outputs to directory (created if needed):
"events"→genome_events.tsv, the event log (the source of truth)."profiles"→profiles.tsv, the family × extant-species copy-count matrix."genomes"→genomes.tsv, every node's gene content, one row per gene copy — ancestors included, whereprofiles.tsvcounts only the extant tips."initial_genome"→initial_genome.tsv, the genome the run started with. Its own file, not a row ingenomes.tsv, because it belongs to no node: it sits at the start of the root branch, and everylineagein that table is a node at the end of one."gene_trees"→gene_tree_fam<family>_{complete,extant}.nwk, each family's true genealogy. A family with no surviving copy writes no_extantfile.
Source code in zombi2/genomes/family.py
zombi2.genomes.OrderedGenomesResult
dataclass
¶
OrderedGenomesResult(complete_tree: Tree, genomes: dict[int, tuple[Chromosome, ...]], events: list[Event], rearrangements: list[Inversion | Transposition | Translocation], chromosome_events: list[ChromosomeEvent], seed: int | None, family_names: dict[str, int] = dict(), event_positions: list[EventPosition] = list(), initial_genome: tuple[Chromosome, ...] = ())
What :func:simulate_genomes_ordered returns: the complete_tree it ran on, the final
genomes at every node as tuples of :class:Chromosome\ s, the shared gene-genealogy
events log, the rearrangements (inversions) and chromosome_events (the chromosome
genealogy) logs, and the seed. The observed genomes are the extant tips; profiles and
gene_trees are derived from the (position-blind) genealogy exactly as for the family core;
gene_order reads a node's layout, and write materialises the chosen outputs.
profiles
cached
property
¶
The phyletic profiles — each gene family's copy count in each extant species — derived
from the observed genomes, flattening across chromosomes (position does not enter). See
:mod:.profiles.
gene_trees
cached
property
¶
{family id: GeneTree} — each family's true genealogy inside the complete tree, derived
from the (position-blind) event log exactly as for the family core. See :mod:.gene_trees.
family_counts ¶
A multiset view of one node's genome: family id → copy count (across all chromosomes).
Source code in zombi2/genomes/ordered.py
has_family ¶
Whether the named family name (declared via family_names=) has ≥ 1 copy in the genome at
node_id (across all chromosomes).
Source code in zombi2/genomes/ordered.py
gene_order ¶
One node's layout as (chromosome, position, strand, family, gene id) rows, chromosome
by chromosome and left to right within each — the ordered analogue of family_counts.
Source code in zombi2/genomes/ordered.py
write ¶
write(directory, outputs=('events', 'profiles', 'gene_order', 'initial_genome', 'gene_trees', 'chromosome_events')) -> None
Materialise chosen outputs to directory (created if needed):
"events"→genome_events.tsv, the run's whole history in one time-ordered table: the gene genealogy, where each event happened, and the ancestry-neutral rearrangements. Withgene_orderthis is enough to replay the run."profiles"→profiles.tsv, the family × extant-species copy-count matrix."gene_order"→gene_order.tsv, every node's layout (one row per gene), ancestors included — so a branch's rearrangements can be replayed from its parent's genome."initial_genome"→initial_genome.tsv, the layout the run started with. Its own file, not a row ingene_order.tsv, because it belongs to no node: it sits at the start of the root branch, and everylineagein that table is a node at the end of one."chromosome_events"→chromosome_events.tsv, the chromosome genealogy edges. The one log kept apart: it is a network over chromosome ids, with list-valued parents and children, joined on a different key from everything above."gene_trees"→gene_tree_fam<family>_{complete,extant}.nwk, each family's true genealogy — unchanged from the family resolution, position being orthogonal to it.
Source code in zombi2/genomes/ordered.py
zombi2.genomes.NucleotideGenomesResult
dataclass
¶
NucleotideGenomesResult(complete_tree: Tree, genomes: dict[int, NucleotideGenome], events: list[Origination | Loss | Duplication | Transfer | Speciation], rearrangements: list[Inversion | Translocation | Transposition], chromosome_events: list[ChromosomeEvent], seed: int | None, gene_spans: dict[int, tuple[int, int, int]] = dict(), gene_names: dict[str, int] = dict(), gene_strands: dict[int, int] = dict(), initial_genome: NucleotideGenome = (lambda: NucleotideGenome([]))(), initial_sequence: dict[int, str] = dict())
What :func:simulate_genomes_nucleotide returns: the complete_tree it ran on, the final
nucleotide genomes (karyotypes) at every node, the copy-lineage genealogy events
(origination, loss, duplication, transfer, speciation — carrying the copy ids
the gene-tree recovery reads), the ancestry-neutral rearrangements (inversion, translocation), the
chromosome_events (the chromosome network), and the seed. mosaic / trace_back /
ancestry read a node's genome.
root_blocks
property
¶
The recovered root partition: (source, start, end) for each maximal never-cut
interval that some node still carries — one per :attr:block_trees entry (by index).
Cut at the breakpoints of every node's genome, not only the extant leaves', which is what
lets any node be reconstructed (:meth:assembly) rather than the survivors alone.
block_trees
property
¶
{root-block index: GeneTree} — a tree for every recovered root block, spacer as
well as genes, keyed by its index in :attr:root_blocks.
:attr:gene_trees covers the declared genes; this covers the whole genome. A block never
splits, so its size is fixed and its genealogy is in the event log just as a gene's is — the
recovery is the same one, pointed at every block instead of a chosen few. That is what makes
an ancestral genome reconstructable at any node rather than only at the loci you declared.
A gene's tree here has the same topology and branch lengths as its :attr:gene_trees one,
but not the same g<id> leaf labels: segment ids are handed out as the recovery walks its
targets, and walking every block numbers them differently from walking three. Use one accessor
or the other within a piece of analysis — they are the same genealogy under different names.
gene_trees
property
¶
{family: GeneTree} — the recovered gene trees.
With genes declared, one tree per gene, keyed by its gene family id (see
:attr:gene_spans); the intergenic root-blocks keep their block ancestry in the log but are not
built into trees. With no genes declared the whole genome is one big intergene, so every
recovered root-block is a family in its own right and the key is its index in
:attr:root_blocks. Every node votes on the partition, so a gene surviving only in lineages
that died still gets a tree — a complete one, with no extant tree to go with it. Only a gene
lost from every node has no root-block and no tree.
block_of ¶
The index in :attr:root_blocks of the block a declared gene family occupies — the join
between the two numbering schemes this resolution has.
They are both plain ints over overlapping ranges, so mixing them up is silent: gene_spans
and gene_trees are keyed by gene family id, while root_blocks, block_trees and
everything a sequence run produces here are keyed by block index (every block evolves, and
spacer has no family). block_of is how you get from a gene to its sequences::
r.alignments[g.block_of(g.gene_names["dnaA"])] # that gene's alignment
Raises KeyError for a family that was never declared, and LookupError for one declared
but surviving nowhere at all — it has no recovered block, so there is nothing to point at.
The reverse lookup is one line: {span: fam for fam, span in g.gene_spans.items()} read at
root_blocks[i].
Source code in zombi2/genomes/nucleotide.py
assembly ¶
How this node's genome is built out of the recovered root blocks:
{chromosome id: [(block, gene, strand), …]} in physical order, where block indexes
:attr:root_blocks, gene is the gene id that block's tree gives this node's copy (the
g<id> label in :attr:block_trees), and strand is +1 read forward or -1
reverse-complemented.
To reconstruct a genome: pair each piece with its block's evolved sequence, flip the
-1\ s, and concatenate. The sequence level does exactly that; nothing here knows about
letters. Every node works — an extinct leaf and the root as readily as a surviving tip —
which is what makes the whole history recoverable rather than only its leaves.
:meth:initial_assembly does the same for the genome the run started with.
A piece is always a whole block, never part of one, because every node votes on where the
partition is cut (see :func:_root_block_partition): this node's own breakpoints are all in
it, so each of its blocks is a whole number of root blocks. What a block is cut into is one
piece per root block it spans — and on a reversed block those come out in descending
coordinate order, since physical order runs down the source.
Source code in zombi2/genomes/nucleotide.py
initial_assembly ¶
:meth:assembly for :attr:initial_genome: {chromosome id: [(block, strand), …]}.
No gene id here, unlike :meth:assembly, and that is the honest shape rather than a saving.
The initial genome sits at the start of the root branch, before any event, so each of its
blocks has exactly one sequence — the founding draw the sequence level records as
founding[block] — and there is no copy to disambiguate. A gene id would in fact be wrong
here: the one :meth:assembly gives is the last gene a copy held, and for an initial copy
that is at the far end of the stem. A loss on the stem can even end it, which is the same
thing said louder.
Source code in zombi2/genomes/nucleotide.py
write ¶
write(directory, outputs=('events', 'genes', 'blocks', 'initial_genome', 'initial_sequence', 'gene_trees', 'chromosome_events', 'gff', 'bed')) -> None
Materialise chosen outputs to directory (created if needed):
"events"→genome_events.tsv, the run's whole history in one time-ordered table: the copy-lineage genealogy and the ancestry-neutral rearrangements. One row per ancestral interval an event touched, so an event that spanned several blocks writes several rows sharing atimeandkind."blocks"→blocks.tsv, every node's genome as its block mosaic (ancestors included, as for the ordered resolution'sgene_order). The one big file here: blocks are not kept maximal during a run, so a rearrangement-heavy genome carries far more of them than it has distinct ancestral runs, and this grows with their number × every node."genes"→genes.tsv, the declared genes and where they sit in root coordinates. Header-only for a run that declared none."initial_genome"→initial_genome.tsv, the block mosaic the run started with. Its own file, not a row inblocks.tsv, because it belongs to no node: it sits at the start of the root branch, and everylineagein that table is a node at the end of one."chromosome_events"→chromosome_events.tsv, the chromosome network's edges. The one log kept apart: it is a network over chromosome ids, with list-valued parents and children, joined on a different key from everything above."gene_trees"→gene_tree_fam<family>_{complete,extant}.nwk, one recovered genealogy per family some node still carries; the_extantfile only where the family has a surviving copy."initial_sequence"→initial_sequence.fasta, the initial DNA the run was given (fasta=), one>source<n>record per replicon. Written only when a FASTA was supplied — it is what lets a separatezombi2 sequencesrun found its blocks from the real sequence."gff"→genome_<lineage>.gff, that genome's genes, in its own coordinates: the annotation to read beside the sequence level'sgenome_<lineage>.fasta."bed"→genome_<lineage>.bed, that genome's blocks — every piece, spacer included, named by the ancestral interval it descends from. The ancestry as a browser track.
Both name their sequences <lineage>_chr<c>, exactly as the FASTA records are named, so a
genome and its annotation join without renaming anything. Written for every node and for the
initial genome, so there are two files per genome.
Source code in zombi2/genomes/nucleotide.py
zombi2.genomes.GeneTree
dataclass
¶
One gene family's true genealogy. complete is the whole tree (lost and extinct-species
lineages included); extant is it pruned to the genes surviving at the extant tips (degree-two
nodes suppressed), or None if the family left no extant gene. to_newick serialises either.
origination is when the family was founded — the exact time of its origination event, or the
root lineage's start for a family declared by initial_families. A :class:GeneNode records when
it ended, so this is the one time the tree cannot derive: it is where the root's branch begins.
to_newick ¶
Newick of the "extant" (default) or "complete" tree; None if it is empty.
Leaves are g<id>; with annotate internal nodes carry <kind>_n<species>; branch
lengths are time differences.
The root carries one too, running from origination to where the root gene ended — the
stem of the family, real time in which that founding gene existed. On the extant tree the
root may be a node whose ancestors were suppressed; its branch still starts at origination
and so absorbs them, exactly as the species tree's extant root absorbs its own.
Source code in zombi2/genomes/gene_trees.py
zombi2.genomes.GeneCopy
dataclass
¶
One gene copy: a member of family family, identified by a globally-unique id. Its
birth/death times and parentage live in the event log (the source of truth); the copy carries
only what a genome snapshot needs to be self-describing — who it is and which family it is in. A
genome may hold several copies sharing a family (that family's copy count).
Sequences¶
zombi2.sequences.simulate_sequences ¶
simulate_sequences(genomes, *, model: SubstitutionModel, length: int | None = None, intergene_model: SubstitutionModel | None = None, intergene_speed=3.0, substitution=1.0, seed=None, parallel=False, progress=False) -> SequencesResult
Evolve one sequence down each family's gene tree under a substitution model.
genomes is a genome run — the :class:~zombi2.genomes.FamilyGenomesResult that
genomes.simulate_genomes_family(...) returned. Its gene_trees are what the sequences
evolve along and its complete_tree is the species tree the lineage clock rides; bare gene
trees are rejected (they would run, but with no clock and no species phylogram — a silent
degradation). Each family's complete gene tree is evolved, so the true history is complete and
ancestral sequences exist for extinct/lost lineages too; the observable alignments are the
extant tips.
model is a substitution model from the menu (:mod:.substitution_models) — nucleotide
jc69 · k80 · hky85 · gtr, or protein poisson · jtt · dayhoff ·
wag · lg; its alphabet is what the sequences are written in (ACGT or the 20 amino
acids). length is the number of sites. substitution is the per-site substitution
rate (default 1.0): a branch of Δt time accrues substitution · Δt substitutions/site.
The founding sequence of each family is drawn from the model's stationary frequencies. Deterministic
given seed.
substitution may carry a lineage clock — one factor per species branch, shared across
families, computed once before evolving, rescaling each gene-tree branch by the clock of the species
branch it sits on: 1.0 * mod.ByLineage(spread=) is the uncorrelated clock (each branch drawn
i.i.d.), and 1.0 * mod.FromParent(spread=) is the autocorrelated clock (the factor drifts
parent→child down the species tree). Any other modifier (the Markov clock, the ByFamily
per-family speed, +Γ) or a non-PerSite scope raises.
On a nucleotide genome run every root block is evolved — spacer as well as genes — each at its
own length in bp, so length does not apply and is rejected. model evolves the genes and
intergene_model (default jc69) the spacer, at intergene_speed times the rate (default
3.0). Because the whole genome is covered, the run also puts the genomes back together:
.genomes holds every node's chromosomes, blocks concatenated in physical order — the complete
tree, reconstructed — and .initial_genome the one the run started with.
The result carries the phylograms the sequences were drawn along — each gene tree and the
species tree, with branch lengths converted from time to substitutions/site by the same
base × clock × Δt.
parallel opts into evolving the gene trees concurrently — one gene tree per worker
process, which is where a run's time goes when the sequences are long or a nucleotide genome has
thousands of blocks. False (the default) runs the serial engine above. True uses every
core; a positive int sets the worker count. It is a separate engine: each family draws
from its own RNG stream (spawned from seed), so every worker count returns the same bytes,
but that realisation differs from the serial one for a given seed — parallel is a speed choice,
made once, not a drop-in for the default. Threads would not help here (numpy releases the GIL too
little for these array sizes), so this is process-backed and only pays off above a work threshold.
Because it spawns processes, a script that calls it with parallel set must guard its entry with
if __name__ == "__main__": (the standard multiprocessing requirement); the zombi2 CLI
already does, so --parallel there needs nothing extra.
Source code in zombi2/sequences/__init__.py
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 | |
zombi2.sequences.SequencesResult
dataclass
¶
SequencesResult(alignments: dict[int, dict[str, str]], ancestral: dict[int, dict[str, str]], founding: dict[int, str], phylograms: dict[int, dict[str, str | None]], species_phylogram: dict[str, str | None], seed: int | None, genomes: 'Mapping[str, dict[int, str]]' = dict(), initial_genome: dict[int, str] = dict(), unit: str = 'family')
What :func:simulate_sequences returns.
alignments—{family: {g<copy>: sequence}}: the observable gene alignment, one entry per extant gene-tree tip, keyed by its (unique, per-segment) gene id — the same labels as the gene tree's / phylogram's Newick leaves. Empty for a family with no surviving copy.ancestral—{family: {g<copy>: sequence}}: the true sequence at every node that is not an extant tip — internal nodes (the family's root gene included) and the dead tips, where a copy was lost or its species went extinct. Withalignmentsit accounts for every node of the tree exactly once, so every label in the complete phylogram names a sequence.founding—{family: sequence}: the sequence the family started with, at its origination — the state the phylogram's root branch leads from. It is drawn from the model's stationary frequencies and then evolves across the stem into the root gene's sequence, so it is not the same string asancestral[family]["g<root copy>"]unless the stem is empty. Kept out ofancestralon purpose: those keys pair one-to-one with phylogram nodes, and the origination is a point on a branch, not a node.phylograms—{family: {"complete": newick, "extant": newick | None}}: each gene tree with branch lengths in substitutions/site (base × lineage-clock × Δt) — the ground-truth tree behind each alignment. Every node is labelled by its gene idg<copy>, so the tips match thealignmentskeys and the internal nodes match theancestralkeys (the phylogram pairs one-to-one with the sequences)."extant"isNonefor a family with no survivor.species_phylogram—{"complete": newick, "extant": newick | None}: the species tree with branch lengths in substitutions/site — the molecular clock made visible (which lineages ran hot / cold). Always present: a run always comes from a genome run, which carries its tree.genomes—{lineage: {chromosome id: sequence}}: every node's assembled genome, its blocks concatenated in physical order (reverse-complemented where the genome carries them inverted) — extant tips, ancestors and the lineages that went extinct alike. The same coverage as the genome level's owngenomes, which is keyed the same way: the observed ones are the extant tips,{node_label(n.id) for n in complete_tree.extant()}. Only a nucleotide genome run has any — a family or ordered run has gene families, not coordinates, so there is no genome to lay out and this is empty.initial_genome—{chromosome id: sequence}: the genome the run started with, at the root lineage's origination. Not ingenomes, because it belongs to no node: the root branch is real simulated time, so the root node's genome is this one plus whatever happened along the stem. It stands togenomesasfoundingstands toancestral.seed— the run's seed.unit— what the integer key ofalignments/ancestral/founding/phylogramsnames:"family"(a gene family id) on a family or ordered run,"block"(an index into the genome run'sroot_blocks) on a nucleotide one, where every block evolves and spacer has no family. They are different numbering schemes over the same ints, so a gene family id is not a key here on a nucleotide run — go through :meth:~zombi2.genomes.NucleotideGenomesResult.block_of. It is also what the filenames say.
write ¶
write(directory, outputs=('alignments', 'phylograms', 'species_phylogram', 'genomes', 'initial_genome')) -> None
Write chosen outputs to directory (created if needed). <u> below is
fam<family> on a family or ordered run and block<index> on a nucleotide one — the
integer keys mean different things, so the files say which (see :attr:unit):
"alignments"→<u>.fasta(skipped for empty families)."ancestral"→sequences_ancestral_<u>.fasta."founding"→sequences_founding.fasta, one record<u>apiece: the sequence each family originated with, before its stem."phylograms"→phylogram_<u>_{complete,extant}.nwk(subs/site)."species_phylogram"→clock_species_tree_{complete,extant}.nwk: the species tree with its branches in substitutions/site — the molecular clock made visible."genomes"→genome_<lineage>.fasta, one file per node — extant, extinct and ancestral alike — with one record per chromosome. Nucleotide runs only; nothing is written otherwise. The big one: a real genome times every node in the tree."initial_genome"→genome_initial.fasta, the genome the run started with.
Source code in zombi2/sequences/__init__.py
Traits¶
zombi2.traits.simulate_continuous ¶
simulate_continuous(tree, *, start=0.0, rate=1.0, reverts_to=None, pull=None, correlation=None, at_speciation=None, regimes=None, seed=None, progress=False) -> TraitsResult
Evolve a continuous trait down a tree and return a :class:TraitsResult. One process, its
variants selected by knobs (SPEC §4): Brownian motion (bare rate), Ornstein–Uhlenbeck
(add reverts_to + pull), early burst (a OnTime skyline on rate), and
variable-rates BM (an FromParent modifier on rate).
Correlated traits ride together in one call (the joint rule inside a level): pass
start and rate as dicts keyed by trait name and a correlation={(a, b): ρ} overlay
(each ρ ∈ [−1, 1]). The traits then diffuse jointly — the branch increment is drawn from
MVN(0, Σ·dt) with Σ = D R D (D = diag(σ_i), R the correlation matrix), so at a tip
the correlation between two traits is exactly their ρ. Correlated Brownian motion only, with bare per-trait rates.
tree is the complete species tree (a :class:~zombi2.species.Tree, or a
:class:~zombi2.species.SpeciesResult whose complete_tree is used). The trait evolves on
every lineage, extant and extinct alike, so the ancestral states are exact and complete; the
observed dataset is the extant tips, result.values.
start is the value at t = 0 (the crown origin, root.birth_time): the root lineage
diffuses over its own branch [0, first split] like any other, so a trait and a genome evolve
over the same branch set, and each node's stored value is the trait at that node's
end_time (node_values[root] is the value at the first split, not start).
rate is the variance-rate σ² (a scope(base) × modifiers rate spec), per lineage: each
lineage diffuses independently at σ², never pooled across the tree. A bare number is Brownian
motion (Normal(0, σ²·dt) over a branch); a OnTime modifier makes σ² change through time —
early burst / ACDC — with the per-branch variance the exact integral ∫ σ²(t) dt; an
FromParent(spread=…) modifier makes σ² drift branch-to-branch — variable-rates BM ("ClaDS
for traits") — each lineage inheriting its parent's σ² times a lognormal kick drawn at the split;
a OnTotalDiversity(cap=…) modifier makes σ² slow as the clade fills up — diversity-dependent /
ecological-limits trait evolution — σ² scaled by (1 − standing_diversity/cap) as the tree's
lineages-through-time grows (the tree is a fixed input the trait reads).
reverts_to (the optimum θ) and pull (the strength α > 0) turn the diffusion into
Ornstein–Uhlenbeck — the value is pulled toward θ while it diffuses, the exact per-branch
transition being Normal(θ + (x−θ)·e^{−α·dt}, σ²/(2α)·(1−e^{−2α·dt})). Give both or
neither. OU with a modified σ² (early burst or variable rates on rate) is not wired yet —
use one or the other.
at_speciation adds an on-speciation jump — Normal(0, at_speciation) on each daughter at
every speciation (the punctuational mode), layered on top of the along-branch anagenesis.
regimes gives multi-optimum OU: pass a discrete :class:TraitsResult (a stochastic map
painted by :func:simulate_discrete on this same tree) and a per-regime reverts_to={regime: θ},
and the value follows OU toward whichever regime's optimum a branch is in. Deterministic given
seed.
Source code in zombi2/traits/continuous.py
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 | |
zombi2.traits.simulate_discrete ¶
simulate_discrete(tree, *, states, switch=None, start=None, liability=None, threshold=None, correlation=None, at_speciation=None, seed=None, progress=False) -> TraitsResult
Evolve a discrete-state trait down a tree and return a :class:TraitsResult. Two mechanisms:
- Mk (
switch=) — a continuous-time Markov chain over thestates, simulated exactly by Gillespie along every branch, so each node's(state, duration)segments are the realized history (.history) and.eventsreads off the transitions.switchis a symmetric rate (0.1), a{"marine->terrestrial": 0.1}dict, or ak×kmatrix (see :func:_q_matrix).startis the root state (a label instates;Nonedraws one uniformly). - Threshold (
liability=+threshold=) — the Wright–Felsenstein model: a discrete state read off an underlying continuous Brownian liability (variance-rateliability), cut intostatesby thethresholdcut point(s) (k−1increasing cuts forkstates).startis the initial liability (a number, default 0.0). Giveliabilityas a dict + acorrelation={(a, b): ρ}overlay to evolve correlated discrete traits jointly — their liabilities diffuse together (Σ = D R D) and each is cut by the shared thresholds. A threshold trait has no Gillespie map, so.historyisNoneand.eventsempty.
tree is the complete species tree (a :class:~zombi2.species.Tree or
:class:~zombi2.species.SpeciesResult); the trait evolves on every lineage (convention B: the root
diffuses over its own branch), and .values reads the extant tips. On an Mk trait,
at_speciation (a probability in [0, 1]) adds an on-speciation shift — each daughter hops
to a uniformly-chosen other state with that chance at every speciation. Deterministic given seed.
Source code in zombi2/traits/discrete.py
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | |
zombi2.traits.TraitsResult
dataclass
¶
TraitsResult(complete_tree: Tree, node_values: dict[int, object], events: list[Change] = list(), seed: int | None = None, kind: str = 'continuous')
What simulate_continuous / simulate_discrete returns: the complete_tree it ran on,
node_values at every node (the value at each node — extant, extinct, and internal alike; a
float for a continuous trait, a state label for a discrete / threshold one, a per-trait dict for
correlated traits), the events log, the seed, and the kind ("continuous" /
"discrete" / "threshold"). The observed dataset is the extant tips, .values.
events is the timestamped event log — the same shape as the genome level's and the source
of truth for a discrete (Mk) trait, from which history (the per-branch stochastic character
map) is derived. A continuous trait has no along-branch events, so its log holds only the
on-speciation jumps (empty without at_speciation) while node_values carries the diffusion;
a threshold trait's crossings are un-timed, so its log is empty and it has no map.
values
property
¶
The observed trait dataset — the value at each extant tip (the comparative-data
vector). Internal and extinct nodes keep their exact ancestral / lineage values in
node_values.
history
cached
property
¶
The per-branch stochastic character map — {node: [(state, duration), …]} whose
durations sum to the branch length — derived from the event log (a discrete / Mk trait
only). None for a continuous trait (a diffusion has no map) and for a threshold trait
(its liability crossings are un-timed).
write ¶
Write chosen outputs to directory (created if needed): "values" →
trait_values.tsv (the node<TAB>trait table over the extant tips); "events" →
trait_events.tsv, the event log (time · kind · lineage · from · to) — one root row
at the crown giving the initial state, then every switch in time order; "tree" →
trait_tree.nwk, the complete tree as Newick with every node annotated [&trait=…]
(a trait tree, carrying the exact ancestral values; opens in FigTree / iTOL).
trait_events.tsv is also the conditioning file: a genome / sequence run drives a rate
with mod.DrivenBy("trait_events.tsv", …), replaying it against the shared tree. A
discrete trait's log reconstructs its state on every lineage exactly (that is what the
root row and the switch times are for); a continuous trait's diffusion cannot be rebuilt
from events, so it carries only the root row and any on-speciation jumps.
Source code in zombi2/traits/result.py
zombi2.traits.Change
dataclass
¶
A realized trait change — one entry of the event log, the trait twin of the genome level's
:class:~zombi2.genomes.Event. On lineage lineage at time (crown-forward, the species-tree
clock) the state went from from_state to to_state. kind is "on_branch" — a switch
along a branch (an Mk transition) — "on_speciation" — a jump at a speciation node (from
at_speciation; for a continuous trait from_state / to_state are the pre- and post-jump
values) — or "root", one synthetic entry at the crown giving the initial state the run
started in (from_state None, time the root's birth_time). That row is what lets the
log stand on its own: the tree plus the root state plus the switches determines the trait on every
lineage at every instant, so no separate driver file is needed.
Conditioning and joining¶
zombi2.joint.simulate_joint ¶
simulate_joint(*, birth, death=0.0, trait=None, genome=None, n_extant=None, total_time=None, seed=None) -> JointResult
Grow a tree and the driver that drives its speciation, in one run (SPEC §2–4).
birth and death are rate specs (per lineage). Make either read the driver with
mod.DrivenBy(source, mapping) — a live level name (not a filename) is what makes this
joint rather than conditioned. Give exactly one driver:
trait = traits.discrete(...)— a discrete trait drives speciation (BiSSE / MuSSE), read asmod.DrivenBy("trait", {"small": 1.0, "large": 2.0}). Driving both birth and death gives state-dependent λ and μ.genome = genomes.family(...)— gene content drives speciation (P(Species, Genomes)), read as the total gene countmod.DrivenBy("genomes:count", curve)or the presence of a named familymod.DrivenBy("genomes:toxin", {"present": 2.0, "absent": 1.0})(declare it withfamily_names=["toxin"]).
::
joint.simulate_joint(
birth = 1.0 * mod.DrivenBy("genomes:toxin", {"present": 3.0, "absent": 1.0}),
genome = genomes.family(origination=0.2, loss=0.1, family_names=["toxin"]),
n_extant = 100, seed = 1)
The driver is an unexecuted process spec, grown with the tree. Stop at exactly n_extant
living lineages (conditioned on survival — a birth-death tree can die out, so it restarts,
advancing the same generator) or at total_time — give exactly one. Returns a
:class:JointResult carrying the grown tree and the driver level (.trait or .genome).
Deterministic given seed. Continuous trait→speciation (QuaSSE), clade drift (FromParent)
combined with driving, and gene transfer in a joint run are not available.
Source code in zombi2/joint/__init__.py
353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 | |
zombi2.joint.JointResult
dataclass
¶
JointResult(species: SpeciesResult, seed: int | None, trait: TraitsResult | None = None, genome: FamilyGenomesResult | None = None)
What :func:simulate_joint returns — both grown levels of a joint run. species is the
grown tree (a :class:~zombi2.species.SpeciesResult: complete_tree, extant_tree, the
speciation/extinction events); the driver level that grew with it is either trait (a
:class:~zombi2.traits.TraitsResult, for a trait→speciation run) or genome (a
:class:~zombi2.genomes.FamilyGenomesResult, for a gene-content→speciation run) — exactly one is set.
The tree is an output, grown by the driver it carries, so the levels share one complete_tree.
events
property
¶
The species events (speciation / extinction). The driver level's own events are
trait.events / genome.events.
write ¶
Write both levels to directory: the species files (species_complete.nwk /
species_extant.nwk / species_events.tsv) and the driver level's — for a trait,
trait_values.tsv / trait_events.tsv / trait_tree.nwk; for a genome,
genome_events.tsv / profiles.tsv.
Source code in zombi2/joint/__init__.py
Rates¶
Rates are written as expressions over scopes and modifiers; the same notation is
shared by the Python API, the CLI and a --params file.
zombi2.rates.scope ¶
Scope wrappers — the "per what?" of a rate (SPEC §5).
Every rate in ZOMBI2 is scope(base) × modifiers. A scope wrapper answers
per what?: it tags a base rate with the unit it applies to, so the engine can
turn that per-unit base into a total rate by multiplying by however many of
the unit are present right now::
birth = scope.PerLineage(1.0) # each lineage speciates at 1.0 -> total = 1.0 × (lineages alive)
birth = scope.Global(1.0) # one shared budget for the whole tree -> total = 1.0 (constant)
loss = scope.PerCopy(0.25) # each gene copy is lost at 0.25 -> total = 0.25 × (copies present)
Wrappers wrap a base; they do not multiply — multiplying is what
modifiers (zombi2.modifiers) do. The word "per" is reserved for these
wrappers; a modifier never starts with "per".
There is deliberately no PerGenome: one genome lives in one lineage, so
"per genome" is PerLineage.
A bare number (birth = 1.0) is coerced by each level to its natural default
wrapper — species birth/death and gene origination default to per lineage,
duplication/transfer/loss to per copy, substitution to per site. The wrappers
here are the explicit override.
Scope
dataclass
¶
A base rate tagged with the unit it applies to.
Abstract: use one of :class:Global, :class:PerLineage, :class:PerCopy,
:class:PerSite, :class:PerChromosome.
total ¶
The total rate given the current counts.
counts supplies the units in scope right now (lineages, copies,
sites, chromosomes); each wrapper reads only the one it needs and
ignores the rest. :class:Global reads none.
Source code in zombi2/rates/scope.py
Global
dataclass
¶
Bases: Scope
One shared budget for the whole system: the total does not scale with anything.
Global (capitalised — global is a Python keyword) makes a process run at a
constant total rate: linear growth, not exponential.
PerLineage
dataclass
¶
Bases: Scope
Per lineage — the total scales with the number of lineages present.
The default for species birth/death and gene origination. Within a single genome
there is one lineage, so this reads as a constant per-genome budget; across the
species tree it is base × (lineages alive) (exponential diversification).
PerCopy
dataclass
¶
Bases: Scope
Per gene copy — the total scales with family/genome size (duplication, transfer, loss).
A large family therefore turns over faster: base × (copies present).
PerSite
dataclass
¶
zombi2.rates.modifiers ¶
Rate modifiers — the context multipliers of a rate (SPEC §5).
Every rate in ZOMBI2 is scope(base) × modifiers. A modifier multiplies a rate
by a dimensionless factor that depends on context — the current time, the standing
diversity, the branch, the family, a driver level. Modifiers multiply (that is
the whole difference from scope wrappers, which wrap), and the word "per" is
reserved for scope, so a modifier never starts with "per".
You reach them through mod::
birth = 1.0 * mod.OnTime({0: 1.0, 3: 0.3}) # a skyline: 1.0, then 0.3 from time 3 on
birth = 1.0 * mod.OnTotalDiversity(cap=100) # slows to 0 as diversity approaches 100
The deterministic modifiers (OnTime, OnTotalDiversity) compute their factor as a pure
function of the context. The stochastic ones also carry a draw the engine drives with a random
generator: FromParent (the rate drifts parent→child, via initial/descend), ByLineage
(one i.i.d. draw per lineage) and ByFamily (one per family), the last two via draw.
Composition (*) belongs to the Rate module; a modifier here knows only how to produce its own
factor, or its own draw.
Modifier ¶
Base for rate modifiers.
A modifier reads the context keys it cares about (time, diversity,
branch, family, …) and returns a dimensionless, non-negative multiplier;
it ignores the rest. Abstract — use a subclass.
next_change ¶
The next time strictly after time at which this modifier's factor changes on
its own — a skyline breakpoint. inf if it never changes with time (the default;
most modifiers change only at events, not autonomously).
Source code in zombi2/rates/modifiers.py
OnTime ¶
Bases: Modifier
The rate changes in time — a skyline / episodic schedule.
schedule maps each interval's start time to a relative factor::
OnTime({0: 1.0, 3: 0.3}) # factor 1.0 on [0, 3), then 0.3 from time 3 on
Factors are relative (dimensionless): on a base of 2.0 the schedule scales it.
Before the earliest breakpoint the earliest factor applies (define the schedule
from time 0 to avoid surprise).
Source code in zombi2/rates/modifiers.py
OnTotalDiversity
dataclass
¶
Bases: Modifier
The rate slows as standing diversity grows — diversity-dependence.
The factor falls linearly from 1 toward 0 as diversity rises to cap (a carrying
capacity), and stays 0 beyond it: OnTotalDiversity(cap=100) halves the rate at 50
lineages and stops it at 100.
FromParent
dataclass
¶
Bases: Modifier
The rate drifts along the tree — each lineage inherits its parent's rate times a random
factor drawn at the split (geometric Brownian motion on the rate: clade drift at the species
level, the autocorrelated clock at the sequence level). spread (σ) sets the drift width.
The per-split factor is lognormal, mean-corrected so E[factor] = 1. Without the
correction the rate inflates down the tree (E[rate] ≈ e^{σ²/2} instead of 1) — a real
historical bug. The draw logic (:meth:initial / :meth:descend) is driven by the engine,
which threads each lineage's current factor and passes it back to :meth:factor as
inherited.
ByLineage
dataclass
¶
Bases: Modifier
The rate varies independently from lineage to lineage — an uncorrelated ("relaxed") clock.
Each lineage draws one i.i.d. multiplier with no memory of its parent (contrast
:class:FromParent, whose rate drifts parent→child). The draw is mean-corrected so
E[factor] = 1 — without it the mean rate inflates down the tree (the historical lognormal-clock
bug). spread (σ) sets the width; dist is "lognormal" (default; σ = the log-scale) or
"gamma" (σ = the coefficient of variation) — the two agree to first order in σ.
At the sequence level this is the lineage clock: the engine draws one value per species lineage
(via :meth:draw) and shares it across every gene family passing through that lineage.
It is the lineage-twin of the genome level's ByFamily.
draw ¶
One independent, mean-1 multiplier for a lineage. spread = 0 gives 1.0 (a strict clock).
Source code in zombi2/rates/modifiers.py
factor ¶
The lineage's drawn factor — the engine threads it and passes it back as bylineage.
ByFamily
dataclass
¶
Bases: Modifier
The rate varies independently from gene family to gene family.
The family-twin of :class:ByLineage, and the same i.i.d.-heterogeneity idea: each family
draws one multiplier with no memory, mean-corrected so E[factor] = 1 — so widening spread
spreads the families out without moving the average one off the base rate. dist is
"lognormal" (default; σ = the log-scale) or "gamma" (σ = the coefficient of variation).
Where you put it decides what varies together. On a single rate, that rate varies by family on its own::
loss = 0.25 * mod.ByFamily(spread=0.5) # a family that loses fast is not thereby
duplication = 0.2 * mod.ByFamily(spread=0.5) # duplicating fast — independent draws
In the family-wide family_speed= slot, one draw scales every rate that family has, so a
fast family is fast at everything::
simulate_genomes_family(tree, duplication=0.2, loss=0.25,
family_speed=mod.ByFamily(spread=0.5))
The two compose: a family-wide tempo, plus extra variation on one rate.
Not accepted on origination, which is the rate at which families are created — at the moment
it is read there is no family to have drawn a factor for. The engine rejects it rather than
quietly ignoring it.
draw ¶
One independent, mean-1 multiplier for a family. spread = 0 gives 1.0 (no variation).
Source code in zombi2/rates/modifiers.py
factor ¶
The family's drawn factor — the engine threads it and passes it back as byfamily.
DrivenBy ¶
Bases: Modifier
The rate is driven by another level — the one coupling mechanism (SPEC §2).
A coupling is Ch2's definition made literal: a parameter that reads its value from another
level instead of a number you type. DrivenBy reads the driver's value on each lineage and
multiplies the base rate by the mapped factor::
loss = 0.25 * mod.DrivenBy("habitat.tsv", {"aquatic": 3.0, "terrestrial": 1.0})
birth = 1.0 * mod.DrivenBy("trait", {"small": 1.0, "large": 2.0}) # a joint model
source says where the driver comes from, and that single choice splits conditioned from
joint — the chapter's spine, can the driver be grown first?:
- a filename (
"habitat.tsv") or a grown driver result (a discreteTraitsResult) — the driver was grown first and handed over (conditioned): two ordinary runs. The result object is the file's in-memory shortcut — same conditioning, nowrite/read step; - a level name (
"trait","genomes:count") — the driver co-evolves in one run (joint): neither level can be grown first.
mapping says how the driver's value becomes the factor — a :class:~zombi2.rates.mapping.Table
(a dict, for a discrete driver), a :class:~zombi2.rates.mapping.Curve (a callable, continuous),
or a :class:~zombi2.rates.mapping.Scalar (a log-link coefficient); a raw dict / callable / number
is coerced (:func:~zombi2.rates.mapping.as_mapping).
Like :class:FromParent (inherited) and :class:ByLineage (bylineage), DrivenBy reads
a value the engine threads per lineage — here a drivers mapping {source: value} — and
is otherwise dumb: it just maps the value to a factor. The engine owns where the value comes from
(a file it loaded, or the live level growing beside the tree) and when it changes (a discrete
driver switches mid-branch, so the engine steps its Gillespie at each switch); a rate reaching an
engine that has not threaded its source gets a factor of 1.0 (inert). DrivenBy targets a
rate (a "how often") and multiplies; it does not drive a value, such as an OU optimum.
Source code in zombi2/rates/modifiers.py
factor ¶
The mapped multiplier for this lineage's driver value — the engine threads the value under
drivers[key] (key is the source string, or the identity of an in-memory driver). No
drivers (or this source absent) ⇒ 1.0, so an unthreaded rate is inert (the engine is
responsible for supplying the value where the coupling is supported).