Skip to content

Commit

Permalink
Implement is_root
Browse files Browse the repository at this point in the history
  • Loading branch information
hyanwong committed Dec 23, 2022
1 parent ec992da commit 5de977c
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 0 deletions.
3 changes: 3 additions & 0 deletions python/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

**Features**

- A new ``Tree.is_root`` method avoids the need to to search the potentially
large list of ``Tree.roots`` (:user:`hyanwong`, :pr:`2669`, :issue:`2620`)

- The ``TreeSequence`` object now has the attributes ``min_time`` and ``max_time``,
which are the minimum and maximum among the node times and mutation times,
respectively. (:user:`szhan`, :pr:`2612`, :issue:`2271`)
Expand Down
15 changes: 15 additions & 0 deletions python/tests/test_highlevel.py
Original file line number Diff line number Diff line change
Expand Up @@ -3867,6 +3867,21 @@ def test_branch_length_empty_tree(self):
assert tree.branch_length(1) == 0
assert tree.total_branch_length == 0

@pytest.mark.parametrize("root_threshold", [1, 2, 3])
def test_is_root(self, root_threshold):
# Make a tree with multiple roots with different numbers of samples under each
ts = tskit.Tree.generate_balanced(5).tree_sequence
ts = ts.decapitate(ts.max_root_time - 0.1)
tables = ts.dump_tables()
tables.nodes.add_row(flags=0) # Isolated non-sample
tables.nodes.add_row(flags=tskit.NODE_IS_SAMPLE) # Isolated sample
ts = tables.tree_sequence()
assert {ts.first().num_samples(u) for u in ts.first().roots} == {1, 2, 3}
tree = ts.first(root_threshold=root_threshold)
roots = set(tree.roots)
for u in range(ts.num_nodes): # Will also test isolated nodes
assert tree.is_root(u) == (u in roots)

def test_is_descendant(self):
def is_descendant(tree, u, v):
path = []
Expand Down
17 changes: 17 additions & 0 deletions python/tskit/trees.py
Original file line number Diff line number Diff line change
Expand Up @@ -1574,6 +1574,23 @@ def root(self):
raise ValueError("More than one root exists. Use tree.roots instead")
return self.left_root

def is_root(self, u) -> bool:
"""
Returns ``True`` if the specified node is a root in this tree (see
:attr:`~Tree.roots` for the definition of a root). This is exactly equivalent to
finding the node ID in :attr:`~Tree.roots`, but is more efficient for trees
with large numbers of roots, such as in regions with extensive
:ref:`sec_data_model_missing_data`. Note that ``False`` is returned for all
other nodes, including :ref:`isolated<sec_data_model_tree_isolated_nodes>`
non-sample nodes which are not found in the topology of the current tree.
:param int u: The node of interest.
:return: ``True`` if u is a root.
"""
return (
self.num_samples(u) >= self.root_threshold and self.parent(u) == tskit.NULL
)

def get_index(self):
# Deprecated alias for self.index
return self.index
Expand Down

0 comments on commit 5de977c

Please sign in to comment.