diff --git a/CMakeLists.txt b/CMakeLists.txt index 614d004da..0bb87083c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,7 @@ find_package(ifdhc REQUIRED ) find_package(ifdh_art REQUIRED ) find_package(Range-v3 REQUIRED ) find_package(sbnobj REQUIRED ) +find_package(sbnalg REQUIRED ) find_package(icarus_signal_processing REQUIRED ) find_package(icarusalg REQUIRED ) find_package(icarusutil REQUIRED ) diff --git a/fcl/SkipTesting.txt b/fcl/SkipTesting.txt index 9283fe989..68c8a793b 100644 --- a/fcl/SkipTesting.txt +++ b/fcl/SkipTesting.txt @@ -18,6 +18,8 @@ services_compat_no_overburden_icarus_v3.fcl services_compat_overburden_icarus_v3.fcl set_caf_calconst.fcl +override_services_for_simulation.fcl + ################################################################################ # broken, won't fix diff --git a/fcl/caf/cafmakerjob_icarus.fcl b/fcl/caf/cafmakerjob_icarus.fcl index 29f91b7d6..115ffea75 100644 --- a/fcl/caf/cafmakerjob_icarus.fcl +++ b/fcl/caf/cafmakerjob_icarus.fcl @@ -22,6 +22,8 @@ services: SpaceChargeService: @local::icarus_spacecharge } +#include "override_services_for_simulation.fcl" + outputs: { outpid: diff --git a/fcl/detsim/CMakeLists.txt b/fcl/detsim/CMakeLists.txt index f86d7dfc8..88708a770 100644 --- a/fcl/detsim/CMakeLists.txt +++ b/fcl/detsim/CMakeLists.txt @@ -1,5 +1,6 @@ # add underlying subfolders add_subdirectory(SBNNoise) +add_subdirectory(partial) add_subdirectory(commissioning) add_subdirectory(archive) diff --git a/fcl/detsim/commissioning/run4642like_detsim_icarus.fcl b/fcl/detsim/commissioning/run4642like_detsim_icarus.fcl index 9c24494ac..7ce90cf49 100644 --- a/fcl/detsim/commissioning/run4642like_detsim_icarus.fcl +++ b/fcl/detsim/commissioning/run4642like_detsim_icarus.fcl @@ -25,7 +25,7 @@ # #include "services_common_icarus.fcl" -#include "opdetsim_pmt_icarus.fcl" +#include "pmtsimulation_icarus.fcl" #include "rootoutput_icarus.fcl" process_name: DetSim4642 @@ -38,7 +38,7 @@ services: @local::icarus_common_services # from services_common_icarus.fcl physics: { producers: { opdaq: { - @table::icarus_simpmt_noise # from opdetsim_pmt_icarus.fcl + @table::icarus_pmtsimulationalg_202202_noise # from pmtsimulation_icarus.fcl # # enabled readout window diff --git a/fcl/detsim/commissioning/run4759like_detsim_icarus.fcl b/fcl/detsim/commissioning/run4759like_detsim_icarus.fcl index e1db032bb..3cec8c4f1 100644 --- a/fcl/detsim/commissioning/run4759like_detsim_icarus.fcl +++ b/fcl/detsim/commissioning/run4759like_detsim_icarus.fcl @@ -25,7 +25,7 @@ # #include "services_common_icarus.fcl" -#include "opdetsim_pmt_icarus.fcl" +#include "pmtsimulation_icarus.fcl" #include "rootoutput_icarus.fcl" process_name: DetSim4759 @@ -38,7 +38,7 @@ services: @local::icarus_common_services # from services_common_icarus.fcl physics: { producers: { opdaq: { - @table::icarus_simpmt_noise # from opdetsim_pmt_icarus.fcl + @table::icarus_pmtsimulationalg_202202_noise # from pmtsimulation_icarus.fcl # # enabled readout window diff --git a/fcl/detsim/detsim_1d_icarus.fcl b/fcl/detsim/detsim_1d_icarus.fcl index e710624e1..81e364794 100644 --- a/fcl/detsim/detsim_1d_icarus.fcl +++ b/fcl/detsim/detsim_1d_icarus.fcl @@ -99,3 +99,6 @@ services.Geometry.GDML: "icarus_complete_20220518_overburden.gdml" services.Geometry.ROOT: "icarus_complete_20220518_overburden.gdml" physics.producers.crtdaq.G4ModuleLabel: "largeant" physics.producers.opdaq.InputModule: "largeant" +physics.producers.opdaq.ApplyTimingDelays: false # keep false for non-overlay MC +physics.producers.opdaq.UseGainDatabase: false # keep false for non-overlay MC +physics.producers.opdaq.UseChannelStatusDatabase: false # keep false for non-overlay MC diff --git a/fcl/detsim/detsim_2d_icarus.fcl b/fcl/detsim/detsim_2d_icarus.fcl index e4394bd1d..4fe9e6a6b 100644 --- a/fcl/detsim/detsim_2d_icarus.fcl +++ b/fcl/detsim/detsim_2d_icarus.fcl @@ -55,3 +55,6 @@ outputs: { services.Geometry.GDML: "icarus_complete_20220518_overburden.gdml" services.Geometry.ROOT: "icarus_complete_20220518_overburden.gdml" physics.producers.crtdaq.G4ModuleLabel: "shifted" +physics.producers.opdaq.ApplyTimingDelays: false # keep false for non-overlay MC +physics.producers.opdaq.UseGainDatabase: false # keep false for non-overlay MC +physics.producers.opdaq.UseChannelStatusDatabase: false # keep false for non-overlay MC diff --git a/fcl/detsim/detsim_2d_icarus_refactored.fcl b/fcl/detsim/detsim_2d_icarus_refactored.fcl index 799170d15..0e026fb70 100644 --- a/fcl/detsim/detsim_2d_icarus_refactored.fcl +++ b/fcl/detsim/detsim_2d_icarus_refactored.fcl @@ -53,5 +53,8 @@ outputs: { physics.producers.crtdaq.G4ModuleLabel: "shifted" physics.producers.opdaq.InputModule: "pdfastsim" +physics.producers.opdaq.ApplyTimingDelays: false # keep false for non-overlay MC +physics.producers.opdaq.UseGainDatabase: false # keep false for non-overlay MC +physics.producers.opdaq.UseChannelStatusDatabase: false # keep false for non-overlay MC physics.producers.shifted.InitAuxDetSimChannelLabel: "genericcrt" physics.producers.shifted.InitSimPhotonsLabel: "pdfastsim" diff --git a/fcl/detsim/detsim_2d_icarus_refactored_Run4.fcl b/fcl/detsim/detsim_2d_icarus_refactored_Run4.fcl new file mode 100644 index 000000000..e1bbabd1c --- /dev/null +++ b/fcl/detsim/detsim_2d_icarus_refactored_Run4.fcl @@ -0,0 +1,7 @@ +#include "detsim_2d_icarus_refactored.fcl" + +# Run3/4 optical tune +physics.producers.opdaq: @local::icarus_simpmt_run4 +physics.producers.opdaq.ApplyTimingDelays: false # keep false for non-overlay MC +physics.producers.opdaq.UseGainDatabase: false # keep false for non-overlay MC +physics.producers.opdaq.UseChannelStatusDatabase: false # keep false for non-overlay MC diff --git a/fcl/detsim/detsim_2d_icarus_refactored_overlay.fcl b/fcl/detsim/detsim_2d_icarus_refactored_overlay.fcl index bf9754683..cb2b2b80a 100644 --- a/fcl/detsim/detsim_2d_icarus_refactored_overlay.fcl +++ b/fcl/detsim/detsim_2d_icarus_refactored_overlay.fcl @@ -1,3 +1,10 @@ #include "detsim_2d_icarus_refactored.fcl" physics.producers.daq: @local::icarus_simwire_wirecell_shifted_overlay + +# turn off mc noise on pmt waveforms +physics.producers.opdaq: @local::icarus_simpmt_nonoise +physics.producers.opdaq.ApplyTimingDelays: true +physics.producers.opdaq.UseGainDatabase: true +physics.producers.opdaq.UseChannelStatusDatabase: true + diff --git a/fcl/detsim/detsim_2d_icarus_refactored_overlay_Run4.fcl b/fcl/detsim/detsim_2d_icarus_refactored_overlay_Run4.fcl new file mode 100644 index 000000000..26f416910 --- /dev/null +++ b/fcl/detsim/detsim_2d_icarus_refactored_overlay_Run4.fcl @@ -0,0 +1,7 @@ +#include "detsim_2d_icarus_refactored_overlay.fcl" + +# Run3/4 optical tune (with no noise) +physics.producers.opdaq: @local::icarus_simpmt_run4_nonoise +physics.producers.opdaq.ApplyTimingDelays: true +physics.producers.opdaq.UseGainDatabase: true +physics.producers.opdaq.UseChannelStatusDatabase: true \ No newline at end of file diff --git a/fcl/detsim/detsim_2d_icarus_refactored_yzsim.fcl b/fcl/detsim/detsim_2d_icarus_refactored_yzsim.fcl index d1f908bfc..23ce1d8f6 100644 --- a/fcl/detsim/detsim_2d_icarus_refactored_yzsim.fcl +++ b/fcl/detsim/detsim_2d_icarus_refactored_yzsim.fcl @@ -62,6 +62,9 @@ outputs: { physics.producers.crtdaq.G4ModuleLabel: "shifted" physics.producers.opdaq.InputModule: "pdfastsim" +physics.producers.opdaq.ApplyTimingDelays: false # keep false for non-overlay MC +physics.producers.opdaq.UseGainDatabase: false # keep false for non-overlay MC +physics.producers.opdaq.UseChannelStatusDatabase: false # keep false for non-overlay MC physics.producers.shifted.InitAuxDetSimChannelLabel: "genericcrt" physics.producers.shifted.InitSimPhotonsLabel: "pdfastsim" physics.producers.filtersed.InitSimEnergyDepositLabel: "shifted" diff --git a/fcl/detsim/detsim_2d_icarus_refactored_yzsim_Run4.fcl b/fcl/detsim/detsim_2d_icarus_refactored_yzsim_Run4.fcl index f2a5339a3..8980ce32a 100644 --- a/fcl/detsim/detsim_2d_icarus_refactored_yzsim_Run4.fcl +++ b/fcl/detsim/detsim_2d_icarus_refactored_yzsim_Run4.fcl @@ -1,3 +1,9 @@ #include "detsim_2d_icarus_refactored_yzsim.fcl" physics.producers.daq.wcls_main.params.YZScaleMapJson: "yzmap_gain_icarus_v4_run4.json" + +# Run3/4 optical tune +physics.producers.opdaq: @local::icarus_simpmt_run4 +physics.producers.opdaq.ApplyTimingDelays: false # keep false for non-overlay MC +physics.producers.opdaq.UseGainDatabase: false # keep false for non-overlay MC +physics.producers.opdaq.UseChannelStatusDatabase: false # keep false for non-overlay MC diff --git a/fcl/detsim/detsim_2d_icarus_refactored_yzsim_notrigger.fcl b/fcl/detsim/detsim_2d_icarus_refactored_yzsim_notrigger.fcl index 0c53caa7f..d31d057c3 100644 --- a/fcl/detsim/detsim_2d_icarus_refactored_yzsim_notrigger.fcl +++ b/fcl/detsim/detsim_2d_icarus_refactored_yzsim_notrigger.fcl @@ -57,3 +57,6 @@ outputs: { } physics.producers.opdaq.InputModule: "pdfastsim" +physics.producers.opdaq.ApplyTimingDelays: false # keep false for non-overlay MC +physics.producers.opdaq.UseGainDatabase: false # keep false for non-overlay MC +physics.producers.opdaq.UseChannelStatusDatabase: false # keep false for non-overlay MC diff --git a/fcl/detsim/detsim_2d_icarus_refactored_yzsim_notrigger_overlay.fcl b/fcl/detsim/detsim_2d_icarus_refactored_yzsim_notrigger_overlay.fcl index 74147585c..4725fb200 100644 --- a/fcl/detsim/detsim_2d_icarus_refactored_yzsim_notrigger_overlay.fcl +++ b/fcl/detsim/detsim_2d_icarus_refactored_yzsim_notrigger_overlay.fcl @@ -1,3 +1,9 @@ #include "detsim_2d_icarus_refactored_yzsim_notrigger.fcl" physics.producers.daq: @local::icarus_simwire_wirecell_yz_overlay + +# turn off mc noise on pmt waveforms +physics.producers.opdaq: @local::icarus_simpmt_nonoise +physics.producers.opdaq.ApplyTimingDelays: true +physics.producers.opdaq.UseGainDatabase: true +physics.producers.opdaq.UseChannelStatusDatabase: true \ No newline at end of file diff --git a/fcl/detsim/detsim_2d_icarus_refactored_yzsim_overlay.fcl b/fcl/detsim/detsim_2d_icarus_refactored_yzsim_overlay.fcl index ed2f25196..d621e63d7 100644 --- a/fcl/detsim/detsim_2d_icarus_refactored_yzsim_overlay.fcl +++ b/fcl/detsim/detsim_2d_icarus_refactored_yzsim_overlay.fcl @@ -1,3 +1,9 @@ #include "detsim_2d_icarus_refactored_yzsim.fcl" physics.producers.daq: @local::icarus_simwire_wirecell_yz_overlay + +# turn off mc noise on pmt waveforms +physics.producers.opdaq: @local::icarus_simpmt_nonoise +physics.producers.opdaq.ApplyTimingDelays: true +physics.producers.opdaq.UseGainDatabase: true +physics.producers.opdaq.UseChannelStatusDatabase: true \ No newline at end of file diff --git a/fcl/detsim/detsim_2d_icarus_refactored_yzsim_overlay_Run4.fcl b/fcl/detsim/detsim_2d_icarus_refactored_yzsim_overlay_Run4.fcl index 92158743e..6673bbc90 100644 --- a/fcl/detsim/detsim_2d_icarus_refactored_yzsim_overlay_Run4.fcl +++ b/fcl/detsim/detsim_2d_icarus_refactored_yzsim_overlay_Run4.fcl @@ -1,3 +1,9 @@ #include "detsim_2d_icarus_refactored_yzsim_overlay.fcl" physics.producers.daq.wcls_main.params.YZScaleMapJson: "yzmap_gain_icarus_v4_run4.json" + +# Run3/4 optical tune (with no noise) +physics.producers.opdaq: @local::icarus_simpmt_run4_nonoise +physics.producers.opdaq.ApplyTimingDelays: true +physics.producers.opdaq.UseGainDatabase: true +physics.producers.opdaq.UseChannelStatusDatabase: true \ No newline at end of file diff --git a/fcl/detsim/partial/CMakeLists.txt b/fcl/detsim/partial/CMakeLists.txt new file mode 100644 index 000000000..6ce4b7b55 --- /dev/null +++ b/fcl/detsim/partial/CMakeLists.txt @@ -0,0 +1,8 @@ +# Install fcl files in /job subdirectory. +install_fhicl() + +# Also put a copy in the source tree. + +FILE(GLOB fcl_files *.fcl) +install_source( EXTRAS ${fcl_files} ) + diff --git a/fcl/detsim/partial/detsim_opdetonly_icarus.fcl b/fcl/detsim/partial/detsim_opdetonly_icarus.fcl new file mode 100644 index 000000000..26e1da570 --- /dev/null +++ b/fcl/detsim/partial/detsim_opdetonly_icarus.fcl @@ -0,0 +1,40 @@ +# +# File: detsim_opdetonly_icarus.fcl +# Purpose: Simulation of the optical detector response. +# Date: January 27, 2025 +# Author: Gianluca Petrillo (petrillo@slac.stanford.edu) +# +# This job configuration runs the standard detector simulation, like +# `detsim_1d_icarus.fcl` does, but only for the optical detector. +# +# +# Output +# ------- +# +# * `std::vector` (`opdaq`): raw PMT waveforms +# +# +# +# +# Changes +# -------- +# +# 20250127 (petrillo@slac.stanford.edu) [v1.0] +# original version (from `detsim_1d_icarus.fcl`) +# + +#include "detsim_1d_icarus.fcl" + +process_name: OpDetSim + +services.DetPedestalService: @erase +services.ChannelStatusService: @erase +services.IICARUSChannelMap: @erase +services.SignalShapingICARUSService: @erase + +# run only the `opdaq` module: +physics.producers: { + opdaq: @local::physics.producers.opdaq # simplify configuration removing all other producers +} +physics.simulate: [ opdaq ] + diff --git a/fcl/detsim/partial/detsim_opdetonly_icarus_overlay_reprocessing_run2.fcl b/fcl/detsim/partial/detsim_opdetonly_icarus_overlay_reprocessing_run2.fcl new file mode 100644 index 000000000..e9a395661 --- /dev/null +++ b/fcl/detsim/partial/detsim_opdetonly_icarus_overlay_reprocessing_run2.fcl @@ -0,0 +1,150 @@ +## File: stage0_run2_opdetonly_icarus_data_reprocessing.fcl +## Purpose: Reprocessing of the optical products for the 2025 SBN "spring" OVERLAY MC production + +#include "detsim_2d_icarus_refactored.fcl" + +## This job selectively re-runs only the PMT/CRT DetSim path on top of an existing file. +## This is has been put together only for the post-processing of the 2025 SBN MC OVERLAY +## productions, and assumes to be starting with its mcstage1 files. +## It is valid only for RUN-2 productions because it selects the Run2 DetSim tune. + +process_name: OpDetSim + +services.DetPedestalService: @erase +services.ChannelStatusService: @erase +services.SignalShapingICARUSService: @erase + +source.inputCommands: [ + "keep *", + + # `stage0` comes from decoding data: should be kept! + # `MCstage0` comes running stage0 on the overlay products: + # so pmt/trigger/crt stuff should be thrown out! + # note: I would really like to drop these things as well: + # "drop raw::OpDetWaveform*_opdaq_*_*", # drop old MC waveforms (if any) + # "drop *_pmtfixedthrinit_*_*", # drop triggersim product + # "drop *_pmtlvdsgatesinit_*_*", # drop triggersim product + # "drop *_pmttriggerwindowsinit_*_*", # drop triggersim product + # "drop *_triggersimgatesinit_*_*", # drop triggersim product + # "drop *_emuTriggerUnshifted_*_*", # drop triggersim product + # but I can't: it triggers art genocidal insticts thus removing all + # downstream products in the dependency tree that we still need. + + "drop raw::OpDetWaveform*_shifted_*_*", # drop old MC (shifted) waveforms (if any) + "drop *_overlayOpWaveforms_*_*", # drop old Overlay waveforms (if any) + "drop *_ophit*_*_*", # drop MCstage0 ophits + "drop *_mcophit_*_*", # drop MCstageo mcophit + "drop *_opflash*_*_*", # drop MCsstage0 east/west opflashes + + "drop *_pmtfixedthr_*_*", # drop MCstage0 trigger product + "drop *_pmtlvdsgates_*_*", # drop MCstage0 trigger product + "drop *_pmtlvdsgates_*_*", # drop MCstage0 trigger product + "drop *_pmtbaselines_*_MCstage0", # drop MCstage0 baselines, not stage0! + "drop *_pmttriggerwindows_*_*", # drop MCstage0 trigger product + "drop *_emuTrigger_*_*", # drop MCstage0 trigger product + + "drop *_crtdaq_*_*", # drop CRT detsim product + "drop *_mccrthit_*_*", # drop CRT mc hits + "drop *_overlayCRTHit_*_*", # drop CRT overlay hits + "drop *_crttrack_*_*", # drop CRT tracks (MCstage0) + "drop *_crtpmt_*_*" # drop CRTPMT matches (MCstage0) +] + +# Running only the optical/trigger/crt modules +# Simplify configuration removing all other producers (tpc) + +physics.producers: { + + # opdaq Run-1/2 tune for overlays (no noise) + opdaq: @local::icarus_simpmt_run2_nonoise + + # triggersim producers + @table::icarus_shifting_triggersim.producers + + # crt hits + crtdaq: @local::icarus_crtsim + +} + +# since some "old" products need to be kept in the tree +# we have products with the same tag, and different process_name +# make sure to select exactly what you need! + +# build waveforms from simPhotons (already shifted) +physics.producers.opdaq.InputModule: "shifted::DetSim" +physics.producers.opdaq.ApplyTimingDelays: true +physics.producers.opdaq.UseGainDatabase: true +physics.producers.opdaq.UseChannelStatusDatabase: true + +# make sure the triggersim chain used the "new" products +# instead of the "old" ones which I cannot drop on input +physics.producers.pmtfixedthrinit.OpticalWaveforms: "opdaq::OpDetSim" # this was re-generated with same tag +physics.producers.pmtlvdsgatesinit.TriggerGatesTag: "pmtfixedthrinit::OpDetSim" # this was re-generated with same tag +physics.producers.pmttriggerwindowsinit.TriggerGatesTag: "pmtlvdsgatesinit::OpDetSim" # this was re-generated with same tag +physics.producers.triggersimgatesinit.BeamGateTag: "beamgate" # keep using old Gen product +physics.producers.emuTriggerUnshifted.BeamGates: "triggersimgatesinit::OpDetSim" # this was re-generated with same tag +physics.producers.emuTriggerUnshifted.TriggerGatesTag: "pmttriggerwindowsinit::OpDetSim" # this was re-generated with same tag + +# need to update the shifting module to pick up new labels +# some were regenerated (tag still good), others were previously shifted +# applying a second shift is okay: everything is consistent +# (eg. waveforms made from `shifted` photons) +# NOTE: still shifing energy deposits although tpc info not regenerated +# however forced to use `filtersed` as `shifted` has been consumed + +physics.producers.shifted.InputTriggerLabel: "emuTriggerUnshifted::OpDetSim" # this was re-generated with same tag +physics.producers.shifted.InitAuxDetSimChannelLabel: "shifted::DetSim" # need to start from previously-shifted, not `genericcrt` +physics.producers.shifted.InitBeamGateInfoLabel: "triggersimgatesinit::OpDetSim" # this was re-generated with same tag +physics.producers.shifted.InitSimEnergyDepositLabel: "filtersed::DetSim" # need to start from previously-shifted, but `shifted` no longer available +physics.producers.shifted.InitSimEnergyDepositLiteLabel: "filtersed::DetSim" # need to start from previously-shifted, but `shifted` no longer available +physics.producers.shifted.InitSimPhotonsLabel: "shifted::DetSim" # need to start from previously-shifted, not `pdfastsim` +physics.producers.shifted.InitWaveformLabel: "opdaq::OpDetSim" # this was re-generated with same tag + +# need to update shiting of priorSCE deposits: +# this is important if future reprocessings will need to regenerate SimPhotons + +physics.producers.shiftedpriorSCE.InputTriggerLabel: "emuTriggerUnshifted::OpDetSim" # this was re-generated with same tag +physics.producers.shiftedpriorSCE.InitSimEnergyDepositLabel: "shiftedpriorSCE::DetSim" # need to start from previously-shifted, not `ionization:priorSCE` + +# and finally make sure the crtsim also uses the newly shifted products +physics.producers.crtsim.G4ModuleLabel: "shifted::OpDetSim" + +# The reprocessing flow is the following: +# - build new pmt waveforms from photons +# - simulate the trigger +# - shift all products to the trigger: +# -- we are shifting photons, pmt waveforms, energy depositis, aux sim channels +# -- note: since tpc is not re-simulated, no longer fully coherent with trigger time +# - build CRT data from aux sim channels + +physics.simulate: [ opdaq, @sequence::icarus_shifting_triggersim.path, crtdaq ] + + +# drop duplicate produtcs in output +# this doesn't trigger art genocidal instincts + +outputs.rootoutput.outputCommands: [ + "keep *", + + # drop `opdaq`, but keep `shifted` for overlaying + "drop *_opdaq_*_*", + + # drop old trigger products now that new ones exist + # specify the old process name "DetSim" + "drop *_pmtfixedthrinit_*_DetSim", + "drop *_pmtlvdsgatesinit_*_DetSim", + "drop *_pmttriggerwindowsinit_*_DetSim", + "drop *_triggersimgatesinit_*_DetSim", + "drop *_emuTriggerUnshifted_*_DetSim", + + # drop old "shifted" products since we shifted things again + "drop sim::SimEnergyDeposits_filtersed_*_DetSim", # this has been regenerated into `shifted` + # mcreco looks for `filtersed` in stage1: potentially can be changed to look for `shifted`? + # however since tpc reco products are not shifted, better to keep old deposits for that matching + # "drop sim::SimEnergyDepositLites_filtersed_*_DetSim", # this has been regenerated into `shifted` + "drop sim::SimEnergyDeposits_shiftedpriorSCE_*_DetSim", # this has been regenerated into the same tag + "drop sim::AuxDetSimChannel_shifted_*_DetSim", # this has been regenerated into the same tag + "drop sim::SimPhotons_shifted_*_DetSim", # this has been regenerated into the same tag + "drop sim::BeamGateInfo_shifted_*_DetSim" # this has been regenerated into the same tag + +] diff --git a/fcl/detsim/partial/detsim_opdetonly_icarus_overlay_reprocessing_run4.fcl b/fcl/detsim/partial/detsim_opdetonly_icarus_overlay_reprocessing_run4.fcl new file mode 100644 index 000000000..4c8e0ac01 --- /dev/null +++ b/fcl/detsim/partial/detsim_opdetonly_icarus_overlay_reprocessing_run4.fcl @@ -0,0 +1,16 @@ +## File: stage0_run4_opdetonly_icarus_data_reprocessing.fcl +## Purpose: Reprocessing of the optical products for the 2025 SBN "spring" OVERLAY MC production + +#include "detsim_opdetonly_icarus_overlay_reprocessing_run2.fcl" + +## This job selectively re-runs only the PMT/CRT DetSim path on top of an existing file. +## This is has been put together only for the post-processing of the 2025 SBN MC OVERLAY +## productions, and assumes to be starting with its mcstage1 files. +## It is valid only for RUN-3/RUN-4 productions because it selects their common DetSim tune. + +# switch to Run3/Run-4 tune +physics.producers.opdaq: @local::icarus_simpmt_run4_nonoise +physics.producers.opdaq.InputModule: "shifted::DetSim" +physics.producers.opdaq.ApplyTimingDelays: true +physics.producers.opdaq.UseGainDatabase: true +physics.producers.opdaq.UseChannelStatusDatabase: true \ No newline at end of file diff --git a/fcl/g4/PDFastSim_icarus.fcl b/fcl/g4/PDFastSim_icarus.fcl index 1344fa340..d11b2c322 100644 --- a/fcl/g4/PDFastSim_icarus.fcl +++ b/fcl/g4/PDFastSim_icarus.fcl @@ -157,7 +157,7 @@ icarus_vis_timing_parameterization: # standard configuration icarus_pdfastsim_pvs: @local::standard_pdfastsim_pvs -icarus_pdfastsim_pvs.SimulationLabel: "ionization" +icarus_pdfastsim_pvs.SimulationLabel: "ionization:priorSCE" icarus_pdfastsim_pvs.IncludePropTime: true icarus_pdfastsim_pvs.ScintTimeTool.SlowDecayTime: 1300.0 @@ -166,4 +166,4 @@ icarus_pdfastsim_pvs.VUVTiming: @local::photon_propagation_timing_icarus icarus_pdfastsim_pvs.VISTiming: @local::icarus_vis_timing_parameterization -END_PROLOG \ No newline at end of file +END_PROLOG diff --git a/fcl/gen/genie/genie_icarus_bnb.fcl b/fcl/gen/genie/genie_icarus_bnb.fcl index aa478ad5b..05a862a22 100644 --- a/fcl/gen/genie/genie_icarus_bnb.fcl +++ b/fcl/gen/genie/genie_icarus_bnb.fcl @@ -30,8 +30,7 @@ # # -#include "genie.fcl" -#include "beamspilltimings.fcl" +#include "genie_beam_settings.fcl" BEGIN_PROLOG @@ -45,30 +44,44 @@ BEGIN_PROLOG ### ### icarus_genie_BNB_base ### +# +# Based on SBN shared BNB configuration, it adds: +# * flux +# * simulation volume: detector enclosure (includes both cryostats) +# * event dump in the log +# +# icarus_genie_BNB_base: { + + @table::sbn_genie_BNB_base # from genie_beam_settings.fcl - @table::standard_genie # from genie.fcl - + # + # flux + # FluxType: "simple_flux" - GenFlavors: [12, 14, -12, -14] -# TopVolume: "volTPCActive" -# TopVolume: "volCryostat" - BeamName: "booster" - EventGeneratorList: "Default" # FluxCopyMethod: "IFDH" FluxSearchPaths: "/cvmfs/sbn.osgstorage.org/pnfs/fnal.gov/usr/sbn/persistent/stash/physics/beam/GENIE/BNB/standard/v01_00/" FluxFiles: ["converted_beammc_icarus_*.root"] - GHepPrintLevel: 13 - POTPerSpill: 5.0e12 - EventsPerSpill: @erase + - GlobalTimeOffset: 0.0 # [ns] - RandomTimeOffset: 1600.0 # [ns] + # + # generation volume + # +# TopVolume: "volTPCActive" +# TopVolume: "volCryostat" + TopVolume: "volDetEnclosure" + + # + # GENIE event vdump verbosity + # + GHepPrintLevel: 13 } # icarus_genie_BNB_base +# # legacy alias +# icarus_genie_simple: @local::icarus_genie_BNB_base @@ -76,15 +89,12 @@ icarus_genie_simple: @local::icarus_genie_BNB_base ### icarus_genie_BNB ### # -# Adds BnB time structure. +# Just as the default. # icarus_genie_BNB: { @table::icarus_genie_BNB_base - SpillTimeConfig: @local::FNAL_BnB_default # from beamspilltimings.fcl - RandomTimeOffset: 0.0 # ns; it's ignored anyway when `SpillTimeConfig` is set - } # icarus_genie_BNB diff --git a/fcl/gen/numi/genie_icarus_numioffaxis.fcl b/fcl/gen/numi/genie_icarus_numioffaxis.fcl index e42321bbe..e923d90a8 100644 --- a/fcl/gen/numi/genie_icarus_numioffaxis.fcl +++ b/fcl/gen/numi/genie_icarus_numioffaxis.fcl @@ -31,7 +31,7 @@ # updated NuMI POT to 6+6 batches configuration # -#include "genie_icarus_bnb.fcl" +#include "genie_beam_settings.fcl" #include "beamspilltimings.fcl" @@ -49,7 +49,7 @@ BEGIN_PROLOG ### icarus_genie_NuMI_base: { - @table::icarus_genie_BNB_base # from genie_icarus_bnb.fcl + @table::sbn_genie_NuMI_base # from genie_beam_settings.fcl # # flux @@ -60,10 +60,18 @@ icarus_genie_NuMI_base: { FluxSearchPaths: "/cvmfs/sbn.osgstorage.org/pnfs/fnal.gov/usr/sbn/persistent/stash/physics/beam/GENIE/NuMI/standard/v03_02/" FluxFiles: ["g4numi*.root"] - POTPerSpill: 6.0e13 # same as BnB per batch, but 6 batches in spill - - RandomTimeOffset: 9600.0 # same batch as BnB, but 6 batches in spill [ns] + # + # generation volume + # +# TopVolume: "volTPCActive" +# TopVolume: "volCryostat" + TopVolume: "volDetEnclosure" + # + # GENIE event vdump verbosity + # + GHepPrintLevel: 13 + } # icarus_genie_NuMI_base @@ -75,15 +83,12 @@ icarus_genienumi_simple: @local::icarus_genie_NuMI_base ### icarus_genie_NuMI ### # -# Adds NuMI beam time structure. +# Just as the default. # icarus_genie_NuMI: { @table::icarus_genie_NuMI_base - SpillTimeConfig: @local::FNAL_NuMI_default # from beamspilltimings.fcl - RandomTimeOffset: 0.0 # ns; it's ignored anyway when `SpillTimeConfig` is set - } # icarus_genie_NuMI # Rock config diff --git a/fcl/overlays/CMakeLists.txt b/fcl/overlays/CMakeLists.txt index 520ed808b..3e60f0520 100644 --- a/fcl/overlays/CMakeLists.txt +++ b/fcl/overlays/CMakeLists.txt @@ -1,4 +1,7 @@ -# Install fcl files in /job subdirectory. +# add subdirectories +add_subdirectory(partial) + +# Install fcl files install_fhicl() diff --git a/fcl/overlays/overlay_waveforms.fcl b/fcl/overlays/overlay_waveforms.fcl index 48714f826..52a71f0b8 100644 --- a/fcl/overlays/overlay_waveforms.fcl +++ b/fcl/overlays/overlay_waveforms.fcl @@ -7,99 +7,102 @@ process_name: Overlay +#include "override_services_for_simulation.fcl" + ## Add the MC module to the list of producers physics.producers: { - mccrthit: @local::standard_crtsimhitproducer - overlayTPCRawEE: - { - module_type: "OverlayProducts" - TPCOverlayRaw: true - TPCOverlayHits: false - TPCHitsWireAssn: false - TPCOverlayROI: false - PMTOverlayRaw: false - PMTOverlayHits: false - CRTOverlayHits: false - TPCRawInputLabels: ["daqTPCROI:PHYSCRATEDATATPCEERAW", "daq:TPCEE"] - } - overlayTPCRawEW: - { - module_type: "OverlayProducts" - TPCOverlayRaw: true - TPCOverlayHits: false - TPCHitsWireAssn: false - TPCOverlayROI: false - PMTOverlayRaw: false - PMTOverlayHits: false - CRTOverlayHits: false - TPCRawInputLabels: ["daqTPCROI:PHYSCRATEDATATPCEWRAW", "daq:TPCEW"] - } - overlayTPCRawWE: - { - module_type: "OverlayProducts" - TPCOverlayRaw: true - TPCOverlayHits: false - TPCHitsWireAssn: false - TPCOverlayROI: false - PMTOverlayRaw: false - PMTOverlayHits: false - CRTOverlayHits: false - TPCRawInputLabels: ["daqTPCROI:PHYSCRATEDATATPCWERAW", "daq:TPCWE"] - } - overlayTPCRawWW: - { - module_type: "OverlayProducts" - TPCOverlayRaw: true - TPCOverlayHits: false - TPCHitsWireAssn: false - TPCOverlayROI: false - PMTOverlayRaw: false - PMTOverlayHits: false - CRTOverlayHits: false - TPCRawInputLabels: ["daqTPCROI:PHYSCRATEDATATPCWWRAW", "daq:TPCWW"] - } - - overlayOpWaveforms: - { - module_type: "OverlayProducts" - TPCOverlayRaw: false - TPCOverlayHits: false - TPCHitsWireAssn: false - TPCOverlayROI: false - PMTOverlayRaw: true - PMTOverlayHits: false - CRTOverlayHits: false - PMTWaveDataLabel: "daqPMT" - PMTWaveSimLabel: "shifted" - PMTWaveBaseLabel: "pmtbaselines" - } + mccrthit: @local::standard_crtsimhitproducer + overlayTPCRawEE: + { + module_type: "OverlayProducts" + TPCOverlayRaw: true + TPCOverlayHits: false + TPCHitsWireAssn: false + TPCOverlayROI: false + PMTOverlayRaw: false + PMTOverlayHits: false + CRTOverlayHits: false + TPCRawInputLabels: ["daqTPCROI:PHYSCRATEDATATPCEERAW", "daq:TPCEE"] + } + overlayTPCRawEW: + { + module_type: "OverlayProducts" + TPCOverlayRaw: true + TPCOverlayHits: false + TPCHitsWireAssn: false + TPCOverlayROI: false + PMTOverlayRaw: false + PMTOverlayHits: false + CRTOverlayHits: false + TPCRawInputLabels: ["daqTPCROI:PHYSCRATEDATATPCEWRAW", "daq:TPCEW"] + } + overlayTPCRawWE: + { + module_type: "OverlayProducts" + TPCOverlayRaw: true + TPCOverlayHits: false + TPCHitsWireAssn: false + TPCOverlayROI: false + PMTOverlayRaw: false + PMTOverlayHits: false + CRTOverlayHits: false + TPCRawInputLabels: ["daqTPCROI:PHYSCRATEDATATPCWERAW", "daq:TPCWE"] + } + overlayTPCRawWW: + { + module_type: "OverlayProducts" + TPCOverlayRaw: true + TPCOverlayHits: false + TPCHitsWireAssn: false + TPCOverlayROI: false + PMTOverlayRaw: false + PMTOverlayHits: false + CRTOverlayHits: false + TPCRawInputLabels: ["daqTPCROI:PHYSCRATEDATATPCWWRAW", "daq:TPCWW"] + } - overlayCRTHit: - { - module_type: "OverlayProducts" - TPCOverlayRaw: false - TPCOverlayHits: false - TPCHitsWireAssn: false - TPCOverlayROI: false - PMTOverlayRaw: false - PMTOverlayHits: false - CRTOverlayHits: true - CRTHitInputLabels: ["crthit","mccrthit"] - } - } + overlayOpWaveforms: + { + module_type: OverlayPMTwaveforms + + DataWaveformTag: "daqPMT" + DataBaselineAssns: "pmtbaselines" + SimWaveformTag: "shifted" # also their baselines + + # produce a baseline collection + # (trigger modules use one directly rather than going through associations) + DuplicateBaselines: true + + # check that no noise is in PMT waveform simulation: we require that at least + # >50% of the PMT waveforms lie flat on the baseline for their first 500 ns + BaselineCheck: { Length: "0.5 us" ForgivenessFraction: 0.5 } + } + + overlayCRTHit: + { + module_type: "OverlayProducts" + TPCOverlayRaw: false + TPCOverlayHits: false + TPCHitsWireAssn: false + TPCOverlayROI: false + PMTOverlayRaw: false + PMTOverlayHits: false + CRTOverlayHits: true + CRTHitInputLabels: ["crthit","mccrthit"] + } +} ## Use the following to run the full defined stage0 set of modules physics.reco: [ mccrthit, overlayTPCRawWW, overlayTPCRawWE, overlayTPCRawEW, overlayTPCRawEE, overlayOpWaveforms, overlayCRTHit] ## boiler plate... -physics.outana: [ ] physics.trigger_paths: [ reco ] -physics.end_paths: [ outana, streamROOT ] +physics.end_paths: [ streamROOT ] ## Note that for output we hijack the "rootOutput" definition (but change the naming convention to make more generic for MC) outputs.rootOutput.fileName: "%ifb_%tc-%p.root" outputs.rootOutput.dataTier: "reconstructed" -outputs.rootOutput.SelectEvents: ["reco"] +outputs.rootOutput.SelectEvents: @erase # Drop the artdaq format files on output outputs.rootOutput.outputCommands: ["keep *_*_*_*", diff --git a/fcl/overlays/partial/CMakeLists.txt b/fcl/overlays/partial/CMakeLists.txt new file mode 100644 index 000000000..6ce4b7b55 --- /dev/null +++ b/fcl/overlays/partial/CMakeLists.txt @@ -0,0 +1,8 @@ +# Install fcl files in /job subdirectory. +install_fhicl() + +# Also put a copy in the source tree. + +FILE(GLOB fcl_files *.fcl) +install_source( EXTRAS ${fcl_files} ) + diff --git a/fcl/overlays/partial/overlay_opdetonly_waveforms_reprocessing.fcl b/fcl/overlays/partial/overlay_opdetonly_waveforms_reprocessing.fcl new file mode 100644 index 000000000..019e68db8 --- /dev/null +++ b/fcl/overlays/partial/overlay_opdetonly_waveforms_reprocessing.fcl @@ -0,0 +1,34 @@ +## File: overlay_opdetonly_waveforms_reprocessing.fcl +## Purpose: Reprocessing of the optical products for the 2025 SBN "spring" OVERLAY MC production + +#include "overlay_waveforms.fcl" + +## This job selectively re-runs only the PMT/CRT overlays path on top of an existing file. +## This is has been put together only for the post-processing of the 2025 SBN MC OVERLAY +## productions, and assumes to be starting with an already re-processed OpDetSim files. +## However, since products names are the same as standard files, it can work on those as well. +## It is valid only for any run period. + +process_name: OpOverlay + +# only perform PMT and CRT overlays after their detsim reprocessing +# previous reprocessing step should have cleaned-up duplicates already +# so technically no need to update any tag names w.r.t the default file + +physics.producers.mccrthit.CrtModuleLabel: "crtdaq" # from previous reprocessing +physics.producers.mccrthit.TriggerLabel: "daqTrigger" + +physics.producers.overlayOpWaveforms.PMTWaveDataLabel: "daqPMT" # from data decoding +phsyics.producers.overlayOpWaveforms.PMTWaveSimLabel: "shifted" # from previous reprocessing +phsyics.producers.overlayOpWaveforms.PMTWaveBaseLabel: "pmtbaselines" # from data decoding + +physics.producers.overlayCRTHit.CRTHitInputLabels: ["crthit", # from data decoding + "mccrthit"] # from current process + +# process includes: +# - make crt hits on mc +# - overlay pmt waveforms +# - overlay crt hits + +physics.reco: [ mccrthit, overlayOpWaveforms, overlayCRTHit] + diff --git a/fcl/reco/Definitions/enable_overlay_sp.fcl b/fcl/reco/Definitions/enable_overlay_sp.fcl index 8275366c5..65f3206ee 100644 --- a/fcl/reco/Definitions/enable_overlay_sp.fcl +++ b/fcl/reco/Definitions/enable_overlay_sp.fcl @@ -1,5 +1,8 @@ +# add timing corrections to optical path by overriding ophit +physics.producers.ophit: @local::icarus_ophit_timing_correction + # Exclude crthit from path -- not needed since it's included at overlay stage -physics.path: [ @sequence::icarus_stage0_mc_PMT, +physics.path: [ @sequence::icarus_stage0_overlay_PMT, MCDecodeTPCROI, @sequence::icarus_stage0_2d_multiTPC, @sequence::icarus_stage0_mc_crtreco @@ -9,7 +12,12 @@ physics.path: [ @sequence::icarus_stage0_mc_PMT, physics.producers.MCDecodeTPCROI.FragmentsLabelVec: ["overlayTPCRawWW", "overlayTPCRawWE", "overlayTPCRawEW", "overlayTPCRawEE"] physics.producers.pmtbaselines.OpticalWaveforms: "overlayOpWaveforms" physics.producers.pmtfixedthr.OpticalWaveforms: "overlayOpWaveforms" -physics.producers.opdetonbeam.OpticalWaveforms: "overlayOpWaveforms" -physics.producers.ophit.InputModule: "overlayOpWaveforms" +physics.producers.pmtfixedthr.Baselines: "overlayOpWaveforms" +physics.producers.opdetonbeam.Waveforms: "overlayOpWaveforms" +physics.producers.opdetonbeam.WaveformBaselineAssns: "overlayOpWaveforms" +physics.producers.ophituncorrected.InputModule: "overlayOpWaveforms" +physics.producers.ophit.InputLabels: [ "ophituncorrected" ] physics.producers.crttrack.DataLabelHits: "overlayCRTHit" physics.producers.crtpmt.CrtHitModuleLabel: "overlayCRTHit" +physics.producers.emuTrigger: + @local::icarus_shifting_triggersim.producers.emuTrigger # from trigger_emulation_icarus.fcl diff --git a/fcl/reco/Definitions/stage0_icarus_defs.fcl b/fcl/reco/Definitions/stage0_icarus_defs.fcl index 8fa8b5d67..a2c981944 100644 --- a/fcl/reco/Definitions/stage0_icarus_defs.fcl +++ b/fcl/reco/Definitions/stage0_icarus_defs.fcl @@ -12,6 +12,7 @@ #include "timing_icarus.fcl" #include "timing_beam.fcl" #include "icarus_ophitfinder.fcl" +#include "ophit_recalibrator_icarus.fcl" #include "icarus_flashfinder.fcl" #include "trigger_emulation_icarus.fcl" #include "crt_decoderdefs_icarus.fcl" @@ -22,6 +23,7 @@ #include "icarus_FilterCRTPMTMatching.fcl" #include "spana_icarus.fcl" #include "sp_filter_parameters.fcl" +#include "pmt_calibration_icarus.fcl" BEGIN_PROLOG @@ -123,7 +125,6 @@ icarus_stage0_producers: ophitfull: @local::icarus_ophit_timing_correction opflashCryoE: @local::ICARUSSimpleFlashDataCryoE opflashCryoW: @local::ICARUSSimpleFlashDataCryoW - daqPMTonbeam: @local::copyPMTonBeam ### Beam timing diff --git a/fcl/reco/Definitions/stage0_icarus_driver_common.fcl b/fcl/reco/Definitions/stage0_icarus_driver_common.fcl index 6aff4f89b..b674ce281 100644 --- a/fcl/reco/Definitions/stage0_icarus_driver_common.fcl +++ b/fcl/reco/Definitions/stage0_icarus_driver_common.fcl @@ -4,6 +4,7 @@ #include "stage0_icarus_defs.fcl" #include "services_common_icarus.fcl" #include "channelmapping_icarus.fcl" +#include "pmt_channel_status_icarus.fcl" process_name: Stage0 @@ -12,6 +13,8 @@ services: TFileService: { } IICARUSChannelMap: @local::icarus_channelmappinggservice IPMTTimingCorrectionService: @local::icarus_pmttimingservice + IPhotonCalibratorService: @local::icarus_photon_calibration + IPMTChannelStatusService: @local::icarus_pmt_channel_status @table::icarus_wirecalibration_minimum_services } diff --git a/fcl/reco/Definitions/stage0_icarus_mc_defs.fcl b/fcl/reco/Definitions/stage0_icarus_mc_defs.fcl index 9052f38f3..0d0a1ff22 100644 --- a/fcl/reco/Definitions/stage0_icarus_mc_defs.fcl +++ b/fcl/reco/Definitions/stage0_icarus_mc_defs.fcl @@ -5,6 +5,7 @@ #include "icarus_flashfinder.fcl" #include "icarus_opana_modules.fcl" #include "recowire_icarus.fcl" +#include "timing_icarus.fcl" #include "crttruehitproducer.fcl" #include "crtsimhitproducer.fcl" @@ -37,40 +38,59 @@ icarus_stage0_mc_producers: pmtbaselines: @local::icarus_opreco_pedestal_fromchannel_MC # from icarus_ophitfinder.fcl opdetonbeam: @local::copyPMTonBeam # from decoderdefs_icarus.fcl - ophit: @local::icarus_ophit_data + + # ophit will produce recob::OpHit with all needed time corrections: + # - in legacy MC, that is actually no correction at all (configuration below); + # - in MC overlay, time correction is needed so ophit must be + # overridden later to become the time correction module + # (run on top of uncorrected hits) + # ophituncorrected never includes time corrections + ophituncorrected: @local::icarus_ophit_data # from icarus_ophitfinder.fcl + ophit: @local::icarus_ophit_MC # from icarus_ophitfinder.fcl mcophit: @local::ICARUSMCOpHit ## crt producer crthit: @local::standard_crtsimhitproducer crttrack: @local::standard_crttrackproducer - crtpmt: @local::standard_crtpmtmatchingproducer + crtpmt: @local::standard_crtpmtmatchingproducer } # make the following modifications to job flow for MC vs the data icarus_stage0_mc_trigger: [ - pmtfixedthr - , @sequence::icarus_standard_triggersim.path # from trigger_emulation_icarus.fcl + pmtfixedthr, + @sequence::icarus_standard_triggersim.path # from trigger_emulation_icarus.fcl ] icarus_stage0_mc_PMT: [ @sequence::icarus_stage0_mc_trigger, pmtbaselines, - ophit, + ophit, # @local::icarus_ophit_MC mcophit, opflashCryoE, opflashCryoW ] +icarus_stage0_overlay_PMT: [ + @sequence::icarus_stage0_mc_trigger, + pmtbaselines, + ophituncorrected, # @local::icarus_ophit_data + ophit, # ovveride to @local::icarus_ophit_timing_correction + # this happens in enable_overlay.fcl + opflashCryoE, + opflashCryoW +] + icarus_stage0_mc_crthit: [crthit] icarus_stage0_mc_crtreco: [crttrack, crtpmt] # adapt input labels icarus_stage0_mc_producers.emuTrigger.BeamGates: shifted +icarus_stage0_mc_producers.emuTrigger.BeamGateReference: Trigger # see `icarus_standard_triggersim` in `trigger_emulation_icarus.fcl` icarus_stage0_mc_producers.triggersimgates.module_type: DummyProducer # Don't rerun. We have already adjusted the BeamGate icarus_stage0_mc_producers.mcophit.SimPhotonsProducer: shifted icarus_stage0_mc_producers.ophit.InputModule: shifted icarus_stage0_mc_producers.opdetonbeam.Waveforms: shifted -icarus_stage0_mc_producers.opdetonbeam.WaveformBaselineAssns: pmtbaselines +icarus_stage0_mc_producers.opdetonbeam.WaveformBaselineAssns: shifted icarus_stage0_mc_producers.pmtbaselines.OpticalWaveforms: shifted icarus_stage0_mc_producers.pmtfixedthr.OpticalWaveforms: shifted diff --git a/fcl/reco/Stage0/CMakeLists.txt b/fcl/reco/Stage0/CMakeLists.txt index 178294463..4fcd34c26 100644 --- a/fcl/reco/Stage0/CMakeLists.txt +++ b/fcl/reco/Stage0/CMakeLists.txt @@ -3,3 +3,4 @@ cet_enable_asserts() add_subdirectory(mc) add_subdirectory(data) add_subdirectory(overlay) +add_subdirectory(partial) diff --git a/fcl/reco/Stage0/data/partial/decodePMT_icarus.fcl b/fcl/reco/Stage0/data/partial/decodePMT_icarus.fcl index d22be3e79..b70b737a1 100644 --- a/fcl/reco/Stage0/data/partial/decodePMT_icarus.fcl +++ b/fcl/reco/Stage0/data/partial/decodePMT_icarus.fcl @@ -53,6 +53,7 @@ #include "services_common_icarus.fcl" #include "channelmapping_icarus.fcl" #include "timing_icarus.fcl" +#include "pmt_channel_status_icarus.fcl" #include "rootoutput_icarus.fcl" #include "decoderdefs_icarus.fcl" @@ -71,7 +72,8 @@ services: { DetectorClocksService: @local::icarus_detectorclocks IICARUSChannelMap: @local::icarus_channelmappinggservice IPMTTimingCorrectionService: @local::icarus_pmttimingservice - + IPMTChannelStatusService: @local::icarus_pmt_channel_status + TFileService: { fileName: "Trees-%ifb_%tc-%p.root" } } diff --git a/fcl/reco/Stage0/mc/stage0_run2_icarus_mc.fcl b/fcl/reco/Stage0/mc/stage0_run2_icarus_mc.fcl index 533e5d486..841445a1f 100644 --- a/fcl/reco/Stage0/mc/stage0_run2_icarus_mc.fcl +++ b/fcl/reco/Stage0/mc/stage0_run2_icarus_mc.fcl @@ -7,6 +7,8 @@ process_name: MCstage0 +#include "override_services_for_simulation.fcl" + ## Add the MC module to the list of producers physics.producers: { @table::icarus_stage0_producers @table::icarus_stage0_mc_producers @@ -25,6 +27,9 @@ physics.outana: [ purityinfoana0, purityinfoana1 ] physics.trigger_paths: [ path ] physics.end_paths: [ outana, streamROOT ] +# because `path` above has no filter, let's not select events and give fast cloning a chance +outputs.rootOutput.SelectEvents: @erase + # Drop data products that are no longer needed, but make sure to keep important items! # For example, we need to drop the RawDigits from the detector simulation stage but want to keep the SimChannel info from WireCell... outputs.rootOutput.outputCommands: ["keep *_*_*_*", @@ -40,9 +45,6 @@ outputs.rootOutput.outputCommands: ["keep *_*_*_*", "keep *_daqPMT_*_*" ] -# Set the expected input for ophit -physics.producers.ophit.InputModule: "shifted" - # Note the default assumption is that our detector simulation input will come from the WireCell 2D drift simulation, a la 'daq' # If we are running the 1D drift simulation we need to switch to using: # `physics.producers.MCDecodeTPCROI.FragmentsLabelVec: ["daq3:PHYSCRATEDATATPCWW","daq2:PHYSCRATEDATATPCWE","daq1:PHYSCRATEDATATPCEW","daq0:PHYSCRATEDATATPCEE"]` @@ -67,3 +69,9 @@ physics.producers.purityana1.RawModuleLabel: ["MCDecodeTPCROI:PHYSCRATEDATATPC # restore legacy G4 labels physics.producers.mcophit.SimPhotonsProducer: "shifted" + +# set RUN-2 tune for optical reconstruction +physics.producers.ophit.SPEArea: @local::SPERun2.Area +physics.producers.ophit.SPEShift: @local::SPERun2.Shift +physics.producers.mcophit.SPEArea: @local::SPERun2.Area +physics.producers.mcophit.SPEAmplitude: @local::SPERun2.Amplitude diff --git a/fcl/reco/Stage0/mc/stage0_run2_icarus_mc_notriggersim.fcl b/fcl/reco/Stage0/mc/stage0_run2_icarus_mc_notriggersim.fcl index 15f5126f0..c38eb9338 100644 --- a/fcl/reco/Stage0/mc/stage0_run2_icarus_mc_notriggersim.fcl +++ b/fcl/reco/Stage0/mc/stage0_run2_icarus_mc_notriggersim.fcl @@ -4,5 +4,6 @@ physics.producers.emuTrigger.BeamGates: "beamgate" physics.producers.mcophit.SimPhotonsProducer: "pdfastsim" physics.producers.opdetonbeam.Waveforms: "opdaq" +physics.producers.ophit.InputModule: "opdaq" physics.producers.pmtbaselines.OpticalWaveforms: "opdaq" physics.producers.pmtfixedthr.OpticalWaveforms: "opdaq" diff --git a/fcl/reco/Stage0/mc/stage0_run2_wc_icarus_mc.fcl b/fcl/reco/Stage0/mc/stage0_run2_wc_icarus_mc.fcl index d0a3118a0..c2efac936 100644 --- a/fcl/reco/Stage0/mc/stage0_run2_wc_icarus_mc.fcl +++ b/fcl/reco/Stage0/mc/stage0_run2_wc_icarus_mc.fcl @@ -7,6 +7,8 @@ process_name: MCstage0 +#include "override_services_for_simulation.fcl" + ## Add the MC module to the list of producers physics.producers: { @table::icarus_stage0_producers @table::icarus_stage0_mc_producers @@ -39,9 +41,6 @@ outputs.rootOutput.outputCommands: ["keep *_*_*_*", "keep *_daqPMT_*_*" ] -# Set the expected input for ophit -physics.producers.ophit.InputModule: "shifted" - # Set up for the single module mutliple TPC mode... physics.producers.MCDecodeTPCROI.FragmentsLabelVec: ["daq:TPCWW","daq:TPCWE","daq:TPCEW","daq:TPCEE"] physics.producers.MCDecodeTPCROI.OutInstanceLabelVec: ["PHYSCRATEDATATPCWW", "PHYSCRATEDATATPCWE", "PHYSCRATEDATATPCEW", "PHYSCRATEDATATPCEE"] @@ -59,3 +58,9 @@ physics.producers.purityana1.RawModuleLabel: ["MCDecodeTPCROI:PHYSCRATEDATATPC # restore legacy G4 labels physics.producers.mcophit.SimPhotonsProducer: "shifted" + +# set RUN-2 tune for optical reconstruction +physics.producers.ophit.SPEArea: @local::SPERun2.Area +physics.producers.ophit.SPEShift: @local::SPERun2.Shift +physics.producers.mcophit.SPEArea: @local::SPERun2.Area +physics.producers.mcophit.SPEAmplitude: @local::SPERun2.Amplitude diff --git a/fcl/reco/Stage0/mc/stage0_run2_wc_icarus_mc_notriggersim.fcl b/fcl/reco/Stage0/mc/stage0_run2_wc_icarus_mc_notriggersim.fcl index 54f81872a..e927d9ab7 100644 --- a/fcl/reco/Stage0/mc/stage0_run2_wc_icarus_mc_notriggersim.fcl +++ b/fcl/reco/Stage0/mc/stage0_run2_wc_icarus_mc_notriggersim.fcl @@ -1,9 +1,9 @@ #include "stage0_run2_wc_icarus_mc.fcl" # restore non-shifted labels -physics.producers.ophit.InputModule: "opdaq" physics.producers.emuTrigger.BeamGates: "beamgate" physics.producers.mcophit.SimPhotonsProducer: "pdfastsim" physics.producers.opdetonbeam.Waveforms: "opdaq" +physics.producers.ophit.InputModule: "opdaq" physics.producers.pmtbaselines.OpticalWaveforms: "opdaq" physics.producers.pmtfixedthr.OpticalWaveforms: "opdaq" diff --git a/fcl/reco/Stage0/partial/CMakeLists.txt b/fcl/reco/Stage0/partial/CMakeLists.txt new file mode 100644 index 000000000..4776c013e --- /dev/null +++ b/fcl/reco/Stage0/partial/CMakeLists.txt @@ -0,0 +1,7 @@ +# Install fcl files in /job subdirectory. +install_fhicl() + +# Also put a copy in the source tree. + +FILE(GLOB fcl_files *.fcl) +install_source( EXTRAS ${fcl_files} ) \ No newline at end of file diff --git a/fcl/reco/Stage0/partial/stage0_run2_opdetonly_icarus_data_reprocessing.fcl b/fcl/reco/Stage0/partial/stage0_run2_opdetonly_icarus_data_reprocessing.fcl new file mode 100644 index 000000000..2d24bdb05 --- /dev/null +++ b/fcl/reco/Stage0/partial/stage0_run2_opdetonly_icarus_data_reprocessing.fcl @@ -0,0 +1,56 @@ +## File: stage0_run2_opdetonly_icarus_data_reprocessing.fcl +## Purpose: Reprocessing of the optical products for the 2025 SBN "spring" DATA production + +#include "stage0_run2_wcdnn_icarus.fcl" + +## This job selectively re-runs only the PMT reconstruction path on DATA. +## This was introduced for the post-processing of the 2025 SBN DATA productions. +## It recalibrates existing optical hits according the specific needs of that sample. +## In particular, it assumes the following: +## - No ophit time re-calibrations needed (they were already correct) +## - Recalibrates ophit PEs with SPEArea database (they were generated with +## the old icarus_spe.fcl defaults) +## As a result, this is valid only for RUN-2 (or RUN-3) data reprocessing from that production. + +process_name: opstage0 + +source.inputCommands: [ + "keep *", + + ## need to throw out anything that is based on optical hits; + ## this means optical flashes and pmt/crt matching stuff! + ## note: old hits will also be thrown out in output + "drop *_opflash*_*_*", # drop stage0 east/west opflashes + "drop *_crtpmt_*_*" # drop CRT/PMT matches (stage0) +] + +## running simplified stage0 pmt/crt path: +## recalibrate hits, then run flash matching + pmtcrt matching +## simplify path removing all other producers + +physics.producers.ophit: @local::ophit_recalibrator # force "ophit" name + +physics.path: [ + ophit, + opflashCryoE, + opflashCryoW, + crtpmt + ] + +## recalibrate previously-produced optical hits using the `OpHitRecalibrator` module +## given the specific target sample, not time recalibration is needed +## PE recalibration uses SPEArea database (channel-by-channel + correct time period) +physics.producers.ophit.InputLabel: "ophit::stage0" # start from hits in previous stage0 execution +physics.producers.ophit.RecalibratePE: true # recalibrate PE values +physics.producers.ophit.UseGainDatabase: true # speArea db +physics.producers.ophit.RecalibrateTime: false # not needed for Run2 + +# make sure flashes are using the new ophits +physics.producers.opflashCryoE.OpHitProducer: "ophit::opstage0" +physics.producers.opflashCryoW.OpHitProducer: "ophit::opstage0" + +# now drop old ophits to avoid confusion +outputs.rootOutput.outputCommands: [ + "keep *", + "drop *_ophit_*_stage0" + ] diff --git a/fcl/reco/Stage0/partial/stage0_run2_opdetonly_icarus_overlay_reprocessing.fcl b/fcl/reco/Stage0/partial/stage0_run2_opdetonly_icarus_overlay_reprocessing.fcl new file mode 100644 index 000000000..9956bb93d --- /dev/null +++ b/fcl/reco/Stage0/partial/stage0_run2_opdetonly_icarus_overlay_reprocessing.fcl @@ -0,0 +1,29 @@ +## File: stage0_run2_opdetonly_icarus_overlay_reprocessing.fcl +## Purpose: Reprocessing of the optical products for the 2025 SBN "spring" MC OVERLAY production + +#include "stage0_run2_wcdnn_icarus_overlay.fcl" + +## This job selectively re-runs only the PMT/CRT reconstruction path. +## This is valid only for the post-processing of the 2025 SBN. + +## Despite the name of the included configuration, the simulation of the optical detector +## in this configuration is independent of the run period because the reconstruction picks +## the SPEArea from the gain database, so it automatically adapts to the correct value. +## This is consistent with the behavior of the DetSim stage in the reprocessing workflow. +## However, trigger simulation settings are still hard-coded for Run2. + +process_name: OpMCstage0 + +## only perform PMT/CRT MCstage0 on top of reprocessed MC overlay files +## Notes: +## - previous MCStage0 PMT/CRT products have already been removed +## - new inputs have been re-baked, but with standard tag names +## - need to pick correct reconstruction tune for PMT flow + +# execute PMT/CRT only path +physics.path: [ @sequence::icarus_stage0_overlay_PMT, + @sequence::icarus_stage0_mc_crtreco # only reco, hits from overlay + ] + + + diff --git a/fcl/reco/Stage0/partial/stage0_run4_opdetonly_icarus_data_reprocessing.fcl b/fcl/reco/Stage0/partial/stage0_run4_opdetonly_icarus_data_reprocessing.fcl new file mode 100644 index 000000000..e5c950151 --- /dev/null +++ b/fcl/reco/Stage0/partial/stage0_run4_opdetonly_icarus_data_reprocessing.fcl @@ -0,0 +1,19 @@ +## File: stage0_run4_opdetonly_icarus_data_reprocessing.fcl +## Purpose: Reprocessing of the optical products for the 2025 SBN "spring" DATA production + +#include "stage0_run2_opdetonly_icarus_data_reprocessing.fcl" + +## This job selectively re-runs only the PMT reconstruction path on DATA. +## This was introduced for the post-processing of the 2025 SBN DATA productions. +## It recalibrates existing optical hits according the specific needs of that sample. +## This is valid only for RUN-4 data reprocessing from that production. + +## on top of the PE recalibration, this RUN-4 sample also needed new time calibrations +## it used older timing tags that did not have a complete sets of tables for RUN-4 +physics.producers.ophit.RecalibrateTime: true + +## these were the tags used in the initial data processing (v10_06_00_01p05) +## be careful: this is valide only for the SBN2025A "spring" production +physics.producers.ophit.OldTimingDBTags.CablesTag: @local::PMT_CalibrationTags_Run3_Feb2025.pmt_cables_delays_data +physics.producers.ophit.OldTimingDBTags.LaserTag: @local::PMT_CalibrationTags_Run3_Feb2025.pmt_laser_timing_data +physics.producers.ophit.OldTimingDBTags.CosmicsTag: @local::PMT_CalibrationTags_Run3_Feb2025.pmt_cosmics_timing_data \ No newline at end of file diff --git a/fcl/reco/Stage1/mc/stage1_run2_icarus_MC.fcl b/fcl/reco/Stage1/mc/stage1_run2_icarus_MC.fcl index 0db3cffcb..8a535c895 100644 --- a/fcl/reco/Stage1/mc/stage1_run2_icarus_MC.fcl +++ b/fcl/reco/Stage1/mc/stage1_run2_icarus_MC.fcl @@ -7,6 +7,8 @@ process_name: MCstage1 +#include "override_services_for_simulation.fcl" + # Disabled Space-Charge service for calorimetry services.SpaceChargeService: { EnableCalEfieldSCE: false diff --git a/fcl/services/override_services_for_simulation.fcl b/fcl/services/override_services_for_simulation.fcl new file mode 100644 index 000000000..98c409ccb --- /dev/null +++ b/fcl/services/override_services_for_simulation.fcl @@ -0,0 +1,40 @@ +# +# File: override_services_for_simulation.fcl +# Purpose: Change the configuration of services for use in simulation jobs +# Author: Gianluca Petrillo (petrillo@slac.stanford.edu) +# Date: April 17, 2026 +# +# Usage: drop this override after defining the services table or after including +# the driver configuration file: +# +# #include "override_services_for_simulation.fcl" +# +# Note that this is a configuration override, which requires a services +# configuration to have already been defined and must live outside of +# PROLOG blocks. +# +# Description: +# +# ICARUS reconstruction workflows are based on "driver" configurations that are +# written for data processing, while simulation definitions are included +# separately and then overriding of the driver configuration with the +# simulation-specific definitions is done explicitly in each job configuration. +# +# This override file simplifies the explicit override of service configurations, +# by performing it with a single-line inclusion (see Usage above). +# +# The current changes are: +# * DetectorClocksService: timing reference should be based on +# * `shifted` trigger time if `AdjustSimForTrigger` was used +# * default trigger time otherwise +# * never from data-based `daqTrigger` +# This holds true also, and especially, for overlay workflows, where +# `daqTrigger` should be used only in the independent overlay data decoding +# stage. +# +# + +services: { + @table::services + DetectorClocksService: @local::icarus_detectorclocks_simulation +} diff --git a/fcl/services/services_icarus_simulation.fcl b/fcl/services/services_icarus_simulation.fcl index b416faa26..953925850 100644 --- a/fcl/services/services_icarus_simulation.fcl +++ b/fcl/services/services_icarus_simulation.fcl @@ -39,6 +39,9 @@ #include "calibrationservices_icarus.fcl" #include "signalservices_icarus.fcl" #include "photpropservices_icarus.fcl" +#include "timing_icarus.fcl" +#include "pmt_channel_status_icarus.fcl" +#include "pmt_calibration_icarus.fcl" BEGIN_PROLOG @@ -51,6 +54,8 @@ icarus_gen_services: { @table::icarus_common_services + DetectorClocksService: @local::icarus_detectorclocks_simulation + } @@ -58,7 +63,8 @@ icarus_gen_services: { icarus_simulation_basic_services: { @table::icarus_common_services - + DetectorClocksService: @local::icarus_detectorclocks_simulation + LArG4Parameters: @local::icarus_largeantparameters LArVoxelCalculator: @local::icarus_larvoxelcalculator SpaceChargeService: @local::icarus_spacecharge @@ -82,6 +88,8 @@ icarus_detsim_dark_services: { # (it turns out they are the same ones needed for the inverse operation...) @table::icarus_wirecalibration_services + DetectorClocksService: @local::icarus_detectorclocks_simulation + } # icarus_detsim_dark_services @@ -114,11 +122,12 @@ icarus_g4_services: { # Define icarus_detsim_services ... (2*) icarus_detsim_services: { - + @table::icarus_detsim_dark_services - - # PmtGainService: @local::icarus_pmtgain_service - + IPMTTimingCorrectionService: @local::icarus_pmttimingservice + IPMTChannelStatusService: @local::icarus_pmt_channel_status + IPhotonCalibratorService: @local::icarus_photon_calibration + } # icarus_detsim_services diff --git a/fcl/utilities/dump_opdetwaveforms_icarus.fcl b/fcl/utilities/dump_opdetwaveforms_icarus.fcl index cdca85544..69d1ff44a 100644 --- a/fcl/utilities/dump_opdetwaveforms_icarus.fcl +++ b/fcl/utilities/dump_opdetwaveforms_icarus.fcl @@ -18,7 +18,7 @@ #include "detectorclocks_icarus.fcl" #include "dump_opdetwaveforms.fcl" -services.DetectorClocksService: @local::icarus_detectorclocks # from `detectorclocks_icarus.fcl` +services.DetectorClocksService: @local::icarus_detectorclocks_simulation # from `detectorclocks_icarus.fcl` physics.analyzers.dumpopdetwaveforms.OpDetWaveformsTag: "opdaq" physics.analyzers.dumpopdetwaveforms.Pedestal: 15000 diff --git a/fcl/utilities/dump_opflashes_icarus.fcl b/fcl/utilities/dump_opflashes_icarus.fcl new file mode 100644 index 000000000..89620e28a --- /dev/null +++ b/fcl/utilities/dump_opflashes_icarus.fcl @@ -0,0 +1,49 @@ +# File: dump_opflashes_icarus.fcl +# Purpose: Dump on screen of the standard optical flash data products. +# Author: Gianluca Petrillo (petrillo@slac.stanford.edu) +# Date: December 18, 2025 +# +# This job expects flashes in `opflashCryoE` and `opflashCryoW`. +# The output file `DumpOpFlashes.log` is produced. +# + +process_name: "DumpOpFlashes" + +physics: { + analyzers: { + dumpopflashesE: { + module_type: DumpOpFlashes + OpFlashModuleLabel: "opflashCryoE" + PrintOpHitAssociations: true + OutputCategory: "DumpOpFlashes" + } + dumpopflashesW: { + module_type: DumpOpFlashes + OpFlashModuleLabel: "opflashCryoW" + PrintOpHitAssociations: true + OutputCategory: "DumpOpFlashes" + } + } + dumpers: [ "dumpopflashesE", "dumpopflashesW" ] +} + +services.message.destinations: { + DumpOpFlashes: { + type: "file" + filename: "DumpOpFlashes.log" + threshold: "INFO" + append: false + categories: { + DumpOpFlashes: { limit: -1 } + default: { limit: 0 } + } + } + LogStandardOut: { + type: "cout" + threshold: "WARNING" + categories: { + DumpOpFlashes: { limit: 0 } + default: {} + } + } +} diff --git a/icaruscode/Decode/CMakeLists.txt b/icaruscode/Decode/CMakeLists.txt index 906e433ba..b499fa8de 100644 --- a/icaruscode/Decode/CMakeLists.txt +++ b/icaruscode/Decode/CMakeLists.txt @@ -1,24 +1,12 @@ cet_enable_asserts() -art_make_library( - EXCLUDE - TriggerConfigurationExtraction_module.cc - PMTconfigurationExtraction_module.cc - DumpTriggerConfiguration_module.cc - DumpPMTconfiguration_module.cc - DumpArtDAQfragments_module.cc - DumpTrigger_module.cc - DaqDecoderICARUSPMT_module.cc -) - set( MODULE_LIBRARIES icarus_signal_processing::Detection icarus_signal_processing::Filters - icaruscode_TPC_Utilities + icaruscode::TPC_Utilities sbndaq_artdaq_core::sbndaq-artdaq-core_Overlays_ICARUS artdaq_core::artdaq-core_Utilities larcorealg::Geometry - lardataobj::RawData larcore::Geometry_Geometry_service lardata::Utilities larevt::Filters @@ -91,7 +79,7 @@ cet_build_plugin(DumpTriggerConfiguration art::module LIBRARIES cet_build_plugin(DumpArtDAQfragments art::module LIBRARIES icaruscode::Decode_DecoderTools_Dumpers - larcorealg::headers + larcorealg::headers artdaq_core::artdaq-core_Data messagefacility::MF_MessageLogger fhiclcpp::fhiclcpp @@ -132,6 +120,16 @@ cet_build_plugin(OverlayProducts art::module LIBRARIES lardata::ArtDataHelper ) +cet_build_plugin(OverlayPMTwaveforms art::module LIBRARIES + sbncode::Utilities + icarusalg::Utilities + icarusalg::PMT_Algorithms + lardata::DetectorClocksService + lardataobj::RawData + sbnobj::ICARUS_PMT_Data + art::Framework_Services_Registry +) + install_headers() install_fhicl() install_source() diff --git a/icaruscode/Decode/OverlayPMTwaveforms_module.cc b/icaruscode/Decode/OverlayPMTwaveforms_module.cc new file mode 100644 index 000000000..af25462f9 --- /dev/null +++ b/icaruscode/Decode/OverlayPMTwaveforms_module.cc @@ -0,0 +1,529 @@ +/** + * @file OverlayPMTwaveforms_module.cc + * @brief Overlays simulated PMT waveforms on top of data PMT waveforms. + * @author Gianluca Petrillo (petrillo@slac.stanford.edu) + * @date March 23, 2026 + */ + +// ICARUS libraries +#include "sbncode/Utilities/AssnsUtils.h" // sbn::RebindAssociatedProducts() +#include "icarusalg/PMT/Algorithms/OverlayPMTwaveformAlg.h" +#include "icarusalg/Utilities/TimeInterval.h" +#include "icarusalg/Utilities/TimeIntervalConfig.h" // TimeIntervalOptionalTable +#include "sbnobj/ICARUS/PMT/Data/WaveformBaseline.h" + +// LArSoft libraries +#include "lardata/DetectorInfoServices/DetectorClocksService.h" +#include "lardataalg/DetectorInfo/DetectorTimings.h" +#include "lardataalg/DetectorInfo/DetectorTimingTypes.h" // detinfo::timescales::electronics_time +#include "lardataalg/Utilities/quantities/spacetime.h" // nanoseconds, ... +#include "lardataobj/RawData/OpDetWaveform.h" +#include "larcorealg/CoreUtils/enumerate.h" +#include "larcorealg/CoreUtils/counter.h" + +// framework libraries +#include "art/Framework/Core/SharedProducer.h" +#include "art/Framework/Core/ModuleMacros.h" +#include "art/Framework/Principal/Event.h" +#include "art/Framework/Principal/Handle.h" +#include "art/Persistency/Common/PtrMaker.h" +#include "canvas/Persistency/Common/FindOneP.h" +#include "canvas/Persistency/Common/Assns.h" +#include "canvas/Utilities/InputTag.h" +#include "messagefacility/MessageLogger/MessageLogger.h" +#include "fhiclcpp/types/Atom.h" +#include "fhiclcpp/types/Table.h" +#include "fhiclcpp/types/OptionalTable.h" + +// C/C++ standard libraries +#include +#include // std::make_unique() +#include +#include +#include // std::move() +#include + + +//------------------------------------------------------------------------------ +using namespace util::quantities::time_literals; + + +//------------------------------------------------------------------------------ +namespace sbn { class OverlayPMTwaveforms; } + +//------------------------------------------------------------------------------ +/** + * @brief Overlays simulated PMT waveforms on top of data PMT waveforms. + * + * This module reads two collections of `raw::OpDetWaveform`: + * - a data collection (the output segmentation matches these waveforms) + * - a simulated collection (added on top of data where time overlaps) + * + * The overlay is performed by `sbn::opdet::OverlayPMTwaveformAlg`. + * + * The overlay is based on the data waveforms: if simulation waveforms overlap + * them, their overlapping samples are added to the data waveforms; the + * simulation samples not in the data waveform coverage intervals are ignored: + * data waveforms are never extended nor shrunk. + * See `sbn::opdet::OverlayPMTwaveformAlg` algorithm documentation for more + * details. + * + * It is possible to limit the overlay to a (single) time interval + * (`LimitToTimeInterval` configuration parameter), in which case the data + * waveforms which do not overlap that interval will be copied in output + * unchanged. The ones overlapping that interval undergo full overlay in their + * whole range, even if wider than the limit interval. + * + * + * Input + * ----- + * + * * `std::vector` (`DataWaveformTag`): input data + * waveforms. + * * `std::vector` (`SimWaveformTag`): simulation waveforms + * to be overlaid to the data ones. They are required not to overlap within + * one channel. + * * `art::Assns` (`DataBaselineAssns`) + * associations between data waveforms and their baseline value. + * These associations are rebased to the new waveforms. Because the actual + * baseline values are not used, the associated baseline data product does + * not need to actually be available. + * * `art::Assns` (`SimBaselineAssns`) + * associations between simulated waveforms and their baseline value. + * The associated baseline data product must also be available. + * The recommended baseline is the one actually used for the generation of + * the simulated waveforms. + * + * + * Output + * ------ + * + * * `std::vector`: overlaid data + simulation waveforms. + * There is always one overlay waveform for each data waveform, in the same + * order as the input. Channel, time interval coverage and baseline are the + * same for the overlay waveform as in its original data waveform. + * * `std::vector` (if `DuplicateBaselines` is set): + * a new baseline collection, one-to-one + * @ref LArSoftProxyDefinitionParallelData "parallel data product", with the + * baseline values identical to the ones from the input waveforms. + * * `art::Assns>`: association + * between the new overlaid waveforms and their baseline value. Because the + * overlaying does not change the waveform baseline, the baseline values + * are the same as the input one. If `DuplicateBaselines` is not set, the + * baseline objects associated to the input data waveforms are directly + * reassociated to the overlaid ones, otherwise the ones from the new data + * product are used. + * + * + * Requirements + * ------------ + * + * ### Noise disabled and check + * + * Simulation of electronics noise must be disabled. Noise is already present + * in the data waveforms and must not be double-counted. + * + * The module checks that the simulated waveforms begin with a constant baseline + * region with no noise (`BaselineCheckLength` configuration parameter): + * if noise is disabled, the first samples of the waveform are expected to be + * exactly equal to the baseline. + * However, depending on the simulation settings, some waveforms may fail the + * check (false positive). For this reason, a fraction of failure can be set + * (`BaselineCheckForgivenessFraction`) that will be ignored. + * The check is considered failed only if more than this fraction of waveforms + * fail. For example, `BaselineCheckForgivenessFraction` set to `0.5` means that + * up to half of the waveform can fail and still pass the check; a value of + * `0.0` means that a single failing waveform will cause the check to fail. + * + * This check can be completely disabled by setting the check length to zero. + * + * + * Configuration + * ------------- + * + * * `DataWaveformTag` (input tag, mandatory): the data waveforms to be used. + * This data product drives the overlay. + * * `DataBaselineAssns` (input tag, default: as `DataWaveformTag`): the + * associations between data waveforms and their baselines. + * * `SimWaveformTag` (input tag, mandatory): the simulated waveforms to be + * overlaid on top of the data. + * * `SimBaselineAssns` (input tag, default: as `SimWaveformTag`): the + * associations between data waveforms and their baselines. + * * `LimitToTimeInterval` (time interval table, optional): if specified, only + * the data waveforms overlapping this interval are overlaid, while the others + * are copied verbatim in the output. Times are expressed on the electronics + * time scale (the same as the waveform timestamps), and the table format is + * documented in `icarus::ns::fhicl::TimeIntervalConfig`. + * * `BaselineCheckLength` (microseconds, default: `"0.5 us"`): interval for + * the validation of the requirement of no noise in simulation waveforms. + * A zero value disables the check. + * * `BaselineCheckForgivenessFraction` (real number, default: `0.5`): + * * `DuplicateBaselines` (flag, default: `false`): if set to true, a new + * baseline collection is produced instead of reusing the existing one. + * This is useful for modules that use it directly rather than via + * associations. + * * `LogCategory` (string, default: `"OverlayPMTwaveforms"`): name of the + * message facility stream for module console output. + * + */ +class sbn::OverlayPMTwaveforms: public art::SharedProducer { + + public: + + // --- BEGIN Configuration ----------------------------------------------------- + struct Config { + + using Name = fhicl::Name; + using Comment = fhicl::Comment; + + using nanoseconds = util::quantities::intervals::nanoseconds; + using microseconds = util::quantities::intervals::microseconds; + using electronics_time = detinfo::timescales::electronics_time; + + struct BaselineCheckConfig { + + fhicl::Atom Length { + Name{ "Length" }, + Comment{ + "expected duration of noiseless initial baseline in simulated waveforms (0 disables)" + }, + 0.5_us + }; + + fhicl::Atom ForgivenessFraction { + Name{ "ForgivenessFraction" }, + Comment{ "tolerated fraction of waveform failing the baseline check" }, + 0.0 + }; + + }; // BaselineCheckConfig + + + fhicl::Atom DataWaveformTag { + Name{ "DataWaveformTag" }, + Comment{ "tag of input data PMT waveforms" } + }; + + fhicl::OptionalAtom DataBaselineAssns { + Name{ "DataBaselineAssns" }, + Comment{ "tag of baseline association for data waveforms (as DataWaveformTag if omitted)" } + }; + + fhicl::Atom SimWaveformTag { + Name{ "SimWaveformTag" }, + Comment{ "tag of input simulated PMT waveforms" } + }; + + fhicl::OptionalAtom SimBaselineAssns { + Name{ "SimBaselineAssns" }, + Comment{ "tag of baseline association for simulated waveforms (as SimWaveformTag if omitted)" } + }; + + icarus::ns::fhicl::TimeIntervalOptionalTable LimitToTimeInterval { + Name{ "LimitToTimeInterval" }, + Comment{ + "optional time interval (electronics time scale) where overlay is applied" + } + }; + + fhicl::Atom DuplicateBaselines { + Name{ "DuplicateBaselines" }, + Comment + { "create a new baseline collection instead of using the input one" }, + false + }; + + fhicl::Table BaselineCheck { + Name{ "BaselineCheck" }, + Comment{ "baseline check configuration" } + }; + + + fhicl::Atom LogCategory { + Name{ "LogCategory" }, + Comment{ "MessageFacility category used for output messages" }, + "OverlayPMTwaveforms" + }; + + }; // struct Config + + using Parameters = art::SharedProducer::Table; + // --- END Configuration ------------------------------------------------------- + + explicit OverlayPMTwaveforms(Parameters const& config, art::ProcessingFrame const&); + + void produce(art::Event& event, art::ProcessingFrame const&) override; + + private: + + // aliases + using nanoseconds = util::quantities::intervals::nanoseconds; + using electronics_time = detinfo::timescales::electronics_time; + + using Interval_t = icarus::ns::util::TimeInterval; + + using BaselineAssns_t = art::Assns; + + + // --- BEGIN --- Configuration --------------------------------------------- + + /// Input data waveform tag. + art::InputTag const fDataWaveformsTag; + + /// Input data baseline-to-waveform association tag. + art::InputTag const fDataBaselineAssnsTag; + + /// Input simulated waveform tag. + art::InputTag const fSimWaveformsTag; + + /// Input simulated baseline-to-waveform association tag. + art::InputTag const fSimBaselineAssnsTag; + + /// Limit to data waveforms overlapping this interval. + std::optional const fLimit; + + /// Whether to produce a baseline collection data product. + bool const fDuplicateBaselines; + + std::string const fLogCategory; ///< Name of console message stream. + + // --- END --- Configuration --------------------------------------------- + + nanoseconds const fOpticalTick; ///< Cached size of sampling tick. + + sbn::opdet::OverlayPMTwaveformAlg fAlgo; ///< Overlay algorithm. + + + /// Prepares a waveform input collection for the algorithm. + std::vector + prepareWaveformInput( + std::vector const& waveforms, + art::FindOneP const& toBaseline + ) const; + +}; // class sbn::OverlayPMTwaveforms + + +//------------------------------------------------------------------------------ +namespace { + + /// Moves `data` to a unique pointer, ready for `art::Event::put()`. + template + [[nodiscard]] std::unique_ptr moveProduct(T& data) + { return std::make_unique(std::move(data)); } + +} // local namespace + + +//------------------------------------------------------------------------------ +//--- sbn::OverlayPMTwaveforms +//------------------------------------------------------------------------------ +sbn::OverlayPMTwaveforms::OverlayPMTwaveforms + (Parameters const& config, art::ProcessingFrame const&) + : art::SharedProducer{ config } + // configuration + , fDataWaveformsTag{ config().DataWaveformTag() } + , fDataBaselineAssnsTag{ config().DataBaselineAssns().value_or(fDataWaveformsTag) } + , fSimWaveformsTag{ config().SimWaveformTag() } + , fSimBaselineAssnsTag{ config().SimBaselineAssns().value_or(fSimWaveformsTag) } + , fLimit{ config().LimitToTimeInterval() } + , fDuplicateBaselines{ config().DuplicateBaselines() } + , fLogCategory{ config().LogCategory() } + // caches + , fOpticalTick{ + detinfo::makeDetectorTimings( + art::ServiceHandle()->DataForJob() + ).OpticalClockPeriod() + } + , fAlgo{ + sbn::opdet::OverlayPMTwaveformAlg::Config{ + fOpticalTick, + { + config().BaselineCheck().Length(), + config().BaselineCheck().ForgivenessFraction() + }, + fLogCategory + } + } +{ + async(); + + consumes>(fDataWaveformsTag); + consumes(fDataBaselineAssnsTag); + + consumes>(fSimWaveformsTag); + consumes(fSimBaselineAssnsTag); + + produces>(); + produces(); + if (fDuplicateBaselines) + produces>(); + + { + mf::LogInfo log{ fLogCategory }; + + log << "OverlayPMTwaveforms configuration:" + << "\n - data waveforms: '" << fDataWaveformsTag.encode() << "'" + << "\n - data baselines association: '" << fDataBaselineAssnsTag.encode() << "'" + << "\n - simulated waveforms: '" << fSimWaveformsTag.encode() << "'" + << "\n - simulated baselines association: '" << fSimBaselineAssnsTag.encode() << "'" + ; + if (fDuplicateBaselines) + log << "\n - create a new baseline collection data product"; + else + log << "\n - associate waveforms to the existing baseline data product"; + + fAlgo.dumpConfiguration(log, " ", "\n - "); + + } // configuration report block + +} // sbn::OverlayPMTwaveforms::OverlayPMTwaveforms() + + +//------------------------------------------------------------------------------ +void sbn::OverlayPMTwaveforms::produce(art::Event& event, art::ProcessingFrame const&) { + + detinfo::DetectorTimings const detTimings = detinfo::makeDetectorTimings( + art::ServiceHandle()->DataFor(event) + ); + + assert(detTimings.OpticalClockPeriod() == fOpticalTick); + + // + // input fetching and preparation + // + auto const& dataHandle = event.getValidHandle>(fDataWaveformsTag); + auto const& simHandle = event.getValidHandle>(fSimWaveformsTag); + + std::vector const& dataWaveforms = *dataHandle; + std::vector const& simWaveforms = *simHandle; + + art::FindOneP const findDataBaseline{ + dataHandle, event, fDataBaselineAssnsTag + }; + if (!findDataBaseline.isValid()) { + throw art::Exception{ art::errors::ProductNotFound } + << "OverlayPMTwaveforms: baseline association product for data waveforms '" + << fDataBaselineAssnsTag.encode() << "' is not available or not valid.\n"; + } // if invalid data baseline finder + + art::FindOneP const findSimBaseline{ + simHandle, event, fSimBaselineAssnsTag + }; + if (!findSimBaseline.isValid()) { + throw art::Exception{ art::errors::ProductNotFound } + << "OverlayPMTwaveforms: baseline association product for simulated waveforms '" + << fSimBaselineAssnsTag.encode() << "' is not available or not valid.\n"; + } // if invalid simulated baseline finder + + std::vector dataIn; + try { + dataIn = prepareWaveformInput(dataWaveforms, findDataBaseline); + } + catch (art::Exception const& e) { + throw art::Exception{ e.categoryCode(), "", e } + << "Error while processing data input from '" + << fDataWaveformsTag.encode() << "'.\n"; + } + + std::vector simIn; + try { + simIn = prepareWaveformInput(simWaveforms, findSimBaseline); + } + catch (art::Exception const& e) { + throw art::Exception{ e.categoryCode(), "", e } + << "Error while processing data input from '" + << fSimWaveformsTag.encode() << "'.\n"; + } + + // + // overlay + // + sbn::opdet::OverlayPMTwaveformAlg::OverlaidWaveforms const algoRes + = fAlgo.overlay(dataIn, simIn, fLimit); + assert(algoRes.waveforms.size() == dataWaveforms.size()); + + // + // data products + // + std::vector outWaveforms = std::move(algoRes.waveforms); + + // check that input and output waveforms share everything but sample values + for ([[maybe_unused]] auto const& [ dataWaveform, overlayWaveform ] + : util::zip(dataWaveforms, outWaveforms) + ) { + assert(overlayWaveform.ChannelNumber() == dataWaveform.ChannelNumber()); + assert(overlayWaveform.TimeStamp() == dataWaveform.TimeStamp()); + assert(overlayWaveform.size() == dataWaveform.size()); + } // for output waveforms + + mf::LogDebug{ fLogCategory } + << "OverlayPMTwaveforms wrote " << outWaveforms.size() << " waveforms."; + + std::optional> outBaselines; + BaselineAssns_t outBaselineAssns; + + art::PtrMaker const makeWaveformPtr{ event }; + + if (fDuplicateBaselines) { + outBaselines.emplace(); + + art::PtrMaker const makeBaselinePtr{ event }; + + for (auto const& [ iWaveform, waveform ]: util::enumerate(outWaveforms)) { + + outBaselines->push_back(*findDataBaseline.at(iWaveform)); // copy + + outBaselineAssns.addSingle + (makeWaveformPtr(iWaveform), makeBaselinePtr(iWaveform)); + + } // for output waveforms + + } + else { + // associate the new waveforms to the old baselines + outBaselineAssns = sbn::RebindAssociatedProducts( + event.getProduct(fDataBaselineAssnsTag), + makeWaveformPtr + ); + } + assert(outBaselineAssns.size() == outWaveforms.size()); + + event.put(moveProduct(outWaveforms)); + if (outBaselines) + event.put(moveProduct(*outBaselines)); + event.put(moveProduct(outBaselineAssns)); + +} // sbn::OverlayPMTwaveforms::produce() + + +//------------------------------------------------------------------------------ +auto sbn::OverlayPMTwaveforms::prepareWaveformInput( + std::vector const& waveforms, + art::FindOneP const& toBaseline +) const + -> std::vector +{ + + std::vector input; + input.reserve(waveforms.size()); + + for (auto const& [ iWaveform, waveform ]: util::enumerate(waveforms)) { + + art::Ptr const& baseline = toBaseline.at(iWaveform); + if (!baseline) { + throw art::Exception{ art::errors::LogicError } + << "OverlayPMTwaveforms: missing baseline associated to waveform #" + << iWaveform << " (CH=" << waveform.ChannelNumber() + << " TS=" << waveform.TimeStamp() << ").\n"; + } // if no baseline + + input.push_back({ &waveform, baseline.get() }); + + } // for waveforms + return input; +} // sbn::OverlayPMTwaveforms::prepareWaveformInput() + + +//------------------------------------------------------------------------------ +DEFINE_ART_MODULE(sbn::OverlayPMTwaveforms) diff --git a/icaruscode/Decode/OverlayProducts_module.cc b/icaruscode/Decode/OverlayProducts_module.cc index c614a2eba..ecccff550 100644 --- a/icaruscode/Decode/OverlayProducts_module.cc +++ b/icaruscode/Decode/OverlayProducts_module.cc @@ -598,10 +598,13 @@ void OverlayProducts::produce(art::Event& e) } // loop overlaps // and fill in any remaining bins left - while ( idxWvfmEntry < wvfm.size() ) { + /// @todo: Temporary override to chop off flat tails after data waveform ends + /// timing shift between data/mc event should be tuned so that this never (?) happens! + /*while ( idxWvfmEntry < wvfm.size() ) { adcVec.push_back( overlayOpWave.baseline + (wvfm[idxWvfmEntry] - simBaseline) ); idxWvfmEntry+=1; - } + }*/ + idxWvfmEntry = wvfm.size(); // Now delete the overlapping waveforms for ( unsigned int iOverlap=0; iOverlap // std::count() +#include +#include // std::move() +#include + + +//------------------------------------------------------------------------------ +/** + * @brief Requires the presence of generated particles crossing a cathode. + * + * This module passes only the events with a minimum number of generated + * particles (from `simb::MCTruth`) predicted to cross any cathode. + * + * All particles in the final state (status `1`) of the specified generated + * records are considered. The number of particles whose predicted trajectory + * crosses any of the cathodes of the detector, plus a margin, is compared to + * the configured requirement. + * + * The counts are currently global: if a particle crosses the cathode of + * cryostat 1 and another the cathode of cryostat 2, the event passes the + * requirement of _two_ cathode-crossing particles. + * + * The crossing prediction is based on whether the predicted particle trajectory + * crosses a rectangularly shaped cathode, with some margin. + * The trajectory is built as simple inertial extrapolation of the particle at + * generator time. + * + * Cathode coordinates are not provided by the detector geometry description, + * so for each TPC a cathode is deduced (see below). + * On top of the cathode dimensions, each side of each cathode is extended by + * a configured amount. For example, if a cathode is 9 × 3.4 m² + * (L × H), with `CathodeLengthWiggle` of `0.1` the cathode length (9 m) + * is extended by 0.9 m on one side _and_ 0.9 m on the other side, for a total + * width of 10.8 m (that is, 20% longer). + * + * + * Cathode size + * ------------- + * + * Cathode coordinates are not provided by the detector geometry description, + * so _for each TPC_ a cathode is deduced. + * + * The definition of the cathode surface is as follow: + * * a cross section of the active volume is determined as the rectangle + * with sides along `geo::TPCGeo::LengthDir()` and `geo::TPCGeo::HeightDir()` + * and sizes `geo::TPCGeo::ActiveLength()` and `geo::TPCGeo::ActiveHeight()`, + * respectively, around the central point of the active volume + * (`geo::TPCGeo::GetActiveVolumeCenter()`). + * * cathode surface is set translating that rectangle by + * `geo::TPCGeo::ActiveHalfWidth()` along the direction `geo::TPCGeo::WidthDir()`, + * which is supposed to be opposite to the drift direction. + * + * + * + * + * Configuration parameters + * ========================= + * + * * `GeneratorTags` (input tags, mandatory): list of tags of generated particle + * records to consider. + * * `MinimumCrossingParticles` (integral, default: `1`): minimum number of + * cathode-crossing particles required for the event to pass the filter. + * * `CathodeLengthWiggle`, `CathodeHeightWiggle` (real, default: `0.0`): + * fraction of each dimension the cathode is extended for the purpose of + * determining the crossing of the particle (see the documentation above). + * * `LogCategory` (string, default: `SelectCathodeCrossingGenParticles`): + * message facility stream name where to write console messages to. + * + * + * Input data products + * ==================== + * + * * `std::vector` (`GeneratorTags`, multiple supported): + * the tags of the generated events to be included in the test. + * + * + * Service dependencies + * ===================== + * + * * `Geometry`: to learn the position of the cathodes. + * + * + * + * Output data products + * ===================== + * + * None. + * + */ +class SelectCathodeCrossingGenParticles: public art::SharedFilter { + + public: + + struct Config { + using Name = fhicl::Name; + using Comment = fhicl::Comment; + + fhicl::Sequence GeneratorTags{ + Name{ "GeneratorTags" }, + Comment{ "list of generated particle tags to check" }, + std::vector{ art::InputTag{ "generator" } } + }; + + fhicl::Atom MinimumCrossingParticles{ + Name{ "MinimumCrossingParticles" }, + Comment{ "minimum required number of cathode-crossing particles" }, + 1U + }; + + fhicl::Atom CathodeLengthWiggle{ + Name{ "CathodeLengthWiggle" }, + Comment + { "extend each cathode length by this fraction (negative contracts)" }, + 0.0 + }; + + fhicl::Atom CathodeHeightWiggle{ + Name{ "CathodeHeightWiggle" }, + Comment + { "extend each cathode height by this fraction (negative contracts)" }, + 0.0 + }; + + fhicl::Atom LogCategory{ + Name{ "LogCategory" }, + Comment{ "name of the message facility stream used by the module" }, + "SelectCathodeCrossingGenParticles" + }; + + }; // Config + + using Parameters = art::SharedFilter::Table; + + + SelectCathodeCrossingGenParticles + (Parameters const& params, art::ProcessingFrame const&); + + /// Evaluate the filtering logic. + virtual bool filter(art::Event& event, art::ProcessingFrame const&) override; + + /// Prints a summary. + virtual void endJob(art::ProcessingFrame const&) override; + + private: + + /// The algorithm storing plane geometry and providing intersection with lines. + using PlaneCrosserAlg_t = util::PlaneCrossers; + + /// Information identifying the cathode rectangle of one TPC. + class CathodePlane_t { + + PlaneCrosserAlg_t fCrossFinder; + + geo::TPCGeo const* fTPC = nullptr; + + public: + + CathodePlane_t( + geo::Point_t center, geo::Vector_t length, geo::Vector_t height, + geo::TPCGeo const& TPC + ) + : fCrossFinder{ std::move(center), std::move(length), std::move(height) } + , fTPC{ &TPC } + {} + + geo::Point_t center() const { return fCrossFinder.center(); } + + /// Versor of the length direction. + geo::Vector_t lengthDir() const { return fCrossFinder.Uaxis().unit(); } + /// Versor of the height direction. + geo::Vector_t heightDir() const { return fCrossFinder.Vaxis().unit(); } + + double length() const { return fCrossFinder.Uaxis().R(); } + double height() const { return fCrossFinder.Vaxis().R(); } + + geo::TPCGeo const& TPC() const { return *fTPC; } + + PlaneCrosserAlg_t crossFinder() const + { return fCrossFinder; } + + /// Finds the crossing between the cathode and the specified line. + /// @see util::PlaneCrossers::findCrossing() + PlaneCrosserAlg_t::CrossingInfo findCrossing + (geo::Point_t const& point, geo::Vector_t const& dir) const + { return fCrossFinder.findCrossing(point, dir); } + + }; // CathodePlane_t + + + /// Value returned to represent the possible crossing of a cathode. + struct CathodeCrossInfo { + + /// Which cathode was crossed (if any). + CathodePlane_t const* cathode = nullptr; + + /// Information about the crossing with a line. + PlaneCrosserAlg_t::CrossingInfo crossing; + + /// How many `length()`s the crossing point is from cathode center. + double cathodeLengthUnits() const { return crossing.u; } + /// How many `height()`s the crossing point is from cathode center. + double cathodeHeightUnits() const { return crossing.v; } + /// How many particle direction units the crossing point is from its position. + double particleDirUnits() const { return crossing.line; } + + /// Returns whether any cathode was crossed. + bool crossed() const noexcept { return crossing; } + + /// Returns whether any cathode was crossed. + operator bool() const noexcept { return crossed(); } + + /// Access to the cathode information (undefined behaviour if `crossed()` is `false`). + CathodePlane_t const* operator-> () const { return cathode; } + + /// Returns the cathode/line intersection point (undefined behaviour if none). + geo::Point_t intersectionPoint() const + { + assert(cathode); + return cathode->crossFinder().intersectionPoint(crossing); + } + + }; // CathodeCrossInfo + + + /// Counts of particle categories to determine the satisfaction of requirements. + struct Counters_t { + + /// Number of truth records seen. + unsigned int nTruthRecords = 0; + + /// Number of particles from any interaction crossing any cathode. + icarus::ns::util::PassCounter<> cathodeCrossing; + + }; // Counters_t + + + // --- BEGIN -- Configuration parameters ------------------------------------- + + std::vector const fGeneratorTags; ///< Input data products. + + /// Minimum number of requested cathode-crossing particles. + unsigned int const fMinimumCrossingPartcles; + + double const fCathodeLengthWiggle; ///< Fractional allowance on cathode length. + double const fCathodeHeightWiggle; ///< Fractional allowance on cathode height. + + std::string const fLogCategory; ///< Name of message facility stream. + + // --- END ---- Configuration parameters ------------------------------------- + + // --- BEGIN -- Caches ------------------------------------------------------- + + // we don't really need to group by cryostat/TPC, but it comes for ~free + std::vector const fCathodes; ///< All cathodes. + + // --- END ---- Caches ------------------------------------------------------- + + /// Count of passed and seen events. + icarus::ns::util::AtomicPassCounter<> fPassed; + + /** + * @brief Analyzes the `truth` information and updates the `counts` accordingly. + * @param truth the generated record to extract information from + * @param[out] counters counters to be updated + * + * This method extracts information but does not take any decision. + */ + void parseTruth(simb::MCTruth const& truth, Counters_t& counters) const; + + + /** + * @brief Returns one TPC whose cathode is crossed by a particle. + * @param pos starting position of the particle + * @param dir direction of the particle + * @return information on the crossing, converts to `false` if no crossing + * + * If there are multiple cathodes crossed by the particle, which one is + * returned is undefined. + */ + CathodeCrossInfo findCrossingTPCcathode + (geo::Point_t const& pos, geo::Vector_t const& dir) const; + + + /// Builds and returns information of the cathode of the specified `TPC`. + static CathodePlane_t buildCathode(geo::TPCGeo const& TPC); + + /// Builds and returns all cathodes, one per logical TPC in the geometry. + static std::vector buildCathodePlanes + (geo::GeometryCore const& geom); + +}; // class SelectCathodeCrossingGenParticles + + +//------------------------------------------------------------------------------ + +SelectCathodeCrossingGenParticles::SelectCathodeCrossingGenParticles + (Parameters const& params, art::ProcessingFrame const&) + : art::SharedFilter{ params } + // configuration + , fGeneratorTags { params().GeneratorTags() } + , fMinimumCrossingPartcles{ params().MinimumCrossingParticles() } + , fCathodeLengthWiggle { params().CathodeLengthWiggle() * 2.0 } // x2 for + , fCathodeHeightWiggle { params().CathodeHeightWiggle() * 2.0 } // half lengths + , fLogCategory { params().LogCategory() } + // caches + , fCathodes{ buildCathodePlanes(*lar::providerFrom()) } +{ + + async(); + + // + // consume declaration + // + for (art::InputTag const& tag: fGeneratorTags) + consumes>(tag); + + // + // dump configuration + // + mf::LogInfo log{ fLogCategory }; + log << "Configuration:"; + + log << "\n * check " << fGeneratorTags.size() << " generated data products:"; + for (art::InputTag const& tag: fGeneratorTags) + log << " '" << tag.encode() << "'"; + + log << "\n * minimum number of cathode-crossing particles: " + << fMinimumCrossingPartcles; + + log << "\n * fractional allowance on cathode dimensions: width " + << (fCathodeLengthWiggle / 2 * 100.0) << "%, height " + << (fCathodeHeightWiggle / 2 * 100.0) << "%"; + + // + // debug dump + // + { + mf::LogTrace log{ fLogCategory }; + log << "Found " << fCathodes.size() << " cathode planes:"; + for (CathodePlane_t const& cathode: fCathodes) { + log << "\n - " << cathode.TPC().ID() + << ": ( " << cathode.length() << " x " << cathode.height() << " ) cm at " + << cathode.center(); + } // for + } + +} // SelectCathodeCrossingGenParticles::SelectCathodeCrossingGenParticles() + + +//------------------------------------------------------------------------------ +bool SelectCathodeCrossingGenParticles::filter + (art::Event& event, art::ProcessingFrame const&) +{ + + // + // collect information + // + mf::LogDebug{ fLogCategory } << "Evaluation started."; + + Counters_t counters; + + for (art::InputTag const& truthTag: fGeneratorTags) { + + auto const& truths = event.getProduct>(truthTag); + mf::LogTrace{ fLogCategory } + << "Now serving: " << truthTag.encode() << " (" << truths.size() << " records)"; + + for (auto const& [ iTruth, truth ]: util::enumerate(truths)) { + mf::LogTrace{ fLogCategory } + << "Truth #" << iTruth << " from '" << truthTag.encode() << ": " + << truth.NParticles() << " particles"; + + parseTruth(truth, counters); + + } // for truth record + + } // for truth tags + + + // + // draw conclusions + // + unsigned int const nCathodeCrossing = counters.cathodeCrossing.passed(); + bool const accepted = (nCathodeCrossing >= fMinimumCrossingPartcles); + + mf::LogInfo{ fLogCategory } + << event.id() << ": " << counters.cathodeCrossing.passed() + << " / " << counters.cathodeCrossing.total() + << " generated particles crossed a cathode."; + + fPassed.add(accepted); + + return accepted; + +} // SelectCathodeCrossingGenParticles::filter() + + +//------------------------------------------------------------------------------ +void SelectCathodeCrossingGenParticles::endJob(art::ProcessingFrame const&) { + + if (fPassed.total() > 0) { + mf::LogInfo{ fLogCategory } + << fPassed.passed() << "/" << fPassed.total() + << " events (" << (fPassed.passed() * 100.0 / fPassed.total()) + << "%) passed the requirements."; + } + +} // SelectCathodeCrossingGenParticles::endJob() + + +//------------------------------------------------------------------------------ +void SelectCathodeCrossingGenParticles::parseTruth + (simb::MCTruth const& truth, Counters_t& counters) const +{ + + ++(counters.nTruthRecords); + + for (auto const iPart: util::counter(truth.NParticles())) { + simb::MCParticle const& particle = truth.GetParticle(iPart); + + bool const isFinal = particle.StatusCode() == 1; + if (!isFinal) continue; + + geo::Point_t const pos = geo::vect::toPoint(particle.EndPosition().Vect()); + geo::Vector_t const dir = geo::vect::toVector(particle.EndMomentum().Vect()); + + CathodeCrossInfo const crossedTPCcathode = findCrossingTPCcathode(pos, dir); + + { + mf::LogTrace log{ fLogCategory }; + log << "Particle #" << iPart << " (" << sim::ParticleName(particle.PdgCode()) + << " at " << pos << " toward " << dir << ")"; + if (crossedTPCcathode) log << " crosses " << crossedTPCcathode->TPC().ID(); + else log << " does not cross any TPC cathode"; + } // block + + counters.cathodeCrossing.add(crossedTPCcathode.crossed()); + + } // for particle + +} // SelectCathodeCrossingGenParticles::parseTruth() + + +//------------------------------------------------------------------------------ +SelectCathodeCrossingGenParticles::CathodeCrossInfo +SelectCathodeCrossingGenParticles::findCrossingTPCcathode + (geo::Point_t const& pos, geo::Vector_t const& dir) const +{ + // try all, one after another + mf::LogTrace{ fLogCategory } + << "Testing particle from " << pos << " cm toward " << dir; + + for (CathodePlane_t const& plane: fCathodes) { + CathodeCrossInfo const crossing { + /* .cathode = */ &plane + , /* .crossing = */ plane.findCrossing(pos, dir) + }; + + if (crossing) { + mf::LogTrace{ fLogCategory } << " - TPC " << plane.TPC().ID() + << " met at " << crossing.intersectionPoint() + << " cm (plane: L=" << crossing.cathodeLengthUnits() + << " cm, H=" << crossing.cathodeHeightUnits() + << "; P=" << crossing.particleDirUnits() << " cm)"; + } + else { + mf::LogTrace{ fLogCategory } << " - TPC " << plane.TPC().ID() + << " not crossed"; + } + + if (!crossing) continue; + + // is crossing point contained in the (expanded) cathode area? + if (std::abs(crossing.cathodeHeightUnits()) > 1. + fCathodeLengthWiggle) continue; + if (std::abs(crossing.cathodeHeightUnits()) > 1. + fCathodeHeightWiggle) continue; + + // is the point in the future of the particle? + if (crossing.particleDirUnits() < 0) continue; + + return crossing; + } // for cathode planes + + return {}; +} // SelectCathodeCrossingGenParticles::findCrossingTPCcathode() + + +//------------------------------------------------------------------------------ +SelectCathodeCrossingGenParticles::CathodePlane_t +SelectCathodeCrossingGenParticles::buildCathode(geo::TPCGeo const& TPC) { + + // the width direction is not guaranteed to point from anode to cathode nor + // the opposite, so we use the drift direction, which should be parallel. + return { + // center + TPC.GetActiveVolumeCenter() - TPC.DriftDir() * TPC.ActiveHalfWidth() + // length + , TPC.LengthDir() * TPC.ActiveLength() + // height + , TPC.HeightDir() * TPC.ActiveHeight() + , TPC + }; + +} // SelectCathodeCrossingGenParticles::buildCathode() + + +//------------------------------------------------------------------------------ +std::vector +SelectCathodeCrossingGenParticles::buildCathodePlanes + (geo::GeometryCore const& geom) +{ + std::vector cathodePlanes; + cathodePlanes.reserve(geom.TotalNTPC()); + + for (auto const& TPC: geom.Iterate()) + cathodePlanes.emplace_back(buildCathode(TPC)); + + return cathodePlanes; +} // SelectCathodeCrossingGenParticles::buildCathodePlanes() + + +//------------------------------------------------------------------------------ +DEFINE_ART_MODULE(SelectCathodeCrossingGenParticles) + + +//------------------------------------------------------------------------------ + diff --git a/icaruscode/Generators/beamspilltimings.fcl b/icaruscode/Generators/beamspilltimings.fcl index d9932f5fc..2705e04b8 100644 --- a/icaruscode/Generators/beamspilltimings.fcl +++ b/icaruscode/Generators/beamspilltimings.fcl @@ -2,8 +2,8 @@ # File: beamspilltimings.fcl # Purpose: Configuration of FNAL beam time structure for GENIEHelper. # Author: Gianluca Petrillo (petrillo@slac.stanford.edu) -# Date: August 19, 2019 -# Version: 1.0 +# Date: December 4, 2024 +# Version: 2.0 # # Configurations are provided to drive GENIEGen/GENIEHelper to produce a # neutrino interactions with time distribution reflecting the beam time @@ -23,67 +23,30 @@ # ----------------- # # Assuming there is a configuration `generator` for `GENIEGen`, FHiCL -# configuration can be amended by appending to it: +# configuration for a NuMI generation can be amended by appending to it: # -# physics.producers.generator.SpillTimeConfig: @local::FNAL_BnB_default +# physics.producers.generator.SpillTimeConfig: @local::FNAL_NuMI_default # # -# # Provided configurations # ------------------------ # # * `FNAL_BnB_default`: settings from `evgb::EvtTimeFNALBeam` itself +# (suggested to use SBN configuration directly) # * `FNAL_NuMI_default`: settings from `evgb::EvtTimeFNALBeam` itself # # -# Explanation of the configuration parameters -# -------------------------------------------- -# -# This documentation reflects the configuration string of -# `evgb::EvtTimeFNALBeam` class in `nugen` `v1_00_01` (`nutools` `v3_02_00`). -# It is parsed by `evgb::EvtTimeFNALBeam::Config()` (the first word, -# representing the name of the algorithm, is stripped by the caller, -# `evgb::GENIEHelper`). -# The configuration string is, well, a single string, sequence of -# case-insensitive words separated by blanks (space, tabulation or new line). -# Parameters are parsed in sequence and the latter overrides the previous. -# Parameters may appear in any order, except for the algorithm name which must -# be the first. -# -# * `evgb::EvtTimeFNALBeam`: the algorithm name; `evgb::EvtTimeFNALBeam` -# describes a beam spill structured in a contiguous sequence of "batches", -# each one with a substructure of "buckets". Some batches can be "disabled", -# and some of the buckets at the end of each batch may be empty. -# Each bucket has a Gaussian time distribution. See the content of -# `nugen/EventGeneratorBase/GENIE/EvtTime*.h` for other possible algorithms. -# * `intentsity ... ` describes the relative intensity -# of the batches in the spill, and at the same time it defines the number -# _M_ of batches in the spill. A standard setting is to have all the batches -# (6 for NuMI, just 1 for BnB) set to `1.0`; `GENIEHelper` will take care of -# normalizing the numbers to a sum of 1. -# * `sigma` or `FWHM` [ns]: the RMS or full width at half maximum peak of the -# time structure of a single bucket. The time distribution is always -# Gaussian (if specified as FWHM, it is converted to RMS for a Gaussian -# distribution). -# * `dtbucket` [ns]: the time between the peak of two consecutive buckets. -# The default value (18.83 ns) assumes an extraction rate of 53.103 MHz. -# * `nperbatch`, `nfilled`: the number of buckets in each spill, and the number -# of those which have actual protons in them. The default values include -# 84 buckets, of which 81 are filled and the remaining 3, always at the end -# of the spill ("notch") are empty. -# * `global` [ns]: offset of the start of the spill (i.e. the time of the peak -# of the first bucket of the first batch) with respect to the start -# of the generator time scale (see `detinfo::DetectorClocks`). -# * `Booster`, `NuMI`: presets including all the above in hard-coded fashion; -# avoid using these to have better control and awareness of the settings. -# # Changes # -------- # # 20190819 (petrillo@slac.stanford.edu) [v1.0] # explicitly pick settings hard-coded in `evgb::EvtTimeFNALBeam` +# 20241204 (petrillo@slac.stanford.edu) [v2.0] +# delegate to SBN configuration (genie_beam_settings.fcl) # +#include "genie_beam_settings.fcl" + BEGIN_PROLOG ################################################################################ @@ -95,8 +58,9 @@ BEGIN_PROLOG # According to the comments in the code, "2.0 - 2.5 ns width for Booster is # reasonable", and "it is expected that the Booster width >> NuMI width due to # higher electric fields / deeper buckets". +# It was used by ICARUS before merging parameters with SBN. # -FNAL_BnB_default: "evgb::EvtTimeFNALBeam +FNAL_BnB_legacy: "evgb::EvtTimeFNALBeam intensity 1.0 sigma 2.00 dtbucket 18.8313277969 @@ -105,6 +69,12 @@ nfilled 81 global 0.0 " +# +# the default time structure is now taken from SBN; +# configurations should refer to that one (`beam_structure_BNB`) directly. +# +FNAL_BnB_default: @local::beam_structure_BNB.SpillTimeConfig # from `genie_beam_settings.fcl` + ################################################################################ ### NuMI @@ -114,8 +84,9 @@ global 0.0 # `evgb::EvtTimeFNALBeam` on `nugen` `v1_00_01` for NuMI. # According to the comments in the code, "0.75 ns sigma for NuMI comes from # MINOS Time-of-Flight paper though it could be currently ~ 1ns". +# The inter-bunch time has been confirmed by ICARUS (SBN DocDB 34988). # -FNAL_NuMI_default: "evgb::EvtTimeFNALBeam +FNAL_NuMI_legacy: "evgb::EvtTimeFNALBeam intensity 1.0 1.0 1.0 1.0 1.0 1.0 sigma 0.75 dtbucket 18.8313277969 @@ -124,6 +95,13 @@ nfilled 81 global 0.0 " +# +# the default time structure is now taken from SBN; +# configurations should refer to that one (`beam_structure_NuMI`) directly. +# +FNAL_NuMI_default: @local::beam_structure_NuMI.SpillTimeConfig # from `genie_beam_settings.fcl` + + ################################################################################ END_PROLOG diff --git a/icaruscode/PMT/Algorithms/CMakeLists.txt b/icaruscode/PMT/Algorithms/CMakeLists.txt index 01a8b7f25..e2ac0ec24 100644 --- a/icaruscode/PMT/Algorithms/CMakeLists.txt +++ b/icaruscode/PMT/Algorithms/CMakeLists.txt @@ -6,6 +6,7 @@ art_make_library( SOURCE ${lib_srcs} LIBRARIES icaruscode::Decode_DecoderTools + icaruscode::PMT_Calibration icarusalg::Utilities larcorealg::Geometry lardataobj::RawData diff --git a/icaruscode/PMT/Algorithms/DiscretePhotoelectronPulse.cxx b/icaruscode/PMT/Algorithms/DiscretePhotoelectronPulse.cxx index 89f7cce95..6488278f0 100644 --- a/icaruscode/PMT/Algorithms/DiscretePhotoelectronPulse.cxx +++ b/icaruscode/PMT/Algorithms/DiscretePhotoelectronPulse.cxx @@ -18,17 +18,83 @@ #include "messagefacility/MessageLogger/MessageLogger.h" // C++ standard library +#include // std::abs() #include // std::function #include // std::cref() +// ----------------------------------------------------------------------------- +namespace { + + /** + * Returns `true` after seeing a given number of samples closer to baseline + * than a specified threshold (either direction). + */ + template + class CloseToBaselineForClass { + + // configuration + SampleType fMin, fMax; + unsigned int fCountGoal; + + // cache + unsigned int fCount = 0; + + public: + + /// Constructor: specifies `threshold` and `count` (baseline is in `ops`). + template + CloseToBaselineForClass + (SampleType threshold, unsigned int count, WaveformOperations ops) + : fMin{ ops.shiftFromBaseline(-threshold) } + , fMax{ ops.shiftFromBaseline(+threshold) } + , fCountGoal{ count } + { + if (fMin > fMax) std::swap(fMin, fMax); + } + + /// Returns whether there have been `fCountGoal` calls with `sample` + /// close enough to the baseline (`closeEnough()`). + template + bool operator() (TimeType, SampleType sample) + { + if (closeEnough(sample)) ++fCount; + else fCount = 0; + return fCount >= fCountGoal; + } + + /// Returns whether `sample` is close enough to the stored baseline. + constexpr bool closeEnough(SampleType sample) const noexcept + { return (sample >= fMin) && (sample <= fMax); } + + }; // CloseToBaselineForClass + +} // local namespace + + // ----------------------------------------------------------------------------- // --- icarus::opdet::DiscretePhotoelectronPulse +// ----------------------------------------------------------------------------- +icarus::opdet::DiscretePhotoelectronPulse::DiscretePhotoelectronPulse( + PulseFunction_t const& pulseShape, + gigahertz samplingFreq, unsigned int nSubsamples, + ADCcount samplingThreshold, nanoseconds minTimeBelowThreshold + ) + : fShape(pulseShape) + , fSamplingFreq(samplingFreq) + , fSampledShape(sampleShape( + shape(), fSamplingFreq, nSubsamples, + samplingThreshold, + static_cast(std::ceil(minTimeBelowThreshold * samplingFreq)) + )) + {} + + // ----------------------------------------------------------------------------- auto icarus::opdet::DiscretePhotoelectronPulse::sampleShape( PulseFunction_t const& pulseShape, gigahertz samplingFreq, unsigned int nSubsamples, - ADCcount threshold + ADCcount threshold, unsigned int minSamplesBelowThreshold ) -> SampledFunction_t { using namespace util::quantities::time_literals; @@ -38,43 +104,23 @@ auto icarus::opdet::DiscretePhotoelectronPulse::sampleShape( // the pulse polarity is included in the values, // the two functions (lambda) are of different type, so they are being wrapped // in the common `std::function` type - - // FIXME Clang 7.0.0 can't figure out the parameters of std::function - // (GCC 8.2 can): specifying them explicitly... - auto const isBelowThreshold = (pulseShape.polarity() == +1) -#if defined(__clang__) && (__clang_major__ < 8) - ? std::function( -#else // not Clang <8 - ? std::function( -#endif // defined(__clang__) && (__clang_major__ < 8) - [baseline=pulseShape.baseline(), threshold](nanoseconds, ADCcount s) - { - return - PositivePolarityOperations::subtractBaseline(s, baseline) - < threshold - ; - } - ) -#if defined(__clang__) && (__clang_major__ < 8) - : std::function( -#else // not Clang <8 - : std::function( -#endif // defined(__clang__) && (__clang_major__ < 8) - [baseline=pulseShape.baseline(), threshold](nanoseconds, ADCcount s) - { - return - NegativePolarityOperations::subtractBaseline(s, baseline) - < threshold - ; - } - ) + + auto isCloseToBaseline = (pulseShape.polarity() == +1) + ? CloseToBaselineForClass{ + threshold, minSamplesBelowThreshold, + PositivePolarityOperations{ pulseShape.baseline() } + } + : CloseToBaselineForClass{ + threshold, minSamplesBelowThreshold, + NegativePolarityOperations{ pulseShape.baseline() } + } ; return SampledFunction_t{ std::cref(pulseShape), // function to sample (by reference because abstract) 0.0_ns, // sampling start time 1.0 / samplingFreq, // tick duration - isBelowThreshold, // when to stop the sampling + isCloseToBaseline, // when to stop the sampling static_cast(nSubsamples), // how many subsamples per tick pulseShape.peakTime() // sample at least until here }; diff --git a/icaruscode/PMT/Algorithms/DiscretePhotoelectronPulse.h b/icaruscode/PMT/Algorithms/DiscretePhotoelectronPulse.h index 877b7784b..01d41b351 100644 --- a/icaruscode/PMT/Algorithms/DiscretePhotoelectronPulse.h +++ b/icaruscode/PMT/Algorithms/DiscretePhotoelectronPulse.h @@ -92,17 +92,30 @@ class icarus::opdet::DiscretePhotoelectronPulse { * @param nSubsamples (default: `1`) the number of samples within a tick * @param samplingThreshold (default: 10^-6^) pulse shape ends when its * value is below this threshold + * @param minTimeBelowThreshold (default: one sample worth) how long the + * waveform should be below threshold for the sampling to terminate * * Samples start from time 0, which is the time of the start of the first * tick. This time is expected to be the arrival time of the photoelectron. * - * The length of the sampling is determined by the sampling threshold: - * at the first tick after the peak time where the shape function is below - * threshold, the sampling ends (that tick under threshold itself is also - * discarded). + * The length of the sampling is determined by the sampling threshold. + * The sampling is over after the signal has been close to the baseline + * by at least `minTimeBelowThreshold` (excluding the last tick too). + * The closeness is defined on both sides of the baseline (for example a + * bipolar or undershooting signal will not necessarily terminate at the + * baseline crossing time). * * The ownership of `pulseShape` is acquired by this object. */ + DiscretePhotoelectronPulse( + PulseFunction_t const& pulseShape, + gigahertz samplingFreq, + unsigned int nSubsamples, + ADCcount samplingThreshold, + nanoseconds minTimeBelowThreshold + ); + + // version with the default values DiscretePhotoelectronPulse( PulseFunction_t const& pulseShape, gigahertz samplingFreq, @@ -219,7 +232,7 @@ class icarus::opdet::DiscretePhotoelectronPulse { static SampledFunction_t sampleShape( PulseFunction_t const& pulseShape, gigahertz samplingFreq, unsigned int nSubsamples, - ADCcount threshold + ADCcount threshold, unsigned int minSamplesBelowThreshold ); }; // class DiscretePhotoelectronPulse<> @@ -242,7 +255,7 @@ inline icarus::opdet::DiscretePhotoelectronPulse::DiscretePhotoelectronPulse( : fShape(pulseShape) , fSamplingFreq(samplingFreq) , fSampledShape - (sampleShape(shape(), fSamplingFreq, nSubsamples, samplingThreshold)) + (sampleShape(shape(), fSamplingFreq, nSubsamples, samplingThreshold, 1)) {} diff --git a/icaruscode/PMT/Algorithms/FastGaussianNoiseGeneratorAlg.h b/icaruscode/PMT/Algorithms/FastGaussianNoiseGeneratorAlg.h index afb8f97d5..887c44ff7 100644 --- a/icaruscode/PMT/Algorithms/FastGaussianNoiseGeneratorAlg.h +++ b/icaruscode/PMT/Algorithms/FastGaussianNoiseGeneratorAlg.h @@ -39,9 +39,10 @@ namespace icarus::opdet { * Generating for any type `ADCT` other that the CLHEP-native `double` requires * a conversion and slows down the generation. * - * Compared to GaussianNoiseGeneratorAlg(), we use a somehow faster random - * generator; to squeeze the CPU cycles, we avoid the CLHEP interface as much as - * possible; the random number from the engine is immediately converted + * Compared to `icarus::opdet::GaussianNoiseGeneratorAlg`, we use a somehow + * faster Gaussian adaptor, implemented in `util::FastAndPoorGauss`, for random + * number generation; to squeeze the CPU cycles, we avoid the CLHEP interface as + * much as possible; the random number from the engine is immediately converted * to single precision, and the rest of the math happens in there as well. * No virtual interfaces nor indirection is involved within this function * (except for CLHEP random engine and the call to `add()`/`fill()`). We @@ -52,6 +53,12 @@ namespace icarus::opdet { * Note that unless the random engine is multi-thread safe, this function * won't gain anything from multi-threading. * + * @note The cost of the fast adaptor is a resolution worsening on the tails + * of the distribution. With `131072` sampling points (value at the time + * of writing), only 8 values are above 4 standard deviations, with a + * maximum value of `4.62`. With `262144` points there would be 17 values + * above 4 s.d. and a maximum of about `4.76`. + * * @note Despite the name, the generator limits to generate one sample per call. * It may be possible, if this proved to be a limitation, to extend * `util::FastAndPoorGauss` to generate an array of numbers, in the hope @@ -117,7 +124,7 @@ class icarus::opdet::FastGaussianNoiseGeneratorAlg private: - using GaussAdapter_t = util::FastAndPoorGauss<32768U, ADCvalue_t>; + using GaussAdapter_t = util::FastAndPoorGauss<131072U, ADCvalue_t>; // --- BEGIN -- Configuration parameters ------------------------------------- diff --git a/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx b/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx index 0aa9d96cf..e30bf2cc7 100644 --- a/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx +++ b/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx @@ -24,6 +24,7 @@ // CLHEP libraries #include "CLHEP/Random/RandFlat.h" +#include "CLHEP/Random/RandGaussQ.h" #include "CLHEP/Random/RandPoisson.h" #include "CLHEP/Random/RandExponential.h" @@ -37,6 +38,12 @@ #include // std::accumulate +// ----------------------------------------------------------------------------- +using namespace util::quantities::time_literals; +using namespace util::quantities::frequency_literals; +using namespace util::quantities::electronics_literals; + + // ----------------------------------------------------------------------------- #if __cplusplus < 202002L // C++20? namespace util { @@ -106,12 +113,14 @@ icarus::opdet::PMTsimulationAlg::PMTsimulationAlg *(fParams.pulseFunction), fSampling, fParams.pulseSubsamples, // tick subsampling - 1.0e-4_ADCf // stop sampling when ADC counts are below this value + 1.0e-4_ADCf, // stop sampling when signal is closer to baseline than this... + 20_ns // ... for at least this long ) + , fNominalSPEArea(std::abs(fParams.pulseFunction->integral().value())) + , fBiasConstant(fParams.pulseFunction->biasConstant()) , fPedestalGen(fParams.pedestalGen) , fDiscrAlgo(selectDiscriminationAlgo(fParams.discrimAlgo)) { - using namespace util::quantities::electronics_literals; // mf::LogDebug("PMTsimulationAlg") << "Sampling = " << fSampling << std::endl; @@ -140,43 +149,92 @@ icarus::opdet::PMTsimulationAlg::PMTsimulationAlg } // check that the sampled waveform has a sufficiently large range, so that - // tails are below 10^-3 ADC counts (absolute value); + // tails are below 5x10^-2 ADC counts (absolute value); // if this test fails, it's better to reduce the threshold in wsp constructor // (for analytical pulses converging to 0) or have a longer sampling that does - // converge to 0 (10^-3 ADC is quite low though). - wsp.checkRange(1.0e-3_ADCf, "PMTsimulationAlg"); + // converge to 0 (5x10^-2 ADC is quite low though). + wsp.checkRange(5.0e-2_ADCf, "PMTsimulationAlg"); } // icarus::opdet::PMTsimulationAlg::PMTsimulationAlg() // ----------------------------------------------------------------------------- -std::tuple, std::optional> - icarus::opdet::PMTsimulationAlg::simulate(sim::SimPhotons const& photons, - sim::SimPhotonsLite const& lite_photons) +std::tuple< + std::vector, std::vector, + std::optional, + std::optional +> +icarus::opdet::PMTsimulationAlg::simulate(sim::SimPhotons const& photons, + sim::SimPhotonsLite const& lite_photons, + bool enableDebug) { std::optional photons_used; + std::optional debug; + + if (enableDebug) debug.emplace(); - Waveform_t const waveform = CreateFullWaveform(photons, lite_photons, photons_used); - + // [Waveform_t, ADCcount] (baseline is single one for the whole channel) + auto const [ waveform, baseline ] = CreateFullWaveform + (photons, lite_photons, photons_used, enableDebug ? &(*debug) : nullptr); + + std::vector waveforms + = CreateFixedSizeOpDetWaveforms(photons.OpChannel(), waveform); + + std::vector baselines + (waveforms.size(), icarus::WaveformBaseline{ baseline.value() }); + return { - CreateFixedSizeOpDetWaveforms(photons.OpChannel(), waveform), - std::move(photons_used) - }; + std::move(waveforms), std::move(baselines), + std::move(photons_used), std::move(debug) + }; } // icarus::opdet::PMTsimulationAlg::simulate() //------------------------------------------------------------------------------ -auto icarus::opdet::PMTsimulationAlg::makeGainFluctuator() const { +icarus::opdet::PMTsimulationAlg::GainFluctuator::GainFluctuator + (double const refGain, CLHEP::RandPoisson rng) +{ + fFluctuate = [refGain, rng = std::move(rng)](double n) mutable -> double { + return rng.fire(n * refGain) / refGain; + }; +} // GainFluctuator::GainFluctuator(Poisson) - using Fluctuator_t = GainFluctuator; - if (fParams.doGainFluctuations) { - double const refGain = fParams.PMTspecs.firstStageGain(); - return Fluctuator_t - { refGain, CLHEP::RandPoisson{ *fParams.gainRandomEngine, refGain } }; +//------------------------------------------------------------------------------ +icarus::opdet::PMTsimulationAlg::GainFluctuator::GainFluctuator + (double const gainRatio, double const relSigma, CLHEP::RandGaussQ rng) +{ + fFluctuate = [gainRatio, relSigma, rng = std::move(rng)](double n) mutable -> double { + double const result = rng.fire(n * gainRatio, std::sqrt(n) * relSigma); + return (result > 0.0)? result: 0.0; + }; +} // GainFluctuator::GainFluctuator(Gaussian) + + +//------------------------------------------------------------------------------ +auto icarus::opdet::PMTsimulationAlg::makeGainFluctuator(int channel) const + -> GainFluctuator +{ + if (!fParams.doGainFluctuations) return GainFluctuator{}; // identity + + if (fParams.useGainCalibDB && fParams.gainCalibProvider) { + // DB Gaussian: per-channel SPE area and width from database + // fNominalSPEArea is the integral of the SPR template, i.e. the mean area per PE + // fBiasConstant covers the bias in the integral definitions btw SPR template and official reco + double const speArea = fParams.gainCalibProvider->getSPEArea(channel); + double const speFitWidth = fParams.gainCalibProvider->getSPEFitWidth(channel); + // gainRatio = speArea / fNominalSPEArea: mean effective PEs per true PE + // relSigma = speFitWidth / fNominalSPEArea: sigma per sqrt(PE) + double const gainRatio = (fNominalSPEArea > 0.0) ? (speArea * fBiasConstant / fNominalSPEArea): 1.0; + double const relSigma = (fNominalSPEArea > 0.0) ? (speFitWidth / fNominalSPEArea): 0.0; + + return GainFluctuator{gainRatio, relSigma, CLHEP::RandGaussQ{ *fParams.gainRandomEngine, gainRatio, relSigma }}; } - else return Fluctuator_t{}; // default-constructed does not fluctuate anything + + // Legacy Poisson mode: fluctuate around first-dynode gain + double const refGain = fParams.PMTspecs.firstStageGain(); + return GainFluctuator{refGain, CLHEP::RandPoisson{ *fParams.gainRandomEngine, refGain }}; } // icarus::opdet::PMTsimulationAlg::makeGainFluctuator() @@ -185,13 +243,11 @@ auto icarus::opdet::PMTsimulationAlg::makeGainFluctuator() const { auto icarus::opdet::PMTsimulationAlg::CreateFullWaveform (sim::SimPhotons const& photons, sim::SimPhotonsLite const& lite_photons, - std::optional& photons_used) - const -> Waveform_t + std::optional& photons_used, + icarus::opdet::DebugInfo* debug) + const -> std::tuple { - using namespace util::quantities::time_literals; - using namespace util::quantities::frequency_literals; - using namespace util::quantities::electronics_literals; using namespace detinfo::timescales; detinfo::DetectorTimings const& timings = *(fParams.detTimings); @@ -201,6 +257,29 @@ auto icarus::opdet::PMTsimulationAlg::CreateFullWaveform raw::Channel_t const channel = photons.OpChannel(); + // + // Preparing the total time delay for this optical channel. + // We get the delay from the timing corrections used in data. + // Corrections are additive, i.e. the service returns negative values. + // Need to flip the sign to use them as delays. + // + microseconds timeDelay = 0_us; + if(fParams.doTimingDelays) { + timeDelay -= microseconds{ fParams.timingDelays->getLaserCorrections(channel) }; + timeDelay -= microseconds{ fParams.timingDelays->getCosmicsCorrections(channel) }; + } + + if (debug) { + debug->opChannel = channel; + debug->timeDelay_us = timeDelay.value(); + debug->triggerOffsetPMT_us = fParams.triggerOffsetPMT.value(); + debug->nSamples = fNsamples; + debug->nSubsamples = wsp.nSubsamples(); + debug->photons.clear(); + debug->peDeposits.clear(); + debug->waveform.clear(); + } + // // collect the amount of photoelectrons arriving at each subtick; // the waveform is split in groups of photons at the same relative subtick @@ -231,20 +310,36 @@ auto icarus::opdet::PMTsimulationAlg::CreateFullWaveform trigger_time const mytime = timings.toTriggerTime(photonTime) - fParams.triggerOffsetPMT + + timeDelay ; if ((mytime < 0.0_us) || (mytime >= fParams.readoutEnablePeriod)) continue; auto const [ tick, subtick ] = toTickAndSubtick(mytime.quantity() * fSampling); + /* mf::LogTrace("PMTsimulationAlg") - << "Photon at " << photonTime << ", optical time " << mytime - << " => tick " << tick_d - << " => sample " << tick << " subsample " << subtick + << "Channel " << channel << ": photon at " << photonTime << ", optical time " << mytime + << " (time delay " << timeDelay << ") " + << " => tick " << tick << " subtick " << subtick ; */ + if (tick >= endSample) continue; ++peMaps[subtick][tick]; + + if (debug) { + icarus::opdet::DebugPhoton dbgPh; + dbgPh.startX = ph.InitialPosition.X(); + dbgPh.startY = ph.InitialPosition.Y(); + dbgPh.startZ = ph.InitialPosition.Z(); + dbgPh.simTime_ns = ph.Time; + dbgPh.trigTime_us = mytime.value(); + dbgPh.tick = tick.value(); + dbgPh.subtick = subtick; + debug->photons.push_back(std::move(dbgPh)); + } + } // for photons // auto end = std::chrono::high_resolution_clock::now(); @@ -269,7 +364,9 @@ auto icarus::opdet::PMTsimulationAlg::CreateFullWaveform simulation_time const photonTime { time_ns + 0.5 }; trigger_time const mytime = timings.toTriggerTime(photonTime) - - fParams.triggerOffsetPMT; + - fParams.triggerOffsetPMT + + timeDelay + ; if ((mytime < 0.0_us) || (mytime >= fParams.readoutEnablePeriod)) continue; auto const [ tick, subtick ] @@ -286,7 +383,7 @@ auto icarus::opdet::PMTsimulationAlg::CreateFullWaveform unsigned int nTotalPE [[maybe_unused]] = 0U; // unused if not in `debug` mode double nTotalEffectivePE [[maybe_unused]] = 0U; // unused if not in `debug` mode - auto gainFluctuation = makeGainFluctuator(); + auto gainFluctuation = makeGainFluctuator(channel); // go though all subsamples (starting each at a fraction of a tick) for (auto const& [ iSubsample, peMap ]: util::enumerate(peMaps)) { @@ -304,10 +401,21 @@ auto icarus::opdet::PMTsimulationAlg::CreateFullWaveform subsample, waveform, startTick, static_cast(nEffectivePE) ); - // std::cout << "Channel=" << channel << ", subsample=" << iSubsample << ", tick=" << startTick << ", nPE=" << nPE << ", ePE=" << nEffectivePE << std::endl; + //std::cout << "Channel=" << channel << ", subsample=" << iSubsample << ", tick=" << startTick + // << ", nPE=" << nPE << ", ePE=" << nEffectivePE << std::endl; + + if (debug) { + DebugPEDeposit dep; + dep.tick = startTick.value(); + dep.subtick = iSubsample; + dep.nPE = nPE; + dep.nEffectivePE = nEffectivePE; + debug->peDeposits.push_back(std::move(dep)); + } } // for sample } // for subsamples + MF_LOG_TRACE("PMTsimulationAlg") << nTotalPE << " photoelectrons at " << std::accumulate( @@ -322,7 +430,7 @@ auto icarus::opdet::PMTsimulationAlg::CreateFullWaveform // start=std::chrono::high_resolution_clock::now(); AddPedestal(channel, waveformStartTS, waveform); - if(fParams.darkNoiseRate > 0.0_Hz) AddDarkNoise(waveform); + if(fParams.darkNoiseRate > 0.0_Hz) AddDarkNoise(waveform, channel); // end=std::chrono::high_resolution_clock::now(); diff = end-start; // std::cout << "\tadded noise... " << channel << " " << diff.count() << std::endl; @@ -340,7 +448,13 @@ auto icarus::opdet::PMTsimulationAlg::CreateFullWaveform // end=std::chrono::high_resolution_clock::now(); diff = end-start; // std::cout << "\tadded saturation... " << channel << " " << diff.count() << std::endl; - return waveform; + if(debug){ + for(std::size_t i=0; iwaveform.push_back(waveform[i].value()); + } + } + + return { std::move(waveform), baseline }; } // CreateFullWaveform() auto icarus::opdet::PMTsimulationAlg::CreateBeamGateTriggers() const @@ -650,7 +764,7 @@ void icarus::opdet::PMTsimulationAlg::AddPedestal // ----------------------------------------------------------------------------- -void icarus::opdet::PMTsimulationAlg::AddDarkNoise(Waveform_t& wave) const { +void icarus::opdet::PMTsimulationAlg::AddDarkNoise(Waveform_t& wave, int channel) const { /* * We assume leakage current ("dark noise") is completely stochastic and * distributed uniformly in time with a fixed and known rate. @@ -681,7 +795,7 @@ void icarus::opdet::PMTsimulationAlg::AddDarkNoise(Waveform_t& wave) const { TimeToTickAndSubtickConverter const toTickAndSubtick(wsp.nSubsamples()); - auto gainFluctuation = makeGainFluctuator(); + auto gainFluctuation = makeGainFluctuator(channel); MF_LOG_TRACE("PMTsimulationAlg") << "Adding dark noise (" << fParams.darkNoiseRate << ") up to " << maxTime; @@ -825,16 +939,6 @@ auto icarus::opdet::PMTsimulationAlg::TimeToTickAndSubtickConverter::operator() } // icarus::opdet::PMTsimulationAlg::TimeToTickAndSubtickConverter::operator() -// ----------------------------------------------------------------------------- -template -double icarus::opdet::PMTsimulationAlg::GainFluctuator::operator() - (double const n) -{ - return fRandomGain - ? (fRandomGain->fire(n * fReferenceGain) / fReferenceGain): n; -} - - // ----------------------------------------------------------------------------- // --- icarus::opdet::PMTsimulationAlgMaker // ----------------------------------------------------------------------------- @@ -886,6 +990,8 @@ icarus::opdet::PMTsimulationAlgMaker::PMTsimulationAlgMaker (PMTspecs.VoltageDistribution()); fBaseConfig.PMTspecs.gain = PMTspecs.Gain(); fBaseConfig.doGainFluctuations = config.FluctuateGain(); + fBaseConfig.useGainCalibDB = config.UseGainDatabase(); + fBaseConfig.doTimingDelays = config.ApplyTimingDelays(); // // single photoelectron response @@ -940,6 +1046,8 @@ icarus::opdet::PMTsimulationAlgMaker::operator()( std::uint64_t beamGateTimestamp, detinfo::LArProperties const& larProp, detinfo::DetectorClocksData const& clockData, + icarusDB::PMTTimingCorrections const* timingDelays, + icarusDB::PhotonCalibratorFromDB const* gainCalibProvider, SinglePhotonResponseFunc_t const& SPRfunction, PedestalGenerator_t& pedestalGenerator, CLHEP::HepRandomEngine& mainRandomEngine, @@ -950,7 +1058,7 @@ icarus::opdet::PMTsimulationAlgMaker::operator()( { return std::make_unique(makeParams( beamGateTimestamp, - larProp, clockData, + larProp, clockData, timingDelays, gainCalibProvider, SPRfunction, pedestalGenerator, mainRandomEngine, darkNoiseRandomEngine, elecNoiseRandomEngine, trackSelectedPhotons @@ -964,6 +1072,8 @@ auto icarus::opdet::PMTsimulationAlgMaker::makeParams( std::uint64_t beamGateTimestamp, detinfo::LArProperties const& larProp, detinfo::DetectorClocksData const& clockData, + icarusDB::PMTTimingCorrections const* timingDelays, + icarusDB::PhotonCalibratorFromDB const* gainCalibProvider, SinglePhotonResponseFunc_t const& SPRfunction, PedestalGenerator_t& pedestalGenerator, CLHEP::HepRandomEngine& mainRandomEngine, @@ -972,7 +1082,6 @@ auto icarus::opdet::PMTsimulationAlgMaker::makeParams( bool trackSelectedPhotons /* = false */ ) const -> PMTsimulationAlg::ConfigurationParameters_t { - using namespace util::quantities::electronics_literals; // // set the configuration @@ -987,6 +1096,11 @@ auto icarus::opdet::PMTsimulationAlgMaker::makeParams( params.larProp = &larProp; params.clockData = &clockData; params.detTimings = detinfo::makeDetectorTimings(params.clockData); + params.timingDelays = timingDelays; + params.gainCalibProvider = gainCalibProvider; + + assert(!params.doTimingDelays || params.timingDelays); + assert(!params.useGainCalibDB || params.gainCalibProvider); params.pulseFunction = &SPRfunction; params.pedestalGen = &pedestalGenerator; diff --git a/icaruscode/PMT/Algorithms/PMTsimulationAlg.h b/icaruscode/PMT/Algorithms/PMTsimulationAlg.h index 152878a73..71b66c72c 100644 --- a/icaruscode/PMT/Algorithms/PMTsimulationAlg.h +++ b/icaruscode/PMT/Algorithms/PMTsimulationAlg.h @@ -19,8 +19,11 @@ #include "icaruscode/PMT/Algorithms/DiscretePhotoelectronPulse.h" #include "icaruscode/PMT/Algorithms/PhotoelectronPulseFunction.h" #include "icaruscode/PMT/Algorithms/PedestalGeneratorAlg.h" +#include "icaruscode/PMT/Calibration/PhotonCalibratorFromDB.h" +#include "icaruscode/Timing/PMTTimingCorrections.h" #include "icaruscode/Utilities/quantities_utils.h" // util::value_t #include "icarusalg/Utilities/SampledFunction.h" +#include "sbnobj/ICARUS/PMT/Data/WaveformBaseline.h" // LArSoft libraries #include "lardataobj/RawData/OpDetWaveform.h" @@ -45,8 +48,11 @@ // CLHEP libraries #include "CLHEP/Random/RandEngine.h" // CLHEP::HepRandomEngine +#include "CLHEP/Random/RandGaussQ.h" +#include "CLHEP/Random/RandPoisson.h" // C++ standard library +#include // std::function #include #include #include @@ -79,9 +85,36 @@ namespace icarus::opdet { class PMTsimulationAlg; class PMTsimulationAlgMaker; - -} // namespace icarus::opdet + struct DebugPhoton { + float startX = 0.f; // photon start X position [cm] + float startY = 0.f; // photon start Y position [cm] + float startZ = 0.f; // photon start Z position [cm] + float simTime_ns = 0.f; // input photon time (simulation) + float trigTime_us = 0.f; // timings.toTriggerTime(...) - offset + delay + int32_t tick = -1; // tick index (optical clock tick) + uint16_t subtick = 0; // subsample index + }; + + struct DebugPEDeposit { + int32_t tick = -1; // where the PE got deposited + uint16_t subtick = 0; + uint16_t nPE = 0; // integer PE count + float nEffectivePE = 0.f; // after gain fluctuation (what you actually used) + float gainFactor() const { return (nPE > 0) ? (nEffectivePE / float(nPE)) : 0.f; } + }; + + struct DebugInfo { + uint32_t opChannel = 0; + float timeDelay_us = 0.f; + float triggerOffsetPMT_us = 0.f; + uint32_t nSamples = 0; + uint16_t nSubsamples = 0; + std::vector photons; // per-photon bookkeeping + std::vector peDeposits; // aggregated PE deposits actually used to build waveform + std::vector> waveform; // waveform data (ADC counts) + }; +} // namespace icarus::opdet // ----------------------------------------------------------------------------- /// Helper class to cut a `raw::OpDetWaveform` from a longer waveform data. @@ -183,6 +216,11 @@ class icarus::opdet::OpDetWaveformMakerClass { * For each converting photon, a photoelectron is added to the channel by * placing a template waveform shape into the channel waveform. * + * If enabled by `ApplyTimingDelays`, the timing correction service + * `icarusDB::IPMTTimingCorrectionService` is used to simulate the chain of + * delays between the photon hitting the photocathode and the time its signal + * is digitized. The delay is dominated by the signal cable length (~200ns). + * * The timestamp of each waveform is based on the same scale as the trigger * time, as defined by `detinfo::DetectorClocks::TriggerTime()`. * On that scale, the timestamp pins down the time of the first sample of @@ -218,6 +256,10 @@ class icarus::opdet::OpDetWaveformMakerClass { * to a nominal gain (`PMTspecs.gain` configuration parameter), which is * then fluctuated to obtain the effective gain. This feature can be * disabled by setting configuration parameter `FluctuateGain` to `false`. + * + * Two gain fluctuation models are available: + * + * **Legacy Poisson model** (default, `UseGainDatabase: false`): * The approximation used here is that the fluctuation is entirely due to * the first stage of multiplication. The gain on the first stage is * described as a random variable with Poisson distribution around the mean @@ -238,6 +280,19 @@ class icarus::opdet::OpDetWaveformMakerClass { * The first stage gain is computed by * `icarus::opdet::PMTsimulationAlg::ConfigurationParameters_t::PMTspecs_t::multiplicationStageGain()`. * + * **Database gain model** (`UseGainDatabase: true`): + * When this mode is enabled the algorithm reads, for each PMT channel, + * the measured SPE area and SPE area width from the calibration database via + * the `calib::ICARUSPhotonCalibratorServiceFromDB` service. + * The number of effective photoelectrons added per true photoelectron is + * drawn from a Gaussian whose mean is the ratio of the channel SPE area + * to the nominal SPR template area, and whose standard + * deviation is the ratio of the channel SPE width to that nominal area. + * This model preserves the mean deposited charge per photoelectron to + * match the measured per-channel gain, while the nominal gain set in + * `PMTspecs` and in `SinglePhotonResponse` cancels out and can be left + * at any convenient value across run periods. + * * * Dark noise * ----------- @@ -372,6 +427,7 @@ class icarus::opdet::PMTsimulationAlg { public: using microsecond = util::quantities::microsecond; + using microseconds = util::quantities::intervals::microseconds; using nanosecond = util::quantities::nanosecond; using hertz = util::quantities::hertz; using megahertz = util::quantities::megahertz; @@ -475,7 +531,9 @@ class icarus::opdet::PMTsimulationAlg { hertz darkNoiseRate; float saturation; //equivalent to the number of p.e. that saturates the electronic signal PMTspecs_t PMTspecs; ///< PMT specifications. - bool doGainFluctuations; ///< Whether to simulate fain fluctuations. + bool doGainFluctuations; ///< Whether to simulate gain fluctuations. + bool useGainCalibDB; ///< Whether to use per-channel DB gain for fluctuations. + bool doTimingDelays; ///< Whether to simulate timing delays. /// @} /// @{ @@ -487,7 +545,13 @@ class icarus::opdet::PMTsimulationAlg { detinfo::LArProperties const* larProp = nullptr; ///< LarProperties service provider. detinfo::DetectorClocksData const* clockData = nullptr; - + + /// Timing delays service interfacing with database + icarusDB::PMTTimingCorrections const* timingDelays = nullptr; + + /// SPE calibration provider for per-channel gain (nullptr = disabled). + icarusDB::PhotonCalibratorFromDB const* gainCalibProvider = nullptr; + // detTimings is not really "optional" but it needs delayed construction. /// Detector clocks data wrapper. std::optional detTimings; @@ -538,20 +602,36 @@ class icarus::opdet::PMTsimulationAlg { /** * @brief Returns the waveforms originating from simulated photons. * @param photons all the photons simulated to land on the channel + * @param lite_photons all the photons simulated to land on the channel + * (compact version) + * @param enableDebug (default: `false`) also return debug information * @return a list of optical waveforms, response to those photons, - * and which photons were used (if requested) + * the baseline of each waveform, + * and which photons were used (if requested), + * and debug information (if requested) * * Due to threshold readout, a single channel may result in multiple * waveforms, which are all on the same channel but disjunct in time. + * For each of the waveforms, a baseline object is also returned, with the + * value from `icarus::opdet::PedestalGeneratorAlg::pedestalLevel()`. + * + * The function takes both `sim::SimPhotons` and `sim::SimPhotonsLite` as + * mandatory input, but the latter is used only if the former collection is + * empty; otherwise, lite photon input is ignored. * - * The second element of the return value is optional and filled only + * The third element of the return value is optional and filled only * if the `trackSelectedPhotons` configuration parameter is set to `true`. * In that case, the returned `sim::SimPhotons` contains a copy of each of * the `photons` contributing to any of the waveforms. */ - std::tuple, std::optional> + std::tuple< + std::vector, std::vector, + std::optional, + std::optional + > simulate(sim::SimPhotons const& photons, - sim::SimPhotonsLite const& lite_photons); + sim::SimPhotonsLite const& lite_photons, + bool enableDebug = false); /// Prints the configuration into the specified output stream. template @@ -601,28 +681,39 @@ class icarus::opdet::PMTsimulationAlg { }; // TimeToTickAndSubtickConverter - /// Applies a random gain fluctuation to the specified number of - /// photoelectrons. - template + /** + * @brief Applies a random gain fluctuation to a number of photoelectrons. + * + * Two modes are supported, selected at construction time: + * - **Legacy Poisson** (doGainFluctuations = true, useGainCalibDB = false): + * models first-dynode statistics via Poisson(n*refGain)/refGain.. + * - **DB Gaussian** (doGainFluctuations = true, useGainCalibDB = true): + * draws from Normal(n*gainRatio, sqrt(n)*relSigma) using per-channel DB values. + * - **Identity** (default-constructed): returns n unchanged. + */ class GainFluctuator { - std::optional fRandomGain; ///< Random gain extractor (optional). - double const fReferenceGain = 0.0; ///< Reference (average) gain. + /// The fluctuation callable; empty means identity (no fluctuation). + std::function fFluctuate; public: + /// Identity fluctuator: returns `n` unchanged. GainFluctuator() = default; - GainFluctuator(double const refGain, Rand&& randomGain) - : fRandomGain(std::move(randomGain)) - , fReferenceGain(refGain) - {} - /// Returns the new number of photoelectrons after fluctuation from `n`. - double operator() (double const n); + /// Legacy Poisson mode: Poisson(n*refGain) / refGain. + GainFluctuator(double refGain, CLHEP::RandPoisson rng); + + /// DB Gaussian mode: Normal(n*gainRatio, sqrt(n)*relSigma), clamped positive. + GainFluctuator(double gainRatio, double relSigma, CLHEP::RandGaussQ rng); + + /// Returns the fluctuated number of effective photoelectrons from n. + double operator()(double n) const + { return fFluctuate? fFluctuate(n): n; } }; // GainFluctuator - /// Returns a configured gain fluctuator object. - auto makeGainFluctuator() const; + /// Returns a gain fluctuator configured for the given optical channel. + GainFluctuator makeGainFluctuator(int channel) const; // --- END -- Helper functors ------------------------------------------------ @@ -633,7 +724,10 @@ class icarus::opdet::PMTsimulationAlg { megahertz fSampling; ///< Wave sampling frequency [MHz]. std::size_t fNsamples; ///< Samples per waveform. - DiscretePhotoelectronPulse wsp; /// Single photon pulse (sampled). + DiscretePhotoelectronPulse wsp; ///< Single photon pulse (sampled). + + double fNominalSPEArea = 0.0; ///< Integral of the SPR template qâ‚€ [ADC×tick]. + double fBiasConstant = 1.0; ///< Bias constant from the FHiCL configuration. /// Pedestal and electronics noise generator algorithm. PedestalGenerator_t* fPedestalGen = nullptr; @@ -643,10 +737,10 @@ class icarus::opdet::PMTsimulationAlg { /** - * @brief Creates `raw::OpDetWaveform` objects from simulated photoelectrons. + * @brief Creates a waveform (sample sequence) from simulated photoelectrons. * @param photons the simulated list of photoelectrons - * @param photons_used (_output_) list of used photoelectrons - * @return a collection of digitised `raw::OpDetWaveform` objects + * @param[out] photons_used list of used photoelectrons + * @return a waveform and its baseline * * This function performs the digitization of a optical detector channel which * is collecting the photoelectrons in the `photons` list. @@ -658,13 +752,12 @@ class icarus::opdet::PMTsimulationAlg { * The `photons_used` output argument is constructed and filled only if the * configuration of the algorithm requires the creation of a list of used * photons. - * */ - Waveform_t CreateFullWaveform( + std::tuple CreateFullWaveform( sim::SimPhotons const& photons, sim::SimPhotonsLite const& lite_photons, - std::optional& photons_used - ) const; + std::optional& photons_used, + icarus::opdet::DebugInfo* debug) const; /** * @brief Creates `raw::OpDetWaveform` objects from a waveform data. @@ -771,7 +864,7 @@ class icarus::opdet::PMTsimulationAlg { (raw::Channel_t channel, std::uint64_t time, Waveform_t& wave) const; // Add "dark" noise to baseline. - void AddDarkNoise(Waveform_t& wave) const; + void AddDarkNoise(Waveform_t& wave, int channel) const; /** @@ -998,6 +1091,16 @@ class icarus::opdet::PMTsimulationAlgMaker { Comment("include gain fluctuation in the photoelectron response"), true }; + fhicl::Atom ApplyTimingDelays { + Name("ApplyTimingDelays"), + Comment("add timing delays (cable, transit time) to photon times"), + true + }; + fhicl::Atom UseGainDatabase { + Name("UseGainDatabase"), + Comment("use per-channel SPE area and width from calibration DB for gain fluctuations"), + true + }; // // single photoelectron response @@ -1068,6 +1171,7 @@ class icarus::opdet::PMTsimulationAlgMaker { * @param beamGateTimestamp the time of beam gate opening, in UTC [ns] * @param larProp instance of `detinfo::LArProperties` to be used * @param detClocks instance of `detinfo::DetectorClocks` to be used + * @param timingDelays instance of `icarusDB::PMTTimingCorrections` to be used * @param SPRfunction function to use for the single photon response * @param pedestalGenerator algorithm generating the pedestal plus noise * @param mainRandomEngine main random engine (quantum efficiency, etc.) @@ -1083,6 +1187,8 @@ class icarus::opdet::PMTsimulationAlgMaker { std::uint64_t beamGateTimestamp, detinfo::LArProperties const& larProp, detinfo::DetectorClocksData const& detClocks, + icarusDB::PMTTimingCorrections const* timingDelays, + icarusDB::PhotonCalibratorFromDB const* gainCalibProvider, SinglePhotonResponseFunc_t const& SPRfunction, PedestalGenerator_t& pedestalGenerator, CLHEP::HepRandomEngine& mainRandomEngine, @@ -1096,6 +1202,7 @@ class icarus::opdet::PMTsimulationAlgMaker { * @param beamGateTimestamp the time of beam gate opening, in UTC [ns] * @param larProp instance of `detinfo::LArProperties` to be used * @param detClocks instance of `detinfo::DetectorClocks` to be used + * @param timingDelays instance of `icarusDB::PMTTimingCorrections` to be used * @param SPRfunction function to use for the single photon response * @param pedestalGenerator algorithm generating the pedestal plus noise * @param mainRandomEngine main random engine (quantum efficiency, etc.) @@ -1113,6 +1220,8 @@ class icarus::opdet::PMTsimulationAlgMaker { std::uint64_t beamGateTimestamp, detinfo::LArProperties const& larProp, detinfo::DetectorClocksData const& clockData, + icarusDB::PMTTimingCorrections const* timingDelays, + icarusDB::PhotonCalibratorFromDB const* gainCalibProvider, SinglePhotonResponseFunc_t const& SPRfunction, PedestalGenerator_t& pedestalGenerator, CLHEP::HepRandomEngine& mainRandomEngine, @@ -1163,12 +1272,12 @@ raw::OpDetWaveform icarus::opdet::OpDetWaveformMakerClass::create // create a new waveform preallocating enough room for the full buffer raw::OpDetWaveform outputWaveform(timeStamp, opChannel, end - start); - // copy the buffer (need to unwrap the ADCcount value) + // copy the buffer (need to unwrap the ADCcount value; +0.5+truncate => round) std::transform( fWaveform.begin() + start, fWaveform.begin() + end, std::back_inserter(outputWaveform), - [](auto sample){ return sample.value(); } + [](auto sample){ return (sample + Sample_t{ 0.5f }).value(); } ); return outputWaveform; @@ -1185,7 +1294,7 @@ void icarus::opdet::PMTsimulationAlg::printConfiguration using namespace util::quantities::electronics_literals; out - << indent << "ADC bits: " << fParams.ADCbits + << indent << "ADC bits: " << fParams.ADCbits << " (" << fParams.ADCrange().first << " -- " << fParams.ADCrange().second << ")" << '\n' << indent << "ReadoutWindowSize: " << fParams.readoutWindowSize << " ticks" @@ -1196,27 +1305,35 @@ void icarus::opdet::PMTsimulationAlg::printConfiguration << '\n' << indent << "Saturation: " << fParams.saturation << " p.e." << '\n' << indent << "doGainFluctuations: " << std::boolalpha << fParams.doGainFluctuations - << '\n' << indent << "PulsePolarity: " << ((fParams.pulsePolarity == 1)? "positive": "negative") << " (=" << fParams.pulsePolarity << ")" + << '\n' << indent << "useGainCalibDB: " + << std::boolalpha << fParams.useGainCalibDB + << '\n' << indent << "doTimingDelays: " + << std::boolalpha << fParams.doTimingDelays + << '\n' << indent << "PulsePolarity: " + << ((fParams.pulsePolarity == 1)? "positive": "negative") + << " (=" << fParams.pulsePolarity << ")" << '\n' << indent << "Sampling: " << fSampling; if (fParams.pulseSubsamples > 1U) out << " (subsampling: x" << fParams.pulseSubsamples << ")"; out << '\n' << indent << "Samples/waveform: " << fNsamples << " ticks" << '\n' << indent << "Gain at first stage: " << fParams.PMTspecs.firstStageGain() + << '\n' << indent << "SPR nominal area: " << fNominalSPEArea << " ADC x tick" + << '\n' << indent << "Bias ratio: " << fBiasConstant ; out << '\n' << indent << "Pedestal: " << fPedestalGen->toString(indent + " ", ""); if (fParams.createBeamGateTriggers) { out << '\n' << indent << "Create " << fParams.beamGateTriggerNReps - << " beam gate triggers, one every " << fParams.beamGateTriggerRepPeriod << "."; + << " beam gate triggers, one every " << fParams.beamGateTriggerRepPeriod << "."; } else out << '\n' << indent << "Do not create beam gate triggers."; out << '\n' << indent << "... and more."; out << '\n' << indent << "Template photoelectron waveform settings:" - << '\n'; + << '\n'; wsp.dump(std::forward(out), indent + " "); out << '\n' << indent << "Track used photons: " diff --git a/icaruscode/PMT/Algorithms/PhotoelectronPulseFunction.h b/icaruscode/PMT/Algorithms/PhotoelectronPulseFunction.h index 78c6f9036..2f3139477 100644 --- a/icaruscode/PMT/Algorithms/PhotoelectronPulseFunction.h +++ b/icaruscode/PMT/Algorithms/PhotoelectronPulseFunction.h @@ -75,6 +75,24 @@ class icarus::opdet::PhotoelectronPulseFunction { /// Returns the polarity of the pulse (`+1`: positive, or `-1`: negative). int polarity() const { return doPolarity(); } + /** + * @brief Returns the integral of the discretised pulse [ADC × tick]. + * + * For sampled implementations this is the exact sum of all stored samples. + * The default implementation returns zero; concrete classes that support + * integration should override this method. + */ + ADCcount integral() const { return doIntegral(); } + + /** + * @brief Returns a bias constant associated with this pulse. + * + * The physical interpretation of this constant is defined by the concrete + * implementation and the FHiCL configuration. Returns `1.0` if the + * implementation does not define one. + */ + double biasConstant() const { return doBiasConstant(); } + // @{ /** @@ -137,6 +155,12 @@ class icarus::opdet::PhotoelectronPulseFunction { virtual int doPolarity() const { return ((peakAmplitude() - baseline()) >= ADCcount{ 0 })? +1: -1; } + /// Returns the integral of the discretised pulse [ADC × tick]. + virtual ADCcount doIntegral() const { return ADCcount{ 0 }; } + + /// Returns the bias constant associated with this pulse (1.0 if not set). + virtual double doBiasConstant() const { return 1.0; } + /** * @brief Prints into the stream the parameters of this shape. diff --git a/icaruscode/PMT/Algorithms/SampledWaveformFunction.h b/icaruscode/PMT/Algorithms/SampledWaveformFunction.h index a232c4e96..c1675c013 100644 --- a/icaruscode/PMT/Algorithms/SampledWaveformFunction.h +++ b/icaruscode/PMT/Algorithms/SampledWaveformFunction.h @@ -63,7 +63,7 @@ class icarus::opdet::SampledWaveformFunction struct WaveformSpecs_t { std::string name { "" }; ///< Name of this waveform (short). std::string description; ///< Description of this waveform. - std::string date { "n/a" }; ///< An indication of the date of the waveform. + std::string date { "n/a" }; ///< An indication of the date of the waveform. unsigned int version { 1 }; ///< Version number. std::vector samples; ///< Samples [mV] Time sampleDuration; ///< Time extension of one sample. @@ -86,7 +86,9 @@ class icarus::opdet::SampledWaveformFunction * The polarity of the waveform is deduced by the value at peak. * */ - SampledWaveformFunction(WaveformSpecs_t specs, Time peakTime, float gain); + SampledWaveformFunction( + WaveformSpecs_t specs, Time peakTime, float gain, + double biasRatio = 1.0); /// @{ /// @name Parameter accessors. @@ -94,6 +96,12 @@ class icarus::opdet::SampledWaveformFunction /// Returns the gain the waveform is representing. float gain() const { return fGain; } + /// Returns the integral of the waveform [ADC × tick]. + ADCcount doIntegral() const override; + + /// Returns the bias constant as set in the FHiCL configuration. + double doBiasConstant() const override { return fBiasRatio; } + /// @} private: @@ -103,6 +111,8 @@ class icarus::opdet::SampledWaveformFunction std::vector const fSamples; ///< All samples. float const fGain; ///< The gain this waveform represents. + + double const fBiasRatio; ///< User-configurable bias ratio from FHiCL. std::size_t const fPeakSample; ///< The sample with the absolute peak. @@ -146,11 +156,7 @@ class icarus::opdet::SampledWaveformFunction /// Returns whether a sample with the specified index is within the range. bool hasSample(std::ptrdiff_t index) const { return (index >= 0) && (std::size_t(index) < fSamples.size()); } - - - /// Returns the integral of the waveform. - ADCcount integral() const; - + /** * @brief Transforms the input waveform. * @param waveform input waveform (in millivolt and for a known gain) @@ -174,12 +180,13 @@ class icarus::opdet::SampledWaveformFunction // ----------------------------------------------------------------------------- template icarus::opdet::SampledWaveformFunction::SampledWaveformFunction - (WaveformSpecs_t waveformSpecs, Time peakTime, float gain) - : fSource { std::move(waveformSpecs) } - , fSamples { buildSamples(gain) } - , fGain { gain } - , fPeakSample{ findPeak(fSamples) } - , fRefTime { peakTime - fPeakSample * sampleDuration() } + (WaveformSpecs_t waveformSpecs, Time peakTime, float gain, double biasRatio) + : fSource { std::move(waveformSpecs) } + , fSamples { buildSamples(gain) } + , fGain { gain } + , fBiasRatio { biasRatio } + , fPeakSample { findPeak(fSamples) } + , fRefTime { peakTime - fPeakSample * sampleDuration() } {} @@ -212,7 +219,7 @@ void icarus::opdet::SampledWaveformFunction::doDump( << " with amplitude " << Base_t::peakAmplitude() << "\n" << indent << " start at " << fRefTime << ", gain " << fGain - << " (integral: " << integral() << ")" + << " (integral: " << doIntegral() << ")" << '\n' ; @@ -221,7 +228,7 @@ void icarus::opdet::SampledWaveformFunction::doDump( // ----------------------------------------------------------------------------- template -auto icarus::opdet::SampledWaveformFunction::integral() const -> ADCcount +auto icarus::opdet::SampledWaveformFunction::doIntegral() const -> ADCcount { return std::reduce(fSamples.begin(), fSamples.end()); } diff --git a/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl b/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl index 7f4f695f8..dfd6948bc 100644 --- a/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl +++ b/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl @@ -5,7 +5,9 @@ # Configurations offered: # # * `PMTsimulationAlg` algorithm: -# * `icarus_pmtsimulationalg_standard`: main configuration, expected to be +# * `icarus_pmtsimulationalg_standard_run2`: main configuration for Run1/2, expected to be +# used in production and by the general user +# * `icarus_pmtsimulationalg_standard_run4`: main configuration for Run3/4/5+, expected to be # used in production and by the general user # * `icarus_pmtsimulationalg_202202`: a general use configuration, # including dark and electronics noise; `noise` and `nonoise` variants @@ -13,6 +15,10 @@ # * `icarus_pmtsimulationalg_2018`: legacy configuration (until June 2022), # including dark and electronics noise; `noise` and `nonoise` variants # (the latter with dark and electronics noise disabled) +# * `icarus_pmtsimulationalg_run2_2025`: run1/2 configuration (developed in 2025), +# including noise, new response function and gain tune. +# * `icarus_pmtsimulationalg_run4_2025`: run3/4/5+ configuration (developed in 2025), +# including noise, new response function and gain tune. # # Usage: # @@ -35,7 +41,9 @@ # introduced configuration for SPR 202202 # 20240125 (petrillo@slac.stanford.edu) # introduced experimental setting PMT gain to 7.5 x 10^6 (and q.e. elsewhere) -# +# 20251111 (mvicenzi@bnl.gov) +# introduced configuration for new Run1/2 SPR (SPRisolHitRun9271) +# introduced configuration for new Run3/4 SPR (SPRisolHitRun12715) #include "opticalproperties_icarus.fcl" @@ -62,7 +70,7 @@ icarus_settings_opdet_2022: { NoiseRMS: 3.0 # in ADC#; "temporary" value, see SBN DocDB 19187 - NominalPedestal: 14999.5 # in ADC#; try to balance rounding errors + NominalPedestal: 15000.0 # in ADC# } # icarus_settings_opdet_2022 @@ -80,10 +88,42 @@ icarus_settings_opdet_202401patch: { } # icarus_settings_opdet_202401patch +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# the following is based on `icarus_settings_opdet_2022` rescaling it to gain +# 7.0x10^6; this is an empirical choice to reconciliate the single PE region in Run1/2 +# NoiseRMS is also reduced to 2.6 ADC; see e.g. SBN DocDB 41115 +icarus_settings_opdet_run2_2025: { + + @table::icarus_settings_opdet_2022 + + nominalPMTgain: 7.0e6 # PMT multiplication gain factor + + NoiseRMS: 2.6 # in ADC# + +} # icarus_settings_opdet_run2_2025 + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# the following is based on `icarus_settings_opdet_2022` rescaling it to gain +# 4.5x10^6; this is an empirical choice to reconciliate the single PE region in Run3/4 +# NoiseRMS is also reduced to 2.6 ADC; see e.g. SBN DocDB 41115 +icarus_settings_opdet_run4_2025: { + + @table::icarus_settings_opdet_2022 + + nominalPMTgain: 4.5e6 # PMT multiplication gain factor + + NoiseRMS: 2.6 # in ADC# + +} # icarus_settings_opdet_run4_2025 + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # settings used in the standard configurations: -icarus_settings_opdet: @local::icarus_settings_opdet_202401patch +icarus_settings_opdet: @local::icarus_settings_opdet_run2_2025 + +icarus_settings_opdet_run2: @local::icarus_settings_opdet_run2_2025 +icarus_settings_opdet_run4: @local::icarus_settings_opdet_run4_2025 ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -140,14 +180,15 @@ icarus_photoelectronresponse_exponentials: { TransitTime: "55.1 ns" RaiseTimeConstant: "1.8 ns" FallTimeConstant: "4.2 ns" - Gain: @local::icarus_settings_opdet.nominalPMTgain # Gain = 7.0e6 => PeakCurrent: -0.14144 mA + Gain: @local::icarus_settings_opdet_2022.nominalPMTgain # Gain = 7.0e6 => PeakCurrent: -0.14144 mA ADC: 409.6 # ADC / mA } # icarus_photoelectronresponse_exponentials # -# Single photoelectron response from data. +# Single photoelectron response from laser data. +# This is valid for Run1/2 cables. # icarus_photoelectronresponse_202202: { @@ -155,11 +196,43 @@ icarus_photoelectronresponse_202202: { WaveformData: "Responses/SPR202202.txt" TransitTime: "55.1 ns" - Gain: @local::icarus_settings_opdet.nominalPMTgain + Gain: @local::icarus_settings_opdet_2022.nominalPMTgain } # icarus_photoelectronresponse_202202 +# +# Single photoelectron response from single-PE data. +# This is valid for Run1/2 cables. +# +icarus_photoelectronresponse_run2_2025: { + + tool_type: SampledWaveformFunctionTool + + WaveformData: "Responses/SPRisolHitRun9271.txt" + TransitTime: "14.0 ns" + Gain: @local::icarus_settings_opdet_run2.nominalPMTgain + BiasRatio: 1.5332 + +} # icarus_photoelectronresponse_run2_2025 + + +# +# Single photoelectron response from single-PE data. +# This is valid for Run3/4/5+ cables. +# +icarus_photoelectronresponse_run4_2025: { + + tool_type: SampledWaveformFunctionTool + + WaveformData: "Responses/SPRisolHitRun12715.txt" + TransitTime: "14.0 ns" + Gain: @local::icarus_settings_opdet_run4.nominalPMTgain + BiasRatio: 1.2196 + +} # icarus_photoelectronresponse_run4_2025 + + # # Custom photoelectron response. # @@ -189,7 +262,9 @@ icarus_photoelectronresponse_customexample: { # # pick one # -icarus_photoelectronresponse_standard: @local::icarus_photoelectronresponse_202202 + +icarus_photoelectronresponse_standard_run2: @local::icarus_photoelectronresponse_run2_2025 +icarus_photoelectronresponse_standard_run4: @local::icarus_photoelectronresponse_run4_2025 ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -286,7 +361,7 @@ icarus_pmtsimulationalg_202202_noise: { Pedestal: @local::icarus_pmt_pedestal_standard - SinglePhotonResponse: @local::icarus_photoelectronresponse_standard + SinglePhotonResponse: @local::icarus_photoelectronresponse_202202 DarkNoiseRate: "1.6 kHz" # per channel, from CERN test stand measurement @@ -309,10 +384,13 @@ icarus_pmtsimulationalg_202202_noise: { ReadoutEnablePeriod: "2.0 ms" # PMT enable gate duration: time for which PMT readout is enabled CreateBeamGateTriggers: true #Option to create unbiased readout around beam spill BeamGateTriggerRepPeriod: "2.0 us" # Repetition period and number of repetitions for BeamGateTriggers: - BeamGateTriggerNReps: 10 # should cover -7/+21 us, instead just goes -1 to 21 us) + BeamGateTriggerNReps: 10 # should cover -7/+21 us, instead just goes -1 to 21 us QE: @local::icarus_opticalproperties.ScintPreScale # from opticalproperties_icarus.fcl FluctuateGain: true # apply per-photoelectron gain fluctuations - + ApplyTimingDelays: false # legacy value + UseGainDatabase: false # legacy value + UseChannelStatusDatabase: false # legacy value + PMTspecs: { DynodeK: 0.75 # gain on a PMT multiplication stage # is proportional to dV^k @@ -358,6 +436,76 @@ icarus_pmtsimulationalg_202401patch: { } # icarus_pmtsimulationalg_202401patch +### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +### Configuration Run2-2025: +### * explicitly based on the configuration "2022" above +### * gain adapted to reconciliate signal integral and amplitude in SPE region +### (SBN DocDB 41115) +### * gain updated to 7.0 x 10^6 +### * threshold for digitization scaled down +### + +icarus_pmtsimulationalg_run2_2025: { + + @table::icarus_pmtsimulationalg_202202_noise + + SinglePhotonResponse: @local::icarus_photoelectronresponse_standard_run2 + + # Apply timing delays (cables, transit time, ..) to the simulated photons + # These delays are extracted from the calibration database + ApplyTimingDelays: false + # Use channel status database to skip dead or bad channels + UseChannelStatusDatabase: false + # Use per-channel gain from database + UseGainDatabase: false + + # Threshold in ADC counts for a PMT self-trigger + ThresholdADC: 15 # ADC counts + + # arranged ad hoc to get ~40% Poisson fluctuation on gain (effectively: 40.8%) + PMTspecs: { + DynodeK: 0.75 # gain on a PMT multiplication stage + VoltageDistribution: [ 3.0, 3.4, 5.0, 3.33, 1.67, 1.0, 1.2, 1.5, 2.2, 3.0 ] + Gain: @local::icarus_settings_opdet_run2.nominalPMTgain # total PMT gain + } + +} # icarus_pmtsimulationalg_run2_2025 + + +### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +### Configuration Run4-2025: +### * explicitly based on the configuration "2022" above +### * gain adapted to reconciliate signal integral and amplitude in SPE region +### * gain updated to 4.5 x 10^6 +### * threshold for digitization scaled down +### + +icarus_pmtsimulationalg_run4_2025: { + + @table::icarus_pmtsimulationalg_202202_noise + + SinglePhotonResponse: @local::icarus_photoelectronresponse_standard_run4 + + # Apply timing delays (cables, transit time, ..) to the simulated photons + # These delays are extracted from the calibration database + ApplyTimingDelays: false + # Use channel status database to skip dead or bad channels + UseChannelStatusDatabase: false + # Use per-channel gain from database + UseGainDatabase: false + + # Threshold in ADC counts for a PMT self-trigger + ThresholdADC: 15 # ADC counts + + # arranged ad hoc to get ~40% Poisson fluctuation on gain (effectively: 40.8%) + PMTspecs: { + DynodeK: 0.75 # gain on a PMT multiplication stage + VoltageDistribution: [ 3.0, 3.4, 5.0, 3.33, 1.67, 1.0, 1.2, 1.5, 2.2, 3.0 ] + Gain: @local::icarus_settings_opdet_run4.nominalPMTgain # total PMT gain + } + +} # icarus_pmtsimulationalg_run4_2025 + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### ### Legacy configuration, pre-2022 @@ -372,7 +520,7 @@ icarus_pmtsimulationalg_2018: { } } # Pedestal - SinglePhotonResponse: @local::icarus_photoelectronresponse_standard + SinglePhotonResponse: @local::icarus_photoelectronresponse_202202 DarkNoiseRate: "1.6 kHz" # from CERN test stand measurement @@ -397,7 +545,10 @@ icarus_pmtsimulationalg_2018: { Saturation: 300 #in number of p.e. to see saturation effects in the signal QE: @local::icarus_opticalproperties.ScintPreScale # from opticalproperties_icarus.fcl FluctuateGain: true # apply per-photoelectron gain fluctuations - + ApplyTimingDelays: false # legacy value + UseGainDatabase: false # legacy value + UseChannelStatusDatabase: false # legacy value + PMTspecs: { DynodeK: 0.75 # gain on a PMT multiplication stage # is proportional to dV^k @@ -434,7 +585,8 @@ icarus_pmtsimulationalg_2018_nonoise: { # # Includes noise. # -icarus_pmtsimulationalg_standard: @local::icarus_pmtsimulationalg_202401patch +icarus_pmtsimulationalg_standard_run2: @local::icarus_pmtsimulationalg_run2_2025 +icarus_pmtsimulationalg_standard_run4: @local::icarus_pmtsimulationalg_run4_2025 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/icaruscode/PMT/CMakeLists.txt b/icaruscode/PMT/CMakeLists.txt index a34d31b95..194591837 100644 --- a/icaruscode/PMT/CMakeLists.txt +++ b/icaruscode/PMT/CMakeLists.txt @@ -5,6 +5,7 @@ add_subdirectory(Algorithms) add_subdirectory(LibraryMappingTools) add_subdirectory(Trigger) add_subdirectory(Calibration) +add_subdirectory(Status) # Removing AVX instructions because assuming the grid nodes # being less than 6 year old proved to be pretentious. @@ -13,7 +14,9 @@ add_subdirectory(Calibration) cet_build_plugin(SimPMTIcarus art::module LIBRARIES - icaruscode_PMT_Algorithms + icaruscode::PMT_Status + icaruscode::PMT_Algorithms + sbnobj::ICARUS_PMT_Data lardataobj::RawData lardataobj::Simulation larcore::Geometry_Geometry_service diff --git a/icaruscode/PMT/Calibration/CMakeLists.txt b/icaruscode/PMT/Calibration/CMakeLists.txt index c4317a71e..7484db3f6 100644 --- a/icaruscode/PMT/Calibration/CMakeLists.txt +++ b/icaruscode/PMT/Calibration/CMakeLists.txt @@ -1,58 +1,117 @@ add_subdirectory(CaloTools) add_subdirectory(fcl) + +include(lar::PhotonCalibratorService) + +cet_make_library( + SOURCE + PhotonCalibratorFromDB.cxx + LIBRARIES + PUBLIC + larreco::PhotonCalibrator # IPhotonCalibrator interface + fhiclcpp::fhiclcpp + messagefacility::MF_MessageLogger + cetlib_except::cetlib_except + larevt::CalibrationDBI_Providers + larevt::CalibrationDBI_IOVData +) + +cet_build_plugin(ICARUSPhotonCalibratorServiceFromDB + lar::PhotonCalibratorService # plugin type from larreco + SOURCE + ICARUSPhotonCalibratorServiceFromDB_service.cc + LIBRARIES + PUBLIC + icaruscode::PMT_Calibration + larreco::PhotonCalibrator + art::Framework_Principal + art::Framework_Services_Registry + fhiclcpp::types + messagefacility::MF_MessageLogger +) + cet_build_plugin(PMTLaserCalibration art::module - LIBRARIES - icaruscode_PMT_Algorithms - icaruscode_PMT_Calibration_CaloTools - icaruscode_IcarusObj - lardataobj::RawData - lardataobj::Simulation - lardataobj::RecoBase - art::Framework_Core - art::Framework_Principal - art::Framework_Services_Registry - art_root_io::TFileService_service - art_root_io::tfile_support - art::Persistency_Provenance - canvas::canvas - messagefacility::MF_MessageLogger - fhiclcpp::fhiclcpp - cetlib::cetlib - cetlib_except::cetlib_except - Boost::system - ) + LIBRARIES + icaruscode_PMT_Algorithms + icaruscode_PMT_Calibration_CaloTools + icaruscode_IcarusObj + lardataobj::RawData + lardataobj::Simulation + lardataobj::RecoBase + art::Framework_Core + art::Framework_Principal + art::Framework_Services_Registry + art_root_io::TFileService_service + art_root_io::tfile_support + art::Persistency_Provenance + canvas::canvas + messagefacility::MF_MessageLogger + fhiclcpp::fhiclcpp + cetlib::cetlib + cetlib_except::cetlib_except + Boost::system +) cet_build_plugin(PMTBackgroundphotonsCalibration art::module - LIBRARIES - icaruscode_PMT_Algorithms - icaruscode_PMT_Calibration_CaloTools - lardataobj::RawData - lardataobj::Simulation - lardataobj::RecoBase - art::Framework_Core - art::Framework_Principal - art::Framework_Services_Registry - art_root_io::TFileService_service - art_root_io::tfile_support - art::Persistency_Provenance - canvas::canvas - messagefacility::MF_MessageLogger - fhiclcpp::fhiclcpp - cetlib::cetlib - cetlib_except::cetlib_except - Boost::system - ) - - -art_make_exec( NAME - fitPulseDistribution - SOURCE - fitPulseDistribution.cc - LIBRARIES - icaruscode_PMT_Algorithms - icaruscode_PMT_Calibration_CaloTools - ) + LIBRARIES + icaruscode_PMT_Algorithms + icaruscode_PMT_Calibration_CaloTools + lardataobj::RawData + lardataobj::Simulation + lardataobj::RecoBase + art::Framework_Core + art::Framework_Principal + art::Framework_Services_Registry + art_root_io::TFileService_service + art_root_io::tfile_support + art::Persistency_Provenance + canvas::canvas + messagefacility::MF_MessageLogger + fhiclcpp::fhiclcpp + cetlib::cetlib + cetlib_except::cetlib_except + Boost::system +) + +cet_build_plugin(OpHitRecalibrator art::module + LIBRARIES + PUBLIC + lardataobj::RecoBase + lardataobj::RawData + larreco::PhotonCalibrator # IPhotonCalibrator interface + icaruscode_Timing +) + +cet_build_plugin(VerticalTrackFlashWaveformAna art::module + LIBRARIES + icaruscode::Timing + larcore::Geometry_Geometry_service + larcorealg::Geometry + lardata::DetectorInfoServices_DetectorClocksServiceStandard_service + lardataalg::DetectorInfo + lardataobj::RawData + lardataobj::RecoBase + art::Framework_Core + art::Framework_Principal + art::Framework_Services_Registry + art_root_io::TFileService_service + art_root_io::tfile_support + canvas::canvas + messagefacility::MF_MessageLogger + fhiclcpp::fhiclcpp + cetlib::cetlib + cetlib_except::cetlib_except + ROOT::Tree + ) + +art_make_exec(NAME fitPulseDistribution + SOURCE + fitPulseDistribution.cc + LIBRARIES + icaruscode_PMT_Algorithms + icaruscode_PMT_Calibration_CaloTools +) install_headers() diff --git a/icaruscode/PMT/Calibration/ICARUSPhotonCalibratorServiceFromDB.h b/icaruscode/PMT/Calibration/ICARUSPhotonCalibratorServiceFromDB.h new file mode 100644 index 000000000..993f2155d --- /dev/null +++ b/icaruscode/PMT/Calibration/ICARUSPhotonCalibratorServiceFromDB.h @@ -0,0 +1,81 @@ +/** + * @file icaruscode/PMT/Calibration/ICARUSPhotonCalibratorServiceFromDB.h + * @brief Framework service interface to `icarusDB::PhotonCalibratorFromDB`. + * @author Michael Carrigan (micarrig@fnal.gov) + * @see icaruscode/PMT/Calibration/ICARUSPhotonCalibratorServiceFromDB_service.cc + */ + +#ifndef ICARUSCODE_PMT_CALIBRATION_ICARUSPHOTONCALIBRATORSERVICEFROMDB_H +#define ICARUSCODE_PMT_CALIBRATION_ICARUSPHOTONCALIBRATORSERVICEFROMDB_H + +// LArSoft Includes +#include "larreco/Calibrator/IPhotonCalibratorService.h" +#include "icaruscode/PMT/Calibration/PhotonCalibratorFromDB.h" +#include "art/Framework/Services/Registry/ActivityRegistry.h" +#include "art/Framework/Principal/Run.h" +#include "art/Framework/Services/Registry/ServiceDeclarationMacros.h" +#include "art/Framework/Services/Registry/ServiceTable.h" +#include "fhiclcpp/types/TableFragment.h" + +namespace calib { + + /** + * @brief Photoelectron calibration service using ICARUS database. + * @see `icarusDB::PhotonCalibratorFromDB` + * + * The service reads the information on the translation from a reconstructed + * optical hit to photoelectrons from an external database. + * + * This is a _art_ framework service, which only delivers a + * framework-independent service provider (`icarusDB::PhotonCalibratorFromDB`) + * that does all the lifting. + * The provider can be summoned in an _art_ context for example by + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * `calib::IPhotonCalibrator const& calibrator + * = *(lar::providerFrom()); + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * (`#include "larcore/CoreUtils/ServiceUtil.h"` for `lar::providerFrom()`). + * + * The service triggers an update of the provider caches on every change of + * run. This allows the service to be thread-safe in _art_ terms (because + * _art_ threads do not mix runs). + * + */ + class ICARUSPhotonCalibratorServiceFromDB : public IPhotonCalibratorService { + public: + using provider_type = icarusDB::PhotonCalibratorFromDB; + + struct ServiceConfiguration_t { + + // this part of the configuration is passed to the provider: + fhicl::TableFragment providerConfig; + + }; + + using Parameters = art::ServiceTable; + + ICARUSPhotonCalibratorServiceFromDB(Parameters const& params, art::ActivityRegistry& reg); + + std::string getAreaDatabaseTag() const& { return fProvider.getAreaDatabaseTag(); } + + double getSPEArea(int channel) const { return fProvider.getSPEArea(channel); } + + double getSPEFitWidth(int channel) const { return fProvider.getSPEFitWidth(channel); } + + provider_type const* provider() const override { return &fProvider; } + + private: + + void preBeginRun(art::Run const& run); + + icarusDB::PhotonCalibratorFromDB fProvider; + + }; + +} + +DECLARE_ART_SERVICE_INTERFACE_IMPL(calib::ICARUSPhotonCalibratorServiceFromDB, + calib::IPhotonCalibratorService, + SHARED) + +#endif // ICARUSCODE_PMT_CALIBRATION_ICARUSPHOTONCALIBRATORSERVICEFROMDB_H diff --git a/icaruscode/PMT/Calibration/ICARUSPhotonCalibratorServiceFromDB_service.cc b/icaruscode/PMT/Calibration/ICARUSPhotonCalibratorServiceFromDB_service.cc new file mode 100644 index 000000000..8ad5d678b --- /dev/null +++ b/icaruscode/PMT/Calibration/ICARUSPhotonCalibratorServiceFromDB_service.cc @@ -0,0 +1,29 @@ +/** + * @file icaruscode/PMT/Calibration/ICARUSPhotonCalibratorServiceFromDB_service.cc + * @brief Framework service interface to `icarusDB::PhotonCalibratorFromDB`. + * @author Michael Carrigan (micarrig@fnal.gov) + * @see icaruscode/PMT/Calibration/ICARUSPhotonCalibratorServiceFromDB.h + */ + +#include "icaruscode/PMT/Calibration/ICARUSPhotonCalibratorServiceFromDB.h" + +#include "art/Framework/Services/Registry/ServiceDefinitionMacros.h" + + +calib::ICARUSPhotonCalibratorServiceFromDB::ICARUSPhotonCalibratorServiceFromDB( + Parameters const& params, + art::ActivityRegistry& reg +) + : fProvider{ params().providerConfig() } +{ + reg.sPreBeginRun.watch(this, &ICARUSPhotonCalibratorServiceFromDB::preBeginRun); +} + +void calib::ICARUSPhotonCalibratorServiceFromDB::preBeginRun(art::Run const& run) +{ + fProvider.readCalibrationFromDB(run.run()); +} + + +DEFINE_ART_SERVICE_INTERFACE_IMPL(calib::ICARUSPhotonCalibratorServiceFromDB, + calib::IPhotonCalibratorService) diff --git a/icaruscode/PMT/Calibration/OpHitRecalibrator_module.cc b/icaruscode/PMT/Calibration/OpHitRecalibrator_module.cc new file mode 100644 index 000000000..bf26c2d6e --- /dev/null +++ b/icaruscode/PMT/Calibration/OpHitRecalibrator_module.cc @@ -0,0 +1,300 @@ +/** + * @file icaruscode/PMT/Calibration/OpHitRecalibrator_module.cc + * @brief Recalibrate PEs and times of optical hits. + * @author Matteo Vicenzi (mvicenzi@bnl.gov) + * @date December 03, 2025 + */ + +// ICARUS libraries +#include "icaruscode/Timing/PMTTimingCorrections.h" +#include "icaruscode/Timing/IPMTTimingCorrectionService.h" +#include "icaruscode/Timing/PMTTimingCorrectionsProvider.h" +#include "icaruscode/PMT/Calibration/ICARUSPhotonCalibratorServiceFromDB.h" + +// framework libraries +#include "canvas/Utilities/InputTag.h" + +#include "art/Framework/Services/Registry/ServiceHandle.h" +#include "art/Framework/Core/SharedProducer.h" +#include "art/Framework/Core/ModuleMacros.h" +#include "art/Framework/Principal/Event.h" +#include "art/Framework/Principal/Handle.h" +#include "canvas/Utilities/InputTag.h" +#include "canvas/Utilities/Exception.h" +#include "messagefacility/MessageLogger/MessageLogger.h" +#include "fhiclcpp/types/Atom.h" +#include "fhiclcpp/types/Sequence.h" + +// LArSoft libraries +#include "lardataobj/RecoBase/OpHit.h" + +// C/C++ standard libraries +#include +#include // std::unique_ptr<> +#include +#include // std::move() + +// ----------------------------------------------------------------------------- +namespace icarus +{ + class OpHitRecalibrator; +} +/** + * @brief Creates a new collection of optical hits with re-calibrated PEs and times. + * + * This module reads reconstructed optical detector hits, removes previously-applied + * gain and/or timing calibrations, and applies new ones. + * A new collection of hits is produced containing a re-calibrated copy of all the + * hits from the input collections. + * + * The PE recalibration is simple: for each optical hit, its `PE` value is recomputed + * from its `Area` [ADC x tick] based on a newly determined single-PE area. + * If `UseGainDatabase` is set, the module calls retrieves the SPE area by channel and run number + * using the gain calibration service. If that option is disabled, a single SPE area value + * is used for all channels and run numbers. This value is read from the `SPEArea` parameter + * set in the configuration. + * + * The timing recalibration requires removing previously-applied timing corrections and + * adding the new ones. Unfortunately, however, it's not possible to easily determine + * which timing corrections were previsouly applied to the optical hits. + * The old corrections -- that need to be removed -- are therefore obtained by locally + * definining an instance of `icarusDB::PMTTimingCorrectionsProvider` and manually setting + * in the configuration the database tags that were used originally (`OldTimingDBTags`). + * These need to be deduced by the `icaruscode` version that was used and the corresponding + * settings in `calibration_database_GlobalTags_icarus.fcl` + * On the other hand, the new corrections are obtained from the current timing correction + * service `icarusDB::PMTTimingCorrections` as defined in `timing_icarus.fcl`. + * + * Input + * ------ + * * `std::vector` data products (as for `InputLabels`) + * + * Output + * ------- + * * a single `std::vector` data product with the recalibrated hits from the + * input collections; the hits are in the order of the data products specified in input. + * + * Configuration parameters + * ------------------------- + * + * * `InputLabel` (art::InputTag, mandatory): optical hit data products to be recalibrated. It must be non-empty. + * * `RecalibratePE` (flag, mandatory): if set, recalibrate hit PE values. + * * `UseGainDatabase` (flag, default: true): if set, use gain values from database + * to re-calibrate hit PEs from hit area. + * * `SPEArea` (double, default: -1): if not using the gain database, single-photoelectron + * area in ADC x tick to be used in the PE calibration. + * * `RecalibrateTime` (flag, mandatory): if set, recalibrate hit times. + * * `OldTimingDBTags` (fhicl::ParameterSet, mandatory): configuration for the previously-applied timing corrections + * that need to be removed/replace by the now ones. It should match what is tipically passed to + * `icarusDB::PMTTimingCorrectionsProvider`, specifying the database tags that were used. + * * `Verbose` (flag, default: `false`): verbose printing + * + */ +class icarus::OpHitRecalibrator : public art::SharedProducer +{ + +public: + /// Constructor: just reads the configuration. + explicit OpHitRecalibrator(fhicl::ParameterSet const &config, art::ProcessingFrame const &); + + /// process the event + void produce(art::Event &event, art::ProcessingFrame const &) override; + + /// configuration at every run change + void beginRun(art::Run &run, art::ProcessingFrame const &) override; + +private: + art::InputTag const fInputLabel; + bool const fRecalibratePE; + bool const fRecalibrateTime; + bool const fUseGainDatabase; + double const fSPEArea; + bool const fVerbose; + + /// Pointers to the online corrections services + icarusDB::PMTTimingCorrections const &fPMTTimingCorrectionsService; + calib::IPhotonCalibrator const &fPhotonCalibratorService; + + /// Pointer to the provider for the old pmt corrections + std::unique_ptr fOldTimingProvider; +}; + +// ----------------------------------------------------------------------------- +icarus::OpHitRecalibrator::OpHitRecalibrator(fhicl::ParameterSet const &config, art::ProcessingFrame const &) + : art::SharedProducer(config), + fInputLabel{config.get("InputLabel")}, + fRecalibratePE{config.get("RecalibratePE")}, + fRecalibrateTime{config.get("RecalibrateTime")}, + fUseGainDatabase{config.get("UseGainDatabase", false)}, + fSPEArea{config.get("SPEArea", -1.)}, + fVerbose{config.get("Verbose", false)}, + fPMTTimingCorrectionsService{*(lar::providerFrom())}, + fPhotonCalibratorService{*(lar::providerFrom())}, + fOldTimingProvider{std::make_unique(config.get("OldTimingDBTags"))} +{ + async(); + + // configuration checks + if (!fRecalibratePE && !fRecalibrateTime) + { + throw art::Exception{art::errors::Configuration} + << "No re-calibration selected. Why are you even running meeee!?!?! :/\n"; + } + + if (fRecalibratePE && !fUseGainDatabase && (fSPEArea < 0)) + { + throw art::Exception{art::errors::Configuration} + << "The gain database for PE recalibration has been disabled ('UseGainDatabase')," + << " but 'SPEArea' (" << fSPEArea + << ") has not been set correctly. Fix the configuration!\n"; + } + + if (fRecalibrateTime) + { + mf::LogInfo("OpHitRecalibrator") << "Re-calibration of timing corrections enabled:\n" + << "LaserTag: old " << fOldTimingProvider->getLaserDatabaseTag() + << " -> new " << fPMTTimingCorrectionsService.getLaserDatabaseTag() + << "\nCosmicsTag: old " << fOldTimingProvider->getCosmicsDatabaseTag() + << " -> new " << fPMTTimingCorrectionsService.getCosmicsDatabaseTag(); + } + + if (fRecalibratePE && fUseGainDatabase) + { + mf::LogInfo("OpHitRecalibrator") + << "Re-calibration of PE (gain) from database enabled with configured service tag."; + } + + // Consumes + consumes>(fInputLabel); + // Produces + produces>(); +} + +// ----------------------------------------------------------------------------- +void icarus::OpHitRecalibrator::beginRun(art::Run &run, art::ProcessingFrame const &) +{ + // make sure we are updating the locally-instantiated correction provider + if (fOldTimingProvider) + { + if (fVerbose) + mf::LogInfo("OpHitRecalibrator") << "Updating old timing corrections for " << run.id().run(); + fOldTimingProvider->readTimeCorrectionDatabase(run); + } +} + +// ----------------------------------------------------------------------------- +void icarus::OpHitRecalibrator::produce(art::Event &event, art::ProcessingFrame const &) +{ + auto log = fVerbose ? std::make_optional("OpHitRecalibrator") : std::nullopt; + + // Create a copy of the OpHits + std::vector recalibratedOpHits; + + auto const &opHits = event.getProduct>(fInputLabel); + + for (auto const &opHit : opHits) + { + // read current times + double peakTime = opHit.PeakTime(); + double peakTimeAbs = opHit.PeakTimeAbs(); + double startTime = opHit.StartTime(); + double riseTime = opHit.RiseTime(); + + // read current PE + double hitPE = opHit.PE(); + + // First, recalibrate PE values (if enabled). + // - if using gain database, fetch new SPEArea for this run/channel + // - if not using gain database, use fSPEArea + // - re-compute PE value with new SPE area + if (fRecalibratePE) + { + double oldSPEArea = opHit.Area() / hitPE; + double newSPEArea = fSPEArea; + + if (fUseGainDatabase) + { + // service directly returns PE from area and channel + // compute SPE area back for logging purposes + hitPE = fPhotonCalibratorService.PE(opHit.Area(), opHit.OpChannel()); + newSPEArea = opHit.Area() / hitPE; + } + else + { + // simple re-computation + hitPE = opHit.Area() / newSPEArea; + } + + if (log) + { + *log << "Channel: " << opHit.OpChannel() + << ", Area: " << opHit.Area() << " [ADC x tick]" + << ", PE " << hitPE + << " (old SPEArea: " << oldSPEArea + << ") --> new PE " << opHit.Area() / newSPEArea + << " (new SPEArea: " << newSPEArea << ")\n"; + } + } + + // Second, recalibrate PMT times (if enabled) + // Previous corrections need to be subtracted + // use locally instantiated provider class to fetch old corrections + // use online pmt corrections servicer to fetch current/new corrections + if (fRecalibrateTime) + { + // get the old timing corrections: these will need to be subtracted! + double const oldLaserTimeCorrection = fOldTimingProvider->getLaserCorrections(opHit.OpChannel()); + double const oldCosmicsCorrection = fOldTimingProvider->getCosmicsCorrections(opHit.OpChannel()); + double const oldTotalCorrection = oldLaserTimeCorrection + oldCosmicsCorrection; + + // get new/current timing: these will need to be added! + double const laserTimeCorrection = fPMTTimingCorrectionsService.getLaserCorrections(opHit.OpChannel()); + double const cosmicsCorrection = fPMTTimingCorrectionsService.getCosmicsCorrections(opHit.OpChannel()); + double const totalCorrection = laserTimeCorrection + cosmicsCorrection; + + double timeCorr = totalCorrection - oldTotalCorrection; + + if (log) + { + *log << "Channel: " << opHit.OpChannel() + << ", startTime " << startTime + << ", peakTime " << peakTime + << ", peakTimeAbs " << peakTimeAbs + << " (total correction: " << oldTotalCorrection + << ") --> new startTime " << startTime + timeCorr + << ", peakTime " << peakTime + timeCorr + << ", peakTimeAbs " << peakTimeAbs + timeCorr + << " (total correction: " << totalCorrection << ")\n"; + } + + peakTime = peakTime + timeCorr; + peakTimeAbs = peakTimeAbs + timeCorr; + startTime = startTime + timeCorr; + // NOTE: riseTime is currently relative to the start time, so no correction needed + } + + recalibratedOpHits.emplace_back( + opHit.OpChannel(), // channel + peakTime, // recalibrated peaktime + peakTimeAbs, // recalibrated peaktimeabs + startTime, // recalibrated starttime + riseTime, // recalibrated risetime + opHit.Frame(), // frame + opHit.Width(), // width + opHit.Area(), // area + opHit.Amplitude(), // peakheight + hitPE, // recalibrated pe + opHit.FastToTotal() // fasttototal + ); + } + + // The new OpHits collection is also saved in the event stream + event.put( + std::make_unique>(std::move(recalibratedOpHits))); + +} // icarus::OpHitRecalibrator::produce + +// ----------------------------------------------------------------------------- +DEFINE_ART_MODULE(icarus::OpHitRecalibrator) + +// ----------------------------------------------------------------------------- diff --git a/icaruscode/PMT/Calibration/PhotonCalibratorFromDB.cxx b/icaruscode/PMT/Calibration/PhotonCalibratorFromDB.cxx new file mode 100644 index 000000000..9bb7a146c --- /dev/null +++ b/icaruscode/PMT/Calibration/PhotonCalibratorFromDB.cxx @@ -0,0 +1,152 @@ +/** + * @file icaruscode/PMT/Calibration/PhotonCalibratorFromDB.cxx + * @brief Implementation of optical hit photoelectron calibration from database. + * @author Michael Carrigan, Matteo Vicenzi + * @see icaruscode/PMT/Calibration/PhotonCalibratorFromDB.h + */ + +#include "icaruscode/PMT/Calibration/PhotonCalibratorFromDB.h" + +// Database interface helpers +#include "larevt/CalibrationDBI/IOVData/TimeStampDecoder.h" + +// Message facility +#include "messagefacility/MessageLogger/MessageLogger.h" + +// C++ standard library +#include // std::setw() + + +// ----------------------------------------------------------------------------- +namespace icarusDB { + + details::PhotonCalibratorInfo convert + (PhotonCalibratorFromDB::Config::DefaultCalib const& config) + { + details::PhotonCalibratorInfo info; + info.speArea = config.SPEArea(); + // all the rest is left to C++ default + return info; + } + +} + + +// ----------------------------------------------------------------------------- +icarusDB::PhotonCalibratorFromDB::PhotonCalibratorFromDB(const Config& config) + : fCalibDefaults ( config.Defaults() ) + , fVerbose ( config.Verbose() ) + , fLogCategory ( config.LogCategory() ) + , fAreaTag ( config.AreaTag() ) + , fOverrideRunNumber ( config.OverrideRunNumber() ) + , fDB ( config.DBname(), "", "", config.AreaTag(), true, false) +{ + mf::LogInfo(fLogCategory) + << "PhotonCalibratorFromDB connected to " << config.DBname() << " DB, tag '" << config.AreaTag() << "'"; +} + + +// ----------------------------------------------------------------------------- +void icarusDB::PhotonCalibratorFromDB::readCalibrationFromDB(unsigned int run) +{ + + if (fOverrideRunNumber >= 0) { + mf::LogInfo(fLogCategory) + << "Overriding run number " << run << " with " << fOverrideRunNumber + << " for DB queries."; + run = static_cast(fOverrideRunNumber); + } + + mf::LogInfo(fLogCategory) << "Reading SPE area calibrations from database for run " << run; + + bool ret = fDB.UpdateData( RunToDatabaseTimestamp(run) ); // select table based on run number + mf::LogTrace(fLogCategory) + << "Calibrations" << (ret? "": " not") << " updated for run " << run + << "\nFetched IoV [ " << fDB.CachedStart().DBStamp() << " ; " << fDB.CachedEnd().DBStamp() + << " ] to cover t=" << RunToDatabaseTimestamp(run) + << " [=" << lariov::TimeStampDecoder::DecodeTimeStamp(RunToDatabaseTimestamp(run)).DBStamp() << "]"; + + std::vector channelList; + if (int res = fDB.GetChannelList(channelList); res != 0) { + throw cet::exception + ( "PhotonCalibratorFromDB" ) << "GetChannelList() returned " << res << " on run " << run << " query\n"; + } + + if (channelList.empty()) { + throw cet::exception("PhotonCalibratorFromDB") << "got an empty channel list for run " << run << "\n"; + } + + mf::LogDebug log(fLogCategory); + log << "Loading calibration for " << channelList.size() << " channels in run " << run; + for( auto channel : channelList ) { + + // SPE area [ADC x tick] + double area = 0; + int error = fDB.GetNamedChannelData( channel, "area", area ); + if( error ) throw cet::exception( fLogCategory ) << "Error (code " << error << ") reading 'area' for channel " << channel << "\n"; + fDatabaseSPECalibrations[channel].speArea = area; // ADC x tick + + // SPE area uncertainty from fit + double areaErr = 0; + error = fDB.GetNamedChannelData( channel, "area_err", areaErr ); + if( error ) throw cet::exception( fLogCategory ) << "Error (code " << error << ") reading 'area_err' for channel " << channel << "\n"; + fDatabaseSPECalibrations[channel].speAreaErr = areaErr; // ADC x tick + + // SPE fit width (sigma) [ADC x tick] + double width = 0; + error = fDB.GetNamedChannelData( channel, "width", width ); + if( error ) throw cet::exception( fLogCategory ) << "Error (code " << error << ") reading 'width' for channel " << channel << "\n"; + fDatabaseSPECalibrations[channel].speFitWidth = width; // ADC x tick + + // SPE fit width uncertainty + double widthErr = 0; + error = fDB.GetNamedChannelData( channel, "width_err", widthErr ); + if( error ) throw cet::exception( fLogCategory ) << "Error (code " << error << ") reading 'width_err' for channel " << channel << "\n"; + fDatabaseSPECalibrations[channel].speFitWidthErr = widthErr; // ADC x tick + + // Fit quality + double fitQuality = 0; + error = fDB.GetNamedChannelData( channel, "fit_quality", fitQuality ); + if( error ) throw cet::exception( fLogCategory ) << "Error (code " << error << ") reading 'fit_quality' for channel " << channel << "\n"; + fDatabaseSPECalibrations[channel].speFitQuality = fitQuality; + + if (fVerbose) + log << "\n channel " << std::setw(3) << channel + << " SPE area " << area << " +/- " << areaErr + << " sigma " << width << " +/- " << widthErr + << " fit quality " << fitQuality; + } +} + + +// ----------------------------------------------------------------------------- +double icarusDB::PhotonCalibratorFromDB::PE(double adcs, int channel) const +{ + return adcs / getChannelCalibOrDefault(channel).speArea; +} + + +// ----------------------------------------------------------------------------- +bool icarusDB::PhotonCalibratorFromDB::UseArea() const +{ + return true; +} + + +// ----------------------------------------------------------------------------- +uint64_t icarusDB::PhotonCalibratorFromDB::RunToDatabaseTimestamp( unsigned int run ) const{ + + // Run number to timestamp used in the db + // DBFolder.h only takes 19 digit (= timestamp in nano second), + // but ICARUS tables are currently using run numbers + // Step 1) Add 1000000000 to the run number; e.g., run XXXXX -> 10000XXXXX + // Step 2) Multiply 1000000000 + uint64_t runNum = uint64_t(run); + uint64_t timestamp = runNum+1'000'000'000; + timestamp *= 1'000'000'000; + + if( fVerbose ) mf::LogInfo(fLogCategory) << "Run " << runNum << " calibrations from DB timestamp " << timestamp; + + return timestamp; +} + diff --git a/icaruscode/PMT/Calibration/PhotonCalibratorFromDB.h b/icaruscode/PMT/Calibration/PhotonCalibratorFromDB.h new file mode 100644 index 000000000..fc3ca19b7 --- /dev/null +++ b/icaruscode/PMT/Calibration/PhotonCalibratorFromDB.h @@ -0,0 +1,193 @@ +/** + * @file icaruscode/PMT/Calibration/PhotonCalibratorFromDB.h + * @brief Implementation of optical hit photoelectron calibration from database. + * @author Michael Carrigan, Matteo Vicenzi + * @see icaruscode/PMT/Calibration/PhotonCalibratorFromDB.cxx + */ + +#ifndef ICARUSCODE_PMT_CALIBRATION_PHOTONCALIBRATORFROMDB_H +#define ICARUSCODE_PMT_CALIBRATION_PHOTONCALIBRATORFROMDB_H + +// LArSoft libraries +#include "larreco/Calibrator/IPhotonCalibrator.h" +#include "larevt/CalibrationDBI/Providers/DBFolder.h" + +// ART includes +#include "fhiclcpp/types/Atom.h" +#include "fhiclcpp/types/Table.h" +#include "fhiclcpp/types/TableAs.h" + +// C++ standard libraries +#include // std::uint64_t +#include +#include + + +namespace icarusDB::details { + + /// Structure for single channel calibration. + struct PhotonCalibratorInfo { + + /// Area [positive, ADC count sum] of response to single photoelectron. + double speArea = -1.0; /// `speArea` from fit [ADC x tick] + double speAreaErr = -1.0; /// fit uncertainty on `speArea` [ADC x tick] + double speFitWidth = -1.0; /// width (sigma) from fit [ADC x tick] + double speFitWidthErr = -1.0; /// fit uncertainty on `speFitWidth` [ADC x tick] + double speFitQuality = -1.0; /// Fit quality + + }; + +} // icarusDB::details + +// ----------------------------------------------------------------------------- +namespace icarusDB { class PhotonCalibratorFromDB; } +/** + * @brief Optical hit photoelectron calibration service with data from database. + * + * This service implements the `IPhotonCalibrator` interface. + * + * The calibration factors are loaded from a database, per PMT channel and per + * calibration period. + * They represent the "area" of a single photoelectron response, that is the + * integral of the response signal in time, divided by the sampling time. + * The typical way to use this provider to get the hit photoelectrons is + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * double hitPhotoelectrons + * (calib::IPhotonCalibrator const& calibrator, recob::OpHit const& hit) + * { + * assert(calibrator.UseArea()); + * return calibrator.PE(hit.Area(), hit.OpChannel()); + * } + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * The example highlights that this service provider operates on the hit area. + * Note however that typically the `recob::OpHit` are already calibrated with + * an implementation of `calib::IPhotonCalibrator` and directly using + * `recob::OpHit::PE()` is better. + * + * The database interface is accessed only on `readCalibrationFromDB()` calls, + * and the relevant information is cached. + * + * The service provider is thread-safe in the standard C++ way: `const` methods + * are safe to call in any thread, non-const methods are not. + * + */ +class icarusDB::PhotonCalibratorFromDB: public calib::IPhotonCalibrator { + + + public: + + /// FHiCL configuration of the calibrator. + struct Config { + using Name = fhicl::Name; + using Comment = fhicl::Comment; + + struct DefaultCalib { + fhicl::Atom SPEArea { + Name{ "SPEArea" }, + Comment{ "area (ADC sum) of the response to a single photoelectron" } + }; + }; // DefaultCalib + + fhicl::Atom DBname { + Name{ "DBname" }, + Comment{ "the SPE area database name" }, + "pmt_speareas_data" + }; + + fhicl::Atom Verbose { + Name{ "Verbose" }, + Comment{ "enable additional messages for debugging" }, + false + }; + + fhicl::Atom LogCategory { + Name{ "LogCategory" }, + Comment{ "name of the message stream where to send console output" }, + "PhotonCalibratorFromDB" + }; + + fhicl::Atom AreaTag { + Name{ "AreaTag" }, + Comment{ "the database version (tag) to use" }, + "" + }; + + fhicl::TableAs Defaults { + Name{ "Defaults" }, + Comment{ "values used for channels not present in the database" } + }; + + fhicl::Atom OverrideRunNumber { + Name{ "OverrideRunNumber" }, + Comment{ "if non-negative, use this run number instead of the actual one for DB queries" }, + -1 + }; + + }; // Config + + + /// Constructor: reads the FHiCL configuration (no access to database yet). + PhotonCalibratorFromDB(const Config& config); + + /** + * @brief Converts the specified value in ADC into photoelectrons. + * @param adcs area of the hit (see `UseArea()`) + * @return photoelectrons corresponding to the specified `adcs` area + */ + double PE(double adcs, int channel) const override; + + /** + * @brief Whether calibration parameter is area or peak amplitude. + * @return `true` (this calibration is area-based) + */ + bool UseArea() const override; + + /// Prepares the calibration for data from the specified `run`. + void readCalibrationFromDB(unsigned int run); + + /** + * @brief Returns the database timestamp-like tag appropriate to the `run`. + * + * The backend (`lariov::DBFolder`) only takes 19-digit numbers + * (timestamp in nanoseconds), but our database tables are currently using + * run numbers, baked so that a run number `XXXXX` results into the + * timestamp `1'000'0XX'XXX'000'000'000`. + */ + std::uint64_t RunToDatabaseTimestamp( unsigned int run ) const; + + /// Get current area database tag + std::string getAreaDatabaseTag() const& { return fAreaTag; } + + /// Returns the SPE area [ADC x tick] for the given channel (default if not in DB). + double getSPEArea(int channel) const + { return getChannelCalibOrDefault(channel).speArea; } + + /// Returns the SPE fit width (sigma) [ADC x tick] for the given channel (default if not in DB). + double getSPEFitWidth(int channel) const + { return getChannelCalibOrDefault(channel).speFitWidth; } + + private: + + using PhotonCalibratorInfo = details::PhotonCalibratorInfo; + + PhotonCalibratorInfo const fCalibDefaults; ///< Default calibration values. + bool const fVerbose; + std::string const fLogCategory; + std::string const fAreaTag; + int const fOverrideRunNumber; ///< If non-negative, overrides the run number for DB queries. + + lariov::DBFolder fDB; ///< Cached database interface. + + /// Map: channel to calibration information. + std::map fDatabaseSPECalibrations; + + /// Internal access to the channel calibration record; returns defaults if not present. + PhotonCalibratorInfo const& getChannelCalibOrDefault(int channelID) const{ + auto const it = fDatabaseSPECalibrations.find(channelID); + return (it == fDatabaseSPECalibrations.end())? fCalibDefaults: it->second; + } + +}; // class icarusDB::PhotonCalibratorFromDB + + +#endif // ICARUSCODE_PMT_CALIBRATION_PHOTONCALIBRATORFROMDB_H diff --git a/icaruscode/PMT/Calibration/VerticalTrackFlashWaveformAna_module.cc b/icaruscode/PMT/Calibration/VerticalTrackFlashWaveformAna_module.cc new file mode 100644 index 000000000..831ba61c4 --- /dev/null +++ b/icaruscode/PMT/Calibration/VerticalTrackFlashWaveformAna_module.cc @@ -0,0 +1,665 @@ +//////////////////////////////////////////////////////////////////////// +// Class: VerticalTrackFlashWaveformAna +// Plugin Type: analyzer +// File: VerticalTrackFlashWaveformAna_module.cc +// +// Select nearly-vertical cosmic muons inside a Z-slice, match them to a +// recob::OpFlash by YZ-barycenter proximity, and dump for every PMT in +// the flash the OpHit info (integral/amplitude/time/pe) together with +// the single raw::OpDetWaveform covering the flash time on that channel. +// +// Output tree feeds the PMT gain calibration on vertical cosmic muons. +// +// mailto:mvicenzi@bnl.gov +//////////////////////////////////////////////////////////////////////// + +#include "art/Framework/Core/EDAnalyzer.h" +#include "art/Framework/Core/ModuleMacros.h" +#include "art/Framework/Principal/Event.h" +#include "art/Framework/Principal/Handle.h" +#include "art_root_io/TFileService.h" +#include "canvas/Persistency/Common/FindManyP.h" +#include "canvas/Persistency/Common/Ptr.h" +#include "canvas/Utilities/InputTag.h" +#include "fhiclcpp/types/Atom.h" +#include "fhiclcpp/types/Sequence.h" +#include "messagefacility/MessageLogger/MessageLogger.h" + +#include "larcore/Geometry/WireReadout.h" +#include "larcore/CoreUtils/ServiceUtil.h" // lar::providerFrom() +#include "larcorealg/Geometry/OpDetGeo.h" +#include "lardata/DetectorInfoServices/DetectorClocksService.h" +#include "lardataalg/DetectorInfo/DetectorTimings.h" +#include "lardataobj/RawData/OpDetWaveform.h" +#include "lardataobj/RecoBase/Hit.h" +#include "lardataobj/RecoBase/OpFlash.h" +#include "lardataobj/RecoBase/OpHit.h" +#include "lardataobj/RecoBase/SpacePoint.h" +#include "lardataobj/RecoBase/Track.h" +#include "lardataobj/Simulation/SimPhotons.h" + +#include "icaruscode/Timing/IPMTTimingCorrectionService.h" +#include "icaruscode/Timing/PMTTimingCorrections.h" + +#include "TTree.h" + +#include +#include +#include +#include + +namespace pmtcalib { + class VerticalTrackFlashWaveformAna; +} + +class pmtcalib::VerticalTrackFlashWaveformAna : public art::EDAnalyzer { +public: + struct Config { + using Name = fhicl::Name; + using Comment = fhicl::Comment; + + fhicl::Sequence TrackLabels{ + Name("TrackLabels"), + Comment("recob::Track labels, one per cryostat (E, W)")}; + + fhicl::Sequence OpFlashLabels{ + Name("OpFlashLabels"), + Comment("recob::OpFlash labels, one per cryostat (E, W) in the same order as TrackLabels")}; + + fhicl::Sequence PandoraLabels{ + Name("PandoraLabels"), + Comment("Pandora producer labels (one per cryostat, E/W), used to fetch the" + " hit->SpacePoint association for the track charge barycenter")}; + + fhicl::Atom OpDetWaveformLabel{ + Name("OpDetWaveformLabel"), + Comment("Single raw::OpDetWaveform collection covering both cryostats." + " Leave empty to skip collecting and saving raw waveforms."), + ""}; + + fhicl::Atom SimPhotonsLabel{ + Name("SimPhotonsLabel"), + Comment("Single sim::SimPhoton collection covering both cryostats"), + ""}; + + fhicl::Atom MinAbsDirY{ + Name("MinAbsDirY"), + Comment("Minimum |dir.Y()| for the track to be considered vertical"), + 0.90}; + + fhicl::Atom ZMin{ + Name("ZMin"), + Comment("Minimum Z (cm): both track endpoints must have Z >= ZMin"), + -500.0}; + + fhicl::Atom ZMax{ + Name("ZMax"), + Comment("Maximum Z (cm): both track endpoints must have Z <= ZMax"), + 500.0}; + + fhicl::Atom MinTrackLength{ + Name("MinTrackLength"), + Comment("Minimum track length in cm"), + 100.0}; + + fhicl::Atom RequireDownwards{ + Name("RequireDownwards"), + Comment("If true, only keep tracks going downward (dir.Y() < 0 after optional flip)"), + true}; + + fhicl::Atom MaxDeltaX{ + Name("MaxDeltaX"), + Comment("Max |X_end - X_start| (cm) between track endpoints"), + 20.0}; + + fhicl::Atom MaxDeltaZ{ + Name("MaxDeltaZ"), + Comment("Max |Z_end - Z_start| (cm) between track endpoints"), + 20.0}; + + fhicl::Atom BarycenterMaxDist{ + Name("BarycenterMaxDist"), + Comment("Max YZ distance (cm) between track midpoint and flash barycenter"), + 30.0}; + }; + + using Parameters = art::EDAnalyzer::Table; + + explicit VerticalTrackFlashWaveformAna(Parameters const& config); + + VerticalTrackFlashWaveformAna(VerticalTrackFlashWaveformAna const&) = delete; + VerticalTrackFlashWaveformAna(VerticalTrackFlashWaveformAna&&) = delete; + VerticalTrackFlashWaveformAna& operator=(VerticalTrackFlashWaveformAna const&) = delete; + VerticalTrackFlashWaveformAna& operator=(VerticalTrackFlashWaveformAna&&) = delete; + + void beginJob() override; + void analyze(art::Event const& e) override; + +private: + // --- configuration --- + std::vector fTrackLabels; + std::vector fOpFlashLabels; + std::vector fPandoraLabels; + art::InputTag fOpDetWaveformLabel; + art::InputTag fSimPhotonsLabel; + double fMinAbsDirY; + double fZMin; + double fZMax; + double fMinTrackLength; + bool fRequireDownwards; + double fMaxDeltaX; + double fMaxDeltaZ; + double fBarycenterMaxDist; + + // --- detector timing (optical tick in us) --- + double fOpticalTickUs = 0.002; // ICARUS PMT digitizer: 500 MHz -> 2 ns + + // --- PMT timing corrections service (DB-provided) --- + // OpHit/OpFlash times already include these corrections, but raw + // OpDetWaveform::TimeStamp() does not. We add the same correction to the + // waveform timestamp when checking coverage of a flash time. + icarusDB::PMTTimingCorrections const* fTimingCorrections = nullptr; + + // --- output tree --- + TTree* fTree = nullptr; + int fNSelTracks = 0; + + // event + unsigned int fRun = 0; + unsigned int fEvent = 0; + int fCryo = -1; + + // track + double fTrkStartX = 0., fTrkStartY = 0., fTrkStartZ = 0.; + double fTrkEndX = 0., fTrkEndY = 0., fTrkEndZ = 0.; + double fTrkDirX = 0., fTrkDirY = 0., fTrkDirZ = 0.; + double fTrkLength = 0.; + double fTrkBaryX = 0., fTrkBaryY = 0., fTrkBaryZ = 0.; + + // flash + double fFlashTime = 0.; + double fFlashTotalPE = 0.; + double fFlashY = 0., fFlashZ = 0.; + double fFlashWidthY = 0., fFlashWidthZ = 0.; + int fFlashNPMTs = 0; + double fFlashBarycenterDist = 0.; + + // per-PMT scalars (one tree row per PMT/waveform) + int fPMTChannel = -1; + double fPMTOpHitIntegral = 0.; + double fPMTOpHitAmplitude = 0.; + double fPMTOpHitPeakTime = 0.; + double fPMTOpHitStartTime = 0.; + double fPMTOpHitPE = 0.; + double fPMTTotalPE = 0.; + std::vector fPMTWaveform; + double fPMTWaveformTimestamp = 0.; + double fPMTPosX = 0., fPMTPosY = 0., fPMTPosZ = 0.; + double fPMTBarycenterDist = 0.; + + // simphotons + int fNSimPhotons = 0; + std::vector fSimPhotonTimes; + std::vector fSimPhotonInitX; + std::vector fSimPhotonInitY; + std::vector fSimPhotonInitZ; + + // --- helpers (stubs; to be filled in next iteration) --- + + /// Initialise branches on the output tree. + void setupTree(); + + /// Clear all per-entry buffers. + void resetEntry(); + + /// Select a nearly-vertical track inside the Z-slice. + /// Returns true and fills start/end/dir/length into members if selected. + bool selectVerticalTrack(recob::Track const& track); + + /// Find the OpFlash in `flashes` whose YZ-barycenter is closest to + /// (trkY, trkZ); return its index (or -1 if none within BarycenterMaxDist). + /// On success sets fFlashBarycenterDist. + int matchFlashByBarycenter(std::vector> const& flashes, + double trkY, double trkZ); + + /// Total timing correction (us) to apply to a raw waveform TimeStamp so + /// that it lives in the same reference frame as OpHit/OpFlash times. + /// Corrections are additive; the sum matches what OpHitTimingCorrection + /// applies to OpHits. + double getTimingCorrection(raw::Channel_t channel) const; + + /// Scan the full waveform collection and return the raw::OpDetWaveform on + /// `channel` whose time window [TimeStamp+corr, TimeStamp+corr+size*tick) + /// contains `flashTime`. `correction` is the per-channel correction already + /// extracted once by the caller. Returns nullptr if none found. + raw::OpDetWaveform const* findCoveringWaveform( + std::vector const& wfs, + raw::Channel_t channel, + double flashTime, + double correction) const; + + /// Fill per-PMT vectors for the matched flash by looping the waveform + /// collection once per PMT channel in the flash. + void fillFlashPMTs( + recob::OpFlash const& flash, + std::vector> const& ophits, + std::vector const& wfs, + std::vector const& simphotonsCollection, + detinfo::DetectorTimings const& timings); +}; + +// ===================================================================== + +pmtcalib::VerticalTrackFlashWaveformAna::VerticalTrackFlashWaveformAna(Parameters const& config) + : art::EDAnalyzer{config} + , fTrackLabels{config().TrackLabels()} + , fOpFlashLabels{config().OpFlashLabels()} + , fPandoraLabels{config().PandoraLabels()} + , fOpDetWaveformLabel{config().OpDetWaveformLabel()} + , fSimPhotonsLabel{config().SimPhotonsLabel()} + , fMinAbsDirY{config().MinAbsDirY()} + , fZMin{config().ZMin()} + , fZMax{config().ZMax()} + , fMinTrackLength{config().MinTrackLength()} + , fRequireDownwards{config().RequireDownwards()} + , fMaxDeltaX{config().MaxDeltaX()} + , fMaxDeltaZ{config().MaxDeltaZ()} + , fBarycenterMaxDist{config().BarycenterMaxDist()} + , fTimingCorrections{lar::providerFrom()} +{ + if (fTrackLabels.size() != fOpFlashLabels.size() + || fTrackLabels.size() != fPandoraLabels.size()) { + throw art::Exception(art::errors::Configuration) + << "TrackLabels, OpFlashLabels and PandoraLabels must all have the same" + " size (one per cryostat)."; + } +} + +void pmtcalib::VerticalTrackFlashWaveformAna::beginJob() +{ + setupTree(); + fNSelTracks = 0; +} + +void pmtcalib::VerticalTrackFlashWaveformAna::analyze(art::Event const& e) +{ + fRun = e.run(); + fEvent = e.event(); + + // Refresh the optical tick period from DetectorClocksService each event + // (cheap and keeps data/MC config consistent). + auto const clocks = + art::ServiceHandle()->DataFor(e); + fOpticalTickUs = clocks.OpticalClock().TickPeriod(); + + // Single collections spanning both cryostats. + static const std::vector emptyWfs; + std::vector const& wfs = (!fOpDetWaveformLabel.empty()) + ? e.getProduct>(fOpDetWaveformLabel) + : emptyWfs; + + auto const timings = detinfo::makeDetectorTimings(clocks); + + static const std::vector emptySimPhotons; + auto const & simphotonsCollection = (!fSimPhotonsLabel.empty()) + ? e.getProduct>(fSimPhotonsLabel) + : emptySimPhotons; + + for (std::size_t icryo = 0; icryo < fTrackLabels.size(); ++icryo) { + auto const& trackHandle = + e.getValidHandle>(fTrackLabels[icryo]); + auto const& tracks = *trackHandle; + auto const& flashHandle = + e.getValidHandle>(fOpFlashLabels[icryo]); + auto const& flashes = *flashHandle; + + if (flashes.empty()) continue; + + // Promote OpFlash vector to Ptrs (for the matcher signature) and fetch + // the flash->OpHit associations once per cryostat. + std::vector> flashPtrs; + flashPtrs.reserve(flashes.size()); + for (std::size_t i = 0; i < flashes.size(); ++i) + flashPtrs.emplace_back(flashHandle, i); + + art::FindManyP ophitsPtr(flashHandle, e, fOpFlashLabels[icryo]); + + // Track -> Hit association, from the track producer (pattern borrowed + // from sbncode TrackCaloSkimmer_module). + art::FindManyP fmtrkHits(trackHandle, e, fTrackLabels[icryo]); + + std::vector> const emptyHitVector; + + for (std::size_t itrk = 0; itrk < tracks.size(); ++itrk) { + resetEntry(); + if (!selectVerticalTrack(tracks[itrk])) continue; + + art::Ptr const trkPtr(trackHandle, itrk); + std::vector> const& trkHits = + fmtrkHits.isValid() ? fmtrkHits.at(trkPtr.key()) : emptyHitVector; + + // Charge-weighted track barycenter using hit Integral as weight and + // the associated SpacePoint(s) for 3D position — same chain as + // sbncode TrackCaloSkimmer: Hit -> SpacePoint assns live under the + // pandora (PFP) producer label. + art::FindManyP fmtrkHitSPs(trkHits, e, fPandoraLabels[icryo]); + + double sumCharge = 0.; + double sumX = 0., sumY = 0., sumZ = 0.; + for (std::size_t k = 0; k < trkHits.size(); ++k) { + auto const& sps = fmtrkHitSPs.at(k); + if (sps.empty()) continue; + double const q = trkHits[k]->Integral(); + auto const* xyz = sps.front()->XYZ(); + sumCharge += q; + sumX += xyz[0] * q; + sumY += xyz[1] * q; + sumZ += xyz[2] * q; + } + if (sumCharge <= 0.) continue; + fTrkBaryX = sumX / sumCharge; + fTrkBaryY = sumY / sumCharge; + fTrkBaryZ = sumZ / sumCharge; + + int const iflash = matchFlashByBarycenter(flashPtrs, fTrkBaryY, fTrkBaryZ); + if (iflash < 0) continue; + + auto const& flash = flashes[iflash]; + fFlashTime = flash.Time(); + fFlashTotalPE = flash.TotalPE(); + fFlashY = flash.YCenter(); + fFlashZ = flash.ZCenter(); + fFlashWidthY = flash.YWidth(); + fFlashWidthZ = flash.ZWidth(); + + mf::LogInfo("VerticalTrackFlashWaveformAna") << "Vertical track matched to flash: " + << "\n TrackYDir: " << fTrkDirY + << "\n TrackX: " << fTrkStartX << " " << fTrkBaryX << " " << fTrkEndX + << "\n TrackY: " << fTrkStartY << " " << fTrkBaryY << " " << fTrkEndY + << "\n TrackZ: " << fTrkStartZ << " " << fTrkBaryZ << " " << fTrkEndZ + << "\n TrkLength: " << fTrkLength + << "\n FlashTime: " << fFlashTime + << "\n FlashY: " << fFlashY + << "\n FlashZ: " << fFlashZ + << "\n FlashPE: " << fFlashTotalPE; + + fNSelTracks++; + fCryo = static_cast(icryo); + + auto const& ophits = ophitsPtr.at(iflash); + fillFlashPMTs(flash, ophits, wfs, simphotonsCollection, timings); + + } + } + + mf::LogInfo("VerticalTrackFlashWaveformAna") << "Total selected tracks: " << fNSelTracks; +} + +// --- helper stubs (bodies to be added in the next step) ----------------- + +void pmtcalib::VerticalTrackFlashWaveformAna::setupTree() +{ + art::ServiceHandle tfs; + fTree = tfs->make("vertical", "Vertical muon / flash / PMT waveform tree"); + + fTree->Branch("run", &fRun); + fTree->Branch("event", &fEvent); + fTree->Branch("cryo", &fCryo); + + fTree->Branch("trk_start_x", &fTrkStartX); + fTree->Branch("trk_start_y", &fTrkStartY); + fTree->Branch("trk_start_z", &fTrkStartZ); + fTree->Branch("trk_end_x", &fTrkEndX); + fTree->Branch("trk_end_y", &fTrkEndY); + fTree->Branch("trk_end_z", &fTrkEndZ); + fTree->Branch("trk_dir_x", &fTrkDirX); + fTree->Branch("trk_dir_y", &fTrkDirY); + fTree->Branch("trk_dir_z", &fTrkDirZ); + fTree->Branch("trk_length", &fTrkLength); + fTree->Branch("trk_barycenter_x", &fTrkBaryX); + fTree->Branch("trk_barycenter_y", &fTrkBaryY); + fTree->Branch("trk_barycenter_z", &fTrkBaryZ); + + fTree->Branch("flash_time", &fFlashTime); + fTree->Branch("flash_total_pe", &fFlashTotalPE); + fTree->Branch("flash_y", &fFlashY); + fTree->Branch("flash_z", &fFlashZ); + fTree->Branch("flash_width_y", &fFlashWidthY); + fTree->Branch("flash_width_z", &fFlashWidthZ); + fTree->Branch("flash_n_pmts", &fFlashNPMTs); + fTree->Branch("flash_barycenter_dist", &fFlashBarycenterDist); + + fTree->Branch("pmt_channel", &fPMTChannel, "pmt_channel/I"); + fTree->Branch("pmt_ophit_integral", &fPMTOpHitIntegral, "pmt_ophit_integral/D"); + fTree->Branch("pmt_ophit_amplitude", &fPMTOpHitAmplitude, "pmt_ophit_amplitude/D"); + fTree->Branch("pmt_ophit_peak_time", &fPMTOpHitPeakTime, "pmt_ophit_peak_time/D"); + fTree->Branch("pmt_ophit_start_time", &fPMTOpHitStartTime, "pmt_ophit_start_time/D"); + fTree->Branch("pmt_ophit_pe", &fPMTOpHitPE, "pmt_ophit_pe/D"); + fTree->Branch("pmt_total_pe", &fPMTTotalPE, "pmt_total_pe/D"); + fTree->Branch("pmt_pos_x", &fPMTPosX, "pmt_pos_x/D"); + fTree->Branch("pmt_pos_y", &fPMTPosY, "pmt_pos_y/D"); + fTree->Branch("pmt_pos_z", &fPMTPosZ, "pmt_pos_z/D"); + fTree->Branch("pmt_barycenter_dist", &fPMTBarycenterDist, "pmt_barycenter_dist/D"); + + if (!fOpDetWaveformLabel.empty()) { + fTree->Branch("pmt_waveform", &fPMTWaveform); + fTree->Branch("pmt_waveform_timestamp", &fPMTWaveformTimestamp, "pmt_waveform_timestamp/D"); + } + + // SimPhotons are filtered against the raw waveform time window, so they + // are only meaningful when waveforms are being saved too. + if (!fSimPhotonsLabel.empty() && !fOpDetWaveformLabel.empty()) + { + fTree->Branch("n_sim_photons", &fNSimPhotons, "n_sim_photons/I"); + fTree->Branch("sim_photon_times", &fSimPhotonTimes); + fTree->Branch("sim_photon_init_x", &fSimPhotonInitX); + fTree->Branch("sim_photon_init_y", &fSimPhotonInitY); + fTree->Branch("sim_photon_init_z", &fSimPhotonInitZ); + } + +} + +void pmtcalib::VerticalTrackFlashWaveformAna::resetEntry() +{ + fCryo = -1; + fTrkStartX = fTrkStartY = fTrkStartZ = 0.; + fTrkEndX = fTrkEndY = fTrkEndZ = 0.; + fTrkDirX = fTrkDirY = fTrkDirZ = 0.; + fTrkLength = 0.; + fTrkBaryX = fTrkBaryY = fTrkBaryZ = 0.; + + fFlashTime = fFlashTotalPE = 0.; + fFlashY = fFlashZ = fFlashWidthY = fFlashWidthZ = 0.; + fFlashNPMTs = 0; + fFlashBarycenterDist = 0.; + + fPMTChannel = -1; + fPMTOpHitIntegral = 0.; + fPMTOpHitAmplitude = 0.; + fPMTOpHitPeakTime = 0.; + fPMTOpHitStartTime = 0.; + fPMTOpHitPE = 0.; + fPMTTotalPE = 0.; + fPMTWaveform.clear(); + fPMTWaveformTimestamp = 0.; + fPMTPosX = fPMTPosY = fPMTPosZ = 0.; + + fNSimPhotons = 0; + fSimPhotonTimes.clear(); + fSimPhotonInitX.clear(); + fSimPhotonInitY.clear(); + fSimPhotonInitZ.clear(); +} + +bool pmtcalib::VerticalTrackFlashWaveformAna::selectVerticalTrack(recob::Track const& track) +{ + + if (track.Length() < fMinTrackLength) return false; + + auto const start = track.Start(); + auto const end = track.End(); + + if (start.Z() < fZMin || start.Z() > fZMax) return false; + if (end.Z() < fZMin || end.Z() > fZMax) return false; + if (std::abs(end.X() - start.X()) > fMaxDeltaX) return false; + if (std::abs(end.Z() - start.Z()) > fMaxDeltaZ) return false; + + auto dir = track.StartDirection(); + // Flip to downgoing if requested (same idiom as TimeTrackTreeStorageCRT). + if (fRequireDownwards && dir.Y() > 0.0) dir = -track.EndDirection(); + if (fRequireDownwards && dir.Y() >= 0.0) return false; + if (std::abs(dir.Y()) < fMinAbsDirY) return false; + + fTrkStartX = start.X(); fTrkStartY = start.Y(); fTrkStartZ = start.Z(); + fTrkEndX = end.X(); fTrkEndY = end.Y(); fTrkEndZ = end.Z(); + fTrkDirX = dir.X(); fTrkDirY = dir.Y(); fTrkDirZ = dir.Z(); + fTrkLength = track.Length(); + + return true; +} + +int pmtcalib::VerticalTrackFlashWaveformAna::matchFlashByBarycenter( + std::vector> const& flashes, + double trkY, double trkZ) +{ + int best = -1; + double bestDist = std::numeric_limits::max(); + for (std::size_t i = 0; i < flashes.size(); ++i) { + double const dy = flashes[i]->YCenter() - trkY; + double const dz = flashes[i]->ZCenter() - trkZ; + double const d = std::sqrt(dy * dy + dz * dz); + if (d < bestDist) { bestDist = d; best = static_cast(i); } + } + if (best < 0 || bestDist > fBarycenterMaxDist) return -1; + fFlashBarycenterDist = bestDist; + return best; +} + +double pmtcalib::VerticalTrackFlashWaveformAna::getTimingCorrection( + raw::Channel_t channel) const +{ + // Same additive combination applied to OpHits in OpHitTimingCorrection. + return fTimingCorrections->getLaserCorrections(channel) + + fTimingCorrections->getCosmicsCorrections(channel); +} + +raw::OpDetWaveform const* +pmtcalib::VerticalTrackFlashWaveformAna::findCoveringWaveform( + std::vector const& wfs, + raw::Channel_t channel, + double flashTime, + double correction) const +{ + // Linear scan (performance is not a concern). Both OpDetWaveform::TimeStamp() + // and OpFlash::Time() are in microseconds. The raw timestamp is un-corrected, + // while flashTime is already corrected, so shift the waveform window by the + // per-channel correction before the containment check. + for (auto const& wf : wfs) { + if (wf.ChannelNumber() != channel) continue; + double const t0 = wf.TimeStamp() + correction; + double const t1 = t0 + wf.size() * fOpticalTickUs; + if (flashTime >= t0 && flashTime < t1) return &wf; + } + return nullptr; +} + +void pmtcalib::VerticalTrackFlashWaveformAna::fillFlashPMTs( + recob::OpFlash const& flash, + std::vector> const& ophits, + std::vector const& wfs, + std::vector const& simphotonsCollection, + detinfo::DetectorTimings const& timings) +{ + auto const& channelMap = art::ServiceHandle()->Get(); + + bool const saveWaveforms = !fOpDetWaveformLabel.empty(); + + // A flash can have more than one OpHit on the same channel. Following + // ICARUSFlashAssAna, keep only the earliest-in-time ophit per channel and + // record its values for that single hit. In parallel, accumulate the sum + // of PE over all hits on the channel for this flash (pmt_total_pe). + std::map firstHit; + std::map totalPEperChannel; + for (auto const& hit : ophits) { + auto const ch = hit->OpChannel(); + auto [it, inserted] = firstHit.try_emplace(ch, hit.get()); + if (!inserted && hit->StartTime() < it->second->StartTime()) + it->second = hit.get(); + totalPEperChannel[ch] += hit->PE(); + } + + fFlashNPMTs = static_cast(firstHit.size()); + + for (auto const& [ch, hitPtr] : firstHit) { + recob::OpHit const& hit = *hitPtr; + + raw::OpDetWaveform const* wf = nullptr; + if (saveWaveforms) { + // Extract the per-channel correction once, then use it to shift the + // raw waveform timestamp into the same frame as the flash time. + double const correction = getTimingCorrection(ch); + wf = findCoveringWaveform(wfs, ch, flash.AbsTime(), correction); + if (!wf) { + mf::LogWarning("VerticalTrackFlashWaveformAna") + << "No raw::OpDetWaveform covering flash AbsTime=" << flash.AbsTime() + << " us on channel " << ch; + continue; + } + } + + auto const pmtPos = channelMap.OpDetGeoFromOpChannel(ch).GetCenter(); + + fPMTChannel = static_cast(ch); + fPMTOpHitIntegral = hit.Area(); + fPMTOpHitAmplitude = hit.Amplitude(); + fPMTOpHitPeakTime = hit.PeakTime(); + fPMTOpHitStartTime = hit.StartTime(); + fPMTOpHitPE = hit.PE(); + fPMTTotalPE = totalPEperChannel[ch]; + fPMTPosX = pmtPos.X(); + fPMTPosY = pmtPos.Y(); + fPMTPosZ = pmtPos.Z(); + fPMTBarycenterDist = std::hypot( fPMTPosX - fTrkBaryX, fPMTPosY - fTrkBaryY, fPMTPosZ - fTrkBaryZ ); + + fPMTWaveform.clear(); + fPMTWaveformTimestamp = 0.; + fNSimPhotons = 0; + fSimPhotonTimes.clear(); + fSimPhotonInitX.clear(); + fSimPhotonInitY.clear(); + fSimPhotonInitZ.clear(); + + if (saveWaveforms) { + fPMTWaveform = wf->Waveform(); + fPMTWaveformTimestamp = wf->TimeStamp() + getTimingCorrection(fPMTChannel); + + // SimPhotons belonging to this channel and falling inside the current + // waveform window. Collection is empty when no SimPhotons product is + // available (data, or MC without photon propagation). + double const wfstart = fPMTWaveformTimestamp; + double const wfend = wfstart + wf->size() * fOpticalTickUs; + + for (auto const& simphotons : simphotonsCollection) { + + if (simphotons.OpChannel() != fPMTChannel) continue; + + for (auto const& ph : simphotons) { + detinfo::timescales::simulation_time const photonTime{ ph.Time }; + double const t = timings.toElectronicsTime(photonTime).value(); + + if (t < wfstart || t > wfend) continue; + + fSimPhotonTimes.push_back(t); + fSimPhotonInitX.push_back(ph.InitialPosition.X()); + fSimPhotonInitY.push_back(ph.InitialPosition.Y()); + fSimPhotonInitZ.push_back(ph.InitialPosition.Z()); + } + } + fNSimPhotons = static_cast(fSimPhotonTimes.size()); + } + + fTree->Fill(); + } +} + +DEFINE_ART_MODULE(pmtcalib::VerticalTrackFlashWaveformAna) diff --git a/icaruscode/PMT/Calibration/fcl/bkgphotons-calibration.fcl b/icaruscode/PMT/Calibration/fcl/bkgphotons-calibration.fcl index 87fa3bc7c..63232139e 100644 --- a/icaruscode/PMT/Calibration/fcl/bkgphotons-calibration.fcl +++ b/icaruscode/PMT/Calibration/fcl/bkgphotons-calibration.fcl @@ -5,7 +5,7 @@ #include "timing_icarus.fcl" #include "icarus_ophitfinder.fcl" -#include "pmt-calibration.fcl" +#include "pmt_calibration_icarus.fcl" process_name: BkgphotonsCalibration diff --git a/icaruscode/PMT/Calibration/fcl/debug_speAreas_db.fcl b/icaruscode/PMT/Calibration/fcl/debug_speAreas_db.fcl new file mode 100644 index 000000000..244886430 --- /dev/null +++ b/icaruscode/PMT/Calibration/fcl/debug_speAreas_db.fcl @@ -0,0 +1,19 @@ +### File: debug_speAreas_db +### Author: M. Carrigan + +### This test FHiCL runs the official `stage0` flow exposing +### the configuration of the `IPhotonCalibratorService` service +### to debug/verify the use of the speAreas database. + +#include "messages_icarus.fcl" +#include "stage0_run2_wcdnn_icarus.fcl" + +process_name: stage0p1 + +### expose speAreas DB service configuration: +services.IPhotonCalibratorService.Verbose: true +services.IPhotonCalibratorService.AreaTag: "v1r0" +services.IPMTTimingCorrectionService.Verbose: true + +### enable interactive deugging messages +services.message: @local::icarus_message_services_interactive_debug diff --git a/icaruscode/PMT/Calibration/fcl/decodePMT_icarus_laser.fcl b/icaruscode/PMT/Calibration/fcl/decodePMT_icarus_laser.fcl index 1df2579eb..73efa173c 100644 --- a/icaruscode/PMT/Calibration/fcl/decodePMT_icarus_laser.fcl +++ b/icaruscode/PMT/Calibration/fcl/decodePMT_icarus_laser.fcl @@ -6,7 +6,7 @@ #include "icarus_opana_modules.fcl" #include "rootoutput_icarus.fcl" #include "timing_icarus.fcl" -#include "pmt-calibration.fcl" +#include "pmt_calibration_icarus.fcl" process_name: laserAnalysis diff --git a/icaruscode/PMT/Calibration/fcl/ophit_recalibrator_icarus.fcl b/icaruscode/PMT/Calibration/fcl/ophit_recalibrator_icarus.fcl new file mode 100644 index 000000000..61082442f --- /dev/null +++ b/icaruscode/PMT/Calibration/fcl/ophit_recalibrator_icarus.fcl @@ -0,0 +1,48 @@ +### File: ophit_recalibrator_icarus.fcl +### Purpose: Default configuration for OpHitRecalibrator module. + +#include "calibration_database_GlobalTags_icarus.fcl" + +BEGIN_PROLOG + +### This module reads a reconstructed optical hit collection, removes the +### previously-applied gain and timing calibrations, and replaces them with +### the current ones (as defined by the active calibration services). +### +### The PE recalibration uses the gain database (ICARUSPhotonCalibratorServiceFromDB) +### to retrieve the SPE area by channel and run number. +### +### The timing recalibration requires knowing which corrections were originally +### applied to the input hits, so that they can be undone before applying the +### new ones. These "old" corrections are specified via OldTimingDBTags. + +ophit_recalibrator: +{ + module_type: OpHitRecalibrator + InputLabel: "ophit" + + RecalibratePE: true # whether to recalibrate ophit PEs + UseGainDatabase: true # whether to use speArea database for recalibration + SPEArea: -1.0 # if UseGainDatabase is false, use this SPEArea for all channels + + RecalibrateTime: true # whether to recalibrate ophit times + ### if time recalibration is active, specify which previous timing calibration needs to be removed + ### from the available tags listed in `calibration_database_PMT_TagSets_icarus.fcl`. + ### Note: these default old tags are actually the current ones - this means no recalibration happens; + ### the module uses the current ones to correct, so you would be subtracting and adding the same + ### Q: how do I know which tags to use to recalibrate my existing samples? + ### A: if you are asking this question, you shouldn't be recalibrating the times. + OldTimingDBTags: { + CorrectionTags: { + CablesTag: @local::ICARUS_Calibration_GlobalTags.pmt_cables_delays_data + LaserTag: @local::ICARUS_Calibration_GlobalTags.pmt_laser_timing_data + CosmicsTag: @local::ICARUS_Calibration_GlobalTags.pmt_cosmics_timing_data + } + Verbose: false + LogCategory: "OldPMTTimingCorrections" + } + + Verbose: false +} + +END_PROLOG diff --git a/icaruscode/PMT/Calibration/fcl/pmt-calibration.fcl b/icaruscode/PMT/Calibration/fcl/pmt-calibration.fcl deleted file mode 100644 index fed4a6092..000000000 --- a/icaruscode/PMT/Calibration/fcl/pmt-calibration.fcl +++ /dev/null @@ -1,30 +0,0 @@ -#include "calibrationtools.fcl" - -BEGIN_PROLOG - -pmt_laser_calibration: -{ - module_type: PMTLaserCalibration - OpDetWaveformLabel: "daqPMT" - PMTWaveformTimingCorrectionLabel: "" #or "daqPMT:globtrg" if the corrections are applied at decoder level already - LaserChannel: 1 - WaveformAnalysis : @local::laser_pulse_configuration - DebugMessage: false -} - -pmt_bkgphotons_calibration: -{ - module_type: PMTBackgroundphotonsCalibration - OpHitModule: "ophit" - TriggerModule: "daqTrigger" - ADCmV: 0.122 - ADCpC: 0.00488 - ChannelMask: [ ] - FilterInGate: [ -3., 7. ] #in us - FilterInTime: true - TimeWindow: 1.0 # in us - AmplitudeBinning: [ 0., 30., 0.15 ] # in mV - IntegralBinning: [ 0., 8., 0.04 ] # in 10^7 electrons -} - -END_PROLOG diff --git a/icaruscode/PMT/Calibration/fcl/pmt_calibration_icarus.fcl b/icaruscode/PMT/Calibration/fcl/pmt_calibration_icarus.fcl new file mode 100644 index 000000000..439a7da33 --- /dev/null +++ b/icaruscode/PMT/Calibration/fcl/pmt_calibration_icarus.fcl @@ -0,0 +1,91 @@ +# File: pmt_calibration_icarus.fcl +# Purpose: Configurations for ICARUS PMT calibration tools. +# Contact: Matteo Vicenzi (mvicenzi@bnl.gov) +# +# Provides the following configuration tables: +# +# * pmt_laser_calibration, pmt_bkgphotons_calibration: hit timing calibrations +# * icarus_photon_calibration: configuration of IPhotonCalibratorService +# + +#include "calibrationtools.fcl" +#include "icarus_spe.fcl" + +BEGIN_PROLOG + +# ============================================================================== +# === Hit time calibration +# === +pmt_laser_calibration: +{ + module_type: PMTLaserCalibration + OpDetWaveformLabel: "daqPMT" + PMTWaveformTimingCorrectionLabel: "" #or "daqPMT:globtrg" if the corrections are applied at decoder level already + LaserChannel: 1 + WaveformAnalysis : @local::laser_pulse_configuration + DebugMessage: false +} + +pmt_bkgphotons_calibration: +{ + module_type: PMTBackgroundphotonsCalibration + OpHitModule: "ophit" + TriggerModule: "daqTrigger" + ADCmV: 0.122 + ADCpC: 0.00488 + ChannelMask: [ ] + FilterInGate: [ -3., 7. ] #in us + FilterInTime: true + TimeWindow: 1.0 # in us + AmplitudeBinning: [ 0., 30., 0.15 ] # in mV + IntegralBinning: [ 0., 8., 0.04 ] # in 10^7 electrons +} + + +# ============================================================================== +# === Hit photoelectron conversion +# === +icarus_photon_calibration: +{ + service_provider: ICARUSPhotonCalibratorServiceFromDB + AreaTag: "v1r2" + Defaults: { + SPEArea: @local::SPERun2.Area # ADC x tick, Run2 default + } + OverrideRunNumber: -1 +} # icarus_photon_calibration + + +# ============================================================================== +# === Vertical-track flash / waveform ntupler (for calibration) +# === +verticaltrack_flashwf_ana: +{ + module_type: "VerticalTrackFlashWaveformAna" + + # per-cryostat track + flash collections (order must match: E, W) + TrackLabels: [ "pandoraTrackGausCryoE", "pandoraTrackGausCryoW" ] + OpFlashLabels: [ "opflashCryoE", "opflashCryoW" ] + PandoraLabels: [ "pandoraGausCryoE", "pandoraGausCryoW" ] + + # single merged collections covering both cryostats + OpDetWaveformLabel: "daqPMT" + SimPhotonsLabel: "" + + # vertical-track selection + MinAbsDirY: 0.98 + MaxDeltaX: 30.0 # cm + MaxDeltaZ: 30.0 # cm + ZMin: -700.0 # cm + ZMax: 700.0 # cm + MinTrackLength: 100.0 # cm + RequireDownwards: true + + # flash match + BarycenterMaxDist: 30.0 # cm (YZ distance) +} + + +# ============================================================================== + +END_PROLOG diff --git a/icaruscode/PMT/CopyBeamTimePMTwaveforms_module.cc b/icaruscode/PMT/CopyBeamTimePMTwaveforms_module.cc index cea2abd27..f8f29f023 100644 --- a/icaruscode/PMT/CopyBeamTimePMTwaveforms_module.cc +++ b/icaruscode/PMT/CopyBeamTimePMTwaveforms_module.cc @@ -335,8 +335,7 @@ icarus::CopyBeamTimePMTwaveforms::CopyBeamTimePMTwaveforms , fWaveMetaTag { config().OpDetWaveformMetaAssns().value_or(fWaveformTag) } , fWaveBaselineTag{ config().WaveformBaselineAssns().value_or(fWaveformTag) } , fWaveRMStag { config().WaveformRMSassns().value_or(fWaveBaselineTag) } - , fTargetInterval - { makeTimeInterval(config().SelectInterval()).value_or(DefaultInterval) } + , fTargetInterval { config().SelectInterval().value_or(DefaultInterval) } , fTimeReference { config().getTimeReference() } , fLogCategory { config().LogCategory() } // cached diff --git a/icaruscode/PMT/OpReco/Algorithms/CMakeLists.txt b/icaruscode/PMT/OpReco/Algorithms/CMakeLists.txt index 07daaf9b6..8db18d5e4 100644 --- a/icaruscode/PMT/OpReco/Algorithms/CMakeLists.txt +++ b/icaruscode/PMT/OpReco/Algorithms/CMakeLists.txt @@ -1,8 +1,10 @@ art_make_library( LIBRARIES larana::OpticalDetector_OpHitFinder - sbnobj::ICARUS_PMT_Data + lardataalg::UtilitiesHeaders + lardataalg::DetectorInfo lardataobj::RecoBase + sbnobj::ICARUS_PMT_Data lardataobj::RawData messagefacility::MF_MessageLogger fhiclcpp::fhiclcpp diff --git a/icaruscode/PMT/OpReco/Algorithms/OpRecoFactoryStuff.h b/icaruscode/PMT/OpReco/Algorithms/OpRecoFactoryStuff.h index 8a1173f76..053481960 100644 --- a/icaruscode/PMT/OpReco/Algorithms/OpRecoFactoryStuff.h +++ b/icaruscode/PMT/OpReco/Algorithms/OpRecoFactoryStuff.h @@ -30,6 +30,7 @@ // ----------------------------------------------------------------------------- // forward declarations namespace art { class Event; } +namespace pmtana { class RiseTimeCalculatorBase; } // ----------------------------------------------------------------------------- @@ -291,15 +292,21 @@ class opdet::factory::AlgorithmFactory { */ std::unique_ptr create (std::string const& name, fhicl::ParameterSet const& pset) const; - + + /// As `create(name, pset)`, also injecting a rise time calculator into the algorithm. + std::unique_ptr create( + std::string const& name, + fhicl::ParameterSet const& pset, + std::unique_ptr calc) const; + /** * @brief Creates an instance of the algorithm constructed with `pset`. * @param pset the configuration of the algorithm * @return the newly created algorithm object - * + * * The type of algorithm is discovered from `pset` itself with the mechanism * documented in `setAlgorithmConfigurationKey()`. - * + * * For example: * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} * factory.setAlgorithmConfigurationKey("Name"); @@ -311,6 +318,11 @@ class opdet::factory::AlgorithmFactory { * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ std::unique_ptr create(fhicl::ParameterSet const& pset) const; + + /// As `create(pset)`, also injecting a rise time calculator into the algorithm. + std::unique_ptr create( + fhicl::ParameterSet const& pset, + std::unique_ptr calc) const; /// Returns a list with the names of supported algorithms. std::vector names() const; @@ -449,7 +461,12 @@ struct opdet::factory::AlgorithmFactory::AlgoMaker { */ virtual std::unique_ptr makeAlgo (fhicl::ParameterSet const& pset) const = 0; - + + /// Creates an algorithm of the wrapped type, injecting a rise time calculator. + virtual std::unique_ptr makeAlgo( + fhicl::ParameterSet const& pset, + std::unique_ptr calc) const = 0; + /// Comparison: sort by name in lexicographic order. bool operator< (AlgoMaker const& other) const { return name < other.name; } @@ -468,7 +485,19 @@ struct opdet::factory::AlgorithmFactory::AlgoMakerFor std::unique_ptr makeAlgo (fhicl::ParameterSet const& pset) const override { return std::make_unique(pset); } - + + std::unique_ptr makeAlgo( + fhicl::ParameterSet const& pset, + std::unique_ptr calc) const override + { + if constexpr (std::is_constructible_v>) + return std::make_unique(pset, std::move(calc)); + else + return std::make_unique(pset); // algorithm does not use a rise time calculator + } + }; // opdet::factory::AlgorithmFactory::AlgoMakerFor @@ -534,6 +563,35 @@ auto opdet::factory::AlgorithmFactory::create } // opdet::factory::AlgorithmFactory::create() +// ----------------------------------------------------------------------------- +template +auto opdet::factory::AlgorithmFactory::create( + std::string const& name, + fhicl::ParameterSet const& pset, + std::unique_ptr calc) const + -> std::unique_ptr +{ + AlgoMaker const* maker = getMaker(name); + if (maker) return maker->makeAlgo(pset, std::move(calc)); + + throw cet::exception{ "AlgorithmFactory" } + << "Unknown algorithm: '" << name << "'.\nSupported algorithms:\n - '" + << names("'\n - '") << "'\n"; + +} // opdet::factory::AlgorithmFactory::create(name, pset, calc) + + +// ----------------------------------------------------------------------------- +template +auto opdet::factory::AlgorithmFactory::create( + fhicl::ParameterSet const& pset, + std::unique_ptr calc) const + -> std::unique_ptr +{ + return create(pset.get(fAlgoNameKey), pset, std::move(calc)); +} // opdet::factory::AlgorithmFactory::create(pset, calc) + + // ----------------------------------------------------------------------------- template std::vector opdet::factory::AlgorithmFactory::names() const { diff --git a/icaruscode/PMT/OpReco/FlashFinder/CMakeLists.txt b/icaruscode/PMT/OpReco/FlashFinder/CMakeLists.txt index c2650c8f8..ec3cafbbd 100644 --- a/icaruscode/PMT/OpReco/FlashFinder/CMakeLists.txt +++ b/icaruscode/PMT/OpReco/FlashFinder/CMakeLists.txt @@ -1,54 +1,21 @@ art_make_library( - LIBRARIES - icarusalg::Geometry - larcore::Geometry_Geometry_service - larcorealg::Geometry - lardataobj::RecoBase - canvas::canvas - cetlib::cetlib - cetlib_except::cetlib_except - art::Framework_Core - art::Framework_Principal - art::Framework_Services_Registry - art_root_io::tfile_support ROOT::Core - art_root_io::TFileService_service - art::Persistency_Common - art::Persistency_Provenance - art::Utilities - messagefacility::MF_MessageLogger - fhiclcpp::fhiclcpp - ROOT::Geom - ROOT::XMLIO - ROOT::Gdml + LIBRARIES + INTERFACE + fhiclcpp::fhiclcpp + PUBLIC + icarusalg::Geometry + larcore::Geometry_Geometry_service + larcorealg::Geometry + art::Framework_Services_Registry ) -set( MODULE_LIBRARIES - icaruscode::PMT_OpReco_FlashFinder - icarusalg::Geometry - larcore::Geometry_Geometry_service - larcorealg::Geometry - lardataobj::RecoBase - lardata::Utilities - larcore::Geometry_AuxDetGeometry_service - canvas::canvas - cetlib::cetlib - cetlib_except::cetlib_except - art::Framework_Core - art::Framework_Principal - art::Framework_Services_Registry - art_root_io::tfile_support - art_root_io::TFileService_service - art::Persistency_Common - art::Persistency_Provenance - art::Utilities - messagefacility::MF_MessageLogger - fhiclcpp::fhiclcpp - ROOT::Geom - ROOT::XMLIO - ROOT::Gdml - ROOT::Core -) -cet_build_plugin(ICARUSFlashFinder art::module LIBRARIES ${MODULE_LIBRARIES}) + +cet_build_plugin(ICARUSFlashFinder art::module + LIBRARIES + icaruscode::PMT_OpReco_FlashFinder + icaruscode::PMT_OpReco_Algorithms + lardataobj::RecoBase + ) install_headers() diff --git a/icaruscode/PMT/OpReco/FlashFinder/FlashFinderFMWKInterface.cxx b/icaruscode/PMT/OpReco/FlashFinder/FlashFinderFMWKInterface.cxx index 30f75cef1..4684d9f5e 100644 --- a/icaruscode/PMT/OpReco/FlashFinder/FlashFinderFMWKInterface.cxx +++ b/icaruscode/PMT/OpReco/FlashFinder/FlashFinderFMWKInterface.cxx @@ -1,19 +1,9 @@ #ifndef __FLASHFINDERFMWKINTERFACE_CXX__ #define __FLASHFINDERFMWKINTERFACE_CXX__ -#include "art/Framework/Core/EDProducer.h" -#include "art/Framework/Core/ModuleMacros.h" -#include "art/Framework/Principal/Event.h" -#include "art/Framework/Principal/Handle.h" -#include "art/Framework/Principal/Run.h" -#include "art/Framework/Principal/SubRun.h" -#include "canvas/Utilities/InputTag.h" -#include "fhiclcpp/ParameterSet.h" -#include "messagefacility/MessageLogger/MessageLogger.h" -#include "art_root_io/TFileService.h" #include "larcore/Geometry/WireReadout.h" #include "larcore/Geometry/Geometry.h" - +#include "art/Framework/Services/Registry/ServiceHandle.h" #include "FlashFinderFMWKInterface.h" namespace pmtana { diff --git a/icaruscode/PMT/OpReco/FlashFinder/FlashFinderFMWKInterface.h b/icaruscode/PMT/OpReco/FlashFinder/FlashFinderFMWKInterface.h index 62626852b..4ff1fb111 100644 --- a/icaruscode/PMT/OpReco/FlashFinder/FlashFinderFMWKInterface.h +++ b/icaruscode/PMT/OpReco/FlashFinder/FlashFinderFMWKInterface.h @@ -3,8 +3,10 @@ //#include "FhiclLite/ConfigManager.h" #include "fhiclcpp/ParameterSet.h" -#include "larcore/Geometry/Geometry.h" -#include + +#include // std::size_t +#include + namespace pmtana { //typedef ::fcllite::PSet Config_t; diff --git a/icaruscode/PMT/OpReco/FlashFinder/ICARUSFlashFinder_module.cc b/icaruscode/PMT/OpReco/FlashFinder/ICARUSFlashFinder_module.cc index 2a8469978..e2cc513af 100644 --- a/icaruscode/PMT/OpReco/FlashFinder/ICARUSFlashFinder_module.cc +++ b/icaruscode/PMT/OpReco/FlashFinder/ICARUSFlashFinder_module.cc @@ -1,58 +1,79 @@ -//////////////////////////////////////////////////////////////////////// -// Class: ICARUSFlashFinder -// Module Type: producer -// File: ICARUSFlashFinder_module.cc -// -// Generated at Tue Sep 13 22:30:26 2016 by Kazuhiro Terao using artmod -// from cetpkgsupport v1_10_02. -//////////////////////////////////////////////////////////////////////// +/** + * @file icaruscode/PMT/OpReco/FlashFinder/ICARUSFlashFinder_module.cc + * @brief ICARUS customization of flash reconstruction in LArSoft + * @author modified from Kazuhiro Terao's original module + */ + #include "icaruscode/PMT/OpReco/Algorithms/OpHitTimeSelector.h" -#include "art/Framework/Core/EDProducer.h" +#include "art/Framework/Core/SharedProducer.h" #include "art/Framework/Core/ModuleMacros.h" #include "art/Framework/Principal/Event.h" #include "art/Framework/Principal/Handle.h" -#include "art/Framework/Principal/Run.h" -#include "art/Framework/Principal/SubRun.h" +#include "art/Persistency/Common/PtrMaker.h" +#include "canvas/Persistency/Common/Assns.h" +#include "canvas/Persistency/Common/Ptr.h" #include "canvas/Utilities/InputTag.h" -#include "fhiclcpp/ParameterSet.h" +#include "canvas/Utilities/Exception.h" +#include "fhiclcpp/types/Atom.h" +#include "fhiclcpp/types/DelegatedParameter.h" #include "messagefacility/MessageLogger/MessageLogger.h" #include "lardataobj/RecoBase/OpHit.h" #include "lardataobj/RecoBase/OpFlash.h" -#include "lardata/Utilities/AssociationUtil.h" #include #include +#include // std::move() +#include #include "FlashFinderManager.h" -#include "FlashFinderFMWKInterface.h" -#include "PECalib.h" +#include "FlashFinderFMWKInterface.h" // ::pmtana::OpDetCenterFromOpChannel() -class ICARUSFlashFinder; -class ICARUSFlashFinder : public art::EDProducer { +class ICARUSFlashFinder : public art::SharedProducer { public: - explicit ICARUSFlashFinder(fhicl::ParameterSet const & p); - // The destructor generated by the compiler is fine for classes - // without bare pointers or other resource use. - - // Plugins should not be copied or assigned. - ICARUSFlashFinder(ICARUSFlashFinder const &) = delete; - ICARUSFlashFinder(ICARUSFlashFinder &&) = delete; - ICARUSFlashFinder & operator = (ICARUSFlashFinder const &) = delete; - ICARUSFlashFinder & operator = (ICARUSFlashFinder &&) = delete; + + struct Config { + + using Name = fhicl::Name; + using Comment = fhicl::Comment; + + fhicl::Atom OpHitProducer { + Name{ "OpHitProducer" }, + Comment{ "input tag for the optical hits to build the flashes from" } + }; + + fhicl::Atom FlashFinderAlgo { + Name{ "FlashFinderAlgo" }, + Comment{ "name of the flash finding algorithm" } + }; + + fhicl::Atom TimeType { + Name{ "TimeType" }, + Comment{ "Type of hit time to be used for flash building" }, + "Start" + }; + + fhicl::DelegatedParameter AlgoConfig { + Name{ "AlgoConfig" }, + Comment{ "configuration of the flash finding algorithm" } + }; + + }; // Config + + using Parameters = art::SharedProducer::Table; + + explicit ICARUSFlashFinder(Parameters const & p, art::ProcessingFrame const&); - // Required functions. - void produce(art::Event & e) override; + void produce(art::Event & e, art::ProcessingFrame const&) override; private: // Declare member data here. ::pmtana::FlashFinderManager _mgr; - ::pmtana::PECalib _pecalib; - std::string _hit_producer; + art::InputTag _hit_producer; /// Extracts a configured time from `recob::OpHit`. recob::OpHitTimeSelector const fHitTime; @@ -62,39 +83,40 @@ class ICARUSFlashFinder : public art::EDProducer { }; -ICARUSFlashFinder::ICARUSFlashFinder(pmtana::Config_t const & p) - : EDProducer{p} - , fHitTime{ recob::opHitTimeType(p.get("TimeType", "Start")) } +ICARUSFlashFinder::ICARUSFlashFinder(Parameters const & p, art::ProcessingFrame const&) + : art::SharedProducer{p} + , fHitTime{ recob::opHitTimeType(p().TimeType()) } // Initialize member data here. { - _hit_producer = p.get("OpHitProducer"); + async(); + + _hit_producer = p().OpHitProducer(); - auto const flash_algo = p.get("FlashFinderAlgo"); - auto const flash_pset = p.get("AlgoConfig"); + auto const flash_algo = p().FlashFinderAlgo(); + auto const flash_pset = p().AlgoConfig.get(); auto algo_ptr = ::pmtana::FlashAlgoFactory::get().create(flash_algo,flash_algo); algo_ptr->Configure(flash_pset); _mgr.SetFlashAlgo(algo_ptr); - _pecalib.Configure(p.get("PECalib")); - produces< std::vector >(); produces< art::Assns >(); } -void ICARUSFlashFinder::produce(art::Event & e) +void ICARUSFlashFinder::produce(art::Event & e, art::ProcessingFrame const&) { // produce OpFlash data-product to be filled within module - std::unique_ptr< std::vector > opflashes(new std::vector); - std::unique_ptr< art::Assns > flash2hit_assn_v ( new art::Assns ); + auto opflashes = std::make_unique>(); + auto flash2hit_assn_v = std::make_unique>(); // load OpHits previously created art::Handle > ophit_h; e.getByLabel(_hit_producer,ophit_h); // make sure hits look good if(!ophit_h.isValid()) { - std::cerr<<"\033[93m[ERROR]\033[00m ... could not locate OpHit!"< 1.e20) trigger_time = oph.PeakTimeAbs() - oph.PeakTime(); loph.peak_time = fHitTime(oph); - size_t opdet = ::pmtana::OpDetFromOpChannel(oph.OpChannel()); - loph.pe = _pecalib.Calibrate(opdet,oph.Area()); + loph.pe = oph.PE(); loph.channel = oph.OpChannel(); ophits.emplace_back(std::move(loph)); } auto const flash_v = _mgr.RecoFlash(ophits); + art::PtrMaker makeFlashPtr{ e }; for(const auto& lflash : flash_v) { double Ycenter, Zcenter, Ywidth, Zwidth; @@ -125,9 +147,10 @@ void ICARUSFlashFinder::produce(art::Event & e) Ycenter, Ywidth, Zcenter, Zwidth); opflashes->emplace_back(std::move(flash)); + art::Ptr const flashPtr = makeFlashPtr(opflashes->size() - 1); for(auto const& hitidx : lflash.asshit_idx) { const art::Ptr hit_ptr(ophit_h, hitidx); - util::CreateAssn(*this, e, *opflashes, hit_ptr, *flash2hit_assn_v); + flash2hit_assn_v->addSingle(hit_ptr, flashPtr); } } diff --git a/icaruscode/PMT/OpReco/ICARUSFlashAssAna_module.cc b/icaruscode/PMT/OpReco/ICARUSFlashAssAna_module.cc index acd96a95e..1fc5a4c75 100644 --- a/icaruscode/PMT/OpReco/ICARUSFlashAssAna_module.cc +++ b/icaruscode/PMT/OpReco/ICARUSFlashAssAna_module.cc @@ -144,6 +144,7 @@ class opana::ICARUSFlashAssAna : public art::EDAnalyzer std::vector &pmt_start_rwm_time, std::vector &pmt_pe, std::vector &pmt_amplitude, + std::vector &pmt_integral, TTree *ophittree); /// Return RWM-relative time from a trigger-relative time @@ -219,6 +220,7 @@ class opana::ICARUSFlashAssAna : public art::EDAnalyzer std::vector m_pmt_start_time_rwm; std::vector m_pmt_pe; std::vector m_pmt_amplitude; + std::vector m_pmt_integral; // Ophit trees int m_channel_id; @@ -409,6 +411,7 @@ void opana::ICARUSFlashAssAna::beginJob() ttree->Branch("time_pmt_rwm", &m_pmt_start_time_rwm); ttree->Branch("pe_pmt", &m_pmt_pe); ttree->Branch("amplitude_pmt", &m_pmt_amplitude); + ttree->Branch("integral_pmt", &m_pmt_integral); fOpFlashTrees.push_back(ttree); @@ -627,6 +630,7 @@ void opana::ICARUSFlashAssAna::processOpHitsFlash(std::vector &pmt_start_time_rwm, std::vector &pmt_pe, std::vector &pmt_amplitude, + std::vector &pmt_integral, TTree *ophittree) { @@ -671,6 +675,7 @@ void opana::ICARUSFlashAssAna::processOpHitsFlash(std::vectorFill(); @@ -869,6 +874,7 @@ void opana::ICARUSFlashAssAna::analyze(art::Event const &e) m_pmt_rise_time.resize(360); m_pmt_start_time_rwm.resize(360); m_pmt_amplitude.resize(360); + m_pmt_integral.resize(360); m_flash_id = idx; m_flash_time = flash.Time(); @@ -894,7 +900,7 @@ void opana::ICARUSFlashAssAna::analyze(art::Event const &e) m_multiplicity_left, m_multiplicity_right, m_sum_pe_left, m_sum_pe_right, m_pmt_start_time, m_pmt_rise_time, m_pmt_start_time_rwm, m_pmt_pe, m_pmt_amplitude, - fOpHitFlashTrees[iFlashLabel]); + m_pmt_integral, fOpHitFlashTrees[iFlashLabel]); m_multiplicity = m_multiplicity_left + m_multiplicity_right; @@ -908,6 +914,8 @@ void opana::ICARUSFlashAssAna::analyze(art::Event const &e) m_pmt_start_time.clear(); m_pmt_rise_time.clear(); m_pmt_start_time_rwm.clear(); + m_pmt_amplitude.clear(); + m_pmt_integral.clear(); idx++; } diff --git a/icaruscode/PMT/OpReco/ICARUSOpHitFinder_module.cc b/icaruscode/PMT/OpReco/ICARUSOpHitFinder_module.cc index f9ae98c29..94f63c16d 100644 --- a/icaruscode/PMT/OpReco/ICARUSOpHitFinder_module.cc +++ b/icaruscode/PMT/OpReco/ICARUSOpHitFinder_module.cc @@ -8,11 +8,13 @@ */ // ICARUS libraries +#include "icaruscode/PMT/Status/IPMTChannelStatusService.h" #include "icaruscode/PMT/OpReco/Algorithms/PedAlgoFixed.h" #include "icaruscode/PMT/OpReco/Algorithms/OpRecoFactoryStuff.h" #include "icaruscode/PMT/Data/WaveformRMS.h" #include "sbnobj/ICARUS/PMT/Data/WaveformBaseline.h" // LArSoft libraries +#include "larana/OpticalDetector/OpHitFinder/RiseTimeTools/RiseTimeCalculatorBase.h" #include "larana/OpticalDetector/OpHitFinder/AlgoCFD.h" #include "larana/OpticalDetector/OpHitFinder/AlgoFixedWindow.h" #include "larana/OpticalDetector/OpHitFinder/AlgoSiPM.h" @@ -45,9 +47,11 @@ #include "messagefacility/MessageLogger/MessageLogger.h" #include "fhiclcpp/types/Sequence.h" #include "fhiclcpp/types/OptionalAtom.h" +#include "fhiclcpp/types/OptionalDelegatedParameter.h" #include "fhiclcpp/types/Atom.h" #include "fhiclcpp/types/DelegatedParameter.h" #include "fhiclcpp/ParameterSet.h" +#include "art/Utilities/make_tool.h" // C++ standard libraries #include // std::copy_if(), std::binary_search() @@ -244,13 +248,18 @@ class opdet::ICARUSOpHitFinder: public art::ReplicatedProducer { fhicl::DelegatedParameter PedAlgoPset { Name{ "PedAlgoPset" }, - Comment{ + Comment{ "parameters of the pedestal extraction algorithm." " The parameters must include the algorithm \"Name\", one of: " + PedAlgoFactory.names(", ") + "." } }; - + + fhicl::OptionalDelegatedParameter RiseTimeCalculator { + Name{ "RiseTimeCalculator" }, + Comment{ "Configuration of the rise time calculator art tool (optional)."} + }; + fhicl::Atom UseCalibrator { Name{ "UseCalibrator" }, Comment{ "Use the photon calibration service configured in the job" }, @@ -260,21 +269,24 @@ class opdet::ICARUSOpHitFinder: public art::ReplicatedProducer { fhicl::Atom AreaToPE { Name{ "AreaToPE" }, Comment{ "Whether the `SPEArea` parameter refers to area or amplitude" }, - [this](){ return !UseCalibrator(); } }; fhicl::Atom SPEArea { Name{ "SPEArea" }, Comment{ "area or amplitude of PMT response to single photoelectron" }, - [this](){ return !UseCalibrator(); } }; fhicl::Atom SPEShift { Name{ "SPEShift" }, Comment{ "shift on the single photoelectron response" }, - [this](){ return !UseCalibrator(); } }; - + + fhicl::Atom UseChannelStatus { + Name{ "UseChannelStatusDatabase" }, + Comment{"ignore waveforms on channels flagged in PMT channel status database."}, + false + }; + }; // Config using Parameters = art::ReplicatedProducer::Table; @@ -298,12 +310,15 @@ class opdet::ICARUSOpHitFinder: public art::ReplicatedProducer { // --- BEGIN -- Cached service values ---------------------------------------- /// Storage for our own calibration algorithm, if any. std::unique_ptr fMyCalib; - + unsigned int const fMaxOpChannel; ///< Number of channels in the detector. - + /// The calibration algorithm to be used (not owned). calib::IPhotonCalibrator const* fCalib = nullptr; - + + /// Optional PMT channel status provider (not owned); null if not in use. + icarusDB::PMTChannelStatus const* fChannelStatus = nullptr; + // --- END ---- Cached service values ---------------------------------------- @@ -325,6 +340,10 @@ class opdet::ICARUSOpHitFinder: public art::ReplicatedProducer { std::vector selectWaveforms (std::vector const& waveforms) const; + /// Creates a rise time calculator tool from config, or returns nullptr if not configured. + static std::unique_ptr + makeRiseTimeCalculator(Config const& config); + }; // opdet::ICARUSOpHitFinder @@ -494,7 +513,8 @@ opdet::ICARUSOpHitFinder::ICARUSOpHitFinder // algorithms , fPulseRecoMgr{} , fThreshAlg - { HitAlgoFactory.create(params().HitAlgoPset.get()) } + { HitAlgoFactory.create(params().HitAlgoPset.get(), + makeRiseTimeCalculator(params())) } , fPedAlg { PedAlgoFactory.create(params().PedAlgoPset.get()) } { @@ -511,14 +531,15 @@ opdet::ICARUSOpHitFinder::ICARUSOpHitFinder // if (params().UseCalibrator()) { fCalib = frame.serviceHandle()->provider(); + mf::LogInfo{ "ICARUSOpHitFinder" } << "Using 'calib::IPhotonCalibratorService' for gain calibration."; } else { - fMyCalib = std::make_unique( - params().AreaToPE() - , params().SPEArea() - , params().SPEShift() - ); + fMyCalib = std::make_unique(params().SPEArea(), params().SPEShift(), params().AreaToPE()); fCalib = fMyCalib.get(); + mf::LogInfo{ "ICARUSOpHitFinder" } << "Using 'AreaToPE' = " << params().AreaToPE() + << ", 'SPEArea' = " << params().SPEArea() + << ", 'SPEShift' = " << params().SPEShift() + << " for gain calibration."; } // if ... else // @@ -527,8 +548,16 @@ opdet::ICARUSOpHitFinder::ICARUSOpHitFinder fPulseRecoMgr.AddRecoAlgo(fThreshAlg.get()); fPulseRecoMgr.SetDefaultPedAlgo(&(fPedAlg->algo())); + // + // channel status service + // + if (params().UseChannelStatus()){ + fChannelStatus = frame.serviceHandle()->provider(); + mf::LogInfo{ "ICARUSOpHitFinder" } << "Using 'icarusDB::IPMTChannelStatusService' for channel status."; + } + // framework hooks to the algorithms - if (!fChannelMasks.empty() && (fPedAlg->algo().Name() == "Fixed")) { + if ((!fChannelMasks.empty() || fChannelStatus) && (fPedAlg->algo().Name() == "Fixed")) { /* TODO * The reason why this is not working is complicate. * The interface of ophit mini-framework is quite minimal and tight, @@ -550,7 +579,7 @@ opdet::ICARUSOpHitFinder::ICARUSOpHitFinder */ throw art::Exception{ art::errors::Configuration } << "Pedestal algorithm \"Fixed\" will not work" - " since a channel mask list is specified.\n"; + " since a channel mask list or channel status database is specified.\n"; } fPedAlg->initialize(consumesCollector()); @@ -577,17 +606,18 @@ void opdet::ICARUSOpHitFinder::produce // MaybeOwnerPtr const> waveforms // = fChannelMasks.empty()? &allWaveforms: selectWaveforms(allWaveforms); using WaveformsPtr_t = MaybeOwnerPtr const>; - WaveformsPtr_t waveforms = fChannelMasks.empty() - ? WaveformsPtr_t{ &allWaveforms } - : WaveformsPtr_t{ selectWaveforms(allWaveforms) } + bool const needsSelection = !fChannelMasks.empty() || fChannelStatus; + WaveformsPtr_t waveforms = needsSelection + ? WaveformsPtr_t{ selectWaveforms(allWaveforms) } + : WaveformsPtr_t{ &allWaveforms } ; - - if (fChannelMasks.empty() && (&allWaveforms != &*waveforms)) { + + if (!needsSelection && (&allWaveforms != &*waveforms)) { // if this happens, contact the author throw art::Exception{ art::errors::LogicError } << "Bug in MaybeOwnerPtr!\n"; } - assert(fChannelMasks.empty() || (&allWaveforms == &*waveforms)); + assert(needsSelection || (&allWaveforms == &*waveforms)); // // run the algorithm @@ -652,20 +682,34 @@ std::vector opdet::ICARUSOpHitFinder::selectWaveforms (std::vector const& waveforms) const { std::vector selected; - - auto const isNotMasked = [this](raw::OpDetWaveform const& waveform) + + auto const isGood = [this](raw::OpDetWaveform const& waveform) { - return !std::binary_search - (fChannelMasks.begin(), fChannelMasks.end(), waveform.ChannelNumber()); + unsigned int const ch = waveform.ChannelNumber(); + if (std::binary_search(fChannelMasks.begin(), fChannelMasks.end(), ch)) + return false; + if (fChannelStatus && !fChannelStatus->isGood(ch)) + return false; + return true; }; - + std::copy_if - (waveforms.begin(), waveforms.end(), back_inserter(selected), isNotMasked); - + (waveforms.begin(), waveforms.end(), back_inserter(selected), isGood); + return selected; } // selectWaveforms() +// ----------------------------------------------------------------------------- +std::unique_ptr +opdet::ICARUSOpHitFinder::makeRiseTimeCalculator(Config const& config) +{ + fhicl::ParameterSet calcPset; + if (!config.RiseTimeCalculator.get_if_present(calcPset)) return nullptr; + return art::make_tool(calcPset); +} // opdet::ICARUSOpHitFinder::makeRiseTimeCalculator() + + // ----------------------------------------------------------------------------- DEFINE_ART_MODULE(opdet::ICARUSOpHitFinder) diff --git a/icaruscode/PMT/OpReco/fcl/icarus_flashalgo.fcl b/icaruscode/PMT/OpReco/fcl/icarus_flashalgo.fcl index a85a3f5cd..f8f69fd82 100644 --- a/icaruscode/PMT/OpReco/fcl/icarus_flashalgo.fcl +++ b/icaruscode/PMT/OpReco/fcl/icarus_flashalgo.fcl @@ -25,9 +25,9 @@ SimpleFlashCryo1.OpChannelRange: [180,359] SimpleFlashDataCryo0: @local::SimpleFlashCryo0 -SimpleFlashDataCryo0.PEThreshold: 100 +SimpleFlashDataCryo0.PEThreshold: 200 SimpleFlashDataCryo0.PEThresholdHit: 1.0 -SimpleFlashDataCryo0.MinPECoinc: 100 +SimpleFlashDataCryo0.MinPECoinc: 200 SimpleFlashDataCryo0.MinMultCoinc: 5 SimpleFlashDataCryo0.IntegralTime: 1. SimpleFlashDataCryo0.PreSample: 0.02 @@ -35,9 +35,9 @@ SimpleFlashDataCryo0.VetoSize: 1. SimpleFlashDataCryo0.TimeResolution: 0.01 SimpleFlashDataCryo1: @local::SimpleFlashCryo1 -SimpleFlashDataCryo1.PEThreshold: 100 +SimpleFlashDataCryo1.PEThreshold: 200 SimpleFlashDataCryo1.PEThresholdHit: 1.0 -SimpleFlashDataCryo1.MinPECoinc: 100 +SimpleFlashDataCryo1.MinPECoinc: 200 SimpleFlashDataCryo1.MinMultCoinc: 5 SimpleFlashDataCryo1.IntegralTime: 1. SimpleFlashDataCryo1.PreSample: 0.02 diff --git a/icaruscode/PMT/OpReco/fcl/icarus_flashcalib.fcl b/icaruscode/PMT/OpReco/fcl/icarus_flashcalib.fcl deleted file mode 100644 index 7d5e6c5b5..000000000 --- a/icaruscode/PMT/OpReco/fcl/icarus_flashcalib.fcl +++ /dev/null @@ -1,34 +0,0 @@ -#include "icarus_spe.fcl" -BEGIN_PROLOG - - -# ------------------------------------------------------------------------------ -# `icarus_pmt_calibration.NoCalib`: no calibration -# -# ICARUS flash reconstruction algorithm requires a calibration. -# To specify a uniform, neutral one, no per-channel calibration must be present, -# and the value of the uniform calibration constant (`SPEAreaGain`, which is -# the area of a single photoelectron, and gets applied to the area reconstructed -# in the hit) needs to be the same value as the single photoelectron area used -# in the hit reconstruction. -# -icarus_pmt_calibration.NoCalib: { - SPEAreaGain: @local::SPE.Area -} - - -# ------------------------------------------------------------------------------ -# `icarus_pmt_calibration.Calib202203`: from March 2021 -# -icarus_pmt_calibration.Calib202203: { - SPEAreaGainList: [224.71,198.23,154.69,218.39,238.21,226.8,189.98,88.385,222.74,220.79,185.43,198.98,229.2,224.65,193.04,194.62,236.86,234.61,194.43,215.73,126.42,211.69,179.43,219.68,225.3,210.01,137.62,217.75,225.57,214.77,231.14,219.41,222.46,178.45,202.72,227.86,207.64,192.96,230.19,212.81,211.35,237.02,110.3,152.77,206.04,196.72,216.93,218.24,237.21,245.52,192.8,230.92,198.44,213.29,172.93,225.95,209.04,213.43,237.86,209.36,197.67,230.27,198.49,213.6,220.15,214.87,228.84,174.13,218.41,235.18,212.00,248.12,231.56,196.43,203.63,223.45,214.92,195.28,192.86,225.79,199.11,228.97,190.79,218.85,222.27,239.62,225.06,227.93,224.61,234.47,215.38,219.44,212.73,242.96,244.61,177.73,227.32,195.65,100.48,226.48,219.12,258.86,205.76,222.81,214.16,158.06,212.00,197.28,234.6,211.98,239.79,231.64,150.32,220.72,225.85,214.42,235.73,209.24,200.15,246.71,183.2,207.64,168.28,192.63,242.94,231.86,237.89,226.96,268.3,222.72,172.12,220.36,241.53,217.6,244.78,226.92,205.75,191.26,216.74,201.13,245.61,238.33,209.16,230.68,227.13,237.73,198.57,227.07,238.81,216.39,229.88,224.39,227.83,144.7,221.17,224.97,229.11,257.89,209.14,193.76,202.99,202.58,220.89,192.8,241.04,237.97,221.4,190.61,173.98,230.05,233.6,229.89,210.46,205.01,216.45,189.68,206.79,170.98,227.99,228.59,243.36,234.23,182.01,218.3,227.44,222.65,219.1,214.98,244.06,184.55,221.53,207.17,197.95,202.28,169.44,211.24,218,217.29,178.64,194.82,191.16,210.72,209.46,213.2,218.53,186.48,208.5,174.21,204.95,232.61,218.39,191.14,221.21,207.32,224.88,230.77,203.83,212.00,209.08,190.09,209.31,120.18,218.14,192.27,208.5,227.18,189.34,228.71,170.11,201.98,194.01,237.13,204.44,168.91,212.84,177.22,174.4,192.25,233.12,217.92,216.25,186.88,251.41,198.01,188.71,186.18,230.6,223.84,241.12,126.47,210.76,216.96,207.41,187.7,227.39,191.68,223.61,219.75,195.01,218.12,246.68,170.33,202.68,202.71,180.27,203.78,238.17,228.4,245.86,221.34,114.05,210.85,235.41,218.78,233.84,205.64,190.31,353.47,173.92,206.03,220.24,218.9,177.34,195.85,239.26,227.36,144.55,217.93,193.29,225.07,196.95,215.85,214.71,161.5,204.15,206.98,197.99,195.59,223.56,203.89,213.82,237.94,207.08,211.44,196.66,275.77,209.58,235.03,184.84,232.76,183.93,191.53,225.68,197.71,193.24,220.97,199.82,205.31,185.86,224.14,211.68,220.71,187.16,197.19,201.05,179.91,214.8,181.65,199.92,228.79,230.76,206.95,208,168.79,190.62,219.97,185.73,184.76,220.46,225.44,185.96,197.85,186.5,219.79,216.31,190.62,193.4,206.51,215.88,179.34,184.8,210.46,214.49,169.85,235.33,350.00,106.58,191.15,185.43,170.06] - -} - - -# ------------------------------------------------------------------------------ -icarus_pmt_calibration.CalibStandard: @local::icarus_pmt_calibration.NoCalib - - - -END_PROLOG diff --git a/icaruscode/PMT/OpReco/fcl/icarus_flashfinder.fcl b/icaruscode/PMT/OpReco/fcl/icarus_flashfinder.fcl index 6b76b04c7..9725c4fc2 100644 --- a/icaruscode/PMT/OpReco/fcl/icarus_flashfinder.fcl +++ b/icaruscode/PMT/OpReco/fcl/icarus_flashfinder.fcl @@ -1,5 +1,4 @@ #include "icarus_flashalgo.fcl" -#include "icarus_flashcalib.fcl" BEGIN_PROLOG @@ -9,7 +8,6 @@ ICARUSSimpleFlash: FlashFinderAlgo : "SimpleFlashAlgo" AlgoConfig : @local::SimpleFlashStandard OpHitProducer : "ophit" - PECalib : @local::icarus_pmt_calibration.CalibStandard } ICARUSSimpleFlashCryoE: @local::ICARUSSimpleFlash diff --git a/icaruscode/PMT/OpReco/fcl/icarus_opana_modules.fcl b/icaruscode/PMT/OpReco/fcl/icarus_opana_modules.fcl index 628ef5c5d..6d39330ca 100644 --- a/icaruscode/PMT/OpReco/fcl/icarus_opana_modules.fcl +++ b/icaruscode/PMT/OpReco/fcl/icarus_opana_modules.fcl @@ -25,8 +25,8 @@ ICARUSMCOpHit: { module_type: "ICARUSMCOpHit" MergePeriod: 0.01 SimPhotonsProducer: "pdfastsim" - SPEArea: @local::SPE.Area - SPEAmplitude: @local::SPE.Amplitude + SPEArea: @local::SPERun2.Area + SPEAmplitude: @local::SPERun2.Amplitude } ICARUSMCOpFlash: { diff --git a/icaruscode/PMT/OpReco/fcl/icarus_ophitfinder.fcl b/icaruscode/PMT/OpReco/fcl/icarus_ophitfinder.fcl index 072ab19d5..18240ac18 100644 --- a/icaruscode/PMT/OpReco/fcl/icarus_ophitfinder.fcl +++ b/icaruscode/PMT/OpReco/fcl/icarus_ophitfinder.fcl @@ -146,7 +146,7 @@ icarus_opreco_hit_slidingwindow: { ADCThreshold: 5 # ADC threshold (absolute) above pedestal mean to fire a pulse NSigmaThreshold: 3 # ADC threshold (N*pedestal sigma) above pedestal mean to fire a pulse TailADCThreshold: 2 # ADC threshold (absolute) below which next pulse is allowed to fire - TailNSigmaThreshold: 2 # ADC threshold (N*pedestal sigma) below which next pulse is allowed to fire + TailNSigma: 2 # ADC threshold (N*pedestal sigma) below which next pulse is allowed to fire EndADCThreshold: 1 # ADC threshold (absolute) at which the pulse ends EndNSigmaThreshold: 1 # ADC threshold (N*pedetal sigma) at which the pulse ends MinPulseWidth: 2 # The width of a pulse needs to be equal or larger than this to be recorded @@ -162,7 +162,7 @@ icarus_opreco_hit_slidingwindow_201910: { # based on icarus_opreco_hit_slidingwi ADCThreshold: 10 # ADC threshold (absolute) above pedestal mean to fire a pulse NSigmaThreshold: 3 # ADC threshold (N*pedestal sigma) above pedestal mean to fire a pulse TailADCThreshold: 6 # ADC threshold (absolute) below which next pulse is allowed to fire - TailNSigmaThreshold: 2 # ADC threshold (N*pedestal sigma) below which next pulse is allowed to fire + TailNSigma: 2 # ADC threshold (N*pedestal sigma) below which next pulse is allowed to fire EndADCThreshold: 2 # ADC threshold (absolute) at which the pulse ends EndNSigmaThreshold: 1 # ADC threshold (N*pedetal sigma) at which the pulse ends MinPulseWidth: 5 # The width of a pulse needs to be equal or larger than this to be recorded @@ -186,24 +186,38 @@ icarus_opreco_hit_cfd: { icarus_ophit: # some basic configuration { - module_type: "OpHitFinder" + ## in the past we were using directly module "OpHitFinder" from larana + ## we are now using our own entry point to the reconstruction "ICARUSOpHitFinder" + ## this allows interfacing with our PMT channel status service + module_type: "ICARUSOpHitFinder" GenModule: "generator" InputModule: "opdaq" - InputLabels: [] - ChannelMasks: [] - HitThreshold: 0.2 # PE + #InputLabels: [] # not supported by ICARUSOpHitFinder + + ChannelMasks: [] + UseChannelStatusDatabase: true # skip channels flagged in the status database + + UseCalibrator: true # use calibrator service (SPEArea database) + # if true, overrides SPEArea / SPEShift below AreaToPE: true # Use area to calculate number of PEs - SPEArea: @local::SPE.Area # If AreaToPE is true, this number is - # used as single PE area (in ADC counts) - SPEShift: @local::SPE.Shift # Used by PhotonCalibratorStandard to compute - # PE = area / SPEArea + SPEShift + SPEArea: @local::SPERun2.Area # If AreaToPE is true, this number is + # used as single PE area (in ADC counts) + SPEShift: @local::SPERun2.Shift # Used by PhotonCalibratorStandard to compute + # PE = area / SPEArea + SPEShift + UseStartTime: false # start and peak times each in its own data member - reco_man: @local::standard_preco_manager + + #reco_man: @local::standard_preco_manager # now managed internally by ICARUSOpHitFinder + + HitThreshold: 0.2 # PE HitAlgoPset: @local::icarus_opreco_hit_slidingwindow_201910 + PedAlgoPset: @local::icarus_opreco_pedestal_DocDB24969 + RiseTimeCalculator: { - tool_type: RiseTimeThreshold - PeakRatio: 0.15 # at 15% of the peak amplitude -- not tuned + tool_type: RiseTimeThreshold + PeakRatio: 0.15 # at 15% of the peak amplitude -- not tuned + #InterpolateSamples: true # sub-sample linear interpolation } } @@ -215,7 +229,8 @@ icarus_ophitdebugger.OutputFile: "ophit_debug.root" # this is the "standard" ICARUS configuration for MC optical hit reconstruction: icarus_ophit_MC: { @table::icarus_ophit - PedAlgoPset: @local::icarus_opreco_pedestal_MC_DocDB24969 + UseCalibrator: false + UseChannelStatusDatabase: false InputModule: "opdaq" } @@ -228,7 +243,6 @@ icarus_ophitdebugger_MC.OutputFile: "ophit_debug.root" icarus_ophit_data: { @table::icarus_ophit InputModule: "daqPMT" -# PedAlgoPset: @local::icarus_opreco_pedestal_DocDB24969 } icarus_ophitdebugger_data: @local::icarus_ophit_data diff --git a/icaruscode/PMT/OpReco/fcl/icarus_spe.fcl b/icaruscode/PMT/OpReco/fcl/icarus_spe.fcl index d1666f42f..e988c60b8 100644 --- a/icaruscode/PMT/OpReco/fcl/icarus_spe.fcl +++ b/icaruscode/PMT/OpReco/fcl/icarus_spe.fcl @@ -1,6 +1,11 @@ BEGIN_PROLOG +### The values below represent the average SPE amplitude/area across all PMT channels. +### The default optical reconstruction now uses the `IPhotonCalibratorService` to +### properly pick-up channel-by-channel SPEArea values for a given time (run number). +### These values here are kept as defaults to support older workflows. + SPEorig: { Area: 212. Amplitude: 49.18 @@ -22,6 +27,40 @@ SPE202401patch: { # amplitude set to 3.23 mV, gain 7.5x10^6 with R = 50 ohm Shift: 0. } +# the following is based on the selection of single-PE ophits from RUN-2 data +# this is a data-driven choice, already including the reconstruction biases +# see SBN DocbDB 45565, slide 13-14 +# NB: this is much smaller than before due to the integral definition +# resulting from the official baseline reco when using ophits; +# since this value is used on ophits, it's good to natually include this bias. +SPE2025Run2: { + Area: 152.01 # ADC x (2 ns) + Amplitude: 28.08 # ADC + Shift: 0. +} + +# the following is based on the selection of single-PE ophits from RUN-3 data +# this is a data-driven choice, already including the reconstruction biases +# see SBN DocbDB 45565, slide 13-14 +SPE2025Run3: { + Area: 127.39 # ADC x (2 ns) + Amplitude: 29.91 # ADC + Shift: 0. +} + +# the following is based on the selection of single-PE ophits from RUN-4 data +# this is a data-driven choice, already including the reconstruction biases +# see SBN DocbDB 45565, slide 13-14 +SPE2025Run4: { + Area: 131.83 # ADC x (2 ns) + Amplitude: 31.14 # ADC + Shift: 0. +} + +SPERun2: @local::SPE2025Run2 +SPERun3: @local::SPE2025Run3 +SPERun4: @local::SPE2025Run4 + SPE: @local::SPE202401patch END_PROLOG diff --git a/icaruscode/PMT/OpReco/fcl/tpcpmtbarycentermatch_config.fcl b/icaruscode/PMT/OpReco/fcl/tpcpmtbarycentermatch_config.fcl index 03ebf930c..0c3de5db3 100644 --- a/icaruscode/PMT/OpReco/fcl/tpcpmtbarycentermatch_config.fcl +++ b/icaruscode/PMT/OpReco/fcl/tpcpmtbarycentermatch_config.fcl @@ -9,6 +9,7 @@ tpcpmtbarycentermatch_common_params: UseTimeRange: true Verbose: false FillMatchTree: false + TriggerDelay: 0.6 TriggerTolerance: 0.15 TimeRangeMargin: 35. } @@ -20,13 +21,11 @@ tpcpmtbarycentermatch_common_params: tpcpmtbarycentermatch_data_params: { TriggerLabel: "daqTrigger" - TriggerDelay: 0.6 } tpcpmtbarycentermatch_mc_params: { TriggerLabel: "emuTrigger" - TriggerDelay: 0.02 } ###### diff --git a/icaruscode/PMT/SampledWaveformFunctionTool_tool.cc b/icaruscode/PMT/SampledWaveformFunctionTool_tool.cc index 8f2059ed7..6c0f3a049 100644 --- a/icaruscode/PMT/SampledWaveformFunctionTool_tool.cc +++ b/icaruscode/PMT/SampledWaveformFunctionTool_tool.cc @@ -93,6 +93,8 @@ namespace icarus::opdet { struct SampledWaveformFunctionTool; } * gain of the response, and that response will be rescaled from its gain * value to the one specified with this parameter. If not specified, * the response is used as is. + * * `BiasRatio` (real, optional): a user-configurable ratio associated with + * the waveform gain; it affects the bias of the response (default: 1.0). * */ struct icarus::opdet::SampledWaveformFunctionTool @@ -119,7 +121,12 @@ struct icarus::opdet::SampledWaveformFunctionTool Name("Gain"), Comment("PMT amplification gain factor") }; - + + fhicl::OptionalAtom BiasRatio { + Name("BiasRatio"), + Comment("user-configurable bias of the response gain (default: 1.0)") + }; + }; // struct Config @@ -351,10 +358,13 @@ auto icarus::opdet::SampledWaveformFunctionTool::makePulseFunction // // create the algorithm // + double const biasRatio = config.BiasRatio().value_or(1.0); + return std::make_unique( std::move(waveformSpecs) // waveformSpecs , config.TransitTime() // peakTime , reqGain // gain + , biasRatio // bias ); } // icarus::opdet::SampledWaveformFunctionTool::makePulseFunction() diff --git a/icaruscode/PMT/SimPMTIcarus_module.cc b/icaruscode/PMT/SimPMTIcarus_module.cc index 98bdaca6c..dce586625 100644 --- a/icaruscode/PMT/SimPMTIcarus_module.cc +++ b/icaruscode/PMT/SimPMTIcarus_module.cc @@ -10,12 +10,16 @@ #include "icaruscode/PMT/PMTpedestalGeneratorTool.h" #include "icaruscode/PMT/PMTnoiseGeneratorTool.h" #include "icaruscode/PMT/SinglePhotonPulseFunctionTool.h" +#include "icaruscode/PMT/Calibration/ICARUSPhotonCalibratorServiceFromDB.h" +#include "icaruscode/PMT/Status/IPMTChannelStatusService.h" #include "icaruscode/PMT/Algorithms/OpDetWaveformMetaUtils.h" // OpDetWaveformMetaMaker #include "icaruscode/PMT/Algorithms/PMTsimulationAlg.h" #include "icaruscode/PMT/Algorithms/PedestalGeneratorAlg.h" #include "icaruscode/PMT/Algorithms/NoiseGeneratorAlg.h" #include "icaruscode/PMT/Algorithms/PhotoelectronPulseFunction.h" #include "icaruscode/IcarusObj/OpDetWaveformMeta.h" +#include "icaruscode/Timing/IPMTTimingCorrectionService.h" +#include "sbnobj/ICARUS/PMT/Data/WaveformBaseline.h" // LArSoft libraries #include "larcore/CoreUtils/ServiceUtil.h" @@ -29,6 +33,7 @@ #include "nurandom/RandomUtils/NuRandomService.h" // framework libraries +#include "art_root_io/TFileService.h" #include "art/Framework/Core/EDProducer.h" #include "art/Framework/Core/ModuleMacros.h" #include "art/Framework/Principal/Event.h" @@ -46,6 +51,9 @@ #include "fhiclcpp/types/Atom.h" #include "fhiclcpp/ParameterSet.h" +// ROOT libraries +#include "TTree.h" + // CLHEP libraries #include "CLHEP/Random/RandEngine.h" // CLHEP::HepRandomEngine @@ -58,7 +66,6 @@ #include // std::move() #include - namespace icarus::opdet { /** @@ -110,6 +117,9 @@ namespace icarus::opdet { * The module utilizes as input a collection of `sim::SimPhotons`, each * containing the photons propagated to a single optical detector channel. * + * If the `sim::SimPhotons` collection is not available, a collection of + * `sim::SimPhotonsLite` is used instead if found. + * * * Output * ======= @@ -118,6 +128,10 @@ namespace icarus::opdet { * (`std::vector`) is produced. * See `icarus::opdet::PMTsimulationAlg` algorithm documentation for details. * + * A collection of baselines (`std::vector`) and + * associations to their waveform is also produced, using the configured + * baseline as value for all the waveforms. + * * If `MakeMetadata` configuration parameter is set `true`, a collection of * `std::vector` is produced, one per waveform, in the * same order as the waveform data product (satisfying the @@ -199,6 +213,12 @@ namespace icarus::opdet { fhicl::TableFragment algoConfig; + fhicl::Atom UseChannelStatusDatabase { + Name("UseChannelStatusDatabase"), + Comment("skip simulation of channels flagged in the PMT channel status DB"), + false + }; + fhicl::Atom MakeMetadata { Name("MakeMetadata"), Comment("writes a metadata object for each waveform"), @@ -239,6 +259,12 @@ namespace icarus::opdet { "HepJamesRandom" }; + fhicl::Atom DebugTree { + Name("DebugTree"), + Comment("enable debug tree output"), + false + }; + }; // struct Config using Parameters = art::EDProducer::Table; @@ -253,6 +279,7 @@ namespace icarus::opdet { SimPMTIcarus & operator = (SimPMTIcarus &&) = delete; // Required functions. + virtual void beginJob() override; void produce(art::Event & e) override; private: @@ -265,12 +292,53 @@ namespace icarus::opdet { using PedestalGenerator_t = icarus::opdet::PMTpedestalGeneratorTool::Generator_t; + /// Debug tree data (one entry per optical channel per event) + struct DebugInfo_t { + // debug tree branch variables (one entry per optical channel per event) + // event identification + int run = -1; ///< Run number. + int event = -1; ///< Event number. + int channel = -1; ///< Optical channel number. + // per-channel timing parameters + float timeDelay_us = 0; ///< Per-channel timing delay applied [us]: laser+cosmics DB corrections. + float triggerOffsetPMT_us = 0; ///< Global PMT readout start offset relative to trigger [us] + // waveform geometry + int nSamples = 0; ///< number of ticks. + int nSubsamples = 0; ///< number of subsamples (sub-tick interpolation steps) + + // full-window waveform (before zero-suppression splitting) + std::vector waveform_adc; ///< full waveform ADC values [ADC counts]. + + // per-photon info + int nDetectedPhotons = 0; ///< Number of photons accepted after QE cut. + int nInputPhotons = 0; ///< Total number of photons in sim::SimPhotons. + std::vector photon_simTime_ns; ///< G4 simulation time of each detected photon [ns]. + std::vector photon_waveformTime_us; ///< Photon time in waveform coordinates [us]: toTriggerTime - triggerOffsetPMT + timeDelay. + std::vector photon_start_x; ///< X position of the photon emission point [cm]. + std::vector photon_start_y; ///< Y position of the photon emission point [cm]. + std::vector photon_start_z; ///< Z position of the photon emission point [cm]. + std::vector photon_tick; ///< Waveform tick index where the photon was placed. + std::vector photon_subtick; ///< Sub-tick index (subsample bin) where the photon was placed. + + // per-PE-deposit info + int nPEDeposits = 0; ///< Number of distinct PE deposit entries. + std::vector pedeposit_tick; ///< Waveform tick of the PE deposit. + std::vector pedeposit_subtick; ///< Sub-tick index of the PE deposit. + std::vector pedeposit_nPE; ///< Integer number of photoelectrons at this deposit (after QE, before gain fluctuation). + std::vector pedeposit_nEffectivePE; ///< Effective PE count after gain fluctuation (what was actually added to the waveform). + std::vector pedeposit_gainFactor; ///< Ratio nEffectivePE/nPE: encodes the per-deposit gain fluctuation (DB-calibrated or Poisson). + }; // DebugInfo_t + + /// Input tag for simulated scintillation photons (or photoelectrons). art::InputTag fInputModuleName; bool fMakeMetadata; ///< Whether to produce waveform metadata. bool fWritePhotons { false }; ///< Whether to save contributing photons. - + bool fDoTimingDelays; ///< Whether timing delay corrections are applied. + bool fUseGainCalibDB; ///< Whether per-channel gain calibration from DB is applied. + bool fUseChannelStatusDB; ///< Whether to skip non-ON channels using the status DB. + CLHEP::HepRandomEngine& fEfficiencyEngine; CLHEP::HepRandomEngine& fDarkNoiseEngine; CLHEP::HepRandomEngine& fElectronicsNoiseEngine; @@ -284,7 +352,6 @@ namespace icarus::opdet { /// The actual simulation algorithm. icarus::opdet::PMTsimulationAlgMaker makePMTsimulator; - /// True if `firstTime()` has already been called. std::atomic_flag fNotFirstTime; @@ -301,7 +368,11 @@ namespace icarus::opdet { /// Returns whether no other event has been processed yet. bool firstTime() { return !fNotFirstTime.test_and_set(); } - + + bool fDoDebugTree { false }; ///< Whether to enable debug tree output. + TTree* fDebugTree { nullptr }; ///< Debug tree pointer (owned by TFileService). + DebugInfo_t fDebugInfo; ///< Collected debug information. + }; // class SimPMTIcarus @@ -314,6 +385,9 @@ SimPMTIcarus::SimPMTIcarus(Parameters const& config) , fInputModuleName(config().inputModuleLabel()) , fMakeMetadata(config().MakeMetadata()) , fWritePhotons(config().writePhotons()) + , fDoTimingDelays(config().algoConfig().ApplyTimingDelays()) + , fUseGainCalibDB(config().algoConfig().UseGainDatabase()) + , fUseChannelStatusDB(config().UseChannelStatusDatabase()) // random engines , fEfficiencyEngine(art::ServiceHandle()->registerAndSeedEngine( createEngine(0, "HepJamesRandom", "Efficiencies"), @@ -345,9 +419,13 @@ SimPMTIcarus::SimPMTIcarus(Parameters const& config) ->makeGenerator(fElectronicsNoiseEngine) } , makePMTsimulator(config().algoConfig()) + , fDoDebugTree(config().DebugTree()) + { // Call appropriate produces<>() functions here. produces>(); + produces>(); + produces>(); if (fMakeMetadata) { produces>(); produces>(); @@ -357,6 +435,38 @@ SimPMTIcarus::SimPMTIcarus(Parameters const& config) fNotFirstTime.clear(); // superfluous in C++20 } // SimPMTIcarus::SimPMTIcarus() + // --------------------------------------------------------------------------- + void SimPMTIcarus::beginJob() + { + if (fDoDebugTree) { + art::ServiceHandle tfs; + fDebugTree = tfs->make("SimPMTDebug","Debug tree for SimPMTIcarus"); + fDebugTree->Branch("run", &fDebugInfo.run, "run/I"); + fDebugTree->Branch("event", &fDebugInfo.event, "event/I"); + fDebugTree->Branch("channel", &fDebugInfo.channel, "channel/I"); + fDebugTree->Branch("nSamples", &fDebugInfo.nSamples, "nSamples/I"); + fDebugTree->Branch("nSubsamples", &fDebugInfo.nSubsamples, "nSubsamples/I"); + fDebugTree->Branch("timeDelay_us", &fDebugInfo.timeDelay_us, "timeDelay_us/F"); + fDebugTree->Branch("triggerOffsetPMT_us", &fDebugInfo.triggerOffsetPMT_us, "triggerOffsetPMT_us/F"); + fDebugTree->Branch("waveform_adc", &fDebugInfo.waveform_adc); + fDebugTree->Branch("nInputPhotons", &fDebugInfo.nInputPhotons, "nInputPhotons/I"); + fDebugTree->Branch("nDetectedPhotons", &fDebugInfo.nDetectedPhotons, "nDetectedPhotons/I"); + fDebugTree->Branch("photon_start_x", &fDebugInfo.photon_start_x); + fDebugTree->Branch("photon_start_y", &fDebugInfo.photon_start_y); + fDebugTree->Branch("photon_start_z", &fDebugInfo.photon_start_z); + fDebugTree->Branch("photon_simTime_ns", &fDebugInfo.photon_simTime_ns); + fDebugTree->Branch("photon_waveformTime_us", &fDebugInfo.photon_waveformTime_us); + fDebugTree->Branch("photon_tick", &fDebugInfo.photon_tick); + fDebugTree->Branch("photon_subtick", &fDebugInfo.photon_subtick); + fDebugTree->Branch("nPEDeposits", &fDebugInfo.nPEDeposits, "nPEDeposits/I"); + fDebugTree->Branch("pedeposit_tick", &fDebugInfo.pedeposit_tick); + fDebugTree->Branch("pedeposit_subtick", &fDebugInfo.pedeposit_subtick); + fDebugTree->Branch("pedeposit_nPE", &fDebugInfo.pedeposit_nPE); + fDebugTree->Branch("pedeposit_nEffectivePE", &fDebugInfo.pedeposit_nEffectivePE); + fDebugTree->Branch("pedeposit_gainFactor", &fDebugInfo.pedeposit_gainFactor); + } + } // SimPMTIcarus::beginJob() + // --------------------------------------------------------------------------- void SimPMTIcarus::produce(art::Event & e) @@ -366,15 +476,6 @@ SimPMTIcarus::SimPMTIcarus(Parameters const& config) // // fetch the input // - auto pulseVecPtr = std::make_unique< std::vector< raw::OpDetWaveform > > (); - - std::unique_ptr> simphVecPtr; - if (fWritePhotons) - simphVecPtr = std::make_unique< std::vector< sim::SimPhotons > > (); - - // - // prepare the output - // art::Handle > pmtVector; art::Handle > pmtLiteVector; pmtVector = e.getHandle< std::vector >(fInputModuleName); @@ -390,6 +491,8 @@ SimPMTIcarus::SimPMTIcarus(Parameters const& config) e.time().value(), // using the event generation time as beam time stamp *(lar::providerFrom()), clockData, + fDoTimingDelays ? lar::providerFrom() : nullptr, + fUseGainCalibDB ? art::ServiceHandle()->provider() : nullptr, *fSinglePhotonResponseFunc, *fPedestalGen, fEfficiencyEngine, @@ -401,52 +504,124 @@ SimPMTIcarus::SimPMTIcarus(Parameters const& config) if (firstTime()) { mf::LogInfo log { "SimPMTIcarus" }; log << "PMT simulation configuration (first event):\n"; + log << "useChannelStatusDB: " << std::boolalpha << fUseChannelStatusDB << "\n"; PMTsimulator->printConfiguration(log); } // if first time // // run the algorithm // - unsigned int nopch = 0; - if(pmtVector.isValid()) { - nopch = pmtVector->size(); - for(auto const& photons : *pmtVector) { - - // Make an empty SimPhotonsLite with the same channel number. - sim::SimPhotonsLite lite_photons(photons.OpChannel()); - - auto const& [ channelWaveforms, photons_used ] - = PMTsimulator->simulate(photons, lite_photons); - std::move( - channelWaveforms.cbegin(), channelWaveforms.cend(), - std::back_inserter(*pulseVecPtr) - ); - if (simphVecPtr && photons_used) - simphVecPtr->emplace_back(std::move(photons_used.value())); - - } // for + // Prefer SimPhotons if available. + // Make sure that there are parallel inputs for both formats; + bool const useLitePhotons = !pmtVector.isValid(); + + // storage for the photons that are not used (but still required) + std::vector fakePhotons; + std::vector fakeLitePhotons; + + // these are the data collections passed to the algorithm + std::vector const* pInputPhotons = &fakePhotons; + std::vector const* pInputLitePhotons = &fakeLitePhotons; + + // fill the relevant fake input vectors, and prepare the pointers + if (useLitePhotons) { + for (sim::SimPhotonsLite const& ph: *pmtLiteVector) + fakePhotons.emplace_back(ph.OpChannel); + pInputLitePhotons = pmtLiteVector.product(); } - else if(pmtLiteVector.isValid()) { - nopch = pmtLiteVector->size(); - for(auto const& lite_photons : *pmtLiteVector) { - - // Make an empty SimPhotons with the same channel number. + else { + for (sim::SimPhotons const& ph: *pmtVector) + fakeLitePhotons.emplace_back(ph.OpChannel()); + pInputPhotons = pmtVector.product(); + } + + assert(pInputLitePhotons->size() == pInputPhotons->size()); + + auto simphVecPtr = fWritePhotons? std::make_unique>(): nullptr; + auto pulseVecPtr = std::make_unique>(); + auto baselineVecPtr = std::make_unique>(); + + for(auto const& [ photons, litePhotons ] + : util::zip(*pInputPhotons, *pInputLitePhotons) + ) { + + int const channel = photons.OpChannel(); + assert(channel == litePhotons.OpChannel); + + if (fUseChannelStatusDB + && !lar::providerFrom()->isGood(channel)) + continue; - sim::SimPhotons photons(lite_photons.OpChannel); + auto const& [ channelWaveforms, channelPedestals, photons_used, debug ] + = PMTsimulator->simulate(photons, litePhotons, fDoDebugTree); + assert(channelWaveforms.size() == channelPedestals.size()); + std::move( + channelWaveforms.cbegin(), channelWaveforms.cend(), + std::back_inserter(*pulseVecPtr) + ); + std::move( + channelPedestals.cbegin(), channelPedestals.cend(), + std::back_inserter(*baselineVecPtr) + ); + assert(pulseVecPtr->size() == baselineVecPtr->size()); - auto const& [ channelWaveforms, photons_used ] - = PMTsimulator->simulate(photons, lite_photons); - std::move( - channelWaveforms.cbegin(), channelWaveforms.cend(), - std::back_inserter(*pulseVecPtr) - ); + if (!useLitePhotons && simphVecPtr && photons_used) + simphVecPtr->emplace_back(std::move(photons_used.value())); + + if(fDoDebugTree && debug && !debug->photons.empty()) { + + fDebugInfo = DebugInfo_t{}; // reset all information - } // for - } + // fill debug tree variables + fDebugInfo.run = e.id().run(); + fDebugInfo.event = e.id().event(); + fDebugInfo.channel = debug->opChannel; + fDebugInfo.timeDelay_us = debug->timeDelay_us; + fDebugInfo.triggerOffsetPMT_us = debug->triggerOffsetPMT_us; + fDebugInfo.nSamples = debug->nSamples; + fDebugInfo.nSubsamples = debug->nSubsamples; + // fill full-window waveform + fDebugInfo.waveform_adc = debug->waveform; + // fill per photon info + fDebugInfo.nInputPhotons = photons.size(); + fDebugInfo.nDetectedPhotons = debug->photons.size(); + for(auto const& phot : debug->photons) { + fDebugInfo.photon_start_x.push_back(phot.startX); + fDebugInfo.photon_start_y.push_back(phot.startY); + fDebugInfo.photon_start_z.push_back(phot.startZ); + fDebugInfo.photon_simTime_ns.push_back(phot.simTime_ns); + fDebugInfo.photon_waveformTime_us.push_back(phot.trigTime_us); + fDebugInfo.photon_tick.push_back(phot.tick); + fDebugInfo.photon_subtick.push_back(phot.subtick); + } + // fill per PE deposit info + fDebugInfo.nPEDeposits = debug->peDeposits.size(); + for(auto const& pedep : debug->peDeposits) { + fDebugInfo.pedeposit_tick.push_back(pedep.tick); + fDebugInfo.pedeposit_subtick.push_back(pedep.subtick); + fDebugInfo.pedeposit_nPE.push_back(pedep.nPE); + fDebugInfo.pedeposit_nEffectivePE.push_back(pedep.nEffectivePE); + fDebugInfo.pedeposit_gainFactor.push_back(pedep.gainFactor()); + } + fDebugTree->Fill(); + } // if debug tree + + } // for scintillation photons + mf::LogInfo("SimPMTIcarus") << "Generated " << pulseVecPtr->size() - << " waveforms out of " << nopch << " optical channels."; + << " waveforms out of " << pInputPhotons->size() << " optical channels."; + + // waveform baselines + auto baselineAssns + = std::make_unique>(); + art::PtrMaker const makeWaveformPtr{ e }; + art::PtrMaker const makeBaselinePtr{ e }; + for (std::size_t const iWaveform: util::counter(pulseVecPtr->size())) { + baselineAssns->addSingle + (makeWaveformPtr(iWaveform), makeBaselinePtr(iWaveform)); + } // for // waveform metadata std::unique_ptr> metadataVec; @@ -469,6 +644,8 @@ SimPMTIcarus::SimPMTIcarus(Parameters const& config) // save the result // e.put(std::move(pulseVecPtr)); + e.put(std::move(baselineVecPtr)); + e.put(std::move(baselineAssns)); if (fMakeMetadata) { e.put(std::move(metadataVec)); e.put(std::move(metadataAssns)); diff --git a/icaruscode/PMT/Status/CMakeLists.txt b/icaruscode/PMT/Status/CMakeLists.txt new file mode 100644 index 000000000..73581e3de --- /dev/null +++ b/icaruscode/PMT/Status/CMakeLists.txt @@ -0,0 +1,27 @@ +cet_enable_asserts() + +set( LIB_LIBRARIES + art::Framework_Services_Registry + messagefacility::MF_MessageLogger + larcorealg::CoreUtils + larevt::CalibrationDBI_IOVData + larevt::CalibrationDBI_Providers + fhiclcpp::fhiclcpp + cetlib_except::cetlib_except +) + +set( SERVICE_LIBRARIES + icaruscode_PMT_Status + art::Framework_Principal + larcore::Geometry_Geometry_service +) + +file(GLOB lib_srcs *.cxx) + +art_make_library( SOURCE ${lib_srcs} LIBRARIES PUBLIC ${LIB_LIBRARIES}) + +cet_build_plugin( PMTChannelStatusService art::service LIBRARIES PUBLIC ${SERVICE_LIBRARIES}) + +install_headers() +install_fhicl() +install_source() diff --git a/icaruscode/PMT/Status/IPMTChannelStatusService.h b/icaruscode/PMT/Status/IPMTChannelStatusService.h new file mode 100644 index 000000000..e429278a7 --- /dev/null +++ b/icaruscode/PMT/Status/IPMTChannelStatusService.h @@ -0,0 +1,32 @@ +/** + * @file icaruscode/PMT/Status/IPMTChannelStatusService.h + * @brief Art service interface for PMT channel status. + * @author Matteo Vicenzi (mvicenzi@bnl.gov) + */ + +#ifndef ICARUSCODE_PMT_STATUS_IPMTCHANNELSTATUSSERVICE_H +#define ICARUSCODE_PMT_STATUS_IPMTCHANNELSTATUSSERVICE_H + +// ICARUS libraries +#include "icaruscode/PMT/Status/PMTChannelStatus.h" + +// LArSoft libraries +#include "larcore/CoreUtils/ServiceProviderWrappers.h" + + +// ----------------------------------------------------------------------------- +namespace icarusDB { + /// The only thing this service does is to return its service provider of type + /// `icarusDB::PMTChannelStatus`. + using IPMTChannelStatusService + = lar::ServiceProviderInterfaceWrapper; +} + + +// ----------------------------------------------------------------------------- +DECLARE_ART_SERVICE_INTERFACE(icarusDB::IPMTChannelStatusService, SHARED) + + +// ----------------------------------------------------------------------------- + +#endif // ICARUSCODE_PMT_STATUS_IPMTCHANNELSTATUSSERVICE_H diff --git a/icaruscode/PMT/Status/PMTChannelStatus.h b/icaruscode/PMT/Status/PMTChannelStatus.h new file mode 100644 index 000000000..86241ce2d --- /dev/null +++ b/icaruscode/PMT/Status/PMTChannelStatus.h @@ -0,0 +1,86 @@ +/** + * @file icaruscode/PMT/Status/PMTChannelStatus.h + * @brief Interface for PMT channel status provider. + * @author Matteo Vicenzi (mvicenzi@bnl.gov) + */ + +#ifndef ICARUSCODE_PMT_STATUS_PMTCHANNELSTATUS_H +#define ICARUSCODE_PMT_STATUS_PMTCHANNELSTATUS_H + +// LArSoft libraries +#include "larcorealg/CoreUtils/UncopiableAndUnmovableClass.h" + +// Framework libraries +#include "art/Framework/Services/Registry/ServiceDeclarationMacros.h" + +// C++ standard libraries +#include +#include + + +namespace icarusDB { + + /// Possible status values for a PMT channel, matching the database integers. + enum PMTChannelStatusValue : int { + kOff = 0, ///< Channel is off (not powered). + kGood = 1, ///< Channel is on and good. + kBad = 2 ///< Channel is powered but flagged as bad. + }; + + + /** + * @brief Interface for PMT channel status information. + * + * Currently, the class provides interface for the following information: + * - channel status: kGood, kOff, or kBad + * - convenience predicates: isGood(), isOff(), isBad() + * - channel sets by status: getOnChannels(), getOffChannels(), getBadChannels() + * + */ + class PMTChannelStatus : private lar::UncopiableAndUnmovableClass { + + public: + + using ChannelSet_t = std::set; + + virtual ~PMTChannelStatus() noexcept = default; + + /// Returns the status of the specified channel. + virtual PMTChannelStatusValue getChannelStatus(unsigned int channel) const = 0; + + /// Returns whether the specified channel is on and good (status kGood). + virtual bool isGood(unsigned int channel) const + { return getChannelStatus(channel) == kGood; } + + /// Returns whether the specified channel is off (not powered). + virtual bool isOff(unsigned int channel) const + { return getChannelStatus(channel) == kOff; } + + /// Returns whether the specified channel is powered but bad. + virtual bool isBad(unsigned int channel) const + { return getChannelStatus(channel) == kBad; } + + /// Returns the set of channels with status kGood. + virtual ChannelSet_t getOnChannels() const = 0; + + /// Returns the set of channels with status kOff. + virtual ChannelSet_t getOffChannels() const = 0; + + /// Returns the set of channels with status kBad. + virtual ChannelSet_t getBadChannels() const = 0; + + /// Returns the nominal voltage [V] of the specified channel. + virtual double getChannelVoltage(unsigned int channel) const = 0; + + /// Returns the database tag currently in use. + virtual std::string getDatabaseTag() const = 0; + + }; // class PMTChannelStatus + +} // namespace icarusDB + + +DECLARE_ART_SERVICE_INTERFACE(icarusDB::PMTChannelStatus, SHARED) + + +#endif // ICARUSCODE_PMT_STATUS_PMTCHANNELSTATUS_H diff --git a/icaruscode/PMT/Status/PMTChannelStatusProvider.cxx b/icaruscode/PMT/Status/PMTChannelStatusProvider.cxx new file mode 100644 index 000000000..ba4e3cc1a --- /dev/null +++ b/icaruscode/PMT/Status/PMTChannelStatusProvider.cxx @@ -0,0 +1,140 @@ +/** + * @file icaruscode/PMT/Status/PMTChannelStatusProvider.cxx + * @brief Implementation of PMT channel status provider. + * @author Matteo Vicenzi (mvicenzi@bnl.gov) + * @see icaruscode/PMT/Status/PMTChannelStatusProvider.h + */ + +#include "icaruscode/PMT/Status/PMTChannelStatusProvider.h" + +// LArSoft libraries +#include "larevt/CalibrationDBI/IOVData/TimeStampDecoder.h" + +// Framework libraries +#include "messagefacility/MessageLogger/MessageLogger.h" + +// C++ standard libraries +#include // std::setw() + + +// ----------------------------------------------------------------------------- +icarusDB::PMTChannelStatusProvider::PMTChannelStatusProvider + (const fhicl::ParameterSet& pset) + : fVerbose ( pset.get ("Verbose", false ) ) + , fLogCategory ( pset.get("LogCategory", "PMTChannelStatusProvider") ) + , fStatusTag ( pset.get("StatusTag", "" ) ) + , fOverrideRunNumber( pset.get("OverrideRunNumber", -1) ) + , fDB ( pset.get("DBname", "pmt_voltage_data"), + "", "", fStatusTag, true, false ) +{ + int const defaultStatusInt = pset.get("DefaultStatus", static_cast(kGood)); + fDefault.status = static_cast(defaultStatusInt); + fDefault.voltage = pset.get("DefaultVoltage", 1500.0); + + mf::LogInfo(fLogCategory) + << "PMTChannelStatusProvider connected to '" + << pset.get("DBname", "pmt_voltage_data") + << "' DB, tag '" << fStatusTag << "'."; +} + + +// ----------------------------------------------------------------------------- +void icarusDB::PMTChannelStatusProvider::readStatusFromDB(unsigned int run) +{ + + if (fOverrideRunNumber >= 0) { + mf::LogInfo(fLogCategory) + << "Overriding run number " << run << " with " << fOverrideRunNumber + << " for DB queries."; + run = static_cast(fOverrideRunNumber); + } + + mf::LogInfo(fLogCategory) + << "Reading PMT channel statuses from database for run " << run; + + bool const ret = fDB.UpdateData( RunToDatabaseTimestamp(run) ); + mf::LogTrace(fLogCategory) + << "Status" << (ret ? "" : " not") << " updated for run " << run + << "\nFetched IoV [ " << fDB.CachedStart().DBStamp() + << " ; " << fDB.CachedEnd().DBStamp() + << " ] to cover t=" << RunToDatabaseTimestamp(run) + << " [=" << lariov::TimeStampDecoder::DecodeTimeStamp(RunToDatabaseTimestamp(run)).DBStamp() << "]"; + + std::vector channelList; + if (int const res = fDB.GetChannelList(channelList); res != 0) { + throw cet::exception(fLogCategory) + << "GetChannelList() returned " << res << " on run " << run << " query\n"; + } + + if (channelList.empty()) { + throw cet::exception(fLogCategory) + << "Got an empty channel list for run " << run << "\n"; + } + + fDatabaseStatus.clear(); + + mf::LogDebug log(fLogCategory); + log << "Loading status for " << channelList.size() << " channels in run " << run; + + for (auto const channel : channelList) { + + long statusInt = 0; + int error = fDB.GetNamedChannelData(channel, "status", statusInt); + if (error) { + throw cet::exception(fLogCategory) + << "Error (code " << error << ") reading 'status' for channel " + << channel << "\n"; + } + + double voltage = 0.0; + error = fDB.GetNamedChannelData(channel, "voltage", voltage); + if (error) { + throw cet::exception(fLogCategory) + << "Error (code " << error << ") reading 'voltage' for channel " + << channel << "\n"; + } + + fDatabaseStatus[channel].status = static_cast(statusInt); + fDatabaseStatus[channel].voltage = voltage; + + if (fVerbose) + log << "\n channel " << std::setw(3) << channel + << " status " << statusInt + << " voltage " << voltage << " V"; + + } // for channel + +} // readStatusFromDB() + + +// ----------------------------------------------------------------------------- +std::uint64_t +icarusDB::PMTChannelStatusProvider::RunToDatabaseTimestamp(unsigned int run) const +{ + // Same convention as other ICARUS DB services: + // run XXXXX -> (XXXXX + 1'000'000'000) * 1'000'000'000 [nanoseconds] + // e.g. run XXXXX -> 10000XXXXX * 1e9 + std::uint64_t timestamp = std::uint64_t(run) + 1'000'000'000ULL; + timestamp *= 1'000'000'000ULL; + + if (fVerbose) + mf::LogInfo(fLogCategory) + << "Run " << run << " status from DB timestamp " << timestamp; + + return timestamp; +} + + +// ----------------------------------------------------------------------------- +icarusDB::PMTChannelStatusProvider::ChannelSet_t +icarusDB::PMTChannelStatusProvider::getChannelsWithStatus + (PMTChannelStatusValue status) const +{ + ChannelSet_t result; + for (auto const& [channel, info] : fDatabaseStatus) + if (info.status == status) result.insert(channel); + return result; +} + + +// ----------------------------------------------------------------------------- diff --git a/icaruscode/PMT/Status/PMTChannelStatusProvider.h b/icaruscode/PMT/Status/PMTChannelStatusProvider.h new file mode 100644 index 000000000..85873b2a6 --- /dev/null +++ b/icaruscode/PMT/Status/PMTChannelStatusProvider.h @@ -0,0 +1,113 @@ +/** + * @file icaruscode/PMT/Status/PMTChannelStatusProvider.h + * @brief Provider for PMT channel status from the conditions database. + * @author Matteo Vicenzi (mvicenzi@bnl.gov) + * @see icaruscode/PMT/Status/PMTChannelStatusProvider.cxx + */ + +#ifndef ICARUSCODE_PMT_STATUS_PMTCHANNELSTATUSPROVIDER_H +#define ICARUSCODE_PMT_STATUS_PMTCHANNELSTATUSPROVIDER_H + +// ICARUS libraries +#include "icaruscode/PMT/Status/PMTChannelStatus.h" + +// LArSoft libraries +#include "larevt/CalibrationDBI/Providers/DBFolder.h" + +// Framework libraries +#include "fhiclcpp/ParameterSet.h" +#include "cetlib_except/exception.h" + +// C++ standard libraries +#include +#include +#include + + +namespace icarusDB::details { + + /// Internal structure holding the status and voltage for a single channel. + struct PMTChannelStatusDB { + PMTChannelStatusValue status = kOff; + double voltage = 0.0; ///< Nominal voltage [V]. + }; + +} // namespace icarusDB::details + + +// ----------------------------------------------------------------------------- +namespace icarusDB { class PMTChannelStatusProvider; } +/** + * @brief Provider for PMT channel status from the conditions database. + * + * Reads the channel status (kOff=0, kGood=1, kBad=2) from the + * `pmt_voltage_data` database table, indexed by run number. + * + * The database interface is accessed only on `readStatusFromDB()` calls, + * and the relevant information is cached. + * + * Configuration parameters + * ------------------------- + * * `DBname` (default: `"pmt_voltage_data"`): database table name. + * * `StatusTag` (default: `""`): database tag to select. + * * `DefaultStatus` (default: `0` = kOff): status for channels absent from DB. + * * `Verbose` (default: `false`): print channel statuses when loading. + * * `LogCategory` (default: `"PMTChannelStatusProvider"`). + * + */ +class icarusDB::PMTChannelStatusProvider : public PMTChannelStatus { + +public: + + /// Constructor from FHiCL configuration (no database access yet). + PMTChannelStatusProvider(const fhicl::ParameterSet& pset); + + /// Loads channel statuses for the given run from the database. + void readStatusFromDB(unsigned int run); + + /// Converts a run number to the internal 19-digit nanosecond timestamp. + std::uint64_t RunToDatabaseTimestamp(unsigned int run) const; + + // --- PMTChannelStatus interface --- + + PMTChannelStatusValue getChannelStatus(unsigned int channel) const override + { return getChannelStatusOrDefault(channel).status; } + + ChannelSet_t getOnChannels() const override { return getChannelsWithStatus(kGood); } + ChannelSet_t getOffChannels() const override { return getChannelsWithStatus(kOff); } + ChannelSet_t getBadChannels() const override { return getChannelsWithStatus(kBad); } + + double getChannelVoltage(unsigned int channel) const override + { return getChannelStatusOrDefault(channel).voltage; } + + std::string getDatabaseTag() const override { return fStatusTag; } + +private: + + using PMTChannelStatusDB = details::PMTChannelStatusDB; + + bool fVerbose; + std::string fLogCategory; + std::string fStatusTag; + int fOverrideRunNumber; ///< If non-negative, overrides the run number for DB queries. + + PMTChannelStatusDB fDefault; ///< Status used for channels not present in DB. + + lariov::DBFolder fDB; ///< Cached database interface. + + /// Map of channel ID to status information. + std::map fDatabaseStatus; + + /// Returns the channel status record, or the default if channel is absent. + PMTChannelStatusDB const& getChannelStatusOrDefault(unsigned int channel) const { + auto const it = fDatabaseStatus.find(channel); + return (it == fDatabaseStatus.end()) ? fDefault : it->second; + } + + /// Returns the set of all channels with the given status. + ChannelSet_t getChannelsWithStatus(PMTChannelStatusValue status) const; + +}; // class icarusDB::PMTChannelStatusProvider + + +#endif // ICARUSCODE_PMT_STATUS_PMTCHANNELSTATUSPROVIDER_H diff --git a/icaruscode/PMT/Status/PMTChannelStatusService_service.cc b/icaruscode/PMT/Status/PMTChannelStatusService_service.cc new file mode 100644 index 000000000..ba031690c --- /dev/null +++ b/icaruscode/PMT/Status/PMTChannelStatusService_service.cc @@ -0,0 +1,60 @@ +/** + * @file icaruscode/PMT/Status/PMTChannelStatusService_service.cc + * @brief Art service for PMT channel status from the conditions database. + * @author Matteo Vicenzi (mvicenzi@bnl.gov) + */ + +#include "icaruscode/PMT/Status/IPMTChannelStatusService.h" +#include "icaruscode/PMT/Status/PMTChannelStatusProvider.h" + +// Framework libraries +#include "art/Framework/Principal/Run.h" +#include "art/Framework/Services/Registry/ActivityRegistry.h" +#include "art/Framework/Services/Registry/ServiceDefinitionMacros.h" +#include "art/Framework/Services/Registry/ServiceDeclarationMacros.h" +#include "fhiclcpp/ParameterSet.h" +#include "cetlib_except/exception.h" + + +// ----------------------------------------------------------------------------- +namespace icarusDB { class PMTChannelStatusService; } +class icarusDB::PMTChannelStatusService + : public IPMTChannelStatusService, private PMTChannelStatusProvider { + + void preBeginRun(const art::Run& run); + + /// Returns a constant pointer to the service provider. + virtual PMTChannelStatusProvider const* do_provider() const override + { return this; } + + public: + + PMTChannelStatusService(const fhicl::ParameterSet& pset, art::ActivityRegistry& reg); + +}; // class icarusDB::PMTChannelStatusService + + +// ----------------------------------------------------------------------------- +// --- Implementation +// ----------------------------------------------------------------------------- +icarusDB::PMTChannelStatusService::PMTChannelStatusService + (const fhicl::ParameterSet& pset, art::ActivityRegistry& reg) + : PMTChannelStatusProvider(pset) +{ + reg.sPreBeginRun.watch(this, &PMTChannelStatusService::preBeginRun); +} + + +// ----------------------------------------------------------------------------- +void icarusDB::PMTChannelStatusService::preBeginRun(const art::Run& run) +{ + readStatusFromDB(run.run()); +} + + +// ----------------------------------------------------------------------------- +DECLARE_ART_SERVICE_INTERFACE_IMPL(icarusDB::PMTChannelStatusService, icarusDB::IPMTChannelStatusService, SHARED) +DEFINE_ART_SERVICE_INTERFACE_IMPL(icarusDB::PMTChannelStatusService, icarusDB::IPMTChannelStatusService) + + +// ----------------------------------------------------------------------------- diff --git a/icaruscode/PMT/Status/pmt_channel_status_icarus.fcl b/icaruscode/PMT/Status/pmt_channel_status_icarus.fcl new file mode 100644 index 000000000..cfaf53788 --- /dev/null +++ b/icaruscode/PMT/Status/pmt_channel_status_icarus.fcl @@ -0,0 +1,19 @@ +# +# @file pmt_channel_status_icarus.fcl +# @brief Standard configuration for the ICARUS PMT channel status service. +# + +BEGIN_PROLOG + +icarus_pmt_channel_status: { + service_provider: PMTChannelStatusService + DBname: "pmt_voltage_data" + StatusTag: "v1r3" + DefaultStatus: 1 # kGood + DefaultVoltage: 1500.0 # [V] + Verbose: false + OverrideRunNumber: -1 + LogCategory: "PMTChannelStatusProvider" +} + +END_PROLOG diff --git a/icaruscode/PMT/Trigger/Algorithms/WindowTopologyAlg.cxx b/icaruscode/PMT/Trigger/Algorithms/WindowTopologyAlg.cxx index 393d8c79b..ddda92bc5 100644 --- a/icaruscode/PMT/Trigger/Algorithms/WindowTopologyAlg.cxx +++ b/icaruscode/PMT/Trigger/Algorithms/WindowTopologyAlg.cxx @@ -146,7 +146,7 @@ auto icarus::trigger::WindowTopologyAlg::emplaceAndDumpMap(Args&&... args) const -> WindowChannelMap { WindowChannelMap const map { std::forward(args)... }; - mfLogTrace() << "Window map: << " << map; + mfLogTrace() << "Window map: " << map; return map; } // icarus::trigger::WindowTopologyAlg::emplaceAndDumpMap() diff --git a/icaruscode/PMT/Trigger/DiscriminatePMTwaveforms_module.cc b/icaruscode/PMT/Trigger/DiscriminatePMTwaveforms_module.cc index bb9b5536b..5aa47eef4 100644 --- a/icaruscode/PMT/Trigger/DiscriminatePMTwaveforms_module.cc +++ b/icaruscode/PMT/Trigger/DiscriminatePMTwaveforms_module.cc @@ -108,6 +108,9 @@ namespace icarus::trigger { class DiscriminatePMTwaveforms; } * expected to be from the * @ref DetectorClocksElectronicsTime "electronics time scale" * and therefore expressed in microseconds. + * * `std::vector` (if `Baselines` is specified): + * @ref LArSoftProxyDefinitionParallelData "parallel data product" of the + * baseline of each of the input waveforms. * * * Service requirements @@ -369,6 +372,8 @@ icarus::trigger::DiscriminatePMTwaveforms::DiscriminatePMTwaveforms // declaration of input // consumes>(fOpDetWaveformTag); + if (fBaselineTag) + consumes>(*fBaselineTag); // // declaration of output diff --git a/icaruscode/PMT/Trigger/DiscriminatedAdderSignal_module.cc b/icaruscode/PMT/Trigger/DiscriminatedAdderSignal_module.cc index e394476dd..430845fd0 100644 --- a/icaruscode/PMT/Trigger/DiscriminatedAdderSignal_module.cc +++ b/icaruscode/PMT/Trigger/DiscriminatedAdderSignal_module.cc @@ -634,11 +634,7 @@ icarus::trigger::DiscriminatedAdderSignal::DiscriminatedAdderSignal , fBaselineTag { config().BaselineTag() } , fWaveformMetaTag { config().WaveformMetaTag().value_or(fWaveformTag) } , fAmplitudeScale { config().AmplitudeScale() } - , fTimeInterval - { - icarus::ns::fhicl::makeTimeInterval(config().TimeInterval()) - .value_or(RelTimeInterval_t{}) - } + , fTimeInterval { config().TimeInterval().value_or(RelTimeInterval_t{}) } , fMissingChannels { sorted(config().MissingChannels()) } , fSaveWaveforms { config().SaveWaveforms() } , fSavePMTcoverage { config().SavePMTcoverage() } @@ -665,7 +661,9 @@ icarus::trigger::DiscriminatedAdderSignal::DiscriminatedAdderSignal // if (fTimeInterval.empty()) { throw art::Exception{ art::errors::Configuration } - << "The '" << config().TimeInterval.name() + << "The '" + // this type cast was supposed to be implicit... is not + << config().TimeInterval.operator fhicl::detail::ParameterBase const&().name() << "' parameters configures an empty time interval [ " << fTimeInterval.start << " -- " << fTimeInterval.stop << " ]\n"; } diff --git a/icaruscode/PMT/Trigger/LVDSgates_module.cc b/icaruscode/PMT/Trigger/LVDSgates_module.cc index fd447d73f..04ca63185 100644 --- a/icaruscode/PMT/Trigger/LVDSgates_module.cc +++ b/icaruscode/PMT/Trigger/LVDSgates_module.cc @@ -192,9 +192,9 @@ class icarus::trigger::LVDSgates: public art::EDProducer { { ComboMode::OR, "OR" } }; - fhicl::OptionalAtom TriggerGatesTag { + fhicl::OptionalAtom TriggerGatesTag { Name("TriggerGatesTag"), - Comment("label of trigger gate extraction module (no instance name)") + Comment("tag of trigger gate extraction module (no instance name)") }; fhicl::Sequence Thresholds { @@ -386,11 +386,11 @@ class icarus::trigger::LVDSgates: public art::EDProducer { /// Converts a threshold string into an input tag. static art::InputTag makeTag - (std::string const& thresholdStr, std::string const& defModule); + (std::string const& thresholdStr, art::InputTag const& defModule); /// Converts an input tag into an instance name for the corresponding output. static std::string makeOutputInstanceName - (art::InputTag const& inputTag, std::string const& defModule); + (art::InputTag const& inputTag, art::InputTag const& defModule); }; // class icarus::trigger::LVDSgates @@ -412,11 +412,11 @@ icarus::trigger::LVDSgates::LVDSgates // // more complex parameter parsing // - std::string const discrModuleLabel = config().TriggerGatesTag().value_or(""); + art::InputTag const discrModuleTag = config().TriggerGatesTag().value_or(""); for (std::string const& thresholdStr: config().Thresholds()) { - art::InputTag const inputTag = makeTag(thresholdStr, discrModuleLabel); + art::InputTag const inputTag = makeTag(thresholdStr, discrModuleTag); fADCthresholds[thresholdStr] - = { inputTag, makeOutputInstanceName(inputTag, discrModuleLabel) }; + = { inputTag, makeOutputInstanceName(inputTag, discrModuleTag) }; } // for all thresholds // @@ -863,34 +863,38 @@ auto icarus::trigger::LVDSgates::ReadTriggerGates( //------------------------------------------------------------------------------ art::InputTag icarus::trigger::LVDSgates::makeTag - (std::string const& thresholdStr, std::string const& defModule) + (std::string const& thresholdStr, art::InputTag const& defModule) { auto const isNumber = [pattern=std::regex{ "[+-]?[0-9]+" }] (std::string const& s) -> bool { return std::regex_match(s, pattern); }; - return + if (!thresholdStr.empty() && !defModule.instance().empty()) { + throw art::Exception(art::errors::Configuration) + << "Both the input gate module tag instance name (`TriggerGatesTag`: '" + << defModule.encode() << "') and the threshold '" << thresholdStr + << "' are set. One of them must be empty.\n"; + } + return ((thresholdStr.find(':') != std::string::npos) || !isNumber(thresholdStr)) ? art::InputTag{ thresholdStr } - : defModule.empty() + : defModule.label().empty() ? throw (art::Exception(art::errors::Configuration) << "No default module label (`TriggerGatesTag`) specified" - ", and it's needed for threshold '" - << thresholdStr << "'.\n") - : art::InputTag{ defModule, thresholdStr } + ", and it's needed for threshold '" << thresholdStr << "'.\n") + : art::InputTag{ defModule.label(), thresholdStr, defModule.process() } ; } // icarus::trigger::LVDSgates::makeTag() //------------------------------------------------------------------------------ std::string icarus::trigger::LVDSgates::makeOutputInstanceName - (art::InputTag const& inputTag, std::string const& defModule) + (art::InputTag const& inputTag, art::InputTag const& defModule) { - return (inputTag.label() == defModule) + return (inputTag.label() == defModule.label()) ? inputTag.instance() - : inputTag.instance().empty() - ? inputTag.label(): inputTag.label() + inputTag.instance() + : inputTag.label() + inputTag.instance() ; -} // icarus::trigger::LVDSgates::makeTag() +} // icarus::trigger::LVDSgates::makeOutputInstanceName() //------------------------------------------------------------------------------ diff --git a/icaruscode/PMT/Trigger/MajorityTriggerSimulation_module.cc b/icaruscode/PMT/Trigger/MajorityTriggerSimulation_module.cc index aae0c544d..fb43c4435 100644 --- a/icaruscode/PMT/Trigger/MajorityTriggerSimulation_module.cc +++ b/icaruscode/PMT/Trigger/MajorityTriggerSimulation_module.cc @@ -359,9 +359,9 @@ class icarus::trigger::MajorityTriggerSimulation using Name = fhicl::Name; using Comment = fhicl::Comment; - fhicl::Atom TriggerGatesTag { + fhicl::Atom TriggerGatesTag { Name("TriggerGatesTag"), - Comment("label of the input trigger gate data product (no instance name)") + Comment("tag of the input trigger gate data product") }; fhicl::Sequence Thresholds { @@ -597,6 +597,11 @@ class icarus::trigger::MajorityTriggerSimulation //@} + /// Returns `defModule` with instance name replaced by `thresholdStr`. + static art::InputTag makeTag + (art::InputTag const& defModule, std::string const& thresholdStr); + + }; // icarus::trigger::MajorityTriggerSimulation @@ -629,10 +634,10 @@ icarus::trigger::MajorityTriggerSimulation::MajorityTriggerSimulation // // more complex parameter parsing // - std::string const discrModuleLabel = config().TriggerGatesTag(); + art::InputTag const& discrModuleTag = config().TriggerGatesTag(); for (raw::ADC_Count_t threshold: config().Thresholds()) { fADCthresholds[icarus::trigger::ADCCounts_t{threshold}] - = art::InputTag{ discrModuleLabel, util::to_string(threshold) }; + = makeTag(discrModuleTag, util::to_string(threshold)); } // initialization of a vector of atomic is not as trivial as it sounds... @@ -943,6 +948,20 @@ icarus::trigger::MajorityTriggerSimulation::triggerInfoToTriggerData } // icarus::trigger::MajorityTriggerSimulation::triggerInfoToTriggerData() +//------------------------------------------------------------------------------ +art::InputTag icarus::trigger::MajorityTriggerSimulation::makeTag + (art::InputTag const& defModule, std::string const& thresholdStr) +{ + if (!thresholdStr.empty() && !defModule.instance().empty()) { + throw art::Exception(art::errors::Configuration) + << "Module tag instance name (`TriggerGatesTag`: '" + << defModule.encode() << "') and the threshold '" << thresholdStr + << "' are both set. One of them must be empty.\n"; + } + return { defModule.label(), thresholdStr, defModule.process() }; +} // icarus::trigger::MajorityTriggerSimulation::makeTag() + + //------------------------------------------------------------------------------ DEFINE_ART_MODULE(icarus::trigger::MajorityTriggerSimulation) diff --git a/icaruscode/PMT/Trigger/SlidingWindowTriggerSimulation_module.cc b/icaruscode/PMT/Trigger/SlidingWindowTriggerSimulation_module.cc index f95638ca3..f356090f7 100644 --- a/icaruscode/PMT/Trigger/SlidingWindowTriggerSimulation_module.cc +++ b/icaruscode/PMT/Trigger/SlidingWindowTriggerSimulation_module.cc @@ -285,9 +285,9 @@ class icarus::trigger::SlidingWindowTriggerSimulation using Name = fhicl::Name; using Comment = fhicl::Comment; - fhicl::Atom TriggerGatesTag { + fhicl::Atom TriggerGatesTag { Name("TriggerGatesTag"), - Comment("label of the input trigger gate data product (no instance name)") + Comment("tag of the input trigger gate data product (no instance name)") }; fhicl::Sequence Thresholds { @@ -573,7 +573,12 @@ class icarus::trigger::SlidingWindowTriggerSimulation static double eventTimestampInSeconds(art::Timestamp const& time); static double eventTimestampInSeconds(art::Event const& event); //@} - + + + /// Returns `defModule` with instance name replaced by `thresholdStr`. + static art::InputTag makeTag + (art::InputTag const& defModule, std::string const& thresholdStr); + }; // icarus::trigger::SlidingWindowTriggerSimulation @@ -622,9 +627,9 @@ icarus::trigger::SlidingWindowTriggerSimulation::SlidingWindowTriggerSimulation // // more complex parameter parsing // - std::string const& discrModuleLabel = config().TriggerGatesTag(); + art::InputTag const& discrModuleTag = config().TriggerGatesTag(); for (std::string const& threshold: config().Thresholds()) - fADCthresholds[threshold] = art::InputTag{ discrModuleLabel, threshold }; + fADCthresholds[threshold] = makeTag(discrModuleTag, threshold); // initialization of a vector of atomic is not as trivial as it sounds... fTriggerCount = std::vector>(fADCthresholds.size()); @@ -1204,6 +1209,20 @@ double icarus::trigger::SlidingWindowTriggerSimulation::eventTimestampInSeconds { return eventTimestampInSeconds(event.time()); } +//------------------------------------------------------------------------------ +art::InputTag icarus::trigger::SlidingWindowTriggerSimulation::makeTag + (art::InputTag const& defModule, std::string const& thresholdStr) +{ + if (!thresholdStr.empty() && !defModule.instance().empty()) { + throw art::Exception(art::errors::Configuration) + << "Module tag instance name (`TriggerGatesTag`: '" + << defModule.encode() << "') and the threshold '" << thresholdStr + << "' are both set. One of them must be empty.\n"; + } + return { defModule.label(), thresholdStr, defModule.process() }; +} // icarus::trigger::SlidingWindowTriggerSimulation::makeTag() + + //------------------------------------------------------------------------------ DEFINE_ART_MODULE(icarus::trigger::SlidingWindowTriggerSimulation) diff --git a/icaruscode/PMT/Trigger/SlidingWindowTrigger_module.cc b/icaruscode/PMT/Trigger/SlidingWindowTrigger_module.cc index ad4e36dd4..4c19ff057 100644 --- a/icaruscode/PMT/Trigger/SlidingWindowTrigger_module.cc +++ b/icaruscode/PMT/Trigger/SlidingWindowTrigger_module.cc @@ -226,9 +226,9 @@ class icarus::trigger::SlidingWindowTrigger: public art::EDProducer { using Comment = fhicl::Comment; - fhicl::Atom TriggerGatesTag { + fhicl::Atom TriggerGatesTag { Name("TriggerGatesTag"), - Comment("label of the input trigger gate data product (no instance name)") + Comment("tag of the input trigger gate data product (no instance name)") }; fhicl::Sequence Thresholds { @@ -385,6 +385,9 @@ class icarus::trigger::SlidingWindowTrigger: public art::EDProducer { art::Assns const& assns ); + static art::InputTag makeTag + (art::InputTag const& defModule, std::string const& thresholdStr); + }; // class icarus::trigger::SlidingWindowTrigger @@ -435,9 +438,9 @@ icarus::trigger::SlidingWindowTrigger::SlidingWindowTrigger // // more complex parameter parsing // - std::string const discrModuleLabel = config().TriggerGatesTag(); + art::InputTag const& discrModuleTag = config().TriggerGatesTag(); for (std::string const& threshold: config().Thresholds()) - fADCthresholds[threshold] = art::InputTag{ discrModuleLabel, threshold }; + fADCthresholds[threshold] = makeTag(discrModuleTag, threshold); // // configuration report (short) @@ -668,6 +671,21 @@ auto icarus::trigger::SlidingWindowTrigger::ReadTriggerGates( } // icarus::trigger::SlidingWindowTrigger::ReadTriggerGates() + +//------------------------------------------------------------------------------ +art::InputTag icarus::trigger::SlidingWindowTrigger::makeTag + (art::InputTag const& defModule, std::string const& thresholdStr) +{ + if (!thresholdStr.empty() && !defModule.instance().empty()) { + throw art::Exception(art::errors::Configuration) + << "Module tag instance name (`TriggerGatesTag`: '" + << defModule.encode() << "') and the threshold '" << thresholdStr + << "' are both set. One of them must be empty.\n"; + } + return { defModule.label(), thresholdStr, defModule.process() }; +} // icarus::trigger::SlidingWindowTrigger::makeTag() + + //------------------------------------------------------------------------------ DEFINE_ART_MODULE(icarus::trigger::SlidingWindowTrigger) diff --git a/icaruscode/PMT/Trigger/TriggerEfficiencyPlotsBase.cxx b/icaruscode/PMT/Trigger/TriggerEfficiencyPlotsBase.cxx index b423ecc66..f769f0c6b 100644 --- a/icaruscode/PMT/Trigger/TriggerEfficiencyPlotsBase.cxx +++ b/icaruscode/PMT/Trigger/TriggerEfficiencyPlotsBase.cxx @@ -531,9 +531,9 @@ icarus::trigger::TriggerEfficiencyPlotsBase::TriggerEfficiencyPlotsBase if (fLogEventDetails.empty()) fLogEventDetails = fLogCategory; } // if EventDetailsLogCategory is specified - std::string const discrModuleLabel = config.TriggerGatesTag(); + art::InputTag const& discrModuleTag = config.TriggerGatesTag(); for (std::string const& threshold: config.Thresholds()) - fADCthresholds[threshold] = art::InputTag{ discrModuleLabel, threshold }; + fADCthresholds[threshold] = makeInputTag(discrModuleTag, threshold); if (config.EventTreeName.hasValue()) { @@ -1497,4 +1497,18 @@ icarus::trigger::TriggerEfficiencyPlotsBase::makeEdepTag( } // icarus::trigger::MakeTriggerSimulationTree::makeEdepTag() +//------------------------------------------------------------------------------ +art::InputTag icarus::trigger::TriggerEfficiencyPlotsBase::makeInputTag + (art::InputTag const& defModule, std::string const& thresholdStr) +{ + if (!thresholdStr.empty() && !defModule.instance().empty()) { + throw art::Exception(art::errors::Configuration) + << "Module tag instance name (`TriggerGatesTag`: '" + << defModule.encode() << "') and the threshold '" << thresholdStr + << "' are both set. One of them must be empty.\n"; + } + return { defModule.label(), thresholdStr, defModule.process() }; +} // icarus::trigger::TriggerEfficiencyPlotsBase::makeInputTag() + + //------------------------------------------------------------------------------ diff --git a/icaruscode/PMT/Trigger/TriggerEfficiencyPlotsBase.h b/icaruscode/PMT/Trigger/TriggerEfficiencyPlotsBase.h index 7741cd726..69b1913a8 100644 --- a/icaruscode/PMT/Trigger/TriggerEfficiencyPlotsBase.h +++ b/icaruscode/PMT/Trigger/TriggerEfficiencyPlotsBase.h @@ -222,7 +222,7 @@ class icarus::trigger::details::TriggerPassCounters { /// Registers a new pattern in the index and returns its index (unchecked). std::size_t registerPattern(std::string const& name); - + }; // icarus::trigger::details::TriggerPassCounters @@ -1010,9 +1010,9 @@ class icarus::trigger::TriggerEfficiencyPlotsBase { Comment("label of energy deposition summary data product") }; - fhicl::Atom TriggerGatesTag { + fhicl::Atom TriggerGatesTag { Name("TriggerGatesTag"), - Comment("label of the input trigger gate data product (no instance name)") + Comment("tag of the input trigger gate data product (no instance name)") }; fhicl::Sequence Thresholds { @@ -1565,6 +1565,10 @@ class icarus::trigger::TriggerEfficiencyPlotsBase { fhicl::OptionalAtom const& EnergyDepositSummaryTag ); + /// Returns `defModule` with instance name replaced by `thresholdStr`. + static art::InputTag makeInputTag + (art::InputTag const& defModule, std::string const& thresholdStr); + }; // icarus::trigger::TriggerEfficiencyPlotsBase diff --git a/icaruscode/PMT/Trigger/TriggerEfficiencyPlots_module.cc b/icaruscode/PMT/Trigger/TriggerEfficiencyPlots_module.cc index 6c47661b9..970dc31b8 100644 --- a/icaruscode/PMT/Trigger/TriggerEfficiencyPlots_module.cc +++ b/icaruscode/PMT/Trigger/TriggerEfficiencyPlots_module.cc @@ -1246,9 +1246,9 @@ class icarus::trigger::TriggerEfficiencyPlots: public art::EDAnalyzer { std::vector{ "largeant:TPCActive" } }; - fhicl::Atom TriggerGatesTag { + fhicl::Atom TriggerGatesTag { Name("TriggerGatesTag"), - Comment("label of the input trigger gate data product (no instance name)") + Comment("tag of the input trigger gate data product (no instance name)") }; fhicl::Sequence Thresholds { @@ -1496,6 +1496,11 @@ class icarus::trigger::TriggerEfficiencyPlots: public art::EDAnalyzer { /// Returns a gate that is `Max()` of all the specified `gates`. template static auto computeMaxGate(TrigGateColl const& gates); + + + /// Returns `defModule` with instance name replaced by `thresholdStr`. + static art::InputTag makeTag + (art::InputTag const& defModule, std::string const& thresholdStr); }; // icarus::trigger::TriggerEfficiencyPlots @@ -1533,10 +1538,10 @@ icarus::trigger::TriggerEfficiencyPlots::TriggerEfficiencyPlots if (fLogEventDetails.empty()) fLogEventDetails = fLogCategory; } // if EventDetailsLogCategory is specified - std::string const discrModuleLabel = config().TriggerGatesTag(); + art::InputTag const& discrModuleTag = config().TriggerGatesTag(); for (raw::ADC_Count_t threshold: config().Thresholds()) { fADCthresholds[icarus::trigger::ADCCounts_t{threshold}] - = art::InputTag{ discrModuleLabel, util::to_string(threshold) }; + = makeTag(discrModuleTag, util::to_string(threshold)); } if (config().EventTreeName.hasValue()) { @@ -2467,6 +2472,20 @@ auto icarus::trigger::TriggerEfficiencyPlots::ReadTriggerGates } // icarus::trigger::TriggerEfficiencyPlots::ReadTriggerGates() +//------------------------------------------------------------------------------ +art::InputTag icarus::trigger::TriggerEfficiencyPlots::makeTag + (art::InputTag const& defModule, std::string const& thresholdStr) +{ + if (!thresholdStr.empty() && !defModule.instance().empty()) { + throw art::Exception(art::errors::Configuration) + << "Module tag instance name (`TriggerGatesTag`: '" + << defModule.encode() << "') and the threshold '" << thresholdStr + << "' are both set. One of them must be empty.\n"; + } + return { defModule.label(), thresholdStr, defModule.process() }; +} // icarus::trigger::TriggerEfficiencyPlots::makeTag() + + //------------------------------------------------------------------------------ //--- EventIDTree //------------------------------------------------------------------------------ diff --git a/icaruscode/PMT/Trigger/TriggerSimulationOnGates_module.cc b/icaruscode/PMT/Trigger/TriggerSimulationOnGates_module.cc index eee5c986a..07d7993d1 100644 --- a/icaruscode/PMT/Trigger/TriggerSimulationOnGates_module.cc +++ b/icaruscode/PMT/Trigger/TriggerSimulationOnGates_module.cc @@ -178,6 +178,8 @@ namespace icarus::trigger { class TriggerSimulationOnGates; } * will not suppress one at the beginning of the next one even if that is * within the set dead time. If set to a very large value (as the default), * only one trigger will be found per input gate. + * * `TriggerDelay` (time, default: `0 ns`): fixed time to add to the time of + * all the triggers. * * `EmitEmpty` (flag, default: `true`): if set, each gate gets at least a * trigger object, and if there is no trigger during a gate its trigger * object will be marked by having no bit set. If unset, when there is no @@ -231,8 +233,9 @@ namespace icarus::trigger { class TriggerSimulationOnGates; } * `KeepThresholdName` configuration parameters); * at least one trigger object is produced for each of the beam gates found * in the input data product specified by the `BeamGates` parameter. - * Each trigger object has the time stamp matching the time the trigger - * criteria are satisfied. All triggers feature the bits specified in + * Each trigger object has the time stamp matching the time when the trigger + * criteria were satisfied, plus the fixed delay in the `TriggerDelay` + * configuration parameter. All triggers feature the bits specified in * `BeamBits` configuration parameter, with the following exceptions: * if there was no trigger found, the bits will all be cleared; and if there * was more than one trigger found in the same gate, all triggers except the @@ -271,7 +274,7 @@ namespace icarus::trigger { class TriggerSimulationOnGates; } * will return `true` even when there was no trigger firing. * * - * ### Trigger bits + * ### Trigger bits in `raw::Trigger` * * The trigger bits may be overridden by the `BeamBits` configuration parameter. * If they are not, the bits from an input "beam" gate will be used verbatim for @@ -293,6 +296,21 @@ namespace icarus::trigger { class TriggerSimulationOnGates; } * beyond the number of available bits effectively disables that option. * * + * ### Trigger timing + * + * This module replicates the logic used by the hardware to provide a trigger + * response, but it does not attempt to emulate the delays that occur in the + * hardware. The trigger time is by default the instant when in the input + * waveforms the conditions are met, assuming that their synchronization already + * reflects the one on the hardware input. + * The only allowance to the hardware timing is the option to add a _fixed_ + * delay (`TriggerDelay` configuration parameter) to all trigger times produced + * by this module. The `beamToTrigger` delay, on the other end, is deliberately + * excluded from that delay, and no specific delay is offered for it. + * + * + * + * * Trigger logic algorithm * ======================== * @@ -406,9 +424,9 @@ class icarus::trigger::TriggerSimulationOnGates using Name = fhicl::Name; using Comment = fhicl::Comment; - fhicl::Atom TriggerGatesTag { + fhicl::Atom TriggerGatesTag { Name("TriggerGatesTag"), - Comment("label of the input trigger gate data product (no instance name)") + Comment("tag of the input trigger gate data product (no instance name)") }; fhicl::Sequence Thresholds { @@ -461,6 +479,12 @@ class icarus::trigger::TriggerSimulationOnGates std::numeric_limits::max() }; + fhicl::Atom TriggerDelay { + Name("TriggerDelay"), + Comment("trigger response delay, added to the trigger times"), + 0_ns // default + }; + fhicl::Atom RetriggeringBit { Name("RetriggeringBit"), Comment( @@ -581,6 +605,8 @@ class icarus::trigger::TriggerSimulationOnGates nanoseconds const fDeadTime; ///< Veto time after a trigger in a gate. + nanoseconds const fTriggerDelay; ///< Time to be added to the trigger time. + /// Bit mask set for triggers after the first one in a gate. TriggerBits_t const fRetriggeringMask; @@ -700,7 +726,7 @@ class icarus::trigger::TriggerSimulationOnGates /** * @brief Converts the trigger information into trigger objects. * @param detTimings detector clocks service provider proxy - * @param beamGates list of all beam gates to evaluate + * @param beamGate beam gate used for the evaluation (in beam gate time scale) * @param eventInfo event-wide information to be transferred to trigger data * @param triggerNumber the "unique" number to assign to this trigger * @param info the information about the fired triggers @@ -776,6 +802,10 @@ class icarus::trigger::TriggerSimulationOnGates (icarus::trigger::WindowChannelMap::WindowInfo_t const& winfo) { return winfo.composition.cryoid; } + /// Returns `defModule` with instance name replaced by `thresholdStr`. + static art::InputTag makeTag + (art::InputTag const& defModule, std::string const& thresholdStr); + }; // icarus::trigger::TriggerSimulationOnGates @@ -843,6 +873,7 @@ icarus::trigger::TriggerSimulationOnGates::TriggerSimulationOnGates , fEmitEmpty (config().EmitEmpty()) , fExtraInfo (config().ExtraInfo()) , fDeadTime (config().DeadTime()) + , fTriggerDelay (config().TriggerDelay()) , fRetriggeringMask (bitMask(config().RetriggeringBit())) , fCryostatZeroMask (bitMask(config().CryostatFirstBit())) , fTriggerTimeResolution(config().TriggerTimeResolution()) @@ -878,9 +909,9 @@ icarus::trigger::TriggerSimulationOnGates::TriggerSimulationOnGates // // more complex parameter parsing // - std::string const& discrModuleLabel = config().TriggerGatesTag(); + art::InputTag const& discrModuleTag = config().TriggerGatesTag(); for (std::string const& threshold: config().Thresholds()) - fADCthresholds[threshold] = art::InputTag{ discrModuleLabel, threshold }; + fADCthresholds[threshold] = makeTag(discrModuleTag, threshold); // initialization of a vector of atomic is not as trivial as it sounds... fTriggerCount = std::vector>(fADCthresholds.size()); @@ -929,27 +960,23 @@ icarus::trigger::TriggerSimulationOnGates::TriggerSimulationOnGates log << "\nConfigured " << fADCthresholds.size() << " thresholds (ADC):"; for (auto const& [ thresholdTag, dataTag ]: fADCthresholds) log << "\n * " << thresholdTag << " (from '" << dataTag.encode() << "')"; -#if 0 // TODO restore after adoption of https://github.com/LArSoft/lardataalg/pull/44 + log << "\nOther parameters:" << "\n * trigger time resolution: " << fTriggerTimeResolution - << "\n * input beam gate reference time: " - << util::StandardSelectorFor{} + << "\n * trigger response delay: " << fTriggerDelay + << "\n * input beam gate: '" << fBeamGateTag.encode() + << "', reference time: " << util::StandardSelectorFor{} .get(fBeamGateReference).name() -#else - util::StandardSelectorFor const timeScaleSelector; - log << "\nOther parameters:" - << "\n * trigger time resolution: " << fTriggerTimeResolution - << "\n * input beam gate reference time: " - << timeScaleSelector.get(fBeamGateReference).name() -#endif ; if (fDeadTime == std::numeric_limits::max()) log << "\n * only one trigger per beam gate (infinite dead time)"; else { log << "\n * veto time after a trigger in one gate: " << fDeadTime << "\n * retriggering bit:"; - if (config().RetriggeringBit() >= NTriggerBits) - log << " none (#" << config().RetriggeringBit() << ", 0x" << std::hex << fRetriggeringMask << std::dec << ")"; + if (config().RetriggeringBit() >= NTriggerBits) { + log << " none (#" << config().RetriggeringBit() + << ", 0x" << std::hex << fRetriggeringMask << std::dec << ")"; + } else { log << " #" << config().RetriggeringBit() << " (0x" << std::hex << fRetriggeringMask << std::dec << ")"; @@ -1002,7 +1029,7 @@ void icarus::trigger::TriggerSimulationOnGates::produce(art::Event& event) { // BEGIN local block - mf::LogDebug log { fLogCategory }; + mf::LogDebug log(fLogCategory); log << "Trigger simulation for " << beamGates.size() << " gates"; if (!beamGates.empty()) { log << " ('" << fBeamGateTag.encode() << "'):"; @@ -1354,11 +1381,14 @@ auto icarus::trigger::TriggerSimulationOnGates::produceForThreshold( for (sim::BeamGateInfo const& beamGate: beamGates) { std::vector triggerInfos; - // relative to the beam gate time (also simulation time for MC); + // relative to the beam gate time (also simulation time for plain MC); nanoseconds start{ toBeamGateTime (util::quantities::nanosecond{ beamGate.Start() }, detTimings) }; nanoseconds const stop{ start + nanoseconds{ beamGate.Width() } }; + sim::BeamGateInfo const beamGateInBGTime + { start.value(), beamGate.Width(), beamGate.BeamType() }; + while (start < stop) { icarus::trigger::ApplyBeamGateClass const applyBeamGate @@ -1400,7 +1430,7 @@ auto icarus::trigger::TriggerSimulationOnGates::produceForThreshold( // create and store the data product // auto [ gateTriggers, extraInfo ] = triggerInfoToTriggerData - (detTimings, beamGate, eventInfo, triggerNumber++, triggerInfos); + (detTimings, beamGateInBGTime, eventInfo, triggerNumber++, triggerInfos); append(*triggers, std::move(gateTriggers)); @@ -1558,21 +1588,11 @@ auto icarus::trigger::TriggerSimulationOnGates::toBeamGateTime( (detinfo::timescales::simulation_time{ time }); break; default: -#if 0 // TODO restore after adoption of https://github.com/LArSoft/lardataalg/pull/44 throw art::Exception{ art::errors::Configuration } << "Conversion of times from reference '" << util::StandardSelectorFor{} .get(fBeamGateReference).name() << "' not supported.\n"; -#else - { - util::StandardSelectorFor const timeScaleSelector; - throw art::Exception{ art::errors::Configuration } - << "Conversion of times from reference '" - << timeScaleSelector.get(fBeamGateReference).name() - << "' not supported.\n"; - } -#endif } // switch return time_es - detTimings.BeamGateTime(); @@ -1631,6 +1651,10 @@ icarus::trigger::TriggerSimulationOnGates::triggerInfoToTriggerData( detinfo::timescales::electronics_time const triggerTime = detTimings.toElectronicsTime(trInfo.info.atTick()); + // include the delay from timestamping (and all other fixed delays) + detinfo::timescales::electronics_time const timestampedTriggerTime + = triggerTime + fTriggerDelay; + // find the location and set the bits accordingly geo::CryostatID const triggeringCryo = WindowCryostat(fWindowMapMan->info(trInfo.extra.windowIndex)); @@ -1639,7 +1663,7 @@ icarus::trigger::TriggerSimulationOnGates::triggerInfoToTriggerData( triggers.emplace_back( triggerNumber, // counter - double(triggerTime), // trigger time + double(timestampedTriggerTime), // trigger time double(beamTime), // beam gate beamBits | retriggerMask | sourceMask // bits ); @@ -1647,7 +1671,7 @@ icarus::trigger::TriggerSimulationOnGates::triggerInfoToTriggerData( retriggerMask = fRetriggeringMask; if (fillExtraInfo) { - nanoseconds const relTrigTime = triggerTime - beamTime; + nanoseconds const relTrigTime = timestampedTriggerTime - beamTime; extraInfo.triggerTimestamp = eventInfo.time; extraInfo.beamGateTimestamp = extraInfo.triggerTimestamp @@ -1775,6 +1799,20 @@ double icarus::trigger::TriggerSimulationOnGates::eventTimestampInSeconds { return eventTimestampInSeconds(event.time()); } +//------------------------------------------------------------------------------ +art::InputTag icarus::trigger::TriggerSimulationOnGates::makeTag + (art::InputTag const& defModule, std::string const& thresholdStr) +{ + if (!thresholdStr.empty() && !defModule.instance().empty()) { + throw art::Exception(art::errors::Configuration) + << "Module tag instance name (`TriggerGatesTag`: '" + << defModule.encode() << "') and the threshold '" << thresholdStr + << "' are both set. One of them must be empty.\n"; + } + return { defModule.label(), thresholdStr, defModule.process() }; +} // icarus::trigger::TriggerSimulationOnGates::makeTag() + + //------------------------------------------------------------------------------ DEFINE_ART_MODULE(icarus::trigger::TriggerSimulationOnGates) diff --git a/icaruscode/PMT/Trigger/trigger_emulation_icarus.fcl b/icaruscode/PMT/Trigger/trigger_emulation_icarus.fcl index 356808b83..15a2aa09b 100644 --- a/icaruscode/PMT/Trigger/trigger_emulation_icarus.fcl +++ b/icaruscode/PMT/Trigger/trigger_emulation_icarus.fcl @@ -60,7 +60,7 @@ # (`icarus_pmtdiscriminatethr`). # The default for MC is `pmtthr` and data products have instance names # according to the discriminated thresholds (e.g. `pmtthr:400`). -# Data default (`pmtthrfixed`?) is a TODO here. +# Data default is also `pmtthr`. # * paired (LVDS) PMT signals (`icarus_lvdsgates`): `pmtlvdsgates` # * window combination of LVDS signals (`icarus_trigslidewindow`): # `pmttriggerwindows` @@ -74,9 +74,13 @@ # : original version # 20221104 (petrillo@slac.stanford.edu) [v2.0] # : added many configurations, and rationalized (?) their names +# 20260403 (petrillo@slac.stanford.edu) [v3.0] +# : * discrimination baselines based on data product for simulation too +# : (breaking change: needs updated SimPMTIcarus writing baselines) +# : * reduced the number of discriminated thresholds in shifting workflow # -#include "trigger_icarus.fcl" +#include "triggeremu_settings_icarus.fcl" #include "triggerwindowdefs_icarus.fcl" @@ -227,12 +231,13 @@ icarus_pmtdiscriminatethr_fixed: { # # This module applies discrimination based on fixed threshold specified in the # configuration. -# The baseline is fixed and read from global settings. +# The baseline is read from a data product. # It produces discriminated waveforms ("ReadoutTriggerGate" objects) # and complimentary waveform summary data (all the waveform information that # is not sample values: `sbn::OpDetWaveformMeta` objects). # # This module does requires `Geometry` and `DetectorClocksService` services. +# It also requires input from an updated SimPMTIcarus producing baselines too. # # Usually this module is usually run only once; standard label: "pmtthr": # @@ -246,9 +251,8 @@ icarus_pmtdiscriminatethr_fixed_MC: { # input optical waveform data product tag: OpticalWaveforms: "opdaq" - # PMT baseline (from standard simulation configuration) - Baselines: @erase - Baseline: @local::icarus_settings_opdet.NominalPedestal + # PMT baseline (written together with the waveforms themselves) + Baselines: opdaq } # icarus_pmtdiscriminatethr_fixed_MC @@ -333,6 +337,12 @@ icarus_trigslidewindow_cryoW: { # ------------------------------------------------------------------------------ # --- workflow helpers # ------------------------------------------------------------------------------ +# +# Configurations for trigger in simulation. +# +# The thresholds are picked from `icarus_triggergate_basic.SelectedThreshold` +# in `trigger_icarus.fcl`. +# icarus_standard_triggersim: { producers: { @@ -360,6 +370,7 @@ icarus_standard_triggersim: { Pattern: @local::icarus_triggergate_basic.SelectedPattern BeamGates: triggersimgates TriggerTimeResolution: "8 ns" # this should probably be 12 or 24 ns + TriggerDelay: @local::triggeremu_config_icarus.settings.TriggerDelay # triggeremu_settings_icarus.fcl EmitEmpty: true ExtraInfo: true } @@ -380,7 +391,10 @@ icarus_shifting_triggersim: { producers: { - pmtfixedthrinit: @local::icarus_pmtdiscriminatethr_fixed_MC + pmtfixedthrinit: { + @table::icarus_pmtdiscriminatethr_fixed_MC + SelectThresholds: [ @local::icarus_triggergate_basic.SelectedThreshold ] + } pmtlvdsgatesinit: { @table::icarus_standard_triggersim.producers.pmtlvdsgates @@ -404,6 +418,7 @@ icarus_shifting_triggersim: { emuTriggerUnshifted: { @table::icarus_standard_triggersim.producers.emuTrigger BeamGates: triggersimgatesinit + BeamGateReference: Simulation TriggerGatesTag: pmttriggerwindowsinit } @@ -416,6 +431,7 @@ icarus_shifting_triggersim: { InitSimEnergyDepositLiteLabel: "sedlite" InitSimPhotonsLabel: "largeant" InitWaveformLabel: "opdaq" + BindWaveformBaselines: "opdaq" ShiftAuxDetIDEs: true ShiftBeamGateInfo: true ShiftSimEnergyDeposits: true @@ -441,6 +457,19 @@ icarus_shifting_triggersim: { AdditionalOffset: 0.0 # [us] Additional offset to apply to the time shift } + emuTrigger: { + @table::icarus_standard_triggersim.producers.emuTrigger + # biting our own tail: the `shifted` beam gate info object is the result + # of the "standard" shifting by `AdjustSimForTrigger` on the old beam gate + # info, whose reference was the simulation start time. At the time of its + # production, simulation start time was also the trigger time (by default) + # so that the same shift that moves the trigger time does apply also to + # numbers expressed in (old) simulation time. + # Not that we have a "new" simulation time (at the time of writing). + BeamGates: shifted + BeamGateReference: Trigger + } + } # producers path: [ diff --git a/icaruscode/PMT/Trigger/trigger_icarus.fcl b/icaruscode/PMT/Trigger/trigger_icarus.fcl index 5c8c11bc8..77c71dcfb 100644 --- a/icaruscode/PMT/Trigger/trigger_icarus.fcl +++ b/icaruscode/PMT/Trigger/trigger_icarus.fcl @@ -17,6 +17,8 @@ # 20221104 (petrillo@slac.stanford.edu) # overhaul of the configuration to just host settings and parameters; # module configurations are now in `trigger_emulation_icarus.fcl` +# 20260403 (petrillo@slac.stanford.edu) +# changed discrimination threshold from 400 ADC# to 390 (only Run1 has 400) # # @@ -84,7 +86,7 @@ NuMI_settings: { icarus_triggergate_basic.Baseline: 15000 # This is the primary PMT discrimination threshold, chosen for the hardware. -icarus_triggergate_basic.SelectedThreshold: 400 # ADC +icarus_triggergate_basic.SelectedThreshold: 390 # ADC # This is the primary PMT adder discrimination threshold (in ADC equivalent). icarus_triggergate_basic.SelectedAdderThreshold: 492 # 60 mV x (2^14 / 2 V) => 491.52 ADC# @@ -272,8 +274,21 @@ icarus_region_finder: { # this configuration takes the configured beam gate duration # and creates extended gates to be used for trigger evaluation; # [20221104] the amount of extension needs to be verified with Run2 data +# [20260415] shifting for the cable delay, that is now simulated at waveform level: +# -200 (margin) + 265 (average delay for Run2 data); +# estimation from the reconstructed triggering flash times, +# from simulation enabling trigger emulation (with no trigger delay) +# and standard hit reconstruction (which includes time correction). +# Simulation adds the delay, then the shifting to trigger time eats it, +# and the hit correction effectively moves the hits away from the trigger. +# There are other ways to estimate it (e.g. average on DB coefficients!) +# [20260507] tuning Run2 gates: spills 128 ns (BNB) and 200 ns (NuMI) after gate +# opening; expecting neutrinos to appear at +30 ns (simulation time) +# because of simulated propagation time from the flux window (-20 m) +# to the face of the detector (~-10 m); values hard-coded because +# they're observed in Run2 data # -icarus_triggersimgates_Run1: { +icarus_triggersimgates_Run2: { module_type: FixBeamGateInfo @@ -282,20 +297,22 @@ icarus_triggersimgates_Run1: { Changes: [ # gates of type not included are left unchanged { # for BNB gates Select: { Types: [ "BNB" ] } - Start: { SetTo: "-0.2 us" } - Width: { SetTo: @local::BNB_settings.spill_duration Add: "0.5 us" } + Start: { SetTo: "+0.100 us" } + Width: { SetTo: "2.1 us" } }, { # for NuMI gates Select: { Types: [ "NuMI" ]} - Start: { SetTo: "-0.2 us" } - Width: { SetTo: @local::NuMI_settings.spill_duration Add: "0.6 us" } + Start: { SetTo: "+0.065 us" } + Width: { SetTo: "10.0 us" } } ] -} # icarus_triggersimgates_Run1 +} # icarus_triggersimgates_Run2 -icarus_triggersimgates: @local::icarus_triggersimgates_Run1 +icarus_triggersimgates_Run1: @local::icarus_triggersimgates_Run2 + +icarus_triggersimgates: @local::icarus_triggersimgates_Run2 ### @@ -311,8 +328,8 @@ icarus_enablegate_sim: { BeamGates: [ { - Duration: @local::icarus_pmtsimulationalg_standard.ReadoutEnablePeriod # from pmtsimulation_icarus.fcl - Start: @local::icarus_pmtsimulationalg_standard.TriggerOffsetPMT # from pmtsimulation_icarus.fcl + Duration: @local::icarus_pmtsimulationalg_standard_run2.ReadoutEnablePeriod # from pmtsimulation_icarus.fcl + Start: @local::icarus_pmtsimulationalg_standard_run2.TriggerOffsetPMT # from pmtsimulation_icarus.fcl } ] @@ -399,16 +416,31 @@ icarus_triggergate_basic.patterns.S5: { inMainWindow: 3 # relevant only to disambiguate the location } +icarus_triggergate_basic.patterns.S7: { + sumOfOppositeWindows: 7 + inMainWindow: 4 # relevant only to disambiguate the location +} + icarus_triggergate_basic.patterns.S8: { sumOfOppositeWindows: 8 inMainWindow: 4 # irrelevant } +icarus_triggergate_basic.patterns.S9: { + sumOfOppositeWindows: 9 + inMainWindow: 5 # relevant only to disambiguate the location +} + icarus_triggergate_basic.patterns.S10: { sumOfOppositeWindows: 10 inMainWindow: 5 # irrelevant } +icarus_triggergate_basic.patterns.S15: { + sumOfOppositeWindows: 15 + inMainWindow: 8 # relevant only to disambiguate the location +} + icarus_triggergate_basic.SelectedPattern: @local::icarus_triggergate_basic.patterns.S5 icarus_triggergate_basic.SelectedPrimitivePattern: @local::icarus_triggergate_basic.patterns.S10 diff --git a/icaruscode/PMT/Trigger/triggeremu_settings_icarus.fcl b/icaruscode/PMT/Trigger/triggeremu_settings_icarus.fcl index b3cc82018..91c98295b 100644 --- a/icaruscode/PMT/Trigger/triggeremu_settings_icarus.fcl +++ b/icaruscode/PMT/Trigger/triggeremu_settings_icarus.fcl @@ -61,6 +61,7 @@ # #include "trigger_icarus.fcl" +#include "triggerwindowdefs_icarus.fcl" # ------------------------------------------------------------------------------ BEGIN_PROLOG @@ -99,6 +100,18 @@ BEGIN_PROLOG triggeremu_config_icarus.settings.missing_pmt_channels: @local::icarus_trigger_channel_ignore_202101 # from trigger_icarus.fcl +# +# trigger timings +# + +# Run3 delay extracted comparing emulated and hardware trigger time +# from run 11952 (majority only), not presented yet +triggeremu_config_icarus.settings.TriggerDelay_Run2: "345 ns" +triggeremu_config_icarus.settings.TriggerDelay_Run3: "350 ns" +triggeremu_config_icarus.settings.TriggerDelay: + @local::triggeremu_config_icarus.settings.TriggerDelay_Run2 + + # beam gate window for trigger emulation: # - duration (BNB: 1.6 us; NuMI: 9.5 us; and some buffer) triggeremu_config_icarus.settings.BeamGate.Duration: "1.70 us" @@ -153,7 +166,7 @@ triggeremu_config_icarus.settings.trigger_channel_pairings_nosingle: [ # settings for plotting efficiency under patterns of sliding window triggers # firing during beam time triggeremu_config_icarus.settings.SlidingWindowTriggerPatternsSingleWindow: [ - { inMainWindow: 1 }, + @local::icarus_triggergate_basic.patterns.M1, # from trigger_icarus.fcl { inMainWindow: 2 }, { inMainWindow: 3 }, { inMainWindow: 4 }, @@ -169,11 +182,11 @@ triggeremu_config_icarus.settings.SlidingWindowTriggerPatternsWindowPair: [ { inMainWindow: 4 inOppositeWindow: 4 }, # [3] { inMainWindow: 5 inOppositeWindow: 5 }, # [4] { inMainWindow: 6 inOppositeWindow: 6 }, # [5] - { inMainWindow: 2 sumOfOppositeWindows: 3 }, # [6] - { inMainWindow: 3 sumOfOppositeWindows: 5 }, # [7] - { inMainWindow: 4 sumOfOppositeWindows: 8 }, # [8] - { inMainWindow: 5 sumOfOppositeWindows: 10 }, # [9] - { inMainWindow: 8 sumOfOppositeWindows: 15 } # [10] + @local::icarus_triggergate_basic.patterns.S3, # [6] + @local::icarus_triggergate_basic.patterns.S5, # [7] + @local::icarus_triggergate_basic.patterns.S8, # [8] + @local::icarus_triggergate_basic.patterns.S10, # [9] + @local::icarus_triggergate_basic.patterns.S15 # [10] ] # SlidingWindowTriggerPatternsWindowPair diff --git a/icaruscode/PMT/Trigger/triggerwindowdefs_icarus.fcl b/icaruscode/PMT/Trigger/triggerwindowdefs_icarus.fcl index 167fc030f..ff663a804 100644 --- a/icaruscode/PMT/Trigger/triggerwindowdefs_icarus.fcl +++ b/icaruscode/PMT/Trigger/triggerwindowdefs_icarus.fcl @@ -35,8 +35,6 @@ # # -#include "trigger_icarus.fcl" - BEGIN_PROLOG # diff --git a/icaruscode/PMT/opdetsim_pmt_icarus.fcl b/icaruscode/PMT/opdetsim_pmt_icarus.fcl index 06a6f00e9..341b5fd39 100644 --- a/icaruscode/PMT/opdetsim_pmt_icarus.fcl +++ b/icaruscode/PMT/opdetsim_pmt_icarus.fcl @@ -5,23 +5,37 @@ BEGIN_PROLOG -icarus_simpmt: +icarus_simpmt_run2: { module_type: "SimPMTIcarus" InputModule: "pdfastsim" - @table::icarus_pmtsimulationalg_standard + @table::icarus_pmtsimulationalg_standard_run2 } +icarus_simpmt_run4: +{ + module_type: "SimPMTIcarus" + InputModule: "pdfastsim" + + @table::icarus_pmtsimulationalg_standard_run4 +} + +icarus_simpmt_run3: @local::icarus_simpmt_run4 -icarus_simpmt_nonoise: { - @table::icarus_simpmt +icarus_simpmt_run2_nonoise: { + @table::icarus_simpmt_run2 @table::icarus_pmtsimulation.disable_noise } -icarus_simpmt_noise: { - @table::icarus_simpmt +icarus_simpmt_run4_nonoise: { + @table::icarus_simpmt_run4 + @table::icarus_pmtsimulation.disable_noise } +icarus_simpmt_run3_nonoise: @local::icarus_simpmt_run4_nonoise + +icarus_simpmt: @local::icarus_simpmt_run2 +icarus_simpmt_nonoise: @local::icarus_simpmt_run2_nonoise END_PROLOG diff --git a/icaruscode/Timing/PMTTimingCorrections.h b/icaruscode/Timing/PMTTimingCorrections.h index 098445471..c011673da 100644 --- a/icaruscode/Timing/PMTTimingCorrections.h +++ b/icaruscode/Timing/PMTTimingCorrections.h @@ -19,6 +19,8 @@ #include "art/Framework/Services/Registry/ServiceDeclarationMacros.h" #include "art/Framework/Principal/Run.h" +#include + namespace icarusDB { class PMTTimingCorrections: lar::UncopiableClass @@ -35,6 +37,10 @@ namespace icarusDB { virtual double getCosmicsCorrections( unsigned int channelID ) const = 0; + virtual std::string getLaserDatabaseTag() const = 0; + + virtual std::string getCosmicsDatabaseTag() const = 0; + }; // end class }// end of namespace diff --git a/icaruscode/Timing/PMTTimingCorrectionsProvider.cxx b/icaruscode/Timing/PMTTimingCorrectionsProvider.cxx index c2be07204..ca618a2bb 100644 --- a/icaruscode/Timing/PMTTimingCorrectionsProvider.cxx +++ b/icaruscode/Timing/PMTTimingCorrectionsProvider.cxx @@ -25,16 +25,17 @@ //-------------------------------------------------------------------------------- icarusDB::PMTTimingCorrectionsProvider::PMTTimingCorrectionsProvider - (const fhicl::ParameterSet& pset) + (const fhicl::ParameterSet& pset) : fVerbose{ pset.get("Verbose", false) } , fLogCategory{ pset.get("LogCategory", "PMTTimingCorrection") } - { + , fOverrideRunNumber{ pset.get("OverrideRunNumber", -1) } + { fhicl::ParameterSet const tags{ pset.get("CorrectionTags") }; fCablesTag = tags.get("CablesTag"); fLaserTag = tags.get("LaserTag"); fCosmicsTag = tags.get("CosmicsTag"); if( fVerbose ) mf::LogInfo(fLogCategory) << "Database tags for timing corrections:\n" - << "Cables corrections " << fCablesTag << "\n" + << "Cable corrections " << fCablesTag << "\n" << "Laser corrections " << fLaserTag << "\n" << "Cosmics corrections " << fCosmicsTag; } @@ -210,12 +211,21 @@ void icarusDB::PMTTimingCorrectionsProvider::ReadCosmicsCorrections( uint32_t ru /// is the PMT channel number void icarusDB::PMTTimingCorrectionsProvider::readTimeCorrectionDatabase(const art::Run& run){ + uint32_t runNumber = run.id().run(); + + if (fOverrideRunNumber >= 0) { + mf::LogInfo(fLogCategory) + << "Overriding run number " << runNumber << " with " << fOverrideRunNumber + << " for DB queries."; + runNumber = static_cast(fOverrideRunNumber); + } + // Clear before the run fDatabaseTimingCorrections.clear(); - ReadPMTCablesCorrections(run.id().run()); - ReadLaserCorrections(run.id().run()); - ReadCosmicsCorrections(run.id().run()); + ReadPMTCablesCorrections(runNumber); + ReadLaserCorrections(runNumber); + ReadCosmicsCorrections(runNumber); if( fVerbose ) { diff --git a/icaruscode/Timing/PMTTimingCorrectionsProvider.h b/icaruscode/Timing/PMTTimingCorrectionsProvider.h index 85f854063..33c83cf7b 100644 --- a/icaruscode/Timing/PMTTimingCorrectionsProvider.h +++ b/icaruscode/Timing/PMTTimingCorrectionsProvider.h @@ -64,29 +64,35 @@ class icarusDB::PMTTimingCorrectionsProvider : public PMTTimingCorrections { PMTTimingCorrectionsProvider(const fhicl::ParameterSet& pset); - /// Read timing corrections from the database + /// Read timing corrections from the database void readTimeCorrectionDatabase(const art::Run& run); - /// Get time delay on the trigger line + /// Get time delay on the trigger line double getTriggerCableDelay( unsigned int channelID ) const override { return getChannelCorrOrDefault(channelID).triggerCableDelay; }; - /// Get time delay on the PPS reset line + /// Get time delay on the PPS reset line double getResetCableDelay( unsigned int channelID ) const override { return getChannelCorrOrDefault(channelID).resetCableDelay; }; - /// Get timing corrections from laser data + /// Get timing corrections from laser data double getLaserCorrections( unsigned int channelID ) const override { return getChannelCorrOrDefault(channelID).laserCableDelay; }; - /// Get timing corrections from cosmics data + /// Get timing corrections from cosmics data double getCosmicsCorrections( unsigned int channelID ) const override { return getChannelCorrOrDefault(channelID).cosmicsCorrections; }; + /// Get current laser corrections database tag + std::string getLaserDatabaseTag() const override { return fLaserTag; } + + /// Get current cosmics corrections database tag + std::string getCosmicsDatabaseTag() const override { return fCosmicsTag; }; + private: using PMTTimeCorrectionsDB = details::PMTTimeCorrectionsDB; @@ -94,11 +100,12 @@ class icarusDB::PMTTimingCorrectionsProvider : public PMTTimingCorrections { bool fVerbose = false; ///< Whether to print the configuration we read. std::string fLogCategory; ///< Category tag for messages. - std::string fCablesTag; ///< Tag for cable corrections database. - std::string fLaserTag; ///< Tag for laser corrections database. - std::string fCosmicsTag; ///< Tag for cosmics corrections database. + int fOverrideRunNumber = -1; ///< If non-negative, overrides the run number for DB queries. + std::string fCablesTag; ///< Tag for cable corrections database. + std::string fLaserTag; ///< Tag for laser corrections database. + std::string fCosmicsTag; ///< Tag for cosmics corrections database. - /// Map of corrections by channel + /// Map of corrections by channel std::map fDatabaseTimingCorrections; /// Internal access to the channel correction record; returns defaults if not present. @@ -109,8 +116,8 @@ class icarusDB::PMTTimingCorrectionsProvider : public PMTTimingCorrections { return (it == fDatabaseTimingCorrections.end())? CorrectionDefaults: it->second; } - /// Convert run number to internal database - uint64_t RunToDatabaseTimestamp(uint32_t run) const; + /// Convert run number to internal database + uint64_t RunToDatabaseTimestamp(uint32_t run) const; void ReadPMTCablesCorrections(uint32_t run); diff --git a/icaruscode/Timing/timing_icarus.fcl b/icaruscode/Timing/timing_icarus.fcl index c09df28b3..b0291232d 100644 --- a/icaruscode/Timing/timing_icarus.fcl +++ b/icaruscode/Timing/timing_icarus.fcl @@ -12,6 +12,7 @@ icarus_pmttimingservice: CosmicsTag: @local::ICARUS_Calibration_GlobalTags.pmt_cosmics_timing_data } Verbose: false + OverrideRunNumber: -1 } icarus_ophit_timing_correction: diff --git a/scripts/manageDataRunDefinitions.py b/scripts/manageDataRunDefinitions.py index cb817dc4d..2596089bd 100755 --- a/scripts/manageDataRunDefinitions.py +++ b/scripts/manageDataRunDefinitions.py @@ -1,11 +1,16 @@ #!/usr/bin/env python +# +# Version 1.1 (20260424, petrillo@slac.stanford.edu) +# * added `--suffix` and `--and` options +# * explicit support for compressed raw data, "raw" became uncompressed only +# import logging import time __author__ = "Gianluca Petrillo (petrillo@slac.stanford.edu)" -__date__ = time.strptime("July 7, 2021", "%B %d, %Y") -__version__ = "1.0" +__date__ = time.strptime("May 13, 2026", "%B %d, %Y") +__version__ = "1.1" __doc__ = """ Manages SAM definitions for ICARUS data run. @@ -45,9 +50,10 @@ DefaultStages = [ 'raw', 'stage0', 'stage1', ] StageDimensions = { - 'raw': "data_tier raw", - 'stage0': "icarus_project.stage stage0", - 'stage1': "icarus_project.stage in ( stage1, stage1_caf_larcv )", + 'raw': "data_tier raw and icarus_project.stage daq", + 'compressedraw': "data_tier raw and icarus_project.stage compression", + 'stage0': "icarus_project.stage stage0", + 'stage1': "icarus_project.stage in ( stage1, stage1_caf_larcv )", } # StageDimensions # this is a modal flag that is dangerous enough to be not user-controlled: @@ -59,13 +65,15 @@ class SampleInfo: def __init__(self, run=None, stage=None, stream=None, projectVersion=None, - minSize: "MiB" = None, + minSize: "MiB" = None, extraDims=[], suffix="", ): self.run = SampleInfo._copyList(run) self.stage = SampleInfo._copyList(stage) self.stream = SampleInfo._copyList(stream) self.projectVersion = SampleInfo._copyList(projectVersion) self.minSize = minSize if minSize else 0 + self.extraDims = SampleInfo._copyList(extraDims) + self.suffix = suffix # __init__() def isRunDefined(self) -> "returns if run is collapsed to a value": @@ -97,7 +105,7 @@ def defName(self): components.append(self.stream.replace("%", "")) # remove SAM wildcards if self.isProjectVersionComplete() and self.projectVersion is not None: components.append(self.projectVersion) - return "_".join(filter(None, components)) + return "_".join(filter(None, components)) + self.suffix # defName() def copy(self, **kwargs): @@ -105,7 +113,9 @@ def copy(self, **kwargs): kwargs.setdefault('stage', self.stage), kwargs.setdefault('stream', self.stream), kwargs.setdefault('projectVersion', self.projectVersion), + kwargs.setdefault('extraDims', self.extraDims), kwargs.setdefault('minSize', self.minSize), + kwargs.setdefault('suffix', self.suffix), return SampleInfo(**kwargs) # copy() @@ -114,6 +124,8 @@ def __str__(self): if self.stage: s += f", stage {self.stage}" if self.stream: s += f", stream {self.stream}" if self.projectVersion: s += f", project version {self.projectVersion}" + if self.extraDims: + s += f", extra constraints: {' AND '.join(self.extraDims)}" return s # __str__() @@ -154,6 +166,7 @@ def __call__(self, dims.append( DimensionQueryMaker.comparedItem('file_size', info.minSize << 20, ">=") ) + dims.extend(info.extraDims) query = " and ".join(filter(None, dims)) if len(dims) < minimum: raise RuntimeError(f"Query resulted in only {len(dims)} constraints: '{query}'") @@ -217,7 +230,7 @@ def iterateProjectVersions(self, info: "SampleInfo object defining iteration ran # expand the project versions: each version will be treated separately if info.projectVersion is None or forceSeparateVersions: - if info.stage != 'raw' or forceSeparateVersions: + if info.stage not in ( 'compressedraw', 'raw' ) or forceSeparateVersions: versionQuery = DimensionQueryMaker()(info, minimum=2) projectVersions = self._discoverProjectVersions(versionQuery) else: projectVersions = [ None ] # no version constraint is ok for raw files @@ -310,13 +323,8 @@ def __init__(self, samweb, create=False, query=False, printDefs=False, self.samweb = samweb if samweb else SAMWebClient() self.buildQuery = DimensionQueryMaker() - try: self.SAMuser = samweb.get_user() - except samexcpt.Error as e: - if self.prependUser: - logging.error("Could not find out your name! %s", e) - raise - self.SAMuser = None - # + self.SAMuser = None + self._fetchSAMuser(required=self.prependUser) # __init__() @@ -464,20 +472,16 @@ def doDeleteDef(self, info, defName=None, dim=None): ForcedMsg = { True: "forced to delete it anyway", False: "won't delete unless forced to", } checksOk = True - if not self.SAMuser: - try: self.SAMuser = self.samweb.get_user() - except samexcpt.Error as e: - logging.error("Could not find out your name! %s", e) - # if not cached already + self._fetchSAMuser() try: SAMgroup = self.samweb.get_group() except samexcpt.Error as e: logging.error("Could not find out the name of your group! %s", e) - logging.debug(f"You appear to be {SAMuser!r} of group {SAMgroup!r}") + logging.debug(f"You appear to be {self.SAMuser!r} of group {SAMgroup!r}") - if defInfo['username'] != SAMuser: + if defInfo['username'] != self.SAMuser: logging.warning( f"Definition {defName!r} was created on {defInfo['create_time']}" - f" by {defInfo['username']}/{defInfo['group']}, not by you ({SAMuser})" + f" by {defInfo['username']}/{defInfo['group']}, not by you ({self.SAMuser})" f": won't delete." ) checksOk = False @@ -571,6 +575,16 @@ def defNameCount(self, defName: "SAM definition name"): def describeSample(self, info): return "ICARUS data " + str(info) + def _fetchSAMuser(self, required=True): + if self.SAMuser: return + try: self.SAMuser = self.samweb.get_user() + except samexcpt.Error as e: + if required: + logging.error("Could not find out your name! %s", e) + raise + # try ... except + # _fetchSAMuser() + # class SampleProcessClass @@ -591,19 +605,25 @@ def collapseList(l): formatter_class=argparse.RawDescriptionHelpFormatter) SampleGroup = parser.add_argument_group(title="Sample selection") - SampleGroup.add_argument("runs", nargs="*" if AllowAllRuns else "+", type=int, - help="runs to process") + SampleGroup.add_argument("runs", nargs="*", type=int, help="runs to process") SampleGroup.add_argument("--stage", "-s", action="append", help=f"stages to include {DefaultStages}") SampleGroup.add_argument("--prjversion", "-p", action="append", help="project versions to include [autodetect (resource-intensive!)]") SampleGroup.add_argument("--stream", "-f", action="append", help="data streams to include (use 'any' for... any) [any]") + SampleGroup.add_argument("--and", "--dim", "-a", dest='dims', + action="append", default=[], + help="additional constraint (not reflected in definition name)", + ) SampleGroup.add_argument("--global", "-g", dest='globalDef', action="store_true", help="do not prepend SAM user name to definitions") SampleGroup.add_argument("--minsize", "-S", dest='minimumSize', action='store', nargs='?', default=None, const=8, type=int, help="include only files larger than this size (MiB) [8 MiB if no value]") + SampleGroup.add_argument("--knownstages", dest='printKnownStages', + action="store_true", + help="prints the stage names that have special configuration") ActionGroup = parser.add_argument_group(title="Actions") ActionGroup.add_argument("--check", action="store_true", @@ -624,6 +644,8 @@ def collapseList(l): help="sets the experiment name (for definitions) [%(default)s]") GeneralOptGroup.add_argument("--samexperiment", "-E", help="sets the experiment name (chooses SAM database) [same as --experiment]") + GeneralOptGroup.add_argument("--suffix", default='', + help="add this suffix (verbatim) to definition name (hint: start with '_')") GeneralOptGroup.add_argument("--force", "-F", action="store_true", help="skips safety checks of some operations") GeneralOptGroup.add_argument("--fake", "--dryrun", "-n", action="store_true", @@ -636,6 +658,14 @@ def collapseList(l): args = parser.parse_args() logging.getLogger().setLevel(logging.DEBUG if args.debug else logging.INFO) + if args.printKnownStages: + print(f"There are {len(StageDimensions)} known stages: {', '.join(StageDimensions)}.") + sys.exit(0) + if not args.runs and not AllowAllRuns: + raise RuntimeError("The run numbers to process are required.") + SampleGroup.add_argument("runs", nargs="*" if AllowAllRuns else "+", type=int, + help="runs to process") + if args.stage is None: args.stage = DefaultStages if args.stream: args.stream = [ None if s == "any" else s for s in args.stream ] @@ -657,7 +687,9 @@ def collapseList(l): stage=collapseList(args.stage), stream=collapseList(args.stream), # None means any projectVersion=collapseList(args.prjversion), # None for autodetect + extraDims=args.dims, minSize=args.minimumSize, + suffix=args.suffix, ) for sampleInfo in iterateSamples(baseSampleInfo):