Skip to content

ENH: Eigen-backed Levenberg-Marquardt; remove netlib MINPACK#6460

Open
hjmjohnson wants to merge 3 commits into
InsightSoftwareConsortium:mainfrom
hjmjohnson:eigen-nonlinear-lm
Open

ENH: Eigen-backed Levenberg-Marquardt; remove netlib MINPACK#6460
hjmjohnson wants to merge 3 commits into
InsightSoftwareConsortium:mainfrom
hjmjohnson:eigen-nonlinear-lm

Conversation

@hjmjohnson

Copy link
Copy Markdown
Member

Migrates the Levenberg-Marquardt path from the vendored netlib MINPACK to Eigen's MINPACK-port Levenberg-Marquardt (unsupported/Eigen/NonLinearOptimization), part of the ITKv7 VNL→Eigen numerics direction (#6403, #6230). itk::LevenbergMarquardtOptimizer is now Eigen-backed, vnl_levenberg_marquardt is auto-routed to the same Eigen engine with its public API unchanged, and the now-unused netlib minpack/ sources are deleted.

Both engines are MINPACK lmdif/lmder ports, so the result is numerically equivalent (residuals agree to machine precision on every converged problem) while Eigen is faster.

Note

Direct downstream callers of vnl_levenberg_marquardt are unaffected. The public header is unchanged; the only direct consumer found across the build forest — Plastimatch's RANSAC SphereParametersEstimator (SlicerRT) — compiles, links, and runs unchanged against this branch (verified locally).

Important

This is an ITK-vendored-only divergence from upstream VXL: itkvnl_algo gains a header-only Eigen3 dependency that cannot be carried to standalone VXL (which has no Eigen), and the netlib deletion cannot stand alone in VXL either. UpdateFromUpstream must re-apply the whole change on each VXL/Eigen re-sync. No change is pushed to the vxl extraction fork.

