Files
Jungfraujoch/tests/DiffractionGeometryTest.cpp
T
leonarski_fandClaude Opus 5 a8d289e7cf Powder calibration: write Poni1/Poni2 in pyFAI's frame, not ours
The same frame mismatch as the rot2/rot3 fix, in the other two fields. Our pixel
coordinates are pixel-centred - 948.0 is the CENTRE of pixel 948 - while pyFAI
measures from the edge of the sensor and puts the centre of pixel i at (i + 0.5) *
pixel size. Poni1/Poni2 went out as beam * pixel size, so anything reading the
file placed the pattern half a pixel (37.5 um at 75 um pixels) off ours. The
previous commit's "Poni1/Poni2 need no such change" was right about the axis
directions and wrong about the origin.

The proof was already in the tree. The pyFAI reference values in
DiffractionGeometryTest were computed for a .poni with Poni2: 0.150 and a 75 um
pixel, which the tests translate to beam_x = 2000 - but pyFAI's numbers are
reproduced only at 1999.5. At 2000 every one of them is out by 2.6e-3 nm^-1, which
the 1e-2 tolerance hid. The tests now use the beam centre those headers actually
mean, and agree with pyFAI to 1e-6 - float precision - across untilted q, azimuth,
rot1, rot1+rot2, rot3, rot1+rot2+rot3 and the solid-angle correction. Tolerances
drop to 1e-4 (1e-5 for solid angle): ~100x the observed float noise, and 26x
tighter than the half pixel they were blind to.

The viewer's calibration window printed "PONI x = ... mm" from the un-offset value
beside the path of the file it disagreed with; it now matches the file.

Also moves the viewer's beam-centre cross half a pixel down and right, where the
spot, prediction, top-pixel and saturation markers already are. Our coordinates
are pixel-centred and the Qt scene's are pixel-cornered, so the map between them
is +0.5, and DrawBeamCenter was the one overlay missing it.

The convention itself is now written down in docs/DETECTOR_GEOMETRY.md, with the
conversions to XDS ORGX/ORGY and to the edge-of-sensor programs, this being the
second bug to come out of it.

Only exported and displayed values change; the fitted geometry, spot positions and
integration were always self-consistent. A .poni written by an earlier build is
half a pixel off.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 03:55:42 +02:00

550 lines
20 KiB
C++

