Files
Jungfraujoch/tests/GridScanSettingsTest.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

629 lines
24 KiB
C++

// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <catch2/catch_all.hpp>
#include "../common/GridScanSettings.h"
#include "../common/JFJochException.h"
TEST_CASE("GridScanSettings basic construction", "[GridScanSettings]") {
SECTION("Valid parameters") {
REQUIRE_NOTHROW(GridScanSettings(10, 1.0f, 2.0f, false, false));
REQUIRE_NOTHROW(GridScanSettings(10, 1.0f, -2.0f, true, false));
REQUIRE_NOTHROW(GridScanSettings(10, -1.0f, 2.0f, false, true));
REQUIRE_NOTHROW(GridScanSettings(10, 1.0f, 2.0f, true, true));
}
SECTION("Invalid parameters") {
// Negative n_fast
REQUIRE_THROWS_AS(GridScanSettings(-5, 1.0f, 2.0f, false, false), JFJochException);
// Zero n_fast
REQUIRE_THROWS_AS(GridScanSettings(0, 1.0f, 2.0f, false, false), JFJochException);
// Zero grid_step_fast_um
REQUIRE_THROWS_AS(GridScanSettings(10, 0.0f, 2.0f, false, false), JFJochException);
// Zero grid_step_slow_um
REQUIRE_THROWS_AS(GridScanSettings(10, 1.0f, 0.0f, false, false), JFJochException);
}
}
TEST_CASE("GridScanSettings image number", "[GridScanSettings]") {
GridScanSettings grid(5, 1.5f, 2.5f, false, false);
CHECK_THROWS(grid.ImageNum(-1));
grid.ImageNum(0);
CHECK(grid.GetNSlow() == 0);
CHECK(grid.GetNFast() == 5);
grid.ImageNum(1);
CHECK(grid.GetNSlow() == 1);
CHECK(grid.GetNFast() == 5);
grid.ImageNum(4);
CHECK(grid.GetNSlow() == 1);
CHECK(grid.GetNFast() == 5);
grid.ImageNum(6);
CHECK(grid.GetNSlow() == 2);
CHECK(grid.GetNFast() == 5);
grid.ImageNum(13);
CHECK(grid.GetNSlow() == 3);
CHECK(grid.GetNFast() == 5);
}
TEST_CASE("GridScanSettings horizontal scan", "[GridScanSettings]") {
// Create a grid with 5 elements in fast direction, 1.5 um step in fast, 2.5 um in slow
GridScanSettings grid(5, 1.5f, 2.5f, false, false);
SECTION("Grid sizes in steps") {
REQUIRE(grid.GetGridSizeX_step() == 5);
REQUIRE(grid.GetGridSizeY_step() == 1);
REQUIRE(grid.GetGridSizeX_um() == Catch::Approx(5 * 1.5f));
REQUIRE(grid.GetGridSizeY_um() == Catch::Approx(1 * 2.5f));
}
grid.ImageNum(40);
SECTION("Grid sizes in steps") {
REQUIRE(grid.GetGridSizeX_step() == 5);
REQUIRE(grid.GetGridSizeY_step() == 8);
REQUIRE(grid.GetGridSizeX_um() == Catch::Approx(5 * 1.5f));
REQUIRE(grid.GetGridSizeY_um() == Catch::Approx(8 * 2.5f));
}
SECTION("Element positions in standard raster") {
// First row
REQUIRE(grid.GetElementPosX_step(0) == 0);
REQUIRE(grid.GetElementPosY_step(0) == 0);
REQUIRE(grid.GetElementPosX_step(1) == 1);
REQUIRE(grid.GetElementPosY_step(1) == 0);
REQUIRE(grid.GetElementPosX_step(4) == 4);
REQUIRE(grid.GetElementPosY_step(4) == 0);
// Second row
REQUIRE(grid.GetElementPosX_step(5) == 0);
REQUIRE(grid.GetElementPosY_step(5) == 1);
REQUIRE(grid.GetElementPosX_step(9) == 4);
REQUIRE(grid.GetElementPosY_step(9) == 1);
// Positions in um
REQUIRE(grid.GetElementPosX_um(4) == Catch::Approx(6.0f)); // 4 * 1.5
REQUIRE(grid.GetElementPosY_um(9) == Catch::Approx(2.5f)); // 1 * 2.5
}
}
TEST_CASE("GridScanSettings vertical scan", "[GridScanSettings]") {
// Create a vertical grid with 5 elements in fast direction
GridScanSettings grid(5, 2.5f, 1.5f, false, true);
grid.ImageNum(15);
SECTION("Grid sizes in steps") {
// For vertical scan, fast = Y
REQUIRE(grid.GetGridSizeY_step() == 5);
REQUIRE(grid.GetGridSizeX_step() == 3);
}
SECTION("Grid sizes in um") {
REQUIRE(grid.GetGridSizeY_um() == Catch::Approx(5 * 1.5f));
REQUIRE(grid.GetGridSizeX_um() == Catch::Approx(3 * 2.5f));
}
SECTION("Grid steps") {
REQUIRE(grid.GetGridStepY_um() == Catch::Approx(1.5f));
REQUIRE(grid.GetGridStepX_um() == Catch::Approx(2.5f));
}
SECTION("Element positions in vertical scan") {
// First column
REQUIRE(grid.GetElementPosX_step(0) == 0);
REQUIRE(grid.GetElementPosY_step(0) == 0);
REQUIRE(grid.GetElementPosX_step(1) == 0);
REQUIRE(grid.GetElementPosY_step(1) == 1);
REQUIRE(grid.GetElementPosX_step(4) == 0);
REQUIRE(grid.GetElementPosY_step(4) == 4);
// Second column
REQUIRE(grid.GetElementPosX_step(5) == 1);
REQUIRE(grid.GetElementPosY_step(5) == 0);
REQUIRE(grid.GetElementPosX_step(9) == 1);
REQUIRE(grid.GetElementPosY_step(9) == 4);
}
}
TEST_CASE("GridScanSettings snake raster scan", "[GridScanSettings]") {
// Create a grid with snake pattern - horizontal
GridScanSettings grid(5, 1.5f, 2.5f, true, false);
grid.ImageNum(50);
SECTION("Element positions in snake raster") {
// First row (left to right)
REQUIRE(grid.GetElementPosX_step(0) == 0);
REQUIRE(grid.GetElementPosY_step(0) == 0);
REQUIRE(grid.GetElementPosX_step(4) == 4);
REQUIRE(grid.GetElementPosY_step(4) == 0);
// Second row (right to left)
REQUIRE(grid.GetElementPosX_step(5) == 4);
REQUIRE(grid.GetElementPosY_step(5) == 1);
REQUIRE(grid.GetElementPosX_step(9) == 0);
REQUIRE(grid.GetElementPosY_step(9) == 1);
// Third row (left to right)
REQUIRE(grid.GetElementPosX_step(10) == 0);
REQUIRE(grid.GetElementPosY_step(10) == 2);
REQUIRE(grid.GetElementPosX_step(14) == 4);
REQUIRE(grid.GetElementPosY_step(14) == 2);
}
}
TEST_CASE("GridScanSettings vertical snake raster scan", "[GridScanSettings]") {
// Create a grid with snake pattern - vertical
GridScanSettings grid(5, 1.5f, 2.5f, true, true);
grid.ImageNum(50);
SECTION("Element positions in vertical snake raster") {
// First column (top to bottom)
REQUIRE(grid.GetElementPosX_step(0) == 0);
REQUIRE(grid.GetElementPosY_step(0) == 0);
REQUIRE(grid.GetElementPosX_step(4) == 0);
REQUIRE(grid.GetElementPosY_step(4) == 4);
// Second column (bottom to top)
REQUIRE(grid.GetElementPosX_step(5) == 1);
REQUIRE(grid.GetElementPosY_step(5) == 4);
REQUIRE(grid.GetElementPosX_step(9) == 1);
REQUIRE(grid.GetElementPosY_step(9) == 0);
// Third column (top to bottom)
REQUIRE(grid.GetElementPosX_step(10) == 2);
REQUIRE(grid.GetElementPosY_step(10) == 0);
REQUIRE(grid.GetElementPosX_step(14) == 2);
REQUIRE(grid.GetElementPosY_step(14) == 4);
}
}
TEST_CASE("GridScanSettings negative fast step", "[GridScanSettings]") {
// Create a grid with negative step sizes
GridScanSettings grid(5, -1.5f, 2.5f, false, false);
grid.ImageNum(35);
SECTION("Element positions") {
// First column
REQUIRE(grid.GetElementPosX_step(0) == 4);
REQUIRE(grid.GetElementPosY_step(0) == 0);
REQUIRE(grid.GetElementPosX_step(4) == 0);
REQUIRE(grid.GetElementPosY_step(4) == 0);
// Second column
REQUIRE(grid.GetElementPosX_step(5) == 4);
REQUIRE(grid.GetElementPosY_step(5) == 1);
REQUIRE(grid.GetElementPosX_step(9) == 0);
REQUIRE(grid.GetElementPosY_step(9) == 1);
}
SECTION("Grid sizes with negative steps") {
// Grid sizes should use absolute values
REQUIRE(grid.GetGridSizeX_um() == Catch::Approx(5 * 1.5f));
REQUIRE(grid.GetGridSizeY_um() == Catch::Approx(7 * 2.5f));
}
SECTION("Element positions with negative steps") {
REQUIRE(grid.GetElementPosX_um(0) == Catch::Approx(4 * 1.5f));
REQUIRE(grid.GetElementPosX_um(4) == Catch::Approx(0));
REQUIRE(grid.GetElementPosY_um(5) == Catch::Approx(2.5f));
}
SECTION("Grid steps maintain sign") {
REQUIRE(grid.GetGridStepX_um() == Catch::Approx(-1.5f));
REQUIRE(grid.GetGridStepY_um() == Catch::Approx(2.5f));
}
}
TEST_CASE("GridScanSettings negative slow step", "[GridScanSettings]") {
// Create a grid with negative step sizes
GridScanSettings grid(5, 1.5f, -2.5f, false, false);
grid.ImageNum(35);
SECTION("Element positions") {
// First column
REQUIRE(grid.GetElementPosX_step(0) == 0);
REQUIRE(grid.GetElementPosY_step(0) == 6);
REQUIRE(grid.GetElementPosX_step(4) == 4);
REQUIRE(grid.GetElementPosY_step(4) == 6);
// Second column
REQUIRE(grid.GetElementPosX_step(5) == 0);
REQUIRE(grid.GetElementPosY_step(5) == 5);
REQUIRE(grid.GetElementPosX_step(9) == 4);
REQUIRE(grid.GetElementPosY_step(9) == 5);
}
SECTION("Grid sizes with negative steps") {
// Grid sizes should use absolute values
REQUIRE(grid.GetGridSizeX_um() == Catch::Approx(5 * 1.5f));
REQUIRE(grid.GetGridSizeY_um() == Catch::Approx(7 * 2.5f));
}
SECTION("Element positions with negative steps") {
// Positions should maintain sign
REQUIRE(grid.GetElementPosX_um(4) == Catch::Approx(4 * 1.5f));
REQUIRE(grid.GetElementPosY_um(5) == Catch::Approx(5 * 2.5f));
}
SECTION("Grid steps maintain sign") {
REQUIRE(grid.GetGridStepX_um() == Catch::Approx(1.5f));
REQUIRE(grid.GetGridStepY_um() == Catch::Approx(-2.5f));
}
}
TEST_CASE("GridScanSettings::Rearrange functionality tests", "[gridscan][rearrange]") {
SECTION("Basic rearrangement in standard grid configuration") {
// Create a basic 3x3 grid with standard parameters
GridScanSettings grid(3, 1.0f, 1.0f, false, false);
grid.ImageNum(9); // 3x3 grid
// Create test input data [0,1,2,3,4,5,6,7,8]
std::vector<float> input(9);
for (int i = 0; i < 9; i++) {
input[i] = static_cast<float>(i);
}
// Rearrange the data
std::vector<float> output = grid.Rearrange(input);
// In standard mode (no snake, no vertical), data should be mapped correctly
// Each row in source (0,1,2), (3,4,5), (6,7,8)
// should appear in the same positions in the output
REQUIRE(output.size() == 9);
for (int i = 0; i < 9; i++) {
REQUIRE(output[i] == Catch::Approx(input[i]));
}
}
SECTION("Snake raster scan rearrangement") {
// Create a grid with snake pattern (alternating row directions)
GridScanSettings grid(3, 1.0f, 1.0f, true, false);
grid.ImageNum(9); // 3x3 grid
// Create test input data [0,1,2,3,4,5,6,7,8]
std::vector<float> input(9);
for (int i = 0; i < 9; i++) {
input[i] = static_cast<float>(i);
}
// Rearrange the data
std::vector<float> output = grid.Rearrange(input);
// In snake mode, odd rows should be reversed
// Input: [0,1,2,3,4,5,6,7,8]
// Expected in rows: [0,1,2], [5,4,3], [6,7,8]
// As flattened array: [0,1,2,5,4,3,6,7,8]
REQUIRE(output.size() == 9);
std::vector<float> expected = {0, 1, 2, 5, 4, 3, 6, 7, 8};
for (size_t i = 0; i < expected.size(); i++) {
REQUIRE(output[i] == Catch::Approx(expected[i]));
}
}
SECTION("Vertical scan rearrangement") {
// Test vertical scan mode
GridScanSettings grid(3, 1.0f, 1.0f, false, true);
grid.ImageNum(9); // 3x3 grid
// Create test input data [0,1,2,3,4,5,6,7,8]
std::vector<float> input(9);
for (int i = 0; i < 9; i++) {
input[i] = static_cast<float>(i);
}
// Rearrange the data
std::vector<float> output = grid.Rearrange(input);
// In vertical mode, scan goes down columns first
// Input: [0,1,2,3,4,5,6,7,8]
// Expected as columns: [0,3,6], [1,4,7], [2,5,8]
// But indexed by row-major order, so positions transform
REQUIRE(output.size() == 9);
// Verify transformations by checking specific indices
// The exact values depend on how vertical scan is implemented
// These assertions may need adjustment based on implementation
REQUIRE(output[0] == Catch::Approx(input[0]));
REQUIRE(output[3] == Catch::Approx(input[1]));
REQUIRE(output[6] == Catch::Approx(input[2]));
REQUIRE(output[1] == Catch::Approx(input[3]));
}
SECTION("Mixed configuration - snake and vertical scan") {
// Test combination of snake and vertical
GridScanSettings grid(3, 1.0f, 1.0f, true, true);
grid.ImageNum(9); // 3x3 grid
// Create test input data [0,1,2,3,4,5,6,7,8]
std::vector<float> input(9);
for (int i = 0; i < 9; i++) {
input[i] = static_cast<float>(i);
}
// Rearrange the data
std::vector<float> output = grid.Rearrange(input);
// 0 5 6
// 1 4 7
// 2 3 8
std::vector<float> expected = {0, 5, 6, 1, 4, 7, 2, 3, 8};
// Complex case with both features enabled
REQUIRE(output.size() == 9);
// Specific values would depend on implementation but ensure we get a valid output
for (size_t i = 0; i < expected.size(); i++) {
CHECK(output[i] == Catch::Approx(expected[i]));
}
}
SECTION("Negative grid steps") {
// Test with negative grid steps
GridScanSettings grid(3, -1.0f, -1.0f, false, false);
grid.ImageNum(9); // 3x3 grid
// Create test input data [0,1,2,3,4,5,6,7,8]
std::vector<float> input(9);
for (int i = 0; i < 9; i++) {
input[i] = static_cast<float>(i);
}
// Rearrange the data
std::vector<float> output = grid.Rearrange(input);
// Both fast and slow directions are negative, so mapping inverts both ways
// Expected: [8,7,6,5,4,3,2,1,0]
REQUIRE(output.size() == 9);
for (int i = 0; i < 9; i++) {
REQUIRE(output[i] == Catch::Approx(input[8-i]));
}
}
SECTION("Input smaller than grid") {
// Test case where input is smaller than grid
GridScanSettings grid(3, 1.0f, 1.0f, false, false);
grid.ImageNum(9); // 3x3 grid
// Create test input data [0,1,2,3,4] (shorter than grid)
std::vector<float> input = {0, 1, 2, 3, 4};
// Rearrange the data
std::vector<float> output = grid.Rearrange(input, 172.0f);
// Output should be full size with NANs for missing data
REQUIRE(output.size() == 9);
for (int i = 0; i < 5; i++) {
REQUIRE(output[i] == Catch::Approx(input[i]));
}
for (int i = 5; i < 9; i++) {
REQUIRE(output[i] == 172.0f);
}
}
SECTION("Input larger than grid") {
// Test case where input is larger than grid
GridScanSettings grid(2, 1.0f, 1.0f, false, false);
grid.ImageNum(4); // 2x2 grid
// Create test input with more elements than grid
std::vector<float> input = {0, 1, 2, 3, 4, 5};
// Rearrange the data
std::vector<float> output = grid.Rearrange(input);
// Output should match grid size and ignore extra input
REQUIRE(output.size() == 4);
for (int i = 0; i < 4; i++) {
REQUIRE(output[i] == Catch::Approx(input[i]));
}
}
}
TEST_CASE("GridScanSettings GetXContainer_m", "[GridScanSettings][container]") {
// Standard horizontal scan, step +2um X, +3um Y, 4 fast, 3 slow (12 total)
GridScanSettings grid_horiz(4, 2.0f, 3.0f, false, false);
grid_horiz.ImageNum(12); // 3 rows of 4
auto x_m = grid_horiz.GetXContainer_m(12);
REQUIRE(x_m.size() == 12);
// 0..3 should be X=0,2,4,6 um, Y=0
REQUIRE(x_m[0] == Catch::Approx(0.0));
REQUIRE(x_m[1] == Catch::Approx(2.0e-6));
REQUIRE(x_m[2] == Catch::Approx(4.0e-6));
REQUIRE(x_m[3] == Catch::Approx(6.0e-6));
// 4..7 should be X=0,2,4,6 um, Y=3 um
REQUIRE(x_m[4] == Catch::Approx(0.0));
REQUIRE(x_m[7] == Catch::Approx(6.0e-6));
// Last row
REQUIRE(x_m[8] == Catch::Approx(0.0));
REQUIRE(x_m[11] == Catch::Approx(6.0e-6));
// Snake raster test (horizontal, 3x2, fast +1um, slow +10um)
GridScanSettings grid_snake(3, 1.0f, 10.0f, true, false);
grid_snake.ImageNum(6); // 2 rows of 3
auto x_snake = grid_snake.GetXContainer_m(6);
// 0,1,2: left-to-right, 3,4,5: right-to-left
REQUIRE(x_snake[0] == Catch::Approx(0.0e-6));
REQUIRE(x_snake[1] == Catch::Approx(1.0e-6));
REQUIRE(x_snake[2] == Catch::Approx(2.0e-6));
REQUIRE(x_snake[3] == Catch::Approx(2.0e-6));
REQUIRE(x_snake[4] == Catch::Approx(1.0e-6));
REQUIRE(x_snake[5] == Catch::Approx(0.0e-6));
// Vertical scan, step X = 20um, fast Y = 5um, 2 fast, 3 slow (6 total)
GridScanSettings grid_vert(3, 20.0f, 5.0f, false, true);
grid_vert.ImageNum(6); // 2 columns, 3 rows (vertical fast)
auto x_vert = grid_vert.GetXContainer_m(6);
// First col: X=0, Y=0,5,10
REQUIRE(x_vert[0] == Catch::Approx(0.0));
REQUIRE(x_vert[1] == Catch::Approx(0.0));
REQUIRE(x_vert[2] == Catch::Approx(0.0));
// Second col: X=20um, Y=0,5,10
REQUIRE(x_vert[3] == Catch::Approx(20.0e-6));
REQUIRE(x_vert[5] == Catch::Approx(20.0e-6));
// Negative steps test
GridScanSettings grid_neg(3, -2.0f, -7.5f, false, false);
grid_neg.ImageNum(6); // 2 rows of 3
auto x_neg = grid_neg.GetXContainer_m(6);
REQUIRE(x_neg[0] == Catch::Approx(4.0e-6)); // 2*(3-1)
REQUIRE(x_neg[1] == Catch::Approx(2.0e-6));
REQUIRE(x_neg[2] == Catch::Approx(0.0));
}
TEST_CASE("GridScanSettings GetYContainer_m", "[GridScanSettings][container]") {
// Standard horizontal scan, step +2um X, +3um Y, 4 fast, 3 slow (12 total)
GridScanSettings grid_horiz(4, 2.0f, 3.0f, false, false);
grid_horiz.ImageNum(12); // 3 rows of 4
auto y_m = grid_horiz.GetYContainer_m(12);
REQUIRE(y_m.size() == 12);
for (int i = 0; i < 4; ++i)
REQUIRE(y_m[i] == Catch::Approx(0.0));
// 4..7 should be X=0,2,4,6 um, Y=3 um
REQUIRE(y_m[4] == Catch::Approx(3.0e-6));
REQUIRE(y_m[7] == Catch::Approx(3.0e-6));
// Last row
REQUIRE(y_m[8] == Catch::Approx(6.0e-6));
REQUIRE(y_m[11] == Catch::Approx(6.0e-6));
// Snake raster test (horizontal, 3x2, fast +1um, slow +10um)
GridScanSettings grid_snake(3, 1.0f, 10.0f, true, false);
grid_snake.ImageNum(6); // 2 rows of
auto y_snake = grid_snake.GetYContainer_m(6);
// 0,1,2: left-to-right, 3,4,5: right-to-left
// Both rows Y
for (int i = 0; i < 3; ++i)
REQUIRE(y_snake[i] == Catch::Approx(0.0));
for (int i = 3; i < 6; ++i)
REQUIRE(y_snake[i] == Catch::Approx(10.0e-6));
// Vertical scan, step X = 20um, fast Y = 5um, 2 fast, 3 slow (6 total)
GridScanSettings grid_vert(3, 20.0f, 5.0f, false, true);
grid_vert.ImageNum(6); // 2 columns, 3 rows (vertical fast)
auto y_vert = grid_vert.GetYContainer_m(6);
// First col: X=0, Y=0,5,10
REQUIRE(y_vert[0] == Catch::Approx(0.0));
REQUIRE(y_vert[1] == Catch::Approx(5.0e-6));
REQUIRE(y_vert[2] == Catch::Approx(10.0e-6));
// Second col: X=20um, Y=0,5,10
REQUIRE(y_vert[3] == Catch::Approx(0.0));
REQUIRE(y_vert[5] == Catch::Approx(10.0e-6));
// Negative steps test
GridScanSettings grid_neg(3, -2.0f, -7.5f, false, false);
grid_neg.ImageNum(6); // 2 rows of 3
auto y_neg = grid_neg.GetYContainer_m(6);
REQUIRE(y_neg[0] == Catch::Approx(7.5e-6));
REQUIRE(y_neg[1] == Catch::Approx(7.5e-6));
REQUIRE(y_neg[2] == Catch::Approx(7.5e-6));
REQUIRE(y_neg[3] == Catch::Approx(0.0));
REQUIRE(y_neg[4] == Catch::Approx(0.0));
REQUIRE(y_neg[5] == Catch::Approx(0.0));
}
namespace {
struct SnakeCase {
const char *name;
float fast_step_um;
float slow_step_um;
int64_t n_slow;
// Expected (fast, slow) element position, one entry per image, in acquisition order.
std::vector<std::pair<int64_t, int64_t>> expected;
};
}
// A snake reverses the stage on alternate rows in acquisition order, so an image's fast position
// cannot depend on how many rows the scan ended up having. Every case below is listed for an even
// and an odd row count for exactly that reason: the fast column of the two must agree image by
// image (the slow column does not, because a negative slow step measures from the last row).
TEST_CASE("GridScanSettings snake raster with negative steps", "[GridScanSettings]") {
constexpr int64_t n_fast = 3;
const std::vector<SnakeCase> cases = {
{"fast +, slow +, 2 rows", 1.5f, 2.5f, 2,
{{0, 0}, {1, 0}, {2, 0}, {2, 1}, {1, 1}, {0, 1}}},
{"fast +, slow +, 3 rows", 1.5f, 2.5f, 3,
{{0, 0}, {1, 0}, {2, 0}, {2, 1}, {1, 1}, {0, 1}, {0, 2}, {1, 2}, {2, 2}}},
{"fast +, slow -, 2 rows", 1.5f, -2.5f, 2,
{{0, 1}, {1, 1}, {2, 1}, {2, 0}, {1, 0}, {0, 0}}},
{"fast +, slow -, 3 rows", 1.5f, -2.5f, 3,
{{0, 2}, {1, 2}, {2, 2}, {2, 1}, {1, 1}, {0, 1}, {0, 0}, {1, 0}, {2, 0}}},
{"fast -, slow +, 2 rows", -1.5f, 2.5f, 2,
{{2, 0}, {1, 0}, {0, 0}, {0, 1}, {1, 1}, {2, 1}}},
{"fast -, slow +, 3 rows", -1.5f, 2.5f, 3,
{{2, 0}, {1, 0}, {0, 0}, {0, 1}, {1, 1}, {2, 1}, {2, 2}, {1, 2}, {0, 2}}},
{"fast -, slow -, 2 rows", -1.5f, -2.5f, 2,
{{2, 1}, {1, 1}, {0, 1}, {0, 0}, {1, 0}, {2, 0}}},
{"fast -, slow -, 3 rows", -1.5f, -2.5f, 3,
{{2, 2}, {1, 2}, {0, 2}, {0, 1}, {1, 1}, {2, 1}, {2, 0}, {1, 0}, {0, 0}}},
};
for (const auto &c : cases) {
for (bool vertical : {false, true}) {
INFO(c.name << (vertical ? ", vertical" : ", horizontal"));
GridScanSettings grid(n_fast,
vertical ? c.slow_step_um : c.fast_step_um,
vertical ? c.fast_step_um : c.slow_step_um,
true, vertical);
grid.ImageNum(n_fast * c.n_slow);
REQUIRE(grid.GetNSlow() == c.n_slow);
for (size_t i = 0; i < c.expected.size(); i++) {
const auto image = static_cast<int64_t>(i);
const int64_t fast = vertical ? grid.GetElementPosY_step(image)
: grid.GetElementPosX_step(image);
const int64_t slow = vertical ? grid.GetElementPosX_step(image)
: grid.GetElementPosY_step(image);
INFO("image " << i);
CHECK(fast == c.expected[i].first);
CHECK(slow == c.expected[i].second);
}
}
}
}
// The same configuration seen through the two consumers of the element positions: the NXmx
// transformation containers and the scan-result rearrangement.
TEST_CASE("GridScanSettings snake with negative slow step, even row count", "[GridScanSettings][container][rearrange]") {
GridScanSettings grid(3, 1.0f, -10.0f, true, false);
grid.ImageNum(6); // 2 rows of 3
const auto x_m = grid.GetXContainer_m(6);
const std::vector<double> x_expected = {0.0e-6, 1.0e-6, 2.0e-6, 2.0e-6, 1.0e-6, 0.0e-6};
for (size_t i = 0; i < x_expected.size(); i++) {
INFO("image " << i);
CHECK(x_m[i] == Catch::Approx(x_expected[i]));
}
const auto y_m = grid.GetYContainer_m(6);
const std::vector<double> y_expected = {10.0e-6, 10.0e-6, 10.0e-6, 0.0, 0.0, 0.0};
for (size_t i = 0; i < y_expected.size(); i++) {
INFO("image " << i);
CHECK(y_m[i] == Catch::Approx(y_expected[i]));
}
// Last row acquired is the top row of the grid, and it was acquired right-to-left.
const std::vector<float> input = {0, 1, 2, 3, 4, 5};
const std::vector<float> output = grid.Rearrange(input);
const std::vector<float> rearranged_expected = {5, 4, 3, 0, 1, 2};
REQUIRE(output.size() == rearranged_expected.size());
for (size_t i = 0; i < rearranged_expected.size(); i++) {
INFO("element " << i);
CHECK(output[i] == Catch::Approx(rearranged_expected[i]));
}
}