Source code for ess.bifrost.io.nexus
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2025 Scipp contributors (https://github.com/scipp)
"""NeXus input/output for BIFROST."""
from collections.abc import Iterable
import scipp as sc
import scippnexus as snx
from ess.spectroscopy.types import (
Analyzer,
InstrumentAngles,
NeXusClass,
NeXusComponentLocationSpec,
NeXusDetectorName,
NeXusFileSpec,
RunType,
)
# See https://github.com/scipp/essreduce/issues/98
[docs]
def moderator_class_for_source() -> NeXusClass[snx.NXsource]:
"""Select NXmoderator as the source."""
return NeXusClass[snx.NXsource](snx.NXmoderator)
[docs]
def load_instrument_angles(
file_spec: NeXusFileSpec[RunType],
) -> InstrumentAngles[RunType]:
# TODO need mechanism in ESSreduce to load specific components of non-unique
# class by name
from ess.reduce.nexus._nexus_loader import _unique_child_group, open_nexus_file
with open_nexus_file(file_spec.value) as file:
parameters = _unique_child_group(
_unique_child_group(file, snx.NXentry, name=None),
snx.NXparameters,
name=None,
)
return InstrumentAngles[RunType](
sc.DataGroup[sc.DataArray](
{name: parameters[name][()]['value'] for name in ('a3', 'a4')}
)
)
def _analyzer_name_for_detector_name(
detector_name: NeXusDetectorName, all_names: Iterable[str]
) -> str:
detector_index = int(detector_name.split('_', 1)[0])
analyzer_index = str(detector_index - 2)
for name in all_names:
if name.startswith(analyzer_index):
return name
raise RuntimeError(f"No analyzer found for detector {detector_name}")
[docs]
def load_analyzer_for_detector(
detector_location: NeXusComponentLocationSpec[snx.NXdetector, RunType],
) -> Analyzer[RunType]:
"""Find and load the right analyzer for a detector triplet.
Note
----
Depends heavily on the names of components being preceded by an in-instrument index,
and the analyzer and detector components being separated in index by 2.
If either condition changes this function will need to be modified.
Parameters
----------
detector_location:
The location of an NXdetector in the NeXus file.
The analyzer is identified based on this location.
Returns
-------
:
The loaded analyzer for the given detector triplet.
Only a subset of fields is returned.
"""
from ess.reduce.nexus._nexus_loader import _open_component_parent
with _open_component_parent(detector_location, nx_class=snx.NXcrystal) as parent:
analyzer_name = _analyzer_name_for_detector_name(
detector_location.component_name, parent.keys()
)
analyzer = snx.compute_positions(
parent[analyzer_name][()], store_transform='transform'
)
return Analyzer[RunType](
sc.DataGroup(
dspacing=analyzer['d_spacing'],
position=analyzer['position'],
transform=analyzer['transform'],
)
)
providers = (
load_analyzer_for_detector,
load_instrument_angles,
moderator_class_for_source,
)