From edbb652613edece8d548fe0a402e2b01dd0d8c4e Mon Sep 17 00:00:00 2001 From: Veronica Andreo Date: Thu, 22 May 2025 11:05:20 -0300 Subject: [PATCH 1/6] Time series tutorials: Adding Part 2 - temporal aggregations --- ...timeline_plot_lst_monthly_and_seasonal.png | Bin 0 -> 12117 bytes .../time_series/time_series_aggregations.qmd | 430 ++++++++++++++++++ ...me_series_management_and_visualization.qmd | 4 +- 3 files changed, 432 insertions(+), 2 deletions(-) create mode 100644 content/tutorials/time_series/images/timeline_plot_lst_monthly_and_seasonal.png create mode 100644 content/tutorials/time_series/time_series_aggregations.qmd diff --git a/content/tutorials/time_series/images/timeline_plot_lst_monthly_and_seasonal.png b/content/tutorials/time_series/images/timeline_plot_lst_monthly_and_seasonal.png new file mode 100644 index 0000000000000000000000000000000000000000..5d3f6e6c05dcec7c35c785d1afda7b527cb455fa GIT binary patch literal 12117 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 literal 0 HcmV?d00001 diff --git a/content/tutorials/time_series/time_series_aggregations.qmd b/content/tutorials/time_series/time_series_aggregations.qmd new file mode 100644 index 0000000..db2e450 --- /dev/null +++ b/content/tutorials/time_series/time_series_aggregations.qmd @@ -0,0 +1,430 @@ +--- +title: "Time series aggregation" +author: "Veronica Andreo" +date: 2024-07-23 +date-modified: today +image: images/timeline_plot_lst_monthly_and_seasonal.png +format: + ipynb: default + html: + toc: true + code-tools: true + code-copy: true + code-fold: false +categories: [time series, raster, advanced, Python] +description: Tutorial on different types of raster time series aggregations, i.e., with granularity, full series, long term averages, and their visualization. +engine: jupyter +execute: + eval: false +--- + +In this second part of the time series tutorials, we will go through different +ways of performing **aggregations**. Temporal aggregation is a very common task +when working with time series as it allows us to summarize information and +find patterns of change both in space and time. + +::: {.callout-note title="Setup"} +This tutorial can be run locally or in Google Colab. However, make sure you +install GRASS GIS 8.4+, download the sample data and set up your project +as explained in [part 1](time_series_management_and_visualization.qmd) +of these time series tutorials. +::: + +There are two main tools to do time series aggregations in GRASS GIS: +[t.rast.aggregate](https://grass.osgeo.org/grass-stable/manuals/t.rast.aggregate.html) +and [t.rast.series](https://grass.osgeo.org/grass-stable/manuals/t.rast.series.html). +We'll demonstrate their usage in the upcoming sections. + +```{python} +#| echo: false + +import os +import sys +import subprocess + +# GRASS GIS database variables +grassbin = "grass-dev" +grassdata = os.path.join(os.path.expanduser('~'), "grassdata") +project = "eu_laea" +mapset = "italy_LST_daily" + +sys.path.append( + subprocess.check_output([grassbin, "--config", "python_path"], text=True).strip() +) +``` + +```{python} +#| echo: false + +# Import the GRASS GIS packages we need +import grass.script as gs +import grass.jupyter as gj + +# Start the GRASS GIS Session +session = gj.init(grassdata, project, mapset); +``` + +## Aggregation with granularity + +**Granularity** is the greatest common divisor of the temporal extents +(and possible gaps) of all maps in a space-time dataset. To perform time +series aggregation with granularity, we use +[t.rast.aggregate](https://grass.osgeo.org/grass-stable/manuals/t.rast.aggregate.html). +This tool allows us to aggregate our time series into larger granularities, i.e., +from hourly to daily, from daily to weekly, monthly, etc. It also permits to +aggregate with *ad hoc* granularities like 3 minutes, 5 days, 3 months, etc. +Supported aggregate methods include average, minimum, maximum, median, etc. See +the [r.series](https://grass.osgeo.org/grass-stable/manuals/r.series.html) +manual page for more details. + +If you are working with data representing absolute time, *t.rast.aggregate* will +shift the start date for each aggregation process depending on the provided +temporal granularity as follows: + +- years: will start at the first of January, hence 14-08-2012 00:01:30 will be shifted to 01-01-2012 00:00:00 +- months: will start at the first day of a month, hence 14-08-2012 will be shifted to 01-08-2012 00:00:00 +- weeks: will start at the first day of a week (Monday), hence 14-08-2012 01:30:30 will be shifted to 13-08-2012 01:00:00 +- days: will start at the first hour of a day, hence 14-08-2012 00:01:30 will be shifted to 14-08-2012 00:00:00 +- hours: will start at the first minute of a hour, hence 14-08-2012 01:30:30 will be shifted to 14-08-2012 01:00:00 +- minutes: will start at the first second of a minute, hence 14-08-2012 01:30:30 will be shifted to 14-08-2012 01:30:00 + +The specification of the temporal relation between the aggregation intervals and +the raster map layers to be aggregated is always formulated from the aggregation +interval viewpoint. By default, it is set to *contains* to aggregate all maps +that are temporally within an aggregation interval. + +### Monthly and seasonal averages + +To demonstrate the basic usage of *t.rast.aggregate*, let's create monthly and +seasonal time series starting from the `lst_daily` time series we created +in the [time series management](time_series_management_and_visualization.qmd) +tutorial. + +```{python} +# Daily to monthly +gs.run_command("t.rast.aggregate", + input="lst_daily", + method="average", + granularity="1 months", + basename="lst_avg", + output="lst_monthly", + suffix="gran", + nprocs=4) +``` + +```{python} +# Daily to (sort of) seasonal +gs.run_command("t.rast.aggregate", + input="lst_daily", + method="average", + granularity="3 months", + basename="lst_avg", + output="lst_seasonal", + suffix="gran", + nprocs=4) +``` + +If we would like to follow the so called +[*meteorological seasons*](https://en.wikipedia.org/wiki/Season#Meteorological), +i.e., that spring started with March, then we could have used the *where* option +to shift the starting point of the aggregation period as follows: +`where="start_time >= '2013-03-01 00:00:00'"`. + +Let's compare the granularities using the timeline plot... + +```{python} +!g.gui.timeline lst_monthly,lst_seasonal +``` + +![](images//timeline_plot_lst_monthly_and_seasonal.png) + +and use `grass.jupyter` to create a nice animation of the seasonal time series: + +```{python} +lstseries = gj.TimeSeriesMap(use_region=True) +lstseries.add_raster_series("lst_seasonal", fill_gaps=False) +lstseries.d_legend(color="black", at=(10,40,2,6)) +lstseries.show() +``` + +```{python} +# Optionally, write out to animated GIF +lstseries.save("lstseries.gif") +``` + +For astronomical seasons, i.e., those defined by solstices and equinoxes, we can use +[t.rast.aggregate.ds](https://grass.osgeo.org/grass-stable/manuals/t.rast.aggregate.ds.html) +as in +[this example](https://grasswiki.osgeo.org/wiki/Temporal_data_processing/seasonal_aggregation), +or for an approximate result: + +```{python} +gs.run_command("t.rast.aggregate", + input="lst_daily", + method="average", + where="start_time >= '2013-03-21 00:00:00'", + granularity="3 months", + basename="lst_avg", + output="lst_seasonal", + suffix="gran", + nprocs=4) +``` + +```{python} +# Check info +gs.read_command("t.info", + input="lst_seasonal", + flags="g") +``` + +```{python} +# Check raster maps in the STRDS +gs.run_command("t.rast.list", input="lst_seasonal") +``` + +### Spring warming + +We define spring warming as the velocity with which temperature increases from +winter into spring and we can approximate it as the linear regression slope +among LST values of February, March and April. Let's see how to use +*t.rast.aggregate* to estimate yearly spring warming values. + +```{python} +# Define list of months +months=['{0:02d}'.format(m) for m in range(2,5)] +print(months) +``` + +```{python} +# Annual spring warming +gs.run_command("t.rast.aggregate", + input="lst_daily", + output="annual_spring_warming", + basename="spring_warming", + suffix="gran", + method="slope", + granularity="1 years", + where=f"strftime('%m',start_time)='{months[0]}' or strftime('%m',start_time)='{months[1]}' or strftime('%m', start_time)='{months[2]}'") +``` + +```{python} +# Check raster maps in the STRDS +gs.run_command("t.rast.list", input="annual_spring_warming") +``` + +```{python} +spring_warming = gj.TimeSeriesMap(use_region=True) +spring_warming.add_raster_series("annual_spring_warming", fill_gaps=False) +spring_warming.d_legend(color="black", at=(10,40,2,6)) +spring_warming.show() +``` + +:::{.callout-caution title="Question" collapse="true"} +How would you obtain the average spring warming over the whole time series? + +```{python} +# Average spring warming +gs.run_command("t.rast.series", + input="annual_spring_warming", + output="avg_spring_warming", + method="average") + +# Display raster map with interactive class +spw_map = gj.InteractiveMap(width = 500, use_region=True, tiles="CartoDark") +spw_map.add_raster("avg_spring_warming") +spw_map.add_layer_control(position = "bottomright") +spw_map.show() +``` +::: + + +## Full series aggregation + +The previous question (as you might have seen) can be solved using +[t.rast.series](https://grass.osgeo.org/grass-stable/manuals/t.rast.series.html). +This tool allows us to aggregate complete time series (or only selected parts) +with a certain method. Available aggregation methods include average, minimum, +maximum, median, etc. See the +[r.series](https://grass.osgeo.org/grass-stable/manuals/r.series.html) +manual page for details. + +### Maximum and minimum LST + +We'll demonstrate how to aggregate the whole `lst_daily` time series to obtain +the maximum and minimum LST value for the period 2014-2018. Note that the input +is a STRDS object while the output is a single map in which pixel values +correspond to the maximum or minimum value of the time series they represent. + +```{python} +# Get maximum and minimum LST in the STRDS +methods=["maximum","minimum"] + +for m in methods: + gs.run_command("t.rast.series", + input="lst_daily", + output=f"lst_{m}", + method=m, + nprocs=4) + +``` + +```{python} +# Change color palette to Celsius +gs.run_command("r.colors", + map="lst_minimum,lst_maximum", + color="celsius") +``` + +Let's use the `InteractiveMap` class from `grass.jupyter` to visualize the +maximum and minimum LST maps. + +```{python} +# Plot +lst_map=gj.InteractiveMap(width = 500) +lst_map.add_raster("lst_minimum") +lst_map.add_raster("lst_maximum") +lst_map.add_layer_control(position = "bottomright") +lst_map.show() +``` + +### Long term aggregations + +If we want to know how is the temperature on a "typical" January, February and so +on, we estimate the so called long-term averages or climatologies for each month. +These are usually computed over 20 or 30 years of data, but for the purpose of +demonstrating the usage of *t.rast.series* for long term aggregations, we will +use our 5-year time series here. + +We will use something we learnt in the listing and selection examples in the +[first time series tutorial](time_series_management_and_visualization.qmd), +i.e., we need to select all maps for each month to perform the aggregation. +So, the input will be the whole time series, but we +will use only some maps to estimate the long term monthly average, minimum and +maximum values. Let's see how to do all that with a simple for cycle looping +over months and methods: + +```{python} +# Estimate long term aggr for all months and methods +months=['{0:02d}'.format(m) for m in range(1,13)] +methods=["average","minimum","maximum"] + +for m in months: + for me in methods: + print(f"Aggregating all {m} maps with {me} method") + gs.run_command("t.rast.series", + input="lst_daily", + method=me, + where=f"strftime('%m', start_time)='{m}'", + output="lst_{}_{}".format(me,m)) +``` + +```{python} +# List newly created maps +map_list = gs.list_grouped(type="raster", + pattern="*{average,minimum,maximum}*")['italy_LST_daily'] +print(map_list) +``` + +Let's create an animation of the long term average LST. + +```{python} +# List of average maps +map_list = gs.list_grouped(type="raster", pattern="*average*")['italy_LST_daily'] + +# Animation with SeriesMap class +series = gj.SeriesMap(height = 500) +series.add_rasters(map_list) +series.d_barscale() +series.show() +``` + +::: {.callout-caution title="Question"} +Why didn't we list with *t.rast.list*? +::: + + +### Bioclimatic variables + +Perhaps you have heard of [Worldclim](https://www.worldclim.org/) or +[CHELSA](https://chelsa-climate.org/) bioclimatic variables? Well, these are 19 +variables that represent potentially limiting conditions for species. They +derive from the combination of temperature and precipitation long term +aggregations. +Let's use those that we estimated in the previous example to estimate the +bioclimatic variables that include temperature. GRASS has a nice extension, +[r.bioclim](https://grass.osgeo.org/grass-stable/manuals/addons/r.bioclim.html), +to estimate bioclimatic variables. + +```{python} +# Install extension +gs.run_command("g.extension", + extension="r.bioclim") +``` + +```{python} +# Get lists of maps needed +tmin=gs.list_grouped(type="raster", pattern="lst_minimum_??")["italy_LST_daily"] +tmax=gs.list_grouped(type="raster", pattern="lst_maximum_??")["italy_LST_daily"] +tavg=gs.list_grouped(type="raster", pattern="lst_average_??")["italy_LST_daily"] + +print(tmin,tmax,tavg) +``` + +```{python} +# Estimate temperature related bioclimatic variables +gs.run_command("r.bioclim", + tmin=tmin, + tmax=tmax, + tavg=tavg, + output="worldclim_") +``` + +```{python} +# List output maps +gs.list_grouped(type="raster", pattern="worldclim*")["italy_LST_daily"] +``` + +Let's have a look at some of the maps we just created. + +```{python} +# Display raster map with interactive class +bio_map = gj.InteractiveMap(width = 500, use_region=True) +bio_map.add_raster("worldclim_bio01") +bio_map.add_raster("worldclim_bio02") +bio_map.add_layer_control(position = "bottomright") +bio_map.show() +``` + +Let's assume a certain insect species can only survive where winter mean +temperatures are above 5 degrees. How would you determine suitable areas? +We'll see that in the [upcoming tutorial](time_series_accumulations.qmd), +stay tuned! + +::: {.callout-tip} +If you are curious, maybe you want to have a look at +[t.rast.mapcalc](https://grass.osgeo.org/grass-stable/manuals/t.rast.mapcalc.html) +and +[t.rast.algebra](https://grass.osgeo.org/grass-stable/manuals/t.rast.algebra.html) +and try to come up with a solution. +::: + +## References + + +- Gebbert, S., Pebesma, E. 2014. +_TGRASS: A temporal GIS for field based environmental modeling._ +Environmental Modelling & Software 53, 1-12. +[DOI](http://dx.doi.org/10.1016/j.envsoft.2013.11.001). +- Gebbert, S., Pebesma, E. 2017. _The GRASS GIS temporal framework._ +International Journal of Geographical Information Science 31, 1273-1292. +[DOI](http://dx.doi.org/10.1080/13658816.2017.1306862). +- [Temporal data processing](https://grasswiki.osgeo.org/wiki/Temporal_data_processing) wiki page. + + +*** + +:::{.smaller} +The development of this tutorial was funded by the US +[National Science Foundation (NSF)](https://www.nsf.gov/), +award [2303651](https://www.nsf.gov/awardsearch/showAward?AWD_ID=2303651). +::: diff --git a/content/tutorials/time_series/time_series_management_and_visualization.qmd b/content/tutorials/time_series/time_series_management_and_visualization.qmd index 891ef7d..0c1ebd8 100644 --- a/content/tutorials/time_series/time_series_management_and_visualization.qmd +++ b/content/tutorials/time_series/time_series_management_and_visualization.qmd @@ -11,8 +11,8 @@ format: code-tools: true code-copy: true code-fold: false -categories: [time series, raster, basic, Python] -description: Introductory tutorial to raster time series creation, management and visualization in GRASS. +categories: [time series, raster, intermediate, Python] +description: Introductory tutorial on raster time series creation, management and visualization in GRASS. engine: jupyter execute: eval: false From ccdad3e016d21f1661fd3a0ddb9cee5e007cf23e Mon Sep 17 00:00:00 2001 From: Veronica Andreo Date: Thu, 22 May 2025 11:09:38 -0300 Subject: [PATCH 2/6] fix link to upcoming tutorial --- content/tutorials/time_series/time_series_aggregations.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/tutorials/time_series/time_series_aggregations.qmd b/content/tutorials/time_series/time_series_aggregations.qmd index db2e450..601a53e 100644 --- a/content/tutorials/time_series/time_series_aggregations.qmd +++ b/content/tutorials/time_series/time_series_aggregations.qmd @@ -397,7 +397,7 @@ bio_map.show() Let's assume a certain insect species can only survive where winter mean temperatures are above 5 degrees. How would you determine suitable areas? -We'll see that in the [upcoming tutorial](time_series_accumulations.qmd), +We'll see that in the [upcoming tutorial](time_series_algebra.qmd), stay tuned! ::: {.callout-tip} From 224f3416a8c446eb6d1586e3e418c5f941c4f950 Mon Sep 17 00:00:00 2001 From: Veronica Andreo Date: Thu, 22 May 2025 12:04:31 -0300 Subject: [PATCH 3/6] include suggestions from https://github.com/ncsu-geoforall-lab/tutorials/pull/11 --- .../time_series/time_series_aggregations.qmd | 215 +++++++++--------- ...me_series_management_and_visualization.qmd | 8 +- 2 files changed, 106 insertions(+), 117 deletions(-) diff --git a/content/tutorials/time_series/time_series_aggregations.qmd b/content/tutorials/time_series/time_series_aggregations.qmd index 601a53e..5df1af1 100644 --- a/content/tutorials/time_series/time_series_aggregations.qmd +++ b/content/tutorials/time_series/time_series_aggregations.qmd @@ -18,7 +18,7 @@ execute: eval: false --- -In this second part of the time series tutorials, we will go through different +In this second time series tutorial, we will go through different ways of performing **aggregations**. Temporal aggregation is a very common task when working with time series as it allows us to summarize information and find patterns of change both in space and time. @@ -26,13 +26,13 @@ find patterns of change both in space and time. ::: {.callout-note title="Setup"} This tutorial can be run locally or in Google Colab. However, make sure you install GRASS GIS 8.4+, download the sample data and set up your project -as explained in [part 1](time_series_management_and_visualization.qmd) -of these time series tutorials. +as explained in the [first](time_series_management_and_visualization.qmd) +time series tutorial. ::: There are two main tools to do time series aggregations in GRASS GIS: [t.rast.aggregate](https://grass.osgeo.org/grass-stable/manuals/t.rast.aggregate.html) -and [t.rast.series](https://grass.osgeo.org/grass-stable/manuals/t.rast.series.html). +and [t.rast.series](https://grass.osgeo.org/grass-stable/manuals/t.rast.series.html). We'll demonstrate their usage in the upcoming sections. ```{python} @@ -42,62 +42,61 @@ import os import sys import subprocess -# GRASS GIS database variables -grassbin = "grass-dev" -grassdata = os.path.join(os.path.expanduser('~'), "grassdata") -project = "eu_laea" -mapset = "italy_LST_daily" - +# Ask GRASS where its Python packages are sys.path.append( subprocess.check_output([grassbin, "--config", "python_path"], text=True).strip() ) -``` -```{python} -#| echo: false - -# Import the GRASS GIS packages we need +# Import the GRASS packages we need import grass.script as gs import grass.jupyter as gj +path_to_project = "eu_laea/italy_LST_daily" + # Start the GRASS GIS Session -session = gj.init(grassdata, project, mapset); +session = gj.init(path_to_project); ``` -## Aggregation with granularity +## Aggregation with granularity -**Granularity** is the greatest common divisor of the temporal extents +**Granularity** is the greatest common divisor of the temporal extents (and possible gaps) of all maps in a space-time dataset. To perform time -series aggregation with granularity, we use +series aggregation with granularity, we use [t.rast.aggregate](https://grass.osgeo.org/grass-stable/manuals/t.rast.aggregate.html). This tool allows us to aggregate our time series into larger granularities, i.e., -from hourly to daily, from daily to weekly, monthly, etc. It also permits to +from hourly to daily, from daily to weekly, monthly, etc. It also permits to aggregate with *ad hoc* granularities like 3 minutes, 5 days, 3 months, etc. -Supported aggregate methods include average, minimum, maximum, median, etc. See -the [r.series](https://grass.osgeo.org/grass-stable/manuals/r.series.html) +Supported aggregate methods include average, minimum, maximum, median, etc. See +the [r.series](https://grass.osgeo.org/grass-stable/manuals/r.series.html) manual page for more details. -If you are working with data representing absolute time, *t.rast.aggregate* will -shift the start date for each aggregation process depending on the provided +If you are working with data representing absolute time, *t.rast.aggregate* will +shift the start date for each aggregation process depending on the provided temporal granularity as follows: -- years: will start at the first of January, hence 14-08-2012 00:01:30 will be shifted to 01-01-2012 00:00:00 -- months: will start at the first day of a month, hence 14-08-2012 will be shifted to 01-08-2012 00:00:00 -- weeks: will start at the first day of a week (Monday), hence 14-08-2012 01:30:30 will be shifted to 13-08-2012 01:00:00 -- days: will start at the first hour of a day, hence 14-08-2012 00:01:30 will be shifted to 14-08-2012 00:00:00 -- hours: will start at the first minute of a hour, hence 14-08-2012 01:30:30 will be shifted to 14-08-2012 01:00:00 -- minutes: will start at the first second of a minute, hence 14-08-2012 01:30:30 will be shifted to 14-08-2012 01:30:00 +- *years*: will start at the first of January, hence 14-08-2012 00:01:30 will be shifted to 01-01-2012 00:00:00 +- *months*: will start at the first day of a month, hence 14-08-2012 will be shifted to 01-08-2012 00:00:00 +- *weeks*: will start at the first day of a week (Monday), hence 14-08-2012 01:30:30 will be shifted to 13-08-2012 01:00:00 +- *days*: will start at the first hour of a day, hence 14-08-2012 00:01:30 will be shifted to 14-08-2012 00:00:00 +- *hours*: will start at the first minute of a hour, hence 14-08-2012 01:30:30 will be shifted to 14-08-2012 01:00:00 +- *minutes*: will start at the first second of a minute, hence 14-08-2012 01:30:30 will be shifted to 14-08-2012 01:30:00 + +The specification of the temporal relation between the aggregation intervals and +the raster map layers to be aggregated is always formulated from the aggregation +interval viewpoint. By default, it is set to the *contains* relation to aggregate +all maps that are temporally within an aggregation interval. -The specification of the temporal relation between the aggregation intervals and -the raster map layers to be aggregated is always formulated from the aggregation -interval viewpoint. By default, it is set to *contains* to aggregate all maps -that are temporally within an aggregation interval. +::: {.callout-note title="Temporal topology"} +**Temporal topology** refers to the temporal relations among time intervals or instances in different time series. They are mostly useful when sampling one STRDS with another (see [t.sample](https://grass.osgeo.org/grass-stable/manuals/t.sample.html) manual). Some examples of temporal relations are *start, equal, during, contain, overlap,* etc. See below a graphic representation of relations and sampling taken from Gebbert and Pebesma (2014). Note that A1 starts in S1, B3 is during S3, B4 equals S4, B1 overlaps S1. + +![](images/temporal_sampling_and_relations.png) +::: ### Monthly and seasonal averages To demonstrate the basic usage of *t.rast.aggregate*, let's create monthly and seasonal time series starting from the `lst_daily` time series we created -in the [time series management](time_series_management_and_visualization.qmd) +in the [time series management](time_series_management_and_visualization.qmd) tutorial. ```{python} @@ -117,17 +116,17 @@ gs.run_command("t.rast.aggregate", gs.run_command("t.rast.aggregate", input="lst_daily", method="average", - granularity="3 months", - basename="lst_avg", - output="lst_seasonal", + granularity="3 months", + basename="lst_avg", + output="lst_seasonal", suffix="gran", nprocs=4) ``` -If we would like to follow the so called +If we would like to follow the so called [*meteorological seasons*](https://en.wikipedia.org/wiki/Season#Meteorological), i.e., that spring started with March, then we could have used the *where* option -to shift the starting point of the aggregation period as follows: +to shift the starting point of the aggregation period as follows: `where="start_time >= '2013-03-01 00:00:00'"`. Let's compare the granularities using the timeline plot... @@ -141,9 +140,9 @@ Let's compare the granularities using the timeline plot... and use `grass.jupyter` to create a nice animation of the seasonal time series: ```{python} -lstseries = gj.TimeSeriesMap(use_region=True) +lstseries = gj.TimeSeriesMap() lstseries.add_raster_series("lst_seasonal", fill_gaps=False) -lstseries.d_legend(color="black", at=(10,40,2,6)) +lstseries.d_legend(color="black", at=(5, 50, 2, 6)) lstseries.show() ``` @@ -152,10 +151,10 @@ lstseries.show() lstseries.save("lstseries.gif") ``` -For astronomical seasons, i.e., those defined by solstices and equinoxes, we can use -[t.rast.aggregate.ds](https://grass.osgeo.org/grass-stable/manuals/t.rast.aggregate.ds.html) -as in -[this example](https://grasswiki.osgeo.org/wiki/Temporal_data_processing/seasonal_aggregation), +For astronomical seasons, i.e., those defined by solstices and equinoxes, we can use +[t.rast.aggregate.ds](https://grass.osgeo.org/grass-stable/manuals/t.rast.aggregate.ds.html) +as in +[this example](https://grasswiki.osgeo.org/wiki/Temporal_data_processing/seasonal_aggregation), or for an approximate result: ```{python} @@ -163,18 +162,16 @@ gs.run_command("t.rast.aggregate", input="lst_daily", method="average", where="start_time >= '2013-03-21 00:00:00'", - granularity="3 months", - basename="lst_avg", - output="lst_seasonal", + granularity="3 months", + basename="lst_avg", + output="lst_seasonal", suffix="gran", nprocs=4) ``` ```{python} # Check info -gs.read_command("t.info", - input="lst_seasonal", - flags="g") +gs.read_command("t.info", input="lst_seasonal") ``` ```{python} @@ -184,14 +181,14 @@ gs.run_command("t.rast.list", input="lst_seasonal") ### Spring warming -We define spring warming as the velocity with which temperature increases from -winter into spring and we can approximate it as the linear regression slope -among LST values of February, March and April. Let's see how to use +We define spring warming as the velocity with which temperature increases from +winter into spring and we can approximate it as the linear regression slope +among LST values of February, March and April. Let's see how to use *t.rast.aggregate* to estimate yearly spring warming values. ```{python} # Define list of months -months=['{0:02d}'.format(m) for m in range(2,5)] +months = [f"{m:02d}" for m in range(2, 5)] print(months) ``` @@ -213,9 +210,9 @@ gs.run_command("t.rast.list", input="annual_spring_warming") ``` ```{python} -spring_warming = gj.TimeSeriesMap(use_region=True) +spring_warming = gj.TimeSeriesMap() spring_warming.add_raster_series("annual_spring_warming", fill_gaps=False) -spring_warming.d_legend(color="black", at=(10,40,2,6)) +spring_warming.d_legend(color="black", at=(5, 50, 2, 6)) spring_warming.show() ``` @@ -240,9 +237,8 @@ spw_map.show() ## Full series aggregation -The previous question (as you might have seen) can be solved using -[t.rast.series](https://grass.osgeo.org/grass-stable/manuals/t.rast.series.html). -This tool allows us to aggregate complete time series (or only selected parts) +The tool [t.rast.series](https://grass.osgeo.org/grass-stable/manuals/t.rast.series.html) +allows us to aggregate complete time series (or only selected parts) with a certain method. Available aggregation methods include average, minimum, maximum, median, etc. See the [r.series](https://grass.osgeo.org/grass-stable/manuals/r.series.html) @@ -252,12 +248,12 @@ manual page for details. We'll demonstrate how to aggregate the whole `lst_daily` time series to obtain the maximum and minimum LST value for the period 2014-2018. Note that the input -is a STRDS object while the output is a single map in which pixel values +is a STRDS object while the output is a single map in which pixel values correspond to the maximum or minimum value of the time series they represent. ```{python} # Get maximum and minimum LST in the STRDS -methods=["maximum","minimum"] +methods = ["maximum", "minimum"] for m in methods: gs.run_command("t.rast.series", @@ -265,7 +261,6 @@ for m in methods: output=f"lst_{m}", method=m, nprocs=4) - ``` ```{python} @@ -280,59 +275,55 @@ maximum and minimum LST maps. ```{python} # Plot -lst_map=gj.InteractiveMap(width = 500) +lst_map=gj.InteractiveMap() lst_map.add_raster("lst_minimum") lst_map.add_raster("lst_maximum") -lst_map.add_layer_control(position = "bottomright") lst_map.show() ``` ### Long term aggregations -If we want to know how is the temperature on a "typical" January, February and so +If we want to know the temperature on a "typical" January, February and so on, we estimate the so called long-term averages or climatologies for each month. -These are usually computed over 20 or 30 years of data, but for the purpose of +These are usually computed over 20 or 30 years of data, but for the purpose of demonstrating the usage of *t.rast.series* for long term aggregations, we will use our 5-year time series here. -We will use something we learnt in the listing and selection examples in the +We will use what we learned in the listing and selection examples in the [first time series tutorial](time_series_management_and_visualization.qmd), i.e., we need to select all maps for each month to perform the aggregation. So, the input will be the whole time series, but we -will use only some maps to estimate the long term monthly average, minimum and -maximum values. Let's see how to do all that with a simple for cycle looping +will use only some maps to estimate the long term monthly average, minimum and +maximum values. Let's see how to do all that with a simple for loop over months and methods: ```{python} # Estimate long term aggr for all months and methods -months=['{0:02d}'.format(m) for m in range(1,13)] -methods=["average","minimum","maximum"] - -for m in months: - for me in methods: - print(f"Aggregating all {m} maps with {me} method") - gs.run_command("t.rast.series", - input="lst_daily", - method=me, - where=f"strftime('%m', start_time)='{m}'", - output="lst_{}_{}".format(me,m)) +months = [f"{m:02d}" for m in range(1, 13)] +methods = ["average", "minimum", "maximum"] + +for month in months: + print(f"Aggregating all {month} maps") + gs.run_command("t.rast.series", + input="lst_daily", + method=methods, + where=f"strftime('%m', start_time)='{month}'", + output=[f"lst_{method}_{month}" for method in methods]) ``` ```{python} # List newly created maps -map_list = gs.list_grouped(type="raster", - pattern="*{average,minimum,maximum}*")['italy_LST_daily'] -print(map_list) +map_list = gs.list_grouped(type="raster", pattern="*{average,minimum,maximum}*")['italy_LST_daily'] ``` Let's create an animation of the long term average LST. ```{python} # List of average maps -map_list = gs.list_grouped(type="raster", pattern="*average*")['italy_LST_daily'] +map_list = gs.list_grouped(type="raster", pattern="*_average_*")['italy_LST_daily'] # Animation with SeriesMap class -series = gj.SeriesMap(height = 500) +series = gj.SeriesMap() series.add_rasters(map_list) series.d_barscale() series.show() @@ -345,38 +336,37 @@ Why didn't we list with *t.rast.list*? ### Bioclimatic variables -Perhaps you have heard of [Worldclim](https://www.worldclim.org/) or -[CHELSA](https://chelsa-climate.org/) bioclimatic variables? Well, these are 19 -variables that represent potentially limiting conditions for species. They -derive from the combination of temperature and precipitation long term -aggregations. +Perhaps you have heard of [Worldclim](https://www.worldclim.org/) or +[CHELSA](https://chelsa-climate.org/) bioclimatic variables? Well, these are 19 +variables that represent potentially limiting conditions for species. They +derive from the combination of temperature and precipitation long term +aggregations. Let's use those that we estimated in the previous example to estimate the -bioclimatic variables that include temperature. GRASS has a nice extension, +bioclimatic variables that include temperature. GRASS has a nice extension, [r.bioclim](https://grass.osgeo.org/grass-stable/manuals/addons/r.bioclim.html), to estimate bioclimatic variables. ```{python} # Install extension -gs.run_command("g.extension", - extension="r.bioclim") +gs.run_command("g.extension", extension="r.bioclim") ``` ```{python} # Get lists of maps needed -tmin=gs.list_grouped(type="raster", pattern="lst_minimum_??")["italy_LST_daily"] -tmax=gs.list_grouped(type="raster", pattern="lst_maximum_??")["italy_LST_daily"] -tavg=gs.list_grouped(type="raster", pattern="lst_average_??")["italy_LST_daily"] +tmin = gs.list_grouped(type="raster", pattern="lst_minimum_??")["italy_LST_daily"] +tmax = gs.list_grouped(type="raster", pattern="lst_maximum_??")["italy_LST_daily"] +tavg = gs.list_grouped(type="raster", pattern="lst_average_??")["italy_LST_daily"] -print(tmin,tmax,tavg) +print(tmin, tmax, tavg) ``` ```{python} # Estimate temperature related bioclimatic variables -gs.run_command("r.bioclim", - tmin=tmin, +gs.run_command("r.bioclim", + tmin=tmin, tmax=tmax, - tavg=tavg, - output="worldclim_") + tavg=tavg, + output="worldclim_") ``` ```{python} @@ -384,26 +374,25 @@ gs.run_command("r.bioclim", gs.list_grouped(type="raster", pattern="worldclim*")["italy_LST_daily"] ``` -Let's have a look at some of the maps we just created. +Let's have a look at some of the maps we just created: ```{python} # Display raster map with interactive class -bio_map = gj.InteractiveMap(width = 500, use_region=True) +bio_map = gj.InteractiveMap() bio_map.add_raster("worldclim_bio01") bio_map.add_raster("worldclim_bio02") -bio_map.add_layer_control(position = "bottomright") bio_map.show() ``` -Let's assume a certain insect species can only survive where winter mean +Let's assume a certain insect species can only survive where winter mean temperatures are above 5 degrees. How would you determine suitable areas? We'll see that in the [upcoming tutorial](time_series_algebra.qmd), stay tuned! ::: {.callout-tip} -If you are curious, maybe you want to have a look at +If you are curious, maybe you want to have a look at [t.rast.mapcalc](https://grass.osgeo.org/grass-stable/manuals/t.rast.mapcalc.html) -and +and [t.rast.algebra](https://grass.osgeo.org/grass-stable/manuals/t.rast.algebra.html) and try to come up with a solution. ::: @@ -413,10 +402,10 @@ and try to come up with a solution. - Gebbert, S., Pebesma, E. 2014. _TGRASS: A temporal GIS for field based environmental modeling._ -Environmental Modelling & Software 53, 1-12. +Environmental Modelling & Software 53, 1-12. [DOI](http://dx.doi.org/10.1016/j.envsoft.2013.11.001). - Gebbert, S., Pebesma, E. 2017. _The GRASS GIS temporal framework._ -International Journal of Geographical Information Science 31, 1273-1292. +International Journal of Geographical Information Science 31, 1273-1292. [DOI](http://dx.doi.org/10.1080/13658816.2017.1306862). - [Temporal data processing](https://grasswiki.osgeo.org/wiki/Temporal_data_processing) wiki page. @@ -424,7 +413,7 @@ International Journal of Geographical Information Science 31, 1273-1292. *** :::{.smaller} -The development of this tutorial was funded by the US -[National Science Foundation (NSF)](https://www.nsf.gov/), +The development of this tutorial was funded by the US +[National Science Foundation (NSF)](https://www.nsf.gov/), award [2303651](https://www.nsf.gov/awardsearch/showAward?AWD_ID=2303651). ::: diff --git a/content/tutorials/time_series/time_series_management_and_visualization.qmd b/content/tutorials/time_series/time_series_management_and_visualization.qmd index 0c1ebd8..bdd47ce 100644 --- a/content/tutorials/time_series/time_series_management_and_visualization.qmd +++ b/content/tutorials/time_series/time_series_management_and_visualization.qmd @@ -62,10 +62,10 @@ scheme. In this way, we have: ::: {.callout-note title="Setup"} To run this tutorial locally or in Google Colab, you should install GRASS GIS 8.4+, and download the -[daily MODIS LST project](https://zenodo.org/records/3564515). -This project contains average daily MODIS LST data reconstructed by -[mundialis GmbH & Co. KG](https://www.mundialis.de/en/) based on -Metz et al (2017). +[daily MODIS LST project](https://zenodo.org/records/13750928). +This project contains average daily MODIS LST (Land Surface Temperature) +data reconstructed by [mundialis GmbH & Co. KG](https://www.mundialis.de/en/) +based on Metz et al (2017). Follow the [Fast track to GRASS](../get_started/fast_track.qmd) and [GRASS in Colab](../get_started/grass_gis_in_google_colab.qmd) tutorials to From 4d9a62bde881d3d4cb13ec2814fefaba4293177b Mon Sep 17 00:00:00 2001 From: Veronica Andreo Date: Thu, 22 May 2025 12:09:11 -0300 Subject: [PATCH 4/6] add zenodo link as suggested in https://github.com/ncsu-geoforall-lab/tutorials/issues/21 --- .../time_series/time_series_management_and_visualization.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/tutorials/time_series/time_series_management_and_visualization.qmd b/content/tutorials/time_series/time_series_management_and_visualization.qmd index bdd47ce..5e3d851 100644 --- a/content/tutorials/time_series/time_series_management_and_visualization.qmd +++ b/content/tutorials/time_series/time_series_management_and_visualization.qmd @@ -62,7 +62,7 @@ scheme. In this way, we have: ::: {.callout-note title="Setup"} To run this tutorial locally or in Google Colab, you should install GRASS GIS 8.4+, and download the -[daily MODIS LST project](https://zenodo.org/records/13750928). +[daily MODIS LST project](https://zenodo.org/doi/10.5281/zenodo.3564514). This project contains average daily MODIS LST (Land Surface Temperature) data reconstructed by [mundialis GmbH & Co. KG](https://www.mundialis.de/en/) based on Metz et al (2017). From 283a5bb6624ee3d866cbbf45ba677a2711eeec42 Mon Sep 17 00:00:00 2001 From: Veronica Andreo Date: Thu, 22 May 2025 14:21:19 -0300 Subject: [PATCH 5/6] fix project name --- .../time_series/time_series_management_and_visualization.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/tutorials/time_series/time_series_management_and_visualization.qmd b/content/tutorials/time_series/time_series_management_and_visualization.qmd index 5e3d851..cea4648 100644 --- a/content/tutorials/time_series/time_series_management_and_visualization.qmd +++ b/content/tutorials/time_series/time_series_management_and_visualization.qmd @@ -103,7 +103,7 @@ import grass.jupyter as gj Now we are ready to start a GRASS session in the downloaded project: ```{python} -path_to_project = "eu_laea/italy_LST_daily" +path_to_project = "italy_eu_laea/italy_LST_daily" # Start the GRASS Session session = gj.init(path_to_project) From 395eb605022b6e11555310d817157fef9bf5a303 Mon Sep 17 00:00:00 2001 From: Anna Petrasova Date: Thu, 22 May 2025 14:01:36 -0400 Subject: [PATCH 6/6] Apply suggestions from code review --- content/tutorials/time_series/time_series_aggregations.qmd | 4 ++-- .../time_series/time_series_management_and_visualization.qmd | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/content/tutorials/time_series/time_series_aggregations.qmd b/content/tutorials/time_series/time_series_aggregations.qmd index 5df1af1..a429561 100644 --- a/content/tutorials/time_series/time_series_aggregations.qmd +++ b/content/tutorials/time_series/time_series_aggregations.qmd @@ -30,7 +30,7 @@ as explained in the [first](time_series_management_and_visualization.qmd) time series tutorial. ::: -There are two main tools to do time series aggregations in GRASS GIS: +There are two main tools to do time series aggregations in GRASS: [t.rast.aggregate](https://grass.osgeo.org/grass-stable/manuals/t.rast.aggregate.html) and [t.rast.series](https://grass.osgeo.org/grass-stable/manuals/t.rast.series.html). We'll demonstrate their usage in the upcoming sections. @@ -53,7 +53,7 @@ import grass.jupyter as gj path_to_project = "eu_laea/italy_LST_daily" -# Start the GRASS GIS Session +# Start the GRASS session session = gj.init(path_to_project); ``` diff --git a/content/tutorials/time_series/time_series_management_and_visualization.qmd b/content/tutorials/time_series/time_series_management_and_visualization.qmd index cea4648..099d07e 100644 --- a/content/tutorials/time_series/time_series_management_and_visualization.qmd +++ b/content/tutorials/time_series/time_series_management_and_visualization.qmd @@ -61,7 +61,7 @@ scheme. In this way, we have: ::: {.callout-note title="Setup"} To run this tutorial locally or in Google Colab, you should install -GRASS GIS 8.4+, and download the +GRASS 8.4+, and download the [daily MODIS LST project](https://zenodo.org/doi/10.5281/zenodo.3564514). This project contains average daily MODIS LST (Land Surface Temperature) data reconstructed by [mundialis GmbH & Co. KG](https://www.mundialis.de/en/)