Skip to content
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

Merged
merged 8 commits into from
Aug 28, 2020
Merged

Conversation

rmanoka
Copy link
Contributor

@rmanoka rmanoka commented Aug 21, 2020

This PR introduces a few traits to abstract the predicates logic for different scalar types.

  • Introduces Kernel, and HasKernel traits to allow customizing the predicates used for different scalars (integer vs float)
  • Defines RobustKernel meant for float types (f32, f64)
  • Defines 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 the orient2d logic.
  • Convex Hull: provide an alternative graham_hull (Graham Scan implementation) using the predicates. This has the advantage of being more correct, and also works on geoms,with int. coords.

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`)
@rmanoka rmanoka force-pushed the feat/robust-winding-order branch from 271207e to d9c195a Compare August 22, 2020 05:48
@rmanoka
Copy link
Contributor Author

rmanoka commented Aug 22, 2020

Here is another option to allow the user to switch between different implementations of the predicate. It is similar to the spade crate, and also CGAL. We abstract the predicates into a trait Kernel as follows:

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: struct FloatKernel, struct SimpleKernel, struct UnsignedIntegerKernel etc. For instance, a FloatKernel could be like this:

#[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 Winding trait becomes:

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:

pub struct Point<K: Kernel<Scalar = T>, T>(pub Coordinate<T>, pub PhantomData<K>)

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.
@rmanoka
Copy link
Contributor Author

rmanoka commented Aug 22, 2020

I'm outta my league here, but based your earlier comments, I understand that this Kernel would only be appropriate for integer types. Is that right?

It is meant for signed integer, and exact arithmetic types (arbitrary precision arithmetic types like eg. gmp float or (signed) integers).

@rmanoka
Copy link
Contributor Author

rmanoka commented Aug 22, 2020

@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.

@rmanoka
Copy link
Contributor Author

rmanoka commented Aug 23, 2020

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).

naive-orientation-map

The robust orientation produces a clean output!

robust-orientation-map

To run tests:

cargo test test_naive -- --ignored
cargo test test_robust -- --ignored

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
@rmanoka rmanoka force-pushed the feat/robust-winding-order branch from 7363e1e to c65b03a Compare August 23, 2020 08:03
@michaelkirk
Copy link
Member

The naive orientation map; the output jumps between cw (black), colinear (gray), and ccw(white).
The robust orientation produces a clean output!

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
@rmanoka rmanoka marked this pull request as ready for review August 23, 2020 16:42
@rmanoka
Copy link
Contributor Author

rmanoka commented Aug 23, 2020

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:

  1. Kernel trait to abstract the predicate logic. We currently have one naive implementation, used for i32, i64; and the robust one used for f32, f64. Includes fairly comprehensive test of robustness.
  2. RobustWinding trait: same interface as Winding trait, but uses the new goodies, and so robust. If all is good, we can replace the existing Winding trait with this.
  3. graham_hull algorithm for computing convex_hull (it can be used for integer coords too). Currently not yet a trait, but we may consider using this algo for ConvexHull trait so that it works for all scalar types.

@michaelkirk @frewsxcv @urschrei Please provide feedback.

Copy link
Member

@michaelkirk michaelkirk left a 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...

  1. 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?

@rmanoka
Copy link
Contributor Author

rmanoka commented Aug 23, 2020

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.

Makes sense; will replace the existing Winding trait as this is a more correct impl.

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)

Currently graham_hull is provided as a free-standing algo, and not as a trait. I didn't want to introduce too many changes in a single PR. Also, I'm not completely sure if they have the same performance in practice, so we should do some benchmarks before switching. It is in this PR to basically show-case the Kernel ideas and to provide the functionality for integer geometries (which wasn't previously possible).

Is this comment outdated? Is there still some question around traits outstanding, and if so, could you restate the question?

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
@urschrei
Copy link
Member

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 O(n log n) on average, so ¯_(ツ)_/¯

+ add more docs
+ add more types to Kernel (i16, i128, isize)
@rmanoka rmanoka force-pushed the feat/robust-winding-order branch from 5b8c06e to a921450 Compare August 25, 2020 16:28
@rmanoka
Copy link
Contributor Author

rmanoka commented Aug 25, 2020

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 O(n log n) on average, so ¯_(ツ)_/¯

@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 point_location), but do not see how to get it working on integer coords. That said, I'm okay with leaving graham scan as a stand-alone algo. that users can import and use if needed.

@urschrei
Copy link
Member

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.

@rmanoka
Copy link
Contributor Author

rmanoka commented Aug 25, 2020

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
@rmanoka
Copy link
Contributor Author

rmanoka commented Aug 28, 2020

Should we merge this? @michaelkirk @urschrei @frewsxcv

@michaelkirk
Copy link
Member

I believe so - thanks for your work @rmanoka !

bors r+

@bors
Copy link
Contributor

bors bot commented Aug 28, 2020

Build succeeded:

@bors bors bot merged commit 3f5fb78 into georust:master Aug 28, 2020
@rmanoka rmanoka deleted the feat/robust-winding-order branch September 17, 2020 03:13
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.

4 participants