Skip to content

Commit

Permalink
Improvement of the dragfactor calculation by using Linear Theil-Sen e…
Browse files Browse the repository at this point in the history
…stimator (#27)

* Explanation of the improvement

* Addition of the defaultValue property

* Change to use Lin. TS for Drag calculation

Changing the drag and recovery slope calculation from OLS to Linear Theil-Sen

* Fixed Lint error

* Improvement of the RowErg profile

Adaptation of the Concept2 RowErg profile to the new algorithms

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Update Rower.test.js

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm

* Adaptation to improved drag calculation algorithm
  • Loading branch information
JaapvanEkris authored Jan 24, 2024
1 parent 265a459 commit 2934fef
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 86 deletions.
38 changes: 21 additions & 17 deletions app/engine/Flywheel.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import loglevel from 'loglevel'
import { createStreamFilter } from './utils/StreamFilter.js'
import { createOLSLinearSeries } from './utils/OLSLinearSeries.js'
import { createTSLinearSeries } from './utils/FullTSLinearSeries.js'
import { createTSQuadraticSeries } from './utils/FullTSQuadraticSeries.js'
import { createWeighedSeries } from './utils/WeighedSeries.js'

Expand All @@ -38,10 +39,10 @@ function createFlywheel (rowerSettings) {
const _angularDistance = createTSQuadraticSeries(flankLength)
const _angularVelocityMatrix = []
const _angularAccelerationMatrix = []
const drag = createStreamFilter(rowerSettings.dragFactorSmoothing, (rowerSettings.dragFactor / 1000000))
const recoveryDeltaTime = createOLSLinearSeries()
const drag = createWeighedSeries(rowerSettings.dragFactorSmoothing, (rowerSettings.dragFactor / 1000000))
const recoveryDeltaTime = createTSLinearSeries()
const strokedetectionMinimalGoodnessOfFit = rowerSettings.minimumStrokeQuality
const minumumRecoverySlope = createStreamFilter(rowerSettings.dragFactorSmoothing, rowerSettings.minumumRecoverySlope)
const minumumRecoverySlope = createWeighedSeries(rowerSettings.dragFactorSmoothing, rowerSettings.minumumRecoverySlope)
let _deltaTimeBeforeFlank
let _angularVelocityAtBeginFlank
let _angularVelocityBeforeFlank
Expand Down Expand Up @@ -117,8 +118,8 @@ function createFlywheel (rowerSettings) {
}

// Let's make room for a new set of values for angular velocity and acceleration
_angularVelocityMatrix[_angularVelocityMatrix.length] = createWeighedSeries()
_angularAccelerationMatrix[_angularAccelerationMatrix.length] = createWeighedSeries()
_angularVelocityMatrix[_angularVelocityMatrix.length] = createWeighedSeries(flankLength, 0)
_angularAccelerationMatrix[_angularAccelerationMatrix.length] = createWeighedSeries(flankLength, 0)

let i = 0
const goodnessOfFit = _angularDistance.goodnessOfFit()
Expand All @@ -133,7 +134,7 @@ function createFlywheel (rowerSettings) {
_angularAccelerationAtBeginFlank = _angularAccelerationMatrix[0].weighedAverage()

// And finally calculate the torque
_torqueAtBeginFlank = (rowerSettings.flywheelInertia * _angularAccelerationAtBeginFlank + drag.clean() * Math.pow(_angularVelocityAtBeginFlank, 2))
_torqueAtBeginFlank = (rowerSettings.flywheelInertia * _angularAccelerationAtBeginFlank + drag.weighedAverage() * Math.pow(_angularVelocityAtBeginFlank, 2))
}

function maintainStateOnly () {
Expand All @@ -153,24 +154,28 @@ function createFlywheel (rowerSettings) {
// Completion of the recovery phase
inRecoveryPhase = false

// As goodnessOfFit is calculated in-situ (for 220 datapoints on a C2) and is CPU intensive, we only calculate it only once and reuse the cached value
const goodnessOfFit = recoveryDeltaTime.goodnessOfFit()

// Calculation of the drag-factor
if (rowerSettings.autoAdjustDragFactor && recoveryDeltaTime.length() > minimumDragFactorSamples && recoveryDeltaTime.slope() > 0 && (!drag.reliable() || recoveryDeltaTime.goodnessOfFit() >= rowerSettings.minimumDragQuality)) {
drag.push(slopeToDrag(recoveryDeltaTime.slope()))
log.debug(`*** Calculated drag factor: ${(slopeToDrag(recoveryDeltaTime.slope()) * 1000000).toFixed(4)}, no. samples: ${recoveryDeltaTime.length()}, Goodness of Fit: ${recoveryDeltaTime.goodnessOfFit().toFixed(4)}`)
if (rowerSettings.autoAdjustDragFactor && recoveryDeltaTime.length() > minimumDragFactorSamples && recoveryDeltaTime.slope() > 0 && (!drag.reliable() || goodnessOfFit >= rowerSettings.minimumDragQuality)) {
drag.push(slopeToDrag(recoveryDeltaTime.slope()), goodnessOfFit)

log.debug(`*** Calculated drag factor: ${(slopeToDrag(recoveryDeltaTime.slope()) * 1000000).toFixed(4)}, no. samples: ${recoveryDeltaTime.length()}, Goodness of Fit: ${goodnessOfFit.toFixed(4)}`)
if (rowerSettings.autoAdjustRecoverySlope) {
// We are allowed to autoadjust stroke detection slope as well, so let's do that
minumumRecoverySlope.push((1 - rowerSettings.autoAdjustRecoverySlopeMargin) * recoveryDeltaTime.slope())
log.debug(`*** Calculated recovery slope: ${recoveryDeltaTime.slope().toFixed(6)}, Goodness of Fit: ${recoveryDeltaTime.goodnessOfFit().toFixed(4)}`)
minumumRecoverySlope.push((1 - rowerSettings.autoAdjustRecoverySlopeMargin) * recoveryDeltaTime.slope(), goodnessOfFit)
log.debug(`*** Calculated recovery slope: ${recoveryDeltaTime.slope().toFixed(6)}, Goodness of Fit: ${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.debug(`*** Calculated recovery slope: ${recoveryDeltaTime.slope().toFixed(6)}, Goodness of Fit: ${goodnessOfFit.toFixed(4)}, not used as autoAdjustRecoverySlope isn't set to true`)
}
} else {
if (!rowerSettings.autoAdjustDragFactor) {
// autoAdjustDampingConstant = false, thus the update is skipped, but let's log the dragfactor anyway
log.debug(`*** Calculated drag factor: ${(slopeToDrag(recoveryDeltaTime.slope()) * 1000000).toFixed(4)}, slope: ${recoveryDeltaTime.slope().toFixed(8)}, not used because autoAdjustDragFactor is not true`)
} else {
log.debug(`*** Calculated drag factor: ${(slopeToDrag(recoveryDeltaTime.slope()) * 1000000).toFixed(4)}, not used because reliability was too low. no. samples: ${recoveryDeltaTime.length()}, fit: ${recoveryDeltaTime.goodnessOfFit().toFixed(4)}`)
log.debug(`*** Calculated drag factor: ${(slopeToDrag(recoveryDeltaTime.slope()) * 1000000).toFixed(4)}, not used because reliability was too low. no. samples: ${recoveryDeltaTime.length()}, fit: ${goodnessOfFit.toFixed(4)}`)
}
}
}
Expand Down Expand Up @@ -217,7 +222,7 @@ function createFlywheel (rowerSettings) {

function dragFactor () {
// Ths function returns the current dragfactor of the flywheel
return drag.clean()
return drag.weighedAverage()
}

function isDwelling () {
Expand All @@ -243,17 +248,16 @@ function createFlywheel (rowerSettings) {
}

function isUnpowered () {
if (deltaTimeSlopeAbove(minumumRecoverySlope.clean()) && torqueAbsent() && _deltaTime.length() >= flankLength) {
if (deltaTimeSlopeAbove(minumumRecoverySlope.weighedAverage()) && torqueAbsent() && _deltaTime.length() >= flankLength) {
// We reached the minimum number of increasing currentDt values
// log.info(`*** INFO: recovery detected based on due to slope exceeding recoveryslope = ${deltaTimeSlopeAbove(minumumRecoverySlope.clean())}, exceeding minumumForceBeforeStroke = ${torqueAbsent()}`)
return true
} else {
return false
}
}

function isPowered () {
if ((deltaTimeSlopeBelow(minumumRecoverySlope.clean()) && torquePresent()) || _deltaTime.length() < flankLength) {
if ((deltaTimeSlopeBelow(minumumRecoverySlope.weighedAverage()) && torquePresent()) || _deltaTime.length() < flankLength) {
return true
} else {
return false
Expand Down
92 changes: 46 additions & 46 deletions app/engine/Rower.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/*
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 Angfular physics
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.
*/
Expand Down Expand Up @@ -169,20 +169,20 @@ test('Test behaviour for three perfect identical strokes, including settingling
rower.handleRotationImpulse(0.010386684)
testStrokeState(rower, 'Drive')
testTotalMovingTimeSinceStart(rower, 0.44915539800000004)
testTotalLinearDistanceSinceStart(rower, 1.6075387252606492)
testTotalLinearDistanceSinceStart(rower, 1.5912564829320934)
testTotalNumberOfStrokes(rower, 2)
testCycleDuration(rower, 0.34889498300000005)
testCycleLinearDistance(rower, 0.9667532273482231)
testCycleLinearDistance(rower, 0.9504709850196674)
testCycleLinearVelocity(rower, 3.2650920019419694)
testCyclePower(rower, 97.46401557792097)
testDriveDuration(rower, 0.19636192600000005)
testDriveLinearDistance(rower, 0.4683645067496696)
testDriveLinearDistance(rower, 0.4520822644211139)
testDriveLength(rower, 0.2638937829015426)
testDriveAverageHandleForce(rower, 270.4531194469761)
testDrivePeakHandleForce(rower, 418.918391852085)
testDriveAverageHandleForce(rower, 251.04336322997108)
testDrivePeakHandleForce(rower, 396.7011215867992)
testRecoveryDuration(rower, 0.152533057)
testRecoveryDragFactor(rower, 343.6343564104484)
testInstantHandlePower(rower, 556.0136323793809)
testRecoveryDragFactor(rower, 309.02744980039836)
testInstantHandlePower(rower, 526.5255378434941)
// Recovery second stroke starts here
rower.handleRotationImpulse(0.010769)
rower.handleRotationImpulse(0.010707554)
Expand All @@ -201,19 +201,19 @@ test('Test behaviour for three perfect identical strokes, including settingling
rower.handleRotationImpulse(0.011209919)
testStrokeState(rower, 'Recovery')
testTotalMovingTimeSinceStart(rower, 0.6101840930000001)
testTotalLinearDistanceSinceStart(rower, 2.388146236510099)
testTotalLinearDistanceSinceStart(rower, 2.3447269236339507)
testTotalNumberOfStrokes(rower, 2)
testCycleDuration(rower, 0.40310000200000007)
testCycleLinearDistance(rower, 1.2489720179991195)
testCycleLinearVelocity(rower, 4.776726663843188)
testCyclePower(rower, 305.1751755713177)
testCycleLinearDistance(rower, 1.2055527051229706)
testCycleLinearVelocity(rower, 4.6106683482425606)
testCyclePower(rower, 274.4414360493952)
testDriveDuration(rower, 0.25056694500000004)
testDriveLinearDistance(rower, 1.1969315172491561)
testDriveLinearDistance(rower, 1.1553213424095137)
testDriveLength(rower, 0.3371976114853044)
testDriveAverageHandleForce(rower, 312.83830506634683)
testDrivePeakHandleForce(rower, 480.0144155860976)
testDriveAverageHandleForce(rower, 290.98159585708896)
testDrivePeakHandleForce(rower, 456.9929898648157)
testRecoveryDuration(rower, 0.152533057)
testRecoveryDragFactor(rower, 343.6343564104484) // As we decelerate the flywheel quite fast, this is expected
testRecoveryDragFactor(rower, 309.02744980039836) // As we decelerate the flywheel quite fast, this is expected
testInstantHandlePower(rower, 0)
// Drive third stroke starts here
rower.handleRotationImpulse(0.011221636)
Expand All @@ -237,20 +237,20 @@ test('Test behaviour for three perfect identical strokes, including settingling
rower.handleRotationImpulse(0.010386684)
testStrokeState(rower, 'Drive')
testTotalMovingTimeSinceStart(rower, 0.8203921620000004)
testTotalLinearDistanceSinceStart(rower, 3.3769157507594016)
testTotalLinearDistanceSinceStart(rower, 3.2991228151896355)
testTotalNumberOfStrokes(rower, 3)
testCycleDuration(rower, 0.3490464680000002)
testCycleLinearDistance(rower, 1.0408100149992658)
testCycleLinearVelocity(rower, 4.7709866068572415)
testCyclePower(rower, 304.0763360087651)
testCycleLinearDistance(rower, 1.004627254269142)
testCycleLinearVelocity(rower, 4.6051278388258226)
testCyclePower(rower, 273.453258990202)
testDriveDuration(rower, 0.25056694500000004)
testDriveLinearDistance(rower, 0.5724455082495962)
testDriveLinearDistance(rower, 0.552544989848028)
testDriveLength(rower, 0.3371976114853044)
testDriveAverageHandleForce(rower, 245.5974258934615)
testDrivePeakHandleForce(rower, 418.91839185069534)
testDriveAverageHandleForce(rower, 223.750606354492)
testDrivePeakHandleForce(rower, 396.7011215854034)
testRecoveryDuration(rower, 0.09847952300000018)
testRecoveryDragFactor(rower, 343.6343564104484)
testInstantHandlePower(rower, 556.0136323776126)
testRecoveryDragFactor(rower, 309.02744980039836)
testInstantHandlePower(rower, 526.5255378417136)
// Recovery third stroke starts here
rower.handleRotationImpulse(0.010769)
rower.handleRotationImpulse(0.010707554)
Expand All @@ -269,19 +269,19 @@ test('Test behaviour for three perfect identical strokes, including settingling
rower.handleRotationImpulse(0.011209919)
testStrokeState(rower, 'Recovery')
testTotalMovingTimeSinceStart(rower, 0.9814208570000005)
testTotalLinearDistanceSinceStart(rower, 4.157523262008851)
testTotalLinearDistanceSinceStart(rower, 4.052593255891493)
testTotalNumberOfStrokes(rower, 3)
testCycleDuration(rower, 0.3712367640000004)
testCycleLinearDistance(rower, 1.353053019499046)
testCycleLinearVelocity(rower, 4.76616864783023)
testCyclePower(rower, 303.1560556797095)
testCycleLinearDistance(rower, 1.3060154305498848)
testCycleLinearVelocity(rower, 4.600477371517923)
testCyclePower(rower, 272.62565872880714)
testDriveDuration(rower, 0.2727572410000002)
testDriveLinearDistance(rower, 1.3010125187490824)
testDriveLinearDistance(rower, 1.2557840678364274)
testDriveLength(rower, 0.36651914291880905)
testDriveAverageHandleForce(rower, 295.526758358351)
testDrivePeakHandleForce(rower, 480.01441558492223)
testDriveAverageHandleForce(rower, 272.7765993429924)
testDrivePeakHandleForce(rower, 456.99298986363897)
testRecoveryDuration(rower, 0.09847952300000018)
testRecoveryDragFactor(rower, 343.6343564104484)
testRecoveryDragFactor(rower, 309.02744980039836)
testInstantHandlePower(rower, 0)
// Dwelling state starts here
rower.handleRotationImpulse(0.020769)
Expand All @@ -302,18 +302,18 @@ test('Test behaviour for three perfect identical strokes, including settingling
testStrokeState(rower, 'WaitingForDrive')
testTotalMovingTimeSinceStart(rower, 1.1137102920000004)
testTotalNumberOfStrokes(rower, 3)
testTotalLinearDistanceSinceStart(rower, 4.782009271008411)
testTotalLinearDistanceSinceStart(rower, 4.655369608452978)
testCycleDuration(rower, 0.4157688410000001)
testCycleLinearDistance(rower, 1.9775390284986059)
testCycleLinearVelocity(rower, 4.756342547801953)
testCyclePower(rower, 301.28492718029133)
testCycleLinearDistance(rower, 1.90879178311137)
testCycleLinearVelocity(rower, 4.590992866421583)
testCyclePower(rower, 270.94296880669305)
testDriveDuration(rower, 0.2727572410000002)
testDriveLinearDistance(rower, 1.3010125187490824)
testDriveLinearDistance(rower, 1.2557840678364274)
testDriveLength(rower, 0.36651914291880905)
testDriveAverageHandleForce(rower, 295.526758358351)
testDrivePeakHandleForce(rower, 480.01441558492223)
testDriveAverageHandleForce(rower, 272.7765993429924)
testDrivePeakHandleForce(rower, 456.99298986363897)
testRecoveryDuration(rower, 0.1430115999999999)
testRecoveryDragFactor(rower, 343.6343564104484)
testRecoveryDragFactor(rower, 309.02744980039836)
testInstantHandlePower(rower, 0)
})

Expand Down Expand Up @@ -379,10 +379,10 @@ test('sample data for NordicTrack RX800 should produce plausible results', async
await replayRowingSession(rower.handleRotationImpulse, { filename: 'recordings/RX800.csv', realtime: false, loop: false })

testTotalMovingTimeSinceStart(rower, 17.389910236000024)
testTotalLinearDistanceSinceStart(rower, 62.365436964788074)
testTotalLinearDistanceSinceStart(rower, 62.49982252262572)
testTotalNumberOfStrokes(rower, 8)
// As dragFactor is dynamic, it should have changed
testRecoveryDragFactor(rower, 490.4918403723983)
testRecoveryDragFactor(rower, 493.1277530352103)
})

test('A full session for SportsTech WRX700 should produce plausible results', async () => {
Expand Down Expand Up @@ -410,11 +410,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.3787439999999)
testTotalLinearDistanceSinceStart(rower, 2029.6167445192161)
testTotalMovingTimeSinceStart(rower, 590.111937)
testTotalLinearDistanceSinceStart(rower, 2029.6932502534587)
testTotalNumberOfStrokes(rower, 206)
// As dragFactor isn't static, it should have changed
testRecoveryDragFactor(rower, 80.60603626039024)
testRecoveryDragFactor(rower, 80.79039510767821)
})

function testStrokeState (rower, expectedValue) {
Expand Down
21 changes: 16 additions & 5 deletions app/engine/utils/WeighedSeries.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
This creates a series with weights
It allows for determining the Average, Weighed Average, Median, Number of Positive, number of Negative
This creates a series with a maximum number of values
It allows for determining the Average, Median, Number of Positive, number of Negative
*/

import { createSeries } from './Series.js'

function createWeighedSeries (maxSeriesLength) {
function createWeighedSeries (maxSeriesLength, defaultValue) {
const dataArray = createSeries(maxSeriesLength)
const weightArray = createSeries(maxSeriesLength)
const weightedArray = createSeries(maxSeriesLength)
Expand Down Expand Up @@ -48,14 +48,20 @@ function createWeighedSeries (maxSeriesLength) {
}

function average () {
return dataArray.average()
if (dataArray.length() > 0) {
// The series contains sufficient values to be valid
return dataArray.average()
} else {
// The array isn't sufficiently filled
return defaultValue
}
}

function weighedAverage () {
if (dataArray.length() > 0 && weightArray.sum() !== 0) {
return (weightedArray.sum() / weightArray.sum())
} else {
return undefined
return defaultValue
}
}

Expand All @@ -71,6 +77,10 @@ function createWeighedSeries (maxSeriesLength) {
return dataArray.median()
}

function reliable () {
return dataArray.length() > 0
}

function series () {
return dataArray.series()
}
Expand All @@ -96,6 +106,7 @@ function createWeighedSeries (maxSeriesLength) {
maximum,
median,
series,
reliable,
reset
}
}
Expand Down
8 changes: 4 additions & 4 deletions config/rowerProfiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,17 +130,17 @@ export default {
minimumDragQuality: 0.95,
dragFactorSmoothing: 3,
minimumTimeBetweenImpulses: 0.005,
maximumTimeBetweenImpulses: 0.020,
maximumTimeBetweenImpulses: 0.0145,
flankLength: 12,
smoothing: 1,
minimumStrokeQuality: 0.36,
minumumForceBeforeStroke: 10,
minumumForceBeforeStroke: 11,
minumumRecoverySlope: 0.00070,
autoAdjustRecoverySlope: true,
autoAdjustRecoverySlopeMargin: 0.04,
autoAdjustRecoverySlopeMargin: 0.01,
minimumDriveTime: 0.40,
minimumRecoveryTime: 0.90,
flywheelInertia: 0.10130,
flywheelInertia: 0.10148,
magicConstant: 2.8
},

Expand Down
Loading

0 comments on commit 2934fef

Please sign in to comment.