forestpd 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- forestpd/__init__.py +65 -0
- forestpd/_evaluate.py +113 -0
- forestpd/_validate.py +278 -0
- forestpd/algo/__init__.py +45 -0
- forestpd/algo/_profile_kernel.py +244 -0
- forestpd/algo/_signature_kernel.py +353 -0
- forestpd/algo/component_dp.py +336 -0
- forestpd/algo/convolve.py +157 -0
- forestpd/algo/local.py +203 -0
- forestpd/algo/local_solver.py +444 -0
- forestpd/algo/profile.py +1178 -0
- forestpd/algo/solver.py +929 -0
- forestpd/algo/survival.py +285 -0
- forestpd/algo/witness.py +186 -0
- forestpd/api.py +235 -0
- forestpd/cluster/__init__.py +19 -0
- forestpd/cluster/_kernels.py +354 -0
- forestpd/cluster/base.py +127 -0
- forestpd/cluster/common.py +763 -0
- forestpd/core/__init__.py +15 -0
- forestpd/core/taxa.py +176 -0
- forestpd/core/tree.py +633 -0
- forestpd/exceptions.py +72 -0
- forestpd/instance.py +671 -0
- forestpd/io/__init__.py +18 -0
- forestpd/io/_newick_kernel.py +132 -0
- forestpd/io/_splits_kernel.py +193 -0
- forestpd/io/convert.py +450 -0
- forestpd/io/newick.py +996 -0
- forestpd/io/pz.py +195 -0
- forestpd/io/splits.py +241 -0
- forestpd/maf/__init__.py +28 -0
- forestpd/maf/base.py +136 -0
- forestpd/maf/embedding.py +273 -0
- forestpd/maf/heuristic.py +494 -0
- forestpd/maf/maffe.py +325 -0
- forestpd/maf/partition.py +345 -0
- forestpd/maf/tiered.py +96 -0
- forestpd/py.typed +0 -0
- forestpd/utils.py +157 -0
- forestpd-0.1.0.dist-info/METADATA +122 -0
- forestpd-0.1.0.dist-info/RECORD +45 -0
- forestpd-0.1.0.dist-info/WHEEL +5 -0
- forestpd-0.1.0.dist-info/licenses/LICENSE +21 -0
- forestpd-0.1.0.dist-info/top_level.txt +1 -0
forestpd/__init__.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
forestpd — maximizing phylogenetic diversity under phylogenetic and extinction uncertainty.
|
|
3
|
+
|
|
4
|
+
Solves MaxMPD: given unrooted weighted trees on a shared taxon set, per-tree weights ``w_j``,
|
|
5
|
+
integer taxon costs and a budget ``B``, choose ``S`` with ``c(S) <= B`` maximizing
|
|
6
|
+
``sum_j w_j PD_{T_j}(S)`` — or, with per-taxon survival probabilities, its expected value over the
|
|
7
|
+
taxa that survive. Given a maximum agreement forest (MAF) of size ``k``, it solves MaxMPD in
|
|
8
|
+
``O(2^k * n * B^2)`` time, where ``n`` is the total size of the trees. ``k`` is taken per common
|
|
9
|
+
cluster, so it measures how much the trees disagree locally.
|
|
10
|
+
|
|
11
|
+
The public entry points are :class:`MaxMPDInstance` (build it once, then call
|
|
12
|
+
:meth:`~MaxMPDInstance.optimize`, :meth:`~MaxMPDInstance.optimize_many` and
|
|
13
|
+
:meth:`~MaxMPDInstance.evaluate_pd`) and the one-call functions :func:`optimize_maxmpd`,
|
|
14
|
+
:func:`optimize_many_maxmpd` and :func:`evaluate_maxmpd`. The pipeline has three stages —
|
|
15
|
+
:mod:`forestpd.cluster`, :mod:`forestpd.maf`, :mod:`forestpd.algo` — described in ``README.md``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
|
|
22
|
+
from .api import evaluate_maxmpd, optimize_many_maxmpd, optimize_maxmpd
|
|
23
|
+
from .core import PhyloTree, TaxonRegistry
|
|
24
|
+
from .exceptions import (
|
|
25
|
+
ForestPDError,
|
|
26
|
+
ForestPDImportError,
|
|
27
|
+
ForestPDInputError,
|
|
28
|
+
ForestPDValueError,
|
|
29
|
+
ForestPDWarning,
|
|
30
|
+
)
|
|
31
|
+
from .instance import MaxMPDInstance, MaxMPDResult, MaxMPDStructure
|
|
32
|
+
from . import io, utils
|
|
33
|
+
from .algo.convolve import HAVE_NUMBA as _HAVE_NUMBA
|
|
34
|
+
|
|
35
|
+
if not _HAVE_NUMBA: # pragma: no cover - depends on the environment
|
|
36
|
+
import warnings as _warnings
|
|
37
|
+
|
|
38
|
+
_warnings.warn(
|
|
39
|
+
"Numba could not be imported, so forestpd runs its NumPy/Python fallbacks: the same "
|
|
40
|
+
"results, but several times slower. Install numba (a dependency of forestpd) for full speed.",
|
|
41
|
+
ForestPDWarning,
|
|
42
|
+
stacklevel=2,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"__version__",
|
|
47
|
+
# Public API
|
|
48
|
+
"MaxMPDInstance",
|
|
49
|
+
"optimize_maxmpd",
|
|
50
|
+
"optimize_many_maxmpd",
|
|
51
|
+
"evaluate_maxmpd",
|
|
52
|
+
"MaxMPDResult",
|
|
53
|
+
"MaxMPDStructure",
|
|
54
|
+
"io",
|
|
55
|
+
"utils",
|
|
56
|
+
# Core types
|
|
57
|
+
"PhyloTree",
|
|
58
|
+
"TaxonRegistry",
|
|
59
|
+
# Exceptions
|
|
60
|
+
"ForestPDError",
|
|
61
|
+
"ForestPDValueError",
|
|
62
|
+
"ForestPDInputError",
|
|
63
|
+
"ForestPDImportError",
|
|
64
|
+
"ForestPDWarning",
|
|
65
|
+
]
|
forestpd/_evaluate.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fast evaluation of the objective of a taxon set: the trees as one weighted split table.
|
|
3
|
+
|
|
4
|
+
The objective of a protected set ``S`` is a sum over all edges of all trees of
|
|
5
|
+
``w_j * len_j(e) * P(one side keeps a survivor) * P(the other side does)``, where a side holding a
|
|
6
|
+
taxon of ``S`` keeps one surely and otherwise with probability ``1 - prod (1 - a(x))`` over its
|
|
7
|
+
taxa. Both probabilities depend on the edge only through its split, so the edges are grouped by
|
|
8
|
+
split once (:func:`forestpd.io.splits.split_system`, compiled) and each distinct split carries its
|
|
9
|
+
summed weight ``W_s``. Each split is stored by its smaller side, as a sparse row of taxon ids.
|
|
10
|
+
|
|
11
|
+
Scoring a set is then two sparse sums per split, independent of the number of trees: the protected
|
|
12
|
+
taxa on the stored side (the other side has the rest), and the summed ``log(1 - a(x))`` of its
|
|
13
|
+
unprotected taxa (the other side has the rest of the total). This regroups the per-edge sum of
|
|
14
|
+
the per-edge definition exactly; without survival probabilities it is the split
|
|
15
|
+
diversity of ``S``, i.e. its weighted PD.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Sequence
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
from .core.tree import PhyloTree
|
|
25
|
+
from .io import _splits_kernel
|
|
26
|
+
from .io import splits as _splits
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SplitTable:
|
|
30
|
+
"""
|
|
31
|
+
The weighted splits of a set of trees, ready to score taxon sets.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
trees : Sequence[PhyloTree]
|
|
36
|
+
The trees, all on leaves ``0..n_taxa-1``.
|
|
37
|
+
weights : numpy.ndarray
|
|
38
|
+
``float64[t]``; the per-tree weights.
|
|
39
|
+
log_loss : numpy.ndarray
|
|
40
|
+
``float64[n_taxa]``; ``log(1 - a(x))`` per taxon.
|
|
41
|
+
|
|
42
|
+
Examples
|
|
43
|
+
--------
|
|
44
|
+
>>> from forestpd.core.tree import PhyloTree
|
|
45
|
+
>>> t = PhyloTree.from_edges(6, [(0, 4), (1, 4), (4, 5), (2, 5), (3, 5)], [1.0] * 5,
|
|
46
|
+
... {i: i for i in range(4)})
|
|
47
|
+
>>> table = SplitTable([t, t], np.array([1.0, 0.5]), np.zeros(4))
|
|
48
|
+
>>> table.value([0, 2]) # the path 0 - 4 - 5 - 2, in both trees: 3 + 1.5
|
|
49
|
+
4.5
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, trees: Sequence[PhyloTree], weights: np.ndarray, log_loss: np.ndarray):
|
|
53
|
+
n_taxa = trees[0].n_leaves
|
|
54
|
+
members, offsets, split_weights = _splits._flat_split_system(trees, weights)
|
|
55
|
+
if _splits.USE_COMPILED and _splits_kernel.HAVE_NUMBA:
|
|
56
|
+
members, offsets = _splits_kernel.smaller_sides(members, offsets, n_taxa)
|
|
57
|
+
else:
|
|
58
|
+
members, offsets = _smaller_sides(members, offsets, n_taxa)
|
|
59
|
+
self.n_taxa = n_taxa
|
|
60
|
+
self.members = members
|
|
61
|
+
self.weights = split_weights
|
|
62
|
+
self.log_loss = np.asarray(log_loss, dtype=np.float64)
|
|
63
|
+
# Rows are nonempty (every side holds a taxon), so reduceat over these starts is exact.
|
|
64
|
+
self.starts = offsets[:-1]
|
|
65
|
+
|
|
66
|
+
def value(self, protected: Sequence[int]) -> float:
|
|
67
|
+
"""
|
|
68
|
+
The objective of protecting the taxa ``protected`` (taxon ids).
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
protected : Sequence[int]
|
|
73
|
+
Distinct taxon ids.
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
-------
|
|
77
|
+
float
|
|
78
|
+
``sum_s W_s P(side A keeps a survivor) P(side B does)``.
|
|
79
|
+
"""
|
|
80
|
+
if self.weights.size == 0:
|
|
81
|
+
return 0.0
|
|
82
|
+
is_protected = np.zeros(self.n_taxa, dtype=bool)
|
|
83
|
+
is_protected[np.asarray(list(protected), dtype=np.int64)] = True
|
|
84
|
+
n_protected = int(is_protected.sum())
|
|
85
|
+
unprotected_loss = np.where(is_protected, 0.0, self.log_loss)
|
|
86
|
+
total_loss = float(unprotected_loss.sum())
|
|
87
|
+
|
|
88
|
+
on_side = is_protected[self.members]
|
|
89
|
+
protected_a = np.add.reduceat(on_side.astype(np.int64), self.starts)
|
|
90
|
+
loss_a = np.add.reduceat(unprotected_loss[self.members], self.starts)
|
|
91
|
+
protected_b = n_protected - protected_a
|
|
92
|
+
loss_b = total_loss - loss_a
|
|
93
|
+
keeps_a = np.where(protected_a > 0, 1.0, 0.0 - np.expm1(loss_a))
|
|
94
|
+
keeps_b = np.where(protected_b > 0, 1.0, 0.0 - np.expm1(loss_b))
|
|
95
|
+
return float(np.dot(self.weights, keeps_a * keeps_b))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _smaller_sides(
|
|
99
|
+
members: np.ndarray, offsets: np.ndarray, n_taxa: int
|
|
100
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
101
|
+
"""The NumPy version of :func:`forestpd.io._splits_kernel.smaller_sides`."""
|
|
102
|
+
sides = []
|
|
103
|
+
everyone = np.arange(n_taxa)
|
|
104
|
+
for r in range(offsets.size - 1):
|
|
105
|
+
side = members[offsets[r] : offsets[r + 1]]
|
|
106
|
+
sides.append(side if 2 * side.size <= n_taxa else np.setdiff1d(everyone, side))
|
|
107
|
+
out_offsets = np.zeros(len(sides) + 1, dtype=np.int64)
|
|
108
|
+
np.cumsum([side.size for side in sides], out=out_offsets[1:])
|
|
109
|
+
flat = np.concatenate(sides) if sides else np.empty(0, dtype=np.int64)
|
|
110
|
+
return flat.astype(np.int64), out_offsets
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
__all__ = ["SplitTable"]
|
forestpd/_validate.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validation of the instance data: per-tree weights, taxon costs, survival probabilities and budgets.
|
|
3
|
+
|
|
4
|
+
Shared by :class:`~forestpd.instance.MaxMPDInstance`, :mod:`forestpd.utils` and
|
|
5
|
+
:mod:`forestpd.io.splits`; every function raises :class:`~forestpd.exceptions.ForestPDInputError`
|
|
6
|
+
on invalid input.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
from typing import Iterable, Mapping, Sequence
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from .core.taxa import TaxonRegistry
|
|
17
|
+
from .core.tree import PhyloTree, has_degree_two
|
|
18
|
+
from .exceptions import ForestPDInputError
|
|
19
|
+
from .io.newick import ROOT_LABEL
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def weights_to_array(
|
|
23
|
+
weights: Sequence[float] | Mapping[int, float] | None, n_trees: int
|
|
24
|
+
) -> np.ndarray:
|
|
25
|
+
"""
|
|
26
|
+
Validate per-tree weights ``w_j`` and return them as an array (no rescaling).
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
weights : Sequence[float] | Mapping[int, float] | None
|
|
31
|
+
Per-tree weights, either aligned with the trees or keyed by tree index (missing indices
|
|
32
|
+
weigh ``1.0``). ``None`` means all ``1.0``.
|
|
33
|
+
n_trees : int
|
|
34
|
+
Number of trees ``t``.
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
numpy.ndarray
|
|
39
|
+
``float64[t]`` array of weights.
|
|
40
|
+
|
|
41
|
+
Raises
|
|
42
|
+
------
|
|
43
|
+
ForestPDInputError
|
|
44
|
+
If a weight is negative or non-finite, a mapping key is not a tree index, or the sequence
|
|
45
|
+
length is wrong.
|
|
46
|
+
"""
|
|
47
|
+
if weights is None:
|
|
48
|
+
return np.ones(n_trees, dtype=np.float64)
|
|
49
|
+
if isinstance(weights, Mapping):
|
|
50
|
+
for key in weights:
|
|
51
|
+
if (
|
|
52
|
+
isinstance(key, (bool, np.bool_))
|
|
53
|
+
or not isinstance(key, (int, np.integer))
|
|
54
|
+
or not 0 <= key < n_trees
|
|
55
|
+
):
|
|
56
|
+
raise ForestPDInputError(
|
|
57
|
+
f"Weight given for tree {key!r}, but trees are indexed 0..{n_trees - 1}"
|
|
58
|
+
)
|
|
59
|
+
values = [float(weights.get(j, 1.0)) for j in range(n_trees)]
|
|
60
|
+
else:
|
|
61
|
+
values = [float(w) for w in weights]
|
|
62
|
+
if len(values) != n_trees:
|
|
63
|
+
raise ForestPDInputError(f"Expected {n_trees} weights, got {len(values)}")
|
|
64
|
+
for j, w in enumerate(values):
|
|
65
|
+
if not math.isfinite(w) or w < 0:
|
|
66
|
+
raise ForestPDInputError(
|
|
67
|
+
f"Weight for tree {j} must be finite and non-negative, got {w}"
|
|
68
|
+
)
|
|
69
|
+
return np.asarray(values, dtype=np.float64)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def costs_to_array(
|
|
73
|
+
registry: TaxonRegistry, costs: Mapping[str, int] | Sequence[int] | None
|
|
74
|
+
) -> np.ndarray:
|
|
75
|
+
"""
|
|
76
|
+
Validate taxon costs and return them as an array indexed by taxon id.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
registry : TaxonRegistry
|
|
81
|
+
The taxon registry fixing the id of each label.
|
|
82
|
+
costs : Mapping[str, int] | Sequence[int] | None
|
|
83
|
+
Integer cost per taxon label (missing labels cost 1), or a sequence of costs indexed by
|
|
84
|
+
taxon id. ``None`` means unit costs.
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
numpy.ndarray
|
|
89
|
+
``int64[n]`` array of costs, indexed by taxon id.
|
|
90
|
+
|
|
91
|
+
Raises
|
|
92
|
+
------
|
|
93
|
+
ForestPDInputError
|
|
94
|
+
If a cost is given for an unknown label, is not a positive integer, or a sequence
|
|
95
|
+
has the wrong length.
|
|
96
|
+
"""
|
|
97
|
+
result = np.ones(registry.n, dtype=np.int64)
|
|
98
|
+
if costs is None:
|
|
99
|
+
return result
|
|
100
|
+
if isinstance(costs, Mapping):
|
|
101
|
+
items = [
|
|
102
|
+
(label, registry.id_of(label) if _user_label(label, registry) else -1, cost)
|
|
103
|
+
for label, cost in costs.items()
|
|
104
|
+
]
|
|
105
|
+
else:
|
|
106
|
+
values = list(costs)
|
|
107
|
+
if len(values) != registry.n:
|
|
108
|
+
raise ForestPDInputError(f"Expected {registry.n} costs, got {len(values)}")
|
|
109
|
+
items = [(registry.label_of(i), i, cost) for i, cost in enumerate(values)]
|
|
110
|
+
for label, taxon, cost in items:
|
|
111
|
+
if taxon < 0:
|
|
112
|
+
raise ForestPDInputError(f"Cost given for unknown taxon: {label!r}")
|
|
113
|
+
if isinstance(cost, (bool, np.bool_)) or not isinstance(cost, (int, np.integer)):
|
|
114
|
+
raise ForestPDInputError(
|
|
115
|
+
f"Cost for taxon {label!r} must be an integer, got {type(cost).__name__}"
|
|
116
|
+
)
|
|
117
|
+
if cost < 1:
|
|
118
|
+
raise ForestPDInputError(f"Cost for taxon {label!r} must be at least 1, got {cost}")
|
|
119
|
+
result[taxon] = cost
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def survival_to_array(
|
|
124
|
+
registry: TaxonRegistry, survival: Mapping[str, float] | Sequence[float] | None
|
|
125
|
+
) -> np.ndarray:
|
|
126
|
+
"""
|
|
127
|
+
Validate survival probabilities and return them as an array indexed by taxon id.
|
|
128
|
+
|
|
129
|
+
Parameters
|
|
130
|
+
----------
|
|
131
|
+
registry : TaxonRegistry
|
|
132
|
+
The taxon registry fixing the id of each label.
|
|
133
|
+
survival : Mapping[str, float] | Sequence[float] | None
|
|
134
|
+
Probability per taxon label (missing labels have ``0``), or a sequence indexed by taxon
|
|
135
|
+
id. ``None`` means ``0`` for every taxon.
|
|
136
|
+
|
|
137
|
+
Returns
|
|
138
|
+
-------
|
|
139
|
+
numpy.ndarray
|
|
140
|
+
``float64[n]`` array of probabilities, indexed by taxon id.
|
|
141
|
+
|
|
142
|
+
Raises
|
|
143
|
+
------
|
|
144
|
+
ForestPDInputError
|
|
145
|
+
If a probability is given for an unknown label, is not a number in ``[0, 1)``, or a
|
|
146
|
+
sequence has the wrong length.
|
|
147
|
+
"""
|
|
148
|
+
result = np.zeros(registry.n, dtype=np.float64)
|
|
149
|
+
if survival is None:
|
|
150
|
+
return result
|
|
151
|
+
if isinstance(survival, Mapping):
|
|
152
|
+
items = [
|
|
153
|
+
(label, registry.id_of(label) if _user_label(label, registry) else -1, value)
|
|
154
|
+
for label, value in survival.items()
|
|
155
|
+
]
|
|
156
|
+
else:
|
|
157
|
+
values = list(survival)
|
|
158
|
+
if len(values) != registry.n:
|
|
159
|
+
raise ForestPDInputError(
|
|
160
|
+
f"Expected {registry.n} survival probabilities, got {len(values)}"
|
|
161
|
+
)
|
|
162
|
+
items = [(registry.label_of(i), i, value) for i, value in enumerate(values)]
|
|
163
|
+
for label, taxon, value in items:
|
|
164
|
+
if taxon < 0:
|
|
165
|
+
raise ForestPDInputError(f"Survival probability given for unknown taxon: {label!r}")
|
|
166
|
+
if isinstance(value, (bool, np.bool_)) or not isinstance(
|
|
167
|
+
value, (int, float, np.integer, np.floating)
|
|
168
|
+
):
|
|
169
|
+
raise ForestPDInputError(
|
|
170
|
+
f"Survival probability for taxon {label!r} must be a number, "
|
|
171
|
+
f"got {type(value).__name__}"
|
|
172
|
+
)
|
|
173
|
+
if not (0.0 <= float(value) < 1.0):
|
|
174
|
+
raise ForestPDInputError(
|
|
175
|
+
f"Survival probability for taxon {label!r} must lie in [0, 1), got {value}"
|
|
176
|
+
)
|
|
177
|
+
result[taxon] = float(value)
|
|
178
|
+
return result
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def validate_budget(budget: int) -> None:
|
|
182
|
+
"""
|
|
183
|
+
Raise :class:`ForestPDInputError` unless ``budget`` is a non-negative integer.
|
|
184
|
+
|
|
185
|
+
Parameters
|
|
186
|
+
----------
|
|
187
|
+
budget : int
|
|
188
|
+
The total budget ``B``.
|
|
189
|
+
|
|
190
|
+
Raises
|
|
191
|
+
------
|
|
192
|
+
ForestPDInputError
|
|
193
|
+
If ``budget`` is not an integer (Python or NumPy; ``bool`` is rejected) or is negative.
|
|
194
|
+
"""
|
|
195
|
+
if isinstance(budget, (bool, np.bool_)) or not isinstance(budget, (int, np.integer)):
|
|
196
|
+
raise ForestPDInputError(f"budget must be an integer, got {type(budget).__name__}")
|
|
197
|
+
if budget < 0:
|
|
198
|
+
raise ForestPDInputError(f"budget must be non-negative, got {budget}")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def validate_uniform(trees: Sequence[PhyloTree], registry: TaxonRegistry) -> None:
|
|
202
|
+
"""
|
|
203
|
+
Check that every tree has the same leaf set, namely all taxa in ``registry``, and no
|
|
204
|
+
internal vertex of degree 2 (trees read from Newick or PhyloZoo never have one).
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
trees : Sequence[PhyloTree]
|
|
209
|
+
The input trees.
|
|
210
|
+
registry : TaxonRegistry
|
|
211
|
+
The registry of all taxa in the instance.
|
|
212
|
+
|
|
213
|
+
Raises
|
|
214
|
+
------
|
|
215
|
+
ForestPDInputError
|
|
216
|
+
If the trees do not all span the same taxon set, or one has an internal vertex of
|
|
217
|
+
degree 2.
|
|
218
|
+
"""
|
|
219
|
+
for j, tree in enumerate(trees):
|
|
220
|
+
if tree.n_leaves != registry.n:
|
|
221
|
+
raise ForestPDInputError(
|
|
222
|
+
f"Tree {j} has {tree.n_leaves} taxa but the instance has {registry.n}. "
|
|
223
|
+
"All trees must carry the same taxon set."
|
|
224
|
+
)
|
|
225
|
+
if has_degree_two(tree):
|
|
226
|
+
raise ForestPDInputError(
|
|
227
|
+
f"Tree {j} has an internal vertex of degree 2; suppress it first with "
|
|
228
|
+
"forestpd.core.tree.suppress_degree_two (this does not change any PD)."
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def taxon_ids(registry: TaxonRegistry, taxa: Iterable[str]) -> set[int]:
|
|
233
|
+
"""
|
|
234
|
+
The ids of a set of taxon labels given by a user.
|
|
235
|
+
|
|
236
|
+
Parameters
|
|
237
|
+
----------
|
|
238
|
+
registry : TaxonRegistry
|
|
239
|
+
The taxon registry.
|
|
240
|
+
taxa : Iterable[str]
|
|
241
|
+
Taxon labels; a single string is rejected rather than read as its characters.
|
|
242
|
+
|
|
243
|
+
Returns
|
|
244
|
+
-------
|
|
245
|
+
set[int]
|
|
246
|
+
Their ids.
|
|
247
|
+
|
|
248
|
+
Raises
|
|
249
|
+
------
|
|
250
|
+
ForestPDInputError
|
|
251
|
+
If ``taxa`` is a string, or a label is unknown (the reserved root label included).
|
|
252
|
+
"""
|
|
253
|
+
if isinstance(taxa, str):
|
|
254
|
+
raise ForestPDInputError(
|
|
255
|
+
f"Expected a collection of taxon labels, got the string {taxa!r}; "
|
|
256
|
+
f"wrap a single label as [{taxa!r}]."
|
|
257
|
+
)
|
|
258
|
+
ids = set()
|
|
259
|
+
for label in taxa:
|
|
260
|
+
if not _user_label(label, registry):
|
|
261
|
+
raise ForestPDInputError(f"Unknown taxon: {label!r}")
|
|
262
|
+
ids.add(registry.id_of(label))
|
|
263
|
+
return ids
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _user_label(label: object, registry: TaxonRegistry) -> bool:
|
|
267
|
+
"""Whether ``label`` names a taxon a user may refer to (not the reserved root leaf)."""
|
|
268
|
+
return label != ROOT_LABEL and label in registry
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
__all__ = [
|
|
272
|
+
"taxon_ids",
|
|
273
|
+
"weights_to_array",
|
|
274
|
+
"costs_to_array",
|
|
275
|
+
"survival_to_array",
|
|
276
|
+
"validate_budget",
|
|
277
|
+
"validate_uniform",
|
|
278
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stage 3 of the pipeline: the phylogenetic-diversity dynamic program.
|
|
3
|
+
|
|
4
|
+
Modules, in the order the algorithm uses them:
|
|
5
|
+
|
|
6
|
+
=========================== =============================================================
|
|
7
|
+
:mod:`.local` ``LocalInstance`` and the ``f_2`` tables that link clusters
|
|
8
|
+
:mod:`.profile` per-cluster precomputation, done once before the ``2^k`` loop
|
|
9
|
+
:mod:`.survival` survival probabilities: side constants and the per-cluster sums
|
|
10
|
+
:mod:`.convolve` max-plus convolution (compiled with Numba)
|
|
11
|
+
:mod:`.component_dp` ``PD_i^{(S)}``, Lemma "compute-PD_i"
|
|
12
|
+
:mod:`.local_solver` Algorithm 3: one cluster, all signatures
|
|
13
|
+
:mod:`.solver` Algorithm 1: bottom-up pass over the tree of clusters
|
|
14
|
+
:mod:`.witness` reconstruction of an optimal taxon set
|
|
15
|
+
=========================== =============================================================
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from .component_dp import component_dp_table, component_table
|
|
21
|
+
from .convolve import HAVE_NUMBA, fold_max_plus, max_plus
|
|
22
|
+
from .local import LocalInstance, NEG_INF, ordinary_f2, pseudo_leaf_f2
|
|
23
|
+
from .local_solver import LocalSolution, signature_weights, solve_local
|
|
24
|
+
from .profile import ClusterProfile, ComponentTree, build_profile
|
|
25
|
+
from .solver import Solution, solve
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"LocalInstance",
|
|
29
|
+
"NEG_INF",
|
|
30
|
+
"ordinary_f2",
|
|
31
|
+
"pseudo_leaf_f2",
|
|
32
|
+
"ClusterProfile",
|
|
33
|
+
"ComponentTree",
|
|
34
|
+
"build_profile",
|
|
35
|
+
"max_plus",
|
|
36
|
+
"fold_max_plus",
|
|
37
|
+
"HAVE_NUMBA",
|
|
38
|
+
"component_table",
|
|
39
|
+
"component_dp_table",
|
|
40
|
+
"LocalSolution",
|
|
41
|
+
"solve_local",
|
|
42
|
+
"signature_weights",
|
|
43
|
+
"solve",
|
|
44
|
+
"Solution",
|
|
45
|
+
]
|