diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 179f7f311..9c4e6af05 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -134,7 +134,7 @@ bool ValidationEvidenceBeatsChance(const ValidationSpotEvidence &e, double signi RotationScaleWalk WalkRotationScale(double first_fit, const std::function &index_at, const std::function(float)> &refit_at, - int max_rounds) { + int max_rounds, int64_t sweep_images) { RotationScaleWalk walk; auto next = static_cast(first_fit); if (next == walk.scale) @@ -145,13 +145,23 @@ RotationScaleWalk WalkRotationScale(double first_fit, }; walk.evidence = index_at(walk.scale); walk.trail = fmt::format("{:.5f}: {:.1f}%", walk.scale, score(walk.evidence)); + double last_step = 0.0; for (int round = 0; round < max_rounds && next != walk.scale; ++round) { const ValidationSpotEvidence e = index_at(next); walk.trail += fmt::format(", {:.5f}: {:.1f}%", next, score(e)); if (!ValidationEvidencePrefers(walk.evidence, e)) break; + const double step = next - walk.scale; walk.scale = next; walk.evidence = e; + // Each refit reads a share of the error left, so the steps contract by about the ratio of the + // last two. Where the step that ratio predicts moves the end of the sweep by less than half a + // frame, the scale it would reach indexes the frames as this one does, and the refit is not made. + if (last_step != 0.0 && std::fabs(step * step / last_step) * static_cast(sweep_images) < 0.5) { + walk.trail += fmt::format(" (next step predicted {:.5f}, under half a frame)", step * step / last_step); + break; + } + last_step = step; const auto fit = refit_at(walk.scale); if (!fit) break; diff --git a/rugnux/Rugnux.h b/rugnux/Rugnux.h index 5bba0695a..e1d703bba 100644 --- a/rugnux/Rugnux.h +++ b/rugnux/Rugnux.h @@ -284,6 +284,7 @@ std::pair ValidationEvidenceDifference(const ValidationSpotEvide // index_at(k): the validation evidence of the lattice indexed with the stored angles scaled by k // refit_at(k): the scale the post-refinement fits on reflections integrated at k, relative to k; // empty where it fitted none +// sweep_images: the frames of the sweep, against which a step too small to refit for is measured struct RotationScaleWalk { float scale = 1.0f; // adopted; 1 = the stored angles stand ValidationSpotEvidence evidence; // at the adopted scale @@ -292,7 +293,7 @@ struct RotationScaleWalk { RotationScaleWalk WalkRotationScale(double first_fit, const std::function &index_at, const std::function(float)> &refit_at, - int max_rounds); + int max_rounds, int64_t sweep_images); // The first pass found no lattice of the crystal in the frames - what a sample with no crystal in the // beam ends in. A statement about the data, not an error in the input: the CLI writes a report with diff --git a/rugnux/RugnuxPasses.cpp b/rugnux/RugnuxPasses.cpp index 986c273f6..4369dd9af 100644 --- a/rugnux/RugnuxPasses.cpp +++ b/rugnux/RugnuxPasses.cpp @@ -729,7 +729,8 @@ RotationScaleWalk Rugnux::WalkRotationScaleBeside(const ProcessResult &pass1, Ro }; constexpr int MAX_ROTATION_SCALE_ROUNDS = 8; const RotationScaleWalk walk = WalkRotationScale(pass1.post_refine->rotation_scale, index_at, - refit_at, MAX_ROTATION_SCALE_ROUNDS); + refit_at, MAX_ROTATION_SCALE_ROUNDS, + experiment_.GetImageNum()); std::string text; if (!walk.trail.empty()) { text = fmt::format( diff --git a/tests/RotationScaleWalkTest.cpp b/tests/RotationScaleWalkTest.cpp index 617610485..7f942cca4 100644 --- a/tests/RotationScaleWalkTest.cpp +++ b/tests/RotationScaleWalkTest.cpp @@ -31,9 +31,9 @@ namespace { return 1.0 + 0.7 * (true_scale / k - 1.0); } - RotationScaleWalk Walk(double first_fit) { + RotationScaleWalk Walk(double first_fit, int64_t sweep_images = 3600) { return WalkRotationScale(first_fit, [this](float k) { return IndexAt(k); }, - [this](float k) { return RefitAt(k); }, 8); + [this](float k) { return RefitAt(k); }, 8, sweep_images); } }; } @@ -65,6 +65,22 @@ TEST_CASE("WalkRotationScale_ReachesTheFixedPoint", "[RotationScale]") { CHECK_FALSE(walk.trail.empty()); } +TEST_CASE("WalkRotationScale_StopsWhereTheNextStepIsUnderHalfAFrame", "[RotationScale]") { + // Spots enough for the validation frames to prefer every step: what ends the walk is the step the + // contraction predicts, measured against the sweep. Each refit reads 70 % of the error left, so the + // steps contract by 0.3. + SyntheticSweep short_sweep{0.97}; + short_sweep.spots = 1000000000; + const auto a = short_sweep.Walk(short_sweep.RefitAt(1.0f).value(), 360); + SyntheticSweep long_sweep{0.97}; + long_sweep.spots = 1000000000; + const auto b = long_sweep.Walk(long_sweep.RefitAt(1.0f).value(), 36000); + CHECK(short_sweep.refit_calls < long_sweep.refit_calls); + // Where it stops, what is left of the error moves the end of the sweep by under a frame. + CHECK(std::fabs(a.scale - 0.97) * 360 < 1.0); + CHECK(std::fabs(b.scale - 0.97) * 36000 < 1.0); +} + TEST_CASE("WalkRotationScale_StoredAnglesStand", "[RotationScale]") { SECTION("A healthy stage: a fit off by noise scores no better than the stored angles") { SyntheticSweep sweep{1.0};