Skip to content

COMP: Prefer the tiered itk::bridge::Math spelling for ITK's Eigen numerics - #623

Merged
hjmjohnson merged 5 commits into
BRAINSia:mainfrom
hjmjohnson:enh-tier-unsupported-math-6620
Aug 21, 2026
Merged

COMP: Prefer the tiered itk::bridge::Math spelling for ITK's Eigen numerics#623
hjmjohnson merged 5 commits into
BRAINSia:mainfrom
hjmjohnson:enh-tier-unsupported-math-6620

Conversation

@hjmjohnson

@hjmjohnson hjmjohnson commented Aug 19, 2026

Copy link
Copy Markdown
Member

Keeps BRAINSTools on ITK's Eigen-backed numerics now that
InsightSoftwareConsortium/ITK#6768 has merged, moving the convenience numerics to
itk::bridge::Math (InsightSoftwareConsortium/ITK#6620). Chained __has_include
guards keep both the post-move and pre-move ITK on the Eigen path, so this builds
against either vintage.

Affects GTRACT algo.cxx, LLSBiasCorrector.hxx, EMSegmentationFilter.hxx,
and fcsv_to_hdf5.cxx.

Why this is needed even though nothing breaks

The pre-existing guards were bare __has_include probes of the pre-move header:

#if __has_include(<itkMathLDLT.h>)
#  include <itkMathLDLT.h>
#endif

After the ITK rename that probe simply fails. There is no compile error — the
capability macro stays undefined and the code silently falls back to
vnl_matrix_inverse, with nothing in any log to say so.

Chaining the probe keeps either ITK vintage on the Eigen path:

#if __has_include(<itkBridgeMathLDLT.h>)
#  include <itkBridgeMathLDLT.h>
#elif __has_include(<itkMathLDLT.h>)
#  include <itkMathLDLT.h>
#endif

Because both arms are retained, this is safe against pre-move and post-move ITK alike.

Test plan — built and measured against merged ITK

Validated against ITK main at 313b5234d52 (i.e. after #6768 merged), installed
from a dedicated build.

1. Guard-arm probe. A syntax-only compile of the guard against the installed ITK,
with one #error per arm, reports which branch the preprocessor actually takes:
all four files' itkBridgeMathLDLT.h chain resolves to ARM=BRIDGE(eigen), not
the vnl fallback.

This matters because a successful build is consistent with all three arms — the
vnl fallback compiles fine, it just quietly abandons Eigen. The probe is what
distinguishes them.

2. Inner build. ninja over the inner project: 895/895 targets, 0 errors,
0 FAILED lines.
All four changed files are in that build:

Changed file Object
GTRACT/Common/algo.cxx GTRACTCommon.dir/algo.cxx.o
BRAINSConstellationDetector/src/fcsv_to_hdf5.cxx fcsv_to_hdf5Lib.dir/fcsv_to_hdf5.cxx.o
BRAINSABC/brainseg/EMSegmentationFilter.hxx BRAINSABCCOMMONLIB.dir/EMSegmentationFilter_float+float.cxx.o
BRAINSABC/brainseg/LLSBiasCorrector.hxx BRAINSABCCOMMONLIB.dir/BRAINSABCUtilities.cxx.o

Configured with USE_ANTS=OFF, BRAINSTools_USE_QT=OFF,
USE_BRAINSConstellationDetectorGUI=OFF, USE_DebugImageViewer=OFF,
USE_BRAINSSuperResolution=OFF — the reused EP tree carries an ITK-5.4-based ANTs
and a Qt-less VTK, and none of those tools contain the files this PR touches.

Not run: the BRAINSTools test suite. This change alters which numerical backend is
selected, not the algorithms.

@hjmjohnson
hjmjohnson force-pushed the enh-tier-unsupported-math-6620 branch from ff93ffe to b9528ae Compare August 20, 2026 21:48
@hjmjohnson hjmjohnson changed the title WIP: Prefer the tiered itk::unsupported::Math spelling (blocked on ITK#6768) WIP: Prefer the tiered itk::bridge::Math spelling (blocked on ITK#6768) Aug 20, 2026
…-inverse (gated)

Route the normal-equations left pseudo-inverse in GTRACT Matrix_Inverse
through a gated SymmetricInverse helper. When a new-enough ITK provides
itk::Math::SolveSymmetric (capability macro ITK_MATH_HAS_SOLVE_SYMMETRIC),
(M^T M)^-1 is built column-by-column from Eigen LDLT solves of (M^T M) X = I;
otherwise the legacy vnl_matrix_inverse is used. Guarded by __has_include plus
the macro, so it builds unchanged against the currently pinned ITK.

Math justification:
Matrix_Inverse(M) returns the left pseudo-inverse (M^T M)^-1 M^T used to solve
the diffusion-tensor least-squares system. The Gram matrix M^T M is symmetric
positive-definite by construction, so its inverse is symmetric and LDLT
(Bunch-Kaufman symmetric pivoting) is the appropriate factorization; solving
(M^T M) X = I column by column yields (M^T M)^-1 exactly. The caller holds the
returned pseudo-inverse matrix and reuses it per voxel, so Matrix_Inverse must
still return the matrix (form-the-inverse-via-solve), and it is a one-time
setup cost, not a per-voxel hot path.

Validation:
Cross-checked the full pseudo-inverse (M^T M)^-1 M^T against the
vnl_matrix_inverse path on random tall M at the float precision used here (8
cases): worst relative difference 6.3e-6, i.e. float machine precision.
(Absolute difference is larger and condition-number dependent because an
ill-conditioned M^T M inverted in float legitimately diverges between
algorithms; the fair metric is relative to the pseudo-inverse magnitude.) The
gated file compiles clean against the pinned ITK vnl headers via the legacy
path (syntax-checked; no in-tree BRAINSTools build was available for a full
object build).

Part of the vnl -> itk::Math de-vendoring effort; symmetric-inverse site
identified in the forest audit following the ANTs adoption.
…verses (gated)

Route two symmetric-matrix inverses through gated SymmetricInverse helpers,
adopting itk::Math::SolveSymmetric (Eigen LDLT) when a new-enough ITK provides
it (capability macro ITK_MATH_HAS_SOLVE_SYMMETRIC) and falling back to
vnl_matrix_inverse otherwise:
  - BRAINSABC LLSBiasCorrector: per-class covariance inverse (invCovars).
  - BRAINSConstellationDetector fcsv_to_hdf5: EPCA Gram inverse (Zi Zi^T).
Both guarded by __has_include plus the macro, so they build unchanged against
the currently pinned ITK. The unrelated QR-R inverse in LLSBiasCorrector (a
general triangular matrix, not symmetric) is intentionally left on
vnl_matrix_inverse.

Math justification:
A covariance matrix and a Gram matrix Zi Zi^T are symmetric positive-(semi)-
definite by construction, so their inverses are symmetric and LDLT
(Bunch-Kaufman symmetric pivoting) is the appropriate factorization; solving
A X = I column by column yields A^-1 exactly. Both sites use the full inverse
matrix downstream and are one-time setup costs (per-class / per-training), not
per-voxel hot paths.

Validation:
The SymmetricInverse-vs-vnl_matrix_inverse equivalence was cross-checked on
random SPD matrices (double: worst relative difference <= 2.6e-15; float SPD
Gram in the sibling ANTs prototypes: ~float machine precision). Compile note:
no in-tree BRAINSTools build was available, so these two files were not
object-compiled; the swap is a minimal restructure of existing vnl operations
on the legacy path, and the helper logic is validated in the shared harness
(.devlocal/tensor-solve-prototype).

Part of the vnl -> itk::Math de-vendoring effort; sites from the forest audit.
…rseSymmetric

Replace the open-coded column-by-column inverse loops in the gated
SymmetricInverse helpers (GTRACT algo.cxx, fcsv_to_hdf5.cxx, LLSBiasCorrector.hxx)
with a single itk::Math::InverseSymmetric(A) call. The prior loop re-factorized
A once per column (O(n^4)); InverseSymmetric factorizes once (O(n^3)). Behavior
and the #ifdef ITK_MATH_HAS_SOLVE_SYMMETRIC gate are unchanged; the legacy
vnl_matrix_inverse fallback is retained and still compiles against the pinned
ITK (GTRACT algo.cxx syntax-checked). Requires an ITK carrying
itk::Math::InverseSymmetric on the new path.
…nce inverse (gated)

Route the per-class covariance inverse in EMSegmentationFilter (used to form the
Gaussian invcov for Mahalanobis posteriors) through a gated em_detail::
SymmetricInverse helper: itk::Math::InverseSymmetric (Eigen LDLT) when available,
vnl_matrix_inverse otherwise. The covariance is symmetric positive-(semi)definite,
so LDLT is the appropriate factorization. Computed once per class before the
per-voxel posterior loop (setup, not hot). Gated by __has_include plus
ITK_MATH_HAS_SOLVE_SYMMETRIC; builds unchanged against the pinned ITK. This site
was found alongside the LLSBiasCorrector covariance during the audit follow-up.
ITK moved the Eigen-backed convenience numerics into itk::bridge and
renamed the headers to itkBridge*.h. Probe the tiered header first so a
post-move ITK stays on the Eigen path, and keep the pre-move arm so older
ITK checkouts still resolve; the vnl fallback is unchanged.

The itk::bridge tier is explicitly a migration aid rather than supported
ITK API. Every call site here is guarded by
ITK_BRIDGE_MATH_HAS_SOLVE_SYMMETRIC and falls back to vnl, so BRAINSTools
does not hard-depend on that tier.
@hjmjohnson
hjmjohnson force-pushed the enh-tier-unsupported-math-6620 branch from b9528ae to 8bd1332 Compare August 21, 2026 22:45
@hjmjohnson
hjmjohnson marked this pull request as ready for review August 21, 2026 22:50

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your trial has ended. Reactivate Greptile to resume code reviews.

@hjmjohnson hjmjohnson changed the title WIP: Prefer the tiered itk::bridge::Math spelling (blocked on ITK#6768) ENH: Prefer the tiered itk::bridge::Math spelling (blocked on ITK#6768) Aug 21, 2026
@hjmjohnson hjmjohnson changed the title ENH: Prefer the tiered itk::bridge::Math spelling (blocked on ITK#6768) ENH: Prefer the tiered itk::bridge::Math spelling Aug 21, 2026
@hjmjohnson
hjmjohnson merged commit f34f9f7 into BRAINSia:main Aug 21, 2026
3 checks passed
@hjmjohnson hjmjohnson changed the title ENH: Prefer the tiered itk::bridge::Math spelling COMP: Prefer the tiered itk::bridge::Math spelling for ITK's Eigen numerics Aug 22, 2026
@hjmjohnson

Copy link
Copy Markdown
Member Author

Correction to this PR's test plan, posted after merge.

The "895/895 targets against ITK main @313b5234d52" result in the body is not
supported
. That build's ITK_DIR did point at an ITK 6 install, but the
SlicerExecutionModel in the reused ExternalProject tree had itself been built
against ITK 5.4 and propagates that ITK's include directories through
INTERFACE_INCLUDE_DIRECTORIES. Every translation unit therefore compiled against
.../BRAINSTools-Release-5.8.0/include/ITK-5.4, confirmed from the build's own
compile_commands.json (271/271 entries). I retract the claim.

It went unnoticed because both ITK vintages compile this code cleanly: against
ITK 5.4 the chained __has_include guard simply takes the #elif arm. A green
build was the expected outcome either way, which is precisely why the guard-arm
probe exists — I applied that reasoning to the probe and failed to apply it to the
build.

What still stands:

  • The guard-arm probe. Compiled directly against the ITK 6 install with
    -isystem <itk-include> and one #error per arm, no build system involved. The
    itkBridgeMathLDLT.h chain resolves to the bridge/Eigen arm, not the vnl
    fallback. This is the claim the PR actually rests on.
  • The merged ITK provides what the guards probe for: itkBridgeMathLDLT.h
    defining ITK_BRIDGE_MATH_HAS_SOLVE_SYMMETRIC.
  • The code is unchanged and correct — the retraction is about evidence, not
    about the diff.

A full BRAINSTools SuperBuild pinned to an ITK 6 main install (with
ITK_LEGACY_REMOVE and ITK_FUTURE_LEGACY_REMOVE both ON) is running now to
produce the build evidence this PR should have had. I will report the result here.

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