Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion scripts/benchmark/src/benchmark/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
36 changes: 36 additions & 0 deletions scripts/benchmark/src/benchmark/charshape.py
Original file line number Diff line number Diff line change
@@ -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))
80 changes: 24 additions & 56 deletions src/algorithms/simplify_charshape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -57,38 +56,6 @@ impl<T: SpadeNum> PartialEq for CharScore<'_, T> {
}
}

#[derive(Debug)]
struct BoundaryNode<'a, T>(VertexHandle<'a, Point2<T>, (), CdtEdge<()>>);

impl<T> PartialEq for BoundaryNode<'_, T> {
fn eq(&self, other: &BoundaryNode<T>) -> bool {
self.0.index() == other.0.index()
}
}

impl<T> Eq for BoundaryNode<'_, T> {}

impl<T> Hash for BoundaryNode<'_, T>
where
T: SpadeNum,
{
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.index().hash(state);
}
}

impl<T> Ord for BoundaryNode<'_, T> {
fn cmp(&self, other: &BoundaryNode<T>) -> Ordering {
self.0.index().partial_cmp(&other.0.index()).unwrap()
}
}

impl<T> PartialOrd for BoundaryNode<'_, T> {
fn partial_cmp(&self, other: &BoundaryNode<T>) -> Option<Ordering> {
Some(self.cmp(other))
}
}

fn characteristic_shape<T>(orig: &Polygon<T>, eps: T, max_len: usize) -> Polygon<T>
where
T: GeoFloat + SpadeNum,
Expand All @@ -100,46 +67,47 @@ where
let eps_2 = eps * eps;

let tri = orig.triangulate();
let boundary_edges = tri.convex_hull().map(|edge| edge.rev()).collect::<Vec<_>>();
let mut boundary_nodes: HashSet<_> =
HashSet::from_iter(boundary_edges.iter().map(|&edge| BoundaryNode(edge.from())));

let mut pq = boundary_edges
.iter()
.map(|&line| CharScore {
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()
.map(|edge| edge.rev())
.map(|line| CharScore {
score: line.length_2(),
edge: line,
})
.collect::<BinaryHeap<_>>();

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;
}

// 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_mask[coprime_node.index()] = true;
len += 1;

recompute_boundary(largest.edge, &mut pq);
}

// Extract boundary nodes
let mut boundary_nodes = boundary_nodes.drain().collect::<Vec<_>>();
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 = tri
.vertices()
.zip(boundary_mask)
.filter_map(|(v, keep)| keep.then_some(v.position().into_coord()))
.collect();
Polygon::new(exterior, vec![])
}

Expand Down
13 changes: 0 additions & 13 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -127,17 +125,6 @@ fn is_valid(poly: Vec<[f64; 2]>) -> PyResult<bool> {
Ok(poly.is_valid() && poly.exterior().is_cw())
}

#[pyfunction]
fn triangulate(poly: Vec<[f64; 2]>) -> PyResult<Vec<[[f64; 2]; 2]>> {
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)?)?;
Expand Down