From f3f62a0a038481e1b7873797920ded30969c8658 Mon Sep 17 00:00:00 2001 From: David Orme Date: Fri, 27 Sep 2024 14:31:31 +0100 Subject: [PATCH 1/7] Added StemAllometry --- pyrealm/demography/t_model_functions.py | 96 +++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/pyrealm/demography/t_model_functions.py b/pyrealm/demography/t_model_functions.py index 4323aa2f..da2750b1 100644 --- a/pyrealm/demography/t_model_functions.py +++ b/pyrealm/demography/t_model_functions.py @@ -5,10 +5,17 @@ calculate stem growth given net primary productivity. """ # 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.canopy_functions import ( + calculate_canopy_r0, + calculate_canopy_z_max, +) +from pyrealm.demography.flora import Flora, StemTraits def _validate_t_model_args(pft_args: list[NDArray], size_args: list[NDArray]) -> None: @@ -600,3 +607,92 @@ def calculate_growth_increments( delta_d = (npp - turnover) / (dWsdt + dWfdt) return (delta_d, dWsdt * delta_d, dWfdt * delta_d) + + +@dataclass +class StemAllometry: + """Calculate T Model allometric predictions across a set of stems. + + This method calculate predictions of stem allometries for stem height, crown area, + crown fraction, stem mass, foliage mass and sapwood mass under the T Model + :cite:`Li:2014bc`, given diameters at breast height for a set of plant functional + traits. + + Args: + pft_data: A dictionary of plant functional trait data, as for example returned + from :attr:`Flora.data` attribute. + dbh: An array of diameter at breast height values for which to predict stem + allometry values. + """ + + # Init vars + stem_traits: InitVar[Flora | StemTraits] + dbh: InitVar[NDArray[np.float32]] + + # Post init allometry attributes + # dbh: NDArray[np.float32] = field(post_init=False) + stem_height: NDArray[np.float32] = field(init=False) + crown_area: NDArray[np.float32] = field(init=False) + crown_fraction: NDArray[np.float32] = field(init=False) + stem_mass: NDArray[np.float32] = field(init=False) + foliage_mass: NDArray[np.float32] = field(init=False) + sapwood_mass: NDArray[np.float32] = field(init=False) + canopy_r0: NDArray[np.float32] = field(init=False) + canopy_z_max: NDArray[np.float32] = field(init=False) + + def __post_init__( + self, stem_traits: Flora | StemTraits, dbh: NDArray[np.float32] + ) -> None: + """Populate the stem allometry attributes from the traits and size data.""" + + self.stem_height = calculate_heights( + h_max=stem_traits.h_max, + a_hd=stem_traits.a_hd, + dbh=dbh, + ) + + # Broadcast dbh to shape of stem height to get congruent shapes + dbh = np.broadcast_to(dbh, self.stem_height.shape) + + self.crown_area = calculate_crown_areas( + ca_ratio=stem_traits.ca_ratio, + a_hd=stem_traits.a_hd, + dbh=dbh, + stem_height=self.stem_height, + ) + + self.crown_fraction = calculate_crown_fractions( + a_hd=stem_traits.a_hd, + dbh=dbh, + stem_height=self.stem_height, + ) + + self.stem_mass = calculate_stem_masses( + rho_s=stem_traits.rho_s, + dbh=dbh, + stem_height=self.stem_height, + ) + + self.foliage_mass = calculate_foliage_masses( + sla=stem_traits.sla, + lai=stem_traits.lai, + crown_area=self.crown_area, + ) + + self.sapwood_mass = calculate_sapwood_masses( + rho_s=stem_traits.rho_s, + ca_ratio=stem_traits.ca_ratio, + stem_height=self.stem_height, + crown_area=self.crown_area, + crown_fraction=self.crown_fraction, + ) + + self.canopy_r0 = calculate_canopy_r0( + q_m=stem_traits.q_m, + crown_area=self.crown_area, + ) + + self.canopy_z_max = calculate_canopy_z_max( + z_max_prop=stem_traits.z_max_prop, + stem_height=self.stem_height, + ) From c86964c79d5fbd898db35c7e7704735a10865a58 Mon Sep 17 00:00:00 2001 From: David Orme Date: Fri, 27 Sep 2024 14:36:03 +0100 Subject: [PATCH 2/7] Adopt StemAllometry in Community --- pyrealm/demography/community.py | 67 +++------------------------------ 1 file changed, 5 insertions(+), 62 deletions(-) diff --git a/pyrealm/demography/community.py b/pyrealm/demography/community.py index f466493e..787afa76 100644 --- a/pyrealm/demography/community.py +++ b/pyrealm/demography/community.py @@ -117,9 +117,8 @@ from numpy.typing import NDArray from pyrealm.core.utilities import check_input_shapes -from pyrealm.demography import canopy_functions -from pyrealm.demography import t_model_functions as t_model from pyrealm.demography.flora import Flora, StemTraits +from pyrealm.demography.t_model_functions import StemAllometry if sys.version_info[:2] >= (3, 11): import tomllib @@ -356,6 +355,7 @@ class Community: # Post init properties number_of_cohorts: int = field(init=False) stem_traits: StemTraits = field(init=False) + stem_allometry: StemAllometry = field(init=False) cohort_data: dict[str, NDArray] = field(init=False) def __post_init__( @@ -405,66 +405,9 @@ def __post_init__( self.number_of_cohorts = len(cohort_pft_names) - # 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["stem_height"] = t_model.calculate_heights( - h_max=self.stem_traits.h_max, - a_hd=self.stem_traits.a_hd, - dbh=self.cohort_data["dbh"], - ) - - self.cohort_data["crown_area"] = t_model.calculate_crown_areas( - ca_ratio=self.stem_traits.ca_ratio, - a_hd=self.stem_traits.a_hd, - dbh=self.cohort_data["dbh"], - stem_height=self.cohort_data["stem_height"], - ) - - self.cohort_data["crown_fraction"] = t_model.calculate_crown_fractions( - a_hd=self.stem_traits.a_hd, - dbh=self.cohort_data["dbh"], - stem_height=self.cohort_data["stem_height"], - ) - - self.cohort_data["stem_mass"] = t_model.calculate_stem_masses( - rho_s=self.stem_traits.rho_s, - dbh=self.cohort_data["dbh"], - stem_height=self.cohort_data["stem_height"], - ) - - self.cohort_data["foliage_mass"] = t_model.calculate_foliage_masses( - sla=self.stem_traits.sla, - lai=self.stem_traits.lai, - crown_area=self.cohort_data["crown_area"], - ) - - self.cohort_data["sapwood_mass"] = t_model.calculate_sapwood_masses( - rho_s=self.stem_traits.rho_s, - ca_ratio=self.stem_traits.ca_ratio, - stem_height=self.cohort_data["stem_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"] = canopy_functions.calculate_canopy_z_max( - z_max_prop=self.stem_traits.z_max_prop, - stem_height=self.cohort_data["stem_height"], - ) - self.cohort_data["canopy_r0"] = canopy_functions.calculate_canopy_r0( - q_m=self.stem_traits.q_m, - crown_area=self.cohort_data["crown_area"], + # Populate the stem allometry + self.stem_allometry = StemAllometry( + stem_traits=self.stem_traits, dbh=cohort_dbh_values ) @classmethod From 2dc486a8f49ea7527d52cb5e1f28f330338cc88c Mon Sep 17 00:00:00 2001 From: David Orme Date: Fri, 27 Sep 2024 14:39:13 +0100 Subject: [PATCH 3/7] Update Canopy to access StemAllometry attributes --- pyrealm/demography/canopy.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pyrealm/demography/canopy.py b/pyrealm/demography/canopy.py index 0442e37a..50958c79 100644 --- a/pyrealm/demography/canopy.py +++ b/pyrealm/demography/canopy.py @@ -79,10 +79,10 @@ def _calculate_canopy(self, community: Community) -> None: # 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"] + community.stem_allometry.crown_area * community.cohort_data["n_individuals"] ).sum() - self.max_stem_height = community.cohort_data["stem_height"].max() + self.max_stem_height = community.stem_allometry.stem_height.max() self.crown_area_per_layer = community.cell_area * (1 - self.canopy_gap_fraction) @@ -109,12 +109,12 @@ def _calculate_canopy(self, community: Community) -> None: solution = root_scalar( solve_community_projected_canopy_area, args=( - community.cohort_data["stem_height"], - community.cohort_data["crown_area"], + community.stem_allometry.stem_height, + community.stem_allometry.crown_area, community.stem_traits.m, community.stem_traits.n, community.stem_traits.q_m, - community.cohort_data["canopy_z_max"], + community.stem_allometry.canopy_z_max, community.cohort_data["n_individuals"], target_area, False, # validate @@ -136,7 +136,7 @@ def _calculate_canopy(self, community: Community) -> None: # 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"], + stem_height=community.stem_allometry.stem_height, m=community.stem_traits.m, n=community.stem_traits.n, validate=False, @@ -146,10 +146,10 @@ def _calculate_canopy(self, community: Community) -> None: 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"], + crown_area=community.stem_allometry.crown_area, + stem_height=community.stem_allometry.stem_height, q_m=community.stem_traits.q_m, - z_max=community.cohort_data["canopy_z_max"], + z_max=community.stem_allometry.canopy_z_max, validate=False, ) @@ -157,10 +157,10 @@ def _calculate_canopy(self, community: Community) -> None: 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"], + crown_area=community.stem_allometry.crown_area, + stem_height=community.stem_allometry.stem_height, f_g=community.stem_traits.f_g, q_m=community.stem_traits.q_m, - z_max=community.cohort_data["canopy_z_max"], + z_max=community.stem_allometry.canopy_z_max, validate=False, ) From 60878bb855bdb0a6178b295f0fb6a4cfb222b7bd Mon Sep 17 00:00:00 2001 From: David Orme Date: Fri, 27 Sep 2024 14:56:13 +0100 Subject: [PATCH 4/7] Updating StemAllometry refs in canopy and tests --- pyrealm/demography/community.py | 17 ++++--- tests/unit/demography/test_canopy.py | 2 +- .../unit/demography/test_canopy_functions.py | 48 +++++++++---------- tests/unit/demography/test_community.py | 2 +- 4 files changed, 37 insertions(+), 32 deletions(-) diff --git a/pyrealm/demography/community.py b/pyrealm/demography/community.py index 787afa76..456088c8 100644 --- a/pyrealm/demography/community.py +++ b/pyrealm/demography/community.py @@ -89,12 +89,17 @@ ... cohort_pft_names=cohort_pft_names ... ) -Convert the community cohort data to a :class:`pandas.DataFrame` for nicer display and -show some of the calculated T Model predictions: - ->>> pd.DataFrame(community.cohort_data)[ -... ['name', 'dbh', 'n_individuals', 'stem_height', 'crown_area', 'stem_mass'] -... ] +Convert some of the data to a :class:`pandas.DataFrame` for nicer display and show some +of the calculated T Model predictions: + +>>> pd.DataFrame({ +... 'name': community.stem_traits.name, +... 'n_individuals': community.cohort_data["n_individuals"], +... 'dbh': community.stem_allometry.dbh, +... 'stem_height': community.stem_allometry.stem_height, +... 'crown_area': community.stem_allometry.crown_area, +... 'stem_mass': community.stem_allometry.stem_mass, +... }) name dbh n_individuals stem_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 diff --git a/tests/unit/demography/test_canopy.py b/tests/unit/demography/test_canopy.py index c937c8eb..615f963a 100644 --- a/tests/unit/demography/test_canopy.py +++ b/tests/unit/demography/test_canopy.py @@ -38,7 +38,7 @@ def test_Canopy__init__(): np.ceil( ( ( - community.cohort_data["crown_area"] + community.stem_allometry.crown_area * community.cohort_data["n_individuals"] ).sum() * (1 + canopy_gap_fraction) diff --git a/tests/unit/demography/test_canopy_functions.py b/tests/unit/demography/test_canopy_functions.py index e0af9d26..7681fa87 100644 --- a/tests/unit/demography/test_canopy_functions.py +++ b/tests/unit/demography/test_canopy_functions.py @@ -401,14 +401,14 @@ def test_calculate_relative_canopy_radius_at_z_values(fixture_community): # Canopy shape model gives the maximum radius at a height z_max z_max = ( - fixture_community.cohort_data["stem_height"] + fixture_community.stem_allometry.stem_height * fixture_community.stem_traits.z_max_prop ) # Get the relative radius at that height q_z_values = calculate_relative_canopy_radius_at_z( z=z_max, - stem_height=fixture_community.cohort_data["stem_height"], + stem_height=fixture_community.stem_allometry.stem_height, m=fixture_community.stem_traits.m, n=fixture_community.stem_traits.n, ) @@ -416,8 +416,8 @@ def test_calculate_relative_canopy_radius_at_z_values(fixture_community): # Now test that the circular crown area from that radius is equivalent to the direct # prediction from the T model allometric equations. assert np.allclose( - fixture_community.cohort_data["crown_area"], - np.pi * (q_z_values * fixture_community.cohort_data["canopy_r0"]) ** 2, + fixture_community.stem_allometry.crown_area, + np.pi * (q_z_values * fixture_community.stem_allometry.canopy_r0) ** 2, ) @@ -509,7 +509,7 @@ def test_calculate_stem_projected_crown_area_at_z_values( # Calculate the required q_z q_z = calculate_relative_canopy_radius_at_z( z=heights, - stem_height=fixture_community.cohort_data["stem_height"], + stem_height=fixture_community.stem_allometry.stem_height, m=fixture_community.stem_traits.m, n=fixture_community.stem_traits.n, ) @@ -518,10 +518,10 @@ def test_calculate_stem_projected_crown_area_at_z_values( Ap_z_values = calculate_stem_projected_crown_area_at_z( z=heights, q_z=q_z, - stem_height=fixture_community.cohort_data["stem_height"], - crown_area=fixture_community.cohort_data["crown_area"], + 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.cohort_data["canopy_z_max"], + z_max=fixture_community.stem_allometry.canopy_z_max, ) assert np.allclose( @@ -548,18 +548,18 @@ def test_solve_community_projected_canopy_area(fixture_community): this_height, this_target, ) in zip( - np.flip(fixture_community.cohort_data["canopy_z_max"]), - np.cumsum(np.flip(fixture_community.cohort_data["crown_area"])), + np.flip(fixture_community.stem_allometry.canopy_z_max), + np.cumsum(np.flip(fixture_community.stem_allometry.crown_area)), ): solved = solve_community_projected_canopy_area( z=this_height, - stem_height=fixture_community.cohort_data["stem_height"], - crown_area=fixture_community.cohort_data["crown_area"], + stem_height=fixture_community.stem_allometry.stem_height, + crown_area=fixture_community.stem_allometry.crown_area, n_individuals=fixture_community.cohort_data["n_individuals"], m=fixture_community.stem_traits.m, n=fixture_community.stem_traits.n, q_m=fixture_community.stem_traits.q_m, - z_max=fixture_community.cohort_data["canopy_z_max"], + z_max=fixture_community.stem_allometry.canopy_z_max, target_area=this_target, ) @@ -623,11 +623,11 @@ 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.cohort_data["canopy_z_max"][:, None] + z_max = fixture_community.stem_allometry.canopy_z_max[:, None] q_z = calculate_relative_canopy_radius_at_z( z=z_max, - stem_height=fixture_community.cohort_data["stem_height"], + stem_height=fixture_community.stem_allometry.stem_height, m=fixture_community.stem_traits.m, n=fixture_community.stem_traits.n, ) @@ -635,11 +635,11 @@ def test_calculate_stem_projected_leaf_area_at_z_values(fixture_community): leaf_area_fg0 = calculate_stem_projected_leaf_area_at_z( z=z_max, q_z=q_z, - stem_height=fixture_community.cohort_data["stem_height"], - crown_area=fixture_community.cohort_data["crown_area"], + stem_height=fixture_community.stem_allometry.stem_height, + 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.cohort_data["canopy_z_max"], + z_max=fixture_community.stem_allometry.canopy_z_max, ) # Pre-calculated values @@ -656,11 +656,11 @@ def test_calculate_stem_projected_leaf_area_at_z_values(fixture_community): # More rigourous check - with f_g = 0, the projected leaf area of each stem in the # lowest layer must equal the crown area (all the crown is now accounted for). - assert np.allclose(leaf_area_fg0[0, :], fixture_community.cohort_data["crown_area"]) + assert np.allclose(leaf_area_fg0[0, :], fixture_community.stem_allometry.crown_area) # Also the diagonal of the resulting matrix (4 heights for 4 cohorts) should _also_ # match the crown areas as the leaf area is all accounted for exactly at z_max. assert np.allclose( - np.diag(leaf_area_fg0), fixture_community.cohort_data["crown_area"] + np.diag(leaf_area_fg0), fixture_community.stem_allometry.crown_area ) # Introduce some crown gap fraction and recalculate @@ -669,11 +669,11 @@ def test_calculate_stem_projected_leaf_area_at_z_values(fixture_community): leaf_area_fg002 = calculate_stem_projected_leaf_area_at_z( z=z_max, q_z=q_z, - stem_height=fixture_community.cohort_data["stem_height"], - crown_area=fixture_community.cohort_data["crown_area"], + stem_height=fixture_community.stem_allometry.stem_height, + 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.cohort_data["canopy_z_max"], + z_max=fixture_community.stem_allometry.canopy_z_max, ) expected_leaf_area_fg002 = np.array( @@ -698,5 +698,5 @@ def test_calculate_stem_projected_leaf_area_at_z_values(fixture_community): # the stem all but the crown gap fraction should be accounted for assert np.allclose( np.diag(leaf_area_fg002), - fixture_community.cohort_data["crown_area"] * 0.98, + fixture_community.stem_allometry.crown_area * 0.98, ) diff --git a/tests/unit/demography/test_community.py b/tests/unit/demography/test_community.py index 80013daf..1e6cdea6 100644 --- a/tests/unit/demography/test_community.py +++ b/tests/unit/demography/test_community.py @@ -42,7 +42,7 @@ def check_expected(community, expected): expected["a_hd"], ) assert np.allclose( - community.cohort_data["stem_height"], + community.stem_allometry.stem_height, expected["height"], ) From ea10d77a9ddf5a7f109ed4ac1517c1a71a655efb Mon Sep 17 00:00:00 2001 From: David Orme Date: Fri, 27 Sep 2024 18:04:57 +0100 Subject: [PATCH 5/7] Fixing dbh in StemAllometry --- pyrealm/demography/community.py | 4 ++-- pyrealm/demography/t_model_functions.py | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pyrealm/demography/community.py b/pyrealm/demography/community.py index 456088c8..11502f35 100644 --- a/pyrealm/demography/community.py +++ b/pyrealm/demography/community.py @@ -94,8 +94,8 @@ >>> pd.DataFrame({ ... 'name': community.stem_traits.name, -... 'n_individuals': community.cohort_data["n_individuals"], ... 'dbh': community.stem_allometry.dbh, +... 'n_individuals': community.cohort_data["n_individuals"], ... 'stem_height': community.stem_allometry.stem_height, ... 'crown_area': community.stem_allometry.crown_area, ... 'stem_mass': community.stem_allometry.stem_mass, @@ -412,7 +412,7 @@ def __post_init__( # Populate the stem allometry self.stem_allometry = StemAllometry( - stem_traits=self.stem_traits, dbh=cohort_dbh_values + stem_traits=self.stem_traits, at_dbh=cohort_dbh_values ) @classmethod diff --git a/pyrealm/demography/t_model_functions.py b/pyrealm/demography/t_model_functions.py index da2750b1..4c3a6234 100644 --- a/pyrealm/demography/t_model_functions.py +++ b/pyrealm/demography/t_model_functions.py @@ -621,16 +621,16 @@ class StemAllometry: Args: pft_data: A dictionary of plant functional trait data, as for example returned from :attr:`Flora.data` attribute. - dbh: An array of diameter at breast height values for which to predict stem + at_dbh: An array of diameter at breast height values at which to predict stem allometry values. """ # Init vars stem_traits: InitVar[Flora | StemTraits] - dbh: InitVar[NDArray[np.float32]] + at_dbh: InitVar[NDArray[np.float32]] # Post init allometry attributes - # dbh: NDArray[np.float32] = field(post_init=False) + dbh: NDArray[np.float32] = field(init=False) stem_height: NDArray[np.float32] = field(init=False) crown_area: NDArray[np.float32] = field(init=False) crown_fraction: NDArray[np.float32] = field(init=False) @@ -641,35 +641,35 @@ class StemAllometry: canopy_z_max: NDArray[np.float32] = field(init=False) def __post_init__( - self, stem_traits: Flora | StemTraits, dbh: NDArray[np.float32] + self, stem_traits: Flora | StemTraits, at_dbh: NDArray[np.float32] ) -> None: """Populate the stem allometry attributes from the traits and size data.""" self.stem_height = calculate_heights( h_max=stem_traits.h_max, a_hd=stem_traits.a_hd, - dbh=dbh, + dbh=at_dbh, ) - # Broadcast dbh to shape of stem height to get congruent shapes - dbh = np.broadcast_to(dbh, self.stem_height.shape) + # Broadcast at_dbh to shape of stem height to get congruent shapes + self.dbh = np.broadcast_to(at_dbh, self.stem_height.shape) self.crown_area = calculate_crown_areas( ca_ratio=stem_traits.ca_ratio, a_hd=stem_traits.a_hd, - dbh=dbh, + dbh=self.dbh, stem_height=self.stem_height, ) self.crown_fraction = calculate_crown_fractions( a_hd=stem_traits.a_hd, - dbh=dbh, + dbh=self.dbh, stem_height=self.stem_height, ) self.stem_mass = calculate_stem_masses( rho_s=stem_traits.rho_s, - dbh=dbh, + dbh=self.dbh, stem_height=self.stem_height, ) From 742f02cb68e58b3d6f889cf50540b5863fa77cfe Mon Sep 17 00:00:00 2001 From: David Orme Date: Fri, 27 Sep 2024 19:12:32 +0100 Subject: [PATCH 6/7] Simple test of StemAllometry --- pyrealm/demography/t_model_functions.py | 13 +++++++++++++ tests/unit/demography/test_t_model_functions.py | 15 +++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/pyrealm/demography/t_model_functions.py b/pyrealm/demography/t_model_functions.py index 4c3a6234..65641dbc 100644 --- a/pyrealm/demography/t_model_functions.py +++ b/pyrealm/demography/t_model_functions.py @@ -6,6 +6,7 @@ """ # noqa: D205 from dataclasses import InitVar, dataclass, field +from typing import ClassVar import numpy as np from numpy.typing import NDArray @@ -625,6 +626,18 @@ class StemAllometry: allometry values. """ + allometry_attrs: ClassVar[tuple[str, ...]] = ( + "dbh", + "stem_height", + "crown_area", + "crown_fraction", + "stem_mass", + "foliage_mass", + "sapwood_mass", + "canopy_r0", + "canopy_z_max", + ) + # Init vars stem_traits: InitVar[Flora | StemTraits] at_dbh: InitVar[NDArray[np.float32]] diff --git a/tests/unit/demography/test_t_model_functions.py b/tests/unit/demography/test_t_model_functions.py index 19795831..3cc65e2e 100644 --- a/tests/unit/demography/test_t_model_functions.py +++ b/tests/unit/demography/test_t_model_functions.py @@ -704,3 +704,18 @@ def test_calculate_dbh_from_height_edge_cases(): # Undefined entries assert np.all(np.isnan(dbh) == np.array([[0, 0], [0, 0], [0, 0], [1, 0], [1, 1]])) + + +def test_StemAllometry(rtmodel_flora, rtmodel_data): + """Test the StemAllometry class.""" + + from pyrealm.demography.t_model_functions import StemAllometry + + stem_allometry = StemAllometry( + stem_traits=rtmodel_flora, at_dbh=rtmodel_data["dbh"][:, [0]] + ) + + # Check the variables provided by the rtmodel implementation + to_check = set(stem_allometry.allometry_attrs) - set(["canopy_r0", "canopy_z_max"]) + for var in to_check: + assert np.allclose(getattr(stem_allometry, var), rtmodel_data[var]) From 44498cf774dc6f8677b82acf79ae93966614da92 Mon Sep 17 00:00:00 2001 From: David Orme Date: Sun, 29 Sep 2024 21:25:50 +0100 Subject: [PATCH 7/7] Fixing missing docstrings and outdated ref --- pyrealm/demography/t_model_functions.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/pyrealm/demography/t_model_functions.py b/pyrealm/demography/t_model_functions.py index 65641dbc..57a81483 100644 --- a/pyrealm/demography/t_model_functions.py +++ b/pyrealm/demography/t_model_functions.py @@ -620,8 +620,9 @@ class StemAllometry: traits. Args: - pft_data: A dictionary of plant functional trait data, as for example returned - from :attr:`Flora.data` attribute. + stem_traits: An instance of :class:`~pyrealm.demography.flora.Flora` or + :class:`~pyrealm.demography.flora.StemTraits`, providing plant functional + trait data for a set of stems. at_dbh: An array of diameter at breast height values at which to predict stem allometry values. """ @@ -640,18 +641,32 @@ class StemAllometry: # Init vars stem_traits: InitVar[Flora | StemTraits] + """ An instance of :class:`~pyrealm.demography.flora.Flora` or + :class:`~pyrealm.demography.flora.StemTraits`, providing plant functional trait data + for a set of stems.""" at_dbh: InitVar[NDArray[np.float32]] + """An array of diameter at breast height values at which to predict stem allometry + values.""" # Post init allometry attributes dbh: NDArray[np.float32] = field(init=False) + """Diameter at breast height (metres)""" stem_height: NDArray[np.float32] = field(init=False) + """Stem height (metres)""" crown_area: NDArray[np.float32] = field(init=False) + """Crown area (square metres)""" crown_fraction: NDArray[np.float32] = field(init=False) + """Vertical fraction of the stem covered by the crown (-)""" stem_mass: NDArray[np.float32] = field(init=False) + """Stem mass (kg)""" foliage_mass: NDArray[np.float32] = field(init=False) + """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) + """Height of maximum crown radius (metres)""" def __post_init__( self, stem_traits: Flora | StemTraits, at_dbh: NDArray[np.float32]