From df78ef65c0e37008016881710773bd45afdd4f2b 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/7] 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 | 18 +++---- 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 | 66 +++++++++++++------------- pymeos/main/ttext.py | 4 +- pymeos/temporal/temporal.py | 41 ++++++++-------- pymeos/temporal/tinstant.py | 4 +- pyproject.toml | 2 +- 22 files changed, 126 insertions(+), 124 deletions(-) diff --git a/pymeos/__init__.py b/pymeos/__init__.py index 1924c21b..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-alpha-1" +__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..e53b1b4b 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__}") @@ -1285,7 +1285,7 @@ def tile( dt = ( timedelta_to_interval(duration) if isinstance(duration, timedelta) - else pg_interval_in(duration, -1) + else interval_in(duration, -1) if isinstance(duration, str) else None ) @@ -1293,9 +1293,9 @@ def tile( datetime_to_timestamptz(start) if isinstance(start, datetime) else ( - pg_timestamptz_in(start, -1) + timestamptz_in(start, -1) if isinstance(start, str) - else pg_timestamptz_in("2000-01-03", -1) + else timestamptz_in("2000-01-03", -1) if self.has_t() else 0 ) @@ -1303,7 +1303,7 @@ def tile( 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)) + else (geog_in("Point(0 0 0)", -1) if self.geodetic() else geom_in("Point(0 0 0)", -1)) ) tiles, count = stbox_space_time_tiles(self._inner, sz, sz, sz, dt, gs, st, border_inc) return [STBox(_inner=tiles + i) for i in range(count)] 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..bfa01329 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,9 +964,9 @@ 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) @@ -1086,19 +1086,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 +1365,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 +1391,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 +1417,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 +1444,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..8acd3504 100644 --- a/pymeos/temporal/temporal.py +++ b/pymeos/temporal/temporal.py @@ -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: @@ -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/pyproject.toml b/pyproject.toml index 066b9e79..c7eb7fd9 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 af8ab8a5c3754ef696fa76762af37ad753c488c4 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/7] 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 | 93 ++++++++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 28 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index edd7300c..db79c921 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,8 +17,51 @@ on: - ".github/ISSUE_TEMPLATE/**" 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 @@ -34,18 +77,21 @@ 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 gdal - name: Update brew if: matrix.os == 'macos-15-intel' @@ -56,31 +102,14 @@ jobs: brew reinstall python@3.11 || brew link --overwrite python@3.11 brew update - - 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: Fetch MEOS sources - run: | - git clone --branch ${{ steps.meos_branch.outputs.meos_branch }} --depth 1 https://github.com/MobilityDB/MobilityDB - - - 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 }} + make -j"$(sysctl -n hw.ncpu)" sudo make install - name: Set up Python ${{ matrix.python-version }} @@ -96,6 +125,12 @@ jobs: git clone --branch ${{ env.BRANCH_NAME }} --depth 1 https://github.com/MobilityDB/PyMEOS-CFFI \ || git clone --branch ${{ github.base_ref || 'master' }} --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: | python -m pip install --upgrade pip @@ -103,9 +138,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 4c66e771ac8c32bf56374c8d36a78d8c8af0c823 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/7] 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 | 102 ++++++-------------------- 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(+), 278 deletions(-) diff --git a/tests/main/tbool_test.py b/tests/main/tbool_test.py index 92400c62..46e95ac4 100644 --- a/tests/main/tbool_test.py +++ b/tests/main/tbool_test.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta, timezone import pytest +from pymeos_cffi import MeosInvalidArgTypeError from pymeos import ( TBool, @@ -345,8 +346,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" @@ -354,7 +355,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' "}", @@ -364,8 +365,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" @@ -374,8 +375,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' @@ -387,8 +388,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" @@ -397,8 +398,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' @@ -410,8 +411,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" @@ -422,8 +423,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' @@ -434,8 +435,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' @@ -822,7 +823,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): @@ -1490,35 +1491,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", [ @@ -1589,35 +1561,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", [ @@ -1681,9 +1624,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 b3e97c59..41c1089a 100644 --- a/tests/main/tfloat_test.py +++ b/tests/main/tfloat_test.py @@ -401,8 +401,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" @@ -410,7 +410,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' "}", @@ -424,8 +424,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" @@ -434,8 +434,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' @@ -451,8 +451,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" @@ -461,8 +461,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' @@ -478,8 +478,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" @@ -490,8 +490,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' @@ -502,8 +502,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' @@ -958,12 +958,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", @@ -2836,8 +2836,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]}"), ], ), ], @@ -2854,15 +2855,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]}"), ], ), ], @@ -2886,9 +2889,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 4e415c37..4f3abeb2 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 MeosInvalidArgValueError +from pymeos_cffi import MeosInvalidArgTypeError, MeosInvalidArgValueError from shapely import LineString, Point, set_srid from pymeos import ( @@ -436,6 +436,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" @@ -447,8 +453,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" @@ -459,7 +465,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' "}", @@ -468,6 +474,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" @@ -479,8 +491,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" @@ -495,8 +507,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' @@ -507,6 +519,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" @@ -518,8 +536,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" @@ -534,8 +552,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' @@ -546,6 +564,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" @@ -557,8 +581,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" @@ -575,8 +599,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' @@ -593,8 +617,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' @@ -984,7 +1008,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): @@ -2235,9 +2259,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): @@ -2470,8 +2495,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]}"), ], ), ], @@ -2494,15 +2520,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 d08c6a03..fa647db8 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 MeosInvalidArgValueError +from pymeos_cffi import MeosInvalidArgTypeError, MeosInvalidArgValueError from shapely import GeometryCollection, LineString, MultiPoint, Point, Polygon from pymeos import ( @@ -466,8 +466,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" @@ -478,7 +478,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' "}", @@ -498,8 +498,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" @@ -514,8 +514,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' @@ -537,8 +537,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" @@ -553,8 +553,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' @@ -576,8 +576,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" @@ -594,8 +594,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' @@ -612,8 +612,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' @@ -1010,10 +1010,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"], ) @@ -2130,35 +2130,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", [ @@ -2259,43 +2230,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", [ @@ -2359,9 +2293,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): @@ -3140,8 +3075,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]}"), ], ), ], @@ -3164,15 +3100,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 42431b0c..6380fce6 100644 --- a/tests/main/tint_test.py +++ b/tests/main/tint_test.py @@ -355,8 +355,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" @@ -364,7 +364,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' "}", @@ -378,8 +378,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" @@ -388,8 +388,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' @@ -405,8 +405,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" @@ -415,8 +415,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' @@ -432,8 +432,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" @@ -444,8 +444,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' @@ -456,8 +456,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' @@ -882,7 +882,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): @@ -2196,7 +2196,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), @@ -2573,8 +2573,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]}"), ], ), ], @@ -2592,9 +2593,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 c575a447..0d109bf0 100644 --- a/tests/main/ttext_test.py +++ b/tests/main/ttext_test.py @@ -358,8 +358,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" @@ -367,7 +367,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' "}", @@ -377,8 +377,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" @@ -387,8 +387,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' @@ -400,8 +400,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" @@ -410,8 +410,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' @@ -423,8 +423,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" @@ -435,8 +435,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' @@ -447,8 +447,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' @@ -838,7 +838,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 579e8eff9b5a96398af48bc33d790422a435d375 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/7] 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 | 21 +++++++++++++++++++++ tests/boxes/tbox_test.py | 12 ++++++++++++ 2 files changed, 33 insertions(+) diff --git a/tests/boxes/stbox_test.py b/tests/boxes/stbox_test.py index bcc676d9..77588045 100644 --- a/tests/boxes/stbox_test.py +++ b/tests/boxes/stbox_test.py @@ -1261,3 +1261,24 @@ 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]" + ) + + def test_tile(self): + tiles = STBox.from_tpoint(self.trip).tile(5e3) + assert [str(tile) for tile in tiles] == [ + "SRID=3857;STBOX X((480000,6585000),(485000,6590000))", + "SRID=3857;STBOX X((485000,6585000),(490000,6590000))", + ] + + def test_tpoint_tile(self): + pieces = self.trip.tile(5e3, remove_empty=True) + 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 d4de97edf48a6f1b38244570c456b090b90f5cb8 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/7] 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 39f5fdd650bdf98e5a3f08e6fbf2365bbf620bbf 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/7] 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 166c728c7c9884f48134e2aaeef789e100ff5101 Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Mon, 18 May 2026 16:24:31 +0200 Subject: [PATCH 7/7] feat: portable bare-name dialect generated from the canonical contract Expose every MobilityDB operator under its canonical portable bare name (overlaps, contains, teq, nearestApproachDistance, ...) as a dedicated `pymeos.portable` module, generated from the cross-binding source of truth (MobilityDB/MEOS-API meta/portable-aliases.json, RFC #920; vendored verbatim). Each bare name dispatches, by the runtime C type of its arguments, to the exact pymeos_cffi function the operator is backed by -- the same native call the type-qualified API uses. Nothing is reimplemented, so each bare name is identical to its operator by construction. - tools/portable_aliases/: the contract plus parity logic vendored byte-identically from MEOS-API, and generate.py - pymeos/portable.py: generated; 29 bare names, type-agnostic across all six in-scope families (temporal, geo, cbuffer, npoint, pose, rgeo) - tests/test_portable_parity.py and a CI step gate the contract: 29/29 backed, 0 unbacked, all six families, zero per-binding exceptions, verified against the live binding --- .github/workflows/test.yml | 11 + pymeos/portable.py | 342 +++++++++++++++ tests/test_portable_parity.py | 160 +++++++ tools/portable_aliases/README.md | 48 +++ tools/portable_aliases/generate.py | 423 +++++++++++++++++++ tools/portable_aliases/portable-aliases.json | 60 +++ tools/portable_aliases/portable.py | 45 ++ tools/portable_aliases/portable_parity.py | 81 ++++ 8 files changed, 1170 insertions(+) create mode 100644 pymeos/portable.py create mode 100644 tests/test_portable_parity.py create mode 100644 tools/portable_aliases/README.md create mode 100644 tools/portable_aliases/generate.py create mode 100644 tools/portable_aliases/portable-aliases.json create mode 100644 tools/portable_aliases/portable.py create mode 100644 tools/portable_aliases/portable_parity.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index db79c921..f2e4333b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -146,6 +146,17 @@ jobs: python ./builder/build_pymeos_functions.py pip install . + - name: Portable bare-name parity gate + # Regenerate pymeos/portable.py from the MEOS just built+installed so + # the shipped module matches the binding under test, and hard-gate + # the canonical contract: 29/29 bare names backed, 0 unbacked, all + # six families, and the live pymeos_cffi backs every one (--cffi). + run: | + export MEOS_INCLUDE=${{ matrix.ld_prefix }}/include + export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${{ matrix.ld_prefix }}/lib + export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:${{ matrix.ld_prefix }}/lib + python tools/portable_aliases/generate.py --cffi --check + - name: Test PyMEOS with pytest run: | export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${{ matrix.ld_prefix }}/lib diff --git a/pymeos/portable.py b/pymeos/portable.py new file mode 100644 index 00000000..cf7dd686 --- /dev/null +++ b/pymeos/portable.py @@ -0,0 +1,342 @@ +# Copyright (c) 2016-2026, Université libre de Bruxelles and PyMEOS +# contributors. Licensed under the PostgreSQL License (see LICENSE). +# +# ============================================================================ +# GENERATED by tools/portable_aliases/generate.py -- DO NOT EDIT. +# Regenerate: python3 tools/portable_aliases/generate.py --headers +# ============================================================================ +# +# The portable bare-name dialect (RFC #920; canonical contract: +# MobilityDB/MEOS-API meta/portable-aliases.json; native in MobilityDB via +# the 1303-alias PR #1075). Every binding exposes the IDENTICAL bare names +# from that one mapping, type-agnostically across all six temporal type +# families (temporal, geo, cbuffer, npoint, pose, rgeo). +# +# Each bare name dispatches, by the runtime C type of its arguments, to the +# EXACT pymeos_cffi function the operator is backed by -- the same native +# call the type-qualified API uses. Nothing is reimplemented: every bare +# name is identical to its operator by construction. +"""Portable bare-name dialect for PyMEOS (generated -- see module comment).""" +from __future__ import annotations + +import datetime as _dt + +# Resolved at call time so importing this module never requires the compiled +# binding to be present (e.g. docs / parity checks). +_cffi = None + + +def _pymeos_cffi(): + global _cffi + if _cffi is None: + import pymeos_cffi as _c + + _cffi = _c + return _cffi + + +def _unwrap(a): + return a._inner if hasattr(a, "_inner") else a + + +def _arg_token(a): + """Runtime C base type of an argument, mirroring pymeos.factory's cdata + field-sniffing (no ffi object needed) plus Python scalars.""" + if isinstance(a, bool): + return "bool" + if isinstance(a, int): + return "int" + if isinstance(a, float): + return "float" + if isinstance(a, str): + return "text" + if isinstance(a, _dt.datetime): + return "timestamptz" + if isinstance(a, _dt.date): + return "date" + if isinstance(a, _dt.timedelta): + return "interval" + o = _unwrap(a) + for attr, tok in ( + ("temptype", "Temporal"), # every temporal subtype (6 families) + ("spansettype", "SpanSet"), + ("spantype", "Span"), + ("settype", "Set"), + ): + if hasattr(o, attr): + return tok + # Boxes / geo / spatial base types: distinguishing struct fields. + for attr, tok in ( + ("srid", "STBox"), # STBox carries an srid; TBox does not + ("period", "TBox"), + ("gflags", "GSERIALIZED"), + ("rid", "Npoint"), + ("radius", "Cbuffer"), + ): + if hasattr(o, attr): + return tok + return type(a).__name__ + + +def _wrap(res): + """Re-wrap MEOS-object results with the existing PyMEOS factories; pass + scalars (bool/float/int/str) and None straight through.""" + if res is None or isinstance(res, (bool, int, float, str, bytes)): + return res + from .factory import _TemporalFactory, _CollectionFactory + + for fn in (_TemporalFactory.create_temporal, + _CollectionFactory.create_collection): + try: + return fn(res) + except Exception: + pass + return res + + +def _dispatch(name, table, args): + key = tuple(_arg_token(a) for a in args) + sym = table.get(key) + if sym is None and len(key) == 2: # symmetric fallback + sym = table.get((key[1], key[0])) + if sym is None: + raise TypeError( + f"{name}(): no portable overload for argument types {key}; " + f"supported: {sorted(table)}" + ) + fn = getattr(_pymeos_cffi(), sym) + return _wrap(fn(*[_unwrap(a) for a in args])) + +__all__ = ['above', 'adjacent', 'after', 'back', 'before', 'below', 'contained', 'contains', 'front', 'left', 'nearestApproachDistance', 'overabove', 'overafter', 'overback', 'overbefore', 'overbelow', 'overfront', 'overlaps', 'overleft', 'overright', 'right', 'same', 'tdistance', 'teq', 'tge', 'tgt', 'tle', 'tlt', 'tne'] + + +_T_above = {('STBox', 'STBox'): 'above_stbox_stbox', ('STBox', 'Temporal'): 'above_stbox_tspatial', ('Temporal', 'STBox'): 'above_tspatial_stbox', ('Temporal', 'Temporal'): 'above_tspatial_tspatial'} + + +def above(*args): + """Portable ``|>>`` (spaceY) -> pymeos_cffi backing call.""" + return _dispatch('above', _T_above, args) + + +_T_adjacent = {('Span', 'Temporal'): 'adjacent_tstzspan_temporal', ('Span', 'bigint'): 'adjacent_span_bigint', ('Span', 'date'): 'adjacent_span_date', ('Span', 'float'): 'adjacent_span_float', ('Span', 'int'): 'adjacent_span_int', ('Span', 'Span'): 'adjacent_span_span', ('Span', 'SpanSet'): 'adjacent_span_spanset', ('Span', 'timestamptz'): 'adjacent_span_timestamptz', ('Span', 'Datum'): 'adjacent_span_value', ('SpanSet', 'bigint'): 'adjacent_spanset_bigint', ('SpanSet', 'date'): 'adjacent_spanset_date', ('SpanSet', 'float'): 'adjacent_spanset_float', ('SpanSet', 'int'): 'adjacent_spanset_int', ('SpanSet', 'Span'): 'adjacent_spanset_span', ('SpanSet', 'SpanSet'): 'adjacent_spanset_spanset', ('SpanSet', 'timestamptz'): 'adjacent_spanset_timestamptz', ('SpanSet', 'Datum'): 'adjacent_spanset_value', ('STBox', 'STBox'): 'adjacent_stbox_stbox', ('STBox', 'Temporal'): 'adjacent_stbox_tspatial', ('TBox', 'TBox'): 'adjacent_tbox_tbox', ('TBox', 'Temporal'): 'adjacent_tbox_tnumber', ('Temporal', 'Temporal'): 'adjacent_tspatial_tspatial', ('Temporal', 'Span'): 'adjacent_tnumber_numspan', ('Temporal', 'TBox'): 'adjacent_tnumber_tbox', ('Temporal', 'STBox'): 'adjacent_tspatial_stbox', ('Datum', 'SpanSet'): 'adjacent_value_spanset'} + + +def adjacent(*args): + """Portable ``-|-`` (topology) -> pymeos_cffi backing call.""" + return _dispatch('adjacent', _T_adjacent, args) + + +_T_after = {('date', 'Set'): 'after_date_set', ('date', 'Span'): 'after_date_span', ('date', 'SpanSet'): 'after_date_spanset', ('Set', 'date'): 'after_set_date', ('Set', 'timestamptz'): 'after_set_timestamptz', ('Span', 'date'): 'after_span_date', ('Span', 'timestamptz'): 'after_span_timestamptz', ('SpanSet', 'date'): 'after_spanset_date', ('SpanSet', 'timestamptz'): 'after_spanset_timestamptz', ('STBox', 'STBox'): 'after_stbox_stbox', ('STBox', 'Temporal'): 'after_stbox_tspatial', ('TBox', 'TBox'): 'after_tbox_tbox', ('TBox', 'Temporal'): 'after_tbox_tnumber', ('Temporal', 'Temporal'): 'after_tspatial_tspatial', ('Temporal', 'Span'): 'after_temporal_tstzspan', ('timestamptz', 'Set'): 'after_timestamptz_set', ('timestamptz', 'Span'): 'after_timestamptz_span', ('timestamptz', 'SpanSet'): 'after_timestamptz_spanset', ('Temporal', 'TBox'): 'after_tnumber_tbox', ('Temporal', 'STBox'): 'after_tspatial_stbox', ('Span', 'Temporal'): 'after_tstzspan_temporal'} + + +def after(*args): + """Portable ``#>>`` (timePosition) -> pymeos_cffi backing call.""" + return _dispatch('after', _T_after, args) + + +_T_back = {('STBox', 'STBox'): 'back_stbox_stbox', ('STBox', 'Temporal'): 'back_stbox_tspatial', ('Temporal', 'STBox'): 'back_tspatial_stbox', ('Temporal', 'Temporal'): 'back_tspatial_tspatial'} + + +def back(*args): + """Portable ``/>>`` (spaceZ) -> pymeos_cffi backing call.""" + return _dispatch('back', _T_back, args) + + +_T_before = {('date', 'Set'): 'before_date_set', ('date', 'Span'): 'before_date_span', ('date', 'SpanSet'): 'before_date_spanset', ('Set', 'date'): 'before_set_date', ('Set', 'timestamptz'): 'before_set_timestamptz', ('Span', 'date'): 'before_span_date', ('Span', 'timestamptz'): 'before_span_timestamptz', ('SpanSet', 'date'): 'before_spanset_date', ('SpanSet', 'timestamptz'): 'before_spanset_timestamptz', ('STBox', 'STBox'): 'before_stbox_stbox', ('STBox', 'Temporal'): 'before_stbox_tspatial', ('TBox', 'TBox'): 'before_tbox_tbox', ('TBox', 'Temporal'): 'before_tbox_tnumber', ('Temporal', 'Temporal'): 'before_tspatial_tspatial', ('Temporal', 'Span'): 'before_temporal_tstzspan', ('timestamptz', 'Set'): 'before_timestamptz_set', ('timestamptz', 'Span'): 'before_timestamptz_span', ('timestamptz', 'SpanSet'): 'before_timestamptz_spanset', ('Temporal', 'TBox'): 'before_tnumber_tbox', ('Temporal', 'STBox'): 'before_tspatial_stbox', ('Span', 'Temporal'): 'before_tstzspan_temporal'} + + +def before(*args): + """Portable ``<<#`` (timePosition) -> pymeos_cffi backing call.""" + return _dispatch('before', _T_before, args) + + +_T_below = {('STBox', 'STBox'): 'below_stbox_stbox', ('STBox', 'Temporal'): 'below_stbox_tspatial', ('Temporal', 'STBox'): 'below_tspatial_stbox', ('Temporal', 'Temporal'): 'below_tspatial_tspatial'} + + +def below(*args): + """Portable ``<<|`` (spaceY) -> pymeos_cffi backing call.""" + return _dispatch('below', _T_below, args) + + +_T_contained = {('bigint', 'Set'): 'contained_bigint_set', ('bigint', 'Span'): 'contained_bigint_span', ('bigint', 'SpanSet'): 'contained_bigint_spanset', ('Cbuffer', 'Set'): 'contained_cbuffer_set', ('date', 'Set'): 'contained_date_set', ('date', 'Span'): 'contained_date_span', ('date', 'SpanSet'): 'contained_date_spanset', ('float', 'Set'): 'contained_float_set', ('float', 'Span'): 'contained_float_span', ('float', 'SpanSet'): 'contained_float_spanset', ('GSERIALIZED', 'Set'): 'contained_geo_set', ('int', 'Set'): 'contained_int_set', ('int', 'Span'): 'contained_int_span', ('int', 'SpanSet'): 'contained_int_spanset', ('Jsonb', 'Set'): 'contained_jsonb_set', ('Npoint', 'Set'): 'contained_npoint_set', ('Span', 'Temporal'): 'contained_tstzspan_temporal', ('Pose', 'Set'): 'contained_pose_set', ('Set', 'Set'): 'contained_set_set', ('Span', 'Span'): 'contained_span_span', ('Span', 'SpanSet'): 'contained_span_spanset', ('SpanSet', 'Span'): 'contained_spanset_span', ('SpanSet', 'SpanSet'): 'contained_spanset_spanset', ('STBox', 'STBox'): 'contained_stbox_stbox', ('STBox', 'Temporal'): 'contained_stbox_tspatial', ('TBox', 'TBox'): 'contained_tbox_tbox', ('TBox', 'Temporal'): 'contained_tbox_tnumber', ('Temporal', 'Temporal'): 'contained_tspatial_tspatial', ('Temporal', 'Span'): 'contained_tnumber_numspan', ('text', 'Set'): 'contained_text_set', ('timestamptz', 'Set'): 'contained_timestamptz_set', ('timestamptz', 'Span'): 'contained_timestamptz_span', ('timestamptz', 'SpanSet'): 'contained_timestamptz_spanset', ('Temporal', 'TBox'): 'contained_tnumber_tbox', ('Temporal', 'STBox'): 'contained_tspatial_stbox', ('Datum', 'Set'): 'contained_value_set', ('Datum', 'Span'): 'contained_value_span', ('Datum', 'SpanSet'): 'contained_value_spanset'} + + +def contained(*args): + """Portable ``<@`` (topology) -> pymeos_cffi backing call.""" + return _dispatch('contained', _T_contained, args) + + +_T_contains = {('Cbuffer', 'Cbuffer'): 'contains_cbuffer_cbuffer', ('Span', 'Temporal'): 'contains_tstzspan_temporal', ('Set', 'bigint'): 'contains_set_bigint', ('Set', 'Cbuffer'): 'contains_set_cbuffer', ('Set', 'date'): 'contains_set_date', ('Set', 'float'): 'contains_set_float', ('Set', 'GSERIALIZED'): 'contains_set_geo', ('Set', 'int'): 'contains_set_int', ('Set', 'Jsonb'): 'contains_set_jsonb', ('Set', 'Npoint'): 'contains_set_npoint', ('Set', 'Pose'): 'contains_set_pose', ('Set', 'Set'): 'contains_set_set', ('Set', 'text'): 'contains_set_text', ('Set', 'timestamptz'): 'contains_set_timestamptz', ('Set', 'Datum'): 'contains_set_value', ('Span', 'bigint'): 'contains_span_bigint', ('Span', 'date'): 'contains_span_date', ('Span', 'float'): 'contains_span_float', ('Span', 'int'): 'contains_span_int', ('Span', 'Span'): 'contains_span_span', ('Span', 'SpanSet'): 'contains_span_spanset', ('Span', 'timestamptz'): 'contains_span_timestamptz', ('Span', 'Datum'): 'contains_span_value', ('SpanSet', 'bigint'): 'contains_spanset_bigint', ('SpanSet', 'date'): 'contains_spanset_date', ('SpanSet', 'float'): 'contains_spanset_float', ('SpanSet', 'int'): 'contains_spanset_int', ('SpanSet', 'Span'): 'contains_spanset_span', ('SpanSet', 'SpanSet'): 'contains_spanset_spanset', ('SpanSet', 'timestamptz'): 'contains_spanset_timestamptz', ('SpanSet', 'Datum'): 'contains_spanset_value', ('STBox', 'STBox'): 'contains_stbox_stbox', ('STBox', 'Temporal'): 'contains_stbox_tspatial', ('TBox', 'TBox'): 'contains_tbox_tbox', ('TBox', 'Temporal'): 'contains_tbox_tnumber', ('Temporal', 'Temporal'): 'contains_tspatial_tspatial', ('Temporal', 'Span'): 'contains_tnumber_numspan', ('Temporal', 'Jsonb', 'bool'): 'contains_tjsonb_jsonb', ('Temporal', 'TBox'): 'contains_tnumber_tbox', ('Temporal', 'STBox'): 'contains_tspatial_stbox'} + + +def contains(*args): + """Portable ``@>`` (topology) -> pymeos_cffi backing call.""" + return _dispatch('contains', _T_contains, args) + + +_T_front = {('STBox', 'STBox'): 'front_stbox_stbox', ('STBox', 'Temporal'): 'front_stbox_tspatial', ('Temporal', 'STBox'): 'front_tspatial_stbox', ('Temporal', 'Temporal'): 'front_tspatial_tspatial'} + + +def front(*args): + """Portable ``< pymeos_cffi backing call.""" + return _dispatch('front', _T_front, args) + + +_T_left = {('bigint', 'Set'): 'left_bigint_set', ('bigint', 'Span'): 'left_bigint_span', ('bigint', 'SpanSet'): 'left_bigint_spanset', ('float', 'Set'): 'left_float_set', ('float', 'Span'): 'left_float_span', ('float', 'SpanSet'): 'left_float_spanset', ('int', 'Set'): 'left_int_set', ('int', 'Span'): 'left_int_span', ('int', 'SpanSet'): 'left_int_spanset', ('Span', 'Temporal'): 'left_numspan_tnumber', ('Set', 'bigint'): 'left_set_bigint', ('Set', 'float'): 'left_set_float', ('Set', 'int'): 'left_set_int', ('Set', 'Set'): 'left_set_set', ('Set', 'text'): 'left_set_text', ('Set', 'Datum'): 'left_set_value', ('Span', 'bigint'): 'left_span_bigint', ('Span', 'float'): 'left_span_float', ('Span', 'int'): 'left_span_int', ('Span', 'Span'): 'left_span_span', ('Span', 'SpanSet'): 'left_span_spanset', ('Span', 'Datum'): 'left_span_value', ('SpanSet', 'bigint'): 'left_spanset_bigint', ('SpanSet', 'float'): 'left_spanset_float', ('SpanSet', 'int'): 'left_spanset_int', ('SpanSet', 'Span'): 'left_spanset_span', ('SpanSet', 'SpanSet'): 'left_spanset_spanset', ('SpanSet', 'Datum'): 'left_spanset_value', ('STBox', 'STBox'): 'left_stbox_stbox', ('STBox', 'Temporal'): 'left_stbox_tspatial', ('TBox', 'TBox'): 'left_tbox_tbox', ('TBox', 'Temporal'): 'left_tbox_tnumber', ('text', 'Set'): 'left_text_set', ('Temporal', 'Span'): 'left_tnumber_numspan', ('Temporal', 'TBox'): 'left_tnumber_tbox', ('Temporal', 'Temporal'): 'left_tspatial_tspatial', ('Temporal', 'STBox'): 'left_tspatial_stbox', ('Datum', 'Set'): 'left_value_set', ('Datum', 'Span'): 'left_value_span', ('Datum', 'SpanSet'): 'left_value_spanset'} + + +def left(*args): + """Portable ``<<`` (spaceX) -> pymeos_cffi backing call.""" + return _dispatch('left', _T_left, args) + + +_T_nearestApproachDistance = {('Cbuffer', 'STBox'): 'nad_cbuffer_stbox', ('STBox', 'GSERIALIZED'): 'nad_stbox_geo', ('STBox', 'STBox'): 'nad_stbox_stbox', ('STBox', 'Temporal'): 'nad_stbox_trgeo', ('TBox', 'TBox'): 'nad_tboxint_tboxint', ('Temporal', 'Cbuffer'): 'nad_tcbuffer_cbuffer', ('Temporal', 'GSERIALIZED'): 'nad_trgeo_geo', ('Temporal', 'STBox'): 'nad_trgeo_stbox', ('Temporal', 'Temporal'): 'nad_trgeo_trgeo', ('Temporal', 'float'): 'nad_tfloat_float', ('Temporal', 'TBox'): 'nad_tnumber_tbox', ('Temporal', 'int'): 'nad_tint_int', ('Temporal', 'Npoint'): 'nad_tnpoint_npoint', ('Temporal', 'Datum'): 'nad_tnumber_number', ('Temporal', 'Pose'): 'nad_tpose_pose'} + + +def nearestApproachDistance(*args): + """Portable ``|=|`` (distance) -> pymeos_cffi backing call.""" + return _dispatch('nearestApproachDistance', _T_nearestApproachDistance, args) + + +_T_overabove = {('STBox', 'STBox'): 'overabove_stbox_stbox', ('STBox', 'Temporal'): 'overabove_stbox_tspatial', ('Temporal', 'STBox'): 'overabove_tspatial_stbox', ('Temporal', 'Temporal'): 'overabove_tspatial_tspatial'} + + +def overabove(*args): + """Portable ``|&>`` (spaceY) -> pymeos_cffi backing call.""" + return _dispatch('overabove', _T_overabove, args) + + +_T_overafter = {('date', 'Set'): 'overafter_date_set', ('date', 'Span'): 'overafter_date_span', ('date', 'SpanSet'): 'overafter_date_spanset', ('Set', 'date'): 'overafter_set_date', ('Set', 'timestamptz'): 'overafter_set_timestamptz', ('Span', 'date'): 'overafter_span_date', ('Span', 'timestamptz'): 'overafter_span_timestamptz', ('SpanSet', 'date'): 'overafter_spanset_date', ('SpanSet', 'timestamptz'): 'overafter_spanset_timestamptz', ('STBox', 'STBox'): 'overafter_stbox_stbox', ('STBox', 'Temporal'): 'overafter_stbox_tspatial', ('TBox', 'TBox'): 'overafter_tbox_tbox', ('TBox', 'Temporal'): 'overafter_tbox_tnumber', ('Temporal', 'Temporal'): 'overafter_tspatial_tspatial', ('Temporal', 'Span'): 'overafter_temporal_tstzspan', ('timestamptz', 'Set'): 'overafter_timestamptz_set', ('timestamptz', 'Span'): 'overafter_timestamptz_span', ('timestamptz', 'SpanSet'): 'overafter_timestamptz_spanset', ('Temporal', 'TBox'): 'overafter_tnumber_tbox', ('Temporal', 'STBox'): 'overafter_tspatial_stbox', ('Span', 'Temporal'): 'overafter_tstzspan_temporal'} + + +def overafter(*args): + """Portable ``#&>`` (timePosition) -> pymeos_cffi backing call.""" + return _dispatch('overafter', _T_overafter, args) + + +_T_overback = {('STBox', 'STBox'): 'overback_stbox_stbox', ('STBox', 'Temporal'): 'overback_stbox_tspatial', ('Temporal', 'STBox'): 'overback_tspatial_stbox', ('Temporal', 'Temporal'): 'overback_tspatial_tspatial'} + + +def overback(*args): + """Portable ``/&>`` (spaceZ) -> pymeos_cffi backing call.""" + return _dispatch('overback', _T_overback, args) + + +_T_overbefore = {('date', 'Set'): 'overbefore_date_set', ('date', 'Span'): 'overbefore_date_span', ('date', 'SpanSet'): 'overbefore_date_spanset', ('Set', 'date'): 'overbefore_set_date', ('Set', 'timestamptz'): 'overbefore_set_timestamptz', ('Span', 'date'): 'overbefore_span_date', ('Span', 'timestamptz'): 'overbefore_span_timestamptz', ('SpanSet', 'date'): 'overbefore_spanset_date', ('SpanSet', 'timestamptz'): 'overbefore_spanset_timestamptz', ('STBox', 'STBox'): 'overbefore_stbox_stbox', ('STBox', 'Temporal'): 'overbefore_stbox_tspatial', ('TBox', 'TBox'): 'overbefore_tbox_tbox', ('TBox', 'Temporal'): 'overbefore_tbox_tnumber', ('Temporal', 'Temporal'): 'overbefore_tspatial_tspatial', ('Temporal', 'Span'): 'overbefore_temporal_tstzspan', ('timestamptz', 'Set'): 'overbefore_timestamptz_set', ('timestamptz', 'Span'): 'overbefore_timestamptz_span', ('timestamptz', 'SpanSet'): 'overbefore_timestamptz_spanset', ('Temporal', 'TBox'): 'overbefore_tnumber_tbox', ('Temporal', 'STBox'): 'overbefore_tspatial_stbox', ('Span', 'Temporal'): 'overbefore_tstzspan_temporal'} + + +def overbefore(*args): + """Portable ``&<#`` (timePosition) -> pymeos_cffi backing call.""" + return _dispatch('overbefore', _T_overbefore, args) + + +_T_overbelow = {('STBox', 'STBox'): 'overbelow_stbox_stbox', ('STBox', 'Temporal'): 'overbelow_stbox_tspatial', ('Temporal', 'STBox'): 'overbelow_tspatial_stbox', ('Temporal', 'Temporal'): 'overbelow_tspatial_tspatial'} + + +def overbelow(*args): + """Portable ``&<|`` (spaceY) -> pymeos_cffi backing call.""" + return _dispatch('overbelow', _T_overbelow, args) + + +_T_overfront = {('STBox', 'STBox'): 'overfront_stbox_stbox', ('STBox', 'Temporal'): 'overfront_stbox_tspatial', ('Temporal', 'STBox'): 'overfront_tspatial_stbox', ('Temporal', 'Temporal'): 'overfront_tspatial_tspatial'} + + +def overfront(*args): + """Portable ``& pymeos_cffi backing call.""" + return _dispatch('overfront', _T_overfront, args) + + +_T_overlaps = {('Span', 'Temporal'): 'overlaps_tstzspan_temporal', ('Set', 'Set'): 'overlaps_set_set', ('Span', 'Span'): 'overlaps_span_span', ('Span', 'SpanSet'): 'overlaps_span_spanset', ('SpanSet', 'Span'): 'overlaps_spanset_span', ('SpanSet', 'SpanSet'): 'overlaps_spanset_spanset', ('STBox', 'STBox'): 'overlaps_stbox_stbox', ('STBox', 'Temporal'): 'overlaps_stbox_tspatial', ('TBox', 'TBox'): 'overlaps_tbox_tbox', ('TBox', 'Temporal'): 'overlaps_tbox_tnumber', ('Temporal', 'Temporal'): 'overlaps_tspatial_tspatial', ('Temporal', 'Span'): 'overlaps_tnumber_numspan', ('Temporal', 'TBox'): 'overlaps_tnumber_tbox', ('Temporal', 'STBox'): 'overlaps_tspatial_stbox'} + + +def overlaps(*args): + """Portable ``&&`` (topology) -> pymeos_cffi backing call.""" + return _dispatch('overlaps', _T_overlaps, args) + + +_T_overleft = {('bigint', 'Set'): 'overleft_bigint_set', ('bigint', 'Span'): 'overleft_bigint_span', ('bigint', 'SpanSet'): 'overleft_bigint_spanset', ('float', 'Set'): 'overleft_float_set', ('float', 'Span'): 'overleft_float_span', ('float', 'SpanSet'): 'overleft_float_spanset', ('int', 'Set'): 'overleft_int_set', ('int', 'Span'): 'overleft_int_span', ('int', 'SpanSet'): 'overleft_int_spanset', ('Span', 'Temporal'): 'overleft_numspan_tnumber', ('Set', 'bigint'): 'overleft_set_bigint', ('Set', 'float'): 'overleft_set_float', ('Set', 'int'): 'overleft_set_int', ('Set', 'Set'): 'overleft_set_set', ('Set', 'text'): 'overleft_set_text', ('Set', 'Datum'): 'overleft_set_value', ('Span', 'bigint'): 'overleft_span_bigint', ('Span', 'float'): 'overleft_span_float', ('Span', 'int'): 'overleft_span_int', ('Span', 'Span'): 'overleft_span_span', ('Span', 'SpanSet'): 'overleft_span_spanset', ('Span', 'Datum'): 'overleft_span_value', ('SpanSet', 'bigint'): 'overleft_spanset_bigint', ('SpanSet', 'float'): 'overleft_spanset_float', ('SpanSet', 'int'): 'overleft_spanset_int', ('SpanSet', 'Span'): 'overleft_spanset_span', ('SpanSet', 'SpanSet'): 'overleft_spanset_spanset', ('SpanSet', 'Datum'): 'overleft_spanset_value', ('STBox', 'STBox'): 'overleft_stbox_stbox', ('STBox', 'Temporal'): 'overleft_stbox_tspatial', ('TBox', 'TBox'): 'overleft_tbox_tbox', ('TBox', 'Temporal'): 'overleft_tbox_tnumber', ('text', 'Set'): 'overleft_text_set', ('Temporal', 'Span'): 'overleft_tnumber_numspan', ('Temporal', 'TBox'): 'overleft_tnumber_tbox', ('Temporal', 'Temporal'): 'overleft_tspatial_tspatial', ('Temporal', 'STBox'): 'overleft_tspatial_stbox', ('Datum', 'Set'): 'overleft_value_set', ('Datum', 'Span'): 'overleft_value_span', ('Datum', 'SpanSet'): 'overleft_value_spanset'} + + +def overleft(*args): + """Portable ``&<`` (spaceX) -> pymeos_cffi backing call.""" + return _dispatch('overleft', _T_overleft, args) + + +_T_overright = {('bigint', 'Set'): 'overright_bigint_set', ('bigint', 'Span'): 'overright_bigint_span', ('bigint', 'SpanSet'): 'overright_bigint_spanset', ('float', 'Set'): 'overright_float_set', ('float', 'Span'): 'overright_float_span', ('float', 'SpanSet'): 'overright_float_spanset', ('int', 'Set'): 'overright_int_set', ('int', 'Span'): 'overright_int_span', ('int', 'SpanSet'): 'overright_int_spanset', ('Span', 'Temporal'): 'overright_numspan_tnumber', ('Set', 'bigint'): 'overright_set_bigint', ('Set', 'float'): 'overright_set_float', ('Set', 'int'): 'overright_set_int', ('Set', 'Set'): 'overright_set_set', ('Set', 'text'): 'overright_set_text', ('Set', 'Datum'): 'overright_set_value', ('Span', 'bigint'): 'overright_span_bigint', ('Span', 'float'): 'overright_span_float', ('Span', 'int'): 'overright_span_int', ('Span', 'Span'): 'overright_span_span', ('Span', 'SpanSet'): 'overright_span_spanset', ('Span', 'Datum'): 'overright_span_value', ('SpanSet', 'bigint'): 'overright_spanset_bigint', ('SpanSet', 'float'): 'overright_spanset_float', ('SpanSet', 'int'): 'overright_spanset_int', ('SpanSet', 'Span'): 'overright_spanset_span', ('SpanSet', 'SpanSet'): 'overright_spanset_spanset', ('SpanSet', 'Datum'): 'overright_spanset_value', ('STBox', 'STBox'): 'overright_stbox_stbox', ('STBox', 'Temporal'): 'overright_stbox_tspatial', ('TBox', 'TBox'): 'overright_tbox_tbox', ('TBox', 'Temporal'): 'overright_tbox_tnumber', ('text', 'Set'): 'overright_text_set', ('Temporal', 'Span'): 'overright_tnumber_numspan', ('Temporal', 'TBox'): 'overright_tnumber_tbox', ('Temporal', 'Temporal'): 'overright_tspatial_tspatial', ('Temporal', 'STBox'): 'overright_tspatial_stbox', ('Datum', 'Set'): 'overright_value_set', ('Datum', 'Span'): 'overright_value_span', ('Datum', 'SpanSet'): 'overright_value_spanset'} + + +def overright(*args): + """Portable ``&>`` (spaceX) -> pymeos_cffi backing call.""" + return _dispatch('overright', _T_overright, args) + + +_T_right = {('bigint', 'Set'): 'right_bigint_set', ('bigint', 'Span'): 'right_bigint_span', ('bigint', 'SpanSet'): 'right_bigint_spanset', ('float', 'Set'): 'right_float_set', ('float', 'Span'): 'right_float_span', ('float', 'SpanSet'): 'right_float_spanset', ('int', 'Set'): 'right_int_set', ('int', 'Span'): 'right_int_span', ('int', 'SpanSet'): 'right_int_spanset', ('Span', 'Temporal'): 'right_numspan_tnumber', ('Set', 'bigint'): 'right_set_bigint', ('Set', 'float'): 'right_set_float', ('Set', 'int'): 'right_set_int', ('Set', 'Set'): 'right_set_set', ('Set', 'text'): 'right_set_text', ('Set', 'Datum'): 'right_set_value', ('Span', 'bigint'): 'right_span_bigint', ('Span', 'float'): 'right_span_float', ('Span', 'int'): 'right_span_int', ('Span', 'Span'): 'right_span_span', ('Span', 'SpanSet'): 'right_span_spanset', ('Span', 'Datum'): 'right_span_value', ('SpanSet', 'bigint'): 'right_spanset_bigint', ('SpanSet', 'float'): 'right_spanset_float', ('SpanSet', 'int'): 'right_spanset_int', ('SpanSet', 'Span'): 'right_spanset_span', ('SpanSet', 'SpanSet'): 'right_spanset_spanset', ('SpanSet', 'Datum'): 'right_spanset_value', ('STBox', 'STBox'): 'right_stbox_stbox', ('STBox', 'Temporal'): 'right_stbox_tspatial', ('TBox', 'TBox'): 'right_tbox_tbox', ('TBox', 'Temporal'): 'right_tbox_tnumber', ('text', 'Set'): 'right_text_set', ('Temporal', 'Span'): 'right_tnumber_numspan', ('Temporal', 'TBox'): 'right_tnumber_tbox', ('Temporal', 'Temporal'): 'right_tspatial_tspatial', ('Temporal', 'STBox'): 'right_tspatial_stbox', ('Datum', 'Set'): 'right_value_set', ('Datum', 'Span'): 'right_value_span', ('Datum', 'SpanSet'): 'right_value_spanset'} + + +def right(*args): + """Portable ``>>`` (spaceX) -> pymeos_cffi backing call.""" + return _dispatch('right', _T_right, args) + + +_T_same = {('Span', 'Temporal'): 'same_tstzspan_temporal', ('STBox', 'STBox'): 'same_stbox_stbox', ('STBox', 'Temporal'): 'same_stbox_tspatial', ('TBox', 'TBox'): 'same_tbox_tbox', ('TBox', 'Temporal'): 'same_tbox_tnumber', ('Temporal', 'Temporal'): 'same_tspatial_tspatial', ('Temporal', 'Span'): 'same_tnumber_numspan', ('Temporal', 'TBox'): 'same_tnumber_tbox', ('Temporal', 'STBox'): 'same_tspatial_stbox'} + + +def same(*args): + """Portable ``~=`` (same) -> pymeos_cffi backing call.""" + return _dispatch('same', _T_same, args) + + +_T_tdistance = {('Temporal', 'Cbuffer'): 'tdistance_tcbuffer_cbuffer', ('Temporal', 'GSERIALIZED'): 'tdistance_trgeo_geo', ('Temporal', 'Temporal'): 'tdistance_trgeo_trgeo', ('Temporal', 'float'): 'tdistance_tfloat_float', ('Temporal', 'int'): 'tdistance_tint_int', ('Temporal', 'Npoint'): 'tdistance_tnpoint_npoint', ('Temporal', 'Datum'): 'tdistance_tnumber_number', ('Temporal', 'Pose'): 'tdistance_tpose_pose'} + + +def tdistance(*args): + """Portable ``<->`` (distance) -> pymeos_cffi backing call.""" + return _dispatch('tdistance', _T_tdistance, args) + + +_T_teq = {('bool', 'Temporal'): 'teq_bool_tbool', ('Cbuffer', 'Temporal'): 'teq_cbuffer_tcbuffer', ('float', 'Temporal'): 'teq_float_tfloat', ('GSERIALIZED', 'Temporal'): 'teq_geo_trgeo', ('int', 'Temporal'): 'teq_int_tint', ('Jsonb', 'Temporal'): 'teq_jsonb_tjsonb', ('Pose', 'Temporal'): 'teq_pose_tpose', ('Temporal', 'bool'): 'teq_tbool_bool', ('Temporal', 'Cbuffer'): 'teq_tcbuffer_cbuffer', ('Temporal', 'Temporal'): 'teq_temporal_temporal', ('text', 'Temporal'): 'teq_text_ttext', ('Temporal', 'float'): 'teq_tfloat_float', ('Temporal', 'GSERIALIZED'): 'teq_trgeo_geo', ('Temporal', 'int'): 'teq_tint_int', ('Temporal', 'Jsonb'): 'teq_tjsonb_jsonb', ('Temporal', 'Npoint'): 'teq_tnpoint_npoint', ('Temporal', 'Pose'): 'teq_tpose_pose', ('Temporal', 'text'): 'teq_ttext_text'} + + +def teq(*args): + """Portable ``#=`` (temporalComparison) -> pymeos_cffi backing call.""" + return _dispatch('teq', _T_teq, args) + + +_T_tge = {('float', 'Temporal'): 'tge_float_tfloat', ('int', 'Temporal'): 'tge_int_tint', ('Temporal', 'Temporal'): 'tge_temporal_temporal', ('text', 'Temporal'): 'tge_text_ttext', ('Temporal', 'float'): 'tge_tfloat_float', ('Temporal', 'int'): 'tge_tint_int', ('Temporal', 'text'): 'tge_ttext_text'} + + +def tge(*args): + """Portable ``#>=`` (temporalComparison) -> pymeos_cffi backing call.""" + return _dispatch('tge', _T_tge, args) + + +_T_tgt = {('float', 'Temporal'): 'tgt_float_tfloat', ('int', 'Temporal'): 'tgt_int_tint', ('Temporal', 'Temporal'): 'tgt_temporal_temporal', ('text', 'Temporal'): 'tgt_text_ttext', ('Temporal', 'float'): 'tgt_tfloat_float', ('Temporal', 'int'): 'tgt_tint_int', ('Temporal', 'text'): 'tgt_ttext_text'} + + +def tgt(*args): + """Portable ``#>`` (temporalComparison) -> pymeos_cffi backing call.""" + return _dispatch('tgt', _T_tgt, args) + + +_T_tle = {('float', 'Temporal'): 'tle_float_tfloat', ('int', 'Temporal'): 'tle_int_tint', ('Temporal', 'Temporal'): 'tle_temporal_temporal', ('text', 'Temporal'): 'tle_text_ttext', ('Temporal', 'float'): 'tle_tfloat_float', ('Temporal', 'int'): 'tle_tint_int', ('Temporal', 'text'): 'tle_ttext_text'} + + +def tle(*args): + """Portable ``#<=`` (temporalComparison) -> pymeos_cffi backing call.""" + return _dispatch('tle', _T_tle, args) + + +_T_tlt = {('float', 'Temporal'): 'tlt_float_tfloat', ('int', 'Temporal'): 'tlt_int_tint', ('Temporal', 'Temporal'): 'tlt_temporal_temporal', ('text', 'Temporal'): 'tlt_text_ttext', ('Temporal', 'float'): 'tlt_tfloat_float', ('Temporal', 'int'): 'tlt_tint_int', ('Temporal', 'text'): 'tlt_ttext_text'} + + +def tlt(*args): + """Portable ``#<`` (temporalComparison) -> pymeos_cffi backing call.""" + return _dispatch('tlt', _T_tlt, args) + + +_T_tne = {('bool', 'Temporal'): 'tne_bool_tbool', ('Cbuffer', 'Temporal'): 'tne_cbuffer_tcbuffer', ('float', 'Temporal'): 'tne_float_tfloat', ('GSERIALIZED', 'Temporal'): 'tne_geo_trgeo', ('int', 'Temporal'): 'tne_int_tint', ('Jsonb', 'Temporal'): 'tne_jsonb_tjsonb', ('Pose', 'Temporal'): 'tne_pose_tpose', ('Temporal', 'bool'): 'tne_tbool_bool', ('Temporal', 'Cbuffer'): 'tne_tcbuffer_cbuffer', ('Temporal', 'Temporal'): 'tne_temporal_temporal', ('text', 'Temporal'): 'tne_text_ttext', ('Temporal', 'float'): 'tne_tfloat_float', ('Temporal', 'GSERIALIZED'): 'tne_trgeo_geo', ('Temporal', 'int'): 'tne_tint_int', ('Temporal', 'Jsonb'): 'tne_tjsonb_jsonb', ('Temporal', 'Npoint'): 'tne_tnpoint_npoint', ('Temporal', 'Pose'): 'tne_tpose_pose', ('Temporal', 'text'): 'tne_ttext_text'} + + +def tne(*args): + """Portable ``#<>`` (temporalComparison) -> pymeos_cffi backing call.""" + return _dispatch('tne', _T_tne, args) diff --git a/tests/test_portable_parity.py b/tests/test_portable_parity.py new file mode 100644 index 00000000..2915c4e3 --- /dev/null +++ b/tests/test_portable_parity.py @@ -0,0 +1,160 @@ +"""Portable bare-name parity gate for PyMEOS. + +The canonical portable dialect (RFC #920; contract: +MobilityDB/MEOS-API ``meta/portable-aliases.json``, vendored verbatim under +``tools/portable_aliases/``) is the single cross-binding source of truth. +This test is the PyMEOS analogue of MobilityDB's +``tools/portable_aliases/generate.py --check`` and MEOS-API's +``portable_parity.py``: it asserts that the generated ``pymeos.portable`` +API exposes EVERY canonical bare name, each backed by a real pymeos_cffi +function, across all six in-scope type families -- 0 unbacked, zero +per-binding exceptions. It runs without the compiled binding (operating on +the contract + the committed generated module + the MEOS function universe), +and additionally verifies against the live pymeos_cffi when present. + + python3 -m pytest tests/test_portable_parity.py + python3 tests/test_portable_parity.py +""" + +import ast +import sys +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +TOOLS = ROOT / "tools" / "portable_aliases" +sys.path.insert(0, str(TOOLS)) + +from portable import attach_portable_aliases # vendored from SoT +from portable_parity import build_parity # vendored from SoT +import generate as gen + +CONTRACT = TOOLS / "portable-aliases.json" +GENERATED = ROOT / "pymeos" / "portable.py" + + +def _contract(): + return attach_portable_aliases({"functions": []}, CONTRACT)["portableAliases"] + + +def _resolve_universe(): + """(universe, source). Live pymeos_cffi if built (CI), else the in-tree + MEOS headers, else (None, None) -> universe-dependent checks skip.""" + try: + return gen.universe_from_cffi(), "pymeos_cffi" + except Exception: + pass + dirs = gen._default_header_dirs() + if dirs: + return gen.universe_from_headers(dirs), f"headers:{dirs}" + return None, None + + +def _committed_all_and_tables(): + """AST-read the committed module without importing it (no binding needed).""" + tree = ast.parse(GENERATED.read_text()) + all_names, tables = None, {} + for node in tree.body: + if not isinstance(node, ast.Assign): + continue + name = getattr(node.targets[0], "id", "") + if name == "__all__": + all_names = ast.literal_eval(node.value) + elif name.startswith("_T_"): + tables[name[3:]] = ast.literal_eval(node.value) + return all_names, tables + + +class ContractIntegrity(unittest.TestCase): + def test_29_bijective_pairs(self): + pa = _contract() + self.assertEqual(pa["count"], 29) + self.assertEqual(len(pa["bareNames"]), 29) + self.assertEqual(len(pa["byOperator"]), 29) + self.assertEqual(len(pa["byBareName"]), 29) + self.assertEqual( + pa["scope"]["inScopeTypeFamilies"], + ["temporal", "geo", "cbuffer", "npoint", "pose", "rgeo"]) + + +class CommittedModule(unittest.TestCase): + """The shipped pymeos/portable.py must expose every canonical bare name + with a real, non-empty backing table -- zero per-binding exceptions.""" + + def test_exposes_every_bare_name(self): + pa = _contract() + all_names, tables = _committed_all_and_tables() + self.assertEqual(sorted(all_names), sorted(pa["bareNames"])) + self.assertEqual(set(tables), set(pa["bareNames"])) + empty = sorted(b for b, t in tables.items() if not t) + self.assertEqual(empty, [], f"bare names with no backing: {empty}") + + def test_every_entry_routes_to_a_real_symbol(self): + _, tables = _committed_all_and_tables() + for bare, tbl in tables.items(): + for key, sym in tbl.items(): + self.assertIsInstance(sym, str) + self.assertRegex(sym, r"^[a-z][a-z0-9_]+$", + f"{bare}: bad backing symbol {sym!r}") + + +@unittest.skipIf(_resolve_universe()[0] is None, + "no pymeos_cffi and no MEOS headers available") +class ParityGate(unittest.TestCase): + """Byte-identical to the MEOS-API SoT parity logic: 29/29, 0 unbacked.""" + + def setUp(self): + self.universe, self.source = _resolve_universe() + + def test_29_of_29_backed_zero_unbacked(self): + rep = build_parity(attach_portable_aliases( + {"functions": [{"name": n} for n in self.universe]}, CONTRACT)) + self.assertEqual(rep["total"], 29) + self.assertEqual(rep["needsExplicitBacking"], 0, + f"unbacked: {rep['unbacked']}") + self.assertEqual(rep["backed"], 29) + self.assertEqual(rep["parityPct"], 100.0) + + def test_all_six_families_present(self): + pa = _contract() + in_scope = pa["scope"]["inScopeTypeFamilies"] + fam_re = gen.family_regexes(in_scope) + witnessed = set() + for bare in pa["bareNames"]: + syms, _ = gen.backing_symbols( + bare, pa.get("explicitBacking", {}).get(bare, []), + self.universe) + for s in syms: + for fam, rx in fam_re.items(): + if rx.search(s): + witnessed.add(fam) + missing = [f for f in in_scope if f not in witnessed] + self.assertEqual(missing, [], f"families with no backing: {missing}") + + +@unittest.skipIf(_resolve_universe()[0] is None, + "no pymeos_cffi and no MEOS headers available") +class CommittedModuleValidity(unittest.TestCase): + """Every backing symbol the shipped module dispatches to must exist in + the resolved universe -- i.e. the committed ``pymeos/portable.py`` is + valid for the binding actually under test (catches hand edits and stale + drift that would break runtime, while tolerating benign MEOS overload + additions across versions, unlike a brittle byte-for-byte snapshot). + """ + + def test_every_committed_symbol_is_real(self): + universe, _ = _resolve_universe() + _, tables = _committed_all_and_tables() + unknown = sorted({ + sym for tbl in tables.values() for sym in tbl.values() + if sym not in universe + }) + self.assertEqual( + unknown, [], + f"pymeos/portable.py dispatches to {len(unknown)} symbol(s) " + f"absent from the binding -- regenerate with " + f"tools/portable_aliases/generate.py: {unknown[:10]}") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tools/portable_aliases/README.md b/tools/portable_aliases/README.md new file mode 100644 index 00000000..583966e5 --- /dev/null +++ b/tools/portable_aliases/README.md @@ -0,0 +1,48 @@ +# Portable bare-name dialect (PyMEOS) + +PyMEOS exposes every MobilityDB operator under a single **portable bare +name** (`overlaps`, `contains`, `teq`, `nearestApproachDistance`, …) so one +query reads the same across every binding/engine. + +## Source of truth + +`portable-aliases.json` is the canonical contract, vendored **verbatim** +from `MobilityDB/MEOS-API` (`meta/portable-aliases.json`, RFC #920; +discussion MobilityDB #861; native in MobilityDB via the 1303-alias +PR #1075). `portable.py` and `portable_parity.py` are likewise vendored +verbatim from MEOS-API so PyMEOS's parity logic is byte-identical to the +ecosystem source of truth. Update them only by re-vendoring from MEOS-API. + +The mapping is **29 operator → bare-name pairs**, type-agnostic across all +six in-scope temporal type families: `temporal, geo, cbuffer, npoint, pose, +rgeo`. + +## What is generated + +`pymeos/portable.py` (GENERATED — do not edit) defines one callable per +bare name. Each dispatches, by the runtime C type of its arguments, to the +**exact `pymeos_cffi` function** the operator is backed by — the same native +call the type-qualified API uses. Nothing is reimplemented: every bare name +is identical to its operator by construction. + +## Regenerate + +``` +python3 tools/portable_aliases/generate.py # uses MEOS at the + # install prefix +python3 tools/portable_aliases/generate.py --check # parity gate +python3 tools/portable_aliases/generate.py --cffi --check # also assert the + # live built binding +``` + +`MEOS_INCLUDE=` (or `--headers `) selects the MEOS headers; the +default is the install prefix, then a sibling MobilityDB checkout. + +## Gate + +`tests/test_portable_parity.py` (run by `pytest`, and the explicit CI step) +asserts: the contract is 29 bijective pairs; the generated API exposes every +bare name with a non-empty backing table; parity is **29/29 backed, 0 +unbacked** (byte-identical to the MEOS-API logic); all six in-scope families +are witnessed; and every committed backing symbol exists in the binding +under test. Zero per-binding exceptions. diff --git a/tools/portable_aliases/generate.py b/tools/portable_aliases/generate.py new file mode 100644 index 00000000..18608956 --- /dev/null +++ b/tools/portable_aliases/generate.py @@ -0,0 +1,423 @@ +#!/usr/bin/env python3 +# Copyright (c) 2016-2026, Université libre de Bruxelles and PyMEOS +# contributors. Licensed under the PostgreSQL License (see LICENSE). +# +# Generate the portable bare-name dialect for PyMEOS. +# +# The canonical portable bare-name mapping (RFC #920, MobilityDB #861) is the +# single cross-binding codegen source of truth: every binding/engine exposes +# the SAME bare names (overlaps, contains, teq, ...) so a user learns one +# reference and assumes the rest. The mapping is type-agnostic -- it applies +# to every temporal type family. +# +# This tool reads +# * the vendored contract tools/portable_aliases/portable-aliases.json +# (verbatim from MobilityDB/MEOS-API; folded into the catalog shape with +# the vendored attach_portable_aliases, byte-identical to the SoT), and +# * the MEOS function universe -- the exact C functions PyMEOS exposes +# through pymeos_cffi -- obtained from the MEOS headers (default) or, in +# CI where pymeos_cffi is built, by introspecting it directly, +# and emits pymeos/portable.py : one callable per canonical bare name that +# DISPATCHES, by the runtime C type of its arguments, to the EXACT pymeos_cffi +# backing function the corresponding operator uses. No behaviour is +# reimplemented -- every leaf is a direct pymeos_cffi call, so each bare name +# is identical to the operator by construction. +# +# Usage: +# python3 tools/portable_aliases/generate.py --headers \ +# [--out pymeos/portable.py] [--report build/portable_parity.json] +# python3 tools/portable_aliases/generate.py --cffi --check # CI gate + +import argparse +import json +import os +import re +import sys +from collections import defaultdict +from pathlib import Path + +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE)) +from portable import attach_portable_aliases # vendored from MEOS-API (SoT) +from portable_parity import build_parity # vendored from MEOS-API (SoT) + +CONTRACT = HERE / "portable-aliases.json" + +# Param/return C base type -> the runtime token the generated dispatcher keys +# on. MEOS passes every temporal subtype as `const Temporal *`, so all +# temporal overloads of an operator collapse onto one generic symbol keyed by +# `Temporal` -- which is why this stays small and unambiguous. +SCALAR_PY_TOKEN = { + "bool": "bool", "int": "int", "int32": "int", "int64": "bigint", + "long": "bigint", "double": "float", "float8": "float", + "text": "text", "TimestampTz": "timestamptz", "Timestamp": "timestamptz", + "DateADT": "date", "Interval": "interval", +} + +# Header declaration: extern (); (one per line in MEOS) +_DECL = re.compile( + r"^\s*extern\s+(.+?[\s*])\b([A-Za-z_]\w*)\s*\(([^;{]*)\)\s*;", re.M) + + +def _base_ctype(decl: str) -> str: + """Last type identifier of a C declarator, sans const / * / arg name.""" + toks = re.findall(r"[A-Za-z_]\w*", decl.replace("const", " ")) + if not toks: + return "" + # `const STBox *box1` -> ['STBox','box1']; `int i` -> ['int','i']; + # `double` -> ['double']. The base type is the token before the name, + # or the only token. A pointer means the prior token is the type. + if "*" in decl: + return toks[0] + return toks[0] if len(toks) == 1 else toks[-2] if len(toks) > 1 else toks[0] + + +def _params(raw: str): + raw = raw.strip() + if not raw or raw == "void": + return () + out = [] + for p in raw.split(","): + p = p.strip() + if p in ("...", ""): + out.append("...") + continue + out.append(_base_ctype(p)) + return tuple(out) + + +def universe_from_headers(dirs): + """{name: (ret_base, (param_base, ...))} parsed from MEOS *.h files.""" + funcs = {} + files = [] + for d in dirs: + d = Path(d) + if d.is_file(): + files.append(d) + elif d.is_dir(): + files += sorted(d.glob("meos*.h")) + if not files: + sys.exit(f"no MEOS headers found in {dirs} -- pass --headers ") + for fp in files: + text = fp.read_text(encoding="utf-8", errors="ignore") + text = re.sub(r"/\*.*?\*/", " ", text, flags=re.S) + text = re.sub(r"//.*", " ", text) + for m in _DECL.finditer(text): + ret, name, params = m.group(1), m.group(2), m.group(3) + funcs[name] = (_base_ctype(ret + " r"), _params(params)) + return funcs + + +def universe_from_cffi(): + """Names actually exported by the installed pymeos_cffi (CI path).""" + import pymeos_cffi # noqa: only available where the binding is built + return { + n: (None, None) + for n in dir(pymeos_cffi) + if not n.startswith("_") and callable(getattr(pymeos_cffi, n)) + } + + +def backing_symbols(bare, explicit_prefixes, universe): + """Mirror portable_parity: name == bare or startswith bare+'_'; else the + verified explicit-backing C prefixes (e.g. nearestApproachDistance->nad).""" + def match(pfx): + return [n for n in universe if n == pfx or n.startswith(pfx + "_")] + + hits, via = match(bare), "prefix" + if not hits: + for pfx in explicit_prefixes: + hits += match(pfx) + via = "explicit" if hits else None + return sorted(set(hits)), via + + +def token(ctype): + """C base type -> dispatcher token (Temporal stays Temporal; scalars + fold to a stable Python-facing token).""" + return SCALAR_PY_TOKEN.get(ctype, ctype) + + +# Each in-scope type family is *witnessed* when some backing symbol of some +# canonical bare name carries that family's MEOS name token -- the same +# per-family convention MobilityDB's tools/portable_aliases/generate.py and +# its six mobilitydb/sql//..._portable_aliases.in.sql files use. This +# proves cbuffer/npoint/pose/rgeo are covered exactly like every other type +# (the hard invariant: never a headline exclusion), without inventing a gate +# that diverges from the SoT's type-agnostic 29/29 parity. +FAMILY_PATTERNS = { + "temporal": r"(^|_)(temporal|tspatial|tnumber|tbool|tint|tfloat|ttext)(_|$)", + "geo": r"(^|_)(geo|tgeo|geom|geog|tgeompoint|tgeogpoint|tpoint)(_|$)", + "cbuffer": r"cbuffer", + "npoint": r"npoint", + "pose": r"(^|_)t?pose(_|$)", + "rgeo": r"(t?rgeo|trgeometry|rgeometry)", +} + + +def family_regexes(in_scope): + return {f: re.compile(FAMILY_PATTERNS.get(f, re.escape(f)), re.I) + for f in in_scope} + + +HEADER = '''\ +# Copyright (c) 2016-2026, Université libre de Bruxelles and PyMEOS +# contributors. Licensed under the PostgreSQL License (see LICENSE). +# +# ============================================================================ +# GENERATED by tools/portable_aliases/generate.py -- DO NOT EDIT. +# Regenerate: python3 tools/portable_aliases/generate.py --headers +# ============================================================================ +# +# The portable bare-name dialect (RFC #920; canonical contract: +# MobilityDB/MEOS-API meta/portable-aliases.json; native in MobilityDB via +# the 1303-alias PR #1075). Every binding exposes the IDENTICAL bare names +# from that one mapping, type-agnostically across all six temporal type +# families (temporal, geo, cbuffer, npoint, pose, rgeo). +# +# Each bare name dispatches, by the runtime C type of its arguments, to the +# EXACT pymeos_cffi function the operator is backed by -- the same native +# call the type-qualified API uses. Nothing is reimplemented: every bare +# name is identical to its operator by construction. +"""Portable bare-name dialect for PyMEOS (generated -- see module comment).""" +from __future__ import annotations + +import datetime as _dt + +# Resolved at call time so importing this module never requires the compiled +# binding to be present (e.g. docs / parity checks). +_cffi = None + + +def _pymeos_cffi(): + global _cffi + if _cffi is None: + import pymeos_cffi as _c + + _cffi = _c + return _cffi + + +def _unwrap(a): + return a._inner if hasattr(a, "_inner") else a + + +def _arg_token(a): + """Runtime C base type of an argument, mirroring pymeos.factory's cdata + field-sniffing (no ffi object needed) plus Python scalars.""" + if isinstance(a, bool): + return "bool" + if isinstance(a, int): + return "int" + if isinstance(a, float): + return "float" + if isinstance(a, str): + return "text" + if isinstance(a, _dt.datetime): + return "timestamptz" + if isinstance(a, _dt.date): + return "date" + if isinstance(a, _dt.timedelta): + return "interval" + o = _unwrap(a) + for attr, tok in ( + ("temptype", "Temporal"), # every temporal subtype (6 families) + ("spansettype", "SpanSet"), + ("spantype", "Span"), + ("settype", "Set"), + ): + if hasattr(o, attr): + return tok + # Boxes / geo / spatial base types: distinguishing struct fields. + for attr, tok in ( + ("srid", "STBox"), # STBox carries an srid; TBox does not + ("period", "TBox"), + ("gflags", "GSERIALIZED"), + ("rid", "Npoint"), + ("radius", "Cbuffer"), + ): + if hasattr(o, attr): + return tok + return type(a).__name__ + + +def _wrap(res): + """Re-wrap MEOS-object results with the existing PyMEOS factories; pass + scalars (bool/float/int/str) and None straight through.""" + if res is None or isinstance(res, (bool, int, float, str, bytes)): + return res + from .factory import _TemporalFactory, _CollectionFactory + + for fn in (_TemporalFactory.create_temporal, + _CollectionFactory.create_collection): + try: + return fn(res) + except Exception: + pass + return res + + +def _dispatch(name, table, args): + key = tuple(_arg_token(a) for a in args) + sym = table.get(key) + if sym is None and len(key) == 2: # symmetric fallback + sym = table.get((key[1], key[0])) + if sym is None: + raise TypeError( + f"{name}(): no portable overload for argument types {key}; " + f"supported: {sorted(table)}" + ) + fn = getattr(_pymeos_cffi(), sym) + return _wrap(fn(*[_unwrap(a) for a in args])) +''' + +FUNC_TMPL = ''' + +_T_{bare} = {table!r} + + +def {bare}(*args): + """Portable ``{op}`` ({family}) -> pymeos_cffi backing call.""" + return _dispatch({bare!r}, _T_{bare}, args) +''' + + +def main(): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--headers", action="append", default=[], + help="MEOS include dir / header (repeatable)") + ap.add_argument("--cffi", action="store_true", + help="introspect the installed pymeos_cffi (CI gate)") + ap.add_argument("--contract", default=str(CONTRACT)) + ap.add_argument("--out", default=str(HERE.parents[1] / "pymeos" + / "portable.py")) + ap.add_argument("--report", default="") + ap.add_argument("--check", action="store_true", + help="exit non-zero unless 29/29 backed across 6 families") + args = ap.parse_args() + + idl = attach_portable_aliases({"functions": []}, Path(args.contract)) + pa = idl["portableAliases"] + by_bare = pa["byBareName"] # bareName -> operator + fam_of = {p["bareName"]: fam + for fam, lst in pa["families"].items() for p in lst} + explicit = pa.get("explicitBacking", {}) + in_scope = pa["scope"]["inScopeTypeFamilies"] + + # Codegen needs C signatures (to key dispatch on argument C types), so + # the universe is ALWAYS the MEOS headers. In CI the headers are the + # freshly built+installed MEOS at the install prefix, so the committed + # module stays in lock-step with the binding. --cffi adds a *separate* + # name-level assertion against the live built binding. + hdrs = args.headers or _default_header_dirs() + universe = universe_from_headers(hdrs) + src = f"headers:{hdrs}" + + # Parity (name-level, byte-identical to the MEOS-API SoT logic). + parity = build_parity( + attach_portable_aliases( + {"functions": [{"name": n} for n in universe]}, + Path(args.contract))) + + cffi_parity = None + if args.cffi: + try: + cffi_u = universe_from_cffi() + cffi_parity = build_parity(attach_portable_aliases( + {"functions": [{"name": n} for n in cffi_u]}, + Path(args.contract))) + except Exception as e: # binding not built + cffi_parity = {"error": str(e)} + + # Codegen tables: bare -> {(arg C-type tokens): symbol}. + fam_re = family_regexes(in_scope) + tables, family_witness = {}, defaultdict(set) + for bare in sorted(by_bare): + syms, _ = backing_symbols(bare, explicit.get(bare, []), universe) + tbl = {} + for s in syms: + for fam, rx in fam_re.items(): + if rx.search(s): + family_witness[fam].add(bare) + ret, params = universe.get(s, (None, None)) + if params is None: # cffi path: name only, no signature + continue + tbl[tuple(token(p) for p in params)] = s + tables[bare] = tbl + + backed = parity["backed"] + total = parity["total"] + missing_fam = [f for f in in_scope if not family_witness.get(f)] + + body = [HEADER, f"\n__all__ = {sorted(by_bare)!r}\n"] + for bare in sorted(by_bare): + body.append(FUNC_TMPL.format( + bare=bare, op=by_bare[bare], family=fam_of[bare], + table=tables[bare])) + Path(args.out).write_text("".join(body), encoding="utf-8") + + report = { + "source": src, + "universeSize": len(universe), + "parity": {k: parity[k] for k in + ("total", "backed", "needsExplicitBacking", "parityPct")}, + "unbacked": parity["unbacked"], + "inScopeFamilies": in_scope, + "familyWitness": {k: sorted(v) for k, v in family_witness.items()}, + "familiesMissing": missing_fam, + "cffiParity": (None if cffi_parity is None else + {k: cffi_parity.get(k) for k in + ("total", "backed", "needsExplicitBacking", + "unbacked", "error")}), + "out": args.out, + } + if args.report: + Path(args.report).parent.mkdir(parents=True, exist_ok=True) + Path(args.report).write_text(json.dumps(report, indent=2)) + + print(f"[portable] source={src} universe={len(universe)} " + f"backed={backed}/{total} ({parity['parityPct']}%) " + f"unbacked={parity['unbacked']} families_missing={missing_fam}") + print(f"[portable] wrote {args.out} ({len(by_bare)} bare names)") + + cffi_fail = False + if cffi_parity is not None: + if "error" in cffi_parity and cffi_parity.get("error"): + print(f"[portable] cffi check: binding not importable " + f"({cffi_parity['error']}) -- skipped") + else: + cffi_fail = (bool(cffi_parity["unbacked"]) + or cffi_parity["backed"] != cffi_parity["total"]) + print(f"[portable] cffi check: " + f"{cffi_parity['backed']}/{cffi_parity['total']} backed, " + f"unbacked={cffi_parity['unbacked']}") + + fail = (bool(parity["unbacked"]) or backed != total or bool(missing_fam) + or cffi_fail) + if args.check: + print("CHECK: " + ("FAIL" if fail else + f"PASS - {total}/{total} backed, 0 unbacked, " + f"all {len(in_scope)} families present")) + return 1 if fail else 0 + return 0 + + +def _default_header_dirs(): + """The single MEOS include dir to generate from, deterministically. + + Install-prefix first so a local run matches CI, where the workflow + builds + installs MEOS to the prefix (``/usr/local`` | ``/opt/homebrew``) + before regenerating; ``MEOS_INCLUDE`` overrides; the sibling MobilityDB + checkout is the last resort. A single dir keeps the committed artifact + reproducible across machines. + """ + for c in (os.environ.get("MEOS_INCLUDE"), + "/usr/local/include", "/opt/homebrew/include", + HERE.parents[2] / "MobilityDB" / "meos" / "include"): + if c and Path(c).is_dir() and any(Path(c).glob("meos*.h")): + return [str(c)] + return [] + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/portable_aliases/portable-aliases.json b/tools/portable_aliases/portable-aliases.json new file mode 100644 index 00000000..1cabac15 --- /dev/null +++ b/tools/portable_aliases/portable-aliases.json @@ -0,0 +1,60 @@ +{ + "_comment": "Canonical portable bare-name dialect — the single codegen source of truth (RFC #920). Every binding/engine generates the SAME bare names from this mapping so users learn one reference and assume the rest. Operators are SQL operator symbols; bareName is the portable function name. The mapping is type-agnostic: it applies to EVERY temporal type family.", + "provenance": { + "discussion": "MobilityDB#861", + "rfc": "MobilityDB RFC #920 (doc/rfc/sql-portability/README.md, branch rfc/sql-portability)", + "nativePR": "MobilityDB#1075 (1303 operator-overload aliases, each reusing the operator's own C symbol — identical by construction; CI-gated by tools/portable_aliases/generate.py --check)", + "manualChapter": "MobilityDB#1078" + }, + "families": { + "topology": [{"operator": "&&", "bareName": "overlaps"}, + {"operator": "@>", "bareName": "contains"}, + {"operator": "<@", "bareName": "contained"}, + {"operator": "-|-", "bareName": "adjacent"}], + "timePosition": [{"operator": "<<#", "bareName": "before"}, + {"operator": "#>>", "bareName": "after"}, + {"operator": "&<#", "bareName": "overbefore"}, + {"operator": "#&>", "bareName": "overafter"}], + "spaceX": [{"operator": "<<", "bareName": "left"}, + {"operator": ">>", "bareName": "right"}, + {"operator": "&<", "bareName": "overleft"}, + {"operator": "&>", "bareName": "overright"}], + "spaceY": [{"operator": "<<|", "bareName": "below"}, + {"operator": "|>>", "bareName": "above"}, + {"operator": "&<|", "bareName": "overbelow"}, + {"operator": "|&>", "bareName": "overabove"}], + "spaceZ": [{"operator": "<>", "bareName": "back"}, + {"operator": "&", "bareName": "overback"}], + "temporalComparison": [{"operator": "#=", "bareName": "teq"}, + {"operator": "#<>", "bareName": "tne"}, + {"operator": "#<", "bareName": "tlt"}, + {"operator": "#<=", "bareName": "tle"}, + {"operator": "#>", "bareName": "tgt"}, + {"operator": "#>=", "bareName": "tge"}], + "distance": [{"operator": "<->", "bareName": "tdistance"}, + {"operator": "|=|", "bareName": "nearestApproachDistance"}], + "same": [{"operator": "~=", "bareName": "same"}] + }, + "alreadyCanonical": [ + {"family": "ever", "operators": ["?="], "pattern": "ever_*"}, + {"family": "always", "operators": ["%="], "pattern": "always_*"}, + {"functions": ["eIntersects", "atTime", "restriction functions", + "spatial-relationship functions"]} + ], + "_explicitBackingComment": "Bare names whose MEOS C family prefix differs from the bare name itself. Verified against the catalog (not guessed): `nearestApproachDistance` is backed by the `nad_*` family (35 functions). Lets the parity audit resolve 100% honestly instead of false-flagging a real, present family.", + "explicitBacking": { + "nearestApproachDistance": ["nad"] + }, + "scope": { + "inScopeTypeFamilies": ["temporal", "geo", "cbuffer", "npoint", "pose", + "rgeo"], + "note": "cbuffer / npoint / pose / rgeo are FULL user-facing temporal types and ARE in scope — covered like every other type. PR #1075 already aliases all six families (1303 aliases). They must NOT be excluded from any parity headline; an upstream/audit note that 'defers' or 'jointly excludes' them is a known error being corrected — where another engine defers them, that is incomplete work to close (a gap with a plan), never an accepted exclusion." + }, + "notes": [ + "Generate aliases by reusing each operator's own backing C function (equivalence by construction), never by reimplementing; mirror MobilityDB tools/portable_aliases/generate.py + its 100%-coverage audit.", + "User-facing API uses the full name `trgeometry`; internal functions keep the `trgeo_` prefix — do NOT normalize the internal prefix.", + "Goal: 100% parity ecosystem-wide — every operator has its bare name on every engine, no gaps, no headline exclusions." + ] +} diff --git a/tools/portable_aliases/portable.py b/tools/portable_aliases/portable.py new file mode 100644 index 00000000..41398ac8 --- /dev/null +++ b/tools/portable_aliases/portable.py @@ -0,0 +1,45 @@ +"""Portable bare-name dialect — the single codegen source of truth. + +`meta/portable-aliases.json` is the curated, authoritative operator → +bare-name mapping (RFC #920; native in MobilityDB via PR #1075). Folding it +into the catalog means every binding/engine generates the *identical* bare +names, so a user learns one reference and assumes the rest. + +This is curated canonical data, not a heuristic — it is preserved verbatim +and only *derived* lookups are added (no guessing of C symbols: upstream +aliases reuse each operator's own backing function, equivalence by +construction). Pure dict → dict; no libclang. +""" + +import json +from pathlib import Path + + +def attach_portable_aliases(idl: dict, path: Path) -> dict: + """Attach ``idl["portableAliases"]`` from the canonical mapping file.""" + if not Path(path).exists(): + return idl + data = json.loads(Path(path).read_text()) + + pairs = [p for fam in data["families"].values() for p in fam] + by_operator = {p["operator"]: p["bareName"] for p in pairs} + by_bare_name = {p["bareName"]: p["operator"] for p in pairs} + + # Integrity: the mapping must be bijective (no operator or bare name may + # map two ways) — a collision would make codegen ambiguous. + if len(by_operator) != len(pairs) or len(by_bare_name) != len(pairs): + raise ValueError("portable-aliases: duplicate operator or bareName") + + idl["portableAliases"] = { + "provenance": data["provenance"], + "families": data["families"], + "alreadyCanonical": data["alreadyCanonical"], + "explicitBacking": data.get("explicitBacking", {}), + "scope": data["scope"], # cbuffer/npoint/pose/rgeo in scope + "notes": data["notes"], + "byOperator": by_operator, # "&&" -> "overlaps" + "byBareName": by_bare_name, # "overlaps" -> "&&" + "bareNames": sorted(by_bare_name), + "count": len(pairs), + } + return idl diff --git a/tools/portable_aliases/portable_parity.py b/tools/portable_aliases/portable_parity.py new file mode 100644 index 00000000..dc1f26b3 --- /dev/null +++ b/tools/portable_aliases/portable_parity.py @@ -0,0 +1,81 @@ +# Portable bare-name parity audit — the meos-api.json analogue of +# MobilityDB's `tools/portable_aliases/generate.py --check`. +# +# python run.py # catalog with `portableAliases` + functions +# python portable_parity.py # -> output/meos-portable-parity.json +# +# For every canonical bare name (PR #8 / RFC #920) it reports the catalog +# function family that backs it, by the MEOS bare-name prefix convention +# (`overlaps_*`, `teq_*`, `same_*`, …). A bare name with no prefix match is +# **not** asserted to be an API gap (some map through a different C prefix, +# e.g. `nearestApproachDistance` ↔ `nad_*`): it is flagged +# `needs-explicit-backing` so the cross-repo work can add an explicit +# operator→C-family entry — an honest signal, never a fabricated verdict. + +import json +import sys +from pathlib import Path + +IN_PATH = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("output/meos-idl.json") +OUT_PATH = (Path(sys.argv[2]) if len(sys.argv) > 2 + else Path("output/meos-portable-parity.json")) + + +def build_parity(catalog: dict) -> dict: + pa = catalog.get("portableAliases") + if not pa: + raise ValueError("catalog has no `portableAliases` — run run.py") + fam_of = {p["bareName"]: (fam, p["operator"]) + for fam, lst in pa["families"].items() for p in lst} + explicit = pa.get("explicitBacking", {}) + names = [f["name"] for f in catalog.get("functions", [])] + + def _matches(prefix): + return [n for n in names + if n == prefix or n.startswith(prefix + "_")] + + by_bare = {} + for bare, (fam, op) in sorted(fam_of.items()): + hits, via = _matches(bare), "prefix" + if not hits: # try the verified explicit map + for pref in explicit.get(bare, []): + hits += _matches(pref) + via = "explicit" if hits else None + by_bare[bare] = { + "operator": op, "family": fam, "via": via, + "backedBy": len(hits), + "sample": sorted(hits)[:3], + "status": "backed" if hits else "needs-explicit-backing", + } + backed = [b for b, v in by_bare.items() if v["status"] == "backed"] + unbacked = sorted(b for b, v in by_bare.items() + if v["status"] == "needs-explicit-backing") + total = len(by_bare) + return { + "total": total, + "backed": len(backed), + "needsExplicitBacking": len(unbacked), + "parityPct": round(len(backed) * 100 / total, 1) if total else 0, + "unbacked": unbacked, # the precise cross-repo worklist + "byBareName": by_bare, + } + + +def main() -> None: + if not IN_PATH.exists(): + sys.exit(f"Catalog not found: {IN_PATH} — run `python run.py` first.") + rep = build_parity(json.loads(IN_PATH.read_text())) + OUT_PATH.parent.mkdir(parents=True, exist_ok=True) + OUT_PATH.write_text(json.dumps(rep, indent=2)) + print(f"[portable-parity] {rep['backed']}/{rep['total']} bare names " + f"backed in the catalog ({rep['parityPct']}%); " + f"{rep['needsExplicitBacking']} need an explicit backing entry " + f"→ {OUT_PATH}", file=sys.stderr) + for b in rep["unbacked"]: + v = rep["byBareName"][b] + print(f" needs-explicit-backing: {b!r} ({v['operator']}, " + f"{v['family']})", file=sys.stderr) + + +if __name__ == "__main__": + main()