From 66ac22eff64fe00694f7bc4ab94738edd955c1d7 Mon Sep 17 00:00:00 2001 From: Luca Delucchi Date: Sat, 30 Dec 2023 08:10:40 +0100 Subject: [PATCH 1/5] added t.rast.climatologies for GRASS 8 --- src/temporal/t.rast.climatologies/Makefile | 7 + .../t.rast.climatologies.html | 32 +++ .../t.rast.climatologies.py | 244 ++++++++++++++++++ .../testsuite/test_climatologies_daily.py | 132 ++++++++++ .../testsuite/test_climatologies_monthly.py | 151 +++++++++++ 5 files changed, 566 insertions(+) create mode 100644 src/temporal/t.rast.climatologies/Makefile create mode 100644 src/temporal/t.rast.climatologies/t.rast.climatologies.html create mode 100644 src/temporal/t.rast.climatologies/t.rast.climatologies.py create mode 100644 src/temporal/t.rast.climatologies/testsuite/test_climatologies_daily.py create mode 100644 src/temporal/t.rast.climatologies/testsuite/test_climatologies_monthly.py 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..ecdfaa1890 --- /dev/null +++ b/src/temporal/t.rast.climatologies/t.rast.climatologies.html @@ -0,0 +1,32 @@ +

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 aggreated 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..c98460b909 --- /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(): + 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__": + import grass.pygrass.modules as pymod + import grass.script as gscript + import grass.temporal as tgis + + 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() From ab97ab794388ba0bdb0e6d636abb81b1602ab0da Mon Sep 17 00:00:00 2001 From: Luca Delucchi Date: Sat, 30 Dec 2023 17:33:24 +0100 Subject: [PATCH 2/5] Update src/temporal/t.rast.climatologies/t.rast.climatologies.html Co-authored-by: Markus Neteler --- src/temporal/t.rast.climatologies/t.rast.climatologies.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/temporal/t.rast.climatologies/t.rast.climatologies.html b/src/temporal/t.rast.climatologies/t.rast.climatologies.html index ecdfaa1890..33a9c24698 100644 --- a/src/temporal/t.rast.climatologies/t.rast.climatologies.html +++ b/src/temporal/t.rast.climatologies/t.rast.climatologies.html @@ -23,7 +23,7 @@

Monthly climatologies

SEE ALSO

-r.series +r.series, t.rast.series From fc33180b5ccea7aae5f641a3fcc8785ff45422f5 Mon Sep 17 00:00:00 2001 From: Luca Delucchi Date: Sat, 30 Dec 2023 17:34:13 +0100 Subject: [PATCH 3/5] Update src/temporal/t.rast.climatologies/t.rast.climatologies.html Co-authored-by: Markus Neteler --- .../t.rast.climatologies.html | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/temporal/t.rast.climatologies/t.rast.climatologies.html b/src/temporal/t.rast.climatologies/t.rast.climatologies.html index 33a9c24698..990adc6630 100644 --- a/src/temporal/t.rast.climatologies/t.rast.climatologies.html +++ b/src/temporal/t.rast.climatologies/t.rast.climatologies.html @@ -1,12 +1,17 @@

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 aggreated maps. + +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. + +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
@@ -14,7 +19,10 @@ 

Daily climatologies

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. + +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

From 2b855e92ee3396065d122b69750443a8e1b3d7be Mon Sep 17 00:00:00 2001
From: Luca Delucchi 
Date: Fri, 5 Jan 2024 18:25:02 +0100
Subject: [PATCH 4/5] improved lazy import

---
 src/temporal/t.rast.climatologies/t.rast.climatologies.py | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/src/temporal/t.rast.climatologies/t.rast.climatologies.py b/src/temporal/t.rast.climatologies/t.rast.climatologies.py
index c98460b909..b43c307f26 100644
--- a/src/temporal/t.rast.climatologies/t.rast.climatologies.py
+++ b/src/temporal/t.rast.climatologies/t.rast.climatologies.py
@@ -88,6 +88,9 @@
 
 
 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"]
@@ -237,8 +240,4 @@ def main():
 
 
 if __name__ == "__main__":
-    import grass.pygrass.modules as pymod
-    import grass.script as gscript
-    import grass.temporal as tgis
-
     main()

From 87e03adce41ff199e346670df2fe9560d8c38378 Mon Sep 17 00:00:00 2001
From: Luca Delucchi 
Date: Fri, 5 Jan 2024 18:33:14 +0100
Subject: [PATCH 5/5] fixed black error

---
 src/temporal/t.rast.climatologies/t.rast.climatologies.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/temporal/t.rast.climatologies/t.rast.climatologies.py b/src/temporal/t.rast.climatologies/t.rast.climatologies.py
index b43c307f26..34a21369a3 100644
--- a/src/temporal/t.rast.climatologies/t.rast.climatologies.py
+++ b/src/temporal/t.rast.climatologies/t.rast.climatologies.py
@@ -91,6 +91,7 @@ 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"]