From d07fb904ccf5b0c7998f7cf172482599452b6762 Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Tue, 19 May 2026 11:33:55 +0200 Subject: [PATCH] refactor: switch TPose regular families to the generated mixin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fans the meos-idl.json faithful codegen out to TPose (second family after TCbuffer #90). - codegen.py: add the `pose` FAMILY_MODEL entry (+ the `point` arg token — tpose distance spells its geometry overload `tdistance_tpose_point`). The Draft _preview path and #89 coverage gate are unchanged. - pymeos/main/_generated/tpose_methods.py: generated TPoseRegularMixin (12 regular methods — comparison, temporal comparison, restriction, distance; pose has no spatial-relationship API). Each dispatches to the exact pymeos_cffi backing the hand-written method used. - pymeos/main/tpose.py: inherits TPoseRegularMixin; the 12 now-generated regular methods removed. never_*/the irregular core and the already-real MEOS-backed value accessors (start_value/end_value/ value_set/value_at_timestamp/value_n) are kept. Adds documented NotImplementedError stubs for from_base_time (MobilityDB#1084) and from_mfjson (MobilityDB#1085) so TPose/Inst/Seq/SeqSet are concrete. Proof: tests/main/tpose_test.py is byte-identical hand-written vs wired (A/B with the 2 stubs held constant) — 31 passed, 4 failed in BOTH; the 4 failures are pre-existing #88 defects OUTSIDE the codegen-switch domain (TestTPoseOutputs/Pose test_str, TestTPoseAccessors test_value_at_timestamp, TestPose test_from_as_hexwkb — hand-written accessors / base Pose, not the regular families). Static check: 12/12 generated methods are backing-and-fallback identical to the hand-written oracle. All switch-domain tests (comparison/distance/restriction) pass. Stacked on #90. --- pymeos/main/_generated/tpose_methods.py | 235 +++++++++++++++++ pymeos/main/tpose.py | 324 ++---------------------- tools/oo_codegen/codegen.py | 19 ++ 3 files changed, 281 insertions(+), 297 deletions(-) create mode 100644 pymeos/main/_generated/tpose_methods.py diff --git a/pymeos/main/_generated/tpose_methods.py b/pymeos/main/_generated/tpose_methods.py new file mode 100644 index 00000000..66b0f0be --- /dev/null +++ b/pymeos/main/_generated/tpose_methods.py @@ -0,0 +1,235 @@ +# Copyright (c) 2016-2026, Université libre de Bruxelles and PyMEOS +# contributors. Licensed under the PostgreSQL License (see LICENSE). +# +# ============================================================================ +# GENERATED by tools/oo_codegen/codegen.py -- DO NOT EDIT. +# Regenerate: +# python3 tools/oo_codegen/codegen.py --mixin pose \ +# --mixin-out pymeos/main/_generated/pose_methods.py +# ============================================================================ +# +# Wired into pymeos.main.TPose via TPoseRegularMixin. Every method +# dispatches by argument type to the EXACT pymeos_cffi backing the +# hand-written method used -- same native call, same transforms, same +# result, never reimplemented: identical by construction. +"""Generated regular OO methods for the TPose family.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import shapely.geometry.base as shpb +from pymeos_cffi import * + +from ...temporal import Temporal +from ...collections.pose import Pose + +if TYPE_CHECKING: + from ..tpose import TPose + + +class TPoseRegularMixin: + """Generated regular families (comparison, spatial relationship, + distance, restriction) for :class:`TPose`.""" + + def always_equal(self, other): + """Generated regular ``always_equal``. + + MEOS Functions: + always_eq_pose_tpose, always_eq_tpose_pose, always_eq_tpose_tpose + """ + from ..tpose import TPose + + if isinstance(other, Pose): + result = always_eq_tpose_pose(self._inner, other._inner) + elif isinstance(other, TPose): + result = always_eq_tpose_tpose(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type " f"{other.__class__}") + return result > 0 + + def always_not_equal(self, other): + """Generated regular ``always_not_equal``. + + MEOS Functions: + always_ne_pose_tpose, always_ne_tpose_pose, always_ne_tpose_tpose + """ + from ..tpose import TPose + + if isinstance(other, Pose): + result = always_ne_tpose_pose(self._inner, other._inner) + elif isinstance(other, TPose): + result = always_ne_tpose_tpose(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type " f"{other.__class__}") + return result > 0 + + def at(self, other): + """Generated regular ``at``. + + MEOS Functions: + tpose_at_geom, tpose_at_pose, tpose_at_stbox + """ + from ...boxes import STBox + + if isinstance(other, shpb.BaseGeometry): + result = tpose_at_geom(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, Pose): + result = tpose_at_pose(self._inner, other._inner) + elif isinstance(other, STBox): + result = tpose_at_stbox(self._inner, other._inner, True) + else: + return super().at(other) + return Temporal._factory(result) + + def distance(self, other): + """Generated regular ``distance``. + + MEOS Functions: + tdistance_tpose_point, tdistance_tpose_pose, tdistance_tpose_tpose + """ + from ..tpose import TPose + + if isinstance(other, shpb.BaseGeometry): + result = tdistance_tpose_point( + self._inner, geo_to_gserialized(other, False) + ) + elif isinstance(other, Pose): + result = tdistance_tpose_pose(self._inner, other._inner) + elif isinstance(other, TPose): + result = tdistance_tpose_tpose(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type " f"{other.__class__}") + return Temporal._factory(result) + + def ever_equal(self, other): + """Generated regular ``ever_equal``. + + MEOS Functions: + ever_eq_pose_tpose, ever_eq_tpose_pose, ever_eq_tpose_tpose + """ + from ..tpose import TPose + + if isinstance(other, Pose): + result = ever_eq_tpose_pose(self._inner, other._inner) + elif isinstance(other, TPose): + result = ever_eq_tpose_tpose(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type " f"{other.__class__}") + return result > 0 + + def ever_not_equal(self, other): + """Generated regular ``ever_not_equal``. + + MEOS Functions: + ever_ne_pose_tpose, ever_ne_tpose_pose, ever_ne_tpose_tpose + """ + from ..tpose import TPose + + if isinstance(other, Pose): + result = ever_ne_tpose_pose(self._inner, other._inner) + elif isinstance(other, TPose): + result = ever_ne_tpose_tpose(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type " f"{other.__class__}") + return result > 0 + + def minus(self, other): + """Generated regular ``minus``. + + MEOS Functions: + tpose_minus_geom, tpose_minus_pose, tpose_minus_stbox + """ + from ...boxes import STBox + + if isinstance(other, shpb.BaseGeometry): + result = tpose_minus_geom(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, Pose): + result = tpose_minus_pose(self._inner, other._inner) + elif isinstance(other, STBox): + result = tpose_minus_stbox(self._inner, other._inner, True) + else: + return super().minus(other) + return Temporal._factory(result) + + def nearest_approach_distance(self, other): + """Generated regular ``nearest_approach_distance``. + + MEOS Functions: + nad_tpose_geo, nad_tpose_pose, nad_tpose_stbox, nad_tpose_tpose + """ + from ..tpose import TPose + from ...boxes import STBox + + if isinstance(other, shpb.BaseGeometry): + result = nad_tpose_geo(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, Pose): + result = nad_tpose_pose(self._inner, other._inner) + elif isinstance(other, TPose): + result = nad_tpose_tpose(self._inner, other._inner) + elif isinstance(other, STBox): + result = nad_tpose_stbox(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type " f"{other.__class__}") + return result + + def nearest_approach_instant(self, other): + """Generated regular ``nearest_approach_instant``. + + MEOS Functions: + nai_tpose_geo, nai_tpose_pose, nai_tpose_tpose + """ + from ..tpose import TPose + + if isinstance(other, shpb.BaseGeometry): + result = nai_tpose_geo(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, Pose): + result = nai_tpose_pose(self._inner, other._inner) + elif isinstance(other, TPose): + result = nai_tpose_tpose(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type " f"{other.__class__}") + return Temporal._factory(result) + + def shortest_line(self, other): + """Generated regular ``shortest_line``. + + MEOS Functions: + shortestline_tpose_geo, shortestline_tpose_pose, shortestline_tpose_tpose + """ + from ..tpose import TPose + + if isinstance(other, shpb.BaseGeometry): + result = shortestline_tpose_geo( + self._inner, geo_to_gserialized(other, False) + ) + elif isinstance(other, Pose): + result = shortestline_tpose_pose(self._inner, other._inner) + elif isinstance(other, TPose): + result = shortestline_tpose_tpose(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type " f"{other.__class__}") + return gserialized_to_shapely_geometry(result, 10) + + def temporal_equal(self, other): + """Generated regular ``temporal_equal``. + + MEOS Functions: + teq_pose_tpose, teq_tpose_pose + """ + if isinstance(other, Pose): + result = teq_tpose_pose(self._inner, other._inner) + else: + return super().temporal_equal(other) + return Temporal._factory(result) + + def temporal_not_equal(self, other): + """Generated regular ``temporal_not_equal``. + + MEOS Functions: + tne_pose_tpose, tne_tpose_pose + """ + if isinstance(other, Pose): + result = tne_tpose_pose(self._inner, other._inner) + else: + return super().temporal_not_equal(other) + return Temporal._factory(result) diff --git a/pymeos/main/tpose.py b/pymeos/main/tpose.py index 022678d7..9e584fae 100644 --- a/pymeos/main/tpose.py +++ b/pymeos/main/tpose.py @@ -13,6 +13,7 @@ from ..collections.pose import Pose, PoseSet from ..mixins import TTemporallyComparable from ..temporal import Temporal, TInstant, TSequence, TSequenceSet, TInterpolation +from ._generated.tpose_methods import TPoseRegularMixin if TYPE_CHECKING: from ..boxes import STBox @@ -21,6 +22,7 @@ class TPose( + TPoseRegularMixin, Temporal[Pose, "TPose", "TPoseInst", "TPoseSeq", "TPoseSeqSet"], TTemporallyComparable, ABC, @@ -58,6 +60,30 @@ def from_tpoint_tfloat(tpoint: TPoint, tradius: TFloat) -> TPose: result = tpose_make(tpoint._inner, tradius._inner) return Temporal._factory(result) + # ------------------------- Value Constructors ---------------------------- + # The value accessors below (start_value/end_value/value_set/ + # value_at_timestamp/value_n) are MEOS-backed and implemented. MEOS does + # not yet export a value-based / MF-JSON constructor for the temporal + # pose; these two overrides keep the class concrete and fail loudly + # instead of making the whole type uninstantiable. Tracked upstream: + # from_base_time -> MobilityDB#1084 (from-base time family), + # from_mfjson -> MobilityDB#1085 (export tpose_from_mfjson). + @staticmethod + def from_base_time(value: Pose, base: Time) -> TPose: + """Pending upstream (MobilityDB#1084).""" + raise NotImplementedError( + "MEOS does not yet export a value-based constructor for the " + "temporal pose (tpose_from_base_*); tracked by MobilityDB#1084." + ) + + @classmethod + def from_mfjson(cls, mfjson: str) -> TPose: + """Pending upstream (MobilityDB#1085).""" + raise NotImplementedError( + "MEOS does not yet export MF-JSON input for the temporal pose " + "(tpose_from_mfjson); tracked by MobilityDB#1085." + ) + # ------------------------- Output ---------------------------------------- def __str__(self): """ @@ -241,9 +267,7 @@ def trajectory(self, precision: int = 15) -> shpb.BaseGeometry: MEOS Functions: tpose_trajectory """ - return gserialized_to_shapely_geometry( - tpose_trajectory(self._inner), precision - ) + return gserialized_to_shapely_geometry(tpose_trajectory(self._inner), precision) def rotation(self) -> TFloat: """ @@ -317,90 +341,6 @@ def transform(self: Self, srid: int) -> Self: return Temporal._factory(result) # ------------------------- Ever and Always Comparisons ------------------- - def always_equal(self, value: Union[Pose, TPose]) -> bool: - """ - Returns whether the values of `self` are always equal to `value`. - - Args: - value: :class:`Pose` or :class:`TPose` to compare. - - Returns: - `True` if the values of `self` are always equal to `value`, - `False` otherwise. - - MEOS Functions: - always_eq_tpose_pose, always_eq_tpose_tpose - """ - if isinstance(value, Pose): - return always_eq_tpose_pose(self._inner, value._inner) > 0 - elif isinstance(value, TPose): - return always_eq_tpose_tpose(self._inner, value._inner) > 0 - else: - raise TypeError(f"Operation not supported with type {value.__class__}") - - def always_not_equal(self, value: Union[Pose, TPose]) -> bool: - """ - Returns whether the values of `self` are always not equal to `value`. - - Args: - value: :class:`Pose` or :class:`TPose` to compare. - - Returns: - `True` if the values of `self` are always not equal to `value`, - `False` otherwise. - - MEOS Functions: - always_ne_tpose_pose, always_ne_tpose_tpose - """ - if isinstance(value, Pose): - return always_ne_tpose_pose(self._inner, value._inner) > 0 - elif isinstance(value, TPose): - return always_ne_tpose_tpose(self._inner, value._inner) > 0 - else: - raise TypeError(f"Operation not supported with type {value.__class__}") - - def ever_equal(self, value: Union[Pose, TPose]) -> bool: - """ - Returns whether the values of `self` are ever equal to `value`. - - Args: - value: :class:`Pose` or :class:`TPose` to compare. - - Returns: - `True` if the values of `self` are ever equal to `value`, `False` - otherwise. - - MEOS Functions: - ever_eq_tpose_pose, ever_eq_tpose_tpose - """ - if isinstance(value, Pose): - return ever_eq_tpose_pose(self._inner, value._inner) > 0 - elif isinstance(value, TPose): - return ever_eq_tpose_tpose(self._inner, value._inner) > 0 - else: - raise TypeError(f"Operation not supported with type {value.__class__}") - - def ever_not_equal(self, value: Union[Pose, TPose]) -> bool: - """ - Returns whether the values of `self` are ever not equal to `value`. - - Args: - value: :class:`Pose` or :class:`TPose` to compare. - - Returns: - `True` if the values of `self` are ever not equal to `value`, - `False` otherwise. - - MEOS Functions: - ever_ne_tpose_pose, ever_ne_tpose_tpose - """ - if isinstance(value, Pose): - return ever_ne_tpose_pose(self._inner, value._inner) > 0 - elif isinstance(value, TPose): - return ever_ne_tpose_tpose(self._inner, value._inner) > 0 - else: - raise TypeError(f"Operation not supported with type {value.__class__}") - def never_equal(self, value: Union[Pose, TPose]) -> bool: """ Returns whether the values of `self` are never equal to `value`. @@ -433,216 +373,6 @@ def never_not_equal(self, value: Union[Pose, TPose]) -> bool: """ return not self.ever_not_equal(value) - # ------------------------- Temporal Comparisons -------------------------- - def temporal_equal(self, other: Union[Pose, TPose]) -> TBool: - """ - Returns the temporal equality relation between `self` and `other`. - - Args: - other: A :class:`Pose` or temporal object to compare to `self`. - - Returns: - A :class:`TBool` with the result of the temporal equality relation. - - MEOS Functions: - teq_tpose_pose, teq_temporal_temporal - """ - if isinstance(other, Pose): - result = teq_tpose_pose(self._inner, other._inner) - else: - return super().temporal_equal(other) - return Temporal._factory(result) - - def temporal_not_equal(self, other: Union[Pose, TPose]) -> TBool: - """ - Returns the temporal not equal relation between `self` and `other`. - - Args: - other: A :class:`Pose` or temporal object to compare to `self`. - - Returns: - A :class:`TBool` with the result of the temporal not equal - relation. - - MEOS Functions: - tne_tpose_pose, tne_temporal_temporal - """ - if isinstance(other, Pose): - result = tne_tpose_pose(self._inner, other._inner) - else: - return super().temporal_not_equal(other) - return Temporal._factory(result) - - # ------------------------- Restrictions ---------------------------------- - def at(self, other: Union[Pose, shpb.BaseGeometry, STBox, Time]) -> TPose: - """ - Returns a new temporal pose with the values of `self` restricted to - `other`. - - Args: - other: An object to restrict the values of `self` to. - - Returns: - A new :class:`TPose` with the values of `self` restricted to - `other`. - - MEOS Functions: - tpose_at_pose, tpose_at_geom, tpose_at_stbox, - temporal_at_timestamp, temporal_at_tstzset, temporal_at_tstzspan, - temporal_at_tstzspanset - """ - from ..boxes import STBox - - if isinstance(other, Pose): - result = tpose_at_pose(self._inner, other._inner) - elif isinstance(other, shpb.BaseGeometry): - gs = geo_to_gserialized(other, False) - result = tpose_at_geom(self._inner, gs) - elif isinstance(other, STBox): - result = tpose_at_stbox(self._inner, other._inner, True) - else: - return super().at(other) - return Temporal._factory(result) - - def minus(self, other: Union[Pose, shpb.BaseGeometry, STBox, Time]) -> TPose: - """ - Returns a new temporal pose with the values of `self` restricted to - the complement of `other`. - - Args: - other: An object to restrict the values of `self` to the - complement of. - - Returns: - A new :class:`TPose` with the values of `self` restricted to the - complement of `other`. - - MEOS Functions: - tpose_minus_pose, tpose_minus_geom, tpose_minus_stbox, - temporal_minus_timestamp, temporal_minus_tstzset, - temporal_minus_tstzspan, temporal_minus_tstzspanset - """ - from ..boxes import STBox - - if isinstance(other, Pose): - result = tpose_minus_pose(self._inner, other._inner) - elif isinstance(other, shpb.BaseGeometry): - gs = geo_to_gserialized(other, False) - result = tpose_minus_geom(self._inner, gs) - elif isinstance(other, STBox): - result = tpose_minus_stbox(self._inner, other._inner, True) - else: - return super().minus(other) - return Temporal._factory(result) - - # ------------------------- Distance Operations --------------------------- - def distance(self, other: Union[shpb.BaseGeometry, Pose, TPose]) -> TFloat: - """ - Returns the temporal distance between `self` and `other`. - - Args: - other: An object to check the distance to. - - Returns: - A new :class:`TFloat` with the temporal distance. - - MEOS Functions: - tdistance_tpose_point, tdistance_tpose_pose, tdistance_tpose_tpose - """ - if isinstance(other, shpb.BaseGeometry): - gs = geo_to_gserialized(other, False) - result = tdistance_tpose_point(self._inner, gs) - elif isinstance(other, Pose): - result = tdistance_tpose_pose(self._inner, other._inner) - elif isinstance(other, TPose): - result = tdistance_tpose_tpose(self._inner, other._inner) - else: - raise TypeError(f"Operation not supported with type {other.__class__}") - return Temporal._factory(result) - - def nearest_approach_distance( - self, other: Union[shpb.BaseGeometry, Pose, STBox, TPose] - ) -> float: - """ - Returns the nearest approach distance between `self` and `other`. - - Args: - other: An object to check the nearest approach distance to. - - Returns: - A :class:`float` with the nearest approach distance. - - MEOS Functions: - nad_tpose_geo, nad_tpose_pose, nad_tpose_stbox, nad_tpose_tpose - """ - from ..boxes import STBox - - if isinstance(other, shpb.BaseGeometry): - gs = geo_to_gserialized(other, False) - return nad_tpose_geo(self._inner, gs) - elif isinstance(other, Pose): - return nad_tpose_pose(self._inner, other._inner) - elif isinstance(other, STBox): - return nad_tpose_stbox(self._inner, other._inner) - elif isinstance(other, TPose): - return nad_tpose_tpose(self._inner, other._inner) - else: - raise TypeError(f"Operation not supported with type {other.__class__}") - - def nearest_approach_instant( - self, other: Union[shpb.BaseGeometry, Pose, TPose] - ) -> TPoseInst: - """ - Returns the nearest approach instant between `self` and `other`. - - Args: - other: An object to check the nearest approach instant to. - - Returns: - A new :class:`TPoseInst` with the nearest approach instant. - - MEOS Functions: - nai_tpose_geo, nai_tpose_pose, nai_tpose_tpose - """ - if isinstance(other, shpb.BaseGeometry): - gs = geo_to_gserialized(other, False) - result = nai_tpose_geo(self._inner, gs) - elif isinstance(other, Pose): - result = nai_tpose_pose(self._inner, other._inner) - elif isinstance(other, TPose): - result = nai_tpose_tpose(self._inner, other._inner) - else: - raise TypeError(f"Operation not supported with type {other.__class__}") - return Temporal._factory(result) - - def shortest_line( - self, other: Union[shpb.BaseGeometry, Pose, TPose] - ) -> shpb.BaseGeometry: - """ - Returns the shortest line between `self` and `other`. - - Args: - other: An object to check the shortest line to. - - Returns: - A new :class:`~shapely.geometry.base.BaseGeometry` with the - shortest line. - - MEOS Functions: - shortestline_tpose_geo, shortestline_tpose_pose, - shortestline_tpose_tpose - """ - if isinstance(other, shpb.BaseGeometry): - gs = geo_to_gserialized(other, False) - result = shortestline_tpose_geo(self._inner, gs) - elif isinstance(other, Pose): - result = shortestline_tpose_pose(self._inner, other._inner) - elif isinstance(other, TPose): - result = shortestline_tpose_tpose(self._inner, other._inner) - else: - raise TypeError(f"Operation not supported with type {other.__class__}") - return gserialized_to_shapely_geometry(result, 10) - # ------------------------- Database Operations --------------------------- @staticmethod def read_from_cursor(value, _=None): diff --git a/tools/oo_codegen/codegen.py b/tools/oo_codegen/codegen.py index 314b86f9..ba3bd6a7 100644 --- a/tools/oo_codegen/codegen.py +++ b/tools/oo_codegen/codegen.py @@ -389,6 +389,24 @@ def emit(fams: dict[str, dict[str, Method]], out_dir: Path) -> None: }, "stbox_lazy": "from ...boxes import STBox", }, + "pose": { + "mixin_class": "TPoseRegularMixin", + "base_class": "Pose", + "base_import": "from ...collections.pose import Pose", + "temporal_class": "TPose", + "temporal_import": "from ..tpose import TPose", + "temporal_token": "tpose", + "tokens": { + "pose": ("Pose", "$o._inner"), + "tpose": ("TPose", "$o._inner"), + "geo": ("shpb.BaseGeometry", "geo_to_gserialized($o, False)"), + "geom": ("shpb.BaseGeometry", "geo_to_gserialized($o, False)"), + # tpose distance spells its geometry overload `_tpose_point` + "point": ("shpb.BaseGeometry", "geo_to_gserialized($o, False)"), + "stbox": ("STBox", "$o._inner"), + }, + "stbox_lazy": "from ...boxes import STBox", + }, } # Result post-processing, derived verbatim from the hand-written oracle. @@ -431,6 +449,7 @@ def emit(fams: dict[str, dict[str, Method]], out_dir: Path) -> None: _ORDER = [ "geo", "geom", + "point", "cbuffer", "tcbuffer", "npoint",