forestpd 0.1.0__tar.gz
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-0.1.0/LICENSE +21 -0
- forestpd-0.1.0/PKG-INFO +122 -0
- forestpd-0.1.0/README.md +83 -0
- forestpd-0.1.0/pyproject.toml +129 -0
- forestpd-0.1.0/setup.cfg +4 -0
- forestpd-0.1.0/src/forestpd/__init__.py +65 -0
- forestpd-0.1.0/src/forestpd/_evaluate.py +113 -0
- forestpd-0.1.0/src/forestpd/_validate.py +278 -0
- forestpd-0.1.0/src/forestpd/algo/__init__.py +45 -0
- forestpd-0.1.0/src/forestpd/algo/_profile_kernel.py +244 -0
- forestpd-0.1.0/src/forestpd/algo/_signature_kernel.py +353 -0
- forestpd-0.1.0/src/forestpd/algo/component_dp.py +336 -0
- forestpd-0.1.0/src/forestpd/algo/convolve.py +157 -0
- forestpd-0.1.0/src/forestpd/algo/local.py +203 -0
- forestpd-0.1.0/src/forestpd/algo/local_solver.py +444 -0
- forestpd-0.1.0/src/forestpd/algo/profile.py +1178 -0
- forestpd-0.1.0/src/forestpd/algo/solver.py +929 -0
- forestpd-0.1.0/src/forestpd/algo/survival.py +285 -0
- forestpd-0.1.0/src/forestpd/algo/witness.py +186 -0
- forestpd-0.1.0/src/forestpd/api.py +235 -0
- forestpd-0.1.0/src/forestpd/cluster/__init__.py +19 -0
- forestpd-0.1.0/src/forestpd/cluster/_kernels.py +354 -0
- forestpd-0.1.0/src/forestpd/cluster/base.py +127 -0
- forestpd-0.1.0/src/forestpd/cluster/common.py +763 -0
- forestpd-0.1.0/src/forestpd/core/__init__.py +15 -0
- forestpd-0.1.0/src/forestpd/core/taxa.py +176 -0
- forestpd-0.1.0/src/forestpd/core/tree.py +633 -0
- forestpd-0.1.0/src/forestpd/exceptions.py +72 -0
- forestpd-0.1.0/src/forestpd/instance.py +671 -0
- forestpd-0.1.0/src/forestpd/io/__init__.py +18 -0
- forestpd-0.1.0/src/forestpd/io/_newick_kernel.py +132 -0
- forestpd-0.1.0/src/forestpd/io/_splits_kernel.py +193 -0
- forestpd-0.1.0/src/forestpd/io/convert.py +450 -0
- forestpd-0.1.0/src/forestpd/io/newick.py +996 -0
- forestpd-0.1.0/src/forestpd/io/pz.py +195 -0
- forestpd-0.1.0/src/forestpd/io/splits.py +241 -0
- forestpd-0.1.0/src/forestpd/maf/__init__.py +28 -0
- forestpd-0.1.0/src/forestpd/maf/base.py +136 -0
- forestpd-0.1.0/src/forestpd/maf/embedding.py +273 -0
- forestpd-0.1.0/src/forestpd/maf/heuristic.py +494 -0
- forestpd-0.1.0/src/forestpd/maf/maffe.py +325 -0
- forestpd-0.1.0/src/forestpd/maf/partition.py +345 -0
- forestpd-0.1.0/src/forestpd/maf/tiered.py +96 -0
- forestpd-0.1.0/src/forestpd/py.typed +0 -0
- forestpd-0.1.0/src/forestpd/utils.py +157 -0
- forestpd-0.1.0/src/forestpd.egg-info/PKG-INFO +122 -0
- forestpd-0.1.0/src/forestpd.egg-info/SOURCES.txt +63 -0
- forestpd-0.1.0/src/forestpd.egg-info/dependency_links.txt +1 -0
- forestpd-0.1.0/src/forestpd.egg-info/requires.txt +23 -0
- forestpd-0.1.0/src/forestpd.egg-info/top_level.txt +1 -0
- forestpd-0.1.0/tests/test_api.py +650 -0
- forestpd-0.1.0/tests/test_complement.py +246 -0
- forestpd-0.1.0/tests/test_component_dp.py +156 -0
- forestpd-0.1.0/tests/test_core.py +294 -0
- forestpd-0.1.0/tests/test_maf_plugins.py +467 -0
- forestpd-0.1.0/tests/test_newick.py +300 -0
- forestpd-0.1.0/tests/test_oracle.py +303 -0
- forestpd-0.1.0/tests/test_paper_example.py +159 -0
- forestpd-0.1.0/tests/test_properties.py +142 -0
- forestpd-0.1.0/tests/test_pz.py +45 -0
- forestpd-0.1.0/tests/test_regression.py +100 -0
- forestpd-0.1.0/tests/test_rooted.py +230 -0
- forestpd-0.1.0/tests/test_stages.py +907 -0
- forestpd-0.1.0/tests/test_survival.py +750 -0
- forestpd-0.1.0/tests/test_utils.py +298 -0
forestpd-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Niels Holtgrefe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
forestpd-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forestpd
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: a package for maximizing phylogenetic diversity under phylogenetic and extinction uncertainty
|
|
5
|
+
Author-email: Niels Holtgrefe <n.a.l.holtgrefe@tudelft.nl>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Documentation, https://nholtgrefe.github.io/forestpd/
|
|
8
|
+
Project-URL: Repository, https://github.com/nholtgrefe/forestpd
|
|
9
|
+
Keywords: phylogenetics,phylogenetic diversity,phylogenetic trees,bioinformatics,computational-biology
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: numpy>=1.20.0
|
|
20
|
+
Requires-Dist: numba>=0.56.0
|
|
21
|
+
Provides-Extra: phylozoo
|
|
22
|
+
Requires-Dist: phylozoo>=0.4.1; extra == "phylozoo"
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
25
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "test"
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
29
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
32
|
+
Requires-Dist: phylozoo>=0.4.1; extra == "dev"
|
|
33
|
+
Provides-Extra: docs
|
|
34
|
+
Requires-Dist: sphinx>=7.0.0; extra == "docs"
|
|
35
|
+
Requires-Dist: pydata-sphinx-theme>=0.15.0; extra == "docs"
|
|
36
|
+
Requires-Dist: sphinx-copybutton>=0.5.0; extra == "docs"
|
|
37
|
+
Requires-Dist: matplotlib>=3.5.0; extra == "docs"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
[](https://pypi.org/project/forestpd/)
|
|
41
|
+
[](https://github.com/nholtgrefe/forestpd/blob/main/LICENSE)
|
|
42
|
+
[](https://nholtgrefe.github.io/forestpd/)
|
|
43
|
+
|
|
44
|
+
<picture>
|
|
45
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/nholtgrefe/forestpd/main/docs/source/_static/forestpd-logo-dark.svg">
|
|
46
|
+
<img src="https://raw.githubusercontent.com/nholtgrefe/forestpd/main/docs/source/_static/forestpd-logo.svg" alt="ForestPD logo" width="360" align="right">
|
|
47
|
+
</picture>
|
|
48
|
+
|
|
49
|
+
**ForestPD** (Python package `forestpd`) chooses which taxa to protect so that as much
|
|
50
|
+
phylogenetic diversity (PD) as possible is preserved across many phylogenetic trees at once. It uses [numba](https://numba.pydata.org/) for JIT-accelerated computation, and
|
|
51
|
+
connects to [phylozoo](https://github.com/nholtgrefe/phylozoo) for plotting and further analysis.
|
|
52
|
+
|
|
53
|
+
<br clear="right">
|
|
54
|
+
|
|
55
|
+
## Key Features
|
|
56
|
+
|
|
57
|
+
- **Many trees at once**: maximize PD summed over a whole collection of trees, such as posterior samples, bootstrap replicates or gene trees, optionally weighted per tree, with no summary tree or network in between.
|
|
58
|
+
- **Budgets and extinction risks**: assign integer costs to taxa and maximize PD under a budget; optional survival probabilities make the objective the expected PD of the surviving taxa.
|
|
59
|
+
- **Whole budget curves**: a single run gives the optimum, and optionally an optimal taxon set, for every budget up to the one asked for.
|
|
60
|
+
- **Exact, and fast when the trees mostly agree**: the running time is exponential only in the size of agreement forests of the clusters shared by all trees, so ForestPD scales to tens of thousands of taxa and thousands of trees.
|
|
61
|
+
- **Agreement forests**: a fast built-in heuristic, or minimum agreement forests through the optional [maffe](https://github.com/roehrt/maffe) solver.
|
|
62
|
+
- **Rooted and unrooted PD**: unrooted by default, rooted with `rooted=True`.
|
|
63
|
+
- **Interoperability**: read and write Newick, export the trees as one weighted split system (NEXUS) for split-diversity tools such as PDA, and convert to and from PhyloZoo trees.
|
|
64
|
+
|
|
65
|
+
## Installation
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install forestpd
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Runtime dependencies (`numpy`, `numba`) are installed automatically. For conversion to and from
|
|
72
|
+
PhyloZoo (e.g. for plotting), or for development:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install "forestpd[phylozoo]" # PhyloZoo conversion
|
|
76
|
+
pip install "forestpd[dev]" # testing
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Minimum agreement forests need [maffe](https://github.com/roehrt/maffe), which is built from
|
|
80
|
+
source; see the [installation guide](https://nholtgrefe.github.io/forestpd/installation.html).
|
|
81
|
+
|
|
82
|
+
## Quickstart
|
|
83
|
+
|
|
84
|
+
Choose the taxa to protect on two trees, then add costs and survival probabilities and solve for
|
|
85
|
+
several budgets in one run:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import forestpd as fpd
|
|
89
|
+
|
|
90
|
+
trees = ["(A:1,B:1,(C:1,D:1):2);", "(A:1,C:1,(B:1,D:1):2);"] # or a file, one Newick tree per line
|
|
91
|
+
|
|
92
|
+
result = fpd.optimize_maxmpd(trees, budget=2, return_taxa=True)
|
|
93
|
+
print(result.value) # 8.0
|
|
94
|
+
print(sorted(result.taxa)) # ['B', 'C'] (['A', 'D'] is optimal too)
|
|
95
|
+
|
|
96
|
+
# Costs (default 1) and survival probabilities of unprotected taxa (default 0)
|
|
97
|
+
instance = fpd.MaxMPDInstance(trees, costs={"A": 2, "C": 3}, survival={"B": 0.5, "D": 0.2})
|
|
98
|
+
budgets = [1, 2, 3, 4]
|
|
99
|
+
for budget, r in zip(budgets, instance.optimize_many(budgets, return_taxa=True)):
|
|
100
|
+
print(budget, round(r.value, 2), sorted(r.taxa))
|
|
101
|
+
# 1 3.0 ['D']
|
|
102
|
+
# 2 6.0 ['B', 'D']
|
|
103
|
+
# 3 9.0 ['A', 'D']
|
|
104
|
+
# 4 10.0 ['A', 'B', 'D']
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
See the [user guide](https://nholtgrefe.github.io/forestpd/manual.html) for everything else,
|
|
108
|
+
including rooted PD, per-tree weights, pruning trees to a taxon set, and the choice of agreement
|
|
109
|
+
forest.
|
|
110
|
+
|
|
111
|
+
## Documentation
|
|
112
|
+
|
|
113
|
+
For the full manual, API reference, worked examples, and installation guide, visit the
|
|
114
|
+
**[ForestPD docs](https://nholtgrefe.github.io/forestpd/)**.
|
|
115
|
+
|
|
116
|
+
## Citation
|
|
117
|
+
|
|
118
|
+
If you use ForestPD, please cite:
|
|
119
|
+
|
|
120
|
+
> Niels Holtgrefe, Jannik Schestag, and Norbert Zeh.
|
|
121
|
+
> **ForestPD: Maximizing Phylogenetic Diversity under Phylogenetic and Extinction Uncertainty.**
|
|
122
|
+
> 2026.
|
forestpd-0.1.0/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
[](https://pypi.org/project/forestpd/)
|
|
2
|
+
[](https://github.com/nholtgrefe/forestpd/blob/main/LICENSE)
|
|
3
|
+
[](https://nholtgrefe.github.io/forestpd/)
|
|
4
|
+
|
|
5
|
+
<picture>
|
|
6
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/nholtgrefe/forestpd/main/docs/source/_static/forestpd-logo-dark.svg">
|
|
7
|
+
<img src="https://raw.githubusercontent.com/nholtgrefe/forestpd/main/docs/source/_static/forestpd-logo.svg" alt="ForestPD logo" width="360" align="right">
|
|
8
|
+
</picture>
|
|
9
|
+
|
|
10
|
+
**ForestPD** (Python package `forestpd`) chooses which taxa to protect so that as much
|
|
11
|
+
phylogenetic diversity (PD) as possible is preserved across many phylogenetic trees at once. It uses [numba](https://numba.pydata.org/) for JIT-accelerated computation, and
|
|
12
|
+
connects to [phylozoo](https://github.com/nholtgrefe/phylozoo) for plotting and further analysis.
|
|
13
|
+
|
|
14
|
+
<br clear="right">
|
|
15
|
+
|
|
16
|
+
## Key Features
|
|
17
|
+
|
|
18
|
+
- **Many trees at once**: maximize PD summed over a whole collection of trees, such as posterior samples, bootstrap replicates or gene trees, optionally weighted per tree, with no summary tree or network in between.
|
|
19
|
+
- **Budgets and extinction risks**: assign integer costs to taxa and maximize PD under a budget; optional survival probabilities make the objective the expected PD of the surviving taxa.
|
|
20
|
+
- **Whole budget curves**: a single run gives the optimum, and optionally an optimal taxon set, for every budget up to the one asked for.
|
|
21
|
+
- **Exact, and fast when the trees mostly agree**: the running time is exponential only in the size of agreement forests of the clusters shared by all trees, so ForestPD scales to tens of thousands of taxa and thousands of trees.
|
|
22
|
+
- **Agreement forests**: a fast built-in heuristic, or minimum agreement forests through the optional [maffe](https://github.com/roehrt/maffe) solver.
|
|
23
|
+
- **Rooted and unrooted PD**: unrooted by default, rooted with `rooted=True`.
|
|
24
|
+
- **Interoperability**: read and write Newick, export the trees as one weighted split system (NEXUS) for split-diversity tools such as PDA, and convert to and from PhyloZoo trees.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install forestpd
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Runtime dependencies (`numpy`, `numba`) are installed automatically. For conversion to and from
|
|
33
|
+
PhyloZoo (e.g. for plotting), or for development:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install "forestpd[phylozoo]" # PhyloZoo conversion
|
|
37
|
+
pip install "forestpd[dev]" # testing
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Minimum agreement forests need [maffe](https://github.com/roehrt/maffe), which is built from
|
|
41
|
+
source; see the [installation guide](https://nholtgrefe.github.io/forestpd/installation.html).
|
|
42
|
+
|
|
43
|
+
## Quickstart
|
|
44
|
+
|
|
45
|
+
Choose the taxa to protect on two trees, then add costs and survival probabilities and solve for
|
|
46
|
+
several budgets in one run:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import forestpd as fpd
|
|
50
|
+
|
|
51
|
+
trees = ["(A:1,B:1,(C:1,D:1):2);", "(A:1,C:1,(B:1,D:1):2);"] # or a file, one Newick tree per line
|
|
52
|
+
|
|
53
|
+
result = fpd.optimize_maxmpd(trees, budget=2, return_taxa=True)
|
|
54
|
+
print(result.value) # 8.0
|
|
55
|
+
print(sorted(result.taxa)) # ['B', 'C'] (['A', 'D'] is optimal too)
|
|
56
|
+
|
|
57
|
+
# Costs (default 1) and survival probabilities of unprotected taxa (default 0)
|
|
58
|
+
instance = fpd.MaxMPDInstance(trees, costs={"A": 2, "C": 3}, survival={"B": 0.5, "D": 0.2})
|
|
59
|
+
budgets = [1, 2, 3, 4]
|
|
60
|
+
for budget, r in zip(budgets, instance.optimize_many(budgets, return_taxa=True)):
|
|
61
|
+
print(budget, round(r.value, 2), sorted(r.taxa))
|
|
62
|
+
# 1 3.0 ['D']
|
|
63
|
+
# 2 6.0 ['B', 'D']
|
|
64
|
+
# 3 9.0 ['A', 'D']
|
|
65
|
+
# 4 10.0 ['A', 'B', 'D']
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
See the [user guide](https://nholtgrefe.github.io/forestpd/manual.html) for everything else,
|
|
69
|
+
including rooted PD, per-tree weights, pruning trees to a taxon set, and the choice of agreement
|
|
70
|
+
forest.
|
|
71
|
+
|
|
72
|
+
## Documentation
|
|
73
|
+
|
|
74
|
+
For the full manual, API reference, worked examples, and installation guide, visit the
|
|
75
|
+
**[ForestPD docs](https://nholtgrefe.github.io/forestpd/)**.
|
|
76
|
+
|
|
77
|
+
## Citation
|
|
78
|
+
|
|
79
|
+
If you use ForestPD, please cite:
|
|
80
|
+
|
|
81
|
+
> Niels Holtgrefe, Jannik Schestag, and Norbert Zeh.
|
|
82
|
+
> **ForestPD: Maximizing Phylogenetic Diversity under Phylogenetic and Extinction Uncertainty.**
|
|
83
|
+
> 2026.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "forestpd"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "Niels Holtgrefe", email = "n.a.l.holtgrefe@tudelft.nl"},
|
|
10
|
+
]
|
|
11
|
+
description = "a package for maximizing phylogenetic diversity under phylogenetic and extinction uncertainty"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = "MIT"
|
|
14
|
+
license-files = ["LICENSE"]
|
|
15
|
+
requires-python = ">=3.10"
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
23
|
+
]
|
|
24
|
+
keywords = [
|
|
25
|
+
"phylogenetics",
|
|
26
|
+
"phylogenetic diversity",
|
|
27
|
+
"phylogenetic trees",
|
|
28
|
+
"bioinformatics",
|
|
29
|
+
"computational-biology",
|
|
30
|
+
]
|
|
31
|
+
dependencies = [
|
|
32
|
+
"numpy>=1.20.0",
|
|
33
|
+
# Compiles the hot loops (Newick parsing, cluster reduction, profiles, the signature DP,
|
|
34
|
+
# split systems). Every kernel keeps a NumPy/Python fallback that is used if Numba cannot be
|
|
35
|
+
# imported, but that path is far slower.
|
|
36
|
+
"numba>=0.56.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
# Optional: conversion to and from PhyloZoo networks (forestpd.io.pz), which opens up plotting
|
|
41
|
+
# and the rest of the PhyloZoo ecosystem. The core parses Newick itself and does not need it.
|
|
42
|
+
phylozoo = [
|
|
43
|
+
"phylozoo>=0.4.1",
|
|
44
|
+
]
|
|
45
|
+
test = [
|
|
46
|
+
"pytest>=7.0.0",
|
|
47
|
+
"pytest-cov>=4.0.0",
|
|
48
|
+
]
|
|
49
|
+
dev = [
|
|
50
|
+
"pytest>=7.0.0",
|
|
51
|
+
"pytest-cov>=4.0.0",
|
|
52
|
+
"mypy>=1.0.0",
|
|
53
|
+
"black>=23.0.0",
|
|
54
|
+
"ruff>=0.1.0",
|
|
55
|
+
"phylozoo>=0.4.1",
|
|
56
|
+
]
|
|
57
|
+
# The documentation site (docs/): Sphinx with the PyData theme; matplotlib draws the examples' figure.
|
|
58
|
+
docs = [
|
|
59
|
+
"sphinx>=7.0.0",
|
|
60
|
+
"pydata-sphinx-theme>=0.15.0",
|
|
61
|
+
"sphinx-copybutton>=0.5.0",
|
|
62
|
+
"matplotlib>=3.5.0",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[project.urls]
|
|
66
|
+
Documentation = "https://nholtgrefe.github.io/forestpd/"
|
|
67
|
+
Repository = "https://github.com/nholtgrefe/forestpd"
|
|
68
|
+
|
|
69
|
+
[tool.setuptools]
|
|
70
|
+
package-dir = {"" = "src"}
|
|
71
|
+
|
|
72
|
+
[tool.setuptools.dynamic]
|
|
73
|
+
version = {attr = "forestpd.__version__"}
|
|
74
|
+
|
|
75
|
+
[tool.setuptools.packages.find]
|
|
76
|
+
where = ["src"]
|
|
77
|
+
|
|
78
|
+
[tool.setuptools.package-data]
|
|
79
|
+
forestpd = ["py.typed"]
|
|
80
|
+
|
|
81
|
+
[tool.pytest.ini_options]
|
|
82
|
+
# Collect the test suite plus the doctests embedded in the source (the docstring Examples double as
|
|
83
|
+
# executable tests, per the project conventions) and in the documentation pages.
|
|
84
|
+
testpaths = ["tests", "src", "docs/source"]
|
|
85
|
+
python_files = ["test_*.py", "*_test.py"]
|
|
86
|
+
python_classes = ["Test*"]
|
|
87
|
+
python_functions = ["test_*"]
|
|
88
|
+
addopts = [
|
|
89
|
+
"--strict-markers",
|
|
90
|
+
"--tb=short",
|
|
91
|
+
"--import-mode=importlib",
|
|
92
|
+
"--doctest-modules",
|
|
93
|
+
"--doctest-glob=*.rst",
|
|
94
|
+
# io/pz.py imports PhyloZoo, an optional extra; tests/test_pz.py covers it instead
|
|
95
|
+
"--ignore=src/forestpd/io/pz.py",
|
|
96
|
+
]
|
|
97
|
+
markers = [
|
|
98
|
+
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
[tool.mypy]
|
|
102
|
+
python_version = "3.10"
|
|
103
|
+
warn_return_any = true
|
|
104
|
+
warn_unused_configs = true
|
|
105
|
+
disallow_untyped_defs = false
|
|
106
|
+
disallow_incomplete_defs = false
|
|
107
|
+
check_untyped_defs = true
|
|
108
|
+
no_implicit_optional = true
|
|
109
|
+
warn_redundant_casts = true
|
|
110
|
+
warn_unused_ignores = true
|
|
111
|
+
warn_no_return = true
|
|
112
|
+
|
|
113
|
+
# PhyloZoo does not ship a py.typed marker yet, so it has no inline type information.
|
|
114
|
+
[[tool.mypy.overrides]]
|
|
115
|
+
module = ["phylozoo", "phylozoo.*"]
|
|
116
|
+
ignore_missing_imports = true
|
|
117
|
+
|
|
118
|
+
[tool.black]
|
|
119
|
+
line-length = 100
|
|
120
|
+
target-version = ['py310', 'py311']
|
|
121
|
+
|
|
122
|
+
[tool.ruff]
|
|
123
|
+
line-length = 100
|
|
124
|
+
target-version = "py310"
|
|
125
|
+
|
|
126
|
+
[tool.ruff.lint]
|
|
127
|
+
# Ruff's classic default set, made explicit so the check does not change when a new
|
|
128
|
+
# ruff release extends its defaults (0.16 enabled I/UP/SIM/RUF rules by default).
|
|
129
|
+
select = ["E4", "E7", "E9", "F"]
|
forestpd-0.1.0/setup.cfg
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
|
+
]
|
|
@@ -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"]
|