Files
Jungfraujoch/tests/XtalRefineTest.cpp
leonarski_f a395f358ef
Build Packages / Create release (push) Successful in 17s
Build Packages / build:viewer:macos-arm64:nocuda (push) Successful in 3m22s
Build Packages / build:rugnux:macos-arm64:nocuda (push) Successful in 2m37s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 9m33s
Build Packages / build:rugnux:linux-x86_64:cuda (push) Successful in 10m39s
Build Packages / build:viewer:linux-x86_64:nocuda (push) Successful in 11m4s
Build Packages / build:viewer:linux-x86_64:cuda (push) Successful in 13m19s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 17m37s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 18m49s
Build Packages / build:viewer:windows-x86_64:nocuda (push) Successful in 19m10s
Build Packages / build:viewer:windows-x86_64:cuda (push) Successful in 24m26s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m31s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 18m54s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 20m45s
Build Packages / Generate python client (push) Successful in 37s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 20m20s
Build Packages / Build documentation (push) Successful in 1m32s
Build Packages / build:rugnux:windows-x86_64:cuda (push) Successful in 14m37s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 21m6s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 19m49s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 20m29s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 17m2s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 14m27s
Build Packages / Unit tests (push) Successful in 1h18m12s
1.0.0-rc.174 (#84)
* Rugnux: Performance improvements on GPU and CPU (more of the pre-scan and of scaling on the GPU, faster CPU spot finding and crystal refinement), with unchanged results.
* Rugnux: More robust processing - patches of persistently hot pixels are masked, an inconsistent merge triggers a retry at the measured beam centre, and builds targeting different CPU levels give the same results.
* Rugnux: Improved scaling and merging - reflections with an overloaded pixel are dropped, as in XDS, sparse rotation sweeps are scaled more reliably, and French-Wilson amplitudes use an anisotropic Wilson prior.
* Rugnux: Improved space-group determination - glide planes in groups without a centre of symmetry, screw axes from short or weak axial rows kept when a higher group is adopted, and more reliable decisions on twinned and pseudo-symmetric crystals.
* Rugnux: Improved small-molecule processing - spots that grow wider than the integration disk and split spots are integrated over their measured footprint, sparse lattices are integrated on every frame, and the `.hkl` file holds unmerged scaled reflections (SHELX HKLF 4).
* Rugnux: Reads Rigaku d*TREK SMV images (Saturn CCD), including detector 2theta and encoded pixel overflows; home-source (rotating-anode) datasets were added to the validation battery.
* jfjoch_viewer: Fixed processing failing at the end with "Wrong JPEG library version" on Linux; the merge window shows the space group with proper subscripts and a checklist of crystal pathologies.

Reviewed-on: #84
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-10-06 14:03:18 +02:00

128 lines
5.5 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
// Ceres first: its logging header defines a CHECK macro of its own, which Catch's must replace here.
#include "XtalRefineCeres.h"
#undef CHECK
#include <catch2/catch_all.hpp>
#include "../image_analysis/geom_refinement/LatticeReduction.h"
#include "../image_analysis/bragg_prediction/BraggPrediction.h"
// The LM solver of XtalRefine is meant to take Ceres' path to Ceres' answer: same steps accepted, same
// stopping rule, same point. These cases hand one problem to both and compare.
namespace {
// A monoclinic crystal rotated about X over ten 3-degree frames, its predicted spots as observations;
// the refinement starts from a perturbed beam, tilt and cell.
XtalRefineProblem RotationProblem(bool reduced, bool weighted) {
DiffractionExperiment exp;
exp.IncidentEnergy_keV(WVL_1A_IN_KEV).BeamX_pxl(1000).BeamY_pxl(1000)
.PoniRot1_rad(0.01).PoniRot2_rad(0.02).DetectorDistance_mm(200);
const auto geom = exp.GetDiffractionGeometry();
const CrystalLattice latt(40, 50, 80, 90, 95, 90);
const GoniometerAxis axis("omega", 0.0f, 3.0f, Coord(1, 0, 0), std::nullopt);
const gemmi::CrystalSystem sys = gemmi::CrystalSystem::Monoclinic;
XtalRefineProblem p;
p.crystal_system = sys;
p.beam_and_orientation_only = reduced;
p.distance_mm = 200;
BraggPrediction prediction;
const BraggPredictionSettings settings{.high_res_A = 1.5, .ewald_dist_cutoff = 0.002};
for (int img = 0; img < 10; img++) {
const float angle_deg = axis.GetAngle_deg(img) + axis.GetWedge_deg() / 2.0f;
const auto n = prediction.Calc(exp, latt.Multiply(axis.GetTransformationAngle(angle_deg).transpose()),
settings);
p.frame_angle_rad.push_back(angle_deg * PI / 180.0);
for (int i = 0; i < n; i++) {
const auto &r = prediction.GetReflections().at(i);
p.residuals.emplace_back(r.predicted_x + 0.3 * std::sin(i), r.predicted_y + 0.3 * std::cos(i),
geom.GetWavelength_A(), geom.GetPixelSize_mm(), 1.0, 0.0,
angle_deg * PI / 180.0, r.h, r.k, r.l, sys);
p.frame.push_back(img);
if (weighted)
p.weight_sq.push_back(0.2 + 0.6 * (i % 5) / 4.0);
}
}
p.beam[0] = 1000.0;
p.beam[1] = 997.0;
p.detector_rot[0] = 0.012;
p.detector_rot[1] = 0.018;
p.rot_vec[0] = 1.0;
p.rot_vec[1] = 0.0;
p.rot_vec[2] = 0.0;
double beta = 0;
LatticeToRodriguesLengthsBeta_Mono(CrystalLattice(39.7f, 50.6f, 79.6f, 90.0f, 94.5f, 90.0f),
p.latt_vec0, p.latt_vec1, beta);
p.latt_vec2[0] = beta;
if (!reduced) {
p.detector_rot_constant = false;
p.rot_vec_constant = false;
p.latt_vec1_constant = false;
p.latt_vec2_constant = false;
for (int i = 0; i < 2; i++) {
p.detector_rot_lower[i] = p.detector_rot[i] - 0.05;
p.detector_rot_upper[i] = p.detector_rot[i] + 0.05;
}
for (int i = 0; i < 3; i++) {
p.latt_vec1_lower[i] = 5.0;
p.latt_vec1_upper[i] = 100.0;
}
p.latt_vec2_lower[0] = PI / 3;
p.latt_vec2_upper[0] = 2 * PI / 3;
p.priors.push_back({XtalRefinePrior::Block::Beam, 1.0, 0.0, p.beam[0], 0.5});
p.priors.push_back({XtalRefinePrior::Block::DetectorRot, 0.0, 1.0, p.detector_rot[1], 50.0});
}
p.options.max_iterations = 50;
return p;
}
void CompareWithCeres(XtalRefineProblem p) {
XtalRefineProblem q = p;
const LMSummary lm = SolveXtalRefine(p, 4);
const ceres::Solver::Summary ref = SolveXtalRefineCeres(q, 4);
REQUIRE(lm.IsSolutionUsable() == ref.IsSolutionUsable());
CHECK(lm.iterations == static_cast<int>(ref.iterations.size()));
CHECK(lm.final_cost == Catch::Approx(ref.final_cost).epsilon(1e-9));
const auto same = [](const double *a, const double *b, int n, double tol) {
for (int i = 0; i < n; i++)
CHECK(a[i] == Catch::Approx(b[i]).margin(tol));
};
same(p.beam, q.beam, 2, 1e-7);
same(p.detector_rot, q.detector_rot, 2, 1e-10);
same(p.rot_vec, q.rot_vec, 3, 1e-10);
same(p.latt_vec0, q.latt_vec0, 3, 1e-10);
same(p.latt_vec1, q.latt_vec1, 3, 1e-8);
same(p.latt_vec2, q.latt_vec2, 3, 1e-10);
}
}
TEST_CASE("XtalRefine_matches_Ceres_full", "[XtalOptimizer]") {
CompareWithCeres(RotationProblem(false, false));
}
TEST_CASE("XtalRefine_matches_Ceres_full_weighted", "[XtalOptimizer]") {
CompareWithCeres(RotationProblem(false, true));
}
TEST_CASE("XtalRefine_matches_Ceres_beam_orientation", "[XtalOptimizer]") {
CompareWithCeres(RotationProblem(true, false));
}
TEST_CASE("XtalRefine_same_answer_at_any_thread_count", "[XtalOptimizer]") {
XtalRefineProblem a = RotationProblem(false, false);
XtalRefineProblem b = a;
SolveXtalRefine(a, 1);
SolveXtalRefine(b, 7);
for (int i = 0; i < 3; i++) {
CHECK(a.latt_vec0[i] == b.latt_vec0[i]);
CHECK(a.latt_vec1[i] == b.latt_vec1[i]);
}
CHECK(a.beam[0] == b.beam[0]);
CHECK(a.beam[1] == b.beam[1]);
}