Files
Jungfraujoch/tests/SpindleCuspLossTest.cpp
leonarski_fandClaude Opus 5.5 0c1c963379 Move SpotWidth to bragg_integration/, SpindleCuspLoss and LeftoverLattices to indexing/
A pure move of three method files out of rugnux/: SpotWidth (spot width and
bandwidth, which set the integration radius) joins JFJochBraggIntegration, which now
links JFJochImagePreprocessing for the ImagePreprocessorBuffer it reads;
SpindleCuspLoss and LeftoverLattices join JFJochIndexing. Only include paths and CMake
lists change.

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

94 lines
4.9 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "../image_analysis/indexing/SpindleCuspLoss.h"
using Catch::Matchers::WithinAbs;
namespace {
// lambda = 1 A against d_min = 1 / (2 sin 15 deg) puts theta_max at exactly 15 deg.
constexpr double WVL = 1.0;
const double D_MIN = 1.0 / (2.0 * std::sin(15.0 * M_PI / 180.0));
Coord Perpendicular(const Coord &v) {
const Coord seed = std::fabs(v.Normalize().x) < 0.9f ? Coord(1, 0, 0) : Coord(0, 1, 0);
return (v % seed).Normalize();
}
}
TEST_CASE("SpindleCuspLoss_Orbit", "[Rugnux][Spindle]") {
SECTION("P1 repairs nothing, anywhere") {
const CrystalLattice latt(UnitCell{40, 50, 60, 83, 95, 102});
const auto r = SpindleUnrepairedFraction(*gemmi::find_spacegroup_by_name("P 1"), latt,
Coord(0.3f, -0.5f, 0.8f), WVL, D_MIN);
REQUIRE(r.has_value());
CHECK_THAT(r->cone_fraction, WithinAbs(1.0, 1e-9));
// Radially weighted double-cone content at theta_max = 15 deg (verified by Monte Carlo).
CHECK_THAT(r->lost_unique_fraction, WithinAbs(0.0203, 0.0005));
CHECK_THAT(r->theta_max_deg, WithinAbs(15.0, 1e-6));
}
SECTION("a lone diad on the spindle, and one perpendicular to it, both lose the whole cone") {
const CrystalLattice latt(UnitCell{40, 50, 60, 90, 100, 90}); // P2, unique axis b
const auto &sg = *gemmi::find_spacegroup_by_name("P 2");
const Coord diad = latt.Vec1(); // b is perpendicular to a and c, so it IS the axis
const auto on = SpindleUnrepairedFraction(sg, latt, diad, WVL, D_MIN);
REQUIRE(on.has_value());
CHECK_THAT(on->cone_fraction, WithinAbs(1.0, 1e-4));
const auto perp = SpindleUnrepairedFraction(sg, latt, Perpendicular(diad), WVL, D_MIN);
REQUIRE(perp.has_value());
CHECK_THAT(perp->cone_fraction, WithinAbs(1.0, 1e-4));
}
SECTION("the same diad at 60 deg from the spindle loses nothing") {
const CrystalLattice latt(UnitCell{40, 50, 60, 90, 100, 90});
const Coord diad = latt.Vec1().Normalize();
const Coord spindle = diad * 0.5f + Perpendicular(diad) * static_cast<float>(std::sqrt(0.75));
const auto r = SpindleUnrepairedFraction(*gemmi::find_spacegroup_by_name("P 2"), latt,
spindle, WVL, D_MIN);
REQUIRE(r.has_value());
CHECK_THAT(r->lost_unique_fraction, WithinAbs(0.0, 1e-9));
}
SECTION("an axis of order >= 3 perpendicular to the spindle repairs the cone completely") {
// The situation the per-image worst-case bound must flag but the known group clears.
const CrystalLattice latt(UnitCell{50, 50, 60, 90, 90, 120});
const Coord three_fold = latt.Vec2(); // c, the 3-fold of P3
const auto r = SpindleUnrepairedFraction(*gemmi::find_spacegroup_by_name("P 3"), latt,
Perpendicular(three_fold), WVL, D_MIN);
REQUIRE(r.has_value());
CHECK_THAT(r->lost_unique_fraction, WithinAbs(0.0, 1e-9));
}
SECTION("a dihedral group repairs an in-plane diad the warning heuristic used to flag") {
// 622 with an in-plane 2-fold exactly on the spindle: the old any-axis-within-15-deg rule
// warned here, but the principal 6-fold maps the cone off itself and nothing is lost.
const CrystalLattice latt(UnitCell{50, 50, 60, 90, 90, 120});
const auto r = SpindleUnrepairedFraction(*gemmi::find_spacegroup_by_name("P 6 2 2"), latt,
latt.Vec0(), WVL, D_MIN);
REQUIRE(r.has_value());
CHECK_THAT(r->lost_unique_fraction, WithinAbs(0.0, 1e-9));
}
SECTION("a cubic group is never severe, whatever the mounting") {
const CrystalLattice latt(UnitCell{60, 60, 60, 90, 90, 90});
const auto &sg = *gemmi::find_spacegroup_by_name("P 4 3 2");
for (const auto &spindle : {Coord(1, 0, 0), Coord(0.3f, -0.5f, 0.8f), Coord(1, 1, 1)}) {
const auto r = SpindleUnrepairedFraction(sg, latt, spindle, WVL, D_MIN);
REQUIRE(r.has_value());
CHECK_THAT(r->lost_unique_fraction, WithinAbs(0.0, 1e-9));
}
}
SECTION("no spindle, no wavelength or no resolution gives no answer") {
const CrystalLattice latt(UnitCell{40, 50, 60, 90, 90, 90});
const auto &sg = *gemmi::find_spacegroup_by_name("P 1");
CHECK_FALSE(SpindleUnrepairedFraction(sg, latt, Coord(0, 0, 0), WVL, D_MIN).has_value());
CHECK_FALSE(SpindleUnrepairedFraction(sg, latt, Coord(0, 0, 1), 0.0, D_MIN).has_value());
CHECK_FALSE(SpindleUnrepairedFraction(sg, latt, Coord(0, 0, 1), WVL, 0.0).has_value());
}
}