Files
Jungfraujoch/image_analysis/indexing/FFTIndexer.h
T
leonarski_fandClaude Opus 5.5 acd4a50b74 FFTIndexer: unfold lattice rows the histogram sampling aliased
The projected histogram is sampled every 1/(2*max_length) of 1/d, so a row
of length L between max_length and 2*max_length folds to 2*max_length - L and
peaks there as sharply as a real row. At the default 500 A bound a 602 A axis
came back as ~400 A and a 701 A axis as ~299 A; the folded cell still indexed
59/60 and 37/60 validation frames (only the reflections whose index lands on
an integer of the folded period), so the long-axis rescue, gated on a poor
pass, never ran. The same fold also turned real >500 A lattice combinations
of shorter-axis cells into spurious shortlist rows.

Each shortlisted row is now asked once, by a direct Fourier sum over the spot
positions (no sampling, so no fold), whether the spots carry its own length or
its partner 2*max_length - L more strongly, and keeps the stronger. Partners
within 0.1 of an integer multiple are left alone (a harmonic, settled
elsewhere). Refinement accepts rows up to 2*max_length when one was unfolded.

Measured: a hexagonal P6122 c=602 A EIGER set and a P6522 c=701 A CCD set,
both indexed de novo on the folded cell before, now index the deposited cell
60/60 and merge to R-free 0.31 / 0.22 against the models (the same numbers as
-C with the deposited cell). Of 13 long-axis battery sets, 7 are bit-identical
(p.hkl md5) and 2 never unfold a row, 8v4o and 9yl4 have the same intensities (CC 1.0) without needing
the long-axis rescue, 4mtk refines a cell closer to the deposit at half the
wall time, 4nwv merges better (R_meas 0.58 -> 0.47, ISa 6.7 -> 8.4).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
2026-10-09 23:07:42 +02:00

75 lines
3.6 KiB
C++

// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include "Indexer.h"
#include "../../common/UnitCell.h"
#include "../../common/Coord.h"
#include "../../common/CrystalLattice.h"
#include "../../common/IndexingSettings.h"
#include "FFTResult.h"
#include "SpindleBlindFraction.h"
#define FFT_MAX_SPOTS (64*1024)
class FFTIndexer : public Indexer {
protected:
const float min_length_A;
const float max_length_A;
const float min_angle_deg;
const float max_angle_deg;
const int nDirections;
const unsigned refine_threads;
float histogram_spacing;
int64_t histogram_size;
std::vector<Coord> direction_vectors;
size_t input_size;
size_t output_size;
// Per-direction best peaks (host)
std::vector<FFTResult> result_fft;
void SetupUnitCell(const std::optional<UnitCell> &cell) override;
// widen=false: combine only the shortest few filtered vectors (fast, correct for compact cells).
// widen=true: anchor the two short axes but let the third range over all vectors (reaches the long
// axis of a large/elongated cell). Used only as a fallback when the standard reduction indexes poorly.
std::vector<CrystalLattice> ReduceResults(const std::vector<Coord> &results, bool widen) const;
// `magnitudes`, when given, receives the FFT peak prominence of each returned row, in the
// same order. The spindle severity needs it to tell a crystal's real rows from the short
// spurious ones a long cell produces; nothing else does, so it is optional.
std::vector<Coord> FilterFFTResults(size_t max_vectors,
std::vector<float> *magnitudes = nullptr) const;
// Replace each row whose FFT length is an alias of a longer one by that longer row (see the definition).
void DeAlias(const std::vector<Coord> &coord, size_t nspots, std::vector<Coord> &rows) const;
std::vector<CrystalLattice> ReduceAndRefine(const std::vector<Coord> &coord, size_t nspots,
const std::vector<Coord> &filtered, bool widen);
float IndexedFraction(const CrystalLattice &latt, const std::vector<Coord> &coord, size_t nspots) const;
void SetupDirectionVectors();
// Replace the search directions with a small cap around `axis` (same count, so the transform and
// its plan are unchanged) and return the strongest peak found in it.
std::optional<Coord> SearchCap(const std::vector<Coord> &coord, size_t nspots,
const Coord &axis, float half_angle_deg);
// Direction perpendicular to every vector of `filtered`, when they all lie in one plane and so
// cannot close a cell; empty otherwise.
std::optional<Coord> DegeneratePlaneNormal(const std::vector<Coord> &filtered) const;
// Filled by RunInternal from the shortlist it already computes; read out by Indexer::Run.
std::optional<SpindleSeverity> spindle_severity;
std::optional<SpindleSeverity> GetSpindleSeverity() const override { return spindle_severity; }
std::optional<SpindleSeverity> RunSeverityOnly(const std::vector<Coord> &coord) override;
virtual void ExecuteFFT(const std::vector<Coord> &coord, size_t nspots) = 0;
// Called after direction_vectors is rewritten, for implementations that keep a copy of it.
virtual void DirectionsChanged() {}
public:
explicit FFTIndexer(const IndexingSettings& settings);
~FFTIndexer() override = default;
std::vector<CrystalLattice> RunInternal(const std::vector<Coord> &coord, size_t nspots) override;
};