Skip to main content

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