Source code for ess.isissans.mantidio
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2024 Scipp contributors (https://github.com/scipp)
"""
File loading functions for ISIS data using Mantid.
"""
from typing import NewType, NoReturn
import sciline
import scipp as sc
import scippneutron as scn
from scipp.constants import g
from ess.sans.types import DirectBeam, DirectBeamFilename, Filename, RunType, SampleRun
from .io import CalibrationFilename, LoadedFileContents
try:
import mantid.api as _mantid_api
import mantid.simpleapi as _mantid_simpleapi
from mantid.api import MatrixWorkspace
except ModuleNotFoundError:
# Catch runtime usages of Mantid
class _MantidFallback:
def __getattr__(self, name: str) -> NoReturn:
raise ImportError(
'Mantid is required to use `sans.isis.mantidio` but is not installed'
) from None
_mantid_api = _MantidFallback()
_mantid_simpleapi = _MantidFallback
# Needed for type annotations
MatrixWorkspace = object
Period = NewType('Period', int | None)
"""Period number of the events."""
CalibrationWorkspace = NewType('CalibrationWorkspace', MatrixWorkspace | None)
[docs]
class DataWorkspace(sciline.Scope[RunType, MatrixWorkspace], MatrixWorkspace):
"""Workspace containing data"""
def _make_detector_info(ws: MatrixWorkspace) -> sc.DataGroup:
det_info = scn.mantid.make_detector_info(ws, 'spectrum')
return sc.DataGroup(det_info.coords)
def _get_detector_ids(ws: DataWorkspace[SampleRun]) -> sc.Variable:
det_info = _make_detector_info(ws)
dim = 'spectrum'
da = sc.DataArray(det_info['detector'], coords={dim: det_info[dim]})
da = sc.sort(da, dim) # sort by spectrum index
if not sc.identical(
da.coords[dim],
sc.arange('detector', da.sizes['detector'], dtype='int32', unit=None),
):
raise ValueError("Spectrum-detector mapping is not 1:1, this is not supported.")
return da.data.rename_dims(detector='spectrum')
[docs]
def load_calibration(filename: CalibrationFilename) -> CalibrationWorkspace:
ws = _mantid_simpleapi.Load(Filename=str(filename), StoreInADS=False)
return CalibrationWorkspace(ws)
[docs]
def load_direct_beam(filename: DirectBeamFilename) -> DirectBeam:
dg = scn.load_with_mantid(
filename=filename,
mantid_alg="LoadRKH",
mantid_args={"FirstColumnValue": "Wavelength"},
)
da = dg['data']
del da.coords['spectrum']
return DirectBeam(da)
[docs]
def from_data_workspace(
ws: DataWorkspace[RunType], calibration: CalibrationWorkspace
) -> LoadedFileContents[RunType]:
if calibration is not None:
_mantid_simpleapi.CopyInstrumentParameters(
InputWorkspace=calibration, OutputWorkspace=ws, StoreInADS=False
)
up = ws.getInstrument().getReferenceFrame().vecPointingUp()
dg = scn.from_mantid(ws)
det_ids = _get_detector_ids(ws)
# In some instruments (e.g. SANS2D), some pixels are used for other purposes (e.g.
# live acquisition). They have no detector ids, so we exclude them from the data.
for dim, shape in det_ids.sizes.items():
dg['data'] = dg['data'][dim, :shape]
dg['data'] = dg['data'].squeeze()
if (dg['data'].bins is not None) and ('tof' in dg['data'].coords):
del dg['data'].coords['tof']
dg['data'].coords['detector_id'] = det_ids
dg['data'].coords['gravity'] = sc.vector(value=-up) * g
return LoadedFileContents[RunType](dg)
[docs]
def load_run(filename: Filename[RunType], period: Period) -> DataWorkspace[RunType]:
loaded = _mantid_simpleapi.Load(
Filename=str(filename), LoadMonitors=True, StoreInADS=False
)
if isinstance(loaded, _mantid_api.Workspace):
# A single workspace
data_ws = loaded
else:
# Separate data and monitor workspaces
data_ws = loaded.OutputWorkspace
if isinstance(data_ws, _mantid_api.WorkspaceGroup):
if period is None:
raise ValueError(
f'Needs {Period} to be set to know what '
'section of the event data to load'
)
data_ws = data_ws.getItem(period)
data_ws.setMonitorWorkspace(loaded.MonitorWorkspace.getItem(period))
else:
data_ws.setMonitorWorkspace(loaded.MonitorWorkspace)
return DataWorkspace[RunType](data_ws)
providers = (
from_data_workspace,
load_calibration,
load_direct_beam,
load_run,
)