diff --git a/docs/source/api/demography_api.md b/docs/source/api/demography_api.md index 1244feff..c327e76b 100644 --- a/docs/source/api/demography_api.md +++ b/docs/source/api/demography_api.md @@ -26,3 +26,19 @@ kernelspec: :autosummary: :members: ``` + +## The {mod}`~pyrealm.demography.community` module + +```{eval-rst} +.. automodule:: pyrealm.demography.community + :autosummary: + :members: +``` + +## The {mod}`~pyrealm.demography.t_model_functions` module + +```{eval-rst} +.. automodule:: pyrealm.demography.t_model_functions + :autosummary: + :members: +``` diff --git a/docs/source/conf.py b/docs/source/conf.py index 3921eba7..9c3a2970 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -96,6 +96,7 @@ class MyReferenceStyle(AuthorYearReferenceStyle): ("py:class", "numpy._typing._array_like._ScalarType_co"), ("py:class", "numpy._typing._generic_alias.ScalarType"), ("py:class", "numpy.float32"), + ("py:class", "numpy.int64"), ("py:class", "numpy.timedelta64"), ("py:class", "numpy.bool_"), ("py:class", "numpy.ndarray"), @@ -144,6 +145,7 @@ class MyReferenceStyle(AuthorYearReferenceStyle): "numpy.ndarray[typing.Any, numpy.dtype[+ScalarType]]]" ), ), + ("py:class", "pandas.core.frame.DataFrame"), ] # + @@ -152,7 +154,9 @@ class MyReferenceStyle(AuthorYearReferenceStyle): "numpy": ("https://numpy.org/doc/stable/", None), "python": ("https://docs.python.org/3/", None), "xarray": ("https://docs.xarray.dev/en/stable/", None), + "pandas": ("http://pandas.pydata.org/pandas-docs/dev/", None), "shapely": ("https://shapely.readthedocs.io/en/stable/", None), + "marshmallow": ("https://marshmallow.readthedocs.io/en/stable/", None), } # - diff --git a/poetry.lock b/poetry.lock index aca832f9..7481dbd5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2394,20 +2394,17 @@ xml = ["lxml (>=4.9.2)"] [[package]] name = "pandas-stubs" -version = "2.2.2.240603" +version = "2.2.2.240909" description = "Type annotations for pandas" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "pandas_stubs-2.2.2.240603-py3-none-any.whl", hash = "sha256:e08ce7f602a4da2bff5a67475ba881c39f2a4d4f7fccc1cba57c6f35a379c6c0"}, - {file = "pandas_stubs-2.2.2.240603.tar.gz", hash = "sha256:2dcc86e8fa6ea41535a4561c1f08b3942ba5267b464eff2e99caeee66f9e4cd1"}, + {file = "pandas_stubs-2.2.2.240909-py3-none-any.whl", hash = "sha256:e230f5fa4065f9417804f4d65cd98f86c002efcc07933e8abcd48c3fad9c30a2"}, + {file = "pandas_stubs-2.2.2.240909.tar.gz", hash = "sha256:3c0951a2c3e45e3475aed9d80b7147ae82f176b9e42e9fb321cfdebf3d411b3d"}, ] [package.dependencies] -numpy = [ - {version = ">=1.23.5", markers = "python_version >= \"3.9\" and python_version < \"3.12\""}, - {version = ">=1.26.0", markers = "python_version >= \"3.12\" and python_version < \"3.13\""}, -] +numpy = ">=1.23.5" types-pytz = ">=2022.1.1" [[package]] @@ -4034,4 +4031,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = ">=3.10" -content-hash = "aef31655540ecd90f7e317fe133563bcfa68b2dadbd711cb61389563fd57e555" +content-hash = "027e109eac2546ede7c68a77c56cf5a02a961c8893771d6a6d6545a1fbcd6f96" diff --git a/pyproject.toml b/pyproject.toml index a170c766..2823f7cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ tabulate = "^0.8.10" marshmallow = "^3.22.0" pandas = "^2.2.2" marshmallow-dataclass = "^8.7.0" +pandas-stubs = "^2.2.2.240909" [tool.poetry.group.types.dependencies] pandas-stubs = "^2.2.0.240218" types-tabulate = "^0.9.0.0" diff --git a/pyrealm/constants/tmodel_const.py b/pyrealm/constants/tmodel_const.py index 41dead73..a4233123 100644 --- a/pyrealm/constants/tmodel_const.py +++ b/pyrealm/constants/tmodel_const.py @@ -35,7 +35,7 @@ class TModelTraits(ConstantsClass): """Fine-root turnover time (:math:`\tau_r`, 1.04, years)""" par_ext: float = 0.5 # k, PAR extinction coefficient (-) """PAR extinction coefficient (:math:`k`, 0.5, -)""" - yld: float = 0.17 # y, Yield_factor (-) + yld: float = 0.6 # y, Yield_factor (-) """Yield_factor (:math:`y`, 0.17, -)""" zeta: float = 0.17 # zeta, Ratio of fine-root mass to foliage area (kgCm-2) r"""Ratio of fine-root mass to foliage area (:math:`\zeta`, 0.17, kg C m-2)""" diff --git a/pyrealm/demography/community.py b/pyrealm/demography/community.py new file mode 100644 index 00000000..7c8b1bc3 --- /dev/null +++ b/pyrealm/demography/community.py @@ -0,0 +1,615 @@ +"""This modules provides the Community class, which contains the set of size-structured +cohorts of plants across a range of plant functional types that occur a given location +(or 'cell') with a given cell id number and area. + +The class provides factory methods to create Community instances from CSV, JSON and TOML +files, using :mod:`marshmallow` schemas to both validate the input data and to perform +post processing to align the input formats to the initialisation arguments to the +Community class. + +Internally, the cohort data in the Community class is represented as a pandas dataframe, +which makes it possible to update cohort attributes in parallel across all cohorts but +also provide a clean interface for adding and removing cohorts to a Community. + +Worked example +============== + +The example code below demonstrates defining PFTs, creating a Flora collection, +initializing a Community, and computing ecological metrics using the T Model for a set +of plant cohorts. + +>>> import pandas as pd +>>> +>>> from pyrealm.demography.flora import PlantFunctionalType, Flora +>>> from pyrealm.demography.t_model_functions import ( +... calculate_heights, calculate_crown_areas, calculate_stem_masses, +... calculate_foliage_masses +... ) + +>>> pft1 = PlantFunctionalType( +... name="Evergreen Tree", +... a_hd=120.0, +... ca_ratio=380.0, +... h_max=30.0, +... rho_s=210.0, +... lai=3.0, +... sla=12.0, +... tau_f=5.0, +... tau_r=1.2, +... par_ext=0.6, +... yld=0.65, +... zeta=0.18, +... resp_r=0.95, +... resp_s=0.045, +... resp_f=0.12, +... m=2.5, +... n=4.5, +... ) + +>>> pft2 = PlantFunctionalType( +... name="Deciduous Shrub", +... a_hd=100.0, +... ca_ratio=350.0, +... h_max=4.0, +... rho_s=180.0, +... lai=2.0, +... sla=15.0, +... tau_f=3.0, +... tau_r=0.8, +... par_ext=0.4, +... yld=0.55, +... zeta=0.15, +... resp_r=0.85, +... resp_s=0.05, +... resp_f=0.1, +... m=3.0, +... n=5.0, +... ) + +Create a Flora collection: + +>>> flora = Flora([pft1, pft2]) + +Define community data as size-structured cohorts of given plant functional types with a +given number of individuals. + +>>> cohort_dbh_values = np.array([0.10, 0.03, 0.12, 0.025]) +>>> cohort_n_individuals = np.array([100, 200, 150, 180]) +>>> cohort_pft_names = np.array( +... ["Evergreen Tree", "Deciduous Shrub", "Evergreen Tree", "Deciduous Shrub"] +... ) + +Initialize a Community into an area of 1000 square meter with the given cohort data: + +>>> community = Community( +... cell_id=1, +... cell_area=1000.0, +... flora=flora, +... cohort_dbh_values=cohort_dbh_values, +... cohort_n_individuals=cohort_n_individuals, +... cohort_pft_names=cohort_pft_names +... ) + +Display the community's cohort data with calculated T Model predictions: + +>>> community.cohort_data[ +... ['name', 'dbh', 'n_individuals', 'height', 'crown_area', 'stem_mass'] +... ] + name dbh n_individuals height crown_area stem_mass +0 Evergreen Tree 0.100 100 9.890399 2.459835 8.156296 +1 Deciduous Shrub 0.030 200 2.110534 0.174049 0.134266 +2 Evergreen Tree 0.120 150 11.436498 3.413238 13.581094 +3 Deciduous Shrub 0.025 180 1.858954 0.127752 0.082126 +""" # noqa: D205 + +from __future__ import annotations + +import json +import sys +from dataclasses import InitVar, dataclass, field +from pathlib import Path +from typing import Any + +import numpy as np +import pandas as pd +from marshmallow import Schema, fields, post_load, validate, validates_schema +from marshmallow.exceptions import ValidationError +from numpy.typing import NDArray + +from pyrealm.core.utilities import check_input_shapes +from pyrealm.demography import t_model_functions as t_model +from pyrealm.demography.flora import Flora + +if sys.version_info[:2] >= (3, 11): + import tomllib + from tomllib import TOMLDecodeError +else: + import tomli as tomllib + from tomli import TOMLDecodeError + + +class CohortSchema(Schema): + """A validation schema for Cohort data objects. + + This schema can be used to validate the ``cohorts`` components of JSON and TOML + community data files, which are simple dictionaries: + + .. code-block:: toml + :caption: TOML + + dbh_value = 0.2 + n_individuals = 6 + pft_name = "broadleaf" + + .. code-block:: json + :caption: JSON + + { + "pft_name": "broadleaf", + "dbh_value": 0.2, + "n_individuals": 6 + } + """ + + dbh_value = fields.Float( + required=True, validate=validate.Range(min=0, min_inclusive=False) + ) + n_individuals = fields.Integer( + strict=True, required=True, validate=validate.Range(min=0, min_inclusive=False) + ) + pft_name = fields.Str(required=True) + + +class CommunityStructuredDataSchema(Schema): + """A validation schema for Cohort data in a structured format (JSON/TOML). + + This schema can be used to validate data for creating a Community instance stored in + a structured format such as JSON or TOML. The format is expected to provide a cell + area and id along with an array of cohort objects providing the plant functional + type name, diameter at breast height (DBH) and number of individuals (see + :class:`~pyrealm.demography.community.CohortSchema`). Example inputs with this + structure are: + + .. code-block:: toml + :caption: TOML + + cell_area = 100 + cell_id = 1 + + [[cohorts]] + dbh_value = 0.2 + n_individuals = 6 + pft_name = "broadleaf" + + [[cohorts]] + dbh_value = 0.25 + n_individuals = 6 + pft_name = "conifer" + + .. code-block:: json + :caption: JSON + + { + "cell_id": 1, + "cell_area": 100, + "cohorts": [ + { + "pft_name": "broadleaf", + "dbh_value": 0.2, + "n_individuals": 6 + }, + { + "pft_name": "broadleaf", + "dbh_value": 0.25, + "n_individuals": 6 + }] + } + + Any data validated with this schema is post-processed to convert the cohort objects + into the arrays of cohort data required to initialise instances of the + :class:`~pyrealm.demography.community.Community` class. + """ + + cell_id = fields.Integer(required=True, strict=True, validate=validate.Range(min=0)) + cell_area = fields.Float( + required=True, validate=validate.Range(min=0, min_inclusive=False) + ) + cohorts = fields.List( + fields.Nested(CohortSchema), required=True, validate=validate.Length(min=1) + ) + + @post_load + def cohort_objects_to_arrays(self, data: dict, **kwargs: Any) -> dict: + """Convert cohorts to arrays. + + This post load method converts the cohort objects into arrays, which is the + format used to initialise a Community object. + + Args: + data: Data passed to the validator + kwargs: Additional keyword arguments passed by marshmallow + """ + + data["cohort_dbh_values"] = np.array([c["dbh_value"] for c in data["cohorts"]]) + data["cohort_n_individuals"] = np.array( + [c["n_individuals"] for c in data["cohorts"]] + ) + data["cohort_pft_names"] = np.array([c["pft_name"] for c in data["cohorts"]]) + + del data["cohorts"] + + return data + + +class CommunityCSVDataSchema(Schema): + """A validation schema for community initialisation data in CSV format. + + This schema can be used to validate data for creating a Community instance stored in + CSV format. The file is expected to provide fields providing cell id and cell area + and then functional type name, diameter at breast height (DBH) and number of + individuals. Each row is taken to represent a cohort and the cell id and area + *must** be consistent across rows. + + .. code-block:: + + cell_id,cell_area,cohort_pft_names,cohort_dbh_values,cohort_n_individuals + 1,100,broadleaf,0.2,6 + 1,100,broadleaf,0.25,6 + 1,100,broadleaf,0.3,3 + 1,100,broadleaf,0.35,1 + 1,100,conifer,0.5,1 + 1,100,conifer,0.6,1 + + The input data is expected to be provided to this schema as a dictionary of lists of + field values keyed by field name, as for example by using + :meth:`pandas.DataFrame.to_dict` with the ``orient='list'`` argument. + + The schema automatically validates that the cell id and area are consistent and then + post-processing is used to simplify those fields to the scalar inputs required to + initialise instances of the :class:`~pyrealm.demography.community.Community` class + and to convert the cohort data into arrays, + """ + + cell_id = fields.List(fields.Integer(strict=True), required=True) + cell_area = fields.List(fields.Float(), required=True) + cohort_dbh_values = fields.List(fields.Float(), required=True) + cohort_n_individuals = fields.List(fields.Integer(strict=True), required=True) + cohort_pft_names = fields.List(fields.Str(), required=True) + + @validates_schema + def validate_consistent_cell_data(self, data: dict, **kwargs: Any) -> None: + """Schema wide validation. + + Args: + data: Data passed to the validator + kwargs: Additional keyword arguments passed by marshmallow + """ + + # Check cell area and cell id consistent + if not all([c == data["cell_id"][0] for c in data["cell_id"]]): + raise ValueError( + "Multiple cell id values fields in community data, see load_communities" + ) + + if not all([c == data["cell_area"][0] for c in data["cell_area"]]): + raise ValueError("Cell area varies in community data") + + @post_load + def make_cell_data_scalar(self, data: dict, **kwargs: Any) -> dict: + """Make cell data scalar. + + This post load method reduces the repeated cell id and cell area across CSV data + rows into the scalar inputs required to initialise a Community object. + """ + + data["cell_id"] = data["cell_id"][0] + data["cell_area"] = data["cell_area"][0] + + data["cohort_dbh_values"] = np.array(data["cohort_dbh_values"]) + data["cohort_n_individuals"] = np.array(data["cohort_n_individuals"]) + data["cohort_pft_names"] = np.array(data["cohort_pft_names"]) + + return data + + +@dataclass +class Community: + """The plant community class. + + A community is a set of size-structured plant cohorts in a given location, where the + location has a specified numeric id and a known area in square meters. + + A cohort defines a number of individual plants with the same diameter at breast + height (DBH) and plant functional type (PFT). Internally, the cohort data is built + into a :class:`pandas.DataFrame` with each row representing a cohort and each column + representing a property of the cohort. The initial input data is extended to include + the plant functional type traits for each cohort (see + :class:`~pyrealm.demography.flora.PlantFunctionalType`) and then is further extended + to include the geometric and canopy predictions of the T Model for each cohort. + + Factory methods are provided to load community data from csv, TOML or JSON files. + + Args: + cell_id: An positive integer id for the community location. + cell_area: An area in square metres for the community location. + flora: A flora object containing the plant functional types for the community + cohort_dbh_values: A numpy array giving the diameter at breast height in metres + for each cohort. + cohort_n_individuals: A numpy array giving the number of individuals in each + cohort. + cohort_pft_names: A numpy array giving the name of the plant functional type in + each cohort. + """ + + # Dataclass attributes for initialisation + # - community wide properties + cell_id: int + cell_area: float + flora: Flora + + # - arrays representing properties of cohorts + cohort_dbh_values: InitVar[NDArray[np.float32]] + cohort_n_individuals: InitVar[NDArray[np.int_]] + cohort_pft_names: InitVar[NDArray[np.str_]] + + # Post init properties + number_of_cohorts: int = field(init=False) + + # Dataframe of cohort data + cohort_data: pd.DataFrame = field(init=False) + + def __post_init__( + self, + cohort_dbh_values: NDArray[np.float32], + cohort_n_individuals: NDArray[np.int_], + cohort_pft_names: NDArray[np.str_], + ) -> None: + """Validate inputs and populate derived community attributes. + + The ``__post_init__`` builds a pandas dataframe of PFT values and T model + predictions across the validated initial cohort data. + """ + + # Check cell area and cell id + if not (isinstance(self.cell_area, float | int) and self.cell_area > 0): + raise ValueError("Community cell area must be a positive number.") + + if not (isinstance(self.cell_id, int) and self.cell_id >= 0): + raise ValueError("Community cell id must be a integer >= 0.") + + # Check cohort data types + if not ( + isinstance(cohort_dbh_values, np.ndarray) + and isinstance(cohort_n_individuals, np.ndarray) + and isinstance(cohort_pft_names, np.ndarray) + ): + raise ValueError("Cohort data not passed as numpy arrays.") + + # Check the cohort inputs are of equal length + try: + check_input_shapes( + cohort_dbh_values, cohort_n_individuals, cohort_dbh_values + ) + except ValueError: + raise ValueError("Cohort arrays are of unequal length") + + # Check the initial PFT values are known + unknown_pfts = set(cohort_pft_names).difference(self.flora.keys()) + + if unknown_pfts: + raise ValueError( + f"Plant functional types unknown in flora: {','.join(unknown_pfts)}" + ) + + # Convert to a dataframe + cohort_data = pd.DataFrame( + { + "name": cohort_pft_names, + "dbh": cohort_dbh_values, + "n_individuals": cohort_n_individuals, + } + ) + # Broadcast the pft trait data to the cohort data by merging with the flora data + # and then store as the cohort data attribute + self.cohort_data = pd.merge(cohort_data, self.flora.data) + self.number_of_cohorts = self.cohort_data.shape[0] + + # Populate the T model fields + self._calculate_t_model() + + def _calculate_t_model(self) -> None: + """Calculate T Model predictions across cohort data. + + This method populates or updates the community attributes predicted by the T + Model :cite:`Li:2014bc` and by the canopy shape extensions to the T Model + implemented in PlantFate :cite:`joshi:2022a`. + """ + + # Add data to cohort dataframes capturing the T Model geometry + # - Classic T Model scaling + self.cohort_data["height"] = t_model.calculate_heights( + h_max=self.cohort_data["h_max"], + a_hd=self.cohort_data["a_hd"], + dbh=self.cohort_data["dbh"], + ) + + self.cohort_data["crown_area"] = t_model.calculate_crown_areas( + ca_ratio=self.cohort_data["ca_ratio"], + a_hd=self.cohort_data["a_hd"], + dbh=self.cohort_data["dbh"], + height=self.cohort_data["height"], + ) + + self.cohort_data["crown_fraction"] = t_model.calculate_crown_fractions( + a_hd=self.cohort_data["a_hd"], + dbh=self.cohort_data["dbh"], + height=self.cohort_data["height"], + ) + + self.cohort_data["stem_mass"] = t_model.calculate_stem_masses( + rho_s=self.cohort_data["rho_s"], + dbh=self.cohort_data["dbh"], + height=self.cohort_data["height"], + ) + + self.cohort_data["foliage_mass"] = t_model.calculate_foliage_masses( + sla=self.cohort_data["sla"], + lai=self.cohort_data["lai"], + crown_area=self.cohort_data["crown_area"], + ) + + self.cohort_data["sapwood_mass"] = t_model.calculate_sapwood_masses( + rho_s=self.cohort_data["rho_s"], + ca_ratio=self.cohort_data["ca_ratio"], + height=self.cohort_data["height"], + crown_area=self.cohort_data["crown_area"], + crown_fraction=self.cohort_data["crown_fraction"], + ) + + # Canopy shape extension to T Model from PlantFATE + self.cohort_data["canopy_z_max"] = t_model.calculate_canopy_z_max( + z_max_prop=self.cohort_data["z_max_prop"], + height=self.cohort_data["height"], + ) + self.cohort_data["canopy_r0"] = t_model.calculate_canopy_r0( + q_m=self.cohort_data["q_m"], + crown_area=self.cohort_data["crown_area"], + ) + + @classmethod + def from_csv(cls, path: Path, flora: Flora) -> Community: + """Create a Community object from a CSV file. + + This factory method checks that the required fields are present in the CSV data + and that the cell_id and cell_area values are constant. It then passes the data + through further validation using the + `meth`:`~pyrealm.demography.community.Community._from_file_data` method and + returns a Community instance. + + Args: + path: A path to a CSV file of community data + flora: A Flora instance providing plant functional types used in the + community data + """ + + # Load the data + try: + file_data = pd.read_csv(path) + except (FileNotFoundError, pd.errors.ParserError) as excep: + raise excep + + # Validate the data - there is an inconsequential typing issue here: + # Argument "data" to "load" of "Schema" has incompatible type + # "dict[Hashable, Any]"; + # expected "Mapping[str, Any] | Iterable[Mapping[str, Any]]" + try: + file_data = CommunityCSVDataSchema().load(data=file_data.to_dict("list")) # type: ignore[arg-type] + except ValidationError as excep: + raise excep + + return cls(**file_data, flora=flora) + + @classmethod + def from_json(cls, path: Path, flora: Flora) -> Community: + """Create a Community object from a JSON file. + + This factory method loads community data from a JSON community file and + validates it using + `class`:`~pyrealm.demography.community.CommunityStructuredDataSchema` before + using the data to initialise a Community instance. + + Args: + path: A path to a JSON file of community data + flora: A Flora instance providing plant functional types used in the + community data + """ + + # Load the data + try: + file_data = json.load(open(path)) + except (FileNotFoundError, json.JSONDecodeError) as excep: + raise excep + + # Validate the data + try: + file_data = CommunityStructuredDataSchema().load(data=file_data) + except ValidationError as excep: + raise excep + + return cls(**file_data, flora=flora) + + @classmethod + def from_toml(cls, path: Path, flora: Flora) -> Community: + """Create a Community object from a TOML file. + + This factory method loads community data from a TOML community file and + validates it using + `class`:`~pyrealm.demography.community.CommunityStructuredDataSchema` before + using the data to initialise a Community instance. + + Args: + path: A path to a TOML file of community data + flora: A Flora instance providing plant functional types used in the + community data + """ + + # Load the data + try: + file_data = tomllib.load(open(path, "rb")) + except (FileNotFoundError, TOMLDecodeError) as excep: + raise excep + + # Validate the data + try: + file_data = CommunityStructuredDataSchema().load(data=file_data) + except ValidationError as excep: + raise excep + + return cls(**file_data, flora=flora) + + # @classmethod + # def load_communities_from_csv( + # cls, cell_area: float, csv_path: str, flora: Flora + # ) -> list[Community]: + # """Loads a list of communities from a csv provided in the appropriate format. + + # The csv should contain the following columns: cell_id, + # diameter_at_breast_height, plant_functional_type, number_of_individuals. Each + # row in the csv should represent one cohort. + + # :param cell_area: the area of the cell at each location, this is assumed to be + # the same across all the locations in the csv. + # :param csv_path: path to the csv containing community data, as detailed above. + # :param flora: a flora object, ie a dictionary of plant functional properties, + # keyed by pft name. + # :return: a list of community objects, loaded from the csv + # file. + # """ + # community_data = pd.read_csv(csv_path) + + # data_grouped_by_community = community_data.groupby(community_data.cell_id) + + # communities = [] + + # for cell_id in data_grouped_by_community.groups: + # community_dataframe = data_grouped_by_community.get_group(cell_id) + # dbh_values = community_dataframe["diameter_at_breast_height"].to_numpy( + # dtype=np.float32 + # ) + # number_of_individuals = community_dataframe[ + # "number_of_individuals" + # ].to_numpy(dtype=np.int_) + # pft_names = community_dataframe["plant_functional_type"].to_numpy( + # dtype=str + # ) + # community_object = Community( + # cell_id, # type:ignore + # cell_area, + # dbh_values, + # number_of_individuals, + # pft_names, + # flora, + # ) + # communities.append(community_object) + + # return communities diff --git a/pyrealm/demography/flora.py b/pyrealm/demography/flora.py index 6c754a7d..57b72100 100644 --- a/pyrealm/demography/flora.py +++ b/pyrealm/demography/flora.py @@ -4,10 +4,11 @@ used to parameterise the traits of different plant functional types. The ``PlantFunctionalType`` dataclass is a subclass of ``PlantFunctionalTypeStrict`` that simply adds default values to the attributes. -* The ``PlantFunctionalTypeStrict`` dataclass is used as the basis of a ``marshmallow`` - schema for validating the creation of plant functional types from data files. This - intentionally enforces a complete description of the traits in the input data. The - ``PlantFunctionalType`` is provided as a more convenient API for programmatic use. +* The ``PlantFunctionalTypeStrict`` dataclass is used as the basis of a + :mod:`~marshmallow` schema for validating the creation of plant functional types from + data files. This intentionally enforces a complete description of the traits in the + input data. The ``PlantFunctionalType`` is provided as a more convenient API for + programmatic use. * The Flora class, which is simply a dictionary of named plant functional types for use in describing a plant community in a simulation. The Flora class also defines factory methods to create instances from plant functional type data stored in JSON, TOML or @@ -20,13 +21,19 @@ import sys from collections import Counter from collections.abc import Sequence -from dataclasses import dataclass, field +from dataclasses import dataclass, field, fields from pathlib import Path import marshmallow_dataclass +import numpy as np import pandas as pd from marshmallow.exceptions import ValidationError +from pyrealm.demography.t_model_functions import ( + calculate_canopy_q_m, + calculate_canopy_z_max_proportion, +) + if sys.version_info[:2] >= (3, 11): import tomllib from tomllib import TOMLDecodeError @@ -108,9 +115,9 @@ def __post_init__(self) -> None: # Calculate q_m and z_max proportion. Need to use __setattr__ because the # dataclass is frozen. - object.__setattr__(self, "q_m", calculate_q_m(m=self.m, n=self.n)) + object.__setattr__(self, "q_m", calculate_canopy_q_m(m=self.m, n=self.n)) object.__setattr__( - self, "z_max_prop", calculate_z_max_proportion(m=self.m, n=self.n) + self, "z_max_prop", calculate_canopy_z_max_proportion(m=self.m, n=self.n) ) @@ -140,7 +147,7 @@ class PlantFunctionalType(PlantFunctionalTypeStrict): tau_f, 4.0, years tau_r, 1.04, years par_ext, 0.5, - - yld, 0.17, - + yld, 0.6, - zeta, 0.17, kg C m-2 resp_r, 0.913, year-1 resp_s, 0.044, year-1 @@ -158,7 +165,7 @@ class PlantFunctionalType(PlantFunctionalTypeStrict): tau_f: float = 4.0 tau_r: float = 1.04 par_ext: float = 0.5 - yld: float = 0.17 + yld: float = 0.6 zeta: float = 0.17 resp_r: float = 0.913 resp_s: float = 0.044 @@ -178,38 +185,6 @@ class PlantFunctionalType(PlantFunctionalTypeStrict): """ -def calculate_q_m(m: float, n: float) -> float: - """Calculate a q_m value. - - The value of q_m is a constant canopy scaling parameter derived from the ``m`` and - ``n`` attributes defined for a plant functional type. - - Args: - m: Canopy shape parameter - n: Canopy shape parameter - """ - return ( - m - * n - * ((n - 1) / (m * n - 1)) ** (1 - 1 / n) - * (((m - 1) * n) / (m * n - 1)) ** (m - 1) - ) - - -def calculate_z_max_proportion(m: float, n: float) -> float: - """Calculate the z_m proportion. - - The z_m proportion is the constant proportion of stem height at which the maximum - crown radius is found for a given plant functional type. - - Args: - m: Canopy shape parameter - n: Canopy shape parameter - """ - - return ((n - 1) / (m * n - 1)) ** (1 / n) - - class Flora(dict[str, type[PlantFunctionalTypeStrict]]): """Defines the flora used in a ``virtual_ecosystem`` model. @@ -252,6 +227,23 @@ def __init__(self, pfts: Sequence[type[PlantFunctionalTypeStrict]]) -> None: for name, pft in zip(pft_names, pfts): self[name] = pft + # Generate an dataframe representation to facilitate merging to cohort data. + # - assemble pft fields into arrays + data = {} + pft_fields = [f.name for f in fields(PlantFunctionalTypeStrict)] + + for pft_field in pft_fields: + data[pft_field] = np.array( + [getattr(pft, pft_field) for pft in self.values()] + ) + + self.data: pd.DataFrame = pd.DataFrame(data) + """A dataframe of trait values as numpy arrays. + + The 'name' column can be used with cohort names to broadcast plant functional + type data out to cohorts. + """ + @classmethod def _from_file_data(cls, file_data: dict) -> Flora: """Create a Flora object from a JSON string. diff --git a/pyrealm/demography/t_model_functions.py b/pyrealm/demography/t_model_functions.py new file mode 100644 index 00000000..9c2fd903 --- /dev/null +++ b/pyrealm/demography/t_model_functions.py @@ -0,0 +1,259 @@ +"""The ``t_model`` module provides the basic scaling relationships of the T Model +:cite:`Li:2014bc`:. This provides scaling relationships using the plant functional type +traits defined in the :mod:`~pyrealm.demography.flora` module and the diameter at breast +height of individual stems to define the stem geometry, masses, respiration and hence +the calculate stem growth given net primary productivity. +""" # noqa: D205 + +import numpy as np +from pandas import Series + + +def calculate_heights(h_max: Series, a_hd: Series, dbh: Series) -> Series: + r"""Calculate tree height under the T Model. + + The height of trees (:math:`H`) are calculated from individual diameters at breast + height (:math:`D`), along with the maximum height (:math:`H_{m}`) and initial slope + of the height/diameter relationship (:math:`a`) of the plant functional types + :cite:p:`{Equation 4, }Li:2014bc`: + + .. math:: + + H = H_{m} \left(1 - \exp(-a \cdot D / H_{m})\right) + + Args: + h_max: Maximum height of the PFT + a_hd: Initial slope of the height/diameter relationship of the PFT + dbh: Diameter at breast height of individuals + """ + + return h_max * (1 - np.exp(-a_hd * dbh / h_max)) + + +def calculate_crown_areas( + ca_ratio: Series, a_hd: Series, dbh: Series, height: Series +) -> Series: + r"""Calculate tree crown area under the T Model. + + The tree crown area (:math:`A_{c}`)is calculated from individual diameters at breast + height (:math:`D`) and stem height (:math:`H`), along with the crown area ratio + (:math:`c`)and the initial slope of the height/diameter relationship (:math:`a`) of + the plant functional type :cite:p:`{Equation 8, }Li:2014bc`: + + .. math:: + + A_{c} =\frac{\pi c}{4 a} D H + + + Args: + ca_ratio: Crown area ratio of the PFT + a_hd: Initial slope of the height/diameter relationship of the PFT + dbh: Diameter at breast height of individuals + height: Stem height of individuals + """ + + return ((np.pi * ca_ratio) / (4 * a_hd)) * dbh * height + + +def calculate_crown_fractions(a_hd: Series, height: Series, dbh: Series) -> Series: + r"""Calculate tree crown fraction under the T Model. + + The crown fraction (:math:`f_{c}`)is calculated from individual diameters at breast + height and stem height (:math:`D`), along with the initial slope of the height / + diameter relationship (:math:`a`) of the plant functional type + :cite:p:`{Equation 11, }Li:2014bc`: + + .. math:: + + \frac{H}{a D} + + Args: + a_hd: Initial slope of the height/diameter relationship of the PFT + dbh: Diameter at breast height of individuals + height: Stem height of individuals + """ + + return height / (a_hd * dbh) + + +def calculate_stem_masses(rho_s: Series, height: Series, dbh: Series) -> Series: + r"""Calculate stem mass under the T Model. + + The stem mass (:math:`W_{s}`) is calculated from individual diameters at breast + height (:math:`D`) and stem height (:math:`H`), along with the wood density + (:math:`\rho_s`)of the plant functional type :cite:p:`{Equation 6, }Li:2014bc`: + + .. math:: + + W_s = (\pi / 8) \rho_s D^2 H + + Args: + rho_s: Wood density of the PFT + dbh: Diameter at breast height of individuals + height: Stem height of individuals + """ + + return (np.pi / 8) * rho_s * (dbh**2) * height + + +def calculate_foliage_masses(sla: Series, lai: Series, crown_area: Series) -> Series: + r"""Calculate foliage mass under the T Model. + + The foliage mass (:math:`W_{f}`) is calculated from the crown area (:math:`A_{c}`), + along with the specific leaf area (:math:`\sigma`) and leaf area index (:math:`L`) + of the plant functional type :cite:p:`Li:2014bc`: + + .. math:: + + W_f = (1 / \sigma) A_c L + + Args: + sla: Specific leaf area of the PFT + lai: Leaf area index of the PFT + crown_area: Crown area of individuals + """ + + return crown_area * lai * (1 / sla) + + +def calculate_sapwood_masses( + rho_s: Series, + ca_ratio: Series, + height: Series, + crown_area: Series, + crown_fraction: Series, +) -> Series: + r"""Calculate sapwood mass under the T Model. + + The sapwood mass (:math:`W_{\cdot s}`) is calculated from the individual crown area + (:math:`A_{c}`), height :math:`H` and canopy fraction (:math:`f_{c}`) along with the + wood density (:math:`\rho_s`) and crown area ratio :math:`A_{c}` of the plant + functional type :cite:p:`{Equation 14, }Li:2014bc`: + + .. math:: + + W_{\cdot s} = \frac{A_c \rho_s H (1 - f_c / 2)}{c} + + Args: + rho_s: Wood density of the PFT + ca_ratio: Crown area ratio of the PFT + height: Stem height of individuals + crown_area: Crown area of individuals + crown_fraction: Crown fraction of individuals + """ + + return crown_area * rho_s * height * (1 - crown_fraction / 2) / ca_ratio + + +def calculate_canopy_q_m(m: float, n: float) -> float: + """Calculate a q_m value. + + The value of q_m is a constant canopy scaling parameter derived from the ``m`` and + ``n`` attributes defined for a plant functional type. + + Args: + m: Canopy shape parameter + n: Canopy shape parameter + """ + return ( + m + * n + * ((n - 1) / (m * n - 1)) ** (1 - 1 / n) + * (((m - 1) * n) / (m * n - 1)) ** (m - 1) + ) + + +def calculate_canopy_z_max_proportion(m: float, n: float) -> float: + r"""Calculate the z_m proportion. + + The z_m proportion (:math:`p_{zm}`) is the constant proportion of stem height at + which the maximum crown radius is found for a given plant functional type. + + .. math:: + + p_{zm} = \left(\dfrac{n-1}{m n -1}\right)^ {\tfrac{1}{n}} + + Args: + m: Canopy shape parameter + n: Canopy shape parameter + """ + + return ((n - 1) / (m * n - 1)) ** (1 / n) + + +def calculate_canopy_z_max(z_max_prop: Series, height: Series) -> Series: + r"""Calculate height of maximum crown radius. + + The height of the maximum crown radius (:math:`z_m`) is derived from the canopy + shape parameters (:math:`m,n`) and the resulting fixed proportion (:math:`p_{zm}`) + for plant functional types. These shape parameters are defined as part of the + extension of the T Model presented by :cite:t:`joshi:2022a`. + + The value :math:`z_m` is the height above ground where the largest canopy radius is + found, given the proportion and the estimated stem height (:math:`H`) of + individuals. + + .. math:: + + z_m = p_{zm} H + + Args: + z_max_prop: Canopy shape parameter of the PFT + height: Crown area of individuals + """ + """Calculate z_m, the height of maximum crown radius.""" + + return height * z_max_prop + + +def calculate_canopy_r0(q_m: Series, crown_area: Series) -> Series: + r"""Calculate scaling factor for height of maximum crown radius. + + This scaling factor (:math:`r_0`) is derived from the canopy shape parameters + (:math:`m,n,q_m`) for plant functional types and the estimated crown area + (:math:`A_c`) of individuals. The shape parameters are defined as part of the + extension of the T Model presented by :cite:t:`joshi:2022a` and :math:`r_0` is used + to scale the crown area such that the crown area at the maximum crown radius fits + the expectations of the T Model. + + .. math:: + + r_0 = 1/q_m \sqrt{A_c / \pi} + + Args: + q_m: Canopy shape parameter of the PFT + crown_area: Crown area of individuals + """ + # Scaling factor to give expected A_c (crown area) at + # z_m (height of maximum crown radius) + + return 1 / q_m * np.sqrt(crown_area / np.pi) + + +def calculate_relative_canopy_radii( + z: float, + height: Series, + m: Series, + n: Series, +) -> Series: + r"""Calculate relative canopy radius at a given height. + + The canopy shape parameters ``m`` and ``n`` define the vertical distribution of + canopy along the stem. For a stem of a given total height, this function calculates + the relative canopy radius at a given height :math:`z`: + + .. math:: + + q(z) = m n \left(\dfrac{z}{H}\right) ^ {n -1} + \left( 1 - \left(\dfrac{z}{H}\right) ^ n \right)^{m-1} + + Args: + z: Height at which to calculate relative radius + height: Total height of individual stem + m: Canopy shape parameter of PFT + n: Canopy shape parameter of PFT + """ + + z_over_height = z / height + + return m * n * z_over_height ** (n - 1) * (1 - z_over_height**n) ** (m - 1) diff --git a/pyrealm_build_data/community/communities.csv b/pyrealm_build_data/community/communities.csv new file mode 100644 index 00000000..ee97c938 --- /dev/null +++ b/pyrealm_build_data/community/communities.csv @@ -0,0 +1,13 @@ +cell_id,cell_area,plant_functional_type,diameter_at_breast_height,number_of_individuals +1,100,test1,0.2,6 +1,100,test1,0.25,6 +1,100,test1,0.3,3 +1,100,test1,0.35,1 +1,100,test2,0.5,1 +1,100,test2,0.6,1 +2,100,test1,0.2,6 +2,100,test1,0.25,6 +2,100,test1,0.3,3 +2,100,test1,0.35,1 +2,100,test2,0.5,1 +2,100,test2,0.6,1 \ No newline at end of file diff --git a/pyrealm_build_data/community/communities.json b/pyrealm_build_data/community/communities.json new file mode 100644 index 00000000..f20baefe --- /dev/null +++ b/pyrealm_build_data/community/communities.json @@ -0,0 +1,76 @@ +{ + "cells": [ + { + "cell_id": 1, + "cell_area": 100, + "cohorts": [ + { + "pft_name": "test1", + "dbh": 0.2, + "number_of_members": 6 + }, + { + "pft_name": "test1", + "dbh": 0.25, + "number_of_members": 6 + }, + { + "pft_name": "test1", + "dbh": 0.3, + "number_of_members": 3 + }, + { + "pft_name": "test1", + "dbh": 0.35, + "number_of_members": 1 + }, + { + "pft_name": "test2", + "dbh": 0.5, + "number_of_members": 1 + }, + { + "pft_name": "test2", + "dbh": 0.6, + "number_of_members": 1 + } + ] + }, + { + "cell_id": 1, + "cell_area": 100, + "cohorts": [ + { + "pft_name": "test1", + "dbh": 0.2, + "number_of_members": 6 + }, + { + "pft_name": "test1", + "dbh": 0.25, + "number_of_members": 6 + }, + { + "pft_name": "test1", + "dbh": 0.3, + "number_of_members": 3 + }, + { + "pft_name": "test1", + "dbh": 0.35, + "number_of_members": 1 + }, + { + "pft_name": "test2", + "dbh": 0.5, + "number_of_members": 1 + }, + { + "pft_name": "test2", + "dbh": 0.6, + "number_of_members": 1 + } + ] + } + ] +} \ No newline at end of file diff --git a/pyrealm_build_data/community/communities.toml b/pyrealm_build_data/community/communities.toml new file mode 100644 index 00000000..d7e29caa --- /dev/null +++ b/pyrealm_build_data/community/communities.toml @@ -0,0 +1,67 @@ +[[cells]] +cell_area = 100 +cell_id = 1 + +[[cells.cohorts]] +dbh = 0.2 +number_of_members = 6 +pft_name = "test1" + +[[cells.cohorts]] +dbh = 0.25 +number_of_members = 6 +pft_name = "test1" + +[[cells.cohorts]] +dbh = 0.3 +number_of_members = 3 +pft_name = "test1" + +[[cells.cohorts]] +dbh = 0.35 +number_of_members = 1 +pft_name = "test1" + +[[cells.cohorts]] +dbh = 0.5 +number_of_members = 1 +pft_name = "test2" + +[[cells.cohorts]] +dbh = 0.6 +number_of_members = 1 +pft_name = "test2" + +[[cells]] +cell_area = 100 +cell_id = 1 + +[[cells.cohorts]] +dbh = 0.2 +number_of_members = 6 +pft_name = "test1" + +[[cells.cohorts]] +dbh = 0.25 +number_of_members = 6 +pft_name = "test1" + +[[cells.cohorts]] +dbh = 0.3 +number_of_members = 3 +pft_name = "test1" + +[[cells.cohorts]] +dbh = 0.35 +number_of_members = 1 +pft_name = "test1" + +[[cells.cohorts]] +dbh = 0.5 +number_of_members = 1 +pft_name = "test2" + +[[cells.cohorts]] +dbh = 0.6 +number_of_members = 1 +pft_name = "test2" diff --git a/pyrealm_build_data/community/community.json b/pyrealm_build_data/community/community.json new file mode 100644 index 00000000..a7cf235a --- /dev/null +++ b/pyrealm_build_data/community/community.json @@ -0,0 +1,36 @@ +{ + "cell_id": 1, + "cell_area": 100, + "cohorts": [ + { + "pft_name": "broadleaf", + "dbh_value": 0.2, + "n_individuals": 6 + }, + { + "pft_name": "broadleaf", + "dbh_value": 0.25, + "n_individuals": 6 + }, + { + "pft_name": "broadleaf", + "dbh_value": 0.3, + "n_individuals": 3 + }, + { + "pft_name": "broadleaf", + "dbh_value": 0.35, + "n_individuals": 1 + }, + { + "pft_name": "conifer", + "dbh_value": 0.5, + "n_individuals": 1 + }, + { + "pft_name": "conifer", + "dbh_value": 0.6, + "n_individuals": 1 + } + ] +} \ No newline at end of file diff --git a/pyrealm_build_data/community/community.toml b/pyrealm_build_data/community/community.toml new file mode 100644 index 00000000..7681abca --- /dev/null +++ b/pyrealm_build_data/community/community.toml @@ -0,0 +1,32 @@ +cell_area = 100 +cell_id = 1 + +[[cohorts]] +dbh_value = 0.2 +n_individuals = 6 +pft_name = "broadleaf" + +[[cohorts]] +dbh_value = 0.25 +n_individuals = 6 +pft_name = "broadleaf" + +[[cohorts]] +dbh_value = 0.3 +n_individuals = 3 +pft_name = "broadleaf" + +[[cohorts]] +dbh_value = 0.35 +n_individuals = 1 +pft_name = "broadleaf" + +[[cohorts]] +dbh_value = 0.5 +n_individuals = 1 +pft_name = "conifer" + +[[cohorts]] +dbh_value = 0.6 +n_individuals = 1 +pft_name = "conifer" diff --git a/pyrealm_build_data/t_model/rtmodel_output.csv b/pyrealm_build_data/t_model/rtmodel_output.csv index b705cb57..192908b5 100644 --- a/pyrealm_build_data/t_model/rtmodel_output.csv +++ b/pyrealm_build_data/t_model/rtmodel_output.csv @@ -1,101 +1,101 @@ "dD","D","H","Ac","Wf","Ws","Wss","GPP","Rm1","Rm2","dWs","dWfr" -1.3261078333006,0.1,9.30685130731739,2.46024211324268,0.316316843131202,7.30958392378022,7.02392939699003,9.1978885806073,0.309052893467561,0.687337521113514,0.540583245523229,0.0507133024451026 -1.32871117783746,0.102652215666601,9.50029060536579,2.57798443422273,0.331455141542923,7.86255012133515,7.54118762906641,9.63808133390068,0.331812255678922,0.720232135264278,0.566520247739465,0.0517199040312726 -1.33117797524213,0.105309638022276,9.69176749651272,2.6980263063892,0.346889096535754,8.44168433502787,8.08126557669089,10.086871214885,0.355575685374399,0.753769193426402,0.592906534593003,0.0527083128736193 -1.33351711170997,0.10797199397276,9.8812773212796,2.82032581596637,0.362613319195676,9.04742961153548,8.64439989345793,10.5441015242512,0.380353595312149,0.787936985813052,0.619729263859446,0.0536786323598661 -1.33573677880796,0.11063902819618,10.0688175615767,2.94484131346525,0.378622454588389,9.68021622549394,9.23080912303363,11.0096165507556,0.40615560141348,0.822723876473295,0.646975781104036,0.0546309827131436 -1.33784453869993,0.113310501753796,10.254387644021,3.07153144993977,0.394911186420828,10.3404618729216,9.8406940662054,11.4832617067678,0.432990538913037,0.858118313421274,0.674633628301898,0.0555654991143128 -1.33984738228969,0.115986190831196,10.4379887617388,3.20035520914713,0.411474241176059,11.0285718725065,10.4742381600064,11.9648836485053,0.460866479040282,0.894108837621106,0.702690551111788,0.0564823300086018 -1.34175178114168,0.118665885595775,10.6196237126915,3.33127193601421,0.428306391773256,11.7449393734876,11.1316078669659,12.4543303824587,0.489790746146501,0.930684090939779,0.73113450494293,0.0573816355767315 -1.34356373392493,0.121349389158059,10.7992967527958,3.46424136176887,0.445402460798854,12.4899455689935,11.8129530727054,12.9514513593477,0.519769935199036,0.967832823168263,0.759953659939556,0.0582635863530963 -1.34528880802845,0.124036516625909,10.9770134623126,3.59922362605665,0.462757323350141,13.2639599138165,12.5184074902556,13.456097556807,0.550809929571247,1.00554389820046,0.78913640499461,0.0591283619756455 -1.34693217691204,0.126727094241966,11.152780624158,3.7361792963304,0.480365909528195,14.0673403457084,13.2480890696131,13.9681215518763,0.582915919062976,1.0438062994502,0.818671350892413,0.0599761500539064 -1.34849865368444,0.12942095859579,11.3266061129416,3.87506938477033,0.498223206613328,14.9004335093748,14.0021004111812,14.4873775842582,0.616092418091971,1.08260913457836,0.848547332669859,0.0608071451431647 -1.34999272133816,0.132117955903159,11.4984987936731,4.01585536296625,0.516324260952803,15.763574982432,14.7805291818641,15.0137216112089,0.650343284002018,1.12194163959478,0.878753411276606,0.0616215478141778 -1.35141856001679,0.134817941345835,11.6684684291974,4.15849917457047,0.534664179587632,16.6570895026657,15.5834485326891,15.5470113548429,0.685671735438319,1.16179318239315,0.909278874606647,0.0624195638089992 -1.35278007164389,0.137520778465868,11.8365255955194,4.3029632461093,0.553238131642625,17.5812911959984,16.4109175169342,16.0871063425527,0.722080370745106,1.20215326577153,0.940113237966488,0.0632014032745269 -1.3540809022024,0.140226338609156,12.0026816042727,4.44921049612278,0.5720413495015,18.5364838046349,17.2629815078311,16.633867941179,0.759571186344567,1.24301152998579,0.97124624403876,0.063967280066314 -1.35532446191842,0.142934500413561,12.166948431665,4.59720434278624,0.591069129786803,19.5229609149102,18.1396726149987,17.1871593855043,0.798145595059945,1.28435775487894,1.00266786239442,0.0647174111159762 -1.35651394357314,0.145645149337398,12.3293386533035,4.74690871015251,0.610316834162465,20.5410061844157,19.0410100988433,17.7468458015905,0.837804444349107,1.32618186162499,1.03436828860153,0.0654520158562425 -1.35765233913991,0.148358177224544,12.4898653843667,4.89828803314066,0.629779889975228,21.5908935680238,19.9670007822307,18.3127942254299,0.87854803441815,1.36847391412277,1.06633794297415,0.0661713156983165 -1.35874245492084,0.151073481902824,12.648542224642,5.05130726138582,0.649453790749606,22.6728875424723,20.9176394588072,18.8848736173375,0.920376136187516,1.41122412007145,1.09856746900067,0.0668755335567674 -1.35978692533676,0.153790966812666,12.8053832079987,5.20593186205372,0.669334096549764,23.7872433292083,21.8929092974061,19.4629548724745,0.96328800908587,1.45442283175684,1.13104773148727,0.0675648934176542 -1.36078822550735,0.156510540663339,12.9604027559094,5.36212782171464,0.689416434220453,24.934207115224,22.8927822420345,20.0469108278535,1.00728241864952,1.49806054657499,1.16376981444898,0.0682396199460212 -1.36174868274229,0.159232117114354,13.1136156346685,5.5198616473629,0.709696497518087,26.1140162716489,23.9172194069884,20.6366162661501,1.05235765390749,1.54212790731695,1.19672501877756,0.0688999381292793 -1.36267048705133,0.161955614479838,13.265036915994,5.67910036666032,0.730170047142041,27.3268995698889,24.9661714666941,21.2319479166135,1.09851154453454,1.58661570223683,1.22990485971311,0.0695460729533359 -1.36355570076892,0.164680955453941,13.4146819407256,5.83981152747531,0.750832910675397,28.5730773951301,26.039579039918,21.8327844533428,1.14574147775639,1.631514864923,1.26330106414353,0.0701782491086297 -1.36440626737884,0.167408066855479,13.5625662853637,6.00196319678315,0.771680982443548,29.8527619570471,27.1373730680311,22.439006491176,1.19404441499337,1.67681647399088,1.29690556775393,0.0707966907235048 -1.36522401961499,0.170136879390237,13.7087057312122,6.16552395898716,0.79271022329835,31.1661574975761,28.2594751870507,23.0504965794132,1.24341690823023,1.72251175261392,1.33071051204603,0.0714016211225926 -1.3660106869066,0.172867327429467,13.8531162359155,6.3304629137157,0.813916660334876,32.5134604956343,29.4057980932198,23.6671391935807,1.29385511610167,1.76859206790807,1.36470824124583,0.0719932626080915 -1.36676790222851,0.17559934880328,13.9958139071947,6.49674967314494,0.835296386547206,33.8948598686812,30.576245901918,24.2888207254217,1.34535481968439,1.81504893018389,1.39889129911607,0.0725718362620231 -1.36749720841148,0.178332884607737,14.1368149786099,6.66435435889352,0.856845560429167,35.3105371710362,31.7707144997275,24.9154294712867,1.39791143798801,1.86187399207896,1.43325242568866,0.0731375617677201 -1.36820006396122,0.18106787902456,14.276135787186,6.83324759853121,0.878560405525442,36.7606667888796,32.9890918895054,25.5468556190806,1.45152004313824,1.90905904758245,1.46778455393073,0.0736906572489545 -1.36887784843045,0.183804279152482,14.4137927527587,7.00340052174008,0.90043720993801,38.2454161318772,34.2312585283419,26.1829912339112,1.50617537524704,1.9565960309627,1.50248080635674,0.0742313391252575 -1.36953186738334,0.186542034849343,14.5498023589066,7.17478475616382,0.922472325792491,39.7649458213811,35.4970876583067,26.823730242571,1.56187185696549,2.00447701560754,1.53733449159815,0.0747598219821061 -1.37016335698822,0.18928109858411,14.684181135347,7.3473724229778,0.944662168668574,41.3194098751705,36.7864456299073,27.4689684169756,1.61860360771592,2.05269421278669,1.5723391009408,0.0752763184547676 -1.37077348827061,0.192021425298086,14.8169456416862,7.52113613220962,0.967003216998379,42.9089558887042,38.0991922182052,28.1186033566689,1.67636445760103,2.10123997034446,1.60748830483932,0.0757810391246956 -1.37136337105563,0.194762972274627,14.9481124524205,7.69604897783787,0.989492011436297,44.5337252128674,39.4351809315523,28.7725344704994,1.7351479609883,2.15010677133039,1.64277594941728,0.0762741924274637 -1.37193405762607,0.197505699016739,15.0776981430947,7.87208453269429,1.01212515420355,46.1938531282029,40.7942593129304,29.4306629575615,1.79494740976894,2.19928723257507,1.6781960529605,0.0767559845713095 -1.37248654611992,0.200249567131991,15.205719277534,8.04921684319259,1.03489930841048,47.8894690156234,42.1762692338887,30.0928917874881,1.8557558462911,2.24877410321746,1.71374280241065,0.0772266194654354 -1.37302178368877,0.202994540224231,15.3321923960683,8.22742042390532,1.05781119735925,49.6206965236084,43.5810471810902,30.7591256801762,1.91756607596797,2.29856026318982,1.74941054986544,0.0776862986572854 -1.37354066943674,0.205740583791608,15.4571340046776,8.40667025200847,1.08085760382966,51.387653731897,45.0084245354927,31.4292710850172,1.98037067956168,2.34863872166562,1.78519380909091,0.0781352212780784 -1.37404405715753,0.208487665130482,15.5805605649921,8.58694176161196,1.10403536935011,53.1904533116893,46.4582278441993,32.1032361597006,2.04416202514477,2.39900261547562,1.82108725205126,0.0785735839959364 -1.3745327578859,0.211235753244797,15.7024884850846,8.76821083799247,1.12734139345617,55.0292026823793,47.930279085025,32.780930748653,2.1089322797411,2.44964520749666,1.85708570546046,0.0790015809760008 -1.375007542278,0.213984818760569,15.8229341109991,8.95045381174418,1.15077263293854,56.9040041648405,49.4243959238375,33.4622663611702,2.17467342064885,2.50055988501747,1.8931841473601,0.0794194038469744 -1.3754691428341,0.216734833845125,15.9419137189638,9.13364745286129,1.17432610108217,58.8149551312955,50.9403919647372,34.1471561492936,2.24137724644844,2.55174015808548,1.92937770372699,0.0798272416735737 -1.37591825597579,0.219485772130793,16.0594435082393,9.31776896476532,1.1979988668984,60.7621481517996,52.4780769931503,34.8355148854808,2.30903538769861,2.60317965783821,1.96566164511397,0.0802252809344147 -1.37635554398887,0.222237608642744,16.1755395945575,9.50279597828918,1.22178805435147,62.7456711373731,54.0372572119161,35.5272589401144,2.37763931732431,2.65487213482248,2.00203138332666,0.0806137055048923 -1.37678163684191,0.224990319730722,16.2902180041109,9.68870654562878,1.24569084158084,64.7656074798214,55.6177354704561,36.2223062588896,2.44718036070007,2.70681145730468,2.03848246813903,0.0809926966446484 -1.37719713388994,0.227743883004406,16.4034946680528,9.87547913427236,1.26970446012073,66.8220361882818,57.2193114871183,36.9205763401179,2.5176497054332,2.75899160957474,2.07501058404989,0.0813624329892513 -1.37760260547171,0.230498277272186,16.5153854174736,10.0630926209166,1.29382619411785,68.9150320225393,58.8417820647944,37.6219902119822,2.58903841085095,2.81140669024644,2.11161154708244,0.0817230905457432 -1.37799859440824,0.233253482483129,16.625905978821,10.2515262853783,1.31805337954864,71.0446656231568,60.4849412999141,38.3264704097744,2.66133741719622,2.86405091055641,2.14828130162851,0.0820748426917308 -1.37838561740991,0.236009479671946,16.7350719697318,10.4407598045086,1.34238340343681,73.2110036384633,62.1485807849218,39.033940953144,2.73453755453656,2.91691859266399,2.18501591733921,0.0824178601777267 -1.37876416639853,0.238766250906766,16.8428988952494,10.630773246118,1.36681370307231,75.4141088484496,63.8324898043466,39.7443273233852,2.80862955139125,2.97000416795394,2.22181158606309,0.0827523111324634 -1.37913470975053,0.241523779239563,16.9494021443989,10.8215470629176,1.39134176523226,77.6540402856182,65.5364555245792,40.457556440786,2.88360404308149,3.0233021753438,2.25866461883311,0.0830783610709286 -1.37949769346672,0.244282048659064,17.054596987096,11.0130620864832,1.41596512540498,79.9308533528382,67.2602631774704,41.1735566420616,2.9594515798087,3.07680725959749,2.29557144290312,0.0833961729048838 -1.37985354227373,0.247041044045997,17.1584985713674,11.2052995212468,1.44068136701744,82.2445999382531,69.0036962378688,41.8922576578924,3.03616263446622,3.13051416964688,2.33252859883487,0.0837059069556505 -1.38020266066182,0.249800751130545,17.2611219208611,11.3982409385218,1.46548812066709,84.5953285272961,70.7665365952173,42.6135905905855,3.11372761018956,3.18441775692235,2.36953273763586,0.0840077209689594 -1.38054543386345,0.252561156451868,17.3624819326269,11.5918682705653,1.4903830633584,86.9830843118602,72.5485647193297,43.3374878918754,3.19213684765051,3.23851297369399,2.40658061794881,0.0843017701316779 -1.38088222877647,0.255322247319595,17.4625933751492,11.7861638046814,1.51536391774475,89.4079092966784,74.3495598204668,44.0638833408802,3.27138063210054,3.29279487142427,2.44366910329273,0.0845882070902399 -1.38121339483573,0.258084011777148,17.5614708866144,11.9811101773703,1.54042845137618,91.8698424029635,76.1693000038361,44.7927120222261,3.35144920016879,3.34725859913336,2.48079515935614,0.0848671819706178 -1.38153926483648,0.260846438566819,17.6591289733977,12.1766903685258,1.56557447595332,94.3689195693615,78.0075624186367,45.5239103043541,3.43233274642001,3.401899401778,2.51795585134227,0.0851388423996881 -1.38186015571264,0.263609517096492,17.7555820087538,12.3728876956838,1.59079984658792,96.9051738502698,79.8641234017721,46.2574158180184,3.51402142967797,3.45671261864476,2.55514834136645,0.085403333527851 -1.38217636927299,0.266373237407918,17.8508442316982,12.5696858083258,1.61610246107046,99.4786355115734,81.738758616354,46.9931674349878,3.59650537911958,3.51169368175845,2.59236988590549,0.0856607980527758 -1.38248819289781,0.269137590146464,17.9449297460657,12.7670686822381,1.6414802591449,102.08933212385,83.6312431851186,47.731105246959,3.67977470014522,3.56683811430633,2.6296178332989,0.085911376244154 -1.38279590019861,0.271902566532259,18.0378525197339,12.9650206139308,1.6669312217911,104.737288653097,85.5413518188771,48.4711705446891,3.76381948003059,3.62214152907876,2.6668896213018,0.0861552059693484 -1.3830997516432,0.274668158332657,18.1296263840012,13.1635262151165,1.69245337051498,107.422527549032,87.4688589401223,49.2133057973558,3.84862979336538,3.67759962692682,2.70418277468911,0.0863924227198364 -1.38339999514825,0.277434357835943,18.2202650331079,13.3625704072522,1.71804476664671,110.145068831016,89.4135388019104,49.95745463215,3.93419570728406,3.73320819523731,2.74149490291071,0.0866231596383532 -1.38369686664136,0.280201157826239,18.3097820238907,13.5621384161446,1.74370351064716,112.904930171656,91.3751656021376,50.7035618141079,4.02050728649405,3.78896310642564,2.77882369779718,0.0868475475466455 -1.3839905905945,0.282968551559522,18.398190775562,13.7622157666206,1.76942774142265,115.702126978129,93.3535135933293,51.4515732261863,4.10755459810649,3.84486031644692,2.81616693131558,0.0870657149737557 -1.38428138053052,0.285736532740711,18.4855045696039,13.9627882772643,1.79521563564826,118.536672471282,95.348357188059,52.2014358495856,4.1953277162746,3.90089586332554,2.85352245337486,0.0872777881847607 -1.38456943950432,0.288505095501772,18.5717365497708,14.163842055221,1.82106540709984,121.408577762562,97.3594710601119,52.9530977443242,4.28381672664492,3.95706586570352,2.89088818968024,0.0874838912098938 -1.3848549605603,0.291274234380781,18.6568997221912,14.3653634910691,1.84697530599459,124.317851928817,99.3866302415064,53.7065080300671,4.37301173062628,4.01336652140789,2.9282621396361,0.0876841458739879 -1.38513812716732,0.294043944301901,18.7410069555627,14.5673392537609,1.87294361834069,127.264502085018,101.429610215488,54.4616168672109,4.46290284948147,4.06979410603723,2.96564237429667,0.0878786718261775 -1.38541911363255,0.296814220556236,18.8240709814329,14.7697562856326,1.89896866529562,130.248533454953,103.488187005604,55.2183754382278,4.55348022824657,4.12634497156747,3.00302703436395,0.0880675865698054 -1.38569808549544,0.299585058783501,18.9061043945603,14.9726017974832,1.92504880253355,133.269949439938,105.562137260968,55.9767359292685,4.64473403948258,4.18301554497726,3.04041432823212,0.0882510054924827 -1.38597519990293,0.302356454954492,18.9871196533502,15.1758632637239,1.95118241962165,136.328751685595,107.651238337825,56.7366515120276,4.73665448686428,4.23980232689267,3.07780253007788,0.0884290418962548 -1.38625060596693,0.305128405354298,19.0671290803573,15.3795284175976,1.97736793940541,139.424940146728,109.755268377516,57.4980763258698,4.82923180861071,4.29670189025159,3.11518997799586,0.0886018070278293 -1.38652444510509,0.307900906566232,19.146144862854,15.5835852464676,2.00360381740298,142.558513150369,111.874006380955,58.2609654602178,4.92245628076204,4.35371087898763,3.15257507217857,0.0887694101088267 -1.38679685136575,0.310673955456442,19.2241790534559,15.7880219871773,2.02988854120851,145.729467457008,114.00723227971,59.0252749372032,5.01631822030724,4.41082600673362,3.18995627313998,0.0889319583660154 -1.38706795173803,0.313447549159173,19.301243570802,15.9928271214792,2.05622062990447,148.937798320074,116.154727003793,59.7909616945779,5.11080798816687,4.46804405554462,3.22733209998213,0.0890895570614977 -1.38733786644775,0.316221685062649,19.3773502002849,16.1979893715345,2.08259863348301,152.183499543695,118.31627254626,60.5579835688876,5.20591599203542,4.52536187464056,3.26470112870399,0.0892423095228158 -1.38760670923999,0.318996360795545,19.4525105948267,16.4034976954812,2.10902113227615,155.466563538787,120.491652024711,61.3262992789042,5.30163268908728,4.58277637916813,3.30206199055174,0.0893903171729477 -1.38787458764901,0.321771574214025,19.5267362756974,16.6093412830724,2.13548673639502,158.786981377511,122.680649739788,62.0958684093185,5.39794858855069,4.6402845489822,3.33941337040986,0.0895336795601679 -1.38814160325629,0.324547323389323,19.6000386333719,16.8155095513834,2.16199408517786,162.144742846137,124.88305123076,62.8666513946897,5.49485425415344,4.69788342744639,3.37675400523218,0.0896724943877476 -1.38840785193704,0.327323606595836,19.6724289284227,17.0219921405867,2.18854184664686,165.539836496357,127.098643328286,63.6386095036516,5.59234030644458,4.75557012025283,3.4140826825121,0.089806857543474 -1.38867342409613,0.33010042229971,19.7439182924444,17.2287789097958,2.21512871697375,168.972249695081,129.327214204448,64.4117048233729,5.69039742499572,4.81334179426093,3.45139823879129,0.0899368631289655 -1.38893840489374,0.332877769147902,19.8145177290086,17.4358599329763,2.24175341995409,172.441968672764,131.568553420137,65.1859002442707,5.78901635048604,4.87119567635505,3.48869955820618,0.0900626034887684 -1.38920287446129,0.335655645957689,19.8842381146448,17.6432254949238,2.26841470649021,175.948978570294,133.822451969871,65.9611594449738,5.88818788667432,4.92912905232084,3.52598557107124,0.0901841692392146 -1.38946690810827,0.338434051706612,19.9530901998459,17.8508660873089,2.29511135408258,179.493263484469,136.088702324138,66.7374468775354,5.98790290226207,4.98713926574019,3.56325525249861,0.0903016492970277 -1.38973057652025,0.341212985522828,20.0210846100956,18.0587724047872,2.32184216632978,183.074806512129,138.367098469337,67.5147277528916,6.08815233265083,5.04522371690463,3.60050762105311,0.0904151309076633 -1.38999394594866,0.343992446675869,20.0882318469153,18.2669353411751,2.3486059724368,186.693589792937,140.657435945397,68.292968026563,6.18892718159746,5.10337986174681,3.63774173744195,0.0905246996733709 -1.39025707839263,0.346772434567766,20.1545422889289,18.4753459856903,2.37540162673161,190.349594550879,142.959511881149,69.0721343845995,6.29021852277055,5.16160521079018,3.67495670323847,0.0906304395809657 -1.39052003177337,0.349552948724552,20.2200261929426,18.6839956192555,2.40222800818999,194.042801134496,145.273125027531,69.8521942297617,6.39201750121138,5.21989732811637,3.7121516596391,0.090732433029303 -1.39078286010134,0.352333988788098,20.284693695039,18.892875710866,2.42908401996849,197.773189055891,147.598075788694,70.6331156679401,6.49431533470253,5.27825383035032,3.74932578625284,0.0908307608564428 -1.39104561363673,0.355115554508301,20.3485548116826,19.1019779140192,2.45596858894532,201.540737028532,149.934166251074,71.4148674948069,6.59710331504726,5.33667238566284,3.7864782999226,0.0909255023665008 -1.39130833904335,0.357897645735574,20.4116194408367,19.3112940632058,2.48288066526932,205.345423003902,152.28120021052,72.1974191826987,6.7003728092629,5.39515071279032,3.8236084535776,0.0910167353561761 -1.39157107953643,0.360680262413661,20.473897363088,19.5208161704628,2.50981922191665,209.187224207002,154.638983197519,72.9807408677275,6.80411526069083,5.45368658007156,3.86071553511624,0.0911045361409513 -1.39183387502449,0.363463404572734,20.5353982427794,19.7305364219853,2.53678325425525,213.066117170762,157.007322500598,73.7648033371152,6.90832219002631,5.51227780450141,3.89779886631865,0.0911889795809593 -1.39209676224564,0.366247072322783,20.5961316291489,19.9404471747987,2.56377177961697,216.982077769369,159.386027187972,74.549578016752,7.01298519627079,5.57092225080091,3.93485780178835,0.0912701391065137 -1.39235977489851,0.369031265847274,20.656106957473,20.1505409534892,2.59078383687719,220.93508125055,161.774908127486,75.3350369589726,7.11809595760939,5.62961783050391,3.97189172792224,0.0913480867432972 -1.39262294376808,0.371815985397071,20.7153335502146,20.3608104469923,2.61781848604187,224.925102266845,164.173778004917,76.1211528305495,7.22364623221635,5.68836250105981,4.00890006190838,0.0914228931372077 +9.23535494468514,0.1,9.30685130731739,2.46024211324268,0.316316843131202,7.30958392378022,7.02392939699003,9.1978885806073,0.309052893467561,0.687337521113514,3.76476031902394,0.353180440335674 +9.41347975186052,0.11847070988937,10.6064694519639,3.3216732508429,0.427072275108373,11.6918358255143,11.0829530843886,12.418444631113,0.4876499357131,0.928002429473987,5.11540450334165,0.402164311708017 +9.56242271047402,0.137297669393091,11.8227317389103,4.29097588211766,0.551696899129414,17.5038687119995,16.341714741243,16.0422902499508,0.719035448614692,1.19880425999427,6.62787138562828,0.446337579848307 +9.69154513020616,0.156422514814039,12.9554153338523,5.35704971954974,0.68876353537068,24.8965832383358,22.8600457539045,20.0279257785112,1.0058420131718,1.49664183654837,8.28100480236048,0.485860611280906 +9.8067299345139,0.175805605074452,14.0065146815861,6.50935370650332,0.836916905121855,34.0005072801248,30.6655594427653,24.3359423049901,1.34928461548167,1.81857021981548,10.0553709356637,0.52100017161193 +9.9118179887506,0.19541906494348,14.9792591514052,7.73806437175455,0.994893990654156,44.9276887698395,39.7582456018626,28.9296136904005,1.74936280648195,2.16184494805204,11.9334707249518,0.552072706446046 +10.0093918091071,0.215242700920981,15.8775424513254,9.03413941905036,1.16153221102076,57.7736797915528,50.1151867893735,33.7751343052069,2.20506821873244,2.52393980261545,13.8997585704866,0.579412356468393 +10.1012253105873,0.235261484539195,16.7055797227729,10.389327995,1.33577074221428,72.619474012326,61.6950922673494,38.841657416978,2.71458405976337,2.90254967658711,15.9405589482595,0.603352606803862 +10.1885538666546,0.25546393516037,17.4676966162467,11.7961506682707,1.51664794306338,89.5333258349725,74.4424750457644,44.1012203403852,3.27546890201363,3.29558498140014,18.0439316139172,0.624215789412495 +10.2722432037438,0.275841042893679,18.1681979492976,13.2478625042661,1.70329660769136,108.572417149682,88.2913823468227,49.5286063877833,3.8848208232602,3.70116133071686,20.1995143511014,0.642307261965835 +10.3528989805214,0.296385529301166,18.8112859111281,14.7384072607093,1.89493807637692,129.784359269137,103.168645349644,55.1011736243075,4.53942039538435,4.11758674368246,22.3983600310141,0.65791243548126 +10.4309402999148,0.317091327262209,19.4010098996293,16.2623676083615,2.09087583536076,153.208530237407,118.996649730928,60.7986687624964,5.23585258816084,4.54334773768881,24.6327776139951,0.671295558231005 +10.5066505553506,0.337953207862039,19.9412369382857,17.8149143933709,2.2904889934334,178.877254866472,135.695650514095,66.6030374739461,5.97060862262016,4.97709515339117,26.8961824217297,0.682699582930953 +10.5802135953775,0.35896650897274,20.4356356570896,19.3917567830863,2.4932258721111,206.816838688539,153.185667373722,72.4982380038911,6.74016936444376,5.41763022654509,29.1829583714671,0.692346693223028 +10.6517401049225,0.380126936163495,20.8876692783712,20.9890943915628,2.69859785034379,237.048468789206,171.388002835312,78.4700621870874,7.54107212475373,5.86389121292604,31.4883332404618,0.70043921831005 +10.7212872876908,0.40143041637334,21.300594588579,22.6035720021614,2.90617354313504,269.588994995487,190.226427898919,84.5059661732193,8.36996282755243,6.31494073881986,33.8082670397389,0.707160761026536 +10.788873837953,0.422872990948721,21.6774648674801,24.2322371960362,3.11557335377609,304.451604634413,209.628079003584,90.5949120162889,9.22363547615769,6.76995396335422,36.1393529904729,0.712677426777046 +10.8544915107154,0.444450738624627,22.0211353971244,25.8725009974224,3.32646441395431,341.646403373464,229.524107986928,96.7272205426512,10.0990607514249,7.22820758365789,38.4787302691996,0.717139081457436 +10.9181141692032,0.466159721646058,22.3342706083702,27.5221015217334,3.53855590993715,381.180913710614,249.850123500867,102.894435445385,10.9934054340381,7.68906967893882,40.8240075270072,0.720680593366289 +10.9797049112577,0.487995949984465,22.6193522183127,29.1790705359805,3.75159478319749,423.060501631756,270.546458704653,109.089198266706,11.9040441830047,8.15199036820116,43.1731961323058,0.723423031954346 +11.0392216947139,0.50995535980698,22.8786879147386,30.8417027966632,3.96536178814241,467.28874088081,291.558296287291,115.30513376426,12.8285650366408,8.61649324392616,45.5246520968697,0.725474808061432 +11.0966217616171,0.532033803196408,23.1144202839139,32.508528007077,4.17966788662418,513.867723244362,312.835678172426,121.536745070403,13.7647698395867,9.08216753756115,47.8770256941991,0.726932748015837 +11.1518650808786,0.554227046719642,23.3285357753348,34.1782852265282,4.39435095769649,562.798322268441,334.333423749197,127.779318018207,14.7106706449647,9.548660970017,50.2292178503936,0.727883098927698 +11.2049169750738,0.576530776881399,23.5228735647065,35.8498995631448,4.60927280097576,614.080416916261,356.010977216961,134.028834004941,15.6644829975463,10.0156732401523,52.5803424692092,0.72840246558233 +11.2557500607435,0.598940610831546,23.699134223315,37.5224609864079,4.82431641253815,667.713080849277,377.832201659479,140.281890780365,16.6246168730171,10.4829501054607,54.929693937393,0.728558681142435 +11.3043456069087,0.621452110953033,23.8588881343156,39.1952051028777,5.03938351322714,723.694742271028,399.765134781884,146.535630574648,17.5896659304029,10.9502780112318,57.2767191390989,0.728411614824685 +11.3506943996244,0.644060802166851,24.0035836186046,40.867495747327,5.25439231037061,782.023318611418,421.781718844873,152.787675013381,18.5583956291744,11.4174792268967,59.6209933863983,0.728013920123719 +11.3947971885715,0.6667621909661,24.1345547479324,42.5388092506586,5.46927547508467,842.696329743805,443.857515200855,159.036066301443,19.5297306688376,11.8844074508305,61.9621997450884,0.72741172722304 +11.4366647830844,0.689551785343243,24.2530288329452,44.2087202549852,5.6839783184981,905.710992912826,465.971411958392,165.279214191096,20.5027421261692,12.3509438473973,64.3001113004591,0.726645283101856 +11.4763178583598,0.712425114909411,24.3601335804589,45.8768889547655,5.89845715132699,971.064302101036,488.105331654886,171.515848281556,21.476634592815,12.8169934824045,66.6345759662775,0.72574954261762 +11.5137865270621,0.735377750626131,24.4569039185977,47.5430496507862,6.11267781224394,1038.75309417099,510.243944383874,177.744975226784,22.4507335528904,13.2824821253373,68.965503492165,0.72475471358056 +11.5491097265677,0.758405323680255,24.5442884912449,49.2070005110274,6.3266143514178,1108.77410378004,532.374390583779,183.965840455342,23.4244731856863,13.7473533887698,71.2928543701998,0.72368675857888 +11.5823344673607,0.781503543133391,24.623155825123,50.8685944390853,6.54024785645383,1181.12400877234,554.486016631851,190.177894030974,24.3973847318015,14.2115661772028,73.6166303814841,0.722567856084471 +11.6135149834225,0.804668212068112,24.6943001741004,52.5277309569395,6.75356540874936,1255.79946750029,576.570125483616,196.380760305435,25.3690855212791,14.6750924192878,75.936866558147,0.721416823179015 +11.6427118207842,0.827895242034957,24.7584470462852,54.1843490145248,6.96655915901033,1332.797149312,598.619743838572,202.574211036269,26.3392687288971,15.1379150589799,78.253624366379,0.72024950209124 +11.6699908957385,0.851180665676526,24.8162584202463,55.8384206438972,7.1792255113582,1412.11375925615,620.62940668237,208.758141662209,27.3076938940243,15.6000262826507,80.5669859421713,0.719079112624556 +11.695422549592,0.874520647468003,24.8683376574133,57.4899453808399,7.39156440610798,1493.74605789843,642.594959540046,214.932550447731,28.274178219762,16.0614259606083,82.8770492339763,0.71791657247362 +11.719080622341,0.897911492567187,24.9152341183743,59.1389453816113,7.60357869192145,1577.69087700956,664.513378360821,221.097520226468,29.2385886478761,16.5221202828238,85.1839239259814,0.716770787371128 +11.7410415633662,0.921349653811869,24.9574474914475,60.7854611672287,7.81527357864369,1663.94513177103,686.382606629988,227.253202490747,30.2008346917195,16.982120569978,87.4877280325238,0.715648912964093 +11.7613835932232,0.944831736938601,24.995431842535,62.4295479322454,8.02665616271726,1752.50583004824,708.201409055604,233.39980359154,31.1608619984466,17.4414422422149,89.7885850687288,0.714556590285057 +11.7801859269303,0.968354504125048,25.0295993958697,64.071272359425,8.23773501764035,1843.37007919871,729.969240996173,239.53757282977,32.1186466038316,17.9001039292314,92.0866217150543,0.713498156651918 +11.7975280658584,0.991914875978908,25.0603240558164,65.7107098860491,8.44851984249203,1936.53509081381,751.68613267014,245.666792236099,33.0741898374862,18.3581267065446,94.38196590435,0.712476833795632 +11.8134891624399,1.01550993211063,25.0879446803725,67.3479423718062,8.65902116208937,2031.99818373354,773.352587109393,251.78776785207,34.0275138328133,18.8155334439505,96.6747452695209,0.711494894974698 +11.8281474594478,1.0391369104355,25.1127681174163,68.9830561222896,8.8692500728658,2129.7567856242,794.969490778912,257.900822340737,34.9786575942721,19.272348253333,98.9650858981251,0.710553812787041 +11.8415798035397,1.0627932053544,25.1350720150602,70.6161402260632,9.07921802906526,2229.80843336607,816.538035775549,264.006288769597,35.9276735741242,19.7285960240771,101.253111347416,0.7096543893328 +11.8538612311044,1.08647636496148,25.1551074176751,72.2472851670227,9.28893666433149,2332.15077246263,838.05965253416,270.104505422751,36.8746247115031,20.1843020353925,103.538941879587,0.708796870315698 +11.8650646231567,1.11018408742369,25.173101159261,73.8765816773654,9.4984176442327,2436.78155565152,859.535952003227,276.195811512602,37.819581888142,20.639491635859,105.822693882435,0.707981044596844 +11.8752604250679,1.13391421667,25.1892580658455,75.5041197998774,9.70767254569852,2543.6986408717,880.968676299715,282.280543674115,38.7626217571875,21.0941899814501,108.104479445418,0.707206330634246 +11.8845164262532,1.15766473752014,25.2037629785039,77.1299881314368,9.91671275975616,2652.89998871831,902.359656910159,288.359033136565,39.703824904047,21.5484218241846,110.384406065233,0.706471851155574 +11.8928975945279,1.18143377037264,25.2167826084184,78.754273222612,10.1255494143358,2764.38365949757,923.710779568305,294.431603478872,40.6432743010054,22.0022113443869,112.662576458682,0.705776497322344 +11.9004659596413,1.2052195655617,25.2284672351395,80.3770591109958,10.3341933142709,2878.14780997727,945.023955006398,300.498568884916,41.5810540202815,22.4555820203118,114.939088463738,0.705118983552523 +11.9072805404736,1.22902049748098,25.2389522588894,81.9984269684694,10.5426548959461,2994.19068991414,966.301094845036,306.560232824797,42.5172481731816,22.9085565295971,117.214035012502,0.704497894076989 +11.9133973104918,1.25283505856193,25.2483596173681,83.6184548449223,10.7509441943472,3112.51063842675,987.544091953677,312.616887096702,43.4519400459618,23.3611566776647,119.487504162134,0.703911722214845 +11.9188691962787,1.27666185318291,25.2567990770969,85.2372174930867,10.9590708205397,3233.106080272,1008.75480467921,318.668811172028,44.3852114058853,23.8134033487836,121.759579171935,0.703358903264433 +11.9237461042401,1.30049959157547,25.2643694088729,86.8547862610741,11.1670439478524,3355.97552207376,1029.93504440227,324.71627179362,45.3171419536999,24.2653164760464,124.030338616569,0.702837841822017 +11.9280749709458,1.32434708378395,25.271159456425,88.4712290409399,11.3748723052637,3481.11754854429,1051.08656593975,330.759522783474,46.2478089013491,24.7169150269997,126.299856526992,0.702346934259345 +11.9318998329353,1.34820323372584,25.2772491068575,90.0866102631639,11.5825641766925,3608.53081873188,1072.21106036679,336.798805022098,47.1772866561388,25.1682170021022,128.568202552003,0.701884587015119 +11.9352619122133,1.37206703339171,25.2827101709614,91.7009909283261,11.7901274050705,3738.21406232222,1093.31014988192,342.834346566941,48.1056465948044,25.6192394435739,130.835442134521,0.701449231284342 +11.9381997140573,1.39593755721614,25.287607180965,93.3144286684936,11.9975694002349,3870.16607601566,1114.38538438539,348.866362881891,49.0329569129571,26.0699984525464,133.10163669769,0.701039334623581 +11.9407491341446,1.41981395664425,25.2919981127896,94.9269778319297,12.2048971498195,4004.38571999813,1135.43823948266,354.895057153972,49.959282537237,26.5205092127289,135.366843836778,0.700653409929674 +11.9429435723781,1.44369545491254,25.2959350393887,96.5386895856977,12.4121172324468,4140.87191451933,1156.47011566282,360.920620676935,50.8846850891641,26.9707860190731,137.631117513583,0.700290022194102 +11.9448140511425,1.4675813420573,25.2994647212688,98.1496120315762,12.6192358326312,4279.6236365888,1177.48233843573,366.943233284627,51.8092228911721,27.4208423101577,139.894508250683,0.699947793385115 +11.9463893360423,1.49147097015958,25.302629139833,99.7597903314401,12.8262587568994,4420.63991679732,1198.47615924184,372.963063819736,52.7329510066408,27.8706907032171,142.157063323418,0.699625405764469 +11.9476960574774,1.51536374883167,25.3054659787496,101.369266838901,13.0331914507158,4563.91983626886,1219.45275697546,378.980270625947,53.6559213069204,28.3203430309185,144.418826947912,0.699321603905093 +11.9487588316758,1.53925914094662,25.3080090581336,102.978081234555,13.2400390158714,4709.46252374619,1240.41323998599,384.995002053579,54.5781825593838,28.7698103791476,146.679840463869,0.699035195639795 +11.9496003800507,1.56315665860998,25.3102887259348,104.586270662666,13.446806228057,4857.26715281142,1261.35864844211,391.007396970588,55.499780531453,29.2191031251943,148.940142511157,0.698765052139036 +11.9502416459605,1.58705585937008,25.3123322105577,106.193869867514,13.6534975543946,5007.33293924156,1282.28995696246,397.017585272329,56.4207581063483,29.6682309758462,151.199769199484,0.698510107287346 +11.9507019081418,1.610956342662,25.3141639383961,107.800911328006,13.8601171707436,5159.65913849779,1303.20807743181,403.025688384807,57.3411554069997,30.1172030049955,153.458754270681,0.698269356503016 +11.9509988902507,1.63485774647828,25.3158058196382,109.407425389423,14.06666897864,5314.24504334618,1324.11386193558,409.031819757225,58.2610099251653,30.5660276904461,155.717129253297,0.698041855123787 +11.9511488660899,1.65875974425878,25.317277505408,111.013440391445,14.2731566217573,5471.08998160699,1345.00810575727,415.036085340631,59.18035665332,31.0147129496812,157.974923609348,0.697826716462099 +11.9511667602222,1.68266204199096,25.3185966190244,112.618982791806,14.4795835018036,5630.19331402889,1365.89155039375,421.038584050197,60.0992282173251,31.4632661744092,160.23216487319,0.697623109616918 +11.9510662437754,1.70656437551141,25.3197789639113,114.224077285093,14.6859527937977,5791.55443228403,1386.76488655164,427.039408209385,61.0176550082721,31.9116942637547,162.488878782583,0.697430257114672 +11.95085982533,1.73046650799896,25.3208387104553,115.828746916384,14.892267460678,5955.17275707956,1407.6287570959,433.038643974783,61.9356653122196,32.3600036560057,164.745089402082,0.697247432439474 +11.9505589368557,1.75436822764962,25.3217885638921,117.433013189507,15.0985302672223,6121.04773638097,1428.48375992771,439.036371740836,62.8532854368194,32.8082003588581,167.000819238952,0.697073957502065 +11.9501740147204,1.77826934552333,25.3226399151073,119.036896169816,15.3047437932621,6289.17884374234,1449.33045077411,445.032666524105,63.770539834061,33.2562899781309,169.256089351856,0.696909200087836 +11.9497145758456,1.80216969355277,25.323402976057,120.64041458148,15.5109104461903,6459.56557673866,1470.16934587636,451.027598326976,64.68745121856,33.7042777449449,171.510919452591,0.696752571316395 +11.9491892891207,1.82606912270446,25.3240869013502,122.243585899309,15.7170324727683,6632.20745549527,1491.00092456774,457.021232480963,65.6040406809804,34.1521685413771,173.765328001152,0.696603523138593 +11.9486060422159,1.8499675012827,25.3246998973831,123.846426435221,15.9231119702428,6807.10402130943,1511.82563173442,463.013629969995,66.5203277963146,34.5999669246193,176.019332294468,0.696461545891287 +11.9479720039624,1.87386471336713,25.3252493202842,125.448951419498,16.1291508967926,6984.25483535922,1532.64388015593,469.004847734164,67.4363307268608,35.0476771496765,178.2729485491,0.696326165925379 +11.947293682477,1.89776065737506,25.3257417638004,127.05117507697,16.3351510813247,7163.65947749504,1553.45605272328,474.994938954582,68.3520663198244,35.4953031906537,180.526191978243,0.696196943318756 +11.9465769792282,1.92165524474001,25.3261831381463,128.653110698346,16.5411142326445,7345.31754510896,1574.26250453518,480.983953320044,69.2675501995478,35.9428487606825,182.779076863339,0.69607346968243 +11.9458272392406,1.94554839869847,25.3265787407367,130.25477070688,16.7470419480275,7529.22865207757,1595.06356487342,486.971937276264,70.1827968544307,36.3903173305468,185.031616620636,0.69595536606546 +11.9450492976437,1.96944005317695,25.3269333196293,131.85616672059,16.9529357212187,7715.39242777381,1615.85953906019,492.95893425849,71.0978197186482,36.837712146065,187.28382386298,0.695842280962021 +11.9442475227673,1.99333015177224,25.3272511304224,133.457309610253,17.1587969498897,7903.80851614384,1636.65071020033,498.944984908336,72.0126312488145,37.2850362442934,189.535710457149,0.695733888422098 +11.9434258559878,2.01721864681777,25.327535987279,135.058209553405,17.3646269425807,8094.47657484475,1657.43734081274,504.930127275648,72.9272429957607,37.7322924686113,191.787287577026,0.695629886265887 +11.9425878485214,2.04110549852975,25.3277913086781,136.658876084556,17.5704269251573,8287.39627443939,1678.21967435503,510.914397006251,73.8416656716215,38.1794834827512,194.038565752866,0.69552999440074 +11.9417366953606,2.06499067422679,25.3280201584364,138.259318141853,17.7761980468097,8482.56729764473,1698.99793664625,516.897827516402,74.755909212435,38.6266117838347,196.289554916939,0.695433953238604 +11.9408752665385,2.08887414761751,25.3282252824847,139.859544110396,17.9819413856223,8679.98933863027,1719.77233719249,522.880450154729,75.6699828364695,39.0736797144742,198.540264445788,0.695341522211167 +11.9400061359016,2.11275589815059,25.3284091418387,141.459561862421,18.1876579537398,8879.66210236323,1740.54307042038,528.862294352461,76.5838950984967,39.5206894739994,200.790703199337,0.695252478379422 +11.9391316075632,2.13663591042239,25.3285739421546,143.059378794547,18.393348702156,9081.58530399742,1761.3103168234,534.843387762681,77.4976539402297,39.9676431288628,203.040879557085,0.695166615133919 +11.9382537402013,2.16051417363752,25.3287216602227,144.659001862283,18.5990145251506,9285.75866830297,1782.07424402603,540.823756389335,78.4112667371455,40.4145426222808,205.290801451581,0.695083740981745 +11.9373743693555,2.18439068111792,25.3288540677147,146.258437611976,18.8046562643969,9492.18192913409,1802.83500777056,546.803424706665,79.3247403419046,40.8613897831587,207.540476399384,0.695003678416091 +11.9364951278701,2.20826542985663,25.3289727524698,147.857692210376,19.0102747127626,9700.85482893232,1823.59275283131,552.782415769751,80.2380811245776,41.3081863343504,209.789911529694,0.694926262864179 +11.9356174646233,2.23213842011237,25.3290791375723,149.456771471983,19.2158706178263,9911.77711826289,1844.34761386094,558.760751316748,81.1512950098814,41.7549339002996,212.039113610826,0.694851341709288 +11.9347426616697,2.25600965504162,25.329174498451,151.055680884333,19.4214446851286,10124.9485553818,1865.09971617314,564.738451863424,82.0643875116182,42.2016340141033,214.288089074682,0.69477877338268 +11.9338718499205,2.27987914036496,25.3292599782041,152.654425631377,19.6269975811771,10340.3689058318,1885.84917646605,570.71553679055,82.9773637645061,42.648288124043,216.536844039384,0.694708426521255 +11.9330060234746,2.3037468840648,25.3293366013341,154.253010615075,19.8325299362239,10558.0379420646,1906.59610349039,576.692024424647,83.890228553577,43.0948975996184,218.785384330193,0.694640179186923 +11.9321460527067,2.32761289611175,25.3294052860573,155.851440475352,20.038042346831,10777.9554430886,1927.34059866618,582.667932112599,84.8029863413119,43.541463737123,221.03371549886,0.694573918143752 +11.9312926962124,2.35147718821716,25.329466855335,157.449719608536,20.2435353782403,11000.1211941389,1948.08275665167,588.643276290556,85.7156412926734,43.9879877647935,223.281842841516,0.694509538189161 +11.9304466117017,2.37533977360959,25.3295220467605,159.047852184374,20.4490095665623,11224.5349863698,1968.82266586788,594.618072547588,86.6281972981868,44.4344708475659,225.529771415216,0.694446941535517 diff --git a/pyrealm_build_data/t_model/rtmodel_test_outputs.r b/pyrealm_build_data/t_model/rtmodel_test_outputs.r index c5da8342..16ce3cdd 100644 --- a/pyrealm_build_data/t_model/rtmodel_test_outputs.r +++ b/pyrealm_build_data/t_model/rtmodel_test_outputs.r @@ -98,7 +98,7 @@ tmodel_run <- tmodel(rep(7, 100), seq(100), tf = 4.0, tr = 1.04, K = 0.5, - y = 0.17, + y = 0.6, zeta = 0.17, rr = 0.913, rs = 0.044 diff --git a/tests/unit/demography/test_community.py b/tests/unit/demography/test_community.py new file mode 100644 index 00000000..76b661e8 --- /dev/null +++ b/tests/unit/demography/test_community.py @@ -0,0 +1,600 @@ +"""test the community object in community.py initialises as expected.""" + +from contextlib import nullcontext as does_not_raise + +import numpy as np +import pytest +from marshmallow.exceptions import ValidationError + + +@pytest.fixture +def fixture_flora(): + """Simple flora object for use in community tests.""" + + from pyrealm.demography.flora import Flora, PlantFunctionalType + + return Flora( + [ + PlantFunctionalType(name="broadleaf", h_max=30), + PlantFunctionalType(name="conifer", h_max=20), + ] + ) + + +@pytest.fixture +def fixture_expected(fixture_flora): + """Expected results for test data. + + This fixture simply calculates the expected results directly to check that the + Community instance maintains row order and calculation as expected. + """ + + from pyrealm.demography.t_model_functions import calculate_heights + + a_hd = np.array([fixture_flora["broadleaf"].a_hd, fixture_flora["conifer"].a_hd]) + h_max = np.array([fixture_flora["broadleaf"].h_max, fixture_flora["conifer"].h_max]) + n_indiv = np.array([6, 1]) + dbh = np.array([0.2, 0.5]) + + expected = { + "n_individuals": n_indiv, + "a_hd": a_hd, + "height": calculate_heights(a_hd=a_hd, h_max=h_max, dbh=dbh), + } + + return expected + + +def check_expected(community, expected): + """Helper function to provide simple check of returned community objects.""" + + assert np.allclose( + community.cohort_data["n_individuals"], + expected["n_individuals"], + ) + assert np.allclose( + community.cohort_data["a_hd"], + expected["a_hd"], + ) + assert np.allclose( + community.cohort_data["height"], + expected["height"], + ) + + +@pytest.mark.parametrize( + argnames="args,outcome,excep_message", + argvalues=[ + pytest.param( + { + "cell_id": 1, + "cell_area": 100, + "cohort_pft_names": np.array(["broadleaf", "conifer"]), + "cohort_n_individuals": np.array([6, 1]), + "cohort_dbh_values": np.array([0.2, 0.5]), + }, + does_not_raise(), + None, + id="correct", + ), + pytest.param( + { + "cell_id": 1, + "cell_area": 100, + "cohort_pft_names": ["broadleaf", "conifer"], + "cohort_n_individuals": [6, 1], + "cohort_dbh_values": [0.2, 0.5], + }, + pytest.raises(ValueError), + "Cohort data not passed as numpy arrays.", + id="lists_not_arrays", + ), + pytest.param( + { + "cell_id": 1, + "cell_area": 100, + "cohort_pft_names": np.array(["broadleaf", "conifer"]), + "cohort_n_individuals": np.array([6, 1]), + "cohort_dbh_values": np.array([0.2, 0.5, 0.9]), + }, + pytest.raises(ValueError), + "Cohort arrays are of unequal length", + id="unequal_cohort_arrays", + ), + pytest.param( + { + "cell_area": 100, + "cohort_pft_names": np.array(["broadleaf", "conifer"]), + "cohort_n_individuals": np.array([6, 1]), + "cohort_dbh_values": np.array([0.2, 0.5]), + }, + pytest.raises(TypeError), + "Community.__init__() missing 1 required positional argument: 'cell_id'", + id="missing_arg", + ), + pytest.param( + { + "cell_id": 1, + "cell_area": 100, + "cell_elevation": 100, + "cohort_pft_names": np.array(["broadleaf", "conifer"]), + "cohort_n_individuals": np.array([6, 1]), + "cohort_dbh_values": np.array([0.2, 0.5]), + }, + pytest.raises(TypeError), + "Community.__init__() got an unexpected keyword argument 'cell_elevation'", + id="extra_arg", + ), + pytest.param( + { + "cell_id": 1, + "cell_area": "100", + "cohort_pft_names": np.array(["broadleaf", "conifer"]), + "cohort_n_individuals": np.array([6, 1]), + "cohort_dbh_values": np.array([0.2, 0.5]), + }, + pytest.raises(ValueError), + "Community cell area must be a positive number.", + id="cell_area_as_string", + ), + pytest.param( + { + "cell_id": 1, + "cell_area": -100, + "cohort_pft_names": np.array(["broadleaf", "conifer"]), + "cohort_n_individuals": np.array([6, 1]), + "cohort_dbh_values": np.array([0.2, 0.5]), + }, + pytest.raises(ValueError), + "Community cell area must be a positive number.", + id="cell_area_negative", + ), + pytest.param( + { + "cell_id": "1", + "cell_area": 100, + "cohort_pft_names": np.array(["broadleaf", "conifer"]), + "cohort_n_individuals": np.array([6, 1]), + "cohort_dbh_values": np.array([0.2, 0.5]), + }, + pytest.raises(ValueError), + "Community cell id must be a integer >= 0.", + id="cell_id_as_string", + ), + pytest.param( + { + "cell_id": -1, + "cell_area": 100, + "cohort_pft_names": np.array(["broadleaf", "conifer"]), + "cohort_n_individuals": np.array([6, 1]), + "cohort_dbh_values": np.array([0.2, 0.5]), + }, + pytest.raises(ValueError), + "Community cell id must be a integer >= 0.", + id="cell_id_negative", + ), + pytest.param( + { + "cell_id": 1, + "cell_area": 100, + "cohort_pft_names": np.array(["broadleaf", "juniper"]), + "cohort_n_individuals": np.array([6, 1]), + "cohort_dbh_values": np.array([0.2, 0.5]), + }, + pytest.raises(ValueError), + "Plant functional types unknown in flora: juniper", + id="unknown_pft", + ), + ], +) +def test_Community__init__( + fixture_flora, fixture_expected, args, outcome, excep_message +): + """Test Community initialisation. + + Test that when a new community object is instantiated, it contains the expected + properties. + """ + + from pyrealm.demography.community import Community + + with outcome as excep: + community = Community(**args, flora=fixture_flora) + + if isinstance(outcome, does_not_raise): + # Simple test that data is loaded and trait and t model data calculated + check_expected(community=community, expected=fixture_expected) + return + + # Check exception message + assert str(excep.value) == excep_message + + +@pytest.mark.parametrize( + argnames="file_data,outcome,excep_message", + argvalues=[ + pytest.param( + """cell_id,cell_area,cohort_pft_names,cohort_dbh_values,cohort_n_individuals +1,100,broadleaf,0.2,6 +1,100,conifer,0.5,1 +""", + does_not_raise(), + None, + id="correct", + ), + pytest.param( + """cell_id,cell_elevation,cohort_pft_names,cohort_dbh_values,cohort_n_individuals +1,100,broadleaf,0.2,6 +1,100,conifer,0.5,1 +""", + pytest.raises(ValidationError), + "{'cell_area': ['Missing data for required field.']," + " 'cell_elevation': ['Unknown field.']}", + id="mislabelled_field", + ), + pytest.param( + """cell_id,cell_area,cohort_pft_names,cohort_dbh_values,cohort_n_individuals +1,100,broadleaf,0.2,6 +11,100,conifer,0.5,1 +""", + pytest.raises(ValueError), + "Multiple cell id values fields in community data, see load_communities", + id="not_just_one_cell_id", + ), + pytest.param( + """cell_id,cell_area,cohort_pft_names,cohort_dbh_values,cohort_n_individuals +1,100,broadleaf,0.2,6 +1,200,conifer,0.5,1 +""", + pytest.raises(ValueError), + "Cell area varies in community data", + id="not_just_one_cell_area", + ), + ], +) +def test_Community_from_csv( + tmp_path, fixture_flora, fixture_expected, file_data, outcome, excep_message +): + """Test that a community can be successfully imported from a csv.""" + + from pyrealm.demography.community import Community + + temp_file = tmp_path / "data.csv" + temp_file.write_text(file_data, encoding="utf-8") + + with outcome as excep: + community = Community.from_csv(path=temp_file, flora=fixture_flora) + + if isinstance(outcome, does_not_raise): + # Simple test that data is loaded and trait and t model data calculated + check_expected(community=community, expected=fixture_expected) + return + + # Test exception explanation + assert str(excep.value) == excep_message + + +@pytest.mark.parametrize( + argnames="file_data,outcome,excep_message", + argvalues=[ + pytest.param( + """{"cell_id":1,"cell_area":100,"cohorts":[ + {"pft_name":"broadleaf","dbh_value":0.2,"n_individuals":6}, + {"pft_name":"conifer","dbh_value":0.5,"n_individuals":1}]}""", + does_not_raise(), + None, + id="correct", + ), + pytest.param( + """{"cell_id":1,"cohorts":[ + {"pft_name":"broadleaf","dbh_value":0.2,"n_individuals":6}, + {"pft_name":"conifer","dbh_value":0.5,"n_individuals":1}]}""", + pytest.raises(ValidationError), + "{'cell_area': ['Missing data for required field.']}", + id="missing_area", + ), + pytest.param( + """{"cell_id":1,"cell_area":"a","cohorts":[ + {"pft_name":"broadleaf","dbh_value":0.2,"n_individuals":6}, + {"pft_name":"conifer","dbh_value":0.5,"n_individuals":1}]}""", + pytest.raises(ValidationError), + "{'cell_area': ['Not a valid number.']}", + id="area_as_string", + ), + pytest.param( + """{"cell_id":1.2,"cell_area":100,"cohorts":[ + {"pft_name":"broadleaf","dbh_value":0.2,"n_individuals":6}, + {"pft_name":"conifer","dbh_value":0.5,"n_individuals":1}]}""", + pytest.raises(ValidationError), + "{'cell_id': ['Not a valid integer.']}", + id="id_as_float", + ), + pytest.param( + """{"cell_id":-1,"cell_area":100,"cohorts":[ + {"pft_name":"broadleaf","dbh_value":0.2,"n_individuals":6}, + {"pft_name":"conifer","dbh_value":0.5,"n_individuals":1}]}""", + pytest.raises(ValidationError), + "{'cell_id': ['Must be greater than or equal to 0.']}", + id="id_negative", + ), + pytest.param( + """{"cell_id":1,"cell_area":0,"cohorts":[ + {"pft_name":"broadleaf","dbh_value":0.2,"n_individuals":6}, + {"pft_name":"conifer","dbh_value":0.5,"n_individuals":1}]}""", + pytest.raises(ValidationError), + "{'cell_area': ['Must be greater than 0.']}", + id="area_zero", + ), + pytest.param( + """{"cell_id":1,"cell_area":100,"cohorts":[]}""", + pytest.raises(ValidationError), + "{'cohorts': ['Shorter than minimum length 1.']}", + id="no_cohorts", + ), + pytest.param( + """{"cell_id":1,"cell_area":100,"cohorts":[ + {"pft_name":1,"dbh_value":0.2,"n_individuals":6}, + {"pft_name":"conifer","dbh_value":0.5,"n_individuals":1}]}""", + pytest.raises(ValidationError), + "{'cohorts': {0: {'pft_name': ['Not a valid string.']}}}", + id="bad_cohort_name", + ), + pytest.param( + """{"cell_id":1,"cell_area":100,"cohorts":[ + {"pft_name":"broadleaf","dbh_value":0,"n_individuals":6}, + {"pft_name":"conifer","dbh_value":0.5,"n_individuals":1}]}""", + pytest.raises(ValidationError), + "{'cohorts': {0: {'dbh_value': ['Must be greater than 0.']}}}", + id="dbh_zero", + ), + pytest.param( + """{"cell_id":1,"cell_area":100,"cohorts":[ + {"pft_name":"broadleaf","dbh_value":0.2,"n_individuals":6.1}, + {"pft_name":"conifer","dbh_value":0.5,"n_individuals":1}]}""", + pytest.raises(ValidationError), + "{'cohorts': {0: {'n_individuals': ['Not a valid integer.']}}}", + id="individuals_float", + ), + pytest.param( + """{"cell_id":1,"cell_area":100,"cohorts":[ + {"pft_name":"broadleaf","dbh_value":0.2,"n_individuals":0}, + {"pft_name":"conifer","dbh_value":0.5,"n_individuals":1}]}""", + pytest.raises(ValidationError), + "{'cohorts': {0: {'n_individuals': ['Must be greater than 0.']}}}", + id="individuals_less_than_one", + ), + ], +) +def test_Community_from_json( + tmp_path, fixture_flora, fixture_expected, file_data, outcome, excep_message +): + """Test that a community can be successfully imported from JSON.""" + + from pyrealm.demography.community import Community + + temp_file = tmp_path / "data.json" + temp_file.write_text(file_data, encoding="utf-8") + + with outcome as excep: + community = Community.from_json(path=temp_file, flora=fixture_flora) + + if isinstance(outcome, does_not_raise): + # Simple test that data is loaded and trait and t model data calculated + check_expected(community=community, expected=fixture_expected) + return + + assert str(excep.value) == excep_message + + +@pytest.mark.parametrize( + argnames="file_data,outcome,excep_message", + argvalues=[ + pytest.param( + """cell_area = 100 +cell_id = 1 + +[[cohorts]] +dbh_value = 0.2 +n_individuals = 6 +pft_name = "broadleaf" + +[[cohorts]] +dbh_value = 0.5 +n_individuals = 1 +pft_name = "conifer" +""", + does_not_raise(), + None, + id="correct", + ), + pytest.param( + """cell_id = 1 + +[[cohorts]] +dbh_value = 0.2 +n_individuals = 6 +pft_name = "broadleaf" + +[[cohorts]] +dbh_value = 0.5 +n_individuals = 1 +pft_name = "conifer" +""", + pytest.raises(ValidationError), + "{'cell_area': ['Missing data for required field.']}", + id="missing_area", + ), + pytest.param( + """cell_area = "a" +cell_id = 1 + +[[cohorts]] +dbh_value = 0.2 +n_individuals = 6 +pft_name = "broadleaf" + +[[cohorts]] +dbh_value = 0.5 +n_individuals = 1 +pft_name = "conifer" +""", + pytest.raises(ValidationError), + "{'cell_area': ['Not a valid number.']}", + id="area_as_string", + ), + pytest.param( + """cell_area = 100 +cell_id = 1.2 + +[[cohorts]] +dbh_value = 0.2 +n_individuals = 6 +pft_name = "broadleaf" + +[[cohorts]] +dbh_value = 0.5 +n_individuals = 1 +pft_name = "conifer" +""", + pytest.raises(ValidationError), + "{'cell_id': ['Not a valid integer.']}", + id="id_as_float", + ), + pytest.param( + """cell_area = 100 +cell_id = -1 + +[[cohorts]] +dbh_value = 0.2 +n_individuals = 6 +pft_name = "broadleaf" + +[[cohorts]] +dbh_value = 0.5 +n_individuals = 1 +pft_name = "conifer" +""", + pytest.raises(ValidationError), + "{'cell_id': ['Must be greater than or equal to 0.']}", + id="id_negative", + ), + pytest.param( + """cell_area = 0 +cell_id = 1 + +[[cohorts]] +dbh_value = 0.2 +n_individuals = 6 +pft_name = "broadleaf" + +[[cohorts]] +dbh_value = 0.5 +n_individuals = 1 +pft_name = "conifer" +""", + pytest.raises(ValidationError), + "{'cell_area': ['Must be greater than 0.']}", + id="area_zero", + ), + pytest.param( + """cell_area = 100 +cell_id = 1 +cohorts = [] +""", + pytest.raises(ValidationError), + "{'cohorts': ['Shorter than minimum length 1.']}", + id="no_cohorts", + ), + pytest.param( + """cell_area = 100 +cell_id = 1 + +[[cohorts]] +dbh_value = 0.2 +n_individuals = 6 +pft_name = 1 + +[[cohorts]] +dbh_value = 0.5 +n_individuals = 1 +pft_name = "conifer" +""", + pytest.raises(ValidationError), + "{'cohorts': {0: {'pft_name': ['Not a valid string.']}}}", + id="bad_cohort_name", + ), + pytest.param( + """cell_area = 100 +cell_id = 1 + +[[cohorts]] +dbh_value = 0 +n_individuals = 6 +pft_name = "broadleaf" + +[[cohorts]] +dbh_value = 0.5 +n_individuals = 1 +pft_name = "conifer" +""", + pytest.raises(ValidationError), + "{'cohorts': {0: {'dbh_value': ['Must be greater than 0.']}}}", + id="dbh_zero", + ), + pytest.param( + """cell_area = 100 +cell_id = 1 + +[[cohorts]] +dbh_value = 0.2 +n_individuals = 6.2 +pft_name = "broadleaf" + +[[cohorts]] +dbh_value = 0.5 +n_individuals = 1 +pft_name = "conifer" +""", + pytest.raises(ValidationError), + "{'cohorts': {0: {'n_individuals': ['Not a valid integer.']}}}", + id="individuals_float", + ), + pytest.param( + """cell_area = 100 +cell_id = 1 + +[[cohorts]] +dbh_value = 0.2 +n_individuals = -6 +pft_name = "broadleaf" + +[[cohorts]] +dbh_value = 0.5 +n_individuals = 1 +pft_name = "conifer" +""", + pytest.raises(ValidationError), + "{'cohorts': {0: {'n_individuals': ['Must be greater than 0.']}}}", + id="individuals_less_than_one", + ), + ], +) +def test_Community_from_toml( + tmp_path, fixture_flora, fixture_expected, file_data, outcome, excep_message +): + """Test that a community can be successfully imported from JSON.""" + + from pyrealm.demography.community import Community + + temp_file = tmp_path / "data.toml" + temp_file.write_text(file_data, encoding="utf-8") + + with outcome as excep: + community = Community.from_toml(path=temp_file, flora=fixture_flora) + + if isinstance(outcome, does_not_raise): + # Simple test that data is loaded and trait and t model data calculated + check_expected(community=community, expected=fixture_expected) + return + + assert str(excep.value) == excep_message diff --git a/tests/unit/demography/test_flora.py b/tests/unit/demography/test_flora.py index 72164314..ad975788 100644 --- a/tests/unit/demography/test_flora.py +++ b/tests/unit/demography/test_flora.py @@ -2,9 +2,11 @@ import sys from contextlib import nullcontext as does_not_raise +from dataclasses import fields from importlib import resources from json import JSONDecodeError +import pandas as pd import pytest from marshmallow.exceptions import ValidationError from pandas.errors import ParserError @@ -55,8 +57,8 @@ def test_PlantFunctionalTypeStrict__init__(args, outcome): from pyrealm.demography.flora import ( PlantFunctionalTypeStrict, - calculate_q_m, - calculate_z_max_proportion, + calculate_canopy_q_m, + calculate_canopy_z_max_proportion, ) with outcome: @@ -66,8 +68,8 @@ def test_PlantFunctionalTypeStrict__init__(args, outcome): if isinstance(outcome, does_not_raise): assert pft.name == "broadleaf" # Expected values from defaults - assert pft.q_m == calculate_q_m(m=2, n=5) - assert pft.z_max_prop == calculate_z_max_proportion(m=2, n=5) + assert pft.q_m == calculate_canopy_q_m(m=2, n=5) + assert pft.z_max_prop == calculate_canopy_z_max_proportion(m=2, n=5) # @@ -87,8 +89,8 @@ def test_PlantFunctionalType__init__(args, outcome): from pyrealm.demography.flora import ( PlantFunctionalType, - calculate_q_m, - calculate_z_max_proportion, + calculate_canopy_q_m, + calculate_canopy_z_max_proportion, ) with outcome: @@ -98,8 +100,8 @@ def test_PlantFunctionalType__init__(args, outcome): if isinstance(outcome, does_not_raise): assert pft.name == "broadleaf" # Expected values from defaults - assert pft.q_m == calculate_q_m(m=2, n=5) - assert pft.z_max_prop == calculate_z_max_proportion(m=2, n=5) + assert pft.q_m == calculate_canopy_q_m(m=2, n=5) + assert pft.z_max_prop == calculate_canopy_z_max_proportion(m=2, n=5) # @@ -169,11 +171,18 @@ def test_Flora__init__(flora_inputs, outcome): with outcome: flora = Flora(pfts=flora_inputs) - # Simple check that PFT instances are correctly keyed by name. if isinstance(outcome, does_not_raise): + # Simple check that PFT instances are correctly keyed by name. for k, v in flora.items(): assert k == v.name + # Check data view is correct + assert isinstance(flora.data, pd.DataFrame) + assert flora.data.shape == ( + len(flora_inputs), + len(fields(next(iter(flora.values())))), + ) + # # Test Flora factory methods from JSON, TOML, CSV @@ -270,9 +279,9 @@ def test_flora_from_csv(filename, outcome): def test_calculate_q_m(m, n, q_m): """Test calculation of q_m.""" - from pyrealm.demography.flora import calculate_q_m + from pyrealm.demography.flora import calculate_canopy_q_m - calculated_q_m = calculate_q_m(m, n) + calculated_q_m = calculate_canopy_q_m(m, n) assert calculated_q_m == pytest.approx(q_m) @@ -293,7 +302,7 @@ def test_calculate_q_m_values_raises_exception_for_invalid_input(): def test_calculate_z_max_ratio(m, n, z_max_ratio): """Test calculation of z_max proportion.""" - from pyrealm.demography.flora import calculate_z_max_proportion + from pyrealm.demography.flora import calculate_canopy_z_max_proportion - calculated_zmr = calculate_z_max_proportion(m, n) + calculated_zmr = calculate_canopy_z_max_proportion(m, n) assert calculated_zmr == pytest.approx(z_max_ratio) diff --git a/tests/unit/demography/test_t_model_functions.py b/tests/unit/demography/test_t_model_functions.py new file mode 100644 index 00000000..26c9523c --- /dev/null +++ b/tests/unit/demography/test_t_model_functions.py @@ -0,0 +1,149 @@ +"""test the functions in t_model_functions.py.""" + +import numpy as np +from numpy.testing import assert_array_almost_equal + +from pyrealm.demography.t_model_functions import ( + calculate_canopy_q_m, + calculate_crown_areas, + calculate_crown_fractions, + calculate_foliage_masses, + calculate_heights, + calculate_sapwood_masses, + calculate_stem_masses, +) + + +def test_calculate_heights(): + """Tests happy path for calculation of heights of tree from diameter.""" + pft_h_max_values = np.array([25.33, 15.33]) + pft_a_hd_values = np.array([116.0, 116.0]) + diameters_at_breast_height = np.array([0.2, 0.6]) + expected_heights = np.array([15.19414157, 15.16639589]) + actual_heights = calculate_heights( + h_max=pft_h_max_values, + a_hd=pft_a_hd_values, + dbh=diameters_at_breast_height, + ) + assert_array_almost_equal(actual_heights, expected_heights, decimal=8) + + +def test_calculate_crown_areas(): + """Tests happy path for calculation of crown areas of trees.""" + pft_ca_ratio_values = np.array([2, 3]) + pft_a_hd_values = np.array([116.0, 116.0]) + diameters_at_breast_height = np.array([0.2, 0.6]) + heights = np.array([15.194142, 15.166396]) + expected_crown_areas = np.array([0.04114983, 0.1848361]) + actual_crown_areas = calculate_crown_areas( + ca_ratio=pft_ca_ratio_values, + a_hd=pft_a_hd_values, + dbh=diameters_at_breast_height, + height=heights, + ) + assert_array_almost_equal(actual_crown_areas, expected_crown_areas, decimal=8) + + +def test_calculate_crown_fractions(): + """Tests happy path for calculation of crown fractions of trees.""" + pft_a_hd_values = np.array([116.0, 116.0]) + diameters_at_breast_height = np.array([0.2, 0.6]) + heights = np.array([15.194142, 15.166396]) + expected_crown_fractions = np.array([0.65491991, 0.21790799]) + actual_crown_fractions = calculate_crown_fractions( + a_hd=pft_a_hd_values, + dbh=diameters_at_breast_height, + height=heights, + ) + assert_array_almost_equal( + actual_crown_fractions, expected_crown_fractions, decimal=8 + ) + + +def test_calculate_stem_masses(): + """Tests happy path for calculation of stem masses.""" + diameters_at_breast_height = np.array([0.2, 0.6]) + heights = np.array([15.194142, 15.166396]) + pft_rho_s_values = np.array([200.0, 200.0]) + expected_stem_masses = np.array([47.73380488, 428.8197443]) + actual_stem_masses = calculate_stem_masses( + dbh=diameters_at_breast_height, height=heights, rho_s=pft_rho_s_values + ) + assert_array_almost_equal(actual_stem_masses, expected_stem_masses, decimal=8) + + +def test_calculate_foliage_masses(): + """Tests happy path for calculation of foliage masses.""" + crown_areas = np.array([0.04114983, 0.1848361]) + pft_lai_values = np.array([1.8, 1.8]) + pft_sla_values = np.array([14.0, 14.0]) + expected_foliage_masses = np.array([0.00529069, 0.02376464]) + actual_foliage_masses = calculate_foliage_masses( + crown_area=crown_areas, lai=pft_lai_values, sla=pft_sla_values + ) + assert_array_almost_equal(actual_foliage_masses, expected_foliage_masses, decimal=8) + + +def test_calculate_sapwood_masses(): + """Tests happy path for calculation of sapwood masses.""" + crown_areas = np.array([0.04114983, 0.1848361]) + pft_rho_s_values = np.array([200.0, 200.0]) + heights = np.array([15.194142, 15.166396]) + crown_fractions = np.array([0.65491991, 0.21790799]) + pft_ca_ratio_values = [390.43, 390.43] + expected_sapwood_masses = np.array([0.21540173, 1.27954667]) + actual_sapwood_masses = calculate_sapwood_masses( + crown_area=crown_areas, + rho_s=pft_rho_s_values, + height=heights, + crown_fraction=crown_fractions, + ca_ratio=pft_ca_ratio_values, + ) + assert_array_almost_equal(actual_sapwood_masses, expected_sapwood_masses, decimal=8) + + +"""Test the functions in jaideeps_t_model_extension_functions.py.""" + + +def test_calculate_calculate_canopy_q_m_returns_q_m_for_valid_input(): + """Test happy path for calculating q_m. + + test that values of q_m are calculated correctly when valid arguments are + provided to the function. + """ + m_values = np.array([2, 3]) + n_values = np.array([5, 4]) + expected_q_m_values = np.array([2.9038988210485766, 2.3953681843215673]) + actual_q_m_values = calculate_canopy_q_m(m=m_values, n=n_values) + assert_array_almost_equal(actual_q_m_values, expected_q_m_values, decimal=8) + + +def test_calculate_q_m_values_raises_exception_for_invalid_input(): + """Test unhappy path for calculating q_m. + + Test that an exception is raised when invalid arguments are provided to the + function. + """ + + pass + + +def test_calculate_z_max_values(): + """Test happy path for calculating z_max.""" + + pass + + +def test_calculate_r_0_values(): + """Test happy path for calculating r_0.""" + pass + + +def test_calculate_projected_canopy_area_for_individuals(): + """Test happy path for calculating canopy area for individuals.""" + pass + + +def test_calculate_relative_canopy_radii(): + """Test happy path for calculating relative canopy radii for individuals.""" + pass