Repository navigation
Fit nlinv maps on a low-resolution grid, with optional phase-pole correction - #150
Merged
Merged
Conversation
One set of nlinv maps can share a phase vortex. The maps are smooth, so at the vortex every coil's sensitivity is driven towards zero, and any coil combination over them, the adjoint or a pics solve, shows a dark spot there. On a 32-channel 7 T phantom slice the adjoint combination over the maps fell to 0.105 of the root sum of squares inside the object. BART's `nlinv --phase-pole` detects the shared poles and moves them into the image; after every Gauss-Newton step it lifts that minimum to 0.938, after the fifth it leaves it at 0.105. nlinv_maps and nlinv_pics now take `phase_pole` and forward it on the Cartesian and the trajectory path. It stays off by default, as in BART: it doubles the time of the fit on the CPU (14 s to 29 s on that slice) and changes nothing where the maps share no pole. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Asked for phase-pole correction, nlinv on a volume with an axis no longer than its detection circle, a twentieth of the longest axis rounded up, fails on an assertion in BART's pole.c (compute_curl_map_normal): a 3D slab of four partitions on a 64-point plane is refused, one of five is not. nlinv_maps documents the limit and the BartError it raises, and a test pins the edge on a 24-point plane (two partitions refused, three accepted), so that a caller asking for the correction knows which volumes it can ask it for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nlinv_maps zero-filled the central `size` lines into the full matrix and ran nlinv there, so the fit and its phase-pole detection worked at full resolution: 14 s for one 220x220, 32-coil slice (29 s with the detection), and the detection circle, a twentieth of the longest axis of that grid, refused any slab of up to 4 partitions on a 64-point plane. The central lines are now cropped and fitted on their own grid, and the maps are zero-filled in k-space back to the grid of the data. BART's Sobolev weighting is relative to the grid, so `a` is scaled by the square of the crop ratio along the shortest cropped axis; with BART's own `a` the maps came out smoother than before (0.38 from parallel at the worst voxel of a 64-point phantom), and with the scaled one they agree with the full-grid maps to 1.4e-2 at worst and 1e-3 at the median. On the 7 T slice the fit takes 0.14 s (0.36 s with the detection), and on a 24-line grid the detection needs 3 partitions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nlinv_maps zeroed the samples beyond `radius` but kept their trajectory, so `nlinv -t` sized its grid from the whole trajectory and fitted at full resolution, and took the zeroed samples for measurements of zero, which pulls the maps towards a band-limited product. It was slow (7.6 s for a 256-point radial plane of 8 coils, 97 s for a 96-cube kooshball) and in three dimensions far from the coils (0.53 from parallel at the worst voxel of that cube). The samples within `radius` are now kept alone and fitted on a grid of 2 ceil(radius) points an axis, with the Sobolev `a` scaled as the Cartesian path scales it, and the maps zero-filled in k-space to the image grid: 0.23 s and 2.2 s, and 0.93 at the worst voxel of the cube; in a plane the maps agree with the whole-grid fit to 1e-2 at worst and 3e-4 at the median. A three-dimensional trajectory now works, with `shape` naming the image grid where BART's estimate from the trajectory is not the one wanted. The trajectory test of nlinv_pics runs on one thread: 32 spokes on a 16-point grid leave the fit loose enough that the order of a threaded sum moved it past the tolerance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntime prints
The thin-volume test matched the refusal by the function name in BART's
assertion message, which glibc prints and the Windows C runtime does not
("Assertion '0 < odims[i]' failed in .../noir/pole.c:123"), so it failed on
both Windows jobs. It now matches the asserted condition, which both print.
Co-Authored-By: Claude Opus 5.5 <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.
What failed
nlinvmaps (apps.nlinv_maps,nlinv -m 1) can share a phase vortex. The maps are smooth, so every coil's sensitivity is driven to zero there, and any combination over them goes dark: the adjoint, or apicssolve.sizelines into the full matrix, sonlinvand the pole detection ran at full resolution: 14 s for a 220x220 slice with 32 coils. BART's pole circle is a twentieth of the grid's longest axis, rounded up, so on that grid it refused any slab of up to 4 partitions on a 64-point plane.radiusbut kept their trajectory.nlinv -ttherefore sized its grid from the whole trajectory, ran at full resolution, and took the zeroed samples for measured zeros, which pulls the maps away from the coils. Three-dimensional trajectories were not supported at all: the reshape assumed a plane.Change
apps.nlinv_maps(..., phase_pole=None):Nonekeeps BART's behaviour; an intdisnlinv --phase-pole d, detection after Gauss-Newton stepd, with 0 meaning every step.apps.nlinv_picsforwards it.min(size, n)lines of every axis are cropped (centre atn // 2), andnlinvruns on that grid.radiusare kept, andnlinv -truns on an explicit grid of2 ceil(radius)points per axis (-x, which BART honours).shapeargument: the image grid of the maps,(y, x)or(z, y, x). By default it is the grid BART estimates from the whole trajectory, as before.(1 + a |k/n|^2)^(-b/2)is relative to the grid, soais scaled by(m/n)^2along the shortest cropped axis (mpoints kept of the axis'sn).a, small-grid Cartesian maps came out much smoother than before: 0.38 from parallel to the full-grid maps at the worst voxel of a 64-point phantom.size=24, that means a slab of fewer than 3 partitions. This is documented, underRaisestoo.nlinv_picstrajectory test now runs on one thread. Its 32 spokes on a 16-point grid fit loosely enough that the order of a threaded sum moved the maps by 2e-2. It failed once at 4.4e-3 against a 1e-3 tolerance, and is bit-exact on one thread.Measurements (CPU)
Old means the full-grid fit; new means the small-grid fit.
|<old,new>|is the per-voxel|sum_c conj(old_c) new_c|(1 = parallel). "vs. true coils" is the worst voxel of the same quantity against the sensitivities that made the data.Cartesian
|<old,new>|min / medianphase_pole=0Non-Cartesian (synthetic,
radius=12)|<old,new>|min / medianphase_pole=0phase_pole=0In three dimensions the two fits disagree because the old one is the worse of the two: it fitted the zeroed samples as data.
Phase-pole detection on the Cartesian path
phase_pole=Nonephase_pole=0The default stays off, which keeps BART's behaviour, avoids the cost where the maps share no pole, and avoids the refusal on thin grids. Callers whose data show the spots ask for it; pulserver/pulserver#282 does, and needs this in a release.
Tests
These ran against the v0.0.15 library (no C changed):
tests/test_apps.py,tests/test_docstrings.py(with numpydoc),tests/test_docs.py,tests/test_namespace.py,tests/test_options.py,tests/test_tools.py: 293 passed, 3 skipped (2 need CUDA, 1 in test_tools). Thenlinvtests passed 3 runs in a row.scripts/lint.sh(ruff, format, codespell): clean.I did not run the docs build.
🤖 Generated with Claude Code