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 @@ -25,6 +25,7 @@
" simulated_elastic_incoherent_with_phonon,\n",
" lookup_table_simulation,\n",
")\n",
"from ess.bifrost.io.mcstas import load_sample_angle\n",
"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",
Expand Down Expand Up @@ -58,8 +59,11 @@
"outputs": [],
"source": [
"workflow = BifrostSimulationBraggPeakMonitorWorkflow()\n",
"\n",
"workflow[Filename[SampleRun]] = simulated_elastic_incoherent_with_phonon()\n",
"workflow[LookupTableFilename] = lookup_table_simulation()\n",
"workflow.insert(load_sample_angle) # Needed for simulation data\n",
"\n",
"workflow[LookupTableRelativeErrorThreshold] = {\n",
" 'detector': np.inf,\n",
" 'normalization_monitor': np.inf,\n",
Expand Down
24 changes: 24 additions & 0 deletions packages/essspectroscopy/src/ess/bifrost/io/mcstas.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,37 @@
from ess.spectroscopy.types import (
EmptyDetector,
NeXusData,
NeXusFileSpec,
PulsePeriod,
RawDetector,
RunType,
SampleAngle,
)

from ess.reduce.nexus import open_component_group
from ess.reduce.nexus.types import NeXusLocationSpec

from ..types import McStasRawDetector


def load_sample_angle(
file_spec: NeXusFileSpec[RunType],
) -> SampleAngle[RunType]:
"""Load the rotation angle of a sample from a McStas BIFROST NeXus file."""
return SampleAngle[RunType](_load_experiment_parameter(file_spec, "a3"))


def _load_experiment_parameter(
file_spec: NeXusFileSpec[RunType], param_name: str
) -> sc.DataArray:
with open_component_group(
NeXusLocationSpec(filename=file_spec.value),
nx_class=snx.NXparameters,
parent_class=snx.NXentry,
) as group:
return group[param_name][()]['value']
Comment on lines +34 to +39

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So McStas stores this differently than PROD?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The McStas files that Greg made have an NXparameters group which doesn't exist in prod because it holds simulation params. The handling of nested value groups in the prod loader can load those data from mcstas files. Unfortunately, the sample stack does not exist in mcstas files. But the instrument angle can be loaded. So I could remove its special handler for mcstas. Should I?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe?



def assemble_detector_data(
detector: EmptyDetector[RunType],
event_data: NeXusData[snx.NXdetector, RunType],
Expand Down Expand Up @@ -74,4 +97,5 @@ def wrap_event_time_offset(event_time_offset: sc.Variable) -> sc.Variable:
providers = (
assemble_detector_data,
convert_simulated_time_to_event_time_offset,
load_sample_angle,
)
52 changes: 38 additions & 14 deletions packages/essspectroscopy/src/ess/bifrost/io/nexus.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,33 +25,57 @@
from ess.reduce.nexus.types import NeXusLocationSpec, TransformationTimeFilter


# See https://github.com/scipp/essreduce/issues/98
def moderator_class_for_source() -> NeXusClass[snx.NXsource]:
"""Select NXmoderator as the source."""
return NeXusClass[snx.NXsource](snx.NXmoderator)


def load_sample_angle(
file_spec: NeXusFileSpec[RunType],
) -> SampleAngle[RunType]:
return SampleAngle[RunType](_load_experiment_parameter(file_spec, "a3"))
"""Load the rotation angle of a sample from a BIFROST NeXus file."""
return SampleAngle[RunType](
_load_rotation_angle(
file_spec,
"114_sample_stack/rotation_stage",
)
)


def load_instrument_angle(
file_spec: NeXusFileSpec[RunType],
) -> InstrumentAngle[RunType]:
return InstrumentAngle[RunType](_load_experiment_parameter(file_spec, "a4"))
"""Load the rotation angle for the BIFROST detector from a NeXus file."""
return InstrumentAngle[RunType](
_load_rotation_angle(
file_spec,
"detector_tank_angle/transformations/detector_tank_angle_r0",
)
)


def _load_experiment_parameter(
file_spec: NeXusFileSpec[RunType], param_name: str
) -> sc.DataArray:
def _load_rotation_angle(file_spec: NeXusFileSpec[RunType], path: str) -> sc.DataArray:
with open_component_group(
NeXusLocationSpec(filename=file_spec.value),
nx_class=snx.NXparameters,
nx_class=snx.NXinstrument,
parent_class=snx.NXentry,
) as group:
return group[param_name][()]['value']
) as instrument:
log: snx.Group = instrument[path] # type: ignore[assignment]
if isinstance(log['value'], snx.Group):
# The NXlog is nested in this group, e.g., the group can be an NXpositioner
# with children 'value', 'target_value', etc.
transform: snx.nxtransformations.Transform = log['value'][()] # type: ignore[assignment]
Comment on lines +59 to +62

@SimonHeybrock SimonHeybrock Jul 23, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Still trying to understand whether this kind of nesting is a bug/bad and should be reported. Have had issues with this elsewhere. But no action here, as I presume it is necessary to make it work now.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, it wouldn't work otherwise.

else:
# `log` is the NXlog we are looking for without further nesting.
transform: snx.nxtransformations.Transform = log[()] # type: ignore[assignment]

if transform.transformation_type != 'rotation':
raise ValueError(
"Expected the instrument angle at detector_tank_angle to be a rotation,"
f" got '{transform.transformation_type}' instead.'"
)
return transform.value # type: ignore[return-value]


# See https://github.com/scipp/ess/issues/120
def moderator_class_for_source() -> NeXusClass[snx.NXsource]:
"""Select NXmoderator as the source."""
return NeXusClass[snx.NXsource](snx.NXmoderator)


def load_analyzer_for_detector(
Expand Down
5 changes: 5 additions & 0 deletions packages/essspectroscopy/tests/bifrost/live_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
lookup_table_simulation,
simulated_elastic_incoherent_with_phonon,
)
from ess.bifrost.io.mcstas import load_sample_angle
from ess.bifrost.live import BifrostQCutWorkflow, CutAxis, CutAxis1, CutAxis2, CutData
from ess.spectroscopy.types import (
Filename,
Expand All @@ -35,6 +36,10 @@ def qcut_workflow(
self, simulation_detector_names: list[NeXusDetectorName]
) -> sciline.Pipeline:
workflow = BifrostQCutWorkflow(simulation_detector_names)
# The test file is from a simulation and
# looks slightly different from production files.
workflow.insert(load_sample_angle)

workflow[Filename[SampleRun]] = simulated_elastic_incoherent_with_phonon()
workflow[LookupTableFilename] = lookup_table_simulation()
workflow[LookupTableRelativeErrorThreshold] = {
Expand Down
Loading