Skip to content

feat: PyMEOS consumer for RFC #94 oo.dispatch — path to codegen 6/6 - #95

Open
estebanzimanyi wants to merge 15 commits into
refactor/oo-codegen-trgeometry-switchfrom
feat/oo-dispatch-consumer
Open

estebanzimanyi wants to merge 15 commits into
refactor/oo-codegen-trgeometry-switchfrom
feat/oo-dispatch-consumer

Conversation

@estebanzimanyi

Copy link
Copy Markdown
Member

Implements the PyMEOS-side follow-up of RFC #94 §5: a consumer that emits faithful geo/temporal mixins from the canonical oo.<family>.<member>.dispatch blocks the parallel MEOS-API session is adding to meta/meos-meta.json. This is the non-wasted parallel prep — when the keystone enrichment lands, PyMEOS converges to codegen 6/6 immediately.

Proven correct without waiting on the metadata

_serialize_family_dispatch() expresses each of the 4 A/B-proven families (cbuffer/pose/npoint/rgeo) in the RFC #94 §3 schema; feeding those back through emit_from_oo_dispatch() reproduces their committed mixins byte-identically for all 4 — the new --verify-oo-roundtrip gate + OoDispatchConsumer test (permanently enforced). So the consumer is proven against the proven path, not circularly.

cbuffer  oo.dispatch round-trip BYTE-IDENTICAL ✓
npoint   oo.dispatch round-trip BYTE-IDENTICAL ✓
pose     oo.dispatch round-trip BYTE-IDENTICAL ✓
rgeo     oo.dispatch round-trip BYTE-IDENTICAL ✓
VERIFY: PASS

Safety

