openrowingmonitor/app/engine/utils/OLSLinearSeries.js

224 lines
5.1 KiB
JavaScript

'use strict'
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
The LinearSeries is a datatype that represents a Linear Series. It allows
values to be retrieved (like a FiFo buffer, or Queue) but it also includes
a Linear Regressor to determine the slope, intercept and R^2 of this timeseries
of x any y coordinates through Simple Linear Regression.
At creation it can be determined that the Time Series is limited (i.e. after it
is filled, the oldest will be pushed out of the queue) or that the the time series
is unlimited (will only expand). The latter is activated by calling the creation with
an empty argument.
please note that for unlimited series it is up to the calling function to handle resetting
the Linear Series when needed through the reset() call.
A key constraint is to prevent heavy calculations at the end (due to large
array based curve fitting) as this function is also used to calculate
drag at the end of the recovery phase, which might happen on a Pi zero
This implementation uses concepts that are described here:
https://www.colorado.edu/amath/sites/default/files/attached-files/ch12_0.pdf
*/
import { createSeries } from './Series.js'
import loglevel from 'loglevel'
const log = loglevel.getLogger('RowingEngine')
function createOLSLinearSeries (maxSeriesLength = 0) {
const X = createSeries(maxSeriesLength)
const XX = createSeries(maxSeriesLength)
const Y = createSeries(maxSeriesLength)
const YY = createSeries(maxSeriesLength)
const XY = createSeries(maxSeriesLength)
const trend = createSeries(maxSeriesLength)
let _slope = 0
let _intercept = 0
let _goodnessOfFit = 0
function push (x, y) {
X.push(x)
XX.push(x * x)
Y.push(y)
YY.push(y * y)
XY.push(x * y)
// Let's approximate the line through OLS
if (X.length() >= 2 && X.sum() > 0) {
_slope = (X.length() * XY.sum() - X.sum() * Y.sum()) / (X.length() * XX.sum() - X.sum() * X.sum())
_intercept = (Y.sum() - (_slope * X.sum())) / X.length()
const sse = YY.sum() - (_intercept * Y.sum()) - (_slope * XY.sum())
const sst = YY.sum() - (Math.pow(Y.sum(), 2) / X.length())
_goodnessOfFit = 1 - (sse / sst)
trend.push(determineTrend(X.length() - 2, X.length() - 1))
} else {
_slope = 0
_intercept = 0
_goodnessOfFit = 0
}
}
function slope () {
return _slope
}
function intercept () {
return _intercept
}
function length () {
return X.length()
}
function goodnessOfFit () {
// This function returns the R^2 as a goodness of fit indicator
if (X.length() >= 2) {
return _goodnessOfFit
} else {
return 0
}
}
function projectX (x) {
if (X.length() >= 2) {
return (_slope * x) + _intercept
} else {
return 0
}
}
function projectY (y) {
if (X.length() >= 2 && _slope !== 0) {
return ((y - _intercept) / _slope)
} else {
return 0
}
}
function numberOfXValuesAbove (testedValue) {
return X.numberOfValuesAbove(testedValue)
}
function numberOfXValuesEqualOrBelow (testedValue) {
return X.numberOfValuesEqualOrBelow(testedValue)
}
function numberOfYValuesAbove (testedValue) {
return Y.numberOfValuesAbove(testedValue)
}
function numberOfYValuesEqualOrBelow (testedValue) {
return Y.numberOfValuesEqualOrBelow(testedValue)
}
function numberOfUpwardTrend () {
return trend.numberOfValuesAbove(0)
}
function numberOfFlatOrDownwardTrend () {
return trend.numberOfValuesEqualOrBelow(0)
}
function xAtSeriesBegin () {
return X.atSeriesBegin()
}
function xAtSeriesEnd () {
return X.atSeriesEnd()
}
function yAtSeriesBegin () {
return Y.atSeriesBegin()
}
function yAtSeriesEnd () {
return Y.atSeriesEnd()
}
function xSum () {
return X.sum()
}
function ySum () {
return Y.sum()
}
function minimumX () {
return X.minimum()
}
function minimumY () {
return Y.minimum()
}
function maximumX () {
return X.maximum()
}
function maximumY () {
return Y.maximum()
}
function xSeries () {
return X.series()
}
function ySeries () {
return Y.series()
}
function determineTrend (pointOne, pointTwo) {
if (pointOne !== pointTwo) {
return (Y.get(pointTwo) - Y.get(pointOne))
} else {
log.error('OLS Linear Regressor, trend determination, trend can not be applied to one point!')
return 0
}
}
function reset () {
X.reset()
XX.reset()
Y.reset()
YY.reset()
XY.reset()
_slope = 0
_intercept = 0
_goodnessOfFit = 0
}
return {
push,
slope,
intercept,
length,
goodnessOfFit,
projectX,
projectY,
numberOfXValuesAbove,
numberOfXValuesEqualOrBelow,
numberOfYValuesAbove,
numberOfYValuesEqualOrBelow,
numberOfUpwardTrend,
numberOfFlatOrDownwardTrend,
xAtSeriesBegin,
xAtSeriesEnd,
yAtSeriesBegin,
yAtSeriesEnd,
xSum,
ySum,
minimumX,
minimumY,
maximumX,
maximumY,
xSeries,
ySeries,
reset
}
}
export { createOLSLinearSeries }