|
23 | 23 | """ |
24 | 24 | Test cases for the supported topological variations and operations. |
25 | 25 | """ |
| 26 | +import collections |
26 | 27 | import functools |
27 | 28 | import io |
28 | 29 | import itertools |
29 | 30 | import json |
30 | 31 | import math |
31 | 32 | import random |
| 33 | +import re |
32 | 34 | import sys |
33 | 35 | import unittest |
34 | 36 |
|
@@ -7951,3 +7953,163 @@ def test_star_branch_length(self): |
7951 | 7953 |
|
7952 | 7954 | assert ts.node(ts.first().root).time == branch_length |
7953 | 7955 | 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