What changed (3 commits)
  1. COMP: Vendor Eigen unsupported modulesNonLinearOptimization, LevenbergMarquardt, NumericalDiff under itkeigen/unsupported/, registered in UpdateFromUpstream.sh, with install rules. Verified MPL2-clean (compiles under EIGEN_MPL2_ONLY, which CI enables).
  2. ENH: Eigen-backed L-M for itk::LevenbergMarquardtOptimizer — a narrow, Eigen-free engine interface (itkEigenLevenbergMarquardtEngine, raw-pointer residual/Jacobian callbacks; no Eigen in the header per ENH: Master tracking — Eigen3 third-party design for ITK 6.x #6230) driven through the existing MultipleValuedVnlCostFunctionAdaptor. GetOptimizer()/InternalOptimizerType retained behind itkLegacyMacro (GetOptimizer() returns nullptr). New GoogleTest (Rosenbrock ±Jacobian, linear least-squares); legacy test drives the optimizer through its own API.
  3. COMP: Re-back vnl_levenberg_marquardt on Eigen; remove netlib MINPACK — public API preserved verbatim (minimize*, MINPACK return codes, iteration/eval counts, start/end RMS, diagnose_outcome, get_JtJ via the R-factor + permutation from the Eigen solve). Deletes the 9 netlib minpack/ routines (sole consumer was vnl_levenberg_marquardt).
Performance / accuracy gate

Conditioning sweep (exponential-sum least squares; single-threaded, Release, median of N reps), Eigen vs netlib through the same optimizer:

  • Speed: Eigen faster in every case (~10–45%), equal-or-fewer iterations.
  • Accuracy: equivalent — on every converged problem the residuals agree to machine precision (e.g. at cond(J) ≈ 1.6e16: 4.460e-9 vs 4.460e-9), with identical convergence behavior. Cases where a naive benchmark suggested a difference were traced to a too-small iteration cap on a numerically-singular (cond ≈ 1e17) problem where both engines fail to converge — not an accuracy difference.

Validated locally: full ITK build (0 failures), all 24 vnl_algo tests, vnl_algo_test_levenberg_marquardt, the ITK optimizer GoogleTest + legacy test, and the Plastimatch-usage compile/link/run.

@github-actions github-actions Bot added type:Infrastructure Infrastructure/ecosystem related changes, such as CMake or buildbots type:Enhancement Improvement of existing methods or implementation area:Python wrapping Python bindings for a class type:Testing Ensure that the purpose of a class is met/the results on a wide set of test cases are correct area:ThirdParty Issues affecting the ThirdParty module area:Numerics Issues affecting the Numerics module labels Jun 17, 2026
@hjmjohnson

hjmjohnson commented Jun 17, 2026

Copy link
Copy Markdown
Member Author

Downstream validation (Plastimatch): the Eigen L-M migration + netlib-MINPACK deletion builds and runs correctly in Plastimatch (radiotherapy registration), a real consumer with a direct vnl_levenberg_marquardt call site. Its bundled RANSAC sphere-estimator unit test — which drives vnl_levenberg_marquardt via GeometricLeastSquaresEstimate — now executes and passes against this branch, and the full test suite passes 579/579.

Why Plastimatch is a meaningful target
  • libs/ransac/SphereParametersEstimator.txx calls vnl_levenberg_marquardt directly — the exact symbol this PR re-backs onto Eigen after deleting netlib MINPACK.
  • Plastimatch does not use itk::LevenbergMarquardtOptimizer itself (it wires LBFGS/LBFGSB, RegularStepGradientDescent, Amoeba, versor optimizers), so the itk:: class is not runtime-exercised here; the vnl_levenberg_marquardt path is now both compile- and runtime-validated, and the broader ITKOptimizers/registration framework is runtime-validated.
Direct vnl_levenberg_marquardt runtime coverage (now executing)

Plastimatch ships libs/ransac/Testing/SphereParametersEstimatorTest.cxx but never builds it (upstream doesn't add_subdirectory the RANSAC test dir, and nothing else instantiates the template). Registering it in ctest makes the L-M path actually run. It fits a sphere through noisy synthetic points and compares the geometric (L-M) estimate to ground truth in 2D/3D/4D:

Geometric least squares estimate [c,r]:
        [-977.61, -887.49, 652.87, 621.13, 399.56]
         Distance between estimated and known sphere centers [0=correct]: 0.54
         Difference between estimated and known sphere radius [0=correct]: 0.11
ransac-sphere-estimator .......... Passed

(First real compilation of SphereParametersEstimator.h also surfaced an unrelated modern-ITK gap — itkNewMacro(Self) now needs a trailing ; — which is a Plastimatch-side fix, not an ITK regression.)

Full test results — 579/579, 0 failed, 0 skipped

Built against this branch's ITK (USE_SYSTEM_ITK, ccache) in the itk_forest_build_testbed; ran the full Plastimatch ctest suite:

100% tests passed, 0 tests failed out of 579

No tests reported Not Run / Skipped / Disabled. The ITK-registration family ran with real timings on synthetic phantoms and matched committed baselines (-stats + -check):

Test Optimizer exercised Result
plm-reg-itk-translation versor / RegularStepGradientDescent Passed
plm-reg-itk-rigid-a versor rigid Passed
plm-reg-itk-similarity similarity Passed
plm-reg-itk-bspline LBFGS / LBFGSB Passed
plm-reg-itk-demons (+ diff/logd/sym-logd) demons Passed
ransac-sphere-estimator vnl_levenberg_marquardt Passed

@dzenanz dzenanz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good on a quick glance.

@hjmjohnson hjmjohnson marked this pull request as ready for review June 17, 2026 18:05
@greptile-apps

This comment was marked as resolved.

@hjmjohnson hjmjohnson force-pushed the eigen-nonlinear-lm branch from 02a837c to e6313ef Compare June 17, 2026 19:07
Add a curated subset of Eigen's unsupported modules to the vendored
itkeigen tree to enable an Eigen-backed Levenberg-Marquardt path for
the VNL->Eigen numerics migration (ITKv7 direction, InsightSoftwareConsortium#6403; Eigen3
third-party design, InsightSoftwareConsortium#6230):

- unsupported/Eigen/NonLinearOptimization (MINPACK lmdif/lmder port +
  HybridNonLinearSolver), the algorithm-identical counterpart to
  vnl_levenberg_marquardt.
- unsupported/Eigen/LevenbergMarquardt (newer sparse-capable
  reimplementation, DenseFunctor/SparseFunctor + QRSolver interface).
- unsupported/Eigen/NumericalDiff (shared forward/central-difference
  Jacobian helper required by both).

Register the new paths in UpdateFromUpstream.sh so the next Eigen
re-sync preserves them, and install the unsupported/Eigen header tree
alongside Eigen/ (COPYING.MINPACK was already in the vendored license
set). The two umbrella headers are mutually exclusive within a single
translation unit (both define Eigen::LevenbergMarquardt); consumers
include one per TU.
…mizer

Back itk::LevenbergMarquardtOptimizer with the MINPACK-port
Levenberg-Marquardt from Eigen's unsupported NonLinearOptimization
module (vendored under ThirdParty/Eigen3) as part of the VNL->Eigen
numerics migration (InsightSoftwareConsortium#6403, InsightSoftwareConsortium#6230). ITKv6 uses the Eigen engine
exclusively.

- itkEigenLevenbergMarquardtEngine: a narrow, Eigen-free interface
  (raw-pointer residual/Jacobian callbacks) delegating to Eigen;
  no Eigen type appears in the header. The engine ftol mirrors
  vnl_nonlinear_minimizer's fixed default so the residual stopping
  criterion matches the historical behavior.
- The optimizer drives the engine through the same
  MultipleValuedVnlCostFunctionAdaptor, so cost-function evaluation and
  parameter scaling are unchanged. Public API is unchanged;
  GetOptimizer() and InternalOptimizerType are retained behind
  itkLegacyMacro (GetOptimizer() now returns nullptr).
- A GoogleTest covers Rosenbrock (with/without analytic Jacobian) and a
  linear least-squares fit; the legacy test drives the optimizer through
  its own API.

A conditioning sweep showed the Eigen and netlib MINPACK engines are
numerically equivalent (residuals agree to machine precision on every
converged problem) while Eigen is faster.
Replace the vendored netlib MINPACK lmdif/lmder engine behind
vnl_levenberg_marquardt with Eigen's MINPACK-port Levenberg-Marquardt
(unsupported/Eigen/NonLinearOptimization), and delete the now-unused
netlib minpack sources. Both are ports of MINPACK lmdif/lmder, so the
result is numerically equivalent; vnl_algo test_levenberg_marquardt
passes unchanged.

The public vnl_levenberg_marquardt API is preserved verbatim
(minimize / minimize_using_gradient / minimize_without_gradient, the
MINPACK info return codes, num_iterations/num_evaluations, start/end RMS
error, diagnose_outcome, and get_JtJ via the R factor and permutation
repopulated from the Eigen solve). Downstream direct callers such as
Plastimatch's ransac SphereParametersEstimator compile and run unchanged.

vnl_levenberg_marquardt was the sole consumer of the netlib minpack
routines (dpmpar/enorm/fdjac2/lmder/lmder1/lmdif/lmpar/qrfac/qrsolv), so
they are removed from v3p/netlib and its build, and the includes dropped
from v3p_netlib_prototypes.h.

This is an ITK-vendored-only divergence from upstream VXL. The re-backing
requires Eigen, which standalone VXL does not have; and the netlib
deletion cannot stand alone in VXL because vnl_levenberg_marquardt there
is still netlib-backed (deletion and re-backing are coupled). Neither
half is therefore upstreamable to the vxl extraction fork, so
UpdateFromUpstream must re-apply the whole change on each VXL/Eigen
re-sync.

Refs: ITKv7 numerics direction InsightSoftwareConsortium#6403, Eigen3 third-party design InsightSoftwareConsortium#6230.
@hjmjohnson hjmjohnson force-pushed the eigen-nonlinear-lm branch from e6313ef to 0208b00 Compare June 18, 2026 00:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:Numerics Issues affecting the Numerics module area:Python wrapping Python bindings for a class area:ThirdParty Issues affecting the ThirdParty module type:Enhancement Improvement of existing methods or implementation type:Infrastructure Infrastructure/ecosystem related changes, such as CMake or buildbots type:Testing Ensure that the purpose of a class is met/the results on a wide set of test cases are correct

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants