From e2de790867910a0d288dc9b58d2c41f372565935 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Thu, 6 Aug 2026 21:32:30 +0200 Subject: [PATCH] Powder calibration: cover the tilt round trip, and correct how a tilt shows itself A detector tilt does NOT appear as a cos(2 phi) modulation of the ring radius, as the previous comment claimed. To first order a misalignment beta gives r(phi) = R + (R^2 / F) (beta_x cos phi + beta_y sin phi) which is a cos(phi) term - the same harmonic a wrong beam centre produces. What separates them is the radius dependence: the centre's amplitude is the same on every ring, the tilt's grows as R^2. So they are told apart across rings, not within one, and on a single ring they are exactly degenerate. Measured on a powder standard the true cos(2 phi) term is of order R^3 beta^2 / F^2 - hundredths of a pixel, at the noise floor - so it carries nothing usable. Also add the tilted round trip, which was missing. It doubles as a check that RingOptimizer's open-coded rotation agrees with DiffractionGeometry's: the fitter applies Rx(-rot2) Ry(+rot1) by hand rather than going through the geometry's Rz(-rot3) Rx(-rot2) Ry(+rot1), and those had never been held against each other. They agree - 0.020 / -0.015 rad recovered as 0.0197 / -0.0148. Dropping rot3 is right rather than an omission, since rings cannot constrain in-plane roll. The tilted case yields fewer ring points than the centred one, which is expected and worth knowing: the extractor searches a window centred on where each ring is EXPECTED, so a large enough geometry error carries part of a ring out of it. Co-Authored-By: Claude Opus 5 (1M context) --- .../geom_refinement/RingsFromProfile.h | 14 ++++++--- tests/RingsFromProfileTest.cpp | 31 +++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/image_analysis/geom_refinement/RingsFromProfile.h b/image_analysis/geom_refinement/RingsFromProfile.h index 9be6b30a..5fc4e863 100644 --- a/image_analysis/geom_refinement/RingsFromProfile.h +++ b/image_analysis/geom_refinement/RingsFromProfile.h @@ -19,10 +19,16 @@ // over a run measures the same ring directly, at every azimuth, with the whole run's counts behind it. // // Where the ring falls is what carries the geometry. A powder ring is a conic centred on the beam, so -// if the beam centre is wrong its apparent radius oscillates once per turn (a cos(phi) term), and if -// the detector is tilted, twice (cos(2 phi)). Neither depends on the calibrant's d-spacings, which is -// why the beam centre is the one thing a powder pattern determines without assuming anything about the -// standard - the distance, by contrast, is only as good as the lattice constant it is measured against. +// a wrong beam centre makes its apparent radius oscillate once per turn - a cos(phi) term, the SAME +// amplitude on every ring. A detector tilt beta produces a cos(phi) term as well, not the cos(2 phi) +// one might expect: to first order r(phi) = R + (R^2/F)(beta_x cos phi + beta_y sin phi), so it grows +// as the ring's radius SQUARED. Measured on a powder standard, the genuine cos(2 phi) term is of order +// R^3 beta^2 / F^2, i.e. hundredths of a pixel and below the noise. So the two are told apart by how +// the cos(phi) amplitude scales with radius, which needs at least two rings - on a single ring they are +// exactly degenerate. Neither depends on the calibrant's d-spacings, which is why the beam centre is +// the one thing a powder pattern determines without assuming anything about the standard; the distance, +// by contrast, is only as good as the lattice constant it is measured against, and its lever collapses +// as the detector moves back and the rings crowd into small 2theta. // // profile is the mean intensity per bin (AzimuthalIntegrationProfile::GetResult()): q_bins x azimuthal // bins, indexed bin = q_bin + phi_bin * q_bins. geom supplies the CURRENT geometry, used only to turn a diff --git a/tests/RingsFromProfileTest.cpp b/tests/RingsFromProfileTest.cpp index 433961c8..8bb02d59 100644 --- a/tests/RingsFromProfileTest.cpp +++ b/tests/RingsFromProfileTest.cpp @@ -90,6 +90,37 @@ TEST_CASE("RingsFromProfile_RecoversBeamCenter", "[DetGeomCalib]") { < std::abs(geom_assumed.GetBeamX_pxl() - geom_true.GetBeamX_pxl())); } +// The same round trip with the detector tilted. A tilt and a centre error BOTH show up as cos(phi); +// what separates them is that the tilt's amplitude grows as the ring radius squared, so it takes +// several rings to tell them apart. This mainly guards the conventions: RingOptimizer open-codes its +// rotation instead of going through DiffractionGeometry, and this holds the two against each other. +// Fewer ring points than the centred case is expected - a tilt this size carries part of some rings +// out of the extractor's search window, which is centred on where the ring is EXPECTED to be. +TEST_CASE("RingsFromProfile_RecoversTilt", "[DetGeomCalib]") { + DiffractionExperiment x(DetJF4M()); + x.QSpacingForAzimInt_recipA(0.004).QRangeForAzimInt_recipA(0.5, 4.0); + auto azint = x.GetAzimuthalIntegrationSettings(); + azint.AzimuthalBinCount(64); + x.ImportAzimuthalIntegrationSettings(azint); + + PixelMask pixel_mask(x); + AzimuthalIntegrationMapping mapping(x, pixel_mask); + + const DiffractionGeometry geom_assumed = x.GetDiffractionGeometry(); + DiffractionGeometry geom_true = geom_assumed; + geom_true.PoniRot1_rad(0.02f).PoniRot2_rad(-0.015f); + + const auto profile = SynthesiseProfile(mapping, geom_assumed, geom_true); + const auto rings = RingsFromAzimuthalProfile(profile, mapping, geom_assumed, LAB6); + REQUIRE(rings.size() > 60); + + RingOptimizer optimizer(geom_assumed); + const auto fitted = optimizer.Run(rings); + + CHECK(fitted.GetPoniRot1_rad() == Catch::Approx(0.02).margin(0.004)); + CHECK(fitted.GetPoniRot2_rad() == Catch::Approx(-0.015).margin(0.004)); +} + // One azimuthal bin is a plain radial profile: the ring has been averaged over every direction, so // nothing is left to say where its centre is. Refuse rather than return points that cannot constrain it. TEST_CASE("RingsFromProfile_NeedsAzimuthalBins", "[DetGeomCalib]") {