pyham 2.0.0__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.
pyham/taxonomy.py ADDED
@@ -0,0 +1,349 @@
1
+ import logging
2
+ import warnings
3
+ import gzip
4
+ import re
5
+ import collections
6
+ from os import PathLike
7
+ from typing import Union, Optional
8
+ from xml.etree.ElementTree import XMLParser
9
+ from ete4 import Tree
10
+
11
+ from .genome import ExtantGenome, AncestralGenome, Genome
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def create_tree_from_newick(tree: Union[str, PathLike], tree_format: Optional[str] = None, use_internal_name: bool = False, quoted_node_names: bool = True):
17
+ if tree_format is None:
18
+ tree_format = 'newick_string' if isinstance(tree, str) and tree.strip().startswith('(') else 'newick'
19
+ if tree_format == 'newick_string':
20
+ # If tree is a string and not a path, treat it as a newick string
21
+ tree = tree.strip()
22
+ if not tree.endswith(';'):
23
+ tree += ';'
24
+ tree = Tree(tree, parser=1) #, quoted_node_names=quoted_node_names)
25
+ elif tree_format == 'newick':
26
+ # If tree is a path, read the newick file
27
+ tree = Tree(str(tree), parser=1) #, quoted_node_names=quoted_node_names)
28
+ else:
29
+ raise ValueError("tree_format must be 'newick' or 'newick_string'")
30
+
31
+ if not use_internal_name:
32
+ # Generate internal node names by concatenating children names
33
+ for node in tree.traverse("postorder"):
34
+ if not node.is_leaf:
35
+ node.name = '/'.join(child.name for child in node.get_children())
36
+
37
+ return tree
38
+
39
+
40
+ def create_tree_from_phyloxml(tree: Union[str, PathLike], phyloxml_leaf_name_tag='clade_name', phyloxml_internal_name_tag='clade_name', use_internal_name=True):
41
+ from .parsers import PhyloXMLToETE
42
+ factory = PhyloXMLToETE()
43
+ parser = XMLParser(target=factory)
44
+
45
+ open_ = gzip.open if str(tree).endswith('.gz') else open
46
+ with open_(tree, 'rt') as fh:
47
+ for line in fh:
48
+ parser.feed(line)
49
+
50
+ def set_leaf_name(node):
51
+ attr = phyloxml_leaf_name_tag.split('_', 1)[-1]
52
+ node.name = node.props.get(attr, None)
53
+ if node.name is None:
54
+ raise KeyError(f"Node {node} in the phyloxml file {tree} has no {phyloxml_leaf_name_tag} attribute to populate the species name")
55
+
56
+ def set_internal_name(node):
57
+ if not use_internal_name:
58
+ # Generate internal node names by concatenating children names
59
+ node.name = '/'.join(child.name for child in node.get_children())
60
+ else:
61
+ attr = phyloxml_internal_name_tag.split('_', 1)[-1]
62
+ node.name = node.props.get(attr, None)
63
+ if node.name is None:
64
+ raise KeyError(f"Node {node} in the phyloxml file {tree} has no {phyloxml_internal_name_tag} attribute to populate the species name")
65
+
66
+ for node in factory.root.traverse("postorder"):
67
+ # assign name to extant species
68
+ if node.is_leaf:
69
+ set_leaf_name(node)
70
+ else:
71
+ set_internal_name(node)
72
+
73
+ return factory.root
74
+
75
+
76
+ class Taxonomy(object):
77
+ """
78
+ Taxonomy is a class to wrap the ete4 tree used as reference species tree by Ham.
79
+
80
+ Attributes:
81
+ | tree_str (:obj:`str`): newick tree string used to build the ete4 Tree object.
82
+ | tree (:obj:`ete4 Tree`): species ete4 Tree tree.
83
+ | internal_nodes (:obj:`set`): Set of Tree node that contained a AncestralGenome.
84
+ | leaves (:obj:`set`): Set of Tree node that contained a ExtantGenome.
85
+
86
+ """
87
+ def __init__(self, tree, **kwargs):
88
+ """
89
+ Args:
90
+ | tree_file (:obj:`str`): Path to the file that contained the taxonomy information.
91
+ | tree_format (:obj:`str`): type of inputted tree file. Defaults to newick_string. Can be 'newick', 'phyloxml, 'newick_string'.
92
+ | use_internal_name (:obj:`Boolean`, optional): Specify wheter using the given internal node name or use the
93
+ | concatenatation of the children name. Defaults to False.
94
+ | quoted_node_names (:obj:'Boolean', optional): Specify whether newick file has quoted node names.
95
+ """
96
+ if not isinstance(tree, Tree):
97
+ # old style API
98
+ warnings.warn("Taxonomy should be constructed from Taxonomy.from_newick or Taxonomy.from_phyloxml", DeprecationWarning)
99
+ tree_format = kwargs.pop('tree_format', 'newick_string')
100
+ if tree_format in ('newick', 'newick_string'):
101
+ quoted_node_names = kwargs.pop('quoted_node_names', True)
102
+ use_internal_name = kwargs.get('use_internal_name', False)
103
+ self.tree = create_tree_from_newick(
104
+ tree, tree_format=tree_format, use_internal_name=use_internal_name, quoted_node_names=quoted_node_names
105
+ )
106
+ elif tree_format == 'phyloxml':
107
+ phyloxml_leaf_name_tag = kwargs.pop('phyloxml_leaf_name_tag', 'clade_name')
108
+ phyloxml_internal_name_tag = kwargs.pop('phyloxml_internal_name_tag', 'clade_name')
109
+ use_internal_name = kwargs.get('use_internal_name', True)
110
+ self.tree = create_tree_from_phyloxml(
111
+ tree,
112
+ phyloxml_leaf_name_tag=phyloxml_leaf_name_tag,
113
+ phyloxml_internal_name_tag=phyloxml_internal_name_tag,
114
+ use_internal_name=use_internal_name
115
+ )
116
+ else:
117
+ # new style API: tree is already an Tree object
118
+ self.tree = tree
119
+
120
+ # lookup table for attr, value pairs
121
+ self._node_lookup = {}
122
+
123
+ # check unicity of leaves name.
124
+ self._check_consistency_names()
125
+
126
+ # add depth to each node of the tree.
127
+ self._add_depth(self.tree.root, depth=0)
128
+
129
+ # tracker for Genome created.
130
+ self.internal_nodes = set()
131
+ self.leaves = set()
132
+
133
+ @classmethod
134
+ def from_newick(cls, tree: Union[str, PathLike], use_internal_name=False, quoted_node_names=True):
135
+ """ Create a Taxonomy object from a newick file or string.
136
+
137
+ Args:
138
+ | tree (:obj:`str`): Path to the file that contained the taxonomy information, or a newick string.
139
+ | use_internal_name (:obj:`Boolean`, optional): Specify whether using the given internal node name or use the
140
+ | concatenatation of the children name. Defaults to False.
141
+ | quoted_node_names (:obj:'Boolean', optional): Specify whether the newick tree has quoted node names. Defaults to True.
142
+
143
+ Returns:
144
+ obj:`Taxonomy`: Taxonomy object with the ete3 tree.
145
+ """
146
+ tree_inst = create_tree_from_newick(tree, quoted_node_names=quoted_node_names, use_internal_name=use_internal_name)
147
+ return cls(tree_inst)
148
+
149
+ @classmethod
150
+ def from_phyloxml(cls, tree: Union[str, PathLike], phyloxml_leaf_name_tag='clade_name', phyloxml_internal_name_tag='clade_name'):
151
+ tree_inst = create_tree_from_phyloxml(tree, phyloxml_leaf_name_tag=phyloxml_leaf_name_tag, phyloxml_internal_name_tag=phyloxml_internal_name_tag)
152
+ return cls(tree_inst)
153
+
154
+ @classmethod
155
+ def from_ete_tree(cls, tree: Tree):
156
+ return cls(tree)
157
+
158
+ @property
159
+ def tree_str(self):
160
+ return self.tree.write(parser=8, format_root_node=True)
161
+
162
+ def nodes_by_attr(self, **kwargs):
163
+ nodes = None
164
+ for key, val in kwargs.items():
165
+ if (key, val) not in self._node_lookup:
166
+ self._node_lookup[(key, val)] = set(self.tree.search_nodes(**{key: val}))
167
+ if nodes is None:
168
+ nodes = self._node_lookup[(key, val)].copy()
169
+ else:
170
+ nodes &= self._node_lookup[(key, val)]
171
+ yield from nodes
172
+
173
+ def get_node_by_name(self, name):
174
+ """Return the node with the given name.
175
+
176
+ Args:
177
+ | name (:obj:`str`): name of the node to search.
178
+
179
+ Returns:
180
+ :obj:`ete4.TreeNode`: node with the given name.
181
+ """
182
+ node = list(self.nodes_by_attr(name=name))
183
+ if not node:
184
+ raise KeyError("Node with name '{}' not found in the taxonomy".format(name))
185
+ if len(node) > 1:
186
+ raise KeyError("Multiple nodes with name '{}' found in the taxonomy".format(name))
187
+ return node[0]
188
+
189
+ def get_extant_taxa_by_name(self, name):
190
+ """Return the extant taxa with the given name.
191
+
192
+ Args:
193
+ | name (:obj:`str`): name of the node to search.
194
+
195
+ Returns:
196
+ :obj:`ete4.TreeNode`: node with the given name.
197
+ """
198
+ node = self.get_node_by_name(name=name)
199
+ if len(node.children) > 0:
200
+ cand = []
201
+ for child in node.children:
202
+ if len(child.name) == 5 and re.match(r'[A-Z][A-Z0-9]{4}', child.name) is not None:
203
+ cand.append(child)
204
+ if len(cand) == 1:
205
+ node = cand[0]
206
+ else:
207
+ raise KeyError(f"Node with name '{name}' is not an extant taxon, it has multiple children or no children.")
208
+ return node
209
+
210
+ def add_genome_to_node(self, node, genome):
211
+ """ add the given genome to the node attribute "genome".
212
+
213
+ Args:
214
+ | node (:obj:`node`): receptor node.
215
+ | genome (:obj:`Genome`): :obj:`Genome` to attach.
216
+
217
+ """
218
+ existing_genome = node.get_prop("genome", None)
219
+ if existing_genome is not None and existing_genome != genome:
220
+ raise ValueError("Node {} already has a genome attached: {}".format(node, existing_genome))
221
+
222
+ node.add_prop("genome", genome)
223
+ genome.set_taxon(node)
224
+
225
+ if isinstance(genome, ExtantGenome):
226
+ self.leaves.add(node)
227
+ elif isinstance(genome, AncestralGenome):
228
+ genome.name = node.name
229
+ self.internal_nodes.add(node)
230
+ else:
231
+ raise TypeError("expect class obj of '{}', got {}".format(type(Genome).__name__,type(genome).__name__))
232
+
233
+ def get_genome_from_taxnode(self, node):
234
+ """ return the genome attached to the node.
235
+
236
+ Args:
237
+ | node (:obj:`node`): node to get the genome from.
238
+
239
+ Returns:
240
+ :obj:`Genome`: genome attached to the node.
241
+ """
242
+ genome = node.get_prop("genome", None)
243
+ if genome is not None:
244
+ return genome
245
+
246
+ genome_cls = ExtantGenome if node.is_leaf else AncestralGenome
247
+ genome = genome_cls()
248
+ self.add_genome_to_node(node, genome)
249
+ return genome
250
+
251
+ def get_mrca_taxnode(self, *tax_nodes):
252
+ """ return the most recent common ancestor of the given tax nodes."""
253
+ return self.tree.common_ancestor(tax_nodes)
254
+
255
+ def get_path_up(self, lowest_node, ancestor_node):
256
+ """
257
+ Return the internal node in between two nodes sorted by recentness.
258
+ The query nodes are not included.
259
+
260
+ Args:
261
+ | lowest_node (:obj:`node`): Youngest node.
262
+ | ancestor_node (:obj:`node`): Oldest node.
263
+
264
+ Returns:
265
+ list of node sorted from most recent to oldest.
266
+
267
+ """
268
+ intermediate_level = []
269
+ for tax in lowest_node.ancestors():
270
+ if tax == ancestor_node:
271
+ break
272
+ intermediate_level.append(tax)
273
+ else:
274
+ raise ValueError(f"lowest_node ({lowest_node} is not a child of {ancestor_node}")
275
+ return intermediate_level
276
+
277
+ def is_child_recursive(self, node, ancestor_node):
278
+ """ Check if the node is a child of the ancestor node.
279
+
280
+ Args:
281
+ | node (:obj:`node`): node to check.
282
+ | ancestor_node (:obj:`node`): ancestor node.
283
+
284
+ Returns:
285
+ obj:`bool` True if the node is a child of the ancestor node.
286
+ """
287
+ for tax in node.ancestors():
288
+ if tax == ancestor_node:
289
+ return True
290
+ return False
291
+
292
+ def get_newick_from_tree(self, node):
293
+ """ return the newick tree string (format 8: all names) rooted at the given node.
294
+
295
+ Args:
296
+ node (:obj:`node`): root node.
297
+
298
+ Returns:
299
+ :obj:`str` of the subtree.
300
+ """
301
+ return node.write(parser=8, format_root_node=True)
302
+
303
+ def _check_consistency_names(self):
304
+
305
+ """
306
+ Check if leaves names and internal node names are uniques.
307
+
308
+ """
309
+
310
+ leaf_names = []
311
+ int_names = []
312
+
313
+ for node in self.tree.traverse():
314
+ if node.name is None and not node.is_root:
315
+ raise KeyError("{} node has no name attribute".format(node))
316
+ if node.is_leaf:
317
+ leaf_names.append(node.name)
318
+ else:
319
+ int_names.append(node.name)
320
+
321
+ self._node_lookup[('name', node.name)] = {node}
322
+ if 'id' in node.props:
323
+ self._node_lookup[('id', node.props['id'])] = {node}
324
+
325
+
326
+ # check for leave names
327
+ if len(leaf_names) != len(set(leaf_names)):
328
+ dupl = collections.Counter(leaf_names)
329
+ dupl = [k for k, c in dupl.items() if c > 1]
330
+ raise KeyError(f"Leaves names are not unique! The following leaves appear multiple times: {dupl}")
331
+
332
+ # Check for internal names
333
+ if len(int_names) != len(set(int_names)):
334
+ dupl = collections.Counter(int_names)
335
+ dupl = [k for k, c in dupl.items() if c > 1]
336
+ raise KeyError(f"Internal Names are not unique. The following internal names appear multiple times: {dupl}.")
337
+
338
+ def _add_depth(self, node:Tree, depth=0):
339
+ """
340
+ Recursive function to add depth to each node of a Etree.
341
+ """
342
+ node.add_prop("depth", depth)
343
+ for n in node.get_children():
344
+ self._add_depth(n, depth + 1)
345
+
346
+
347
+ def build_taxon_node(id, name=None, **kwargs):
348
+ node = Tree({"name": name, "id": int(id)})
349
+ return node
pyham/utils.py ADDED
@@ -0,0 +1,31 @@
1
+ from . import taxonomy as tax
2
+
3
+
4
+ def get_newick_string(source_path, type="nwk"):
5
+ '''
6
+ Get the newick tree as string from a newick file, a hdf5 file or from the note tag of an orthoXML
7
+
8
+ Args:
9
+ | newick_source: path of the file
10
+ | type: type of file where to fetch the newick tree. Available: "nwk", "h5" and "orthoXML"
11
+ '''
12
+
13
+ if type == "nwk":
14
+ with open(source_path, 'r') as nwk_file:
15
+ return nwk_file.read()
16
+
17
+ elif type == "phylip":
18
+ pass
19
+
20
+ elif type == "h5":
21
+ pass
22
+
23
+ elif type == 'orthoXML':
24
+ pass
25
+
26
+
27
+
28
+
29
+
30
+
31
+