diff --git a/dvmp/analysis/mt.h b/dvmp/analysis/mt.h new file mode 100644 index 0000000000000000000000000000000000000000..198050c6ddc37e68518761b8ccc410e0d71ea123 --- /dev/null +++ b/dvmp/analysis/mt.h @@ -0,0 +1,11 @@ +#ifndef MT_H +#define MT_H + +// Defines the number of threads to run within the ROOT analysis scripts. +// TODO: make this a file configured by the CI scripts so we can specify +// the number of threads (and the number of processes) at a global +// level + +constexpr const int kNumThreads = 8; + +#endif diff --git a/dvmp/analysis/plot.h b/dvmp/analysis/plot.h new file mode 100644 index 0000000000000000000000000000000000000000..69ebba341af239541496a410c66f78eb33e8adac --- /dev/null +++ b/dvmp/analysis/plot.h @@ -0,0 +1,40 @@ +#ifndef PLOT_H +#define PLOT_H + +#include <TColor.h> +#include <fmt/core.h> +#include <vector> + +namespace plot { + +const int kMpBlue = TColor::GetColor(0x1f, 0x77, 0xb4); +const int kMpOrange = TColor::GetColor(0xff, 0x7f, 0x0e); +const int kMpGreen = TColor::GetColor(0x2c, 0xa0, 0x2c); +const int kMpRed = TColor::GetColor(0xd6, 0x27, 0x28); +const int kMpPurple = TColor::GetColor(0x94, 0x67, 0xbd); +const int kMpBrown = TColor::GetColor(0x8c, 0x56, 0x4b); +const int kMpPink = TColor::GetColor(0xe3, 0x77, 0xc2); +const int kMpGrey = TColor::GetColor(0x7f, 0x7f, 0x7f); +const int kMpMoss = TColor::GetColor(0xbc, 0xbd, 0x22); +const int kMpCyan = TColor::GetColor(0x17, 0xbe, 0xcf); + +const std::vector<int> kPalette = {kMpBlue, kMpOrange, kMpGreen, kMpRed, + kMpPurple, kMpBrown, kMpPink, kMpGrey, + kMpMoss, kMpCyan}; + +void draw_label(int ebeam, int pbeam, const std::string_view detector, + std::string_view vm, std::string_view what) { + auto t = new TPaveText(.15, 0.800, .7, .925, "NB NDC"); + t->SetFillColorAlpha(kWhite, 0.4); + t->SetTextFont(43); + t->SetTextSize(25); + t->AddText(fmt::format("#bf{{{} }}SIMULATION", detector).c_str()); + t->AddText(fmt::format("{} for {} DVMP.", what, vm).c_str()); + t->AddText(fmt::format("{} GeV on {} GeV", ebeam, pbeam, what).c_str()); + t->SetTextAlign(12); + t->Draw(); +} + +} // namespace plot + +#endif diff --git a/dvmp/analysis/util.h b/dvmp/analysis/util.h new file mode 100644 index 0000000000000000000000000000000000000000..874b70240c41323df0419a3b5276faae2e292054 --- /dev/null +++ b/dvmp/analysis/util.h @@ -0,0 +1,127 @@ +#ifndef UTIL_H +#define UTIL_H + +#include <algorithm> +#include <cmath> +#include <exception> +#include <fmt/core.h> +#include <limits> +#include <string> +#include <vector> + +#include <Math/Vector4D.h> + +#include "dd4pod/Geant4ParticleCollection.h" +#include "eicd/TrackParametersCollection.h" + +namespace util { + +// Exception definition for unknown particle errors +// FIXME: A utility exception base class should be included in the analysis +// utility library, so we can skip most of this boilerplate +class unknown_particle_error : public std::exception { +public: + unknown_particle_error(std::string_view particle) : m_particle{particle} {} + virtual const char* what() const throw() { + return fmt::format("Unknown particle type: {}", m_particle).c_str(); + } + virtual const char* type() const throw() { return "unknown_particle_error"; } + +private: + const std::string m_particle; +}; + +// Simple function to return the appropriate PDG mass for the particles +// we care about for this process. +// FIXME: consider something more robust (maybe based on hepPDT) to the +// analysis utility library +inline double get_pdg_mass(std::string_view part) { + if (part == "electron") { + return 0.0005109989461; + } else if (part == "muon") { + return .1056583745; + } else if (part == "jpsi") { + return 3.0969; + } else if (part == "upsilon") { + return 9.49630; + } else { + throw unknown_particle_error{part}; + } +} + +// Get a vector of 4-momenta from raw tracking info, using an externally +// provided particle mass assumption. +// FIXME: should be part of utility library +inline auto +momenta_from_tracking(const std::vector<eic::TrackParametersData>& tracks, + const double mass) { + std::vector<ROOT::Math::PxPyPzMVector> momenta{tracks.size()}; + // transform our raw tracker info into proper 4-momenta + std::transform(tracks.begin(), tracks.end(), momenta.begin(), + [mass](const auto& track) { + // make sure we don't divide by zero + if (fabs(track.qOverP) < 1e-9) { + return ROOT::Math::PtEtaPhiMVector{}; + } + const double pt = 1. / track.qOverP * sin(track.theta); + const double eta = -log(tan(track.theta / 2)); + const double phi = track.phi; + return ROOT::Math::PtEtaPhiMVector{pt, eta, phi, mass}; + }); + return momenta; +} + +// Get a vector of 4-momenta from the simulation data. +// FIXME: should be part of utility library +// TODO: Add PID selector (maybe using ranges?) +inline auto +momenta_from_simulation(const std::vector<dd4pod::Geant4ParticleData>& parts) { + std::vector<ROOT::Math::PxPyPzMVector> momenta{parts.size()}; + // transform our simulation particle data into 4-momenta + std::transform(parts.begin(), parts.end(), momenta.begin(), + [](const auto& part) { + return ROOT::Math::PxPyPzMVector{part.psx, part.psy, + part.psz, part.mass}; + }); + return momenta; +} + +// Find the decay pair candidates from a vector of particles (parts), +// with invariant mass closest to a desired value (pdg_mass) +// FIXME: not sure if this belongs here, or in the utility library. Probably the +// utility library +inline std::pair<ROOT::Math::PxPyPzMVector, ROOT::Math::PxPyPzMVector> +find_decay_pair(const std::vector<ROOT::Math::PxPyPzMVector>& parts, + const double pdg_mass) { + int first = -1; + int second = -1; + double best_mass = -1; + + // go through all particle combinatorics, calculate the invariant mass + // for each combination, and remember which combination is the closest + // to the desired pdg_mass + for (int i = 0; i < parts.size(); ++i) { + for (int j = i + 1; j < parts.size(); ++j) { + const double new_mass{(parts[i] + parts[j]).mass()}; + if (fabs(new_mass - pdg_mass) < fabs(best_mass - pdg_mass)) { + first = i; + second = j; + best_mass = new_mass; + } + } + } + if (first < 0) { + return {{}, {}}; + } + return {parts[first], parts[second]}; +} +// Calculate the invariant mass of a given pair of particles +inline double +get_im(const std::pair<ROOT::Math::PxPyPzMVector, ROOT::Math::PxPyPzMVector>& + particle_pair) { + return (particle_pair.first + particle_pair.second).mass(); +} + +} // namespace util + +#endif diff --git a/dvmp/analysis/vm_mass.cxx b/dvmp/analysis/vm_mass.cxx index 320ecd6cfea9a2c108de7af30de236d06289b81b..19c56db8957e780333f690e4033aee988a3a5a99 100644 --- a/dvmp/analysis/vm_mass.cxx +++ b/dvmp/analysis/vm_mass.cxx @@ -1,34 +1,108 @@ +#include "mt.h" +#include "plot.h" +#include "util.h" + #include <ROOT/RDataFrame.hxx> #include <cmath> +#include <fmt/color.h> +#include <fmt/core.h> #include <iostream> #include <string> #include <vector> -#include "dd4pod/Geant4ParticleCollection.h" -#include "eicd/TrackParametersCollection.h" - -// 4-momenta for all tracks, using the raw reconstruction results -auto momenta(const std::vector<eic::TrackParametersData>& in) { - std::vector<ROOT::Math::XYZMVector> results; - std::transform(in.begin(), in.end(), results.begin(), [](const auto& part) { - return { - part.p.px, part.p.py, part.p.pz, - .000511 // hard-assume these are electrons for now, will need PID down - // the road FIXME - }; - }); - return results; -} +// Run VM invariant-mass-based benchmarks on an input reconstruction file for +// a desired vector meson (e.g. jpsi) and a desired decay particle (e.g. muon) +// Output figures are written to our output prefix (which includes the output +// file prefix), and labeled with our detector name. +// TODO: I think it would be better to pass small json configuration file to +// the test, instead of this ever-expanding list of function arguments. +int vm_mass(std::string_view rec_file, std::string_view vm_name, + std::string_view decay_name, std::string_view detector, + std::string output_prefix) { + fmt::print(fmt::emphasis::bold | fg(fmt::color::forest_green), + "Running VM invariant mass analysis...\n"); + fmt::print(" - Vector meson: {}\n", vm_name); + fmt::print(" - Decay particle: {}\n", decay_name); + fmt::print(" - Detector package: {}\n", detector); + fmt::print(" - output prefix: {}\n", output_prefix); + + // Run this in multi-threaded mode if desired + ROOT::EnableImplicitMT(kNumThreads); -// masses for all combinatorics -auto calc_masses(const std::vector<ROOT::Math::XYZMVector>& p) { - std::vector<double> masses; - for (int i = 0; i < p.size(); ++i) { - for (int j = i + 1; i < p.size(); ++j) { - masses.push_back((p[i] + p[j]).mass()); - } + // The particles we are looking for. E.g. J/psi decaying into e+e- + const double vm_mass = util::get_pdg_mass(vm_name); + const double decay_mass = util::get_pdg_mass(decay_name); + + // Ensure our output prefix always ends on a dot, a slash or a dash + if (output_prefix.back() != '.' && output_prefix.back() != '/' && + output_prefix.back() != '-') { + output_prefix += "-"; } - return masses -} -auto jpsi_mass(const std::string + // Open our input file file as a dataframe + ROOT::RDataFrame d{"events", rec_file}; + + // utility lambda functions to bind the vector meson and decay particle types + auto momenta_from_tracking = + [decay_mass](const std::vector<eic::TrackParametersData>& tracks) { + return util::momenta_from_tracking(tracks, decay_mass); + }; + auto find_decay_pair = + [vm_mass](const std::vector<ROOT::Math::PxPyPzMVector>& parts) { + return util::find_decay_pair(parts, vm_mass); + }; + + // Define analysis flow + auto d_im = + d.Define("p_rec", momenta_from_tracking, {"outputTrackParameters"}) + .Define("N", "p_rec.size()") + .Define("p_sim", util::momenta_from_simulation, {"mcparticles2"}) + .Define("decay_pair_rec", find_decay_pair, {"p_rec"}) + .Define("decay_pair_sim", find_decay_pair, {"p_sim"}) + .Define("mass_rec", util::get_im, {"decay_pair_rec"}) + .Define("mass_sim", util::get_im, {"decay_pair_sim"}); + + // Define output histograms + auto h_im_rec = d_im.Histo1D( + {"h_im_rec", ";m_{ll'} (GeV);#", 100, -1.1, vm_mass + 5}, "mass_rec"); + auto h_im_sim = d_im.Histo1D( + {"h_im_sim", ";m_{ll'} (GeV);#", 100, -1.1, vm_mass + 5}, "mass_sim"); + + // Plot our histograms. + // TODO: to start I'm explicitly plotting the histograms, but want to + // factorize out the plotting code moving forward. + { + TCanvas c{"canvas", "canvas", 800, 800}; + gPad->SetLogx(false); + gPad->SetLogy(false); + auto& h0 = *h_im_sim; + auto& h1 = *h_im_rec; + // histogram style + h0.SetLineColor(plot::kMpBlue); + h0.SetLineWidth(2); + h1.SetLineColor(plot::kMpOrange); + h1.SetLineWidth(2); + // axes + h0.GetXaxis()->CenterTitle(); + h0.GetYaxis()->CenterTitle(); + // draw everything + h0.DrawClone("hist"); + h1.DrawClone("hist same"); + // FIXME hardcoded beam configuration + plot::draw_label(10, 100, detector, vm_name, "Invariant mass"); + TText* tptr; + auto t = new TPaveText(.6, .8417, .9, .925, "NB NDC"); + t->SetFillColorAlpha(kWhite, 0); + t->SetTextFont(43); + t->SetTextSize(25); + tptr = t->AddText("simulated"); + tptr->SetTextColor(plot::kMpBlue); + tptr = t->AddText("reconstructed"); + tptr->SetTextColor(plot::kMpOrange); + t->Draw(); + // Print canvas to output file + c.Print(fmt::format("{}vm_mass.png", output_prefix).c_str()); + } + // That's all! + return 0; +} diff --git a/dvmp/dvmp.sh b/dvmp/dvmp.sh index 379dedc3181423aca244a1d7b49c923fce9272c7..2c68a2a5475eaa2d36b3b0d1c33daa8366910392 100644 --- a/dvmp/dvmp.sh +++ b/dvmp/dvmp.sh @@ -48,14 +48,14 @@ bash util/build_detector.sh ## ============================================================================= ## Step 2: Run the simulation echo "Running Geant4 simulation" -npsim --runType batch \ +echo npsim --runType batch \ --part.minimalKineticEnergy 1000*GeV \ -v WARNING \ --numberOfEvents ${JUGGLER_N_EVENTS} \ --compactFile ${DETECTOR_PATH}/${JUGGLER_DETECTOR}.xml \ --inputFiles ${JUGGLER_GEN_FILE} \ --outputFile ${JUGGLER_SIM_FILE} -if [[ "$?" -ne "0" ]] ; then +if [ "$?" -ne "0" ] ; then echo "ERROR running npsim" exit 1 fi @@ -64,9 +64,9 @@ fi ## Step 3: Run digitization & reconstruction echo "Running the digitization and reconstruction" # FIXME Need to figure out how to pass file name to juggler from the commandline -xenv -x ${JUGGLER_INSTALL_PREFIX}/Juggler.xenv \ +echo xenv -x ${JUGGLER_INSTALL_PREFIX}/Juggler.xenv \ gaudirun.py options/tracker_reconstruction.py -if [[ "$?" -ne "0" ]] ; then +if [ "$?" -ne "0" ] ; then echo "ERROR running juggler" exit 1 fi @@ -74,12 +74,17 @@ ls -l ## ============================================================================= ## Step 4: Analysis -echo "STAND-IN FOR ANALYSIS SCRIPT" -#root -b -q "dis/scripts/rec_dis_electrons.cxx(\"${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE}\")" -#if [[ "$?" -ne "0" ]] ; then -# echo "ERROR running root script" -# exit 1 -#fi +root -b -q "dvmp/analysis/vm_mass.cxx(\ + \"results/dvmp/${JUGGLER_REC_FILE}\", \ + \"jpsi\", \ + \"electron\", \ + \"${JUGGLER_DETECTOR}\", \ + \"results/dvmp/plot\")" + +if [ "$?" -ne "0" ] ; then + echo "ERROR running root script" + exit 1 +fi ## ============================================================================= ## Step 5: finalize @@ -87,7 +92,7 @@ echo "Finalizing ${JUGGLER_FILE_NAME_TAG} benchmark" ## Copy over reconsturction artifacts as long as we don't have ## too many events -if [[ "${JUGGLER_N_EVENTS}" -lt "500" ]] ; then +if [ "${JUGGLER_N_EVENTS}" -lt "500" ] ; then cp ${JUGGLER_REC_FILE} results/dvmp/. fi