Source code for ess.powder.calibration
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2024 Scipp contributors (https://github.com/scipp)
"""Tools for detector calibration."""
from __future__ import annotations
from collections.abc import Callable, ItemsView, Iterable, Iterator, KeysView, Mapping
import scipp as sc
import scipp.constants
from .types import CountsDspacing, SampleRun
[docs]
class OutputCalibrationData(Mapping[int, sc.Variable]):
r"""Calibration data for output ToF data.
Only one value is stored per coefficient.
This means that individual detector pixels are *not* resolved but merged
into average quantities.
This is a mapping :math:`M` from powers :math:`p` to coefficients :math:`c`
according to
.. math::
t = \sum_{(p, c) \in M}\, c d^p
where :math:`d` is d-spacing and :math:`t` is time-of-flight.
"""
[docs]
def __init__(
self,
coefficients: Mapping[int, sc.Variable] | Iterable[tuple[int | sc.Variable]],
) -> None:
self._coefficients = dict(coefficients)
def __getitem__(self, power: int) -> sc.Variable:
return self._coefficients[power]
def __iter__(self) -> Iterator[int]:
return iter(self._coefficients)
def __len__(self) -> int:
return len(self._coefficients)
[docs]
def keys(self) -> KeysView[int]:
return self._coefficients.keys()
[docs]
def items(self) -> ItemsView[int, sc.Variable]:
return self._coefficients.items()
def __str__(self) -> str:
return str(self._coefficients)
def __repr__(self) -> str:
return f"ScalarCalibrationData({self._coefficients})"
[docs]
def to_cif_units(self) -> OutputCalibrationData:
"""Convert to the units used in CIF pd_calib_d_to_tof."""
def unit(p: int) -> sc.Unit:
base = sc.Unit(f'us / (angstrom^{abs(p)})')
return sc.reciprocal(base) if p < 0 else base
return OutputCalibrationData({p: c.to(unit=unit(p)) for p, c in self.items()})
[docs]
def assemble_output_calibration(
data: CountsDspacing[SampleRun],
) -> OutputCalibrationData:
"""Construct output calibration data from average pixel positions."""
# Use nanmean because pixels without events have position=NaN.
average_l = sc.nanmean(data.coords["Ltotal"])
average_two_theta = sc.nanmean(data.coords["two_theta"])
difc = sc.to_unit(
2
* sc.constants.m_n
/ sc.constants.h
* average_l
* sc.sin(0.5 * average_two_theta),
unit='us / angstrom',
)
return OutputCalibrationData({1: difc})
providers = (assemble_output_calibration,)