gondola_core/tof/
algorithms.rs

1//! Algorithms used to exract information from 
2//! the TOF waveforms.
3// This file is part of gaps-online-software and published 
4// under the GPLv3 license
5
6use crate::prelude::*;
7
8/// Return the index of the maximum value in an 
9/// array of floats. 
10///
11/// Protip: f32 does not obey Ord, because of NaN, 
12///         so this is done "by hand"
13///
14/// # Arguments:
15///
16///
17pub fn get_max_value_idx<T : std::cmp::PartialOrd + std::fmt::Display + Copy>(values    : &[T],
18         start_idx : usize,
19         n_idx     : usize) -> Result<usize, WaveformError> {
20  if start_idx >= values.len() {
21    error!("Invalid value for start index {}", start_idx);
22    return Err(WaveformError::OutOfRangeLowerBound);
23  }
24  if start_idx + n_idx >= values.len() {
25    error!("Start index {} + n steps of {} is larger tan array size!", start_idx, n_idx); 
26    return Err(WaveformError::OutOfRangeUpperBound);
27  }
28  let mut maxval   = values[start_idx];
29  let mut maxbin = start_idx;
30  for n in start_idx..start_idx + n_idx {
31    if values[n] > maxval {
32      maxval  = values[n];
33      maxbin  = n;
34    }
35  } // end for
36  trace!("Got index {} for a max value of {}", maxbin, maxval);
37  Ok(maxbin)
38} // end fn
39
40//---------------------------------------------------
41
42#[cfg(feature="pybindings")]
43#[pyfunction]
44#[pyo3(name="get_max_value_idx")]
45pub fn get_max_value_idx_py<'_py>(value : Bound<'_py,PyArray1<f32>>,
46                                  start_idx :usize,
47                                  n_idx : usize) -> PyResult<usize> {
48  unsafe {
49    match get_max_value_idx::<f32>(value.as_slice().unwrap(), start_idx, n_idx) {
50      Err(err) => {
51        return Err(PyValueError::new_err(err.to_string()));
52      }
53      Ok(max_val) => {
54        return Ok(max_val);
55      }
56    }
57  }
58}
59
60/// Linear interpolation of the time within a single bin of a TOF waveform
61///
62/// # Arguments:
63///   * voltages    : Waveform in mV 
64///   * nanoseconds : Calibrated time for the waveform bins in ns 
65///   * threshold   : Threshold in mV which is supposed to be crossed 
66///                   within the bin 
67///   * idx         : Together with size define a range for the search for 
68///                   the bin which should have the implementation applied 
69///                   to \[voltages\[idx\], voltages\[idx + size\]\]
70///   * size        : Together with idx define a range for the search for 
71///                   the bin which should have the implementation applied 
72///                   to \[voltages\[idx\], voltages\[idx + size\]\]
73pub fn interpolate_time<T : AsRef<[f32]>> (volts         : &T,
74                                           times         : &T, 
75                                           mut threshold : f32,
76                                           mut idx       : usize,
77                                           size          : usize) -> Result<f32, WaveformError> {
78  let voltages    = volts.as_ref();
79  let nanoseconds = times.as_ref();
80  if idx + 1 > nanoseconds.len() {
81    return Err(WaveformError::OutOfRangeUpperBound);
82  }
83  threshold     = threshold.abs();
84  let mut lval  = (voltages[idx]).abs();
85  let mut hval : f32 = 0.0; 
86  if size == 1 {
87    hval = (voltages[idx+1]).abs();
88  } else {
89    for n in idx+1..idx+size {
90      hval = voltages[n].abs();
91      if (hval>=threshold) && (threshold<=lval) { // Threshold crossing?
92        idx = n-1; // Reset idx to point before crossing
93        break;
94      }
95      lval = hval;
96    }
97  }
98  if ((lval > threshold) && (size != 1)) || lval == hval {
99    return Ok(nanoseconds[idx]);
100  } else {
101    return Ok(nanoseconds[idx] 
102          + (threshold-lval)/(hval-lval) * (nanoseconds[idx+1]
103          - nanoseconds[idx]));
104  }
105}
106
107
108#[cfg(feature = "pybindings")]
109#[pyfunction]
110#[pyo3(name="interpolate_time")]
111/// Linear interpolation of the time within a single bin of a TOF waveform
112///
113/// # Arguments:
114///   * voltages    : Waveform in mV 
115///   * nanoseconds : Calibrated time for the waveform bins in ns 
116///   * threshold   : Threshold in mV which is supposed to be crossed 
117///                   within the bin 
118///   * idx         : Together with size define a range for the search for 
119///                   the bin which should have the implementation applied 
120///                   to \[voltages\[idx\], voltages\[idx + size\]\]
121///   * size        : Together with idx define a range for the search for 
122///                   the bin which should have the implementation applied 
123///                   to \[voltages\[idx\], voltages\[idx + size\]\]
124pub fn interpolate_time_py(voltages    : PyReadonlyArray1<f32>,
125                           nanoseconds : PyReadonlyArray1<f32>,
126                           threshold   : f32,
127                           idx         : usize,
128                           size        : usize) -> PyResult<f32> {
129  let i   = idx;
130  match interpolate_time(&voltages.readonly().as_slice().unwrap(),
131                         &nanoseconds.readonly().as_slice().unwrap(),
132                         threshold, i, size) {
133    Ok(time) => {
134      return Ok(time);
135    }
136    Err(err) => {
137      return Err(PyValueError::new_err(err.to_string()));
138    }
139  }
140}
141
142
143/// Integrate a waveform
144///
145/// That this works right, prior to the 
146/// integration we should subtract the 
147/// baseline.
148///
149/// # Arguments:
150///
151/// * impedance : typically this is 
152pub fn integrate(voltages     : &Vec<f32>,
153                 nanoseconds  : &Vec<f32>,
154                 lo_bin       : usize,
155                 upper_bin    : usize,
156                 impedance    : f32) -> Result<f32, WaveformError>  {
157  if upper_bin > voltages.len() {
158    return Err(WaveformError::OutOfRangeUpperBound);
159  }
160  if lo_bin < 1 {
161    return Err(WaveformError::OutOfRangeLowerBound);
162  }
163  let mut sum = 0f32;
164  for n in lo_bin..upper_bin {
165    sum += voltages[n] * (nanoseconds[n] - nanoseconds[n-1]) ;
166  }
167  sum /= impedance;
168  Ok(sum)
169}
170
171/// Given a time in ns, find the bin most closely corresponding to that time
172/// # Arguments
173/// 
174pub fn time2bin(nanoseconds : &Vec<f32>,
175                t_ns        : f32) -> Result<usize, WaveformError> {
176  for n in 0..nanoseconds.len() {
177    if nanoseconds[n] > t_ns {
178      return Ok(n-1);
179    }
180  }
181  debug!("Did not find a bin corresponding to the given time {}!", t_ns);
182  return Err(WaveformError::TimesTooSmall);
183}
184
185/// The pedestal is the baseline of the waveform
186///
187/// # Arguments
188///
189/// * voltages      : calibrated waveform
190/// * threshold     : consider everything below threshold
191///                   the pedestal (typical 10mV)
192/// * ped_begin_bin : beginning of the window for pedestal
193///                   calculation (bin)
194/// * ped_range_bin : length of the window for pedestal
195///                   calculation (in bins)
196///
197/// # Return
198/// pedestal value with error (quadratic error)
199pub fn calculate_pedestal(voltages      : &Vec<f32>,
200                          threshold     : f32,
201                          ped_begin_bin : usize,
202                          ped_range_bin : usize) -> (f32,f32) {
203  let mut sum  = 0f32;
204  let mut sum2 = 0f32;
205  for k in ped_begin_bin..ped_begin_bin + ped_range_bin {
206    if f32::abs(voltages[k]) < threshold {
207      sum  += voltages[k];
208      sum2 += voltages[k]*voltages[k];
209    }
210  }
211  let average = sum/(ped_range_bin as f32);
212  let sigma   = f32::sqrt(sum2/(ped_range_bin as f32 - (average*average)));
213  (average, sigma)
214}
215
216/// Find the onset time of a peak with a 
217/// constant fraction discrimination method.
218///
219/// The peaks have to be sane
220/// FIXME: Maybe introduce a separate check?
221pub fn cfd_simple(voltages    : &Vec<f32>,
222                  nanoseconds : &Vec<f32>,
223                  cfd_frac    : f32,
224                  start_peak  : usize,
225                  end_peak    : usize) -> Result<f32, WaveformError> {
226
227  let idx = get_max_value_idx(&voltages, start_peak, end_peak-start_peak)?;
228  let mut sum : f32 = 0.0;
229  for n in idx-1..idx+1{
230    sum += voltages[n];
231  }
232  let tmp_thresh : f32 = f32::abs(cfd_frac * (sum / 3.0));
233  trace!("Calculated tmp threshold of {}", tmp_thresh);
234  // Now scan through the waveform around the peak to find the bin
235  // crossing the calculated threshold. Bin idx is the peak so it is
236  // definitely above threshold. So let's walk backwards through the
237  // trace until we find a bin value less than the threshold.
238  let mut lo_bin : usize = voltages.len();
239  let mut n = idx;
240  if idx < start_peak {
241    error!("The index {} is smaller than the beginning of the peak {}!", idx, start_peak);
242    return Err(WaveformError::OutOfRangeLowerBound);
243  }
244  if start_peak >= 10 {
245    while n > start_peak - 10 {
246      if f32::abs(voltages[n]) < tmp_thresh {
247        lo_bin = n;
248        break;
249      }
250      n -= 1;
251    }  
252  } else {
253    debug!("We require that the peak is at least 10 bins away from the start!");
254    return Err(WaveformError::OutOfRangeLowerBound);
255  }
256
257  trace!("Lo bin {} , start peak {}", lo_bin, start_peak);
258  let cfd_time : f32;
259  if lo_bin < nanoseconds.len() -1 {
260    cfd_time = interpolate_time(voltages, nanoseconds, tmp_thresh, lo_bin, 1)?;  
261  } else {
262    cfd_time = nanoseconds[nanoseconds.len() - 1];
263  } 
264  Ok(cfd_time)
265}
266
267/// Find peaks in a given time window (in ns) by 
268/// comparing the waveform voltages with the 
269/// given threshold. 
270///
271/// #Arguments:
272/// * start_time     : begin to look for peaks after 
273///                    this (local) waveform time 
274/// * window_size    : (in ns)
275/// * min_peak_width : minimum number of consequtive bins
276///                    which have to be over threshold
277///                    so that it is considered a peak
278/// * threshold      : peaks are found when voltages go
279///                    over threshold for at leas
280///                    min_peak_width bins
281/// * max_peaks      : stop algorithm after max_peaks are
282///                    found, the rest will be ignored
283/// #Returns:
284///    `Vec<(peak_begin_bin, peak_end_bin)>`
285///
286pub fn find_peaks(voltages       : &Vec<f32>,
287                  nanoseconds    : &Vec<f32>,
288                  start_time     : f32,
289                  window_size    : f32,
290                  min_peak_width : usize,
291                  threshold      : f32,
292                  max_peaks      : usize)
293-> Result<Vec<(usize,usize)>, WaveformError> {
294  let mut peaks      = Vec::<(usize,usize)>::new();
295  let mut start_bin  = time2bin(nanoseconds, start_time)?;
296  if start_bin <= 10 {
297    debug!("We deliberatly do not search for peaks within the first 10 bins! Correcting..");
298    start_bin = 10;
299  }
300  let window_bin = time2bin(nanoseconds, start_time + window_size)? - start_bin;
301  if start_bin + window_bin > voltages.len () {
302    return Err(WaveformError::OutOfRangeUpperBound);
303  }
304
305  let mut pos = 0usize;
306  // find the first bin when voltage
307  // goes over threshold
308  for k in start_bin..start_bin + window_bin {
309    if voltages[k] >= threshold {
310      pos = k;
311      break;
312    }
313  }
314  if pos == 0 && start_bin == 0 && voltages[pos] < threshold {
315    // waveform did not cross threshold
316    return Err(WaveformError::DidNotCrossThreshold)
317  }
318  // actual peak finding
319  let mut nbins_peak   = 0usize;
320  let mut begin_peak   = pos;
321  let mut end_peak  : usize;
322  if (pos + window_bin) > voltages.len() {
323    return Err(WaveformError::OutOfRangeUpperBound);
324  }
325  for k in pos..(pos + window_bin) {
326    if voltages[k] >= threshold {
327      nbins_peak += 1;
328      let mut slope = 0i16; // slope can be positive (1)
329                            // or negative (-1)
330                            // as soon as the slope turns, 
331                            // we declare the peak over, 
332                            // if it is still positive, we
333                            // continue to count the bins
334      if nbins_peak == min_peak_width {
335        // in this case, we don't care about the slope
336        begin_peak  = k - min_peak_width -1;
337      } else if nbins_peak > min_peak_width {
338        for j in 0..min_peak_width {
339          if voltages[k -j] > voltages[k-j-1] {
340            slope = 1; // still ascending
341          }
342        }
343        if slope == 1 {
344          // we consider this the same peak
345          continue;
346        } 
347        if slope == 0 {
348          // each bump counts as separate peak
349          end_peak = k;
350          nbins_peak = 0; // peak is done
351          peaks.push((begin_peak, end_peak));
352          if peaks.len() == max_peaks {
353            break;
354          }
355        }
356      } // if nbins_peak < min_peak_width, we just 
357        // continue going to check if it is still 
358        // over threshold
359    } else {
360      if nbins_peak > min_peak_width {
361        end_peak = k;
362        peaks.push((begin_peak, end_peak));
363        if peaks.len() == max_peaks {
364          break;
365        }
366      }
367      nbins_peak = 0;
368    }
369  }
370  // FIXME - remove invalid peaks
371  let len_pks_dirty = peaks.len();
372  peaks.retain(|&x| {(x.0 < NWORDS - 1) & (x.1 <= NWORDS - 1)});
373  let len_pks_clean = peaks.len();
374  if len_pks_clean != len_pks_dirty {
375    debug!("We removed {} pks because they had values outside of 0-{}!", len_pks_dirty - len_pks_clean, NWORDS);
376  }
377  Ok(peaks)
378}
379
380
381#[cfg(feature = "advanced-algorithms")]
382fn find_sequence_ranges(vec: Vec<usize>) -> Vec<(usize, usize)> {
383  let mut ranges = Vec::new();
384  let mut start = vec[0];
385  let mut end   = vec[0];
386
387  for &value in vec.iter().skip(1) {
388    if value == end + 1 {
389      // Extend the current sequence
390      end = value;
391    } else {
392      // End of current sequence, start of a new one
393      ranges.push((start, end));
394      start = value;
395      end = value;
396    }
397  }
398
399  // Add the last sequence
400  ranges.push((start, end));
401  ranges
402}
403
404#[cfg(feature = "advanced-algorithms")]
405/// Z-scores peak finding algorithm
406///
407/// Brakel, J.P.G. van (2014).
408/// "Robust peak detection algorithm using z-scores". 
409/// Stack Overflow.
410/// Available at: <https://stackoverflow.com/questions/22583391/peak-signal-detection-in-realtime-timeseries-data/i22640362#22640362> (version: 2020-11-08).
411///
412/// Robust peak detection algorithm (using z-scores)
413///
414/// [..] algorithm that works very well for these types of datasets.
415/// It is based on the principle of dispersion:
416/// if a new datapoint is a given x number of standard deviations away
417/// from a moving mean, the algorithm gives a signal.
418/// The algorithm is very robust because it constructs a separate moving mean
419/// and deviation, such that previous signals do not corrupt
420/// the signalling threshold for future signals.
421/// The sensitivity of the algorithm is therefore robust to previous signals.
422///
423/// # Arguments:
424///
425/// * nanoseconds   : calibrated waveform times
426/// * voltages      : calibrated waveform voltages
427/// * start_time    : restrict the algorithm on a 
428///                   certain time window, start 
429///                   at start_time
430/// * window_size   : in ns
431/// * lag           : The lag of the moving window that calculates the mean
432///                   and standard deviation of historical data.
433///                   A longer window takes more historical data in account.
434///                   A shorter window is more adaptive,
435///                   such that the algorithm will adapt to new information
436///                   more quickly.
437///                   For example, a lag of 5 will use the last 5 observations
438///                   to smooth the data.
439/// * threshold     : The "z-score" at which the algorithm signals.
440///                   Simply put, if the distance between a new datapoint
441///                   and the moving mean is larger than the threshold
442///                   multiplied with the moving standard deviation of the data,
443///                   the algorithm provides a signal.
444///                   For example, a threshold of 3.5 will signal if a datapoint
445///                   is 3.5 standard deviations away from the moving mean. 
446/// * influence     : The influence (between 0 and 1) of new signals on
447///                   the calculation of the moving mean and moving standard deviation.
448///                   For example, an influence parameter of 0.5 gives new signals
449///                   half of the influence that normal datapoints have.
450///                   Likewise, an influence of 0 ignores signals completely
451///                   for recalculating the new threshold.
452///                   An influence of 0 is therefore the most robust option 
453///                   (but assumes stationarity);
454///                   putting the influence option at 1 is least robust.
455///                   For non-stationary data, the influence option should
456///                   therefore be put between 0 and 1.
457pub fn find_peaks_zscore(nanoseconds    : &Vec<f32>,
458                         voltages       : &Vec<f32>,
459                         start_time     : f32,
460                         window_size    : f32,
461                         lag            : usize,
462                         threshold      : f64,
463                         influence      : f64)
464-> Result<Vec<(usize,usize)>, WaveformError> {
465  let mut peaks = Vec::<(usize, usize)>::new();
466  let start_bin = time2bin(nanoseconds, start_time)?;
467  let end_bin   = time2bin(nanoseconds, start_time + window_size)?;
468  let mut ranged_voltage = Vec::<f32>::with_capacity(end_bin - start_bin);
469  ranged_voltage.extend_from_slice(&voltages[start_bin..=end_bin]);
470  //30, 5.0, 0.0
471
472  let output: Vec<_> = voltages
473            .into_iter()
474            .enumerate()
475            .peaks(PeaksDetector::new(lag, threshold, influence), |e| *e.1 as f64)
476            .map(|((i, _), p)| (i, p))
477            .collect();
478  // we ignore low peaks
479  if output.len() == 0 {
480    return Ok(peaks);
481  }
482  let mut peak_high = Vec::<usize>::new();
483  for k in output.iter() {
484    if matches!(k.1, Peak::High) {
485      peak_high.push(k.0);
486    }
487  }
488  if peaks.len() > 0 {
489    peaks = find_sequence_ranges(peak_high); 
490  }
491  Ok(peaks)
492}
493
494//---------------------------------------------------
495
496/// Sine fit without using external libraries
497pub fn fit_sine_simple<T>(volts: &[T], times: &[T]) -> (f32, f32, f32) 
498  where T: Float + NumAssign + NumAssignOps + NumOps + Copy + NumCast + FloatConst {
499  let start_bin = 20;
500  let size_bin = 900;
501  let mut data_size = T::zero();
502
503  let mut xi_yi   = T::zero();
504  let mut xi_zi   = T::zero();
505  let mut yi_zi   = T::zero();
506  let mut xi_xi   = T::zero();
507  let mut yi_yi   = T::zero();
508  let mut xi_sum  = T::zero();
509  let mut yi_sum  = T::zero();
510  let mut zi_sum  = T::zero();
511
512  let c1 = T::from(2).unwrap();
513  let c2 = T::from(0.02f32).unwrap();
514  for i in start_bin..(start_bin + size_bin) {
515      let xi = (c1 * T::PI() * c2 * times[i]).cos();
516      let yi = (c1 * T::PI() * c2 * times[i]).sin();
517      let zi = volts[i];
518
519      xi_yi += xi * yi;
520      xi_zi += xi * zi;
521      yi_zi += yi * zi;
522      xi_xi += xi * xi;
523      yi_yi += yi * yi;
524      xi_sum += xi;
525      yi_sum += yi;
526      zi_sum += zi;
527
528      data_size += T::one();
529  }
530
531  let mut a_matrix = [[T::zero(); 3]; 3];
532  a_matrix[0][0] = xi_xi;
533  a_matrix[0][1] = xi_yi;
534  a_matrix[0][2] = xi_sum;
535  a_matrix[1][0] = xi_yi;
536  a_matrix[1][1] = yi_yi;
537  a_matrix[1][2] = yi_sum;
538  a_matrix[2][0] = xi_sum;
539  a_matrix[2][1] = yi_sum;
540  a_matrix[2][2] = data_size;
541
542  let determinant = a_matrix[0][0] * a_matrix[1][1] * a_matrix[2][2]
543      + a_matrix[0][1] * a_matrix[1][2] * a_matrix[2][0]
544      + a_matrix[0][2] * a_matrix[1][0] * a_matrix[2][1]
545      - a_matrix[0][0] * a_matrix[1][2] * a_matrix[2][1]
546      - a_matrix[0][1] * a_matrix[1][0] * a_matrix[2][2]
547      - a_matrix[0][2] * a_matrix[1][1] * a_matrix[2][0];
548
549  let inverse_factor = T::one() / determinant;
550
551  let mut cofactor_matrix = [[T::zero(); 3]; 3];
552  cofactor_matrix[0][0] = a_matrix[1][1] * a_matrix[2][2] - a_matrix[2][1] * a_matrix[1][2];
553  cofactor_matrix[0][1] = (a_matrix[1][0] * a_matrix[2][2] - a_matrix[2][0] * a_matrix[1][2]) * -T::one();
554  cofactor_matrix[0][2] = a_matrix[1][0] * a_matrix[2][1] - a_matrix[2][0] * a_matrix[1][1];
555  cofactor_matrix[1][0] = (a_matrix[0][1] * a_matrix[2][2] - a_matrix[2][1] * a_matrix[0][2]) * -T::one();
556  cofactor_matrix[1][1] = a_matrix[0][0] * a_matrix[2][2] - a_matrix[2][0] * a_matrix[0][2];
557  cofactor_matrix[1][2] = (a_matrix[0][0] * a_matrix[2][1] - a_matrix[2][0] * a_matrix[0][1]) * -T::one();
558  cofactor_matrix[2][0] = a_matrix[0][1] * a_matrix[1][2] - a_matrix[1][1] * a_matrix[0][2];
559  cofactor_matrix[2][1] = (a_matrix[0][0] * a_matrix[1][2] - a_matrix[1][0] * a_matrix[0][2]) * -T::one();
560  cofactor_matrix[2][2] = a_matrix[0][0] * a_matrix[1][1] - a_matrix[1][0] * a_matrix[0][1];
561
562  let mut inverse_matrix = [[T::zero(); 3]; 3];
563  for i in 0..3 {
564      for j in 0..3 {
565          inverse_matrix[i][j] = cofactor_matrix[j][i] * inverse_factor;
566      }
567  }
568
569  let p = [xi_zi, yi_zi, zi_sum];
570  let a = inverse_matrix[0][0] * p[0] + inverse_matrix[1][0] * p[1] + inverse_matrix[2][0] * p[2];
571  let b = inverse_matrix[0][1] * p[0] + inverse_matrix[1][1] * p[1] + inverse_matrix[2][1] * p[2];
572
573  let phi    = <f32 as NumCast>::from(a.atan2(b)).unwrap();
574  let amp    = <f32 as NumCast>::from((a*a + b*b).sqrt()).unwrap();
575  let freq   = 0.02 as f32;
576
577  (amp, freq, phi)
578}
579
580#[cfg(feature="pybindings")]
581#[pyfunction]
582#[pyo3(name="fit_sine_simple")]
583pub fn fit_sine_simple_py<'_py>(xs    : Bound<'_py,PyArray1<f32>>, ys: Bound<'_py, PyArray1<f32>>)  -> (f32,f32,f32) {
584  unsafe {
585    fit_sine_simple::<f32>(ys.as_slice().unwrap(), xs.as_slice().unwrap())
586  }
587}
588