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/TreeProfile.py +408 -0
- pyham/__init__.py +20 -0
- pyham/_version.py +24 -0
- pyham/abstractgene.py +726 -0
- pyham/genome.py +148 -0
- pyham/ham.py +984 -0
- pyham/id_formats.py +137 -0
- pyham/iham.py +364 -0
- pyham/mapper.py +414 -0
- pyham/parsers.py +959 -0
- pyham/taxonomy.py +349 -0
- pyham/utils.py +31 -0
- pyham/validation.py +914 -0
- pyham-2.0.0.dist-info/METADATA +200 -0
- pyham-2.0.0.dist-info/RECORD +19 -0
- pyham-2.0.0.dist-info/WHEEL +5 -0
- pyham-2.0.0.dist-info/entry_points.txt +2 -0
- pyham-2.0.0.dist-info/licenses/LICENSE.txt +19 -0
- pyham-2.0.0.dist-info/top_level.txt +1 -0
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
|
+
|