# API Reference

I recommend that TACT only be used via its console commands.

While TACT can be used as a Python library, its internal interface is subject to change at any time, even for minor or patch versions. This API documentation is provided merely for the sake of completeness.

## Numerical functions

Functions in tact/lib.py.

Functions to handle various numerical operations, including optimization.

### crown_capture_probability(n, k)

Calculate the probability that a sample of k taxa from a clade of n total taxa includes a root node, under a Yule process.

This equation is taken from:

Sanderson, M. J. 1996. How many taxa must be sampled to identify the root node of a large clade? Systematic Biology 45:168-173

Parameters:

Name Type Description Default
n int

total number of taxa

required
k int

sampled taxa

required

Returns:

Type Description
float

probability of including a root node.

Source code in tact/lib.py
 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 def crown_capture_probability(n, k): """ Calculate the probability that a sample of `k` taxa from a clade of `n` total taxa includes a root node, under a Yule process. This equation is taken from: Sanderson, M. J. 1996. How many taxa must be sampled to identify the root node of a large clade? Systematic Biology 45:168-173 Args: n (int): total number of taxa k (int): sampled taxa Returns: (float): probability of including a root node. """ if n < k: raise Exception(f"n must be greater than or equal to k (n={n}, k={k})") if n == 1 and k == 1: return 0 # not technically correct but it works for our purposes return 1 - 2 * (n - k) / ((n - 1) * (k + 1))

### get_bd(r, a)

Converts turnover and relative extinction to birth and death rates.

Parameters:

Name Type Description Default
r float

turnover or net diversification (birth - death)

required
a float

relative extinction (death / birth)

required

Returns:

Type Description
(float, float)

birth, death

Source code in tact/lib.py
 20 21 22 23 24 25 26 27 28 29 30 31 def get_bd(r, a): """ Converts turnover and relative extinction to birth and death rates. Args: r (float): turnover or net diversification (birth - death) a (float): relative extinction (death / birth) Returns: (float, float): birth, death """ return -r / (a - 1), -a * r / (a - 1)

### get_new_times(ages, birth, death, missing, told=None, tyoung=None)

Simulates new speciation events in an incomplete phylogeny assuming a constant-rate birth-death process.

N. Cusimano, T. Stadler, S. Renner. A new method for handling missing species in diversification analysis applicable to randomly or non-randomly sampled phylogenies. Syst. Biol., 61(5): 785-792, 2012.

Parameters:

Name Type Description Default
ages list

vector of waiting times

required
birth float

birth rate

required
death float

death rate

required
missing int

number of missing taxa to simulate

required
told float

maximum simulated age (default: max(ages))

None
tyoung float

minimum simulated age bound (default: 0)

None

Returns:

Type Description
list

vector of simulated waiting times.

Source code in tact/lib.py
 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 def get_new_times(ages, birth, death, missing, told=None, tyoung=None): """ Simulates new speciation events in an incomplete phylogeny assuming a constant-rate birth-death process. Adapted from the R function `TreeSim::corsim` written by Tanja Stadler. N. Cusimano, T. Stadler, S. Renner. A new method for handling missing species in diversification analysis applicable to randomly or non-randomly sampled phylogenies. Syst. Biol., 61(5): 785-792, 2012. Args: ages (list): vector of waiting times birth (float): birth rate death (float): death rate missing (int): number of missing taxa to simulate told (float): maximum simulated age (default: `max(ages)`) tyoung (float): minimum simulated age bound (default: `0`) Returns: (list): vector of simulated waiting times. """ if told is None: told = max(ages) if len(ages) > 0: if max(ages) > told and abs(max(ages) - told) > sys.float_info.epsilon: raise Exception("Zero or negative branch lengths detected in backbone phylogeny") if tyoung is None: tyoung = 0 ages.sort(reverse=True) times = [x for x in ages if told >= x >= tyoung] times = [told] + times + [tyoung] ranks = range(0, len(times)) only_new = [] while missing > 0: if len(ranks) > 2: distrranks = [] for i in range(1, len(ranks)): temp = ranks[i] * (intp1(times[i - 1], birth, death) - intp1(times[i], birth, death)) distrranks.append(temp) try: dsum = sum(distrranks) distrranks = [x / dsum for x in distrranks] for i in range(1, len(distrranks)): distrranks[i] = distrranks[i] + distrranks[i - 1] r = random.uniform(0, 1) addrank = min([idx for idx, x in enumerate(distrranks) if x > r]) except ZeroDivisionError: addrank = 0 except ValueError: addrank = 0 else: addrank = 0 r = random.uniform(0, 1) const = intp1(times[addrank], birth, death) - intp1(times[addrank + 1], birth, death) try: temp = intp1(times[addrank + 1], birth, death) / const except ZeroDivisionError: temp = 0.0 xnew = 1 / (death - birth) * log((1 - (r + temp) * const * birth) / (1 - (r + temp) * const * death)) only_new.append(xnew) missing -= 1 only_new.sort(reverse=True) return only_new

