diff --git a/src/temporal/t.rast.climatologies/Makefile b/src/temporal/t.rast.climatologies/Makefile new file mode 100644 index 0000000000..1f9cb18c1f --- /dev/null +++ b/src/temporal/t.rast.climatologies/Makefile @@ -0,0 +1,7 @@ +MODULE_TOPDIR = ../.. + +PGM = t.rast.climatologies + +include $(MODULE_TOPDIR)/include/Make/Script.make + +default: script diff --git a/src/temporal/t.rast.climatologies/t.rast.climatologies.html b/src/temporal/t.rast.climatologies/t.rast.climatologies.html new file mode 100644 index 0000000000..990adc6630 --- /dev/null +++ b/src/temporal/t.rast.climatologies/t.rast.climatologies.html @@ -0,0 +1,40 @@ +

DESCRIPTION

+ +t.rast.climatologies is a module to calculate climatologies, i.e., long term stats, for single days +or months in a time series. If the s flag is used, the module outputs a new space time +raster dataset of relative temporal type with the aggregated maps. + +

EXAMPLE

+ +

Daily climatologies

+ +Starting from a space time raster dataset of daily granularity (or granularity lower than one day), +the module will create a new space time raster dataset of relative temporal type containing the +long term average for each day along years. + +
+
+        t.rast.climatologies input=myinput output=dailyoutput
+    
+
+ +

Monthly climatologies

+ +Starting from a space time raster dataset of monthly granularity (or lower than one month), +the module will create two new space time raster datasets containing the long term minimum +and maximum for each month along years. +
+
+        t.rast.climatologies input=myinput granularity=month method=min,max output=monthlyoutputmin,monthlyoutputmax
+    
+
+ +

SEE ALSO

+ +r.series, +t.rast.series + + +

AUTHOR

