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/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
+