Skip to content

Commit

Permalink
Additions and fixings to root/unroot-ing functions.
Browse files Browse the repository at this point in the history
Add root_at() (different from set_outgroup()), base set_outgroup() on it,
base unroot() on it too (and by doing so, also fix some problems it had).
  • Loading branch information
jordibc committed Nov 27, 2023
1 parent 6f373f3 commit b0c39a1
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 50 deletions.
84 changes: 66 additions & 18 deletions ete4/core/operations.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -16,36 +16,84 @@ def sort(tree, key=None, reverse=False):
node.children.sort(key=key, reverse=reverse)


def set_outgroup(node, bprops=None):
"""Reroot the tree at the given outgroup node.
def root_at(node, bprops=None):
"""Set the given node as the root of the tree.
The original root node will be used as the new root node, so any
reference to it in the code will still be valid.
:param node: Node where to set root (future first child of the root).
:param node: Node to set as root. Its reference will be lost.
:param bprops: List of branch properties (other than "dist" and "support").
"""
old_root = node.root
positions = node.id # child positions from root to node (like [1, 0, ...])
root = node.root

assert_root_consistency(old_root, bprops)
assert node != old_root, 'cannot set the absolute tree root as outgroup'
if root is node:
return # nothing to do!

# Make a new node to replace the old root.
replacement = old_root.__class__() # could be Tree() or PhyloTree(), etc.
assert_root_consistency(root, bprops)

children = old_root.remove_children()
replacement.add_children(children) # take its children
positions = node.id # child positions from root to node (like [1, 0, ...])

# Now we can insert the old root, which has no children, in its new place.
insert_intermediate(node, old_root, bprops)
interchange_references(root, node) # root <--> node
old_root = node # now "node" points to where the old root was

root = replacement # current root, which will change in each iteration
current_root = old_root # current root, which will change in each iteration
for child_pos in positions:
root = rehang(root, child_pos, bprops)
current_root = rehang(current_root, child_pos, bprops)

if len(old_root.children) == 1:
join_branch(old_root)


def interchange_references(node1, node2):
"""Interchange the references of the given nodes."""
# node1 will point where node2 was, and viceversa.
if node1 is node2:
return

# Interchange properties.
node1.props, node2.props = node2.props, node1.props

# Interchange children.
children1 = node1.remove_children()
children2 = node2.remove_children()
node1.add_children(children2)
node2.add_children(children1)

# Interchange parents.
up1 = node1.up
up2 = node2.up
pos1 = up1.children.index(node1) if up1 else None
pos2 = up2.children.index(node2) if up2 else None

if up1 is not None:
up1.children.pop(pos1)
up1.children.insert(pos1, node2)

if up2 is not None:
up2.children.pop(pos2)
up2.children.insert(pos2, node1)

node1.up = up2
node2.up = up1


def set_outgroup(node, bprops=None):
"""Change tree so the given node is set as outgroup.
The original root node will be used as the new root node, so any
reference to it in the code will still be valid.
:param node: Node to set as outgroup (future first child of the root).
:param bprops: List of branch properties (other than "dist" and "support").
"""
assert not node.is_root, 'cannot set the absolute tree root as outgroup'
assert_root_consistency(node.root, bprops)

intermediate = node.__class__() # could be Tree() or PhyloTree(), etc.
insert_intermediate(node, intermediate, bprops)

if len(replacement.children) == 1:
join_branch(replacement)
root_at(intermediate, bprops)


def assert_root_consistency(root, bprops=None):
Expand All @@ -62,7 +110,7 @@ def assert_root_consistency(root, bprops=None):


def rehang(root, child_pos, bprops):
"""Rehang node on its child at position child_pos and return it."""
"""Rehang root on its child at position child_pos and return it."""
# root === child -> child === root
child = root.pop_child(child_pos)

Expand Down
41 changes: 17 additions & 24 deletions ete4/core/tree.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -1067,35 +1067,28 @@ cdef class Tree(object):
ops.populate(self, size, names, model, dist_fn, support_fn)

def set_outgroup(self, node, bprops=None):
"""Reroot the tree at the given outgroup node."""
"""Change tree so the given node is set as outgroup.
The original root node will be used as the new root node, so any
reference to it in the code will still be valid.
:param node: Node to set as outgroup (future first child of the root).
:param bprops: List of branch properties (other than "dist" and "support").
"""
node = self[node] if type(node) == str else node # translates if needed
ops.set_outgroup(node, bprops)

def unroot(self, mode='legacy'):
"""Unroot current node.
def unroot(self):
"""Unroot the tree, that is, make the root not have 2 children.
This function is expected to be used on the absolute tree root
node, but it can be also be applied to any other internal
node. It will convert a split into a multifurcation.
:param mode: The value can be "legacy" or "keep". If value is
"keep", it keeps the distance between the leaves by adding
the distance associated to the deleted edge to the
remaining edge. Otherwise that distance is just dropped.
The convention in phylogenetic trees is that if the root has 2
children, it is a "rooted" tree (the root is a real ancestor).
Otherwise (typically a root with 3 children), the root is just
an arbitrary place to hang the tree.
"""
if not (mode == 'legacy' or mode == 'keep'):
raise ValueError("The value of the mode parameter must be 'legacy' or 'keep'")
if len(self.children)==2:
if not self.children[0].is_leaf:
if mode == "keep":
self.children[1].dist+=self.children[0].dist
self.children[0].delete()
elif not self.children[1].is_leaf:
if mode == "keep":
self.children[0].dist+=self.children[1].dist
self.children[1].delete()
else:
raise TreeError("Cannot unroot a tree with only two leaves")
assert self.is_root, 'unroot only makes sense from the root node'
if len(self.children) == 2:
ops.root_at(self.children[0])

def show(self, layout=None, tree_style=None, name="ETE"):
"""Start an interactive session to visualize the current node."""
Expand Down
11 changes: 3 additions & 8 deletions tests/test_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -903,14 +903,9 @@ def test_rooting_distances(self):
self.assertEqual(t.children[1].dist, 5.0)

def test_unroot(self):
t = Tree("(('a':0.5, 'b':0.5):0.5, ('c':0.2, 'd':0.2):0.8):1;" )
t2 = Tree("(('a':0.5, 'b':0.5):0.5, ('c':0.2, 'd':0.2):0.8):1;" )
t.unroot(mode="keep")
with self.assertRaises(ValueError):
t.unroot(mode="new")
t2.unroot(mode="legacy")
self.assertEqual("((c:0.2,d:0.2):1.3,a:0.5,b:0.5);", t.write())
self.assertEqual("((c:0.2,d:0.2):0.8,a:0.5,b:0.5);", t2.write())
t = Tree('((a:0.5,b:0.5):0.5,(c:0.2,d:0.2):0.8);')
t.unroot()
self.assertEqual('(a:0.5,b:0.5,(c:0.2,d:0.2):1.3);', t.write())

def test_tree_navigation(self):
t = Tree('(((A,B)H,C)I,(D,F)J)root;', parser=1)
Expand Down

0 comments on commit b0c39a1

Please sign in to comment.