diff --git a/tests/modules/CMakeLists.txt b/tests/modules/CMakeLists.txt index 3bb6cf8db10c42d58b4862d01dde1e297b07ea2b..015093588f7ec42d0496ba5bb0d323badc425f4d 100644 --- a/tests/modules/CMakeLists.txt +++ b/tests/modules/CMakeLists.txt @@ -13,6 +13,7 @@ set (test_modules_sources testSibyll.cpp testEpos.cpp testRadio.cpp + testEMThinning.cpp testSophia.cpp ) diff --git a/tests/modules/testEMThinning.cpp b/tests/modules/testEMThinning.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9b23d0c129d94fcce3c63873b7af8254ffe6eae5 --- /dev/null +++ b/tests/modules/testEMThinning.cpp @@ -0,0 +1,223 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ + +#include <corsika/framework/core/PhysicalUnits.hpp> +#include <corsika/framework/geometry/Point.hpp> +#include <corsika/framework/geometry/Vector.hpp> +#include <corsika/framework/utility/CorsikaFenv.hpp> +#include <corsika/media/Environment.hpp> +#include <corsika/modules/thinning/EMThinning.hpp> + +#include <SetupStack.hpp> +#include <SetupTestTrajectory.hpp> +#include <SetupTestEnvironment.hpp> + +#include <catch2/catch.hpp> + +#include <boost/accumulators/accumulators.hpp> +#include <boost/accumulators/statistics/stats.hpp> +#include <boost/accumulators/statistics/mean.hpp> + +using namespace corsika; + +using DummyEnvironmentInterface = IMediumPropertyModel<IMagneticFieldModel<IMediumModel>>; +using DummyEnvironment = Environment<DummyEnvironmentInterface>; + +TEST_CASE("EMThinning", "process,secondary") { + RNGManager<>::getInstance().registerRandomStream("thinning"); + logging::set_level(logging::level::info); + + feenableexcept(FE_INVALID); + using EnvType = DummyEnvironment; + + EnvType env; + CoordinateSystemPtr const& rootCS = env.getCoordinateSystem(); + + // setup empty particle stack + test::Stack stack; + stack.clear(); + + SECTION("non-EM primary") { + auto prim = stack.addParticle(std::make_tuple(Code::Proton, 10_GeV, + DirectionVector(rootCS, {1, 0, 0}), + Point(rootCS, 0_m, 0_m, 0_m), 0_ns)); + test::StackView view{prim}; + auto projectile = view.getProjectile(); + + auto sec1 = projectile.addSecondary( + std::make_tuple(Code::PiPlus, 2_GeV, DirectionVector(rootCS, {1, 0, 0}))); + auto sec2 = projectile.addSecondary( + std::make_tuple(Code::KMinus, 8_GeV, DirectionVector(rootCS, {1, 0, 0}))); + + EMThinning thinning{100_GeV, 1e20}; + thinning.doSecondaries(view); + REQUIRE(view.getEntries() == 2); + REQUIRE(sec1.getWeight() == 1.); + REQUIRE(sec2.getWeight() == 1.); + } + + SECTION("non-EM interaction") { + auto prim = stack.addParticle(std::make_tuple(Code::Photon, 10_GeV, + DirectionVector(rootCS, {1, 0, 0}), + Point(rootCS, 0_m, 0_m, 0_m), 0_ns)); + test::StackView view{prim}; + auto projectile = view.getProjectile(); + + // mimic photohadronic interaction, more than 2 secondaries + auto sec1 = projectile.addSecondary( + std::make_tuple(Code::PiPlus, 2_GeV, DirectionVector(rootCS, {1, 0, 0}))); + auto sec2 = projectile.addSecondary( + std::make_tuple(Code::KMinus, 3_GeV, DirectionVector(rootCS, {1, 0, 0}))); + auto sec3 = projectile.addSecondary( + std::make_tuple(Code::Proton, 5_GeV, DirectionVector(rootCS, {1, 0, 0}))); + + EMThinning thinning{100_GeV, 1e20}; + thinning.doSecondaries(view); + REQUIRE(view.getEntries() == 3); + REQUIRE(sec1.getWeight() == 1.); + REQUIRE(sec2.getWeight() == 1.); + REQUIRE(sec3.getWeight() == 1.); + } + + SECTION("above threshold") { + auto prim = stack.addParticle(std::make_tuple(Code::Photon, 10_GeV, + DirectionVector(rootCS, {1, 0, 0}), + Point(rootCS, 0_m, 0_m, 0_m), 0_ns)); + + test::StackView view{prim}; + auto projectile = view.getProjectile(); + + auto sec1 = projectile.addSecondary( + std::make_tuple(Code::Electron, 2_GeV, DirectionVector(rootCS, {1, 0, 0}))); + auto sec2 = projectile.addSecondary( + std::make_tuple(Code::Positron, 8_GeV, DirectionVector(rootCS, {1, 0, 0}))); + + EMThinning thinning{1_MeV, 1e20}; + thinning.doSecondaries(view); + REQUIRE(view.getEntries() == 2); + REQUIRE(sec1.getWeight() == 1.); + REQUIRE(sec2.getWeight() == 1.); + } + + SECTION("below threshold") { + EMThinning thinning{100_GeV, 1e20}; + + boost::accumulators::accumulator_set< + double, boost::accumulators::stats<boost::accumulators::tag::mean>> + acc; + boost::accumulators::accumulator_set< + HEPEnergyType, boost::accumulators::stats<boost::accumulators::tag::mean>> + accE; + + // statistical test, 10000 iterations + for (int i = 0; i < 10'000; ++i) { + stack.clear(); + auto prim = stack.addParticle(std::make_tuple(Code::Photon, 10_GeV, + DirectionVector(rootCS, {1, 0, 0}), + Point(rootCS, 0_m, 0_m, 0_m), 0_ns)); + + test::StackView view{prim}; + auto projectile = view.getProjectile(); + + projectile.addSecondary( + std::make_tuple(Code::Electron, 2_GeV, DirectionVector(rootCS, {1, 0, 0}))); + projectile.addSecondary( + std::make_tuple(Code::Positron, 8_GeV, DirectionVector(rootCS, {1, 0, 0}))); + + thinning.doSecondaries(view); + REQUIRE(view.getEntries() == 1); + + double sumWeight{}; + HEPEnergyType sumE{}; + + for (auto& p : view) { + sumWeight += p.getWeight(); + sumE += p.getWeight() * p.getEnergy(); + } + + acc(sumWeight); + accE(sumE); + } + + REQUIRE(boost::accumulators::mean(acc) == Approx(2).epsilon(2e-2)); + REQUIRE(boost::accumulators::mean(accE) / 1_GeV == Approx(10).epsilon(2e-2)); + } + + SECTION("weight limitation") { + double constexpr maxWeight = 3; // min acceptance prob. >= 0.3333 + EMThinning thinning{100_GeV, maxWeight}; + + boost::accumulators::accumulator_set< + double, boost::accumulators::stats<boost::accumulators::tag::mean>> + acc; + boost::accumulators::accumulator_set< + HEPEnergyType, boost::accumulators::stats<boost::accumulators::tag::mean>> + accE; + + // statistical test, 10000 iterations + for (int i = 0; i < 10'000; ++i) { + stack.clear(); + auto prim = stack.addParticle(std::make_tuple(Code::Photon, 10_GeV, + DirectionVector(rootCS, {1, 0, 0}), + Point(rootCS, 0_m, 0_m, 0_m), 0_ns)); + + test::StackView view{prim}; + auto projectile = view.getProjectile(); + + projectile.addSecondary( + std::make_tuple(Code::Electron, 2_GeV, DirectionVector(rootCS, {1, 0, 0}))); + projectile.addSecondary( + std::make_tuple(Code::Positron, 8_GeV, DirectionVector(rootCS, {1, 0, 0}))); + + thinning.doSecondaries(view); + + double sumWeight{}; + HEPEnergyType sumE{}; + + for (auto& p : view) { + auto const w = p.getWeight(); + REQUIRE(w <= maxWeight); + sumWeight += w; + sumE += p.getEnergy() * w; + } + + acc(sumWeight); + accE(sumE); + } + + REQUIRE(boost::accumulators::mean(acc) == Approx(2).epsilon(2e-2)); + REQUIRE(boost::accumulators::mean(accE) / 1_GeV == Approx(10).epsilon(2e-2)); + } + + SECTION("max. weight reached") { + double constexpr maxWeight = 3; // min acceptance prob. >= 0.3333 + EMThinning thinning{100_GeV, maxWeight}; + + for (int i = 0; i < 10'000; ++i) { + stack.clear(); + auto prim = stack.addParticle(std::make_tuple(Code::Photon, 10_GeV, + DirectionVector(rootCS, {1, 0, 0}), + Point(rootCS, 0_m, 0_m, 0_m), 0_ns)); + prim.setWeight(3.5); + + test::StackView view{prim}; + auto projectile = view.getProjectile(); + + auto sec1 = projectile.addSecondary( + std::make_tuple(Code::Electron, 2_GeV, DirectionVector(rootCS, {1, 0, 0}))); + auto sec2 = projectile.addSecondary( + std::make_tuple(Code::Positron, 8_GeV, DirectionVector(rootCS, {1, 0, 0}))); + + // cannot perform thinning anymore, always keep all secondaries, weight unchanged + thinning.doSecondaries(view); + REQUIRE(view.getEntries() == 2); + REQUIRE(sec1.getWeight() == 3.5); + REQUIRE(sec2.getWeight() == 3.5); + } + } +}