18
18
)
19
19
20
20
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 :
22
24
r"""Apply a memory effect to a variable.
23
25
24
26
Three key photosynthetic parameters (:math:`\xi`, :math:`V_{cmax25}` and
@@ -27,37 +29,91 @@ def memory_effect(values: NDArray, alpha: float = 0.067) -> NDArray:
27
29
average to apply a lagged response to one of these parameters.
28
30
29
31
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
31
33
(:math:`O`):
32
34
33
35
.. math::
34
36
35
- E_ {t} = E_ {t-1}(1 - \alpha) + O_{t} \alpha
37
+ R_ {t} = R_ {t-1}(1 - \alpha) + O_{t} \alpha
36
38
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} =
38
40
O_{0}`.
39
41
40
42
The ``values`` array can have multiple dimensions but the first dimension is always
41
43
assumed to represent time and the memory effect is calculated only along the first
42
44
dimension.
43
45
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
+
44
68
Args:
45
69
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.
47
72
48
73
Returns:
49
74
An array of the same shape as ``values`` with the memory effect applied.
50
75
"""
51
76
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
+
52
82
# Initialise the output storage and set the first values to be a slice along the
53
83
# first axis of the input values
54
84
memory_values = np .empty_like (values , dtype = np .float32 )
55
85
memory_values [0 ] = values [0 ]
56
86
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.
59
99
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
+ )
61
117
62
118
return memory_values
63
119
@@ -92,7 +148,8 @@ class FastSlowPModel:
92
148
* The :meth:`~pyrealm.pmodel.subdaily.memory_effect` function is then used to
93
149
calculate realised slowly responding values for :math:`\xi`, :math:`V_{cmax25}`
94
150
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.
96
153
* The realised values are then filled back onto the original subdaily timescale,
97
154
with :math:`V_{cmax}` and :math:`J_{max}` then being calculated from the slowly
98
155
responding :math:`V_{cmax25}` and :math:`J_{max25}` and the actual subdaily
@@ -107,6 +164,8 @@ class FastSlowPModel:
107
164
fapar: The :math:`f_{APAR}` for each observation.
108
165
ppfd: The PPDF for each observation.
109
166
alpha: The :math:`\alpha` weight.
167
+ handle_nan: Should the :func:`~pyrealm.pmodel.subdaily.memory_effect` function
168
+ be allowe to handle missing values.
110
169
kphio: The quantum yield efficiency of photosynthesis (:math:`\phi_0`, -).
111
170
fill_kind: The approach used to fill daily realised values to the subdaily
112
171
timescale, currently one of 'previous' or 'linear'.
@@ -120,6 +179,7 @@ def __init__(
120
179
fapar : NDArray ,
121
180
alpha : float = 1 / 15 ,
122
181
kphio : float = 1 / 8 ,
182
+ handle_nan : bool = False ,
123
183
fill_kind : str = "previous" ,
124
184
) -> None :
125
185
# Warn about the API
@@ -192,11 +252,17 @@ def __init__(
192
252
)
193
253
194
254
# 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
+ )
196
258
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
+ )
198
262
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
+ )
200
266
r"""Realised daily slow responses in :math:`J_{max25}`"""
201
267
202
268
# Fill the daily realised values onto the subdaily scale
@@ -292,6 +358,8 @@ class FastSlowPModel_JAMES:
292
358
fapar: The :math:`f_{APAR}` for each observation.
293
359
ppfd: The PPDF for each observation.
294
360
alpha: The :math:`\alpha` weight.
361
+ handle_nan: Should the :func:`~pyrealm.pmodel.subdaily.memory_effect` function
362
+ be allowe to handle missing values.
295
363
kphio: The quantum yield efficiency of photosynthesis (:math:`\phi_0`, -).
296
364
vpd_scaler: An alternate
297
365
:class:`~pyrealm.pmodel.fast_slow_scaler.FastSlowScaler` instance used to
@@ -310,6 +378,7 @@ def __init__(
310
378
ppfd : NDArray ,
311
379
fapar : NDArray ,
312
380
alpha : float = 1 / 15 ,
381
+ handle_nan : bool = False ,
313
382
kphio : float = 1 / 8 ,
314
383
vpd_scaler : Optional [FastSlowScaler ] = None ,
315
384
fill_from : Optional [np .timedelta64 ] = None ,
@@ -388,9 +457,13 @@ def __init__(
388
457
)
389
458
390
459
# 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
+ )
392
463
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
+ )
394
467
r"""Realised daily slow responses in :math:`J_{max25}`"""
395
468
396
469
# Calculate the daily xi value, which does not have a slow reponse in this
0 commit comments