Click any figure for the code that produces it. Every example simulates with ZOMBI2 and plots with Phylustrator, a separate package — so to run one you need both:
pip install zombi2 phylustrator
Pure birth, no extinction — a forward tree of 100 lineages.
pure birth (Yule)The full history behind 50 survivors — their branches solid, extinct lineages dashed.
birth–deathA pulse at t = 3 culls 75% of lineages — the skyline drops sharply at the dashed line, then recovers.
mass extinction · + skylineSpeciation runs slow, then fast, then slow — the burst packs branches between the two dashed regime lines.
time-varying birthSpeciation slows as diversity fills up; the skyline rises and plateaus at the cap of 100.
birth–death · + skylineTwo thousand trees under each of two processes. zombi2 tools tree --gamma separates them almost perfectly.
Seven lineages from the three circular chromosomes drawn above the stem. Fission splits one in two, fusion joins two into one, and each is marked on the branch it happened on — so the karyotypes on the right are what those marks did, counted from the same start. Tips end with one, two or three chromosomes, and nothing about them differs except which events fell where.
phylustrator · karyotypeA genome as a ring — genes evenly spaced by rank, coloured by family, arrows by strand. plot(g, layout="circular") + genes().
Two genomes, one per row; ribbons link same-family genes and cross where the order was rearranged. stack([a,b]) + synteny().
Every tip's gene order beside the tree. Genes are coloured by their ancestral position, so each rearrangement is a break in the gradient.
phylustrator · syntenyOne family's history on the species tree: duplications (squares), losses (crosses), transfers (arrows, donor→recipient).
phylustrator · eventsA family × genome copy-number heatmap, its rows locked to the tips. beside(tree, heatmap(profiles)).
A real bacterium: Mycoplasma genitalium, 546 genes at their true base positions. The forward/reverse switch marks the replication origin.
phylustrator · real GFFOne inversion on a circular genome: the segment is reversed and its strands flip. The band marks it in both rings.
phylustrator · circularTransfers steered to run between two clades, by topology rather than by a trait. The barplot counts them by clade pair, so A↔B towers over within-clade.
clades · transfer_toTwo runs at the same mean rates. Every family alike gives no core at all; letting families differ gives 28 core families and a U-shaped spectrum.
phylustrator · heterogeneityThe shading marks the clade the run selected. Its lineages lose genes at the base rate up to the dashed line and twenty times faster after it, so the colour changes along the branches the line crosses — nothing about them differs beforehand. The bars are what each genome is left with: about 140 genes against 270 outside, all of it lost in the last third of the run. One factor, scoped to a group and to a time — chaining scaled_by with changing_at cannot say this, because the two factors would each apply to every lineage. scaled_by(clade, {'selected': {0: 1.0, 2.0: 20.0}, 'rest': 1.0}).
Every lineage draws its own rate, with no memory of its parent, so the colour is salt-and-pepper. substitution = PerSite().varying_among('lineages', LogNormal(0.0, 0.55)).
The same independent draw with a gamma instead of a lognormal. varying_among('lineages', Gamma(shape=3.31, scale=0.302)).
A daughter starts at its parent's rate and is nudged, so the colour moves in clades rather than branch to branch. substitution = PerSite().varying_among('lineages', Drift(LogNormal(0.0, 0.4))).
The same inherited drift in steps: the rate takes one of a few values and a daughter moves to a neighbouring one. varying_among('lineages', Drift(LogNormal(0.0, 0.45), bins=6)).
A small tree with its internal nodes numbered, and beside it the sequence at each. The rows are one per node, not aligned to the tips.
phylustrator · ancestralThe real M. genitalium MG_RS00730, 303 bp, evolving down twenty species. The alignment is 316 columns wide, because half the tree gained 13 bases the other half never had: the pale band at 158–170 is one insertion, and the eight species that carry it are exactly one of the two clades at the root. An indel is a shared derived character, and here it can be read straight off the topology beside it. r.alignments[block] gives the locus: the block's own columns and the runs inserted into it.
A single-copy family across 20 species, residues coloured (with a nucleotide key), each row locked to its tip. beside(tree, alignment(aln)).
Rates say how fast a lineage evolves; the model says what the change looks like. Branches are coloured by the model they run under, and the inset is what the two models differ in — one pulls its sequences toward A and T, the other toward nothing in particular. The bars are the GC content each tip arrived at, and they sit on their own model's equilibrium: 0.20 in the clade, 0.50 outside. A rate cannot say this however it is scoped, and an AT-rich branch misleads a tree-builder differently from a fast one. Models().set_by(Clade({...}), {'at': at_rich, 'rest': hky85()}).
Free diffusion — sister lineages drift apart with time.
continuousPulled to an optimum: a high start (yellow) converges to blue.
continuousA two-state trait hops between habitats; each branch is painted by its state history.
discrete · MkTwo traits evolve together (r = 0.9) — two trees, coloured by each trait, and the tip scatter.
continuous · + scatterTwo binary characters where one's flip rate depends on the other's state. X in green, Y in purple, so you can see Y is present where X is; the 2×2 chain is the model.
discrete · dependentA habitat trait sets how fast body size diffuses: twenty times faster where the habitat fluctuates. rate = PerLineage(0.25).scaled_by(habitat, {…}).
A lifestyle trait drives gene loss. Endosymbionts shed genes faster and gain fewer, so their genomes end up a fifth the size.
trait → lossA selection trait drives duplication. Under relaxed selection duplicates accumulate and the genomes grow fivefold.
trait → duplicationA competence trait drives who receives a transfer rather than a rate. Competent lineages take up DNA more often.
trait → transfer uptakeA diffusing trait drives gene gain. A Curve turns each value into a factor, so genome size follows the trait.
The same run with a different Curve. Gene gain rises with the trait and then levels off, so the factor is bounded.
The response is any function you write. Here it peaks at an intermediate value, which no table of per-state multipliers can express.
continuous trait → originationA temperature trait is grown first; body size then diffuses at a rate that reads it. The scale is centred on where it started, so white means it has not moved.
trait → traitCarrying a toxin family makes a lineage become pathogenic forty times faster. 74% of the tips with the gene end up pathogenic, against none of those without.
gene → traitHow much of a four-gene module a lineage keeps decides its metabolism, through a step: lambda f: 20.0 if f > 0.5 else 1.0.
A gene family drives the substitution rate. The same tree in time, then in substitutions: branches that have lost the mismatch-repair family run four times longer.
gene → substitutionThe same target reached from the other end of the map. A driver is read wherever it changes, so a lineage that switches halfway down accumulates at two rates.
trait → substitutionOne level conditioning itself. Carrying the element makes a lineage donate twenty-five times more often — a driven transfer is about giving, not receiving.
gene → transferHot lineages invert fifteen times as often, and each inversion takes a longer run of genes. Rate and extent are separate targets and multiply.
trait → inversionThe driver is a fraction, not a yes/no. At the ordered resolution a loss takes a run of neighbours, so an operon goes in blocks and completion drops in steps.
module → substitutionThe repair operon again, now setting how fast a trait diffuses instead of how fast sequences evolve. One driver reaches every level on the tree.
module → trait rateA sequence drives a sequence. It cannot drive the gene it grows inside — that would read a run’s own output — but it can drive a different one, run after it.
GC → substitutionThe only pair whose driver is grown at the sequence level and whose target is not. gc() is a number, so the connection carries a Curve.
Gene content drives speciation, so the tree is an output. Loss runs at twice origination — on a tree that does not feel it the family reaches ~9% of tips; here it reaches 83%, because its carriers split five times as often.
gene content → speciationA two-state trait drives speciation — the fast state's clades take over; the inset is the state Markov chain.
trait → speciationOne state dies far faster; the doomed lineages (dashed) drop out.
trait → extinctionThree graded speciation rates with constant death — the fastest state fills the tree, extinct lineages dashed.
trait → speciation