Skip to content

Commit 0de08ab

Browse files
authored
Merge pull request #617 from scipp/more-pdf-functions
[ESSDIFFRACTION] Implement more PDF functions
2 parents bbe1525 + 5deb6cf commit 0de08ab

9 files changed

Lines changed: 466 additions & 183 deletions

File tree

packages/essdiffraction/docs/api-reference/index.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,25 @@
11
# API Reference
22

3+
## ESSdiffraction
4+
5+
### Submodules
6+
7+
```{eval-rst}
8+
.. currentmodule:: ess.diffraction
9+
10+
.. autosummary::
11+
:toctree: ../generated/modules
12+
:template: module-template.rst
13+
:recursive:
14+
15+
pdf
16+
peaks
17+
```
18+
319
## ESSpowder
420

521
### Module Attributes
22+
623
```{eval-rst}
724
.. currentmodule:: ess.powder
825
@@ -37,17 +54,14 @@
3754
grouping
3855
logging
3956
masking
40-
peaks
4157
smoothing
42-
transform
4358
types
4459
```
4560

4661
## DREAM
4762

4863
### Workflows
4964

50-
5165
```{eval-rst}
5266
.. currentmodule:: ess.dream
5367
@@ -103,7 +117,6 @@
103117

104118
### Workflows
105119

106-
107120
```{eval-rst}
108121
.. currentmodule:: ess.beer
109122
@@ -144,7 +157,6 @@
144157

145158
### Workflows
146159

