diff --git a/app/engine/MovingFlankDetector.js b/app/engine/MovingFlankDetector.js new file mode 100644 index 0000000..e6be0f8 --- /dev/null +++ b/app/engine/MovingFlankDetector.js @@ -0,0 +1,68 @@ +'use strict' +/* + Open Rowing Monitor, https://github.com/laberning/openrowingmonitor + + This keeps an array, which we can test for an upgoing or downgoing flank + + Please note: The array contains flankLenght + 1 measured currentDt's, thus flankLenght number of flanks between them + They are arranged that dataPoints[0] is the youngest, and dataPoints[flankLength] the youngest +*/ +function createMovingFlankDetector (flankLength, initValue, numberOfErrorsAllowed) { + const dataPoints = new Array(flankLength + 1) + dataPoints.fill(initValue) + + function pushValue (dataPoint) { + // add the new dataPoint to the array, we have to move datapoints starting at the oldst ones + let i = flankLength + while (i > 0) { + // older datapoints are moved toward the higher numbers + dataPoints[i] = dataPoints[i - 1] + i = i - 1 + } + dataPoints[0] = dataPoint + } + + function isDecelerating () { + let i = flankLength + let numberOfErrors = 0 + while (i > 0) { + if (dataPoints[i] < dataPoints[i - 1]) { + // Oldest interval (dataPoints[i]) is shorter than the younger one (datapoint[i-1], as the distance is fixed, we are decelerating + } else { + numberOfErrors = numberOfErrors + 1 + } + i = i - 1 + } + if (numberOfErrors > numberOfErrorsAllowed) { + return false + } else { + return true + } + } + + function isAccelerating () { + let i = flankLength + let numberOfErrors = 0 + while (i > 0) { + if (dataPoints[i] > dataPoints[i - 1]) { + // Oldest interval (dataPoints[i]) is longer than the younger one (datapoint[i-1], as the distance is fixed, we are accelerating + } else { + numberOfErrors = numberOfErrors + 1 + } + i = i - 1 + } + if (numberOfErrors > numberOfErrorsAllowed) { + return false + } else { + return true + } + } + + return { + pushValue, + isDecelerating, + isAccelerating + } +} + +export { createMovingFlankDetector } diff --git a/app/engine/RowingEngine.js b/app/engine/RowingEngine.js index 5feeab9..a6d9ddc 100644 --- a/app/engine/RowingEngine.js +++ b/app/engine/RowingEngine.js @@ -12,6 +12,7 @@ */ import loglevel from 'loglevel' import { createWeightedAverager } from './WeightedAverager.js' +import { createMovingFlankDetector } from './MovingFlankDetector.js' import { createTimer } from './Timer.js' const log = loglevel.getLogger('RowingEngine') @@ -55,6 +56,8 @@ function createRowingEngine (rowerSettings) { let workoutHandler const kDampEstimatorAverager = createWeightedAverager(3) + const flankDetector = createMovingFlankDetector(rowerSettings.numOfImpulsesPerRevolution, rowerSettings.maximumTimeBetweenMagnets, 0) + let prevDt = rowerSettings.maximumTimeBetweenMagnets let kPower = 0.0 let jPower = 0.0 let kDampEstimator = 0.0 @@ -84,6 +87,18 @@ function createRowingEngine (rowerSettings) { return } + // remember the state of drive phase from the previous impulse, we need it to detect state changes + wasInDrivePhase = isInDrivePhase + + // STEP 1: reduce noise in the measurements by applying some sanity checks + // noise filter on the value of currentDt: it should be within sane levels and should not deviate too much from the previous reading + if (currentDt < rowerSettings.minimumTimeBetweenMagnets || currentDt > rowerSettings.maximumTimeBetweenMagnets || currentDt < (rowerSettings.maximumDownwardChange * prevDt) || currentDt > (rowerSettings.maximumUpwardChange * prevDt)) { + // impulses are outside plausible ranges, so we assume it is close to the previous one + currentDt = prevDt + log.debug(`noise filter corrected currentDt, ${currentDt} was dubious, changed to ${prevDt}`) + } + prevDt = currentDt + // each revolution of the flywheel adds distance of distancePerRevolution strokeDistance += distancePerRevolution / numOfImpulsesPerRevolution @@ -109,8 +124,7 @@ function createRowingEngine (rowerSettings) { // used to be 15 const accelerationIsPositive = omegaDotVector[0] > 0 - wasInDrivePhase = isInDrivePhase - + // STEP 2: detect where we are in the rowing phase (drive or recovery) if (liquidFlywheel) { // Identification of drive and recovery phase on water rowers is still Work in Progress // ω does not seem to decay that linear on water rower in recovery phase, so this would not be @@ -121,11 +135,23 @@ function createRowingEngine (rowerSettings) { // todo: do some measurements and find a better stable indicator for water rowers isInDrivePhase = accelerationIsPositive } else { - // ω decays linear on rowers with a solid flywheel, so we can use that to differentiate the phases - isInDrivePhase = accelerationIsChanging || (accelerationIsPositive && wasInDrivePhase) + flankDetector.pushValue(currentDt) + // Here we use a finite state machine that goes between "Drive" and "Recovery", provinding sufficient time has passed and there is a credible flank + // We analyse the current impulse, depending on where we are in the stroke + if (wasInDrivePhase) { + // during the previous impulse, we were in the "Drive" phase + const strokeElapsed = timer.getValue('drive') + // finish drive phase if we have been long enough in the Drive phase, and we see a clear deceleration + isInDrivePhase = !((strokeElapsed > rowerSettings.minimumDriveTime) && flankDetector.isDecelerating()) + } else { + // during the previous impulse, we were in the "Recovery" phase + const recoveryElapsed = timer.getValue('stroke') + // if we are long enough in the Recovery phase, and we see a clear acceleration, we need to change to the Drive phase + isInDrivePhase = ((recoveryElapsed > rowerSettings.minimumRecoveryTime) && flankDetector.isAccelerating()) + } } - // handle the current impulse, depending on where we are in the stroke + // STEP 3: handle the current impulse, depending on where we are in the stroke if (isInDrivePhase && !wasInDrivePhase) { startDrivePhase(currentDt) } if (!isInDrivePhase && wasInDrivePhase) { startRecoveryPhase() } if (isInDrivePhase && wasInDrivePhase) { updateDrivePhase(currentDt) } @@ -166,7 +192,9 @@ function createRowingEngine (rowerSettings) { if (strokeElapsed !== 0 && workoutHandler) { workoutHandler.handleStroke({ - power: (jPower + kPower) / strokeElapsed, + // if the recoveryPhase is shorter than 0.2 seconds we set it to 2 seconds, this mitigates the problem + // that we do not have a recovery phase on the first stroke + power: (jPower + kPower) / (((strokeElapsed - driveElapsed) < 0.2) ? strokeElapsed + 2 : strokeElapsed), duration: strokeElapsed, durationDrivePhase: driveElapsed, distance: strokeDistance, diff --git a/app/engine/RowingEngine.test.js b/app/engine/RowingEngine.test.js index 8aeb734..b1c4960 100644 --- a/app/engine/RowingEngine.test.js +++ b/app/engine/RowingEngine.test.js @@ -48,9 +48,7 @@ test('sample data for WRX700 should produce plausible results with rower profile rowingEngine.notify(workoutEvaluator) await replayRowingSession(rowingEngine.handleRotationImpulse, { filename: 'recordings/WRX700_2magnets.csv' }) assert.is(workoutEvaluator.getNumOfStrokes(), 16, 'number of strokes does not meet expectation') - // todo: maximum power of the first stroke is too high because it does not contain a recovery part - // should fix that in the RowingEngine and adjust the maximum power here to 220 - assert.ok(workoutEvaluator.getMaxStrokePower() < 370, `maximum stroke power should be below 370w, but is ${workoutEvaluator.getMaxStrokePower()}w`) + assert.ok(workoutEvaluator.getMaxStrokePower() < 220, `maximum stroke power should be below 220w, but is ${workoutEvaluator.getMaxStrokePower()}w`) assert.ok(workoutEvaluator.getMinStrokePower() > 50, `minimum stroke power should be above 50w, but is ${workoutEvaluator.getMinStrokePower()}w`) }) @@ -60,10 +58,18 @@ test('sample data for DKNR320 should produce plausible results with rower profil rowingEngine.notify(workoutEvaluator) await replayRowingSession(rowingEngine.handleRotationImpulse, { filename: 'recordings/DKNR320.csv' }) assert.is(workoutEvaluator.getNumOfStrokes(), 10, 'number of strokes does not meet expectation') - // todo: maximum power of the first stroke is too high because it does not contain a recovery part - // should fix that in the RowingEngine and adjust the maximum power here to 200 - assert.ok(workoutEvaluator.getMaxStrokePower() < 370, `maximum stroke power should be below 370w, but is ${workoutEvaluator.getMaxStrokePower()}w`) + assert.ok(workoutEvaluator.getMaxStrokePower() < 200, `maximum stroke power should be below 200w, but is ${workoutEvaluator.getMaxStrokePower()}w`) assert.ok(workoutEvaluator.getMinStrokePower() > 75, `minimum stroke power should be above 75w, but is ${workoutEvaluator.getMinStrokePower()}w`) }) +test('sample data for RX800 should produce plausible results with rower profile', async () => { + const rowingEngine = createRowingEngine(rowerProfiles.RX800) + const workoutEvaluator = createWorkoutEvaluator() + rowingEngine.notify(workoutEvaluator) + await replayRowingSession(rowingEngine.handleRotationImpulse, { filename: 'recordings/RX800.csv' }) + assert.is(workoutEvaluator.getNumOfStrokes(), 10, 'number of strokes does not meet expectation') + assert.ok(workoutEvaluator.getMaxStrokePower() < 260, `maximum stroke power should be below 260, but is ${workoutEvaluator.getMaxStrokePower()}w`) + assert.ok(workoutEvaluator.getMinStrokePower() > 160, `minimum stroke power should be above 160w, but is ${workoutEvaluator.getMinStrokePower()}w`) +}) + test.run() diff --git a/app/gpio/GpioTimerService.js b/app/gpio/GpioTimerService.js index 251a272..5f6b501 100644 --- a/app/gpio/GpioTimerService.js +++ b/app/gpio/GpioTimerService.js @@ -8,8 +8,21 @@ */ import process from 'process' import { Gpio } from 'onoff' +import os from 'os' +import config from '../tools/ConfigManager.js' +import log from 'loglevel' + +log.setLevel(config.loglevel.default) export function createGpioTimerService () { + // setting top (near-real-time) priority for the Gpio process, as we don't want to miss anything + log.debug('setting priority for the Gpio-service to maximum (-20)') + try { + // setting priority of current process + os.setPriority(-20) + } catch (err) { + log.error('error while setting priority of Gpio-Thread: ', err) + } // mode can be rising, falling, both const reedSensor = new Gpio(17, 'in', 'rising') // use hrtime for time measurement to get a higher time precision diff --git a/app/server.js b/app/server.js index 3cd9e18..3b9c0ed 100644 --- a/app/server.js +++ b/app/server.js @@ -75,6 +75,21 @@ rowingStatistics.on('strokeFinished', (metrics) => { `, split: ${metrics.splitFormatted}, ratio: ${metrics.powerRatio}, dist: ${metrics.distanceTotal}m` + `, cal: ${metrics.caloriesTotal}kcal, SPM: ${metrics.strokesPerMinute}, speed: ${metrics.speed}km/h` + `, cal/hour: ${metrics.caloriesPerHour}kcal, cal/minute: ${metrics.caloriesPerMinute}kcal`) + // Quick hack to generate tcx-trackpoints to get the basic concepts of TCX-export working + /* + const d = new Date() + const timestamp = d.toISOString() + fs.appendFile('exports/currentlog.tcx', + `\n \n` + + `${metrics.distanceTotal}\n` + + `${Math.round(metrics.strokesPerMinute)}\n` + + 'Present\n' + + '\n \n' + + `${metrics.power}\n` + + `${(metrics.speed / 3.6).toFixed(2)}\n` + + '\n \n\n', + (err) => { if (err) log.error(err) }) + */ webServer.notifyClients(metrics) peripheralManager.notifyMetrics('strokeFinished', metrics) }) diff --git a/config/rowerProfiles.js b/config/rowerProfiles.js index 8f4dde8..0463e87 100644 --- a/config/rowerProfiles.js +++ b/config/rowerProfiles.js @@ -16,6 +16,18 @@ export default { // i.e. the number of magnets if used with a reed sensor numOfImpulsesPerRevolution: 1, + // Filter values for sanity checks + // First are the sane minimum and maximum times between magnets during active rows + minimumTimeBetweenMagnets: 0.014, + maximumTimeBetweenMagnets: 0.5, + // Procentual change between successive intervals + maximumDownwardChange: 0.2, // effectively the maximum deceleration + maximumUpwardChange: 1.75, // effectively the maximum acceleration + // Settings for the phase detection + minimumDriveTime: 0.300, + minimumRecoveryTime: 0.750, + + // Needed to determine the damping constant of the rowing machine. This value can be measured in the recovery phase // of the stroke (some ergometers do this constantly). // However I still keep it constant here, as I still have to figure out the damping physics of a water rower (see below) @@ -50,10 +62,10 @@ export default { // Sportstech WRX700 WRX700: { numOfImpulsesPerRevolution: 2, - minimumTimeBetweenMagnets: 0.1, - maximumTimeBetweenMagnets: 0.4, - maximumDownwardChange: 0.40, - maximumUpwardChange: 1.50, + minimumTimeBetweenMagnets: 0.05, + maximumTimeBetweenMagnets: 1, + maximumDownwardChange: 0.25, + maximumUpwardChange: 2, minimumDriveTime: 0.500, minimumRecoveryTime: 0.800, omegaDotDivOmegaSquare: 0.046, @@ -77,8 +89,8 @@ export default { // NordicTrack RX800 Air Rower RX800: { - liquidFlywheel: false, numOfImpulsesPerRevolution: 4, + liquidFlywheel: false, // Damper setting 10 minimumTimeBetweenMagnets: 0.018, diff --git a/docs/Modifying_Rower_Settings.md b/docs/Modifying_Rower_Settings.md new file mode 100644 index 0000000..2950c6a --- /dev/null +++ b/docs/Modifying_Rower_Settings.md @@ -0,0 +1,30 @@ +# Set Up of the Open Rowing Monitor settings for a specific rower + +This guide helps you to adjust the rowing monitor specifically for your rower or even for you + +## Why have setings + +No rowingmachine is the same, and their physical construction is important for the Rowing Monitor to understand to be able to understand your rowing. Easiest way is to select your rower from owerProfiles.js and put its name in default.config.js instead of "rowerProfiles.DEFAULT". + +If your rower isn't there, this guide will help you set it up (please send in the data and settings, so we can add it to the OpenRowingMonitor). + +Settings important for Open Rowing Monitor: + +* numOfImpulsesPerRevolution: tells Open Rowing Monitor how many impulses per rotation of the flywheel to expect. Although sometimes not easy to detect, you can sometimes find it in the manual under the parts-list +* liquidFlywheel: tells OpenRowingMonitor if you are using a waterrower (true) or a solid flywheel with magnetic or air-resistance (false) +* omegaDotDivOmegaSquare: tells OpenRowingMonitor how much damping and thus resistance your flywheel is offering. This is typically also dependent on your damper-setting (if present). To measure it for your rowing machine, comment in the logging at the end of "startDrivePhase" function. Then do some strokes on the rower and estimate a value based on the logging. +* jMoment: The inertia of the flywheel, which in practice influences your power values and distance. This typically is set by rowing and see what kind of power is displayed on the monitor. Typical ranges are weigth dependent (see [this explanation](https://www.rowingmachine-guide.com/tabata-rowing-workouts.html)). +* Noise reduction settings. You should only change these settings if you experience issues. + * minimumTimeBetweenImpulses + * maximumTimeBetweenImpulses + * maximumDownwardChange + * maximumUpwardChange +* Stroke detection settings. + * minimumDriveTime + * minimumRecoveryTime + +For the noise reduction settings and stroke detection settings, you can use the Excel tool. When OpenRowingMonitor records a log (comment out the line in server.js), you can paste the values in the first column of the "Raw Data" tab (please observe that the Raspberry uses a point as seperator, and your version of Excel might expect a comma). From there, the Excel file simulates the calculations the OpenRowingMonitor makes, allowing you to play with these settings. + +By changing the noise reduction settings, you can remove any obvious errors. You don't need to filter everything: it is just to remove obvious errors that might frustrate the stroke detection, but in the end you can't prevent every piece of noise out there. Begin with the noise filtering, when you are satisfied, you can adjust the stroke detection. + +Please note that changing the noise filtering and stroke detection settings will affect your omegaDotDivOmegaSquare and jMoment. So it is best to start with rowing a few strokes to determine settings for noise filtering and stroke detection, and then move on to the other settings. diff --git a/docs/Rowing_Settings_Analysis_Small.xlsx b/docs/Rowing_Settings_Analysis_Small.xlsx new file mode 100644 index 0000000..c932151 Binary files /dev/null and b/docs/Rowing_Settings_Analysis_Small.xlsx differ diff --git a/install/install.sh b/install/install.sh index c13b000..f5daf6b 100755 --- a/install/install.sh +++ b/install/install.sh @@ -25,7 +25,8 @@ print "This script will set up Open Rowing Monitor on a Raspberry Pi 3 / 4 with print "You should only run this script on a SD Card that does not contain any important data." print -if [[ "$(uname -n)" != "raspberrypi" ]]; then +OSID=$(grep -oP '(?<=^ID=).+' /etc/os-release | tr -d '"') +if [[ $OSID != "raspbian" ]]; then cancel "This script currently only works on Raspberry Pi OS, you will have to do a manual installation." fi @@ -35,6 +36,8 @@ if [[ $VERSION != "10 (buster)" ]]; then print "You are running Raspberry Pi OS $VERSION, are you sure that you want to continue?" fi +# todo: once we know what hardware we support we can check for that via /sys/firmware/devicetree/base/model + print read -p "Press RETURN to continue or CTRL + C to abort" diff --git a/recordings/RX800.csv b/recordings/RX800.csv new file mode 100644 index 0000000..23e5857 --- /dev/null +++ b/recordings/RX800.csv @@ -0,0 +1,931 @@ +115.678730412 +7.94730349 +0.110655271 +0.077529811 +0.062992882 +0.053962555 +0.047993133 +0.043563433 +0.040500379 +0.037841367 +0.035730194 +0.033988063 +0.032694066 +0.031287698 +0.03023863 +0.029224544 +0.028584529 +0.027804209 +0.027228518 +0.026601604 +0.02622834 +0.025706705 +0.025334405 +0.025007717 +0.024811381 +0.02453186 +0.024368987 +0.024340543 +0.024487044 +0.024602878 +0.024564859 +0.024720028 +0.025279589 +0.02465499 +0.025149718 +0.025149365 +0.025259812 +0.025432313 +0.025509907 +0.02563813 +0.025838689 +0.025927634 +0.025995875 +0.026110136 +0.026352212 +0.026413232 +0.026417213 +0.026604456 +0.026904274 +0.026781476 +0.026986071 +0.027189332 +0.027265704 +0.027384224 +0.027518281 +0.027624877 +0.027859003 +0.027958782 +0.028054579 +0.028164673 +0.028417454 +0.028516639 +0.028558973 +0.028746289 +0.028958662 +0.029114553 +0.029130072 +0.029342333 +0.02957265 +0.02962728 +0.029873597 +0.029832375 +0.030176193 +0.030259786 +0.030379659 +0.03055529 +0.030806885 +0.030893256 +0.031081851 +0.031216426 +0.031459502 +0.031524466 +0.03170106 +0.031872858 +0.032085064 +0.031965693 +0.031717746 +0.031340261 +0.030476501 +0.029614239 +0.028671859 +0.027737258 +0.027034733 +0.026210503 +0.025537071 +0.024957325 +0.024545025 +0.023962463 +0.023533571 +0.023152975 +0.022930417 +0.022550007 +0.022305263 +0.02208515 +0.021964297 +0.021756721 +0.021738295 +0.02186987 +0.02129668 +0.021308476 +0.02200065 +0.02156783 +0.021978149 +0.022081743 +0.022036594 +0.022198206 +0.022369301 +0.022337801 +0.022457524 +0.022883991 +0.022335375 +0.022751897 +0.023201587 +0.02251482 +0.023127364 +0.023257346 +0.022998529 +0.023309106 +0.023810389 +0.023168789 +0.023598238 +0.023773503 +0.023775277 +0.023906482 +0.02399452 +0.02407765 +0.02429867 +0.02432617 +0.024424042 +0.024526765 +0.02470647 +0.024749545 +0.02486149 +0.024947824 +0.02514453 +0.025196197 +0.025300939 +0.02539544 +0.02559533 +0.025752202 +0.025703349 +0.025826313 +0.026817266 +0.025348124 +0.026221317 +0.026347614 +0.026551283 +0.026602727 +0.026710135 +0.02684221 +0.027094694 +0.027109639 +0.027203381 +0.027410289 +0.027474068 +0.027628144 +0.027736478 +0.02788259 +0.028100203 +0.028185202 +0.028227738 +0.028413369 +0.028648815 +0.028713298 +0.028844187 +0.02902467 +0.029164005 +0.029129375 +0.029028003 +0.028715371 +0.028230942 +0.027403268 +0.026514317 +0.025669996 +0.024969342 +0.024160336 +0.023867796 +0.022693047 +0.022650991 +0.022198247 +0.021833022 +0.021517908 +0.021319777 +0.021019479 +0.02080281 +0.020632549 +0.020567141 +0.020413788 +0.020344436 +0.020309991 +0.020409881 +0.020586994 +0.020312269 +0.020637864 +0.020928477 +0.020416677 +0.020740883 +0.020987478 +0.020907348 +0.02105222 +0.021295684 +0.021022905 +0.021335929 +0.021515244 +0.021309335 +0.021518485 +0.022034063 +0.021357094 +0.021787838 +0.021993932 +0.021921672 +0.022061507 +0.022377343 +0.021966987 +0.022375287 +0.022552585 +0.022377416 +0.022565288 +0.022832345 +0.022696567 +0.022873383 +0.02302668 +0.023156644 +0.023027884 +0.023247089 +0.023342738 +0.023537554 +0.023543684 +0.023658611 +0.023728593 +0.023912001 +0.023941057 +0.024042724 +0.024150688 +0.024311671 +0.024365264 +0.024458969 +0.02459095 +0.024706136 +0.02478936 +0.02489062 +0.02503525 +0.025131491 +0.02523614 +0.025332048 +0.025463773 +0.025590698 +0.02568518 +0.025785125 +0.025866958 +0.026099515 +0.026158257 +0.026251517 +0.026376573 +0.026573501 +0.026637482 +0.026751112 +0.026859835 +0.027081652 +0.027135022 +0.027240986 +0.027369543 +0.027756878 +0.02733606 +0.027475525 +0.027725655 +0.026831743 +0.026708927 +0.02611533 +0.025523067 +0.024970472 +0.024312819 +0.023734927 +0.023239386 +0.022890662 +0.0224464 +0.022083583 +0.021769211 +0.021553709 +0.021266226 +0.021045892 +0.020831131 +0.020753186 +0.020596592 +0.020478981 +0.020447655 +0.020518947 +0.020718467 +0.020377187 +0.02065504 +0.020983727 +0.0206228 +0.020881115 +0.021115654 +0.020988597 +0.021144858 +0.021619786 +0.020896857 +0.021452508 +0.021660101 +0.021355119 +0.021634879 +0.021936677 +0.021694602 +0.021868417 +0.022137752 +0.022055548 +0.022093381 +0.022786312 +0.021745472 +0.022486846 +0.022588588 +0.022551013 +0.022682996 +0.023087721 +0.022701644 +0.02297509 +0.023565094 +0.022699311 +0.023277999 +0.023439278 +0.023387926 +0.023629464 +0.023870855 +0.023559964 +0.023847577 +0.024125004 +0.023969429 +0.024162949 +0.02427719 +0.02443412 +0.024506765 +0.024580248 +0.024685433 +0.024856397 +0.024947324 +0.025012602 +0.025120306 +0.025315232 +0.025371011 +0.02546277 +0.025564974 +0.025774457 +0.025814421 +0.025928513 +0.026035995 +0.026229459 +0.026297274 +0.026674164 +0.026244977 +0.026738276 +0.026812517 +0.026804499 +0.027016852 +0.027338446 +0.0271385 +0.027401335 +0.027507335 +0.027737189 +0.027782097 +0.027756096 +0.02761417 +0.027369761 +0.026738572 +0.026071699 +0.025357288 +0.024756082 +0.024050819 +0.023470031 +0.022975463 +0.022622389 +0.022212831 +0.021848144 +0.021575162 +0.021398828 +0.021141956 +0.020915696 +0.02076764 +0.020709103 +0.020557825 +0.020475083 +0.020441435 +0.020516324 +0.0208824 +0.020203137 +0.020692196 +0.02093166 +0.020601936 +0.020884067 +0.021384161 +0.0209749 +0.02081214 +0.02122542 +0.02151481 +0.021201642 +0.021485569 +0.02173194 +0.021428605 +0.02175733 +0.021974497 +0.021782348 +0.021889848 +0.022251368 +0.02207333 +0.022183312 +0.022623666 +0.022233942 +0.022455424 +0.023861967 +0.021408809 +0.022842278 +0.022991705 +0.022864871 +0.023062835 +0.023389387 +0.023109059 +0.023356763 +0.023604745 +0.023485616 +0.02361943 +0.023870246 +0.02372219 +0.024017266 +0.024178858 +0.024056617 +0.024241692 +0.0244936 +0.024421378 +0.024563156 +0.024694472 +0.024843916 +0.024890064 +0.025007287 +0.025220621 +0.025245287 +0.025292621 +0.025450344 +0.025550529 +0.02584179 +0.025754974 +0.025907957 +0.026291644 +0.025872864 +0.02625768 +0.026443736 +0.026446569 +0.026693478 +0.026781571 +0.026840941 +0.026994571 +0.027214905 +0.027294202 +0.027376851 +0.027500962 +0.027689521 +0.027693276 +0.027614202 +0.027347034 +0.026932607 +0.026196346 +0.025462437 +0.02478951 +0.024261415 +0.023633858 +0.023142356 +0.022748615 +0.022468522 +0.022088687 +0.021797298 +0.02154026 +0.02138689 +0.021137555 +0.020965888 +0.021211 +0.020445998 +0.020534424 +0.020850128 +0.020236257 +0.020565758 +0.020728795 +0.020597035 +0.020743072 +0.021064647 +0.020751036 +0.020997666 +0.021524094 +0.020719554 +0.021249 +0.021410889 +0.021312722 +0.02156389 +0.021988947 +0.021290593 +0.021679482 +0.02222691 +0.021593593 +0.02198328 +0.02211454 +0.022137443 +0.022277172 +0.022386858 +0.022454154 +0.022589191 +0.022630709 +0.022718191 +0.022811062 +0.022959359 +0.023008136 +0.0230971 +0.023185081 +0.023357304 +0.023393267 +0.023487859 +0.023575934 +0.023742934 +0.023786082 +0.023881971 +0.023974749 +0.024267824 +0.024098416 +0.024273398 +0.024496565 +0.024459028 +0.024605084 +0.024733862 +0.024814399 +0.024967084 +0.025036047 +0.025147215 +0.025273307 +0.025447678 +0.025470512 +0.025604327 +0.025772049 +0.025838604 +0.025945308 +0.02606729 +0.026149939 +0.026373217 +0.026454032 +0.026514106 +0.029372646 +0.024299214 +0.026766809 +0.02724531 +0.026901568 +0.027349848 +0.027429551 +0.027561421 +0.027643163 +0.027711385 +0.027513069 +0.027226773 +0.026645383 +0.026096439 +0.025404752 +0.02491753 +0.02416527 +0.023825102 +0.023376584 +0.023004805 +0.022632083 +0.022424563 +0.022105786 +0.021883989 +0.021674544 +0.021585711 +0.021380896 +0.021255248 +0.021153784 +0.021188803 +0.021369507 +0.020650302 +0.02102997 +0.021639322 +0.020711599 +0.021290358 +0.021449136 +0.021460099 +0.021547359 +0.021632507 +0.021699227 +0.021858288 +0.021883992 +0.021968288 +0.022047715 +0.022206011 +0.02223627 +0.022318511 +0.02240103 +0.022560566 +0.022594752 +0.022694862 +0.022755863 +0.022924955 +0.022977196 +0.023081604 +0.023109603 +0.023290623 +0.023350901 +0.023434623 +0.023632197 +0.023634919 +0.02369479 +0.023831845 +0.023930901 +0.024107937 +0.02416579 +0.024232456 +0.024347252 +0.024517975 +0.024571716 +0.024661068 +0.024767623 +0.024945161 +0.024998513 +0.025097456 +0.025206586 +0.025402365 +0.025430549 +0.025545383 +0.025717457 +0.02578229 +0.025925289 +0.025969365 +0.026119921 +0.026331216 +0.026384106 +0.026482976 +0.02660679 +0.026809643 +0.026838698 +0.026979143 +0.027098568 +0.027311364 +0.027379698 +0.02750018 +0.027600605 +0.027821254 +0.027885901 +0.028153308 +0.028103772 +0.028140494 +0.028043754 +0.027678864 +0.027128254 +0.026512716 +0.025741198 +0.025076476 +0.024474588 +0.024034013 +0.023519421 +0.023091736 +0.02275818 +0.022489625 +0.022155087 +0.021895365 +0.02167757 +0.021563291 +0.021364347 +0.021239402 +0.021144329 +0.02111657 +0.02099871 +0.020966167 +0.020930685 +0.021043888 +0.02142337 +0.020759722 +0.021209795 +0.021955146 +0.020781815 +0.021602759 +0.021749869 +0.02132761 +0.021719147 +0.022234442 +0.021459685 +0.021988258 +0.022147887 +0.022077683 +0.022227073 +0.022466146 +0.022376554 +0.022639387 +0.022535221 +0.022700831 +0.022835294 +0.022798497 +0.022958887 +0.023137498 +0.023184683 +0.023257386 +0.023339756 +0.023519997 +0.023562108 +0.02372422 +0.023726738 +0.023924756 +0.023944386 +0.024020608 +0.024135182 +0.024329737 +0.024366404 +0.024470422 +0.024548015 +0.024751811 +0.024793636 +0.024921683 +0.024996386 +0.025196293 +0.025206812 +0.0253172 +0.025433626 +0.025633977 +0.025681829 +0.025790866 +0.025897236 +0.026101403 +0.026144403 +0.026283698 +0.026368218 +0.026574309 +0.026627531 +0.026738124 +0.026992809 +0.026950994 +0.027134568 +0.027158456 +0.027364123 +0.027576641 +0.027657159 +0.027778993 +0.0278644 +0.028082862 +0.028053992 +0.027852677 +0.027509141 +0.027151179 +0.026383958 +0.025696848 +0.025127849 +0.024619202 +0.02399948 +0.023564759 +0.023171753 +0.022900725 +0.022543079 +0.022238283 +0.021994709 +0.021873709 +0.021626618 +0.021497229 +0.021381673 +0.021356896 +0.021232896 +0.021182452 +0.021129859 +0.021221303 +0.021639802 +0.02084012 +0.021332581 +0.021859987 +0.0211616 +0.021616432 +0.021803154 +0.021856265 +0.021751969 +0.021956338 +0.022520189 +0.021661544 +0.02219006 +0.022312912 +0.022391634 +0.022559208 +0.0226248 +0.022659504 +0.022757171 +0.022910596 +0.022977632 +0.023024559 +0.02313167 +0.023295188 +0.023340984 +0.023412484 +0.02351565 +0.023694205 +0.023731242 +0.023812872 +0.023918353 +0.024087379 +0.024130596 +0.024223317 +0.02434028 +0.024493947 +0.024544853 +0.024653464 +0.024725742 +0.024928223 +0.024975778 +0.025079556 +0.025208258 +0.025392425 +0.025399388 +0.025553888 +0.025549906 +0.025823424 +0.025998868 +0.025865461 +0.026131404 +0.02625683 +0.026343107 +0.02645757 +0.026576032 +0.026783606 +0.02683655 +0.026965624 +0.027074512 +0.027281401 +0.027333271 +0.027407993 +0.027577807 +0.027798677 +0.027872695 +0.02796725 +0.028118935 +0.028317305 +0.028374564 +0.028373278 +0.028223657 +0.027903992 +0.027208883 +0.02650146 +0.025809925 +0.02528102 +0.0246758 +0.024167727 +0.023704191 +0.023414211 +0.022959065 +0.022660677 +0.022364233 +0.022168698 +0.021985957 +0.02162994 +0.021570847 +0.021424348 +0.021360163 +0.021154164 +0.021102313 +0.021189923 +0.021629218 +0.020866979 +0.021217053 +0.021707366 +0.021245071 +0.021565181 +0.021939309 +0.0214702 +0.021823032 +0.02204005 +0.021857828 +0.022149271 +0.022573678 +0.021833292 +0.022337364 +0.022574104 +0.022462474 +0.022607436 +0.023096434 +0.022456122 +0.022914917 +0.023221312 +0.022849865 +0.023207345 +0.023634213 +0.022982902 +0.023455807 +0.023694343 +0.02362138 +0.02376875 +0.023959212 +0.023906064 +0.024067211 +0.024217618 +0.024209637 +0.024437506 +0.024557228 +0.024516469 +0.024703375 +0.024877041 +0.02496567 +0.025000023 +0.02504167 +0.02529617 +0.025353243 +0.025516891 +0.025564668 +0.02575537 +0.025809833 +0.025912666 +0.025995129 +0.026223461 +0.026277035 +0.026386886 +0.026501924 +0.026742237 +0.026711737 +0.02687557 +0.027060735 +0.027143124 +0.027273271 +0.027341164 +0.027508437 +0.027724863 +0.027823955 +0.027899121 +0.028032565 +0.028283527 +0.028205564 +0.028228415