phylogenie 2.1.0__py3-none-any.whl → 2.1.2__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.
- phylogenie/__init__.py +20 -1
- phylogenie/generators/configs.py +11 -12
- phylogenie/generators/dataset.py +6 -9
- phylogenie/generators/factories.py +19 -10
- phylogenie/generators/trees.py +28 -7
- phylogenie/io.py +22 -10
- phylogenie/tree.py +42 -8
- phylogenie/treesimulator/__init__.py +16 -0
- phylogenie/treesimulator/events/__init__.py +39 -0
- phylogenie/treesimulator/events/contact_tracing.py +125 -0
- phylogenie/treesimulator/{events.py → events/core.py} +29 -119
- phylogenie/treesimulator/events/mutations.py +105 -0
- phylogenie/treesimulator/gillespie.py +10 -15
- phylogenie/treesimulator/model.py +48 -20
- phylogenie/typings.py +0 -1
- phylogenie/utils.py +17 -0
- {phylogenie-2.1.0.dist-info → phylogenie-2.1.2.dist-info}/METADATA +1 -2
- phylogenie-2.1.2.dist-info/RECORD +32 -0
- phylogenie-2.1.0.dist-info/RECORD +0 -28
- {phylogenie-2.1.0.dist-info → phylogenie-2.1.2.dist-info}/LICENSE.txt +0 -0
- {phylogenie-2.1.0.dist-info → phylogenie-2.1.2.dist-info}/WHEEL +0 -0
- {phylogenie-2.1.0.dist-info → phylogenie-2.1.2.dist-info}/entry_points.txt +0 -0
|
@@ -1,106 +1,69 @@
|
|
|
1
|
-
from abc import ABC, abstractmethod
|
|
2
|
-
from collections.abc import Sequence
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
|
|
5
|
-
import numpy as np
|
|
6
1
|
from numpy.random import Generator
|
|
7
2
|
|
|
8
3
|
from phylogenie.skyline import (
|
|
9
4
|
SkylineMatrixCoercible,
|
|
10
|
-
SkylineParameter,
|
|
11
5
|
SkylineParameterLike,
|
|
12
6
|
SkylineVectorCoercible,
|
|
13
7
|
skyline_matrix,
|
|
14
|
-
skyline_parameter,
|
|
15
8
|
skyline_vector,
|
|
16
9
|
)
|
|
17
|
-
from phylogenie.treesimulator.model import Model
|
|
10
|
+
from phylogenie.treesimulator.model import Event, Model
|
|
18
11
|
|
|
19
12
|
INFECTIOUS_STATE = "I"
|
|
20
13
|
EXPOSED_STATE = "E"
|
|
21
14
|
SUPERSPREADER_STATE = "S"
|
|
22
|
-
CT_POSTFIX = "-CT"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _get_CT_state(state: str) -> str:
|
|
26
|
-
return f"{state}{CT_POSTFIX}"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def _is_CT_state(state: str) -> bool:
|
|
30
|
-
return state.endswith(CT_POSTFIX)
|
|
31
|
-
|
|
32
15
|
|
|
33
|
-
@dataclass
|
|
34
|
-
class Event(ABC):
|
|
35
|
-
rate: SkylineParameter
|
|
36
|
-
state: str
|
|
37
16
|
|
|
38
|
-
def draw_individual(self, model: Model, rng: Generator) -> int:
|
|
39
|
-
return rng.choice(model.get_population(self.state))
|
|
40
|
-
|
|
41
|
-
def get_propensity(self, model: Model, time: float) -> float:
|
|
42
|
-
n_individuals = model.count_individuals(self.state)
|
|
43
|
-
rate = self.rate.get_value_at_time(time)
|
|
44
|
-
if rate == np.inf and not n_individuals:
|
|
45
|
-
return 0
|
|
46
|
-
return rate * n_individuals
|
|
47
|
-
|
|
48
|
-
@abstractmethod
|
|
49
|
-
def apply(self, model: Model, time: float, rng: Generator) -> None: ...
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@dataclass
|
|
53
17
|
class Birth(Event):
|
|
54
|
-
child_state: str
|
|
18
|
+
def __init__(self, state: str, rate: SkylineParameterLike, child_state: str):
|
|
19
|
+
super().__init__(state, rate)
|
|
20
|
+
self.child_state = child_state
|
|
55
21
|
|
|
56
22
|
def apply(self, model: Model, time: float, rng: Generator) -> None:
|
|
57
23
|
individual = self.draw_individual(model, rng)
|
|
58
24
|
model.birth_from(individual, self.child_state, time)
|
|
59
25
|
|
|
26
|
+
def __repr__(self) -> str:
|
|
27
|
+
return f"Birth(state={self.state}, rate={self.rate}, child_state={self.child_state})"
|
|
28
|
+
|
|
60
29
|
|
|
61
30
|
class Death(Event):
|
|
62
31
|
def apply(self, model: Model, time: float, rng: Generator) -> None:
|
|
63
32
|
individual = self.draw_individual(model, rng)
|
|
64
33
|
model.remove(individual, time)
|
|
65
34
|
|
|
35
|
+
def __repr__(self) -> str:
|
|
36
|
+
return f"Death(state={self.state}, rate={self.rate})"
|
|
37
|
+
|
|
66
38
|
|
|
67
|
-
@dataclass
|
|
68
39
|
class Migration(Event):
|
|
69
|
-
target_state: str
|
|
40
|
+
def __init__(self, state: str, rate: SkylineParameterLike, target_state: str):
|
|
41
|
+
super().__init__(state, rate)
|
|
42
|
+
self.target_state = target_state
|
|
70
43
|
|
|
71
44
|
def apply(self, model: Model, time: float, rng: Generator) -> None:
|
|
72
45
|
individual = self.draw_individual(model, rng)
|
|
73
46
|
model.migrate(individual, self.target_state, time)
|
|
74
47
|
|
|
48
|
+
def __repr__(self) -> str:
|
|
49
|
+
return f"Migration(state={self.state}, rate={self.rate}, target_state={self.target_state})"
|
|
50
|
+
|
|
75
51
|
|
|
76
|
-
@dataclass
|
|
77
52
|
class Sampling(Event):
|
|
78
|
-
removal: bool
|
|
53
|
+
def __init__(self, state: str, rate: SkylineParameterLike, removal: bool):
|
|
54
|
+
super().__init__(state, rate)
|
|
55
|
+
self.removal = removal
|
|
79
56
|
|
|
80
57
|
def apply(self, model: Model, time: float, rng: Generator) -> None:
|
|
81
58
|
individual = self.draw_individual(model, rng)
|
|
82
59
|
model.sample(individual, time, self.removal)
|
|
83
60
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
class SamplingWithContactTracing(Event):
|
|
87
|
-
max_notified_contacts: int
|
|
88
|
-
notification_probability: SkylineParameter
|
|
89
|
-
|
|
90
|
-
def apply(self, model: Model, time: float, rng: Generator) -> None:
|
|
91
|
-
individual = self.draw_individual(model, rng)
|
|
92
|
-
model.sample(individual, time, True)
|
|
93
|
-
population = model.get_population()
|
|
94
|
-
for contact in model.get_lineage(individual)[-self.max_notified_contacts :]:
|
|
95
|
-
if contact in population:
|
|
96
|
-
state = model.get_state(contact)
|
|
97
|
-
p = self.notification_probability.get_value_at_time(time)
|
|
98
|
-
if not _is_CT_state(state) and rng.random() < p:
|
|
99
|
-
model.migrate(contact, _get_CT_state(state), time)
|
|
61
|
+
def __repr__(self) -> str:
|
|
62
|
+
return f"Sampling(state={self.state}, rate={self.rate}, removal={self.removal})"
|
|
100
63
|
|
|
101
64
|
|
|
102
65
|
def get_canonical_events(
|
|
103
|
-
states:
|
|
66
|
+
states: list[str],
|
|
104
67
|
sampling_rates: SkylineVectorCoercible,
|
|
105
68
|
remove_after_sampling: bool,
|
|
106
69
|
birth_rates: SkylineVectorCoercible = 0,
|
|
@@ -116,27 +79,27 @@ def get_canonical_events(
|
|
|
116
79
|
|
|
117
80
|
events: list[Event] = []
|
|
118
81
|
for i, state in enumerate(states):
|
|
119
|
-
events.append(Birth(birth_rates[i], state
|
|
120
|
-
events.append(Death(death_rates[i]
|
|
121
|
-
events.append(Sampling(sampling_rates[i],
|
|
82
|
+
events.append(Birth(state, birth_rates[i], state))
|
|
83
|
+
events.append(Death(state, death_rates[i]))
|
|
84
|
+
events.append(Sampling(state, sampling_rates[i], remove_after_sampling))
|
|
122
85
|
|
|
123
86
|
if migration_rates is not None:
|
|
124
87
|
migration_rates = skyline_matrix(migration_rates, N, N - 1)
|
|
125
88
|
for i, state in enumerate(states):
|
|
126
89
|
for j, other_state in enumerate([s for s in states if s != state]):
|
|
127
|
-
events.append(Migration(migration_rates[i, j],
|
|
90
|
+
events.append(Migration(state, migration_rates[i, j], other_state))
|
|
128
91
|
|
|
129
92
|
if birth_rates_among_states is not None:
|
|
130
93
|
birth_rates_among_states = skyline_matrix(birth_rates_among_states, N, N - 1)
|
|
131
94
|
for i, state in enumerate(states):
|
|
132
95
|
for j, other_state in enumerate([s for s in states if s != state]):
|
|
133
|
-
events.append(Birth(birth_rates_among_states[i, j],
|
|
96
|
+
events.append(Birth(state, birth_rates_among_states[i, j], other_state))
|
|
134
97
|
|
|
135
98
|
return [event for event in events if event.rate]
|
|
136
99
|
|
|
137
100
|
|
|
138
101
|
def get_FBD_events(
|
|
139
|
-
states:
|
|
102
|
+
states: list[str],
|
|
140
103
|
sampling_proportions: SkylineVectorCoercible = 1,
|
|
141
104
|
diversification: SkylineVectorCoercible = 0,
|
|
142
105
|
turnover: SkylineVectorCoercible = 0,
|
|
@@ -170,7 +133,7 @@ def get_FBD_events(
|
|
|
170
133
|
|
|
171
134
|
|
|
172
135
|
def get_epidemiological_events(
|
|
173
|
-
states:
|
|
136
|
+
states: list[str],
|
|
174
137
|
sampling_proportions: SkylineVectorCoercible = 1,
|
|
175
138
|
reproduction_numbers: SkylineVectorCoercible = 0,
|
|
176
139
|
become_uninfectious_rates: SkylineVectorCoercible = 0,
|
|
@@ -254,56 +217,3 @@ def get_BDSS_events(
|
|
|
254
217
|
become_uninfectious_rates=1 / infectious_period,
|
|
255
218
|
sampling_proportions=sampling_proportion,
|
|
256
219
|
)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
def get_contact_tracing_events(
|
|
260
|
-
events: Sequence[Event],
|
|
261
|
-
max_notified_contacts: int = 1,
|
|
262
|
-
notification_probability: SkylineParameterLike = 1,
|
|
263
|
-
sampling_rate_after_notification: SkylineParameterLike = np.inf,
|
|
264
|
-
samplable_states_after_notification: Sequence[str] | None = None,
|
|
265
|
-
) -> list[Event]:
|
|
266
|
-
ct_events = [e for e in events if not isinstance(e, Sampling)]
|
|
267
|
-
|
|
268
|
-
for event in events:
|
|
269
|
-
if isinstance(event, Migration):
|
|
270
|
-
ct_events.append(
|
|
271
|
-
Migration(
|
|
272
|
-
event.rate,
|
|
273
|
-
_get_CT_state(event.state),
|
|
274
|
-
_get_CT_state(event.target_state),
|
|
275
|
-
)
|
|
276
|
-
)
|
|
277
|
-
elif isinstance(event, Birth):
|
|
278
|
-
ct_events.append(
|
|
279
|
-
Birth(event.rate, _get_CT_state(event.state), event.child_state)
|
|
280
|
-
)
|
|
281
|
-
elif isinstance(event, Sampling):
|
|
282
|
-
if not event.removal:
|
|
283
|
-
raise ValueError(
|
|
284
|
-
"Contact tracing requires removal to be set for all sampling events."
|
|
285
|
-
)
|
|
286
|
-
ct_events.append(
|
|
287
|
-
SamplingWithContactTracing(
|
|
288
|
-
event.rate,
|
|
289
|
-
event.state,
|
|
290
|
-
max_notified_contacts,
|
|
291
|
-
skyline_parameter(notification_probability),
|
|
292
|
-
)
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
for state in (
|
|
296
|
-
samplable_states_after_notification
|
|
297
|
-
if samplable_states_after_notification is not None
|
|
298
|
-
else {e.state for e in events}
|
|
299
|
-
):
|
|
300
|
-
ct_events.append(
|
|
301
|
-
SamplingWithContactTracing(
|
|
302
|
-
skyline_parameter(sampling_rate_after_notification),
|
|
303
|
-
_get_CT_state(state),
|
|
304
|
-
max_notified_contacts,
|
|
305
|
-
skyline_parameter(notification_probability),
|
|
306
|
-
)
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
return ct_events
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Type
|
|
4
|
+
|
|
5
|
+
from numpy.random import Generator
|
|
6
|
+
|
|
7
|
+
from phylogenie.skyline import SkylineParameterLike
|
|
8
|
+
from phylogenie.treesimulator.events.contact_tracing import (
|
|
9
|
+
BirthWithContactTracing,
|
|
10
|
+
SamplingWithContactTracing,
|
|
11
|
+
)
|
|
12
|
+
from phylogenie.treesimulator.events.core import (
|
|
13
|
+
Birth,
|
|
14
|
+
Death,
|
|
15
|
+
Event,
|
|
16
|
+
Migration,
|
|
17
|
+
Sampling,
|
|
18
|
+
)
|
|
19
|
+
from phylogenie.treesimulator.model import Model
|
|
20
|
+
from phylogenie.utils import Distribution
|
|
21
|
+
|
|
22
|
+
MUTATION_PREFIX = "MUT-"
|
|
23
|
+
MUTATIONS_KEY = "MUTATIONS"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_mutation(state: str) -> str | None:
|
|
27
|
+
return state.split(".")[0] if state.startswith(MUTATION_PREFIX) else None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _get_mutated_state(mutation_id: int, state: str) -> str:
|
|
31
|
+
if state.startswith(MUTATION_PREFIX):
|
|
32
|
+
state = state.split(".")[1]
|
|
33
|
+
return f"{MUTATION_PREFIX}{mutation_id}.{state}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TargetType(str, Enum):
|
|
37
|
+
BIRTH = "birth"
|
|
38
|
+
DEATH = "death"
|
|
39
|
+
MIGRATION = "migration"
|
|
40
|
+
SAMPLING = "sampling"
|
|
41
|
+
MUTATION = "mutation"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Mutation(Event):
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
state: str,
|
|
48
|
+
rate: SkylineParameterLike,
|
|
49
|
+
rate_scalers: dict[TargetType, Distribution],
|
|
50
|
+
):
|
|
51
|
+
super().__init__(state, rate)
|
|
52
|
+
self.rate_scalers = rate_scalers
|
|
53
|
+
|
|
54
|
+
def apply(self, model: Model, time: float, rng: Generator) -> None:
|
|
55
|
+
if MUTATIONS_KEY not in model.context:
|
|
56
|
+
model.context[MUTATIONS_KEY] = 0
|
|
57
|
+
model.context[MUTATIONS_KEY] += 1
|
|
58
|
+
mutation_id = model.context[MUTATIONS_KEY]
|
|
59
|
+
|
|
60
|
+
individual = self.draw_individual(model, rng)
|
|
61
|
+
model.migrate(individual, _get_mutated_state(mutation_id, self.state), time)
|
|
62
|
+
|
|
63
|
+
rate_scalers = {
|
|
64
|
+
target_type: getattr(rng, rate_scaler.type)(**rate_scaler.args)
|
|
65
|
+
for target_type, rate_scaler in self.rate_scalers.items()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for event in [
|
|
69
|
+
deepcopy(e)
|
|
70
|
+
for e in model.events
|
|
71
|
+
if _get_mutation(self.state) == _get_mutation(e.state)
|
|
72
|
+
]:
|
|
73
|
+
event.state = _get_mutated_state(mutation_id, event.state)
|
|
74
|
+
if isinstance(event, Birth | BirthWithContactTracing):
|
|
75
|
+
event.child_state = _get_mutated_state(mutation_id, event.child_state)
|
|
76
|
+
elif isinstance(event, Migration):
|
|
77
|
+
event.target_state = _get_mutated_state(mutation_id, event.target_state)
|
|
78
|
+
elif not isinstance(
|
|
79
|
+
event, Mutation | Death | Sampling | SamplingWithContactTracing
|
|
80
|
+
):
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"Mutation not defined for event of type {type(event)}."
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
for target_type, rate_scaler in rate_scalers.items():
|
|
86
|
+
if target_type not in TARGETS:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"Unsupported target type {target_type} for mutation."
|
|
89
|
+
)
|
|
90
|
+
if isinstance(event, TARGETS[target_type]):
|
|
91
|
+
event.rate *= rate_scaler
|
|
92
|
+
|
|
93
|
+
model.add_event(event)
|
|
94
|
+
|
|
95
|
+
def __repr__(self) -> str:
|
|
96
|
+
return f"Mutation(state={self.state}, rate={self.rate}, rate_scalers={self.rate_scalers})"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
TARGETS: dict[TargetType, tuple[Type[Event], ...]] = {
|
|
100
|
+
TargetType.BIRTH: (Birth, BirthWithContactTracing),
|
|
101
|
+
TargetType.DEATH: (Death,),
|
|
102
|
+
TargetType.MIGRATION: (Migration,),
|
|
103
|
+
TargetType.SAMPLING: (Sampling, SamplingWithContactTracing),
|
|
104
|
+
TargetType.MUTATION: (Mutation,),
|
|
105
|
+
}
|
|
@@ -8,21 +8,21 @@ from tqdm import tqdm
|
|
|
8
8
|
|
|
9
9
|
from phylogenie.io import dump_newick
|
|
10
10
|
from phylogenie.tree import Tree
|
|
11
|
-
from phylogenie.treesimulator.
|
|
12
|
-
|
|
11
|
+
from phylogenie.treesimulator.model import Event, Model
|
|
12
|
+
|
|
13
|
+
MAX_TIPS = 2**32
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
def simulate_tree(
|
|
16
17
|
events: Sequence[Event],
|
|
17
18
|
min_tips: int = 1,
|
|
18
|
-
max_tips: int =
|
|
19
|
+
max_tips: int = MAX_TIPS,
|
|
19
20
|
max_time: float = np.inf,
|
|
20
21
|
init_state: str | None = None,
|
|
21
22
|
sampling_probability_at_present: float = 0.0,
|
|
22
|
-
max_tries: int | None = None,
|
|
23
23
|
seed: int | None = None,
|
|
24
24
|
) -> Tree | None:
|
|
25
|
-
if max_time == np.inf and max_tips ==
|
|
25
|
+
if max_time == np.inf and max_tips == MAX_TIPS:
|
|
26
26
|
raise ValueError("Either max_time or max_tips must be specified.")
|
|
27
27
|
|
|
28
28
|
if max_time == np.inf and sampling_probability_at_present:
|
|
@@ -30,7 +30,7 @@ def simulate_tree(
|
|
|
30
30
|
"sampling_probability_at_present cannot be set when max_time is infinite."
|
|
31
31
|
)
|
|
32
32
|
|
|
33
|
-
states = {e.state for e in events}
|
|
33
|
+
states = {e.state for e in events if e.state}
|
|
34
34
|
if init_state is None and len(states) > 1:
|
|
35
35
|
raise ValueError(
|
|
36
36
|
"Init state must be provided for models with more than one state."
|
|
@@ -41,15 +41,15 @@ def simulate_tree(
|
|
|
41
41
|
raise ValueError(f"Init state {init_state} not found in event states: {states}")
|
|
42
42
|
|
|
43
43
|
rng = default_rng(seed)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
model = Model(init_state)
|
|
44
|
+
while True:
|
|
45
|
+
model = Model(init_state, events)
|
|
47
46
|
current_time = 0.0
|
|
48
47
|
change_times = sorted(set(t for e in events for t in e.rate.change_times))
|
|
49
48
|
next_change_time = change_times.pop(0) if change_times else np.inf
|
|
50
49
|
|
|
51
50
|
target_n_tips = rng.integers(min_tips, max_tips) if max_time == np.inf else None
|
|
52
51
|
while current_time < max_time:
|
|
52
|
+
events = model.events
|
|
53
53
|
rates = [e.get_propensity(model, current_time) for e in events]
|
|
54
54
|
|
|
55
55
|
instantaneous_events = [e for e, r in zip(events, rates) if r == np.inf]
|
|
@@ -85,21 +85,17 @@ def simulate_tree(
|
|
|
85
85
|
|
|
86
86
|
if min_tips <= model.n_sampled <= max_tips:
|
|
87
87
|
return model.get_sampled_tree()
|
|
88
|
-
n_tries += 1
|
|
89
|
-
|
|
90
|
-
print("WARNING: Maximum number of tries reached, returning None.")
|
|
91
88
|
|
|
92
89
|
|
|
93
90
|
def generate_trees(
|
|
94
91
|
output_dir: str,
|
|
95
92
|
n_trees: int,
|
|
96
|
-
events:
|
|
93
|
+
events: list[Event],
|
|
97
94
|
min_tips: int = 1,
|
|
98
95
|
max_tips: int = 2**32,
|
|
99
96
|
max_time: float = np.inf,
|
|
100
97
|
init_state: str | None = None,
|
|
101
98
|
sampling_probability_at_present: float = 0.0,
|
|
102
|
-
max_tries: int | None = None,
|
|
103
99
|
seed: int | None = None,
|
|
104
100
|
n_jobs: int = -1,
|
|
105
101
|
) -> None:
|
|
@@ -116,7 +112,6 @@ def generate_trees(
|
|
|
116
112
|
max_time=max_time,
|
|
117
113
|
init_state=init_state,
|
|
118
114
|
sampling_probability_at_present=sampling_probability_at_present,
|
|
119
|
-
max_tries=max_tries,
|
|
120
115
|
seed=int(rng.integers(2**32)),
|
|
121
116
|
)
|
|
122
117
|
for _ in range(n_trees)
|
|
@@ -1,9 +1,35 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
1
2
|
from collections import defaultdict
|
|
3
|
+
from collections.abc import Sequence
|
|
2
4
|
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
3
6
|
|
|
7
|
+
import numpy as np
|
|
8
|
+
from numpy.random import Generator
|
|
9
|
+
|
|
10
|
+
from phylogenie.skyline import SkylineParameterLike, skyline_parameter
|
|
4
11
|
from phylogenie.tree import Tree
|
|
5
12
|
|
|
6
13
|
|
|
14
|
+
class Event(ABC):
|
|
15
|
+
def __init__(self, state: str, rate: SkylineParameterLike):
|
|
16
|
+
self.state = state
|
|
17
|
+
self.rate = skyline_parameter(rate)
|
|
18
|
+
|
|
19
|
+
def draw_individual(self, model: "Model", rng: Generator) -> int:
|
|
20
|
+
return rng.choice(model.get_population(self.state))
|
|
21
|
+
|
|
22
|
+
def get_propensity(self, model: "Model", time: float) -> float:
|
|
23
|
+
n_individuals = model.count_individuals(self.state)
|
|
24
|
+
rate = self.rate.get_value_at_time(time)
|
|
25
|
+
if rate == np.inf and not n_individuals:
|
|
26
|
+
return 0
|
|
27
|
+
return rate * n_individuals
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def apply(self, model: "Model", time: float, rng: Generator) -> None: ...
|
|
31
|
+
|
|
32
|
+
|
|
7
33
|
@dataclass
|
|
8
34
|
class Individual:
|
|
9
35
|
id: int
|
|
@@ -12,19 +38,27 @@ class Individual:
|
|
|
12
38
|
|
|
13
39
|
|
|
14
40
|
class Model:
|
|
15
|
-
def __init__(self, init_state: str):
|
|
41
|
+
def __init__(self, init_state: str, events: Sequence[Event]):
|
|
16
42
|
self._next_node_id = 0
|
|
17
43
|
self._next_individual_id = 0
|
|
18
44
|
self._population: dict[int, Individual] = {}
|
|
19
45
|
self._states: dict[str, set[int]] = defaultdict(set)
|
|
20
|
-
self._lineages: dict[int, list[Individual]] = defaultdict(list)
|
|
21
46
|
self._sampled: set[str] = set()
|
|
22
47
|
self._tree = self._get_new_individual(init_state).node
|
|
48
|
+
self._events = list(events)
|
|
49
|
+
self.context: dict[str, Any] = {}
|
|
23
50
|
|
|
24
51
|
@property
|
|
25
52
|
def n_sampled(self) -> int:
|
|
26
53
|
return len(self._sampled)
|
|
27
54
|
|
|
55
|
+
@property
|
|
56
|
+
def events(self) -> tuple[Event, ...]:
|
|
57
|
+
return tuple(self._events)
|
|
58
|
+
|
|
59
|
+
def add_event(self, event: Event) -> None:
|
|
60
|
+
self._events.append(event)
|
|
61
|
+
|
|
28
62
|
def _get_new_node(self, state: str) -> Tree:
|
|
29
63
|
self._next_node_id += 1
|
|
30
64
|
return Tree(f"{self._next_node_id}|{state}")
|
|
@@ -54,9 +88,8 @@ class Model:
|
|
|
54
88
|
def remove(self, id: int, time: float) -> None:
|
|
55
89
|
individual = self._population[id]
|
|
56
90
|
self._set_branch_length(individual.node, time)
|
|
57
|
-
state = individual.state
|
|
58
91
|
self._population.pop(id)
|
|
59
|
-
self._states[state].remove(id)
|
|
92
|
+
self._states[individual.state].remove(id)
|
|
60
93
|
|
|
61
94
|
def migrate(self, id: int, state: str, time: float) -> None:
|
|
62
95
|
individual = self._population[id]
|
|
@@ -65,13 +98,12 @@ class Model:
|
|
|
65
98
|
self._states[state].add(id)
|
|
66
99
|
self._stem(individual, time)
|
|
67
100
|
|
|
68
|
-
def birth_from(self, id: int, state: str, time: float) ->
|
|
101
|
+
def birth_from(self, id: int, state: str, time: float) -> int:
|
|
69
102
|
individual = self._population[id]
|
|
70
103
|
new_individual = self._get_new_individual(state)
|
|
71
104
|
individual.node.add_child(new_individual.node)
|
|
72
105
|
self._stem(individual, time)
|
|
73
|
-
|
|
74
|
-
self._lineages[new_individual.id].append(individual)
|
|
106
|
+
return new_individual.id
|
|
75
107
|
|
|
76
108
|
def sample(self, id: int, time: float, removal: bool) -> None:
|
|
77
109
|
individual = self._population[id]
|
|
@@ -85,9 +117,6 @@ class Model:
|
|
|
85
117
|
individual.node.add_child(sample_node)
|
|
86
118
|
self._stem(individual, time)
|
|
87
119
|
|
|
88
|
-
def get_lineage(self, id: int) -> list[int]:
|
|
89
|
-
return [individual.id for individual in self._lineages[id]]
|
|
90
|
-
|
|
91
120
|
def get_state(self, id: int) -> str:
|
|
92
121
|
return self._population[id].state
|
|
93
122
|
|
|
@@ -98,29 +127,28 @@ class Model:
|
|
|
98
127
|
if node.parent is None:
|
|
99
128
|
raise ValueError("No samples in the tree.")
|
|
100
129
|
else:
|
|
101
|
-
node.parent.
|
|
130
|
+
node.parent.remove_child(node)
|
|
102
131
|
elif len(node.children) == 1:
|
|
103
132
|
(child,) = node.children
|
|
104
|
-
child.
|
|
133
|
+
child.set_parent(node.parent)
|
|
105
134
|
assert child.branch_length is not None
|
|
106
135
|
assert node.branch_length is not None
|
|
107
136
|
child.branch_length += node.branch_length
|
|
108
137
|
if node.parent is None:
|
|
109
138
|
return child
|
|
110
139
|
else:
|
|
111
|
-
node.parent.
|
|
112
|
-
node.parent.children.remove(node)
|
|
140
|
+
node.parent.remove_child(node)
|
|
113
141
|
return tree
|
|
114
142
|
|
|
115
143
|
def get_full_tree(self) -> Tree:
|
|
116
144
|
return self._tree.copy()
|
|
117
145
|
|
|
118
|
-
def get_population(self,
|
|
119
|
-
if
|
|
146
|
+
def get_population(self, states: str | None = None) -> list[int]:
|
|
147
|
+
if states is None:
|
|
120
148
|
return list(self._population)
|
|
121
|
-
return list(self._states[
|
|
149
|
+
return list(self._states[states])
|
|
122
150
|
|
|
123
|
-
def count_individuals(self,
|
|
124
|
-
if
|
|
151
|
+
def count_individuals(self, states: str | None = None) -> int:
|
|
152
|
+
if states is None:
|
|
125
153
|
return len(self._population)
|
|
126
|
-
return len(self._states[
|
|
154
|
+
return len(self._states[states])
|
phylogenie/typings.py
CHANGED
phylogenie/utils.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StrictBaseModel(BaseModel):
|
|
7
|
+
model_config = ConfigDict(extra="forbid")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Distribution(BaseModel):
|
|
11
|
+
type: str
|
|
12
|
+
model_config = ConfigDict(extra="allow")
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def args(self) -> dict[str, Any]:
|
|
16
|
+
assert self.model_extra is not None
|
|
17
|
+
return self.model_extra
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: phylogenie
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2
|
|
4
4
|
Summary: Generate phylogenetic datasets with minimal setup effort
|
|
5
5
|
Author: Gabriele Marino
|
|
6
6
|
Author-email: gabmarino.8601@gmail.com
|
|
@@ -24,7 +24,6 @@ Description-Content-Type: text/markdown
|
|
|
24
24
|
|
|
25
25
|
[](https://iqtree.github.io/doc/AliSim)
|
|
26
26
|
[](https://pypi.org/project/phylogenie/)
|
|
27
|
-
[](https://pypi.org/project/phylogenie/)
|
|
28
27
|
|
|
29
28
|
Phylogenie is a [Python](https://www.python.org/) package designed to easily simulate phylogenetic datasets—such as trees and multiple sequence alignments (MSAs)—with minimal setup effort. Simply specify the distributions from which your parameters should be sampled, and Phylogenie will handle the rest!
|
|
30
29
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
phylogenie/__init__.py,sha256=T2mRLsYtoLlWt8GlxrrUnfXJ9XVioq7hTvVq3uJpwQI,2215
|
|
2
|
+
phylogenie/generators/__init__.py,sha256=zsOxy28-9j9alOQLIgrOAFfmM58NNHO_NEtW-KXQXAY,888
|
|
3
|
+
phylogenie/generators/alisim.py,sha256=dDqlSwLDbRE2u5SZlsq1mArobTBtuk0aeXY3m1N-bWA,2374
|
|
4
|
+
phylogenie/generators/configs.py,sha256=AiiFS6rpH9BPwDKCkT4SVrRzfLFFrwRCJM4CRj0Srdk,1072
|
|
5
|
+
phylogenie/generators/dataset.py,sha256=wYtb5fxjrM0bD9KVDlgZPpiR4ACezdKKfsmsXyc4__0,2755
|
|
6
|
+
phylogenie/generators/factories.py,sha256=14p1wvJtjdXgM4mPqV9vX34HYcKpemx9TPfemvoB5Wo,7265
|
|
7
|
+
phylogenie/generators/trees.py,sha256=EEj5KPuu7FSOoti_gGTmgWgiX68rEKFh57Hp6BDw8po,10049
|
|
8
|
+
phylogenie/generators/typeguards.py,sha256=yj4VkhOaUXJ2OrY-6zhOeY9C4yKIQxjZtk2d-vIxttQ,828
|
|
9
|
+
phylogenie/io.py,sha256=y7nQIvLgCvqELsXFKfm1GgKJO_saoQ-7zQpE3Kvajzc,3509
|
|
10
|
+
phylogenie/main.py,sha256=vtvSpQxBNlYABoFQ25czl-l3fIr4QRo3svWVd-jcArw,1170
|
|
11
|
+
phylogenie/msa.py,sha256=JDGyZUsAq6-m-SQjoCDjAkAZIxfgyl_PDIhdYn5HOow,2064
|
|
12
|
+
phylogenie/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
phylogenie/skyline/__init__.py,sha256=7pF4CUb4ZCLzNYJNhOjpuTOLTRhlK7L6ugfccNqjIGo,620
|
|
14
|
+
phylogenie/skyline/matrix.py,sha256=Gl8OgKjtieG0NwPYiPimKI36gefV8fm_OeorjdXxPTs,9146
|
|
15
|
+
phylogenie/skyline/parameter.py,sha256=EM9qlPt0JhMBy3TbztM0dj24BaGNEy8KWKdTObDKhbI,4644
|
|
16
|
+
phylogenie/skyline/vector.py,sha256=bJP7_FNX_Klt6wXqsyfj0KX3VNj6-dIhzCKSJuQcOV0,7115
|
|
17
|
+
phylogenie/tree.py,sha256=Cum74mTdmgfGXk25dnvUngr4zDYRyWFq5zThBh0QFog,2677
|
|
18
|
+
phylogenie/treesimulator/__init__.py,sha256=XG_xwETKWgDmCihqNUFCcMHtFg4WvZu5qbqWn9Dndt8,879
|
|
19
|
+
phylogenie/treesimulator/events/__init__.py,sha256=UGfvXOVJ_ZAkk_8sBPihjmxciiaEnXZEPFIY53sttWI,940
|
|
20
|
+
phylogenie/treesimulator/events/contact_tracing.py,sha256=_nJ85yhgGkeruQgMHvGpDYoyhheBf8M4LgZWiWdi5dY,4801
|
|
21
|
+
phylogenie/treesimulator/events/core.py,sha256=JokGmieAv2xEX7KsjBWZr05jHN1jB-XZbpxe9gwdbDA,7953
|
|
22
|
+
phylogenie/treesimulator/events/mutations.py,sha256=xkXUIppbLIWZqKwVf-hi7d-_pS42TG2EPVfJA_grxBg,3443
|
|
23
|
+
phylogenie/treesimulator/gillespie.py,sha256=TMDNKBkFwVyAEhBlbwxCTA61GuGwP-42HxpsAVXiU0s,4275
|
|
24
|
+
phylogenie/treesimulator/model.py,sha256=0Im6cFTlpMlJrSP4pTTKtvLT9qrQWV8MSTesAsBxT8g,5422
|
|
25
|
+
phylogenie/typeguards.py,sha256=JtqmbEWJZBRHbWgCvcl6nrWm3VcBfzRbklbTBYHItn0,1325
|
|
26
|
+
phylogenie/typings.py,sha256=GknvAFXyiaWeeYJ8Lk5d6E2VHT-xW6ONEojYbtJYiB8,476
|
|
27
|
+
phylogenie/utils.py,sha256=pCg9ob0RpLUHwM49x4knKxL4FNPr3-EU_6zMXsvxtAg,370
|
|
28
|
+
phylogenie-2.1.2.dist-info/LICENSE.txt,sha256=NUrDqElK-eD3I0WqC004CJsy6cs0JgsAoebDv_42-pw,1071
|
|
29
|
+
phylogenie-2.1.2.dist-info/METADATA,sha256=RoxXfMaPI6auRdjONAE8HfcfhZjHX0hTmx_oJlrzaws,5375
|
|
30
|
+
phylogenie-2.1.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
31
|
+
phylogenie-2.1.2.dist-info/entry_points.txt,sha256=Rt6_usN0FkBX1ZfiqCirjMN9FKOgFLG8rydcQ8kugeE,51
|
|
32
|
+
phylogenie-2.1.2.dist-info/RECORD,,
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
phylogenie/__init__.py,sha256=sv8sfqkbarYgeaCFGiGMtV_fTUPuXEyJS_I3W9mlIto,1801
|
|
2
|
-
phylogenie/generators/__init__.py,sha256=zsOxy28-9j9alOQLIgrOAFfmM58NNHO_NEtW-KXQXAY,888
|
|
3
|
-
phylogenie/generators/alisim.py,sha256=dDqlSwLDbRE2u5SZlsq1mArobTBtuk0aeXY3m1N-bWA,2374
|
|
4
|
-
phylogenie/generators/configs.py,sha256=4jSBUZiFo2GacXWed5dy7lUEkaOWZkZG-KY9vHfhqGU,993
|
|
5
|
-
phylogenie/generators/dataset.py,sha256=UTsf8u868_8K6aMwIpLZrIfSY7s9skXlLUqQBuetiNQ,2954
|
|
6
|
-
phylogenie/generators/factories.py,sha256=Y7cTsIblyV9T7ZhYvyQ5Wd7JcCpMG1dkEF8jJ1iQxN8,6928
|
|
7
|
-
phylogenie/generators/trees.py,sha256=R5ZlZ-UNy2euPBt2P3lyit4txC3xweLrPPA1FI6m5PQ,9210
|
|
8
|
-
phylogenie/generators/typeguards.py,sha256=yj4VkhOaUXJ2OrY-6zhOeY9C4yKIQxjZtk2d-vIxttQ,828
|
|
9
|
-
phylogenie/io.py,sha256=d1xF6rwER6KpnA5OrFgDn7Ow-YymvxA7OXX7hi_nfB4,2951
|
|
10
|
-
phylogenie/main.py,sha256=vtvSpQxBNlYABoFQ25czl-l3fIr4QRo3svWVd-jcArw,1170
|
|
11
|
-
phylogenie/msa.py,sha256=JDGyZUsAq6-m-SQjoCDjAkAZIxfgyl_PDIhdYn5HOow,2064
|
|
12
|
-
phylogenie/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
phylogenie/skyline/__init__.py,sha256=7pF4CUb4ZCLzNYJNhOjpuTOLTRhlK7L6ugfccNqjIGo,620
|
|
14
|
-
phylogenie/skyline/matrix.py,sha256=Gl8OgKjtieG0NwPYiPimKI36gefV8fm_OeorjdXxPTs,9146
|
|
15
|
-
phylogenie/skyline/parameter.py,sha256=EM9qlPt0JhMBy3TbztM0dj24BaGNEy8KWKdTObDKhbI,4644
|
|
16
|
-
phylogenie/skyline/vector.py,sha256=bJP7_FNX_Klt6wXqsyfj0KX3VNj6-dIhzCKSJuQcOV0,7115
|
|
17
|
-
phylogenie/tree.py,sha256=dk8Sj1tqyGOunVO2crtIqb0LH-ws-PXqA8SuNcYfVHI,1738
|
|
18
|
-
phylogenie/treesimulator/__init__.py,sha256=DGn_sRDwL4OY1x1fT36kh4ghhwqSGt_8FnrV_TcQCjs,563
|
|
19
|
-
phylogenie/treesimulator/events.py,sha256=xV64Y_oH9tsAFVVJEGW6-VgiOcX-xNPR_niTxTmpARo,10583
|
|
20
|
-
phylogenie/treesimulator/gillespie.py,sha256=q5t0jfZWRqoyoiXiImMmo8fXqo7Cw1ea-OS_8aCD6Mc,4491
|
|
21
|
-
phylogenie/treesimulator/model.py,sha256=Zl82nlbuq0htrLZV7x5LAB-thuN4lzbDv5pDSrG3oM8,4595
|
|
22
|
-
phylogenie/typeguards.py,sha256=JtqmbEWJZBRHbWgCvcl6nrWm3VcBfzRbklbTBYHItn0,1325
|
|
23
|
-
phylogenie/typings.py,sha256=O1X6lGKTjJ2YJz3ApQ-rYb_tEJNUIcHdUIeYlSM4s5o,500
|
|
24
|
-
phylogenie-2.1.0.dist-info/LICENSE.txt,sha256=NUrDqElK-eD3I0WqC004CJsy6cs0JgsAoebDv_42-pw,1071
|
|
25
|
-
phylogenie-2.1.0.dist-info/METADATA,sha256=5_QZd6c3rvlHwhRC-TT6bqcNG6gZDJJGLuNWVUorzlg,5472
|
|
26
|
-
phylogenie-2.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
27
|
-
phylogenie-2.1.0.dist-info/entry_points.txt,sha256=Rt6_usN0FkBX1ZfiqCirjMN9FKOgFLG8rydcQ8kugeE,51
|
|
28
|
-
phylogenie-2.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|