The Bravais class is chosen by a walk that reads two axes as EQUAL when they agree to a fixed relative tolerance, and the class carrying that equality is then imposed on everything below: the cell is refined with a = b, reflections are predicted from it, and they are integrated at those predicted positions. On a small cell the tolerance is far wider than the spot positions resolve, so a genuinely orthorhombic crystal whose a and b differ by 2 % is integrated as tetragonal and every decision downstream is read off the wreckage. Measure the equality instead of assuming it. The indexer already refines each candidate a second time with nothing held, so the first pass now reports how far that free refinement leaves the two axes apart. A relative split of eps displaces a reflection at radius r by eps/2 * r pixels: where that displacement at the far corner of the detector stays inside the integration disc, imposing the equality moves nothing out of its own box and the higher symmetry is kept with no extra pass. Where it does not, both hypotheses are run as probe-only passes - the promoted class, and the class the same walk carries when it is granted no length equality at all - and whichever realises the lower held-out positional residual is kept, the promoted class on a tie. Two small-molecule sets whose axes differ by 2 % now index, refine and merge in their own orthorhombic lattice instead of a tetragonal mean: one goes from a cell 1 % wrong and P 1 at CC1/2 0.19 to the deposited cell within 0.2 % and P 2 2 2 at CC1/2 0.95, the other from a tetragonal mean to a cell matching its reference to 0.4 %. Protein sets whose symmetry is real (P41212, P4222, I23, P6422, F4132) are unchanged: their free refinements leave the axes 0.02-0.10 % apart, a few tenths of a pixel, so the question is never asked. Where it was asked on a weak sweep whose free refinement diverged, the arms decided for the higher symmetry and the output was identical. No new threshold: the comparison is the integration radius the run already integrates at, and the arms are judged by HeldOutResidualFell, as the geometry walk's rounds are. postrefine_probe_only_ returns for the arms' sake - a pass run only to measure stops before the scaling engine is built, so neither arm pays for a merge or a space-group search. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nW6FNRP1bBJJ8pfHiByAT
124 lines
6.7 KiB
C++
124 lines
6.7 KiB
C++
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#pragma once
|
|
|
|
#include <cstdint>
|
|
|
|
// Upper bound accepted by IndexingSettings::RefineThreads. Callers deriving a thread count from the
|
|
// machine must clamp to it - on a big host -N/2 exceeds it and the setter throws.
|
|
constexpr int MAX_REFINE_THREADS = 64;
|
|
|
|
enum class IndexingAlgorithmEnum {FFBIDX, FFT, FFTW, Auto, None};
|
|
// Flex = "let the pipeline decide": try several per-image refinements and keep whichever indexes the
|
|
// most spots (CLI -r flex; the legacy -r multi name is still accepted as an alias).
|
|
enum class GeomRefinementAlgorithmEnum {None, OrientationOnly, BeamCenter, Flex};
|
|
|
|
class IndexingSettings {
|
|
IndexingAlgorithmEnum algorithm;
|
|
int64_t fft_num_vectors = 16*1024;
|
|
float fft_max_unit_cell_A = 500.0;
|
|
float fft_min_unit_cell_A = 10.0;
|
|
float fft_max_angle_deg = 150.0;
|
|
float fft_min_angle_deg = 30.0;
|
|
float fft_high_resolution_A = 2.0;
|
|
float indexing_tolerance = 0.1;
|
|
float max_angle_from_ewald_deg = 2.0;
|
|
float unit_cell_dist_tolerance_vs_reference = 0.05; // relative
|
|
static constexpr float unit_cell_angle_tolerance_deg = 5.0; // degree
|
|
public:
|
|
// The longest cell the FFT search can be asked to reach, and so the longest it can ever find:
|
|
// FFTIndexer sizes its histogram from fft_max_unit_cell_A and the transform's last usable bin IS
|
|
// that length. Callers that widen the bound (a given cell, the long-axis rescue) must clamp to
|
|
// this rather than let the setter throw - a rescue that recovers an implausible axis must not
|
|
// take the whole run down with it.
|
|
static constexpr float fft_max_unit_cell_limit_A = 1200.0;
|
|
// The other end of the same search: any candidate whose reduced cell has an axis shorter than
|
|
// fft_min_unit_cell_A is discarded, and this is the lowest floor the setter accepts. A caller
|
|
// lowering the floor to reach a given cell clamps to it rather than let the setter throw.
|
|
static constexpr float fft_min_unit_cell_limit_A = 5.0;
|
|
private:
|
|
int64_t indexing_threads = 4;
|
|
// Threads splitting the candidate-cell refinement WITHIN one indexer call. 1 (the default) is the
|
|
// right answer whenever indexers already run one per image across all workers; it is raised only
|
|
// where few indexer threads exist and cores would otherwise sit idle.
|
|
int64_t refine_threads = 1;
|
|
// The setter's own floor. A frame that carries fewer spots than this cannot validate a
|
|
// lattice, and on a weakly diffracting crystal a bar set above what its frames can reach
|
|
// is arithmetic rather than evidence: the true lattice is found, indexes every frame it
|
|
// can, and is refused because too few frames could clear a count they never had the spots
|
|
// for. The fraction of frames that must validate is the gate that carries the meaning.
|
|
//
|
|
// It is NOT only the validation bar, and lowering it loosens more than that: the same number
|
|
// is the order statistic the candidate scorer ranks cells on, the stopping condition of the
|
|
// iterative residual contraction, and the adoption gate of per-frame refinement (see
|
|
// PostIndexingRefinement.cpp). Those three want a count that makes a fit trustworthy, which
|
|
// is not the same question as whether a frame had spots to spare, and they are coupled here
|
|
// only because one constant has always served both.
|
|
int64_t viable_cell_min_spots = 6;
|
|
|
|
int64_t max_extra_lattices = 2;
|
|
|
|
bool blocking_behavior = true;
|
|
bool index_ice_rings = false;
|
|
|
|
// Refuse the Bravais class the metric was promoted to on a LENGTH equality, and index in the
|
|
// class left when that equality is struck out. Not a user setting: rugnux runs one pass with it
|
|
// and one without, and keeps the one whose re-integrated held-out positional residual is lower
|
|
// (see RunAllPasses). The promotion is decided against a 3 % relative tolerance on a cell the
|
|
// spot positions place to ~0.1 %, so on a small cell a genuinely orthorhombic crystal is
|
|
// integrated with a = b imposed and nothing downstream can recover from it.
|
|
bool lattice_class_demoted = false;
|
|
|
|
bool enable_rotation_indexing = false;
|
|
float rotation_indexing_min_angular_range_deg = 20.0;
|
|
float rotation_indexing_angular_stride_deg = 0.5;
|
|
|
|
GeomRefinementAlgorithmEnum refinement = GeomRefinementAlgorithmEnum::BeamCenter;
|
|
public:
|
|
IndexingSettings();
|
|
|
|
IndexingSettings& ViableCellMinSpots(int64_t input);
|
|
IndexingSettings& Algorithm(IndexingAlgorithmEnum input);
|
|
IndexingSettings& FFT_MaxUnitCell_A(float input);
|
|
IndexingSettings& FFT_MinUnitCell_A(float input);
|
|
IndexingSettings& FFT_MaxAngle_deg(float input);
|
|
IndexingSettings& FFT_MinAngle_deg(float input);
|
|
IndexingSettings& FFT_NumVectors(int64_t input);
|
|
IndexingSettings& FFT_HighResolution_A(float input);
|
|
IndexingSettings& Tolerance(float input);
|
|
IndexingSettings& IndexingThreads(int64_t input);
|
|
IndexingSettings& RefineThreads(int64_t input);
|
|
IndexingSettings& UnitCellDistTolerance(float input);
|
|
IndexingSettings& GeomRefinementAlgorithm(GeomRefinementAlgorithmEnum input);
|
|
IndexingSettings& IndexIceRings(bool input);
|
|
IndexingSettings& RotationIndexing(bool input);
|
|
IndexingSettings& RotationIndexingMinAngularRange_deg(float input);
|
|
IndexingSettings& RotationIndexingAngularStride_deg(float input);
|
|
IndexingSettings& BlockingBehavior(bool input);
|
|
IndexingSettings& MaxExtraLattices(int64_t input);
|
|
IndexingSettings& LatticeClassDemoted(bool input);
|
|
|
|
[[nodiscard]] int64_t GetViableCellMinSpots() const;
|
|
[[nodiscard]] IndexingAlgorithmEnum GetAlgorithm() const;
|
|
[[nodiscard]] GeomRefinementAlgorithmEnum GetGeomRefinementAlgorithm() const;
|
|
[[nodiscard]] float GetFFT_MaxUnitCell_A() const;
|
|
[[nodiscard]] float GetFFT_MinUnitCell_A() const;
|
|
[[nodiscard]] int64_t GetFFT_NumVectors() const;
|
|
[[nodiscard]] float GetFFT_HighResolution_A() const;
|
|
[[nodiscard]] float GetTolerance() const;
|
|
[[nodiscard]] float GetFFT_MinAngle_deg() const;
|
|
[[nodiscard]] float GetFFT_MaxAngle_deg() const;
|
|
[[nodiscard]] int64_t GetIndexingThreads() const;
|
|
[[nodiscard]] int64_t GetRefineThreads() const;
|
|
[[nodiscard]] float GetUnitCellDistTolerance() const;
|
|
[[nodiscard]] float GetUnitCellAngleTolerance_deg() const;
|
|
[[nodiscard]] bool GetIndexIceRings() const;
|
|
[[nodiscard]] bool GetRotationIndexing() const;
|
|
[[nodiscard]] float GetRotationIndexingMinAngularRange_deg() const;
|
|
[[nodiscard]] float GetRotationIndexingAngularStride_deg() const;
|
|
[[nodiscard]] bool GetBlockingBehavior() const;
|
|
[[nodiscard]] int64_t GetMaxExtraLattices() const;
|
|
[[nodiscard]] bool GetLatticeClassDemoted() const;
|
|
};
|