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
75 lines
3.6 KiB
C++
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;
|
|
};
|
|
|