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)); +}