|
| 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 | + ) |
0 commit comments