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
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
"from ess.bifrost.single_crystal import BifrostSimulationBraggPeakMonitorWorkflow, make_q_map\n",
"from ess.bifrost.single_crystal.types import *\n",
"from ess.bifrost.types import McStasRawDetector\n",
"from ess.reduce.unwrap import DetectorLtotal\n",
"from ess.spectroscopy.types import *"
]
},
Expand Down Expand Up @@ -113,24 +112,7 @@
" return McStasRawDetector[RunType](monitor)\n",
"\n",
"\n",
"def detector_ltotal_from_straight_line_approximation(\n",
" detector_beamline: RawDetector[RunType],\n",
" source_position: Position[snx.NXsource, RunType],\n",
" sample_position: Position[snx.NXsample, RunType],\n",
" gravity: GravityVector,\n",
") -> DetectorLtotal[RunType]:\n",
" from ess.reduce.unwrap import to_wavelength\n",
" return to_wavelength.detector_ltotal_from_straight_line_approximation(\n",
" detector_beamline, # type: ignore[arg-type]\n",
"\n",
" source_position=source_position,\n",
" sample_position=sample_position,\n",
" gravity=gravity,\n",
" )\n",
"\n",
"\n",
"workflow.insert(assemble_detector_data_flatten)\n",
"workflow.insert(detector_ltotal_from_straight_line_approximation)"
"workflow.insert(assemble_detector_data_flatten)"
]
},
{
Expand Down
18 changes: 7 additions & 11 deletions packages/essspectroscopy/src/ess/bifrost/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from ess.spectroscopy.indirect.conversion import add_spectrometer_coords
from ess.spectroscopy.types import (
Analyzer,
DetectorPositionOffset,
EmptyDetector,
NeXusComponent,
NeXusTransformation,
Expand Down Expand Up @@ -86,7 +85,6 @@ def get_calibrated_detector_bifrost(
analyzer: Analyzer[RunType],
*,
transform: NeXusTransformation[snx.NXdetector, RunType],
offset: DetectorPositionOffset[RunType],
primary_graph: PrimarySpecCoordTransformGraph[RunType],
secondary_graph: SecondarySpecCoordTransformGraph[RunType],
) -> EmptyDetector[RunType]:
Expand All @@ -107,8 +105,6 @@ def get_calibrated_detector_bifrost(
Loaded analyzer parameters.
transform:
Transformation that determines the detector position.
offset:
Offset to add to the detector position.
primary_graph:
Coordinate transformation graph for the primary spectrometer.
secondary_graph:
Expand All @@ -122,9 +118,7 @@ def get_calibrated_detector_bifrost(
Detector geometry and spectrometer coordinates.
"""

da = get_base_calibrated_detector_bifrost(
detector, analyzer, transform=transform, offset=offset
)
da = get_base_calibrated_detector_bifrost(detector, analyzer, transform=transform)
da = da.rename(dim_0='tube', dim_1='length')

arc, channel = arc_and_channel_from_detector_number(da.coords['detector_number'])
Expand All @@ -147,7 +141,6 @@ def get_base_calibrated_detector_bifrost(
analyzer: Analyzer[RunType],
*,
transform: NeXusTransformation[snx.NXdetector, RunType],
offset: DetectorPositionOffset[RunType],
) -> sc.DataArray:
"""Extract the data array corresponding to a detector's signal field.

Expand All @@ -163,8 +156,6 @@ def get_base_calibrated_detector_bifrost(
Loaded analyzer parameters.
transform:
Transformation that determines the detector position.
offset:
Offset to add to the detector position.

Returns
-------
Expand All @@ -173,9 +164,14 @@ def get_base_calibrated_detector_bifrost(
"""

from ess.reduce.nexus import compute_detector_position, extract_signal_data_array
from ess.reduce.nexus.workflow import no_offset

da = extract_signal_data_array(detector)
position = compute_detector_position(da, transform=transform, offset=offset)
# BIFROST's detectors ride the rotating tank, so a lab-frame offset added after
# the transform cannot express any correction one would actually want; the
# transformation chain is the handle. compute_detector_position requires the
# argument, so pin it to zero here.
position = compute_detector_position(da, transform=transform, offset=no_offset)
return _assign_detector_position(da, position)


Expand Down
90 changes: 79 additions & 11 deletions packages/essspectroscopy/src/ess/bifrost/single_crystal/detector.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,100 @@
# 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 ..detector import _assign_detector_position, get_base_calibrated_detector_bifrost


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

BIFROST's Bragg peak monitor is the elastic monitor, named ``elastic_monitor``
in NeXus files (``cbm5`` in instrument documentation) and 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.

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)
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`` -- the event group in a file 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(
detector: NeXusComponent[snx.NXdetector, RunType],
analyzer: Analyzer[RunType],
*,
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 @@ -33,17 +104,14 @@ def get_calibrated_bragg_peak_detector(
Loaded analyzer parameters.
transform:
Transformation that determines the detector position.
offset:
Offset to add to the detector position.

Returns
-------
:
Detector with geometry coordinates.
"""
return get_base_calibrated_detector_bifrost(
detector, analyzer, transform=transform, offset=offset
)
return get_base_calibrated_detector_bifrost(detector, analyzer, transform=transform)


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 @@ -6,7 +6,10 @@
import scippnexus as snx
from ess.spectroscopy.types import (
DataGroupedByRotation,
EmptyDetector,
ErrorLimitedLookupTable,
GravityVector,
Position,
PulseStrideOffset,
RawDetector,
RunType,
Expand All @@ -17,6 +20,36 @@
from ess.reduce.unwrap.types import DetectorLtotal


def detector_ltotal(
sample_data: DataGroupedByRotation[RunType],
source_position: Position[snx.NXsource, RunType],
sample_position: Position[snx.NXsample, RunType],
gravity: GravityVector,
) -> DetectorLtotal[RunType]:
"""
Compute Ltotal from the straight-line approximation.

The Bragg peak monitor views the beam direct from the sample, without an analyzer
in between, so a straight line is the correct flight path.

Computed after grouping so that ``Ltotal`` carries the dimensions of the grouped
data. BIFROST's tank rotates and the Bragg peak monitor is mounted on it, so the
monitor position -- and hence ``Ltotal`` -- is time-dependent; ``group_by_rotation``
turns that 'time' dimension into 'a4'. Deriving ``Ltotal`` from the ungrouped
geometry instead would leave it labelled with a dimension the data no longer has.

This is a wrapper around
:func:`ess.reduce.unwrap.detector_ltotal_from_straight_line_approximation`
for different input types.
"""
return reduce_unwrap.to_wavelength.detector_ltotal_from_straight_line_approximation(
detector=EmptyDetector[RunType](sample_data),
source_position=source_position,
sample_position=sample_position,
gravity=gravity,
)


def detector_wavelength_data(
sample_data: DataGroupedByRotation[RunType],
lookup: ErrorLimitedLookupTable[RunType, snx.NXdetector],
Expand All @@ -40,4 +73,4 @@ def detector_wavelength_data(
)


providers = (detector_wavelength_data,)
providers = (detector_ltotal, detector_wavelength_data)
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
SampleRun,
)

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 +30,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 @@ -43,8 +43,7 @@ def BifrostBraggPeakMonitorWorkflow() -> sciline.Pipeline:
run_types=(SampleRun,),
monitor_types=(ElasticMonitor, NormalizationMonitor),
)
# Use the vanilla implementation instead of the indirect geometry one:
workflow.insert(reduce_unwrap.to_wavelength.detector_wavelength_data)
workflow[NeXusName[ElasticMonitor]] = 'elastic_monitor'
for provider in _PROVIDERS:
workflow.insert(provider)
for key, val in default_parameters().items():
Expand All @@ -57,8 +56,7 @@ def BifrostSimulationBraggPeakMonitorWorkflow() -> sciline.Pipeline:
run_types=(SampleRun,),
monitor_types=(ElasticMonitor, NormalizationMonitor),
)
# Use the vanilla implementation instead of the indirect geometry one:
workflow.insert(reduce_unwrap.to_wavelength.detector_wavelength_data)
workflow[NeXusName[ElasticMonitor]] = 'elastic_monitor'
for provider in _SIMULATION_PROVIDERS:
workflow.insert(provider)
for key, val in simulation_default_parameters().items():
Expand Down
Loading