From d2777471cc68bff382eac41ddd88f59d5e5a0b40 Mon Sep 17 00:00:00 2001 From: Alan Coleman <alanc@udel.edu> Date: Fri, 8 Mar 2024 12:57:51 +0100 Subject: [PATCH] 2nd draft of first interaction reader --- applications/c8_air_shower.cpp | 6 +- .../modules/writers/InteractionWriter.inl | 71 +++++++++----- corsika/modules/writers/EnergyLossWriter.hpp | 2 +- corsika/modules/writers/InteractionWriter.hpp | 9 +- python/corsika/io/outputs/__init__.py | 1 + python/corsika/io/outputs/interaction.py | 90 ++++++++++++++++++ python/examples/first_interactions.py | 93 +++++++++++++++++++ tests/output/testInteractionWriter.cpp | 13 ++- 8 files changed, 256 insertions(+), 29 deletions(-) create mode 100644 python/corsika/io/outputs/interaction.py create mode 100644 python/examples/first_interactions.py diff --git a/applications/c8_air_shower.cpp b/applications/c8_air_shower.cpp index 7abd71bbb..ff61c9a9d 100644 --- a/applications/c8_air_shower.cpp +++ b/applications/c8_air_shower.cpp @@ -27,6 +27,7 @@ #include <corsika/framework/utility/SaveBoostHistogram.hpp> #include <corsika/modules/writers/EnergyLossWriter.hpp> +#include <corsika/modules/writers/InteractionWriter.hpp> #include <corsika/modules/writers/LongitudinalWriter.hpp> #include <corsika/modules/writers/PrimaryWriter.hpp> #include <corsika/modules/writers/SubWriter.hpp> @@ -571,10 +572,13 @@ int main(int argc, char** argv) { // register ZHS with the output manager output.add("ZHS", zhs); + InteractionWriter<setup::Tracking, ParticleWriterParquet> inter_writer(showerAxis, observationLevel); + output.add("interactions", inter_writer); + // assemble the final process sequence with radio auto sequence = make_sequence(stackInspect, neutrinoPrimaryPythia, hadronSequence, decayPythia, emCascade, emContinuous, coreas, zhs, - longprof, observationLevel, thinning, cut); + longprof, observationLevel, inter_writer, thinning, cut); /* === END: SETUP PROCESS LIST === */ diff --git a/corsika/detail/modules/writers/InteractionWriter.inl b/corsika/detail/modules/writers/InteractionWriter.inl index 61784ae1c..596844035 100644 --- a/corsika/detail/modules/writers/InteractionWriter.inl +++ b/corsika/detail/modules/writers/InteractionWriter.inl @@ -12,15 +12,18 @@ namespace corsika { - inline InteractionWriter::InteractionWriter(ShowerAxis const& axis) - : showerAxis_(axis) + template <typename TTracking, typename TOutput> + inline InteractionWriter<TTracking, TOutput>::InteractionWriter(ShowerAxis const& axis, ObservationPlane<TTracking, TOutput> const& obsPlane) + : obsPlane_(obsPlane) + , showerAxis_(axis) , interactionCounter_(0) , showerId_(0) , nSecondaries_(0) {} + template <typename TTracking, typename TOutput> template <typename TStackView> - inline void InteractionWriter::doSecondaries(TStackView& vS) { + inline void InteractionWriter<TTracking, TOutput>::doSecondaries(TStackView& vS) { if (interactionCounter_++) { return; } @@ -30,12 +33,17 @@ namespace corsika { CORSIKA_LOG_INFO("First interaction at dX {}", dX); CORSIKA_LOG_INFO("Primary: {}, E {}", primary.getPID(), primary.getKineticEnergy()); + Vector const dr_prim = primary.getPosition() - obsPlane_.getPlane().getCenter(); + auto const p_prim = primary.getMomentum(); + *(output_.getWriter()) << showerId_ << true - << static_cast<int>(primary.getPID()) - << static_cast<float>(primary.getKineticEnergy() / 1_GeV) - << static_cast<float>(1.0) - << static_cast<float>(0.0) - << static_cast<float>(0.0) + << static_cast<int>(get_PDG(primary.getPID())) + << static_cast<float>(dr_prim.dot(obsPlane_.getXAxis()) / 1_m) + << static_cast<float>(dr_prim.dot(obsPlane_.getYAxis()) / 1_m) + << static_cast<float>(dr_prim.dot(obsPlane_.getPlane().getNormal()) / 1_m) + << static_cast<float>(p_prim.dot(obsPlane_.getXAxis()) / 1_GeV) + << static_cast<float>(p_prim.dot(obsPlane_.getYAxis()) / 1_GeV) + << static_cast<float>(p_prim.dot(obsPlane_.getPlane().getNormal()) / 1_GeV) << static_cast<double>(primary.getTime() / 1_ns) << static_cast<float>(dX / (1_g / 1_cm / 1_cm)) << parquet::EndRow; @@ -44,12 +52,17 @@ namespace corsika { auto particle = vS.begin(); while (particle != vS.end()) { + Vector const dr_2nd = particle.getPosition() - obsPlane_.getPlane().getCenter(); + auto const p_2nd = particle.getMomentum(); + *(output_.getWriter()) << showerId_ << false - << static_cast<int>(particle.getPID()) - << static_cast<float>(particle.getKineticEnergy() / 1_GeV) - << static_cast<float>(1.0) - << static_cast<float>(0.0) - << static_cast<float>(0.0) + << static_cast<int>(get_PDG(particle.getPID())) + << static_cast<float>(dr_2nd.dot(obsPlane_.getXAxis()) / 1_m) + << static_cast<float>(dr_2nd.dot(obsPlane_.getYAxis()) / 1_m) + << static_cast<float>(dr_2nd.dot(obsPlane_.getPlane().getNormal()) / 1_m) + << static_cast<float>(p_2nd.dot(obsPlane_.getXAxis()) / 1_GeV) + << static_cast<float>(p_2nd.dot(obsPlane_.getYAxis()) / 1_GeV) + << static_cast<float>(p_2nd.dot(obsPlane_.getPlane().getNormal()) / 1_GeV) << static_cast<double>(particle.getTime() / 1_ns) << static_cast<float>(dX / (1_g / 1_cm / 1_cm)) << parquet::EndRow; @@ -60,20 +73,25 @@ namespace corsika { } } - inline void InteractionWriter::startOfLibrary(boost::filesystem::path const& directory) { + template <typename TTracking, typename TOutput> + inline void InteractionWriter<TTracking, TOutput>::startOfLibrary(boost::filesystem::path const& directory) { output_.initStreamer((directory / ("interactions.parquet")).string()); output_.addField("primary", parquet::Repetition::REQUIRED, parquet::Type::BOOLEAN, parquet::ConvertedType::NONE); output_.addField("pdg", parquet::Repetition::REQUIRED, parquet::Type::INT32, parquet::ConvertedType::INT_32); - output_.addField("kinetic_energy", parquet::Repetition::REQUIRED, - parquet::Type::FLOAT, parquet::ConvertedType::NONE); - output_.addField("nx", parquet::Repetition::REQUIRED, parquet::Type::FLOAT, + output_.addField("x", parquet::Repetition::REQUIRED, parquet::Type::FLOAT, + parquet::ConvertedType::NONE); + output_.addField("y", parquet::Repetition::REQUIRED, parquet::Type::FLOAT, + parquet::ConvertedType::NONE); + output_.addField("z", parquet::Repetition::REQUIRED, parquet::Type::FLOAT, + parquet::ConvertedType::NONE); + output_.addField("px", parquet::Repetition::REQUIRED, parquet::Type::FLOAT, parquet::ConvertedType::NONE); - output_.addField("ny", parquet::Repetition::REQUIRED, parquet::Type::FLOAT, + output_.addField("py", parquet::Repetition::REQUIRED, parquet::Type::FLOAT, parquet::ConvertedType::NONE); - output_.addField("nz", parquet::Repetition::REQUIRED, parquet::Type::FLOAT, + output_.addField("pz", parquet::Repetition::REQUIRED, parquet::Type::FLOAT, parquet::ConvertedType::NONE); output_.addField("time", parquet::Repetition::REQUIRED, parquet::Type::DOUBLE, parquet::ConvertedType::NONE); @@ -88,21 +106,25 @@ namespace corsika { summary_ = YAML::Node(); } - inline void InteractionWriter::startOfShower(unsigned int const showerId) { + template <typename TTracking, typename TOutput> + inline void InteractionWriter<TTracking, TOutput>::startOfShower(unsigned int const showerId) { showerId_ = showerId; interactionCounter_ = 0; nSecondaries_ = 0; } - inline void InteractionWriter::endOfShower(unsigned int const) { + template <typename TTracking, typename TOutput> + inline void InteractionWriter<TTracking, TOutput>::endOfShower(unsigned int const) { summary_["shower_" + std::to_string(showerId_)]["n_secondaries"] = nSecondaries_; } - inline void InteractionWriter::endOfLibrary() { + template <typename TTracking, typename TOutput> + inline void InteractionWriter<TTracking, TOutput>::endOfLibrary() { output_.closeStreamer(); } - inline YAML::Node InteractionWriter::getConfig() const { + template <typename TTracking, typename TOutput> + inline YAML::Node InteractionWriter<TTracking, TOutput>::getConfig() const { YAML::Node node; node["type"] = "Interactions"; node["units"]["energy"] = "GeV"; @@ -112,7 +134,8 @@ namespace corsika { return node; } - inline YAML::Node InteractionWriter::getSummary() const { + template <typename TTracking, typename TOutput> + inline YAML::Node InteractionWriter<TTracking, TOutput>::getSummary() const { return summary_; } diff --git a/corsika/modules/writers/EnergyLossWriter.hpp b/corsika/modules/writers/EnergyLossWriter.hpp index 6f42de944..d0a201062 100644 --- a/corsika/modules/writers/EnergyLossWriter.hpp +++ b/corsika/modules/writers/EnergyLossWriter.hpp @@ -161,7 +161,7 @@ namespace corsika { /** * Return a summary. */ - YAML::Node getSummary() const; + YAML::Node getSummary() const override; /** * Return the configuration of this output. diff --git a/corsika/modules/writers/InteractionWriter.hpp b/corsika/modules/writers/InteractionWriter.hpp index 6ba3fee30..6c1509da2 100644 --- a/corsika/modules/writers/InteractionWriter.hpp +++ b/corsika/modules/writers/InteractionWriter.hpp @@ -12,19 +12,22 @@ #include <corsika/media/ShowerAxis.hpp> +#include <corsika/modules/ObservationPlane.hpp> + #include <corsika/output/BaseOutput.hpp> #include <corsika/output/ParquetStreamer.hpp> namespace corsika { - class InteractionWriter : public SecondariesProcess<InteractionWriter>, BaseOutput{ + template <typename TTracking, typename TOutput> + class InteractionWriter : public SecondariesProcess<InteractionWriter<TTracking, TOutput>>, public BaseOutput{ public: /** * * @param axis - axis used for calculating grammage */ - InteractionWriter(ShowerAxis const& axis); + InteractionWriter(ShowerAxis const& axis, ObservationPlane<TTracking, TOutput> const& obsPlane); /** * @@ -46,7 +49,9 @@ namespace corsika { auto getInteractionCounter() { return interactionCounter_; } private: + ObservationPlane<TTracking, TOutput> const obsPlane_; ShowerAxis const& showerAxis_; + unsigned int interactionCounter_; unsigned int showerId_; unsigned int nSecondaries_; diff --git a/python/corsika/io/outputs/__init__.py b/python/corsika/io/outputs/__init__.py index 54731dd42..7b35c005b 100644 --- a/python/corsika/io/outputs/__init__.py +++ b/python/corsika/io/outputs/__init__.py @@ -16,6 +16,7 @@ from .particle_cut import ParticleCut from .primary import Particle, PrimaryParticle from .radio_process import RadioProcess from .track_writer import TrackWriter +from .interaction import Interactions __all__ = [ "Output", diff --git a/python/corsika/io/outputs/interaction.py b/python/corsika/io/outputs/interaction.py new file mode 100644 index 000000000..aabe38907 --- /dev/null +++ b/python/corsika/io/outputs/interaction.py @@ -0,0 +1,90 @@ +""" + Read data written by EnergyLoss. + + (c) Copyright 2024 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. +""" + +import logging +import os.path as op +from typing import Any + +import pyarrow.parquet as pq + +from ..converters import arrow_to_numpy +from .output import Output + +class Interactions(Output): + """ + Reads the interactions and secondaries + """ + + def __init__(self, path: str): + """ + Load the particle data into a parquet table. + + Parameters + ---------- + path: str + The path to the directory containing this output. + """ + super().__init__(path) + + # try and load our data + try: + self.__data = pq.read_table(op.join(path, "interactions.parquet")) + except Exception as e: + logging.getLogger("corsika").warn( + f"An error occured loading an Interaction: {e}" + ) + + def is_good(self) -> bool: + """ + Returns true if this output has been read successfully + and has the correct files/state/etc. + + Returns + ------- + bool: + True if this is a good output. + """ + return self.__data is not None + + def astype(self, dtype: str = "pandas", **kwargs: Any) -> Any: + """ + Load the particle data of the particle interactions. + + All additional keyword arguments are passed to `parquet.read_table` + + Parameters + ---------- + dtype: str + The data format to return the data in (i.e. numpy, pandas, etc.) + + Returns + ------- + Any: + The return type of this method is determined by `dtype`. + """ + if dtype == "arrow": + return self.__data + elif dtype == "pandas": + return self.__data.to_pandas() + elif dtype == "numpy": + return arrow_to_numpy.convert_to_numpy(self.__data) + else: + raise ValueError( + ( + f"Unknown format '{dtype}' for Interaction. " + "We currently only support ['arrow', 'pandas', 'numpy']." + ) + ) + + def __repr__(self) -> str: + """ + Return a string representation of this class. + """ + return f"Interaction('{self.config['name']}')" diff --git a/python/examples/first_interactions.py b/python/examples/first_interactions.py new file mode 100644 index 000000000..ef2635638 --- /dev/null +++ b/python/examples/first_interactions.py @@ -0,0 +1,93 @@ +#!/usr/bin/python3 + +import argparse +import os + +import matplotlib.pyplot as plt +import numpy as np +import particle + +import corsika + +here = os.path.abspath(os.path.dirname(__file__)) + +parser = argparse.ArgumentParser() +parser.add_argument( + "--input-dir", required=True, help="output directory of the CORSIKA 8 simulation" +) +parser.add_argument( + "--output-dir", + default=os.path.join(here, "example_plots"), + help="output directory for plots", +) +args = parser.parse_args() + +if not os.path.isdir(args.output_dir): + print("Making directory", args.output_dir) + os.makedirs(args.output_dir) + +# Load the shower simulation output +lib = corsika.Library(args.input_dir) + +# Load the primary particle information from the shower +interactions = lib.get("interactions").data +interaction_config = lib.get("interactions").config + +shower_ids = np.unique(interactions["shower"]) + +ncols = 1 +nrows = len(shower_ids) +fig, ax = plt.subplots( + ncols=ncols, + nrows=nrows, + figsize=(ncols * 8, nrows * 3), + gridspec_kw={"wspace": 0.2, "hspace": 0.3}, +) +ax = np.atleast_1d(ax).flatten() + +for sh_id in shower_ids: + data = interactions[interactions["shower"] == sh_id] + + mother = data[data["primary"]] + norm = np.sqrt(mother["px"]**2 + mother["py"]**2 + mother["pz"]**2) + angle = np.arctan2(mother["py"].iloc[0], mother["px"].iloc[0]) + pz = mother["pz"].iloc[0] + px = np.cos(-angle) * mother["px"].iloc[0] + np.sin(-angle) * mother["py"].iloc[0] + + pdg = int(mother["pdg"].iloc[0]) + name = f"${particle.Particle.from_pdgid(pdg).latex_name}$" + ax[sh_id].plot([-px, 0], [-pz, 0], label=name) + + + # Get daughters, filter out zero-momentum particles + daughters = data[data["primary"] == False] + p_tot = np.sqrt(daughters["px"]**2 + daughters["py"]**2 + daughters["pz"]**2) + daughters = daughters[p_tot > 0] + + print(daughters) + + for i in range(len(daughters)): + pz = daughters["pz"].iloc[i] + px = np.cos(-angle) * daughters["px"].iloc[i] + np.sin(-angle) * daughters["py"].iloc[i] + + pdg = int(daughters["pdg"].iloc[i]) + try: + name = f"${particle.Particle.from_pdgid(pdg).latex_name}$" + except Exception: + name = "Unknown" + print(pdg) + + ax[sh_id].plot([px, 0], [pz, 0], label=name) + + ax[sh_id].set_aspect("equal") + ax[sh_id].legend() + + ymin, ymax = ax[sh_id].get_ylim() + xmin, xmax = ax[sh_id].get_xlim() + maxVal = max(ymin, ymax, xmin, xmax) + ax[sh_id].set_ylim(-maxVal, maxVal) + ax[sh_id].set_xlim(-maxVal, maxVal) + +plot_path = os.path.join(args.output_dir, "interactions.png") +print("Saving", plot_path) +fig.savefig(plot_path, bbox_inches="tight") diff --git a/tests/output/testInteractionWriter.cpp b/tests/output/testInteractionWriter.cpp index 302c0ccd6..8f982a4ba 100644 --- a/tests/output/testInteractionWriter.cpp +++ b/tests/output/testInteractionWriter.cpp @@ -20,6 +20,8 @@ #include <corsika/media/Environment.hpp> #include <corsika/media/HomogeneousMedium.hpp> +#include <corsika/setup/SetupTrajectory.hpp> + #include <SetupTestStack.hpp> #include <SetupTestTrajectory.hpp> @@ -61,7 +63,16 @@ TEST_CASE("InteractionWriter", "process") { ShowerAxis axis(start, length, *env); - InteractionWriter writer(axis); + Plane const plane(Point(rootCS, {0_m, 0_m, 0_m}), + DirectionVector(rootCS, {0., 0., 1.})); + + ObservationPlane<setup::Tracking, ParticleWriterParquet> obsPlane{ + plane, DirectionVector(rootCS, {1., 0., 0.}), + true, // plane should "absorb" particles + false}; + + + InteractionWriter<setup::Tracking, ParticleWriterParquet> writer(axis, obsPlane); writer.startOfLibrary(outputDir); writer.startOfShower(0); -- GitLab