From 3ddfbb2da904ff972f5ee2ef34959878fb19f355 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sun, 30 Aug 2026 00:24:33 +0200 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01T3yNBXk4wKdMZy1ak2NY7f --- tests/BeamCenterFromBackgroundTest.cpp | 21 +++++++++++++++++++ tests/BeamCenterFromSpotsTest.cpp | 29 ++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/tests/BeamCenterFromBackgroundTest.cpp b/tests/BeamCenterFromBackgroundTest.cpp index dc0007b90..af82090c7 100644 --- a/tests/BeamCenterFromBackgroundTest.cpp +++ b/tests/BeamCenterFromBackgroundTest.cpp @@ -124,3 +124,24 @@ TEST_CASE("BeamCenterFromBackground_AnAzimuthalShadowIsNotACentreError", "[BeamC CHECK(estimate->beam_x_pxl == Catch::Approx(geom_true.GetBeamX_pxl()).margin(1.0)); CHECK(estimate->beam_y_pxl == Catch::Approx(geom_true.GetBeamY_pxl()).margin(1.0)); } + +// The same, with the detector tilted. Every pixel's 2-theta and azimuth, and the derivative of +// 2-theta with respect to the centre that the fit is built on, go through the detector rotation +// matrix, so the tilt is not a detail of the geometry here - it is in the Jacobian. +TEST_CASE("BeamCenterFromBackground_SurvivesADetectorTilt", "[BeamCenter]") { + DiffractionExperiment x = TestExperiment(); + x.PoniRot1_rad(0.005f).PoniRot2_rad(-0.003f); + PixelMask pixel_mask(x); + + const DiffractionGeometry geom_true = OffsetBy(x.GetDiffractionGeometry(), 3.0f, -2.5f); + const auto [direct_x, direct_y] = geom_true.GetDirectBeam_pxl(); + REQUIRE(std::hypot(direct_x - geom_true.GetBeamX_pxl(), direct_y - geom_true.GetBeamY_pxl()) > 5.0f); + + const auto projection = SynthesiseProjection(x, pixel_mask, geom_true, 60.0f, 1.0f); + const auto estimate = FindBeamCenterFromBackground(x, pixel_mask, projection); + + REQUIRE(estimate.has_value()); + CHECK(estimate->beam_x_pxl == Catch::Approx(geom_true.GetBeamX_pxl()).margin(0.5)); + CHECK(estimate->beam_y_pxl == Catch::Approx(geom_true.GetBeamY_pxl()).margin(0.5)); + CHECK(estimate->sigma_pxl < 1.0f); +} diff --git a/tests/BeamCenterFromSpotsTest.cpp b/tests/BeamCenterFromSpotsTest.cpp index e5ee73d99..fa4102621 100644 --- a/tests/BeamCenterFromSpotsTest.cpp +++ b/tests/BeamCenterFromSpotsTest.cpp @@ -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)); +}