zombi2 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zombi2/__init__.py +148 -0
- zombi2/__main__.py +6 -0
- zombi2/_rust.py +457 -0
- zombi2/_sampling.py +81 -0
- zombi2/cli.py +2195 -0
- zombi2/coevolve/__init__.py +51 -0
- zombi2/coevolve/cladogenetic_genome.py +183 -0
- zombi2/coevolve/gene_conditioned_trait.py +169 -0
- zombi2/coevolve/gene_diversification.py +416 -0
- zombi2/coevolve/sse.py +513 -0
- zombi2/coevolve/trait_coupling.py +443 -0
- zombi2/coevolve/trait_gene_feedback.py +224 -0
- zombi2/distributions.py +117 -0
- zombi2/experimental/__init__.py +72 -0
- zombi2/experimental/gene_conversion.py +123 -0
- zombi2/genomes/__init__.py +44 -0
- zombi2/genomes/events.py +212 -0
- zombi2/genomes/genome.py +471 -0
- zombi2/genomes/genome_sim.py +455 -0
- zombi2/genomes/gff.py +149 -0
- zombi2/genomes/nucleotide_genome.py +843 -0
- zombi2/genomes/nucleotide_sim.py +638 -0
- zombi2/genomes/profiles.py +225 -0
- zombi2/genomes/rates.py +325 -0
- zombi2/genomes/reconciliation.py +459 -0
- zombi2/genomes/simulation.py +510 -0
- zombi2/genomes/transfers.py +43 -0
- zombi2/parallel.py +122 -0
- zombi2/sequences/__init__.py +43 -0
- zombi2/sequences/_aa_models.py +134 -0
- zombi2/sequences/clocks.py +438 -0
- zombi2/sequences/evolution.py +252 -0
- zombi2/sequences/models.py +462 -0
- zombi2/species/__init__.py +28 -0
- zombi2/species/forward.py +513 -0
- zombi2/species/ghosts.py +310 -0
- zombi2/species/model.py +407 -0
- zombi2/species/sim.py +161 -0
- zombi2/tools/__init__.py +59 -0
- zombi2/tools/reconciliation/__init__.py +159 -0
- zombi2/tools/reconciliation/_rust.py +122 -0
- zombi2/tools/reconciliation/dated.py +295 -0
- zombi2/tools/reconciliation/genetree.py +104 -0
- zombi2/tools/reconciliation/scoring.py +89 -0
- zombi2/tools/reconciliation/species.py +96 -0
- zombi2/tools/reconciliation/undated.py +272 -0
- zombi2/traits/__init__.py +39 -0
- zombi2/traits/biogeography.py +208 -0
- zombi2/traits/models.py +1328 -0
- zombi2/tree.py +223 -0
- zombi2-0.2.0.dist-info/METADATA +156 -0
- zombi2-0.2.0.dist-info/RECORD +55 -0
- zombi2-0.2.0.dist-info/WHEEL +4 -0
- zombi2-0.2.0.dist-info/entry_points.txt +2 -0
- zombi2-0.2.0.dist-info/licenses/LICENSE +21 -0
zombi2/__init__.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""ZOMBI2 — simulation of species trees (backward) and gene families (forward).
|
|
2
|
+
|
|
3
|
+
Public API::
|
|
4
|
+
|
|
5
|
+
import zombi2 as z
|
|
6
|
+
|
|
7
|
+
tree = z.simulate_species_tree(z.BirthDeath(birth=1.0, death=0.3),
|
|
8
|
+
n_tips=20, age=5.0, seed=1)
|
|
9
|
+
|
|
10
|
+
# every family the same rates:
|
|
11
|
+
genomes = z.simulate_genomes(tree, duplication=0.2, transfer=0.1, loss=0.25,
|
|
12
|
+
origination=0.5, seed=42)
|
|
13
|
+
|
|
14
|
+
# every family its own rates, sampled from distributions:
|
|
15
|
+
genomes = z.simulate_genomes(tree, z.FamilySampledRates(
|
|
16
|
+
duplication=z.Gamma(2, 0.1), transfer=z.Exponential(0.1),
|
|
17
|
+
loss=z.Gamma(2, 0.12), origination=0.5), seed=42)
|
|
18
|
+
|
|
19
|
+
genomes.profiles.matrix # families x extant-species copy numbers
|
|
20
|
+
genomes.write("out/")
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
__version__ = "0.2.0"
|
|
26
|
+
|
|
27
|
+
# Low-level primitives (kept at top level only; no scikit-style namespace).
|
|
28
|
+
from zombi2.genomes.events import EventType, GeneOp, EventRecord, Selection, Region, TargetParams
|
|
29
|
+
from zombi2._rust import available as rust_available
|
|
30
|
+
|
|
31
|
+
# scikit-learn-style namespaces are the single source of truth for the
|
|
32
|
+
# public API: importing from them here guarantees that, e.g.,
|
|
33
|
+
# ``zombi2.BirthDeath is zombi2.species.BirthDeath`` (same object). The
|
|
34
|
+
# namespace modules are thin re-exports over the implementation modules and do
|
|
35
|
+
# not redefine anything; see zombi2/species.py, zombi2/genomes.py, etc.
|
|
36
|
+
from zombi2.species import (
|
|
37
|
+
Tree, TreeNode, read_newick, prune,
|
|
38
|
+
BirthDeath, Yule, EpisodicBirthDeath, ClaDS, DiversityDependent,
|
|
39
|
+
CladeShiftBirthDeath, simulate_species_tree, add_ghost_lineages,
|
|
40
|
+
)
|
|
41
|
+
from zombi2.genomes import (
|
|
42
|
+
Gene, Genome, UnorderedGenome, OrderedGene, OrderedGenome,
|
|
43
|
+
NucleotideGenome, Segment, simulate_nucleotide_genomes, NucleotideResult, Block,
|
|
44
|
+
read_gff, GffGenome,
|
|
45
|
+
RateModel, SharedRates, PerGenomeRates, FamilySampledRates, BranchRates,
|
|
46
|
+
EventWeight, TransferModel,
|
|
47
|
+
GenomeSimulator, GenomeResult, ProfileMatrix,
|
|
48
|
+
simulate_genomes, Genomes, GenomeTrace, read_events_trace,
|
|
49
|
+
build_gene_trees, run_replicates,
|
|
50
|
+
)
|
|
51
|
+
from zombi2.distributions import (
|
|
52
|
+
Distribution, Fixed, Exponential, Gamma, LogNormal, Uniform, as_distribution,
|
|
53
|
+
)
|
|
54
|
+
from zombi2.sequences import (
|
|
55
|
+
# relaxed molecular clocks (chronogram -> phylogram; the shared lineage clock family)
|
|
56
|
+
Clock, RateScaledTree, StrictClock, UncorrelatedLogNormalClock,
|
|
57
|
+
UncorrelatedGammaClock, WhiteNoiseClock, AutocorrelatedLogNormalClock,
|
|
58
|
+
CIRClock, RateVariation,
|
|
59
|
+
# substitution models + the gene x lineage substitution clock
|
|
60
|
+
SequenceEvolution, GenePhylograms,
|
|
61
|
+
SubstitutionModel, GammaRates, jc69, k80, hky85, gtr,
|
|
62
|
+
poisson, lg, wag, jtt, dayhoff, make_model, is_protein_model,
|
|
63
|
+
DNA_MODELS, PROTEIN_MODELS, AMINO_ACIDS,
|
|
64
|
+
evolve_on_tree, read_fasta, write_fasta,
|
|
65
|
+
)
|
|
66
|
+
from zombi2.traits import (
|
|
67
|
+
BrownianMotion, OrnsteinUhlenbeck, MultivariateBrownian, MultivariateOU,
|
|
68
|
+
MultiOptimumOU, ThresholdModel, EarlyBurst, Mk, CorrelatedBinary,
|
|
69
|
+
CorrelatedBinaryK, HiddenStateMk, Cladogenesis, simulate_traits,
|
|
70
|
+
replicate_traits, TraitResult,
|
|
71
|
+
pagel_lambda, pagel_delta, pagel_kappa,
|
|
72
|
+
DEC, simulate_biogeography,
|
|
73
|
+
)
|
|
74
|
+
from zombi2.coevolve import (
|
|
75
|
+
TraitGeneCoupling, TraitTrajectory, TraitLinkedRates, TraitLinkedResult,
|
|
76
|
+
simulate_trait_linked_genomes,
|
|
77
|
+
GeneDiversification, GeneDiversificationResult, simulate_gene_diversification,
|
|
78
|
+
simulate_co_diversification,
|
|
79
|
+
CladogeneticGenome, CladogeneticGenomeResult, simulate_cladogenetic_genome,
|
|
80
|
+
GeneConditionedTrait, GeneConditionedTraitResult, simulate_gene_conditioned_trait,
|
|
81
|
+
TraitGeneFeedback, TraitGeneFeedbackResult, simulate_trait_gene_feedback,
|
|
82
|
+
BiSSE, MuSSE, HiSSE, QuaSSE, simulate_sse,
|
|
83
|
+
)
|
|
84
|
+
# NOTE: ABC profile-matching inference has moved out of the core to
|
|
85
|
+
# ``ZOMBI2_FUTURE/abc-inference/`` — inference is a Phase-3 Extension, not a core simulation
|
|
86
|
+
# level. See that folder's README to revive it as an Extension.
|
|
87
|
+
|
|
88
|
+
__all__ = [
|
|
89
|
+
"__version__",
|
|
90
|
+
# events
|
|
91
|
+
"EventType", "GeneOp", "EventRecord", "Selection", "Region", "TargetParams",
|
|
92
|
+
# tree
|
|
93
|
+
"Tree", "TreeNode", "read_newick", "prune",
|
|
94
|
+
# species tree
|
|
95
|
+
"BirthDeath", "Yule", "EpisodicBirthDeath", "ClaDS", "DiversityDependent",
|
|
96
|
+
"CladeShiftBirthDeath", "simulate_species_tree", "add_ghost_lineages",
|
|
97
|
+
# genome
|
|
98
|
+
"Gene", "Genome", "UnorderedGenome", "OrderedGene", "OrderedGenome",
|
|
99
|
+
# nucleotide genome (structural events at nucleotide resolution)
|
|
100
|
+
"NucleotideGenome", "Segment", "simulate_nucleotide_genomes", "NucleotideResult", "Block",
|
|
101
|
+
"read_gff", "GffGenome",
|
|
102
|
+
# rates & transfers
|
|
103
|
+
"RateModel", "SharedRates", "PerGenomeRates", "FamilySampledRates",
|
|
104
|
+
"BranchRates", "EventWeight", "TransferModel",
|
|
105
|
+
# distributions
|
|
106
|
+
"Distribution", "Fixed", "Exponential", "Gamma", "LogNormal", "Uniform", "as_distribution",
|
|
107
|
+
# simulation
|
|
108
|
+
"GenomeSimulator", "GenomeResult", "ProfileMatrix", "simulate_genomes", "Genomes", "GenomeTrace",
|
|
109
|
+
"read_events_trace", "build_gene_trees",
|
|
110
|
+
# trait-conditioned gene families (trait <-> gene-family coupling)
|
|
111
|
+
"TraitGeneCoupling", "TraitTrajectory", "TraitLinkedRates", "TraitLinkedResult",
|
|
112
|
+
"simulate_trait_linked_genomes",
|
|
113
|
+
# relaxed molecular clocks (chronogram -> phylogram; the shared lineage clock family)
|
|
114
|
+
"Clock", "RateScaledTree", "StrictClock", "UncorrelatedLogNormalClock",
|
|
115
|
+
"UncorrelatedGammaClock", "WhiteNoiseClock", "AutocorrelatedLogNormalClock",
|
|
116
|
+
"CIRClock", "RateVariation",
|
|
117
|
+
# family sequence evolution (gene x lineage substitution clock)
|
|
118
|
+
"SequenceEvolution", "GenePhylograms",
|
|
119
|
+
# sequence simulation (evolve DNA or protein along a gene tree; ancestral genome reconstruction)
|
|
120
|
+
"SubstitutionModel", "GammaRates", "jc69", "k80", "hky85", "gtr",
|
|
121
|
+
"poisson", "lg", "wag", "jtt", "dayhoff", "make_model", "is_protein_model",
|
|
122
|
+
"DNA_MODELS", "PROTEIN_MODELS", "AMINO_ACIDS",
|
|
123
|
+
"evolve_on_tree", "read_fasta", "write_fasta",
|
|
124
|
+
# trait evolution (phylogenetic comparative models)
|
|
125
|
+
"BrownianMotion", "OrnsteinUhlenbeck", "MultivariateBrownian", "MultivariateOU",
|
|
126
|
+
"MultiOptimumOU", "ThresholdModel", "EarlyBurst", "Mk", "CorrelatedBinary",
|
|
127
|
+
"CorrelatedBinaryK", "HiddenStateMk", "Cladogenesis", "simulate_traits",
|
|
128
|
+
"replicate_traits", "TraitResult",
|
|
129
|
+
"pagel_lambda", "pagel_delta", "pagel_kappa",
|
|
130
|
+
# historical biogeography (range evolution)
|
|
131
|
+
"DEC", "simulate_biogeography",
|
|
132
|
+
# state-dependent diversification (trait drives the tree — coevolve traits:species)
|
|
133
|
+
"BiSSE", "MuSSE", "HiSSE", "QuaSSE", "simulate_sse",
|
|
134
|
+
# gene-content-dependent diversification (genes drive the tree — coevolve genes:species,
|
|
135
|
+
# + species:genes = the co-diversification joint model)
|
|
136
|
+
"GeneDiversification", "GeneDiversificationResult", "simulate_gene_diversification",
|
|
137
|
+
"simulate_co_diversification",
|
|
138
|
+
# cladogenetic genome evolution (speciation drives gene content — coevolve species:genes)
|
|
139
|
+
"CladogeneticGenome", "CladogeneticGenomeResult", "simulate_cladogenetic_genome",
|
|
140
|
+
# gene-conditioned trait (gene content drives a trait — coevolve genes:traits)
|
|
141
|
+
"GeneConditionedTrait", "GeneConditionedTraitResult", "simulate_gene_conditioned_trait",
|
|
142
|
+
# trait<->genes joint model (trait-gene feedback — coevolve traits:genes + genes:traits)
|
|
143
|
+
"TraitGeneFeedback", "TraitGeneFeedbackResult", "simulate_trait_gene_feedback",
|
|
144
|
+
# parallelism
|
|
145
|
+
"run_replicates",
|
|
146
|
+
# Rust engine diagnostic (the built-in model requires the compiled extension)
|
|
147
|
+
"rust_available",
|
|
148
|
+
]
|
zombi2/__main__.py
ADDED
zombi2/_rust.py
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
"""Internal bridge to the compiled ``zombi2_core`` Rust engine.
|
|
2
|
+
|
|
3
|
+
This module is **not** part of the public API. It is called by
|
|
4
|
+
:func:`~zombi2.simulate_genomes` and :func:`~zombi2.simulate_nucleotide_genomes`, which
|
|
5
|
+
route the *built-in* model (order-free ``UnorderedGenome`` + ``SharedRates``) to Rust
|
|
6
|
+
automatically — there is no separate "fast" function and no engine switch. Flexible models
|
|
7
|
+
(family/genome-wise/branch rates, ordered genomes, carrying capacity, custom samplers) run
|
|
8
|
+
on the pure-Python engine instead.
|
|
9
|
+
|
|
10
|
+
The extension must be compiled. Build it once from the repo::
|
|
11
|
+
|
|
12
|
+
pip install maturin
|
|
13
|
+
cd rust && maturin build --release -i python3
|
|
14
|
+
pip install --force-reinstall rust/target/wheels/*.whl
|
|
15
|
+
|
|
16
|
+
If it isn't built, :func:`available` returns ``False`` and the built-in model raises a clear
|
|
17
|
+
error telling the user to build it (there is deliberately no Python fallback for the
|
|
18
|
+
built-in model, so a given ``seed`` is reproducible against a single engine).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
from collections import Counter
|
|
26
|
+
|
|
27
|
+
from zombi2.genomes.events import EventLog, EventRecord, EventType, GeneOp
|
|
28
|
+
from zombi2.genomes.genome_sim import resolve_max_family_size
|
|
29
|
+
from zombi2.genomes.nucleotide_sim import NucleotideResult
|
|
30
|
+
from zombi2.genomes.profiles import ProfileMatrix
|
|
31
|
+
from zombi2.genomes.rates import SharedRates
|
|
32
|
+
from zombi2.genomes.simulation import Genomes
|
|
33
|
+
|
|
34
|
+
try: # optional native extension
|
|
35
|
+
import zombi2_core as _core
|
|
36
|
+
except ImportError: # pragma: no cover - depends on whether the wheel is built
|
|
37
|
+
_core = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_BUILD_HINT = (
|
|
41
|
+
"the built-in gene-family model runs on the compiled zombi2_core extension, which is not "
|
|
42
|
+
"built. Build it with:\n"
|
|
43
|
+
" pip install maturin\n"
|
|
44
|
+
" cd rust && maturin build --release -i python3\n"
|
|
45
|
+
" pip install --force-reinstall rust/target/wheels/*.whl\n"
|
|
46
|
+
"or use a flexible rate model / ordered genome (which run on the Python engine)."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def available() -> bool:
|
|
51
|
+
"""True if the compiled ``zombi2_core`` extension is importable."""
|
|
52
|
+
return _core is not None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def require() -> None:
|
|
56
|
+
"""Raise a clear build error if the extension is missing."""
|
|
57
|
+
if _core is None:
|
|
58
|
+
raise RuntimeError(_BUILD_HINT)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def eligible(rates, genome_factory, sampler) -> bool:
|
|
62
|
+
"""True if this model is the built-in one the Rust engine implements: the default
|
|
63
|
+
``UnorderedGenome``, a plain ``SharedRates`` (no soft carrying capacity, no
|
|
64
|
+
rearrangements), and no custom event sampler. Everything else runs on Python."""
|
|
65
|
+
from zombi2.genomes.genome import UnorderedGenome
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
genome_factory is UnorderedGenome
|
|
69
|
+
and sampler is None
|
|
70
|
+
and type(rates) is SharedRates
|
|
71
|
+
and rates.carrying_capacity is None
|
|
72
|
+
and not rates.inversion
|
|
73
|
+
and not rates.transposition
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _FastNucleotideResult(NucleotideResult):
|
|
78
|
+
"""A NucleotideResult from the Rust profile path (no event log → no gene trees)."""
|
|
79
|
+
|
|
80
|
+
def block_gene_trees(self):
|
|
81
|
+
raise NotImplementedError(
|
|
82
|
+
"the Rust nucleotide profile path emits only leaf segments (profile / mosaic / "
|
|
83
|
+
"trace-back); per-block gene trees need the event log — use "
|
|
84
|
+
"simulate_nucleotide_genomes(...) (the default) for those."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def block_histories(self):
|
|
88
|
+
raise NotImplementedError(
|
|
89
|
+
"block_histories needs the event log; use simulate_nucleotide_genomes(...)."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# --- rate / tree / cap / transfer plumbing ---------------------------------------
|
|
94
|
+
|
|
95
|
+
def _resolve_rates(rates):
|
|
96
|
+
"""Return (d, t, l, o) from a SharedRates, rejecting features Rust does not implement."""
|
|
97
|
+
if type(rates) is not SharedRates:
|
|
98
|
+
raise TypeError(
|
|
99
|
+
f"the Rust engine only supports SharedRates, not {type(rates).__name__}; "
|
|
100
|
+
f"use simulate_genomes for that model"
|
|
101
|
+
)
|
|
102
|
+
unsupported = []
|
|
103
|
+
if rates.carrying_capacity is not None:
|
|
104
|
+
unsupported.append("carrying_capacity")
|
|
105
|
+
if rates.inversion:
|
|
106
|
+
unsupported.append("inversion")
|
|
107
|
+
if rates.transposition:
|
|
108
|
+
unsupported.append("transposition")
|
|
109
|
+
if unsupported:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
f"the Rust engine does not support {', '.join(unsupported)}; "
|
|
112
|
+
f"use a flexible model on the Python engine"
|
|
113
|
+
)
|
|
114
|
+
return (float(rates.duplication), float(rates.transfer),
|
|
115
|
+
float(rates.loss), float(rates.origination))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _tree_arrays(species_tree):
|
|
119
|
+
"""Flatten a Tree into the index-based arrays the Rust engines consume.
|
|
120
|
+
|
|
121
|
+
A single pre-order pass: because a node is always visited before its children, each parent's
|
|
122
|
+
index is already assigned when its children are reached, so the parent pointers, times and
|
|
123
|
+
extant-leaf mask are filled in one sweep. The previous version made ~six passes over the ~2N
|
|
124
|
+
nodes (materialise, build an object-keyed index dict, then a comprehension each for parents /
|
|
125
|
+
times / mask / root) — this is ~1.6x faster at a million tips. It stays Python-bound because
|
|
126
|
+
it walks the ``TreeNode`` object graph; a Rust-speed tree would have to be parsed/built as
|
|
127
|
+
arrays in Rust rather than materialised as Python objects first."""
|
|
128
|
+
nodes = []
|
|
129
|
+
parent = []
|
|
130
|
+
times = []
|
|
131
|
+
extant_leaf = []
|
|
132
|
+
pos: dict[int, int] = {} # id(node) -> pre-order index (parent seen before child)
|
|
133
|
+
root = -1
|
|
134
|
+
for i, n in enumerate(species_tree.nodes_preorder()):
|
|
135
|
+
nodes.append(n)
|
|
136
|
+
pos[id(n)] = i
|
|
137
|
+
p = n.parent
|
|
138
|
+
if p is None:
|
|
139
|
+
parent.append(-1)
|
|
140
|
+
root = i
|
|
141
|
+
else:
|
|
142
|
+
parent.append(pos[id(p)])
|
|
143
|
+
times.append(float(n.time))
|
|
144
|
+
extant_leaf.append(n.is_extant and not n.children)
|
|
145
|
+
return nodes, parent, times, extant_leaf, root
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _cap_and_seed(max_family_size, n_extant, seed):
|
|
149
|
+
cap = -1 if max_family_size is None else int(resolve_max_family_size(max_family_size, n_extant))
|
|
150
|
+
seed_val = (int(np.random.SeedSequence(seed).generate_state(1)[0])
|
|
151
|
+
if seed is None else int(seed))
|
|
152
|
+
return cap, seed_val
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _transfer_params(transfers):
|
|
156
|
+
"""(replacement, distance_decay, allow_self) for Rust; decay = -1.0 means uniform."""
|
|
157
|
+
if transfers is None:
|
|
158
|
+
return 0.0, -1.0, False
|
|
159
|
+
decay = transfers.distance_decay
|
|
160
|
+
return (float(transfers.replacement),
|
|
161
|
+
-1.0 if decay is None else float(decay),
|
|
162
|
+
bool(transfers.allow_self))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# --- profiles (counts only; the fast ABC / σ-dataset path) -----------------------
|
|
166
|
+
|
|
167
|
+
def profiles(species_tree, rates, *, initial_size, transfers, max_family_size, seed):
|
|
168
|
+
"""Simulate gene families in Rust over per-family *counts* only and return the profile
|
|
169
|
+
matrix — no gene ids, event log or trees. This is the path behind
|
|
170
|
+
``simulate_genomes(..., output="profiles")`` (the fast route for ABC / large σ datasets)."""
|
|
171
|
+
require()
|
|
172
|
+
d, t, l, o = _resolve_rates(rates)
|
|
173
|
+
nodes, parent, times, extant_leaf, root = _tree_arrays(species_tree)
|
|
174
|
+
cap, seed_val = _cap_and_seed(max_family_size, sum(extant_leaf), seed)
|
|
175
|
+
rep, dec, aself = _transfer_params(transfers)
|
|
176
|
+
|
|
177
|
+
result = _core.simulate_profiles(
|
|
178
|
+
len(nodes), parent, times, extant_leaf, root,
|
|
179
|
+
d, t, l, o, int(initial_size), cap, seed_val, rep, dec, aself,
|
|
180
|
+
)
|
|
181
|
+
return _assemble_profiles(result, nodes)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def profiles_parallel(species_tree, rates, *, initial_size, transfers, max_family_size, seed,
|
|
185
|
+
threads):
|
|
186
|
+
"""Parallel counts-only path (behind ``simulate_genomes(..., output="profiles", threads=N)``).
|
|
187
|
+
|
|
188
|
+
Runs ``threads`` **independent** copies of the engine, each with origination rate ``o/threads``
|
|
189
|
+
and a ``1/threads`` share of the founding families, and sums the profiles. Gene families are
|
|
190
|
+
independent and a Poisson process splits, so this is distributionally identical to one serial
|
|
191
|
+
run — a different but equivalent realization. The result depends on ``(seed, threads)`` but not
|
|
192
|
+
on scheduling (thread-count-independent given the copy count), and every transfer mode is
|
|
193
|
+
supported (the recipient choice depends only on the tree + donor, so it decomposes unchanged)."""
|
|
194
|
+
require()
|
|
195
|
+
d, t, l, o = _resolve_rates(rates)
|
|
196
|
+
nodes, parent, times, extant_leaf, root = _tree_arrays(species_tree)
|
|
197
|
+
cap, seed_val = _cap_and_seed(max_family_size, sum(extant_leaf), seed)
|
|
198
|
+
rep, dec, aself = _transfer_params(transfers)
|
|
199
|
+
|
|
200
|
+
# copies = threads (one balanced Poisson-thinned copy per worker); pool size = threads
|
|
201
|
+
result = _core.simulate_profiles_parallel(
|
|
202
|
+
len(nodes), parent, times, extant_leaf, root,
|
|
203
|
+
d, t, l, o, int(initial_size), cap, seed_val, rep, dec, aself,
|
|
204
|
+
int(threads), int(threads),
|
|
205
|
+
)
|
|
206
|
+
return _assemble_profiles(result, nodes)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _assemble_profiles(result, nodes) -> ProfileMatrix:
|
|
210
|
+
"""Build a ProfileMatrix from the engine's flat COO byte buffers (``simulate_profiles`` /
|
|
211
|
+
``simulate_trace``).
|
|
212
|
+
|
|
213
|
+
``result`` is ``(leaf_nodes, col, fam, cnt)`` of packed native-endian bytes: ``leaf_nodes[c]``
|
|
214
|
+
is the node index of column ``c`` (columns in the engine's emission order), then one cell per
|
|
215
|
+
present family as parallel ``u32`` column / ``u64`` family id / ``u32`` count. Everything is
|
|
216
|
+
``np.frombuffer``-d (one memcpy, no per-cell Python objects) and mapped to sorted family rows
|
|
217
|
+
and natkey-ordered species columns with vectorised numpy — the per-cell Python loop this
|
|
218
|
+
replaces was ~40% of the wall-clock at millions of tips. Stays sparse COO throughout, so the
|
|
219
|
+
dense O(N²) array is never built."""
|
|
220
|
+
from zombi2.genomes.profiles import _natkey
|
|
221
|
+
|
|
222
|
+
leaf_nodes_b, col_b, fam_b, cnt_b = result
|
|
223
|
+
leaf_idx = np.frombuffer(leaf_nodes_b, dtype=np.uint32)
|
|
224
|
+
col = np.frombuffer(col_b, dtype=np.uint32)
|
|
225
|
+
fam = np.frombuffer(fam_b, dtype=np.uint64)
|
|
226
|
+
cnt = np.frombuffer(cnt_b, dtype=np.uint32)
|
|
227
|
+
|
|
228
|
+
# species (columns): extant-leaf names in natkey order; remap the engine's emission-order
|
|
229
|
+
# column index to that order via the inverse permutation.
|
|
230
|
+
leaf_names = [nodes[i].name for i in leaf_idx.tolist()]
|
|
231
|
+
order = sorted(range(len(leaf_names)), key=lambda j: _natkey(leaf_names[j]))
|
|
232
|
+
species = [leaf_names[j] for j in order]
|
|
233
|
+
inv = np.empty(len(order), dtype=np.int64)
|
|
234
|
+
inv[np.asarray(order, dtype=np.int64)] = np.arange(len(order), dtype=np.int64)
|
|
235
|
+
cols = inv[col.astype(np.int64)] if col.size else np.zeros(0, dtype=np.int64)
|
|
236
|
+
|
|
237
|
+
# families (rows): sorted unique family ids, 1-based ZOMBI-style labels.
|
|
238
|
+
uniq = np.unique(fam) if fam.size else np.zeros(0, dtype=np.uint64)
|
|
239
|
+
families = [str(int(f) + 1) for f in uniq.tolist()]
|
|
240
|
+
rows = np.searchsorted(uniq, fam).astype(np.int64) if fam.size else np.zeros(0, dtype=np.int64)
|
|
241
|
+
|
|
242
|
+
return ProfileMatrix(families=families, species=species,
|
|
243
|
+
coo=(rows, cols, cnt.astype(np.int64)))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# --- full genealogy (event log + gene trees), materialized as a Genomes ----------
|
|
247
|
+
|
|
248
|
+
class _RustGene:
|
|
249
|
+
"""Minimal Gene stand-in for gid->species lookup (str gid, str family)."""
|
|
250
|
+
__slots__ = ("gid", "family")
|
|
251
|
+
|
|
252
|
+
def __init__(self, gid, family):
|
|
253
|
+
self.gid = gid
|
|
254
|
+
self.family = family
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class _RustGenome:
|
|
258
|
+
"""Minimal leaf genome exposing just what Genomes/ProfileMatrix need."""
|
|
259
|
+
__slots__ = ("_pairs", "_counts")
|
|
260
|
+
|
|
261
|
+
def __init__(self, pairs): # pairs: list[(gid:int, family:int)]
|
|
262
|
+
self._pairs = pairs
|
|
263
|
+
self._counts = Counter(str(f + 1) for _, f in pairs) # 1-based labels, ZOMBI-style
|
|
264
|
+
|
|
265
|
+
def genes(self):
|
|
266
|
+
# g-prefixed gids so the schema matches the Python engine (only the RNG differs)
|
|
267
|
+
return [_RustGene(f"g{gid}", str(fam + 1)) for gid, fam in self._pairs]
|
|
268
|
+
|
|
269
|
+
def families(self):
|
|
270
|
+
return list(self._counts.keys())
|
|
271
|
+
|
|
272
|
+
def copy_number(self, family):
|
|
273
|
+
return self._counts.get(family, 0)
|
|
274
|
+
|
|
275
|
+
def size(self):
|
|
276
|
+
return len(self._pairs)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# event code (Rust) -> EventType, and per-event GeneOp roles for genes[0], genes[1], genes[2]
|
|
280
|
+
_EV = (EventType.ORIGINATION, EventType.DUPLICATION, EventType.TRANSFER,
|
|
281
|
+
EventType.LOSS, EventType.SPECIATION)
|
|
282
|
+
_ROLES = (
|
|
283
|
+
("origin",), # O
|
|
284
|
+
("parent", "left", "right"), # D
|
|
285
|
+
("parent", "donor_copy", "transfer_copy"), # T
|
|
286
|
+
("lost",), # L
|
|
287
|
+
("parent", "child", "child"), # S
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _build_event_log(cols, nodes) -> EventLog:
|
|
292
|
+
ev, br, tm, dn, rc, fm, g0, g1, g2 = cols
|
|
293
|
+
names = [n.name for n in nodes]
|
|
294
|
+
log = EventLog()
|
|
295
|
+
add = log.add
|
|
296
|
+
for k in range(len(ev)):
|
|
297
|
+
code = ev[k]
|
|
298
|
+
family = str(fm[k] + 1) # 1-based labels, ZOMBI-style
|
|
299
|
+
roles = _ROLES[code]
|
|
300
|
+
genes = [GeneOp(f"g{g0[k]}", family, roles[0])] # g-prefixed gids (schema parity)
|
|
301
|
+
if len(roles) == 3:
|
|
302
|
+
genes.append(GeneOp(f"g{g1[k]}", family, roles[1]))
|
|
303
|
+
genes.append(GeneOp(f"g{g2[k]}", family, roles[2]))
|
|
304
|
+
donor = names[dn[k]] if dn[k] >= 0 else None
|
|
305
|
+
recipient = names[rc[k]] if rc[k] >= 0 else None
|
|
306
|
+
add(EventRecord(_EV[code], names[br[k]], tm[k], genes,
|
|
307
|
+
donor=donor, recipient=recipient))
|
|
308
|
+
return log
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def events_trace_tsv(columns, nodes) -> str:
|
|
312
|
+
"""Serialise the raw Rust event columns as the compact ``Events_trace.tsv`` text — the fast
|
|
313
|
+
path behind ``output="trace"``. This never builds an :class:`~zombi2.events.EventRecord`;
|
|
314
|
+
it formats the columns directly, which is the whole point (object construction is the wall
|
|
315
|
+
on large trees). The format matches the record-based
|
|
316
|
+
:func:`zombi2.simulation.events_trace_from_log` exactly (g-prefixed gids, 1-based families)."""
|
|
317
|
+
from zombi2.genomes.simulation import EVENTS_TRACE_HEADER
|
|
318
|
+
|
|
319
|
+
ev, br, tm, dn, rc, fm, g0, g1, g2 = columns
|
|
320
|
+
n = len(br)
|
|
321
|
+
names = np.array([nd.name for nd in nodes], dtype=object)
|
|
322
|
+
ev_a = np.frombuffer(ev, dtype=np.uint8)
|
|
323
|
+
br_a = np.asarray(br, dtype=np.int64)
|
|
324
|
+
dn_a = np.asarray(dn, dtype=np.int64)
|
|
325
|
+
rc_a = np.asarray(rc, dtype=np.int64)
|
|
326
|
+
# resolve names / event chars vectorially; -1 (no donor/recipient) becomes ""
|
|
327
|
+
evchar = np.array([e.value for e in _EV], dtype=object)[ev_a].tolist()
|
|
328
|
+
brn = names[br_a].tolist()
|
|
329
|
+
dnn = np.where(dn_a >= 0, names[np.clip(dn_a, 0, None)], "").tolist()
|
|
330
|
+
rcn = np.where(rc_a >= 0, names[np.clip(rc_a, 0, None)], "").tolist()
|
|
331
|
+
tm_l = list(tm)
|
|
332
|
+
fm_l = (np.asarray(fm, dtype=np.int64) + 1).tolist() # 1-based family labels
|
|
333
|
+
g0_l, g1_l, g2_l = list(g0), list(g1), list(g2)
|
|
334
|
+
|
|
335
|
+
def gid(x):
|
|
336
|
+
return f"g{x}" if x >= 0 else ""
|
|
337
|
+
|
|
338
|
+
rows = [EVENTS_TRACE_HEADER]
|
|
339
|
+
rows.extend(
|
|
340
|
+
f"{tm_l[k]:.10g}\t{evchar[k]}\t{brn[k]}\t{dnn[k]}\t{rcn[k]}\t{fm_l[k]}\t"
|
|
341
|
+
f"g{g0_l[k]}\t{gid(g1_l[k])}\t{gid(g2_l[k])}"
|
|
342
|
+
for k in range(n)
|
|
343
|
+
)
|
|
344
|
+
return "\n".join(rows) + "\n"
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def trace(species_tree, rates, *, initial_size, transfers, max_family_size, seed):
|
|
348
|
+
"""Simulate the compact **event trace** in Rust and return a :class:`~zombi2.GenomeTrace`.
|
|
349
|
+
|
|
350
|
+
Uses the ``simulate_trace`` engine: identical D/T/L/O dynamics to :func:`genomes`, but
|
|
351
|
+
speciations neither re-mint gene ids nor emit records (a lineage keeps its id across
|
|
352
|
+
speciations). The returned columns are therefore O/D/T/L only — ~6x smaller than the full
|
|
353
|
+
log in the low-rate regime — and the per-event Python objects are never built. The
|
|
354
|
+
genealogy is reconstructed on demand by replaying the trace against the species tree
|
|
355
|
+
(:func:`zombi2.reconciliation.expand_trace`). This is the path behind
|
|
356
|
+
``simulate_genomes(..., output="trace")``."""
|
|
357
|
+
require()
|
|
358
|
+
d, t, l, o = _resolve_rates(rates)
|
|
359
|
+
nodes, parent, times, extant_leaf, root = _tree_arrays(species_tree)
|
|
360
|
+
cap, seed_val = _cap_and_seed(max_family_size, sum(extant_leaf), seed)
|
|
361
|
+
rep, dec, aself = _transfer_params(transfers)
|
|
362
|
+
|
|
363
|
+
cols, leaf_coo = _core.simulate_trace(
|
|
364
|
+
len(nodes), parent, times, extant_leaf, root,
|
|
365
|
+
d, t, l, o, int(initial_size), cap, seed_val, rep, dec, aself,
|
|
366
|
+
)
|
|
367
|
+
# leaf_coo is the flat COO profile buffers (per-family counts, no per-gene objects); assemble
|
|
368
|
+
# straight from it. The compact trace's leaf identities are recovered by replaying against the
|
|
369
|
+
# species tree, not read off leaf genomes, so leaf_genomes stays empty for this path.
|
|
370
|
+
profs = _assemble_profiles(leaf_coo, nodes)
|
|
371
|
+
|
|
372
|
+
from zombi2.genomes.simulation import GenomeTrace
|
|
373
|
+
return GenomeTrace(species_tree=species_tree, leaf_genomes={},
|
|
374
|
+
profiles=profs, _columns=cols, _nodes=nodes)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def genomes(species_tree, rates, *, initial_size, transfers, max_family_size, seed):
|
|
378
|
+
"""Simulate the full genealogy in Rust and return a materialized :class:`~zombi2.Genomes`
|
|
379
|
+
(event log, gene trees, profiles, write). This is the built-in path behind the default
|
|
380
|
+
``simulate_genomes(...)`` (``output="genomes"``)."""
|
|
381
|
+
require()
|
|
382
|
+
d, t, l, o = _resolve_rates(rates)
|
|
383
|
+
nodes, parent, times, extant_leaf, root = _tree_arrays(species_tree)
|
|
384
|
+
cap, seed_val = _cap_and_seed(max_family_size, sum(extant_leaf), seed)
|
|
385
|
+
rep, dec, aself = _transfer_params(transfers)
|
|
386
|
+
|
|
387
|
+
cols, leaves = _core.simulate_log(
|
|
388
|
+
len(nodes), parent, times, extant_leaf, root,
|
|
389
|
+
d, t, l, o, int(initial_size), cap, seed_val, rep, dec, aself,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
leaf_genomes = {nodes[li]: _RustGenome(pairs) for li, pairs in leaves}
|
|
393
|
+
event_log = _build_event_log(cols, nodes)
|
|
394
|
+
profs = ProfileMatrix.from_leaf_genomes(leaf_genomes)
|
|
395
|
+
return Genomes(species_tree=species_tree, leaf_genomes=leaf_genomes,
|
|
396
|
+
event_log=event_log, profiles=profs)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# --- nucleotide-genome profile path (leaf segments -> blocks/profile/mosaics) ------
|
|
400
|
+
|
|
401
|
+
class _FastNucGenome:
|
|
402
|
+
"""Minimal leaf genome exposing just what NucleotideResult reads (segments + to_cells)."""
|
|
403
|
+
__slots__ = ("_segments", "_length")
|
|
404
|
+
|
|
405
|
+
def __init__(self, segments):
|
|
406
|
+
self._segments = segments
|
|
407
|
+
self._length = sum(s.length for s in segments)
|
|
408
|
+
|
|
409
|
+
def n_segments(self):
|
|
410
|
+
return len(self._segments)
|
|
411
|
+
|
|
412
|
+
def size(self):
|
|
413
|
+
return self._length
|
|
414
|
+
|
|
415
|
+
def to_cells(self):
|
|
416
|
+
out = []
|
|
417
|
+
for seg in self._segments:
|
|
418
|
+
if seg.strand == 1:
|
|
419
|
+
out.extend((seg.source, p, 1) for p in range(seg.src_start, seg.src_end))
|
|
420
|
+
else:
|
|
421
|
+
out.extend((seg.source, p, -1) for p in range(seg.src_end - 1, seg.src_start - 1, -1))
|
|
422
|
+
return out
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def nucleotide(species_tree, *, inversion, loss, duplication, transfer, transposition,
|
|
426
|
+
origination, root_length, extension, initial_size, transfers, seed):
|
|
427
|
+
"""Simulate the nucleotide structural model in Rust (leaf segments only) and return a
|
|
428
|
+
:class:`NucleotideResult` supporting ``profile_matrix()`` / ``leaf_mosaic()`` /
|
|
429
|
+
``trace_back()`` but not the event-log products. This is the path behind
|
|
430
|
+
``simulate_nucleotide_genomes(..., output="profiles")``."""
|
|
431
|
+
require()
|
|
432
|
+
from zombi2.genomes.events import EventLog
|
|
433
|
+
from zombi2.genomes.nucleotide_genome import Segment, SegmentRegistry
|
|
434
|
+
from zombi2.genomes.nucleotide_sim import _build_blocks
|
|
435
|
+
|
|
436
|
+
nodes, parent, times, extant_leaf, root = _tree_arrays(species_tree)
|
|
437
|
+
_, seed_val = _cap_and_seed(None, sum(extant_leaf), seed)
|
|
438
|
+
rep, dec, aself = _transfer_params(transfers)
|
|
439
|
+
|
|
440
|
+
leaves = _core.simulate_nucleotide(
|
|
441
|
+
len(nodes), parent, times, extant_leaf, root,
|
|
442
|
+
float(inversion), float(loss), float(duplication), float(transfer),
|
|
443
|
+
float(transposition), float(origination), int(root_length), float(extension),
|
|
444
|
+
int(initial_size), seed_val, rep, dec, aself,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
leaf_genomes = {}
|
|
448
|
+
for leaf_idx, segs in leaves:
|
|
449
|
+
objs = [Segment("", str(src + 1), start, end, strand) # 1-based source labels
|
|
450
|
+
for (src, start, end, strand) in segs]
|
|
451
|
+
leaf_genomes[nodes[leaf_idx]] = _FastNucGenome(objs)
|
|
452
|
+
|
|
453
|
+
blocks = _build_blocks(leaf_genomes, root_length)
|
|
454
|
+
return _FastNucleotideResult(
|
|
455
|
+
species_tree=species_tree, leaf_genomes=leaf_genomes,
|
|
456
|
+
event_log=EventLog(), registry=SegmentRegistry(), blocks=blocks, root_length=root_length,
|
|
457
|
+
)
|