The T Model module

Run this notebook

The T Model (Li et al., 2014) provides a model of both:

  • stem :term:allometry, given a set of stem traits for a plant functional type (PFT), and

  • a carbon allocation model, given stem allometry and potential GPP.

from matplotlib import pyplot as plt
import numpy as np
import pandas as pd

from pyrealm.demography.flora import PlantFunctionalType, Flora
from pyrealm.demography.tmodel import (
    StemAllocation,
    StemAllometry,
    calculate_whole_crown_gpp,
)

To generate predictions under the T Model, we need a Flora object providing the trait values for each of the PFTs to be modelled:

# Three PFTS
short_pft = PlantFunctionalType(name="short", h_max=10)
medium_pft = PlantFunctionalType(name="medium", h_max=20)
tall_pft = PlantFunctionalType(name="tall", h_max=30)

# Combine into a Flora instance
flora = Flora([short_pft, medium_pft, tall_pft])

Stem allometry

We can visualise how the stem size, canopy size and various masses of PFTs change with stem diameter by using the StemAllometry class. Creating a StemAllometry instance needs an existing Flora instance and an array of values for diameter at breast height (DBH, metres). The returned class contains the predictions of the T Model for:

  • Stem height (stem_height, m),

  • Crown area (crown_area, m2),

  • Crown fraction (crown_fraction, -),

  • Stem mass (stem_mass, kg),

  • Foliage mass (foliage_mass, kg),

  • Sapwood mass (sapwood_mass, kg),

  • Crown radius scaling factor (crown_r0, -), and

  • Height of maximum crown radius (crown_z_max, m).

Note that stem_height denotes the total tree height, as used interchangeable in Li et al. (2014), rather than just the height of the trunk below the canopy.

The DBH input can be a scalar array or a one dimensional array providing a single value for each PFT. This then calculates a single estimate at the given size for each stem.

# Calculate a single prediction
single_allometry = StemAllometry(stem_traits=flora, at_dbh=np.array([0.1, 0.1, 0.1]))
/home/docs/checkouts/readthedocs.org/user_builds/pyrealm/checkouts/latest/pyrealm/core/experimental.py:72: ExperimentalFeatureWarning: 'Be aware that StemAllometry is an experimental feature of pyrealm and the implementation and API may change within major versions.'
  warn(qualname, ExperimentalFeatureWarning)

The StemAllometry() class provides the to_pandas() method to export the stem data for data exploration.

single_allometry.to_pandas().transpose()
column_stem_index 0 1 2
dbh 0.100000 0.100000 0.100000
stem_height 6.865138 8.802033 9.620475
crown_area 1.814782 2.326795 2.543148
crown_fraction 0.591822 0.758796 0.829351
stem_mass 5.391867 6.913100 7.555903
foliage_mass 0.233329 0.299159 0.326976
fine_root_mass 0.555323 0.711999 0.778203
reproductive_tissue_mass 0.000000 0.000000 0.000000
sapwood_mass 4.493533 6.510900 7.335868
crown_r0 0.261731 0.296362 0.309834
crown_z_max 5.837310 7.484219 8.180126

However, the DBH values can also be a column array (an N x 1 array). In this case, the predictions are made at each DBH value for each PFT and the allometry attributes with predictions arranged with each PFT as a column and each DBH prediction as a row. This makes them convenient to plot using matplotlib.

# Column array of DBH values from 0.01 to 1.6 metres
dbh_col = np.arange(0.01, 1.6, 0.01)[:, None]
# Get the predictions
allometries = StemAllometry(stem_traits=flora, at_dbh=dbh_col)
/home/docs/checkouts/readthedocs.org/user_builds/pyrealm/checkouts/latest/pyrealm/core/experimental.py:72: ExperimentalFeatureWarning: 'Be aware that StemAllometry is an experimental feature of pyrealm and the implementation and API may change within major versions.'
  warn(qualname, ExperimentalFeatureWarning)

The code below shows how to use the returned allometries to generate a plot of the scaling relationships across all of the PFTs in a Flora instance.

fig, axes = plt.subplots(ncols=2, nrows=4, sharex=True, figsize=(10, 10))

plot_details = [
    ("stem_height", "Stem height (m)"),
    ("crown_area", "Crown area (m2)"),
    ("crown_fraction", "Crown fraction (-)"),
    ("stem_mass", "Stem mass (kg)"),
    ("foliage_mass", "Foliage mass (kg)"),
    ("sapwood_mass", "Sapwood mass (kg)"),
    ("crown_r0", "Crown scaling factor (-)"),
    ("crown_z_max", "Height of maximum\ncrown radius (m)"),
]

