|
24 | 24 | Tree sequence IO via the tables API. |
25 | 25 | """ |
26 | 26 | import base64 |
| 27 | +import collections |
27 | 28 | import datetime |
28 | 29 | import itertools |
29 | 30 | import json |
@@ -2756,3 +2757,204 @@ def union( |
2756 | 2757 | self.provenances.add_row( |
2757 | 2758 | record=json.dumps(provenance.get_provenance_dict(parameters)) |
2758 | 2759 | ) |
| 2760 | + |
| 2761 | + def randomly_split_polytomies( |
| 2762 | + self, |
| 2763 | + *, |
| 2764 | + epsilon=None, |
| 2765 | + squash_edges=True, |
| 2766 | + record_provenance=True, |
| 2767 | + random_seed=None, |
| 2768 | + ): |
| 2769 | + """ |
| 2770 | + Modifies the table collection in place, adding extra nodes and edges |
| 2771 | + so that any node with greater than 2 children (i.e. a multifurcation |
| 2772 | + or "polytomy") is resolved into successive bifurcations. This is identical |
| 2773 | + to :meth:`TreeSequence.randomly_split_polytomies` but acts *in place* to |
| 2774 | + alter the data in this :class:`TableCollection`. Please see |
| 2775 | + :meth:`TreeSequence.randomly_split_polytomies` for a fuller description, |
| 2776 | + and details of parameters. |
| 2777 | + """ |
| 2778 | + if epsilon is None: |
| 2779 | + epsilon = 1e-10 |
| 2780 | + rng = np.random.default_rng(seed=random_seed) |
| 2781 | + |
| 2782 | + def is_unknown_time_array(a): |
| 2783 | + np_unknown_time = np.float64(UNKNOWN_TIME) |
| 2784 | + return a.view(np.uint64) == np_unknown_time.view(np.uint64) |
| 2785 | + |
| 2786 | + def resolve_polytomy(parent_node_id, child_ids, new_nodes_by_time_desc): |
| 2787 | + """ |
| 2788 | + For a polytomy and list of child node ids, return a list of (child, parent) |
| 2789 | + tuples, describing a bifurcating tree, rooted at parent_node_id, where the |
| 2790 | + new_nodes_by_time_desc have been used to break polytomies. All possible |
| 2791 | + topologies should be equiprobable. |
| 2792 | + """ |
| 2793 | + nonlocal rng |
| 2794 | + assert len(child_ids) == len(new_nodes_by_time_desc) + 2 |
| 2795 | + # Polytomies broken by sequentially splicing onto edges, so an initial edge |
| 2796 | + # is required. This will always remain above the top node & is removed later |
| 2797 | + edges = [ |
| 2798 | + [child_ids[0], None], |
| 2799 | + ] |
| 2800 | + # We know beforehand how many random ints are needed: generate them all now |
| 2801 | + edge_choice = rng.integers(0, np.arange(1, len(child_ids) * 2 - 1, 2)) |
| 2802 | + tmp_new_node_lab = [parent_node_id] + new_nodes_by_time_desc |
| 2803 | + assert len(edge_choice) == len(child_ids) - 1 |
| 2804 | + for node_lab, child_id, target_edge_id in zip( |
| 2805 | + tmp_new_node_lab, child_ids[1:], edge_choice |
| 2806 | + ): |
| 2807 | + target_edge = edges[target_edge_id] |
| 2808 | + # Insert in the right place, to keep edges in parent time order |
| 2809 | + edges.insert(target_edge_id, [child_id, node_lab]) |
| 2810 | + edges.insert(target_edge_id, [target_edge[0], node_lab]) |
| 2811 | + target_edge[0] = node_lab |
| 2812 | + top_edge = edges.pop() # remove the edge above the top node |
| 2813 | + assert top_edge[1] is None |
| 2814 | + |
| 2815 | + # Re-map the internal nodes IDs so they are used in time order |
| 2816 | + real_node = iter(new_nodes_by_time_desc) |
| 2817 | + node_map = {c: c for c in child_ids} |
| 2818 | + node_map[edges[-1][1]] = parent_node_id # last edge == oldest parent |
| 2819 | + for e in reversed(edges): |
| 2820 | + # Reversing along the edges, parents are in inverse time order |
| 2821 | + for idx in (1, 0): # look at parent (1) then child (0) |
| 2822 | + if e[idx] not in node_map: |
| 2823 | + node_map[e[idx]] = next(real_node) |
| 2824 | + e[idx] = node_map[e[idx]] |
| 2825 | + assert len(node_map) == len(new_nodes_by_time_desc) + len(child_ids) + 1 |
| 2826 | + return edges |
| 2827 | + |
| 2828 | + edge_table = self.edges |
| 2829 | + node_table = self.nodes |
| 2830 | + # Store existing left, so we can change it if the edge is split |
| 2831 | + existing_edges_left = edge_table.left |
| 2832 | + # Keep other edge arrays etc. for fast read access |
| 2833 | + existing_edges_right = edge_table.right |
| 2834 | + existing_edges_parent = edge_table.parent |
| 2835 | + existing_edges_child = edge_table.child |
| 2836 | + existing_node_time = node_table.time |
| 2837 | + |
| 2838 | + # We can save a lot of effort if we don't need to check the time of mutations |
| 2839 | + # We definitely don't need to check on the first iteration, a |
| 2840 | + check_mutations = np.any( |
| 2841 | + np.logical_not(is_unknown_time_array(self.mutations.time)) |
| 2842 | + ) |
| 2843 | + ts = self.tree_sequence() # Only needed to check mutations |
| 2844 | + tree_iter = ts.trees() # ditto |
| 2845 | + |
| 2846 | + edge_table.clear() |
| 2847 | + |
| 2848 | + edges_from_node = collections.defaultdict(set) # Active descendant edge ids |
| 2849 | + nodes_changed = set() |
| 2850 | + |
| 2851 | + for interval, e_out, e_in in ts.edge_diffs(include_terminal=True): |
| 2852 | + pos = interval[0] |
| 2853 | + prev_tree = None if pos == 0 else next(tree_iter) |
| 2854 | + |
| 2855 | + for edge in itertools.chain(e_out, e_in): |
| 2856 | + if edge.parent != tskit.NULL: |
| 2857 | + nodes_changed.add(edge.parent) |
| 2858 | + |
| 2859 | + if check_mutations and prev_tree is not None: |
| 2860 | + # This is grim. There must be a more efficient way. |
| 2861 | + # It would also help if mutations were sorted such that all mutations |
| 2862 | + # above the same node appeared consecutively, with oldest first. |
| 2863 | + oldest_mutation_for_node = {} |
| 2864 | + for site in prev_tree.sites(): |
| 2865 | + for mutation in site.mutations: |
| 2866 | + if not util.is_unknown_time(mutation.time): |
| 2867 | + oldest_mutation_for_node[mutation.node] = max( |
| 2868 | + oldest_mutation_for_node[mutation.node], mutation.time |
| 2869 | + ) |
| 2870 | + |
| 2871 | + for parent_node in nodes_changed: |
| 2872 | + child_edge_ids = edges_from_node[parent_node] |
| 2873 | + if len(child_edge_ids) >= 3: |
| 2874 | + # We have a previous polytomy to break |
| 2875 | + parent_time = existing_node_time[parent_node] |
| 2876 | + new_nodes = [] |
| 2877 | + child_ids = existing_edges_child[list(child_edge_ids)] |
| 2878 | + left = None |
| 2879 | + max_time = 0 |
| 2880 | + # Split existing edges |
| 2881 | + for edge_id, child_id in zip(child_edge_ids, child_ids): |
| 2882 | + max_time = max(max_time, existing_node_time[child_id]) |
| 2883 | + if check_mutations and child_id in oldest_mutation_for_node: |
| 2884 | + max_time = max(max_time, oldest_mutation_for_node[child_id]) |
| 2885 | + if left is None: |
| 2886 | + left = existing_edges_left[edge_id] |
| 2887 | + else: |
| 2888 | + assert left == existing_edges_left[edge_id] |
| 2889 | + if existing_edges_right[edge_id] > interval[0]: |
| 2890 | + # make sure we carry on the edge after this polytomy |
| 2891 | + existing_edges_left[edge_id] = pos |
| 2892 | + # Arbitrarily, if epsilon is not small enough, use half the min dist |
| 2893 | + dt = min((parent_time - max_time) / (len(child_ids) * 2), epsilon) |
| 2894 | + # Break this N-degree polytomy. This requires N-2 extra nodes to be |
| 2895 | + # introduced: create them here in order of decreasing time |
| 2896 | + new_nodes = [ |
| 2897 | + node_table.add_row(time=parent_time - (i * dt)) |
| 2898 | + for i in range(1, len(child_ids) - 1) |
| 2899 | + ] |
| 2900 | + # print("New nodes:", new_nodes, node_table.time[new_nodes]) |
| 2901 | + for new_edge in resolve_polytomy(parent_node, child_ids, new_nodes): |
| 2902 | + edge_table.add_row( |
| 2903 | + left=left, right=pos, child=new_edge[0], parent=new_edge[1], |
| 2904 | + ) |
| 2905 | + # print("new_edge: left={}, right={}, child={}, parent={}" |
| 2906 | + # .format(left, pos, new_edge[0], new_edge[1])) |
| 2907 | + else: |
| 2908 | + # Previous node was not a polytomy - just add the edges_out |
| 2909 | + for edge_id in child_edge_ids: |
| 2910 | + if existing_edges_right[edge_id] == pos: # is an out edge |
| 2911 | + edge_table.add_row( |
| 2912 | + left=existing_edges_left[edge_id], |
| 2913 | + right=pos, |
| 2914 | + parent=parent_node, |
| 2915 | + child=existing_edges_child[edge_id], |
| 2916 | + ) |
| 2917 | + |
| 2918 | + for edge in e_out: |
| 2919 | + if edge.parent != tskit.NULL: |
| 2920 | + # print("REMOVE", edge.id) |
| 2921 | + edges_from_node[edge.parent].remove(edge.id) |
| 2922 | + for edge in e_in: |
| 2923 | + if edge.parent != tskit.NULL: |
| 2924 | + # print("ADD", edge.id) |
| 2925 | + edges_from_node[edge.parent].add(edge.id) |
| 2926 | + |
| 2927 | + # Chop if we have created a polytomy: the polytomy itself will be resolved |
| 2928 | + # at a future iteration, when any edges move into or out of the polytomy |
| 2929 | + while nodes_changed: |
| 2930 | + node = nodes_changed.pop() |
| 2931 | + edge_ids = edges_from_node[node] |
| 2932 | + # print("Looking at", node) |
| 2933 | + |
| 2934 | + if len(edge_ids) == 0: |
| 2935 | + del edges_from_node[node] |
| 2936 | + # if this node has changed *to* a polytomy, we need to cut all of the |
| 2937 | + # child edges that were previously present by adding the previous |
| 2938 | + # segment and left-truncating |
| 2939 | + elif len(edge_ids) >= 3: |
| 2940 | + for edge_id in edge_ids: |
| 2941 | + if existing_edges_left[edge_id] < interval[0]: |
| 2942 | + self.edges.add_row( |
| 2943 | + left=existing_edges_left[edge_id], |
| 2944 | + right=interval[0], |
| 2945 | + parent=existing_edges_parent[edge_id], |
| 2946 | + child=existing_edges_child[edge_id], |
| 2947 | + ) |
| 2948 | + existing_edges_left[edge_id] = interval[0] |
| 2949 | + assert len(edges_from_node) == 0 |
| 2950 | + self.sort() |
| 2951 | + |
| 2952 | + if squash_edges: |
| 2953 | + self.edges.squash() |
| 2954 | + self.sort() # Bug: https://github.com/tskit-dev/tskit/issues/808 |
| 2955 | + |
| 2956 | + if record_provenance: |
| 2957 | + parameters = {"command": "randomly_split_polytomies"} |
| 2958 | + self.provenances.add_row( |
| 2959 | + record=json.dumps(provenance.get_provenance_dict(parameters)) |
| 2960 | + ) |
0 commit comments