147-
148160
```{eval-rst}
149161
.. currentmodule:: ess.magic
150162

packages/essdiffraction/docs/conf.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,12 @@
249249
# build already tests if those plots can be made.
250250
# So we simply disable plots in doctests.
251251
doctest_global_setup = """
252+
# Private import because of https://github.com/mctools/ncrystal/issues/361
253+
from NCrystal._common import set_ncrystal_print_fct
252254
import numpy as np
253255
256+
set_ncrystal_print_fct(lambda *args, **kwargs: None)
257+
254258
try:
255259
import scipp as sc
256260

packages/essdiffraction/src/ess/diffraction/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@
77

88
import importlib.metadata
99

10+
from . import pdf
11+
1012
try:
1113
__version__ = importlib.metadata.version("essdiffraction")
1214
except importlib.metadata.PackageNotFoundError:
1315
__version__ = "0.0.0"
1416

1517
del importlib
18+
19+
__all__ = ["pdf"]
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
# SPDX-License-Identifier: BSD-3-Clause
2+
# Copyright (c) 2026 Scipp contributors (https://github.com/scipp)
3+
4+
r"""Pair distribution functions and related functions.
5+
6+
Here, we use the following definitions and naming convention:
7+
8+
.. list-table::
9+
:header-rows: 1
10+
11+
* - Name
12+
- Definition
13+
* - :func:`Pair correlation function <pair_correlation_function>`
14+
- :math:`G(r) = \frac{2}{\pi} \int_o^\infty\, Q (S(Q) - 1) \sin(Qr) \text{d}Q`
15+
* - :func:`Pair distribution function <pair_distribution_function>`
16+
- :math:`g(r) = 1 + \frac{G(r)}{4 \pi \rho r}`
17+
* - :func:`Radial distribution function <radial_distribution_function>`
18+
- :math:`\text{RDF}(r) = 4\pi r^2 \rho g(r)`
19+
* - :func:`Linearized radial distribution function <linearized_radial_distribution_function>`
20+
- :math:`T(r) = \text{RDF}(r) / r`
21+
* - :func:`Running coordination number <running_coordination_number>`
22+
- :math:`C(r) = \int_0^r\, \text{RDF}(r') \text{d}r'`
23+
24+
Where :math:`\rho` is the atomic density.
25+
26+
The implementations provided here are defined in terms of each other and must be called
27+
in the order shown in the table.
28+
""" # noqa: E501
29+
30+
import scipp as sc
31+
32+
from ess.reduce.uncertainty import UncertaintyBroadcastMode, broadcast_uncertainties
33+
34+
35+
def pair_correlation_function(
36+
s: sc.DataArray,
37+
r: sc.Variable,
38+
*,
39+
uncertainty_broadcast_mode: UncertaintyBroadcastMode = UncertaintyBroadcastMode.drop, # noqa: E501
40+
return_covariances: bool = False,
41+
) -> sc.DataArray | tuple[sc.DataArray, sc.DataArray]:
42+
"""Compute the pair correlation function from a structure factor.
43+
44+
Computes the pair correlation function :math:`G(r)` from the overall
45+
scattering function :math:`S(Q)`.
46+
See `Review: Pair distribution functions from neutron total scattering for the study of local structure in disordered materials <https://www.sciencedirect.com/science/article/pii/S2773183922000374>`_
47+
for the definition of :math:`G(r)` or :mod:`ess.diffraction.pdf`.
48+
(Note, in the reference, the pair correlation function is denoted as :math:`D(r)`,
49+
but since :math:`G(r)` is the more common name, that is what is used here).
50+
51+
The inputs to the function are:
52+
53+
* A histogram representing :math:`S(Q)` with :math:`N` bins on a bin-edge grid with
54+
:math:`N+1` edges :math:`Q_j` for :math:`j=0\\ldots N`.
55+
* The bin-edge grid over :math:`r`. The output histogram representing :math:`G(r)`
56+
will be computed on this grid.
57+
58+
In each output bin, the output is computed as:
59+
60+
.. math::
61+
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 \\\\
62+
&\\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}}
63+
64+
Note that in the above expression the subscript :math:`_{j+\\frac{1}{2}}` is used
65+
to denote quantities belonging to the :math:`j^\\text{th}` bin of a histogram,
66+
:math:`\\bar{Q}_{j+\\frac{1}{2}} = \\frac{Q_j + Q_{j+1}}{2}` and
67+
:math:`\\Delta Q_{j+\\frac{1}{2}} = Q_{j+1} - Q_{j}`.
68+
69+
Parameters
70+
----------
71+
s:
72+
1D DataArray representing :math:`S(Q)` with
73+
a bin-edge coordinate called ``'Q'``.
74+
r:
75+
1D array, bin-edges of output grid.
76+
uncertainty_broadcast_mode: UncertaintyBroadcastMode,
77+
Choose how uncertainties in S(Q) are broadcast to G(r).
78+
Defaults to ``UncertaintyBroadcastMode.drop``.
79+
return_covariances:
80+
If true the second output of the function will be a 2D array representing
81+
the covariance matrix of the entries in the first output.
82+
83+
Returns
84+
-------
85+
G: scipp.DataArray
86+
1D array representing :math:`G(r)` with a bin-edge coordinate called
87+
``'r'`` that is the provided output grid.
88+
cov: scipp.DataArray
89+
2D array representing the covariance matrix of the entries in ``g``.
90+
Only returned if ``return_covariances=True``.
91+
92+
See Also
93+
--------
94+
ess.diffraction.pdf:
95+
Module with related functions.
96+
""" # noqa: E501
97+
q = s.coords['Q']
98+
qm = sc.midpoints(q)
99+
dq = q[1:] - q[:-1]
100+
dr = r[1:] - r[:-1]
101+
102+
v = sc.cos(qm * r * sc.scalar(1, unit='rad'))
103+
v = v[r.dim, :-1] - v[r.dim, 1:]
104+
105+
ioq = (s - sc.scalar(1.0, unit=s.unit)) * dq
106+
mat_ioq = broadcast_uncertainties(ioq, prototype=v, mode=uncertainty_broadcast_mode)
107+
c = 2 / sc.constants.pi / dr
108+
g = c * (v * mat_ioq).sum(q.dim)
109+
g = sc.DataArray(g.data, coords={'r': r})
110+
if return_covariances:
111+
cov_g = _covariance_of_matrix_vector_product(c * v, ioq)
112+
cov_g = sc.DataArray(
113+
cov_g, coords={d: r.rename_dims({r.dim: d}) for d in cov_g.dims}
114+
)
115+
return g, cov_g
116+
return g
117+
118+
119+
def pair_distribution_function(
120+
G: sc.DataArray, *, atomic_density: sc.Variable
121+
) -> sc.DataArray:
122+
r"""Compute the pair distribution function from a pair correlation function.
123+
124+
Computes the pair distribution function
125+
126+
.. math::
127+
128+
g(r) = 1 + \frac{G(r)}{4 \pi \rho r}
129+
130+
where :math:`G(r)` is the :func:`pair_correlation_function`
131+
and :math:`\rho` is the atomic density.
132+
133+
:math:`g` and :math:`G` are both defined on bin-edges in :math:`r`.
134+
The :math:`r` variable in the denominator is computed on the
135+
:func:`midpoints <scipp.midpoints>` of ``r``.
136+
137+
Parameters
138+
----------
139+
G:
140+
:func:`Pair correlation function <pair_correlation_function>` :math:`G(r)`.
141+
atomic_density:
142+
The atomic density :math:`\rho` of the material.
143+
144+
Returns
145+
-------
146+
g: scipp.DataArray
147+
1D array representing :math:`g(r)`.
148+
149+
See Also
150+
--------
151+
ess.diffraction.pdf:
152+
Module with related functions.
153+
"""
154+
return 1 + G / (4 * sc.constants.pi * atomic_density * sc.midpoints(G.coords['r']))
155+
156+
157+
def radial_distribution_function(
158+
g: sc.DataArray, *, atomic_density: sc.Variable
159+
) -> sc.DataArray:
160+
r"""Compute the radial distribution function from a pair distribution function.
161+
162+
Computes the radial distribution function
163+
164+
.. math::
165+
166+
\text{RDF}(r) = 4 \pi r^2 \rho g(r)
167+
168+
where :math:`g(r)` is the :func:`pair_distribution_function`
169+
and :math:`\rho` is the atomic density.
170+
171+
:math:`\text{RDF}` and :math:`g` are both defined on bin-edges in :math:`r`.
172+
The :math:`r^2` factor is computed on the
173+
:func:`midpoints <scipp.midpoints>` of ``r``.
174+
175+
Parameters
176+
----------
177+
g:
178+
:func:`Pair distribution function <pair_distribution_function>` :math:`g(r)`.
179+
atomic_density:
180+
The atomic density :math:`\rho` of the material.
181+
182+
Returns
183+
-------
184+
rdf: scipp.DataArray
185+
1D array representing :math:`\text{RDF}(r)`.
186+
187+
See Also
188+
--------
189+
ess.diffraction.pdf:
190+
Module with related functions.
191+
"""
192+
return 4 * sc.constants.pi * atomic_density * sc.midpoints(g.coords['r']) ** 2 * g
193+
194+
195+
def linearized_radial_distribution_function(rdf: sc.DataArray) -> sc.DataArray:
196+
r"""Compute the linearized radial distribution function
197+
from a radial distribution function.
198+
199+
Computes the linearized radial distribution function
200+
201+
.. math::
202+
203+
\text{T}(r) = \frac{\text{RDF}(r)}{r}
204+
205+
where :math:`\text{RDF}(r)` is the :func:`radial_distribution_function`.
206+
207+
:math:`\text{RDF}` and :math:`T` are both defined on bin-edges in :math:`r`.
208+
The :math:`r` variable in the denominator is computed on the
209+
:func:`midpoints <scipp.midpoints>` of ``r``.
210+
211+
Parameters
212+
----------
213+
rdf:
214+
:func:`Radial distribution function <radial_distribution_function>`
215+
:math:`\text{RDF}(r)`.
216+
217+
Returns
218+
-------
219+
t: scipp.DataArray
220+
1D array representing :math:`T(r)`.
221+
222+
See Also
223+
--------
224+
ess.diffraction.pdf:
225+
Module with related functions.
226+
"""
227+
return rdf / sc.midpoints(rdf.coords['r'])
228+
229+
230+
def running_coordination_number(rdf: sc.DataArray) -> sc.DataArray:
231+
r"""Compute the running coordination number from a radial distribution function.
232+
233+
Computes the running coordination number
234+
235+
.. math::
236+
237+
C(r) = \int_0^r\, \text{RDF}(r') \text{d}r'
238+
239+
where :math:`\text{RDF}(r)` is the :func:`radial_distribution_function`.
240+
241+
Parameters
242+
----------
243+
rdf:
244+
:func:`Radial distribution function <radial_distribution_function>`
245+
:math:`\text{RDF}(r)`.
246+
247+
Returns
248+
-------
249+
c: scipp.DataArray
250+
1D array representing :math:`C(r)`.
251+
252+
See Also
253+
--------
254+
ess.diffraction.pdf:
255+
Module with related functions.
256+
"""
257+
r = rdf.coords['r']
258+
if not sc.issorted(r, dim=r.dim, order='ascending'):
259+
raise ValueError('The bin-edges in `r` must be sorted in ascending order.')
260+
261+
return sc.cumsum(rdf * (r[1:] - r[:-1]))
262+
263+
264+
def _covariance_of_matrix_vector_product(
265+
A: sc.typing.VariableLike, v: sc.typing.VariableLike
266+
) -> sc.Variable:
267+
if A.variances is not None:
268+
raise ValueError('The expression is not valid if the matrix has variances.')
269+
v = sc.variances(v) # type: ignore[arg-type] # faulty annotation in variances()?
270+
if A.dims[1] != v.dim:
271+
A = A.transpose()
272+
cov = (sc.sqrt(v) * A).values
273+
cov = cov @ cov.T
274+
return sc.array(
275+
dims=[A.dims[0], A.dims[0] + '_2'], values=cov, unit=v.unit * A.unit**2
276+
)

packages/essdiffraction/src/ess/diffraction/peaks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def dspacing_peaks_from_cif(cif, intensity_threshold=None, **kwargs) -> sc.DataA
1919
(https://www.crystallography.net/cod/) or the Materials Project
2020
(https://www.materialsproject.org/).
2121
22-
For more details, see :py:`ncrystal.cifutils.CIFSource`.
22+
For more details, see ``ncrystal.cifutils.CIFSource``.
2323
2424
intensity_threshold:
2525
The minimum intensity of the peaks to return.

packages/essdiffraction/src/ess/powder/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
grouping,
1515
masking,
1616
smoothing,
17-
transform,
1817
workflow,
1918
)
2019
from .correction import RunNormalization
@@ -50,7 +49,6 @@
5049
"masking",
5150
"providers",
5251
"smoothing",
53-
"transform",
5452
"with_pixel_mask_filenames",
5553
"workflow",
5654
]

0 commit comments

Comments
 (0)