Source code for orgmatt.metabolism.ref_human_metabolism
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2022-2023 Tanguy Fardet
# SPDX-License-Identifier: GPL-3.0-or-later
# orgmatt/metabolism/constant_metabolic_model.py
from typing import Literal
import numpy as np
from .._utils.age import _array_numeric_age
from .._utils.tools import is_arraylike
from ..typing import NumericOrArray
from ..units import Quantity, check_dim, ureg
from .diets import Diet, get_diet
from .intake_fraction import intake_fraction_age
[docs]
class ReferenceHumanMetabolism:
"""
Reference metabolic model that provides consistent results base on fixed
excretion rates in urine and feces, as well as nutrient retention and
various losses. The model is consistent, meaning that the sum of all
outputs and retention is always equal to the nutrient input from food.
This generic model provides approximate results for all age groups and
sex/genders. The results for the excretions of an individual are computed
based on the baseline ingestion from an average adult.
Note
----
This model should be rather good for most physiological conditions but
will not provide relevant results for situations where extreme nutrient
deprivation occurs. It may also deviate from experimental results for
nutrient intakes that are significantly greater than recommended intakes.
"""
def __init__(self, diet: str | Diet | tuple[Quantity, ...] | None = None):
'''
Initialize a metabolic model to estimate the intake and excretions of
a person based on their age (group), sex, and diet.
Parameters
----------
diet : str :class:`~orgmatt.metabolism.Diet` or (N, P, K) values for the average adult intake, optional
Ingestion from an adult, either a pre-defined diet or custom values.
'''
self._diet = get_diet(diet or "INCA3")
@property
def diet(self):
'''Diet used in the model.'''
return Diet(
N_adult=self._diet.N_adult,
P_adult=self._diet.P_adult,
K_adult=self._diet.K_adult,
Na_adult=self._diet.Na_adult,
water_intake=self._diet.water_intake,
name=self._diet.name,
)
[docs]
def nutrient_intake(
self,
duration: Quantity,
nutrient: str,
age: NumericOrArray | str | None = None,
sex: Literal["male", "female"] | None = None,
) -> Quantity:
'''
Return nutrient intake from food.
Parameters
----------
duration : float [time]
Time interval over which the nitrogen is excreted.
nutrient : str
Nutrient of interest among ('N', 'K', 'P', 'Mg', 'Ca').
age : int, array, or str, optional (default: "adult")
Age or age group for which the nutrient intake should be
calculated. Can be a year, a range of years like "18-44", or a
group among "infant", "toddler", "kid", "teenager", "young adult",
"adult", "senior".
sex : "male" or "female", optional (default: average value)
Sex of the individual.
Returns
-------
intake: quantity
Intake of `nutrient` over `duration`. If `age` was an array,
contains one entry per age.
'''
age = "adult" if age is None else age
adult_intake = getattr(self._diet, f"{nutrient}_adult")
return (
duration.to("day").m
* intake_fraction_age(age, adult_intake, sex)
* adult_intake
)
[docs]
def flow_fraction(
self,
nutrient: str,
flow: Literal["urine", "feces", "retention", "loss"],
age: NumericOrArray | str | None = None,
sex: Literal["male", "female"] | None = None,
) -> NumericOrArray:
'''
Fraction of a nutrient going in a type of flow ("urine", "feces",
or "retention" in the body).
Parameters
----------
nutrient : str
The nutrient of interest.
flow : str ("urine", "feces", or "retention")
The flow considered.
age : int, array, or str, optional (default: "adult")
Age or age group for which the nutrient intake should be
calculated. Can be a year, a range of years like "18-44", or a
group among "infant", "toddler", "kid", "teenager", "young adult",
"adult", "senior".
sex : "male" or "female", optional (default: average value)
Sex of the individual.
Returns
-------
fraction: float or array of floats
Fraction of `nutrient` containd in `flow`. If `age` was an array,
contains one entry per age.
Note
----
The retention fraction is generated such that, multiplied by the
diet's intakes, it gives the reference retention in g/day obtained in
``nutrient_retention.ipynb`` from US growth curves and body
composition data. This means that models using different diets will
lead to different fractions but identical absolute retention.
'''
assert nutrient in ("N", "P", "K"), (
"`nutrient` should be 'N', 'P', or 'K'."
)
age = "adult" if age is None else age
if flow == "urine":
# we need to compute everything at once for consistency
retention = self._fraction_nutrient_retention(nutrient, age, sex)
loss = _fractions["loss"][nutrient]
feces = _fractions["feces"][nutrient]
return 1 - loss - feces - retention
elif flow == "retention":
return self._fraction_nutrient_retention(nutrient, age, sex)
if isinstance(age, (int, float, np.integer, str)):
return _fractions[flow][nutrient]
return np.array([_fractions[flow][nutrient]] * len(age))
[docs]
@check_dim(arglist=('duration', 1, '[time]'), result='[mass]*[X]')
def nutrient_excretion(
self,
duration: Quantity,
nutrient: Literal["N", "P", "K"],
excreta: Literal["urine", "feces", "all"] = "all",
age: NumericOrArray | str | None = None,
sex: Literal["male", "female"] | None = None,
) -> Quantity:
'''
Return the amount of nutrient excreted in urine and/or feces.
Parameters
----------
duration : float [time]
Time interval over which the nitrogen is excreted.
nutrient : str
Nutrient of interest among ('N', 'P', 'K').
excreta : str (optional, default: "all")
Type of excreta ("urine", "feces", or "all").
age : int, array, or str, optional (default: "adult")
Age or age group for which the nutrient intake should be
calculated. Can be a year, a range of years like "18-44", or a
group among "infant", "toddler", "kid", "teenager", "young adult",
"adult", "senior".
sex : "male" or "female", optional (default: average value)
Sex of the individual.
Returns
-------
excretion: Quantity [mass]*[nutrient]
Amount of `nutrient` excreted over `duration`. If `age` was an
array, contains one entry per age.
'''
intake = self.nutrient_intake(duration, nutrient, age=age, sex=sex)
frac = np.zeros(len(intake)) if is_arraylike(age) else 0
if excreta in ("all", "urine"):
frac += self.flow_fraction(nutrient, "urine", age, sex)
if excreta in ("all", "feces"):
frac += _fractions["feces"][nutrient]
return frac * intake
[docs]
@check_dim(arglist=('duration', 1, '[time]'), result='[length]**3')
def urine_excretion(
self,
duration: Quantity,
age: NumericOrArray | str | None = None,
sex: Literal["male", "female"] | None = None,
) -> Quantity:
r'''
Return the amount of urine excreted.
This is computed as 65% of the total water intake for a 25 year-old
adult, :math:`I_{H_2O}^{\text{tot}}`.
For individuals :math:`y` years old (:math:`y` < 25`), it is defined
as:
.. math::
V_u = V_u^{\text{min}} + \left(V_u^{(25yo)} - V_u^{\text{min}}\right) \cdot (1 - y / 25)
where :math:`V_u^{\text{min}} = \max(0.35 L, 0.16 \cdot I_{H_2O}^{\text{tot}})`
and :math:`V_u^{(25yo)} = 0.65 \cdot I_{H_2O}^{\text{tot}}`.
For individuals older than 25, it is defined as:
.. math::
V_u = V_u^{(25yo)} + 0.003 \cdot (y - 25)
Parameters
----------
duration : float [time]
Time interval over which the nitrogen is excreted.
age : int, array, or str, optional (default: "adult")
Age or age group for which the nutrient intake should be
calculated. Can be a year, a range of years like "18-44", or a
group among "infant", "toddler", "kid", "teenager", "young adult",
"adult", "senior".
sex : "male" or "female", optional (default: average value)
Sex of the individual.
Returns
-------
volume: Quantity [length]**3
Volume of urine excreted over `duration`. If `age` was an
array, contains one entry per age.
'''
adult_water_intake = self._diet.water_intake
age_adult = 25
age_arr = _array_numeric_age(age_adult if age is None else age)
volume = np.zeros(len(age_arr)) * ureg.L
# compute reference volumes
vmin = max(0.35 * ureg.L, 0.16 * adult_water_intake)
vadult = 0.65 * adult_water_intake
# compute all urine volumes
volume += vadult + (vmin - vadult) * (
1 - np.minimum(age_arr, age_adult) / age_adult
)
older = age_arr > age_adult
volume[older] += (
0.003 * ureg.L * (np.minimum(age_arr[older], 75) - age_adult)
)
if is_arraylike(age):
return volume * duration.to("day").m
return volume[0] * duration.to("day").m
[docs]
def body_composition(
self,
nutrient: Literal["N", "P"],
age: NumericOrArray | str | None = None,
sex: Literal["male", "female", "mixed"] | None = None,
) -> NumericOrArray:
'''
Return the body composition for a given age and gender.
Parameters
----------
nutrient : str
Nutrient of interest, either 'N' (nitrogen) pr 'P' (phosphorus).
age : int, array, or str, optional (default: "adult")
Age or age group for which the nutrient intake should be
calculated. Can be a year, a range of years like "18-44", or a
group among "infant", "toddler", "kid", "teenager", "young adult",
"adult", "senior".
sex : "male" or "female", optional (default: average value)
Sex of the individual.
'''
numeric_age = _array_numeric_age("adult" if age is None else age)
if isinstance(sex, str) and sex != "mixed":
assert sex in ("male", "female"), (
"Only 'male', 'female', or 'mixed' are present in the "
"available databases."
)
sex = [sex]
else:
sex = ["male", "female"]
frac = np.zeros(len(numeric_age))
for s in sex:
age_start = _composition[s][nutrient]["age_start"]
fstart = _composition[s][nutrient]["start"]
frac[numeric_age <= age_start] += fstart
# increase to peak
age_peak = _composition[s][nutrient]["age_extremum"]
fpeak = _composition[s][nutrient]["extremum"]
keep = (numeric_age > age_start) & (numeric_age <= age_peak)
frac[keep] += fstart + (fpeak - fstart) * (
numeric_age[keep] - age_start
) / (age_peak - age_start)
# decrease to end
age_end = _composition[s][nutrient]["age_end"]
fend = _composition[s][nutrient]["end"]
keep = (numeric_age > age_peak) & (numeric_age <= age_end)
frac[keep] += fpeak + (fend - fpeak) * (
numeric_age[keep] - age_peak
) / (age_end - age_peak)
frac[numeric_age > age_end] += fend
frac /= len(sex)
if len(frac) == 1 and isinstance(age, (str, int, float, np.integer)):
return frac[0]
return frac
def _fraction_nutrient_retention(
self,
nutrient: str,
age: NumericOrArray | str | None = None,
sex: Literal["male", "female", "mixed"] | None = None,
) -> NumericOrArray:
'''
Model the fraction of nutrient that is retained in the body as a
function of the age and sex.
Note
----
The fraction is generated such that, multiplied by the diet's intakes,
it gives the reference retention in g/day obtained in
``nutrient_retention.ipynb`` from US growth curves and body
composition data. This means that models using different diets will
lead to different fractions but identical absolute retention.
'''
numeric_age = _array_numeric_age("adult" if age is None else age)
if isinstance(sex, str) and sex != "mixed":
assert sex in ("male", "female"), (
"Only 'male', 'female', or 'mixed' are present in the "
"available databases."
)
sex = [sex]
else:
sex = ["male", "female"]
frac = np.zeros(len(numeric_age))
for s in sex:
ages = _ages_retention[s]
fractions = _frac_retention[s]
num_entries = len(ages)
assert len(fractions) == num_entries, (
"Internal error with retention fractions, please raise an "
"issue here: https://lists.sr.ht/~tfardet/CAFE-discuss"
)
for i, frac_ret in enumerate(fractions[:-1]):
age = ages[i]
next_age = ages[i + 1]
next_frac = fractions[i + 1]
delta_age = next_age - age
delta_frac = next_frac - frac_ret
keep = numeric_age >= age
if i != (num_entries - 2):
keep &= numeric_age < next_age
frac[keep] += _frac_max[nutrient][s] * np.maximum(
0,
frac_ret
+ delta_frac * (numeric_age[keep] - age) / delta_age,
)
frac /= len(sex)
if len(frac) == 1 and isinstance(age, (str, int, float, np.integer)):
return frac[0]
return frac
_fractions = {
"loss": {"N": 0.04, "P": 0, "K": 0.05},
"feces": {"N": 0.17, "P": 0.43, "K": 0.1},
}
_frac_max = {
"N": {"male": 0.0308, "female": 0.0302},
"P": {"male": 0.0845, "female": 0.083},
"K": {"male": 0.0092, "female": 0.0085},
}
_frac_retention = {
"male": [1, 0.5, 0.6, 0.9, 1, 0.9, 0.35, 0.2],
"female": [1, 0.5, 0.6, 0.9, 1, 0.9, 0.3, 0.12],
}
_ages_retention = {
"male": [0, 2, 8.3, 11.6, 13.5, 15, 18, 20],
"female": [0, 2, 6.5, 9.5, 11.2, 12.6, 16, 20],
}
_composition = {
"male": {
"N": {
"start": 0.025,
"extremum": 0.029,
"end": 0.02,
"age_start": 10,
"age_extremum": 18,
"age_end": 85,
},
"P": {
"start": 0.00725,
"extremum": 0.00725,
"end": 0.0059,
"age_start": 5,
"age_extremum": 25,
"age_end": 55,
},
"K": {
"start": 0.00175,
"extremum": 0.0021,
"end": 0.00175,
"age_start": 10,
"age_extremum": 18,
"age_end": 85,
},
},
"female": {
"N": {
"start": 0.025,
"extremum": 0.024,
"end": 0.019,
"age_start": 10,
"age_extremum": 25,
"age_end": 70,
},
"P": {
"start": 0.00725,
"extremum": 0.00725,
"end": 0.0056,
"age_start": 5,
"age_extremum": 25,
"age_end": 55,
},
"K": {
"start": 0.00175,
"extremum": 0.00155,
"end": 0.00155,
"age_start": 10,
"age_extremum": 20,
"age_end": 85,
},
},
}