### get_ra(b, d)

Converts birth and death to turnover and relative extinction rates.

Parameters:

Name Type Description Default
b float

birth rate

required
d float

extinction rate

required

Returns:

Type Description
(float, float)

turnover, relative extinction

Source code in tact/lib.py
 34 35 36 37 38 39 40 41 42 43 44 45 def get_ra(b, d): """ Converts birth and death to turnover and relative extinction rates. Args: b (float): birth rate d (float): extinction rate Returns: (float, float): turnover, relative extinction """ return (b - d, d / b)

### intp1(t, l, m)

Source code in tact/lib.py
 218 219 220 221 222 def intp1(t, l, m): # noqa: E741 try: return (1 - exp(-(l - m) * t)) / (l - m * exp(-(l - m) * t)) except OverflowError: return float(intp1_exact(t, l, m))

### intp1_exact(t, l, m)

Exact version of intp1 using Decimal math.

Source code in tact/lib.py
 208 209 210 211 212 213 214 215 def intp1_exact(t, l, m): # noqa: E741 """Exact version of `intp1` using Decimal math.""" l = D(l) # noqa: E741 m = D(m) t = D(t) num = D(1) - (-(l - m) * t).exp() denom = l - m * (-(l - m) * t).exp() return num / denom

### lik_constant(vec, rho, t, root=1, survival=1, p1=p1)

Calculates the likelihood of a constant-rate birth-death process, conditioned on the waiting times of a phylogenetic tree and degree of incomplete sampling.

Based off of the R function TreePar::LikConstant written by Tanja Stadler.

T. Stadler. On incomplete sampling under birth-death models and connections to the sampling-based coalescent. Jour. Theo. Biol. 261: 58-66, 2009.

Parameters:

Name Type Description Default
vec (float, float)

two element tuple of birth and death

required
rho float

sampling fraction

required
t list

vector of waiting times

required
root bool

include the root or not? (default: 1)

1
survival bool

assume survival of the process? (default: 1)

1

Returns:

Type Description
float

likelihood of the birth-death process.

Source code in tact/lib.py
 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 def lik_constant(vec, rho, t, root=1, survival=1, p1=p1): """ Calculates the likelihood of a constant-rate birth-death process, conditioned on the waiting times of a phylogenetic tree and degree of incomplete sampling. Based off of the R function `TreePar::LikConstant` written by Tanja Stadler. T. Stadler. On incomplete sampling under birth-death models and connections to the sampling-based coalescent. Jour. Theo. Biol. 261: 58-66, 2009. Args: vec (float, float): two element tuple of birth and death rho (float): sampling fraction t (list): vector of waiting times root (bool): include the root or not? (default: 1) survival (bool): assume survival of the process? (default: 1) Returns: (float): likelihood of the birth-death process. """ l = vec[0] # noqa: E741 m = vec[1] t.sort(reverse=True) lik = (root + 1) * log(p1(t[0], l, m, rho)) for tt in t[1:]: lik += log(l) + log(p1(tt, l, m, rho)) if survival == 1: lik -= (root + 1) * log(1 - p0(t[0], l, m, rho)) return -lik

### optim_bd(ages, sampling, min_bound=1e-09)

Optimizes birth and death parameters given a vector of splitting times and sampling fraction.

Parameters:

Name Type Description Default
ages list

vector of node ages

required
sampling float

sampling fraction (0, 1]

required
min_bound float

minimum birth rate

1e-09

Returns:

Name Type Description
birth float

optimized birth rate.

death float

optimized death rate.

