Skip to content

Output files

One table per level, listing the files it writes: the file, and what it holds. Every file listed is written with no arguments unless its line says otherwise; a few are written only when you name their token, one of the short names outputs= and --write take, events, alignments or ancestral. Those lines say so, naming the token where it is not simply the file's own name. What a run gives back in Python follows each table: the objects and accessors, which are not files. Trees are Newick, tables and logs TSV, sequences FASTA; branch lengths are time, except in the sequence phylograms, which are in substitutions per site.

Where a file needs more than a phrase, it gets a paragraph under its table, in the table's own order.

Columns may be added; read them by name. A later version may add a column to any of these tables, because a new kind of event needs somewhere to say what it did. What will not change is an existing column's name or its meaning, and no column is removed within a major version. So a script that reads a column by its header keeps working across upgrades, and one that reads by position, or assumes a column count, does not. New columns go at the end of the row. The same file already differs by resolution, since genome_events.tsv has five columns at the family resolution and ten at the ordered one, so matching by name is what these tables have always required.

Where the files go

out/run.zombi2              the run report: one page for the whole run
out/joint_summary.json      the joint run's own file                          (joint runs)
out/species/                species_{complete,extant}.nwk · species_events.tsv · species_fates.tsv
out/species/                species_fossils.tsv                               (fossil sampling)
out/genomes/                genome_events.tsv · initial_genome.tsv
out/genomes/                genomes.tsv                                       (family)
out/genomes/                profiles.tsv                                      (family, ordered)
out/genomes/                rearrangement_events.tsv · chromosome_events.tsv  (ordered, nucleotide)
out/genomes/                gene_order.tsv                                    (ordered)
out/genomes/                block_events.tsv · blocks.tsv · genes.tsv         (nucleotide)
out/genomes/                initial_sequence.fasta                            (nucleotide + fasta)
out/genomes/                names.tsv                                    (--from with own labels)
out/genomes/markers.tsv     the marker table, one file for the run: Appendix D  (via zombi2 tools format)
out/genomes/gene_trees/     gene_tree_fam<f>_complete.nwk · …_extant.nwk
out/genomes/                gene_trees.tsv, in place of gene_trees/          (--compact)
out/genomes/gff/            genome_<lineage>.gff · genome_initial.gff         (nucleotide)
out/genomes/bed/            genome_<lineage>.bed · genome_initial.bed         (nucleotide)
out/genomes/homology/       homology_fam<f>.tsv                          (via zombi2 tools format)
out/genomes/recphylo/       recphylo_fam<f>.xml                          (via zombi2 tools format)
out/sequences/              clock_species_tree_complete.nwk · …_extant.nwk
out/sequences/              sequence_events.tsv                          (record=True)
out/sequences/              sequences_founding.fasta                     (the founding token)
out/sequences/alignments/   fam<f>.fasta
out/sequences/ancestral/    sequences_ancestral_fam<f>.fasta             (the ancestral token)
out/sequences/phylograms/   phylogram_fam<f>_*.nwk
out/sequences/              alignments.fasta · ancestral.fasta · phylograms.tsv  (--compact)
out/sequences/genomes/      genome_<lineage>.fasta · genome_initial.fasta  (nucleotide runs)
out/traits/                 trait_values.tsv · trait_tree.nwk · trait_events.tsv · trait_path.tsv
out/traits/<name>/          the same files, when --name was given
out/<level>/conditioned_on  the driver record                            (conditioned runs)

Two rules hold everywhere before any table. A lineage is written n<id>, and e<id> once it goes extinct, in every file including the trees, while an unsampled tip keeps its n<id>; a gene copy is n<species>_g<copy> on the same convention. And a run whose tree came from --from keeps a copy of that tree, with species_fates.tsv beside it, in the run's own species/ directory, so the directory stands alone for whatever reads it next.

run.zombi2: the run report, plain text for reading rather than parsing. One section for each level in the directory, holding the seed, the parameters as resolved, what came out in numbers, and each file with a one-line gloss; then a closing TO REPRODUCE with the exact commands, seeds filled in, that rebuild the whole chain. Each command rewrites it, so it always describes the directory as it stands.

Each level directory also holds that command's log (species.log, genomes.log, sequences.log, traits.log) with the version, the command line and every resolved parameter, rates in their written form. Beside it, a summary of what came out: species_summary.json, genome_summary.json, sequences_summary.json, trait_summary.json, written at every level, and at all three genome resolutions, except by a streamed family run (zombi2 genomes --stream at the family resolution), which writes each family straight to disk and never holds the whole run to summarise. A streamed ordered run writes it. A streamed run also keeps its own species_complete.nwk beside its tables. It still reads the tree from species/, as any genomes run does; the copy is so that the genomes/ directory stands alone as the handoff a later level or another tool reads.

