LCOV - code coverage report
Current view: top level - triggeralgs/src - TAMakerMichelElectronAlgorithm.cpp (source / functions) Coverage Total Hit
Test: code.result Lines: 0.6 % 171 1
Test Date: 2026-02-16 10:18:04 Functions: 7.1 % 14 1

            Line data    Source code
       1              : /**
       2              :  * @file TAMakerMichelElectronAlgorithm.cpp
       3              :  *
       4              :  * This is part of the DUNE DAQ Application Framework, copyright 2021.
       5              :  * Licensing/copyright details are in the COPYING file that you should have
       6              :  * received with this code.
       7              :  */
       8              : 
       9              : #include "triggeralgs/MichelElectron/TAMakerMichelElectronAlgorithm.hpp"
      10              : #include "TRACE/trace.h"
      11              : #define TRACE_NAME "TAMakerMichelElectronAlgorithm"
      12              : #include <vector>
      13              : #include <algorithm>
      14              : 
      15              : using namespace triggeralgs;
      16              : 
      17              : using Logging::TLVL_DEBUG_MEDIUM;
      18              : 
      19              : void
      20            0 : TAMakerMichelElectronAlgorithm::process(const TriggerPrimitive& input_tp,
      21              :                                             std::vector<TriggerActivity>& output_ta)
      22              : {
      23              : 
      24              :   // The first time operator() is called, reset the window object.
      25            0 :   if (m_current_window.is_empty()) {
      26            0 :     m_current_window.reset(input_tp);
      27            0 :     m_primitive_count++;
      28            0 :     return;
      29              :   }
      30              : 
      31              :   // If the difference between the current TP's start time and the start of the window
      32              :   // is less than the specified window size, add the TP to the window.
      33            0 :   if ((input_tp.time_start - m_current_window.time_start) < m_window_length) {
      34            0 :     m_current_window.add(input_tp);
      35              :   }
      36              : 
      37              :   // Check Michel Candidate ========================================================
      38              :   // We've filled the window, now require a sufficient length track AND that the track
      39              :   // has a potential Bragg P, and then a kink.
      40            0 :   else if (longest_activity().size() > m_adjacency_threshold) {
      41              :      
      42              :      
      43              :      // We have a good length acitivity, now search for Bragg peak and kinks
      44            0 :      std::vector<TriggerPrimitive> trackHits = longest_activity();
      45              :      
      46            0 :      if (check_bragg_peak(trackHits)){
      47            0 :        if (check_kinks(trackHits)){
      48            0 :          TLOG_DEBUG(TLVL_DEBUG_MEDIUM) << "[TAM:ME] Emitting a trigger for candidate Michel event.";
      49            0 :          output_ta.push_back(construct_ta());
      50            0 :          m_current_window.reset(input_tp);
      51              :        } // Kinks 
      52              :      } // Bragg peak
      53              : 
      54            0 :   }
      55              : 
      56              :   // Otherwise, slide the window along using the current TP.
      57              :   else {
      58            0 :     m_current_window.move(input_tp, m_window_length);
      59              :   }
      60              : 
      61            0 :   m_primitive_count++;
      62            0 :   return;
      63              : }
      64              : 
      65              : void
      66            0 : TAMakerMichelElectronAlgorithm::configure(const nlohmann::json& config)
      67              : {
      68            0 :   TriggerActivityMaker::configure(config);
      69              : 
      70              :   // FIX ME: Use some schema here. Also can't work out how to pass booleans.
      71            0 :   if (config.is_object()) {
      72            0 :     if (config.contains("window_length"))
      73            0 :       m_window_length = config["window_length"];
      74            0 :     if (config.contains("adjacency_tolerance"))
      75            0 :       m_adj_tolerance = config["adjacency_tolerance"];
      76            0 :     if (config.contains("adjacency_threshold"))
      77            0 :       m_adjacency_threshold = config["adjacency_threshold"];
      78              :   }
      79              : 
      80            0 : }
      81              : 
      82              : TriggerActivity
      83            0 : TAMakerMichelElectronAlgorithm::construct_ta() const
      84              : {
      85              : 
      86            0 :   TriggerPrimitive latest_tp_in_window = m_current_window.inputs.back();
      87              : 
      88            0 :   TriggerActivity ta;
      89            0 :   ta.time_start = m_current_window.time_start;
      90            0 :   ta.time_end = latest_tp_in_window.time_start + latest_tp_in_window.samples_over_threshold * 32;  // FIXME: Replace the hard-coded SOT to TOT scaling.
      91            0 :   ta.time_peak = latest_tp_in_window.samples_to_peak * 32 + latest_tp_in_window.time_start;  // FIXME: Replace STP to `time_peak` conversion.
      92            0 :   ta.time_activity = ta.time_peak;
      93            0 :   ta.channel_start = latest_tp_in_window.channel;
      94            0 :   ta.channel_end = latest_tp_in_window.channel;
      95            0 :   ta.channel_peak = latest_tp_in_window.channel;
      96            0 :   ta.adc_integral = m_current_window.adc_integral;
      97            0 :   ta.adc_peak = latest_tp_in_window.adc_peak;
      98            0 :   ta.detid = latest_tp_in_window.detid;
      99            0 :   ta.type = TriggerActivity::Type::kTPC;
     100            0 :   ta.algorithm = TriggerActivity::Algorithm::kMichelElectron;
     101            0 :   ta.inputs = m_current_window.inputs;
     102              : 
     103            0 :   return ta;
     104            0 : }
     105              : 
     106              : std::vector<TriggerPrimitive>
     107            0 : TAMakerMichelElectronAlgorithm::longest_activity() const
     108              : {
     109              :   // This function attempts to return a vector of hits that correspond to the longest
     110              :   // piece of activity in the current window. The logic follows that from the HMA
     111              :   // check_adjacency() function and further details can be found there.
     112            0 :   std::vector<TriggerPrimitive> trackHits;
     113            0 :   std::vector<TriggerPrimitive> finalHits; // The vector of track hits, which we return
     114              : 
     115            0 :   uint16_t adj = 1;              // Initialise adjacency, 1 for the first wire.
     116            0 :   uint16_t max = 0;
     117            0 :   unsigned int channel = 0;      // Current channel ID
     118            0 :   unsigned int next_channel = 0; // Next channel ID
     119            0 :   unsigned int next = 0;         // The next position in the hit channels vector
     120            0 :   unsigned int tol_count = 0;    // Tolerance count, should not pass adj_tolerance
     121              : 
     122              :   // Generate a channelID ordered list of hit channels for this window
     123            0 :   std::vector<TriggerPrimitive> hitList;
     124            0 :   for (auto tp : m_current_window.inputs) {
     125            0 :     hitList.push_back(tp);
     126              :   }
     127            0 :   std::sort(hitList.begin(), hitList.end(), [](TriggerPrimitive a, TriggerPrimitive b)
     128            0 :             { return a.channel < b.channel; });
     129              : 
     130              :   // ADAJACENCY LOGIC ====================================================================
     131              :   // =====================================================================================
     132              :   // Adjcancency Tolerance = Number of times prepared to skip missed hits before resetting 
     133              :   // the adjacency count. This accounts for things like dead channels / missed TPs. The 
     134              :   // maximum gap is 4 which comes from tuning on December 2021 coldbox data, and June 2022 
     135              :   // coldbox runs.
     136            0 :   for (int i = 0; i < hitList.size(); ++i) {
     137              : 
     138            0 :     next = (i + 1) % hitList.size(); // Loops back when outside of channel list range
     139            0 :     channel = hitList.at(i).channel;
     140            0 :     next_channel = hitList.at(next).channel; // Next channel with a hit
     141              : 
     142            0 :     if (trackHits.size() == 0 ){ trackHits.push_back(hitList.at(i)); }
     143              : 
     144              :     // End of vector condition.
     145            0 :     if (next_channel == 0) { next_channel = channel - 1; }
     146              : 
     147              :     // Skip same channel hits for adjacency counting, but add to the track!
     148            0 :     if (next_channel == channel) { 
     149            0 :       trackHits.push_back(hitList.at(next));
     150            0 :       continue; }
     151              : 
     152              :     // If next hit is on next channel, increment the adjacency count.
     153            0 :     else if (next_channel == channel + 1){ 
     154            0 :       trackHits.push_back(hitList.at(next));
     155            0 :       ++adj; }
     156              : 
     157              :     // If next channel is not on the next hit, but the 'second next', increase adjacency 
     158              :     // but also tally up with the tolerance counter.
     159            0 :     else if (((next_channel == channel + 2) || (next_channel == channel + 3) ||
     160            0 :               (next_channel == channel + 4) || (next_channel == channel + 5))
     161            0 :              && (tol_count < m_adj_tolerance)) {
     162            0 :       trackHits.push_back(hitList.at(next));
     163            0 :       ++adj;
     164            0 :       for (int i = 0 ; i < next_channel-channel ; ++i){ ++tol_count; }
     165              :     }
     166              : 
     167              :     // If next hit isn't within reach, end the adjacency count and check for a new max.
     168              :     // Reset variables for next iteration.
     169              :     else {
     170            0 :       if (adj > max) { 
     171            0 :         max = adj;
     172            0 :         finalHits.clear(); // Clear previous track
     173            0 :         for (auto h : trackHits){
     174            0 :           finalHits.push_back(h);
     175              :         }  
     176              :       }
     177            0 :       adj = 1;
     178            0 :       tol_count = 0;
     179            0 :       trackHits.clear();
     180              :     }
     181              :   }
     182              : 
     183            0 :   return finalHits;
     184            0 : }
     185              : 
     186              : 
     187              : // Function that tries to identify a Bragg peak via a running
     188              : // mean average of the ADC values. We use the running mean as it's less susceptible to
     189              : // spikes of activity that might trick the algorithm. We establish a baseline, then
     190              : // count up clusters of charge deposition above that baseline. If the largest is at
     191              : // one of the ends of that collection, signal a potential Bragg peak.
     192              : bool
     193            0 : TAMakerMichelElectronAlgorithm::check_bragg_peak(std::vector<TriggerPrimitive> trackHits)
     194              : {
     195            0 :   bool bragg = false; 
     196            0 :   std::vector<float> adc_means_list;
     197            0 :   uint16_t convolve_value = 6;
     198              : 
     199              :   // Loop over hits that correspond to high adjacency activity
     200            0 :   for (uint16_t i = 0; i < trackHits.size(); ++i){
     201            0 :     float adc_sum = 0;
     202              :     float adc_mean = 0;
     203              : 
     204              :     // Calculate running ADC mean of this track 
     205            0 :     for (uint16_t j = i; j < i+convolve_value; ++j){
     206            0 :        int hit = (j) % trackHits.size(); 
     207            0 :        adc_sum += trackHits.at(hit).adc_integral;
     208              :     }
     209              : 
     210            0 :     adc_mean = adc_sum / convolve_value;
     211            0 :     adc_means_list.push_back(adc_mean);
     212            0 :     adc_sum = 0;
     213              :   } 
     214              : 
     215              :   // We now have a list of convolved adc means. 
     216            0 :   float ped = std::accumulate(adc_means_list.begin(), adc_means_list.end(), 0.0) / adc_means_list.size();
     217            0 :   float charge = 0;
     218            0 :   std::vector<float> charge_dumps;
     219              : 
     220              :   // Now go through the list, picking up clusters of charge above the baseline/ped
     221            0 :   for (auto a : adc_means_list){
     222            0 :     if (a > ped){
     223            0 :        charge += a;
     224              :     }
     225            0 :     else if( a < ped && charge !=0 ){
     226            0 :      charge_dumps.push_back(charge);
     227            0 :      charge = 0; 
     228              :     } 
     229              :   } 
     230              : 
     231              :   // If the maximum of that list of charge dumps is near(at?) either end of it
     232            0 :   float max_charge = *max_element(charge_dumps.begin(), charge_dumps.end()); 
     233            0 :   if(max_charge == charge_dumps.front() || max_charge == charge_dumps.back()){ bragg=true; }
     234              : 
     235            0 :   return bragg;
     236            0 :  }
     237              : 
     238              : bool
     239            0 : TAMakerMichelElectronAlgorithm::check_kinks(std::vector<TriggerPrimitive> finalHits)
     240              : {
     241            0 :     bool kinks = false;  // We actually required two kinks in the coldbox, the michel kink and the wes kink
     242            0 :     std::vector<float> runningGradient;
     243            0 :     std::vector<float> runningMeanGradient;
     244              : 
     245              :     // Choice to be made here. Do we want to scane in collection (z) or time (x) direction when calculating gradient between hits. I
     246              :     // would say if we have already made the request to pass a track of length specific threshold which is longer than the drift
     247              :     // direction for the coldbox, it makes sense to scan across channels a little more.
     248            0 :     std::sort(finalHits.begin(), finalHits.end(), [](TriggerPrimitive a, TriggerPrimitive b) { return a.channel < b.channel; });
     249              : 
     250              :     // Populate the runningGradient with the track hits. Do this between ith and i+kth TPs, to small scale fluctuations of the track
     251              :     // Yet k should be kept small, so that enough gradient information is preserved at the end of the track to identify kinks
     252            0 :     for (int i=0 ; i < finalHits.size()-2; i++){
     253              :    
     254              :       // Skip same channel hits or if the start times are the same - no div by zero! 
     255            0 :       if (finalHits.at(i+2).channel == finalHits.at(i).channel || (finalHits.at(i+2).time_start == finalHits.at(i).time_start) ) { continue; }
     256              : 
     257              :       // Check that the next TP is closeby; enough in space and time directions so as to avoid obtaining a gradient value from
     258              :       // same channel hits at large time difference or vice versa due to kink topology or showers. Clearly we shouldn't be very far in 
     259              :       // channel number, but since we might later try to do this check in the time direction, leave the condition in.
     260            0 :       int diff = finalHits.at(i+2).time_start - finalHits.at(i).time_start;
     261            0 :       if((std::abs(diff) > 1000) || ((std::abs(channel_diff_t(finalHits.at(i+2).channel) - channel_diff_t(finalHits.at(i).channel)) > 6))) { continue; } 
     262              : 
     263              :       // Gradient is just change in z (collection) over change in x (drift). x is admitedly roughly converted from
     264              :       // hit start time, but I don't think diffusion effects are a huge concern over 20cm. Using mm for readability/visualisation 
     265            0 :       float dz = (finalHits.at(i+2).channel - finalHits.at(i).channel)*4.67; // Change in collection wire z to separation in mm
     266            0 :       long long int dt = finalHits.at(i+2).time_start - finalHits.at(i).time_start;
     267            0 :       float dx = dt*0.028; // Change time to separation in x mm
     268            0 :       float g = dz/dx;
     269              : 
     270            0 :       runningGradient.push_back(g); 
     271              :     }
     272              :  
     273              :     // Require a decent length of the gradients vector. Otherwise some adjacent events are showers and the conditions above mean
     274              :     // we don't get enough entries. In essence, this provides some confidence that it's track-like rather than shower-like. Which
     275              :     // is what we want for a Michel event.
     276            0 :     if ( runningGradient.size() > 10 ){
     277              :       
     278              :       // Now lets take a running mean of the gradients between TPs, less susceptible to wild changes due to deltas/etc
     279            0 :       for(int g=0 ; g < runningGradient.size()-1 ; g++){
     280            0 :         float gsum = runningGradient.at(g) + runningGradient.at(g+1);
     281            0 :         runningMeanGradient.push_back(gsum/2);
     282              :       } 
     283              :    
     284              :       // We have a list of gradients, now just demand that the two ends have gradients that differ significantly
     285              :       // from the mean gradient of the activity at both ends. This aims to pick out wesKinks and michelKinks
     286            0 :       if(runningMeanGradient.size() > 10 ){
     287              : 
     288            0 :         float mean = (std::abs(std::accumulate(runningMeanGradient.begin(), runningMeanGradient.end(), 0.0)))/(runningMeanGradient.size());
     289              : 
     290              :         // If you're testing on simulation or december data, you won't see the wes kink, so use an || instead of &&
     291            0 :         if((std::abs(runningMeanGradient.front()) + mean > 2.5*mean)  ||  ((std::abs(runningMeanGradient.back() + mean)) > 2.5*mean)){ kinks=true; }   
     292              :       } 
     293              :     } 
     294              : 
     295            0 :   return kinks;
     296            0 : }
     297              : 
     298              : // ===============================================================================================
     299              : // ===============================================================================================
     300              : // Functions below this line are for debugging purposes.
     301              : // ===============================================================================================
     302              : 
     303              : void
     304            0 : TAMakerMichelElectronAlgorithm::add_window_to_record(Window window)
     305              : {
     306            0 :   m_window_record.push_back(window);
     307            0 :   return;
     308              : }
     309              : 
     310              : 
     311              : // Function to dump the details of the TA window currently on record
     312              : void
     313            0 : TAMakerMichelElectronAlgorithm::dump_window_record()
     314              : {
     315              :   // FIX ME: Need to index this outfile in the name by detid or something similar.
     316            0 :   std::ofstream outfile;
     317            0 :   outfile.open("window_record_tam.csv", std::ios_base::app);
     318              : 
     319            0 :   for (auto window : m_window_record) {
     320            0 :     outfile << window.time_start << ",";
     321            0 :     outfile << window.inputs.back().time_start << ",";
     322            0 :     outfile << window.inputs.back().time_start - window.time_start << ","; // window_length - from TP start times
     323            0 :     outfile << window.adc_integral << ",";
     324            0 :     outfile << window.n_channels_hit() << ",";       // Number of unique channels with hits
     325            0 :     outfile << window.inputs.size() << ",";          // Number of TPs in Window
     326            0 :     outfile << window.inputs.back().channel << ",";  // Last TP Channel ID
     327            0 :     outfile << window.inputs.front().channel << ","; // First TP Channel ID
     328            0 :     outfile << longest_activity().size() << std::endl;             // New adjacency value for the window
     329            0 :   }
     330              : 
     331            0 :   outfile.close();
     332              : 
     333            0 :   m_window_record.clear();
     334              : 
     335            0 :   return;
     336            0 : }
     337              : 
     338              : // Function to add current TP details to a text file for testing and debugging.
     339              : void
     340            0 : TAMakerMichelElectronAlgorithm::dump_tp(TriggerPrimitive const& input_tp)
     341              : {
     342            0 :   std::ofstream outfile;
     343            0 :   outfile.open("coldbox_tps.txt", std::ios_base::app);
     344              : 
     345              :   // Output relevant TP information to file
     346            0 :   outfile << input_tp.time_start << " ";          // Start time of TP
     347            0 :   outfile << input_tp.samples_over_threshold << " "; // in multiples of 25
     348            0 :   outfile << input_tp.samples_to_peak << " ";           //
     349            0 :   outfile << input_tp.channel << " ";             // Offline channel ID
     350            0 :   outfile << input_tp.adc_integral << " ";        // ADC Sum
     351            0 :   outfile << input_tp.adc_peak << " ";            // ADC Peak Value
     352            0 :   outfile << input_tp.detid << " ";               // Det ID - Identifies detector element, APA or PDS part etc...
     353            0 :   outfile.close();
     354              : 
     355            0 :   return;
     356            0 : }
     357              : 
     358              : /*
     359              : void
     360              : TAMakerMichelElectronAlgorithm::flush(timestamp_t, std::vector<TriggerActivity>& output_ta)
     361              : {
     362              :   // Check the status of the current window, construct TA if conditions are met. Regardless
     363              :   // of whether the conditions are met, reset the window.
     364              :   if(m_current_window.adc_integral > m_adc_threshold && m_trigger_on_adc){
     365              :   //else if(m_current_window.adc_integral > m_conf.adc_threshold && m_conf.trigger_on_adc){
     366              :     //TLOG_DEBUG(TRACE_NAME) << "ADC integral in window is greater than specified threshold.";
     367              :     output_ta.push_back(construct_ta());
     368              :   }
     369              :   else if(m_current_window.n_channels_hit() > m_n_channels_threshold && m_trigger_on_n_channels){
     370              :   //else if(m_current_window.n_channels_hit() > m_conf.n_channels_threshold && m_conf.trigger_on_n_channels){
     371              :     //TLOG_DEBUG(TRACE_NAME) << "Number of channels hit in the window is greater than specified threshold.";
     372              :     output_ta.push_back(construct_ta());
     373              :   }
     374              : 
     375              :   //TLOG_DEBUG(TRACE_NAME) << "Clearing the current window, on the arrival of the next input_tp, the window will be
     376              : reset."; m_current_window.clear();
     377              : 
     378              :   return;
     379              : }*/
     380              : 
     381              : // Register algo in TA Factory
     382           12 : REGISTER_TRIGGER_ACTIVITY_MAKER(TRACE_NAME, TAMakerMichelElectronAlgorithm)
        

Generated by: LCOV version 2.0-1