diff --git a/CMakeLists.txt b/CMakeLists.txt index bcf5570..dd87a15 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,44 +68,44 @@ add_subdirectory(tests) # ####################################################################### # Installation -install( - TARGETS dx2 - EXPORT DX2Targets - LIBRARY DESTINATION lib +# install( +# TARGETS dx2 +# EXPORT DX2Targets +# LIBRARY DESTINATION lib - # FILE_SET HEADERS DESTINATION include/dx2 -) -install( - FILES - include/dx2/dx2.h - DESTINATION include/dx2 -) +# # FILE_SET HEADERS DESTINATION include/dx2 +# ) +# install( +# FILES +# include/dx2/dx2.h +# DESTINATION include/dx2 +# ) -install( - EXPORT DX2Targets - FILE DX2Targets.cmake - DESTINATION lib/cmake/DX2 -) +# install( +# EXPORT DX2Targets +# FILE DX2Targets.cmake +# DESTINATION lib/cmake/DX2 +# ) -include(CMakePackageConfigHelpers) -configure_package_config_file( - ${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in - "${CMAKE_CURRENT_BINARY_DIR}/DX2Config.cmake" - INSTALL_DESTINATION "lib/cmake/DX2" - NO_SET_AND_CHECK_MACRO - NO_CHECK_REQUIRED_COMPONENTS_MACRO -) -write_basic_package_version_file( - "${CMAKE_CURRENT_BINARY_DIR}/DX2ConfigVersion.cmake" - VERSION "${DX2_VERSION_MAJOR}.${DX2_VERSION_MINOR}" - COMPATIBILITY AnyNewerVersion -) -install( - FILES - ${CMAKE_CURRENT_BINARY_DIR}/DX2Config.cmake - ${CMAKE_CURRENT_BINARY_DIR}/DX2ConfigVersion.cmake - DESTINATION lib/cmake/DX2 -) -export(EXPORT DX2Targets - FILE "${CMAKE_CURRENT_BINARY_DIR}/DX2Targets.cmake" -) +# include(CMakePackageConfigHelpers) +# configure_package_config_file( +# ${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in +# "${CMAKE_CURRENT_BINARY_DIR}/DX2Config.cmake" +# INSTALL_DESTINATION "lib/cmake/DX2" +# NO_SET_AND_CHECK_MACRO +# NO_CHECK_REQUIRED_COMPONENTS_MACRO +# ) +# write_basic_package_version_file( +# "${CMAKE_CURRENT_BINARY_DIR}/DX2ConfigVersion.cmake" +# VERSION "${DX2_VERSION_MAJOR}.${DX2_VERSION_MINOR}" +# COMPATIBILITY AnyNewerVersion +# ) +# install( +# FILES +# ${CMAKE_CURRENT_BINARY_DIR}/DX2Config.cmake +# ${CMAKE_CURRENT_BINARY_DIR}/DX2ConfigVersion.cmake +# DESTINATION lib/cmake/DX2 +# ) +# export(EXPORT DX2Targets +# FILE "${CMAKE_CURRENT_BINARY_DIR}/DX2Targets.cmake" +# ) diff --git a/dx2/CMakeLists.txt b/dx2/CMakeLists.txt index b5c3d73..b2a8921 100644 --- a/dx2/CMakeLists.txt +++ b/dx2/CMakeLists.txt @@ -4,7 +4,7 @@ add_library( ) target_link_libraries( dx2 - PRIVATE + PUBLIC Eigen3::Eigen nlohmann_json::nlohmann_json hdf5::hdf5 @@ -16,4 +16,3 @@ target_include_directories( $ $ ) -target_link_libraries(dx2 PUBLIC $) diff --git a/include/dx2/h5/h5read_processed.hpp b/include/dx2/h5/h5read_processed.hpp new file mode 100644 index 0000000..2c21da5 --- /dev/null +++ b/include/dx2/h5/h5read_processed.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * @brief Reads a dataset from an HDF5 file into an std::vector. + * + * @tparam T The type of data to read (e.g., int, double). + * @param filename The path to the HDF5 file. + * @param dataset_name The name of the dataset to read. + * @return A std::vector containing the data from the dataset. + * @throws std::runtime_error If the file, dataset, or datatype cannot be opened + * or read. + */ +template +std::vector read_array_from_h5_file(const std::string &filename, + const std::string &dataset_name) { + // Start measuring time + auto start_time = std::chrono::high_resolution_clock::now(); + + // Open the HDF5 file + hid_t file = H5Fopen(filename.c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); + if (file < 0) { + throw std::runtime_error("Error: Unable to open file: " + filename); + } + + try { + // Open the dataset + hid_t dataset = H5Dopen(file, dataset_name.c_str(), H5P_DEFAULT); + if (dataset < 0) { + throw std::runtime_error("Error: Unable to open dataset: " + + dataset_name); + } + + try { + // Get the datatype and check size + hid_t datatype = H5Dget_type(dataset); + size_t datatype_size = H5Tget_size(datatype); + if (datatype_size != sizeof(T)) { + throw std::runtime_error( + "Error: Dataset type size does not match expected type size."); + } + + // Get the dataspace and the number of elements + hid_t dataspace = H5Dget_space(dataset); + size_t num_elements = H5Sget_simple_extent_npoints(dataspace); + + // Allocate a vector to hold the data + std::vector data_out(num_elements); + + // Read the data into the vector + herr_t status = H5Dread(dataset, datatype, H5S_ALL, H5S_ALL, H5P_DEFAULT, + data_out.data()); + if (status < 0) { + throw std::runtime_error("Error: Unable to read dataset: " + + dataset_name); + } + + // Close the dataset and return the data + H5Dclose(dataset); + H5Fclose(file); + + // Log timing + auto end_time = std::chrono::high_resolution_clock::now(); + double elapsed_time = + std::chrono::duration(end_time - start_time).count(); + std::cout << "READ TIME for " << dataset_name << " : " << elapsed_time + << "s" << std::endl; + + return data_out; + + } catch (...) { + H5Dclose(dataset); // Ensure dataset is closed in case of failure + throw; + } + } catch (...) { + H5Fclose(file); // Ensure file is closed in case of failure + throw; + } +} diff --git a/include/dx2/h5/h5write.hpp b/include/dx2/h5/h5write.hpp new file mode 100644 index 0000000..98f5e85 --- /dev/null +++ b/include/dx2/h5/h5write.hpp @@ -0,0 +1,263 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +/** + * @brief Recursively traverses or creates groups in an HDF5 file based + * on the given path. + * + * This function takes a parent group identifier and a path string, and + * recursively traverses or creates the groups specified in the path. If + * a group in the path does not exist, it is created. + * + * @param parent The identifier of the parent group in the HDF5 file. + * @param path The path of groups to traverse or create, specified as a + * string with '/' as the delimiter. + * @return The identifier of the final group in the path. + * @throws std::runtime_error If a group cannot be created or opened. + */ +hid_t traverse_or_create_groups(hid_t parent, const std::string &path) { + // Strip leading '/' characters, if any, to prevent empty group names + size_t start_pos = path.find_first_not_of('/'); + if (start_pos == std::string::npos) { + return parent; // Return parent if the path is entirely '/' + } + std::string cleaned_path = path.substr(start_pos); + + /* + * This is the base case for recursion. When the path is empty, we + * have reached the final group in the path and we return the parent + * group. + */ + if (cleaned_path.empty()) { + return parent; + } + + // Split the path into the current group name and the remaining path + size_t pos = cleaned_path.find('/'); + std::string group_name = + (pos == std::string::npos) ? cleaned_path : cleaned_path.substr(0, pos); + std::string remaining_path = + (pos == std::string::npos) ? "" : cleaned_path.substr(pos + 1); + + // Attempt to open the group. If it does not exist, create it. + H5Eset_auto2(H5E_DEFAULT, NULL, NULL); // Suppress errors to stdout when + // trying to open a file/group that may not exist. + hid_t next_group = H5Gopen(parent, group_name.c_str(), H5P_DEFAULT); + if (next_group < 0) { + next_group = H5Gcreate(parent, group_name.c_str(), H5P_DEFAULT, H5P_DEFAULT, + H5P_DEFAULT); + if (next_group < 0) { + std::runtime_error("Error: Unable to create or open group: " + + group_name); + } + } + + // Recurse to the next group in the hierarchy + hid_t final_group = traverse_or_create_groups(next_group, remaining_path); + + // Close the current group to avoid resource leaks, except for the final group + if (next_group != final_group) { + H5Gclose(next_group); + } + + return final_group; +} + +/** + * @brief Deduce the shape of a nested container. + * + * This helper function recursively determines the shape of nested + * containers. This is used to determine the shape of the dataset to be + * created in an HDF5 file. + * + * @tparam Container The type of the container. + * @param container The container whose shape is to be determined. + * @return A vector of dimensions representing the shape of the + * container. + */ +template +std::vector deduce_shape(const Container &container) { + if (container.empty()) { + return {0}; + } + if constexpr (std::is_arithmetic_v) { + // Base case: container holds arithmetic types (e.g., double, int) + return {container.size()}; + } else { + // Recursive case: container holds other containers + + // Check that all inner containers have the same size + size_t inner_size = container.begin()->size(); + for (const auto &sub_container : container) { + if (sub_container.size() != inner_size) { + throw std::runtime_error("Cannot deduce shape: inner containers have " + "different sizes."); + } + } + + auto sub_shape = deduce_shape(*container.begin()); + sub_shape.insert(sub_shape.begin(), container.size()); + return sub_shape; + } +} + +/** + * @brief Flatten nested containers into a 1D vector. + * + * This helper function recursively flattens nested containers into a 1D + * vector for writing to HDF5. If the input container is already 1D, it + * simply returns it. + * + * @tparam Container The type of the container. + * @param container The container to flatten. + * @return A flat vector containing all elements of the input container + * in a linear order. + */ +template auto flatten(const Container &container) { + // Determine the type of the elements in the container + using ValueType = typename Container::value_type; + + // Base case: If the container holds arithmetic types (e.g., int, double), + // it is already 1D, so we return a copy of the container as a std::vector. + if constexpr (std::is_arithmetic_v) { + return std::vector(container.begin(), container.end()); + } else { + // Recursive case: The container holds nested containers, so we need to + // flatten them. + + // Determine the type of elements in the flattened result. + // This is deduced by recursively calling flatten on the first + // sub-container. + using InnerType = + typename decltype(flatten(*container.begin()))::value_type; + + // Create a vector to store the flattened data + std::vector flat_data; + + // Iterate over the outer container + for (const auto &sub_container : container) { + // Recursively flatten each sub-container + auto sub_flat = flatten(sub_container); + + // Append the flattened sub-container to the result + flat_data.insert(flat_data.end(), sub_flat.begin(), sub_flat.end()); + } + + // Return the fully flattened data + return flat_data; + } +} + +/** + * @brief Writes multidimensional data to an HDF5 file. + * + * This function writes a dataset to an HDF5 file. The dataset's shape + * is determined dynamically based on the input container. + * + * @tparam Container The type of the container holding the data. + * @param filename The path to the HDF5 file. + * @param dataset_path The full path to the dataset, including group + * hierarchies. + * @param data The data to write to the dataset. + * @throws std::runtime_error If the dataset cannot be created or data + * cannot be written. + */ +template +void write_data_to_h5_file(const std::string &filename, + const std::string &dataset_path, + const Container &data) { + // Open or create the HDF5 file + H5Eset_auto2(H5E_DEFAULT, NULL, NULL); // Suppress errors to stdout when + // trying to open a file/group that may not exist. + hid_t file = H5Fopen(filename.c_str(), H5F_ACC_RDWR, H5P_DEFAULT); + if (file < 0) { + file = H5Fcreate(filename.c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT); + if (file < 0) { + throw std::runtime_error("Error: Unable to create or open file: " + + filename); + } + } + + try { + // Separate the dataset path into group path and dataset name + size_t last_slash_pos = dataset_path.find_last_of('/'); + if (last_slash_pos == std::string::npos) { + throw std::runtime_error("Error: Invalid dataset path, no '/' found: " + + dataset_path); + } + + std::string group_path = dataset_path.substr(0, last_slash_pos); + std::string dataset_name = dataset_path.substr(last_slash_pos + 1); + + // Traverse or create the groups leading to the dataset + hid_t group = traverse_or_create_groups(file, group_path); + + // Deduce the shape of the data + std::vector shape = deduce_shape(data); + + // Flatten the data into a 1D vector + auto flat_data = flatten(data); + + // Check if dataset exists + hid_t dataset = H5Dopen(group, dataset_name.c_str(), H5P_DEFAULT); + if (dataset < 0) { + // Dataset does not exist, create it + hid_t dataspace = H5Screate_simple(shape.size(), shape.data(), NULL); + if (dataspace < 0) { + throw std::runtime_error( + "Error: Unable to create dataspace for dataset: " + dataset_name); + } + + dataset = H5Dcreate(group, dataset_name.c_str(), H5T_NATIVE_DOUBLE, + dataspace, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); + if (dataset < 0) { + H5Sclose(dataspace); + throw std::runtime_error("Error: Unable to create dataset: " + + dataset_name); + } + + H5Sclose(dataspace); + } else { + // Dataset exists, check if the shape matches + hid_t existing_space = H5Dget_space(dataset); + int ndims = H5Sget_simple_extent_ndims(existing_space); + std::vector existing_dims(ndims); + H5Sget_simple_extent_dims(existing_space, existing_dims.data(), NULL); + H5Sclose(existing_space); + + if (existing_dims != shape) { + H5Dclose(dataset); + throw std::runtime_error( + "Error: Dataset shape mismatch. Cannot overwrite dataset: " + + dataset_name); + } + + // Dataset exists and has the correct shape, proceed to overwrite + } + + // Write the data to the dataset + herr_t status = H5Dwrite(dataset, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, + H5P_DEFAULT, flat_data.data()); + if (status < 0) { + H5Dclose(dataset); + throw std::runtime_error("Error: Unable to write data to dataset: " + + dataset_name); + } + + // Cleanup resources + H5Dclose(dataset); + H5Gclose(group); + } catch (...) { + H5Fclose(file); + throw; // Re-throw the exception to propagate it upwards + } + + // Close the file + H5Fclose(file); +} diff --git a/include/dx2/h5read_processed.h b/include/dx2/h5read_processed.h deleted file mode 100644 index 1b636ca..0000000 --- a/include/dx2/h5read_processed.h +++ /dev/null @@ -1,40 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -template -std::vector read_array_from_h5_file(std::string filename, - std::string array_name) { - auto start_time = std::chrono::high_resolution_clock::now(); - hid_t file = H5Fopen(filename.c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); - if (file < 0) { - std::cout << "Error: Unable to open " << filename.c_str() - << " as a hdf5 reflection table" << std::endl; - std::exit(1); - } - - hid_t dataset = H5Dopen(file, array_name.c_str(), H5P_DEFAULT); - hid_t datatype = H5Dget_type(dataset); - size_t datatype_size = H5Tget_size(datatype); - assert((datatype_size == sizeof(T))); - hid_t dataspace = H5Dget_space(dataset); - size_t num_elements = H5Sget_simple_extent_npoints(dataspace); - hid_t space = H5Dget_space(dataset); - std::vector data_out(num_elements); - - H5Dread(dataset, datatype, H5S_ALL, space, H5P_DEFAULT, &data_out[0]); - float total_time = std::chrono::duration_cast>( - std::chrono::high_resolution_clock::now() - start_time) - .count(); - std::cout << "READ TIME for " << array_name << " : " << total_time << "s" - << std::endl; - - H5Dclose(dataset); - H5Fclose(file); - return data_out; -} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2de1601..1216b17 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,7 +5,11 @@ target_link_libraries(test_make_models GTest::gtest_main dx2 Eigen3::Eigen nlohm add_executable(test_read_h5_array test_read_h5_array.cxx) target_link_libraries(test_read_h5_array GTest::gtest_main dx2 hdf5::hdf5) +add_executable(test_write_h5_array test_write_h5_array.cxx) +target_link_libraries(test_write_h5_array GTest::gtest_main dx2 hdf5::hdf5) + include(GoogleTest) gtest_discover_tests(test_make_models PROPERTIES LABELS dx2tests) gtest_discover_tests(test_read_h5_array WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}/tests" PROPERTIES LABELS dx2tests) +gtest_discover_tests(test_write_h5_array WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}/tests" PROPERTIES LABELS dx2tests) diff --git a/tests/data/cut_strong.refl b/tests/data/cut_strong.refl index 4a751da..b515665 100644 Binary files a/tests/data/cut_strong.refl and b/tests/data/cut_strong.refl differ diff --git a/tests/test_read_h5_array.cxx b/tests/test_read_h5_array.cxx index 3b96cc3..cedecb3 100644 --- a/tests/test_read_h5_array.cxx +++ b/tests/test_read_h5_array.cxx @@ -1,29 +1,120 @@ -#include +#include #include #include -#include +#include +#include +#include -// A test that we can read data arrays from a h5 processing file. +// Test fixture for HDF5 read tests +class HDF5ReadTest : public ::testing::Test { +protected: + std::filesystem::path test_file_path; -TEST(ExampleTests, ReadArrayTest) { + void SetUp() override { + // Set the test file path (assumes the tests directory as the working + // directory) + test_file_path = std::filesystem::current_path() / "data/cut_strong.refl"; + // Create the empty dataset if it does not exist + create_empty_dataset(test_file_path, "/dials/processing/empty_dataset"); + } + + void create_empty_dataset(const std::string &filename, + const std::string &dataset_path) { + // Open the HDF5 file + hid_t file = H5Fopen(filename.c_str(), H5F_ACC_RDWR, H5P_DEFAULT); + if (file < 0) { + throw std::runtime_error("Error: Unable to open file: " + filename); + } + + // Check if the dataset exists + hid_t dataset = H5Dopen(file, dataset_path.c_str(), H5P_DEFAULT); + if (dataset >= 0) { + // Dataset already exists, close and return + H5Dclose(dataset); + H5Fclose(file); + return; + } + + // Create the empty dataset + hsize_t dims[1] = {0}; // Zero elements + hid_t dataspace = H5Screate_simple(1, dims, NULL); + dataset = H5Dcreate(file, dataset_path.c_str(), H5T_NATIVE_DOUBLE, + dataspace, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); + if (dataset < 0) { + H5Sclose(dataspace); + H5Fclose(file); + throw std::runtime_error("Error: Unable to create empty dataset: " + + dataset_path); + } + + // Close handles + H5Dclose(dataset); + H5Sclose(dataspace); + H5Fclose(file); + } +}; + +// --------------- read_array_from_h5_file TESTS --------------- +#pragma region read_array_from_h5_file tests + +TEST_F(HDF5ReadTest, ReadDoubleArrayFromH5) { std::string array_name = "/dials/processing/group_0/xyzobs.px.value"; - std::string flags = "/dials/processing/group_0/flags"; - // The test has been given the tests directory as the working directory - // so we can get the path to the test file. - std::filesystem::path cwd = std::filesystem::current_path(); - std::string fpath = cwd.generic_string(); - fpath.append("/data/cut_strong.refl"); + // Read array from the test HDF5 file std::vector xyzobs_px = - read_array_from_h5_file(fpath, array_name); - // check a random value + read_array_from_h5_file(test_file_path, array_name); + + // Check a specific value double expected_value = 528.86470588235295; EXPECT_EQ(xyzobs_px[10], expected_value); +} + +TEST_F(HDF5ReadTest, ReadSizeTArrayFromH5) { + std::string flags_name = "/dials/processing/group_0/flags"; + // Read array from the test HDF5 file std::vector flags_array = - read_array_from_h5_file(fpath, flags); - // check a random value + read_array_from_h5_file(test_file_path, flags_name); + + // Check a specific value std::size_t expected_flag_value = 32; EXPECT_EQ(flags_array[5], expected_flag_value); } + +// Test reading from a non-existent file +TEST_F(HDF5ReadTest, ReadFromNonExistentFile) { + std::string invalid_file = "invalid_file.h5"; + std::string dataset_name = "/some/dataset"; + + EXPECT_THROW(read_array_from_h5_file(invalid_file, dataset_name), + std::runtime_error); +} + +// Test reading a non-existent dataset +TEST_F(HDF5ReadTest, ReadNonExistentDataset) { + std::string invalid_dataset = "/this/does/not/exist"; + + EXPECT_THROW(read_array_from_h5_file(test_file_path, invalid_dataset), + std::runtime_error); +} + +// Test reading an empty dataset +TEST_F(HDF5ReadTest, ReadEmptyDataset) { + std::string empty_dataset = "/dials/processing/empty_dataset"; + + std::vector result = + read_array_from_h5_file(test_file_path, empty_dataset); + EXPECT_TRUE(result.empty()) << "Expected an empty vector for empty dataset."; +} + +// Test data type mismatch +TEST_F(HDF5ReadTest, ReadWithIncorrectType) { + std::string dataset = "/dials/processing/group_0/xyzobs.px.value"; + + // Try to read a float dataset as int (should fail) + EXPECT_THROW(read_array_from_h5_file(test_file_path, dataset), + std::runtime_error); +} + +#pragma endregion read_array_from_h5_file tests diff --git a/tests/test_write_h5_array.cxx b/tests/test_write_h5_array.cxx new file mode 100644 index 0000000..010aeb2 --- /dev/null +++ b/tests/test_write_h5_array.cxx @@ -0,0 +1,232 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +// Test fixture for HDF5-related tests +class HDF5Test : public ::testing::Test { +protected: + std::filesystem::path test_file_path; + + void SetUp() override { + test_file_path = std::filesystem::current_path() / "test_hdf5_file.h5"; + } + + void TearDown() override { + if (std::filesystem::exists(test_file_path)) { + std::filesystem::remove(test_file_path); + } + } +}; + +// --------------- traverse_or_create_groups TESTS --------------- +#pragma region traverse_or_create_groups tests + +TEST_F(HDF5Test, TraverseOrCreateGroups) { + // Create an HDF5 file + hid_t file = H5Fcreate(test_file_path.c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, + H5P_DEFAULT); + ASSERT_GE(file, 0) << "Failed to create HDF5 file."; + + try { + std::string group_path = "/dials/processing/group_0"; + hid_t final_group = traverse_or_create_groups(file, group_path); + ASSERT_GE(final_group, 0) << "Failed to create or open the final group."; + + // Verify group hierarchy exists + hid_t dials_group = H5Gopen(file, "dials", H5P_DEFAULT); + ASSERT_GE(dials_group, 0) << "Failed to open the 'dials' group."; + + hid_t processing_group = H5Gopen(dials_group, "processing", H5P_DEFAULT); + ASSERT_GE(processing_group, 0) << "Failed to open the 'processing' group."; + + hid_t group_0 = H5Gopen(processing_group, "group_0", H5P_DEFAULT); + ASSERT_GE(group_0, 0) << "Failed to open the 'group_0' group."; + + // Close all groups + H5Gclose(group_0); + H5Gclose(processing_group); + H5Gclose(dials_group); + H5Gclose(final_group); + } catch (const std::runtime_error &e) { + FAIL() << "Runtime error occurred: " << e.what(); + } + + // Close and validate file existence + H5Fclose(file); + ASSERT_TRUE(std::filesystem::exists(test_file_path)) + << "HDF5 file was not created."; +} + +#pragma endregion traverse_or_create_groups tests +// --------------- deduce_shape TESTS --------------- +#pragma region deduce_shape tests + +TEST(DeduceShapeTests, OneDimensionalVector) { + std::vector data = {1.0, 2.0, 3.0, 4.0}; + EXPECT_EQ(deduce_shape(data), (std::vector{4})); +} + +TEST(DeduceShapeTests, TwoDimensionalVector) { + std::vector> data = {{1.0, 2.0, 3.0}, {4.0, 5.0, 6.0}}; + EXPECT_EQ(deduce_shape(data), (std::vector{2, 3})); +} + +TEST(DeduceShapeTests, TwoDimensionalVectorArray) { + std::vector> data = { + {1.0, 2.0, 3.0}, {4.0, 5.0, 6.0}, {7.0, 8.0, 9.0}}; + EXPECT_EQ(deduce_shape(data), (std::vector{3, 3})); +} + +TEST(DeduceShapeTests, ThreeDimensionalVector) { + std::vector>> data = {{{1, 2}, {3, 4}}, + {{5, 6}, {7, 8}}}; + EXPECT_EQ(deduce_shape(data), (std::vector{2, 2, 2})); +} + +TEST(DeduceShapeTests, EmptyContainer) { + std::vector> data = {}; + EXPECT_EQ(deduce_shape(data), (std::vector{0})); +} + +TEST(DeduceShapeTests, MixedSizeInnerContainers) { + std::vector> data = {{1.0, 2.0}, {3.0, 4.0, 5.0}}; + EXPECT_THROW(deduce_shape(data), std::exception); +} + +#pragma endregion deduce_shape tests +// --------------- flatten TESTS --------------- +#pragma region flatten tests + +TEST(FlattenTests, OneDimensionalVector) { + std::vector data = {1.0, 2.0, 3.0, 4.0}; + EXPECT_EQ(flatten(data), data); +} + +TEST(FlattenTests, TwoDimensionalVector) { + std::vector> data = {{1.0, 2.0, 3.0}, {4.0, 5.0, 6.0}}; + EXPECT_EQ(flatten(data), (std::vector{1.0, 2.0, 3.0, 4.0, 5.0, 6.0})); +} + +TEST(FlattenTests, TwoDimensionalVectorArray) { + std::vector> data = { + {1.0, 2.0, 3.0}, {4.0, 5.0, 6.0}, {7.0, 8.0, 9.0}}; + EXPECT_EQ(flatten(data), + (std::vector{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0})); +} + +TEST(FlattenTests, ThreeDimensionalVector) { + std::vector>> data = {{{1, 2}, {3, 4}}, + {{5, 6}, {7, 8}}}; + EXPECT_EQ(flatten(data), (std::vector{1, 2, 3, 4, 5, 6, 7, 8})); +} + +TEST(FlattenTests, EmptyContainer) { + std::vector> data = {}; + EXPECT_EQ(flatten(data), (std::vector{})); +} + +#pragma endregion flatten tests +// --------------- write_h5 TESTS --------------- +#pragma region write_h5 tests + +TEST_F(HDF5Test, WriteOneDimensionalVector) { + std::string dataset_path = "/group_1/dataset_1d"; + std::vector data = {1.0, 2.0, 3.0, 4.0}; + + write_data_to_h5_file(test_file_path, dataset_path, data); + EXPECT_EQ(read_array_from_h5_file(test_file_path, dataset_path), + data); +} + +TEST_F(HDF5Test, WriteTwoDimensionalVector) { + std::string dataset_path = "/group_2/dataset_2d"; + std::vector> data = {{1.0, 2.0, 3.0}, {4.0, 5.0, 6.0}}; + + write_data_to_h5_file(test_file_path, dataset_path, data); + EXPECT_EQ(read_array_from_h5_file(test_file_path, dataset_path), + flatten(data)); +} + +TEST_F(HDF5Test, WriteEmptyDataset) { + std::string dataset_path = "/group_empty/dataset_empty"; + std::vector data = {}; + + write_data_to_h5_file(test_file_path, dataset_path, data); + EXPECT_TRUE( + read_array_from_h5_file(test_file_path, dataset_path).empty()); +} + +// Test writing to a file that already exists +TEST_F(HDF5Test, WriteToExistingFile) { + hid_t file = H5Fcreate(test_file_path.c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, + H5P_DEFAULT); + ASSERT_GE(file, 0) << "Failed to create HDF5 file."; + H5Fclose(file); + + std::string dataset_path = "/existing_group/existing_dataset"; + std::vector data = {1.0, 2.0, 3.0, 4.0}; + + write_data_to_h5_file(test_file_path, dataset_path, data); + EXPECT_EQ(read_array_from_h5_file(test_file_path, dataset_path), + data); +} + +// Test writing to a group that already exists +TEST_F(HDF5Test, WriteToExistingGroup) { + hid_t file = H5Fcreate(test_file_path.c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, + H5P_DEFAULT); + ASSERT_GE(file, 0) << "Failed to create HDF5 file."; + + hid_t group = + H5Gcreate(file, "/group_1", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); + ASSERT_GE(group, 0) << "Failed to create group."; + H5Gclose(group); + H5Fclose(file); + + std::string dataset_path = "/group_1/dataset_1"; + std::vector data = {10.0, 20.0, 30.0, 40.0}; + + write_data_to_h5_file(test_file_path, dataset_path, data); + EXPECT_EQ(read_array_from_h5_file(test_file_path, dataset_path), + data); +} + +// Test writing to a dataset that already exists +TEST_F(HDF5Test, WriteToExistingDataset) { + hid_t file = H5Fcreate(test_file_path.c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, + H5P_DEFAULT); + ASSERT_GE(file, 0) << "Failed to create HDF5 file."; + + hid_t group = + H5Gcreate(file, "/group_2", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); + ASSERT_GE(group, 0) << "Failed to create group."; + + hsize_t dims[1] = {4}; + hid_t dataspace = H5Screate_simple(1, dims, NULL); + hid_t dataset = H5Dcreate(group, "dataset_existing", H5T_NATIVE_DOUBLE, + dataspace, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); + ASSERT_GE(dataset, 0) << "Failed to create dataset."; + + std::vector initial_data = {1.1, 2.2, 3.3, 4.4}; + H5Dwrite(dataset, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, + initial_data.data()); + + H5Dclose(dataset); + H5Sclose(dataspace); + H5Gclose(group); + H5Fclose(file); + + // Now attempt to overwrite the dataset + std::vector new_data = {5.5, 6.6, 7.7, 8.8}; + write_data_to_h5_file(test_file_path, "/group_2/dataset_existing", new_data); + EXPECT_EQ(read_array_from_h5_file(test_file_path, + "/group_2/dataset_existing"), + new_data); +} + +#pragma endregion write_h5 tests