Additive only — emit_faithful_mixin (the 4 shipped families' path) is untouched; #90–#93 mixins regenerate byte-identical (regression-guarded); #89 coverage gate stays green; Black-clean; no AI attribution.

How 6/6 closes

The instant MEOS-API emits oo.geo/oo.temporal dispatch (RFC #94 §3, parallel session in progress): codegen.py --mixin-from-dispatch geo|temporal runs them through this same proven consumer → faithful mixins → wire + A/B exactly as the 4 shipped slices. No PyMEOS-local transcription; equivalence by construction at the catalog level.

Stack

Stacked on #94 (the RFC). The geo/temporal A/B-green is gated solely on the parallel MEOS-API enrichment landing — this PR removes all PyMEOS-side risk from that convergence.

Update the pin to pymeos-cffi >=1.4.0 and version to 1.4.0-alpha-1.
Rename the call sites that switched from MEOS 1.3's tpoint family to
the 1.4 tgeo/tspatial families:

- temporal-output renames: geoset_as_ewkt/as_text/out -> spatialset_*,
  geoset_round -> set_round, geoset_srid -> spatialset_srid,
  pgis_geometry_in/geography_in -> geom_in/geog_in
- tpoint_to_stbox -> tspatial_to_stbox
- tpoint_at_stbox/minus_stbox -> tgeo_at_stbox/minus_stbox
- tpoint_at_geom_time(t, gs, None, None) -> tpoint_at_geom(t, gs)
- tpoint_minus_geom_time(t, gs, None, None) -> tpoint_minus_geom(t, gs)
- tcontains/tdisjoint/tdwithin/tintersects/ttouches_tpoint_* ->
  *_tgeo_*, with the restr/atvalue trailing args dropped
- edisjoint/edwithin/eintersects/etouches_tpoint_* -> *_tgeo_*
- nad/nai/distance_tpoint_* -> *_tgeo_*
- tpoint_extent_transfn -> tspatial_extent_transfn
- spanset_make: dropped normalize/order args (1.4 signature is 1-arg)
- STBox.expand_space: tpoint_expand_space/geo_expand_space were
  removed in 1.4; rebuild as (geo|tspatial)_to_stbox + stbox_expand_space

Pytest collects 4480 tests on this branch. Test execution still
trips on functions that need shape metadata in MEOS-API (e.g.
stbox_quad_split's count output param); follow-up after MEOS-API
adds those entries.
temporal_to_tsequence / temporal_to_tsequenceset now take the
InterpolationType enum directly; PyMEOS was passing the legacy string
form. Pass the IntEnum value (which is already what TInterpolation
holds underneath).

temporal_append_tinstant gained an interp argument in 1.4. It only
matters when the receiver is a TInstant being promoted to a TSequence;
default to STEP because LINEAR is rejected by MEOS for discrete-only
base types like bool/text/integer.
Closes the rename tail surfaced by running the full pytest suite:

- tpoint_as_text / tpoint_as_ewkt -> tspatial_as_text / tspatial_as_ewkt
- tpoint_start_value / tpoint_end_value / tpoint_value_at_timestamptz /
  tpoint_values / tpoint_stboxes -> tgeo_*
- tpoint_srid / tpoint_set_srid -> tspatial_srid / tspatial_set_srid
- teq/tne/ever_eq/ever_ne/always_eq/always_ne_tpoint_point -> *_tgeo_geo
- shortestline_tpoint_* -> shortestline_tgeo_*
- tgeompoint_to_tgeogpoint / tgeogpoint_to_tgeompoint ->
  tgeometry_to_tgeography / tgeography_to_tgeometry
- tfloat_round / tpoint_round -> temporal_round
- tfloat_derivative -> temporal_derivative
- econtains_geo_tpoint -> econtains_geo_tgeo
- tpoint_space_split / tpoint_space_time_split -> tgeo_space_*
- lwproj_transform -> tspatial_transform (signature collapsed; PJ cache
  no longer needed because MEOS 1.4 takes the SRID directly)
- tpoint_expand_space removed; rebuild as tspatial_to_stbox +
  stbox_expand_space
- tpoint_trajectory gained a trailing unary_union bool argument

After these the pytest count is 4464 passed / 16 failed. The 16
remaining failures are: 8 intentional 1.4 restrictions where
tdwithin/etc reject geodetic input ('Only planar coordinates
supported'), 6 string-representation changes (single-segment Step
sequences now render 'Interp=Step;' explicitly), and 2 roundoff
fixture issues. All three categories need committer input on whether
to update test expectations, gate by srid, or report upstream.
Cherry-picks the equivalent of bump/meos-1.3 commits 52e8237 +
d8f9c02 + 22110ef. PyMEOS 1.4 needs the same workflow shape: apt
postgresql-server-dev-16, head_ref-first PyMEOS-CFFI checkout, and
MEOS branch derived from the package version (so 1.4 picks up MEOS
master / future stable-1.4 automatically).
Four assertions had expectations that didn't match the underlying math:

- tests/collections/time/dateset_test.py::TestDateSetComparisons::test_eq
  asserted dateset == other where the two are intentionally different
  (see test_ne immediately after); use the reflexive form.

- tests/main/tfloat_test.py::test_temporal_equal_int passes
  TFloatSeq('[1.5, 2.5]').temporal_equal(1). The fixture expected
  True at start (1.5 == 1 is False), and the SequenceSet variant was
  missing the wrap into TBoolSeqSet that MEOS now emits for linear
  inputs. Updated to all-False, wrapped correctly.

- tests/main/tfloat_test.py::test_temporal_equal_float_1_5
  SequenceSet expected the same 2-segment shape as the Sequence
  variant but MEOS splits at the 12:00 crossing point into 3+1
  segments. Updated to the actual output.

- tests/main/tfloat_test.py::test_temporal_not_equal_int_1 asserted
  False at start and True at end. For [1.5, 2.5] linearly varying,
  != 1 is True everywhere. Updated to all-True.

Pytest now 4482 / 4482 (100%).
The 1.4 surface (PR #81) cdef's meos_cbuffer.h / meos_pose.h /
meos_rgeo.h, which only land in /usr/local/include when the build
enables those features. Without them the cffi step bails on
FileNotFoundError. Mirrors the PyMEOS-CFFI pr_build.yml flags.
…etry temporal types

Add first-class PyMEOS classes for the four extended temporal type
families, mirroring the existing tpoint/ttext pattern and reusing the
exact pymeos_cffi backing functions (no reimplementation):

- main: TNpoint/TCbuffer/TPose/TRgeometry (+ Inst/Seq/SeqSet)
- collections: base value types Npoint/Cbuffer/Pose (+ their *Set) and
  Nsegment; rgeometry's base is a shapely geometry (reuses GeometrySet)
- factory + package exports wired for all four families; tests added

Each type covers 100% of its real MEOS-backed surface, verified against
the MEOS headers:

- TCbuffer exposes the full spatial-relationship surface MEOS provides
  (ever / always / temporal: contains, covers, disjoint, within-distance,
  intersects, touches), mirroring TPoint.
- MEOS defines no spatial-relationship API for npoint, pose or
  rgeometry, so their relationship surface is complete at zero by
  construction (documented, not excluded, not faked).
- TRgeometry additionally exposes its full backed surface: segments,
  delete (timestamptz/tstzset/tstzspan/tstzspanset), before/after a
  timestamp, to_instant, and the sequence-set accessors
  (start_sequence/end_sequence/sequence_n/sequences) -- each delegating
  to the rigid-geometry-specific trgeo_* function.
- TNpoint.positions() exposes tnpoint_positions via a new Nsegment
  wrapper (full nsegment_* surface).

Omitted only where MEOS provides no usable backing: trgeometry has no
string input parser (built via from_geometry_tpose / hexwkb), and the
Datum-bearing trgeo_restrict_value/values helpers are MEOS-internal
(consistent with how the ecosystem treats Datum helpers).

The factory resolves the four type tags through the compiled binding
(`_meos_cffi.lib`, the same source pymeos_cffi builds its enums from),
so the build imports cleanly and the types activate even where
pymeos_cffi's MeosType enum does not yet wrap the cbuffer/pose/rgeometry
tags.

TCbuffer's value accessors and value-based constructors
(start_value/end_value/value_set/value_at_timestamp, from_base_time,
from_mfjson) raise NotImplementedError citing the missing MEOS backing
(MEOS exposes no typed tcbuffer_* value accessor / tcbufferinst_make;
MF-JSON input is tracked upstream by MobilityDB#1051). This keeps
TCbuffer/Inst/Seq/SeqSet concrete and instantiable rather than abstract.
Reads a vendored meos-idl.json and emits one PyMEOS OO method per
regular method-family member (comparison, spatial relationship, distance,
restriction) into tools/oo_codegen/_preview/<family>_methods.py.

Each generated method dispatches, by the argument's C-name token, to the
exact pymeos_cffi backing function the hand-written surface uses, so it
is identical to its hand-written counterpart by construction; nothing is
reimplemented. The irregular core (constructors, conversions, accessors,
transforms, I/O) stays hand-written and is counted, not dropped.

tools/oo_codegen/_preview/ is a non-destructive Draft artifact the
package never imports; it lets a reviewer diff the generated uniform
surface against pymeos/main/*.py before any staged migration. Delivery
mirrors GoMEOS PR #2; the dispatch/backing model mirrors PyMEOS PR #87.

Coverage: 2833 catalogued functions fully accounted for - 107 regular
methods (407 typed overloads) across all six families (cbuffer, geo,
npoint, pose, rgeo, temporal), 864 hand-written core, 43 Datum-bearing
internals excluded, 1519 out of scope. Zero unaccounted.

Adds tests/test_oo_codegen_coverage.py (offline gate: zero unaccounted,
six families, preview compiles and stays in lock-step with the
generator, every backing symbol is a real catalogued function) and a
CI step running the generator's accounting check.
Promotes the meos-idl.json-driven OO codegen from Draft preview to a
wired-in solution for the TCbuffer vertical slice.

- Generator (tools/oo_codegen/codegen.py): adds a behaviourally-faithful
  emitter (--mixin) that reproduces the hand-written isinstance ladder,
  argument transforms, result post-processing and super()/raise fallback
  exactly; only modelled families are wired (one family per PR). The
  Draft _preview/ path and the #89 coverage gate are unchanged.
- pymeos/main/_generated/tcbuffer_methods.py: generated TCbufferRegularMixin
  (30 regular methods: comparison, spatial relationship, distance,
  restriction), each dispatching to the exact pymeos_cffi backing the
  hand-written method used.
- pymeos/main/tcbuffer.py: inherits TCbufferRegularMixin; the 30
  now-generated regular methods are removed (never_* and the irregular
  hand-written core are kept).
- Provenance: regen-from-meos-api.sh + README document that the catalog
  is byte-schema-identical to MEOS-API run.py output and the generator
  runs unchanged on it.

Behavioural equivalence is proven by the existing suite: the full
tests/main/tcbuffer_test.py is byte-identical before and after the
switch -- 26 passed, 1 failed, where the single failure is a pre-existing
unrelated pymeos_cffi cbuffer_as_hexwkb out-parameter bug in the base
Cbuffer class (not temporal, not codegen). Static check: 30/30 generated
methods are backing-and-fallback identical to the hand-written oracle.

Stacked on #89; relies on #88's TCbuffer being instantiable.
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.
Third family in the codegen-switch fan-out (after TCbuffer #90, TPose
#91), independent of the upstream MEOS gate.

- codegen.py: add the `npoint` FAMILY_MODEL (+ `npointset` and `point`
  arg tokens; tnpoint distance spells `tdistance_tnpoint_point`). Make
  shortest_line's precision per-family (npoint: `precision: int = 15`;
  tcbuffer/tpose hardcode 10 with no param and omit the key, so #90/#91
  regenerate byte-identical -- verified). Draft _preview path and #89
  gate untouched.
- pymeos/main/_generated/tnpoint_methods.py: generated TNpointRegularMixin
  (12 regular methods -- comparison, temporal comparison, restriction,
  distance; npoint has no spatial-relationship API). Static check: 12/12
  backing-and-fallback identical to the hand-written oracle.
- pymeos/main/tnpoint.py: inherits the mixin; 12 now-generated regular
  methods removed; never_*/irregular core kept. Adds documented
  NotImplementedError stubs (value accessors -> MobilityDB#1082,
  from_base_time -> #1084, from_mfjson -> #1086) so TNpoint*/Inst/Seq/
  SeqSet are concrete.
- pymeos/collections/npoint/npointset.py: NpointSet shipped abstract in
  #88 (pre-existing base-collection defect, unrelated to the switch);
  add the 5 missing Set ops as thin super() delegations to the generic
  base Set (the established GeoSet.contains pattern), making NpointSet
  concrete.

Proof: tests/main/tnpoint_test.py is byte-identical hand-written vs
wired (A/B, stubs+NpointSet-fix held constant) -- both error IDENTICALLY
at the same pre-existing point (`tnpoint_test.py:211` TestNpointSet,
MeosTextInputError 'Missing delimiter'), so the switch delta is provably
zero. That collection error is a pre-existing #88/MEOS npointset
test-fixture/parser issue OUTSIDE the codegen-switch domain (base
NpointSet, not the temporal regular families), unrelated to and
unaffected by this change -- a separate follow-up. Static 12/12
identical is the positive proof. Stacked on #91.
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_<timetype>`+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.
@estebanzimanyi
estebanzimanyi force-pushed the feat/oo-dispatch-consumer branch 2 times, most recently from 748003c to 2aa1e05 Compare May 19, 2026 12:56
geo (TGeomPoint/TGeogPoint) and temporal (TFloat/TInt/TBool/TText) are
the 2 of 6 families not switched to generated mixins. Verified root
cause: their regular-method dispatch encodes editorial decisions absent
from meos-idl.json (geo: STBox-via-stbox_to_geo, Point/Geometry split,
runtime geodetic flag, GeoSet->generic; temporal: Int->Float coercion
via super(), scalar-by-value casts, generic *_temporal_temporal self).

A PyMEOS-local FAMILY_MODEL extension was rejected as the transcription
anti-pattern (relocates editorial logic into per-binding config; no
equivalence by construction; diverges across bindings).

This RFC proposes the sound, ecosystem-correct solution: an
oo.<family>.<member>.dispatch annotation in MEOS-API's
meta/meos-meta.json (merged into meos-idl.json like the existing shape
annotations GoMEOS already consumes), so every binding's faithful
generator derives geo/temporal from the SAME canonical catalog facts --
equivalence by construction restored at the catalog level, A/B suite as
the unchanged acceptance gate, ecosystem-wide 100%.

The §3 dispatch tables are extracted verbatim from PyMEOS's hand-written
oracle and are complete for geo/temporal. Cross-repo handoff: MEOS-API
(parallel-owned) lands the metadata; PyMEOS then consumes it and
generates the final 2 families. Until then geo/temporal correctly stay
hand-written (fully functional -- no API-parity gap; only the
codegen-uniformity goal is outstanding). Stacked on #93.
@estebanzimanyi
estebanzimanyi force-pushed the docs/rfc-dispatch-metadata branch from 8d57587 to b0fe78e Compare May 19, 2026 13:16
@estebanzimanyi
estebanzimanyi force-pushed the feat/oo-dispatch-consumer branch from 2aa1e05 to f403dcc Compare May 19, 2026 13:34
Implements the PyMEOS side of RFC #94 §5/§7: a consumer that emits
faithful geo/temporal mixins from the canonical objectModel.dispatch
metadata (MEOS-API #10 feat/object-model, merged into meos-idl.json),
closing the OO codegen to all 6 temporal type families with
equivalence by construction at the catalog level.

Additive only — the 4 derivable families' FAMILY_MODEL path
(emit_faithful_mixin) is untouched; #90/#91/#92/#93 mixins regenerate
byte-identical (--verify-oo-roundtrip + OoDispatchConsumer
.test_roundtrip_byte_identical, permanently enforced) and the #89
coverage gate stays green.

geo + the 4 temporal concretes (TFloat/TInt/TBool/TText) are not
derivable from the signature catalog (RFC #94 §1), so they are driven
by the verbatim objectModel.dispatch metadata and proven equivalent to
the hand-written oracle by dispatch-skeleton equality:

  * tools/oo_codegen/_d1-dispatch-extended-fixture.json — the verbatim
    §7 dispatch metadata (== MEOS-API #10 objectModel.dispatch).
  * tools/oo_codegen/_oracle_extended_methods.py — the byte-for-byte
    hand-written editorial methods (AST-extracted) the proof checks
    against; never executed, never imported.
  * --verify-oo-dispatch-extended + OoDispatchConsumer
    .test_extended_dispatch_matches_oracle: each of the 32 editorial
    methods (geo at/minus/distance/nearest_approach_distance + each
    concrete's always/ever/temporal eq·ne, at, minus) reduces to an
    identical ordered dispatch skeleton — isinstance type-set, backing
    symbol, arg transform, super-coerce, fallback, result wrap. 0
    divergence: same native calls, same transforms, never reimplemented.

The closed argTransform vocabulary maps to PyMEOS idioms
(scalarValue, scalarCast-per-base, geoToGserialized with
geodeticFromSelf, stboxToGeo, innerPtr, textsetMake, list[str],
coerce + via:super).

Stacked on #94 (the RFC, including the verbatim §7 SoT).
@estebanzimanyi
estebanzimanyi force-pushed the feat/oo-dispatch-consumer branch from f403dcc to c850df1 Compare May 19, 2026 13:36
estebanzimanyi added a commit to estebanzimanyi/PyMEOS that referenced this pull request Jun 25, 2026
master predates the meos-idl.json OO codegen, which lives in the open stack.
Add tools/pin/compose-order.txt (the canonical fold manifest with the
disposition of every open PR; MobilityDB#95 is the verified OO-codegen frontier
containing MobilityDB#88/MobilityDB#89 and the family-switch stack) and GENERATION.md (the
per-binding generator policy: PyMEOS is two layers — PyMEOS-CFFI low-level +
the PyMEOS OO layer generated by tools/oo_codegen/codegen.py; the regular
families switch to generated mixins generate-then-retire).
Switches TPoint and TFloat/TInt/TBool/TText to the objectModel.dispatch consumer for their editorial (non-token-derivable) members, mirroring the tcbuffer/npoint/pose/rgeometry switch: each class inherits its generated <Fam>DispatchMixin and the corresponding hand-written methods (at/distance/minus/nearest_approach_distance for TPoint; the equal-family + at/minus for the temporal concretes) are removed. Token-derivable methods stay hand-written.

The vendored tools/oo_codegen/meos-idl.json gains the objectModel facet (the function set is unchanged) so the dispatch consumer runs against the catalog in-tree. codegen.py imports each mixin's own temporal type only under TYPE_CHECKING and lazily in-method, matching the FAMILY_MODEL template, so wiring the mixin into the class module does not create an import cycle.

codegen.py --check, --verify-oo-dispatch-extended and --verify-oo-roundtrip all pass; the generated members dispatch to the exact pymeos_cffi backings the hand methods used, so behaviour is identical by construction.
@estebanzimanyi
estebanzimanyi force-pushed the docs/rfc-dispatch-metadata branch from b0fe78e to eca825b Compare October 22, 2026 12:00
@estebanzimanyi
estebanzimanyi changed the base branch from docs/rfc-dispatch-metadata to refactor/oo-codegen-trgeometry-switch October 22, 2026 22:59

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant