ENH: Add Eigen-backed SparseLUSolverTraits and deprecate VNLSparseLUSolverTraits#6458
ENH: Add Eigen-backed SparseLUSolverTraits and deprecate VNLSparseLUSolverTraits#6458hjmjohnson wants to merge 8 commits into
Conversation
…verage Eigen::SparseLU-backed sibling of VNLSparseLUSolverTraits, exposing the same static solver-traits API used by the QuadEdgeMesh filters. The GTest mirrors VNLSparseLUSolverTraitsGTest plus the portable cases from vxl core/vnl/algo/tests/test_sparse_lu.cxx (Kundert mat0 solve, determinant, transpose solve; permutation matrix; Poisson birth-death queue). vnl_sparse_lu::rcond() and max_error_bound() have no Eigen::SparseLU equivalent and are not ported.
The \return text described the iterative solver while the method returns true. Also remove a stray vim artifact from the \sa line.
Cross-checks VNLSparseLUSolverTraits and EigenSparseLUSolverTraits on identical 2-D Laplacian systems: the two direct solvers must agree on the solution (relative to its scale, since an ill-conditioned A makes the solution large for an arbitrary RHS). Agreement is 1e-9 relative on well-conditioned systems and 1e-6 on an ill-conditioned one. This is the numerical-equivalence gate that must hold before the Eigen::SparseLU algorithm may be placed behind the vnl_sparse_lu API.
VNLSparseLUSolverTraits and EigenSparseLUSolverTraits both gain a MatVecMult static (vnl_sparse_matrix::mult / Eigen operator*), and LaplacianDeformationQuadEdgeMeshFilterWithSoftConstraints performs its M^T B products through SolverTraits::MatVecMult instead of the vnl-specific MatrixType::mult, so the filter is backend-neutral across both solver traits.
The Laplacian-deformation and parameterization filter tests, and the filters' doc, now select the Eigen-backed EigenSparseLUSolverTraits as the sparse direct LU solver. VNLSparseLUSolverTraits remains available; the iterative VNLIterativeSparseSolverTraits path is unchanged.
Now that EigenSparseLUSolverTraits is proven faster and more accurate (both axes) and the QuadEdgeMesh consumers use it, gate the VNL spelling: silent by default, a deprecation warning under ITK_LEGACY_REMOVE, and a hard error under ITK_FUTURE_LEGACY_REMOVE, pointing at the Eigen replacement. The __has_include(<itkConfigure.h>) form keeps the guard inert for non-ITK consumers of the header. The two GTests that exercise the deprecated traits define ITK_LEGACY_TEST to opt out.
|
ITK.Linux failed at the Azure dashboard step (exit 255) while CDash for this commit shows 0 compile errors, 0 test failures, 4608 tests pass — the known "dashboard treats a compiler warning as fatal" flake. The single warning in the |
|
/azp run ITK.Linux |
|
| Filename | Overview |
|---|---|
| Modules/Core/Common/include/EigenSparseLUSolverTraits.h | New Eigen::SparseLU-backed traits class; API mirrors VNLSparseLUSolverTraits. All three matrix-taking Solve overloads make an unnecessary copy of the input matrix before constructing SparseLU (which copies again internally), doubling allocation cost. |
| Modules/Core/Common/include/VNLSparseLUSolverTraits.h | Adds three-state deprecation guard (silent/warn/error), corrects the IsDirectSolver() doc comment, and adds MatVecMult. Deprecation logic is clean and correct. |
| Modules/Core/Common/test/EigenSparseLUSolverTraitsGTest.cxx | New GTest file with ConvertedLegacyTest wrapper plus three dedicated test cases ported from vxl; MatVecMult is not directly unit-tested. |
| Modules/Core/Common/test/VnlEigenSparseLUEquivalenceGTest.cxx | New equivalence test verifying VNL and Eigen solutions agree to 1e-9 (well-conditioned) and 1e-6 (ill-conditioned) relative tolerance across grid sizes; solid coverage. |
| Modules/Filtering/QuadEdgeMeshFiltering/include/itkLaplacianDeformationQuadEdgeMeshFilterWithSoftConstraints.hxx | Replaces three direct VNL-specific Mt.mult() calls with SolverTraits::MatVecMult(), making the filter backend-neutral. Change is minimal and correct. |
| Modules/Core/Common/test/VNLSparseLUSolverTraitsGTest.cxx | Adds #define ITK_LEGACY_TEST before the VNL include to opt out of the new deprecation warning; one-line change, correct. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Filter as QuadEdgeMeshFilter
participant Traits as EigenSparseLUSolverTraits
participant Eigen as Eigen::SparseLU
Filter->>Traits: InitializeSparseMatrix(N, N)
Filter->>Traits: FillMatrix / AddToMatrix (builds M)
Filter->>Traits: MatVecMult(Mt, Bx, Cx)
Traits->>Eigen: "oX = iA * iB (dense result)"
Filter->>Traits: Solve(A, Cx, Cy, Cz, X, Y, Z)
Traits->>Eigen: SparseLU(A) — copies + compresses + factorizes
Eigen-->>Traits: factorization done, info()
Traits->>Eigen: solver.solve(Cx) → X
Traits->>Eigen: solver.solve(Cy) → Y
Traits->>Eigen: solver.solve(Cz) → Z
Eigen-->>Traits: back-substitution results
Traits-->>Filter: "(X, Y, Z), success = info()==Success"
Filter->>Filter: update mesh point positions
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Filter as QuadEdgeMeshFilter
participant Traits as EigenSparseLUSolverTraits
participant Eigen as Eigen::SparseLU
Filter->>Traits: InitializeSparseMatrix(N, N)
Filter->>Traits: FillMatrix / AddToMatrix (builds M)
Filter->>Traits: MatVecMult(Mt, Bx, Cx)
Traits->>Eigen: "oX = iA * iB (dense result)"
Filter->>Traits: Solve(A, Cx, Cy, Cz, X, Y, Z)
Traits->>Eigen: SparseLU(A) — copies + compresses + factorizes
Eigen-->>Traits: factorization done, info()
Traits->>Eigen: solver.solve(Cx) → X
Traits->>Eigen: solver.solve(Cy) → Y
Traits->>Eigen: solver.solve(Cz) → Z
Eigen-->>Traits: back-substitution results
Traits-->>Filter: "(X, Y, Z), success = info()==Success"
Filter->>Filter: update mesh point positions
Comments Outside Diff (1)
-
Modules/Core/Common/test/EigenSparseLUSolverTraitsGTest.cxx, line 474-475 (link)MatVecMulthas no direct unit test in either traits GTest fileMatVecMultis newly introduced in this PR for bothEigenSparseLUSolverTraitsandVNLSparseLUSolverTraits, yet neither GTest file contains a test that directly exercises it. It is only covered indirectly throughitkLaplacianDeformationQuadEdgeMeshFilterWithSoftConstraintsTest. A standalone GTest case (e.g., verifyoX == A * bcomponent-wise for a small known matrix) would give isolated failure diagnostics if the implementation ever regresses.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Reviews (1): Last reviewed commit: "ENH: Deprecate VNLSparseLUSolverTraits w..." | Re-trigger Greptile
|
Are there other external uses of VNL's LU solver? I am not sure it is worth us to add an ITK specific numeric interfaces. It seem like it is more to maintain, and may make changes harder in the future. Maybe we just update the QuadEdge to directly use Eigen? |
Per review feedback (InsightSoftwareConsortium#6458): give the new traits a backend-neutral name. The class is now SparseLUSolverTraits; that it is implemented with Eigen::SparseLU is documented in the class Doxygen rather than baked into the public name, so the engine can change later without renaming the public type. The VNLSparseLUSolverTraits deprecation guard and the QuadEdge mesh filter docs/tests now point at SparseLUSolverTraits. No functional change; the header was renamed (EigenSparseLUSolverTraits.h -> SparseLUSolverTraits.h) along with its GoogleTest. The sparse-LU traits GoogleTests and all QuadEdgeMeshFiltering tests pass.
|
Good questions. Other external uses of the VNL LU solver: across the downstream testbed (ITK + ANTs, BRAINSTools, Slicer, elastix, SimpleITK, and ~257 Slicer remote modules) I found one inactive consumer of On adding an ITK-specific numeric interface: Changes pushed (cf5b842):
|
The matrix-taking SparseLUSolverTraits::Solve overloads copied iA and called makeCompressed() before handing it to the Eigen::SparseLU constructor. SparseLU::analyzePattern() already copies the input (m_mat = mat) and handles the uncompressed case internally, so the manual copy/compress materialized a second full copy of the sparse matrix on every call. Pass iA directly. Addresses a greptile P2 finding on InsightSoftwareConsortium#6458. The sparse-LU traits and QuadEdgeMeshFiltering tests pass unchanged.
Adds an Eigen-backed
EigenSparseLUSolverTraits(Eigen::SparseLU) alongside the existingVNLSparseLUSolverTraits, migrates the QuadEdgeMesh consumers to it, and deprecates the VNL traits behind ITK's three-state legacy policy. Part of the VNL→Eigen numerics direction (#6403, #6230). No VNL code is removed — the VNL path stays fully functional through the deprecation window.Eigen is proven faster and more accurate on both axes (see report), and the two engines are proven numerically equivalent by a new GoogleTest, so the migration is safe.
Commits (additive; nothing deleted/renamed)
ENH: Add EigenSparseLUSolverTraits prototype with vxl-derived test coveragetest_sparse_luDOC: Correct IsDirectSolver() Doxygen in VNLSparseLUSolverTraitsENH: Add VNL vs Eigen sparse-LU equivalence GoogleTestENH: Add MatVecMult to sparse-LU traits and route filter through itMatVecMult; the Soft-constraints filter routes M^T·B through the traits so it is backend-neutralENH: Migrate QuadEdgeMesh consumers to EigenSparseLUSolverTraitsENH: Deprecate VNLSparseLUSolverTraits with a three-state guardITK_LEGACY_REMOVEwarning /ITK_FUTURE_LEGACY_REMOVEerror, pointing at the Eigen replacementPerformance proof — Eigen passes the gate on BOTH axes
2-D Laplacian systems (representative of the QuadEdgeMesh parameterization matrices). Single-threaded, median of 13, Apple M3 Max / clang 19.1.7. Lower is better in every column;
fwd= forward error,resid= relative residual.Eigen wins speed in 5/5 (margin grows with N) and accuracy in 5/5 (forward and residual). Numerical equivalence VNL↔Eigen holds to 1e-9 relative (well-conditioned) / 1e-6 (ill-conditioned), covered by
VnlEigenSparseLUEquivalenceGTest.Ecosystem usage scan — who actually uses this solver
Searched all source in a 32-repo downstream forest (ITK + ANTs, BRAINSTools, Slicer, elastix, MITK, SimpleITK, RTK, PlusLib, IGSIO, OpenIGTLink, vtkAddon, SCIFIO, CudaCommon, WebAssemblyInterface, TubeTK, c3d, … and the ITK remote modules):
vnl_sparse_lu/VNLSparseLUSolverTraitsitkThinPlateSplineTransformPerformanceTest.cxx, and all 4 hits are commented-out dead code (// #include, a never-builtinvert()experiment)rcond/max_error_bound/solve_transpose/estimate_condition)test_sparse_lu.cxxSo this sparse-LU solver is, in practice, consumed only by ITK's QuadEdgeMesh path (now on Eigen) and the algorithm's own unit test — the deprecation strands no external caller.
Test status
Full local build (4251 targets, 0 errors) and the complete ITK test suite 4428/4428 passing on macOS/AppleClang via the pinned conda toolchain. The 32 LU + QuadEdgeMesh tests (restored
vnl_algo_test_sparse_lu, both traits' GTests, the equivalence test, 24 QuadEdgeMesh consumers) are green; the deprecation guard's three states (silent/warning/error) were verified by a preprocessor probe.