// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include "../../common/CrystalLattice.h" #include "../../common/DiffractionExperiment.h" #include "../../common/JFJochMessages.h" #include "../../common/SpotToSave.h" #include "SpindleBlindFraction.h" struct IndexerResult { std::vector lattice; float indexing_time_s; bool executed = false; // If indexing was not performed (due to indexer being not-available) mark it, so indexing result is marked accordingly // What the indexer threw, when it threw. A THIRD outcome, not a flavour of the other two: an // empty lattice list with no error is a real negative result about the frame - it was examined // and no lattice fits it - whereas an error says nothing about the frame at all, the indexer // never having got to look, and will recur on the next one. std::optional error; // How much of a single sweep's blind cone this orientation makes unrecoverable, in [0,1]. // Absent when the frame gives nothing to decide on (no spindle defined, no lattice rows). std::optional spindle_blind_fraction; std::optional spindle_row_length_A; std::optional spindle_miss_angle_deg; }; class Indexer { protected: int64_t viable_cell_min_spots = 9; float indexing_tolerance = 0.1; float dist_tolerance_vs_reference = 0.05; bool index_ice_rings = true; DiffractionGeometry geom; std::optional reference_unit_cell; // The rotation axis a subsequent sweep would use. Present on a grid scan too, where the axis is // defined but stationary - that is exactly the case the spindle severity is for. std::optional spindle_axis; float wavelength_A = 0; // Half-angle of the blind double cone a sweep would leave: asin(lambda/2d) at the GEOMETRIC // resolution of the setup - the detector corner at the recorded distance and wavelength - not at // the still's own spot resolution. A weak, attenuated grid still reaching 3 A would score a row // at an 11 deg miss-angle as exactly 0 while the real sweep, at the detector's 1.8 A, loses a // fifth of its wider cone. The corner is an upper bound on any sweep collected without moving // the detector, it is there on every frame, and it is unbiased where the still's spot list is // systematically shallow; a wider cone errs towards reporting loss, which is the cheap error. // 0 when the geometry cannot give it. float spindle_theta_max_deg = 0; virtual void SetupUnitCell(const std::optional& cell) = 0; virtual std::vector RunInternal(const std::vector &coord, size_t nspots) = 0; // Set by RunInternal when the implementation computes it; read out by Run. virtual std::optional GetSpindleSeverity() const { return {}; } // The row pass alone, over the whole of `coord`, to produce the spindle severity without // reducing or refining anything. Implemented by the FFT-family indexers, which own a row // search; the others have nothing to answer with and return nothing. virtual std::optional RunSeverityOnly(const std::vector &coord) { return {}; } public: virtual ~Indexer() = default; void Setup(const DiffractionExperiment& experiment); // severity_only = true runs RunSeverityOnly instead of indexing: no lattice comes back and // `executed` stays false, since nothing was an indexing attempt. Used when the seed escalation // indexed a frame from fewer spots than the severity's floor - the score must not be absent on // precisely the frames that index cleanly. IndexerResult Run(const std::vector &coord, bool severity_only = false); };