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

Improve event ordering logic in segment division #11

Conversation

bluenote10
Copy link
Contributor

@bluenote10 bluenote10 commented Feb 22, 2020

I spend a while thinking about how to solve #3 / w8r/martinez#99 and came to the conclusion that this is the easiest solution. The idea has two steps:

  • When computing the intersection between two line segments, we constrain the resulting intersection point into the bounding box defined by the segment interval overlap. For instance, if line segment A is defined on x ∈ [0, 2] and line segment B on x ∈ [1, 3], the intersection point must fall in x ∈ [1, 2]. Constraining the result makes sure that numerical instabilities in the intersection point computation can never lead to values that are completely impossible, and it limits the corner cases for the "subdivide segments" logic. Note: issue 99 is a good example of intersection points falling outside the valid range.
  • In subdividing segments, we have to take care of the two possible cases in which the order of the subdivided events would "flip".

The most extreme example is a line segment that differs in just one ULP (unit of least precision) in x, going from top to bottom:

2020-02-22 11 34 49-2

The intersection point can either take the left x value (case 1) or the the right x value (case 2).

In terms of the event order, the normal case after subdivision is se_l < se_intersection < se_r. The problem is that perfectly vertical line segments must be consistently processed in bottom-to-top order, but the original line segment has top-to-bottom order. This means that the order of the newly produced vertical segment must be reversed.

In the second case reversing the order is not a problem, because both events are future events that will be processed later.

Case 1 is a bigger problem though, because se_l is the event currently being processed, and a theoretically correct processing order would have required to process se_intersection earlier. To avoid overly complex backtracking logic, I think it should be fine to simply increment the x-value of the intersection point by 1 ULP (i.e., always avoiding the order reversal in the first subsegment). I've measured the accuracy of the intersection point computation, and due to cancellation effects the error is anyway much larger than 1 ULP.

Minor changes:

  • Computing the segment interval bounding box can also be used as a small performance optimization, because the non-overlapping case can be detected early, allowing to skip calling the actual intersection point computation. I've measured a 3-4 % improvement in runtime.
  • I've added a small criterion.rs benchmark suite to make performance tracking easier.
  • Working with ULPs requires a nextafter function, which Rust doesn't offer by default unfortunately. At first I was using the float_extra crate, but since it doesn't offer the f32 variant of nextafter, I decided to follow the SO recommendation of manually wrapping the C function.
  • Added a few new test cases, in particular some that address the "x value differs by 1 ULP" problem.
  • Applied cargo fmt & cargo clippy (clippy suggested to remove a few existing clones, I hope they weren't there an purpose).

Visualization of new/modified test cases: test_cases.pdf

@untoldwind untoldwind merged commit 213a72b into 21re:master Feb 24, 2020
@untoldwind
Copy link
Contributor

Looks good ... and good to know that num_traits conversion seems to work now.

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