Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Annual fAPAR_max and LAI_max #403

Draft
wants to merge 24 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
797b6b7
wip on fapar code
MarionBWeinzierl Jan 7, 2025
a20cfb5
added @davidorme 's example to the build data, adapted fapar_limitation
MarionBWeinzierl Jan 24, 2025
64f7f84
draft implementation for computing fapar_max from pmodel
MarionBWeinzierl Jan 27, 2025
73b4ffb
fixed typehints and typing, added docstrings etc.
MarionBWeinzierl Jan 28, 2025
dbbff60
fixed linting issues
MarionBWeinzierl Jan 29, 2025
54053fb
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 29, 2025
7e4d24a
make sure there isn't a strange string
MarionBWeinzierl Jan 29, 2025
169c143
resolved merge conflicts
MarionBWeinzierl Jan 29, 2025
88ed9a1
update import for lower Python versions
MarionBWeinzierl Jan 29, 2025
07ea2b2
very much wip on including growing season
MarionBWeinzierl Jan 31, 2025
283c69d
include growing season, slight refactor for farpar_limitation classes…
MarionBWeinzierl Feb 4, 2025
12045d5
merged mean and total fcts
MarionBWeinzierl Feb 4, 2025
ee7895f
Adding some documentation
MarionBWeinzierl Feb 4, 2025
9139375
Get rid of superfluous functions
MarionBWeinzierl Feb 4, 2025
f1dbac6
Accept @davidorme 's suggestion from code review
MarionBWeinzierl Feb 10, 2025
ee747ca
Accept @davidorme 's suggestion from code review
MarionBWeinzierl Feb 10, 2025
09621e7
Accept @davidorme 's suggestion from code review
MarionBWeinzierl Feb 10, 2025
ff3b195
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 10, 2025
62f8aa2
create phenology constants class
MarionBWeinzierl Feb 10, 2025
b0006a0
add check for array sizes
MarionBWeinzierl Feb 10, 2025
aea4ba0
add f_0 coefficients to phenology constants
MarionBWeinzierl Feb 10, 2025
87d4206
Accept @davidorme 's suggestion from code review
MarionBWeinzierl Feb 11, 2025
a077164
add datetimes check
MarionBWeinzierl Feb 12, 2025
feda9f3
started implementing tests for fapar limitation
MarionBWeinzierl Feb 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 208 additions & 0 deletions pyrealm/phenology/fapar_limitation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
"""Class to compute the fAPAR_max and annual peak Leaf Area Index (LAI)."""

import numpy as np
from numpy.ma.core import zeros
MarionBWeinzierl marked this conversation as resolved.
Show resolved Hide resolved
from numpy.typing import NDArray
from typing_extensions import Self

from pyrealm.pmodel import PModel, SubdailyScaler


def get_annual(
x: NDArray,
datetimes: NDArray[np.datetime64],
growing_season: NDArray[np.bool],
method: str,
) -> NDArray:
"""Computes an array of the annual total or mean of an entity x given datetimes.

Args:
x: Array of values to be converted to annual values. Should be either daily (
same datetimes as growing_season) or subdaily (same datetimes as datetimes
array)
datetimes: Datetimes of the measurements as np.datetime62 arrays.
MarionBWeinzierl marked this conversation as resolved.
Show resolved Hide resolved
growing_season: Bool array of days, indicating whether they are ain growing
season or not.
method: Either "total" (sum all values of the year) or "mean" (take the mean
of all values of the year)
"""

# Extract years from datetimes
all_years = [np.datetime64(i, "Y") for i in datetimes]
MarionBWeinzierl marked this conversation as resolved.
Show resolved Hide resolved

# Create scaler object to handle conversion between scales
scaler = SubdailyScaler(datetimes)
scaler.set_nearest(np.timedelta64(12, "h"))
MarionBWeinzierl marked this conversation as resolved.
Show resolved Hide resolved

if len(x) == len(growing_season):
daily_x = x
elif len(x) == len(datetimes):
# Convert values to daily to match with growing_season
daily_x = scaler.get_daily_means(x)
else:
raise ValueError("Input array does not fit datetimes nor growing_season array")

# Get rid of that extra dimension of size 1
years_by_day = np.squeeze(scaler.get_window_values(np.asarray(all_years)))
# Which years are present?
years = np.unique(all_years)

# Compute annual totals or means, only taking into account the days which are in
# growing season.

annual_x = zeros(len(years))

if method == "total":
for i in range(len(years)):
annual_x[i] = sum(
daily_x[growing_season & (years_by_day == years[i])].astype(np.int64)
)
elif method == "mean":
for i in range(len(years)):
annual_x[i] = np.mean(
daily_x[growing_season & (years_by_day == years[i])].astype(np.int64)
)
MarionBWeinzierl marked this conversation as resolved.
Show resolved Hide resolved
else:
raise ValueError("No valid method given for annual values")

return annual_x


def compute_annual_total_precip(
precip: NDArray, datetimes: NDArray[np.datetime64], growing_season: NDArray[np.bool]
) -> NDArray:
"""Returns the sum of annual precipitation."""

annual_precip = get_annual(precip, datetimes, growing_season, "total")

return convert_precipitation_to_molar(annual_precip)


def convert_precipitation_to_molar(precip_mm: NDArray) -> NDArray:
"""Convert precipitation from mm/m2 to mol/m2.

- 1 mm/m2 = 1000000 mm3 = 1000 mL = 1 L
- Molar mass of water = 18g
- Assuming density = 1 (but really temp varying), molar volume = 18mL
- So 1 mm/m2 = 1000 / 18 ~ 55.55 mols/m2
"""

water_mm_to_mol = 1000 / 18
precip_molar = precip_mm * water_mm_to_mol

