-
Notifications
You must be signed in to change notification settings - Fork 205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Robust Winding Order #502
Robust Winding Order #502
Conversation
Compute Winding Order using robust predicates. + derive `PartialOrd` for `Coordinate` + add `is_closed` utility to `LineString` + add `robust-0.2.2` crate + add `RobustWindingOrder` trait + impl `RobustWindingOrder` for `LineString<T: Float>` + add tests (copied from `WindingOrder`)
271207e
to
d9c195a
Compare
Here is another option to allow the user to switch between different implementations of the predicate. It is similar to the pub trait Kernel {
type Scalar: CoordinateType;
fn orient2d(
p: Coordinate<Self::Scalar>,
q: Coordinate<Self::Scalar>,
r: Coordinate<Self::Scalar>,
) -> Option<WindingOrder>;
} Then we implement different kernels: #[derive(Default)]
pub struct FloatKernel<T>(PhantomData<T>);
impl<T: Float> Kernel for FloatKernel<T> {
type Scalar = T;
fn orient2d(
p: Coordinate<Self::Scalar>,
q: Coordinate<Self::Scalar>,
r: Coordinate<Self::Scalar>,
) -> Option<WindingOrder> {
// implementation using robust::orient2d predicate
}
} Then, the pub trait RobustWinding<K>
where
K: Kernel
{
/// Return the winding order of this object
fn robust_winding_order(&self) -> Option<WindingOrder>;
} and implementation for LineString: impl<T: CoordinateType, K: Kernel<Scalar = T>> RobustWinding<K> for LineString<T> {
fn robust_winding_order(&self) -> Option<WindingOrder> {
// ....
}
} The usage becomes a bit verbose, but we allow the user to choose the Kernel they want to use: #[test]
fn robust_winding_order() {
use crate::Point;
// 3 points forming a triangle
let a = Point::new(0., 0.);
let b = Point::new(2., 0.);
let c = Point::new(1., 2.);
use super::kernels::FloatKernel;
// That triangle, but in clockwise ordering
let cw_line = LineString::from(vec![a.0, c.0, b.0, a.0]);
let orientation = RobustWinding::<FloatKernel<_>>::robust_winding_order(&cw_line);
} The added verbosity is unfortunate, but it makes the usage more flexible. If we could go ahead and modify the geometry structures, then we can reduce the verbosity:
Unfortunately, that's too many changes to the existing types. Please let me know what you think about this. Edit: slight improvement to usability. |
+ add `Kernel` trait (for now only requires `orient2d`) + provide `RobustKernel`, and `SimpleKernel` abilities + implement `RobustWinding<Kernel>` for `LineString`s + add tests
Allows assigning Kernel for each of the basic coordinate types.
It is meant for signed integer, and exact arithmetic types (arbitrary precision arithmetic types like eg. gmp float or (signed) integers). |
@urschrei Could you opine on the traits design for using different predicates for different types. Please see HasKernel assignments and the usage syntax for signed integers and for floats; they're actually using different predicate implementations underneath. Should we go this way? I like this, but there are caveats. A more flexible, but much more verbose approach is explored here. |
Added tests to verify robustness, and also to visualize the non-robustness of the naive implementation. The tests are ported from the demos in from https://github.com/danshapero/predicates . They take 3 points, modify the middle point at small increments over a grid of values, and output a png map. The output is quite intriguing; had to see it to believe the non-robustness. The naive orientation map; the output jumps between cw (black), colinear (gray), and ccw(white). The robust orientation produces a clean output! To run tests:
|
Tests to produce png output of output from `orient2d` over a grid of close-by floats. These are ignored by default, and should be explicitly run. Edits: fix some stable-rust compilation error in CI
7363e1e
to
c65b03a
Compare
That's really satisfying to see. |
Unlike quick hull, this doesn't need `T: Float`, and thus works for both integer and float scalars. + move `lexicographically_least_index` to utils + make `LineString::close` public + make separate enum `Orientation` for `Kernel::orient2d` + add quick hull tests to graham hull
Requesting review / discussion and merging of this PR. Currently, the new code is fairly non-intrusive (separate traits, that could later replace existing ones). Here are the key additions:
@michaelkirk @frewsxcv @urschrei Please provide feedback. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work!
In the interest of having the best possible defaults, and not imposing unnecessary choice on geo users...
- Currently, the new code is fairly non-intrusive (separate traits, that could later replace existing ones).
It seems like there are some correctness wins with the new implementations. Is there a benefit to keeping the old ones at all? If not, I'd be in favor of replacing them.
If there is some legit benefit to keeping the old ones, is there a reason we shouldn't default to the new, more accurate, implementations?
(similar concern for graham_hull vs. the existing convex hull impl)
And re: this comment from the PR description:
This PR side-steps the whole trait issue, and just creates a new trait, only meant to be implemented for T: Float. The rationale is to get the logic right, and hope for some expert advice on the trait magic.
Is this comment outdated? Is there still some question around traits outstanding, and if so, could you restate the question?
Makes sense; will replace the existing Winding trait as this is a more correct impl.
Currently
Yeah :). Will edit to top comment and update the current status. |
+ make `kernels` module `pub(crate)` + remove `kernels` test in favor of adding it in the `robust` crate + replace existing `Winding` trait with robust variant + fix `Orient` trait to use new `Winding` trait
This is really great work, @rmanoka. Apart from my two minor comments, I don't think there's anything else that needs to be added here. There are typos here and there, but let's defer those to a separate PR. I have a strong preference to retain the QuickHull implementation, but it's based purely on sentimentality and I wouldn't want to do so at the expense of the Graham implementation and they're both |
+ add more docs + add more types to Kernel (i16, i128, isize)
5b8c06e
to
a921450
Compare
@urschrei The graham scan algorithm has two added benefits: it uses robust predicates, and also works on integer coords. The quick hull impl. could be made robust (by fixing |
Noo the point of all this is to integrate robust predicates to any existing traits that can use them, so the convex hull algorithm should be using Graham by default! We can fix up / revisit Quickhull later on if need be. |
Sounds good! Let's do that in a separate issue / PR; see a few simple improvements, and bug-fixes to the trait. |
+ revert back to not deriving `PartialOrd` for `Coordinate` + remove unused `ConvexHull` trait in graham_hull impl
a921450
to
51e8a11
Compare
Should we merge this? @michaelkirk @urschrei @frewsxcv |
I believe so - thanks for your work @rmanoka ! bors r+ |
Build succeeded: |
This PR introduces a few traits to abstract the predicates logic for different scalar types.
Kernel
, andHasKernel
traits to allow customizing the predicates used for different scalars (integer vs float)RobustKernel
meant for float types (f32
,f64
)SimpleKernel
meant for exact arithmetic types (i32
,i64
; can be extended to support arbitrary precision types)Also port a few key, but simple algos to use the predicates. This improves the correctness of these algos.
Winding
: shift from area logic into using theorient2d
logic.graham_hull
(Graham Scan implementation) using the predicates. This has the advantage of being more correct, and also works on geoms,with int. coords.