From 86db80ffc34da12454fe8505052430340df62f39 Mon Sep 17 00:00:00 2001 From: Max Luebke Date: Thu, 2 Mar 2023 16:42:05 +0100 Subject: [PATCH 1/5] feat: add Field data structure as substitution of field declaration using std::vector --- include/poet/Field.hpp | 151 ++++++++++++++++++++++++++++++ src/CMakeLists.txt | 4 +- src/DataStructures/CMakeLists.txt | 6 ++ src/DataStructures/Field.cpp | 134 ++++++++++++++++++++++++++ 4 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 include/poet/Field.hpp create mode 100644 src/DataStructures/CMakeLists.txt create mode 100644 src/DataStructures/Field.cpp diff --git a/include/poet/Field.hpp b/include/poet/Field.hpp new file mode 100644 index 000000000..4a35c99e2 --- /dev/null +++ b/include/poet/Field.hpp @@ -0,0 +1,151 @@ +#ifndef FIELD_H_ +#define FIELD_H_ + +#include +#include +#include +#include +#include +#include + +namespace poet { + +using FieldColumn = std::vector; + +/** + * Stores data for input/output of a module. The class keeps track of all + * defined properties with name and import/export to 1D and 2D STL vectors. + * Also, it eases the update process of a field with an input from another + * field. + * + * It can be seen as an R-like data frame, but with less access to the members. + * Species values are stored as "columns", where column is a STL vector. + */ +class Field : private std::unordered_map { +public: + /** + * Creates a new instance of a field with fixed expected vector size. + * + * \param vec_s expected vector size of each component/column. \ + */ + Field(uint32_t vec_s) : req_vec_size(vec_s){}; + + /** + * Initializes instance with a 2D vector and according names for each columnn. + * There is no check if names were given multiple times. The order of name + * vector also defines the ordering of the output. + * + * \param input 2D vector using STL semantic describing the current state of + * the field. + * \param prop_vec Name of each vector in the input. Shall match + * the count of vectors. + * + * \exception std::runtime_error If prop_vec size doesn't match input vector + * size or column vectors size doesn't match expected vector size. + */ + void InitFromVec(const std::vector> &input, + const std::vector &prop_vec); + + /** + * Initializes instance with a 1D continious memory vector and according names + * for each columnn. There is no check if names were given multiple times. The + * order of name vector also defines the ordering of the output. + * + * \param input 1D vector using STL semantic describing the current state of + * the field storing each column starting at index *i times requested vector + * size*. + * \param prop_vec Name of each vector in the input. Shall match the + * count of vectors. + * + * \exception std::runtime_error If prop_vec size doesn't match input vector + * size or column vectors size doesn't match expected vector size. + */ + void InitFromVec(const std::vector &input, + const std::vector &prop_vec); + + /** + * Returns a reference to the column vector with given name. Creates a new + * vector if prop was not found. The prop name will be added to the end of the + * list. + * + * \param key Name of the prop. + * + * \return Reference to the column vector. + */ + FieldColumn &operator[](const std::string &key); + + /** + * Returns the names of all species defined in the instance. + * + * \return Vector containing all species names in output order. + */ + auto GetProps() const { return this->props; }; + + /** + * Return the requested vector size. + * + * \return Requested vector size set in the instanciation of the object. + */ + auto GetRequestedVecSize() const { return this->req_vec_size; }; + + /** + * Updates all species with values from another field. If one element of the + * input field doesn't match the names of the calling instance it will get + * skipped. + * + * \param input Field to update the current instance's columns. + */ + void UpdateFromField(const Field &input); + + /** + * Builds a new 1D vector with the current state of the instance. The output + * order is given by the given species name vector set earlier and/or added + * values using the [] operator. + * + * \return 1D STL vector stored each column one after another. + */ + auto AsVector() const -> FieldColumn; + + /** + * Builds a new 2D vector with the current state of the instance. The output + * order is given by the given species name vector set earlier and/or added + * values using the [] operator. + * + * \return 2D STL vector stored each column one after anothe in a new vector + * element. + */ + auto As2DVector() const -> std::vector; + + /** + * Read in a (previously exported) 1D vector. It has to have the same + * dimensions as the current column count times the requested vector size of + * this instance. Import order is given by the species name vector. + * + * \param cont_field 1D field as vector. + * + * \exception std::runtime_error Input vector does not match the expected + * size. + */ + void SetFromVector(const FieldColumn &cont_field); + + /** + * Read in a (previously exported) 2D vector. It has to have the same + * dimensions as the current column count of this instance and each vector + * must have the size of the requested vector size. Import order is given by + * the species name vector. + * + * \param cont_field 2D field as vector. + * + * \exception std::runtime_error Input vector has more or less elements than + * the instance or a column vector does not match expected vector size. + */ + void SetFromVector(const std::vector &cont_field); + +private: + const uint32_t req_vec_size; + + std::vector props; +}; + +} // namespace poet +#endif // FIELD_H_ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d1061dc00..b411d8f55 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,3 +1,5 @@ +add_subdirectory(DataStructures) + file(GLOB poet_lib_SRC CONFIGURE_DEPENDS "*.cpp" "*.c") @@ -10,7 +12,7 @@ option(POET_DHT_DEBUG "Build with DHT debug info" OFF) add_library(poet_lib ${poet_lib_SRC}) target_include_directories(poet_lib PUBLIC ${PROJECT_SOURCE_DIR}/include) target_link_libraries(poet_lib PUBLIC - MPI::MPI_C ${MATH_LIBRARY} ${CRYPTO_LIBRARY} RRuntime tug PhreeqcRM) + MPI::MPI_C ${MATH_LIBRARY} ${CRYPTO_LIBRARY} RRuntime tug PhreeqcRM DataStructures) target_compile_definitions(poet_lib PUBLIC STRICT_R_HEADERS OMPI_SKIP_MPICXX) if(POET_DHT_DEBUG) diff --git a/src/DataStructures/CMakeLists.txt b/src/DataStructures/CMakeLists.txt new file mode 100644 index 000000000..0c3f6a156 --- /dev/null +++ b/src/DataStructures/CMakeLists.txt @@ -0,0 +1,6 @@ +file(GLOB DataStructures_SRC + CONFIGURE_DEPENDS + "*.cpp" "*.c") + +add_library(DataStructures ${DataStructures_SRC}) +target_include_directories(DataStructures PUBLIC ${PROJECT_SOURCE_DIR}/include) diff --git a/src/DataStructures/Field.cpp b/src/DataStructures/Field.cpp new file mode 100644 index 000000000..13b143c1e --- /dev/null +++ b/src/DataStructures/Field.cpp @@ -0,0 +1,134 @@ +#include "poet/Field.hpp" +#include +#include +#include +#include +#include + +void poet::Field::InitFromVec(const std::vector> &input, + const std::vector &prop_vec) { + if (prop_vec.size() != input.size()) { + throw std::runtime_error("Prop vector shall name all elements."); + } + + auto name_it = prop_vec.begin(); + + for (const auto &in_vec : input) { + if (in_vec.size() != req_vec_size) { + throw std::runtime_error( + "Input vector doesn't match expected vector size."); + } + this->insert({*(name_it++), in_vec}); + } + + this->props = prop_vec; +} + +void poet::Field::InitFromVec(const std::vector &input, + const std::vector &prop_vec) { + const uint32_t expected_size = prop_vec.size() * req_vec_size; + + if (expected_size != input.size()) { + throw std::runtime_error( + "Input vector have more (or less) elements than expected."); + } + + auto name_it = prop_vec.begin(); + + for (uint32_t i = 0; i < expected_size; i += req_vec_size) { + auto input_pair = std::make_pair( + *(name_it++), std::vector(&input[i], &input[i + req_vec_size])); + this->insert(input_pair); + } + + this->props = prop_vec; +} + +auto poet::Field::AsVector() const -> poet::FieldColumn { + const uint32_t min_size = req_vec_size * this->size(); + + poet::FieldColumn output; + output.reserve(min_size); + + for (const auto &elem : props) { + const auto map_it = this->find(elem); + + const auto start = map_it->second.begin(); + const auto end = map_it->second.end(); + + output.insert(output.end(), start, end); + } + + return output; +} + +void poet::Field::SetFromVector(const poet::FieldColumn &cont_field) { + if (cont_field.size() != this->size() * req_vec_size) { + throw std::runtime_error( + "Field::SetFromVector: vector does not match expected size"); + } + + uint32_t vec_p = 0; + for (const auto &elem : props) { + const auto start = cont_field.begin() + vec_p; + const auto end = start + req_vec_size; + + const auto map_it = this->find(elem); + + map_it->second = FieldColumn(start, end); + + vec_p += req_vec_size; + } +} + +void poet::Field::SetFromVector( + const std::vector &cont_field) { + if (cont_field.size() != this->size()) { + throw std::runtime_error( + "Input field contains more or less elements than this container."); + } + + auto in_field_it = cont_field.begin(); + + for (const auto &elem : props) { + if (in_field_it->size() != req_vec_size) { + throw std::runtime_error( + "One vector contains more or less elements than expected."); + } + + const auto map_it = this->find(elem); + + map_it->second = *(in_field_it++); + } +} + +void poet::Field::UpdateFromField(const poet::Field &input) { + for (const auto &input_elem : input) { + auto it_self = this->find(input_elem.first); + if (it_self == this->end()) { + continue; + } + + it_self->second = input_elem.second; + } +} + +auto poet::Field::As2DVector() const -> std::vector { + std::vector output; + output.reserve(this->size()); + + for (const auto &elem : props) { + const auto map_it = this->find(elem); + output.push_back(map_it->second); + } + + return output; +} + +poet::FieldColumn &poet::Field::operator[](const std::string &key) { + if (this->find(key) == this->end()) { + props.push_back(key); + } + + return std::unordered_map::operator[](key); +} From 1129761df30aa8679c47555b3db550f7b58d2d59 Mon Sep 17 00:00:00 2001 From: Max Luebke Date: Thu, 2 Mar 2023 16:44:59 +0100 Subject: [PATCH 2/5] chore: add doctest as dependency --- .gitmodules | 3 +++ ext/doctest | 1 + 2 files changed, 4 insertions(+) create mode 160000 ext/doctest diff --git a/.gitmodules b/.gitmodules index 43b424fd9..f39a21a09 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,3 +5,6 @@ [submodule "ext/phreeqcrm"] path = ext/phreeqcrm url = ../../mluebke/phreeqcrm-gfz.git +[submodule "ext/doctest"] + path = ext/doctest + url = https://github.com/doctest/doctest.git diff --git a/ext/doctest b/ext/doctest new file mode 160000 index 000000000..8fdfd113d --- /dev/null +++ b/ext/doctest @@ -0,0 +1 @@ +Subproject commit 8fdfd113dcb4ad1a31705ff8ddb13a21a505bad8 From 357936b639eb809052593085ffa3b37d55bca578 Mon Sep 17 00:00:00 2001 From: Max Luebke Date: Fri, 3 Mar 2023 14:16:26 +0100 Subject: [PATCH 3/5] test: add test cases for Field class --- CMakeLists.txt | 10 +++ test/CMakeLists.txt | 11 +++ test/setup.cpp | 2 + test/testDataStructures.cpp | 175 ++++++++++++++++++++++++++++++++++++ 4 files changed, 198 insertions(+) create mode 100644 test/CMakeLists.txt create mode 100644 test/setup.cpp create mode 100644 test/testDataStructures.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a277ed9c..01946e0b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,9 +24,19 @@ add_subdirectory(data) add_subdirectory(app) add_subdirectory(bench/dolo_diffu_inner) +# as tug will also pull in doctest as a dependency +set(TUG_ENABLE_TESTING OFF CACHE BOOL "" FORCE) + add_subdirectory(ext/tug EXCLUDE_FROM_ALL) add_subdirectory(ext/phreeqcrm EXCLUDE_FROM_ALL) +option(POET_ENABLE_TESTING "Build test suite for POET" OFF) + +if (POET_ENABLE_TESTING) + add_subdirectory(ext/doctest EXCLUDE_FROM_ALL) + add_subdirectory(test) +endif() + option(BUILD_DOC "Build documentation with doxygen" OFF) if(BUILD_DOC) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 000000000..9d3a5ac43 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,11 @@ +file(GLOB test_SRC + CONFIGURE_DEPENDS + "*.cpp" "*.c") + +add_executable(testPOET ${test_SRC}) +target_link_libraries(testPOET doctest poet_lib) + +add_custom_target(check + COMMAND $ + DEPENDS testPOET +) diff --git a/test/setup.cpp b/test/setup.cpp new file mode 100644 index 000000000..0a3f254ea --- /dev/null +++ b/test/setup.cpp @@ -0,0 +1,2 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include diff --git a/test/testDataStructures.cpp b/test/testDataStructures.cpp new file mode 100644 index 000000000..c3046f275 --- /dev/null +++ b/test/testDataStructures.cpp @@ -0,0 +1,175 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace poet; + +#define CHECK_AND_FAIL_LOOP(val1, val2) \ + if (val1 != val2) { \ + FAIL_CHECK("Value differs: ", val1, " != ", val2); \ + } + +TEST_CASE("Field") { + constexpr uint32_t VEC_SIZE = 5; + constexpr uint32_t VEC_COUNT = 3; + constexpr double INIT_VAL = 1; + + Field dut(VEC_SIZE); + + std::vector names = {"C", "Ca", "Na"}; + std::vector init_values(names.size(), + FieldColumn(VEC_SIZE, INIT_VAL)); + + SUBCASE("Initialize field with 2D vector") { + dut.InitFromVec(init_values, names); + + auto props = dut.GetProps(); + + CHECK_EQ(names.size(), props.size()); + + const auto res = dut["C"]; + + CHECK_EQ(res.size(), VEC_SIZE); + + for (const auto &elem : res) { + CHECK_AND_FAIL_LOOP(elem, INIT_VAL); + } + } + + SUBCASE("Initialize field with 2D vector") { + std::vector init_values(VEC_SIZE * VEC_COUNT, 1); + dut.InitFromVec(init_values, names); + + auto props = dut.GetProps(); + + CHECK_EQ(names.size(), props.size()); + + const auto res = dut["C"]; + + CHECK_EQ(res.size(), VEC_SIZE); + + for (const auto &elem : res) { + CHECK_AND_FAIL_LOOP(elem, INIT_VAL); + } + } + + dut.InitFromVec(init_values, names); + + SUBCASE("Return vector") { + std::vector output = dut.AsVector(); + + CHECK(output.size() == VEC_SIZE * VEC_COUNT); + } + + constexpr double NEW_VAL = 2.; + std::vector new_val_vec(VEC_SIZE, NEW_VAL); + + dut["C"] = new_val_vec; + + SUBCASE("Check manipulation of column") { + + auto test_it = new_val_vec.begin(); + + for (const auto &val : dut["C"]) { + CHECK_EQ(val, *test_it); + test_it++; + } + } + + SUBCASE("Check correctly manipulated values of 1D vector") { + auto out_res = dut.AsVector(); + auto out_it = out_res.begin(); + + for (uint32_t i = 0; i < VEC_SIZE; i++, out_it++) { + CHECK_AND_FAIL_LOOP(NEW_VAL, *out_it); + } + + for (; out_it != out_res.end(); out_it++) { + CHECK_AND_FAIL_LOOP(INIT_VAL, *out_it); + } + } + + std::vector new_field(VEC_SIZE * VEC_COUNT); + + for (uint32_t i = 0; i < VEC_COUNT; i++) { + for (uint32_t j = 0; j < VEC_SIZE; j++) { + new_field[j + (i * VEC_SIZE)] = (double)(i + 1) / (double)(j + 1); + } + } + + dut.SetFromVector(new_field); + + SUBCASE("SetFromVector return correct field vector") { + auto ret_vec = dut.AsVector(); + + auto ret_it = ret_vec.begin(); + auto new_it = new_field.begin(); + + for (; ret_it != ret_vec.end(); ret_it++, new_it++) { + CHECK_AND_FAIL_LOOP(*ret_it, *new_it); + } + } + + SUBCASE("Get single column with new values") { + auto new_it = new_field.begin() + 2 * VEC_SIZE; + auto ret_vec = dut["Na"]; + auto ret_it = ret_vec.begin(); + + CHECK_EQ(ret_vec.size(), VEC_SIZE); + + for (; ret_it != ret_vec.end(); ret_it++, new_it++) { + CHECK_AND_FAIL_LOOP(*ret_it, *new_it); + } + } + + std::vector mg_vec = {1, 2, 3, 4, 5}; + dut["Mg"] = mg_vec; + + SUBCASE("Operator creates new element and places it at the end") { + + auto new_props = dut.GetProps(); + + CHECK_EQ(new_props.size(), 4); + CHECK_EQ(new_props[3], "Mg"); + + auto ret_vec = dut.As2DVector(); + auto mg_vec_it = mg_vec.begin(); + + for (const auto &val : ret_vec[3]) { + CHECK_AND_FAIL_LOOP(val, *(mg_vec_it++)); + } + } + + // reset field + names = dut.GetProps(); + dut.SetFromVector( + std::vector(names.size(), FieldColumn(VEC_SIZE, INIT_VAL))); + + constexpr double SOME_OTHER_VAL = -0.5; + Field some_other_field(VEC_SIZE); + std::vector some_other_props = {"Na", "Cl"}; + std::vector some_other_values(VEC_SIZE * some_other_props.size(), + SOME_OTHER_VAL); + + some_other_field.InitFromVec(some_other_values, some_other_props); + + SUBCASE("Update existing field from another field") { + dut.UpdateFromField(some_other_field); + + auto ret_vec = dut.As2DVector(); + auto ret_it = ret_vec.begin(); + + for (const auto &prop : names) { + const auto &curr_vec = *(ret_it++); + + for (const auto &val : curr_vec) { + CHECK_AND_FAIL_LOOP((prop == "Na" ? SOME_OTHER_VAL : INIT_VAL), val); + } + } + } +} From 7202a0d1ff21b98933ca2417565cd385de7ddd91 Mon Sep 17 00:00:00 2001 From: Max Luebke Date: Fri, 3 Mar 2023 14:40:52 +0100 Subject: [PATCH 4/5] chore: add 'test' to commit group --- .chglog/config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.chglog/config.yml b/.chglog/config.yml index 357d2ceed..76e761fe6 100644 --- a/.chglog/config.yml +++ b/.chglog/config.yml @@ -17,6 +17,7 @@ options: - doc - chore - ci + - test commit_groups: title_maps: feat: Features @@ -29,6 +30,7 @@ options: doc: Documentation chore: Householding and Cleanup ci: CI + test: Software Testing header: pattern: "^(\\w*)\\:\\s(.*)$" pattern_maps: From fbb2ad6a67ea17bbd2a67fa3aa5dc15df35b6bb8 Mon Sep 17 00:00:00 2001 From: Max Luebke Date: Fri, 3 Mar 2023 14:41:17 +0100 Subject: [PATCH 5/5] ci: enable run of tests in CI --- .gitlab-ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 749e3e6f2..28eb47940 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,6 +21,7 @@ image: git.gfz-potsdam.de:5000/sec34/port:builder stages: # List of stages for jobs, and their order of execution - build - release + - test variables: GIT_SUBMODULE_STRATEGY: recursive @@ -34,6 +35,13 @@ build-poet: # This job runs in the build stage, which runs first. - cmake .. - make -j$(nproc) +test-poet: + stage: test + script: + - mkdir build_test && cd build_test + - cmake -DPOET_ENABLE_TESTING=ON .. + - make -j$(nproc) check + archive-sources: # This job runs in the build stage, which runs first. image: python:3 stage: release