+ +Luca Delucchi, Fondazione Edmund Mach diff --git a/src/temporal/t.rast.climatologies/t.rast.climatologies.py b/src/temporal/t.rast.climatologies/t.rast.climatologies.py new file mode 100644 index 0000000000..34a21369a3 --- /dev/null +++ b/src/temporal/t.rast.climatologies/t.rast.climatologies.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python + +################################################ +# +# MODULE: t.rast.climatologies +# AUTHOR(S): Luca Delucchi, Fondazione Edmund Mach +# PURPOSE: t.rast.climatologies calculates climatologies for space time raster datasets +# +# COPYRIGHT: (C) 2023 by Luca Delucchi +# +# This program is free software under the GNU General Public +# License (>=v2). Read the file COPYING that comes with GRASS +# for details. +# +################################################ + +# %module +# % description: Calculates climatologies from a space time raster dataset of absolute temporal type +# % keyword: temporal +# % keyword: raster +# % keyword: aggregation +# % keyword: series +# % keyword: time +# %end + +# %option G_OPT_STRDS_INPUT +# %end + +# %option G_OPT_STRDS_OUTPUT +# % required: no +# %end + +# %option +# % key: basename +# % type: string +# % label: Basename of the newly generated output maps +# % description: Either a numerical suffix or the start time (s-flag) separated by an underscore will be attached to create a unique identifier +# % required: yes +# % multiple: no +# % gisprompt: +# %end + +# %option +# % key: method +# % type: string +# % description: Aggregate operation to be performed on the raster maps +# % required: yes +# % multiple: yes +# % options: average,count,median,mode,minimum,min_raster,maximum,max_raster,stddev,range,sum,variance,diversity,slope,offset,detcoeff,quart1,quart3,perc90,quantile,skewness,kurtosis +# % answer: average +# %end + +# %option +# % key: quantile +# % type: double +# % description: Quantile to calculate for method=quantile +# % required: no +# % multiple: yes +# % options: 0.0-1.0 +# %end + +# %option +# % key: granularity +# % type: string +# % label: Aggregate by day or month +# % required: yes +# % multiple: no +# % options: day, month +# % answer: day +# %end + +# %option +# % key: nprocs +# % type: integer +# % description: Number of processes to run in parallel +# % required: no +# % multiple: no +# % answer: 1 +# %end + +# %flag +# % key: s +# % description: Do not create a space time raster dataset as output +# %end + +import copy +from datetime import datetime + + +def main(): + import grass.pygrass.modules as pymod + import grass.script as gscript + import grass.temporal as tgis + + options, flags = gscript.parser() + strds = options["input"] + output = options["output"] + method = options["method"] + gran = options["granularity"] + basename = options["basename"] + nprocs = options["nprocs"] + quantile = options["quantile"] + space_time = flags["s"] + + # check if quantile value is used correctly + if "quantile" in method and not quantile: + gscript.fatal(_("Number requested methods and output maps do not match.")) + elif quantile and "quantile" not in method: + gscript.warning( + _("Quantile option set but quantile not selected in method option") + ) + + # Check if number of methods and output maps matches + if "quantile" in method: + len_method = len(method.split(",")) - 1 + else: + len_method = len(method.split(",")) + + if not space_time: + if (len(list(filter(None, quantile.split(",")))) + len_method) != len( + output.split(",") + ): + gscript.fatal(_("Number requested methods and output maps do not match.")) + + tgis.init() + # We need a database interface + dbif = tgis.SQLDatabaseInterfaceConnection() + dbif.connect() + + insp = tgis.open_old_stds(strds, "strds", dbif) + temporal_type, semantic_type, title, description = insp.get_initial_values() + if temporal_type != "absolute": + gscript.fatal( + _( + "Space time raster dataset temporal type is not absolute, this module requires absolute time" + ) + ) + maps = insp.get_registered_maps_as_objects(None, "start_time", dbif) + if maps is None: + gscript.fatal( + _( + "No maps selected in the space time raster dataset {};" + " it might be empty or the where option returns no data".format(strds) + ) + ) + # start the r.series module to be used in a ParallelModuleQueue + mod = pymod.Module("r.series") + mod.inputs.method = method + mod.flags.quiet = True + if quantile: + mod.inputs.quantile = quantile + process_queue = pymod.ParallelModuleQueue(int(nprocs)) + mapset = tgis.core.get_current_mapset() + # depending on granularity it calculates daily or monthly climatologies + outmaps = [] + if gran == "day": + outunit = "days" + # for each day + for doy in range(1, 367): + doystr = datetime.strptime(f"2000 {doy}", "%Y %j").strftime("%m_%d") + thiswhere = f"strftime('%m_%d', start_time) == '{doystr}'" + selemaps = insp.get_registered_maps_as_objects( + thiswhere, "start_time", dbif + ) + maps_name = [sam.get_id() for sam in selemaps] + # check if there are maps for that day + if len(maps_name) > 0: + outname = f"{basename}_{doystr}" + runmod = copy.deepcopy(mod) + runmod.inputs.input = ",".join(maps_name) + runmod.outputs.output = outname + process_queue.put(runmod) + map_layer = tgis.space_time_datasets.RasterDataset( + f"{outname}@{mapset}" + ) + extent = tgis.RelativeTemporalExtent( + start_time=doy - 1, + end_time=doy, + unit=outunit, + ) + map_layer.set_temporal_extent(extent=extent) + outmaps.append(map_layer) + + if doy % 10 == 0: + gscript.percent(doy, 366, 1) + else: + outunit = "months" + for month in range(1, 13): + monthstr = "{:02d}".format(month) + thiswhere = f"strftime('%m', start_time) == '{monthstr}'" + selemaps = insp.get_registered_maps_as_objects( + thiswhere, "start_time", None + ) + maps_name = [sam.get_id() for sam in selemaps] + if len(maps_name) > 0: + outname = f"{basename}_{monthstr}" + runmod = copy.deepcopy(mod) + runmod.inputs.input = ",".join(maps_name) + runmod.outputs.output = outname + process_queue.put(runmod) + map_layer = tgis.space_time_datasets.RasterDataset( + f"{outname}@{mapset}" + ) + extent = tgis.RelativeTemporalExtent( + start_time=month - 1, + end_time=month, + unit=outunit, + ) + map_layer.set_temporal_extent(extent=extent) + outmaps.append(map_layer) + gscript.percent(month, 12, 1) + + if not space_time: + # create new space time raster dataset + output_strds = tgis.open_new_stds( + output, + "strds", + "relative", + f"{gran} {method} climatologies", + f"Climatologies created from {strds}, {gran} {method} maps", + semantic_type, + dbif, + gscript.overwrite(), + ) + register_null = False + # register maps into space time raster dataset + tgis.register_map_object_list( + "rast", + outmaps, + output_strds, + register_null, + outunit, + dbif, + ) + + # Update the raster metadata table entries + output_strds.metadata.update(dbif) + + dbif.close() + return True + + +if __name__ == "__main__": + main() diff --git a/src/temporal/t.rast.climatologies/testsuite/test_climatologies_daily.py b/src/temporal/t.rast.climatologies/testsuite/test_climatologies_daily.py new file mode 100644 index 0000000000..6c56c6d0be --- /dev/null +++ b/src/temporal/t.rast.climatologies/testsuite/test_climatologies_daily.py @@ -0,0 +1,132 @@ +"""Test t.rast.climatologies +(C) 2023 by the GRASS Development Team +This program is free software under the GNU General Public +License (>=v2). Read the file COPYING that comes with GRASS +for details. +@author Luca Delucchi +""" + +import grass.temporal as tgis +from grass.gunittest.case import TestCase +from grass.gunittest.main import test + + +class TestClimatologies(TestCase): + @classmethod + def setUpClass(cls): + """Initiate the temporal GIS and set the region""" + tgis.init(True) # Raise on error instead of exit(1) + cls.use_temp_region() + cls.runModule("g.region", s=0, n=80, w=0, e=120, b=0, t=50, res=10, res3=10) + + cls.runModule("r.mapcalc", expression="a_1 = 5", overwrite=True) + cls.runModule("r.mapcalc", expression="a_2 = 10", overwrite=True) + cls.runModule("r.mapcalc", expression="a_3 = 15", overwrite=True) + cls.runModule("r.mapcalc", expression="a_4 = 20", overwrite=True) + cls.runModule("r.mapcalc", expression="a_5 = 25", overwrite=True) + cls.runModule("r.mapcalc", expression="a_6 = 30", overwrite=True) + cls.runModule("r.mapcalc", expression="a_7 = 35", overwrite=True) + cls.runModule("r.mapcalc", expression="a_8 = 40", overwrite=True) + cls.runModule("r.mapcalc", expression="a_9 = 45", overwrite=True) + cls.runModule("r.mapcalc", expression="b_1 = 5", overwrite=True) + cls.runModule("r.mapcalc", expression="b_2 = 10", overwrite=True) + cls.runModule("r.mapcalc", expression="b_3 = 15", overwrite=True) + cls.runModule("r.mapcalc", expression="b_4 = 20", overwrite=True) + cls.runModule("r.mapcalc", expression="b_5 = 25", overwrite=True) + cls.runModule("r.mapcalc", expression="b_6 = 30", overwrite=True) + cls.runModule("r.mapcalc", expression="b_7 = 35", overwrite=True) + cls.runModule("r.mapcalc", expression="b_8 = 40", overwrite=True) + cls.runModule("r.mapcalc", expression="b_9 = 45", overwrite=True) + cls.runModule("r.mapcalc", expression="c_1 = 5", overwrite=True) + cls.runModule("r.mapcalc", expression="c_2 = 10", overwrite=True) + cls.runModule("r.mapcalc", expression="c_3 = 15", overwrite=True) + cls.runModule("r.mapcalc", expression="c_4 = 20", overwrite=True) + cls.runModule("r.mapcalc", expression="c_5 = 25", overwrite=True) + cls.runModule("r.mapcalc", expression="c_6 = 30", overwrite=True) + cls.runModule("r.mapcalc", expression="c_7 = 35", overwrite=True) + cls.runModule("r.mapcalc", expression="c_8 = 40", overwrite=True) + cls.runModule("r.mapcalc", expression="c_9 = 45", overwrite=True) + + cls.runModule( + "t.create", + type="strds", + temporaltype="absolute", + output="daily", + title="Daily test", + description="Daily test", + overwrite=True, + ) + cls.runModule( + "t.register", + flags="i", + type="raster", + input="daily", + maps="a_1,a_2,a_3,a_4,a_5,a_6,a_7,a_8,a_9", + start="2001-01-01", + increment="1 day", + overwrite=True, + ) + cls.runModule( + "t.register", + flags="i", + type="raster", + input="daily", + maps="b_1,b_2,b_3,b_4,b_5,b_6,b_7,b_8,b_9", + start="2002-01-01", + increment="1 day", + overwrite=True, + ) + cls.runModule( + "t.register", + flags="i", + type="raster", + input="daily", + maps="c_1,c_2,c_3,c_4,c_5,c_6,c_7,c_8,c_9", + start="2003-01-01", + increment="1 day", + overwrite=True, + ) + + @classmethod + def tearDownClass(cls): + """Remove the time series""" + cls.runModule("t.remove", flags="fd", type="strds", inputs="daily") + + def test_1(self): + """Test average data""" + self.assertModule( + "t.rast.climatologies", + input="daily", + output="daily_clima", + method="average", + basename="daily_avg", + granularity="day", + overwrite=True, + verbose=True, + ) + out = tgis.open_old_stds("daily_clima", type="strds") + self.assertEqual(out.metadata.get_number_of_maps(), 9) + self.assertEqual(out.metadata.get_min_min(), 5) + self.assertEqual(out.metadata.get_max_max(), 45) + self.runModule("t.remove", flags="df", type="strds", inputs="daily_clima") + + def test_noout(self): + """Don't create output strds""" + self.assertModule( + "t.rast.climatologies", + input="daily", + flags=["s"], + method="average", + basename="daily_avg", + granularity="day", + overwrite=True, + verbose=True, + ) + self.assertRasterExists("daily_avg_01_01") + self.assertRasterExists("daily_avg_01_09") + self.assertRasterDoesNotExist("daily_avg_01_10") + self.runModule("g.remove", flags="f", type="raster", pattern="daily_avg_*") + + +if __name__ == "__main__": + test() diff --git a/src/temporal/t.rast.climatologies/testsuite/test_climatologies_monthly.py b/src/temporal/t.rast.climatologies/testsuite/test_climatologies_monthly.py new file mode 100644 index 0000000000..848994022f --- /dev/null +++ b/src/temporal/t.rast.climatologies/testsuite/test_climatologies_monthly.py @@ -0,0 +1,151 @@ +"""Test t.rast.climatologies +(C) 2023 by the GRASS Development Team +This program is free software under the GNU General Public +License (>=v2). Read the file COPYING that comes with GRASS +for details. +@author Luca Delucchi +""" + +import grass.temporal as tgis +from grass.gunittest.case import TestCase +from grass.gunittest.main import test + + +class TestClimatologies(TestCase): + @classmethod + def setUpClass(cls): + """Initiate the temporal GIS and set the region""" + tgis.init(True) # Raise on error instead of exit(1) + cls.use_temp_region() + cls.runModule("g.region", s=0, n=80, w=0, e=120, b=0, t=50, res=10, res3=10) + + cls.runModule("r.mapcalc", expression="a_1 = 5", overwrite=True) + cls.runModule("r.mapcalc", expression="a_2 = 10", overwrite=True) + cls.runModule("r.mapcalc", expression="a_3 = 15", overwrite=True) + cls.runModule("r.mapcalc", expression="a_4 = 20", overwrite=True) + cls.runModule("r.mapcalc", expression="a_5 = 25", overwrite=True) + cls.runModule("r.mapcalc", expression="a_6 = 30", overwrite=True) + cls.runModule("r.mapcalc", expression="a_7 = 35", overwrite=True) + cls.runModule("r.mapcalc", expression="a_8 = 40", overwrite=True) + cls.runModule("r.mapcalc", expression="a_9 = 45", overwrite=True) + cls.runModule("r.mapcalc", expression="b_1 = 5", overwrite=True) + cls.runModule("r.mapcalc", expression="b_2 = 10", overwrite=True) + cls.runModule("r.mapcalc", expression="b_3 = 15", overwrite=True) + cls.runModule("r.mapcalc", expression="b_4 = 20", overwrite=True) + cls.runModule("r.mapcalc", expression="b_5 = 25", overwrite=True) + cls.runModule("r.mapcalc", expression="b_6 = 30", overwrite=True) + cls.runModule("r.mapcalc", expression="b_7 = 35", overwrite=True) + cls.runModule("r.mapcalc", expression="b_8 = 40", overwrite=True) + cls.runModule("r.mapcalc", expression="b_9 = 45", overwrite=True) + cls.runModule("r.mapcalc", expression="c_1 = 5", overwrite=True) + cls.runModule("r.mapcalc", expression="c_2 = 10", overwrite=True) + cls.runModule("r.mapcalc", expression="c_3 = 15", overwrite=True) + cls.runModule("r.mapcalc", expression="c_4 = 20", overwrite=True) + cls.runModule("r.mapcalc", expression="c_5 = 25", overwrite=True) + cls.runModule("r.mapcalc", expression="c_6 = 30", overwrite=True) + cls.runModule("r.mapcalc", expression="c_7 = 35", overwrite=True) + cls.runModule("r.mapcalc", expression="c_8 = 40", overwrite=True) + cls.runModule("r.mapcalc", expression="c_9 = 45", overwrite=True) + cls.runModule("r.mapcalc", expression="d_1 = 5", overwrite=True) + cls.runModule("r.mapcalc", expression="d_2 = 10", overwrite=True) + cls.runModule("r.mapcalc", expression="d_3 = 15", overwrite=True) + cls.runModule("r.mapcalc", expression="d_4 = 20", overwrite=True) + cls.runModule("r.mapcalc", expression="d_5 = 25", overwrite=True) + cls.runModule("r.mapcalc", expression="d_6 = 30", overwrite=True) + cls.runModule("r.mapcalc", expression="d_7 = 35", overwrite=True) + cls.runModule("r.mapcalc", expression="d_8 = 40", overwrite=True) + cls.runModule("r.mapcalc", expression="d_9 = 45", overwrite=True) + + cls.runModule( + "t.create", + type="strds", + temporaltype="absolute", + output="daily", + title="Daily test", + description="Daily test", + overwrite=True, + ) + cls.runModule( + "t.register", + flags="i", + type="raster", + input="daily", + maps="a_1,a_2,a_3,a_4,a_5,a_6,a_7,a_8,a_9", + start="2001-01-01", + increment="1 day", + overwrite=True, + ) + cls.runModule( + "t.register", + flags="i", + type="raster", + input="daily", + maps="b_1,b_2,b_3,b_4,b_5,b_6,b_7,b_8,b_9", + start="2002-01-01", + increment="1 day", + overwrite=True, + ) + cls.runModule( + "t.register", + flags="i", + type="raster", + input="daily", + maps="c_1,c_2,c_3,c_4,c_5,c_6,c_7,c_8,c_9", + start="2002-02-01", + increment="1 day", + overwrite=True, + ) + cls.runModule( + "t.register", + flags="i", + type="raster", + input="daily", + maps="d_1,d_2,d_3,d_4,d_5,d_6,d_7,d_8,d_9", + start="2002-02-01", + increment="1 day", + overwrite=True, + ) + + @classmethod + def tearDownClass(cls): + """Remove the time series""" + cls.runModule("t.remove", flags="df", type="strds", inputs="daily") + + def test_1(self): + """Test average data""" + self.assertModule( + "t.rast.climatologies", + input="daily", + output="monthly_clima", + method="average", + basename="monthly_avg", + granularity="month", + overwrite=True, + verbose=True, + ) + out = tgis.open_old_stds("monthly_clima", type="strds") + self.assertEqual(out.metadata.get_number_of_maps(), 2) + self.assertEqual(out.metadata.get_min_min(), 25.0) + self.assertEqual(out.metadata.get_max_max(), 25.0) + self.runModule("t.remove", flags="df", type="strds", inputs="monthly_clima") + + def test_noout(self): + """Don't create output strds""" + self.assertModule( + "t.rast.climatologies", + input="daily", + flags=["s"], + method="average", + basename="monthly_avg", + granularity="month", + overwrite=True, + verbose=True, + ) + self.assertRasterExists("monthly_avg_01") + self.assertRasterExists("monthly_avg_02") + self.assertRasterDoesNotExist("monthly_avg_03") + self.runModule("g.remove", flags="f", type="raster", pattern="monthly_avg_*") + + +if __name__ == "__main__": + test()