--flat writes every file straight into the run directory instead, except two that a flat run does not write at all: run.zombi2 and conditioned_on. Both are built from a grouped run's level directories, which a flat run does not have.

--compact writes each per-family output as one compact file: gene_trees.tsv, alignments.fasta, ancestral.fasta and phylograms.tsv replace gene_trees/, alignments/, ancestral/ and phylograms/. This reduces the file count on clusters, which often limit how many files a user stores. --compact is off by default; from Python it is result.write("out/", compact=True). zombi2 tools compact and zombi2 tools expand convert a written run from one layout to the other (Appendix D).

The grouping above is the CLI's. A Python run has no level directories: result.write("out/") fills the directory you name with that result's files, the per-family and per-node directories included, so it is not --flat, which flattens those too. The same run written from Python and from the CLI therefore leaves two different layouts, and run.zombi2 and conditioned_on are written by the CLI alone. To group a Python run, name the level directory yourself: result.write("out/species/").

Every directory a run fills with one file per family or per node (gene_trees/, gff/ and bed/ at the genome level, alignments/, ancestral/, phylograms/ and genomes/ at the sequence level) is emptied before that run fills it, so its contents describe one run and nothing else. That step also removes a compact file left by an earlier run, such as gene_trees.tsv. So a level directory never holds both layouts. Two exceptions: a --flat run empties nothing, and the homology/ and recphylo/ directories that zombi2 tools format writes are left as they are.

Species trees: simulate_species_tree

File What it holds
species_complete.nwk every lineage, extinct and unsampled included
species_extant.nwk only the sampled survivors
species_events.tsv every speciation and extinction, one row each
species_fates.tsv each tip's resolved fate: lineage · fate (extant / extinct / unsampled)
species_fossils.tsv sampled fossil lineages: lineage · time. Only if fossil sampling recovered any
species_summary.json counts by fate, tree height, stem, total branch length, the realised birth and death rates, the seed

From Python: .complete_tree · .extant_tree (trees), .fossils (the sampled fossils and their times, present when the run asked for them), .lineage_rates(kind) (a dict).

species_events.tsv: time · kind · parents · children. A speciation row is the lineage that ended and its two children, ;-packed (n0 → n1;n2); an extinction row is the dying lineage as the parent with no children. A lineage that died is written e<id>.

.complete_tree and .extant_tree. The whole tree that grew, extinct lineages and all, is what the next level runs along, which is what lets a gene be transferred out of a lineage that later dies; the extant tree is the survivors', dated and bifurcating, the one an analysis would use as input. Each holds every node, internal ones included, in .nodes, and answers .leaves(), .extant_leaves(), .extinct_leaves() and .unsampled_leaves().

.lineage_rates(kind): {lineage: rate}, the birth (or "death") rate each lineage itself ran under, taken at the lineage's birth, which is when its factors are drawn. Every lineage of the complete tree has one, extinct ones included. A rate that varies among lineages gives each a different number; one that does not gives the same number for all of them. Under a rate that also depends on time or diversity, this is its value at the start of the branch.

Genomes, family: simulate_genomes_family

File What it holds
genome_events.tsv the run's true history: time · kind · family · parents · children, one row per event
profiles.tsv family × extant-species copy counts
genomes.tsv every node's gene content, ancestors included: lineage · family · copy, one row per gene copy
initial_genome.tsv the genome the run started with, at the start of the root branch: family · copy
gene_tree_fam<f>_complete.nwk · …_extant.nwk each family's true genealogy, in genomes/gene_trees/
gene_trees.tsv every family's complete and extant gene trees, one row per family, in place of gene_trees/. Only with --compact
genome_summary.json events by kind, families born, surviving and died out, genes per genome, empty_genomes, whether the family-size cap bound, the seed
links.tsv the links the run read from its own gene content: family · target · driver · modifier · mapping, one row per link
family_multipliers.tsv each family's rate multipliers: family · duplication · transfer · loss, one row per family
lineage_multipliers.tsv each species branch's rate multipliers: lineage · duplication · transfer · loss · origination, one row per branch
species_complete.nwk the tree the run evolved along, without which the directory cannot be read
species/species_fates.tsv each tip's fate, in the format the species level writes. Only when the tree came from --from
names.tsv node · name, mapping ZOMBI2's n<id> back to the labels you supplied. Only when the tree came from --from with its own tip labels
conditioned_on the levels this run depends on as a driver, one per line. Only when something was conditioned

From Python: .genomes · .node_genomes (the genomes), .family_counts(node) · .has_family(node, name) (a node's genome as family → copies, and whether a named family has a copy there), .gene_trees[f].origination (when a family was founded, where its gene tree's root branch begins), the driver views .presence(name) · .completion(name), and .gene_trees · .profiles · .initial_genome · .links · .family_multipliers · .lineage_multipliers · .events · .seed, the same objects the files hold before .write() puts them on disk.

