Record whether the image is mirrored in Y, and label the .poni orientation
Which way the detector's rows run was decided once, in the module assembly, and never stated again: not on the wire, not in the file, nowhere a consumer could read it. mirror_y was consumed inside the DetectorGeometryModular constructor and discarded. It is now a declared property of the detector setup, carried into the start message, written to HDF5 under detectorSpecific, and read back. Absence means true, which is the MX convention and the only thing Jungfraujoch has ever produced. Deliberately a boolean and not a corner enum: the assembled image can only be flipped in Y, so a four-corner value would encode states that cannot occur. DECTRIS stream2 has no field for this - checked against the specification - so the key is new rather than an extension of theirs, and a consumer that does not know it skips it and behaves exactly as before. The .poni file gains pyFAI's orientation. Without it pyFAI applies its own default, 3 (bottom left), and believes increasing row means physically upwards. The numbers still agreed - a mirror preserves 2theta, so radial integration was never affected - but the azimuth came out with the opposite sense, which matters for cake and sector integration. Declaring orientation 2 is not a one-line addition: it re-anchors Poni1 to the top edge and reverses rot2 and rot3, a row flip being improper. Measured against pyFAI 2026.5.0 by searching all four orientations, both Poni1 anchorings and all eight sign combinations: exactly two combinations reproduce the lab position DiffractionGeometry computes to 1.4e-17 m - the unlabelled form written before, and (orientation 2, Poni1 = height-1-beam_y, +rot1/+rot2/-rot3), which is now written. Calibration_PoniFileAxisConvention pins it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+16
-11
@@ -98,23 +98,28 @@ TEST_CASE("Calibration_PoniFileAxisConvention", "[DetGeomCalib]") {
|
||||
std::remove(path.c_str());
|
||||
|
||||
const double pixel_m = geom.GetPixelSize_mm() * 1e-3;
|
||||
CHECK(keys["poni_version"] == "2");
|
||||
CHECK(keys["poni_version"] == "2.1");
|
||||
// orientation 2 = "top left seen from the sample", the MX convention we assemble to. Without it
|
||||
// pyFAI applies its own default (3, bottom left) and gets the azimuth sense backwards.
|
||||
CHECK(keys["Detector_config"].find("\"orientation\": 2") != std::string::npos);
|
||||
// The half pixel is the origin convention (docs/DETECTOR_GEOMETRY.md): our beam centre is
|
||||
// pixel-centred, pyFAI measures from the edge of the sensor and puts the centre of pixel i at
|
||||
// (i + 0.5) * pixel size.
|
||||
CHECK(std::stod(keys["Poni1"]) == Catch::Approx(1275.5 * pixel_m)); // slow axis = y
|
||||
// Declaring orientation 2 anchors Poni1 at the top edge, so the same physical point is
|
||||
// (height - 1 - beam_y) rows down from it.
|
||||
CHECK(std::stod(keys["Poni1"])
|
||||
== Catch::Approx((x.GetYPixelsNumConv() - 1 - 1275 + 0.5) * pixel_m)); // slow axis = y
|
||||
CHECK(std::stod(keys["Poni2"]) == Catch::Approx(1000.5 * pixel_m)); // fast axis = x
|
||||
CHECK(std::stod(keys["Distance"]) == Catch::Approx(0.150));
|
||||
// Only rot2 is NEGATED into pyFAI's frame; rot1 and rot3 are not. Pinned by feeding these exact
|
||||
// values to pyFAI and checking it reproduces the lab position DiffractionGeometry computes: the
|
||||
// mapping below agrees to 1.4e-17 m over the whole detector, with each angle tested separately,
|
||||
// while negating rot3 puts a pixel 25 mm out. Do not "fix" these signs without repeating that
|
||||
// check against pyFAI itself - its own documentation contradicts itself on the direction of its
|
||||
// axis 2, and a powder-ring check cannot test rot3, which is a rotation about the beam and moves
|
||||
// only the azimuth.
|
||||
// With orientation declared, (Rot1, Rot2, Rot3) = (+rot1, +rot2, -rot3): a row flip is improper,
|
||||
// so it reverses rotations about x and about the beam and leaves the one about the vertical.
|
||||
// Pinned against pyFAI 2026.5.0 - an exhaustive search over all four orientations, both Poni1
|
||||
// anchorings and all eight sign combinations found exactly two exact solutions, this one and the
|
||||
// unlabelled orientation-3 form written before. Do not "fix" these signs without repeating that
|
||||
// search: a powder-ring check cannot test rot3, which moves only the azimuth.
|
||||
CHECK(std::stod(keys["Rot1"]) == Catch::Approx(0.01));
|
||||
CHECK(std::stod(keys["Rot2"]) == Catch::Approx(0.02));
|
||||
CHECK(std::stod(keys["Rot3"]) == Catch::Approx(0.03));
|
||||
CHECK(std::stod(keys["Rot2"]) == Catch::Approx(-0.02));
|
||||
CHECK(std::stod(keys["Rot3"]) == Catch::Approx(-0.03));
|
||||
CHECK(std::stod(keys["Wavelength"]) == Catch::Approx(geom.GetWavelength_A() * 1e-10));
|
||||
// max_shape is [rows, cols] - the same slow-then-fast order as Poni1/Poni2.
|
||||
const std::string shape = "[" + std::to_string(x.GetYPixelsNumConv()) + ", "
|
||||
|
||||
Reference in New Issue
Block a user