From 400ec9a32c6783f183ca1dd401fa62846f3ec82a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Tue, 20 Oct 2026 13:03:24 +0200 Subject: [PATCH 1/9] Bind PyMEOS to the MEOS master surface PyMEOS 1.4.0a1 requires pymeos-cffi 1.4.0a1 or later and calls MEOS master's names and signatures. Text, timestamp, date and interval input and output go through cstring_to_text, text_to_cstring, timestamptz_in, date_in and interval_in; multiplication through the mul_* family; the conversions to an instant, sequence or sequence set through temporal_as_*; the temporal distance of numbers through the tdistance_* family, which takes an int for a TInt and a float for a TFloat; and the box nearest approach distance of a temporal point through nad_tgeo_stbox. The spatial relationships drop their restr and atvalue arguments, and the distance, length, speed and nearest approach functions take spheroid=True, the MobilityDB default. The tiling and split functions pass border_inc=True, the MobilityDB default, so a value on the upper border lands in the tile that starts there and time_split_n answers an (n+1)th tile holding the end value when the end is inclusive. Set and span set accessors read the array from the (array, count) pair the functions return, and the time distances convert the returned interval to a timedelta. --- pymeos/__init__.py | 2 +- pymeos/aggregators/aggregator.py | 4 +- pymeos/boxes/stbox.py | 147 +++++++++++++++---------- pymeos/boxes/tbox.py | 8 +- pymeos/collections/base/spanset.py | 2 +- pymeos/collections/geo/geoset.py | 2 +- pymeos/collections/number/floatset.py | 2 +- pymeos/collections/number/intset.py | 2 +- pymeos/collections/text/textset.py | 8 +- pymeos/collections/time/dateset.py | 6 +- pymeos/collections/time/datespan.py | 4 +- pymeos/collections/time/tstzset.py | 8 +- pymeos/collections/time/tstzspan.py | 8 +- pymeos/collections/time/tstzspanset.py | 6 +- pymeos/main/tfloat.py | 10 +- pymeos/main/tint.py | 10 +- pymeos/main/tnumber.py | 33 +++--- pymeos/main/tpoint.py | 107 ++++++------------ pymeos/main/ttext.py | 4 +- pymeos/temporal/temporal.py | 47 ++++---- pymeos/temporal/tinstant.py | 4 +- pymeos/temporal/tsequence.py | 2 - pymeos/temporal/tsequenceset.py | 3 +- pyproject.toml | 2 +- 24 files changed, 211 insertions(+), 220 deletions(-) diff --git a/pymeos/__init__.py b/pymeos/__init__.py index 1aa97301..004ad8ab 100644 --- a/pymeos/__init__.py +++ b/pymeos/__init__.py @@ -30,7 +30,7 @@ from .meos_init import * from .temporal import * -__version__ = "1.3.0" +__version__ = "1.4.0-alpha-1" __all__ = [ # initialization "pymeos_initialize", diff --git a/pymeos/aggregators/aggregator.py b/pymeos/aggregators/aggregator.py index be86e04d..96d0dc63 100644 --- a/pymeos/aggregators/aggregator.py +++ b/pymeos/aggregators/aggregator.py @@ -243,12 +243,12 @@ def _add( interval_converted = ( timedelta_to_interval(interval) if isinstance(interval, timedelta) - else pg_interval_in(interval, -1) + else interval_in(interval, -1) if isinstance(interval, str) else None ) origin_converted = ( - datetime_to_timestamptz(origin) if isinstance(origin, datetime) else pg_timestamptz_in(origin, -1) + datetime_to_timestamptz(origin) if isinstance(origin, datetime) else timestamptz_in(origin, -1) ) return cls._add_function(state, temporal._inner, interval_converted, origin_converted) diff --git a/pymeos/boxes/stbox.py b/pymeos/boxes/stbox.py index 9d24f7bc..0cfbfd21 100644 --- a/pymeos/boxes/stbox.py +++ b/pymeos/boxes/stbox.py @@ -178,7 +178,7 @@ def from_geometry(geom: shp.BaseGeometry, geodetic: bool = False) -> STBox: A new :class:`STBox` instance. MEOS Functions: - pgis_geometry_in, geo_to_stbox + geom_in, geog_in, geo_to_stbox """ gs = geo_to_gserialized(geom, geodetic) return STBox(_inner=geo_to_stbox(gs)) @@ -1157,15 +1157,15 @@ def nearest_approach_distance(self, other: shp.BaseGeometry | STBox | TPoint) -> ``self`` and ``other``. MEOS Functions: - nad_stbox_geo, nad_stbox_stbox + nad_stbox_geo, nad_stbox_stbox, nad_tgeo_stbox """ if isinstance(other, shp.BaseGeometry): gs = geo_to_gserialized(other, self.geodetic()) - return nad_stbox_geo(self._inner, gs) + return nad_stbox_geo(self._inner, gs, spheroid=True) elif isinstance(other, STBox): - return nad_stbox_stbox(self._inner, other._inner) + return nad_stbox_stbox(self._inner, other._inner, spheroid=True) elif isinstance(other, TPoint): - return nad_tpoint_stbox(other._inner, self._inner) + return nad_tgeo_stbox(other._inner, self._inner, spheroid=True) else: raise TypeError(f"Operation not supported with type {other.__class__}") @@ -1243,69 +1243,102 @@ def quad_split(self) -> list[list[STBox]] | list[list[list[STBox]]]: [STBox(_inner=boxes + i) for i in range(2, 4)], ] - def tile( + def space_tiles( self, - size: float | None = None, - duration: timedelta | str | None = None, - origin: shp.BaseGeometry | None = None, - start: datetime | str | None = None, - border_inc: bool = True, + xsize: float, + ysize: float, + zsize: float, + origin: shp.BaseGeometry, + border_inc: bool, ) -> list[STBox]: """ - Returns a list of `STBox` instances representing the tiles of - ``self``. + Returns the spatial tiles of ``self``. + + The arguments are those of the MEOS function, marshalled as + #TPoint.space_split marshals the origin. Args: - size: The size of the spatial tiles. If the `STBox` instance has a - spatial dimension and this argument is not provided, the tiling - will be only temporal. - duration: The duration of the temporal tiles. If the `STBox` - instance has a time dimension and this argument is not - provided, the tiling will be only spatial. - origin: The origin of the spatial tiling. If not provided, the - origin will be (0, 0, 0). - start: The start time of the temporal tiling. If not provided, - the start time used by default is Monday, January 3, 2000. - border_inc: Whether the border is inclusive or not. + xsize: The size of the tiles along the x dimension. + ysize: The size of the tiles along the y dimension; 0 takes ``xsize``. + zsize: The size of the tiles along the z dimension, read only for a + box with Z; 0 takes ``xsize``. + origin: The origin of the spatial tiling. + border_inc: Whether a tile contains its upper border. Returns: A list of `STBox` instances. MEOS Functions: - stbox_tile_list + stbox_space_tiles """ - sz = size or ( - max( - self.xmax() - self.xmin(), - self.ymax() - self.ymin(), - (self.zmax() - self.zmin() if self.has_z() else 0), - ) - + 1 - ) - dt = ( - timedelta_to_interval(duration) - if isinstance(duration, timedelta) - else pg_interval_in(duration, -1) - if isinstance(duration, str) - else None - ) - st = ( - datetime_to_timestamptz(start) - if isinstance(start, datetime) - else ( - pg_timestamptz_in(start, -1) - if isinstance(start, str) - else pg_timestamptz_in("2000-01-03", -1) - if self.has_t() - else 0 - ) - ) - gs = ( - geo_to_gserialized(origin, self.geodetic()) - if origin is not None - else (pgis_geography_in("Point(0 0 0)", -1) if self.geodetic() else pgis_geometry_in("Point(0 0 0)", -1)) - ) - tiles, count = stbox_space_time_tiles(self._inner, sz, sz, sz, dt, gs, st, border_inc) + gs = geo_to_gserialized(origin, self.geodetic()) + tiles, count = stbox_space_tiles(self._inner, xsize, ysize, zsize, gs, border_inc) + return [STBox(_inner=tiles + i) for i in range(count)] + + def space_time_tiles( + self, + xsize: float, + ysize: float, + zsize: float, + duration: timedelta | str, + origin: shp.BaseGeometry, + start: datetime | str, + border_inc: bool, + ) -> list[STBox]: + """ + Returns the spatiotemporal tiles of ``self``. + + The arguments are those of the MEOS function, marshalled as + #TPoint.space_time_split marshals the duration, the origin and the start. + + Args: + xsize: The size of the tiles along the x dimension. + ysize: The size of the tiles along the y dimension; 0 takes ``xsize``. + zsize: The size of the tiles along the z dimension, read only for a + box with Z; 0 takes ``xsize``. + duration: The duration of the tiles. + origin: The origin of the spatial tiling. + start: The origin of the temporal tiling. + border_inc: Whether a tile contains its upper border. + + Returns: + A list of `STBox` instances. + + MEOS Functions: + stbox_space_time_tiles + """ + dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else interval_in(duration, -1) + gs = geo_to_gserialized(origin, self.geodetic()) + st = datetime_to_timestamptz(start) if isinstance(start, datetime) else timestamptz_in(start, -1) + tiles, count = stbox_space_time_tiles(self._inner, xsize, ysize, zsize, dt, gs, st, border_inc) + return [STBox(_inner=tiles + i) for i in range(count)] + + def time_tiles( + self, + duration: timedelta | str, + start: datetime | str, + border_inc: bool, + ) -> list[STBox]: + """ + Returns the temporal tiles of ``self``. + + The arguments are those of the MEOS function, marshalled as + #TPoint.space_time_split marshals the duration and the start. + + Args: + duration: The duration of the tiles. + start: The origin of the temporal tiling. + border_inc: Whether a tile contains its upper border. + + Returns: + A list of `STBox` instances. + + MEOS Functions: + stbox_time_tiles + """ + dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else interval_in(duration, -1) + st = datetime_to_timestamptz(start) if isinstance(start, datetime) else timestamptz_in(start, -1) + tiles, count = stbox_time_tiles(self._inner, dt, st, border_inc) return [STBox(_inner=tiles + i) for i in range(count)] # ------------------------- Comparisons ----------------------------------- diff --git a/pymeos/boxes/tbox.py b/pymeos/boxes/tbox.py index 336ecca1..bc4b2967 100644 --- a/pymeos/boxes/tbox.py +++ b/pymeos/boxes/tbox.py @@ -1086,16 +1086,16 @@ def tile( MEOS Functions: tintbox_tile_list, tfloabox_tile_list """ - dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else pg_interval_in(duration, -1) + dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else interval_in(duration, -1) st = ( datetime_to_timestamptz(start) if isinstance(start, datetime) - else (pg_timestamptz_in(start, -1) if isinstance(start, str) else pg_timestamptz_in("2000-01-03", -1)) + else (timestamptz_in(start, -1) if isinstance(start, str) else timestamptz_in("2000-01-03", -1)) ) if self._is_float(): - tiles, count = tfloatbox_value_time_tiles(self._inner, size, dt, origin, st) + tiles, count = tfloatbox_value_time_tiles(self._inner, size, dt, origin, st, border_inc=True) else: - tiles, count = tintbox_value_time_tiles(self._inner, int(size), dt, int(origin), st) + tiles, count = tintbox_value_time_tiles(self._inner, int(size), dt, int(origin), st, border_inc=True) return [TBox(_inner=tiles + c) for c in range(count)] # ------------------------- Comparisons ----------------------------------- diff --git a/pymeos/collections/base/spanset.py b/pymeos/collections/base/spanset.py index 232fb5f3..9b63c0d0 100644 --- a/pymeos/collections/base/spanset.py +++ b/pymeos/collections/base/spanset.py @@ -211,7 +211,7 @@ def spans(self) -> list[Span]: MEOS Functions: spanset_spans """ - return spanset_spanarr(self._inner) + return spanset_spanarr(self._inner)[0] def __hash__(self) -> int: """ diff --git a/pymeos/collections/geo/geoset.py b/pymeos/collections/geo/geoset.py index 5b794d0a..fd3efde4 100644 --- a/pymeos/collections/geo/geoset.py +++ b/pymeos/collections/geo/geoset.py @@ -161,7 +161,7 @@ def elements(self) -> list[shp.Geometry]: MEOS Functions: geoset_values """ - elems = geoset_values(self._inner) + elems = geoset_values(self._inner)[0] return [gserialized_to_shapely_geometry(elems[i]) for i in range(self.num_elements())] def srid(self) -> int: diff --git a/pymeos/collections/number/floatset.py b/pymeos/collections/number/floatset.py index f1332d30..23a77774 100644 --- a/pymeos/collections/number/floatset.py +++ b/pymeos/collections/number/floatset.py @@ -141,7 +141,7 @@ def elements(self) -> list[float]: MEOS Functions: floattset_values """ - elems = floatset_values(self._inner) + elems = floatset_values(self._inner)[0] return [elems[i] for i in range(self.num_elements())] # ------------------------- Transformations ------------------------------------ diff --git a/pymeos/collections/number/intset.py b/pymeos/collections/number/intset.py index a512c7d5..299ad444 100644 --- a/pymeos/collections/number/intset.py +++ b/pymeos/collections/number/intset.py @@ -140,7 +140,7 @@ def elements(self) -> list[int]: MEOS Functions: inttset_values """ - elems = intset_values(self._inner) + elems = intset_values(self._inner)[0] return [elems[i] for i in range(self.num_elements())] # ------------------------- Transformations ------------------------------------ diff --git a/pymeos/collections/text/textset.py b/pymeos/collections/text/textset.py index 0a1caddf..28eb0e8b 100644 --- a/pymeos/collections/text/textset.py +++ b/pymeos/collections/text/textset.py @@ -95,7 +95,7 @@ def element_n(self, n: int): textset_value_n """ super().element_n(n) - return text2cstring(textset_value_n(self._inner, n + 1)[0]) + return text_to_cstring(textset_value_n(self._inner, n + 1)[0]) def elements(self): """ @@ -107,8 +107,8 @@ def elements(self): MEOS Functions: textset_values """ - elems = textset_values(self._inner) - return [text2cstring(elems[i]) for i in range(self.num_elements())] + elems = textset_values(self._inner)[0] + return [text_to_cstring(elems[i]) for i in range(self.num_elements())] # ------------------------- Topological Operations -------------------------------- @@ -224,7 +224,7 @@ def subtract_from(self, other: str) -> str | None: :meth:`minus` """ result = minus_text_set(other, self._inner) - return text2cstring(result[0]) if result is not None else None + return text_to_cstring(result[0]) if result is not None else None def union(self, other: TextSet | str) -> TextSet: """ diff --git a/pymeos/collections/time/dateset.py b/pymeos/collections/time/dateset.py index 0675eeb2..29e64672 100644 --- a/pymeos/collections/time/dateset.py +++ b/pymeos/collections/time/dateset.py @@ -8,6 +8,7 @@ before_set_date, contains_set_date, date_adt_to_date, + date_in, date_to_date_adt, dateset_end_value, dateset_in, @@ -26,7 +27,6 @@ minus_set_set, overafter_set_date, overbefore_set_date, - pg_date_in, union_set_date, union_set_set, ) @@ -63,7 +63,7 @@ class DateSet(Set[date], TimeCollection[date]): _mobilitydb_name = "dateset" _parse_function = dateset_in - _parse_value_function = lambda x: pg_date_in(x) if isinstance(x, str) else date_to_date_adt(x) + _parse_value_function = lambda x: date_in(x) if isinstance(x, str) else date_to_date_adt(x) _make_function = dateset_make # ------------------------- Constructors ---------------------------------- @@ -162,7 +162,7 @@ def elements(self) -> list[date]: MEOS Functions: dateset_values """ - tss = dateset_values(self._inner) + tss = dateset_values(self._inner)[0] return [date_adt_to_date(tss[i]) for i in range(self.num_elements())] # ------------------------- Transformations ------------------------------- diff --git a/pymeos/collections/time/datespan.py b/pymeos/collections/time/datespan.py index 934e2529..45f24c3b 100644 --- a/pymeos/collections/time/datespan.py +++ b/pymeos/collections/time/datespan.py @@ -9,6 +9,7 @@ before_span_date, contains_span_date, date_adt_to_date, + date_in, date_to_date_adt, datespan_duration, datespan_in, @@ -30,7 +31,6 @@ minus_span_spanset, overafter_span_date, overbefore_span_date, - pg_date_in, union_span_date, ) @@ -69,7 +69,7 @@ class DateSpan(Span[date], TimeCollection[date]): _mobilitydb_name = "datespan" _parse_function = datespan_in - _parse_value_function = lambda x: pg_date_in(x) if isinstance(x, str) else date_to_date_adt(x) + _parse_value_function = lambda x: date_in(x) if isinstance(x, str) else date_to_date_adt(x) _make_function = datespan_make # ------------------------- Constructors ---------------------------------- diff --git a/pymeos/collections/time/tstzset.py b/pymeos/collections/time/tstzset.py index 545b885e..08d0fa15 100644 --- a/pymeos/collections/time/tstzset.py +++ b/pymeos/collections/time/tstzset.py @@ -39,7 +39,7 @@ class TsTzSet(Set[datetime], TimeCollection[datetime]): _mobilitydb_name = "tstzset" _parse_function = tstzset_in - _parse_value_function = lambda x: pg_timestamptz_in(x, -1) if isinstance(x, str) else datetime_to_timestamptz(x) + _parse_value_function = lambda x: timestamptz_in(x, -1) if isinstance(x, str) else datetime_to_timestamptz(x) _make_function = tstzset_make # ------------------------- Constructors ---------------------------------- @@ -116,7 +116,7 @@ def elements(self) -> list[datetime]: MEOS Functions: set_timestamptzs """ - tss = tstzset_values(self._inner) + tss = tstzset_values(self._inner)[0] return [timestamptz_to_datetime(tss[i]) for i in range(self.num_elements())] # ------------------------- Transformations ------------------------------- @@ -512,9 +512,9 @@ def distance(self, other: Time | Temporal | Box) -> timedelta: from .tstzspanset import TsTzSpanSet if isinstance(other, datetime): - return timedelta(seconds=distance_set_timestamptz(self._inner, datetime_to_timestamptz(other))) + return interval_to_timedelta(distance_set_timestamptz(self._inner, datetime_to_timestamptz(other))) elif isinstance(other, TsTzSet): - return timedelta(seconds=distance_tstzset_tstzset(self._inner, other._inner)) + return interval_to_timedelta(distance_tstzset_tstzset(self._inner, other._inner)) elif isinstance(other, (TsTzSpan, TsTzSpanSet)): return self.to_spanset().distance(other) elif isinstance(other, (Temporal, get_args(Box))): diff --git a/pymeos/collections/time/tstzspan.py b/pymeos/collections/time/tstzspan.py index 966e5c98..62c8b5d9 100644 --- a/pymeos/collections/time/tstzspan.py +++ b/pymeos/collections/time/tstzspan.py @@ -41,7 +41,7 @@ class TsTzSpan(Span[datetime], TimeCollection[datetime]): _mobilitydb_name = "tstzspan" _parse_function = tstzspan_in - _parse_value_function = lambda x: pg_timestamptz_in(x, -1) if isinstance(x, str) else datetime_to_timestamptz(x) + _parse_value_function = lambda x: timestamptz_in(x, -1) if isinstance(x, str) else datetime_to_timestamptz(x) _make_function = tstzspan_make # ------------------------- Constructors ---------------------------------- @@ -505,13 +505,13 @@ def distance(self, other: Time | Box | Temporal) -> timedelta: from .tstzspanset import TsTzSpanSet if isinstance(other, datetime): - return timedelta(seconds=distance_span_timestamptz(self._inner, datetime_to_timestamptz(other))) + return interval_to_timedelta(distance_span_timestamptz(self._inner, datetime_to_timestamptz(other))) elif isinstance(other, TsTzSet): return self.distance(other.to_spanset()) elif isinstance(other, TsTzSpan): - return timedelta(seconds=distance_tstzspan_tstzspan(self._inner, other._inner)) + return interval_to_timedelta(distance_tstzspan_tstzspan(self._inner, other._inner)) elif isinstance(other, TsTzSpanSet): - return timedelta(seconds=distance_tstzspanset_tstzspan(other._inner, self._inner)) + return interval_to_timedelta(distance_tstzspanset_tstzspan(other._inner, self._inner)) elif isinstance(other, Temporal): return self.distance(other.tstzspan()) elif isinstance(other, get_args(Box)): diff --git a/pymeos/collections/time/tstzspanset.py b/pymeos/collections/time/tstzspanset.py index 9e5f10fb..0b824080 100644 --- a/pymeos/collections/time/tstzspanset.py +++ b/pymeos/collections/time/tstzspanset.py @@ -631,13 +631,13 @@ def distance(self, other: Time | Box | Temporal) -> timedelta: from .tstzspan import TsTzSpan if isinstance(other, datetime): - return timedelta(seconds=distance_spanset_timestamptz(self._inner, datetime_to_timestamptz(other))) + return interval_to_timedelta(distance_spanset_timestamptz(self._inner, datetime_to_timestamptz(other))) elif isinstance(other, TsTzSet): return self.distance(other.to_spanset()) elif isinstance(other, TsTzSpan): - return timedelta(seconds=distance_tstzspanset_tstzspan(self._inner, other._inner)) + return interval_to_timedelta(distance_tstzspanset_tstzspan(self._inner, other._inner)) elif isinstance(other, TsTzSpanSet): - return timedelta(seconds=distance_tstzspanset_tstzspanset(self._inner, other._inner)) + return interval_to_timedelta(distance_tstzspanset_tstzspanset(self._inner, other._inner)) if isinstance(other, Temporal): return self.distance(other.tstzspan()) elif isinstance(other, get_args(Box)): diff --git a/pymeos/main/tfloat.py b/pymeos/main/tfloat.py index 242b3513..acf659da 100644 --- a/pymeos/main/tfloat.py +++ b/pymeos/main/tfloat.py @@ -896,7 +896,7 @@ def value_split(self, size: float, start: float | None = 0) -> list[Temporal]: MEOS Functions: tfloat_value_split """ - fragments, _, count = tfloat_value_split(self._inner, size, start) + fragments, _, count = tfloat_value_split(self._inner, size, start, border_inc=True) return [Temporal._factory(fragments[i]) for i in range(count)] def value_time_split( @@ -924,15 +924,15 @@ def value_time_split( tfloat_value_time_split """ if time_start is None: - st = pg_timestamptz_in("2000-01-03", -1) + st = timestamptz_in("2000-01-03", -1) else: st = ( datetime_to_timestamptz(time_start) if isinstance(time_start, datetime) - else pg_timestamptz_in(time_start, -1) + else timestamptz_in(time_start, -1) ) - dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else pg_interval_in(duration, -1) - fragments, _, _, count = tfloat_value_time_split(self._inner, value_size, dt, value_start, st) + dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else interval_in(duration, -1) + fragments, _, _, count = tfloat_value_time_split(self._inner, value_size, dt, value_start, st, border_inc=True) return [Temporal._factory(fragments[i]) for i in range(count)] # ------------------------- Database Operations --------------------------- diff --git a/pymeos/main/tint.py b/pymeos/main/tint.py index 658e457e..7d218c3b 100644 --- a/pymeos/main/tint.py +++ b/pymeos/main/tint.py @@ -833,7 +833,7 @@ def value_split(self, size: int, start: int | None = 0) -> list[TInt]: MEOS Functions: tint_value_split """ - fragments, values, count = tint_value_split(self._inner, size, start) + fragments, values, count = tint_value_split(self._inner, size, start, border_inc=True) return [Temporal._factory(fragments[i]) for i in range(count)] def value_time_split( @@ -861,15 +861,15 @@ def value_time_split( tint_value_time_split """ if time_start is None: - st = pg_timestamptz_in("2000-01-03", -1) + st = timestamptz_in("2000-01-03", -1) else: st = ( datetime_to_timestamptz(time_start) if isinstance(time_start, datetime) - else pg_timestamptz_in(time_start, -1) + else timestamptz_in(time_start, -1) ) - dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else pg_interval_in(duration, -1) - tiles, _, _, count = tint_value_time_split(self._inner, value_size, dt, value_start, st) + dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else interval_in(duration, -1) + tiles, _, _, count = tint_value_time_split(self._inner, value_size, dt, value_start, st, border_inc=True) return [Temporal._factory(tiles[i]) for i in range(count)] # ------------------------- Database Operations --------------------------- diff --git a/pymeos/main/tnumber.py b/pymeos/main/tnumber.py index c8e33e42..854db1c9 100644 --- a/pymeos/main/tnumber.py +++ b/pymeos/main/tnumber.py @@ -401,17 +401,17 @@ def mul(self, other: int | float | TNumber) -> TNumber: A new temporal object of the same subtype as `self`. MEOS Functions: - mult_tint_int, mult_tfloat_float, mult_tnumber_tnumber + mul_tint_int, mul_tfloat_float, mul_tnumber_tnumber """ from .tfloat import TFloat from .tint import TInt if isinstance(self, TInt) and isinstance(other, int): - result = mult_tint_int(self._inner, other) + result = mul_tint_int(self._inner, other) elif isinstance(self, TFloat) and isinstance(other, (int, float)): - result = mult_tfloat_float(self._inner, float(other)) + result = mul_tfloat_float(self._inner, float(other)) elif isinstance(other, TNumber): - result = mult_tnumber_tnumber(self._inner, other._inner) + result = mul_tnumber_tnumber(self._inner, other._inner) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) @@ -428,15 +428,15 @@ def rmul(self, other: int | float) -> TNumber: A new temporal object of the same subtype as `self`. MEOS Functions: - mult_int_tint, mult_float_tfloat + mul_int_tint, mul_float_tfloat """ from .tfloat import TFloat from .tint import TInt if isinstance(self, TInt) and isinstance(other, int): - result = mult_int_tint(other, self._inner) + result = mul_int_tint(other, self._inner) elif isinstance(self, TFloat) and isinstance(other, (int, float)): - result = mult_float_tfloat(float(other), self._inner) + result = mul_float_tfloat(float(other), self._inner) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) @@ -566,7 +566,7 @@ def __mul__(self, other): A new temporal object of the same subtype as `self`. MEOS Functions: - mult_tint_int, mult_tfloat_float, mult_tnumber_tnumber + mul_tint_int, mul_tfloat_float, mul_tnumber_tnumber """ return self.mul(other) @@ -582,7 +582,7 @@ def __rmul__(self, other): A new temporal object of the same subtype as `self`. MEOS Functions: - mult_int_tint, mult_float_tfloat + mul_int_tint, mul_float_tfloat """ return self.rmul(other) @@ -660,14 +660,17 @@ def distance(self, other: int | float | TNumber) -> TFloat: A :class:`TFloat` with the distance between `self` and `other`. MEOS Functions: - distance_tfloat_float, distance_tnumber_tnumber + tdistance_tint_int, tdistance_tfloat_float, tdistance_tnumber_tnumber """ - if isinstance(other, int): - result = distance_tfloat_float(self._inner, float(other)) - elif isinstance(other, float): - result = distance_tfloat_float(self._inner, other) + from .tfloat import TFloat + from .tint import TInt + + if isinstance(self, TInt) and isinstance(other, int): + result = tdistance_tint_int(self._inner, other) + elif isinstance(self, TFloat) and isinstance(other, (int, float)): + result = tdistance_tfloat_float(self._inner, float(other)) elif isinstance(other, TNumber): - result = distance_tnumber_tnumber(self._inner, other._inner) + result = tdistance_tnumber_tnumber(self._inner, other._inner) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) diff --git a/pymeos/main/tpoint.py b/pymeos/main/tpoint.py index 9ae1441b..fb5e2ffe 100644 --- a/pymeos/main/tpoint.py +++ b/pymeos/main/tpoint.py @@ -235,7 +235,7 @@ def length(self) -> float: MEOS Functions: tpoint_length """ - return tpoint_length(self._inner) + return tpoint_length(self._inner, spheroid=True) def cumulative_length(self) -> TFloat: """ @@ -247,7 +247,7 @@ def cumulative_length(self) -> TFloat: MEOS Functions: tpoint_cumulative_length """ - result = tpoint_cumulative_length(self._inner) + result = tpoint_cumulative_length(self._inner, spheroid=True) return Temporal._factory(result) def speed(self) -> TFloat: @@ -260,7 +260,7 @@ def speed(self) -> TFloat: MEOS Functions: tpoint_speed """ - result = tpoint_speed(self._inner) + result = tpoint_speed(self._inner, spheroid=True) return Temporal._factory(result) def x(self) -> TF: @@ -787,11 +787,11 @@ def is_ever_within_distance(self, other: shpb.BaseGeometry | TPoint | STBox, dis if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = edwithin_tgeo_geo(self._inner, gs, distance) + result = edwithin_tgeo_geo(self._inner, gs, distance, spheroid=True) elif isinstance(other, STBox): - result = edwithin_tgeo_geo(self._inner, stbox_to_geo(other._inner), distance) + result = edwithin_tgeo_geo(self._inner, stbox_to_geo(other._inner), distance, spheroid=True) elif isinstance(other, TPoint): - result = edwithin_tgeo_tgeo(self._inner, other._inner, distance) + result = edwithin_tgeo_tgeo(self._inner, other._inner, distance, spheroid=True) else: raise TypeError(f"Operation not supported with type {other.__class__}") return result == 1 @@ -866,10 +866,10 @@ def is_spatially_contained_in(self, container: shpb.BaseGeometry | STBox) -> TBo if isinstance(container, shpb.BaseGeometry): gs = geo_to_gserialized(container, isinstance(self, TGeogPoint)) - result = tcontains_geo_tgeo(gs, self._inner, False, False) + result = tcontains_geo_tgeo(gs, self._inner) elif isinstance(container, STBox): gs = stbox_to_geo(container._inner) - result = tcontains_geo_tgeo(gs, self._inner, False, False) + result = tcontains_geo_tgeo(gs, self._inner) else: raise TypeError(f"Operation not supported with type {container.__class__}") return Temporal._factory(result) @@ -892,11 +892,11 @@ def distance(self, other: shpb.BaseGeometry | TPoint | STBox) -> TFloat: if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = tdistance_tgeo_geo(self._inner, gs) + result = tdistance_tgeo_geo(self._inner, gs, spheroid=True) elif isinstance(other, STBox): - result = tdistance_tgeo_geo(self._inner, stbox_to_geo(other._inner)) + result = tdistance_tgeo_geo(self._inner, stbox_to_geo(other._inner), spheroid=True) elif isinstance(other, TPoint): - result = tdistance_tgeo_tgeo(self._inner, other._inner) + result = tdistance_tgeo_tgeo(self._inner, other._inner, spheroid=True) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) @@ -918,11 +918,11 @@ def nearest_approach_distance(self, other: shpb.BaseGeometry | STBox | TPoint) - if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - return nad_tgeo_geo(self._inner, gs) + return nad_tgeo_geo(self._inner, gs, spheroid=True) elif isinstance(other, STBox): - return nad_tgeo_stbox(self._inner, other._inner) + return nad_tgeo_stbox(self._inner, other._inner, spheroid=True) elif isinstance(other, TPoint): - return nad_tgeo_tgeo(self._inner, other._inner) + return nad_tgeo_tgeo(self._inner, other._inner, spheroid=True) else: raise TypeError(f"Operation not supported with type {other.__class__}") @@ -941,9 +941,9 @@ def nearest_approach_instant(self, other: shpb.BaseGeometry | TPoint) -> TI: """ if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = nai_tgeo_geo(self._inner, gs) + result = nai_tgeo_geo(self._inner, gs, spheroid=True) elif isinstance(other, TPoint): - result = nai_tgeo_tgeo(self._inner, other._inner) + result = nai_tgeo_tgeo(self._inner, other._inner, spheroid=True) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) @@ -964,54 +964,13 @@ def shortest_line(self, other: shpb.BaseGeometry | TPoint) -> shpb.BaseGeometry: """ if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = shortestline_tgeo_geo(self._inner, gs) + result = shortestline_tgeo_geo(self._inner, gs, spheroid=True) elif isinstance(other, TPoint): - result = shortestline_tgeo_tgeo(self._inner, other._inner) + result = shortestline_tgeo_tgeo(self._inner, other._inner, spheroid=True) else: raise TypeError(f"Operation not supported with type {other.__class__}") return gserialized_to_shapely_geometry(result, 10) - # ------------------------- Tiling Operations ----------------------------- - def tile( - self, - size: float, - duration: timedelta | str | None = None, - origin: shpb.BaseGeometry | None = None, - start: datetime | str | None = None, - remove_empty: bool | None = False, - ) -> list[TG]: - """ - Split the temporal point into segments following the tiling of the - bounding box. - - Args: - size: The size of the spatial tiles. If `self` has a spatial - dimension and this argument is not provided, the tiling will be - only temporal. - duration: The duration of the temporal tiles. If `self` has a time - dimension and this argument is not provided, the tiling will be - only spatial. - origin: The origin of the spatial tiling. If not provided, the - origin will be (0, 0, 0). - start: The start time of the temporal tiling. If not provided, - the start time used by default is Monday, January 3, 2000. - remove_empty: If True, remove the tiles that are empty. - - Returns: - A list of :class:`TPoint` objects. - - See Also: - :meth:`STBox.tile` - """ - from ..boxes import STBox - - bbox = STBox.from_tpoint(self) - tiles = bbox.tile(size, duration, origin, start) - if remove_empty: - return [x for x in (self.at(tile) for tile in tiles) if x] - else: - return [self.at(tile) for tile in tiles] - # ------------------------- Split Operations ------------------------------ def space_split( self, @@ -1086,19 +1045,19 @@ def space_time_split( """ ysz = ysize if ysize is not None else xsize zsz = zsize if zsize is not None else xsize - dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else pg_interval_in(duration, -1) + dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else interval_in(duration, -1) gs = ( geo_to_gserialized(origin, isinstance(self, TGeogPoint)) if origin is not None else (geog_in("Point(0 0 0)", -1) if isinstance(self, TGeogPoint) else geom_in("Point(0 0 0)", -1)) ) if time_start is None: - st = pg_timestamptz_in("2000-01-03", -1) + st = timestamptz_in("2000-01-03", -1) else: st = ( datetime_to_timestamptz(time_start) if isinstance(time_start, datetime) - else pg_timestamptz_in(time_start, -1) + else timestamptz_in(time_start, -1) ) fragments, points, times, count = tgeo_space_time_split( self._inner, xsize, ysz, zsz, dt, gs, st, bitmatrix, include_border @@ -1365,11 +1324,11 @@ def disjoint(self, other: shpb.BaseGeometry | STBox | TPoint) -> TBool: if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = tdisjoint_tgeo_geo(self._inner, gs, False, False) + result = tdisjoint_tgeo_geo(self._inner, gs) elif isinstance(other, STBox): - result = tdisjoint_tgeo_geo(self._inner, stbox_to_geo(other._inner), False, False) + result = tdisjoint_tgeo_geo(self._inner, stbox_to_geo(other._inner)) elif isinstance(other, TPoint): - result = tdisjoint_tgeo_tgeo(self._inner, other._inner, False, False) + result = tdisjoint_tgeo_tgeo(self._inner, other._inner) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) @@ -1391,11 +1350,11 @@ def intersects(self, other: shpb.BaseGeometry | STBox | TPoint) -> TBool: if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = tintersects_tgeo_geo(self._inner, gs, False, False) + result = tintersects_tgeo_geo(self._inner, gs) elif isinstance(other, STBox): - result = tintersects_tgeo_geo(self._inner, stbox_to_geo(other._inner), False, False) + result = tintersects_tgeo_geo(self._inner, stbox_to_geo(other._inner)) elif isinstance(other, TPoint): - result = tintersects_tgeo_tgeo(self._inner, other._inner, False, False) + result = tintersects_tgeo_tgeo(self._inner, other._inner) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) @@ -1417,11 +1376,11 @@ def touches(self, other: shpb.BaseGeometry | STBox | TGeomPoint) -> TBool: if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = ttouches_tgeo_geo(self._inner, gs, False, False) + result = ttouches_tgeo_geo(self._inner, gs) elif isinstance(other, STBox): - result = ttouches_tgeo_geo(self._inner, stbox_to_geo(other._inner), False, False) + result = ttouches_tgeo_geo(self._inner, stbox_to_geo(other._inner)) elif isinstance(other, TGeomPoint): - result = ttouches_tgeo_tgeo(self._inner, other._inner, False, False) + result = ttouches_tgeo_tgeo(self._inner, other._inner) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) @@ -1444,11 +1403,11 @@ def within_distance(self, other: shpb.BaseGeometry | TGeomPoint | STBox, distanc if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = tdwithin_tgeo_geo(self._inner, gs, distance, False, False) + result = tdwithin_tgeo_geo(self._inner, gs, distance, spheroid=True) elif isinstance(other, STBox): - result = tdwithin_tgeo_geo(self._inner, stbox_to_geo(other._inner), distance, False, False) + result = tdwithin_tgeo_geo(self._inner, stbox_to_geo(other._inner), distance, spheroid=True) elif isinstance(other, TGeomPoint): - result = tdwithin_tgeo_tgeo(self._inner, other._inner, distance, False, False) + result = tdwithin_tgeo_tgeo(self._inner, other._inner, distance, spheroid=True) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) diff --git a/pymeos/main/ttext.py b/pymeos/main/ttext.py index e956e732..2dfd521b 100644 --- a/pymeos/main/ttext.py +++ b/pymeos/main/ttext.py @@ -143,7 +143,7 @@ def value_set(self) -> set[str]: ttext_values """ values, count = ttext_values(self._inner) - return {text2cstring(values[i]) for i in range(count)} + return {text_to_cstring(values[i]) for i in range(count)} def min_value(self) -> str: """ @@ -233,7 +233,7 @@ def value_at_timestamp(self, timestamp: datetime) -> str: ttext_value_at_timestamp """ result = ttext_value_at_timestamptz(self._inner, datetime_to_timestamptz(timestamp), True) - return text2cstring(result[0]) + return text_to_cstring(result[0]) # ------------------------- Ever and Always Comparisons ------------------- def always_less(self, value: str | TText) -> bool: diff --git a/pymeos/temporal/temporal.py b/pymeos/temporal/temporal.py index b168ae66..9099df25 100644 --- a/pymeos/temporal/temporal.py +++ b/pymeos/temporal/temporal.py @@ -167,7 +167,7 @@ def from_merge(cls: type[Self], *temporals: TG) -> Self: temporal_merge_array """ _temporals = [temp._inner for temp in temporals if temp is not None] - result = temporal_merge_array(_temporals, len(_temporals)) + result = temporal_merge_array(_temporals) return Temporal._factory(result) @classmethod @@ -184,7 +184,7 @@ def from_merge_array(cls: type[Self], temporals: list[TG]) -> Self: objects. """ _temporals = [temp._inner for temp in temporals if temp is not None] - result = temporal_merge_array(_temporals, len(_temporals)) + result = temporal_merge_array(_temporals) return Temporal._factory(result) # ------------------------- Output ---------------------------------------- @@ -592,15 +592,15 @@ def temporal_sample( temporal_tsample """ if start is None: - st = pg_timestamptz_in("2000-01-03", -1) + st = timestamptz_in("2000-01-03", -1) elif isinstance(start, datetime): st = datetime_to_timestamptz(start) else: - st = pg_timestamptz_in(start, -1) + st = timestamptz_in(start, -1) if isinstance(duration, timedelta): dt = timedelta_to_interval(duration) else: - dt = pg_interval_in(duration, -1) + dt = interval_in(duration, -1) if interpolation is None: intrp = self.interpolation() else: @@ -627,15 +627,15 @@ def temporal_precision( temporal_tprecision """ if start is None: - st = pg_timestamptz_in("2000-01-03", -1) + st = timestamptz_in("2000-01-03", -1) elif isinstance(start, datetime): st = datetime_to_timestamptz(start) else: - st = pg_timestamptz_in(start, -1) + st = timestamptz_in(start, -1) if isinstance(duration, timedelta): dt = timedelta_to_interval(duration) else: - dt = pg_interval_in(duration, -1) + dt = interval_in(duration, -1) result = temporal_tprecision(self._inner, dt, st) return Temporal._factory(result) @@ -644,9 +644,9 @@ def to_instant(self) -> TI: Returns `self` as a :class:`TInstant`. MEOS Functions: - temporal_to_tinstant + temporal_as_tinstant """ - inst = temporal_to_tinstant(self._inner) + inst = temporal_as_tinstant(self._inner) return Temporal._factory(inst) def to_sequence(self, interpolation: TInterpolation) -> TS: @@ -656,7 +656,7 @@ def to_sequence(self, interpolation: TInterpolation) -> TS: MEOS Functions: temporal_to_sequence """ - seq = temporal_to_tsequence(self._inner, interpolation) + seq = temporal_as_tsequence(self._inner, interpolation) return Temporal._factory(seq) def to_sequenceset(self, interpolation: TInterpolation) -> TSS: @@ -664,9 +664,9 @@ def to_sequenceset(self, interpolation: TInterpolation) -> TSS: Returns `self` as a :class:`TSequenceSet`. MEOS Functions: - temporal_to_tsequenceset + temporal_as_tsequenceset """ - ss = temporal_to_tsequenceset(self._inner, interpolation) + ss = temporal_as_tsequenceset(self._inner, interpolation) return Temporal._factory(ss) def to_dataframe(self) -> pd.DataFrame: @@ -737,7 +737,7 @@ def merge(self, other: type(None) | Temporal[TBase] | list[Temporal[TBase]]) -> elif isinstance(other, Temporal): new_temp = temporal_merge(self._inner, other._inner) elif isinstance(other, list): - new_temp = temporal_merge_array([self._inner, *(o._inner for o in other)], len(other) + 1) + new_temp = temporal_merge_array([self._inner, *(o._inner for o in other)]) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(new_temp) @@ -1160,7 +1160,7 @@ def frechet_distance(self, other: Temporal) -> float: MEOS Functions: temporal_frechet_distance """ - return temporal_frechet_distance(self._inner, other._inner) + return temporal_frechet_distance(self._inner, other._inner, spheroid=True) def dyntimewarp_distance(self, other: Temporal) -> float: """ @@ -1175,7 +1175,7 @@ def dyntimewarp_distance(self, other: Temporal) -> float: MEOS Functions: temporal_dyntimewarp_distance """ - return temporal_dyntimewarp_distance(self._inner, other._inner) + return temporal_dyntimewarp_distance(self._inner, other._inner, spheroid=True) def hausdorff_distance(self, other: Temporal) -> float: """ @@ -1190,7 +1190,7 @@ def hausdorff_distance(self, other: Temporal) -> float: MEOS Functions: temporal_hausdorff_distance """ - return temporal_hausdorff_distance(self._inner, other._inner) + return temporal_hausdorff_distance(self._inner, other._inner, spheroid=True) # ------------------------- Split Operations ------------------------------ def time_split( @@ -1217,11 +1217,11 @@ def time_split( temporal_time_split """ if start is None: - st = pg_timestamptz_in("2000-01-03", -1) + st = timestamptz_in("2000-01-03", -1) else: - st = datetime_to_timestamptz(start) if isinstance(start, datetime) else pg_timestamptz_in(start, -1) - dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else pg_interval_in(duration, -1) - fragments, times, count = temporal_time_split(self._inner, dt, st) + st = datetime_to_timestamptz(start) if isinstance(start, datetime) else timestamptz_in(start, -1) + dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else interval_in(duration, -1) + fragments, times, count = temporal_time_split(self._inner, dt, st, border_inc=True) from ..factory import _TemporalFactory return [_TemporalFactory.create_temporal(fragments[i]) for i in range(count)] @@ -1230,7 +1230,8 @@ def time_split_n(self, n: int) -> list[TG]: """ Returns a list of temporal objects of the same subtype as `self` with the same values as `self` but split in n temporal tiles of equal - duration. + duration. When `self` includes its end timestamp, that timestamp + starts an (n+1)th tile, which holds only the end value. Args: n: An :class:`int` with the number of temporal tiles. @@ -1245,7 +1246,7 @@ def time_split_n(self, n: int) -> list[TG]: return [self] st = temporal_start_timestamptz(self._inner) dt = timedelta_to_interval((self.end_timestamp() - self.start_timestamp()) / n) - fragments, times, count = temporal_time_split(self._inner, dt, st) + fragments, times, count = temporal_time_split(self._inner, dt, st, border_inc=True) from ..factory import _TemporalFactory return [_TemporalFactory.create_temporal(fragments[i]) for i in range(count)] diff --git a/pymeos/temporal/tinstant.py b/pymeos/temporal/tinstant.py index d79ffbce..5bd2f2d6 100644 --- a/pymeos/temporal/tinstant.py +++ b/pymeos/temporal/tinstant.py @@ -44,9 +44,7 @@ def __init__( self._inner = as_tinstant(self.__class__._parse_function(string)) else: ts = ( - datetime_to_timestamptz(timestamp) - if isinstance(timestamp, datetime) - else pg_timestamptz_in(timestamp, -1) + datetime_to_timestamptz(timestamp) if isinstance(timestamp, datetime) else timestamptz_in(timestamp, -1) ) self._inner = self.__class__._make_function(self.__class__._cast_function(value), ts) diff --git a/pymeos/temporal/tsequence.py b/pymeos/temporal/tsequence.py index 77587217..457a367a 100644 --- a/pymeos/temporal/tsequence.py +++ b/pymeos/temporal/tsequence.py @@ -46,10 +46,8 @@ def __init__( (x._inner if isinstance(x, self.ComponentClass) else self.__class__._parse_function(x)) for x in instant_list ] - count = len(self._instants) self._inner = tsequence_make( self._instants, - count, lower_inc, upper_inc, interpolation.value, diff --git a/pymeos/temporal/tsequenceset.py b/pymeos/temporal/tsequenceset.py index e63ffc20..7b2bfc40 100644 --- a/pymeos/temporal/tsequenceset.py +++ b/pymeos/temporal/tsequenceset.py @@ -46,8 +46,7 @@ def __init__( (x._inner if isinstance(x, self.ComponentClass) else self.__class__._parse_function(x)) for x in sequence_list ] - count = len(sequences) - self._inner = tsequenceset_make(sequences, count, normalize) + self._inner = tsequenceset_make(sequences, normalize) @classmethod def from_sequences( diff --git a/pyproject.toml b/pyproject.toml index 50f1774e..90a70b86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ license = { file = 'LICENSE' } requires-python = '>=3.10' dependencies = [ - 'pymeos-cffi >=1.3.0, <2', + 'pymeos-cffi >=1.4.0a1, <2', 'python-dateutil', 'shapely>=2.0.0', ] From 330ac319c361ea44467abff0fcf7fe96339986c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Tue, 20 Oct 2026 13:03:24 +0200 Subject: [PATCH 2/9] Build the all-families MEOS and its catalog in the test workflow The PyMEOS-CFFI cdef covers every optional MEOS family and is projected from the MEOS-API catalog, so the test workflow derives the catalog once with the shared provision-meos action, from the MobilityDB branch the package version maps to, and each test job builds libmeos with every family: through provision-meos on Linux and with -DALL=ON on macOS. PyMEOS-CFFI's header is projected from that catalog and the installed library before it is built. --- .github/workflows/test.yml | 126 +++++++++++++++++++++++++++++-------- 1 file changed, 101 insertions(+), 25 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 76fe5216..8a198df9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,8 +23,51 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + # Derive the MEOS catalog once from the MobilityDB branch the package version + # maps to, through the shared provision-meos action. PyMEOS-CFFI projects its + # cdef header from this catalog, so every test job builds the same surface. + # The catalog parses the headers a libmeos build installs, as PyMEOS-CFFI's + # pr_build does, so it declares no internal header the extension cannot include. + catalog: + name: Derive the MEOS catalog + runs-on: ubuntu-latest + outputs: + meos_branch: ${{ steps.meos_branch.outputs.meos_branch }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Derive MEOS branch from package version + id: meos_branch + shell: bash + run: | + pymeos_version=$(sed -nE 's/^__version__ = "([^"]+)".*/\1/p' pymeos/__init__.py) + major_minor=$(echo "$pymeos_version" | sed -nE 's/^([0-9]+\.[0-9]+).*/\1/p') + candidate="stable-${major_minor}" + if git ls-remote --exit-code https://github.com/MobilityDB/MobilityDB "$candidate" >/dev/null 2>&1; then + meos_branch=$candidate + else + meos_branch="master" + fi + echo "Package version $pymeos_version => MEOS branch $meos_branch" + echo "meos_branch=$meos_branch" >> "$GITHUB_OUTPUT" + + - name: Provision the MEOS catalog + id: provision + uses: MobilityDB/MEOS-API/.github/actions/provision-meos@master + with: + mobilitydb-ref: ${{ steps.meos_branch.outputs.meos_branch }} + build-libmeos: "true" + + - name: Upload the catalog + uses: actions/upload-artifact@v4 + with: + name: meos-idl + path: ${{ steps.provision.outputs.catalog-path }} + test: name: Test PyMEOS - Python ${{ matrix.python-version }} on ${{ matrix.os }} + needs: catalog runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -44,44 +87,69 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Get dependencies from apt (cache) - uses: awalsh128/cache-apt-pkgs-action@v1.4.3 + # The CFFI cdef covers every optional MEOS family, so libmeos is built + # with all of them: on Linux by the provision-meos action, from the same + # MobilityDB branch as the catalog. + - name: Build and install the all-families MEOS (Linux) if: runner.os == 'Linux' + uses: MobilityDB/MEOS-API/.github/actions/provision-meos@master with: - packages: build-essential cmake postgresql-server-dev-16 libproj-dev libjson-c-dev libgsl-dev libgeos-dev - version: 1.1 + mobilitydb-ref: ${{ needs.catalog.outputs.meos_branch }} + build-libmeos: "true" - name: Get dependencies from homebrew (cache) uses: tecolicom/actions-use-homebrew-tools@v1 if: runner.os == 'macOS' with: - tools: cmake libpq proj json-c gsl geos + tools: cmake libpq proj json-c gsl geos h3 - - name: Derive MEOS branch from package version - id: meos_branch - shell: bash + # The raster family links GDAL. Homebrew's GDAL links Apache Arrow for its Arrow and + # Parquet drivers, and pyarrow's bundled Arrow then binds to that copy and fails to load, + # so GDAL is built here without Arrow or Parquet, as MobilityDuck builds its own: core + # drivers only, against Homebrew's PROJ. Its install names are absolute, so libmeos finds + # it without a library path. + - name: Key the GDAL build (macOS) + id: gdal + if: runner.os == 'macOS' run: | - pymeos_version=$(sed -nE 's/^__version__ = "([^"]+)".*/\1/p' pymeos/__init__.py) - major_minor=$(echo "$pymeos_version" | sed -nE 's/^([0-9]+\.[0-9]+).*/\1/p') - candidate="stable-${major_minor}" - if git ls-remote --exit-code https://github.com/MobilityDB/MobilityDB "$candidate" >/dev/null 2>&1; then - meos_branch=$candidate - else - meos_branch="master" - fi - echo "Package version $pymeos_version => MEOS branch $meos_branch" - echo "meos_branch=$meos_branch" >> "$GITHUB_OUTPUT" + echo "version=3.11.4" >> "$GITHUB_OUTPUT" + echo "prefix=$HOME/gdal" >> "$GITHUB_OUTPUT" + echo "proj=$(brew list --versions proj | cut -d' ' -f2)" >> "$GITHUB_OUTPUT" - - name: Fetch MEOS sources + - name: Restore the GDAL build (macOS) + id: gdal-cache + if: runner.os == 'macOS' + uses: actions/cache@v4 + with: + path: ${{ steps.gdal.outputs.prefix }} + key: gdal-${{ steps.gdal.outputs.version }}-proj-${{ steps.gdal.outputs.proj }}-${{ matrix.os }} + + - name: Build GDAL without Arrow (macOS) + if: runner.os == 'macOS' && steps.gdal-cache.outputs.cache-hit != 'true' run: | - git clone --branch ${{ steps.meos_branch.outputs.meos_branch }} --depth 1 https://github.com/MobilityDB/MobilityDB + curl -sSL -o gdal.tar.gz \ + https://github.com/OSGeo/gdal/releases/download/v${{ steps.gdal.outputs.version }}/gdal-${{ steps.gdal.outputs.version }}.tar.gz + tar xzf gdal.tar.gz + cmake -S gdal-${{ steps.gdal.outputs.version }} -B gdal-build -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=${{ steps.gdal.outputs.prefix }} \ + -DCMAKE_INSTALL_NAME_DIR=${{ steps.gdal.outputs.prefix }}/lib \ + -DCMAKE_PREFIX_PATH="$(brew --prefix)" \ + -DBUILD_APPS=OFF -DBUILD_TESTING=OFF -DBUILD_DOCS=OFF -DBUILD_PYTHON_BINDINGS=OFF \ + -DGDAL_BUILD_OPTIONAL_DRIVERS=OFF -DOGR_BUILD_OPTIONAL_DRIVERS=OFF \ + -DGDAL_USE_ARROW=OFF -DGDAL_USE_PARQUET=OFF \ + -DCMAKE_DISABLE_FIND_PACKAGE_Arrow=ON -DCMAKE_DISABLE_FIND_PACKAGE_Parquet=ON + cmake --build gdal-build -j"$(sysctl -n hw.ncpu)" + cmake --install gdal-build - - name: Install MEOS + - name: Build and install the all-families MEOS (macOS) + if: runner.os == 'macOS' run: | + git clone --branch ${{ needs.catalog.outputs.meos_branch }} --depth 1 https://github.com/MobilityDB/MobilityDB mkdir MobilityDB/build cd MobilityDB/build - cmake .. -DMEOS=on -DCMAKE_INSTALL_PREFIX=${{ matrix.ld_prefix }} - make -j + cmake .. -DMEOS=ON -DALL=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${{ matrix.ld_prefix }} \ + -DCMAKE_PREFIX_PATH=${{ steps.gdal.outputs.prefix }} + make -j"$(sysctl -n hw.ncpu)" sudo make install - name: Set up Python ${{ matrix.python-version }} @@ -98,7 +166,13 @@ jobs: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} run: | git clone --branch ${{ env.BRANCH_NAME }} --depth 1 https://github.com/MobilityDB/PyMEOS-CFFI \ - || git clone --branch ${{ steps.meos_branch.outputs.meos_branch }} --depth 1 https://github.com/MobilityDB/PyMEOS-CFFI + || git clone --branch ${{ needs.catalog.outputs.meos_branch }} --depth 1 https://github.com/MobilityDB/PyMEOS-CFFI + + - name: Download the catalog + uses: actions/download-artifact@v4 + with: + name: meos-idl + path: PyMEOS-CFFI/builder - name: Install python dependencies run: | @@ -107,9 +181,11 @@ jobs: pip install -r dev-requirements.txt - name: Install pymeos_cffi + shell: bash run: | cd PyMEOS-CFFI - python ./builder/build_header.py + python ./builder/build_header.py builder/meos-idl.json \ + ${{ matrix.ld_prefix }}/lib/${{ runner.os == 'macOS' && 'libmeos.dylib' || 'libmeos.so' }} builder/meos.h python ./builder/build_pymeos_functions.py pip install . From 06c2d4ebe513c95f4001c4b69f36ff83fbfafcd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Tue, 20 Oct 2026 13:03:24 +0200 Subject: [PATCH 3/9] Align the test expectations with MEOS master MF-JSON writes the full ISO 8601 offset (+00:00) and states the CRS of a geography; the temporal hash values are those temporal_hash answers; the split tests expect the tile that holds a value on the upper border; and the min and max restrictions, which MobilityDB defines for tint, tbigint, tfloat and ttext, raise for tbool and the temporal points. The adjacent instant of the tint topological test takes the value 2 at the same timestamp: two instants at different timestamps are apart in time, so their boxes are not adjacent. --- tests/main/tbool_test.py | 103 ++++++-------------------- tests/main/tfloat_test.py | 68 +++++++++--------- tests/main/tgeogpoint_test.py | 84 ++++++++++++++-------- tests/main/tgeompoint_test.py | 132 +++++++++------------------------- tests/main/tint_test.py | 50 ++++++------- tests/main/ttext_test.py | 36 +++++----- 6 files changed, 194 insertions(+), 279 deletions(-) diff --git a/tests/main/tbool_test.py b/tests/main/tbool_test.py index f91b1997..d71d658e 100644 --- a/tests/main/tbool_test.py +++ b/tests/main/tbool_test.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta, timezone import pytest -from pymeos_cffi import MeosException +from pymeos_cffi import MeosException, MeosInvalidArgTypeError from pymeos import ( TBool, @@ -401,8 +401,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingBoolean",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-01T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-01T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -410,7 +410,7 @@ def test_as_hexwkb(self, temporal, expected): " true\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00"\n' " ],\n" ' "interpolation": "None"\n' "}", @@ -420,8 +420,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingBoolean",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -430,8 +430,8 @@ def test_as_hexwkb(self, temporal, expected): " false\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -443,8 +443,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingBoolean",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -453,8 +453,8 @@ def test_as_hexwkb(self, temporal, expected): " false\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -466,8 +466,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingBoolean",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-05T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-05T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -478,8 +478,8 @@ def test_as_hexwkb(self, temporal, expected): " false\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -490,8 +490,8 @@ def test_as_hexwkb(self, temporal, expected): " true\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-03T00:00:00+00",\n' - ' "2019-09-05T00:00:00+00"\n' + ' "2019-09-03T00:00:00+00:00",\n' + ' "2019-09-05T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -878,7 +878,7 @@ def test_segments(self, temporal, expected): @pytest.mark.parametrize( "temporal, expected", - [(tbi, 440045287), (tbds, 2385901957), (tbs, 2385901957), (tbss, 1543175996)], + [(tbi, 440045318), (tbds, 3614979951), (tbs, 3614979951), (tbss, 1190925880)], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) def test_hash(self, temporal, expected): @@ -1546,35 +1546,6 @@ def test_when_false(self, temporal, expected): def test_at(self, temporal, restrictor, expected): assert temporal.at(restrictor) == expected - @pytest.mark.parametrize( - "temporal, expected", - [ - (tbi, TBoolInst("True@2019-09-01")), - (tbds, TBoolSeq("{True@2019-09-01}")), - (tbs, TBoolSeq("[True@2019-09-01, True@2019-09-02)")), - ( - tbss, - TBoolSeqSet("{[True@2019-09-01, True@2019-09-02),[True@2019-09-03, True@2019-09-05]}"), - ), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_at_max(self, temporal, expected): - assert temporal.at_max() == expected - - @pytest.mark.parametrize( - "temporal, expected", - [ - (tbi, TBoolInst("True@2019-09-01")), - (tbds, TBoolSeq("{False@2019-09-02}")), - (tbs, TBoolSeq("[False@2019-09-02]")), - (tbss, TBoolSeqSet("{[False@2019-09-02]}")), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_at_min(self, temporal, expected): - assert temporal.at_min() == expected - @pytest.mark.parametrize( "temporal, restrictor, expected", [ @@ -1645,35 +1616,6 @@ def test_at_min(self, temporal, expected): def test_minus(self, temporal, restrictor, expected): assert temporal.minus(restrictor) == expected - @pytest.mark.parametrize( - "temporal, expected", - [ - (tbi, None), - (tbds, TBoolSeq("{False@2019-09-02}")), - (tbs, TBoolSeq("[False@2019-09-02]")), - (tbss, TBoolSeqSet("{[False@2019-09-02]}")), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_minus_max(self, temporal, expected): - assert temporal.minus_max() == expected - - @pytest.mark.parametrize( - "temporal, expected", - [ - (tbi, None), - (tbds, TBoolSeq("{True@2019-09-01}")), - (tbs, TBoolSeq("[True@2019-09-01, True@2019-09-02)")), - ( - tbss, - TBoolSeqSet("{[True@2019-09-01, True@2019-09-02),[True@2019-09-03, True@2019-09-05]}"), - ), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_minus_min(self, temporal, expected): - assert temporal.minus_min() == expected - @pytest.mark.parametrize( "temporal, restrictor", [ @@ -1737,9 +1679,10 @@ def test_at_minus(self, temporal, restrictor): [tbi, tbds, tbs, tbss], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) - def test_at_minus_min_max(self, temporal): - assert TBool.from_merge(temporal.at_min(), temporal.minus_min()) == temporal - assert TBool.from_merge(temporal.at_max(), temporal.minus_max()) == temporal + def test_min_max_restrictors_raise(self, temporal): + for restrict in (temporal.at_min, temporal.at_max, temporal.minus_min, temporal.minus_max): + with pytest.raises(MeosInvalidArgTypeError): + restrict() class TestTBoolComparisons(TestTBool): diff --git a/tests/main/tfloat_test.py b/tests/main/tfloat_test.py index fe53546c..fd12b4d0 100644 --- a/tests/main/tfloat_test.py +++ b/tests/main/tfloat_test.py @@ -537,8 +537,8 @@ def test_as_hexwkb(self, temporal, expected): " 1.5\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-01T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-01T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -546,7 +546,7 @@ def test_as_hexwkb(self, temporal, expected): " 1.5\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00"\n' " ],\n" ' "interpolation": "None"\n' "}", @@ -560,8 +560,8 @@ def test_as_hexwkb(self, temporal, expected): " 2.5\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -570,8 +570,8 @@ def test_as_hexwkb(self, temporal, expected): " 2.5\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -587,8 +587,8 @@ def test_as_hexwkb(self, temporal, expected): " 2.5\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -597,8 +597,8 @@ def test_as_hexwkb(self, temporal, expected): " 2.5\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -614,8 +614,8 @@ def test_as_hexwkb(self, temporal, expected): " 2.5\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-05T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-05T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -626,8 +626,8 @@ def test_as_hexwkb(self, temporal, expected): " 2.5\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -638,8 +638,8 @@ def test_as_hexwkb(self, temporal, expected): " 1.5\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-03T00:00:00+00",\n' - ' "2019-09-05T00:00:00+00"\n' + ' "2019-09-03T00:00:00+00:00",\n' + ' "2019-09-05T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -1094,12 +1094,12 @@ def test_segments(self, temporal, expected): @pytest.mark.parametrize( "temporal, expected", [ - (tfi, 1307112078), - (tfds, 1935376725), - (tfs, 1935376725), - (tfss, 4247071962), - (tfs, 1935376725), - (tfss, 4247071962), + (tfi, 1307112109), + (tfds, 3164454719), + (tfs, 3164454719), + (tfss, 1518183798), + (tfs, 3164454719), + (tfss, 1518183798), ], ids=[ "Instant", @@ -2972,8 +2972,9 @@ def test_value_split(self, temporal, expected): ( tfss, [ - TFloatSeq("[1@2019-09-01,2@2019-09-02]"), - TFloatSeq("[1@2019-09-03, 1@2019-09-05]"), + TFloatSeqSet("{[1@2019-09-01, 2@2019-09-02]}"), + TFloatSeqSet("{[1@2019-09-03, 1@2019-09-05)}"), + TFloatSeqSet("{[1@2019-09-05]}"), ], ), ], @@ -2990,15 +2991,17 @@ def test_time_split(self, temporal, expected): ( tfs, [ - TFloatSeq("[1@2019-09-01, 1.5@2019-09-01 12:00:00+00)"), - TFloatSeq("[1.5@2019-09-01 12:00:00+00, 2@2019-09-02]"), + TFloatSeq("[1@2019-09-01, 1.5@2019-09-01 12:00)"), + TFloatSeq("[1.5@2019-09-01 12:00, 2@2019-09-02)"), + TFloatSeq("[2@2019-09-02]"), ], ), ( tfss, [ - TFloatSeq("[1@2019-09-01,2@2019-09-02]"), - TFloatSeq("[1@2019-09-03, 1@2019-09-05]"), + TFloatSeqSet("{[1@2019-09-01, 2@2019-09-02]}"), + TFloatSeqSet("{[1@2019-09-03, 1@2019-09-05)}"), + TFloatSeqSet("{[1@2019-09-05]}"), ], ), ], @@ -3022,9 +3025,10 @@ def test_time_split_n(self, temporal, expected): ( tfss, [ - TFloatSeq("{[1@2019-09-01, 2@2019-09-02)}"), - TFloatSeq("{[1@2019-09-03, 1@2019-09-05]}"), - TFloatSeq("{[2@2019-09-02]}"), + TFloatSeqSet("{[1@2019-09-01, 2@2019-09-02)}"), + TFloatSeqSet("{[1@2019-09-03, 1@2019-09-05)}"), + TFloatSeqSet("{[1@2019-09-05]}"), + TFloatSeqSet("{[2@2019-09-02]}"), ], ), ], diff --git a/tests/main/tgeogpoint_test.py b/tests/main/tgeogpoint_test.py index d97b7699..962f5db1 100644 --- a/tests/main/tgeogpoint_test.py +++ b/tests/main/tgeogpoint_test.py @@ -5,7 +5,7 @@ import numpy as np import pytest import shapely.geometry -from pymeos_cffi import MeosException, MeosInvalidArgValueError +from pymeos_cffi import MeosException, MeosInvalidArgTypeError, MeosInvalidArgValueError from shapely import LineString, Point, set_srid from pymeos import ( @@ -578,6 +578,12 @@ def test_as_hexwkb(self, temporal, expected): tpi, "{\n" ' "type": "MovingPoint",\n' + ' "crs": {\n' + ' "type": "Name",\n' + ' "properties": {\n' + ' "name": "EPSG:4326"\n' + " }\n" + " },\n" ' "bbox": [\n' " [\n" " 1,\n" @@ -589,8 +595,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-01T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-01T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -601,7 +607,7 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00"\n' " ],\n" ' "interpolation": "None"\n' "}", @@ -610,6 +616,12 @@ def test_as_hexwkb(self, temporal, expected): tpds, "{\n" ' "type": "MovingPoint",\n' + ' "crs": {\n' + ' "type": "Name",\n' + ' "properties": {\n' + ' "name": "EPSG:4326"\n' + " }\n" + " },\n" ' "bbox": [\n' " [\n" " 1,\n" @@ -621,8 +633,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -637,8 +649,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -649,6 +661,12 @@ def test_as_hexwkb(self, temporal, expected): tps, "{\n" ' "type": "MovingPoint",\n' + ' "crs": {\n' + ' "type": "Name",\n' + ' "properties": {\n' + ' "name": "EPSG:4326"\n' + " }\n" + " },\n" ' "bbox": [\n' " [\n" " 1,\n" @@ -660,8 +678,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -676,8 +694,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -688,6 +706,12 @@ def test_as_hexwkb(self, temporal, expected): tpss, "{\n" ' "type": "MovingPoint",\n' + ' "crs": {\n' + ' "type": "Name",\n' + ' "properties": {\n' + ' "name": "EPSG:4326"\n' + " }\n" + " },\n" ' "bbox": [\n' " [\n" " 1,\n" @@ -699,8 +723,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-05T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-05T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -717,8 +741,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -735,8 +759,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-03T00:00:00+00",\n' - ' "2019-09-05T00:00:00+00"\n' + ' "2019-09-03T00:00:00+00:00",\n' + ' "2019-09-05T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -1126,7 +1150,7 @@ def test_segments(self, temporal, expected): @pytest.mark.parametrize( "temporal, expected", - [(tpi, 1181779687), (tpds, 1545137628), (tps, 1545137628), (tpss, 1008965061)], + [(tpi, 1181779718), (tpds, 2774215622), (tps, 2774215622), (tpss, 429936847)], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) def test_hash(self, temporal, expected): @@ -2377,9 +2401,10 @@ def test_at_minus(self, temporal, restrictor): [tpi, tpds, tps, tpss], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) - def test_at_minus_min_max(self, temporal): - assert TGeogPoint.from_merge(temporal.at_min(), temporal.minus_min()) == temporal - assert TGeogPoint.from_merge(temporal.at_max(), temporal.minus_max()) == temporal + def test_min_max_restrictors_raise(self, temporal): + for restrict in (temporal.at_min, temporal.at_max, temporal.minus_min, temporal.minus_max): + with pytest.raises(MeosInvalidArgTypeError): + restrict() class TestTGeogPointEverSpatialOperations(TestTGeogPoint): @@ -2612,8 +2637,9 @@ class TestTGeogPointSplitOperations(TestTGeogPoint): ( tpss, [ - TGeogPointSeq("[Point(1 1)@2019-09-01,Point(2 2)@2019-09-02]"), - TGeogPointSeq("[Point(1 1)@2019-09-03, Point(1 1)@2019-09-05]"), + TGeogPointSeqSet("{[POINT(1 1)@2019-09-01, POINT(2 2)@2019-09-02]}"), + TGeogPointSeqSet("{[POINT(1 1)@2019-09-03, POINT(1 1)@2019-09-05)}"), + TGeogPointSeqSet("{[POINT(1 1)@2019-09-05]}"), ], ), ], @@ -2636,15 +2662,17 @@ def test_time_split(self, temporal, expected): ( tps, [ - TGeogPointSeq("[Point(1 1)@2019-09-01, Point(1.5 1.5)@2019-09-01 12:00:00+00)"), - TGeogPointSeq("[Point(1.5 1.5)@2019-09-01 12:00:00+00, Point(2 2)@2019-09-02]"), + TGeogPointSeq("[POINT(1 1)@2019-09-01, POINT(1.5 1.5)@2019-09-01 12:00)"), + TGeogPointSeq("[POINT(1.5 1.5)@2019-09-01 12:00, POINT(2 2)@2019-09-02)"), + TGeogPointSeq("[POINT(2 2)@2019-09-02]"), ], ), ( tpss, [ - TGeogPointSeq("[Point(1 1)@2019-09-01,Point(2 2)@2019-09-02]"), - TGeogPointSeq("[Point(1 1)@2019-09-03, Point(1 1)@2019-09-05]"), + TGeogPointSeqSet("{[POINT(1 1)@2019-09-01, POINT(2 2)@2019-09-02]}"), + TGeogPointSeqSet("{[POINT(1 1)@2019-09-03, POINT(1 1)@2019-09-05)}"), + TGeogPointSeqSet("{[POINT(1 1)@2019-09-05]}"), ], ), ], diff --git a/tests/main/tgeompoint_test.py b/tests/main/tgeompoint_test.py index 54144dc7..04b73c17 100644 --- a/tests/main/tgeompoint_test.py +++ b/tests/main/tgeompoint_test.py @@ -5,7 +5,7 @@ import numpy as np import pytest -from pymeos_cffi import MeosException, MeosInvalidArgValueError +from pymeos_cffi import MeosException, MeosInvalidArgTypeError, MeosInvalidArgValueError from shapely import GeometryCollection, LineString, MultiPoint, Point, Polygon from pymeos import ( @@ -608,8 +608,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-01T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-01T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -620,7 +620,7 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00"\n' " ],\n" ' "interpolation": "None"\n' "}", @@ -640,8 +640,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -656,8 +656,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -679,8 +679,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -695,8 +695,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -718,8 +718,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-05T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-05T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -736,8 +736,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -754,8 +754,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-03T00:00:00+00",\n' - ' "2019-09-05T00:00:00+00"\n' + ' "2019-09-03T00:00:00+00:00",\n' + ' "2019-09-05T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -1152,10 +1152,10 @@ def test_segments(self, temporal, expected): @pytest.mark.parametrize( "temporal, expected", [ - (tpi, 382694564), - (tpds, 1664033448), - (tps, 1664033448), - (tpss, 2878566103), + (tpi, 382694595), + (tpds, 2893111442), + (tps, 2893111442), + (tpss, 1203500793), ], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) @@ -2272,35 +2272,6 @@ class TestTGeomPointRestrictors(TestTGeomPoint): def test_at(self, temporal, restrictor, expected): assert temporal.at(restrictor) == expected - @pytest.mark.parametrize( - "temporal, expected", - [ - (tpi, TGeomPointSeqSet("{[Point(1 1)@2019-09-01]}")), - (tpds, TGeomPointSeqSet("{[Point(1 1)@2019-09-01]}")), - (tps, TGeomPointSeqSet("{[Point(1 1)@2019-09-01]}")), - ( - tpss, - TGeomPointSeqSet("{[Point(1 1)@2019-09-01],[Point(1 1)@2019-09-03,Point(1 1)@2019-09-05]}"), - ), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_at_min(self, temporal, expected): - assert temporal.at_min() == expected - - @pytest.mark.parametrize( - "temporal, expected", - [ - (tpi, TGeomPointSeqSet("{[Point(1 1)@2019-09-01]}")), - (tpds, TGeomPointSeqSet("{[Point(2 2)@2019-09-02]}")), - (tps, TGeomPointSeqSet("{[Point(2 2)@2019-09-02]}")), - (tpss, TGeomPointSeqSet("{[Point(2 2)@2019-09-02]}")), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_at_max(self, temporal, expected): - assert temporal.at_max() == expected - @pytest.mark.parametrize( "temporal, restrictor, expected", [ @@ -2401,43 +2372,6 @@ def test_at_max(self, temporal, expected): def test_minus(self, temporal, restrictor, expected): assert temporal.minus(restrictor) == expected - @pytest.mark.parametrize( - "temporal, expected", - [ - (tpi, None), - (tpds, TGeomPointSeqSet("{[Point(2 2)@2019-09-02]}")), - (tps, TGeomPointSeqSet("{(Point(1 1)@2019-09-01,Point(2 2)@2019-09-02]}")), - (tpss, TGeomPointSeqSet("{(Point(1 1)@2019-09-01,Point(2 2)@2019-09-02]}")), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_minus_min(self, temporal, expected): - if expected is None: - assert temporal.minus_min() is None - else: - assert temporal.minus_min() == expected - - @pytest.mark.parametrize( - "temporal, expected", - [ - (tpi, None), - (tpds, TGeomPointSeqSet("{[Point(1 1)@2019-09-01]}")), - (tps, TGeomPointSeqSet("{[Point(1 1)@2019-09-01,Point(2 2)@2019-09-02)}")), - ( - tpss, - TGeomPointSeqSet( - "{[Point(1 1)@2019-09-01,Point(2 2)@2019-09-02),[Point(1 1)@2019-09-03,Point(1 1)@2019-09-05]}" - ), - ), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_minus_max(self, temporal, expected): - if expected is None: - assert temporal.minus_max() is None - else: - assert temporal.minus_max() == expected - @pytest.mark.parametrize( "temporal, restrictor", [ @@ -2501,9 +2435,10 @@ def test_at_minus(self, temporal, restrictor): [tpi, tpds, tps, tpss], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) - def test_at_minus_min_max(self, temporal): - assert TGeomPoint.from_merge(temporal.at_min(), temporal.minus_min()) == temporal - assert TGeomPoint.from_merge(temporal.at_max(), temporal.minus_max()) == temporal + def test_min_max_restrictors_raise(self, temporal): + for restrict in (temporal.at_min, temporal.at_max, temporal.minus_min, temporal.minus_max): + with pytest.raises(MeosInvalidArgTypeError): + restrict() class TestTGeomPointTopologicalFunctions(TestTGeomPoint): @@ -3282,8 +3217,9 @@ class TestTGeomPointSplitOperations(TestTGeomPoint): ( tpss, [ - TGeomPointSeq("[Point(1 1)@2019-09-01,Point(2 2)@2019-09-02]"), - TGeomPointSeq("[Point(1 1)@2019-09-03, Point(1 1)@2019-09-05]"), + TGeomPointSeqSet("{[POINT(1 1)@2019-09-01, POINT(2 2)@2019-09-02]}"), + TGeomPointSeqSet("{[POINT(1 1)@2019-09-03, POINT(1 1)@2019-09-05)}"), + TGeomPointSeqSet("{[POINT(1 1)@2019-09-05]}"), ], ), ], @@ -3306,15 +3242,17 @@ def test_time_split(self, temporal, expected): ( tps, [ - TGeomPointSeq("[Point(1 1)@2019-09-01, Point(1.5 1.5)@2019-09-01 12:00:00+00)"), - TGeomPointSeq("[Point(1.5 1.5)@2019-09-01 12:00:00+00, Point(2 2)@2019-09-02]"), + TGeomPointSeq("[POINT(1 1)@2019-09-01, POINT(1.5 1.5)@2019-09-01 12:00)"), + TGeomPointSeq("[POINT(1.5 1.5)@2019-09-01 12:00, POINT(2 2)@2019-09-02)"), + TGeomPointSeq("[POINT(2 2)@2019-09-02]"), ], ), ( tpss, [ - TGeomPointSeq("[Point(1 1)@2019-09-01,Point(2 2)@2019-09-02]"), - TGeomPointSeq("[Point(1 1)@2019-09-03, Point(1 1)@2019-09-05]"), + TGeomPointSeqSet("{[POINT(1 1)@2019-09-01, POINT(2 2)@2019-09-02]}"), + TGeomPointSeqSet("{[POINT(1 1)@2019-09-03, POINT(1 1)@2019-09-05)}"), + TGeomPointSeqSet("{[POINT(1 1)@2019-09-05]}"), ], ), ], diff --git a/tests/main/tint_test.py b/tests/main/tint_test.py index c52ebfd7..28c51eed 100644 --- a/tests/main/tint_test.py +++ b/tests/main/tint_test.py @@ -440,8 +440,8 @@ def test_as_hexwkb(self, temporal, expected): " 1\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-01T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-01T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -449,7 +449,7 @@ def test_as_hexwkb(self, temporal, expected): " 1\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00"\n' " ],\n" ' "interpolation": "None"\n' "}", @@ -463,8 +463,8 @@ def test_as_hexwkb(self, temporal, expected): " 2\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -473,8 +473,8 @@ def test_as_hexwkb(self, temporal, expected): " 2\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -490,8 +490,8 @@ def test_as_hexwkb(self, temporal, expected): " 2\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -500,8 +500,8 @@ def test_as_hexwkb(self, temporal, expected): " 2\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -517,8 +517,8 @@ def test_as_hexwkb(self, temporal, expected): " 2\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-05T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-05T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -529,8 +529,8 @@ def test_as_hexwkb(self, temporal, expected): " 2\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -541,8 +541,8 @@ def test_as_hexwkb(self, temporal, expected): " 1\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-03T00:00:00+00",\n' - ' "2019-09-05T00:00:00+00"\n' + ' "2019-09-03T00:00:00+00:00",\n' + ' "2019-09-05T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -967,7 +967,7 @@ def test_segments(self, temporal, expected): @pytest.mark.parametrize( "temporal, expected", - [(tii, 440045287), (tids, 3589664982), (tis, 3589664982), (tiss, 205124107)], + [(tii, 440045318), (tids, 523775680), (tis, 523775680), (tiss, 2660990281)], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) def test_hash(self, temporal, expected): @@ -2281,7 +2281,7 @@ class TestTIntTopologicalFunctions(TestTInt): "temporal, argument, expected", [ (tii, TIntInst("1@2019-09-02"), False), - (tii, TIntInst("2@2019-09-02"), True), + (tii, TIntInst("2@2019-09-01"), True), (tids, TIntInst("1@2019-09-03"), False), (tids, TIntInst("3@2019-09-02"), True), (tis, TIntInst("1@2019-09-03"), False), @@ -2658,8 +2658,9 @@ def test_value_split(self, temporal, expected): ( tiss, [ - TIntSeq("[1@2019-09-01,2@2019-09-02]"), - TIntSeq("[1@2019-09-03, 1@2019-09-05]"), + TIntSeqSet("{[1@2019-09-01, 2@2019-09-02]}"), + TIntSeqSet("{[1@2019-09-03, 1@2019-09-05)}"), + TIntSeqSet("{[1@2019-09-05]}"), ], ), ], @@ -2677,9 +2678,10 @@ def test_time_split(self, temporal, expected): ( tiss, [ - TIntSeq("{[1@2019-09-01, 1@2019-09-02)}"), - TIntSeq("{[1@2019-09-03, 1@2019-09-05]}"), - TIntSeq("{[2@2019-09-02]}"), + TIntSeqSet("{[1@2019-09-01, 1@2019-09-02)}"), + TIntSeqSet("{[1@2019-09-03, 1@2019-09-05)}"), + TIntSeqSet("{[1@2019-09-05]}"), + TIntSeqSet("{[2@2019-09-02]}"), ], ), ], diff --git a/tests/main/ttext_test.py b/tests/main/ttext_test.py index f4437e53..f63d1a16 100644 --- a/tests/main/ttext_test.py +++ b/tests/main/ttext_test.py @@ -410,8 +410,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingText",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-01T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-01T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -419,7 +419,7 @@ def test_as_hexwkb(self, temporal, expected): ' "AAA"\n' " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00"\n' " ],\n" ' "interpolation": "None"\n' "}", @@ -429,8 +429,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingText",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -439,8 +439,8 @@ def test_as_hexwkb(self, temporal, expected): ' "BBB"\n' " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -452,8 +452,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingText",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -462,8 +462,8 @@ def test_as_hexwkb(self, temporal, expected): ' "BBB"\n' " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -475,8 +475,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingText",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-05T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-05T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -487,8 +487,8 @@ def test_as_hexwkb(self, temporal, expected): ' "BBB"\n' " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -499,8 +499,8 @@ def test_as_hexwkb(self, temporal, expected): ' "AAA"\n' " ],\n" ' "datetimes": [\n' - ' "2019-09-03T00:00:00+00",\n' - ' "2019-09-05T00:00:00+00"\n' + ' "2019-09-03T00:00:00+00:00",\n' + ' "2019-09-05T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -890,7 +890,7 @@ def test_segments(self, temporal, expected): @pytest.mark.parametrize( "temporal, expected", - [(tti, 1893808825), (ttds, 1223816819), (tts, 1223816819), (ttss, 2199213310)], + [(tti, 1893808856), (ttds, 2452894813), (tts, 2452894813), (ttss, 3439543446)], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) def test_hash(self, temporal, expected): From 5f33b26ceb35af7336d82a758c0242584ede6f1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Tue, 20 Oct 2026 13:03:24 +0200 Subject: [PATCH 4/9] Test the space tiles and the value-time tiles STBox.tile of a trip's box answers the 5 km space tiles of the trip, with their space dimension, and TGeomPoint.tile answers the trip's pieces in them. TBox.tile answers the value-time tiles in value order within each day, covering the box's last day and no value band beyond its upper value. --- tests/boxes/stbox_test.py | 23 +++++++++++++++++++++++ tests/boxes/tbox_test.py | 12 ++++++++++++ 2 files changed, 35 insertions(+) diff --git a/tests/boxes/stbox_test.py b/tests/boxes/stbox_test.py index bcc676d9..9e7d034d 100644 --- a/tests/boxes/stbox_test.py +++ b/tests/boxes/stbox_test.py @@ -1261,3 +1261,26 @@ def test_gt(self): def test_ge(self): _ = self.stbxt >= self.other + + +class TestSTBoxTiling(TestSTBox): + # Mirrors #test_time_split of the temporal split tests, on the space tiles + trip = TGeomPointSeq( + "SRID=3857;[Point(481000 6586000)@2020-06-01 08:00:00+00, Point(487000 6588000)@2020-06-01 08:30:00+00]" + ) + + # The space tiles of the box, as #test_time_split reads the time bins + def test_space_tiles(self): + tiles = STBox.from_tpoint(self.trip).space_tiles(5e3, 0, 0, Point(0, 0), True) + assert [str(tile) for tile in tiles] == [ + "SRID=3857;STBOX X((480000,6585000),(485000,6590000))", + "SRID=3857;STBOX X((485000,6585000),(490000,6590000))", + ] + + # The fragments of the trip in those tiles, as #test_time_split reads the time fragments + def test_tpoint_space_split(self): + pieces = self.trip.space_split(5e3) + assert [str(piece) for piece in pieces] == [ + "{[POINT(481000 6586000)@2020-06-01 08:00:00+00, POINT(485000 6587333.333333333)@2020-06-01 08:20:00+00)}", + "{[POINT(485000 6587333.333333333)@2020-06-01 08:20:00+00, POINT(487000 6588000)@2020-06-01 08:30:00+00]}", + ] diff --git a/tests/boxes/tbox_test.py b/tests/boxes/tbox_test.py index c25a86e2..1ec4edc9 100644 --- a/tests/boxes/tbox_test.py +++ b/tests/boxes/tbox_test.py @@ -1123,3 +1123,15 @@ def test_gt(self): def test_ge(self): _ = self.tbfxt >= self.other + + +class TestTBoxTiling(TestTBox): + # Mirrors #test_value_time_split of the temporal split tests, on the value-time tiles + def test_tile(self): + box = TBox("TBOXFLOAT XT([0, 33.35670867892772],[2020-06-01 08:00:15.533+02, 2020-06-06 01:53:54.08198+02])") + tiles = box.tile(10, "1 day", 0.0, "2020-06-01 00:00:00+02") + assert tiles == [ + TBox(f"TBOXFLOAT XT([{v}, {v + 10}),[2020-06-0{d} 00:00:00+02, 2020-06-0{d + 1} 00:00:00+02))") + for d in range(1, 7) + for v in (0, 10, 20, 30) + ] From e1e6912721e5d9a8e3cb84be71a0bfa922bb26ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Wed, 21 Oct 2026 01:37:36 +0200 Subject: [PATCH 5/9] Test every aggregator on two overlapping inputs tests/aggregators runs each aggregator of pymeos.aggregators on two inputs that overlap in time, so every result holds a stretch where both contribute and a stretch where one does: the boolean, number and text temporal aggregates, the three extents, the instant and period counts, the instantaneous time union, and an incremental aggregation against the one-call form. Witness: on PyMEOS-CFFI regenerated from MobilityDB master 1eca005aeb the 18 cases pass, and 16 of them fail on the surface of master adbb2ebad1, where each aggregation starts from a None state the transition wrapper casts unguarded. Measured: each expected value checks by its definition on the two inputs, the integer sum being 1, then 1 + 2 = 3 from 2000-01-02, then 3 + 2 = 5 at 2000-01-03, then 2 to 2000-01-04. The suite passes 4471 of 4471. --- tests/aggregators/__init__.py | 0 tests/aggregators/aggregators_test.py | 170 ++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 tests/aggregators/__init__.py create mode 100644 tests/aggregators/aggregators_test.py diff --git a/tests/aggregators/__init__.py b/tests/aggregators/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/aggregators/aggregators_test.py b/tests/aggregators/aggregators_test.py new file mode 100644 index 00000000..b2967997 --- /dev/null +++ b/tests/aggregators/aggregators_test.py @@ -0,0 +1,170 @@ +import pytest + +from pymeos import ( + STBox, + TBoolSeq, + TBoolSeqSet, + TBox, + TFloatSeq, + TFloatSeqSet, + TGeomPointSeq, + TIntSeq, + TIntSeqSet, + TsTzSet, + TsTzSpan, + TTextSeq, + TTextSeqSet, +) +from pymeos.aggregators import ( + TemporalAndAggregator, + TemporalAverageAggregator, + TemporalExtentAggregator, + TemporalFloatMaxAggregator, + TemporalFloatMinAggregator, + TemporalFloatSumAggregator, + TemporalInstantCountAggregator, + TemporalIntMaxAggregator, + TemporalIntMinAggregator, + TemporalIntSumAggregator, + TemporalNumberExtentAggregator, + TemporalOrAggregator, + TemporalPeriodCountAggregator, + TemporalPointExtentAggregator, + TemporalTextMaxAggregator, + TemporalTextMinAggregator, + TimeInstantaneousUnionAggregator, +) +from tests.conftest import TestPyMEOS + + +class TestAggregators(TestPyMEOS): + """Every aggregator on two overlapping inputs, laid out as #TestTBoxConstructors is.""" + + ints = [TIntSeq("[1@2000-01-01, 3@2000-01-03]"), TIntSeq("[2@2000-01-02, 2@2000-01-04]")] + floats = [TFloatSeq("[1@2000-01-01, 3@2000-01-03]"), TFloatSeq("[2@2000-01-02, 2@2000-01-04]")] + bools = [TBoolSeq("[t@2000-01-01, f@2000-01-03]"), TBoolSeq("[t@2000-01-02, t@2000-01-04]")] + texts = [TTextSeq('["a"@2000-01-01, "c"@2000-01-03]'), TTextSeq('["b"@2000-01-02, "b"@2000-01-04]')] + points = [ + TGeomPointSeq("[POINT(0 0)@2000-01-01, POINT(2 2)@2000-01-03]"), + TGeomPointSeq("[POINT(1 3)@2000-01-02, POINT(1 3)@2000-01-04]"), + ] + spans = [TsTzSpan("[2000-01-01, 2000-01-03]"), TsTzSpan("[2000-01-02, 2000-01-04]")] + sets = [TsTzSet("{2000-01-01, 2000-01-03}"), TsTzSet("{2000-01-02, 2000-01-03}")] + + @pytest.mark.parametrize( + "aggregator, inputs, expected", + [ + ( + TemporalAndAggregator, + bools, + TBoolSeqSet("{[t@2000-01-01, f@2000-01-03], (t@2000-01-03, t@2000-01-04]}"), + ), + (TemporalOrAggregator, bools, TBoolSeqSet("{[t@2000-01-01, t@2000-01-04]}")), + ( + TemporalIntMaxAggregator, + ints, + TIntSeqSet("{[1@2000-01-01, 2@2000-01-02, 3@2000-01-03], (2@2000-01-03, 2@2000-01-04]}"), + ), + ( + TemporalIntMinAggregator, + ints, + TIntSeqSet("{[1@2000-01-01, 2@2000-01-03, 2@2000-01-04]}"), + ), + ( + TemporalIntSumAggregator, + ints, + TIntSeqSet("{[1@2000-01-01, 3@2000-01-02, 5@2000-01-03], (2@2000-01-03, 2@2000-01-04]}"), + ), + ( + TemporalFloatMaxAggregator, + floats, + TFloatSeqSet("{[1@2000-01-01, 3@2000-01-03], (2@2000-01-03, 2@2000-01-04]}"), + ), + ( + TemporalFloatMinAggregator, + floats, + TFloatSeqSet("{[1@2000-01-01, 2@2000-01-02, 2@2000-01-04]}"), + ), + ( + TemporalFloatSumAggregator, + floats, + TFloatSeqSet( + "{[1@2000-01-01, 2@2000-01-02), [4@2000-01-02, 5@2000-01-03], (2@2000-01-03, 2@2000-01-04]}" + ), + ), + ( + TemporalAverageAggregator, + floats, + TFloatSeqSet("{[1@2000-01-01, 2@2000-01-02, 2.5@2000-01-03], (2@2000-01-03, 2@2000-01-04]}"), + ), + ( + TemporalTextMaxAggregator, + texts, + TTextSeqSet('{["a"@2000-01-01, "b"@2000-01-02, "c"@2000-01-03], ("b"@2000-01-03, "b"@2000-01-04]}'), + ), + ( + TemporalTextMinAggregator, + texts, + TTextSeqSet('{["a"@2000-01-01, "b"@2000-01-03, "b"@2000-01-04]}'), + ), + ( + TemporalInstantCountAggregator, + sets, + TIntSeq("{1@2000-01-01, 1@2000-01-02, 2@2000-01-03}"), + ), + ( + TemporalPeriodCountAggregator, + spans, + TIntSeqSet("{[1@2000-01-01, 2@2000-01-02, 2@2000-01-03], (1@2000-01-03, 1@2000-01-04]}"), + ), + ( + TimeInstantaneousUnionAggregator, + sets, + TsTzSet("{2000-01-01, 2000-01-02, 2000-01-03}"), + ), + ], + ids=[ + "And", + "Or", + "IntMax", + "IntMin", + "IntSum", + "FloatMax", + "FloatMin", + "FloatSum", + "Average", + "TextMax", + "TextMin", + "InstantCount", + "PeriodCount", + "InstantaneousUnion", + ], + ) + def test_aggregate(self, aggregator, inputs, expected): + assert aggregator.aggregate(inputs) == expected + + @pytest.mark.parametrize( + "aggregator, inputs, expected", + [ + ( + TemporalNumberExtentAggregator, + floats, + TBox("TBOXFLOAT XT([1, 3],[2000-01-01, 2000-01-04])"), + ), + ( + TemporalPointExtentAggregator, + points, + STBox("STBOX XT(((0,0),(2,3)),[2000-01-01, 2000-01-04])"), + ), + (TemporalExtentAggregator, ints, TsTzSpan("[2000-01-01, 2000-01-04]")), + ], + ids=["NumberExtent", "PointExtent", "TimeExtent"], + ) + def test_extent(self, aggregator, inputs, expected): + assert aggregator.aggregate(inputs) == expected + + def test_incremental_aggregation(self): + aggregation = TemporalIntSumAggregator.start_aggregation() + for temporal in self.ints: + aggregation.add(temporal) + assert aggregation.aggregation() == TemporalIntSumAggregator.aggregate(self.ints) From e986fcdf7bb04ccd530ced0b13df36eefe6ef595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Wed, 21 Oct 2026 01:44:02 +0200 Subject: [PATCH 6/9] Aggregate continuous time through the span union transition functions TimeContinuousUnionAggregator aggregates as TimeInstantaneousUnionAggregator does: span_union_transfn and spanset_union_transfn add each span or span set to the state, which starts as None, and spanset_union_finalfn finishes it. union_spanset_span and union_spanset_spanset are the binary unions of two values, and MEOS refuses a NULL span set there, so they cannot start an aggregation. Witness: the union of [2000-01-01, 2000-01-03] and [2000-01-02, 2000-01-04] aggregates to {[2000-01-01, 2000-01-04]}, a case added to tests/aggregators; through union_spanset_span the first add raises TypeError: an integer is required, from casting the None state the wrapper does not guard. The suite passes 4472 of 4472. --- pymeos/aggregators/time_aggregators.py | 17 +++++++++++------ tests/aggregators/aggregators_test.py | 8 ++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/pymeos/aggregators/time_aggregators.py b/pymeos/aggregators/time_aggregators.py index 159503d4..a730ae8e 100644 --- a/pymeos/aggregators/time_aggregators.py +++ b/pymeos/aggregators/time_aggregators.py @@ -4,9 +4,10 @@ datetime_to_timestamptz, set_union_finalfn, set_union_transfn, + span_union_transfn, + spanset_union_finalfn, + spanset_union_transfn, timestamptz_union_transfn, - union_spanset_span, - union_spanset_spanset, ) from ..collections import TsTzSet, TsTzSpan, TsTzSpanSet @@ -43,20 +44,24 @@ class TimeContinuousUnionAggregator(BaseAggregator[TsTzSpan | TsTzSpanSet, TsTzS Temporal union of continuous time objects (:class:`~pymeos.time.tstzspan.TsTzSpan` and :class:`~pymeos.time.tstzspanset.TsTzSpanSet`). + Aggregates as #TimeInstantaneousUnionAggregator does, through the transition and final + functions MEOS provides for the union of spans. + MEOS Functions: - union_spanset_span, union_spanset_spanset + span_union_transfn, spanset_union_transfn, spanset_union_finalfn """ @classmethod def _add(cls, state, temporal): if isinstance(temporal, TsTzSpan): - state = union_spanset_span(state, temporal._inner) + state = span_union_transfn(state, temporal._inner) elif isinstance(temporal, TsTzSpanSet): - state = union_spanset_spanset(state, temporal._inner) + state = spanset_union_transfn(state, temporal._inner) else: cls._error(temporal) return state @classmethod def _finish(cls, state) -> TsTzSpanSet: - return TsTzSpanSet(_inner=state) + result = spanset_union_finalfn(state) + return TsTzSpanSet(_inner=result) diff --git a/tests/aggregators/aggregators_test.py b/tests/aggregators/aggregators_test.py index b2967997..51741a08 100644 --- a/tests/aggregators/aggregators_test.py +++ b/tests/aggregators/aggregators_test.py @@ -12,6 +12,7 @@ TIntSeqSet, TsTzSet, TsTzSpan, + TsTzSpanSet, TTextSeq, TTextSeqSet, ) @@ -32,6 +33,7 @@ TemporalPointExtentAggregator, TemporalTextMaxAggregator, TemporalTextMinAggregator, + TimeContinuousUnionAggregator, TimeInstantaneousUnionAggregator, ) from tests.conftest import TestPyMEOS @@ -122,6 +124,11 @@ class TestAggregators(TestPyMEOS): sets, TsTzSet("{2000-01-01, 2000-01-02, 2000-01-03}"), ), + ( + TimeContinuousUnionAggregator, + spans, + TsTzSpanSet("{[2000-01-01, 2000-01-04]}"), + ), ], ids=[ "And", @@ -138,6 +145,7 @@ class TestAggregators(TestPyMEOS): "InstantCount", "PeriodCount", "InstantaneousUnion", + "ContinuousUnion", ], ) def test_aggregate(self, aggregator, inputs, expected): From 0cf216f990b1421f9e7184bf48c590ba7264e74f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Sat, 31 Oct 2026 11:12:57 +0100 Subject: [PATCH 7/9] Build GDAL without GEOS and install neither GEOS nor GSL on macOS MEOS 1.4 calls neither GEOS nor GSL, so the macOS jobs install neither from Homebrew, and they build GDAL 3.12.0, the version MEOS-API's provision-meos builds for the Linux jobs, with -DGDAL_USE_GEOS=OFF beside the Arrow and Parquet switches. The GDAL cache key carries the version, so the first run builds the new GDAL. Its source and build directories lie in the runner's temporary directory, outside the checkout, so pytest collects neither GDAL's Python bindings nor its tests. --- .github/workflows/test.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8a198df9..63db3bb7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -101,18 +101,19 @@ jobs: uses: tecolicom/actions-use-homebrew-tools@v1 if: runner.os == 'macOS' with: - tools: cmake libpq proj json-c gsl geos h3 + tools: cmake libpq proj json-c h3 # The raster family links GDAL. Homebrew's GDAL links Apache Arrow for its Arrow and # Parquet drivers, and pyarrow's bundled Arrow then binds to that copy and fails to load, - # so GDAL is built here without Arrow or Parquet, as MobilityDuck builds its own: core - # drivers only, against Homebrew's PROJ. Its install names are absolute, so libmeos finds - # it without a library path. + # so GDAL is built here without Arrow or Parquet, as MobilityDuck builds its own: GDAL + # 3.12.0, core drivers only, without GEOS, against Homebrew's PROJ. MEOS calls neither GEOS + # nor GSL, so neither is installed. Its install names are absolute, so libmeos finds it + # without a library path. - name: Key the GDAL build (macOS) id: gdal if: runner.os == 'macOS' run: | - echo "version=3.11.4" >> "$GITHUB_OUTPUT" + echo "version=3.12.0" >> "$GITHUB_OUTPUT" echo "prefix=$HOME/gdal" >> "$GITHUB_OUTPUT" echo "proj=$(brew list --versions proj | cut -d' ' -f2)" >> "$GITHUB_OUTPUT" @@ -124,9 +125,11 @@ jobs: path: ${{ steps.gdal.outputs.prefix }} key: gdal-${{ steps.gdal.outputs.version }}-proj-${{ steps.gdal.outputs.proj }}-${{ matrix.os }} - - name: Build GDAL without Arrow (macOS) + - name: Build GDAL without Arrow and GEOS (macOS) if: runner.os == 'macOS' && steps.gdal-cache.outputs.cache-hit != 'true' run: | + # The source and the build stay outside the checkout, so pytest collects neither. + cd "$RUNNER_TEMP" curl -sSL -o gdal.tar.gz \ https://github.com/OSGeo/gdal/releases/download/v${{ steps.gdal.outputs.version }}/gdal-${{ steps.gdal.outputs.version }}.tar.gz tar xzf gdal.tar.gz @@ -136,7 +139,7 @@ jobs: -DCMAKE_PREFIX_PATH="$(brew --prefix)" \ -DBUILD_APPS=OFF -DBUILD_TESTING=OFF -DBUILD_DOCS=OFF -DBUILD_PYTHON_BINDINGS=OFF \ -DGDAL_BUILD_OPTIONAL_DRIVERS=OFF -DOGR_BUILD_OPTIONAL_DRIVERS=OFF \ - -DGDAL_USE_ARROW=OFF -DGDAL_USE_PARQUET=OFF \ + -DGDAL_USE_ARROW=OFF -DGDAL_USE_PARQUET=OFF -DGDAL_USE_GEOS=OFF \ -DCMAKE_DISABLE_FIND_PACKAGE_Arrow=ON -DCMAKE_DISABLE_FIND_PACKAGE_Parquet=ON cmake --build gdal-build -j"$(sysctl -n hw.ncpu)" cmake --install gdal-build From 2e484afd4c7cebac6c720c687cebd28986947e06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Sat, 31 Oct 2026 11:13:29 +0100 Subject: [PATCH 8/9] State the geodetic crossing MEOS misses on arm64 as an expected failure The temporal equality of a geodetic sequence and of a geodetic sequence set against a sequence they meet at 12:00 is a strict expected failure on arm64 and an ordinary test elsewhere. MEOS dates the meeting with the Cartesian fraction of tgeogpointsegm_distance_turnpt and compares the two great-circle points it interpolates there with exact equality, so it finds the meeting where the two roundings agree, on x86, and misses it where they do not, on arm64. The mark is strict, so the run fails once MEOS dates the meeting exactly on every platform and the mark is due to leave. --- tests/main/tgeogpoint_test.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/main/tgeogpoint_test.py b/tests/main/tgeogpoint_test.py index 962f5db1..276d84cf 100644 --- a/tests/main/tgeogpoint_test.py +++ b/tests/main/tgeogpoint_test.py @@ -1,3 +1,4 @@ +import platform from copy import copy from datetime import datetime, timedelta, timezone from operator import not_ @@ -2774,6 +2775,18 @@ def test_ever_equal_always_not_equal(self, temporal, argument, expected): assert temporal.never_equal(argument) == not_(expected) +# MEOS dates the meeting of two geodetic segments with the Cartesian fraction of +# tgeogpointsegm_distance_turnpt and tests the two great-circle points it interpolates there +# for exact equality, so the meeting at 12:00 is found only where the two roundings agree: on +# x86 they do, on arm64 they do not. Strict, so the mark fails the run once MEOS dates the +# meeting exactly on every platform. +GEODETIC_CROSSING_ON_ARM64 = pytest.mark.xfail( + platform.machine() in ("arm64", "aarch64"), + reason="MEOS dates a geodetic crossing by a Cartesian fraction and exact point equality", + strict=True, +) + + class TestTGeogPointTemporalComparisons(TestTGeogPoint): tpi = TGeogPointInst("Point(1 1)@2019-09-01") tpds = TGeogPointSeq("{Point(1 1)@2019-09-01, Point(2 2)@2019-09-02}") @@ -2788,18 +2801,20 @@ class TestTGeogPointTemporalComparisons(TestTGeogPoint): [ (tpi, TBoolInst("False@2019-09-01")), (tpds, TBoolSeq("{False@2019-09-01, False@2019-09-02}")), - ( + pytest.param( tps, TBoolSeqSet( "{[False@2019-09-01, True@2019-09-01 12:00:00+00],(False@2019-09-01 12:00:00+00, False@2019-09-02]}" ), + marks=GEODETIC_CROSSING_ON_ARM64, ), - ( + pytest.param( tpss, TBoolSeqSet( "{[False@2019-09-01, True@2019-09-01 12:00:00+00]," "(False@2019-09-01 12:00:00+00, False@2019-09-02],[True@2019-09-03]}" ), + marks=GEODETIC_CROSSING_ON_ARM64, ), ], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], From 54316b45b58cd60314c01aa468c54960099b053e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Sat, 31 Oct 2026 12:01:48 +0100 Subject: [PATCH 9/9] Shift or scale a time value or a box through its own MEOS function MEOS states the time shift and the time scale of a timestamptz set, span and span set and of a temporal box and a spatiotemporal box as their own functions, tstzset_shift and tstzset_scale, tstzspan_shift and tstzspan_scale, tstzspanset_shift and tstzspanset_scale, tbox_shift_time and tbox_scale_time, and stbox_shift_time and stbox_scale_time, and the shift and scale function of each takes both intervals. TsTzSet, TsTzSpan, TsTzSpanSet, TBox and STBox answer shift and scale through them, as Temporal answers shift_time and scale_time, and shift_scale, whose two arguments stay optional, calls the shift alone, the scale alone or both by the intervals it is given. --- pymeos/boxes/stbox.py | 21 +++++++++++---------- pymeos/boxes/tbox.py | 21 +++++++++++---------- pymeos/collections/time/tstzset.py | 21 +++++++++++---------- pymeos/collections/time/tstzspan.py | 21 +++++++++++---------- pymeos/collections/time/tstzspanset.py | 21 +++++++++++---------- 5 files changed, 55 insertions(+), 50 deletions(-) diff --git a/pymeos/boxes/stbox.py b/pymeos/boxes/stbox.py index 0cfbfd21..1a4dd2bc 100644 --- a/pymeos/boxes/stbox.py +++ b/pymeos/boxes/stbox.py @@ -619,12 +619,12 @@ def shift_time(self, delta: timedelta) -> STBox: A new :class:`STBox` instance MEOS Functions: - stbox_shift_scale_time + stbox_shift_time See Also: :meth:`STBox.shift` """ - return self.shift_scale_time(shift=delta) + return STBox(_inner=stbox_shift_time(self._inner, timedelta_to_interval(delta))) def scale_time(self, duration: timedelta) -> STBox: """ @@ -638,12 +638,12 @@ def scale_time(self, duration: timedelta) -> STBox: A new :class:`STBox` instance MEOS Functions: - tstzspan_shift_scale + stbox_scale_time See Also: :meth:`STBox.scale` """ - return self.shift_scale_time(duration=duration) + return STBox(_inner=stbox_scale_time(self._inner, timedelta_to_interval(duration))) def shift_scale_time(self, shift: timedelta | None = None, duration: timedelta | None = None) -> STBox: """ @@ -658,17 +658,18 @@ def shift_scale_time(self, shift: timedelta | None = None, duration: timedelta | A new :class:`STBox` instance MEOS Functions: - stbox_shift_scale_time + stbox_shift_time, stbox_scale_time, stbox_shift_scale_time See Also: :meth:`TsTzSpan.shift_scale` """ assert shift is not None or duration is not None, "shift and scale deltas must not be both None" - result = stbox_shift_scale_time( - self._inner, - timedelta_to_interval(shift) if shift else None, - timedelta_to_interval(duration) if duration else None, - ) + if shift is None: + result = stbox_scale_time(self._inner, timedelta_to_interval(duration)) + elif duration is None: + result = stbox_shift_time(self._inner, timedelta_to_interval(shift)) + else: + result = stbox_shift_scale_time(self._inner, timedelta_to_interval(shift), timedelta_to_interval(duration)) return STBox(_inner=result) def round(self, max_decimals: int | None = 0) -> STBox: diff --git a/pymeos/boxes/tbox.py b/pymeos/boxes/tbox.py index bc4b2967..734b462f 100644 --- a/pymeos/boxes/tbox.py +++ b/pymeos/boxes/tbox.py @@ -499,12 +499,12 @@ def shift_time(self, delta: timedelta) -> TBox: A new :class:`TBox` instance MEOS Functions: - tstzspan_shift_scale + tbox_shift_time See Also: :meth:`TsTzSpan.shift` """ - return self.shift_scale_time(shift=delta) + return TBox(_inner=tbox_shift_time(self._inner, timedelta_to_interval(delta))) def scale_value(self, width: int | float) -> TBox: """ @@ -535,12 +535,12 @@ def scale_time(self, duration: timedelta) -> TBox: A new :class:`TBox` instance MEOS Functions: - tstzspan_shift_scale + tbox_scale_time See Also: :meth:`TsTzSpan.scale` """ - return self.shift_scale_time(duration=duration) + return TBox(_inner=tbox_scale_time(self._inner, timedelta_to_interval(duration))) def shift_scale_value( self, @@ -612,17 +612,18 @@ def shift_scale_time(self, shift: timedelta | None = None, duration: timedelta | A new :class:`TBox` instance MEOS Functions: - tstzspan_shift_scale + tbox_shift_time, tbox_scale_time, tbox_shift_scale_time See Also: :meth:`TsTzSpan.shift_scale` """ assert shift is not None or duration is not None, "shift and duration deltas must not be both None" - result = tbox_shift_scale_time( - self._inner, - timedelta_to_interval(shift) if shift else None, - timedelta_to_interval(duration) if duration else None, - ) + if shift is None: + result = tbox_scale_time(self._inner, timedelta_to_interval(duration)) + elif duration is None: + result = tbox_shift_time(self._inner, timedelta_to_interval(shift)) + else: + result = tbox_shift_scale_time(self._inner, timedelta_to_interval(shift), timedelta_to_interval(duration)) return TBox(_inner=result) def round(self, max_decimals: int = 0) -> TBox: diff --git a/pymeos/collections/time/tstzset.py b/pymeos/collections/time/tstzset.py index 08d0fa15..839862f6 100644 --- a/pymeos/collections/time/tstzset.py +++ b/pymeos/collections/time/tstzset.py @@ -136,9 +136,9 @@ def shift(self, delta: timedelta) -> TsTzSet: A new :class:`TsTzSpanSet` instance MEOS Functions: - tstzset_shift_scale + tstzset_shift """ - return self.shift_scale(shift=delta) + return TsTzSet(_inner=tstzset_shift(self._inner, timedelta_to_interval(delta))) def scale(self, duration: timedelta) -> TsTzSet: """ @@ -157,9 +157,9 @@ def scale(self, duration: timedelta) -> TsTzSet: A new :class:`TsTzSpanSet` instance MEOS Functions: - tstzset_shift_scale + tstzset_scale """ - return self.shift_scale(duration=duration) + return TsTzSet(_inner=tstzset_scale(self._inner, timedelta_to_interval(duration))) def shift_scale(self, shift: timedelta | None = None, duration: timedelta | None = None) -> TsTzSet: """ @@ -179,14 +179,15 @@ def shift_scale(self, shift: timedelta | None = None, duration: timedelta | None A new :class:`TsTzSpanSet` instance MEOS Functions: - tstzset_shift_scale + tstzset_shift, tstzset_scale, tstzset_shift_scale """ assert shift is not None or duration is not None, "shift and scale deltas must not be both None" - tss = tstzset_shift_scale( - self._inner, - timedelta_to_interval(shift) if shift else None, - timedelta_to_interval(duration) if duration else None, - ) + if shift is None: + tss = tstzset_scale(self._inner, timedelta_to_interval(duration)) + elif duration is None: + tss = tstzset_shift(self._inner, timedelta_to_interval(shift)) + else: + tss = tstzset_shift_scale(self._inner, timedelta_to_interval(shift), timedelta_to_interval(duration)) return TsTzSet(_inner=tss) # ------------------------- Topological Operations ------------------------ diff --git a/pymeos/collections/time/tstzspan.py b/pymeos/collections/time/tstzspan.py index 62c8b5d9..017b22e5 100644 --- a/pymeos/collections/time/tstzspan.py +++ b/pymeos/collections/time/tstzspan.py @@ -130,9 +130,9 @@ def shift(self, delta: timedelta) -> TsTzSpan: A new :class:`TsTzSpan` instance MEOS Functions: - tstzspan_shift_scale + tstzspan_shift """ - return self.shift_scale(shift=delta) + return TsTzSpan(_inner=tstzspan_shift(self._inner, timedelta_to_interval(delta))) def scale(self, duration: timedelta) -> TsTzSpan: """ @@ -150,9 +150,9 @@ def scale(self, duration: timedelta) -> TsTzSpan: A new :class:`TsTzSpan` instance MEOS Functions: - tstzspan_shift_scale + tstzspan_scale """ - return self.shift_scale(duration=duration) + return TsTzSpan(_inner=tstzspan_scale(self._inner, timedelta_to_interval(duration))) def shift_scale(self, shift: timedelta | None = None, duration: timedelta | None = None) -> TsTzSpan: """ @@ -172,14 +172,15 @@ def shift_scale(self, shift: timedelta | None = None, duration: timedelta | None A new :class:`TsTzSpan` instance MEOS Functions: - tstzspan_shift_scale + tstzspan_shift, tstzspan_scale, tstzspan_shift_scale """ assert shift is not None or duration is not None, "shift and scale deltas must not be both None" - modified = tstzspan_shift_scale( - self._inner, - timedelta_to_interval(shift) if shift else None, - timedelta_to_interval(duration) if duration else None, - ) + if shift is None: + modified = tstzspan_scale(self._inner, timedelta_to_interval(duration)) + elif duration is None: + modified = tstzspan_shift(self._inner, timedelta_to_interval(shift)) + else: + modified = tstzspan_shift_scale(self._inner, timedelta_to_interval(shift), timedelta_to_interval(duration)) return TsTzSpan(_inner=modified) # ------------------------- Topological Operations ------------------------ diff --git a/pymeos/collections/time/tstzspanset.py b/pymeos/collections/time/tstzspanset.py index 0b824080..424491ad 100644 --- a/pymeos/collections/time/tstzspanset.py +++ b/pymeos/collections/time/tstzspanset.py @@ -237,9 +237,9 @@ def shift(self, delta: timedelta) -> TsTzSpanSet: A new :class:`TsTzSpanSet` instance MEOS Functions: - tstzspanset_shift_scale + tstzspanset_shift """ - return self.shift_scale(shift=delta) + return TsTzSpanSet(_inner=tstzspanset_shift(self._inner, timedelta_to_interval(delta))) def scale(self, duration: timedelta) -> TsTzSpanSet: """ @@ -258,9 +258,9 @@ def scale(self, duration: timedelta) -> TsTzSpanSet: A new :class:`PeriodSet` instance MEOS Functions: - tstzspanset_shift_scale + tstzspanset_scale """ - return self.shift_scale(duration=duration) + return TsTzSpanSet(_inner=tstzspanset_scale(self._inner, timedelta_to_interval(duration))) def shift_scale(self, shift: timedelta | None = None, duration: timedelta | None = None) -> TsTzSpanSet: """ @@ -280,14 +280,15 @@ def shift_scale(self, shift: timedelta | None = None, duration: timedelta | None A new :class:`PeriodSet` instance MEOS Functions: - tstzspanset_shift_scale + tstzspanset_shift, tstzspanset_scale, tstzspanset_shift_scale """ assert shift is not None or duration is not None, "shift and scale deltas must not be both None" - ps = tstzspanset_shift_scale( - self._inner, - timedelta_to_interval(shift) if shift else None, - timedelta_to_interval(duration) if duration else None, - ) + if shift is None: + ps = tstzspanset_scale(self._inner, timedelta_to_interval(duration)) + elif duration is None: + ps = tstzspanset_shift(self._inner, timedelta_to_interval(shift)) + else: + ps = tstzspanset_shift_scale(self._inner, timedelta_to_interval(shift), timedelta_to_interval(duration)) return TsTzSpanSet(_inner=ps) # ------------------------- Topological Operations ------------------------