Skip to content

Commit ed29b96

Browse files
authored
Merge pull request #79 from davidorme/release/0.10.1
Release 0.10.1
2 parents 9c7e1dd + 4f9c9b4 commit ed29b96

File tree

7 files changed

+172
-42
lines changed

7 files changed

+172
-42
lines changed

CHANGES.txt

+3-1
Original file line numberDiff line numberDiff line change
@@ -140,4 +140,6 @@
140140
new function is calc_soilmstress_mengoli.
141141
- The soilmstress argument to PModel is removed and both the Mengoli and Stocker
142142
approaches are now intended to be applied as penalties to GPP after P Model
143-
fitting, allowing the two to be compared from the same P Model outputs.
143+
fitting, allowing the two to be compared from the same P Model outputs.
144+
0.10.1 - Addition of draft extention of subdaily memory_effect function that allows for
145+
missing data arising from non-estimable Jmax and Vcmax.

docs/source/users/pmodel/subdaily_details/subdaily_calculations.md

+21-19
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ steps used in the estimation process in order to show intermediates results but
2020
practice, as shown in the [worked example](worked_example), most of these calculations
2121
are handled internally by the model fitting in `pyrealm`.
2222

23-
```{code-cell} ipython3
23+
```{code-cell}
2424
:tags: [hide-input]
2525
2626
from importlib import resources
@@ -46,16 +46,15 @@ The code below uses half hourly data from 2014 for the [BE-Vie FluxNET
4646
site](https://fluxnet.org/doi/FLUXNET2015/BE-Vie), which was also used as a
4747
demonstration in {cite:t}`mengoli:2022a`.
4848

49-
```{code-cell} ipython3
50-
49+
```{code-cell}
5150
data_path = resources.files("pyrealm_build_data") / "subdaily_BE_Vie_2014.csv"
5251
data = np.genfromtxt(
5352
data_path,
5453
names=True,
5554
delimiter=",",
5655
dtype=None,
5756
encoding="UTF8",
58-
missing_values = "NA",
57+
missing_values="NA",
5958
)
6059
6160
# Extract the key half hourly timestep variables
@@ -74,7 +73,7 @@ This dataset can then be used to calculate the photosynthetic environment at the
7473
subdaily timescale. The code below also estimates GPP under the standard P Model with no
7574
slow responses for comparison.
7675

77-
```{code-cell} ipython3
76+
```{code-cell}
7877
# Calculate the photosynthetic environment
7978
subdaily_env = PModelEnvironment(
8079
tc=temp_subdaily,
@@ -100,7 +99,7 @@ best to sample those conditions. Typically those might be the observed environme
10099
conditions at the observation closest to noon, or the mean environmental conditions in a
101100
window around noon.
102101

103-
```{code-cell} ipython3
102+
```{code-cell}
104103
# Create the fast slow scaler
105104
fsscaler = FastSlowScaler(datetime_subdaily)
106105
@@ -114,19 +113,18 @@ fsscaler.set_window(
114113
pmodel_fastslow = FastSlowPModel(
115114
env=subdaily_env,
116115
fs_scaler=fsscaler,
116+
handle_nan=True,
117117
ppfd=ppfd_subdaily,
118118
fapar=fapar_subdaily,
119119
)
120120
```
121121

122-
```{code-cell} ipython3
122+
```{code-cell}
123123
:tags: [hide-input]
124124
125125
idx = np.arange(48 * 120, 48 * 130)
126126
plt.figure(figsize=(10, 4))
127-
plt.plot(
128-
datetime_subdaily[idx], pmodel_subdaily.gpp[idx], label="Instantaneous model"
129-
)
127+
plt.plot(datetime_subdaily[idx], pmodel_subdaily.gpp[idx], label="Instantaneous model")
130128
plt.plot(datetime_subdaily[idx], pmodel_fastslow.gpp[idx], "r-", label="Slow responses")
131129
plt.ylabel = "GPP"
132130
plt.legend(frameon=False)
@@ -145,7 +143,7 @@ The daily average conditions during the acclimation window can be sampled and us
145143
inputs to the standard P Model to calculate the optimal behaviour of plants under those
146144
conditions.
147145

148-
```{code-cell} ipython3
146+
```{code-cell}
149147
# Get the daily acclimation conditions for the forcing variables
150148
temp_acclim = fsscaler.get_daily_means(temp_subdaily)
151149
co2_acclim = fsscaler.get_daily_means(co2_subdaily)
@@ -176,7 +174,7 @@ temperatures so $J_{max}$ and $V_{cmax}$ must first be standardised to expected
176174
at 25°C. This is acheived by multiplying by the reciprocal of the exponential part of
177175
the Arrhenius equation ($h^{-1}$ in {cite}`mengoli:2022a`).
178176

179-
```{code-cell} ipython3
177+
```{code-cell}
180178
# Are these any of the existing values in the constants?
181179
ha_vcmax25 = 65330
182180
ha_jmax25 = 43900
@@ -191,18 +189,18 @@ jmax25_acclim = pmodel_acclim.jmax * (1 / calc_ftemp_arrh(tk_acclim, ha_jmax25))
191189
The memory effect can now be applied to the three parameters with slow
192190
responses to calculate realised values, here using the default 15 day window.
193191

194-
```{code-cell} ipython3
192+
```{code-cell}
195193
# Calculation of memory effect in xi, vcmax25 and jmax25
196194
xi_real = memory_effect(pmodel_acclim.optchi.xi, alpha=1 / 15)
197-
vcmax25_real = memory_effect(vcmax25_acclim, alpha=1 / 15)
198-
jmax25_real = memory_effect(jmax25_acclim, alpha=1 / 15)
195+
vcmax25_real = memory_effect(vcmax25_acclim, alpha=1 / 15, handle_nan=True)
196+
jmax25_real = memory_effect(jmax25_acclim, alpha=1 / 15, handle_nan=True)
199197
```
200198

201199
The plots below show the instantaneously acclimated values for $J_{max25}$,
202200
$V_{cmax25}$ and $\xi$ in grey along with the realised slow reponses.
203201
applied.
204202

205-
```{code-cell} ipython3
203+
```{code-cell}
206204
:tags: [hide-input]
207205
208206
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
@@ -236,7 +234,7 @@ temperature at fast scales:
236234
* These values are adjusted to the actual half hourly temperatures to give the fast
237235
responses of $J_{max}$ and $V_{cmax}$.
238236

239-
```{code-cell} ipython3
237+
```{code-cell}
240238
tk_subdaily = subdaily_env.tc + pmodel_subdaily.const.k_CtoK
241239
242240
# Fill the realised jmax and vcmax from subdaily to daily
@@ -256,7 +254,7 @@ $\Gamma^\ast$ with the realised slow responses of $\xi$. The original implementa
256254
$\Gamma^{\ast}$ and $c_a$, interpolated to the subdaily timescale and the actual
257255
subdaily variation in VPD.
258256

259-
```{code-cell} ipython3
257+
```{code-cell}
260258
# Interpolate xi to subdaily scale
261259
xi_subdaily = fsscaler.fill_daily_to_subdaily(xi_real)
262260
@@ -273,7 +271,7 @@ Model, where $c_i$ includes the slow responses of $\xi$ and $V_{cmax}$ and $J_{m
273271
include the slow responses of $V_{cmax25}$ and $J_{max25}$ and fast responses to
274272
temperature.
275273

276-
```{code-cell} ipython3
274+
```{code-cell}
277275
# Calculate Ac
278276
Ac_subdaily = (
279277
vcmax_subdaily
@@ -300,3 +298,7 @@ GPP_subdaily = np.minimum(Ac_subdaily, Aj_subdaily) * PModelConst.k_c_molmass
300298
diff = GPP_subdaily - pmodel_fastslow.gpp
301299
print(np.nanmin(diff), np.nanmax(diff))
302300
```
301+
302+
```{code-cell}
303+
304+
```

docs/source/users/pmodel/subdaily_details/worked_example.md

+8-7
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ focal_datetime = np.where(datetimes == np.datetime64("2018-06-12 12:00:00"))[0]
6363
# Plot the temperature data for an example timepoint and show the sites
6464
focal_temp = ds["Tair"][focal_datetime] - 273.15
6565
focal_temp.plot()
66-
plt.plot(sites["lon"], sites["lat"], "xr");
66+
plt.plot(sites["lon"], sites["lat"], "xr")
6767
```
6868

6969
The WFDE data need some conversion for use in the PModel, along with the definition of
@@ -119,7 +119,12 @@ fsscaler.set_window(
119119
half_width=np.timedelta64(1, "h"),
120120
)
121121
fs_pmod = FastSlowPModel(
122-
env=pm_env, fs_scaler=fsscaler, fapar=fapar, ppfd=ppfd, alpha=1 / 15
122+
env=pm_env,
123+
fs_scaler=fsscaler,
124+
handle_nan=True,
125+
fapar=fapar,
126+
ppfd=ppfd,
127+
alpha=1 / 15,
123128
)
124129
```
125130

@@ -161,7 +166,7 @@ ax2.set_title("Fast Slow")
161166
# Add a colour bar
162167
subfig2.colorbar(
163168
im, ax=[ax1, ax2], shrink=0.55, label=r"GPP ($\mu g C\,m^{-2}\,s^{-1}$)"
164-
);
169+
)
165170
```
166171

167172
## Time series predictions
@@ -200,7 +205,3 @@ for ax, st in zip(axes, sites["stid"].values):
200205
axes[0].legend(loc="lower center", bbox_to_anchor=[0.5, 1], ncols=2, frameon=False)
201206
plt.tight_layout()
202207
```
203-
204-
```{code-cell}
205-
206-
```

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "pyrealm"
3-
version = "0.10.0"
3+
version = "0.10.1"
44
description = "Python implementations of REALM models"
55
authors = ["David Orme <[email protected]>"]
66
homepage = "https://pyrealm.readthedocs.io/"

pyrealm/pmodel/subdaily.py

+87-14
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
)
1919

2020

21-
def memory_effect(values: NDArray, alpha: float = 0.067) -> NDArray:
21+
def memory_effect(
22+
values: NDArray, alpha: float = 0.067, handle_nan: bool = False
23+
) -> NDArray:
2224
r"""Apply a memory effect to a variable.
2325
2426
Three key photosynthetic parameters (:math:`\xi`, :math:`V_{cmax25}` and
@@ -27,37 +29,91 @@ def memory_effect(values: NDArray, alpha: float = 0.067) -> NDArray:
2729
average to apply a lagged response to one of these parameters.
2830
2931
The estimation uses the paramater `alpha` (:math:`\alpha`) to control the speed of
30-
convergence of the estimated values (:math:`E`) to the calculated optimal values
32+
convergence of the realised values (:math:`R`) to the calculated optimal values
3133
(:math:`O`):
3234
3335
.. math::
3436
35-
E_{t} = E_{t-1}(1 - \alpha) + O_{t} \alpha
37+
R_{t} = R_{t-1}(1 - \alpha) + O_{t} \alpha
3638
37-
For :math:`t_{0}`, the first value in the optimal values is used so :math:`E_{0} =
39+
For :math:`t_{0}`, the first value in the optimal values is used so :math:`R_{0} =
3840
O_{0}`.
3941
4042
The ``values`` array can have multiple dimensions but the first dimension is always
4143
assumed to represent time and the memory effect is calculated only along the first
4244
dimension.
4345
46+
By default, the ``values`` array must not contain missing values (`numpy.nan`).
47+
However, :math:`V_{cmax}` and :math:`J_{max}` are not estimable in some conditions
48+
(namely when :math:`m \le c^{\ast}`, see
49+
:class:`~pyrealm.pmodel.pmodel.CalcOptimalChi`) and so missing values in P Model
50+
predictions can arise even when the forcing data is complete, breaking the recursion
51+
shown above. When ``handle_nan=True``, this function fills missing data as follow:
52+
53+
+-------------------+--------+-------------------------------------------------+
54+
| | | Current optimal (:math:`O_{t}`) |
55+
+-------------------+--------+-----------------+-------------------------------+
56+
| | | NA | not NA |
57+
+-------------------+--------+-----------------+-------------------------------+
58+
| Previous | NA | NA | O_{t} |
59+
| realised +--------+-----------------+-------------------------------+
60+
| (:math:`R_{t-1}`) | not NA | :math:`R_{t-1}` | :math:`R_{t-1}(1-a) + O_{t}a` |
61+
+-------------------+--------+-----------------+-------------------------------+
62+
63+
Initial missing values are kept, and the first observed optimal value is accepted as
64+
the first realised value (as with the start of the recursion above). After this, if
65+
the current optimal value is missing, then the previous estimate of the realised
66+
value is held over until it can next be updated from observed data.
67+
4468
Args:
4569
values: The values to apply the memory effect to.
46-
alpha: The relative weight applied to the most recent observation
70+
alpha: The relative weight applied to the most recent observation.
71+
handle_nan: Allow missing values to be handled.
4772
4873
Returns:
4974
An array of the same shape as ``values`` with the memory effect applied.
5075
"""
5176

77+
# Check for nan and nan handling
78+
nan_present = np.any(np.isnan(values))
79+
if nan_present and not handle_nan:
80+
raise ValueError("Missing values in data passed to memory_effect")
81+
5282
# Initialise the output storage and set the first values to be a slice along the
5383
# first axis of the input values
5484
memory_values = np.empty_like(values, dtype=np.float32)
5585
memory_values[0] = values[0]
5686

57-
# Loop over the first axis, in each case taking slices through the first axis of the
58-
# inputs. This handles arrays of any dimension.
87+
# Handle the data if there are no missing data,
88+
if not nan_present:
89+
# Loop over the first axis, in each case taking slices through the first axis of
90+
# the inputs. This handles arrays of any dimension.
91+
for idx in range(1, len(memory_values)):
92+
memory_values[idx] = (
93+
memory_values[idx - 1] * (1 - alpha) + values[idx] * alpha
94+
)
95+
96+
return memory_values
97+
98+
# Otherwise, do the same thing but handling missing data at each step.
5999
for idx in range(1, len(memory_values)):
60-
memory_values[idx] = memory_values[idx - 1] * (1 - alpha) + values[idx] * alpha
100+
# Need to check for nan conditions:
101+
# - the previous value might be nan from an initial nan or sequence of nans, in
102+
# which case the current value is accepted without weighting - it could be nan
103+
# itself to extend a chain of initial nan values.
104+
# - the current value might be nan, in which case the previous value gets
105+
# held over as the current value.
106+
prev_nan = np.isnan(memory_values[idx - 1])
107+
curr_nan = np.isnan(values[idx])
108+
memory_values[idx] = np.where(
109+
prev_nan,
110+
values[idx],
111+
np.where(
112+
curr_nan,
113+
memory_values[idx - 1],
114+
memory_values[idx - 1] * (1 - alpha) + values[idx] * alpha,
115+
),
116+
)
61117

62118
return memory_values
63119

@@ -92,7 +148,8 @@ class FastSlowPModel:
92148
* The :meth:`~pyrealm.pmodel.subdaily.memory_effect` function is then used to
93149
calculate realised slowly responding values for :math:`\xi`, :math:`V_{cmax25}`
94150
and :math:`J_{max25}`, given a weight :math:`\alpha \in [0,1]` that sets the speed
95-
of acclimation.
151+
of acclimation. The ``handle_nan`` argument is passed to this function to set
152+
whether missing values in the optimal predictions are permitted and handled.
96153
* The realised values are then filled back onto the original subdaily timescale,
97154
with :math:`V_{cmax}` and :math:`J_{max}` then being calculated from the slowly
98155
responding :math:`V_{cmax25}` and :math:`J_{max25}` and the actual subdaily
@@ -107,6 +164,8 @@ class FastSlowPModel:
107164
fapar: The :math:`f_{APAR}` for each observation.
108165
ppfd: The PPDF for each observation.
109166
alpha: The :math:`\alpha` weight.
167+
handle_nan: Should the :func:`~pyrealm.pmodel.subdaily.memory_effect` function
168+
be allowe to handle missing values.
110169
kphio: The quantum yield efficiency of photosynthesis (:math:`\phi_0`, -).
111170
fill_kind: The approach used to fill daily realised values to the subdaily
112171
timescale, currently one of 'previous' or 'linear'.
@@ -120,6 +179,7 @@ def __init__(
120179
fapar: NDArray,
121180
alpha: float = 1 / 15,
122181
kphio: float = 1 / 8,
182+
handle_nan: bool = False,
123183
fill_kind: str = "previous",
124184
) -> None:
125185
# Warn about the API
@@ -192,11 +252,17 @@ def __init__(
192252
)
193253

194254
# Calculate the realised values from the instantaneous optimal values
195-
self.xi_real: NDArray = memory_effect(self.pmodel_acclim.optchi.xi, alpha=alpha)
255+
self.xi_real: NDArray = memory_effect(
256+
self.pmodel_acclim.optchi.xi, alpha=alpha, handle_nan=handle_nan
257+
)
196258
r"""Realised daily slow responses in :math:`\xi`"""
197-
self.vcmax25_real: NDArray = memory_effect(self.vcmax25_opt, alpha=alpha)
259+
self.vcmax25_real: NDArray = memory_effect(
260+
self.vcmax25_opt, alpha=alpha, handle_nan=handle_nan
261+
)
198262
r"""Realised daily slow responses in :math:`V_{cmax25}`"""
199-
self.jmax25_real: NDArray = memory_effect(self.jmax25_opt, alpha=alpha)
263+
self.jmax25_real: NDArray = memory_effect(
264+
self.jmax25_opt, alpha=alpha, handle_nan=handle_nan
265+
)
200266
r"""Realised daily slow responses in :math:`J_{max25}`"""
201267

202268
# Fill the daily realised values onto the subdaily scale
@@ -292,6 +358,8 @@ class FastSlowPModel_JAMES:
292358
fapar: The :math:`f_{APAR}` for each observation.
293359
ppfd: The PPDF for each observation.
294360
alpha: The :math:`\alpha` weight.
361+
handle_nan: Should the :func:`~pyrealm.pmodel.subdaily.memory_effect` function
362+
be allowe to handle missing values.
295363
kphio: The quantum yield efficiency of photosynthesis (:math:`\phi_0`, -).
296364
vpd_scaler: An alternate
297365
:class:`~pyrealm.pmodel.fast_slow_scaler.FastSlowScaler` instance used to
@@ -310,6 +378,7 @@ def __init__(
310378
ppfd: NDArray,
311379
fapar: NDArray,
312380
alpha: float = 1 / 15,
381+
handle_nan: bool = False,
313382
kphio: float = 1 / 8,
314383
vpd_scaler: Optional[FastSlowScaler] = None,
315384
fill_from: Optional[np.timedelta64] = None,
@@ -388,9 +457,13 @@ def __init__(
388457
)
389458

390459
# Calculate the realised values from the instantaneous optimal values
391-
self.vcmax25_real: NDArray = memory_effect(self.vcmax25_opt, alpha=alpha)
460+
self.vcmax25_real: NDArray = memory_effect(
461+
self.vcmax25_opt, alpha=alpha, handle_nan=handle_nan
462+
)
392463
r"""Realised daily slow responses in :math:`V_{cmax25}`"""
393-
self.jmax25_real: NDArray = memory_effect(self.jmax25_opt, alpha=alpha)
464+
self.jmax25_real: NDArray = memory_effect(
465+
self.jmax25_opt, alpha=alpha, handle_nan=handle_nan
466+
)
394467
r"""Realised daily slow responses in :math:`J_{max25}`"""
395468

396469
# Calculate the daily xi value, which does not have a slow reponse in this

0 commit comments

Comments
 (0)