Use this skill to verify GIS correctness of spatial operations, check predicate semantics, validate geometry conventions (winding, closure, DE-9IM), understand edge cases (degenerate geometries, null/empty handling), or answer "does this make sense?" questions about spatial algorithms. This is the domain knowledge oracle for autonomous agents working on vibeSpatial. Trigger on: "is this correct", "should this return", "what does X mean in GIS", "DE-9IM", "winding", "orientation", "edge case", "degenerate", "predicate semantics", "topology", "validity".
Use this reference to verify whether a spatial operation produces the correct result, to understand edge cases, or to validate design decisions.
Question: $ARGUMENTS
The 9 standard binary predicates are defined by the Dimensionally Extended 9-Intersection Model (DE-9IM). Each predicate tests a specific relationship between the interior (I), boundary (B), and exterior (E) of two geometries.
| Predicate | Plain English | Key Rule |
|---|---|---|
intersects(a, b) | Any shared points | NOT disjoint |
disjoint(a, b) | No shared points | Interior, boundary, exterior all separate |
within(a, b) | a entirely inside b | Interior of a inside interior of b; boundary of a may touch boundary of b |
contains(a, b) |
| b entirely inside a |
within(b, a) |
touches(a, b) | Boundaries touch, interiors don't | Shared points exist but only on boundaries |
crosses(a, b) | Geometries cross | Interior intersection has lower dimension than max of inputs |
overlaps(a, b) | Partial overlap, same dimension | Interior intersection has same dimension as both inputs |
covers(a, b) | No point of b outside a | Like contains but boundary-on-boundary is OK |
covered_by(a, b) | No point of a outside b | covers(b, a) |
contains_properly(a, b) | b in interior of a | No boundary contact at all |
Point on polygon boundary:
within(point, polygon) = True (boundary contact allowed)contains(polygon, point) = Truetouches(point, polygon) = True (boundary-only intersection)contains_properly(polygon, point) = False (point is on boundary, not interior)intersects(point, polygon) = TruePoint on polygon vertex: Same as point-on-boundary — vertices are part of the boundary.
Line endpoint touching polygon boundary:
touches(line, polygon) = True (if only endpoint touches)intersects(line, polygon) = Truecrosses(line, polygon) = False (no interior penetration)Identical geometries:
within(a, a) = Truecontains(a, a) = Trueoverlaps(a, a) = False for points/lines (same dim but identical = not "partial")equals(a, a) = TrueEmpty geometries:
intersects(empty, anything) = Falsedisjoint(empty, anything) = TrueNull geometries:
| Symmetric | Asymmetric |
|---|---|
| intersects, disjoint, touches, crosses, overlaps | within/contains, covers/covered_by, contains_properly |
For asymmetric predicates: within(a, b) == contains(b, a).
signed_area = 0.5 * sum(x[i]*y[i+1] - x[i+1]*y[i])A positive shoelace result means CCW. This is the OGC/ISO convention used by Shapely, PostGIS, and vibeSpatial.
Rings MUST be closed: first coordinate == last coordinate. A triangle
requires 4 coordinate pairs: [(0,0), (1,0), (0,1), (0,0)].
Minimum valid ring: 4 coordinates (3 unique + closing).
| Invalid Geometry | Description | Result of make_valid() |
|---|---|---|
| Bowtie polygon | Self-intersecting exterior ring | Split into two triangles (MultiPolygon) |
| Touching hole | Hole boundary touches exterior at >1 point | Split into valid parts |
| Inverted hole | Hole has CCW winding (same as exterior) | Reverse hole orientation |
| Unclosed ring | First != last coordinate | Close ring by appending first point |
| Spike/cut | Zero-area protrusion or cut | Remove degenerate parts |
is_closed: first point == last point (forms a LinearRing)distance(a, b) <= d(minx, miny, maxx, maxy)(NaN, NaN, NaN, NaN)(NaN, NaN, NaN, NaN)(x, y, x, y) (zero-extent box)These are set operations on the point sets of two geometries:
| Operation | Result | Points in result |
|---|---|---|
intersection(a, b) | A AND B | Points in both a and b |
union(a, b) | A OR B | Points in a or b (or both) |
difference(a, b) | A AND NOT B | Points in a but not b |
symmetric_difference(a, b) | A XOR B | Points in a or b but not both |
Return type depends on input and result dimension:
Edge cases:
intersection(a, a) = a (identity)union(a, a) = adifference(a, a) = emptyintersection(a, disjoint_b) = emptybuffer(geom, distance): All points within distance of geombuffer(geom, 0): For polygons, equivalent to make_valid (repair trick)buffer(point, r): Circle approximation (polygon with quad_segs segments per quarter)buffer(line, r): Corridor around the linebuffer(polygon, -r): Erode inward (may produce empty if too thin)dissolve(by=column): Union all geometries sharing the same group keyThis is the #1 source of subtle bugs. Null and empty are DIFFERENT.
| Null | Empty | |
|---|---|---|
| Meaning | "No value" / missing | "Valid geometry with no points" |
| In pandas | None / NaN | shapely.Point() (empty Point) |
| Predicate result | None (propagate) | False (except disjoint=True) |
| Metric result | NaN | 0.0 |
| Bounds result | (NaN, NaN, NaN, NaN) | (NaN, NaN, NaN, NaN) |
| In OwnedGeometryArray | validity[i] = False | validity[i] = True, zero-span offsets |
| Buffer | None | Empty |
| Union with valid | None | The valid geometry |
| Intersection with valid | None | Empty |
vibeSpatial maintains a deterministic corpus of degenerate geometries for testing edge cases. When writing or verifying operations, test against these:
File: src/vibespatial/testing/degeneracy.py
| Case | What It Tests |
|---|---|
shared_vertex_lines | Two lines meeting at endpoint → touches = True |
collinear_overlap_lines | Lines on same line with overlap → overlaps = True |
donut_window_polygon | Polygon with hole, clipped by window crossing hole |
duplicate_vertex_polygon | Repeated adjacent vertices → stable under overlay |
bowtie_invalid_polygon | Self-intersecting → requires make_valid first |
touching_hole_invalid_polygon | Hole touching shell → invalid |
null_and_empty_polygon_rows | Null/empty rows disappear cleanly |
When verifying correctness, compare against Shapely (the reference implementation):
# Geometry equality
shapely_result.equals_exact(gpu_result, tolerance=1e-9)
# Float equality
math.isclose(gpu_value, shapely_value, rel_tol=1e-7, abs_tol=1e-9)
# Null handling
# Both null → equal
# One null, one not → NOT equal
# Both empty, same type → equal
Standard tolerances:
rtol=1e-7 (relative) for fp64 computeatol=1e-9 (absolute) for fp64 computertol=1e-3 to 1e-4 depending on kernel class| Class | Guarantee | What It Means |
|---|---|---|
| COARSE | Bounded error | Result is approximate but within known error bounds |
| METRIC | Bounded error | Accumulation uses Kahan summation to control drift |
| PREDICATE | Exact | Correct boolean result even for degenerate inputs |
| CONSTRUCTIVE | Exact | Output topology is valid; no missed intersections |
Exact predicates use adaptive precision:
|orientation| <= error_bound: mark ambiguouserror = (3.0 + 16.0 * eps) * eps * magnitudeUse these rules to sanity-check operation results: