Source code for ess.bifrost.live

# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2025 Scipp contributors (https://github.com/scipp)

"""Live data reduction workflows for BIFROST."""

from collections.abc import Callable
from dataclasses import dataclass
from typing import NewType

import sciline
import scipp as sc

from ess.spectroscopy.types import (
    EnergyData,
    NeXusDetectorName,
    RunType,
)

from .workflow import BifrostWorkflow


[docs] @dataclass(frozen=True, kw_only=True, slots=True) class CutAxis: """Axis and bins for cutting 4D Q - delta E data. Each axis defines a projection of the :math:`Q` - :math:`\\Delta E` space onto a 1D line as well as bin edges on that line. Examples -------- Cut along :math:`Q_x`: (see also :meth:`CutAxis.from_q_vector`) >>> from ess.bifrost.live import CutAxis >>> axis = CutAxis( ... output='Qx', ... fn=lambda sample_table_momentum_transfer: sc.dot( ... sc.vector([1, 0, 0]), ... sample_table_momentum_transfer, ... ), ... bins=sc.linspace(dim='Qx', start=-0.5, stop=0.5, num=100, unit='1/Å'), ... ) Cut along the norm :math:`|Q|`: (Note that ``sc.norm`` is wrapped in a lambda to use the proper name for the input coordinate, see :attr:`CutAxis.fn`.) >>> axis = CutAxis( ... output='|Q|', ... fn=lambda sample_table_momentum_transfer: sc.norm( ... sample_table_momentum_transfer ... ), ... bins=sc.linspace(dim='|Q|', start=-0.9, stop=3.0, num=100, unit='1/Å'), ... ) Cut along :math:`\\Delta E`: >>> axis = CutAxis( ... output='E', ... fn=lambda energy_transfer: energy_transfer, ... bins=sc.linspace('E', -0.1, 0.1, 300, unit='meV') ... ) """ output: str """Name of the output coordinate.""" fn: Callable[[...], sc.Variable] """Function to perform the cut. Used in :func:`scipp.transform_coords` and so should request input coordinates by name. """ bins: sc.Variable """Bin edges for the cut."""
[docs] @classmethod def from_q_vector(cls, output: str, vec: sc.Variable, bins: sc.Variable): """Construct from an arbitrary direction in Q.""" vec = vec / sc.norm(vec) return cls( output=output, fn=lambda sample_table_momentum_transfer: sc.dot( vec, sample_table_momentum_transfer ), bins=bins, )
CutAxis1 = NewType('CutAxis1', CutAxis) """Sciline domain type for cut axis 1.""" CutAxis2 = NewType('CutAxis2', CutAxis) """Sciline domain type for cut axis 1."""
[docs] class CutData(sciline.Scope[RunType, sc.DataArray], sc.DataArray): """Data that was cut along CutAxis1 and CutAxis2."""
[docs] def cut( data: EnergyData[RunType], *, axis_1: CutAxis1, axis_2: CutAxis2 ) -> CutData[RunType]: """Cut data along two axes. This function projects the input ``data`` expressed in :math:`Q` and :math:`\\Delta E` onto a 2D surface defined by the cut axes. This integrates over the other dimensions. Then, the projected data is histogrammed according to the axis bins. Parameters ---------- data: Input data with coordinates "sample_table_momentum_transfer" and "energy_transfer". axis_1: Defines the projection onto and binning in the first axis. axis_2: Defines the projection onto and binning in the second axis. Returns ------- : ``data`` projected and histogrammed along the cut axes. """ new_coords = {axis_1.output, axis_2.output} projected = data.bins.concat().transform_coords( new_coords, graph={axis_1.output: axis_1.fn, axis_2.output: axis_2.fn}, keep_inputs=False, ) projected = projected.drop_coords(list(set(projected.coords.keys()) - new_coords)) return CutData[RunType]( projected.hist({axis_2.output: axis_2.bins, axis_1.output: axis_1.bins}) )
[docs] def BifrostQCutWorkflow(detector_names: list[NeXusDetectorName]) -> sciline.Pipeline: """Workflow for BIFROST to compute cuts in Q-E-space.""" workflow = BifrostWorkflow(detector_names) workflow.insert(cut) return workflow