diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 82f652a3..a69fc632 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,6 +10,7 @@ This is an UNSTABLE release. It includes many experimental features, as well as * A DECTRIS detector sending signed images is no longer declared unsigned in the image stream and in HDF5. * rugnux: `_process.h5` now describes the pixel format of the images it links to, instead of the container rugnux processes in. * HDF5: the detector tilt (`rot1`/`rot2`/`rot3`) is now exported correctly in the NXmx transformation chain; a tilted geometry was previously written so that other programs placed the detector wrongly (tens of mm at a few degrees of tilt). Untilted geometries are unaffected. +* rugnux: the `.poni` file written by `--mode calibration` no longer negates `Rot3`, which exported a detector rotation about the beam with the wrong sign. ### 1.0.0-rc.161 This is an UNSTABLE release. It includes many experimental features, as well as many AI generated fixes. We recommend using rc.152 for production use. diff --git a/rugnux/RugnuxCalibration.cpp b/rugnux/RugnuxCalibration.cpp index 3aa24e1f..78300b67 100644 --- a/rugnux/RugnuxCalibration.cpp +++ b/rugnux/RugnuxCalibration.cpp @@ -104,23 +104,22 @@ void WritePoniFile(const std::string &path, const DiffractionExperiment &experim f << fmt::format("Distance: {:.9g}\n", geom.GetDetectorDistance_mm() * 1e-3); f << fmt::format("Poni1: {:.9g}\n", geom.GetBeamY_pxl() * pixel_m + half_pixel_m); f << fmt::format("Poni2: {:.9g}\n", geom.GetBeamX_pxl() * pixel_m + half_pixel_m); - // rot2 and rot3 change SIGN on the way out, and rot1 does not. pyFAI has the slow axis increasing - // BOTTOM to TOP; we use the MX convention, top to bottom. The two frames therefore differ by a - // reflection in y, and conjugating a rotation by a reflection gives R(n, theta) -> R(Mn, -theta). - // For rot1 the axis IS y, so the axis reverses and the sense reverses and the two cancel; for rot2 - // (about x) and rot3 (about the beam) the axis lies in the mirror plane, so only the sense reverses. - // The angles mean the same thing in both frames - it is only the handedness of the frame that - // differs - and the same two flips would apply on the way IN if a PONI file were ever read. - // Poni1/Poni2 need no such change: they are distances from the corner of the sensor along each - // axis, which the direction the axis runs in does not affect. - // Verified against pyFAI on a LaB6 image: written without the flip, the rings pyFAI integrates are - // BROADER than with no tilt at all (peak 42 against 30, mean ring-position error 0.0045 1/A against - // 0.0027); with it they sharpen to 132 and 0.0005. + // Only rot2 changes sign on the way out; rot1 and rot3 do not. The mapping + // (Rot1, Rot2, Rot3) = (+rot1, -rot2, +rot3) + // is pinned empirically against pyFAI itself, not derived from its documentation, which + // contradicts itself on the direction of its axis 2: feeding these values to pyFAI reproduces the + // lab position DiffractionGeometry computes, over the whole detector and each angle separately, to + // 1.4e-17 m. Negating rot3 instead puts a pixel 25 mm out. + // Poni1/Poni2 need no change: they are distances from the corner of the sensor along each axis, + // which the direction the axis runs in does not affect. + // A LaB6 check confirmed the rot2 flip early on (rings sharpen from a peak of 42 to 132, mean + // ring-position error 0.0045 -> 0.0005 1/A) but could not have tested rot3, which is a rotation + // about the beam and so leaves q and 2theta untouched - it moves only the azimuth. f << fmt::format("Rot1: {:.9g}\n", geom.GetPoniRot1_rad()); // negate() rather than a bare minus so an unrefined angle prints as 0 and not -0. const auto negate = [](float v) { return v == 0.0f ? 0.0f : -v; }; f << fmt::format("Rot2: {:.9g}\n", negate(geom.GetPoniRot2_rad())); - f << fmt::format("Rot3: {:.9g}\n", negate(geom.GetPoniRot3_rad())); + f << fmt::format("Rot3: {:.9g}\n", geom.GetPoniRot3_rad()); f << fmt::format("Wavelength: {:.9g}\n", geom.GetWavelength_A() * 1e-10); f.flush(); if (!f) diff --git a/tests/CalibrationTest.cpp b/tests/CalibrationTest.cpp index 74231bc4..a12578c2 100644 --- a/tests/CalibrationTest.cpp +++ b/tests/CalibrationTest.cpp @@ -80,7 +80,7 @@ TEST_CASE("Calibration_PoniFileAxisConvention", "[DetGeomCalib]") { x.BeamX_pxl(1000.0f).BeamY_pxl(1275.0f).DetectorDistance_mm(150.0f); DiffractionGeometry geom = x.GetDiffractionGeometry(); - geom.PoniRot1_rad(0.01f).PoniRot2_rad(-0.02f); + geom.PoniRot1_rad(0.01f).PoniRot2_rad(-0.02f).PoniRot3_rad(0.03f); const std::string path = "poni_test.poni"; WritePoniFile(path, x, geom); @@ -105,15 +105,16 @@ TEST_CASE("Calibration_PoniFileAxisConvention", "[DetGeomCalib]") { CHECK(std::stod(keys["Poni1"]) == Catch::Approx(1275.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)); - // rot2 and rot3 are NEGATED into pyFAI's frame and rot1 is not: pyFAI's slow axis runs bottom to - // top where the MX convention runs top to bottom, so the frames differ by a reflection in y. That - // reverses the sense of a rotation about x or about the beam, while for a rotation about y itself - // the axis reverses too and the two cancel. Cross-checked against pyFAI on a real LaB6 image - the - // unflipped file integrates rings broader than a zero-tilt one. Do not "fix" these signs to match - // the stored values without repeating that check. + // 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. 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.0)); + 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()) + ", "