COMP: Use itk::Math::MatrixExponential instead of removed vnl_matrix_exp#1452
COMP: Use itk::Math::MatrixExponential instead of removed vnl_matrix_exp#1452hjmjohnson wants to merge 1 commit into
Conversation
2e19386 to
c33e80a
Compare
|
Performance impact of migrating AffineLogTransform from Per-call timing + net AffineLogTransform impact
Per
The d² Numerical equivalence + accuracyFor near-identity log-domain matrices the absolute output difference between the two methods is ~1e-12 — far below any registration tolerance, so registration results are unchanged. Eigen (Higham scaling-and-squaring) is also ~2–3 orders of magnitude more accurate than vnl's Taylor series on the This PR depends on |
|
Thanks Hans. Would this compile for both ITK 5 and 6, when we add something like |
c33e80a to
5aeef7f
Compare
|
@N-Dekker Good catch — yes, and a guard is necessary: elastix's minimum is ITK 5.4.1 ( Why __has_include rather than #if ITK_VERSION_MAJOR >= 6
#ifndef ELX_HAS_ITK_MATRIX_EXPONENTIAL
# if __has_include("itkMatrixExponential.h")
# define ELX_HAS_ITK_MATRIX_EXPONENTIAL 1
# else
# define ELX_HAS_ITK_MATRIX_EXPONENTIAL 0
# endif
#endifVerified both branches compile locally: the Eigen path builds the full elastix/transformix against ITK main; the vnl fallback (forced via |
N-Dekker
left a comment
There was a problem hiding this comment.
Approved unconditionally, but feel free to comment on my question about the initial #ifndef ELX_HAS_ITK_MATRIX_EXPONENTIAL 😃
|
@hjmjohnson Maybe we should still check if it won't break our regression tests, as it might yield slightly different result, right? But for now, I guess we have no choice, because without this PR, elastix + ITK6 won't build anymore 🤷 |
@hjmjohnson This sounds very promising. Can you possibly share the benchmark code? |
|
Verified this change against an ITK-v6- 3-way test comparison (163 tests each)
The same two tests fail in all three configurations (both pre-existing, ITK-version-independent):
Runtimes were ~23.4–23.6 s in all three runs. What this establishes
|
VXL removed core/vnl/vnl_matrix_exp.h, breaking AffineLogTransform's include of <vnl/vnl_matrix_exp.h> (InsightSoftwareConsortium/ITK#6452). Switch to itk::Math::MatrixExponential (itkMatrixExponential.h), an ITK-supported Eigen-backed replacement using Higham scaling-and-squaring. elastix's minimum is ITK 5.4.1, which lacks itkMatrixExponential.h, so guard the include and both call sites on __has_include and fall back to vnl_matrix_exp for older ITK. ITK#6454 adds the function and restores the vnl_matrix_exp shim. Call-site argument types (vnl_matrix_fixed, vnl_matrix) are unchanged.
5aeef7f to
df58ef4
Compare
Happy to, @N-Dekker. The full performance and accuracy write-up (with the per-size timing table and the The code is below. Two files: a consolidated timing harness (vnl vs Eigen across the sizes One honesty note on reading the numbers: the published table's small (2×2/3×3 log-domain) rows used fixed-size types ( Timing harness — vnl_matrix_exp vs itk::Math::MatrixExponential (best-of-11)// Build: c++ -O3 -DNDEBUG -std=c++17 <ITK + VNL include/link flags> bench_matrix_exp.cxx
#include "itkMatrixExponential.h"
#include <vnl/vnl_matrix.h>
#include <vnl/vnl_matrix_exp.h>
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <random>
#include <vector>
using clk = std::chrono::steady_clock;
template <typename F>
double best_of(F && f, int reps = 11)
{
double best = 1e300;
for (int r = 0; r < reps; ++r)
best = std::min(best, f());
return best;
}
template <unsigned D>
void run(const char * label)
{
std::mt19937 rng(1);
std::uniform_real_distribution<double> ud(-0.3, 0.3); // small-norm, like log-domain inputs
const int pool = 256;
std::vector<vnl_matrix<double>> in(pool, vnl_matrix<double>(D, D));
for (auto & m : in)
for (unsigned i = 0; i < D; ++i)
for (unsigned j = 0; j < D; ++j)
m(i, j) = ud(rng);
const int N = 200000;
volatile double sink = 0;
auto timeVnl = [&] {
auto t0 = clk::now();
for (int k = 0; k < N; ++k)
sink += vnl_matrix_exp(in[k & (pool - 1)])(0, 0);
auto t1 = clk::now();
return std::chrono::duration<double, std::nano>(t1 - t0).count() / N;
};
auto timeItk = [&] {
auto t0 = clk::now();
for (int k = 0; k < N; ++k)
sink += itk::Math::MatrixExponential(in[k & (pool - 1)])(0, 0);
auto t1 = clk::now();
return std::chrono::duration<double, std::nano>(t1 - t0).count() / N;
};
const double vnl = best_of(timeVnl);
const double itk = best_of(timeItk);
std::printf("%-8s vnl_matrix_exp=%8.1f ns itk(Eigen)=%8.1f ns speedup=%.2fx\n",
label, vnl, itk, vnl / itk);
(void)sink;
}
int main()
{
std::printf("best-of-11 min ns/call, small-norm inputs\n");
run<2>("2x2"); // log-domain (2D)
run<3>("3x3"); // log-domain (3D)
run<4>("4x4"); // augmented A_bar (2D Jacobian loop)
run<6>("6x6"); // augmented A_bar (3D Jacobian loop)
return 0;
}Representative run (Apple Silicon, dynamic Correctness / equivalence check — exact AffineLogTransform call types#include "itkMatrixExponential.h"
#include <vnl/vnl_matrix_exp.h>
#include <iostream>
#include <iomanip>
int main()
{
using itk::Math::MatrixExponential;
int fails = 0;
// itk::Matrix 3x3 vs restored vnl_matrix_exp on the same data
itk::Matrix<double, 3, 3> A;
A(0,0)=4;A(0,1)=1;A(0,2)=-2; A(1,0)=3;A(1,1)=-5;A(1,2)=1; A(2,0)=0;A(2,1)=2;A(2,2)=6;
auto itkExp = MatrixExponential(A);
vnl_matrix<double> vexp = vnl_matrix_exp(A.GetVnlMatrix().as_matrix());
double err = 0;
for (unsigned i=0;i<3;++i) for (unsigned j=0;j<3;++j) err = std::max(err, std::abs(itkExp(i,j)-vexp(i,j)));
std::cout << "itk::Matrix vs vnl_matrix_exp max_err=" << std::scientific << err << (err<1e-7?" PASS":" FAIL") << "\n";
if (err>=1e-7) ++fails;
// vnl_matrix_fixed overload (exactly elastix's GetVnlMatrix() type)
vnl_matrix_fixed<double, 2, 2> G; G(0,0)=0;G(0,1)=-0.9;G(1,0)=0.9;G(1,1)=0;
auto gf = MatrixExponential(G);
double c = std::cos(0.9), s = std::sin(0.9);
double e2 = std::max(std::max(std::abs(gf(0,0)-c), std::abs(gf(0,1)+s)), std::max(std::abs(gf(1,0)-s), std::abs(gf(1,1)-c)));
std::cout << "vnl_matrix_fixed rotation max_err=" << e2 << (e2<1e-9?" PASS":" FAIL") << "\n";
if (e2>=1e-9) ++fails;
// dynamic vnl_matrix overload (exactly elastix's A_bar type)
vnl_matrix<double> N(2,2); N(0,0)=0;N(0,1)=1;N(1,0)=0;N(1,1)=0;
auto nf = MatrixExponential(N);
double e3 = std::max(std::max(std::abs(nf(0,0)-1), std::abs(nf(0,1)-1)), std::max(std::abs(nf(1,0)), std::abs(nf(1,1)-1)));
std::cout << "vnl_matrix nilpotent max_err=" << e3 << (e3<1e-12?" PASS":" FAIL") << "\n";
if (e3>=1e-12) ++fails;
std::cout << (fails ? "\nRESULT: FAIL\n" : "\nRESULT: ALL PASS\n");
return fails;
} |
|
@N-Dekker I do not have merge rights on elastix. Take this to any terminal state that you wish. NOTE: if ITK_FUTURE_LEGACY_REMOVE is used, <vnl/vnl_matrix_exp.h>. The intent is that <vnl/vnl_matrix_exp.h> is removed in ITKv7. |
|
Thanks @hjmjohnson Before merging, I would like to see if it affects the results of our regression test, at the CI. Ijust started elastix builds with both ITK-v6.0b02 and ITK-v6-main. Afterwards, I'll try cherry-picking your PR on the second branch.
Update - cherry-picking your PR: |
VXL removed
core/vnl/vnl_matrix_exp, whichAffineLogTransformincluded via<vnl/vnl_matrix_exp.h>(InsightSoftwareConsortium/ITK#6452). This migrates the two call sites toitk::Math::MatrixExponential, an ITK-supported Eigen-backed replacement.Change
Call-site argument types (
vnl_matrix_fixed,vnl_matrix) are unchanged; the new function provides matching overloads. BothAffineLogTransformandAffineLogStackTransformcomponents rebuild clean against the new header.