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 92 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 added 3 commits June 1, 2025 12:42
…status! (mostly has to do with the isless definition) but this works in all degenerate cases in FP precision.
… needs more time or more brains. Also may have fixed FP32 robustness. trying to move away from snapping to grid until pushing to the output (maintaining true spatial relationships)
@juliohm
Copy link
Member

juliohm commented Jun 17, 2025

Thanks for pushing this @souma4 ❤️ I will try to review by the end of the week 🙏🏽

@souma4
Copy link
Contributor Author

souma4 commented Jun 17, 2025

I'm honestly glad for the extra time, managed to fix some huge uneeded time sinks. I know there's still optimization there (just have to wrap my head around it) and FP32 still failing is aggravating. I need to research more robust checks. I saw one paper suggesting signed area is "second order" which makes FP64 work but not 32. A solution might be buried in there without requiring a whole new data structure. But you'll probably have some ideas to resolve type instabilities in the intersection calc.

souma4 added 2 commits June 27, 2025 09:25
…orientation tests. also fixed tests for FP32. When the values of FP32 are correct, the algorithm works. Rotation isn't 100% FP32 robust and the intersection calculations for the tests at L127 don't end up near the correct points, thus rounding isn't working.
Copy link
Contributor

github-actions bot commented Jun 28, 2025

Benchmark Results (Julia vlts)

Time benchmarks
master 3ddb02f... master / 3ddb02f...
clipping/SutherlandHodgman 3.77 ± 0.16 μs 3.69 ± 0.15 μs 1.02 ± 0.06
discretization/simplexify 26.1 ± 1.7 ms 26.1 ± 1.8 ms 1 ± 0.094
sideof/ring/large 6.53 ± 0.01 μs 6.53 ± 0.01 μs 1 ± 0.0022
sideof/ring/small 0.05 ± 0.001 μs 0.05 ± 0.001 μs 1 ± 0.028
winding/mesh 0.0422 ± 0.0019 s 0.0423 ± 0.0018 s 0.998 ± 0.063
time_to_load 1.47 ± 0.002 s 1.48 ± 0.0095 s 0.996 ± 0.0066
Memory benchmarks
master 3ddb02f... master / 3ddb02f...
clipping/SutherlandHodgman 0.053 k allocs: 4.97 kB 0.053 k allocs: 4.97 kB 1
discretization/simplexify 0.226 M allocs: 21.8 MB 0.226 M allocs: 21.8 MB 1
sideof/ring/large 0 allocs: 0 B 0 allocs: 0 B
sideof/ring/small 0 allocs: 0 B 0 allocs: 0 B
winding/mesh 0.231 M allocs: 23 MB 0.231 M allocs: 23 MB 1
time_to_load 0.153 k allocs: 14.5 kB 0.153 k allocs: 14.5 kB 1

Copy link
Contributor

github-actions bot commented Jun 28, 2025

Benchmark Results (Julia v1)

Time benchmarks
master 3ddb02f... master / 3ddb02f...
clipping/SutherlandHodgman 3.33 ± 0.19 μs 3.42 ± 0.3 μs 0.973 ± 0.1
discretization/simplexify 25.5 ± 2.6 ms 24.8 ± 3.4 ms 1.03 ± 0.17
sideof/ring/large 6.83 ± 0.01 μs 6.78 ± 0.011 μs 1.01 ± 0.0022
sideof/ring/small 0.06 ± 0 μs 0.06 ± 0 μs 1 ± 0
winding/mesh 0.0423 ± 0.0025 s 0.0425 ± 0.0028 s 0.996 ± 0.088
time_to_load 1.48 ± 0.025 s 1.53 ± 0.0092 s 0.967 ± 0.017
Memory benchmarks
master 3ddb02f... master / 3ddb02f...
clipping/SutherlandHodgman 0.053 k allocs: 4.83 kB 0.053 k allocs: 4.83 kB 1
discretization/simplexify 0.324 M allocs: 21.8 MB 0.324 M allocs: 21.8 MB 1
sideof/ring/large 0 allocs: 0 B 0 allocs: 0 B
sideof/ring/small 0 allocs: 0 B 0 allocs: 0 B
winding/mesh 0.329 M allocs: 22.9 MB 0.329 M allocs: 22.9 MB 1
time_to_load 0.159 k allocs: 11.2 kB 0.159 k allocs: 11.2 kB 1

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.

I know this PR is taking longer than what we expected, but I think the only way to speed things up is to make sure we have a version that is ready for review. My understanding is that you are still exploring ideas and variations of the code, and that it is not polished for final review yet. Could you please try to clean the implementation as much as possible, follow our code style and variable names, and then add comments explaining what is happening?

