Python usage¶
Install from PyPI with uv add topology-checker, or use
uv add 'topology-checker[geopandas]' for GeoDataFrame exports and file loading
through GeoPandas. Python 3.10 through 3.14 on regular CPython are supported.
The base package uses Shapely and pyproj. It does not require GeoPandas or a
separate Rust installation when a compatible wheel is available. See the
API reference for every class and function.
Register and validate¶
import geopandas as gpd
from topology_checker import Rule, Topology
roads_gdf = gpd.read_file("roads.gpkg")
topology = Topology(tolerance=0.001)
topology.add_rule(Rule.MUST_NOT_HAVE_DANGLES, roads_gdf)
report = topology.validate()
errors = report.to_geodataframe()
print(report.is_valid, report.error_count, report.warnings)
add_rule accepts a Shapely geometry, a geometry iterable, a GeoSeries,
a GeoDataFrame, a GeoJSON mapping, or an object with __geo_interface__.
Pass the second layer with the keyword argument other=. Use enum members
such as Rule.MUST_NOT_OVERLAP; parse serialized names with
Rule("must-not-overlap") before registration.
Each raw input creates a fresh XY snapshot at registration. Later edits to a
GeoDataFrame or list do not change that layer. Passing the same raw object to
two rules creates two layers, and a generator is consumed on each call.
validate() returns an immutable report snapshot; topology violations are
results, while invalid registrations raise exceptions and leave the topology
unchanged.
Reuse a layer explicitly¶
from shapely.geometry import LineString
from topology_checker import Rule, Topology
topology = Topology(tolerance=0.001)
roads = topology.add_layer(
[LineString([(0, 0), (1, 0)]), LineString([(1, 0), (2, 0)])],
name="roads",
fids=[41, 92],
crs="EPSG:28992",
)
topology.add_rule(Rule.MUST_NOT_HAVE_DANGLES, roads)
topology.add_rule(Rule.MUST_NOT_OVERLAP, roads)
report = topology.validate()
print(topology.layer(roads).name)
print(report.to_dict())
The returned LayerId belongs to its originating Topology. Reuse it across
rules to share the registered layer and native caches. Binary rules require
distinct IDs. A RuleSpec returned by add_rule also exposes the registered
IDs through .layer and .other.
FIDs default to original input positions, independent of a GeoDataFrame's
index or GeoJSON feature IDs. Explicit FIDs must be unique signed 64-bit
integers, one per original position, including positions subsequently skipped.
Reports preserve FID and multipart provenance in .origin and .destination.
Missing and empty features are skipped with warnings, extra dimensions are
discarded, and nonfinite retained XY coordinates are rejected. Mixed nonempty
families are accepted only by rules with an ANY input signature.
Declare ambiguous empty layers¶
from topology_checker import GeometryKind, Rule, Topology
topology = Topology()
empty_lines = topology.add_layer([], kind=GeometryKind.LINE)
topology.add_rule(Rule.MUST_NOT_OVERLAP, empty_lines)
assert topology.validate().is_valid
Family inference uses nonempty parts first, then empty geometry hints. An empty
list, only missing values, or conflicting empty hints have no inferred family.
Rules with multiple possible signatures then require a declaration through
add_layer(..., kind=GeometryKind.POINT), LINE, or POLYGON. A declaration
must agree with every retained nonempty part. A uniquely matching rule, such as
MUST_NOT_HAVE_DANGLES, can resolve even with unknown empty input.
CRS and tolerance¶
The engine operates in a Cartesian XY plane. It does not reproject, calculate geodesic distances, or infer a CRS from coordinates. Reproject geographic data to an appropriate projected CRS before using distance tolerances.
Tolerance is finite, nonnegative, and expressed in the inputs' coordinate units.
For a CRS using metres, 0.001 is one millimetre. It is interpreted by each rule,
such as a distance threshold, a minimum retained line length, or a polygon sliver
filter. It does not apply a universal geometry buffer or repair geometries.
Inspect the concrete rule description for the relevant behavior.
All known layer CRSs must agree. Explicit CRS metadata must agree with metadata
on the input. Unknown CRSs are accepted as caller-supplied coordinates. When
known and unknown metadata are mixed, validation continues with a warning and
the export CRS is unset. Report.to_dict() works without GeoPandas;
Report.to_geodataframe() requires the geopandas extra.
Geometry-dependent semantics¶
The 24 public constraints resolve to the 38 concrete rules using the ordered
geometry families and whether other= is supplied. These examples illustrate
why the concrete variant matters:
| Public rule and inputs | Meaning |
|---|---|
MUST_NOT_OVERLAP, points |
Coincident point pairs within tolerance. |
MUST_NOT_OVERLAP, lines |
Shared linework between distinct features; crossings and self-overlaps are excluded. |
MUST_NOT_OVERLAP, polygons |
Shared areas between distinct features, excluding slivers below tolerance. |
MUST_NOT_OVERLAP, lines then polygons |
Interior or shared boundary line pieces longer than tolerance; isolated contacts are allowed. |
MUST_BE_INSIDE, points then polygons |
Strict interior membership; boundary proximity within tolerance is outside. |
MUST_BE_INSIDE, lines then polygons |
Pieces outside the polygon union longer than tolerance; boundary linework is covered. |
MUST_NOT_INTERSECT, lines |
Overlaps and interior crossings between distinct features; endpoint touches are allowed. |
MUST_NOT_INTERSECT, lines then polygons |
Interior pieces, shared boundary pieces and isolated contacts, including boundary proximity within tolerance. |
MUST_NOT_HAVE_GAPS, polygons |
Enclosed gaps and an outer-boundary diagnostic; inspect error categories. |
MUST_NOT_BE_CONTAINED_BY, any then any |
Whole cleaned features contained by individual targets, excluding mutual containment; no tolerance buffer. |
Order matters: line then polygon signatures do not imply polygon then line support. Multipart parts of one feature retain their FID; use the dedicated self-intersection or self-overlap constraints to examine a feature internally.
from shapely.geometry import Point, box
from topology_checker import Rule, Topology
topology = Topology()
topology.add_rule(Rule.MUST_BE_INSIDE, [Point(0.5, 0.5)], other=[box(0, 0, 1, 1)])
result = topology.validate().rules[0]
print(result.resolved_rule.value)
print(result.resolved_rule.description)
for variant in Rule.MUST_NOT_OVERLAP.variants:
print(variant.inputs, variant.description)
Use .resolved_rule on a RuleSpec, RuleReport, or Violation to inspect
the selected semantics. Invalid arity, unsupported families, or ambiguous
empty inputs are reported at registration with the supported signatures.
Line utilities¶
from shapely.geometry import LineString
from topology_checker import explode_lines, merge_lines
lines = [LineString([(0, 0), (1, 0)]), LineString([(1, 0), (2, 0)])]
merged = merge_lines(lines, tolerance=0.001)
segments = explode_lines(merged)
assert len(segments) == 2
Both utilities return tuples of Shapely lines. They accept the same input adapters and operate on planar snapshots.