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, }, }, }