# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2025 Scipp contributors (https://github.com/scipp)
import scipp as sc
from scipp.constants import pi
from scippneutron._utils import elem_dtype
from scippneutron.conversion import graph
from scippneutron.conversion.tof import wavelength_from_tof
from scippnexus import NXsample, NXsource
from ess.reduce.nexus.types import DetectorBankSizes, Position
from ..reflectometry.conversions import reflectometry_q
from ..reflectometry.types import (
CoordTransformationGraph,
DetectorRotation,
RunType,
SampleRotation,
)
[docs]
def reflectometry_q_x(
wavelength: sc.Variable, theta: sc.Variable, sample_rotation: sc.Variable
) -> sc.Variable:
"""
Compute momentum transfer in off-specular direction.
.. math::
Q_x = \\frac{2 \\pi}{\\lambda} (cos(\\theta_o) - cos(\\theta_i))
Where :math:`\\theta_o` is the reflection angle (:func:`theta`)
and :math:`\\theta_i` is the incident angle on the sample surface.
Note that here we assume the incident angle is equal to ``sample_rotation``.
Source:
`Frédéric Ott, "Off-specular data representations in neutron reflectivity" <https://doi.org/10.1107/S0021889811002858>`_
Parameters
----------
wavelength:
Wavelength values for the events.
theta:
Angle of reflection for the events.
sample_rotation:
Angle of incidence.
Returns
-------
:
Qx-values.
"""
dtype = elem_dtype(wavelength)
c = (2 * pi).astype(dtype)
return (
c
* (
sc.cos(theta.astype(dtype, copy=False))
- sc.cos(sample_rotation.to(unit=theta.unit, dtype=dtype))
)
/ wavelength
)
[docs]
def theta(
divergence_angle: sc.Variable,
sample_rotation: sc.Variable,
):
'''
Angle of reflection.
Computes the angle between the scattering direction of
the neutron and the sample surface.
Parameters
------------
divergence_angle:
Divergence angle of the scattered beam.
sample_rotation:
Rotation of the sample from to its zero position.
Returns
-----------
The reflection angle of the neutron.
'''
return divergence_angle + sample_rotation.to(
unit=divergence_angle.unit, dtype='float64'
)
[docs]
def divergence_angle(
position: sc.Variable,
sample_position: sc.Variable,
detector_rotation: sc.Variable,
):
"""
Angle between the scattering ray and
the ray that travels parallel to the sample surface
when the sample rotation is zero.
Parameters
------------
position:
Detector position where the neutron was detected.
sample_position:
Position of the sample.
detector_rotation:
Rotation of the detector from its zero position.
Returns
----------
The divergence angle of the scattered beam.
"""
p = position - sample_position.to(unit=position.unit)
return sc.atan2(y=p.fields.x, x=p.fields.z) - detector_rotation.to(
unit='rad', dtype='float64'
)
[docs]
def add_coords(
da: sc.DataArray,
graph: dict,
) -> sc.DataArray:
"Adds scattering coordinates to the raw detector data."
return da.transform_coords(
(
"wavelength",
"theta",
"divergence_angle",
"Q",
"Qx",
"L1",
"L2",
"blade",
"wire",
"strip",
"z_index",
"sample_rotation",
"detector_rotation",
"sample_size",
),
graph,
rename_dims=False,
keep_intermediate=False,
keep_aliases=False,
)
providers = (coordinate_transformation_graph,)