diff --git a/app/engine/Flywheel.js b/app/engine/Flywheel.js index 072088cf8a..8654661a6f 100644 --- a/app/engine/Flywheel.js +++ b/app/engine/Flywheel.js @@ -61,9 +61,12 @@ export function createFlywheel (rowerSettings) { const cyclicErrorFilter = createCyclicErrorFilter(rowerSettings, minimumDragFactorSamples, recoveryDeltaTime) const strokedetectionMinimalGoodnessOfFit = rowerSettings.minimumStrokeQuality const minimumRecoverySlope = createWeighedSeries(rowerSettings.dragFactorSmoothing, rowerSettings.minimumRecoverySlope) - let totalTime - let currentAngularDistance - let _deltaTimeBeforeFlank + let rawTime = 0 + let rawNumberOfImpulses = 0 + let totalTimeSpinning = 0 + let totalNumberOfImpulses = 0 + let _totalWork = 0 + let _deltaTimeBeforeFlank = {} let _angularVelocityAtBeginFlank let _angularVelocityBeforeFlank let _angularAccelerationAtBeginFlank @@ -72,11 +75,8 @@ export function createFlywheel (rowerSettings) { let _torqueBeforeFlank let inRecoveryPhase let maintainMetrics - let totalNumberOfImpulses - let totalTimeSpinning - let _totalWork reset() - + /** * @param {float} dataPoint - The lenght of the impuls (currentDt) in seconds * @description This function is called from Rower.js each time the sensor detected an impulse. It transforms this (via the buffers) into a robust flywheel position, speed and acceleration. @@ -112,8 +112,8 @@ export function createFlywheel (rowerSettings) { // value before the shift is certain to be part of a specific rowing phase (i.e. Drive or Recovery), once the buffer is filled completely totalNumberOfImpulses += 1 - _deltaTimeBeforeFlank = cyclicErrorFilter.clean.atSeriesBegin() - totalTimeSpinning += _deltaTimeBeforeFlank + _deltaTimeBeforeFlank = cyclicErrorFilter.atSeriesBegin() + totalTimeSpinning += _deltaTimeBeforeFlank.clean _angularVelocityBeforeFlank = _angularVelocityAtBeginFlank _angularAccelerationBeforeFlank = _angularAccelerationAtBeginFlank // As drag is recalculated at the begin of the drive, we need to recalculate the torque @@ -121,9 +121,9 @@ export function createFlywheel (rowerSettings) { if (inRecoveryPhase) { // Feed the drag calculation, as we didn't reset the Semaphore in the previous cycle based on the current flank - recoveryDeltaTime.push(totalTimeSpinning, _deltaTimeBeforeFlank) + recoveryDeltaTime.push(totalTimeSpinning, _deltaTimeBeforeFlank.clean, _deltaTimeBeforeFlank.goodnessOfFit) // Feed the systematic error filter buffer - cyclicErrorFilter.recordRawDatapoint(totalNumberOfImpulses, totalTimeSpinning, cyclicErrorFilter.raw.atSeriesBegin()) + cyclicErrorFilter.recordRawDatapoint(totalNumberOfImpulses, totalTimeSpinning, _deltaTimeBeforeFlank.raw) } else { // Accumulate the energy total as we are in the drive phase _totalWork += Math.max(_torqueBeforeFlank * angularDisplacementPerImpulse, 0) @@ -131,21 +131,22 @@ export function createFlywheel (rowerSettings) { cyclicErrorFilter.processNextRawDatapoint() } } else { - _deltaTimeBeforeFlank = 0 + _deltaTimeBeforeFlank.clean = 0 _angularVelocityBeforeFlank = 0 _angularAccelerationBeforeFlank = 0 _torqueBeforeFlank = 0 } const cleanCurrentDt = cyclicErrorFilter.applyFilter(dataPoint, totalNumberOfImpulses + flankLength) - totalTime += cleanCurrentDt.value - currentAngularDistance += angularDisplacementPerImpulse + rawTime += cleanCurrentDt.value + rawNumberOfImpulses++ + const currentAngularDistance = rawNumberOfImpulses * angularDisplacementPerImpulse // Let's feed the stroke detection algorithm - _deltaTime.push(totalTime, cleanCurrentDt.value) + _deltaTime.push(rawTime, cleanCurrentDt.value, cleanCurrentDt.goodnessOfFit) // Calculate the metrics that are needed for more advanced metrics, like the foce curve - _angularDistance.push(totalTime, currentAngularDistance) + _angularDistance.push(rawTime, currentAngularDistance, cleanCurrentDt.goodnessOfFit) _angularVelocityAtBeginFlank = _angularDistance.firstDerivative(0) _angularAccelerationAtBeginFlank = _angularDistance.secondDerivative(0) _torqueAtBeginFlank = (rowerSettings.flywheelInertia * _angularAccelerationAtBeginFlank + drag.weighedAverage() * Math.pow(_angularVelocityAtBeginFlank, 2)) @@ -190,10 +191,10 @@ export function createFlywheel (rowerSettings) { if (rowerSettings.autoAdjustRecoverySlope) { // We are allowed to autoadjust stroke detection slope as well, so let's do that minimumRecoverySlope.push((1 - rowerSettings.autoAdjustRecoverySlopeMargin) * recoveryDeltaTime.slope(), recoveryDeltaTime.goodnessOfFit()) - log.debug(`*** Calculated recovery slope: ${recoveryDeltaTime.slope().toFixed(6)}, Goodness of Fit: ${recoveryDeltaTime.goodnessOfFit().toFixed(4)}`) + log.trace(`*** Calculated recovery slope: ${recoveryDeltaTime.slope().toFixed(6)}, Goodness of Fit: ${recoveryDeltaTime.goodnessOfFit().toFixed(4)}`) } else { // We aren't allowed to adjust the slope, let's report the slope to help help the user configure it - log.debug(`*** Calculated recovery slope: ${recoveryDeltaTime.slope().toFixed(6)}, Goodness of Fit: ${recoveryDeltaTime.goodnessOfFit().toFixed(4)}, not used as autoAdjustRecoverySlope isn't set to true`) + log.trace(`*** Calculated recovery slope: ${recoveryDeltaTime.slope().toFixed(6)}, Goodness of Fit: ${recoveryDeltaTime.goodnessOfFit().toFixed(4)}, not used as autoAdjustRecoverySlope isn't set to true`) } } else { // As the drag calculation is considered unreliable, we must skip updating the systematic error filter that depends on it @@ -225,7 +226,7 @@ export function createFlywheel (rowerSettings) { * @returns {float} the current DeltaTime BEFORE the flank */ function deltaTime () { - return _deltaTimeBeforeFlank + return _deltaTimeBeforeFlank.clean } /** @@ -397,27 +398,35 @@ export function createFlywheel (rowerSettings) { } } + /** + * @param {float} slope - Recovery slope to be converted + * @returns {float} Dragfactor to be used in all calculations + * @description Helper function to convert a recovery slope into a dragfactor + */ function slopeToDrag (slope) { return ((slope * rowerSettings.flywheelInertia) / angularDisplacementPerImpulse) } + /** + * @description This function is used for clearing all data, returning the flywheel.js to its initial state + */ function reset () { maintainMetrics = false inRecoveryPhase = false + rawTime = 0 + rawNumberOfImpulses = 0 + totalTimeSpinning = 0 + totalNumberOfImpulses = -1 + _totalWork = 0 drag.reset() cyclicErrorFilter.reset() cyclicErrorFilter.applyFilter(0, flankLength - 1) recoveryDeltaTime.reset() _deltaTime.reset() _angularDistance.reset() - totalTime = 0 - currentAngularDistance = 0 - totalNumberOfImpulses = -1 - totalTimeSpinning = 0 - _totalWork = 0 _deltaTime.push(0, 0) _angularDistance.push(0, 0) - _deltaTimeBeforeFlank = 0 + _deltaTimeBeforeFlank.clean = 0 _angularVelocityBeforeFlank = 0 _angularAccelerationBeforeFlank = 0 _torqueAtBeginFlank = 0 diff --git a/app/engine/Flywheel.test.js b/app/engine/Flywheel.test.js index c44c3ad1c8..d98494d350 100644 --- a/app/engine/Flywheel.test.js +++ b/app/engine/Flywheel.test.js @@ -1,9 +1,8 @@ 'use strict' /** - * Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor - */ -/** - * Tests of the Flywheel object + * @copyright [OpenRowingMonitor]{@link https://github.com/JaapvanEkris/openrowingmonitor} + * + * @file Tests of the Flywheel object */ import { test } from 'uvu' import * as assert from 'uvu/assert' @@ -25,7 +24,7 @@ const baseConfig = { // Based on Concept 2 settings, as this is the validation s maximumTimeBetweenImpulses: 0.020, flankLength: 12, systematicErrorAgressiveness: 0, - systematicErrorMaximumChange: 1, + systematicErrorNumberOfDatapoints: 1, minimumStrokeQuality: 0.36, minimumForceBeforeStroke: 10, minimumRecoverySlope: 0.00070, @@ -37,7 +36,9 @@ const baseConfig = { // Based on Concept 2 settings, as this is the validation s magicConstant: 2.8 } -// Test behaviour for no datapoints +/** + * @description Test behaviour for no datapoints + */ test('Correct Flywheel behaviour at initialisation', () => { const flywheel = createFlywheel(baseConfig) testDeltaTime(flywheel, 0) @@ -52,14 +53,20 @@ test('Correct Flywheel behaviour at initialisation', () => { testIsPowered(flywheel, false) }) -// Test behaviour for one datapoint +/** + * @todo Test behaviour for one datapoint + */ -// Test behaviour for perfect upgoing flank +/** + * @todo Test behaviour for perfect upgoing flank + */ -// Test behaviour for perfect downgoing flank +/** + * @todo Test behaviour for perfect downgoing flank + */ /** - * Test of the integration of the underlying FullTSQuadraticEstimator object + * @description Test of the integration of the underlying FullTSQuadraticEstimator object * This uses the same data as the function y = 2 x^2 + 4 * x */ test('Test of correct algorithmic integration of FullTSQuadraticEstimator and Flywheel object for quadratic function f(x) = 2 * x^2 + 4 * x', () => { @@ -75,7 +82,7 @@ test('Test of correct algorithmic integration of FullTSQuadraticEstimator and Fl maximumTimeBetweenImpulses: 1, flankLength: 12, systematicErrorAgressiveness: 0, - systematicErrorMaximumChange: 1, + systematicErrorNumberOfDatapoints: 1, minimumStrokeQuality: 0.36, minimumForceBeforeStroke: 0, minimumRecoverySlope: 0.00070, @@ -162,126 +169,126 @@ test('Test of correct algorithmic integration of FullTSQuadraticEstimator and Fl testDeltaTime(flywheel, 0) // Values from Datapoint 0 are now passsing through testSpinningTime(flywheel, 0) testAngularPosition(flywheel, 0) - testAngularVelocity(flywheel, 4.000000000000003) - testAngularAcceleration(flywheel, 3.999999999999983) + testAngularVelocity(flywheel, 4.000000000000004) + testAngularAcceleration(flywheel, 3.99999999999998) flywheel.pushValue(0.095324565640171) // Datapoint 13 testDeltaTime(flywheel, 0.234341433963188) // Values from Datapoint 1 are now passsing through testSpinningTime(flywheel, 0.234341433963188) testAngularPosition(flywheel, 1.0471975511965976) - testAngularVelocity(flywheel, 4.937365735852749) - testAngularAcceleration(flywheel, 3.999999999999982) + testAngularVelocity(flywheel, 4.937365735852752) + testAngularAcceleration(flywheel, 3.999999999999979) flywheel.pushValue(0.092177973027300) // Datapoint 14 testDeltaTime(flywheel, 0.196461680094298) // Values from Datapoint 2 are now passsing through testSpinningTime(flywheel, 0.430803114057486) testAngularPosition(flywheel, 2.0943951023931953) - testAngularVelocity(flywheel, 5.7232124562299385) - testAngularAcceleration(flywheel, 3.9999999999999822) + testAngularVelocity(flywheel, 5.723212456229939) + testAngularAcceleration(flywheel, 3.999999999999979) flywheel.pushValue(0.089323823233014) // Datapoint 15 testDeltaTime(flywheel, 0.172567188397595) // Values from Datapoint 3 are now passsing through testSpinningTime(flywheel, 0.6033703024550809) testAngularPosition(flywheel, 3.141592653589793) testAngularVelocity(flywheel, 6.413481209820315) - testAngularAcceleration(flywheel, 3.999999999999983) + testAngularAcceleration(flywheel, 3.9999999999999787) flywheel.pushValue(0.086719441920360) // Datapoint 16 testDeltaTime(flywheel, 0.155718979643243) // Values from Datapoint 4 are now passsing through testSpinningTime(flywheel, 0.7590892820983239) testAngularPosition(flywheel, 4.1887902047863905) testAngularVelocity(flywheel, 7.036357128393282) - testAngularAcceleration(flywheel, 3.999999999999982) + testAngularAcceleration(flywheel, 3.999999999999977) flywheel.pushValue(0.084330395149166) // Datapoint 17 testDeltaTime(flywheel, 0.143013206725950) // Values from Datapoint 5 are now passsing through testSpinningTime(flywheel, 0.9021024888242739) testAngularPosition(flywheel, 5.235987755982988) testAngularVelocity(flywheel, 7.608409955297075) - testAngularAcceleration(flywheel, 3.9999999999999805) + testAngularAcceleration(flywheel, 3.999999999999975) flywheel.pushValue(0.082128549835466) // Datapoint 18 testDeltaTime(flywheel, 0.132987841748253) // Values from Datapoint 6 are now passsing through testSpinningTime(flywheel, 1.035090330572527) testAngularPosition(flywheel, 6.283185307179586) testAngularVelocity(flywheel, 8.140361322290087) - testAngularAcceleration(flywheel, 3.9999999999999845) + testAngularAcceleration(flywheel, 3.9999999999999782) flywheel.pushValue(0.080090664596669) // Datapoint 19 testDeltaTime(flywheel, 0.124815090780014) // Values from Datapoint 7 are now passsing through testSpinningTime(flywheel, 1.159905421352541) testAngularPosition(flywheel, 7.330382858376184) - testAngularVelocity(flywheel, 8.639621685410141) - testAngularAcceleration(flywheel, 3.999999999999988) + testAngularVelocity(flywheel, 8.639621685410138) + testAngularAcceleration(flywheel, 3.99999999999998) flywheel.pushValue(0.078197347646078) // Datapoint 20 testDeltaTime(flywheel, 0.117986192571703) // Values from Datapoint 8 are now passsing through testSpinningTime(flywheel, 1.277891613924244) testAngularPosition(flywheel, 8.377580409572781) - testAngularVelocity(flywheel, 9.111566455696956) - testAngularAcceleration(flywheel, 3.99999999999999) + testAngularVelocity(flywheel, 9.111566455696952) + testAngularAcceleration(flywheel, 3.999999999999985) flywheel.pushValue(0.076432273828253) // Datapoint 21 testDeltaTime(flywheel, 0.112168841458569) // Values from Datapoint 9 are now passsing through testSpinningTime(flywheel, 1.390060455382813) testAngularPosition(flywheel, 9.42477796076938) - testAngularVelocity(flywheel, 9.560241821531234) - testAngularAcceleration(flywheel, 3.999999999999988) + testAngularVelocity(flywheel, 9.560241821531228) + testAngularAcceleration(flywheel, 3.9999999999999845) flywheel.pushValue(0.074781587915460) // Datapoint 22 testDeltaTime(flywheel, 0.107135523306685) // Values from Datapoint 10 are now passsing through testSpinningTime(flywheel, 1.4971959786894982) testAngularPosition(flywheel, 10.471975511965976) - testAngularVelocity(flywheel, 9.988783914757978) - testAngularAcceleration(flywheel, 3.9999999999999813) + testAngularVelocity(flywheel, 9.98878391475797) + testAngularAcceleration(flywheel, 3.99999999999998) flywheel.pushValue(0.073233443959153) // Datapoint 23 testDeltaTime(flywheel, 0.102724506937187) // Values from Datapoint 11 are now passsing through testSpinningTime(flywheel, 1.599920485626685) testAngularPosition(flywheel, 11.519173063162574) - testAngularVelocity(flywheel, 10.399681942506728) - testAngularAcceleration(flywheel, 3.9999999999999707) + testAngularVelocity(flywheel, 10.399681942506724) + testAngularAcceleration(flywheel, 3.999999999999972) flywheel.pushValue(0.071777645486524) // Datapoint 24 testDeltaTime(flywheel, 0.098817239158663) // Values from Datapoint 12 are now passsing through testSpinningTime(flywheel, 1.6987377247853481) testAngularPosition(flywheel, 12.566370614359172) - testAngularVelocity(flywheel, 10.79495089914138) - testAngularAcceleration(flywheel, 3.9999999999999565) + testAngularVelocity(flywheel, 10.794950899141375) + testAngularAcceleration(flywheel, 3.99999999999996) flywheel.pushValue(0.070405361445316) // Datapoint 25 testDeltaTime(flywheel, 0.095324565640171) // Values from Datapoint 13 are now passsing through testSpinningTime(flywheel, 1.794062290425519) testAngularPosition(flywheel, 13.613568165555769) - testAngularVelocity(flywheel, 11.176249161702064) - testAngularAcceleration(flywheel, 3.9999999999999423) + testAngularVelocity(flywheel, 11.17624916170206) + testAngularAcceleration(flywheel, 3.9999999999999463) flywheel.pushValue(0.069108899742145) // Datapoint 26 testDeltaTime(flywheel, 0.092177973027300) // Values from Datapoint 14 are now passsing through testSpinningTime(flywheel, 1.886240263452819) testAngularPosition(flywheel, 14.660765716752367) - testAngularVelocity(flywheel, 11.54496105381126) - testAngularAcceleration(flywheel, 3.999999999999928) + testAngularVelocity(flywheel, 11.544961053811264) + testAngularAcceleration(flywheel, 3.999999999999933) flywheel.pushValue(0.067881525062373) // Datapoint 27 testDeltaTime(flywheel, 0.089323823233014) // Values from Datapoint 15 are now passsing through testSpinningTime(flywheel, 1.975564086685833) testAngularPosition(flywheel, 15.707963267948964) testAngularVelocity(flywheel, 11.902256346743307) - testAngularAcceleration(flywheel, 3.999999999999916) + testAngularAcceleration(flywheel, 3.9999999999999245) flywheel.pushValue(0.066717311088441) // Datapoint 28 testDeltaTime(flywheel, 0.086719441920360) // Values from Datapoint 16 are now passsing through testSpinningTime(flywheel, 2.062283528606193) testAngularPosition(flywheel, 16.755160819145562) testAngularVelocity(flywheel, 12.249134114424734) - testAngularAcceleration(flywheel, 3.999999999999907) + testAngularAcceleration(flywheel, 3.9999999999999245) flywheel.pushValue(0.065611019694526) // Datapoint 29 testDeltaTime(flywheel, 0.084330395149166) // Values from Datapoint 17 are now passsing through testSpinningTime(flywheel, 2.1466139237553588) testAngularPosition(flywheel, 17.80235837034216) - testAngularVelocity(flywheel, 12.586455695021382) - testAngularAcceleration(flywheel, 3.999999999999908) + testAngularVelocity(flywheel, 12.586455695021384) + testAngularAcceleration(flywheel, 3.9999999999999396) flywheel.pushValue(0.064558001484125) // Datapoint 30 testDeltaTime(flywheel, 0.082128549835466) // Values from Datapoint 18 are now passsing through testSpinningTime(flywheel, 2.228742473590825) testAngularPosition(flywheel, 18.84955592153876) - testAngularVelocity(flywheel, 12.914969894363228) - testAngularAcceleration(flywheel, 3.9999999999999165) + testAngularVelocity(flywheel, 12.914969894363232) + testAngularAcceleration(flywheel, 3.9999999999999574) flywheel.pushValue(0.063554113352442) // Datapoint 31 testDeltaTime(flywheel, 0.080090664596669) // Values from Datapoint 19 are now passsing through testSpinningTime(flywheel, 2.308833138187494) testAngularPosition(flywheel, 19.896753472735355) - testAngularVelocity(flywheel, 13.235332552749883) - testAngularAcceleration(flywheel, 3.9999999999999303) + testAngularVelocity(flywheel, 13.235332552749886) + testAngularAcceleration(flywheel, 3.9999999999999867) }) /** - * Test of the integration of the underlying FullTSQuadraticEstimator object + * @description Test of the integration of the underlying FullTSQuadraticEstimator object * The data follows the function y = X^3 + 2 * x^2 + 4 * x * To test if multiple quadratic regressions can decently approximate a cubic function */ @@ -298,7 +305,7 @@ test('Test of correct algorithmic integration of FullTSQuadraticEstimator and Fl maximumTimeBetweenImpulses: 1, flankLength: 12, systematicErrorAgressiveness: 0, - systematicErrorMaximumChange: 1, + systematicErrorNumberOfDatapoints: 1, minimumStrokeQuality: 0.36, minimumForceBeforeStroke: 0, minimumRecoverySlope: 0.00070, @@ -385,126 +392,126 @@ test('Test of correct algorithmic integration of FullTSQuadraticEstimator and Fl testDeltaTime(flywheel, 0) // Values from Datapoint 0 are now passsing through testSpinningTime(flywheel, 0) testAngularPosition(flywheel, 0) - testAngularVelocity(flywheel, 3.161921856069136) // Theoretical value: 4 - testAngularAcceleration(flywheel, 7.251023549310242) // Theoretical value: 4 + testAngularVelocity(flywheel, 3.1619218560691382) // Theoretical value: 4 + testAngularAcceleration(flywheel, 7.251023549310239) // Theoretical value: 4 flywheel.pushValue(0.064914611722656) // Datapoint 13 testDeltaTime(flywheel, 0.231815755285445) // Values from Datapoint 1 are now passsing through testSpinningTime(flywheel, 0.231815755285445) testAngularPosition(flywheel, 1.0471975511965976) - testAngularVelocity(flywheel, 4.795017407170436) // Theoretical value: 5.088478654, error: -6,64% - testAngularAcceleration(flywheel, 7.324931550092111) // Theoretical value: 5.390894532, error: 38,46% + testAngularVelocity(flywheel, 4.7950174071704375) // Theoretical value: 5.088478654, error: -6,64% + testAngularAcceleration(flywheel, 7.324931550092107) // Theoretical value: 5.390894532, error: 38,46% flywheel.pushValue(0.061784830519864) // Datapoint 14 testDeltaTime(flywheel, 0.186170118209325) // Values from Datapoint 2 are now passsing through testSpinningTime(flywheel, 0.41798587349477) testAngularPosition(flywheel, 2.0943951023931953) testAngularVelocity(flywheel, 6.098616558470422) // Theoretical value: 6.196080065, error: -2,14% - testAngularAcceleration(flywheel, 7.656104494382673) // Theoretical value: 6.507915241, error: 18,21% + testAngularAcceleration(flywheel, 7.6561044943826655) // Theoretical value: 6.507915241, error: 18,21% flywheel.pushValue(0.058995265576639) // Datapoint 15 testDeltaTime(flywheel, 0.155673811324399) // Values from Datapoint 3 are now passsing through testSpinningTime(flywheel, 0.5736596848191691) testAngularPosition(flywheel, 3.141592653589793) - testAngularVelocity(flywheel, 7.26104514687623) // Theoretical value: 7.281895041, error: -0,79% - testAngularAcceleration(flywheel, 8.125127482273886) // Theoretical value: 7.441958109, error: 9,49% + testAngularVelocity(flywheel, 7.261045146876231) // Theoretical value: 7.281895041, error: -0,79% + testAngularAcceleration(flywheel, 8.125127482273879) // Theoretical value: 7.441958109, error: 9,49% flywheel.pushValue(0.056491331538715) // Datapoint 16 testDeltaTime(flywheel, 0.134264409859047) // Values from Datapoint 4 are now passsing through testSpinningTime(flywheel, 0.707924094678216) testAngularPosition(flywheel, 4.1887902047863905) - testAngularVelocity(flywheel, 8.335452316712823) // Theoretical value: 8.33516595, error: -0,42% - testAngularAcceleration(flywheel, 8.59108553240516) // Theoretical value: 8.247544568, error: 4,32% + testAngularVelocity(flywheel, 8.335452316712825) // Theoretical value: 8.33516595, error: -0,42% + testAngularAcceleration(flywheel, 8.591085532405152) // Theoretical value: 8.247544568, error: 4,32% flywheel.pushValue(0.054229670373632) // Datapoint 17 testDeltaTime(flywheel, 0.118490308292909) // Values from Datapoint 5 are now passsing through testSpinningTime(flywheel, 0.826414402971125) testAngularPosition(flywheel, 5.235987755982988) - testAngularVelocity(flywheel, 9.346198019520207) // Theoretical value: 9.354539908, error: -0,44% - testAngularAcceleration(flywheel, 9.058162877855928) // Theoretical value: 8.958486418, error: 1,06% + testAngularVelocity(flywheel, 9.346198019520214) // Theoretical value: 9.354539908, error: -0,44% + testAngularAcceleration(flywheel, 9.058162877855903) // Theoretical value: 8.958486418, error: 1,06% flywheel.pushValue(0.052175392433679) // Datapoint 18 testDeltaTime(flywheel, 0.106396192260267) // Values from Datapoint 6 are now passsing through testSpinningTime(flywheel, 0.932810595231392) testAngularPosition(flywheel, 6.283185307179586) - testAngularVelocity(flywheel, 10.314972131734729) // Theoretical value: 10.3416492, error: -0,56% - testAngularAcceleration(flywheel, 9.531782371110216) // Theoretical value: 9.596863571, error: -0,95% + testAngularVelocity(flywheel, 10.314972131734738) // Theoretical value: 10.3416492, error: -0,56% + testAngularAcceleration(flywheel, 9.531782371110172) // Theoretical value: 9.596863571, error: -0,95% flywheel.pushValue(0.05030009417797) // Datapoint 19 testDeltaTime(flywheel, 0.096822693623239) // Values from Datapoint 7 are now passsing through testSpinningTime(flywheel, 1.029633288854631) testAngularPosition(flywheel, 7.330382858376184) - testAngularVelocity(flywheel, 11.25302645243177) // Theoretical value: 11.29896728, error: -0,68% - testAngularAcceleration(flywheel, 10.006689891934789) // Theoretical value: 10.17779973, error: -2,15% + testAngularVelocity(flywheel, 11.253026452431792) // Theoretical value: 11.29896728, error: -0,68% + testAngularAcceleration(flywheel, 10.006689891934712) // Theoretical value: 10.17779973, error: -2,15% flywheel.pushValue(0.04858040892819) // Datapoint 20 testDeltaTime(flywheel, 0.08904704613513) // Values from Datapoint 8 are now passsing through testSpinningTime(flywheel, 1.118680334989761) testAngularPosition(flywheel, 8.377580409572781) - testAngularVelocity(flywheel, 12.167114512288885) // Theoretical value: 12.22905842, error: -0,76% - testAngularAcceleration(flywheel, 10.479926499860365) // Theoretical value: 10.71208201, error: -2,78% + testAngularVelocity(flywheel, 12.167114512288897) // Theoretical value: 12.22905842, error: -0,76% + testAngularAcceleration(flywheel, 10.479926499860289) // Theoretical value: 10.71208201, error: -2,78% flywheel.pushValue(0.046996930546829) // Datapoint 21 testDeltaTime(flywheel, 0.08259777558252) // Values from Datapoint 9 are now passsing through testSpinningTime(flywheel, 1.201278110572281) testAngularPosition(flywheel, 9.42477796076938) - testAngularVelocity(flywheel, 13.06228935387465) // Theoretical value: 13.13431974, error: -0,79% - testAngularAcceleration(flywheel, 10.945741904208703) // Theoretical value: 11.20766866, error: -3,03% + testAngularVelocity(flywheel, 13.062289353874645) // Theoretical value: 13.13431974, error: -0,79% + testAngularAcceleration(flywheel, 10.945741904208647) // Theoretical value: 11.20766866, error: -3,03% flywheel.pushValue(0.045533402601137) // Datapoint 22 testDeltaTime(flywheel, 0.077155055952201) // Values from Datapoint 10 are now passsing through testSpinningTime(flywheel, 1.278433166524482) testAngularPosition(flywheel, 10.471975511965976) - testAngularVelocity(flywheel, 13.940750925066359) // Theoretical value: 14.01690675, error: -0,78% - testAngularAcceleration(flywheel, 11.403650671998252) // Theoretical value: 11.670599, error: -2,98% + testAngularVelocity(flywheel, 13.94075092506632) // Theoretical value: 14.01690675, error: -0,78% + testAngularAcceleration(flywheel, 11.403650671998298) // Theoretical value: 11.670599, error: -2,98% flywheel.pushValue(0.044176099545603) // Datapoint 23 testDeltaTime(flywheel, 0.072494552013330) // Values from Datapoint 11 are now passsing through testSpinningTime(flywheel, 1.350927718537812) testAngularPosition(flywheel, 11.519173063162574) - testAngularVelocity(flywheel, 14.806694981766503) // Theoretical value: 14.87872798, error: -0,69% - testAngularAcceleration(flywheel, 11.85668968195581) // Theoretical value: 12.10556631, error: -2,69% + testAngularVelocity(flywheel, 14.80669498176648) // Theoretical value: 14.87872798, error: -0,69% + testAngularAcceleration(flywheel, 11.856689681955814) // Theoretical value: 12.10556631, error: -2,69% flywheel.pushValue(0.042913348809906) // Datapoint 24 testDeltaTime(flywheel, 0.068454336759262) // Values from Datapoint 12 are now passsing through testSpinningTime(flywheel, 1.419382055297074) testAngularPosition(flywheel, 12.566370614359172) - testAngularVelocity(flywheel, 15.65933144364903) // Theoretical value: 15.72146448, error: -0,57% - testAngularAcceleration(flywheel, 12.303309060001375) // Theoretical value: 12.51629233, error: -2,22% + testAngularVelocity(flywheel, 15.659331443649155) // Theoretical value: 15.72146448, error: -0,57% + testAngularAcceleration(flywheel, 12.303309060000915) // Theoretical value: 12.51629233, error: -2,22% flywheel.pushValue(0.041735157665124) // Datapoint 25 testDeltaTime(flywheel, 0.064914611722656) // Values from Datapoint 13 are now passsing through, so we cleared all startup noise testSpinningTime(flywheel, 1.484296667019730) testAngularPosition(flywheel, 13.613568165555769) - testAngularVelocity(flywheel, 16.49273676896862) // Theoretical value: 16.54659646, error: -0,47% - testAngularAcceleration(flywheel, 12.721354618621646) // Theoretical value: 12.90578, error: -1,86% + testAngularVelocity(flywheel, 16.492736768968747) // Theoretical value: 16.54659646, error: -0,47% + testAngularAcceleration(flywheel, 12.721354618621062) // Theoretical value: 12.90578, error: -1,86% flywheel.pushValue(0.040632918960300) // Datapoint 26 testDeltaTime(flywheel, 0.061784830519864) // Values from Datapoint 14 are now passsing through testSpinningTime(flywheel, 1.546081497539594) testAngularPosition(flywheel, 14.660765716752367) - testAngularVelocity(flywheel, 17.30769121071966) // Theoretical value: 17.35542998, error: -0,40% - testAngularAcceleration(flywheel, 13.113972550977003) // Theoretical value: 13.27648899, error: -1,59% + testAngularVelocity(flywheel, 17.307691210719753) // Theoretical value: 17.35542998, error: -0,40% + testAngularAcceleration(flywheel, 13.11397255097641) // Theoretical value: 13.27648899, error: -1,59% flywheel.pushValue(0.039599176898486) // Datapoint 27 testDeltaTime(flywheel, 0.058995265576639) // Values from Datapoint 15 are now passsing through testSpinningTime(flywheel, 1.605076763116233) testAngularPosition(flywheel, 15.707963267948964) - testAngularVelocity(flywheel, 18.106493986724598) // Theoretical value: 18.1491213, error: -0,34% - testAngularAcceleration(flywheel, 13.486098587072432) // Theoretical value: 13.63046058, error: -1,38% + testAngularVelocity(flywheel, 18.10649398672465) // Theoretical value: 18.1491213, error: -0,34% + testAngularAcceleration(flywheel, 13.486098587071863) // Theoretical value: 13.63046058, error: -1,38% flywheel.pushValue(0.038627438996519) // Datapoint 28 testDeltaTime(flywheel, 0.056491331538715) // Values from Datapoint 16 are now passsing through testSpinningTime(flywheel, 1.661568094654948) testAngularPosition(flywheel, 16.755160819145562) - testAngularVelocity(flywheel, 18.89042654239583) // Theoretical value: 18.92869798, error: -0,29% - testAngularAcceleration(flywheel, 13.840428977172227) // Theoretical value: 13.96940857, error: -1,20% + testAngularVelocity(flywheel, 18.890426542395847) // Theoretical value: 18.92869798, error: -0,29% + testAngularAcceleration(flywheel, 13.840428977171639) // Theoretical value: 13.96940857, error: -1,20% flywheel.pushValue(0.037712023914259) // Datapoint 29 testDeltaTime(flywheel, 0.054229670373632) // Values from Datapoint 17 are now passsing through testSpinningTime(flywheel, 1.715797765028580) testAngularPosition(flywheel, 17.80235837034216) - testAngularVelocity(flywheel, 19.660398675998643) // Theoretical value: 19.69507697, error: -0,26% - testAngularAcceleration(flywheel, 14.178743620220319) // Theoretical value: 14.29478659, error: -1,06% + testAngularVelocity(flywheel, 19.660398675998614) // Theoretical value: 19.69507697, error: -0,26% + testAngularAcceleration(flywheel, 14.178743620219855) // Theoretical value: 14.29478659, error: -1,06% flywheel.pushValue(0.036847937394809) // Datapoint 30 testDeltaTime(flywheel, 0.052175392433679) // Values from Datapoint 18 are now passsing through testSpinningTime(flywheel, 1.767973157462259) testAngularPosition(flywheel, 18.84955592153876) - testAngularVelocity(flywheel, 20.41744737019348) // Theoretical value: 20.44907989, error: -0,23% - testAngularAcceleration(flywheel, 14.502790132816703) // Theoretical value: 14.60783894, error: -0,94% + testAngularVelocity(flywheel, 20.41744737019342) // Theoretical value: 20.44907989, error: -0,23% + testAngularAcceleration(flywheel, 14.502790132816358) // Theoretical value: 14.60783894, error: -0,94% flywheel.pushValue(0.036030770419579) // Datapoint 31 testDeltaTime(flywheel, 0.05030009417797) // Values from Datapoint 19 are now passsing through testSpinningTime(flywheel, 1.8182732516402291) testAngularPosition(flywheel, 19.896753472735355) - testAngularVelocity(flywheel, 21.162376267362447) // Theoretical value: 21.19144586, error: -0,20% - testAngularAcceleration(flywheel, 14.813903373334773) // Theoretical value: 14.90963951, error: -0,83% + testAngularVelocity(flywheel, 21.162376267362376) // Theoretical value: 21.19144586, error: -0,20% + testAngularAcceleration(flywheel, 14.813903373334538) // Theoretical value: 14.90963951, error: -0,83% }) /** - * Test of the integration of the underlying FullTSQuadraticEstimator object + * @description Test of the integration of the underlying FullTSQuadraticEstimator object * The data follows the function y = X^3 + 2 * x^2 + 4 * x with a +/-0.0001 sec injected noise in currentDt * To test if multiple quadratic regressions can decently approximate a cubic function with noise * Please note: theoretical values are based on the perfect function (i.e. without noise) @@ -522,7 +529,7 @@ test('Test of correct algorithmic integration of FullTSQuadraticEstimator and Fl maximumTimeBetweenImpulses: 1, flankLength: 12, systematicErrorAgressiveness: 0, - systematicErrorMaximumChange: 1, + systematicErrorNumberOfDatapoints: 1, minimumStrokeQuality: 0.36, minimumForceBeforeStroke: 0, minimumRecoverySlope: 0.00070, @@ -609,125 +616,131 @@ test('Test of correct algorithmic integration of FullTSQuadraticEstimator and Fl testDeltaTime(flywheel, 0) // Values from Datapoint 0 are now passsing through testSpinningTime(flywheel, 0) testAngularPosition(flywheel, 0) - testAngularVelocity(flywheel, 3.1651252708296953) // Theoretical value: 4 + testAngularVelocity(flywheel, 3.1651252708296993) // Theoretical value: 4 testAngularAcceleration(flywheel, 7.2468812808500696) // Theoretical value: 4 flywheel.pushValue(0.065014611722656) // Datapoint 13 testDeltaTime(flywheel, 0.231915755285445) // Values from Datapoint 1 are now passsing through testSpinningTime(flywheel, 0.231915755285445) testAngularPosition(flywheel, 1.0471975511965976) - testAngularVelocity(flywheel, 4.798189682557517) // Theoretical value: 5.088478654, error: -6.58% - testAngularAcceleration(flywheel, 7.32078401292801) // Theoretical value: 5.390894532, error: 38.38% + testAngularVelocity(flywheel, 4.7981896825575205) // Theoretical value: 5.088478654, error: -6.58% + testAngularAcceleration(flywheel, 7.320784012928006) // Theoretical value: 5.390894532, error: 38.38% flywheel.pushValue(0.061684830519864) // Datapoint 14 testDeltaTime(flywheel, 0.186070118209325) // Values from Datapoint 2 are now passsing through testSpinningTime(flywheel, 0.41798587349477) testAngularPosition(flywheel, 2.0943951023931953) - testAngularVelocity(flywheel, 6.100352571838147) // Theoretical value: 6.196080065, error: -2.11% - testAngularAcceleration(flywheel, 7.650380140052499) // Theoretical value: 6.507915241, error: 18.14% + testAngularVelocity(flywheel, 6.100352571838149) // Theoretical value: 6.196080065, error: -2.11% + testAngularAcceleration(flywheel, 7.650380140052492) // Theoretical value: 6.507915241, error: 18.14% flywheel.pushValue(0.059095265576639) // Datapoint 15 testDeltaTime(flywheel, 0.155773811324398) // Values from Datapoint 3 are now passsing through testSpinningTime(flywheel, 0.573759684819168) testAngularPosition(flywheel, 3.141592653589793) - testAngularVelocity(flywheel, 7.262664999379815) // Theoretical value: 7.281895041, error: -0.77% - testAngularAcceleration(flywheel, 8.117964017032834) // Theoretical value: 7.441958109, error: 9.40% + testAngularVelocity(flywheel, 7.262664999379819) // Theoretical value: 7.281895041, error: -0.77% + testAngularAcceleration(flywheel, 8.117964017032822) // Theoretical value: 7.441958109, error: 9.40% flywheel.pushValue(0.056391331538715) // Datapoint 16 testDeltaTime(flywheel, 0.134164409859047) // Values from Datapoint 4 are now passsing through testSpinningTime(flywheel, 0.7079240946782149) testAngularPosition(flywheel, 4.1887902047863905) - testAngularVelocity(flywheel, 8.33567148791435) // Theoretical value: 8.33516595, error: -0.42% - testAngularAcceleration(flywheel, 8.584272213871065) // Theoretical value: 8.247544568, error: 4.24% + testAngularVelocity(flywheel, 8.335671487914347) // Theoretical value: 8.33516595, error: -0.42% + testAngularAcceleration(flywheel, 8.58427221387106) // Theoretical value: 8.247544568, error: 4.24% flywheel.pushValue(0.054329670373632) // Datapoint 17 testDeltaTime(flywheel, 0.118590308292909) // Values from Datapoint 5 are now passsing through testSpinningTime(flywheel, 0.8265144029711239) testAngularPosition(flywheel, 5.235987755982988) - testAngularVelocity(flywheel, 9.3471099262632) // Theoretical value: 9.354539908, error: -0.44% - testAngularAcceleration(flywheel, 9.052626876076237) // Theoretical value: 8.958486418, error: 1.00% + testAngularVelocity(flywheel, 9.347109926263196) // Theoretical value: 9.354539908, error: -0.44% + testAngularAcceleration(flywheel, 9.052626876076234) // Theoretical value: 8.958486418, error: 1.00% flywheel.pushValue(0.052075392433679) // Datapoint 18 testDeltaTime(flywheel, 0.106296192260267) // Values from Datapoint 6 are now passsing through testSpinningTime(flywheel, 0.9328105952313909) testAngularPosition(flywheel, 6.283185307179586) - testAngularVelocity(flywheel, 10.31470693514443) // Theoretical value: 10.3416492, error: -0.56% - testAngularAcceleration(flywheel, 9.526534690784072) // Theoretical value: 9.596863571, error: -1.00% + testAngularVelocity(flywheel, 10.314706935144432) // Theoretical value: 10.3416492, error: -0.56% + testAngularAcceleration(flywheel, 9.52653469078407) // Theoretical value: 9.596863571, error: -1.00% flywheel.pushValue(0.05040009417797) // Datapoint 19 testDeltaTime(flywheel, 0.096922693623239) // Values from Datapoint 7 are now passsing through testSpinningTime(flywheel, 1.0297332888546298) testAngularPosition(flywheel, 7.330382858376184) - testAngularVelocity(flywheel, 11.25365342146104) // Theoretical value: 11.29896728, error: -0.67% - testAngularAcceleration(flywheel, 10.001358612662708) // Theoretical value: 10.17779973, error: -2.21% + testAngularVelocity(flywheel, 11.253653421461035) // Theoretical value: 11.29896728, error: -0.67% + testAngularAcceleration(flywheel, 10.001358612662711) // Theoretical value: 10.17779973, error: -2.21% flywheel.pushValue(0.04848040892819) // Datapoint 20 testDeltaTime(flywheel, 0.08894704613513) // Values from Datapoint 8 are now passsing through testSpinningTime(flywheel, 1.1186803349897598) testAngularPosition(flywheel, 8.377580409572781) - testAngularVelocity(flywheel, 12.1667674474633) // Theoretical value: 12.22905842, error: -0.76% - testAngularAcceleration(flywheel, 10.47394441606816) // Theoretical value: 10.71208201, error: -2.84% + testAngularVelocity(flywheel, 12.166767447463288) // Theoretical value: 12.22905842, error: -0.76% + testAngularAcceleration(flywheel, 10.47394441606818) // Theoretical value: 10.71208201, error: -2.84% flywheel.pushValue(0.047096930546829) // Datapoint 21 testDeltaTime(flywheel, 0.08269777558252) // Values from Datapoint 9 are now passsing through testSpinningTime(flywheel, 1.2013781105722798) testAngularPosition(flywheel, 9.42477796076938) - testAngularVelocity(flywheel, 13.062997567333905) // Theoretical value: 13.13431974, error: -0.79% - testAngularAcceleration(flywheel, 10.940063240068048) // Theoretical value: 11.20766866, error: -3.08% + testAngularVelocity(flywheel, 13.062997567333893) // Theoretical value: 13.13431974, error: -0.79% + testAngularAcceleration(flywheel, 10.940063240068076) // Theoretical value: 11.20766866, error: -3.08% flywheel.pushValue(0.045433402601137) // Datapoint 22 testDeltaTime(flywheel, 0.077055055952201) // Values from Datapoint 10 are now passsing through testSpinningTime(flywheel, 1.2784331665244808) testAngularPosition(flywheel, 10.471975511965976) - testAngularVelocity(flywheel, 13.940480188006532) // Theoretical value: 14.01690675, error: -0.78% - testAngularAcceleration(flywheel, 11.397389413208403) // Theoretical value: 11.670599, error: -3.04% + testAngularVelocity(flywheel, 13.940480188006552) // Theoretical value: 14.01690675, error: -0.78% + testAngularAcceleration(flywheel, 11.397389413208364) // Theoretical value: 11.670599, error: -3.04% flywheel.pushValue(0.044276099545603) // Datapoint 23 testDeltaTime(flywheel, 0.07259455201333) // Values from Datapoint 11 are now passsing through testSpinningTime(flywheel, 1.3510277185378108) testAngularPosition(flywheel, 11.519173063162574) - testAngularVelocity(flywheel, 14.807840698982407) // Theoretical value: 14.87872798, error: -0.68% - testAngularAcceleration(flywheel, 11.848780564150392) // Theoretical value: 12.10556631, error: -2.76% + testAngularVelocity(flywheel, 14.807840698982423) // Theoretical value: 14.87872798, error: -0.68% + testAngularAcceleration(flywheel, 11.848780564150369) // Theoretical value: 12.10556631, error: -2.76% flywheel.pushValue(0.042813348809906) // Datapoint 24 testDeltaTime(flywheel, 0.068354336759262) // Values from Datapoint 12 are now passsing through testSpinningTime(flywheel, 1.4193820552970728) testAngularPosition(flywheel, 12.566370614359172) - testAngularVelocity(flywheel, 15.659177267217961) // Theoretical value: 15.72146448, error: -0.57% - testAngularAcceleration(flywheel, 12.293943915780241) // Theoretical value: 12.51629233, error: -2.30% + testAngularVelocity(flywheel, 15.65917726721796) // Theoretical value: 15.72146448, error: -0.57% + testAngularAcceleration(flywheel, 12.293943915780252) // Theoretical value: 12.51629233, error: -2.30% flywheel.pushValue(0.041835157665124) // Datapoint 25 testDeltaTime(flywheel, 0.065014611722656) // Values from Datapoint 13 are now passsing through, so we cleared all startup noise testSpinningTime(flywheel, 1.4843966670197288) testAngularPosition(flywheel, 13.613568165555769) - testAngularVelocity(flywheel, 16.494472505376102) // Theoretical value: 16.54659646, error: -0.46% - testAngularAcceleration(flywheel, 12.710407075508463) // Theoretical value: 12.90578, error: -1.95% + testAngularVelocity(flywheel, 16.49447250537608) // Theoretical value: 16.54659646, error: -0.46% + testAngularAcceleration(flywheel, 12.710407075508567) // Theoretical value: 12.90578, error: -1.95% flywheel.pushValue(0.040532918960300) // Datapoint 26 testDeltaTime(flywheel, 0.061684830519864) // Values from Datapoint 14 are now passsing through testSpinningTime(flywheel, 1.546081497539593) testAngularPosition(flywheel, 14.660765716752367) - testAngularVelocity(flywheel, 17.308891329044478) // Theoretical value: 17.35542998, error: -0.39% - testAngularAcceleration(flywheel, 13.100466914875762) // Theoretical value: 13.27648899, error: -1.70% + testAngularVelocity(flywheel, 17.308891329044464) // Theoretical value: 17.35542998, error: -0.39% + testAngularAcceleration(flywheel, 13.100466914875906) // Theoretical value: 13.27648899, error: -1.70% flywheel.pushValue(0.039699176898486) // Datapoint 27 testDeltaTime(flywheel, 0.059095265576639) // Values from Datapoint 15 are now passsing through testSpinningTime(flywheel, 1.605176763116232) testAngularPosition(flywheel, 15.707963267948964) - testAngularVelocity(flywheel, 18.10970282977479) // Theoretical value: 18.1491213, error: -0.32% - testAngularAcceleration(flywheel, 13.469377816872042) // Theoretical value: 13.63046058, error: -1.51% + testAngularVelocity(flywheel, 18.109702829774772) // Theoretical value: 18.1491213, error: -0.32% + testAngularAcceleration(flywheel, 13.469377816872242) // Theoretical value: 13.63046058, error: -1.51% flywheel.pushValue(0.038527438996519) // Datapoint 28 testDeltaTime(flywheel, 0.056391331538715) // Values from Datapoint 16 are now passsing through testSpinningTime(flywheel, 1.661568094654947) testAngularPosition(flywheel, 16.755160819145562) - testAngularVelocity(flywheel, 18.892749084779716) // Theoretical value: 18.92869798, error: -0.28% - testAngularAcceleration(flywheel, 13.819955339923922) // Theoretical value: 13.96940857, error: -1.35% + testAngularVelocity(flywheel, 18.892749084779705) // Theoretical value: 18.92869798, error: -0.28% + testAngularAcceleration(flywheel, 13.819955339924142) // Theoretical value: 13.96940857, error: -1.35% flywheel.pushValue(0.037812023914259) // Datapoint 29 testDeltaTime(flywheel, 0.054329670373632) // Values from Datapoint 17 are now passsing through testSpinningTime(flywheel, 1.715897765028579) testAngularPosition(flywheel, 17.80235837034216) - testAngularVelocity(flywheel, 19.664430174199467) // Theoretical value: 19.69507697, error: -0.24% - testAngularAcceleration(flywheel, 14.154531841302589) // Theoretical value: 14.29478659, error: -1.23% + testAngularVelocity(flywheel, 19.664430174199474) // Theoretical value: 19.69507697, error: -0.24% + testAngularAcceleration(flywheel, 14.154531841302834) // Theoretical value: 14.29478659, error: -1.23% flywheel.pushValue(0.036747937394809) // Datapoint 30 testDeltaTime(flywheel, 0.052075392433679) // Values from Datapoint 18 are now passsing through testSpinningTime(flywheel, 1.767973157462258) testAngularPosition(flywheel, 18.84955592153876) - testAngularVelocity(flywheel, 20.419916102229323) // Theoretical value: 20.44907989, error: -0.21% - testAngularAcceleration(flywheel, 14.474639639378722) // Theoretical value: 14.60783894, error: -1.13% + testAngularVelocity(flywheel, 20.419916102229333) // Theoretical value: 20.44907989, error: -0.21% + testAngularAcceleration(flywheel, 14.474639639378996) // Theoretical value: 14.60783894, error: -1.13% flywheel.pushValue(0.036130770419579) // Datapoint 31 testDeltaTime(flywheel, 0.05040009417797) // Values from Datapoint 19 are now passsing through testSpinningTime(flywheel, 1.818373251640228) testAngularPosition(flywheel, 19.896753472735355) - testAngularVelocity(flywheel, 21.166541683421826) // Theoretical value: 21.19144586, error: -0.18% - testAngularAcceleration(flywheel, 14.782028789603572) // Theoretical value: 14.90963951, error: -1.05% + testAngularVelocity(flywheel, 21.16654168342182) // Theoretical value: 21.19144586, error: -0.18% + testAngularAcceleration(flywheel, 14.782028789603949) // Theoretical value: 14.90963951, error: -1.05% }) -// Test behaviour for perfect stroke +/** + * @todo Test behaviour with noise CEC filter active + */ + +/** + * @description Test behaviour for perfect stroke + */ test('Correct Flywheel behaviour for a noisefree stroke', () => { const flywheel = createFlywheel(baseConfig) flywheel.maintainStateAndMetrics() @@ -764,8 +777,8 @@ test('Correct Flywheel behaviour for a noisefree stroke', () => { testSpinningTime(flywheel, 0.077918634) testAngularPosition(flywheel, 7.330382858376184) testAngularVelocity(flywheel, 94.88636656676766) - testAngularAcceleration(flywheel, 28.483961147947365) - testTorque(flywheel, 3.927072875980104) + testAngularAcceleration(flywheel, 28.483961147946758) + testTorque(flywheel, 3.9270728759800413) testDragFactor(flywheel, 0.00011) testIsDwelling(flywheel, false) testIsUnpowered(flywheel, false) @@ -788,9 +801,9 @@ test('Correct Flywheel behaviour for a noisefree stroke', () => { testDeltaTime(flywheel, 0.010722165) testSpinningTime(flywheel, 0.23894732900000007) testAngularPosition(flywheel, 23.03834612632515) - testAngularVelocity(flywheel, 97.06865123831885) - testAngularAcceleration(flywheel, -32.75873752642775) - testTorque(flywheel, -2.340970303119799) + testAngularVelocity(flywheel, 97.06865123831865) + testAngularAcceleration(flywheel, -32.75873752642214) + testTorque(flywheel, -2.340970303119225) testDragFactor(flywheel, 0.00011) testIsDwelling(flywheel, false) testIsUnpowered(flywheel, true) @@ -813,26 +826,38 @@ test('Correct Flywheel behaviour for a noisefree stroke', () => { testDeltaTime(flywheel, 0.020722165) testSpinningTime(flywheel, 0.43343548300000007) testAngularPosition(flywheel, 38.746309394274114) - testAngularVelocity(flywheel, 50.97532159524012) - testAngularAcceleration(flywheel, -157.76768934416657) - testTorque(flywheel, -15.980015596092377) + testAngularVelocity(flywheel, 50.975321595240146) + testAngularAcceleration(flywheel, -157.76768934416432) + testTorque(flywheel, -15.980015596092146) testDragFactor(flywheel, 0.00011) testIsDwelling(flywheel, true) testIsUnpowered(flywheel, true) testIsPowered(flywheel, false) }) -// Test behaviour for noisy upgoing flank +/** + * @todo Test behaviour for noisy upgoing flank + */ -// Test behaviour for noisy downgoing flank +/** + * @todo Test behaviour for noisy downgoing flank + */ -// Test behaviour for noisy stroke +/** + * @todo Test behaviour for noisy stroke + */ -// Test drag factor calculation +/** + * @todo Test drag factor calculation + */ -// Test Dynamic stroke detection +/** + * @todo Test Dynamic stroke detection + */ -// Test behaviour for not maintaining metrics +/** + * @description Test behaviour for not maintaining metrics + */ test('Correct Flywheel behaviour at maintainStateOnly', () => { const flywheel = createFlywheel(baseConfig) flywheel.maintainStateAndMetrics() @@ -903,6 +928,9 @@ test('Correct Flywheel behaviour at maintainStateOnly', () => { testIsPowered(flywheel, false) }) +/** + * @description Test behaviour for the WRX700 + */ test('Correct Flywheel behaviour with a SportsTech WRX700', async () => { const flywheel = createFlywheel(deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Sportstech_WRX700)) testSpinningTime(flywheel, 0) @@ -918,6 +946,9 @@ test('Correct Flywheel behaviour with a SportsTech WRX700', async () => { testDragFactor(flywheel, (rowerProfiles.Sportstech_WRX700.dragFactor / 1000000)) }) +/** + * @description Test behaviour for the DKN R-320 + */ test('Correct Flywheel behaviour with a DKN R-320', async () => { const flywheel = createFlywheel(deepMerge(rowerProfiles.DEFAULT, rowerProfiles.DKN_R320)) @@ -934,6 +965,9 @@ test('Correct Flywheel behaviour with a DKN R-320', async () => { testDragFactor(flywheel, (rowerProfiles.DKN_R320.dragFactor / 1000000)) }) +/** + * @description Test behaviour for the NordicTrack RX800 + */ test('Correct Flywheel behaviour with a NordicTrack RX800', async () => { const flywheel = createFlywheel(deepMerge(rowerProfiles.DEFAULT, rowerProfiles.NordicTrack_RX800)) testSpinningTime(flywheel, 0) @@ -950,6 +984,9 @@ test('Correct Flywheel behaviour with a NordicTrack RX800', async () => { testDragFactor(flywheel, (rowerProfiles.NordicTrack_RX800.dragFactor / 1000000)) }) +/** + * @description Test behaviour for the SportsTech WRX700 + */ test('Correct Flywheel behaviour with a full session on a SportsTech WRX700', async () => { const flywheel = createFlywheel(deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Sportstech_WRX700)) testSpinningTime(flywheel, 0) @@ -966,6 +1003,9 @@ test('Correct Flywheel behaviour with a full session on a SportsTech WRX700', as testDragFactor(flywheel, (rowerProfiles.Sportstech_WRX700.dragFactor / 1000000)) }) +/** + * @description Test behaviour for the C2 Model C + */ test('A full session for a Concept2 Model C should produce plausible results', async () => { const flywheel = createFlywheel(deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Concept2_Model_C)) testSpinningTime(flywheel, 0) @@ -981,6 +1021,9 @@ test('A full session for a Concept2 Model C should produce plausible results', a testDragFactor(flywheel, (rowerProfiles.Concept2_Model_C.dragFactor / 1000000)) }) +/** + * @description Test behaviour for the C2 RowErg + */ test('A full session for a Concept2 RowErg should produce plausible results', async () => { const flywheel = createFlywheel(deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Concept2_RowErg)) testSpinningTime(flywheel, 0) @@ -996,7 +1039,9 @@ test('A full session for a Concept2 RowErg should produce plausible results', as testDragFactor(flywheel, (rowerProfiles.Concept2_RowErg.dragFactor / 1000000)) }) -// Test behaviour after reset +/** + * @todo Test behaviour after reset + */ function testDeltaTime (flywheel, expectedValue) { assert.ok(flywheel.deltaTime() === expectedValue, `deltaTime should be ${expectedValue} sec at ${flywheel.spinningTime()} sec, is ${flywheel.deltaTime()}`) diff --git a/app/engine/Rower.test.js b/app/engine/Rower.test.js index 03018bb3d2..b1019ec77d 100644 --- a/app/engine/Rower.test.js +++ b/app/engine/Rower.test.js @@ -1,9 +1,8 @@ 'use strict' -/* - Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor -*/ /** - * This test is a test of the Rower object, that tests wether this object fills all fields correctly, given one validated rower, (the + * @copyright [OpenRowingMonitor]{@link https://github.com/JaapvanEkris/openrowingmonitor} + * + * @file This test is a test of the Rower object, that tests wether this object fills all fields correctly, given one validated rower, (the * Concept2 RowErg) using a validated cycle of strokes. This thoroughly tests the raw physics of the translation of Angular physics * to Linear physics. The combination with all possible known rowers is tested when testing the above function RowingStatistics, as * these statistics are dependent on these settings as well. @@ -40,7 +39,9 @@ const baseConfig = { // Based on Concept 2 settings, as this is the validation s magicConstant: 2.8 } -// Test behaviour for no datapoints +/** + * @description Test behaviour for no datapoints + */ test('Correct rower behaviour at initialisation', () => { const rower = createRower(baseConfig) testStrokeState(rower, 'WaitingForDrive') @@ -61,9 +62,13 @@ test('Correct rower behaviour at initialisation', () => { testInstantHandlePower(rower, 0) }) -// Test behaviour for one datapoint +/** + * @todo Test behaviour for one datapoint + */ -// Test behaviour for three perfect identical strokes, including settingling behaviour of metrics +/** + * @description Test behaviour for three perfect identical strokes, including settingling behaviour of metrics + */ test('Test behaviour for three perfect identical strokes, including settingling behaviour of metrics', () => { const rower = createRower(baseConfig) testStrokeState(rower, 'WaitingForDrive') @@ -117,7 +122,7 @@ test('Test behaviour for three perfect identical strokes, including settingling testDrivePeakHandleForce(rower, undefined) testRecoveryDuration(rower, undefined) testRecoveryDragFactor(rower, undefined) - testInstantHandlePower(rower, 367.9769643691954) + testInstantHandlePower(rower, 367.97696436918955) // Recovery initial stroke starts here rower.handleRotationImpulse(0.010769) rower.handleRotationImpulse(0.010707554) @@ -145,8 +150,8 @@ test('Test behaviour for three perfect identical strokes, including settingling testDriveDuration(rower, 0.143485717) testDriveLinearDistance(rower, 0.4271903319416174) testDriveLength(rower, 0.1759291886010284) - testDriveAverageHandleForce(rower, 276.6342676838766) - testDrivePeakHandleForce(rower, 332.9918222212992) + testDriveAverageHandleForce(rower, 276.6342676838739) + testDrivePeakHandleForce(rower, 332.99182222129025) testRecoveryDuration(rower, undefined) testRecoveryDragFactor(rower, undefined) testInstantHandlePower(rower, 0) @@ -181,11 +186,11 @@ test('Test behaviour for three perfect identical strokes, including settingling testDriveDuration(rower, 0.143485717) testDriveLinearDistance(rower, 0.3895903211923076) testDriveLength(rower, 0.1759291886010284) - testDriveAverageHandleForce(rower, 236.9227932798214) - testDrivePeakHandleForce(rower, 378.6022382024728) + testDriveAverageHandleForce(rower, 236.92279327988305) + testDrivePeakHandleForce(rower, 378.60223820258005) testRecoveryDuration(rower, 0.21654112800000003) testRecoveryDragFactor(rower, 281.5961372923874) - testInstantHandlePower(rower, 502.73778232982295) + testInstantHandlePower(rower, 502.7377823299629) // Recovery second stroke starts here rower.handleRotationImpulse(0.010769) rower.handleRotationImpulse(0.010707554) @@ -213,8 +218,8 @@ test('Test behaviour for three perfect identical strokes, including settingling testDriveDuration(rower, 0.22872752900000004) testDriveLinearDistance(rower, 1.0226745931298076) testDriveLength(rower, 0.3078760800517996) - testDriveAverageHandleForce(rower, 288.45140756250663) - testDrivePeakHandleForce(rower, 447.10851434893794) + testDriveAverageHandleForce(rower, 288.45140756259053) + testDrivePeakHandleForce(rower, 447.108514349131) testRecoveryDuration(rower, 0.21654112800000003) testRecoveryDragFactor(rower, 281.5961372923874) testInstantHandlePower(rower, 0) @@ -249,11 +254,11 @@ test('Test behaviour for three perfect identical strokes, including settingling testDriveDuration(rower, 0.22872752900000004) testDriveLinearDistance(rower, 0.5843854817884615) testDriveLength(rower, 0.3078760800517996) - testDriveAverageHandleForce(rower, 192.2653879294713) - testDrivePeakHandleForce(rower, 378.6022382013243) + testDriveAverageHandleForce(rower, 192.2653879294337) + testDrivePeakHandleForce(rower, 378.6022382039591) testRecoveryDuration(rower, 0.09812447700000015) testRecoveryDragFactor(rower, 281.5961372923874) - testInstantHandlePower(rower, 502.7377823283394) + testInstantHandlePower(rower, 502.73778233173203) // Recovery third stroke starts here rower.handleRotationImpulse(0.010769) rower.handleRotationImpulse(0.010707554) @@ -281,8 +286,8 @@ test('Test behaviour for three perfect identical strokes, including settingling testDriveDuration(rower, 0.27311228700000023) testDriveLinearDistance(rower, 1.2174697537259611) testDriveLength(rower, 0.36651914291880905) - testDriveAverageHandleForce(rower, 256.5447026919334) - testDrivePeakHandleForce(rower, 447.1085143475723) + testDriveAverageHandleForce(rower, 256.5447026931294) + testDrivePeakHandleForce(rower, 447.1085143512751) testRecoveryDuration(rower, 0.09812447700000015) testRecoveryDragFactor(rower, 281.5961372923874) testInstantHandlePower(rower, 0) @@ -313,33 +318,28 @@ test('Test behaviour for three perfect identical strokes, including settingling testDriveDuration(rower, 0.27311228700000023) testDriveLinearDistance(rower, 1.2174697537259611) testDriveLength(rower, 0.36651914291880905) - testDriveAverageHandleForce(rower, 256.5447026919334) - testDrivePeakHandleForce(rower, 447.1085143475723) + testDriveAverageHandleForce(rower, 256.5447026931294) + testDrivePeakHandleForce(rower, 447.1085143512751) testRecoveryDuration(rower, 0.17448815399999995) testRecoveryDragFactor(rower, 281.5961372923874) testInstantHandlePower(rower, 0) }) -// Test behaviour for noisy upgoing flank - -// Test behaviour for noisy downgoing flank - -// Test behaviour for noisy stroke - -// Test behaviour after reset - -// Test behaviour for one datapoint - -// Test behaviour for noisy stroke - -// Test drag factor calculation - -// Test Dynamic stroke detection +/** + * @todo Test behaviour for noisy stroke + */ -// Test behaviour after reset +/** + * @todo Test behaviour after reset + */ -// Test behaviour with real-life data +/** + * @todo Test drag factor calculation + */ +/** + * @description Test behaviour for the Sportstech WRX700 + */ test('sample data for Sportstech WRX700 should produce plausible results', async () => { const rower = createRower(deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Sportstech_WRX700)) testTotalMovingTimeSinceStart(rower, 0) @@ -356,6 +356,9 @@ test('sample data for Sportstech WRX700 should produce plausible results', async testRecoveryDragFactor(rower, rowerProfiles.Sportstech_WRX700.dragFactor) }) +/** + * @description Test behaviour for the DKN R-320 + */ test('sample data for DKN R-320 should produce plausible results', async () => { const rower = createRower(deepMerge(rowerProfiles.DEFAULT, rowerProfiles.DKN_R320)) testTotalMovingTimeSinceStart(rower, 0) @@ -373,6 +376,9 @@ test('sample data for DKN R-320 should produce plausible results', async () => { testRecoveryDragFactor(rower, rowerProfiles.DKN_R320.dragFactor) }) +/** + * @description Test behaviour for the NordicTrack RX800 + */ test('sample data for NordicTrack RX800 should produce plausible results', async () => { const rower = createRower(deepMerge(rowerProfiles.DEFAULT, rowerProfiles.NordicTrack_RX800)) testTotalMovingTimeSinceStart(rower, 0) @@ -389,6 +395,9 @@ test('sample data for NordicTrack RX800 should produce plausible results', async testRecoveryDragFactor(rower, 493.8082148322739) }) +/** + * @description Test behaviour for the SportsTech WRX700 in a full session + */ test('A full session for SportsTech WRX700 should produce plausible results', async () => { const rower = createRower(deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Sportstech_WRX700)) testTotalMovingTimeSinceStart(rower, 0) @@ -405,6 +414,9 @@ test('A full session for SportsTech WRX700 should produce plausible results', as testRecoveryDragFactor(rower, rowerProfiles.Sportstech_WRX700.dragFactor) }) +/** + * @description Test behaviour for the C2 Model C + */ test('A full session for a Concept2 Model C should produce plausible results', async () => { const rower = createRower(deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Concept2_Model_C)) testTotalMovingTimeSinceStart(rower, 0) @@ -418,9 +430,12 @@ test('A full session for a Concept2 Model C should produce plausible results', a testTotalLinearDistanceSinceStart(rower, 552.2056895088467) testTotalNumberOfStrokes(rower, 83) // As dragFactor isn't static, it should have changed - testRecoveryDragFactor(rower, 123.64632740545652) + testRecoveryDragFactor(rower, 123.64632740545646) }) +/** + * @description Test behaviour for the C2 RowErg + */ test('A full session for a Concept2 RowErg should produce plausible results', async () => { const rower = createRower(deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Concept2_RowErg)) testTotalMovingTimeSinceStart(rower, 0) @@ -430,11 +445,11 @@ test('A full session for a Concept2 RowErg should produce plausible results', as await replayRowingSession(rower.handleRotationImpulse, { filename: 'recordings/Concept2_RowErg_Session_2000meters.csv', realtime: false, loop: false }) - testTotalMovingTimeSinceStart(rower, 590.0294331572366) - testTotalLinearDistanceSinceStart(rower, 2027.8951016561075) + testTotalMovingTimeSinceStart(rower, 590.0232672488145) + testTotalLinearDistanceSinceStart(rower, 2027.8404221844912) testTotalNumberOfStrokes(rower, 206) // As dragFactor isn't static, it should have changed - testRecoveryDragFactor(rower, 80.70650785533269) + testRecoveryDragFactor(rower, 80.70871681343775) }) function testStrokeState (rower, expectedValue) { diff --git a/app/engine/RowingStatistics.test.js b/app/engine/RowingStatistics.test.js index 841c9c6f6a..18867954b7 100644 --- a/app/engine/RowingStatistics.test.js +++ b/app/engine/RowingStatistics.test.js @@ -1,14 +1,10 @@ 'use strict' -/* - Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor -*/ /** - * This test is a test of the Rower object, that tests wether this object fills all fields correctly, given one validated rower, (the - * Concept2 RowErg) using a validated cycle of strokes. This thoroughly tests the raw physics of the translation of Angular physics - * to Linear physics. The combination with all possible known rowers is tested when testing the above function RowingStatistics, as - * these statistics are dependent on these settings as well. -*/ -// @ToDo: test the effects of smoothing parameters + * @copyright [OpenRowingMonitor]{@link https://github.com/JaapvanEkris/openrowingmonitor} + * + * @file This test is a test of the Rowingstatistics object, that tests wether this object fills all fields correctly, given one validated rower, (the + * Concept2 RowErg) using a validated cycle of strokes. The combination with all possible known rowers is tested. + */ import { test } from 'uvu' import * as assert from 'uvu/assert' import rowerProfiles from '../../config/rowerProfiles.js' @@ -34,7 +30,8 @@ const baseConfig = { minimumTimeBetweenImpulses: 0.005, maximumTimeBetweenImpulses: 0.017, flankLength: 12, - smoothing: 1, + systematicErrorAgressiveness: 0, + systematicErrorMaximumChange: 1, minimumStrokeQuality: 0.36, minimumForceBeforeStroke: 20, // Modification to standard settings to shorten test cases minimumRecoverySlope: 0.00070, @@ -47,7 +44,9 @@ const baseConfig = { } } -// Test behaviour for no datapoints +/** + * @description Test behaviour for no datapoints + */ test('Correct rower behaviour at initialisation', () => { const rowingStatistics = createRowingStatistics(baseConfig) testStrokeState(rowingStatistics, 'WaitingForDrive') @@ -68,9 +67,13 @@ test('Correct rower behaviour at initialisation', () => { testInstantHandlePower(rowingStatistics, undefined) }) -// Test behaviour for one datapoint +/** + * @todo Test behaviour for one datapoint + */ -// Test behaviour for three perfect identical strokes, including settingling behaviour of metrics +/** + * @description Test behaviour for three perfect identical strokes, including settingling behaviour of metrics + */ test('Test behaviour for three perfect identical strokes, including settingling behaviour of metrics', () => { const rowingStatistics = createRowingStatistics(baseConfig) testStrokeState(rowingStatistics, 'WaitingForDrive') @@ -152,8 +155,8 @@ test('Test behaviour for three perfect identical strokes, including settingling testDriveDuration(rowingStatistics, undefined) testDriveDistance(rowingStatistics, 0.4271903319416174) testDriveLength(rowingStatistics, 0.1759291886010284) - testDriveAverageHandleForce(rowingStatistics, 276.6342676838766) - testDrivePeakHandleForce(rowingStatistics, 332.9918222212992) + testDriveAverageHandleForce(rowingStatistics, 276.6342676838739) + testDrivePeakHandleForce(rowingStatistics, 332.99182222129025) testRecoveryDuration(rowingStatistics, undefined) testDragFactor(rowingStatistics, undefined) testInstantHandlePower(rowingStatistics, undefined) @@ -188,8 +191,8 @@ test('Test behaviour for three perfect identical strokes, including settingling testDriveDuration(rowingStatistics, 0.143485717) testDriveDistance(rowingStatistics, 0.4271903319416174) testDriveLength(rowingStatistics, 0.1759291886010284) - testDriveAverageHandleForce(rowingStatistics, 276.6342676838766) - testDrivePeakHandleForce(rowingStatistics, 332.9918222212992) + testDriveAverageHandleForce(rowingStatistics, 276.6342676838739) + testDrivePeakHandleForce(rowingStatistics, 332.99182222129025) testRecoveryDuration(rowingStatistics, 0.21654112800000003) testDragFactor(rowingStatistics, 281.5961372923874) testInstantHandlePower(rowingStatistics, undefined) @@ -220,8 +223,8 @@ test('Test behaviour for three perfect identical strokes, including settingling testDriveDuration(rowingStatistics, 0.22872752900000004) testDriveDistance(rowingStatistics, 1.0226745931298076) testDriveLength(rowingStatistics, 0.3078760800517996) - testDriveAverageHandleForce(rowingStatistics, 288.45140756250663) - testDrivePeakHandleForce(rowingStatistics, 447.10851434893794) + testDriveAverageHandleForce(rowingStatistics, 288.45140756259053) + testDrivePeakHandleForce(rowingStatistics, 447.108514349131) testRecoveryDuration(rowingStatistics, 0.21654112800000003) testDragFactor(rowingStatistics, 281.5961372923874) testInstantHandlePower(rowingStatistics, undefined) @@ -256,8 +259,8 @@ test('Test behaviour for three perfect identical strokes, including settingling testDriveDuration(rowingStatistics, 0.22872752900000004) testDriveDistance(rowingStatistics, 1.0226745931298076) testDriveLength(rowingStatistics, 0.3078760800517996) - testDriveAverageHandleForce(rowingStatistics, 288.45140756250663) - testDrivePeakHandleForce(rowingStatistics, 447.10851434893794) + testDriveAverageHandleForce(rowingStatistics, 288.45140756259053) + testDrivePeakHandleForce(rowingStatistics, 447.108514349131) testRecoveryDuration(rowingStatistics, 0.09812447700000015) testDragFactor(rowingStatistics, 281.5961372923874) testInstantHandlePower(rowingStatistics, undefined) @@ -288,8 +291,8 @@ test('Test behaviour for three perfect identical strokes, including settingling testDriveDuration(rowingStatistics, 0.27311228700000023) testDriveDistance(rowingStatistics, 1.2174697537259611) testDriveLength(rowingStatistics, 0.36651914291880905) - testDriveAverageHandleForce(rowingStatistics, 256.5447026919334) - testDrivePeakHandleForce(rowingStatistics, 447.1085143475723) + testDriveAverageHandleForce(rowingStatistics, 256.5447026931294) + testDrivePeakHandleForce(rowingStatistics, 447.1085143512751) testRecoveryDuration(rowingStatistics, 0.09812447700000015) testDragFactor(rowingStatistics, 281.5961372923874) testInstantHandlePower(rowingStatistics, undefined) @@ -327,26 +330,21 @@ test('Test behaviour for three perfect identical strokes, including settingling testInstantHandlePower(rowingStatistics, undefined) }) -// Test behaviour for noisy upgoing flank - -// Test behaviour for noisy downgoing flank - -// Test behaviour for noisy stroke - -// Test behaviour after reset - -// Test behaviour for one datapoint - -// Test behaviour for noisy stroke - -// Test drag factor calculation - -// Test Dynamic stroke detection +/** + * @todo Test the effects of smoothing parameters + */ -// Test behaviour after reset +/** + * @todo Test force curve behaviour + */ -// Test behaviour with real-life data +/** + * @todo Test behaviour after reset + */ +/** + * @description Test behaviour for the Sportstech WRX700 + */ test('sample data for Sportstech WRX700 should produce plausible results', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Sportstech_WRX700) const testConfig = { @@ -372,6 +370,9 @@ test('sample data for Sportstech WRX700 should produce plausible results', async testDragFactor(rowingStatistics, rowerProfiles.Sportstech_WRX700.dragFactor) }) +/** + * @description Test behaviour for the DKN R-320 + */ test('sample data for DKN R-320 should produce plausible results', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.DKN_R320) const testConfig = { @@ -397,6 +398,9 @@ test('sample data for DKN R-320 should produce plausible results', async () => { testDragFactor(rowingStatistics, rowerProfiles.DKN_R320.dragFactor) }) +/** + * @description Test behaviour for the NordicTrack RX800 + */ test('sample data for NordicTrack RX800 should produce plausible results', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.NordicTrack_RX800) const testConfig = { @@ -422,6 +426,9 @@ test('sample data for NordicTrack RX800 should produce plausible results', async testDragFactor(rowingStatistics, 493.8082148322739) }) +/** + * @description Test behaviour for the SportsTech WRX700 in a full session + */ test('A full session for SportsTech WRX700 should produce plausible results', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Sportstech_WRX700) const testConfig = { @@ -447,6 +454,9 @@ test('A full session for SportsTech WRX700 should produce plausible results', as testDragFactor(rowingStatistics, rowerProfiles.Sportstech_WRX700.dragFactor) }) +/** + * @description Test behaviour for the C2 Model C + */ test('A full session for a Concept2 Model C should produce plausible results', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Concept2_Model_C) const testConfig = { @@ -469,9 +479,12 @@ test('A full session for a Concept2 Model C should produce plausible results', a testTotalLinearDistance(rowingStatistics, 552.2056895088467) testTotalNumberOfStrokes(rowingStatistics, 82) // As dragFactor isn't static, it should have changed - testDragFactor(rowingStatistics, 123.64632740545652) + testDragFactor(rowingStatistics, 123.64632740545646) }) +/** + * @description Test behaviour for the C2 RowErg + */ test('A full session for a Concept2 RowErg should produce plausible results', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Concept2_RowErg) const testConfig = { @@ -490,11 +503,11 @@ test('A full session for a Concept2 RowErg should produce plausible results', as await replayRowingSession(rowingStatistics.handleRotationImpulse, { filename: 'recordings/Concept2_RowErg_Session_2000meters.csv', realtime: false, loop: false }) - testTotalMovingTime(rowingStatistics, 590.0294331572366) - testTotalLinearDistance(rowingStatistics, 2027.8951016561075) + testTotalMovingTime(rowingStatistics, 590.0232672488145) + testTotalLinearDistance(rowingStatistics, 2027.8404221844912) testTotalNumberOfStrokes(rowingStatistics, 205) // As dragFactor isn't static, it should have changed - testDragFactor(rowingStatistics, 80.70650785533269) + testDragFactor(rowingStatistics, 80.70871681343775) }) function testStrokeState (rowingStatistics, expectedValue) { diff --git a/app/engine/SessionManager.test.js b/app/engine/SessionManager.test.js index 5b505f03bc..f84141e1cf 100644 --- a/app/engine/SessionManager.test.js +++ b/app/engine/SessionManager.test.js @@ -1,12 +1,10 @@ 'use strict' -/* - Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor -*/ /** - * This test is a test of the SessionManager, that tests wether this object fills all fields correctly, + * @copyright [OpenRowingMonitor]{@link https://github.com/JaapvanEkris/openrowingmonitor} + * + * @file This test is a test of the SessionManager, that tests wether this object fills all fields correctly, * and cuts off a session, interval and split decently */ -// @ToDo: test the effects of smoothing parameters import { test } from 'uvu' import * as assert from 'uvu/assert' import rowerProfiles from '../../config/rowerProfiles.js' @@ -15,6 +13,21 @@ import { deepMerge } from '../tools/Helper.js' import { createSessionManager } from './SessionManager.js' +/** + * @todo Add inspections to all tests to inspect whether the 'workout' object contains all correct values as well + */ + +/** + * @todo Add inspections to all tests to inspect whether the 'interval' object contains all correct values + */ + +/** + * @todo Add splits and tests to inspect whether the 'split' object contains all correct values as well + */ + +/** + * @description Test behaviour for the Sportstech WRX700 in a 'Just Row' session + */ test('sample data for Sportstech WRX700 should produce plausible results for an unlimited run', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Sportstech_WRX700) const testConfig = { @@ -39,12 +52,15 @@ test('sample data for Sportstech WRX700 should produce plausible results for an testTotalMovingTime(sessionManager, 46.302522627) testTotalLinearDistance(sessionManager, 165.58832475070278) - testTotalCalories(sessionManager, 13.14287499723497) + testTotalCalories(sessionManager, 13.142874997261865) testTotalNumberOfStrokes(sessionManager, 15) // As dragFactor is static, it should remain in place testDragFactor(sessionManager, rowerProfiles.Sportstech_WRX700.dragFactor) }) +/** + * @description Test behaviour for the Sportstech WRX700 in a single interval session with a Distance target + */ test('sample data for Sportstech WRX700 should produce plausible results for a 150 meter session', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Sportstech_WRX700) const testConfig = { @@ -78,12 +94,15 @@ test('sample data for Sportstech WRX700 should produce plausible results for a 1 testTotalMovingTime(sessionManager, 41.876875768000005) testTotalLinearDistance(sessionManager, 150.02019165448286) - testTotalCalories(sessionManager, 12.047320967434432) + testTotalCalories(sessionManager, 12.047320967455441) testTotalNumberOfStrokes(sessionManager, 14) // As dragFactor is static, it should remain in place testDragFactor(sessionManager, rowerProfiles.Sportstech_WRX700.dragFactor) }) +/** + * @description Test behaviour for the Sportstech WRX700 in a single interval session with a Time target + */ test('sample data for Sportstech WRX700 should produce plausible results for a 45 seconds session', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Sportstech_WRX700) const testConfig = { @@ -117,12 +136,15 @@ test('sample data for Sportstech WRX700 should produce plausible results for a 4 testTotalMovingTime(sessionManager, 45.077573161000004) testTotalLinearDistance(sessionManager, 162.75775509684462) - testTotalCalories(sessionManager, 13.040795875068302) + testTotalCalories(sessionManager, 13.040795875095199) testTotalNumberOfStrokes(sessionManager, 15) // As dragFactor is static, it should remain in place testDragFactor(sessionManager, rowerProfiles.Sportstech_WRX700.dragFactor) }) +/** + * @description Test behaviour for the SportsTech WRX700 in a single interval session with a Calorie target + */ test('sample data for Sportstech WRX700 should produce plausible results for a 13 calories session', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Sportstech_WRX700) const testConfig = { @@ -155,12 +177,15 @@ test('sample data for Sportstech WRX700 should produce plausible results for a 1 testTotalMovingTime(sessionManager, 44.674583250000005) testTotalLinearDistance(sessionManager, 161.3424702699155) - testTotalCalories(sessionManager, 13.00721338248497) + testTotalCalories(sessionManager, 13.007213382511864) testTotalNumberOfStrokes(sessionManager, 15) // As dragFactor is static, it should remain in place testDragFactor(sessionManager, rowerProfiles.Sportstech_WRX700.dragFactor) }) +/** + * @description Test behaviour for the DKN R-320 in a 'Just Row' session + */ test('sample data for DKN R-320 should produce plausible results', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.DKN_R320) const testConfig = { @@ -185,12 +210,15 @@ test('sample data for DKN R-320 should produce plausible results', async () => { testTotalMovingTime(sessionManager, 21.701535821) testTotalLinearDistance(sessionManager, 69.20242183779045) - testTotalCalories(sessionManager, 6.761544006859074) + testTotalCalories(sessionManager, 6.7615440068583315) testTotalNumberOfStrokes(sessionManager, 9) // As dragFactor is static, it should remain in place testDragFactor(sessionManager, rowerProfiles.DKN_R320.dragFactor) }) +/** + * @description Test behaviour for the NordicTrack RX800 in a 'Just Row' session + */ test('sample data for NordicTrack RX800 should produce plausible results without intervalsettings', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.NordicTrack_RX800) const testConfig = { @@ -215,12 +243,15 @@ test('sample data for NordicTrack RX800 should produce plausible results without testTotalMovingTime(sessionManager, 22.368358745999995) testTotalLinearDistance(sessionManager, 80.8365747440095) - testTotalCalories(sessionManager, 4.848781772500018) + testTotalCalories(sessionManager, 4.8487817727235765) testTotalNumberOfStrokes(sessionManager, 9) // As dragFactor is dynamic, it should have changed testDragFactor(sessionManager, 493.8082148322739) }) +/** + * @description Test behaviour for the NordicTrack RX800 in a single interval session with a Time target + */ test('sample data for NordicTrack RX800 should produce plausible results for a 20 seconds session', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.NordicTrack_RX800) const testConfig = { @@ -254,12 +285,15 @@ test('sample data for NordicTrack RX800 should produce plausible results for a 2 testTotalMovingTime(sessionManager, 20.02496380499998) testTotalLinearDistance(sessionManager, 72.36563503912126) - testTotalCalories(sessionManager, 4.369289275331837) + testTotalCalories(sessionManager, 4.369289275497461) testTotalNumberOfStrokes(sessionManager, 8) // As dragFactor is dynamic, it should have changed testDragFactor(sessionManager, 489.6362497474688) }) +/** + * @description Test behaviour for the NordicTrack RX800 in a single interval session with a Calorie target + */ test('sample data for NordicTrack RX800 should produce plausible results for a 20 calories session', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.NordicTrack_RX800) const testConfig = { @@ -292,12 +326,15 @@ test('sample data for NordicTrack RX800 should produce plausible results for a 2 testTotalMovingTime(sessionManager, 22.368358745999995) testTotalLinearDistance(sessionManager, 80.8365747440095) - testTotalCalories(sessionManager, 4.848781772500018) + testTotalCalories(sessionManager, 4.8487817727235765) testTotalNumberOfStrokes(sessionManager, 9) // As dragFactor is dynamic, it should have changed testDragFactor(sessionManager, 493.8082148322739) }) +/** + * @description Test behaviour for the NordicTrack RX800 in a single interval session with a Distance target + */ test('sample data for NordicTrack RX800 should produce plausible results for a 75 meter session', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.NordicTrack_RX800) const testConfig = { @@ -331,12 +368,15 @@ test('sample data for NordicTrack RX800 should produce plausible results for a 7 testTotalMovingTime(sessionManager, 20.78640177499998) testTotalLinearDistance(sessionManager, 75.02272363260582) - testTotalCalories(sessionManager, 4.7014508748360155) + testTotalCalories(sessionManager, 4.701450875048449) testTotalNumberOfStrokes(sessionManager, 9) // As dragFactor is dynamic, it should have changed testDragFactor(sessionManager, 493.8082148322739) }) +/** + * @description Test behaviour for the SportsTech WRX700 in a 'Just Row' session + */ test('A full unlimited session for SportsTech WRX700 should produce plausible results', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Sportstech_WRX700) const testConfig = { @@ -361,12 +401,15 @@ test('A full unlimited session for SportsTech WRX700 should produce plausible re testTotalMovingTime(sessionManager, 2340.0100514160117) testTotalLinearDistance(sessionManager, 8406.084229545408) - testTotalCalories(sessionManager, 659.4761650968578) + testTotalCalories(sessionManager, 659.4761649276804) testTotalNumberOfStrokes(sessionManager, 845) // As dragFactor is static, it should remain in place testDragFactor(sessionManager, rowerProfiles.Sportstech_WRX700.dragFactor) }) +/** + * @description Test behaviour for the SportsTech WRX700 in a single interval session with a Distance target + */ test('A 8000 meter session for SportsTech WRX700 should produce plausible results', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Sportstech_WRX700) const testConfig = { @@ -400,12 +443,15 @@ test('A 8000 meter session for SportsTech WRX700 should produce plausible result testTotalMovingTime(sessionManager, 2236.631120457007) testTotalLinearDistance(sessionManager, 8000.605126630226) - testTotalCalories(sessionManager, 625.5636651284267) + testTotalCalories(sessionManager, 625.5636651176962) testTotalNumberOfStrokes(sessionManager, 804) // As dragFactor is static, it should remain in place testDragFactor(sessionManager, rowerProfiles.Sportstech_WRX700.dragFactor) }) +/** + * @description Test behaviour for the SportsTech WRX700 in a single interval session with a Time target + */ test('A 2300 sec session for SportsTech WRX700 should produce plausible results', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Sportstech_WRX700) const testConfig = { @@ -439,13 +485,16 @@ test('A 2300 sec session for SportsTech WRX700 should produce plausible results' testTotalMovingTime(sessionManager, 2300.00695516701) testTotalLinearDistance(sessionManager, 8251.818183410143) - testTotalCalories(sessionManager, 646.8205259437337) + testTotalCalories(sessionManager, 646.8205257461132) testTotalNumberOfStrokes(sessionManager, 830) // As dragFactor is static, it should remain in place testDragFactor(sessionManager, rowerProfiles.Sportstech_WRX700.dragFactor) }) -test('A 2400 sec session for SportsTech WRX700 should produce plausible results', async () => { +/** + * @description Test behaviour for the SportsTech WRX700 in a single interval session with a Time target, which will not be reached (test of stopping behaviour) + */ +test('A 2400 sec session with premature stop for SportsTech WRX700 should produce plausible results', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Sportstech_WRX700) const testConfig = { loglevel: { @@ -478,12 +527,15 @@ test('A 2400 sec session for SportsTech WRX700 should produce plausible results' testTotalMovingTime(sessionManager, 2340.0100514160117) testTotalLinearDistance(sessionManager, 8406.084229545408) - testTotalCalories(sessionManager, 659.4761650968578) + testTotalCalories(sessionManager, 659.4761649276804) testTotalNumberOfStrokes(sessionManager, 845) // As dragFactor is static, it should remain in place testDragFactor(sessionManager, rowerProfiles.Sportstech_WRX700.dragFactor) }) +/** + * @description Test behaviour for the C2 Model C in a 'Just Row' session + */ test('A full session for a Concept2 Model C should produce plausible results', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Concept2_Model_C) const testConfig = { @@ -508,12 +560,15 @@ test('A full session for a Concept2 Model C should produce plausible results', a testTotalMovingTime(sessionManager, 181.47141999999985) testTotalLinearDistance(sessionManager, 552.2056895088467) - testTotalCalories(sessionManager, 33.961418860794744) + testTotalCalories(sessionManager, 33.96141888570208) testTotalNumberOfStrokes(sessionManager, 82) // As dragFactor isn't static, it should have changed - testDragFactor(sessionManager, 123.64632740545652) + testDragFactor(sessionManager, 123.64632740545646) }) +/** + * @description Test behaviour for the C2 Model C in a single interval session with a Distance target + */ test('A 500 meter session for a Concept2 Model C should produce plausible results', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Concept2_Model_C) const testConfig = { @@ -547,12 +602,15 @@ test('A 500 meter session for a Concept2 Model C should produce plausible result testTotalMovingTime(sessionManager, 156.83075199999985) testTotalLinearDistance(sessionManager, 500.0178754492436) - testTotalCalories(sessionManager, 30.87012555729047) + testTotalCalories(sessionManager, 30.87012556034265) testTotalNumberOfStrokes(sessionManager, 73) // As dragFactor isn't static, it should have changed testDragFactor(sessionManager, 123.18123281481081) }) +/** + * @description Test behaviour for the C2 Model C in a single interval session with a Time target + */ test('A 3 minute session for a Concept2 Model C should produce plausible results', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Concept2_Model_C) const testConfig = { @@ -586,12 +644,15 @@ test('A 3 minute session for a Concept2 Model C should produce plausible results testTotalMovingTime(sessionManager, 180.96533299999987) testTotalLinearDistance(sessionManager, 551.9836036368948) - testTotalCalories(sessionManager, 33.91002250954926) + testTotalCalories(sessionManager, 33.91002253445811) testTotalNumberOfStrokes(sessionManager, 82) // As dragFactor isn't static, it should have changed - testDragFactor(sessionManager, 123.64632740545652) + testDragFactor(sessionManager, 123.64632740545646) }) +/** + * @description Test behaviour for the C2 Model C in a single interval session with a Calorie target + */ test('A 30 calorie session for a Concept2 Model C should produce plausible results', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Concept2_Model_C) const testConfig = { @@ -624,12 +685,15 @@ test('A 30 calorie session for a Concept2 Model C should produce plausible resul testTotalMovingTime(sessionManager, 153.93554999999992) testTotalLinearDistance(sessionManager, 490.5541073829962) - testTotalCalories(sessionManager, 30.018254906974597) + testTotalCalories(sessionManager, 30.018254924945477) testTotalNumberOfStrokes(sessionManager, 72) // As dragFactor isn't static, it should have changed testDragFactor(sessionManager, 123.18123281481081) }) +/** + * @description Test behaviour for the C2 RowErg in a 'Just Row' session + */ test('A full session for a Concept2 RowErg should produce plausible results', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Concept2_RowErg) const testConfig = { @@ -652,14 +716,17 @@ test('A full session for a Concept2 RowErg should produce plausible results', as await replayRowingSession(sessionManager.handleRotationImpulse, { filename: 'recordings/Concept2_RowErg_Session_2000meters.csv', realtime: false, loop: false }) - testTotalMovingTime(sessionManager, 590.0294331572366) - testTotalLinearDistance(sessionManager, 2027.8951016561075) - testTotalCalories(sessionManager, 113.55660950119214) + testTotalMovingTime(sessionManager, 590.0232672488145) + testTotalLinearDistance(sessionManager, 2027.8404221844912) + testTotalCalories(sessionManager, 113.70888316891056) testTotalNumberOfStrokes(sessionManager, 205) // As dragFactor isn't static, it should have changed - testDragFactor(sessionManager, 80.70650785533269) + testDragFactor(sessionManager, 80.70871681343775) }) +/** + * @description Test behaviour for the C2 RowErg in a single interval session with a Distance target + */ test('A 2000 meter session for a Concept2 RowErg should produce plausible results', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Concept2_RowErg) const testConfig = { @@ -691,14 +758,17 @@ test('A 2000 meter session for a Concept2 RowErg should produce plausible result await replayRowingSession(sessionManager.handleRotationImpulse, { filename: 'recordings/Concept2_RowErg_Session_2000meters.csv', realtime: false, loop: false }) - testTotalMovingTime(sessionManager, 582.0058299961318) - testTotalLinearDistance(sessionManager, 2000.0206027129661) - testTotalCalories(sessionManager, 112.16536746119625) + testTotalMovingTime(sessionManager, 582.0172075458094) + testTotalLinearDistance(sessionManager, 2000.0305986433395) + testTotalCalories(sessionManager, 112.3248747739861) testTotalNumberOfStrokes(sessionManager, 203) // As dragFactor isn't static, it should have changed - testDragFactor(sessionManager, 80.68314716929032) + testDragFactor(sessionManager, 80.67710663511464) }) +/** + * @description Test behaviour for the C2 RowErg in a single interval session with a Time target + */ test('A 580 seconds session for a Concept2 RowErg should produce plausible results', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Concept2_RowErg) const testConfig = { @@ -730,14 +800,17 @@ test('A 580 seconds session for a Concept2 RowErg should produce plausible resul await replayRowingSession(sessionManager.handleRotationImpulse, { filename: 'recordings/Concept2_RowErg_Session_2000meters.csv', realtime: false, loop: false }) - testTotalMovingTime(sessionManager, 580.0016078988951) - testTotalLinearDistance(sessionManager, 1993.2788181883743) - testTotalCalories(sessionManager, 111.76461106588519) + testTotalMovingTime(sessionManager, 580.0044837517516) + testTotalLinearDistance(sessionManager, 1993.2568687660857) + testTotalCalories(sessionManager, 111.91391015510767) testTotalNumberOfStrokes(sessionManager, 202) // As dragFactor isn't static, it should have changed - testDragFactor(sessionManager, 80.70729014258711) + testDragFactor(sessionManager, 80.69990852674464) }) +/** + * @description Test behaviour for the C2 RowErg in a single interval session with a Calorie target + */ test('A 100 calories session for a Concept2 RowErg should produce plausible results', async () => { const rowerProfile = deepMerge(rowerProfiles.DEFAULT, rowerProfiles.Concept2_RowErg) const testConfig = { @@ -768,14 +841,22 @@ test('A 100 calories session for a Concept2 RowErg should produce plausible resu await replayRowingSession(sessionManager.handleRotationImpulse, { filename: 'recordings/Concept2_RowErg_Session_2000meters.csv', realtime: false, loop: false }) - testTotalMovingTime(sessionManager, 520.3824691827283) - testTotalLinearDistance(sessionManager, 1786.2212497568994) - testTotalCalories(sessionManager, 100.00025111255141) + testTotalMovingTime(sessionManager, 518.8113511778027) + testTotalLinearDistance(sessionManager, 1780.6467553027344) + testTotalCalories(sessionManager, 100.00018360476473) testTotalNumberOfStrokes(sessionManager, 181) // As dragFactor isn't static, it should have changed - testDragFactor(sessionManager, 80.69402503758549) + testDragFactor(sessionManager, 80.66540957116986) }) +/** + * @todo Add tests for multiple planned intervals of the same type + */ + +/** + * @todo Add tests for multiple planned intervals of a different type, including pauses + */ + function testTotalMovingTime (sessionManager, expectedValue) { assert.ok(sessionManager.getMetrics().totalMovingTime === expectedValue, `totalMovingTime should be ${expectedValue} sec at ${sessionManager.getMetrics().totalMovingTime} sec, is ${sessionManager.getMetrics().totalMovingTime}`) } diff --git a/app/engine/utils/CyclicErrorFilter.js b/app/engine/utils/CyclicErrorFilter.js index 64b4679700..b9decf730c 100644 --- a/app/engine/utils/CyclicErrorFilter.js +++ b/app/engine/utils/CyclicErrorFilter.js @@ -1,10 +1,11 @@ 'use strict' /** * @copyright [OpenRowingMonitor]{@link https://github.com/JaapvanEkris/openrowingmonitor} - * + * * @file This implements a cyclic error filter. This is used to create a profile * The filterArray does the calculation, the slope and intercept arrays contain the results for easy retrieval * the slopeCorrection and interceptCorrection ensure preventing time dilation due to excessive corrections + * @see {@link https://github.com/JaapvanEkris/openrowingmonitor/blob/main/docs/Mathematical_Foundations.md|for the underlying math description) */ import loglevel from 'loglevel' import { createSeries } from './Series.js' @@ -28,7 +29,7 @@ export function createCyclicErrorFilter (rowerSettings, minimumDragFactorSamples const _flankLength = rowerSettings.flankLength const _agressiveness = Math.min(Math.max(rowerSettings.systematicErrorAgressiveness, 0), 1) const _invAgressiveness = Math.min(Math.max(1 - _agressiveness, 0), 1) - const _numberOfFilterSamples = Math.max((minimumDragFactorSamples / _numberOfMagnets) * 2.5, 5) + const _numberOfFilterSamples = Math.max(Math.round((rowerSettings.systematicErrorNumberOfDatapoints / _numberOfMagnets)), 5) const _minimumTimeBetweenImpulses = rowerSettings.minimumTimeBetweenImpulses const _maximumTimeBetweenImpulses = rowerSettings.maximumTimeBetweenImpulses const raw = createSeries(_flankLength) @@ -54,7 +55,7 @@ export function createCyclicErrorFilter (rowerSettings, minimumDragFactorSamples * @param {float} the raw recorded value to be cleaned up * @param {integer} the position of the flywheel * @returns {object} result - * @returns {float} result.value - the resulting clean value + * @returns {float} result.value - the resulting clean value * @returns {float} result.goodnessOfFit - The goodness of fit indication for the specific datapoint * @description Applies the filter on the raw value for the given position (i.e. magnet). Please note: this function is NOT stateless, it also fills a hystoric buffer of raw and clean values */ @@ -85,6 +86,28 @@ export function createCyclicErrorFilter (rowerSettings, minimumDragFactorSamples return (rawValue * slope[magnet] * slopeCorrection) + (intercept[magnet] - interceptCorrection) } + /** + * @returns {object} result - provides the (oldest) object at the head of the FiFo buffer, as once returned as a repsonse to the 'applyFilter()' function + * @returns {float} result.clean - the resulting clean value as once returned + * @returns {float} result.raw - the initial (raw) datapoint before applying the filter + * @returns {float} result.goodnessOfFit - The goodness of fit indication for the specific datapoint + */ + function atSeriesBegin () { + if (clean.length() > 0) { + return { + clean: clean.atSeriesBegin(), + raw: raw.atSeriesBegin(), + goodnessOfFit: goodnessOfFit.atSeriesBegin() + } + } else { + return { + clean: undefined, + raw: undefined, + goodnessOfFit: 0 + } + } + } + /** * @param {integer} relativePosition - the position of the recorded datapoint (i.e the sequence number of the datapoint) * @param {float} absolutePosition - the total spinning time of the flywheel @@ -146,7 +169,7 @@ export function createCyclicErrorFilter (rowerSettings, minimumDragFactorSamples * @description This function is used for clearing the buffers in order to prepare to record for a new set of datapoints, or clear it when the buffer is filled with a recovery with too weak GoF */ function warmRestart () { - if (!isNaN(lowerCursor)) { log.debug('*** WARNING: cyclic error filter has forcefully been restarted (warm)') } + if (!isNaN(lowerCursor)) { log.trace('*** Cyclic error filter had a warm restart for the next recovery') } recordedRelativePosition = [] recordedAbsolutePosition = [] recordedRawValue = [] @@ -209,8 +232,7 @@ export function createCyclicErrorFilter (rowerSettings, minimumDragFactorSamples recordRawDatapoint, processNextRawDatapoint, updateFilter, - raw, - clean, + atSeriesBegin, warmRestart, coldRestart, reset diff --git a/app/engine/utils/MovingWindowRegressor.js b/app/engine/utils/MovingWindowRegressor.js index bb138fec34..2f5f325088 100644 --- a/app/engine/utils/MovingWindowRegressor.js +++ b/app/engine/utils/MovingWindowRegressor.js @@ -1,9 +1,8 @@ 'use strict' -/* - Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor -*/ /** - * This implements a Moving Regression Algorithm to obtain a coefficients, first (angular velocity) and + * @copyright [OpenRowingMonitor]{@link https://github.com/JaapvanEkris/openrowingmonitor} + * + * @file This implements a Moving Regression Algorithm to obtain a coefficients, first (angular velocity) and * second derivative (angular acceleration) at the front of the flank */ import { createTSQuadraticSeries } from './TSQuadraticSeries.js' @@ -22,8 +21,8 @@ export function createMovingRegressor (bandwith) { * @param {float} the x value of the datapoint * @param {float} the y value of the datapoint */ - function push (x, y) { - quadraticTheilSenRegressor.push(x, y) + function push (x, y, w = 1) { + quadraticTheilSenRegressor.push(x, y, w) // Let's shift the matrix to make room for a new datapoint if (aMatrix.length >= flankLength) { @@ -165,6 +164,9 @@ export function createMovingRegressor (bandwith) { } } + /** + * Resets the series to its initial state + */ function reset () { quadraticTheilSenRegressor.reset() let i = aMatrix.length @@ -198,6 +200,10 @@ export function createMovingRegressor (bandwith) { cMatrix = [] } + /** + * @param {integer} position - position to be retrieved, starting at 0 + * @returns {float} X value at that specific postion in the series + */ function Xget (position = 0) { if (position < quadraticTheilSenRegressor.length()) { return quadraticTheilSenRegressor.X.get(position) @@ -206,6 +212,10 @@ export function createMovingRegressor (bandwith) { } } + /** + * @param {integer} position - position to be retrieved, starting at 0 + * @returns {float} Y value at that specific postion in the series + */ function Yget (position = 0) { if (position < quadraticTheilSenRegressor.length()) { return quadraticTheilSenRegressor.Y.get(position) diff --git a/app/engine/utils/MovingWindowRegressor.test.js b/app/engine/utils/MovingWindowRegressor.test.js index 4c7e0a6c97..985c7656d4 100644 --- a/app/engine/utils/MovingWindowRegressor.test.js +++ b/app/engine/utils/MovingWindowRegressor.test.js @@ -1,8 +1,8 @@ -'use strict' /** - * Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor - */ +'use strict' /** - * Tests of the movingRegressor object + * @copyright [OpenRowingMonitor]{@link https://github.com/JaapvanEkris/openrowingmonitor} + * + * @file Tests of the movingRegressor object */ import { test } from 'uvu' import * as assert from 'uvu/assert' @@ -12,7 +12,9 @@ function flywheelPosition (position) { return ((position * Math.PI) / 3) } -// Test behaviour for no datapoints +/** + * @description Test behaviour for no datapoints + */ test('Correct movingRegressor behaviour at initialisation', () => { const flankLength = 12 const movingRegressor = createMovingRegressor(flankLength) @@ -20,14 +22,20 @@ test('Correct movingRegressor behaviour at initialisation', () => { testSecondDerivative(movingRegressor, undefined) }) -// Test behaviour for one datapoint +/** + * @todo Test behaviour for one datapoint + */ -// Test behaviour for perfect upgoing flank +/** + * @todo Test behaviour for perfect upgoing flank + */ -// Test behaviour for perfect downgoing flank +/** + * @todo Test behaviour for perfect downgoing flank + */ /** - * Test of the integration of the underlying FullTSQuadraticEstimator object + * @description Test of the integration of the underlying FullTSQuadraticEstimator object * This uses the same data as the function y = 2 x^2 + 4 * x */ test('Test of correct algorithmic integration of FullTSQuadraticEstimator and movingRegressor object for quadratic function f(x) = 2 * x^2 + 4 * x', () => { @@ -133,7 +141,7 @@ test('Test of correct algorithmic integration of FullTSQuadraticEstimator and mo }) /** - * Test of the integration of the underlying FullTSQuadraticEstimator object + * @description Test of the integration of the underlying FullTSQuadraticEstimator object * The data follows the function y = X^3 + 2 * x^2 + 4 * x * To test if multiple quadratic regressions can decently approximate a cubic function */ @@ -240,10 +248,10 @@ test('Test of correct algorithmic integration of FullTSQuadraticEstimator and mo }) /** - * Test of the integration of the underlying FullTSQuadraticEstimator object + * @description Test of the integration of the underlying FullTSQuadraticEstimator object * The data follows the function y = X^3 + 2 * x^2 + 4 * x with a +/-0.0001 sec injected noise in currentDt * To test if multiple quadratic regressions can decently approximate a cubic function with noise - * Please note: theoretical values are based on the perfect function (i.e. without noise) + * Please note: theoretical values are based on the perfect function (i.e. without noise) */ test('Test of correct algorithmic integration of FullTSQuadraticEstimator and movingRegressor object for cubic function f(x) = X^3 + 2 * x^2 + 4 * x with +/- 0.0001 error', () => { const flankLength = 12 @@ -347,8 +355,12 @@ test('Test of correct algorithmic integration of FullTSQuadraticEstimator and mo testSecondDerivative(movingRegressor, 15.113402988997308) // Datapoint 20, Theoretical value: 14.90963951, error: -1.05% }) -// Test behaviour for no datapoints -test('Test of correct algorithmic behaviourof FullTSQuadraticEstimator in movingRegressor object for function f(x) = = (x + 3,22398390803294)^3 + 33,5103216382911', () => { +/** + * @description Test of the integration of the underlying FullTSQuadraticEstimator object + * The data follows the function y = (x + 3,22398390803294)^3 + 33,5103216382911 + * To test if multiple quadratic regressions can decently approximate a cubic function + */ +test('Test of correct algorithmic behaviourof FullTSQuadraticEstimator in movingRegressor object for function f(x) = (x + 3,22398390803294)^3 + 33,5103216382911', () => { const flankLength = 11 const movingRegressor = createMovingRegressor(flankLength) @@ -432,62 +444,62 @@ test('Test of correct algorithmic behaviourof FullTSQuadraticEstimator in moving testFirstDerivative(movingRegressor, 14.287862296451902) // Datapoint: 22, Theoretical value: 14.3595335732101, Error: -0.4991% testSecondDerivative(movingRegressor, -12.86939888687925) // Datapoint: 22, Theoretical value: -13.1268580733747, Error: -1.9613% movingRegressor.push(4.2394752055962, flywheelPosition(33)) // Datapoint 33 - testFirstDerivative(movingRegressor, 13.344069231831451) // Datapoint: 23, Theoretical value: 13.3855228467043, Error: -0.3097% - testSecondDerivative(movingRegressor, -12.596943554868657) // Datapoint: 23, Theoretical value: -12.6738421230679, Error: -0.6068% + testFirstDerivative(movingRegressor, 13.343446992890124) // Datapoint: 23, Theoretical value: 13.3855228467043, Error: -0.3097% + testSecondDerivative(movingRegressor, -12.593658621633756) // Datapoint: 23, Theoretical value: -12.6738421230679, Error: -0.6068% movingRegressor.push(4.50342276981795, flywheelPosition(34)) // Datapoint 34 - testFirstDerivative(movingRegressor, 12.27277433707641) // Datapoint: 24, Theoretical value: 12.3746709051205, Error: -0.8234% - testSecondDerivative(movingRegressor, -11.94611298378567) // Datapoint: 24, Theoretical value: -12.1858955707591, Error: -1.9677% + testFirstDerivative(movingRegressor, 12.272424425339942) // Datapoint: 24, Theoretical value: 12.3746709051205, Error: -0.8234% + testSecondDerivative(movingRegressor, -11.943049564910012) // Datapoint: 24, Theoretical value: -12.1858955707591, Error: -1.9677% movingRegressor.push(4.68857579559446, flywheelPosition(35)) // Datapoint 35 - testFirstDerivative(movingRegressor, 11.111270142918675) // Datapoint: 25, Theoretical value: 11.3206759756907, Error: -1.8498% - testSecondDerivative(movingRegressor, -11.104255084717169) // Datapoint: 25, Theoretical value: -11.6553898136565, Error: -4.7286% + testFirstDerivative(movingRegressor, 11.111275577176187) // Datapoint: 25, Theoretical value: 11.3206759756907, Error: -1.8498% + testSecondDerivative(movingRegressor, -11.10165547134061) // Datapoint: 25, Theoretical value: -11.6553898136565, Error: -4.7286% movingRegressor.push(4.83597586204941, flywheelPosition(36)) // Datapoint 36 - testFirstDerivative(movingRegressor, 9.856257396415623) // Datapoint: 26, Theoretical value: 10.2150657644303, Error: -3.5125% - testSecondDerivative(movingRegressor, -10.133558490306132) // Datapoint: 26, Theoretical value: -11.0716208918642, Error: -8.4727% + testFirstDerivative(movingRegressor, 9.85670546231101) // Datapoint: 26, Theoretical value: 10.2150657644303, Error: -3.5125% + testSecondDerivative(movingRegressor, -10.131638260894363) // Datapoint: 26, Theoretical value: -11.0716208918642, Error: -8.4727% movingRegressor.push(4.96044960092562, flywheelPosition(37)) // Datapoint 37 - testFirstDerivative(movingRegressor, 8.33034413131051) // Datapoint: 27, Theoretical value: 9.04593930777978, Error: -7.9107% - testSecondDerivative(movingRegressor, -8.90342412705638) // Datapoint: 27, Theoretical value: -10.4187941573561, Error: -14.5446% + testFirstDerivative(movingRegressor, 8.33150256290571) // Datapoint: 27, Theoretical value: 9.04593930777978, Error: -7.9107% + testSecondDerivative(movingRegressor, -8.90260010296994) // Datapoint: 27, Theoretical value: -10.4187941573561, Error: -14.5446% movingRegressor.push(5.06925405667697, flywheelPosition(38)) // Datapoint 38 - testFirstDerivative(movingRegressor, 6.614017373941227) // Datapoint: 28, Theoretical value: 7.79555417944151, Error: -15.1565% - testSecondDerivative(movingRegressor, -7.485201796668778) // Datapoint: 28, Theoretical value: -9.67195172409882, Error: -22.6092% + testFirstDerivative(movingRegressor, 6.616103614905059) // Datapoint: 28, Theoretical value: 7.79555417944151, Error: -15.1565% + testSecondDerivative(movingRegressor, -7.485916659905859) // Datapoint: 28, Theoretical value: -9.67195172409882, Error: -22.6092% movingRegressor.push(5.16654887697569, flywheelPosition(39)) // Datapoint 39 - testFirstDerivative(movingRegressor, 4.885955137652653) // Datapoint: 29, Theoretical value: 6.43508819133308, Error: -24.0732% - testSecondDerivative(movingRegressor, -5.954069768024937) // Datapoint: 29, Theoretical value: -8.78755132536914, Error: -32.2443% + testFirstDerivative(movingRegressor, 4.888940778715439) // Datapoint: 29, Theoretical value: 6.43508819133308, Error: -24.0732% + testSecondDerivative(movingRegressor, -5.956802316997369) // Datapoint: 29, Theoretical value: -8.78755132536914, Error: -32.2443% movingRegressor.push(5.25496650315946, flywheelPosition(40)) // Datapoint 40 - testFirstDerivative(movingRegressor, 3.6285904570805947) // Datapoint: 30, Theoretical value: 4.91089140313715, Error: -26.1114% - testSecondDerivative(movingRegressor, -4.766667985585422) // Datapoint: 30, Theoretical value: -7.67663317071005, Error: -37.9068% + testFirstDerivative(movingRegressor, 3.6316979581923494) // Datapoint: 30, Theoretical value: 4.91089140313715, Error: -26.1114% + testSecondDerivative(movingRegressor, -4.771588801465927) // Datapoint: 30, Theoretical value: -7.67663317071005, Error: -37.9068% movingRegressor.push(5.33629092854426, flywheelPosition(41)) // Datapoint 41 - testFirstDerivative(movingRegressor, 2.305636451416337) // Datapoint: 31, Theoretical value: 3.09366772628014, Error: -25.4724% - testSecondDerivative(movingRegressor, -3.5514156470006624) // Datapoint: 31, Theoretical value: -6.09294778537956, Error: -41.7127% + testFirstDerivative(movingRegressor, 2.3077423939611448) // Datapoint: 31, Theoretical value: 3.09366772628014, Error: -25.4724% + testSecondDerivative(movingRegressor, -3.5593152612469012) // Datapoint: 31, Theoretical value: -6.09294778537956, Error: -41.7127% movingRegressor.push(5.41179358692871, flywheelPosition(42)) // Datapoint 42 - testFirstDerivative(movingRegressor, 1.5762722749892073) // Datapoint: 32, Theoretical value: 0 - testSecondDerivative(movingRegressor, 3.845172493155469e-14) // Datapoint: 32, Theoretical value: 0 + testFirstDerivative(movingRegressor, 1.5335044322403928) // Datapoint: 32, Theoretical value: 0 + testSecondDerivative(movingRegressor, 3.8379764035844055e-14) // Datapoint: 32, Theoretical value: 0 movingRegressor.push(5.48241633596003, flywheelPosition(43)) // Datapoint 43 - testFirstDerivative(movingRegressor, 2.3056364514163405) // Datapoint: 33, Theoretical value: 3.09366772628014, Error: -25.4724% - testSecondDerivative(movingRegressor, 3.551415647000658) // Datapoint: 33, Theoretical value: 6.09294778537956, Error: -41.7127% + testFirstDerivative(movingRegressor, 2.3077423939611457) // Datapoint: 33, Theoretical value: 3.09366772628014, Error: -25.4724% + testSecondDerivative(movingRegressor, 3.5593152612468977) // Datapoint: 33, Theoretical value: 6.09294778537956, Error: -41.7127% movingRegressor.push(5.54887861105219, flywheelPosition(44)) // Datapoint 44 - testFirstDerivative(movingRegressor, 3.6285904570805094) // Datapoint: 34, Theoretical value: 4.91089140313715, Error: -26.1114% - testSecondDerivative(movingRegressor, 4.766667985585649) // Datapoint: 34, Theoretical value: 7.67663317071005, Error: -37.9068% + testFirstDerivative(movingRegressor, 3.6316979581922624) // Datapoint: 34, Theoretical value: 4.91089140313715, Error: -26.1114% + testSecondDerivative(movingRegressor, 4.771588801466153) // Datapoint: 34, Theoretical value: 7.67663317071005, Error: -37.9068% movingRegressor.push(5.61174382107199, flywheelPosition(45)) // Datapoint 45 - testFirstDerivative(movingRegressor, 4.885955137652726) // Datapoint: 35, Theoretical value: 6.43508819133308, Error: -24.0732% - testSecondDerivative(movingRegressor, 5.954069768025052) // Datapoint: 35, Theoretical value: 8.78755132536914, Error: -32.2443% + testFirstDerivative(movingRegressor, 4.888940778715508) // Datapoint: 35, Theoretical value: 6.43508819133308, Error: -24.0732% + testSecondDerivative(movingRegressor, 5.956802316997485) // Datapoint: 35, Theoretical value: 8.78755132536914, Error: -32.2443% movingRegressor.push(5.6714624031923, flywheelPosition(46)) // Datapoint 46 - testFirstDerivative(movingRegressor, 6.614017373941454) // Datapoint: 36, Theoretical value: 7.79555417944151, Error: -15.1565% - testSecondDerivative(movingRegressor, 7.4852017966686715) // Datapoint: 36, Theoretical value: 9.67195172409882, Error: -22.6092% + testFirstDerivative(movingRegressor, 6.616103614905288) // Datapoint: 36, Theoretical value: 7.79555417944151, Error: -15.1565% + testSecondDerivative(movingRegressor, 7.485916659905754) // Datapoint: 36, Theoretical value: 9.67195172409882, Error: -22.6092% movingRegressor.push(5.72840080746097, flywheelPosition(47)) // Datapoint 47 - testFirstDerivative(movingRegressor, 8.330344131310724) // Datapoint: 37, Theoretical value: 9.04593930777979, Error: -7.9107% - testSecondDerivative(movingRegressor, 8.903424127056287) // Datapoint: 37, Theoretical value: 10.4187941573561, Error: -14.5446% + testFirstDerivative(movingRegressor, 8.331502562905932) // Datapoint: 37, Theoretical value: 9.04593930777979, Error: -7.9107% + testSecondDerivative(movingRegressor, 8.902600102969847) // Datapoint: 37, Theoretical value: 10.4187941573561, Error: -14.5446% movingRegressor.push(5.78286163160296, flywheelPosition(48)) // Datapoint 48 - testFirstDerivative(movingRegressor, 9.856257396415842) // Datapoint: 38, Theoretical value: 10.2150657644303, Error: -3.5125% - testSecondDerivative(movingRegressor, 10.133558490305996) // Datapoint: 38, Theoretical value: 11.0716208918642, Error: -8.4727% + testFirstDerivative(movingRegressor, 9.856705462311233) // Datapoint: 38, Theoretical value: 10.2150657644303, Error: -3.5125% + testSecondDerivative(movingRegressor, 10.131638260894228) // Datapoint: 38, Theoretical value: 11.0716208918642, Error: -8.4727% movingRegressor.push(5.83509798693099, flywheelPosition(49)) // Datapoint 49 - testFirstDerivative(movingRegressor, 11.11127014291916) // Datapoint: 39, Theoretical value: 11.3206759756907, Error: -1.8498% - testSecondDerivative(movingRegressor, 11.104255084716218) // Datapoint: 39, Theoretical value: 11.6553898136565, Error: -4.7286% + testFirstDerivative(movingRegressor, 11.111275577176677) // Datapoint: 39, Theoretical value: 11.3206759756907, Error: -1.8498% + testSecondDerivative(movingRegressor, 11.101655471339662) // Datapoint: 39, Theoretical value: 11.6553898136565, Error: -4.7286% movingRegressor.push(5.88532398701588, flywheelPosition(50)) // Datapoint 50 - testFirstDerivative(movingRegressor, 12.272774337076662) // Datapoint: 40, Theoretical value: 12.3746709051205, Error: -0.8234% - testSecondDerivative(movingRegressor, 11.946112983784932) // Datapoint: 40, Theoretical value: 12.1858955707591, Error: -1.9677% + testFirstDerivative(movingRegressor, 12.272424425340205) // Datapoint: 40, Theoretical value: 12.3746709051205, Error: -0.8234% + testSecondDerivative(movingRegressor, 11.943049564909279) // Datapoint: 40, Theoretical value: 12.1858955707591, Error: -1.9677% movingRegressor.push(5.93372256071353, flywheelPosition(51)) // Datapoint 51 - testFirstDerivative(movingRegressor, 13.34406923183132) // Datapoint: 41, Theoretical value: 13.3855228467043, Error: -0.3097% - testSecondDerivative(movingRegressor, 12.59694355486861) // Datapoint: 41, Theoretical value: 12.6738421230679, Error: -0.6068% + testFirstDerivative(movingRegressor, 13.343446992889987) // Datapoint: 41, Theoretical value: 13.3855228467043, Error: -0.3097% + testSecondDerivative(movingRegressor, 12.593658621633706) // Datapoint: 41, Theoretical value: 12.6738421230679, Error: -0.6068% movingRegressor.push(5.98045137563747, flywheelPosition(52)) // Datapoint 52 testFirstDerivative(movingRegressor, 14.287862296451628) // Datapoint: 42, Theoretical value: 14.3595335732101, Error: -0.4991% testSecondDerivative(movingRegressor, 12.869398886878852) // Datapoint: 42, Theoretical value: 13.1268580733747, Error: -1.9613% @@ -624,62 +636,62 @@ test('Test of correct algorithmic behaviourof FullTSQuadraticEstimator in moving testFirstDerivative(movingRegressor, 14.287862296451593) // Datapoint: 86, Theoretical value: 14.3595335732101, Error: -0.4991% testSecondDerivative(movingRegressor, -12.86939888687885) // Datapoint: 86, Theoretical value: -13.1268580733747, Error: -1.9613% movingRegressor.push(10.6535038284662, flywheelPosition(97)) // Datapoint 97 - testFirstDerivative(movingRegressor, 13.344069231831583) // Datapoint: 87, Theoretical value: 13.3855228467043, Error: -0.3097% - testSecondDerivative(movingRegressor, -12.596943554867401) // Datapoint: 87, Theoretical value: -12.6738421230679, Error: -0.6068% + testFirstDerivative(movingRegressor, 13.343446992890279) // Datapoint: 87, Theoretical value: 13.3855228467043, Error: -0.3097% + testSecondDerivative(movingRegressor, -12.5936586216325) // Datapoint: 87, Theoretical value: -12.6738421230679, Error: -0.6068% movingRegressor.push(10.9174513926879, flywheelPosition(98)) // Datapoint 98 - testFirstDerivative(movingRegressor, 12.272774337077237) // Datapoint: 88, Theoretical value: 12.3746709051205, Error: -0.8234% - testSecondDerivative(movingRegressor, -11.94611298378346) // Datapoint: 88, Theoretical value: -12.1858955707591, Error: -1.9677% + testFirstDerivative(movingRegressor, 12.272424425340773) // Datapoint: 88, Theoretical value: 12.3746709051205, Error: -0.8234% + testSecondDerivative(movingRegressor, -11.943049564907804) // Datapoint: 88, Theoretical value: -12.1858955707591, Error: -1.9677% movingRegressor.push(11.1026044184645, flywheelPosition(99)) // Datapoint 99 - testFirstDerivative(movingRegressor, 11.11127014291931) // Datapoint: 89, Theoretical value: 11.3206759756907, Error: -1.8498% - testSecondDerivative(movingRegressor, -11.10425508471583) // Datapoint: 89, Theoretical value: -11.6553898136565, Error: -4.7286% + testFirstDerivative(movingRegressor, 11.111275577176826) // Datapoint: 89, Theoretical value: 11.3206759756907, Error: -1.8498% + testSecondDerivative(movingRegressor, -11.10165547133927) // Datapoint: 89, Theoretical value: -11.6553898136565, Error: -4.7286% movingRegressor.push(11.2500044849194, flywheelPosition(100)) // Datapoint 100 - testFirstDerivative(movingRegressor, 9.856257396415984) // Datapoint: 90, Theoretical value: 10.2150657644303, Error: -3.5125% - testSecondDerivative(movingRegressor, -10.133558490305735) // Datapoint: 90, Theoretical value: -11.0716208918642, Error: -8.4727% + testFirstDerivative(movingRegressor, 9.856705462311382) // Datapoint: 90, Theoretical value: 10.2150657644303, Error: -3.5125% + testSecondDerivative(movingRegressor, -10.131638260893967) // Datapoint: 90, Theoretical value: -11.0716208918642, Error: -8.4727% movingRegressor.push(11.3744782237956, flywheelPosition(101)) // Datapoint 101 - testFirstDerivative(movingRegressor, 8.330344131310596) // Datapoint: 91, Theoretical value: 9.04593930777978, Error: -7.9107% - testSecondDerivative(movingRegressor, -8.903424127056597) // Datapoint: 91, Theoretical value: -10.4187941573561, Error: -14.5446% + testFirstDerivative(movingRegressor, 8.331502562905783) // Datapoint: 91, Theoretical value: 9.04593930777978, Error: -7.9107% + testSecondDerivative(movingRegressor, -8.902600102970158) // Datapoint: 91, Theoretical value: -10.4187941573561, Error: -14.5446% movingRegressor.push(11.483282679547, flywheelPosition(102)) // Datapoint 102 - testFirstDerivative(movingRegressor, 6.614017373941209) // Datapoint: 92, Theoretical value: 7.79555417944151, Error: -15.1565% - testSecondDerivative(movingRegressor, -7.485201796668469) // Datapoint: 92, Theoretical value: -9.67195172409882, Error: -22.6092% + testFirstDerivative(movingRegressor, 6.616103614905029) // Datapoint: 92, Theoretical value: 7.79555417944151, Error: -15.1565% + testSecondDerivative(movingRegressor, -7.485916659905551) // Datapoint: 92, Theoretical value: -9.67195172409882, Error: -22.6092% movingRegressor.push(11.5805774998457, flywheelPosition(103)) // Datapoint 103 - testFirstDerivative(movingRegressor, 4.885955137652751) // Datapoint: 93, Theoretical value: 6.43508819133308, Error: -24.0732% - testSecondDerivative(movingRegressor, -5.95406976802524) // Datapoint: 93, Theoretical value: -8.78755132536914, Error: -32.2443% + testFirstDerivative(movingRegressor, 4.888940778715536) // Datapoint: 93, Theoretical value: 6.43508819133308, Error: -24.0732% + testSecondDerivative(movingRegressor, -5.956802316997673) // Datapoint: 93, Theoretical value: -8.78755132536914, Error: -32.2443% movingRegressor.push(11.6689951260294, flywheelPosition(104)) // Datapoint 104 - testFirstDerivative(movingRegressor, 3.628590457080918) // Datapoint: 94, Theoretical value: 4.91089140313715, Error: -26.1114% - testSecondDerivative(movingRegressor, -4.766667985586247) // Datapoint: 94, Theoretical value: -7.67663317071005, Error: -37.9068% + testFirstDerivative(movingRegressor, 3.6316979581926674) // Datapoint: 94, Theoretical value: 4.91089140313715, Error: -26.1114% + testSecondDerivative(movingRegressor, -4.771588801466752) // Datapoint: 94, Theoretical value: -7.67663317071005, Error: -37.9068% movingRegressor.push(11.7503195514143, flywheelPosition(105)) // Datapoint 105 - testFirstDerivative(movingRegressor, 2.3056364514165004) // Datapoint: 95, Theoretical value: 3.09366772628014, Error: -25.4724% - testSecondDerivative(movingRegressor, -3.5514156470012384) // Datapoint: 95, Theoretical value: -6.09294778537956, Error: -41.7127% + testFirstDerivative(movingRegressor, 2.3077423939613055) // Datapoint: 95, Theoretical value: 3.09366772628014, Error: -25.4724% + testSecondDerivative(movingRegressor, -3.559315261247479) // Datapoint: 95, Theoretical value: -6.09294778537956, Error: -41.7127% movingRegressor.push(11.8258222097987, flywheelPosition(106)) // Datapoint 1066 - testFirstDerivative(movingRegressor, 1.5762722749896647) // Datapoint: 96, Theoretical value: 0 - testSecondDerivative(movingRegressor, -2.0664377115840596e-12) // Datapoint: 96, Theoretical value: 0 + testFirstDerivative(movingRegressor, 1.5335044322408324) // Datapoint: 96, Theoretical value: 0 + testSecondDerivative(movingRegressor, -2.0596270771553654e-12) // Datapoint: 96, Theoretical value: 0 movingRegressor.push(11.89644495883, flywheelPosition(107)) // Datapoint 107 - testFirstDerivative(movingRegressor, 2.3056364514154026) // Datapoint: 97, Theoretical value: 3.09366772628014, Error: -25.4724% - testSecondDerivative(movingRegressor, 3.551415647001751) // Datapoint: 97, Theoretical value: 6.09294778537956, Error: -41.7127% + testFirstDerivative(movingRegressor, 2.307742393960204) // Datapoint: 97, Theoretical value: 3.09366772628014, Error: -25.4724% + testSecondDerivative(movingRegressor, 3.559315261247989) // Datapoint: 97, Theoretical value: 6.09294778537956, Error: -41.7127% movingRegressor.push(11.9629072339222, flywheelPosition(108)) // Datapoint 108 - testFirstDerivative(movingRegressor, 3.62859045708084) // Datapoint: 98, Theoretical value: 4.91089140313715, Error: -26.1114% - testSecondDerivative(movingRegressor, 4.766667985584684) // Datapoint: 98, Theoretical value: 7.67663317071005, Error: -37.9068% + testFirstDerivative(movingRegressor, 3.6316979581925963) // Datapoint: 98, Theoretical value: 4.91089140313715, Error: -26.1114% + testSecondDerivative(movingRegressor, 4.771588801465188) // Datapoint: 98, Theoretical value: 7.67663317071005, Error: -37.9068% movingRegressor.push(12.025772443942, flywheelPosition(109)) // Datapoint 109 - testFirstDerivative(movingRegressor, 4.885955137653781) // Datapoint: 99, Theoretical value: 6.43508819133308, Error: -24.0732% - testSecondDerivative(movingRegressor, 5.954069768023376) // Datapoint: 99, Theoretical value: 8.78755132536914, Error: -32.2443% + testFirstDerivative(movingRegressor, 4.888940778716552) // Datapoint: 99, Theoretical value: 6.43508819133308, Error: -24.0732% + testSecondDerivative(movingRegressor, 5.956802316995809) // Datapoint: 99, Theoretical value: 8.78755132536914, Error: -32.2443% movingRegressor.push(12.0854910260623, flywheelPosition(110)) // Datapoint 110 - testFirstDerivative(movingRegressor, 6.614017373942133) // Datapoint: 100, Theoretical value: 7.79555417944151, Error: -15.1565% - testSecondDerivative(movingRegressor, 7.485201796666093) // Datapoint: 100, Theoretical value: 9.67195172409882, Error: -22.6092% + testFirstDerivative(movingRegressor, 6.616103614905953) // Datapoint: 100, Theoretical value: 7.79555417944151, Error: -15.1565% + testSecondDerivative(movingRegressor, 7.485916659903175) // Datapoint: 100, Theoretical value: 9.67195172409882, Error: -22.6092% movingRegressor.push(12.142429430331, flywheelPosition(111)) // Datapoint 111 - testFirstDerivative(movingRegressor, 8.330344131312316) // Datapoint: 101, Theoretical value: 9.04593930777979, Error: -7.9107% - testSecondDerivative(movingRegressor, 8.903424127051075) // Datapoint: 101, Theoretical value: 10.4187941573561, Error: -14.5446% + testFirstDerivative(movingRegressor, 8.331502562907502) // Datapoint: 101, Theoretical value: 9.04593930777979, Error: -7.9107% + testSecondDerivative(movingRegressor, 8.902600102964639) // Datapoint: 101, Theoretical value: 10.4187941573561, Error: -14.5446% movingRegressor.push(12.1968902544729, flywheelPosition(112)) // Datapoint 112 - testFirstDerivative(movingRegressor, 9.856257396417504) // Datapoint: 102, Theoretical value: 10.2150657644303, Error: -3.5125% - testSecondDerivative(movingRegressor, 10.133558490300592) // Datapoint: 102, Theoretical value: 11.0716208918642, Error: -8.4727% + testFirstDerivative(movingRegressor, 9.856705462312902) // Datapoint: 102, Theoretical value: 10.2150657644303, Error: -3.5125% + testSecondDerivative(movingRegressor, 10.131638260888828) // Datapoint: 102, Theoretical value: 11.0716208918642, Error: -8.4727% movingRegressor.push(12.249126609801, flywheelPosition(113)) // Datapoint 113 - testFirstDerivative(movingRegressor, 11.111270142920347) // Datapoint: 103, Theoretical value: 11.3206759756907, Error: -1.8498% - testSecondDerivative(movingRegressor, 11.104255084710369) // Datapoint: 103, Theoretical value: 11.6553898136565, Error: -4.7286% + testFirstDerivative(movingRegressor, 11.111275577177864) // Datapoint: 103, Theoretical value: 11.3206759756907, Error: -1.8498% + testSecondDerivative(movingRegressor, 11.101655471333814) // Datapoint: 103, Theoretical value: 11.6553898136565, Error: -4.7286% movingRegressor.push(12.2993526098859, flywheelPosition(114)) // Datapoint 114 - testFirstDerivative(movingRegressor, 12.272774337075006) // Datapoint: 104, Theoretical value: 12.3746709051205, Error: -0.8234% - testSecondDerivative(movingRegressor, 11.946112983782152) // Datapoint: 104, Theoretical value: 12.1858955707591, Error: -1.9677% + testFirstDerivative(movingRegressor, 12.272424425338542) // Datapoint: 104, Theoretical value: 12.3746709051205, Error: -0.8234% + testSecondDerivative(movingRegressor, 11.943049564906497) // Datapoint: 104, Theoretical value: 12.1858955707591, Error: -1.9677% movingRegressor.push(12.3477511835835, flywheelPosition(115)) // Datapoint 115 - testFirstDerivative(movingRegressor, 13.344069231830105) // Datapoint: 105, Theoretical value: 13.3855228467043, Error: -0.3097% - testSecondDerivative(movingRegressor, 12.596943554867831) // Datapoint: 105, Theoretical value: 12.6738421230679, Error: -0.6068% + testFirstDerivative(movingRegressor, 13.343446992888772) // Datapoint: 105, Theoretical value: 13.3855228467043, Error: -0.3097% + testSecondDerivative(movingRegressor, 12.59365862163293) // Datapoint: 105, Theoretical value: 12.6738421230679, Error: -0.6068% movingRegressor.push(12.3944799985075, flywheelPosition(116)) // Datapoint 116 testFirstDerivative(movingRegressor, 14.28786229645084) // Datapoint: 106, Theoretical value: 14.3595335732101, Error: -0.4991% testSecondDerivative(movingRegressor, 12.86939888687176) // Datapoint: 106, Theoretical value: 13.1268580733747, Error: -1.9613% @@ -821,68 +833,68 @@ test('Test of correct algorithmic behaviourof FullTSQuadraticEstimator in moving testFirstDerivative(movingRegressor, 35.03463828636044) // Datapoint: 21, Theoretical value: 35.3270234685551, Error: -0.8277% testSecondDerivative(movingRegressor, -84.72131405455713) // Datapoint: 21, Theoretical value: -86.6727901598317, Error: -2.2515% movingRegressor.push(2.01853237434599, flywheelPosition(32)) // Datapoint 32 - testFirstDerivative(movingRegressor, 32.556646337181334) // Datapoint: 22, Theoretical value: 32.7335342472893, Error: -0.5404% - testSecondDerivative(movingRegressor, -81.74563687145616) // Datapoint: 22, Theoretical value: -81.8553682135038, Error: -0.1341% + testFirstDerivative(movingRegressor, 32.554704278009275) // Datapoint: 22, Theoretical value: 32.7335342472893, Error: -0.5404% + testSecondDerivative(movingRegressor, -81.7211071442608) // Datapoint: 22, Theoretical value: -81.8553682135038, Error: -0.1341% movingRegressor.push(3.02779856151898, flywheelPosition(33)) // Datapoint 33 - testFirstDerivative(movingRegressor, 30.0053262177018) // Datapoint: 23, Theoretical value: 30.0875556300011, Error: -0.2733% - testSecondDerivative(movingRegressor, -78.8103638663143) // Datapoint: 23, Theoretical value: -76.8409405553369, Error: 2.563% + testFirstDerivative(movingRegressor, 30.000816353086208) // Datapoint: 23, Theoretical value: 30.0875556300011, Error: -0.2733% + testSecondDerivative(movingRegressor, -78.74803078952486) // Datapoint: 23, Theoretical value: -76.8409405553369, Error: 2.563% movingRegressor.push(3.17787478330574, flywheelPosition(34)) // Datapoint 34 - testFirstDerivative(movingRegressor, 27.3590650062608) // Datapoint: 24, Theoretical value: 27.3819824840534, Error: -0.0837% - testSecondDerivative(movingRegressor, -75.91047202675192) // Datapoint: 24, Theoretical value: -71.5980441226457, Error: 6.0231% + testFirstDerivative(movingRegressor, 27.35085176667772) // Datapoint: 24, Theoretical value: 27.3819824840534, Error: -0.0837% + testSecondDerivative(movingRegressor, -75.79177233150754) // Datapoint: 24, Theoretical value: -71.5980441226457, Error: 6.0231% movingRegressor.push(3.27580649001517, flywheelPosition(35)) // Datapoint 35 - testFirstDerivative(movingRegressor, 24.26443935991906) // Datapoint: 25, Theoretical value: 24.6077174058151, Error: -1.395% - testSecondDerivative(movingRegressor, -70.22915254416483) // Datapoint: 25, Theoretical value: -66.0854711273398, Error: 6.2702% + testFirstDerivative(movingRegressor, 24.26071071895941) // Datapoint: 25, Theoretical value: 24.6077174058151, Error: -1.395% + testSecondDerivative(movingRegressor, -70.11339732080359) // Datapoint: 25, Theoretical value: -66.0854711273398, Error: 6.2702% movingRegressor.push(3.35026709239635, flywheelPosition(36)) // Datapoint 36 - testFirstDerivative(movingRegressor, 20.83493250613911) // Datapoint: 26, Theoretical value: 21.7527364967177, Error: -4.2193% - testSecondDerivative(movingRegressor, -63.20422464017087) // Datapoint: 26, Theoretical value: -60.2473455054648, Error: 4.9079% + testFirstDerivative(movingRegressor, 20.837518238274917) // Datapoint: 26, Theoretical value: 21.7527364967177, Error: -4.2193% + testSecondDerivative(movingRegressor, -63.09857048895648) // Datapoint: 26, Theoretical value: -60.2473455054648, Error: 4.9079% movingRegressor.push(3.41104686909844, flywheelPosition(37)) // Datapoint 37 - testFirstDerivative(movingRegressor, 16.540358644462728) // Datapoint: 27, Theoretical value: 18.8004784715502, Error: -12.0216% - testSecondDerivative(movingRegressor, -53.9026545272266) // Datapoint: 27, Theoretical value: -54.0044029484732, Error: -0.1884% + testFirstDerivative(movingRegressor, 16.553286547446575) // Datapoint: 27, Theoretical value: 18.8004784715502, Error: -12.0216% + testSecondDerivative(movingRegressor, -53.82083440126937) // Datapoint: 27, Theoretical value: -54.0044029484732, Error: -0.1884% movingRegressor.push(3.46276108279553, flywheelPosition(38)) // Datapoint 38 - testFirstDerivative(movingRegressor, 12.023279845406904) // Datapoint: 28, Theoretical value: 15.7268191179949, Error: -23.5492% - testSecondDerivative(movingRegressor, -44.515559757405676) // Datapoint: 28, Theoretical value: -47.2370928078489, Error: -5.7614% + testFirstDerivative(movingRegressor, 12.04945082759771) // Datapoint: 28, Theoretical value: 15.7268191179949, Error: -23.5492% + testSecondDerivative(movingRegressor, -44.46299143724117) // Datapoint: 28, Theoretical value: -47.2370928078489, Error: -5.7614% movingRegressor.push(3.50798032628076, flywheelPosition(39)) // Datapoint 39 - testFirstDerivative(movingRegressor, 7.9550999676935525) // Datapoint: 29, Theoretical value: 12.4936663152318, Error: -36.3269% - testSecondDerivative(movingRegressor, -35.69289163563467) // Datapoint: 29, Theoretical value: -39.7484244987643, Error: -10.203% + testFirstDerivative(movingRegressor, 7.995979006137617) // Datapoint: 29, Theoretical value: 12.4936663152318, Error: -36.3269% + testSecondDerivative(movingRegressor, -35.67688197212385) // Datapoint: 29, Theoretical value: -39.7484244987643, Error: -10.203% movingRegressor.push(3.54829385426288, flywheelPosition(40)) // Datapoint 40 - testFirstDerivative(movingRegressor, 4.490581497457299) // Datapoint: 30, Theoretical value: 9.03268562508831, Error: -50.2852% - testSecondDerivative(movingRegressor, -26.59823204822437) // Datapoint: 30, Theoretical value: -31.164858820935, Error: -14.6531% + testFirstDerivative(movingRegressor, 4.545499777144109) // Datapoint: 30, Theoretical value: 9.03268562508831, Error: -50.2852% + testSecondDerivative(movingRegressor, -26.644166155256052) // Datapoint: 30, Theoretical value: -31.164858820935, Error: -14.6531% movingRegressor.push(3.58475763981283, flywheelPosition(41)) // Datapoint 41 - testFirstDerivative(movingRegressor, 3.815904626117252) // Datapoint: 31, Theoretical value: 5.18791555937216, Error: -26.4463% - testSecondDerivative(movingRegressor, -19.03067846863168) // Datapoint: 31, Theoretical value: -20.5611388761721, Error: -7.4435% + testFirstDerivative(movingRegressor, 3.8462783014954773) // Datapoint: 31, Theoretical value: 5.18791555937216, Error: -26.4463% + testSecondDerivative(movingRegressor, -19.207586578866348) // Datapoint: 31, Theoretical value: -20.5611388761721, Error: -7.4435% movingRegressor.push(3.61811148377765, flywheelPosition(42)) // Datapoint 42 - testFirstDerivative(movingRegressor, 3.1453638235989767) // Datapoint: 32, Theoretical value: 0 - testSecondDerivative(movingRegressor, 4.5598970291679296e-15) // Datapoint: 32, Theoretical value: 0 + testFirstDerivative(movingRegressor, 3.1383576841321967) // Datapoint: 32, Theoretical value: 0 + testSecondDerivative(movingRegressor, 4.409564582673597e-15) // Datapoint: 32, Theoretical value: 0 movingRegressor.push(3.64889518617698, flywheelPosition(43)) // Datapoint 43 - testFirstDerivative(movingRegressor, 3.8159046261167617) // Datapoint: 33, Theoretical value: 5.18791555937215, Error: -26.4463% - testSecondDerivative(movingRegressor, 19.03067846863373) // Datapoint: 33, Theoretical value: 20.5611388761721, Error: -7.4435% + testFirstDerivative(movingRegressor, 3.8462783014949977) // Datapoint: 33, Theoretical value: 5.18791555937215, Error: -26.4463% + testSecondDerivative(movingRegressor, 19.2075865788684) // Datapoint: 33, Theoretical value: 20.5611388761721, Error: -7.4435% movingRegressor.push(3.67751551598147, flywheelPosition(44)) // Datapoint 44 - testFirstDerivative(movingRegressor, 4.490581497456901) // Datapoint: 34, Theoretical value: 9.03268562508831, Error: -50.2852% - testSecondDerivative(movingRegressor, 26.598232048227107) // Datapoint: 34, Theoretical value: 31.164858820935, Error: -14.6531% + testFirstDerivative(movingRegressor, 4.545499777143718) // Datapoint: 34, Theoretical value: 9.03268562508831, Error: -50.2852% + testSecondDerivative(movingRegressor, 26.64416615525877) // Datapoint: 34, Theoretical value: 31.164858820935, Error: -14.6531% movingRegressor.push(3.7042871320382, flywheelPosition(45)) // Datapoint 45 - testFirstDerivative(movingRegressor, 7.955099967691808) // Datapoint: 35, Theoretical value: 12.4936663152318, Error: -36.3269% - testSecondDerivative(movingRegressor, 35.69289163564905) // Datapoint: 35, Theoretical value: 39.7484244987643, Error: -10.203% + testFirstDerivative(movingRegressor, 7.995979006135855) // Datapoint: 35, Theoretical value: 12.4936663152318, Error: -36.3269% + testSecondDerivative(movingRegressor, 35.67688197213815) // Datapoint: 35, Theoretical value: 39.7484244987643, Error: -10.203% movingRegressor.push(3.72945878658716, flywheelPosition(46)) // Datapoint 46 - testFirstDerivative(movingRegressor, 12.023279845401277) // Datapoint: 36, Theoretical value: 15.7268191179949, Error: -23.5492% - testSecondDerivative(movingRegressor, 44.51555975743898) // Datapoint: 36, Theoretical value: 47.2370928078489, Error: -5.7614% + testFirstDerivative(movingRegressor, 12.049450827592068) // Datapoint: 36, Theoretical value: 15.7268191179949, Error: -23.5492% + testSecondDerivative(movingRegressor, 44.46299143727433) // Datapoint: 36, Theoretical value: 47.2370928078489, Error: -5.7614% movingRegressor.push(3.75323076432218, flywheelPosition(47)) // Datapoint 47 - testFirstDerivative(movingRegressor, 16.540358644457484) // Datapoint: 37, Theoretical value: 18.8004784715502, Error: -12.0216% - testSecondDerivative(movingRegressor, 53.90265452726991) // Datapoint: 37, Theoretical value: 54.0044029484732, Error: -0.1884% + testFirstDerivative(movingRegressor, 16.55328654744136) // Datapoint: 37, Theoretical value: 18.8004784715502, Error: -12.0216% + testSecondDerivative(movingRegressor, 53.820834401312524) // Datapoint: 37, Theoretical value: 54.0044029484732, Error: -0.1884% movingRegressor.push(3.77576686986435, flywheelPosition(48)) // Datapoint 48 - testFirstDerivative(movingRegressor, 20.834932506136) // Datapoint: 38, Theoretical value: 21.7527364967177, Error: -4.2193% - testSecondDerivative(movingRegressor, 63.20422464021262) // Datapoint: 38, Theoretical value: 60.2473455054648, Error: 4.9079% + testFirstDerivative(movingRegressor, 20.837518238271798) // Datapoint: 38, Theoretical value: 21.7527364967177, Error: -4.2193% + testSecondDerivative(movingRegressor, 63.0985704889981) // Datapoint: 38, Theoretical value: 60.2473455054648, Error: 4.9079% movingRegressor.push(3.79720289770384, flywheelPosition(49)) // Datapoint 49 - testFirstDerivative(movingRegressor, 24.264439359918924) // Datapoint: 39, Theoretical value: 24.6077174058151, Error: -1.395% - testSecondDerivative(movingRegressor, 70.22915254420002) // Datapoint: 39, Theoretical value: 66.0854711273397, Error: 6.2702% + testFirstDerivative(movingRegressor, 24.260710718959217) // Datapoint: 39, Theoretical value: 24.6077174058151, Error: -1.395% + testSecondDerivative(movingRegressor, 70.11339732083867) // Datapoint: 39, Theoretical value: 66.0854711273397, Error: 6.2702% movingRegressor.push(3.81765276034255, flywheelPosition(50)) // Datapoint 50 - testFirstDerivative(movingRegressor, 27.359065006261318) // Datapoint: 40, Theoretical value: 27.3819824840534, Error: -0.0837% - testSecondDerivative(movingRegressor, 75.91047202678642) // Datapoint: 40, Theoretical value: 71.5980441226458, Error: 6.0231% + testFirstDerivative(movingRegressor, 27.35085176667826) // Datapoint: 40, Theoretical value: 27.3819824840534, Error: -0.0837% + testSecondDerivative(movingRegressor, 75.79177233154196) // Datapoint: 40, Theoretical value: 71.5980441226458, Error: 6.0231% movingRegressor.push(3.83721301460343, flywheelPosition(51)) // Datapoint 51 - testFirstDerivative(movingRegressor, 30.005326217703555) // Datapoint: 41, Theoretical value: 30.0875556300011, Error: -0.2733% - testSecondDerivative(movingRegressor, 78.81036386634156) // Datapoint: 41, Theoretical value: 76.8409405553369, Error: 2.563% + testFirstDerivative(movingRegressor, 30.000816353087885) // Datapoint: 41, Theoretical value: 30.0875556300011, Error: -0.2733% + testSecondDerivative(movingRegressor, 78.74803078955205) // Datapoint: 41, Theoretical value: 76.8409405553369, Error: 2.563% movingRegressor.push(3.85596626603776, flywheelPosition(52)) // Datapoint 52 - testFirstDerivative(movingRegressor, 32.556646337185555) // Datapoint: 42, Theoretical value: 32.7335342472893, Error: -0.5404% - testSecondDerivative(movingRegressor, 81.74563687146653) // Datapoint: 42, Theoretical value: 81.8553682135038, Error: -0.1341% + testFirstDerivative(movingRegressor, 32.5547042780135) // Datapoint: 42, Theoretical value: 32.7335342472893, Error: -0.5404% + testSecondDerivative(movingRegressor, 81.72110714427114) // Datapoint: 42, Theoretical value: 81.8553682135038, Error: -0.1341% movingRegressor.push(3.8739837710181, flywheelPosition(53)) // Datapoint 53 testFirstDerivative(movingRegressor, 35.03463828636535) // Datapoint: 43, Theoretical value: 35.3270234685551, Error: -0.8277% testSecondDerivative(movingRegressor, 84.72131405455926) // Datapoint: 43, Theoretical value: 86.6727901598316, Error: -2.2515% @@ -1013,68 +1025,68 @@ test('Test of correct algorithmic behaviourof FullTSQuadraticEstimator in moving testFirstDerivative(movingRegressor, 35.034638286358415) // Datapoint: 85, Theoretical value: 35.3270234685551, Error: -0.8277% testSecondDerivative(movingRegressor, -84.7213140545765) // Datapoint: 85, Theoretical value: -86.6727901598317, Error: -2.2515% movingRegressor.push(6.04282057476568, flywheelPosition(96)) // Datapoint 96 - testFirstDerivative(movingRegressor, 32.55664633717993) // Datapoint: 86, Theoretical value: 32.7335342472893, Error: -0.5404% - testSecondDerivative(movingRegressor, -81.74563687147501) // Datapoint: 86, Theoretical value: -81.8553682135038, Error: -0.1341% + testFirstDerivative(movingRegressor, 32.55470427800782) // Datapoint: 86, Theoretical value: 32.7335342472893, Error: -0.5404% + testSecondDerivative(movingRegressor, -81.72110714427964) // Datapoint: 86, Theoretical value: -81.8553682135038, Error: -0.1341% movingRegressor.push(7.05208676193867, flywheelPosition(97)) // Datapoint 97 - testFirstDerivative(movingRegressor, 30.005326217700258) // Datapoint: 87, Theoretical value: 30.0875556300011, Error: -0.2733% - testSecondDerivative(movingRegressor, -78.81036386632607) // Datapoint: 87, Theoretical value: -76.8409405553369, Error: 2.563% + testFirstDerivative(movingRegressor, 30.0008163530847) // Datapoint: 87, Theoretical value: 30.0875556300011, Error: -0.2733% + testSecondDerivative(movingRegressor, -78.74803078953659) // Datapoint: 87, Theoretical value: -76.8409405553369, Error: 2.563% movingRegressor.push(7.20216298372543, flywheelPosition(98)) // Datapoint 98 - testFirstDerivative(movingRegressor, 27.35906500626146) // Datapoint: 88, Theoretical value: 27.3819824840534, Error: -0.0837% - testSecondDerivative(movingRegressor, -75.91047202675729) // Datapoint: 88, Theoretical value: -71.5980441226457, Error: 6.0231% + testFirstDerivative(movingRegressor, 27.350851766678318) // Datapoint: 88, Theoretical value: 27.3819824840534, Error: -0.0837% + testSecondDerivative(movingRegressor, -75.7917723315129) // Datapoint: 88, Theoretical value: -71.5980441226457, Error: 6.0231% movingRegressor.push(7.30009469043486, flywheelPosition(99)) // Datapoint 99 - testFirstDerivative(movingRegressor, 24.264439359917958) // Datapoint: 89, Theoretical value: 24.6077174058151, Error: -1.395% - testSecondDerivative(movingRegressor, -70.22915254417521) // Datapoint: 89, Theoretical value: -66.0854711273398, Error: 6.2702% + testFirstDerivative(movingRegressor, 24.260710718958364) // Datapoint: 89, Theoretical value: 24.6077174058151, Error: -1.395% + testSecondDerivative(movingRegressor, -70.11339732081393) // Datapoint: 89, Theoretical value: -66.0854711273398, Error: 6.2702% movingRegressor.push(7.37455529281604, flywheelPosition(100)) // Datapoint 100 - testFirstDerivative(movingRegressor, 20.83493250613799) // Datapoint: 90, Theoretical value: 21.7527364967177, Error: -4.2193% - testSecondDerivative(movingRegressor, -63.20422464018079) // Datapoint: 90, Theoretical value: -60.2473455054648, Error: 4.9079% + testFirstDerivative(movingRegressor, 20.837518238273844) // Datapoint: 90, Theoretical value: 21.7527364967177, Error: -4.2193% + testSecondDerivative(movingRegressor, -63.09857048896637) // Datapoint: 90, Theoretical value: -60.2473455054648, Error: 4.9079% movingRegressor.push(7.43533506951812, flywheelPosition(101)) // Datapoint 101 - testFirstDerivative(movingRegressor, 16.540358644462685) // Datapoint: 91, Theoretical value: 18.8004784715502, Error: -12.0216% - testSecondDerivative(movingRegressor, -53.90265452723021) // Datapoint: 91, Theoretical value: -54.0044029484732, Error: -0.1884% + testFirstDerivative(movingRegressor, 16.55328654744659) // Datapoint: 91, Theoretical value: 18.8004784715502, Error: -12.0216% + testSecondDerivative(movingRegressor, -53.82083440127297) // Datapoint: 91, Theoretical value: -54.0044029484732, Error: -0.1884% movingRegressor.push(7.48704928321522, flywheelPosition(102)) // Datapoint 102 - testFirstDerivative(movingRegressor, 12.023279845405199) // Datapoint: 92, Theoretical value: 15.7268191179949, Error: -23.5492% - testSecondDerivative(movingRegressor, -44.51555975741568) // Datapoint: 92, Theoretical value: -47.2370928078489, Error: -5.7614% + testFirstDerivative(movingRegressor, 12.04945082759599) // Datapoint: 92, Theoretical value: 15.7268191179949, Error: -23.5492% + testSecondDerivative(movingRegressor, -44.46299143725113) // Datapoint: 92, Theoretical value: -47.2370928078489, Error: -5.7614% movingRegressor.push(7.53226852670045, flywheelPosition(103)) // Datapoint 103 - testFirstDerivative(movingRegressor, 7.955099967693059) // Datapoint: 93, Theoretical value: 12.4936663152318, Error: -36.3269% - testSecondDerivative(movingRegressor, -35.69289163563958) // Datapoint: 93, Theoretical value: -39.7484244987643, Error: -10.203% + testFirstDerivative(movingRegressor, 7.995979006137162) // Datapoint: 93, Theoretical value: 12.4936663152318, Error: -36.3269% + testSecondDerivative(movingRegressor, -35.676881972128726) // Datapoint: 93, Theoretical value: -39.7484244987643, Error: -10.203% movingRegressor.push(7.57258205468257, flywheelPosition(104)) // Datapoint 104 - testFirstDerivative(movingRegressor, 4.490581497457271) // Datapoint: 94, Theoretical value: 9.03268562508831, Error: -50.2852% - testSecondDerivative(movingRegressor, -26.598232048225814) // Datapoint: 94, Theoretical value: -31.164858820935, Error: -14.6531% + testFirstDerivative(movingRegressor, 4.5454997771441015) // Datapoint: 94, Theoretical value: 9.03268562508831, Error: -50.2852% + testSecondDerivative(movingRegressor, -26.64416615525746) // Datapoint: 94, Theoretical value: -31.164858820935, Error: -14.6531% movingRegressor.push(7.60904584023252, flywheelPosition(105)) // Datapoint 105 - testFirstDerivative(movingRegressor, 3.81590462611652) // Datapoint: 95, Theoretical value: 5.18791555937216, Error: -26.4463% - testSecondDerivative(movingRegressor, -19.03067846863412) // Datapoint: 95, Theoretical value: -20.5611388761721, Error: -7.4435% + testFirstDerivative(movingRegressor, 3.846278301494735) // Datapoint: 95, Theoretical value: 5.18791555937216, Error: -26.4463% + testSecondDerivative(movingRegressor, -19.20758657886873) // Datapoint: 95, Theoretical value: -20.5611388761721, Error: -7.4435% movingRegressor.push(7.64239968419734, flywheelPosition(106)) // Datapoint 106 - testFirstDerivative(movingRegressor, 3.145363823598987) // Datapoint: 96, Theoretical value: 0 - testSecondDerivative(movingRegressor, 1.3823022937382807e-14) // Datapoint: 96, Theoretical value: 0 + testFirstDerivative(movingRegressor, 3.1383576841322074) // Datapoint: 96, Theoretical value: 0 + testSecondDerivative(movingRegressor, 1.3705117896314261e-14) // Datapoint: 96, Theoretical value: 0 movingRegressor.push(7.67318338659667, flywheelPosition(107)) // Datapoint 107 - testFirstDerivative(movingRegressor, 3.8159046261164633) // Datapoint: 97, Theoretical value: 5.18791555937215, Error: -26.4463% - testSecondDerivative(movingRegressor, 19.030678468636108) // Datapoint: 97, Theoretical value: 20.5611388761721, Error: -7.4435% + testFirstDerivative(movingRegressor, 3.846278301494692) // Datapoint: 97, Theoretical value: 5.18791555937215, Error: -26.4463% + testSecondDerivative(movingRegressor, 19.20758657887071) // Datapoint: 97, Theoretical value: 20.5611388761721, Error: -7.4435% movingRegressor.push(7.70180371640116, flywheelPosition(108)) // Datapoint 108 - testFirstDerivative(movingRegressor, 4.490581497456958) // Datapoint: 98, Theoretical value: 9.03268562508831, Error: -50.2852% - testSecondDerivative(movingRegressor, 26.598232048228212) // Datapoint: 98, Theoretical value: 31.164858820935, Error: -14.6531% + testFirstDerivative(movingRegressor, 4.545499777143846) // Datapoint: 98, Theoretical value: 9.03268562508831, Error: -50.2852% + testSecondDerivative(movingRegressor, 26.644166155259857) // Datapoint: 98, Theoretical value: 31.164858820935, Error: -14.6531% movingRegressor.push(7.72857533245789, flywheelPosition(109)) // Datapoint 109 - testFirstDerivative(movingRegressor, 7.955099967689733) // Datapoint: 99, Theoretical value: 12.4936663152318, Error: -36.3269% - testSecondDerivative(movingRegressor, 35.692891635658704) // Datapoint: 99, Theoretical value: 39.7484244987643, Error: -10.203% + testFirstDerivative(movingRegressor, 7.995979006133808) // Datapoint: 99, Theoretical value: 12.4936663152318, Error: -36.3269% + testSecondDerivative(movingRegressor, 35.67688197214775) // Datapoint: 99, Theoretical value: 39.7484244987643, Error: -10.203% movingRegressor.push(7.75374698700685, flywheelPosition(110)) // Datapoint 110 - testFirstDerivative(movingRegressor, 12.023279845411594) // Datapoint: 100, Theoretical value: 15.7268191179949, Error: -23.5492% - testSecondDerivative(movingRegressor, 44.51555975741129) // Datapoint: 100, Theoretical value: 47.2370928078489, Error: -5.7614% + testFirstDerivative(movingRegressor, 12.049450827602357) // Datapoint: 100, Theoretical value: 15.7268191179949, Error: -23.5492% + testSecondDerivative(movingRegressor, 44.46299143724677) // Datapoint: 100, Theoretical value: 47.2370928078489, Error: -5.7614% movingRegressor.push(7.77751896474187, flywheelPosition(111)) // Datapoint 111 - testFirstDerivative(movingRegressor, 16.540358644468085) // Datapoint: 101, Theoretical value: 18.8004784715502, Error: -12.0216% - testSecondDerivative(movingRegressor, 53.9026545272291) // Datapoint: 101, Theoretical value: 54.0044029484732, Error: -0.1884% + testFirstDerivative(movingRegressor, 16.55328654745199) // Datapoint: 101, Theoretical value: 18.8004784715502, Error: -12.0216% + testSecondDerivative(movingRegressor, 53.82083440127187) // Datapoint: 101, Theoretical value: 54.0044029484732, Error: -0.1884% movingRegressor.push(7.80005507028404, flywheelPosition(112)) // Datapoint 112 - testFirstDerivative(movingRegressor, 20.834932506145492) // Datapoint: 102, Theoretical value: 21.7527364967177, Error: -4.2193% - testSecondDerivative(movingRegressor, 63.2042246401701) // Datapoint: 102, Theoretical value: 60.2473455054648, Error: 4.9079% + testFirstDerivative(movingRegressor, 20.837518238281234) // Datapoint: 102, Theoretical value: 21.7527364967177, Error: -4.2193% + testSecondDerivative(movingRegressor, 63.09857048895572) // Datapoint: 102, Theoretical value: 60.2473455054648, Error: 4.9079% movingRegressor.push(7.82149109812353, flywheelPosition(113)) // Datapoint 113 - testFirstDerivative(movingRegressor, 24.264439359926428) // Datapoint: 103, Theoretical value: 24.6077174058151, Error: -1.395% - testSecondDerivative(movingRegressor, 70.22915254415649) // Datapoint: 103, Theoretical value: 66.0854711273397, Error: 6.2702% + testFirstDerivative(movingRegressor, 24.260710718966607) // Datapoint: 103, Theoretical value: 24.6077174058151, Error: -1.395% + testSecondDerivative(movingRegressor, 70.11339732079524) // Datapoint: 103, Theoretical value: 66.0854711273397, Error: 6.2702% movingRegressor.push(7.84194096076223, flywheelPosition(114)) // Datapoint 114 - testFirstDerivative(movingRegressor, 27.359065006266974) // Datapoint: 104, Theoretical value: 27.3819824840534, Error: -0.0837% - testSecondDerivative(movingRegressor, 75.91047202674271) // Datapoint: 104, Theoretical value: 71.5980441226458, Error: 6.0231% + testFirstDerivative(movingRegressor, 27.350851766683718) // Datapoint: 104, Theoretical value: 27.3819824840534, Error: -0.0837% + testSecondDerivative(movingRegressor, 75.79177233149834) // Datapoint: 104, Theoretical value: 71.5980441226458, Error: 6.0231% movingRegressor.push(7.86150121502312, flywheelPosition(115)) // Datapoint 115 - testFirstDerivative(movingRegressor, 30.005326217706283) // Datapoint: 105, Theoretical value: 30.0875556300011, Error: -0.2733% - testSecondDerivative(movingRegressor, 78.81036386630774) // Datapoint: 105, Theoretical value: 76.8409405553369, Error: 2.563% + testFirstDerivative(movingRegressor, 30.000816353090613) // Datapoint: 105, Theoretical value: 30.0875556300011, Error: -0.2733% + testSecondDerivative(movingRegressor, 78.7480307895183) // Datapoint: 105, Theoretical value: 76.8409405553369, Error: 2.563% movingRegressor.push(7.88025446645745, flywheelPosition(116)) // Datapoint 116 - testFirstDerivative(movingRegressor, 32.5566463371847) // Datapoint: 106, Theoretical value: 32.7335342472893, Error: -0.5404% - testSecondDerivative(movingRegressor, 81.74563687144989) // Datapoint: 106, Theoretical value: 81.8553682135038, Error: -0.1341% + testFirstDerivative(movingRegressor, 32.55470427801254) // Datapoint: 106, Theoretical value: 32.7335342472893, Error: -0.5404% + testSecondDerivative(movingRegressor, 81.72110714425453) // Datapoint: 106, Theoretical value: 81.8553682135038, Error: -0.1341% movingRegressor.push(7.89827197143778, flywheelPosition(117)) // Datapoint 117 testFirstDerivative(movingRegressor, 35.034638286363815) // Datapoint: 107, Theoretical value: 35.3270234685551, Error: -0.8277% testSecondDerivative(movingRegressor, 84.72131405454387) // Datapoint: 107, Theoretical value: 86.6727901598316, Error: -2.2515% diff --git a/app/engine/utils/Series.js b/app/engine/utils/Series.js index 7cdf6ca2ce..f0bd0057d1 100644 --- a/app/engine/utils/Series.js +++ b/app/engine/utils/Series.js @@ -1,25 +1,27 @@ 'use strict' /** * @copyright [OpenRowingMonitor]{@link https://github.com/JaapvanEkris/openrowingmonitor} - * - * @file This creates a series with a maximum number of values. It allows for determining the Average, Median, Number of Positive, number of Negative BE AWARE: The median function is extremely CPU intensive for larger series. Use the BinarySearchTree for that situation instead! + * + * @file This creates a series with a maximum number of values. It allows for determining the Average, Median, Number of Positive, number of Negative + * BE AWARE: The median function is extremely CPU intensive for larger series. Use the BinarySearchTree for that situation instead! + * BE AWARE: Accumulators (seriesSum especially) are vulnerable to floating point rounding errors causing drift. Special tests are present in the unit-tests, which should be run manually when this module is changed */ /** - * @param {number} [maxSeriesLength] The maximum length of the series (0 for unlimited) + * @param {number} maxSeriesLength - The maximum length of the series (0 for unlimited) */ export function createSeries (maxSeriesLength = 0) { /** * @type {Array} */ let seriesArray = [] - let seriesSum = 0 let numPos = 0 let numNeg = 0 let min = undefined let max = undefined + let seriesSum = null /** - * @param {float} value to be added to the series + * @param {float} value - value to be added to the series */ function push (value) { if (value === undefined || isNaN(value)) { return } @@ -28,9 +30,7 @@ export function createSeries (maxSeriesLength = 0) { if (max !== undefined) { max = Math.max(max, value) } if (maxSeriesLength > 0 && seriesArray.length >= maxSeriesLength) { - // The maximum of the array has been reached, we have to create room by removing the first - // value from the array - seriesSum -= seriesArray[0] + // The maximum of the array has been reached, we have to create room by removing the first value from the array if (seriesArray[0] > 0) { numPos-- } else { @@ -45,7 +45,8 @@ export function createSeries (maxSeriesLength = 0) { seriesArray.shift() } seriesArray.push(value) - seriesSum += value + seriesSum = null + if (value > 0) { numPos++ } else { @@ -83,7 +84,7 @@ export function createSeries (maxSeriesLength = 0) { } /** - * @param {integer} position to be retrieved, starting at 0 + * @param {integer} position - position to be retrieved, starting at 0 * @returns {float} value at that specific postion in the series */ function get (position) { @@ -95,7 +96,7 @@ export function createSeries (maxSeriesLength = 0) { } /** - * @param {float} tested value + * @param {float} testedValue - tested value * @returns {integer} count of values in the series above the tested value */ function numberOfValuesAbove (testedValue) { @@ -115,7 +116,7 @@ export function createSeries (maxSeriesLength = 0) { } /** - * @param {float} tested value + * @param {float} testedValue - tested value * @returns {integer} number of values in the series below or equal to the tested value */ function numberOfValuesEqualOrBelow (testedValue) { @@ -136,8 +137,12 @@ export function createSeries (maxSeriesLength = 0) { /** * @returns {float} sum of the entire series + * @description This determines the total sum of the series. As a running sum becomes unstable after longer running sums, we need to summarise this via a reduce */ function sum () { + if (seriesSum === null) { + seriesSum = (seriesArray.length > 0 ? seriesArray.reduce((total, item) => total + item) : 0) + } return seriesSum } @@ -146,7 +151,7 @@ export function createSeries (maxSeriesLength = 0) { */ function average () { if (seriesArray.length > 0) { - return seriesSum / seriesArray.length + return sum() / seriesArray.length } else { return 0 } @@ -177,7 +182,7 @@ export function createSeries (maxSeriesLength = 0) { } /** - * @returns {float} median of the series + * @returns {float} median of the series * @description returns the median of the series. As this is a CPU intensive approach, DO NOT USE FOR LARGE SERIES!. For larger series, use the BinarySearchTree.js instead */ function median () { @@ -207,7 +212,6 @@ export function createSeries (maxSeriesLength = 0) { function reset () { seriesArray = /** @type {Array} */(/** @type {unknown} */(null)) seriesArray = [] - seriesSum = 0 numPos = 0 numNeg = 0 min = undefined diff --git a/app/engine/utils/Series.test.js b/app/engine/utils/Series.test.js index 1d9962c3ec..94d63d45fb 100644 --- a/app/engine/utils/Series.test.js +++ b/app/engine/utils/Series.test.js @@ -1,15 +1,19 @@ 'use strict' -/* - Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor -*/ /** - * As this object is fundamental for most other utility objects, we must test its behaviour quite thoroughly + * @copyright [OpenRowingMonitor]{@link https://github.com/JaapvanEkris/openrowingmonitor} + * + * @file Tests of the Series object. As this object is fundamental for most other utility objects, we must test its behaviour quite thoroughly + * Please note: this file contains commented out stress tests of the length(), sum(), average() functions, to detect any issues with numerical stability + * As these tests tend to run in the dozens of minutes, we do not run them systematically, but they should be run when the series object is changed. */ import { test } from 'uvu' import * as assert from 'uvu/assert' import { createSeries } from './Series.js' +/** + * @description Test behaviour for no datapoints + */ test('Series behaviour with an empty series', () => { const dataSeries = createSeries(3) testLength(dataSeries, 0) @@ -26,6 +30,9 @@ test('Series behaviour with an empty series', () => { testMaximum(dataSeries, 0) }) +/** + * @description Test behaviour for a single datapoint + */ test('Series behaviour with a single pushed value. Series = [9]', () => { const dataSeries = createSeries(3) dataSeries.push(9) @@ -43,6 +50,9 @@ test('Series behaviour with a single pushed value. Series = [9]', () => { testMaximum(dataSeries, 9) }) +/** + * @description Test behaviour for two datapoints + */ test('Series behaviour with a second pushed value. Series = [9, 3]', () => { const dataSeries = createSeries(3) dataSeries.push(9) @@ -61,6 +71,9 @@ test('Series behaviour with a second pushed value. Series = [9, 3]', () => { testMaximum(dataSeries, 9) }) +/** + * @description Test behaviour for three datapoints + */ test('Series behaviour with a third pushed value. Series = [9, 3, 6]', () => { const dataSeries = createSeries(3) dataSeries.push(9) @@ -80,6 +93,9 @@ test('Series behaviour with a third pushed value. Series = [9, 3, 6]', () => { testMaximum(dataSeries, 9) }) +/** + * @description Test behaviour for four datapoints + */ test('Series behaviour with a fourth pushed value. Series = [3, 6, 12]', () => { const dataSeries = createSeries(3) dataSeries.push(9) @@ -100,6 +116,9 @@ test('Series behaviour with a fourth pushed value. Series = [3, 6, 12]', () => { testMaximum(dataSeries, 12) }) +/** + * @description Test behaviour for five datapoints + */ test('Series behaviour with a fifth pushed value. Series = [6, 12, -3]', () => { const dataSeries = createSeries(3) dataSeries.push(9) @@ -121,6 +140,9 @@ test('Series behaviour with a fifth pushed value. Series = [6, 12, -3]', () => { testMaximum(dataSeries, 12) }) +/** + * @description Test behaviour for recalculations of the min/max values + */ test('Series behaviour pushing out the min and max value and forcing a recalculate of min/max via the array.', () => { const dataSeries = createSeries(3) dataSeries.push(9) @@ -136,6 +158,9 @@ test('Series behaviour pushing out the min and max value and forcing a recalcula testMaximum(dataSeries, 6) }) +/** + * @description Test behaviour for recalculations of the min/max values + */ test('Series behaviour pushing out the min and max value, replacing them just in time.', () => { const dataSeries = createSeries(3) dataSeries.push(9) @@ -151,6 +176,9 @@ test('Series behaviour pushing out the min and max value, replacing them just in testMaximum(dataSeries, 12) }) +/** + * @description Test behaviour after a reset() + */ test('Series behaviour with a five pushed values followed by a reset, Series = []', () => { const dataSeries = createSeries(3) dataSeries.push(9) @@ -171,6 +199,43 @@ test('Series behaviour with a five pushed values followed by a reset, Series = [ testMedian(dataSeries, 0) }) +/* These stress tests test the reliability of the sum(), average() and length() function after a huge number of updates +// This specific test takes a long time (over 10 minutes), so only run them manually when changing the series module +// Javascript maximum array length is 4294967295, as heap memory is limited, we stay with 2^25 datapoints +test('Stress test of the series object, 33.554.432 (2^25) datapoints', () => { + const dataSeries = createSeries() + let j = 0 + let randomvalue + while (j < 16777216) { + randomvalue = Math.random() + dataSeries.push(randomvalue) + dataSeries.push(1 - randomvalue) + j++ + } + testLength(dataSeries, 33554432) + testSum(dataSeries, 16777216) + testAverage(dataSeries, 0.5) + testMedian(dataSeries, 0.5) +}) + +// Javascript maximum array length is 4294967295, as heap memory is limited, we stay with 2^25 datapoints +// This test takes several hours (!) due to the many large array shifts, so only run them manually when changing the series module +test('Stress test of the series object, 67.108.864 datapoints, with a maxLength of 33.554.432 (2^25)', () => { + const dataSeries = createSeries(33554432) + let j = 0 + let randomvalue + while (j < 33554432) { + randomvalue = Math.random() + dataSeries.push(randomvalue) + dataSeries.push(1 - randomvalue) + j++ + } + testLength(dataSeries, 33554432) + testSum(dataSeries, 16777216) + testAverage(dataSeries, 0.5) + testMedian(dataSeries, 0.5) +}) */ + function testLength (series, expectedValue) { assert.ok(series.length() === expectedValue, `Expected length should be ${expectedValue}, encountered ${series.length()}`) } diff --git a/app/engine/utils/TSLinearSeries.js b/app/engine/utils/TSLinearSeries.js index e8dd460477..17b8f8099e 100644 --- a/app/engine/utils/TSLinearSeries.js +++ b/app/engine/utils/TSLinearSeries.js @@ -1,11 +1,10 @@ 'use strict' -/* - Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor -*/ /** - * The TSLinearSeries is a datatype that represents a Linear Series. It allows + * @copyright [OpenRowingMonitor]{@link https://github.com/JaapvanEkris/openrowingmonitor} + * + * @file The TSLinearSeries is a datatype that represents a Weighted Linear Series. It allows * values to be retrieved (like a FiFo buffer, or Queue) but it also includes - * a Theil-Sen estimator Linear Regressor to determine the slope of this timeseries. + * a Weighted Theil-Sen estimator Linear Regressor to determine the slope of this timeseries. * * At creation its length is determined. After it is filled, the oldest will be pushed * out of the queue) automatically. This is a property of the Series object @@ -30,11 +29,13 @@ import loglevel from 'loglevel' const log = loglevel.getLogger('RowingEngine') /** - * @param {integer} the maximum length of the quadratic series, 0 for unlimited + * @param {integer} maxSeriesLength - the maximum length of the quadratic series, default = 0 for unlimited */ export function createTSLinearSeries (maxSeriesLength = 0) { const X = createSeries(maxSeriesLength) const Y = createSeries(maxSeriesLength) + const weight = createSeries(maxSeriesLength) + const WY = createSeries(maxSeriesLength) const A = createLabelledBinarySearchTree() let _A = 0 @@ -43,37 +44,44 @@ export function createTSLinearSeries (maxSeriesLength = 0) { let _goodnessOfFit = 0 /** - * @param {float} the x value of the datapoint - * @param {float} the y value of the datapoint + * @param {float} x - the x value of the datapoint + * @param {float} y - the y value of the datapoint + * @param {float} w - the weight of the datapoint (optional, defaults to 1 for unweighted regression) * Invariant: BinarySearchTree A contains all calculated a's (as in the general formula y = a * x + b), * where the a's are labeled in the BinarySearchTree with their Xi when they BEGIN in the point (Xi, Yi) */ - function push (x, y) { + function push (x, y, w = 1) { if (x === undefined || isNaN(x) || y === undefined || isNaN(y)) { return } if (maxSeriesLength > 0 && X.length() >= maxSeriesLength) { // The maximum of the array has been reached, so when pushing the x,y the array gets shifted, - // thus we have to remove the a's belonging to the current position X0 as well before this value is trashed + // thus we have to remove the a's belonging to the current position X[0] as well before this value is trashed A.remove(X.get(0)) } X.push(x) Y.push(y) + weight.push(w) + WY.push(w * y) // Calculate all the slopes of the newly added point if (X.length() > 1) { // There are at least two points in the X and Y arrays, so let's add the new datapoint let i = 0 + let slope + let combinedweight while (i < X.length() - 1) { // Calculate the slope with all preceeding datapoints and X.length() - 1'th datapoint (as the array starts at zero) - A.push(X.get(i), calculateSlope(i, X.length() - 1), 1) + slope = calculateSlope(i, X.length() - 1) + combinedweight = weight.get(i) * w + A.push(X.get(i), slope, combinedweight) i++ } } // Calculate the median of the slopes if (X.length() > 1) { - _A = A.median() + _A = A.weightedMedian() } else { _A = 0 } @@ -122,36 +130,43 @@ export function createTSLinearSeries (maxSeriesLength = 0) { } /** - * @returns {float} the R^2 as a goodness of fit indicator + * @returns {float} the R^2 as a global goodness of fit indicator * It will automatically recalculate the _goodnessOfFit when it isn't defined - * This lazy approach is intended to prevent unneccesary calculations, especially when there is a batch of datapoint pushes - * from the TSQuadratic regressor processing its linear residu + * This lazy approach is intended to prevent unneccesary calculations, especially when there is a batch of datapoints + * pushes from the TSQuadratic regressor processing its linear residu + * @see [Goodness-of-Fit Statistics] {@link https://web.maths.unsw.edu.au/~adelle/Garvan/Assays/GoodnessOfFit.html} */ function goodnessOfFit () { let i = 0 let sse = 0 + calculateIntercept() if (_goodnessOfFit === null) { if (X.length() >= 2) { _sst = 0 + + // Calculate weighted R^2 + const weightedAverageY = WY.sum() / weight.sum() + while (i < X.length()) { - sse += Math.pow((Y.get(i) - projectX(X.get(i))), 2) - _sst += Math.pow((Y.get(i) - Y.average()), 2) + sse += weight.get(i) * Math.pow(Y.get(i) - projectX(X.get(i)), 2) + _sst += weight.get(i) * Math.pow(Y.get(i) - weightedAverageY, 2) i++ } + switch (true) { case (sse === 0): _goodnessOfFit = 1 break case (sse > _sst): // This is a pretty bad fit as the error is bigger than just using the line for the average y as intercept - _goodnessOfFit = 0 + _goodnessOfFit = 0.01 break case (_sst !== 0): _goodnessOfFit = 1 - (sse / _sst) break default: // When SST = 0, R2 isn't defined - _goodnessOfFit = 0 + _goodnessOfFit = 0.01 } } else { _goodnessOfFit = 0 @@ -161,6 +176,7 @@ export function createTSLinearSeries (maxSeriesLength = 0) { } /** + * @param {integer} position - The position in the series for which the Local Goodness Of Fit has to be calcuated * @returns {float} the local R^2 as a local goodness of fit indicator */ function localGoodnessOfFit (position) { @@ -169,22 +185,22 @@ export function createTSLinearSeries (maxSeriesLength = 0) { goodnessOfFit() } if (X.length() >= 2 && position < X.length()) { - const squaredError = Math.pow((Y.get(position) - projectX(X.get(position))), 2) + const weightedSquaredError = weight.get(position) * Math.pow((Y.get(position) - projectX(X.get(position))), 2) /* eslint-disable no-unreachable -- rather be systematic and add a break in all case statements */ switch (true) { - case (squaredError === 0): + case (weightedSquaredError === 0): return 1 break - case (squaredError > _sst): + case (weightedSquaredError > _sst): // This is a pretty bad fit as the error is bigger than just using the line for the average y as intercept - return 0 + return 0.01 break case (_sst !== 0): - return Math.min(Math.max(1 - ((squaredError * X.length()) / _sst), 0), 1) + return Math.min(Math.max(1 - ((weightedSquaredError * X.length()) / _sst), 0), 1) break default: // When _SST = 0, localGoodnessOfFit isn't defined - return 0 + return 0.01 } /* eslint-enable no-unreachable */ } else { @@ -193,7 +209,7 @@ export function createTSLinearSeries (maxSeriesLength = 0) { } /** - * @param {float} the x value to be projected + * @param {float} x - the x value to be projected * @returns {float} the resulting y value when projected via the linear function */ function projectX (x) { @@ -206,8 +222,8 @@ export function createTSLinearSeries (maxSeriesLength = 0) { } /** - * @param {float} the y value to be projected - * @returns {float} the resulting x value when projected via the linear function + * @param {float} y - the y value to be solved + * @returns {float} the resulting x value when solved via the linear function */ function projectY (y) { if (X.length() >= 2 && _A !== 0) { @@ -219,6 +235,11 @@ export function createTSLinearSeries (maxSeriesLength = 0) { } } + /** + * @param {integer} pointOne - The position in the series of the first datapoint used for the slope calculation + * @param {integer} pointTwo - The position in the series of the second datapoint used for the slope calculation + * @returns {float} the slope of the linear function + */ function calculateSlope (pointOne, pointTwo) { if (pointOne !== pointTwo && X.get(pointOne) !== X.get(pointTwo)) { return ((Y.get(pointTwo) - Y.get(pointOne)) / (X.get(pointTwo) - X.get(pointOne))) @@ -228,6 +249,9 @@ export function createTSLinearSeries (maxSeriesLength = 0) { } } + /** + * @description This helper function calculates the intercept and stores it in _B + */ function calculateIntercept () { // Calculate all the intercepts for the newly added point and the newly calculated A, when needed // This function is only called when an intercept is really needed, as this saves a lot of CPU cycles when only a slope suffices @@ -237,11 +261,11 @@ export function createTSLinearSeries (maxSeriesLength = 0) { // There are at least two points in the X and Y arrays, so let's calculate the intercept let i = 0 while (i < X.length()) { - // Please note , as we need to recreate the B-tree for each newly added datapoint anyway, the label i isn't relevant - B.push(i, (Y.get(i) - (_A * X.get(i)))) + // Please note, we recreate the B-tree for each newly added datapoint anyway, so the label i isn't relevant + B.push(i, (Y.get(i) - (_A * X.get(i))), weight.get(i)) i++ } - _B = B.median() + _B = B.weightedMedian() } else { _B = 0 } @@ -256,11 +280,16 @@ export function createTSLinearSeries (maxSeriesLength = 0) { return (X.length() >= 2) } + /** + * @description This function is used for clearing data and state, bringing it back to its original state + */ function reset () { if (X.length() > 0) { // There is something to reset X.reset() Y.reset() + weight.reset() + WY.reset() A.reset() _A = 0 _B = 0 diff --git a/app/engine/utils/TSLinearSeries.test.js b/app/engine/utils/TSLinearSeries.test.js index 7169262b18..e6aec4f722 100644 --- a/app/engine/utils/TSLinearSeries.test.js +++ b/app/engine/utils/TSLinearSeries.test.js @@ -110,6 +110,34 @@ test('Correct behaviour of a series after several puhed values, function y = 3x testLocalGoodnessOfFitEquals(dataSeries, 3, 0) // Overshooting the length of the series }) +test('Correct behaviour of a series after several puhed values, function y = 3x - 6, noisefree, 3 datapoints, uniform (halved) weights', () => { + const dataSeries = createTSLinearSeries(3) + dataSeries.push(5, 9, 0.5) + dataSeries.push(3, 3, 0.5) + dataSeries.push(4, 6, 0.5) + testLength(dataSeries, 3) + testXAtSeriesBegin(dataSeries, 5) + testYAtSeriesBegin(dataSeries, 9) + testXAtSeriesEnd(dataSeries, 4) + testYAtSeriesEnd(dataSeries, 6) + testNumberOfXValuesAbove(dataSeries, 0, 3) + testNumberOfYValuesAbove(dataSeries, 0, 3) + testNumberOfXValuesEqualOrBelow(dataSeries, 0, 0) + testNumberOfYValuesEqualOrBelow(dataSeries, 0, 0) + testNumberOfXValuesAbove(dataSeries, 10, 0) + testNumberOfYValuesAbove(dataSeries, 10, 0) + testNumberOfXValuesEqualOrBelow(dataSeries, 10, 3) + testNumberOfYValuesEqualOrBelow(dataSeries, 10, 3) + testXSum(dataSeries, 12) + testYSum(dataSeries, 18) + testSlopeEquals(dataSeries, 3) + testInterceptEquals(dataSeries, -6) + testGoodnessOfFitEquals(dataSeries, 1) + testLocalGoodnessOfFitEquals(dataSeries, 0, 1) + testLocalGoodnessOfFitEquals(dataSeries, 1, 1) + testLocalGoodnessOfFitEquals(dataSeries, 2, 1) +}) + test('Correct behaviour of a series after several puhed values, function y = 3x - 6, noisefree, 4 datapoints', () => { const dataSeries = createTSLinearSeries(3) dataSeries.push(5, 9) @@ -169,6 +197,50 @@ test('Correct behaviour of a series after several puhed values, function y = 3x testLocalGoodnessOfFitEquals(dataSeries, 2, 1) }) +test('Series with 5 elements, with 2 noisy datapoints, ideal function y = 3x - 6, uniform weights', () => { + const dataSeries = createTSLinearSeries(5) + dataSeries.push(5, 9) + dataSeries.push(3, 2) + dataSeries.push(4, 7) + dataSeries.push(6, 12) + dataSeries.push(1, -3) + testSlopeEquals(dataSeries, 3) // Theoretical noisefree value 3 + testInterceptEquals(dataSeries, -6) // Theoretical noisefree value -6 + testGoodnessOfFitEquals(dataSeries, 0.9858356940509915) // Ideal value 1 + testLocalGoodnessOfFitEquals(dataSeries, 0, 1) + testXProjectionEquals(dataSeries, 1, -3) // Theoretical noisefree value -3 + testLocalGoodnessOfFitEquals(dataSeries, 1, 0.9645892351274787) + testXProjectionEquals(dataSeries, 3, 3) // Theoretical noisefree value 3 + testLocalGoodnessOfFitEquals(dataSeries, 2, 0.9645892351274787) + testXProjectionEquals(dataSeries, 4, 6) // Theoretical noisefree value 6 + testLocalGoodnessOfFitEquals(dataSeries, 3, 1) + testXProjectionEquals(dataSeries, 5, 9) // Theoretical noisefree value 9 + testLocalGoodnessOfFitEquals(dataSeries, 4, 1) + testXProjectionEquals(dataSeries, 6, 12) // Theoretical noisefree value 12 +}) + +test('Series with 5 elements, with 2 noisy datapoints, ideal function y = 3x - 6, non-uniform weights', () => { + const dataSeries = createTSLinearSeries(5) + dataSeries.push(5, 9, 1) + dataSeries.push(3, 2, 0.5) + dataSeries.push(4, 7, 0.5) + dataSeries.push(6, 12, 1) + dataSeries.push(1, -3, 1) + testSlopeEquals(dataSeries, 3) // Theoretical noisefree value 3 + testInterceptEquals(dataSeries, -6) // Theoretical noisefree value -6 + testGoodnessOfFitEquals(dataSeries, 0.9925338310779281) // Ideal value 1 + testLocalGoodnessOfFitEquals(dataSeries, 0, 1) + testXProjectionEquals(dataSeries, 1, -3) // Theoretical noisefree value -3 + testLocalGoodnessOfFitEquals(dataSeries, 1, 0.9813345776948204) + testXProjectionEquals(dataSeries, 3, 3) // Theoretical noisefree value 3 + testLocalGoodnessOfFitEquals(dataSeries, 2, 0.9813345776948204) + testXProjectionEquals(dataSeries, 4, 6) // Theoretical noisefree value 6 + testLocalGoodnessOfFitEquals(dataSeries, 3, 1) + testXProjectionEquals(dataSeries, 5, 9) // Theoretical noisefree value 9 + testLocalGoodnessOfFitEquals(dataSeries, 4, 1) + testXProjectionEquals(dataSeries, 6, 12) // Theoretical noisefree value 12 +}) + test('Correct behaviour of a series after several puhed values, function y = 3x - 6, noisefree, 4 datapoints and a reset', () => { const dataSeries = createTSLinearSeries(3) dataSeries.push(5, 9) @@ -197,23 +269,6 @@ test('Correct behaviour of a series after several puhed values, function y = 3x testLocalGoodnessOfFitEquals(dataSeries, 0, 0) }) -test('Series for function y = 3x - 6, with 5 elements, with 2 noisy datapoints', () => { - const dataSeries = createTSLinearSeries(5) - dataSeries.push(5, 9) - dataSeries.push(3, 2) - dataSeries.push(4, 7) - dataSeries.push(6, 12) - dataSeries.push(1, -3) - testSlopeBetween(dataSeries, 2.9, 3.1) - testInterceptBetween(dataSeries, -6.3, -5.8) - testGoodnessOfFitBetween(dataSeries, 0.9, 1.0) - testLocalGoodnessOfFitEquals(dataSeries, 0, 1) - testLocalGoodnessOfFitEquals(dataSeries, 1, 0.9645892351274787) - testLocalGoodnessOfFitEquals(dataSeries, 2, 0.9645892351274787) - testLocalGoodnessOfFitEquals(dataSeries, 3, 1) - testLocalGoodnessOfFitEquals(dataSeries, 4, 1) -}) - function testLength (series, expectedValue) { assert.ok(series.length() === expectedValue, `Expected length should be ${expectedValue}, encountered a ${series.length()}`) } @@ -262,31 +317,20 @@ function testSlopeEquals (series, expectedValue) { assert.ok(series.slope() === expectedValue, `Expected slope to be ${expectedValue}, encountered a ${series.slope()}`) } -function testSlopeBetween (series, expectedValueAbove, expectedValueBelow) { - assert.ok(series.slope() > expectedValueAbove, `Expected slope to be above ${expectedValueAbove}, encountered a ${series.slope()}`) - assert.ok(series.slope() < expectedValueBelow, `Expected slope to be below ${expectedValueBelow}, encountered a ${series.slope()}`) -} - function testInterceptEquals (series, expectedValue) { assert.ok(series.intercept() === expectedValue, `Expected intercept to be ${expectedValue}, encountered ${series.intercept()}`) } -function testInterceptBetween (series, expectedValueAbove, expectedValueBelow) { - assert.ok(series.intercept() > expectedValueAbove, `Expected intercept to be above ${expectedValueAbove}, encountered ${series.intercept()}`) - assert.ok(series.intercept() < expectedValueBelow, `Expected intercept to be below ${expectedValueBelow}, encountered ${series.intercept()}`) -} - function testGoodnessOfFitEquals (series, expectedValue) { assert.ok(series.goodnessOfFit() === expectedValue, `Expected goodnessOfFit to be ${expectedValue}, encountered ${series.goodnessOfFit()}`) } -function testGoodnessOfFitBetween (series, expectedValueAbove, expectedValueBelow) { - assert.ok(series.goodnessOfFit() > expectedValueAbove, `Expected goodnessOfFit to be above ${expectedValueAbove}, encountered ${series.goodnessOfFit()}`) - assert.ok(series.goodnessOfFit() < expectedValueBelow, `Expected goodnessOfFit to be below ${expectedValueBelow}, encountered ${series.goodnessOfFit()}`) -} - function testLocalGoodnessOfFitEquals (series, position, expectedValue) { assert.ok(series.localGoodnessOfFit(position) === expectedValue, `Expected localGoodnessOfFit at position ${position} to be ${expectedValue}, encountered ${series.localGoodnessOfFit(position)}`) } +function testXProjectionEquals (series, value, expectedValue) { + assert.ok(series.projectX(value) === expectedValue, `Expected projectX at value ${value} to be ${expectedValue}, encountered ${series.projectX(value)}`) +} + test.run() diff --git a/app/engine/utils/TSQuadraticSeries.js b/app/engine/utils/TSQuadraticSeries.js index bf411b268e..c7d3ca07e0 100644 --- a/app/engine/utils/TSQuadraticSeries.js +++ b/app/engine/utils/TSQuadraticSeries.js @@ -1,9 +1,8 @@ 'use strict' -/* - Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor -*/ /** - * The FullTSQuadraticSeries is a datatype that represents a Quadratic Series. It allows + * @copyright [OpenRowingMonitor]{@link https://github.com/JaapvanEkris/openrowingmonitor} + * + * @file The FullTSQuadraticSeries is a datatype that represents a Quadratic Series. It allows * values to be retrieved (like a FiFo buffer, or Queue) but it also includes * a Theil-Sen Quadratic Regressor to determine the coefficients of this dataseries. * @@ -33,11 +32,13 @@ import loglevel from 'loglevel' const log = loglevel.getLogger('RowingEngine') /** - * @param {integer} the maximum length of the quadratic series, 0 for unlimited + * @param {integer} maxSeriesLength - the maximum length of the quadratic series, 0 for unlimited */ export function createTSQuadraticSeries (maxSeriesLength = 0) { const X = createSeries(maxSeriesLength) const Y = createSeries(maxSeriesLength) + const weight = createSeries(maxSeriesLength) + const WY = createSeries(maxSeriesLength) const A = createLabelledBinarySearchTree() const linearResidu = createTSLinearSeries(maxSeriesLength) let _A = 0 @@ -47,12 +48,14 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) { let _goodnessOfFit = 0 /** - * @param {float} the x value of the datapoint - * @param {float} the y value of the datapoint + * @param {float} x - the x value of the datapoint + * @param {float} y - the y value of the datapoint + * @param {float} w - the weight of the datapoint (defaults to 1) * Invariant: BinrySearchTree A contains all calculated a's (as in the general formula y = a * x^2 + b * x + c), * where the a's are labeled in the BinarySearchTree with their Xi when they BEGIN in the point (Xi, Yi) */ - function push (x, y) { + /* eslint-disable max-statements -- A lot of variables have to be set */ + function push (x, y, w = 1) { if (x === undefined || isNaN(x) || y === undefined || isNaN(y)) { return } if (maxSeriesLength > 0 && X.length() >= maxSeriesLength) { @@ -63,43 +66,46 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) { X.push(x) Y.push(y) + weight.push(w) + WY.push(w * y) + _A = 0 + _B = 0 + _C = 0 + _sst = 0 + _goodnessOfFit = 0 - // Calculate the coefficient a for the new interval by adding the newly added datapoint - let i = 0 - let j = 0 + if (X.length() >= 3) { + // 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 + let i = 0 + let j = 0 - switch (true) { - case (X.length() >= 3): - // 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 - // First we calculate the A for the formula - while (i < X.length() - 2) { - j = i + 1 - while (j < X.length() - 1) { - A.push(X.get(i), calculateA(i, j, X.length() - 1), 1) - j++ - } - i++ + // First we calculate the A for the formula + let combinedweight = 0 + let coeffA = 1 + while (i < X.length() - 2) { + j = i + 1 + while (j < X.length() - 1) { + combinedweight = weight.get(i) * weight.get(j) * w + coeffA = calculateA(i, j, X.length() - 1) + A.push(X.get(i), coeffA, combinedweight) + j++ } - _A = A.median() + i++ + } + _A = A.weightedMedian() - // We invalidate the linearResidu, B, C, and goodnessOfFit, as this will trigger a recalculate when they are needed - linearResidu.reset() - _B = null - _C = null - _sst = null - _goodnessOfFit = null - break - default: - _A = 0 - _B = 0 - _C = 0 - _sst = 0 - _goodnessOfFit = 0 + // We invalidate the linearResidu, B, C, and goodnessOfFit, as this will trigger a recalculate when they are needed + linearResidu.reset() + _B = null + _C = null + _sst = null + _goodnessOfFit = null } } + /* eslint-enable max-statements */ /** - * @param {integer} the position in the flank of the requested value (default = 0) + * @param {integer} position - the position in the flank of the requested value (default = 0) * @returns {float} the firdt derivative of the quadratic function y = a x^2 + b x + c */ function firstDerivativeAtPosition (position = 0) { @@ -112,7 +118,7 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) { } /** - * @param {integer} the position in the flank of the requested value (default = 0) + * @param {integer} position - the position in the flank of the requested value (default = 0) * @returns {float} the second derivative of the quadratic function y = a x^2 + b x + c */ function secondDerivativeAtPosition (position = 0) { @@ -124,7 +130,7 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) { } /** - * @param {float} the x value of the requested value + * @param {float} x - the x value of the requested value * @returns {float} the slope of the linear function */ function slope (x) { @@ -137,14 +143,14 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) { } /** - * @returns {float} the coefficient a of the quadratic function y = a x^2 + b x + c + * @returns {float} the (quadratic) coefficient a of the quadratic function y = a x^2 + b x + c */ function coefficientA () { return _A } /** - * @returns {float} the coefficient b of the quadratic function y = a x^2 + b x + c + * @returns {float} the (linear) coefficient b of the quadratic function y = a x^2 + b x + c */ function coefficientB () { calculateB() @@ -152,7 +158,7 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) { } /** - * @returns {float} the coefficient c of the quadratic function y = a x^2 + b x + c + * @returns {float} the (intercept) coefficient c of the quadratic function y = a x^2 + b x + c */ function coefficientC () { calculateB() @@ -177,33 +183,38 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) { } /** - * @returns {float} the R^2 as a goodness of fit indicator + * @returns {float} the R^2 as a global goodness of fit indicator */ function goodnessOfFit () { let i = 0 let sse = 0 if (_goodnessOfFit === null) { + calculateB() + calculateC() if (X.length() >= 3) { _sst = 0 + const weightedAverageY = WY.sum() / weight.sum() + while (i < X.length()) { - sse += Math.pow((Y.get(i) - projectX(X.get(i))), 2) - _sst += Math.pow((Y.get(i) - Y.average()), 2) + sse += weight.get(i) * Math.pow(Y.get(i) - projectX(X.get(i)), 2) + _sst += weight.get(i) * Math.pow(Y.get(i) - weightedAverageY, 2) i++ } + switch (true) { case (sse === 0): _goodnessOfFit = 1 break case (sse > _sst): // This is a pretty bad fit as the error is bigger than just using the line for the average y as intercept - _goodnessOfFit = 0 + _goodnessOfFit = 0.01 break case (_sst !== 0): _goodnessOfFit = 1 - (sse / _sst) break default: // When _SST = 0, R2 isn't defined - _goodnessOfFit = 0 + _goodnessOfFit = 0.01 } } else { _goodnessOfFit = 0 @@ -229,14 +240,14 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) { break case (squaredError > _sst): // This is a pretty bad fit as the error is bigger than just using the line for the average y as intercept - return 0 + return 0.01 break case (_sst !== 0): return Math.min(Math.max(1 - ((squaredError * X.length()) / _sst), 0), 1) break default: // When _SST = 0, localGoodnessOfFit isn't defined - return 0 + return 0.01 } /* eslint-enable no-unreachable */ } else { @@ -245,7 +256,7 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) { } /** - * @param {float} the x value to be projected + * @param {float} x - the x value to be projected * @returns {float} the resulting y value when projected via the linear function */ function projectX (x) { @@ -258,6 +269,12 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) { } } + /** + * @param {integer} pointOne - The position in the series of the first datapoint used for the quadratic coefficient calculation + * @param {integer} pointTwo - The position in the series of the second datapoint used for the quadratic coefficient calculation + * @param {integer} pointThree - The position in the series of the third datapoint used for the quadratic coefficient calculation + * @returns {float} the coefficient A of the linear function + */ function calculateA (pointOne, pointTwo, pointThree) { let result = 0 if (X.get(pointOne) !== X.get(pointTwo) && X.get(pointOne) !== X.get(pointThree) && X.get(pointTwo) !== X.get(pointThree)) { @@ -270,6 +287,9 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) { } } + /** + * @description This helper function calculates the slope of the linear residu and stores it in _B + */ function calculateB () { // Calculate all the linear slope for the newly added point and the newly calculated A // 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 +303,9 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) { } } + /** + * @description This helper function calculates the intercept of the linear residu and stores it in _C + */ function calculateC () { // Calculate all the intercept for the newly added point and the newly calculated A // 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 +319,15 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) { } } + /** + * @description This helper function fills the linear residu + */ function fillLinearResidu () { // To calculate the B and C via Linear regression over the residu, we need to fill it if empty if (linearResidu.length() === 0) { let i = 0 while (i < X.length()) { - linearResidu.push(X.get(i), Y.get(i) - (_A * Math.pow(X.get(i), 2))) + linearResidu.push(X.get(i), Y.get(i) - (_A * Math.pow(X.get(i), 2)), weight.get(i)) i++ } } @@ -314,11 +340,16 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) { return (X.length() >= 3) } + /** + * @description This function is used for clearing data and state + */ function reset () { if (X.length() > 0) { // There is something to reset X.reset() Y.reset() + weight.reset() + WY.reset() A.reset() linearResidu.reset() _A = 0 diff --git a/app/engine/utils/WLSLinearSeries.js b/app/engine/utils/WLSLinearSeries.js index 255ad93dac..b596616ea3 100644 --- a/app/engine/utils/WLSLinearSeries.js +++ b/app/engine/utils/WLSLinearSeries.js @@ -1,7 +1,7 @@ 'use strict' /** * @copyright [OpenRowingMonitor]{@link https://github.com/JaapvanEkris/openrowingmonitor} - * + * * @file The WLSLinearSeries 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 Weighted Linear Regressor to determine the slope, intercept and R^2 of this series @@ -45,7 +45,7 @@ export function createWLSLinearSeries (maxSeriesLength = 0) { /** * @param {float} x - the x value of the datapoint * @param {float} y - the y value of the datapoint - * @param {float} w - the observation weight of the datapoint, default = 1 + * @param {float} w - the weight of the datapoint, default = 1 */ function push (x, y, w = 1) { if (x === undefined || isNaN(x) || y === undefined || isNaN(y)) { return } @@ -69,13 +69,13 @@ export function createWLSLinearSeries (maxSeriesLength = 0) { _intercept = (WY.sum() - _slope * WX.sum()) / weight.sum() // Calculate weighted R^2 - const yMean = WY.sum() / weight.sum() - const ssRes = WYY.sum() - (2 * _intercept * WY.sum()) - (2 * _slope * WXY.sum()) + + const weighedAverageY = WY.sum() / weight.sum() + const sse = WYY.sum() - (2 * _intercept * WY.sum()) - (2 * _slope * WXY.sum()) + (_intercept * _intercept * weight.sum()) + (2 * _slope * _intercept * WX.sum()) + (_slope * _slope * WXX.sum()) - const ssTot = WYY.sum() - (yMean * yMean * weight.sum()) + const sst = WYY.sum() - (weighedAverageY * weighedAverageY * weight.sum()) - _goodnessOfFit = (ssTot !== 0) ? 1 - (ssRes / ssTot) : 0 + _goodnessOfFit = (sst !== 0) ? 1 - (sse / sst) : 0 } else { _slope = 0 _intercept = 0 @@ -147,6 +147,9 @@ export function createWLSLinearSeries (maxSeriesLength = 0) { return (X.length() >= 2 && _slope !== 0) } + /** + * @description This function is used for clearing all data, typically when flywheel.js is completely reset + */ function reset () { X.reset() Y.reset() diff --git a/app/engine/utils/WLSLinearSeries.test.js b/app/engine/utils/WLSLinearSeries.test.js index c51bd35d0f..2d7ee4692b 100644 --- a/app/engine/utils/WLSLinearSeries.test.js +++ b/app/engine/utils/WLSLinearSeries.test.js @@ -1,7 +1,7 @@ 'use strict' /** * @copyright [OpenRowingMonitor]{@link https://github.com/JaapvanEkris/openrowingmonitor} - * + * * @file This constains all tests for the WLS Linear Series */ import { test } from 'uvu' diff --git a/app/engine/utils/WeighedSeries.js b/app/engine/utils/WeighedSeries.js index 0d5aaddad9..e5568ca1b7 100644 --- a/app/engine/utils/WeighedSeries.js +++ b/app/engine/utils/WeighedSeries.js @@ -1,7 +1,7 @@ 'use strict' /** * @copyright [OpenRowingMonitor]{@link https://github.com/JaapvanEkris/openrowingmonitor} - * + * * @file This creates a weighed series with a maximum number of values. It allows for determining the Average, Weighed Averge, Median, Number of Positive, number of Negative. DO NOT USE MEDIAN ON LARGE SERIES! */ import { createSeries } from './Series.js' @@ -117,7 +117,7 @@ export function createWeighedSeries (maxSeriesLength = 0, defaultValue) { } /** - * @returns {float} median of the series + * @returns {float} median of the series * @description returns the median of the series. As this is a CPU intensive approach, DO NOT USE FOR LARGE SERIES!. For larger series, use the BinarySearchTree.js instead */ function median () { @@ -126,7 +126,7 @@ export function createWeighedSeries (maxSeriesLength = 0, defaultValue) { /** * @returns {boolean} if the weighed series results are to be considered reliable - */ + */ function reliable () { return dataArray.length() > 0 } diff --git a/app/peripherals/ble/pm5/rowing-service/other-characteristics/ForceCurveCharacteristic.js b/app/peripherals/ble/pm5/rowing-service/other-characteristics/ForceCurveCharacteristic.js index 14076ac786..3cd22ba81f 100644 --- a/app/peripherals/ble/pm5/rowing-service/other-characteristics/ForceCurveCharacteristic.js +++ b/app/peripherals/ble/pm5/rowing-service/other-characteristics/ForceCurveCharacteristic.js @@ -1,11 +1,10 @@ 'use strict' -/* - Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor -*/ /** - * Implementation of the Force Curve Data as defined in: - * https://www.concept2.co.uk/files/pdf/us/monitors/PM5_BluetoothSmartInterfaceDefinition.pdf - * https://www.concept2.co.uk/files/pdf/us/monitors/PM5_CSAFECommunicationDefinition.pdf + * @copyright [OpenRowingMonitor]{@link https://github.com/JaapvanEkris/openrowingmonitor} + * + * @file Implementation of the Force Curve Data as defined in: + * - @see {@link https://www.concept2.co.uk/files/pdf/us/monitors/PM5_BluetoothSmartInterfaceDefinition.pdf|The PM5 Bluetooth Smart Interface Definition} + * - @see {@link https://www.concept2.co.uk/files/pdf/us/monitors/PM5_CSAFECommunicationDefinition.pdf|The PM5 CSAFE Communication Definition} */ import loglevel from 'loglevel' @@ -51,7 +50,7 @@ export class ForceCurveCharacteristic extends GattNotifyCharacteristic { const split = Math.floor(data.driveHandleForceCurve.length / chunkSize + (data.driveHandleForceCurve.length % chunkSize === 0 ? 0 : 1)) let i = 0 - log.debug(`Force curve data count: ${data.driveHandleForceCurve.length} chunk size(number of values): ${chunkSize}, number of chunks: ${split}`) + log.trace(`Force curve data count: ${data.driveHandleForceCurve.length} chunk size(number of values): ${chunkSize}, number of chunks: ${split}`) while (i < split) { const end = (i + 1) * chunkSize < data.driveHandleForceCurve.length ? chunkSize * (i + 1) : data.driveHandleForceCurve.length diff --git a/config/rowerProfiles.js b/config/rowerProfiles.js index 18b1db87e8..32b4077d93 100644 --- a/config/rowerProfiles.js +++ b/config/rowerProfiles.js @@ -30,13 +30,12 @@ export default { // NOISE FILTER SETTINGS // Filter Settings to reduce noise in the measured data - // Systematic error agressibveness determines the strength of the systematic error filter. 0 turns it off, 1 turns it to its maximum. - // A value of 0.10 is known to work well, but some machines can handle 0.90. + // Systematic error agressiveness determines the strength of the systematic error filter. 0 turns it off (default), 1 turns it to its maximum. + // A value of 0.10 is known to bring some benefits, but some machines can handle 0.90 to 1.0. Don't set too high if the machine has a lot of signal bounce. systematicErrorAgressiveness: 0, - // Systematic error maximum change determines the maximum change the systematic error filter will allow by a single datapoint. - // This is a percentage, with a minimum of 0, and a maximum of 1. Values closer to 0 will make the filter behave more smoothly, but react slower to changes - systematicErrorMaximumChange: 1, + // Size of the total buffer for the systematic error filter. We recomend to use at least use the length of the longest recovery here (logs can tell this). + systematicErrorNumberOfDatapoints: 1, // Flank length determines the number of measuments that are used for determining the angular velocity and angular acceleration flankLength: 3, @@ -160,8 +159,8 @@ export default { minimumTimeBetweenImpulses: 0.005, maximumTimeBetweenImpulses: 0.01375, flankLength: 12, - systematicErrorAgressiveness: 0.85, - systematicErrorMaximumChange: 0.0065, + systematicErrorAgressiveness: 0.95, + systematicErrorNumberOfDatapoints: 240, minimumStrokeQuality: 0.26, minimumForceBeforeStroke: 22, minimumRecoverySlope: 0.00070,