Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Improved Code for Segments Intersect #11553

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

Merged
merged 16 commits into from
Mar 2, 2019

Conversation

TarasKuzyo
Copy link
Contributor

PR Summary

Supposedly fixes #11527 (Inconsistent path intersection).

Path.intersects_path didn't work properly on
collinear paths intersection because underlying
segments_intersect
handled parallel segments as non-intersecting (ignoring possible segments' overlap).

This PR handles this case of collinear paths and makes Path.intersects_path return True
if paths have at least one common point.

Added an appropriate test case to https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/tests/test_path.py

PR Checklist

  • Has Pytest style unit tests
  • Code is PEP 8 compliant
  • New features are documented, with examples if plot related
  • Documentation is sphinx and numpydoc compliant
  • Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)
  • Documented in doc/api/api_changes.rst if API changed in a backward-incompatible way

@jklymak
Copy link
Member

jklymak commented Jul 2, 2018

Thanks so much for the PR!

I suspect this PR is fine, and the failing tests are for some other reason. However, you have many PEP8 errors (see the 3.6 CI test)

src/_path.h Outdated
if (den == 0.0) {
if (den == 0.0) { // collinear segments
// check if any of segments' edges are contained
// in the other segment
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm missing a subtlety, but wouldn't it be more elegant just to check if the y-intercepts of the two lines are the same? I appreciate that does the same thing as this, but seems like two lines of code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I fear, I do not fully understand your point. Could you give a quick example, please?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mA = (y2 - y1) / (x2 - x1)
mB = (y4 - y3) / (x4 - y4)
if mA == mB
  intA = y2 - mA * x2
  intB = y4 - mB * x4
  if intA == intB
    return true
  return false

But maybe I'm misunderstanding. Do the two lines need to intersect within the line segments? If so, maybe the longer code is necessary, or at least another set of checks if intA==intB

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intersect point do needs to be within both segments (see https://math.stackexchange.com/a/175944 for some math details).
In your example you calculate slopes of the segments similarly to den
but this code will fail for vertical lines (zero division). I fear the equality of
intA and intB is not a sufficient condition of segments' intersect: it looks
this can be True if only (x2, y2) = (x4, y4). Probably, we don't need to check
all 4 points (3 might be enough) for being inside the segment but I need to verify it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Yes, if the slope is infinite, you need to special case
  2. If the slope is finite, and the y intercepts the same, then all you need to do is check if x1 < x3 < x2 or x1 < x4 < x2. Much easier.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the slope is finite we have to:

  1. check if intercepts are the same
  2. check if one of segments is a part of the other.
    The second part is not quite easy as it might seem, because:
    -- We cannot be sure that x1 < x2 (the same applies for the other pair and y's)
    -- There are many edge cases (x1 = x4, x1 = x2, x3 = x4, x3 = x2, same for y's)

In order to handle all those I've came up with segment_contains which simply checks
if points belongs to a segment (including infinite slopes). The only overhead in segment_contains is the check for intercept, which can be done in the body of
segments_intersect but in that case segment_contains will have a limited use.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree that its not easy to test "if one segment is part of the other". You only need to check x values because the y values are one-to-one dependent on the x values at this point; and checking the overlap of two intervals is pretty easy (i.e. https://stackoverflow.com/questions/3269434/whats-the-most-efficient-way-to-test-two-integer-ranges-for-overlap)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still, it needs two separate cases for infinite and non-infinite slopes. Anyway, I will consider writing some simpler code for the equal slopes case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, do the interval check on y instead of x and then most of the code works in both cases... Of course you still need to check for both cases, but the infinite slope case is easy to get if they are on the same line by checking two values of x,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I modified the code, so it makes less checks (not sure if it is more readable though). We still need more checks than you expected because there are many relative positions and orientations of both segments to take into account.

@jklymak
Copy link
Member

jklymak commented Jul 2, 2018

You’ve checked if the slopes of the two lines are the same. They don’t intersect unless their y intercepts are the same which is one eq per line.

@TarasKuzyo
Copy link
Contributor Author

It is true but even segments have the same slopes and intercepts they do not necessarily intersect.
For example [(0, 1), (0, 5)] and [(0, 7), (0, 10)]. They lie on the same line but there is a gap between them. That's why I added segment_contains to handle this case.

@tacaswell tacaswell added this to the v3.0 milestone Jul 4, 2018
@tacaswell tacaswell requested a review from QuLogic July 4, 2018 00:32
@tacaswell
Copy link
Member

Thanks for digging into this @TarasKuzyo !

Would you be willing to paramaterize the tests a bit (make sure it works at a range of slopes) like we do with other tests in this file?

@TarasKuzyo
Copy link
Contributor Author

@tacaswell, I added one more test case for a range of slopes (0 - 180 deg). Hope, that is what you meant.

if (intercept == 0.0) { // segments lie on the same line
return (fmin(x1, x2) <= fmin(x3, x4) && fmin(x3, x4) <= fmax(x1, x2)) ||
(fmin(x3, x4) <= fmin(x1, x2) && fmin(x1, x2) <= fmax(x3, x4));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks better to me, though I didn't check your math. Note that you could just have the one return statement using y only if you wanted it all to be more elegant. Thanks for sticking with this!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can use y's only because of the same reasoning why we cannot use only x's.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If intercept == 0, it doesn't matter if you check y or x, everything is on the same line...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the intercept == 0, we excluded x = const lines, but if we replace x's with y's in the return statement the code will fail for y = const lines.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, OK, fair enough... Thanks!

@tacaswell
Copy link
Member

I meant to suggest checking co-linear intersection / overlap / non-overlap at a variety of slopes.

I would like @QuLogic to review this, but will merge in the next day or if @QuLogic does not beat me to it.

@TarasKuzyo
Copy link
Contributor Author

@tacaswell, it sounds reasonable. I will write those test tomorrow.

@jklymak
Copy link
Member

jklymak commented Jul 15, 2018

Ping @TarasKuzyo

@jklymak jklymak modified the milestones: v3.0, v3.1 Jul 16, 2018
@jklymak
Copy link
Member

jklymak commented Jul 16, 2018

Bumping to 3.1 as this still needs some work. If it sneaks in before 3.0, great!

@TarasKuzyo
Copy link
Contributor Author

After some inactivity I started with parametric tests (for arbitrary orientation of intersecting paths). I faced a numerical precision problem when comparing floating point numbers. Namely, I tested two orthogonal segments sharing a single intersection point.
The exact paths being tested are:
[[-0.8660254 , -0.5], [-2.59807621, -1.5]]
and
[[-3.09807621, -0.6339746], [-2.59807621, -1.5]]
For this setup the return condition in https://github.com/matplotlib/matplotlib/blob/master/src/_path.h#L832 fails because u1 = 1.0000000000000002.
Apparently, we need something like np.is_close in all (?) floating point comparisons, yet I am not sure on rtol and atol values.

@jklymak
Copy link
Member

jklymak commented Sep 28, 2018

@TarasKuzyo Sorry this fell by the wayside. I think you are pretty close. np.allclose, with relative tolerances should be fine. rtol should be small, 1e-10 or so? Basically how close can you be too zero where it is good enough?

@jklymak
Copy link
Member

jklymak commented Feb 7, 2019

ping @TarasKuzyo ; this still seems useful and was 98% of the way there!

@TarasKuzyo
Copy link
Contributor Author

@jklymak, thank you for the remainder. I'll take care of it the next week.

@jklymak
Copy link
Member

jklymak commented Feb 7, 2019

Cool, be sure to harass us because it doesn't show up at the top of anyone's github feed

@jklymak
Copy link
Member

jklymak commented Feb 25, 2019

Pushing to 3.2, but this would be very good to get in...

@jklymak jklymak modified the milestones: v3.1.0, v3.2.0 Feb 25, 2019
@TarasKuzyo
Copy link
Contributor Author

@jklymak, @QuLogic
Finally I found some time to finalize this PR.

Test cases written are quite extensive and cover
most relative path configurations. The trickiest part
is the edge case when two segments 'almost' touch
(that\s why I've abandoned it for a while).

Even though the function is working it has magic number(s) hard coded.
Those numbers define a minimal distance when two segments are considered
to be touching (1e-13 currently).

Obviously, it is always possible to design a failing test case,
so I am not sure how to handle it:

  • either leave as it is
  • or pass two additional arguments for relative and absolute tolerance

@jklymak
Copy link
Member

jklymak commented Feb 26, 2019

Can’t you just do with a relative tolerance? Absolute tolerance doesn’t let you work with small numbers. Relative tolerance works with the numbers you have.

@TarasKuzyo
Copy link
Contributor Author

TarasKuzyo commented Feb 26, 2019

No, because in several cases we compare a variable with zero.
Relative tolerance is useless there.

@jklymak
Copy link
Member

jklymak commented Feb 26, 2019

Both den and intercept are the subtraction of two numbers. You can check if those two numbers are relatively close.

@TarasKuzyo
Copy link
Contributor Author

Good point. Thank you!

@TarasKuzyo
Copy link
Contributor Author

TarasKuzyo commented Feb 26, 2019

But what if den = 0 - eps?

@jklymak
Copy link
Member

jklymak commented Feb 26, 2019

Well up to you. You can rewrite all the elements as relative math, but maybe an absolute tolerance is fine.

lib/matplotlib/tests/test_path.py Outdated Show resolved Hide resolved
@@ -273,6 +273,82 @@ def test_path_deepcopy():
copy.deepcopy(path2)


def test_path_intersect_path():
# test for the range of intersection angles
for phi in np.linspace(0, 2*np.pi, 180):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going through 180 angles seems a bit much. Have you checked, that the test still runs fast (<< 1s)? Otherwise it should be sufficient to go through some selected angles like

angles = np.array([0, 30, 45, 90, ...])
angles = np.concatenate([angles, angles+1, angles-1])

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@timhoffm, it does take about 1s to complete. I will use the approach you've suggested.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I've reduced the total range of angles to 30 so it runs faster.

@timhoffm timhoffm modified the milestones: v3.2.0, v3.1.0 Mar 2, 2019
@timhoffm timhoffm merged commit 4d1380f into matplotlib:master Mar 2, 2019
meeseeksmachine pushed a commit to meeseeksmachine/matplotlib that referenced this pull request Mar 2, 2019
jklymak added a commit that referenced this pull request Mar 3, 2019
…553-on-v3.1.x

Backport PR #11553 on branch v3.1.x (Improved Code for Segments Intersect)
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.

Inconsistent path intersection
4 participants
Morty Proxy This is a proxified and sanitized view of the page, visit original site.