diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9d2c3147..3a545ee0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,11 @@ jobs: - name: Set up MATLAB uses: matlab-actions/setup-matlab@v1.2.3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + # ------------------------------------------------------------------------------- # Ubuntu - name: Install dependencies for Ubuntu @@ -123,6 +128,20 @@ jobs: run: | cmake --build . --config ${{ matrix.build_type }} + # ------------------------------------------------------------------------------- + # Unit tests + - name: Produce MATLAB unit test data + if: matrix.build_testing == 'ON' + uses: matlab-actions/run-command@v1 + with: + command: cd('tdms/tests/unit/benchmark_scripts/'), setup_unit_tests + + - name: Produce hdf5 unit test data + if: matrix.build_testing == 'ON' + run: | + pip install -r ${{ github.workspace }}/tdms/tests/requirements.txt + python ${{ github.workspace }}/tdms/tests/unit/benchmark_scripts/create_hdf5_test_file.py + - name: Run TDMS unit tests if: matrix.build_testing == 'ON' working-directory: ${{ runner.workspace }}/build @@ -148,6 +167,8 @@ jobs: if: matrix.build_testing == 'ON' && contains(matrix.os, 'ubuntu') uses: codecov/codecov-action@v3 + # ------------------------------------------------------------------------------- + # Upload build artefact for system tests - name: Tar the build result to maintain permissions # https://github.com/actions/upload-artifact#maintaining-file-permissions-and-case-sensitive-files # https://github.com/actions/upload-artifact/issues/38 diff --git a/.gitignore b/.gitignore index da1e8fa68..e2a28d0c0 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ html/ **/.pytest_cache/ tdms/tests/system/data/*.zip **.mat +tdms/tests/unit/benchmark_scripts/unit_test_data/** # text editor files .idea/ diff --git a/doc/developers.md b/doc/developers.md index f1fe6d244..f5a363d70 100644 --- a/doc/developers.md +++ b/doc/developers.md @@ -254,7 +254,22 @@ The doxygen-style comments will be included in this developer documentation. To run the unit tests, [compile](#compiling) with `-DBUILD_TESTING=ON`. Then run `ctest` from the build directory or execute the test executable `./tdms_tests`. -It's good practice, and reassuring for your pull-request reviewers, if new C++ functionality is at covered by unit tests. +It's good practice, and reassuring for your pull-request reviewers, if new C++ functionality is covered by unit tests. + +#### Benchmark Scripts and Data Generation + +The `tdms/tests/unit/benchmarking` directory contains scripts that produce input data for the unit tests, or that provide benchmarks for the units that are tested. + +The `C++` unit tests require the presence of a `.mat` or `.hdf5` file to read/write data from/to during testing. +The locations of these files are coded into `tests/include/unit_test_utils.h`, but the files themselves are not committed to the repository - they can be created by running the `setup_unit_tests.m` and `create_hdf5_data.py` scripts. +These scripts can then be updated to add/remove/edit the data available to the unit tests: +- `create_hdf5_test_file.py` : Produces `hdf5_test_file.hdf5`; used when testing read/writing from `.hdf5` files. +- `create_structure_array.m` : Produces `structure_array.mat`; used when testing reading/writing MATLAB `struct` arrays. +- `create_class_data.m` : Produces `class_data.mat`; used when testing reading/writing `tdms` objects to/from `.mat` files. +- `create_bad_class_data.m` : Produces `bad_class_data.mat`; used for testing failure cases when reading/writing `tdms` objects to/from `.mat` files. + +The `benchmark_` scripts perform band-limited interpolation (BLi) using `MATLAB`'s `interp` function. +`TDMS`'s interpolation schemes are based off this `MATLAB` function (specficially, in the coefficients the scheme uses to interpolate), and are thus used to benchmark the accuracy of the scheme. ### Test coverage {#coverage} diff --git a/tdms/CMakeLists.txt b/tdms/CMakeLists.txt index 6c9725f35..1508ea460 100644 --- a/tdms/CMakeLists.txt +++ b/tdms/CMakeLists.txt @@ -1,7 +1,7 @@ # General setup --------------------------------------------------------------- cmake_minimum_required(VERSION 3.21) -project(tdms LANGUAGES CXX) +project(tdms LANGUAGES CXX C) set(CMAKE_CXX_STANDARD 17) # Allow building with testing, and default to off @@ -84,6 +84,12 @@ else() add_definitions(-DSPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_INFO) endif() +# Compile definintions +if (CMAKE_SOURCE_DIR) + add_compile_definitions(CMAKE_SOURCE_DIR="${CMAKE_SOURCE_DIR}") + message(STATUS "Unit tests will use ${CMAKE_SOURCE_DIR} as a reference for finding data files.") +endif() + # TDMS version ---------------------------------------------------------------- # if supplied via the cmake configuration (-DTDMS_VERSION=whatever_I_want) then diff --git a/tdms/include/arrays.h b/tdms/include/arrays.h index 81f55c738..efdac0de7 100644 --- a/tdms/include/arrays.h +++ b/tdms/include/arrays.h @@ -7,6 +7,7 @@ #include #include #include +#include #include @@ -14,6 +15,13 @@ #include "matlabio.h" #include "utils.h" +template +struct xyz_vector { + std::vector x = {}; + std::vector y = {}; + std::vector z = {}; +}; + template class XYZTensor3D { public: @@ -201,28 +209,23 @@ class DMaterial : public DCollectionBase, MaterialCollection { explicit DMaterial(const mxArray *ptr); }; -class DispersiveMultiLayer { -public: - double *alpha = nullptr; - double *beta = nullptr; - double *gamma = nullptr; - XYZVectors kappa; - XYZVectors sigma; - +struct DispersiveMultiLayer { public: - explicit DispersiveMultiLayer(const mxArray *ptr); + std::vector alpha; + std::vector beta; + std::vector gamma; + xyz_vector kappa; + xyz_vector sigma; /** * @brief Determines whether the (background) medium is dispersive * - * @param K_tot Number of Yee cells in the z-direction (number of entries in - * this->gamma) * @param near_zero_tolerance Tolerance for non-zero gamma (attenuation) * values * @return true Background is dispersive * @return false Background is not dispersive */ - bool is_dispersive(int K_tot, double near_zero_tolerance = 1e-15); + bool is_dispersive(double near_zero_tolerance = 1e-15) const; }; template @@ -272,6 +275,9 @@ class Matrix { } }; + int get_n_cols() const { return n_cols; } + int get_n_rows() const { return n_rows; } + /** * Destructor. Must be defined in the header */ @@ -322,12 +328,9 @@ class FrequencyExtractVector : public Vector { double max(); }; -class FrequencyVectors { -public: - Vector x; - Vector y; - - void initialise(const mxArray *ptr); +struct FrequencyVectors { + std::vector x; + std::vector y; }; /** diff --git a/tdms/include/cuboid.h b/tdms/include/cuboid.h new file mode 100644 index 000000000..8d7c10846 --- /dev/null +++ b/tdms/include/cuboid.h @@ -0,0 +1,21 @@ +#pragma once + +#include "mat_io.h" + +/** + * @brief Defines a cuboid by specifying the first and last Yee cells in each +axial direction that form part of the cube. + * + * @details For example, { 0, 5, 2, 7, 1, 10 } corresponds to the cuboid that +contains all Yee cells indexed (i,j,k) where; + * 0 <= i <= 5, + * 2 <= j <= 7, + * 1 <= k <= 10. + * + * TODO: Check inclusivity of the inequalities above. + */ +struct Cuboid { + int array[6] = {0, 0, 0, 0, 0, 0}; + + int operator[](int index) const { return array[index]; } +}; diff --git a/tdms/include/fdtd_grid_initialiser.h b/tdms/include/fdtd_grid_initialiser.h index 62181469b..49d0499dc 100644 --- a/tdms/include/fdtd_grid_initialiser.h +++ b/tdms/include/fdtd_grid_initialiser.h @@ -15,9 +15,9 @@ class fdtdGridInitialiser { private: - const mxArray *pointer; //< Pointer to the array - const char *mat_filename; //< Filename of the MATLAB file - std::vector dimensions;//< The dimensions of the array + const mxArray *pointer = nullptr; //< Pointer to the array + const char *mat_filename = nullptr; //< Filename of the MATLAB file + std::vector dimensions = {0, 0, 0};//< The dimensions of the array /** * @brief Get a value from a integer attribute of the FDTD grid defined in a @@ -29,6 +29,8 @@ class fdtdGridInitialiser { mwSize value_of_attribute(const std::string &key); public: + /** @brief Construct a new fdtd Grid Initialiser object */ + fdtdGridInitialiser() {} /** * @brief Construct a new fdtd Grid Initialiser object * diff --git a/tdms/include/hdf5_io/hdf5_base.h b/tdms/include/hdf5_io/hdf5_base.h new file mode 100644 index 000000000..188bb8476 --- /dev/null +++ b/tdms/include/hdf5_io/hdf5_base.h @@ -0,0 +1,93 @@ +/** + * @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" +#include "hdf5_io/hdf5_dimension.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 `dataname`. + * @param dataname The name of the data table. + * @return The dimensions of the data. + */ + H5Dimension shape_of(const std::string &dataname) const; + /** + * @brief Return shape/dimensionality information about array data stored + * within a group. + * + * @param group_name The name of the HDF5 Group in which the data array is + * stored. + * @param dataname The name of the data array to check dimensions of. + * @return The dimensions of the data. + */ + H5Dimension shape_of(const std::string &group_name, + 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_dimension.h b/tdms/include/hdf5_io/hdf5_dimension.h new file mode 100644 index 000000000..63b202d35 --- /dev/null +++ b/tdms/include/hdf5_io/hdf5_dimension.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +#include + +class H5Dimension : public std::vector { +public: + H5Dimension() = default; + H5Dimension(const H5::DataSpace &data_space); + H5Dimension(const H5::DataSet &data_set) : H5Dimension(data_set.getSpace()){}; + + /** + * @brief Whether these dimensions describe an array that is castable to a 1D + * array. + * @details In the event that these dimensions only have one entry, or at + * most one of the entries is greater than 1, the shape described can be cast + * to a 1D-array of length max_dim(). + * + * @return true These dimensions describe (an object castable to) a 1D-array + * @return false Otherwise + */ + bool is_1D() const; + + /** + * @brief Returns the dimension of the greatest extent. + * @details For instances where is_1D() returns true, this conincides with the + * number of elements in the array, and the length of a 1D array necessary to + * hold all the elements. + * + * @return hsize_t + */ + hsize_t max_dim() const { return *std::max_element(begin(), end()); }; + + /** @brief The total number of elements (product of the dimensions) */ + int number_of_elements() const { + int product = 1; + for (hsize_t axis_size : *this) { product *= axis_size; } + return product; + } +}; diff --git a/tdms/include/hdf5_io/hdf5_reader.h b/tdms/include/hdf5_io/hdf5_reader.h new file mode 100644 index 000000000..d890a8c80 --- /dev/null +++ b/tdms/include/hdf5_io/hdf5_reader.h @@ -0,0 +1,181 @@ +#pragma once + +#include "hdf5_io/hdf5_base.h" + +#include "arrays.h" +#include "cuboid.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 Read the dataset stored within a group into the buffer provided. + * @details Can be used to read MATLAB structs by treating the struct as the + * Group and field as the Dataset. + * @tparam T C++ datatype to read data into. + * @param group The Group within the file in which the dataset lives. + * @param dataset The name of the dataset to fetch data from. + * @param data The buffer into which to write the data. + */ + template + void read_dataset_in_group(const std::string &group, + const std::string &dataset, T *data) const { + spdlog::debug("Reading {} from file: {}", group, filename_); + + // Structs are saved as groups, so we need to fetch the group this struct is + // contained in + H5::Group structure_array = file_->openGroup(group); + // Then fetch the requested data and read it into the buffer provided + H5::DataSet requested_field = structure_array.openDataSet(dataset); + requested_field.read(data, requested_field.getDataType()); + } + + /** + * @brief Read the dataset stored within a group into the buffer provided, + * resizing the vector buffer accordingly. + * @details Can be used to read MATLAB structs by treating the struct as the + * Group and field as the Dataset. + * @tparam T C++ datatype to read data into. + * @param group The Group within the file in which the dataset lives. + * @param dataset The name of the dataset to fetch data from. + * @param[out] data The buffer into which to write the data. + */ + template + void read_dataset_in_group(const std::string &group, + const std::string &dataset, + std::vector &data) const { + spdlog::debug("Reading {} from file: {}", group, filename_); + + // Structs are saved as groups, so we need to fetch the group this struct is + // contained in + H5::Group structure_array = file_->openGroup(group); + // Then fetch the requested data and read it into the buffer provided, + // resizing the buffer if necessary + H5::DataSet requested_field = structure_array.openDataSet(dataset); + H5Dimension field_size(requested_field); + int number_of_elements = field_size.number_of_elements(); + data.resize(number_of_elements); + requested_field.read(data.data(), requested_field.getDataType()); + } + + /** + * @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."); + } + + /** + * @brief Reads a 2D-dataset into a Matrix object. + * + * @tparam T C++ datatype of the Matrix object + * @param dataset_name Name of the dataset to read data from + * @param data_location Matrix object buffer + */ + 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; + } + + /** + * @brief Read an InterfaceComponent into the buffer provided. + * + * @param[in] plane The plane {I,J,K}{0,1} to read from the file. + * @param[out] ic InterfaceComponent reference to populate/overwrite. + */ + void read(const std::string &plane, InterfaceComponent *ic) const; + /** + * @brief Read an InterfaceComponent from the file. + * + * @param plane The plane {I,J,K}{0,1} to read from the file. + * @return InterfaceComponent corresponding to the requested plane. + */ + InterfaceComponent read(const std::string &plane) const { + InterfaceComponent ic; + read(plane, &ic); + return ic; + } + + /** + * @brief Read FrequencyVectors into the buffer provided. + * + * @param[out] f_vec FrequencyVectors reference to populate/overwrite. + */ + void read(FrequencyVectors *f_vec) const; + /** + * @brief Read FrequencyVectors from the file. + * + * @return FrequencyVectors object containing the data from the input file. + */ + FrequencyVectors read() const { + FrequencyVectors f_vec; + read(&f_vec); + return f_vec; + } + + /** + * @brief + * + * Cuboid is just the phasorsurface array which is it's own dataset, but needs + * to be offset by -1 b/c indexing things + * @param cube + */ + void read(Cuboid *cube) const; + + /** + * @brief Read data from the file into a DispersiveMultiLayerObject + * @details Data is read from the "dispersive_aux" group. If this group does + * not exist, no data is written but no exception is thrown. + * + * If the group does exist, the alpha, beta, gamma, kappa, and sigma members + * are populated with the corresponding data entries. + * @param dml DispersiveMultiLayer object into which to write data. + */ + void read(DispersiveMultiLayer *dml) const; +}; 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/input_matrices.h b/tdms/include/input_matrices.h index 92d7ed59f..690e7694b 100644 --- a/tdms/include/input_matrices.h +++ b/tdms/include/input_matrices.h @@ -39,6 +39,11 @@ class InputMatrices { void validate_assigned_pointers(); public: + /*! Name of the input file from which MATLAB objects were extracted. Akward + * middle-child between full removal of MATLAB and the slow removal of class + * dependencies */ + std::string input_filename; + InputMatrices() = default; 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/include/output_matrices/output_matrices.h b/tdms/include/output_matrices/output_matrices.h index 3c9e07748..ab622b4bc 100644 --- a/tdms/include/output_matrices/output_matrices.h +++ b/tdms/include/output_matrices/output_matrices.h @@ -7,6 +7,7 @@ #include #include "cell_coordinate.h" +#include "cuboid.h" #include "field.h" #include "fieldsample.h" #include "grid_labels.h" @@ -14,7 +15,6 @@ #include "matrix.h" #include "output_matrices/id_variables.h" #include "output_matrices/output_matrix_pointers.h" -#include "shapes.h" #include "simulation_parameters.h" #include "surface_phasors.h" #include "vertex_phasors.h" diff --git a/tdms/include/shapes.h b/tdms/include/shapes.h deleted file mode 100644 index f624b69bd..000000000 --- a/tdms/include/shapes.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "mat_io.h" - -class Cuboid { -private: - /* The indices of the first and last Yee cells in each axial direction that - are encompassed by the cuboid. For example, { 0, 5, 2, 7, 1, 10 } corresponds - to the cuboid that contains all Yee cells indexed (i,j,k) where 0 <= i <= 5, 2 - <= j <= 7, 1 <= k <= 10. - - TODO: Check inclusivity of the inequalities above. - */ - int array[6] = {0, 0, 0, 0, 0, 6}; - -public: - void initialise(const mxArray *ptr, int J_tot); - - inline int operator[](int value) const { return array[value]; }; -}; diff --git a/tdms/include/simulation_manager/objects_from_infile.h b/tdms/include/simulation_manager/objects_from_infile.h index 0a186c27c..d26f31041 100644 --- a/tdms/include/simulation_manager/objects_from_infile.h +++ b/tdms/include/simulation_manager/objects_from_infile.h @@ -11,6 +11,7 @@ #include "arrays.h" #include "cell_coordinate.h" +#include "cuboid.h" #include "field.h" #include "fieldsample.h" #include "globals.h" @@ -18,7 +19,6 @@ #include "input_flags.h" #include "input_matrices.h" #include "interface.h" -#include "shapes.h" #include "simulation_parameters.h" #include "source.h" #include "vertex_phasors.h" diff --git a/tdms/src/arrays.cpp b/tdms/src/arrays.cpp index 7d92d539e..d30c48c77 100644 --- a/tdms/src/arrays.cpp +++ b/tdms/src/arrays.cpp @@ -162,26 +162,9 @@ void DCollection::init_xyz_vectors(const mxArray *ptr, XYZVectors &arrays, } } -DispersiveMultiLayer::DispersiveMultiLayer(const mxArray *ptr) { - - if (mxIsEmpty(ptr)) { return; } - assert_is_struct_with_n_fields(ptr, 9, "dispersive_aux"); - - alpha = mxGetPr(ptr_to_vector_in(ptr, "alpha", "dispersive_aux")); - beta = mxGetPr(ptr_to_vector_in(ptr, "beta", "dispersive_aux")); - gamma = mxGetPr(ptr_to_vector_in(ptr, "gamma", "dispersive_aux")); - kappa.x = mxGetPr(ptr_to_matrix_in(ptr, "kappa_x", "dispersive_aux")); - kappa.y = mxGetPr(ptr_to_matrix_in(ptr, "kappa_y", "dispersive_aux")); - kappa.z = mxGetPr(ptr_to_matrix_in(ptr, "kappa_z", "dispersive_aux")); - sigma.x = mxGetPr(ptr_to_matrix_in(ptr, "sigma_x", "dispersive_aux")); - sigma.y = mxGetPr(ptr_to_matrix_in(ptr, "sigma_y", "dispersive_aux")); - sigma.z = mxGetPr(ptr_to_matrix_in(ptr, "sigma_z", "dispersive_aux")); -} - -bool DispersiveMultiLayer::is_dispersive(int K_tot, - double near_zero_tolerance) { - for (int i = 0; i < K_tot; i++) { - if (fabs(gamma[i]) > near_zero_tolerance) { +bool DispersiveMultiLayer::is_dispersive(double near_zero_tolerance) const { + for (double gamma_val : gamma) { + if (fabs(gamma_val) > near_zero_tolerance) { // non-zero attenuation constant of a Yee cell implies media is dispersive return true; } @@ -236,15 +219,6 @@ double FrequencyExtractVector::max() { return tmp; } -void FrequencyVectors::initialise(const mxArray *ptr) { - - if (mxIsEmpty(ptr)) { return; } - - assert_is_struct_with_n_fields(ptr, 2, "f_vec"); - x = Vector(ptr_to_vector_in(ptr, "fx_vec", "f_vec")); - y = Vector(ptr_to_vector_in(ptr, "fy_vec", "f_vec")); -} - void Pupil::initialise(const mxArray *ptr, int n_rows, int n_cols) { if (mxIsEmpty(ptr)) { return; } diff --git a/tdms/src/hdf5_io/hdf5_base.cpp b/tdms/src/hdf5_io/hdf5_base.cpp new file mode 100644 index 000000000..d07a957e1 --- /dev/null +++ b/tdms/src/hdf5_io/hdf5_base.cpp @@ -0,0 +1,68 @@ +/** + * @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 + +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; + + // iterate over all objects in the file + for (unsigned int i = 0; i < file_->getNumObjs(); i++) { + H5G_obj_t object_type = file_->getObjTypeByIdx(i); + + // if the current object is a H5::Dataset then grab its name + if (object_type == H5G_DATASET) { + H5std_string object_name = file_->getObjnameByIdx(i); + names.push_back(object_name); + } + } + return names; +} + +void HDF5Base::ls() const { + vector names = this->get_datanames(); + for (auto name : names) cout << name << endl; + return; +} + +H5Dimension HDF5Base::shape_of(const string &dataname) const { + // Get the dataspace (contains dimensionality info) + H5::DataSpace dataspace = file_->openDataSet(dataname).getSpace(); + return H5Dimension(dataspace); +} + +H5Dimension HDF5Base::shape_of(const string &group_name, + const string &dataname) const { + // Open the group that contains the dataset + H5::Group group = file_->openGroup(group_name); + // Get the DataSpace for the DataSet within the group + H5::DataSpace dataspace = group.openDataSet(dataname).getSpace(); + return H5Dimension(dataspace); +} + +bool HDF5Base::is_ok() const { + // TODO: check for file health might be unnessicary given we've constructed + // the object. + return file_->isHdf5(filename_); + // return file_->isAccessible(filename_) && file_->isHdf5(filename_); +} diff --git a/tdms/src/hdf5_io/hdf5_dimension.cpp b/tdms/src/hdf5_io/hdf5_dimension.cpp new file mode 100644 index 000000000..8ba6aa45b --- /dev/null +++ b/tdms/src/hdf5_io/hdf5_dimension.cpp @@ -0,0 +1,23 @@ +#include "hdf5_io/hdf5_dimension.h" + +#include + +using namespace std; + +H5Dimension::H5Dimension(const H5::DataSpace &data_space) { + // Fetch rank to declare the vector size + int rank = data_space.getSimpleExtentNdims(); + // Resize to the correct rank then populate entries with the data + resize(rank); + data_space.getSimpleExtentDims(data(), nullptr); +} + +bool H5Dimension::is_1D() const { + int n_non_trivial_dimensions = 0; + if (size() != 1) { + for (hsize_t dim : *this) { + if (dim > 1) { n_non_trivial_dimensions++; } + } + } + return n_non_trivial_dimensions <= 1; +} diff --git a/tdms/src/hdf5_io/hdf5_reader.cpp b/tdms/src/hdf5_io/hdf5_reader.cpp new file mode 100644 index 000000000..95de8f6be --- /dev/null +++ b/tdms/src/hdf5_io/hdf5_reader.cpp @@ -0,0 +1,81 @@ +#include "hdf5_io/hdf5_reader.h" +#include "hdf5_io/hdf5_dimension.h" + +#include +#include + +#include + +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_dataset_in_group("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]; +} + +void HDF5Reader::read(FrequencyVectors *f_vec) const { + // Allocate memory in f_vec + H5Dimension x_dims = shape_of("f_vec", "fx_vec"); + H5Dimension y_dims = shape_of("f_vec", "fy_vec"); + // Check that we have one dimensional arrays + if (!x_dims.is_1D() || !y_dims.is_1D()) { + throw runtime_error("f_vec members are not 1D arrays!"); + } + // Allocate memory - resize() must be used over reserve() to ensure enough + // buffer when we read in, _and_ that size() correctly returns the number of + // elements in the buffer. + f_vec->x.resize(x_dims.max_dim()); + f_vec->y.resize(y_dims.max_dim()); + // Now read the data into the vectors + read_dataset_in_group("f_vec", "fx_vec", f_vec->x.data()); + read_dataset_in_group("f_vec", "fy_vec", f_vec->y.data()); +} + +void HDF5Reader::read(Cuboid *cube) const { + string cuboid_dataset = "phasorsurface"; + // Check that we are reading in a 1D array with 6 elements + H5Dimension cuboid_dims(file_->openDataSet(cuboid_dataset).getSpace()); + if (!(cuboid_dims.is_1D() && cuboid_dims.max_dim() == 6)) { + throw runtime_error( + "Error: phasorsurface is not a 1D vector of 6 elements"); + } + // Read buffer then adjust for indexing offset between MATLAB and C++ + // NOTE: Buffer is saved as doubles in .mat file, but we want to read as + // integers here. + double intermediate_buffer[6]; + read(cuboid_dataset, intermediate_buffer); + for (int i = 0; i < 6; i++) { + cube->array[i] = (int) intermediate_buffer[i] - 1; + } +} + +void HDF5Reader::read(DispersiveMultiLayer *dml) const { + string group_name = "dispersive_aux"; + // Deal with the case of an empty input + if (!file_->nameExists(group_name)) { + spdlog::info(group_name + " is not a group: assuming empty input"); + return; + } else { + // This is a group - it should have 9 members and we can quickly check this + H5::Group dispersive_aux = file_->openGroup(group_name); + if (dispersive_aux.getNumObjs() != 9) { + throw runtime_error("dispersive_aux does not have exactly 9 members!"); + } + } + // Assuming non-empty input, setup the data appropriately + read_dataset_in_group(group_name, "alpha", dml->alpha); + read_dataset_in_group(group_name, "beta", dml->beta); + read_dataset_in_group(group_name, "gamma", dml->gamma); + read_dataset_in_group(group_name, "kappa_x", dml->kappa.x); + read_dataset_in_group(group_name, "kappa_y", dml->kappa.y); + read_dataset_in_group(group_name, "kappa_z", dml->kappa.z); + read_dataset_in_group(group_name, "sigma_x", dml->sigma.x); + read_dataset_in_group(group_name, "sigma_y", dml->sigma.y); + read_dataset_in_group(group_name, "sigma_z", dml->sigma.z); +} 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/src/input_matrices.cpp b/tdms/src/input_matrices.cpp index 77c734be6..8085d621d 100644 --- a/tdms/src/input_matrices.cpp +++ b/tdms/src/input_matrices.cpp @@ -24,6 +24,7 @@ int InputMatrices::index_from_matrix_name(const string &matrix_name) { } void InputMatrices::set_from_input_file(const char *mat_filename) { + input_filename = std::string(mat_filename); MatrixCollection infile_expected(matrixnames_input_with_grid); MatFileMatrixCollection infile_contains(mat_filename); spdlog::info("Input file: " + string(mat_filename) + @@ -51,6 +52,7 @@ void InputMatrices::set_from_input_file(const char *mat_filename) { } void InputMatrices::set_from_input_file(const char *mat_filename, const char *gridfile) { + input_filename = std::string(mat_filename); MatrixCollection infile_expected(matrixnames_infile); MatFileMatrixCollection infile_contains(mat_filename); spdlog::info("Input file: " + string(mat_filename) + diff --git a/tdms/src/shapes.cpp b/tdms/src/shapes.cpp deleted file mode 100644 index 43f296daa..000000000 --- a/tdms/src/shapes.cpp +++ /dev/null @@ -1,29 +0,0 @@ -#include "shapes.h" - -#include -#include - -using namespace std; - -void Cuboid::initialise(const mxArray *ptr, int J_tot) { - - auto ndims = mxGetNumberOfDimensions(ptr); - auto dims = mxGetDimensions((mxArray *) ptr); - - if (ndims != 2) { - throw runtime_error("expected phasorsurface to be a vector of length 6"); - } - if (dims[0] != 1 || dims[1] != 6) { - throw runtime_error("expected phasorsurface to be a vector of length 6"); - } - - for (int i = 0; i < 6; i++) { - array[i] = max((int) *(mxGetPr(ptr) + i) - - 1,// Change from MATLAB -> C indexing - 0); - } - if (J_tot == 0 && array[2] != array[3]) { - throw runtime_error( - "When doing a 2D simulation, J0 should equal J1 in phasorsurface."); - } -} diff --git a/tdms/src/simulation_manager/objects_from_infile.cpp b/tdms/src/simulation_manager/objects_from_infile.cpp index f71f76db7..30b1210ef 100644 --- a/tdms/src/simulation_manager/objects_from_infile.cpp +++ b/tdms/src/simulation_manager/objects_from_infile.cpp @@ -3,10 +3,9 @@ #include #include -// For ptr_to_vector_in, ptr_to_vector_or_empty_in, int_cast_from_double_in -#include "matlabio.h" -// for init_grid_arrays #include "array_init.h" +#include "hdf5_io/hdf5_reader.h" +#include "matlabio.h" using tdms_math_constants::DCPI; using namespace tdms_flags; @@ -18,15 +17,7 @@ IndependentObjectsFromInfile::IndependentObjectsFromInfile( Dmaterial(matrices_from_input_file["Dmaterial"]),// get Dmaterial C(matrices_from_input_file["C"]), // get C D(matrices_from_input_file["D"]), // get D - I0(matrices_from_input_file["interface"], "I0"), // get the interface(s) - I1(matrices_from_input_file["interface"], "I1"), - J0(matrices_from_input_file["interface"], "J0"), - J1(matrices_from_input_file["interface"], "J1"), - K0(matrices_from_input_file["interface"], "K0"), - K1(matrices_from_input_file["interface"], "K1"), - matched_layer( - matrices_from_input_file["dispersive_aux"]),// get dispersive_aux - Ei(matrices_from_input_file["tdfield"]) // get tdfield + Ei(matrices_from_input_file["tdfield"]) // get tdfield { /* Set FDTD/PSTD-dependent variable skip_tdf [1: PSTD, 6: FDTD] */ skip_tdf = in_flags["use_pstd"] ? 1 : 6; @@ -39,6 +30,20 @@ IndependentObjectsFromInfile::IndependentObjectsFromInfile( E_s.set_preferred_interpolation_methods(i_method); H_s.set_preferred_interpolation_methods(i_method); + // HDF5Reader to extract data from the input file + HDF5Reader INPUT_FILE(matrices_from_input_file.input_filename); + + // Read the interface components + I0 = INPUT_FILE.read("I0"); + I1 = INPUT_FILE.read("I1"); + J0 = INPUT_FILE.read("J0"); + J1 = INPUT_FILE.read("J1"); + K0 = INPUT_FILE.read("K0"); + K1 = INPUT_FILE.read("K1"); + + // Read the layer structure of the obstacle + INPUT_FILE.read(&matched_layer); + // unpack the parameters for this simulation params.unpack_from_input_matrices(matrices_from_input_file); @@ -66,7 +71,11 @@ IndependentObjectsFromInfile::IndependentObjectsFromInfile( // Get phasorsurface cuboid = Cuboid(); if (params.exphasorssurface && params.run_mode == RunMode::complete) { - cuboid.initialise(matrices_from_input_file["phasorsurface"], IJK_tot.j); + INPUT_FILE.read(&cuboid); + if (IJK_tot.j == 0 && cuboid[2] != cuboid[3]) { + throw std::runtime_error("In a 2D simulation, J0 should equal J1 in " + "phasorsurface."); + } } // Get conductive_aux, and setup with pointers @@ -88,7 +97,7 @@ IndependentObjectsFromInfile::IndependentObjectsFromInfile( D_tilde = DTilde(); // if exdetintegral is flagged, setup pupil, D_tilde, and f_vec accordingly if (params.exdetintegral) { - f_vec.initialise(matrices_from_input_file["f_vec"]); + f_vec = INPUT_FILE.read(); pupil.initialise(matrices_from_input_file["Pupil"], f_vec.x.size(), f_vec.y.size()); D_tilde.initialise(matrices_from_input_file["D_tilde"], f_vec.x.size(), @@ -132,9 +141,7 @@ IndependentObjectsFromInfile::IndependentObjectsFromInfile( } // work out if we have a dispersive background - if (params.is_disp_ml) { - params.is_disp_ml = matched_layer.is_dispersive(IJK_tot.k); - } + if (params.is_disp_ml) { params.is_disp_ml = matched_layer.is_dispersive(); } // Set dt so that an integer number of time periods fits within a sinusoidal // period diff --git a/tdms/tests/include/array_test_class.h b/tdms/tests/include/array_test_class.h index 4534682a4..7f71a4d2c 100644 --- a/tdms/tests/include/array_test_class.h +++ b/tdms/tests/include/array_test_class.h @@ -60,24 +60,6 @@ class DetectorSensitivityArraysTest : public AbstractArrayTest { std::string get_class_name() override { return "DetectorSensitivityArray"; } }; -/** @brief Unit tests for DispersiveMultilayer */ -class DispersiveMultilayerTest : public AbstractArrayTest { -private: - const int n_fields = 9; - const char *fieldnames[9] = {"alpha", "beta", "gamma", - "kappa_x", "kappa_y", "kappa_z", - "sigma_x", "sigma_y", "sigma_z"}; - - void test_empty_construction() override; - void test_wrong_input_type() override; - void test_correct_construction() override; - // test: is_dispersive() - void test_other_methods() override; - -public: - std::string get_class_name() override { return "DispersiveMultilayer"; } -}; - // Test methods check the performance of initialise, as this is the de-facto // constructor class DTildeTest : public AbstractArrayTest { @@ -110,23 +92,6 @@ class FieldSampleTest : public AbstractArrayTest { std::string get_class_name() override { return "FieldSample"; } }; -// Test methods check the performance of initialise, as this is the de-facto -// constructor -class FrequencyVectorsTest : public AbstractArrayTest { -private: - const int n_fields = 2; - const char *fieldnames[2] = {"fx_vec", "fy_vec"}; - - void test_empty_construction() override; - void test_wrong_input_type() override; - void test_incorrect_number_of_fields() override; - void test_correct_construction() override; - void test_initialise_method() override; - -public: - std::string get_class_name() override { return "FrequencyVectors"; } -}; - /** @brief Unit tests for FullFieldSnapshot */ class FullFieldSnapshotTest : public AbstractArrayTest { private: diff --git a/tdms/tests/include/unit_test_utils.h b/tdms/tests/include/unit_test_utils.h index 4288a2c17..4ca72fcdb 100644 --- a/tdms/tests/include/unit_test_utils.h +++ b/tdms/tests/include/unit_test_utils.h @@ -11,6 +11,40 @@ #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/benchmark_scripts/" + "unit_test_data/class_data.mat"); +inline std::string tdms_bad_object_data(std::string(CMAKE_SOURCE_DIR) + + "/tests/unit/benchmark_scripts/" + "unit_test_data/bad_class_data.mat"); +inline std::string hdf5_test_file(std::string(CMAKE_SOURCE_DIR) + + "/tests/unit/benchmark_scripts/" + "unit_test_data/hdf5_test_file.hdf5"); +inline std::string struct_testdata(std::string(CMAKE_SOURCE_DIR) + + "/tests/unit/benchmark_scripts/" + "unit_test_data/structure_array.mat"); +#else +inline std::string tdms_object_data(std::filesystem::current_path() / + "../tests/unit/benchmark_scripts/" + "unit_test_data/class_data.mat"); +inline std::string tdms_bad_object_data(std::filesystem::current_path() / + "../tests/unit/benchmark_scripts/" + "unit_test_data/bad_class_data.mat"); +inline std::string hdf5_test_file(std::filesystem::current_path() / + "../tests/unit/benchmark_scripts/" + "unit_test_data/hdf5_test_file.hdf5"); +inline std::string struct_testdata(std::filesystem::current_path() / + "../tests/unit/benchmark_scripts/" + "unit_test_data/structure_array.mat"); +#endif + +}// namespace tdms_unit_test_data + namespace tdms_tests { inline double TOLERANCE = 1e-16;//< Floating-point comparison tolerance @@ -153,4 +187,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/array_tests/test_DispersiveMultiLayer.cpp b/tdms/tests/unit/array_tests/test_DispersiveMultiLayer.cpp index 4a45a8856..ac3437a56 100644 --- a/tdms/tests/unit/array_tests/test_DispersiveMultiLayer.cpp +++ b/tdms/tests/unit/array_tests/test_DispersiveMultiLayer.cpp @@ -1,9 +1,8 @@ /** * @file test_DispersiveMultiLayer.cpp * @author William Graham (ccaegra@ucl.ac.uk) - * @brief Tests for the DispersiveMultiLayer class and its subclasses + * @brief Tests for the DispersiveMultiLayer struct and its methods */ -#include #include #include @@ -12,81 +11,32 @@ #include "unit_test_utils.h" using namespace std; -using Catch::Approx; -using tdms_tests::TOLERANCE; -void DispersiveMultilayerTest::test_empty_construction() { - // Constructor should error if recieving a struct with no fields - create_1by1_struct(0, {}); - REQUIRE_THROWS_AS(DispersiveMultiLayer(matlab_input), runtime_error); -} +/** @brief Test the methods associated to the DispersiveMultiLayer struct */ +TEST_CASE("DispersiveMultiLayer") { + spdlog::debug("[Unit] DispersiveMultiLayer"); -void DispersiveMultilayerTest::test_wrong_input_type() { - // Constructor should throw runtime_error at not recieving struct - dimensions_2d[0] = 2; - dimensions_2d[1] = 3; - create_numeric_array(2, dimensions_2d, mxUINT16_CLASS); - REQUIRE_THROWS_AS(DispersiveMultiLayer(matlab_input), runtime_error); -} + int N_ELEMENTS = 5; + DispersiveMultiLayer dml; -void DispersiveMultilayerTest::test_correct_construction() { - create_1by1_struct(n_fields, fieldnames); - // build "data" for each of the fields, which is going to be the same array - // filled with consecutive integers - const int array_size[2] = {1, n_numeric_elements}; - mxArray *field_array_ptrs[n_fields]; - for (int i = 0; i < n_fields; i++) { - field_array_ptrs[i] = mxCreateNumericArray(2, (const mwSize *) array_size, - mxDOUBLE_CLASS, mxREAL); - mxDouble *where_to_place_data = mxGetPr(field_array_ptrs[i]); - for (int i = 0; i < n_numeric_elements; i++) { - where_to_place_data[i] = (double) i; - } - mxSetField(matlab_input, 0, fieldnames[i], field_array_ptrs[i]); - } - // we should now be able to create a DispersiveMultiLayer object - REQUIRE_NOTHROW(DispersiveMultiLayer(matlab_input)); - DispersiveMultiLayer dml(matlab_input); - // now check that the data has been correctly assigned - for (int i = 0; i < n_numeric_elements; i++) { - CHECK(dml.alpha[i] == Approx(i).epsilon(TOLERANCE)); - CHECK(dml.beta[i] == Approx(i).epsilon(TOLERANCE)); - CHECK(dml.gamma[i] == Approx(i).epsilon(TOLERANCE)); - CHECK(dml.kappa.x[i] == Approx(i).epsilon(TOLERANCE)); - CHECK(dml.kappa.y[i] == Approx(i).epsilon(TOLERANCE)); - CHECK(dml.kappa.z[i] == Approx(i).epsilon(TOLERANCE)); - CHECK(dml.sigma.x[i] == Approx(i).epsilon(TOLERANCE)); - CHECK(dml.sigma.y[i] == Approx(i).epsilon(TOLERANCE)); - CHECK(dml.sigma.z[i] == Approx(i).epsilon(TOLERANCE)); - } -} - -void DispersiveMultilayerTest::test_other_methods() { + // Test the is_dispersive() method SECTION("is_dispersive()") { - create_1by1_struct(n_fields, fieldnames); - // build "data" for each of the fields, just a constant array of 1s - const int array_size[2] = {1, n_numeric_elements}; - mxArray *field_array_ptrs[n_fields]; - for (int i = 0; i < n_fields; i++) { - field_array_ptrs[i] = mxCreateNumericArray(2, (const mwSize *) array_size, - mxDOUBLE_CLASS, mxREAL); - mxDouble *where_to_place_data = mxGetPr(field_array_ptrs[i]); - for (int i = 0; i < n_numeric_elements; i++) { - where_to_place_data[i] = 1.; - } - mxSetField(matlab_input, 0, fieldnames[i], field_array_ptrs[i]); + SECTION("non-empty vector") { + /* Initialise the gamma component and set to 0 */ + dml.gamma.resize(N_ELEMENTS); + // All entries being zero should result in a non-dispersive medium + REQUIRE(!dml.is_dispersive()); + + // Populate an element with the value 1. + dml.gamma[N_ELEMENTS / 2] = 1.; + /* A tolerance of 1.5 should still flag the dml as not dispersive */ + REQUIRE(!dml.is_dispersive(1.5)); + /* Yet a tolerance of 0.5 should flag it as dispersive */ + REQUIRE(dml.is_dispersive(0.5)); + } + SECTION("empty vector") { + /* When empty, is_dispersive() should not error and return false */ + REQUIRE(!dml.is_dispersive()); } - // create DispersiveMultiLayer object - DispersiveMultiLayer dml(matlab_input); - - // all entries in gamma are 1. -> so a tolerance of 1.5 should flag the dml - // as not dispersive - REQUIRE(!dml.is_dispersive(n_numeric_elements, 1.5)); - // yet a tolerance of 0.5 should flag it as dispersive - REQUIRE(dml.is_dispersive(n_numeric_elements, 0.5)); } } - -TEST_CASE("DispersiveMultiLayer") { - DispersiveMultilayerTest().run_all_class_tests(); -} diff --git a/tdms/tests/unit/array_tests/test_FrequencyVectors.cpp b/tdms/tests/unit/array_tests/test_FrequencyVectors.cpp deleted file mode 100644 index 2587f67a2..000000000 --- a/tdms/tests/unit/array_tests/test_FrequencyVectors.cpp +++ /dev/null @@ -1,96 +0,0 @@ -/** - * @file test_FrequencyVectors.cpp - * @author William Graham (ccaegra@ucl.ac.uk) - * @brief Tests for the FrequencyVectors class and its subclasses - */ -#include -#include - -#include "array_test_class.h" -#include "arrays.h" -#include "unit_test_utils.h" - -using namespace std; -using tdms_tests::TOLERANCE; - -void FrequencyVectorsTest::test_empty_construction() { - // initialise() method should exit without assignment if we pass in a pointer - // to an empty array (regardless of whether this is a struct or not) - FrequencyVectors fv; - dimensions_2d[0] = 0; - create_numeric_array(2, dimensions_2d, mxUINT8_CLASS); - // attempt assignment - fv.initialise(matlab_input); - // should still be unassigned vectors - bool not_assigned = (!fv.x.has_elements() && !fv.y.has_elements()); - REQUIRE(not_assigned); -} - -void FrequencyVectorsTest::test_wrong_input_type() { - FrequencyVectors fv; - // throw error if we attempt to provide a non-empty, non-struct array - dimensions_2d[0] = 2; - dimensions_2d[1] = 3; - create_numeric_array(2, dimensions_2d, mxUINT8_CLASS); - REQUIRE_THROWS_AS(fv.initialise(matlab_input), runtime_error); -} - -void FrequencyVectorsTest::test_incorrect_number_of_fields() { - // assignment will throw error if we attempt to provide a struct array that - // doesn't have two fields - FrequencyVectors fv; - SECTION("Struct with too many inputs") { - const char *too_many_names[3] = {"field1", "field2", "field3"}; - create_1by1_struct(3, too_many_names); - CHECK_THROWS_AS(fv.initialise(matlab_input), runtime_error); - } - SECTION("Struct with too few inputs") { - create_1by1_struct(n_fields - 1, fieldnames); - CHECK_THROWS_AS(fv.initialise(matlab_input), runtime_error); - } -} - -void FrequencyVectorsTest::test_correct_construction() { - FrequencyVectors fv; - // members should start unassigned - bool not_assigned = (!fv.x.has_elements() && !fv.y.has_elements()); - REQUIRE(not_assigned); -} - -void FrequencyVectorsTest::test_initialise_method() { - FrequencyVectors fv; - const int field_array_dimensions[2] = {1, n_numeric_elements}; - // create the struct - create_1by1_struct(n_fields, fieldnames); - // create the data for the fields of our struct - mxArray *field_array_ptrs[2]; - for (int i = 0; i < 2; i++) { - field_array_ptrs[i] = mxCreateNumericArray( - 2, (const mwSize *) field_array_dimensions, mxDOUBLE_CLASS, mxREAL); - mxDouble *where_to_place_data = mxGetPr(field_array_ptrs[i]); - // 0th field, fx_vec[i], will be 1/(i+1) - // 1st field, fy_vec[i], will be -1/(i+1) - for (int j = 0; j < n_numeric_elements; j++) { - where_to_place_data[j] = pow(-1., (double) i) / ((double) (j + 1)); - } - mxSetField(matlab_input, 0, fieldnames[i], field_array_ptrs[i]); - } - // attempt to create a vector from this struct - REQUIRE_NOTHROW(fv.initialise(matlab_input)); - // check that we actually assigned values to the Vectors under the hood - bool not_assigned = (!fv.x.has_elements() && !fv.y.has_elements()); - bool expected_size = (fv.x.size() == n_numeric_elements && - fv.y.size() == n_numeric_elements); - bool assigned_and_correct_size = ((!not_assigned) && expected_size); - REQUIRE(assigned_and_correct_size); - // and the values themselves are what we expect - bool values_are_correct = true; - for (int i = 0; i < n_numeric_elements; i++) { - values_are_correct = values_are_correct && - (abs(fv.x[i] - 1. / ((double) (i + 1))) < TOLERANCE) && - (abs(fv.y[i] + 1. / ((double) (i + 1))) < TOLERANCE); - } - REQUIRE(values_are_correct); -} - -TEST_CASE("FrequencyVectors") { FrequencyVectorsTest().run_all_class_tests(); } diff --git a/tdms/tests/unit/matlab_benchmark_scripts/.clang-format b/tdms/tests/unit/benchmark_scripts/.clang-format similarity index 100% rename from tdms/tests/unit/matlab_benchmark_scripts/.clang-format rename to tdms/tests/unit/benchmark_scripts/.clang-format diff --git a/tdms/tests/unit/matlab_benchmark_scripts/benchmark_test_BLi_vs_cubic_validation.m b/tdms/tests/unit/benchmark_scripts/benchmark_test_BLi_vs_cubic_validation.m similarity index 100% rename from tdms/tests/unit/matlab_benchmark_scripts/benchmark_test_BLi_vs_cubic_validation.m rename to tdms/tests/unit/benchmark_scripts/benchmark_test_BLi_vs_cubic_validation.m diff --git a/tdms/tests/unit/matlab_benchmark_scripts/benchmark_test_field_interpolation_E.m b/tdms/tests/unit/benchmark_scripts/benchmark_test_field_interpolation_E.m similarity index 100% rename from tdms/tests/unit/matlab_benchmark_scripts/benchmark_test_field_interpolation_E.m rename to tdms/tests/unit/benchmark_scripts/benchmark_test_field_interpolation_E.m diff --git a/tdms/tests/unit/matlab_benchmark_scripts/benchmark_test_field_interpolation_H.m b/tdms/tests/unit/benchmark_scripts/benchmark_test_field_interpolation_H.m similarity index 100% rename from tdms/tests/unit/matlab_benchmark_scripts/benchmark_test_field_interpolation_H.m rename to tdms/tests/unit/benchmark_scripts/benchmark_test_field_interpolation_H.m diff --git a/tdms/tests/unit/matlab_benchmark_scripts/benchmark_test_interpolation_functions.m b/tdms/tests/unit/benchmark_scripts/benchmark_test_interpolation_functions.m similarity index 100% rename from tdms/tests/unit/matlab_benchmark_scripts/benchmark_test_interpolation_functions.m rename to tdms/tests/unit/benchmark_scripts/benchmark_test_interpolation_functions.m diff --git a/tdms/tests/unit/benchmark_scripts/create_bad_class_data.m b/tdms/tests/unit/benchmark_scripts/create_bad_class_data.m new file mode 100644 index 000000000..a233504f1 --- /dev/null +++ b/tdms/tests/unit/benchmark_scripts/create_bad_class_data.m @@ -0,0 +1,20 @@ +%% This script creates the bad_class_data.mat file for use in the hdf5 tests for reading in TDMS objects. +% These variables are used to check failure-cases for when the reader methods should detect an error. +close all; +clear; + +%% f_vec +% f_vec is a struct with two fields, fx_vec and fy_vec. +f_vec = struct(); +f_vec.fx_vec = zeros(2,2); % Non-2D elements are not expected! +f_vec.fy_vec = zeros(2,1); % This is permitted, but the former field should error + +%% phasorsurface +% This array is read into the Cuboid class. It is just an array of 6 integers (stored as doubles OFC) that correspond to Yee cell indices in the various axial directions +phasorsurface = [1; 4; 2; 5; 3; 6; 7;]; % 7 elements should throw an error + +%% 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("unit_test_data/bad_class_data.mat", "-v7.3"); diff --git a/tdms/tests/unit/benchmark_scripts/create_class_data.m b/tdms/tests/unit/benchmark_scripts/create_class_data.m new file mode 100644 index 000000000..096be2e0b --- /dev/null +++ b/tdms/tests/unit/benchmark_scripts/create_class_data.m @@ -0,0 +1,55 @@ +%% 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 + +%% f_vec & f_vec_bad +% f_vec is a struct with two fields, fx_vec and fy_vec. +% These are each 1D vectors of doubles, usually they have the same length +% however for our testing purposes we will make them different lengths. +f_vec = struct(); +f_vec.fx_vec = [0.25 0.5 0.75 1.]; % Row vector w/ 4 elements +f_vec.fy_vec = [-0.25; -0.5; -0.75; -1.]; % Col vector w/ 4 elements + +% f_vec_bad will be used as the failure case in our tests. +f_vec_bad = struct(); +f_vec_bad.fx_vec = zeros(2,2); % Non-2D elements are not expected! +f_vec_bad.fy_vec = zeros(2,1); % This is permitted, but the former field should error + +%% phasorsurface +% This array is read into the Cuboid class. It is just an array of 6 integers (stored as doubles OFC) that correspond to Yee cell indices in the various axial directions +phasorsurface = [1 4 2 5 3 6]; + +%% dispersive_aux +% Structure array that populates a DispersiveMultiLayer object. +% Has 9 fields, which are all vectors (theoretically of the same length but for testing purposes nope): +% alpha, beta, gamma, kappa_x, kappa_y, kappa_z, sigma_x, sigma_y, sigma_z +dispersive_aux = struct(); +dispersive_aux.alpha = 0:9; +dispersive_aux.beta = 0:9; +dispersive_aux.gamma = 0:9; +dispersive_aux.kappa_x = 0:9; +dispersive_aux.kappa_y = 0:9; +dispersive_aux.kappa_z = 0:9; +dispersive_aux.sigma_x = 0:9; +dispersive_aux.sigma_y = 0:9; +dispersive_aux.sigma_z = 0:9; + +%% 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("unit_test_data/class_data.mat", "-v7.3"); diff --git a/tdms/tests/unit/benchmark_scripts/create_hdf5_test_file.py b/tdms/tests/unit/benchmark_scripts/create_hdf5_test_file.py new file mode 100644 index 000000000..be43b2c56 --- /dev/null +++ b/tdms/tests/unit/benchmark_scripts/create_hdf5_test_file.py @@ -0,0 +1,42 @@ +import os +import sys + +import h5py +import numpy as np + +FNAME_TO_CREATE = os.path.abspath( + os.path.dirname(__file__) + "/unit_test_data/hdf5_test_file.hdf5" +) + + +def create_hdf5_test_file() -> None: + """ """ + # Create the directory for the file (if it doesn't exist), and the file itself + if not os.path.exists(os.path.dirname(FNAME_TO_CREATE)): + os.mkdir(os.path.dirname(FNAME_TO_CREATE)) + file = h5py.File(FNAME_TO_CREATE, "w") + + # Create a group under root + read_in_test = file.require_group("read_in_test") + + # Create test data to read in + consecutive_numbers = np.arange(0, 12, dtype=float) + + # Populate group with test data + read_in_test.create_dataset( + "vector_int", data=consecutive_numbers, shape=(12,), dtype=int + ) + read_in_test.create_dataset( + "matrix_double", data=consecutive_numbers, shape=(2, 6), dtype=float + ) + read_in_test.create_dataset( + "tensor_double", data=consecutive_numbers, shape=(2, 3, 2), dtype=float + ) + + file.close() + return + + +if __name__ == "__main__": + create_hdf5_test_file() + sys.exit(0) diff --git a/tdms/tests/unit/benchmark_scripts/create_structure_array.m b/tdms/tests/unit/benchmark_scripts/create_structure_array.m new file mode 100644 index 000000000..79528fabc --- /dev/null +++ b/tdms/tests/unit/benchmark_scripts/create_structure_array.m @@ -0,0 +1,26 @@ +%% This script creates the structure_array.mat file for use in the hdf5 tests for reading in structure arrays. +close all; +clear; + +%% An example to test how different matlab datatypes are stored in HDF5 files +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.]; + +%% An example to check that buffers are read in correctly from .mat files +read_in_test = struct(); +read_in_test.vector = int32(0:11); +read_in_test.matrix = reshape(0:11, 2, 6); +read_in_test.tensor = reshape(0:11, 2, 3, 2); + +%% 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("unit_test_data/structure_array.mat", "-v7.3"); diff --git a/tdms/tests/unit/benchmark_scripts/hdf5_test_file.hdf5 b/tdms/tests/unit/benchmark_scripts/hdf5_test_file.hdf5 new file mode 100644 index 000000000..c96da6360 Binary files /dev/null and b/tdms/tests/unit/benchmark_scripts/hdf5_test_file.hdf5 differ diff --git a/tdms/tests/unit/benchmark_scripts/setup_unit_tests.m b/tdms/tests/unit/benchmark_scripts/setup_unit_tests.m new file mode 100644 index 000000000..e38e5b14d --- /dev/null +++ b/tdms/tests/unit/benchmark_scripts/setup_unit_tests.m @@ -0,0 +1,9 @@ +if ~exist('unit_test_data', 'dir') + mkdir('unit_test_data'); +end + +create_structure_array; +create_class_data; +create_bad_class_data; + +exit; diff --git a/tdms/tests/unit/hdf5_and_tdms_objects/test_hdf5_Cuboid.cpp b/tdms/tests/unit/hdf5_and_tdms_objects/test_hdf5_Cuboid.cpp new file mode 100644 index 000000000..8260afd1f --- /dev/null +++ b/tdms/tests/unit/hdf5_and_tdms_objects/test_hdf5_Cuboid.cpp @@ -0,0 +1,40 @@ +#include "hdf5_io/hdf5_reader.h" + +// external +#include +#include +#include + +#include + +#include "unit_test_utils.h" + +using tdms_unit_test_data::tdms_object_data, + tdms_unit_test_data::tdms_bad_object_data; + +/** + * @brief Test that a Cuboid object can be successfully read in. + * + * The (correct) Cuboid we have prepared is just the array {1, 4, 2, 5, 3, 6}. + * The bad data provides us with a column vector of 7 elements. + */ +TEST_CASE("HDF5: Read Cuboid") { + Cuboid cube; + + SECTION("Read into existing object") { + HDF5Reader MATFile(tdms_object_data); + MATFile.read(&cube); + // Check expected values, noting the -1 offset that is applied because of + // MATLAB indexing + bool expected_values_recieved = cube[0] == 0 && cube[1] == 3 && + cube[2] == 1 && cube[3] == 4 && + cube[4] == 2 && cube[5] == 5; + REQUIRE(expected_values_recieved); + } + + SECTION("Throw error if too many elements provided") { + HDF5Reader MATFile(tdms_bad_object_data); + // Error should be thrown due to incorrect dimensions + REQUIRE_THROWS_AS(MATFile.read(&cube), std::runtime_error); + } +} diff --git a/tdms/tests/unit/hdf5_and_tdms_objects/test_hdf5_DispersiveMultiLayer.cpp b/tdms/tests/unit/hdf5_and_tdms_objects/test_hdf5_DispersiveMultiLayer.cpp new file mode 100644 index 000000000..6c1aedb31 --- /dev/null +++ b/tdms/tests/unit/hdf5_and_tdms_objects/test_hdf5_DispersiveMultiLayer.cpp @@ -0,0 +1,48 @@ +#include "hdf5_io/hdf5_reader.h" + +#include + +#include +#include + +#include "unit_test_utils.h" + +using Catch::Approx; +using namespace std; +using tdms_unit_test_data::tdms_object_data; + +TEST_CASE("HDF5: Read DispersiveMultiLayer") { + HDF5Reader MATFile(tdms_object_data); + // read from dispersive_aux group + DispersiveMultiLayer dml; + + SECTION("Correct data") { + vector consecutive_integers(10); + for (int i = 0; i < 10; i++) { consecutive_integers[i] = (double) i; } + + MATFile.read(&dml); + + // Assert correct data - each entry should just be the integers 0->9 + // inclusive + bool correct_data_read = true; + for (int i = 0; i < 10; i++) { + correct_data_read = correct_data_read && dml.alpha[i] == Approx(i) && + dml.beta[i] == Approx(i) && dml.gamma[i] == Approx(i); + } + correct_data_read = correct_data_read && + equal(dml.kappa.x.begin(), dml.kappa.x.end(), + consecutive_integers.begin()) && + equal(dml.kappa.y.begin(), dml.kappa.y.end(), + consecutive_integers.begin()) && + equal(dml.kappa.z.begin(), dml.kappa.z.end(), + consecutive_integers.begin()) && + equal(dml.sigma.x.begin(), dml.sigma.x.end(), + consecutive_integers.begin()) && + equal(dml.sigma.y.begin(), dml.sigma.y.end(), + consecutive_integers.begin()) && + equal(dml.sigma.z.begin(), dml.sigma.z.end(), + consecutive_integers.begin()); + + REQUIRE(correct_data_read); + } +} diff --git a/tdms/tests/unit/hdf5_and_tdms_objects/test_hdf5_FrequencyVector.cpp b/tdms/tests/unit/hdf5_and_tdms_objects/test_hdf5_FrequencyVector.cpp new file mode 100644 index 000000000..f6664a618 --- /dev/null +++ b/tdms/tests/unit/hdf5_and_tdms_objects/test_hdf5_FrequencyVector.cpp @@ -0,0 +1,65 @@ +#include "hdf5_io/hdf5_reader.h" + +#include + +// external +#include +#include +#include + +// tdms +#include "unit_test_utils.h" + +using Catch::Approx; +using tdms_unit_test_data::tdms_object_data, + tdms_unit_test_data::tdms_bad_object_data; + +inline int EXPECTED_VEC_SIZE = 4; + +/** + * @brief Test the reading of frequency vectors from the input file, and failure + * cases when they are of the incorrect dimension. + * + * f_vec is a group whose data consists of: + * fx_vec: double[4] ROW vector whose entries are 1/4, 2/4, 3/4, 4/4 + * fy_vec: double[4] COL vector whose entries are -1/4, -2/4, -3/4, -4/4 + * + * f_vec_bad is a group whose data consists of: + * fx_vec: double[2,2] of zero entries + * fy_vec: double[1,2] of zero entries. + */ +TEST_CASE("HDF5: Read FrequencyVector") { + // FrequencyVectors are automatically read from the structure array "f_vec", + // and consist of real-valued arrays of the same length "fx_vec" and "fy_vec" + FrequencyVectors f_vec; + + SECTION("Correct data") { + HDF5Reader MATFile(tdms_object_data); + + SECTION("Read into existing FrequencyVectors object") { + MATFile.read(&f_vec); + } + SECTION("Return FrequencyVectors object") { f_vec = MATFile.read(); } + + // Confirm expected sizes + CHECK(f_vec.x.size() == EXPECTED_VEC_SIZE); + CHECK(f_vec.y.size() == EXPECTED_VEC_SIZE); + // Confirm correct assignment of entries + bool x_entries_correct = true; + bool y_entries_correct = true; + for (int i = 0; i < EXPECTED_VEC_SIZE; i++) { + x_entries_correct = + x_entries_correct && (f_vec.x[i] == Approx((i + 1) / 4.)); + y_entries_correct = + y_entries_correct && (f_vec.y[i] == Approx(-(i + 1) / 4.)); + } + REQUIRE(x_entries_correct); + REQUIRE(y_entries_correct); + } + + // Bad object data provides a 2D array for fx_vec component + SECTION("Incorrect sizes") { + HDF5Reader MATFile(tdms_bad_object_data); + REQUIRE_THROWS_AS(MATFile.read(&f_vec), std::runtime_error); + } +} 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/test_hdf5_dimension.cpp b/tdms/tests/unit/hdf5_io_tests/test_hdf5_dimension.cpp new file mode 100644 index 000000000..6babc7b27 --- /dev/null +++ b/tdms/tests/unit/hdf5_io_tests/test_hdf5_dimension.cpp @@ -0,0 +1,41 @@ +/** + * @file test_hdf5_dimension.cpp + * @author William Graham + * @brief Unit tests for the H5Dimension object + */ +#include "hdf5_io/hdf5_dimension.h" +#include "hdf5_io/hdf5_reader.h" + +#include + +#include + +#include "unit_test_utils.h" + +using namespace std; +using tdms_unit_test_data::struct_testdata; + +TEST_CASE("Fetch dimensions correctly") { + HDF5Reader MATFile(struct_testdata); + + SECTION("2D array") { + H5Dimension two_by_two = MATFile.shape_of("example_struct", "double_22"); + bool two_by_two_dimensions_read_in = + (two_by_two == vector{2, 2}) && (!two_by_two.is_1D()); + REQUIRE(two_by_two_dimensions_read_in); + } + + SECTION("3D array") { + H5Dimension three_four_five = + MATFile.shape_of("example_struct", "uint_345"); + bool three_four_five_read_in = + (three_four_five == vector{3, 4, 5}) && + (!three_four_five.is_1D()); + } + + SECTION("1D char array") { + H5Dimension tdms_dims = MATFile.shape_of("example_struct", "string"); + bool tdms_dims_correct = (tdms_dims.is_1D()) && (tdms_dims.max_dim() == 4); + REQUIRE(tdms_dims_correct); + } +} 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..01ea48c0e --- /dev/null +++ b/tdms/tests/unit/hdf5_io_tests/test_hdf5_reader.cpp @@ -0,0 +1,139 @@ +/** + * @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 + +#include + +#include "unit_test_utils.h" + +using namespace std; +using tdms_tests::uint16s_to_string; +using tdms_unit_test_data::struct_testdata, tdms_unit_test_data::hdf5_test_file; + +TEST_CASE("HDF5: 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_dataset_in_group("example_struct", "double_half", &one_half); + MATFile.read_dataset_in_group("example_struct", "double_no_decimal", + &unity); + MATFile.read_dataset_in_group("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_dataset_in_group("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_dataset_in_group("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_dataset_in_group("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] + } +} + +/** @brief Test the performance of read_dataset_in_group, on both MATLAB files + * and HDF5 files */ +TEST_CASE("HDF5Reader::read_dataset_in_group") { + vector read_buffer; + read_buffer.reserve(12); + // Used to check that data has been read in correctly + bool entries_read_correctly = true; + + SECTION(".mat files") { + HDF5Reader Hfile(struct_testdata); + + SECTION("Vector [int32]") { + // We do an extra loop over the entries here to ensure that our final + // check confirms that both the int data and the doubles that they were + // cast to are correct + vector int_buffer(12); + Hfile.read_dataset_in_group("read_in_test", "vector", int_buffer.data()); + for (int i = 0; i < 12; i++) { + entries_read_correctly = entries_read_correctly && int_buffer[i] == i; + read_buffer[i] = (double) int_buffer[i]; + } + } + SECTION("Matrix [double]") { + Hfile.read_dataset_in_group("read_in_test", "matrix", read_buffer.data()); + } + SECTION("Tensor [double]") { + Hfile.read_dataset_in_group("read_in_test", "tensor", read_buffer.data()); + } + } + + SECTION(".hdf5 files") { + HDF5Reader Hfile(hdf5_test_file); + + // h5py saves int dtype at 64-bit integers + SECTION("Vector [int64]") { + // We do an extra loop over the entries here to ensure that our final + // check confirms that both the int data and the doubles that they were + // cast to are correct + vector int_buffer(12); + Hfile.read_dataset_in_group("read_in_test", "vector_int", + int_buffer.data()); + for (int i = 0; i < 12; i++) { + entries_read_correctly = entries_read_correctly && int_buffer[i] == i; + read_buffer[i] = (double) int_buffer[i]; + } + } + SECTION("Matrix [double]") { + Hfile.read_dataset_in_group("read_in_test", "matrix_double", + read_buffer.data()); + } + SECTION("Tensor [double]") { + Hfile.read_dataset_in_group("read_in_test", "tensor_double", + read_buffer.data()); + } + } + + for (int i = 0; i < 12; i++) { + entries_read_correctly = + entries_read_correctly && read_buffer[i] == Catch::Approx(i); + } + REQUIRE(entries_read_correctly); +} diff --git a/tdms/tests/unit/matlab_benchmark_scripts/readme.md b/tdms/tests/unit/matlab_benchmark_scripts/readme.md deleted file mode 100644 index 6f2b925bd..000000000 --- a/tdms/tests/unit/matlab_benchmark_scripts/readme.md +++ /dev/null @@ -1,8 +0,0 @@ -# **`MATLAB` Benchmarking Scripts** - -The directory `matlab_benchmark_scripts` contains scripts which perform bandlimited interpolation (BLi) using `MATLAB`'s `interp` function. -`TDMS`'s interpolation schemes are based off this `MATLAB` function (specficially, in the coefficients the scheme uses to interpolate). - -In order to test that the interpolation is correctly implimented in the `TDMS` source, we provide unit tests that benchmark against `MATLAB`'s implimentations. These scripts are provided here for developer use and documentation. - -Currently; the unit tests have the output error values from `MATLAB` hard-coded into the source, and are required to achieve a comparable level of accuracy for the tests to pass. Each script contains a note referencing which unit test it provides a benchmark for.