return precip_molar


class FaparLimitation:
r"""FaparLimitation class to compute fAPAR_max and LAI.

This class takes the annual total potential GPP and precipitation, the annual
mean CA, Chi and VPD, as well as the aridity index and some constants to compute
the annual peak fractional absorbed photosynthetically active radiation (
fAPAR_max) and annual peak Leaf Area Index (LAI).
"""

def __init__(
self,
annual_total_potential_gpp: NDArray[np.float64],
annual_mean_ca: NDArray[np.float64],
annual_mean_chi: NDArray[np.float64],
annual_mean_vpd: NDArray[np.float64],
annual_total_precip: NDArray[np.float64],
aridity_index: NDArray[np.float64],
z: float = 12.227,
k: float = 0.5,
) -> None:
MarionBWeinzierl marked this conversation as resolved.
Show resolved Hide resolved
r"""Annual peak fractional absorbed photosynthetically active radiation (fAPAR).

Computes fAPAR_max as minimum of a water-limited and an energy-limited
quantity.

Args:
annual_total_potential_gpp: Aka A_0, the annual sum of potential GPP.
Potential GPP would be achieved if fAPAR = 1. [mol m^{-2} year^{-1}]
annual_mean_ca: Ambient CO2 partial pressure. [Pa]
annual_mean_chi: Annual mean ratio of leaf-internal CO2 partial pressure to
c_a during the growing season (>0℃). [Pa]
annual_mean_vpd: Aka D, annual mean vapour pressure deficit (VPD) during the
growing season (>0℃). [Pa]
annual_total_precip: Aka P, annual total precipitation. [mol m^{-2}
year^{-1}]
aridity_index: Aka AI, climatological estimate of local aridity index.
z: accounts for the costs of building and maintaining leaves and the total
below-ground allocation required to support the nutrient demand of those
leaves. [mol m^{-2} year^{-1}]
k: Light extinction coefficient.
"""

MarionBWeinzierl marked this conversation as resolved.
Show resolved Hide resolved
# f_0 is the ratio of annual total transpiration of annual total
# precipitation, which is an empirical function of the climatic Aridity Index
# (AI).
b = 0.604169
f_0 = 0.65 * np.exp(-b * np.log(aridity_index / 1.9))

fapar_energylim = 1.0 - z / (k * annual_total_potential_gpp)
fapar_waterlim = (
f_0
* annual_total_precip
* annual_mean_ca
* (1 - annual_mean_chi)
/ (1.6 * annual_mean_vpd * annual_total_potential_gpp)
)

self.faparmax = -9999 * np.ones(np.shape(fapar_waterlim))
MarionBWeinzierl marked this conversation as resolved.
Show resolved Hide resolved
self.energylim = -9999 * np.ones(np.shape(fapar_waterlim))
self.annual_precip_molar = annual_total_precip

for i in range(len(fapar_waterlim)):
if fapar_waterlim[i] < fapar_energylim[i]:
self.faparmax[i] = fapar_waterlim[i]
self.energylim[i] = False
else:
self.faparmax[i] = fapar_energylim[i]
self.energylim[i] = True
MarionBWeinzierl marked this conversation as resolved.
Show resolved Hide resolved

self.laimax = -(1 / k) * np.log(1.0 - self.faparmax)

@classmethod
def from_pmodel(
cls,
pmodel: PModel,
growing_season: NDArray[np.bool],
datetimes: NDArray[np.datetime64],
precip: NDArray[np.float64],
aridity_index: NDArray[np.float64],
) -> Self:
r"""Get FaparLimitation from PModel input.

Computes the input for fAPAR_max from the P Model and additional inputs.

Args:
pmodel: pyrealm.pmodel.PModel
growing_season: Bool array indicating which times are within growing
season by some definition and implementation.
datetimes: Array of datetimes to consider.
precip: Precipitation for given datetimes.
aridity_index: Climatological estimate of local aridity index.
"""

annual_total_potential_gpp = get_annual(
pmodel.gpp, datetimes, growing_season, "total"
Comment on lines +249 to +251
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this need to check we are dealing with whole years in the input?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering whether this should be something similar as in __init__ of scaler, or something much more simple (like checking that growing_season can be divided by 365 or 366).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - this was tying me in knots as well. I think it needs to be something similar to the scaler init. We want to:

  • Check that the timing is regular - although that is a pain when we want to support e.g. weekly or monthly inputs
  • Check that there are the same number of observations per year - although that is a pain for leap years at daily precision.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I implemented a check for the datetimes. I am sure I have missed something...

Probably a good time to implement tests.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It currently assumes daily or subdaily inputs

)
annual_mean_ca = get_annual(pmodel.env.ca, datetimes, growing_season, "mean")
annual_mean_chi = get_annual(
pmodel.optchi.chi.round(5), datetimes, growing_season, "mean"
)
annual_mean_vpd = get_annual(pmodel.env.vpd, datetimes, growing_season, "mean")
annual_total_precip = compute_annual_total_precip(
precip, datetimes, growing_season
)

return cls(
annual_total_potential_gpp,
annual_mean_ca,
annual_mean_chi,
annual_mean_vpd,
annual_total_precip,
aridity_index,
)
2 changes: 2 additions & 0 deletions pyrealm_build_data/LAI_in_pyrealm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
outputs/*
reports/*
10 changes: 10 additions & 0 deletions pyrealm_build_data/LAI_in_pyrealm/DE-GRI_site_data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"Dataset": "Flux2015",
"Site": "DE-Gri",
"Lat": 50.95,
"Lon": 13.5126,
"Vegetation": "GRA",
"Elev": 385,
"AI": 1.094517,
"f0": 0.540873739473232
}
Loading