diff --git a/dx2/goniometer.cxx b/dx2/goniometer.cxx index 38ada3d..ca459cc 100644 --- a/dx2/goniometer.cxx +++ b/dx2/goniometer.cxx @@ -70,6 +70,11 @@ Goniometer::Goniometer(std::vector axes, std::vector angles, init(); } +Goniometer::Goniometer(Matrix3d sample_rotation, Vector3d rotation_axis, + Matrix3d setting_rotation) + : sample_rotation_{sample_rotation}, rotation_axis_{rotation_axis}, + setting_rotation_{setting_rotation} {} + Matrix3d Goniometer::get_setting_rotation() const { return setting_rotation_; } Matrix3d Goniometer::get_sample_rotation() const { return sample_rotation_; } @@ -77,15 +82,40 @@ Matrix3d Goniometer::get_sample_rotation() const { return sample_rotation_; } Vector3d Goniometer::get_rotation_axis() const { return rotation_axis_; } Goniometer::Goniometer(json goniometer_data) { - std::vector required_keys = {"axes", "angles", "names", - "scan_axis"}; - for (const auto &key : required_keys) { + // The goniometer data can either be single or multi axis form. + std::vector multi_axis_keys = {"axes", "angles", "names", + "scan_axis"}; + std::vector single_axis_keys = { + "rotation_axis", "fixed_rotation", "setting_rotation"}; + + for (const auto &key : multi_axis_keys) { if (goniometer_data.find(key) == goniometer_data.end()) { - throw std::invalid_argument("Key " + key + - " is missing from the input goniometer JSON"); + // Could be a single axis gonio - they only provide rotation, fixed and + // setting + for (const auto &akey : single_axis_keys) { + if (goniometer_data.find(akey) == goniometer_data.end()) { + throw std::invalid_argument("Key " + key + + " is missing from the input goniometer " + "JSON - treating as single axis but" + + " key " + akey + " also missing."); + } + } + // We can create from the rotation axis data. + rotation_axis_ = Vector3d(goniometer_data["rotation_axis"][0], + goniometer_data["rotation_axis"][1], + goniometer_data["rotation_axis"][2]); + json setting = goniometer_data["setting_rotation"]; + setting_rotation_ << setting[0], setting[1], setting[2], setting[3], + setting[4], setting[5], setting[6], setting[7], setting[8]; // F + json sample = goniometer_data["fixed_rotation"]; + sample_rotation_ << sample[0], sample[1], sample[2], sample[3], sample[4], + sample[5], sample[6], sample[7], sample[8]; // S + return; } } std::vector axes; + std::vector names; + std::vector angles; for (json::iterator it = goniometer_data["axes"].begin(); it != goniometer_data["axes"].end(); it++) { Vector3d axis; @@ -95,13 +125,11 @@ Goniometer::Goniometer(json goniometer_data) { axes.push_back(axis); } axes_ = axes; - std::vector angles; for (json::iterator it = goniometer_data["angles"].begin(); it != goniometer_data["angles"].end(); it++) { angles.push_back(*it); } angles_ = angles; - std::vector names; for (json::iterator it = goniometer_data["names"].begin(); it != goniometer_data["names"].end(); it++) { names.push_back(*it); @@ -113,9 +141,25 @@ Goniometer::Goniometer(json goniometer_data) { json Goniometer::to_json() const { json goniometer_data; - goniometer_data["axes"] = axes_; - goniometer_data["angles"] = angles_; - goniometer_data["names"] = names_; - goniometer_data["scan_axis"] = scan_axis_; + if (axes_.size() > 0) { + // Multi-axis format + goniometer_data["axes"] = axes_; + goniometer_data["angles"] = angles_; + goniometer_data["names"] = names_; + goniometer_data["scan_axis"] = scan_axis_; + } else { + // Single-axis format + goniometer_data["rotation_axis"] = rotation_axis_; + goniometer_data["fixed_rotation"] = std::vector{ + sample_rotation_(0, 0), sample_rotation_(0, 1), sample_rotation_(0, 2), + sample_rotation_(1, 0), sample_rotation_(1, 1), sample_rotation_(1, 2), + sample_rotation_(2, 0), sample_rotation_(2, 1), sample_rotation_(2, 2)}; + goniometer_data["setting_rotation"] = + std::vector{setting_rotation_(0, 0), setting_rotation_(0, 1), + setting_rotation_(0, 2), setting_rotation_(1, 0), + setting_rotation_(1, 1), setting_rotation_(1, 2), + setting_rotation_(2, 0), setting_rotation_(2, 1), + setting_rotation_(2, 2)}; + } return goniometer_data; } diff --git a/include/dx2/goniometer.hpp b/include/dx2/goniometer.hpp index ab247c1..436022d 100644 --- a/include/dx2/goniometer.hpp +++ b/include/dx2/goniometer.hpp @@ -12,25 +12,33 @@ Matrix3d axis_and_angle_as_matrix(Vector3d axis, double angle, class Goniometer { // A class to represent a multi-axis goniometer. + // A single-axis goniometer just doesn't have axes and angles defined. public: Goniometer() = default; Goniometer(std::vector axes, std::vector angles, std::vector names, std::size_t scan_axis); Goniometer(json goniometer_data); + Goniometer(Matrix3d sample_rotation, Vector3d axis, + Matrix3d setting_rotation); Matrix3d get_setting_rotation() const; Matrix3d get_sample_rotation() const; Vector3d get_rotation_axis() const; json to_json() const; protected: - void init(); // Sets the matrices from the axes and angles + void init(); // Sets the matrices from the axes and angles for multi-axis + // goniometers. + // These two functions calculate F and S for multi-axis goniometers. Matrix3d calculate_setting_rotation(); Matrix3d calculate_sample_rotation(); + // The core 2 matrices and rotation axis that define a goniometer. Matrix3d sample_rotation_{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}; // F Vector3d rotation_axis_{{1.0, 0.0, 0.0}}; // R' Matrix3d setting_rotation_{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}; // S - std::vector axes_{{1.0, 0.0, 0.0}}; - std::vector angles_{{0.0}}; - std::vector names_{"omega"}; - std::size_t scan_axis_{0}; + // The next three quantities do not get non-empty defaults - only needed + // to hold additional information for multi-axis goniometers. + std::vector axes_{}; + std::vector angles_{}; + std::vector names_{}; + std::size_t scan_axis_ = 0; }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e2a8f6e..ca98a11 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -14,6 +14,9 @@ target_link_libraries(test_reflection_table GTest::gtest_main dx2) add_executable(test_detector_attenuations test_detector_attenuations.cxx) target_link_libraries(test_detector_attenuations GTest::gtest_main dx2) +add_executable(test_goniometer test_goniometer.cxx) +target_link_libraries(test_goniometer GTest::gtest_main dx2 nlohmann_json::nlohmann_json) + include(GoogleTest) gtest_discover_tests(test_crystal PROPERTIES LABELS dx2tests) @@ -21,3 +24,4 @@ gtest_discover_tests(test_read_h5_array WORKING_DIRECTORY "${PROJECT_SOURCE_DIR} gtest_discover_tests(test_write_h5_array WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}/tests" PROPERTIES LABELS dx2tests) gtest_discover_tests(test_reflection_table WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}/tests" PROPERTIES LABELS dx2tests) gtest_discover_tests(test_detector_attenuations WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}/tests" PROPERTIES LABELS dx2tests) +gtest_discover_tests(test_goniometer WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}/tests" PROPERTIES LABELS dx2tests) diff --git a/tests/test_goniometer.cxx b/tests/test_goniometer.cxx new file mode 100644 index 0000000..255287b --- /dev/null +++ b/tests/test_goniometer.cxx @@ -0,0 +1,76 @@ + +#include +#include +#include + +TEST(ModelTests, SingleAxisGoniometerTest) { + // Test loading single axis goniometer from json. + // Note the fixed rotation is not strictly valid, but fine for testing + // serialization. + std::string json_str = R"({ + "goniometer": [ + { + "rotation_axis": [1.0, 0.0, 0.0], + "fixed_rotation": [0.99, 0.01, 0.0, -0.01, 0.99, 0.0, 0.0, 0.0, 1.0], + "setting_rotation": [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0] + } + ] + })"; + json j = json::parse(json_str); + auto goniometer_data = j["goniometer"][0]; + Goniometer gonio(goniometer_data); + Matrix3d setting = gonio.get_setting_rotation(); + Matrix3d sample = gonio.get_sample_rotation(); + Matrix3d expected_setting{{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}; + Matrix3d expected_sample{ + {0.99, 0.01, 0.0}, {-0.01, 0.99, 0.0}, {0.00, 0.00, 1.0}}; + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + EXPECT_NEAR(setting(i, j), expected_setting(i, j), 1E-6); + EXPECT_NEAR(sample(i, j), expected_sample(i, j), 1E-6); + } + } + json output = gonio.to_json(); + std::vector expected_fixed = {0.99, 0.01, 0.0, -0.01, 0.99, + 0.0, 0.0, 0.0, 1.0}; + for (int i = 0; i < 9; ++i) { + ASSERT_EQ(output["fixed_rotation"][i], expected_fixed[i]); + } +} + +TEST(ModelTests, MultiAxisGoniometerTest) { + // Test loading multi axis goniometer from json. + std::string json_str = R"({ + "goniometer": [ + { + "axes": [ + [1.0, -0.0025, 0.0056], + [-0.006, -0.0264, -0.9996], + [1.0, 0.0, 0.0] + ], + "angles": [0.0, 5.0, 0.0], + "names": ["phi", "chi", "omega"], + "scan_axis": 2 + } + ] + })"; + json j = json::parse(json_str); + auto goniometer_data = j["goniometer"][0]; + Goniometer gonio(goniometer_data); + Matrix3d setting = gonio.get_setting_rotation(); + Matrix3d sample = gonio.get_sample_rotation(); + Matrix3d expected_setting{{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}; + Matrix3d expected_sample{{0.996195, 0.0871244, -0.00227816}, + {-0.0871232, 0.996197, 0.000623378}, + {0.00232381, -0.000422525, 0.999997}}; + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + EXPECT_NEAR(setting(i, j), expected_setting(i, j), 1E-6); + EXPECT_NEAR(sample(i, j), expected_sample(i, j), 1E-6); + } + } + json output = gonio.to_json(); + ASSERT_EQ(output["angles"][0], 0.0); + ASSERT_EQ(output["angles"][1], 5.0); + ASSERT_EQ(output["angles"][0], 0.0); +} \ No newline at end of file