Merge branch 'firstpass-split' into rc175: the first pass as named steps, no output change
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
This commit is contained in:
@@ -9,7 +9,10 @@ ADD_LIBRARY(JFJochRugnux STATIC
|
||||
RugnuxPreScan.cpp
|
||||
RugnuxPasses.cpp
|
||||
RugnuxImagePass.cpp
|
||||
RugnuxFirstPass.h
|
||||
RugnuxFirstPass.cpp
|
||||
RugnuxFirstPassRescues.cpp
|
||||
RugnuxFirstPassAccept.cpp
|
||||
RugnuxScaleMerge.cpp
|
||||
RugnuxDefaults.cpp
|
||||
RugnuxDefaults.h
|
||||
|
||||
@@ -917,6 +917,8 @@ class Rugnux {
|
||||
// The first pass of the two-pass rotation indexing: index the sweep and hand the lattice to the
|
||||
// indexer. True where the pass ends there - an indexing probe, whose result is its validation score.
|
||||
bool FirstPassRotationIndexing(PipelineLocals &p);
|
||||
// Its steps and the state they share (RugnuxFirstPass.h).
|
||||
struct FirstPassRun;
|
||||
void ImagePass(PipelineLocals &p);
|
||||
// Scaling and merging, the space-group search, the analyses of the merge and the merged output.
|
||||
// True where the pass ends early without merging: a post-refinement probe, or a starved pass.
|
||||
|
||||
+993
-2376
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,265 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
// Private to the Rugnux implementation files, like RugnuxPipeline.h: the first pass of a rotation
|
||||
// run (Rugnux::FirstPassRotationIndexing) as the state its steps share - the spot cache, the
|
||||
// validation frames, the sampling schemes, the spot-list settings the run stands on - and one
|
||||
// member function per step. FirstPassRotationIndexing itself is the ordered list of the steps; the
|
||||
// standing lattice (FirstPass) goes in and comes out of each of them.
|
||||
//
|
||||
// RugnuxFirstPass.cpp: the shared machinery (spots, scoring, schemes, the ladder) and the order.
|
||||
// RugnuxFirstPassRescues.cpp: the rescues and hypotheses that may replace the standing lattice.
|
||||
// RugnuxFirstPassAccept.cpp: the no-crystal test and what the run takes from the accepted lattice.
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "RugnuxPipeline.h"
|
||||
#include "../common/AzimuthalIntegrationProfile.h"
|
||||
#include "../image_analysis/MXAnalysisWithoutFPGA.h"
|
||||
|
||||
namespace rugnux_internal {
|
||||
// A candidate lattice of the first pass and what it scored on the validation frames.
|
||||
// indexer_error: what the indexer threw, if it threw. An empty result with no error is a
|
||||
// verdict on the crystal; an empty result with one is a verdict on the machine.
|
||||
struct FirstPass { std::optional<RotationIndexerResult> result; int score = -1; std::string name; double vol = 0.0;
|
||||
std::optional<std::string> indexer_error; };
|
||||
}
|
||||
|
||||
struct Rugnux::FirstPassRun {
|
||||
FirstPassRun(Rugnux &rx, PipelineLocals &p);
|
||||
|
||||
using FirstPass = rugnux_internal::FirstPass;
|
||||
using SchemeIndexers = std::vector<std::unique_ptr<RotationIndexer>>;
|
||||
using Schemes = std::vector<std::pair<std::string, std::vector<int>>>;
|
||||
// The pooled evidence for one candidate lattice: the validation frames' non-ice spots, how many
|
||||
// of them lie on the candidate, and how many a wrong spindle angle still puts there.
|
||||
using PooledEvidence = ValidationSpotEvidence;
|
||||
|
||||
// --- The steps, in the order FirstPassRotationIndexing takes them ---
|
||||
|
||||
// The spots of every frame the pass is certain to want, in one batch.
|
||||
void PrefetchCertainFrames();
|
||||
// The schemes, indexed and scored, with the speculative passes started beside them.
|
||||
FirstPass IndexSchemes();
|
||||
FirstPass AxisSignRescue(FirstPass best);
|
||||
// The beam-centre check; sets long_axis_asked where it has asked the long-axis rescue itself.
|
||||
FirstPass BeamCentreCheck(FirstPass best, bool &long_axis_asked);
|
||||
std::optional<FirstPass> LongAxisRescue(const FirstPass &standing);
|
||||
FirstPass AddEndWedge(FirstPass bp, IndexerThreadPool &pool, IndexAndRefine &idx);
|
||||
void ReleaseSpotEngines();
|
||||
// Throws where the pass ends with no lattice at all.
|
||||
void RequireLattice(const FirstPass &best) const;
|
||||
FirstPass ShortAxisHypothesis(FirstPass best);
|
||||
FirstPass SubLatticeHypothesis(FirstPass best);
|
||||
FirstPass Pass1Fallback(FirstPass best);
|
||||
std::optional<FirstPass> FirstPassLadder(int score_before, int min_gain, int min_score);
|
||||
|
||||
// The pooled spots of the validation frames on the lattice the pass ended on, and their share.
|
||||
struct Verdict {
|
||||
PooledEvidence evidence;
|
||||
double pooled = 0.0;
|
||||
double pooled_chance = 0.0;
|
||||
};
|
||||
Verdict MeasureVerdict(const FirstPass &best, const std::vector<double> &first_pass_key,
|
||||
const std::optional<ValidationSpotEvidence> &memo_hit);
|
||||
// Throws where the lattice does not beat chance.
|
||||
void RejectChanceLattice(const FirstPass &best, const Verdict &v) const;
|
||||
void AdoptLattice(const FirstPass &best, const Verdict &v);
|
||||
void StartLeftoverCensus(const FirstPass &best);
|
||||
void ReportTiltWalk(const FirstPass &best);
|
||||
void MeasureSpotBudget(const FirstPass &best);
|
||||
void Pass1SupercellGuard(const FirstPass &best);
|
||||
|
||||
// --- The beam-centre check, by case ---
|
||||
|
||||
// The two centres the check compares.
|
||||
struct Centres {
|
||||
BeamCenterEstimate measured;
|
||||
float header_x = 0.0f, header_y = 0.0f;
|
||||
float measured_x = 0.0f, measured_y = 0.0f;
|
||||
float moved = 0.0f;
|
||||
};
|
||||
FirstPass IndexAtMeasuredCentre();
|
||||
FirstPass CentreWhenFileFails(FirstPass best, FirstPass alt, const Centres &c, bool &long_axis_asked);
|
||||
FirstPass CentreByPooledSpots(FirstPass best, FirstPass alt, const Centres &c, bool &long_axis_asked);
|
||||
FirstPass CentreWhenBothIndex(FirstPass best, const FirstPass &alt, const Centres &c);
|
||||
FirstPass CentresDisagree(FirstPass best, const FirstPass &alt, const Centres &c, double ratio);
|
||||
|
||||
// --- The machinery the steps share ---
|
||||
|
||||
std::optional<float> RotAngle(int ordinal) const;
|
||||
std::vector<SpotToSave> FindSpots(MXAnalysisWithoutFPGA &analysis, AzimuthalIntegrationProfile &profile,
|
||||
JFJochReaderRawImage &img, int ordinal);
|
||||
static void UseGpuOfWorker(size_t t);
|
||||
void PrefetchSpots(const std::vector<int> &ordinals);
|
||||
void TryBeamCenter(float x, float y);
|
||||
void RestoreBeamCenter(float x, float y);
|
||||
std::vector<Coord> ValidationCloud(const RotationIndexerResult &r);
|
||||
int CountIndexed(IndexAndRefine &idx, const RotationIndexerResult &r);
|
||||
std::pair<int64_t, int64_t> PooledOnLattice(IndexAndRefine &idx, const RotationIndexerResult &r, int shift);
|
||||
PooledEvidence MeasurePooled(IndexAndRefine &idx, const RotationIndexerResult &r);
|
||||
int64_t MeasureBudget(IndexAndRefine &idx, const RotationIndexerResult &r);
|
||||
std::unique_ptr<RotationIndexer> FeedScheme(IndexerThreadPool &pool, const std::vector<int> &ordinals,
|
||||
const DiffractionExperiment *x = nullptr);
|
||||
SchemeIndexers FeedSchemes(IndexerThreadPool &pool, const DiffractionExperiment *x = nullptr);
|
||||
static void RunIndexing(const std::vector<RotationIndexer *> &ris);
|
||||
static void RunIndexing(const SchemeIndexers &ris);
|
||||
FirstPass ScoreSchemes(const SchemeIndexers &ris, IndexAndRefine &idx, const Schemes *named = nullptr,
|
||||
FirstPass bp = {});
|
||||
FirstPass PickBest(IndexerThreadPool &pool, IndexAndRefine &idx,
|
||||
const std::function<void()> &alongside = {});
|
||||
void ApplyState(const FirstPassState &st);
|
||||
IndexingSettings ShortAxisSettings(const DiffractionExperiment &x) const;
|
||||
void StartCentreSpeculation();
|
||||
void StartShortAxisSpeculation();
|
||||
|
||||
// --- State ---
|
||||
|
||||
Rugnux ℞
|
||||
Logger &logger;
|
||||
ProcessResult &result;
|
||||
const bool geometry_prepass;
|
||||
const int start_image;
|
||||
// Every frame the first pass reads is in the first part of the sweep (Rugnux::PrepassEnd).
|
||||
const int early_images;
|
||||
const std::shared_ptr<const JFJochReaderDataset> &dataset;
|
||||
AzimuthalIntegrationMapping &mapping;
|
||||
std::unique_ptr<IndexerThreadPool> &indexer_pool;
|
||||
std::unique_ptr<IndexAndRefine> &indexer;
|
||||
const float indexing_bound_A;
|
||||
std::future<LeftoverLatticeCensus> &census_future;
|
||||
|
||||
// Spots for a first-pass image ordinal, cached (a frame is reused across schemes and the
|
||||
// validation set; re-finding is expensive on the --redo-rotation-spots path). The workers below
|
||||
// hand their spots back and this thread puts them in, so the map itself is still only ever
|
||||
// touched from here and needs no locking.
|
||||
std::map<int, std::vector<SpotToSave>> spot_cache;
|
||||
// The azimuthal mapping the first pass finds its spots against. It is `mapping` until a
|
||||
// beam-centre hypothesis moves the centre, and the hypothesis's own mapping while it stands -
|
||||
// the mapping is where the resolution of every pixel and the finder's radial background bins
|
||||
// come from, and both move with the centre.
|
||||
const AzimuthalIntegrationMapping *spot_mapping = &mapping;
|
||||
|
||||
// The workers PrefetchSpots finds spots on, each with an analysis engine it keeps (see there).
|
||||
const int spot_workers;
|
||||
struct SpotEngine {
|
||||
// Before the analysis engine, which page-locks these bytes for its uploads and
|
||||
// unregisters them when it is destroyed: it must not outlive the buffer it was given.
|
||||
JFJochReaderRawImage raw_image;
|
||||
std::unique_ptr<MXAnalysisWithoutFPGA> analysis;
|
||||
std::unique_ptr<AzimuthalIntegrationProfile> profile;
|
||||
};
|
||||
std::vector<SpotEngine> engines;
|
||||
int prefetch_batches = 0;
|
||||
double prefetch_time_s = 0.0;
|
||||
|
||||
// Evaluating a beam-centre hypothesis means re-finding the spots at that centre. The spot
|
||||
// POSITIONS do not move with the centre - which is why the cache is keyed by image alone - but
|
||||
// the spot LIST does: every spot's d, the ice-ring flag, the resolution mask the finder
|
||||
// applies, the high-resolution gap cut and the strongest-N budget are all radial, and the
|
||||
// adaptive finder's ring background is binned about the centre. Scoring a trial centre against
|
||||
// spots found at the file's puts the trial in a world the run never enters: measured, a centre
|
||||
// scored 42/60 as a trial and 5/60 when that same value was given on the command line.
|
||||
//
|
||||
// A trial therefore gets its own azimuthal mapping, its own engines and its own cache, and the
|
||||
// starting centre's spots are PARKED rather than thrown away - a hypothesis that is not adopted
|
||||
// has to leave the rest of the run exactly as it found it.
|
||||
std::unique_ptr<AzimuthalIntegrationMapping> trial_mapping;
|
||||
std::map<int, std::vector<SpotToSave>> home_spot_cache;
|
||||
|
||||
// How many validation frames a candidate global lattice actually indexes per image, scored on
|
||||
// the *real* per-image path (force the candidate, then run the same indexing+refinement the
|
||||
// main loop uses). A cell that merely fits the accumulated first-pass cloud (a spurious
|
||||
// sub-lattice from an under-sampled scheme) indexes few frames, while the true cell indexes
|
||||
// many - this is the discriminator that lets the best scheme win.
|
||||
const std::vector<int> validation = rugnux_internal::select_equally_spaced_image_ordinals(
|
||||
early_images, std::min(early_images, 60));
|
||||
|
||||
const float index_tol = rx.experiment_.GetIndexingSettings().GetTolerance();
|
||||
|
||||
// The run's spot-finding settings, with indexing on: what a validation frame is indexed under.
|
||||
SpotFindingSettings validation_settings;
|
||||
|
||||
// How many spot-finding keys first_pass_spots_ holds; a handful covers a run, the oldest go first.
|
||||
static constexpr size_t MAX_SPOT_KEYS = 8;
|
||||
// How far ahead of itself FeedScheme keeps the spot cache.
|
||||
static constexpr int SPOT_PREFETCH_CHUNK = 64;
|
||||
size_t first_pass_spots_per_image = 0; // 0 = the indexer's own default
|
||||
|
||||
// Sampling schemes, tried in order of preference. We index each and keep the one that indexes
|
||||
// the most frames; the schemes cover complementary failure modes and no single one is best for
|
||||
// every crystal:
|
||||
// - "spread": images spread over the first part of the rotation (PrepassEnd) - wide angular range,
|
||||
// coarse stride; the long-standing default, best for typical crystals.
|
||||
// - "wedge": consecutive frames from the clean start - native stride keeps each reflection's
|
||||
// rocking curve continuous across frames, so the FFT can trace a long axis whose fine
|
||||
// reciprocal spacing the coarse spread cannot resolve (e.g. an F-cubic cell viewed down
|
||||
// [111]); bounded by the accumulation cap to the least radiation-damaged early wedge.
|
||||
std::vector<int> all_consecutive;
|
||||
std::vector<int> spread_ordinals;
|
||||
Schemes schemes;
|
||||
// A third sample of the same frames, asked only when nothing else indexes a majority (see
|
||||
// AddEndWedge): the wedge read back from the END of the first part. Which orientations a sweep
|
||||
// passes through first is chance, and the first part can open on frames that see the long axis
|
||||
// badly - measured on a sparse crystal with a ~390 A axis, the spread and the wedge over its
|
||||
// first 120 deg returned wrong cells (5/60 and 0/60 validation frames) at every spread density
|
||||
// tried, while consecutive frames from 60 deg on indexed 60/60. Still inside [start,
|
||||
// PrepassEnd), so nothing before pass 2 waits for the rest of the sweep.
|
||||
Schemes end_wedge;
|
||||
|
||||
// What the run is CURRENTLY reading each frame at. It starts at what the file asked for, and
|
||||
// only an ADOPTED rung moves it. Every rung the ladder tries is put back to this, so a rung
|
||||
// tried and rejected leaves nothing behind - and a later pass that finds no rung restores what
|
||||
// an earlier pass adopted rather than resetting the run to the defaults.
|
||||
const std::optional<float> first_pass_file_d_min;
|
||||
FirstPassState committed;
|
||||
|
||||
// The short-axis pass below indexes the same schemes at a lower floor, after the rescues in
|
||||
// between - and on nearly every run they change nothing it reads. So it is started here, on a
|
||||
// copy of the experiment, beside the standard indexing it would otherwise wait for, and taken
|
||||
// at its place only if what it read is still what the run has there (ExperimentKey, the spot
|
||||
// list's mapping and budget); otherwise it is run again there as before.
|
||||
struct ShortAxisSpeculation {
|
||||
std::vector<double> key;
|
||||
const AzimuthalIntegrationMapping *mapping = nullptr;
|
||||
size_t spots_per_image = 0;
|
||||
std::unique_ptr<DiffractionExperiment> experiment;
|
||||
std::unique_ptr<IndexerThreadPool> pool;
|
||||
std::vector<std::unique_ptr<RotationIndexer>> sris;
|
||||
std::vector<std::future<void>> futures; // last, so it is waited for before the rest goes
|
||||
};
|
||||
std::optional<ShortAxisSpeculation> short_spec;
|
||||
// The beam-centre check below runs a second first pass at the centre measured from the
|
||||
// background, and it reads the first one's answer only after that pass is over. So the spots at
|
||||
// the measured centre are found, and both schemes fed and indexed, here beside the file centre's
|
||||
// pass - on a copy of the experiment at that centre, with workers, a mapping and a spot list of
|
||||
// their own - and taken at the check's place only if what they read is still what the run has
|
||||
// there (the experiment with its rotation angles, SpotFindingKey at the measured centre, the spot
|
||||
// budget); otherwise that pass is run there as before. The schemes are scored there in either
|
||||
// case, on the run's own indexer.
|
||||
//
|
||||
// Only with a second GPU: on one card the two passes share the card and the reader, and measured,
|
||||
// running them side by side saved nothing. There the check runs its pass after the first, as it
|
||||
// always did. With two or more its spot finding spreads over all the cards, as the first pass's
|
||||
// does: the file centre's spot finding is over long before this pass's, so this one is what the
|
||||
// first pass waits on.
|
||||
struct CentreSpeculation {
|
||||
std::vector<double> experiment_key;
|
||||
std::vector<double> key;
|
||||
size_t spots_per_image = 0;
|
||||
std::unique_ptr<DiffractionExperiment> experiment;
|
||||
std::unique_ptr<PixelMask> mask;
|
||||
std::unique_ptr<AzimuthalIntegrationMapping> mapping;
|
||||
std::map<int, std::vector<SpotToSave>> spots;
|
||||
SchemeIndexers ris;
|
||||
std::future<void> done; // last, so it is waited for before the rest goes
|
||||
};
|
||||
std::optional<CentreSpeculation> centre_spec;
|
||||
};
|
||||
@@ -0,0 +1,500 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include <cstdlib>
|
||||
#include "Rugnux.h"
|
||||
#include "../image_analysis/structure_refinement/ModelValidation.h"
|
||||
#include "../writer/WriteModel.h"
|
||||
#include "../image_analysis/indexing/SpindleCuspLoss.h"
|
||||
#include "../image_analysis/bragg_integration/SpotWidth.h"
|
||||
#include "../image_analysis/bragg_integration/SpotFootprint.h"
|
||||
#include "../image_analysis/bragg_integration/BraggStencil.h" // BRAGG_FOOTPRINT_NSIGMA
|
||||
#include "../image_analysis/hot_pixels/HotPixels.h"
|
||||
#include "../image_analysis/SensorAbsorption.h"
|
||||
#include "DiagnosticOutput.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <fstream>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
#include <numeric>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
|
||||
#include "../reader/JFJochHDF5Reader.h"
|
||||
#include "../common/JFJochMath.h"
|
||||
#include "../common/ParallelFor.h"
|
||||
#include "../common/Logger.h"
|
||||
#include "../common/AzimuthalIntegrationMapping.h"
|
||||
#include "../common/AzimuthalIntegrationProfile.h"
|
||||
#include "../common/CUDAWrapper.h"
|
||||
#include "../common/JFJochException.h"
|
||||
#include "../common/time_utc.h"
|
||||
#include "../writer/FileWriter.h"
|
||||
#include "../image_analysis/MXAnalysisWithoutFPGA.h"
|
||||
#include "../image_analysis/beam_stop/ShadowFinder.h"
|
||||
#include "../image_analysis/IndexAndRefine.h"
|
||||
#include "../image_analysis/indexing/AnalyzeIndexing.h"
|
||||
#include "../image_analysis/indexing/HarmonicContamination.h"
|
||||
#include "../image_analysis/geom_refinement/BeamCenterFFT.h"
|
||||
#include "../image_analysis/geom_refinement/BeamCenterFromBackground.h"
|
||||
#include "../image_analysis/geom_refinement/GeometryRefiner.h"
|
||||
#include "../image_analysis/indexing/IndexerThreadPool.h"
|
||||
#include "../image_analysis/spot_finding/ImageSpotFinderCPU.h"
|
||||
#include "../image_analysis/spot_finding/SpotUtils.h"
|
||||
#include "gemmi/twin.hpp"
|
||||
#include "../image_analysis/azint/AzIntEngineCPU.h"
|
||||
#include "../image_analysis/image_preprocessing/ImagePreprocessorCPU.h"
|
||||
#include "../image_analysis/image_preprocessing/ImagePreprocessorBuffer.h"
|
||||
#ifdef JFJOCH_USE_CUDA
|
||||
#include "../image_analysis/image_preprocessing/ImagePreprocessorGPU.h"
|
||||
#include "../image_analysis/image_preprocessing/ImagePreprocessorBufferGPU.h"
|
||||
#include "../image_analysis/spot_finding/AdaptiveSpotFinderGPU.h"
|
||||
#endif
|
||||
#include "../image_analysis/scale_merge/Merge.h"
|
||||
#include "../image_analysis/scale_merge/RfreeFlags.h"
|
||||
#include "../image_analysis/scale_merge/RotationScaleMerge.h"
|
||||
#include "../image_analysis/scale_merge/ResolutionCutoff.h"
|
||||
#include "../image_analysis/scale_merge/ReindexAmbiguity.h"
|
||||
#include "../image_analysis/scale_merge/ScalingResult.h"
|
||||
#include "../image_analysis/scale_merge/SearchSpaceGroup.h"
|
||||
#include "../image_analysis/geom_refinement/PostRefine.h"
|
||||
#include "../image_analysis/lattice_search/LatticeSearch.h"
|
||||
#include "../image_analysis/lattice_search/LePageLattice.h"
|
||||
#include "../image_analysis/scale_merge/AnisotropyAnalysis.h"
|
||||
#include "../image_analysis/scale_merge/FrenchWilson.h"
|
||||
#include "../image_analysis/scale_merge/TwinningAnalysis.h"
|
||||
#include "../image_analysis/scale_merge/TranslationalNCS.h"
|
||||
#include "../image_analysis/scale_merge/HKLKey.h"
|
||||
#include "../image_analysis/scale_merge/ScaleOnTheFly.h"
|
||||
#include "../image_analysis/scale_merge/StillsPartialityRefine.h"
|
||||
#include "../image_analysis/WriteReflections.h"
|
||||
#include "../image_analysis/bragg_integration/CalcISigma.h"
|
||||
#include "../common/Definitions.h"
|
||||
#include "../common/CorrelationCoefficient.h"
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include "RugnuxPipeline.h"
|
||||
#include "RugnuxFirstPass.h"
|
||||
|
||||
// The end of the first pass: whether it found a lattice at all, the no-crystal test on the pooled
|
||||
// validation spots, and what the run takes from the lattice it accepts. FirstPassRotationIndexing
|
||||
// gives their order.
|
||||
|
||||
using namespace rugnux_internal;
|
||||
|
||||
namespace {
|
||||
// The two ways the first pass can end without a usable lattice that are about the DATA.
|
||||
// They are one situation from the user's side - no crystal was found in these frames - so
|
||||
// both name the cause and then give the same remedies.
|
||||
constexpr const char *next_steps =
|
||||
" Nothing was integrated. Check the beam centre (--beam-x / --beam-y), raise "
|
||||
"--max-spots if the frames are well populated with spots, and use --mode azint to "
|
||||
"write the per-image analysis for inspection.";
|
||||
}
|
||||
|
||||
void Rugnux::FirstPassRun::RequireLattice(const FirstPass &best) const {
|
||||
if (!best.result.has_value()) {
|
||||
// An indexer that FAILED is a third outcome, and none of those remedies fit it: it
|
||||
// never examined the frames, so nothing about the data or the processing options is
|
||||
// implicated and changing them cannot help. Name the failure instead.
|
||||
if (best.indexer_error)
|
||||
throw JFJochException(JFJochExceptionCategory::IndexerError,
|
||||
fmt::format("Rotation indexing could not run: {}. Nothing was "
|
||||
"integrated. The indexer never examined the frames, "
|
||||
"so this is a failure of the machine and not of the "
|
||||
"data - free the resource it needs (a GPU shared with "
|
||||
"another process is the usual cause) and run again.",
|
||||
*best.indexer_error));
|
||||
throw NoLatticeFound(std::string("Two-pass rotation indexing found no lattice.") + next_steps);
|
||||
}
|
||||
}
|
||||
|
||||
// Whether the run has a lattice at all is decided here, on the pooled spots rather than on
|
||||
// the frame count the rescues above are steered by. A frame count asks each frame to clear
|
||||
// a bar of its own and then counts the frames that did, which measures the spots per frame
|
||||
// as much as the lattice; the pooled fraction asks the whole validation set at once and is
|
||||
// judged against what the same lattice scores at a wrong spindle angle, so a sparse pattern
|
||||
// is refused only when its spots genuinely do not know about the lattice.
|
||||
auto Rugnux::FirstPassRun::MeasureVerdict(const FirstPass &best, const std::vector<double> &first_pass_key,
|
||||
const std::optional<ValidationSpotEvidence> &memo_hit) -> Verdict {
|
||||
const PooledEvidence evidence = MeasurePooled(*indexer, *best.result);
|
||||
const double pooled = static_cast<double>(evidence.on_lattice)
|
||||
/ static_cast<double>(std::max<int64_t>(1, evidence.spots));
|
||||
const double pooled_chance = static_cast<double>(evidence.by_chance)
|
||||
/ static_cast<double>(std::max<int64_t>(1, evidence.spots));
|
||||
result.validation_evidence = evidence;
|
||||
if (memo_hit && (memo_hit->spots != evidence.spots || memo_hit->on_lattice != evidence.on_lattice
|
||||
|| memo_hit->by_chance != evidence.by_chance))
|
||||
throw MemoMismatch(fmt::format("First-pass memo mismatch: stored {}/{}/{}, recomputed {}/{}/{}",
|
||||
memo_hit->on_lattice, memo_hit->spots, memo_hit->by_chance,
|
||||
evidence.on_lattice, evidence.spots, evidence.by_chance));
|
||||
// Kept only where the pass ended on the inputs it started from.
|
||||
if (!geometry_prepass && rx.FirstPassInputKey() == first_pass_key)
|
||||
rx.AddFirstPassMemo(FirstPassMemo{first_pass_key, evidence});
|
||||
return {evidence, pooled, pooled_chance};
|
||||
}
|
||||
|
||||
void Rugnux::FirstPassRun::RejectChanceLattice(const FirstPass &best, const Verdict &v) const {
|
||||
const PooledEvidence &evidence = v.evidence;
|
||||
const double pooled = v.pooled, pooled_chance = v.pooled_chance;
|
||||
if (!ValidationEvidenceBeatsChance(evidence)) {
|
||||
{
|
||||
// Name the cell and Bravais class that was rejected. The commonest cause is a metric
|
||||
// symmetry promoted one class too far - the constrained cell then misses every
|
||||
// reflection by the small angle the constraint snapped away - and without the cell in
|
||||
// the message there is nothing to see that from.
|
||||
const auto &c = best.result->search_result.conventional.GetUnitCell();
|
||||
throw NoLatticeFound(fmt::format("Two-pass rotation indexing found only a lattice that "
|
||||
"takes {:.1f}% of the validation spots against the "
|
||||
"{:.1f}% a wrong spindle angle takes ({} distinct "
|
||||
"reflections against {} over {} wrong angles) - it is "
|
||||
"not this crystal's lattice. It was {}-centred {}, "
|
||||
"{:.2f} {:.2f} {:.2f} {:.2f} {:.2f} {:.2f}.{}",
|
||||
100.0 * pooled, 100.0 * pooled_chance,
|
||||
evidence.reflections, evidence.reflections_by_chance,
|
||||
VALIDATION_NULL_DISPLACEMENTS,
|
||||
best.result->search_result.centering,
|
||||
gemmi::crystal_system_str(best.result->search_result.system),
|
||||
c.a, c.b, c.c, c.alpha, c.beta, c.gamma, next_steps));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Rugnux::FirstPassRun::AdoptLattice(const FirstPass &best, const Verdict &v) {
|
||||
const double pooled = v.pooled, pooled_chance = v.pooled_chance;
|
||||
const PooledEvidence &evidence = v.evidence;
|
||||
indexer->ForceRotationIndexerResult(*best.result);
|
||||
// A lattice the pooled spots accept while most validation frames fail the per-frame floor
|
||||
// (LATTICE_MIN_INDEXED_FRACTION) on their own, because the sweep as a whole puts fewer of its
|
||||
// spots on the lattice than that floor asks of each frame: the crystal is real but its
|
||||
// spots are a minority of every frame - a weak crystal inside a powder of its own
|
||||
// microcrystals and ice. The floor cannot tell its frames from anything else, so integrate
|
||||
// every frame from the sweep's lattice, as XDS and DIALS do, and leave the frames to the
|
||||
// scaling. The frames that do clear it are the upper tail of the same population: measured
|
||||
// on a crystal in a powder of itself (13% of the pooled spots on the lattice), 12/60
|
||||
// cleared it, and keeping the floor integrated 15% of the sweep. A sweep that is sparse
|
||||
// only in spots per frame (a small molecule) keeps the floor.
|
||||
const bool sparse_lattice = best.score < static_cast<int>(validation.size()) / 2
|
||||
&& pooled < LATTICE_MIN_INDEXED_FRACTION;
|
||||
indexer->IntegrateEveryFrame(sparse_lattice);
|
||||
if (sparse_lattice)
|
||||
logger.Warning("Sparse lattice: {}/{} validation frames clear the per-frame floor, but the "
|
||||
"pooled spots put {:.1f}% on the lattice against {:.1f}% by chance - every "
|
||||
"frame is integrated from the sweep's lattice",
|
||||
best.score, static_cast<int>(validation.size()), 100.0 * pooled,
|
||||
100.0 * pooled_chance);
|
||||
logger.Info("First-pass spot finding: {} frames in {} batches, {:.2f} s",
|
||||
spot_cache.size(), prefetch_batches, prefetch_time_s);
|
||||
logger.Info("Two-pass rotation indexing found lattice (scheme '{}': {}/{} validation frames, "
|
||||
"{}/{} validation spots = {:.1f}% against {:.1f}% at a wrong spindle angle; {} "
|
||||
"distinct reflections against {} over {} wrong angles)",
|
||||
best.name, best.score, static_cast<int>(validation.size()),
|
||||
evidence.on_lattice, evidence.spots, 100.0 * pooled, 100.0 * pooled_chance,
|
||||
evidence.reflections, evidence.reflections_by_chance, VALIDATION_NULL_DISPLACEMENTS);
|
||||
{
|
||||
const auto &c = best.result->search_result.conventional.GetUnitCell();
|
||||
rx.Note(fmt::format("lattice {} {}: {:.2f} {:.2f} {:.2f} {:.1f} {:.1f} {:.1f}",
|
||||
gemmi::crystal_system_str(best.result->search_result.system),
|
||||
best.result->search_result.centering, c.a, c.b, c.c, c.alpha, c.beta, c.gamma));
|
||||
rx.Note(fmt::format("{}/{} validation frames, {:.0f}% of their spots on it",
|
||||
best.score, validation.size(), 100.0 * pooled));
|
||||
}
|
||||
}
|
||||
|
||||
// Leftover-lattice census, report only: index what the crystal's lattice leaves over, to name
|
||||
// a split crystal, a twin domain or a second crystal in the report. Every spot of the spread
|
||||
// and validation frames the lattice takes is marked; the rest (not on ice) goes to a fresh
|
||||
// rotation indexer; a lattice it finds is judged on the validation leftovers against the same
|
||||
// displaced-spindle-angle null as the first pass, its spots are marked in turn, and the search
|
||||
// goes on for up to three lattices. Its own IndexAndRefine instances and indexer, so nothing
|
||||
// the run goes on with is touched. Run on the pass that writes the result only.
|
||||
//
|
||||
// Its spots are found here; the rest runs in the background, beside the image loop, on
|
||||
// copies of everything it reads, and its answer is collected before the pass returns. Not on
|
||||
// a pass that only post-refines: nothing reads its result there.
|
||||
void Rugnux::FirstPassRun::StartLeftoverCensus(const FirstPass &best) {
|
||||
std::vector<int> census_frames = spread_ordinals;
|
||||
census_frames.insert(census_frames.end(), validation.begin(), validation.end());
|
||||
std::ranges::sort(census_frames);
|
||||
census_frames.erase(std::unique(census_frames.begin(), census_frames.end()), census_frames.end());
|
||||
PrefetchSpots(census_frames);
|
||||
std::map<int, std::vector<SpotToSave>> census_spots;
|
||||
for (const int f : census_frames)
|
||||
census_spots.emplace(f, spot_cache.at(f));
|
||||
census_future = std::async(std::launch::async,
|
||||
[&logger = logger, &indexer_pool = indexer_pool, &config_ = rx.config_,
|
||||
x = rx.experiment_, census_frames, census_spots, lattice = *best.result,
|
||||
validation = validation, spread_ordinals = spread_ordinals,
|
||||
validation_settings = validation_settings]() -> LeftoverLatticeCensus {
|
||||
const auto census_start = std::chrono::steady_clock::now();
|
||||
const bool ice_ok = x.GetIndexingSettings().GetIndexIceRings();
|
||||
const auto &spot_cache = census_spots;
|
||||
const auto rot_angle = [&](int ordinal) -> std::optional<float> {
|
||||
if (const auto g = x.GetGoniometer())
|
||||
return g->GetAngle_deg(static_cast<float>(ordinal)) + g->GetWedge_deg() / 2.0f;
|
||||
return std::nullopt;
|
||||
};
|
||||
// assign[frame][spot]: -2 ice, -1 no lattice, 0 the crystal's lattice, 1.. a leftover lattice
|
||||
std::map<int, std::vector<int>> assign;
|
||||
for (const int f : census_frames) {
|
||||
auto &a = assign[f];
|
||||
a.assign(spot_cache.at(f).size(), -1);
|
||||
for (size_t k = 0; k < a.size(); k++)
|
||||
if (!ice_ok && spot_cache.at(f)[k].ice_ring)
|
||||
a[k] = -2;
|
||||
}
|
||||
// Mark the still unassigned spots lattice `r` takes as `id`.
|
||||
auto mark = [&](const RotationIndexerResult &r, int id) {
|
||||
IndexAndRefine idx(x, indexer_pool.get(), /*retain_outcomes=*/false);
|
||||
idx.ForceRotationIndexerResult(r);
|
||||
idx.IntegrateEveryFrame(true);
|
||||
std::mutex m_assign;
|
||||
ParallelFor(static_cast<int>(census_frames.size()),
|
||||
std::min<size_t>(census_frames.size(), config_.nthreads), [&](int i) {
|
||||
const int f = census_frames[i];
|
||||
DataMessage m{};
|
||||
m.number = f;
|
||||
m.spots = spot_cache.at(f);
|
||||
idx.CountSpotsOnLattice(m, validation_settings);
|
||||
std::unique_lock ul(m_assign);
|
||||
auto &a = assign[f];
|
||||
if (m.spots.size() != a.size())
|
||||
return;
|
||||
for (size_t k = 0; k < a.size(); k++)
|
||||
if (a[k] == -1 && m.spots[k].indexed && m.spots[k].lattice == 0)
|
||||
a[k] = id;
|
||||
});
|
||||
};
|
||||
// Leftover validation spots on `r` at the true spindle angles, and the median of five
|
||||
// displaced ones - the first pass's null (MeasurePooled) on the leftovers.
|
||||
auto leftover_evidence = [&](const RotationIndexerResult &r) -> std::pair<int64_t, int64_t> {
|
||||
IndexAndRefine idx(x, indexer_pool.get(), /*retain_outcomes=*/false);
|
||||
idx.ForceRotationIndexerResult(r);
|
||||
const int nv = static_cast<int>(validation.size());
|
||||
auto at_shift = [&](int shift) {
|
||||
std::atomic<int64_t> n = 0;
|
||||
ParallelFor(nv, std::min<size_t>(validation.size(), config_.nthreads), [&](int i) {
|
||||
DataMessage m{};
|
||||
m.number = validation[(i + shift) % nv];
|
||||
const auto &sp = spot_cache.at(validation[i]);
|
||||
const auto &a = assign.at(validation[i]);
|
||||
for (size_t k = 0; k < sp.size(); k++)
|
||||
if (a[k] == -1)
|
||||
m.spots.push_back(sp[k]);
|
||||
n += idx.CountSpotsOnLattice(m, validation_settings);
|
||||
});
|
||||
return n.load();
|
||||
};
|
||||
std::vector<int64_t> chance;
|
||||
for (int k = 1; k <= 5; k++)
|
||||
chance.push_back(at_shift(std::max(1, k * nv / 7)));
|
||||
std::ranges::sort(chance);
|
||||
return {at_shift(0), chance[2]};
|
||||
};
|
||||
// Spots and summed intensity assigned `id`, and the share of them in the 2 densest of 8
|
||||
// equal blocks of the census frames (0.25 is uniform over the sweep).
|
||||
struct Tally { int64_t n = 0; double intensity = 0.0; double concentration = 0.0; };
|
||||
auto tally = [&](int id) {
|
||||
Tally t;
|
||||
std::array<int64_t, 8> block{};
|
||||
const size_t nf = census_frames.size();
|
||||
for (size_t i = 0; i < nf; i++) {
|
||||
const int f = census_frames[i];
|
||||
const auto &a = assign.at(f);
|
||||
for (size_t k = 0; k < a.size(); k++)
|
||||
if (a[k] == id) {
|
||||
t.n++;
|
||||
t.intensity += spot_cache.at(f)[k].intensity;
|
||||
// numpy.array_split: the first nf % 8 blocks hold one frame more
|
||||
const size_t q = nf / 8, r = nf % 8;
|
||||
block[i < r * (q + 1) ? i / (q + 1) : r + (i - r * (q + 1)) / std::max<size_t>(1, q)]++;
|
||||
}
|
||||
}
|
||||
std::ranges::sort(block);
|
||||
t.concentration = static_cast<double>(block[6] + block[7]) / std::max<int64_t>(1, t.n);
|
||||
return t;
|
||||
};
|
||||
auto reduced = [](const RotationIndexerResult &r) {
|
||||
return LatticeSearch(r.lattice.ToPrimitive(r.search_result.centering)).primitive_reduced;
|
||||
};
|
||||
|
||||
mark(lattice, 0);
|
||||
int64_t n_all = 0;
|
||||
double i_all = 0.0;
|
||||
for (const auto &[f, a] : assign)
|
||||
for (size_t k = 0; k < a.size(); k++)
|
||||
if (a[k] != -2) {
|
||||
n_all++;
|
||||
i_all += spot_cache.at(f)[k].intensity;
|
||||
}
|
||||
const Tally t0 = tally(0);
|
||||
LeftoverLatticeCensus census;
|
||||
census.frames = static_cast<int>(census_frames.size());
|
||||
census.primary_spots_pct = 100.0 * t0.n / std::max<int64_t>(1, n_all);
|
||||
census.primary_intensity_pct = 100.0 * t0.intensity / std::max(1.0, i_all);
|
||||
const CrystalLattice primary_reduced = reduced(lattice);
|
||||
for (int id = 1; id <= 3; id++) {
|
||||
RotationIndexer ri(x, *indexer_pool);
|
||||
ri.VerifyMemo(config_.verify_first_pass_memo);
|
||||
for (const int f : spread_ordinals) {
|
||||
std::vector<SpotToSave> left;
|
||||
const auto &sp = spot_cache.at(f);
|
||||
const auto &a = assign.at(f);
|
||||
for (size_t k = 0; k < sp.size(); k++)
|
||||
if (a[k] == -1)
|
||||
left.push_back(sp[k]);
|
||||
ri.ProcessImage(f, left, rot_angle(f));
|
||||
}
|
||||
ri.RunIndexing();
|
||||
const auto r = ri.GetLattice();
|
||||
if (!r)
|
||||
break;
|
||||
const auto [real, chance] = leftover_evidence(*r);
|
||||
mark(*r, id);
|
||||
const Tally t = tally(id);
|
||||
const CrystalLattice r_reduced = reduced(*r);
|
||||
LeftoverLattice l;
|
||||
l.cell = r->search_result.conventional.GetUnitCell();
|
||||
l.centering = r->search_result.centering;
|
||||
l.system = gemmi::crystal_system_str(r->search_result.system);
|
||||
l.volume_ratio = std::abs(r_reduced.CalcVolume()) / std::max(1.0f, std::abs(primary_reduced.CalcVolume()));
|
||||
l.misorientation_deg = LatticeMisorientationDeg(primary_reduced, r_reduced);
|
||||
l.z = (real - chance) / std::sqrt(std::max<double>(1.0, chance));
|
||||
l.spots_pct = 100.0 * t.n / std::max<int64_t>(1, n_all);
|
||||
l.intensity_pct = 100.0 * t.intensity / std::max(1.0, i_all);
|
||||
l.sweep_concentration = t.concentration;
|
||||
l.kind = ClassifyLeftoverLattice(l.misorientation_deg, l.volume_ratio, l.sweep_concentration);
|
||||
logger.Info("Leftover lattice {}: {}-centred {} {:.2f} {:.2f} {:.2f} {:.2f} {:.2f} {:.2f}, "
|
||||
"volume ratio {:.3f}, misorientation {:.2f} deg, z {:.1f} ({} vs {} at a wrong "
|
||||
"angle), {:.1f}% of non-ice spots, {:.1f}% of their intensity, sweep "
|
||||
"concentration {:.2f} -> {}",
|
||||
id, l.centering, l.system, l.cell.a, l.cell.b, l.cell.c, l.cell.alpha, l.cell.beta,
|
||||
l.cell.gamma, l.volume_ratio, l.misorientation_deg, l.z, real, chance, l.spots_pct,
|
||||
l.intensity_pct, l.sweep_concentration, LeftoverLatticeKindName(l.kind));
|
||||
if (l.z >= 5.0 && l.kind != LeftoverLatticeKind::Self)
|
||||
census.lattices.push_back(l);
|
||||
if (real - chance < 3.0 * std::sqrt(std::max<double>(1.0, chance)) + 5)
|
||||
break;
|
||||
}
|
||||
logger.Info("Leftover-lattice census: the crystal's lattice takes {:.1f}% of {} non-ice spots "
|
||||
"({:.1f}% of their intensity) on {} frames; {} further lattice(s); {:.1f} s",
|
||||
census.primary_spots_pct, n_all, census.primary_intensity_pct, census.frames,
|
||||
census.lattices.size(),
|
||||
std::chrono::duration<double>(std::chrono::steady_clock::now() - census_start).count());
|
||||
return census;
|
||||
});
|
||||
}
|
||||
|
||||
void Rugnux::FirstPassRun::ReportTiltWalk(const FirstPass &best) {
|
||||
// What the walked tilt does to the prediction: the keystone at the far corner of the
|
||||
// detector, against the disc the integration sums over, and the whole-pattern shift
|
||||
// it is equivalent to, which the beam centre carries instead where it is refused.
|
||||
const auto &tw = *best.result->tilt_walk;
|
||||
const auto &g = best.result->geom;
|
||||
const double start_rot1 = tw.refused ? g.GetPoniRot1_rad() : rx.experiment_.GetPoniRot1_rad();
|
||||
const double start_rot2 = tw.refused ? g.GetPoniRot2_rad() : rx.experiment_.GetPoniRot2_rad();
|
||||
const double walk_rad = std::hypot(tw.tilt_rad[0] - start_rot1, tw.tilt_rad[1] - start_rot2);
|
||||
const double corner_px = std::hypot(
|
||||
std::max<double>(g.GetBeamX_pxl(), rx.experiment_.GetXPixelsNumConv() - g.GetBeamX_pxl()),
|
||||
std::max<double>(g.GetBeamY_pxl(), rx.experiment_.GetYPixelsNumConv() - g.GetBeamY_pxl()));
|
||||
const double lever_px = g.GetDetectorDistance_mm() / g.GetPixelSize_mm();
|
||||
const std::string what = fmt::format(
|
||||
"Detector tilt: the free fit walked to {:.4f},{:.4f} deg, {:.2f} deg from the "
|
||||
"{:.4f},{:.4f} deg this pass started at - further than a mounted detector is off "
|
||||
"square by - which moves the far corner of the detector by {:.0f} px against an "
|
||||
"integration disc of {:.1f} px, so the lattice was refined again at the starting "
|
||||
"tilt with the beam centre free to take the {:.0f} px shift the tilt is equivalent "
|
||||
"to, and the two were judged on the spots they index: {:.0f} at the walked tilt "
|
||||
"against {:.0f} at the starting one",
|
||||
tw.tilt_rad[0] * 180.0 / PI, tw.tilt_rad[1] * 180.0 / PI, walk_rad * 180.0 / PI,
|
||||
start_rot1 * 180.0 / PI, start_rot2 * 180.0 / PI,
|
||||
walk_rad * corner_px * corner_px / lever_px,
|
||||
rx.experiment_.GetBraggIntegrationSettings().GetR1(), walk_rad * lever_px,
|
||||
tw.spots_walked, tw.spots_held);
|
||||
if (tw.refused)
|
||||
logger.Warning("{} - the spots cannot tell the two apart, the walked tilt measured "
|
||||
"nothing, and the pass runs at the tilt it started at", what);
|
||||
else
|
||||
logger.Warning("{} - the spots confirm the walked tilt, and the pass runs at it; the "
|
||||
"file misdescribes this detector's tilt", what);
|
||||
}
|
||||
|
||||
void Rugnux::FirstPassRun::MeasureSpotBudget(const FirstPass &best) {
|
||||
// The spots in hand were already cut to the budget in force, so the measurement can only
|
||||
// shorten a budget, never lengthen one - which is why RunAllPasses hands the second pass the
|
||||
// list the first one started from rather than the list the first one settled on.
|
||||
const int64_t budget = MeasureBudget(*indexer, *best.result);
|
||||
// The measurement can honestly land under MIN_SPOT_COUNT, a floor written for a
|
||||
// stills FIT, where fewer spots cannot pin a lattice down. Here the lattice is
|
||||
// already in hand and merely tested - but the validation gate still needs
|
||||
// ViableCellMinSpots indexed spots per frame (AnalyzeIndexing), so a shorter list
|
||||
// could never index anything: keep the floor's worth of spots rather than die on
|
||||
// the setter. A budget that shallow is a finding in its own right - the lattice and
|
||||
// the file's geometry explain only the brightest few detections (measured on a
|
||||
// header detector distance 20% off, compensated by a uniformly scaled cell that
|
||||
// only matches near the beam centre) - so it is warned about, not just adopted.
|
||||
const int64_t adopted = std::max<int64_t>(budget, MIN_SPOT_COUNT);
|
||||
if (budget > 0 && adopted < rx.experiment_.GetMaxSpotCount()) {
|
||||
if (adopted != budget)
|
||||
logger.Warning("Spot budget: only the strongest {} spots per image lie on "
|
||||
"the lattice (measured on {} frames) - fewer than the {} the "
|
||||
"frame gate can act on, so {} are kept. A budget this shallow "
|
||||
"usually means the geometry in the file (detector distance, "
|
||||
"beam centre) or the lattice is wrong, not that the crystal "
|
||||
"is weak", budget, static_cast<int>(validation.size()),
|
||||
MIN_SPOT_COUNT, adopted);
|
||||
else
|
||||
logger.Info("Spot budget: keeping the strongest {} spots per image "
|
||||
"(measured on {} frames; was {})", budget,
|
||||
static_cast<int>(validation.size()), rx.experiment_.GetMaxSpotCount());
|
||||
rx.experiment_.MaxSpotCount(adopted);
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: compare the de-novo lattice with pass 1's HERE, before integrating every image
|
||||
// with it. A markedly larger cell is the bistable supercell collapse, which was already
|
||||
// rejected in Run() - but only after the whole pass had run, so it cost an entire extra pass
|
||||
// on a dataset that ended up on pass-1's lattice anyway. Take pass-1's result now instead:
|
||||
// the same remedy, one pass earlier.
|
||||
void Rugnux::FirstPassRun::Pass1SupercellGuard(const FirstPass &best) {
|
||||
// Primitive volumes, like the scheme comparison above: a centred conventional cell is an
|
||||
// exact integer multiple of its primitive one, so two settings of the SAME lattice differ
|
||||
// by that factor and comparing the conventional cells reads a mere change of setting as a
|
||||
// supercell - which then forces pass 1's setting and, with it, its lower symmetry.
|
||||
const double v1 = std::abs(rx.prepass_result_->lattice
|
||||
.ToPrimitive(rx.prepass_result_->search_result.centering).CalcVolume());
|
||||
const double v2 = std::abs(best.result->lattice
|
||||
.ToPrimitive(best.result->search_result.centering).CalcVolume());
|
||||
const bool supercell = v1 > 1.0 && v2 > 1.5 * v1;
|
||||
// Dropping to the primitive sub-cell of pass-1's centred lattice is the same
|
||||
// disagreement seen from the other side, and the volumes above cannot see it: the two
|
||||
// have the SAME primitive metric. Integrating there never predicts the reflections the
|
||||
// centring implies, and the pass then merges in P1 (measured on a C-centred monoclinic
|
||||
// crystal). Only this direction - a centring pass 2 finds where pass 1 had none is the
|
||||
// refined geometry doing its job, and the intensity test in the search confirms it.
|
||||
// Same primitive metric only: a pass 2 that lands on a primitive cell several times
|
||||
// smaller is a different lattice - pass 1's supercell corrected - not a dropped centring.
|
||||
const bool lost_centring = rx.prepass_result_->search_result.centering != 'P'
|
||||
&& best.result->search_result.centering == 'P'
|
||||
&& v1 > 1.0 && std::abs(v2 / v1 - 1.0) < 0.1;
|
||||
if (supercell || lost_centring) {
|
||||
logger.Info("Two-pass: that lattice ({}-centred, volume {:.0f} A^3) disagrees with "
|
||||
"pass 1 ({}-centred, {:.0f} A^3) - integrating with pass-1's lattice instead",
|
||||
best.result->search_result.centering, v2,
|
||||
rx.prepass_result_->search_result.centering, v1);
|
||||
RotationIndexerResult forced = *rx.prepass_result_;
|
||||
if (rx.prepass_rotation_scale_ && forced.axis)
|
||||
forced.axis = ScaleRotation(*forced.axis, *rx.prepass_rotation_scale_);
|
||||
indexer->ForceRotationIndexerResult(forced);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user