Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 58 additions & 8 deletions python/grass/temporal/univar_statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,19 @@
import grass.temporal as tgis

tgis.print_gridded_dataset_univar_statistics(
type, input, output, where, extended, no_header, fs, rast_region
"strds",
input,
output,
where,
extended,
percentile=percentile,
no_header=no_header,
fs=separator,
zones=zones,
rast_region=rast_region,
region_relation=region_relation,
nprocs=nprocs,
granularity=options["granularity"] or None,
)

..
Expand All @@ -21,6 +33,7 @@
:authors: Soeren Gebbert
"""

from .datetime_math import adjust_datetime_to_granularity, increment_datetime_by_string
from multiprocessing import Pool
from subprocess import PIPE

Expand Down Expand Up @@ -125,6 +138,7 @@ def print_gridded_dataset_univar_statistics(
zones=None,
percentile=None,
nprocs: int = 1,
granularity: str | None = None,
) -> None:
"""Print univariate statistics for a space time raster or raster3d dataset.

Expand Down Expand Up @@ -174,6 +188,26 @@ def print_gridded_dataset_univar_statistics(
spatial_extent=spatial_extent,
spatial_relation=region_relation,
)
granule_groups = None
if granularity and rows:
win_start = adjust_datetime_to_granularity(rows[0]["start_time"], granularity)
# last map's end_time can be None for point-in-time maps — fall back to start
last = rows[-1]
win_end_global = last["end_time"] or last["start_time"]

# walk through time and collect which maps fall inside each window
granule_groups = []
t = win_start
while t < win_end_global:
t_end = increment_datetime_by_string(t, granularity)
group = [
row
for row in rows
if row["start_time"] >= t and row["start_time"] < t_end
]
if group:
granule_groups.append((t, t_end, group))
t = t_end

if not rows and rows != [""]:
dbif.close()
Expand Down Expand Up @@ -250,14 +284,30 @@ def print_gridded_dataset_univar_statistics(
)

nprocs = max(nprocs, 1)
if nprocs == 1:
if granule_groups is not None:
# granularity mode: each group of maps gets merged into one r.univar call.
# r.univar handles multiple maps natively — no temp files, no aggregate step.
# Output row uses the granule window as start/end, not individual map ids.
strings = []
for win_start, win_end, group in granule_groups:
map_ids = ",".join(row["id"] for row in group)

# fake a row dict that compute_univar_stats can work with —
# id becomes the map list, timestamps become the granule bounds
synthetic_row = {
"id": map_ids,
"start_time": win_start,
"end_time": win_end,
# granule rows carry no single semantic label; leave it blank
"semantic_label": None,
}
result = compute_univar_stats(synthetic_row, univar_module, fs, rast_region)
if result:
strings.append(result)

elif nprocs == 1:
strings = [
compute_univar_stats(
row,
univar_module,
fs,
)
for row in rows
compute_univar_stats(row, univar_module, fs, rast_region) for row in rows
]
else:
with Pool(min(nprocs, len(rows))) as pool:
Expand Down
12 changes: 10 additions & 2 deletions temporal/t.rast.univar/t.rast.univar.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# MODULE: t.rast.univar
# AUTHOR(S): Soeren Gebbert
#
# PURPOSE: Calculates univariate statistics from the non-null cells for each registered raster map of a space time raster dataset
# PURPOSE: Calculates statistics univariate from the non-null cells for each registered raster map of a space time raster dataset
# COPYRIGHT: (C) 2011-2017, Soeren Gebbert and the GRASS Development Team
#
# This program is free software; you can redistribute it and/or modify
Expand Down Expand Up @@ -59,6 +59,14 @@
# % guisection: Selection
# %end

# %option
# % key: granularity
# % type: string
# % description: Aggregation granularity, format absolute time "x years, x months, x weeks, x days, x hours, x minutes, x seconds" or an integer value for relative time
# % required: no
# % multiple: no
# %end