Source code in tact/lib.py
 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 def optim_bd(ages, sampling, min_bound=1e-9): """ Optimizes birth and death parameters given a vector of splitting times and sampling fraction. Args: ages (list): vector of node ages sampling (float): sampling fraction (0, 1] min_bound (float): minimum birth rate Returns: birth (float): optimized birth rate. death (float): optimized death rate. """ if max(ages) < 0.000001: init_r = 1e-3 else: # Magallon-Sanderson crown estimator init_r = (log((len(ages) + 1) / sampling) - log(2)) / max(ages) init_r = max(1e-3, init_r) bounds = ((min_bound, 100), (0, 1 - min_bound)) result = two_step_optim(wrapped_lik_constant, x0=(init_r, min_bound), bounds=bounds, args=(sampling, ages)) return get_bd(*result)

### optim_yule(ages, sampling, min_bound=1e-09)

Optimizes birth parameter under a Yule model, given a vector of splitting times and sampling fraction.

Parameters:

Name Type Description Default
ages list

vector of node ages

required
sampling float

sampling fraction (0, 1]

required
min_bound float

minimum birth rate

1e-09

Returns:

Name Type Description
birth float

optimized birth rate.

death float

optimized death rate. Always 0.

Source code in tact/lib.py
 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 def optim_yule(ages, sampling, min_bound=1e-9): """ Optimizes birth parameter under a Yule model, given a vector of splitting times and sampling fraction. Args: ages (list): vector of node ages sampling (float): sampling fraction (0, 1] min_bound (float): minimum birth rate Returns: birth (float): optimized birth rate. death (float): optimized death rate. Always 0. """ bounds = (min_bound, 100) result = minimize_scalar(wrapped_lik_constant_yule, bounds=bounds, args=(sampling, ages), method="Bounded") if result["success"]: return (result["x"], 0.0) raise Exception(f"Optimization failed: {result['message']} (code {result['status']})")

### p0(t, l, m, rho)

Source code in tact/lib.py
 160 161 162 163 164 def p0(t, l, m, rho): # noqa: E741 try: return 1 - rho * (l - m) / (rho * l + (l * (1 - rho) - m) * exp(-(l - m) * t)) except FloatingPointError: return float(p0_exact(t, l, m, rho))

### p0_exact(t, l, m, rho)

Exact version of p0 using Decimal math.

Source code in tact/lib.py
 151 152 153 154 155 156 157 def p0_exact(t, l, m, rho): # noqa: E741 "Exact version of `p0` using Decimal math." t = D(t) l = D(l) # noqa: E741 m = D(m) rho = D(rho) return D(1) - rho * (l - m) / (rho * l + (l * (D(1) - rho) - m) * (-(l - m) * t).exp())

### p1(t, l, m, rho)

Optimized version of p1_orig using common subexpression elimination and strength reduction from exponentiation to multiplication.

Source code in tact/lib.py
 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 def p1(t, l, m, rho): # noqa: E741 """ Optimized version of `p1_orig` using common subexpression elimination and strength reduction from exponentiation to multiplication. """ try: ert = np.exp(-(l - m) * t, dtype=np.float64) num = rho * (l - m) ** 2 * ert denom = (rho * l + (l * (1 - rho) - m) * ert) ** 2 res = num / denom except (OverflowError, FloatingPointError): res = float(p1_exact(t, l, m, rho)) if res == 0.0: return sys.float_info.min return res

### p1_exact(t, l, m, rho)

Exact version of p1 using Decimal math.

Source code in tact/lib.py
 167 168 169 170 171 172 173 174 175 def p1_exact(t, l, m, rho): # noqa: E741 """Exact version of `p1` using Decimal math.""" t = D(t) l = D(l) # noqa: E741 m = D(m) rho = D(rho) num = rho * (l - m) ** D(2) * (-(l - m) * t).exp() denom = (rho * l + (l * (1 - rho) - m) * (-(l - m) * t).exp()) ** D(2) return num / denom

### p1_orig(t, l, m, rho)

Original version of p1, here for testing and comparison purposes.

Source code in tact/lib.py
 178 179 180 181 182 183 184 185 186 187 188 def p1_orig(t, l, m, rho): # noqa: E741 """Original version of `p1`, here for testing and comparison purposes.""" try: num = rho * (l - m) ** 2 * np.exp(-(l - m) * t) denom = (rho * l + (l * (1 - rho) - m) * np.exp(-(l - m) * t)) ** 2 res = num / denom except (OverflowError, FloatingPointError): res = float(p1_exact(t, l, m, rho)) if res == 0.0: return sys.float_info.min return res

