Chemical crystallography reaches high angle by swinging the detector out on a 2theta arm. Both readers had the number and neither used it: the miniCBF header's Detector_2theta was parsed into a struct member nothing ever read, and on the NXmx side the rotation was in the depends_on chain, which was not followed at all. A sweep taken at 30 degrees was therefore processed with its detector plane 30 degrees from where it stood, and nothing indexed. The geometry could already express it, and needed no change: the arm turns the detector about the sample, so the distance is still measured along the detector normal and the beam centre is still the point of normal incidence - which is exactly the PONI convention, and a swung detector is one PONI rotation. What moves is the direct beam, by distance*tan(2theta), off the beam centre and often off the detector. NXmx is the harder half, because the swing has no field of its own: it is one rotation in the chain the detector's position depends on, and "two_theta" is only one beamline's name for that dataset. So the chain is followed and its rotations composed, rather than a field of one name being looked for - each transformation states its vector in the frame of the one it depends on, which is why the product is the whole placement. Translations are skipped; they are the distance and the beam centre, which the file states separately in the square-on frame. Vectors come from McStas through the same 180-degree turn about z the module directions already use, a proper rotation, so an axis carried through it turns the same way. The three rotations a file this system writes ARE that chain, and are also read as the PONI angles - so those three paths are skipped, or every tilted file we have ever written would come back tilted twice. That is the one way this change could have broken existing data, and the test for it writes a tilted file and reads it back. For miniCBF the arm turns about the base spindle axis: on the four-circle geometry those headers describe the two are one axis, and the imgCIF axis table such a header carries states them with the same vector. Both now come from one constant, so a later correction to the frame moves them together. Measured. On a swung NXmx sweep the chain gives rot2 = -0.34907 rad for the 20 degrees it states, and the sweep goes from "nothing was integrated" to 25000 reflections at 82.2% completeness and CC(1/2) 0.9993, in the same space group and the same cell to 0.03 A as the square-on sweep of that crystal; the opposite sign indexes nothing. A miniCBF sweep at 30 degrees goes the same way, to 0.585 A, and a second sweep of that crystal at 55 degrees reaches 0.476 A and reproduces the cell again - with a low-resolution limit of 2.36 A rather than 13 A, which is what a detector swung that far records. On all of them post-refinement recovers the header's own beam centre and distance, and the beam stop shadow sits within four pixels of where the swung geometry puts the direct beam, 417 and 537 pixels from where the unswung one does. Seven sets whose detector is square to the beam, three of them carrying a chain whose 2theta is zero, are byte-identical in .hkl, .mtz, .cif and the image statistics. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3yNBXk4wKdMZy1ak2NY7f
879 lines
37 KiB
C++
879 lines
37 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"
|
|
#include "../common/JFJochMath.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.GetDetectorMatrix();
|
|
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));
|
|
}
|
|
// Cross-check of a TILTED detector against two independent implementations, pyFAI and DIALS/dxtbx.
|
|
//
|
|
// Every other geometry test here is either self-consistent (round trips) or exercises one angle at a
|
|
// time. This one pins all three PONI angles at once, non-zero and of mixed sign, against reference
|
|
// positions computed outside Jungfraujoch. That matters because the errors this guards against are
|
|
// second order: a wrong composition order or a swapped axis is invisible unless two angles are
|
|
// non-zero simultaneously, and a wrong pivot is invisible to anything that only checks directions.
|
|
//
|
|
// HOW TO REGENERATE THE NUMBERS
|
|
//
|
|
// pyFAI (`pip install pyFAI`), which is an independent implementation of the PONI convention:
|
|
//
|
|
// from pyFAI.geometry import Geometry
|
|
// from pyFAI.detectors import Detector
|
|
// px = 75e-6
|
|
// det = Detector(pixel1=px, pixel2=px, max_shape=(2164, 2030))
|
|
// g = Geometry(dist=0.150, poni1=1275*px + px/2, poni2=1000*px + px/2,
|
|
// rot1=0.05, rot2=+0.03, rot3=0.02, # (+rot1, -rot2, +rot3); see WritePoniFile
|
|
// detector=det, wavelength=1e-10)
|
|
// t3, t1, t2 = g.calc_pos_zyx(d1=[y], d2=[x]) # metres, pyFAI's own axes
|
|
// lab_mm = (t2*1e3, t1*1e3, t3*1e3) # pyFAI (t1,t2,t3) -> our (x,y,z)
|
|
//
|
|
// The half pixel in poni1/poni2 is the origin convention (docs/DETECTOR_GEOMETRY.md): our beam
|
|
// centre is pixel-centred, pyFAI measures from the sensor edge.
|
|
//
|
|
// DIALS: write a master, patch this geometry into it, and read the panel back.
|
|
//
|
|
// source /opt/dials-v3-27-0/dials_env.sh
|
|
// build/tools/jfjoch_hdf5_test <input.h5> -n1 -S -o g # writes g_master.h5
|
|
// # with h5py, set /entry/instrument/detector/{beam_center_x,beam_center_y,distance},
|
|
// # transformations/{rot1,rot2,rot3}, and recompute transformations/translation - both its
|
|
// # magnitude and its @vector - as {bx*px, by*px, distance} normalised, since the writer
|
|
// # derives it from the beam centre and distance.
|
|
// p = ExperimentListFactory.from_filenames(['g_master.h5'])[0].detector[0]
|
|
// lab = p.get_origin() + x*px_mm*p.get_fast_axis() + y*px_mm*p.get_slow_axis()
|
|
//
|
|
// Use get_origin()/get_fast_axis()/get_slow_axis() as above, NOT get_pixel_lab_coord(): that applies
|
|
// a parallax correction from the sensor thickness and material which DiffractionGeometry does not
|
|
// model, and it costs ~0.1 mm at the detector edge - enough to look like a geometry error.
|
|
//
|
|
// DIALS reports in the imgCIF frame, which is ours turned 180 degrees about x (diag(1,-1,-1)) - a
|
|
// proper rotation, not a mirror. The test applies that mapping, so it pins the frame relation too.
|
|
TEST_CASE("DiffractionGeometry_Tilted_vs_PyFAI_and_DIALS", "[DiffractionGeometry]") {
|
|
DiffractionGeometry geom;
|
|
geom.BeamX_pxl(1000.0f).BeamY_pxl(1275.0f).DetectorDistance_mm(150.0f)
|
|
.PixelSize_mm(0.075f).Wavelength_A(1.0f)
|
|
.PoniRot1_rad(0.05f).PoniRot2_rad(-0.03f).PoniRot3_rad(0.02f);
|
|
|
|
struct Reference {
|
|
int x, y;
|
|
float pyfai[3]; // our frame: x, y, z [mm]
|
|
float dials[3]; // imgCIF frame: x, y, z [mm]
|
|
};
|
|
|
|
const std::vector<Reference> reference = {
|
|
{ 0, 0, {-69.399541334f, -98.819975327f, 150.623559791f},
|
|
{-69.399544982f, 98.819979800f, -150.623559832f}},
|
|
{ 2029, 2163, { 85.802269836f, 60.488193357f, 147.887421982f},
|
|
{ 85.802273560f, -60.488196450f, -147.887421893f}},
|
|
{ 1000, 1275, { 7.405508016f, -4.642730847f, 149.745128473f},
|
|
{ 7.405508016f, 4.642730847f, -149.745128473f}},
|
|
{ 300, 1800, {-44.232874953f, 35.676608756f, 153.548927011f},
|
|
{-44.232877406f, -35.676610671f, -153.548927191f}},
|
|
{ 1700, 400, { 58.519162201f, -71.195011374f, 145.153948055f},
|
|
{ 58.519164629f, 71.195014535f, -145.153947836f}},
|
|
};
|
|
|
|
// 2 um, i.e. 1/37 of a pixel. The references agree with each other to ~4e-6 mm; the margin is
|
|
// set by float32 rounding in DiffractionGeometry and in the HDF5 file the DIALS values came from.
|
|
const double margin = 2e-3;
|
|
|
|
for (const auto &r: reference) {
|
|
const Coord lab = geom.LabCoord(static_cast<float>(r.x), static_cast<float>(r.y));
|
|
|
|
CHECK(lab.x == Catch::Approx(r.pyfai[0]).margin(margin));
|
|
CHECK(lab.y == Catch::Approx(r.pyfai[1]).margin(margin));
|
|
CHECK(lab.z == Catch::Approx(r.pyfai[2]).margin(margin));
|
|
|
|
CHECK(lab.x == Catch::Approx( r.dials[0]).margin(margin));
|
|
CHECK(lab.y == Catch::Approx(-r.dials[1]).margin(margin));
|
|
CHECK(lab.z == Catch::Approx(-r.dials[2]).margin(margin));
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// PONI angles <-> detector axis vectors, and the discrete image orientation
|
|
// ---------------------------------------------------------------------------------------------
|
|
|
|
namespace {
|
|
void CheckSameMatrix(const RotMatrix &a, const RotMatrix &b, float margin = 1e-6f) {
|
|
for (int i = 0; i < 3; i++) {
|
|
const Coord ca = a.Column(i), cb = b.Column(i);
|
|
CHECK(ca.x == Catch::Approx(cb.x).margin(margin));
|
|
CHECK(ca.y == Catch::Approx(cb.y).margin(margin));
|
|
CHECK(ca.z == Catch::Approx(cb.z).margin(margin));
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("PoniAngles_matrix_roundtrip") {
|
|
const float half_pi = static_cast<float>(PI) / 2.0f;
|
|
// rot1 and rot3 are recovered by atan2, so the branch cut at +-pi makes an angle comparison there
|
|
// meaningless (+pi and -pi are the same rotation). The matrix comparison below covers it; the
|
|
// angle comparison uses everything else, including the exact multiples of 90 degrees that are not
|
|
// on the cut.
|
|
const std::vector<float> angles = {0.0f, 0.01f, -0.03f, 0.7f, -1.2f, half_pi, -half_pi};
|
|
|
|
for (float rot1: angles) {
|
|
for (float rot3: angles) {
|
|
for (float rot2: {0.0f, 0.02f, -0.4f, 1.0f, -1.4f}) {
|
|
float r1, r2, r3;
|
|
PoniAnglesFromMatrix(PoniRotMatrix(rot1, rot2, rot3), r1, r2, r3);
|
|
CHECK(r1 == Catch::Approx(rot1).margin(1e-5));
|
|
CHECK(r2 == Catch::Approx(rot2).margin(1e-5));
|
|
CHECK(r3 == Catch::Approx(rot3).margin(1e-5));
|
|
CheckSameMatrix(PoniRotMatrix(r1, r2, r3), PoniRotMatrix(rot1, rot2, rot3));
|
|
}
|
|
}
|
|
}
|
|
|
|
// A half turn is on the atan2 branch cut, so only the matrix can be required to come back.
|
|
for (float rot1: {static_cast<float>(PI), -static_cast<float>(PI)}) {
|
|
float r1, r2, r3;
|
|
PoniAnglesFromMatrix(PoniRotMatrix(rot1, 0.1f, 0.2f), r1, r2, r3);
|
|
CheckSameMatrix(PoniRotMatrix(r1, r2, r3), PoniRotMatrix(rot1, 0.1f, 0.2f));
|
|
}
|
|
|
|
// Gimbal lock: at rot2 = +-90 degrees only rot1 +- rot3 is determined, and the convention is to
|
|
// put it all into rot1. A triple that already has rot3 = 0 therefore comes back unchanged, and
|
|
// the matrix comes back whatever rot3 was.
|
|
for (float rot2: {half_pi, -half_pi}) {
|
|
for (float rot1: {0.0f, 0.3f, -1.2f}) {
|
|
float r1, r2, r3;
|
|
PoniAnglesFromMatrix(PoniRotMatrix(rot1, rot2, 0.0f), r1, r2, r3);
|
|
CHECK(r1 == Catch::Approx(rot1).margin(1e-5));
|
|
CHECK(r2 == Catch::Approx(rot2).margin(1e-5));
|
|
CHECK(r3 == 0.0f);
|
|
|
|
PoniAnglesFromMatrix(PoniRotMatrix(rot1, rot2, 0.4f), r1, r2, r3);
|
|
CheckSameMatrix(PoniRotMatrix(r1, r2, r3), PoniRotMatrix(rot1, rot2, 0.4f));
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("DetectorAxes_roundtrip") {
|
|
for (int64_t quarter_turns = 0; quarter_turns < 4; quarter_turns++) {
|
|
for (bool mirror: {false, true}) {
|
|
for (float rot1: {0.0f, 0.05f, -0.9f}) {
|
|
for (float rot2: {0.0f, -0.03f, 1.1f}) {
|
|
for (float rot3: {0.0f, 0.2f, -1.5f}) {
|
|
DiffractionGeometry geom;
|
|
geom.Orientation(DetectorOrientation(mirror, quarter_turns))
|
|
.PoniRot1_rad(rot1).PoniRot2_rad(rot2).PoniRot3_rad(rot3);
|
|
|
|
const Coord fast = geom.GetFastAxis();
|
|
const Coord slow = geom.GetSlowAxis();
|
|
const RotMatrix before = geom.GetDetectorMatrix();
|
|
|
|
// Feeding the two axes straight back must not move anything.
|
|
DiffractionGeometry from_axes;
|
|
from_axes.Orientation(DetectorOrientation(mirror, quarter_turns))
|
|
.DetectorAxes(fast, slow);
|
|
CheckSameMatrix(from_axes.GetDetectorMatrix(), before, 1e-5f);
|
|
CHECK(from_axes.GetPoniRot1_rad() == Catch::Approx(rot1).margin(1e-5));
|
|
CHECK(from_axes.GetPoniRot2_rad() == Catch::Approx(rot2).margin(1e-5));
|
|
CHECK(from_axes.GetPoniRot3_rad() == Catch::Approx(rot3).margin(1e-5));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("DetectorOrientation_identity_is_todays_geometry") {
|
|
DiffractionGeometry with_default;
|
|
with_default.PoniRot1_rad(0.04f).PoniRot2_rad(-0.02f).PoniRot3_rad(0.11f);
|
|
|
|
DiffractionGeometry with_identity;
|
|
with_identity.Orientation(DetectorOrientation(false, 0))
|
|
.PoniRot1_rad(0.04f).PoniRot2_rad(-0.02f).PoniRot3_rad(0.11f);
|
|
|
|
// Bit for bit: the discrete part must cost existing data nothing.
|
|
CheckSameMatrix(with_identity.GetDetectorMatrix(), with_default.GetDetectorMatrix(), 0.0f);
|
|
CheckSameMatrix(with_default.GetDetectorMatrix(), PoniRotMatrix(0.04f, -0.02f, 0.11f), 0.0f);
|
|
CHECK(with_default.GetOrientation().IsIdentity());
|
|
}
|
|
|
|
TEST_CASE("DetectorOrientation_maps_the_detector_plane") {
|
|
// Untilted, so the lab coordinate of a pixel is the discrete orientation applied to its offset
|
|
// from the PONI, in mm.
|
|
auto make = [](bool mirror, int64_t quarter_turns) {
|
|
DiffractionGeometry g;
|
|
g.BeamX_pxl(100).BeamY_pxl(200).DetectorDistance_mm(100).PixelSize_mm(0.1f)
|
|
.Orientation(DetectorOrientation(mirror, quarter_turns));
|
|
return g;
|
|
};
|
|
|
|
// One pixel along the fast direction is 0.1 mm from the PONI.
|
|
const Coord fast_step = make(false, 0).LabCoord(101, 200) - make(false, 0).LabCoord(100, 200);
|
|
CHECK(fast_step.x == Catch::Approx(0.1).margin(1e-6));
|
|
CHECK(fast_step.y == Catch::Approx(0.0).margin(1e-6));
|
|
|
|
// A quarter turn about the beam takes the fast direction to lab +y ...
|
|
CHECK(make(false, 1).GetFastAxis().y == Catch::Approx(1.0).margin(1e-6));
|
|
// ... and the slow direction to lab -x.
|
|
CHECK(make(false, 1).GetSlowAxis().x == Catch::Approx(-1.0).margin(1e-6));
|
|
// A mirror in Y leaves the fast direction alone and reverses the slow one.
|
|
CHECK(make(true, 0).GetFastAxis().x == Catch::Approx(1.0).margin(1e-6));
|
|
CHECK(make(true, 0).GetSlowAxis().y == Catch::Approx(-1.0).margin(1e-6));
|
|
// Two quarter turns is a half turn.
|
|
CHECK(make(false, 2).GetFastAxis().x == Catch::Approx(-1.0).margin(1e-6));
|
|
CHECK(make(false, 2).GetSlowAxis().y == Catch::Approx(-1.0).margin(1e-6));
|
|
|
|
// Every orientation is orthogonal, and improper exactly when it mirrors.
|
|
for (int64_t k = 0; k < 4; k++)
|
|
for (bool mirror: {false, true}) {
|
|
const DetectorOrientation o(mirror, k);
|
|
CheckSameMatrix(o.Matrix() * o.Matrix().transpose(), RotMatrix(), 1e-6f);
|
|
const Coord expected_normal = mirror ? -(o.Matrix().Column(0) % o.Matrix().Column(1))
|
|
: (o.Matrix().Column(0) % o.Matrix().Column(1));
|
|
CheckSameMatrix(RotMatrix(o.Matrix().Column(0), o.Matrix().Column(1), expected_normal),
|
|
o.Matrix(), 1e-6f);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("DetectorOrientation_preserves_radius_and_solid_angle") {
|
|
// Both generators are signed permutations of (u, v), so the distance from the PONI - and with it
|
|
// the solid-angle correction, the resolution of a ring and every radius-only consumer - cannot
|
|
// move. This is why most of the pipeline needs no change.
|
|
const float ref = [] {
|
|
DiffractionGeometry g;
|
|
g.BeamX_pxl(500).BeamY_pxl(700).DetectorDistance_mm(120).PixelSize_mm(0.075f);
|
|
return g.CalcAzIntSolidAngleCorr(823, 311);
|
|
}();
|
|
|
|
for (int64_t k = 0; k < 4; k++)
|
|
for (bool mirror: {false, true}) {
|
|
DiffractionGeometry g;
|
|
g.BeamX_pxl(500).BeamY_pxl(700).DetectorDistance_mm(120).PixelSize_mm(0.075f)
|
|
.PoniRot1_rad(0.03f).PoniRot2_rad(-0.02f)
|
|
.Orientation(DetectorOrientation(mirror, k));
|
|
CHECK(g.CalcAzIntSolidAngleCorr(823, 311) == Catch::Approx(ref).margin(1e-7));
|
|
}
|
|
}
|
|
|
|
TEST_CASE("DetectorOrientation_and_polarization") {
|
|
// Polarization depends on the azimuth in the LABORATORY, so what the discrete orientation changes
|
|
// is which pixel lands where. A quarter turn moves a pixel from the polarization plane to across
|
|
// it; a mirror in Y sends phi to -phi and so cannot move it at all.
|
|
auto corr = [](bool mirror, int64_t quarter_turns, float x, float y) {
|
|
DiffractionGeometry g;
|
|
g.BeamX_pxl(500).BeamY_pxl(500).DetectorDistance_mm(100).PixelSize_mm(0.075f)
|
|
.Orientation(DetectorOrientation(mirror, quarter_turns));
|
|
return g.CalcAzIntPolarizationCorr(x, y, 0.99f);
|
|
};
|
|
|
|
const float along_x = corr(false, 0, 700, 500);
|
|
const float along_y = corr(false, 0, 500, 700);
|
|
CHECK(along_x != Catch::Approx(along_y));
|
|
|
|
CHECK(corr(false, 1, 700, 500) == Catch::Approx(along_y));
|
|
CHECK(corr(true, 0, 700, 500) == Catch::Approx(along_x));
|
|
CHECK(corr(true, 0, 500, 700) == Catch::Approx(along_y));
|
|
}
|
|
|
|
TEST_CASE("DetectorOrientation_recip_roundtrip") {
|
|
for (int64_t k = 0; k < 4; k++)
|
|
for (bool mirror: {false, true}) {
|
|
DiffractionGeometry geom;
|
|
geom.BeamX_pxl(1000).BeamY_pxl(1000).DetectorDistance_mm(150)
|
|
.PixelSize_mm(0.075f).Wavelength_A(1.0f)
|
|
.PoniRot1_rad(0.05f).PoniRot2_rad(-0.03f).PoniRot3_rad(0.2f)
|
|
.Orientation(DetectorOrientation(mirror, k));
|
|
|
|
for (const auto &[x, y]: std::vector<std::pair<float, float>>{
|
|
{500, 500}, {1500, 500}, {500, 1500}, {1200, 800}}) {
|
|
const auto [px, py] = geom.RecipToDetector(geom.DetectorToRecip(x, y));
|
|
CHECK(px == Catch::Approx(x).margin(0.001));
|
|
CHECK(py == Catch::Approx(y).margin(0.001));
|
|
}
|
|
}
|
|
}
|
|
|
|
// A detector swung out on a 2theta arm, which is how chemical crystallography reaches high angle.
|
|
// The arm turns the detector about the sample, so the geometry that describes it is the PONI rotation
|
|
// and nothing else moves: the distance stays the distance along the detector normal and the beam
|
|
// centre stays the point of normal incidence. What DOES move is the direct beam, which is no longer
|
|
// at the beam centre - the two coincide only on a detector square to the beam.
|
|
TEST_CASE("DiffractionGeometry_TwoThetaArm", "[LinearAlgebra][Coord]") {
|
|
const float two_theta = 30.0f * PI / 180.0f;
|
|
const float distance_mm = 160.0f, pixel_mm = 0.172f, wavelength = 0.6889f;
|
|
const float bx = 740.0f, by = 866.0f;
|
|
|
|
DiffractionGeometry geom;
|
|
geom.BeamX_pxl(bx).BeamY_pxl(by).DetectorDistance_mm(distance_mm)
|
|
.PixelSize_mm(pixel_mm).Wavelength_A(wavelength);
|
|
// The arm turns about the internal x axis; a rotation of +2theta about it is rot2 = -2theta.
|
|
geom.PoniRot2_rad(-two_theta);
|
|
|
|
// The beam centre pixel is the PONI: still on the detector normal through the sample, and now
|
|
// 2theta away from the beam.
|
|
CHECK(geom.TwoTheta_rad(bx, by) == Catch::Approx(two_theta));
|
|
CHECK(geom.LabCoord(bx, by).Length() == Catch::Approx(distance_mm));
|
|
CHECK(geom.GetNormalAxis() * Coord(0, 0, 1) == Catch::Approx(cosf(two_theta)));
|
|
// The plane turned about x, so the fast axis - along +x - did not move, and the slow one tipped
|
|
// out of the detector plane by the full 2theta.
|
|
CHECK((geom.GetFastAxis() - Coord(1, 0, 0)).Length() < 1e-6f);
|
|
CHECK(geom.GetSlowAxis() * Coord(0, 0, 1) == Catch::Approx(sinf(two_theta)));
|
|
|
|
// The direct beam is off the PONI by D*tan(2theta), along the direction the arm swung.
|
|
auto [direct_x, direct_y] = geom.GetDirectBeam_pxl();
|
|
CHECK(direct_x == Catch::Approx(bx));
|
|
CHECK(direct_y == Catch::Approx(by + distance_mm * tanf(two_theta) / pixel_mm));
|
|
|
|
// Resolution at the PONI is the Bragg spacing of 2theta, not of a pixel at zero distance from
|
|
// the beam centre - the reason a swung detector reaches so much further than a square-on one.
|
|
CHECK(geom.PxlToRes(bx, by) == Catch::Approx(wavelength / (2.0f * sinf(two_theta / 2.0f))));
|
|
|
|
// Round trip through reciprocal space, at the PONI and away from it in both directions.
|
|
const std::vector<std::pair<float, float>> probes =
|
|
{{bx, by}, {bx + 300.0f, by - 500.0f}, {bx - 700.0f, by + 200.0f}};
|
|
for (const auto &[x, y]: probes) {
|
|
auto [back_x, back_y] = geom.RecipToDetector(geom.DetectorToRecip(x, y));
|
|
CHECK(back_x == Catch::Approx(x));
|
|
CHECK(back_y == Catch::Approx(y));
|
|
}
|
|
}
|