Skip to content

BUG: Fix crashing tract_math operations - #143

Open
demianw wants to merge 1 commit into
masterfrom
fix-tract-math-operations
Open

demianw wants to merge 1 commit into
masterfrom
fix-tract-math-operations

Conversation

@demianw

@demianw demianw commented Nov 4, 2026 •

Copy link
Copy Markdown
Owner

What was wrong

Several tract_math operations crashed with the supported dependencies (NumPy 2.5.4, SciPy 1.18.1, NiBabel 5.4.2 here). I ran each operation listed in #105 on a 100-tract subset of the test .trk file and the test atlas, calling the registered operations directly (tract_querier.tract_math.operations[...]) so the real exceptions were visible.

Listed in #105, all reproduced:

operation error on master
tract_kappa UnboundLocalError: cannot access local variable 'numpy'
tract_kappa_volume UFuncTypeError (image compared with the threshold string)
tract_tract_confidence TypeError: histogram() got an unexpected keyword argument 'normed'
tract_prototype_median, tract_prototype_mean ImportError: cannot import name 'in1d' from 'numpy'
tract_prototype_mean '0' > 0 compares str with int
tract_bhattacharyya_coefficient TypeError: bins must be an integer; mn_ used where mx_ is meant
tract_flip_endpoints_in_label TypeError: takes from 3 to 4 positional arguments but 5 were given; then iterates over dict keys
tract_generate_population_probability_map returns a tuple, so no output file is written
tract_deform and instead of or: a 4-D image whose last axis is not 3 passes the check and fails later with IndexError

Found while reproducing (each reproduced the same way):

  • tract_prototype_*: after the in1d fix, AttributeError: 'numpy.ndarray' object has no attribute 'ptp' (removed in NumPy 2) in tract_obb.py.
  • tract_tract_confidence: after the normed fix, saving failed in the trk writer because the new per-point data was 1-D; it is now (n, 1) like other scalars.
  • tract_generate_population_probability_map with a single input tractography: IndexError from tractographies[1].
  • scalar_mean_std: TypeError: only 0-dimensional arrays can be converted to Python scalars (float() of a shape-(1,) array).
  • tract_bhattacharyya_coefficient: the tract file name was appended 3 times per tract, so with two or more other tracts every row after the first was labelled with the wrong file.
  • tract_map_image with a 4-D image: iterated over the first spatial axis instead of the volumes, and stored the flat (n_points, 1) array as tract data (trk writer: Missing scalars for some points!).

The change

Smallest fix per item: in1d → isin, .ptp() → ptp(), normed → density, float(threshold), int(smooth_order), mx_, integer bins, optional_flags + .items(), no trailing comma, tractographies[-1], or, .item(). The identical kappa computation of tract_kappa and tract_kappa_volume is moved to one _kappa helper (not decorator-registered, so the operations list is unchanged). tract_map_image now reuses the 3-D branch's per-tract split for each volume image_data[..., i], and the 4-D outputs keep the input's affine/image_dims like the 3-D output does.

Behaviour changes: tract_bhattacharyya_coefficient rows after the first now show the right tract file (before, they repeated earlier names) and the bounding box now uses the true maximum (mx_) of all tracts. tract_map_image on 4-D images now writes one file per volume with the values of that volume; before it could not produce correct output.

Not fixed (maintainer decision needed)

  • tract_kappa_volume still fails: tract_mask(mask, tractography) is given a NumPy array instead of an image (AttributeError: 'numpy.ndarray' object has no attribute 'affine'), and tract_mask returns an array, not the voxel set _kappa expects; its help text also omits the resolution parameter. The intended semantics (which voxel grid to compare in) are not recoverable from the code, so I left it. Hence "Part of BUG: several tract_math operations crash #105".
  • The kappa normalisation in _kappa (unchanged code from both operations): N = (all_voxels.max(0) - all_voxels.min(0)).prod() undercounts the bounding-box voxels (inclusive bounds would need + 1), so nn can go negative and kappa leaves [-1, 1] on small inputs (on the synthetic test tracts I got -1.72, 2.92 and -inf). On the 100-tract test subset the value is 0.277. Fixing it changes reported kappa values, so I left it; test_tract_kappa therefore checks only the output rows, not the value.

Testing

  • tests/test_tract_math.py: new smoke tests on a small synthetic tractography for tract_kappa, tract_tract_confidence, tract_prototype_median, tract_prototype_mean, tract_bhattacharyya_coefficient (rows labelled correctly), tract_flip_endpoints_in_label (points and data flipped), tract_generate_population_probability_map (single input, file written), tract_deform (non-deformation 4-D image rejected), scalar_mean_std, tract_map_image (synthetic 4-D image, per-tract data per volume). SciPy-dependent tests use pytest.importorskip('scipy'). All 10 fail on master and pass with this branch.
  • python -m pytest -q -p no:cacheprovider tract_querier: 52 passed, 1 skipped (pre-existing unconditional skip in test_query_rewrite.py).
  • Reproduction script re-run: all listed operations except tract_kappa_volume now run on the test data subset; tract_math a.trk tract_prototype_median out.trk and tract_math a.trk tract_kappa 2 b.trk work from the CLI.

Merge order

Needed for #123 (Tractography validating per-tract data makes the old tract_map_image 4-D branch raise). Conflicts with my upcoming PRs for #102 (length std labels in operations.py) and #107 (tract_obb.py); merge this first. #138 also appends tests to tests/test_tract_math.py (textual conflict only).

Part of #105

🤖 Generated with Claude Code

Several tract_math operations could not run with NumPy >= 1.26/2.x or
had plain bugs:

- tract_kappa, tract_kappa_volume: a local variable named `numpy`
  shadowed the module (UnboundLocalError). The identical kappa
  computation is moved to a `_kappa` helper. tract_kappa_volume also
  compared the image with the threshold string.
- tract_tract_confidence: `histogram(normed=)` was removed from NumPy;
  use `density=`. The per-point data was 1-D, which the trk writer
  rejects; make it (n, 1).
- tract_prototype_*: tract_obb imported `numpy.in1d` and called
  `ndarray.ptp()`, both removed in NumPy 2; use `isin` and `ptp()`.
- tract_prototype_mean: compared the smooth order string with 0.
- tract_bhattacharyya_coefficient: float `bins`, `mn_` used for `mx_`,
  and the tract file name was appended three times per tract, so rows
  after the first were labelled with the wrong file.
- tract_flip_endpoints_in_label: missing `optional_flags` parameter
  and iteration over dict keys instead of items.
- tract_generate_population_probability_map: trailing comma returned a
  tuple, so nothing was saved; with one input tractography it raised
  IndexError.
- tract_deform: `and` instead of `or` in the deformation field check.
- scalar_mean_std: `float()` of a shape-(1,) array fails on NumPy 2.
- tract_map_image: the 4-D branch iterated over the first spatial axis
  instead of the volumes, and stored the flat point array instead of
  per-tract data; reuse the per-tract split of the 3-D branch.

Add smoke tests for each fixed operation on a synthetic tractography.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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