summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Schlaeger <chris@linux.com>2015-11-22 06:18:24 +0100
committerChris Schlaeger <chris@linux.com>2015-11-22 06:18:24 +0100
commitac090a01a8202e37971a67af117eeeed0470c042 (patch)
tree2b00d2d3b8a9456f9d90326e58a442f7a3ad9c1f
parent062d713e71d71e5caa18a9936877c22e2faf4593 (diff)
downloadpostrunner-ac090a01a8202e37971a67af117eeeed0470c042.zip
Adding support for heart rate variability score.
-rw-r--r--lib/postrunner/ActivitySummary.rb6
-rw-r--r--lib/postrunner/HRV_Analyzer.rb105
2 files changed, 111 insertions, 0 deletions
diff --git a/lib/postrunner/ActivitySummary.rb b/lib/postrunner/ActivitySummary.rb
index c9234ac..b01dfed 100644
--- a/lib/postrunner/ActivitySummary.rb
+++ b/lib/postrunner/ActivitySummary.rb
@@ -14,6 +14,7 @@ require 'fit4ruby'
require 'postrunner/FlexiTable'
require 'postrunner/ViewFrame'
+require 'postrunner/HRV_Analyzer'
module PostRunner
@@ -112,6 +113,11 @@ module PostRunner
vo2max = @fit_activity.vo2max
t.row([ 'VO2max:', vo2max ? vo2max : '-' ])
+ hrv = HRV_Analyzer.new(@fit_activity)
+ if hrv.has_hrv_data?
+ t.row([ 'HRV Score:', hrv.lnrmssdx20 ])
+ end
+
t
end
diff --git a/lib/postrunner/HRV_Analyzer.rb b/lib/postrunner/HRV_Analyzer.rb
new file mode 100644
index 0000000..60d6134
--- /dev/null
+++ b/lib/postrunner/HRV_Analyzer.rb
@@ -0,0 +1,105 @@
+#!/usr/bin/env ruby -w
+# encoding: UTF-8
+#
+# = HRV_Analyzer.rb -- PostRunner - Manage the data from your Garmin sport devices.
+#
+# Copyright (c) 2015 by Chris Schlaeger <cs@taskjuggler.org>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of version 2 of the GNU General Public License as
+# published by the Free Software Foundation.
+#
+
+module PostRunner
+
+ # This class analyzes the heart rate variablity based on the R-R intervals
+ # in the given FIT file. It can compute RMSSD and a HRV score if the data
+ # quality is good enough.
+ class HRV_Analyzer
+
+ def initialize(fit_file)
+ @fit_file = fit_file
+ collect_rr_intervals
+ end
+
+ # The method can be used to check if we have valid HRV data. The FIT file
+ # must have HRV data, it must have correctly measured 90% of the beats and
+ # the measurement duration must be at least 30 seconds.
+ def has_hrv_data?
+ !@fit_file.hrv.empty? &&
+ @missed_beats < (0.1 * @rr_intervals.length) &&
+ total_duration > 30.0
+ end
+
+ # Return the total duration of all measured intervals in seconds.
+ def total_duration
+ @rr_intervals.inject(:+)
+ end
+
+ # root mean square of successive differences
+ def rmssd
+ last_i = nil
+ sum = 0.0
+ @rr_intervals.each do |i|
+ if last_i
+ sum += (last_i - i) ** 2.0
+ end
+ last_i = i
+ end
+ Math.sqrt(sum / (@rr_intervals.length - 1))
+ end
+
+ # The RMSSD value is not very easy to memorize. Alternatively, we can
+ # multiply the natural logarithm of RMSSD by -20. This usually results in
+ # values between 40 (for untrained) and 100 (for higly trained) athletes.
+ def lnrmssdx20
+ (-20.0 * Math.log(rmssd)).round.to_i
+ end
+
+ private
+
+ def collect_rr_intervals
+ raw_rr_intervals = []
+ @fit_file.hrv.each do |hrv|
+ raw_rr_intervals += hrv.time.compact
+ end
+
+ prev_dts = []
+ avg_dt = nil
+ @missed_beats = 0
+ @rr_intervals = []
+ raw_rr_intervals.each do |dt|
+ # Sometimes the hrv data is missing one or more beats. The next
+ # detected beat is then listed with a time interval since the last
+ # detected beat. We try to detect these skipped beats by looking for
+ # time intervals that are 1.8 or more times larger than the average of
+ # the last 5 good intervals.
+ if avg_dt && dt > 1.8 * avg_dt
+ # If we have found skipped beats we calcluate how many beats were
+ # skipped.
+ skip = (dt / avg_dt).round.to_i
+ # We count the total number of skipped beats. We don't use the HRV
+ # data if too many beats were skipped.
+ @missed_beats += skip
+ # Insert skip times the average skipped beat intervals.
+ skip.times do
+ new_dt = dt / skip
+ @rr_intervals << new_dt
+ prev_dts << new_dt
+ prev_dts.shift if prev_dts.length > 5
+ end
+ else
+ @rr_intervals << dt
+ # We keep a list of the previous 5 good intervals and compute the
+ # average value of them.
+ prev_dts << dt
+ prev_dts.shift if prev_dts.length > 5
+ end
+ avg_dt = prev_dts.inject(:+) / prev_dts.length
+ end
+ end
+
+ end
+
+end
+