PROACTIVELY USE THIS SKILL when wiring a new operation into the vibeSpatial Python dispatch stack — adding a public API method, connecting to GeometryArray, writing the dispatch wrapper, adding CPU fallbacks, handling OwnedGeometryArray coercion, or updating DGA/GeoPandas surface methods. This is the Python-side complement to $new-kernel-checklist (which covers the kernel itself). Trigger on: "wire dispatch", "add API", "add method", "GeometryArray", "DGA method", "CPU fallback", "dispatch wrapper", "public API", "coerce", "OwnedGeometryArray".
You are wiring a new operation into vibeSpatial's Python dispatch stack. This skill covers everything OUTSIDE the kernel: from the GeoPandas- compatible public API down to the dispatch wrapper that calls the GPU/CPU implementation.
Target operation: $ARGUMENTS
GeoSeries.area / .within(other) / .buffer(distance) [Layer 1: Public API]
|
_delegate_property() / _binary_op() / _delegate_geo_method() [Layer 2: Delegation]
|
GeometryArray.area / ._binary_method() / .buffer() [Layer 3: GeometryArray]
|
+-- if self._owned: dispatch to owned-path [Layer 4: Owned routing]
| |
| area_owned() / evaluate_geopandas_binary_predicate() [Layer 5: Dispatch wrapper]
| |
| plan_dispatch_selection() -> RuntimeSelection [Layer 6: Runtime selection]
| select_precision_plan() -> PrecisionPlan [Layer 7: Precision planning]
| |
| +-- GPU: _area_gpu(owned, precision_plan) [Layer 8: GPU kernel]
| +-- CPU: _area_cpu(owned) [Layer 9: CPU fallback]
|
+-- else: shapely.area(self._data) [Layer 10: Shapely fallback]
| Type | Examples | Entry Pattern | Return Type |
|---|---|---|---|
| Property | area, length, bounds | _delegate_property() | Series[float64] |
| Unary method | centroid, make_valid, simplify, normalize | _delegate_geo_method() | GeoSeries |
| Binary predicate | within, contains, intersects, touches | _binary_op() | Series[bool] |
| Binary constructive | intersection, union, difference | _binary_op() | GeoSeries |
| Parameterized | buffer(distance), offset_curve(distance) | _delegate_geo_method() | GeoSeries |
File: src/vibespatial/api/geometry_array.py
This is where the owned-path routing happens. Add your method here.
@property
def area(self):
if self._owned is not None:
from vibespatial.constructive.measurement import area_owned
return area_owned(self._owned)
return shapely.area(self._data)
def centroid(self):
if self._owned is not None:
from vibespatial.constructive.point import centroid_owned
result_owned = centroid_owned(self._owned)
return GeometryArray.from_owned(result_owned, crs=self.crs)
return GeometryArray(shapely.centroid(self._data), crs=self.crs)
Binary predicates route through _binary_method() which already handles
the dispatch. To add a new predicate:
src/vibespatial/predicates/binary.py in
supports_binary_predicate()._evaluate_gpu_de9im_candidates() handles it.GeometryArray._binary_method() — it already
dispatches all supported predicates.def simplify(self, tolerance, preserve_topology=True):
if self._owned is not None:
from vibespatial.constructive.simplify import simplify_owned
result_owned, selected = simplify_owned(
self._owned, tolerance, preserve_topology=preserve_topology,
)
record_dispatch_event(
surface="geopandas.array.simplify",
operation="simplify",
selected=selected,
)
if result_owned is not None:
return GeometryArray.from_owned(result_owned, crs=self.crs)
return GeometryArray(
shapely.simplify(self._data, tolerance, preserve_topology=preserve_topology),
crs=self.crs,
)
GeometryArray classself._owned when availableshapely.*() when _owned is Nonerecord_dispatch_event() called for observability*_owned() function)This is the bridge between GeometryArray and the kernel. Lives in the
implementation module (e.g., constructive/measurement.py).
from vibespatial.runtime.adaptive import plan_dispatch_selection
from vibespatial.runtime.precision import (
KernelClass, PrecisionMode, select_precision_plan,
)
from vibespatial.runtime._runtime import ExecutionMode
def area_owned(
owned: OwnedGeometryArray,
*,
dispatch_mode: ExecutionMode | str = ExecutionMode.AUTO,
precision: PrecisionMode | str = PrecisionMode.AUTO,
) -> np.ndarray:
row_count = owned.row_count
selection = plan_dispatch_selection(
kernel_name="geometry_area",
kernel_class=KernelClass.METRIC,
row_count=row_count,
requested_mode=dispatch_mode,
)
if selection.selected is ExecutionMode.GPU:
precision_plan = select_precision_plan(
runtime_selection=selection,
kernel_class=KernelClass.METRIC,
requested=precision,
)
try:
return _area_gpu(owned, precision_plan=precision_plan)
except Exception:
pass
return _area_cpu(owned)
def simplify_owned(
owned: OwnedGeometryArray,
tolerance: float,
*,
dispatch_mode: ExecutionMode | str = ExecutionMode.AUTO,
precision: PrecisionMode | str = PrecisionMode.AUTO,
) -> tuple[OwnedGeometryArray | None, ExecutionMode]:
selection = plan_dispatch_selection(
kernel_name="geometry_simplify",
kernel_class=KernelClass.CONSTRUCTIVE,
row_count=owned.row_count,
requested_mode=dispatch_mode,
)
if selection.selected is ExecutionMode.GPU:
precision_plan = select_precision_plan(
runtime_selection=selection,
kernel_class=KernelClass.CONSTRUCTIVE,
requested=precision,
)
try:
result = _simplify_gpu(owned, tolerance, precision_plan)
return result, ExecutionMode.GPU
except Exception:
pass
return _simplify_cpu(owned, tolerance), ExecutionMode.CPU
dispatch_mode and precision parametersplan_dispatch_selection() with correct kernel_name and kernel_classselect_precision_plan() before GPU pathEvery GPU operation MUST have a CPU fallback. Pattern:
@register_kernel_variant(
"geometry_simplify",
"cpu",
kernel_class=KernelClass.CONSTRUCTIVE,
execution_modes=(ExecutionMode.CPU,),
geometry_families=("polygon", "linestring", "multipolygon", "multilinestring"),
supports_mixed=True,
tags=("shapely",),
)
def _simplify_cpu(owned: OwnedGeometryArray, tolerance: float) -> OwnedGeometryArray:
shapely_geoms = owned.to_shapely()
results = shapely.simplify(shapely_geoms, tolerance)
return OwnedGeometryArray.from_shapely(results)
@register_kernel_variant (variant="cpu")owned.to_shapely() for input conversionOwnedGeometryArray.from_shapely() for output conversionFile: src/vibespatial/api/geo_base.py
Most methods are already wired via delegation helpers. If adding a new one:
# For properties:
@property
def my_property(self):
return _delegate_property("my_property", self)
# For unary methods:
def my_method(self, param):
return _delegate_geo_method("my_method", self, param=param)
# For binary operations:
def my_binary(self, other, align=None):
return _binary_op("my_binary", self, other, align)
align parameter present for binary ops (GeoPandas contract)File: src/vibespatial/geometry/device_array.py
If the operation should be callable on device-resident arrays without
materializing to Shapely, add it to DeviceGeometryArray:
@property
def area(self):
from vibespatial.constructive.measurement import area_owned
return area_owned(self._owned)
*_owned() (never materializes Shapely)When accepting external geometry inputs (e.g., binary operations where the right side comes from the user), use the coercion utilities:
from vibespatial.geometry.owned import coerce_geometry_array
# Coerce any input (Shapely list, GeoSeries, numpy array) to OwnedGeometryArray
owned = coerce_geometry_array(
input_data,
arg_name="right",
expected_families=(GeometryFamily.POLYGON, GeometryFamily.MULTIPOLYGON),
)
Coercion handles:
list[shapely.Geometry] → OwnedGeometryArray
np.ndarray of Shapely objects → OwnedGeometryArray
GeoSeries → extract .values._owned or coerce .values._data
Already an OwnedGeometryArray → pass through
External inputs coerced via coerce_geometry_array()
expected_families specified to catch type mismatches early
arg_name set for clear error messages
Record what happened for observability:
from vibespatial.runtime.dispatch import record_dispatch_event
record_dispatch_event(
surface="geopandas.array.simplify",
operation="simplify",
requested=dispatch_mode,
selected=selection.selected,
implementation="simplify_gpu_kernel",
reason=selection.reason,
)
| What | File |
|---|---|
| GeometryArray method | src/vibespatial/api/geometry_array.py |
| Public API (GeoSeries) | src/vibespatial/api/geo_base.py |
| DeviceGeometryArray | src/vibespatial/geometry/device_array.py |
| Dispatch wrapper | src/vibespatial/{module}/{operation}.py |
| Binary predicate routing | src/vibespatial/predicates/binary.py |
| Predicate support check | src/vibespatial/predicates/support.py |
| Dispatch planning | src/vibespatial/runtime/adaptive.py |
| Precision planning | src/vibespatial/runtime/precision.py |
| Dispatch events | src/vibespatial/runtime/dispatch.py |
| Kernel variant registry | src/vibespatial/runtime/kernel_registry.py |
| OwnedGeometryArray | src/vibespatial/geometry/owned.py |
| Coercion utilities | src/vibespatial/geometry/owned.py |
Forgetting the Shapely fallback — Every path through GeometryArray
must work when _owned is None. The else: shapely.*() branch is not
optional.
Materializing to host in GPU path — If your dispatch wrapper calls
owned.to_shapely() in the GPU branch, you have a bug. That's a D->H
transfer. Use device buffers directly.
Not recording dispatch events — Silent fallbacks are the #1 source of performance regressions. Always record what ran and why.
Wrong return type — Properties return numpy arrays. Geometry methods
return GeometryArray. Binary predicates return numpy bool arrays.
Getting this wrong breaks the GeoPandas contract.
Missing null propagation — Null rows (~owned.validity) must
produce NaN (metrics), None (predicates), or null geometry (constructive).
Check this explicitly.
Circular imports — Use lazy imports (from X import Y inside the
method body) for implementation modules. The GeometryArray module is
imported early; implementation modules import heavy CUDA dependencies.