v1.0.0-rc.175 (#85)
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 20m49s
Build Packages / Create release (push) Successful in 7s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 4m56s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 9m10s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 13m51s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 16m56s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 20m59s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m25s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 21m17s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 14m14s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m59s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 25m48s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 15m52s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 19m33s
Build Packages / Generate python client (push) Successful in 29s
Build Packages / Build documentation (push) Successful in 1m19s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 25m31s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 25m24s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 26m5s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 22m19s
Build Packages / Unit tests (push) Failing after 2h28m24s
Build Packages / Validation Battery (push) Successful in 40m33s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 20m49s
Build Packages / Create release (push) Successful in 7s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 4m56s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 9m10s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 13m51s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 16m56s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 20m59s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m25s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 21m17s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 14m14s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m59s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 25m48s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 15m52s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 19m33s
Build Packages / Generate python client (push) Successful in 29s
Build Packages / Build documentation (push) Successful in 1m19s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 25m31s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 25m24s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 26m5s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 22m19s
Build Packages / Unit tests (push) Failing after 2h28m24s
Build Packages / Validation Battery (push) Successful in 40m33s
* Rugnux: Processes a collection live while jfjoch_broker writes it (`rugnux <master.h5> <broker URL>`). * Rugnux: Processing starts on the first images and confirms its choices once all are in, so analysis can begin during collection. * Rugnux: Faster processing, especially on servers with several GPUs and CPU sockets and on computers without a GPU (`--no-gpu`, macOS). * Rugnux: Uses much less GPU memory, so large data sets stay on the GPU of smaller cards, and a data set too large for the card is processed on the CPU instead. * Rugnux: On macOS, Bragg integration runs on the Apple GPU. * Rugnux: Better handling of twinning and pseudo-symmetry, with the twin hypothesis written as `<prefix>_twin_<SG>.mtz`, and a wrong `--model` no longer confirms the data or decides the hand. * Rugnux: `--model` accepts predicted (AlphaFold) models and falls back to molecular replacement when the model is not placed as a rigid body (experimental `--mr` forces it), settling space group, hand and twinning and writing a different answer as `<prefix>_mr.mtz`. * Rugnux: Molecular replacement is faster and settles the space group in more cases with several copies in the asymmetric unit. * Rugnux: Deposition-ready mmCIF, a richer report, and numbered steps on the terminal with the full log in `<prefix>_log.txt`; `_P1.mtz`, `--estimate-beam-center` and `--beam-center-search` are removed. * Rugnux: The beam-stop shadow is found correctly on tilted detectors. * Rugnux: Stills files that record a stationary goniometer axis get the default stills geometry refinement. * jfjoch_viewer: "Analyze dataset" uses the same defaults as a bare `rugnux <file>`, and shows the results of processed CBF, marCCD and SMV sweeps. * jfjoch_viewer: The Files panel refreshes on F5 and every few seconds, and the plot type and other selections stay put during live collection. * jfjoch_writer: Memory stays bounded and old data files are closed efficiently on long runs, any failure ends the run in an error state the broker is told about, and the master file appears only after its data files; the ZeroMQ pusher logs when a stalled writer makes it drop images. * Fixes: The TCP image pusher no longer blocks the broker's receiver, GPU out-of-memory failures stop with a clear message, and the XDS plugin masks module and chip edges. --------- Co-authored-by: Filip Leonarski <jungfrau@filips-mac-mini.home> Reviewed-on: #85 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
This commit was merged in pull request #85.
This commit is contained in:
1 parent
a395f358ef
commit
19b135186d
779 files changed
+201230
-17809
No files matched your search
@@ -3,11 +3,16 @@
|
||||
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <cstring>
|
||||
#include <random>
|
||||
#include "../image_analysis/bragg_integration/CalcISigma.h"
|
||||
#include "../image_analysis/scale_merge/ErrorModel.h"
|
||||
#include "../image_analysis/scale_merge/HKLKey.h"
|
||||
#include "../image_analysis/scale_merge/Merge.h"
|
||||
#include "../image_analysis/scale_merge/ResolutionCutoff.h"
|
||||
#include "../image_analysis/scale_merge/ScaleOnTheFly.h"
|
||||
#include "../image_analysis/scale_merge/StillsPartialityRefine.h"
|
||||
#include "../image_analysis/scale_merge/SublatticeRelabel.h"
|
||||
#include "gemmi/reciproc.hpp"
|
||||
|
||||
TEST_CASE("HKLKey_NoSG_noMergeFriedel") {
|
||||
@@ -316,6 +321,7 @@ namespace {
|
||||
std::normal_distribution<double> g(0.0, sigma);
|
||||
for (int j = 0; j < n; ++j) {
|
||||
MergedReflection m;
|
||||
m.h = static_cast<int32_t>(v.size()); // every reflection its own index
|
||||
m.d = static_cast<float>(1.0 / std::sqrt(s_from + (j + 0.5) * (s_to - s_from) / n));
|
||||
const double a = g(rng), b = g(rng);
|
||||
m.I_half[0] = static_cast<float>(a);
|
||||
@@ -345,6 +351,29 @@ TEST_CASE("ResolutionCutoff_CleanFallOff") {
|
||||
CHECK(*rc.d_cut == Catch::Approx(1.92).margin(0.15));
|
||||
}
|
||||
|
||||
// Past the fall-off, a few single reflections read hundreds of times the shell's noise in both halves -
|
||||
// an artefact the half-sets reproduce. Each carries nearly all of its shell's covariance, so a Pearson
|
||||
// CC1/2 reads those shells as signal and the cut runs on through them; one reflection per shell must not
|
||||
// decide where the signal stops.
|
||||
TEST_CASE("ResolutionCutoff_SingleReflectionsDoNotHoldTheCutOpen") {
|
||||
Logger logger("test");
|
||||
std::mt19937 rng(12345);
|
||||
std::vector<MergedReflection> merged;
|
||||
AddBand(merged, rng, 0.01, 0.25, 480, true);
|
||||
AddBand(merged, rng, 0.25, 0.51, 520, false);
|
||||
for (const double s : {0.26, 0.28, 0.30, 0.32}) {
|
||||
MergedReflection m;
|
||||
m.h = static_cast<int32_t>(merged.size());
|
||||
m.d = static_cast<float>(1.0 / std::sqrt(s));
|
||||
m.I_half[0] = m.I_half[1] = 300.0f;
|
||||
merged.push_back(m);
|
||||
}
|
||||
|
||||
const auto rc = ComputeCCHalfLogisticCutoff(merged, 0.30, logger);
|
||||
REQUIRE(rc.d_fit);
|
||||
CHECK(*rc.d_fit == Catch::Approx(2.0).margin(0.15));
|
||||
}
|
||||
|
||||
// A sweep with a long stretch where the crystal barely diffracted: the reflections measured only there
|
||||
// are noise scaled up by 1/G, sigmas with them, and there are many of them at every resolution. Counted
|
||||
// by the information they carry they must not hide where the well-measured reflections stop.
|
||||
@@ -502,3 +531,335 @@ TEST_CASE("ErrorModel_BNotMeasuredOnWeakData") {
|
||||
CHECK(fit.b2 == 0.0);
|
||||
CHECK(fit.a == Catch::Approx(0.9).epsilon(0.03));
|
||||
}
|
||||
|
||||
#ifdef JFJOCH_USE_CUDA
|
||||
#include "../common/CUDAWrapper.h"
|
||||
#include "../image_analysis/scale_merge/ErrorModelGPU.h"
|
||||
|
||||
// The device fit against the host one. Same bins and same medians; only the order each bin's two sums
|
||||
// are added in differs, so a and b^2 agree to rounding and every flag agrees exactly. The same pool
|
||||
// twice on the device gives the same bits.
|
||||
TEST_CASE("ErrorModel_DeviceFitIsTheHostFit") {
|
||||
if (get_gpu_count() == 0) {
|
||||
WARN("No CUDA GPU present. Skipping ErrorModel_DeviceFitIsTheHostFit");
|
||||
return;
|
||||
}
|
||||
for (const auto &pool : {SyntheticErrorModelSamples(1.3, 0.03, 300.0, 0.003),
|
||||
SyntheticErrorModelSamples(0.9, 0.05, 1.5, 0.0)}) {
|
||||
std::vector<ErrorModelBinned> scratch;
|
||||
const auto host = FitErrorModel(pool, scratch, 4);
|
||||
const auto dev = FitErrorModelGPU(pool);
|
||||
const auto again = FitErrorModelGPU(pool);
|
||||
CHECK(dev.active == host.active);
|
||||
CHECK(dev.b_measured == host.b_measured);
|
||||
CHECK(dev.b_resolved == host.b_resolved);
|
||||
CHECK(dev.a == Catch::Approx(host.a).epsilon(1e-12));
|
||||
CHECK(dev.b2 == Catch::Approx(host.b2).epsilon(1e-12).margin(1e-300));
|
||||
CHECK(again.a == dev.a);
|
||||
CHECK(again.b2 == dev.b2);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// The dataset-wide Wilson B: a pure exp(-B / 2d^2) fall-off, read back over the fit window. A crystal
|
||||
// diffracting to just under 4 A has almost nothing between its d_min and the 4 A convention, so the
|
||||
// window is widened to the 1/d^2 width a 3 A dataset gets; and a high B is a measurement, not garbage.
|
||||
TEST_CASE("Global Wilson B over a widened window, with no upper cap", "[wilson]") {
|
||||
const auto merge = [](double b, double d_min) {
|
||||
std::vector<MergedReflection> out;
|
||||
for (int i = 0; i < 20000; ++i) {
|
||||
// Uniform in 1/d^3 from 20 A to d_min, as reflections are.
|
||||
const double s3 = (i + 0.5) / 20000.0 * (1.0 / std::pow(d_min, 3) - 1.0 / 8000.0) + 1.0 / 8000.0;
|
||||
MergedReflection r;
|
||||
r.h = i; r.k = 0; r.l = 0;
|
||||
r.d = static_cast<float>(1.0 / std::cbrt(s3));
|
||||
r.I = static_cast<float>(1e6 * std::exp(-b / (2.0 * r.d * r.d)));
|
||||
r.sigma = 0.01f * r.I;
|
||||
out.push_back(r);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const GlobalWilsonB high_res = CalcGlobalWilsonB(merge(40.0, 2.0));
|
||||
CHECK(high_res.b == Catch::Approx(40.0).epsilon(0.02));
|
||||
CHECK(high_res.d_low_A == Catch::Approx(4.0));
|
||||
|
||||
const GlobalWilsonB low_res = CalcGlobalWilsonB(merge(300.0, 3.9));
|
||||
CHECK(low_res.b == Catch::Approx(300.0).epsilon(0.02));
|
||||
CHECK(low_res.n_shells >= 3);
|
||||
CHECK(1.0 / (3.9 * 3.9) - 1.0 / (low_res.d_low_A * low_res.d_low_A)
|
||||
== Catch::Approx(1.0 / 9.0 - 1.0 / 16.0).epsilon(0.01));
|
||||
}
|
||||
|
||||
// The parallel AddImages is the serial AddImage loop, bit for bit: the per-observation work is spread
|
||||
// over the workers, but every sum and every half-set choice is still made in image order. More images
|
||||
// than one of its blocks, so the block boundary is crossed.
|
||||
TEST_CASE("MergeOnTheFly_AddImagesIsTheSerialMerge") {
|
||||
DiffractionExperiment x;
|
||||
x.SetSpaceGroup(TestSpaceGroup());
|
||||
|
||||
std::mt19937 rng(7);
|
||||
std::uniform_int_distribution<int> index(-6, 6);
|
||||
std::uniform_real_distribution<float> unit(0.0f, 1.0f);
|
||||
std::vector<IntegrationOutcome> outcomes(5000);
|
||||
for (auto &o: outcomes) {
|
||||
for (int j = 0; j < 20; ++j) {
|
||||
Reflection r{};
|
||||
r.h = index(rng);
|
||||
r.k = index(rng);
|
||||
r.l = index(rng);
|
||||
if (r.h == 0 && r.k == 0 && r.l == 0)
|
||||
continue;
|
||||
r.d = 20.0f / std::sqrt(static_cast<float>(r.h * r.h + r.k * r.k + r.l * r.l));
|
||||
r.I = 100.0f * unit(rng) - 5.0f;
|
||||
r.sigma = 1.0f + 5.0f * unit(rng);
|
||||
r.var_bkg = r.sigma * r.sigma * unit(rng);
|
||||
r.prescaling_corr = 1.0f;
|
||||
r.partiality = 0.5f + 0.5f * unit(rng);
|
||||
r.image_scale_corr = 0.5f + unit(rng);
|
||||
r.on_ice_ring = unit(rng) < 0.05f;
|
||||
o.reflections.push_back(r);
|
||||
}
|
||||
}
|
||||
|
||||
MergeOnTheFly serial(x);
|
||||
serial.RefineErrorModel(outcomes);
|
||||
for (size_t i = 0; i < outcomes.size(); ++i)
|
||||
serial.AddImage(outcomes[i], static_cast<int64_t>(i));
|
||||
const auto serial_merged = serial.ExportReflections();
|
||||
|
||||
MergeOnTheFly parallel(x);
|
||||
parallel.Threads(8);
|
||||
parallel.RefineErrorModel(outcomes);
|
||||
parallel.AddImages(outcomes);
|
||||
const auto parallel_merged = parallel.ExportReflections();
|
||||
|
||||
REQUIRE(serial.ErrorModelActive());
|
||||
CHECK(serial.ErrorModelA() == parallel.ErrorModelA());
|
||||
CHECK(serial.ErrorModelB() == parallel.ErrorModelB());
|
||||
REQUIRE(serial_merged.size() == parallel_merged.size());
|
||||
for (size_t i = 0; i < serial_merged.size(); ++i) {
|
||||
CHECK(serial_merged[i].h == parallel_merged[i].h);
|
||||
CHECK(serial_merged[i].k == parallel_merged[i].k);
|
||||
CHECK(serial_merged[i].l == parallel_merged[i].l);
|
||||
CHECK(serial_merged[i].I == parallel_merged[i].I);
|
||||
CHECK(serial_merged[i].sigma == parallel_merged[i].sigma);
|
||||
for (int half = 0; half < 2; ++half) // NaN where a half is empty, on both sides
|
||||
CHECK(std::memcmp(&serial_merged[i].I_half[half], ¶llel_merged[i].I_half[half], sizeof(float)) == 0);
|
||||
}
|
||||
|
||||
const auto serial_stats = serial.MergeStats(serial_merged, outcomes);
|
||||
const auto parallel_stats = parallel.MergeStats(parallel_merged, outcomes);
|
||||
CHECK(serial_stats.overall.total_observations == parallel_stats.overall.total_observations);
|
||||
CHECK(serial_stats.overall.cc_half == parallel_stats.overall.cc_half);
|
||||
CHECK(serial_stats.overall.r_meas == parallel_stats.overall.r_meas);
|
||||
}
|
||||
|
||||
// Stills partiality post-refinement on noiseless synthetic crystals: each crystal's true orientation is
|
||||
// tilted from the stored one by up to 0.05 deg, its intensities are the true partials of a fixed set of
|
||||
// full intensities, and its stored partialities those of the stored orientation. The refinement has to
|
||||
// move the partialities towards the true ones. (The Ceres solve this replaced gave the same numbers: error
|
||||
// 0.0559 -> 0.0302, mean tilt 0.0206 deg.)
|
||||
TEST_CASE("StillsPartialityRefine_RecoversTheTilt") {
|
||||
constexpr float CELL = 50.0f, WAVELENGTH = 1.0f, D_MIN = 2.0f;
|
||||
constexpr double GAMMA0 = 0.001, ACCEPT = 1.7 * GAMMA0; // RLP width; |dist_ewald| accepted at prediction
|
||||
DiffractionExperiment x;
|
||||
x.SetSpaceGroup(gemmi::get_spacegroup_by_name("P 1"));
|
||||
DiffractionGeometry geom;
|
||||
geom.Wavelength_A(WAVELENGTH);
|
||||
const Coord S0 = geom.GetScatteringVector();
|
||||
|
||||
std::mt19937 rng(11);
|
||||
std::normal_distribution<double> gauss(0.0, 1.0);
|
||||
std::uniform_real_distribution<double> unit(0.0, 1.0);
|
||||
auto full_I = [](int h, int k, int l) { // a fixed, Friedel-symmetric pseudo-random full intensity
|
||||
const uint64_t key = HKLKeyGenerator(true, gemmi::get_spacegroup_by_name("P 1"))(h, k, l).pack();
|
||||
return 10.0 + 990.0 * static_cast<double>((key * 0x9e3779b97f4a7c15ULL) >> 40) / static_cast<double>(1ULL << 24);
|
||||
};
|
||||
auto rotate = [](const double R[9], const Coord &v) {
|
||||
return Coord(static_cast<float>(R[0] * v.x + R[1] * v.y + R[2] * v.z),
|
||||
static_cast<float>(R[3] * v.x + R[4] * v.y + R[5] * v.z),
|
||||
static_cast<float>(R[6] * v.x + R[7] * v.y + R[8] * v.z));
|
||||
};
|
||||
auto rotation = [](double ax, double ay, double az, double angle, double R[9]) { // Rodrigues
|
||||
const double n = std::sqrt(ax * ax + ay * ay + az * az);
|
||||
ax /= n; ay /= n; az /= n;
|
||||
const double c = std::cos(angle), s = std::sin(angle), t = 1.0 - c;
|
||||
const double M[9] = {c + t * ax * ax, t * ax * ay - s * az, t * ax * az + s * ay,
|
||||
t * ax * ay + s * az, c + t * ay * ay, t * ay * az - s * ax,
|
||||
t * ax * az - s * ay, t * ay * az + s * ax, c + t * az * az};
|
||||
std::copy(M, M + 9, R);
|
||||
};
|
||||
auto partiality = [&](const Coord &q) {
|
||||
const double de = (q + S0).Length() - 1.0 / WAVELENGTH;
|
||||
return std::exp(-0.5 * de * de / (GAMMA0 * GAMMA0));
|
||||
};
|
||||
|
||||
std::vector<IntegrationOutcome> outcomes(150);
|
||||
std::vector<std::vector<double>> true_p(outcomes.size());
|
||||
const int hmax = static_cast<int>(CELL / D_MIN);
|
||||
for (size_t c = 0; c < outcomes.size(); ++c) {
|
||||
double R[9], T[9];
|
||||
rotation(gauss(rng), gauss(rng), gauss(rng), 2.0 * M_PI * unit(rng), R);
|
||||
const double tilt = (0.02 + 0.03 * unit(rng)) * M_PI / 180.0, phi = 2.0 * M_PI * unit(rng);
|
||||
rotation(std::cos(phi), std::sin(phi), 0.0, tilt, T); // about an axis normal to the beam
|
||||
auto &o = outcomes[c];
|
||||
o.geom = geom;
|
||||
o.latt = CrystalLattice(rotate(R, Coord(CELL, 0, 0)), rotate(R, Coord(0, CELL, 0)),
|
||||
rotate(R, Coord(0, 0, CELL)));
|
||||
const double G = 0.5 + unit(rng);
|
||||
for (int h = -hmax; h <= hmax; ++h)
|
||||
for (int k = -hmax; k <= hmax; ++k)
|
||||
for (int l = -hmax; l <= hmax; ++l) {
|
||||
const Coord q = o.latt.Astar() * h + o.latt.Bstar() * k + o.latt.Cstar() * l;
|
||||
const float dstar = q.Length();
|
||||
if (dstar == 0.0f || dstar > 1.0f / D_MIN)
|
||||
continue;
|
||||
if (std::fabs((q + S0).Length() - 1.0 / WAVELENGTH) > ACCEPT)
|
||||
continue;
|
||||
const double p_true = partiality(rotate(T, q));
|
||||
Reflection r{};
|
||||
r.h = h; r.k = k; r.l = l;
|
||||
r.d = 1.0f / dstar;
|
||||
r.I = static_cast<float>(G * p_true * full_I(h, k, l));
|
||||
r.sigma = static_cast<float>(1.0 + 0.01 * r.I);
|
||||
r.prescaling_corr = 1.0f;
|
||||
r.partiality = static_cast<float>(partiality(q));
|
||||
r.image_scale_corr = 1.0f / r.partiality;
|
||||
o.reflections.push_back(r);
|
||||
true_p[c].push_back(p_true);
|
||||
}
|
||||
REQUIRE(o.reflections.size() >= 40);
|
||||
}
|
||||
|
||||
auto mean_abs_error = [&]() {
|
||||
double sum = 0.0;
|
||||
size_t n = 0;
|
||||
for (size_t c = 0; c < outcomes.size(); ++c)
|
||||
for (size_t i = 0; i < outcomes[c].reflections.size(); ++i, ++n)
|
||||
sum += std::fabs(outcomes[c].reflections[i].partiality - true_p[c][i]);
|
||||
return sum / static_cast<double>(n);
|
||||
};
|
||||
const double before = mean_abs_error();
|
||||
ScaleOnTheFly(x, MergeAll(x, outcomes, 4, std::nullopt)).Scale(outcomes, 4);
|
||||
const double mean_tilt = StillsPartialityRefine(x).Run(outcomes, 4, std::nullopt);
|
||||
const double after = mean_abs_error();
|
||||
CHECK(mean_tilt > 0.015);
|
||||
CHECK(mean_tilt < 0.06);
|
||||
CHECK(after < 0.6 * before); // measured 0.54: the prior holds the larger tilts back
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The basis vectors a, b, c of a cell, turned by a fixed arbitrary rotation, as a frame's lattice.
|
||||
std::array<Coord, 3> TurnedAxes(const UnitCell &cell) {
|
||||
const CrystalLattice l(cell);
|
||||
const double c = std::cos(0.7), s = std::sin(0.7); // about (1, 2, 3)/sqrt(14)
|
||||
const double n[3] = {1.0 / std::sqrt(14.0), 2.0 / std::sqrt(14.0), 3.0 / std::sqrt(14.0)};
|
||||
auto turn = [&](const Coord &v) {
|
||||
const double dot = n[0] * v.x + n[1] * v.y + n[2] * v.z;
|
||||
const double x[3] = {n[1] * v.z - n[2] * v.y, n[2] * v.x - n[0] * v.z, n[0] * v.y - n[1] * v.x};
|
||||
return Coord(static_cast<float>(v.x * c + x[0] * s + n[0] * dot * (1 - c)),
|
||||
static_cast<float>(v.y * c + x[1] * s + n[1] * dot * (1 - c)),
|
||||
static_cast<float>(v.z * c + x[2] * s + n[2] * dot * (1 - c)));
|
||||
};
|
||||
return {turn(l.Vec0()), turn(l.Vec1()), turn(l.Vec2())};
|
||||
}
|
||||
|
||||
Coord ScatteringVector(const CrystalLattice &l, const Reflection &r) {
|
||||
return l.Astar() * static_cast<float>(r.h) + l.Bstar() * static_cast<float>(r.k)
|
||||
+ l.Cstar() * static_cast<float>(r.l);
|
||||
}
|
||||
|
||||
IntegrationOutcome FrameWithAllIndices(const CrystalLattice &latt, int hmax) {
|
||||
IntegrationOutcome o;
|
||||
o.latt = latt;
|
||||
for (int h = -hmax; h <= hmax; ++h)
|
||||
for (int k = -hmax; k <= hmax; ++k)
|
||||
for (int l = -hmax; l <= hmax; ++l) {
|
||||
Reflection r{};
|
||||
r.h = h; r.k = k; r.l = l;
|
||||
r.predicted_x = static_cast<float>(o.reflections.size()); // identifies the observation
|
||||
r.I = 100.0f;
|
||||
r.sigma = 10.0f;
|
||||
const float q = ScatteringVector(latt, r).Length();
|
||||
r.d = q > 0.0f ? 1.0f / q : 100.0f;
|
||||
o.reflections.push_back(r);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
}
|
||||
|
||||
// A frame indexed with its c axis doubled: its reciprocal lattice holds every node of the true one plus
|
||||
// the planes in between. Relabelling keeps exactly the true nodes, each at the same scattering vector,
|
||||
// and gives the frame the true cell.
|
||||
TEST_CASE("SublatticeRelabel_DoubledAxisGoesBackToTheTrueCell") {
|
||||
const UnitCell reference{60, 60, 25, 90, 90, 90}; // synthetic tetragonal
|
||||
const auto ax = TurnedAxes(reference);
|
||||
const CrystalLattice super(ax[0], ax[1], ax[2] * 2.0f);
|
||||
// A frame on the true cell beside it sets how fine the relabelled reflections may go.
|
||||
std::vector<IntegrationOutcome> outcomes{FrameWithAllIndices(super, 4),
|
||||
FrameWithAllIndices(CrystalLattice(ax[0], ax[1], ax[2]), 4)};
|
||||
const auto before = outcomes[0];
|
||||
|
||||
IndexingSettings settings;
|
||||
const auto result = RelabelOnSublattice(outcomes, reference, settings);
|
||||
CHECK(result.frames == 1);
|
||||
CHECK(result.reflections_kept == 9 * 9 * 5); // l even: -4, -2, 0, 2, 4
|
||||
CHECK(result.reflections_dropped == 9 * 9 * 4);
|
||||
CHECK(outcomes[0].latt.GetUnitCell().is_close(reference, 0.001, 0.01));
|
||||
|
||||
for (const auto &r: outcomes[0].reflections) {
|
||||
const auto it = std::find_if(before.reflections.begin(), before.reflections.end(),
|
||||
[&](const Reflection &b) { return b.predicted_x == r.predicted_x; });
|
||||
REQUIRE(it != before.reflections.end());
|
||||
CHECK(it->l % 2 == 0);
|
||||
const Coord q_before = ScatteringVector(before.latt, *it);
|
||||
const Coord q_after = ScatteringVector(outcomes[0].latt, r);
|
||||
CHECK((q_after - q_before).Length() < 1e-5f);
|
||||
}
|
||||
}
|
||||
|
||||
// The true lattice in another setting (axes permuted) is the same lattice: every reflection is kept and
|
||||
// only re-indexed. A frame whose lattice does not contain the reference is left alone.
|
||||
TEST_CASE("SublatticeRelabel_OtherSettingAndUnrelatedLattice") {
|
||||
const UnitCell reference{40, 50, 60, 90, 90, 90}; // synthetic orthorhombic
|
||||
const auto ax = TurnedAxes(reference);
|
||||
std::vector<IntegrationOutcome> outcomes{FrameWithAllIndices(CrystalLattice(ax[1], ax[2], ax[0]), 3),
|
||||
FrameWithAllIndices(CrystalLattice(UnitCell{37, 53, 71, 90, 90, 90}), 3),
|
||||
FrameWithAllIndices(CrystalLattice(ax[0], ax[1], ax[2]), 4)};
|
||||
const auto permuted = outcomes[0];
|
||||
const auto unrelated = outcomes[1];
|
||||
|
||||
IndexingSettings settings;
|
||||
const auto result = RelabelOnSublattice(outcomes, reference, settings);
|
||||
CHECK(result.frames == 1);
|
||||
CHECK(result.reflections_dropped == 0);
|
||||
CHECK(outcomes[0].latt.GetUnitCell().is_close(reference, 0.001, 0.01));
|
||||
for (size_t i = 0; i < outcomes[0].reflections.size(); ++i) {
|
||||
const Coord q_before = ScatteringVector(permuted.latt, permuted.reflections[i]);
|
||||
const Coord q_after = ScatteringVector(outcomes[0].latt, outcomes[0].reflections[i]);
|
||||
CHECK((q_after - q_before).Length() < 1e-5f);
|
||||
}
|
||||
CHECK(outcomes[1].reflections.size() == unrelated.reflections.size());
|
||||
CHECK(outcomes[1].reflections[5].h == unrelated.reflections[5].h);
|
||||
}
|
||||
|
||||
// A doubled axis that makes the supercell metric cubic fits the reference along any of the three axes;
|
||||
// only the true one has signal at its nodes, and that is the one taken.
|
||||
TEST_CASE("SublatticeRelabel_SignalPicksTheAxisThatWasDoubled") {
|
||||
const UnitCell reference{60, 60, 30, 90, 90, 90}; // synthetic tetragonal; doubled c gives a cube
|
||||
const auto ax = TurnedAxes(reference);
|
||||
std::vector<IntegrationOutcome> outcomes{FrameWithAllIndices(CrystalLattice(ax[0], ax[1], ax[2] * 2.0f), 4),
|
||||
FrameWithAllIndices(CrystalLattice(ax[0], ax[1], ax[2]), 4)};
|
||||
for (auto &r: outcomes[0].reflections)
|
||||
r.I = r.l % 2 == 0 ? 100.0f : 0.0f; // the nodes between the true ones are empty
|
||||
const auto before = outcomes[0];
|
||||
|
||||
IndexingSettings settings;
|
||||
const auto result = RelabelOnSublattice(outcomes, reference, settings);
|
||||
REQUIRE(result.frames == 1);
|
||||
for (const auto &r: outcomes[0].reflections) {
|
||||
const auto &b = before.reflections[static_cast<size_t>(r.predicted_x)];
|
||||
CHECK(b.l % 2 == 0);
|
||||
CHECK((ScatteringVector(outcomes[0].latt, r) - ScatteringVector(before.latt, b)).Length() < 1e-5f);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user