phylogenie 3.1.7__tar.gz → 3.1.18__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.

Potentially problematic release.


This version of phylogenie might be problematic. Click here for more details.

Files changed (46) hide show
  1. {phylogenie-3.1.7/src/phylogenie.egg-info → phylogenie-3.1.18}/PKG-INFO +5 -6
  2. {phylogenie-3.1.7 → phylogenie-3.1.18}/README.md +4 -5
  3. {phylogenie-3.1.7 → phylogenie-3.1.18}/pyproject.toml +2 -2
  4. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/__init__.py +8 -14
  5. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/draw.py +154 -53
  6. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/generators/factories.py +17 -13
  7. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/generators/trees.py +17 -3
  8. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/io/fasta.py +9 -3
  9. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/msa.py +15 -7
  10. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/treesimulator/__init__.py +8 -0
  11. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/treesimulator/features.py +6 -0
  12. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/treesimulator/gillespie.py +11 -2
  13. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/treesimulator/tree.py +10 -0
  14. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/treesimulator/utils.py +11 -0
  15. {phylogenie-3.1.7 → phylogenie-3.1.18/src/phylogenie.egg-info}/PKG-INFO +5 -6
  16. {phylogenie-3.1.7 → phylogenie-3.1.18}/LICENSE.txt +0 -0
  17. {phylogenie-3.1.7 → phylogenie-3.1.18}/setup.cfg +0 -0
  18. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/generators/__init__.py +0 -0
  19. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/generators/alisim.py +0 -0
  20. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/generators/configs.py +0 -0
  21. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/generators/dataset.py +0 -0
  22. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/generators/typeguards.py +0 -0
  23. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/io/__init__.py +0 -0
  24. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/main.py +0 -0
  25. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/mixins.py +0 -0
  26. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/py.typed +0 -0
  27. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/skyline/__init__.py +0 -0
  28. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/skyline/matrix.py +0 -0
  29. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/skyline/parameter.py +0 -0
  30. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/skyline/vector.py +0 -0
  31. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/treesimulator/events/__init__.py +0 -0
  32. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/treesimulator/events/base.py +0 -0
  33. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/treesimulator/events/contact_tracing.py +0 -0
  34. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/treesimulator/events/core.py +0 -0
  35. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/treesimulator/events/mutations.py +0 -0
  36. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/treesimulator/io/__init__.py +0 -0
  37. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/treesimulator/io/newick.py +0 -0
  38. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/treesimulator/io/nexus.py +0 -0
  39. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/treesimulator/model.py +0 -0
  40. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/typeguards.py +0 -0
  41. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie/typings.py +0 -0
  42. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie.egg-info/SOURCES.txt +0 -0
  43. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie.egg-info/dependency_links.txt +0 -0
  44. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie.egg-info/entry_points.txt +0 -0
  45. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie.egg-info/requires.txt +0 -0
  46. {phylogenie-3.1.7 → phylogenie-3.1.18}/src/phylogenie.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phylogenie
