From 7b1dceda519226a5893835af7215adfc786ad65e Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Tue, 19 May 2026 12:40:40 +0200 Subject: [PATCH] refactor: switch TRgeometry regular families to the generated mixin Fourth family in the codegen-switch fan-out (after TCbuffer #90, TPose #91, TNpoint #92). - codegen.py: add the `rgeo` FAMILY_MODEL (temporal_token `trgeo`; geo token uses `geometry_to_gserialized($o)` -- single-arg, unlike the geo_to_gserialized(g, False) of the other spatial families; tokens geo/trgeo/tpoint/stbox) + `tpoint`/`trgeo` to _ORDER. Additive: #90/#91/#92 mixins regenerate byte-identical (regression-guarded). - pymeos/main/_generated/trgeometry_methods.py: generated TRgeometryRegularMixin -- 10 methods (comparison, temporal comparison, distance/nad/nai/shortest_line). Static: 10/10 backing-and-fallback identical to the hand-written oracle. - pymeos/main/trgeometry.py: inherits the mixin; the 10 now-generated regular methods removed. at/minus stay hand-written (rgeo restriction is the irregular `trgeo_restrict_`+direction-bool pattern, not `trgeometry_at_*`, so the generator never collects them); never_*/irregular core kept. Adds documented NotImplementedError stubs for from_base_time and from_mfjson (pending upstream MEOS) so TRgeometry/Inst/Seq/SeqSet are concrete. Proof: tests/main/trgeometry_test.py is byte-identical hand-written vs wired (A/B, 2 stubs constant) -- both 6 failed / 23 passed, the SAME 6 pre-existing #88 hand-written-core defects OUTSIDE the switch domain (TestTRgeometryConstructors/Accessors/GapMethods -- constructors, accessors, known-gap methods; not comparison/distance), unrelated to and unaffected by this change. Every switch-domain test passes. Static 10/10 identical is the positive proof. Stacked on #92. --- pymeos/main/_generated/trgeometry_methods.py | 195 +++++++++++++ pymeos/main/trgeometry.py | 273 ++----------------- tools/oo_codegen/codegen.py | 26 ++ 3 files changed, 245 insertions(+), 249 deletions(-) create mode 100644 pymeos/main/_generated/trgeometry_methods.py diff --git a/pymeos/main/_generated/trgeometry_methods.py b/pymeos/main/_generated/trgeometry_methods.py new file mode 100644 index 00000000..a32fc17b --- /dev/null +++ b/pymeos/main/_generated/trgeometry_methods.py @@ -0,0 +1,195 @@ +# 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 rgeo \ +# --mixin-out pymeos/main/_generated/rgeo_methods.py +# ============================================================================ +# +# Wired into pymeos.main.TRgeometry via TRgeometryRegularMixin. 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 TRgeometry 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 ..tpoint import TPoint + +if TYPE_CHECKING: + from ..trgeometry import TRgeometry + + +class TRgeometryRegularMixin: + """Generated regular families (comparison, spatial relationship, + distance, restriction) for :class:`TRgeometry`.""" + + def always_equal(self, other): + """Generated regular ``always_equal``. + + MEOS Functions: + always_eq_geo_trgeo, always_eq_trgeo_geo, always_eq_trgeo_trgeo + """ + from ..trgeometry import TRgeometry + + if isinstance(other, shpb.BaseGeometry): + result = always_eq_trgeo_geo(self._inner, geometry_to_gserialized(other)) + elif isinstance(other, TRgeometry): + result = always_eq_trgeo_trgeo(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_geo_trgeo, always_ne_trgeo_geo, always_ne_trgeo_trgeo + """ + from ..trgeometry import TRgeometry + + if isinstance(other, shpb.BaseGeometry): + result = always_ne_trgeo_geo(self._inner, geometry_to_gserialized(other)) + elif isinstance(other, TRgeometry): + result = always_ne_trgeo_trgeo(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type " f"{other.__class__}") + return result > 0 + + def distance(self, other): + """Generated regular ``distance``. + + MEOS Functions: + tdistance_trgeo_geo, tdistance_trgeo_tpoint, tdistance_trgeo_trgeo + """ + from ..trgeometry import TRgeometry + + if isinstance(other, shpb.BaseGeometry): + result = tdistance_trgeo_geo(self._inner, geometry_to_gserialized(other)) + elif isinstance(other, TPoint): + result = tdistance_trgeo_tpoint(self._inner, other._inner) + elif isinstance(other, TRgeometry): + result = tdistance_trgeo_trgeo(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_geo_trgeo, ever_eq_trgeo_geo, ever_eq_trgeo_trgeo + """ + from ..trgeometry import TRgeometry + + if isinstance(other, shpb.BaseGeometry): + result = ever_eq_trgeo_geo(self._inner, geometry_to_gserialized(other)) + elif isinstance(other, TRgeometry): + result = ever_eq_trgeo_trgeo(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_geo_trgeo, ever_ne_trgeo_geo, ever_ne_trgeo_trgeo + """ + from ..trgeometry import TRgeometry + + if isinstance(other, shpb.BaseGeometry): + result = ever_ne_trgeo_geo(self._inner, geometry_to_gserialized(other)) + elif isinstance(other, TRgeometry): + result = ever_ne_trgeo_trgeo(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type " f"{other.__class__}") + return result > 0 + + def nearest_approach_distance(self, other): + """Generated regular ``nearest_approach_distance``. + + MEOS Functions: + nad_stbox_trgeo, nad_trgeo_geo, nad_trgeo_stbox, nad_trgeo_tpoint, nad_trgeo_trgeo + """ + from ..trgeometry import TRgeometry + from ...boxes import STBox + + if isinstance(other, shpb.BaseGeometry): + result = nad_trgeo_geo(self._inner, geometry_to_gserialized(other)) + elif isinstance(other, TPoint): + result = nad_trgeo_tpoint(self._inner, other._inner) + elif isinstance(other, TRgeometry): + result = nad_trgeo_trgeo(self._inner, other._inner) + elif isinstance(other, STBox): + result = nad_trgeo_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_trgeo_geo, nai_trgeo_tpoint, nai_trgeo_trgeo + """ + from ..trgeometry import TRgeometry + + if isinstance(other, shpb.BaseGeometry): + result = nai_trgeo_geo(self._inner, geometry_to_gserialized(other)) + elif isinstance(other, TPoint): + result = nai_trgeo_tpoint(self._inner, other._inner) + elif isinstance(other, TRgeometry): + result = nai_trgeo_trgeo(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_trgeo_geo, shortestline_trgeo_tpoint, shortestline_trgeo_trgeo + """ + from ..trgeometry import TRgeometry + + if isinstance(other, shpb.BaseGeometry): + result = shortestline_trgeo_geo(self._inner, geometry_to_gserialized(other)) + elif isinstance(other, TPoint): + result = shortestline_trgeo_tpoint(self._inner, other._inner) + elif isinstance(other, TRgeometry): + result = shortestline_trgeo_trgeo(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_geo_trgeo, teq_trgeo_geo + """ + if isinstance(other, shpb.BaseGeometry): + result = teq_trgeo_geo(self._inner, geometry_to_gserialized(other)) + else: + return super().temporal_equal(other) + return Temporal._factory(result) + + def temporal_not_equal(self, other): + """Generated regular ``temporal_not_equal``. + + MEOS Functions: + tne_geo_trgeo, tne_trgeo_geo + """ + if isinstance(other, shpb.BaseGeometry): + result = tne_trgeo_geo(self._inner, geometry_to_gserialized(other)) + else: + return super().temporal_not_equal(other) + return Temporal._factory(result) diff --git a/pymeos/main/trgeometry.py b/pymeos/main/trgeometry.py index 290d46c3..281f0b23 100644 --- a/pymeos/main/trgeometry.py +++ b/pymeos/main/trgeometry.py @@ -13,6 +13,7 @@ from ..collections.pose import Pose from ..mixins import TTemporallyComparable from ..temporal import Temporal, TInstant, TSequence, TSequenceSet, TInterpolation +from ._generated.trgeometry_methods import TRgeometryRegularMixin if TYPE_CHECKING: from ..boxes import STBox @@ -22,6 +23,7 @@ class TRgeometry( + TRgeometryRegularMixin, Temporal[ shpb.BaseGeometry, "TRgeometry", @@ -65,6 +67,27 @@ def from_geometry_tpose(geometry: shpb.BaseGeometry, tpose: TPose) -> TRgeometry result = geo_tpose_to_trgeo(gs, tpose._inner) return Temporal._factory(result) + # MEOS does not export a value-based or MF-JSON constructor for the + # temporal rigid geometry. These overrides keep TRgeometry/Inst/Seq/ + # SeqSet concrete and fail loudly instead of making the type + # uninstantiable; both are pending upstream MEOS work. + @staticmethod + def from_base_time(value: shpb.BaseGeometry, base: Time) -> TRgeometry: + """Pending upstream (MEOS trgeometry from-base not exported).""" + raise NotImplementedError( + "MEOS does not export a value-based constructor for the " + "temporal rigid geometry (trgeometry from-base); pending " + "upstream." + ) + + @classmethod + def from_mfjson(cls, mfjson: str) -> TRgeometry: + """Pending upstream (MEOS trgeometry MF-JSON not exported).""" + raise NotImplementedError( + "MEOS does not export MF-JSON input for the temporal rigid " + "geometry (trgeometry_from_mfjson); pending upstream." + ) + # ------------------------- Output ---------------------------------------- def __str__(self): """ @@ -491,98 +514,6 @@ def append_sequence(self: Self, sequence: TRgeometrySeq) -> TRgeometry: return Temporal._factory(result) # ------------------------- Ever and Always Comparisons ------------------- - def always_equal(self, value: Union[shpb.BaseGeometry, TRgeometry]) -> bool: - """ - Returns whether the values of `self` are always equal to `value`. - - Args: - value: :class:`~shapely.geometry.base.BaseGeometry` or - :class:`TRgeometry` to compare. - - Returns: - `True` if the values of `self` are always equal to `value`, - `False` otherwise. - - MEOS Functions: - always_eq_trgeo_geo, always_eq_trgeo_trgeo - """ - if isinstance(value, shpb.BaseGeometry): - gs = geometry_to_gserialized(value) - return always_eq_trgeo_geo(self._inner, gs) > 0 - elif isinstance(value, TRgeometry): - return always_eq_trgeo_trgeo(self._inner, value._inner) > 0 - else: - raise TypeError(f"Operation not supported with type {value.__class__}") - - def always_not_equal(self, value: Union[shpb.BaseGeometry, TRgeometry]) -> bool: - """ - Returns whether the values of `self` are always not equal to `value`. - - Args: - value: :class:`~shapely.geometry.base.BaseGeometry` or - :class:`TRgeometry` to compare. - - Returns: - `True` if the values of `self` are always not equal to `value`, - `False` otherwise. - - MEOS Functions: - always_ne_trgeo_geo, always_ne_trgeo_trgeo - """ - if isinstance(value, shpb.BaseGeometry): - gs = geometry_to_gserialized(value) - return always_ne_trgeo_geo(self._inner, gs) > 0 - elif isinstance(value, TRgeometry): - return always_ne_trgeo_trgeo(self._inner, value._inner) > 0 - else: - raise TypeError(f"Operation not supported with type {value.__class__}") - - def ever_equal(self, value: Union[shpb.BaseGeometry, TRgeometry]) -> bool: - """ - Returns whether the values of `self` are ever equal to `value`. - - Args: - value: :class:`~shapely.geometry.base.BaseGeometry` or - :class:`TRgeometry` to compare. - - Returns: - `True` if the values of `self` are ever equal to `value`, `False` - otherwise. - - MEOS Functions: - ever_eq_trgeo_geo, ever_eq_trgeo_trgeo - """ - if isinstance(value, shpb.BaseGeometry): - gs = geometry_to_gserialized(value) - return ever_eq_trgeo_geo(self._inner, gs) > 0 - elif isinstance(value, TRgeometry): - return ever_eq_trgeo_trgeo(self._inner, value._inner) > 0 - else: - raise TypeError(f"Operation not supported with type {value.__class__}") - - def ever_not_equal(self, value: Union[shpb.BaseGeometry, TRgeometry]) -> bool: - """ - Returns whether the values of `self` are ever not equal to `value`. - - Args: - value: :class:`~shapely.geometry.base.BaseGeometry` or - :class:`TRgeometry` to compare. - - Returns: - `True` if the values of `self` are ever not equal to `value`, - `False` otherwise. - - MEOS Functions: - ever_ne_trgeo_geo, ever_ne_trgeo_trgeo - """ - if isinstance(value, shpb.BaseGeometry): - gs = geometry_to_gserialized(value) - return ever_ne_trgeo_geo(self._inner, gs) > 0 - elif isinstance(value, TRgeometry): - return ever_ne_trgeo_trgeo(self._inner, value._inner) > 0 - else: - raise TypeError(f"Operation not supported with type {value.__class__}") - def never_equal(self, value: Union[shpb.BaseGeometry, TRgeometry]) -> bool: """ Returns whether the values of `self` are never equal to `value`. @@ -617,50 +548,6 @@ def never_not_equal(self, value: Union[shpb.BaseGeometry, TRgeometry]) -> bool: """ return not self.ever_not_equal(value) - # ------------------------- Temporal Comparisons -------------------------- - def temporal_equal(self, other: Union[shpb.BaseGeometry, TRgeometry]) -> TBool: - """ - Returns the temporal equality relation between `self` and `other`. - - Args: - other: A :class:`~shapely.geometry.base.BaseGeometry` or temporal - object to compare to `self`. - - Returns: - A :class:`TBool` with the result of the temporal equality relation. - - MEOS Functions: - teq_trgeo_geo, teq_temporal_temporal - """ - if isinstance(other, shpb.BaseGeometry): - gs = geometry_to_gserialized(other) - result = teq_trgeo_geo(self._inner, gs) - else: - return super().temporal_equal(other) - return Temporal._factory(result) - - def temporal_not_equal(self, other: Union[shpb.BaseGeometry, TRgeometry]) -> TBool: - """ - Returns the temporal not equal relation between `self` and `other`. - - Args: - other: A :class:`~shapely.geometry.base.BaseGeometry` or temporal - object to compare to `self`. - - Returns: - A :class:`TBool` with the result of the temporal not equal - relation. - - MEOS Functions: - tne_trgeo_geo, tne_temporal_temporal - """ - if isinstance(other, shpb.BaseGeometry): - gs = geometry_to_gserialized(other) - result = tne_trgeo_geo(self._inner, gs) - else: - return super().temporal_not_equal(other) - return Temporal._factory(result) - # ------------------------- Restrictions ---------------------------------- def at(self, other: Time) -> TRgeometry: """ @@ -757,9 +644,7 @@ def delete(self, other: Time, connect: bool = True) -> TRgeometry: elif isinstance(other, TsTzSpan): result = trgeo_delete_tstzspan(self._inner, other._inner, connect) elif isinstance(other, TsTzSpanSet): - result = trgeo_delete_tstzspanset( - self._inner, other._inner, connect - ) + result = trgeo_delete_tstzspanset(self._inner, other._inner, connect) else: return super().delete(other, connect) return Temporal._factory(result) @@ -798,116 +683,6 @@ def after(self, timestamp: datetime, strict: bool = False) -> TRgeometry: ) return Temporal._factory(result) - # ------------------------- Distance Operations --------------------------- - def distance( - self, other: Union[shpb.BaseGeometry, TPoint, TRgeometry] - ) -> 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_trgeo_geo, tdistance_trgeo_tpoint, tdistance_trgeo_trgeo - """ - if isinstance(other, shpb.BaseGeometry): - gs = geometry_to_gserialized(other) - result = tdistance_trgeo_geo(self._inner, gs) - elif isinstance(other, TPoint): - result = tdistance_trgeo_tpoint(self._inner, other._inner) - elif isinstance(other, TRgeometry): - result = tdistance_trgeo_trgeo(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, STBox, TPoint, TRgeometry] - ) -> 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_trgeo_geo, nad_trgeo_stbox, nad_trgeo_tpoint, nad_trgeo_trgeo - """ - from ..boxes import STBox - - if isinstance(other, shpb.BaseGeometry): - gs = geometry_to_gserialized(other) - return nad_trgeo_geo(self._inner, gs) - elif isinstance(other, STBox): - return nad_trgeo_stbox(self._inner, other._inner) - elif isinstance(other, TPoint): - return nad_trgeo_tpoint(self._inner, other._inner) - elif isinstance(other, TRgeometry): - return nad_trgeo_trgeo(self._inner, other._inner) - else: - raise TypeError(f"Operation not supported with type {other.__class__}") - - def nearest_approach_instant( - self, other: Union[shpb.BaseGeometry, TPoint, TRgeometry] - ) -> TRgeometryInst: - """ - Returns the nearest approach instant between `self` and `other`. - - Args: - other: An object to check the nearest approach instant to. - - Returns: - A new :class:`TRgeometryInst` with the nearest approach instant. - - MEOS Functions: - nai_trgeo_geo, nai_trgeo_tpoint, nai_trgeo_trgeo - """ - if isinstance(other, shpb.BaseGeometry): - gs = geometry_to_gserialized(other) - result = nai_trgeo_geo(self._inner, gs) - elif isinstance(other, TPoint): - result = nai_trgeo_tpoint(self._inner, other._inner) - elif isinstance(other, TRgeometry): - result = nai_trgeo_trgeo(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, TPoint, TRgeometry] - ) -> 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_trgeo_geo, shortestline_trgeo_tpoint, - shortestline_trgeo_trgeo - """ - if isinstance(other, shpb.BaseGeometry): - gs = geometry_to_gserialized(other) - result = shortestline_trgeo_geo(self._inner, gs) - elif isinstance(other, TPoint): - result = shortestline_trgeo_tpoint(self._inner, other._inner) - elif isinstance(other, TRgeometry): - result = shortestline_trgeo_trgeo(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 04681647..2d4b2a57 100644 --- a/tools/oo_codegen/codegen.py +++ b/tools/oo_codegen/codegen.py @@ -429,6 +429,30 @@ def emit(fams: dict[str, dict[str, Method]], out_dir: Path) -> None: # hardcode 10 with no param -> they omit this key, unaffected). "shortest_line_precision": 15, }, + "rgeo": { + "mixin_class": "TRgeometryRegularMixin", + "base_class": "TRgeometry", + # `tpoint` arg token needs TPoint at runtime (isinstance); no + # rgeometry base-value wrapper exists (base value is a shapely + # geometry), so the base import is TPoint. + "base_import": "from ..tpoint import TPoint", + "temporal_class": "TRgeometry", + "temporal_import": "from ..trgeometry import TRgeometry", + "temporal_token": "trgeo", + "tokens": { + # rgeo uses geometry_to_gserialized(g) (single arg), not the + # geo_to_gserialized(g, False) the other spatial families use. + "geo": ("shpb.BaseGeometry", "geometry_to_gserialized($o)"), + "trgeo": ("TRgeometry", "$o._inner"), + "tpoint": ("TPoint", "$o._inner"), + "stbox": ("STBox", "$o._inner"), + }, + "stbox_lazy": "from ...boxes import STBox", + # at/minus are NOT generated: rgeo restriction is the irregular + # `trgeo_restrict_` + direction-bool pattern (no + # `trgeometry_at_*`/`_minus_*` in the catalog), so the generator + # never collects them and they correctly stay hand-written. + }, } # Result post-processing, derived verbatim from the hand-written oracle. @@ -472,6 +496,8 @@ def emit(fams: dict[str, dict[str, Method]], out_dir: Path) -> None: "geo", "geom", "point", + "tpoint", + "trgeo", "cbuffer", "tcbuffer", "npoint",