phylogenie 2.1.19__py3-none-any.whl → 2.1.21__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 +24 -2
- phylogenie/draw.py +142 -0
- phylogenie/tree.py +20 -4
- phylogenie/utils.py +117 -0
- {phylogenie-2.1.19.dist-info → phylogenie-2.1.21.dist-info}/METADATA +1 -1
- {phylogenie-2.1.19.dist-info → phylogenie-2.1.21.dist-info}/RECORD +9 -9
- phylogenie/plot.py +0 -136
- {phylogenie-2.1.19.dist-info → phylogenie-2.1.21.dist-info}/LICENSE.txt +0 -0
- {phylogenie-2.1.19.dist-info → phylogenie-2.1.21.dist-info}/WHEEL +0 -0
- {phylogenie-2.1.19.dist-info → phylogenie-2.1.21.dist-info}/entry_points.txt +0 -0
phylogenie/__init__.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from phylogenie.draw import Coloring, draw_tree
|
|
1
2
|
from phylogenie.generators import (
|
|
2
3
|
AliSimDatasetGenerator,
|
|
3
4
|
BDEITreeDatasetGenerator,
|
|
@@ -12,7 +13,6 @@ from phylogenie.generators import (
|
|
|
12
13
|
)
|
|
13
14
|
from phylogenie.io import dump_newick, load_fasta, load_newick
|
|
14
15
|
from phylogenie.msa import MSA
|
|
15
|
-
from phylogenie.plot import Coloring, plot_tree
|
|
16
16
|
from phylogenie.skyline import (
|
|
17
17
|
SkylineMatrix,
|
|
18
18
|
SkylineMatrixCoercible,
|
|
@@ -46,6 +46,18 @@ from phylogenie.treesimulator import (
|
|
|
46
46
|
get_FBD_events,
|
|
47
47
|
simulate_tree,
|
|
48
48
|
)
|
|
49
|
+
from phylogenie.utils import (
|
|
50
|
+
compute_colless_index,
|
|
51
|
+
compute_mean_leaf_pairwise_distance,
|
|
52
|
+
compute_sackin_index,
|
|
53
|
+
get_distance,
|
|
54
|
+
get_mrca,
|
|
55
|
+
get_node_depth_levels,
|
|
56
|
+
get_node_depths,
|
|
57
|
+
get_node_height_levels,
|
|
58
|
+
get_node_heights,
|
|
59
|
+
get_node_leaf_counts,
|
|
60
|
+
)
|
|
49
61
|
|
|
50
62
|
__all__ = [
|
|
51
63
|
"AliSimDatasetGenerator",
|
|
@@ -92,5 +104,15 @@ __all__ = [
|
|
|
92
104
|
"load_newick",
|
|
93
105
|
"MSA",
|
|
94
106
|
"Coloring",
|
|
95
|
-
"
|
|
107
|
+
"draw_tree",
|
|
108
|
+
"compute_colless_index",
|
|
109
|
+
"compute_mean_leaf_pairwise_distance",
|
|
110
|
+
"compute_sackin_index",
|
|
111
|
+
"get_distance",
|
|
112
|
+
"get_mrca",
|
|
113
|
+
"get_node_depths",
|
|
114
|
+
"get_node_depth_levels",
|
|
115
|
+
"get_node_heights",
|
|
116
|
+
"get_node_height_levels",
|
|
117
|
+
"get_node_leaf_counts",
|
|
96
118
|
]
|
phylogenie/draw.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Any
|
|
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 mpl_toolkits.axes_grid1.inset_locator import inset_axes # pyright: ignore
|
|
9
|
+
|
|
10
|
+
from phylogenie.tree import Tree
|
|
11
|
+
from phylogenie.utils import 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(tree: Tree, ax: Axes, colors: Color | dict[Tree, Color]) -> Axes:
|
|
23
|
+
if not isinstance(colors, dict):
|
|
24
|
+
colors = {node: colors for node in tree}
|
|
25
|
+
|
|
26
|
+
xs = (
|
|
27
|
+
get_node_depth_levels(tree)
|
|
28
|
+
if any(node.branch_length is None for node in tree)
|
|
29
|
+
else get_node_depths(tree)
|
|
30
|
+
)
|
|
31
|
+
ys = {node: i for i, node in enumerate(tree.inorder_traversal())}
|
|
32
|
+
|
|
33
|
+
for node in tree:
|
|
34
|
+
x1, y1 = xs[node], ys[node]
|
|
35
|
+
if node.parent is None:
|
|
36
|
+
ax.hlines(y=y1, xmin=0, xmax=x1, color=colors[node]) # pyright: ignore
|
|
37
|
+
continue
|
|
38
|
+
x0, y0 = xs[node.parent], ys[node.parent]
|
|
39
|
+
ax.vlines(x=x0, ymin=y0, ymax=y1, color=colors[node]) # pyright: ignore
|
|
40
|
+
ax.hlines(y=y1, xmin=x0, xmax=x1, color=colors[node]) # pyright: ignore
|
|
41
|
+
|
|
42
|
+
ax.set_yticks([]) # pyright: ignore
|
|
43
|
+
return ax
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def draw_tree(
|
|
47
|
+
tree: Tree,
|
|
48
|
+
ax: Axes | None = None,
|
|
49
|
+
color_by: str | None = None,
|
|
50
|
+
coloring: str | Coloring | None = None,
|
|
51
|
+
default_color: Color = "black",
|
|
52
|
+
cmap: str | None = None,
|
|
53
|
+
show_legend: bool = True,
|
|
54
|
+
vmin: float | None = None,
|
|
55
|
+
vmax: float | None = None,
|
|
56
|
+
show_hist: bool = True,
|
|
57
|
+
hist_kwargs: dict[str, Any] | None = None,
|
|
58
|
+
hist_axes_kwargs: dict[str, Any] | None = None,
|
|
59
|
+
) -> Axes | tuple[Axes, Axes]:
|
|
60
|
+
if ax is None:
|
|
61
|
+
ax = plt.gca()
|
|
62
|
+
|
|
63
|
+
if color_by is None:
|
|
64
|
+
return _draw_colored_tree(tree, ax, colors=default_color)
|
|
65
|
+
|
|
66
|
+
features = [node.get(color_by) for node in tree if color_by in node.features]
|
|
67
|
+
|
|
68
|
+
if coloring is None:
|
|
69
|
+
coloring = (
|
|
70
|
+
Coloring.CONTINUOUS
|
|
71
|
+
if any(isinstance(f, float) for f in features)
|
|
72
|
+
else Coloring.DISCRETE
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if coloring == Coloring.DISCRETE:
|
|
76
|
+
if any(isinstance(f, float) for f in features):
|
|
77
|
+
raise ValueError(
|
|
78
|
+
"Discrete coloring selected but feature values are not all categorical."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
colormap = plt.get_cmap("tab20" if cmap is None else cmap)
|
|
82
|
+
feature_colors = {
|
|
83
|
+
f: mcolors.to_hex(colormap(i)) for i, f in enumerate(set(features))
|
|
84
|
+
}
|
|
85
|
+
colors = {
|
|
86
|
+
node: (
|
|
87
|
+
feature_colors[node.get(color_by)]
|
|
88
|
+
if color_by in node.features
|
|
89
|
+
else default_color
|
|
90
|
+
)
|
|
91
|
+
for node in tree
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if show_legend:
|
|
95
|
+
legend_handles = [
|
|
96
|
+
mpatches.Patch(color=feature_colors[f], label=str(f))
|
|
97
|
+
for f in feature_colors
|
|
98
|
+
]
|
|
99
|
+
if any(color_by not in node.features for node in tree):
|
|
100
|
+
legend_handles.append(mpatches.Patch(color=default_color, label="NA"))
|
|
101
|
+
ax.legend(handles=legend_handles) # pyright: ignore
|
|
102
|
+
|
|
103
|
+
return _draw_colored_tree(tree, ax, colors)
|
|
104
|
+
|
|
105
|
+
if coloring == Coloring.CONTINUOUS:
|
|
106
|
+
vmin = min(features) if vmin is None else vmin
|
|
107
|
+
vmax = max(features) if vmax is None else vmax
|
|
108
|
+
norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
|
|
109
|
+
colormap = plt.get_cmap("viridis" if cmap is None else cmap)
|
|
110
|
+
colors = {
|
|
111
|
+
node: (
|
|
112
|
+
colormap(norm(float(node.get(color_by))))
|
|
113
|
+
if color_by in node.features
|
|
114
|
+
else default_color
|
|
115
|
+
)
|
|
116
|
+
for node in tree
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if show_hist:
|
|
120
|
+
default_hist_axes_kwargs = {"width": "25%", "height": "25%"}
|
|
121
|
+
if hist_axes_kwargs is not None:
|
|
122
|
+
default_hist_axes_kwargs.update(hist_axes_kwargs)
|
|
123
|
+
hist_ax = inset_axes(ax, **default_hist_axes_kwargs) # pyright: ignore
|
|
124
|
+
|
|
125
|
+
hist_kwargs = {} if hist_kwargs is None else hist_kwargs
|
|
126
|
+
_, bins, patches = hist_ax.hist(features, **hist_kwargs) # pyright: ignore
|
|
127
|
+
|
|
128
|
+
for patch, b0, b1 in zip( # pyright: ignore
|
|
129
|
+
patches, bins[:-1], bins[1:] # pyright: ignore
|
|
130
|
+
):
|
|
131
|
+
midpoint = (b0 + b1) / 2 # pyright: ignore
|
|
132
|
+
patch.set_facecolor(colormap(norm(midpoint))) # pyright: ignore
|
|
133
|
+
return _draw_colored_tree(tree, ax, colors), hist_ax # pyright: ignore
|
|
134
|
+
|
|
135
|
+
else:
|
|
136
|
+
sm = plt.cm.ScalarMappable(cmap=colormap, norm=norm)
|
|
137
|
+
ax.get_figure().colorbar(sm, ax=ax) # pyright: ignore
|
|
138
|
+
return _draw_colored_tree(tree, ax, colors)
|
|
139
|
+
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"Unknown coloring method: {coloring}. Choices are {list(Coloring)}."
|
|
142
|
+
)
|
phylogenie/tree.py
CHANGED
|
@@ -73,6 +73,8 @@ class Tree:
|
|
|
73
73
|
del self._features[key]
|
|
74
74
|
|
|
75
75
|
def add_child(self, child: "Tree") -> "Tree":
|
|
76
|
+
if child.parent is not None:
|
|
77
|
+
raise ValueError(f"Node {child.name} already has a parent.")
|
|
76
78
|
child._parent = self
|
|
77
79
|
self._children.append(child)
|
|
78
80
|
return self
|
|
@@ -81,10 +83,12 @@ class Tree:
|
|
|
81
83
|
self._children.remove(child)
|
|
82
84
|
child._parent = None
|
|
83
85
|
|
|
84
|
-
def set_parent(self,
|
|
85
|
-
self.
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
def set_parent(self, parent: "Tree | None"):
|
|
87
|
+
if self.parent is not None:
|
|
88
|
+
self.parent.remove_child(self)
|
|
89
|
+
self._parent = parent
|
|
90
|
+
if parent is not None:
|
|
91
|
+
parent._children.append(self)
|
|
88
92
|
|
|
89
93
|
def inorder_traversal(self) -> Iterator["Tree"]:
|
|
90
94
|
if self.is_leaf():
|
|
@@ -107,6 +111,12 @@ class Tree:
|
|
|
107
111
|
yield from child.postorder_traversal()
|
|
108
112
|
yield self
|
|
109
113
|
|
|
114
|
+
def iter_ancestors(self, stop: "Tree | None" = None) -> Iterator["Tree"]:
|
|
115
|
+
node = self
|
|
116
|
+
while node is not None and node is not stop:
|
|
117
|
+
yield node
|
|
118
|
+
node = node.parent
|
|
119
|
+
|
|
110
120
|
def breadth_first_traversal(self) -> Iterator["Tree"]:
|
|
111
121
|
queue: deque["Tree"] = deque([self])
|
|
112
122
|
while queue:
|
|
@@ -126,6 +136,12 @@ class Tree:
|
|
|
126
136
|
def get_leaves(self) -> tuple["Tree", ...]:
|
|
127
137
|
return tuple(node for node in self if node.is_leaf())
|
|
128
138
|
|
|
139
|
+
def get_internal_nodes(self) -> tuple["Tree", ...]:
|
|
140
|
+
return tuple(node for node in self if not node.is_leaf())
|
|
141
|
+
|
|
142
|
+
def is_binary(self) -> bool:
|
|
143
|
+
return all(len(node.children) in (0, 2) for node in self)
|
|
144
|
+
|
|
129
145
|
def ladderize(self, criterion: Callable[["Tree"], Any]) -> None:
|
|
130
146
|
self._children.sort(key=criterion)
|
|
131
147
|
for child in self.children:
|
phylogenie/utils.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
from itertools import chain
|
|
2
|
+
from math import comb
|
|
3
|
+
|
|
1
4
|
from phylogenie.tree import Tree
|
|
2
5
|
|
|
3
6
|
|
|
@@ -57,3 +60,117 @@ def get_node_heights(tree: Tree) -> dict[Tree, float]:
|
|
|
57
60
|
for child in node.children
|
|
58
61
|
)
|
|
59
62
|
return heights
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_mrca(node1: Tree, node2: Tree) -> Tree:
|
|
66
|
+
node1_ancestors = set(node1.iter_ancestors())
|
|
67
|
+
for node2_ancestor in node2.iter_ancestors():
|
|
68
|
+
if node2_ancestor in node1_ancestors:
|
|
69
|
+
return node2_ancestor
|
|
70
|
+
raise ValueError(f"No common ancestor found between node {node1} and node {node2}.")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_distance(node1: Tree, node2: Tree) -> float:
|
|
74
|
+
mrca = get_mrca(node1, node2)
|
|
75
|
+
path = list(chain(node1.iter_ancestors(stop=mrca), node2.iter_ancestors(stop=mrca)))
|
|
76
|
+
if any(node.branch_length is None for node in path):
|
|
77
|
+
return len(path)
|
|
78
|
+
return sum(node.branch_length for node in path) # pyright: ignore
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def compute_sackin_index(tree: Tree, normalize: bool = False) -> float:
|
|
82
|
+
"""
|
|
83
|
+
Compute the Sackin index of a tree.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
tree : Tree
|
|
88
|
+
The input tree.
|
|
89
|
+
normalize : bool, optional
|
|
90
|
+
Whether to normalize the index between 0 and 1, by default False.
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
float
|
|
95
|
+
The Sackin index of the tree.
|
|
96
|
+
|
|
97
|
+
References
|
|
98
|
+
----------
|
|
99
|
+
- Kwang-Tsao Shao and Robert R Sokal. Tree Balance. Systematic Zoology, 39(3):266, 1990.
|
|
100
|
+
"""
|
|
101
|
+
leaves = tree.get_leaves()
|
|
102
|
+
depth_levels = get_node_depth_levels(tree)
|
|
103
|
+
sackin_index = sum(depth_levels[leaf] for leaf in leaves)
|
|
104
|
+
if normalize:
|
|
105
|
+
n = len(leaves)
|
|
106
|
+
max_sackin_index = (n + 2) * (n - 1) / 2
|
|
107
|
+
return (sackin_index - n) / (max_sackin_index - n)
|
|
108
|
+
return sackin_index
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def compute_colless_index(tree: Tree, normalize: bool = False) -> float:
|
|
112
|
+
"""
|
|
113
|
+
Compute the Colless index of a binary tree.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
tree : Tree
|
|
118
|
+
The input binary tree.
|
|
119
|
+
normalize : bool, optional
|
|
120
|
+
Whether to normalize the index between 0 and 1, by default False.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
float
|
|
125
|
+
The Colless index of the tree.
|
|
126
|
+
|
|
127
|
+
References
|
|
128
|
+
----------
|
|
129
|
+
- Donald H. Colless. Review of phylogenetics: the theory and practice of phylogenetic systematics. Systematic Zoology, 31(1):100–104, 1982.
|
|
130
|
+
"""
|
|
131
|
+
if not tree.is_binary():
|
|
132
|
+
raise ValueError("Colless index is only defined for binary trees.")
|
|
133
|
+
|
|
134
|
+
internal_nodes = tree.get_internal_nodes()
|
|
135
|
+
if not internal_nodes:
|
|
136
|
+
raise ValueError(
|
|
137
|
+
"Tree must have at least one internal node to compute the Colless index."
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
colless_index = 0
|
|
141
|
+
leaf_counts = get_node_leaf_counts(tree)
|
|
142
|
+
for node in internal_nodes:
|
|
143
|
+
left, right = node.children
|
|
144
|
+
colless_index += abs(leaf_counts[left] - leaf_counts[right])
|
|
145
|
+
if normalize:
|
|
146
|
+
n_leaves = len(leaf_counts) - len(internal_nodes)
|
|
147
|
+
max_colless_index = comb(n_leaves, 2)
|
|
148
|
+
return colless_index / max_colless_index
|
|
149
|
+
return colless_index
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def compute_mean_leaf_pairwise_distance(tree: Tree) -> float:
|
|
153
|
+
"""
|
|
154
|
+
Compute the mean pairwise distance between all pairs of leaves in the tree.
|
|
155
|
+
|
|
156
|
+
Parameters
|
|
157
|
+
----------
|
|
158
|
+
tree : Tree
|
|
159
|
+
The input tree.
|
|
160
|
+
|
|
161
|
+
Returns
|
|
162
|
+
-------
|
|
163
|
+
float
|
|
164
|
+
The mean pairwise distance between all pairs of leaves in the tree.
|
|
165
|
+
"""
|
|
166
|
+
leaves = tree.get_leaves()
|
|
167
|
+
n_leaves = len(leaves)
|
|
168
|
+
if n_leaves < 2:
|
|
169
|
+
return 0.0
|
|
170
|
+
|
|
171
|
+
total_distance = sum(
|
|
172
|
+
get_distance(leaves[i], leaves[j])
|
|
173
|
+
for i in range(n_leaves)
|
|
174
|
+
for j in range(i + 1, n_leaves)
|
|
175
|
+
)
|
|
176
|
+
return total_distance / comb(n_leaves, 2)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
phylogenie/__init__.py,sha256=
|
|
1
|
+
phylogenie/__init__.py,sha256=8ulA-U7-WnBLNsqRYqPOPrJG8X4ZzyEU02oaDbR_Hxs,2849
|
|
2
|
+
phylogenie/draw.py,sha256=mXCBS5xtJh0zJza8K2QkmRg_YbeCINst0vh6YMv6HQg,4959
|
|
2
3
|
phylogenie/generators/__init__.py,sha256=zsOxy28-9j9alOQLIgrOAFfmM58NNHO_NEtW-KXQXAY,888
|
|
3
4
|
phylogenie/generators/alisim.py,sha256=0aCLuGInifWgAvfh7zARWSKF4EMw3TjlPXMLSECui0k,2783
|
|
4
5
|
phylogenie/generators/configs.py,sha256=WFoeKpgj9ZQIom7BKqwpgXbriiQGg3jFBMLoD8KButk,1073
|
|
@@ -10,13 +11,12 @@ phylogenie/io.py,sha256=nwy8DOknt0HqF9qMeFZHrCmSXpM5AGrU5oajwTtD6vY,3973
|
|
|
10
11
|
phylogenie/main.py,sha256=vtvSpQxBNlYABoFQ25czl-l3fIr4QRo3svWVd-jcArw,1170
|
|
11
12
|
phylogenie/models.py,sha256=pCg9ob0RpLUHwM49x4knKxL4FNPr3-EU_6zMXsvxtAg,370
|
|
12
13
|
phylogenie/msa.py,sha256=JDGyZUsAq6-m-SQjoCDjAkAZIxfgyl_PDIhdYn5HOow,2064
|
|
13
|
-
phylogenie/plot.py,sha256=5xwPbZWO0jX_spEVPH7UUnxc7evqQbhSPKHWpXHNXro,5121
|
|
14
14
|
phylogenie/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
15
|
phylogenie/skyline/__init__.py,sha256=7pF4CUb4ZCLzNYJNhOjpuTOLTRhlK7L6ugfccNqjIGo,620
|
|
16
16
|
phylogenie/skyline/matrix.py,sha256=Gl8OgKjtieG0NwPYiPimKI36gefV8fm_OeorjdXxPTs,9146
|
|
17
17
|
phylogenie/skyline/parameter.py,sha256=EM9qlPt0JhMBy3TbztM0dj24BaGNEy8KWKdTObDKhbI,4644
|
|
18
18
|
phylogenie/skyline/vector.py,sha256=bJP7_FNX_Klt6wXqsyfj0KX3VNj6-dIhzCKSJuQcOV0,7115
|
|
19
|
-
phylogenie/tree.py,sha256=
|
|
19
|
+
phylogenie/tree.py,sha256=mv8Jrb2jzK5q-vx5E1-mAQLagkP4aHgaL6tNpAOPfIE,5194
|
|
20
20
|
phylogenie/treesimulator/__init__.py,sha256=yqS2vtYMhdWSXc9RAnX1dd4zAqSQweMLyVKTnJLfGTU,1106
|
|
21
21
|
phylogenie/treesimulator/events/__init__.py,sha256=6zSgZ0MEUMvTK4yPlSolJnRWzCARLS-jYreTzh45mQo,1033
|
|
22
22
|
phylogenie/treesimulator/events/contact_tracing.py,sha256=_nJ85yhgGkeruQgMHvGpDYoyhheBf8M4LgZWiWdi5dY,4801
|
|
@@ -27,9 +27,9 @@ phylogenie/treesimulator/gillespie.py,sha256=LZHB2Ko147E78LoUCtN_BN7NYO1xhMYRy5P
|
|
|
27
27
|
phylogenie/treesimulator/model.py,sha256=Nyg6R8XmMwZMSw1-dII81sU9uU7tDe-NMs8v1qKE4_M,5746
|
|
28
28
|
phylogenie/typeguards.py,sha256=JtqmbEWJZBRHbWgCvcl6nrWm3VcBfzRbklbTBYHItn0,1325
|
|
29
29
|
phylogenie/typings.py,sha256=GknvAFXyiaWeeYJ8Lk5d6E2VHT-xW6ONEojYbtJYiB8,476
|
|
30
|
-
phylogenie/utils.py,sha256=
|
|
31
|
-
phylogenie-2.1.
|
|
32
|
-
phylogenie-2.1.
|
|
33
|
-
phylogenie-2.1.
|
|
34
|
-
phylogenie-2.1.
|
|
35
|
-
phylogenie-2.1.
|
|
30
|
+
phylogenie/utils.py,sha256=ehVk_2kvjW8Q_EyM2kxBPHYiK-KlPmZQx7JeVN6Fh-E,5419
|
|
31
|
+
phylogenie-2.1.21.dist-info/LICENSE.txt,sha256=NUrDqElK-eD3I0WqC004CJsy6cs0JgsAoebDv_42-pw,1071
|
|
32
|
+
phylogenie-2.1.21.dist-info/METADATA,sha256=De8Vqs1ngZOKgBdivLw1hWrtMDdtFWLRDW3M4-IAAyg,5477
|
|
33
|
+
phylogenie-2.1.21.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
34
|
+
phylogenie-2.1.21.dist-info/entry_points.txt,sha256=Rt6_usN0FkBX1ZfiqCirjMN9FKOgFLG8rydcQ8kugeE,51
|
|
35
|
+
phylogenie-2.1.21.dist-info/RECORD,,
|
phylogenie/plot.py
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
|
-
from typing import Any
|
|
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 mpl_toolkits.axes_grid1.inset_locator import inset_axes # pyright: ignore
|
|
9
|
-
|
|
10
|
-
from phylogenie.tree import Tree
|
|
11
|
-
from phylogenie.utils import get_node_depth_levels, get_node_depths
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class Coloring(str, Enum):
|
|
15
|
-
DISCRETE = "discrete"
|
|
16
|
-
CONTINUOUS = "continuous"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def plot_tree(
|
|
20
|
-
tree: Tree,
|
|
21
|
-
ax: Axes | None = None,
|
|
22
|
-
color_by: str | None = None,
|
|
23
|
-
default_color: str = "black",
|
|
24
|
-
coloring: str | Coloring | None = None,
|
|
25
|
-
cmap: str | None = None,
|
|
26
|
-
show_legend: bool = True,
|
|
27
|
-
show_hist: bool = True,
|
|
28
|
-
hist_kwargs: dict[str, Any] | None = None,
|
|
29
|
-
hist_axes_kwargs: dict[str, Any] | None = None,
|
|
30
|
-
) -> Axes | tuple[Axes, Axes]:
|
|
31
|
-
if ax is None:
|
|
32
|
-
ax = plt.gca()
|
|
33
|
-
|
|
34
|
-
xs = (
|
|
35
|
-
get_node_depth_levels(tree)
|
|
36
|
-
if any(node.branch_length is None for node in tree)
|
|
37
|
-
else get_node_depths(tree)
|
|
38
|
-
)
|
|
39
|
-
ys = {node: i for i, node in enumerate(tree.inorder_traversal())}
|
|
40
|
-
|
|
41
|
-
if color_by is not None:
|
|
42
|
-
features = [node.get(color_by) for node in tree if color_by in node.features]
|
|
43
|
-
|
|
44
|
-
if coloring is None and any(isinstance(f, float) for f in features):
|
|
45
|
-
coloring = Coloring.CONTINUOUS
|
|
46
|
-
elif coloring is None:
|
|
47
|
-
coloring = Coloring.DISCRETE
|
|
48
|
-
|
|
49
|
-
if coloring == Coloring.DISCRETE:
|
|
50
|
-
if any(isinstance(f, float) for f in features):
|
|
51
|
-
raise ValueError(
|
|
52
|
-
"Discrete coloring selected but feature values are not all categorical."
|
|
53
|
-
)
|
|
54
|
-
colormap = plt.get_cmap("tab20" if cmap is None else cmap)
|
|
55
|
-
feature_colors = {
|
|
56
|
-
f: mcolors.to_hex(colormap(i)) for i, f in enumerate(set(features))
|
|
57
|
-
}
|
|
58
|
-
colors = {
|
|
59
|
-
node: (
|
|
60
|
-
feature_colors[node.get(color_by)]
|
|
61
|
-
if color_by in node.features
|
|
62
|
-
else default_color
|
|
63
|
-
)
|
|
64
|
-
for node in tree
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if show_legend:
|
|
68
|
-
legend_handles = [
|
|
69
|
-
mpatches.Patch(color=feature_colors[f], label=str(f))
|
|
70
|
-
for f in feature_colors
|
|
71
|
-
]
|
|
72
|
-
if any(color_by not in node.features for node in tree):
|
|
73
|
-
legend_handles.append(
|
|
74
|
-
mpatches.Patch(color=default_color, label="NA")
|
|
75
|
-
)
|
|
76
|
-
ax.legend(handles=legend_handles, title=color_by) # pyright: ignore
|
|
77
|
-
|
|
78
|
-
elif coloring == Coloring.CONTINUOUS:
|
|
79
|
-
norm = mcolors.Normalize(vmin=min(features), vmax=max(features))
|
|
80
|
-
colormap = plt.get_cmap("viridis" if cmap is None else cmap)
|
|
81
|
-
colors = {
|
|
82
|
-
node: (
|
|
83
|
-
colormap(norm(float(node.get(color_by))))
|
|
84
|
-
if color_by in node.features
|
|
85
|
-
else default_color
|
|
86
|
-
)
|
|
87
|
-
for node in tree
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if show_hist:
|
|
91
|
-
default_hist_axes_kwargs = {"width": "25%", "height": "25%"}
|
|
92
|
-
if hist_axes_kwargs is not None:
|
|
93
|
-
default_hist_axes_kwargs.update(hist_axes_kwargs)
|
|
94
|
-
hist_ax = inset_axes(ax, **default_hist_axes_kwargs) # pyright: ignore
|
|
95
|
-
|
|
96
|
-
hist_kwargs = {} if hist_kwargs is None else hist_kwargs
|
|
97
|
-
_, bins, patches = hist_ax.hist( # pyright: ignore
|
|
98
|
-
features, **hist_kwargs
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
for patch, b0, b1 in zip( # pyright: ignore
|
|
102
|
-
patches, bins[:-1], bins[1:] # pyright: ignore
|
|
103
|
-
):
|
|
104
|
-
midpoint = (b0 + b1) / 2 # pyright: ignore
|
|
105
|
-
patch.set_facecolor(colormap(norm(midpoint))) # pyright: ignore
|
|
106
|
-
else:
|
|
107
|
-
sm = plt.cm.ScalarMappable(cmap=colormap, norm=norm)
|
|
108
|
-
ax.get_figure().colorbar(sm, ax=ax) # pyright: ignore
|
|
109
|
-
|
|
110
|
-
else:
|
|
111
|
-
raise ValueError(
|
|
112
|
-
f"Unknown coloring method: {coloring}. Choices are {list(Coloring)}."
|
|
113
|
-
)
|
|
114
|
-
else:
|
|
115
|
-
colors = {node: default_color for node in tree}
|
|
116
|
-
|
|
117
|
-
for node in tree:
|
|
118
|
-
x1, y1 = xs[node], ys[node]
|
|
119
|
-
if node.parent is None:
|
|
120
|
-
ax.hlines(y=y1, xmin=0, xmax=x1, color=colors[node]) # pyright: ignore
|
|
121
|
-
continue
|
|
122
|
-
x0, y0 = xs[node.parent], ys[node.parent]
|
|
123
|
-
ax.vlines(x=x0, ymin=y0, ymax=y1, color=colors[node]) # pyright: ignore
|
|
124
|
-
ax.hlines(y=y1, xmin=x0, xmax=x1, color=colors[node]) # pyright: ignore
|
|
125
|
-
|
|
126
|
-
for node in tree:
|
|
127
|
-
x1, y1 = xs[node], ys[node]
|
|
128
|
-
if node.parent is None:
|
|
129
|
-
ax.hlines(y=y1, xmin=0, xmax=x1, color=colors[node]) # pyright: ignore
|
|
130
|
-
continue
|
|
131
|
-
x0, y0 = xs[node.parent], ys[node.parent]
|
|
132
|
-
ax.vlines(x=x0, ymin=y0, ymax=y1, color=colors[node]) # pyright: ignore
|
|
133
|
-
ax.hlines(y=y1, xmin=x0, xmax=x1, color=colors[node]) # pyright: ignore
|
|
134
|
-
|
|
135
|
-
ax.set_yticks([]) # pyright: ignore
|
|
136
|
-
return ax
|
|
File without changes
|
|
File without changes
|
|
File without changes
|