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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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]") {
|
||||
|
||||
Reference in New Issue
Block a user