diff --git a/packages/essdiffraction/docs/api-reference/index.md b/packages/essdiffraction/docs/api-reference/index.md index dbc995c8b..c93339e61 100644 --- a/packages/essdiffraction/docs/api-reference/index.md +++ b/packages/essdiffraction/docs/api-reference/index.md @@ -1,8 +1,25 @@ # API Reference +## ESSdiffraction + +### Submodules + +```{eval-rst} +.. currentmodule:: ess.diffraction + +.. autosummary:: + :toctree: ../generated/modules + :template: module-template.rst + :recursive: + + pdf + peaks +``` + ## ESSpowder ### Module Attributes + ```{eval-rst} .. currentmodule:: ess.powder @@ -37,9 +54,7 @@ grouping logging masking - peaks smoothing - transform types ``` @@ -47,7 +62,6 @@ ### Workflows - ```{eval-rst} .. currentmodule:: ess.dream @@ -103,7 +117,6 @@ ### Workflows - ```{eval-rst} .. currentmodule:: ess.beer @@ -144,7 +157,6 @@ ### Workflows - ```{eval-rst} .. currentmodule:: ess.magic diff --git a/packages/essdiffraction/docs/conf.py b/packages/essdiffraction/docs/conf.py index 29988706d..f40fa0c77 100644 --- a/packages/essdiffraction/docs/conf.py +++ b/packages/essdiffraction/docs/conf.py @@ -249,8 +249,12 @@ # build already tests if those plots can be made. # So we simply disable plots in doctests. doctest_global_setup = """ +# Private import because of https://github.com/mctools/ncrystal/issues/361 +from NCrystal._common import set_ncrystal_print_fct import numpy as np +set_ncrystal_print_fct(lambda *args, **kwargs: None) + try: import scipp as sc diff --git a/packages/essdiffraction/src/ess/diffraction/__init__.py b/packages/essdiffraction/src/ess/diffraction/__init__.py index d5c25d527..76981bcd7 100644 --- a/packages/essdiffraction/src/ess/diffraction/__init__.py +++ b/packages/essdiffraction/src/ess/diffraction/__init__.py @@ -7,9 +7,13 @@ import importlib.metadata +from . import pdf + try: __version__ = importlib.metadata.version("essdiffraction") except importlib.metadata.PackageNotFoundError: __version__ = "0.0.0" del importlib + +__all__ = ["pdf"] diff --git a/packages/essdiffraction/src/ess/diffraction/pdf.py b/packages/essdiffraction/src/ess/diffraction/pdf.py new file mode 100644 index 000000000..facda0cf3 --- /dev/null +++ b/packages/essdiffraction/src/ess/diffraction/pdf.py @@ -0,0 +1,276 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 Scipp contributors (https://github.com/scipp) + +r"""Pair distribution functions and related functions. + +Here, we use the following definitions and naming convention: + +.. list-table:: + :header-rows: 1 + + * - Name + - Definition + * - :func:`Pair correlation function ` + - :math:`G(r) = \frac{2}{\pi} \int_o^\infty\, Q (S(Q) - 1) \sin(Qr) \text{d}Q` + * - :func:`Pair distribution function ` + - :math:`g(r) = 1 + \frac{G(r)}{4 \pi \rho r}` + * - :func:`Radial distribution function ` + - :math:`\text{RDF}(r) = 4\pi r^2 \rho g(r)` + * - :func:`Linearized radial distribution function ` + - :math:`T(r) = \text{RDF}(r) / r` + * - :func:`Running coordination number ` + - :math:`C(r) = \int_0^r\, \text{RDF}(r') \text{d}r'` + +Where :math:`\rho` is the atomic density. + +The implementations provided here are defined in terms of each other and must be called +in the order shown in the table. +""" # noqa: E501 + +import scipp as sc + +from ess.reduce.uncertainty import UncertaintyBroadcastMode, broadcast_uncertainties + + +def pair_correlation_function( + s: sc.DataArray, + r: sc.Variable, + *, + uncertainty_broadcast_mode: UncertaintyBroadcastMode = UncertaintyBroadcastMode.drop, # noqa: E501 + return_covariances: bool = False, +) -> sc.DataArray | tuple[sc.DataArray, sc.DataArray]: + """Compute the pair correlation function from a structure factor. + + Computes the pair correlation function :math:`G(r)` from the overall + scattering function :math:`S(Q)`. + See `Review: Pair distribution functions from neutron total scattering for the study of local structure in disordered materials `_ + for the definition of :math:`G(r)` or :mod:`ess.diffraction.pdf`. + (Note, in the reference, the pair correlation function is denoted as :math:`D(r)`, + but since :math:`G(r)` is the more common name, that is what is used here). + + The inputs to the function are: + + * A histogram representing :math:`S(Q)` with :math:`N` bins on a bin-edge grid with + :math:`N+1` edges :math:`Q_j` for :math:`j=0\\ldots N`. + * The bin-edge grid over :math:`r`. The output histogram representing :math:`G(r)` + will be computed on this grid. + + In each output bin, the output is computed as: + + .. math:: + G_{i+\\frac{1}{2}} &= \\frac{2}{\\pi(r_{i+1}-r_i)} \\int_{r_i}^{r_{i+1}} \\int_{0}^\\infty (S(Q) - 1) Q \\sin(Q r) dQ \\ dr \\\\ + &\\approx \\frac{2}{\\pi(r_{i+1}-r_i)} \\sum_{j=0}^{N-1} (S(Q)_{j+\\frac{1}{2}} - 1) (\\cos(\\bar{Q}_{j+\\frac{1}{2}} r_{i})-\\cos(\\bar{Q}_{j+\\frac{1}{2}} r_{i+1})) \\Delta Q_{j+\\frac{1}{2}} + + Note that in the above expression the subscript :math:`_{j+\\frac{1}{2}}` is used + to denote quantities belonging to the :math:`j^\\text{th}` bin of a histogram, + :math:`\\bar{Q}_{j+\\frac{1}{2}} = \\frac{Q_j + Q_{j+1}}{2}` and + :math:`\\Delta Q_{j+\\frac{1}{2}} = Q_{j+1} - Q_{j}`. + + Parameters + ---------- + s: + 1D DataArray representing :math:`S(Q)` with + a bin-edge coordinate called ``'Q'``. + r: + 1D array, bin-edges of output grid. + uncertainty_broadcast_mode: UncertaintyBroadcastMode, + Choose how uncertainties in S(Q) are broadcast to G(r). + Defaults to ``UncertaintyBroadcastMode.drop``. + return_covariances: + If true the second output of the function will be a 2D array representing + the covariance matrix of the entries in the first output. + + Returns + ------- + g: scipp.DataArray + 1D array representing :math:`G(r)` with a bin-edge coordinate called + ``'r'`` that is the provided output grid. + cov: scipp.DataArray + 2D array representing the covariance matrix of the entries in ``g``. + Only returned if ``return_covariances=True``. + + See Also + -------- + ess.diffraction.pdf: + Module with related functions. + """ # noqa: E501 + q = s.coords['Q'] + qm = sc.midpoints(q) + dq = q[1:] - q[:-1] + dr = r[1:] - r[:-1] + + v = sc.cos(qm * r * sc.scalar(1, unit='rad')) + v = v[r.dim, :-1] - v[r.dim, 1:] + + ioq = (s - sc.scalar(1.0, unit=s.unit)) * dq + mat_ioq = broadcast_uncertainties(ioq, prototype=v, mode=uncertainty_broadcast_mode) + c = 2 / sc.constants.pi / dr + g = c * (v * mat_ioq).sum(q.dim) + g = sc.DataArray(g.data, coords={'r': r}) + if return_covariances: + cov_g = _covariance_of_matrix_vector_product(c * v, ioq) + cov_g = sc.DataArray( + cov_g, coords={d: r.rename_dims({r.dim: d}) for d in cov_g.dims} + ) + return g, cov_g + return g + + +def pair_distribution_function( + g: sc.DataArray, *, atomic_density: sc.Variable +) -> sc.DataArray: + r"""Compute the pair distribution function from a pair correlation function. + + Computes the pair distribution function + + .. math:: + + g(r) = 1 + \frac{G(r)}{4 \pi \rho r} + + where :math:`G(r)` is the :func:`pair_correlation_function` + and :math:`\rho` is the atomic density. + + :math:`g` and :math:`G` are both defined on bin-edges in :math:`r`. + The :math:`r` variable in the denominator is computed on the + :func:`midpoints ` of ``r``. + + Parameters + ---------- + g: + :func:`Pair correlation function ` :math:`G(r)`. + atomic_density: + The atomic density :math:`\rho` of the material. + + Returns + ------- + g: scipp.DataArray + 1D array representing :math:`g(r)`. + + See Also + -------- + ess.diffraction.pdf: + Module with related functions. + """ + return 1 + g / (4 * sc.constants.pi * atomic_density * sc.midpoints(g.coords['r'])) + + +def radial_distribution_function( + g: sc.DataArray, *, atomic_density: sc.Variable +) -> sc.DataArray: + r"""Compute the radial distribution function from a pair distribution function. + + Computes the radial distribution function + + .. math:: + + \text{RDF}(r) = 4 \pi r^2 \rho g(r) + + where :math:`g(r)` is the :func:`pair_distribution_function` + and :math:`\rho` is the atomic density. + + :math:`\text{RDF}` and :math:`g` are both defined on bin-edges in :math:`r`. + The :math:`r^2` factor is computed on the + :func:`midpoints ` of ``r``. + + Parameters + ---------- + g: + :func:`Pair distribution function ` :math:`g(r)`. + atomic_density: + The atomic density :math:`\rho` of the material. + + Returns + ------- + rdf: scipp.DataArray + 1D array representing :math:`\text{RDF}(r)`. + + See Also + -------- + ess.diffraction.pdf: + Module with related functions. + """ + return 4 * sc.constants.pi * atomic_density * sc.midpoints(g.coords['r']) ** 2 * g + + +def linearized_radial_distribution_function(rdf: sc.DataArray) -> sc.DataArray: + r"""Compute the linearized radial distribution function + from a radial distribution function. + + Computes the linearized radial distribution function + + .. math:: + + \text{T}(r) = \frac{\text{RDF}(r)}{r} + + where :math:`\text{RDF}(r)` is the :func:`radial_distribution_function`. + + :math:`\text{RDF}` and :math:`T` are both defined on bin-edges in :math:`r`. + The :math:`r` variable in the denominator is computed on the + :func:`midpoints ` of ``r``. + + Parameters + ---------- + rdf: + :func:`Radial distribution function ` + :math:`\text{RDF}(r)`. + + Returns + ------- + t: scipp.DataArray + 1D array representing :math:`T(r)`. + + See Also + -------- + ess.diffraction.pdf: + Module with related functions. + """ + return rdf / sc.midpoints(rdf.coords['r']) + + +def running_coordination_number(rdf: sc.DataArray) -> sc.DataArray: + r"""Compute the running coordination number from a radial distribution function. + + Computes the running coordination number + + .. math:: + + C(r) = \int_0^r\, \text{RDF}(r') \text{d}r' + + where :math:`\text{RDF}(r)` is the :func:`radial_distribution_function`. + + Parameters + ---------- + rdf: + :func:`Radial distribution function ` + :math:`\text{RDF}(r)`. + + Returns + ------- + c: scipp.DataArray + 1D array representing :math:`C(r)`. + + See Also + -------- + ess.diffraction.pdf: + Module with related functions. + """ + r = rdf.coords['r'] + if not sc.issorted(r, dim=r.dim, order='ascending'): + raise ValueError('The bin-edges in `r` must be sorted in ascending order.') + + return sc.cumsum(rdf * (r[1:] - r[:-1])) + + +def _covariance_of_matrix_vector_product( + A: sc.typing.VariableLike, v: sc.typing.VariableLike +) -> sc.Variable: + if A.variances is not None: + raise ValueError('The expression is not valid if the matrix has variances.') + v = sc.variances(v) # type: ignore[arg-type] # faulty annotation in variances()? + if A.dims[1] != v.dim: + A = A.transpose() + cov = (sc.sqrt(v) * A).values + cov = cov @ cov.T + return sc.array( + dims=[A.dims[0], A.dims[0] + '_2'], values=cov, unit=v.unit * A.unit**2 + ) diff --git a/packages/essdiffraction/src/ess/diffraction/peaks.py b/packages/essdiffraction/src/ess/diffraction/peaks.py index b3bf2bf96..77753b178 100644 --- a/packages/essdiffraction/src/ess/diffraction/peaks.py +++ b/packages/essdiffraction/src/ess/diffraction/peaks.py @@ -19,7 +19,7 @@ def dspacing_peaks_from_cif(cif, intensity_threshold=None, **kwargs) -> sc.DataA (https://www.crystallography.net/cod/) or the Materials Project (https://www.materialsproject.org/). - For more details, see :py:`ncrystal.cifutils.CIFSource`. + For more details, see ``ncrystal.cifutils.CIFSource``. intensity_threshold: The minimum intensity of the peaks to return. diff --git a/packages/essdiffraction/src/ess/powder/__init__.py b/packages/essdiffraction/src/ess/powder/__init__.py index 619854710..faa956396 100644 --- a/packages/essdiffraction/src/ess/powder/__init__.py +++ b/packages/essdiffraction/src/ess/powder/__init__.py @@ -14,7 +14,6 @@ grouping, masking, smoothing, - transform, workflow, ) from .correction import RunNormalization @@ -50,7 +49,6 @@ "masking", "providers", "smoothing", - "transform", "with_pixel_mask_filenames", "workflow", ] diff --git a/packages/essdiffraction/src/ess/powder/transform.py b/packages/essdiffraction/src/ess/powder/transform.py deleted file mode 100644 index 3207c5e86..000000000 --- a/packages/essdiffraction/src/ess/powder/transform.py +++ /dev/null @@ -1,89 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2023 Scipp contributors (https://github.com/scipp) -"""Signal transformation algorithms for powder diffraction.""" - -import scipp as sc - -from ess.reduce.uncertainty import UncertaintyBroadcastMode, broadcast_uncertainties - - -def _covariance_of_matrix_vector_product(A, v): - if A.variances is not None: - raise ValueError('The expression is not valid if the matrix has variances.') - v = sc.variances(v) - if A.dims[1] != v.dim: - A = A.transpose() - cov = (sc.sqrt(v) * A).values - cov = cov @ cov.T - return sc.array( - dims=[A.dims[0], A.dims[0] + '_2'], values=cov, unit=v.unit * A.unit**2 - ) - - -def compute_pdf_from_structure_factor( - s: sc.DataArray, - r: sc.Variable, - *, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.drop, - return_covariances=False, -) -> sc.DataArray: - ''' - Compute a pair distribution function from a structure factor. - - Computes the pair distribution function :math:`G(r)` as defined in - `Review: Pair distribution functions from neutron total scattering for the study of local structure in disordered materials `_ (note, in the reference, the pair distribution function is denoted :math:`D(r)`, but since :math:`G(r)` is the more common name, that is what is used here). - from the overall scattering function :math:`S(Q)`. - - The inputs to the algorithm are: - - * A histogram representing :math:`S(Q)` with :math:`N` bins on a bin-edge grid with :math:`N+1` edges :math:`Q_j` for :math:`j=0\\ldots N`. - * The bin-edge grid over :math:`r` the output histogram representing :math:`G(r)` will be computed on. - - In each output bin, the output is computed as: - - .. math:: - G_{i+\\frac{1}{2}} &= \\frac{2}{\\pi(r_{i+1}-r_i)} \\int_{r_i}^{r_{i+1}} \\int_{0}^\\infty (S(Q) - 1) Q \\sin(Q r) dQ \\ dr \\\\ - &\\approx \\frac{2}{\\pi(r_{i+1}-r_i)} \\sum_{j=0}^{N-1} (S(Q)_{j+\\frac{1}{2}} - 1) (\\cos(\\bar{Q}_{j+\\frac{1}{2}} r_{i})-\\cos(\\bar{Q}_{j+\\frac{1}{2}} r_{i+1})) \\Delta Q_{j+\\frac{1}{2}} - - Note that in the above expression the subscript :math:`_{j+\\frac{1}{2}}` is used to denote - quantities belonging to the :math:`j^\\text{th}` bin of a histogram, :math:`\\bar{Q}_{j+\\frac{1}{2}} = \\frac{Q_j + Q_{j+1}}{2}` and :math:`\\Delta Q_{j+\\frac{1}{2}} = Q_{j+1} - Q_{j}`. - - Parameters - ---------- - s: - 1D DataArray representing :math:`S(Q)` with a bin-edge coordinate called :math:`Q` - r: - 1D array, bin-edges of output grid - uncertainty_broadcast_mode: UncertaintyBroadcastMode, - Choose how uncertainties in S(Q) are broadcast to G(r). - Defaults to ``UncertaintyBroadcastMode.drop``. - return_covariances: - bool, if True the second output of the function will be a 2D array representing the covariance - matrix of the entries in the first output. - - Returns - ------- - : - 1D DataArray representing :math:`G(r)` with a bin-edge coordinate called :math:`r` that is the provided output grid, and optionally a 2D DataArray representing the covariances of :math:`G(r)`. - - ''' # noqa: E501 - q = s.coords['Q'] - qm = sc.midpoints(q) - dq = q[1:] - q[:-1] - dr = r[1:] - r[:-1] - - v = sc.cos(qm * r * sc.scalar(1, unit='rad')) - v = v[r.dim, :-1] - v[r.dim, 1:] - - ioq = (s - sc.scalar(1.0, unit=s.unit)) * dq - mat_ioq = broadcast_uncertainties(ioq, prototype=v, mode=uncertainty_broadcast_mode) - c = 2 / sc.constants.pi / dr - g = c * (v * mat_ioq).sum(q.dim) - g = sc.DataArray(g.data, coords={'r': r}) - if return_covariances: - cov_g = _covariance_of_matrix_vector_product(c * v, ioq) - cov_g = sc.DataArray( - cov_g, coords={d: r.rename_dims({r.dim: d}) for d in cov_g.dims} - ) - return g, cov_g - return g diff --git a/packages/essdiffraction/tests/diffraction/pdf_test.py b/packages/essdiffraction/tests/diffraction/pdf_test.py new file mode 100644 index 000000000..9065e984d --- /dev/null +++ b/packages/essdiffraction/tests/diffraction/pdf_test.py @@ -0,0 +1,164 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 Scipp contributors (https://github.com/scipp) +import pytest +import scipp as sc +import scipp.testing +from ess.diffraction.pdf import ( + _covariance_of_matrix_vector_product, + linearized_radial_distribution_function, + pair_correlation_function, + pair_distribution_function, + radial_distribution_function, + running_coordination_number, +) + + +def test_pdf_structure_factor_needs_q_coord() -> None: + da = sc.DataArray(sc.ones(sizes={'Q': 3})) + r = sc.array(dims='r', values=[2, 3, 4, 5.0]) + + with pytest.raises(KeyError): + pair_correlation_function( + da, + r, + ) + + +def test_pair_correlation_function() -> None: + da = sc.DataArray( + sc.ones(sizes={'Q': 3}), + coords={'Q': sc.array(dims='Q', values=[0, 1, 2, 3.0], unit='1/angstrom')}, + ) + da.variances = da.data.values.copy() + r = sc.array(dims='r', values=[2, 3, 4, 5.0], unit='angstrom') + + G: sc.DataArray = pair_correlation_function( # type: ignore[assignment] + da, + r, + ) + assert G.data.unit == '1/angstrom^2' + sc.testing.assert_identical(G.coords['r'], r) + + +def test_pair_correlation_function_can_return_covariances() -> None: + da = sc.DataArray( + sc.ones(sizes={'Q': 3}), + coords={'Q': sc.array(dims='Q', values=[0, 1, 2, 3.0], unit='1/angstrom')}, + ) + da.variances = da.data.values.copy() + r = sc.array(dims='r', values=[2, 3, 4, 5.0], unit='angstrom') + + [G, cov] = pair_correlation_function(da, r, return_covariances=True) + assert G.data.unit == '1/angstrom^2' + assert cov.data.unit == '1/angstrom^4' + assert cov.ndim == 2 + + +def test_pair_correlation_function_result_unchanged() -> None: + # Note: bogus data + da = sc.DataArray( + sc.array(dims='Q', values=[1, 2, 4.0]), + coords={'Q': sc.array(dims='Q', values=[0, 1, 2, 3.0], unit='1/angstrom')}, + ) + r = sc.array(dims='r', values=[2, 3, 4, 5.0], unit='angstrom') + + G: sc.DataArray = pair_correlation_function( # type: ignore[assignment] + da, + r, + ) + sc.testing.assert_allclose( + G.data, + sc.array(dims='r', values=[-0.616322, 1.51907, -3.11757], unit='1/angstrom^2'), + rtol=sc.scalar(1e-5), + ) + + +def test_pair_distribution_function() -> None: + da = sc.DataArray( + sc.ones(sizes={'Q': 3}), + coords={'Q': sc.array(dims='Q', values=[0, 1, 2, 3.0], unit='1/angstrom')}, + ) + da.variances = da.data.values.copy() + r = sc.array(dims='r', values=[2, 3, 4, 5.0], unit='angstrom') + atomic_density = sc.scalar(1.0, unit='1/angstrom^3') + + G: sc.DataArray = pair_correlation_function(da, r) # type: ignore[assignment] + g = pair_distribution_function(G, atomic_density=atomic_density) + assert g.data.unit == '1' + sc.testing.assert_identical(G.coords['r'], r) + + +def test_radial_distribution_function() -> None: + da = sc.DataArray( + sc.ones(sizes={'Q': 3}), + coords={'Q': sc.array(dims='Q', values=[0, 1, 2, 3.0], unit='1/angstrom')}, + ) + da.variances = da.data.values.copy() + r = sc.array(dims='r', values=[2, 3, 4, 5.0], unit='angstrom') + atomic_density = sc.scalar(1.0, unit='1/angstrom^3') + + G: sc.DataArray = pair_correlation_function(da, r) # type: ignore[assignment] + g = pair_distribution_function(G, atomic_density=atomic_density) + rdf = radial_distribution_function(g, atomic_density=atomic_density) + assert rdf.data.unit == '1/angstrom' + sc.testing.assert_identical(G.coords['r'], r) + + +def test_linearized_radial_distribution_function() -> None: + da = sc.DataArray( + sc.ones(sizes={'Q': 3}), + coords={'Q': sc.array(dims='Q', values=[0, 1, 2, 3.0], unit='1/angstrom')}, + ) + da.variances = da.data.values.copy() + r = sc.array(dims='r', values=[2, 3, 4, 5.0], unit='angstrom') + atomic_density = sc.scalar(1.0, unit='1/angstrom^3') + + G: sc.DataArray = pair_correlation_function(da, r) # type: ignore[assignment] + g = pair_distribution_function(G, atomic_density=atomic_density) + rdf = radial_distribution_function(g, atomic_density=atomic_density) + T = linearized_radial_distribution_function(rdf) + assert T.data.unit == '1/angstrom^2' + sc.testing.assert_identical(T.coords['r'], r) + + +def test_running_coordination_number() -> None: + da = sc.DataArray( + sc.ones(sizes={'Q': 3}), + coords={'Q': sc.array(dims='Q', values=[0, 1, 2, 3.0], unit='1/angstrom')}, + ) + da.variances = da.data.values.copy() + r = sc.array(dims='r', values=[2, 4, 5, 6.0], unit='angstrom') + atomic_density = sc.scalar(1.0, unit='1/angstrom^3') + + G: sc.DataArray = pair_correlation_function(da, r) # type: ignore[assignment] + g = pair_distribution_function(G, atomic_density=atomic_density) + rdf = radial_distribution_function(g, atomic_density=atomic_density) + C = running_coordination_number(rdf) + assert C.data.unit == '1' + sc.testing.assert_identical(C.coords['r'], r) + sc.testing.assert_identical(C[0].value, rdf[0].value * 2) # *2 from bin size + sc.testing.assert_identical(C[1].value, rdf[0].value * 2 + rdf[1].value * 1) + + +def test_matrix_vector_covariance() -> None: + A = sc.array(dims='ab', values=[[1, 2], [3, 4]]) + v = sc.array(dims='b', values=[0, 0.0], variances=[1.0, 2.0]) + C = _covariance_of_matrix_vector_product(A, v) + a00, a01, a10, a11 = A.values.ravel() + v0, v1 = v.variances + sc.testing.assert_allclose( + C, + sc.array( + dims=['a', 'a_2'], + values=[ + [ + v0 * a00 * a00 + v1 * a01 * a01, + v0 * a00 * a10 + v1 * a01 * a11, + ], + [ + v0 * a10 * a00 + v1 * a11 * a01, + v0 * a10 * a10 + v1 * a11 * a11, + ], + ], + ), + ) diff --git a/packages/essdiffraction/tests/powder/transform_test.py b/packages/essdiffraction/tests/powder/transform_test.py deleted file mode 100644 index df2fcf3c7..000000000 --- a/packages/essdiffraction/tests/powder/transform_test.py +++ /dev/null @@ -1,86 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2023 Scipp contributors (https://github.com/scipp) -import pytest -import scipp as sc -import scipp.testing -from ess.powder.transform import ( - _covariance_of_matrix_vector_product, - compute_pdf_from_structure_factor, -) - - -def test_pdf_structure_factor_needs_q_coord(): - da = sc.DataArray(sc.ones(sizes={'Q': 3})) - r = sc.array(dims='r', values=[2, 3, 4, 5.0]) - with pytest.raises(KeyError): - compute_pdf_from_structure_factor( - da, - r, - ) - - -def test_pdf_structure_factor(): - da = sc.DataArray( - sc.ones(sizes={'Q': 3}), - coords={'Q': sc.array(dims='Q', values=[0, 1, 2, 3.0], unit='1/angstrom')}, - ) - da.variances = da.data.values.copy() - r = sc.array(dims='r', values=[2, 3, 4, 5.0], unit='angstrom') - v = compute_pdf_from_structure_factor( - da, - r, - ) - assert v.data.unit == '1/angstrom^2' - sc.testing.assert_identical(v.coords['r'], r) - - -def test_pdf_structure_factor_can_return_covariances(): - da = sc.DataArray( - sc.ones(sizes={'Q': 3}), - coords={'Q': sc.array(dims='Q', values=[0, 1, 2, 3.0], unit='1/angstrom')}, - ) - da.variances = da.data.values.copy() - r = sc.array(dims='r', values=[2, 3, 4, 5.0], unit='angstrom') - compute_pdf_from_structure_factor(da, r, return_covariances=True) - - -def test_pdf_structure_factor_result_unchanged(): - # Note: bogus data - da = sc.DataArray( - sc.array(dims='Q', values=[1, 2, 4.0]), - coords={'Q': sc.array(dims='Q', values=[0, 1, 2, 3.0], unit='1/angstrom')}, - ) - r = sc.array(dims='r', values=[2, 3, 4, 5.0], unit='angstrom') - v = compute_pdf_from_structure_factor( - da, - r, - ) - sc.testing.assert_allclose( - v.data, - sc.array(dims='r', values=[-0.616322, 1.51907, -3.11757], unit='1/angstrom^2'), - rtol=sc.scalar(1e-5), - ) - - -def test_matrix_vector_covariance(): - A = sc.array(dims='ab', values=[[1, 2], [3, 4]]) - v = sc.array(dims='b', values=[0, 0.0], variances=[1.0, 2.0]) - C = _covariance_of_matrix_vector_product(A, v) - a00, a01, a10, a11 = A.values.ravel() - v0, v1 = v.variances - sc.testing.assert_allclose( - C, - sc.array( - dims=['a', 'a_2'], - values=[ - [ - v0 * a00 * a00 + v1 * a01 * a01, - v0 * a00 * a10 + v1 * a01 * a11, - ], - [ - v0 * a10 * a00 + v1 * a11 * a01, - v0 * a10 * a10 + v1 * a11 * a11, - ], - ], - ), - )