diff --git a/dx2/CMakeLists.txt b/dx2/CMakeLists.txt index ca7b921..bf28b2e 100644 --- a/dx2/CMakeLists.txt +++ b/dx2/CMakeLists.txt @@ -1,5 +1,6 @@ add_library( dx2 + utils.cxx reflection.cxx h5/h5read_processed.cxx h5/h5write.cxx @@ -9,6 +10,7 @@ add_library( goniometer.cxx scan.cxx detector_attenuations.cxx + imagesequence.cxx ) target_link_libraries( dx2 diff --git a/dx2/detector.cxx b/dx2/detector.cxx index 6dde3f4..cb8737e 100644 --- a/dx2/detector.cxx +++ b/dx2/detector.cxx @@ -255,6 +255,12 @@ Detector::Detector(json detector_data) { } } +Detector::Detector(std::vector panels) { + for (auto it = panels.begin(); it != panels.end(); ++it) { + _panels.push_back(*it); + } +} + json Detector::to_json() const { json detector_data; std::vector panels_array; diff --git a/dx2/imagesequence.cxx b/dx2/imagesequence.cxx new file mode 100644 index 0000000..3205a56 --- /dev/null +++ b/dx2/imagesequence.cxx @@ -0,0 +1,68 @@ +#include "dx2/imagesequence.hpp" + +using json = nlohmann::json; + +// Constructor for MultiImage formats e.g. h5. +ImageSequence::ImageSequence(std::string filename, int n_images) + : filename_(filename), n_images_(n_images) { + single_file_indices_.reserve(n_images_); + for (std::size_t i = 0; i < n_images_; ++i) { + single_file_indices_.push_back(i); + } +} + +// Constructor for non-MultiImage formats e.g. cbf. +ImageSequence::ImageSequence(std::string filename) : filename_(filename) {} + +ImageSequence::ImageSequence(json imagesequence_data) { + std::vector required_keys = {"template", "__id__"}; + for (const auto &key : required_keys) { + if (imagesequence_data.find(key) == imagesequence_data.end()) { + throw std::invalid_argument("Key " + key + + " is missing from the input imageset JSON"); + } + } + if (imagesequence_data["__id__"] != std::string("ImageSequence")) { + throw std::runtime_error("Only ImageSequences are supported"); + } + filename_ = imagesequence_data["template"]; + if (imagesequence_data.find("single_file_indices") != + imagesequence_data.end()) { + // for non-multimage formats (e.g. non-h5), allow parsing. + json indices = imagesequence_data["single_file_indices"]; + single_file_indices_ = {}; + if (*(indices.begin()) < 0) { + throw std::runtime_error("Starting file index <0"); + } + for (json::iterator it = indices.begin(); it != indices.end(); ++it) { + single_file_indices_.push_back(*it); + } + n_images_ = single_file_indices_.size(); + } + imagesequence_data_ = + imagesequence_data; // To propagate during serialization/deserialization. +} + +json ImageSequence::to_json() const { + json imageset_data = imagesequence_data_; + imageset_data["__id__"] = "ImageSequence"; + imageset_data["template"] = filename_; + if (single_file_indices_.size() > 0) { // i.e. MultiImage formats (h5). + imageset_data["single_file_indices"] = single_file_indices_; + } + // Set defaults and null for now. + std::vector optional_keys = {"mask", "gain", "pedestal", "dx", + "dy"}; + for (const auto &key : optional_keys) { + if (imagesequence_data_.find(key) == imagesequence_data_.end()) { + imageset_data[key] = nullptr; + } + } + if (imagesequence_data_.find("params") == imagesequence_data_.end()) { + json params; + params["dynamic_shadowing"] = "Auto"; + params["multi_panel"] = false; + imageset_data["params"] = params; + } + return imageset_data; +} \ No newline at end of file diff --git a/dx2/reflection.cxx b/dx2/reflection.cxx index df0cc01..369efcc 100644 --- a/dx2/reflection.cxx +++ b/dx2/reflection.cxx @@ -4,6 +4,7 @@ */ #include "dx2/reflection.hpp" +#include "dx2/utils.hpp" #include #include #include @@ -142,32 +143,6 @@ void ReflectionTable::merge_into_set(std::unordered_set &set, const std::vector &rows) const { set.insert(rows.begin(), rows.end()); } - -std::string ReflectionTable::ersatz_uuid4() const { - // Generate 16 random bytes - std::array bytes; - std::random_device rd; - std::uniform_int_distribution dist(0, 255); - for (auto &b : bytes) { - b = static_cast(dist(rd)); - } - - // Convert bytes to a single 128-bit hex string (little endian) - std::ostringstream oss; - for (auto it = bytes.rbegin(); it != bytes.rend(); ++it) { - oss << std::hex << std::setw(2) << std::setfill('0') - << static_cast(*it); - } - std::string hex = oss.str(); - - // Format as UUID: 8-4-4-4-12 - std::ostringstream uuid; - uuid << hex.substr(0, 8) << "-" << hex.substr(8, 4) << "-" - << hex.substr(12, 4) << "-" << hex.substr(16, 4) << "-" - << hex.substr(20, 12); - - return uuid.str(); -} #pragma endregion #pragma region Selection Methods diff --git a/dx2/utils.cxx b/dx2/utils.cxx new file mode 100644 index 0000000..48c12f0 --- /dev/null +++ b/dx2/utils.cxx @@ -0,0 +1,68 @@ +#include "dx2/utils.hpp" +#include +#include +#include +#include +#include + +using Eigen::Vector3d; + +double angle_between_vectors_degrees(Vector3d v1, Vector3d v2) { + double l1 = v1.norm(); + double l2 = v2.norm(); + double dot = v1.dot(v2); + double normdot = dot / (l1 * l2); + if (std::abs(normdot - 1.0) < 1E-6) { + return 0.0; + } + if (std::abs(normdot + 1.0) < 1E-6) { + return 180.0; + } + double angle = std::acos(normdot) * 180.0 / M_PI; + return angle; +} + +/** + * @brief Generate a pseudo-random UUID-like identifier. + * + * This function replicates the behaviour of the Python function + * `ersatz_uuid4` from the dxtbx library. It generates a 128-bit + * random value and formats it as a UUID-style string using + * little-endian byte order, without enforcing RFC 4122 compliance. + * + * The output is a 36-character string in the format: + * `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`, where each `x` is a + * hexadecimal digit. + * + * @return A string representing the generated UUID-like identifier. + * + * @note This function does not set the version or variant bits as + * specified in RFC 4122. It is intended for internal use where + * uniqueness is sufficient, and compliance with UUID standards + * is unnecessary. + */ +std::string ersatz_uuid4() { + // Generate 16 random bytes + std::array bytes; + std::random_device rd; + std::uniform_int_distribution dist(0, 255); + for (auto &b : bytes) { + b = static_cast(dist(rd)); + } + + // Convert bytes to a single 128-bit hex string (little endian) + std::ostringstream oss; + for (auto it = bytes.rbegin(); it != bytes.rend(); ++it) { + oss << std::hex << std::setw(2) << std::setfill('0') + << static_cast(*it); + } + std::string hex = oss.str(); + + // Format as UUID: 8-4-4-4-12 + std::ostringstream uuid; + uuid << hex.substr(0, 8) << "-" << hex.substr(8, 4) << "-" + << hex.substr(12, 4) << "-" << hex.substr(16, 4) << "-" + << hex.substr(20, 12); + + return uuid.str(); +} \ No newline at end of file diff --git a/include/dx2/detector.hpp b/include/dx2/detector.hpp index af8fd11..0fea7b4 100644 --- a/include/dx2/detector.hpp +++ b/include/dx2/detector.hpp @@ -106,6 +106,7 @@ class Detector { public: Detector() = default; Detector(json detector_data); + Detector(std::vector panels); json to_json() const; std::vector panels() const; std::optional get_ray_intersection(const Vector3d &s1) const; diff --git a/include/dx2/experiment.hpp b/include/dx2/experiment.hpp index d19cc70..8d9bd91 100644 --- a/include/dx2/experiment.hpp +++ b/include/dx2/experiment.hpp @@ -4,7 +4,9 @@ #include #include #include +#include #include +#include #include using Eigen::Vector3d; @@ -22,7 +24,13 @@ template class Experiment { Detector &detector(); const Crystal &crystal() const; void set_crystal(Crystal crystal); + void set_beam(BeamType beam); + void set_scan(Scan scan); + void set_detector(Detector detector); + void set_goniometer(Goniometer goniometer); + void set_imagesequence(ImageSequence imagesequence); void set_identifier(std::string identifier); + void generate_identifier(); protected: BeamType _beam{}; @@ -30,7 +38,7 @@ template class Experiment { Goniometer _goniometer{}; Detector _detector{}; Crystal _crystal{}; - json _imageset_json{}; + ImageSequence _imagesequence{}; std::string _identifier{}; }; @@ -51,8 +59,9 @@ Experiment::Experiment(json experiment_data) { this->_goniometer = gonio; this->_detector = detector; // Save the imageset json to propagate when saving to file. - json imageset_data = experiment_data["imageset"][0]; - this->_imageset_json = imageset_data; + json imagesequence_data = experiment_data["imageset"][0]; + ImageSequence imagesequence(imagesequence_data); + this->_imagesequence = imagesequence; try { // We don't always have a crystal model e.g. before indexing. json crystal_data = experiment_data["crystal"][0]; Crystal crystal(crystal_data); @@ -67,7 +76,6 @@ template json Experiment::to_json() const { json elist_out; // a list of potentially multiple experiments elist_out["__id__"] = "ExperimentList"; json expt_out; // our single experiment - // no imageset (for now?). expt_out["__id__"] = "Experiment"; expt_out["identifier"] = _identifier; expt_out["beam"] = @@ -82,7 +90,7 @@ template json Experiment::to_json() const { elist_out["goniometer"] = std::array{_goniometer.to_json()}; elist_out["beam"] = std::array{_beam.to_json()}; elist_out["detector"] = std::array{_detector.to_json()}; - elist_out["imageset"] = std::array{_imageset_json}; + elist_out["imageset"] = std::array{_imagesequence.to_json()}; if (_crystal.get_U_matrix().determinant()) { expt_out["crystal"] = 0; @@ -115,10 +123,33 @@ void Experiment::set_crystal(Crystal crystal) { _crystal = crystal; } +template void Experiment::set_beam(BeamType beam) { + _beam = beam; +} + template BeamType &Experiment::beam() { return _beam; } +template void Experiment::set_scan(Scan scan) { + _scan = scan; +} + +template +void Experiment::set_detector(Detector detector) { + _detector = detector; +} + +template +void Experiment::set_goniometer(Goniometer goniometer) { + _goniometer = goniometer; +} + +template +void Experiment::set_imagesequence(ImageSequence imagesequence) { + _imagesequence = imagesequence; +} + template const std::string &Experiment::identifier() const { return _identifier; @@ -126,4 +157,8 @@ const std::string &Experiment::identifier() const { template void Experiment::set_identifier(std::string identifier) { _identifier = identifier; +} + +template void Experiment::generate_identifier() { + _identifier = ersatz_uuid4(); } \ No newline at end of file diff --git a/include/dx2/imagesequence.hpp b/include/dx2/imagesequence.hpp new file mode 100644 index 0000000..a386472 --- /dev/null +++ b/include/dx2/imagesequence.hpp @@ -0,0 +1,22 @@ +#pragma once +#include + +using json = nlohmann::json; + +class ImageSequence { +public: + ImageSequence() = default; + ImageSequence(std::string filename, + int n_images); // Constructor for MultiImage formats e.g. h5. + ImageSequence( + std::string filename); // Constructor for non-MultiImage formats e.g. cbf. + ImageSequence(json imagesequence_data); + json to_json() const; + +protected: + int n_images_{}; + std::string filename_{}; + std::vector single_file_indices_{}; + json imagesequence_data_{}; // For propagating additional metadata during + // serialization/deserialization. +}; \ No newline at end of file diff --git a/include/dx2/reflection.hpp b/include/dx2/reflection.hpp index 5563f76..4a9c468 100644 --- a/include/dx2/reflection.hpp +++ b/include/dx2/reflection.hpp @@ -316,27 +316,6 @@ class ReflectionTable { template struct is_column_predicate> : std::true_type {}; - /** - * @brief Generate a pseudo-random UUID-like identifier. - * - * This function replicates the behaviour of the Python function - * `ersatz_uuid4` from the dxtbx library. It generates a 128-bit - * random value and formats it as a UUID-style string using - * little-endian byte order, without enforcing RFC 4122 compliance. - * - * The output is a 36-character string in the format: - * `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`, where each `x` is a - * hexadecimal digit. - * - * @return A string representing the generated UUID-like identifier. - * - * @note This function does not set the version or variant bits as - * specified in RFC 4122. It is intended for internal use where - * uniqueness is sufficient, and compliance with UUID standards - * is unnecessary. - */ - std::string ersatz_uuid4() const; - public: #pragma region Constructors /// Re-exported type aliase for convenience diff --git a/include/dx2/utils.hpp b/include/dx2/utils.hpp index 65079b4..a0ea002 100644 --- a/include/dx2/utils.hpp +++ b/include/dx2/utils.hpp @@ -1,20 +1,8 @@ #pragma once #include -#include using Eigen::Vector3d; -double angle_between_vectors_degrees(Vector3d v1, Vector3d v2) { - double l1 = v1.norm(); - double l2 = v2.norm(); - double dot = v1.dot(v2); - double normdot = dot / (l1 * l2); - if (std::abs(normdot - 1.0) < 1E-6) { - return 0.0; - } - if (std::abs(normdot + 1.0) < 1E-6) { - return 180.0; - } - double angle = std::acos(normdot) * 180.0 / M_PI; - return angle; -} +double angle_between_vectors_degrees(Vector3d v1, Vector3d v2); + +std::string ersatz_uuid4(); \ No newline at end of file