Skip to content

Commit ec7378c

Browse files
committed
Implement is_empty method
Fixes #2640
1 parent ec992da commit ec7378c

File tree

2 files changed

+108
-0
lines changed

2 files changed

+108
-0
lines changed

python/tests/test_topology.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1764,6 +1764,85 @@ def test_multiple_trees(self):
17641764
self.verify(ts, no_root_ts)
17651765

17661766

1767+
class TestEmptyTrees(TopologyTestCase):
1768+
"""
1769+
Tests for different sorts of "empty" trees
1770+
"""
1771+
1772+
@pytest.mark.parametrize("check_roots", [None, True, False])
1773+
def test_no_nodes(self, check_roots):
1774+
tables = tskit.TableCollection(1)
1775+
tree = tables.tree_sequence().first()
1776+
assert tree.is_empty(check_roots=check_roots)
1777+
1778+
@pytest.mark.parametrize("check_roots", [None, True, False])
1779+
@pytest.mark.parametrize("root_threshold", [1, 2, 3])
1780+
def test_normal(self, check_roots, root_threshold):
1781+
tree = tskit.Tree.generate_balanced(2, root_threshold=root_threshold)
1782+
if check_roots and root_threshold > 2:
1783+
assert tree.is_empty(check_roots=check_roots)
1784+
else:
1785+
assert not tree.is_empty(check_roots=check_roots)
1786+
1787+
@pytest.mark.parametrize("check_roots", [None, True, False])
1788+
@pytest.mark.parametrize("root_threshold", [1, 2])
1789+
def test_stick(self, check_roots, root_threshold):
1790+
ts = tskit.Tree.generate_balanced(2).tree_sequence
1791+
stick_tree = ts.simplify([0], keep_unary=True).first(
1792+
root_threshold=root_threshold
1793+
)
1794+
if check_roots and root_threshold > 1:
1795+
assert stick_tree.is_empty(check_roots=check_roots)
1796+
else:
1797+
assert not stick_tree.is_empty(check_roots=check_roots)
1798+
1799+
@pytest.mark.parametrize("check_roots", [None, True, False])
1800+
@pytest.mark.parametrize("root_threshold", [1, 2])
1801+
def test_upsidedown_stick(self, check_roots, root_threshold):
1802+
ts = tskit.Tree.generate_balanced(2).tree_sequence
1803+
tables = ts.simplify([0], keep_unary=True).dump_tables()
1804+
# swap flags so that non-sample is dangling off sample
1805+
tables.nodes.flags = 1 - tables.nodes.flags
1806+
upsidedown_stick_tree = tables.tree_sequence().first(
1807+
root_threshold=root_threshold
1808+
)
1809+
if check_roots and root_threshold > 1:
1810+
assert upsidedown_stick_tree.is_empty(check_roots=check_roots)
1811+
else:
1812+
assert not upsidedown_stick_tree.is_empty(check_roots=check_roots)
1813+
1814+
@pytest.mark.parametrize("check_roots", [None, True, False])
1815+
@pytest.mark.parametrize("root_threshold", [1, 2])
1816+
def test_multiroot_non_empty(self, check_roots, root_threshold):
1817+
tables = tskit.Tree.generate_balanced(2).tree_sequence.dump_tables()
1818+
tables.edges.truncate(1)
1819+
multiroot_tree = tables.tree_sequence().first(root_threshold=root_threshold)
1820+
assert multiroot_tree.num_roots == (2 if root_threshold == 1 else 0)
1821+
if check_roots and root_threshold > 1:
1822+
assert multiroot_tree.is_empty(check_roots=check_roots)
1823+
else:
1824+
assert not multiroot_tree.is_empty(check_roots=check_roots)
1825+
1826+
@pytest.mark.parametrize("check_roots", [True, False])
1827+
@pytest.mark.parametrize("root_threshold", [1, 2])
1828+
def test_empty(self, check_roots, root_threshold):
1829+
tables = tskit.Tree.generate_balanced(2).tree_sequence.dump_tables()
1830+
tables.delete_intervals([[0, 1]])
1831+
# check that sites & mutations make no difference
1832+
s = tables.sites.add_row(position=0.5, ancestral_state="0")
1833+
s = tables.mutations.add_row(site=s, derived_state="1", node=0)
1834+
tree = tables.tree_sequence().first(root_threshold=root_threshold)
1835+
assert tree.is_empty(check_roots=check_roots)
1836+
1837+
def test_dead_branch(self):
1838+
tables = tskit.Tree.generate_balanced(2).tree_sequence.dump_tables()
1839+
tables.nodes.flags = np.zeros_like(tables.nodes.flags)
1840+
tables.nodes.add_row(flags=tskit.NODE_IS_SAMPLE)
1841+
tree = tables.tree_sequence().first()
1842+
assert not tree.is_empty(check_roots=False)
1843+
assert tree.is_empty(check_roots=True)
1844+
1845+
17671846
class TestEmptyTreeSequences(TopologyTestCase):
17681847
"""
17691848
Tests covering tree sequences that have zero edges.

python/tskit/trees.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,6 +851,35 @@ def seek(self, position):
851851
raise ValueError("Position out of bounds")
852852
self._ll_tree.seek(position)
853853

854+
def is_empty(self, check_roots=None) -> bool:
855+
"""
856+
Check if this tree is "empty" (i.e. has no topology). A tree is empty if it has
857+
no edges. However, it may also be considered empty if it contains edges which
858+
represent :ref:`dead branches<sec_data_model_tree_dead_leaves_and_branches>`
859+
(i.e. not reachable from the :meth:`~Tree.roots` of the tree). To consider such
860+
a tree as empty too, which is more involved, specify ``check_roots=True``.
861+
862+
Note that this is purely a property of the topology. An "empty" tree can still
863+
contain sites and there may even be mutations on those sites.
864+
865+
:param bool check_roots: Should we also consider a tree empty if it has
866+
topology but the topology is unconnected to any of the roots of the tree?
867+
Default: ``None`` treated as ``False``.
868+
:return: ``True`` if this tree is empty, ``False`` otherwise.
869+
"""
870+
if self.num_edges == 0:
871+
return True
872+
873+
if not check_roots:
874+
return False
875+
876+
# Exhaustively check the roots: it's not simply enough to check that the roots
877+
# are all samples, as they could still have children
878+
for u in self.roots:
879+
if self.num_children(u) != 0:
880+
return False
881+
return True
882+
854883
def rank(self) -> tskit.Rank:
855884
"""
856885
Produce the rank of this tree in the enumeration of all leaf-labelled

0 commit comments

Comments
 (0)