Source code for virtual_ecosystem.models.plants.canopy
"""The :mod:`~virtual_ecosystem.models.plants.canopy` submodule provides functionality
to initialise the canopy layer data held in the simulation
:class:`~virtual_ecosystem.core.data.Data` instance and to generate
:class:`~pyrealm.demography.canopy.Canopy` instances from the plant community data
within each grid cell.
""" # noqa: D205
from __future__ import annotations
from pyrealm.demography.canopy import Canopy
from xarray import DataArray
from virtual_ecosystem.core.core_components import LayerStructure
from virtual_ecosystem.core.data import Data
from virtual_ecosystem.core.exceptions import InitialisationError
from virtual_ecosystem.core.logger import LOGGER
from virtual_ecosystem.models.plants.communities import PlantCommunities
[docs]
def initialise_canopy_layers(
data: Data, layer_structure: LayerStructure
) -> dict[str, DataArray]:
"""Initialise the canopy layer height and leaf area index data.
This function initialises the following data arrays describing the plant canopy
structure and soil layer structure within a Data object: ``layer_heights``,
``leaf_area_index``, ``layer_fapar``, ``layer_leaf_mass`` and
``shortwave_absorption``.
Args:
data: A Data object to update.
layer_structure: A layer structure object containing the layer configuration
Returns:
A data object with the layers added.
Raises:
InitialisationError: if the layers already exist in the data object
"""
# TODO - maybe this should happen somewhere before models start to be defined?
# The other models rely on it
# Check that layers do not already exist
layers_to_create = (
"layer_heights",
"leaf_area_index",
"layer_fapar",
"layer_leaf_mass",
"shortwave_absorption",
)
layers_found = set(layers_to_create).intersection(data.data.variables)
if layers_found:
msg = (
"Cannot initialise canopy layers, already "
f"present: {','.join(str(x) for x in layers_found)}"
)
LOGGER.critical(msg)
raise InitialisationError(msg)
# Initialise a data array for each layer from the layer structure template
layers: dict[str, DataArray] = dict()
for each_layer_name in layers_to_create:
layers[each_layer_name] = layer_structure.from_template()
# Initialise the fixed layer heights
# TODO: See issue #442 about centralising the layer_heights variable initialisation
layers["layer_heights"].loc[dict(layers=layer_structure.index_all_soil)] = (
layer_structure.soil_layer_depths.reshape(-1, 1)
)
layers["layer_heights"].loc[dict(layers=layer_structure.index_surface)] = (
layer_structure.surface_layer_height
)
return layers
[docs]
def calculate_canopies(
communities: PlantCommunities, max_canopy_layers: int
) -> dict[int, Canopy]:
"""Calculate the canopy structure of communities.
This function takes a PlantCommunities object and calculates the canopy
representation for each community, using the perfect plasticity approximation to
calculate the closure heights of the canopy layers.
Args:
communities: The PlantCommunities object to convert
max_canopy_layers: The maximum number of permitted canopy layers
Returns:
A dictionary of :class:`pyrealm.demography.canopy.Canopy` instances, keyed by
grid cell id. There is a one-to-one mapping of canopy representations to grid
cells.
"""
# TODO - this could be a method of PlantCommunities but creates circular import of
# PlantCohorts
# TODO - maybe return dict[str, NDArray] as the number of layers is only going to
# increase with the need for more resources and cohort data.
# Loop over the communities in each cell
canopies: dict[int, Canopy] = {}
for cell_id, community in communities.items():
# Calculate the PPA canopy model for the community in the cell
canopies[cell_id] = Canopy(community, fit_ppa=True)
# Fail if canopy representation has more layers than the configuration.
n_canopy_layers = canopies[cell_id].heights.size
if max_canopy_layers < n_canopy_layers:
msg = (
f"Canopy representation for the plant community in cell "
f"{cell_id} has {n_canopy_layers} layers, "
f"configured maximum is {max_canopy_layers}"
)
LOGGER.critical(msg)
raise RuntimeError(msg)
return canopies