genomes.tsv: copy is the identifier the event log uses, so a gene can be traced back to the event that made it. profiles.tsv is the same information counted, for the extant tips only.

initial_genome.tsv has no lineage column because it is no node's genome: every node sits at the end of its branch.

gene_tree_fam<f>_*.nwk: leaves are n<species>_g<copy>; internal nodes are labelled <event>_n<species> (duplication_n45, transfer_n45), naming the event that ended that gene and the branch it was on. A family with no surviving copy writes no _extant file.

gene_trees.tsv: family · complete · extant, tab-separated, one row per family in family order. Each cell holds the tree that the matching per-family file holds. The extant cell is empty when no copy survived.

links.tsv lists every modifier that reads gene content the run is building ("genomes:<family>", "genomes:module:<group>", "genomes:count"). family is the declared family whose parameter it is, or run for the run's own; target is that parameter (loss, transfer_to, …); modifier is scaled_by, set_by or weighted_by. mapping is state=value pairs joined by ;, or donor>receiver=weight pairs for a Between, with default= when the default is not 1. A function read on a module is written at each completion level, k/n=value; any other function is written by its name. A driver read from a trait or a file is not listed. A run with no link writes the header alone, so a method that looks for links between gene families can always be compared against the file.

family_multipliers.tsv holds each family's multipliers. A rate written with varying_among("families", …) draws one value for each family when the family is born. The family keeps that value for its whole life. The multiplier is that value divided by the law's mean, so multipliers have an expected value of 1. The family's duplication rate is then the run's duplication rate times the family's duplication multiplier. In a run where some rate varies among families, a rate that does not vary gives a multiplier of 1.0. When a declared family sets its own value for a rate, its cell for that rate is empty, because that value replaces the run's rate. A run whose rates do not vary among families writes the header alone.

lineage_multipliers.tsv holds each species branch's multipliers. A rate written with varying_among('lineages', …) draws one multiplier for each branch of the species tree, before the run starts. That multiplier scales the rate of every family on the branch, so one branch speeds up or slows down a whole genome. A family multiplier varies the rate among families instead: one family's multiplier applies on every branch. A rate can carry both draws. A family's rate on a branch is then the run's rate times the branch's multiplier times the family's multiplier. Every branch has a row, extinct ones included. The lineage column holds the branch's label, the name that genomes.tsv and the species tree use. A rate that does not vary among lineages is 1.0 on every branch. A run whose rates do not vary among lineages writes the header alone. On transfer the multiplier belongs to the donor: it scales how often that branch donates, and transfer_to still chooses the recipient. An ordered run writes the same file, with a column for each rearrangement and each chromosome event as well. A branch's multiplier scales whatever the event acts on. A family's multiplier applies to the genes a segment covers, so it is on the per-gene events only, and never on an event that acts on a whole replicon.

species_complete.nwk. Every other file here is indexed by its node labels, so the directory is not readable, by anyone or by genomes.read_run(), without it. result.write() writes it by default, so a directory written from Python stands alone; zombi2 genomes leaves it out, except under --stream, because a run already keeps one copy at species/species_complete.nwk, shared by every level. Its token in outputs= and --write is species_tree, not the file's name.

names.tsv: the join from every other output back to your taxa.

conditioned_on is written whether the driver was used for a rate or for transfer_to. Re-running a driver level then refuses rather than leaving this run stale, unless you pass --force.

.genomes and .node_genomes. .genomes is the observed dataset, the genome at each extant tip, keyed by the tip name the tree writes (n5), so it joins to the tree and to a trait grown on the same tree. .node_genomes is the run's own record, every node, extant and extinct and internal alike, keyed by node id, for joining against .complete_tree.nodes or the event log. The pair is the same at every resolution, and the distinction is the model's: one is what a dataset contains, the other is what happened.

.presence(name) and .completion(name): a named family's presence (present / absent) and a declared module's completion (a fraction) along every lineage, for use as a driver (Ch8). Read off the families' gene trees, so they change inside a branch; the ordered resolution gives the same two.

One row per event

Every event log opens with time and kind, then the payload columns for that file, the same on every row. Here they are family, parents and children: the gene copies the event ended and the ones it began, ;-packed.

A gene copy is written n<species>_g<copy>, so copy 30 on branch n2 is n2_g30, and e2_g30 if that lineage went extinct. The branch is part of the name, so the file needs no lineage, recipient or donor column: a transfer is one row naming the donor's copy and the copy that arrived.

Kind parents children
origination — the founding copy
duplication the copy that ends its two descendants, both on the same branch
speciation the copy that ends at the split one copy in each daughter species
loss the copy that ends —
transfer_additive the donor's copy the continuation on the donor branch, then the copy that arrived
transfer_replacing the donor's copy, then the copy it overwrote the continuations of those two lineages

One row of each kind, from a real run:

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

A transfer's children read donor first, recipient second, so the row alone says which way the material went: the transfer_additive above left n1 and landed on n5. A transfer_replacing carries two parents, the donor's copy and then the copy it overwrote on the recipient branch, and writes no separate loss row for the copy it displaced. That death is what the kind means.

The file is time-ordered, one row per event, so counting rows by kind counts events, except at the ordered resolution, where a segmental event writes one row per gene it covers; dedupe on time there before counting (the ordered section's Segment note). An absent value is an empty cell. genome_summary.json counts the biology instead, and differs in three places: it reports transfer as one number over both kinds, it counts each replacing transfer's displaced copy under loss, and it separates the families the run started with (initial) from the ones the origination rate made. It also reports empty_genomes, the extant genomes that came out with no genes at all, which are easy to miss: their column of profiles.tsv is all zeros, they have no row in genomes.tsv, and no _extant gene tree names them.

Speciation is the largest kind in the file, and those rows are not redundant. A gene tree labels its internal nodes speciation_n14, with no copy id, so this log is the only record of the internal gene copies and their parentage. In Python an Event is one event; the per-edge record is a GeneEdge, and zombi2.genomes.events.edges_from_tsv expands each row into one per edge, which is what the gene trees are built from.

Genomes, ordered: simulate_genomes_ordered

File What it holds
genome_events.tsv the gene genealogy, with the place each event happened
rearrangement_events.tsv every inversion, transposition and translocation
profiles.tsv family × extant-species copy counts
gene_order.tsv signed gene order of every node, ancestors included: lineage · chromosome · topology · position · strand · family · copy
chromosome_events.tsv chromosome-network edges: time · kind · parents · children
initial_genome.tsv the genome the run started with: chromosome · topology · position · strand · family · copy
gene_tree_fam<f>_complete.nwk · …_extant.nwk as at the family resolution, since position is orthogonal to genealogy
gene_trees.tsv as at the family resolution. Only with --compact
genome_summary.json events as biology rather than rows, families born/surviving/died out, genes and chromosomes per genome, rearrangements and chromosome events by kind
links.tsv as at the family resolution; target can also name an extent, such as loss_extent
family_multipliers.tsv as at the family resolution, with an inversion, a transposition and a translocation column
lineage_multipliers.tsv as at the family resolution, with a column for each rearrangement and each chromosome event too
species_complete.nwk as at the family resolution
names.tsv as at the family resolution
conditioned_on as at the family resolution, and written when a rate or transfer_to was conditioned

From Python: .genomes · .node_genomes (as at the family resolution, but each genome is a tuple of Chromosome objects, each an id, a topology, and an ordered list of Gene objects with id, family and strand), .gene_order(node) (one node's layout gene by gene, as (chromosome, position, strand, family, gene id)), and .rearrangements · .chromosome_events · .gene_trees · .profiles · .links · .family_multipliers · .lineage_multipliers in memory. A run streamed to disk (stream_to=DIR, or --stream) writes the same files and gives back a StreamedRun instead: the directory, the seed, how many families and gene-tree edges the run made, and the outputs written.

genome_events.tsv. The family resolution's five columns plus five more: time · kind · family · parents · children · chromosome · start · length · dest_chromosome · dest_position. Kinds and copy names are the shared ones. Coordinates are in that branch's own genome just before the event, as gene_order.tsv numbers it. dest_position and dest_chromosome are where the material landed, so one row carries both ends of a transfer. A speciation copies a genome whole and leaves all five coordinate cells empty.

rearrangement_events.tsv: time · kind · lineage · chromosome · start · length · dest_chromosome · dest_position · flipped. These begin and end no gene lineage, so they have no parents and children and get a file of their own; a segment has no name either, which is why this is the one event log that still puts its branch in a column. dest_chromosome is set only by a translocation; flipped is 1 or 0 on the two events that move a segment, which may land inverted, and empty on an inversion, which flips by definition. The events token writes this file and the log above together.

A segment, in both of those files, is named by start (its first position, in the chromosome's frame just before the event) and length (how many genes it covered), counted rightwards from start and wrapping past position 0 on a circular chromosome, so start + length greater than the chromosome's gene count means the segment crossed the origin. dest_position is an index into what was left after it was excised. A segmental event acts on a run of genes of several families at once and genome_events.tsv has one family column, so it writes one row per gene lineage, each repeating the same arc.

gene_order.tsv. topology is circular or linear, written beside every gene: it decides where a segmental event stops and which chromosomes may fuse. A chromosome with no genes has no rows here, and so no topology. A streamed run lists each node's rows when that node's branch ends, so the nodes are in the order their branches end rather than in node order.

chromosome_events.tsv. Chromosomes are named n<species>_c<id>, on the same pattern as gene copies. Kinds are initial (a replicon the run starts with, at time 0), speciation, fission, fusion, origination and loss; a fusion is the one row with two parents.

initial_genome.tsv has no lineage column, for the reason given at the family resolution.

Genomes, nucleotide: simulate_genomes_nucleotide

From zombi2 genomes --resolution nucleotide or result.write(dir, outputs=[...]).

File What it holds
genome_events.tsv the genealogy, in the format every resolution writes
block_events.tsv this resolution's own record: one row per ancestral interval an event touched
rearrangement_events.tsv inversions, transpositions and translocations, in physical coordinates
chromosome_events.tsv chromosome-network edges, same format and same kinds as ordered
blocks.tsv every node's blocks, ancestors included: lineage · chromosome · position · source · start · end · strand · copy · gene
genes.tsv the declared genes in initial coordinates: family · name · source · start · end · strand (the coding strand). Header-only when none were declared
initial_genome.tsv the genome the run started with: chromosome · position · source · start · end · strand · copy · gene
initial_sequence.fasta the initial DNA the run was given (--fasta), one >source<n> record per replicon. Only when a FASTA was supplied
genome_summary.json what came out, counted three separate ways
gene_tree_fam<f>_complete.nwk · …_extant.nwk one tree per declared gene (else per recovered root-block), in gene_trees/
gene_trees.tsv as at the family resolution. Only with --compact
gff/genome_<lineage>.gff each node's genes in its own coordinates, plus genome_initial.gff
bed/genome_<lineage>.bed each node's blocks, spacer included, each named by the ancestral interval it descends from
species_complete.nwk as at the family resolution
names.tsv as at the family resolution
conditioned_on as at the family resolution, written only when a rate or transfer_to was conditioned

From Python: .genomes · .node_genomes (each a NucleotideGenome: a list of Chromosomes, each a list of Blocks), .root_blocks · .block_trees (the recovered root partition and a tree for every interval), .assembly(node) · .initial_assembly(), .mosaic(node) · .trace_back(node) · .describe(node), .deletions (the indel log), .gene_spans · .gene_names · .gene_strands · .block_of(family), and the driver views .presence(name) · .completion(name), which here are based on declared genes: the name is the GFF's ID / Name, and what takes a gene away is an arc of DNA rather than a whole copy.

genome_events.tsv: the same time · kind · family · parents · children as at the family resolution, one row per event, so one reader serves all three resolutions and zombi2 tools works here unchanged. family is the declared gene (else the recovered root-block), and the copy in each token is a gene id, the one the gene trees, alignments and homology tables use. It is derived onto the root-block partition, where a copy either covers a block in full or does not touch it, which is what makes a duplication a bifurcation here. A nucleotide transfer is always additive, so transfer_replacing cannot appear.

block_events.tsv: time · kind · parents · children · chromosome · source · start · end · cuts; copy lineages and chromosomes are named as everywhere else. There is one row per ancestral interval an event touched, not one row per event, so an event covering several blocks writes several rows sharing a time and kind, and a duplication starts a child without ending its parent (the parent still covers the rest of its extent). That is why the genealogy above is a separate table: this shape suits sequence and not a gene tree. Kinds are initial · origination · insertion · duplication · deletion · loss · transfer · speciation, where initial is the starting genome laid down at time 0, one row per replicon, and insertion / deletion are the indels: insertion is novel spacer on a fresh source, a root like an origination but carrying no gene family, and deletion removes material without ending a copy lineage. cuts gives the ancestral breakpoints that event used as source:position pairs, semicolon separated: it is what tells an indel breakpoint from an ordinary one, and without it a run with indels could not be read back. A speciation splits one copy lineage into one per daughter without touching sequence, so it writes a single row with both daughters and every coordinate cell empty; a transfer's blocks keep their source coordinates and leave chromosome empty. source/start/end are ancestral coordinates; the physical ones live in the rearrangement log. This is what read_nucleotide_genomes replays.

rearrangement_events.tsv: the same columns the ordered resolution writes with base pairs in place of genes, along the chromosome as blocks.tsv numbers it, plus cuts, the ancestral breakpoints the rearrangement used, which only this resolution can give (the ordered one leaves it blank). No token of its own either: events writes all three tables here. A nucleotide translocation records no dest_position: its blocks keep their source coordinates and the engine places the arc.

blocks.tsv: position is the block's physical offset, and the rows of one chromosome tile it end to end from 0. source names the coordinate frame an interval lives in, either an initial replicon or the fresh source an insertion or origination laid down, and start/end are the interval on that source. copy is the copy-lineage token the event log uses for the block's carrier, and gene numbers the declared gene a block is, 1 upward in declaration order, 0 for spacer. The largest file this level writes: blocks are not kept maximal during a run, so it grows with their number × every node.

genome_summary.json: events counted as biology rather than rows (see the ordered section), block_events counting this resolution's own log the same way, genes (every gene the run holds, those declared at the start and the de-novo ones origination added), and base pairs and chromosomes per genome. No phyletic profiles here: the unit is the base pair. The two counters do not bound each other. events counts gene-tree branchings and block_events counts events on the DNA: an arc covering three genes is three gene events, one covering none is zero. A gene is never split, so an event changes gene content only when its arc covers a gene end to end, which is why a run with small extents against big genes reports block events by the dozen and gene events at zero, and warns that it will. deletions and base_pairs_deleted count the indel log, which is neither of the other two: an indel removes material without ending a copy lineage, so it appears in no event counter. block_events also gains a kind events has no counterpart for: insertion brings sequence rather than a gene family, so the gene-level counters cannot see it.

initial_sequence.fasta is what lets a separate zombi2 sequences run found its blocks from the real sequence.

gff/ and bed/: one file per node, plus genome_initial.gff / .bed for the genome the run started with. The GFF is the annotation to read beside the sequence level's genome_<lineage>.fasta, which names its sequences to match; the BED is the ancestry as a browser track.

.describe(node) gives one node's genome written out block by block, for reading by eye: mosaic as text, one line per block, each labelled with the gene it is or intergene. Genes declared with a name are named; the rest are numbered by family.

.assembly(node) and .initial_assembly() say how a node's genome is built from the recovered root blocks: (block, gene, strand, lo, hi) in physical order, [lo, hi) being the sub-range of that block the node carries, the whole block unless an indel took a stretch out of it or opened a gap inside it. .mosaic(node) and .trace_back(node) are the same genome at two grains, per block and per nucleotide.

.root_blocks and .block_trees: the maximal never-cut intervals that some node still carries, and a recovered tree for every one of them, spacer as well as gene. Cut at every node's breakpoints rather than only the survivors', which is what lets any node's genome be rebuilt.

.deletions holds one Deletion per indel deletion: time · lineage · chromosome · deleted, the last being (copy, source, start, end) rows in ancestral coordinates, the same payload a loss carries. Kept out of events because a deletion ends no copy lineage, so the gene-tree recovery must not read it. Only deletions are here: an insertion brings material that descends from nothing, so it must begin a copy lineage and is recorded in events as a root of its own kind. Written into block_events.tsv under kind deletion, which is also how it is read back.

.gene_spans and .block_of(family): {family: (source, start, end)} in initial coordinates, a named gene's family id and coding strand from a GFF, and the block index a family occupies. That last one is the join between the two numbering schemes here: .gene_spans and .gene_trees are keyed by family, .root_blocks and .block_trees by block index, both plain integers, so mixing them up is otherwise silent.

Three event files, because a nucleotide run records three different things: the genealogy in the one format every resolution writes, the interval record only this resolution has, and what moved without beginning or ending anything. Ancestral coordinates are in block_events.tsv and physical ones in rearrangement_events.tsv: two frames, so two files, and no row carrying columns from both.

The events index against the species tree canonicalised so its n<id> labels match the branch named inside each copy token, so a genomes run needs that exact tree to be replayable. A run grown by zombi2 species already has it; a run whose tree came from --from gets a copy written to its own species/species_complete.nwk, rather than a second file under another name, with species_fates.tsv beside it so a later level on this run reads each tip's fate from the record instead of guessing it from tip depth. Either way zombi2 sequences can replay the gene genealogy from the run directory alone. Like names.tsv (external input trees) and the .log, those copies are CLI artifacts, not result.write() outputs.

Sequences: simulate_sequences

The zombi2 sequences command replays a prior zombi2 genomes run, its own run directory or --from another, reading its species tree and its genome_events.tsv. Gene outputs are written one file per gene family (<f> = family number); a family with no surviving copy writes none. Every node is labelled n<species>_g<copy>, so a phylogram's tips pair with its alignment and its internal nodes with the ancestral sequences.

File What it holds
fam<f>.fasta one row per extant gene copy: nucleotides or amino acids, following the model, in alignments/
phylogram_fam<f>_complete.nwk · …_extant.nwk the gene tree each family's sequences were drawn along, in substitutions per site, in phylograms/
clock_species_tree_complete.nwk · …_extant.nwk the species tree with its branches in substitutions per site, the molecular clock made visible
sequences_ancestral_fam<f>.fasta the nodes the alignment leaves out, one file per family in ancestral/. Only when you name its token, ancestral
alignments.fasta every family's alignment in one FASTA file, in place of alignments/. Only with --compact
phylograms.tsv every family's complete and extant phylograms, one row per family, in place of phylograms/. Only with --compact
ancestral.fasta every family's ancestral sequences in one FASTA file, in place of ancestral/. Only with --compact and the ancestral token
sequences_founding.fasta one record fam<f> per family, the sequence it originated with. Only when you name its token, founding
genome_<lineage>.fasta one file per node of the complete tree, one record <lineage>_chr<c> per chromosome, in genomes/. Nucleotide genome runs only
genome_initial.fasta the genome the run started with, as sequence. Nucleotide runs only
family_multipliers.tsv each family's substitution multiplier: family · substitution, one row per family
sequences_summary.json unit (family or block), families with sequences, how many, sites min/max, mean_pairwise_identity, assembled genomes, the seed
conditioned_on the levels this run depended on as a driver. Only when the substitution rate was conditioned

From Python: .alignments · .ancestral (the sequences), .genomes · .node_genomes · .initial_genome (the assembled genomes, present only when the run came from a nucleotide genome), .founding · .phylograms · .species_phylogram · .family_multipliers, and the driver views .gc() · .composition(letters).

family_multipliers.tsv is the same table the genome run writes, with the one rate this level draws among families. A substitution rate written with varying_among("families", …) draws one multiplier for each family before any site evolves. The family then evolves at the run's rate times that multiplier, for its whole life. The multipliers have an expected value of 1, because the law's own mean is divided out. A run whose substitution rate does not vary among families writes the header alone. A nucleotide run refuses the draw, because it evolves blocks rather than gene families.

phylogram_fam<f>_*.nwk: with rate variation across sites the branch length is the mean over sites, which is what the rate classes are normalised to. Under a per-clade model set (Models) the lengths still mean substitutions per site, since every model is normalised to one per unit length, but that normalisation holds at stationarity, so on a branch whose composition is still relaxing toward its clade's frequencies the realised count falls a little short of the length written here.

clock_species_tree_*.nwk: the mean over sites when rates vary across them, as for the phylograms. A driven substitution rate shows here too: a branch is the rate times the driver integrated along it, so this is where you read what the trait did. The token that writes the pair, in outputs= and --write, is species_phylogram, not the file's name.

sequences_ancestral_fam<f>.fasta: internal nodes, and the tips where a copy was lost or its species died.

alignments.fasta and ancestral.fasta: the records of every family, one family after another. Each header is the sequence name, a space and the family tag: >n12_g1200 fam4. The name is unchanged, so it still matches the gene tree's tip label. Most FASTA readers keep the family as a description. Sequence lines wrap at 70 columns, as in the per-family files.

phylograms.tsv: the same columns as gene_trees.tsv. On a nucleotide run, a column block replaces family.

genome_<lineage>.fasta: the assembled genome, its blocks concatenated in physical order, for every node, extant, extinct and ancestral alike. A family or ordered run has gene families, not coordinates, so there is nothing to lay out and no genomes/ is created. It is the biggest thing this level writes, a whole genome times every node. genome_initial.fasta is the state the stem leads from, which is no node's.

conditioned_on: one level per line (traits, say). It records the dependency so re-running the trait refuses to leave this run silently stale, or clears it under --force. A run with no driven rate writes no such file.

sequences_summary.json. mean_pairwise_identity is the saturation check the command warns on: every within-family pair, so it is the same number whether the run was held in memory or streamed.

.alignments and .ancestral. .alignments is the observable data, for each family the sequence at every extant gene copy, the alignment a phylogenetic method would use as input; .ancestral is every node the alignment leaves out. The run wrote a sequence at each node as it went, so these are the exact ancestors, not estimates, and together they account for every node of the tree exactly once.

.gc() and .composition(letters): the share of a lineage's sequence that is those letters, pooled over all its families, for use as a driver (Ch8): GC content, or any amino-acid frequency. A number, so it takes a Curve or a Scalar; it drives a trait or a further sequence run, never the genome its gene trees came from.

On a nucleotide genome run every block evolves, spacer as well as gene, so a genome of b blocks writes b alignments and b phylograms, and that is what makes the genomes assemblable. The number in those filenames is then a root block index, not a gene family id, and the files say so: block6.fasta, phylogram_block6_complete.nwk and sequences_ancestral_block6.fasta in place of fam6.…, and sequences_founding.fasta's records are block<b> rather than fam<f>. The two numbering schemes are different, so go from a gene to its block with genomes.block_of(family) (Ch5). zombi2 sequences reads a nucleotide handoff too, recognising one by its blocks.tsv, so all of this is reachable from the command line.

Joint: joint.simulate / zombi2 joint

A joint run simulates two levels at once, so it writes both, each in the format its own command would give it: the species files, and then the driver's own, whether the trait's or the genomes'. Its own output is one file: joint_summary.json, at the run root beside the run report.

File What it holds
species/species_complete.nwk · …_extant.nwk · species_events.tsv · species_fates.tsv · species_summary.json the grown tree, complete, so the extinct lineages whose fate the driver decided are kept
traits/trait_values.tsv · trait_events.tsv · trait_path.tsv · trait_tree.nwk · trait_summary.json as the traits level writes them, when the trait was the driver
genomes/genome_events.tsv · profiles.tsv · genomes.tsv · initial_genome.tsv · gene_trees/ · genome_summary.json · species_complete.nwk as the genomes level writes them, when the genome was the driver
joint_summary.json both levels' summaries in one file, at the run root, the one file that is the joint run's own
species/joint.log the resolved parameters, as every command writes

One driver per run, never both, so a run writes the trait files or the genome files, not each.

joint_summary.json holds four keys: seed; driver, saying which level it was (trait / genome); species, whose realised rates are what the driver did; and a fourth named trait or genome, the driver's own summary. It sits at the run root because neither level was grown first. The same payloads the two levels write alone, so there is nothing new to read.

The genome files include the species tree, which a joint run grew and so keeps here as well as under species/.

Traits: simulate_continuous / simulate_discrete

zombi2 traits --name NAME writes this trait's files to traits/NAME/ instead of traits/, so one run can hold several traits and one can drive another (Ch8). --name and --flat are refused together.

File What it holds
trait_values.tsv the value at every node: node · kind · one column per trait, headed by the trait's name (trait when unnamed, side by side in a correlated run); kind is the tip's fate (extant / extinct / unsampled) or ancestor
trait_events.tsv the trait's whole history: an initial row giving the state at t=0, then every switch
trait_path.tsv a continuous trait between the nodes: node · time · trait · variance, one row per point along each branch
trait_tree.nwk the tree with every node annotated [&trait=…], for FigTree or iTOL
trait_summary.json tips · nodes · events, then states · most_common_share for a discrete trait, or values (min/mean/max) · value_at_root_node for a continuous one
names.tsv as at the genome level. Only when the tree came from --from with its own tip labels
conditioned_on the levels this run depends on as a driver, in the trait's own directory. Only when --rate or --switch was conditioned

zombi2 traits and the Python TraitsResult.write write the same set, and it matches the kind: a continuous run writes the values, the path, the tree and the summary, having no switches to log, and a discrete run writes the values, the events, the tree and the summary, having no path.

From Python: .values · .node_values (the values), .values_by_id (.values keyed by the bare integer node id, 5 for tip n5, for joining against .node_values), and .history.

trait_events.tsv: time · kind · lineage · from · to, where kind is initial · on_branch · on_speciation. The one event log whose payload is a state change rather than a birth and a death, so it keeps its lineage and has no parents / children. Times are full precision, since they drive a conditioned run's Gillespie. This is also the driver file: a genome, sequence or trait run drives a rate with scaled_by("trait_events.tsv", …), replaying it against the shared tree. A continuous trait carries only the initial row and any at_speciation jumps (a diffusion cannot be rebuilt from events), and that holds for a multi-optimum (regimes=) run and a correlated multi-trait one alike. A correlated run widens the table instead of repeating a row per trait, giving from:<trait> · to:<trait>, one pair apiece, exactly as trait_values.tsv widens, because a correlated jump moves every trait at once and is one event.

trait_path.tsv: node · time · trait · variance, one row per point, sorted by node id then time. A continuous trait is recorded only at the nodes, and this is what it did in between: its path along each branch, drawn conditioned on the value at each end. Each branch contributes its left endpoint, then one point per stretch of at most step, then its node value, so a branch stands on its own — in particular the left endpoint is the value after an at_speciation jump, which is not the parent's node value. variance is what the trait accrued since the point before it, which is what lets a reader wanting another resolution derive its values from the two written points around each time it needs, rather than draw a different path. Times and values are full precision, since a driven run steps its Gillespie at these times and reads these values. This is the second half of the driver file: a continuous trait drives a rate with scaled_by("trait_values.tsv", …), which reads this file beside it; without it the driver falls back to the straight line between node values and says so. Written by write(dir, step=…) at a resolution you choose, or at 1% of the tree's height by default — the same default a reader takes. A discrete or threshold trait has no path file, and neither does a correlated run, which cannot be a driver.

trait_summary.json: what came out, not what was asked for. The root node sits at the end of the stem, so value_at_root_node is not the value the run started from.

conditioned_on. A trait driven by a trait grown first records traits (Ch8). Both sides sit under traits/, so the record is kept but re-running the driver trait does not invalidate this run.

.values and .node_values. .values is the observable vector, the trait at each extant tip, keyed by tip name (n5), the same names the Newick and trait_values.tsv use, so the dataset joins the tree it came from. .node_values is every node, extant, extinct and internal alike: the true ancestors at each split, from the same process that produced the tips. Discrete traits store the state labels you gave, not integer indices.

.history is, for a discrete trait, the per-branch stochastic character map derived from the event log: the ordered (state, duration) segments each branch passed through. None for a continuous trait, which has no map, and for a threshold trait, whose liability crossings are un-timed.

Conditioning and joining: no new files

Neither adds a format. A conditioned run writes the driven level's own files and one extra record, conditioned_on, naming what it depended on (above), so the pairing is kept on disk; a joint run writes both levels, each in its own format. A family genome run that reads its own gene content lists those links in links.tsv (above).

The zombi2 tools commands write their own files, the homology matrix and the reconciliation/scoring outputs, catalogued in Appendix D.