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.
@@ -0,0 +1,2 @@
1
+ YEAR: 2018
2
+ COPYRIGHT HOLDER: Instituto Nacional de Salud Pública
@@ -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))