From c75cdab3e7460f5b3d437da1193821dd72cd03f5 Mon Sep 17 00:00:00 2001 From: Niall Oswald Date: Mon, 27 Oct 2025 17:11:29 +0000 Subject: [PATCH 1/5] feat: charshape performance tweaks --- Cargo.toml | 1 - src/algorithms/simplify_charshape.rs | 73 +++++++--------------------- 2 files changed, 18 insertions(+), 56 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6209ab7..0448ee1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,6 @@ crate-type = ["cdylib", "rlib"] [dependencies] geo = "0.31.0" -hashbrown = "0.15.5" pyo3 = "0.25.0" rayon = "1.10.0" rstar = "0.12.2" diff --git a/src/algorithms/simplify_charshape.rs b/src/algorithms/simplify_charshape.rs index 7a21dee..b98b62f 100644 --- a/src/algorithms/simplify_charshape.rs +++ b/src/algorithms/simplify_charshape.rs @@ -18,14 +18,13 @@ // Copyright 2025- Niall Oswald and Kenneth Martin and Jo Wayne Tan +use crate::extensions::conversions::IntoCoord; use crate::extensions::triangulate::Triangulate; -use geo::{Coord, GeoFloat, LineString, Polygon}; -use hashbrown::HashSet; -use spade::handles::{DirectedEdgeHandle, VertexHandle}; +use geo::{GeoFloat, Polygon}; +use spade::handles::DirectedEdgeHandle; use spade::{CdtEdge, Point2, SpadeNum, Triangulation}; use std::cmp::Ordering; use std::collections::BinaryHeap; -use std::hash::Hash; #[derive(Debug)] struct CharScore<'a, T> @@ -57,38 +56,6 @@ impl PartialEq for CharScore<'_, T> { } } -#[derive(Debug)] -struct BoundaryNode<'a, T>(VertexHandle<'a, Point2, (), CdtEdge<()>>); - -impl PartialEq for BoundaryNode<'_, T> { - fn eq(&self, other: &BoundaryNode) -> bool { - self.0.index() == other.0.index() - } -} - -impl Eq for BoundaryNode<'_, T> {} - -impl Hash for BoundaryNode<'_, T> -where - T: SpadeNum, -{ - fn hash(&self, state: &mut H) { - self.0.index().hash(state); - } -} - -impl Ord for BoundaryNode<'_, T> { - fn cmp(&self, other: &BoundaryNode) -> Ordering { - self.0.index().partial_cmp(&other.0.index()).unwrap() - } -} - -impl PartialOrd for BoundaryNode<'_, T> { - fn partial_cmp(&self, other: &BoundaryNode) -> Option { - Some(self.cmp(other)) - } -} - fn characteristic_shape(orig: &Polygon, eps: T, max_len: usize) -> Polygon where T: GeoFloat + SpadeNum, @@ -100,13 +67,15 @@ where let eps_2 = eps * eps; let tri = orig.triangulate(); - let boundary_edges = tri.convex_hull().map(|edge| edge.rev()).collect::>(); - let mut boundary_nodes: HashSet<_> = - HashSet::from_iter(boundary_edges.iter().map(|&edge| BoundaryNode(edge.from()))); + let mut boundary_nodes = tri + .convex_hull() + .map(|edge| edge.from()) + .collect::>(); - let mut pq = boundary_edges - .iter() - .map(|&line| CharScore { + let mut pq = tri + .convex_hull() + .map(|edge| edge.rev()) + .map(|line| CharScore { score: line.length_2(), edge: line, }) @@ -118,28 +87,22 @@ where } // Regularity check - let coprime_node = BoundaryNode(largest.edge.opposite_vertex().unwrap()); - if boundary_nodes.contains(&coprime_node) { - continue; - } - if largest.edge.is_constraint_edge() { continue; } // Update boundary nodes and edges - boundary_nodes.insert(coprime_node); + let coprime_node = largest.edge.opposite_vertex().unwrap(); + boundary_nodes.push(coprime_node); recompute_boundary(largest.edge, &mut pq); } // Extract boundary nodes - let mut boundary_nodes = boundary_nodes.drain().collect::>(); - boundary_nodes.sort(); - - let exterior = LineString::from_iter(boundary_nodes.into_iter().map(|n| { - let p = n.0.position(); - Coord { x: p.x, y: p.y } - })); + let exterior = boundary_nodes + .into_sorted_vec() + .into_iter() + .map(|v| v.position().into_coord()) + .collect(); Polygon::new(exterior, vec![]) } From d07dc7a9ccf327c4d51429397637db2f1abad2b3 Mon Sep 17 00:00:00 2001 From: Niall Oswald Date: Mon, 27 Oct 2025 18:15:20 +0000 Subject: [PATCH 2/5] chore: fix benchmarks --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 8e460ae..1220e21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,8 @@ cache-keys = [{ file = "pyproject.toml" }, { file = "rust/Cargo.toml" }, { file # Uncomment to build rust code in development mode # config-settings = { build-args = '--profile=dev' } +[tool.uv.workspace] +members = ["scripts/benchmark"] [project.scripts] polyshell = "polyshell._cli:app" From e6ee932c8c60cfd74166e6ea29becc07a00eecd9 Mon Sep 17 00:00:00 2001 From: Niall Oswald Date: Mon, 27 Oct 2025 18:15:32 +0000 Subject: [PATCH 3/5] chore: add charshape benchmark --- scripts/benchmark/src/benchmark/__init__.py | 3 +- scripts/benchmark/src/benchmark/charshape.py | 36 ++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 scripts/benchmark/src/benchmark/charshape.py diff --git a/scripts/benchmark/src/benchmark/__init__.py b/scripts/benchmark/src/benchmark/__init__.py index 5cfb501..7955904 100644 --- a/scripts/benchmark/src/benchmark/__init__.py +++ b/scripts/benchmark/src/benchmark/__init__.py @@ -21,7 +21,8 @@ # +from .charshape import BENCHMARKS as CHAR_BENCH from .rdp import BENCHMARKS as RDP_BENCH from .vw import BENCHMARKS as VW_BENCH -BENCHMARKS = [*VW_BENCH, *RDP_BENCH] +BENCHMARKS = [*VW_BENCH, *RDP_BENCH, *CHAR_BENCH] diff --git a/scripts/benchmark/src/benchmark/charshape.py b/scripts/benchmark/src/benchmark/charshape.py new file mode 100644 index 0000000..60969f5 --- /dev/null +++ b/scripts/benchmark/src/benchmark/charshape.py @@ -0,0 +1,36 @@ +# +# Copyright 2025- European Centre for Medium-Range Weather Forecasts (ECMWF) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# In applying this licence, ECMWF does not waive the privileges and immunities +# granted to it by virtue of its status as an intergovernmental organisation nor +# does it submit to any jurisdiction. +# +# Copyright 2025- Niall Oswald and Kenneth Martin and Jo Wayne Tan +# + + +"""Benchmarks for Charshape.""" + +from polyshell import reduce_polygon + + +def polyshell_charshape(poly, eps): + return reduce_polygon(poly, "epsilon", eps, method="charshape") + + +RUNNERS = [polyshell_charshape] +LABELS = ["polyshell (charshape)"] + +BENCHMARKS = list(zip(RUNNERS, LABELS)) From f7484a8b17a826e72d2958d212ba10b93859296a Mon Sep 17 00:00:00 2001 From: Niall Oswald Date: Wed, 29 Oct 2025 13:09:24 +0000 Subject: [PATCH 4/5] feat: replace binary heap with a mask --- src/algorithms/simplify_charshape.rs | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/algorithms/simplify_charshape.rs b/src/algorithms/simplify_charshape.rs index b98b62f..edcbb95 100644 --- a/src/algorithms/simplify_charshape.rs +++ b/src/algorithms/simplify_charshape.rs @@ -67,10 +67,13 @@ where let eps_2 = eps * eps; let tri = orig.triangulate(); - let mut boundary_nodes = tri - .convex_hull() - .map(|edge| edge.from()) - .collect::>(); + + let mut boundary_mask = vec![false; tri.num_vertices()]; + let mut len = 0; + tri.convex_hull().for_each(|edge| { + boundary_mask[edge.from().index()] = true; + len += 1; + }); let mut pq = tri .convex_hull() @@ -82,7 +85,7 @@ where .collect::>(); while let Some(largest) = pq.pop() { - if largest.score < eps_2 || boundary_nodes.len() >= max_len { + if largest.score < eps_2 || len >= max_len { break; } @@ -93,15 +96,17 @@ where // Update boundary nodes and edges let coprime_node = largest.edge.opposite_vertex().unwrap(); - boundary_nodes.push(coprime_node); + boundary_mask[coprime_node.index()] = true; + len += 1; + recompute_boundary(largest.edge, &mut pq); } // Extract boundary nodes - let exterior = boundary_nodes - .into_sorted_vec() - .into_iter() - .map(|v| v.position().into_coord()) + let exterior = tri + .vertices() + .zip(boundary_mask) + .filter_map(|(v, keep)| keep.then_some(v.position().into_coord())) .collect(); Polygon::new(exterior, vec![]) } From a806f76cd9e4e02d698a1259f23a4f00f6225e38 Mon Sep 17 00:00:00 2001 From: Niall Oswald Date: Wed, 29 Oct 2025 13:14:05 +0000 Subject: [PATCH 5/5] chore: remove dead code --- src/lib.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a50725b..a2425da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,6 @@ // Copyright 2025- Niall Oswald and Kenneth Martin and Jo Wayne Tan -use crate::extensions::triangulate::Triangulate; use crate::extensions::validation::InvalidPolygon; use algorithms::simplify_charshape::SimplifyCharshape; use algorithms::simplify_rdp::SimplifyRDP; @@ -27,7 +26,6 @@ use extensions::validation::Validate; use geo::{Polygon, Winding}; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; -use spade::Triangulation; mod algorithms; mod extensions; @@ -127,17 +125,6 @@ fn is_valid(poly: Vec<[f64; 2]>) -> PyResult { Ok(poly.is_valid() && poly.exterior().is_cw()) } -#[pyfunction] -fn triangulate(poly: Vec<[f64; 2]>) -> PyResult> { - let poly = Polygon::new(poly.into(), vec![]); - let cdt = poly.triangulate(); - let edges = cdt - .undirected_edges() - .map(|edge| edge.positions().map(|p| p.into())) - .collect(); - Ok(edges) -} - #[pymodule] fn _polyshell(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(reduce_polygon_vw, m)?)?;