zombi2.joint¶
Two levels that cannot be run in order, grown in one run. A pair must be run jointly when neither level can be grown first — a trait that drives speciation, or gene content that does. When the driver can be grown first, the run is conditioned instead: two ordinary runs in order, the finished driver handed to the second as an object (in Python) or as its written log (across two commands). See Dependent runs and Joint runs.
A level joined to itself stays on that level's own function, with joint=True:
simulate_genomes_family for a gene family driving the
rest of its genome, and simulate_traits for two traits that
depend on each other.
zombi2.joint.simulate
¶
simulate(*participants, tree=None, genomes=None, seed=None, record: bool = False, max_lineages=100000) -> JointResult
Simulate two levels at once, because neither can be finished before the other starts (SPEC §2–4).
Each participant is a process spec, and you give what you are not simulating::
# the tree is one of the two, so it comes out of the run
joint.simulate(species.birth_death(birth=faster_if_large, death=0.2, n_extant=100),
traits.discrete(name="size", states=["small", "large"], switch=0.1), seed=1)
joint.simulate(species.birth_death(birth=faster_with_toxin, n_extant=100),
genomes.genome(origination=0.2, loss=0.1, families=[family("toxin")]), seed=1)
A rate reads the other participant by name, "<level>:<handle>" — "traits:size" for a
named trait, "genomes:toxin" for a declared family, "genomes:count" for a lineage's whole
gene count. A run holding one unnamed trait also answers to "trait".
~zombi2.traits.continuous is a participant too, and it is the one driver that does not race
exactly: a diffusion moves at every instant, so the run holds it fixed across a step= written
on the connection and releases it at each boundary.
The species tree is an output exactly when ~zombi2.species.birth_death is one of the
participants; otherwise tree supplies it. A level driving itself does not come here at
all — that is one level and one result, so it stays on that level's own function with
joint=True.
Returns a JointResult. Deterministic given seed.
Source code in zombi2/joint/__init__.py
zombi2.joint.JointResult
dataclass
¶
JointResult(species: SpeciesResult, seed: int | None, trait: TraitsResult | None = None, genome: FamilyGenomesResult | None = None, sequences: 'Any | None' = None)
What simulate() returns — both simulated levels of a joint run. species is the
grown tree (a SpeciesResult: complete_tree, extant_tree, the
speciation/extinction events); the driver level that grew with it is either trait (a
TraitsResult, for a trait→speciation run) or genome (a
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.
summary
¶
What this run produced, as a plain dict — the payload of joint_summary.json.
A joint run grew two levels at once, so this holds both of their summaries under one roof
rather than inventing a third vocabulary: species is the tree that came out, and exactly
one of trait / genome is the driver that shaped it. The tree is an output here, which
is the whole point of the command, so its realised birth and death rates are the numbers worth
reading — they are what the driver did.
Source code in zombi2/joint/__init__.py
write
¶
Write both levels to directory (created if needed), each exactly as its own command
writes it: "species" → the SpeciesResult files (species_complete.nwk /
species_extant.nwk / species_events.tsv / species_fates.tsv /
species_summary.json); "driver" → the level that grew with it, a trait's
trait_values.tsv / trait_events.tsv / trait_tree.nwk / trait_summary.json or
a genome's genome_events.tsv / profiles.tsv / genomes.tsv /
initial_genome.tsv / gene_trees/ / genome_summary.json; "summary" →
joint_summary.json, the one file that is the joint run's own.
The tokens are the two levels, not their files, because each is written with that level's
own default — which is what makes a joint run's directory the two runs it stands in for. Pick
files within a level through the level itself: result.species.write(d, outputs=…),
result.trait.write(d, outputs=…). flat is passed to the driver level, the only one of
the two with a many-files-per-run output.
Both levels land in the one directory named here; zombi2 joint groups them under
species/ and traits/ / genomes/ instead, and writes the same files.