Skip to content

Remove Boost and optimize half-edge graph and TSP implementations - #184

Open
vespakoen wants to merge 5 commits into
aewallin:masterfrom
vespakoen:feat/drop-boost
Open

vespakoen wants to merge 5 commits into
aewallin:masterfrom
vespakoen:feat/drop-boost

Conversation

@vespakoen

@vespakoen vespakoen commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

note: vibe coded with chatgpt astra xhigh

Remove Boost from OpenCAMLib's source and build dependencies, replace its graph and TSP implementations with code maintained in the project, and reduce copying and temporary allocations throughout cutter execution. Add parallel processing of independent adaptive path spans and fix OpenMP runtime loading on macOS.

The main performance changes are a KD-tree that copies each triangle once and visits query candidates directly, cheaper waterline interval/contact handling, and parallel adaptive path drop-cutter. Cutter contact equations, sampling defaults, and adaptive flatness criteria are retained.

Algorithms and data structures

Area Original implementation Implementation in this PR
Half-edge graph (src/common/halfedgediagram.hpp) HEDIGraph backed by Boost's adjacency list. Project-owned graph with distinct vertex/edge index handles, deque-backed records, reusable deleted slots, and small contiguous adjacency lists. Growth preserves existing property references. Snapshot enumeration supports mutation during traversal; out_edges_view() supports traversal without allocating a snapshot.
Weave construction/traversal (src/algo/{simple_weave,smart_weave,weave}.cpp) Boost descriptors and temporary adjacency collections. Both weave implementations use the new graph. SmartWeave connects its four neighbors using fixed arrays and fewer edge lookups; face traversal borrows adjacency lists. Loop conversion reserves output storage.
TSP (src/algo/tsp.hpp) Boost's metric TSP approximation over a stored complete adjacency matrix. Dense Prim computes an MST using distances on demand, followed by iterative preorder traversal and shortcutting. O(n²) time and O(n) auxiliary memory. Tours remain closed and retain the metric 2-approximation guarantee; equal-weight ties resolve deterministically by vertex index.
KD-tree construction (src/common/{kdtree,kdnode}.hpp) Copy triangle lists repeatedly while finding splits and building child nodes. Make one owned triangle snapshot, calculate spreads in fixed storage, and partition with list splicing. Empty trees are supported, and a replacement tree is built before replacing the existing root.
KD-tree queries and cutters Allocate and copy a candidate triangle list for each cutter location or fiber. visit() / visit_cutter_overlap() invoke callbacks on tree-owned triangles. The default batch/point drop-cutter and batch/fiber push-cutter execution paths use these visitors. Existing owning search APIs remain available. Preserve hi-before-lo traversal and candidate order, including equal-height contact selection.
Small Boost utilities BOOST_FOREACH, boost::tie, and Boost floating-point classification. Range-based loops, std::tie, and std::isnan, using the existing C++14 toolchain.

ocl::hedi stands for half-edge diagram. It is the existing OpenCAMLib abstraction, now implemented directly in the project. Storage, adjacency operations, and traversal can be tuned locally without depending on Boost Graph.

Allocation and ownership changes

  • Use stack intervals in push-cutter loops and copy contact values directly in Interval::updateLower() / updateUpper(), eliminating temporary heap objects.
  • Merge overlapping fiber intervals by accumulating their union and compacting surviving intervals once. Preserve interval order and tied endpoint contacts without allocating an overlap vector or repeatedly shifting the tail.
  • Slice triangle vertices into fixed-size arrays instead of allocating vectors in Triangle::zslice_verts().
  • Pass adaptive recursion endpoints by const reference, avoiding repeated CLPoint and Fiber copies. Use stack objects for the adaptive-waterline sampling line/span.
  • Composite cutters pass only fiber geometry to their components and merge accepted contacts directly, avoiding repeated copies of accumulated intervals and temporary contact vectors. Accepting a facet/edge contact now releases the previous contact object.
  • Fix leaked replaced sub-operations in AdaptiveWaterline, leaked contact objects in BatchPushCutter::getCLPoints(), and the heap-allocated empty vector in Operation::getCLPoints(). The base getter still returns an empty vector; derived getters continue returning their results.
  • Remove further unnecessary copies in cutter loops and transfer the filtered point list with swap().

OpenMP: what is new and what changed

