// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include #include "../../common/UnitCell.h" #include "../../common/SpotToSave.h" #include "RingOptimizer.h" struct RingClusters { std::vector spots; float R_obs; int ring_idx = -1; }; struct FindCircleCenterResult { int64_t total_votes; int64_t votes_for_beam_center; float x; float y; }; // Beam centre from the circumcentres of spot triples, voted into a pixel grid. The grid spans the // spots' own bounding box: a powder ring encloses the beam centre, so that is where the answer has to // be, and it is the only bound the spots themselves justify. It used to be a fixed 4000x4000 box, // which both allocated 128 MB whatever the detector and put the centre outside the grid - so every // vote was discarded - on any detector larger than that in either direction. FindCircleCenterResult FindCircleCenter(const std::vector &v, int64_t max_spots = 500); std::vector> ClusterSpotsIntoRings(const std::vector& r, float eps = 1.5, int minPts = 5); std::vector AnalyzeClusters(const std::vector& r, const std::vector> &clusters); // Which hkl a lattice actually diffracts into. A cell alone does not say - the centring and any glide // plane do - and a ring that is systematically absent is worse than a missing one, because the fit // pairs the innermost OBSERVED ring with the innermost listed one. Only the conditions the calibrants // need are here; a cell of unknown symmetry (the viewer's "current sample") takes All. enum class ReflectionCondition { All, // primitive: no absences FaceCentred, // h, k, l all even or all odd Diamond // face-centred, and h + k + l = 4n when they are all even }; std::vector CalculateXtalRings(const UnitCell &cell, ReflectionCondition condition = ReflectionCondition::All, int hkl_max = 6); // The same, for a cell whose symmetry is known: the absences come from the space group itself rather // than from the three conditions above, so a centring, a glide and a screw are all handled. This is what // a user-supplied calibrant cell needs - the fit pairs the innermost OBSERVED ring with the innermost // LISTED one, so a list that opens with a forbidden reflection scales the whole calibration by the ratio // between them. std::vector CalculateXtalRings(const UnitCell &cell, const gemmi::SpaceGroup &sg, int hkl_max = 6); std::vector CalculateCubicXtalRings( float a, int hkl_max = 4); float GuessDetectorDistance(const DiffractionGeometry& geom, float ring_radius_pxl, float d_A); // The calibrant enters the three functions below as its ring q values (2*pi/d, 1/A, ascending - what // CalculateXtalRings and CalibrantRings return), not as a UnitCell, so a standard whose rings are // measured rather than enumerated from a cell - hexagonal ice - can be calibrated against as well. // GuessInitialGeometry needs only the outermost d-spacing, to seed the distance from the innermost ring. std::vector GuessInitialGeometry(DiffractionGeometry &geom, const std::vector &v, float largest_ring_d_A); // refine_tilt = false pins rot1/rot2 where the geometry already has them (see RingOptimizer). void GuessGeometry(DiffractionGeometry &geom, const std::vector &v, const std::vector &ring_q, bool refine_tilt = true); void OptimizeGeometry(DiffractionGeometry &geom, const std::vector &v, const std::vector &ring_q, bool refine_tilt = true, RingFitUncertainty *unc = nullptr); // Widest a ring's match window is allowed to be, in q. The clamp below narrows it where the rings // crowd; this is the value for a well-separated ring. constexpr float RING_MATCH_Q_RECIPA = 0.1f; // Half-width of the window ring i may be matched in, clamped so it can never reach the neighbouring // ring. Silver behenate's orders sit 0.108 1/A apart and hexagonal ice has three rings within 0.06, so // a fixed window takes in the neighbour's flank - and a search that stops at its first match then puts // every point on the lower-q ring of the pair. float RingMatchWindow(const std::vector &ring_q, size_t i, float max_window); // Each spot paired with the calibrant ring nearest its observed q, as the points RingOptimizer fits. // Spots further from every ring than RingMatchWindow allows are dropped rather than forced onto the // closest one. std::vector AssignSpotsToRings(const DiffractionGeometry &geom, const std::vector &v, const std::vector &ring_q);