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.
Files changed (65) hide show
  1. forestpd-0.1.0/LICENSE +21 -0
  2. forestpd-0.1.0/PKG-INFO +122 -0
  3. forestpd-0.1.0/README.md +83 -0
  4. forestpd-0.1.0/pyproject.toml +129 -0
  5. forestpd-0.1.0/setup.cfg +4 -0
  6. forestpd-0.1.0/src/forestpd/__init__.py +65 -0
  7. forestpd-0.1.0/src/forestpd/_evaluate.py +113 -0
  8. forestpd-0.1.0/src/forestpd/_validate.py +278 -0
  9. forestpd-0.1.0/src/forestpd/algo/__init__.py +45 -0
  10. forestpd-0.1.0/src/forestpd/algo/_profile_kernel.py +244 -0
  11. forestpd-0.1.0/src/forestpd/algo/_signature_kernel.py +353 -0
  12. forestpd-0.1.0/src/forestpd/algo/component_dp.py +336 -0
  13. forestpd-0.1.0/src/forestpd/algo/convolve.py +157 -0
  14. forestpd-0.1.0/src/forestpd/algo/local.py +203 -0
  15. forestpd-0.1.0/src/forestpd/algo/local_solver.py +444 -0
  16. forestpd-0.1.0/src/forestpd/algo/profile.py +1178 -0
  17. forestpd-0.1.0/src/forestpd/algo/solver.py +929 -0
  18. forestpd-0.1.0/src/forestpd/algo/survival.py +285 -0
  19. forestpd-0.1.0/src/forestpd/algo/witness.py +186 -0
  20. forestpd-0.1.0/src/forestpd/api.py +235 -0
  21. forestpd-0.1.0/src/forestpd/cluster/__init__.py +19 -0
  22. forestpd-0.1.0/src/forestpd/cluster/_kernels.py +354 -0
  23. forestpd-0.1.0/src/forestpd/cluster/base.py +127 -0
  24. forestpd-0.1.0/src/forestpd/cluster/common.py +763 -0
  25. forestpd-0.1.0/src/forestpd/core/__init__.py +15 -0
  26. forestpd-0.1.0/src/forestpd/core/taxa.py +176 -0
  27. forestpd-0.1.0/src/forestpd/core/tree.py +633 -0
  28. forestpd-0.1.0/src/forestpd/exceptions.py +72 -0
  29. forestpd-0.1.0/src/forestpd/instance.py +671 -0
  30. forestpd-0.1.0/src/forestpd/io/__init__.py +18 -0
  31. forestpd-0.1.0/src/forestpd/io/_newick_kernel.py +132 -0
  32. forestpd-0.1.0/src/forestpd/io/_splits_kernel.py +193 -0
  33. forestpd-0.1.0/src/forestpd/io/convert.py +450 -0
  34. forestpd-0.1.0/src/forestpd/io/newick.py +996 -0
  35. forestpd-0.1.0/src/forestpd/io/pz.py +195 -0
  36. forestpd-0.1.0/src/forestpd/io/splits.py +241 -0
  37. forestpd-0.1.0/src/forestpd/maf/__init__.py +28 -0
  38. forestpd-0.1.0/src/forestpd/maf/base.py +136 -0
  39. forestpd-0.1.0/src/forestpd/maf/embedding.py +273 -0
  40. forestpd-0.1.0/src/forestpd/maf/heuristic.py +494 -0
  41. forestpd-0.1.0/src/forestpd/maf/maffe.py +325 -0
  42. forestpd-0.1.0/src/forestpd/maf/partition.py +345 -0
  43. forestpd-0.1.0/src/forestpd/maf/tiered.py +96 -0
  44. forestpd-0.1.0/src/forestpd/py.typed +0 -0
  45. forestpd-0.1.0/src/forestpd/utils.py +157 -0
  46. forestpd-0.1.0/src/forestpd.egg-info/PKG-INFO +122 -0
  47. forestpd-0.1.0/src/forestpd.egg-info/SOURCES.txt +63 -0
  48. forestpd-0.1.0/src/forestpd.egg-info/dependency_links.txt +1 -0
  49. forestpd-0.1.0/src/forestpd.egg-info/requires.txt +23 -0
  50. forestpd-0.1.0/src/forestpd.egg-info/top_level.txt +1 -0
  51. forestpd-0.1.0/tests/test_api.py +650 -0
  52. forestpd-0.1.0/tests/test_complement.py +246 -0
  53. forestpd-0.1.0/tests/test_component_dp.py +156 -0
  54. forestpd-0.1.0/tests/test_core.py +294 -0
  55. forestpd-0.1.0/tests/test_maf_plugins.py +467 -0
  56. forestpd-0.1.0/tests/test_newick.py +300 -0
  57. forestpd-0.1.0/tests/test_oracle.py +303 -0
  58. forestpd-0.1.0/tests/test_paper_example.py +159 -0
  59. forestpd-0.1.0/tests/test_properties.py +142 -0
  60. forestpd-0.1.0/tests/test_pz.py +45 -0
  61. forestpd-0.1.0/tests/test_regression.py +100 -0
  62. forestpd-0.1.0/tests/test_rooted.py +230 -0
  63. forestpd-0.1.0/tests/test_stages.py +907 -0
  64. forestpd-0.1.0/tests/test_survival.py +750 -0
  65. 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.
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/forestpd)](https://pypi.org/project/forestpd/)
41
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue)](https://github.com/nholtgrefe/forestpd/blob/main/LICENSE)
42
+ [![Docs](https://img.shields.io/badge/docs-stable-blue)](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.
@@ -0,0 +1,83 @@
1
+ [![PyPI](https://img.shields.io/pypi/v/forestpd)](https://pypi.org/project/forestpd/)
2
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue)](https://github.com/nholtgrefe/forestpd/blob/main/LICENSE)
3
+ [![Docs](https://img.shields.io/badge/docs-stable-blue)](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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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"]