diff --git a/.gitignore b/.gitignore index 8fdeae7104dc3f3bc8bdc0586e2f8cab338df867..370a1989db9c164205c6b3144b09f234db1bdb93 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ BUILD*/* RELEASE*/* TEST*/* .local/* +build/* +install/* # cmake CMakeCache.txt diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..df59b35cc09bf3967a092c4415e6d1cbb0f13242 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.0.0 FATAL_ERROR) + +project(reconstruction_benchmarks LANGUAGES CXX) + +cmake_policy(SET CMP0074 NEW) # use `<PackageName>_ROOT` variables +include(GNUInstallDirs) + +# ROOT +find_package(ROOT 6 REQUIRED COMPONENTS Core RIO Hist) +include(${ROOT_USE_FILE}) + +# data model +find_package(podio REQUIRED) +find_package(EDM4HEP REQUIRED) +find_package(EDM4EIC REQUIRED) + +# logging +find_package(spdlog REQUIRED) +add_compile_definitions(SPDLOG_FMT_EXTERNAL) + +# benchmarks +add_subdirectory(benchmarks/rich) diff --git a/benchmarks/rich/CMakeLists.txt b/benchmarks/rich/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..9afbcef280e6c37a6097880ac6fde901f0f894cf --- /dev/null +++ b/benchmarks/rich/CMakeLists.txt @@ -0,0 +1,34 @@ +get_filename_component(det_name ${CMAKE_CURRENT_LIST_DIR} NAME) +project(${CMAKE_PROJECT_NAME}_${det_name}) + +# sources +set(algo_exe_source ${CMAKE_CURRENT_SOURCE_DIR}/src/benchmark.cc) +file(GLOB algo_headers CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/include/*.h) +file(GLOB algo_sources CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cc) +list(REMOVE_ITEM algo_sources ${algo_exe_source}) + +# library +set(algo_lib ${PROJECT_NAME}) +add_library(${algo_lib} SHARED ${algo_sources}) +target_include_directories(${algo_lib} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_compile_options(${algo_lib} PRIVATE -Wall -Wno-misleading-indentation) + +# executable +set(algo_exe benchmark_${det_name}_reconstruction) +add_executable(${algo_exe} ${algo_exe_source}) + +# linking +target_link_libraries(${algo_exe} + ${algo_lib} + ROOT::Core + ROOT::Hist + podio::podio + podio::podioRootIO + EDM4EIC::edm4eic + EDM4HEP::edm4hep + spdlog::spdlog + ) + +# installation +install(FILES ${algo_headers} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME}/${det_name}) +install(TARGETS ${algo_exe} ${algo_lib}) diff --git a/benchmarks/rich/README.md b/benchmarks/rich/README.md new file mode 100644 index 0000000000000000000000000000000000000000..3f2a53f74034149bcadb7a7f1778924dbd65414e --- /dev/null +++ b/benchmarks/rich/README.md @@ -0,0 +1,16 @@ +# RICH Benchmarks + +## Setup +- Build `reconstruction_benchmarks` with `cmake` + - Builds code in `src/` and `include/` to a library + - Benchmarks are designed as independent algorithms, similar to reconstruction algorithms + - Algorithm inputs are PODIO collections produced from reconstruction, and the output + is typically a set of `ROOT` plots + - Builds executable `benchmark_rich_reconstruction`, which runs any or all of the benchmark + algorithms and streams any plots to an output `ROOT` file + +## Running +- Run the benchmark executable using the wrapper script `run_benchmark.rb` + - Run with no arguments for usage guide + - This script can run simulation, reconstruction and/or benchmarks + - Run `benchmark_rich_reconstruction` to just run the benchmark algorithms diff --git a/benchmarks/rich/draw_benchmark.py b/benchmarks/rich/draw_benchmark.py new file mode 100755 index 0000000000000000000000000000000000000000..df220f6e1dcc56fc48958e6ae1efc85019dc655a --- /dev/null +++ b/benchmarks/rich/draw_benchmark.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python +# Copyright 2023, Christopher Dilks +# Subject to the terms in the LICENSE file found in the top-level directory. + +import ROOT as r +import sys, getopt, pathlib, math + +# suppress graphics +r.gROOT.SetBatch(True) + +# GLOBAL SETTINGS +################################################################ +RESID_MAX = 10 # cherenkov angle |residual| maximum +RADIATORS = { + 'Aerogel': { 'p_max': 30, 'p_rebin': 12, 'rindex_ref': 1.0190}, + 'Gas': { 'p_max': 70, 'p_rebin': 30, 'rindex_ref': 1.00076}, + 'Merged': { 'p_max': 70, 'p_rebin': 30, }, + } +PARTICLE_MASS = { + 'e': 0.00051, + 'pi': 0.13957, + 'K': 0.49368, + 'p': 0.93827, + } + +# ARGUMENTS +################################################################ + +ana_file_name = 'out/ana.edm4hep.root' +output_dir = 'out/ana.plots' + +helpStr = f''' +{sys.argv[0]} [OPTIONS] + + -i <input file>: specify an input file, e.g., hepmc + default: {ana_file_name} + + -o <output dir>: specify an output directory + default: {output_dir} + + -h: show this usage guide + + ''' + +try: + opts, args = getopt.getopt(sys.argv[1:], 'i:o:h') +except getopt.GetoptError: + print('\n\nERROR: invalid argument\n', helpStr, file=sys.stderr) + sys.exit(2) +for opt, arg in opts: + if(opt == '-i'): ana_file_name = arg.lstrip() + if(opt == '-o'): output_dir = arg.lstrip() + if(opt == '-h'): + print(helpStr) + sys.exit(2) +print(f''' +ana_file_name = {ana_file_name} +output_dir = {output_dir} +''') + + +# PLOTTING +################################################################ + +# make canvases +ana_file = r.TFile.Open(ana_file_name, "READ") +def make_canv(name, nx, ny): + canv = r.TCanvas(name, name, nx*600, ny*400) + canv.Divide(nx,ny) + return canv +canv_dict = { + "photon_spectra": make_canv("photon_spectra", 2, 2), + "digitization": make_canv("digitization", 2, 2), + "pidAerogel": make_canv("pidAerogel", 5, 3), + "pidGas": make_canv("pidGas", 5, 3), + "pidMerged": make_canv("pidMerged", 2, 3), +} + +# helper functions +### minimum momentum for Cherenkov radiation +def calculate_mom_min(mass,n): + return mass / math.sqrt(n**2-1) +### Cherenkov angle at saturation +def calculate_theta_max(n): + return 1000 * math.acos(1/n) +### execute `op` for every pad of `canv` +def loop_pads(canv, op): + for pad in canv.GetListOfPrimitives(): + if pad.InheritsFrom(r.TVirtualPad.Class()): + op(pad) +### draw a profile on 2D histogram `hist` +def draw_profile(hist, rad, style="BOX"): + prof = hist.ProfileX("_pfx", 1, -1, "i") + prof.SetMarkerStyle(r.kFullCircle) + prof.SetMarkerColor(r.kRed) + prof.SetMarkerSize(2) + hist_rebinned = hist.Clone(f'{hist.GetName()}_rebin') + if 'vs_p' in hist.GetName(): + hist_rebinned.RebinX(RADIATORS[rad]['p_rebin']) + else: + hist_rebinned.RebinX(4) + hist_rebinned.SetLineColor(r.kBlue) + hist_rebinned.SetFillColor(r.kBlue) + for p in [hist_rebinned, prof]: + if 'vs_p' in hist.GetName(): + p.GetXaxis().SetRangeUser(0, RADIATORS[rad]['p_max']) + if 'rindex_ref' in RADIATORS[rad] and 'theta_vs_p' in hist.GetName(): + p.GetYaxis().SetRangeUser(0, 1.5*calculate_theta_max(RADIATORS[rad]['rindex_ref'])) + if 'thetaResid_vs' in hist.GetName(): + p.GetYaxis().SetRangeUser(-RESID_MAX, RESID_MAX) + hist_rebinned.Draw(style) + prof.Draw("SAME") + +# Cherenkov angle vs. p functions +for rad, radHash in RADIATORS.items(): + if rad == "Merged": + continue + radHash['cherenkov_curves'] = [] + n = radHash['rindex_ref'] + for part, mass in PARTICLE_MASS.items(): + ftn = r.TF1( + f'ftn_theta_{part}_{rad}', + f'1000*TMath::ACos(TMath::Sqrt(x^2+{mass}^2)/({n}*x))', + calculate_mom_min(mass, n), + radHash['p_max'] + ) + ftn.SetLineColor(r.kBlack) + ftn.SetLineWidth(1) + radHash['cherenkov_curves'].append(ftn) + +# draw photon spectra +canv = canv_dict["photon_spectra"] +def canv_op(pad): + pad.SetGrid(1,1) + pad.SetLogy() +loop_pads(canv, canv_op) +canv.cd(1) +ana_file.Get("phot/phot_spectrum_sim").Draw() +canv.cd(2) +ana_file.Get("phot/nphot_dist").Draw() +canv.cd(3) +ana_file.Get("digi/phot_spectrum_rec").Draw() +canv.cd(4) +ana_file.Get("digi/nhits_dist").Draw() + +# draw digitization +canv = canv_dict["digitization"] +def canv_op(pad): + pad.SetGrid(1,1) + if pad.GetNumber() < 3: + pad.SetLogy() + else: + pad.SetLogz() +loop_pads(canv, canv_op) +canv.cd(1) +ana_file.Get("digi/adc_dist").Draw() +canv.cd(2) +ana_file.Get("digi/tdc_dist").Draw() +canv.cd(3) +ana_file.Get("digi/tdc_vs_adc").Draw("COLZ") + +# draw CherenkovPID for each radiator +for rad in ["Aerogel", "Gas"]: + pid_name = f'pid{rad}' + canv = canv_dict[pid_name] + def canv_op(pad): + pad.SetGrid(1,1) + loop_pads(canv, canv_op) + canv.cd(1) + ana_file.Get(f'{pid_name}/npe_dist_{rad}').Draw() + canv.cd(2) + ana_file.Get(f'{pid_name}/theta_dist_{rad}').Draw() + canv.cd(3) + hist = ana_file.Get(f'{pid_name}/thetaResid_dist_{rad}') + hist.DrawCopy() + canv.cd(4) + hist.SetTitle(hist.GetTitle() + " - ZOOM") + hist.GetXaxis().SetRangeUser(-RESID_MAX, RESID_MAX) + hist.Draw() + resid_mode = hist.GetBinCenter(hist.GetMaximumBin()) + resid_dev = hist.GetStdDev() + nsigma = 3 + hist.Fit("gaus", "", "", resid_mode - nsigma*resid_dev, resid_mode + nsigma*resid_dev) + if hist.GetFunction("gaus"): + hist.GetFunction("gaus").SetLineWidth(5) + canv.cd(5) + ana_file.Get(f'{pid_name}/highestWeight_dist_{rad}').Draw() + canv.cd(6) + hist = ana_file.Get(f'{pid_name}/npe_vs_p_{rad}') + draw_profile(hist, rad) + canv.cd(7) + hist = ana_file.Get(f'{pid_name}/theta_vs_p_{rad}') + draw_profile(hist, rad) + if 'cherenkov_curves' in RADIATORS[rad]: + for ftn in RADIATORS[rad]['cherenkov_curves']: + ftn.Draw("SAME") + canv.cd(8) + hist = ana_file.Get(f'{pid_name}/thetaResid_vs_p_{rad}') + hist.GetYaxis().SetRangeUser(-RESID_MAX, RESID_MAX) + draw_profile(hist, rad) + canv.cd(9) + ana_file.Get(f'{pid_name}/photonTheta_vs_photonPhi_{rad}').Draw("COLZ") + canv.cd(10) + ana_file.Get(f'{pid_name}/mcRindex_{rad}').Draw() + canv.cd(11) + hist = ana_file.Get(f'{pid_name}/npe_vs_eta_{rad}') + draw_profile(hist, rad) + canv.cd(12) + hist = ana_file.Get(f'{pid_name}/theta_vs_eta_{rad}') + draw_profile(hist, rad) + canv.cd(13) + hist = ana_file.Get(f'{pid_name}/thetaResid_vs_eta_{rad}') + hist.GetYaxis().SetRangeUser(-RESID_MAX, RESID_MAX) + draw_profile(hist, rad) + +# draw CherenkovPID for merged radiators +pid_name = f'pidMerged' +canv = canv_dict[pid_name] +def canv_op(pad): + pad.SetGrid(1,1) +loop_pads(canv, canv_op) +canv.cd(1) +ana_file.Get(f'{pid_name}/npe_dist_Merged').Draw() +canv.cd(2) +ana_file.Get(f'{pid_name}/highestWeight_dist_Merged').Draw() +canv.cd(3) +hist = ana_file.Get(f'{pid_name}/npe_vs_p_Merged') +draw_profile(hist, 'Merged') +canv.cd(4) +ana_file.Get(f'{pid_name}/photonTheta_vs_photonPhi_Merged').Draw("COLZ") +canv.cd(5) +hist = ana_file.Get(f'{pid_name}/npe_vs_eta_Merged') +draw_profile(hist, 'Merged') + +# FINISH +################################################################ + +pathlib.Path(output_dir).mkdir(parents=True, exist_ok=True) +for name, canvas in canv_dict.items(): + canvas.SaveAs(f'{output_dir}/{name}.png') +ana_file.Close() diff --git a/benchmarks/rich/forward_hadrons.sh b/benchmarks/rich/forward_hadrons.sh deleted file mode 100644 index aebf4ea89974a50507854fade7216405e18fce26..0000000000000000000000000000000000000000 --- a/benchmarks/rich/forward_hadrons.sh +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/bash - -./util/print_env.sh - -## To run the reconstruction, we need the following global variables: -## - DETECTOR: the detector package we want to use for this benchmark -## - DETECTOR_VERSION: the detector package we want to use for this benchmark -## - DETECTOR_PATH: full path to the detector definitions -## -## You can ready options/env.sh for more in-depth explanations of the variables -## and how they can be controlled. -source options/env.sh -export DETECTOR_PATH=${DETECTOR_PATH} - -if [[ ! -n "${JUGGLER_N_EVENTS}" ]] ; then - export JUGGLER_N_EVENTS=100 -fi - -export JUGGLER_FILE_NAME_TAG="rich_forward_hadrons" -export JUGGLER_GEN_FILE="${JUGGLER_FILE_NAME_TAG}.hepmc" - -export JUGGLER_SIM_FILE="sim_${JUGGLER_FILE_NAME_TAG}.edm4hep.root" -export JUGGLER_REC_FILE="rec_${JUGGLER_FILE_NAME_TAG}.root" - -echo "JUGGLER_N_EVENTS = ${JUGGLER_N_EVENTS}" -echo "DETECTOR = ${DETECTOR}" - - -# generate the input events -# note datasets is now only used to develop datasets. -#git clone https://eicweb.phy.anl.gov/EIC/datasets.git datasets -python benchmarks/rich/scripts/rich_data_gen.py \ - ${JUGGLER_FILE_NAME_TAG}.hepmc \ - -n ${JUGGLER_N_EVENTS} \ - --pmin 5.0 \ - --pmax 100.0 \ - --angmin 3.0 \ - --angmax 8.0 -if [[ "$?" -ne "0" ]] ; then - echo "ERROR running script" - exit 1 -fi - -# This script appears to be missing -## run geant4 simulations -#python benchmakrs/options/ForwardRICH/simu.py \ -# --compact=${DETECTOR_PATH}/${DETECTOR_CONFIG}.xml \ -# -i ${JUGGLER_FILE_NAME_TAG}.hepmc \ -# -o ${JUGGLER_SIM_FILE} -# -n ${JUGGLER_N_EVENTS} -#if [[ "$?" -ne "0" ]] ; then -# echo "ERROR running script" -# exit 1 -#fi - -#### skipping -exit 0 - - -ddsim --runType batch \ - --part.minimalKineticEnergy 1000*GeV \ - --filter.tracker edep0 \ - -v WARNING \ - --numberOfEvents ${JUGGLER_N_EVENTS} \ - --compactFile ${DETECTOR_PATH}/${DETECTOR_CONFIG}.xml \ - --inputFiles ${JUGGLER_FILE_NAME_TAG}.hepmc \ - --outputFile ${JUGGLER_SIM_FILE} -if [[ "$?" -ne "0" ]] ; then - echo "ERROR running npdet" - exit 1 -fi - -# @TODO changeable simulation file name and detector xml file name -gaudirun.py benchmarks/rich/options/rich_reco.py -status=$? -if [[ "$status" -ne "0" && "$status" -ne "4" ]] ; then - echo "ERROR running juggler, got $status" - exit 1 -fi - -# @TODO add analysis scripts -#root_filesize=$(stat --format=%s "${DETECTOR}/${JUGGLER_REC_FILE}") -#if [[ "${JUGGLER_N_EVENTS}" -lt "500" ]] ; then -# # file must be less than 10 MB to upload -# if [[ "${root_filesize}" -lt "10000000" ]] ; then -# cp ${DETECTOR}/${JUGGLER_REC_FILE} results/. -# fi -#fi - diff --git a/benchmarks/rich/include/ChargedParticle.h b/benchmarks/rich/include/ChargedParticle.h new file mode 100644 index 0000000000000000000000000000000000000000..254819a2a8145e9332da0194013e5b3ed80b869c --- /dev/null +++ b/benchmarks/rich/include/ChargedParticle.h @@ -0,0 +1,42 @@ +// Copyright 2023, Christopher Dilks +// Subject to the terms in the LICENSE file found in the top-level directory. + +#pragma once + +#include <spdlog/spdlog.h> + +#include <edm4hep/MCParticleCollection.h> +#include <edm4hep/utils/vector_utils.h> + +namespace benchmarks { + + class ChargedParticle { + + public: + + ChargedParticle(std::shared_ptr<spdlog::logger> log) : m_log(log) {}; + ~ChargedParticle() {} + + // accessors + double GetMomentum() { return m_momentum; }; + double GetEta() { return m_eta; }; + int GetPDG() { return m_pdg; }; + + // set info from a single MCParticle + void SetSingleParticle(const edm4hep::MCParticle& mc_part); + // set info from a (thrown) MCParticle of a collection + void SetSingleParticle(const edm4hep::MCParticleCollection& mc_parts); + + private: + + // members + double m_momentum; + double m_eta; + int m_pdg = 0; + + // logger + std::shared_ptr<spdlog::logger> m_log; + + }; + +} diff --git a/benchmarks/rich/include/CherenkovPIDAnalysis.h b/benchmarks/rich/include/CherenkovPIDAnalysis.h new file mode 100644 index 0000000000000000000000000000000000000000..76a310e73d028856d5068c5984059deea75863dc --- /dev/null +++ b/benchmarks/rich/include/CherenkovPIDAnalysis.h @@ -0,0 +1,72 @@ +// Copyright 2023, Christopher Dilks +// Subject to the terms in the LICENSE file found in the top-level directory. + +#pragma once + +#include <spdlog/spdlog.h> + +#include <TH1D.h> +#include <TH2D.h> +#include <TMath.h> +#include <TRegexp.h> + +#include <edm4eic/CherenkovParticleIDCollection.h> +#include <edm4hep/MCParticleCollection.h> +#include <edm4hep/utils/vector_utils.h> +#include <edm4hep/utils/kinematics.h> + +#include "Tools.h" +#include "ChargedParticle.h" + +namespace benchmarks { + + class CherenkovPIDAnalysis { + + public: + CherenkovPIDAnalysis() = default; + ~CherenkovPIDAnalysis() {} + + // algorithm methods + void AlgorithmInit( + std::string rad_name, + std::shared_ptr<spdlog::logger>& logger + ); + void AlgorithmProcess( + const edm4hep::MCParticleCollection& mc_parts, + const edm4eic::CherenkovParticleIDCollection& cherenkov_pids + ); + void AlgorithmFinish(); + + private: + + // radiator name + std::string m_rad_name; + TString m_rad_name_trun; + TString m_rad_title; + + // histograms + // - distributions + TH1D *m_npe_dist; + TH1D *m_theta_dist; + TH1D *m_thetaResid_dist; + TH1D *m_mcWavelength_dist; + TH1D *m_mcRindex_dist; + TH1D *m_highestWeight_dist; + TH2D *m_photonTheta_vs_photonPhi; + // - momentum scans + TH2D *m_npe_vs_p; + TH2D *m_theta_vs_p; + TH2D *m_thetaResid_vs_p; + TH2D *m_highestWeight_vs_p; + // - pseudorapidity scans + TH2D *m_npe_vs_eta; + TH2D *m_theta_vs_eta; + TH2D *m_thetaResid_vs_eta; + TH2D *m_highestWeight_vs_eta; + + // logger + std::shared_ptr<spdlog::logger> m_log; + + }; + +} diff --git a/benchmarks/rich/include/RawHitAnalysis.h b/benchmarks/rich/include/RawHitAnalysis.h new file mode 100644 index 0000000000000000000000000000000000000000..56b0b839b0e3cac38b1368ef8a816fbb81fe5d9e --- /dev/null +++ b/benchmarks/rich/include/RawHitAnalysis.h @@ -0,0 +1,52 @@ +// Copyright 2023, Christopher Dilks +// Subject to the terms in the LICENSE file found in the top-level directory. + +#pragma once + +#include <spdlog/spdlog.h> + +#include <TH1D.h> +#include <TH2D.h> +#include <TMath.h> + +#include <edm4eic/RawTrackerHitCollection.h> +#include <edm4eic/MCRecoTrackerHitAssociationCollection.h> + +#include "Tools.h" + +namespace benchmarks { + + class RawHitAnalysis { + + public: + + RawHitAnalysis() = default; + ~RawHitAnalysis() {} + + // algorithm methods + void AlgorithmInit(std::shared_ptr<spdlog::logger>& logger); + void AlgorithmProcess( + const edm4eic::RawTrackerHitCollection& raw_hits, + const edm4eic::MCRecoTrackerHitAssociationCollection& assocs + ); + void AlgorithmFinish(); + + private: + + // binning + const int adc_max = std::pow(2,10); + const int tdc_max = std::pow(2,10); + + // histograms + TH1D *m_adc_dist; + TH1D *m_tdc_dist; + TH2D *m_tdc_vs_adc; + TH1D *m_phot_spectrum; + TH1D *m_nhits_dist; + + // logging + std::shared_ptr<spdlog::logger> m_log; + + }; + +} diff --git a/benchmarks/rich/include/ReconstructedParticleAnalysis.h b/benchmarks/rich/include/ReconstructedParticleAnalysis.h new file mode 100644 index 0000000000000000000000000000000000000000..5ca8f6945d0db1b22234dfae9c1569dc56265f83 --- /dev/null +++ b/benchmarks/rich/include/ReconstructedParticleAnalysis.h @@ -0,0 +1,42 @@ +// Copyright 2023, Christopher Dilks +// Subject to the terms in the LICENSE file found in the top-level directory. + +#pragma once + +#include <spdlog/spdlog.h> + +#include <TH2D.h> +#include <TGraphErrors.h> + +#include <edm4eic/MCRecoParticleAssociationCollection.h> +#include <edm4hep/utils/kinematics.h> + +#include "Tools.h" + +namespace benchmarks { + + class ReconstructedParticleAnalysis { + + public: + ReconstructedParticleAnalysis() = default; + ~ReconstructedParticleAnalysis() {} + + // algorithm methods + void AlgorithmInit(std::shared_ptr<spdlog::logger>& logger); + void AlgorithmProcess( + const edm4eic::MCRecoParticleAssociationCollection& in_assocs + ); + void AlgorithmFinish(); + + private: + + // plots + TGraphErrors *m_correct_vs_p; + TH2D *m_correct_vs_p_transient; // (not written) + + // additional objects + std::shared_ptr<spdlog::logger> m_log; + + }; + +} diff --git a/benchmarks/rich/include/SimHitAnalysis.h b/benchmarks/rich/include/SimHitAnalysis.h new file mode 100644 index 0000000000000000000000000000000000000000..836a6ad2999c4d25a21e967d5cccaee1acab4ff2 --- /dev/null +++ b/benchmarks/rich/include/SimHitAnalysis.h @@ -0,0 +1,46 @@ +// Copyright 2023, Christopher Dilks +// Subject to the terms in the LICENSE file found in the top-level directory. + +#pragma once + +#include <spdlog/spdlog.h> + +#include <TH1D.h> +#include <TH2D.h> + +#include <edm4hep/SimTrackerHitCollection.h> +#include <edm4hep/MCParticleCollection.h> + +#include "Tools.h" +#include "ChargedParticle.h" + +namespace benchmarks { + + class SimHitAnalysis { + + public: + SimHitAnalysis() = default; + ~SimHitAnalysis() {} + + // algorithm methods + void AlgorithmInit(std::shared_ptr<spdlog::logger>& logger); + void AlgorithmProcess( + const edm4hep::MCParticleCollection& mc_parts, + const edm4hep::SimTrackerHitCollection& sim_hits + ); + void AlgorithmFinish(); + + private: + + // histograms + TH1D *m_nphot_dist; + TH2D *m_nphot_vs_p; + TH1D *m_nphot_vs_p__transient; // transient (not written) + TH1D *m_phot_spectrum; + + // logger + std::shared_ptr<spdlog::logger> m_log; + + }; + +} diff --git a/benchmarks/rich/include/Tools.h b/benchmarks/rich/include/Tools.h new file mode 100644 index 0000000000000000000000000000000000000000..c34a142a9708ce45730ed6113f3ae77733de05ca --- /dev/null +++ b/benchmarks/rich/include/Tools.h @@ -0,0 +1,103 @@ +// Copyright 2023, Christopher Dilks +// Subject to the terms in the LICENSE file found in the top-level directory. + +#pragma once + +// general +#include <map> +#include <spdlog/spdlog.h> + +// ROOT +#include <TVector3.h> + +// IRT +#include <IRT/ParametricSurface.h> + +// DD4hep +#include <Evaluator/DD4hepUnits.h> + +// data model +#include <edm4hep/SimTrackerHitCollection.h> +#include <edm4hep/utils/kinematics.h> + +namespace benchmarks { + + // Tools class, filled with miscellanous helper functions + class Tools { + public: + + // ------------------------------------------------------------------------------------- + // physics constants + + // h*c constant, for wavelength <=> energy conversion [GeV*nm] + // obtained from `dd4hep::h_Planck * dd4hep::c_light / (dd4hep::GeV * dd4hep::nm)` + static constexpr double HC = 1.239841875e-06; + + // opticalphoton PDG + static constexpr int PHOTON_PDG = -22; + + // ------------------------------------------------------------------------------------- + // PDG mass lookup + + // local PDG mass database + // FIXME: cannot use `TDatabasePDG` since it is not thread safe; until we + // have a proper PDG database service, we hard-code the masses we need; + // use Tools::GetPDGMass for access + static std::unordered_map<int,double> GetPDGMasses() { + return std::unordered_map<int,double>{ + { 11, 0.000510999 }, + { 211, 0.13957 }, + { 321, 0.493677 }, + { 2212, 0.938272 } + }; + } + + static double GetPDGMass(int pdg) { + double mass; + try { mass = GetPDGMasses().at(std::abs(pdg)); } + catch(const std::out_of_range& e) { + throw std::runtime_error(fmt::format("RUNTIME ERROR: unknown PDG={} in algorithms/pid/Tools::GetPDGMass",pdg)); + } + return mass; + } + + static int GetNumPDGs() { return GetPDGMasses().size(); }; + + + // ------------------------------------------------------------------------------------- + // get photon wavelength + static double GetPhotonWavelength(const edm4hep::SimTrackerHit& hit, std::shared_ptr<spdlog::logger> log) { + auto phot = hit.getMCParticle(); + if(!phot.isAvailable()) { + log->error("no MCParticle in hit"); + return -1.0; + } + if(phot.getPDG() != PHOTON_PDG) { + log->warn("hit MCParticle is not an opticalphoton; PDG is {}; ignoring", phot.getPDG()); + return -1.0; + } + auto momentum = edm4hep::utils::p(phot); + auto wavelength = momentum > 0 ? HC / momentum : 0.0; + return wavelength; + } + + // ------------------------------------------------------------------------------------- + // binning + static constexpr int n_bins = 100; + static constexpr int momentum_bins = 500; + static constexpr double momentum_max = 70; + static constexpr int eta_bins = 50; + static constexpr double eta_min = 0; + static constexpr double eta_max = 5; + static constexpr int npe_max = 50; + static constexpr int nphot_max = 3000; + static constexpr int theta_bins = 1500; + static constexpr double theta_max = 300; + static constexpr double thetaResid_max = 100; + static constexpr int phi_bins = 100; + static constexpr int wl_bins = 120; + static constexpr double wl_min = 0; + static constexpr double wl_max = 1200; + + }; // class Tools +} // namespace benchmarks diff --git a/benchmarks/rich/options/rich_reco.py b/benchmarks/rich/options/rich_reco.py deleted file mode 100644 index f27f62a534302aa0f266e1d10f046e867ef7d283..0000000000000000000000000000000000000000 --- a/benchmarks/rich/options/rich_reco.py +++ /dev/null @@ -1,43 +0,0 @@ -from Gaudi.Configuration import * -from GaudiKernel import SystemOfUnits as units - -from Configurables import ApplicationMgr, EICDataSvc, PodioOutput, GeoSvc - -detector_path = str(os.environ.get("DETECTOR_PATH", ".")) -detector_name = str(os.environ.get("DETECTOR_CONFIG", "epic")) -detector_config = str(os.environ.get("DETECTOR_CONFIG", detector_name)) -detector_version = str(os.environ.get("DETECTOR_VERSION", "main")) - -# todo add checks -input_sim_file = str(os.environ["JUGGLER_SIM_FILE"]) -output_rec_file = str(os.environ["JUGGLER_REC_FILE"]) -n_events = str(os.environ["JUGGLER_N_EVENTS"]) - -geo_service = GeoSvc("GeoSvc", detectors=["{}/{}.xml".format(detector_path, detector_name)]) -podioevent = EICDataSvc("EventDataSvc", inputs=[input_sim_file], OutputLevel=DEBUG) - -from Configurables import PodioInput -from Configurables import Jug__Digi__PhotoMultiplierDigi as PhotoMultiplierDigi -from Configurables import Jug__Reco__PhotoMultiplierReco as PhotoMultiplierReco -from Configurables import Jug__Reco__PhotoRingClusters as PhotoRingClusters - -qe_data = [(1.0, 0.25), (7.5, 0.25),] -podioinput = PodioInput("PodioReader", collections=["MCParticles", "ForwardRICHHits"], OutputLevel=DEBUG) - -pmtdigi = PhotoMultiplierDigi(inputHitCollection="ForwardRICHHits", outputHitCollection="DigiForwardRICHHits", - quantumEfficiency=[(a*units.eV, b) for a, b in qe_data]) -pmtreco = PhotoMultiplierReco(inputHitCollection="DigiForwardRICHHits", outputHitCollection="RecoForwardRICHHits") -richcluster = PhotoRingClusters(inputHitCollection="RecoForwardRICHHits", #inputTrackCollection="MCParticles", - outputClusterCollection="RICHClusters") - -out = PodioOutput("out", filename=output_rec_file) -out.outputCommands = ["keep *"] - -ApplicationMgr( - TopAlg = [podioinput, pmtdigi, pmtreco, richcluster, out], - EvtSel = 'NONE', - EvtMax = n_events, - ExtSvc = [podioevent], - OutputLevel=DEBUG - ) - diff --git a/benchmarks/rich/run_benchmark.rb b/benchmarks/rich/run_benchmark.rb new file mode 100755 index 0000000000000000000000000000000000000000..0a1a748e3643529aafad8756a412f52090c7cee3 --- /dev/null +++ b/benchmarks/rich/run_benchmark.rb @@ -0,0 +1,240 @@ +#!/usr/bin/env ruby +# Copyright 2023, Christopher Dilks +# Subject to the terms in the LICENSE file found in the top-level directory. + +require 'optparse' +require 'ostruct' + +################################### +# constants: +IdealTheta = 23.5 # [degrees] +IdealEnergy = 20.0 # [GeV] +IdealParticle = 'pi+' +################################### + +# setup +# --------------------------------------------------- + +# default opt +opt = OpenStruct.new +opt.evgen = '' +opt.sim_file = 'out/sim.edm4hep.root' +opt.rec_file = 'out/rec.edm4hep.root' +opt.ana_file = 'out/ana.edm4hep.root' +opt.run_sim = true +opt.run_rec = true +opt.run_ana = true +opt.run_rec_down = false +opt.run_ana_down = false +opt.num_events = 10 +opt.benchmark_exe = 'benchmark_rich_reconstruction' +opt.algos = Array.new +opt.plots = Array.new +opt.verbosity = 0 +opt.dry_run = false + +# available event generation modes +avail_evgens = [ + 'idealAngle', + 'minTheta', + 'maxTheta', +] + +# parse options +required_set = false +OptionParser.new do |o| + o.banner = "USAGE: #{$0} [OPTIONS]..." + o.separator '' + o.separator 'required options, one of either:'.upcase + o.separator '' + o.on("-r", "--rec-only", "Run only the reconstruction, then the analysis benchmark") do |a| + opt.run_rec_down = true + required_set = true + end + o.separator '' + o.on("-b", "--ana-only", "Run only the analysis benchmark") do |a| + opt.run_ana_down = true + required_set = true + end + o.separator '' + o.on("-e", "--evgen [EVGEN_MODE]", "Run the event generation, reconstruction, and analysis", + "[EVGEN_MODE] must be one of:") do |a| + unless avail_evgens.include? a + $stderr.puts "ERROR: unknown event generation mode '#{a}'" + exit 1 + end + opt.evgen = a + required_set = true + end + avail_evgens.each{ |it| o.separator ' '*40+it } + o.separator '' + o.separator 'optional options:'.upcase + o.separator '' + o.on("--sim-file [FILE]", "simulation file name", "default = #{opt.sim_file}") { |a| opt.sim_file=a } + o.on("--rec-file [FILE]", "reconstruction file name", "default = #{opt.rec_file}") { |a| opt.rec_file=a } + o.on("--ana-file [FILE]", "analysis file name", "default = #{opt.ana_file}") { |a| opt.ana_file=a } + o.separator '' + o.on("--[no-]run-sim", "simulation on/off", "default = #{opt.run_sim ? 'on' : 'off'}") { |a| opt.run_sim=a } + o.on("--[no-]run-rec", "reconstruction on/off", "default = #{opt.run_rec ? 'on' : 'off'}") { |a| opt.run_rec=a } + o.on("--[no-]run-ana", "analysis benchmark on/off", "default = #{opt.run_ana ? 'on' : 'off'}") { |a| opt.run_ana=a } + o.separator '' + o.on("-n", "--num-events [NUM_EVENTS]", Integer, "Number of events", "default = #{opt.num_events}") { |a| opt.num_events=a } + o.on("-a", "--algos [ALGORITHMS]...", Array, "List of analysis algorithms to run; default = all", + "delimit by commas, no spaces", + "for more info, run: #{opt.benchmark_exe}") { |a| opt.algos=a } + o.on("-p", "--plots [PLOTS]...", Array, "List of plots to draw", + "delimit by commas, no spaces", + "default = all") { |a| opt.plots=a } + o.on("-x", "--benchmark-exe [EXECUTABLE]", "benchmark executable", + "default = #{opt.benchmark_exe} (from $PATH)") { |a| opt.benchmark_exe=a} + o.separator '' + o.on("-v", "--verbose", "Increase verbosity (-vv for more verbose)") { |a| opt.verbosity+=1 } + o.on("-d", "--dry-run", "Dry run (just print commands)") { |a| opt.dry_run=true } + o.separator '' + o.on_tail("-h", "--help", "Show this message") do + puts o + exit 2 + end +end.parse!(ARGV.length>0 ? ARGV : ['--help']) + +# print options +puts 'settings: {'.upcase +opt.each_pair { |k,v| puts "#{k.to_s.rjust(20)} => #{v}" } +puts '}' + +# check for required options +unless required_set + $stderr.puts "ERROR: required options have not been set" + $stderr.puts "run '#{$0} --help' for guidance" + exit 1 +end + +# figure out which steps to run +run_step = { :sim=>false, :rec=>false, :ana=>false, } +if opt.run_ana_down + run_step[:ana] = true +elsif opt.run_rec_down + run_step[:rec] = true + run_step[:ana] = true +else + run_step[:sim] = opt.run_sim + run_step[:rec] = opt.run_rec + run_step[:ana] = opt.run_ana +end +puts "steps to run: #{run_step}" + +# get compact file +if ENV['DETECTOR_PATH'].nil? or ENV['DETECTOR_CONFIG'].nil? + $stderr.puts "ERROR: unknown DETECTOR_PATH or DETECTOR_CONFIG" + exit 1 +end +compact_file = "#{ENV['DETECTOR_PATH']}/#{ENV['DETECTOR_CONFIG']}.xml" + + +# event generator command generators +# --------------------------------------------------- +def theta2xyz(theta) + rad = theta * Math::PI / 180 + [ Math.sin(rad), 0.0, Math.cos(rad) ] +end + +# fixed angle particle gun +evgen_fixed_angle = Proc.new do |theta, energy, particle| + [ + "npsim", + "--runType batch", + "--compactFile #{compact_file}", + "--outputFile #{opt.sim_file}", + "--part.userParticleHandler=''", + "--enableGun", + "--numberOfEvents #{opt.num_events}", + "--gun.particle #{particle}", + "--gun.energy #{energy}*GeV", + "--gun.direction (#{theta2xyz(theta).join ", "})", + ] +end + + +# define event generator command +# --------------------------------------------------- +evgen_cmd = Array.new +case opt.evgen +when 'idealAngle' + evgen_cmd = evgen_fixed_angle.call IdealTheta, IdealEnergy, IdealParticle +else + exit 1 if run_step[:sim] +end + + +# define reconstruction command +# --------------------------------------------------- +output_collections = [ + "DRICHHits", + "MCParticles", + "DRICHRawHits", + "DRICHRawHitsAssociations", + "DRICHAerogelTracks", + "DRICHGasTracks", + "DRICHAerogelIrtCherenkovParticleID", + "DRICHGasIrtCherenkovParticleID", + "DRICHMergedIrtCherenkovParticleID", + "ReconstructedChargedParticleAssociationsWithDRICHPID", +] +recon_cmd = [ + 'eicrecon', + "-Ppodio:output_include_collections=\"#{output_collections.join ','}\"", + '-Pjana:nevents="0"', + '-Pjana:debug_plugin_loading="1"', + '-Pacts:MaterialMap="calibrations/materials-map.cbor"', + "-Ppodio:output_file=\"#{opt.rec_file}\"", + opt.sim_file, +] + + +# define analysis benchmark command +# --------------------------------------------------- +analysis_cmd = [ + opt.benchmark_exe, + "-i #{opt.rec_file}", + "-o #{opt.ana_file}", +] +analysis_cmd.append "-a #{opt.algos.join ' '}" if opt.algos.size > 0 +analysis_cmd.append '-' + 'v'*opt.verbosity if opt.verbosity > 0 + +# define analysis draw command +# --------------------------------------------------- +draw_cmd = [ + "#{__dir__}/draw_benchmark.py", + "-i #{opt.ana_file}", + "-o #{opt.ana_file.gsub(/edm4hep.root$/,"plots")}" +] + +# execute commands +# --------------------------------------------------- + +# proc: execute a command; raise runtime exception if failure (exit nonzero) +exe = Proc.new do |cmd_args, name, step| + if run_step[step] + cmd = cmd_args.join ' ' + puts "benchmark #{name} command:".upcase + cmd_args.each_with_index do |arg,i| + line = i==0 ? '' : ' ' + line += arg + line += ' \\' unless i+1==cmd_args.size + puts line + end + unless opt.dry_run + puts '-'*50 + puts "#{name} execution:".upcase + system cmd or raise "benchmark #{name} failed!".upcase + end + puts '-'*50 + end +end +puts '-'*50 + +# execute the commands +exe.call evgen_cmd, 'event generation', :sim +exe.call recon_cmd, 'reconstruction', :rec +exe.call analysis_cmd, 'analysis', :ana +exe.call draw_cmd, 'draw', :ana diff --git a/benchmarks/rich/scripts/rich_samples.py b/benchmarks/rich/scripts/rich_samples.py deleted file mode 100644 index 0dcc2bdbcd5e73e31bb70fb4e0028dc37cf1112f..0000000000000000000000000000000000000000 --- a/benchmarks/rich/scripts/rich_samples.py +++ /dev/null @@ -1,44 +0,0 @@ -import ROOT -import numpy as np -from ROOT import gROOT -import argparse -from matplotlib import pyplot as plt -from matplotlib.patches import Ellipse -import matplotlib.transforms as transforms - -parser = argparse.ArgumentParser(description='rich reconstruction analysis') -parser.add_argument('nevents', type=int, help='number of events') -args = parser.parse_args() - -gROOT.Macro('rootlogon.C') -f = ROOT.TFile("rich_test_reco.root") -events = f.events - -nev = 0 -while nev < args.nevents: - iev = np.random.randint(0, events.GetEntries()) - events.GetEntry(iev) - if events.RICHClusters.size() == 0: - continue - - nev += 1 - x, y = [], [] - for hit in events.RecoForwardRICHHits: - x.append(hit.position.x) - y.append(hit.position.y) - - fig, ax = plt.subplots(figsize=(9, 9), dpi=120) - colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] - ax.scatter(x, y, s=9, marker='s', color=colors[0]) - - for cluster in events.RICHClusters: - ellipse = Ellipse((cluster.position.x, cluster.position.y), width=cluster.radius * 2, height=cluster.radius * 2, - edgecolor='red', fill=False, lw=2, alpha=0.8) - ellipse.set_transform(ax.transData) - ax.add_patch(ellipse) - ax.set_xlim(-60, 60) - ax.set_ylim(-60, 60) - ax.set_xlabel('X (mm)') - ax.set_ylabel('Y (mm)') - fig.savefig('rich_cluster_{}.png'.format(iev)) - diff --git a/benchmarks/rich/src/ChargedParticle.cc b/benchmarks/rich/src/ChargedParticle.cc new file mode 100644 index 0000000000000000000000000000000000000000..60fbd938b56481ee974ea4a2c5a5487bf5bd4597 --- /dev/null +++ b/benchmarks/rich/src/ChargedParticle.cc @@ -0,0 +1,28 @@ +// Copyright 2023, Christopher Dilks +// Subject to the terms in the LICENSE file found in the top-level directory. + +#include "ChargedParticle.h" + +namespace benchmarks { + + // set info from a single MCParticle + void ChargedParticle::SetSingleParticle(const edm4hep::MCParticle& mc_part) { + m_momentum = edm4hep::utils::magnitude(mc_part.getMomentum()); + m_eta = edm4hep::utils::eta(mc_part.getMomentum()); + m_pdg = mc_part.getPDG(); + } + + // set info from a (thrown) MCParticle of a collection + void ChargedParticle::SetSingleParticle(const edm4hep::MCParticleCollection& mc_parts) { + int n_thrown = 0; + for(const auto& mc_part : mc_parts) { + if(mc_part.getGeneratorStatus() == 1) { + this->SetSingleParticle(mc_part); + n_thrown++; + } + } + if(n_thrown == 0) m_log->warn("ChargedParticle::SetSingleParticle: no particle found"); + if(n_thrown > 1) m_log->warn("ChargedParticle::SetSingleParticle: found {} particles (more than one)", n_thrown); + } + +} diff --git a/benchmarks/rich/src/CherenkovPIDAnalysis.cc b/benchmarks/rich/src/CherenkovPIDAnalysis.cc new file mode 100644 index 0000000000000000000000000000000000000000..795dbe814d9a7640b253fddec5f7570063a4ef47 --- /dev/null +++ b/benchmarks/rich/src/CherenkovPIDAnalysis.cc @@ -0,0 +1,234 @@ +// Copyright 2023, Christopher Dilks +// Subject to the terms in the LICENSE file found in the top-level directory. + +#include "CherenkovPIDAnalysis.h" + +namespace benchmarks { + + // AlgorithmInit + //--------------------------------------------------------------------------- + void CherenkovPIDAnalysis::AlgorithmInit( + std::string rad_name, + std::shared_ptr<spdlog::logger>& logger + ) + { + m_rad_name = rad_name; + m_log = logger; + + // set radiator title + m_rad_title = TString(m_rad_name); + if(m_rad_name=="Merged") m_rad_title += " Aerogel+Gas"; + + // truncate `m_rad_name` (for object names) + m_rad_name_trun = TString(m_rad_name); + m_rad_name_trun(TRegexp(" .*")) = ""; + + // distributions + m_npe_dist = new TH1D( + "npe_dist_"+m_rad_name_trun, + "Overall NPE for "+m_rad_title+";NPE", + Tools::npe_max, 0, Tools::npe_max + ); + m_theta_dist = new TH1D( + "theta_dist_"+m_rad_name_trun, + "Estimated Cherenkov Angle for "+m_rad_title+";#theta [mrad]", + Tools::theta_bins, 0, Tools::theta_max + ); + m_thetaResid_dist = new TH1D( + "thetaResid_dist_"+m_rad_name_trun, + "Estimated Cherenkov Angle Residual for "+m_rad_title+";#Delta#theta [mrad]", + Tools::theta_bins, -Tools::thetaResid_max, Tools::thetaResid_max + ); + m_photonTheta_vs_photonPhi = new TH2D( + "photonTheta_vs_photonPhi_"+m_rad_name_trun, + "Estimated Photon #theta vs #phi for "+m_rad_title+";#phi [rad];#theta [mrad]", + Tools::phi_bins, -TMath::Pi(), TMath::Pi(), + Tools::theta_bins, 0, Tools::theta_max + ); + + // truth distributions + m_mcWavelength_dist = new TH1D( + "mcWavelength_"+m_rad_name_trun, + "MC Photon Wavelength for "+m_rad_title+";#lambda [nm]", + Tools::n_bins, 0, 1000 + ); + m_mcRindex_dist = new TH1D( + "mcRindex_"+m_rad_name_trun, + "MC Refractive Index for "+m_rad_title+";n", + 10*Tools::n_bins, 0.99, 1.03 + ); + + // PID + m_highestWeight_dist = new TH1D( + "highestWeight_dist_"+m_rad_name_trun, + "Highest PDG Weight for "+m_rad_title+";PDG", + Tools::GetNumPDGs()+1, 0, Tools::GetNumPDGs()+1 + ); + + // momentum scans + m_npe_vs_p = new TH2D( + "npe_vs_p_"+m_rad_name_trun, + "Overall NPE vs. Particle Momentum for "+m_rad_title+";p [GeV];NPE", + Tools::momentum_bins, 0, Tools::momentum_max, + Tools::npe_max, 0, Tools::npe_max + ); + m_theta_vs_p = new TH2D( + "theta_vs_p_"+m_rad_name_trun, + "Estimated Cherenkov Angle vs. Particle Momentum for "+m_rad_title+";p [GeV];#theta [mrad]", + Tools::momentum_bins, 0, Tools::momentum_max, + Tools::theta_bins, 0, Tools::theta_max + ); + m_thetaResid_vs_p = new TH2D( + "thetaResid_vs_p_"+m_rad_name_trun, + "Estimated Cherenkov Angle Residual vs. Particle Momentum for "+m_rad_title+";p [GeV];#Delta#theta [mrad]", + Tools::momentum_bins, 0, Tools::momentum_max, + Tools::theta_bins, -Tools::thetaResid_max, Tools::thetaResid_max + ); + m_highestWeight_vs_p = new TH2D( + "highestWeight_vs_p_"+m_rad_name_trun, + "Highest PDG Weight vs. Particle Momentum for "+m_rad_title+";p [GeV]", + Tools::momentum_bins, 0, Tools::momentum_max, + Tools::GetNumPDGs()+1, 0, Tools::GetNumPDGs()+1 + ); + + // pseudorapidity scans + m_npe_vs_eta = new TH2D( + "npe_vs_eta_"+m_rad_name_trun, + "Overall NPE vs. Pseudorapidity for "+m_rad_title+";#eta;NPE", + Tools::eta_bins, Tools::eta_min, Tools::eta_max, + Tools::npe_max, 0, Tools::npe_max + ); + m_theta_vs_eta = new TH2D( + "theta_vs_eta_"+m_rad_name_trun, + "Estimated Cherenkov Angle vs. Pseudorapidity for "+m_rad_title+";#eta;#theta [mrad]", + Tools::eta_bins, Tools::eta_min, Tools::eta_max, + Tools::theta_bins, 0, Tools::theta_max + ); + m_thetaResid_vs_eta = new TH2D( + "thetaResid_vs_eta_"+m_rad_name_trun, + "Estimated Cherenkov Angle Residual vs. Pseudorapidity for "+m_rad_title+";#eta;#Delta#theta [mrad]", + Tools::eta_bins, Tools::eta_min, Tools::eta_max, + Tools::theta_bins, -Tools::thetaResid_max, Tools::thetaResid_max + ); + m_highestWeight_vs_eta = new TH2D( + "highestWeight_vs_eta_"+m_rad_name_trun, + "Highest PDG Weight vs. Pseudorapidity for "+m_rad_title+";#eta", + Tools::eta_bins, Tools::eta_min, Tools::eta_max, + Tools::GetNumPDGs()+1, 0, Tools::GetNumPDGs()+1 + ); + + // format histograms + auto format1D = [] (auto h) { + h->SetLineColor(kAzure); + h->SetFillColor(kAzure); + }; + format1D(m_npe_dist); + format1D(m_theta_dist); + format1D(m_thetaResid_dist); + format1D(m_mcWavelength_dist); + format1D(m_mcRindex_dist); + format1D(m_highestWeight_dist); + } + + + // AlgorithmProcess + //--------------------------------------------------------------------------- + void CherenkovPIDAnalysis::AlgorithmProcess( + const edm4hep::MCParticleCollection& mc_parts, + const edm4eic::CherenkovParticleIDCollection& cherenkov_pids + ) + { + m_log->trace("-> {} Radiator:", m_rad_name); + + // get thrown momentum from a single MCParticle + auto part = std::make_unique<ChargedParticle>(m_log); + part->SetSingleParticle(mc_parts); + auto thrown_momentum = part->GetMomentum(); + auto thrown_eta = part->GetEta(); + auto thrown_pdg = part->GetPDG(); + + // loop over `CherenkovParticleID` objects + for(const auto& cherenkov_pid : cherenkov_pids) { + + // skip if NPE==0 + if(cherenkov_pid.getNpe() == 0) { + m_log->warn("Event found with NPE=0"); + continue; + } + + // estimate the charged particle momentum using the momentum of the first TrackPoint at this radiator's entrance + /* + auto charged_particle = cherenkov_pid.getChargedParticle(); + if(!charged_particle.isAvailable()) { m_log->warn("Charged particle not available in this radiator"); continue; } + if(charged_particle.points_size()==0) { m_log->warn("Charged particle has no TrackPoints in this radiator"); continue; } + auto charged_particle_momentum = edm4hep::utils::magnitude( charged_particle.getPoints(0).momentum ); + m_log->trace(" Charged Particle p = {} GeV at radiator entrance", charged_particle_momentum); + m_log->trace(" If it is a pion, E = {} GeV", std::hypot(charged_particle_momentum, Tools::GetPDGMass(211))); + */ + // alternatively: use `thrown_momentum` (FIXME: will not work for multi-track events) + auto charged_particle_momentum = thrown_momentum; + auto charged_particle_eta = thrown_eta; + auto charged_particle_pdg = thrown_pdg; + auto charged_particle_mass = Tools::GetPDGMass(charged_particle_pdg); + m_log->trace(" Charged Particle PDG={}, mass={} GeV, p={} GeV from truth", + charged_particle_pdg, charged_particle_mass, charged_particle_momentum); + + // get average Cherenkov angle: `theta_rec` + double theta_rec = 0.0; + for(const auto& [theta,phi] : cherenkov_pid.getThetaPhiPhotons()) + theta_rec += theta; + theta_rec /= cherenkov_pid.getNpe(); + auto theta_rec_mrad = theta_rec * 1e3; // [rad] -> [mrad] + + // calculate expected Cherenkov angle `theta_exp` and residual `theta_resid`, + // using refractive index from MC truth + auto mc_rindex = cherenkov_pid.getRefractiveIndex(); // average refractive index for photons used in this `cherenkov_pid` + auto theta_exp = TMath::ACos( + TMath::Hypot( charged_particle_momentum, charged_particle_mass ) / + ( mc_rindex * charged_particle_momentum ) + ); + auto theta_resid = theta_rec - theta_exp; + auto theta_resid_mrad = theta_resid * 1e3; // [rad] -> [mrad] + + // fill PID histograms + m_npe_dist->Fill( cherenkov_pid.getNpe()); + m_npe_vs_p->Fill( charged_particle_momentum, cherenkov_pid.getNpe()); + m_npe_vs_eta->Fill( charged_particle_eta, cherenkov_pid.getNpe()); + m_theta_dist->Fill( theta_rec_mrad); + m_theta_vs_p->Fill( charged_particle_momentum, theta_rec_mrad); + m_theta_vs_eta->Fill( charged_particle_eta, theta_rec_mrad); + m_thetaResid_dist->Fill( theta_resid_mrad); + m_thetaResid_vs_p->Fill( charged_particle_momentum, theta_resid_mrad); + m_thetaResid_vs_eta->Fill( charged_particle_eta, theta_resid_mrad); + for(const auto& [theta,phi] : cherenkov_pid.getThetaPhiPhotons()) + m_photonTheta_vs_photonPhi->Fill( phi, theta*1e3); // [rad] -> [mrad] + m_mcWavelength_dist->Fill( Tools::HC / cherenkov_pid.getPhotonEnergy() ); // energy [GeV] -> wavelength [nm] + m_mcRindex_dist->Fill( mc_rindex); + + // find the PDG hypothesis with the highest weight + float max_weight = -1000; + int pdg_max_weight = 0; + for(const auto& hyp : cherenkov_pid.getHypotheses()) { + if(hyp.weight > max_weight) { + max_weight = hyp.weight; + pdg_max_weight = hyp.PDG; + } + } + std::string pdg_max_weight_str = "UNKNOWN"; + if(pdg_max_weight!=0 && !std::isnan(pdg_max_weight)) + pdg_max_weight_str = std::to_string(pdg_max_weight); + m_log->trace(" Highest weight is {} for PDG {} (string='{}')", max_weight, pdg_max_weight, pdg_max_weight_str); + m_highestWeight_dist->Fill( pdg_max_weight_str.c_str(), 1); + m_highestWeight_vs_p->Fill( charged_particle_momentum, pdg_max_weight_str.c_str(), 1); + m_highestWeight_vs_eta->Fill( charged_particle_eta, pdg_max_weight_str.c_str(), 1); + + } // end loop over `CherenkovParticleID` objects + } + + + // AlgorithmFinish + //--------------------------------------------------------------------------- + void CherenkovPIDAnalysis::AlgorithmFinish() { + } + +} diff --git a/benchmarks/rich/src/RawHitAnalysis.cc b/benchmarks/rich/src/RawHitAnalysis.cc new file mode 100644 index 0000000000000000000000000000000000000000..d55ac99d7819489c6a408e537e15d2ca7455cd5f --- /dev/null +++ b/benchmarks/rich/src/RawHitAnalysis.cc @@ -0,0 +1,82 @@ +// Copyright 2023, Christopher Dilks +// Subject to the terms in the LICENSE file found in the top-level directory. + +#include "RawHitAnalysis.h" + +namespace benchmarks { + + // AlgorithmInit + //--------------------------------------------------------------------------- + void RawHitAnalysis::AlgorithmInit( + std::shared_ptr<spdlog::logger>& logger + ) + { + m_log = logger; + + // define histograms + m_adc_dist = new TH1D("adc_dist", "ADC Distribution;counts", adc_max, 0, adc_max); + m_tdc_dist = new TH1D("tdc_dist", "TDC Distribution;counts", tdc_max, 0, tdc_max); + m_tdc_vs_adc = new TH2D("tdc_vs_adc", "TDC vs. ADC;ADC counts;TDC counts", + adc_max/2, 0, adc_max/2, + tdc_max/2, 0, tdc_max/2 + ); + m_phot_spectrum = new TH1D( + "phot_spectrum_rec", + "Photon wavelength for digitized hits;#lambda [nm], at emission point", + Tools::wl_bins, Tools::wl_min, Tools::wl_max + ); + m_nhits_dist = new TH1D("nhits_dist", "Number of digitized hits;N_{hits}", + Tools::npe_max, 0, Tools::npe_max + ); + + // format histograms + auto format1D = [] (auto h) { + h->SetLineColor(kBlack); + h->SetFillColor(kBlack); + }; + format1D(m_adc_dist); + format1D(m_tdc_dist); + m_phot_spectrum->SetLineColor(kViolet+2); + m_phot_spectrum->SetFillColor(kViolet+2); + m_nhits_dist->SetLineColor(kCyan+2); + m_nhits_dist->SetFillColor(kCyan+2); + } + + + // AlgorithmProcess + //--------------------------------------------------------------------------- + void RawHitAnalysis::AlgorithmProcess( + const edm4eic::RawTrackerHitCollection& raw_hits, + const edm4eic::MCRecoTrackerHitAssociationCollection& assocs + ) + { + + // fill nhits + m_nhits_dist->Fill(raw_hits.size()); + + // loop over all raw hits (including noise) + for(const auto& raw_hit : raw_hits) { + auto adc = raw_hit.getCharge(); + auto tdc = raw_hit.getTimeStamp(); + m_adc_dist->Fill(adc); + m_tdc_dist->Fill(tdc); + m_tdc_vs_adc->Fill(adc,tdc); + } + + // loop over hits with associations (no noise) + for(const auto& assoc : assocs) { + for(const auto& hit : assoc.getSimHits()) { + auto wavelength = Tools::GetPhotonWavelength(hit, m_log); + if(wavelength>=0) + m_phot_spectrum->Fill(wavelength); + } + } + } + + + // AlgorithmFinish + //--------------------------------------------------------------------------- + void RawHitAnalysis::AlgorithmFinish() { + } + +} diff --git a/benchmarks/rich/src/ReconstructedParticleAnalysis.cc b/benchmarks/rich/src/ReconstructedParticleAnalysis.cc new file mode 100644 index 0000000000000000000000000000000000000000..974551701976825d134d6766dae8c73c46a227f3 --- /dev/null +++ b/benchmarks/rich/src/ReconstructedParticleAnalysis.cc @@ -0,0 +1,98 @@ +// Copyright 2023, Christopher Dilks +// Subject to the terms in the LICENSE file found in the top-level directory. + +#include "ReconstructedParticleAnalysis.h" + +namespace benchmarks { + + // AlgorithmInit + //--------------------------------------------------------------------------- + void ReconstructedParticleAnalysis::AlgorithmInit(std::shared_ptr<spdlog::logger>& logger) { + m_log = logger; + + // initialize plots + m_correct_vs_p_transient = new TH2D("correct_vs_p_transient", "Fraction with Correct PDG vs. Thrown Momentum;p [GeV];fraction", + Tools::momentum_bins, 0, Tools::momentum_max, + 2, 0, 2 + ); + m_correct_vs_p = new TGraphErrors(); + m_correct_vs_p->SetName("correct_vs_p"); + m_correct_vs_p->SetTitle(m_correct_vs_p_transient->GetTitle()); + m_correct_vs_p->GetXaxis()->SetTitle(m_correct_vs_p_transient->GetXaxis()->GetTitle()); + m_correct_vs_p->GetYaxis()->SetTitle(m_correct_vs_p_transient->GetYaxis()->GetTitle()); + m_correct_vs_p->SetMarkerStyle(kFullCircle); + } + + + // AlgorithmProcess + //--------------------------------------------------------------------------- + void ReconstructedParticleAnalysis::AlgorithmProcess( + const edm4eic::MCRecoParticleAssociationCollection& in_assocs + ) + { + if(!in_assocs.isValid()) { m_log->error("invalid input collection 'in_assocs'"); return; } + + // loop over input associations + for(const auto& assoc : in_assocs) { + + // get particles + m_log->trace("{:-^50}"," Particle "); + auto& simpart = assoc.getSim(); + auto& recpart = assoc.getRec(); + if(!recpart.isAvailable()) { m_log->warn("reconstructed particle not available for this association"); continue; } + if(!simpart.isAvailable()) { m_log->warn("simulated particle not available for this association"); continue; } + + // get PDG values + auto simpart_pdg = simpart.getPDG(); // from MC truth + auto recpart_pdg = recpart.getPDG(); // PDG member of ReconstructedParticle + auto recpart_pid_used = recpart.getParticleIDUsed(); + auto pid_pdg = recpart_pid_used.isAvailable() ? recpart_pid_used.getPDG() : 0; + if(!recpart_pid_used.isAvailable()) + m_log->error("reconstructed particle does not have particleIDUsed relation"); + m_log->trace("PDGs: sim.PDG | rec.PDG | PDG from PID = {:^6} | {:^6} | {:^6}", simpart_pdg, recpart_pdg, pid_pdg); + + // check goodness of PID: if near zero, assume PID was not used for this particle and skip + if(recpart.getGoodnessOfPID()<0.01) { + m_log->warn("skip particle with low goodnessOfPID (likely has no PID)"); + continue; + } + + // get momenta + auto simpart_p = edm4hep::utils::p(simpart); + // auto recpart_p = edm4hep::utils::p(recpart); // unused + + // fill plots + m_correct_vs_p_transient->Fill( + simpart_p, + simpart_pdg == pid_pdg ? 1 : 0 + ); + + } // end loop over input associations + + } + + + // AlgorithmFinish + //--------------------------------------------------------------------------- + void ReconstructedParticleAnalysis::AlgorithmFinish() { + + // compute fraction of correct PDG for each x-axis bin + for(int bx=1; bx<=m_correct_vs_p_transient->GetNbinsX(); bx++) { + auto n_wrong = m_correct_vs_p_transient->GetBinContent(bx,1); + auto n_right = m_correct_vs_p_transient->GetBinContent(bx,2); + auto n_total = n_wrong + n_right; + auto momentum = m_correct_vs_p_transient->GetXaxis()->GetBinCenter(bx); + if(n_total>0) { + auto frac = n_right / n_total; + m_correct_vs_p->AddPoint(momentum, frac); + } + } + + // delete transient histograms, so they don't get written + delete m_correct_vs_p_transient; + + // write objects which are not automatically saved + m_correct_vs_p->Write(); + } + +} diff --git a/benchmarks/rich/src/SimHitAnalysis.cc b/benchmarks/rich/src/SimHitAnalysis.cc new file mode 100644 index 0000000000000000000000000000000000000000..f204b5125073a3d5ae3297e49638897979662513 --- /dev/null +++ b/benchmarks/rich/src/SimHitAnalysis.cc @@ -0,0 +1,98 @@ +// Copyright 2023, Christopher Dilks +// Subject to the terms in the LICENSE file found in the top-level directory. + +#include "SimHitAnalysis.h" + +namespace benchmarks { + + // AlgorithmInit + //--------------------------------------------------------------------------- + void SimHitAnalysis::AlgorithmInit( + std::shared_ptr<spdlog::logger>& logger + ) + { + m_log = logger; + + // initialize histograms + m_nphot_dist = new TH1D("nphot_dist", "Number of incident photons;N_{photons}", + Tools::nphot_max, 0, Tools::nphot_max + ); + m_nphot_vs_p = new TH2D("nphot_vs_p", "Number of incident photons vs. Thrown Momentum;p [GeV];N_{photons}", + Tools::momentum_bins, 0, Tools::momentum_max, + Tools::nphot_max, 0, Tools::nphot_max + ); + m_nphot_vs_p__transient = new TH1D("nphot_vs_p__transient", "", + m_nphot_vs_p->GetNbinsX(), + m_nphot_vs_p->GetXaxis()->GetXmin(), + m_nphot_vs_p->GetXaxis()->GetXmax() + ); + m_phot_spectrum = new TH1D( + "phot_spectrum_sim", + "Incident photon wavelength;#lambda [nm], at emission point", + Tools::wl_bins, Tools::wl_min, Tools::wl_max + ); + m_phot_spectrum->SetLineColor(kViolet+2); + m_phot_spectrum->SetFillColor(kViolet+2); + m_nphot_dist->SetLineColor(kCyan+2); + m_nphot_dist->SetFillColor(kCyan+2); + } + + + // AlgorithmProcess + //--------------------------------------------------------------------------- + void SimHitAnalysis::AlgorithmProcess( + const edm4hep::MCParticleCollection& mc_parts, + const edm4hep::SimTrackerHitCollection& sim_hits + ) + { + + // get thrown momentum from a single MCParticle + auto part = std::make_unique<ChargedParticle>(m_log); + part->SetSingleParticle(mc_parts); + auto thrown_momentum = part->GetMomentum(); + + // fill nphot distribution + m_nphot_dist->Fill(sim_hits.size()); + + // loop over hits + for(const auto& sim_hit : sim_hits) { + + // - fill 1D thrown momentum histogram `m_nphot_vs_p__transient` for each (pre-digitized) sensor hit + // FIXME: get thrown momentum from multi-track events; but in this attempt + // below the parent momentum is not consistent; instead for now we use + // `thrown_momentum` from truth above + /* + auto photon = sim_hit.getMCParticle(); + if(photon.parents_size()>0) thrown_momentum = edm4hep::utils::p(photon.getParents(0)); + */ + m_nphot_vs_p__transient->Fill(thrown_momentum); + + // fill photon spectrum + auto wavelength = Tools::GetPhotonWavelength(sim_hit, m_log); + if(wavelength>=0) + m_phot_spectrum->Fill(wavelength); + } + + // use `m_nphot_vs_p__transient` results to fill 2D hist `m_nphot_vs_p` for this event + for(int b=1; b<=m_nphot_vs_p__transient->GetNbinsX(); b++) { + auto nphot = m_nphot_vs_p__transient->GetBinContent(b); + if(nphot>0) { + auto momentum = m_nphot_vs_p__transient->GetBinCenter(b); + m_nphot_vs_p->Fill(momentum,nphot); + } + } + + // clear `m_nphot_vs_p__transient` to be ready for the next event + m_nphot_vs_p__transient->Reset(); + + } + + + // AlgorithmFinish + //--------------------------------------------------------------------------- + void SimHitAnalysis::AlgorithmFinish() { + // delete transient histograms, so they don't get written + delete m_nphot_vs_p__transient; + } + +} diff --git a/benchmarks/rich/src/benchmark.cc b/benchmarks/rich/src/benchmark.cc new file mode 100644 index 0000000000000000000000000000000000000000..d988601f1727dd71d7e6a13548e4aa021b470359 --- /dev/null +++ b/benchmarks/rich/src/benchmark.cc @@ -0,0 +1,299 @@ +// Copyright 2023, Christopher Dilks +// Subject to the terms in the LICENSE file found in the top-level directory. + +#include <functional> +#include <iostream> +#include <unistd.h> +#include <spdlog/spdlog.h> + +#include <TFile.h> + +#include <podio/ROOTFrameReader.h> +#include <podio/Frame.h> + +#include "SimHitAnalysis.h" +#include "RawHitAnalysis.h" +#include "CherenkovPIDAnalysis.h" +#include "ReconstructedParticleAnalysis.h" + +using namespace benchmarks; + +// ------------------------------------------------------------- + +// main logger +std::shared_ptr<spdlog::logger> m_log; + +// get a collection, and check its validity +template<class C> +const C& GetCollection(podio::Frame& frame, std::string name) { + const auto& coll = frame.get<C>(name); + if(!coll.isValid()) + m_log->error("invalid collection '{}'", name); + return coll; +} + +// ------------------------------------------------------------- +// MAIN +// ------------------------------------------------------------- +int main(int argc, char** argv) { + + // ------------------------------------------------------------- + // setup and CLI option parsing + // ------------------------------------------------------------- + + // loggers + m_log = spdlog::default_logger()->clone("benchmark_rich"); + auto log_level = spdlog::level::info; + m_log->set_level(log_level); + + // default options + int verbosity = 0; + long long num_events = 0; + std::string ana_file_name = "out_rich.root"; + std::vector<std::string> rec_files; + std::vector<std::string> analysis_algorithms = { + "SimHit", + "RawHit", + "CherenkovPID", + "ReconstructedParticle" + }; + auto list_algorithms = [&analysis_algorithms] () { + for(auto analysis_algorithm : analysis_algorithms) + std::cout << " " << analysis_algorithm << std::endl; + std::cout << std::endl; + }; + + // usage guide + auto PrintUsage = [&argv, &list_algorithms, ana_file_name] () { + std::cout << "\nUSAGE: " << argv[0] << " -i [INPUT FILES]... [OPTIONS]..." << std::endl + << "\nREQUIRED OPTIONS:\n" + << " -i [INPUT FILES]...\n" + << " input file from reconstruction output;\n" + << " can specify more than one (delimited by space)\n" + << "\nOPTIONAL OPTIONS:\n" + << " -o [OUTPUT FILE]\n" + << " output analysis file (default: " << ana_file_name << ")\n" + << " -n [NUMBER OF EVENTS]\n" + << " number of events to process (default: process all)\n" + << " -v: verbose output\n" + << " -vv: more verbose output\n" + << " -a [ALGORITHMS]...\n" + << " list of analysis algorithms to run (delimited by spaces)\n" + << " default: run all of them\n" + << " available algorithms:\n"; + list_algorithms(); + }; + if(argc==1) { + PrintUsage(); + return 2; + } + + // parse options + int opt; + while( (opt=getopt(argc, argv, "i:o:n:v|a:")) != -1) { + switch(opt) { + case 'i': + optind--; + for(; optind<argc && *argv[optind]!='-'; optind++) + rec_files.push_back(std::string(argv[optind])); + break; + case 'o': + ana_file_name = std::string(optarg); + break; + case 'n': + num_events = std::atoll(optarg); + break; + case 'v': + verbosity++; + break; + case 'a': + analysis_algorithms.clear(); + optind--; + for(; optind<argc && *argv[optind]!='-'; optind++) + analysis_algorithms.push_back(std::string(argv[optind])); + break; + default: + m_log->error("Unknown option '{}'", opt); + PrintUsage(); + return 1; + } + } + + // print options + m_log->info("{:-^50}", " User options "); + m_log->info(" input files:"); + for(auto rec_file : rec_files) + m_log->info(" {}", rec_file); + m_log->info(" output file: {}", ana_file_name); + m_log->info(" algorithms:"); + for(auto analysis_algorithm : analysis_algorithms) + m_log->info(" {}", analysis_algorithm); + m_log->info(" num_events: {}", num_events); + m_log->info(" verbosity: {}", verbosity); + m_log->info("{:-^50}", ""); + + // check options + std::vector<std::string> bad_options; + if(rec_files.size()==0) bad_options.push_back("no [INPUT FILES] have been specified"); + if(analysis_algorithms.size()==0) bad_options.push_back("no [ALGORITHMS] have been specified"); + for(auto bad_option : bad_options) m_log->error(bad_option); + if(bad_options.size()>0) { + PrintUsage(); + return 1; + } + + // set log level + switch(verbosity) { + case 0: log_level = spdlog::level::info; break; + case 1: log_level = spdlog::level::debug; break; + default: log_level = spdlog::level::trace; + } + m_log->set_level(log_level); + + // start the output file + auto ana_file = new TFile(TString(ana_file_name), "RECREATE"); + + // struct to hold an algorithm execution processor + struct AnalysisAlgorithm { + std::shared_ptr<spdlog::logger> log; // algorithm-specific logger + std::function<void(podio::Frame&)> process; // call AlgorithmProcess + std::function<void()> finish; // call AlgorithmFinish + }; + std::vector<AnalysisAlgorithm> algorithm_processors; + + // loop over algorithms to run, and define how to run them + for(auto analysis_algorithm : analysis_algorithms) { + AnalysisAlgorithm algo; + + // algorithm logger + algo.log = spdlog::default_logger()->clone(analysis_algorithm); + algo.log->set_level(log_level); + algo.log->debug("{:-<50}","INITIALIZE "); + + // -------------------------------------------------------------- + // define how to run each algorithm + // -------------------------------------------------------------- + + // truth hits (photons) ......................................... + if(analysis_algorithm == "SimHit") { + auto sim_algo = std::make_shared<SimHitAnalysis>(); + ana_file->mkdir("phot")->cd(); + sim_algo->AlgorithmInit(algo.log); + algo.process = [sim_algo] (podio::Frame& frame) { + sim_algo->AlgorithmProcess( + GetCollection<edm4hep::MCParticleCollection>(frame,"MCParticles"), + GetCollection<edm4hep::SimTrackerHitCollection>(frame,"DRICHHits") + ); + }; + algo.finish = [sim_algo] () { sim_algo->AlgorithmFinish(); }; + } + + // digitizer .................................................... + else if(analysis_algorithm == "RawHit") { + auto digi_algo = std::make_shared<RawHitAnalysis>(); + ana_file->mkdir("digi")->cd(); + digi_algo->AlgorithmInit(algo.log); + algo.process = [digi_algo] (podio::Frame& frame) { + digi_algo->AlgorithmProcess( + GetCollection<edm4eic::RawTrackerHitCollection>(frame,"DRICHRawHits"), + GetCollection<edm4eic::MCRecoTrackerHitAssociationCollection>(frame,"DRICHRawHitsAssociations") + ); + }; + algo.finish = [digi_algo] () { digi_algo->AlgorithmFinish(); }; + } + + // IRT .......................................................... + else if(analysis_algorithm == "CherenkovPID") { + std::vector<std::string> radiators = { "Aerogel", "Gas", "Merged" }; + std::map<std::string, std::shared_ptr<CherenkovPIDAnalysis>> irt_algos; + for(auto radiator : radiators) + irt_algos.insert({radiator, std::make_shared<CherenkovPIDAnalysis>()}); + + for(auto& [radiator, irt_algo] : irt_algos) { + ana_file->mkdir(("pid"+radiator).c_str())->cd(); + irt_algo->AlgorithmInit(radiator, algo.log); + } + + algo.process = [irt_algos] (podio::Frame& frame) { + const auto& mc_parts = GetCollection<edm4hep::MCParticleCollection>(frame,"MCParticles"); + for(auto& [radiator, irt_algo] : irt_algos) { + const auto& cherenkov_pids = GetCollection<edm4eic::CherenkovParticleIDCollection>( + frame, + "DRICH" + radiator + "IrtCherenkovParticleID" + ); + irt_algo->AlgorithmProcess(mc_parts, cherenkov_pids); + } + }; + + algo.finish = [irt_algos] () { + for(auto& [radiator, irt_algo] : irt_algos) + irt_algo->AlgorithmFinish(); + }; + + } + + // linking to reconstructed particles ........................... + else if(analysis_algorithm == "ReconstructedParticle") { + auto link_algo = std::make_shared<ReconstructedParticleAnalysis>(); + ana_file->mkdir("link")->cd(); + link_algo->AlgorithmInit(algo.log); + algo.process = [link_algo] (podio::Frame& frame) { + link_algo->AlgorithmProcess( + GetCollection<edm4eic::MCRecoParticleAssociationCollection>( + frame, + "ReconstructedChargedParticleAssociations" + ) + ); + }; + algo.finish = [link_algo] () { link_algo->AlgorithmFinish(); }; + } + + // unknown algorithm ............................................ + else { + m_log->error("Unknown [ALGORITHM] '{}'; ignoring", analysis_algorithm); + continue; + } + + // -------------------------------------------------------------- + + algorithm_processors.push_back(algo); + } + + // ------------------------------------------------------------- + // read the input file and run all algorithms + // ------------------------------------------------------------- + + // open the input files + podio::ROOTFrameReader podioReader; + m_log->warn("podio::ROOTFrameReader cannot yet support multiple files; reading only the first"); + // podioReader.openFiles(rec_files); + podioReader.openFile(rec_files.front()); + + // get the number of events to process + const std::string tree_name = "events"; + long long num_entries = podioReader.getEntries(tree_name); + if(num_events>0) num_entries = std::min(num_events, num_entries); + + // event loop + m_log->info("BEGIN EVENT LOOP"); + for(long long e=0; e<num_entries; e++) { + if(e%100==0) m_log->info("Progress: {:.3f}%", 100.0 * e/num_entries); + auto podioFrame = podio::Frame(podioReader.readNextEntry(tree_name)); + for(auto& algo : algorithm_processors) { + algo.log->debug("{:-<50}","PROCESS EVENT "); + algo.process(podioFrame); + } + } + + // finish + for(auto& algo : algorithm_processors) { + algo.log->debug("{:-<50}","FINISH "); + algo.finish(); + } + + // write output + ana_file->Write(); + ana_file->Close(); + m_log->info("Done. Wrote analysis file:"); + m_log->info(" {}", ana_file_name); +}