Skip to content

Valid polygon with holes panics at Option::unwrap() in face triangulation #194

Description

@ArneDefauw

Summary

Bermuda panics while triangulating a valid Napari polygon-with-holes path.

The panic occurs in face_triangulation.rs at an unchecked Option::unwrap(). The issue reproduces by calling Bermuda directly, without Napari being involved.

Reproducer

The attached file, create_holes_triangulation_failure.txt, contains the exact (570, 2) float32 array passed to Bermuda.

from pathlib import Path

import bermuda
import numpy as np

vertices = np.loadtxt(
    Path("create_holes_triangulation_failure.txt"),
    dtype=np.float32,
)

assert vertices.shape == (570, 2)
assert np.isfinite(vertices).all()

bermuda.triangulate_polygons_face([vertices])

The same panic also occurs through:

bermuda.triangulate_polygons_with_edge([vertices])

Observed result

thread '<unnamed>' panicked at crates/triangulation/src/face_triangulation.rs:531:60:
called `Option::unwrap()` on a `None` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Traceback (most recent call last):
  File "<stdin>", line 10, in <module>
pyo3_runtime.PanicException: called `Option::unwrap()` on a `None` value

Expected result

The valid polygon should triangulate successfully.

At minimum, if Bermuda considers the input unsupported, the Python API should return a regular, structured exception rather than allowing the Rust implementation to panic.

Input topology

The array uses Napari's single-path encoding for a polygon with two holes:

  • indices 0:546: exterior ring, including closure;
  • indices 546:557: first hole, including closure;
  • index 557: repeated exterior anchor separating the holes;
  • indices 558:569: second hole, including closure;
  • index 569: repeated exterior anchor/final closure.

Coordinates are in Napari (y, x) order.

The fixture has:

  • no non-finite values;
  • no consecutive duplicate coordinates;
  • closed exterior and interior rings;
  • a valid, non-empty decoded Shapely polygon.

The decoded geometry can be validated with:

from shapely.geometry import Polygon

exterior = vertices[:546, ::-1]
hole_1 = vertices[546:557, ::-1]
hole_2 = vertices[558:569, ::-1]

polygon = Polygon(exterior, [hole_1, hole_2])

assert polygon.is_valid
assert not polygon.is_empty

Additional diagnostics:

Exterior area:        140075382.17486215
First-hole area:         277656.0446577072
Second-hole area:        377113.79895591736
Final polygon area:   139420612.33124852
Minimum clearance:           17.5421233974749
Distance between holes:     630.1430630856535

Both holes are strictly contained by the exterior.

Failure isolation

The problem appears to be inside face triangulation:

  • the exterior alone succeeds;
  • each hole alone succeeds;
  • the exterior with the second hole succeeds;
  • the exterior with the first hole panics;
  • the exterior with both holes panics, independently of the order of the holes.

The result is also sensitive to geometrically equivalent representations:

  • cyclically changing the first vertex of the first hole can make the panic disappear;
  • translating the coordinates can change the result;
  • scaling the coordinates can change the result.

This may indicate sensitivity to floating-point comparisons or sweep-line event ordering, but I have not investigated the Rust implementation sufficiently to identify the appropriate fix.

Environment

Bermuda: 0.1.7
Napari:  0.7.1
NumPy:   2.3.5
Shapely: 2.1.2
Python:  3.13.11
OS:      macOS 26.4.1
Arch:    arm64

Fixture integrity

File SHA-256:
7f09c0043050e518052e52736fd68d7cd400611697675b1e3dc6c4977f4639d2

Loaded float32 array byte SHA-256:
21df8806d99580c208b26e075297fdbefeac4ce8f134ac370328203af55975ec

create_holes_triangulation_failure.txt

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions