Analysis-Utilities 26.9.9
C++/ROOT utilities for nuclear measurement data analysis
Loading...
Searching...
No Matches
WaveformProcessingUtils.cpp
Go to the documentation of this file.
2#include "IOUtils.hpp"
3#include <TArrayS.h>
4
7
9 const FileProcessingConfig &config)
10 : polarity_(config.polarity), trigger_threshold_(config.trigger_threshold),
11 num_samples_baseline_(config.num_samples_baseline),
12 pre_samples_(config.pre_samples), post_samples_(config.post_samples),
13 pre_gate_(config.pre_gate), short_gate_(config.short_gate),
14 long_gate_(config.long_gate), max_events_(config.max_events),
15 verbose_(config.verbose),
16 adc_saturation_code_(config.adc_saturation_code),
17 sample_waveforms_to_save_(config.sample_waveforms_to_save),
18 sample_waveforms_saved_(0), output_file_(nullptr), output_tree_(nullptr),
19 store_waveforms_(config.store_waveforms), save_waveform_(new TArrayF()),
20 input_format_(config.input_format) {}
21
23 if (output_file_) {
24 if (output_file_->IsOpen()) {
25 output_file_->Close();
26 }
27 delete output_file_;
28 output_file_ = nullptr;
29 if (store_waveforms_) {
30 save_waveform_ = nullptr;
31 }
32 }
33 delete save_waveform_;
34 save_waveform_ = nullptr;
35}
36
37Bool_t WaveformProcessingUtils::ProcessWaveform(const TArrayI &samples) {
38 Int_t n = samples.GetSize();
39
40 if (n == 0)
41 return kFALSE;
42
43 Int_t raw_max = samples.At(0);
44
45 if (polarity_ == 1) {
46 for (Int_t i = 1; i < n; ++i) {
47 if (samples[i] > raw_max)
48 raw_max = samples[i];
49 }
50 } else {
51 for (Int_t i = 1; i < n; ++i) {
52 if (samples[i] < raw_max)
53 raw_max = samples[i];
54 }
55 }
56
57 SubtractBaseline(samples);
58
59 Float_t trigger_pos = FindTrigger(*save_waveform_);
60 if (trigger_pos < 0) {
61 stats_.rejected_no_trigger++;
62 return kFALSE;
63 }
64
65 if (trigger_pos < pre_samples_ ||
66 (save_waveform_->GetSize() - trigger_pos) <= post_samples_) {
67 stats_.rejected_insufficient_samples++;
68 return kFALSE;
69 }
70
71 CropWaveform(*save_waveform_, trigger_pos);
72
73 WaveformFeatures features = ExtractFeatures(*save_waveform_);
74 features.raw_pulse_height = std::abs(raw_max);
75 features.trigger_position = FindTrigger(*save_waveform_);
76 Bool_t passes_cuts = ApplyQualityCuts(features);
77 features.passes_cuts = passes_cuts;
78
79 if (!passes_cuts) {
80 return kFALSE;
81 }
82
83 if (sample_waveforms_saved_ < sample_waveforms_to_save_) {
84 SaveSampleWaveform(*save_waveform_);
85 }
86
87 current_features_ = features;
88 output_tree_->Fill();
89
90 stats_.accepted++;
91 if (current_baseline_rms_valid_) {
92 stats_.sum_baseline_rms_accepted += current_baseline_rms_;
93 stats_.baseline_rms_count_accepted++;
94 }
95 return kTRUE;
96}
97
98std::mutex WaveformProcessingUtils::canvas_mutex_;
99
100void WaveformProcessingUtils::SaveSampleWaveform(const TArrayF &waveform) {
101
102 std::lock_guard<std::mutex> lock(canvas_mutex_);
103
104 Int_t n = waveform.GetSize();
105 const Float_t *arr = waveform.GetArray();
106 std::vector<Double_t> x(n), y(n);
107 for (Int_t i = 0; i < n; ++i) {
108 x[i] = i;
109 y[i] = arr[i];
110 }
111
113
114 TGraph *graph = new TGraph(n, x.data(), y.data());
115 TCanvas *canvas = PlottingUtils::GetConfiguredCanvas(kFALSE);
116
117 PlottingUtils::ConfigureGraph(graph, kBlue + 1, ";Sample;Amplitude [ADC]");
118 graph->Draw("AL");
119
120 TString output_name = Form("%s_waveform_%04d", current_output_name_.Data(),
121 sample_waveforms_saved_);
122
123 PlottingUtils::SaveFigure(canvas, output_name, "samplewaveforms",
125 delete graph;
126 delete canvas;
127
128 sample_waveforms_saved_++;
129}
130
131void WaveformProcessingUtils::SubtractBaseline(const TArrayI &samples) {
132 Int_t n = samples.GetSize();
133
134 Float_t baseline = 0;
135 Int_t baseline_samples = TMath::Min(num_samples_baseline_, n);
136 for (Int_t i = 0; i < baseline_samples; ++i) {
137 baseline += samples.GetAt(i);
138 }
139 baseline /= baseline_samples;
140
141 current_baseline_rms_valid_ = kFALSE;
142 if (baseline_samples > 1) {
143 Double_t sum_sq = 0.0;
144 for (Int_t i = 0; i < baseline_samples; ++i) {
145 Double_t d = samples.GetAt(i) - baseline;
146 sum_sq += d * d;
147 }
148 Float_t rms = TMath::Sqrt(sum_sq / (baseline_samples - 1));
149 stats_.sum_baseline_rms += rms;
150 stats_.baseline_rms_count++;
151 current_baseline_rms_ = rms;
152 current_baseline_rms_valid_ = kTRUE;
153 }
154
155 save_waveform_->Set(n);
156 if (polarity_ == -1) {
157 for (Int_t i = 0; i < n; ++i) {
158 save_waveform_->SetAt(baseline - samples.GetAt(i), i);
159 }
160 } else {
161 for (Int_t i = 0; i < n; ++i) {
162 save_waveform_->SetAt(samples.GetAt(i) - baseline, i);
163 }
164 }
165}
166
167Float_t WaveformProcessingUtils::FindTrigger(const TArrayF &waveform) {
168 Int_t n = waveform.GetSize();
169 const Float_t *arr = waveform.GetArray();
170
171 Float_t peak_value = *std::max_element(arr, arr + n);
172 Float_t trigger_level = peak_value * trigger_threshold_;
173
174 for (Int_t i = 0; i < n; ++i) {
175 if (arr[i] >= trigger_level) {
176 return Float_t(i);
177 }
178 }
179
180 return -1.0;
181}
182
183void WaveformProcessingUtils::CropWaveform(const TArrayF &waveform,
184 Int_t trigger_pos) {
185 Int_t start = trigger_pos - pre_samples_;
186 Int_t end = TMath::Min(trigger_pos + post_samples_, waveform.GetSize());
187 Int_t crop_size = end - start;
188
189 TArrayF cropped(crop_size);
190 const Float_t *src = waveform.GetArray();
191 for (Int_t i = 0; i < crop_size; ++i) {
192 cropped[i] = src[start + i];
193 }
194
195 *save_waveform_ = cropped;
196}
197
200 WaveformFeatures features;
201 Int_t integration_start = pre_samples_ - pre_gate_;
202
203 Int_t n = cropped_wf.GetSize();
204 const Float_t *arr = cropped_wf.GetArray();
205
206 const Float_t *max_it = std::max_element(arr, arr + n);
207 features.pulse_height = *max_it;
208 features.peak_position = std::distance(arr, max_it);
209
210 features.short_integral = 0;
211 features.long_integral = 0;
212
213 Int_t negative_samples = 0;
214 Int_t short_end = TMath::Min(integration_start + short_gate_, n);
215 Int_t long_end = TMath::Min(integration_start + long_gate_, n);
216
217 for (Int_t i = integration_start; i < long_end; ++i) {
218 Float_t sample_value = arr[i];
219 features.long_integral += sample_value;
220 if (i < short_end) {
221 features.short_integral += sample_value;
222 }
223 if (sample_value < 0)
224 negative_samples++;
225 }
226 features.timestamp = current_timestamp_;
227
228 features.passes_cuts = kTRUE;
229 features.negative_fraction =
230 Float_t(negative_samples) / Float_t(long_end - integration_start);
231
232 return features;
233}
234
235Bool_t
237
238 if (((features.raw_pulse_height == adc_saturation_code_) &&
239 (polarity_ == 1)) ||
240 ((features.raw_pulse_height == 0) && (polarity_ == -1))) {
241 stats_.rejected_clipped++;
242 return kFALSE;
243 }
244
245 if (features.negative_fraction > 0.50) {
246 stats_.rejected_baseline++;
247 return kFALSE;
248 }
249
250 if (features.long_integral <= 0) {
251 stats_.rejected_negative_integral++;
252 return kFALSE;
253 }
254
255 return kTRUE;
256}
257
259 std::cout << "Waveform processing statistics..." << std::endl;
260 std::cout << "Total processed: " << stats_.total_processed << std::endl;
261 std::cout << std::endl;
262 std::cout << "Accepted: " << stats_.accepted << std::endl;
263 std::cout << std::endl;
264 std::cout << "Rejected no trigger: " << stats_.rejected_no_trigger
265 << std::endl;
266 std::cout << "Rejected clipped ADC: " << stats_.rejected_clipped << std::endl;
267 std::cout << "Rejected insufficient samples: "
268 << stats_.rejected_insufficient_samples << std::endl;
269 std::cout << "Rejected negative integral: "
270 << stats_.rejected_negative_integral << std::endl;
271 std::cout << "Rejected bad baseline: " << stats_.rejected_baseline
272 << std::endl;
273 std::cout << std::endl;
274
275 if (stats_.total_processed > 0) {
276 std::cout << "Acceptance rate: "
277 << 100 * Float_t(stats_.accepted) /
278 Float_t(stats_.total_processed)
279 << "%" << std::endl;
280 }
281 if (stats_.baseline_rms_count > 0) {
282 std::cout << "Mean baseline RMS (all processed): "
283 << stats_.sum_baseline_rms / stats_.baseline_rms_count
284 << " ADC counts (over " << stats_.baseline_rms_count
285 << " waveforms)" << std::endl;
286 }
287 if (stats_.baseline_rms_count_accepted > 0) {
288 std::cout << "Mean baseline RMS (accepted only): "
289 << stats_.sum_baseline_rms_accepted /
290 stats_.baseline_rms_count_accepted
291 << " ADC counts (over " << stats_.baseline_rms_count_accepted
292 << " waveforms)" << std::endl;
293 }
294 std::cout << std::endl;
295
296 TString stats_path =
297 IO::GetRootFilesBaseDir() + "/" + current_output_name_ + ".stats";
298 std::ofstream stats_file(stats_path.Data(), std::ios::app);
299 if (stats_file.is_open()) {
300 stats_file << "Waveform processing statistics..." << std::endl;
301 stats_file << "Total processed: " << stats_.total_processed << std::endl;
302 stats_file << std::endl;
303 stats_file << "Accepted: " << stats_.accepted << std::endl;
304 stats_file << std::endl;
305 stats_file << "Rejected no trigger: " << stats_.rejected_no_trigger
306 << std::endl;
307 stats_file << "Rejected clipped ADC: " << stats_.rejected_clipped
308 << std::endl;
309 stats_file << "Rejected insufficient samples: "
310 << stats_.rejected_insufficient_samples << std::endl;
311 stats_file << "Rejected negative integral: "
312 << stats_.rejected_negative_integral << std::endl;
313 stats_file << "Rejected bad baseline: " << stats_.rejected_baseline
314 << std::endl;
315 stats_file << std::endl;
316
317 if (stats_.total_processed > 0) {
318 stats_file << "Acceptance rate: "
319 << 100 * Float_t(stats_.accepted) /
320 Float_t(stats_.total_processed)
321 << "%" << std::endl;
322 }
323 if (stats_.baseline_rms_count > 0) {
324 stats_file << "Mean baseline RMS (all processed): "
325 << stats_.sum_baseline_rms / stats_.baseline_rms_count
326 << " ADC counts (over " << stats_.baseline_rms_count
327 << " waveforms)" << std::endl;
328 }
329 if (stats_.baseline_rms_count_accepted > 0) {
330 stats_file << "Mean baseline RMS (accepted only): "
331 << stats_.sum_baseline_rms_accepted /
332 stats_.baseline_rms_count_accepted
333 << " ADC counts (over " << stats_.baseline_rms_count_accepted
334 << " waveforms)" << std::endl;
335 }
336 stats_file << std::endl;
337 }
338}
339
340Bool_t WaveformProcessingUtils::ProcessFile(const TString filepath,
341 const TString output_name) {
342 current_output_name_ = output_name;
343 sample_waveforms_saved_ = 0;
344 if (!save_waveform_) {
345 save_waveform_ = new TArrayF();
346 }
347
348 const TString base_dir = IO::GetRootFilesBaseDir();
349 if (gSystem->AccessPathName(base_dir)) {
350 gSystem->mkdir(base_dir, kTRUE);
351 }
352
353 // clear file
354 TString clear_path = base_dir + "/" + output_name + ".stats";
355 std::ofstream(clear_path.Data(), std::ios::trunc);
356
357 TString output_subpath = output_name + ".root";
358 TString output_filename = base_dir + "/" + output_subpath;
359 output_file_ = IO::OpenForWriting(output_subpath);
360 if (!output_file_ || output_file_->IsZombie()) {
361 std::cout << "ERROR: Could not create output file " << output_filename
362 << std::endl;
363 return kFALSE;
364 }
365
366 output_tree_ = new TTree("features", "Waveform Features");
367
368 output_tree_->Branch("pulse_height", &current_features_.pulse_height,
369 "pulse_height/F");
370 output_tree_->Branch("trigger_position", &current_features_.trigger_position,
371 "trigger_position/I");
372 output_tree_->Branch("short_integral", &current_features_.short_integral,
373 "short_integral/F");
374 output_tree_->Branch("long_integral", &current_features_.long_integral,
375 "long_integral/F");
376 output_tree_->Branch("timestamp", &current_features_.timestamp,
377 "timestamp/l");
378
379 if (store_waveforms_) {
380 output_tree_->Branch("Samples", &save_waveform_);
381 std::cout << "Storing events that pass cuts." << std::endl;
382 }
383
384 TFile *file = TFile::Open(filepath, "READ");
385 if (!file || file->IsZombie()) {
386 std::cout << "ERROR opening file: " << filepath << std::endl;
387 return kFALSE;
388 }
389
390 TTree *tree = static_cast<TTree *>(file->Get("Data_R"));
391 if (!tree) {
392 std::cout << "ERROR: TTree 'Data_R' not found in " << filepath << std::endl;
393 file->Close();
394 return kFALSE;
395 }
396
397 TArrayS *samples = nullptr;
398 TArrayI *solaris_trace0 = nullptr;
399 UInt_t trigger_time_tag = 0;
400 TArrayI converted_samples;
401
402 if (input_format_ == InputFormat::kCOMPASS) {
403 samples = new TArrayS();
404 tree->SetBranchAddress("Samples", &samples);
405 tree->SetBranchAddress("Timestamp", &current_timestamp_);
406 } else if (input_format_ == InputFormat::kWAVEDUMP) {
407 samples = new TArrayS();
408 tree->SetBranchAddress("Samples", &samples);
409 tree->SetBranchAddress("TriggerTimeTag", &trigger_time_tag);
410 } else if (input_format_ == InputFormat::kSOLARIS) {
411 solaris_trace0 = new TArrayI();
412 tree->SetBranchAddress("Trace0", &solaris_trace0);
413 tree->SetBranchAddress("Timestamp", &current_timestamp_);
414 }
415
416 Long64_t n_entries = tree->GetEntries();
417
418 for (Long64_t entry = 0; entry < n_entries; ++entry) {
419 if (max_events_ > 0 && stats_.accepted >= max_events_) {
420 break;
421 }
422 tree->GetEntry(entry);
423 stats_.total_processed++;
424
425 if (input_format_ == InputFormat::kCOMPASS) {
426 Int_t n = samples->GetSize();
427 converted_samples.Set(n);
428 for (Int_t i = 0; i < n; ++i) {
429 converted_samples.SetAt(samples->At(i), i);
430 }
431 ProcessWaveform(converted_samples);
432 } else if (input_format_ == InputFormat::kWAVEDUMP) {
433 current_timestamp_ = static_cast<ULong64_t>(trigger_time_tag);
434 Int_t n = samples->GetSize();
435 converted_samples.Set(n);
436 for (Int_t i = 0; i < n; ++i) {
437 converted_samples.SetAt(samples->At(i), i);
438 }
439 ProcessWaveform(converted_samples);
440 } else if (input_format_ == InputFormat::kSOLARIS) {
441 ProcessWaveform(*solaris_trace0);
442 }
443 }
444
445 delete samples;
446 delete solaris_trace0;
447 file->Close();
448 delete file;
449
450 output_file_->cd();
451 output_tree_->Write("", TObject::kOverwrite);
452 output_file_->Close();
453 delete output_file_;
454 output_file_ = nullptr;
455 output_tree_ = nullptr;
456 if (store_waveforms_) {
457 save_waveform_ = nullptr;
458 }
459
460 if (verbose_) {
462 }
463
464 return kTRUE;
465}
466
468 const std::vector<TString> &filepaths,
469 const std::vector<TString> &output_names,
470 const FileProcessingConfig &config, Int_t max_workers) {
471
472 ROOT::EnableThreadSafety();
473 IO::SetThreadSafe(kTRUE);
474
475 Int_t n_files = Int_t(filepaths.size());
476 Int_t n_workers = max_workers > 0
477 ? max_workers
478 : Int_t(std::thread::hardware_concurrency());
479 n_workers = TMath::Min(n_workers, n_files);
480
481 std::cout << "Processing " << n_files << " files with " << n_workers
482 << " workers." << std::endl;
483
484 std::function<Bool_t(const TString &, const TString &)> process_one =
485 [&config](const TString &filepath, const TString &output_name) -> Bool_t {
486 WaveformProcessingUtils *processor = new WaveformProcessingUtils(config);
487 Bool_t result = processor->ProcessFile(filepath, output_name);
488 delete processor;
489 return result;
490 };
491
492 for (Int_t i = 0; i < n_files; i += n_workers) {
493 std::vector<std::future<Bool_t>> futures;
494 Int_t batch_end = TMath::Min(i + n_workers, n_files);
495
496 for (Int_t j = i; j < batch_end; ++j) {
497 futures.push_back(std::async(std::launch::async, process_one,
498 std::cref(filepaths[j]),
499 std::cref(output_names[j])));
500 }
501
502 for (size_t j = 0; j < futures.size(); ++j) {
503 Bool_t result = futures[j].get();
504 std::cout << "Finished: " << output_names[i + j]
505 << (result ? " [OK]" : " [FAILED]") << std::endl;
506 }
507 }
508}
@ kLINEAR
Linear y only.
@ kPNG
PNG raster output; line width 2.
@ kWAVEDUMP
CAEN WaveDump (DT5742 family).
@ kSOLARIS
SOLARIS DAQ (SOL).
@ kCOMPASS
CAEN CoMPASS.
static void SaveFigure(TCanvas *canvas, TString output_filename, TString output_subdirectory="", PlotSaveOptions save_options=PlotSaveOptions::kBOTH)
Write a canvas to disk under the configured plots base directory.
static void ConfigureGraph(TGraph *graph, Int_t color, const TString title="")
Apply the house style to a graph without drawing it.
static void SetStylePreferences(PlotSaveFormat save_format=PlotSaveFormat::kPNG)
Install the global ROOT style and choose the output format.
static TCanvas * GetConfiguredCanvas(Bool_t logy=kFALSE)
Create a 1200x800 canvas with grid and ticks already set up.
Bool_t ApplyQualityCuts(const WaveformFeatures &features)
Decide whether a waveform survives the quality cuts.
static void ProcessFilesParallel(const std::vector< TString > &filepaths, const std::vector< TString > &output_names, const FileProcessingConfig &config, Int_t max_workers=4)
Process many files concurrently, one instance per worker.
void PrintAllStatistics() const
Print the run statistics, including mean baseline RMS.
Bool_t ProcessFile(const TString filepath, const TString output_name)
Process one input file end to end.
WaveformProcessingUtils()
Construct with the FileProcessingConfig defaults.
Bool_t ProcessWaveform(const TArrayI &samples)
Run one raw waveform through the whole pipeline.
~WaveformProcessingUtils()
Closes any open output file and releases the internal buffer.
WaveformFeatures ExtractFeatures(const TArrayF &cropped_wf)
Compute the per-waveform features of a cropped waveform.
void CropWaveform(const TArrayF &waveform, Int_t trigger_pos)
Cut the region of interest around the trigger.
void SaveSampleWaveform(const TArrayF &waveform)
Write one waveform out as a figure for visual inspection.
Float_t FindTrigger(const TArrayF &waveform)
Find the first sample crossing the fractional trigger level.
void SubtractBaseline(const TArrayI &samples)
Estimate and remove the baseline, writing to the internal buffer.
TString GetRootFilesBaseDir()
Current base directory for relative subpaths, without trailing slash.
Definition IOUtils.cpp:37
void SetThreadSafe(Bool_t enabled=kTRUE)
Enable ROOT thread safety and serialise file opening.
Definition IOUtils.cpp:39
TFile * OpenForWriting(const TString &subpath, const TString mode="RECREATE")
Open a ROOT file for writing, creating parent directories first.
Definition IOUtils.cpp:68
Everything needed to configure one processing run.
Per-waveform quantities extracted by WaveformProcessingUtils::ExtractFeatures().
Int_t peak_position
Index of that maximum within the cropped waveform, in samples.
Int_t trigger_position
Trigger index re-evaluated on the cropped waveform, in samples.
Float_t pulse_height
Maximum of the baseline-subtracted cropped waveform, in ADC counts.
Int_t raw_pulse_height
Extremum of the raw, pre-baseline-subtraction trace, as a magnitude.
Bool_t passes_cuts
Whether this waveform survived the quality cuts.
Float_t long_integral
Sum over the long gate, in ADC counts x samples.
Float_t negative_fraction
Fraction of samples in the long gate that are below zero, in [0, 1].
Float_t short_integral
Sum over the short gate, in ADC counts x samples.
ULong64_t timestamp
Acquisition timestamp carried through from the input record.