feat: port normalization to NeuNorm 2.0 (route a, #412)#420
Open
KedoKudo wants to merge 2 commits into
Open
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## next #420 +/- ##
==========================================
+ Coverage 10.68% 11.01% +0.33%
==========================================
Files 195 196 +1
Lines 17827 17832 +5
Branches 1829 1835 +6
==========================================
+ Hits 1905 1965 +60
+ Misses 15875 15819 -56
- Partials 47 48 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Core engine swap, gated by the value-pinning contract tests of #415 (all 9 pass unmodified, plus 223-test suite green): - core/processing/normalization.py: the transmission division runs through neunorm.processing.normalizer.normalize_transmission on scipp DataArrays; the 1.x behaviors 2.0 dropped are bridged locally (in-memory stacks, float32 working dtype, pooled multi-ROI inclusive air correction, 1:1 vs nanmedian(OB) pairing, zero-OB zeroing, per-frame normalized_image_NNNN.tif export) - normalize_stacks() is the single math path; step2's parallel GUI implementation now delegates to it, keeping its dialogs, status messages and step3 re-import. Two audit-documented no-ops (OB never smoothed; "normalization then moving average" not reaching outputs) are preserved on purpose so this PR changes one variable at a time - the four TIFF-writer-only sites (step6 export, tof_bin x2, tof_combine) drop NeuNorm for core/io/tiff.write_float32_tiff, preserving exact on-disk names including the 1.x extension rewrite - about box reports neunorm (duplicated version blocks deduped) - deps: neunorm >=2.0.0,<3 + scipp (pyproject/pixi/environment.yml); conda recipe replaces the 1.x pip-vendoring with a real neunorm run requirement; lockfile resolved (neunorm 2.0.0, scipp 26.3.1) - drop module-level `import inflect` in tof_bin/export_images.py: the package was never declared nor locked, so the import was a latent crash; it only pluralized a log word DO NOT MERGE until CIS (Jean) completes the side-by-side testing protocol in the PR description (flagship rule). Assisted-With: Claude Fable 5 (1M context) <noreply@anthropic.com>
0f58158 to
744c5b2
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
Ports iBeatles’ normalization pipeline to NeuNorm 2.0 by routing the core transmission normalization through neunorm.processing.normalizer.normalize_transmission (via scipp DataArrays), while preserving key NeuNorm 1.x numeric semantics and on-disk TIFF export layout relied on by downstream steps/tools.
Changes:
- Introduces a single shared normalization math path (
normalize_stacks) plusexport_normalized_stackand a NeuNorm-1.x-compatible float32 TIFF writer (write_float32_tiff). - Refactors the step2 GUI normalization flow to delegate computation/export to the shared core normalization helpers.
- Replaces NeuNorm-based “writer-only” export sites (step6, tof_bin, tof_combine) with the new float32 TIFF writer and updates dependencies for NeuNorm 2.0 + scipp.
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/ibeatles/core/processing/normalization.py |
Implements NeuNorm 2.0-backed normalization (normalize_stacks) and preserves 1.x semantics + per-frame export layout. |
src/ibeatles/core/io/tiff.py |
Adds a dedicated float32 single-page TIFF writer to replace NeuNorm export usage. |
src/ibeatles/step2/normalization.py |
Updates GUI normalization to use normalize_stacks() + export_normalized_stack() while retaining GUI plumbing. |
src/ibeatles/step6/export.py |
Replaces NeuNorm export with write_float32_tiff, preserving .tif naming behavior. |
src/ibeatles/tools/tof_bin/tof_bin_export_launcher.py |
Switches bin export TIFF writing from NeuNorm to write_float32_tiff. |
src/ibeatles/tools/tof_bin/export_images.py |
Switches export-images TIFF writing from NeuNorm to write_float32_tiff and removes inflect usage. |
src/ibeatles/tools/tof_combine/export/export_images.py |
Switches combine export TIFF writing from NeuNorm to write_float32_tiff. |
src/ibeatles/about/about_launcher.py |
Updates “About” dependency reporting from NeuNorm to neunorm and removes duplicate blocks. |
tests/unit/ibeatles/core/processing/test_normalize_stacks.py |
Adds unit tests for input handling, ROI bounds, sample-only edge behavior, and export naming. |
tests/unit/ibeatles/core/io/test_tiff.py |
Adds unit tests for float32 TIFF writer round-trip and input validation. |
pyproject.toml |
Updates dependencies to neunorm>=2,<3 and adds scipp. |
environment.yml |
Updates environment dependencies for NeuNorm 2.0 + scipp. |
conda.recipe/meta.yaml |
Removes pip-vendored NeuNorm install and declares neunorm + scipp runtime requirements. |
pixi.lock |
Locks new dependency resolution including scipp and neunorm 2.0.0. |
Copilot review on #420: the caught ValueError (e.g. no OB + no background ROI) was only logged, leaving the user with the generic "Normalization Failed" message. running_normalization now records the failure reason and run_and_export shows it in the status bar; the ROI table retrieval error gets a specific message too. Assisted-With: Claude Fable 5 (1M context) <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.
This is a load-bearing PR under the flagship rule: it swaps the normalization engine that produces every downstream number (binning → fitting → strain). CI green and review are not sufficient. Jean (CIS) must complete the side-by-side protocol below on real data first.
What
The route-(a) NeuNorm 2.0 port decided 2026-06-12 (#412):
core/processing/normalization.py— the transmission division now runs throughneunorm.processing.normalizer.normalize_transmissionon scipp DataArrays. The 1.x behaviors that 2.0 dropped are bridged locally in a new single math path,normalize_stacks():nanmedian(OB)fallback when they don'tnormalized_image_NNNN.tiffloat32 export — the on-disk layout step3's folder re-import depends onnormalize_stacks(), keeping its dialogs, status messages, session bookkeeping, and the step3 auto re-import.core/io/tiff.write_float32_tiff(PIL, the same writer 1.x used). On-disk names are preserved exactly, including 1.x's quirk of rewriting step6's.tiffbase names to.tif.neunorm: 2.0.0; the duplicated version blocks are deduped.neunorm>=2.0.0,<3+scippin pyproject/pixi/environment.yml; the conda recipe's 1.x pip-vendoring workaround is replaced by a realneunormrun requirement (2.0 publishes a clean conda package to neutronimaging — the recipe's own TODO comment). Lockfile resolved: neunorm 2.0.0 (PyPI wheel), scipp 26.3.1 (conda-forge).Numeric gate
rtol ≤ 1e-5), not bit-identity — 1.x accumulated sums in float32; the port computes in float64 and rounds once at the end. Differences should be last-ulp only.Behavior preserved vs changed
Preserved on purpose so this PR changes one variable at a time (both are audit findings; fixes are in the coordinated science-fixes PR, not here):
Changed (UX/logging only, no numbers):
ValueErrorescape the Qt slot (PyQt5 aborts on that);None;.tifpath;import inflectremoved from tof_bin/export_images.py — the package was never declared in any manifest nor present in any lockfile, so the module-level import was a latent crash; it only pluralized one log word.Testing protocol for CIS (Jean)
Setup: current release (1.x engine) vs this branch, same machine, same data. Suggested comparison:
np.testing.assert_allclose(old, new, rtol=1e-5)over the exported TIFFs, plus end-to-end strain/d-spacing outputs. Run with moving average OFF for the numeric comparisons (the smoothing axis itself is a known bug being fixed separately; with MA on, compare only "runs and exports without errors").normalized_*.tifvalues; step3 auto-import looks rightibeatles --no-guiwith a config) end-to-endneunorm: 2.0.0, single entry per libraryAcceptance: scenarios 1–7 agree within float32 precision; 8–9 bit-identical; 5 fails loud-but-graceful; no GUI flow regressions.
Deferred (next PR, also CIS-tested)
Science fixes that change published numbers, kept out of the engine swap deliberately: moving-average axis (smooths wavelength/TOF instead of spatial), the "Normalization, Moving Average" order no-op, step6 strain-error formula, spectra empty-bin alignment, shipped config.json
debugging: true.Also queued for that batch (surfaced by Copilot review, pre-existing in the current release): tof_bin's
metadata.jsonand logs recordimage_NNNN.tifnames while the files land as.tiff— a 1.xexport(file_type="tiff")extension-rewrite quirk this PR preserves byte-for-byte on purpose.Closes #412 (with the science-fixes PR completing the epic).
🤖 Generated with Claude Code