Joining
Joint runs¶
Levels can also be simulated jointly, as the figure shows. Two pairs reach the species tree: a trait or the gene content drives speciation, and the tree is then a result of the run. Three pairs join two different levels along a tree that is handed to the run: a trait and a genome, a trait and a sequence, a genome and a sequence. In the remaining three cases the two joined parts belong to the same level: a gene family changes the rates of the genome that carries it, one trait changes how another trait evolves, and each of two genes changes how fast the other one evolves. A joint run holds exactly two parts, and asking for a third is refused. The species tree and a sequence cannot be joined: a sequence evolves along a gene tree, which evolves along the species tree, so joining the two would require simulating the genome as well.
Naming the driver¶
The connections of a joint run are written with the same verbs and mappings as everywhere else. The difference is the driver. It cannot be passed as a finished result, because it does not exist yet, so it is named instead: "trait", "genomes:toxin", "sequences:rpoB". The name refers to another level of the same run, and the run grows both together. A run with a single trait can call it plainly "trait"; names exist so that a run can hold two. Appendix C lists the named drivers and what each one offers.
A family whose presence drives must be declared in the genome spec, with families=[family("toxin")]. A gene whose composition drives a rate must declare it with offers=composition("KR", absent=0.02): which letters are counted, and the value used on lineages that carry no copy. A gene that drives a rate usually also declares start=, a second substitution model whose stationary frequencies are used only to found the gene. The gene starts with that composition and then evolves under its own model, so its composition moves toward its own equilibrium. Without start= a gene is founded at its equilibrium, its composition barely changes, and the rate that depends on it stays constant (Jo2).
A joint run is written from the specs of the levels it simulates, for example joint.simulate(species.birth_death(...), traits.discrete(...)). If the species spec is present, the tree is simulated, and the stop condition (n_extant=, total_time=) is written on that spec; if it is not, the tree is passed with tree= and the run ends where the tree ends. The result is a JointResult that carries both simulated levels, the same result objects the ordinary functions return, and both levels write their usual files. When the two joined parts belong to the same level, the run stays on that level's own function, with joint=True: simulate_traits, simulate_genomes_family or simulate_sequences. The flag is checked both ways: asking for it without a live driver is an error, and depending on a live driver without it is an error. One more restriction: a transfer needs the set of lineages alive at an instant, which is unknown while the tree is still being simulated, so a genome run that grows the tree refuses transfer. The gallery presents every joint model in full, with its code (Jo1 to Jo12).
Exact or sliced¶
A joint run advances by racing every possible event against every other, with the same Gillespie algorithm as everywhere in ZOMBI2. A discrete driver changes only at events, so between two events every rate is constant and the race is exact. When a speciation fires, it uses the trait state of the lineage it happens on; when a trait switches, the tree is untouched; both daughters of a split start from the parent's state.
Two kinds of driver change between events: a continuous trait diffuses at every instant, and a composition moves with every substitution. There is no interval over which the driven rate holds still, so there is nothing exact to race. These models slice time instead. Time is cut into steps of step, and the driver is held fixed within each slice, so the rates are constant inside it. At the slice boundary the driver moves: a diffusing trait by its exact transition law, a composition by the substitutions that accumulated. The approximation is only in the timing: the target uses the value the driver had at the start of the slice.
| Model | Tree | Advances by |
|---|---|---|
| a trait drives speciation | simulated | events, exact |
| gene content drives speciation | simulated | events, exact |
| a diffusing trait drives speciation | simulated | slices of step |
| a trait and a genome | handed | events, exact |
| two traits, each driving the other | handed | events, exact |
| a trait and a gene's sequence | handed | slices; the composition is taken at the start of the slice |
| a genome and a gene's sequence | handed | slices; the composition is held within a slice |
| a gene family drives its own genome | handed | events, exact |
| two genes, each depending on the other | handed | slices; both compositions held within a slice |
step is written on the link, because the right size depends on how the value is taken: a steep curve needs a finer step than a flat one. In a joint run it has no default, and leaving it out is an error, because the timescale belongs to a model that does not exist yet. The practical check is to halve step, rerun, and confirm that the numbers you report move by less than their seed-to-seed spread (Jo12).
On the command line¶
zombi2 joint covers the two models that drive speciation from a discrete driver. The rate is written exactly as in Python, and the remaining flags are the ones zombi2 traits or zombi2 genomes would take. Giving flags from both levels at once is an error.
zombi2 joint out/ --death 0.2 --states small,large --switch 0.3 \
--n-extant 100 --seed 1 \
--birth "PerLineage(1.0).scaled_by('trait', {'small': 1.0, 'large': 3.0})"
zombi2 joint out/ --origination 0.2 --loss 0.1 --family-names toxin \
--n-extant 60 --seed 1 \
--birth "PerLineage(1.0).scaled_by('genomes:toxin', {'present': 3.0, 'absent': 1.0})"
The other models are Python only, deliberately: each needs something no flag can carry, such as a curve, a rate for a single named family, a list of specs, or a start model built as a matrix.
Literature¶
The state-dependent diversification models are usually known by their acronyms. This table gives the correspondence.
| What it does | From the literature | Gallery |
|---|---|---|
| a binary trait drives speciation (and extinction) | BiSSE (Maddison et al. 2007) | Jo8 |
| a multi-state trait drives speciation | MuSSE (FitzJohn 2012) | Jo10 |
| a trait drives speciation and jumps at the split | ClaSSE (Goldberg & Igić 2012) | Jo11 |
| a continuous trait drives speciation | QuaSSE (FitzJohn 2010) | Jo12 |