11'use strict'
2- /*
3- Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor
4- */
52/**
6- * The FullTSQuadraticSeries is a datatype that represents a Quadratic Series. It allows
3+ * @copyright [OpenRowingMonitor]{@link https://github.com/JaapvanEkris/openrowingmonitor}
4+ *
5+ * @file The FullTSQuadraticSeries is a datatype that represents a Quadratic Series. It allows
76 * values to be retrieved (like a FiFo buffer, or Queue) but it also includes
87 * a Theil-Sen Quadratic Regressor to determine the coefficients of this dataseries.
98 *
@@ -33,11 +32,13 @@ import loglevel from 'loglevel'
3332const log = loglevel . getLogger ( 'RowingEngine' )
3433
3534/**
36- * @param {integer } the maximum length of the quadratic series, 0 for unlimited
35+ * @param {integer } maxSeriesLength - the maximum length of the quadratic series, 0 for unlimited
3736 */
3837export function createTSQuadraticSeries ( maxSeriesLength = 0 ) {
3938 const X = createSeries ( maxSeriesLength )
4039 const Y = createSeries ( maxSeriesLength )
40+ const weight = createSeries ( maxSeriesLength )
41+ const WY = createSeries ( maxSeriesLength )
4142 const A = createLabelledBinarySearchTree ( )
4243 const linearResidu = createTSLinearSeries ( maxSeriesLength )
4344 let _A = 0
@@ -47,12 +48,13 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
4748 let _goodnessOfFit = 0
4849
4950 /**
50- * @param {float } the x value of the datapoint
51- * @param {float } the y value of the datapoint
51+ * @param {float } x - the x value of the datapoint
52+ * @param {float } y - the y value of the datapoint
53+ * @param {float } w - the weight of the datapoint (defaults to 1)
5254 * Invariant: BinrySearchTree A contains all calculated a's (as in the general formula y = a * x^2 + b * x + c),
5355 * where the a's are labeled in the BinarySearchTree with their Xi when they BEGIN in the point (Xi, Yi)
5456 */
55- function push ( x , y ) {
57+ function push ( x , y , w = 1 ) {
5658 if ( x === undefined || isNaN ( x ) || y === undefined || isNaN ( y ) ) { return }
5759
5860 if ( maxSeriesLength > 0 && X . length ( ) >= maxSeriesLength ) {
@@ -63,6 +65,8 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
6365
6466 X . push ( x )
6567 Y . push ( y )
68+ weight . push ( w )
69+ WY . push ( w * y )
6670
6771 // Calculate the coefficient a for the new interval by adding the newly added datapoint
6872 let i = 0
@@ -72,15 +76,19 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
7276 case ( X . length ( ) >= 3 ) :
7377 // There are now at least three datapoints in the X and Y arrays, so let's calculate the A portion belonging for the new datapoint via Quadratic Theil-Sen regression
7478 // First we calculate the A for the formula
79+ let combinedweight = 0
80+ let coeffA = 1
7581 while ( i < X . length ( ) - 2 ) {
7682 j = i + 1
7783 while ( j < X . length ( ) - 1 ) {
78- A . push ( X . get ( i ) , calculateA ( i , j , X . length ( ) - 1 ) , 1 )
84+ combinedweight = weight . get ( i ) * weight . get ( j ) * w
85+ coeffA = calculateA ( i , j , X . length ( ) - 1 )
86+ A . push ( X . get ( i ) , coeffA , combinedweight )
7987 j ++
8088 }
8189 i ++
8290 }
83- _A = A . median ( )
91+ _A = A . weightedMedian ( )
8492
8593 // We invalidate the linearResidu, B, C, and goodnessOfFit, as this will trigger a recalculate when they are needed
8694 linearResidu . reset ( )
@@ -99,7 +107,7 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
99107 }
100108
101109 /**
102- * @param {integer } the position in the flank of the requested value (default = 0)
110+ * @param {integer } position - the position in the flank of the requested value (default = 0)
103111 * @returns {float } the firdt derivative of the quadratic function y = a x^2 + b x + c
104112 */
105113 function firstDerivativeAtPosition ( position = 0 ) {
@@ -112,7 +120,7 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
112120 }
113121
114122 /**
115- * @param {integer } the position in the flank of the requested value (default = 0)
123+ * @param {integer } position - the position in the flank of the requested value (default = 0)
116124 * @returns {float } the second derivative of the quadratic function y = a x^2 + b x + c
117125 */
118126 function secondDerivativeAtPosition ( position = 0 ) {
@@ -124,7 +132,7 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
124132 }
125133
126134 /**
127- * @param {float } the x value of the requested value
135+ * @param {float } x - the x value of the requested value
128136 * @returns {float } the slope of the linear function
129137 */
130138 function slope ( x ) {
@@ -137,22 +145,22 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
137145 }
138146
139147 /**
140- * @returns {float } the coefficient a of the quadratic function y = a x^2 + b x + c
148+ * @returns {float } the (quadratic) coefficient a of the quadratic function y = a x^2 + b x + c
141149 */
142150 function coefficientA ( ) {
143151 return _A
144152 }
145153
146154 /**
147- * @returns {float } the coefficient b of the quadratic function y = a x^2 + b x + c
155+ * @returns {float } the (linear) coefficient b of the quadratic function y = a x^2 + b x + c
148156 */
149157 function coefficientB ( ) {
150158 calculateB ( )
151159 return _B
152160 }
153161
154162 /**
155- * @returns {float } the coefficient c of the quadratic function y = a x^2 + b x + c
163+ * @returns {float } the (intercept) coefficient c of the quadratic function y = a x^2 + b x + c
156164 */
157165 function coefficientC ( ) {
158166 calculateB ( )
@@ -177,33 +185,38 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
177185 }
178186
179187 /**
180- * @returns {float } the R^2 as a goodness of fit indicator
188+ * @returns {float } the R^2 as a global goodness of fit indicator
181189 */
182190 function goodnessOfFit ( ) {
183191 let i = 0
184192 let sse = 0
185193 if ( _goodnessOfFit === null ) {
194+ calculateB ( )
195+ calculateC ( )
186196 if ( X . length ( ) >= 3 ) {
187197 _sst = 0
198+ const weightedAverageY = WY . sum ( ) / weight . sum ( )
199+
188200 while ( i < X . length ( ) ) {
189- sse += Math . pow ( ( Y . get ( i ) - projectX ( X . get ( i ) ) ) , 2 )
190- _sst += Math . pow ( ( Y . get ( i ) - Y . average ( ) ) , 2 )
201+ sse += weight . get ( i ) * Math . pow ( Y . get ( i ) - projectX ( X . get ( i ) ) , 2 )
202+ _sst += weight . get ( i ) * Math . pow ( Y . get ( i ) - weightedAverageY , 2 )
191203 i ++
192204 }
205+
193206 switch ( true ) {
194207 case ( sse === 0 ) :
195208 _goodnessOfFit = 1
196209 break
197210 case ( sse > _sst ) :
198211 // This is a pretty bad fit as the error is bigger than just using the line for the average y as intercept
199- _goodnessOfFit = 0
212+ _goodnessOfFit = 0.01
200213 break
201214 case ( _sst !== 0 ) :
202215 _goodnessOfFit = 1 - ( sse / _sst )
203216 break
204217 default :
205218 // When _SST = 0, R2 isn't defined
206- _goodnessOfFit = 0
219+ _goodnessOfFit = 0.01
207220 }
208221 } else {
209222 _goodnessOfFit = 0
@@ -229,14 +242,14 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
229242 break
230243 case ( squaredError > _sst ) :
231244 // This is a pretty bad fit as the error is bigger than just using the line for the average y as intercept
232- return 0
245+ return 0.01
233246 break
234247 case ( _sst !== 0 ) :
235248 return Math . min ( Math . max ( 1 - ( ( squaredError * X . length ( ) ) / _sst ) , 0 ) , 1 )
236249 break
237250 default :
238251 // When _SST = 0, localGoodnessOfFit isn't defined
239- return 0
252+ return 0.01
240253 }
241254 /* eslint-enable no-unreachable */
242255 } else {
@@ -245,7 +258,7 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
245258 }
246259
247260 /**
248- * @param {float } the x value to be projected
261+ * @param {float } x - the x value to be projected
249262 * @returns {float } the resulting y value when projected via the linear function
250263 */
251264 function projectX ( x ) {
@@ -258,6 +271,12 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
258271 }
259272 }
260273
274+ /**
275+ * @param {integer } pointOne - The position in the series of the first datapoint used for the quadratic coefficient calculation
276+ * @param {integer } pointTwo - The position in the series of the second datapoint used for the quadratic coefficient calculation
277+ * @param {integer } pointThree - The position in the series of the third datapoint used for the quadratic coefficient calculation
278+ * @returns {float } the coefficient A of the linear function
279+ */
261280 function calculateA ( pointOne , pointTwo , pointThree ) {
262281 let result = 0
263282 if ( X . get ( pointOne ) !== X . get ( pointTwo ) && X . get ( pointOne ) !== X . get ( pointThree ) && X . get ( pointTwo ) !== X . get ( pointThree ) ) {
@@ -270,6 +289,9 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
270289 }
271290 }
272291
292+ /**
293+ * @description This helper function calculates the slope of the linear residu and stores it in _B
294+ */
273295 function calculateB ( ) {
274296 // Calculate all the linear slope for the newly added point and the newly calculated A
275297 // This function is only called when a linear slope is really needed, as this saves a lot of CPU cycles when only a slope suffices
@@ -283,6 +305,9 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
283305 }
284306 }
285307
308+ /**
309+ * @description This helper function calculates the intercept of the linear residu and stores it in _C
310+ */
286311 function calculateC ( ) {
287312 // Calculate all the intercept for the newly added point and the newly calculated A
288313 // This function is only called when a linear intercept is really needed, as this saves a lot of CPU cycles when only a slope suffices
@@ -296,12 +321,15 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
296321 }
297322 }
298323
324+ /**
325+ * @description This helper function fills the linear residu
326+ */
299327 function fillLinearResidu ( ) {
300328 // To calculate the B and C via Linear regression over the residu, we need to fill it if empty
301329 if ( linearResidu . length ( ) === 0 ) {
302330 let i = 0
303331 while ( i < X . length ( ) ) {
304- linearResidu . push ( X . get ( i ) , Y . get ( i ) - ( _A * Math . pow ( X . get ( i ) , 2 ) ) )
332+ linearResidu . push ( X . get ( i ) , Y . get ( i ) - ( _A * Math . pow ( X . get ( i ) , 2 ) ) , weight . get ( i ) )
305333 i ++
306334 }
307335 }
@@ -314,11 +342,16 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
314342 return ( X . length ( ) >= 3 )
315343 }
316344
345+ /**
346+ * @description This function is used for clearing data and state
347+ */
317348 function reset ( ) {
318349 if ( X . length ( ) > 0 ) {
319350 // There is something to reset
320351 X . reset ( )
321352 Y . reset ( )
353+ weight . reset ( )
354+ WY . reset ( )
322355 A . reset ( )
323356 linearResidu . reset ( )
324357 _A = 0
0 commit comments