186 lines
5.1 KiB
JavaScript
186 lines
5.1 KiB
JavaScript
|
|
/**
|
|
* 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) {
|
|
this._threshold = threshold;
|
|
this._queue = [];
|
|
this._last_min = 0;
|
|
this._counter = 0;
|
|
}
|
|
|
|
getThreshold() {
|
|
return this._threshold;
|
|
}
|
|
|
|
addVector(vec) {
|
|
let result = [];
|
|
|
|
for (let i = 0; i < vec.length; ++i) {
|
|
const res = this.add(vec[i]);
|
|
if(res !== null)
|
|
result.push(res);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
add(value) {
|
|
let result = null;
|
|
this._queue.push(value);
|
|
if (this._queue.length > 3) {
|
|
this._queue.shift();
|
|
}
|
|
if (this._queue.length !== 3) {
|
|
return null;
|
|
}
|
|
const [last, current, next] = this._queue;
|
|
const is_maximum = current > next && current > last;
|
|
if (is_maximum && (current - this._last_min) > this._threshold) {
|
|
result = this._counter + 1;
|
|
this._last_min = current;
|
|
}
|
|
this._last_min = Math.min(this._last_min, current);
|
|
this._counter += 1;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
}
|
|
}
|