### two_step_optim(func, x0, bounds, args)

Conduct a two-step function optimization, first by using the fast L-BFGS-B method, and if that fails, use simulated annealing.

Parameters:

Name Type Description Default
func callable

function to optimize

required
x0 tuple

initial conditions

required
bounds tuple

boundary conditions

required
args list

additional arguments to pass to func

required

Returns:

Name Type Description
params tuple

optimized parameter values

Source code in tact/lib.py
 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 def two_step_optim(func, x0, bounds, args): """ Conduct a two-step function optimization, first by using the fast L-BFGS-B method, and if that fails, use simulated annealing. Args: func (callable): function to optimize x0 (tuple): initial conditions bounds (tuple): boundary conditions args (list): additional arguments to pass to `func` Returns: params (tuple): optimized parameter values """ try: result = minimize(func, x0=x0, bounds=bounds, args=args, method="L-BFGS-B") if result["success"]: return result["x"].tolist() except FloatingPointError: pass result = dual_annealing(func, x0=x0, bounds=bounds, args=args) if result["success"]: return result["x"].tolist() raise Exception(f"Optimization failed: {result['message']} (code {result['status']})")

### wrapped_lik_constant(x, sampling, ages)

Wrapper for birth-death likelihood to make optimizing more convenient.

Parameters:

Name Type Description Default
x (float, float)

turnover, relative extinction

required
sampling float

sampling fraction (0, 1]

required
ages list

vector of node ages

required

Returns:

Type Description
float

a likelihood

Source code in tact/lib.py
 48 49 50 51 52 53 54 55 56 57 58 59 60 def wrapped_lik_constant(x, sampling, ages): """ Wrapper for birth-death likelihood to make optimizing more convenient. Args: x (float, float): turnover, relative extinction sampling (float): sampling fraction (0, 1] ages (list): vector of node ages Returns: (float): a likelihood """ return lik_constant(get_bd(*x), sampling, ages)

### wrapped_lik_constant_yule(x, sampling, ages)

Wrapper for Yule likelihood to make optimizing more convenient.

Parameters:

Name Type Description Default
x float

birth rate

required
sampling float

sampling fraction (0, 1]

required
ages list

vector of node ages

required

Returns:

Type Description
float

a likelihood

Source code in tact/lib.py
 63 64 65 66 67 68 69 70 71 72 73 74 75 def wrapped_lik_constant_yule(x, sampling, ages): """ Wrapper for Yule likelihood to make optimizing more convenient. Args: x (float): birth rate sampling (float): sampling fraction (0, 1] ages (list): vector of node ages Returns: (float): a likelihood """ return lik_constant((x, 0.0), sampling, ages)

## Tree functions

Functions in tact/tree_util.py.

Functions specifically to handle DendroPy tree objects.

### compute_node_depths(tree)

Returns a dictionary of node depths for each node with a label.

Source code in tact/tree_util.py
 119 120 121 122 123 124 125 126 127 128 def compute_node_depths(tree): """Returns a dictionary of node depths for each node with a label.""" res = {} for leaf in tree.leaf_node_iter(): cnt = 0 for anc in leaf.ancestor_iter(): if anc.label: cnt += 1 res[leaf.taxon.label] = cnt return res

### count_locked(node)

How many edges under node are locked?

Source code in tact/tree_util.py
 214 215 216 def count_locked(node): """How many edges under `node` are locked?""" sum([x.label == "locked" for x in edge_iter(node)])

### edge_iter(node, filter_fn=None)

Iterates over the child edge of node and all its descendants. Can optionally be filtered by filter_fn.

Source code in tact/tree_util.py
 56 57 58 59 60 61 62 63 64 65 66 def edge_iter(node, filter_fn=None): """ Iterates over the child edge of `node` and all its descendants. Can optionally be filtered by `filter_fn`. """ stack = list(node.child_edge_iter()) while stack: edge = stack.pop() if filter_fn is None or filter_fn(edge): yield edge stack.extend(edge.head_node.child_edge_iter())

### get_age_intervals(node)

Gets the (possibly disjoint) interval that could be generated in the clade under node, assuming that grafts to locked edges are restricted.

