Skip to content

Improve voronoi computation#515

Open
schroedtert wants to merge 2 commits intoPedestrianDynamics:mainfrom
schroedtert:improve-voronoi-computation
Open

Improve voronoi computation#515
schroedtert wants to merge 2 commits intoPedestrianDynamics:mainfrom
schroedtert:improve-voronoi-computation

Conversation

@schroedtert
Copy link
Collaborator

Replace manual scipy-based Voronoi polygon reconstruction with
shapely.voronoi_polygons(ordered=True). Replacing manually written code
from stackoverflow with library call.

  • Vectorize intersection and cutoff using shapely array ops
    instead of per-polygon Python loops
  • Extract _resolve_multipolygons helper for GeometryCollection
    results from non-convex walkable area intersections
  • Bump Shapely minimum version to 2.1 (ordered parameter)

Also add unit tests for the function.

Benchmark: compute_individual_voronoi_polygons

Data: bi_corr_400_b_08.txt (777,733 rows, 5,756 frames, 703 pedestrians, 25 fps)

Scenario v1.4.0 (scipy) v1.5.0 (shapely) Improvement
without cutoff 23.16s ± 0.81s 10.70s ± 0.31s 53.8%
with cutoff (r=0.8, q=3) 33.56s ± 0.75s 18.60s ± 0.25s 44.6%

Add tests for Voronoi polygon computation

Test scenarios:
- output schema and column types
- density correctness
- polygon containment within walkable area
- pedestrian position inside own polygon
- symmetric placement producing equal areas
- full area coverage (no gaps)
- cutoff behavior (area reduction, circle bounds,
  large cutoff, quad segments)
- blind points on/off with warning emission
- multiple frames, non-sequential IDs and frames
- single pedestrian coverage
- non-convex and obstacle walkable areas
- collinear and boundary pedestrians
- determinism
- polygon validity
- close pedestrians
- non-overlapping polygons
- multipolygon resolution for complex geometrie
Replace manual scipy-based Voronoi polygon reconstruction with
shapely.voronoi_polygons(ordered=True). Replacing manually written code
from stackoverflow with library call.

- Vectorize intersection and cutoff using shapely array ops
  instead of per-polygon Python loops
- Extract _resolve_multipolygons helper for GeometryCollection
  results from non-convex walkable area intersections
- Bump Shapely minimum version to 2.1 (ordered parameter)
@codecov
Copy link

codecov bot commented Feb 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.08%. Comparing base (29b3e16) to head (5b82c31).

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@schroedtert schroedtert marked this pull request as ready for review February 28, 2026 14:47
@schroedtert schroedtert requested a review from Copilot February 28, 2026 15:41
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates PedPy’s individual Voronoi computation to use Shapely’s built-in Voronoi polygon generation (with ordering), replacing the prior SciPy-based reconstruction and aiming to improve performance and robustness. It also adds extensive unit tests and bumps the minimum Shapely version to support the new API.

Changes:

  • Replace SciPy Voronoi reconstruction with shapely.voronoi_polygons(..., ordered=True) and vectorized Shapely array operations.
  • Add _resolve_multipolygons helper to handle non-Polygon intersection outputs.
  • Bump Shapely minimum version to >=2.1 and add unit tests for compute_individual_voronoi_polygons.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pedpy/methods/method_utils.py Switch Voronoi generation to Shapely, vectorize clipping/cutoff, add _resolve_multipolygons.
tests/unit_tests/methods/test_compute_individual_voronoi_polygons.py Add comprehensive unit tests for Voronoi polygon computation and helper behavior.
requirements.txt Bump Shapely minimum version to >=2.1,<3.0.
pyproject.toml Bump Shapely minimum version to >=2.1,<3.0.
.pre-commit-config.yaml Update pre-commit hook environment to use Shapely ~=2.1.
Comments suppressed due to low confidence (2)

