Skip to content

Commit 9bdbd5e

Browse files
Time series tutorials: Adding Part 4 - Temporal accumulation (#58)
--------- Co-authored-by: Anna Petrasova <[email protected]>
1 parent 047c0a7 commit 9bdbd5e

File tree

2 files changed

+392
-0
lines changed

2 files changed

+392
-0
lines changed
63.2 KB
Loading
Lines changed: 392 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,392 @@
1+
---
2+
title: "Time series accumulations"
3+
author: "Veronica Andreo"
4+
date: 2024-07-26
5+
date-modified: today
6+
image: images/north_italy_LST_Aedes_albopictus.png
7+
format:
8+
ipynb: default
9+
html:
10+
toc: true
11+
code-tools: true
12+
code-copy: true
13+
code-fold: false
14+
categories: [time series, raster, advanced, Python]
15+
description: Tutorial on accumulation of time series values to identify suitable areas for mosquitoes and the number and duration of their cycles.
16+
engine: jupyter
17+
execute:
18+
eval: false
19+
---
20+
21+
In this fourth tutorial on time series, we will go through data
22+
accumulation. We'll mostly follow a modified version of the example
23+
presented in the
24+
[t.rast.accumulate](https://grass.osgeo.org/grass-stable/manuals/t.rast.accumulate.html)
25+
and [t.rast.accdetect](https://grass.osgeo.org/grass-stable/manuals/t.rast.accdetect.html)
26+
tools.
27+
28+
::: {.callout-note title="Setup"}
29+
This tutorial can be run locally or in Google Colab. However, make sure you
30+
install GRASS 8.4+, download the LST sample data and set up your project
31+
as explained in the [first](time_series_management_and_visualization.qmd)
32+
time series tutorial.
33+
:::
34+
35+
```{python}
36+
#| echo: false
37+
38+
import os
39+
import sys
40+
import subprocess
41+
42+
# Ask GRASS where its Python packages are
43+
sys.path.append(
44+
subprocess.check_output(["grass", "--config", "python_path"], text=True).strip()
45+
)
46+
# Import the GRASS packages we need
47+
import grass.script as gs
48+
import grass.jupyter as gj
49+
50+
path_to_project = "italy_eu_laea/italy_LST_daily"
51+
52+
# Start the GRASS Session
53+
session = gj.init(path_to_project)
54+
```
55+
56+
57+
## Temporal data accumulation
58+
59+
[t.rast.accumulate](https://grass.osgeo.org/grass-stable/manuals/t.rast.accumulate.html)
60+
performs temporal accumulations of raster time series. Data
61+
accumulations are common in ecology or agriculture, especially temperature
62+
accumulation. Usually, to determine if insects or plants can survive in a
63+
certain place, a measure of accumulated temperature is used. For example, a
64+
certain plant species might need x growing degree days (GDD) to bloom, or a
65+
mosquito species might need x GDD to complete their development from egg to
66+
adult. Therefore, it is usual to accumulate temperature data, but other
67+
variables can be accumulated too, e.g. chlorophyll concentration to determine
68+
algal bloom occurrences in water bodies.
69+
70+
*t.rast.accumulate* expects a raster time series (STRDS) as input that will be
71+
sampled with a given granularity. All maps that have the start time during the
72+
actual granule will be accumulated with the predecessor granule accumulation
73+
result using the raster tool
74+
[r.series.accumulate](https://grass.osgeo.org/grass-stable/manuals/r.series.accumulate.html).
75+
The default granularity is one day, but any temporal granularity can be set.
76+
The start and end time of the accumulation process must be set. In
77+
addition, a cycle can be specified to defines after which interval of time
78+
the accumulation process restarts. The offset option specifies the time that
79+
should be skipped between two cycles.
80+
81+
The lower and upper limits of the accumulation process can be set either by
82+
using space time raster datasets or by using fixed values for all raster cells
83+
and time steps by means of the limits option is used, eg. `limits=10,30`. The
84+
upper limit is only used in the Biologically Effective Degree Days (BEDD)
85+
calculation.
86+
87+
The output is a new space time raster dataset with the provided start time,
88+
end time and granularity containing the accumulated raster maps.
89+
90+
91+
### Accumulation using BEDD
92+
93+
Let's consider the mosquito *Aedes albopictus*. Adults require a minimum average
94+
temperature of 11 °C to survive. We will compute the Biologically Effective
95+
Degree Days (BEDD) from 2014 until 2018 for each year with a granularity of
96+
one day.
97+
The base temperature will be 11°C, and the upper limit 30°C where adult mosquito
98+
survival is known to decrease. Hence the accumulation will start at 11°C and stop
99+
at 30°C.
100+
101+
```{python}
102+
# Accumulation of degree days
103+
gs.run_command("t.rast.accumulate",
104+
input="lst_daily",
105+
output="mosq_daily_bedd",
106+
basename="mosq_daily_bedd",
107+
suffix="gran",
108+
start="2014-01-01",
109+
stop="2019-01-01",
110+
cycle="12 months",
111+
method="bedd",
112+
limits="11,30")
113+
```
114+
115+
```{python}
116+
# Get basic info
117+
gs.run_command("t.info", input="mosq_daily_bedd")
118+
```
119+
120+
121+
### Suitable areas for mosquitos
122+
123+
According to Kobashayi et al (2002), populations of *Aedes albopictus*
124+
establish where at least 1350 DD are accumulated. These DD should be reached
125+
before October 1st to consider a place as suitable. Let's find out when and where
126+
that condition is met.
127+
128+
We will first discard all cells with BEDD < 1350 from our accumulated time
129+
series, and then we will save the day of the year (DOY) where BEDD >=
130+
1350.
131+
132+
```{python}
133+
exp="doy_bedd_higher_1350 = if(mosq_daily_bedd >= 1350, start_doy(mosq_daily_bedd, 0), null())"
134+
135+
gs.run_command("t.rast.algebra",
136+
expression=exp,
137+
basename="doy_bedd_higher_1350",
138+
suffix="gran",
139+
nprocs=4)
140+
```
141+
142+
Then, we aggregate the `doy_bedd_higher_1350` STRDS on an annual basis, as we want
143+
to see possible changes among years. We use `method=minimum` to get the earliest
144+
day on which the condition is met each year.
145+
146+
```{python}
147+
gs.run_command("t.rast.aggregate",
148+
input="doy_bedd_higher_1350",
149+
method="minimum",
150+
granularity="1 year",
151+
output="annual_doy_bedd_higher_1350",
152+
basename="annual_doy_bedd_higher_1350",
153+
suffix="gran",
154+
nprocs=4)
155+
```
156+
157+
```{python}
158+
gs.run_command("t.rast.list",
159+
input="annual_doy_bedd_higher_1350",
160+
columns="id,min,max")
161+
```
162+
163+
Following Neteler et al (2013), if the 1350 DD are achieved on or before August
164+
1st, a place is considered highly suitable for *Aedes albopictus*, while if the
165+
condition is met after October 1st, the place is not suitable.
166+
Everything in between is defined as a linear function of DOY. Once again, we use
167+
the temporal algebra to create yearly suitability maps. Note we are using a
168+
nested if statement.
169+
170+
```{python}
171+
expression="suitability = if(annual_doy_bedd_higher_1350 <= 214, 1, if(annual_doy_bedd_higher_1350 > 214 && annual_doy_bedd_higher_1350 <= 274, (274 - annual_doy_bedd_higher_1350)/60.0, if(annual_doy_bedd_higher_1350 > 274, 0)))"
172+
173+
gs.run_command("t.rast.algebra",
174+
expression=expression,
175+
basename="suitability",
176+
suffix="gran",
177+
nprocs=4)
178+
```
179+
180+
Let's see how suitable area and suitability values change with time by means of
181+
an animation.
182+
183+
```{python}
184+
# Animation of annual anomalies
185+
suitability = gj.TimeSeriesMap()
186+
suitability.add_raster_series("suitability", fill_gaps=False)
187+
suitability.d_legend(at=(6, 10, 5, 45))
188+
suitability.show()
189+
```
190+
191+
Let's do some basic math to quantify the suitable area increase
192+
from 2014 to 2018. We use `r.univar` to get the number of non-null cells in each
193+
map.
194+
195+
```{python}
196+
land_cells = gs.parse_command("r.univar", map="lst_2014.001_avg", flags="g")['n']
197+
suit_2014 = gs.parse_command("r.univar", map="suitability_2014", flags="g")['n']
198+
suit_2018 = gs.parse_command("r.univar", map="suitability_2018", flags="g")['n']
199+
change = ((float(suit_2018) - float(suit_2014))/float(land_cells))*100.0
200+
print(f"The increase in suitable area was {change: .1f} %")
201+
```
202+
203+
204+
## Detection of cycles
205+
206+
[t.rast.accdetect](https://grass.osgeo.org/grass-stable/manuals/t.rast.accdetect.html)
207+
is used to detect accumulation patterns in temporally
208+
accumulated STDRS created by *t.rast.accumulate*. The start and end time do not
209+
need to be the same but the cycle and offset options must be exactly the same
210+
that were used in the accumulation process that generated the input STRDS.
211+
Minimum and maximum values for pattern detection can be set either by using
212+
STRDS or fixed values for all raster cells and time steps (`range` option).
213+
214+
Using a STRDS would allow specifying minimum and maximum values for each raster
215+
cell and each time step. For example, if you want to detect the germination
216+
(minimum value) and harvesting (maximum value) dates for different crops using
217+
the growing-degree-day (GDD) method for several years. Different crops may
218+
grow in different raster cells and change with time because of crop rotation.
219+
Hence we need to specify different GDD germination/harvesting (minimum/maximum)
220+
values for different raster cells and different years.
221+
222+
The *t.rast.accdetect* tool produces two output STRDS:
223+
224+
- **occurrence**: The occurrence STRDS stores the time in days from the
225+
beginning of a given cycle for each raster. These values can be used to
226+
compute the duration of the recognized accumulation pattern.
227+
- **indicator**: The indicator STRDS uses three integer values to mark raster
228+
cells as beginning (1), intermediate state (2) or end (3) of an accumulation
229+
pattern. These values can be used to identify places with complete cycles.
230+
231+
232+
### Detection of mosquito generations
233+
234+
Following Kobashayi et al (2002), each mosquito generation might take around
235+
365 DD. Let's use this reference value to identify how many mosquito
236+
generations we could expect over our study area.
237+
238+
```{python}
239+
cycle = list(range(1, 10))
240+
cycle_beg = list(range(1, 3286, 365))
241+
cycle_end = list(range(365, 3286, 365))
242+
243+
for i in range(len(cycle)):
244+
print(f"cycle: {cycle[i]} - {cycle_beg[i]} {cycle_end[i]}")
245+
246+
# Identify generations
247+
gs.run_command("t.rast.accdetect",
248+
input="mosq_daily_bedd",
249+
occurrence=f"mosq_occurrence_gen_{cycle[i]}",
250+
indicator=f"mosq_indicator_gen_{cycle[i]}",
251+
basename=f"mosq_gen_{cycle[i]}",
252+
start="2014-01-01",
253+
stop="2019-01-01",
254+
cycle="12 months",
255+
range=f"{cycle_beg[i]},{cycle_end[i]}")
256+
257+
gs.run_command("t.rast.aggregate",
258+
input=f"mosq_indicator_gen_{cycle[i]}",
259+
output=f"mosq_gen{cycle[i]}_yearly",
260+
basename=f"mosq_gen{cycle[i]}_yearly",
261+
granularity="1 year",
262+
method="maximum",
263+
suffix="gran")
264+
265+
# Keep only complete generations
266+
exp = f"if(mosq_gen{cycle[i]}_yearly == 3, {cycle[i]}, null())"
267+
gs.run_command("t.rast.mapcalc",
268+
input=f"mosq_gen{cycle[i]}_yearly",
269+
output=f"mosq_gen{cycle[i]}_yearly_clean",
270+
basename=f"mosq_clean_gen{cycle[i]}",
271+
expression=exp)
272+
273+
# Duration of each mosquito generation
274+
# Beginning
275+
gs.run_command("t.rast.aggregate",
276+
input=f"mosq_occurrence_gen_{cycle[i]}",
277+
output=f"mosq_min_day_gen{cycle[i]}",
278+
basename=f"occ_min_day_gen{cycle[i]}",
279+
method="minimum",
280+
granularity="1 year",
281+
suffix="gran")
282+
# End
283+
gs.run_command("t.rast.aggregate",
284+
input=f"mosq_occurrence_gen_{cycle[i]}",
285+
output=f"mosq_max_day_gen{cycle[i]}",
286+
basename=f"occ_max_day_gen{cycle[i]}",
287+
method="maximum",
288+
granularity="1 year",
289+
suffix="gran")
290+
# Difference
291+
exp = f"mosq_max_day_gen{cycle[i]} - mosq_min_day_gen{cycle[i]} + 1"
292+
gs.run_command("t.rast.mapcalc",
293+
input=f"mosq_min_day_gen{cycle[i]},mosq_max_day_gen{cycle[i]}",
294+
output=f"mosq_duration_gen{cycle[i]}",
295+
basename=f"mosq_duration_gen{cycle[i]}",
296+
expression=exp)
297+
```
298+
299+
300+
### Maximum number of generations per year
301+
302+
Let's now see which is the maximum number of generations in each cell.
303+
304+
```{python}
305+
for i in range(1, 6):
306+
maps = gs.list_grouped(type="raster", pattern=f"mosq_clean_gen*_{i}")
307+
gs.run_command("r.series",
308+
input=maps,
309+
output=f"mosq_max_n_generations_{i}",
310+
method="maximum")
311+
```
312+
313+
Let's see an animation:
314+
315+
```{python}
316+
# List of average maps
317+
map_list = gs.list_grouped(type="raster", pattern="mosq_max_n_generations_*")
318+
319+
# Animation with SeriesMap class
320+
series = gj.SeriesMap()
321+
series.add_rasters(map_list)
322+
series.d_barscale()
323+
series.show()
324+
```
325+
326+
327+
### Median duration of mosquito generations per year
328+
329+
We can also estimate the median duration of the mosquito cycles per year and see
330+
the results with an animation.
331+
332+
```{python}
333+
for i in range(1, 6):
334+
maps = gs.list_grouped(type="raster", pattern=f"mosq_duration_gen*_{i}")
335+
gs.run_command("r.series",
336+
input=maps,
337+
output=f"mosq_med_duration_generations_{i}",
338+
method="median")
339+
```
340+
341+
```{python}
342+
# List of average maps
343+
map_list = gs.list_grouped(type="raster", pattern="mosq_med_duration_generations_*")
344+
345+
# Animation with SeriesMap class
346+
series = gj.SeriesMap()
347+
series.add_rasters(map_list)
348+
series.d_barscale()
349+
series.show()
350+
```
351+
352+
This process creates a large number of intermediate maps. When we have
353+
obtained the desired output, we can remove all intermediate series and
354+
maps with [t.remove](https://grass.osgeo.org/grass-stable/manuals/t.remove.html).
355+
356+
```{python}
357+
gs.run_command("t.list",
358+
type="strds",
359+
where="name LIKE '%gen%'",
360+
output="to_remove.txt")
361+
gs.run_command("t.remove",
362+
flags="df",
363+
file="to_remove.txt")
364+
```
365+
366+
367+
## References
368+
369+
- Neteler, M., Metz, M., Rocchini, D., Rizzoli, A., Flacio, E., et al. 2013.
370+
_Is Switzerland Suitable for the Invasion ofAedes albopictus?_ PLOS ONE 8(12).
371+
[DOI](https://doi.org/10.1371/journal.pone.0082090).
372+
- Kobayashi, M., Nihei, N., Kurihara, T. 2002.
373+
_Analysis of Northern Distribution of *Aedes albopictus* (Diptera: Culicidae) in Japan by Geographical Information System._
374+
Journal of Medical Entomology 39(1), 4–11. [DOI](https://doi.org/10.1603/0022-2585-39.1.4).
375+
- Gebbert, S., Pebesma, E. 2014.
376+
_TGRASS: A temporal GIS for field based environmental modeling._
377+
Environmental Modelling & Software 53, 1-12.
378+
[DOI](http://dx.doi.org/10.1016/j.envsoft.2013.11.001).
379+
- Gebbert, S., Pebesma, E. 2017. _The GRASS GIS temporal framework._
380+
International Journal of Geographical Information Science 31, 1273-1292.
381+
[DOI](http://dx.doi.org/10.1080/13658816.2017.1306862).
382+
- [Temporal data processing](https://grasswiki.osgeo.org/wiki/Temporal_data_processing) wiki page.
383+
384+
385+
***
386+
387+
:::{.smaller}
388+
The development of this tutorial was funded by the US
389+
[National Science Foundation (NSF)](https://www.nsf.gov/),
390+
award [2303651](https://www.nsf.gov/awardsearch/showAward?AWD_ID=2303651).
391+
:::
392+

0 commit comments

Comments
 (0)