Back to home page

EIC code displayed by LXR

 
 

    


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

0001 // SPDX-License-Identifier: LGPL-3.0-or-later
0002 // Copyright (C) 2026 Wouter Deconinck
0003 //
0004 // Tests for Input<T...>, Output<T...> and the Algorithm base class, with required (T),
0005 // optional (std::optional<T>), vector (std::vector<T>) and vector of optional
0006 // (std::vector<std::optional<T>>) arguments.
0007 //
0008 #include <algorithms/algorithm.h>
0009 
0010 #include <catch2/catch_test_macros.hpp>
0011 
0012 #include <edm4hep/MCParticleCollection.h>
0013 
0014 #include <algorithm>
0015 #include <array>
0016 #include <cstddef>
0017 #include <initializer_list>
0018 #include <memory>
0019 #include <optional>
0020 #include <string>
0021 #include <tuple>
0022 #include <type_traits>
0023 #include <vector>
0024 
0025 using namespace algorithms;
0026 
0027 namespace {
0028 
0029 using MixedInput  = Input<int, std::optional<double>, std::vector<std::string>>;
0030 using MixedOutput = Output<int, std::optional<double>, std::vector<std::string>>;
0031 
0032 // Copies each input argument to the output argument of the same type
0033 class MixedAlgorithm : public Algorithm<MixedInput, MixedOutput> {
0034 public:
0035   static constexpr double kMissing = -1.;
0036 
0037   MixedAlgorithm()
0038       : Algorithm{"MixedAlgorithm",
0039                   {"inputInt", "inputDouble", "inputStrings"},
0040                   {"outputInt", "outputDouble", "outputStrings"},
0041                   "Copies each input argument to the matching output argument"} {}
0042 
0043   void init() final { m_initialized = true; }
0044 
0045   void process(const Input& input, const Output& output) const final {
0046     const auto& [int_in, double_in, strings_in]    = input;
0047     const auto& [int_out, double_out, strings_out] = output;
0048 
0049     *int_out = *int_in;
0050     if (double_out != nullptr) {
0051       *double_out = (double_in != nullptr) ? *double_in : kMissing;
0052     }
0053     for (std::size_t i = 0; i < std::min(strings_in.size(), strings_out.size()); ++i) {
0054       *strings_out[i] = *strings_in[i];
0055     }
0056   }
0057 
0058   bool initialized() const { return m_initialized; }
0059 
0060 private:
0061   bool m_initialized = false;
0062 };
0063 
0064 } // namespace
0065 
0066 TEST_CASE("Input and Output combine all argument types", "[algorithm]") {
0067   STATIC_REQUIRE(MixedInput::kSize == 3);
0068   STATIC_REQUIRE(MixedOutput::kSize == 3);
0069 
0070   STATIC_REQUIRE(std::is_same_v<MixedInput::data_type,
0071                                 std::tuple<int, std::optional<double>, std::vector<std::string>>>);
0072   STATIC_REQUIRE(std::is_same_v<MixedOutput::data_type,
0073                                 std::tuple<int, std::optional<double>, std::vector<std::string>>>);
0074 
0075   STATIC_REQUIRE(
0076       std::is_same_v<MixedInput::value_type,
0077                      std::tuple<gsl::not_null<const int*>, const double*,
0078                                 const std::vector<gsl::not_null<const std::string*>>>>);
0079   STATIC_REQUIRE(std::is_same_v<MixedOutput::value_type,
0080                                 std::tuple<gsl::not_null<int*>, double*,
0081                                            const std::vector<gsl::not_null<std::string*>>>>);
0082   STATIC_REQUIRE(std::is_base_of_v<MixedInput::value_type, MixedInput>);
0083   STATIC_REQUIRE(std::is_base_of_v<MixedOutput::value_type, MixedOutput>);
0084 
0085   STATIC_REQUIRE(std::is_same_v<MixedInput::key_type, std::array<const std::string, 3>>);
0086   STATIC_REQUIRE(std::is_same_v<MixedOutput::key_type, std::array<const std::string, 3>>);
0087 }
0088 
0089 TEST_CASE("Input and Output types are recognized", "[algorithm]") {
0090   STATIC_REQUIRE(is_input_v<MixedInput>);
0091   STATIC_REQUIRE_FALSE(is_output_v<MixedInput>);
0092   STATIC_REQUIRE(is_output_v<MixedOutput>);
0093   STATIC_REQUIRE_FALSE(is_input_v<MixedOutput>);
0094   STATIC_REQUIRE_FALSE(is_input_v<MixedInput::value_type>);
0095   STATIC_REQUIRE_FALSE(is_output_v<MixedOutput::value_type>);
0096   STATIC_REQUIRE_FALSE(is_input_v<int>);
0097   STATIC_REQUIRE_FALSE(is_output_v<int>);
0098 }
0099 
0100 TEST_CASE("Algorithm exposes its argument types", "[algorithm]") {
0101   STATIC_REQUIRE(std::is_same_v<MixedAlgorithm::input_type, MixedInput>);
0102   STATIC_REQUIRE(std::is_same_v<MixedAlgorithm::output_type, MixedOutput>);
0103   STATIC_REQUIRE(std::is_same_v<MixedAlgorithm::algorithm_type,
0104                                 Algorithm<MixedInput, MixedOutput>>);
0105   STATIC_REQUIRE(std::is_same_v<MixedAlgorithm::Input, MixedInput::value_type>);
0106   STATIC_REQUIRE(std::is_same_v<MixedAlgorithm::Output, MixedOutput::value_type>);
0107   STATIC_REQUIRE(std::is_same_v<MixedAlgorithm::InputNames, MixedInput::key_type>);
0108   STATIC_REQUIRE(std::is_same_v<MixedAlgorithm::OutputNames, MixedOutput::key_type>);
0109   STATIC_REQUIRE(std::is_base_of_v<AlgorithmBase, MixedAlgorithm>);
0110   STATIC_REQUIRE(std::has_virtual_destructor_v<MixedAlgorithm::algorithm_type>);
0111 }
0112 
0113 TEST_CASE("Algorithm keeps its name, description and argument names", "[algorithm]") {
0114   const MixedAlgorithm algo;
0115 
0116   CHECK(algo.name() == "MixedAlgorithm");
0117   CHECK(algo.description() == "Copies each input argument to the matching output argument");
0118   CHECK(algo.inputNames() == MixedAlgorithm::InputNames{"inputInt", "inputDouble", "inputStrings"});
0119   CHECK(algo.outputNames() ==
0120         MixedAlgorithm::OutputNames{"outputInt", "outputDouble", "outputStrings"});
0121 }
0122 
0123 TEST_CASE("Algorithm is configurable and has a log level", "[algorithm]") {
0124   class ConfigurableAlgorithm : public Algorithm<Input<int>, Output<int>> {
0125   public:
0126     ConfigurableAlgorithm() : Algorithm{"ConfigurableAlgorithm", {"in"}, {"out"}, ""} {}
0127     void process(const Input& input, const Output& output) const final {
0128       *std::get<0>(output) = *std::get<0>(input) * m_factor;
0129     }
0130 
0131   private:
0132     Property<int> m_factor{this, "factor", 2, "Multiplication factor"};
0133   };
0134 
0135   ConfigurableAlgorithm algo;
0136   algo.level(LogLevel::kDebug);
0137   CHECK(algo.level() == LogLevel::kDebug);
0138 
0139   const int in = 21;
0140   int out      = 0;
0141   algo.process({&in}, {&out});
0142   CHECK(out == 42);
0143 
0144   algo.setProperty("factor", 3);
0145   algo.process({&in}, {&out});
0146   CHECK(out == 63);
0147 }
0148 
0149 TEST_CASE("Algorithm base class has default init and process", "[algorithm]") {
0150   using Base = Algorithm<Input<int>, Output<int>>;
0151   auto algo  = std::make_unique<Base>("Base", Base::InputNames{"in"}, Base::OutputNames{"out"},
0152                                      "Base algorithm without overrides");
0153 
0154   const int in = 1;
0155   int out      = 0;
0156   algo->init();
0157   algo->process({&in}, {&out});
0158   CHECK(out == 0);
0159 }
0160 
0161 TEST_CASE("Algorithm init can be overridden", "[algorithm]") {
0162   MixedAlgorithm algo;
0163   CHECK_FALSE(algo.initialized());
0164   algo.init();
0165   CHECK(algo.initialized());
0166 }
0167 
0168 TEST_CASE("Algorithm processes all argument types", "[algorithm]") {
0169   const MixedAlgorithm algo;
0170 
0171   const int int_in       = 42;
0172   const double double_in = 3.5;
0173   const std::string a{"a"};
0174   const std::string b{"b"};
0175 
0176   int int_out       = 0;
0177   double double_out = 0.;
0178   std::string x;
0179   std::string y;
0180 
0181   SECTION("all arguments present") {
0182     algo.process({&int_in, &double_in, {&a, &b}}, {&int_out, &double_out, {&x, &y}});
0183     CHECK(int_out == 42);
0184     CHECK(double_out == 3.5);
0185     CHECK(x == "a");
0186     CHECK(y == "b");
0187   }
0188 
0189   SECTION("optional input absent") {
0190     algo.process({&int_in, nullptr, {&a, &b}}, {&int_out, &double_out, {&x, &y}});
0191     CHECK(int_out == 42);
0192     CHECK(double_out == MixedAlgorithm::kMissing);
0193     CHECK(x == "a");
0194     CHECK(y == "b");
0195   }
0196 
0197   SECTION("optional output absent") {
0198     algo.process({&int_in, &double_in, {&a, &b}}, {&int_out, nullptr, {&x, &y}});
0199     CHECK(int_out == 42);
0200     CHECK(double_out == 0.);
0201     CHECK(x == "a");
0202     CHECK(y == "b");
0203   }
0204 
0205   SECTION("vector arguments empty") {
0206     algo.process({&int_in, &double_in, {}}, {&int_out, &double_out, {}});
0207     CHECK(int_out == 42);
0208     CHECK(double_out == 3.5);
0209     CHECK(x.empty());
0210     CHECK(y.empty());
0211   }
0212 
0213   SECTION("vector arguments with a single element") {
0214     algo.process({&int_in, &double_in, {&b}}, {&int_out, &double_out, {&x}});
0215     CHECK(x == "b");
0216     CHECK(y.empty());
0217   }
0218 }
0219 
0220 namespace {
0221 
0222 // Sums the present entries of a vector of optional inputs, and writes the sum to each
0223 // present entry of a vector of optional outputs
0224 class SumOptionals : public Algorithm<Input<std::optional<int>, std::vector<std::optional<double>>>,
0225                                       Output<std::vector<std::optional<double>>>> {
0226 public:
0227   SumOptionals()
0228       : Algorithm{"SumOptionals",
0229                   {"maybeInt", "maybeDoubles"},
0230                   {"maybeSums"},
0231                   "Sums all present inputs into all present outputs"} {}
0232 
0233   void process(const Input& input, const Output& output) const final {
0234     const auto& [maybe_int, maybe_doubles] = input;
0235     const auto& [maybe_sums]               = output;
0236 
0237     double sum = (maybe_int != nullptr) ? *maybe_int : 0;
0238     for (const auto* value : maybe_doubles) {
0239       if (value != nullptr) {
0240         sum += *value;
0241       }
0242     }
0243     for (auto* out : maybe_sums) {
0244       if (out != nullptr) {
0245         *out = sum;
0246       }
0247     }
0248   }
0249 };
0250 
0251 } // namespace
0252 
0253 TEST_CASE("Input and Output support vectors of optional arguments", "[algorithm]") {
0254   using In  = SumOptionals::input_type;
0255   using Out = SumOptionals::output_type;
0256   STATIC_REQUIRE(
0257       std::is_same_v<In::value_type, std::tuple<const int*, const std::vector<const double*>>>);
0258   STATIC_REQUIRE(std::is_same_v<Out::value_type, std::tuple<const std::vector<double*>>>);
0259   STATIC_REQUIRE(
0260       std::is_same_v<In::data_type,
0261                      std::tuple<std::optional<int>, std::vector<std::optional<double>>>>);
0262 }
0263 
0264 TEST_CASE("Algorithm processes vectors of optional arguments", "[algorithm]") {
0265   const SumOptionals algo;
0266   CHECK(algo.inputNames() == SumOptionals::InputNames{"maybeInt", "maybeDoubles"});
0267   CHECK(algo.outputNames() == SumOptionals::OutputNames{"maybeSums"});
0268 
0269   const int i    = 1;
0270   const double a = 2.;
0271   const double b = 4.;
0272   double x       = 0.;
0273   double y       = 0.;
0274 
0275   SECTION("all entries present") {
0276     algo.process({&i, {&a, &b}}, {{&x, &y}});
0277     CHECK(x == 7.);
0278     CHECK(y == 7.);
0279   }
0280 
0281   SECTION("some entries absent") {
0282     algo.process({nullptr, {nullptr, &b, nullptr}}, {{nullptr, &y}});
0283     CHECK(x == 0.);
0284     CHECK(y == 4.);
0285   }
0286 
0287   SECTION("all entries absent") {
0288     algo.process({nullptr, {nullptr, nullptr}}, {{nullptr, nullptr}});
0289     CHECK(x == 0.);
0290     CHECK(y == 0.);
0291   }
0292 
0293   SECTION("no entries") {
0294     algo.process({&i, {}}, {{}});
0295     CHECK(x == 0.);
0296     CHECK(y == 0.);
0297   }
0298 }
0299 
0300 namespace {
0301 
0302 using Particles = edm4hep::MCParticleCollection;
0303 
0304 // Merges particles from a required, an optional, a vector, and a vector of optional collections
0305 class MergeParticles
0306     : public Algorithm<Input<Particles, std::optional<Particles>, std::vector<Particles>,
0307                              std::vector<std::optional<Particles>>>,
0308                        Output<Particles>> {
0309 public:
0310   MergeParticles()
0311       : Algorithm{"MergeParticles",
0312                   {"requiredParticles", "optionalParticles", "otherParticles",
0313                    "optionalOtherParticles"},
0314                   {"mergedParticles"},
0315                   "Merges particles from all input collections"} {}
0316 
0317   void process(const Input& input, const Output& output) const final {
0318     const auto& [required, optional, others, optional_others] = input;
0319     const auto& [merged]                                      = output;
0320 
0321     copy(*required, *merged);
0322     if (optional != nullptr) {
0323       copy(*optional, *merged);
0324     }
0325     for (const auto& other : others) {
0326       copy(*other, *merged);
0327     }
0328     for (const auto* other : optional_others) {
0329       if (other != nullptr) {
0330         copy(*other, *merged);
0331       }
0332     }
0333   }
0334 
0335 private:
0336   static void copy(const Particles& from, Particles& to) {
0337     for (const auto& particle : from) {
0338       to.create().setPDG(particle.getPDG());
0339     }
0340   }
0341 };
0342 
0343 Particles makeParticles(std::initializer_list<int> pdgs) {
0344   Particles particles;
0345   for (int pdg : pdgs) {
0346     particles.create().setPDG(pdg);
0347   }
0348   return particles;
0349 }
0350 
0351 std::vector<int> pdgs(const Particles& particles) {
0352   std::vector<int> ret;
0353   for (const auto& particle : particles) {
0354     ret.push_back(particle.getPDG());
0355   }
0356   return ret;
0357 }
0358 
0359 } // namespace
0360 
0361 TEST_CASE("Algorithm processes all argument types of EDM collections", "[algorithm][edm]") {
0362   const MergeParticles algo;
0363 
0364   const auto required = makeParticles({11});
0365   const auto optional = makeParticles({22, 22});
0366   const auto other1   = makeParticles({2212});
0367   const auto other2   = makeParticles({2112, -211});
0368   const auto other3   = makeParticles({13});
0369 
0370   Particles merged;
0371 
0372   SECTION("all arguments present") {
0373     algo.process({&required, &optional, {&other1, &other2}, {&other3}}, {&merged});
0374     CHECK(pdgs(merged) == std::vector<int>{11, 22, 22, 2212, 2112, -211, 13});
0375   }
0376 
0377   SECTION("optional input absent") {
0378     algo.process({&required, nullptr, {&other1, &other2}, {}}, {&merged});
0379     CHECK(pdgs(merged) == std::vector<int>{11, 2212, 2112, -211});
0380   }
0381 
0382   SECTION("vector input empty") {
0383     algo.process({&required, &optional, {}, {}}, {&merged});
0384     CHECK(pdgs(merged) == std::vector<int>{11, 22, 22});
0385   }
0386 
0387   SECTION("vector of optional input with absent entries") {
0388     algo.process({&required, nullptr, {}, {nullptr, &other3, nullptr}}, {&merged});
0389     CHECK(pdgs(merged) == std::vector<int>{11, 13});
0390   }
0391 }