Source code for scippneutron.core.conversions

# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
# @author Jan-Lukas Wynen

from collections.abc import Callable

import scipp as sc

from ..conversion import graph as _graphs


def _inelastic_scatter_graph(energy_mode):
    inelastic_graph_factory = {
        'direct_inelastic': _graphs.tof.direct_inelastic,
        'indirect_inelastic': _graphs.tof.indirect_inelastic,
    }
    return {
        **_graphs.beamline.beamline(scatter=True),
        **inelastic_graph_factory[energy_mode](start='tof'),
    }


def _reachable_by(target, graph):
    return any(
        target == targets if isinstance(targets, str) else target in targets
        for targets in graph.keys()
    )


def _elastic_scatter_graph(origin, target):
    scatter_graph_kinematics = _graphs.beamline.beamline(scatter=True)
    if _reachable_by(target, scatter_graph_kinematics):
        return dict(scatter_graph_kinematics)
    return {**scatter_graph_kinematics, **_graphs.tof.elastic(origin)}


def _scatter_graph(origin, target, energy_mode):
    graph = (
        _elastic_scatter_graph(origin, target)
        if energy_mode == 'elastic'
        else _inelastic_scatter_graph(energy_mode)
    )
    return graph


def conversion_graph(
    origin: str, target: str, scatter: bool, energy_mode: str
) -> dict[str | tuple[str], Callable]:
    """
    Get a conversion graph for given parameters.

    The graph can be used with `scipp.transform_coords`.

    :param origin: Name of the input coordinate.
    :param target: Name of the output coordinate.
    :param scatter: Choose whether to use scattering or non-scattering conversions.
    :param energy_mode: Select if energy is conserved. One of `'elastic'`,
                        `'direct_inelastic'`, `'indirect_inelastic'`.
    :return: Conversion graph.
    :seealso: :py:func:`scippneutron.convert`,
              :py:func:`scippneutron.deduce_conversion_graph`.
    """

    # Results are copied to ensure users do not modify the global dictionaries.
    if scatter:
        return dict(_scatter_graph(origin, target, energy_mode))
    else:
        return {
            **_graphs.beamline.beamline(scatter=False),
            **_graphs.tof.kinematic(start='tof'),
        }


def _find_inelastic_inputs(data):
    return [name for name in ('incident_energy', 'final_energy') if name in data.coords]


def _deduce_energy_mode(data, origin, target):
    inelastic_inputs = _find_inelastic_inputs(data)
    if target == 'energy_transfer':
        if len(inelastic_inputs) > 1:
            raise RuntimeError(
                "Data contains coords for incident *and* final energy, cannot have "
                "both for inelastic scattering."
            )
        if len(inelastic_inputs) == 0:
            raise RuntimeError(
                "Data contains neither coords for incident nor for final energy, this "
                "does not appear to be inelastic-scattering data, cannot convert to "
                "energy transfer."
            )
        return {
            'incident_energy': 'direct_inelastic',
            'final_energy': 'indirect_inelastic',
        }[inelastic_inputs[0]]

    if 'energy' in (origin, target):
        if inelastic_inputs:
            raise RuntimeError(
                f"Data contains coords for inelastic scattering "
                f"({inelastic_inputs}) but conversion with elastic energy requested. "
                f"This is not implemented."
            )
    return 'elastic'


def deduce_conversion_graph(
    data: sc.DataArray | sc.Dataset, origin: str, target: str, scatter: bool
) -> dict[str | tuple[str], Callable]:
    """
    Get the conversion graph used by :py:func:`scippneutron.convert`
    when called with identical arguments.

    :param data: Input data.
    :param origin: Name of the input coordinate.
    :param target: Name of the output coordinate.
    :param scatter: Choose whether to use scattering or non-scattering conversions.
    :return: Conversion graph.
    :seealso: :py:func:`scippneutron.convert`, :py:func:`scippneutron.conversion_graph`.
    """
    return conversion_graph(
        origin, target, scatter, _deduce_energy_mode(data, origin, target)
    )


[docs] def convert( data: sc.DataArray | sc.Dataset, origin: str, target: str, scatter: bool ) -> sc.DataArray | sc.Dataset: """ Perform a unit conversion from the given origin unit to target. See the documentation page on "Coordinate Transformations" (https://scipp.github.io/scippneutron/user-guide/coordinate-transformations.html) for more details. :param data: Input data. :param origin: Name of the input coordinate. :param target: Name of the output coordinate. :param scatter: Choose whether to use scattering or non-scattering conversions. :return: A new scipp.DataArray or scipp.Dataset with the new coordinate. :seealso: :py:func:`scippneutron.deduce_conversion_graph` and :py:func:`scippneutron.conversion_graph` to inspect the possible conversions. """ graph = deduce_conversion_graph(data, origin, target, scatter) try: converted = data.transform_coords(target, graph=graph) except KeyError as err: if err.args[0] == target: raise RuntimeError( f"No viable conversion from '{origin}' to '{target}' " f"with scatter={scatter}." ) from None raise RuntimeError( f"Missing coordinate '{err.args[0]}' for conversion " f"from '{origin}' to '{target}'" ) from None return converted