Operation Before Change in this PR
AdaptivePathDropCutter Path spans processed serially. New parallelism: dynamically schedule independent line/arc spans across up to min(requested threads, span count) workers. Each span owns its output; concatenate in original path order. A const point-drop query uses the shared read-only KD-tree and returns a local call count, combined with an OpenMP reduction. A single span remains serial.
BatchDropCutter / uniform PathDropCutter Already parallel across cutter locations. Keep that parallelism, remove query-result allocations, use call-count reductions, and set the team size on the parallel region.
BatchPushCutter / uniform Waterline Already parallel across fibers. Keep that parallelism, use stack intervals and direct candidate visits, and replace the shared counter update with a reduction.
AdaptiveWaterline Already dispatched X/Y fiber directions using tasks or platform-specific sections. Use two parallel sections, capped at two workers, and honor setThreads(1). Recursive refinement within each direction remains serial.

Thread counts are scoped with num_threads(...) instead of changing the calling thread's OpenMP default with omp_set_num_threads(). setThreads(0) is normalized to one. OpenMP was already enabled by default and remains optional: USE_OPENMP=OFF keeps serial execution, and Emscripten builds continue to disable it. Weave construction and TSP remain serial.

Build, packaging, and compatibility

  • Remove Boost discovery, include/link requirements, downloads, installer options, package dependencies, and CI installation steps across the C++, Python, Node.js, and Emscripten builds. Remove the exported CMake package's Boost requirement. The obsolete --install-boost, --install-boost-from-repo, and --boost-prefix installer flags are removed.
  • Fix macOS libomp loading. Native builds use brew install libomp; cross-compilation still fetches the target architecture's bottle, relocates its dylib ID to @rpath/libomp.dylib, and re-signs it. Raw bottles retain Homebrew placeholder paths until relocated. The default macOS target architecture now follows the host.
  • Add native CTests and run them from the installer C++ test step (BUILD_TESTING=OFF opts out). Add the optional cutter benchmark (BUILD_BENCHMARKS=ON) and comparison script, including cylindrical, ball, bull, cone, and composite cutter selection.
  • Rebuild C++ consumers: graph/interval layouts and low-level graph types changed. Direct graph users should use HEDIGraph<VertexProps, EdgeProps, FaceProps> and the hedi descriptor types. TSP tour order may differ when distances tie; its closed-tour and approximation guarantees are retained.

Full original-Boost-to-current comparison

Every before measurement below is the original Boost implementation at 93c3cf3; every after measurement is the current implementation, including the latest allocation and composite-cutter changes. These are fresh direct comparisons of the complete PR.

Both libraries and benchmark drivers use AppleClang 21 on macOS ARM64. Libraries are Release builds with -O3, thin LTO, and the same OpenMP runtime; the original uses Boost 1.87.0. Each value is the median of five alternating before/after samples. Toolpath times include KD-tree setup, execution, output collection, and destruction; STL loading is excluded. Speedup is before time divided by after time.

Common fixture: sampling 0.1, waterline Z=1, and 25 horizontal drop-cutter spans at Z=-1 across the model bounds. Adaptive minimum-sampling defaults are retained. setThreads and OMP_NUM_THREADS both use the reported count, with OMP_DYNAMIC=FALSE. The four-thread adaptive-path results include the benefit of newly parallel spans; the original ran those spans serially. Adaptive waterline has two independent directions and therefore uses at most two workers in the current implementation.

Cylindrical cutter: CylCutter(4, 20).

Tux (gnu_tux_mod.stl):

Operation 1 thread: Boost → current Speedup 4 threads: Boost → current Speedup
Waterline 412.67 → 172.69 ms 2.39× 198.47 → 64.70 ms 3.07×
Adaptive waterline 1,134.01 → 513.71 ms 2.21× 660.32 → 279.74 ms 2.36×
Path drop-cutter 953.34 → 163.07 ms 5.85× 330.66 → 50.28 ms 6.58×
Adaptive path drop-cutter 4,096.96 → 668.04 ms 6.13× 3,983.12 → 215.94 ms 18.45×

Sphere-on-plate (sphere_on_plate.stl):

Operation 1 thread: Boost → current Speedup 4 threads: Boost → current Speedup
Waterline 895.93 → 458.48 ms 1.95× 753.88 → 380.05 ms 1.98×
Adaptive waterline 1,746.07 → 906.66 ms 1.93× 1,461.53 → 780.54 ms 1.87×
Path drop-cutter 384.61 → 64.81 ms 5.93× 138.75 → 22.59 ms 6.14×
Adaptive path drop-cutter 947.56 → 143.75 ms 6.59× 949.96 → 44.68 ms 21.26×

