Source code for orgmatt.metabolism.metabolic_model

# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2022-2023 Tanguy Fardet
# SPDX-License-Identifier: GPL-3.0-or-later
# orgmatt/metabolism/metabolic_model.py

from typing import Literal, Optional

from .._utils.tools import _group_from_age
from ..typing import NumericArrayLike, NumericOrArray
from ..units import check_dim, ureg
from .corrections import correction_factor
from .nutrient_flows import (
    fraction_nutrient_excreta, fraction_nutrient_retention)


__all__ = ["MetabolicDataModel"]


_age_groups = {
    "infant": "infant",
    "toddler": "toddler",
    "kid": "kid",
    "teenager": "teenager",
    "young": "young|adult",
    "young adult": "young|adult",
    "young|adult": "young|adult",
    "adult": "adult",
    "senior": "senior"
}

_age_from_group = {
    "infant": "< 1",
    "toddler": "1-3",
    "kid": "4-9",
    "teenager": "10-17",
    "young|adult": "18-25",
    "adult": "26-59",
    "senior": ">= 60",
}

_available_diets = {None, "omnivorous", "vegetarian", "vegan"}

_available_sexes = {None, "male", "female"}


[docs] class MetabolicDataModel: """ Data-only metabolic model to account for the consumption and excretion of an individual characterized by an age (group), a sex, a diet, and a localization (country or world region). The model returns values present in the databases, potentially applying a correction to try to account for all requested characteristics. These corrections are computed, within the whole database, as the average contribution from the fields that were missing from the requested data. E.g. a Spanish vegan male teenager is requested but only "vegan male" is available in the database, so correction for "Spanish teenager" is computed, if this is also missing, correction for "Spanish" and "teenager" are applied separately. If one of the items is also missing, no correction is applied for this field and a warning is raised. .. note:: Complete consistency of all results is not guaranteed in this model, in particular, nutrient excretion in urine and feces may be greater than nutrient intake in some cases. """ def __init__( self, age_group: str, sex: Optional[str] = None, diet: Optional[str] = None, country: Optional[str] = None, region: Optional[str] = None ): ''' Initialize a metabolic model to estimate the intake and excretions of a person based on their age (group), sex, and diet. Parameters ---------- age_group : str Precise age or age group (among "infant", "toddler", "kid", "teenager", "young adult", "adult", "senior"). sex : str, optional (default: None) Sex of the modeled person. diet : str, optional (default: "omnivorous") Diet of the modeled person. country : str, optional (default: None) Country of the modeled person. region : str, optional (default: None) World region of the modeled person (among "Africa", "Asia", "Europe", "North America", "Oceania", "South America") ''' self._kwargs: dict[str, str] = {} if age_group in _age_groups: self._group = _age_groups[age_group] self._age = _age_from_group[self._group] else: # age was provided, get associated group self._group = _group_from_age(age_group) self._age = age_group self._kwargs["group"] = self._group assert sex in _available_sexes, \ f"Available options for `sex` are {_available_sexes}" assert diet in _available_diets, \ f"Available options for `diet` are {_available_diets}" self._sex = sex self._diet = diet or "ommnivorous" self._country = country self._region = region self._last_correction_status: int = 0 # store kwargs keys = ("sex", "diet", "country", "region") vals = (sex, diet, country, region) for k, v in zip(keys, vals): if v is not None: self._kwargs[k] = v @property def last_correction_status(self) -> int: ''' Number of corrections made to obtain the last returned value. This can take the following values: * 0 if the result was exact (directly present in the database) * -n < 0 if no correction could be provided for n separate fields * m > 0 if n individual corrections were combined to compute the final value ''' return self._last_correction_status @property def age(self) -> str: ''' Return the age of the modeled person ''' return self._age @property def group(self) -> str: ''' Return the age group of the modeled person ''' return self._group @property def diet(self) -> Optional[str]: ''' Return the diet of the modeled person ''' return self._diet @property def country(self) -> Optional[str]: ''' Return the country of the modeled person ''' return self._country @property def region(self) -> Optional[str]: ''' Return the region of the modeled person ''' return self._region
[docs] @check_dim(arglist=('duration', 1, '[time]'), result='[mass]*[X]') def nutrient_intake( self, duration: ureg.Quantity, nutrient: str, ci: int = 0, q: Optional[NumericArrayLike] = None, ignore_missing: bool = True ) -> ureg.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'). ci : int (optional, default: 0) Confidence interval (CI). If non-zero, the function will also return the low and high expected values corresponding to that CI. q : array-like of floats (optional: default: None) Percentiles to compute (supersedes `ci`). ignore_missing : bool, optional (default: True Whether to raise an error or not if the value was not available and could not be properly corrected. Returns ------- intake: quantity Intake of `nutrient` over `duration`. ''' from ..agrifood import nutrient_intake skipped: list[str] = [] res = nutrient_intake( 1, duration, nutrient, ci=ci, q=q, auto_correct=True, skipped_constraints=skipped, **self._kwargs) if skipped: # get correction factor missing = tuple( (k, v) for k, v in self._kwargs.items() if k in skipped ) filters = (("compound", nutrient),) correction, ncorr = correction_factor( "nutrients_intake", "intake", filters, missing, tuple(self._kwargs.items())) if correction == 1. and not ignore_missing: raise RuntimeError( f"Could not correct missing factors: {skipped}.") self._last_correction_status = ncorr return res * correction self._last_correction_status = 0 return res
[docs] @check_dim(arglist=('duration', 1, '[time]'), result='[mass]*[X]') def nutrient_excretion( self, duration: ureg.Quantity, nutrient: str, excreta: str = "all", ci: int = 0, q: Optional[NumericArrayLike] = None, ignore_missing: bool = True ) -> ureg.Quantity: ''' Return the amount of nutrient excreted in urine and feces. .. warning:: Computing excretion from urine and feces separately may lead to individual or combined excretions that are larger than the nutrient intake from food. Only ``excreta="all"`` is guaranteed to provide consistent results with the intake. Parameters ---------- duration : float [time] Time interval over which the nitrogen is excreted. nutrient : str Nutrient of interest among ('N', 'K', 'P', 'Mg', 'Ca'). excreta : str (optional, default: "all") Type of excreta ("urine", "feces", or "all"). ci : int (optional, default: 0) Confidence interval (CI). If non-zero, the function will also return the low and high expected values corresponding to that CI. q : array-like of floats (optional: default: None) Percentiles to compute (supersedes `ci`). ignore_missing : bool, optional (default: True Whether to raise an error or not if the value was not available and could not be properly corrected. Returns ------- excretion: Quantity [mass]*[nutrient] Amount of `nutrient` excreted over `duration`. ''' from ..nutrients import nutrient_from_population skipped: list[str] = [] if excreta == "all": factor = fraction_nutrient_excreta( nutrient, auto_correct=True, skipped_constraints=skipped, **self._kwargs) res = factor * self.nutrient_intake( duration, nutrient, ci=ci, q=q, ignore_missing=ignore_missing) else: res = nutrient_from_population( 1, duration, nutrient, excreta=excreta, ci=ci, q=q, auto_correct=True, skipped_constraints=skipped, **self._kwargs) if skipped: # get correction factor missing = tuple( (k, v) for k, v in self._kwargs.items() if k in skipped ) filters = [("compound", nutrient)] correction = 1. if excreta == "all": correction, ncorr = correction_factor( "nutrients_intake", "intake", tuple(filters), missing, tuple(self._kwargs.items())) else: filters.append(("flow", excreta)) correction, ncorr = correction_factor( "nutrients_flows_body", "amount", tuple(filters), missing, tuple(self._kwargs.items())) if excreta == "urine": # use correction furthest from 1 between flows and intake intake_corr, ncorr2 = correction_factor( "nutrients_intake", "intake", tuple(filters[:-1]), missing, tuple(self._kwargs.items())) if abs(1 - correction) < abs(1 - intake_corr): correction = intake_corr ncorr = ncorr2 if correction == 1. and not ignore_missing: raise RuntimeError( f"Could not correct missing factors: {skipped}.") self._last_correction_status = ncorr return res * correction self._last_correction_status = 0 return res
[docs] def flow_fraction( self, nutrient: str, flow: Literal["urine", "feces", "retention"], ci: int = 0, q: Optional[NumericArrayLike] = None, ignore_missing: bool = True ) -> 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. ci : int (optional, default: 0) Confidence interval (CI). If non-zero, the function will also return the low and high expected values corresponding to that CI. q : array-like of floats (optional: default: None) Percentiles to compute (supersedes `ci`). ''' func = fraction_nutrient_retention if flow == "retention" \ else fraction_nutrient_excreta kwargs = self._kwargs.copy() if flow != "retention": kwargs["excreta"] = flow skipped: list[str] = [] res = func(nutrient, ci=ci, q=q, **kwargs, auto_correct=True, skipped_constraints=skipped) if skipped: # get correction factor missing = tuple( (k, v) for k, v in self._kwargs.items() if k in skipped ) filters = (("compound", nutrient), ("flow", flow),) correction, ncorr = correction_factor( "nutrients_flows_body", "amount", tuple(filters), missing, tuple(self._kwargs.items())) if correction == 1. and not ignore_missing: raise RuntimeError( f"Could not correct missing factors: {skipped}.") self._last_correction_status = ncorr return res * correction self._last_correction_status = 0 return res
[docs] @check_dim(arglist=('duration', 1, '[time]'), result='[length]**3') def urine_excretion( self, duration: ureg.Quantity, water_intake: Optional[ureg.Quantity] = None ) -> ureg.Quantity: ''' Amount of urine excreted over `duration`. Parameters ---------- duration : float [time] Time interval over which urine is excreted. water_intake : float [length]**3, optional (default: inferred) Amount of water ingested over `duration` from all solids and liquids. ''' dwi = { "senior": 2.5*ureg.L, "adult": 2.5*ureg.L, "young|adult": 2.5*ureg.L, "teenager": 2*ureg.L, "infant": 0.6*ureg.L, "toddler": 1*ureg.L, "kid": 1.5*ureg.L, } if water_intake is None: water_intake = dwi[self._group] non_urine_loss = (0.1*ureg.L + dwi[self._group] / 3) self._last_correction_status = 0 return water_intake - duration/ureg.day*non_urine_loss
[docs] @check_dim(result='[mass]*[X]/[length]**3') def urine_concentration( self, nutrient: str, water_intake: Optional[ureg.Quantity] = None, ci: int = 0, q: Optional[NumericArrayLike] = None ) -> ureg.Quantity: ''' Average concentration of a nutrient in urine. Parameters ---------- nutrient : str Nutrient of interest among ('N', 'P', 'K'). water_intake : float [length]**3, optional (default: inferred) Daily amount of water ingested over `duration` from all solids and liquids. ci : int (optional, default: 0) Confidence interval (CI). If non-zero, the function will also return the low and high expected values corresponding to that CI. q : array-like of floats (optional: default: None) Percentiles to compute (supersedes `ci`). ''' n_excr = self.nutrient_excretion( 1*ureg.day, nutrient, excreta="urine", ci=ci, q=q) self._last_correction_status = 0 return n_excr / self.urine_excretion(1*ureg.day, water_intake)