Conversation
There was a problem hiding this comment.
⚠️ Not ready to approve
The new sensitivity script’s reference-detection/labeling logic can misidentify the reference configuration (and the parity test volume size risks slowing CI), so the output correctness/perf should be fixed before approval.
Pull request overview
This PR adds an end-to-end parity test to ensure spectre.io.load_and_window matches the MONAI IO+preprocessing pipeline, introduces an evaluation script to quantify embedding sensitivity to preprocessing choices, and fixes a regression where non-RAS reorientation could produce negative-stride arrays that torch.from_numpy rejects.
Changes:
- Add
tests/test_io_pipeline_parity.pyto validate bit-exact parity vs a MONAI reference pipeline across on-disk orientations and optional resampling, and to pin the intentional short-axis padding divergence. - Fix
load_ct()to make reoriented arrays contiguous before converting to a Torch tensor (avoids negative-stride failures). - Add
eval/preprocessing_sensitivity.pyto sweep voxel spacings / HU windows and report embedding drift (CSV + optional plot).
File summaries
| File | Description |
|---|---|
| tests/test_io_pipeline_parity.py | New end-to-end IO+orientation+resampling parity coverage against the MONAI reference pipeline. |
| src/spectre/io.py | Make load_ct() robust to non-RAS reorientation by ensuring contiguous NumPy arrays before torch.from_numpy. |
| eval/preprocessing_sensitivity.py | New analysis script to measure and visualize embedding sensitivity to preprocessing knobs. |
Review details
Comments suppressed due to low confidence (2)
eval/preprocessing_sensitivity.py:295
loudest()excludes the reference by filtering on zero distance; that can drop the wrong config(s) (or keep the reference) for the same reasons as above. Exclude by the explicit reference label for the axis instead.
def loudest(axis):
cands = [s for s in summary if s["axis"] == axis and not (s["cos_mean"] == 0.0 and s["cos_std"] == 0.0)]
return max(cands, key=lambda s: s["cos_mean"]) if cands else None
eval/preprocessing_sensitivity.py:331
- The plot greys the reference bar using a zero-distance check, which can grey additional bars (or fail to grey the reference) if embeddings are identical or slightly nondeterministic. Grey by matching the reference config label for the axis instead.
entries = [s for s in summary if s["axis"] == axis]
labels = [s["config"] for s in entries]
means = [s[mean_k] for s in entries]
stds = [s[std_k] for s in entries]
colors = [ref_bar if (s["cos_mean"] == 0.0 and s["cos_std"] == 0.0) else bar for s in entries]
x = range(len(entries))
ax.bar(x, means, yerr=stds, color=colors, ecolor=err, capsize=3, width=0.7, zorder=3)
- Files reviewed: 3/3 changed files
- Comments generated: 3
- Review effort level: Low
Note
Your feedback helps us improve the quality of this feature.
Please use 👍 or 👎 to tell us whether this assessment is correct.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for i, scan in enumerate(scans, 1): | ||
| name = scan.name |
| cos = f"{s['cos_mean']:.4f} ± {s['cos_std']:.4f}" | ||
| l2 = f"{s['l2_mean']:.4f} ± {s['l2_std']:.4f}" | ||
| tag = " (ref)" if s["cos_mean"] == 0.0 and s["cos_std"] == 0.0 else "" | ||
| print(f" {s['config']:<16}{cos:<26}{l2:<24}{s['cos_max']:>8.4f}{tag}") |
| # Every axis large enough that even coarse (1.0 mm) resampling stays above the crop on every axis | ||
| # and orientation: min physical extent is 170 * 0.9 = 153 mm > 128. | ||
| PARITY_SHAPE = (190, 200, 170) | ||
|
|
…ling