Skip to content

API Reference

Library usage not recommended

I recommend using TACT mainly via its console commands for stability and best results. To get started, see the Tutorial.

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: int, k: int) -> float

Calculate probability of observing the crown node in an incomplete sample.

Probability that a random sample of k taxa from a clade of n total taxa includes the crown (root) node, under a Yule process.

Reference: Sanderson (1996), Systematic Biology 45:168-173.

Parameters:

Name Type Description Default
n int

Total number of taxa in the clade.

required
k int

Number of sampled taxa.

required

Returns:

Type Description
float

Probability of crown node inclusion.

Raises:

Type Description
Exception

If n < k.

Source code in tact/lib.py
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
def crown_capture_probability(n: int, k: int) -> float:
    """Calculate probability of observing the crown node in an incomplete sample.

    Probability that a random sample of `k` taxa from a clade of `n` total taxa
    includes the crown (root) node, under a Yule process.

    Reference: Sanderson (1996), Systematic Biology 45:168-173.

    Args:
        n: Total number of taxa in the clade.
        k: Number of sampled taxa.

    Returns:
        Probability of crown node inclusion.

    Raises:
        Exception: If `n < k`.
    """
    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.0  # not technically correct but it works for our purposes
    return 1 - 2 * (n - k) / ((n - 1) * (k + 1))

get_bd(r: float, a: float) -> tuple[float, float]

Convert turnover and relative extinction to birth and death rates.

Parameters:

Name Type Description Default
r float

Turnover rate (net diversification, birth - death).

required
a float

Relative extinction rate (death / birth).

required

Returns:

Type Description
tuple[float, float]

Tuple of (birth rate, death rate).

Source code in tact/lib.py
17
18
19
20
21
22
23
24
25
26
27
def get_bd(r: float, a: float) -> tuple[float, float]:
    """Convert turnover and relative extinction to birth and death rates.

    Args:
        r: Turnover rate (net diversification, birth - death).
        a: Relative extinction rate (death / birth).

    Returns:
        Tuple of (birth rate, death rate).
    """
    return -r / (a - 1), -a * r / (a - 1)

get_new_times(ages: list[float], birth: float, death: float, missing: int, told: float | None = None, tyoung: float | None = None) -> list[float]

Simulate new speciation event times in an incomplete phylogeny.

Simulates missing speciation events under a constant-rate birth-death process. Adapted from TreeSim::corsim by Tanja Stadler. Reference: Cusimano et al. (2012), Systematic Biology 61(5):785-792.

Parameters:

Name Type Description Default
ages list[float]

List of existing waiting times (will be sorted in place).

required
birth float

Birth rate.

required
death float

Death rate.

required
missing int

Number of missing taxa to simulate.

required
told float | None

Maximum simulated age. Defaults to max(ages).

None
tyoung float | None

Minimum simulated age. Defaults to 0.

None

Returns:

Type Description
list[float]

List of simulated waiting times, sorted in descending order.

Raises:

Type Description
Exception

If zero or negative branch lengths are detected.

Source code in tact/lib.py
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
def get_new_times(
    ages: list[float],
    birth: float,
    death: float,
    missing: int,
    told: float | None = None,
    tyoung: float | None = None,
) -> list[float]:
    """Simulate new speciation event times in an incomplete phylogeny.

    Simulates missing speciation events under a constant-rate birth-death process.
    Adapted from `TreeSim::corsim` by Tanja Stadler. Reference: Cusimano et al.
    (2012), Systematic Biology 61(5):785-792.

    Args:
        ages: List of existing waiting times (will be sorted in place).
        birth: Birth rate.
        death: Death rate.
        missing: Number of missing taxa to simulate.
        told: Maximum simulated age. Defaults to `max(ages)`.
        tyoung: Minimum simulated age. Defaults to 0.

    Returns:
        List of simulated waiting times, sorted in descending order.

    Raises:
        Exception: If zero or negative branch lengths are detected.
    """
    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: float, d: float) -> tuple[float, float]

Convert birth and death rates to turnover and relative extinction.

Parameters:

Name Type Description Default
b float

Birth rate.

required
d float

Death rate.

required

Returns:

Type Description
tuple[float, float]

Tuple of (turnover rate, relative extinction rate).

Source code in tact/lib.py
30
31
32
33
34
35
36
37
38
39
40
def get_ra(b: float, d: float) -> tuple[float, float]:
    """Convert birth and death rates to turnover and relative extinction.

    Args:
        b: Birth rate.
        d: Death rate.

    Returns:
        Tuple of (turnover rate, relative extinction rate).
    """
    return (b - d, d / b)

intp1(t: float, l: float, m: float) -> float

Compute integration constant for sampling missing speciation event times.

This is a portion of the cdf used to perform inverse-transform sampling of missing speciation event times under a constant-rate birth-death model. It is the c_2 term from equation A.2 in Cusimano et al. (2012), Systematic Biology 61(5):785-792. Originally implemented as TreeSim:::intp1.

Parameters:

Name Type Description Default
t float

Time before present.

required
l float

Birth rate (lambda).

required
m float

Death rate (mu).

required

Returns:

Type Description
float

Integration constant value.