Source code in tact/tree_util.py
 242 243 244 245 246 247 248 249 250 def get_age_intervals(node): """ Gets the (possibly disjoint) interval that could be generated in the clade under `node`, assuming that grafts to locked edges are restricted. """ acc = portion.empty() for edge in edge_iter(node, lambda x: x.label != "locked"): acc = acc | portion.closed(edge.head_node.age, edge.tail_node.age) return acc

### get_ages(node, include_root=False)

Returns the list of ages of the children of a given node, optionally including the node's age if include_root is True.

Source code in tact/tree_util.py
 37 38 39 40 41 42 43 44 45 def get_ages(node, include_root=False): """ Returns the list of ages of the children of a given `node`, optionally including the `node`'s age if `include_root` is True. """ ages = [x.age for x in node.ageorder_iter(include_leaves=False, descending=True)] if include_root: ages += [node.age] return ages

### get_birth_death_rates(node, sampfrac, yule=False, include_root=False)

Estimates the birth and death rates for the subtree descending from node with sampling fraction sampfrac. Optionally restrict to a Yule pure-birth model.

Source code in tact/tree_util.py
 16 17 18 19 20 21 22 23 24 25 def get_birth_death_rates(node, sampfrac, yule=False, include_root=False): """ Estimates the birth and death rates for the subtree descending from `node` with sampling fraction `sampfrac`. Optionally restrict to a Yule pure-birth model. """ if yule: return optim_yule(get_ages(node, include_root), sampfrac) return optim_bd(get_ages(node, include_root), sampfrac)

### get_min_age(node)

Gets the minimum possible age that could be generated in a clade under node, assuming that grafts to locked edges are restricted.

Source code in tact/tree_util.py
 226 227 228 229 230 231 232 233 234 235 236 237 238 239 def get_min_age(node): """ Gets the minimum possible age that could be generated in a clade under `node`, assuming that grafts to locked edges are restricted. """ interval = get_age_intervals(node) if not interval.atomic: raise DisjointConstraintError(f"Constraint on {node} implies disjoint interval {interval}") if interval.empty: return 0.0 return interval.lower

### get_monophyletic_node(tree, species)

Returns the node or None that is the MRCA of the species in tree.

Source code in tact/tree_util.py
 28 29 30 31 32 33 34 def get_monophyletic_node(tree, species): """Returns the node or None that is the MRCA of the `species` in `tree`.""" mrca = tree.mrca(taxon_labels=species) if mrca and species.issuperset(get_tip_labels(mrca)): return mrca return None

### get_short_branches(node)

Yields an iterator of especially short edges under node.

Source code in tact/tree_util.py
 112 113 114 115 116 def get_short_branches(node): """Yields an iterator of especially short edges under `node`.""" for edge in edge_iter(node): if edge.length <= 0.001: yield edge

### get_tip_labels(tree_or_node)

Returns a set of tip labels for a node or tree.

Source code in tact/tree_util.py
 48 49 50 51 52 53 def get_tip_labels(tree_or_node): """Returns a `set` of tip labels for a node or tree.""" try: return {x.taxon.label for x in tree_or_node.leaf_node_iter()} except AttributeError: return {x.taxon.label for x in tree_or_node.leaf_iter()}

### get_tree(path, namespace=None)

Gets a DendroPy tree from a path and precalculate its node ages and bipartition bitmask.

Source code in tact/tree_util.py
 69 70 71 72 73 74 75 def get_tree(path, namespace=None): """ Gets a DendroPy tree from a path and precalculate its node ages and bipartition bitmask. """ tree = dendropy.Tree.get_from_path(path, schema="newick", taxon_namespace=namespace, rooting="default-rooted") update_tree_view(tree) return tree

### graft_node(graft_recipient, graft, stem=False)

Grafts a node graft randomly in the subtree below node graft_recipient. The attribute graft.age must be set so we know where is the best place to graft the node. The node graft can optionally have child nodes, in this case the edge.length attribute should be set on all child nodes if the tree is to remain ultrametric.

We graft things "below" a node by picking one of the children of that node and forcing it to be sister to the grafted node and adjusting the edge lengths accordingly. Therefore, the node above which the graft lives (i.e., the one that will be the child of the new graft) must fulfill the following requirements:

