Skip to content

Cancel abandoned loads, surface expansion failures, apply MIF scaling - #3

Merged
marcoduering merged 3 commits into
mainfrom
claude/expansion-errors-cancellation
Sep 27, 2026
Merged

marcoduering merged 3 commits into
mainfrom
claude/expansion-errors-cancellation

Conversation

@marcoduering

Copy link
Copy Markdown
Owner

Summary

Three fixes, one commit each. Independent of #2 (branched from main); both touch MiqParser.ParsePartial, so whichever merges second will need a small conflict resolution there.

1. First load could not be cancelled

Plugin.View called MiqParser.ParsePartial(path) without a token and checked cancellation only after the full read/decompress. Arrowing through large files left every abandoned load running to completion (CPU, I/O and memory for files no longer on screen).

  • ParsePartial(string, CancellationToken ct = default) threads the token through the full-parse fallbacks, the volume-0 reads (ReadFully) and GunzipPartial.
  • Reads are chunked at 4 MB so the token is checked during multi-GB reads, including the managed Gunzip loop.
  • Cancellation escapes the partial-load fallback catch blocks instead of triggering a full parse.
  • Plugin.View passes cts.Token and ignores OperationCanceledException after nav-away.

Remaining gap: the libdeflate path's File.ReadAllBytes of the compressed file is not interruptible (the token is checked just before it).

2. Background expansion failures were swallowed

StartExpansion caught every exception, so an out-of-memory, I/O error or changed file left the scrubber on "loading…" forever with no retry. Now only cancellation is ignored; any other failure calls MiqTriPlanarControl.ExpansionFailed. That keeps volume 0 visible, shows "first volume only (full load failed)" (new ScrubMode.Failed), and puts the exception message in the tooltip.

3. MRtrix MIF scaling: field was ignored

MifParser hard-coded SclSlope/SclInter to 0. It now maps scaling: offset,scale (value = offset + scale × stored) to SclSlope = scale, SclInter = offset. The voxel readout and Scaling metadata row are now correct, negative scales no longer invert the image, and scaled data no longer passes the identity-scaling check for segmentation colouring. Absent, identity, malformed, non-finite or zero-scale values stay unscaled; the optional field never fails the parse.

Tests

  • ParseCancellationTests (non-parallel collection, because one test lowers PartialLoadThreshold): a pre-cancelled token throws on the multi-volume .nii.gz partial path, single-volume .nii.gz full path, small .nii full path and large .nii volume-0 path, and each file still loads without a token. Also covers GunzipPartial directly.
  • MifScalingTests: scaled voxel value, Scaling metadata text, segmentation colouring disabled by scaling (with an unscaled control that does colour), and six absent/unusable variants that stay unscaled.
  • The expansion-failure UI lives in the plugin project, which has no test project, so it is not covered by automated tests.

Not yet built or run: the authoring environment had no .NET SDK (download blocked by network policy), and CI builds only the plugin, not MIQ.Tests. Please run dotnet build and dotnet test locally before merging. It is also worth confirming the offset,scale order against a real mrconvert output with non-identity scaling.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XqBwhnTRsqxk3SEvp9z4T9


Generated by Claude Code

First load not cancellable: Plugin.View ran MiqParser.ParsePartial with
no token and only checked cancellation after the full read/decompress,
so arrowing through large files left every abandoned load running.
ParsePartial now takes a CancellationToken and threads it through the
full-parse fallbacks, the volume-0 reads (ReadFully) and GunzipPartial.
Reads are chunked at 4 MB so the token is checked during multi-GB reads,
including the managed Gunzip loop. Cancellation escapes the partial-load
fallback catches instead of triggering a full parse.

Expansion failures swallowed: StartExpansion caught every exception,
leaving the scrubber on "loading..." forever after an out-of-memory,
I/O error or changed file. Only cancellation is now ignored; any other
failure calls MiqTriPlanarControl.ExpansionFailed, which shows
"first volume only (full load failed)" with the reason in a tooltip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XqBwhnTRsqxk3SEvp9z4T9
MifParser hard-coded SclSlope/SclInter to 0 and never read the
`scaling: offset,scale` field MRtrix writes for non-identity intensity
scaling (e.g. integer data converted from DICOM with Rescale
Slope/Intercept). The voxel readout showed raw stored values, the
Scaling metadata row was missing, a negative scale rendered inverted,
and scaled data passed the identity-scaling check for segmentation
colouring.

The field is now mapped to SclSlope = scale, SclInter = offset. Absent,
identity, malformed, non-finite or zero-scale values stay unscaled, so
an optional field never fails the parse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XqBwhnTRsqxk3SEvp9z4T9
Resolve MiqParser.ParsePartial conflicts: keep main's header-based
blocking (NiftiFileExtent, no gzip ISIZE) and pass this branch's
cancellation token to every fallback Parse call.

Also split ParsePartial into a one-argument overload plus
ParsePartial(string, CancellationToken): with an optional parameter the
method group no longer converted to Func<string, MiqImage>, which
NiftiFormatLabelTests relies on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XqBwhnTRsqxk3SEvp9z4T9
@marcoduering
marcoduering merged commit 3c0c162 into main Sep 27, 2026
1 check passed
@marcoduering
marcoduering deleted the claude/expansion-errors-cancellation branch September 27, 2026 12:02
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.

2 participants