for ax, (var, ylab) in zip(axes.flatten(), plot_details):
    ax.plot(dbh_col, getattr(allometries, var), label=flora.name)
    ax.set_xlabel("Diameter at breast height (m)")
    ax.set_ylabel(ylab)

    if var == "sapwood_mass":
        ax.legend(frameon=False)
../../_images/cc46067b2d287fc8592108e10352d83defe06dcf8099eb7538aa0efb448eb381.png

The to_pandas() method of the StemAllometry() class can still be used, but the values are stacked into columns along with a index showing the different cohorts.

allometries.to_pandas().transpose()
column_stem_index 0 0 0 0 0 0 0 0 0 0 ... 2 2 2 2 2 2 2 2 2 2
dbh 0.010000 0.020000 0.030000 0.040000 0.050000 0.060000 0.070000 0.080000 0.090000 0.100000 ... 1.500000 1.510000 1.520000 1.530000 1.540000 1.550000 1.560000 1.570000 1.580000 1.590000
stem_height 1.095248 2.070539 2.939011 3.712364 4.401016 5.014244 5.560308 6.046564 6.479563 6.865138 ... 29.909173 29.912618 29.915933 29.919121 29.922189 29.925140 29.927979 29.930711 29.933339 29.935867
crown_area 0.028953 0.109468 0.233076 0.392542 0.581699 0.795301 1.028897 1.278715 1.541570 1.814782 ... 118.596191 119.400584 120.204633 121.008349 121.811741 122.614819 123.417592 124.220067 125.022254 125.824161
crown_fraction 0.944179 0.892474 0.844543 0.800079 0.758796 0.720437 0.684767 0.651569 0.620648 0.591822 ... 0.171892 0.170773 0.169668 0.168577 0.167500 0.166436 0.165385 0.164346 0.163320 0.162307
stem_mass 0.008602 0.065048 0.207746 0.466509 0.864138 1.417744 2.139857 3.039335 4.122120 5.391867 ... 5285.387210 5356.710860 5428.497008 5500.745789 5573.457339 5646.631792 5720.269284 5794.369950 5868.933926 5943.961347
foliage_mass 0.003722 0.014074 0.029967 0.050470 0.074790 0.102253 0.132287 0.164406 0.198202 0.233329 ... 15.248082 15.351504 15.454881 15.558216 15.661510 15.764762 15.867976 15.971151 16.074290 16.177392
fine_root_mass 0.008859 0.033497 0.071321 0.120118 0.178000 0.243362 0.314843 0.391287 0.471720 0.555323 ... 36.290435 36.536579 36.782618 37.028555 37.274393 37.520135 37.765783 38.011340 38.256810 38.502193
reproductive_tissue_mass 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
sapwood_mass 0.008575 0.064296 0.202726 0.447864 0.813863 1.306940 1.927216 2.670347 3.528914 4.493533 ... 1660.863220 1673.344166 1685.816687 1698.280999 1710.737312 1723.185835 1735.626770 1748.060318 1760.486674 1772.906027
crown_r0 0.033059 0.064282 0.093798 0.121727 0.148181 0.173264 0.197074 0.219700 0.241227 0.261731 ... 2.115821 2.122984 2.130121 2.137230 2.144313 2.151370 2.158401 2.165407 2.172387 2.179343
crown_z_max 0.931271 1.760544 2.498991 3.156560 3.742109 4.263526 4.727835 5.141291 5.509462 5.837310 ... 25.431262 25.434191 25.437009 25.439720 25.442328 25.444838 25.447252 25.449575 25.451809 25.453959

11 rows × 477 columns

Productivity allocation

The T Model also predicts how GPP will be allocated to respiration, turnover and growth for stems with a given PFT and allometry using the StemAllometry() class.

This requires an estimate of the GPP available to a stem. The original implementation of the T Model implemented this (Equation 12, Li et al., 2014)using an estimate of the potential GPP per square metre (\(P_0\)), scaled up to the crown area of the stem (\(A_c\)) and using the Beer-Lambert equation to estimate the proportion of potential GPP captured by the crown as a function of the canopy light extinction coefficient (\(k\)) and the canopy leaf area index (\(L\)):

\[ \textrm{GPP} = P_0 A_c (1 - e^{-kL}) \]

This is implemented in the function calculate_whole_crown_gpp:

whole_crown_gpp = calculate_whole_crown_gpp(
    potential_gpp=np.array([55]),
    crown_area=single_allometry.crown_area,
    par_ext=flora.par_ext,
    lai=flora.lai,
)
print(whole_crown_gpp)
[[59.23205397 75.94347843 83.00495708]]

Those realised stem GPP values can then be provided to the StemAllocation class:

single_allocation = StemAllocation(
    stem_traits=flora, stem_allometry=single_allometry, whole_crown_gpp=whole_crown_gpp
)
single_allocation
/home/docs/checkouts/readthedocs.org/user_builds/pyrealm/checkouts/latest/pyrealm/core/experimental.py:72: ExperimentalFeatureWarning: 'Be aware that StemAllocation is an experimental feature of pyrealm and the implementation and API may change within major versions.'
  warn(qualname, ExperimentalFeatureWarning)
StemAllocation: Prediction for 3 stems at 1 observations.

The to_pandas() method of the StemAllocation() class can be used to export data for exploration.

single_allocation.to_pandas().transpose()
column_stem_index 0 1 2
whole_crown_gpp 59.232054 75.943478 83.004957
sapwood_respiration 0.197715 0.286480 0.322778
foliar_respiration 5.923205 7.594348 8.300496
fine_root_respiration 0.507010 0.650055 0.710500
reproductive_tissue_respiration 0.000000 0.000000 0.000000
npp 31.562474 40.447557 44.202710
foliage_turnover 0.058332 0.074790 0.081744
fine_root_turnover 0.533965 0.684615 0.748272
reproductive_tissue_turnover 0.000000 0.000000 0.000000
delta_dbh 0.208607 0.191874 0.186059
delta_stem_mass 28.453546 36.316410 39.632111
delta_foliage_mass 0.744565 0.997557 1.106681
delta_fine_root_mass 1.772066 2.374186 2.633901

Using a column array of potential GPP values can be used to predict multiple estimates of allocation per stem. In the first example, the code takes the allometric predictions from above and calculates the GPP allocation for stems of varying size with the same potential GPP:

# Calculate the stem GPP from potential GPP following the Li et al model
potential_gpp = np.full((dbh_col.size, 1), fill_value=5)

whole_crown_gpp = calculate_whole_crown_gpp(
    potential_gpp=potential_gpp,
    crown_area=single_allometry.crown_area,
    par_ext=flora.par_ext,
    lai=flora.lai,
)

# Calculate the T Model allocation of that GPP
allocation = StemAllocation(
    stem_traits=flora, stem_allometry=allometries, whole_crown_gpp=whole_crown_gpp
)
/home/docs/checkouts/readthedocs.org/user_builds/pyrealm/checkouts/latest/pyrealm/core/experimental.py:72: ExperimentalFeatureWarning: 'Be aware that StemAllocation is an experimental feature of pyrealm and the implementation and API may change within major versions.'
  warn(qualname, ExperimentalFeatureWarning)
fig, axes = plt.subplots(ncols=2, nrows=5, sharex=True, figsize=(10, 12))

plot_details = [
    ("whole_crown_gpp", "whole_crown_gpp"),
    ("sapwood_respiration", "sapwood_respiration"),
    ("foliar_respiration", "foliar_respiration"),
    ("fine_root_respiration", "fine_root_respiration"),
    ("npp", "npp"),
    ("foliage_turnover", "foliage_turnover"),
    ("fine_root_turnover", "fine_root_turnover"),
    ("delta_dbh", "delta_dbh"),
    ("delta_stem_mass", "delta_stem_mass"),
    ("delta_foliage_mass", "delta_foliage_mass"),
]

axes = axes.flatten()

for ax, (var, ylab) in zip(axes, plot_details):
    ax.plot(dbh_col, getattr(allocation, var), label=flora.name)
    ax.set_xlabel("Diameter at breast height (m)")
    ax.set_ylabel(ylab)

    if var == "whole_crown_gpp":
        ax.legend(frameon=False)

# Delete unused panel in 5 x 2 grid
fig.delaxes(axes[-1])
../../_images/3ed8e995f3e31e94dcb4866571c5dc0c52080b12cbd40e15ee74ce894a2fd557.png

An alternative calculation is to make allocation predictions for varying potential GPP for constant allometries:

# Column array of identical DBH values
dbh_constant = np.full((50, 1), fill_value=0.2)

# Get the allometric predictions for those stems
constant_allometries = StemAllometry(stem_traits=flora, at_dbh=dbh_constant)

# Calculate the stem GPP with _varying_ potential GPP
potential_gpp_varying = np.linspace(1, 10, num=50)[:, None]

whole_crown_gpp_varying = calculate_whole_crown_gpp(
    potential_gpp=potential_gpp_varying,
    crown_area=constant_allometries.crown_area,
    par_ext=flora.par_ext,
    lai=flora.lai,
)