1. Must not be the crown node (cannot graft things above crown node)
2. Must be younger than the graft node (no negative branches)
3. Seed node must be older than graft node (no negative branches)
4. Must not be locked (intruding on monophyly)
Source code in tact/tree_util.py
 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 def graft_node(graft_recipient, graft, stem=False): """ Grafts a node `graft` randomly in the subtree below node `graft_recipient`. The attribute `graft.age` must be set so we know where is the best place to graft the node. The node `graft` can optionally have child nodes, in this case the `edge.length` attribute should be set on all child nodes if the tree is to remain ultrametric. We graft things "below" a node by picking one of the children of that node and forcing it to be sister to the grafted node and adjusting the edge lengths accordingly. Therefore, the node *above* which the graft lives (i.e., the one that will be the child of the new graft) must fulfill the following requirements: 1. Must not be the crown node (cannot graft things above crown node) 2. Must be younger than the graft node (no negative branches) 3. Seed node must be older than graft node (no negative branches) 4. Must not be locked (intruding on monophyly) """ def filter_fn(x): return x.head_node.age <= graft.age and x.head_node.parent_node.age >= graft.age and x.label != "locked" all_edges = list(edge_iter(graft_recipient)) if stem: # also include the crown node's subtending edge all_edges.append(graft_recipient.edge) eligible_edges = [x for x in all_edges if filter_fn(x)] if not eligible_edges: raise Exception(f"could not place node {graft} in clade {graft_recipient}") focal_node = random.choice([x.head_node for x in eligible_edges]) seed_node = focal_node.parent_node sisters = focal_node.sibling_nodes() # pick a child edge and detach its corresponding node # # DendroPy's Node.remove_child() messes with the edge lengths. # But, Node.clear_child_nodes() simply cuts that bit of the tree out. seed_node.clear_child_nodes() # set the correct edge length on the grafted node and make the grafted # node a child of the seed node graft.edge.length = seed_node.age - graft.age if graft.edge.length < 0: raise Exception("negative branch length") sisters.append(graft) seed_node.set_child_nodes(sisters) # make the focal node a child of the grafted node and set edge length focal_node.edge.length = graft.age - focal_node.age if focal_node.edge.length < 0: raise Exception("negative branch length") graft.add_child(focal_node) # return the (potentially new) crown of the clade if graft_recipient.parent_node == graft: return graft return graft_recipient

### is_binary(node)

Is the subtree under node a fully bifurcating tree?

Source code in tact/tree_util.py
 90 91 92 93 94 95 def is_binary(node): """Is the subtree under `node` a fully bifurcating tree?""" for internal_node in node.preorder_internal_node_iter(): if len(internal_node.child_nodes()) != 2: return False return True

### is_fully_locked(node)

Are all the edges below node locked?

Source code in tact/tree_util.py
 219 220 221 222 223 def is_fully_locked(node): """ Are all the edges below `node` locked? """ return all(x.label == "locked" for x in edge_iter(node))

### is_ultrametric(tree, tolerance=1e-06)

Is the tree ultrametric, within a specified tolerance?

Uses the relative difference between minimum and maximum root-to-tip distances.

Source code in tact/tree_util.py
 98 99 100 101 102 103 104 105 106 107 108 109 def is_ultrametric(tree, tolerance=1e-6): """Is the `tree` ultrametric, within a specified `tolerance`? Uses the relative difference between minimum and maximum root-to-tip distances. """ tree.calc_node_root_distances() lengths = {} for leaf in tree.leaf_node_iter(): lengths[leaf.taxon.label] = leaf.root_distance t_min = min(lengths.items(), key=lambda x: x[1]) t_max = max(lengths.items(), key=lambda x: x[1]) return (math.isclose(t_min[1], t_max[1], rel_tol=tolerance), (t_min, t_max))

Locks a clade descending from node so future grafts will avoid locked edges.

Source code in tact/tree_util.py
 194 195 196 197 198 199 200 201 def lock_clade(node, stem=False): """ Locks a clade descending from `node` so future grafts will avoid locked edges. """ for edge in edge_iter(node): edge.label = "locked" if stem: node.edge.label = "locked"

Unlocks a clade descending from node so new tips can be grafted to its edges.

Source code in tact/tree_util.py
 204 205 206 207 208 209 210 211 def unlock_clade(node, stem=False): """ Unlocks a clade descending from `node` so new tips can be grafted to its edges. """ for edge in edge_iter(node): edge.label = "" if stem: node.edge.label = ""

### update_tree_view(tree)

Mutates a DendroPy tree object with updated node ages and bipartition bitmask. We also correct for minor ultrametricity errors.

Returns a list of tip labels.

Source code in tact/tree_util.py
 78 79 80 81 82 83 84 85 86 87 def update_tree_view(tree): """ Mutates a DendroPy tree object with updated node ages and bipartition bitmask. We also correct for minor ultrametricity errors. Returns a list of tip labels. """ tree.calc_node_ages(is_force_max_age=True) tree.update_bipartitions() return get_tip_labels(tree)

## FastMRCA

Functions in tact/fastmrca.py.

Singleton object that helps speed up MRCA lookups.

Gets a bitmask for the taxa in labels, potentially in parallel.

Source code in tact/fastmrca.py
 18 19 20 21 22 23 24 def bitmask(labels): """ Gets a bitmask for the taxa in `labels`, potentially in parallel. """ global tree tn = tree.taxon_namespace return tn.taxa_bitmask(labels=labels)

### fastmrca_getter(tn, x)

Helper function for submitting stuff.

Source code in tact/fastmrca.py
 37 38 39 40 41 42 43 def fastmrca_getter(tn, x): """Helper function for submitting stuff.""" taxa = tn.get_taxa(labels=x) mask = 0 for taxon in taxa: mask |= tn.taxon_bitmask(taxon) return mask

### get(labels)

Pulls a MRCA node out for the taxa in labels.

Source code in tact/fastmrca.py
 27 28 29 30 31 32 33 34 def get(labels): """Pulls a MRCA node out for the taxa in `labels`.""" global tree labels = set(labels) mrca = tree.mrca(leafset_bitmask=bitmask(labels)) if mrca and labels.issuperset(get_tip_labels(mrca)): return mrca return None

### initialize(phy)

Initialize the fastmrca singleton with a tree.

Source code in tact/fastmrca.py
 10 11 12 13 14 15 def initialize(phy): """ Initialize the fastmrca singleton with a tree. """ global tree tree = phy

## Validation

Functions in tact/validation.py.

Various validation functions for click classes and parameters.

### BackboneCommand

Bases: Command

Helper class to validate a Click Command that contains a backbone tree.

At a minimum, the Command must contain a backbone parameter, which is validated by validate_newick and checked to ensure it is a binary tree.

If the command also contains a taxonomy parameter, representing a taxonomic phylogeny, this is also validated to ensure that the DendroPy TaxonNamespace is non-strict superset of the taxa contained in backbone. An optional outgroups parameter may add other taxa not in the taxonomy.

If the command also contains an ultrametricity_precision parameter, the ultrametricity of the backbone is also checked.

Source code in tact/validation.py
 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 class BackboneCommand(click.Command): """ Helper class to validate a Click Command that contains a backbone tree. At a minimum, the Command must contain a `backbone` parameter, which is validated by `validate_newick` and checked to ensure it is a binary tree. If the command also contains a `taxonomy` parameter, representing a taxonomic phylogeny, this is also validated to ensure that the DendroPy TaxonNamespace is non-strict superset of the taxa contained in `backbone`. An optional `outgroups` parameter may add other taxa not in the `taxonomy`. If the command also contains an `ultrametricity_precision` parameter, the ultrametricity of the `backbone` is also checked. """ def validate_backbone_variables(self, ctx, params): if "taxonomy" in params: tn = params["taxonomy"].taxon_namespace tn.is_mutable = True if "outgroups" in params and params["outgroups"]: tn.new_taxa(params["outgroups"]) tn.is_mutable = False try: backbone = validate_newick(ctx, params, params["backbone"], taxon_namespace=tn) except dendropy.utility.error.ImmutableTaxonNamespaceError as e: msg = f""" DendroPy error: {e} This usually indicates your backbone has species that are not present in your taxonomy. Outgroups not in the taxonomy can be excluded with the --outgroups argument. """ raise click.BadParameter(msg) else: backbone = validate_newick(ctx, params, params["backbone"]) if not is_binary(backbone): raise click.BadParameter("Backbone tree is not binary!") update_tree_view(backbone) if "ultrametricity_precision" in params: ultra, res = is_ultrametric(backbone, params["ultrametricity_precision"]) if not ultra: msg = f""" Tree is not ultrametric! {res[0][0]} has a root distance of {res[0][1]}, but {res[1][0]} has {res[1][1]} Increase `--ultrametricity-precision` or use phytools::force.ultrametric in R """ raise click.BadParameter(msg) params["backbone"] = backbone return params def make_context(self, *args, **kwargs): ctx = super(BackboneCommand, self).make_context(*args, **kwargs) ctx.params = self.validate_backbone_variables(ctx, ctx.params) return ctx

#### make_context(*args, **kwargs)

Source code in tact/validation.py
 107 108 109 110 def make_context(self, *args, **kwargs): ctx = super(BackboneCommand, self).make_context(*args, **kwargs) ctx.params = self.validate_backbone_variables(ctx, ctx.params) return ctx

#### validate_backbone_variables(ctx, params)

Source code in tact/validation.py
 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 def validate_backbone_variables(self, ctx, params): if "taxonomy" in params: tn = params["taxonomy"].taxon_namespace tn.is_mutable = True if "outgroups" in params and params["outgroups"]: tn.new_taxa(params["outgroups"]) tn.is_mutable = False try: backbone = validate_newick(ctx, params, params["backbone"], taxon_namespace=tn) except dendropy.utility.error.ImmutableTaxonNamespaceError as e: msg = f""" DendroPy error: {e} This usually indicates your backbone has species that are not present in your taxonomy. Outgroups not in the taxonomy can be excluded with the --outgroups argument. """ raise click.BadParameter(msg) else: backbone = validate_newick(ctx, params, params["backbone"]) if not is_binary(backbone): raise click.BadParameter("Backbone tree is not binary!") update_tree_view(backbone) if "ultrametricity_precision" in params: ultra, res = is_ultrametric(backbone, params["ultrametricity_precision"]) if not ultra: msg = f""" Tree is not ultrametric! {res[0][0]} has a root distance of {res[0][1]}, but {res[1][0]} has {res[1][1]} Increase `--ultrametricity-precision` or use phytools::force.ultrametric in R """ raise click.BadParameter(msg) params["backbone"] = backbone return params

### validate_newick(ctx, param, value, **kwargs)

Validates a Newick tree, using appropriate defaults.

Source code in tact/validation.py
 28 29 30 def validate_newick(ctx, param, value, **kwargs): """Validates a Newick tree, using appropriate defaults.""" return dendropy.Tree.get_from_stream(value, schema="newick", rooting="default-rooted", **kwargs)

### validate_outgroups(ctx, param, value)

Validates an outgroups parameter, by splitting on commas and transforming underscores to spaces.

Source code in tact/validation.py
 16 17 18 19 20 21 22 23 24 25 def validate_outgroups(ctx, param, value): """Validates an `outgroups` parameter, by splitting on commas and transforming underscores to spaces.""" if value is None: return try: value = value.split(",") except AttributeError: # Tuples and lists shouldn't have the .split method pass return [x.replace("_", " ") for x in value]

### validate_taxonomy_tree(ctx, param, value)

Validates a taxonomy tree.

Source code in tact/validation.py
 47 48 49 50 def validate_taxonomy_tree(ctx, param, value): """Validates a taxonomy tree.""" value = validate_newick(ctx, param, value) return validate_tree_node_depths(ctx, param, value)

### validate_tree_node_depths(ctx, param, value)

Validates a DendroPy tree, ensuring that the node depth is equal for all tips.

Source code in tact/validation.py
 33 34 35 36 37 38 39 40 41 42 43 44 def validate_tree_node_depths(ctx, param, value): """Validates a DendroPy tree, ensuring that the node depth is equal for all tips.""" node_depths = compute_node_depths(value) stats = collections.defaultdict(int) for v in node_depths.values(): stats[v] += 1 if len(stats) > 1: msg = "The tips of your taxonomy tree do not have equal numbers of ranked clades in their ancestor chain:\n" for k in sorted(stats.keys()): msg += f"* {stats[k]} tips have {k} ranked ancestors\n" raise click.BadParameter(msg) return value

## Exceptions

Functions in tact/exceptions.py.

### DisjointConstraintError

Bases: TactError

Exception raised when a set of constraints lead to a disjoint implied age interval.

Source code in tact/exceptions.py
 5 6 class DisjointConstraintError(TactError): """Exception raised when a set of constraints lead to a disjoint implied age interval."""

### TactError

Bases: Exception

Base class for errors raised by TACT.

Source code in tact/exceptions.py
 1 2 class TactError(Exception): """Base class for errors raised by TACT."""