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:
@@ -7,9 +7,14 @@
|
||||
#include "../common/ScanResultGenerator.h"
|
||||
#include "../writer/FileWriter.h"
|
||||
#include "../reader/JFJochHDF5Reader.h"
|
||||
#include "../reader/JFJochCBFReader.h"
|
||||
#include "../reader/MiniCBF.h"
|
||||
#include "../compression/JFJochCompressor.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <future>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
TEST_CASE("HDF5DataType_Sign","[HDF5]") {
|
||||
HDF5DataType type_u8((uint8_t)0), type_fl(0.0f), type_i32((int32_t) 0), type_u32((uint32_t) 0);
|
||||
@@ -39,6 +44,76 @@ TEST_CASE("HDF5DataType_ElemType","[HDF5]") {
|
||||
CHECK(type_u32.IsInteger());
|
||||
}
|
||||
|
||||
TEST_CASE("JFJochReader_SpaceGroupSetting", "[HDF5][Full]") {
|
||||
// A space group is carried through the master file as its name, not its number, because a number
|
||||
// only ever names the reference setting. Both groups here are non-reference settings that a
|
||||
// number destroys: "P 1 1 2" comes back from 3 as "P 1 2 1", and "R 3:R" from 146 as "R 3:H"
|
||||
// (short_name() loses that one too - only xhm() is faithful).
|
||||
const auto setting = GENERATE(std::string("P 1 1 2"), std::string("R 3:R"));
|
||||
const gemmi::SpaceGroup *sg = gemmi::find_spacegroup_by_name(setting);
|
||||
REQUIRE(sg != nullptr);
|
||||
|
||||
// The number is not a carrier for it, which is the whole reason the name is written.
|
||||
DiffractionExperiment by_number(DetJF(1));
|
||||
by_number.SpaceGroupNumber(sg->number);
|
||||
CHECK(by_number.GetGemmiSpaceGroup()->xhm() != setting);
|
||||
|
||||
DiffractionExperiment x(DetJF(1));
|
||||
x.FilePrefix("test_sg_setting").ImagesPerTrigger(1).OverwriteExistingFiles(true);
|
||||
x.SetSpaceGroup(*sg);
|
||||
CHECK(x.GetGemmiSpaceGroup()->xhm() == setting);
|
||||
CHECK(x.GetSpaceGroupNumber() == sg->number);
|
||||
|
||||
RegisterHDF5Filter();
|
||||
{
|
||||
StartMessage start_message;
|
||||
x.FillMessage(start_message);
|
||||
|
||||
EndMessage end_message;
|
||||
end_message.max_image_number = 0;
|
||||
end_message.space_group_name = sg->xhm();
|
||||
std::unique_ptr<NXmx> master = std::make_unique<NXmx>(start_message);
|
||||
master->Finalize(end_message);
|
||||
}
|
||||
{
|
||||
JFJochHDF5Reader reader;
|
||||
REQUIRE_NOTHROW(reader.ReadFile("test_sg_setting_master.h5"));
|
||||
auto dataset = reader.GetDataset();
|
||||
REQUIRE(dataset->experiment.GetGemmiSpaceGroup().has_value());
|
||||
CHECK(dataset->experiment.GetGemmiSpaceGroup()->xhm() == setting);
|
||||
}
|
||||
remove("test_sg_setting_master.h5");
|
||||
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("JFJochReader_SpaceGroupNumberOnly", "[HDF5][Full]") {
|
||||
// A file written before the name was recorded carries only the number; it still reads back, as
|
||||
// the reference setting the number names.
|
||||
DiffractionExperiment x(DetJF(1));
|
||||
x.FilePrefix("test_sg_number").ImagesPerTrigger(1).OverwriteExistingFiles(true);
|
||||
x.SpaceGroupNumber(96);
|
||||
|
||||
RegisterHDF5Filter();
|
||||
{
|
||||
StartMessage start_message;
|
||||
x.FillMessage(start_message);
|
||||
|
||||
EndMessage end_message;
|
||||
end_message.max_image_number = 0;
|
||||
std::unique_ptr<NXmx> master = std::make_unique<NXmx>(start_message);
|
||||
master->Finalize(end_message);
|
||||
}
|
||||
{
|
||||
JFJochHDF5Reader reader;
|
||||
REQUIRE_NOTHROW(reader.ReadFile("test_sg_number_master.h5"));
|
||||
auto dataset = reader.GetDataset();
|
||||
REQUIRE(dataset->experiment.GetGemmiSpaceGroup().has_value());
|
||||
CHECK(dataset->experiment.GetGemmiSpaceGroup()->xhm() == "P 43 21 2");
|
||||
}
|
||||
remove("test_sg_number_master.h5");
|
||||
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("JFJochReader_MasterFile", "[HDF5][Full]") {
|
||||
DiffractionExperiment x(DetJF(1));
|
||||
|
||||
@@ -3290,6 +3365,24 @@ TEST_CASE("JFJochReader_AxisRecovery", "[HDF5][Full]") {
|
||||
cleanup("test_ax_grid");
|
||||
}
|
||||
|
||||
// A grid scan is taken at a stationary spindle, and the angle it stood at is what relates one
|
||||
// grid to another taken elsewhere on the circle. It is stated by sending the axis with step 0;
|
||||
// send nothing and the spindle is recorded at 0, which says only that nobody told us.
|
||||
SECTION("a grid scan at a stationary head position") {
|
||||
DiffractionExperiment x(DetJF(1));
|
||||
x.ImagesPerTrigger(6).GridScan(GridScanSettings(3, 10.0f, 20.0f, false, false).ImageNum(6))
|
||||
.Goniometer(GoniometerAxis("omega", 90.0f, 0.0f, Coord(-1,0,0), {}));
|
||||
const auto out = round_trip(x, "test_ax_gridstill");
|
||||
REQUIRE(out.GetGridScan().has_value());
|
||||
CHECK(out.GetGridScan()->GetNFast() == 3);
|
||||
REQUIRE(out.GetGoniometer().has_value());
|
||||
CHECK(out.GetGoniometer()->GetName() == "omega");
|
||||
CHECK(!out.GetGoniometer()->IsScanning());
|
||||
CHECK(out.GetGoniometer()->GetStart_deg() == Catch::Approx(90).margin(1e-3));
|
||||
CHECK(out.GetGoniometer()->GetAxis() == Coord(-1,0,0));
|
||||
cleanup("test_ax_gridstill");
|
||||
}
|
||||
|
||||
SECTION("a grid scan under a turning spindle") {
|
||||
DiffractionExperiment x(DetJF(1));
|
||||
x.ImagesPerTrigger(6).GridScan(GridScanSettings(3, 10.0f, 20.0f, false, false).ImageNum(6))
|
||||
@@ -3508,3 +3601,500 @@ TEST_CASE("JFJochReader_Goniometer_NamedPhiIsNotSmargon", "[HDF5][Full]") {
|
||||
remove("test_phispindle_out_master.h5");
|
||||
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
|
||||
}
|
||||
|
||||
// A valid NXmx master written outside the DECTRIS toolchain, in the shape a Diamond-written one
|
||||
// takes: lengths in millimetres, no detectorSpecific, the distance one level up in NXinstrument, a
|
||||
// pixel_mask that is an external link into a file not holding it, and per-file links naming a plain
|
||||
// /data rather than /entry/data/data. Every one of those was enough on its own to stop the file
|
||||
// opening, and the last one did it without an error - the run reported no images and succeeded.
|
||||
namespace {
|
||||
void WriteThirdPartyDataFile(const std::string &filename, const std::vector<uint16_t> &image,
|
||||
hsize_t nimages, hsize_t ny, hsize_t nx) {
|
||||
std::vector<uint16_t> block;
|
||||
for (hsize_t i = 0; i < nimages; i++)
|
||||
block.insert(block.end(), image.begin(), image.end());
|
||||
HDF5File file(filename);
|
||||
file.SaveVector("/data", block, {nimages, ny, nx});
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("JFJochReader_ThirdPartyNXmxMaster", "[HDF5][Full]") {
|
||||
const hsize_t nx = 8, ny = 6, per_file = 2;
|
||||
std::vector<uint16_t> image(nx * ny);
|
||||
for (size_t i = 0; i < image.size(); i++)
|
||||
image[i] = static_cast<uint16_t>(i * 3 + 1);
|
||||
|
||||
WriteThirdPartyDataFile("third_party_000001.h5", image, per_file, ny, nx);
|
||||
WriteThirdPartyDataFile("third_party_000002.h5", image, per_file, ny, nx);
|
||||
|
||||
{
|
||||
HDF5File master("third_party_master.h5");
|
||||
HDF5Group entry(master, "entry");
|
||||
entry.SaveScalar("definition", "NXmx");
|
||||
HDF5Group instrument(entry, "instrument");
|
||||
// The distance NXdetector does not carry, in millimetres
|
||||
instrument.SaveScalar("detector_distance", 287.5)->Units("mm");
|
||||
HDF5Group beam(instrument, "beam");
|
||||
beam.SaveScalar("incident_wavelength", 0.9794)->Units("angstrom");
|
||||
HDF5Group detector(instrument, "detector");
|
||||
detector.SaveScalar("description", "Eiger 16M");
|
||||
detector.SaveScalar("beam_center_x", 4.0)->Units("pixels");
|
||||
detector.SaveScalar("beam_center_y", 3.0)->Units("pixels");
|
||||
detector.SaveScalar("count_time", 0.01);
|
||||
detector.SaveScalar("saturation_value", static_cast<int64_t>(65535));
|
||||
detector.SaveScalar("x_pixel_size", 0.075)->Units("mm");
|
||||
detector.SaveScalar("y_pixel_size", 0.075)->Units("mm");
|
||||
detector.SaveScalar("sensor_thickness", 0.45)->Units("mm");
|
||||
// Links into a file that does not exist at all, so neither can be dereferenced
|
||||
detector.ExternalLink("third_party_no_such_meta.h5", "/mask", "pixel_mask");
|
||||
HDF5Group data(entry, "data");
|
||||
data.ExternalLink("third_party_000001.h5", "/data", "data_000001");
|
||||
data.ExternalLink("third_party_000002.h5", "/data", "data_000002");
|
||||
}
|
||||
|
||||
{
|
||||
JFJochHDF5Reader reader;
|
||||
REQUIRE_NOTHROW(reader.ReadFile("third_party_master.h5"));
|
||||
auto dataset = reader.GetDataset();
|
||||
// Images found through the link's own target path, and sized from the array itself
|
||||
CHECK(dataset->experiment.GetImageNum() == 2 * per_file);
|
||||
CHECK(dataset->experiment.GetXPixelsNum() == nx);
|
||||
CHECK(dataset->experiment.GetYPixelsNum() == ny);
|
||||
// Millimetres read as millimetres
|
||||
CHECK(dataset->experiment.GetDetectorDistance_mm() == Catch::Approx(287.5));
|
||||
CHECK(dataset->experiment.GetDetectorSetup().GetPixelSize_mm() == Catch::Approx(0.075));
|
||||
CHECK(dataset->experiment.GetDetectorSetup().GetSensorThickness_um() == Catch::Approx(450.0));
|
||||
// Both mask links dangle; the reader must fall back to an empty mask, not throw
|
||||
REQUIRE(dataset->pixel_mask);
|
||||
|
||||
std::shared_ptr<JFJochReaderRawImage> reader_image;
|
||||
for (int i = 0; i < 2 * static_cast<int>(per_file); i++) {
|
||||
REQUIRE_NOTHROW(reader_image = reader.GetRawImage(i));
|
||||
CHECK(reader_image->image.GetWidth() == nx);
|
||||
CHECK(reader_image->image.GetHeight() == ny);
|
||||
}
|
||||
}
|
||||
|
||||
// A master that names data files nothing can be read from is an error, not an empty data set
|
||||
remove("third_party_000001.h5");
|
||||
remove("third_party_000002.h5");
|
||||
{
|
||||
JFJochHDF5Reader reader;
|
||||
REQUIRE_THROWS(reader.ReadFile("third_party_master.h5"));
|
||||
}
|
||||
|
||||
remove("third_party_master.h5");
|
||||
// No leftover HDF5 objects
|
||||
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
|
||||
}
|
||||
|
||||
// A detector swung out on a 2theta arm. NXmx has no field for it: the swing is one rotation in the
|
||||
// depends_on chain the detector's position is stated as, and "two_theta" is only one beamline's name
|
||||
// for that dataset. So the chain is what the reader follows, and the chain here carries two rotations
|
||||
// about different axes, outboard of the translation that sets the distance - a file that stated only
|
||||
// the innermost one, or composed them the other way round, gives a different plane.
|
||||
//
|
||||
// Both axes are stated in McStas, which is the internal frame turned half a turn about z: a reader
|
||||
// that takes the vector as it stands swings the detector the wrong way, which is twice the error of
|
||||
// not reading it at all.
|
||||
TEST_CASE("JFJochReader_DetectorTwoThetaArm", "[HDF5][Full]") {
|
||||
const hsize_t nx = 8, ny = 6;
|
||||
const double two_theta_deg = 20.0, tilt_deg = 7.0;
|
||||
std::vector<uint16_t> image(nx * ny, 5);
|
||||
WriteThirdPartyDataFile("two_theta_000001.h5", image, 2, ny, nx);
|
||||
|
||||
{
|
||||
HDF5File master("two_theta_master.h5");
|
||||
HDF5Group entry(master, "entry");
|
||||
entry.SaveScalar("definition", "NXmx");
|
||||
HDF5Group instrument(entry, "instrument");
|
||||
HDF5Group beam(instrument, "beam");
|
||||
beam.SaveScalar("incident_wavelength", 0.6889)->Units("angstrom");
|
||||
HDF5Group transformations(instrument, "transformations");
|
||||
// Outermost first in the file, innermost first along the chain: det_z -> two_theta -> tilt
|
||||
transformations.SaveVector("tilt", std::vector<double>{tilt_deg})
|
||||
->Transformation("deg", ".", "detector", "", "rotation", {0, 1, 0});
|
||||
transformations.SaveVector("two_theta", std::vector<double>{two_theta_deg})
|
||||
->Transformation("deg", "/entry/instrument/transformations/tilt",
|
||||
"detector", "", "rotation", {-1, 0, 0});
|
||||
transformations.SaveVector("det_z", std::vector<double>{160.0})
|
||||
->Transformation("mm", "/entry/instrument/transformations/two_theta",
|
||||
"detector", "", "translation", {0, 0, 1});
|
||||
HDF5Group detector(instrument, "detector");
|
||||
detector.SaveScalar("depends_on", "/entry/instrument/transformations/det_z");
|
||||
detector.SaveScalar("description", "PILATUS 2M");
|
||||
detector.SaveScalar("beam_center_x", 4.0)->Units("pixels");
|
||||
detector.SaveScalar("beam_center_y", 3.0)->Units("pixels");
|
||||
detector.SaveScalar("distance", 0.160)->Units("m");
|
||||
detector.SaveScalar("x_pixel_size", 0.172)->Units("mm");
|
||||
detector.SaveScalar("y_pixel_size", 0.172)->Units("mm");
|
||||
detector.SaveScalar("sensor_thickness", 0.32)->Units("mm");
|
||||
detector.SaveScalar("count_time", 0.2);
|
||||
detector.SaveScalar("saturation_value", static_cast<int64_t>(65535));
|
||||
HDF5Group data(entry, "data");
|
||||
data.ExternalLink("two_theta_000001.h5", "/data", "data_000001");
|
||||
}
|
||||
|
||||
DiffractionGeometry geom;
|
||||
{
|
||||
JFJochHDF5Reader reader;
|
||||
REQUIRE_NOTHROW(reader.ReadFile("two_theta_master.h5"));
|
||||
geom = reader.GetDataset()->experiment.GetDiffractionGeometry();
|
||||
}
|
||||
|
||||
// The chain as it stands in the internal frame: McStas (-1,0,0) is internal (1,0,0) and McStas
|
||||
// (0,1,0) is internal (0,-1,0), and the outer rotation multiplies on the left.
|
||||
const auto to_rad = [](double deg) { return static_cast<float>(deg * PI / 180.0); };
|
||||
const RotMatrix expected = RotMatrix(to_rad(tilt_deg), {0, -1, 0})
|
||||
* RotMatrix(to_rad(two_theta_deg), {1, 0, 0});
|
||||
for (int64_t column = 0; column < 3; column++)
|
||||
CHECK((geom.GetDetectorMatrix().Column(column) - expected.Column(column)).Length() < 1e-5f);
|
||||
|
||||
// Distance and beam centre are the ones the file states: the arm turns the detector about the
|
||||
// sample and moves neither.
|
||||
CHECK(geom.GetDetectorDistance_mm() == Catch::Approx(160.0));
|
||||
CHECK(geom.GetBeamX_pxl() == Catch::Approx(4.0));
|
||||
CHECK(geom.GetBeamY_pxl() == Catch::Approx(3.0));
|
||||
// And the beam centre pixel is now that far from the beam - the whole point of a 2theta arm.
|
||||
CHECK(geom.TwoTheta_rad(4.0f, 3.0f) * 180.0f / PI
|
||||
== Catch::Approx(angle_deg(expected * Coord(0, 0, 1), Coord(0, 0, 1))));
|
||||
CHECK(geom.TwoTheta_rad(4.0f, 3.0f) * 180.0f / PI > two_theta_deg);
|
||||
|
||||
remove("two_theta_000001.h5");
|
||||
remove("two_theta_master.h5");
|
||||
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
|
||||
}
|
||||
|
||||
// The same file with the arm parked at zero: the chain is there, nothing in it turns, and the
|
||||
// geometry must be exactly the square-on one. This is nearly every file, so it has to cost nothing.
|
||||
TEST_CASE("JFJochReader_DetectorTwoThetaZeroIsSquareOn", "[HDF5][Full]") {
|
||||
const hsize_t nx = 8, ny = 6;
|
||||
std::vector<uint16_t> image(nx * ny, 5);
|
||||
WriteThirdPartyDataFile("two_theta_zero_000001.h5", image, 2, ny, nx);
|
||||
|
||||
{
|
||||
HDF5File master("two_theta_zero_master.h5");
|
||||
HDF5Group entry(master, "entry");
|
||||
entry.SaveScalar("definition", "NXmx");
|
||||
HDF5Group instrument(entry, "instrument");
|
||||
HDF5Group beam(instrument, "beam");
|
||||
beam.SaveScalar("incident_wavelength", 0.6889)->Units("angstrom");
|
||||
HDF5Group transformations(instrument, "transformations");
|
||||
transformations.SaveVector("two_theta", std::vector<double>{0.0})
|
||||
->Transformation("deg", ".", "detector", "", "rotation", {-1, 0, 0});
|
||||
transformations.SaveVector("det_z", std::vector<double>{160.0})
|
||||
->Transformation("mm", "/entry/instrument/transformations/two_theta",
|
||||
"detector", "", "translation", {0, 0, 1});
|
||||
HDF5Group detector(instrument, "detector");
|
||||
detector.SaveScalar("depends_on", "/entry/instrument/transformations/det_z");
|
||||
detector.SaveScalar("description", "PILATUS 2M");
|
||||
detector.SaveScalar("beam_center_x", 4.0)->Units("pixels");
|
||||
detector.SaveScalar("beam_center_y", 3.0)->Units("pixels");
|
||||
detector.SaveScalar("distance", 0.160)->Units("m");
|
||||
detector.SaveScalar("x_pixel_size", 0.172)->Units("mm");
|
||||
detector.SaveScalar("y_pixel_size", 0.172)->Units("mm");
|
||||
detector.SaveScalar("sensor_thickness", 0.32)->Units("mm");
|
||||
detector.SaveScalar("count_time", 0.2);
|
||||
detector.SaveScalar("saturation_value", static_cast<int64_t>(65535));
|
||||
HDF5Group data(entry, "data");
|
||||
data.ExternalLink("two_theta_zero_000001.h5", "/data", "data_000001");
|
||||
}
|
||||
|
||||
DiffractionGeometry geom;
|
||||
{
|
||||
JFJochHDF5Reader reader;
|
||||
REQUIRE_NOTHROW(reader.ReadFile("two_theta_zero_master.h5"));
|
||||
geom = reader.GetDataset()->experiment.GetDiffractionGeometry();
|
||||
}
|
||||
|
||||
CHECK(geom.GetPoniRot1_rad() == 0.0f);
|
||||
CHECK(geom.GetPoniRot2_rad() == 0.0f);
|
||||
CHECK(geom.GetPoniRot3_rad() == 0.0f);
|
||||
CHECK(geom.TwoTheta_rad(4.0f, 3.0f) == 0.0f);
|
||||
|
||||
remove("two_theta_zero_000001.h5");
|
||||
remove("two_theta_zero_master.h5");
|
||||
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
|
||||
}
|
||||
|
||||
// A file this system wrote states its own PONI angles twice: as the three scalars the reader takes
|
||||
// them from, and as three rotations in the detector's depends_on chain. Following the chain must
|
||||
// therefore skip them - applied on top of the scalars they would tilt the detector twice, which is
|
||||
// how a correct 2theta reader breaks every tilted file this system has ever written. A test that only
|
||||
// wrote an untilted detector could not see it.
|
||||
TEST_CASE("JFJochReader_DetectorChainDoesNotDoubleTheTilt", "[HDF5][Full]") {
|
||||
const float rot1 = 0.031f, rot2 = -0.047f, rot3 = 0.019f;
|
||||
|
||||
DiffractionExperiment x(DetJF(1));
|
||||
x.ImagesPerTrigger(2).OverwriteExistingFiles(true).FilePrefix("test_ponichain");
|
||||
x.BeamX_pxl(100).BeamY_pxl(200).DetectorDistance_mm(150)
|
||||
.IncidentEnergy_keV(WVL_1A_IN_KEV).PixelSigned(false).BitDepthImage(16)
|
||||
.FrameTime(std::chrono::microseconds(500), std::chrono::microseconds(10));
|
||||
x.PoniRot1_rad(rot1).PoniRot2_rad(rot2).PoniRot3_rad(rot3);
|
||||
|
||||
RegisterHDF5Filter();
|
||||
std::vector<uint16_t> image(x.GetPixelsNum(), 0);
|
||||
|
||||
StartMessage start_message;
|
||||
x.FillMessage(start_message);
|
||||
FileWriter file_set(start_message);
|
||||
DataMessage message{};
|
||||
for (int i = 0; i < x.GetImageNum(); i++) {
|
||||
message.image = CompressedImage(image, x.GetXPixelsNum(), x.GetYPixelsNum());
|
||||
message.number = i;
|
||||
REQUIRE_NOTHROW(file_set.WriteHDF5(message));
|
||||
}
|
||||
EndMessage end_message;
|
||||
end_message.max_image_number = x.GetImageNum();
|
||||
file_set.WriteHDF5(end_message);
|
||||
file_set.Finalize();
|
||||
|
||||
DiffractionGeometry geom;
|
||||
{
|
||||
JFJochHDF5Reader reader;
|
||||
REQUIRE_NOTHROW(reader.ReadFile("test_ponichain_master.h5"));
|
||||
geom = reader.GetDataset()->experiment.GetDiffractionGeometry();
|
||||
}
|
||||
CHECK(geom.GetPoniRot1_rad() == Catch::Approx(rot1).margin(1e-6));
|
||||
CHECK(geom.GetPoniRot2_rad() == Catch::Approx(rot2).margin(1e-6));
|
||||
CHECK(geom.GetPoniRot3_rad() == Catch::Approx(rot3).margin(1e-6));
|
||||
for (int64_t column = 0; column < 3; column++)
|
||||
CHECK((geom.GetDetectorMatrix().Column(column)
|
||||
- PoniRotMatrix(rot1, rot2, rot3).Column(column)).Length() < 1e-5f);
|
||||
|
||||
remove("test_ponichain_master.h5");
|
||||
remove("test_ponichain_data_000001.h5");
|
||||
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
|
||||
}
|
||||
|
||||
// A miniCBF header states more about the instrument than the "# " lines do: the CBF template block
|
||||
// some beamlines write carries a full imgCIF axis table, saying which laboratory direction the image's
|
||||
// columns and rows run along and which the spindle turns about. The reader assumed all three, and two
|
||||
// instruments in the corpus are not what it assumed - one stores its image a quarter turn round, the
|
||||
// other turns about the VERTICAL. Either way the spindle ends up 90 degrees from the image, which is
|
||||
// not a sign and so is beyond the axis-sign rescue; both indexed nothing at all.
|
||||
namespace {
|
||||
// Two frames of a sweep whose pixels are all zero. Every delta of a zero image is zero, so the
|
||||
// byte-offset stream is one 0x00 per pixel - which is a valid stream and enough to open a sweep.
|
||||
void WriteMiniCBFSweep(const std::string &prefix, const std::string &header_body,
|
||||
int64_t nx, int64_t ny) {
|
||||
for (int frame = 1; frame <= 2; frame++) {
|
||||
std::ostringstream head;
|
||||
head << "###CBF: VERSION 1.5\n_array_data.header_convention \"PILATUS_1.2\"\n"
|
||||
<< "_array_data.header_contents\n"
|
||||
<< "# Detector: PILATUS3 6M, S/N 60-0119\n"
|
||||
<< "# Pixel_size 172e-6 m x 172e-6 m\n"
|
||||
<< "# Silicon sensor, thickness 0.000450 m\n"
|
||||
<< "# Exposure_time 0.1 s\n# Exposure_period 0.1 s\n# Count_cutoff 768595 counts\n"
|
||||
<< "# Wavelength 0.96864 A\n# Detector_distance 0.33161 m\n"
|
||||
<< "# Beam_xy (12.00, 8.00) pixels\n"
|
||||
<< "# Start_angle " << (frame - 1) * 0.1 << " deg.\n# Angle_increment 0.1000 deg.\n"
|
||||
<< "# Omega " << (frame - 1) * 0.1 << " deg.\n# Omega_increment 0.1000 deg.\n"
|
||||
<< "# Phi 0.0000 deg.\n# Phi_increment 0.0000 deg.\n"
|
||||
<< "# Chi 0.0000 deg.\n# Chi_increment 0.0000 deg.\n"
|
||||
<< header_body
|
||||
<< "_array_data.data\n--CIF-BINARY-FORMAT-SECTION--\n"
|
||||
<< "Content-Type: application/octet-stream;\n"
|
||||
<< " conversions=\"x-CBF_BYTE_OFFSET\"\n"
|
||||
<< "Content-Transfer-Encoding: BINARY\n"
|
||||
<< "X-Binary-Size: " << nx * ny << "\n"
|
||||
<< "X-Binary-Element-Type: \"signed 32-bit integer\"\n"
|
||||
<< "X-Binary-Number-of-Elements: " << nx * ny << "\n"
|
||||
<< "X-Binary-Size-Fastest-Dimension: " << nx << "\n"
|
||||
<< "X-Binary-Size-Second-Dimension: " << ny << "\n\n";
|
||||
|
||||
std::ostringstream name;
|
||||
name << prefix << "_" << std::setfill('0') << std::setw(4) << frame << ".cbf";
|
||||
std::ofstream f(name.str(), std::ios::binary);
|
||||
const std::string text = head.str();
|
||||
f.write(text.data(), static_cast<std::streamsize>(text.size()));
|
||||
f.write(reinterpret_cast<const char *>(minicbf::BINARY_SEPARATOR),
|
||||
sizeof(minicbf::BINARY_SEPARATOR));
|
||||
const std::vector<char> zeros(static_cast<size_t>(nx * ny), 0);
|
||||
f.write(zeros.data(), static_cast<std::streamsize>(zeros.size()));
|
||||
}
|
||||
}
|
||||
|
||||
void RemoveMiniCBFSweep(const std::string &prefix) {
|
||||
for (int frame = 1; frame <= 2; frame++) {
|
||||
std::ostringstream name;
|
||||
name << prefix << "_" << std::setfill('0') << std::setw(4) << frame << ".cbf";
|
||||
remove(name.str().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// The axis table in the form these headers write it, several tags to a line.
|
||||
std::string AxisTable(const std::string &rows, int64_t nx, int64_t ny) {
|
||||
return "loop_\n_axis.id\n_axis.type\n_axis.equipment\n_axis.depends_on\n"
|
||||
"_axis.vector[1] _axis.vector[2] _axis.vector[3]\n"
|
||||
"_axis.offset[1] _axis.offset[2] _axis.offset[3]\n"
|
||||
+ rows +
|
||||
"loop_\n_array_structure_list.array_id\n_array_structure_list.index\n"
|
||||
"_array_structure_list.dimension\n_array_structure_list.precedence\n"
|
||||
"_array_structure_list.direction\n_array_structure_list.axis_set_id\n"
|
||||
"ARRAY1 1 " + std::to_string(nx) + " 1 increasing ELEMENT_X\n"
|
||||
"ARRAY1 2 " + std::to_string(ny) + " 2 increasing ELEMENT_Y\n"
|
||||
"loop_\n_array_structure_list_axis.axis_set_id\n_array_structure_list_axis.axis_id\n"
|
||||
"_array_structure_list_axis.displacement\n_array_structure_list_axis.displacement_increment\n"
|
||||
"ELEMENT_X ELEMENT_X 0.0 0.1720\nELEMENT_Y ELEMENT_Y 0.0 0.1720\n";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("JFJochCBFReader_AxisTableStatesTheMounting", "[HDF5][Full]") {
|
||||
const int64_t nx = 24, ny = 16;
|
||||
|
||||
// A header that states nothing: the assumption, and the behaviour of nearly every file there is.
|
||||
SECTION("no table, no hint - the assumption stands") {
|
||||
WriteMiniCBFSweep("cbfaxis_plain", "# Detector_2theta 0.0000 deg.\n# Oscillation_axis OMEGA\n",
|
||||
nx, ny);
|
||||
JFJochCBFReader reader;
|
||||
REQUIRE_NOTHROW(reader.ReadFiles("cbfaxis_plain_0001.cbf"));
|
||||
const auto x = reader.GetDataset()->experiment;
|
||||
REQUIRE(x.GetGoniometer().has_value());
|
||||
CHECK((x.GetGoniometer()->GetAxis() - Coord(-1, 0, 0)).Length() < 1e-6f);
|
||||
CHECK(x.GetDetectorSetup().GetImageOrientation().IsIdentity());
|
||||
CHECK(x.GetDiffractionGeometry().GetPoniRot2_rad() == 0.0f);
|
||||
reader.Close();
|
||||
RemoveMiniCBFSweep("cbfaxis_plain");
|
||||
}
|
||||
|
||||
// A spindle that turns about the VERTICAL, with the image mounted the usual way round. imgCIF Y is
|
||||
// up and the internal frame's y is down, so the stated (0,1,0) is internal (0,-1,0) - and NOT the
|
||||
// (-1,0,0) that was assumed, which is 90 degrees away and indexes nothing.
|
||||
SECTION("vertical spindle, standard image") {
|
||||
WriteMiniCBFSweep("cbfaxis_vert",
|
||||
"# Detector_2theta 0.0000 deg.\n# Oscillation_axis X.CW +SLOW\n"
|
||||
+ AxisTable("GON_OMEGA rotation goniometer . 0 1 0 . . .\n"
|
||||
"DET_Z translation detector . 0 0 -1 0 0 0\n"
|
||||
"ELEMENT_X translation detector DET_Z 1 0 0 -1 1 0\n"
|
||||
"ELEMENT_Y translation detector ELEMENT_X 0 -1 0 0 0 0\n", nx, ny),
|
||||
nx, ny);
|
||||
JFJochCBFReader reader;
|
||||
REQUIRE_NOTHROW(reader.ReadFiles("cbfaxis_vert_0001.cbf"));
|
||||
const auto x = reader.GetDataset()->experiment;
|
||||
REQUIRE(x.GetGoniometer().has_value());
|
||||
CHECK((x.GetGoniometer()->GetAxis() - Coord(0, -1, 0)).Length() < 1e-6f);
|
||||
// The image itself is standard, so nothing about it is turned - the axis was the whole error.
|
||||
CHECK(x.GetDetectorSetup().GetImageOrientation().IsIdentity());
|
||||
reader.Close();
|
||||
RemoveMiniCBFSweep("cbfaxis_vert");
|
||||
}
|
||||
|
||||
// The same vertical spindle, stated only by the "+SLOW" token, which is all a header with no axis
|
||||
// table says. Two datasets from that instrument are in this state.
|
||||
SECTION("vertical spindle from the +SLOW token alone") {
|
||||
WriteMiniCBFSweep("cbfaxis_slow", "# Detector_2theta 0.0000 deg.\n# Oscillation_axis X.CW +SLOW\n",
|
||||
nx, ny);
|
||||
JFJochCBFReader reader;
|
||||
REQUIRE_NOTHROW(reader.ReadFiles("cbfaxis_slow_0001.cbf"));
|
||||
const auto x = reader.GetDataset()->experiment;
|
||||
REQUIRE(x.GetGoniometer().has_value());
|
||||
CHECK((x.GetGoniometer()->GetAxis() - Coord(0, -1, 0)).Length() < 1e-6f);
|
||||
reader.Close();
|
||||
RemoveMiniCBFSweep("cbfaxis_slow");
|
||||
}
|
||||
|
||||
// An image stored a quarter turn round, on a detector swung out to 30 degrees. The two are read
|
||||
// together or not at all: the arm turns about a laboratory axis, and which way that runs across
|
||||
// the stored image is exactly what the mounting says.
|
||||
SECTION("quarter-turned image on a swung arm") {
|
||||
WriteMiniCBFSweep("cbfaxis_turn",
|
||||
"# Detector_2theta 30.0000 deg.\n# Oscillation_axis OMEGA\n"
|
||||
+ AxisTable("GON_OMEGA rotation goniometer . 1 0 0 . . .\n"
|
||||
"DET_2THETA rotation detector . 1 0 0 . . .\n"
|
||||
"DET_Z translation detector DET_2THETA 0 0 -1 0 0 0\n"
|
||||
"ELEMENT_X translation detector DET_Z 0 1 0 -1 1 0\n"
|
||||
"ELEMENT_Y translation detector ELEMENT_X 1 0 0 0 0 0\n", nx, ny),
|
||||
nx, ny);
|
||||
JFJochCBFReader reader;
|
||||
REQUIRE_NOTHROW(reader.ReadFiles("cbfaxis_turn_0001.cbf"));
|
||||
const auto x = reader.GetDataset()->experiment;
|
||||
REQUIRE(x.GetGoniometer().has_value());
|
||||
CHECK((x.GetGoniometer()->GetAxis() - Coord(1, 0, 0)).Length() < 1e-6f);
|
||||
// fast = imgCIF (0,1,0) = internal (0,-1,0), slow = imgCIF (1,0,0) = internal (1,0,0)
|
||||
CHECK(x.GetDetectorSetup().GetImageOrientation() == DetectorOrientation(false, 3));
|
||||
// and the arm turns about its own stated axis, internal +x, by the stated 30 degrees
|
||||
const auto geom = x.GetDiffractionGeometry();
|
||||
const RotMatrix expected = RotMatrix(static_cast<float>(30.0 * PI / 180.0), {1, 0, 0})
|
||||
* DetectorOrientation(false, 3).Matrix();
|
||||
for (int64_t column = 0; column < 3; column++)
|
||||
CHECK((geom.GetDetectorMatrix().Column(column) - expected.Column(column)).Length() < 1e-5f);
|
||||
reader.Close();
|
||||
RemoveMiniCBFSweep("cbfaxis_turn");
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// A byte-offset CBF whose header lines the caller chooses, so a header that is MISSING something
|
||||
// can be built. WriteMiniCBFSweep above always writes a complete PILATUS head.
|
||||
void WriteRawMiniCBF(const std::string &name, const std::string &head_lines,
|
||||
int64_t nx, int64_t ny) {
|
||||
std::ostringstream head;
|
||||
head << "###CBF: VERSION 1.5\n_array_data.header_contents\n" << head_lines
|
||||
<< "_array_data.data\n--CIF-BINARY-FORMAT-SECTION--\n"
|
||||
<< "Content-Type: application/octet-stream;\n"
|
||||
<< " conversions=\"x-CBF_BYTE_OFFSET\"\n"
|
||||
<< "Content-Transfer-Encoding: BINARY\n"
|
||||
<< "X-Binary-Size: " << nx * ny << "\n"
|
||||
<< "X-Binary-Element-Type: \"signed 32-bit integer\"\n"
|
||||
<< "X-Binary-Number-of-Elements: " << nx * ny << "\n"
|
||||
<< "X-Binary-Size-Fastest-Dimension: " << nx << "\n"
|
||||
<< "X-Binary-Size-Second-Dimension: " << ny << "\n\n";
|
||||
std::ofstream f(name, std::ios::binary);
|
||||
const std::string text = head.str();
|
||||
f.write(text.data(), static_cast<std::streamsize>(text.size()));
|
||||
f.write(reinterpret_cast<const char *>(minicbf::BINARY_SEPARATOR),
|
||||
sizeof(minicbf::BINARY_SEPARATOR));
|
||||
const std::vector<char> zeros(static_cast<size_t>(nx * ny), 0);
|
||||
f.write(zeros.data(), static_cast<std::streamsize>(zeros.size()));
|
||||
}
|
||||
}
|
||||
|
||||
// Three ways a CBF that is not a detector image, or is one with a hole in its head, used to be
|
||||
// opened anyway - each of them silently, which is the failure this project cares most about.
|
||||
TEST_CASE("JFJochCBFReader_incomplete_header_is_refused_not_misread", "[HDF5][Full]") {
|
||||
const int64_t nx = 24, ny = 16;
|
||||
|
||||
SECTION("a byte-offset CBF with no PILATUS header is not ours to read") {
|
||||
// XDS writes its correction files in exactly this shape: a real byte-offset binary section
|
||||
// and not one '#' line. Claiming it opened it with a pixel size of zero, which collapses
|
||||
// every resolution and scattering vector the run computes.
|
||||
WriteRawMiniCBF("cbfbare_0001.cbf", "", nx, ny);
|
||||
CHECK_FALSE(JFJochCBFReader::CanRead("cbfbare_0001.cbf"));
|
||||
JFJochCBFReader reader;
|
||||
CHECK_THROWS_AS(reader.ReadFiles("cbfbare_0001.cbf"), JFJochException);
|
||||
remove("cbfbare_0001.cbf");
|
||||
}
|
||||
|
||||
SECTION("a header number that does not parse is a malformed header, not a raw std throw") {
|
||||
// The captures are character classes, not number grammars: "[\\d.eE+-]+" matches a bare ".".
|
||||
// std::stod answers that with std::invalid_argument, which CanRead does not catch - so merely
|
||||
// LOOKING at the file threw out of the format probe.
|
||||
WriteRawMiniCBF("cbfbadnum_0001.cbf",
|
||||
"# Pixel_size 172e-6 m x 172e-6 m\n# Wavelength . A\n"
|
||||
"# Detector_distance 0.3 m\n# Count_cutoff 1000 counts\n", nx, ny);
|
||||
CHECK_NOTHROW(JFJochCBFReader::CanRead("cbfbadnum_0001.cbf"));
|
||||
CHECK_FALSE(JFJochCBFReader::CanRead("cbfbadnum_0001.cbf"));
|
||||
remove("cbfbadnum_0001.cbf");
|
||||
}
|
||||
|
||||
SECTION("no Count_cutoff does not mean every pixel is saturated") {
|
||||
// SaturationLimitFromValue(0) is 1, so an absent line marked every pixel at or above one
|
||||
// count as an overload and the integration accept gate then dropped the whole reflection.
|
||||
WriteMiniCBFSweep("cbfnocut", "# Oscillation_axis OMEGA\n", nx, ny);
|
||||
// ...rewrite frame 1 without the Count_cutoff line, keeping everything else.
|
||||
WriteRawMiniCBF("cbfnocut_0001.cbf",
|
||||
"# Detector: PILATUS3 6M, S/N 60-0119\n"
|
||||
"# Pixel_size 172e-6 m x 172e-6 m\n"
|
||||
"# Silicon sensor, thickness 0.000450 m\n"
|
||||
"# Wavelength 0.96864 A\n# Detector_distance 0.33161 m\n"
|
||||
"# Beam_xy (12.00, 8.00) pixels\n"
|
||||
"# Start_angle 0 deg.\n# Angle_increment 0.1000 deg.\n", nx, ny);
|
||||
JFJochCBFReader reader;
|
||||
REQUIRE_NOTHROW(reader.ReadFiles("cbfnocut_0001.cbf"));
|
||||
const auto x = reader.GetDataset()->experiment;
|
||||
CHECK(x.GetSaturationLimit() > 1);
|
||||
reader.Close();
|
||||
RemoveMiniCBFSweep("cbfnocut");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user