pedpy/methods/method_utils.py:549

  • The use_blind_points docstring no longer matches the implementation: the function now relies on shapely.voronoi_polygons(..., extend_to=...) and does not add 4 external “blind points”, but the docstring still claims those points are added and have no effect on polygon sizes. Please update the docstring to describe the current behavior (including what use_blind_points=False does for frames with <4 pedestrians).
    For allowing the computation of the Voronoi polygons when less than 4
    pedestrians are in the walkable area, 4 extra points will be added outside
    the walkable area with a significant distance. These will have no effect
    on the size of the computed Voronoi polygons. This behavior can be turned
    off by setting :code:`use_blind_points = False`. When turned off no Voronoi
    polygons will be computed for frames with less than 4 persons, also
    pedestrians walking in a line can lead to issues in the computation of the
    Voronoi tesselation.

    Args:
        traj_data (TrajectoryData): trajectory data
        walkable_area (WalkableArea): bounding area, where pedestrian are
                supposed to walk
        cut_off (Cutoff): cutoff information, which provide the largest
                possible extend of a single Voronoi polygon
        use_blind_points (bool): adds extra 4 points outside the walkable area
                to also compute voronoi cells when less than 4 peds are in the
                walkable area (default: on!)

    Returns:
        DataFrame containing the columns 'id', 'frame','polygon' (
        :class:`shapely.Polygon`), and 'density' in :math:`1/m^2`.
    """
    all_ids = []
    all_frames = []
    all_polygons = []

    wa_polygon = walkable_area.polygon

    for frame, peds_in_frame in traj_data.data.groupby(traj_data.data.frame):
        n_peds = len(peds_in_frame)

        if not use_blind_points and n_peds < 4:
            _log.warning(
                f"Not enough pedestrians (N="
                f"{n_peds}) available to "
                f"calculate Voronoi cells for frame = {frame}. "
                f"Consider enable use of blind points."
            )
            continue

pedpy/methods/method_utils.py:548

  • Log warning text has a grammatical issue: "Consider enable use of blind points." reads awkwardly. Consider changing it to something like "Consider enabling use_blind_points." to make the guidance clearer.
            _log.warning(
                f"Not enough pedestrians (N="
                f"{n_peds}) available to "
                f"calculate Voronoi cells for frame = {frame}. "
                f"Consider enable use of blind points."
            )

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +625 to +631
within_mask = shapely.within(points[idx], parts)
if within_mask.any():
resolved[idx] = parts[within_mask][0]
else:
# Fallback: use the part nearest to the point
distances = shapely.distance(points[idx], parts)
resolved[idx] = parts[np.argmin(distances)]
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_resolve_multipolygons can return a non-Polygon geometry: for a GeometryCollection that contains non-polygon parts (e.g., lines/points), the fallback path picks the nearest part without filtering by polygon type. This can yield a LineString/Point with zero area and later produce invalid densities (division by zero) or violate the function’s promise to return polygons. Filter parts down to polygonal parts before within/distance selection, and handle the case where no polygon parts exist (e.g., raise or return an empty polygon and drop that row).

Suggested change
within_mask = shapely.within(points[idx], parts)
if within_mask.any():
resolved[idx] = parts[within_mask][0]
else:
# Fallback: use the part nearest to the point
distances = shapely.distance(points[idx], parts)
resolved[idx] = parts[np.argmin(distances)]
# Filter to polygonal parts only (type_id == 3 corresponds to Polygon)
parts_type_ids = shapely.get_type_id(parts)
polygon_parts = parts[parts_type_ids == 3]
if len(polygon_parts) == 0:
# No polygonal parts available: fall back to an empty Polygon to
# preserve the return type contract and avoid zero-area non-polygons.
_log.warning(
"Geometry at index %s has no polygonal parts; using empty Polygon.",
idx,
)
resolved[idx] = shapely.Polygon()
continue
within_mask = shapely.within(points[idx], polygon_parts)
if within_mask.any():
resolved[idx] = polygon_parts[within_mask][0]
else:
# Fallback: use the polygon part nearest to the point
distances = shapely.distance(points[idx], polygon_parts)
resolved[idx] = polygon_parts[np.argmin(distances)]

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants