Repository navigation
Pass pixel masks as a list of files and add a NeXus mask format - #861
Conversation
Workflows now take PixelMaskFilenames and read all mask files in one provider, instead of mapping over the files with sciline's map/reduce. This is the change that #746 lists for esssans and essdiffraction. The result does not depend on the number of files, so an empty list needs no special case. The reader and the conversion to boolean masks move to ess.reduce.masking, so that esssans and essdiffraction share them. The reader selects the format by file suffix (ISIS XML or scipp HDF5). Masks are named after the file without its directory, since directories differ between hosts. Two files with the same name raise an error, because one mask would otherwise replace the other. Breaking: with_pixel_mask_filenames, PixelMaskFilename and MaskedDetectorIDs are removed. esssans and essdiffraction need a release of essreduce that contains ess.reduce.masking. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A NeXus file can now hold a mask for each detector bank, in the logical dimensions of the bank. This allows small masks of tubes or straws (#767), which a list of detector IDs cannot express without listing every pixel. Masks use the pixel_mask field of NXdetector, with HDF5 dimension labels. Mask files are archived for years, so they do not use the scipp HDF5 layout, which is defined only by the code of scipp. The ADR records the layout and the reasons. Masks are checked when converted, because scipp silently ignores a mask with a dimension that the data lacks. A key that is not a bank of the instrument raises, since a misspelled name would otherwise mask nothing. The SANS provider now needs the bank name. Zoom has none, so ISIS gets a provider without it, and a per-bank file raises there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The value does not have to come from a file: it can be set directly, as the BEER workflow does. It holds the pixel masks of the whole instrument, and DetectorMasks holds them for one detector. to_pixel_masks is renamed to to_detector_masks, since it returns what the SANS workflows call DetectorMasks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PixelMasks can be set directly, so its values are not limited to what the file readers produce. A boolean variable was treated as a list of detector IDs, so that [True] masked the pixel with ID 1, and an integer mask for a bank was passed on as a mask. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sans2d builds DetectorMasks from its edge and sample holder masks only, so setting PixelMaskFilenames had no effect and gave no error. It now combines both. The list of files defaults to empty, so that the workflow works without mask files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reader looks only at /entry. Any other entry would be ignored without an error, which would leave pixels unmasked. This also applies if files with several entries are supported later, and an older reader gets one of them. The ADR describes that possible extension. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mask file may use any order of dimensions, so DetectorMasks could have a different layout depending on the file. It now always follows the detector. Add tests for masks with transposed dimensions or a subset of them, and for files whose sizes or labels do not fit the detector, including a comparison of I(Q) with an equivalent list of detector IDs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| return PixelMasks(masks) | ||
|
|
||
|
|
||
| def to_detector_masks( |
There was a problem hiding this comment.
Do we have to do this because the ISIS format is to list masks for a flat detector pixel list?
I would have expected/hoped we could just keep the logical dimensions of the detectors in the masks.
It should preferably not be that formats from other facilities force us to do extra work, but instead do extra work when working with data from other facilities.
There was a problem hiding this comment.
Do we have to do this
Is "this" referring to the entire function?
Stripping XML support completely would be an option, but I did not want to make the decision here. Since it is already supported there was no reason to remove it now.
There was a problem hiding this comment.
By "this" I meant having to map a mask with logical dimensions to detector numbers. I would have hoped the RawDetector is a DataArray with logical dims (folded after loading), and the mask also have logical dims that are compatible with the RawDetector.
There was a problem hiding this comment.
Good question. The code made this hard to see. Masks per bank (the NeXus format) are matched to the folded detector by logical dims only, with no detector-number lookup. The ID lookup is used only for files that list detector IDs, such as ISIS XML. to_detector_masks used to take only the IDs, which suggested every mask went through them. It now takes the detector and uses detector_number only for ID-list masks (de65bac).
Mask files hold any number of named masks, as datasets in /entry/instrument/<bank>/masks (NXcollection), instead of one NeXus pixel_mask bit field per bank. The dataset name is the mask name, a nonzero value masks a pixel, and each mask has its own dimensions. This allows one file for all masks of an instrument, and tube and straw masks no longer have to be combined into one array. Masks with different validity periods go into separate files; workflows use the masks of all files and raise on duplicate names. The writer stores uint8, since HDF5 has no boolean type. The reader accepts any integer type and the h5py boolean enumeration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@nvaytet Thanks for the review. Your comments convinced us that following the NeXus
The ADR gives the reasoning, including why the masks sit in an |
LokiAtLarmorTutorialWorkflow sets PixelMaskFilenames to a list of paths. Creating its widget failed on str.join, and the parameter box then showed no parameters at all, which broke the LoKI widget notebook. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| `ess.reduce.masking.write_pixel_masks` sets bit 8, "user defined mask". | ||
| - Dimensions of `pixel_mask` are named with HDF5 dimension labels. | ||
| - The name of a dataset in `masks` is the name of the mask in the reduced data. | ||
| Datasets with the same name in several banks are one mask. |
There was a problem hiding this comment.
What do you mean exactly by "are one mask"? Do they get loaded as a single data object that spans multiple NXdetectors?
| pixel_mask int32, HDF5 dimension labels ("layer", "tube") | ||
| masks NXcollection | ||
| broken_tubes uint8, HDF5 dimension labels ("tube",) | ||
| beamstop uint8, HDF5 dimension labels ("layer", "tube", "straw") |
There was a problem hiding this comment.
Maybe I missed it but should we explain why it's unit8 and not booleans?
There was a problem hiding this comment.
HDF5 does not support bool.
There was a problem hiding this comment.
Yes, I remembered that. I was asking if we could explain this in the ADR, so that the next person coming along doesn't ask the same question?
Masks given per bank match the detector by its logical dimensions and never use detector IDs. Taking only the IDs hid this, and suggested that every mask goes through an ID lookup. The function now takes the detector data array: masks per bank use its sizes, and only masks given as a list of IDs read the ID coordinate. Its name is a parameter, since ISIS data from Mantid and POWGEN data do not use 'detector_number'. The SANS DetectorIDs key existed only to feed masking, so it is removed and the mask providers take EmptyDetector[SampleRun]. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| NeXus defines them as the start and end of a measurement, which can differ from the validity of a mask. | ||
| For example, a mask derived from a run in November can be valid from the day in October when a tube broke. | ||
| A reader of an archived file would also take them for measurement times. | ||
| `end_time` is usually unknown when a file is archived, since a static mask stops being valid when the hardware changes, and the file cannot be edited then. |
There was a problem hiding this comment.
Even if there is no validity encoded, we should still add a creation time. That way, you can at least get a rough idea of whether a file might still be valid.
There was a problem hiding this comment.
Good point. write_pixel_masks now writes the NeXus root attributes file_time and creator, the same as kafka-to-nexus does for raw files.
| ```text | ||
| /entry NXentry | ||
| valid_from "2026-11-01T00:00:00Z", not read by workflows | ||
| instrument NXinstrument |
There was a problem hiding this comment.
I'd add a name dataset to the instrument since the detector names are not always so easy to recognise. (Same as raw data files)
There was a problem hiding this comment.
Good point, maybe also facility or similar fields we have in raw NeXus files?
There was a problem hiding this comment.
Seems there is no facility in the NeXus files, so not doing that for now (only sth. part of NXsource), I think the instrument name should suffice.
write_pixel_masks takes the instrument name and writes it to /entry/instrument/name, as in raw data files, since bank names do not always identify the instrument. It also writes the NeXus root attributes file_time and creator, so a file without a validity date still shows roughly when it was made. Workflows do not read these. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| instrument = entry.create_class('instrument', snx.NXinstrument) | ||
| with h5py.File(filename, 'w') as h5file: | ||
| h5file.attrs['file_time'] = datetime.now(UTC).isoformat(timespec='seconds') | ||
| h5file.attrs['creator'] = f'essreduce {__version__}' |
There was a problem hiding this comment.
NeXus uses a separate creator and version:
@creator: (optional) [NX_CHAR](https://manual.nexusformat.org/nxdl-types.html#nx-char)
facility or program where file originated
@creator_version: (optional) [NX_CHAR](https://manual.nexusformat.org/nxdl-types.html#nx-char)
Version of facility or program used in writing the file
NeXus defines the root attributes `creator` (program that wrote the file) and `creator_version` (its version) as separate fields. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Summary
Workflows now take a list of mask files,
PixelMaskFilenames, instead of mapping the workflow over filenames withwith_pixel_mask_filenames. This applies to the SANS and powder workflows. A single provider reads all files intoPixelMasks, a dict by mask name that covers the whole instrument. It can also be set directly from memory. A separate provider converts it toDetectorMasksfor one detector.This is a breaking change:
with_pixel_mask_filenames,PixelMaskFilenameandMaskedDetectorIDsare removed.Why
Map/reduce changes the graph depending on the number of files, and we are migrating away from it (#746). Reading is independent of the run and the bank, so it runs once. Only the conversion depends on the bank. With
sciline.Stage,DetectorMasksstays a flatdict[str, sc.Variable], and the bank level exists only in the file content. It also fixes two existing bugs: an empty list of files crashed in SANS, andLokiAtLarmorTutorialWorkflowassigned a list to a single-filename parameter. Sans2d now applies mask files together with its computed edge and sample holder masks.Masks from NeXus files are named by their dataset names. Masks from ID files are named by the file name without its directory, since directories differ between hosts. The graph does not depend on the number or the type of mask files.
Mask file format
The second commit adds a format for masks of detector banks, needed for masks of tubes or straws (#767). It can be split into its own PR if reviewers prefer. Mask files are archived for years, so the format uses plain HDF5 types and the group structure of NeXus, and not the scipp HDF5 layout:
The dataset name is the mask name, and each mask has its own dimensions. A file can hold all masks of an instrument, or masks can be split into files, for example by validity period. The ADR in this PR records the layout and the rejected alternatives, including the NeXus
pixel_maskfield.ISIS XML files and scipp HDF5 files with a list of detector IDs are still read. Powder workflows accept only these ID-based files for now, because they do not pass a bank name when converting masks.
Workflows do not check validity dates. Selecting the mask files valid for a run is left to the caller that sets the list of filenames.
Before merging
esssans and essdiffraction import
ess.reduce.masking, so they need an essreduce release that contains it. Theessreduce>=26.10.1pin is not raised yet, because that release does not exist yet.esslivedata and essapps call the removed API and need updating after the release.
Follow-up
DetectorMasks[RunType], in a separate PR.Test plan
loki-iofq,dream-powder-reductionand esssanszoomnotebooks. The other edited notebooks were not run, and the docs were not built.🤖 Generated with Claude Code