// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #pragma once // Where on its rocking curve a rotation frame records a reflection. // // The rocking curve is a Gaussian in the rotation angle, centred on the exact diffracting condition, // and a frame records the part of it inside its own oscillation: [phi - w/2, phi + w/2] in the // convention of the rotation predictor, where phi is the frame's offset from that condition and // c1 = zeta / (sqrt(2) sigma_M) is the curve's reciprocal width. The flux-weighted centre of that slice // is the mean of a truncated normal, // // c = (exp(-a^2) - exp(-b^2)) / (2 sqrt(pi) c1 p), a = (phi - w/2) c1, b = (phi + w/2) c1, // // with p the slice's mass - the partiality. A frame whose oscillation straddles the condition // symmetrically gets c = 0; one that only catches the curve's tail gets a c of up to several curve // widths. // // It matters because the recorded spot is not fixed on the detector while the crystal rotates through // the curve: it walks along its Debye ring, one detector velocity per radian of rotation, and a partial // recorded at c sits c times that velocity from the exact-condition position. Measured on rotation // data the walk follows this c with slope ~1 (0.9 at high angle, where the r1 centroid it was measured // with is truncated least), and at |zeta| < 0.2 it is pixels - the reflection moves nearly tangent to // the Ewald sphere, so its curve is wide and the spot travels far along the ring while it lasts. #include #ifdef __CUDACC__ #define ROCKING_SLICE_HD __host__ __device__ inline #else #define ROCKING_SLICE_HD inline #endif // The frame's slice of the rocking curve, (erf(b) - erf(a)) / 2 with a = (phi - w/2) c1, b = (phi + w/2) c1. // Where the slice lies in one tail of the curve both erf are near +-1 and their difference cancels to a // few float ulps of 1, so the same difference is taken from erfc, which keeps its relative precision there. ROCKING_SLICE_HD float RockingSlicePartiality(float phi, float half_wedge, float c1) { const float a = (phi - half_wedge) * c1, b = (phi + half_wedge) * c1; if (a > 0.0f) return (erfcf(a) - erfcf(b)) / 2.0f; if (b < 0.0f) return (erfcf(-b) - erfcf(-a)) / 2.0f; return (erff(b) - erff(a)) / 2.0f; } // Flux-weighted centre of the frame's slice of the rocking curve, relative to the exact diffracting // condition, in radians of rotation. partiality is the slice's own mass (the caller has it already). ROCKING_SLICE_HD float RockingSliceCentroid_rad(float phi, float half_wedge, float c1, float partiality) { if (!(partiality > 0.0f) || !(c1 > 0.0f)) return 0.0f; const float a = (phi - half_wedge) * c1, b = (phi + half_wedge) * c1; return (expf(-a * a) - expf(-b * b)) / (2.0f * sqrtf(3.14159265f) * c1 * partiality); }