# %option
# % key: region_relation
# % description: Process only maps with this spatial relation to the current computational region
Expand Down Expand Up @@ -101,7 +109,6 @@
def main():
# Get the options and flags
options, flags = gs.parser()

# lazy imports
import grass.temporal as tgis

Expand Down Expand Up @@ -150,6 +157,7 @@ def main():
rast_region=rast_region,
region_relation=region_relation,
nprocs=nprocs,
granularity=options["granularity"] or None,
)


Expand Down
90 changes: 90 additions & 0 deletions temporal/t.rast.univar/testsuite/test_t_rast_univar.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,96 @@ def test_with_spatial_filter_is_contained(self):
self.assertLooksLike(ref_line, res_line)


class TestRasterUnivarGranularity(TestCase):
"""Tests for the granularity aggregation option added in #3042.
Four maps registered 3 months apart (Jan, Apr, Jul, Oct 2001),
values 100/200/300/400 - chosen so the expected stats are dead simple
to verify by hand."""

@classmethod
def setUpClass(cls):
cls.use_temp_region()
cls.runModule("g.region", s=0, n=80, w=0, e=120, res=1)
for i, val in enumerate([100, 200, 300, 400], start=1):
cls.runModule(
"r.mapcalc",
expression=f"gran_test_{i} = {val}",
overwrite=True,
)
cls.runModule(
"t.create",
type="strds",
temporaltype="absolute",
output="gran_A",
title="granularity test",
description="granularity test",
overwrite=True,
)
cls.runModule(
"t.register",
flags="i",
type="raster",
input="gran_A",
maps="gran_test_1,gran_test_2,gran_test_3,gran_test_4",
start="2001-01-01",
increment="3 months",
overwrite=True,
)

@classmethod
def tearDownClass(cls):
cls.runModule("t.remove", flags="df", type="strds", inputs="gran_A")
cls.del_temp_region()

def test_granularity_6months(self):
"""6-month windows should collapse 4 maps into 2 rows"""
t_rast_univar = SimpleModule(
"t.rast.univar",
input="gran_A",
granularity="6 months",
verbose=True,
)
self.assertModule(t_rast_univar)
lines = [l for l in t_rast_univar.outputs.stdout.strip().split("\n") if l]
# header + 2 data rows
self.assertEqual(len(lines), 3)
# first granule: Jan-Jul, mean of 100+200 = 150
self.assertIn("2001-01-01 00:00:00", lines[1])
self.assertIn("2001-07-01 00:00:00", lines[1])
self.assertIn("150", lines[1])
# second granule: Jul-Jan, mean of 300+400 = 350
self.assertIn("2001-07-01 00:00:00", lines[2])
self.assertIn("2002-01-01 00:00:00", lines[2])
self.assertIn("350", lines[2])

def test_granularity_1year(self):
"""1-year window should collapse all 4 maps into a single row"""
t_rast_univar = SimpleModule(
"t.rast.univar",
input="gran_A",
granularity="1 year",
verbose=True,
)
self.assertModule(t_rast_univar)
lines = [l for l in t_rast_univar.outputs.stdout.strip().split("\n") if l]
# header + 1 data row
self.assertEqual(len(lines), 2)
self.assertIn("2001-01-01 00:00:00", lines[1])
self.assertIn("2002-01-01 00:00:00", lines[1])
self.assertIn("250", lines[1])

def test_no_granularity_unchanged(self):
"""Without granularity the output should still be one row per map"""
t_rast_univar = SimpleModule(
"t.rast.univar",
input="gran_A",
verbose=True,
)
self.assertModule(t_rast_univar)
lines = [l for l in t_rast_univar.outputs.stdout.strip().split("\n") if l]
self.assertEqual(len(lines), 5) # header + 4 maps


if __name__ == "__main__":
from grass.gunittest.main import test

Expand Down
Loading