hallmodel 2026.6.19.6__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.
- hallmodel-2026.6.19.6/LICENSE +2 -0
- hallmodel-2026.6.19.6/LICENSE.md +21 -0
- hallmodel-2026.6.19.6/PKG-INFO +152 -0
- hallmodel-2026.6.19.6/README.md +110 -0
- hallmodel-2026.6.19.6/bindings/bw_cpp.cpp +240 -0
- hallmodel-2026.6.19.6/bw_cpp/__init__.py +137 -0
- hallmodel-2026.6.19.6/external/hallmodel-core/src/adult.cpp +658 -0
- hallmodel-2026.6.19.6/external/hallmodel-core/src/child.cpp +490 -0
- hallmodel-2026.6.19.6/external/hallmodel-core/src/energy.cpp +131 -0
- hallmodel-2026.6.19.6/hallmodel.egg-info/PKG-INFO +152 -0
- hallmodel-2026.6.19.6/hallmodel.egg-info/SOURCES.txt +17 -0
- hallmodel-2026.6.19.6/hallmodel.egg-info/dependency_links.txt +1 -0
- hallmodel-2026.6.19.6/hallmodel.egg-info/not-zip-safe +1 -0
- hallmodel-2026.6.19.6/hallmodel.egg-info/requires.txt +1 -0
- hallmodel-2026.6.19.6/hallmodel.egg-info/top_level.txt +2 -0
- hallmodel-2026.6.19.6/pyproject.toml +58 -0
- hallmodel-2026.6.19.6/setup.cfg +4 -0
- hallmodel-2026.6.19.6/setup.py +36 -0
- hallmodel-2026.6.19.6/tests/test_contract_parity.py +76 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 Instituto Nacional de Salud Pública
|
|
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,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hallmodel
|
|
3
|
+
Version: 2026.6.19.6
|
|
4
|
+
Summary: Python interface to the Hall body-weight dynamics models, backed by the hallmodel-core C++ kernel.
|
|
5
|
+
Author: ChocoTonic
|
|
6
|
+
License: # MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2018 Instituto Nacional de Salud Pública
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/ChocoTonic/hallmodel-py
|
|
29
|
+
Project-URL: Core kernel, https://github.com/ChocoTonic/hallmodel-core
|
|
30
|
+
Project-URL: Upstream R package, https://github.com/INSP-RH/bw
|
|
31
|
+
Keywords: body-weight,metabolism,Hall,dynamic-weight-model
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Programming Language :: Python :: 3
|
|
34
|
+
Classifier: Programming Language :: C++
|
|
35
|
+
Classifier: Topic :: Scientific/Engineering
|
|
36
|
+
Requires-Python: >=3.10
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
License-File: LICENSE
|
|
39
|
+
License-File: LICENSE.md
|
|
40
|
+
Requires-Dist: numpy>=1.24
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# hallmodel-py
|
|
44
|
+
|
|
45
|
+
Python interface to the [Hall](https://www.niddk.nih.gov/about-niddk/staff-directory/biography/hall-kevin)
|
|
46
|
+
adult- and child- body-weight dynamics models, backed by the C++ kernel in
|
|
47
|
+
[hallmodel-core](../hallmodel-core) (vendored here as a git submodule).
|
|
48
|
+
|
|
49
|
+
The kernel is the same math the R package [`INSP-RH/bw`](https://github.com/INSP-RH/bw)
|
|
50
|
+
compiles. Compiled in matched toolchains with `-ffp-contract=off`, the
|
|
51
|
+
outputs are byte-for-byte reproducible against the upstream parity contract;
|
|
52
|
+
under the contract's published tolerance (`rtol=1e-9`, `atol=1e-12`), all 18
|
|
53
|
+
contract cases pass.
|
|
54
|
+
|
|
55
|
+
This is the first reference consumer of `hallmodel-core` — it is also the
|
|
56
|
+
worked example for "here is how you write a new language binding against the
|
|
57
|
+
core."
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
git clone --recurse-submodules <repo-url> hallmodel-py
|
|
65
|
+
cd hallmodel-py
|
|
66
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
67
|
+
pip install -e .
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Build needs a C++17 compiler and `pybind11` (declared in `pyproject.toml`).
|
|
71
|
+
|
|
72
|
+
If you cloned without `--recurse-submodules`:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
git submodule update --init --recursive
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Use
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import bw_cpp
|
|
82
|
+
|
|
83
|
+
# Adult model — single individual, baseline EI from Mifflin-St Jeor
|
|
84
|
+
res = bw_cpp.adult_weight(bw=76, ht=1.73, age=36, sex="male")
|
|
85
|
+
print(res["Body_Weight"].shape) # (1, 366)
|
|
86
|
+
|
|
87
|
+
# Adult with a 500-kcal deficit over 365 days
|
|
88
|
+
import numpy as np
|
|
89
|
+
res = bw_cpp.adult_weight(
|
|
90
|
+
bw=90, ht=1.80, age=40, sex="male",
|
|
91
|
+
EIchange=np.full((1, 365), -500.0),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Child model — auto FFM/FM/EI defaults from age + sex
|
|
95
|
+
res = bw_cpp.child_weight(age=6, sex="male", days=365)
|
|
96
|
+
|
|
97
|
+
# Energy interpolation
|
|
98
|
+
out = bw_cpp.EnergyBuilder().build(
|
|
99
|
+
energy=np.array([[2000, 2200, 1800]]),
|
|
100
|
+
time=np.array([0, 5, 10]),
|
|
101
|
+
method="Linear",
|
|
102
|
+
)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
See the docstrings in [`bw_cpp/__init__.py`](bw_cpp/__init__.py) for the full
|
|
106
|
+
API, and the upstream [R wrappers](https://github.com/INSP-RH/bw/tree/master/R)
|
|
107
|
+
for the semantics of each parameter — they are the same.
|
|
108
|
+
|
|
109
|
+
## Parity proof
|
|
110
|
+
|
|
111
|
+
`./proof/verify.sh` builds the binding against the pinned `hallmodel-core`
|
|
112
|
+
submodule, drives all 18 contract cases through `bw_cpp`, and asserts the
|
|
113
|
+
contract's tolerance via `contract/compare.py`. Exit 0 = parity holds.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
./proof/verify.sh
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The committed [`proof/last_run.log`](proof/last_run.log) and
|
|
120
|
+
[`proof/outputs/`](proof/outputs/) are the frozen evidence of the most
|
|
121
|
+
recent green run. CI re-runs them on every PR. The pinned submodule sha is
|
|
122
|
+
the version contract: bumping it triggers a re-verify.
|
|
123
|
+
|
|
124
|
+
The one excluded field is `BMI_Category` — the contract's `generate.R`
|
|
125
|
+
coerces character matrices through `as.numeric()`, producing a column of
|
|
126
|
+
literal `"NA"` strings in every adult golden. See
|
|
127
|
+
[`hallmodel-core/proof/verify.sh`](external/hallmodel-core/proof/verify.sh)
|
|
128
|
+
for the full explanation.
|
|
129
|
+
|
|
130
|
+
## How this maps to upstream
|
|
131
|
+
|
|
132
|
+
| upstream R | here |
|
|
133
|
+
| ------------------------------------- | ----------------------------- |
|
|
134
|
+
| `INSP-RH/bw::adult_weight()` | `bw_cpp.adult_weight()` |
|
|
135
|
+
| `INSP-RH/bw::child_weight()` | `bw_cpp.child_weight()` |
|
|
136
|
+
| `INSP-RH/bw::child_reference_EI()` | `bw_cpp.child_reference_EI()` |
|
|
137
|
+
| `INSP-RH/bw::child_reference_FFMandFM()` | `bw_cpp.child_reference_FFMandFM()` |
|
|
138
|
+
| `INSP-RH/bw::EnergyBuilder()` | `bw_cpp.EnergyBuilder().build()` |
|
|
139
|
+
|
|
140
|
+
Argument names, semantics, and defaults all match `INSP-RH/bw`'s R wrappers
|
|
141
|
+
because this Python shim was deliberately written as a direct port of those
|
|
142
|
+
wrappers. The contract gate enforces it.
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT, carried forward from [`INSP-RH/bw`](https://github.com/INSP-RH/bw).
|
|
147
|
+
|
|
148
|
+
## Citing
|
|
149
|
+
|
|
150
|
+
Cite the original `bw` package and Hall et al.'s underlying papers. See the
|
|
151
|
+
README of [`INSP-RH/bw`](https://github.com/INSP-RH/bw) and
|
|
152
|
+
[`hallmodel-core/docs/ATTRIBUTION.md`](external/hallmodel-core/docs/ATTRIBUTION.md).
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# hallmodel-py
|
|
2
|
+
|
|
3
|
+
Python interface to the [Hall](https://www.niddk.nih.gov/about-niddk/staff-directory/biography/hall-kevin)
|
|
4
|
+
adult- and child- body-weight dynamics models, backed by the C++ kernel in
|
|
5
|
+
[hallmodel-core](../hallmodel-core) (vendored here as a git submodule).
|
|
6
|
+
|
|
7
|
+
The kernel is the same math the R package [`INSP-RH/bw`](https://github.com/INSP-RH/bw)
|
|
8
|
+
compiles. Compiled in matched toolchains with `-ffp-contract=off`, the
|
|
9
|
+
outputs are byte-for-byte reproducible against the upstream parity contract;
|
|
10
|
+
under the contract's published tolerance (`rtol=1e-9`, `atol=1e-12`), all 18
|
|
11
|
+
contract cases pass.
|
|
12
|
+
|
|
13
|
+
This is the first reference consumer of `hallmodel-core` — it is also the
|
|
14
|
+
worked example for "here is how you write a new language binding against the
|
|
15
|
+
core."
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git clone --recurse-submodules <repo-url> hallmodel-py
|
|
23
|
+
cd hallmodel-py
|
|
24
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
25
|
+
pip install -e .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Build needs a C++17 compiler and `pybind11` (declared in `pyproject.toml`).
|
|
29
|
+
|
|
30
|
+
If you cloned without `--recurse-submodules`:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
git submodule update --init --recursive
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Use
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import bw_cpp
|
|
40
|
+
|
|
41
|
+
# Adult model — single individual, baseline EI from Mifflin-St Jeor
|
|
42
|
+
res = bw_cpp.adult_weight(bw=76, ht=1.73, age=36, sex="male")
|
|
43
|
+
print(res["Body_Weight"].shape) # (1, 366)
|
|
44
|
+
|
|
45
|
+
# Adult with a 500-kcal deficit over 365 days
|
|
46
|
+
import numpy as np
|
|
47
|
+
res = bw_cpp.adult_weight(
|
|
48
|
+
bw=90, ht=1.80, age=40, sex="male",
|
|
49
|
+
EIchange=np.full((1, 365), -500.0),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Child model — auto FFM/FM/EI defaults from age + sex
|
|
53
|
+
res = bw_cpp.child_weight(age=6, sex="male", days=365)
|
|
54
|
+
|
|
55
|
+
# Energy interpolation
|
|
56
|
+
out = bw_cpp.EnergyBuilder().build(
|
|
57
|
+
energy=np.array([[2000, 2200, 1800]]),
|
|
58
|
+
time=np.array([0, 5, 10]),
|
|
59
|
+
method="Linear",
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
See the docstrings in [`bw_cpp/__init__.py`](bw_cpp/__init__.py) for the full
|
|
64
|
+
API, and the upstream [R wrappers](https://github.com/INSP-RH/bw/tree/master/R)
|
|
65
|
+
for the semantics of each parameter — they are the same.
|
|
66
|
+
|
|
67
|
+
## Parity proof
|
|
68
|
+
|
|
69
|
+
`./proof/verify.sh` builds the binding against the pinned `hallmodel-core`
|
|
70
|
+
submodule, drives all 18 contract cases through `bw_cpp`, and asserts the
|
|
71
|
+
contract's tolerance via `contract/compare.py`. Exit 0 = parity holds.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
./proof/verify.sh
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The committed [`proof/last_run.log`](proof/last_run.log) and
|
|
78
|
+
[`proof/outputs/`](proof/outputs/) are the frozen evidence of the most
|
|
79
|
+
recent green run. CI re-runs them on every PR. The pinned submodule sha is
|
|
80
|
+
the version contract: bumping it triggers a re-verify.
|
|
81
|
+
|
|
82
|
+
The one excluded field is `BMI_Category` — the contract's `generate.R`
|
|
83
|
+
coerces character matrices through `as.numeric()`, producing a column of
|
|
84
|
+
literal `"NA"` strings in every adult golden. See
|
|
85
|
+
[`hallmodel-core/proof/verify.sh`](external/hallmodel-core/proof/verify.sh)
|
|
86
|
+
for the full explanation.
|
|
87
|
+
|
|
88
|
+
## How this maps to upstream
|
|
89
|
+
|
|
90
|
+
| upstream R | here |
|
|
91
|
+
| ------------------------------------- | ----------------------------- |
|
|
92
|
+
| `INSP-RH/bw::adult_weight()` | `bw_cpp.adult_weight()` |
|
|
93
|
+
| `INSP-RH/bw::child_weight()` | `bw_cpp.child_weight()` |
|
|
94
|
+
| `INSP-RH/bw::child_reference_EI()` | `bw_cpp.child_reference_EI()` |
|
|
95
|
+
| `INSP-RH/bw::child_reference_FFMandFM()` | `bw_cpp.child_reference_FFMandFM()` |
|
|
96
|
+
| `INSP-RH/bw::EnergyBuilder()` | `bw_cpp.EnergyBuilder().build()` |
|
|
97
|
+
|
|
98
|
+
Argument names, semantics, and defaults all match `INSP-RH/bw`'s R wrappers
|
|
99
|
+
because this Python shim was deliberately written as a direct port of those
|
|
100
|
+
wrappers. The contract gate enforces it.
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT, carried forward from [`INSP-RH/bw`](https://github.com/INSP-RH/bw).
|
|
105
|
+
|
|
106
|
+
## Citing
|
|
107
|
+
|
|
108
|
+
Cite the original `bw` package and Hall et al.'s underlying papers. See the
|
|
109
|
+
README of [`INSP-RH/bw`](https://github.com/INSP-RH/bw) and
|
|
110
|
+
[`hallmodel-core/docs/ATTRIBUTION.md`](external/hallmodel-core/docs/ATTRIBUTION.md).
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// pybind11 bindings exposing the pure-C++ bw kernel (src/kernel, src/include)
|
|
2
|
+
// to Python — the same kernel the R package compiles, so results are identical.
|
|
3
|
+
//
|
|
4
|
+
// These mirror the [[Rcpp::export]] wrappers in src/*_rcpp.cpp one-to-one
|
|
5
|
+
// (same arguments, same call patterns), but marshal NumPy arrays instead of
|
|
6
|
+
// Rcpp types. The thin orchestration shim lives in Python (bw_cpp/__init__.py),
|
|
7
|
+
// mirroring R/*.R.
|
|
8
|
+
//
|
|
9
|
+
// Build with -ffp-contract=off (see pyproject) so the arithmetic matches the
|
|
10
|
+
// contract's golden bit-for-bit when compiled in the same toolchain.
|
|
11
|
+
|
|
12
|
+
#include <pybind11/pybind11.h>
|
|
13
|
+
#include <pybind11/numpy.h>
|
|
14
|
+
#include <pybind11/stl.h>
|
|
15
|
+
|
|
16
|
+
#include <cmath>
|
|
17
|
+
#include <cstddef>
|
|
18
|
+
#include <stdexcept>
|
|
19
|
+
#include <string>
|
|
20
|
+
#include <vector>
|
|
21
|
+
|
|
22
|
+
#include "bw/adult.hpp"
|
|
23
|
+
#include "bw/child.hpp"
|
|
24
|
+
#include "bw/energy.hpp"
|
|
25
|
+
|
|
26
|
+
namespace py = pybind11;
|
|
27
|
+
|
|
28
|
+
// ---- input marshaling ------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
static std::vector<double> vec1d(const py::array_t<double>& a) {
|
|
31
|
+
auto b = a.template unchecked<1>();
|
|
32
|
+
std::vector<double> out(static_cast<std::size_t>(b.shape(0)));
|
|
33
|
+
for (py::ssize_t i = 0; i < b.shape(0); ++i) out[static_cast<std::size_t>(i)] = b(i);
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 2D NumPy (row-major) -> bw::Matrix preserving (i, j).
|
|
38
|
+
static bw::Matrix mat2d(const py::array_t<double>& a) {
|
|
39
|
+
auto b = a.template unchecked<2>();
|
|
40
|
+
bw::Matrix m(static_cast<std::size_t>(b.shape(0)), static_cast<std::size_t>(b.shape(1)));
|
|
41
|
+
for (py::ssize_t i = 0; i < b.shape(0); ++i)
|
|
42
|
+
for (py::ssize_t j = 0; j < b.shape(1); ++j)
|
|
43
|
+
m(static_cast<std::size_t>(i), static_cast<std::size_t>(j)) = b(i, j);
|
|
44
|
+
return m;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---- output marshaling -----------------------------------------------------
|
|
48
|
+
|
|
49
|
+
static py::array_t<double> from_mat(const bw::Matrix& m) {
|
|
50
|
+
py::array_t<double> out({m.nrow, m.ncol});
|
|
51
|
+
auto r = out.mutable_unchecked<2>();
|
|
52
|
+
for (std::size_t i = 0; i < m.nrow; ++i)
|
|
53
|
+
for (std::size_t j = 0; j < m.ncol; ++j)
|
|
54
|
+
r(i, j) = m(i, j);
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static py::array_t<double> from_vec(const std::vector<double>& v) {
|
|
59
|
+
py::array_t<double> out(static_cast<py::ssize_t>(v.size()));
|
|
60
|
+
auto r = out.mutable_unchecked<1>();
|
|
61
|
+
for (std::size_t i = 0; i < v.size(); ++i) r(static_cast<py::ssize_t>(i)) = v[i];
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Child result matrices are laid out data[i + nind*j]; reshape to (nind, nsteps).
|
|
66
|
+
static py::array_t<double> from_child(const std::vector<double>& data,
|
|
67
|
+
std::size_t nind, std::size_t nsteps) {
|
|
68
|
+
py::array_t<double> out({nind, nsteps});
|
|
69
|
+
auto r = out.mutable_unchecked<2>();
|
|
70
|
+
for (std::size_t i = 0; i < nind; ++i)
|
|
71
|
+
for (std::size_t j = 0; j < nsteps; ++j)
|
|
72
|
+
r(i, j) = data[i + nind * j];
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static py::dict adult_result(const bw::AdultResult& res) {
|
|
77
|
+
// BMI_Category: list of per-individual lists of strings (rows = individuals).
|
|
78
|
+
py::list cat;
|
|
79
|
+
for (std::size_t i = 0; i < res.BMI_Category.nrow; ++i) {
|
|
80
|
+
py::list row;
|
|
81
|
+
for (std::size_t j = 0; j < res.BMI_Category.ncol; ++j)
|
|
82
|
+
row.append(res.BMI_Category(i, j));
|
|
83
|
+
cat.append(row);
|
|
84
|
+
}
|
|
85
|
+
py::dict d;
|
|
86
|
+
d["Time"] = from_vec(res.Time);
|
|
87
|
+
d["Age"] = from_mat(res.Age);
|
|
88
|
+
d["Adaptive_Thermogenesis"] = from_mat(res.Adaptive_Thermogenesis);
|
|
89
|
+
d["Extracellular_Fluid"] = from_mat(res.Extracellular_Fluid);
|
|
90
|
+
d["Glycogen"] = from_mat(res.Glycogen);
|
|
91
|
+
d["Fat_Mass"] = from_mat(res.Fat_Mass);
|
|
92
|
+
d["Lean_Mass"] = from_mat(res.Lean_Mass);
|
|
93
|
+
d["Body_Weight"] = from_mat(res.Body_Weight);
|
|
94
|
+
d["Body_Mass_Index"] = from_mat(res.Body_Mass_Index);
|
|
95
|
+
d["BMI_Category"] = cat;
|
|
96
|
+
d["Energy_Intake"] = from_mat(res.Energy_Intake);
|
|
97
|
+
d["Correct_Values"] = res.Correct_Values;
|
|
98
|
+
d["Model_Type"] = res.Model_Type;
|
|
99
|
+
return d;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static py::dict child_result(const bw::ChildRK4Result& R) {
|
|
103
|
+
py::dict d;
|
|
104
|
+
d["Time"] = from_vec(R.Time);
|
|
105
|
+
d["Age"] = from_child(R.Age, R.nind, R.nsteps);
|
|
106
|
+
d["Fat_Free_Mass"] = from_child(R.Fat_Free_Mass, R.nind, R.nsteps);
|
|
107
|
+
d["Fat_Mass"] = from_child(R.Fat_Mass, R.nind, R.nsteps);
|
|
108
|
+
d["Body_Weight"] = from_child(R.Body_Weight, R.nind, R.nsteps);
|
|
109
|
+
d["Correct_Values"] = R.Correct_Values;
|
|
110
|
+
d["Model_Type"] = std::string("Children");
|
|
111
|
+
return d;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---- exported functions (mirror src/*_rcpp.cpp) ----------------------------
|
|
115
|
+
|
|
116
|
+
static py::dict adult_baseline(py::array_t<double> bw, py::array_t<double> ht,
|
|
117
|
+
py::array_t<double> age, py::array_t<double> sex,
|
|
118
|
+
py::array_t<double> EIchange, py::array_t<double> NAchange,
|
|
119
|
+
py::array_t<double> PAL, py::array_t<double> pcarb_base,
|
|
120
|
+
py::array_t<double> pcarb, double dt, double days, bool checkValues) {
|
|
121
|
+
bw::Adult P(vec1d(bw), vec1d(ht), vec1d(age), vec1d(sex),
|
|
122
|
+
mat2d(EIchange), mat2d(NAchange),
|
|
123
|
+
vec1d(PAL), vec1d(pcarb), vec1d(pcarb_base), dt, checkValues);
|
|
124
|
+
return adult_result(P.rk4(days));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
static py::dict adult_ei(py::array_t<double> bw, py::array_t<double> ht,
|
|
128
|
+
py::array_t<double> age, py::array_t<double> sex,
|
|
129
|
+
py::array_t<double> EIchange, py::array_t<double> NAchange,
|
|
130
|
+
py::array_t<double> PAL, py::array_t<double> pcarb_base,
|
|
131
|
+
py::array_t<double> pcarb, double dt, py::array_t<double> extradata,
|
|
132
|
+
double days, bool checkValues, bool isEnergy) {
|
|
133
|
+
bw::Adult P(vec1d(bw), vec1d(ht), vec1d(age), vec1d(sex),
|
|
134
|
+
mat2d(EIchange), mat2d(NAchange),
|
|
135
|
+
vec1d(PAL), vec1d(pcarb), vec1d(pcarb_base), dt,
|
|
136
|
+
vec1d(extradata), checkValues, isEnergy);
|
|
137
|
+
return adult_result(P.rk4(days));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
static py::dict adult_ei_fat(py::array_t<double> bw, py::array_t<double> ht,
|
|
141
|
+
py::array_t<double> age, py::array_t<double> sex,
|
|
142
|
+
py::array_t<double> EIchange, py::array_t<double> NAchange,
|
|
143
|
+
py::array_t<double> PAL, py::array_t<double> pcarb_base,
|
|
144
|
+
py::array_t<double> pcarb, double dt, py::array_t<double> input_EI,
|
|
145
|
+
py::array_t<double> input_fat, double days, bool checkValues) {
|
|
146
|
+
bw::Adult P(vec1d(bw), vec1d(ht), vec1d(age), vec1d(sex),
|
|
147
|
+
mat2d(EIchange), mat2d(NAchange),
|
|
148
|
+
vec1d(PAL), vec1d(pcarb), vec1d(pcarb_base), dt,
|
|
149
|
+
vec1d(input_EI), vec1d(input_fat), checkValues);
|
|
150
|
+
return adult_result(P.rk4(days));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
static py::dict child_classic(py::array_t<double> age, py::array_t<double> sex,
|
|
154
|
+
py::array_t<double> FFM, py::array_t<double> FM,
|
|
155
|
+
py::array_t<double> EIntake, double days, double dt, bool checkValues) {
|
|
156
|
+
auto b = EIntake.unchecked<2>();
|
|
157
|
+
std::size_t nr = static_cast<std::size_t>(b.shape(0));
|
|
158
|
+
std::size_t nc = static_cast<std::size_t>(b.shape(1));
|
|
159
|
+
std::vector<double> ei(nr * nc);
|
|
160
|
+
for (std::size_t r = 0; r < nr; ++r)
|
|
161
|
+
for (std::size_t c = 0; c < nc; ++c) ei[r * nc + c] = b(static_cast<py::ssize_t>(r), static_cast<py::ssize_t>(c));
|
|
162
|
+
bw::Child P(vec1d(age), vec1d(sex), vec1d(FFM), vec1d(FM), ei, nr, nc, dt, checkValues);
|
|
163
|
+
return child_result(P.rk4(days - 1)); // days-1: see child_rcpp.cpp
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
static py::dict child_richardson(py::array_t<double> age, py::array_t<double> sex,
|
|
167
|
+
py::array_t<double> FFM, py::array_t<double> FM,
|
|
168
|
+
double K, double Q, double A, double B, double nu, double C,
|
|
169
|
+
double days, double dt, bool checkValues) {
|
|
170
|
+
bw::Child P(vec1d(age), vec1d(sex), vec1d(FFM), vec1d(FM), K, Q, A, B, nu, C, dt, checkValues);
|
|
171
|
+
return child_result(P.rk4(days - 1));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// mass_reference_wrapper: returns {FM, FFM} (mirrors src/child_rcpp.cpp).
|
|
175
|
+
static py::dict mass_reference(py::array_t<double> age, py::array_t<double> sex) {
|
|
176
|
+
std::vector<double> age_v = vec1d(age), sex_v = vec1d(sex);
|
|
177
|
+
std::vector<double> ffm(age_v.size(), 0.0), fm(age_v.size(), 0.0);
|
|
178
|
+
std::vector<double> ei(1, 0.0);
|
|
179
|
+
bw::Child P(age_v, sex_v, ffm, fm, ei, 1, 1, 1.0, false);
|
|
180
|
+
py::dict d;
|
|
181
|
+
d["FM"] = from_vec(P.FMReference(age_v));
|
|
182
|
+
d["FFM"] = from_vec(P.FFMReference(age_v));
|
|
183
|
+
return d;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// intake_reference_wrapper(age, sex, FFM, FM, days, dt) -> (nind, ncols).
|
|
187
|
+
static py::array_t<double> intake_reference(py::array_t<double> age, py::array_t<double> sex,
|
|
188
|
+
py::array_t<double> FFM, py::array_t<double> FM, double days, double dt) {
|
|
189
|
+
std::vector<double> age_v = vec1d(age), sex_v = vec1d(sex);
|
|
190
|
+
std::vector<double> ffm_v = vec1d(FFM), fm_v = vec1d(FM);
|
|
191
|
+
std::vector<double> ei(1, 0.0);
|
|
192
|
+
bw::Child P(age_v, sex_v, ffm_v, fm_v, ei, 1, 1, dt, false);
|
|
193
|
+
std::size_t nind = age_v.size();
|
|
194
|
+
std::size_t ncols = static_cast<std::size_t>(std::floor(days / dt) + 1);
|
|
195
|
+
bw::Matrix out(nind, ncols);
|
|
196
|
+
for (double i = 0; i < std::floor(days / dt) + 1; i += 1.0) {
|
|
197
|
+
std::vector<double> t(nind);
|
|
198
|
+
for (std::size_t k = 0; k < nind; ++k) t[k] = age_v[k] + dt * i / 365.0;
|
|
199
|
+
std::vector<double> col = P.IntakeReference(t);
|
|
200
|
+
std::size_t ci = static_cast<std::size_t>(i);
|
|
201
|
+
for (std::size_t k = 0; k < nind; ++k) out(k, ci) = col[k];
|
|
202
|
+
}
|
|
203
|
+
return from_mat(out);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// EnergyBuilder: deterministic methods only. Energy is (nrow, ntimes).
|
|
207
|
+
static py::array_t<double> energy_builder(py::array_t<double> Energy,
|
|
208
|
+
py::array_t<double> Time, const std::string& interpol) {
|
|
209
|
+
auto e = Energy.unchecked<2>();
|
|
210
|
+
std::size_t nrow = static_cast<std::size_t>(e.shape(0));
|
|
211
|
+
std::size_t nt = static_cast<std::size_t>(e.shape(1));
|
|
212
|
+
std::vector<double> tvec = vec1d(Time);
|
|
213
|
+
// build_deterministic expects column-major Energy[r + c*nrow]
|
|
214
|
+
std::vector<double> ecol(nrow * nt);
|
|
215
|
+
for (std::size_t r = 0; r < nrow; ++r)
|
|
216
|
+
for (std::size_t c = 0; c < nt; ++c) ecol[r + c * nrow] = e(static_cast<py::ssize_t>(r), static_cast<py::ssize_t>(c));
|
|
217
|
+
std::size_t ncols = bw::energy::output_ncols(tvec.data(), tvec.size());
|
|
218
|
+
std::vector<double> evals(nrow * ncols, 0.0);
|
|
219
|
+
bw::energy::Method m;
|
|
220
|
+
if (!bw::energy::parse_method(interpol.c_str(), &m))
|
|
221
|
+
throw std::invalid_argument("unknown/unsupported interpolation: " + interpol);
|
|
222
|
+
bw::energy::build_deterministic(ecol.data(), nrow, nt, tvec.data(), m, evals.data());
|
|
223
|
+
// evals column-major -> (nrow, ncols)
|
|
224
|
+
bw::Matrix out(nrow, ncols);
|
|
225
|
+
for (std::size_t r = 0; r < nrow; ++r)
|
|
226
|
+
for (std::size_t c = 0; c < ncols; ++c) out(r, c) = evals[r + c * nrow];
|
|
227
|
+
return from_mat(out);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
PYBIND11_MODULE(_bw_cpp, m) {
|
|
231
|
+
m.doc() = "C++ kernel bindings for the bw dynamic body-weight models.";
|
|
232
|
+
m.def("adult_baseline", &adult_baseline);
|
|
233
|
+
m.def("adult_ei", &adult_ei);
|
|
234
|
+
m.def("adult_ei_fat", &adult_ei_fat);
|
|
235
|
+
m.def("child_classic", &child_classic);
|
|
236
|
+
m.def("child_richardson", &child_richardson);
|
|
237
|
+
m.def("mass_reference", &mass_reference);
|
|
238
|
+
m.def("intake_reference", &intake_reference);
|
|
239
|
+
m.def("energy_builder", &energy_builder);
|
|
240
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""bw_cpp — Python interface to the bw dynamic body-weight models, backed by the
|
|
2
|
+
pure-C++ kernel (the same kernel the R package compiles).
|
|
3
|
+
|
|
4
|
+
Python ergonomics, C++ performance, and — built in the pinned toolchain with
|
|
5
|
+
-ffp-contract=off — output bit-identical to the upstream R/C++ results.
|
|
6
|
+
|
|
7
|
+
This module is a thin orchestration shim mirroring R/adult_weight.R and
|
|
8
|
+
R/child_weight.R: it validates inputs, converts sex to 0/1, fills defaults, and
|
|
9
|
+
dispatches to the compiled kernel in _bw_cpp. All numeric work is in C++.
|
|
10
|
+
"""
|
|
11
|
+
import math
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
import _bw_cpp
|
|
16
|
+
|
|
17
|
+
__all__ = ["adult_weight", "child_weight", "EnergyBuilder",
|
|
18
|
+
"child_reference_FFMandFM", "child_reference_EI"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _sex_to_num(sex):
|
|
22
|
+
"""'male'->0, 'female'->1 (accepts scalar/list/array of strings or numbers)."""
|
|
23
|
+
arr = np.atleast_1d(np.asarray(sex, dtype=object))
|
|
24
|
+
out = np.zeros(len(arr), dtype=float)
|
|
25
|
+
for i, s in enumerate(arr):
|
|
26
|
+
if isinstance(s, str):
|
|
27
|
+
if s == "female":
|
|
28
|
+
out[i] = 1.0
|
|
29
|
+
elif s != "male":
|
|
30
|
+
raise ValueError(f"Invalid sex '{s}'. Use 'male' or 'female'.")
|
|
31
|
+
else:
|
|
32
|
+
out[i] = float(s)
|
|
33
|
+
return out
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _vec(x, n):
|
|
37
|
+
a = np.atleast_1d(np.asarray(x, dtype=float))
|
|
38
|
+
if a.size == 1 and n > 1:
|
|
39
|
+
a = np.full(n, a.item())
|
|
40
|
+
return np.ascontiguousarray(a, dtype=float)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def adult_weight(bw, ht, age, sex, *, EIchange=None, NAchange=None, EI=None,
|
|
44
|
+
fat=None, PAL=1.5, pcarb_base=0.5, pcarb=None, days=365, dt=1.0,
|
|
45
|
+
check_values=True):
|
|
46
|
+
"""Adult body-weight trajectory. Mirrors R::adult_weight."""
|
|
47
|
+
bw = _vec(bw, 1); n = bw.size
|
|
48
|
+
ht = _vec(ht, n); age = _vec(age, n)
|
|
49
|
+
newsex = _sex_to_num(sex)
|
|
50
|
+
PAL = _vec(PAL, n); pcarb_base = _vec(pcarb_base, n)
|
|
51
|
+
pcarb = pcarb_base.copy() if pcarb is None else _vec(pcarb, n)
|
|
52
|
+
|
|
53
|
+
ncol = abs(math.ceil(days / dt))
|
|
54
|
+
EIchange = np.zeros((n, ncol)) if EIchange is None else np.atleast_2d(np.asarray(EIchange, float))
|
|
55
|
+
NAchange = np.zeros((n, ncol)) if NAchange is None else np.atleast_2d(np.asarray(NAchange, float))
|
|
56
|
+
|
|
57
|
+
fat_arr = np.full(n, np.nan) if fat is None else _vec(fat, n)
|
|
58
|
+
isfat = bool(np.any(np.isnan(fat_arr))) # True == fat NOT supplied
|
|
59
|
+
if EI is None:
|
|
60
|
+
EI_arr = np.full(n, np.nan)
|
|
61
|
+
else:
|
|
62
|
+
EI_arr = _vec(EI, n)
|
|
63
|
+
isEI = bool(np.any(np.isnan(EI_arr))) # True == EI NOT supplied
|
|
64
|
+
|
|
65
|
+
# kernel expects EIchange/NAchange transposed (rows = day, cols = individual)
|
|
66
|
+
EIc = np.ascontiguousarray(EIchange.T, dtype=float)
|
|
67
|
+
NAc = np.ascontiguousarray(NAchange.T, dtype=float)
|
|
68
|
+
cd = float(math.ceil(days))
|
|
69
|
+
|
|
70
|
+
if isfat and isEI:
|
|
71
|
+
res = _bw_cpp.adult_baseline(bw, ht, age, newsex, EIc, NAc, PAL, pcarb_base, pcarb, dt, cd, check_values)
|
|
72
|
+
elif (not isEI) and isfat:
|
|
73
|
+
res = _bw_cpp.adult_ei(bw, ht, age, newsex, EIc, NAc, PAL, pcarb_base, pcarb, dt, EI_arr, cd, check_values, True)
|
|
74
|
+
elif isEI and (not isfat):
|
|
75
|
+
res = _bw_cpp.adult_ei(bw, ht, age, newsex, EIc, NAc, PAL, pcarb_base, pcarb, dt, fat_arr, cd, check_values, False)
|
|
76
|
+
else:
|
|
77
|
+
res = _bw_cpp.adult_ei_fat(bw, ht, age, newsex, EIc, NAc, PAL, pcarb_base, pcarb, dt, EI_arr, fat_arr, cd, check_values)
|
|
78
|
+
|
|
79
|
+
if res["Correct_Values"] is False:
|
|
80
|
+
raise ValueError("One of the variables takes negative/NaN/NA/infinity values")
|
|
81
|
+
return res
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def child_reference_FFMandFM(age, sex):
|
|
85
|
+
"""Reference FFM/FM. Mirrors R::child_reference_FFMandFM."""
|
|
86
|
+
age = _vec(age, 1)
|
|
87
|
+
return _bw_cpp.mass_reference(age, _sex_to_num(sex))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def child_reference_EI(age, sex, FM, FFM, days, dt=1.0):
|
|
91
|
+
"""Default child energy intake. Mirrors R::child_reference_EI.
|
|
92
|
+
|
|
93
|
+
NOTE the argument positions: R passes (age, sex, FM, FFM) into a wrapper whose
|
|
94
|
+
params are (age, sex, FFM, FM) — i.e. FM and FFM occupy swapped slots. We
|
|
95
|
+
reproduce that exactly, then transpose the result.
|
|
96
|
+
"""
|
|
97
|
+
age = _vec(age, 1)
|
|
98
|
+
out = _bw_cpp.intake_reference(age, _sex_to_num(sex),
|
|
99
|
+
np.asarray(FM, float), np.asarray(FFM, float),
|
|
100
|
+
float(days), float(dt))
|
|
101
|
+
return np.ascontiguousarray(np.asarray(out).T, dtype=float)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def child_weight(age, sex, *, FM=None, FFM=None, EI=None, richardson_params=None,
|
|
105
|
+
days=365, dt=1.0, check_values=True):
|
|
106
|
+
"""Child weight trajectory. Mirrors R::child_weight."""
|
|
107
|
+
age = _vec(age, 1); n = age.size
|
|
108
|
+
newsex = _sex_to_num(sex)
|
|
109
|
+
if FM is None or FFM is None:
|
|
110
|
+
ref = child_reference_FFMandFM(age, sex)
|
|
111
|
+
FM = ref["FM"] if FM is None else _vec(FM, n)
|
|
112
|
+
FFM = ref["FFM"] if FFM is None else _vec(FFM, n)
|
|
113
|
+
else:
|
|
114
|
+
FM = _vec(FM, n); FFM = _vec(FFM, n)
|
|
115
|
+
|
|
116
|
+
rp = richardson_params
|
|
117
|
+
have_rich = rp is not None and all(rp.get(k) is not None for k in ("K", "Q", "A", "B", "nu", "C"))
|
|
118
|
+
|
|
119
|
+
if EI is None and not have_rich:
|
|
120
|
+
EI = child_reference_EI(age, sex, FM, FFM, days, dt)
|
|
121
|
+
|
|
122
|
+
if EI is not None:
|
|
123
|
+
EImat = np.atleast_2d(np.asarray(EI, dtype=float))
|
|
124
|
+
return _bw_cpp.child_classic(age, newsex, np.asarray(FFM, float), np.asarray(FM, float),
|
|
125
|
+
np.ascontiguousarray(EImat), float(days), float(dt), check_values)
|
|
126
|
+
return _bw_cpp.child_richardson(age, newsex, np.asarray(FFM, float), np.asarray(FM, float),
|
|
127
|
+
float(rp["K"]), float(rp["Q"]), float(rp["A"]), float(rp["B"]),
|
|
128
|
+
float(rp["nu"]), float(rp["C"]), float(days), float(dt), check_values)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class EnergyBuilder:
|
|
132
|
+
"""Energy-intake interpolation (deterministic methods), backed by C++."""
|
|
133
|
+
|
|
134
|
+
def build(self, energy, time, method="Linear"):
|
|
135
|
+
energy = np.atleast_2d(np.asarray(energy, dtype=float))
|
|
136
|
+
time = np.ascontiguousarray(np.asarray(time, dtype=float))
|
|
137
|
+
return _bw_cpp.energy_builder(np.ascontiguousarray(energy), time, str(method))
|