Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -1,19 +1,94 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2026 Scipp contributors (https://github.com/scipp)

"""Bragg peak detector handling for BIFROST."""
"""Bragg peak monitor handling for BIFROST."""

import scipp as sc
import scippnexus as snx
from ess.spectroscopy.types import (
Analyzer,
DetectorPositionOffset,
ElasticMonitor,
EmptyDetector,
NeXusComponent,
NeXusData,
NeXusTransformation,
RawDetector,
RunType,
)

from ..detector import get_base_calibrated_detector_bifrost
from ess.reduce.nexus.types import MonitorPositionOffset

from ..detector import _assign_detector_position, get_base_calibrated_detector_bifrost


def get_calibrated_bragg_peak_monitor(
monitor: NeXusComponent[ElasticMonitor, RunType],
*,
transform: NeXusTransformation[ElasticMonitor, RunType],
offset: MonitorPositionOffset[RunType, ElasticMonitor],
) -> EmptyDetector[RunType]:
"""Extract the data array corresponding to the Bragg peak monitor's signal field.

BIFROST's Bragg peak monitor is the elastic monitor (``cbm5``), written as an
``NXmonitor``. It has no pixel offsets, so its position is the transformed origin
as in :func:`ess.reduce.nexus.workflow.get_calibrated_monitor`, rather than the
per-pixel computation used for detectors. The position is assigned with the
BIFROST-specific broadcasting because the monitor is mounted on the detector tank
and therefore moves with the instrument angle.

Parameters
----------
monitor:
Loaded NeXus monitor.
transform:
Transformation that determines the monitor position.
offset:
Offset to add to the monitor position.

Returns
-------
:
Monitor with geometry coordinates.
"""
from ess.reduce.nexus import extract_signal_data_array

da = extract_signal_data_array(monitor)
unit = transform.value.unit
position = transform.value * sc.vector([0.0, 0.0, 0.0], unit=unit) + offset.to(
unit=unit
)
return EmptyDetector[RunType](_assign_detector_position(da, position))


def assemble_bragg_peak_monitor_data(
monitor: EmptyDetector[RunType],
data: NeXusData[ElasticMonitor, RunType],
) -> RawDetector[RunType]:
"""Combine the Bragg peak monitor's geometry with its event data.

Assembled as a monitor, not as a detector: ``assemble_detector_data`` groups
events by ``event_id`` onto a ``detector_number`` grid, and the Bragg peak
monitor is a single pixel with nothing to group by. Its geometry is therefore
assigned straight onto the events. The result is the same whether or not the
events carry an ``event_id`` -- a file-loaded ``cbm5_events`` group does, a
stream may not -- because the coordinate is simply left untouched.

Parameters
----------
monitor:
Monitor geometry from :func:`get_calibrated_bragg_peak_monitor`.
data:
Monitor event data.

Returns
-------
:
Events with geometry coordinates.
"""
from ess.reduce.nexus.workflow import assemble_monitor_data

return RawDetector[RunType](assemble_monitor_data(monitor, data))


def get_calibrated_bragg_peak_detector(
Expand All @@ -23,7 +98,11 @@ def get_calibrated_bragg_peak_detector(
transform: NeXusTransformation[snx.NXdetector, RunType],
offset: DetectorPositionOffset[RunType],
) -> EmptyDetector[RunType]:
"""Extract the data array corresponding to the Bragg peak detector's signal field.
"""Extract the data array corresponding to a detector's signal field.

Simulated data contains no Bragg peak monitor, so a bank ('triplet') of the
regular inelastic detector stands in for it. Real data uses
:func:`get_calibrated_bragg_peak_monitor` instead.

Parameters
----------
Expand All @@ -46,4 +125,5 @@ def get_calibrated_bragg_peak_detector(
)


providers = (get_calibrated_bragg_peak_detector,)
providers = (get_calibrated_bragg_peak_monitor, assemble_bragg_peak_monitor_data)
simulation_providers = (get_calibrated_bragg_peak_detector,)
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ def detector_wavelength_data(
:func:`ess.reduce.unwrap.detector_wavelength_data`
for different input types.
"""
# A time-dependent detector position (BIFROST's tank rotates, and the Bragg peak
# monitor is mounted on it) makes ``ltotal`` depend on 'time'. The instrument angle
# is the only dynamic parameter, so ``group_by_rotation`` has already turned that
# same 'time' dimension into 'a4'. Rename to match, or the broadcast below rejects
# ``ltotal`` as having a dimension the data does not.
if 'time' in ltotal.dims and 'time' not in sample_data.dims:
ltotal = ltotal.rename_dims(time='a4')
return reduce_unwrap.to_wavelength.detector_wavelength_data(
detector_data=RawDetector[RunType](sample_data),
lookup=lookup,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
)

from ess.reduce import unwrap as reduce_unwrap
from ess.reduce.nexus.types import NeXusName

from ..cutting import group_by_rotation
from ..io import nexus
Expand All @@ -30,7 +31,7 @@
_SIMULATION_PROVIDERS = (
*nexus.providers,
*conversion.providers,
*detector.providers,
*detector.simulation_providers,
*q_map.providers,
*time_of_flight.providers,
convert_simulated_time_to_event_time_offset,
Expand All @@ -45,6 +46,12 @@ def BifrostBraggPeakMonitorWorkflow() -> sciline.Pipeline:
)
# Use the vanilla implementation instead of the indirect geometry one:
workflow.insert(reduce_unwrap.to_wavelength.detector_wavelength_data)
# The Bragg peak monitor sees the direct beam, so its flight path is a straight
# line rather than the analyzer-folded path of the inelastic detectors.
workflow.insert(
reduce_unwrap.to_wavelength.detector_ltotal_from_straight_line_approximation
)
workflow[NeXusName[ElasticMonitor]] = 'elastic_monitor'
for provider in _PROVIDERS:
workflow.insert(provider)
for key, val in default_parameters().items():
Expand All @@ -59,6 +66,12 @@ def BifrostSimulationBraggPeakMonitorWorkflow() -> sciline.Pipeline:
)
# Use the vanilla implementation instead of the indirect geometry one:
workflow.insert(reduce_unwrap.to_wavelength.detector_wavelength_data)
# The Bragg peak monitor sees the direct beam, so its flight path is a straight
# line rather than the analyzer-folded path of the inelastic detectors.
workflow.insert(
reduce_unwrap.to_wavelength.detector_ltotal_from_straight_line_approximation
)
workflow[NeXusName[ElasticMonitor]] = 'elastic_monitor'
for provider in _SIMULATION_PROVIDERS:
workflow.insert(provider)
for key, val in simulation_default_parameters().items():
Expand Down
Loading