-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
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
Conversation
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 |
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.
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.
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.
Sorry, I fear, I do not fully understand your point. Could you give a quick example, please?
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.
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
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.
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.
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.
- Yes, if the slope is infinite, you need to special case
- 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.
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.
If the slope is finite we have to:
- check if intercepts are the same
- 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 thatx1 < x2
(the same applies for the other pair andy
'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.
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.
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)
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.
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.
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.
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,
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.
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.
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. |
It is true but even segments have the same slopes and intercepts they do not necessarily intersect. |
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? |
@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)); | ||
} |
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.
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!
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.
I don't think we can use y
's only because of the same reasoning why we cannot use only x
's.
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.
If intercept == 0
, it doesn't matter if you check y or x, everything is on the same line...
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.
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.
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.
Ah, OK, fair enough... Thanks!
@tacaswell, it sounds reasonable. I will write those test tomorrow. |
Ping @TarasKuzyo |
Bumping to 3.1 as this still needs some work. If it sneaks in before 3.0, great! |
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. |
@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? |
ping @TarasKuzyo ; this still seems useful and was 98% of the way there! |
@jklymak, thank you for the remainder. I'll take care of it the next week. |
Cool, be sure to harass us because it doesn't show up at the top of anyone's github feed |
Pushing to 3.2, but this would be very good to get in... |
@jklymak, @QuLogic Test cases written are quite extensive and cover Even though the function is working it has magic number(s) hard coded. Obviously, it is always possible to design a failing test case,
|
…tplotlib into segments-intersect
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. |
No, because in several cases we compare a variable with zero. |
Both den and intercept are the subtraction of two numbers. You can check if those two numbers are relatively close. |
Good point. Thank you! |
But what if |
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
@@ -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): |
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.
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])
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.
@timhoffm, it does take about 1s to complete. I will use the approach you've suggested.
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.
So, I've reduced the total range of angles to 30 so it runs faster.
…tplotlib into segments-intersect
…553-on-v3.1.x Backport PR #11553 on branch v3.1.x (Improved Code for Segments Intersect)
PR Summary
Supposedly fixes #11527 (Inconsistent path intersection).
Path.intersects_path
didn't work properly oncollinear 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
returnTrue
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