feat: Hierarchical LSCM (HLSCM) - cascadic multigrid UV parameterization#44
Merged
csparker247 merged 24 commits intodevelopfrom Mar 20, 2026
Merged
feat: Hierarchical LSCM (HLSCM) - cascadic multigrid UV parameterization#44csparker247 merged 24 commits intodevelopfrom
csparker247 merged 24 commits intodevelopfrom
Conversation
…(F1) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Seven tests covering pyramid (single-level fallback), explicit pins, instance API, double precision, ABF++ integration, hemisphere (289 vertices with curvature), and wavy surface (400 vertices with varying curvature). Hemisphere and wavy surface tests verify validity (finite UVs, z=0, no triangle flips) rather than exact match against AngleBasedLSCM since HLSCM uses an iterative solver (LSCG). The stub delegates to AngleBasedLSCM and will be replaced with the real hierarchical implementation in subsequent phases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add detail::hlscm namespace with QEM-based mesh decimation engine: - Quadric<T> for Garland-Heckbert error metric - DecimationMesh<T> with flat adjacency, half-edge collapse, validity checks - buildHierarchy() for creating multi-level mesh hierarchy - buildLevelMesh() for constructing HalfEdgeMesh from hierarchy levels - CollapseRecord for barycentric prolongation data Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace AngleBasedLSCM delegation stubs with the real hierarchical solver: build QEM-decimated mesh hierarchy, solve LSCM on coarsest level, then prolongate and refine at each finer level using CG with initial guess. Small meshes fall back to single-level LSCM for exact equivalence. Fix barycentric coordinate computation in collapse records to correctly express removed vertex position in the post-collapse triangle. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add timed comparison of HLSCM vs AngleBasedLSCM (both using LSCG solver) on a 75x75 wavy surface (5625 verts, ~11K faces). Demonstrates ~3x speedup from hierarchical initial guess. Fix decimation validity: implement minimum-angle threshold (10 degrees) from the original paper, checking ALL post-collapse faces incident to the kept vertex. Prevents gradual triangle degradation across multiple collapses that caused degenerate faces at higher mesh densities. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- CRITICAL: preserve original mesh angles at finest hierarchy level instead of recomputing from geometry (ABF-optimized angles were lost) - HIGH: rebuild edge list before each hierarchy level's PQ construction to avoid iterating stale/dead edges - MEDIUM: compact dead face indices from vertFaces_ after each collapse - MEDIUM: handle non-LSCG solver types via if-constexpr instead of unconditionally calling solveWithGuess - MEDIUM: extract auto-pin selection into shared AutoSelectPins helper - LOW: add tests for setLevelRatio/setMinCoarseVertices instance API - LOW: add test verifying multi-level hierarchy is triggered Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Confirms that ABF-optimized angles flow through to the finest hierarchy level by checking that ABF++ → HLSCM produces different UVs than geometry-only HLSCM. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Measures angle error between the input 3D mesh and the 2D parameterization. ABF++ → HLSCM achieves ~99.9% lower squared angle error than geometry-only HLSCM on a 20x20 wavy surface. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Edge idx values are assigned from the full half-edge pool (including boundary half-edges), so they are not contiguous in [0, num_edges()). Use unordered_map instead of flat vector to avoid out-of-bounds write. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
3 tasks
* feat: add BenchmarkFlattening example app (E1) Benchmarks three flattening configurations on user-supplied meshes and prints a Markdown table: ABF++ angle optimization time + LSCM SparseLU, LSCM LSCG, and HLSCM runtimes. Motivated by volume-cartographer#123. Closes #45 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: benchmark LSCG at 1 thread and powers of 2 up to --threads N Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * build: link OpenMP to example apps when available Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add --output-dir and HLSCM thread columns to benchmark (E1) - --output-dir DIR writes {stem}_lscm_lu.obj, _lscm_lscg.obj, _hlscm.obj - HLSCM is also timed across 1..N threads (uses LSCG internally) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: include --threads N in benchmark sequence when not a power of 2 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add --builtin, CG column, and C++17 compatibility to benchmark (E1) - Add --builtin [MAX] flag to generate built-in wavy-surface sequence (50k, 100k, 200k, 400k, 600k, 800k, 1M faces) up to MAX faces, matching the face-count sequence from volume-cartographer#123 - Add LSCM CG (ConjugateGradient) column alongside LSCM LSCG; CG operates on normal equations (AtA) and is ~4x faster than LSCG - Fix C++17 incompatibility: avoid capturing structured bindings in lambdas (C++20 extension) by using a named loop variable instead - Update doc-comment to describe all options in a consistent format Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: replace LSCG with CG for HLSCM and drop LSCM LSCG column ConjugateGradient (CG) works on the normal equations (AtA) and avoids storing the full rectangular system, eliminating OOM failures on large meshes. HLSCM now uses CG as its inner solver, consistent with LSCM CG. LSCM LSCG column removed as CG is faster and more memory-efficient. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: enable warm-start for ConjugateGradient in HLSCM The solveWithGuess branch was gated on is_instance_of_v<..., LSCG>, which doesn't work for Eigen::ConjugateGradient due to its non-type int UpLo template parameter. Replace with std::is_base_of_v against Eigen's CRTP base IterativeSolverBase<SolverType>, which covers both CG and LSCG (and any future iterative solver) correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: make ConjugateGradient the default solver for HLSCM CG solves the normal equations (AtA x = Atb), avoiding storing the full rectangular system. This makes it faster and more memory-efficient than LSCG for most mesh sizes, enabling larger meshes without OOM. HLSCM now dispatches on solver type: - LSCG: passes rectangular A directly to solver (existing behavior) - Other iterative solvers (CG): builds AtA/Atb and uses solveWithGuess for warm-starting from the coarser-level solution - Direct solvers (SparseLU etc): falls through to SolveLeastSquares Update PerformanceComparison test to compare HLSCM vs flat LSCM using the same CG solver (apples-to-apples). Allow tiny negative areas in the no-flip check to handle near-zero numerical noise (~1e-7) vs real flips which are O(1e-4) and above. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * revert: restore LSCG as default solver for HLSCM Benchmarking shows HLSCM+CG is 10-25% slower than flat LSCM+CG due to normal equations (AtA) squaring the condition number and negating the warm-start benefit. LSCG operates on the rectangular system directly, making it the only inner solver for which the hierarchical warm-start produces a meaningful speedup (10-15x on 50k-100k face meshes). Update doc-comment and revert PerformanceComparison test to LSCG. The CG dispatch path in solveLSCMLevel is retained for users who explicitly pass ConjugateGradient as the Solver template parameter. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: use LSCG (default) for HLSCM in benchmark HLSCM+CG is slower than flat LSCM+CG due to normal equations squaring the condition number. Use the default LSCG to show HLSCM at its best. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: correct HLSCM column label to LSCG in benchmark output Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: save original 3D mesh to output-dir alongside flattened results Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- BenchmarkFlattening: change LSCM CG solver to ConjugateGradient<..., Lower|Upper> so Eigen uses its full-matrix SpMV code path, which is OpenMP-parallelized. The default Lower-only variant routes through selfadjointView<Lower> which is never multi-threaded regardless of setNbThreads(). This restores the expected threading speedup (~3.5x at 4 threads, ~3.6x at 8 threads). - BenchmarkFlattening: add one untimed warmup run of LSCM CG and HLSCM before the timed thread-count sweep to prevent cold-cache effects from artificially penalizing the 1-thread column. - AngleBasedLSCM: document the Lower|Upper requirement in the @tparam Solver doc so downstream users know to use it for threading. - HierarchicalLSCM: treat LSCG/iterative NoConvergence as non-fatal; only throw on NumericalIssue or InvalidInput. A solver that exhausts its iteration budget at an intermediate hierarchy level still produces a usable warm-start for the next finer level. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add HLSCM CG (ConjugateGradient, Lower|Upper) benchmark column alongside HLSCM LSCG to allow direct performance comparison between the two solvers inside the hierarchical algorithm. - Change --threads to accept an explicit space-separated list of thread counts (e.g. --threads 1 4 12) instead of only a single maximum value. When no --threads is given the default power-of-2 sequence up to hardware concurrency is preserved. - Wrap all solver calls in try/catch for std::bad_alloc and OpenABF::SolverException; failed cells print "N/A" in the table so the benchmark continues rather than crashing (e.g. SparseLU OOM on large meshes). - Guard output-dir OBJ writes behind nullptr checks so missing meshes (failed solvers) do not cause a crash. - Add HLSCM CG to the cache-warmup pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…erance - HierarchicalLSCM: use uint64_t in edgeKey to prevent overflow on meshes with n^2 > SIZE_MAX vertices (safe for 32-bit size_t platforms) - HierarchicalLSCM::buildHierarchy: skip stale PQ entries via isAlive() before calling tryCollapse, reducing invalid-collapse attempts - HierarchicalLSCM::solveLSCMLevel: extract duplicate x0-building loop into a shared lambda used by both the LSCG and iterative-solver branches - TestParameterization: relax EXPECT_GT(area, 0.f) to EXPECT_GE(area, -1e-5f) in Hemisphere and WavySurface tests to tolerate FP rounding on near-degenerate faces (consistent with PerformanceComparison test) - BenchmarkFlattening: remove unused LSCG type alias Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Level
- Replace std::map with std::unordered_map for freeIdxTable: reduces
per-lookup cost from O(log n) to O(1) average; the table is queried
roughly 3*3*numFaces times per hierarchy level solve
- Replace per-face std::vector<T>{sin0, sin1, sin2} with std::array<T,3>:
eliminates a heap allocation per face in the LSCM inner loop
Both includes (<array>, <unordered_map>) were already present.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The benchmark was using float throughout, which causes the LSCM CG solver to produce numerically garbage UV coordinates on large real-world meshes (condition number too high for 7-digit float precision). Changed to double via a single FloatT alias, matching VC's behavior. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The paper (Ray & Lévy, 2003) does not protect boundary vertices from collapse. Our implementation did, which limited decimation of open meshes (e.g. scroll fragments) to ~1 effective hierarchy level instead of 3-4. Now boundary vertices can be collapsed with guards for non-manifold topology (two boundary verts via interior edge) and degenerate faces. Also removes temporary diagnostic stderr logging from AngleBasedLSCM and HierarchicalLSCM. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- F1: update design.md default solver (LSCG→CG Lower|Upper), boundary handling (allow collapse with non-manifold guards), CG tolerance - F1: update plan.md decimation validity checks to match code - F1: mark all spec acceptance criteria complete - E1: mark all tasks and acceptance criteria complete, close track - E1: update plan/spec to reflect actual benchmark features (5 solver configs, --builtin/--threads/--output-dir, OOM handling) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 tasks
Address all remaining items from the 4-dimensional code review: Testing gaps (High): - Add HLSCM.MeshExceptionOnClosedMesh: verifies MeshException thrown on closed (boundary-free) mesh; also fixes AutoSelectPins to guard against empty boundary range instead of segfaulting - Add HLSCM.SingleTriangle: exercises single-level fallback path (pyramid = all-boundary, no collapses possible) - Add HLSCMInternal.DecimationMesh_RejectsPinnedVertex: directly tests that tryCollapse returns nullopt when vRemove is pinned Testing gaps (Medium/Low): - Add HLSCM.DirectSolverBranch: exercises SparseLU template parameter - Add HLSCM.FlatGridNoDistortion: zero-curvature mesh, verifies no distortion - Add HLSCM.LevelRatioBoundaryValues: setLevelRatio(0/1) and setMinCoarseVertices(0/1/2) throw std::invalid_argument - Add HLSCM.DoubleOnHemisphere: double-precision test on non-trivial mesh - Rename HLSCM.PerformanceComparison → HLSCM.LargeMeshValidation Architecture/Docs (High/Medium): - Add @throws to both static Compute() overloads (MeshException, SolverException) - Add class-level doc notes on solver default difference vs AngleBasedLSCM and single-level fallback solver behavior Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This was referenced Mar 20, 2026
…tems - P5 (#63): HLSCM hot-path allocation reduction (UV map → vector, vertexNeighbors, originalToLocal, buildLevelMesh face conversion, buildEdges_ incremental) - A8 (#64): Extract shared LSCM system-building logic from solveLSCMLevel and AngleBasedLSCM::ComputeImpl into detail::lscm utility - T5 (#65): Direct unit tests for buildHierarchy, prolongateUVs, solveLSCMLevel Also filed issues #66-#68 for low-priority items (sin/cos precompute, static Compute() levelRatio overloads, @internal Doxygen markers) without full tracks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements Hierarchical LSCM (HLSCM) from Ray & Lévy, "Hierarchical Least Squares Conformal Map" (PG 2003). Uses cascadic multigrid to accelerate LSCM convergence on large meshes: the mesh is decimated into a hierarchy of coarser levels, LSCM is solved at the coarsest level, then the solution is prolongated and refined upward using conjugate gradient warm-started from the prolongated UVs.
Core algorithm (
include/OpenABF/HierarchicalLSCM.hpp)DecimationMesh) with link-condition validation, normal-flip rejection, and minimum-angle gating (10°)minCoarseVertices(default: 100)AngleBasedLSCM: staticCompute()overloads + instance API withsetPinnedVertices(),setLevelRatio(),setMinCoarseVertices()ConjugateGradient<SparseMatrix<T>, Lower|Upper>(enables OpenMP-parallelized SpMV on the symmetric AᵀA matrix)Tests (
tests/src/TestParameterization.cpp,Utils.hpp)ConstructHemisphereandConstructWavySurfacemesh fixturesdoubleprecision, ABF++ angle preservation, ABF++ conformal distortion reduction, hemisphere/wavy validity + no-flip checks, multi-level hierarchy, instance API, large-mesh performance comparison,MeshExceptionon closed meshBenchmark example (
examples/src/BenchmarkFlattening.cpp)--threads,--output-dir,--builtinCLI options; OOM-safe with per-cell error reportingDocumentation & housekeeping
docs/citations.bib, and headerAngleBasedLSCMdoc updated withLower|UpperCG guidancesingle_include/OpenABF/OpenABF.hppregeneratedTest plan