/** * 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; } 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; } } } }