Coming from ZOMBI v1¶
ZOMBI2 is a rewrite, not a new release of ZOMBI. Nothing carries over automatically.
The commands¶
v1 took a mode letter, a parameters file and an output directory:
python3 Zombi.py T ./Parameters/SpeciesTreeParameters.tsv ./Output_folder
python3 Zombi.py G ./Parameters/GenomeParameters.tsv ./Output_folder
python3 Zombi.py S ./Parameters/SequenceParameters.tsv ./Output_folder
v2 names the run directory once per command, and each level finds the previous one in it. v1's three
*Parameters.tsv files become one TOML file, with a table named for each command. A key outside any
table is shared by every command; the command's own table overrides it.
# zombi2.toml
seed = 1
[species]
birth = 1.0
death = 0.2
n-extant = 20
[genomes]
duplication = 0.1
transfer = 0.2
loss = 0.3
origination = 0.2
[sequences]
model = "wag"
length = 100
divergence = 0.2
zombi2 species out/ --params zombi2.toml
zombi2 genomes out/ --params zombi2.toml
zombi2 sequences out/ --params zombi2.toml
Every key is a long option of that command, hyphens or underscores alike, so the same run can be written as flags with no file at all. A flag on the command line overrides the file.
zombi2 species out/ --birth 1.0 --death 0.2 --n-extant 20 --seed 1
zombi2 genomes out/ --duplication 0.1 --transfer 0.2 --loss 0.3 --origination 0.2 --seed 1
zombi2 sequences out/ --model wag --length 100 --divergence 0.2 --seed 1
There are no mode letters and no aliases for them: zombi2 T is an error naming the six commands. The
levels are species, genomes, sequences, traits, plus joint and tools.
Parameters: species trees¶
Every rate takes the same written form in the flag, in the file and in Python.
| v1 key | v2 |
|---|---|
SPECIATION |
--birth |
EXTINCTION |
--death |
STOPPING_RULE |
gone as a key: pass --total-time (v1's rule 0) or --n-extant (rule 1), never both |
TOTAL_TIME |
--total-time |
TOTAL_LINEAGES |
--n-extant — and it is conditioned on survival: a run that dies out is retried |
MAX_LINEAGES |
--max-lineages (default 100000). It raises rather than truncating: a tree cut off at a size is not a sample from the process you asked for |
MIN_LINEAGES |
gone. v1 called a run a failure below it; v2 refuses only the degenerate case, a run with nothing alive at the present |
MASS_EXTINCTION |
--mass-extinction TIME FRACTION, repeatable |
SCALE_TREE |
zombi2 tools tree TREE --rescale-height H or --rescale-factor F |
VERBOSE |
--quiet (inverted) |
SEED |
--seed |
SHIFT_SPECIATION_RATE_FREQUENCY, NUM_SPECIATION_RATE_CATEGORIES, BASE_SPECIATION (and the EXTINCTION trio) |
the rate grammar: --birth "PerLineage(1.0).varying_among('lineages', Drift(LogNormal(0.0, 0.2)))" for a rate inherited and nudged at each split, changing_at for a schedule, scaled_by(TotalDiversity(cap=N)) for a ceiling. v2 has no category count — the drift is continuous |
TURNOVER |
no equivalent. v2 parameterises birth and death directly |
LINEAGE_PROFILE |
no direct equivalent. The nearest thing is --birth "PerLineage(1.0).scaled_by(TotalDiversity(cap=N))", which levels diversity off at a ceiling |
v1 encoded a distribution choice as a prefix on the value (f:1, l:1, n:0.7;0.2). v2 writes it as
part of the rate instead, in one notation shared by the flag, the --params file and Python:
from zombi2 import species
from zombi2.params import Drift, LogNormal, PerLineage
species.simulate_species_tree(birth=1.0, death=0.2, n_extant=20, seed=1) # fixed
species.simulate_species_tree(birth=PerLineage(1.0).varying_among('lineages', Drift(LogNormal(0.0, 0.2))), n_extant=20, seed=1) # drifting
Parameters: genomes¶
| v1 key | v2 |
|---|---|
DUPLICATION · TRANSFER · LOSS · ORIGINATION |
--duplication · --transfer · --loss · --origination. D/T/L are per copy, origination per lineage |
INVERSION · TRANSPOSITION |
--inversion · --transposition, with --resolution ordered or nucleotide |
DUPLICATION_EXTENSION and the other *_EXTENSION keys |
--duplication-extent and friends, on --resolution ordered (in genes, default 1) or --resolution nucleotide (in base pairs, default 50). Only a driven extent is Python-only |
REPLACEMENT_TRANSFER |
--replacement |
ASSORTATIVE_TRANSFER |
--transfer-to distance — nearest equivalent, not identical: v2 weights each candidate by exp(-decay × d / depth) |
INITIAL_GENOME_SIZE |
--initial-families (default 100) |
| (no v1 key) | --max-family-size (default 10) is new, and on by default: a cap on how many copies of one family a genome may hold. It is Poisson thinning, so while it binds the realised duplication and transfer rates fall below the ones you declare — porting v1 rates straight across is where a returning user meets it. The run warns on standard error when it actually bound; --max-family-size none removes it |
EVENTS_PER_BRANCH |
gone, and there is no lineage column to replace it: a participant carries its own branch, so a gene copy reads n<species>_g<copy>. Split a token on its single _ and group on the left half — see What will break in your parsing code below |
PROFILES · GENE_TREES |
--write profiles gene_trees |
RECONCILED_TREES |
zombi2 tools format DIR --format recphylo — recPhyloXML, not Newick, so this needs a new parser |
RATE_FILE · SCALE_RATES |
the rate grammar. For per-family variation — what these keys were usually for — that is varying_among('families', …): --loss "PerCopy(0.25).varying_among('families', LogNormal(0.0, 0.5))" draws one factor per family. From Python, one Random('families', …) object shared by several rates gives a family a single tempo across all of them. changing_at varies a rate in time instead, and scaled_by scales it by a value from another level |
GENE_LENGTH · INTERGENE_LENGTH |
--gene-length · the spacer is what lies between genes on --resolution nucleotide |
MIN_GENOME_SIZE |
not a setting. On --resolution ordered and nucleotide a loss never takes a chromosome below its last gene: the run that would empty it does not fire. That is a floor on a chromosome, not on a genome — at ordered a chromosome_loss can still take the only chromosome that had genes on it. On --resolution family there is no floor at all: a high loss rate empties a genome completely, and the run says so — empty_genomes in genome_summary.json, and a line on standard error |
ALPHA |
no equivalent as a genome parameter |
PSEUDOGENIZATION |
not in v2 |
Parameters: sequences¶
| v1 key | v2 |
|---|---|
SEQUENCE_SIZE |
--length |
AA_MODEL |
--model wag (also jtt, dayhoff, lg, poisson) |
the AC…TG and A/C/G/T keys |
--model gtr --exchangeabilities … --frequencies …, or hky85 / k80 / jc69 |
KAPPA |
--kappa |
ST_RATE_MULTIPLIERS · GF_RATE_MULTIPLIERS |
the lineage clock: --substitution "PerSite(1.0).varying_among('lineages', LogNormal(0.0, 0.3))" (uncorrelated) or varying_among('lineages', Drift(LogNormal(0.0, 0.3))) (autocorrelated) |
SHIFT_SUBSTITUTION_RATE · SHIFT_CATEGORIES · BASE_RATE |
the same clock. No category count |
SCALING · SCALE_GENE_TREES |
--divergence D sets the rate from the height of the tree, which is what scaling was for |
SEQUENCE codon · CODON_MODEL |
not in v2. There are no codon models, so no dN/dS |
ALPHA |
--gamma-shape A — the Gamma shape for rate variation across sites, with --rate-categories N for the class count (4 by default). It decorates the model, not the rate |
BETA |
--invariant P — the proportion of sites that never change (+I), if that is what it set; v1's key is not documented here, so check your old control file |
Where the files went¶
| v1 | v2 |
|---|---|
T/CompleteTree.nwk |
species/species_complete.nwk |
T/ExtantTree.nwk |
species/species_extant.nwk |
T/Events.tsv |
species/species_events.tsv |
| — | species/species_fates.tsv — new, and the only place that tells an unsampled tip from an extant one |
G/Events.tsv |
genomes/genome_events.tsv |
G/Profiles.tsv |
genomes/profiles.tsv |
G/Gene_families/<n>_completetree.nwk |
genomes/gene_trees/gene_tree_fam<n>_complete.nwk |
G/Gene_families/<n>_prunedtree.nwk |
genomes/gene_trees/gene_tree_fam<n>_extant.nwk — "pruned" is now "extant" |
G/<node>_GENOME.tsv |
genomes/genomes.tsv, one table for every node rather than a file each |
S/SubstitutionScaledCompleteTree.nwk |
sequences/clock_species_tree_complete.nwk |
S/<n>_substitution_scaled.nwk |
sequences/phylograms/phylogram_fam<n>_complete.nwk |
| — | sequences/alignments/fam<n>.fasta |
| — | <level>.log — version, timestamp, command line, every resolved parameter, and the SHA-256 of each input file |
| — | species_summary.json · genome_summary.json · sequences_summary.json · trait_summary.json — what came out at that level (the genomes one: deduplicated event counts, families born and surviving, whether the family-size cap bit) |
--flat writes everything into one directory, but the v2 names stay: it is a layout switch, not a
compatibility mode.
What will break in your parsing code¶
This is the part that takes the time. Six changes each break a script, silently or loudly:
Event vocabulary. v1 wrote letter codes; v2 writes words: speciation, extinction,
duplication, loss, origination, and two transfer kinds, transfer_additive and
transfer_replacing.
One row per event. v1's genome log was positional, with a NODES field whose meaning depended on
the event type. genome_events.tsv is now five columns, and one row is one thing that happened:
time kind family parents children
0.0 origination 0 n0_g0
0.281867 speciation 14 n0_g14 n1_g15;n2_g29
0.2959603504961474 duplication 4 n1_g19 n1_g43;n1_g44
0.11415202408218499 loss 0 n0_g0
1.2866954826434076 transfer_additive 8 n1_g23 n1_g255;n5_g256
0.36393613420080373 transfer_replacing 1 n2_g30;n1_g16 n2_g47;n1_g48
parents is what the event ended and children what it began, packed with ; where there are two;
an origination has no parents and a loss no children. Counting rows by kind now counts events, so
grep -c gives you a number you can use. species_events.tsv and chromosome_events.tsv have the
same shape. The exception is a nucleotide run's block_events.tsv, which is keyed by ancestral
interval, so one event can write several rows.
Names carry the branch. A gene copy is written n<species>_g<copy>, so n2_g30 is copy 30 on
branch n2. That is why the lineage, recipient and donor columns are gone: split a name on its
single _ and you have both halves. Lineages in species_events.tsv (n0 → n1;n2) and
chromosomes in chromosome_events.tsv (n<species>_c<id>) follow the same pattern. A transfer's
children read donor first, recipient second, so one row says which way the material went.
A replacing transfer writes no loss row. transfer_replacing has two parents, the donor's copy
and the copy it overwrote, and no separate loss row for the one it displaced. A script that counts
losses straight out of the log therefore comes up short by exactly the number of replacing transfers.
genome_summary.json counts the biology instead. This is the change that gives a plausible wrong
number rather than an error, so check it first.
Rearrangements are their own file. An inversion, transposition or translocation begins and ends
no gene lineage, so it has nothing to put in parents and children. At the ordered and nucleotide
resolutions they live in rearrangement_events.tsv, which is the one place a branch is still a
column, because a segment has no name to carry one.
Label prefixes. A lineage that went extinct is e<id>, not n<id>, in the complete tree and the
event logs — including inside a copy name, as e6_g138. So an n\d+ pattern silently drops every
extinct lineage. Anything naming only extant tips (the extant tree, profiles.tsv, the alignments)
is n<id> throughout.
The full column list for every file is in Output files.