From 9d725f68788ae617b34ac972b151a4e509687f58 Mon Sep 17 00:00:00 2001 From: David Orme Date: Fri, 27 Sep 2024 22:13:00 +0100 Subject: [PATCH 01/12] CrownProfile and calculate_canopy_radius --- pyrealm/demography/canopy_functions.py | 129 +++++++++++++++++++++---- 1 file changed, 111 insertions(+), 18 deletions(-) diff --git a/pyrealm/demography/canopy_functions.py b/pyrealm/demography/canopy_functions.py index 15eae194..7f22a83f 100644 --- a/pyrealm/demography/canopy_functions.py +++ b/pyrealm/demography/canopy_functions.py @@ -2,10 +2,14 @@ used in PlantFATE :cite:t:`joshi:2022a`. """ # noqa: D205 +from dataclasses import InitVar, dataclass, field + import numpy as np from numpy.typing import NDArray from pyrealm.core.utilities import check_input_shapes +from pyrealm.demography.flora import Flora, StemTraits +from pyrealm.demography.t_model_functions import StemAllometry def calculate_canopy_q_m( @@ -78,7 +82,7 @@ def calculate_canopy_z_max( def calculate_canopy_r0( q_m: NDArray[np.float32], crown_area: NDArray[np.float32] ) -> NDArray[np.float32]: - r"""Calculate scaling factor for height of maximum crown radius. + r"""Calculate scaling factor for width 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 @@ -228,6 +232,38 @@ def calculate_relative_canopy_radius_at_z( return m * n * z_over_height ** (n - 1) * (1 - z_over_height**n) ** (m - 1) +def calculate_canopy_radius( + q_z: NDArray[np.float32], + r0: NDArray[np.float32], + validate: bool = True, +) -> NDArray[np.float32]: + r"""Calculate canopy radius from relative radius and canopy r0. + + The relative canopy radius (:math:`q(z)`) at a given height :math:`z` describes the + vertical profile of the canopy shape, but only varies with the ``m`` and ``n`` shape + parameters and the stem height. The actual crown radius at a given height + (:math:`r(z)`) needs to be scaled using :math:`r_0` such that the maximum crown area + equals the expected crown area given the crown area ratio traiit for the plant + functional type: + + .. math:: + + r(z) = r_0 q(z) + + This function calculates :math:`r(z)` given estimated ``r0`` and an array of + relative radius values. + + Args: + q_z: TODO + r0: TODO + validate: Boolean flag to suppress argument validation. + """ + + # TODO - think about validation here. qz must be row array or 2D (N, n_pft) + + return r0 * q_z + + def calculate_stem_projected_crown_area_at_z( z: NDArray[np.float32], q_z: NDArray[np.float32], @@ -404,28 +440,85 @@ def calculate_stem_projected_leaf_area_at_z( return A_cp -# def calculate_total_canopy_A_cp(z: float, f_g: float, community: Community) -> float: -# """Calculate total leaf area at a given height. +@dataclass +class CrownProfile: + """Calculate vertical crown profiles for stems. -# :param f_g: -# :param community: -# :param z: Height above ground. -# :return: Total leaf area in the canopy at a given height. -# """ -# A_cp_for_individuals = calculate_projected_leaf_area_for_individuals( -# z, f_g, community -# ) + This method calculates canopy profile predictions, given an array of vertical + heights (``z``) for: -# A_cp_for_cohorts = A_cp_for_individuals * community.cohort_number_of_individuals + * relativate canopy radius, + * actual canopy radius, + * projected crown area, and + * project leaf area. -# return A_cp_for_cohorts.sum() + The predictions require a set of plant functional types (PFTs) but also the expected + allometric predictions of stem height, crown area and z_max for an actual stem of a + given size for each PFT. + Args: + stem_traits: + stem_allometry: A Ste + z: An array of vertical height values at which to calculate canopy profiles. + stem_height: A row array providing expected stem height for each PFT. + crown_area: A row array providing expected crown area for each PFT. + r0: A row array providing expected r0 for each PFT. + z_max: A row array providing expected z_max height for each PFT. + """ -# def calculate_gpp(cell_ppfd: NDArray, lue: NDArray) -> float: -# """Estimate the gross primary productivity. + stem_traits: InitVar[StemTraits | Flora] + """A Flora or StemTraits instance providing plant functional trait data.""" + stem_allometry: InitVar[StemAllometry] + """A StemAllometry instance setting the stem allometries for the crown profile.""" + z: InitVar[NDArray[np.float32]] + """An array of vertical height values at which to calculate canopy profiles.""" + + relativate_crown_radius: NDArray[np.float32] = field(init=False) + """An array of the relative crown radius of stems at z heights""" + crown_radius: NDArray[np.float32] = field(init=False) + """An array of the actual crown radius of stems at z heights""" + projected_crown_area: NDArray[np.float32] = field(init=False) + """An array of the projected crown area of stems at z heights""" + project_leaf_area: NDArray[np.float32] = field(init=False) + """An array of the projected leaf area of stems at z heights""" + + def __post_init__( + self, + stem_traits: StemTraits | Flora, + stem_allometry: StemAllometry, + z: NDArray[np.float32], + ) -> None: + """Populate canopy profile attributes from the traits, allometry and height.""" + # Calculate relative crown radius + self.relative_crown_radius = calculate_relative_canopy_radius_at_z( + z=z, + m=stem_traits.m, + n=stem_traits.n, + stem_height=stem_allometry.stem_height, + ) -# Not sure where to place this - need an array of LUE that matches to the + # Calculate actual radius + self.crown_radius = calculate_canopy_radius( + q_z=self.relative_crown_radius, r0=stem_allometry.canopy_r0 + ) -# """ + # Calculate projected crown area + self.projected_crown_area = calculate_stem_projected_crown_area_at_z( + z=z, + q_z=self.relativate_crown_radius, + crown_area=stem_allometry.crown_area, + q_m=stem_traits.q_m, + stem_height=stem_allometry.stem_height, + z_max=stem_allometry.canopy_z_max, + ) -# return 100 + # Calculate projected leaf area + self.projected_leaf_area = calculate_stem_projected_leaf_area_at_z( + z=z, + q_z=self.relative_crown_radius, + f_g=stem_traits.f_g, + q_m=stem_traits.q_m, + crown_area=stem_allometry.crown_area, + stem_height=stem_allometry.stem_height, + z_max=stem_allometry.canopy_z_max, + ) From 8e1076722ded2b479ea88ce3ff653a363667e392 Mon Sep 17 00:00:00 2001 From: David Orme Date: Fri, 27 Sep 2024 22:21:22 +0100 Subject: [PATCH 02/12] Canopy -> crown in functions and docstrings --- pyrealm/demography/canopy.py | 4 +- pyrealm/demography/canopy_functions.py | 94 +++++++++---------- pyrealm/demography/flora.py | 8 +- pyrealm/demography/t_model_functions.py | 8 +- .../unit/demography/test_canopy_functions.py | 44 ++++----- tests/unit/demography/test_flora.py | 24 ++--- 6 files changed, 91 insertions(+), 91 deletions(-) diff --git a/pyrealm/demography/canopy.py b/pyrealm/demography/canopy.py index 50958c79..b6aba29c 100644 --- a/pyrealm/demography/canopy.py +++ b/pyrealm/demography/canopy.py @@ -5,7 +5,7 @@ from scipy.optimize import root_scalar # type: ignore [import-untyped] from pyrealm.demography.canopy_functions import ( - calculate_relative_canopy_radius_at_z, + calculate_relative_crown_radius_at_z, calculate_stem_projected_crown_area_at_z, calculate_stem_projected_leaf_area_at_z, solve_community_projected_canopy_area, @@ -134,7 +134,7 @@ def _calculate_canopy(self, community: Community) -> None: # 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( + self.stem_relative_radius = calculate_relative_crown_radius_at_z( z=self.layer_heights, stem_height=community.stem_allometry.stem_height, m=community.stem_traits.m, diff --git a/pyrealm/demography/canopy_functions.py b/pyrealm/demography/canopy_functions.py index 7f22a83f..42cf02a5 100644 --- a/pyrealm/demography/canopy_functions.py +++ b/pyrealm/demography/canopy_functions.py @@ -1,4 +1,4 @@ -"""A set of functions implementing the canopy shape and vertical leaf distribution model +"""A set of functions implementing the crown shape and vertical leaf distribution model used in PlantFATE :cite:t:`joshi:2022a`. """ # noqa: D205 @@ -12,17 +12,17 @@ from pyrealm.demography.t_model_functions import StemAllometry -def calculate_canopy_q_m( +def calculate_crown_q_m( m: float | NDArray[np.float32], n: float | NDArray[np.float32] ) -> float | NDArray[np.float32]: - """Calculate the canopy scaling paramater ``q_m``. + """Calculate the crown scaling paramater ``q_m``. - The value of q_m is a constant canopy scaling parameter derived from the ``m`` and + The value of q_m is a constant crown scaling parameter derived from the ``m`` and ``n`` attributes defined for a plant functional type. Args: - m: Canopy shape parameter - n: Canopy shape parameter + m: Crown shape parameter + n: Crown shape parameter """ return ( m @@ -32,7 +32,7 @@ def calculate_canopy_q_m( ) -def calculate_canopy_z_max_proportion( +def calculate_crown_z_max_proportion( m: float | NDArray[np.float32], n: float | NDArray[np.float32] ) -> float | NDArray[np.float32]: r"""Calculate the z_m proportion. @@ -45,24 +45,24 @@ def calculate_canopy_z_max_proportion( p_{zm} = \left(\dfrac{n-1}{m n -1}\right)^ {\tfrac{1}{n}} Args: - m: Canopy shape parameter - n: Canopy shape parameter + m: Crown shape parameter + n: Crown shape parameter """ return ((n - 1) / (m * n - 1)) ** (1 / n) -def calculate_canopy_z_max( +def calculate_crown_z_max( z_max_prop: NDArray[np.float32], stem_height: NDArray[np.float32] ) -> NDArray[np.float32]: r"""Calculate height of maximum crown radius. - The height of the maximum crown radius (:math:`z_m`) is derived from the canopy + The height of the maximum crown radius (:math:`z_m`) is derived from the crown 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 + The value :math:`z_m` is the height above ground where the largest crown radius is found, given the proportion and the estimated stem height (:math:`H`) of individuals. @@ -71,7 +71,7 @@ def calculate_canopy_z_max( z_m = p_{zm} H Args: - z_max_prop: Canopy shape parameter of the PFT + z_max_prop: Crown shape parameter of the PFT stem_height: Stem height of individuals """ """Calculate z_m, the height of maximum crown radius.""" @@ -79,12 +79,12 @@ def calculate_canopy_z_max( return stem_height * z_max_prop -def calculate_canopy_r0( +def calculate_crown_r0( q_m: NDArray[np.float32], crown_area: NDArray[np.float32] ) -> NDArray[np.float32]: r"""Calculate scaling factor for width of maximum crown radius. - This scaling factor (:math:`r_0`) is derived from the canopy shape parameters + This scaling factor (:math:`r_0`) is derived from the crown 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 @@ -96,7 +96,7 @@ def calculate_canopy_r0( r_0 = 1/q_m \sqrt{A_c / \pi} Args: - q_m: Canopy shape parameter of the PFT + q_m: Crown shape parameter of the PFT crown_area: Crown area of individuals """ # Scaling factor to give expected A_c (crown area) at @@ -110,16 +110,16 @@ def _validate_z_qz_args( stem_properties: list[NDArray[np.float32]], q_z: NDArray[np.float32] | None = None, ) -> None: - """Shared validation of for canopy function arguments. + """Shared validation of for crown function arguments. - Several of the canopy functions in this module require a vertical height (``z``) - argument and, in some cases, the relative canopy radius (``q_z``) at that height. + Several of the crown functions in this module require a vertical height (``z``) + argument and, in some cases, the relative crown radius (``q_z``) at that height. These arguments need to have shapes that are congruent with each other and with the arrays providing stem properties for which values are calculated. This function provides the following validation checks (see also the documentation of accepted shapes for ``z`` in - :meth:`~pyrealm.demography.canopy_functions.calculate_relative_canopy_radius_at_z`). + :meth:`~pyrealm.demography.canopy_functions.calculate_relative_crown_radius_at_z`). * Stem properties are identically shaped row (1D) arrays. * The ``z`` argument is then one of: @@ -132,9 +132,9 @@ def _validate_z_qz_args( n_stem_properties``). Args: - z: An array input to the ``z`` argument of a canopy function. + z: An array input to the ``z`` argument of a crown function. stem_properties: A list of array inputs representing stem properties. - q_z: An optional array input to the ``q_z`` argument of a canopy function. + q_z: An optional array input to the ``q_z`` argument of a crown function. """ # Check the stem properties @@ -184,18 +184,18 @@ def _validate_z_qz_args( return -def calculate_relative_canopy_radius_at_z( +def calculate_relative_crown_radius_at_z( z: NDArray[np.float32], stem_height: NDArray[np.float32], m: NDArray[np.float32], n: NDArray[np.float32], validate: bool = True, ) -> NDArray[np.float32]: - r"""Calculate relative canopy radius at a given height. + r"""Calculate relative crown 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`: + The crown shape parameters ``m`` and ``n`` define the vertical distribution of + crown along the stem. For a stem of a given total height, this function calculates + the relative crown radius at a given height :math:`z`: .. math:: @@ -232,15 +232,15 @@ def calculate_relative_canopy_radius_at_z( return m * n * z_over_height ** (n - 1) * (1 - z_over_height**n) ** (m - 1) -def calculate_canopy_radius( +def calculate_crown_radius( q_z: NDArray[np.float32], r0: NDArray[np.float32], validate: bool = True, ) -> NDArray[np.float32]: - r"""Calculate canopy radius from relative radius and canopy r0. + r"""Calculate crown radius from relative crown radius and crown r0. - The relative canopy radius (:math:`q(z)`) at a given height :math:`z` describes the - vertical profile of the canopy shape, but only varies with the ``m`` and ``n`` shape + The relative crown radius (:math:`q(z)`) at a given height :math:`z` describes the + vertical profile of the crown shape, but only varies with the ``m`` and ``n`` shape parameters and the stem height. The actual crown radius at a given height (:math:`r(z)`) needs to be scaled using :math:`r_0` such that the maximum crown area equals the expected crown area given the crown area ratio traiit for the plant @@ -280,7 +280,7 @@ def calculate_stem_projected_crown_area_at_z( arguments ``stem_height``,``crown_area``,``q_m`` and ``z_max``, which must be one-dimensional arrays ('row vectors') of equal length. The array of vertical heights ``z`` accepts a range of input shapes (see - :meth:`~pyrealm.demography.canopy_functions.calculate_relative_canopy_radius_at_z` + :meth:`~pyrealm.demography.canopy_functions.calculate_relative_crown_radius_at_z` ) and this function then also requires the expected relative stem radius (``q_z``) calculated from those heights. @@ -322,14 +322,14 @@ def solve_community_projected_canopy_area( target_area: float = 0, validate: bool = True, ) -> NDArray[np.float32]: - """Solver function for community wide projected crown area. + """Solver function for community wide projected canopy area. This function takes the number of individuals in each cohort along with the stem height and crown area and a given vertical height (:math:`z`). It then uses the - canopy shape parameters associated with each cohort to calculate the community wide + crown shape parameters associated with each cohort to calculate the community wide projected crown area above that height (:math:`A_p(z)`). This is simply the sum of - the products of the individual stem projected area at :math:`z` and the number of - individuals in each cohort. + the products of the individual stem crown projected area at :math:`z` and the number + of individuals in each cohort. The return value is the difference between the calculated :math:`A_p(z)` and a user-specified target area, This allows the function to be used with a root solver @@ -346,10 +346,10 @@ def solve_community_projected_canopy_area( n_individuals: Number of individuals in each cohort crown_area: Crown area of each cohort stem_height: Stem height of each cohort - m: Canopy shape parameter ``m``` for each cohort - n: Canopy shape parameter ``n``` for each cohort - q_m: Canopy shape parameter ``q_m``` for each cohort - z_max: Canopy shape parameter ``z_m``` for each cohort + m: Crown shape parameter ``m``` for each cohort + n: Crown shape parameter ``n``` for each cohort + q_m: Crown shape parameter ``q_m``` for each cohort + z_max: Crown shape parameter ``z_m``` for each cohort target_area: A target projected crown area. validate: Boolean flag to suppress argument validation. """ @@ -362,7 +362,7 @@ def solve_community_projected_canopy_area( stem_properties=[n_individuals, crown_area, stem_height, m, n, q_m, z_max], ) - q_z = calculate_relative_canopy_radius_at_z( + q_z = calculate_relative_crown_radius_at_z( z=z_arr, stem_height=stem_height, m=m, n=n, validate=False ) # Calculate A(p) for the stems in each cohort @@ -394,14 +394,14 @@ def calculate_stem_projected_leaf_area_at_z( This function calculates the projected leaf area of a set of stems with given properties at a set of vertical heights. This differs from crown area in allowing for crown openness within the crown of an individual stem that results in the - displacement of leaf area further down into the canopy. The degree of openness is + displacement of leaf area further down into the crown. The degree of openness is controlled by the crown gap fraction property of each stem. The stem properties are given in the arguments ``stem_height``,``crown_area``,``f_g``,``q_m`` and ``z_max``, which must be one-dimensional arrays ('row vectors') of equal length. The array of vertical heights ``z`` accepts a range of input shapes (see - :meth:`~pyrealm.demography.canopy_functions.calculate_relative_canopy_radius_at_z` + :meth:`~pyrealm.demography.canopy_functions.calculate_relative_crown_radius_at_z` ) and this function then also requires the expected relative stem radius (``q_z``) calculated from those heights. @@ -412,13 +412,13 @@ def calculate_stem_projected_leaf_area_at_z( stem_height: Total height of a stem f_g: Within crown gap fraction for each stem. q_m: Canopy shape parameter ``q_m``` for each stem - z_max: Height of maximum canopy radius for each stem + z_max: Height of maximum crown radius for each stem validate: Boolean flag to suppress argument validation. """ # NOTE: Although the internals of this function overlap a lot with # calculate_stem_projected_crown_area_at_z, we want that function to be as - # lean as possible, as it used within solve_community_projected_canopy_area. + # lean as possible, as it used within solve_community_projected_crown_area. if validate: _validate_z_qz_args( @@ -490,7 +490,7 @@ def __post_init__( ) -> None: """Populate canopy profile attributes from the traits, allometry and height.""" # Calculate relative crown radius - self.relative_crown_radius = calculate_relative_canopy_radius_at_z( + self.relative_crown_radius = calculate_relative_crown_radius_at_z( z=z, m=stem_traits.m, n=stem_traits.n, @@ -498,7 +498,7 @@ def __post_init__( ) # Calculate actual radius - self.crown_radius = calculate_canopy_radius( + self.crown_radius = calculate_crown_radius( q_z=self.relative_crown_radius, r0=stem_allometry.canopy_r0 ) diff --git a/pyrealm/demography/flora.py b/pyrealm/demography/flora.py index e8ec5c2e..8dadc788 100644 --- a/pyrealm/demography/flora.py +++ b/pyrealm/demography/flora.py @@ -34,8 +34,8 @@ from numpy.typing import NDArray from pyrealm.demography.canopy_functions import ( - calculate_canopy_q_m, - calculate_canopy_z_max_proportion, + calculate_crown_q_m, + calculate_crown_z_max_proportion, ) if sys.version_info[:2] >= (3, 11): @@ -122,9 +122,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_canopy_q_m(m=self.m, n=self.n)) + object.__setattr__(self, "q_m", calculate_crown_q_m(m=self.m, n=self.n)) object.__setattr__( - self, "z_max_prop", calculate_canopy_z_max_proportion(m=self.m, n=self.n) + self, "z_max_prop", calculate_crown_z_max_proportion(m=self.m, n=self.n) ) diff --git a/pyrealm/demography/t_model_functions.py b/pyrealm/demography/t_model_functions.py index 23101d4b..096f5ae6 100644 --- a/pyrealm/demography/t_model_functions.py +++ b/pyrealm/demography/t_model_functions.py @@ -13,8 +13,8 @@ from pyrealm.core.utilities import check_input_shapes from pyrealm.demography.canopy_functions import ( - calculate_canopy_r0, - calculate_canopy_z_max, + calculate_crown_r0, + calculate_crown_z_max, ) from pyrealm.demography.flora import Flora, StemTraits @@ -715,12 +715,12 @@ def __post_init__( crown_fraction=self.crown_fraction, ) - self.canopy_r0 = calculate_canopy_r0( + self.canopy_r0 = calculate_crown_r0( q_m=stem_traits.q_m, crown_area=self.crown_area, ) - self.canopy_z_max = calculate_canopy_z_max( + self.canopy_z_max = calculate_crown_z_max( z_max_prop=stem_traits.z_max_prop, stem_height=self.stem_height, ) diff --git a/tests/unit/demography/test_canopy_functions.py b/tests/unit/demography/test_canopy_functions.py index 7681fa87..0cd178d6 100644 --- a/tests/unit/demography/test_canopy_functions.py +++ b/tests/unit/demography/test_canopy_functions.py @@ -41,24 +41,24 @@ def fixture_community(): ) -def test_calculate_canopy_q_m(fixture_canopy_shape): - """Test calculate_canopy_q_m.""" +def test_calculate_crown_q_m(fixture_canopy_shape): + """Test calculate_crown_q_m.""" - from pyrealm.demography.canopy_functions import calculate_canopy_q_m + from pyrealm.demography.canopy_functions import calculate_crown_q_m - actual_q_m_values = calculate_canopy_q_m( + actual_q_m_values = calculate_crown_q_m( m=fixture_canopy_shape["m"], n=fixture_canopy_shape["n"] ) assert np.allclose(actual_q_m_values, fixture_canopy_shape["q_m"]) -def test_calculate_canopy_z_max_proportion(fixture_canopy_shape): - """Test calculate_canopy_z_max_proportion.""" +def test_calculate_crown_z_max_proportion(fixture_canopy_shape): + """Test calculate_crown_z_max_proportion.""" - from pyrealm.demography.canopy_functions import calculate_canopy_z_max_proportion + from pyrealm.demography.canopy_functions import calculate_crown_z_max_proportion - actual_p_zm = calculate_canopy_z_max_proportion( + actual_p_zm = calculate_crown_z_max_proportion( m=fixture_canopy_shape["m"], n=fixture_canopy_shape["n"] ) @@ -75,9 +75,9 @@ def test_calculate_canopy_z_max_proportion(fixture_canopy_shape): def test_calculate_r_0_values(fixture_canopy_shape, crown_areas, expected_r0): """Test happy path for calculating r_0.""" - from pyrealm.demography.canopy_functions import calculate_canopy_r0 + from pyrealm.demography.canopy_functions import calculate_crown_r0 - actual_r0_values = calculate_canopy_r0( + actual_r0_values = calculate_crown_r0( q_m=fixture_canopy_shape["q_m"], crown_area=crown_areas ) @@ -361,14 +361,14 @@ def test__validate_z_qz__args(fixture_z_qz_stem_properties): ], indirect=["fixture_z_qz_stem_properties"], ) -def test_calculate_relative_canopy_radius_at_z_inputs(fixture_z_qz_stem_properties): - """Test calculate_relative_canopy_radius_at_z input and output shapes . +def test_calculate_relative_crown_radius_at_z_inputs(fixture_z_qz_stem_properties): + """Test calculate_relative_crown_radius_at_z input and output shapes . This test checks the function behaviour with different inputs. """ from pyrealm.demography.canopy_functions import ( - calculate_relative_canopy_radius_at_z, + calculate_relative_crown_radius_at_z, ) # Build inputs @@ -377,7 +377,7 @@ def test_calculate_relative_canopy_radius_at_z_inputs(fixture_z_qz_stem_properti with outcome as excep: # Get the relative radius at that height - q_z_values = calculate_relative_canopy_radius_at_z(z, *stem_args) + q_z_values = calculate_relative_crown_radius_at_z(z, *stem_args) if isinstance(outcome, does_not_raise): assert q_z_values.shape == out_shape @@ -387,8 +387,8 @@ def test_calculate_relative_canopy_radius_at_z_inputs(fixture_z_qz_stem_properti assert str(excep.value).startswith(excep_msg) -def test_calculate_relative_canopy_radius_at_z_values(fixture_community): - """Test calculate_relative_canopy_radius_at_z. +def test_calculate_relative_crown_radius_at_z_values(fixture_community): + """Test calculate_relative_crown_radius_at_z. This test validates the expectation that the canopy shape model correctly predicts the crown area from the T Model equations at the predicted height of @@ -396,7 +396,7 @@ def test_calculate_relative_canopy_radius_at_z_values(fixture_community): """ from pyrealm.demography.canopy_functions import ( - calculate_relative_canopy_radius_at_z, + calculate_relative_crown_radius_at_z, ) # Canopy shape model gives the maximum radius at a height z_max @@ -406,7 +406,7 @@ def test_calculate_relative_canopy_radius_at_z_values(fixture_community): ) # Get the relative radius at that height - q_z_values = calculate_relative_canopy_radius_at_z( + q_z_values = calculate_relative_crown_radius_at_z( z=z_max, stem_height=fixture_community.stem_allometry.stem_height, m=fixture_community.stem_traits.m, @@ -502,12 +502,12 @@ def test_calculate_stem_projected_crown_area_at_z_values( """ from pyrealm.demography.canopy_functions import ( - calculate_relative_canopy_radius_at_z, + calculate_relative_crown_radius_at_z, calculate_stem_projected_crown_area_at_z, ) # Calculate the required q_z - q_z = calculate_relative_canopy_radius_at_z( + q_z = calculate_relative_crown_radius_at_z( z=heights, stem_height=fixture_community.stem_allometry.stem_height, m=fixture_community.stem_traits.m, @@ -617,7 +617,7 @@ def test_calculate_stem_projected_leaf_area_at_z_values(fixture_community): """ from pyrealm.demography.canopy_functions import ( - calculate_relative_canopy_radius_at_z, + calculate_relative_crown_radius_at_z, calculate_stem_projected_leaf_area_at_z, ) @@ -625,7 +625,7 @@ def test_calculate_stem_projected_leaf_area_at_z_values(fixture_community): # to the highest z_max = fixture_community.stem_allometry.canopy_z_max[:, None] - q_z = calculate_relative_canopy_radius_at_z( + q_z = calculate_relative_crown_radius_at_z( z=z_max, stem_height=fixture_community.stem_allometry.stem_height, m=fixture_community.stem_traits.m, diff --git a/tests/unit/demography/test_flora.py b/tests/unit/demography/test_flora.py index bf82be7f..74e93145 100644 --- a/tests/unit/demography/test_flora.py +++ b/tests/unit/demography/test_flora.py @@ -56,8 +56,8 @@ def test_PlantFunctionalTypeStrict__init__(args, outcome): from pyrealm.demography.flora import ( PlantFunctionalTypeStrict, - calculate_canopy_q_m, - calculate_canopy_z_max_proportion, + calculate_crown_q_m, + calculate_crown_z_max_proportion, ) with outcome: @@ -67,8 +67,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_canopy_q_m(m=2, n=5) - assert pft.z_max_prop == calculate_canopy_z_max_proportion(m=2, n=5) + assert pft.q_m == calculate_crown_q_m(m=2, n=5) + assert pft.z_max_prop == calculate_crown_z_max_proportion(m=2, n=5) # @@ -88,8 +88,8 @@ def test_PlantFunctionalType__init__(args, outcome): from pyrealm.demography.flora import ( PlantFunctionalType, - calculate_canopy_q_m, - calculate_canopy_z_max_proportion, + calculate_crown_q_m, + calculate_crown_z_max_proportion, ) with outcome: @@ -99,8 +99,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_canopy_q_m(m=2, n=5) - assert pft.z_max_prop == calculate_canopy_z_max_proportion(m=2, n=5) + assert pft.q_m == calculate_crown_q_m(m=2, n=5) + assert pft.z_max_prop == calculate_crown_z_max_proportion(m=2, n=5) # @@ -115,9 +115,9 @@ def test_PlantFunctionalType__init__(args, outcome): def test_pft_calculate_q_m(m, n, q_m): """Test calculation of q_m.""" - from pyrealm.demography.flora import calculate_canopy_q_m + from pyrealm.demography.flora import calculate_crown_q_m - calculated_q_m = calculate_canopy_q_m(m, n) + calculated_q_m = calculate_crown_q_m(m, n) assert calculated_q_m == pytest.approx(q_m) @@ -138,9 +138,9 @@ def test_calculate_q_m_values_raises_exception_for_invalid_input(): def test_pft_calculate_z_max_ratio(m, n, z_max_ratio): """Test calculation of z_max proportion.""" - from pyrealm.demography.flora import calculate_canopy_z_max_proportion + from pyrealm.demography.flora import calculate_crown_z_max_proportion - calculated_zmr = calculate_canopy_z_max_proportion(m, n) + calculated_zmr = calculate_crown_z_max_proportion(m, n) assert calculated_zmr == pytest.approx(z_max_ratio) From 8dfdfd528d92cfd791a2b4412e9aac035aa1b760 Mon Sep 17 00:00:00 2001 From: David Orme Date: Fri, 27 Sep 2024 22:23:04 +0100 Subject: [PATCH 03/12] More canopy -> crown --- pyrealm/demography/canopy.py | 6 +++--- pyrealm/demography/canopy_functions.py | 18 +++++++++--------- pyrealm/demography/t_model_functions.py | 14 +++++++------- tests/unit/demography/test_canopy_functions.py | 14 +++++++------- .../unit/demography/test_t_model_functions.py | 2 +- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/pyrealm/demography/canopy.py b/pyrealm/demography/canopy.py index b6aba29c..91694326 100644 --- a/pyrealm/demography/canopy.py +++ b/pyrealm/demography/canopy.py @@ -114,7 +114,7 @@ def _calculate_canopy(self, community: Community) -> None: community.stem_traits.m, community.stem_traits.n, community.stem_traits.q_m, - community.stem_allometry.canopy_z_max, + community.stem_allometry.crown_z_max, community.cohort_data["n_individuals"], target_area, False, # validate @@ -149,7 +149,7 @@ def _calculate_canopy(self, community: Community) -> None: crown_area=community.stem_allometry.crown_area, stem_height=community.stem_allometry.stem_height, q_m=community.stem_traits.q_m, - z_max=community.stem_allometry.canopy_z_max, + z_max=community.stem_allometry.crown_z_max, validate=False, ) @@ -161,6 +161,6 @@ def _calculate_canopy(self, community: Community) -> None: stem_height=community.stem_allometry.stem_height, f_g=community.stem_traits.f_g, q_m=community.stem_traits.q_m, - z_max=community.stem_allometry.canopy_z_max, + z_max=community.stem_allometry.crown_z_max, validate=False, ) diff --git a/pyrealm/demography/canopy_functions.py b/pyrealm/demography/canopy_functions.py index 42cf02a5..65cf2d38 100644 --- a/pyrealm/demography/canopy_functions.py +++ b/pyrealm/demography/canopy_functions.py @@ -444,11 +444,11 @@ def calculate_stem_projected_leaf_area_at_z( class CrownProfile: """Calculate vertical crown profiles for stems. - This method calculates canopy profile predictions, given an array of vertical + This method calculates crown profile predictions, given an array of vertical heights (``z``) for: - * relativate canopy radius, - * actual canopy radius, + * relativate crown radius, + * actual crown radius, * projected crown area, and * project leaf area. @@ -459,7 +459,7 @@ class CrownProfile: Args: stem_traits: stem_allometry: A Ste - z: An array of vertical height values at which to calculate canopy profiles. + z: An array of vertical height values at which to calculate crown profiles. stem_height: A row array providing expected stem height for each PFT. crown_area: A row array providing expected crown area for each PFT. r0: A row array providing expected r0 for each PFT. @@ -471,7 +471,7 @@ class CrownProfile: stem_allometry: InitVar[StemAllometry] """A StemAllometry instance setting the stem allometries for the crown profile.""" z: InitVar[NDArray[np.float32]] - """An array of vertical height values at which to calculate canopy profiles.""" + """An array of vertical height values at which to calculate crown profiles.""" relativate_crown_radius: NDArray[np.float32] = field(init=False) """An array of the relative crown radius of stems at z heights""" @@ -488,7 +488,7 @@ def __post_init__( stem_allometry: StemAllometry, z: NDArray[np.float32], ) -> None: - """Populate canopy profile attributes from the traits, allometry and height.""" + """Populate crown profile attributes from the traits, allometry and height.""" # Calculate relative crown radius self.relative_crown_radius = calculate_relative_crown_radius_at_z( z=z, @@ -499,7 +499,7 @@ def __post_init__( # Calculate actual radius self.crown_radius = calculate_crown_radius( - q_z=self.relative_crown_radius, r0=stem_allometry.canopy_r0 + q_z=self.relative_crown_radius, r0=stem_allometry.crown_r0 ) # Calculate projected crown area @@ -509,7 +509,7 @@ def __post_init__( crown_area=stem_allometry.crown_area, q_m=stem_traits.q_m, stem_height=stem_allometry.stem_height, - z_max=stem_allometry.canopy_z_max, + z_max=stem_allometry.crown_z_max, ) # Calculate projected leaf area @@ -520,5 +520,5 @@ def __post_init__( q_m=stem_traits.q_m, crown_area=stem_allometry.crown_area, stem_height=stem_allometry.stem_height, - z_max=stem_allometry.canopy_z_max, + z_max=stem_allometry.crown_z_max, ) diff --git a/pyrealm/demography/t_model_functions.py b/pyrealm/demography/t_model_functions.py index 096f5ae6..b4a78f4d 100644 --- a/pyrealm/demography/t_model_functions.py +++ b/pyrealm/demography/t_model_functions.py @@ -635,8 +635,8 @@ class StemAllometry: "stem_mass", "foliage_mass", "sapwood_mass", - "canopy_r0", - "canopy_z_max", + "crown_r0", + "crown_z_max", ) # Init vars @@ -663,9 +663,9 @@ class StemAllometry: """Foliage mass (kg)""" sapwood_mass: NDArray[np.float32] = field(init=False) """Sapwood mass (kg)""" - canopy_r0: NDArray[np.float32] = field(init=False) - """Canopy radius scaling factor (-)""" - canopy_z_max: NDArray[np.float32] = field(init=False) + crown_r0: NDArray[np.float32] = field(init=False) + """Crown radius scaling factor (-)""" + crown_z_max: NDArray[np.float32] = field(init=False) """Height of maximum crown radius (metres)""" def __post_init__( @@ -715,12 +715,12 @@ def __post_init__( crown_fraction=self.crown_fraction, ) - self.canopy_r0 = calculate_crown_r0( + self.crown_r0 = calculate_crown_r0( q_m=stem_traits.q_m, crown_area=self.crown_area, ) - self.canopy_z_max = calculate_crown_z_max( + self.crown_z_max = calculate_crown_z_max( z_max_prop=stem_traits.z_max_prop, stem_height=self.stem_height, ) diff --git a/tests/unit/demography/test_canopy_functions.py b/tests/unit/demography/test_canopy_functions.py index 0cd178d6..2d93576f 100644 --- a/tests/unit/demography/test_canopy_functions.py +++ b/tests/unit/demography/test_canopy_functions.py @@ -417,7 +417,7 @@ def test_calculate_relative_crown_radius_at_z_values(fixture_community): # prediction from the T model allometric equations. assert np.allclose( fixture_community.stem_allometry.crown_area, - np.pi * (q_z_values * fixture_community.stem_allometry.canopy_r0) ** 2, + np.pi * (q_z_values * fixture_community.stem_allometry.crown_r0) ** 2, ) @@ -521,7 +521,7 @@ def test_calculate_stem_projected_crown_area_at_z_values( stem_height=fixture_community.stem_allometry.stem_height, crown_area=fixture_community.stem_allometry.crown_area, q_m=fixture_community.stem_traits.q_m, - z_max=fixture_community.stem_allometry.canopy_z_max, + z_max=fixture_community.stem_allometry.crown_z_max, ) assert np.allclose( @@ -548,7 +548,7 @@ def test_solve_community_projected_canopy_area(fixture_community): this_height, this_target, ) in zip( - np.flip(fixture_community.stem_allometry.canopy_z_max), + np.flip(fixture_community.stem_allometry.crown_z_max), np.cumsum(np.flip(fixture_community.stem_allometry.crown_area)), ): solved = solve_community_projected_canopy_area( @@ -559,7 +559,7 @@ def test_solve_community_projected_canopy_area(fixture_community): m=fixture_community.stem_traits.m, n=fixture_community.stem_traits.n, q_m=fixture_community.stem_traits.q_m, - z_max=fixture_community.stem_allometry.canopy_z_max, + z_max=fixture_community.stem_allometry.crown_z_max, target_area=this_target, ) @@ -623,7 +623,7 @@ def test_calculate_stem_projected_leaf_area_at_z_values(fixture_community): # Calculate the leaf areas at the locations of z_max for each stem from the lowest # to the highest - z_max = fixture_community.stem_allometry.canopy_z_max[:, None] + z_max = fixture_community.stem_allometry.crown_z_max[:, None] q_z = calculate_relative_crown_radius_at_z( z=z_max, @@ -639,7 +639,7 @@ def test_calculate_stem_projected_leaf_area_at_z_values(fixture_community): crown_area=fixture_community.stem_allometry.crown_area, f_g=fixture_community.stem_traits.f_g, q_m=fixture_community.stem_traits.q_m, - z_max=fixture_community.stem_allometry.canopy_z_max, + z_max=fixture_community.stem_allometry.crown_z_max, ) # Pre-calculated values @@ -673,7 +673,7 @@ def test_calculate_stem_projected_leaf_area_at_z_values(fixture_community): crown_area=fixture_community.stem_allometry.crown_area, f_g=fixture_community.stem_traits.f_g, q_m=fixture_community.stem_traits.q_m, - z_max=fixture_community.stem_allometry.canopy_z_max, + z_max=fixture_community.stem_allometry.crown_z_max, ) expected_leaf_area_fg002 = np.array( diff --git a/tests/unit/demography/test_t_model_functions.py b/tests/unit/demography/test_t_model_functions.py index 0cb420f8..f183662a 100644 --- a/tests/unit/demography/test_t_model_functions.py +++ b/tests/unit/demography/test_t_model_functions.py @@ -722,7 +722,7 @@ def test_StemAllometry(rtmodel_flora, rtmodel_data): vars_to_check = ( v for v in stem_allometry.allometry_attrs - if v not in ["canopy_r0", "canopy_z_max"] + if v not in ["crown_r0", "crown_z_max"] ) for var in vars_to_check: assert np.allclose(getattr(stem_allometry, var), rtmodel_data[var]) From de412677f96ef542eec65459e2cefca1f75e8fac Mon Sep 17 00:00:00 2001 From: David Orme Date: Fri, 27 Sep 2024 22:28:17 +0100 Subject: [PATCH 04/12] Move crown trait functions for crown_qm and crown_z_max into flora module --- pyrealm/demography/canopy_functions.py | 40 ----------------------- pyrealm/demography/flora.py | 45 +++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 45 deletions(-) diff --git a/pyrealm/demography/canopy_functions.py b/pyrealm/demography/canopy_functions.py index 65cf2d38..18face46 100644 --- a/pyrealm/demography/canopy_functions.py +++ b/pyrealm/demography/canopy_functions.py @@ -12,46 +12,6 @@ from pyrealm.demography.t_model_functions import StemAllometry -def calculate_crown_q_m( - m: float | NDArray[np.float32], n: float | NDArray[np.float32] -) -> float | NDArray[np.float32]: - """Calculate the crown scaling paramater ``q_m``. - - The value of q_m is a constant crown scaling parameter derived from the ``m`` and - ``n`` attributes defined for a plant functional type. - - Args: - m: Crown shape parameter - n: Crown shape parameter - """ - return ( - m - * n - * ((n - 1) / (m * n - 1)) ** (1 - 1 / n) - * (((m - 1) * n) / (m * n - 1)) ** (m - 1) - ) - - -def calculate_crown_z_max_proportion( - m: float | NDArray[np.float32], n: float | NDArray[np.float32] -) -> float | NDArray[np.float32]: - 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: Crown shape parameter - n: Crown shape parameter - """ - - return ((n - 1) / (m * n - 1)) ** (1 / n) - - def calculate_crown_z_max( z_max_prop: NDArray[np.float32], stem_height: NDArray[np.float32] ) -> NDArray[np.float32]: diff --git a/pyrealm/demography/flora.py b/pyrealm/demography/flora.py index 8dadc788..b77d4ea8 100644 --- a/pyrealm/demography/flora.py +++ b/pyrealm/demography/flora.py @@ -33,11 +33,6 @@ from marshmallow.exceptions import ValidationError from numpy.typing import NDArray -from pyrealm.demography.canopy_functions import ( - calculate_crown_q_m, - calculate_crown_z_max_proportion, -) - if sys.version_info[:2] >= (3, 11): import tomllib from tomllib import TOMLDecodeError @@ -46,6 +41,46 @@ from tomli import TOMLDecodeError +def calculate_crown_q_m( + m: float | NDArray[np.float32], n: float | NDArray[np.float32] +) -> float | NDArray[np.float32]: + """Calculate the crown scaling trait ``q_m``. + + The value of q_m is a constant crown scaling parameter derived from the ``m`` and + ``n`` attributes defined for a plant functional type. + + Args: + m: Crown shape parameter + n: Crown shape parameter + """ + return ( + m + * n + * ((n - 1) / (m * n - 1)) ** (1 - 1 / n) + * (((m - 1) * n) / (m * n - 1)) ** (m - 1) + ) + + +def calculate_crown_z_max_proportion( + m: float | NDArray[np.float32], n: float | NDArray[np.float32] +) -> float | NDArray[np.float32]: + r"""Calculate the z_m trait. + + 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: Crown shape parameter + n: Crown shape parameter + """ + + return ((n - 1) / (m * n - 1)) ** (1 / n) + + @dataclass(frozen=True) class PlantFunctionalTypeStrict: """The PlantFunctionalTypeStrict dataclass. From e3800db31996ffbc3b6cb90105587e84f53eeee4 Mon Sep 17 00:00:00 2001 From: David Orme Date: Fri, 27 Sep 2024 22:31:18 +0100 Subject: [PATCH 05/12] Move crown_r0 and crown_zmax allometry functions into t_model_functions --- pyrealm/demography/canopy_functions.py | 53 ----------------------- pyrealm/demography/t_model_functions.py | 57 +++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 57 deletions(-) diff --git a/pyrealm/demography/canopy_functions.py b/pyrealm/demography/canopy_functions.py index 18face46..7461cff2 100644 --- a/pyrealm/demography/canopy_functions.py +++ b/pyrealm/demography/canopy_functions.py @@ -12,59 +12,6 @@ from pyrealm.demography.t_model_functions import StemAllometry -def calculate_crown_z_max( - z_max_prop: NDArray[np.float32], stem_height: NDArray[np.float32] -) -> NDArray[np.float32]: - r"""Calculate height of maximum crown radius. - - The height of the maximum crown radius (:math:`z_m`) is derived from the crown - 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 crown 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: Crown shape parameter of the PFT - stem_height: Stem height of individuals - """ - """Calculate z_m, the height of maximum crown radius.""" - - return stem_height * z_max_prop - - -def calculate_crown_r0( - q_m: NDArray[np.float32], crown_area: NDArray[np.float32] -) -> NDArray[np.float32]: - r"""Calculate scaling factor for width of maximum crown radius. - - This scaling factor (:math:`r_0`) is derived from the crown 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: Crown 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 _validate_z_qz_args( z: NDArray[np.float32], stem_properties: list[NDArray[np.float32]], diff --git a/pyrealm/demography/t_model_functions.py b/pyrealm/demography/t_model_functions.py index b4a78f4d..fa1be81d 100644 --- a/pyrealm/demography/t_model_functions.py +++ b/pyrealm/demography/t_model_functions.py @@ -12,10 +12,6 @@ from numpy.typing import NDArray from pyrealm.core.utilities import check_input_shapes -from pyrealm.demography.canopy_functions import ( - calculate_crown_r0, - calculate_crown_z_max, -) from pyrealm.demography.flora import Flora, StemTraits @@ -290,6 +286,59 @@ def calculate_sapwood_masses( return crown_area * rho_s * stem_height * (1 - crown_fraction / 2) / ca_ratio +def calculate_crown_z_max( + z_max_prop: NDArray[np.float32], stem_height: NDArray[np.float32] +) -> NDArray[np.float32]: + r"""Calculate height of maximum crown radius. + + The height of the maximum crown radius (:math:`z_m`) is derived from the crown + 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 crown 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: Crown shape parameter of the PFT + stem_height: Stem height of individuals + """ + """Calculate z_m, the height of maximum crown radius.""" + + return stem_height * z_max_prop + + +def calculate_crown_r0( + q_m: NDArray[np.float32], crown_area: NDArray[np.float32] +) -> NDArray[np.float32]: + r"""Calculate scaling factor for width of maximum crown radius. + + This scaling factor (:math:`r_0`) is derived from the crown 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: Crown 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_whole_crown_gpp( potential_gpp: NDArray[np.float32], crown_area: NDArray[np.float32], From 9073becbb2de47dc9f3fe4589ddc17af7167dcc4 Mon Sep 17 00:00:00 2001 From: David Orme Date: Fri, 27 Sep 2024 22:47:47 +0100 Subject: [PATCH 06/12] Fixing tests --- .../unit/demography/test_canopy_functions.py | 58 ------------------- tests/unit/demography/test_flora.py | 35 +++++++---- .../unit/demography/test_t_model_functions.py | 18 ++++++ 3 files changed, 41 insertions(+), 70 deletions(-) diff --git a/tests/unit/demography/test_canopy_functions.py b/tests/unit/demography/test_canopy_functions.py index 2d93576f..49e51b96 100644 --- a/tests/unit/demography/test_canopy_functions.py +++ b/tests/unit/demography/test_canopy_functions.py @@ -7,21 +7,6 @@ import pytest -@pytest.fixture -def fixture_canopy_shape(): - """Fixture providing input and expected values for shape parameter calculations. - - These are hand calculated and only really test that the calculations haven't changed - from the initial implementation. - """ - return { - "m": np.array([2, 3]), - "n": np.array([5, 4]), - "q_m": np.array([2.9038988210485766, 2.3953681843215673]), - "p_zm": np.array([0.850283, 0.72265688]), - } - - @pytest.fixture def fixture_community(): """A fixture providing a simple community.""" @@ -41,49 +26,6 @@ def fixture_community(): ) -def test_calculate_crown_q_m(fixture_canopy_shape): - """Test calculate_crown_q_m.""" - - from pyrealm.demography.canopy_functions import calculate_crown_q_m - - actual_q_m_values = calculate_crown_q_m( - m=fixture_canopy_shape["m"], n=fixture_canopy_shape["n"] - ) - - assert np.allclose(actual_q_m_values, fixture_canopy_shape["q_m"]) - - -def test_calculate_crown_z_max_proportion(fixture_canopy_shape): - """Test calculate_crown_z_max_proportion.""" - - from pyrealm.demography.canopy_functions import calculate_crown_z_max_proportion - - actual_p_zm = calculate_crown_z_max_proportion( - m=fixture_canopy_shape["m"], n=fixture_canopy_shape["n"] - ) - - assert np.allclose(actual_p_zm, fixture_canopy_shape["p_zm"]) - - -@pytest.mark.parametrize( - argnames="crown_areas, expected_r0", - argvalues=( - (np.array([20, 30]), np.array([0.86887756, 1.29007041])), - (np.array([30, 40]), np.array([1.06415334, 1.489645])), - ), -) -def test_calculate_r_0_values(fixture_canopy_shape, crown_areas, expected_r0): - """Test happy path for calculating r_0.""" - - from pyrealm.demography.canopy_functions import calculate_crown_r0 - - actual_r0_values = calculate_crown_r0( - q_m=fixture_canopy_shape["q_m"], crown_area=crown_areas - ) - - assert np.allclose(actual_r0_values, expected_r0) - - ZQZInput = namedtuple( "ZQZInput", ["z", "stem", "more_stem", "q_z", "outcome", "excep_msg", "output_shape"], diff --git a/tests/unit/demography/test_flora.py b/tests/unit/demography/test_flora.py index 74e93145..d9959350 100644 --- a/tests/unit/demography/test_flora.py +++ b/tests/unit/demography/test_flora.py @@ -5,6 +5,7 @@ from importlib import resources from json import JSONDecodeError +import numpy as np import pytest from marshmallow.exceptions import ValidationError from pandas.errors import ParserError @@ -104,17 +105,29 @@ def test_PlantFunctionalType__init__(args, outcome): # -# Test PlantFunctionalType __post_init__ functions +# Test PlantFunctionalType __post_init__ trait calculation functions # -@pytest.mark.parametrize( - argnames="m,n,q_m", - argvalues=[(2, 5, 2.9038988210485766), (3, 4, 2.3953681843215673)], -) -def test_pft_calculate_q_m(m, n, q_m): +@pytest.fixture +def fixture_crown_shape(): + """Fixture of input and expected values for crown shape parameter calculations. + + These are hand calculated and only really test that the calculations haven't changed + from the initial implementation. + """ + return ( + np.array([2, 3]), # m + np.array([5, 4]), # n + np.array([2.9038988210485766, 2.3953681843215673]), # q_m + np.array([0.850283, 0.72265688]), # p_zm + ) + + +def test_pft_calculate_q_m(fixture_crown_shape): """Test calculation of q_m.""" + m, n, q_m, _ = fixture_crown_shape from pyrealm.demography.flora import calculate_crown_q_m calculated_q_m = calculate_crown_q_m(m, n) @@ -131,17 +144,15 @@ def test_calculate_q_m_values_raises_exception_for_invalid_input(): pass -@pytest.mark.parametrize( - argnames="m,n,z_max_ratio", - argvalues=[(2, 5, 0.8502830004171938), (3, 4, 0.7226568811456053)], -) -def test_pft_calculate_z_max_ratio(m, n, z_max_ratio): +def test_pft_calculate_z_max_ratio(fixture_crown_shape): """Test calculation of z_max proportion.""" from pyrealm.demography.flora import calculate_crown_z_max_proportion + m, n, _, p_zm = fixture_crown_shape + calculated_zmr = calculate_crown_z_max_proportion(m, n) - assert calculated_zmr == pytest.approx(z_max_ratio) + assert calculated_zmr == pytest.approx(p_zm) # diff --git a/tests/unit/demography/test_t_model_functions.py b/tests/unit/demography/test_t_model_functions.py index f183662a..661a8bf3 100644 --- a/tests/unit/demography/test_t_model_functions.py +++ b/tests/unit/demography/test_t_model_functions.py @@ -6,6 +6,24 @@ import pytest +@pytest.mark.parametrize( + argnames="crown_areas, expected_r0", + argvalues=( + (np.array([20, 30]), np.array([0.86887756, 1.29007041])), + (np.array([30, 40]), np.array([1.06415334, 1.489645])), + ), +) +def test_calculate_crown_r_0_values(crown_areas, expected_r0): + """Test happy path for calculating r_0.""" + + from pyrealm.demography.t_model_functions import calculate_crown_r0 + + q_m = np.array([2.9038988210485766, 2.3953681843215673]) + actual_r0_values = calculate_crown_r0(q_m=q_m, crown_area=crown_areas) + + assert np.allclose(actual_r0_values, expected_r0) + + @pytest.mark.parametrize( argnames="pft_args, size_args, outcome, excep_message", argvalues=[ From e8afa7797e7119d3b06b24c43d9ca206f9089a6f Mon Sep 17 00:00:00 2001 From: David Orme Date: Mon, 30 Sep 2024 11:05:32 +0100 Subject: [PATCH 07/12] Typo in crown radius attribute --- pyrealm/demography/canopy_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrealm/demography/canopy_functions.py b/pyrealm/demography/canopy_functions.py index 7461cff2..8808596e 100644 --- a/pyrealm/demography/canopy_functions.py +++ b/pyrealm/demography/canopy_functions.py @@ -412,7 +412,7 @@ def __post_init__( # Calculate projected crown area self.projected_crown_area = calculate_stem_projected_crown_area_at_z( z=z, - q_z=self.relativate_crown_radius, + q_z=self.relative_crown_radius, crown_area=stem_allometry.crown_area, q_m=stem_traits.q_m, stem_height=stem_allometry.stem_height, From d28afeeb43168f440540b6c663aa337e2ac6fb66 Mon Sep 17 00:00:00 2001 From: David Orme Date: Mon, 30 Sep 2024 11:12:28 +0100 Subject: [PATCH 08/12] Moar typos --- pyrealm/demography/canopy_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyrealm/demography/canopy_functions.py b/pyrealm/demography/canopy_functions.py index 8808596e..4957554d 100644 --- a/pyrealm/demography/canopy_functions.py +++ b/pyrealm/demography/canopy_functions.py @@ -354,7 +354,7 @@ class CrownProfile: This method calculates crown profile predictions, given an array of vertical heights (``z``) for: - * relativate crown radius, + * relative crown radius, * actual crown radius, * projected crown area, and * project leaf area. @@ -380,7 +380,7 @@ class CrownProfile: z: InitVar[NDArray[np.float32]] """An array of vertical height values at which to calculate crown profiles.""" - relativate_crown_radius: NDArray[np.float32] = field(init=False) + relative_crown_radius: NDArray[np.float32] = field(init=False) """An array of the relative crown radius of stems at z heights""" crown_radius: NDArray[np.float32] = field(init=False) """An array of the actual crown radius of stems at z heights""" From b2a5edd8de8857ccdf2c4d1fbb315e9790df38f2 Mon Sep 17 00:00:00 2001 From: David Orme Date: Mon, 30 Sep 2024 11:16:12 +0100 Subject: [PATCH 09/12] Yet moar typos --- pyrealm/demography/canopy_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyrealm/demography/canopy_functions.py b/pyrealm/demography/canopy_functions.py index 4957554d..68b41c61 100644 --- a/pyrealm/demography/canopy_functions.py +++ b/pyrealm/demography/canopy_functions.py @@ -357,7 +357,7 @@ class CrownProfile: * relative crown radius, * actual crown radius, * projected crown area, and - * project leaf area. + * projected leaf area. The predictions require a set of plant functional types (PFTs) but also the expected allometric predictions of stem height, crown area and z_max for an actual stem of a @@ -386,7 +386,7 @@ class CrownProfile: """An array of the actual crown radius of stems at z heights""" projected_crown_area: NDArray[np.float32] = field(init=False) """An array of the projected crown area of stems at z heights""" - project_leaf_area: NDArray[np.float32] = field(init=False) + projected_leaf_area: NDArray[np.float32] = field(init=False) """An array of the projected leaf area of stems at z heights""" def __post_init__( From deae51907bd35513fbcc7307df76c4063dd0f44f Mon Sep 17 00:00:00 2001 From: David Orme Date: Mon, 30 Sep 2024 12:36:11 +0100 Subject: [PATCH 10/12] Added a simple validation that CanopyProfile gets the same answers as more detailed tests --- .../unit/demography/test_canopy_functions.py | 45 +++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/tests/unit/demography/test_canopy_functions.py b/tests/unit/demography/test_canopy_functions.py index 49e51b96..db3bc370 100644 --- a/tests/unit/demography/test_canopy_functions.py +++ b/tests/unit/demography/test_canopy_functions.py @@ -341,15 +341,9 @@ def test_calculate_relative_crown_radius_at_z_values(fixture_community): calculate_relative_crown_radius_at_z, ) - # Canopy shape model gives the maximum radius at a height z_max - z_max = ( - fixture_community.stem_allometry.stem_height - * fixture_community.stem_traits.z_max_prop - ) - - # Get the relative radius at that height + # Get the relative radius at that heights of the crown z_max values q_z_values = calculate_relative_crown_radius_at_z( - z=z_max, + z=fixture_community.stem_allometry.crown_z_max, stem_height=fixture_community.stem_allometry.stem_height, m=fixture_community.stem_traits.m, n=fixture_community.stem_traits.n, @@ -642,3 +636,38 @@ def test_calculate_stem_projected_leaf_area_at_z_values(fixture_community): np.diag(leaf_area_fg002), fixture_community.stem_allometry.crown_area * 0.98, ) + + +def test_CrownProfile(fixture_community): + """Test the CrownProfile class. + + This implements a subset of the tests in the more detailed function checks above to + validate that this wrapper class works as intended. + """ + + from pyrealm.demography.canopy_functions import CrownProfile + + # Estimate the profile at the heights of the maximum crown radii for each cohort + crown_profile = CrownProfile( + stem_traits=fixture_community.stem_traits, + stem_allometry=fixture_community.stem_allometry, + z=fixture_community.stem_allometry.crown_z_max[:, None], + ) + + # Crown radius on diagonal predicts crown area accurately + assert np.allclose( + np.diag(crown_profile.crown_radius) ** 2 * np.pi, + fixture_community.stem_allometry.crown_area, + ) + + # Same is true for projected crown area at z_max heights + assert np.allclose( + np.diag(crown_profile.projected_crown_area), + fixture_community.stem_allometry.crown_area, + ) + + # And since f_g=0, so is projected leaf area + assert np.allclose( + np.diag(crown_profile.projected_leaf_area), + fixture_community.stem_allometry.crown_area, + ) From 062d8207ab1672963309e90e6d9077c14c800c20 Mon Sep 17 00:00:00 2001 From: David Orme Date: Mon, 30 Sep 2024 13:00:43 +0100 Subject: [PATCH 11/12] Renaming canopy_functions to crown --- pyrealm/demography/canopy.py | 4 ++-- .../{canopy_functions.py => crown.py} | 6 +++--- ...{test_canopy_functions.py => test_crown.py} | 18 +++++++++--------- 3 files changed, 14 insertions(+), 14 deletions(-) rename pyrealm/demography/{canopy_functions.py => crown.py} (98%) rename tests/unit/demography/{test_canopy_functions.py => test_crown.py} (97%) diff --git a/pyrealm/demography/canopy.py b/pyrealm/demography/canopy.py index 91694326..05c35631 100644 --- a/pyrealm/demography/canopy.py +++ b/pyrealm/demography/canopy.py @@ -4,13 +4,13 @@ from numpy.typing import NDArray from scipy.optimize import root_scalar # type: ignore [import-untyped] -from pyrealm.demography.canopy_functions import ( +from pyrealm.demography.community import Community +from pyrealm.demography.crown import ( calculate_relative_crown_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: diff --git a/pyrealm/demography/canopy_functions.py b/pyrealm/demography/crown.py similarity index 98% rename from pyrealm/demography/canopy_functions.py rename to pyrealm/demography/crown.py index 68b41c61..2f945996 100644 --- a/pyrealm/demography/canopy_functions.py +++ b/pyrealm/demography/crown.py @@ -26,7 +26,7 @@ def _validate_z_qz_args( This function provides the following validation checks (see also the documentation of accepted shapes for ``z`` in - :meth:`~pyrealm.demography.canopy_functions.calculate_relative_crown_radius_at_z`). + :meth:`~pyrealm.demography.crown.calculate_relative_crown_radius_at_z`). * Stem properties are identically shaped row (1D) arrays. * The ``z`` argument is then one of: @@ -187,7 +187,7 @@ def calculate_stem_projected_crown_area_at_z( arguments ``stem_height``,``crown_area``,``q_m`` and ``z_max``, which must be one-dimensional arrays ('row vectors') of equal length. The array of vertical heights ``z`` accepts a range of input shapes (see - :meth:`~pyrealm.demography.canopy_functions.calculate_relative_crown_radius_at_z` + :meth:`~pyrealm.demography.crown.calculate_relative_crown_radius_at_z` ) and this function then also requires the expected relative stem radius (``q_z``) calculated from those heights. @@ -308,7 +308,7 @@ def calculate_stem_projected_leaf_area_at_z( ``stem_height``,``crown_area``,``f_g``,``q_m`` and ``z_max``, which must be one-dimensional arrays ('row vectors') of equal length. The array of vertical heights ``z`` accepts a range of input shapes (see - :meth:`~pyrealm.demography.canopy_functions.calculate_relative_crown_radius_at_z` + :meth:`~pyrealm.demography.crown.calculate_relative_crown_radius_at_z` ) and this function then also requires the expected relative stem radius (``q_z``) calculated from those heights. diff --git a/tests/unit/demography/test_canopy_functions.py b/tests/unit/demography/test_crown.py similarity index 97% rename from tests/unit/demography/test_canopy_functions.py rename to tests/unit/demography/test_crown.py index db3bc370..221eac2c 100644 --- a/tests/unit/demography/test_canopy_functions.py +++ b/tests/unit/demography/test_crown.py @@ -275,7 +275,7 @@ def fixture_z_qz_stem_properties(request): def test__validate_z_qz__args(fixture_z_qz_stem_properties): """Tests the validation function for arguments to canopy functions.""" - from pyrealm.demography.canopy_functions import _validate_z_qz_args + from pyrealm.demography.crown import _validate_z_qz_args # Unpack the input arguments for the test case - not testing outputs here z, stem, more_stem, q_z, outcome, excep_msg, _ = fixture_z_qz_stem_properties @@ -309,7 +309,7 @@ def test_calculate_relative_crown_radius_at_z_inputs(fixture_z_qz_stem_propertie This test checks the function behaviour with different inputs. """ - from pyrealm.demography.canopy_functions import ( + from pyrealm.demography.crown import ( calculate_relative_crown_radius_at_z, ) @@ -337,7 +337,7 @@ def test_calculate_relative_crown_radius_at_z_values(fixture_community): maximum crown radius. """ - from pyrealm.demography.canopy_functions import ( + from pyrealm.demography.crown import ( calculate_relative_crown_radius_at_z, ) @@ -378,7 +378,7 @@ def test_calculate_relative_crown_radius_at_z_values(fixture_community): ) def test_calculate_stem_projected_crown_area_at_z_inputs(fixture_z_qz_stem_properties): """Tests the validation of inputs to calculate_stem_projected_crown_area_at_z.""" - from pyrealm.demography.canopy_functions import ( + from pyrealm.demography.crown import ( calculate_stem_projected_crown_area_at_z, ) @@ -437,7 +437,7 @@ def test_calculate_stem_projected_crown_area_at_z_values( * 1 metre below z_max - all values should be equal to crown area """ - from pyrealm.demography.canopy_functions import ( + from pyrealm.demography.crown import ( calculate_relative_crown_radius_at_z, calculate_stem_projected_crown_area_at_z, ) @@ -476,7 +476,7 @@ def test_solve_community_projected_canopy_area(fixture_community): 2 and so on. """ - from pyrealm.demography.canopy_functions import ( + from pyrealm.demography.crown import ( solve_community_projected_canopy_area, ) @@ -523,7 +523,7 @@ def test_solve_community_projected_canopy_area(fixture_community): ) def test_calculate_stem_projected_leaf_area_at_z_inputs(fixture_z_qz_stem_properties): """Tests the validation of inputs to calculate_stem_projected_crown_area_at_z.""" - from pyrealm.demography.canopy_functions import ( + from pyrealm.demography.crown import ( calculate_stem_projected_leaf_area_at_z, ) @@ -552,7 +552,7 @@ def test_calculate_stem_projected_leaf_area_at_z_values(fixture_community): robust theoretical checks about the expectations and crown area. """ - from pyrealm.demography.canopy_functions import ( + from pyrealm.demography.crown import ( calculate_relative_crown_radius_at_z, calculate_stem_projected_leaf_area_at_z, ) @@ -645,7 +645,7 @@ def test_CrownProfile(fixture_community): validate that this wrapper class works as intended. """ - from pyrealm.demography.canopy_functions import CrownProfile + from pyrealm.demography.crown import CrownProfile # Estimate the profile at the heights of the maximum crown radii for each cohort crown_profile = CrownProfile( From 502968dada8303e6581d27acdd544974250236d8 Mon Sep 17 00:00:00 2001 From: David Orme Date: Mon, 30 Sep 2024 16:01:12 +0100 Subject: [PATCH 12/12] Docstring fixes from @j-emberton --- pyrealm/demography/crown.py | 10 ++++++---- pyrealm/demography/t_model_functions.py | 1 - 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pyrealm/demography/crown.py b/pyrealm/demography/crown.py index 2f945996..ed93fa32 100644 --- a/pyrealm/demography/crown.py +++ b/pyrealm/demography/crown.py @@ -161,8 +161,8 @@ def calculate_crown_radius( relative radius values. Args: - q_z: TODO - r0: TODO + q_z: An array of relative crown radius values + r0: An array of crown radius scaling factor values validate: Boolean flag to suppress argument validation. """ @@ -364,8 +364,10 @@ class CrownProfile: given size for each PFT. Args: - stem_traits: - stem_allometry: A Ste + stem_traits: A Flora or StemTraits instance providing plant functional trait + data. + stem_allometry: A StemAllometry instance setting the stem allometries for the + crown profile. z: An array of vertical height values at which to calculate crown profiles. stem_height: A row array providing expected stem height for each PFT. crown_area: A row array providing expected crown area for each PFT. diff --git a/pyrealm/demography/t_model_functions.py b/pyrealm/demography/t_model_functions.py index fa1be81d..122aeb8e 100644 --- a/pyrealm/demography/t_model_functions.py +++ b/pyrealm/demography/t_model_functions.py @@ -308,7 +308,6 @@ def calculate_crown_z_max( z_max_prop: Crown shape parameter of the PFT stem_height: Stem height of individuals """ - """Calculate z_m, the height of maximum crown radius.""" return stem_height * z_max_prop