phylogenie 2.1.12__py3-none-any.whl → 2.1.14__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.
@@ -9,6 +9,7 @@ from phylogenie.generators.dataset import DatasetGenerator, DataType
9
9
  from phylogenie.generators.factories import data, string
10
10
  from phylogenie.generators.trees import TreeDatasetGeneratorConfig
11
11
  from phylogenie.io import dump_newick
12
+ from phylogenie.utils import get_node_depths
12
13
 
13
14
  MSAS_DIRNAME = "MSAs"
14
15
  TREES_DIRNAME = "trees"
@@ -71,8 +72,9 @@ class AliSimDatasetGenerator(DatasetGenerator):
71
72
  "Tree simulation timed out, retrying with different parameters..."
72
73
  )
73
74
 
75
+ times = get_node_depths(tree)
74
76
  for leaf in tree.get_leaves():
75
- leaf.name += f"|{leaf.get_time()}"
77
+ leaf.name += f"|{times[leaf]}"
76
78
  dump_newick(tree, f"{tree_filename}.nwk")
77
79
 
78
80
  self._generate_one_from_tree(msa_filename, f"{tree_filename}.nwk", rng, d)
phylogenie/plot.py CHANGED
@@ -6,7 +6,7 @@ import matplotlib.pyplot as plt
6
6
  from matplotlib.axes import Axes
7
7
 
8
8
  from phylogenie.tree import Tree
9
- from phylogenie.utils import get_times
9
+ from phylogenie.utils import get_node_depths
10
10
 
11
11
 
12
12
  class Coloring(str, Enum):
@@ -22,45 +22,69 @@ def plot_tree(
22
22
  coloring: str | Coloring | None = None,
23
23
  cmap: str | None = None,
24
24
  show_legend: bool = True,
25
+ x_feature: str | None = None,
25
26
  ) -> Axes:
26
27
  if ax is None:
27
28
  ax = plt.gca()
28
29
 
29
- xs = get_times(tree)
30
+ xs = (
31
+ get_node_depths(tree)
32
+ if x_feature is None
33
+ else {node: node.get(x_feature) for node in tree}
34
+ )
30
35
  ys = {node: i for i, node in enumerate(tree.inorder_traversal())}
31
36
 
32
37
  if color_by is not None:
33
- features = set(node.get(color_by) for node in tree)
38
+ features = set(node.get(color_by) for node in tree if color_by in node.features)
34
39
  if coloring is None and any(isinstance(f, float) for f in features):
35
40
  coloring = Coloring.CONTINUOUS
36
41
  elif coloring is None:
37
42
  coloring = Coloring.DISCRETE
43
+
38
44
  if coloring == Coloring.DISCRETE:
39
45
  if any(isinstance(f, float) for f in features):
40
46
  raise ValueError(
41
47
  "Discrete coloring selected but feature values are not all categorical."
42
48
  )
43
- cmap = "tab20" if cmap is None else cmap
44
- colormap = plt.get_cmap(cmap, len(features))
49
+ colormap = plt.get_cmap("tab20" if cmap is None else cmap)
45
50
  feature_colors = {
46
51
  f: mcolors.to_hex(colormap(i)) for i, f in enumerate(features)
47
52
  }
48
- colors = {node: feature_colors[node.get(color_by)] for node in tree}
49
- legend_handles = [
50
- mpatches.Patch(color=feature_colors[f], label=str(f)) for f in features
51
- ]
53
+ colors = {
54
+ node: (
55
+ feature_colors[node.get(color_by)]
56
+ if color_by in node.features
57
+ else default_color
58
+ )
59
+ for node in tree
60
+ }
61
+
52
62
  if show_legend:
63
+ legend_handles = [
64
+ mpatches.Patch(color=feature_colors[f], label=str(f))
65
+ for f in features
66
+ ]
67
+ if any(color_by not in node.features for node in tree):
68
+ legend_handles.append(
69
+ mpatches.Patch(color=default_color, label="None")
70
+ )
53
71
  ax.legend(handles=legend_handles, title=color_by) # pyright: ignore
