MaxInscribedCircle
Finds the largest circle that fits inside a polygon. Holes act as boundary, so the circle must clear them too.
The maximum inscribed circle is the largest circle that fits inside a polygon. Every boundary segment limits it, so an interior ring limits it as well.
API
| Function | Use |
|---|---|
maximum_inscribed_circle |
Solve one polygon |
maximum_inscribed_circle_multipolygon |
Keep the best component |
maximum_inscribed_circle_slice |
Solve a batch with rayon |
maximum_inscribed_circle_with_workspace |
Reuse a built workspace |
MicResult reports the centre and the radius. It also reports the squared
radius, the indices of the segments the circle touches, and the candidate count.
used_engine and approach name the stage that answered. component_index
names the multipolygon component.
The batch entry point needs no feature, because rayon is a plain dependency.
Run cargo run --example quickstart for a complete program.
How it solves
The crate runs two stages.
The first stage handles a single ring of four or five vertices. It solves the triangle incenter, the axis-aligned rectangle, and the convex quad in closed form. It then measures each result against the segment table. A result that misses by more than 1e-9 relative falls through to the second stage.
The second stage is a grid solver. It runs branch and bound over the bounding
box and orders cells by the 1-Lipschitz bound on boundary distance. It refines
to diag * 1e-7. A compass hill climb then sharpens the incumbent. The search
pops at most 50,000 cells, and the climb finishes a long thin shape that the
budget cannot resolve.
The reported radius is always measured, never inferred. The crate measures the distance from the centre to the nearest boundary segment at the end of the solve, so a returned circle fits the polygon.
Correctness
tests/corpus_invariants.rs checks two properties.
The first property is fit. The reported radius must equal the measured clearance at the reported centre.
The second property is reach. No dense interior sample may clear more than the returned circle.
The workflow in .github/workflows/ci.yml runs the suite on every push and pull
request. It runs four feature sets, the 1.88 MSRV check, and a clippy, fmt, and
rustdoc job. No check needs an external solver.
Performance
The corpus holds 290 Polish cadastral parcels in
benches/data/realworld_290.geojson. Both solvers receive the same polygon and
the same tolerance. The timer covers the solve only, because both sides build
their indexes before the clock starts. Ratio is GEOS divided by native. Both
sides run serially, so the table measures the solver and not the threading.
| Measure | native | GEOS | Ratio |
|---|---|---|---|
| total, 290 parcels | 41.4 ms | 347.5 ms | 8.39x |
| cases native solves faster | 279 of 290 | 11 of 290 | 96% |
The ratio varies by case. Across the 290 parcels:
| Per-case ratio | Value |
|---|---|
| p10 | 2.0x |
| median | 6.3x |
| p90 | 14.2x |
The margin narrows as vertex count grows.
| Exterior vertices | n | native | GEOS | Ratio |
|---|---|---|---|---|
| 0-7 | 42 | 1.99 ms | 30.6 ms | 15.3x |
| 8-16 | 111 | 16.7 ms | 226 ms | 13.5x |
| 17-32 | 75 | 8.95 ms | 74.6 ms | 8.34x |
| 33-64 | 28 | 4.01 ms | 13.5 ms | 3.36x |
| 129+ | 14 | 6.91 ms | 13.1 ms | 1.90x |
Pinned to one core the ratio holds at 7.9x. The batch entry point is separate.
maximum_inscribed_circle_slice returns bit-identical results in input order and
runs 6.1x faster on 12 cores.
Repeat runs of the same tree land between 8.39x and 8.78x, so the second digit is noise. Regenerate before quoting a new figure.
Regenerate with
cargo run --release --features bench-geos --example compare_apples. The build
needs the GEOS C library. The GEOS dependency reads GEOS_LIB_DIR and
GEOS_VERSION when pkg-config cannot find an install.
Integration with the geo ecosystem
Enable geo-traits to accept a foreign polygon directly. WKB views, GeoArrow
arrays, and GeoZero streams then need no conversion.
| Function | Accepts |
|---|---|
maximum_inscribed_circle_polygon_trait |
any PolygonTrait with f64 coordinates |
maximum_inscribed_circle_multipolygon_trait |
any MultiPolygonTrait with f64 coordinates |
maximum_inscribed_circle_geometry |
geo_types::Geometry, polygon and multipolygon only |
The dispatch function rejects every other variant with MicError::InvalidInput
and names the variant in the message.
Python bindings
The python feature builds the extension module max_inscribed_circle. Build
it with maturin, or with cargo build --features python,extension-module.
| Function | Signature |
|---|---|
solve_mic_py |
(exterior, holes=None) |
solve_mic_multipolygon_py |
(polygons) |
The result class reports center_x, center_y, radius, radius_sq,
used_engine, candidate_count, and component_index. Rings arrive as
(x, y) tuples. A rejected input raises ValueError, and so does a solver
panic.
C API
src/ffi.rs is always compiled. It exposes mic_solve,
mic_solve_multipolygon, and mic_options_default. Flat [x0, y0, ...]
coordinate arrays go in, and #[repr(C)] structs come out.
Each function returns 0 on success and -1 on failure. A panic becomes -1, and a
null or short buffer is rejected instead of crashing. MicOptionsC carries no
tunable field yet. It exists so the ABI has a stable options pointer.
Features
| Feature | Description | Default |
|---|---|---|
bench-geos |
Comparison only: links the GEOS C library so benchmarks can cross-check the native solver. Never compiled into the solve path. | no |
geo-traits |
Generic solves over any geo_traits polygon, plus geo_types::Geometry dispatch |
no |
shewchuk |
Exact orient2d predicates from robust in the containment and orientation tests |
no |
simd |
4-lane batched segment distance through the stable wide crate |
no |
hotpath |
hotpath::measure instrumentation on the solver entry points |
no |
python |
PyO3 extension module named max_inscribed_circle |
no |
extension-module |
Build the Python module as an importable extension | no |
License
Apache-2.0