diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 321f188744..687753963f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,7 +124,9 @@ jobs: - name: test shell: bash - run: $GITHUB_WORKSPACE/tools/ci/gha-script.sh + run: | + CTEST_OUTPUT_ON_FAILURE=1 make test -C $GITHUB_WORKSPACE/build/ + $GITHUB_WORKSPACE/tools/ci/gha-script.sh - name: after_success shell: bash diff --git a/.gitmodules b/.gitmodules index ff91200103..4dde5fd5ee 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "vendor/fmt"] path = vendor/fmt url = https://github.com/fmtlib/fmt.git +[submodule "vendor/Catch2"] + path = vendor/Catch2 + url = https://github.com/catchorg/Catch2.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 4737ff47b2..11ae71ac65 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,6 +31,7 @@ endif() #=============================================================================== option(OPENMC_USE_OPENMP "Enable shared-memory parallelism with OpenMP" ON) +option(OPENMC_BUILD_TESTS "Build tests" ON) option(OPENMC_ENABLE_PROFILE "Compile with profiling flags" OFF) option(OPENMC_ENABLE_COVERAGE "Compile with coverage analysis flags" OFF) option(OPENMC_USE_DAGMC "Enable support for DAGMC (CAD) geometry" OFF) @@ -285,6 +286,17 @@ if (NOT gsl-lite_FOUND) target_compile_definitions(gsl-lite-v1 INTERFACE gsl_CONFIG_ALLOWS_NONSTRICT_SPAN_COMPARISON=1) endif() +#=============================================================================== +# Catch2 library +#=============================================================================== + +if(OPENMC_BUILD_TESTS) + find_package_write_status(Catch2) + if (NOT Catch2) + add_subdirectory(vendor/Catch2) + endif() +endif() + #=============================================================================== # RPATH information #=============================================================================== @@ -504,6 +516,12 @@ if (OPENMC_USE_MPI) target_link_libraries(libopenmc MPI::MPI_CXX) endif() +if (OPENMC_BUILD_TESTS) + # Add cpp tests directory + include(CTest) + add_subdirectory(tests/cpp_unit_tests) +endif() + if (OPENMC_USE_MCPL) target_compile_definitions(libopenmc PUBLIC OPENMC_MCPL) target_link_libraries(libopenmc MCPL::mcpl) diff --git a/docs/source/devguide/tests.rst b/docs/source/devguide/tests.rst index 922f47eb31..d076511b1d 100644 --- a/docs/source/devguide/tests.rst +++ b/docs/source/devguide/tests.rst @@ -45,7 +45,7 @@ Prerequisites Running Tests ------------- -To execute the test suite, go to the ``tests/`` directory and run:: +To execute the Python test suite, go to the ``tests/`` directory and run:: pytest @@ -55,6 +55,14 @@ installed and run:: pytest --cov=../openmc --cov-report=html +To execute the C++ test suite, go to your build directory and run:: + + ctest + +If you want to view testing output on failure run:: + + ctest --output-on-failure + Generating XML Inputs --------------------- @@ -66,6 +74,23 @@ run:: pytest --build-inputs +Adding C++ Unit Tests +--------------------- + +The C++ test suite uses Catch2 integrated with CTest. Each header file should +have a corresponding test file in ``tests/cpp_unit_tests/``. If the test file +does not exist run:: + + touch test_.cpp + +The file must be added to the CMake build system in +``tests/cpp_unit_tests/CMakeLists.txt``. ``test_`` should +be added to ``TEST_NAMES``. + +To add a test case to ``test_.cpp`` ensure +``catch2/catch_test_macros.hpp`` is included. A unit test can then be added +using the ``TEST_CASE`` macro and the ``REQUIRE`` assertion from Catch2. + Adding Tests to the Regression Suite ------------------------------------ diff --git a/include/openmc/distribution.h b/include/openmc/distribution.h index d3fe958caa..4fc0db2e09 100644 --- a/include/openmc/distribution.h +++ b/include/openmc/distribution.h @@ -47,14 +47,20 @@ public: // Properties const vector& x() const { return x_; } - const vector& p() const { return p_; } + const vector& prob() const { return prob_; } + const vector& alias() const { return alias_; } private: - vector x_; //!< Possible outcomes - vector p_; //!< Probability of each outcome + vector x_; //!< Possible outcomes + vector prob_; //!< Probability of accepting the uniformly sampled bin, + //!< mapped to alias method table + vector alias_; //!< Alias table //! Normalize distribution so that probabilities sum to unity void normalize(); + + //! Initialize alias tables for distribution + void init_alias(vector& x, vector& p); }; //============================================================================== diff --git a/src/distribution.cpp b/src/distribution.cpp index fc0297fec2..4b4ebd69d7 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -1,6 +1,7 @@ #include "openmc/distribution.h" #include // for copy +#include #include // for sqrt, floor, max #include // for back_inserter #include // for accumulate @@ -26,30 +27,76 @@ Discrete::Discrete(pugi::xml_node node) auto params = get_node_array(node, "parameters"); std::size_t n = params.size(); - std::copy(params.begin(), params.begin() + n / 2, std::back_inserter(x_)); - std::copy(params.begin() + n / 2, params.end(), std::back_inserter(p_)); + std::vector x_vec(params.begin(), params.begin() + n / 2); + std::vector p_vec(params.begin() + n / 2, params.end()); - normalize(); + this->init_alias(x_vec, p_vec); } Discrete::Discrete(const double* x, const double* p, int n) - : x_ {x, x + n}, p_ {p, p + n} { + std::vector x_vec(x, x + n); + std::vector p_vec(p, p + n); + + this->init_alias(x_vec, p_vec); +} + +void Discrete::init_alias(vector& x, vector& p) +{ + x_ = x; + prob_ = p; normalize(); + + // The initialization and sampling method is based on Vose + // (DOI: 10.1109/32.92917) + // Vectors for large and small probabilities based on 1/n + vector large; + vector small; + + // Set and allocate memory + vector alias(x_.size(), 0); + alias_ = alias; + + // Fill large and small vectors based on 1/n + for (size_t i = 0; i < x_.size(); i++) { + prob_[i] *= x_.size(); + if (prob_[i] > 1.0) { + large.push_back(i); + } else { + small.push_back(i); + } + } + + while (!large.empty() && !small.empty()) { + int j = small.back(); + int k = large.back(); + + // Remove last element of small + small.pop_back(); + + // Update probability and alias based on Vose's algorithm + prob_[k] += prob_[j] - 1.0; + alias_[j] = k; + + // Move large index to small vector, if it is no longer large + if (prob_[k] < 1.0) { + small.push_back(k); + large.pop_back(); + } + } } double Discrete::sample(uint64_t* seed) const { + // Alias sampling of discrete distribution int n = x_.size(); if (n > 1) { - double xi = prn(seed); - double c = 0.0; - for (int i = 0; i < n; ++i) { - c += p_[i]; - if (xi < c) - return x_[i]; + int u = prn(seed) * n; + if (prn(seed) < prob_[u]) { + return x_[u]; + } else { + return x_[alias_[u]]; } - throw std::runtime_error {"Error when sampling probability mass function."}; } else { return x_[0]; } @@ -58,8 +105,8 @@ double Discrete::sample(uint64_t* seed) const void Discrete::normalize() { // Renormalize density function so that it sums to unity - double norm = std::accumulate(p_.begin(), p_.end(), 0.0); - for (auto& p_i : p_) { + double norm = std::accumulate(prob_.begin(), prob_.end(), 0.0); + for (auto& p_i : prob_) { p_i /= norm; } } diff --git a/tests/cpp_unit_tests/CMakeLists.txt b/tests/cpp_unit_tests/CMakeLists.txt new file mode 100644 index 0000000000..3d48545b4c --- /dev/null +++ b/tests/cpp_unit_tests/CMakeLists.txt @@ -0,0 +1,10 @@ +set(TEST_NAMES + test_distribution + # Add additional unit test files here +) + +foreach(test ${TEST_NAMES}) + add_executable(${test} ${test}.cpp) + target_link_libraries(${test} Catch2::Catch2WithMain libopenmc) + add_test(NAME ${test} COMMAND ${test} WORKING_DIRECTORY ${UNIT_TEST_BIN_OUTPUT_DIR}) +endforeach() diff --git a/tests/cpp_unit_tests/test_distribution.cpp b/tests/cpp_unit_tests/test_distribution.cpp new file mode 100644 index 0000000000..e1a212db55 --- /dev/null +++ b/tests/cpp_unit_tests/test_distribution.cpp @@ -0,0 +1,81 @@ +#include "openmc/distribution.h" +#include "openmc/random_lcg.h" +#include +#include +#include +#include + +TEST_CASE("Test alias method sampling of a discrete distribution") +{ + constexpr int n_samples = 1000000; + double x[5] = {-1.6, 1.1, 20.3, 4.7, 0.9}; + double p[5] = {0.2, 0.1, 0.65, 0.02, 0.03}; + + // Initialize distribution + openmc::Discrete dist(x, p, 5); + uint64_t seed = openmc::init_seed(0, 0); + + // Calculate expected distribution mean + double mean = 0.0; + for (size_t i = 0; i < 5; i++) { + mean += x[i] * p[i]; + } + + // Sample distribution and calculate mean, standard deviation, and number of + // x[0] sampled + double dist_mean = 0.0; + double std = 0.0; + int counter = 0; + + for (size_t i = 0; i < n_samples; i++) { + auto sample = dist.sample(&seed); + std += sample * sample / n_samples; + dist_mean += sample; + + if (sample == x[0]) + counter++; + } + + dist_mean /= n_samples; + std -= dist_mean * dist_mean; + + // Require sampled distribution mean is within 4 standard deviations of the + // expected mean + REQUIRE(std::abs(dist_mean - mean) < 4 * std); + + // Require counter of number of x[0] is within the 95% confidence interval + // assuming a Poisson distribution of 200,000 + REQUIRE(std::abs((double)counter / n_samples - p[0]) < + 1.96 * std::sqrt(p[0] / n_samples)); +} + +TEST_CASE("Test alias sampling method for pugixml constructor") +{ + // XML doc node for Discrete contructor + pugi::xml_document doc; + pugi::xml_node energy = doc.append_child("energy"); + pugi::xml_node parameters = energy.append_child("parameters"); + parameters.append_child(pugi::node_pcdata) + .set_value("800 500000 30000 0.1 0.6 0.3"); + + // Initialize discrete distribution and seed + openmc::Discrete dist(energy); + uint64_t seed = openmc::init_seed(0, 0); + auto sample = dist.sample(&seed); + + // Assertions + REQUIRE(dist.x().size() == 3); + REQUIRE(dist.prob().size() == 3); + REQUIRE(dist.alias().size() == 3); + + openmc::vector correct_x = {800, 500000, 30000}; + openmc::vector correct_prob = {0.3, 1.0, 0.9}; + openmc::vector correct_alias = {1, 0, 1}; + + for (size_t i = 0; i < 3; i++) { + REQUIRE(dist.x()[i] == correct_x[i]); + REQUIRE_THAT( + dist.prob()[i], Catch::Matchers::WithinAbs(correct_prob[i], 1e-12)); + REQUIRE(dist.alias()[i] == correct_alias[i]); + } +} diff --git a/tests/regression_tests/deplete_with_transport/last_step_reference_materials.xml b/tests/regression_tests/deplete_with_transport/last_step_reference_materials.xml index fc9fd0cf5b..b818566837 100644 --- a/tests/regression_tests/deplete_with_transport/last_step_reference_materials.xml +++ b/tests/regression_tests/deplete_with_transport/last_step_reference_materials.xml @@ -4,43 +4,43 @@ - + - + - + - + - + - - + + - + - + - - + + @@ -48,12 +48,12 @@ - - + + - + @@ -67,7 +67,7 @@ - + @@ -80,7 +80,7 @@ - + @@ -89,26 +89,26 @@ - - + + - + - + - + - + @@ -116,13 +116,13 @@ - - + + - + @@ -132,40 +132,40 @@ - + - + - + - + - - + + - + - + - - + + @@ -178,35 +178,35 @@ - + - + - - + + - - + + - + - + @@ -214,7 +214,7 @@ - + @@ -228,13 +228,13 @@ - + - + @@ -256,12 +256,12 @@ - + - + @@ -270,7 +270,7 @@ - + @@ -284,33 +284,33 @@ - + - + - + - - + + - + - - + + @@ -324,13 +324,13 @@ - - - + + + - + @@ -339,13 +339,13 @@ - + - - + + @@ -353,27 +353,27 @@ - + - - + + - + - + - - + + @@ -386,7 +386,7 @@ - + @@ -395,7 +395,7 @@ - + @@ -414,21 +414,21 @@ - + - + - + - + @@ -441,21 +441,21 @@ - + - + - + - + @@ -463,14 +463,14 @@ - - + + - - + + @@ -478,13 +478,13 @@ - + - + @@ -492,34 +492,34 @@ - - + + - + - - - + + + - + - - + + @@ -534,13 +534,13 @@ - + - - + + @@ -548,67 +548,67 @@ - - + + - - + + - - + + - - + + - - - + + + - + - - - + + + - - + + - - - + + + - + @@ -622,8 +622,8 @@ - - + + @@ -631,13 +631,13 @@ - + - - + + @@ -646,11 +646,11 @@ - + - + @@ -658,7 +658,7 @@ - + @@ -673,67 +673,67 @@ - + - + - + - - + + - + - - + + - + - + - + - - + + - - - + + + - + @@ -747,8 +747,8 @@ - - + + @@ -761,7 +761,7 @@ - + @@ -775,16 +775,16 @@ - + - + - + @@ -797,26 +797,26 @@ - - + + - + - - - + + + - + @@ -839,26 +839,26 @@ - - + + - - + + - + - + - + @@ -867,13 +867,13 @@ - - + + - - + + @@ -881,7 +881,7 @@ - + @@ -900,21 +900,21 @@ - - + + - + - + - + @@ -922,8 +922,8 @@ - - + + @@ -934,14 +934,14 @@ - + - + - + @@ -950,7 +950,7 @@ - + @@ -964,12 +964,12 @@ - - + + - + @@ -983,21 +983,21 @@ - + - + - + diff --git a/tests/regression_tests/deplete_with_transport/test_reference.h5 b/tests/regression_tests/deplete_with_transport/test_reference.h5 index d478d628ef..952994b86b 100644 Binary files a/tests/regression_tests/deplete_with_transport/test_reference.h5 and b/tests/regression_tests/deplete_with_transport/test_reference.h5 differ diff --git a/tests/regression_tests/source/results_true.dat b/tests/regression_tests/source/results_true.dat index 62eaf6eff9..f0d69afb5b 100644 --- a/tests/regression_tests/source/results_true.dat +++ b/tests/regression_tests/source/results_true.dat @@ -1,2 +1,2 @@ k-combined: -2.865754E-01 6.762423E-03 +3.073726E-01 2.535074E-03 diff --git a/tests/regression_tests/surface_source/surface_source_true.h5 b/tests/regression_tests/surface_source/surface_source_true.h5 index c26afee867..eb65846d1c 100644 Binary files a/tests/regression_tests/surface_source/surface_source_true.h5 and b/tests/regression_tests/surface_source/surface_source_true.h5 differ diff --git a/vendor/Catch2 b/vendor/Catch2 new file mode 160000 index 0000000000..41990e0fe6 --- /dev/null +++ b/vendor/Catch2 @@ -0,0 +1 @@ +Subproject commit 41990e0fe6274d716134eecfd4d780e976c8fbf5