54
- elif coloring in {Coloring.CONTINUOUS}:
55
- cmap = "viridis" if cmap is None else cmap
56
- values = list(map(float, features))
57
- norm = mcolors.Normalize(vmin=min(values), vmax=max(values))
58
- colormap = plt.get_cmap(cmap)
59
- colors = {node: colormap(norm(float(node.get(color_by)))) for node in tree}
72
+
73
+ elif coloring == Coloring.CONTINUOUS:
74
+ norm = mcolors.Normalize(vmin=min(features), vmax=max(features))
75
+ colormap = plt.get_cmap("viridis" if cmap is None else cmap)
76
+ colors = {
77
+ node: (
78
+ colormap(norm(float(node.get(color_by))))
79
+ if color_by in node.features
80
+ else default_color
81
+ )
82
+ for node in tree
83
+ }
60
84
 
61
85
  sm = plt.cm.ScalarMappable(cmap=colormap, norm=norm)
62
- fig = ax.get_figure()
63
- fig.colorbar(sm, ax=ax, label=color_by) # pyright: ignore
86
+ ax.get_figure().colorbar(sm, ax=ax) # pyright: ignore
87
+
64
88
  else:
65
89
  raise ValueError(
66
90
  f"Unknown coloring method: {coloring}. Choices are {list(Coloring)}."
@@ -69,11 +93,11 @@ def plot_tree(
69
93
  colors = {node: default_color for node in tree}
70
94
 
71
95
  for node in tree:
72
- x1, y1 = xs[node.name], ys[node]
96
+ x1, y1 = xs[node], ys[node]
73
97
  if node.parent is None:
74
98
  ax.hlines(y=y1, xmin=0, xmax=x1, color=colors[node]) # pyright: ignore
75
99
  continue
76
- x0, y0 = xs[node.parent.name], ys[node.parent]
100
+ x0, y0 = xs[node.parent], ys[node.parent]
77
101
  ax.vlines(x=x0, ymin=y0, ymax=y1, color=colors[node]) # pyright: ignore
78
102
  ax.hlines(y=y1, xmin=x0, xmax=x1, color=colors[node]) # pyright: ignore
79
103
 
phylogenie/tree.py CHANGED
@@ -23,12 +23,49 @@ class Tree:
23
23
  return self._features.copy()
24
24
 
25
25
  @property
26
- def time_to_parent(self) -> float:
27
- if self.parent is None and self.branch_length is None:
26
+ def depth(self) -> float:
27
+ if self.parent is None:
28
28
  return 0.0
29
29
  if self.branch_length is None:
30
30
  raise ValueError(f"Branch length of node {self.name} is not set.")
31
- return self.branch_length
31
+ return self.parent.depth + self.branch_length
32
+
33
+ @property
34
+ def depth_level(self) -> int:
35
+ if self.parent is None:
36
+ return 0
37
+ return self.parent.depth_level + 1
38
+
39
+ @property
40
+ def height(self) -> float:
41
+ if self.is_leaf():
42
+ return 0.0
43
+ if any(child.branch_length is None for child in self.children):
44
+ raise ValueError(
45
+ f"Branch length of one or more children of node {self.name} is not set."
46
+ )
47
+ return max(
48
+ child.branch_length + child.height # pyright: ignore
49
+ for child in self.children
50
+ )
51
+
52
+ @property
53
+ def height_level(self) -> int:
54
+ if self.is_leaf():
55
+ return 0
56
+ return 1 + max(child.height_level for child in self.children)
57
+
58
+ def set(self, key: str, value: Any) -> None:
59
+ self._features[key] = value
60
+
61
+ def update_features(self, features: dict[str, Any]) -> None:
62
+ self._features.update(features)
63
+
64
+ def get(self, key: str) -> Any:
65
+ return self._features[key]
66
+
67
+ def delete(self, key: str) -> None:
68
+ del self._features[key]
32
69
 
33
70
  def add_child(self, child: "Tree") -> "Tree":
34
71
  child._parent = self
@@ -75,23 +112,12 @@ class Tree:
75
112
  return not self.children
76
113
 
77
114
  def get_leaves(self) -> tuple["Tree", ...]:
78
- return tuple(node for node in self if not node.children)
115
+ return tuple(node for node in self if node.is_leaf())
79
116
 
80
- def get_time(self) -> float:
81
- parent_time = 0 if self.parent is None else self.parent.get_time()
82
- return self.time_to_parent + parent_time
83
-
84
- def set(self, key: str, value: Any) -> None:
85
- self._features[key] = value
86
-
87
- def update_features(self, features: dict[str, Any]) -> None:
88
- self._features.update(features)
89
-
90
- def get(self, key: str) -> Any:
91
- return self._features[key]
92
-
93
- def delete(self, key: str) -> None:
94
- del self._features[key]
117
+ def ladderize(self, feature: str) -> None:
118
+ self._children.sort(key=lambda x: x.get(feature))
119
+ for child in self.children:
120
+ child.ladderize(feature)
95
121
 
96
122
  def copy(self):
97
123
  new_tree = Tree(self.name, self.branch_length)
@@ -103,5 +129,8 @@ class Tree:
103
129
  def __iter__(self) -> Iterator["Tree"]:
104
130
  return self.preorder_traversal()
105
131
 
132
+ def __len__(self) -> int:
133
+ return sum(1 for _ in self)
134
+
106
135
  def __repr__(self) -> str:
107
136
  return f"TreeNode(name='{self.name}', branch_length={self.branch_length}, features={self.features})"
@@ -4,31 +4,41 @@ from enum import Enum
4
4
  from phylogenie.tree import Tree
5
5
  from phylogenie.treesimulator.events import get_mutation_id
6
6
  from phylogenie.treesimulator.model import get_node_state
7
- from phylogenie.utils import get_heights, get_n_tips, get_times
7
+ from phylogenie.utils import (
8
+ get_node_depth_levels,
9
+ get_node_depths,
10
+ get_node_height_levels,
11
+ get_node_heights,
12
+ get_node_leaf_counts,
13
+ )
8
14
 
9
15
 
10
- def _get_states(tree: Tree) -> dict[str, str]:
11
- return {node.name: get_node_state(node.name) for node in tree}
16
+ def _get_states(tree: Tree) -> dict[Tree, str]:
17
+ return {node: get_node_state(node.name) for node in tree}
12
18
 
13
19
 
14
- def _get_mutations(tree: Tree) -> dict[str, int]:
15
- return {node.name: get_mutation_id(node.name) for node in tree}
20
+ def _get_mutations(tree: Tree) -> dict[Tree, int]:
21
+ return {node: get_mutation_id(node.name) for node in tree}
16
22
 
17
23
 
18
24
  class Feature(str, Enum):
19
- STATE = "state"
20
- MUTATION = "mutation"
21
- N_TIPS = "n_tips"
22
- TIME = "time"
25
+ DEPTH = "depth"
26
+ DEPTH_LEVEL = "depth_level"
23
27
  HEIGHT = "height"
28
+ HEIGHT_LEVEL = "height_level"
29
+ MUTATION = "mutation"
30
+ N_LEAVES = "n_leaves"
31
+ STATE = "state"
24
32
 
25
33
 
26
34
  FEATURES_EXTRACTORS = {
27
- Feature.STATE: _get_states,
35
+ Feature.DEPTH: get_node_depths,
36
+ Feature.DEPTH_LEVEL: get_node_depth_levels,
37
+ Feature.HEIGHT: get_node_heights,
38
+ Feature.HEIGHT_LEVEL: get_node_height_levels,
28
39
  Feature.MUTATION: _get_mutations,
29
- Feature.N_TIPS: get_n_tips,
30
- Feature.TIME: get_times,
31
- Feature.HEIGHT: get_heights,
40
+ Feature.N_LEAVES: get_node_leaf_counts,
41
+ Feature.STATE: _get_states,
32
42
  }
33
43
 
34
44
 
@@ -36,4 +46,4 @@ def set_features(tree: Tree, features: Iterable[Feature]) -> None:
36
46
  for feature in features:
37
47
  feature_maps = FEATURES_EXTRACTORS[feature](tree)
38
48
  for node in tree:
39
- node.set(feature.value, feature_maps[node.name])
49
+ node.set(feature.value, feature_maps[node])
@@ -89,9 +89,7 @@ class Model:
89
89
  def _set_branch_length(self, node: Tree, time: float) -> None:
90
90
  if node.branch_length is not None:
91
91
  raise ValueError(f"Branch length of node {node.name} is already set.")
92
- node.branch_length = (
93
- time if node.parent is None else time - node.parent.get_time()
94
- )
92
+ node.branch_length = time if node.parent is None else time - node.parent.depth
95
93
 
96
94
  def _stem(self, individual: Individual, time: float) -> None:
97
95
  self._set_branch_length(individual.node, time)
phylogenie/utils.py CHANGED
@@ -1,28 +1,59 @@
1
1
  from phylogenie.tree import Tree
2
2
 
3
3
 
4
- def get_n_tips(tree: Tree) -> dict[str, int]:
5
- n_tips: dict[str, int] = {}
4
+ def get_node_leaf_counts(tree: Tree) -> dict[Tree, int]:
5
+ n_leaves: dict[Tree, int] = {}
6
6
  for node in tree.postorder_traversal():
7
- n_tips[node.name] = (
8
- 1 if node.is_leaf() else sum(n_tips[child.name] for child in node.children)
9
- )
10
- return n_tips
7
+ n_leaves[node] = sum(n_leaves[child] for child in node.children) or 1
8
+ return n_leaves
11
9
 
12
10
 
13
- def get_times(tree: Tree) -> dict[str, float]:
14
- times: dict[str, float] = {}
11
+ def get_node_depth_levels(tree: Tree) -> dict[Tree, int]:
12
+ depth_levels: dict[Tree, int] = {}
15
13
  for node in tree:
16
- parent_time = 0 if node.parent is None else times[node.parent.name]
17
- times[node.name] = node.time_to_parent + parent_time
18
- return times
14
+ if node.parent is None:
15
+ depth_levels[node] = 0
16
+ else:
17
+ depth_levels[node] = depth_levels[node.parent] + 1
18
+ return depth_levels
19
+
20
+
21
+ def get_node_depths(tree: Tree) -> dict[Tree, float]:
22
+ depths: dict[Tree, float] = {}
23
+ for node in tree:
24
+ if node.parent is None:
25
+ depths[node] = 0
26
+ else:
27
+ if node.branch_length is None:
28
+ raise ValueError(f"Branch length of node {node.name} is not set.")
29
+ depths[node] = depths[node.parent] + node.branch_length
30
+ return depths
31
+
32
+
33
+ def get_node_height_levels(tree: Tree) -> dict[Tree, int]:
34
+ height_levels: dict[Tree, int] = {}
35
+ for node in tree.postorder_traversal():
36
+ if node.is_leaf():
37
+ height_levels[node] = 0
38
+ else:
39
+ height_levels[node] = max(
40
+ 1 + height_levels[child] for child in node.children
41
+ )
42
+ return height_levels
19
43
 
20
44
 
21
- def get_heights(tree: Tree) -> dict[str, int]:
22
- heights: dict[str, int] = {}
45
+ def get_node_heights(tree: Tree) -> dict[Tree, float]:
46
+ heights: dict[Tree, float] = {}
23
47
  for node in tree.postorder_traversal():
24
48
  if node.is_leaf():
25
- heights[node.name] = 0
49
+ heights[node] = 0
26
50
  else:
27
- heights[node.name] = 1 + max(heights[child.name] for child in node.children)
51
+ if any(child.branch_length is None for child in node.children):
52
+ raise ValueError(
53
+ f"Branch length of one or more children of node {node.name} is not set."
54
+ )
55
+ heights[node] = max(
56
+ child.branch_length + heights[child] # pyright: ignore
57
+ for child in node.children
58
+ )
28
59
  return heights
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: phylogenie
3
- Version: 2.1.12
3
+ Version: 2.1.14
4
4
  Summary: Generate phylogenetic datasets with minimal setup effort
5
5
  Author: Gabriele Marino
6
6
  Author-email: gabmarino.8601@gmail.com
@@ -1,6 +1,6 @@
1
1
  phylogenie/__init__.py,sha256=FQE_BakIftKaCnMaCZ4qpoSUH0JJPh2igAZLl_5pA28,2296
2
2
  phylogenie/generators/__init__.py,sha256=zsOxy28-9j9alOQLIgrOAFfmM58NNHO_NEtW-KXQXAY,888
3
- phylogenie/generators/alisim.py,sha256=G7p6tfcDWncg4xl3NhEXxgikcZDUi_RdE33zWn_CvzY,2704
3
+ phylogenie/generators/alisim.py,sha256=0aCLuGInifWgAvfh7zARWSKF4EMw3TjlPXMLSECui0k,2783
4
4
  phylogenie/generators/configs.py,sha256=WFoeKpgj9ZQIom7BKqwpgXbriiQGg3jFBMLoD8KButk,1073
5
5
  phylogenie/generators/dataset.py,sha256=pPwW9yxm9fkU0PPllFq8EsPlqau8tth-4OatbA_hEHo,2120
6
6
  phylogenie/generators/factories.py,sha256=TuVFQWRjq33Hewjw_Lp8tQ0l_IPtqYDyQCNJhtiHpw8,7882
@@ -10,26 +10,26 @@ phylogenie/io.py,sha256=nwy8DOknt0HqF9qMeFZHrCmSXpM5AGrU5oajwTtD6vY,3973
10
10
  phylogenie/main.py,sha256=vtvSpQxBNlYABoFQ25czl-l3fIr4QRo3svWVd-jcArw,1170
11
11
  phylogenie/models.py,sha256=pCg9ob0RpLUHwM49x4knKxL4FNPr3-EU_6zMXsvxtAg,370
12
12
  phylogenie/msa.py,sha256=JDGyZUsAq6-m-SQjoCDjAkAZIxfgyl_PDIhdYn5HOow,2064
13
- phylogenie/plot.py,sha256=hjftVhapi9AoD2_e200E82rVtYHY5dNptvyMKYcbnQI,2999
13
+ phylogenie/plot.py,sha256=5InWceLwjZV2qVYv2jaTJPs_0gfAEaKYGQt_jnLpaz0,3615
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=bWQqbr8CbWZZHKQr-iav4gU2sLbnGRXuBk5_ZJZEZBU,3345
19
+ phylogenie/tree.py,sha256=PjyfsjYWadvIQSOxyIO0oaRVN2pecXEnUR-k1mM4Vic,4159
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
23
23
  phylogenie/treesimulator/events/core.py,sha256=RF7oHzAjkU675PnczaVc66d9gNrHBL-IhmVHtcy7MKE,7949
24
24
  phylogenie/treesimulator/events/mutations.py,sha256=erEvgfiv_X3G_DwK9Hqu-fAR8otupfwq66cp5tRZamM,3591
25
- phylogenie/treesimulator/features.py,sha256=f1t7DE_dw8MhD-FAzQr0j8pxHdvLENy5AQeAjJyOjOQ,1082
25
+ phylogenie/treesimulator/features.py,sha256=Wj1rjbOAHHE9XqhrLND2GMgqTB_4M9MdAD5OWoUoatc,1356
26
26
  phylogenie/treesimulator/gillespie.py,sha256=LZHB2Ko147E78LoUCtN_BN7NYO1xhMYRy5PUZbN93c0,5283
27
- phylogenie/treesimulator/model.py,sha256=Ct0lfn6maKtjuFxivWx1MbFHvH3Y-fiJ0XXMdkN3Cak,5775
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=tVorfhMuRkxogeNu45aBv4SDBflHg4faiIpnHNJPKPY,871
31
- phylogenie-2.1.12.dist-info/LICENSE.txt,sha256=NUrDqElK-eD3I0WqC004CJsy6cs0JgsAoebDv_42-pw,1071
32
- phylogenie-2.1.12.dist-info/METADATA,sha256=65IaQc4XK0qJH_CwrqkdHtVpU2u8mxQLZxuzpDIljK8,5477
33
- phylogenie-2.1.12.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
34
- phylogenie-2.1.12.dist-info/entry_points.txt,sha256=Rt6_usN0FkBX1ZfiqCirjMN9FKOgFLG8rydcQ8kugeE,51
35
- phylogenie-2.1.12.dist-info/RECORD,,
30
+ phylogenie/utils.py,sha256=l7kJ0mkdhplz_hTUseBxaGpEKngsWUOSiCMyLMIWYZQ,1936
31
+ phylogenie-2.1.14.dist-info/LICENSE.txt,sha256=NUrDqElK-eD3I0WqC004CJsy6cs0JgsAoebDv_42-pw,1071
32
+ phylogenie-2.1.14.dist-info/METADATA,sha256=elXtIXUWfGalXgdhTgretrdYVQwOWwiSfHUiZicf5WQ,5477
33
+ phylogenie-2.1.14.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
34
+ phylogenie-2.1.14.dist-info/entry_points.txt,sha256=Rt6_usN0FkBX1ZfiqCirjMN9FKOgFLG8rydcQ8kugeE,51
35
+ phylogenie-2.1.14.dist-info/RECORD,,