// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include #include "../../common/DiffractionGeometry.h" #include "../../common/SpotToSave.h" // Higher-order contamination of the beam, read off the spots a run could not index. // // A Si(111) monochromator passes lambda/3 as well as lambda, and an undulator harmonic can feed it. // A reflection excited by the lambda/3 photons scatters into the same direction as a reflection with // three times its d-spacing would at lambda, so the crystal writes a SECOND spot population that no // single lattice explains, all of it at coarse apparent resolution. // // The test is exact rather than empirical. A spot's reciprocal vector read at the fundamental is // q = (s_hat - z_hat) / lambda, so at lambda/n it is n*q - and n*q automatically sits on the Ewald // sphere that wavelength draws. Contamination therefore asks one question of each spot the lattice // did not take: is 3*q an integer point of the lattice the frame indexed? The same question at a // scale no monochromator passes (SCALE_NULL) is the null, and it is a real null rather than a // nominal one: the distance from a random point to the nearest lattice point does not depend on the // scale, so the two rates are directly comparable. // // This does not distinguish a contaminated beam from a crystal whose true cell is three times the // one indexed: a genuine 3x superlattice puts its extra reflections in exactly these places. What // separates them is where the population stops. A harmonic spot's true d is a third of its apparent // one, so the population dies out at three times the crystal's resolution limit, while a superlattice // runs as far as the crystal diffracts - which is why the apparent resolution of the hits is reported // beside their number. // How near an integer a scaled spot has to fall before it counts as a lattice point of that harmonic. constexpr float HARMONIC_INDEX_TOLERANCE = 0.10f; // A spot this close to a lattice point as it stands is a reflection the indexer only just rejected. // It then lands on an integer at EVERY integer scale and says nothing about a harmonic, so it is not // asked. On a clean run this is most of the unindexed list. constexpr float HARMONIC_NEAR_MISS_TOLERANCE = 0.20f; // The harmonic looked for, and the scale used as its null. 2.5 is not a harmonic of anything. constexpr float HARMONIC_SCALE = 3.0f; constexpr float HARMONIC_SCALE_NULL = 2.5f; // Indexed spots a frame must carry before its orientation is fitted from them. constexpr size_t HARMONIC_MIN_INDEXED_SPOTS = 20; // What the run has counted so far. Counts only, so the total does not depend on the order the images // were summed in or on how many threads found them. struct HarmonicEvidence { int64_t frames = 0; // frames whose orientation could be fitted int64_t tested = 0; // unindexed spots that are not near-misses of that frame's lattice int64_t hits = 0; // of those, the ones 3*q places on the lattice int64_t null_hits = 0; // the same at HARMONIC_SCALE_NULL double hit_d_sum_A = 0.0; // apparent d of the hits, summed double indexed_d_sum_A = 0.0; int64_t indexed = 0; void Add(const HarmonicEvidence &other); }; // Fit this frame's orientation from its indexed spots and ask the question above of the rest. Does // nothing when the frame has too few indexed spots to fit one. void AddHarmonicEvidence(const std::vector &spots, const DiffractionGeometry &geometry, HarmonicEvidence &evidence); struct HarmonicContaminationResult { bool measurable = false; bool detected = false; float fraction = 0.0f; // per cent of the tested spots that 3*q puts on the lattice float null_fraction = 0.0f; // the same at HARMONIC_SCALE_NULL int64_t tested = 0; int64_t hits = 0; int64_t frames = 0; float mean_d_A = 0.0f; // apparent resolution the extra population sits at float mean_indexed_d_A = 0.0f; // and where the crystal's own spots sit, for comparison }; HarmonicContaminationResult HarmonicFromEvidence(const HarmonicEvidence &evidence);