Skip to content

Add Bentley-Ottman Algorithm #1168

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

Open
wants to merge 63 commits into
base: master
Choose a base branch
from

Conversation

souma4
Copy link
Contributor

@souma4 souma4 commented Feb 11, 2025

see issue #1126 and PR #1125

I needed to refactor and I messed up with the last draft #1143.

This produces a dictionary of vertices with a vector of segment values. Future needs: A test that the outputs are correct, and general method for rebuilding polygons with this structure.

@juliohm sorry about messing up the prior draft. Live and learn. I'll let you look this over. I think a polygon build method for this output is a new PR. This outputs vertices with segment info. If someone just wants the vertices they can grab the keys. If someone wants a polygon they can use a build method.

@juliohm
Copy link
Member

juliohm commented Feb 17, 2025

@souma4 please let me know when the PR is ready for review. I have some review comments already, just waiting for your green light.

@souma4 souma4 marked this pull request as ready for review February 17, 2025 15:35
@souma4
Copy link
Contributor Author

souma4 commented Feb 17, 2025

@juliohm Thanks for reminding me!

Copy link
Member

@juliohm juliohm left a comment

Choose a reason for hiding this comment

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

Thank you @souma4 for working on this. It is really nice to see the amazing progress you've made already ❤️

Attached is my first round of review with a couple of suggestions to improve the PR. Let's work on it together slowly, possibly improving the BinaryTrees.jl dependency before coming back here.

Looking forward to the next round. It looks very promising!

…tests. Edited test to be more intensive. updated entire script to meet style and Julia best practices. Removed unneded functions. Shifted BinaryTrees relevant materials to PR JuliaGeometry#12 in BinaryTrees and adjusted code as needed. Reordered point processing to reduce the likelihood of a bug when processing start and intersection segments.
…d function. removed println calls used for debugging
Copy link
Member

@juliohm juliohm left a comment

Choose a reason for hiding this comment

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

Attaching a few minor comments. Tomorrow I will try to find some time to run tests locally.

The good news is that the output of @code_warntype is all green.

@juliohm
Copy link
Member

juliohm commented Mar 3, 2025

Tests are failing because the utility function is not exported. We can either export it or qualify the calls in the test suite with the Meshes prefix.

Copy link
Member

@juliohm juliohm left a comment

Choose a reason for hiding this comment

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

More basic fixes before the actual review of the algorithm.

Copy link
Member

@juliohm juliohm left a comment

Choose a reason for hiding this comment

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

Tests are failing due to the latest review changes.

@souma4
Copy link
Contributor Author

souma4 commented Mar 17, 2025

So this works, which is awesome. Problem, doing some performance testing shows its n^2. Most of the time spent (according to profview) is occurring in the intersection checks from the \ solving for intersect parameters. I do wonder, though, if dictionary checking is really adding up. When looking at allocations, the inds = [lookup[s] for s in ....] does appear to be a significant driver.

@juliohm
Copy link
Member

juliohm commented Mar 17, 2025

Really nice progress. How are you checking the n^2 performance?

Some tests are failing with Float32.

@souma4
Copy link
Contributor Author

souma4 commented Mar 17, 2025

I checked time complexity with the REPL off hand. created numbers = 1:10:10_000, generated n of numbers random segments over numbers, then iterated through each random generation, sending the output of @belapsed to a time variable. Then plotted it. I did fit a line by doing n .* n .* S where S is a scaling factor I just messed around with until it fit good enough for me.

Yeah, some tests are failing with Float32, I'm not sure why, and I'll deal with it tomorrow haha.

souma4 added 3 commits March 19, 2025 16:44
…ght it would so I'm just hard coding the Float32 tolerance.
…e removal of ending segments from the status structure. My local tests suggest roughly logarithmic time, although caching from garbage collection makes it sort of stepwise
@juliohm
Copy link
Member

juliohm commented Mar 20, 2025

@souma4 I will try to review tomorrow or over the weekend. Thank you for looking into it! :)

@juliohm
Copy link
Member

juliohm commented Mar 20, 2025

In the meantime, I have a question regarding the overall structure of the algorithm.