// SPDX-FileCopyrightText: 2024 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <catch2/catch_all.hpp>
#include <iostream>
#include "../common/DiffractionGeometry.h"
#include "../common/DiffractionExperiment.h"
TEST_CASE("RecipToDetector_1", "[LinearAlgebra][Coord]") {
DiffractionExperiment x(DetJF(8, 2));
x.BeamX_pxl(1024).BeamY_pxl(1024).DetectorDistance_mm(120);
DiffractionGeometry geom = x.GetDiffractionGeometry();
float pos_x = 512, pos_y = 512;
auto recip = geom.DetectorToRecip(pos_x, pos_y);
auto [proj_x, proj_y] = geom.RecipToDetector(recip);
REQUIRE(proj_x == Catch::Approx(pos_x));
REQUIRE(proj_y == Catch::Approx(pos_y));
REQUIRE((recip - geom.DetectorToRecip(proj_x, proj_y)).Length() < 0.00000001f);
REQUIRE(std::fabs(geom.DistFromEwaldSphere(recip)) < 4e-4);
}
TEST_CASE("RecipToDetector_2", "[LinearAlgebra][Coord]") {
DiffractionExperiment x(DetJF(8, 2));
x.BeamX_pxl(1024).BeamY_pxl(1024).DetectorDistance_mm(120);
float pos_x = 1023, pos_y = 1023;
DiffractionGeometry geom = x.GetDiffractionGeometry();
auto recip = geom.DetectorToRecip(pos_x, pos_y);
auto [proj_x, proj_y] = geom.RecipToDetector(recip);
REQUIRE(proj_x == Catch::Approx(pos_x));
REQUIRE(proj_y == Catch::Approx(pos_y));
REQUIRE((recip - geom.DetectorToRecip(proj_x, proj_y)).Length() < 0.00000001f);
REQUIRE(std::fabs(geom.DistFromEwaldSphere(recip)) < 4e-4);
}
TEST_CASE("RecipToDetector_3", "[LinearAlgebra][Coord]") {
DiffractionExperiment x(DetJF(8, 2));
x.BeamX_pxl(1024).BeamY_pxl(1024).DetectorDistance_mm(120);
float pos_x = 30, pos_y = 30;
DiffractionGeometry geom = x.GetDiffractionGeometry();
auto recip = geom.DetectorToRecip(pos_x, pos_y);
auto [proj_x, proj_y] = geom.RecipToDetector(recip);
REQUIRE(proj_x == Catch::Approx(pos_x));
REQUIRE(proj_y == Catch::Approx(pos_y));
REQUIRE((recip - geom.DetectorToRecip(proj_x, proj_y)).Length() < 0.00000001f);
REQUIRE(std::fabs(geom.DistFromEwaldSphere(recip)) < 4e-4);
}
TEST_CASE("DiffractionGeometry_Phi","") {
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(75).IncidentEnergy_keV(WVL_1A_IN_KEV);
x.BeamX_pxl(1000).BeamY_pxl(1000);
DiffractionGeometry geom = x.GetDiffractionGeometry();
CHECK(geom.Phi_rad(2000, 1000) * (180.0 / M_PI) == Catch::Approx(0.0));
CHECK(geom.Phi_rad(2000, 0) * (180.0 / M_PI) == Catch::Approx(315.0f));
CHECK(geom.Phi_rad(1000, 0) * (180.0 / M_PI) == Catch::Approx(270.0f));
CHECK(geom.Phi_rad(0, 0) * (180.0 / M_PI) == Catch::Approx(225.0f));
CHECK(geom.Phi_rad(0, 1000) * (180.0 / M_PI) == Catch::Approx(180.0f));
CHECK(geom.Phi_rad(1000, 2000) * (180.0 / M_PI) == Catch::Approx(90.f));
CHECK(geom.Phi_rad(2000, 2000) * (180.0 / M_PI) == Catch::Approx(45.0f));
}
TEST_CASE("DiffractionGeometry_Cos2Theta","") {
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(75).IncidentEnergy_keV(WVL_1A_IN_KEV);
x.BeamX_pxl(1000).BeamY_pxl(1000);
DiffractionGeometry geom = x.GetDiffractionGeometry();
// det distance == 1000 pixel
// theta = 30 deg
// tan(2 * theta) = sqrt(3)
REQUIRE(cosf(geom.TwoTheta_rad(1000, 1000 * (1.0 + sqrt(3)))) == Catch::Approx(0.5f));
}
TEST_CASE("DiffractionGeometry_PxlToRes","") {
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(75).IncidentEnergy_keV(WVL_1A_IN_KEV);
DiffractionGeometry geom = x.GetDiffractionGeometry();
// sin(theta) = 1/2
// theta = 30 deg
// tan(2 * theta) = sqrt(3)
REQUIRE(geom.PxlToRes( 0, 1000 * sqrt(3)) == Catch::Approx(1.0));
// sin(theta) = 1/4
// theta = 14.47 deg
// tan(2 * theta) = 0.55328333517
REQUIRE(geom.PxlToRes(1000 * 0.55328333517 * cosf(1), 1000 * 0.55328333517 * sinf(1)) == Catch::Approx(2.0));
}
TEST_CASE("DiffractionGeometry_ResToPxl","") {
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(75).IncidentEnergy_keV(WVL_1A_IN_KEV);
DiffractionGeometry geom = x.GetDiffractionGeometry();
// sin(theta) = 1/2
// theta = 30 deg
// tan(2 * theta) = sqrt(3)
REQUIRE(geom.ResToPxl(1.0) == Catch::Approx(1000 * sqrt(3)));
// sin(theta) = 1/4
// theta = 14.47 deg
// tan(2 * theta) = 0.55328333517
REQUIRE(geom.ResToPxl(2.0) == Catch::Approx(1000 * 0.55328333517));
}
TEST_CASE("DiffractionGeometry_SolidAngleCorrection","") {
DiffractionExperiment x;
x.IncidentEnergy_keV(WVL_1A_IN_KEV);
x.BeamX_pxl(1000).BeamY_pxl(1000).DetectorDistance_mm(75);
DiffractionGeometry geom = x.GetDiffractionGeometry();
// At the beam centre the correction is 1
REQUIRE(geom.CalcAzIntSolidAngleCorr(1000, 1000) == 1.0f);
// 2 * theta = 60 deg -> cos(2 * theta) = 1/2 -> correction = (1/2)^3
REQUIRE(geom.CalcAzIntSolidAngleCorr(1000 * (1.0 + sqrt(3)), 1000) == Catch::Approx(0.5f * 0.5f * 0.5f));
REQUIRE(geom.CalcAzIntSolidAngleCorr(1000, 1000 * (1.0 + sqrt(3))) == Catch::Approx(0.5f * 0.5f * 0.5f));
}
TEST_CASE("DiffractionGeometry_SolidAngleCorrection_TiltInvariant","") {
// The solid-angle correction depends on the incidence angle to the detector
// normal, so for a given pixel it must be invariant under a rigid detector tilt
// (rot1/rot2/rot3) -- the same behaviour as PyFAI solidAngleArray.
DiffractionExperiment x;
x.IncidentEnergy_keV(WVL_1A_IN_KEV);
x.BeamX_pxl(1000).BeamY_pxl(1000).DetectorDistance_mm(75);
DiffractionGeometry flat = x.GetDiffractionGeometry();
x.PoniRot1_rad(0.2).PoniRot2_rad(-0.1).PoniRot3_rad(0.5);
DiffractionGeometry tilted = x.GetDiffractionGeometry();
CHECK(tilted.CalcAzIntSolidAngleCorr(100, 100) == Catch::Approx(flat.CalcAzIntSolidAngleCorr(100, 100)));
CHECK(tilted.CalcAzIntSolidAngleCorr(1500, 400) == Catch::Approx(flat.CalcAzIntSolidAngleCorr(1500, 400)));
CHECK(tilted.CalcAzIntSolidAngleCorr(800, 1900) == Catch::Approx(flat.CalcAzIntSolidAngleCorr(800, 1900)));
CHECK(tilted.CalcAzIntSolidAngleCorr(1000, 1000) == Catch::Approx(flat.CalcAzIntSolidAngleCorr(1000, 1000)));
}
TEST_CASE("DiffractionGeometry_PolarizationCorrection","") {
DiffractionExperiment x;
x.IncidentEnergy_keV(WVL_1A_IN_KEV);
x.BeamX_pxl(1000).BeamY_pxl(1000).DetectorDistance_mm(75);
DiffractionGeometry geom = x.GetDiffractionGeometry();
// Circular polarization 0.5*(1+cos(2theta)^2)
x.PolarizationFactor(0);
REQUIRE(geom.CalcAzIntPolarizationCorr(1000 * (1.0 + sqrt(3)), 1000, 0) == Catch::Approx(0.5f * (1 + 0.5f * 0.5f)));
REQUIRE(geom.CalcAzIntPolarizationCorr(1000, 1000 * (1.0 + sqrt(3)), 0) == Catch::Approx(0.5f * (1 + 0.5f * 0.5f)));
// Horizontal polarization
x.PolarizationFactor(1);
// No correction in vertical direction
REQUIRE(geom.CalcAzIntPolarizationCorr(1000, 1000 * (1.0 + sqrt(3)), 1) == Catch::Approx(1.0f));
REQUIRE(geom.CalcAzIntPolarizationCorr(1000, 1000 * (1.0 - sqrt(3)), 1) == Catch::Approx(1.0f));
// cos(2*theta)^2 in horizontal direction
REQUIRE(geom.CalcAzIntPolarizationCorr(1000 * (1.0 + sqrt(3)), 1000, 1) == Catch::Approx(0.5f * 0.5f));
REQUIRE(geom.CalcAzIntPolarizationCorr(1000 * (1.0 - sqrt(3)), 1000, 1) == Catch::Approx(0.5f * 0.5f));
}
TEST_CASE("DiffractionGeometry_AngleFromEwaldSphere") {
DiffractionGeometry geom;
geom.Wavelength_A(1.0);
// Center of Ewald sphere == (0,0,-1)
// Points on Ewald sphere
REQUIRE(geom.AngleFromEwaldSphere_deg(Coord(1, 0, -1)) == 0.0f);
REQUIRE(geom.AngleFromEwaldSphere_deg(Coord(1.0f / sqrtf(2.0f), 1.0f / sqrtf(2.0f), -1)) == 0.0f);
REQUIRE(geom.AngleFromEwaldSphere_deg(Coord(1, 0, 1)) == Catch::Approx(90.0f));
REQUIRE(geom.AngleFromEwaldSphere_deg(Coord(-sqrtf(2.0f), 0, 0)) == Catch::Approx(45.0f));
REQUIRE(geom.AngleFromEwaldSphere_deg(Coord(-sqrtf(3.0f), 0, 0)) == Catch::Approx(60.0f));
float cos_1deg = cosf(1.0f * M_PI / 180.0f);
float sin_1deg = sinf(1.0f * M_PI / 180.0f);
REQUIRE(fabsf(geom.AngleFromEwaldSphere_deg((Coord(cos_1deg - sin_1deg, 0, -(cos_1deg + sin_1deg)))) - 1.0f) < 0.0005);
// Cannot be rotated to fit into the Ewald sphere
REQUIRE(isnanf(geom.AngleFromEwaldSphere_deg(Coord(0, 0, 1))));
}
TEST_CASE("DiffractionGeometry_AngleFromEwaldSphere_Wvl2A") {
DiffractionGeometry geom;
geom.BeamX_pxl(1000).BeamY_pxl(1000).DetectorDistance_mm(100).Wavelength_A(2.0);
CHECK(geom.AngleFromEwaldSphere_deg(geom.DetectorToRecip(300,300)) < 0.05f);
CHECK(geom.AngleFromEwaldSphere_deg(geom.DetectorToRecip(200,1700)) < 0.05f);
CHECK(geom.AngleFromEwaldSphere_deg(geom.DetectorToRecip(1200,1800)) < 0.05f);
CHECK(geom.AngleFromEwaldSphere_deg(geom.DetectorToRecip(1500,100)) < 0.05f);
}
TEST_CASE("DiffractionGeometry_ProjectToEwaldSphere") {
DiffractionGeometry geom;
geom.BeamX_pxl(1000).BeamY_pxl(437).DetectorDistance_mm(100).Wavelength_A(2.0);
Coord p0 = geom.DetectorToRecip(300,300);
Coord p1 = geom.ProjectToEwaldSphere(p0);
REQUIRE(p0.x == Catch::Approx(p1.x));
REQUIRE(p0.y == Catch::Approx(p1.y));
REQUIRE(p0.z == Catch::Approx(p1.z));
Coord p2 = Coord(1,0,0);
REQUIRE(std::fabs(geom.DistFromEwaldSphere(p2) > 0.01));
REQUIRE(std::fabs(geom.DistFromEwaldSphere(geom.ProjectToEwaldSphere(p2))) < 0.0001);
}
TEST_CASE("DiffractionGeometry_DirectBeam") {
DiffractionGeometry geom;
geom.Wavelength_A(1.0);
geom.BeamX_pxl(1230).BeamY_pxl(1450);
auto [x, y] = geom.GetDirectBeam_pxl();
REQUIRE(x == Catch::Approx(1230.0f));
REQUIRE(y == Catch::Approx(1450.0f));
}
TEST_CASE("DiffractionGeometry_DirectBeam_RotZ") {
DiffractionGeometry geom;
geom.Wavelength_A(1.0);
geom.BeamX_pxl(1230).BeamY_pxl(1450);
geom.PoniRot3_rad(-M_PI_2);
auto [x, y] = geom.GetDirectBeam_pxl();
REQUIRE(x == Catch::Approx(1230.0f));
REQUIRE(y == Catch::Approx(1450.0f));
}
TEST_CASE("DiffractionGeometry_DirectBeam_RotY") {
DiffractionGeometry geom;
geom.Wavelength_A(1.0);
geom.DetectorDistance_mm(100);
geom.PixelSize_mm(1.0);
geom.BeamX_pxl(1230).BeamY_pxl(1450);
geom.PoniRot2_rad(-M_PI_4); // 45 deg rotation
auto [x, y] = geom.GetDirectBeam_pxl();
CHECK(x == Catch::Approx(1230.0f)); // no Change for X
CHECK(y >1450.0f);
}
TEST_CASE("DiffractionGeometry_PONI","") {
/*
poni_version: 2
Detector: Eiger4M
Detector_config: {}
Distance: 1.0
Poni1: 0.075
Poni2: 0.150
Rot1: 0.0
Rot2: 0.0
Rot3: 0.0
Wavelength: 1e-10
*/
// PyFAI uses nm^-1 for Q?
// The beam centre is Poni/pixel_size - 0.5 in every PONI test here: our coordinates are pixel-centred
// (0.0 is the centre of the first pixel) while pyFAI measures from the edge of the sensor and puts the
// centre of pixel i at (i + 0.5) * pixel size - see docs/DETECTOR_GEOMETRY.md. So 0.150 m / 75 um gives
// 1999.5, not 2000. With the half pixel the reference values below are reproduced to float precision;
// without it every one of them is out by 2.6e-3 nm^-1, which the old 1e-2 tolerance hid.
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(1000).BeamX_pxl(1999.5).BeamY_pxl(999.5).IncidentEnergy_keV(WVL_1A_IN_KEV);
DiffractionGeometry geom = x.GetDiffractionGeometry();
float diff_800_400 = fabs(geom.PxlToQ( 800,400)*10.0 - 6.295358803860941);
float diff_400_800 = fabs(geom.PxlToQ( 400,800)*10.0 - 7.554628215027982);
float diff_1300_2000 = fabs(geom.PxlToQ( 1300,2000)*10.0 - 5.73479724964891);
REQUIRE(diff_800_400 < 1e-4);
REQUIRE(diff_400_800 < 1e-4);
REQUIRE(diff_1300_2000 < 1e-4);
}
TEST_CASE("DiffractionGeometry_PONI_phi","") {
/*
poni_version: 2
Detector: Eiger4M
Detector_config: {}
Distance: 1.0
Poni1: 0.075
Poni2: 0.150
Rot1: 0.0
Rot2: 0.0
Rot3: 0.0
Wavelength: 1e-10
*/
// PyFAI uses nm^-1 for Q?
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(1000).BeamX_pxl(1999.5).BeamY_pxl(999.5).IncidentEnergy_keV(WVL_1A_IN_KEV);
DiffractionGeometry geom = x.GetDiffractionGeometry();
float phi_2000_0 = fabs(geom.Phi_rad(2000,0) - 2 * M_PI + 1.5702959937284997);
float phi_2000_2000 = fabs(geom.Phi_rad(2000,2000) - 1.5702964938446844);
float phi_0_1000 = fabs(geom.Phi_rad(0,1000) - 3.1413425992666903);
float phi_2000_1300 = fabs(geom.Phi_rad(1300,2000) - 2.1809518509415025);
CHECK(phi_2000_0 < 1e-4);
CHECK(phi_2000_2000 < 1e-4);
CHECK(phi_0_1000 < 1e-4);
CHECK(phi_2000_1300 < 1e-4);
}
TEST_CASE("DiffractionGeometry_PONI_phi_rot3","") {
/*
poni_version: 2
Detector: Eiger4M
Detector_config: {}
Distance: 1.0
Poni1: 0.075
Poni2: 0.150
Rot1: 0.0
Rot2: 0.0
Rot3: 0.5
Wavelength: 1e-10
*/
// PyFAI uses nm^-1 for Q?
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(1000).BeamX_pxl(1999.5).BeamY_pxl(999.5).IncidentEnergy_keV(WVL_1A_IN_KEV)
.PoniRot3_rad(0.5);
DiffractionGeometry geom = x.GetDiffractionGeometry();
REQUIRE(geom.GetPoniRot3_rad() == Catch::Approx(0.5f));
float phi_800_400 = fabs(geom.Phi_rad(800,400) - 3.105073518019684);
float phi_2000_1300 = fabs(geom.Phi_rad(1300,2000) - 1.6809518509415027);
CHECK(phi_800_400 < 1e-4);
CHECK(phi_2000_1300 < 1e-4);
}
TEST_CASE("DiffractionGeometry_PONI_phi_rot1_rot2_rot3","") {
/*
poni_version: 2
Detector: Eiger4M
Detector_config: {}
Distance: 1.0
Poni1: 0.075
Poni2: 0.150
Rot1: 0.2
Rot2: 0.1
Rot3: 0.5
Wavelength: 1e-10
*/
// PyFAI uses nm^-1 for Q?
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(1000).BeamX_pxl(1999.5).BeamY_pxl(999.5).IncidentEnergy_keV(WVL_1A_IN_KEV)
.PoniRot1_rad(0.2).PoniRot2_rad(-0.1).PoniRot3_rad(0.5);
DiffractionGeometry geom = x.GetDiffractionGeometry();
REQUIRE(geom.GetPoniRot1_rad() == Catch::Approx(0.2f));
REQUIRE(geom.GetPoniRot2_rad() == Catch::Approx(-0.1f));
REQUIRE(geom.GetPoniRot3_rad() == Catch::Approx(0.5f));
float phi_800_400 = fabs(geom.Phi_rad(800,400) - 2 * M_PI + 1.4175001633470816);
float phi_2000_1300 = fabs(geom.Phi_rad(1300,2000) - 2 * M_PI + 0.6630282166663707);
CHECK(phi_800_400 < 1e-4);
CHECK(phi_2000_1300 < 1e-4);
}
TEST_CASE("DiffractionGeometry_PONI_rot1","") {
/*
poni_version: 2
Detector: Eiger4M
Detector_config: {}
Distance: 1.0
Poni1: 0.075
Poni2: 0.150
Rot1: 0.2
Rot2: 0.0
Rot3: 0.0
Wavelength: 1e-10
*/
// PyFAI uses nm^-1 for Q?
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(1000).BeamX_pxl(1999.5).BeamY_pxl(999.5).IncidentEnergy_keV(WVL_1A_IN_KEV);
DiffractionGeometry geom = x.GetDiffractionGeometry();
geom.PoniRot1_rad(0.2);
float diff_800_400 = fabs(geom.PxlToQ( 800,400)*10.0 - 7.471276390173706);
float diff_400_800 = fabs(geom.PxlToQ( 400,800)*10.0 - 5.148411999405654);
float diff_1300_2000 = fabs(geom.PxlToQ( 1300,2000)*10.0 - 10.37635963741911);
CHECK(diff_800_400 < 1e-4);
CHECK(diff_400_800 < 1e-4);
CHECK(diff_1300_2000 < 1e-4);
}
TEST_CASE("DiffractionGeometry_PONI_rot1_rot2","") {
/*
poni_version: 2
Detector: Eiger4M
Detector_config: {}
Distance: 1.0
Poni1: 0.075
Poni2: 0.150
Rot1: 0.2
Rot2: 0.1
Rot3: 0.0
Wavelength: 1e-10
*/
// PyFAI uses nm^-1 for Q?
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(1000).BeamX_pxl(1999.5).BeamY_pxl(999.5).IncidentEnergy_keV(WVL_1A_IN_KEV);
DiffractionGeometry geom = x.GetDiffractionGeometry();
geom.PoniRot1_rad(0.2).PoniRot2_rad(-0.1);
float diff_800_400 = fabs(geom.PxlToQ( 800,400)*10.0 - 11.412737079654118);
float diff_400_800 = fabs(geom.PxlToQ( 400,800)*10.0 - 8.805012278158177);
float diff_1300_2000 = fabs(geom.PxlToQ( 1300,2000)*10.0 - 9.363455481328781);
CHECK(diff_800_400 < 1e-4);
CHECK(diff_400_800 < 1e-4);
CHECK(diff_1300_2000 < 1e-4);
}
TEST_CASE("DiffractionGeometry_PyFAI_Solid_angle","") {
/*
poni_version: 2
Detector: Eiger4M
Detector_config: {}
Distance: 0.2
Poni1: 0.075
Poni2: 0.150
Rot1: 0.0
Rot2: 0.0
Rot3: 0.0
Wavelength: 1e-10
*/
// PyFAI solidAngleArray is computed from the incidence angle to the detector normal,
// so it is independent of the poni rotation (tilt). CalcAzIntSolidAngleCorr matches this;
// the invariance is checked in DiffractionGeometry_SolidAngleCorrection_TiltInvariant.
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(200).BeamX_pxl(1999.5).BeamY_pxl(999.5).IncidentEnergy_keV(WVL_1A_IN_KEV);
DiffractionGeometry geom = x.GetDiffractionGeometry();
float diff_100_100 = fabs(geom.CalcAzIntSolidAngleCorr( 100,100) - 0.4844596502755233);
CHECK(diff_100_100 < 1e-5);
float diff_400_800 = fabs(geom.CalcAzIntSolidAngleCorr( 400,800)- 0.6267921080721112);
CHECK(diff_400_800 < 1e-5);
}
TEST_CASE("ResPhiToPxl") {
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(75).IncidentEnergy_keV(WVL_1A_IN_KEV);
DiffractionGeometry geom = x.GetDiffractionGeometry();
auto out = geom.ResPhiToPxl(1.0, 0);
CHECK(geom.PxlToRes(out.first, out.second) == Catch::Approx(1.0));
CHECK(fabs(geom.Phi_rad(out.first, out.second)) < 0.001 );
out = geom.ResPhiToPxl(1.0, M_PI);
CHECK(geom.PxlToRes(out.first, out.second) == Catch::Approx(1.0));
CHECK(fabs(geom.Phi_rad(out.first, out.second) - M_PI) < 0.001 );
out = geom.ResPhiToPxl(2.0, 0.7567);
CHECK(geom.PxlToRes(out.first, out.second) == Catch::Approx(2.0));
CHECK(fabs(geom.Phi_rad(out.first, out.second) - 0.7567) < 0.001 );
}
TEST_CASE("ResPhiToPxl_poni_rot") {
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(75).IncidentEnergy_keV(WVL_1A_IN_KEV);
DiffractionGeometry geom = x.GetDiffractionGeometry();
geom.PoniRot3_rad(0.5).PoniRot2_rad(-0.1).PoniRot2_rad(0.3);
auto out = geom.ResPhiToPxl(1.0, 0);
CHECK(geom.PxlToRes(out.first, out.second) == Catch::Approx(1.0));
CHECK(fabs(geom.Phi_rad(out.first, out.second)) < 0.001 );
out = geom.ResPhiToPxl(1.0, M_PI);
CHECK(geom.PxlToRes(out.first, out.second) == Catch::Approx(1.0));
CHECK(fabs(geom.Phi_rad(out.first, out.second) - M_PI) < 0.001 );
out = geom.ResPhiToPxl(2.0, 0.7567);
CHECK(geom.PxlToRes(out.first, out.second) == Catch::Approx(2.0));
CHECK(fabs(geom.Phi_rad(out.first, out.second) - 0.7567) < 0.001 );
}
TEST_CASE("DiffractionGeometry_DetectorToRecip_RecipToDetector_tilted") {
// Verify roundtrip consistency with non-zero rot1/rot2
DiffractionGeometry geom;
geom.BeamX_pxl(1000).BeamY_pxl(1000).DetectorDistance_mm(150)
.PixelSize_mm(0.075).Wavelength_A(1.0)
.PoniRot1_rad(0.05).PoniRot2_rad(-0.03);
// Test multiple points across the detector
std::vector<std::pair<float, float>> test_points = {
{500, 500}, {1500, 500}, {500, 1500}, {1500, 1500},
{800, 1200}, {1200, 800}, {300, 1700}, {1700, 300}
};
for (const auto& [x, y] : test_points) {
Coord recip = geom.DetectorToRecip(x, y);
auto [proj_x, proj_y] = geom.RecipToDetector(recip);
CHECK(proj_x == Catch::Approx(x).margin(0.001));
CHECK(proj_y == Catch::Approx(y).margin(0.001));
}
}
TEST_CASE("DiffractionGeometry_PONI_matrix_consistency") {
// Verify that the PONI rotation matrix gives consistent results
// when used for both forward and inverse transformations
DiffractionGeometry geom;
geom.BeamX_pxl(1000).BeamY_pxl(1000).DetectorDistance_mm(100)
.PixelSize_mm(0.075).Wavelength_A(1.0)
.PoniRot1_rad(0.04).PoniRot2_rad(-0.025);
const auto& poni_rot = geom.GetPoniRotMatrix();
const auto poni_rot_T = poni_rot.transpose();
// Test: poni_rot * poni_rot^T should be identity (orthogonal matrix)
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
Coord ei, ej;
ei[i] = 1.0f;
ej[j] = 1.0f;
float expected = (i == j) ? 1.0f : 0.0f;
CHECK((poni_rot * (poni_rot_T * ej))[i] == Catch::Approx(expected).margin(1e-6));
}
}
// Test: S0 vector transformation
Coord S0 = geom.GetScatteringVector();
// For beam along z, S0 = (0, 0, 1/λ)
CHECK(S0.x == Catch::Approx(0.0f));
CHECK(S0.y == Catch::Approx(0.0f));
CHECK(S0.z == Catch::Approx(1.0f));
}