Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of the canopy model #288

Merged
merged 27 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5d7fde7
Checkout of initial files from #231
davidorme Sep 16, 2024
83f0daf
Fixing new import locations in canopy modules
davidorme Sep 17, 2024
a40501e
Refining functions and separating canopy functions from specific Cano…
davidorme Sep 17, 2024
3dc0f81
Relocating functions into canopy_functions and renaming imports
davidorme Sep 17, 2024
a6e4e46
Docstrings and initial unit tests for canopy functions
davidorme Sep 17, 2024
068dddf
Merge branch 'develop' into 286-implementation-of-the-canopy-model
davidorme Sep 18, 2024
959d91e
More renaming of height to stem_height, fixing tests and docstrings
davidorme Sep 18, 2024
3eff100
Add crown gap fraction to PlantFunctionalType classes
davidorme Sep 18, 2024
1e62bd4
Testing of projected leaf area
davidorme Sep 18, 2024
5e294fd
Added more testing of input shapes, fixing new signature usages
davidorme Sep 19, 2024
d4f2426
Fixed missing new f_g trait in test_flora inputs
davidorme Sep 19, 2024
2890eb4
Merge branch '291-look-at-replacing-pandas-in-pyrealmdemography' into…
davidorme Sep 22, 2024
95759ad
Clearing out old Series.to_numpy() casting
davidorme Sep 22, 2024
dcda1a0
Fixing broken reference to stem_height in docstring
davidorme Sep 22, 2024
894c2bb
Array based testing of T model functions using inputs from R implemen…
davidorme Sep 22, 2024
dc7940b
Extending T Model unit test suite to all T model functions
davidorme Sep 23, 2024
4b8a533
Adding optional input validation to T model functions
davidorme Sep 23, 2024
527c9cb
Updating regression tests
davidorme Sep 23, 2024
49c8b35
Fixing tmodel unit tests for new failure mode
davidorme Sep 23, 2024
e1eb3d7
Merge branch 'develop' into 286-implementation-of-the-canopy-model
davidorme Sep 23, 2024
88c6103
Merge branch '288_292_merge_prep_work' into 286-implementation-of-the…
davidorme Sep 23, 2024
901b790
Fixed docstring and default from @omarjamil
davidorme Sep 23, 2024
6a13444
Fixes from @MarionBWeinzierl
davidorme Sep 23, 2024
9a5d1d2
Refactor of Canopy.__init__ to isolate init from calculation
davidorme Sep 24, 2024
1656156
Updated typing on q_m and p_zm functions
davidorme Sep 25, 2024
1b4b812
Merge canopy validation functions to avoid duplication and provide be…
davidorme Sep 25, 2024
c2814e3
Updating the rest of the test_canopy_functions
davidorme Sep 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions pyrealm/demography/canopy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"""Functionality for canopy modelling."""

import numpy as np
from numpy.typing import NDArray
from scipy.optimize import root_scalar # type: ignore [import-untyped]

from pyrealm.demography.canopy_functions import (
calculate_relative_canopy_radius_at_z,
calculate_stem_projected_crown_area_at_z,
calculate_stem_projected_leaf_area_at_z,
solve_community_projected_canopy_area,
)
from pyrealm.demography.community import Community


class Canopy:
"""Model of the canopy for a plant community.

This class generates a canopy structure for a community of trees using the
perfect-plasticity approximation model :cite:`purves:2008a`. In this approach, each
individual is assumed to arrange its canopy crown area plastically to take up space
in canopy layers and that new layers form below the canopy top as the available
space is occupied.

Real canopies contain canopy gaps, through process such as crown shyness. This
is included in the model through the canopy gap fraction, which sets the proportion
of the available space that will remain unfilled by any canopy.

Args:
community: A Community object that will be used to generate the canopy model.
canopy_gap_fraction: The proportion of the available space unfilled by canopy
(default: 0.05).
layer_tolerance: The minimum precision used by the solver to find canopy layer
closure heights (default: 0.001 metres)
"""