Composite cutter: CylConeCutter(2, 4, 0.7); the angle is in radians.

Tux (gnu_tux_mod.stl):

Operation 1 thread: Boost → current Speedup 4 threads: Boost → current Speedup
Waterline 1,099.42 → 579.78 ms 1.90× 382.12 → 169.19 ms 2.26×
Adaptive waterline 3,863.32 → 2,154.60 ms 1.79× 2,137.86 → 1,106.80 ms 1.93×
Path drop-cutter 1,831.64 → 1,080.84 ms 1.69× 584.95 → 289.39 ms 2.02×
Adaptive path drop-cutter 8,505.16 → 5,177.96 ms 1.64× 8,562.79 → 1,505.37 ms 5.69×

Sphere-on-plate (sphere_on_plate.stl):

Operation 1 thread: Boost → current Speedup 4 threads: Boost → current Speedup
Waterline 1,027.36 → 483.31 ms 2.13× 788.83 → 382.75 ms 2.06×
Adaptive waterline 2,016.07 → 943.60 ms 2.14× 1,664.21 → 798.85 ms 2.08×
Path drop-cutter 519.44 → 200.23 ms 2.59× 176.57 → 58.65 ms 3.01×
Adaptive path drop-cutter 1,301.91 → 490.47 ms 2.65× 1,287.85 → 145.98 ms 8.82×

Focused weave and TSP workloads, also measured directly against the original Boost implementation:

Workload Boost Current Speedup
SimpleWeave, 160×160 grid 20.83 ms 9.60 ms 2.17×
SmartWeave, 160×160 grid 1.25 ms 0.68 ms 1.84×
TSP, 1,500 points 16.07 ms 6.44 ms 2.49×

Each microbenchmark sample averages 20 SimpleWeave runs, 100 SmartWeave runs, or 20 TSP runs; the table reports the median of five alternating samples. Weave workloads include fiber creation, graph construction/traversal, and output collection. The TSP workload uses the same 1,500 points generated with a fixed seed in both builds. These results are local measurements for the stated fixtures; model geometry and available cores affect speedup.

Validation

  • All five CTests pass in Release with OpenMP enabled, Release with OpenMP disabled, and Debug with AddressSanitizer/UndefinedBehaviorSanitizer.
  • Direct comparisons against the original Boost build match coordinates and contact data exactly on both models for cylindrical, ball, bull, cone, cylinder-cone, and ball-cone cutters. Coverage includes uniform/adaptive drop-cutter, adaptive waterlines at Z=1/4, and uniform waterlines at Z=1/4/8. Loop starting vertices and loop order are normalized for comparison; no coordinate rounding is used.
  • Current serial/parallel tests verify ordered points, interval endpoints, contact types, and call counts, including empty paths and adaptive line/arc spans. They also verify that operations leave the caller's OpenMP thread default unchanged.
  • Graph tests cover slot reuse, property-reference stability, copies, parallel edges, self-loops, face traversal, edge splitting, and randomized mutations. TSP tests check closed tours, repeatability, empty/single/duplicate points, deep trees, and the approximation bound against brute-force optima on small inputs.
  • KD-tree tests cover copy counts, ownership after input destruction, ordering, empty/rebuilt trees, different search planes/bucket sizes, and concurrent read-only queries. Geometry tests cover vertex-order permutations when slicing and interval endpoint ties. The new composite contact-release regression test fails on the pre-fix implementation and passes with the fix.

To reproduce toolpath comparisons, build the same benchmark driver against each revision's own headers/library and run:

export OMP_DYNAMIC=FALSE
python3 benchmarks/compare.py build/before/cutter_benchmark build/after/cutter_benchmark stl/gnu_tux_mod.stl stl/sphere_on_plate.stl --threads 1 4 --repetitions 5 --cutter cyl --output build/cyl-comparison.json
python3 benchmarks/compare.py build/before/cutter_benchmark build/after/cutter_benchmark stl/gnu_tux_mod.stl stl/sphere_on_plate.stl --threads 1 4 --repetitions 5 --cutter cyl-cone --output build/composite-comparison.json

Use the same compiler, optimization settings, OpenMP runtime, and OMP_DYNAMIC=FALSE for both builds. BUILD_BENCHMARKS=ON builds the driver on the current branch; the original revision needs the same driver compiled separately against its Boost-enabled library.

@vespakoen
vespakoen marked this pull request as ready for review October 17, 2026 10:52
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.

1 participant