phylogenie 3.1.5__tar.gz → 3.1.8__tar.gz
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-3.1.5/src/phylogenie.egg-info → phylogenie-3.1.8}/PKG-INFO +1 -1
- {phylogenie-3.1.5 → phylogenie-3.1.8}/pyproject.toml +1 -1
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/__init__.py +17 -4
- phylogenie-3.1.8/src/phylogenie/draw.py +694 -0
- phylogenie-3.1.8/src/phylogenie/io/__init__.py +3 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/io/fasta.py +8 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/mixins.py +3 -11
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/treesimulator/__init__.py +6 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/treesimulator/features.py +6 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/treesimulator/io/nexus.py +2 -2
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/treesimulator/tree.py +15 -2
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/treesimulator/utils.py +11 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8/src/phylogenie.egg-info}/PKG-INFO +1 -1
- phylogenie-3.1.5/src/phylogenie/draw.py +0 -152
- phylogenie-3.1.5/src/phylogenie/io/__init__.py +0 -3
- {phylogenie-3.1.5 → phylogenie-3.1.8}/LICENSE.txt +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/README.md +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/setup.cfg +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/generators/__init__.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/generators/alisim.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/generators/configs.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/generators/dataset.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/generators/factories.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/generators/trees.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/generators/typeguards.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/main.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/msa.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/py.typed +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/skyline/__init__.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/skyline/matrix.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/skyline/parameter.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/skyline/vector.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/treesimulator/events/__init__.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/treesimulator/events/base.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/treesimulator/events/contact_tracing.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/treesimulator/events/core.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/treesimulator/events/mutations.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/treesimulator/gillespie.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/treesimulator/io/__init__.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/treesimulator/io/newick.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/treesimulator/model.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/typeguards.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/typings.py +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie.egg-info/SOURCES.txt +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie.egg-info/dependency_links.txt +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie.egg-info/entry_points.txt +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie.egg-info/requires.txt +0 -0
- {phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie.egg-info/top_level.txt +0 -0
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
from phylogenie.draw import
|
|
1
|
+
from phylogenie.draw import (
|
|
2
|
+
draw_colored_dated_tree_categorical,
|
|
3
|
+
draw_colored_dated_tree_continuous,
|
|
4
|
+
draw_colored_tree_categorical,
|
|
5
|
+
draw_colored_tree_continuous,
|
|
6
|
+
draw_dated_tree,
|
|
7
|
+
draw_tree,
|
|
8
|
+
)
|
|
2
9
|
from phylogenie.generators import (
|
|
3
10
|
AliSimDatasetGenerator,
|
|
4
11
|
BDEITreeDatasetGenerator,
|
|
@@ -11,8 +18,8 @@ from phylogenie.generators import (
|
|
|
11
18
|
FBDTreeDatasetGenerator,
|
|
12
19
|
TreeDatasetGeneratorConfig,
|
|
13
20
|
)
|
|
14
|
-
from phylogenie.io import load_fasta
|
|
15
|
-
from phylogenie.msa import MSA
|
|
21
|
+
from phylogenie.io import dump_fasta, load_fasta
|
|
22
|
+
from phylogenie.msa import MSA, Sequence
|
|
16
23
|
from phylogenie.skyline import (
|
|
17
24
|
SkylineMatrix,
|
|
18
25
|
SkylineMatrixCoercible,
|
|
@@ -64,7 +71,11 @@ from phylogenie.treesimulator import (
|
|
|
64
71
|
)
|
|
65
72
|
|
|
66
73
|
__all__ = [
|
|
67
|
-
"
|
|
74
|
+
"draw_colored_dated_tree_categorical",
|
|
75
|
+
"draw_colored_dated_tree_continuous",
|
|
76
|
+
"draw_colored_tree_categorical",
|
|
77
|
+
"draw_colored_tree_continuous",
|
|
78
|
+
"draw_dated_tree",
|
|
68
79
|
"draw_tree",
|
|
69
80
|
"AliSimDatasetGenerator",
|
|
70
81
|
"BDEITreeDatasetGenerator",
|
|
@@ -76,8 +87,10 @@ __all__ = [
|
|
|
76
87
|
"EpidemiologicalTreeDatasetGenerator",
|
|
77
88
|
"FBDTreeDatasetGenerator",
|
|
78
89
|
"TreeDatasetGeneratorConfig",
|
|
90
|
+
"dump_fasta",
|
|
79
91
|
"load_fasta",
|
|
80
92
|
"MSA",
|
|
93
|
+
"Sequence",
|
|
81
94
|
"SkylineMatrix",
|
|
82
95
|
"SkylineMatrixCoercible",
|
|
83
96
|
"SkylineParameter",
|
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any, Literal, overload
|
|
4
|
+
|
|
5
|
+
import matplotlib.colors as mcolors
|
|
6
|
+
import matplotlib.dates as mdates
|
|
7
|
+
import matplotlib.patches as mpatches
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
from matplotlib.axes import Axes
|
|
10
|
+
from matplotlib.colors import Colormap
|
|
11
|
+
from mpl_toolkits.axes_grid1.inset_locator import inset_axes # pyright: ignore
|
|
12
|
+
|
|
13
|
+
from phylogenie.treesimulator import (
|
|
14
|
+
Tree,
|
|
15
|
+
get_node_ages,
|
|
16
|
+
get_node_depth_levels,
|
|
17
|
+
get_node_depths,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class CalibrationNode:
|
|
23
|
+
node: Tree
|
|
24
|
+
date: datetime
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
Color = str | tuple[float, float, float] | tuple[float, float, float, float]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def draw_tree(
|
|
31
|
+
tree: Tree,
|
|
32
|
+
ax: Axes | None = None,
|
|
33
|
+
colors: Color | dict[Tree, Color] = "black",
|
|
34
|
+
backward_time: bool = False,
|
|
35
|
+
) -> Axes:
|
|
36
|
+
"""
|
|
37
|
+
Draw a phylogenetic tree with colored branches.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
tree : Tree
|
|
42
|
+
The phylogenetic tree to draw.
|
|
43
|
+
ax : Axes | None, optional
|
|
44
|
+
The matplotlib Axes to draw on. If None, uses the current Axes.
|
|
45
|
+
colors : Color | dict[Tree, Color], optional
|
|
46
|
+
A single color for all branches or a dictionary mapping each node to a color.
|
|
47
|
+
backward_time : bool, optional
|
|
48
|
+
If True, the x-axis is inverted to represent time going backward.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
Axes
|
|
53
|
+
The Axes with the drawn tree.
|
|
54
|
+
"""
|
|
55
|
+
if ax is None:
|
|
56
|
+
ax = plt.gca()
|
|
57
|
+
|
|
58
|
+
if not isinstance(colors, dict):
|
|
59
|
+
colors = {node: colors for node in tree}
|
|
60
|
+
|
|
61
|
+
xs = (
|
|
62
|
+
get_node_ages(tree)
|
|
63
|
+
if backward_time
|
|
64
|
+
else get_node_depth_levels(tree)
|
|
65
|
+
if any(node.branch_length is None for node in tree.iter_descendants())
|
|
66
|
+
else get_node_depths(tree)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
ys: dict[Tree, float] = {node: i for i, node in enumerate(tree.get_leaves())}
|
|
70
|
+
for node in tree.postorder_traversal():
|
|
71
|
+
if node.is_internal():
|
|
72
|
+
ys[node] = sum(ys[child] for child in node.children) / len(node.children)
|
|
73
|
+
|
|
74
|
+
if tree.branch_length is not None:
|
|
75
|
+
xmin = xs[tree] + tree.branch_length if backward_time else 0
|
|
76
|
+
ax.hlines(y=ys[tree], xmin=xmin, xmax=xs[tree], color=colors[tree]) # pyright: ignore
|
|
77
|
+
for node in tree:
|
|
78
|
+
x1, y1 = xs[node], ys[node]
|
|
79
|
+
for child in node.children:
|
|
80
|
+
x2, y2 = xs[child], ys[child]
|
|
81
|
+
ax.hlines(y=y2, xmin=x1, xmax=x2, color=colors[child]) # pyright: ignore
|
|
82
|
+
ax.vlines(x=x1, ymin=y1, ymax=y2, color=colors[child]) # pyright: ignore
|
|
83
|
+
|
|
84
|
+
if backward_time:
|
|
85
|
+
ax.invert_xaxis()
|
|
86
|
+
|
|
87
|
+
ax.set_yticks([]) # pyright: ignore
|
|
88
|
+
return ax
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _depth_to_date(
|
|
92
|
+
depth: float, calibration_nodes: tuple[CalibrationNode, CalibrationNode]
|
|
93
|
+
) -> datetime:
|
|
94
|
+
"""
|
|
95
|
+
Convert a depth value to a date using linear interpolation between two calibration nodes.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
depth : float
|
|
100
|
+
The depth value to convert.
|
|
101
|
+
calibration_nodes : tuple[CalibrationNode, CalibrationNode]
|
|
102
|
+
Two calibration nodes defining the mapping from depth to date.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
datetime
|
|
107
|
+
The interpolated date corresponding to the given depth.
|
|
108
|
+
"""
|
|
109
|
+
node1, node2 = calibration_nodes
|
|
110
|
+
depth1, depth2 = node1.node.depth, node2.node.depth
|
|
111
|
+
date1, date2 = node1.date, node2.date
|
|
112
|
+
return date1 + (depth - depth1) * (date2 - date1) / (depth2 - depth1)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def draw_dated_tree(
|
|
116
|
+
tree: Tree,
|
|
117
|
+
calibration_nodes: tuple[CalibrationNode, CalibrationNode],
|
|
118
|
+
ax: Axes | None = None,
|
|
119
|
+
colors: Color | dict[Tree, Color] = "black",
|
|
120
|
+
) -> Axes:
|
|
121
|
+
"""
|
|
122
|
+
Draw a phylogenetic tree with branches positioned according to calibrated dates.
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
----------
|
|
126
|
+
tree : Tree
|
|
127
|
+
The phylogenetic tree to draw.
|
|
128
|
+
calibration_nodes : tuple[CalibrationNode, CalibrationNode]
|
|
129
|
+
Two calibration nodes defining the mapping from depth to date.
|
|
130
|
+
ax : Axes | None, optional
|
|
131
|
+
The matplotlib Axes to draw on. If None, uses the current Axes.
|
|
132
|
+
colors : Color | dict[Tree, Color], optional
|
|
133
|
+
A single color for all branches or a dictionary mapping each node to a color.
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
Axes
|
|
138
|
+
The Axes with the drawn dated tree.
|
|
139
|
+
"""
|
|
140
|
+
if ax is None:
|
|
141
|
+
ax = plt.gca()
|
|
142
|
+
|
|
143
|
+
if not isinstance(colors, dict):
|
|
144
|
+
colors = {node: colors for node in tree}
|
|
145
|
+
|
|
146
|
+
xs = {
|
|
147
|
+
node: _depth_to_date(depth=depth, calibration_nodes=calibration_nodes)
|
|
148
|
+
for node, depth in get_node_depths(tree).items()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
ys: dict[Tree, float] = {node: i for i, node in enumerate(tree.get_leaves())}
|
|
152
|
+
for node in tree.postorder_traversal():
|
|
153
|
+
if node.is_internal():
|
|
154
|
+
ys[node] = sum(ys[child] for child in node.children) / len(node.children)
|
|
155
|
+
|
|
156
|
+
if tree.branch_length is not None:
|
|
157
|
+
origin_date = _depth_to_date(depth=0, calibration_nodes=calibration_nodes)
|
|
158
|
+
ax.hlines( # pyright: ignore
|
|
159
|
+
y=ys[tree],
|
|
160
|
+
xmin=mdates.date2num(origin_date), # pyright: ignore
|
|
161
|
+
xmax=mdates.date2num(xs[tree]), # pyright: ignore
|
|
162
|
+
color=colors[tree],
|
|
163
|
+
)
|
|
164
|
+
for node in tree:
|
|
165
|
+
x1, y1 = xs[node], ys[node]
|
|
166
|
+
for child in node.children:
|
|
167
|
+
x2, y2 = xs[child], ys[child]
|
|
168
|
+
ax.hlines( # pyright: ignore
|
|
169
|
+
y=y2,
|
|
170
|
+
xmin=mdates.date2num(x1), # pyright: ignore
|
|
171
|
+
xmax=mdates.date2num(x2), # pyright: ignore
|
|
172
|
+
color=colors[child],
|
|
173
|
+
)
|
|
174
|
+
ax.vlines(x=mdates.date2num(x1), ymin=y1, ymax=y2, color=colors[child]) # pyright: ignore
|
|
175
|
+
|
|
176
|
+
ax.xaxis.set_major_locator(mdates.AutoDateLocator())
|
|
177
|
+
ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m-%d"))
|
|
178
|
+
ax.tick_params(axis="x", labelrotation=45) # pyright: ignore
|
|
179
|
+
|
|
180
|
+
ax.set_yticks([]) # pyright: ignore
|
|
181
|
+
return ax
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _init_colored_tree_categorical(
|
|
185
|
+
tree: Tree,
|
|
186
|
+
color_by: str,
|
|
187
|
+
ax: Axes | None = None,
|
|
188
|
+
default_color: Color = "black",
|
|
189
|
+
colormap: str | Colormap = "tab20",
|
|
190
|
+
show_legend: bool = True,
|
|
191
|
+
labels: dict[Any, str] | None = None,
|
|
192
|
+
legend_kwargs: dict[str, Any] | None = None,
|
|
193
|
+
) -> tuple[Axes, dict[Tree, Color]]:
|
|
194
|
+
"""
|
|
195
|
+
Initialize colors for drawing a tree based on categorical metadata.
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
tree : Tree
|
|
200
|
+
The phylogenetic tree.
|
|
201
|
+
color_by : str
|
|
202
|
+
The metadata key to color branches by.
|
|
203
|
+
ax : Axes | None, optional
|
|
204
|
+
The matplotlib Axes to draw on. If None, uses the current Axes.
|
|
205
|
+
default_color : Color, optional
|
|
206
|
+
The color to use for nodes without the specified metadata.
|
|
207
|
+
colormap : str | Colormap, optional
|
|
208
|
+
The colormap to use for coloring categories. Defaults to 'tab20'.
|
|
209
|
+
show_legend : bool, optional
|
|
210
|
+
Whether to display a legend for the categories.
|
|
211
|
+
labels : dict[Any, str] | None, optional
|
|
212
|
+
A mapping from category values to labels for the legend.
|
|
213
|
+
legend_kwargs : dict[str, Any] | None, optional
|
|
214
|
+
Additional keyword arguments to pass to the legend.
|
|
215
|
+
|
|
216
|
+
Returns
|
|
217
|
+
-------
|
|
218
|
+
tuple[Axes, dict[Tree, Color]]
|
|
219
|
+
The Axes and a dictionary mapping each node to its assigned color.
|
|
220
|
+
"""
|
|
221
|
+
if ax is None:
|
|
222
|
+
ax = plt.gca()
|
|
223
|
+
|
|
224
|
+
if isinstance(colormap, str):
|
|
225
|
+
colormap = plt.get_cmap(colormap)
|
|
226
|
+
|
|
227
|
+
features = {node: node[color_by] for node in tree if color_by in node.metadata}
|
|
228
|
+
feature_colors = {
|
|
229
|
+
f: mcolors.to_hex(colormap(i)) for i, f in enumerate(set(features.values()))
|
|
230
|
+
}
|
|
231
|
+
colors = {
|
|
232
|
+
node: feature_colors[features[node]] if node in features else default_color
|
|
233
|
+
for node in tree
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if show_legend:
|
|
237
|
+
legend_handles = [
|
|
238
|
+
mpatches.Patch(
|
|
239
|
+
color=feature_colors[f],
|
|
240
|
+
label=str(f) if labels is None else labels[f],
|
|
241
|
+
)
|
|
242
|
+
for f in feature_colors
|
|
243
|
+
]
|
|
244
|
+
if any(color_by not in node.metadata for node in tree):
|
|
245
|
+
legend_handles.append(mpatches.Patch(color=default_color, label="NA"))
|
|
246
|
+
if legend_kwargs is None:
|
|
247
|
+
legend_kwargs = {}
|
|
248
|
+
ax.legend(handles=legend_handles, **legend_kwargs) # pyright: ignore
|
|
249
|
+
|
|
250
|
+
return ax, colors
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def draw_colored_tree_categorical(
|
|
254
|
+
tree: Tree,
|
|
255
|
+
color_by: str,
|
|
256
|
+
ax: Axes | None = None,
|
|
257
|
+
backward_time: bool = False,
|
|
258
|
+
default_color: Color = "black",
|
|
259
|
+
colormap: str | Colormap = "tab20",
|
|
260
|
+
show_legend: bool = True,
|
|
261
|
+
labels: dict[Any, str] | None = None,
|
|
262
|
+
legend_kwargs: dict[str, Any] | None = None,
|
|
263
|
+
):
|
|
264
|
+
"""
|
|
265
|
+
Draw a phylogenetic tree with branches colored based on categorical metadata.
|
|
266
|
+
|
|
267
|
+
Parameters
|
|
268
|
+
----------
|
|
269
|
+
tree : Tree
|
|
270
|
+
The phylogenetic tree to draw.
|
|
271
|
+
color_by : str
|
|
272
|
+
The metadata key to color branches by.
|
|
273
|
+
ax : Axes | None, optional
|
|
274
|
+
The matplotlib Axes to draw on. If None, uses the current Axes.
|
|
275
|
+
backward_time : bool, optional
|
|
276
|
+
If True, the x-axis is inverted to represent time going backward.
|
|
277
|
+
default_color : Color, optional
|
|
278
|
+
The color to use for nodes without the specified metadata.
|
|
279
|
+
colormap : str | Colormap, optional
|
|
280
|
+
The colormap to use for coloring categories. Defaults to 'tab20'.
|
|
281
|
+
show_legend : bool, optional
|
|
282
|
+
Whether to display a legend for the categories.
|
|
283
|
+
labels : dict[Any, str] | None, optional
|
|
284
|
+
A mapping from category values to labels for the legend.
|
|
285
|
+
legend_kwargs : dict[str, Any] | None, optional
|
|
286
|
+
Additional keyword arguments to pass to the legend.
|
|
287
|
+
|
|
288
|
+
Returns
|
|
289
|
+
-------
|
|
290
|
+
Axes
|
|
291
|
+
The Axes with the drawn colored tree.
|
|
292
|
+
"""
|
|
293
|
+
ax, colors = _init_colored_tree_categorical(
|
|
294
|
+
tree=tree,
|
|
295
|
+
color_by=color_by,
|
|
296
|
+
ax=ax,
|
|
297
|
+
default_color=default_color,
|
|
298
|
+
colormap=colormap,
|
|
299
|
+
show_legend=show_legend,
|
|
300
|
+
labels=labels,
|
|
301
|
+
legend_kwargs=legend_kwargs,
|
|
302
|
+
)
|
|
303
|
+
return draw_tree(tree=tree, ax=ax, colors=colors, backward_time=backward_time)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def draw_colored_dated_tree_categorical(
|
|
307
|
+
tree: Tree,
|
|
308
|
+
calibration_nodes: tuple[CalibrationNode, CalibrationNode],
|
|
309
|
+
color_by: str,
|
|
310
|
+
ax: Axes | None = None,
|
|
311
|
+
default_color: Color = "black",
|
|
312
|
+
colormap: str | Colormap = "tab20",
|
|
313
|
+
show_legend: bool = True,
|
|
314
|
+
labels: dict[Any, str] | None = None,
|
|
315
|
+
legend_kwargs: dict[str, Any] | None = None,
|
|
316
|
+
) -> Axes:
|
|
317
|
+
"""
|
|
318
|
+
Draw a dated phylogenetic tree with branches colored based on categorical metadata.
|
|
319
|
+
|
|
320
|
+
Parameters
|
|
321
|
+
----------
|
|
322
|
+
tree : Tree
|
|
323
|
+
The phylogenetic tree to draw.
|
|
324
|
+
calibration_nodes : tuple[CalibrationNode, CalibrationNode]
|
|
325
|
+
Two calibration nodes defining the mapping from depth to date.
|
|
326
|
+
color_by : str
|
|
327
|
+
The metadata key to color branches by.
|
|
328
|
+
ax : Axes | None, optional
|
|
329
|
+
The matplotlib Axes to draw on. If None, uses the current Axes.
|
|
330
|
+
default_color : Color, optional
|
|
331
|
+
The color to use for nodes without the specified metadata.
|
|
332
|
+
colormap : str | Colormap, optional
|
|
333
|
+
The colormap to use for coloring categories. Defaults to 'tab20'.
|
|
334
|
+
show_legend : bool, optional
|
|
335
|
+
Whether to display a legend for the categories.
|
|
336
|
+
labels : dict[Any, str] | None, optional
|
|
337
|
+
A mapping from category values to labels for the legend.
|
|
338
|
+
legend_kwargs : dict[str, Any] | None, optional
|
|
339
|
+
Additional keyword arguments to pass to the legend.
|
|
340
|
+
|
|
341
|
+
Returns
|
|
342
|
+
-------
|
|
343
|
+
Axes
|
|
344
|
+
The Axes with the drawn colored dated tree.
|
|
345
|
+
"""
|
|
346
|
+
ax, colors = _init_colored_tree_categorical(
|
|
347
|
+
tree=tree,
|
|
348
|
+
color_by=color_by,
|
|
349
|
+
ax=ax,
|
|
350
|
+
default_color=default_color,
|
|
351
|
+
colormap=colormap,
|
|
352
|
+
show_legend=show_legend,
|
|
353
|
+
labels=labels,
|
|
354
|
+
legend_kwargs=legend_kwargs,
|
|
355
|
+
)
|
|
356
|
+
return draw_dated_tree(
|
|
357
|
+
tree=tree, calibration_nodes=calibration_nodes, ax=ax, colors=colors
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@overload
|
|
362
|
+
def _init_colored_tree_continuous(
|
|
363
|
+
tree: Tree,
|
|
364
|
+
color_by: str,
|
|
365
|
+
ax: Axes | None = ...,
|
|
366
|
+
default_color: Color = ...,
|
|
367
|
+
colormap: str | Colormap = ...,
|
|
368
|
+
vmin: float | None = ...,
|
|
369
|
+
vmax: float | None = ...,
|
|
370
|
+
*,
|
|
371
|
+
show_hist: Literal[False],
|
|
372
|
+
hist_kwargs: dict[str, Any] | None = ...,
|
|
373
|
+
hist_axes_kwargs: dict[str, Any] | None = ...,
|
|
374
|
+
) -> tuple[Axes, dict[Tree, Color]]: ...
|
|
375
|
+
@overload
|
|
376
|
+
def _init_colored_tree_continuous(
|
|
377
|
+
tree: Tree,
|
|
378
|
+
color_by: str,
|
|
379
|
+
ax: Axes | None = ...,
|
|
380
|
+
default_color: Color = ...,
|
|
381
|
+
colormap: str | Colormap = ...,
|
|
382
|
+
vmin: float | None = ...,
|
|
383
|
+
vmax: float | None = ...,
|
|
384
|
+
*,
|
|
385
|
+
show_hist: Literal[True],
|
|
386
|
+
hist_kwargs: dict[str, Any] | None = ...,
|
|
387
|
+
hist_axes_kwargs: dict[str, Any] | None = ...,
|
|
388
|
+
) -> tuple[Axes, dict[Tree, Color], Axes]: ...
|
|
389
|
+
def _init_colored_tree_continuous(
|
|
390
|
+
tree: Tree,
|
|
391
|
+
color_by: str,
|
|
392
|
+
ax: Axes | None = None,
|
|
393
|
+
default_color: Color = "black",
|
|
394
|
+
colormap: str | Colormap = "viridis",
|
|
395
|
+
vmin: float | None = None,
|
|
396
|
+
vmax: float | None = None,
|
|
397
|
+
*,
|
|
398
|
+
show_hist: bool = True,
|
|
399
|
+
hist_kwargs: dict[str, Any] | None = None,
|
|
400
|
+
hist_axes_kwargs: dict[str, Any] | None = None,
|
|
401
|
+
) -> tuple[Axes, dict[Tree, Color]] | tuple[Axes, dict[Tree, Color], Axes]:
|
|
402
|
+
"""
|
|
403
|
+
Initialize colors for drawing a tree based on continuous metadata.
|
|
404
|
+
|
|
405
|
+
Parameters
|
|
406
|
+
----------
|
|
407
|
+
tree : Tree
|
|
408
|
+
The phylogenetic tree.
|
|
409
|
+
color_by : str
|
|
410
|
+
The metadata key to color branches by.
|
|
411
|
+
ax : Axes | None, optional
|
|
412
|
+
The matplotlib Axes to draw on. If None, uses the current Axes.
|
|
413
|
+
default_color : Color, optional
|
|
414
|
+
The color to use for nodes without the specified metadata.
|
|
415
|
+
colormap : str | Colormap, optional
|
|
416
|
+
The colormap to use for coloring continuous values. Defaults to 'viridis'.
|
|
417
|
+
vmin : float | None, optional
|
|
418
|
+
The minimum value for normalization. If None, uses the minimum of the data.
|
|
419
|
+
vmax : float | None, optional
|
|
420
|
+
The maximum value for normalization. If None, uses the maximum of the data.
|
|
421
|
+
show_hist : bool, optional
|
|
422
|
+
Whether to display a histogram of the continuous values.
|
|
423
|
+
hist_kwargs : dict[str, Any] | None, optional
|
|
424
|
+
Additional keyword arguments to pass to the histogram.
|
|
425
|
+
hist_axes_kwargs : dict[str, Any] | None, optional
|
|
426
|
+
Additional keyword arguments to define the histogram Axes.
|
|
427
|
+
|
|
428
|
+
Returns
|
|
429
|
+
-------
|
|
430
|
+
tuple[Axes, dict[Tree, Color]] | tuple[Axes, dict[Tree, Color], Axes]
|
|
431
|
+
The Axes, a dictionary mapping each node to its assigned color,
|
|
432
|
+
and optionally the histogram Axes if `show_hist` is True.
|
|
433
|
+
"""
|
|
434
|
+
if ax is None:
|
|
435
|
+
ax = plt.gca()
|
|
436
|
+
|
|
437
|
+
if isinstance(colormap, str):
|
|
438
|
+
colormap = plt.get_cmap(colormap)
|
|
439
|
+
|
|
440
|
+
features = {node: node[color_by] for node in tree if color_by in node.metadata}
|
|
441
|
+
values = list(features.values())
|
|
442
|
+
vmin = min(values) if vmin is None else vmin
|
|
443
|
+
vmax = max(values) if vmax is None else vmax
|
|
444
|
+
norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
|
|
445
|
+
colors = {
|
|
446
|
+
node: colormap(norm(float(features[node])))
|
|
447
|
+
if node in features
|
|
448
|
+
else default_color
|
|
449
|
+
for node in tree
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if show_hist:
|
|
453
|
+
default_hist_axes_kwargs = {"width": "25%", "height": "25%"}
|
|
454
|
+
if hist_axes_kwargs is not None:
|
|
455
|
+
default_hist_axes_kwargs.update(hist_axes_kwargs)
|
|
456
|
+
hist_ax = inset_axes(ax, **default_hist_axes_kwargs) # pyright: ignore
|
|
457
|
+
|
|
458
|
+
hist_kwargs = {} if hist_kwargs is None else hist_kwargs
|
|
459
|
+
_, bins, patches = hist_ax.hist(values, **hist_kwargs) # pyright: ignore
|
|
460
|
+
|
|
461
|
+
for patch, b0, b1 in zip(patches, bins[:-1], bins[1:]): # pyright: ignore
|
|
462
|
+
midpoint = (b0 + b1) / 2 # pyright: ignore
|
|
463
|
+
patch.set_facecolor(colormap(norm(midpoint))) # pyright: ignore
|
|
464
|
+
return ax, colors, hist_ax # pyright: ignore
|
|
465
|
+
|
|
466
|
+
sm = plt.cm.ScalarMappable(cmap=colormap, norm=norm)
|
|
467
|
+
ax.get_figure().colorbar(sm, ax=ax) # pyright: ignore
|
|
468
|
+
return ax, colors
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
@overload
|
|
472
|
+
def draw_colored_tree_continuous(
|
|
473
|
+
tree: Tree,
|
|
474
|
+
color_by: str,
|
|
475
|
+
ax: Axes | None = None,
|
|
476
|
+
backward_time: bool = False,
|
|
477
|
+
default_color: Color = "black",
|
|
478
|
+
colormap: str | Colormap = "viridis",
|
|
479
|
+
vmin: float | None = None,
|
|
480
|
+
vmax: float | None = None,
|
|
481
|
+
*,
|
|
482
|
+
show_hist: Literal[False],
|
|
483
|
+
hist_kwargs: dict[str, Any] | None = None,
|
|
484
|
+
hist_axes_kwargs: dict[str, Any] | None = None,
|
|
485
|
+
) -> Axes: ...
|
|
486
|
+
@overload
|
|
487
|
+
def draw_colored_tree_continuous(
|
|
488
|
+
tree: Tree,
|
|
489
|
+
color_by: str,
|
|
490
|
+
ax: Axes | None = None,
|
|
491
|
+
backward_time: bool = False,
|
|
492
|
+
default_color: Color = "black",
|
|
493
|
+
colormap: str | Colormap = "viridis",
|
|
494
|
+
vmin: float | None = None,
|
|
495
|
+
vmax: float | None = None,
|
|
496
|
+
*,
|
|
497
|
+
show_hist: Literal[True],
|
|
498
|
+
hist_kwargs: dict[str, Any] | None = None,
|
|
499
|
+
hist_axes_kwargs: dict[str, Any] | None = None,
|
|
500
|
+
) -> tuple[Axes, Axes]: ...
|
|
501
|
+
def draw_colored_tree_continuous(
|
|
502
|
+
tree: Tree,
|
|
503
|
+
color_by: str,
|
|
504
|
+
ax: Axes | None = None,
|
|
505
|
+
backward_time: bool = False,
|
|
506
|
+
default_color: Color = "black",
|
|
507
|
+
colormap: str | Colormap = "viridis",
|
|
508
|
+
vmin: float | None = None,
|
|
509
|
+
vmax: float | None = None,
|
|
510
|
+
show_hist: bool = True,
|
|
511
|
+
hist_kwargs: dict[str, Any] | None = None,
|
|
512
|
+
hist_axes_kwargs: dict[str, Any] | None = None,
|
|
513
|
+
) -> Axes | tuple[Axes, Axes]:
|
|
514
|
+
"""
|
|
515
|
+
Draw a phylogenetic tree with branches colored based on continuous metadata.
|
|
516
|
+
|
|
517
|
+
Parameters
|
|
518
|
+
----------
|
|
519
|
+
tree : Tree
|
|
520
|
+
The phylogenetic tree to draw.
|
|
521
|
+
color_by : str
|
|
522
|
+
The metadata key to color branches by.
|
|
523
|
+
ax : Axes | None, optional
|
|
524
|
+
The matplotlib Axes to draw on. If None, uses the current Axes.
|
|
525
|
+
backward_time : bool, optional
|
|
526
|
+
If True, the x-axis is inverted to represent time going backward.
|
|
527
|
+
default_color : Color, optional
|
|
528
|
+
The color to use for nodes without the specified metadata.
|
|
529
|
+
colormap : str | Colormap, optional
|
|
530
|
+
The colormap to use for coloring continuous values. Defaults to 'viridis'.
|
|
531
|
+
vmin : float | None, optional
|
|
532
|
+
The minimum value for normalization. If None, uses the minimum of the data.
|
|
533
|
+
vmax : float | None, optional
|
|
534
|
+
The maximum value for normalization. If None, uses the maximum of the data.
|
|
535
|
+
show_hist : bool, optional
|
|
536
|
+
Whether to display a histogram of the continuous values.
|
|
537
|
+
hist_kwargs : dict[str, Any] | None, optional
|
|
538
|
+
Additional keyword arguments to pass to the histogram.
|
|
539
|
+
hist_axes_kwargs : dict[str, Any] | None, optional
|
|
540
|
+
Additional keyword arguments to define the histogram Axes.
|
|
541
|
+
|
|
542
|
+
Returns
|
|
543
|
+
-------
|
|
544
|
+
Axes | tuple[Axes, Axes]
|
|
545
|
+
The Axes with the drawn colored tree,
|
|
546
|
+
and optionally the histogram Axes if `show_hist` is True.
|
|
547
|
+
"""
|
|
548
|
+
if show_hist:
|
|
549
|
+
ax, colors, hist_ax = _init_colored_tree_continuous(
|
|
550
|
+
tree=tree,
|
|
551
|
+
color_by=color_by,
|
|
552
|
+
ax=ax,
|
|
553
|
+
default_color=default_color,
|
|
554
|
+
colormap=colormap,
|
|
555
|
+
vmin=vmin,
|
|
556
|
+
vmax=vmax,
|
|
557
|
+
show_hist=show_hist,
|
|
558
|
+
hist_kwargs=hist_kwargs,
|
|
559
|
+
hist_axes_kwargs=hist_axes_kwargs,
|
|
560
|
+
)
|
|
561
|
+
return draw_tree(
|
|
562
|
+
tree=tree, ax=ax, colors=colors, backward_time=backward_time
|
|
563
|
+
), hist_ax
|
|
564
|
+
|
|
565
|
+
ax, colors = _init_colored_tree_continuous(
|
|
566
|
+
tree=tree,
|
|
567
|
+
color_by=color_by,
|
|
568
|
+
ax=ax,
|
|
569
|
+
default_color=default_color,
|
|
570
|
+
colormap=colormap,
|
|
571
|
+
vmin=vmin,
|
|
572
|
+
vmax=vmax,
|
|
573
|
+
show_hist=show_hist,
|
|
574
|
+
hist_kwargs=hist_kwargs,
|
|
575
|
+
hist_axes_kwargs=hist_axes_kwargs,
|
|
576
|
+
)
|
|
577
|
+
return draw_tree(tree=tree, ax=ax, colors=colors, backward_time=backward_time)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
@overload
|
|
581
|
+
def draw_colored_dated_tree_continuous(
|
|
582
|
+
tree: Tree,
|
|
583
|
+
calibration_nodes: tuple[CalibrationNode, CalibrationNode],
|
|
584
|
+
color_by: str,
|
|
585
|
+
ax: Axes | None = None,
|
|
586
|
+
default_color: Color = "black",
|
|
587
|
+
colormap: str | Colormap = "viridis",
|
|
588
|
+
vmin: float | None = None,
|
|
589
|
+
vmax: float | None = None,
|
|
590
|
+
*,
|
|
591
|
+
show_hist: Literal[False],
|
|
592
|
+
hist_kwargs: dict[str, Any] | None = None,
|
|
593
|
+
hist_axes_kwargs: dict[str, Any] | None = None,
|
|
594
|
+
) -> Axes: ...
|
|
595
|
+
@overload
|
|
596
|
+
def draw_colored_dated_tree_continuous(
|
|
597
|
+
tree: Tree,
|
|
598
|
+
calibration_nodes: tuple[CalibrationNode, CalibrationNode],
|
|
599
|
+
color_by: str,
|
|
600
|
+
ax: Axes | None = None,
|
|
601
|
+
default_color: Color = "black",
|
|
602
|
+
colormap: str | Colormap = "viridis",
|
|
603
|
+
vmin: float | None = None,
|
|
604
|
+
vmax: float | None = None,
|
|
605
|
+
*,
|
|
606
|
+
show_hist: Literal[True],
|
|
607
|
+
hist_kwargs: dict[str, Any] | None = None,
|
|
608
|
+
hist_axes_kwargs: dict[str, Any] | None = None,
|
|
609
|
+
) -> tuple[Axes, Axes]: ...
|
|
610
|
+
def draw_colored_dated_tree_continuous(
|
|
611
|
+
tree: Tree,
|
|
612
|
+
calibration_nodes: tuple[CalibrationNode, CalibrationNode],
|
|
613
|
+
color_by: str,
|
|
614
|
+
ax: Axes | None = None,
|
|
615
|
+
default_color: Color = "black",
|
|
616
|
+
colormap: str | Colormap = "viridis",
|
|
617
|
+
vmin: float | None = None,
|
|
618
|
+
vmax: float | None = None,
|
|
619
|
+
show_hist: bool = True,
|
|
620
|
+
hist_kwargs: dict[str, Any] | None = None,
|
|
621
|
+
hist_axes_kwargs: dict[str, Any] | None = None,
|
|
622
|
+
) -> Axes | tuple[Axes, Axes]:
|
|
623
|
+
"""
|
|
624
|
+
Draw a dated phylogenetic tree with branches colored based on continuous metadata.
|
|
625
|
+
|
|
626
|
+
Parameters
|
|
627
|
+
----------
|
|
628
|
+
tree : Tree
|
|
629
|
+
The phylogenetic tree to draw.
|
|
630
|
+
calibration_nodes : tuple[CalibrationNode, CalibrationNode]
|
|
631
|
+
Two calibration nodes defining the mapping from depth to date.
|
|
632
|
+
color_by : str
|
|
633
|
+
The metadata key to color branches by.
|
|
634
|
+
ax : Axes | None, optional
|
|
635
|
+
The matplotlib Axes to draw on. If None, uses the current Axes.
|
|
636
|
+
default_color : Color, optional
|
|
637
|
+
The color to use for nodes without the specified metadata.
|
|
638
|
+
colormap : str | Colormap, optional
|
|
639
|
+
The colormap to use for coloring continuous values. Defaults to 'viridis'.
|
|
640
|
+
vmin : float | None, optional
|
|
641
|
+
The minimum value for normalization. If None, uses the minimum of the data.
|
|
642
|
+
vmax : float | None, optional
|
|
643
|
+
The maximum value for normalization. If None, uses the maximum of the data.
|
|
644
|
+
show_hist : bool, optional
|
|
645
|
+
Whether to display a histogram of the continuous values.
|
|
646
|
+
hist_kwargs : dict[str, Any] | None, optional
|
|
647
|
+
Additional keyword arguments to pass to the histogram.
|
|
648
|
+
hist_axes_kwargs : dict[str, Any] | None, optional
|
|
649
|
+
Additional keyword arguments to define the histogram Axes.
|
|
650
|
+
|
|
651
|
+
Returns
|
|
652
|
+
-------
|
|
653
|
+
Axes | tuple[Axes, Axes]
|
|
654
|
+
The Axes with the drawn colored dated tree,
|
|
655
|
+
and optionally the histogram Axes if `show_hist` is True.
|
|
656
|
+
"""
|
|
657
|
+
if show_hist:
|
|
658
|
+
ax, colors, hist_ax = _init_colored_tree_continuous(
|
|
659
|
+
tree=tree,
|
|
660
|
+
color_by=color_by,
|
|
661
|
+
ax=ax,
|
|
662
|
+
default_color=default_color,
|
|
663
|
+
colormap=colormap,
|
|
664
|
+
vmin=vmin,
|
|
665
|
+
vmax=vmax,
|
|
666
|
+
show_hist=show_hist,
|
|
667
|
+
hist_kwargs=hist_kwargs,
|
|
668
|
+
hist_axes_kwargs=hist_axes_kwargs,
|
|
669
|
+
)
|
|
670
|
+
return draw_dated_tree(
|
|
671
|
+
tree=tree,
|
|
672
|
+
calibration_nodes=calibration_nodes,
|
|
673
|
+
ax=ax,
|
|
674
|
+
colors=colors,
|
|
675
|
+
), hist_ax
|
|
676
|
+
|
|
677
|
+
ax, colors = _init_colored_tree_continuous(
|
|
678
|
+
tree=tree,
|
|
679
|
+
color_by=color_by,
|
|
680
|
+
ax=ax,
|
|
681
|
+
default_color=default_color,
|
|
682
|
+
colormap=colormap,
|
|
683
|
+
vmin=vmin,
|
|
684
|
+
vmax=vmax,
|
|
685
|
+
show_hist=show_hist,
|
|
686
|
+
hist_kwargs=hist_kwargs,
|
|
687
|
+
hist_axes_kwargs=hist_axes_kwargs,
|
|
688
|
+
)
|
|
689
|
+
return draw_dated_tree(
|
|
690
|
+
tree=tree,
|
|
691
|
+
calibration_nodes=calibration_nodes,
|
|
692
|
+
ax=ax,
|
|
693
|
+
colors=colors,
|
|
694
|
+
)
|
|
@@ -24,3 +24,11 @@ def load_fasta(
|
|
|
24
24
|
chars = next(f).strip()
|
|
25
25
|
sequences.append(Sequence(id, chars, time))
|
|
26
26
|
return MSA(sequences)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def dump_fasta(msa: MSA | list[Sequence], fasta_file: str | Path) -> None:
|
|
30
|
+
with open(fasta_file, "w") as f:
|
|
31
|
+
sequences = msa.sequences if isinstance(msa, MSA) else msa
|
|
32
|
+
for seq in sequences:
|
|
33
|
+
f.write(f">{seq.id}\n")
|
|
34
|
+
f.write(f"{seq.chars}\n")
|
|
@@ -1,39 +1,31 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
1
2
|
from types import MappingProxyType
|
|
2
|
-
from typing import Any
|
|
3
|
+
from typing import Any
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class MetadataMixin:
|
|
6
|
-
"""A mixin that provides metadata management with dictionary-like access."""
|
|
7
|
-
|
|
8
7
|
def __init__(self) -> None:
|
|
9
8
|
self._metadata: dict[str, Any] = {}
|
|
10
9
|
|
|
11
10
|
@property
|
|
12
11
|
def metadata(self) -> Mapping[str, Any]:
|
|
13
|
-
"""Return a read-only view of all metadata."""
|
|
14
12
|
return MappingProxyType(self._metadata)
|
|
15
13
|
|
|
16
14
|
def set(self, key: str, value: Any) -> None:
|
|
17
|
-
"""Set or update a metadata value."""
|
|
18
15
|
self._metadata[key] = value
|
|
19
16
|
|
|
20
17
|
def update(self, metadata: Mapping[str, Any]) -> None:
|
|
21
|
-
"""Bulk update metadata values."""
|
|
22
18
|
self._metadata.update(metadata)
|
|
23
19
|
|
|
24
|
-
def get(self, key: str, default:
|
|
25
|
-
"""Get a metadata value, returning `default` if not found."""
|
|
20
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
26
21
|
return self._metadata.get(key, default)
|
|
27
22
|
|
|
28
23
|
def delete(self, key: str) -> None:
|
|
29
|
-
"""Delete a metadata if it exists, else do nothing."""
|
|
30
24
|
self._metadata.pop(key, None)
|
|
31
25
|
|
|
32
26
|
def clear(self) -> None:
|
|
33
|
-
"""Remove all metadata."""
|
|
34
27
|
self._metadata.clear()
|
|
35
28
|
|
|
36
|
-
# Dict-like behavior
|
|
37
29
|
def __getitem__(self, key: str) -> Any:
|
|
38
30
|
return self._metadata[key]
|
|
39
31
|
|
|
@@ -27,11 +27,14 @@ from phylogenie.treesimulator.utils import (
|
|
|
27
27
|
compute_sackin_index,
|
|
28
28
|
get_distance,
|
|
29
29
|
get_mrca,
|
|
30
|
+
get_node_ages,
|
|
30
31
|
get_node_depth_levels,
|
|
31
32
|
get_node_depths,
|
|
32
33
|
get_node_height_levels,
|
|
33
34
|
get_node_heights,
|
|
34
35
|
get_node_leaf_counts,
|
|
36
|
+
get_node_times,
|
|
37
|
+
get_path,
|
|
35
38
|
)
|
|
36
39
|
|
|
37
40
|
__all__ = [
|
|
@@ -65,9 +68,12 @@ __all__ = [
|
|
|
65
68
|
"compute_sackin_index",
|
|
66
69
|
"get_distance",
|
|
67
70
|
"get_mrca",
|
|
71
|
+
"get_node_ages",
|
|
68
72
|
"get_node_depth_levels",
|
|
69
73
|
"get_node_depths",
|
|
70
74
|
"get_node_height_levels",
|
|
71
75
|
"get_node_heights",
|
|
72
76
|
"get_node_leaf_counts",
|
|
77
|
+
"get_node_times",
|
|
78
|
+
"get_path",
|
|
73
79
|
]
|
|
@@ -5,11 +5,13 @@ from phylogenie.treesimulator.events.mutations import get_mutation_id
|
|
|
5
5
|
from phylogenie.treesimulator.model import get_node_state
|
|
6
6
|
from phylogenie.treesimulator.tree import Tree
|
|
7
7
|
from phylogenie.treesimulator.utils import (
|
|
8
|
+
get_node_ages,
|
|
8
9
|
get_node_depth_levels,
|
|
9
10
|
get_node_depths,
|
|
10
11
|
get_node_height_levels,
|
|
11
12
|
get_node_heights,
|
|
12
13
|
get_node_leaf_counts,
|
|
14
|
+
get_node_times,
|
|
13
15
|
)
|
|
14
16
|
|
|
15
17
|
|
|
@@ -22,6 +24,7 @@ def _get_mutations(tree: Tree) -> dict[Tree, int]:
|
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
class Feature(str, Enum):
|
|
27
|
+
AGE = "age"
|
|
25
28
|
DEPTH = "depth"
|
|
26
29
|
DEPTH_LEVEL = "depth_level"
|
|
27
30
|
HEIGHT = "height"
|
|
@@ -29,9 +32,11 @@ class Feature(str, Enum):
|
|
|
29
32
|
MUTATION = "mutation"
|
|
30
33
|
N_LEAVES = "n_leaves"
|
|
31
34
|
STATE = "state"
|
|
35
|
+
TIME = "time"
|
|
32
36
|
|
|
33
37
|
|
|
34
38
|
FEATURES_EXTRACTORS = {
|
|
39
|
+
Feature.AGE: get_node_ages,
|
|
35
40
|
Feature.DEPTH: get_node_depths,
|
|
36
41
|
Feature.DEPTH_LEVEL: get_node_depth_levels,
|
|
37
42
|
Feature.HEIGHT: get_node_heights,
|
|
@@ -39,6 +44,7 @@ FEATURES_EXTRACTORS = {
|
|
|
39
44
|
Feature.MUTATION: _get_mutations,
|
|
40
45
|
Feature.N_LEAVES: get_node_leaf_counts,
|
|
41
46
|
Feature.STATE: _get_states,
|
|
47
|
+
Feature.TIME: get_node_times,
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
|
|
@@ -15,7 +15,7 @@ def _parse_translate_block(lines: Iterator[str]) -> dict[str, str]:
|
|
|
15
15
|
if ";" in line:
|
|
16
16
|
return translations
|
|
17
17
|
else:
|
|
18
|
-
raise ValueError(
|
|
18
|
+
raise ValueError("Invalid translate line. Expected '<num> <name>'.")
|
|
19
19
|
translations[match.group(1)] = match.group(2)
|
|
20
20
|
raise ValueError("Translate block not terminated with ';'.")
|
|
21
21
|
|
|
@@ -33,7 +33,7 @@ def _parse_trees_block(lines: Iterator[str]) -> dict[str, Tree]:
|
|
|
33
33
|
match = re.match(r"^TREE\s*\*?\s+(\S+)\s*=\s*(.+)$", line, re.IGNORECASE)
|
|
34
34
|
if match is None:
|
|
35
35
|
raise ValueError(
|
|
36
|
-
|
|
36
|
+
"Invalid tree line. Expected 'TREE <name> = <newick>'."
|
|
37
37
|
)
|
|
38
38
|
name = match.group(1)
|
|
39
39
|
if name in trees:
|
|
@@ -153,14 +153,27 @@ class Tree(MetadataMixin):
|
|
|
153
153
|
child.branch_length_or_raise() + child.height for child in self.children
|
|
154
154
|
)
|
|
155
155
|
|
|
156
|
+
@property
|
|
157
|
+
def time(self) -> float:
|
|
158
|
+
return self.depth
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def age(self) -> float:
|
|
162
|
+
if self.parent is None:
|
|
163
|
+
return self.height
|
|
164
|
+
return self.parent.age - self.branch_length_or_raise()
|
|
165
|
+
|
|
156
166
|
# -------------
|
|
157
167
|
# Miscellaneous
|
|
158
168
|
# -------------
|
|
159
169
|
# Other useful miscellaneous methods.
|
|
160
170
|
|
|
161
171
|
def ladderize(self, key: Callable[["Tree"], Any] | None = None) -> None:
|
|
172
|
+
def _default_key(node: Tree) -> int:
|
|
173
|
+
return node.n_leaves
|
|
174
|
+
|
|
162
175
|
if key is None:
|
|
163
|
-
key =
|
|
176
|
+
key = _default_key
|
|
164
177
|
self._children.sort(key=key)
|
|
165
178
|
for child in self.children:
|
|
166
179
|
child.ladderize(key)
|
|
@@ -169,7 +182,7 @@ class Tree(MetadataMixin):
|
|
|
169
182
|
for node in self:
|
|
170
183
|
if node.name == name:
|
|
171
184
|
return node
|
|
172
|
-
raise ValueError(f"Node
|
|
185
|
+
raise ValueError(f"Node {name} not found.")
|
|
173
186
|
|
|
174
187
|
def copy(self):
|
|
175
188
|
new_tree = Tree(self.name, self.branch_length)
|
|
@@ -51,6 +51,17 @@ def get_node_heights(tree: Tree) -> dict[Tree, float]:
|
|
|
51
51
|
return heights
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def get_node_times(tree: Tree) -> dict[Tree, float]:
|
|
55
|
+
return get_node_depths(tree)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_node_ages(tree: Tree) -> dict[Tree, float]:
|
|
59
|
+
ages: dict[Tree, float] = {tree: tree.height}
|
|
60
|
+
for node in tree.iter_descendants():
|
|
61
|
+
ages[node] = ages[node.parent] - node.branch_length # pyright: ignore
|
|
62
|
+
return ages
|
|
63
|
+
|
|
64
|
+
|
|
54
65
|
def get_mrca(node1: Tree, node2: Tree) -> Tree:
|
|
55
66
|
node1_ancestors = set(node1.iter_upward())
|
|
56
67
|
for node2_ancestor in node2.iter_upward():
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
|
-
from typing import Any, Callable
|
|
3
|
-
|
|
4
|
-
import matplotlib.colors as mcolors
|
|
5
|
-
import matplotlib.patches as mpatches
|
|
6
|
-
import matplotlib.pyplot as plt
|
|
7
|
-
from matplotlib.axes import Axes
|
|
8
|
-
from matplotlib.colors import Colormap
|
|
9
|
-
from mpl_toolkits.axes_grid1.inset_locator import inset_axes # pyright: ignore
|
|
10
|
-
|
|
11
|
-
from phylogenie.treesimulator import Tree, get_node_depth_levels, get_node_depths
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class Coloring(str, Enum):
|
|
15
|
-
DISCRETE = "discrete"
|
|
16
|
-
CONTINUOUS = "continuous"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Color = str | tuple[float, float, float] | tuple[float, float, float, float]
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def draw_colored_tree(
|
|
23
|
-
tree: Tree, ax: Axes | None = None, colors: Color | dict[Tree, Color] = "black"
|
|
24
|
-
) -> Axes:
|
|
25
|
-
if ax is None:
|
|
26
|
-
ax = plt.gca()
|
|
27
|
-
|
|
28
|
-
if not isinstance(colors, dict):
|
|
29
|
-
colors = {node: colors for node in tree}
|
|
30
|
-
|
|
31
|
-
xs = (
|
|
32
|
-
get_node_depth_levels(tree)
|
|
33
|
-
if any(node.branch_length is None for node in tree.iter_descendants())
|
|
34
|
-
else get_node_depths(tree)
|
|
35
|
-
)
|
|
36
|
-
ys: dict[Tree, float] = {node: i for i, node in enumerate(tree.get_leaves())}
|
|
37
|
-
for node in tree.postorder_traversal():
|
|
38
|
-
if node.is_internal():
|
|
39
|
-
ys[node] = sum(ys[child] for child in node.children) / len(node.children)
|
|
40
|
-
|
|
41
|
-
if tree.branch_length is not None:
|
|
42
|
-
ax.hlines(y=ys[tree], xmin=0, xmax=xs[tree], color=colors[tree]) # pyright: ignore
|
|
43
|
-
for node in tree:
|
|
44
|
-
x1, y1 = xs[node], ys[node]
|
|
45
|
-
for child in node.children:
|
|
46
|
-
x2, y2 = xs[child], ys[child]
|
|
47
|
-
ax.hlines(y=y2, xmin=x1, xmax=x2, color=colors[child]) # pyright: ignore
|
|
48
|
-
ax.vlines(x=x1, ymin=y1, ymax=y2, color=colors[child]) # pyright: ignore
|
|
49
|
-
|
|
50
|
-
ax.set_yticks([]) # pyright: ignore
|
|
51
|
-
return ax
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def draw_tree(
|
|
55
|
-
tree: Tree,
|
|
56
|
-
ax: Axes | None = None,
|
|
57
|
-
color_by: str | dict[str, Any] | None = None,
|
|
58
|
-
coloring: str | Coloring | None = None,
|
|
59
|
-
default_color: Color = "black",
|
|
60
|
-
colormap: str | Colormap | None = None,
|
|
61
|
-
vmin: float | None = None,
|
|
62
|
-
vmax: float | None = None,
|
|
63
|
-
show_legend: bool = True,
|
|
64
|
-
labels: dict[Any, Any] | None = None,
|
|
65
|
-
legend_kwargs: dict[str, Any] | None = None,
|
|
66
|
-
show_hist: bool = True,
|
|
67
|
-
hist_kwargs: dict[str, Any] | None = None,
|
|
68
|
-
hist_axes_kwargs: dict[str, Any] | None = None,
|
|
69
|
-
) -> Axes | tuple[Axes, Axes]:
|
|
70
|
-
if ax is None:
|
|
71
|
-
ax = plt.gca()
|
|
72
|
-
|
|
73
|
-
if color_by is None:
|
|
74
|
-
return draw_colored_tree(tree, ax, colors=default_color)
|
|
75
|
-
|
|
76
|
-
if isinstance(color_by, str):
|
|
77
|
-
features = {node: node[color_by] for node in tree if color_by in node.metadata}
|
|
78
|
-
else:
|
|
79
|
-
features = {node: color_by[node.name] for node in tree if node.name in color_by}
|
|
80
|
-
values = list(features.values())
|
|
81
|
-
|
|
82
|
-
if coloring is None:
|
|
83
|
-
coloring = (
|
|
84
|
-
Coloring.CONTINUOUS
|
|
85
|
-
if any(isinstance(f, float) for f in values)
|
|
86
|
-
else Coloring.DISCRETE
|
|
87
|
-
)
|
|
88
|
-
if colormap is None:
|
|
89
|
-
colormap = "tab20" if coloring == Coloring.DISCRETE else "viridis"
|
|
90
|
-
if isinstance(colormap, str):
|
|
91
|
-
colormap = plt.get_cmap(colormap)
|
|
92
|
-
|
|
93
|
-
def _get_colors(feature_map: Callable[[Any], Color]) -> dict[Tree, Color]:
|
|
94
|
-
return {
|
|
95
|
-
node: feature_map(features[node]) if node in features else default_color
|
|
96
|
-
for node in tree
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if coloring == Coloring.DISCRETE:
|
|
100
|
-
if any(isinstance(f, float) for f in values):
|
|
101
|
-
raise ValueError(
|
|
102
|
-
"Discrete coloring selected but feature values are not all categorical."
|
|
103
|
-
)
|
|
104
|
-
feature_colors = {
|
|
105
|
-
f: mcolors.to_hex(colormap(i)) for i, f in enumerate(set(values))
|
|
106
|
-
}
|
|
107
|
-
colors = _get_colors(lambda f: feature_colors[f])
|
|
108
|
-
|
|
109
|
-
if show_legend:
|
|
110
|
-
legend_handles = [
|
|
111
|
-
mpatches.Patch(
|
|
112
|
-
color=feature_colors[f],
|
|
113
|
-
label=str(f) if labels is None else labels[f],
|
|
114
|
-
)
|
|
115
|
-
for f in feature_colors
|
|
116
|
-
]
|
|
117
|
-
if any(color_by not in node.metadata for node in tree):
|
|
118
|
-
legend_handles.append(mpatches.Patch(color=default_color, label="NA"))
|
|
119
|
-
if legend_kwargs is None:
|
|
120
|
-
legend_kwargs = {}
|
|
121
|
-
ax.legend(handles=legend_handles, **legend_kwargs) # pyright: ignore
|
|
122
|
-
|
|
123
|
-
return draw_colored_tree(tree, ax, colors)
|
|
124
|
-
|
|
125
|
-
if coloring == Coloring.CONTINUOUS:
|
|
126
|
-
vmin = min(values) if vmin is None else vmin
|
|
127
|
-
vmax = max(values) if vmax is None else vmax
|
|
128
|
-
norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
|
|
129
|
-
colors = _get_colors(lambda f: colormap(norm(float(f))))
|
|
130
|
-
|
|
131
|
-
if show_hist:
|
|
132
|
-
default_hist_axes_kwargs = {"width": "25%", "height": "25%"}
|
|
133
|
-
if hist_axes_kwargs is not None:
|
|
134
|
-
default_hist_axes_kwargs.update(hist_axes_kwargs)
|
|
135
|
-
hist_ax = inset_axes(ax, **default_hist_axes_kwargs) # pyright: ignore
|
|
136
|
-
|
|
137
|
-
hist_kwargs = {} if hist_kwargs is None else hist_kwargs
|
|
138
|
-
_, bins, patches = hist_ax.hist(values, **hist_kwargs) # pyright: ignore
|
|
139
|
-
|
|
140
|
-
for patch, b0, b1 in zip(patches, bins[:-1], bins[1:]): # pyright: ignore
|
|
141
|
-
midpoint = (b0 + b1) / 2 # pyright: ignore
|
|
142
|
-
patch.set_facecolor(colormap(norm(midpoint))) # pyright: ignore
|
|
143
|
-
return draw_colored_tree(tree, ax, colors), hist_ax # pyright: ignore
|
|
144
|
-
|
|
145
|
-
else:
|
|
146
|
-
sm = plt.cm.ScalarMappable(cmap=colormap, norm=norm)
|
|
147
|
-
ax.get_figure().colorbar(sm, ax=ax) # pyright: ignore
|
|
148
|
-
return draw_colored_tree(tree, ax, colors)
|
|
149
|
-
|
|
150
|
-
raise ValueError(
|
|
151
|
-
f"Unknown coloring method: {coloring}. Choices are {list(Coloring)}."
|
|
152
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{phylogenie-3.1.5 → phylogenie-3.1.8}/src/phylogenie/treesimulator/events/contact_tracing.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|