# Calculate the resulting changes in the allocation with varying productivity
allocation_2 = StemAllocation(
    stem_traits=flora,
    stem_allometry=constant_allometries,
    whole_crown_gpp=whole_crown_gpp_varying,
)
/home/docs/checkouts/readthedocs.org/user_builds/pyrealm/checkouts/latest/pyrealm/core/experimental.py:72: ExperimentalFeatureWarning: 'Be aware that StemAllometry is an experimental feature of pyrealm and the implementation and API may change within major versions.'
  warn(qualname, ExperimentalFeatureWarning)
fig, axes = plt.subplots(ncols=2, nrows=5, sharex=True, figsize=(10, 12))

axes = axes.flatten()

for ax, (var, ylab) in zip(axes, plot_details):
    ax.plot(potential_gpp_varying, getattr(allocation_2, var), label=flora.name)
    ax.set_xlabel("Potential GPP")
    ax.set_ylabel(ylab)

    if var == "whole_crown_gpp":
        ax.legend(frameon=False)

# Delete unused panel in 5 x 2 grid
fig.delaxes(axes[-1])
../../_images/9c77b10fd0c54d0721ad80741b8defc717069dd4aad520eb098f59c9f145e36a.png

As before, the to_pandas() method of the StemAllometry() class can be used to export the data for each stem:

allocation.to_pandas().transpose()
column_stem_index 0 0 0 0 0 0 0 0 0 0 ... 2 2 2 2 2 2 2 2 2 2
whole_crown_gpp 5.384732 5.384732 5.384732 5.384732 5.384732 5.384732 5.384732 5.384732 5.384732 5.384732 ... 7.545905 7.545905 7.545905 7.545905 7.545905 7.545905 7.545905 7.545905 7.545905 7.545905
sapwood_respiration 0.000377 0.002829 0.008920 0.019706 0.035810 0.057505 0.084797 0.117495 0.155272 0.197715 ... 73.077982 73.627143 74.175934 74.724364 75.272442 75.820177 76.367578 76.914654 77.461414 78.007865
foliar_respiration 0.538473 0.538473 0.538473 0.538473 0.538473 0.538473 0.538473 0.538473 0.538473 0.538473 ... 0.754591 0.754591 0.754591 0.754591 0.754591 0.754591 0.754591 0.754591 0.754591 0.754591
fine_root_respiration 0.008089 0.030583 0.065116 0.109667 0.162514 0.222190 0.287451 0.357245 0.430681 0.507010 ... 33.133167 33.357896 33.582530 33.807071 34.031521 34.255883 34.480160 34.704354 34.928467 35.152503
reproductive_tissue_respiration 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
npp 2.902676 2.887708 2.863334 2.830131 2.788761 2.739938 2.684406 2.622911 2.556184 2.484920 ... -59.651900 -60.116235 -60.580290 -61.044072 -61.507589 -61.970847 -62.433854 -62.896616 -63.359140 -63.821432
foliage_turnover 0.000931 0.003519 0.007492 0.012617 0.018697 0.025563 0.033072 0.041102 0.049550 0.058332 ... 3.812020 3.837876 3.863720 3.889554 3.915377 3.941191 3.966994 3.992788 4.018572 4.044348
fine_root_turnover 0.008519 0.032209 0.068578 0.115498 0.171154 0.234002 0.302733 0.376237 0.453577 0.533965 ... 34.894649 35.131326 35.367902 35.604380 35.840762 36.077053 36.313253 36.549366 36.785394 37.021340
reproductive_tissue_turnover 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
delta_dbh 0.581376 0.205379 0.107872 0.067370 0.046323 0.033812 0.025671 0.020015 0.015885 0.012748 ... -0.013768 -0.013780 -0.013793 -0.013805 -0.013816 -0.013828 -0.013840 -0.013851 -0.013862 -0.013873
delta_stem_mass 1.471862 1.929432 2.118555 2.188913 2.191902 2.150818 2.078310 1.982189 1.867750 1.738829 ... -97.877197 -98.603832 -99.330079 -100.055947 -100.781447 -101.506589 -102.231382 -102.955836 -103.679960 -104.403763
delta_foliage_mass 0.420522 0.272943 0.197843 0.151806 0.120416 0.097502 0.079968 0.066090 0.054824 0.045501 ... -0.142418 -0.142486 -0.142554 -0.142621 -0.142687 -0.142752 -0.142816 -0.142880 -0.142943 -0.143005
delta_fine_root_mass 1.000842 0.649605 0.470866 0.361297 0.286591 0.232054 0.190324 0.157294 0.130482 0.108293 ... -0.338955 -0.339118 -0.339279 -0.339437 -0.339594 -0.339749 -0.339903 -0.340054 -0.340204 -0.340352

13 rows × 477 columns