Files
Jungfraujoch/tests/CrystalLatticeTest.cpp
T
leonarski_f 680c36c20d
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
v1.0.0-rc.166 (#76)
* `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>
2026-09-02 21:17:31 +02:00

210 lines
8.1 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 "../common/CrystalLattice.h"
TEST_CASE("CrystalLattice") {
CrystalLattice l(50,60,80, 90, 90, 90);
REQUIRE(l.Vec0().Length() == Catch::Approx(50));
REQUIRE(l.Vec1().Length() == Catch::Approx(60));
REQUIRE(l.Vec2().Length() == Catch::Approx(80));
REQUIRE(angle_deg(l.Vec0(), l.Vec2()) == 90);
REQUIRE(angle_deg(l.Vec0(), l.Vec1()) == 90);
REQUIRE(angle_deg(l.Vec1(), l.Vec2()) == 90);
auto uc0 = l.GetUnitCell();
REQUIRE(uc0.a == Catch::Approx(50));
REQUIRE(uc0.b == Catch::Approx(60));
REQUIRE(uc0.c == Catch::Approx(80));
REQUIRE(uc0.alpha == Catch::Approx(90));
REQUIRE(uc0.beta == Catch::Approx(90));
REQUIRE(uc0.gamma == Catch::Approx(90));
l = CrystalLattice(30, 40, 70, 90, 95, 90);
REQUIRE(l.Vec0().Length() == Catch::Approx(30));
REQUIRE(l.Vec1().Length() == Catch::Approx(40));
REQUIRE(l.Vec2().Length() == Catch::Approx(70));
REQUIRE(angle_deg(l.Vec0(), l.Vec2()) == 95);
REQUIRE(angle_deg(l.Vec0(), l.Vec1()) == 90);
REQUIRE(angle_deg(l.Vec1(), l.Vec2()) == 90);
auto uc1 = l.GetUnitCell();
REQUIRE(uc1.a == Catch::Approx(30));
REQUIRE(uc1.b == Catch::Approx(40));
REQUIRE(uc1.c == Catch::Approx(70));
REQUIRE(uc1.alpha == Catch::Approx(90));
REQUIRE(uc1.beta == Catch::Approx(95));
REQUIRE(uc1.gamma == Catch::Approx(90));
l = CrystalLattice(45, 45, 70, 90, 90, 120);
REQUIRE(l.Vec0().Length() == Catch::Approx(45));
REQUIRE(l.Vec1().Length() == Catch::Approx(45));
REQUIRE(l.Vec2().Length() == Catch::Approx(70));
REQUIRE(angle_deg(l.Vec0(), l.Vec2()) == Catch::Approx(90));
REQUIRE(angle_deg(l.Vec0(), l.Vec1()) == Catch::Approx(120));
REQUIRE(angle_deg(l.Vec1(), l.Vec2()) == Catch::Approx(90));
auto uc2 = l.GetUnitCell();
REQUIRE(uc2.a == Catch::Approx(45));
REQUIRE(uc2.b == Catch::Approx(45));
REQUIRE(uc2.c == Catch::Approx(70));
REQUIRE(uc2.alpha == Catch::Approx(90));
REQUIRE(uc2.beta == Catch::Approx(90));
REQUIRE(uc2.gamma == Catch::Approx(120));
}
TEST_CASE("CrystalLattice_Sort") {
CrystalLattice l(80,60,50, 120, 90, 90);
l.Sort();
REQUIRE(l.Vec0().Length() == Catch::Approx(50));
REQUIRE(l.Vec1().Length() == Catch::Approx(60));
REQUIRE(l.Vec2().Length() == Catch::Approx(80));
REQUIRE(angle_deg(l.Vec0(), l.Vec2()) == Catch::Approx(90));
REQUIRE(angle_deg(l.Vec0(), l.Vec1()) == Catch::Approx(120));
REQUIRE(angle_deg(l.Vec1(), l.Vec2()) == Catch::Approx(90));
}
TEST_CASE("CrystalLattice_ReorderMonoclinic") {
std::vector<CrystalLattice> latt = {
{60, 85, 70, 90, 70, 90},
{60, 85, 70, 90, 110, 90},
};
for (const auto &l_in :latt) {
CrystalLattice l = l_in;
l.ReorderMonoclinic();
CHECK(l.Vec0().Length() == Catch::Approx(60));
CHECK(l.Vec1().Length() == Catch::Approx(85));
CHECK(l.Vec2().Length() == Catch::Approx(70));
CHECK(l.GetUnitCell().beta == Catch::Approx(110.0));
}
}
TEST_CASE("CrystalLattice_ReorderTetragonal") {
CrystalLattice l(40,60,60, 90, 90, 90);
l.ReorderABEqual();
REQUIRE(l.Vec0().Length() == Catch::Approx(60));
REQUIRE(l.Vec1().Length() == Catch::Approx(60));
REQUIRE(l.Vec2().Length() == Catch::Approx(40));
}
TEST_CASE("CrystalLattice_ReorderTetragonal_ForHexagonal") {
CrystalLattice l(40,60,60, 120, 90, 90);
l.ReorderABEqual();
REQUIRE(l.Vec0().Length() == Catch::Approx(60));
REQUIRE(l.Vec1().Length() == Catch::Approx(60));
REQUIRE(l.Vec2().Length() == Catch::Approx(40));
}
TEST_CASE("CrystalLattice_Handedness") {
CrystalLattice l(Coord(1,0,0), Coord(0,1,0), Coord(0,0,-1));
REQUIRE(l.Vec0().x == Catch::Approx(1));
REQUIRE(l.Vec1().y == Catch::Approx(1));
REQUIRE(l.Vec2().z == Catch::Approx(1));
}
TEST_CASE("CrystalLattice_Volume") {
CrystalLattice l(50, 60, 80, 90, 90, 90);
REQUIRE(l.CalcVolume() == 50 * 60 * 80);
CrystalLattice l2(50,60,80, 90, 120, 90);
float sin120 = std::sqrt(3) / 2;
REQUIRE(l2.CalcVolume() == Catch::Approx(50 * 60 * 80 * sin120));
}
TEST_CASE("CrystalLattice_VolumeFraction") {
CrystalLattice ortho(50, 60, 80, 90, 90, 90);
REQUIRE(ortho.VolumeFraction() == Catch::Approx(1.0));
// All three angles at 60 deg is about as oblique as a reduced cell gets, and is still an order
// of magnitude clear of MIN_BASIS_VOLUME_FRACTION.
CrystalLattice oblique(50, 50, 50, 60, 60, 60);
REQUIRE(oblique.VolumeFraction() == Catch::Approx(std::sqrt(0.5)).margin(1e-4));
REQUIRE(oblique.VolumeFraction() > MIN_BASIS_VOLUME_FRACTION);
// Three coplanar rows: ordinary lengths, ordinary angles, no volume. This is what an indexing
// candidate built from directions that all lie in one dense reciprocal plane looks like, and
// what nothing downstream can refine - 1/V is not finite.
CrystalLattice flat(Coord(50, 0, 0), Coord(0, 60, 0), Coord(30, 40, 0));
REQUIRE(flat.VolumeFraction() < MIN_BASIS_VOLUME_FRACTION);
CrystalLattice degenerate(Coord(0, 0, 0), Coord(0, 60, 0), Coord(0, 0, 80));
REQUIRE(degenerate.VolumeFraction() == 0.0f);
}
TEST_CASE("CrystalLattice_Recip") {
CrystalLattice l(50,60,80, 90, 90, 90);
REQUIRE(l.Astar().Length() == Catch::Approx(1/50.0));
REQUIRE(l.Astar().x == Catch::Approx(1/50.0));
REQUIRE(l.Bstar().Length() == Catch::Approx(1/60.0));
REQUIRE(l.Bstar().y == Catch::Approx(1/60.0));
REQUIRE(l.Cstar().Length() == Catch::Approx(1/80.0));
REQUIRE(l.Cstar().z == Catch::Approx(1/80.0));
}
TEST_CASE("CrystalLattice_ToPrimitive") {
// Conventional cubic (take as I-conventional basis): a=b=c=50, orthogonal
CrystalLattice conv(50, 50, 50, 90, 90, 90);
const float Vconv = conv.CalcVolume();
REQUIRE(Vconv == Catch::Approx(50.f * 50.f * 50.f).margin(1e-4f));
CrystalLattice prim = conv.ToPrimitive('I');
// Volume halves (index 2)
CHECK(prim.CalcVolume() == Catch::Approx(Vconv * 0.5f).margin(1e-4f));
// bcc primitive lengths and angles
auto uc_p = prim.GetUnitCell();
const float a0 = 50.f;
const float expected_len = a0 * std::sqrt(3.f) * 0.5f; // a*sqrt(3)/2
const float expected_angle = 109.4712206f; // arccos(-1/3) in degrees
CHECK(uc_p.a == Catch::Approx(expected_len).margin(1e-3f));
CHECK(uc_p.b == Catch::Approx(expected_len).margin(1e-3f));
CHECK(uc_p.c == Catch::Approx(expected_len).margin(1e-3f));
CHECK(uc_p.alpha == Catch::Approx(expected_angle).margin(1e-3f));
CHECK(uc_p.beta == Catch::Approx(expected_angle).margin(1e-3f));
CHECK(uc_p.gamma == Catch::Approx(expected_angle).margin(1e-3f));
}
TEST_CASE("CrystalLattice_FromPrimitive") {
// Conventional cubic (take as I-conventional basis): a=b=c=50, orthogonal
const float a0 = 50.f;
const float expected_len = a0 * std::sqrt(3.f) * 0.5f; // a*sqrt(3)/2
const float expected_angle = 109.4712206f; // arccos(-1/3) in degrees
CrystalLattice conv(expected_len, expected_len, expected_len,
expected_angle, expected_angle, expected_angle);
auto prim = conv.FromPrimitive('I');
auto uc_c = prim.GetUnitCell();
// Back to conventional cube with original volume
CHECK(prim.CalcVolume() == Catch::Approx(a0 * a0 * a0).margin(1e-4f));
CHECK(uc_c.a == Catch::Approx(a0).margin(1e-3f));
CHECK(uc_c.b == Catch::Approx(a0).margin(1e-3f));
CHECK(uc_c.c == Catch::Approx(a0).margin(1e-3f));
CHECK(uc_c.alpha == Catch::Approx(90.f).margin(1e-3f));
CHECK(uc_c.beta == Catch::Approx(90.f).margin(1e-3f));
CHECK(uc_c.gamma == Catch::Approx(90.f).margin(1e-3f));
}
TEST_CASE("CrystalLattice_NiggliReduce") {
Coord a = {3,0,0};
Coord b = {0,4,0};
Coord c = {0,0,5};
CrystalLattice l(a + 2 * b, b, c - 5 * b);
auto red_uc = l.NiggliReduce().GetUnitCell();
CHECK(red_uc.a == Catch::Approx(3.0));
CHECK(red_uc.b == Catch::Approx(4.0));
CHECK(red_uc.c == Catch::Approx(5.0));
CHECK(red_uc.alpha == Catch::Approx(90.0));
CHECK(red_uc.beta == Catch::Approx(90.0));
CHECK(red_uc.gamma == Catch::Approx(90.0));
}