A rotation sweep never records a double cone of half-angle asin(lambda/2d) about the spindle. That loss is normally repaired by the point group; it is not repaired when an operator's axis lies inside the cone, because the cone then maps onto itself. A still cannot know the point group, but it can see where the crystal's short lattice rows are, and a symmetry axis is always one of them - measured over 107 solved cells its length is 1.4x the shortest row at the median. The number is the fraction of the blind cone that a 2-fold about the nearest short row carries back into the blind cone: two equal caps of angular radius theta_max whose centres are 2*beta apart, which is the circle-lens area in beta/theta_max and within 0.035 of the spherical value even for a 55 deg cone. 0 means one sweep reaches everything symmetry could give; 1 means the row is on the spindle and the whole cone is lost coherently. No goniometer geometry enters, so the number describes the problem and leaves the remedy to the beamline. Free: it rides on the FilterFFTResults shortlist the indexer already builds, needing only the spindle, the wavelength and the frame's own resolution. FilterFFTResults gains an optional out-parameter for each row's peak prominence, which the length window is taken over. Measured on 384 stills of 22 solved crystals against the frames' own symmetry axes, with the same procedure re-run along five decoy directions per frame as the null: 0.93 of severe orientations reported severe, 0.010 of harmless ones reported severe, AUC 0.948, and the value tracks the true severity to 0.10 at the 90th percentile. Below 60 spots the misses triple, so that is where it stops answering. It is blind to a symmetry axis much longer than the crystal's shortest row: measured on synthetic stills the search grid finds a 150 A row every time, a 200 A row half the time and a 300 A row once in sixteen. Two cheap consistency tests refuse to answer for part of that regime and cost nothing on real frames, but they do not cover it - the reach is a documented property of the number, not something every frame can detect. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N
35 lines
1.4 KiB
C++
35 lines
1.4 KiB
C++
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "Indexer.h"
|
|
|
|
void Indexer::Setup(const DiffractionExperiment& experiment) {
|
|
geom = experiment.GetDiffractionGeometry();
|
|
indexing_tolerance = experiment.GetIndexingSettings().GetTolerance();
|
|
dist_tolerance_vs_reference = experiment.GetIndexingSettings().GetUnitCellDistTolerance();
|
|
viable_cell_min_spots = experiment.GetIndexingSettings().GetViableCellMinSpots();
|
|
index_ice_rings = experiment.GetIndexingSettings().GetIndexIceRings();
|
|
wavelength_A = experiment.GetWavelength_A();
|
|
spindle_axis = {};
|
|
if (const auto goniometer = experiment.GetGoniometer())
|
|
spindle_axis = goniometer->GetAxis();
|
|
SetupUnitCell(experiment.GetUnitCell());
|
|
}
|
|
|
|
IndexerResult Indexer::Run(const std::vector<Coord> &coord) {
|
|
IndexerResult ret;
|
|
auto start = std::chrono::steady_clock::now();
|
|
ret.lattice = RunInternal(coord, coord.size());
|
|
|
|
auto end = std::chrono::steady_clock::now();
|
|
std::chrono::duration<float> duration = end - start;
|
|
ret.indexing_time_s = duration.count();
|
|
ret.executed = true;
|
|
if (const auto severity = GetSpindleSeverity()) {
|
|
ret.spindle_blind_fraction = severity->score;
|
|
ret.spindle_row_length_A = severity->row_length_A;
|
|
ret.spindle_miss_angle_deg = severity->miss_angle_deg;
|
|
}
|
|
return ret;
|
|
}
|