tests: give both beam-centre estimators a case with the detector tilted

No test anywhere set a detector tilt, so PoniRot1/2 were zero in every one of them and the PONI and
the direct beam sat on top of each other. That matters because the conversion between the two is
used three times in the spot estimator - to centre the vote, to start each tooth's refinement, and
to turn the answer back out of the spindle frame - and with the two centres coincident it is the
identity, so its sign was unobservable. Verified by flipping it: with DirectBeamOffset negated, the
ten pre-existing beam-centre cases all still pass and only the new one fails.

The tilt used here puts the direct beam about 12 px from the PONI, twenty-four times the tolerance
asserted, and the case also pins that the tilt is not read as a spindle azimuth - what the fit sees
of the detector belongs to the detector.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3yNBXk4wKdMZy1ak2NY7f
This commit is contained in:
2026-08-30 08:37:55 +02:00
co-authored by Claude Opus 5
parent 8e9ca1f6d2
commit 3ddfbb2da9
2 changed files with 50 additions and 0 deletions
+29
View File
@@ -282,3 +282,32 @@ TEST_CASE("BeamCenterFromSpots_SurvivesASpindleOffPerpendicular", "[BeamCenter]"
CHECK(std::hypot(uncorrected->beam_x_pxl - truth.GetBeamX_pxl(),
uncorrected->beam_y_pxl - truth.GetBeamY_pxl()) > 3.0f * corrected_error);
}
// No detector is mounted exactly square to the beam, and a tilt separates the two centres this
// estimator works with: the PONI it reports and the DIRECT BEAM both mirror lines are taken about.
// The conversion between them is exact - it is where the vote is centred, where each tooth's
// refinement starts, and how the answer is turned back out of the spindle frame - so a sign error
// in it is a sign error of twice the offset in the answer. Here that offset is about 12 px, which
// is 24 times the tolerance below.
TEST_CASE("BeamCenterFromSpots_SurvivesADetectorTilt", "[BeamCenter]") {
DiffractionExperiment x = TestExperiment(Coord(1, 0, 0));
x.PoniRot1_rad(0.005f).PoniRot2_rad(-0.003f);
const DiffractionGeometry truth = OffsetBy(x.GetDiffractionGeometry(), 3.0f, -2.5f);
const Sweep sweep = MakeSweep(x, truth, PAIRS);
// The tilt has to be worth testing: with the two centres on top of each other the conversion
// is the identity and its sign is unobservable.
const auto [direct_x, direct_y] = truth.GetDirectBeam_pxl();
REQUIRE(std::hypot(direct_x - truth.GetBeamX_pxl(), direct_y - truth.GetBeamY_pxl()) > 5.0f);
SpindleEstimate fitted;
const auto estimate = FindBeamCenterFromSpotSymmetry(x, sweep.angle_deg, sweep.spots, &fitted);
REQUIRE(estimate.has_value());
CHECK(estimate->beam_x_pxl == Catch::Approx(truth.GetBeamX_pxl()).margin(0.5));
CHECK(estimate->beam_y_pxl == Catch::Approx(truth.GetBeamY_pxl()).margin(0.5));
CHECK(estimate->sigma_pxl < 1.0f);
// The spindle is perpendicular to the beam and on a lab axis here, so the tilt must not be
// read as an azimuth: what the fit sees of the detector belongs to the detector.
CHECK(fitted.azimuth_rad == Catch::Approx(0.0f).margin(5e-4));
}