Skip to content
Merged
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
1 change: 0 additions & 1 deletion .github/workflows/pytest_args_gunittest.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
--ignore-glob=**/tests/**
--ignore=gui/
--ignore=python/grass/temporal/unit_tests.py
lib/gis/testsuite/test_gis_lib_getl.py
lib/init/testsuite/test_grass_tmp_mapset.py
lib/vector/Vlib/testsuite/test_vlib_box.py
Expand Down
70 changes: 70 additions & 0 deletions lib/vector/rtree/tests/lib_vector_rtree_ctypes_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Tests for the R-tree search through the ctypes bindings

The R-tree supports one to four dimensions. Each case below inserts the ten
rectangles [i, i + 1] along every dimension and searches with [2, 7] along
every dimension, so the same ten rectangles and the same expected result apply
to all dimensions. Rectangles that only touch the search rectangle at a
boundary are part of the result, which is why [1, 2] and [7, 8] are included.

The R-tree is a pure in-memory structure, so no GRASS session is needed here.
"""

from ctypes import byref

import pytest

from grass.lib import gis, rtree, vector

# The identifiers the search is expected to return. Identifiers are one-based,
# so the rectangle [i, i + 1] has the identifier i + 1.
EXPECTED_IDS = [2, 3, 4, 5, 6, 7, 8]

# The function setting the bounds of a rectangle, one per dimensionality.
SET_RECT = {
1: rtree.RTreeSetRect1D,
2: rtree.RTreeSetRect2D,
3: rtree.RTreeSetRect3D,
4: rtree.RTreeSetRect4D,
}


def set_bounds(rect, tree, minimum, maximum) -> None:
"""Set the same bounds along every dimension of the tree"""
dimensions = tree.contents.ndims
SET_RECT[dimensions](rect, tree, *([minimum, maximum] * dimensions))


def search(tree, minimum, maximum):
"""Return the identifiers of the rectangles overlapping the search bounds"""
rect = rtree.RTreeAllocRect(tree)
set_bounds(rect, tree, minimum, maximum)

found = gis.ilist()
number = vector.RTreeSearch2(tree, rect, byref(found))
rtree.RTreeFreeRect(rect)

identifiers = sorted(found.value[i] for i in range(found.n_values))
# The return value counts the rectangles the search put into the list.
assert number == len(identifiers)
return identifiers


@pytest.fixture(params=[1, 2, 3, 4], ids=["1d", "2d", "3d", "4d"])
def tree(request):
"""An R-tree of the given dimensionality holding the ten rectangles"""
tree = rtree.RTreeCreateTree(-1, 0, request.param)
for i in range(10):
rect = rtree.RTreeAllocRect(tree)
set_bounds(rect, tree, float(i), float(i + 1))
rtree.RTreeInsertRect(rect, i + 1, tree)
yield tree
rtree.RTreeDestroyTree(tree)


def test_search_returns_the_overlapping_rectangles(tree) -> None:
assert search(tree, 2.0, 7.0) == EXPECTED_IDS


def test_search_without_a_match_returns_nothing(tree) -> None:
"""A search rectangle beyond every inserted rectangle finds nothing"""
assert search(tree, 20.0, 30.0) == []
2 changes: 1 addition & 1 deletion python/grass/temporal/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ GDIR = $(PYDIR)/grass
DSTDIR = $(GDIR)/temporal
DSTDIRPLY = $(DSTDIR)/ply

MODULES = ply/__init__ ply/lex ply/yacc base core abstract_dataset abstract_map_dataset abstract_space_time_dataset space_time_datasets open_stds factory gui_support list_stds register sampling metadata spatial_extent temporal_extent datetime_math temporal_granularity spatio_temporal_relationships unit_tests aggregation stds_export stds_import extract mapcalc univar_statistics temporal_topology_dataset_connector spatial_topology_dataset_connector c_libraries_interface temporal_algebra temporal_vector_algebra temporal_raster_base_algebra temporal_raster_algebra temporal_raster3d_algebra temporal_operator
MODULES = ply/__init__ ply/lex ply/yacc base core abstract_dataset abstract_map_dataset abstract_space_time_dataset space_time_datasets open_stds factory gui_support list_stds register sampling metadata spatial_extent temporal_extent datetime_math temporal_granularity spatio_temporal_relationships aggregation stds_export stds_import extract mapcalc univar_statistics temporal_topology_dataset_connector spatial_topology_dataset_connector c_libraries_interface temporal_algebra temporal_vector_algebra temporal_raster_base_algebra temporal_raster_algebra temporal_raster3d_algebra temporal_operator

CLEAN_SUBDIRS = ply

Expand Down
65 changes: 65 additions & 0 deletions python/grass/temporal/tests/grass_temporal_datetime_math_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Tests for datetime arithmetic and granularity adjustment

Both functions are pure datetime arithmetic, so no GRASS session and no
temporal database are needed here.
"""

from datetime import datetime

import pytest

from grass.temporal.datetime_math import (
adjust_datetime_to_granularity,
increment_datetime_by_string,
)


@pytest.mark.parametrize(
("start", "increment", "expected"),
[
# A single increment string may combine every supported unit. Seconds
# carry into minutes and weeks are added on top of the days.
(
datetime(2001, 9, 1),
"60 seconds, 4 minutes, 12 hours, 10 days, 1 weeks, 5 months, 1 years",
datetime(2003, 2, 18, 12, 5),
),
# A month increment lands on the same day of the following month.
(datetime(2001, 11, 1), "1 months", datetime(2001, 12, 1)),
# More than twelve months has to roll the year over.
(datetime(2001, 11, 1), "13 months", datetime(2002, 12, 1)),
(datetime(2001, 1, 1), "72 months", datetime(2007, 1, 1)),
],
)
def test_increment_datetime_by_string(start, increment, expected) -> None:
assert increment_datetime_by_string(start, increment) == expected


@pytest.mark.parametrize(
("granularity", "expected"),
[
# The date is truncated to the granularity unit, so a granularity
# finer than or equal to the seconds of the date leaves it unchanged.
("5 seconds", datetime(2001, 8, 8, 12, 30, 30)),
("20 minutes", datetime(2001, 8, 8, 12, 30)),
("3 hours", datetime(2001, 8, 8, 12, 0)),
("5 days", datetime(2001, 8, 8, 0, 0)),
# Weeks snap back to the Monday of the week, 2001-08-08 is a Wednesday.
("2 weeks", datetime(2001, 8, 6, 0, 0)),
("6 months", datetime(2001, 8, 1, 0, 0)),
("2 years", datetime(2001, 1, 1, 0, 0)),
# With several units the finest one present wins, because truncating to
# it already satisfies all the coarser ones.
(
"2 years, 3 months, 5 days, 3 hours, 3 minutes, 2 seconds",
datetime(2001, 8, 8, 12, 30, 30),
),
("3 months, 5 days, 3 minutes", datetime(2001, 8, 8, 12, 30)),
("3 weeks, 5 days", datetime(2001, 8, 8, 0, 0)),
],
)
def test_adjust_datetime_to_granularity(granularity, expected) -> None:
assert (
adjust_datetime_to_granularity(datetime(2001, 8, 8, 12, 30, 30), granularity)
== expected
)
103 changes: 103 additions & 0 deletions python/grass/temporal/tests/grass_temporal_granularity_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""Tests for the absolute time granularity computation

The granularity of a map list is the largest unit in which the start and end
times of every map can be expressed. Maps are built in memory only, so no
GRASS session and no temporal database are needed here.
"""

from datetime import datetime

import pytest

from grass.temporal.datetime_math import increment_datetime_by_string
from grass.temporal.space_time_datasets import RasterDataset
from grass.temporal.temporal_granularity import compute_absolute_time_granularity


def interval_maps(start, increment, count):
"""Return count maps covering consecutive intervals of the given increment"""
maps = []
for i in range(count):
map_ = RasterDataset(None)
map_.set_absolute_time(
increment_datetime_by_string(start, increment, i),
increment_datetime_by_string(start, increment, i + 1),
)
maps.append(map_)
return maps


def point_maps(start, increment, count):
"""Return count maps which are time points spaced by the given increment"""
maps = []
for i in range(count):
map_ = RasterDataset(None)
map_.set_absolute_time(increment_datetime_by_string(start, increment, i), None)
maps.append(map_)
return maps


@pytest.mark.parametrize(
("start", "increment", "count", "expected"),
[
(datetime(2001, 1, 1), "1 year", 10, "1 year"),
(datetime(2001, 1, 1), "3 years", 10, "3 years"),
(datetime(2001, 5, 1), "1 month", 20, "1 month"),
(datetime(2001, 1, 1), "3 months", 20, "3 months"),
(datetime(2001, 1, 1), "1 day", 6, "1 day"),
(datetime(2001, 1, 14), "14 days", 6, "14 days"),
(datetime(2001, 6, 12), "6 hours", 20, "6 hours"),
(datetime(2001, 1, 1), "20 minutes", 20, "20 minutes"),
(datetime(2001, 12, 31, 12, 30, 30), "3600 seconds", 24, "3600 seconds"),
# Mixed units collapse to the finest unit that can express every
# boundary. Months and days cannot be combined, so days remain.
(datetime(2001, 3, 1), "1 month, 4 days", 20, "1 day"),
(datetime(2001, 2, 11), "1 days, 1 hours", 20, "25 hours"),
(datetime(2001, 1, 1), "5 hours, 25 minutes", 20, "325 minutes"),
(datetime(2001, 1, 1), "5 minutes, 30 seconds", 20, "330 seconds"),
(datetime(2001, 12, 31), "60 minutes, 30 seconds", 24, "3630 seconds"),
],
)
def test_granularity_of_intervals(start, increment, count, expected) -> None:
assert (
compute_absolute_time_granularity(interval_maps(start, increment, count))
== expected
)


@pytest.mark.parametrize(
("start", "increment", "count", "expected"),
[
(datetime(2001, 12, 31, 12, 30, 30), "3600 seconds", 24, "3600 seconds"),
(datetime(2001, 12, 31), "20 days", 24, "20 days"),
(datetime(2001, 12, 1), "5 months", 24, "5 months"),
],
)
def test_granularity_of_time_points(start, increment, count, expected) -> None:
assert (
compute_absolute_time_granularity(point_maps(start, increment, count))
== expected
)


@pytest.mark.parametrize(
("interval_start", "point_start", "increment", "count", "expected"),
[
(
datetime(2001, 12, 31, 12, 30, 30),
datetime(2002, 2, 1, 12, 30, 30),
"3600 seconds",
24,
"3600 seconds",
),
(datetime(2001, 1, 1), datetime(2001, 2, 2), "2 days", 8, "2 days"),
],
)
def test_granularity_of_intervals_and_time_points(
interval_start, point_start, increment, count, expected
) -> None:
"""A list may mix intervals and time points and still have a granularity"""
maps = interval_maps(interval_start, increment, count) + point_maps(
point_start, increment, count
)
assert compute_absolute_time_granularity(maps) == expected
Loading
Loading