def __init__(
self,
community: Community,
canopy_gap_fraction: float = 0.05,
layer_tolerance: float = 0.001,
davidorme marked this conversation as resolved.
Show resolved Hide resolved
) -> None:
self.canopy_gap_fraction: float = canopy_gap_fraction
"""Canopy gap fraction."""
self.layer_tolerance: float = layer_tolerance
"""Numerical tolerance for solving canopy layer closure."""
self.total_community_crown_area: float
"""Total crown area across individuals in the community (metres 2)."""
self.max_stem_height: float
"""Maximum height of any individual in the community (metres)."""
self.crown_area_per_layer: float
"""Total crown area permitted in a single canopy layer, given the available
cell area of the community and its canopy gap fraction."""
self.n_layers: int
"""Total number of canopy layers."""
self.n_cohorts: int
"""Total number of cohorts in the canopy."""
self.layer_heights: NDArray[np.float32]
"""Column vector of the heights of canopy layers."""
self.stem_relative_radius: NDArray[np.float32]
"""Relative radius values of stems at canopy layer heights."""
self.stem_crown_area: NDArray[np.float32]
"""Stem projected crown area at canopy layer heights."""
self.stem_leaf_area: NDArray[np.float32]
"""Stem projected leaf area at canopy layer heights."""

self._calculate_canopy(community=community)

def _calculate_canopy(self, community: Community) -> None:
"""Calculate the canopy structure.

This private method runs the calculations needed to populate the instance
attributes.

Args:
community: The Community object passed to the instance.
"""

# Calculate community wide properties: total crown area, maximum height, crown
# area required to fill a layer and total number of canopy layers
self.total_community_crown_area = (
community.cohort_data["crown_area"] * community.cohort_data["n_individuals"]
).sum()

self.max_stem_height = community.cohort_data["stem_height"].max()

self.crown_area_per_layer = community.cell_area * (1 - self.canopy_gap_fraction)

self.n_layers = int(
np.ceil(self.total_community_crown_area / self.crown_area_per_layer)
)
self.n_cohorts = community.number_of_cohorts

# Find the closure heights of the canopy layers under the perfect plasticity
# approximation by solving Ac(z) - L_n = 0 across the community where L is the
# total cumulative crown area in layer n and above, discounted by the canopy gap
# fraction.

self.layer_heights = np.zeros((self.n_layers, 1), dtype=np.float32)

# Loop over the layers except for the final layer, which will be the partial
# remaining vegetation below the last closed layer.
starting_guess = self.max_stem_height
for layer in np.arange(self.n_layers - 1):
target_area = (layer + 1) * self.crown_area_per_layer

# TODO - the solution here is typically closer to the upper bracket, might
# be a better algorithm to find the root (#293).
solution = root_scalar(
solve_community_projected_canopy_area,
args=(
community.cohort_data["stem_height"],
community.cohort_data["crown_area"],
community.cohort_data["m"],
community.cohort_data["n"],
community.cohort_data["q_m"],
community.cohort_data["canopy_z_max"],
community.cohort_data["n_individuals"],
target_area,
False, # validate
),
bracket=(0, starting_guess),
xtol=self.layer_tolerance,
)

if not solution.converged:
raise RuntimeError(
"Estimation of canopy layer closure heights failed to converge."
)

self.layer_heights[layer] = starting_guess = solution.root

# Find relative canopy radius at the layer heights
# NOTE - here and in the calls below, validate=False is enforced because the
# Community class structures and code should guarantee valid inputs and so
# turning off the validation internally should simply speed up the code.
self.stem_relative_radius = calculate_relative_canopy_radius_at_z(
z=self.layer_heights,
stem_height=community.cohort_data["stem_height"],
m=community.cohort_data["m"],
n=community.cohort_data["n"],
validate=False,
)

# Calculate projected crown area of a cohort stem at canopy closure heights.
self.stem_crown_area = calculate_stem_projected_crown_area_at_z(
z=self.layer_heights,
q_z=self.stem_relative_radius,
crown_area=community.cohort_data["crown_area"],
stem_height=community.cohort_data["stem_height"],
q_m=community.cohort_data["q_m"],
z_max=community.cohort_data["canopy_z_max"],
validate=False,
)

# Find the projected leaf area of a cohort stem at canopy closure heights.
self.stem_leaf_area = calculate_stem_projected_leaf_area_at_z(
z=self.layer_heights,
q_z=self.stem_relative_radius,
crown_area=community.cohort_data["crown_area"],
stem_height=community.cohort_data["stem_height"],
f_g=community.cohort_data["f_g"],
q_m=community.cohort_data["q_m"],
z_max=community.cohort_data["canopy_z_max"],
validate=False,
)
Loading
Loading