3
- Version: 3.1.7
3
+ Version: 3.1.18
4
4
  Summary: Generate phylogenetic datasets with minimal setup effort
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -23,7 +23,6 @@ Dynamic: license-file
23
23
  [![PyPI version](https://img.shields.io/pypi/v/phylogenie)](https://pypi.org/project/phylogenie/)
24
24
  ![Downloads](https://img.shields.io/pypi/dm/phylogenie)
25
25
 
26
-
27
26
  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!
28
27
 
29
28
  ## ✨ Features
@@ -73,21 +72,21 @@ Phylogenie relies on [AliSim](https://iqtree.github.io/doc/AliSim) for simulatin
73
72
 
74
73
  ## 🚀 Quick Start
75
74
 
76
- Once you have installed Phylogenie, check out the [examples](https://github.com/gabriele-marino/phylogenie/tree/main/examples) folder.
75
+ Once you have installed Phylogenie, check out the [tutorials](https://github.com/gabriele-marino/phylogenie/tree/main/tutorials) folder.
77
76
  It includes a collection of thoroughly commented configuration files, organized as a step-by-step tutorial. These examples will help you understand how to use Phylogenie in practice and can be easily adapted to fit your own workflow.
78
77
 
79
78
  For quick start, pick your favorite config file and run Phylogenie with:
80
79
  ```bash
81
- phylogenie examples/config_file.yaml
80
+ phylogenie tutorials/config_file.yaml
82
81
  ```
83
82
  This command will create the output dataset in the folder specified inside the configuration file, including data directories and metadata files for each dataset split defined in the config.
84
83
 
85
84
  >❗ *Tip*: Can’t choose just one config file?
86
- You can run them all at once by pointing Phylogenie to the folder! Just use: `phylogenie examples`. In this mode, Phylogenie will automatically find all `.yaml` files in the folder you specified and run for each of them!
85
+ You can run them all at once by pointing Phylogenie to the folder! Just use: `phylogenie tutorials`. In this mode, Phylogenie will automatically find all `.yaml` files in the folder you specified and run for each of them!
87
86
 
88
87
  ## 📖 Documentation
89
88
 
90
- - The [examples](https://github.com/gabriele-marino/phylogenie/tree/main/examples) folder contains many ready-to-use, extensively commented configuration files that serve as a step-by-step tutorial to guide you through using Phylogenie. You can explore them to learn how it works or adapt them directly to your own workflows.
89
+ - The [tutorials](https://github.com/gabriele-marino/phylogenie/tree/main/tutorials) folder contains many ready-to-use, extensively commented configuration files that serve as a step-by-step tutorial to guide you through using Phylogenie. You can explore them to learn how it works or adapt them directly to your own workflows.
91
90
  - A complete user guide and API reference are under development. In the meantime, feel free to [reach out](mailto:gabmarino.8601@email.com) if you have any questions about integrating Phylogenie into your workflows.
92
91
 
93
92
  ## 📄 License
@@ -8,7 +8,6 @@
8
8
  [![PyPI version](https://img.shields.io/pypi/v/phylogenie)](https://pypi.org/project/phylogenie/)
9
9
  ![Downloads](https://img.shields.io/pypi/dm/phylogenie)
10
10
 
11
-
12
11
  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!
13
12
 
14
13
  ## ✨ Features
@@ -58,21 +57,21 @@ Phylogenie relies on [AliSim](https://iqtree.github.io/doc/AliSim) for simulatin
58
57
 
59
58
  ## 🚀 Quick Start
60
59
 
61
- Once you have installed Phylogenie, check out the [examples](https://github.com/gabriele-marino/phylogenie/tree/main/examples) folder.
60
+ Once you have installed Phylogenie, check out the [tutorials](https://github.com/gabriele-marino/phylogenie/tree/main/tutorials) folder.
62
61
  It includes a collection of thoroughly commented configuration files, organized as a step-by-step tutorial. These examples will help you understand how to use Phylogenie in practice and can be easily adapted to fit your own workflow.
63
62
 
64
63
  For quick start, pick your favorite config file and run Phylogenie with:
65
64
  ```bash
66
- phylogenie examples/config_file.yaml
65
+ phylogenie tutorials/config_file.yaml
67
66
  ```
68
67
  This command will create the output dataset in the folder specified inside the configuration file, including data directories and metadata files for each dataset split defined in the config.
69
68
 
70
69
  >❗ *Tip*: Can’t choose just one config file?
71
- You can run them all at once by pointing Phylogenie to the folder! Just use: `phylogenie examples`. In this mode, Phylogenie will automatically find all `.yaml` files in the folder you specified and run for each of them!
70
+ You can run them all at once by pointing Phylogenie to the folder! Just use: `phylogenie tutorials`. In this mode, Phylogenie will automatically find all `.yaml` files in the folder you specified and run for each of them!
72
71
 
73
72
  ## 📖 Documentation
74
73
 
75
- - The [examples](https://github.com/gabriele-marino/phylogenie/tree/main/examples) folder contains many ready-to-use, extensively commented configuration files that serve as a step-by-step tutorial to guide you through using Phylogenie. You can explore them to learn how it works or adapt them directly to your own workflows.
74
+ - The [tutorials](https://github.com/gabriele-marino/phylogenie/tree/main/tutorials) folder contains many ready-to-use, extensively commented configuration files that serve as a step-by-step tutorial to guide you through using Phylogenie. You can explore them to learn how it works or adapt them directly to your own workflows.
76
75
  - A complete user guide and API reference are under development. In the meantime, feel free to [reach out](mailto:gabmarino.8601@email.com) if you have any questions about integrating Phylogenie into your workflows.
77
76
 
78
77
  ## 📄 License
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "phylogenie"
3
- version = "3.1.7"
3
+ version = "3.1.18"
4
4
  description = "Generate phylogenetic datasets with minimal setup effort"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -29,4 +29,4 @@ phylogenie = "phylogenie.main:main"
29
29
 
30
30
  [build-system]
31
31
  requires = ["setuptools>=42"]
32
- build-backend = "setuptools.build_meta"
32
+ build-backend = "setuptools.build_meta"
@@ -1,11 +1,3 @@
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
- )
9
1
  from phylogenie.generators import (
10
2
  AliSimDatasetGenerator,
11
3
  BDEITreeDatasetGenerator,
@@ -46,6 +38,7 @@ from phylogenie.treesimulator import (
46
38
  Tree,
47
39
  compute_mean_leaf_pairwise_distance,
48
40
  compute_sackin_index,
41
+ count_hops,
49
42
  dump_newick,
50
43
  generate_trees,
51
44
  get_BD_events,
@@ -58,12 +51,15 @@ from phylogenie.treesimulator import (
58
51
  get_FBD_events,
59
52
  get_mrca,
60
53
  get_mutation_id,
54
+ get_node_ages,
61
55
  get_node_depth_levels,
62
56
  get_node_depths,
63
57
  get_node_height_levels,
64
58
  get_node_heights,
65
59
  get_node_leaf_counts,
66
60
  get_node_state,
61
+ get_node_times,
62
+ get_path,
67
63
  load_newick,
68
64
  load_nexus,
69
65
  set_features,
@@ -71,12 +67,6 @@ from phylogenie.treesimulator import (
71
67
  )
72
68
 
73
69
  __all__ = [
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",
79
- "draw_tree",
80
70
  "AliSimDatasetGenerator",
81
71
  "BDEITreeDatasetGenerator",
82
72
  "BDSSTreeDatasetGenerator",
@@ -114,6 +104,7 @@ __all__ = [
114
104
  "Tree",
115
105
  "compute_mean_leaf_pairwise_distance",
116
106
  "compute_sackin_index",
107
+ "count_hops",
117
108
  "dump_newick",
118
109
  "generate_trees",
119
110
  "get_BD_events",
@@ -126,12 +117,15 @@ __all__ = [
126
117
  "get_FBD_events",
127
118
  "get_mrca",
128
119
  "get_mutation_id",
120
+ "get_node_ages",
129
121
  "get_node_depth_levels",
130
122
  "get_node_depths",
131
123
  "get_node_height_levels",
132
124
  "get_node_heights",
133
125
  "get_node_leaf_counts",
134
126
  "get_node_state",
127
+ "get_node_times",
128
+ "get_path",
135
129
  "load_newick",
136
130
  "load_nexus",
137
131
  "set_features",
@@ -1,5 +1,6 @@
1
+ import datetime
2
+ from collections.abc import Mapping
1
3
  from dataclasses import dataclass
2
- from datetime import datetime
3
4
  from typing import Any, Literal, overload
4
5
 
5
6
  import matplotlib.colors as mcolors
@@ -10,13 +11,18 @@ from matplotlib.axes import Axes
10
11
  from matplotlib.colors import Colormap
11
12
  from mpl_toolkits.axes_grid1.inset_locator import inset_axes # pyright: ignore
12
13
 
13
- from phylogenie.treesimulator import Tree, get_node_depth_levels, get_node_depths
14
+ from phylogenie.treesimulator import (
15
+ Tree,
16
+ get_node_ages,
17
+ get_node_depth_levels,
18
+ get_node_depths,
19
+ )
14
20
 
15
21
 
16
22
  @dataclass
17
23
  class CalibrationNode:
18
24
  node: Tree
19
- date: datetime
25
+ date: datetime.date
20
26
 
21
27
 
22
28
  Color = str | tuple[float, float, float] | tuple[float, float, float, float]
@@ -25,8 +31,10 @@ Color = str | tuple[float, float, float] | tuple[float, float, float, float]
25
31
  def draw_tree(
26
32
  tree: Tree,
27
33
  ax: Axes | None = None,
28
- colors: Color | dict[Tree, Color] = "black",
34
+ colors: Color | Mapping[Tree, Color] = "black",
29
35
  backward_time: bool = False,
36
+ branch_kwargs: dict[str, Any] | None = None,
37
+ sampled_ancestor_kwargs: dict[str, Any] | None = None,
30
38
  ) -> Axes:
31
39
  """
32
40
  Draw a phylogenetic tree with colored branches.
@@ -37,10 +45,14 @@ def draw_tree(
37
45
  The phylogenetic tree to draw.
38
46
  ax : Axes | None, optional
39
47
  The matplotlib Axes to draw on. If None, uses the current Axes.
40
- colors : Color | dict[Tree, Color], optional
41
- A single color for all branches or a dictionary mapping each node to a color.
48
+ colors : Color | Mapping[Tree, Color], optional
49
+ A single color for all branches or a mapping from each node to a color.
42
50
  backward_time : bool, optional
43
51
  If True, the x-axis is inverted to represent time going backward.
52
+ branch_kwargs : dict[str, Any] | None, optional
53
+ Additional keyword arguments to pass to the branch drawing functions.
54
+ sampled_ancestor_kwargs : dict[str, Any] | None, optional
55
+ Additional keyword arguments to highlight sampled ancestors.
44
56
 
45
57
  Returns
46
58
  -------
@@ -49,33 +61,51 @@ def draw_tree(
49
61
  """
50
62
  if ax is None:
51
63
  ax = plt.gca()
52
-
53
- if not isinstance(colors, dict):
64
+ if branch_kwargs is None:
65
+ branch_kwargs = {}
66
+ if sampled_ancestor_kwargs is None:
67
+ sampled_ancestor_kwargs = {}
68
+ if "marker" not in sampled_ancestor_kwargs:
69
+ sampled_ancestor_kwargs["marker"] = "o"
70
+
71
+ if not isinstance(colors, Mapping):
54
72
  colors = {node: colors for node in tree}
55
73
 
56
74
  xs = (
57
- get_node_depth_levels(tree)
75
+ get_node_ages(tree)
76
+ if backward_time
77
+ else get_node_depth_levels(tree)
58
78
  if any(node.branch_length is None for node in tree.iter_descendants())
59
79
  else get_node_depths(tree)
60
80
  )
61
- if backward_time:
62
- max_x = max(xs.values())
63
- xs = {node: max_x - x for node, x in xs.items()}
64
81
 
65
- ys: dict[Tree, float] = {node: i for i, node in enumerate(tree.get_leaves())}
82
+ leaves = tree.get_leaves()
83
+ ys: dict[Tree, float] = {
84
+ node: i
85
+ for i, node in enumerate(leaves)
86
+ if node.parent is None or node.branch_length != 0
87
+ }
66
88
  for node in tree.postorder_traversal():
67
89
  if node.is_internal():
68
- ys[node] = sum(ys[child] for child in node.children) / len(node.children)
90
+ children = [child for child in node.children if child.branch_length != 0]
91
+ ys[node] = sum(ys[child] for child in children) / len(children)
92
+ for leaf in leaves:
93
+ if leaf.parent is not None and leaf.branch_length == 0:
94
+ ys[leaf] = ys[leaf.parent]
69
95
 
70
96
  if tree.branch_length is not None:
71
97
  xmin = xs[tree] + tree.branch_length if backward_time else 0
72
- ax.hlines(y=ys[tree], xmin=xmin, xmax=xs[tree], color=colors[tree]) # pyright: ignore
98
+ ax.hlines( # pyright: ignore
99
+ y=ys[tree], xmin=xmin, xmax=xs[tree], color=colors[tree], **branch_kwargs
100
+ )
73
101
  for node in tree:
74
102
  x1, y1 = xs[node], ys[node]
103
+ if node.parent is not None and node.branch_length == 0:
104
+ ax.plot(x1, y1, color=colors[node], **sampled_ancestor_kwargs) # pyright: ignore
75
105
  for child in node.children:
76
106
  x2, y2 = xs[child], ys[child]
77
- ax.hlines(y=y2, xmin=x1, xmax=x2, color=colors[child]) # pyright: ignore
78
- ax.vlines(x=x1, ymin=y1, ymax=y2, color=colors[child]) # pyright: ignore
107
+ ax.hlines(y=y2, xmin=x1, xmax=x2, color=colors[child], **branch_kwargs) # pyright: ignore
108
+ ax.vlines(x=x1, ymin=y1, ymax=y2, color=colors[child], **branch_kwargs) # pyright: ignore
79
109
 
80
110
  if backward_time:
81
111
  ax.invert_xaxis()
@@ -86,7 +116,7 @@ def draw_tree(
86
116
 
87
117
  def _depth_to_date(
88
118
  depth: float, calibration_nodes: tuple[CalibrationNode, CalibrationNode]
89
- ) -> datetime:
119
+ ) -> datetime.date:
90
120
  """
91
121
  Convert a depth value to a date using linear interpolation between two calibration nodes.
92
122
 
@@ -99,7 +129,7 @@ def _depth_to_date(
99
129
 
100
130
  Returns
101
131
  -------
102
- datetime
132
+ datetime.date
103
133
  The interpolated date corresponding to the given depth.
104
134
  """
105
135
  node1, node2 = calibration_nodes
@@ -112,7 +142,8 @@ def draw_dated_tree(
112
142
  tree: Tree,
113
143
  calibration_nodes: tuple[CalibrationNode, CalibrationNode],
114
144
  ax: Axes | None = None,
115
- colors: Color | dict[Tree, Color] = "black",
145
+ colors: Color | Mapping[Tree, Color] = "black",
146
+ branch_kwargs: dict[str, Any] | None = None,
116
147
  ) -> Axes:
117
148
  """
118
149
  Draw a phylogenetic tree with branches positioned according to calibrated dates.
@@ -125,8 +156,10 @@ def draw_dated_tree(
125
156
  Two calibration nodes defining the mapping from depth to date.
126
157
  ax : Axes | None, optional
127
158
  The matplotlib Axes to draw on. If None, uses the current Axes.
128
- colors : Color | dict[Tree, Color], optional
129
- A single color for all branches or a dictionary mapping each node to a color.
159
+ colors : Color | Mapping[Tree, Color], optional
160
+ A single color for all branches or a mapping from each node to a color.
161
+ branch_kwargs : dict[str, Any] | None, optional
162
+ Additional keyword arguments to pass to the branch drawing functions.
130
163
 
131
164
  Returns
132
165
  -------
@@ -135,8 +168,10 @@ def draw_dated_tree(
135
168
  """
136
169
  if ax is None:
137
170
  ax = plt.gca()
171
+ if branch_kwargs is None:
172
+ branch_kwargs = {}
138
173
 
139
- if not isinstance(colors, dict):
174
+ if not isinstance(colors, Mapping):
140
175
  colors = {node: colors for node in tree}
141
176
 
142
177
  xs = {
@@ -156,6 +191,7 @@ def draw_dated_tree(
156
191
  xmin=mdates.date2num(origin_date), # pyright: ignore
157
192
  xmax=mdates.date2num(xs[tree]), # pyright: ignore
158
193
  color=colors[tree],
194
+ **branch_kwargs,
159
195
  )
160
196
  for node in tree:
161
197
  x1, y1 = xs[node], ys[node]
@@ -166,8 +202,15 @@ def draw_dated_tree(
166
202
  xmin=mdates.date2num(x1), # pyright: ignore
167
203
  xmax=mdates.date2num(x2), # pyright: ignore
168
204
  color=colors[child],
205
+ **branch_kwargs,
206
+ )
207
+ ax.vlines( # pyright: ignore
208
+ x=mdates.date2num(x1), # pyright: ignore
209
+ ymin=y1,
210
+ ymax=y2,
211
+ color=colors[child],
212
+ **branch_kwargs,
169
213
  )
170
- ax.vlines(x=mdates.date2num(x1), ymin=y1, ymax=y2, color=colors[child]) # pyright: ignore
171
214
 
172
215
  ax.xaxis.set_major_locator(mdates.AutoDateLocator())
173
216
  ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m-%d"))
@@ -182,9 +225,9 @@ def _init_colored_tree_categorical(
182
225
  color_by: str,
183
226
  ax: Axes | None = None,
184
227
  default_color: Color = "black",
185
- colormap: str | Colormap = "tab20",
228
+ colormap: str | Mapping[str, Color] | Colormap = "tab20",
186
229
  show_legend: bool = True,
187
- labels: dict[Any, str] | None = None,
230
+ labels: Mapping[Any, str] | None = None,
188
231
  legend_kwargs: dict[str, Any] | None = None,
189
232
  ) -> tuple[Axes, dict[Tree, Color]]:
190
233
  """
@@ -200,11 +243,14 @@ def _init_colored_tree_categorical(
200
243
  The matplotlib Axes to draw on. If None, uses the current Axes.
201
244
  default_color : Color, optional
202
245
  The color to use for nodes without the specified metadata.
203
- colormap : str | Colormap, optional
204
- The colormap to use for coloring categories. Defaults to 'tab20'.
246
+ colormap : str | Mapping[str, Color] | Colormap, optional
247
+ The colormap to use for coloring categories.
248
+ If a string, it is used to get a matplotlib colormap.
249
+ If a mapping, it maps category values to colors directly.
250
+ Defaults to 'tab20'.
205
251
  show_legend : bool, optional
206
252
  Whether to display a legend for the categories.
207
- labels : dict[Any, str] | None, optional
253
+ labels : Mapping[Any, str] | None, optional
208
254
  A mapping from category values to labels for the legend.
209
255
  legend_kwargs : dict[str, Any] | None, optional
210
256
  Additional keyword arguments to pass to the legend.
@@ -217,13 +263,16 @@ def _init_colored_tree_categorical(
217
263
  if ax is None:
218
264
  ax = plt.gca()
219
265
 
266
+ features = {node: node[color_by] for node in tree if color_by in node.metadata}
220
267
  if isinstance(colormap, str):
221
268
  colormap = plt.get_cmap(colormap)
269
+ if isinstance(colormap, Colormap):
270
+ feature_colors = {
271
+ f: mcolors.to_hex(colormap(i)) for i, f in enumerate(set(features.values()))
272
+ }
273
+ else:
274
+ feature_colors = colormap
222
275
 
223
- features = {node: node[color_by] for node in tree if color_by in node.metadata}
224
- feature_colors = {
225
- f: mcolors.to_hex(colormap(i)) for i, f in enumerate(set(features.values()))
226
- }
227
276
  colors = {
228
277
  node: feature_colors[features[node]] if node in features else default_color
229
278
  for node in tree
@@ -252,10 +301,12 @@ def draw_colored_tree_categorical(
252
301
  ax: Axes | None = None,
253
302
  backward_time: bool = False,
254
303
  default_color: Color = "black",
255
- colormap: str | Colormap = "tab20",
304
+ colormap: str | Mapping[str, Color] | Colormap = "tab20",
256
305
  show_legend: bool = True,
257
- labels: dict[Any, str] | None = None,
306
+ labels: Mapping[Any, str] | None = None,
258
307
  legend_kwargs: dict[str, Any] | None = None,
308
+ branch_kwargs: dict[str, Any] | None = None,
309
+ sampled_ancestor_kwargs: dict[str, Any] | None = None,
259
310
  ):
260
311
  """
261
312
  Draw a phylogenetic tree with branches colored based on categorical metadata.
@@ -272,14 +323,21 @@ def draw_colored_tree_categorical(
272
323
  If True, the x-axis is inverted to represent time going backward.
273
324
  default_color : Color, optional
274
325
  The color to use for nodes without the specified metadata.
275
- colormap : str | Colormap, optional
276
- The colormap to use for coloring categories. Defaults to 'tab20'.
326
+ colormap : str | Mapping[str, Color] | Colormap, optional
327
+ The colormap to use for coloring categories.
328
+ If a string, it is used to get a matplotlib colormap.
329
+ If a mapping, it maps category values to colors directly.
330
+ Defaults to 'tab20'.
277
331
  show_legend : bool, optional
278
332
  Whether to display a legend for the categories.
279
- labels : dict[Any, str] | None, optional
333
+ labels : Mapping[Any, str] | None, optional
280
334
  A mapping from category values to labels for the legend.
281
335
  legend_kwargs : dict[str, Any] | None, optional
282
336
  Additional keyword arguments to pass to the legend.
337
+ branch_kwargs : dict[str, Any] | None, optional
338
+ Additional keyword arguments to pass to the branch drawing functions.
339
+ sampled_ancestor_kwargs : dict[str, Any] | None, optional
340
+ Additional keyword arguments to highlight sampled ancestors.
283
341
 
284
342
  Returns
285
343
  -------
@@ -296,7 +354,14 @@ def draw_colored_tree_categorical(
296
354
  labels=labels,
297
355
  legend_kwargs=legend_kwargs,
298
356
  )
299
- return draw_tree(tree=tree, ax=ax, colors=colors, backward_time=backward_time)
357
+ return draw_tree(
358
+ tree=tree,
359
+ ax=ax,
360
+ colors=colors,
361
+ backward_time=backward_time,
362
+ branch_kwargs=branch_kwargs,
363
+ sampled_ancestor_kwargs=sampled_ancestor_kwargs,
364
+ )
300
365
 
301
366
 
302
367
  def draw_colored_dated_tree_categorical(
@@ -305,10 +370,11 @@ def draw_colored_dated_tree_categorical(
305
370
  color_by: str,
306
371
  ax: Axes | None = None,
307
372
  default_color: Color = "black",
308
- colormap: str | Colormap = "tab20",
373
+ colormap: str | Mapping[str, Color] | Colormap = "tab20",
309
374
  show_legend: bool = True,
310
- labels: dict[Any, str] | None = None,
375
+ labels: Mapping[Any, str] | None = None,
311
376
  legend_kwargs: dict[str, Any] | None = None,
377
+ branch_kwargs: dict[str, Any] | None = None,
312
378
  ) -> Axes:
313
379
  """
314
380
  Draw a dated phylogenetic tree with branches colored based on categorical metadata.
@@ -325,14 +391,19 @@ def draw_colored_dated_tree_categorical(
325
391
  The matplotlib Axes to draw on. If None, uses the current Axes.
326
392
  default_color : Color, optional
327
393
  The color to use for nodes without the specified metadata.
328
- colormap : str | Colormap, optional
329
- The colormap to use for coloring categories. Defaults to 'tab20'.
394
+ colormap : str | Mapping[str, Color] | Colormap, optional
395
+ The colormap to use for coloring categories.
396
+ If a string, it is used to get a matplotlib colormap.
397
+ If a mapping, it maps category values to colors directly.
398
+ Defaults to 'tab20'.
330
399
  show_legend : bool, optional
331
400
  Whether to display a legend for the categories.
332
- labels : dict[Any, str] | None, optional
401
+ labels : Mapping[Any, str] | None, optional
333
402
  A mapping from category values to labels for the legend.
334
403
  legend_kwargs : dict[str, Any] | None, optional
335
404
  Additional keyword arguments to pass to the legend.
405
+ branch_kwargs : dict[str, Any] | None, optional
406
+ Additional keyword arguments to pass to the branch drawing functions.
336
407
 
337
408
  Returns
338
409
  -------
@@ -350,7 +421,11 @@ def draw_colored_dated_tree_categorical(
350
421
  legend_kwargs=legend_kwargs,
351
422
  )
352
423
  return draw_dated_tree(
353
- tree=tree, calibration_nodes=calibration_nodes, ax=ax, colors=colors
424
+ tree=tree,
425
+ calibration_nodes=calibration_nodes,
426
+ ax=ax,
427
+ colors=colors,
428
+ branch_kwargs=branch_kwargs,
354
429
  )
355
430
 
356
431
 
@@ -378,7 +453,7 @@ def _init_colored_tree_continuous(
378
453
  vmin: float | None = ...,
379
454
  vmax: float | None = ...,
380
455
  *,
381
- show_hist: Literal[True],
456
+ show_hist: Literal[True] = True,
382
457
  hist_kwargs: dict[str, Any] | None = ...,
383
458
  hist_axes_kwargs: dict[str, Any] | None = ...,
384
459
  ) -> tuple[Axes, dict[Tree, Color], Axes]: ...
@@ -390,7 +465,6 @@ def _init_colored_tree_continuous(
390
465
  colormap: str | Colormap = "viridis",
391
466
  vmin: float | None = None,
392
467
  vmax: float | None = None,
393
- *,
394
468
  show_hist: bool = True,
395
469
  hist_kwargs: dict[str, Any] | None = None,
396
470
  hist_axes_kwargs: dict[str, Any] | None = None,
@@ -474,6 +548,8 @@ def draw_colored_tree_continuous(
474
548
  colormap: str | Colormap = "viridis",
475
549
  vmin: float | None = None,
476
550
  vmax: float | None = None,
551
+ branch_kwargs: dict[str, Any] | None = None,
552
+ sampled_ancestor_kwargs: dict[str, Any] | None = None,
477
553
  *,
478
554
  show_hist: Literal[False],
479
555
  hist_kwargs: dict[str, Any] | None = None,
@@ -489,8 +565,9 @@ def draw_colored_tree_continuous(
489
565
  colormap: str | Colormap = "viridis",
490
566
  vmin: float | None = None,
491
567
  vmax: float | None = None,
492
- *,
493
- show_hist: Literal[True],
568
+ branch_kwargs: dict[str, Any] | None = None,
569
+ sampled_ancestor_kwargs: dict[str, Any] | None = None,
570
+ show_hist: Literal[True] = True,
494
571
  hist_kwargs: dict[str, Any] | None = None,
495
572
  hist_axes_kwargs: dict[str, Any] | None = None,
496
573
  ) -> tuple[Axes, Axes]: ...
@@ -503,6 +580,8 @@ def draw_colored_tree_continuous(
503
580
  colormap: str | Colormap = "viridis",
504
581
  vmin: float | None = None,
505
582
  vmax: float | None = None,
583
+ branch_kwargs: dict[str, Any] | None = None,
584
+ sampled_ancestor_kwargs: dict[str, Any] | None = None,
506
585
  show_hist: bool = True,
507
586
  hist_kwargs: dict[str, Any] | None = None,
508
587
  hist_axes_kwargs: dict[str, Any] | None = None,
@@ -528,6 +607,10 @@ def draw_colored_tree_continuous(
528
607
  The minimum value for normalization. If None, uses the minimum of the data.
529
608
  vmax : float | None, optional
530
609
  The maximum value for normalization. If None, uses the maximum of the data.
610
+ branch_kwargs : dict[str, Any] | None, optional
611
+ Additional keyword arguments to pass to the branch drawing functions.
612
+ sampled_ancestor_kwargs : dict[str, Any] | None, optional
613
+ Additional keyword arguments to highlight sampled ancestors.
531
614
  show_hist : bool, optional
532
615
  Whether to display a histogram of the continuous values.
533
616
  hist_kwargs : dict[str, Any] | None, optional
@@ -555,7 +638,12 @@ def draw_colored_tree_continuous(
555
638
  hist_axes_kwargs=hist_axes_kwargs,
556
639
  )
557
640
  return draw_tree(
558
- tree=tree, ax=ax, colors=colors, backward_time=backward_time
641
+ tree=tree,
642
+ ax=ax,
643
+ colors=colors,
644
+ backward_time=backward_time,
645
+ branch_kwargs=branch_kwargs,
646
+ sampled_ancestor_kwargs=sampled_ancestor_kwargs,
559
647
  ), hist_ax
560
648
 
561
649
  ax, colors = _init_colored_tree_continuous(
@@ -570,7 +658,14 @@ def draw_colored_tree_continuous(
570
658
  hist_kwargs=hist_kwargs,
571
659
  hist_axes_kwargs=hist_axes_kwargs,
572
660
  )
573
- return draw_tree(tree=tree, ax=ax, colors=colors, backward_time=backward_time)
661
+ return draw_tree(
662
+ tree=tree,
663
+ ax=ax,
664
+ colors=colors,
665
+ backward_time=backward_time,
666
+ branch_kwargs=branch_kwargs,
667
+ sampled_ancestor_kwargs=sampled_ancestor_kwargs,
668
+ )
574
669
 
575
670
 
576
671
  @overload
@@ -583,6 +678,7 @@ def draw_colored_dated_tree_continuous(
583
678
  colormap: str | Colormap = "viridis",
584
679
  vmin: float | None = None,
585
680
  vmax: float | None = None,
681
+ branch_kwargs: dict[str, Any] | None = None,
586
682
  *,
587
683
  show_hist: Literal[False],
588
684
  hist_kwargs: dict[str, Any] | None = None,
@@ -598,8 +694,8 @@ def draw_colored_dated_tree_continuous(
598
694
  colormap: str | Colormap = "viridis",
599
695
  vmin: float | None = None,
600
696
  vmax: float | None = None,
601
- *,
602
- show_hist: Literal[True],
697
+ branch_kwargs: dict[str, Any] | None = None,
698
+ show_hist: Literal[True] = True,
603
699
  hist_kwargs: dict[str, Any] | None = None,
604
700
  hist_axes_kwargs: dict[str, Any] | None = None,
605
701
  ) -> tuple[Axes, Axes]: ...
@@ -612,6 +708,7 @@ def draw_colored_dated_tree_continuous(
612
708
  colormap: str | Colormap = "viridis",
613
709
  vmin: float | None = None,
614
710
  vmax: float | None = None,
711
+ branch_kwargs: dict[str, Any] | None = None,
615
712
  show_hist: bool = True,
616
713
  hist_kwargs: dict[str, Any] | None = None,
617
714
  hist_axes_kwargs: dict[str, Any] | None = None,
@@ -637,6 +734,8 @@ def draw_colored_dated_tree_continuous(
637
734
  The minimum value for normalization. If None, uses the minimum of the data.
638
735
  vmax : float | None, optional
639
736
  The maximum value for normalization. If None, uses the maximum of the data.
737
+ branch_kwargs : dict[str, Any] | None, optional
738
+ Additional keyword arguments to pass to the branch drawing functions.
640
739
  show_hist : bool, optional
641
740
  Whether to display a histogram of the continuous values.
642
741
  hist_kwargs : dict[str, Any] | None, optional
@@ -668,6 +767,7 @@ def draw_colored_dated_tree_continuous(
668
767
  calibration_nodes=calibration_nodes,
669
768
  ax=ax,
670
769
  colors=colors,
770
+ branch_kwargs=branch_kwargs,
671
771
  ), hist_ax
672
772
 
673
773
  ax, colors = _init_colored_tree_continuous(
@@ -687,4 +787,5 @@ def draw_colored_dated_tree_continuous(
687
787
  calibration_nodes=calibration_nodes,
688
788
  ax=ax,
689
789
  colors=colors,
790
+ branch_kwargs=branch_kwargs,
690
791
  )
@@ -19,18 +19,22 @@ from phylogenie.skyline import (
19
19
  from phylogenie.treesimulator import EventType, Mutation
20
20
 
21
21
 
22
- def _eval_expression(expression: str, data: dict[str, Any]) -> Any:
22
+ def eval_expression(
23
+ expression: str, data: dict[str, Any], extra_globals: dict[str, Any] | None = None
24
+ ) -> Any:
25
+ if extra_globals is None:
26
+ extra_globals = {}
23
27
  return np.array(
24
28
  eval(
25
29
  expression,
26
- {"np": np, **{k: np.array(v) for k, v in data.items()}},
30
+ {"np": np, **{k: np.array(v) for k, v in data.items()}, **extra_globals},
27
31
  )
28
32
  ).tolist()
29
33
 
30
34
 
31
35
  def integer(x: cfg.Integer, data: dict[str, Any]) -> int:
32
36
  if isinstance(x, str):
33
- e = _eval_expression(x, data)
37
+ e = eval_expression(x, data)
34
38
  if isinstance(e, int):
35
39
  return e
36
40
  raise ValueError(
@@ -41,7 +45,7 @@ def integer(x: cfg.Integer, data: dict[str, Any]) -> int:
41
45
 
42
46
  def scalar(x: cfg.Scalar, data: dict[str, Any]) -> pgt.Scalar:
43
47
  if isinstance(x, str):
44
- e = _eval_expression(x, data)
48
+ e = eval_expression(x, data)
45
49
  if isinstance(e, pgt.Scalar):
46
50
  return e
47
51
  raise ValueError(
@@ -54,13 +58,13 @@ def string(s: Any, data: dict[str, Any]) -> str:
54
58
  if not isinstance(s, str):
55
59
  return str(s)
56
60
  return re.sub(
57
- r"\{([^{}]+)\}", lambda match: str(_eval_expression(match.group(1), data)), s
61
+ r"\{([^{}]+)\}", lambda match: str(eval_expression(match.group(1), data)), s
58
62
  ) # Match content inside curly braces
59
63
 
60
64
 
61
65
  def many_scalars(x: cfg.ManyScalars, data: dict[str, Any]) -> pgt.ManyScalars:
62
66
  if isinstance(x, str):
63
- e = _eval_expression(x, data)
67
+ e = eval_expression(x, data)
64
68
  if tg.is_many_scalars(e):
65
69
  return e
66
70
  raise ValueError(
@@ -73,7 +77,7 @@ def one_or_many_scalars(
73
77
  x: cfg.OneOrManyScalars, data: dict[str, Any]
74
78
  ) -> pgt.OneOrManyScalars:
75
79
  if isinstance(x, str):
76
- e = _eval_expression(x, data)
80
+ e = eval_expression(x, data)
77
81
  if tg.is_one_or_many_scalars(e):
78
82
  return e
79
83
  raise ValueError(
@@ -99,7 +103,7 @@ def skyline_vector(
99
103
  x: cfg.SkylineVector, data: dict[str, Any]
100
104
  ) -> SkylineVectorCoercible:
101
105
  if isinstance(x, str):
102
- e = _eval_expression(x, data)
106
+ e = eval_expression(x, data)
103
107
  if tg.is_one_or_many_scalars(e):
104
108
  return e
105
109
  raise ValueError(
@@ -114,7 +118,7 @@ def skyline_vector(
114
118
 
115
119
  change_times = many_scalars(x.change_times, data)
116
120
  if isinstance(x.value, str):
117
- e = _eval_expression(x.value, data)
121
+ e = eval_expression(x.value, data)
118
122
  if tg.is_many_one_or_many_scalars(e):
119
123
  value = e
120
124
  else:
@@ -142,7 +146,7 @@ def one_or_many_2D_scalars(
142
146
  x: cfg.OneOrMany2DScalars, data: dict[str, Any]
143
147
  ) -> pgt.OneOrMany2DScalars:
144
148
  if isinstance(x, str):
145
- e = _eval_expression(x, data)
149
+ e = eval_expression(x, data)
146
150
  if tg.is_one_or_many_2D_scalars(e):
147
151
  return e
148
152
  raise ValueError(
@@ -160,7 +164,7 @@ def skyline_matrix(
160
164
  return None
161
165
 
162
166
  if isinstance(x, str):
163
- e = _eval_expression(x, data)
167
+ e = eval_expression(x, data)
164
168
  if tg.is_one_or_many_2D_scalars(e):
165
169
  return e
166
170
  raise ValueError(
@@ -175,7 +179,7 @@ def skyline_matrix(
175
179
 
176
180
  change_times = many_scalars(x.change_times, data)
177
181
  if isinstance(x.value, str):
178
- e = _eval_expression(x.value, data)
182
+ e = eval_expression(x.value, data)
179
183
  if tg.is_many_one_or_many_2D_scalars(e):
180
184
  value = e
181
185
  else:
@@ -213,7 +217,7 @@ def distribution(x: cfg.Distribution, data: dict[str, Any]) -> cfg.Distribution:
213
217
  args = x.args
214
218
  for arg_name, arg_value in args.items():
215
219
  if isinstance(arg_value, str):
216
- args[arg_name] = _eval_expression(arg_value, data)
220
+ args[arg_name] = eval_expression(arg_value, data)
217
221
  return cfg.Distribution(type=x.type, **args)
218
222
 
219
223
 
@@ -12,6 +12,7 @@ from phylogenie.generators.configs import Distribution
12
12
  from phylogenie.generators.dataset import DatasetGenerator, DataType
13
13
  from phylogenie.generators.factories import (
14
14
  data,
15
+ eval_expression,
15
16
  integer,
16
17
  mutations,
17
18
  scalar,
@@ -57,6 +58,7 @@ class TreeDatasetGenerator(DatasetGenerator):
57
58
  timeout: float = np.inf
58
59
  node_features: list[Feature] | None = None
59
60
  acceptance_criterion: str | None = None
61
+ logs: dict[str, str] | None = None
60
62
 
61
63
  @abstractmethod
62
64
  def _get_events(self, data: dict[str, Any]) -> list[Event]: ...
@@ -77,10 +79,21 @@ class TreeDatasetGenerator(DatasetGenerator):
77
79
  acceptance_criterion: None | Callable[[Tree], bool] = (
78
80
  None
79
81
  if self.acceptance_criterion is None
80
- else lambda tree: eval(
81
- self.acceptance_criterion, {}, {"tree": tree} # pyright: ignore
82
+ else lambda tree: eval_expression(
83
+ self.acceptance_criterion, # pyright: ignore
84
+ data,
85
+ {"tree": tree},
82
86
  )
83
87
  )
88
+ logs: None | dict[str, Callable[[Tree], Any]] = (
89
+ None
90
+ if self.logs is None
91
+ else {
92
+ key: lambda tree, expr=expr: eval_expression(expr, data, {"tree": tree})
93
+ for key, expr in self.logs.items()
94
+ }
95
+ )
96
+
84
97
  return simulate_tree(
85
98
  events=events,
86
99
  n_tips=None if self.n_tips is None else integer(self.n_tips, data),
@@ -92,6 +105,7 @@ class TreeDatasetGenerator(DatasetGenerator):
92
105
  seed=seed,
93
106
  timeout=self.timeout,
94
107
  acceptance_criterion=acceptance_criterion,
108
+ logs=logs,
95
109
  )
96
110
 
97
111
  def generate_one(
@@ -111,7 +125,7 @@ class TreeDatasetGenerator(DatasetGenerator):
111
125
  dump_newick(tree, f"{filename}.nwk")
112
126
  break
113
127
  except TimeoutError:
114
- print("Simulation timed out, retrying with different parameters...")
128
+ print("Simulation timed out. Retrying with different parameters...")
115
129
  return d | metadata
116
130
 
117
131
 
@@ -1,3 +1,4 @@
1
+ from datetime import date
1
2
  from pathlib import Path
2
3
  from typing import Callable
3
4
 
@@ -5,7 +6,8 @@ from phylogenie.msa import MSA, Sequence
5
6
 
6
7
 
7
8
  def load_fasta(
8
- fasta_file: str | Path, extract_time_from_id: Callable[[str], float] | None = None
9
+ fasta_file: str | Path,
10
+ extract_time_from_id: Callable[[str], float | date] | None = None,
9
11
  ) -> MSA:
10
12
  sequences: list[Sequence] = []
11
13
  with open(fasta_file, "r") as f:
@@ -17,10 +19,14 @@ def load_fasta(
17
19
  if extract_time_from_id is not None:
18
20
  time = extract_time_from_id(id)
19
21
  elif "|" in id:
22
+ last_metadata = id.split("|")[-1]
20
23
  try:
21
- time = float(id.split("|")[-1])
24
+ time = float(last_metadata)
22
25
  except ValueError:
23
- pass
26
+ try:
27
+ time = date.fromisoformat(last_metadata)
28
+ except ValueError:
29
+ pass
24
30
  chars = next(f).strip()
25
31
  sequences.append(Sequence(id, chars, time))
26
32
  return MSA(sequences)
@@ -1,5 +1,6 @@
1
- from collections.abc import Iterator
1
+ from collections.abc import Iterable, Iterator
2
2
  from dataclasses import dataclass
3
+ from datetime import date
3
4
 
4
5
  import numpy as np
5
6
 
@@ -8,25 +9,32 @@ import numpy as np
8
9
  class Sequence:
9
10
  id: str
10
11
  chars: str
11
- time: float | None = None
12
+ time: float | date | None = None
13
+
14
+ def __len__(self) -> int:
15
+ return len(self.chars)
12
16
 
13
17
 
14
18
  class MSA:
15
- def __init__(self, sequences: list[Sequence]):
16
- self.sequences = sequences
17
- lengths = {len(sequence.chars) for sequence in sequences}
19
+ def __init__(self, sequences: Iterable[Sequence]):
20
+ self._sequences = sequences
21
+ lengths = {len(sequence) for sequence in sequences}
18
22
  if len(lengths) > 1:
19
23
  raise ValueError(
20
24
  f"All sequences in the alignment must have the same length (got lengths: {lengths})"
21
25
  )
22
26
 
27
+ @property
28
+ def sequences(self) -> tuple[Sequence, ...]:
29
+ return tuple(self._sequences)
30
+
23
31
  @property
24
32
  def ids(self) -> list[str]:
25
33
  return [sequence.id for sequence in self.sequences]
26
34
 
27
35
  @property
28
- def times(self) -> list[float]:
29
- times: list[float] = []
36
+ def times(self) -> list[float | date]:
37
+ times: list[float | date] = []
30
38
  for sequence in self:
31
39
  if sequence.time is None:
32
40
  raise ValueError(f"Time is not set for sequence {sequence.id}.")
@@ -25,13 +25,17 @@ from phylogenie.treesimulator.tree import Tree
25
25
  from phylogenie.treesimulator.utils import (
26
26
  compute_mean_leaf_pairwise_distance,
27
27
  compute_sackin_index,
28
+ count_hops,
28
29
  get_distance,
29
30
  get_mrca,
31
+ get_node_ages,
30
32
  get_node_depth_levels,
31
33
  get_node_depths,
32
34
  get_node_height_levels,
33
35
  get_node_heights,
34
36
  get_node_leaf_counts,
37
+ get_node_times,
38
+ get_path,
35
39
  )
36
40
 
37
41
  __all__ = [
@@ -63,11 +67,15 @@ __all__ = [
63
67
  "Tree",
64
68
  "compute_mean_leaf_pairwise_distance",
65
69
  "compute_sackin_index",
70
+ "count_hops",
66
71
  "get_distance",
67
72
  "get_mrca",
73
+ "get_node_ages",
68
74
  "get_node_depth_levels",
69
75
  "get_node_depths",
70
76
  "get_node_height_levels",
71
77
  "get_node_heights",
72
78
  "get_node_leaf_counts",
79
+ "get_node_times",
80
+ "get_path",
73
81
  ]
@@ -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
 
@@ -25,6 +25,7 @@ def simulate_tree(
25
25
  seed: int | None = None,
26
26
  timeout: float = np.inf,
27
27
  acceptance_criterion: Callable[[Tree], bool] | None = None,
28
+ logs: dict[str, Callable[[Tree], Any]] | None = None,
28
29
  ) -> tuple[Tree, dict[str, Any]]:
29
30
  if (max_time != np.inf) == (n_tips is not None):
30
31
  raise ValueError("Exactly one of max_time or n_tips must be specified.")
@@ -85,7 +86,13 @@ def simulate_tree(
85
86
  model.sample(individual, current_time, True)
86
87
 
87
88
  tree = model.get_sampled_tree()
88
- if acceptance_criterion is None or acceptance_criterion(tree):
89
+
90
+ if acceptance_criterion is not None and not acceptance_criterion(tree):
91
+ continue
92
+
93
+ if logs is not None:
94
+ for key, func in logs.items():
95
+ metadata[key] = func(tree)
89
96
  return (tree, metadata)
90
97
 
91
98
 
@@ -102,6 +109,7 @@ def generate_trees(
102
109
  n_jobs: int = -1,
103
110
  timeout: float = np.inf,
104
111
  acceptance_criterion: Callable[[Tree], bool] | None = None,
112
+ logs: dict[str, Callable[[Tree], Any]] | None = None,
105
113
  ) -> pd.DataFrame:
106
114
  if isinstance(output_dir, str):
107
115
  output_dir = Path(output_dir)
@@ -121,6 +129,7 @@ def generate_trees(
121
129
  seed=seed,
122
130
  timeout=timeout,
123
131
  acceptance_criterion=acceptance_criterion,
132
+ logs=logs,
124
133
  )
125
134
  metadata["file_id"] = i
126
135
  if node_features is not None:
@@ -128,7 +137,7 @@ def generate_trees(
128
137
  dump_newick(tree, output_dir / f"{i}.nwk")
129
138
  return metadata
130
139
  except TimeoutError:
131
- print("Simulation timed out, retrying with a different seed...")
140
+ print("Simulation timed out. Retrying with a different seed...")
132
141
  seed += 1
133
142
 
134
143
  rng = default_rng(seed)
@@ -153,6 +153,16 @@ 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
  # -------------
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phylogenie
3
- Version: 3.1.7
3
+ Version: 3.1.18
4
4
  Summary: Generate phylogenetic datasets with minimal setup effort
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -23,7 +23,6 @@ Dynamic: license-file
23
23
  [![PyPI version](https://img.shields.io/pypi/v/phylogenie)](https://pypi.org/project/phylogenie/)
24
24
  ![Downloads](https://img.shields.io/pypi/dm/phylogenie)
25
25
 
26
-
27
26
  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!
28
27
 
29
28
  ## ✨ Features
@@ -73,21 +72,21 @@ Phylogenie relies on [AliSim](https://iqtree.github.io/doc/AliSim) for simulatin
73
72
 
74
73
  ## 🚀 Quick Start
75
74
 
76
- Once you have installed Phylogenie, check out the [examples](https://github.com/gabriele-marino/phylogenie/tree/main/examples) folder.
75
+ Once you have installed Phylogenie, check out the [tutorials](https://github.com/gabriele-marino/phylogenie/tree/main/tutorials) folder.
77
76
  It includes a collection of thoroughly commented configuration files, organized as a step-by-step tutorial. These examples will help you understand how to use Phylogenie in practice and can be easily adapted to fit your own workflow.
78
77
 
79
78
  For quick start, pick your favorite config file and run Phylogenie with:
80
79
  ```bash
81
- phylogenie examples/config_file.yaml
80
+ phylogenie tutorials/config_file.yaml
82
81
  ```
83
82
  This command will create the output dataset in the folder specified inside the configuration file, including data directories and metadata files for each dataset split defined in the config.
84
83
 
85
84
  >❗ *Tip*: Can’t choose just one config file?
86
- You can run them all at once by pointing Phylogenie to the folder! Just use: `phylogenie examples`. In this mode, Phylogenie will automatically find all `.yaml` files in the folder you specified and run for each of them!
85
+ You can run them all at once by pointing Phylogenie to the folder! Just use: `phylogenie tutorials`. In this mode, Phylogenie will automatically find all `.yaml` files in the folder you specified and run for each of them!
87
86
 
88
87
  ## 📖 Documentation
89
88
 
90
- - The [examples](https://github.com/gabriele-marino/phylogenie/tree/main/examples) folder contains many ready-to-use, extensively commented configuration files that serve as a step-by-step tutorial to guide you through using Phylogenie. You can explore them to learn how it works or adapt them directly to your own workflows.
89
+ - The [tutorials](https://github.com/gabriele-marino/phylogenie/tree/main/tutorials) folder contains many ready-to-use, extensively commented configuration files that serve as a step-by-step tutorial to guide you through using Phylogenie. You can explore them to learn how it works or adapt them directly to your own workflows.
91
90
  - A complete user guide and API reference are under development. In the meantime, feel free to [reach out](mailto:gabmarino.8601@email.com) if you have any questions about integrating Phylogenie into your workflows.
92
91
 
93
92
  ## 📄 License
File without changes
File without changes