def simulate_tree(self, ) -> CassiopeiaTree: """Simulates a complete binary tree. Returns: A CassiopeiaTree with the tree topology initialized with the simulated tree """ def node_name_generator() -> Generator[str, None, None]: """Generates unique node names for the tree.""" i = 0 while True: yield str(i) i += 1 names = node_name_generator() tree = nx.balanced_tree(2, self.depth, create_using=nx.DiGraph) mapping = {"root": next(names)} mapping.update({node: next(names) for node in tree.nodes}) # Add root, which indicates the initiating cell tree.add_edge("root", 0) nx.relabel_nodes(tree, mapping, copy=False) cassiopeia_tree = CassiopeiaTree(tree=tree) # Initialize branch lengths time_dict = { node: cassiopeia_tree.get_time(node) / (self.depth + 1) for node in cassiopeia_tree.nodes } cassiopeia_tree.set_times(time_dict) return cassiopeia_tree
def simulate_tree(self, ) -> CassiopeiaTree: """Simulates trees from a general birth/death process with fitness. A forward-time birth/death process is simulated by tracking a series of lineages and sampling event waiting times for each lineage. Each lineage draws death waiting times from the same distribution, but maintains its own birth scale parameter that determines the shape of its birth waiting time distribution. At each division event, fitness mutation events are sampled, and the birth scale parameter is scaled by their multiplicative coefficients. This updated birth scale passed onto successors. Returns: A CassiopeiaTree with the tree topology initialized with the simulated tree Raises: TreeSimulatorError if all lineages die before a stopping condition """ def node_name_generator() -> Generator[str, None, None]: """Generates unique node names for the tree.""" i = 0 while True: yield str(i) i += 1 names = node_name_generator() # Set the seed if self.random_seed: np.random.seed(self.random_seed) # Instantiate the implicit root tree = nx.DiGraph() root = next(names) tree.add_node(root) tree.nodes[root]["birth_scale"] = self.initial_birth_scale tree.nodes[root]["time"] = 0 current_lineages = PriorityQueue() # Records the nodes that are observed at the end of the experiment observed_nodes = [] starting_lineage = { "id": root, "birth_scale": self.initial_birth_scale, "total_time": 0, "active": True, } # Sample the waiting time until the first division self.sample_lineage_event(starting_lineage, current_lineages, tree, names, observed_nodes) # Perform the process until there are no active extant lineages left while not current_lineages.empty(): # If number of extant lineages is the stopping criterion, at the # first instance of having n extant tips, stop the experiment # and set the total lineage time for each lineage to be equal to # the minimum, to produce ultrametric trees. Also, the birth_scale # parameter of each leaf is rolled back to equal its parent's. if self.num_extant: if current_lineages.qsize() == self.num_extant: remaining_lineages = [] while not current_lineages.empty(): _, _, lineage = current_lineages.get() remaining_lineages.append(lineage) min_total_time = remaining_lineages[0]["total_time"] for lineage in remaining_lineages: parent = list(tree.predecessors(lineage["id"]))[0] tree.nodes[lineage["id"]]["time"] += ( min_total_time - lineage["total_time"]) tree.nodes[lineage["id"]]["birth_scale"] = tree.nodes[ parent]["birth_scale"] observed_nodes.append(lineage["id"]) break # Pop the minimum age lineage to simulate forward time _, _, lineage = current_lineages.get() # If the lineage is no longer active, just remove it from the queue. # This represents the time at which the lineage dies. if lineage["active"]: for _ in range(2): self.sample_lineage_event(lineage, current_lineages, tree, names, observed_nodes) cassiopeia_tree = CassiopeiaTree(tree=tree) time_dictionary = {} for i in tree.nodes: time_dictionary[i] = tree.nodes[i]["time"] cassiopeia_tree.set_times(time_dictionary) # Prune dead lineages and collapse resulting unifurcations to_remove = list(set(cassiopeia_tree.leaves) - set(observed_nodes)) cassiopeia_tree.remove_leaves_and_prune_lineages(to_remove) if self.collapse_unifurcations and len(cassiopeia_tree.nodes) > 1: cassiopeia_tree.collapse_unifurcations(source="1") # If only implicit root remains after pruning dead lineages, error if len(cassiopeia_tree.nodes) == 1: raise TreeSimulatorError( "All lineages died before stopping condition") return cassiopeia_tree