Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/ess/dream/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)

"""
Components for DREAM
"""
import importlib.metadata

from . import data
from .io import fold_nexus_detectors, load_nexus

try:
__version__ = importlib.metadata.version(__package__ or __name__)
except importlib.metadata.PackageNotFoundError:
__version__ = "0.0.0"

del importlib

__all__ = [
"data",
"fold_nexus_detectors",
"load_nexus",
]
34 changes: 34 additions & 0 deletions src/ess/dream/data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
_version = '1'

__all__ = ['get_path']


def _make_pooch():
import pooch

return pooch.create(
path=pooch.os_cache('ess/powgen'),
env='ESS_DATA_DIR',
base_url='https://public.esss.dk/groups/scipp/ess/dream/{version}/',
version=_version,
registry={
'DREAM_nexus_sorted-2023-12-07.nxs': 'md5:22824e14f6eb950d24a720b2a0e2cb66',
},
)


_pooch = _make_pooch()


def get_path(name: str, unzip: bool = False) -> str:
"""
Return the path to a data file bundled with ess.dream.

This function only works with example data and cannot handle
paths to custom files.
"""
import pooch

return _pooch.fetch(name, processor=pooch.Unzip() if unzip else None)
133 changes: 133 additions & 0 deletions src/ess/dream/io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)

import os
from typing import Any, Dict, Union

import scipp as sc
import scippnexus as snx


def load_nexus(
path: Union[str, os.PathLike],
*,
load_pixel_shape: bool = False,
entry: str = 'entry',
fold_detectors: bool = True,
) -> sc.DataGroup:
"""
Load an unprocessed DREAM NeXus file.

See https://confluence.esss.lu.se/pages/viewpage.action?pageId=462000005
and the ICD DREAM interface specification for details.
Comment on lines +21 to +22
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't great. Are there publicly accessible resources about this? Can we make these public?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why public? This is an ESS repo, so I think everyone who needs access can get to the info?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how that will work for visitors. But fine.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will only matter for instrument scientist, mainly during hot commissioning.


Notes (2023-12-06):

- Mounting-unit, cassette, and counter roughly correspond to the azimuthal angle
in the mantle detector. However, counter is reversed in the current files. This
may also be the case in the other detectors.
- The endcap detectors have a irregular structure that cannot be fully folded.
This is not a problem but note again that the counter may be reversed. It is
currently not clear if this is a bug in the file.
- The high-resolution detector has a very odd numbering scheme. The SANS detector
is using the same, but is not populated in the current files. The scheme
attempts to follows some sort of physical ordering in space (x,y,z), but it
looks partially messed up.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these be issues to help us follow up on them?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure. It depends on how we will handle versioning in the loader. Will we have to keep it working with the current files, if ECDC makes fixes/changes in the future?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe not the current ones as they should be replaced by newer ones, I guess. But ultimately, we will have to keep supporting old ones.


Parameters
----------
path:
Path to the NeXus file.
load_pixel_shape:
If True, load the pixel shape from the file's NXoff_geometry group. This is
often unused by would slow down file loading. Default is False.
entry:
Name of the entry to load. Default is "entry".
fold_detectors:
If True, fold the detector data to (partially) mimic the logical detector
structure. Default is True.

Returns
-------
:
A data group with the loaded file contents.
"""
definitions = snx.base_definitions()
if not load_pixel_shape:
definitions["NXdetector"] = FilteredDetector

with snx.File(path, definitions=definitions) as f:
dg = f[entry][()]
dg = snx.compute_positions(dg)
return fold_nexus_detectors(dg) if fold_detectors else dg


def fold_nexus_detectors(dg: sc.DataGroup) -> sc.DataGroup:
"""
Fold the detector data in a DREAM NeXus file.

The detector banks in the returned data group will have a multi-dimensional shape,
following the logical structure as far as possible. Note that the full structure
cannot be folded, as some dimensions are irregular.
"""
dg = dg.copy()
dg['instrument'] = dg['instrument'].copy()
instrument = dg['instrument']
mantle = instrument['mantle_detector']
mantle['mantle_event_data'] = mantle['mantle_event_data'].fold(
dim='detector_number',
sizes={
'wire': 32,
'mounting_unit': 5,
'cassette': 6,
'counter': 2,
'strip': 256,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These names correspond to the ICD, not GEANT4 names on the confluence page. Are these the ones we want to go with? Should we rename the dims in the GEANT4 loader?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can necessarily keep the GEANT4 naming, since the NeXus files order things in a different way so logic folding according to GEANT4 names is not possible (in at least 1 case, I think).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

},
)
for direction in ('backward', 'forward'):
endcap = instrument[f'endcap_{direction}_detector']
endcap[f'endcap_{direction}_event_data'] = endcap[
f'endcap_{direction}_event_data'
].fold(
dim='detector_number',
sizes={
'strip': 16,
'wire': 16,
'sector': 5 if direction == 'forward' else 11,
'sumo_cass_ctr': -1, # sumo*cassette*counter, irregular, cannot fold
},
)
high_resolution = instrument['high_resolution_detector']
high_resolution['high_resolution_event_data'] = high_resolution[
'high_resolution_event_data'
].fold(
dim='detector_number',
sizes={
'strip': 32,
'other': -1, # some random order that is hard to follow
},
)
sans = instrument['sans_detector']
sans['sans_event_data'] = sans['sans_event_data'].fold(
dim='detector_number',
sizes={
'strip': 32,
'other': -1, # some random order that is hard to follow
},
)
return dg