If you feel that Float32 is an unattainable goal, we can focus on a working version that is robust with Float64, and only consider tests in that context. The Float32 case could be useful in the context of GPUs, but we need a bullet-proof CPU implementation first.

src/Meshes.jl Outdated
@@ -29,6 +30,7 @@ using DelaunayTriangulation: get_polygon_points
using StatsBase: AbstractWeights, Weights, quantile
using TiledIteration: TileIterator
using ScopedValues: ScopedValue
using AdaptivePredicates: orient2, orient2fast
Copy link
Member

Choose a reason for hiding this comment

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

orient2fast is not used in the PR

Also, could you please confirm that Meshes.signarea was not enough and that we really need orient2 to make this work?

Copy link
Contributor Author

@souma4 souma4 Jul 1, 2025

Choose a reason for hiding this comment

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

Thank you for asking me to confirm because signarea actually was sufficient. I added AdaptivePredicates.jl as a way to have maximum robustness and to somewhat get around FP32 lack of precision. Plus, the overhead for it was minimal. But signarea is giving the same passing tests. I've just left a breadcrumb comment in case we ever need to revisit this.

test/utils.jl Outdated
Comment on lines 85 to 88
segs = Segment.([(cart(0, 0), cart(2, 2)), (cart(0, 2), cart(2, 0)), (cart(0, 1), cart(0.5, 1))])
points, seginds = Meshes.bentleyottmann(segs)
@test length(points) == 7
@test length(seginds) == 7
Copy link
Member

Choose a reason for hiding this comment

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

I believe we discussed this issue before. The algorithm should only return the intersection points. Right now it is returning all points, leading to unnecessary allocations.

souma4 and others added 3 commits July 5, 2025 14:59
…e input data is ordered in a useful way. updated documentation to note FP32 concerns. split start and end initialization into one function that can handle either (maintainability)
@juliohm
Copy link
Member

juliohm commented Jul 7, 2025

@souma4 I tried to cleanup the code further, but I am finding it difficult to do so. I believe you are used to other programming languages where types appear in many places. Can you please try to reduce the amount of explicit type handling? Many functions like _ybounds didn't need a T type argument. You could simply extract the type internally in the function body (if necessary).

Also, try to follow our code style with comments in lowercase, avoid unnecessary underscores (e.g., _SweepLine), etc. I know it is hard to catch up with the current code style, but the project is so large at this point that you can try to read other source files to mimic the current style as much as possible.

I still believe that we can simplify this implementation further to a point where we can clearly read it and know what is happening without too much back and forth with low-level information passed around. For example, sometimes you define auxiliary functions in terms of s = (a, b) and sometimes you define them in terms of a and b separate arguments. The more consistent these arguments are, the smaller are the chances we hit bugs. Also, try to name all variables consistently everywhere. If a function call a variable p, the other function should use p for the same purpose. It is hard to track the algorithm otherwise.

I really think we have something promising here, but we will only reach a point where we feel safe about the implementation when these iterations and reviews start to converge into a cohesive, consistent code. Thanks for the amazing effort you already put into this 🙏🏽 ❤️

souma4 and others added 2 commits July 13, 2025 12:42
…roughout function barriers to easily track what kind of object is pass to which function. Fixed the tests to account for ordering of outputs because hashtables change upon new sessions, leading to variable ordering of outputs. This simplifies tests.
seg = first(segments)
ℒ = lentype(seg)
τ = ustrip(eps(ℒ))
round(Int, 0.8 * (-log10(τ))) # 0.8 is a heuristic to avoid numerical issues
Copy link
Member

Choose a reason for hiding this comment

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

This 0.8 was not here before. Can we simply use tau = atol(numtype(lentype(seg)))?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That does work, however it does seem to make one more Float32 test fail (easy to skip it). The 0.8 arose because I had switched to using machine epsilon. If I remember right that was largely to more specifically reflect the precision of the numtype. But this atol switch is a bit more forgiving and seems to work. I'm cool with tau = atol(numtype(lentype(seg)) rather than being reliant on a tuned parameter from eps.

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 remember the code that was there before, but the 0.8 factor is very ad-hoc. We shouldn't have that.

_keyseg(ns) = _segment(BinaryTrees.key(ns))

# handles the degenerate case of trivial (0-length) segments
_istrivial(seg) = seg[1] == seg[2]
Copy link
Member

Choose a reason for hiding this comment

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

I am concerned that I am still reviewing dead code. This function isn't used anywhere.

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