# Migrating from NeuNorm 1.x to 2.0 NeuNorm 2.0 is a complete, [scipp](https://scipp.github.io/)-based rewrite. It is a **breaking change**: code written against the 1.x `NeuNorm.normalization.Normalization` API will not run unchanged, and there is **no drop-in compatibility shim**. The flat-field normalization *physics* is preserved, but the *API* is new. ```{note} If you are not ready to migrate, pin the legacy line: `pip install "NeuNorm<2"`. The 1.x source is archived under [`archive/neunorm-1.x/`](https://github.com/ornlneutronimaging/NeuNorm/tree/main/archive/neunorm-1.x). ``` ## What changed at a glance | | 1.x | 2.0 | |---|---|---| | Import | `import NeuNorm` | `import neunorm` (lowercase) | | Distribution name | `NeuNorm` | `NeuNorm` (unchanged — `pip install NeuNorm`) | | Minimum Python | 2.7 / 3.x | **3.11+** | | Core data type | NumPy arrays | `scipp.DataArray` (carries variances) | | Uncertainty | not propagated | **propagated automatically** | | API style | one stateful `Normalization` object | **functions + per-detector pipelines** | | Primary output | TIFF | **HDF5** (TIFF secondary) | | TOF / event mode | none | **first-class** (VENUS, Timepix) | | Dev environment | conda `environment.yml` | **pixi** (`pixi.lock`) | ## The fastest path: use a pipeline In 1.x you drove a stateful object through `load` → `normalization` → `export`. In 2.0 the equivalent is a single **pipeline** call for your detector/facility, which loads, corrects, normalizes, and writes the result (with uncertainty, masks, and provenance) to HDF5. **1.x** ```python from NeuNorm.normalization import Normalization o_norm = Normalization() o_norm.load(file="sample.tif", data_type="sample") o_norm.load(folder="/data/ob/", data_type="ob") o_norm.load(folder="/data/df/", data_type="df") # dark / "df" frames o_norm.normalization() o_norm.export(folder="/data/normalized/", data_type="normalized") normalized = o_norm.get_normalized_data() # list of NumPy arrays ``` **2.0** (MARS CCD/CMOS — continuous source, the closest analogue to the 1.x CCD case) ```python from pathlib import Path from neunorm.pipelines.mars_ccd import run_mars_ccd_pipeline # Each inner list is one acquisition "run" to combine before processing. transmission = run_mars_ccd_pipeline( sample_paths=[["sample_0001.tif", "sample_0002.tif"]], ob_paths=[["ob_0001.tif", "ob_0002.tif"]], dark_paths=[["dark_0001.tif"]], output_path=Path("/data/normalized.hdf5"), roi=(x0, y0, x1, y1), # optional gamma_filter=True, # on by default ) # `transmission` is a scipp.DataArray; the NumPy values are transmission.values ``` Pick the pipeline that matches your detector and facility: | Pipeline | Detector | Facility | TOF | |---|---|---|---| | `run_mars_ccd_pipeline` | CCD/CMOS | MARS (HFIR) | no | | `run_mars_tpx3_pipeline` | Timepix3 | MARS (HFIR) | no | | `run_venus_ccd_pipeline` | CCD/CMOS | VENUS (SNS) | no | | `run_venus_tpx1_pipeline` | Timepix1 | VENUS (SNS) | yes | | `run_venus_tpx3_histogram_pipeline` | Timepix3 | VENUS (SNS) | yes | | `run_venus_tpx3_event_pipeline` | Timepix3 (event) | VENUS (SNS) | yes | These share the same *flow* but not the same *signature* — TPX detectors omit `dark_paths`, the TOF pipelines add `rebin_by_tof`/`rebin_by_spatial`, and `run_venus_tpx3_event_pipeline` takes a `BinningConfig` and flat (per-run) path lists. Check each function's signature in the {doc}`api` reference. ## The composable path: build your own workflow If you need finer control than a pipeline offers, 2.0 exposes the individual steps as functions (the pipelines themselves are just compositions of these — read a pipeline's source for the exact ordering). The core operation is `normalize_transmission`: ```python from neunorm.loaders.stack_loader import load_stack from neunorm.processing.normalizer import normalize_transmission sample = load_stack(["sample_0001.tif", "sample_0002.tif"]) # scipp.DataArray ob = load_stack(["ob_0001.tif", "ob_0002.tif"]) transmission = normalize_transmission(sample, ob) # T = sample / ob, variances tracked ``` ## Method / concept mapping | 1.x | 2.0 | |---|---| | `Normalization()` (stateful object) | a `run_*_pipeline(...)` call, or composable functions | | `.load(..., data_type="sample"/"ob")` | pipeline `sample_paths` / `ob_paths`, or `neunorm.loaders.stack_loader.load_stack` / `tiff_loader.load_tiff_stack` / `fits_loader.load_fits_stack` | | `.load(..., data_type="df")` (dark/"df") | pipeline `dark_paths`, or `neunorm.processing.dark_corrector.subtract_dark(data, dark)` | | `.normalization(roi=...)` | Flux normalization by a background ROI: `normalize_transmission(..., background_roi=(x0, y0, x1, y1))`, or pass `background_roi=` to `run_mars_ccd_pipeline` / `run_mars_tpx3_pipeline` / `run_venus_ccd_pipeline` ([#159](https://github.com/ornlneutronimaging/NeuNorm/issues/159)). A proton-charge proxy for when proton charge is unavailable; mutually exclusive with it. Accepts one ROI or a **pooled sequence** of ROIs; use `ROI(..., inclusive=True)` for the 1.x inclusive extents, and `apply_background_roi(data, background_roi)` for the open-beam-less form. Note: 2.x `roi=` is a spatial **crop** (`apply_roi`), not this. | | `.df_correction()` | `neunorm.processing.dark_corrector.subtract_dark(data, dark)` | | `.crop(roi=ROI(...))` | `neunorm.processing.roi_clipper.apply_roi(data, ROI(x0=.., y0=.., x1=.., y1=..))` (or a bare `(x0, y0, x1, y1)` tuple) | | auto/manual gamma filtering on `load()` | `neunorm.filters.gamma_filter.apply_gamma_filter(...)`, or pipeline `gamma_filter=True` | | `.export(folder=..., file_type="tif")` | `neunorm.exporters.hdf5_writer.write_hdf5(...)` (primary) or `tiff_writer.write_tiff_stack(...)`; pipelines write automatically via `output_path` | | `.get_normalized_data()` | the pipeline **returns** a `scipp.DataArray`; use `.values` for the NumPy array | | `from NeuNorm.roi import ROI; ROI(x0, y0, x1, y1)` | `from neunorm.data_models.roi import ROI; ROI(x0=10, y0=20, x1=30, y1=40)` (also `ROI(x0=10, y0=20, width=20, height=20)`), or a bare `(x0, y0, x1, y1)` tuple. Accepted by `apply_roi`, `apply_air_region_correction`, `normalize_transmission(background_roi=)`, and the pipelines. | | `NeuNorm.normalization.DataType` | not needed — inputs are explicit function arguments | ## ROI 1.x had an `ROI` object; 2.0 has one too (`neunorm.data_models.roi.ROI`) and also accepts a bare 4-integer tuple in the same order. 2.x stops are **exclusive** (1.x `x1`/`y1` were inclusive): ```python # 1.x from NeuNorm.roi import ROI roi = ROI(x0=10, y0=10, x1=110, y1=110) # 2.0 — named ROI (exclusive stops) or a bare tuple from neunorm.data_models.roi import ROI roi = ROI(x0=10, y0=10, x1=111, y1=111) # explicit stops roi = ROI(x0=10, y0=10, width=101, height=101) # or by size roi = (10, 10, 111, 111) # or a bare (x0, y0, x1, y1) tuple ``` ## Working with the result A 2.0 pipeline returns a `scipp.DataArray` instead of a NumPy array: ```python transmission.values # NumPy array of transmission values transmission.variances # propagated variances (None in 1.x — not tracked) transmission.coords # axis coordinates (e.g. wavelength for TOF data) transmission.masks # detector masks (e.g. dead pixels) ``` The normalized result is also written to `output_path` as HDF5 (and optionally TIFF), so downstream tools can read it directly without re-running the pipeline. ## Next steps - {doc}`api` — full API reference for every loader, processing function, and pipeline. - The per-workflow guides under {doc}`workflows/README` walk through each detector/facility combination end to end.