Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-10-29 08:25:02

0001 // SPDX-License-Identifier: LGPL-3.0-or-later
0002 // Copyright (C) 2026 Wouter Deconinck
0003 //
0004 // Tests for the CachedBitGenerator, the Generator front-end and the RandomSvc
0005 //
0006 #include <algorithms/random.h>
0007 
0008 #include <catch2/catch_approx.hpp>
0009 #include <catch2/catch_test_macros.hpp>
0010 
0011 #include <concepts>
0012 #include <cstddef>
0013 #include <cstdint>
0014 #include <limits>
0015 #include <memory>
0016 #include <random>
0017 #include <string>
0018 #include <string_view>
0019 #include <vector>
0020 
0021 using namespace algorithms;
0022 using Catch::Approx;
0023 
0024 namespace {
0025 
0026 using value_type = detail::CachedBitGenerator::result_type;
0027 
0028 // Engine that returns a running counter and records how often and with which size it is called
0029 struct CountingEngine {
0030   std::shared_ptr<value_type> next = std::make_shared<value_type>(0);
0031   std::shared_ptr<std::vector<std::size_t>> calls =
0032       std::make_shared<std::vector<std::size_t>>();
0033 
0034   std::vector<value_type> operator()(std::size_t n) const {
0035     calls->push_back(n);
0036     std::vector<value_type> ret(n);
0037     for (auto& v : ret) {
0038       v = (*next)++;
0039     }
0040     return ret;
0041   }
0042 };
0043 
0044 // Engine based on a seeded Mersenne twister
0045 RandomEngineCB mtEngine(std::size_t seed) {
0046   auto gen = std::make_shared<std::mt19937_64>(seed);
0047   return [gen](std::size_t n) {
0048     std::vector<value_type> ret(n);
0049     for (auto& v : ret) {
0050       v = (*gen)();
0051     }
0052     return ret;
0053   };
0054 }
0055 
0056 // Engine that always returns the same value
0057 RandomEngineCB constantEngine(value_type value) {
0058   return [value](std::size_t n) { return std::vector<value_type>(n, value); };
0059 }
0060 
0061 struct Message {
0062   LogLevel level;
0063   std::string msg;
0064   bool operator==(const Message&) const = default;
0065 };
0066 
0067 std::vector<Message>& messages() {
0068   static std::vector<Message> m;
0069   return m;
0070 }
0071 
0072 void capture() {
0073   messages().clear();
0074   LogSvc::instance().init([](LogLevel l, std::string_view, std::string_view msg) {
0075     messages().push_back({l, std::string{msg}});
0076   });
0077 }
0078 
0079 constexpr int kDraws = 10000;
0080 
0081 } // namespace
0082 
0083 TEST_CASE("CachedBitGenerator is a uniform random bit generator", "[random]") {
0084   STATIC_REQUIRE(std::uniform_random_bit_generator<detail::CachedBitGenerator>);
0085   STATIC_REQUIRE(detail::CachedBitGenerator::min() == 0);
0086   STATIC_REQUIRE(detail::CachedBitGenerator::max() == std::numeric_limits<value_type>::max());
0087 }
0088 
0089 TEST_CASE("CachedBitGenerator refreshes its cache when exhausted", "[random]") {
0090   const CountingEngine engine;
0091   detail::CachedBitGenerator gen{engine, 3};
0092 
0093   // no numbers are requested before the first access
0094   CHECK(engine.calls->empty());
0095 
0096   std::vector<value_type> values;
0097   for (int i = 0; i < 7; ++i) {
0098     values.push_back(gen());
0099   }
0100   CHECK(values == std::vector<value_type>{0, 1, 2, 3, 4, 5, 6});
0101   CHECK(*engine.calls == std::vector<std::size_t>{3, 3, 3});
0102 }
0103 
0104 TEST_CASE("Generator draws uniform integers", "[random]") {
0105   const Generator gen{mtEngine(1), 16};
0106 
0107   for (int i = 0; i < kDraws; ++i) {
0108     const auto def = gen.uniform_int();
0109     STATIC_REQUIRE(std::is_same_v<decltype(def), const int>);
0110     REQUIRE((def == 0 || def == 1));
0111 
0112     const auto custom = gen.uniform_int<int64_t>(-5, 5);
0113     STATIC_REQUIRE(std::is_same_v<decltype(custom), const int64_t>);
0114     REQUIRE(custom >= -5);
0115     REQUIRE(custom <= 5);
0116   }
0117 }
0118 
0119 TEST_CASE("Generator draws uniform floating point numbers", "[random]") {
0120   const Generator gen{mtEngine(2), 16};
0121 
0122   double sum = 0;
0123   for (int i = 0; i < kDraws; ++i) {
0124     const auto def = gen.uniform_double();
0125     STATIC_REQUIRE(std::is_same_v<decltype(def), const double>);
0126     REQUIRE(def >= 0.);
0127     REQUIRE(def < 1.);
0128     sum += def;
0129 
0130     const auto custom = gen.uniform_double<float>(2.f, 3.f);
0131     STATIC_REQUIRE(std::is_same_v<decltype(custom), const float>);
0132     REQUIRE(custom >= 2.f);
0133     REQUIRE(custom <= 3.f);
0134   }
0135   CHECK(sum / kDraws == Approx(0.5).margin(0.02));
0136 }
0137 
0138 TEST_CASE("Generator draws Poisson distributed numbers", "[random]") {
0139   const Generator gen{mtEngine(3), 16};
0140 
0141   double sum_def    = 0;
0142   double sum_custom = 0;
0143   for (int i = 0; i < kDraws; ++i) {
0144     const auto def = gen.poisson();
0145     STATIC_REQUIRE(std::is_same_v<decltype(def), const int>);
0146     REQUIRE(def >= 0);
0147     sum_def += def;
0148 
0149     const auto custom = gen.poisson<int64_t>(2.5);
0150     STATIC_REQUIRE(std::is_same_v<decltype(custom), const int64_t>);
0151     REQUIRE(custom >= 0);
0152     sum_custom += custom;
0153   }
0154   CHECK(sum_def / kDraws == Approx(1.).margin(0.05));
0155   CHECK(sum_custom / kDraws == Approx(2.5).margin(0.1));
0156 }
0157 
0158 TEST_CASE("Generator draws exponentially distributed numbers", "[random]") {
0159   const Generator gen{mtEngine(4), 16};
0160 
0161   double sum_def    = 0;
0162   double sum_custom = 0;
0163   for (int i = 0; i < kDraws; ++i) {
0164     const auto def = gen.exponential();
0165     REQUIRE(def >= 0.);
0166     sum_def += def;
0167 
0168     const auto custom = gen.exponential<float>(4.f);
0169     STATIC_REQUIRE(std::is_same_v<decltype(custom), const float>);
0170     REQUIRE(custom >= 0.f);
0171     sum_custom += custom;
0172   }
0173   CHECK(sum_def / kDraws == Approx(1.).margin(0.05));
0174   CHECK(sum_custom / kDraws == Approx(0.25).margin(0.0125));
0175 }
0176 
0177 TEST_CASE("Generator draws Gaussian distributed numbers", "[random]") {
0178   const Generator gen{mtEngine(5), 16};
0179 
0180   double sum_def    = 0;
0181   double sum2_def   = 0;
0182   double sum_custom = 0;
0183   for (int i = 0; i < kDraws; ++i) {
0184     const auto def = gen.gaussian();
0185     sum_def += def;
0186     sum2_def += def * def;
0187 
0188     const auto custom = gen.gaussian<float>(10.f, 2.f);
0189     STATIC_REQUIRE(std::is_same_v<decltype(custom), const float>);
0190     sum_custom += custom;
0191   }
0192   CHECK(sum_def / kDraws == Approx(0.).margin(0.05));
0193   CHECK(sum2_def / kDraws == Approx(1.).margin(0.05));
0194   CHECK(sum_custom / kDraws == Approx(10.).margin(0.1));
0195 }
0196 
0197 TEST_CASE("Generators with the same engine sequence draw the same numbers", "[random]") {
0198   const Generator gen1{mtEngine(6), 16};
0199   const Generator gen2{mtEngine(6), 16};
0200   for (int i = 0; i < 100; ++i) {
0201     REQUIRE(gen1.gaussian() == gen2.gaussian());
0202   }
0203 }
0204 
0205 TEST_CASE("RandomSvc creates engines that return the requested number of values", "[random]") {
0206   auto engine = RandomSvc::instance().createEngine(7);
0207   CHECK(engine(0).empty());
0208   CHECK(engine(5).size() == 5);
0209 }
0210 
0211 TEST_CASE("RandomSvc engines with the same seed draw the same numbers", "[random]") {
0212   auto& svc    = RandomSvc::instance();
0213   auto engine1 = svc.createEngine(8);
0214   auto engine2 = svc.createEngine(8);
0215   CHECK(engine1(5) == engine2(5));
0216 }
0217 
0218 TEST_CASE("RandomSvc engines draw new numbers on every call", "[random]") {
0219   auto engine = RandomSvc::instance().createEngine(9);
0220   CHECK(engine(5) != engine(5));
0221 }
0222 
0223 TEST_CASE("RandomSvc engines with different seeds draw different numbers", "[random]") {
0224   auto& svc    = RandomSvc::instance();
0225   auto engine1 = svc.createEngine(10);
0226   auto engine2 = svc.createEngine(11);
0227   CHECK(engine1(5) != engine2(5));
0228 }
0229 
0230 TEST_CASE("RandomSvc engines are independent", "[random]") {
0231   auto& svc    = RandomSvc::instance();
0232   auto engine1 = svc.createEngine(12);
0233   auto engine2 = svc.createEngine(12);
0234   auto engine3 = svc.createEngine(13);
0235 
0236   // drawing from other engines does not affect the sequence of an engine
0237   const auto first = engine1(5);
0238   engine3(5);
0239   const auto second = engine1(5);
0240   CHECK(first != second);
0241   CHECK(engine2(5) == first);
0242   CHECK(engine2(5) == second);
0243 }
0244 
0245 TEST_CASE("Generators do not repeat when their cache is refreshed", "[random]") {
0246   const Generator gen{RandomSvc::instance().createEngine(14), 4};
0247   std::vector<double> values;
0248   for (int i = 0; i < 8; ++i) {
0249     values.push_back(gen.uniform_double());
0250   }
0251   CHECK(std::vector<double>(values.begin(), values.begin() + 4) !=
0252         std::vector<double>(values.begin() + 4, values.end()));
0253 }
0254 
0255 // The RandomSvc is a process-wide singleton, so its lifecycle is tested in a single test case
0256 TEST_CASE("RandomSvc provides generators", "[random]") {
0257   capture();
0258   auto& svc = RandomSvc::instance();
0259   CHECK(svc.name() == "RandomSvc");
0260 
0261   // the default engine works before initialization
0262   const double before = svc.generator().uniform_double();
0263   CHECK(before >= 0.);
0264   CHECK(before < 1.);
0265 
0266   // initialization without seed keeps the default engine
0267   svc.init();
0268   CHECK(messages().empty());
0269 
0270   // an external engine replaces the default engine
0271   svc.init(constantEngine(0));
0272   CHECK(svc.generator().uniform_double() == 0.);
0273   CHECK(messages() ==
0274         std::vector<Message>{{LogLevel::kInfo, "Loading external generator function."}});
0275 
0276   // generators request numbers from the engine in batches of the cache size
0277   const CountingEngine counting;
0278   svc.setProperty("cacheSize", std::size_t{4});
0279   svc.init(counting);
0280   const auto gen = svc.generator();
0281   for (int i = 0; i < 5; ++i) {
0282     gen.uniform_int<uint64_t>(0, std::numeric_limits<uint64_t>::max());
0283   }
0284   CHECK(*counting.calls == std::vector<std::size_t>{4, 4});
0285 
0286   // a custom seed is used for the internal engine, but ignored for an external engine
0287   messages().clear();
0288   svc.setProperty("seed", std::size_t{42});
0289   svc.init();
0290   const double seeded = svc.generator().uniform_double();
0291   CHECK(seeded >= 0.);
0292   CHECK(seeded < 1.);
0293   svc.init(constantEngine(0));
0294   CHECK(svc.generator().uniform_double() == 0.);
0295   CHECK(messages() ==
0296         std::vector<Message>{
0297             {LogLevel::kInfo, "Custom random seed requested: 42"},
0298             {LogLevel::kInfo, "Loading external generator function."},
0299             {LogLevel::kWarning,
0300              "Custom random seed request ignored when using external generator function"}});
0301 }