Skip to content

Expose the slice step that affine_of_package already computes (#177) - #180

Merged
headmeister merged 1 commit into
isi-nmr:masterfrom
gdevenyi:fix/expose-slice-distance
Jul 27, 2026
Merged

Expose the slice step that affine_of_package already computes (#177)#180
headmeister merged 1 commit into
isi-nmr:masterfrom
gdevenyi:fix/expose-slice-distance

Conversation

@gdevenyi

Copy link
Copy Markdown
Contributor

Fixes #177.

affine_of_package builds the centre-to-centre slice step, uses it as the third column of the affine, and discards it. This exposes it:

@property
def slice_distance(self):
    """Centre-to-centre slice spacing of each slice package, in mm."""
    return [float(np.linalg.norm(self.affine_of_package(package)[:3, 2]))
            for package in range(len(self.slice_packages_index()))]

Shape and availability are as the issue proposed: one value per package, because packages can be spaced differently (spec 7.10), and available exactly where affine_of_package is — note that is affine_of_package, not affine, so a parametric map whose third axis is a frame group still reports its spacing.

SlicePackageSplitter._split_VisuCoreSlicePacksSliceDist had the same norm inline; it now asks for it.

Measured on our corpus

1631 vendor 2dseq reconstructions (PV 5.1, 6.0, 6.0.1, 7.0.0, 360.3.4–360.3.7), against np.linalg.norm(affine_of_package(i)[:3, 2]):

source disagrees with the affine
VisuCoreSlicePacksSliceDist, falling back to VisuCoreFrameThickness 197 / 1599
resolution[2] 82 / 1599

slice_distance itself is finite and positive for all 1599, and unavailable for the 27 that carry no VisuCorePosition/VisuCoreOrientation. 0 bad values.

Both alternatives fail structurally rather than marginally:

                                   parameter      affine
T1_FLASH_3D_iso                         12.0       0.125    VisuCoreFrameThickness is the slab
UTE3D                                   25.0       0.195
aswendt_gfap_pt/GV_T3_1_1_baseline/3    25.0       0.391

T2star_map_MGE/pdata/2                   0.0       1.000    resolution[2], third axis not spatial
lego_phantom/3                     28.284272       1.000    resolution[2], 20*sqrt(2) across packages

Of the 82 resolution[2] disagreements, 13 are the 0.0 parametric-map case and 61 are the cross-package diagonal.

Notes for review

  • Not added to COMPUTED_REPORT_PROPERTIES. affine is in bruker report output and this arguably belongs there too, but that changes the report schema and all four property references, so I left it as an API-only addition for you to decide on. Happy to add it.
  • Tests are synthetic (test/synthetic.py), covering the gapped stack, the 3-D slab, the per-package list, the parametric map, and the no-geometry failure.
  • Suite: 2114 passed, 12 skipped. The 5 test_affine_agrees_with_the_paravision_nifti_sform failures are the same ones master has.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NuK1cZi8U54WXAdXMmGpzy

…si-nmr#177

affine_of_package builds the centre-to-centre slice step, uses it as the third
column of the affine, and discards it. A consumer that needs the number -- a
NIfTI writer's third zoom, a DICOM SpacingBetweenSlices -- had to re-derive it,
and the two obvious parameter sources are both wrong for a large share of real
data. Over the 1599 reconstructions of our corpus that have geometry,
VisuCoreSlicePacksSliceDist falling back to VisuCoreFrameThickness disagrees
with the affine for 197 and resolution[2] for 82.

Both fail structurally, not marginally. VisuCoreFrameThickness is the slab
thickness of a 3-D acquisition, so T1_FLASH_3D_iso reports 12.0 mm where the
plane step is 0.125 mm. resolution[2] is 0.0 where the third dimension is not
spatial (13 parametric maps here) and a cross-package diagonal where the
reconstruction holds several packages -- 20*sqrt(2) between two perpendicular
packages whose spacings are 2.0 mm (61 here).

slice_distance is the length of the affine's slice column, per package, so it
inherits the affine's handling of all three cases without special-casing any of
them: finite and positive for every one of those 1599, and unavailable for the
27 with no geometry -- the same availability as affine_of_package itself.

Spacing is not thickness. They differ for a gapped acquisition, and
VisuCoreFrameThickness remains the source for the thickness; a consumer writing
BIDS needs both, separately.

SlicePackageSplitter, which had the same norm inline, now asks for it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuK1cZi8U54WXAdXMmGpzy
Copilot AI review requested due to automatic review settings July 26, 2026 18:39

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@headmeister
headmeister merged commit 8218720 into isi-nmr:master Jul 27, 2026
7 checks passed
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.

Expose the slice step that affine_of_package already computes

3 participants