App: peak detection, basic graph works
This commit is contained in:
186
data-analysis/PeakDetection.js
Normal file
186
data-analysis/PeakDetection.js
Normal file
@@ -0,0 +1,186 @@
|
||||
|
||||
/**
|
||||
* A simple peak detector
|
||||
*
|
||||
* Usage: Successively add values via add() and query the indices of the peaks with peaks
|
||||
*
|
||||
* A peak is detected if the current point is local maximum (no filtering!) and the current
|
||||
* value is larger than (threshold + minimum_since_last_peak)
|
||||
*/
|
||||
class PeakDetectorSimple {
|
||||
constructor(threshold, handleNewPeaks) {
|
||||
this.peaks = [];
|
||||
this._threshold = threshold;
|
||||
this._queue = [];
|
||||
this._last_min = 0;
|
||||
this._counter = 0;
|
||||
this._handleNewPeaks = handleNewPeaks;
|
||||
}
|
||||
|
||||
addVector(vec) {
|
||||
const callbackBackup = this._handleNewPeaks;
|
||||
const numPeaksBefore = this.peaks.length;
|
||||
this._handleNewPeaks = null;
|
||||
for (let i = 0; i < vec.length; ++i) {
|
||||
this.add(vec[i]);
|
||||
}
|
||||
this._handleNewPeaks = callbackBackup;
|
||||
if (numPeaksBefore != this.peaks.length) {
|
||||
this._handleNewPeaks(this.peaks);
|
||||
}
|
||||
}
|
||||
|
||||
add(value) {
|
||||
this._queue.push(value);
|
||||
if (this._queue.length > 3) {
|
||||
this._queue.shift();
|
||||
}
|
||||
if (this._queue.length !== 3) {
|
||||
return;
|
||||
}
|
||||
const [last, current, next] = this._queue;
|
||||
const is_maximum = current > next && current > last;
|
||||
if (is_maximum && (current - this._last_min) > this._threshold) {
|
||||
this.peaks.push(this._counter);
|
||||
this._last_min = current;
|
||||
if (this._handleNewPeaks) {
|
||||
this._handleNewPeaks(this.peaks);
|
||||
}
|
||||
}
|
||||
this._last_min = Math.min(this._last_min, current);
|
||||
this._counter += 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implementation of Z-Score peak detection according to
|
||||
* https://stackoverflow.com/questions/22583391/peak-signal-detection-in-realtime-timeseries-data
|
||||
*/
|
||||
class PeakDetectorZScore {
|
||||
constructor(lag, threshold, influence, handleNewPeaks) {
|
||||
this.peaks = [];
|
||||
this._filter = ZScoreFilter(lag, threshold, influence);
|
||||
this._counter = 0;
|
||||
this._previous_signal = 0;
|
||||
this._max = null;
|
||||
this._max_index = null;
|
||||
this._handleNewPeaks = handleNewPeaks;
|
||||
}
|
||||
|
||||
addVector(vec) {
|
||||
const callbackBackup = this._handleNewPeaks;
|
||||
const numPeaksBefore = this.peaks.length;
|
||||
this._handleNewPeaks = null;
|
||||
for (let i = 0; i < vec.length; ++i) {
|
||||
this.add(vec[i]);
|
||||
}
|
||||
this._handleNewPeaks = callbackBackup;
|
||||
if (numPeaksBefore != this.peaks.length) {
|
||||
this._handleNewPeaks(this.peaks);
|
||||
}
|
||||
}
|
||||
|
||||
add(value) {
|
||||
const signal = this._filter.add(value);
|
||||
if (signal != null) {
|
||||
const rising_flank = this._previous_signal !== 1 && signal === 1;
|
||||
const falling_flank = this._previous_signal === 1 && signal !== 1;
|
||||
if (rising_flank)
|
||||
this._max = -1;
|
||||
if (signal === 1 && this._max != null && value > this._max) {
|
||||
this._max = value;
|
||||
this._max_index = this._counter;
|
||||
}
|
||||
if (falling_flank) {
|
||||
this.peaks.push(this._max_index);
|
||||
if (this._handleNewPeaks) {
|
||||
this._handleNewPeaks(this.peaks);
|
||||
}
|
||||
}
|
||||
|
||||
this._previous_signal = signal;
|
||||
}
|
||||
this._counter += 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export { PeakDetectorSimple, PeakDetectorZScore };
|
||||
|
||||
|
||||
// --------------------------------------- Helper classes -------------------------------------------------------------
|
||||
|
||||
class StatisticsQueue {
|
||||
constructor(size) {
|
||||
this._size = size;
|
||||
|
||||
this._queue = [];
|
||||
this._queue_sum = 0; // running sum over all elements currently in _queue
|
||||
this._queue_sum_sq = 0; // sum of squared elements in _queue
|
||||
}
|
||||
|
||||
add(value) {
|
||||
this._queue.push(value);
|
||||
|
||||
this._queue_sum += value;
|
||||
this._queue_sum_sq += value * value;
|
||||
|
||||
if (this._queue.length > this._size) {
|
||||
const removed = this._queue[0];
|
||||
this._queue.shift();
|
||||
this._queue_sum -= removed;
|
||||
this._queue_sum_sq -= removed * removed;
|
||||
}
|
||||
}
|
||||
|
||||
get avg() {
|
||||
return this._queue_sum / self._queue.length;
|
||||
}
|
||||
|
||||
get variance() {
|
||||
const exp_sq = this._queue_sum_sq / self._queue.length;
|
||||
const my_avg = self.avg;
|
||||
return exp_sq - (my_avg * my_avg);
|
||||
}
|
||||
|
||||
get std_deviation() {
|
||||
return Math.sqrt(this.variance);
|
||||
}
|
||||
|
||||
get filled() {
|
||||
return this._queue.length === this._size;
|
||||
}
|
||||
}
|
||||
|
||||
class ZScoreFilter {
|
||||
|
||||
constructor(lag, threshold, influence) {
|
||||
this._threshold = threshold;
|
||||
this._influence = influence;
|
||||
|
||||
this._last_value = null;
|
||||
this._stat_queue = StatisticsQueue(lag);
|
||||
}
|
||||
|
||||
add(value) {
|
||||
let sq = this._stat_queue;
|
||||
if (!sq.filled) {
|
||||
sq.add(value);
|
||||
this._last_value = value;
|
||||
return null;
|
||||
} else {
|
||||
const avg = sq.avg;
|
||||
if (Math.abs(value - avg) > this._threshold * sq.std_deviation) {
|
||||
const signal = value > avg ? 1 : -1;
|
||||
const filtered = this._influence * value + (1 - this._influence) * this._last_value;
|
||||
sq.add(filtered);
|
||||
this._last_value = filtered;
|
||||
return signal;
|
||||
} else {
|
||||
sq.add(value);
|
||||
this._last_value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user