224 lines
5.1 KiB
JavaScript
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 }
|