summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/postrunner/ActivitySummary.rb7
-rw-r--r--lib/postrunner/ChartView.rb5
-rw-r--r--lib/postrunner/HRV_Analyzer.rb93
3 files changed, 66 insertions, 39 deletions
diff --git a/lib/postrunner/ActivitySummary.rb b/lib/postrunner/ActivitySummary.rb
index 70a9f80..fb01623 100644
--- a/lib/postrunner/ActivitySummary.rb
+++ b/lib/postrunner/ActivitySummary.rb
@@ -159,9 +159,12 @@ module PostRunner
t.row([ 'Suggested Recovery Time:',
rec_time ? secsToDHMS(rec_time * 60) : '-' ])
- hrv = HRV_Analyzer.new(@fit_activity)
+ rr_intervals = @activity.fit_activity.hrv.map do |hrv|
+ hrv.time.compact
+ end.flatten
+ hrv = HRV_Analyzer.new(rr_intervals)
if hrv.has_hrv_data?
- t.row([ 'HRV Score:', "%.1f" % hrv.lnrmssdx20_1sigma ])
+ t.row([ 'HRV Score:', "%.1f" % hrv.one_sigma(:hrv_score) ])
end
t
diff --git a/lib/postrunner/ChartView.rb b/lib/postrunner/ChartView.rb
index 191f9f0..d5f47d7 100644
--- a/lib/postrunner/ChartView.rb
+++ b/lib/postrunner/ChartView.rb
@@ -21,7 +21,10 @@ module PostRunner
@sport = activity.sport
@unit_system = unit_system
@empty_charts = {}
- @hrv_analyzer = HRV_Analyzer.new(@activity.fit_activity)
+ rr_intervals = @activity.fit_activity.hrv.map do |hrv|
+ hrv.time.compact
+ end.flatten
+ @hrv_analyzer = HRV_Analyzer.new(rr_intervals)
@charts = [
{
diff --git a/lib/postrunner/HRV_Analyzer.rb b/lib/postrunner/HRV_Analyzer.rb
index 664f7c9..c240046 100644
--- a/lib/postrunner/HRV_Analyzer.rb
+++ b/lib/postrunner/HRV_Analyzer.rb
@@ -19,20 +19,25 @@ module PostRunner
# quality is good enough.
class HRV_Analyzer
- attr_reader :rr_intervals, :timestamps
+ attr_reader :rr_intervals, :timestamps, :errors
+
+ # Typical values for healthy, adult humans are between 2.94 and 4.32. We
+ # use a slighly broader interval.
+ LN_RMSSD_MIN = 2.9
+ LN_RMSSD_MAX = 4.4
# Create a new HRV_Analyzer object.
- # @param fit_file [Fit4Ruby::Activity] FIT file to analyze.
- def initialize(fit_file)
- @fit_file = fit_file
- collect_rr_intervals
+ # @param rr_intervals [Array of Float] R-R (or NN) time delta in seconds.
+ def initialize(rr_intervals)
+ @errors = 0
+ cleanup_rr_intervals(rr_intervals)
end
# The method can be used to check if we have valid HRV data. The FIT file
# must have HRV data and the measurement duration must be at least 30
# seconds.
def has_hrv_data?
- !@fit_file.hrv.empty? && total_duration > 30.0
+ !@rr_intervals.empty? && total_duration > 30.0
end
# Return the total duration of all measured intervals in seconds.
@@ -40,7 +45,11 @@ module PostRunner
@timestamps[-1]
end
- # Compute the root mean square of successive differences.
+ # Compute the root mean square of successive differences. According to
+ # Nunan et. al. 2010
+ # (http://www.qeeg.co.uk/HRV/NUNAN-2010-A%20Quantitative%20Systematic%20Review%20of%20Normal%20Values%20for.pdf)
+ # rMSSD (ms) are expected to be in the rage of 19 to 75 in healthy, adult
+ # humans.
# @param start_time [Float] Determines at what time mark (in seconds) the
# computation should start.
# @param duration [Float] The duration of the total inteval in seconds to
@@ -70,7 +79,9 @@ module PostRunner
cnt = 0
@rr_intervals[start_idx..end_idx].each do |i|
if i && last_i
- sum += (last_i - i) ** 2.0
+ # Input values are in seconds, but rmssd is usually computed from
+ # milisecond values.
+ sum += ((last_i - i) * 1000) ** 2.0
cnt += 1
end
last_i = i
@@ -79,27 +90,37 @@ module PostRunner
Math.sqrt(sum / cnt)
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 1.0 (for untrained) and 100.0 (for higly trained)
- # athletes. Values larger than 100.0 are rare but possible.
+ # The natural logarithm of rMSSD.
# @param start_time [Float] Determines at what time mark (in seconds) the
# computation should start.
# @param duration [Float] The duration of the total inteval in seconds to
# be considered for the computation. This value should be larger
# then 30 seconds to produce meaningful values.
- def lnrmssdx20(start_time = 0.0, duration = nil)
- -20.0 * Math.log(rmssd(start_time, duration))
+ def ln_rmssd(start_time = 0.0, duration = nil)
+ Math.log(rmssd(start_time, duration))
+ end
+
+ # The ln_rmssd values are hard to interpret. Since we know the expected
+ # range we'll transform it into a value in the range 0 - 100. If the HRV
+ # is measured early in the morning while standing upright and with a
+ # regular 3s in/3s out breathing pattern the HRV Score is a performance
+ # indicator. The higher it is, the better the performance condition.
+ def hrv_score(start_time = 0.0, duration = nil)
+ ssd = ln_rmssd(start_time, duration)
+ ssd = LN_RMSSD_MIN if ssd < LN_RMSSD_MIN
+ ssd = LN_RMSSD_MAX if ssd > LN_RMSSD_MAX
+
+ (ssd - LN_RMSSD_MIN) * (100.0 / (LN_RMSSD_MAX - LN_RMSSD_MIN))
end
- # This method is similar to lnrmssdx20 but it tries to search the data for
- # the best time period to compute the lnrmssdx20 value from.
- def lnrmssdx20_1sigma
+ # This method tries to find a window of values that all lie within the
+ # TP84 range and then calls the given block for that range.
+ def one_sigma(calc_method)
# Create a new Array that consists of rr_intervals and timestamps
# tuples.
set = []
0.upto(@rr_intervals.length - 1) do |i|
- set << [ @rr_intervals[i] ? @rr_intervals[i] : 0.0, @timestamps[i] ]
+ set << [ @rr_intervals[i] || 0.0, @timestamps[i] ]
end
percentiles = Percentiles.new(set)
@@ -107,29 +128,32 @@ module PostRunner
# PT84 (aka +1sigma range. Sort the list by time.
not_1sigma = percentiles.not_tp_x(84.13).sort { |e1, e2| e1[1] <=> e2[1] }
- # Then find the largest time gap in that list. So all the values in that
- # gap are within TP84.
- gap_start = gap_end = 0
+ # Then find the largest window RR interval list so that all the values
+ # in that window are within TP84.
+ window_start = window_end = 0
last = nil
not_1sigma.each do |e|
if last
- if (e[1] - last) > (gap_end - gap_start)
- gap_start = last
- gap_end = e[1]
+ if (e[1] - last) > (window_end - window_start)
+ window_start = last + 1
+ window_end = e[1] - 1
end
end
last = e[1]
end
- # That gap should be at least 30 seconds long. Otherwise we'll just use
+
+ # That window should be at least 30 seconds long. Otherwise we'll just use
# all the values.
- return lnrmssdx20 if gap_end - gap_start < 30
+ if window_end - window_start < 30 || window_end < window_start
+ return send(calc_method, 0.0, nil)
+ end
- lnrmssdx20(gap_start, gap_end - gap_start)
+ send(calc_method, window_start, window_end - window_start)
end
private
- def collect_rr_intervals
+ def cleanup_rr_intervals(rr_intervals)
# The rr_intervals Array stores the beat-to-beat time intervals (R-R).
# If one or move beats have been skipped during measurement, a nil value
# is inserted.
@@ -141,19 +165,15 @@ module PostRunner
# Each Fit4Ruby::HRV object has an Array called 'time' that contains up
# to 5 R-R interval durations. If less than 5 are present, they are
# filled with nil.
- raw_rr_intervals = []
- @fit_file.hrv.each do |hrv|
- raw_rr_intervals += hrv.time.compact
- end
- return if raw_rr_intervals.empty?
+ return if rr_intervals.empty?
- window = 20
- intro_mean = raw_rr_intervals[0..4 * window].reduce(:+) / (4 * window)
+ window = [ rr_intervals.length / 4, 20 ].min
+ intro_mean = rr_intervals[0..4 * window].reduce(:+) / (4 * window)
predictor = LinearPredictor.new(window, intro_mean)
# The timer accumulates the interval durations.
timer = 0.0
- raw_rr_intervals.each do |dt|
+ rr_intervals.each do |dt|
timer += dt
@timestamps << timer
@@ -164,6 +184,7 @@ module PostRunner
# value for this interval.
if (next_dt = predictor.predict) && dt > 1.5 * next_dt
@rr_intervals << nil
+ @errors += 1
else
@rr_intervals << dt
# Feed the value into the predictor.