def _skip(name: str, obj: Union[snx.Field, snx.Group]) -> bool:
skip_classes = (snx.NXoff_geometry,)
return isinstance(obj, snx.Group) and (obj.nx_class in skip_classes)


class FilteredDetector(snx.NXdetector):
def __init__(
self, attrs: Dict[str, Any], children: Dict[str, Union[snx.Field, snx.Group]]
):
children = {
name: child for name, child in children.items() if not _skip(name, child)
}
super().__init__(attrs=attrs, children=children)
77 changes: 77 additions & 0 deletions tests/dream/io_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
import pytest

from ess import dream


@pytest.fixture
def filename():
return dream.data.get_path('DREAM_nexus_sorted-2023-12-07.nxs')


@pytest.mark.filterwarnings("ignore:Failed to load /entry/instrument/monitor_bunker")
@pytest.mark.filterwarnings("ignore:Failed to load /entry/instrument/monitor_cave")
@pytest.mark.filterwarnings("ignore:Failed to load /entry/instrument/polarizer/rate")
@pytest.mark.filterwarnings("ignore:Failed to load /entry/instrument/sans_detector")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you planning to remove those filters when we have a complete file?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose, what are you asking?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to know if these are permanent and a problem with the loader or just temporary until we get better files.

def test_load_nexus_loads_file(filename):
dg = dream.load_nexus(filename)
assert 'instrument' in dg
instr = dg['instrument']
for name in (
'mantle',
'endcap_backward',
'endcap_forward',
'high_resolution',
'sans',
):
assert f'{name}_detector' in instr
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the GEANT4 loader, those are called name, i.e., without "_detector". We should use the same names in both and I'm in favour of the shorter names in the GEANT4 loader. What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@celinedurniak Should we remove the suffix? The name is also repeated on the event data, e.g.,

/entry/instrument/endcap_forward_detector/endcap_forward_event_data

What do you prefer? Something like /entry/instrument/endcap_forward/events? Or something longer? Should we keep the naming from the NeXus file?

det = instr[f'{name}_detector']
assert 'pixel_shape' not in det


def test_load_nexus_fails_if_entry_not_found(filename):
with pytest.raises(KeyError):
dream.load_nexus(filename, entry='foo')


@pytest.mark.filterwarnings("ignore:Failed to load /entry/instrument/monitor_bunker")
@pytest.mark.filterwarnings("ignore:Failed to load /entry/instrument/monitor_cave")
@pytest.mark.filterwarnings("ignore:Failed to load /entry/instrument/polarizer/rate")
@pytest.mark.filterwarnings("ignore:Failed to load /entry/instrument/sans_detector")
def test_load_nexus_folds_detectors_by_default(filename):
dg = dream.load_nexus(filename)
instr = dg['instrument']
# sans_detector is not populated in the current files
for name in ('mantle', 'endcap_backward', 'endcap_forward', 'high_resolution'):
det = instr[f'{name}_detector']
# There may be other dims, but some are irregular and this may be subject to
# change
assert 'strip' in det.dims


@pytest.mark.filterwarnings("ignore:Failed to load /entry/instrument/monitor_bunker")
@pytest.mark.filterwarnings("ignore:Failed to load /entry/instrument/monitor_cave")
@pytest.mark.filterwarnings("ignore:Failed to load /entry/instrument/polarizer/rate")
@pytest.mark.filterwarnings("ignore:Failed to load /entry/instrument/sans_detector")
def test_load_nexus_with_disabled_fold(filename):
dg = dream.load_nexus(filename, fold_detectors=False)
instr = dg['instrument']
for name in ('mantle', 'endcap_backward', 'endcap_forward', 'high_resolution'):
det = instr[f'{name}_detector']
assert det.dims == ('detector_number',)


@pytest.mark.filterwarnings("ignore:Failed to load /entry/instrument/monitor_bunker")
@pytest.mark.filterwarnings("ignore:Failed to load /entry/instrument/monitor_cave")
@pytest.mark.filterwarnings("ignore:Failed to load /entry/instrument/polarizer/rate")
@pytest.mark.filterwarnings("ignore:Failed to load /entry/instrument/sans_detector")
def test_load_nexus_with_pixel_shape(filename):
dg = dream.load_nexus(filename, load_pixel_shape=True)
assert 'instrument' in dg
instr = dg['instrument']
# sans_detector is not populated in the current files
for name in ('mantle', 'endcap_backward', 'endcap_forward', 'high_resolution'):
assert f'{name}_detector' in instr
det = instr[f'{name}_detector']
assert 'pixel_shape' in det