Source code for virtual_ecosystem.models.animal.functional_group

"""The `models.animal.functional_group` module contains a class that organizes
constants and rate equations used by AnimalCohorts in the
:mod:`~virtual_ecosystem.models.animal` module.
"""  # noqa: D205

from collections.abc import Iterable
from math import isnan
from pathlib import Path

import pandas as pd

from virtual_ecosystem.core.logger import LOGGER
from virtual_ecosystem.models.animal.animal_traits import (
    DevelopmentStatus,
    DevelopmentType,
    DietType,
    ExcretionType,
    MetabolicType,
    MigrationType,
    ReproductiveEnvironment,
    ReproductiveType,
    TaxaType,
    VerticalOccupancy,
)
from virtual_ecosystem.models.animal.model_config import AnimalConstants


[docs] class FunctionalGroup: """This is a class of animal functional groups. The goal of this class is to collect the correct constants and scaling relationships needed by an animal cohort such that they are accessed at initialization and stored in the AnimalCohort object as attributes. This should result in a system where an animal cohort can be auto-generated with a few keywords and numbers but that this procedure only need run once, at initialization, and that all further references to constants and scaling relationships are accessed through attributes of the AnimalCohort in question. """ def __init__( self, name: str, taxa: str, diet: str, metabolic_type: str, reproductive_environment: str, reproductive_type: str, development_type: str, development_status: str, offspring_functional_group: str, excretion_type: str, migration_type: str, vertical_occupancy: str, birth_mass: float, adult_mass: float, density_individuals_m2: float | None = None, t_opt: float | None = None, t_max_crit: float | None = None, t_min_crit: float | None = None, constants: AnimalConstants = AnimalConstants(), ) -> None: """The constructor for the FunctionalGroup class. TODO: Remove unused attributes. TODO: density test """ self.name = name """The name of the functional group.""" self.taxa = TaxaType(taxa) """The taxa of the functional group.""" self.diet = DietType.parse(diet) """The diet of the functional group.""" self.metabolic_type = MetabolicType(metabolic_type) """The metabolic type of the functional group.""" self.reproductive_environment = ReproductiveEnvironment( reproductive_environment ) """The reproductive environment used by the functional group.""" self.reproductive_type = ReproductiveType(reproductive_type) """The reproductive type of the functional group.""" self.development_type = DevelopmentType(development_type) """The development type of the functional group.""" self.development_status = DevelopmentStatus(development_status) """The development status of the functional group.""" self.offspring_functional_group = offspring_functional_group """The offspring type produced by this functional group in reproduction or metamorphosis.""" self.excretion_type = ExcretionType(excretion_type) """The excretion type of the functional group.""" self.migration_type = MigrationType(migration_type) """The migration type of the functional group.""" self.vertical_occupancy = VerticalOccupancy.parse(vertical_occupancy) """The vertical occupancy type of the functional group.""" self.birth_mass = birth_mass """The mass of the functional group at birth.""" self.adult_mass = adult_mass """The mass of the functional group at adulthood.""" self.density_individuals_m2 = density_individuals_m2 """Optional empirical density in individuals per m² for initialization.""" self.t_opt = _none_or_float(t_opt) """Optional optimal activity temperature for ectotherms [°C].""" self.t_max_crit = _none_or_float(t_max_crit) """Optional upper critical temperature for ectotherms [°C].""" self.t_min_crit = _none_or_float(t_min_crit) """Optional lower critical temperature for ectotherms [°C].""" self.constants = constants """Animal constants.""" self.broad_diet: DietType = self.diet.coarse_category() """The broad trophic category, herbivore, carnivore, omnivore.""" self.cnp_proportions = self.constants.cnp_proportion_terms[self.taxa] """The proportions of carbon/nitrogen/phosphorus in the functional group, example {"C": 0.8, "N": 0.15, "P": 0.05}.""" self.metabolic_rate_terms = self.constants.metabolic_rate_terms[ self.metabolic_type ] """The coefficient and exponent of metabolic rate.""" self.population_density_terms = self.constants.get_population_density_terms( self.taxa, self.broad_diet ) """The coefficient and exponent terms for the population density scaling.""" self.conversion_efficiency = self.constants.conversion_efficiency[ self.broad_diet ] """The conversion efficiency of the functional group based on diet.""" self.mechanical_efficiency = self.constants.mechanical_efficiency[ self.broad_diet ] """The mechanical transfer efficiency of a functional group based on diet.""" self.prey_scaling = self.constants.prey_mass_scaling_terms[self.metabolic_type][ self.taxa ] """The predator-prey mass ratio scaling relationship.""" # Taxonomic convenience flags self.is_invertebrate: bool = self.taxa == TaxaType("invertebrate") """Whether the functional group is an invertebrate.""" self.is_vertebrate: bool = self.taxa in { TaxaType("bird"), TaxaType("mammal"), TaxaType("amphibian"), TaxaType("reptile"), } """Whether the functional group is a vertebrate."""
[docs] def import_functional_groups( fg_csv_file: Path, constants: AnimalConstants ) -> list[FunctionalGroup]: """The function to import pre-defined functional groups. This function is a first-pass of how we might import pre-defined functional groups, the specific options of which can be found in functional_group.py. This allows a user to set out a basic outline of functional groups that accept our definitions of parameters and scaling relationships based on those traits. TODO: A structure for user-selection of which traits to employ. TODO: density test Args: fg_csv_file: The location of the csv file holding the functional group definitions. constants: An object providing animal model constants. Returns: A list of the FunctionalGroup instances created by the import. """ try: fg_data = pd.read_csv(fg_csv_file, na_values=["None"]) except FileNotFoundError: msg = f"Animal functional group definition file not found: {fg_csv_file!s}" LOGGER.error(msg) raise except pd.errors.ParserError: msg = f"Cannot parse animal functional group definition file: {fg_csv_file!s}" LOGGER.error(msg) raise required_headers = { "name", "taxa", "diet", "metabolic_type", "reproductive_environment", "reproductive_type", "development_type", "development_status", "offspring_functional_group", "excretion_type", "migration_type", "vertical_occupancy", "birth_mass", "adult_mass", } missing_headers = required_headers.difference(fg_data.columns) if missing_headers: raise ValueError( "Missing required headers in animal functional group definition file:" + ",".join(missing_headers) ) # Set individual densities if not provided if "density_individuals_m2" not in fg_data: fg_data["density_individuals_m2"] = None # Set thermal tolerance values if not provided for col in ("t_opt", "t_max_crit", "t_min_crit"): if col not in fg_data: fg_data[col] = None # Build the functional group list - ignore mypy moaning about unpacking column # headers from pandas: they are all strings functional_group_list: list[FunctionalGroup] = [ FunctionalGroup(constants=constants, **row) # type: ignore [misc] for row in fg_data.to_dict(orient="records") ] return functional_group_list
[docs] def get_functional_group_by_name( functional_groups: Iterable[FunctionalGroup], name: str ) -> FunctionalGroup: """Retrieve a FunctionalGroup by its name from a tuple of FunctionalGroup instances. Args: functional_groups: Tuple of FunctionalGroup instances. name: The name of the FunctionalGroup to retrieve. Returns: The FunctionalGroup instance with the matching name. Raises: ValueError: If no FunctionalGroup with the given name is found. """ for fg in functional_groups: if fg.name == name: return fg raise ValueError(f"No FunctionalGroup with name '{name}' found.")
def _none_or_float(value: float | None) -> float | None: """Convert NaN to None, passing through valid floats and None unchanged. Args: value: A float value or None, potentially NaN. Returns: None if the value is None or NaN, otherwise the original float. """ if value is None or (isinstance(value, float) and isnan(value)): return None return value