Validation & repair¶
Real-world geometry includes self-intersections, disconnected interiors, non-finite coordinates, missing CRS metadata, and excess coordinate precision.
Construction validates structure, not topology¶
Constructors reject structurally malformed input immediately. Non-finite
coordinates and rings with too few vertices have no valid interpretation and
raise InvalidGeometryError:
import gometry as gm
for ring in ([(0, 0), (1, 0)], [(0, 0), (1, 0), (float("nan"), 1)]):
try:
gm.Polygon(ring)
except gm.InvalidGeometryError as e:
print("rejected:", e)
rejected: polygon rings require at least three coordinates, got 2
rejected: coordinate must be finite
What constructors do not do is enforce OGC topological validity. A ring that closes cleanly but self-intersects — a "bowtie" — is a well-formed structure with an ill-formed shape, so it is accepted as an invalid geometry rather than rejected:
import gometry as gm
bowtie = gm.Polygon([(0, 0), (2, 2), (2, 0), (0, 2)])
print("built:", bowtie.geometry_type, "| valid:", bowtie.validate().valid)
built: Polygon | valid: False
Constructors and parsers can admit structurally valid but topologically invalid
geometry. Foreign data can arrive through gm.from_wkt,
gm.from_wkb, or gm.from_geojson.
Either way, you detect invalidity with
validate() and fix it with repair().
Cosmetic noise that does not change the shape is, by contrast, valid: a
repeated consecutive vertex does not itself invalidate a line with at least two
distinct XY positions or a ring with at least three; a clockwise shell is also
valid because ring orientation does not affect validity. A nonempty line whose
coordinates all share one XY position is invalid;
repair() converts it to a Point.
remove_repeated_points and
orient_polygons can canonicalize valid
redundancy when required.
Diagnosing: is_valid and validate¶
The Geometry.is_valid property returns a plain
bool; vectorized code uses the GeometryArray.is_valid mask.
validate returns a
ValidationReport without raising. The report
supports branching on validity, logging diagnostics, and repairing only bad records
(GeometryArray.validate() yields one report per element).
import gometry as gm
bowtie = gm.Polygon([(0, 0), (2, 2), (2, 0), (0, 2)])
try:
gm.require(bowtie)
except gm.InvalidGeometryError as e:
print("invalid:", e)
report = (gm.box(0, 0, 1, 1)).validate()
print("valid box →", report.valid)
invalid: exterior ring has a self-intersection
valid box → True
The returned ValidationReport exposes .valid,
.reason (a human-readable message that names the actual fault — e.g. a
self-intersection in the exterior ring), .location (the (x, y) coordinate of
the first problem), .path (a structural path to it, e.g. '$.shell'),
and the stable .code and human-readable
.detail fields. For
an OGC-valid geometry .reason, .location, .path, .code, and .detail
are all None; they carry detail when topology validation finds a problem.
Geographic validity recognizes longitude aliases, the antimeridian, and poles, including pole-enclosing shells and holes. Shared seam edges with polygon interiors on opposite sides are chart cuts; overlapping interiors on the same side remain invalid.
Coordinate-domain checks also apply:
an out-of-range latitude reports latitude_out_of_range, and a once-winding
unmarked ring whose latitude range does not identify one pole reports
ambiguous_full_turn_ring. is_valid returns False for either issue without
raising, and GeometryArray.is_valid writes False for the affected row.
validate reports the issue without raising.
Geographic operations still raise
InvalidGeometryError for unsupported coordinates. repair raises the same
exception when it cannot reconstruct an out-of-domain coordinate.
For quotient-topology issues, the witness is a canonical coordinate of the issue's contact set, chosen deterministically by minimum latitude then longitude.
The same coordinates without a CRS, or under a projected CRS, remain ordinary planar coordinates. Attaching a geographic frame supplies its periodic longitude seam and pole, while a planar frame has no identified seam.
is_valid, is_simple, is_ring, validate, require, and
self_intersections all use the same
geographic frame. repair validates the quotient result before returning it,
and snap_to_grid(..., repair=True) checks
each snapped result in that same frame.
The latitude domain issue retains its source ring path and coordinate witness:
import gometry as gm
polygon = gm.Polygon([(0, 0), (10, 0), (10, 100), (0, 0)], crs=4326)
report = polygon.validate()
print(report.code, report.path, report.location)
print("valid:", polygon.is_valid)
latitude_out_of_range $.shell (10.0, 100.0)
valid: False
Validation reports and boundary errors
validate() returns a report without raising. require() raises the
matching domain exception when its validity, CRS, or axes contract fails.
is_simple and is_ring answer narrower topological questions; neither is
a general structural check or a replacement for polygon validity.
Fixing: repair¶
When repair succeeds, it returns a valid geometry; it can refuse input it
cannot reconstruct. It offers two strategies via method:
method |
Strategy |
|---|---|
'linework' (default) |
re-node the boundary linework and reassemble — boundary-faithful |
'structure' |
rebuild valid polygonal areas from the input — area-faithful |
The bowtie repairs into a valid geometry:
import gometry as gm
bowtie = gm.Polygon([(0, 0), (2, 2), (2, 0), (0, 2)])
fixed = bowtie.repair()
print(fixed.geometry_type, "valid:", fixed.validate().valid)
fixed_struct = bowtie.repair(method="structure")
print(fixed_struct.geometry_type, "valid:", fixed_struct.validate().valid)
MultiPolygon valid: True
MultiPolygon valid: True
- input
- result
The two strategies genuinely differ once regions overlap: linework folds
by even-odd parity (a doubly-covered overlap cancels), structure unions the
enclosed areas:
import gometry as gm
overlap = gm.from_wkt(
"MULTIPOLYGON (((0 0, 4 0, 4 4, 0 4, 0 0)), ((2 2, 6 2, 6 6, 2 6, 2 2)))"
)
print("linework:", overlap.repair().area)
print("structure:", overlap.repair(method="structure").area)
linework: 28.0
structure: 28.0
A collapsed line repairs to its first coordinate as a Point. Collapsed MultiLineString members become Point or MultiPoint values; surviving linework is retained alongside them in a GeometryCollection:
import gometry as gm
collapsed = gm.from_wkt("LINESTRING (2 3, 2 3)")
print(collapsed.is_valid, collapsed.validate().code, collapsed.repair().to_wkt())
mixed = gm.from_wkt("MULTILINESTRING ((0 0, 0 0), (1 1, 2 2))")
print(mixed.repair().to_wkt())
False line_collapsed POINT (2 3)
GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (1 1, 2 2))
Already-valid input is returned as-is after the validation check on every surface:
geom.repair() and array.repair() reuse valid rows. Z/M ordinates are carried
through the rebuild.
For a geographic periodic-seam crossing, the validity check runs before any
rebuild. A valid input therefore keeps its original coordinates exactly; an
invalid crossing is repaired from the seam-split topology. The same
rule powers snap_to_grid(..., repair=True).
repair can split, merge, or drop parts to reach validity. Choose the method
whose boundary or area interpretation matches your data.
Enforcing contracts at the boundary¶
Beyond OGC validity, a boundary can require a CRS, valid topology, or a
particular dimensionality.
gm.require is the single boundary API: it accepts an
existing geometry or an external geometry-like object, returns the geometry
with its coordinate values unchanged on success, and raises the matching
specific exception otherwise.
With crs=, it stamps CRS-free coordinates,
relabels an operationally equivalent existing CRS to the requested spelling,
and rejects a conflicting frame with CRSMismatchError.
When crs is omitted or None, an existing CRS is preserved. Coordinate
epochs are preserved and revalidated against the resulting CRS.
The crs= parameter uses the CrsInput contract documented by
CRS. Accepted forms include an EPSG integer, an authority,
WKT, or PROJ string, an (authority, code) pair, a PROJJSON or CF mapping,
and an object exposing to_wkt(), such as gometry.CRS or pyproj.CRS.
Conflicting legacy CRS metadata in GeoJSON raises ParseError;
native geometries and EWKT/EWKB embedded SRIDs raise
CRSMismatchError.
InvalidGeometryError remains the exception for invalid geometry or mismatched
axes.
import gometry as gm
def ellipsoidal_area_km2(poly):
poly = gm.require(poly, crs=4326)
return poly.area / 1e6
ok = gm.box(2, 48, 3, 49, crs=4326)
print(f"{ellipsoidal_area_km2(ok):.1f} km²")
try:
ellipsoidal_area_km2(gm.box(2, 48, 3, 49))
except gm.CRSMismatchError as e:
print("rejected:", e)
8217.7 km²
The optional crs= and axes= keywords add frame and storage contracts to
that same validation call:
import gometry as gm
raw = "POLYGON ((2 48, 3 48, 3 49, 2 49, 2 48))"
area = gm.require(raw, crs=4326, axes="XY")
print("meets contract:", area.crs == gm.CRS(4326), area.coordinate_axes)
meets contract: True XY
Use axes='xy' when your storage accepts only 2D geometry.
Cleaning: quantize and remove_repeated_points¶
Floating-point coordinates can carry more digits than the data requires, affecting snapping and overlay results. Two targeted cleaners handle common coordinate noise without a full repair.
geom.quantize(precision) rounds every coordinate to precision decimal
places. It gives coordinates from different sources a common precision for
overlay, equality, deduplication, and interchange.
Negative precision rounds to tens (-1), hundreds (-2), and larger decimal
steps. All present X/Y/Z/M ordinates use ties-to-even rounding, as with Python's
round; tiny coordinates can use more than 15 decimal places. WKT and WKB
writers' precision= applies this same operation before serialization.
import gometry as gm
noisy = gm.LineString([(1.11119, 2.22229), (3.33339, 4.44449)])
print((noisy.quantize(2)).to_wkt())
LINESTRING (1.11 2.22, 3.33 4.44)
- input
- result
remove_repeated_points drops consecutive
duplicate vertices. The optional tolerance also collapses vertices closer than
that distance, allowing oversampled tracks to be thinned.
import gometry as gm
dup = gm.LineString([(0, 0), (0, 0), (1, 1), (1, 1), (2, 2)])
print(dup.remove_repeated_points().to_wkt())
LINESTRING (0 0, 1 1, 2 2)
- input
- result
The related tools snap and normalize handle the other common sources of
geometric noise (near-misses that should coincide, and non-canonical vertex
order / ring orientation).
Precision workflow before overlay
Overlay operations are sensitive to near-coincident vertices. An explicit
quantize / snap_to_grid pass can help sources align — but it can also
create invalidity on tight geometries (see choosing). Prefer
a precision workflow followed by validate / repair rather than
treating quantize as a universal sliver cure — see
constructive operations.
A typical ingest pipeline¶
An ingest pipeline can combine a dimension contract, validity checking, and repair:
import gometry as gm
def ingest(payload):
try:
geom = gm.from_wkt(payload, crs=4326)
except gm.ParseError:
raise
geom = geom.remove_repeated_points()
if not geom.is_valid:
geom = geom.repair()
return gm.require(geom, crs=4326, axes="XY")
result = ingest("POLYGON ((0 0, 2 2, 2 0, 0 2, 0 0))")
print(result.geometry_type, "valid:", result.validate().valid)
MultiPolygon valid: True
Parse failures (ParseError) stay separate from geometric invalidity
(InvalidGeometryError / repair). The detect → repair → assert flow keeps invalid
geometry from ever reaching your predicates,
overlays, or measurements, where it would otherwise
produce unexpected answers. See Security for parsing
and resource limits.
Crossing a geographic longitude seam (±180° for EPSG:4326) does not itself make
geometry invalid. gometry's topology operations handle the crossing;
see Working across the antimeridian for when
you must call split_antimeridian yourself.
See also¶
- Predicates — robust boundary semantics on validated input.
- Geometry — the Z/M ordinate model and storage contracts.
- CRS, units & measurement — reprojection and the antimeridian.
- Security — untrusted formats and resource limits.
- API: Geometry.validate · Geometry.repair · require