Source code in tact/lib.py
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
def intp1(t: float, l: float, m: float) -> float:  # noqa: E741
    """Compute integration constant for sampling missing speciation event times.

    This is a portion of the cdf used to perform inverse-transform sampling of missing speciation event times
    under a constant-rate birth-death model. It is the c_2 term from equation A.2 in Cusimano et al. (2012),
    Systematic Biology 61(5):785-792. Originally implemented as `TreeSim:::intp1`.

    Args:
        t: Time before present.
        l: Birth rate (lambda).
        m: Death rate (mu).

    Returns:
        Integration constant value.
    """
    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: float, l: float, m: float) -> D

Compute intp1 using exact Decimal arithmetic.

Used as fallback when floating-point arithmetic fails due to overflow.

Parameters:

Name Type Description Default
t float

Time before present.

required
l float

Birth rate (lambda).

required
m float

Death rate (mu).

required

Returns:

Type Description
Decimal

Integration constant as a Decimal.

Source code in tact/lib.py
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
def intp1_exact(t: float, l: float, m: float) -> D:  # noqa: E741
    """Compute intp1 using exact Decimal arithmetic.

    Used as fallback when floating-point arithmetic fails due to overflow.

    Args:
        t: Time before present.
        l: Birth rate (lambda).
        m: Death rate (mu).

    Returns:
        Integration constant as a Decimal.
    """
    l_dec = D(l)
    m_dec = D(m)
    t_dec = D(t)
    num = D(1) - (-(l_dec - m_dec) * t_dec).exp()
    denom = l_dec - m_dec * (-(l_dec - m_dec) * t_dec).exp()
    return num / denom

lik_constant(vec: tuple[float, float], rho: float, t: list[float], root: int = 1, survival: int = 1, p1: Callable[[float, float, float, float], float] = p1) -> float

Calculate likelihood of a constant-rate birth-death process.

Likelihood conditioned on waiting times and incomplete sampling. Based on TreePar::LikConstant by Tanja Stadler. Reference: Stadler (2009), Journal of Theoretical Biology 261:58-66.

Parameters:

Name Type Description Default
vec tuple[float, float]

Tuple of (birth rate, death rate).

required
rho float

Sampling fraction.

required
t list[float]

List of waiting times (will be sorted in place).

required
root int

Include root contribution (1) or not (0). Default: 1.

1
survival int

Assume process survival (1) or not (0). Default: 1.

1
p1 Callable[[float, float, float, float], float]

Function to compute p1 probability (default: p1).

p1

Returns:

Type Description
float

Negative log-likelihood value.

Source code in tact/lib.py
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
def lik_constant(
    vec: tuple[float, float],
    rho: float,
    t: list[float],
    root: int = 1,
    survival: int = 1,
    p1: Callable[[float, float, float, float], float] = p1,
) -> float:
    """Calculate likelihood of a constant-rate birth-death process.

    Likelihood conditioned on waiting times and incomplete sampling. Based on
    `TreePar::LikConstant` by Tanja Stadler. Reference: Stadler (2009), Journal
    of Theoretical Biology 261:58-66.

    Args:
        vec: Tuple of (birth rate, death rate).
        rho: Sampling fraction.
        t: List of waiting times (will be sorted in place).
        root: Include root contribution (1) or not (0). Default: 1.
        survival: Assume process survival (1) or not (0). Default: 1.
        p1: Function to compute p1 probability (default: `p1`).

    Returns:
        Negative log-likelihood value.
    """
    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: list[float], sampling: float, min_bound: float = 1e-09) -> tuple[float, float]

Optimize birth and death rates from node ages under a birth-death model.

Uses maximum likelihood estimation with the Magallon-Sanderson crown estimator for initial values.

Parameters:

Name Type Description Default
ages list[float]

List of node ages (splitting times).

required
sampling float

Sampling fraction in (0, 1].

required
min_bound float

Minimum allowed birth rate (default: 1e-9).

1e-09

Returns:

Type Description
tuple[float, float]

Tuple of (optimized birth rate, optimized death rate).

Source code in tact/lib.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
def optim_bd(ages: list[float], sampling: float, min_bound: float = 1e-9) -> tuple[float, float]:
    """Optimize birth and death rates from node ages under a birth-death model.

    Uses maximum likelihood estimation with the Magallon-Sanderson crown estimator
    for initial values.

    Args:
        ages: List of node ages (splitting times).
        sampling: Sampling fraction in (0, 1].
        min_bound: Minimum allowed birth rate (default: 1e-9).

    Returns:
        Tuple of (optimized birth rate, 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: list[float], sampling: float, min_bound: float = 1e-09) -> tuple[float, float]

Optimize birth rate under a Yule (pure birth) model.

Assumes zero extinction rate.

Parameters:

Name Type Description Default
ages list[float]

List of node ages (splitting times).

required
sampling float

Sampling fraction in (0, 1].

required
min_bound float

Minimum allowed birth rate (default: 1e-9).

1e-09

Returns:

Type Description
tuple[float, float]

Tuple of (optimized birth rate, 0.0).

Raises:

Type Description
Exception

If optimization fails.

Source code in tact/lib.py
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
def optim_yule(ages: list[float], sampling: float, min_bound: float = 1e-9) -> tuple[float, float]:
    """Optimize birth rate under a Yule (pure birth) model.

    Assumes zero extinction rate.

    Args:
        ages: List of node ages (splitting times).
        sampling: Sampling fraction in (0, 1].
        min_bound: Minimum allowed birth rate (default: 1e-9).

    Returns:
        Tuple of (optimized birth rate, 0.0).

    Raises:
        Exception: If optimization fails.
    """
    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: float, l: float, m: float, rho: float) -> float

Compute the probability of no sampled descendants.

Probability that an individual alive at time t before present has no sampled descendants (extant or extinct), assuming no past sampling. Falls back to exact Decimal arithmetic if floating-point overflow occurs.

Reference: Stadler (2010), Journal of Theoretical Biology 267(3):396-404, remark 3.2. Originally implemented as TreePar:::p0.

Parameters:

Name Type Description Default
t float

Time before present.

required
l float

Birth rate (lambda).

required
m float

Death rate (mu).

required
rho float

Sampling fraction.

required

Returns:

Type Description
float

Probability of no sampled descendants.

Source code in tact/lib.py
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
def p0(t: float, l: float, m: float, rho: float) -> float:  # noqa: E741
    """Compute the probability of no sampled descendants.

    Probability that an individual alive at time `t` before present has no sampled
    descendants (extant or extinct), assuming no past sampling. Falls back to
    exact Decimal arithmetic if floating-point overflow occurs.

    Reference: Stadler (2010), Journal of Theoretical Biology 267(3):396-404,
    remark 3.2. Originally implemented as `TreePar:::p0`.

    Args:
        t: Time before present.
        l: Birth rate (lambda).
        m: Death rate (mu).
        rho: Sampling fraction.

    Returns:
        Probability of no sampled descendants.
    """
    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: float, l: float, m: float, rho: float) -> D

Compute p0 using exact Decimal arithmetic.

Used as fallback when floating-point arithmetic fails due to overflow.

Parameters:

Name Type Description Default
t float

Time before present.

required
l float

Birth rate (lambda).

required
m float

Death rate (mu).

required
rho float

Sampling fraction.

required

Returns:

Type Description
Decimal

Probability of no sampled descendants as a Decimal.

Source code in tact/lib.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
def p0_exact(t: float, l: float, m: float, rho: float) -> D:  # noqa: E741
    """Compute p0 using exact Decimal arithmetic.

    Used as fallback when floating-point arithmetic fails due to overflow.

    Args:
        t: Time before present.
        l: Birth rate (lambda).
        m: Death rate (mu).
        rho: Sampling fraction.

    Returns:
        Probability of no sampled descendants as a Decimal.
    """
    t_dec = D(t)
    l_dec = D(l)
    m_dec = D(m)
    rho_dec = D(rho)
    return D(1) - rho_dec * (l_dec - m_dec) / (
        rho_dec * l_dec + (l_dec * (D(1) - rho_dec) - m_dec) * (-(l_dec - m_dec) * t_dec).exp()
    )

p1(t: float, l: float, m: float, rho: float) -> float

Compute the probability of exactly one sampled descendant.

Probability that an individual alive at time t before present has precisely one sampled extant descendant and no sampled extinct descendants, assuming no past sampling. Optimized version using common subexpression elimination.

Reference: Stadler (2010), Journal of Theoretical Biology 267(3):396-404, remark 3.2. Originally implemented as TreePar:::p1.

Parameters:

Name Type Description Default
t float

Time before present.

required
l float

Birth rate (lambda).

required
m float

Death rate (mu).

required
rho float

Sampling fraction.

required

Returns:

Type Description
float

Probability of exactly one sampled descendant.

Source code in tact/lib.py
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
def p1(t: float, l: float, m: float, rho: float) -> float:  # noqa: E741
    """Compute the probability of exactly one sampled descendant.

    Probability that an individual alive at time `t` before present has precisely
    one sampled extant descendant and no sampled extinct descendants, assuming
    no past sampling. Optimized version using common subexpression elimination.

    Reference: Stadler (2010), Journal of Theoretical Biology 267(3):396-404,
    remark 3.2. Originally implemented as `TreePar:::p1`.

    Args:
        t: Time before present.
        l: Birth rate (lambda).
        m: Death rate (mu).
        rho: Sampling fraction.

    Returns:
        Probability of exactly one sampled descendant.
    """
    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: float = 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: float, l: float, m: float, rho: float) -> D

Compute p1 using exact Decimal arithmetic.

Used as fallback when floating-point arithmetic fails due to overflow.

Parameters:

Name Type Description Default
t float

Time before present.

required
l float

Birth rate (lambda).

required
m float

Death rate (mu).

required
rho float

Sampling fraction.

required

Returns:

Type Description
Decimal

Probability of exactly one sampled descendant as a Decimal.

Source code in tact/lib.py
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
def p1_exact(t: float, l: float, m: float, rho: float) -> D:  # noqa: E741
    """Compute p1 using exact Decimal arithmetic.

    Used as fallback when floating-point arithmetic fails due to overflow.

    Args:
        t: Time before present.
        l: Birth rate (lambda).
        m: Death rate (mu).
        rho: Sampling fraction.

    Returns:
        Probability of exactly one sampled descendant as a Decimal.
    """
    t_dec = D(t)
    l_dec = D(l)
    m_dec = D(m)
    rho_dec = D(rho)
    num = rho_dec * (l_dec - m_dec) ** D(2) * (-(l_dec - m_dec) * t_dec).exp()
    denom = (rho_dec * l_dec + (l_dec * (D(1) - rho_dec) - m_dec) * (-(l_dec - m_dec) * t_dec).exp()) ** D(2)
    return num / denom

p1_orig(t: float, l: float, m: float, rho: float) -> float

Original implementation of p1 for testing and comparison.

Parameters:

Name Type Description Default
t float

Time before present.

required
l float

Birth rate (lambda).

required
m float

Death rate (mu).

required
rho float

Sampling fraction.

required

Returns:

Type Description
float

Probability of exactly one sampled descendant.

Source code in tact/lib.py
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
def p1_orig(t: float, l: float, m: float, rho: float) -> float:  # noqa: E741
    """Original implementation of p1 for testing and comparison.

    Args:
        t: Time before present.
        l: Birth rate (lambda).
        m: Death rate (mu).
        rho: Sampling fraction.

    Returns:
        Probability of exactly one sampled descendant.
    """
    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: float = 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: Callable[..., float], x0: tuple[float, ...], bounds: tuple[tuple[float, float], ...], args: tuple[Any, ...]) -> list[float]

Optimize a function using a two-step approach.

First attempts L-BFGS-B (fast gradient-based method), then falls back to simulated annealing if L-BFGS-B fails.

Parameters:

Name Type Description Default
func Callable[..., float]

Objective function to minimize.

required
x0 tuple[float, ...]

Initial parameter values.

required
bounds tuple[tuple[float, float], ...]

Parameter bounds as tuples of (min, max) for each parameter.

required
args tuple[Any, ...]

Additional arguments to pass to the objective function.

required

Returns:

Type Description
list[float]

Optimized parameter values as a list.

Raises:

Type Description
Exception

If both optimization methods fail.

Source code in tact/lib.py
 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
def two_step_optim(
    func: Callable[..., float], x0: tuple[float, ...], bounds: tuple[tuple[float, float], ...], args: tuple[Any, ...]
) -> list[float]:
    """Optimize a function using a two-step approach.

    First attempts L-BFGS-B (fast gradient-based method), then falls back to
    simulated annealing if L-BFGS-B fails.

    Args:
        func: Objective function to minimize.
        x0: Initial parameter values.
        bounds: Parameter bounds as tuples of (min, max) for each parameter.
        args: Additional arguments to pass to the objective function.

    Returns:
        Optimized parameter values as a list.

    Raises:
        Exception: If both optimization methods fail.
    """
    try:
        result = minimize(func, x0=x0, bounds=bounds, args=args, method="L-BFGS-B")
        if result["success"]:
            return result["x"].tolist()  # type: ignore[no-any-return]
    except FloatingPointError:
        pass

    result = dual_annealing(func, x0=x0, bounds=bounds, args=args)
    if result["success"]:
        return result["x"].tolist()  # type: ignore[no-any-return]

    raise Exception(f"Optimization failed: {result['message']} (code {result['status']})")

wrapped_lik_constant(x: tuple[float, float], sampling: float, ages: list[float]) -> float

Wrapper for birth-death likelihood function for optimization.

Converts turnover and relative extinction parameters to birth/death rates before computing the likelihood.

Parameters:

Name Type Description Default
x tuple[float, float]

Tuple of (turnover, relative extinction).

required
sampling float

Sampling fraction in (0, 1].

required
ages list[float]

List of node ages.

required

Returns:

Type Description
float

Negative log-likelihood value.

Source code in tact/lib.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def wrapped_lik_constant(x: tuple[float, float], sampling: float, ages: list[float]) -> float:
    """Wrapper for birth-death likelihood function for optimization.

    Converts turnover and relative extinction parameters to birth/death rates
    before computing the likelihood.

    Args:
        x: Tuple of (turnover, relative extinction).
        sampling: Sampling fraction in (0, 1].
        ages: List of node ages.

    Returns:
        Negative log-likelihood value.
    """
    return lik_constant(get_bd(*x), sampling, ages)

wrapped_lik_constant_yule(x: float, sampling: float, ages: list[float]) -> float

Wrapper for Yule model likelihood function for optimization.

Assumes zero extinction (pure birth process).

Parameters:

Name Type Description Default
x float

Birth rate.

required
sampling float

Sampling fraction in (0, 1].

required
ages list[float]

List of node ages.

required

Returns:

Type Description
float

Negative log-likelihood value.

Source code in tact/lib.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def wrapped_lik_constant_yule(x: float, sampling: float, ages: list[float]) -> float:
    """Wrapper for Yule model likelihood function for optimization.

    Assumes zero extinction (pure birth process).

    Args:
        x: Birth rate.
        sampling: Sampling fraction in (0, 1].
        ages: List of node ages.

    Returns:
        Negative log-likelihood value.
    """
    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: dendropy.Tree) -> dict[str, int]

Compute node depths for all labeled nodes.

Depth is defined as the number of labeled ancestor nodes.

Parameters:

Name Type Description Default
tree Tree

DendroPy tree object.

required

Returns:

Type Description
dict[str, int]

Dictionary mapping tip labels to their node depths.

Source code in tact/tree_util.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
def compute_node_depths(tree: dendropy.Tree) -> dict[str, int]:
    """Compute node depths for all labeled nodes.

    Depth is defined as the number of labeled ancestor nodes.

    Args:
        tree: DendroPy tree object.

    Returns:
        Dictionary mapping tip labels to their node depths.
    """
    res: dict[str, int] = {}
    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: dendropy.Node) -> int

Count the number of locked edges in a subtree.

Parameters:

Name Type Description Default
node Node

Node representing the root of the subtree.

required

Returns:

Type Description
int

Number of edges marked as locked.

Source code in tact/tree_util.py
309
310
311
312
313
314
315
316
317
318
def count_locked(node: dendropy.Node) -> int:
    """Count the number of locked edges in a subtree.

    Args:
        node: Node representing the root of the subtree.

    Returns:
        Number of edges marked as locked.
    """
    return sum([x.label == "locked" for x in edge_iter(node)])

edge_iter(node: dendropy.Node, filter_fn: Callable[[dendropy.Edge], bool] | None = None) -> dendropy.Edge

Iterate over edges in a subtree.

Parameters:

Name Type Description Default
node Node

DendroPy node representing the root of the subtree.

required
filter_fn Callable[[Edge], bool] | None

Optional function to filter edges. Default: None.

None

Yields:

Type Description
Edge

DendroPy edge objects in the subtree.

Source code in tact/tree_util.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def edge_iter(node: dendropy.Node, filter_fn: Callable[[dendropy.Edge], bool] | None = None) -> dendropy.Edge:
    """Iterate over edges in a subtree.

    Args:
        node: DendroPy node representing the root of the subtree.
        filter_fn: Optional function to filter edges. Default: None.

    Yields:
        DendroPy edge objects in the subtree.
    """
    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: dendropy.Node) -> portion.Interval

Get the age interval for possible grafts in a clade.

Computes the union of all age intervals from unlocked edges, which represents all possible ages where a graft could be placed.

Parameters:

Name Type Description Default
node Node

Node representing the root of the clade.

required

Returns:

Type Description
Interval

Portion interval (possibly disjoint) representing valid graft ages.

Source code in tact/tree_util.py
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
def get_age_intervals(node: dendropy.Node) -> portion.Interval:
    """Get the age interval for possible grafts in a clade.

    Computes the union of all age intervals from unlocked edges, which
    represents all possible ages where a graft could be placed.

    Args:
        node: Node representing the root of the clade.

    Returns:
        Portion interval (possibly disjoint) representing valid graft ages.
    """
    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: dendropy.Node, include_root: bool = False) -> list[float]

Get list of node ages in a subtree.

Parameters:

Name Type Description Default
node Node

DendroPy node representing the root of the subtree.

required
include_root bool

If True, include the root node's age. Default: False.

False

Returns:

Type Description
list[float]

List of node ages, sorted in descending order.

Source code in tact/tree_util.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def get_ages(node: dendropy.Node, include_root: bool = False) -> list[float]:
    """Get list of node ages in a subtree.

    Args:
        node: DendroPy node representing the root of the subtree.
        include_root: If True, include the root node's age. Default: False.

    Returns:
        List of node ages, sorted in descending order.
    """
    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: dendropy.Node, sampfrac: float, yule: bool = False, include_root: bool = False) -> tuple[float, float]

Estimate birth-death rates from a subtree.

Computes maximum likelihood birth and death rates for the subtree descending from the given node, optionally using a Yule (pure birth) model.

Parameters:

Name Type Description Default
node Node

DendroPy node representing the root of the subtree.

required
sampfrac float

Sampling fraction in (0, 1].

required
yule bool

If True, use Yule model (zero extinction). Default: False.

False
include_root bool

If True, include root node age in calculations. Default: False.

False

Returns:

Type Description
tuple[float, float]

Tuple of (birth rate, death rate).

Source code in tact/tree_util.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def get_birth_death_rates(
    node: dendropy.Node, sampfrac: float, yule: bool = False, include_root: bool = False
) -> tuple[float, float]:
    """Estimate birth-death rates from a subtree.

    Computes maximum likelihood birth and death rates for the subtree
    descending from the given node, optionally using a Yule (pure birth) model.

    Args:
        node: DendroPy node representing the root of the subtree.
        sampfrac: Sampling fraction in (0, 1].
        yule: If True, use Yule model (zero extinction). Default: False.
        include_root: If True, include root node age in calculations. Default: False.

    Returns:
        Tuple of (birth rate, death rate).
    """
    if yule:
        return optim_yule(get_ages(node, include_root), sampfrac)

    return optim_bd(get_ages(node, include_root), sampfrac)

get_min_age(node: dendropy.Node) -> float

Get the minimum possible age for a graft in a clade.

Computes the minimum age that could be generated by grafting into the clade, considering only unlocked edges.

Parameters:

Name Type Description Default
node Node

Node representing the root of the clade.

required

Returns:

Type Description
float

Minimum possible age (0.0 if interval is empty).

Raises:

Type Description
DisjointConstraintError

If the age interval is not atomic (disjoint).

Source code in tact/tree_util.py
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
def get_min_age(node: dendropy.Node) -> float:
    """Get the minimum possible age for a graft in a clade.

    Computes the minimum age that could be generated by grafting into the
    clade, considering only unlocked edges.

    Args:
        node: Node representing the root of the clade.

    Returns:
        Minimum possible age (0.0 if interval is empty).

    Raises:
        DisjointConstraintError: If the age interval is not atomic (disjoint).
    """
    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 float(interval.lower)

get_monophyletic_node(tree: dendropy.Tree, species: set[str]) -> dendropy.Node | None

Get the MRCA node if it forms a monophyletic group.

Parameters:

Name Type Description Default
tree Tree

DendroPy tree object.

required
species set[str]

Set of taxon labels to check for monophyly.

required

Returns:

Type Description
Node | None

MRCA node if all its descendants are in the species set, None otherwise.

Source code in tact/tree_util.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def get_monophyletic_node(tree: dendropy.Tree, species: set[str]) -> dendropy.Node | None:
    """Get the MRCA node if it forms a monophyletic group.

    Args:
        tree: DendroPy tree object.
        species: Set of taxon labels to check for monophyly.

    Returns:
        MRCA node if all its descendants are in the species set, None otherwise.
    """
    mrca = tree.mrca(taxon_labels=species)
    if mrca and species.issuperset(get_tip_labels(mrca)):
        return mrca

    return None

get_short_branches(node: dendropy.Node) -> dendropy.Edge

Get edges with very short branch lengths.

Parameters:

Name Type Description Default
node Node

DendroPy node representing the root of the subtree.

required

Yields:

Type Description
Edge

DendroPy edge objects with length <= 0.001.

Source code in tact/tree_util.py
176
177
178
179
180
181
182
183
184
185
186
187
def get_short_branches(node: dendropy.Node) -> dendropy.Edge:
    """Get edges with very short branch lengths.

    Args:
        node: DendroPy node representing the root of the subtree.

    Yields:
        DendroPy edge objects with length <= 0.001.
    """
    for edge in edge_iter(node):
        if edge.length <= 0.001:
            yield edge

get_tip_labels(tree_or_node: dendropy.Tree | dendropy.Node) -> set[str]

Get set of tip labels for a tree or node.

Parameters:

Name Type Description Default
tree_or_node Tree | Node

DendroPy tree or node object.

required

Returns:

Type Description
set[str]

Set of taxon labels for all tips.

Source code in tact/tree_util.py
70
71
72
73
74
75
76
77
78
79
80
81
82
def get_tip_labels(tree_or_node: dendropy.Tree | dendropy.Node) -> set[str]:
    """Get set of tip labels for a tree or node.

    Args:
        tree_or_node: DendroPy tree or node object.

    Returns:
        Set of taxon labels for all tips.
    """
    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: str, namespace: dendropy.TaxonNamespace | None = None) -> dendropy.Tree

Load a DendroPy tree from a file path.

Loads a Newick-format tree and precalculates node ages and bipartition bitmasks for efficient operations.

Parameters:

Name Type Description Default
path str

File path to the tree file.

required
namespace TaxonNamespace | None

Optional taxon namespace to use. Default: None.

None

Returns:

Type Description
Tree

DendroPy tree object with ages and bipartitions calculated.

Source code in tact/tree_util.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
def get_tree(path: str, namespace: dendropy.TaxonNamespace | None = None) -> dendropy.Tree:
    """Load a DendroPy tree from a file path.

    Loads a Newick-format tree and precalculates node ages and bipartition
    bitmasks for efficient operations.

    Args:
        path: File path to the tree file.
        namespace: Optional taxon namespace to use. Default: None.

    Returns:
        DendroPy tree object with ages and bipartitions calculated.
    """
    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: dendropy.Node, graft: dendropy.Node, stem: bool = False) -> dendropy.Node

Graft a node randomly into a subtree.

Grafts a node into the subtree below the recipient node, randomly selecting an eligible edge. The graft node's age must be set. Edge lengths are adjusted to maintain ultrametricity.

The eligible edge (above which the graft is placed) must satisfy: 1. Not be the crown node 2. Head node younger than graft node 3. Tail node older than graft node 4. Not be locked

Parameters:

Name Type Description Default
graft_recipient Node

Node representing the clade to graft into.

required
graft Node

Node to graft (must have age attribute set).

required
stem bool

If True, allow grafting on the stem edge. Default: False.

False

Returns:

Type Description
Node

The crown node of the clade (may be the graft if it becomes the new crown).

Raises:

Type Description
Exception

If no eligible edge is found or negative branch lengths result.

Source code in tact/tree_util.py
211
212
213
214
215
216
217
218
219
220
221
222
223
224
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
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
def graft_node(graft_recipient: dendropy.Node, graft: dendropy.Node, stem: bool = False) -> dendropy.Node:
    """Graft a node randomly into a subtree.

    Grafts a node into the subtree below the recipient node, randomly selecting
    an eligible edge. The graft node's age must be set. Edge lengths are
    adjusted to maintain ultrametricity.

    The eligible edge (above which the graft is placed) must satisfy:
    1. Not be the crown node
    2. Head node younger than graft node
    3. Tail node older than graft node
    4. Not be locked

    Args:
        graft_recipient: Node representing the clade to graft into.
        graft: Node to graft (must have age attribute set).
        stem: If True, allow grafting on the stem edge. Default: False.

    Returns:
        The crown node of the clade (may be the graft if it becomes the new crown).

    Raises:
        Exception: If no eligible edge is found or negative branch lengths result.
    """

    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: dendropy.Node) -> bool

Check if a subtree is fully bifurcating.

Parameters:

Name Type Description Default
node Node

DendroPy node representing the root of the subtree.

required

Returns:

Type Description
bool

True if all internal nodes have exactly two children, False otherwise.

Source code in tact/tree_util.py
138
139
140
141
142
143
144
145
146
147
148
149
150
def is_binary(node: dendropy.Node) -> bool:
    """Check if a subtree is fully bifurcating.

    Args:
        node: DendroPy node representing the root of the subtree.

    Returns:
        True if all internal nodes have exactly two children, False otherwise.
    """
    for internal_node in node.preorder_internal_node_iter():
        if len(internal_node.child_nodes()) != 2:
            return False
    return True

is_fully_locked(node: dendropy.Node) -> bool

Check if all edges in a subtree are locked.

Parameters:

Name Type Description Default
node Node

Node representing the root of the subtree.

required

Returns:

Type Description
bool

True if all edges are locked, False otherwise.

Source code in tact/tree_util.py
321
322
323
324
325
326
327
328
329
330
def is_fully_locked(node: dendropy.Node) -> bool:
    """Check if all edges in a subtree are locked.

    Args:
        node: Node representing the root of the subtree.

    Returns:
        True if all edges are locked, False otherwise.
    """
    return all(x.label == "locked" for x in edge_iter(node))

is_ultrametric(tree: dendropy.Tree, tolerance: float = 1e-06) -> tuple[bool, tuple[tuple[str, float], tuple[str, float]]]

Check if a tree is ultrametric within a specified tolerance.

Parameters:

Name Type Description Default
tree Tree

DendroPy tree object to check.

required
tolerance float

Relative tolerance for ultrametricity check. Default: 1e-6.

1e-06

Returns:

Type Description
bool

Tuple of (is_ultrametric, (min_tip, max_tip)) where min_tip and max_tip

tuple[tuple[str, float], tuple[str, float]]

are tuples of (label, root_distance) for the tips with minimum and

tuple[bool, tuple[tuple[str, float], tuple[str, float]]]

maximum root-to-tip distances.

Source code in tact/tree_util.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
def is_ultrametric(
    tree: dendropy.Tree, tolerance: float = 1e-6
) -> tuple[bool, tuple[tuple[str, float], tuple[str, float]]]:
    """Check if a tree is ultrametric within a specified tolerance.

    Args:
        tree: DendroPy tree object to check.
        tolerance: Relative tolerance for ultrametricity check. Default: 1e-6.

    Returns:
        Tuple of (is_ultrametric, (min_tip, max_tip)) where min_tip and max_tip
        are tuples of (label, root_distance) for the tips with minimum and
        maximum root-to-tip distances.
    """
    tree.calc_node_root_distances()
    lengths: dict[str, float] = {}
    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))

lock_clade(node: dendropy.Node, stem: bool = False) -> None

Lock a clade to prevent future grafts.

Marks all edges in the subtree as locked, preventing new grafts from being placed on them.

Parameters:

Name Type Description Default
node Node

Node representing the root of the clade to lock.

required
stem bool

If True, also lock the stem edge. Default: False.

False
Source code in tact/tree_util.py
278
279
280
281
282
283
284
285
286
287
288
289
290
291
def lock_clade(node: dendropy.Node, stem: bool = False) -> None:
    """Lock a clade to prevent future grafts.

    Marks all edges in the subtree as locked, preventing new grafts from
    being placed on them.

    Args:
        node: Node representing the root of the clade to lock.
        stem: If True, also lock the stem edge. Default: False.
    """
    for edge in edge_iter(node):
        edge.label = "locked"
    if stem:
        node.edge.label = "locked"

unlock_clade(node: dendropy.Node, stem: bool = False) -> None

Unlock a clade to allow future grafts.

Removes the locked label from all edges in the subtree.

Parameters:

Name Type Description Default
node Node

Node representing the root of the clade to unlock.

required
stem bool

If True, also unlock the stem edge. Default: False.

False
Source code in tact/tree_util.py
294
295
296
297
298
299
300
301
302
303
304
305
306
def unlock_clade(node: dendropy.Node, stem: bool = False) -> None:
    """Unlock a clade to allow future grafts.

    Removes the locked label from all edges in the subtree.

    Args:
        node: Node representing the root of the clade to unlock.
        stem: If True, also unlock the stem edge. Default: False.
    """
    for edge in edge_iter(node):
        edge.label = ""
    if stem:
        node.edge.label = ""

update_tree_view(tree: dendropy.Tree) -> set[str]

Update a tree with node ages and bipartition bitmasks.

Performs in-place updates to calculate node ages and bipartition bitmasks, correcting for minor ultrametricity errors.

Parameters:

Name Type Description Default
tree Tree

DendroPy tree object to update.

required

Returns:

Type Description
set[str]

Set of tip labels in the tree.

Source code in tact/tree_util.py
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def update_tree_view(tree: dendropy.Tree) -> set[str]:
    """Update a tree with node ages and bipartition bitmasks.

    Performs in-place updates to calculate node ages and bipartition bitmasks,
    correcting for minor ultrametricity errors.

    Args:
        tree: DendroPy tree object to update.

    Returns:
        Set of tip labels in the tree.
    """
    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.

tree: dendropy.Tree | None = None module-attribute

bitmask(labels: list[str]) -> int

Get a bitmask for the specified taxa labels.

Parameters:

Name Type Description Default
labels list[str]

List of taxon labels.

required

Returns:

Type Description
int

Bitmask representing the taxa.

Raises:

Type Description
RuntimeError

If fastmrca has not been initialized.

Source code in tact/fastmrca.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def bitmask(labels: list[str]) -> int:
    """Get a bitmask for the specified taxa labels.

    Args:
        labels: List of taxon labels.

    Returns:
        Bitmask representing the taxa.

    Raises:
        RuntimeError: If fastmrca has not been initialized.
    """
    global tree
    if tree is None:
        raise RuntimeError("fastmrca not initialized")
    tn = tree.taxon_namespace
    return tn.taxa_bitmask(labels=labels)  # type: ignore[no-any-return]

fastmrca_getter(tn: dendropy.TaxonNamespace, x: list[str]) -> int

Helper function to compute bitmask for parallel processing.

Parameters:

Name Type Description Default
tn TaxonNamespace

Taxon namespace object.

required
x list[str]

List of taxon labels.

required

Returns:

Type Description
int

Bitmask representing the taxa.

Source code in tact/fastmrca.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def fastmrca_getter(tn: "dendropy.TaxonNamespace", x: list[str]) -> int:
    """Helper function to compute bitmask for parallel processing.

    Args:
        tn: Taxon namespace object.
        x: List of taxon labels.

    Returns:
        Bitmask representing the taxa.
    """
    taxa = tn.get_taxa(labels=x)
    mask = 0
    for taxon in taxa:
        mask |= int(tn.taxon_bitmask(taxon))
    return mask

get(labels: list[str]) -> dendropy.Node | None

Get the MRCA node for the specified taxa.

Returns the most recent common ancestor node if all descendants of that node are included in the label set, otherwise returns None.

Parameters:

Name Type Description Default
labels list[str]

List of taxon labels to find MRCA for.

required

Returns:

Type Description
Node | None

MRCA node if it forms a monophyletic group, None otherwise.

Raises:

Type Description
RuntimeError

If fastmrca has not been initialized.

Source code in tact/fastmrca.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def get(labels: list[str]) -> "dendropy.Node | None":
    """Get the MRCA node for the specified taxa.

    Returns the most recent common ancestor node if all descendants of that
    node are included in the label set, otherwise returns None.

    Args:
        labels: List of taxon labels to find MRCA for.

    Returns:
        MRCA node if it forms a monophyletic group, None otherwise.

    Raises:
        RuntimeError: If fastmrca has not been initialized.
    """
    global tree
    if tree is None:
        raise RuntimeError("fastmrca not initialized")
    labels_set = set(labels)
    mrca = tree.mrca(leafset_bitmask=bitmask(labels))
    if mrca and labels_set.issuperset(get_tip_labels(mrca)):
        return mrca
    return None

initialize(phy: dendropy.Tree) -> None

Initialize the fastmrca singleton with a tree.

Parameters:

Name Type Description Default
phy Tree

DendroPy tree object to use for MRCA lookups.

required
Source code in tact/fastmrca.py
13
14
15
16
17
18
19
20
def initialize(phy: "dendropy.Tree") -> None:
    """Initialize the fastmrca singleton with a tree.

    Args:
        phy: DendroPy tree object to use for MRCA lookups.
    """
    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
 48
 49
 50
 51
 52
 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
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):
        """Validates variables related to the backbone and taxonomy files."""
        if "taxonomy" in params:
            tn = params["taxonomy"].taxon_namespace
            tn.is_mutable = True
            if params.get("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) from None
        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) from None

        params["backbone"] = backbone
        return params

    def make_context(self, *args, **kwargs):
        """Set up the proper Click context for a command handler."""
        ctx = super().make_context(*args, **kwargs)
        ctx.params = self.validate_backbone_variables(ctx, ctx.params)
        return ctx

make_context(*args, **kwargs)

Set up the proper Click context for a command handler.

Source code in tact/validation.py
102
103
104
105
106
def make_context(self, *args, **kwargs):
    """Set up the proper Click context for a command handler."""
    ctx = super().make_context(*args, **kwargs)
    ctx.params = self.validate_backbone_variables(ctx, ctx.params)
    return ctx

validate_backbone_variables(ctx, params)

Validates variables related to the backbone and taxonomy files.

Source code in tact/validation.py
 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
def validate_backbone_variables(self, ctx, params):
    """Validates variables related to the backbone and taxonomy files."""
    if "taxonomy" in params:
        tn = params["taxonomy"].taxon_namespace
        tn.is_mutable = True
        if params.get("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) from None
    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) from None

    params["backbone"] = backbone
    return params

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

Validates a Newick tree, using appropriate defaults.

Source code in tact/validation.py
23
24
25
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
11
12
13
14
15
16
17
18
19
20
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
42
43
44
45
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
28
29
30
31
32
33
34
35
36
37
38
39
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, int] = 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.

Exceptions used by TACT.

DisjointConstraintError

Bases: TactError

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

Source code in tact/exceptions.py
8
9
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
4
5
class TactError(Exception):
    """Base class for errors raised by TACT."""