From f69f59af1656c9e7be9a1f0a98ea75d6c49658bb Mon Sep 17 00:00:00 2001 From: Will Graham <32364977+willGraham01@users.noreply.github.com> Date: Tue, 2 May 2023 09:32:35 +0100 Subject: [PATCH 1/3] Handle structs (except with complex data, that's nasty) (#278) * Trying to decipher structs * Further towards reading a struct * Bypass the stack-smash bug. MATLAB saves character arrays as uint16s, and HDF5 is unable to interpret them as chars. As such, we need to do manual conversion in the test. * Rename binary and update contents --- tdms/include/hdf5_io.h | 16 ++++- tdms/tests/unit/small_fdtdgrid.mat | Bin 18584 -> 0 bytes tdms/tests/unit/structure_array.mat | Bin 0 -> 10744 bytes tdms/tests/unit/test_hdf5_io.cpp | 89 +++++++++++++++++++++++++--- 4 files changed, 94 insertions(+), 11 deletions(-) delete mode 100644 tdms/tests/unit/small_fdtdgrid.mat create mode 100644 tdms/tests/unit/structure_array.mat diff --git a/tdms/include/hdf5_io.h b/tdms/include/hdf5_io.h index 439f19a44..6792a2bc6 100644 --- a/tdms/include/hdf5_io.h +++ b/tdms/include/hdf5_io.h @@ -115,11 +115,23 @@ class HDF5Reader : public HDF5Base { H5::DataSpace dataspace = dataset.getSpace(); // now get the data type - H5::DataType datatype = dataset.getDataType(); - dataset.read(data, datatype); + 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_); diff --git a/tdms/tests/unit/small_fdtdgrid.mat b/tdms/tests/unit/small_fdtdgrid.mat deleted file mode 100644 index a80058a32c084c1d5b1d87d21b22a465886c77be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18584 zcmeHOO;ejU5Y}ts;P4fbq^XmVMujA$lz=hj%SnqJzTUKucv=qJ8W)@583P$(W`I+U z>8;27gdTJ3rT3mX{Rtd%%s)uA+GlNJY{GCM1g*z@w5yf$YTtcVlD%F^FE6Ga>xrRJ zy_8;luvRGM2X&^HEw62rHj;W_@##u>JTa(eO8IO#pVM2LNj+aG=%s8)#}j&FJQ+_U z<5M~wi;rr5c79B6ude4evU+4F7Sltz5h@i z!0;aQ+CR)Qr_^&4Jm4$fpJ?wXdMOo!yc_-3>n;B@H>ZrIj3LA3Wum~JpC=2*= z=?qPH1+&(`f8b|e)mpBc`>j;S3Ex-BuWk2{XE9Am#R<_P7`;vK!e{YvdJF8`?Vfy*$vsq6CD)G0!mF1$J0srDM%O$?*;9)%< z{5t%eYcI}cvvsWAeji%L!tICc?Bc-1@gbV2!~t2yBAEUkc>cs1Srp_s^`+vJ=$Y6- zeMQirhlt>LERbbXFlg2}t8dzHOi0<0L+B^q1#H7G`XT*BbQP^h`R{*k=QN7#IOw2S$NCKn?5#-UZ$V_5%ligTNtR3^)Re14n@g;23ZmI02jlP63m^ zhrma`?||O}r-3QPmq3*U4^)rA1Jw+8pqd2_RCC~gY92gLEr16qA3RV!0S{C^fCs7{ z!2{JIc%WJW&v-Px#1zp<_e;60onMQ2iYwBt)%aOh`D2;%vd{Y~3I*a!!*vG?jvFnN zxRL3w!#Dkq=<`YnO*EsB@6rbE7}BU)zj2JxIjC}R!^I8jrY|o>+;INx{N4GxSy$b@ zFvZJr9O(9iY>)H3FLd$U#dqt+Za-tq(ENPQPy5mD1KhcvE#uDpY=@8Q-0$MMi|^Ji zU3|A@aPi&k-}!m?G``n$@!iGuixl7OT+PSauKgL-;Jy{j?d<}=?}OiGJsv8iW2p9fj@Ogxn7nhmUJBLy zy~pc)?6oe=xj1Ki)a_%f8QebB?PK|QaQj%YgZp02<>T=4a6a;JB+oI0jio2~f~WqS zv6=_TU52eWj^>@EtURb5nN4jV*b=(b8ou1t-qG20CDMKM+V#(Fd>*}d>x-V-U*6GQ z_4eJpcmM1DZw9^{d@wW|8;Or5#>OWmr;-m(lg|_~Bg;N|r?6S3J?v@onIzlOPR4xj zK0BmLDwL(@JrtTiM&tuhXpAcmzZ+~^FVg2I91M%3F|I@WU%YYsSxe=APxX299mvS_ zRF}zpjY>Z3S|MM|(HV)3H4*csG%&iylCRro%0z;+Z+Sg#MOLqt4;XPRPvu&kVtg%6 z{#47;{|jn)`pZBqPftWGPx*4Sd|MsgUdL0)9JP__r#UUmJX^FrO_Mhy{!RT7$vKtg zMpTvaxl?#SImgO+R+Y0p&6%ho$GMf}PE?iixfg$%sL16U%+aB$FXv*KACdGU=VY=C zQB}^@LpCC+$Z5T0j~#s!BuX=!OsD{)CBnmSGbQB(=L5OCE5tTqR3=3zf$OV56m?MNUl zInrB?y;SP4$NYm{Ir0-Ya^%R3&g{%M&WoRuI0q2fwesfe?9A-$x3e?*Hru70tjjPeHS#Pj>neCc>cPw*|IMlY zia**xp{?Wmw>ifh7Ejo(6Z^CfQ2*VRX?2DE^CiEiUoC#a^;goKg}i<9U=A|)!z7LQ zx>^hUexDjh{WJVi|5m@JHY&g0e{jcx)G+=o&N5%+D^l-%&U}rpCVg10EcK@NJ-42p zXHp)E<9A#f$Xq&%^Z7y0&6E((IG}kfC$Rh}`%h8wxx`l@o)WLr^AlfAS{O@1uS>Oyz@hppN63>Emckqn# z$qJ8%d*yfK0qPv+C&j}rhBg3dekba*YJO<6i}B;+I7ujRc@tX?VLNgR0_tZOSvU_C zt%kVEFi*wTRqJW=CwiYZw`47fuzCqr-%I{E>VLeR>EX(~y+s`!^}9TeC3 z1M)Z3o5db7P+qH)cFLT0NB!)__`!2H9^0+t;aTxs^z(VChfNY)S(Ub7&_H{qcP}sYE8dK9cH0O1o;c2`JvSrRO>-4s^gZlC5Mj6b1=59 zb~JxYyKfc8d;*TzPZAx)k!5&C9mSFPX`Cs^O%t!^=&s`kqbHQ*&WxYhJ1c%gG305! zPBTM4AJqROtm9WUmpK3AcowaJH2eg3rkT1-vko+N#cdZol};$r;-{xAJDHIO=c8&T zqHWM><4SW8e$x*dmVw`YtB$F#BE?TFUNqSnRm`T}0>lBaZ^F`~VOBeiFC*6CuA8gr?aGJMcLpJEKG%4QbzLjeX zh{qp|=hP<^jYk^Sb-o&(H15;*sc5`XefKyjUD#YXIh$^Qp=?>JV@U dI0PI54grUNL%<>65O4@M1RMemflm>E{{bhPjR^n% literal 0 HcmV?d00001 diff --git a/tdms/tests/unit/test_hdf5_io.cpp b/tdms/tests/unit/test_hdf5_io.cpp index 533808f48..d84e57944 100644 --- a/tdms/tests/unit/test_hdf5_io.cpp +++ b/tdms/tests/unit/test_hdf5_io.cpp @@ -19,7 +19,6 @@ // tdms #include "arrays.h" -#include "fdtd_grid_initialiser.h" #include "unit_test_utils.h" using tdms_tests::create_tmp_dir;// unit_test_utils.h @@ -188,21 +187,93 @@ TEST_CASE("Test w/r TDMS objects") { std::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 std::string TESTDATA(std::string(CMAKE_SOURCE_DIR) + - "/tests/unit/small_fdtdgrid.mat"); + "/tests/unit/structure_array.mat"); #else std::string TESTDATA(std::filesystem::current_path() / - "../tests/unit/small_fdtdgrid.mat"); + "../tests/unit/structure_array.mat"); #endif -TEST_CASE("Read from example input") { - spdlog::info("I'm going to try and find: {}", TESTDATA); +/** @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 + */ +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; +} + +TEST_CASE("Read from a MATLAB struct") { HDF5Reader MATFile(TESTDATA); - SECTION("Read FDTD grid") { - SKIP("Not implemented yet"); - fdtdGridInitialiser gridinitialiser; - MATFile.read(gridinitialiser); + 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); + std::string tdms = uint16s_to_string(read_uints16, 4); + REQUIRE(tdms == "tdms"); + } + // The uint 3*4*5 uint matrix contains only 1s + { + std::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] } } From 4b3b3d36d49789417f5d0f13f85e3678820b1934 Mon Sep 17 00:00:00 2001 From: willGraham01 <1willgraham@gmail.com> Date: Fri, 19 May 2023 10:04:01 +0100 Subject: [PATCH 2/3] `HDF5` file structure reorganisation, and `InterfaceComponent` readability (#280) * Reorganise hdf5 unit tests - Move uint16 to char/str functions to the unit test utils file - Create hdf5_and_tdms_objects subdirectory of unit/ to hold unit tests on interaction between hdf5 and tdms classes - Move the Matrix test from test_hdf5_io into the new subdirectory - Data files needed for unit tests are defined in a unit_test_utils namespace to avoid redefinition across multiple files * Create file to test interface and hdf5 interactions - Add docstrings to interface.h since these are missing and I've just had to work out what they do - Create a matlab script that can reproduce the class_data.mat file which the hdf5 unit tests will try to create tdms objects from - Create the barebones test_hdf5_interface file * HDF5Reader can read from .mat file and produce an InterfaceComponent * File restructure: accounting for how many tests we are going to have with HDF5 * Prune includes * Add .mat file for HDF5-TDMS-object unit tests to run. - Adds scripts to reproduce this data, so in theory a new user can run a short MATLAB script to reproduce this - Had a play with trying to get setup-matlab to run these scripts before the unit tests, but alas, no. --- tdms/include/hdf5_io.h | 214 -------------- tdms/include/hdf5_io/hdf5_base.h | 92 ++++++ tdms/include/hdf5_io/hdf5_reader.h | 89 ++++++ tdms/include/hdf5_io/hdf5_writer.h | 50 ++++ tdms/include/interface.h | 26 +- .../{hdf5_io.cpp => hdf5_io/hdf5_base.cpp} | 47 ++- tdms/src/hdf5_io/hdf5_reader.cpp | 14 + tdms/src/hdf5_io/hdf5_writer.cpp | 19 ++ tdms/tests/include/unit_test_utils.h | 55 ++++ .../unit/hdf5_and_tdms_objects/class_data.mat | Bin 0 -> 10064 bytes .../create_tdms_object_data.m | 25 ++ .../test_hdf5_InterfaceComponent.cpp | 69 +++++ .../test_hdf5_Matrix.cpp | 56 ++++ .../hdf5_io_tests/create_structure_array.m | 19 ++ .../{ => hdf5_io_tests}/structure_array.mat | Bin .../tests/unit/hdf5_io_tests/test_hdf5_io.cpp | 150 ++++++++++ .../unit/hdf5_io_tests/test_hdf5_reader.cpp | 73 +++++ tdms/tests/unit/test_hdf5_io.cpp | 279 ------------------ 18 files changed, 755 insertions(+), 522 deletions(-) delete mode 100644 tdms/include/hdf5_io.h create mode 100644 tdms/include/hdf5_io/hdf5_base.h create mode 100644 tdms/include/hdf5_io/hdf5_reader.h create mode 100644 tdms/include/hdf5_io/hdf5_writer.h rename tdms/src/{hdf5_io.cpp => hdf5_io/hdf5_base.cpp} (52%) create mode 100644 tdms/src/hdf5_io/hdf5_reader.cpp create mode 100644 tdms/src/hdf5_io/hdf5_writer.cpp create mode 100644 tdms/tests/unit/hdf5_and_tdms_objects/class_data.mat create mode 100644 tdms/tests/unit/hdf5_and_tdms_objects/create_tdms_object_data.m create mode 100644 tdms/tests/unit/hdf5_and_tdms_objects/test_hdf5_InterfaceComponent.cpp create mode 100644 tdms/tests/unit/hdf5_and_tdms_objects/test_hdf5_Matrix.cpp create mode 100644 tdms/tests/unit/hdf5_io_tests/create_structure_array.m rename tdms/tests/unit/{ => hdf5_io_tests}/structure_array.mat (100%) create mode 100644 tdms/tests/unit/hdf5_io_tests/test_hdf5_io.cpp create mode 100644 tdms/tests/unit/hdf5_io_tests/test_hdf5_reader.cpp delete mode 100644 tdms/tests/unit/test_hdf5_io.cpp diff --git a/tdms/include/hdf5_io.h b/tdms/include/hdf5_io.h deleted file mode 100644 index 6792a2bc6..000000000 --- a/tdms/include/hdf5_io.h +++ /dev/null @@ -1,214 +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 "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 std::vector 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 &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 fdtdGridInitialiser &initialiser, - const std::string &dataset_name = "fdtdgrid") const { - - // This method will take in the fdtdGridInit... object, assign the pointer - // member, and then run the object's - throw std::logic_error("Not yet implemented"); - 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 52% rename from tdms/src/hdf5_io.cpp rename to tdms/src/hdf5_io/hdf5_base.cpp index 0b778fc52..b511fff8f 100644 --- a/tdms/src/hdf5_io.cpp +++ b/tdms/src/hdf5_io/hdf5_base.cpp @@ -1,37 +1,30 @@ -#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 -/****************************************************************************** - * 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); +using namespace std; - // 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."); +ijk to_ijk(const std::vector dimensions) { + unsigned int rank = dimensions.size(); + ijk out; + if (rank > 0) out.i = (int) dimensions[0]; + if (rank > 1) out.j = (int) dimensions[1]; + if (rank > 2) out.k = (int) dimensions[2]; + if (rank > 3) spdlog::warn("Rank > 3"); + return out; } -/****************************************************************************** - * 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++) { @@ -47,8 +40,8 @@ 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; } @@ -61,7 +54,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 0000000000000000000000000000000000000000..37d7be667656278a38ca573010b88bdc09be48a5 GIT binary patch literal 10064 zcmeHL%~Bdc5bnjljiNzgsz^>#HJRmjar4@G)uMmNlI<(>>j2U5;JPG?mA7ksA|=edZ&5TIag|4#p9{uVk)_y=3;a6Dzo-B zsTzf2_ry{0*;q`?s%`{Aq0Ou`4Zj-%KI(TiI{bXto87jdA0o7n5V0$!+>7fnNdQOO zu$W&k|HA$j;2%Jg_2WFg-!8wuz7E<_<`d3t@MC~lwkQn$uMo(rr*rtghy2uv`8SR; z7NJ~iy7iJ%a9J@`cT0^aj4YxhF}x+W^p5C^-w4O=;Uo=k$>a2j-_ubtjNNixWA?BR z@W-#j;RT;-^0b>AAe(X0Y_Yt>@;1xcEK6J~8NkO6cK6mS2(`}N=^-)@zZ}b_k4P`) ztLnRMmtpzXw_e|L4^|AyV}AU4odfpBb(|k8VCKQPBH%vm{mk&eIQ z7T8`Mi|~LJp9q#k3Q42F`*fbe9$X)t2`k>r13XU13VDt;kAwEzXo9`bIPrSF7zR4| zEEt8b?$ehJegAmxo}4z%(H}>l716BJ79D7xw!hu>FI1dH1O1=q|HrK{018gERxKBt zijpb1uanM6M2YC9?|rb^upS89jI(LagTqBR`g!uBj5S__r`V} zy2=$UWGgRagL4Ey&bO>;QT6e0oozS{a$3@a^L4Nr&TA({U7H>5_|a` zAlGrx%lCoK=S4sHUVv?+qaJkvS(#shfI+|@U=T0}7z7Lg1_6VBLBJqj5HJWB1iB&c E8woubM*si- literal 0 HcmV?d00001 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 d84e57944..000000000 --- a/tdms/tests/unit/test_hdf5_io.cpp +++ /dev/null @@ -1,279 +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 -#include - -// tdms -#include "arrays.h" -#include "unit_test_utils.h" - -using tdms_tests::create_tmp_dir;// unit_test_utils.h - -TEST_CASE("Wrong datatype passed to ijk.") { - auto tmp = create_tmp_dir(); - HDF5Writer r(tmp.string() + "/why.h5"); -} - -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()); - }// Destructor called as we leave scope - - 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_AS(HDF5Reader(tmp.string() + "/this_file_doesnt_exist.h5"), - H5::FileIException); - } - - 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_AS(fr.read("nonexistantdata", data), H5::FileIException); - } - - // Normal operation: we should be able to create a file and write to it, then - // read from it. - SECTION("Check write then read.") { - // Create a file and write some data. - { - 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."); - std::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."); - std::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 -std::string TESTDATA(std::string(CMAKE_SOURCE_DIR) + - "/tests/unit/structure_array.mat"); -#else -std::string TESTDATA(std::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 - */ -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; -} - -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); - std::string tdms = uint16s_to_string(read_uints16, 4); - REQUIRE(tdms == "tdms"); - } - // The uint 3*4*5 uint matrix contains only 1s - { - std::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] - } -} From ecba65a29f73a4a9538bc6f6111e06d17d2c3a64 Mon Sep 17 00:00:00 2001 From: Sam Cunliffe Date: Tue, 23 May 2023 10:57:17 +0100 Subject: [PATCH 3/3] Some tidyup from the comments of PR #312. Co-authored-by: Will Graham <32364977+willGraham01@users.noreply.github.com> --- tdms/include/hdf5_io/hdf5_base.h | 10 ---- tdms/include/interface.h | 15 +++--- tdms/src/hdf5_io/hdf5_base.cpp | 22 ++------ tdms/tests/include/unit_test_utils.h | 1 + .../tests/unit/hdf5_io_tests/test_hdf5_io.cpp | 52 +++++++++++++++---- 5 files changed, 57 insertions(+), 43 deletions(-) diff --git a/tdms/include/hdf5_io/hdf5_base.h b/tdms/include/hdf5_io/hdf5_base.h index 865f93eb4..925c48bde 100644 --- a/tdms/include/hdf5_io/hdf5_base.h +++ b/tdms/include/hdf5_io/hdf5_base.h @@ -14,16 +14,6 @@ #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 diff --git a/tdms/include/interface.h b/tdms/include/interface.h index f565b5cd0..498e2f695 100644 --- a/tdms/include/interface.h +++ b/tdms/include/interface.h @@ -11,11 +11,11 @@ * @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. + * @details 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 @@ -27,8 +27,9 @@ */ class InterfaceComponent { public: - /*! Whether or not a source or boundary condition is applied at this - * interface */ + /*! + * 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; diff --git a/tdms/src/hdf5_io/hdf5_base.cpp b/tdms/src/hdf5_io/hdf5_base.cpp index b511fff8f..479d8b393 100644 --- a/tdms/src/hdf5_io/hdf5_base.cpp +++ b/tdms/src/hdf5_io/hdf5_base.cpp @@ -11,20 +11,8 @@ #include #include -using namespace std; - -ijk to_ijk(const std::vector dimensions) { - unsigned int rank = dimensions.size(); - ijk out; - if (rank > 0) out.i = (int) dimensions[0]; - if (rank > 1) out.j = (int) dimensions[1]; - if (rank > 2) out.k = (int) dimensions[2]; - if (rank > 3) spdlog::warn("Rank > 3"); - return out; -} - -vector HDF5Base::get_datanames() const { - vector names; +std::vector HDF5Base::get_datanames() const { + std::vector names; // iterate over all objects in the file for (unsigned int i = 0; i < file_->getNumObjs(); i++) { @@ -40,8 +28,8 @@ vector HDF5Base::get_datanames() const { } void HDF5Base::ls() const { - vector names = this->get_datanames(); - for (auto name : names) cout << name << endl; + std::vector names = this->get_datanames(); + for (auto name : names) std::cout << name << std::endl; return; } @@ -54,7 +42,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(); - vector dimensions(rank); + std::vector dimensions(rank); dataspace.getSimpleExtentDims(dimensions.data(), nullptr); return dimensions; } diff --git a/tdms/tests/include/unit_test_utils.h b/tdms/tests/include/unit_test_utils.h index 74e1c6125..c051c6997 100644 --- a/tdms/tests/include/unit_test_utils.h +++ b/tdms/tests/include/unit_test_utils.h @@ -15,6 +15,7 @@ using tdms_math_constants::DCPI; namespace tdms_unit_test_data { +// Small test data files shipped with the test code. #ifdef CMAKE_SOURCE_DIR inline std::string tdms_object_data(std::string(CMAKE_SOURCE_DIR) + diff --git a/tdms/tests/unit/hdf5_io_tests/test_hdf5_io.cpp b/tdms/tests/unit/hdf5_io_tests/test_hdf5_io.cpp index 031b4c472..09e5fa5e1 100644 --- a/tdms/tests/unit/hdf5_io_tests/test_hdf5_io.cpp +++ b/tdms/tests/unit/hdf5_io_tests/test_hdf5_io.cpp @@ -10,6 +10,7 @@ #include // external +#include #include #include #include @@ -17,8 +18,12 @@ // tdms #include "unit_test_utils.h" -using namespace std; -using tdms_tests::create_tmp_dir; +using tdms_tests::create_tmp_dir;// unit_test_utils.h + +TEST_CASE("Wrong datatype passed to ijk.") { + auto tmp = create_tmp_dir(); + HDF5Writer r(tmp.string() + "/why.h5"); +} TEST_CASE("Test file I/O construction/destruction.") { // test-case wide setup - temporary directory @@ -27,7 +32,7 @@ TEST_CASE("Test file I/O construction/destruction.") { SECTION("Check file creation.") { HDF5Writer f(tmp.string() + "/test_file_constructor.h5"); CHECK(f.is_ok()); - } + }// Destructor called as we leave scope SECTION("Check all reasonable file extensions are OK.") { for (auto extension : {".hdf5", ".h5", ".mat"}) { @@ -43,7 +48,8 @@ TEST_CASE("Test file I/O construction/destruction.") { } SECTION("Check can't open nonexistent file.") { - CHECK_THROWS(HDF5Reader(tmp.string() + "/this_file_doesnt_exist.h5")); + CHECK_THROWS_AS(HDF5Reader(tmp.string() + "/this_file_doesnt_exist.h5"), + H5::FileIException); } SECTION("Check can't read nonexistent data.") { @@ -55,12 +61,13 @@ TEST_CASE("Test file I/O construction/destruction.") { double data[1]; HDF5Reader fr(tmp.string() + "/this_file_does_exist_but_is_empty.h5"); - CHECK_THROWS(fr.read("nonexistantdata", data)); + CHECK_THROWS_AS(fr.read("nonexistantdata", data), H5::FileIException); } // Normal operation: we should be able to create a file and write to it, then // read from it. SECTION("Check write then read.") { + // Create a file and write some data. { HDF5Writer fw(tmp.string() + "/test_file_wr.h5"); hsize_t dimensions[1] = {1}; @@ -102,7 +109,7 @@ TEST_CASE("Test file I/O construction/destruction.") { CHECK(f2.is_ok()); - }// destructor called as we leave scope + }// 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. @@ -120,10 +127,10 @@ TEST_CASE("Test file I/O construction/destruction.") { // teardown - remove temporary directory and all files SPDLOG_DEBUG("Removing temporary directory."); - filesystem::remove_all(tmp); + std::filesystem::remove_all(tmp); } -TEST_CASE("Test read/write wrt standard datatypes") { +TEST_CASE("Test read/write standard datatypes") { // test-case wide setup - temporary directory auto tmp = create_tmp_dir(); @@ -144,7 +151,34 @@ TEST_CASE("Test read/write wrt standard datatypes") { } } + 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); + std::filesystem::remove_all(tmp); }