File indexing completed on 2026-10-29 08:25:01
0001
0002
0003
0004
0005
0006
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
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 }
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
0223
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 }
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
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 }
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 }