I noticed that the pseudo-code by Bentley-Ottman in their original paper has a different structure:

image

It can be read as

for p in ...
  if s is a segment with p in the left
    # do something
  elseif s is a segment with p in the right
    # do somethine else
  else # p is some intersection
    # do something else
  end
end

In this PR the structure is a bit different:

for p in ...
  for s in left segments
    # do something
  end
  for s in right segments
    # do something else
  end
  for s in middle segments
    # do something else
  end
end

I understand that the latter structure is explicitly "greedy" and loops over all segments related to point p. The former structure in the paper seems incomplete in terms of these details. Just double checking if we are not missing something. Did you have a chance to think about it? Are these two structures equivalent?

@souma4
Copy link
Contributor Author

souma4 commented Mar 20, 2025

Yeah I've wracked my head over this a good bit. From my testing and thinking, the structures are equivalent when segments are expected to be completely independent (ie no corners) and there aren't more than two lines intersecting. But when corners and more segments are involved, the former algorithm will "skip" line segments and/or not capture intersections. This is because the earlier segments for a given point may intersect with later segments, but due to handling, this intersection may not be captured because there are segments "in between" the two focal segments within the status structure.

@juliohm
Copy link
Member

juliohm commented Mar 22, 2025

I introduced a helper _digits function to extract the digits based on the atol of the underlying floating point type. Let's see if tests pass. If they still fail in Float32, we can relax the heuristic a bit more.

@juliohm
Copy link
Member

juliohm commented Mar 22, 2025

The heuristic for the number of digits worked well. Assuming a tolerance of 1e-10, we get 10-1 digits by default. The same for 1e-5, which becomes 5-1 digits.

I noticed that the output of the algorithm is including the original points besides the intersections. For instance, here is what I get in the second test:

viz(segs)
viz!(points, color="red")

image

Could you please take a look to make sure we are only returning intersection points?

@souma4
Copy link
Contributor Author

souma4 commented Mar 22, 2025

Oh if we only want intersections (not all points) then this is an easy change. I'll commit fixes and update tests to reflect. Although I am finding some variance in tests, so I'll need to debug and see if it's real or not.

Outside of that, the facetgrid test returns nothing because the facet generator only generates cornerintersections, which are start/endpoints and not included.

@juliohm
Copy link
Member

juliohm commented Mar 23, 2025

Outside of that, the facetgrid test returns nothing because the facet generator only generates cornerintersections, which are start/endpoints and not included.

Very good point. I think we need to refactor this test by constructing segments manually in a grid-like arrangement. We could do the following instead:

horizontal = [Segment((1, i), (n, i)) for i in 1:n]
vertical = [Segment((i, 1), (i, n)) for i in 1:n]
segs = [horizontal; vertical]

…. EVENTS ARENT BEING HANDLED PROPERLY. I DONT HAVE TIME TO ADDRESS PROMPTLY
@juliohm
Copy link
Member

juliohm commented Mar 23, 2025

Thank you @souma4 for looking into it ❤️ Tests with random segments show that the algorithm is not there yet. Perhaps we could try to follow the algorithmic structure in Chapter 2 of Berg et al. Computational Geometry - Applications and Algorithms instead. This structure is discussed step-by-step in the video I linked in the issue. More specifically, it is discussed in this second video: https://www.youtube.com/watch?v=qkhUNzCGDt0

(If you need access to the book, I have the PDF and can send it to you directly)

We are almost there!

@souma4
Copy link
Contributor Author

souma4 commented Mar 23, 2025

agreed. My plan was to pull up De Berg to see exactly what they do. I'm fairly busy this week, but, it'll keep moving haha. I think we are pretty close though.

…s far as I got with De Berg implementation. All tests fail. I think this is occuring between the status structure isn't reflecting the order where the plane crosses, but the total order in x and y (so a line with a low x will always be the minimum even if where it intersects the plane is the maximum). I tried having a sort method to maybe get it but that didn't work. I'm sort of at a loss outside of computing intersections to the plane, which I can't think of an efficient way of doing that right now. Maybe y'all will have some ideas
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.

3 participants