// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #pragma once // ============================================================================= // SpotFootprint - how far a spot really reaches, measured on the data // ============================================================================= // // The integrator's r1 disk and r2..r3 background ring are fixed in pixels, chosen from spots near the // beam. Away from it a spot can grow several times wider: radially from the sensor's parallax and the // obliquity of the incidence, tangentially from the crystal's own spread (an azimuthal mosaic spread // rotates the diffracted beam about the incident one, smearing the spot along the ring). Measured on // small-molecule data at 20 keV, the standard deviation goes from 1 px near the beam to 5 px at the // edge, so the fixed disk holds a quarter of the flux there and the background ring a third of it - // an intensity loss that grows with resolution and that no scaling can see, because symmetry mates // share their resolution. // // The widths the integrator learns itself cannot follow: they are second moments taken inside the r1 // disk, so they saturate near r1^2/4. Here each strong spot is measured with a window that follows // the spot instead - three of its own standard deviations, iterated - separately along the radius and // across it, and the widths are tabulated against the distance from the beam centre. The table goes // to the integrator through BraggIntegrationSettings; where it says a spot outgrows the r1 disk, the // background ring is moved clear of the spot and the profile is fitted at the measured width // (BraggStencil.h). Compact spots leave the integration exactly as it was. // // A spot can also be in the wrong PLACE for the disk: a crystal of slightly misaligned domains - the // ferroelastic domains of a crystal below a phase transition, a cracked or split crystal - records // each reflection as two or more spots around the position the averaged lattice predicts, and they // move apart with resolution. Each of them is compact, so the widths above, measured about each spot, // say nothing; the r1 disk holds the gap between them and the background ring lands on them. What // does see it is where the spots sit against the prediction: once the lattice is known, every // indexed spot is compared with the predicted position of its own reflection on the same frame, and // the mean square of that offset is added to the width of the spot, radially and tangentially apart. // The table then describes the reflection rather than the spot - and on a crystal whose spots sit on // their predictions it moves the widths by the prediction error alone, a fraction of a pixel. // ============================================================================= #include #include #include "../../common/BraggIntegrationSettings.h" #include "../../common/Reflection.h" #include "../../common/SpotToSave.h" #include "BraggStencil.h" // One spot's measured widths, and its distance from the beam centre [px]. struct FootprintSpot { float r_px = 0.0f; float sigma_rad = 0.0f; float sigma_tan = 0.0f; }; // Measure the spots at the given (sub-pixel) positions on one preprocessed frame (INT32_MIN masked, // INT32_MAX saturated) of a flat detector. A spot whose window holds an unreadable pixel, or no signal above its // background, is left out. Results are appended in the order of the positions. void MeasureFootprintSpots(const int32_t *img, int width, int height, float beam_x, float beam_y, const std::vector &x, const std::vector &y, std::vector &out); // The pixels [x0, x0 + w) x [y0, y0 + h) of a frame, rows w apart: the whole frame, or a square cut // out of it about one spot. struct FootprintTile { const int32_t *pixels = nullptr; int x0 = 0, y0 = 0, w = 0, h = 0; }; enum class FootprintOutcome { Measured, LeftOut, OffTile }; // One spot at (x, y) of the image, on panel `pn` (BraggPanel: its plane position, the panel's pixel axes // and its box), reading the frame through `tile`. The window stays on the panel; OffTile: it walked off // the tile while still on the panel, so the spot has to be measured again, from the start, on a tile // that holds more of the frame - the answer is then the one the whole frame gives. FootprintOutcome MeasureFootprintSpot(const FootprintTile &tile, const BraggPanel &pn, float beam_x, float beam_y, float x, float y, FootprintSpot &spot); // Tabulate the spots: the median widths in bins of distance from the beam centre out to r_max. A bin // holding too few spots takes the nearest bin that has enough; with no such bin the table is empty. SpotFootprint FootprintFromSpots(const std::vector &spots, float r_max); // One indexed spot's offset from the predicted position of its reflection [px], along and across the // radius, and the prediction's distance from the beam centre [px]. struct FootprintOffset { float r_px = 0.0f; float off_rad = 0.0f; float off_tan = 0.0f; bool operator<(const FootprintOffset &o) const { return r_px != o.r_px ? r_px < o.r_px : off_rad != o.off_rad ? off_rad < o.off_rad : off_tan < o.off_tan; } }; // The offsets of one frame: every spot indexed on the main lattice against the reflection of the same // hkl predicted on that frame. Spots whose reflection is not among the predictions are left out. void MeasureFootprintOffsets(const std::vector &spots, const std::vector &reflections, float beam_x, float beam_y, std::vector &out); // The width table with the offsets added in quadrature: in each distance bin of `widths`, the mean // square offset along and across the radius. A bin holding too few offsets takes the nearest bin // that has enough; with no such bin, or no widths, the widths come back unchanged. SpotFootprint FootprintWithOffsets(const SpotFootprint &widths, std::vector offsets); // Spots per distance bin a table needs before it is believed. constexpr int FOOTPRINT_MIN_SPOTS_PER_BIN = 20; constexpr int FOOTPRINT_BINS = 12;