Skip to content

Commit 99b2362

Browse files
committed
First draft at polytomy breaking function
1 parent ed78a39 commit 99b2362

4 files changed

Lines changed: 427 additions & 0 deletions

File tree

python/CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@
7070

7171
**Features**
7272

73+
- Add ``randomly_split_polytomies`` methods for tables and tree sequences
74+
(:user:`hyanwong`, :issue:`809`, :pr:`815`)
75+
7376
- Tree accessor functions (e.g. ``ts.first()``, ``ts.at()`` pass extra parameters such as
7477
``sample_indexes`` to the underlying ``Tree`` constructor; also ``root_threshold`` can
7578
be specified when calling ``ts.trees()`` (:user:`hyanwong`, :issue:`847`, :pr:`848`)

python/tests/test_topology.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@
2323
"""
2424
Test cases for the supported topological variations and operations.
2525
"""
26+
import collections
2627
import functools
2728
import io
2829
import itertools
2930
import json
3031
import math
3132
import random
33+
import re
3234
import sys
3335
import unittest
3436

@@ -7951,3 +7953,163 @@ def test_star_branch_length(self):
79517953

79527954
assert ts.node(ts.first().root).time == branch_length
79537955
assert ts.kc_distance(topological_equiv_ts) == 0
7956+
7957+
7958+
class TestPolytomySplitting(unittest.TestCase):
7959+
"""
7960+
Test the ability to randomly split polytomies
7961+
"""
7962+
7963+
# A complex ts with polytomies - in the first 2 trees the polytomy involves
7964+
# the same children, so should be resolved in one go
7965+
#
7966+
# 1.00┊ 6 ┊ 6 ┊ 6 ┊ ┊ 6 ┊
7967+
# ┊ ┏━┳┻┳━┓ ┊ ┏━┳┻┳━┓ ┊ ┏━━╋━┓ ┊ ┊ ┏━┳┻┳━┓ ┊
7968+
# 0.50┊ 5 ┃ ┃ ┃ ┊ 5 ┃ ┃ ┃ ┊ 5 ┃ ┃ ┊ 5 ┊ ┃ ┃ ┃ ┃ ┊
7969+
# ┊ ┃ ┃ ┃ ┃ . ┊ ┃ ┃ ┃ ┃ ┊ . ┏┻┓ ┃ ┃ ┊ . ┏━┳┻┳━┓ ┊ . ┃ ┃ ┃ ┃ ┊
7970+
# 0.00┊ 0 2 3 4 1 ┊ 0 1 2 3 4 ┊ 0 1 2 3 4 ┊ 0 1 2 3 4 ┊ 0 1 2 3 4 ┊
7971+
# 0.00 0.20 0.40 0.60 0.80 1.00
7972+
nodes_polytomy_44344 = """\
7973+
id is_sample population time
7974+
0 1 0 0.0
7975+
1 1 0 0.0
7976+
2 1 0 0.0
7977+
3 1 0 0.0
7978+
4 1 0 0.0
7979+
5 0 0 0.5
7980+
6 0 0 1.0
7981+
"""
7982+
edges_polytomy_44344 = """\
7983+
id left right parent child
7984+
0 0.0 0.2 5 0
7985+
1 0.0 0.8 5 1
7986+
2 0.0 0.4 6 2
7987+
3 0.4 0.8 5 2
7988+
4 0.0 0.6 6 3,4
7989+
5 0.0 0.6 6 5
7990+
6 0.6 0.8 5 3,4
7991+
7 0.8 1.0 6 1,2,3,4
7992+
"""
7993+
7994+
def ts_polytomy_4(self):
7995+
return tskit.Tree.generate_star(4).tree_sequence
7996+
7997+
def ts_polytomy_44344(self):
7998+
return tskit.load_text(
7999+
nodes=io.StringIO(self.nodes_polytomy_44344),
8000+
edges=io.StringIO(self.edges_polytomy_44344),
8001+
strict=False,
8002+
)
8003+
8004+
# Statistical test coded here for reference
8005+
# @unittest.skip("Testing statistical properties is nontrivial in a unit test")
8006+
def test_equiprobable(self):
8007+
n_tops_4 = 15 # 4-tomy has 15 poss. resolutions
8008+
n_tops_3 = 3 # 3-tomy has 3 poss. resolutions
8009+
# Simplify the example tree to remove isolated node 0
8010+
ts = self.ts_polytomy_44344().simplify(np.arange(1, 5), keep_unary=True)
8011+
assert ts.num_trees == 4
8012+
8013+
num_tree_topologies = [collections.Counter() for _ in range(ts.num_trees)]
8014+
start_seed = 123
8015+
n = 3000 # This may take some time, e.g. 5 secs
8016+
for seed in range(start_seed, start_seed + n):
8017+
ts_split = ts.split_polytomies(random_seed=seed * 10).simplify(
8018+
keep_unary=False
8019+
)
8020+
# Tree 0 should have been concatenated into tree 1 by simplification
8021+
for tree in ts_split.trees():
8022+
num_tree_topologies[tree.index].update([tree.rank()])
8023+
for i, n_topologies in enumerate([n_tops_4, n_tops_3, n_tops_4, n_tops_4]):
8024+
assert len(num_tree_topologies[i]) == n_topologies
8025+
# If equal probabilities, max difference
8026+
freqs = [c / n for c in num_tree_topologies[i].values()]
8027+
# all counts freqs should be roughly 1/nth of the trees
8028+
assert min(freqs) > 1 / n_topologies * 0.8
8029+
assert max(freqs) < 1 / n_topologies * 1.2
8030+
8031+
def test_simple_examples(self):
8032+
ts = self.ts_polytomy_4().split_polytomies(random_seed=12)
8033+
for tree in ts.trees():
8034+
for node in tree.nodes():
8035+
assert tree.num_children(node) < 3
8036+
8037+
def test_complex_examples(self):
8038+
ts = self.ts_polytomy_44344().split_polytomies(random_seed=12)
8039+
for tree in ts.trees():
8040+
for node in tree.nodes():
8041+
assert tree.num_children(node) < 3
8042+
# Tree 0 should have same resolution (same internal nodes) as tree 1
8043+
t = ts.at_index(0)
8044+
n0 = {n for n in t.nodes() if not (t.is_leaf(n) or t.parent(n) == tskit.NULL)}
8045+
t = ts.at_index(1)
8046+
n1 = {n for n in t.nodes() if not (t.is_leaf(n) or t.parent(n) == tskit.NULL)}
8047+
assert n0 == n1
8048+
# Tree -2 should not have same resolution as tree -1 (all internal nodes should
8049+
# differ) as the root has changed, even though the children are the same
8050+
t = ts.at_index(-2)
8051+
n0 = {n for n in t.nodes() if not (t.is_leaf(n) or t.parent(n) == tskit.NULL)}
8052+
t = ts.at_index(-1)
8053+
n1 = {n for n in t.nodes() if not (t.is_leaf(n) or t.parent(n) == tskit.NULL)}
8054+
assert len(n0 & n1) == 0
8055+
8056+
def test_nonbinary_simulation(self):
8057+
demographic_events = [
8058+
msprime.SimpleBottleneck(time=1.0, population=0, proportion=0.95)
8059+
]
8060+
ts = msprime.simulate(
8061+
20,
8062+
recombination_rate=10,
8063+
mutation_rate=5,
8064+
demographic_events=demographic_events,
8065+
random_seed=7,
8066+
)
8067+
n_poly = 0
8068+
for e in ts.edgesets():
8069+
if len(e.children) > 2:
8070+
n_poly += 1
8071+
assert n_poly > 3
8072+
ts_binary = ts.split_polytomies(random_seed=123)
8073+
for tree in ts_binary.trees():
8074+
for node in tree.nodes():
8075+
assert tree.num_children(node) < 3
8076+
8077+
def test_bad_method(self):
8078+
with pytest.raises(ValueError):
8079+
self.ts_polytomy_4().split_polytomies(method="something_else")
8080+
8081+
def test_epsilon_for_nodes(self):
8082+
with pytest.raises(
8083+
ValueError, match="not small enough to create new nodes under node 4"
8084+
) as exc_info:
8085+
self.ts_polytomy_4().split_polytomies(epsilon=1)
8086+
m = re.search(r"must be < ([-\d.e]+).", exc_info.value.args[0])
8087+
assert m is not None
8088+
suggested_epsilon = float(m.group(1)) * 0.999
8089+
self.ts_polytomy_4().split_polytomies(epsilon=suggested_epsilon)
8090+
8091+
def test_epsilon_for_mutations(self):
8092+
tables = self.ts_polytomy_4().dump_tables()
8093+
root_time = tables.nodes.time[-1]
8094+
assert root_time > 0.1
8095+
site = tables.sites.add_row(position=0.5, ancestral_state="0")
8096+
mut_diff = 0.01
8097+
tables.mutations.add_row(
8098+
site=site, time=root_time - mut_diff, node=0, derived_state="1"
8099+
)
8100+
ts = tables.tree_sequence()
8101+
with pytest.raises(
8102+
ValueError, match="not small enough to create new nodes below a polytomy"
8103+
):
8104+
ts.split_polytomies(epsilon=mut_diff)
8105+
# A 4-tomy creates 2 new nodes => epsilon of ~ 1/3 of mut_diff should work
8106+
self.ts_polytomy_4().split_polytomies(epsilon=mut_diff / 3)
8107+
8108+
def test_provenance(self):
8109+
ts = self.ts_polytomy_4()
8110+
ts_split = ts.split_polytomies(random_seed=12)
8111+
record = json.loads(ts_split.provenance(ts_split.num_provenances - 1).record)
8112+
assert record["parameters"]["command"] == "split_polytomies"
8113+
ts_split = ts.split_polytomies(random_seed=12, record_provenance=False)
8114+
record = json.loads(ts_split.provenance(ts_split.num_provenances - 1).record)
8115+
assert record["parameters"]["command"] != "split_polytomies"

0 commit comments

Comments
 (0)