diff --git a/tdms/include/hdf5_io.h b/tdms/include/hdf5_io.h deleted file mode 100644 index c381eeebc..000000000 --- a/tdms/include/hdf5_io.h +++ /dev/null @@ -1,217 +0,0 @@ -/** - * @file hdf5_io.h - * @brief Helper classes for HDF5 file I/O. - * @details The main classes are `HDF5Reader` and `HDF5Writer` with the methods - * `HDF5Reader::read` and `HDF5Writer::write` respectively. - */ -#pragma once - -#include -#include -#include -#include -#include - -#include -#include - -#include "arrays.h" -#include "cell_coordinate.h" -#include "fdtd_grid_initialiser.h" - -/** - * @brief The base class for HDF5 I/O. - * @details Common functionality and wraps handling the std::unique_ptr to hold - * the H5::File object. - */ -class HDF5Base { - -protected: - std::string filename_; /**< The name of the file. */ - std::shared_ptr file_; /**< Pointer to the underlying H5::File. */ - - /** - * @brief Construct a new HDF5{Reader/Writer} for a named file. - * @param filename The name of the file. - * @param mode The H5 file access mode (RDONLY for a HDF5Reader, TRUNC for a - * HDF5Writer.) - * @throws H5::FileIException if the file doesn't exist or can't be created. - */ - HDF5Base(const std::string &filename, int mode = H5F_ACC_RDONLY) - : filename_(filename) { - file_ = std::make_unique(filename, mode); - } - - /** - * @brief Destructor closes the file. - * @details Closes file when HDF5Reader(or HDF5Writer) goes out of scope. - * Since the file pointer is a smart pointer it is deallocated automatically. - */ - ~HDF5Base() { file_->close(); } - -public: - /** - * @brief Get the name of the file. - * @return std::string the filename. - */ - std::string get_filename() const { return filename_; } - - /** - * @brief Get the names of all datasets (data tables) currently in the file. - * @return std::vector A vector of their names. - */ - std::vector get_datanames() const; - - /** - * @brief Print the names of all datasets to std::out. - */ - void ls() const; - - /** - * @brief Return shape/dimensionality information about the array data stored - * with `name`. - * @param dataname The name of the data table. - * @return IJKDimensions The dimensions of the data. - */ - // IJKDimensions shape_of(const std::string &dataname) const; - std::vector shape_of(const std::string &dataname) const; - - /** - * @brief Checks the file is a valid HDF5 file, and everything is OK. - * TODO: Can perhaps remove. - * - * @return true If all is well. - * @return false Otherwise. - */ - bool is_ok() const; -}; - -/** - * @brief Class wrapper of the reading of HDF5 format files. - * @details Opens files in readonly and retrieves the datasets (in our case - * **double, but can be anything in general). - */ -class HDF5Reader : public HDF5Base { - -public: - /** - * @brief Construct a new HDF5Reader for a named file. - * @param filename The name of the file. - * @throws H5::FileIException if the file can't be created. - */ - HDF5Reader(const std::string &filename) - : HDF5Base(filename, H5F_ACC_RDONLY) {} - - /** - * @brief Reads a named dataset from the HDF5 file. - * @param dataname The name of the datset to be read. - * @param data A pointer to an array of correct size. - */ - // template - // void read(const std::string &dataname, T *data) const; - template - void read(const std::string &dataset_name, T *data) const { - spdlog::debug("Reading {} from file: {}", dataset_name, filename_); - - // get the dataset and dataspace - H5::DataSet dataset = file_->openDataSet(dataset_name); - H5::DataSpace dataspace = dataset.getSpace(); - - // now get the data type - dataset.read(data, dataset.getDataType()); - spdlog::trace("Read successful."); - } - - template - void read_field_from_struct(const std::string &struct_name, - const std::string &field_name, T *data) const { - spdlog::debug("Reading {} from file: {}", struct_name, filename_); - - // Structs are saved as groups, so we need to fetch the group this struct is - // contained in - H5::Group structure_array = file_->openGroup(struct_name); - // Then fetch the requested data and read it into the buffer provided - H5::DataSet requested_field = structure_array.openDataSet(field_name); - requested_field.read(data, requested_field.getDataType()); - } - - template - void read(const std::string &dataset_name, Matrix &data_location) const { - spdlog::debug("Reading {} from file: {}", dataset_name, filename_); - - std::vector dimensions = shape_of(dataset_name); - if (dimensions.size() != 2) { - throw std::runtime_error( - "Cannot read " + dataset_name + " into a 2D matrix, it has " + - std::to_string(dimensions.size()) + " dimensions"); - } - int n_rows = dimensions[0]; - int n_cols = dimensions[1]; - - SPDLOG_DEBUG("n_rows = {}; n_cols = {}", n_rows, n_cols); - T *buff = (T *) malloc(n_rows * n_cols * sizeof(T)); - read(dataset_name, buff); - - data_location.allocate(n_rows, n_cols); - for (unsigned int i = 0; i < n_rows; i++) { - for (unsigned int j = 0; j < n_cols; j++) { - data_location[i][j] = buff[i * n_cols + j]; - } - } - return; - } - - /* - //void read(const std::string &dataset_name, mxArray*) const { - - // This method will take in the fdtdGridInit... object, assign the pointer - // member, and then run the object's - return; - } - */ -}; - -class HDF5Writer : public HDF5Base { - -public: - /** - * @brief Construct a new HDF5Writer, creates a file. - * @param filename The name of the file to be created. - * @throws H5::FileIException if the file can't be created. - */ - HDF5Writer(const std::string &filename) : HDF5Base(filename, H5F_ACC_TRUNC) {} - - /** - * @brief Write `data` to the file with `dataname`. - * - * @param dataname The name of the data table. - * @param data The data itself. - * @param size The size of the data array. - * @param dimensions The number of dimensions of the array. - */ - void write(const std::string &dataname, double *data, int size, - hsize_t *dimensions); - - /** - * @brief Write `data` to the file with `dataname`. - * - * @param dataname The name of the data table. - * @param data The data itself. - * @param size The size of the data array. - * @param dimensions The number of dimensions of the array. - */ - template - void write(const std::string &dataname, const Matrix &data) { - int n_cols = data.get_n_cols(); - int n_rows = data.get_n_rows(); - hsize_t dimension[2] = {static_cast(n_rows), - static_cast(n_cols)}; - T *buff = (T *) malloc(n_rows * n_cols * sizeof(T)); - for (unsigned int i = 0; i < n_rows; i++) { - for (unsigned int j = 0; j < n_cols; j++) { - buff[i * n_cols + j] = data[i][j]; - } - } - write(dataname, buff, 2, dimension); - } -}; diff --git a/tdms/include/hdf5_io/hdf5_base.h b/tdms/include/hdf5_io/hdf5_base.h new file mode 100644 index 000000000..865f93eb4 --- /dev/null +++ b/tdms/include/hdf5_io/hdf5_base.h @@ -0,0 +1,92 @@ +/** + * @file hdf5_io.h + * @brief Helper classes for HDF5 file I/O. + * @details The main classes are `HDF5Reader` and `HDF5Writer` with the methods + * `HDF5Reader::read` and `HDF5Writer::write` respectively. + */ +#pragma once + +#include +#include +#include + +#include + +#include "cell_coordinate.h" + +/** + * @brief Convert from a vector of HDF5's hsize_t back to our struct of ints. + * @note Local scope utility function as only this code needs to interact with + * the HDF5 H5Cpp library. + * + * @param dimensions a 1, 2, or 3 element vector of dimensions. + * @return ijk The dimensions in a struct. + */ +ijk to_ijk(const std::vector dimensions); + +/** + * @brief The base class for HDF5 I/O. + * @details Common functionality and wraps handling the std::unique_ptr to hold + * the H5::File object. + */ +class HDF5Base { + +protected: + std::string filename_; /**< The name of the file. */ + std::shared_ptr file_; /**< Pointer to the underlying H5::File. */ + + /** + * @brief Construct a new HDF5{Reader/Writer} for a named file. + * @param filename The name of the file. + * @param mode The H5 file access mode (RDONLY for a HDF5Reader, TRUNC for a + * HDF5Writer.) + * @throws H5::FileIException if the file doesn't exist or can't be created. + */ + HDF5Base(const std::string &filename, int mode = H5F_ACC_RDONLY) + : filename_(filename) { + file_ = std::make_unique(filename, mode); + } + + /** + * @brief Destructor closes the file. + * @details Closes file when HDF5Reader(or HDF5Writer) goes out of scope. + * Since the file pointer is a smart pointer it is deallocated automatically. + */ + ~HDF5Base() { file_->close(); } + +public: + /** + * @brief Get the name of the file. + * @return std::string the filename. + */ + std::string get_filename() const { return filename_; } + + /** + * @brief Get the names of all datasets (data tables) currently in the file. + * @return std::vector A vector of their names. + */ + std::vector get_datanames() const; + + /** + * @brief Print the names of all datasets to std::out. + */ + void ls() const; + + /** + * @brief Return shape/dimensionality information about the array data stored + * with `name`. + * @param dataname The name of the data table. + * @return IJKDimensions The dimensions of the data. + */ + // IJKDimensions shape_of(const std::string &dataname) const; + std::vector shape_of(const std::string &dataname) const; + + /** + * @brief Checks the file is a valid HDF5 file, and everything is OK. + * TODO: Can perhaps remove. + * + * @return true If all is well. + * @return false Otherwise. + */ + bool is_ok() const; +}; diff --git a/tdms/include/hdf5_io/hdf5_reader.h b/tdms/include/hdf5_io/hdf5_reader.h new file mode 100644 index 000000000..e107be161 --- /dev/null +++ b/tdms/include/hdf5_io/hdf5_reader.h @@ -0,0 +1,89 @@ +#pragma once + +#include "hdf5_io/hdf5_base.h" + +#include "arrays.h" +#include "interface.h" + +/** + * @brief Class wrapper of the reading of HDF5 format files. + * @details Opens files in readonly and retrieves the datasets (in our case + * **double, but can be anything in general). + */ +class HDF5Reader : public HDF5Base { + +public: + /** + * @brief Construct a new HDF5Reader for a named file. + * @param filename The name of the file. + * @throws H5::FileIException if the file can't be created. + */ + HDF5Reader(const std::string &filename) + : HDF5Base(filename, H5F_ACC_RDONLY) {} + + /** + * @brief Reads a named dataset from the HDF5 file. + * @param dataname The name of the datset to be read. + * @param data A pointer to an array of correct size. + */ + // template + // void read(const std::string &dataname, T *data) const; + template + void read(const std::string &dataset_name, T *data) const { + spdlog::debug("Reading {} from file: {}", dataset_name, filename_); + + // get the dataset and dataspace + H5::DataSet dataset = file_->openDataSet(dataset_name); + H5::DataSpace dataspace = dataset.getSpace(); + + // now get the data type + dataset.read(data, dataset.getDataType()); + spdlog::trace("Read successful."); + } + + template + void read_field_from_struct(const std::string &struct_name, + const std::string &field_name, T *data) const { + spdlog::debug("Reading {} from file: {}", struct_name, filename_); + + // Structs are saved as groups, so we need to fetch the group this struct is + // contained in + H5::Group structure_array = file_->openGroup(struct_name); + // Then fetch the requested data and read it into the buffer provided + H5::DataSet requested_field = structure_array.openDataSet(field_name); + requested_field.read(data, requested_field.getDataType()); + } + + template + void read(const std::string &dataset_name, Matrix &data_location) const { + spdlog::debug("Reading {} from file: {}", dataset_name, filename_); + + std::vector dimensions = shape_of(dataset_name); + if (dimensions.size() != 2) { + throw std::runtime_error( + "Cannot read " + dataset_name + " into a 2D matrix, it has " + + std::to_string(dimensions.size()) + " dimensions"); + } + int n_rows = dimensions[0]; + int n_cols = dimensions[1]; + + SPDLOG_DEBUG("n_rows = {}; n_cols = {}", n_rows, n_cols); + T *buff = (T *) malloc(n_rows * n_cols * sizeof(T)); + read(dataset_name, buff); + + data_location.allocate(n_rows, n_cols); + for (unsigned int i = 0; i < n_rows; i++) { + for (unsigned int j = 0; j < n_cols; j++) { + data_location[i][j] = buff[i * n_cols + j]; + } + } + return; + } + + void read(const std::string &plane, InterfaceComponent *ic) const; + InterfaceComponent read(const std::string &plane) const { + InterfaceComponent ic; + read(plane, &ic); + return ic; + } +}; diff --git a/tdms/include/hdf5_io/hdf5_writer.h b/tdms/include/hdf5_io/hdf5_writer.h new file mode 100644 index 000000000..433d59dd1 --- /dev/null +++ b/tdms/include/hdf5_io/hdf5_writer.h @@ -0,0 +1,50 @@ +#pragma once + +#include "hdf5_io/hdf5_base.h" + +#include "arrays.h" + +class HDF5Writer : public HDF5Base { + +public: + /** + * @brief Construct a new HDF5Writer, creates a file. + * @param filename The name of the file to be created. + * @throws H5::FileIException if the file can't be created. + */ + HDF5Writer(const std::string &filename) : HDF5Base(filename, H5F_ACC_TRUNC) {} + + /** + * @brief Write `data` to the file with `dataname`. + * + * @param dataname The name of the data table. + * @param data The data itself. + * @param size The size of the data array. + * @param dimensions The number of dimensions of the array. + */ + void write(const std::string &dataname, double *data, int size, + hsize_t *dimensions); + + /** + * @brief Write `data` to the file with `dataname`. + * + * @param dataname The name of the data table. + * @param data The data itself. + * @param size The size of the data array. + * @param dimensions The number of dimensions of the array. + */ + template + void write(const std::string &dataname, const Matrix &data) { + int n_cols = data.get_n_cols(); + int n_rows = data.get_n_rows(); + hsize_t dimension[2] = {static_cast(n_rows), + static_cast(n_cols)}; + T *buff = (T *) malloc(n_rows * n_cols * sizeof(T)); + for (unsigned int i = 0; i < n_rows; i++) { + for (unsigned int j = 0; j < n_cols; j++) { + buff[i * n_cols + j] = data[i][j]; + } + } + write(dataname, buff, 2, dimension); + } +}; diff --git a/tdms/include/interface.h b/tdms/include/interface.h index bbf44bb4a..f565b5cd0 100644 --- a/tdms/include/interface.h +++ b/tdms/include/interface.h @@ -7,10 +7,32 @@ #include "mat_io.h" +/** + * @brief Defines a plane over which a source/boundary condition is (or is not) + * to be applied. + * + * There are 6 planes on which a source condition can be applied; I0, I1, J0, + * J1, K0, and K1. + * The {I,J,K} character indicates the axial direction to which the plane is + * perpendicular, whilst the {0,1} character indicates whether this is the first + * or second such plane perpendicular to that axial direction. + * + * The index member stores the value of the (constant) Yee cell index of all Yee + * cells that lie in the plane defined. That is, index is the I-index of all Yee + * cells in the I0 or I1 planes, the J-index for the J0 and J1 planes, and the + * K-index of the K0 and K1 planes. + * + * The apply member flags whether an interface condition is to be applied across + * that particular interface/plane. + */ class InterfaceComponent { public: - bool apply; - int index; + /*! Whether or not a source or boundary condition is applied at this + * interface */ + bool apply = false; + /*! The value of the constant Yee-cell index for cells in this plane */ + int index = 0; + InterfaceComponent() = default; InterfaceComponent(const mxArray *ptr, const std::string &name); }; diff --git a/tdms/src/hdf5_io.cpp b/tdms/src/hdf5_io/hdf5_base.cpp similarity index 51% rename from tdms/src/hdf5_io.cpp rename to tdms/src/hdf5_io/hdf5_base.cpp index ca7a8c5ec..7f0aeabc4 100644 --- a/tdms/src/hdf5_io.cpp +++ b/tdms/src/hdf5_io/hdf5_base.cpp @@ -1,20 +1,18 @@ -#include "hdf5_io.h" -#include "cell_coordinate.h" +/** + * @file hdf5_io.cpp + * @authors Sam Cunliffe, William Graham + * @brief Common HDF5 I/O methods abstracted to the base class. + */ +#include "hdf5_io/hdf5_base.h" -#include #include #include +#include #include -/** - * @brief Convert from a vector of HDF5's hsize_t back to our struct of ints. - * @note Local scope utility function as only this code needs to interact with - * the HDF5 H5Cpp library. - * - * @param dimensions a 1, 2, or 3 element vector of dimensions. - * @return ijk The dimensions in a struct. - */ +using namespace std; + ijk to_ijk(const std::vector dimensions) { unsigned int rank = dimensions.size(); ijk out; @@ -25,30 +23,8 @@ ijk to_ijk(const std::vector dimensions) { return out; } -/****************************************************************************** - * HDF5Writer - */ -void HDF5Writer::write(const std::string &dataset_name, double *data, int size, - hsize_t *dimensions) { - spdlog::debug("Writing {} to file: {}", dataset_name, filename_); - - // declare a dataspace - H5::DataSpace dataspace(size, dimensions); - H5::DataType datatype(H5::PredType::NATIVE_DOUBLE); - - // write the data to the dataset object in the file - H5::DataSet dataset = file_->createDataSet(dataset_name, datatype, dataspace); - dataset.write(data, H5::PredType::NATIVE_DOUBLE); - spdlog::trace("Write successful."); -} - -/****************************************************************************** - * HDF5Base - * - * Common HDF5 I/O methods abstracted to the base class. - */ -std::vector HDF5Base::get_datanames() const { - std::vector names; +vector HDF5Base::get_datanames() const { + vector names; // iterate over all objects in the file for (unsigned int i = 0; i < file_->getNumObjs(); i++) { @@ -64,14 +40,14 @@ std::vector HDF5Base::get_datanames() const { } void HDF5Base::ls() const { - std::vector names = this->get_datanames(); - for (auto name : names) std::cout << name << std::endl; + vector names = this->get_datanames(); + for (auto name : names) cout << name << endl; return; } // IJKDimensions HDF5Base::shape_of(const std::string &dataname) const { // return to_ijk(dimensions); -std::vector HDF5Base::shape_of(const std::string &dataname) const { +vector HDF5Base::shape_of(const string &dataname) const { SPDLOG_DEBUG("shape_of"); // get the dataset and dataspace (contains dimensionality info) @@ -80,7 +56,7 @@ std::vector HDF5Base::shape_of(const std::string &dataname) const { // need the rank in order to declare the vector size int rank = dataspace.getSimpleExtentNdims(); - std::vector dimensions(rank); + vector dimensions(rank); dataspace.getSimpleExtentDims(dimensions.data(), nullptr); return dimensions; } diff --git a/tdms/src/hdf5_io/hdf5_reader.cpp b/tdms/src/hdf5_io/hdf5_reader.cpp new file mode 100644 index 000000000..d002bc0f5 --- /dev/null +++ b/tdms/src/hdf5_io/hdf5_reader.cpp @@ -0,0 +1,14 @@ +#include "hdf5_io/hdf5_reader.h" + +using namespace std; + +void HDF5Reader::read(const string &plane, InterfaceComponent *ic) const { + // Read the InterfaceComponent in as a 2-element double array + double read_buffer[2]; + read_field_from_struct("interface", plane, read_buffer); + // The index that is read in should have 1 subtracted from it, to account for + // MATLAB indexing + ic->index = max((int) read_buffer[0] - 1, 0); + // The apply flag should be cast from the double that is read in + ic->apply = (bool) read_buffer[1]; +} diff --git a/tdms/src/hdf5_io/hdf5_writer.cpp b/tdms/src/hdf5_io/hdf5_writer.cpp new file mode 100644 index 000000000..f424b35ed --- /dev/null +++ b/tdms/src/hdf5_io/hdf5_writer.cpp @@ -0,0 +1,19 @@ +#include "hdf5_io/hdf5_writer.h" + +#include + +using namespace std; + +void HDF5Writer::write(const string &dataset_name, double *data, int size, + hsize_t *dimensions) { + spdlog::debug("Writing {} to file: {}", dataset_name, filename_); + + // declare a dataspace + H5::DataSpace dataspace(size, dimensions); + H5::DataType datatype(H5::PredType::NATIVE_DOUBLE); + + // write the data to the dataset object in the file + H5::DataSet dataset = file_->createDataSet(dataset_name, datatype, dataspace); + dataset.write(data, H5::PredType::NATIVE_DOUBLE); + spdlog::trace("Write successful."); +} diff --git a/tdms/tests/include/unit_test_utils.h b/tdms/tests/include/unit_test_utils.h index 4288a2c17..74e1c6125 100644 --- a/tdms/tests/include/unit_test_utils.h +++ b/tdms/tests/include/unit_test_utils.h @@ -11,6 +11,44 @@ #include "globals.h" +using tdms_math_constants::DCPI; + +namespace tdms_unit_test_data { + +#ifdef CMAKE_SOURCE_DIR +inline std::string + tdms_object_data(std::string(CMAKE_SOURCE_DIR) + + "/tests/unit/hdf5_and_tdms_objects/class_data.mat"); +#else +inline std::string + tdms_object_data(std::filesystem::current_path() / + "../tests/unit/hdf5_and_tdms_objects/class_data.mat"); +#endif + +/* The struct_testdata is a .mat file containing a single structure array, +example_struct. This structure array has the following data. + +MEMBER: value, MATLAB type +double_no_decimal: 1.0, double +double_half: 0.5, double +string: 'tdms', char array +boolean: 1, logical +uint_345: 3 * 4 * 5 array of 1s, uint8 +double_22: [0.25, 0.5; 0.75, 1.], double +complex_22: [0., -i; i, 0], complex +*/ +#ifdef CMAKE_SOURCE_DIR +inline std::string + struct_testdata(std::string(CMAKE_SOURCE_DIR) + + "/tests/unit/hdf5_io_tests/structure_array.mat"); +#else +inline std::string + struct_testdata(std::filesystem::current_path() / + "../tests/unit/hdf5_io_tests/structure_array.mat"); +#endif + +}// namespace tdms_unit_test_data + namespace tdms_tests { inline double TOLERANCE = 1e-16;//< Floating-point comparison tolerance @@ -153,4 +191,21 @@ inline std::filesystem::path create_tmp_dir() { return path; } +/** @brief Returns the char represented by a uint16 */ +inline char uint16_to_char(const uint16_t &repr) { return *((char *) &repr); } + +/** + * @brief Returns the string composed of the characters represented by + * subsequent uint16s. + * + * @param repr Buffer of uint16s that represent individual characters + * @param buffer_length Length of the buffer + * @return string The string composed of the converted characters + */ +inline std::string uint16s_to_string(uint16_t *repr, int buffer_length) { + std::string output; + for (int i = 0; i < buffer_length; i++) { output += uint16_to_char(repr[i]); } + return output; +} + }// namespace tdms_tests diff --git a/tdms/tests/unit/hdf5_and_tdms_objects/class_data.mat b/tdms/tests/unit/hdf5_and_tdms_objects/class_data.mat new file mode 100644 index 000000000..37d7be667 Binary files /dev/null and b/tdms/tests/unit/hdf5_and_tdms_objects/class_data.mat differ diff --git a/tdms/tests/unit/hdf5_and_tdms_objects/create_tdms_object_data.m b/tdms/tests/unit/hdf5_and_tdms_objects/create_tdms_object_data.m new file mode 100644 index 000000000..992d46c25 --- /dev/null +++ b/tdms/tests/unit/hdf5_and_tdms_objects/create_tdms_object_data.m @@ -0,0 +1,25 @@ +%% This script creates the class_data.mat file for use in the hdf5 tests for reading in TDMS objects. +close all; +clear; + +% % interface % interface is a struct with fields { + I, J, K +} {0, 1}, corresponding to the planes at which a source field is introduced.% + Each field is a 1 - + by - 2 array of doubles; +the first element being the % + {I, J, K} index of the Yee cell in which the particular plane lies.The % + second element is cast to a bool and indicates whether any boundary % + conditions are to be applied on said plane.interface = struct(); +interface.I0 = [1 0]; +% I0 in the I = 1 plane, no source condition interface.I1 = [4 1]; +% I1 in the I = 4 plane, source condition applied interface.J0 = [2 0]; +% J0 in the J = 2 plane, no source condition interface.J1 = [5 0]; +% J1 in the J = 5 plane, no source condition interface.K0 = [3 1]; +% K0 in the K = 3 plane, source condition applied interface.K1 = [6 1]; %K1 in the K=6 plane, source condition applied + +%% save variables to the file we need +% Save the files to the expected filename for the unit tests to read the +% data back in. + +save("class_data.mat", "-v7.3"); diff --git a/tdms/tests/unit/hdf5_and_tdms_objects/test_hdf5_InterfaceComponent.cpp b/tdms/tests/unit/hdf5_and_tdms_objects/test_hdf5_InterfaceComponent.cpp new file mode 100644 index 000000000..101b3da4a --- /dev/null +++ b/tdms/tests/unit/hdf5_and_tdms_objects/test_hdf5_InterfaceComponent.cpp @@ -0,0 +1,69 @@ +/** + * @file test_hdf5_io.cpp + * @brief Tests of the HDF5 file I/O functionality. + */ +#include "hdf5_io/hdf5_reader.h" + +// external +#include +#include + +// tdms +#include "interface.h" +#include "unit_test_utils.h" + +using namespace std; +using tdms_unit_test_data::tdms_object_data; + +/** + * @brief Check that HDF5 can read an InterfaceComponent from a HDF5 file. + * + * .mat files save InterfaceComponents as datasets of the "interface" group. + * + * In the .mat file, the planes are saved as: + * interface.I0 = [1 0]; %I0 in the I=1 plane, no source condition + * interface.I1 = [4 1]; %I1 in the I=4 plane, source condition applied + * interface.J0 = [2 0]; %J0 in the J=2 plane, no source condition + * interface.J1 = [5 0]; %J1 in the J=5 plane, no source condition + * interface.K0 = [3 1]; %K0 in the K=3 plane, source condition applied + * interface.K1 = [6 1]; %K1 in the K=6 plane, source condition applied + * + * Do not forget the index offset when initialising from MATLAB indices! The + * first element in each of these arrays should be offset by -1 upon being read + * in. + * + * We will explicitly compare bools to false in what follows to make it + * explicitly clear that we are testing that the values read into our object + * _match_ those we expect from the data file. + */ +TEST_CASE("HDF5: Read InterfaceComponent") { + HDF5Reader MATFile(tdms_object_data); + + SECTION("Read into existing InterfaceComponent") { + InterfaceComponent I0, J0, K0; + + MATFile.read("I0", &I0); + MATFile.read("J0", &J0); + MATFile.read("K0", &K0); + + bool I0_correct = (I0.index == 0) && (I0.apply == false); + bool J0_correct = (J0.index == 1) && (J0.apply == false); + bool K0_correct = (K0.index == 2) && (K0.apply == true); + CHECK(I0_correct); + CHECK(J0_correct); + CHECK(K0_correct); + } + + SECTION("Return InterfaceComponent object") { + InterfaceComponent I1 = MATFile.read("I1"); + InterfaceComponent J1 = MATFile.read("J1"); + InterfaceComponent K1 = MATFile.read("K1"); + + bool I1_correct = (I1.index == 3) && (I1.apply == true); + bool J1_correct = (J1.index == 4) && (J1.apply == false); + bool K1_correct = (K1.index == 5) && (K1.apply == true); + CHECK(I1_correct); + CHECK(J1_correct); + CHECK(K1_correct); + } +} diff --git a/tdms/tests/unit/hdf5_and_tdms_objects/test_hdf5_Matrix.cpp b/tdms/tests/unit/hdf5_and_tdms_objects/test_hdf5_Matrix.cpp new file mode 100644 index 000000000..733731b1b --- /dev/null +++ b/tdms/tests/unit/hdf5_and_tdms_objects/test_hdf5_Matrix.cpp @@ -0,0 +1,56 @@ +/** + * @file test_hdf5_Matrix.cpp + * @brief Tests of the HDF5 file I/O functionality when reading/writing Matrix + * objects. + */ +#include "hdf5_io/hdf5_reader.h" +#include "hdf5_io/hdf5_writer.h" + +#include + +// external +#include +#include + +// tdms +#include "arrays.h" +#include "unit_test_utils.h" + +using std::filesystem::remove_all; +using tdms_tests::create_tmp_dir; + +TEST_CASE("HDF5: Read/Write Matrix") { + // test-case wide setup - temporary directory + auto tmp = create_tmp_dir(); + + SECTION("5-by-6 2D array [double]") { + SPDLOG_INFO("5-by-6 2D array"); + Matrix counting_matrix(5, 6); + for (int i = 0; i < 5; i++) { + for (int j = 0; j < 6; j++) { counting_matrix[i][j] = 6. * i + j; } + } + Matrix read_back; + + { + HDF5Writer f1(tmp.string() + "/five-by-six.h5"); + f1.write("five-by-six", counting_matrix); + } + { + SPDLOG_DEBUG("About to read..."); + HDF5Reader f2(tmp.string() + "/five-by-six.h5"); + f2.read("five-by-six", read_back); + } + + for (unsigned int i = 0; i < 5; i++) { + for (unsigned int j = 0; j < 6; j++) { + SPDLOG_INFO("Checking {} == {}", counting_matrix[i][j], + read_back[i][j]); + CHECK(counting_matrix[i][j] == Catch::Approx(read_back[i][j])); + } + } + } + + // teardown - remove temporary directory and all files + SPDLOG_DEBUG("Removing temporary directory."); + remove_all(tmp); +} diff --git a/tdms/tests/unit/hdf5_io_tests/create_structure_array.m b/tdms/tests/unit/hdf5_io_tests/create_structure_array.m new file mode 100644 index 000000000..d4f3e5128 --- /dev/null +++ b/tdms/tests/unit/hdf5_io_tests/create_structure_array.m @@ -0,0 +1,19 @@ +%% This script creates the structure_array.mat file for use in the hdf5 tests for reading in structure arrays. +close all; +clear; + +example_struct = struct(); + +example_struct.double_no_decimal = 1.; +example_struct.double_half = 0.5; +example_struct.string = 'tdms'; +example_struct.boolean = true; +example_struct.uint_345 = uint8(ones(3, 4, 5)); +example_struct.double_22 = [0.25, 0.5; 0.75, 1.]; +example_struct.complex_22 = [0., -1.i; 1.i, 0.]; + +%% save variables to the file we need +% Save the files to the expected filename for the unit tests to read the +% data back in. + +save("structure_array.mat", "example_struct", "-v7.3"); diff --git a/tdms/tests/unit/structure_array.mat b/tdms/tests/unit/hdf5_io_tests/structure_array.mat similarity index 100% rename from tdms/tests/unit/structure_array.mat rename to tdms/tests/unit/hdf5_io_tests/structure_array.mat diff --git a/tdms/tests/unit/hdf5_io_tests/test_hdf5_io.cpp b/tdms/tests/unit/hdf5_io_tests/test_hdf5_io.cpp new file mode 100644 index 000000000..031b4c472 --- /dev/null +++ b/tdms/tests/unit/hdf5_io_tests/test_hdf5_io.cpp @@ -0,0 +1,150 @@ +/** + * @file test_hdf5_io.cpp + * @brief Tests of the HDF5 file I/O functionality. + */ +#include "hdf5_io/hdf5_reader.h" +#include "hdf5_io/hdf5_writer.h" + +// std +#include +#include + +// external +#include +#include +#include + +// tdms +#include "unit_test_utils.h" + +using namespace std; +using tdms_tests::create_tmp_dir; + +TEST_CASE("Test file I/O construction/destruction.") { + // test-case wide setup - temporary directory + auto tmp = create_tmp_dir(); + + SECTION("Check file creation.") { + HDF5Writer f(tmp.string() + "/test_file_constructor.h5"); + CHECK(f.is_ok()); + } + + SECTION("Check all reasonable file extensions are OK.") { + for (auto extension : {".hdf5", ".h5", ".mat"}) { + { + HDF5Writer fw(tmp.string() + "/test_file" + extension); + CHECK(fw.is_ok()); + + }// Destructor called as we leave scope. + + HDF5Reader fr(tmp.string() + "/test_file" + extension); + CHECK(fr.is_ok()); + } + } + + SECTION("Check can't open nonexistent file.") { + CHECK_THROWS(HDF5Reader(tmp.string() + "/this_file_doesnt_exist.h5")); + } + + SECTION("Check can't read nonexistent data.") { + { + HDF5Writer fw(tmp.string() + "/this_file_does_exist_but_is_empty.h5"); + CHECK(fw.is_ok()); + + }// Destructor called as we leave scope. + + double data[1]; + HDF5Reader fr(tmp.string() + "/this_file_does_exist_but_is_empty.h5"); + CHECK_THROWS(fr.read("nonexistantdata", data)); + } + + // Normal operation: we should be able to create a file and write to it, then + // read from it. + SECTION("Check write then read.") { + { + HDF5Writer fw(tmp.string() + "/test_file_wr.h5"); + hsize_t dimensions[1] = {1}; + double writeme = 1337.; + fw.write("testdata", &writeme, 1, dimensions); + SPDLOG_DEBUG("Written data"); + + CHECK(fw.is_ok()); + fw.ls(); + + }// Destructor called as we leave scope. + + double data[1]; + HDF5Reader fr(tmp.string() + "/test_file_wr.h5"); + fr.read("testdata", data); + SPDLOG_DEBUG("Have read {}!", data[0]); + } + + SECTION("Check write then (overwrite) then read.") { + // Create the file and write some data. + { + HDF5Writer f1(tmp.string() + "/test_file_wor.h5"); + hsize_t dimensions[1] = {1}; + double writeme = 12345; + f1.write("testdata", &writeme, 1, dimensions); + SPDLOG_DEBUG("Written first data"); + + CHECK(f1.is_ok()); + + }// Destructor called as we leave scope. + + // Overwrite the file and add some different data. + { + HDF5Writer f2(tmp.string() + "/test_file_wor.h5"); + hsize_t dimensions[1] = {1}; + double writeme = 54321.; + f2.write("testdata2", &writeme, 1, dimensions); + SPDLOG_DEBUG("Written second data"); + + CHECK(f2.is_ok()); + + }// destructor called as we leave scope + + // Now open the file with a Reader. The first data should not be there (and + // should throw an exception). The second data should be there. + HDF5Reader f3(tmp.string() + "/test_file_wor.h5"); + + CHECK(f3.get_datanames().size() == 1); + + double data[1]; + HDF5Reader fr(tmp.string() + "/test_file_wor.h5"); + CHECK_THROWS(f3.read("testdata", data)); + + f3.read("testdata2", data); + CHECK(data[0] == Catch::Approx(54321.)); + } + + // teardown - remove temporary directory and all files + SPDLOG_DEBUG("Removing temporary directory."); + filesystem::remove_all(tmp); +} + +TEST_CASE("Test read/write wrt standard datatypes") { + // test-case wide setup - temporary directory + auto tmp = create_tmp_dir(); + + SECTION("5-element 1D array") { + double to_write[5] = {1 / 137.0, 3.0, 2.71215, 3.14159, 916.0}; + double read_back[5]; + { + hsize_t dimensions[1] = {5}; + HDF5Writer f1(tmp.string() + "/five_elements.h5"); + f1.write("five_elements", to_write, 1, dimensions); + } + { + HDF5Reader f2(tmp.string() + "/five_elements.h5"); + f2.read("five_elements", read_back); + } + for (unsigned int i = 0; i < 5; i++) { + CHECK(to_write[i] == Catch::Approx(read_back[i])); + } + } + + // teardown - remove temporary directory and all files + SPDLOG_DEBUG("Removing temporary directory."); + filesystem::remove_all(tmp); +} diff --git a/tdms/tests/unit/hdf5_io_tests/test_hdf5_reader.cpp b/tdms/tests/unit/hdf5_io_tests/test_hdf5_reader.cpp new file mode 100644 index 000000000..513790319 --- /dev/null +++ b/tdms/tests/unit/hdf5_io_tests/test_hdf5_reader.cpp @@ -0,0 +1,73 @@ +/** + * @file test_hdf5_reader.cpp + * @author William Graham + * @brief Unit tests for HDF5Reader I/O functions that are non-specific to a + * TDMS class or datatype + */ +#include "hdf5_io/hdf5_reader.h" + +#include +#include + +#include +#include + +#include "unit_test_utils.h" + +using namespace std; +using tdms_tests::uint16s_to_string; +using tdms_unit_test_data::struct_testdata; + +TEST_CASE("Read from a MATLAB struct") { + HDF5Reader MATFile(struct_testdata); + + SECTION("Read numeric scalars") { + /* Read scalar values from the MATLAB struct into the array. */ + // Initialise with values distinct from the expected values + double one_half = 0., unity = 0.; + bool logical_read = false; + // Read values + MATFile.read_field_from_struct("example_struct", "double_half", &one_half); + MATFile.read_field_from_struct("example_struct", "double_no_decimal", + &unity); + MATFile.read_field_from_struct("example_struct", "boolean", &logical_read); + // Validate read in data + REQUIRE(one_half == Catch::Approx(0.5)); + REQUIRE(unity == Catch::Approx(1.)); + REQUIRE(int(unity) == 1); + REQUIRE(logical_read); + } + + SECTION("Read array data") { + /* Read in the character array data */ + // string field is set to "tdms". Note that MATLAB saves this as uint16s, so + // we need to convert manually... + { + uint16_t read_uints16[4]; + MATFile.read_field_from_struct("example_struct", "string", read_uints16); + string tdms = uint16s_to_string(read_uints16, 4); + REQUIRE(tdms == "tdms"); + } + // The uint 3*4*5 uint matrix contains only 1s + { + vector uint_matrix(3 * 4 * 5, 0); + MATFile.read_field_from_struct("example_struct", "uint_345", + uint_matrix.data()); + bool all_values_unity = true; + for (uint8_t &value : uint_matrix) { + if (value != 1) { all_values_unity = false; } + } + REQUIRE(all_values_unity); + } + // The double 2*2 matrix contains 0.25, 0.5, 0.75, 1. + { + double two_by_two[4]; + MATFile.read_field_from_struct("example_struct", "double_22", two_by_two); + REQUIRE(two_by_two[0] == Catch::Approx(0.25)); + REQUIRE(two_by_two[1] == Catch::Approx(0.75)); + REQUIRE(two_by_two[2] == Catch::Approx(0.5)); + REQUIRE(two_by_two[3] == Catch::Approx(1.0)); + } + // The complex matrix is the Pauli-y matrix [0, -i; i, 0] + } +} diff --git a/tdms/tests/unit/test_hdf5_io.cpp b/tdms/tests/unit/test_hdf5_io.cpp deleted file mode 100644 index 361c53e1c..000000000 --- a/tdms/tests/unit/test_hdf5_io.cpp +++ /dev/null @@ -1,277 +0,0 @@ -/** - * @file test_hdf5_io.cpp - * @brief Tests of the HDF5 file I/O functionality. - */ -#include "hdf5_io.h" - -// std -#include -#include -#include -#include -#include - -// external -#include -#include -#include - -// tdms -#include "arrays.h" -#include "unit_test_utils.h" - -using namespace std; -using tdms_tests::create_tmp_dir; - -TEST_CASE("Wrong datatype passed to ijk.") { - auto tmp = create_tmp_dir(); - HDF5Writer r(tmp.string() + "/why.h5"); - // r.data_dump("x"); -} - -TEST_CASE("Test file I/O construction/destruction.") { - - // test-case wide setup - temporary directory - auto tmp = create_tmp_dir(); - - SECTION("Check file creation.") { - HDF5Writer f(tmp.string() + "/test_file_constructor.h5"); - CHECK(f.is_ok()); - } - - SECTION("Check all reasonable file extensions are OK.") { - for (auto extension : {".hdf5", ".h5", ".mat"}) { - { - HDF5Writer fw(tmp.string() + "/test_file" + extension); - CHECK(fw.is_ok()); - - }// Destructor called as we leave scope. - - HDF5Reader fr(tmp.string() + "/test_file" + extension); - CHECK(fr.is_ok()); - } - } - - SECTION("Check can't open nonexistent file.") { - CHECK_THROWS(HDF5Reader(tmp.string() + "/this_file_doesnt_exist.h5")); - } - - SECTION("Check can't read nonexistent data.") { - { - HDF5Writer fw(tmp.string() + "/this_file_does_exist_but_is_empty.h5"); - CHECK(fw.is_ok()); - - }// Destructor called as we leave scope. - - double data[1]; - HDF5Reader fr(tmp.string() + "/this_file_does_exist_but_is_empty.h5"); - CHECK_THROWS(fr.read("nonexistantdata", data)); - } - - // Normal operation: we should be able to create a file and write to it, then - // read from it. - SECTION("Check write then read.") { - { - HDF5Writer fw(tmp.string() + "/test_file_wr.h5"); - hsize_t dimensions[1] = {1}; - double writeme = 1337.; - fw.write("testdata", &writeme, 1, dimensions); - SPDLOG_DEBUG("Written data"); - - CHECK(fw.is_ok()); - fw.ls(); - - }// Destructor called as we leave scope. - - double data[1]; - HDF5Reader fr(tmp.string() + "/test_file_wr.h5"); - fr.read("testdata", data); - SPDLOG_DEBUG("Have read {}!", data[0]); - } - - SECTION("Check write then (overwrite) then read.") { - // Create the file and write some data. - { - HDF5Writer f1(tmp.string() + "/test_file_wor.h5"); - hsize_t dimensions[1] = {1}; - double writeme = 12345; - f1.write("testdata", &writeme, 1, dimensions); - SPDLOG_DEBUG("Written first data"); - - CHECK(f1.is_ok()); - - }// Destructor called as we leave scope. - - // Overwrite the file and add some different data. - { - HDF5Writer f2(tmp.string() + "/test_file_wor.h5"); - hsize_t dimensions[1] = {1}; - double writeme = 54321.; - f2.write("testdata2", &writeme, 1, dimensions); - SPDLOG_DEBUG("Written second data"); - - CHECK(f2.is_ok()); - - }// destructor called as we leave scope - - // Now open the file with a Reader. The first data should not be there (and - // should throw an exception). The second data should be there. - HDF5Reader f3(tmp.string() + "/test_file_wor.h5"); - - CHECK(f3.get_datanames().size() == 1); - - double data[1]; - HDF5Reader fr(tmp.string() + "/test_file_wor.h5"); - CHECK_THROWS(f3.read("testdata", data)); - - f3.read("testdata2", data); - CHECK(data[0] == Catch::Approx(54321.)); - } - - // teardown - remove temporary directory and all files - SPDLOG_DEBUG("Removing temporary directory."); - filesystem::remove_all(tmp); -} - -TEST_CASE("Test w/r TDMS objects") { - // test-case wide setup - temporary directory - auto tmp = create_tmp_dir(); - - SECTION("5-element 1D array") { - double to_write[5] = {1 / 137.0, 3.0, 2.71215, 3.14159, 916.0}; - double read_back[5]; - { - hsize_t dimensions[1] = {5}; - HDF5Writer f1(tmp.string() + "/five_elements.h5"); - f1.write("five_elements", to_write, 1, dimensions); - } - { - HDF5Reader f2(tmp.string() + "/five_elements.h5"); - f2.read("five_elements", read_back); - } - for (unsigned int i = 0; i < 5; i++) { - CHECK(to_write[i] == Catch::Approx(read_back[i])); - } - } - - SECTION("5-by-6 2D array") { - SPDLOG_INFO("5-by-6 2D array"); - Matrix counting_matrix(5, 6); - for (int i = 0; i < 5; i++) { - for (int j = 0; j < 6; j++) { counting_matrix[i][j] = 6. * i + j; } - } - Matrix read_back; - - { - HDF5Writer f1(tmp.string() + "/five-by-six.h5"); - f1.write("five-by-six", counting_matrix); - } - { - SPDLOG_DEBUG("About to read..."); - HDF5Reader f2(tmp.string() + "/five-by-six.h5"); - f2.read("five-by-six", read_back); - } - - for (unsigned int i = 0; i < 5; i++) { - for (unsigned int j = 0; j < 6; j++) { - SPDLOG_INFO("Checking {} == {}", counting_matrix[i][j], - read_back[i][j]); - CHECK(counting_matrix[i][j] == Catch::Approx(read_back[i][j])); - } - } - } - - // teardown - remove temporary directory and all files - SPDLOG_DEBUG("Removing temporary directory."); - filesystem::remove_all(tmp); -} - -/* The TESTDATA is a .mat file containing a single structure array, -example_struct. This structure array has the following data. - -MEMBER: value, MATLAB type -double_no_decimal: 1.0, double -double_half: 0.5, double -string: 'tdms', char array -boolean: 1, logical -uint_345: 3 * 4 * 5 array of 1s, uint8 -double_22: [0.25, 0.5; 0.75, 1.], double -complex_22: [0., -i; i, 0], complex -*/ -#ifdef CMAKE_SOURCE_DIR -string TESTDATA(string(CMAKE_SOURCE_DIR) + "/tests/unit/structure_array.mat"); -#else -string TESTDATA(filesystem::current_path() / - "../tests/unit/structure_array.mat"); -#endif - -/** @brief Returns the char represented by a uint16 */ -char uint16_to_char(const uint16_t &repr) { return *((char *) &repr); } - -/** - * @brief Returns the string composed of the characters represented by - * subsequent uint16s. - * - * @param repr Buffer of uint16s that represent individual characters - * @param buffer_length Length of the buffer - * @return string The string composed of the converted characters - */ -string uint16s_to_string(uint16_t *repr, int buffer_length) { - string output; - for (int i = 0; i < buffer_length; i++) { output += uint16_to_char(repr[i]); } - return output; -} - -TEST_CASE("Read from a MATLAB struct") { - HDF5Reader MATFile(TESTDATA); - - SECTION("Read numeric scalars") { - /* Read scalar values from the MATLAB struct into the array. */ - // Initialise with values distinct from the expected values - double one_half = 0., unity = 0.; - bool logical_read = false; - // Read values - MATFile.read_field_from_struct("example_struct", "double_half", &one_half); - MATFile.read_field_from_struct("example_struct", "double_no_decimal", - &unity); - MATFile.read_field_from_struct("example_struct", "boolean", &logical_read); - // Validate read in data - REQUIRE(one_half == Catch::Approx(0.5)); - REQUIRE(unity == Catch::Approx(1.)); - REQUIRE(int(unity) == 1); - REQUIRE(logical_read); - } - - SECTION("Read array data") { - /* Read in the character array data */ - // string field is set to "tdms". Note that MATLAB saves this as uint16s, so - // we need to convert manually... - { - uint16_t read_uints16[4]; - MATFile.read_field_from_struct("example_struct", "string", read_uints16); - string tdms = uint16s_to_string(read_uints16, 4); - REQUIRE(tdms == "tdms"); - } - // The uint 3*4*5 uint matrix contains only 1s - { - vector uint_matrix(3 * 4 * 5, 0); - MATFile.read_field_from_struct("example_struct", "uint_345", - uint_matrix.data()); - bool all_values_unity = true; - for (uint8_t &value : uint_matrix) { - if (value != 1) { all_values_unity = false; } - } - REQUIRE(all_values_unity); - } - // The double 2*2 matrix contains 0.25, 0.5, 0.75, 1. - { - double two_by_two[4]; - MATFile.read_field_from_struct("example_struct", "double_22", two_by_two); - REQUIRE(two_by_two[0] == Catch::Approx(0.25)); - REQUIRE(two_by_two[1] == Catch::Approx(0.75)); - REQUIRE(two_by_two[2] == Catch::Approx(0.5)); - REQUIRE(two_by_two[3] == Catch::Approx(1.0)); - } - // The complex matrix is the Pauli-y matrix [0, -i; i, 0] - } -}