Source code for dendropy.datamodel.treemodel._node

#! /usr/bin/env python
# -*- coding: utf-8 -*-

from io import StringIO
from dendropy.utility import deprecate
from dendropy.utility import error
from dendropy.datamodel import basemodel
from dendropy.datamodel.treemodel import _edge

[docs] class Node(basemodel.DataObject, basemodel.Annotable): """ A :term:|Node| on a :term:|Tree|. """
[docs] @classmethod def edge_factory(cls, **kwargs): """ Creates and returns a |Edge| object. Derived classes can override this method to provide support for specialized or different types of edges on the tree. Parameters ---------- kwargs : keyword arguments Passed directly to constructor of |Edge|. Returns ------- |Edge| A new |Edge| object. """ return _edge.Edge(**kwargs)
def __init__(self, **kwargs): """ Keyword Arguments ----------------- taxon : |Taxon|, optional The |Taxon| instance representing the operational taxonomic unit concept associated with this Node. label : string, optional A label for this node. edge_length : numeric, optional Length or weight of the edge subtending this node. """ basemodel.DataObject.__init__(self, label=kwargs.pop("label", None)) self.taxon = kwargs.pop("taxon", None) self.age = None self._edge = None self._child_nodes = [] self._parent_node = None self.edge = self.edge_factory( head_node=self, length=kwargs.pop("edge_length", None) ) if kwargs: raise TypeError("Unsupported keyword arguments: {}".format(kwargs)) self.comments = [] def __copy__(self, memo=None): raise TypeError("Cannot directly copy Edge")
[docs] def taxon_namespace_scoped_copy(self, memo=None): raise TypeError("Cannot directly copy Node")
def __deepcopy__(self, memo=None): return basemodel.Annotable.__deepcopy__(self, memo=memo) # if memo is None: # memo = {} # other = basemodel.Annotable.__deepcopy__(self, memo=memo) # memo[id(self._child_nodes)] = other._child_nodes # for ch in self._child_nodes: # try: # och = memo[id(ch)] # if och not in other._child_nodes: # other._child_nodes.append(och) # except KeyError: # och = copy.deepcopy(ch, memo) # memo[id(chd)] = och # if och not in other._child_nodes: # other._child_nodes.append(och) # return other # return super(Node, self).__deepcopy__(memo=memo) def __hash__(self): return id(self) def __eq__(self, other): # IMPORTANT LESSON LEARNED: if you define __hash__, you *must* define __eq__ return self is other def __repr__(self): return "<{} object at {}: '{}' ({})>".format( self.__class__.__name__, hex(id(self)), self._label, repr(self.taxon) ) def __iter__(self, *args, **kwargs): return self.preorder_iter(*args, **kwargs)
[docs] def preorder_iter(self, filter_fn=None): """ Pre-order iterator over nodes of subtree rooted at this node. Visits self and all descendant nodes, with each node visited before its children. Nodes can optionally be filtered by ``filter_fn``: only nodes for which ``filter_fn`` returns |True| when called with the node as an argument are yielded. Parameters ---------- filter_fn : function object, optional A function object that takes a |Node| object as an argument and returns |True| if the |Node| object is to be yielded by the iterator, or |False| if not. If ``filter_fn`` is |None| (default), then all nodes visited will be yielded. Returns ------- :py:class:`collections.Iterator` [|Node|] An iterator yielding nodes of the subtree rooted at this node in pre-order sequence. """ stack = [self] while stack: node = stack.pop() if filter_fn is None or filter_fn(node): yield node stack.extend(n for n in reversed(node._child_nodes))
[docs] def preorder_internal_node_iter(self, filter_fn=None, exclude_seed_node=False): """ Pre-order iterator over internal nodes of subtree rooted at this node. Visits self and all internal descendant nodes, with each node visited before its children. In DendroPy, "internal nodes" are nodes that have at least one child node, and thus the root or seed node is typically included unless ``exclude_seed_node`` is |True|. Nodes can optionally be filtered by ``filter_fn``: only nodes for which ``filter_fn`` returns |True| when passed the node as an argument are yielded. Parameters ---------- filter_fn : function object, optional A function object that takes a |Node| object as an argument and returns |True| if the |Node| object is to be yielded by the iterator, or |False| if not. If ``filter_fn`` is |None| (default), then all nodes visited will be yielded. exclude_seed_node : boolean, optional If |False| (default), then the seed node or root is visited. If |True|, then the seed node is skipped. Returns ------- :py:class:`collections.Iterator` [|Node|] An iterator yielding the internal nodes of the subtree rooted at this node in pre-order sequence. """ if exclude_seed_node: froot = lambda x: x._parent_node is not None else: froot = lambda x: True if filter_fn: f = lambda x: (froot(x) and x._child_nodes and filter_fn(x)) or None else: f = lambda x: (x and froot(x) and x._child_nodes) or None return self.preorder_iter(filter_fn=f)
[docs] def postorder_iter(self, filter_fn=None): """ Post-order iterator over nodes of subtree rooted at this node. Visits self and all descendant nodes, with each node visited after its children. Nodes can optionally be filtered by ``filter_fn``: only nodes for which ``filter_fn`` returns |True| when called with the node as an argument are yielded. Parameters ---------- filter_fn : function object, optional A function object that takes a |Node| object as an argument and returns |True| if the |Node| object is to be yielded by the iterator, or |False| if not. If ``filter_fn`` is |None| (default), then all nodes visited will be yielded. Returns ------- :py:class:`collections.Iterator` [|Node|] An iterator yielding the nodes of the subtree rooted at this node in post-order sequence. """ # if self._child_nodes: # for nd in self._child_nodes: # for ch in nd.postorder_iter(filter_fn=filter_fn): # yield ch # if filter_fn is None or filter_fn(self): # yield self # return # stack = [(self, False)] # while stack: # node, state = stack.pop(0) # if state: # if filter_fn is None or filter_fn(node): # yield node # else: # stack.insert(0, (node, True)) # child_nodes = [(n, False) for n in node._child_nodes] # child_nodes.extend(stack) # stack = child_nodes ## Prefer `pop()` to `pop(0)`. ## Thanks to Mark T. Holder ## From peyotl commits: d1ffef2 + 19fdea1 stack = [(self, False)] while stack: node, state = stack.pop() if state: if filter_fn is None or filter_fn(node): yield node else: stack.append((node, True)) stack.extend([(n, False) for n in reversed(node._child_nodes)])
[docs] def postorder_internal_node_iter(self, filter_fn=None, exclude_seed_node=False): """ Pre-order iterator over internal nodes of subtree rooted at this node. Visits self and all internal descendant nodes, with each node visited after its children. In DendroPy, "internal nodes" are nodes that have at least one child node, and thus the root or seed node is typically included unless ``exclude_seed_node`` is |True|. Nodes can optionally be filtered by ``filter_fn``: only nodes for which ``filter_fn`` returns |True| when passed the node as an argument are yielded. Parameters ---------- filter_fn : function object, optional A function object that takes a |Node| object as an argument and returns |True| if the |Node| object is to be yielded by the iterator, or |False| if not. If ``filter_fn`` is |None| (default), then all nodes visited will be yielded. exclude_seed_node : boolean, optional If |False| (default), then the seed node or root is visited. If |True|, then the seed node is skipped. Returns ------- :py:class:`collections.Iterator` [|Node|] An iterator yielding the internal nodes of the subtree rooted at this node in post-order sequence. """ if exclude_seed_node: froot = lambda x: x._parent_node is not None else: froot = lambda x: True if filter_fn: f = lambda x: (froot(x) and x._child_nodes and filter_fn(x)) or None else: f = lambda x: (x and froot(x) and x._child_nodes) or None return self.postorder_iter(filter_fn=f)
[docs] def levelorder_iter(self, filter_fn=None): """ Level-order iteration over nodes of subtree rooted at this node. Visits self and all descendant nodes, with each node and other nodes at the same level (distance from root) visited before their children. Nodes can optionally be filtered by ``filter_fn``: only nodes for which ``filter_fn`` returns |True| when called with the node as an argument are visited. Parameters ---------- filter_fn : function object, optional A function object that takes a |Node| object as an argument and returns |True| if the |Node| object is to be yielded by the iterator, or |False| if not. If ``filter_fn`` is |None| (default), then all nodes visited will be yielded. Returns ------- :py:class:`collections.Iterator` [|Node|] An iterator yielding nodes of the subtree rooted at this node in level-order sequence. """ if filter_fn is None or filter_fn(self): yield self remaining = self.child_nodes() while len(remaining) > 0: node = remaining.pop(0) if filter_fn is None or filter_fn(node): yield node child_nodes = node.child_nodes() remaining.extend(child_nodes)
[docs] def level_order_iter(self, filter_fn=None): """ DEPRECATED: Use :meth:`Node.levelorder_iter()` instead. """ deprecate.dendropy_deprecation_warning( message=( "Deprecated since DendroPy 4: 'level_order_iter()' will no longer be" " supported in future releases; use 'levelorder_iter()' instead" ), stacklevel=3, ) return self.levelorder_iter(filter_fn=filter_fn)
[docs] def inorder_iter(self, filter_fn=None): """ In-order iteration over nodes of subtree rooted at this node. Visits self and all descendant nodes, with each node visited in-between its children. Only valid for strictly-bifurcating trees. Nodes can optionally be filtered by ``filter_fn``: only nodes for which ``filter_fn`` returns |True| when called with the node as an argument are yielded. Parameters ---------- filter_fn : function object, optional A function object that takes a |Node| object as an argument and returns |True| if the |Node| object is to be yielded by the iterator, or |False| if not. If ``filter_fn`` is |None| (default), then all nodes visited will be yielded. Returns ------- :py:class:`collections.Iterator` [|Node|] An iterator yielding nodes of the subtree rooted at this node in infix or in-order sequence. """ if len(self._child_nodes) == 0: if filter_fn is None or filter_fn(self): yield self elif len(self._child_nodes) == 2: for nd in self._child_nodes[0].inorder_iter(filter_fn=filter_fn): yield nd if filter_fn is None or filter_fn(self): yield self for nd in self._child_nodes[1].inorder_iter(filter_fn=filter_fn): yield nd else: raise TypeError("In-order traversal only supported for binary trees")
[docs] def leaf_iter(self, filter_fn=None): """ Iterate over all tips or leaves that ultimately descend from this node. Visits all leaf or tip nodes descended from this node. Nodes can optionally be filtered by ``filter_fn``: only nodes for which ``filter_fn`` returns |True| when called with the node as an argument are yielded. Parameters ---------- filter_fn : function object, optional A function object that takes a |Node| object as an argument and returns |True| if the |Node| object is to be yielded by the iterator, or |False| if not. If ``filter_fn`` is |None| (default), then all nodes visited will be yielded. Returns ------- :py:class:`collections.Iterator` [|Node|] An iterator yielding leaf nodes of the subtree rooted at this node. """ if filter_fn: ff = lambda x: x.is_leaf() and filter_fn(x) or None else: ff = lambda x: x.is_leaf() and x or None for node in self.postorder_iter(ff): yield node
[docs] def child_node_iter(self, filter_fn=None): """ Iterator over all nodes that are the (immediate) children of this node. Parameters ---------- filter_fn : function object, optional A function object that takes a |Node| object as an argument and returns |True| if the |Node| object is to be yielded by the iterator, or |False| if not. If ``filter_fn`` is |None| (default), then all nodes visited will be yielded. Returns ------- :py:class:`collections.Iterator` [|Node|] An iterator yielding nodes that have this node as a parent. """ for node in self._child_nodes: if filter_fn is None or filter_fn(node): yield node
[docs] def child_edge_iter(self, filter_fn=None): """ Iterator over all edges that are the (immediate) children of this edge. Parameters ---------- filter_fn : function object, optional A function object that takes a |Edge| object as an argument and returns |True| if the |Edge| object is to be yielded by the iterator, or |False| if not. If ``filter_fn`` is |None| (default), then all edges visited will be yielded. Returns ------- :py:class:`collections.Iterator` [|Edge|] An iterator yielding edges that have this edge as a parent. """ for node in self._child_nodes: if filter_fn is None or filter_fn(node.edge): yield node.edge
[docs] def ancestor_iter(self, filter_fn=None, inclusive=False): """ Iterator over all ancestors of this node. Visits all nodes that are the ancestors of this node. If ``inclusive`` is |True|, ``self`` is returned as the first item of the sequence; otherwise ``self`` is skipped. Nodes can optionally be filtered by ``filter_fn``: only nodes for which ``filter_fn`` returns |True| when passed the node as an argument are yielded. Parameters ---------- filter_fn : function object, optional A function object that takes a |Node| object as an argument and returns |True| if the |Node| object is to be yielded by the iterator, or |False| if not. If ``filter_fn`` is |None| (default), then all nodes visited will be yielded. inclusive : boolean, optional If |True|, includes this node in the sequence. If |False|, this is skipped. Returns ------- :py:class:`collections.Iterator` [|Node|] Iterator over all predecessor/ancestor nodes of this node. """ if inclusive and (filter_fn is None or filter_fn(self)): yield self node = self while node is not None: node = node._parent_node if node is not None and (filter_fn is None or filter_fn(node)): yield node
[docs] def ageorder_iter(self, filter_fn=None, include_leaves=True, descending=False): """ Iterator over nodes of subtree rooted at this node in order of the age of the node (i.e., the time since the present). Iterates over nodes in order of age ('age' is as given by the ``age`` attribute, which is usually the sum of edge lengths from tips to node, i.e., time since present). If ``include_leaves`` is |True| (default), leaves are included in the iteration; if ``include_leaves`` is |False|, leaves will be skipped. If ``descending`` is |False| (default), younger nodes will be returned before older ones; if |True|, older nodes will be returned before younger ones. Parameters ---------- filter_fn : function object, optional A function object that takes a |Node| object as an argument and returns |True| if the |Node| object is to be yielded by the iterator, or |False| if not. If ``filter_fn`` is |None| (defau include_leaves : boolean, optional If |True| (default), then leaf nodes are included in the iteration. If |False|, then leaf nodes are skipped.lt), then all nodes visited will be yielded. descending : boolean, optional If |False| (default), then younger nodes are visited before older ones. If |True|, then older nodes are visited before younger ones. Returns ------- :py:class:`collections.Iterator` [|Node|] Iterator over age-ordered sequence of nodes in subtree rooted at this node. """ # if not descending: # leaves = [nd for nd in self.leaf_iter()] # queued_pairs = [] # in_queue = set() # for leaf in leaves: # age_nd_tuple = (leaf.age, leaf) # queued_pairs.insert(bisect.bisect(queued_pairs, age_nd_tuple), age_nd_tuple) # in_queue.add(leaf) # while queued_pairs: # next_el = queued_pairs.pop(0) # age, nd = next_el # in_queue.remove(nd) # p = nd._parent_node # if p and p not in in_queue: # age_nd_tuple = (p.age, p) # queued_pairs.insert(bisect.bisect(queued_pairs, age_nd_tuple), age_nd_tuple) # in_queue.add(p) # if include_leaves or nd.is_internal(): # yield nd # else: # nds = [(nd.age, nd) for nd in self.preorder_iter()] # nds.sort(reverse=True) # for nd in nds: # if include_leaves or nd[1].is_internal(): # yield nd[1] nds = [nd for nd in self.preorder_iter()] if descending: reverse = True else: reverse = False nds.sort(key=lambda x: x.age, reverse=reverse) for nd in nds: if (include_leaves or nd._child_nodes) and ( filter_fn is None or filter_fn(nd) ): yield nd
[docs] def age_order_iter(self, include_leaves=True, filter_fn=None, descending=False): """ Deprecated: use :meth:`Node.ageorder_iter()` instead. """ deprecate.dendropy_deprecation_warning( message=( "Deprecated since DendroPy 4: 'age_order_iter()' will no longer be" " supported in future releases; use 'ageorder_iter()' instead" ), stacklevel=3, ) return self.ageorder_iter( include_leaves=include_leaves, filter_fn=filter_fn, descending=descending )
[docs] def apply(self, before_fn=None, after_fn=None, leaf_fn=None): r""" Applies function ``before_fn`` and ``after_fn`` to all internal nodes and ``leaf_fn`` to all terminal nodes in subtree starting with ``self``, with nodes visited in pre-order. Given a tree with preorder sequence of nodes of [a,b,i,e,j,k,c,g,l,m,f,n,h,o,p,]:: a / \ / \ / \ / \ / \ / \ / c b / \ / \ / \ / e / f / / \ / / \ / / \ g / h / / \ / \ / / \ i j k l m n o p the following order of function calls results: before_fn(a) before_fn(b) leaf_fn(i) before_fn(e) leaf_fn(j) leaf_fn(k) after_fn(e) after_fn(b) before_fn(c) before_fn(g) leaf_fn(l) leaf_fn(m) after_fn(g) before_fn(f) leaf_fn(n) before_fn(h) leaf_fn(o) leaf_fn(p) after_fn(h) after_fn(f) after_fn(c) after_fn(a) Parameters ---------- before_fn : function object or |None| A function object that takes a |Node| as its argument. after_fn : function object or |None| A function object that takes a |Node| as its argument. leaf_fn : function object or |None| A function object that takes a |Node| as its argument. Notes ----- Adapted from work by Mark T. Holder (the ``peyotl`` module of the Open Tree of Life Project): https://github.com/OpenTreeOfLife/peyotl.git """ stack = [self] while stack: node = stack.pop() if not node._child_nodes: if leaf_fn: leaf_fn(node) # (while node is the last child of parent ...) while (node._parent_node is None) or ( node._parent_node._child_nodes[-1] is node ): node = node._parent_node if node is not None: if after_fn is not None: after_fn(node) else: break else: if before_fn is not None: before_fn(node) stack.extend([i for i in reversed(node._child_nodes)]) return
[docs] def set_child_nodes(self, child_nodes): """ Assigns the set of child nodes for this node. Results in the ``parent_node`` attribute of each |Node| in ``nodes`` as well as the ``tail_node`` attribute of corresponding |Edge| objects being assigned to ``self``. Parameters ---------- child_nodes : collections.Iterable[|Node|] The (iterable) collection of child nodes to be assigned this node as a parent. """ self.clear_child_nodes() # Go through add to ensure book-keeping # (e.g. avoiding multiple adds) takes # place. for nd in child_nodes: self.add_child(nd)
[docs] def set_children(self, child_nodes): """Deprecated: use :meth:`Node.set_child_nodes()` instead.""" return self.set_child_nodes(child_nodes)
[docs] def add_child(self, node): """ Adds a child node to this node if it is not already a child. Results in the ``parent_node`` attribute of ``node`` as well as the ``tail_node`` attribute of ``node.edge`` being assigned to ``self``. Parameters ---------- node : |Node| The node to be added as a child of this node. Returns ------- |Node| The node that was added. """ assert node is not self, "Cannot add node as child of itself" assert self._parent_node is not node, ( "Cannot add a node's parent as its child: remove the node from its parent's" " child set first" ) node._parent_node = self if node not in self._child_nodes: self._child_nodes.append(node) return node
[docs] def insert_child(self, index, node): """ Adds a child node to this node. If the node is already a child of this node, then it is moved to the specified position. Results in the ``parent_node`` attribute of ``node`` as well as the ``tail_node`` attribute of ``node.edge`` being assigned to ``self``. Parameters ---------- index : integer The index before which to insert the new node. node : |Node| The node to be added as a child of this node. Returns ------- |Node| The node that was added. """ node._parent_node = self try: cur_index = self._child_nodes.index(node) except ValueError: pass else: if cur_index == index: return self._child_nodes.remove(node) self._child_nodes.insert(index, node) return node
[docs] def new_child(self, **kwargs): """ Create and add a new child to this node. Parameters ---------- kwargs : keyword arguments Keyword arguments will be passed directly to the |Node| constructor (:meth:`Node.__init()__`). Returns ------- |Node| The new child node that was created and added. """ node = self.__class__(**kwargs) return self.add_child(node=node)
[docs] def insert_new_child(self, index, **kwargs): """ Create and add a new child to this node at a particular position. Results in the ``parent_node`` attribute of ``node`` as well as the ``tail_node`` attribute of ``node.edge`` being assigned to ``self``. Parameters ---------- index : integer The index before which to insert the new node. kwargs : keyword arguments, optional Keyword arguments will be passed directly to the |Node| constructor (:meth:`Node.__init()__`). Returns ------- |Node| The new child node that was created and added. """ node = self.__class__(**kwargs) return self.insert_child(index=index, node=node)
[docs] def remove_child(self, node, suppress_unifurcations=False): """ Removes a node from the child set of this node. Results in the parent of the node being removed set to |None|. If ``suppress_unifurcations`` is |True|, if this node ends up having only one child after removal of the specified node, then this node will be removed from the tree, with its single child added to the child node set of its parent and the edge length adjusted accordingly. ``suppress_unifurcations`` should only be |True| for unrooted trees. Parameters ---------- node : |Node| The node to be removed. suppress_unifurcations : boolean, optional If |False| (default), no action is taken. If |True|, then if the node removal results in a node with degree of two (i.e., a single parent and a single child), then it will be removed from the tree and its (sole) child will be added as a child of its parent (with edge lengths adjusted accordingly). Returns ------- |Node| The node removed. """ if not node: raise ValueError("Tried to remove an non-existing or null node") children = self._child_nodes if node in children: node._parent_node = None node.edge.tail_node = None index = children.index(node) children.remove(node) if suppress_unifurcations: if self._parent_node: if len(children) == 1: child = children[0] pos = self._parent_node._child_nodes.index(self) self._parent_node.insert_child(pos, child) self._parent_node.remove_child( self, suppress_unifurcations=False ) try: child.edge.length += self.edge.length except: pass self._child_nodes = [] else: to_remove = None if len(children) == 2: if children[0].is_internal(): to_remove = children[0] other = children[1] elif children[1].is_internal(): to_remove = children[1] other = children[0] if to_remove is not None: try: other.edge.length += to_remove.edge.length except: pass pos = self._child_nodes.index(to_remove) self.remove_child(to_remove, suppress_unifurcations=False) tr_children = to_remove._child_nodes tr_children.reverse() for c in tr_children: self.insert_child(pos, c) to_remove._child_nodes = [] else: raise ValueError("Tried to remove a node that is not listed as a child") return node
[docs] def clear_child_nodes(self): """ Removes all child nodes. """ self._child_nodes.clear()
[docs] def reversible_remove_child(self, node, suppress_unifurcations=False): """ This function is a (less-efficient) version of remove_child that also returns the data needed by reinsert_nodes to "undo" the removal. Returns a list of tuples. The first element of each tuple is the node removed, the other elements are the information needed by ``reinsert_nodes`` in order to restore the tree to the same topology as it was before the call to ``remove_child.`` If ``suppress_unifurcations`` is False then the returned list will contain only one item. ``suppress_unifurcations`` should only be called on unrooted trees. """ if not node: raise ValueError("Tried to remove an non-existing or null node") children = self._child_nodes try: pos = children.index(node) except: raise ValueError("Tried to remove a node that is not listed as a child") removed = [(node, self, pos, [], None)] node._parent_node = None node.edge.tail_node = None children.remove(node) if suppress_unifurcations: p = self._parent_node if p: if len(children) == 1: child = children[0] pos = p._child_nodes.index(self) p.insert_child(pos, child) self._child_nodes = [] p.remove_child(self, suppress_unifurcations=False) e = child.edge try: e.length += self.edge.length except: e = None t = (self, p, pos, [child], e) removed.append(t) else: to_remove = None if len(children) == 2: if children[0].is_internal(): to_remove = children[0] other = children[1] elif children[1].is_internal(): to_remove = children[1] other = children[0] if to_remove is not None: e = other.edge try: e.length += to_remove.edge.length except: e = None pos = self._child_nodes.index(to_remove) self.remove_child(to_remove, suppress_unifurcations=False) tr_children = to_remove._child_nodes to_remove._child_nodes = [] for n, c in enumerate(tr_children): new_pos = pos + n self.insert_child(pos, c) t = (to_remove, self, pos, tr_children, e) removed.append(t) return removed
[docs] def reinsert_nodes(self, nd_connection_list): """ This function should be used to "undo" the effects of Node.reversible_remove_child NOTE: the behavior is only guaranteed if the tree has not been modified between the remove_child and reinsert_nodes calls! (or the tree has been restored such that the node/edge identities are identical to the state before the remove_child call. The order of info in each tuple is: 0 - node removed 1 - parent of node removed 2 - pos in parent matrix 3 - children of node removed that were "stolen" 4 - edge that was lengthened by "stealing" length from node's edge """ # we unroll the stack of operations for blob in nd_connection_list[-1::-1]: # _LOG.debug(blob) n, p, pos, children, e = blob for c in children: cp = c._parent_node if cp: cp.remove_child(c) n.add_child(c) p.insert_child(pos, n) if e is not None: e.length -= n.edge.length
def collapse_neighborhood(self, dist): if dist < 1: return children = self.child_nodes() for ch in children: if not ch.is_leaf(): ch.edge.collapse() if self._parent_node: p = self._parent_node self.edge.collapse() p.collapse_neighborhood(dist - 1) else: self.collapse_neighborhood(dist - 1)
[docs] def collapse_clade(self): """Collapses all internal edges that are descendants of self.""" if self.is_leaf(): return leaves = [i for i in self.leaf_iter()] self.set_child_nodes(leaves)
[docs] def collapse_conflicting(self, bipartition): """ Collapses every edge in the subtree that conflicts with the given bipartition. This can include the edge subtending subtree_root. """ to_collapse_head_nodes = [] for nd in self.postorder_iter(): if nd._child_nodes and nd.edge.bipartition.is_incompatible_with( bipartition ): to_collapse_head_nodes.append(nd) for nd in to_collapse_head_nodes: e = nd.edge e.collapse()
def _get_edge(self): """ Returns the edge subtending this node. """ return self._edge def _set_edge(self, new_edge): """ Sets the edge subtending this node, and sets head_node of ``edge`` to point to self. """ # if edge is None: # raise ValueError("A Node cannot have 'None' for an edge") if new_edge is self._edge: return if self._parent_node is not None: try: self._parent_node._child_nodes.remove(self) except ValueError: pass ## Minimal management self._edge = new_edge if self._edge: self._edge._head_node = self edge = property(_get_edge, _set_edge) def _get_edge_length(self): """ Returns the length of the edge subtending this node. """ return self._edge.length def _set_edge_length(self, v=None): """ Sets the edge subtending this node, and sets head_node of ``edge`` to point to self. """ self._edge.length = v edge_length = property(_get_edge_length, _set_edge_length) def _get_bipartition(self): """ Returns the bipartition for the edge subtending this node. """ return self._edge.bipartition def _set_bipartition(self, v=None): """ Sets the bipartition for the edge subtending this node. """ self._edge.bipartition = v bipartition = property(_get_bipartition, _set_bipartition) def _get_split_bitmask(self): return self._edge.bipartition._split_bitmask def _set_split_bitmask(self, h): self._edge.bipartition._split_bitmask = h split_bitmask = property(_get_split_bitmask, _set_split_bitmask) def _get_leafset_bitmask(self): return self._edge.bipartition._leafset_bitmask def _set_leafset_bitmask(self, h): self._edge.bipartition._leafset_bitmask = h leafset_bitmask = property(_get_leafset_bitmask, _set_leafset_bitmask) def _get_tree_leafset_bitmask(self): return self._edge.bipartition._tree_leafset_bitmask def _set_tree_leafset_bitmask(self, h): self._edge.bipartition._tree_leafset_bitmask = h tree_leafset_bitmask = property( _get_tree_leafset_bitmask, _set_tree_leafset_bitmask ) def split_as_bitstring(self): return self._edge.bipartition.split_as_bitstring() def leafset_as_bitstring(self): return self._edge.bipartition.leafset_as_bitstring() def _get_parent_node(self): """Returns the parent node of this node.""" return self._parent_node def _set_parent_node(self, parent): """Sets the parent node of this node.""" if self._parent_node is not None: try: self._parent_node._child_nodes.remove(self) except ValueError: pass self._parent_node = parent if self._parent_node is not None: if self not in self._parent_node._child_nodes: self._parent_node._child_nodes.append(self) parent_node = property(_get_parent_node, _set_parent_node)
[docs] def is_leaf(self): """ Returns |True| if the node is a tip or a leaf node, i.e. has no child nodes. Returns ------- boolean |True| if the node is a leaf, i.e., has no child nodes. |False| otherwise. """ return bool(not self._child_nodes)
[docs] def is_internal(self): """ Returns |True| if the node is *not* a tip or a leaf node. Returns ------- boolean |True| if the node is not a leaf. |False| otherwise. """ return bool(self._child_nodes)
[docs] def leaf_nodes(self): """ Returns list of all leaf_nodes descended from this node (or just list with ``self`` as the only member if ``self`` is a leaf). Note ---- Usage of `leaf_iter()` is preferable for efficiency reasons unless actual list is required. Returns ------- :py:class:`list` [|Node|] A ``list`` of |Node| objects descended from this node (inclusive of ``self``) that are the leaves. """ return [ node for node in self.postorder_iter(lambda x: bool(len(x.child_nodes()) == 0)) ]
[docs] def num_child_nodes(self): """ Returns number of child nodes. Returns ------- int Number of children in ``self``. """ return len(self._child_nodes)
[docs] def child_nodes(self): """ Returns a shallow-copy list of all child nodes of this node. Note ---- Unless an actual ``list`` is needed, iterating over the child nodes using :meth:`Node.child_node_iter()` is preferable to avoid the overhead of list construction. Returns ------- :py:class:`list` [|Node|] A ``list`` of |Node| objects that have ``self`` as a parent. """ return list(self._child_nodes)
[docs] def child_edges(self): """ Returns a shallow-copy list of all child edges of this node. Note ---- Unless an actual ``list`` is needed, iterating over the child edges using :meth:`Node.child_edge_iter()` is preferable to avoid the overhead of list construction. Returns ------- :py:class:`list` [|Edge|] A ``list`` of |Edge| objects that have ``self`` as a tail node. """ return list(ch.edge for ch in self._child_nodes)
[docs] def incident_edges(self): """ Return parent and child edges. Returns ------- :py:class:`list` [|Edge|] A list of edges linking to this node, with outgoing edges (edges connecting to child nodes) followed by the edge connecting this node to its parent. """ e = [c.edge for c in self._child_nodes] e.append(self.edge) return e
[docs] def get_incident_edges(self): """Legacy synonym for :meth:`Node.incident_edges()`.""" return self.incident_edges()
[docs] def adjacent_nodes(self): """ Return parent and child nodes. Returns ------- :py:class:`list` [|Node|] A list with all child nodes and parent node of this node. """ n = [c for c in self._child_nodes] if self._parent_node: n.append(self._parent_node) return n
[docs] def get_adjacent_nodes(self): """Legacy synonym for :meth:`Node.adjacent_edges()`""" return self.adjacent_nodes()
[docs] def sibling_nodes(self): """ Return all other children of parent, excluding self. Returns ------- :py:class:`list` [|Node|] A list of all nodes descended from the same parent as ``self``, excluding ``self``. """ p = self._parent_node if not p: return [] sisters = [nd for nd in p.child_nodes() if nd is not self] return sisters
[docs] def sister_nodes(self): """Legacy synonym for :meth:`Node.sister_nodes()`""" return self.sibling_nodes()
[docs] def extract_subtree( self, extraction_source_reference_attr_name="extraction_source", node_filter_fn=None, suppress_unifurcations=True, is_apply_filter_to_leaf_nodes=True, is_apply_filter_to_internal_nodes=False, node_factory=None, ): """ Returns a clone of the structure descending from this node. Parameters ---------- extraction_source_reference_attr_name : str Name of attribute to set on cloned nodes that references corresponding original node. If ``None``, then attribute (and reference) will not be created. node_filter_fn : None or function object If ``None``, then entire tree structure is cloned. If not ``None``, must be a function object that returns ``True`` if a particular |Node| instance on the original tree should be included in the cloned tree, or ``False`` otherwise. is_apply_filter_to_leaf_nodes : bool If ``True`` then the above filter will be applied to leaf nodes. If ``False`` then it will not (and all leaf nodes will be automatically included, unless excluded by an ancestral node being filtered out). is_apply_filter_to_internal_nodes : bool If ``True`` then the above filter will be applied to internal nodes. If ``False`` then it will not (internal nodes without children will still be filtered out). node_factory : function If not ``None``, must be a function that takes no arguments and returns a new |Node| (or equivalent) instance. Returns ------- nd : |Node| A node with descending subtree mirroring this one. """ memo = {} is_excluded_nodes = False start_node = None start_node_to_match = self if node_factory is None: node_factory = self.__class__ nd1 = None # verbosity to mollify linter for nd0 in self.postorder_iter(): if node_filter_fn is not None: if nd0._child_nodes: if is_apply_filter_to_internal_nodes: is_apply_filter = True else: is_apply_filter = False else: if is_apply_filter_to_leaf_nodes: is_apply_filter = True else: is_apply_filter = False if is_apply_filter and not node_filter_fn(nd0): is_excluded_nodes = True continue original_node_has_children = False children_to_add = [] for ch_nd0 in nd0.child_node_iter(): original_node_has_children = True ch_nd1 = memo.get(ch_nd0, None) if ch_nd1 is not None: children_to_add.append(ch_nd1) if not children_to_add and original_node_has_children: # filter removes all descendents of internal node, # so this internal node is not added if nd0.parent_node is None: raise error.SeedNodeDeletionException( "Attempting to remove seed node or node without parent" ) if nd0 is self: start_node_to_match = nd0.parent_node continue elif len(children_to_add) == 1 and suppress_unifurcations: if nd0.edge.length is not None: if children_to_add[0].edge.length is None: children_to_add[0].edge.length = nd0.edge.length else: children_to_add[0].edge.length += nd0.edge.length else: nd1.edge.length = children_to_add[0].edge.length if nd0.parent_node is None: start_node = children_to_add[0] break if nd0 is self: start_node_to_match = nd0.parent_node memo[nd0] = children_to_add[0] else: nd1 = node_factory() nd1.label = nd0.label nd1.taxon = nd0.taxon nd1.edge.length = nd0.edge.length nd1.edge.label = nd0.edge.label for ch_nd1 in children_to_add: nd1.add_child(ch_nd1) if nd0 is start_node_to_match: start_node = nd1 memo[nd0] = nd1 if extraction_source_reference_attr_name: setattr(nd1, extraction_source_reference_attr_name, nd0) if start_node is not None: return start_node else: ## TODO: find a replacement node raise ValueError
[docs] def level(self): """ Returns the number of nodes between ``self`` and the seed node of the tree. Returns ------- integer The number of nodes between ``self`` and the seed node of the tree, or 0 if ``self`` has no parent. """ if self._parent_node: return self._parent_node.level() + 1 else: return 0
[docs] def distance_from_root(self): """ Weighted path length of ``self`` from root. Returns ------- numeric Total weight of all edges connecting ``self`` with the root of the tree. """ if self._parent_node and self.edge.length != None: if self._parent_node.distance_from_root == None: return float(self.edge.length) else: distance_from_root = float(self.edge.length) parent_node = self._parent_node # The root is identified when a node with no # parent is encountered. If we want to use some # other criteria (e.g., where a is_root property # is True), we modify it here. while parent_node: if parent_node.edge.length != None: distance_from_root = distance_from_root + float( parent_node.edge.length ) parent_node = parent_node._parent_node return distance_from_root elif not self._parent_node and self.edge.length != None: return float(self.edge.length) elif self._parent_node and self.edge.length == None: # what do we do here: parent node exists, but my # length does not? return float(self._parent_node.edge.length) elif not self._parent_node and self.edge.length == None: # no parent node, and no edge length return 0.0 else: # WTF???? return 0.0
[docs] def distance_from_tip(self): """ Maximum weighted length of path of ``self`` to tip. If tree is not ultrametric (i.e., descendent edges have different lengths), then count the maximum of edge lengths. Note that :meth:`Tree.calc_node_ages()` is a more efficient way of doing this over the whole tree if this value is need for many or all the nodes on the tree. Returns ------- numeric Maximum weight of edges connecting ``self`` to tip. """ if not self._child_nodes: return 0.0 else: distance_from_tips = [] for ch in self._child_nodes: if ch.edge.length is not None: curr_edge_length = ch.edge_length else: curr_edge_length = 0.0 if not hasattr(ch, "_distance_from_tip"): ch._distance_from_tip = ch.distance_from_tip() distance_from_tips.append(ch._distance_from_tip + curr_edge_length) self._distance_from_tip = float(max(distance_from_tips)) return self._distance_from_tip
[docs] def description( self, depth=1, indent=0, itemize="", output=None, taxon_namespace=None ): """ Returns description of object, up to level ``depth``. """ if depth is None or depth < 0: return output_strio = StringIO() label = str(self) output_strio.write( "%s%sNode object at %s%s" % (indent * " ", itemize, hex(id(self)), label) ) if depth >= 1: leader1 = " " * (indent + 4) leader2 = " " * (indent + 8) output_strio.write("\n%s[Edge]" % leader1) if self.edge is not None: edge_desc = self.edge.description(0) else: edge_desc = "None" output_strio.write("\n%s%s" % (leader2, edge_desc)) output_strio.write("\n%s[Taxon]" % leader1) if self.taxon is not None: taxon_desc = self.taxon.description(0) else: taxon_desc = "None" output_strio.write("\n%s%s" % (leader2, taxon_desc)) output_strio.write("\n%s[Parent]" % leader1) if self._parent_node is not None: parent_node_desc = self._parent_node.description(0) else: parent_node_desc = "None" output_strio.write("\n%s%s" % (leader2, parent_node_desc)) output_strio.write("\n%s[Children]" % leader1) if len(self._child_nodes) == 0: output_strio.write("\n%sNone" % leader2) else: for i, cnd in enumerate(self._child_nodes): output_strio.write("\n%s[%d] %s" % (leader2, i, cnd.description(0))) s = output_strio.getvalue() if output is not None: output.write(s) return s
## For debugging we build-in a full-fledged NEWICK composition independent ## of the nexus/newick family of modules. Client code should prefer to ## use Newick/Nexus readers/writers, or Tree.write(), TreeList.write(), ## DataSet.write() etc. def _as_newick_string(self, **kwargs): """ This returns the Node as a NEWICK statement according to the given formatting rules. This should be used for debugging purposes only. For production purposes, use the the full-fledged 'as_string()' method of the object. """ out = StringIO() self._write_newick(out, **kwargs) return out.getvalue() def _write_newick(self, out, **kwargs): """ This returns the Node as a NEWICK statement according to the given formatting rules. This should be used for debugging purposes only. For production purposes, use the the full-fledged 'write_to_stream()' method of the object. """ edge_lengths = not kwargs.get("suppress_edge_lengths", False) edge_lengths = kwargs.get("edge_lengths", edge_lengths) child_nodes = self.child_nodes() if child_nodes: out.write("(") f_child = child_nodes[0] for child in child_nodes: if child is not f_child: out.write(",") child._write_newick(out, **kwargs) out.write(")") out.write(self._get_node_token(**kwargs)) if edge_lengths: e = self.edge if e: sel = e.length if sel is not None: fmt = kwargs.get("edge_length_formatter", None) if fmt: out.write(":%s" % fmt(sel)) else: s = "" try: s = float(sel) s = str(s) except ValueError: s = str(sel) if s: out.write(":%s" % s) def _get_node_token(self, **kwargs): """returns a string that is an identifier for the node. This is called by the newick-writing functions, so the kwargs that affect how node labels show up in a newick string are the same ones used here: ``suppress_internal_labels`` is a Boolean, and defaults to False. """ is_leaf = len(self._child_nodes) == 0 if not is_leaf: if kwargs.get("suppress_internal_labels", False) or not kwargs.get( "include_internal_labels", True ): return "" if self.taxon is not None: if self.taxon.label: label = self.taxon.label else: # return "_" # taxon, but no label: anonymous label = ( # "_" is not anonoymous/unnamed, but a name of <blank>; so we return nothing instead "" ) else: if self.label: label = self.label else: label = "" if not label or kwargs.get("raw_labels", False): return label elif " " in label and "_" in label: if "'" in label: label.replace("'", "''") return "'{}'".format(label) elif " " in label and not kwargs.get("preserve_spaces"): return label.replace(" ", "_") else: return label def _get_indented_form(self, **kwargs): out = StringIO() self._write_indented_form(out, **kwargs) return out.getvalue() def _write_indented_form(self, out, **kwargs): indentation = kwargs.get("indentation", " ") level = kwargs.get("level", 0) ancestors = [] siblings = [] n = self while n is not None: n._write_indented_form_line(out, level, **kwargs) n, lev = n._preorder_list_manip(siblings, ancestors) level += lev def _get_indented_form_line(self, level, **kwargs): out = StringIO() self._write_indented_form_line(out, level, **kwargs) return out.getvalue() def _write_indented_form_line(self, out, level, **kwargs): indentation = kwargs.get("indentation", " ") label = self._format_node(**kwargs) if kwargs.get("bipartitions"): cm = "%s " % self.edge.bipartition._format_bipartition(**kwargs) else: cm = "" out.write("%s%s%s\n" % (cm, indentation * level, label)) def _format_node(self, **kwargs): nf = kwargs.get('node_formatter', None) if nf: return nf(self) if self.taxon is not None: return str(self.taxon) if self.label is not None: return self.label return "" def _preorder_list_manip(self, siblings, ancestors): """ Helper function for recursion free preorder traversal, that does not rely on attributes of the node other than child_nodes() (thus it is useful for debuggging). Returns the next node (or None) and the number of levels toward the root the function "moved". """ levels_moved = 0 c = self.child_nodes() if c: levels_moved += 1 ancestors.append(list(siblings)) del siblings[:] siblings.extend(c[1:]) return c[0], levels_moved while not siblings: if ancestors: levels_moved -= 1 del siblings[:] siblings.extend(ancestors.pop()) else: return None, levels_moved return siblings.pop(0), levels_moved def _convert_node_to_root_polytomy(self): """If ``self`` has two children and at least one of them is an internal node, then it will be converted to an out-degree three node (with the edge length added as needed). Returns a tuple of child nodes that were detached (or() if the tree was not modified). This can be useful for removing the deleted node from the split_edge_map dictionary. """ nd_children = self.child_nodes() if len(nd_children) > 2: return () try: left_child = nd_children[0] except: return () if not left_child: return () if len(nd_children) == 1: right_child = None dest_edge_head = self else: right_child = nd_children[1] dest_edge_head = right_child curr_add = None if right_child and right_child.is_internal(): try: left_child.edge.length += right_child.edge.length except: pass self.remove_child(right_child) grand_kids = right_child.child_nodes() for gc in grand_kids: self.add_child(gc) curr_add = right_child elif left_child.is_internal(): try: dest_edge_head.edge.length += left_child.edge.length except: pass self.remove_child(left_child) grand_kids = left_child.child_nodes() for gc in grand_kids: self.add_child(gc) curr_add = left_child if curr_add: ndl = [curr_add] t = self._convert_node_to_root_polytomy() ndl.extend(t) return tuple(ndl) return ()