v1.0.0-rc.166 (#76)
Build Packages / Unit tests (push) Successful in 1h22m15s
Build Packages / build:windows:nocuda (push) Successful in 18m0s
Build Packages / build:windows:cuda (push) Successful in 20m30s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m32s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m39s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 8m55s
Build Packages / build:rugnux:windows (push) Successful in 11m25s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 20m6s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m27s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 20m19s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 15m34s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 20m25s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m36s
Build Packages / build:rpm (rocky8) (push) Successful in 17m43s
Build Packages / build:rpm (rocky9) (push) Successful in 13m34s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 21m28s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 18m19s
Build Packages / DIALS test (push) Successful in 12m36s
Build Packages / XDS test (durin plugin) (push) Successful in 6m56s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 6m48s
Build Packages / XDS test (neggia plugin) (push) Successful in 6m7s
Build Packages / Generate python client (push) Successful in 11s
Build Packages / Build documentation (push) Successful in 36s
Build Packages / Create release (push) Skipped
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 5m11s
Build Packages / Unit tests (push) Successful in 1h22m15s
Build Packages / build:windows:nocuda (push) Successful in 18m0s
Build Packages / build:windows:cuda (push) Successful in 20m30s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m32s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m39s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 8m55s
Build Packages / build:rugnux:windows (push) Successful in 11m25s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 20m6s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m27s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 20m19s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 15m34s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 20m25s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m36s
Build Packages / build:rpm (rocky8) (push) Successful in 17m43s
Build Packages / build:rpm (rocky9) (push) Successful in 13m34s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 21m28s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 18m19s
Build Packages / DIALS test (push) Successful in 12m36s
Build Packages / XDS test (durin plugin) (push) Successful in 6m56s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 6m48s
Build Packages / XDS test (neggia plugin) (push) Successful in 6m7s
Build Packages / Generate python client (push) Successful in 11s
Build Packages / Build documentation (push) Successful in 36s
Build Packages / Create release (push) Skipped
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 5m11s
* `rugnux --mode calibration` writes `<prefix>.json` beside the `.poni`, whose `dataset_settings` member is a `jfjoch_broker` `dataset_settings` body as it stands. * `rugnux` and `jfjoch_viewer` read PILATUS miniCBF sweeps natively, without conversion. * Masters written by other facilities open, including Eiger 1.x and third-party NXmx variants. * `rugnux` measures the beam centre on every run, and indexes with it when the file's value indexes nothing. * A detector swung out on a 2theta arm is placed where the file says it stands, and the calibration can hold the tilt fixed. * `rugnux` writes the unmerged MTZ by default, and a P1 merge beside it, so a wrong space group can be re-merged without reprocessing. * Significant improvements to symmetry handling in `rugnux`: the lattice, the point group, the setting and the systematic absences. * The `rugnux` report gives the resolution the CC1/2 fit reached, beside the range the reflections were written to. * The `rugnux` report gives the twinning statistics measured before the space group was decided, beside the ones measured after. * The `rugnux` report gives the strong-direction diffraction limit, and warns when CC1/2 is not monotone with resolution. * `rugnux` ranks screw axes on the evidence their absences carry, rather than on how many control reflections a candidate happens to have. * Twinning is no longer reported when the L-test contradicts it. * The `rugnux` report gives the detector tilt, the measured tilt and the direct beam beside the beam centre, and a post-refined beam centre is judged against the run's own measurement rather than the file's. * `--no-refine-tilt` holds the detector tilt at the value in the file, instead of zeroing it, when the calibration starts from the spots. * The `jfjoch_viewer` grid scan view draws the cells in the proportion of the scan steps, so the map has the shape of the scanned area. Reviewed-on: #76 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
This commit was merged in pull request #76.
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
#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));
|
||||
@@ -527,7 +528,7 @@ TEST_CASE("DiffractionGeometry_PONI_matrix_consistency") {
|
||||
.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 = geom.GetDetectorMatrix();
|
||||
const auto poni_rot_T = poni_rot.transpose();
|
||||
|
||||
// Test: poni_rot * poni_rot^T should be identity (orthogonal matrix)
|
||||
@@ -631,3 +632,247 @@ TEST_CASE("DiffractionGeometry_Tilted_vs_PyFAI_and_DIALS", "[DiffractionGeometry
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user