-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathNode.py
More file actions
3486 lines (3134 loc) · 134 KB
/
Copy pathNode.py
File metadata and controls
3486 lines (3134 loc) · 134 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""Newick format parser module.
Manipulates trees in the Newick format. This format is mainly used to
describe phylogenetic binary trees but can have a much wider use. Some
details on the format can be found here:
http://evolution.genetics.washington.edu/phylip/newicktree.html
Examples of Newick format:
* (Bovine:0.69395,(Gibbon:0.36079,(Orang:0.33636,(Gorilla:0.17147,(Chimp:0.19268, Human:0.11927):0.08386):0.06124):0.15057):0.54939,Mouse:1.21460):0.10;
* '((A:0.1,B:0.2,C:0.1)ABCnode:0.2,(D:0.4,E:0.1)99:0.1);'
Note that bracked delimited nested and unnested comments are ignored.
Sequences can be assigned to each node for simulating evolution.
Derived from module 'tree.py' from 'alfacinha' package by Leonor Palmeira and Laurent Guéguen (http://pbil.univ-lyon1.fr/software/alfacinha/)
Copyright 2007, Leonor Palmeira <palmeira@biomserv.univ-lyon1.fr>;
Copyright 2013, Florent Lassalle <florent.lassalle@univ-lyon1.fr>;
Copyright 2015, Florent Lassalle <florent.lassalle@ucl.ac.uk>;
Copyright 2016, Florent Lassalle <f.lassalle@imperial.ac.uk>.
"""
__author__ = "Leonor Palmeira <palmeira@biomserv.univ-lyon1.fr>, Laurent Guéguen <laurent.gueguen@univ-lyon1.fr>, Florent Lassalle <f.lassalle@imperial.ac.uk>"
__date__ = "10 January 2016"
__credits__ = """Guido van Rossum, for an excellent programming language; Leonor Palmeira and Laurent Guéguen for initiating the Node class"""
import string
import copy
import subprocess
import re
import shutil, os
#import evol
import tree2
from tree2 import svgNode
import numpy as np
def sumlen(*L):
"""sum branch lengths (floats or None from list L), with None taking value 0 unless only None are summed in which case None is returned"""
l = None
for k in L:
if k is not None:
if l is not None: l += k
else: l = k
return l
#######################################################################
#######################################################################
######## Node class: attributes and instance creation
class Node(object):
"""Node is the smallest unit of definition for a tree.
A Node instance is linked to its father-Node and its children-Nodes:
it defines a sub-tree for which it is the root.
Node instances are reccursively contained in their parents up to the
tree root-Node, which is equivalent to the whole tree.
"""
def __init__(self, branch_lengths=True, keep_comments=False, combrackets='[]', labquotes=False, namesAsNum=False, leafNamesAsNum=False, bootInComm=False, **kw):
"""Create a Node.
* Keyword argument to build directly the Node from a Newick string: [newick=string] or [nwk=string]
or from a file: [file=filepath] or [fic=filepath]
Examples of Newick format:
- with branch length and leaf labels:
'(Bovine:0.69395,(Gibbon:0.36079,(Orang:0.33636,(Gorilla:0.17147,(Chimp:0.19268,Human:0.11927):0.08386):0.06124):0.15057):0.54939,Mouse:1.21460):0.10;'
- with branch length, leaf labels and branch supports:
'((SpeciesA:0.45,SpeciesB:0.42)0.84:0.75,(SpeciesC:0.58,SpeciesD:0.85)0.95:0.115,SpeciesE:0.50);'
- with branch length, leaf labels, branch supports and bracketted comments:
'((SpeciesA[Phenotype1]:0.45,SpeciesB[Phenotype1]:0.42)0.84[Phenotype1]:0.75,(SpeciesC[Phenotype1]:0.58,SpeciesD[Phenotype2]:0.85)0.95[Phenotype2]:0.115,SpeciesE[Phenotype3]:0.50);'
- with branch length, leaf labels, bracketted comments and internal labels instead of branch supports.
'((SpeciesA[Phenotype1]:0.45,SpeciesB[Phenotype1]:0.42)Clade1[Phenotype1]:0.75,(SpeciesC[Phenotype1]:0.58,SpeciesD[Phenotype2]:0.85)Clade2[Phenotype2]:0.115,SpeciesE[Phenotype3]:0.50);'
* Default behaviour is to discard comments ; can be switched by specifying [keep_comments=True].
Characters used for comment brackets can be changed using [combrackets=charset] (avoid parenthesis !!!).
Bracketted comments can be located on either end of the label (or support for internal nodes), and these can be mixed:
'([Phenotype1]SpeciesC:0.58,[Phenotype2]SpeciesD:0.85);' <=> '(SpeciesC[Phenotype1]:0.58,SpeciesD[Phenotype2]:0.85);' <=> '(SpeciesC[Phenotype1]:0.58,[Phenotype2]SpeciesD:0.85);'
* Numerals after a semicolon indicating branch lengths are expected for proper parsing,
but in the absence of branch length information (cladogram),
this default behavour can be changed by specifying [branch_lengths=False].
* Leaf or internal node labels sometimes happen to be integers, as is the case of ClonalFrame trees (http://www.xavierdidelot.xtreemhost.com/clonalframe.htm).
Specify [namesAsNum=True] or [leafNamesAsNum=True] for adequate parsing of numeric labels at all nodes or leaf nodes only, respectively.
* A new tree with a multifurcated ('star') toppology can be created using the keyword 'lleaves', specifying the names of the leaves directly attached to the instance (root node).
* Names and values of arbitrary attributes can be passed as a dict (or iterable coercible to a dict) via keyword argument 'setattr';
if these attributes have same name as default ones, the values passed via 'settatr' override the default values.
"""
self.__l=kw.get('l') # length of the branch to the father-Node
self.__lab=kw.get('lab') # Node label
self.__seq=None # Node sequence
self.__boot=None # bootstrap value at the Node
self.__comment=None # nested comment attached to Node
self.__father=None # father Node instance
self.__children=[] # list of child Node instances
for kwnwk in ['newick', 'nwk']:
if kwnwk in kw:
# read from a Newick string
self.parser(s=kw[kwnwk], branch_lengths=branch_lengths, combrackets=combrackets, labquotes=labquotes, keep_comments=keep_comments, namesAsNum=namesAsNum, leafNamesAsNum=leafNamesAsNum, bootInComm=bootInComm)
break
else:
for kwfic in ['file', 'fic']:
if kwfic in kw:
# read from file containing a Newick string
self.read_nf(a=kw[kwfic], branch_lengths=branch_lengths, combrackets=combrackets, labquotes=labquotes, keep_comments=keep_comments, namesAsNum=namesAsNum, leafNamesAsNum=leafNamesAsNum, bootInComm=bootInComm)
break
else:
if 'lleaves' in kw:
# generate multifurcated tree with all leaves with names in 'lleaves' attached to the root (self) node
for leaf in kw['lleaves']:
c = self.newnode()
c.add_label(leaf)
self.link_child(c, newlen=0, newboot=0)
if 'setattr' in kw:
try:
# try to coerce the object to a dict
dattr = dict(kw['setattr'])
except ValueError, e:
raise ValueError, "encountered when setting attribute names and values for Node instance, expecting a dict or object coercible to a dict: '%s'"%str(e)
self.__dict__.update(dattr)
#####################################################
############## methods for access to object atributes:
def newnode(self, branch_lengths=True, keep_comments=False, **kw):
"""class-specific instance generator for construction of trees of Nodes"""
return Node(branch_lengths=branch_lengths, keep_comments=keep_comments, **kw)
def labelgetnode(self, n, mustMatch=False):
"""Return the first child of Node on a pre-order traversal labelled with intput string 'n'.
If mustMatch=True, raises an IndexError when no node if found; default is False, as used in __getitem__().
Assume type of input is controlled ahead of call (as in __getitem__()) so does not check for efficiency saving.
"""
if self.__lab==n: return self
for c in self.__children:
if c.label()==n:
return c
else:
t=c.labelgetnode(n, mustMatch=mustMatch) # (recursive function)
if t:
return t
if not mustMatch: return None
else: return IndexError, "no node labelled '%s' in tree"%n
def labelmatchgetnode(self, n, mustMatch=False, side='start'):
"""Return the first child of Node on a pre-order traversal wich label matches with intput string 'n'.
If side is 'start', string must match the start of input string, if 'end', mustmatch its end.
If mustMatch=True, raises an IndexError when no node if found; default is False, as used in __getitem__().
Assume type of input is controlled ahead of call (as in __getitem__()) so does not check for efficiency saving.
"""
if self.__lab==n: return self
for c in self.__children:
if side=='start':
if c.label().startswith(n):
return c
elif side=='end':
if c.label().endswith(n):
return c
else:
raise ValueError, "incorrect value for 'side' parameter"
t=c.labelgetnode(n, mustMatch=mustMatch, side=side) # (recursive function)
if t:
return t
if not mustMatch: return None
else: return IndexError, "no node label %s matched '%s' in tree"%(side, n)
def prepare_fast_lookup(self):
"""create a static dictionary (i.e. cache) of labels to their respective nodes to avoid full tree search on later __getitem__ calls
Ignores empty-string labels.
Needs refreshing whenever editiing the tree structure or labelling, otherwise will lead to WRONG responses.
"""
dlabel2nodes = {}
for node in self.preordertraversal_generator():
lab = node.__lab
if lab: dlabel2nodes[lab] = node
self.__label2nodes = dlabel2nodes
def __getitem__(self,n):
"""<==> self[n]. Return the Node with input label. If input is an iterable returning labels or node objects, find their MRCA"""
if type(n) is str:
if hasattr(self, '__label2nodes'):
return self.__label2nodes.get(n, self.labelgetnode(n))
return self.labelgetnode(n)
#~ elif type(n) in (tuple, list):
elif hasattr(type(n), '__iter__'):
return self.coalesce(n)
else:
raise TypeError, "unexpected type %s for key: %s"%(str(type(n)), repr(n))
return None
def __getattr__(self, attr):
if attr=='boot': return self.__boot
elif attr=='l': return self.__l
elif attr in ['lab']: return self.__lab
elif attr in ['fat', 'father']: return self.__father
elif attr=='children': return self.__children
else: raise AttributeError
def __str__(self):
"""Return printable string in Newick format of the sub-tree defined by the Node."""
return self.newick()
def __repr__(self):
"""Return printable string naming the class, number of children nodes and label the Node."""
return "<%s object: %d nodes; label:%s>"%(str(type(self)).split("'")[1], len(self), repr(self.label()))
def __iter__(self):
#~ return self.generator()
return self.preordertraversal_generator()
# Python 3:
def __bool__(self):
"""Boolean value of an instance of this class (True).
NB: If this method is not defined, but ``__len__`` is, then the object
is considered true if the result of ``__len__()`` is nonzero. We want
Node instances to always be considered True.
(stolen from BioPython)
__len__() points to get_all_children() which should always return a
non-empty list with at least [self]; this saves time though.
"""
return True
# Python 2:
__nonzero__ = __bool__
def __len__(self):
return len(self.get_all_children())
def __copy__(self):
"""produces a shalow copy of the node, with new deep copies of all attributes but shalow copies of the links to father and children
the new copy keep knowing where it is located in the tree, but the other nodes in the tree will not know it exists (they know the original [self] instance)
"""
newinstance = self.newnode()
for attr in self.__dict__:
if not attr in ['_Node__children', '_Node__father']:
newinstance.__dict__[attr] = copy.deepcopy(self.__dict__[attr])
elif attr == '_Node__children':
newinstance.__dict__[attr] = copy.copy(self.__dict__[attr]) # create a new list container, but with same element pointers in it
elif attr == '_Node__father':
newinstance.__dict__[attr] = self.__dict__[attr] # just use the same pointer
return newinstance
def deepcopybelow(self, keep_lg=False, add_ref_attr=False, shallow_copy_attr=[]):
"""return similar subtree object than pop(self) result, but as a copy (not the same object, and the original instance remains untouched attached to the tree).
compared to regular __copy__(), avoids duplicating the tree above. On a root node and with keep_lg=True, equivalent to copy.deepcopy(self).
"""
newinstance = self.newnode()
for attr in self.__dict__:
if (attr == '_Node__father') or (attr == '_Node__l' and not keep_lg):
newinstance.__dict__[attr] = None
elif attr in shallow_copy_attr:
newinstance.__dict__[attr] = self.__dict__[attr]
else:
newinstance.__dict__[attr] = copy.deepcopy(self.__dict__[attr])
for child in newinstance.get_children():
# associate the copied children with the newinstance, rather than an unlinked deep copy of their father
child.change_father(newinstance, newlen=child.lg(), newboot=child.bs())
if add_ref_attr:
# tag copy nodes with reference to their original counterpart (add an attribute)
newnodes = newinstance.get_all_children()
oldnodes = self.get_all_children()
# rely on the fact that object are structured equally so that ordering of nodes in lists generated by pre-order traversal will be the same
for i in range(len(newnodes)):
newnodes[i].ref = oldnodes[i]
return newinstance
def deepcopyabove(self, shallow_copy_attr=[]):
"""return similar root-side tree object than self after calling pop(), but as a copy (not the same object, and the original instance remains untouched with subtree attached).
compared to regular __copy__(), avoids duplicating the tree below.
"""
newinstance = self.newnode()
for attr in self.__dict__:
if attr == '_Node__children':
newinstance.__dict__[attr] = []
elif attr in shallow_copy_attr:
newinstance.__dict__[attr] = self.__dict__[attr]
else:
newinstance.__dict__[attr] = copy.deepcopy(self.__dict__[attr])
# associate the copied father with the newinstance, rather than with an unlinked deep copy of its child
oldfat = newinstance.go_father()
if oldfat:
# find the apprropriate child toremove
children = oldfat.get_children()
appro = []
for i in range(len(children)):
child = children[i]
if child.label()==newinstance.label() and child.lg()==newinstance.lg() and child.bs()==newinstance.bs(): appro.append(i)
if len(appro)<1: raise IndexError, "could find no appropriate deep copy of the node to replace with the new instance"
elif len(appro)>1: raise IndexError, "found several candidate deep copies of the node to replace with the new instance, cannot choose"
else:
if children[i] is newinstance:
print "deep copy of the child is already the newinstance; surprising, but that's fine: pass"
else:
oldfat.rm_child(children[i])
oldfat.add_child(newinstance)
return newinstance
# defining a cmp method is dangerous as many tests involve testing identity of nodes using '==' operator
# (and by default comparing values yielded by hash(object)), instead of clean 'is' operator
#~ def __cmp__(self, other):
#~ """to sort sequences with deep nodes first (should prefer the use of sort(key=fun()))"""
#~ sd = self.depth()
#~ so = other.depth()
#~ if sd > so: return -1
#~ elif sd == so: return 0
#~ else: return 1
#~ def generator(self):
#~ children = self.get_all_children()
#~ for child in children:
#~ yield child
def sequence(self):
"""Return the Sequence of the Node."""
return self.__seq
def lg(self):
"""Return the length of the edge to the father."""
return self.__l
def bs(self):
"""Return the bootstrap value at Node."""
return self.__boot
def label(self, comments=False):
"""Return the label."""
#~ return self.__lab
text = self.__lab
if comments==True and self.__comment: text += '[%s]'%self.__comment
return text
def comment(self):
"""Return the comment."""
return self.__comment
def getattr_down_n_nodes(self, attr, n, ommitself=False, stopatnodes=[], leafval='same'):
"""return a list of the chosen attribute value for all branches/nodes down the tree from the focal node.
Operate reccursively over n children (maximum, stops at leaves) if n >= 0,
or until exploration of all leaves if n < 0.
"""
lval = []
if not ommitself:
if leafval=='same': val = getattr(self, attr)
else: val = leafval if self.is_leaf() else getattr(self, attr)
if callable(val): lval.append(val())
else: lval.append(val)
if n!=0:
for sn in self.__children:
if not (sn in stopatnodes):
lval += sn.getattr_down_n_nodes(attr, n-1, stopatnodes=stopatnodes)
return lval
def preordertraversal_generator(self):
"""recursively yield all the nodes below the Node, including itself, in a pre-order traversal"""
yield self
for child in self.__children:
for rechild in child.preordertraversal_generator():
yield rechild
def get_preordertraversal_children(self):
"""Return the list of all nodes below the Node, including itself, in a pre-order traversal"""
return list(self.preordertraversal_generator())
#~ def get_all_children(self):
#~ """Return the list of all nodes below the Node, including itself, in a pre-order traversal"""
#~ a=[self]
#~ for i in self.__children:
#~ a+=i.get_all_children()
#~ return a
# rather use an alias for code consistency
get_all_children = get_preordertraversal_children
def get_all_children_cache(self, cachedict={}):
"""Return the list of all nodes below the Node, including itself, in a pre-order traversal"""
a=[self]
for i in self.__children:
if i in cachedict:
a+=cachedict[i]
else:
a+=i.get_all_children_cache(cachedict)
return a
def postordertraversal_generator(self, deepfirst=False, deeplast=False):
"""recursively yield all the nodes below the Node, including itself in a post-order traversal"""
children = self.__children
if deepfirst: children.sort(key=lambda x: x.max_leaf_depth())
elif deeplast: children.sort(key=lambda x: -1*x.max_leaf_depth())
for child in children:
for rechild in child.postordertraversal_generator():
yield rechild
yield self
def get_postordertraversal_children(self, deepfirst=False, deeplast=False):
"""Return the list of all nodes below the Node, including itself, in a post-order traversal"""
#~ a=[]
#~ children = self.__children
#~ if deepfirst: children.sort(key=lambda x: x.max_leaf_depth())
#~ elif deeplast: children.sort(key=lambda x: -1*x.max_leaf_depth())
#~ if children!=[]:
#~ for i in children:
#~ a+=i.get_postordertraversal_children()
#~ a += [self]
#~ return a
return list(self.postordertraversal_generator(deepfirst=deepfirst, deeplast=deeplast))
def midordertraversal_generator(self, righttfirst=False):
"""recursively yield all the nodes below the Node, including itself in a mid-order traversal;
i.e. the left child first, then the Node, then the other children from left to right
"""
children = self.__children
ordch = range(len(children))
if righttfirst: ordch.reverse() # use list of index to not change order of self.__children list!
if children:
firstch = children[ordch[0]]
for rechild in firstch.midordertraversal_generator(righttfirst=righttfirst):
yield rechild
yield self
for i in ordch[1:]:
for rechild in children[i].midordertraversal_generator(righttfirst=righttfirst):
yield rechild
def get_midordertraversal_children(self, righttfirst=False):
"""Return the list of all nodes below the Node, including itself, in a mid-order traversal"""
#~ a=[]
#~ children = self.__children
#~ ordch = range(len(children))
#~ if righttfirst: ordch.reverse()
#~ if children:
#~ firstch = children[ordch[0]]
#~ a+=firstch.get_midordertraversal_children(righttfirst=righttfirst)
#~ a += [self]
#~ for i in ordch[1:]:
#~ a+=children[i].get_midordertraversal_children(righttfirst=righttfirst)
#~ return a
return list(self.midordertraversal_generator(righttfirst=righttfirst))
def sorted_generator(self, order=1):
"""yield all the nodes below the Node, including itself, based on a specified order
order=-1: ordered by increasing depth (i.e. decreasing node distance from root).
order= 0: pre-oder traversal (classic root-to-leaves exploration)
order= 1: ordered by decreasing depth (i.e. increasing node distance from root).
order= 2: post-oder traversal (exploration of each group of leaves, then the nodes above ; compatible with Count's rate files node enumeration)
order= 3: post-oder traversal (exploration of each group of leaves, then the nodes above ; compatible with Count's rate files node enumeration) with always exploring the deepest node first
order= 4: mid-order (in-order) traversal (first the left child(ren), then the father, then the right child(ren))
order= 5: mid-order (in-order) traversal (first the right child(ren), then the father, then the left child(ren))
order= 6: post-oder traversal (exploration of each group of leaves, then the nodes above ; compatible with Count's rate files node enumeration) with always exploring the deepest node last
"""
if order in [-1, 1]:
a = self.get_all_children()
a.sort(key=lambda x: x.depth()*order)
for n in a: yield n
elif order == 0:
for n in self.preordertraversal_generator(): yield n
elif order == 2:
for n in self.postordertraversal_generator(): yield n
elif order == 3:
for n in self.postordertraversal_generator(deepfirst=True): yield n
elif order == 4:
for n in self.midordertraversal_generator(righttfirst=False): yield n
elif order == 5:
for n in self.midordertraversal_generator(righttfirst=True): yield n
elif order == 6:
for n in self.postordertraversal_generator(deeplast=True): yield n
def get_sorted_children(self, order=1):
"""Return the list of all nodes below the Node, including itself
order=-1: ordered by increasing depth (i.e. decreasing node distance from root).
order= 0: pre-oder traversal (classic root-to-leaves exploration)
order= 1: ordered by decreasing depth (i.e. increasing node distance from root).
order= 2: post-oder traversal (exploration of each group of leaves, then the nodes above ; compatible with Count's rate files node enumeration)
order= 3: post-oder traversal (exploration of each group of leaves, then the nodes above ; compatible with Count's rate files node enumeration) with always exploring the deepest node first
order= 4: mid-order traversal (first the left child(ren), then the father, then the right child(ren))
order= 5: mid-order traversal (first the right child(ren), then the father, then the left child(ren))
order= 6: post-oder traversal (exploration of each group of leaves, then the nodes above ; compatible with Count's rate files node enumeration) with always exploring the deepest node last
"""
if order in [-1, 0, 1]:
a = self.get_all_children()
a.sort(key=lambda x: x.depth()*order)
elif order == 2:
a = self.get_postordertraversal_children()
elif order == 3:
a = self.get_postordertraversal_children(deepfirst=True)
elif order == 4:
a = self.get_midordertraversal_children(righttfirst=False)
elif order == 5:
a = self.get_midordertraversal_children(righttfirst=True)
elif order == 6:
a = self.get_postordertraversal_children(deeplast=True)
return a
def get_comtemporary_branches(self, t, dp_drootdist=None, tagwithlabels=True):
"""retrieves all the branches of the tree encompassing the time t from the root"""
if not dp_drootdist: drootdist={}
else: drootdist=dp_drootdist
cb = []
if tagwithlabels:
stag = self.label()
if self.__father: ftag = self.__father.label()
else: ftag = None
else:
stag = self
ftag = self.__father
# dynamic programing to obtain node-to-root distances as the tree is explored; avoids individual parcours of each node's lineage
if stag in drootdist: raise IndexError, "stag '%s' already recorded in drootdist"%stag # should not happen, test could be removed to improve performance
d = drootdist.setdefault(stag, drootdist.get(ftag, 0)+self.__l)
if d > t:
cb += [self]
else:
for c in self.__children:
# recursive filling of the list
cb += c.get_comtemporary_branches(t, dp_drootdist=drootdist, tagwithlabels=tagwithlabels)
return cb
def get_branch_chronology(self, ltimes, drootdist={}, dtimeindexes={}, tagwithlabels=True):
"""for each time t in list 'ltimes', retrieves all the branches of the tree encompassing the time t from the root; return a dictionary of times to lists of branches"""
dtimebranches = {}
if tagwithlabels:
stag = self.label()
if self.__father: ftag = self.__father.label()
else: ftag = None
else:
stag = self
ftag = self.__father
# dynamic programing to obtain node-to-root distances as the tree is explored; avoids individual parcours of each node's lineage
d = drootdist.setdefault(stag, drootdist.get(ftag, 0)+self.__l)
# similar for indexes of the time list; avoids exploring times that are known to be younger than the node's father (and hence than the node)
itime = dtimeindexes.get(ftag, 0)
while itime<len(ltimes):
if d > ltimes[itime]:
dtimebranches.setdefault(ltimes[itime], []).append(self)
itime += 1
else:
break
dtimeindexes[stag] = itime
for c in self.__children:
# recursive filling of the dict
tree2.updateappend(dtimebranches, c.get_branch_chronology(ltimes, drootdist=drootdist, dtimeindexes=dtimeindexes, tagwithlabels=tagwithlabels))
return dtimebranches
def get_all_parents(self):
"""Return the list of all nodes above the Node, excluding itself, ordered by increasing depth (i.e. decreasing node distance from root)."""
lparents = []
f = self.go_father()
while f:
lparents.append(f)
f = f.go_father()
return lparents
def sort(self, lnodes, order=1, apendOutSelf=True, labels=False):
"""sorts a list of nodes contained in self in different orders (cf. Node.get_sorted_children())"""
orderedchildren = self.get_sorted_children(order=order)
ln = copy.copy(lnodes)
l = []
for oc in orderedchildren:
if not labels: o = oc
else: o = oc.label()
while o in ln :
l.append(ln.pop(ln.index(o)))
if apendOutSelf:
l += ln
elif lnodes:
raise IndexError, "nodes in %s are not in %s (self)"%(str([n.label() for n in lnodes]), self.label())
return l
##############################################################
### HOGENOM Species/Protein identifiers correspondency methods
def get_prot_labels_from_aln(self, aln):
outtree = copy.deepcopy(self)
## retrieving of full protein labels from alignments
lleaves = self.get_leaf_labels()
dspe_prot = {}
faln = open(aln, 'r')
aln = faln.read()
faln.close()
for leaf in lleaves:
s = re.compile(leaf+"_._.+?[ \n]")
ss = s.search(aln)
sprot = ss.group().rstrip(" \n")
dspe_prot[leaf] = sprot
for leaf in lleaves:
outtree[leaf].edit_label(dspe_prot[leaf])
return outtree
def listSpecies(self, llab=None, ignoreTransfers=False, asSet=True, splitparam=('_',1), **kw):
"""Return list of identifiers of species present in leaves
by default, split is done at 1st occurence of '_'.
I one assume labels are under the HOGENOM model SPECIES_NUMREPLICON_PROTID -> return SPECIES.
Separator string and number in string can be specified otherwise.
can be restricted to a particular species set.
"""
lspe = []
lleaves = self.get_leaves()
if isinstance(self, tree2.GeneTree) and ignoreTransfers:
ltransleaves = []
for node in self:
if node.transfer():
ltransleaves += self.idgetnode(node.transferchild()).get_leaves()
lleaves = list(set(lleaves) - set(ltransleaves))
for leaf in lleaves:
if (not llab) or (leaf.label() in llab):
spe = leaf.label().split(splitparam[0], splitparam[1])[0]
lspe.append(spe)
if asSet: lspe = list(set(lspe))
return lspe
def dictLeafLabelsToSpecies(self, llab=None, splitparam=('_',1)):
"""Return dictionary of leaf labels (under the HOGENOM model SPECIES_NUMREPLICON_PROTID) to species identifiers
can be restricted to a particular species set
Needs SPECIES[_ANYTHING] type (HOGENOM-type) labels at leaves."""
d = {}
for leaf in self.get_leaves():
if (not llab) or (leaf.label() in llab):
spe = leaf.label().split(splitparam[0], splitparam[1])[0]
d[leaf.label()] = spe
return d
def dictSpeciesToLeafLabels(self, lspe=None, catValues=False):
"""Return dictionary of species identifiers (under the HOGENOM model SPECIES_NUMREPLICON_PROTID) to leaf labels
can be restricted to a particular leaf label set
Needs SPECIES[_ANYTHING] type (HOGENOM-type) labels at leaves."""
if not catValues: d = {}
else: d = []
for leaf in self.get_leaves():
spe = leaf.label().split('_',1)[0]
if (not lspe) or (spe in lspe):
if not catValues: d[spe] = d.setdefault(spe, []) + [leaf.label()]
else: d.append(leaf.label())
return d
def dictLeavesToSpecies(self, llab=None):
"""Return dictionary of Node objects (leaf) (under the HOGENOM model SPECIES_NUMREPLICON_PROTID) to species identifiers
can be restricted to a particular species set
Needs SPECIES[_ANYTHING] type (HOGENOM-type) labels at leaves."""
d = {}
for leaf in self.get_leaves():
if (not llab) or (leaf.label() in llab):
spe = leaf.label().split('_',1)[0]
d[leaf] = spe
return d
def dictSpeciesToLeaves(self, lspe=None, catValues=False):
"""Return dictionary of species identifiers (under the HOGENOM model SPECIES_NUMREPLICON_PROTID) to Node objects (leaf)
can be restricted to a particular leaf label set
Needs SPECIES[_ANYTHING] type (HOGENOM-type) labels at leaves."""
if not catValues: d = {}
else: d = []
for leaf in self.get_leaves():
spe = leaf.label().split('_',1)[0]
if (not lspe) or (spe in lspe):
if not catValues: d[spe] = d.setdefault(spe, []) + [leaf]
else: d.append(leaf)
return d
#####################################################
############## methods for editing object atributes:
def __setattr__(self, name, value):
self.__dict__[name] = value
def set_lg(self,l):
"""Set the length of the edge to the father to $2 if it is >=0."""
try:
lg = float(l)
except TypeError, e:
if l==None:
lg = None
else:
raise TypeError, e
self.__l = lg
def set_bs(self, bs):
try:
boot = float(bs)
except TypeError, e:
if bs==None:
boot = None
else:
raise TypeError, e
self.__boot = boot
def add_label(self, lab):
try:
label = str(lab)
except TypeError, e:
if lab==None:
label = None
else:
raise TypeError, e
self.__lab = label
def edit_label(self, label, mode="write", sep=", "):
"""edits the label attached to the node"""
if mode in ["w", "write"] or (self.__lab in ["", None]):
self.__lab = str(label)
elif mode in ["a", "append"]:
self.__lab += sep+str(label)
else:
raise ValueError
def edit_all_labels(self, label, mode="append", sep="-"):
"""edits the label attached to all the nodes below the node, including itself; defaults to appending a string"""
for node in self:
node.edit_label(label, mode=mode, sep=sep)
def edit_comment(self, comment, mode="write"):
"""edits the comment attached to the node"""
if mode in ["w", "write"] or not self.__comment:
self.__comment = str(comment)
elif mode in ["a", "append"]:
self.__comment += ', '+str(comment)
else:
raise ValueError
@staticmethod
def collapse(f, c, gf=None, tellReplacingNode=False, keepRefToFather=False, silent=True):
"""how to operate fusion of two given nodes, either by removing the intermediate node (newpop) or mutating the intermediate into the top one (oldpop)"""
def oldpop(f, c):
""" old implementation: `mutation' of the father node into its non-poped child"""
if not silent: print "oldpop(%s, %s)"%(str(f.label()), str(c.label()))
df = f.__dict__
dc = c.__dict__
for attr in df:
if attr=='_Node__l':
f.set_lg(sumlen(f.lg(),c.lg()))
elif attr=='_Node__boot':
f.set_bs(max(f.bs(), c.bs()))
elif attr=='_Node__children':
f._Node__children = []
for gc in c.get_children():
f.link_child(gc, newlen=gc.lg(), newboot=gc.bs(), silent=silent)
if gc.go_father() is c: raise IndexError
c._Node__children = []
elif attr=='_Node__father':
pass
else:
df[attr] = dc[attr]
del c
def newpop(gf, f, c):
""" new implementation : no `mutation' of the node object, the father node is disconnected from above and below, and the child is reconnected to the grand-father"""
if not silent: print "newpop(%s, %s, %s)"%(str(gf.label()), str(f.label()), str(c.label()))
gf.add_child(c, silent=silent)
c.change_father(gf, newlen=sumlen(f.lg(),c.lg()), newboot=max(f.bs(), c.bs()), silent=silent)
gf.rm_child(f, silent=silent)
f.rm_child(c, silent=silent)
if keepRefToFather or (not gf):
# poping under the root or another node to keep referenced
if tellReplacingNode:
return (c.label(), f.label())
else:
oldpop(f, c)
else:
if tellReplacingNode:
return (f.label(), c.label())
else:
newpop(gf, f, c)
return None
def pop(self, name, keepRefToFather=False, tellReplacingNode=False, noCollapse=False, verbose=False):
"""Return the Node with this name, and removes it from the tree.
New implementation: father node is completely disconected from the tree, its other child are grafted to the grand-father
if the father is the root (no grand-father), uses old implementation
Old implementation: If node removal leaves only one node under the father node, the father node takes all the attributes from the brother node.
> used for poping node under the root or under a node the user would keep referenced
In both implementations, removing a node leads to merge two branches: their lengths are summed and the highest support is kept.
Caution : function used in Node.reRoot() (coded while using old implementation).
if tellReplacingNode is True, will not change the tree but will return the pair of node to be collapsed if a node is poped as a tuple (removed_node, staying_node).
if noCollapse is True, the father node of the node to pop is only deleted if it ends up with no child leading to an original leaf.
Otherwise, the node is kept with up to one child, leading to branches not being fused / nodes not being collapsed into a single one
but be made of several segment delimited by nodes. Useful in the xODT/ALE representation of speciation-loss events in reconciled gen trees.
"""
# find node to pop 'np'
if isinstance(name, type(self)):
np = name
elif isinstance(name, str):
np = self[name]
else:
raise TypeError, "element to be poped must be a %s object or a string (label) corresponding to a node in self"%type(self)
# NB: if types are right but no node from self is matched, None is returned
if np:
f=np.go_father()
if f:
if not tellReplacingNode:
f.unlink_child(np)
gf = f.go_father()
if noCollapse:
if verbose: print "noCollapse on node %s"%str(f.label())
if f.nb_children()==0:
# a lineage leading to false leaf (i.e. a terminal node that is not a true tip) has been created;
# go up the lineage and cut off fals leaf nodes until meeting a node with more than 1 child (i.e. with another descendant lineage than the false leaf)
while gf and gf.nb_children()==1:
f = gf
gf = f.go_father()
if gf:
gf.unlink_child(f) # (great)-grandfather node 'gf' gets rid of the leafless lineage
else:
pass # reached the root; proceed as usual (collapse nodes with oldpop)
else:
return np # stop here
else:
# find 'c', the brother node of 'np', to collapse it with f
if f.nb_children()==1 or tellReplacingNode: # edit rule (tellReplacingNode and f.nb_children()==2) into just (tellReplacingNode) to restore support for multifurcated trees... must test if that does not lead to bugs
for child in f.get_children():
if child is not np:
c = child
break
else:
raise IndexError, "where is the brother of node to pop 'np'?"
collapsed = self.collapse(f, c, gf, tellReplacingNode=tellReplacingNode, silent=(not verbose))
if tellReplacingNode: return collapsed
else:
if tellReplacingNode: return (np.label(), np.label())
return np
def dictCollapsed(self, prunedleaves, everyStep=False, new2old=True, silent=True):
"""check the pattern of node collapsing if one would prune those leaves"""
dcollapsed = {} # d[staying] = removed
for leaf in prunedleaves:
collapsednodes = self.pop(leaf, tellReplacingNode=True)
# follow the series of pruning steps
while collapsednodes[0] in dcollapsed.values():
collapsednodes = self.pop(collapsednodes[0], tellReplacingNode=True)
dcollapsed[collapsednodes[1]] = collapsednodes[0]
return dcollapsed
def restrictToLeaves(self, lleaves, useSpeDict=False, force=False, returnCopy=True):
"""returns a copy of the tree restricted to the input leaf set"""
if not returnCopy:
st = self
else:
c = copy.deepcopy(self)
# maps to the common ancestor of all leaves
mrca = c.map_to_node(lleaves, useSpeDict=useSpeDict, force=force)
# extract this subtree
if mrca is c: st = c
elif mrca: st = c.pop(mrca)
else: return None
# remove the other leaves
for leaflab in st.get_leaf_labels():
if not useSpeDict: leaf = leaflab
else: leaf = leaflab.split('_')[0]
if leaf not in lleaves: st.pop(leaflab)
return st
def complete_label(self, prefix='N', labels=None, force=True):
"""gives label to an internal node given a set of pre-existing labels in the tree"""
if not labels: labels = []
children = self.get_sorted_children()
for c in children: # first builds a list of existing labels to avoid redundancy of new names
cl = c.label()
if (cl!="") and (not cl in labels):
labels.append(cl)
n = 1
if self.label()=="" or force:
lab = "%s%d"%(prefix, n)
while lab in labels:
n += 1
lab = "%s%d"%(prefix, n)
self.add_label(lab)
def complete_internal_labels(self, prefix='N', labels=None, ffel=False, force=False, exclude=[], excludeLeaves=False, onlyLeaves=False, silent=True, order=1, fast=False):
"""give a numeric label following a specified order to ALL nodes that lack a label; naturally skips editing the leaf that should be already labelled.
a list of nodes of self to not edit can be passed as 'exclude'.
a list of labels can be passed as 'labels' to extend the list of labels to avoid (in addition to the labels already present in the tree).
if 'force' is True, all nodes are renamed, including leaves (unless 'excludeLeaves' is set to True).
if 'fast' is True, no lookup of what is already present in the tree is done;
safer used in combination with 'force=True' (and 'excludeLeaves=True'), as provided by combo option 'ffel'.
"""
if isinstance(order, list):
# orderred node list is given
children = order
else:
children = self.sorted_generator(order=order)
if not (fast or ffel):
# first builds a list of existing labels to avoid redundancy of new names
if not labels: labs = []
else: labs = labels
for c in children:
cl = c.label()
if (cl not in ["", None]) and (not cl in labs):
labs.append(cl)
n = 1
lab = "%s%d"%(prefix, n)
for c in children:
if c in exclude: continue
if (excludeLeaves or ffel) and c.is_leaf(): continue
if onlyLeaves and not c.is_leaf(): continue
if (force or ffel) or (c.label() in ["", None]):
if (fast or ffel):
lab = "%s%d"%(prefix, n)
n += 1
else:
while lab in labs:
n += 1
lab = "%s%d"%(prefix, n)
c.add_label(lab)
if not (fast or ffel): labs.append(lab)
n += 1
if not silent: print lab
def check_unique_labelling(self):
nodelabs = self.get_children()
assert len(set(nodelabs))==len(nodelabs)
#####################################################
############## methods for access to object's child atributes:
def get_children(self):
"""Return the list of direct children of the Node"""
return self.__children
def get_target_child(self, node=None, label=None, nodeid=None):
if node:
n = node
elif label:
n = self[label]
elif nodeid:
if isinstance(self, tree2.AnnotatedNode):
n = self.idgetnode(nodeid)
else:
raise ValueError, "need an AnnotatedNode instance to fetch a node from an nodeid"
else:
raise ValueError, "need a clue (Nde instance, label or nodeid) to find the target child"
for c in self.get_children():
if n in c:
return c
def nb_children(self):
"""Return the number of direct children."""
return len(self.__children)
def nb_all_children(self):
"""Return the number of descendants."""
return len(self.get_all_children())
def children_labels(self):
"""Return the list of direct child labels."""
llc = []
for c in self.__children:
llc.append(c.label())
return llc
def children_comments(self):
"""Return the list of child comments."""
lcc = []
for c in self.__children:
lcc.append(c.comment())
return lcc
def get_children_labels(self, sorted=1):
"""Return the list of labels of all nodes below the Node, including itself"""
if sorted: