phenoforge 0.1.2__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.
- phenoforge-0.1.2/LICENSE +21 -0
- phenoforge-0.1.2/PKG-INFO +105 -0
- phenoforge-0.1.2/README.md +82 -0
- phenoforge-0.1.2/pyproject.toml +40 -0
- phenoforge-0.1.2/setup.cfg +4 -0
- phenoforge-0.1.2/src/phenoforge/__init__.py +41 -0
- phenoforge-0.1.2/src/phenoforge/ensemble/__init__.py +19 -0
- phenoforge-0.1.2/src/phenoforge/ensemble/bape.py +172 -0
- phenoforge-0.1.2/src/phenoforge/ensemble/bootstrap.py +98 -0
- phenoforge-0.1.2/src/phenoforge/ensemble/glue.py +102 -0
- phenoforge-0.1.2/src/phenoforge/ensemble/ic.py +80 -0
- phenoforge-0.1.2/src/phenoforge/ensemble/stacking.py +96 -0
- phenoforge-0.1.2/src/phenoforge/families/__init__.py +12 -0
- phenoforge-0.1.2/src/phenoforge/families/base.py +162 -0
- phenoforge-0.1.2/src/phenoforge/families/comminution.py +122 -0
- phenoforge-0.1.2/src/phenoforge/families/flotation.py +273 -0
- phenoforge-0.1.2/src/phenoforge/families/registry.py +31 -0
- phenoforge-0.1.2/src/phenoforge/fit/__init__.py +3 -0
- phenoforge-0.1.2/src/phenoforge/fit/nls.py +105 -0
- phenoforge-0.1.2/src/phenoforge/metrics/__init__.py +20 -0
- phenoforge-0.1.2/src/phenoforge/metrics/calibration.py +66 -0
- phenoforge-0.1.2/src/phenoforge/metrics/point.py +23 -0
- phenoforge-0.1.2/src/phenoforge/metrics/structural.py +43 -0
- phenoforge-0.1.2/src/phenoforge/router.py +22 -0
- phenoforge-0.1.2/src/phenoforge.egg-info/PKG-INFO +105 -0
- phenoforge-0.1.2/src/phenoforge.egg-info/SOURCES.txt +33 -0
- phenoforge-0.1.2/src/phenoforge.egg-info/dependency_links.txt +1 -0
- phenoforge-0.1.2/src/phenoforge.egg-info/requires.txt +6 -0
- phenoforge-0.1.2/src/phenoforge.egg-info/top_level.txt +1 -0
- phenoforge-0.1.2/tests/test_ensembles.py +107 -0
- phenoforge-0.1.2/tests/test_families.py +61 -0
- phenoforge-0.1.2/tests/test_fit_nls.py +54 -0
- phenoforge-0.1.2/tests/test_metrics.py +55 -0
- phenoforge-0.1.2/tests/test_router.py +22 -0
- phenoforge-0.1.2/tests/test_small_n_fallback.py +48 -0
phenoforge-0.1.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Felipe Santibanez-Leal
|
|
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,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phenoforge
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Phenomenological model-family bank and ensemble calibration engine for industrial processes (BAPE: Bootstrap-Aggregated Phenomenological Ensembles)
|
|
5
|
+
Author-email: Felipe Santibanez-Leal <fsantibanez@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/fsantibanezleal/CAOS_PhenoForge
|
|
8
|
+
Keywords: phenomenological-models,ensemble,model-averaging,mineral-processing,flotation,uncertainty-quantification,bootstrap,stacking
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: numpy>=1.24
|
|
18
|
+
Requires-Dist: scipy>=1.10
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
21
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# phenoforge
|
|
25
|
+
|
|
26
|
+
Phenomenological model-family bank and ensemble calibration engine for industrial
|
|
27
|
+
processes. The reference implementation of **BAPE (Bootstrap-Aggregated
|
|
28
|
+
Phenomenological Ensembles)**: instead of selecting one phenomenological model for a
|
|
29
|
+
process dataset, fit many realizations (model families x parameter multistarts x
|
|
30
|
+
bootstrap resamples) from a curated family bank and aggregate them into a calibrated
|
|
31
|
+
ensemble with structural inclusion probabilities.
|
|
32
|
+
|
|
33
|
+
Consumed by the Fragua research product (`CAOS_RES_Fragua`); designed as a
|
|
34
|
+
standalone, domain-agnostic library. The core is pure numpy/scipy (Pyodide-safe).
|
|
35
|
+
|
|
36
|
+
## What it provides
|
|
37
|
+
|
|
38
|
+
- **A family bank** (`phenoforge.families`): named, cited, bounded phenomenological
|
|
39
|
+
models with physical parameter ranges and declared calibration-data contracts.
|
|
40
|
+
Shipping now: the flotation kinetics zoo (Garcia-Zuniga first-order, Klimpel,
|
|
41
|
+
Kelsall, modified Kelsall, gamma rate distribution, second-order, fully mixed, bank
|
|
42
|
+
of N mixers) and the comminution energy-size laws (Rittinger, Kick, Bond, Morrell
|
|
43
|
+
Mi). The bank grows with the Fragua build (thickening, leaching, utilities, PBM).
|
|
44
|
+
- **Fitting** (`phenoforge.fit`): bounded multistart trust-region nonlinear least
|
|
45
|
+
squares per family; information criteria (AIC/AICc/BIC) on every fit.
|
|
46
|
+
- **Ensembles** (`phenoforge.ensemble`):
|
|
47
|
+
- `select` / `akaike_weights` / `averaged_prediction`: IC selection and multimodel
|
|
48
|
+
averaging (Burnham-Anderson).
|
|
49
|
+
- `glue_fit`: GLUE behavioural parameter-set ensembles (Beven-Binley).
|
|
50
|
+
- `bootstrap_fit`: bagging/bragging of one family (paired or moving-block).
|
|
51
|
+
- `stack_fit`: cross-validated convex stacking over the bank (super-learner
|
|
52
|
+
recipe; M-open rationale per Yao-Vehtari-Simpson-Gelman).
|
|
53
|
+
- `bape_fit`: the BAPE ensemble (bootstrap x family-library subsampling, one
|
|
54
|
+
fitted model per member, inclusion probabilities over families).
|
|
55
|
+
- **Metrics** (`phenoforge.metrics`): point (RMSE/MAE/R2), probabilistic calibration
|
|
56
|
+
(ensemble CRPS, PIT, interval coverage), and structural readouts (weight entropy,
|
|
57
|
+
known-truth structural recovery that refuses to run silently null).
|
|
58
|
+
- **Routing** (`phenoforge.route`): match a dataset's declared data kinds to the
|
|
59
|
+
families they can calibrate.
|
|
60
|
+
|
|
61
|
+
## Quick start
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import numpy as np
|
|
65
|
+
from phenoforge import list_families
|
|
66
|
+
from phenoforge.families import flotation
|
|
67
|
+
from phenoforge.ensemble import bape_fit
|
|
68
|
+
|
|
69
|
+
t = np.array([0.5, 1, 2, 4, 8, 12, 16, 20], dtype=float)
|
|
70
|
+
r = 0.85 * (1 - np.exp(-1.2 * t)) # a batch flotation test
|
|
71
|
+
|
|
72
|
+
ens = bape_fit(flotation.BATCH_FAMILIES, t, r, n_members=200, seed=0)
|
|
73
|
+
print(ens.selection_shares()) # which family explains the data, with what share
|
|
74
|
+
print(ens.quantiles(t)) # calibrated predictive bands
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Method background (primary sources)
|
|
78
|
+
|
|
79
|
+
- Fasel, Kutz, Brunton, Brunton 2022. Ensemble-SINDy. Proc. R. Soc. A 478:20210904.
|
|
80
|
+
DOI 10.1098/rspa.2021.0904.
|
|
81
|
+
- Pinto, de Azevedo, Oliveira, von Stosch 2019. Bioprocess Biosyst. Eng.
|
|
82
|
+
42:1853-1865. DOI 10.1007/s00449-019-02181-y.
|
|
83
|
+
- Duan, Ajami, Gao, Sorooshian 2007. Adv. Water Resour. 30:1371-1386.
|
|
84
|
+
DOI 10.1016/j.advwatres.2006.11.014.
|
|
85
|
+
- Beven, Binley 1992. Hydrol. Process. 6:279-298; Beven 2006. J. Hydrol. 320:18-36.
|
|
86
|
+
DOI 10.1016/j.jhydrol.2005.07.007.
|
|
87
|
+
- Yao, Vehtari, Simpson, Gelman 2018. Bayesian Anal. 13:917-1007.
|
|
88
|
+
DOI 10.1214/17-BA1091.
|
|
89
|
+
- Burnham, Anderson 2004. Sociol. Methods Res. 33:261-304.
|
|
90
|
+
DOI 10.1177/0049124104268644.
|
|
91
|
+
- Polat, Chander 2000. Int. J. Miner. Process. 58:145-166.
|
|
92
|
+
DOI 10.1016/S0301-7516(99)00069-1 (flotation family library).
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
py -3.12 -m venv .venv
|
|
98
|
+
.venv/Scripts/python -m pip install -e ".[dev]"
|
|
99
|
+
.venv/Scripts/python -m pytest tests -v
|
|
100
|
+
.venv/Scripts/python -m ruff check src tests
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# phenoforge
|
|
2
|
+
|
|
3
|
+
Phenomenological model-family bank and ensemble calibration engine for industrial
|
|
4
|
+
processes. The reference implementation of **BAPE (Bootstrap-Aggregated
|
|
5
|
+
Phenomenological Ensembles)**: instead of selecting one phenomenological model for a
|
|
6
|
+
process dataset, fit many realizations (model families x parameter multistarts x
|
|
7
|
+
bootstrap resamples) from a curated family bank and aggregate them into a calibrated
|
|
8
|
+
ensemble with structural inclusion probabilities.
|
|
9
|
+
|
|
10
|
+
Consumed by the Fragua research product (`CAOS_RES_Fragua`); designed as a
|
|
11
|
+
standalone, domain-agnostic library. The core is pure numpy/scipy (Pyodide-safe).
|
|
12
|
+
|
|
13
|
+
## What it provides
|
|
14
|
+
|
|
15
|
+
- **A family bank** (`phenoforge.families`): named, cited, bounded phenomenological
|
|
16
|
+
models with physical parameter ranges and declared calibration-data contracts.
|
|
17
|
+
Shipping now: the flotation kinetics zoo (Garcia-Zuniga first-order, Klimpel,
|
|
18
|
+
Kelsall, modified Kelsall, gamma rate distribution, second-order, fully mixed, bank
|
|
19
|
+
of N mixers) and the comminution energy-size laws (Rittinger, Kick, Bond, Morrell
|
|
20
|
+
Mi). The bank grows with the Fragua build (thickening, leaching, utilities, PBM).
|
|
21
|
+
- **Fitting** (`phenoforge.fit`): bounded multistart trust-region nonlinear least
|
|
22
|
+
squares per family; information criteria (AIC/AICc/BIC) on every fit.
|
|
23
|
+
- **Ensembles** (`phenoforge.ensemble`):
|
|
24
|
+
- `select` / `akaike_weights` / `averaged_prediction`: IC selection and multimodel
|
|
25
|
+
averaging (Burnham-Anderson).
|
|
26
|
+
- `glue_fit`: GLUE behavioural parameter-set ensembles (Beven-Binley).
|
|
27
|
+
- `bootstrap_fit`: bagging/bragging of one family (paired or moving-block).
|
|
28
|
+
- `stack_fit`: cross-validated convex stacking over the bank (super-learner
|
|
29
|
+
recipe; M-open rationale per Yao-Vehtari-Simpson-Gelman).
|
|
30
|
+
- `bape_fit`: the BAPE ensemble (bootstrap x family-library subsampling, one
|
|
31
|
+
fitted model per member, inclusion probabilities over families).
|
|
32
|
+
- **Metrics** (`phenoforge.metrics`): point (RMSE/MAE/R2), probabilistic calibration
|
|
33
|
+
(ensemble CRPS, PIT, interval coverage), and structural readouts (weight entropy,
|
|
34
|
+
known-truth structural recovery that refuses to run silently null).
|
|
35
|
+
- **Routing** (`phenoforge.route`): match a dataset's declared data kinds to the
|
|
36
|
+
families they can calibrate.
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import numpy as np
|
|
42
|
+
from phenoforge import list_families
|
|
43
|
+
from phenoforge.families import flotation
|
|
44
|
+
from phenoforge.ensemble import bape_fit
|
|
45
|
+
|
|
46
|
+
t = np.array([0.5, 1, 2, 4, 8, 12, 16, 20], dtype=float)
|
|
47
|
+
r = 0.85 * (1 - np.exp(-1.2 * t)) # a batch flotation test
|
|
48
|
+
|
|
49
|
+
ens = bape_fit(flotation.BATCH_FAMILIES, t, r, n_members=200, seed=0)
|
|
50
|
+
print(ens.selection_shares()) # which family explains the data, with what share
|
|
51
|
+
print(ens.quantiles(t)) # calibrated predictive bands
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Method background (primary sources)
|
|
55
|
+
|
|
56
|
+
- Fasel, Kutz, Brunton, Brunton 2022. Ensemble-SINDy. Proc. R. Soc. A 478:20210904.
|
|
57
|
+
DOI 10.1098/rspa.2021.0904.
|
|
58
|
+
- Pinto, de Azevedo, Oliveira, von Stosch 2019. Bioprocess Biosyst. Eng.
|
|
59
|
+
42:1853-1865. DOI 10.1007/s00449-019-02181-y.
|
|
60
|
+
- Duan, Ajami, Gao, Sorooshian 2007. Adv. Water Resour. 30:1371-1386.
|
|
61
|
+
DOI 10.1016/j.advwatres.2006.11.014.
|
|
62
|
+
- Beven, Binley 1992. Hydrol. Process. 6:279-298; Beven 2006. J. Hydrol. 320:18-36.
|
|
63
|
+
DOI 10.1016/j.jhydrol.2005.07.007.
|
|
64
|
+
- Yao, Vehtari, Simpson, Gelman 2018. Bayesian Anal. 13:917-1007.
|
|
65
|
+
DOI 10.1214/17-BA1091.
|
|
66
|
+
- Burnham, Anderson 2004. Sociol. Methods Res. 33:261-304.
|
|
67
|
+
DOI 10.1177/0049124104268644.
|
|
68
|
+
- Polat, Chander 2000. Int. J. Miner. Process. 58:145-166.
|
|
69
|
+
DOI 10.1016/S0301-7516(99)00069-1 (flotation family library).
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
py -3.12 -m venv .venv
|
|
75
|
+
.venv/Scripts/python -m pip install -e ".[dev]"
|
|
76
|
+
.venv/Scripts/python -m pytest tests -v
|
|
77
|
+
.venv/Scripts/python -m ruff check src tests
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "phenoforge"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "Phenomenological model-family bank and ensemble calibration engine for industrial processes (BAPE: Bootstrap-Aggregated Phenomenological Ensembles)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "Felipe Santibanez-Leal", email = "fsantibanez@gmail.com" }]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
dependencies = ["numpy>=1.24", "scipy>=1.10"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Scientific/Engineering",
|
|
20
|
+
]
|
|
21
|
+
keywords = ["phenomenological-models", "ensemble", "model-averaging", "mineral-processing", "flotation", "uncertainty-quantification", "bootstrap", "stacking"]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Repository = "https://github.com/fsantibanezleal/CAOS_PhenoForge"
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = ["pytest>=8", "ruff>=0.4"]
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
testpaths = ["tests"]
|
|
34
|
+
|
|
35
|
+
[tool.ruff]
|
|
36
|
+
line-length = 100
|
|
37
|
+
target-version = "py310"
|
|
38
|
+
|
|
39
|
+
[tool.ruff.lint]
|
|
40
|
+
select = ["E", "F", "W", "I", "UP", "B"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""phenoforge: phenomenological model-family bank + ensemble calibration engine.
|
|
2
|
+
|
|
3
|
+
The package implements the BAPE methodology (Bootstrap-Aggregated Phenomenological
|
|
4
|
+
Ensembles): instead of selecting ONE phenomenological model for a process dataset,
|
|
5
|
+
fit MANY realizations (model families x parameter multistarts x bootstrap resamples)
|
|
6
|
+
from a curated family bank and aggregate them into a calibrated ensemble with
|
|
7
|
+
structural inclusion probabilities.
|
|
8
|
+
|
|
9
|
+
Nearest prior art, cited and differentiated (see the Fragua research dossiers):
|
|
10
|
+
- Fasel, Kutz, Brunton, Brunton 2022, Ensemble-SINDy, Proc. R. Soc. A 478:20210904,
|
|
11
|
+
DOI 10.1098/rspa.2021.0904 (bagging over generic term libraries).
|
|
12
|
+
- Pinto, de Azevedo, Oliveira, von Stosch 2019, Bioprocess Biosyst. Eng. 42:1853-1865,
|
|
13
|
+
DOI 10.1007/s00449-019-02181-y (bootstrap-aggregated hybrid models, one family).
|
|
14
|
+
- Duan, Ajami, Gao, Sorooshian 2007, Adv. Water Resour. 30:1371-1386,
|
|
15
|
+
DOI 10.1016/j.advwatres.2006.11.014 (BMA across model structures, hydrology).
|
|
16
|
+
- Beven, Binley 1992, Hydrol. Process. 6:279-298 (GLUE); Beven 2006,
|
|
17
|
+
J. Hydrol. 320:18-36, DOI 10.1016/j.jhydrol.2005.07.007 (equifinality).
|
|
18
|
+
- Yao, Vehtari, Simpson, Gelman 2018, Bayesian Anal. 13:917-1007,
|
|
19
|
+
DOI 10.1214/17-BA1091 (stacking in the M-open setting).
|
|
20
|
+
|
|
21
|
+
The core is pure numpy/scipy (Pyodide-safe by design).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
__version__ = "0.01.002"
|
|
25
|
+
|
|
26
|
+
from phenoforge.families.base import DataKind, FitResult, ModelFamily, Param
|
|
27
|
+
from phenoforge.families.registry import get_family, list_families
|
|
28
|
+
from phenoforge.fit.nls import fit_family
|
|
29
|
+
from phenoforge.router import route
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"DataKind",
|
|
33
|
+
"FitResult",
|
|
34
|
+
"ModelFamily",
|
|
35
|
+
"Param",
|
|
36
|
+
"__version__",
|
|
37
|
+
"fit_family",
|
|
38
|
+
"get_family",
|
|
39
|
+
"list_families",
|
|
40
|
+
"route",
|
|
41
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from phenoforge.ensemble.bape import BapeEnsemble, bape_fit
|
|
2
|
+
from phenoforge.ensemble.bootstrap import BootstrapEnsemble, bootstrap_fit
|
|
3
|
+
from phenoforge.ensemble.glue import GlueEnsemble, glue_fit
|
|
4
|
+
from phenoforge.ensemble.ic import akaike_weights, averaged_prediction, select
|
|
5
|
+
from phenoforge.ensemble.stacking import StackedEnsemble, stack_fit
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"BapeEnsemble",
|
|
9
|
+
"BootstrapEnsemble",
|
|
10
|
+
"GlueEnsemble",
|
|
11
|
+
"StackedEnsemble",
|
|
12
|
+
"akaike_weights",
|
|
13
|
+
"averaged_prediction",
|
|
14
|
+
"bape_fit",
|
|
15
|
+
"bootstrap_fit",
|
|
16
|
+
"glue_fit",
|
|
17
|
+
"select",
|
|
18
|
+
"stack_fit",
|
|
19
|
+
]
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""BAPE: Bootstrap-Aggregated Phenomenological Ensembles (rung 6, the novel method).
|
|
2
|
+
|
|
3
|
+
The random-forest recipe with phenomenological model families as base learners:
|
|
4
|
+
|
|
5
|
+
- Each ensemble MEMBER sees (a) a bootstrap resample of the data (the bagging axis,
|
|
6
|
+
Breiman 1996) and (b) a random SUBSET of the family library (the feature-subsampling
|
|
7
|
+
analog applied to equation structure; in E-SINDy this is "library bagging" over
|
|
8
|
+
candidate terms, Fasel et al. 2022, DOI 10.1098/rspa.2021.0904; here it operates
|
|
9
|
+
over whole curated families).
|
|
10
|
+
- Within its subset, the member fits every family and keeps the AICc-best fit (one
|
|
11
|
+
member = one fitted phenomenological model, the way one random-forest member is one
|
|
12
|
+
tree).
|
|
13
|
+
- The ensemble aggregates member predictions (mean/median/quantiles) and reads
|
|
14
|
+
STRUCTURE off the member population: the inclusion probability of a family is the
|
|
15
|
+
fraction of members that selected it (the E-SINDy inclusion-probability readout
|
|
16
|
+
lifted from terms to families).
|
|
17
|
+
|
|
18
|
+
Differentiation from prior art (all cited in the package docstring): E-SINDy bags
|
|
19
|
+
sparse regressions over generic term libraries; Pinto et al. 2019 bags ONE hybrid
|
|
20
|
+
structure; BMA/stacking weight given models without resampling; GLUE keeps parameter
|
|
21
|
+
sets of given structures. BAPE crosses the family axis with the bootstrap axis.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
|
|
30
|
+
from phenoforge.ensemble.bootstrap import bootstrap_indices
|
|
31
|
+
from phenoforge.families.base import FitResult, ModelFamily
|
|
32
|
+
from phenoforge.fit.nls import fit_family
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class BapeMember:
|
|
37
|
+
family: ModelFamily
|
|
38
|
+
fit: FitResult
|
|
39
|
+
boot_index: int
|
|
40
|
+
subset_keys: tuple[str, ...]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class BapeEnsemble:
|
|
45
|
+
families: tuple[ModelFamily, ...]
|
|
46
|
+
members: list[BapeMember]
|
|
47
|
+
requested: int
|
|
48
|
+
meta: dict = field(default_factory=dict)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def kept(self) -> int:
|
|
52
|
+
return len(self.members)
|
|
53
|
+
|
|
54
|
+
def member_predictions(self, x: np.ndarray) -> np.ndarray:
|
|
55
|
+
return np.stack([m.family.predict(x, m.fit.theta) for m in self.members])
|
|
56
|
+
|
|
57
|
+
def predict(self, x: np.ndarray, aggregate: str = "mean") -> np.ndarray:
|
|
58
|
+
m = self.member_predictions(x)
|
|
59
|
+
if aggregate == "mean":
|
|
60
|
+
return m.mean(axis=0)
|
|
61
|
+
if aggregate == "median":
|
|
62
|
+
return np.median(m, axis=0)
|
|
63
|
+
raise ValueError("aggregate must be 'mean' or 'median'")
|
|
64
|
+
|
|
65
|
+
def quantiles(
|
|
66
|
+
self, x: np.ndarray, qs: tuple[float, ...] = (0.05, 0.25, 0.5, 0.75, 0.95)
|
|
67
|
+
) -> np.ndarray:
|
|
68
|
+
return np.quantile(self.member_predictions(x), qs, axis=0)
|
|
69
|
+
|
|
70
|
+
def inclusion_probabilities(self) -> dict[str, float]:
|
|
71
|
+
"""P(family selected | member), the structural-uncertainty readout.
|
|
72
|
+
|
|
73
|
+
Normalizing by the number of members in which the family was OFFERED (it was
|
|
74
|
+
in the member's random subset) rather than by all members, so a family is not
|
|
75
|
+
penalized for having been absent from a member's menu.
|
|
76
|
+
"""
|
|
77
|
+
offered: dict[str, int] = {f.key: 0 for f in self.families}
|
|
78
|
+
selected: dict[str, int] = {f.key: 0 for f in self.families}
|
|
79
|
+
for m in self.members:
|
|
80
|
+
for key in m.subset_keys:
|
|
81
|
+
offered[key] += 1
|
|
82
|
+
selected[m.family.key] += 1
|
|
83
|
+
return {
|
|
84
|
+
k: (selected[k] / offered[k]) if offered[k] > 0 else 0.0 for k in offered
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
def selection_shares(self) -> dict[str, float]:
|
|
88
|
+
"""Fraction of members won by each family (sums to 1)."""
|
|
89
|
+
shares: dict[str, int] = {f.key: 0 for f in self.families}
|
|
90
|
+
for m in self.members:
|
|
91
|
+
shares[m.family.key] += 1
|
|
92
|
+
total = max(self.kept, 1)
|
|
93
|
+
return {k: v / total for k, v in shares.items()}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def bape_fit(
|
|
97
|
+
families: tuple[ModelFamily, ...],
|
|
98
|
+
x: np.ndarray,
|
|
99
|
+
y: np.ndarray,
|
|
100
|
+
*,
|
|
101
|
+
n_members: int = 200,
|
|
102
|
+
subset_size: int | None = None,
|
|
103
|
+
seed: int = 0,
|
|
104
|
+
block: int | None = None,
|
|
105
|
+
n_starts: int = 6,
|
|
106
|
+
criterion: str = "aicc",
|
|
107
|
+
) -> BapeEnsemble:
|
|
108
|
+
"""Fit a BAPE ensemble over the bank.
|
|
109
|
+
|
|
110
|
+
subset_size defaults to ceil(sqrt(len(families))) + 1, the random-forest
|
|
111
|
+
heuristic adapted to small libraries (guarantees >= 2 families per member when
|
|
112
|
+
the bank has >= 2).
|
|
113
|
+
"""
|
|
114
|
+
x = np.asarray(x, dtype=float)
|
|
115
|
+
y = np.asarray(y, dtype=float)
|
|
116
|
+
n = y.shape[0]
|
|
117
|
+
n_fam = len(families)
|
|
118
|
+
if n_fam == 0:
|
|
119
|
+
raise ValueError("empty family bank")
|
|
120
|
+
if subset_size is None:
|
|
121
|
+
subset_size = min(n_fam, int(np.ceil(np.sqrt(n_fam))) + 1)
|
|
122
|
+
subset_size = max(1, min(subset_size, n_fam))
|
|
123
|
+
|
|
124
|
+
rng = np.random.default_rng(seed)
|
|
125
|
+
idx = bootstrap_indices(n, n_members, rng, block=block)
|
|
126
|
+
|
|
127
|
+
members: list[BapeMember] = []
|
|
128
|
+
for b in range(n_members):
|
|
129
|
+
xb, yb = x[idx[b]], y[idx[b]]
|
|
130
|
+
chosen = rng.choice(n_fam, size=subset_size, replace=False)
|
|
131
|
+
subset = tuple(families[int(c)] for c in chosen)
|
|
132
|
+
fits: list[tuple[ModelFamily, FitResult]] = []
|
|
133
|
+
for j, fam in enumerate(subset):
|
|
134
|
+
res = fit_family(
|
|
135
|
+
fam, xb, yb, n_starts=n_starts, seed=seed + 104729 * (b + 1) + j
|
|
136
|
+
)
|
|
137
|
+
if res.success and np.isfinite(res.rss):
|
|
138
|
+
fits.append((fam, res))
|
|
139
|
+
# coherent per-member criterion: preferred unless it is +inf for every
|
|
140
|
+
# candidate in this member's menu (tiny resamples), then BIC for all
|
|
141
|
+
crit = criterion
|
|
142
|
+
if fits and not any(np.isfinite(getattr(r, criterion)) for _, r in fits):
|
|
143
|
+
crit = "bic"
|
|
144
|
+
best: tuple[ModelFamily, FitResult] | None = None
|
|
145
|
+
best_val = float("inf")
|
|
146
|
+
for fam, res in fits:
|
|
147
|
+
val = getattr(res, crit)
|
|
148
|
+
if np.isfinite(val) and val < best_val:
|
|
149
|
+
best_val = val
|
|
150
|
+
best = (fam, res)
|
|
151
|
+
if best is not None:
|
|
152
|
+
members.append(
|
|
153
|
+
BapeMember(
|
|
154
|
+
family=best[0],
|
|
155
|
+
fit=best[1],
|
|
156
|
+
boot_index=b,
|
|
157
|
+
subset_keys=tuple(f.key for f in subset),
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return BapeEnsemble(
|
|
162
|
+
families=families,
|
|
163
|
+
members=members,
|
|
164
|
+
requested=n_members,
|
|
165
|
+
meta={
|
|
166
|
+
"subset_size": subset_size,
|
|
167
|
+
"criterion": criterion,
|
|
168
|
+
"seed": seed,
|
|
169
|
+
"block": block,
|
|
170
|
+
"n_starts": n_starts,
|
|
171
|
+
},
|
|
172
|
+
)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Bootstrap-aggregated fits of one family (rung 5 substrate).
|
|
2
|
+
|
|
3
|
+
Paired (case) bootstrap for independent observations and a moving-block bootstrap for
|
|
4
|
+
serially correlated data (plant time series). Aggregation by mean (bagging, Breiman
|
|
5
|
+
1996) or median (bragging, the E-SINDy usage: Fasel et al. 2022,
|
|
6
|
+
DOI 10.1098/rspa.2021.0904). The single-family bootstrap generalizes the mechanism of
|
|
7
|
+
Pinto et al. 2019 (DOI 10.1007/s00449-019-02181-y) beyond one hybrid structure.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from phenoforge.families.base import FitResult, ModelFamily
|
|
17
|
+
from phenoforge.fit.nls import fit_family
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def bootstrap_indices(
|
|
21
|
+
n: int, n_boot: int, rng: np.random.Generator, block: int | None = None
|
|
22
|
+
) -> np.ndarray:
|
|
23
|
+
"""(n_boot, n) resample index matrix; block > 1 switches to moving-block."""
|
|
24
|
+
if block is None or block <= 1:
|
|
25
|
+
return rng.integers(0, n, size=(n_boot, n))
|
|
26
|
+
n_blocks = int(np.ceil(n / block))
|
|
27
|
+
starts = rng.integers(0, max(n - block + 1, 1), size=(n_boot, n_blocks))
|
|
28
|
+
idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_boot, -1)
|
|
29
|
+
return idx[:, :n]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class BootstrapEnsemble:
|
|
34
|
+
"""Fitted bootstrap fleet of ONE family."""
|
|
35
|
+
|
|
36
|
+
family: ModelFamily
|
|
37
|
+
fits: list[FitResult]
|
|
38
|
+
kept: int
|
|
39
|
+
requested: int
|
|
40
|
+
meta: dict = field(default_factory=dict)
|
|
41
|
+
|
|
42
|
+
def thetas(self) -> np.ndarray:
|
|
43
|
+
return np.stack([f.theta for f in self.fits])
|
|
44
|
+
|
|
45
|
+
def member_predictions(self, x: np.ndarray) -> np.ndarray:
|
|
46
|
+
"""(n_members, n_x) member prediction matrix."""
|
|
47
|
+
return np.stack([self.family.predict(x, f.theta) for f in self.fits])
|
|
48
|
+
|
|
49
|
+
def predict(self, x: np.ndarray, aggregate: str = "mean") -> np.ndarray:
|
|
50
|
+
m = self.member_predictions(x)
|
|
51
|
+
if aggregate == "mean":
|
|
52
|
+
return m.mean(axis=0)
|
|
53
|
+
if aggregate == "median":
|
|
54
|
+
return np.median(m, axis=0)
|
|
55
|
+
raise ValueError("aggregate must be 'mean' (bagging) or 'median' (bragging)")
|
|
56
|
+
|
|
57
|
+
def quantiles(
|
|
58
|
+
self, x: np.ndarray, qs: tuple[float, ...] = (0.05, 0.25, 0.5, 0.75, 0.95)
|
|
59
|
+
) -> np.ndarray:
|
|
60
|
+
return np.quantile(self.member_predictions(x), qs, axis=0)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def bootstrap_fit(
|
|
64
|
+
family: ModelFamily,
|
|
65
|
+
x: np.ndarray,
|
|
66
|
+
y: np.ndarray,
|
|
67
|
+
*,
|
|
68
|
+
n_boot: int = 200,
|
|
69
|
+
seed: int = 0,
|
|
70
|
+
block: int | None = None,
|
|
71
|
+
n_starts: int = 6,
|
|
72
|
+
) -> BootstrapEnsemble:
|
|
73
|
+
"""Fit `family` on n_boot bootstrap resamples of (x, y).
|
|
74
|
+
|
|
75
|
+
Members whose fit fails (non-finite RSS) are dropped and COUNTED: `kept` vs
|
|
76
|
+
`requested` is part of the record, never silently equalized.
|
|
77
|
+
"""
|
|
78
|
+
x = np.asarray(x, dtype=float)
|
|
79
|
+
y = np.asarray(y, dtype=float)
|
|
80
|
+
n = y.shape[0]
|
|
81
|
+
rng = np.random.default_rng(seed)
|
|
82
|
+
idx = bootstrap_indices(n, n_boot, rng, block=block)
|
|
83
|
+
|
|
84
|
+
fits: list[FitResult] = []
|
|
85
|
+
for b in range(n_boot):
|
|
86
|
+
xb, yb = x[idx[b]], y[idx[b]]
|
|
87
|
+
res = fit_family(family, xb, yb, n_starts=n_starts, seed=seed + 7919 * (b + 1))
|
|
88
|
+
if res.success and np.isfinite(res.rss):
|
|
89
|
+
res.meta["boot_index"] = b
|
|
90
|
+
fits.append(res)
|
|
91
|
+
|
|
92
|
+
return BootstrapEnsemble(
|
|
93
|
+
family=family,
|
|
94
|
+
fits=fits,
|
|
95
|
+
kept=len(fits),
|
|
96
|
+
requested=n_boot,
|
|
97
|
+
meta={"block": block, "seed": seed},
|
|
98
|
+
)
|