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/abstractgene.py
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
import numbers
|
|
2
|
+
import dataclasses
|
|
3
|
+
from ete4 import Tree
|
|
4
|
+
from .genome import ExtantGenome, AncestralGenome, Genome, EvolutionaryConceptError
|
|
5
|
+
from .iham import IHAM
|
|
6
|
+
import abc
|
|
7
|
+
|
|
8
|
+
class AbstractGene(metaclass=abc.ABCMeta):
|
|
9
|
+
"""
|
|
10
|
+
AbstractGene is an abstract class representing extant or ancestral genes. An AbstractGene is defined by an unique
|
|
11
|
+
id, the genome it belongs to and its parent AbstractGene.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
| parent (:obj:`pyham.abstractgene.HOG`): Direct parent HOG this gene is descendant to. For and only for top level HOG, parent
|
|
15
|
+
attribute is set to None.
|
|
16
|
+
| genome (:obj:`pyham.genome.Genome`): Related Genome object.
|
|
17
|
+
| arose_by_duplication: pyham.abstractgene.DuplicationNode if this AbstractGene arose by a duplication from its parent otherwise False.
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, arose_by_duplication=False, **kwargs):
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
Attributes:
|
|
25
|
+
| arose_by_duplication (:obj:`Bool`): True if this AbstractGene arose by a duplication from its parent.
|
|
26
|
+
| ** kwargs: dictionary of attribute and value required to create the: obj:`pyham.abstractgene.AbstractGene`.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
self.parent = None
|
|
30
|
+
self.genome = None
|
|
31
|
+
self._properties = {}
|
|
32
|
+
self.arose_by_duplication = arose_by_duplication
|
|
33
|
+
|
|
34
|
+
@abc.abstractmethod
|
|
35
|
+
def set_genome(self, genome):
|
|
36
|
+
"""Set the genome attribute using the given :obj:`Genome`."""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@abc.abstractmethod
|
|
40
|
+
def is_singleton(self):
|
|
41
|
+
""" Return true if singletons otherwise false"""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
def search_ancestor_hog_in_ancestral_genome(self, ancestral_genome):
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
Get the :obj:`HOG` related to the query :obj:`AncestralGenome` from which this :obj:`AbstractGene` is
|
|
48
|
+
descendant otherwise return None. In addition, it returns a boolean whether a duplication occurs in between the two
|
|
49
|
+
:obj:`AbstractGene`.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
ancestral_genome (:obj:`pyham.genome.AncestralGenome`): Ancestral genome of interest.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
return :obj:`pyham.abstractgene.HOG` or None and :obj:`bool`
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
found = None
|
|
60
|
+
current_hog = self
|
|
61
|
+
paralog = current_hog.arose_by_duplication
|
|
62
|
+
|
|
63
|
+
# Until we reach the top level HOG or the ancestor HOG is founded
|
|
64
|
+
while current_hog.parent is not None and found is None:
|
|
65
|
+
current_hog = current_hog.parent
|
|
66
|
+
if current_hog.genome == ancestral_genome:
|
|
67
|
+
return current_hog, paralog
|
|
68
|
+
if current_hog.arose_by_duplication != False:
|
|
69
|
+
paralog = True
|
|
70
|
+
return found, paralog
|
|
71
|
+
|
|
72
|
+
def get_top_level_hog(self):
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
Get the related top level :obj:`pyham.abstractgene.HOG`. If :obj:`pyham.abstractgene.HOG` return self, the :obj:`pyham.abstractgene.HOG` is already top level. If :obj:`pyham.abstractgene.Gene` return self, the :obj:`pyham.abstractgene.Gene` is a singleton.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
return :obj:`pyham.abstractgene.HOG`.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
current_hog = self
|
|
82
|
+
|
|
83
|
+
while current_hog.parent is not None:
|
|
84
|
+
current_hog = current_hog.parent
|
|
85
|
+
|
|
86
|
+
return current_hog
|
|
87
|
+
|
|
88
|
+
def get_at_level(self, genome):
|
|
89
|
+
|
|
90
|
+
"""
|
|
91
|
+
Get for the query HOG, the ancestor or children AbstractGene in the genome of interest
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
genome (:obj:`pyham.genome.Genome`): Genome of interest.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
list of :obj:`pyham.abstractgene.AbstractGene`.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
TypeError: if genome is not a Genome.
|
|
101
|
+
KeyError: If no HOG matches the query genome or it return itself.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if not isinstance(genome, Genome):
|
|
106
|
+
raise TypeError("expect subclass obj of '{}', got {}"
|
|
107
|
+
.format(Genome.__name__,
|
|
108
|
+
type(genome).__name__))
|
|
109
|
+
|
|
110
|
+
tl = self.get_top_level_hog()
|
|
111
|
+
|
|
112
|
+
def pre(node, elem):
|
|
113
|
+
if node.genome == genome:
|
|
114
|
+
elem.append(node)
|
|
115
|
+
return elem
|
|
116
|
+
|
|
117
|
+
def leaf(node, leaf, elem):
|
|
118
|
+
if leaf.genome == genome:
|
|
119
|
+
elem.append(leaf)
|
|
120
|
+
return elem
|
|
121
|
+
|
|
122
|
+
rlist = tl.visit([], function_extant_gene=leaf, function_prefix=pre)
|
|
123
|
+
|
|
124
|
+
if not rlist:
|
|
125
|
+
raise KeyError("Level {} not found within this HOG".format(genome))
|
|
126
|
+
|
|
127
|
+
for e in rlist:
|
|
128
|
+
if e == self:
|
|
129
|
+
raise KeyError("get level cannot return itself".format())
|
|
130
|
+
|
|
131
|
+
return rlist
|
|
132
|
+
|
|
133
|
+
def add_property(self, name, value):
|
|
134
|
+
self._properties[name] = value
|
|
135
|
+
|
|
136
|
+
def __getitem__(self, item):
|
|
137
|
+
return self._properties[item]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class HOG(AbstractGene):
|
|
141
|
+
|
|
142
|
+
"""
|
|
143
|
+
HOG class represents ancestral genes. An AbstractGene is defined the genome it belongs to and its child/parent
|
|
144
|
+
AbstractGene. Only top level HOG doesn't have parent attribute (set to None).
|
|
145
|
+
|
|
146
|
+
Attributes:
|
|
147
|
+
| hog_id (:obj:`str`): hog id. Defaults is None.
|
|
148
|
+
| children (:obj:`list` of :obj:`pyham.abstractgene.AbstractGene`): A list of direct descendants AbstractGene.
|
|
149
|
+
| hogvis (:obj:`pyham.IHAM`): :obj:`pyham.IHAM` object of this HOG.
|
|
150
|
+
| duplications (:obj:`list` of :obj:`pyham.abstractgene.DuplicationNode`): list of all duplication node child of this HOG.
|
|
151
|
+
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def __init__(self, id=None, **kwargs):
|
|
155
|
+
super(HOG, self).__init__(**kwargs)
|
|
156
|
+
self.hog_id = id
|
|
157
|
+
if id is None:
|
|
158
|
+
self.hog_id = kwargs.get('og', id) # If we have the gene named in og tag, use this.
|
|
159
|
+
self.og = kwargs.get('og')
|
|
160
|
+
self.taxon_id = int(kwargs['taxonId']) if 'taxonId' in kwargs else None
|
|
161
|
+
self.children = []
|
|
162
|
+
self.hogvis = None
|
|
163
|
+
self.duplications = []
|
|
164
|
+
|
|
165
|
+
def add_child(self, child_to_add):
|
|
166
|
+
|
|
167
|
+
"""
|
|
168
|
+
Add in the children attribute a given :obj:`pyham.abstractgene.AbstractGene` and update parent attribute in child AbstractGene.
|
|
169
|
+
|
|
170
|
+
Attributes:
|
|
171
|
+
child_to_add (:obj:`pyham.abstractgene.AbstractGene`): add_child to add.
|
|
172
|
+
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
if not isinstance(child_to_add, AbstractGene):
|
|
176
|
+
raise TypeError("expect subclass obj of '{}', got {}"
|
|
177
|
+
.format(AbstractGene.__name__,
|
|
178
|
+
type(child_to_add).__name__))
|
|
179
|
+
|
|
180
|
+
if self == child_to_add:
|
|
181
|
+
raise EvolutionaryConceptError('Cannot be a child of itself')
|
|
182
|
+
|
|
183
|
+
self.children.append(child_to_add)
|
|
184
|
+
child_to_add.parent = self
|
|
185
|
+
|
|
186
|
+
def remove_child(self, child_to_remove):
|
|
187
|
+
|
|
188
|
+
"""
|
|
189
|
+
Remove from the children attribute a given :obj:`AbstractGene` and update parent attribute in child
|
|
190
|
+
AbstractGene.
|
|
191
|
+
|
|
192
|
+
Attributes:
|
|
193
|
+
child_to_add (:obj:`AbstractGene`): add_child to add.
|
|
194
|
+
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
if not isinstance(child_to_remove, AbstractGene):
|
|
198
|
+
raise TypeError("expect subclass obj of '{}', got {}"
|
|
199
|
+
.format(AbstractGene.__name__,
|
|
200
|
+
type(child_to_remove).__name__))
|
|
201
|
+
|
|
202
|
+
if child_to_remove in self.children:
|
|
203
|
+
self.children.remove(child_to_remove)
|
|
204
|
+
child_to_remove.parent = None
|
|
205
|
+
|
|
206
|
+
else:
|
|
207
|
+
raise ValueError("element not found in the hog children")
|
|
208
|
+
|
|
209
|
+
def score(self, score_id, value=None):
|
|
210
|
+
"""
|
|
211
|
+
Get or set a score attribute for this HOG.
|
|
212
|
+
|
|
213
|
+
If no value is passed, the stored value is returned or a KeyError is raised if this score has not been stored.
|
|
214
|
+
|
|
215
|
+
Attributes:
|
|
216
|
+
score_id (:obj:`ExtantGenome`): score to be set or returned.
|
|
217
|
+
value (optional): A numeric value for the score of this HOG.
|
|
218
|
+
score_id (:obj:`ExtantGenome`): score to be set or returned.
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
KeyError: if score_id is unknown and accessed.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
if value is None:
|
|
225
|
+
try:
|
|
226
|
+
return self.scores[score_id]
|
|
227
|
+
except (AttributeError, KeyError):
|
|
228
|
+
raise KeyError("Score '{}' not set".format(score_id))
|
|
229
|
+
|
|
230
|
+
if not isinstance(value, numbers.Number):
|
|
231
|
+
raise ValueError("value must be numeric")
|
|
232
|
+
scores = getattr(self, 'scores', {})
|
|
233
|
+
scores[score_id] = value
|
|
234
|
+
self.scores = scores
|
|
235
|
+
|
|
236
|
+
def set_genome(self, genome):
|
|
237
|
+
|
|
238
|
+
"""
|
|
239
|
+
This method set :obj:`pyham.genome.AncestralGenome` given as parameter as genome attribute.
|
|
240
|
+
|
|
241
|
+
Attributes:
|
|
242
|
+
| genome (:obj:`pyham.genome.AncestralGenome`): Ancestral genome to be set as genome.
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
# Check that genome is an :obj:`AncestralGenome`
|
|
246
|
+
if not isinstance(genome, AncestralGenome):
|
|
247
|
+
raise TypeError("expect subclass obj of '{}', got {}"
|
|
248
|
+
.format(AncestralGenome.__name__,
|
|
249
|
+
type(genome).__name__))
|
|
250
|
+
|
|
251
|
+
if self.genome is not None and genome != self.genome:
|
|
252
|
+
raise EvolutionaryConceptError("HOG can only be mapped to one ancestral genome")
|
|
253
|
+
|
|
254
|
+
self.genome = genome
|
|
255
|
+
|
|
256
|
+
def visit(self, elem, function_extant_gene=None, function_postfix=None, function_prefix=None):
|
|
257
|
+
|
|
258
|
+
"""
|
|
259
|
+
Recursive function to traverse the HOG nested children hierarchy.
|
|
260
|
+
|
|
261
|
+
An arbitrary object "elem" is return at each visit() and is pass/returned from each recursive state function
|
|
262
|
+
calls.
|
|
263
|
+
|
|
264
|
+
There is tree recursive state functions:
|
|
265
|
+
|
|
266
|
+
- function_extant_gene: Function called at each ExtantGene child. Its take as argument the child ExtantGene
|
|
267
|
+
and the "elem" and return processed "elem".
|
|
268
|
+
- function_postfix: Function called after each HOG child visit(). Its take as argument the child HOG and
|
|
269
|
+
the "elem" and return processed "elem".
|
|
270
|
+
- function_prefix: First function called that take as argument the current HOG and the "elem" and return
|
|
271
|
+
processed "elem".
|
|
272
|
+
|
|
273
|
+
Attributes:
|
|
274
|
+
| elem (arbitrary object): object that is pass through visit recursive and recursive state functions calls.
|
|
275
|
+
| function_extant_gene (callback function): Callback function for ExtantGenes (leaves).
|
|
276
|
+
| function_postfix (callback function): Callback function for after child visit current HOG.
|
|
277
|
+
| function_prefix (callback function): Callback function for current HOG.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
return arbitrary
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
if function_prefix is not None:
|
|
284
|
+
elem = function_prefix(self, elem)
|
|
285
|
+
|
|
286
|
+
for child in self.children:
|
|
287
|
+
if isinstance(child, Gene):
|
|
288
|
+
if function_extant_gene is not None:
|
|
289
|
+
elem = function_extant_gene(self, child, elem)
|
|
290
|
+
else:
|
|
291
|
+
child.visit(elem, function_extant_gene=function_extant_gene, function_postfix=function_postfix,
|
|
292
|
+
function_prefix=function_prefix)
|
|
293
|
+
if function_postfix is not None:
|
|
294
|
+
elem = function_postfix(self, child, elem)
|
|
295
|
+
return elem
|
|
296
|
+
|
|
297
|
+
# TODO: UT
|
|
298
|
+
|
|
299
|
+
def get_all_descendant_genes(self):
|
|
300
|
+
|
|
301
|
+
"""
|
|
302
|
+
Get all :obj:`Gene` present in this HOG.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
list of :obj:`pyham.abstractgene.Gene`
|
|
306
|
+
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
def append_leaf(current, child, list):
|
|
310
|
+
list.append(child)
|
|
311
|
+
return list
|
|
312
|
+
|
|
313
|
+
return self.visit([], function_extant_gene=append_leaf)
|
|
314
|
+
|
|
315
|
+
def get_all_descendant_genes_clustered_by_species(self):
|
|
316
|
+
"""
|
|
317
|
+
Get all :obj:`pyham.abstractgene.Gene` present in this HOG clustered by species :obj:`pyham.genomeExtantGenome`.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Dictionary of :obj:`pyham.genome.ExtantGenome` map their list of :obj:`pyham.abstractgene.Gene`.
|
|
321
|
+
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
def append_add_lead_with_genome(current, child, dict):
|
|
325
|
+
dict.setdefault(child.genome, []).append(child)
|
|
326
|
+
return dict
|
|
327
|
+
|
|
328
|
+
return self.visit({}, function_extant_gene=append_add_lead_with_genome)
|
|
329
|
+
|
|
330
|
+
def get_all_descendant_hogs(self): # TODO: self is also returned with it
|
|
331
|
+
|
|
332
|
+
"""
|
|
333
|
+
Get all :obj:`pyham.abstractgene.HOG` present in this HOG hierarchy.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Dictionary of :obj:`pyham.genomeExtantGenome` map their list of :obj:`pyham.abstractgene.Gene`.
|
|
337
|
+
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
def append_child(current, list):
|
|
341
|
+
list.append(current)
|
|
342
|
+
return list
|
|
343
|
+
|
|
344
|
+
return self.visit([], function_prefix=append_child)
|
|
345
|
+
|
|
346
|
+
def get_all_descendant_hog_levels(self): # TODO: self is also returned with it
|
|
347
|
+
|
|
348
|
+
"""
|
|
349
|
+
Get all :obj:`pyham.genome.Genome` present in this HOG hierarchy.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
List of :obj:`pyham.genome.Genome`.
|
|
353
|
+
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
def append_current_genome(current,list):
|
|
357
|
+
list.append(current.genome)
|
|
358
|
+
return list
|
|
359
|
+
|
|
360
|
+
return self.visit([], function_prefix=append_current_genome)
|
|
361
|
+
|
|
362
|
+
def get_number_losses(self):
|
|
363
|
+
|
|
364
|
+
"""
|
|
365
|
+
Compute the number of implied gene losses within this HOG's hierarchy, following the
|
|
366
|
+
Dollo parsimony principle: once a gene lineage is lost it cannot be regained, so a
|
|
367
|
+
whole clade lacking any representative under this HOG is explained by a single loss
|
|
368
|
+
event placed on the branch leading to that clade, rather than one loss per missing
|
|
369
|
+
descendant leaf.
|
|
370
|
+
|
|
371
|
+
For every ancestral level in this HOG's subtree (starting at self), each immediate
|
|
372
|
+
child taxon in the species tree that has no matching child :obj:`HOG`/:obj:`Gene` here
|
|
373
|
+
contributes one loss; that clade is not explored further since it is already fully
|
|
374
|
+
accounted for by this single event.
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
:obj:`int`: number of implied Dollo loss events in this HOG's subtree.
|
|
378
|
+
"""
|
|
379
|
+
|
|
380
|
+
present_children_taxa = {child.genome.taxon for child in self.children}
|
|
381
|
+
losses = sum(1 for taxon in self.genome.taxon.children if taxon not in present_children_taxa)
|
|
382
|
+
|
|
383
|
+
for child in self.children:
|
|
384
|
+
if isinstance(child, HOG):
|
|
385
|
+
losses += child.get_number_losses()
|
|
386
|
+
|
|
387
|
+
return losses
|
|
388
|
+
|
|
389
|
+
def get_hog_vis(self, newick_str):
|
|
390
|
+
|
|
391
|
+
""" Lazy getter of the :obj:`pyham.abstractgene.HOG` IHAM.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
newick_str (:obj:`str`): newick species tree used by the create_iHam.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
:obj:`pyham.IHAM` of this HOG.
|
|
398
|
+
|
|
399
|
+
"""
|
|
400
|
+
|
|
401
|
+
if self.hogvis is None:
|
|
402
|
+
self.hogvis = IHAM(newick_str, self)
|
|
403
|
+
|
|
404
|
+
return self.hogvis
|
|
405
|
+
|
|
406
|
+
# TODO: end UT
|
|
407
|
+
|
|
408
|
+
def is_singleton(self):
|
|
409
|
+
return False
|
|
410
|
+
|
|
411
|
+
def to_ete_tree(self):
|
|
412
|
+
"""
|
|
413
|
+
Convert this :obj:`HOG` and its nested sub-HOGs into an :obj:`ete4.Tree`.
|
|
414
|
+
|
|
415
|
+
Each nested :obj:`HOG` becomes an internal node named after its hog_id
|
|
416
|
+
(also stored as the "hog_id" prop), and each terminal :obj:`Gene` becomes
|
|
417
|
+
a leaf named after its external gene id (geneId, protId or transcriptId,
|
|
418
|
+
in that order of preference, falling back to its internal unique_id),
|
|
419
|
+
with all available external ids attached as leaf props.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
:obj:`ete4.Tree`: tree representation of this HOG hierarchy, rooted at self.
|
|
423
|
+
"""
|
|
424
|
+
|
|
425
|
+
def leaf_name(gene):
|
|
426
|
+
xref = gene.get_dict_xref()
|
|
427
|
+
for key in ("geneId", "protId", "transcriptId", "id"):
|
|
428
|
+
if key in xref:
|
|
429
|
+
return str(xref[key])
|
|
430
|
+
return str(gene.unique_id)
|
|
431
|
+
|
|
432
|
+
def build(hog):
|
|
433
|
+
node = Tree({"name": hog.hog_id})
|
|
434
|
+
node.add_prop("hog_id", hog.hog_id)
|
|
435
|
+
node.add_prop("taxon", hog.genome.name)
|
|
436
|
+
node.add_prop("duplicate", bool(hog.arose_by_duplication))
|
|
437
|
+
if hog.arose_by_duplication:
|
|
438
|
+
# identifies which sibling nodes share the same duplication event
|
|
439
|
+
# (hog.arose_by_duplication is a shared DuplicationNode instance);
|
|
440
|
+
# a plain object id is enough since this only needs to be stable
|
|
441
|
+
# within one export, not meaningful across separate runs.
|
|
442
|
+
node.add_prop("duplication_id", id(hog.arose_by_duplication))
|
|
443
|
+
for child in hog.children:
|
|
444
|
+
if isinstance(child, Gene):
|
|
445
|
+
leaf = node.add_child(name=leaf_name(child))
|
|
446
|
+
for key, value in child.get_dict_xref().items():
|
|
447
|
+
leaf.add_prop(key, value)
|
|
448
|
+
leaf.add_prop("taxon", child.genome.name)
|
|
449
|
+
else:
|
|
450
|
+
node.add_child(build(child))
|
|
451
|
+
return node
|
|
452
|
+
|
|
453
|
+
return build(self)
|
|
454
|
+
|
|
455
|
+
def find_by_id(self, hog_id: str, taxid: int = None):
|
|
456
|
+
"""
|
|
457
|
+
Recursive function to find a HOG by its id and optionally its taxid.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
| hog_id (:obj:`str`): HOG id to be found.
|
|
461
|
+
| taxid (optional, :obj:`int`): taxid of the genome of the HOG to be found.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
:obj:`pyham.abstractgene.HOG` if found otherwise None.
|
|
465
|
+
"""
|
|
466
|
+
|
|
467
|
+
cur_hog_without_taxid = self.hog_id.split('_')[0]
|
|
468
|
+
if cur_hog_without_taxid == hog_id and (taxid is None or self.taxon_id == taxid or self.hog_id == f"{hog_id}_{taxid}"):
|
|
469
|
+
return self
|
|
470
|
+
elif not hog_id.startswith(cur_hog_without_taxid):
|
|
471
|
+
return None
|
|
472
|
+
|
|
473
|
+
for child in getattr(self, 'children', []):
|
|
474
|
+
if isinstance(child, HOG):
|
|
475
|
+
found = child.find_by_id(hog_id, taxid)
|
|
476
|
+
if found is not None:
|
|
477
|
+
return found
|
|
478
|
+
return None
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def __repr__(self):
|
|
482
|
+
ids = []
|
|
483
|
+
if self.hog_id is not None:
|
|
484
|
+
ids.append("id={}".format(self.hog_id))
|
|
485
|
+
if self.og is not None and self.og != self.hog_id:
|
|
486
|
+
ids.append("og={}".format(self.og))
|
|
487
|
+
if len(ids) == 0:
|
|
488
|
+
n = self.parent
|
|
489
|
+
while n.hog_id is None and n.parent is not None:
|
|
490
|
+
n = n.parent
|
|
491
|
+
ids.append("id={}-{}".format(n.hog_id, self.__hash__()))
|
|
492
|
+
try:
|
|
493
|
+
ids.append("level={}".format(self.genome.name))
|
|
494
|
+
except AttributeError:
|
|
495
|
+
pass
|
|
496
|
+
return "<{}({})>".format(self.__class__.__name__, ";".join(ids))
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class Gene(AbstractGene):
|
|
500
|
+
"""Gene class for extant genes (inherit from AbstractGene).
|
|
501
|
+
|
|
502
|
+
Attributes:
|
|
503
|
+
| unique_id internal unique id
|
|
504
|
+
| gene_id id used to mapped to external ids.
|
|
505
|
+
| prot_id id used to mapped to external ids.
|
|
506
|
+
| transcript_id id used to mapped to external ids.
|
|
507
|
+
|
|
508
|
+
"""
|
|
509
|
+
|
|
510
|
+
def __init__(self, id, geneId=None, protId=None, transcriptId=None, **kwargs):
|
|
511
|
+
super(Gene, self).__init__(**kwargs)
|
|
512
|
+
self.unique_id = id
|
|
513
|
+
self.gene_id = geneId
|
|
514
|
+
self.prot_id = protId
|
|
515
|
+
self.transcript_id = transcriptId
|
|
516
|
+
|
|
517
|
+
def set_LOFT(self, loft_id):
|
|
518
|
+
"""Set the LOFT identifier for the gene.
|
|
519
|
+
|
|
520
|
+
In case the ID has already been set, a ValueError is raised. It is assumed
|
|
521
|
+
that a gene can only have one LOFT identifier."""
|
|
522
|
+
if hasattr(self, 'hog_id'):
|
|
523
|
+
raise ValueError("Gene {} has already a LOFT identifier assigned ({}).".format(self, self.hog_id))
|
|
524
|
+
self.hog_id = loft_id
|
|
525
|
+
|
|
526
|
+
def set_genome(self, genome):
|
|
527
|
+
"""
|
|
528
|
+
This method set :obj:`pyham.genome.ExtantGenome` given as parameter as genome attribute.
|
|
529
|
+
|
|
530
|
+
Attributes:
|
|
531
|
+
genome (:obj:`pyham.genome.ExtantGenome`): Extant genome to be set as genome.
|
|
532
|
+
|
|
533
|
+
Raises:
|
|
534
|
+
TypeError: If genome is not :obj:`pyham.genome.ExtantGenome`.
|
|
535
|
+
EvolutionaryConceptError: If we try to add more than one genome.
|
|
536
|
+
"""
|
|
537
|
+
|
|
538
|
+
# Check that genome is an :obj:`ExtantGenome`
|
|
539
|
+
if not isinstance(genome, ExtantGenome):
|
|
540
|
+
raise TypeError("expect subclass obj of '{}', got {}"
|
|
541
|
+
.format(ExtantGenome.__name__,
|
|
542
|
+
type(genome).__name__))
|
|
543
|
+
|
|
544
|
+
if self.genome is not None and genome != self.genome:
|
|
545
|
+
raise EvolutionaryConceptError("Gene can only belong to one genome")
|
|
546
|
+
|
|
547
|
+
self.genome = genome
|
|
548
|
+
|
|
549
|
+
def get_dict_xref(self): # <-- TODO: UT
|
|
550
|
+
|
|
551
|
+
""" Get the dictionary of cross references.
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
Dictionary of external id tag map with their value.
|
|
555
|
+
"""
|
|
556
|
+
|
|
557
|
+
xref = {}
|
|
558
|
+
|
|
559
|
+
if self.unique_id is not None:
|
|
560
|
+
xref["id"] = self.unique_id
|
|
561
|
+
if self.gene_id is not None:
|
|
562
|
+
xref["geneId"] = self.gene_id
|
|
563
|
+
if self.prot_id is not None:
|
|
564
|
+
xref["protId"] = self.prot_id
|
|
565
|
+
if self.transcript_id is not None:
|
|
566
|
+
xref["transcriptId"] = self.transcript_id
|
|
567
|
+
|
|
568
|
+
return xref
|
|
569
|
+
|
|
570
|
+
def is_singleton(self):
|
|
571
|
+
if self.parent is None:
|
|
572
|
+
return True
|
|
573
|
+
else:
|
|
574
|
+
return False
|
|
575
|
+
|
|
576
|
+
def __repr__(self):
|
|
577
|
+
return "{}({})".format(self.__class__.__name__, self.unique_id)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def short_level_name(name, max_len=60):
|
|
581
|
+
"""Abbreviate a level name synthesized from its children's names (``A/B/C/...``) for messages."""
|
|
582
|
+
if name is None or len(name) <= max_len or "/" not in name:
|
|
583
|
+
return name
|
|
584
|
+
parts = name.split("/")
|
|
585
|
+
return f"{parts[0]}/.../{parts[-1]} ({len(parts)} species)"
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
class SameLevelHOGError(Exception):
|
|
589
|
+
pass
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
@dataclasses.dataclass
|
|
593
|
+
class TaxonomicConflict:
|
|
594
|
+
"""One HOG whose claimed taxonomic level conflicts with the given species tree.
|
|
595
|
+
|
|
596
|
+
Attributes:
|
|
597
|
+
| family_id (:obj:`str`): top-level HOG id this conflict was found in.
|
|
598
|
+
| hog_id (:obj:`str`): the HOG whose TaxRange/taxid claim is in question.
|
|
599
|
+
| claim_property (:obj:`str`): "TaxRange" or "taxid".
|
|
600
|
+
| claim_value (:obj:`str`): the raw property value that was claimed.
|
|
601
|
+
| resolved_level (:obj:`str`): name of the taxon node actually used. Equal to
|
|
602
|
+
claim_value at present (this only fires once the claim itself resolved to a
|
|
603
|
+
node), kept as a separate field for forward-compatibility rather than as a
|
|
604
|
+
redundant duplicate.
|
|
605
|
+
| kind (:obj:`str`): "same_rank" (a child collapses onto the same level as
|
|
606
|
+
its own parent), "disjoint" (a child sits in an unrelated branch), or
|
|
607
|
+
"inverted" (the claimed level is itself a descendant of the child).
|
|
608
|
+
| offending_members (:obj:`list`): list of (label, species, detail) tuples
|
|
609
|
+
describing each child that doesn't fit under the claimed level.
|
|
610
|
+
| sibling_members (:obj:`list`): list of (label, species) tuples describing
|
|
611
|
+
this HOG's other children, for context on what else is present at this level.
|
|
612
|
+
"""
|
|
613
|
+
family_id: str
|
|
614
|
+
hog_id: str
|
|
615
|
+
claim_property: str
|
|
616
|
+
claim_value: str
|
|
617
|
+
resolved_level: str
|
|
618
|
+
kind: str
|
|
619
|
+
offending_members: list
|
|
620
|
+
sibling_members: list
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
class TaxonomicConflictError(Exception):
|
|
624
|
+
"""Raised when one or more HOGs' claimed taxonomic level conflicts with the
|
|
625
|
+
given species tree. Carries the full list of :obj:`TaxonomicConflict` records
|
|
626
|
+
found (possibly just one, in fail_fast mode) in the `conflicts` attribute."""
|
|
627
|
+
|
|
628
|
+
def __init__(self, message: str, conflicts: list):
|
|
629
|
+
super().__init__(message)
|
|
630
|
+
self.conflicts = conflicts
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
class DuplicationNode(object):
|
|
634
|
+
"""
|
|
635
|
+
This object link together all abstract genes that emerges from the same duplication event. Its composed of a set of genes
|
|
636
|
+
all descendant from this duplication event and their direct related parent hog. In addition it contained the parent of the MRCA of all
|
|
637
|
+
children genes (otherwise MRCA return genome father duplication).
|
|
638
|
+
|
|
639
|
+
Attributes:
|
|
640
|
+
| parent (:obj:`pyham.abstractgene.HOG`): Direct parent HOG this DuplicationNode.
|
|
641
|
+
| children: List of all duplicated genes
|
|
642
|
+
| MRCA: Parent of the MRCA of all children gene that contained the parent HOG.
|
|
643
|
+
|
|
644
|
+
"""
|
|
645
|
+
|
|
646
|
+
def __init__(self, parser, id=None):
|
|
647
|
+
self.parser = parser
|
|
648
|
+
self.MRCA = None
|
|
649
|
+
self.children = []
|
|
650
|
+
self.parent = None
|
|
651
|
+
self.id = id
|
|
652
|
+
|
|
653
|
+
def set_parent(self, hog):
|
|
654
|
+
"""
|
|
655
|
+
Args:
|
|
656
|
+
| hog (:obj:`pyham.abstractegene.HOG`): parent :obj:`pyham.abstractegene.HOG` of this duplication.
|
|
657
|
+
|
|
658
|
+
"""
|
|
659
|
+
|
|
660
|
+
if not isinstance(hog, HOG):
|
|
661
|
+
raise TypeError("expect subclass obj of '{}', got {}"
|
|
662
|
+
.format(HOG.__name__,
|
|
663
|
+
type(hog).__name__))
|
|
664
|
+
|
|
665
|
+
hog.duplications.append(self)
|
|
666
|
+
self.parent = hog
|
|
667
|
+
|
|
668
|
+
def set_MRCA(self):
|
|
669
|
+
"""
|
|
670
|
+
Compute the MRCA of all genes genomes.
|
|
671
|
+
|
|
672
|
+
It is always defined as the level of the genome which is the parent
|
|
673
|
+
of the branch on which the duplication event occured. E.g. if the
|
|
674
|
+
duplication event on the branch between Mammalia and Euchontoglires,
|
|
675
|
+
the MRCA is set to Mammalia.
|
|
676
|
+
|
|
677
|
+
"""
|
|
678
|
+
|
|
679
|
+
children_taxa = set([child.genome.taxon for child in self.children])
|
|
680
|
+
if len(children_taxa) < 2:
|
|
681
|
+
mrca = children_taxa.pop().up
|
|
682
|
+
else:
|
|
683
|
+
mrca = self.parser.taxonomy.get_mrca_taxnode(*children_taxa)
|
|
684
|
+
if mrca.up is None:
|
|
685
|
+
copies = ", ".join(
|
|
686
|
+
f"{c.hog_id if isinstance(c, HOG) else (c.prot_id or c.gene_id or c.unique_id)} "
|
|
687
|
+
f"at \"{short_level_name(c.genome.name)}\"" for c in self.children)
|
|
688
|
+
raise EvolutionaryConceptError(
|
|
689
|
+
f"The copies of {'duplication ' + str(self.id) if self.id is not None else 'a duplication'} "
|
|
690
|
+
f"({copies}) only meet at the root of the species tree (\"{short_level_name(mrca.name)}\"), "
|
|
691
|
+
f"so the duplication would predate the tree. The species tree probably lacks levels the "
|
|
692
|
+
f"OrthoXML was built with, e.g. because it is pruned.")
|
|
693
|
+
mrca = mrca.up
|
|
694
|
+
self.MRCA = self.parser.taxonomy.get_genome_from_taxnode(mrca)
|
|
695
|
+
|
|
696
|
+
def add_child(self, child):
|
|
697
|
+
"""
|
|
698
|
+
Add child to list of children
|
|
699
|
+
"""
|
|
700
|
+
|
|
701
|
+
if not isinstance(child, AbstractGene):
|
|
702
|
+
raise TypeError("expect subclass obj of '{}', got {}"
|
|
703
|
+
.format(AbstractGene.__name__,
|
|
704
|
+
type(child).__name__))
|
|
705
|
+
|
|
706
|
+
self.children.append(child)
|
|
707
|
+
child.arose_by_duplication = self
|
|
708
|
+
|
|
709
|
+
def remove_child(self, child_to_remove):
|
|
710
|
+
"""
|
|
711
|
+
remove child to list of children
|
|
712
|
+
"""
|
|
713
|
+
|
|
714
|
+
if not isinstance(child_to_remove, AbstractGene):
|
|
715
|
+
raise TypeError("expect subclass obj of '{}', got {}"
|
|
716
|
+
.format(AbstractGene.__name__,
|
|
717
|
+
type(child_to_remove).__name__))
|
|
718
|
+
|
|
719
|
+
if child_to_remove in self.children:
|
|
720
|
+
self.children.remove(child_to_remove)
|
|
721
|
+
child_to_remove.arose_by_duplication = False
|
|
722
|
+
|
|
723
|
+
else:
|
|
724
|
+
raise ValueError("element not found in the duplication children")
|
|
725
|
+
|
|
726
|
+
|