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]") {