summaryrefslogtreecommitdiff
path: root/lib/postrunner/SleepCycle.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/postrunner/SleepCycle.rb')
-rw-r--r--lib/postrunner/SleepCycle.rb198
1 files changed, 198 insertions, 0 deletions
diff --git a/lib/postrunner/SleepCycle.rb b/lib/postrunner/SleepCycle.rb
new file mode 100644
index 0000000..d579258
--- /dev/null
+++ b/lib/postrunner/SleepCycle.rb
@@ -0,0 +1,198 @@
+#!/usr/bin/env ruby -w
+# encoding: UTF-8
+#
+# = SleepCycle.rb -- PostRunner - Manage the data from your Garmin sport devices.
+#
+# Copyright (c) 2016 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
+
+ # A SleepPhase is a segment of a sleep cycle. It captures the start and
+ # end time as well as the kind of phase.
+ class SleepPhase
+
+ attr_reader :from_time, :to_time, :phase
+
+ # Create a new sleep phase.
+ # @param from_time [Time] Start time of the phase
+ # @param to_time [Time] End time of the phase
+ # @param phase [Symbol] The kind of phase [ :rem, :nrem1, :nrem2, :nrem3 ]
+ def initialize(from_time, to_time, phase)
+ @from_time = from_time
+ @to_time = to_time
+ @phase = phase
+ end
+
+ # Duration of the phase in seconds.
+ # @return [Fixnum] duration
+ def duration
+ @to_time - @from_time
+ end
+
+ end
+
+ # A sleep cycle consists of several sleep phases. This class is used to
+ # gather and store the relevant data of a sleep cycle. Data is analzyed and
+ # stored with a one minute granularity. Time values are stored as minutes
+ # past the zero_idx_time.
+ class SleepCycle
+
+ attr_reader :total_seconds, :totals
+ attr_accessor :start_idx, :end_idx,
+ :high_low_trans_idx, :low_high_trans_idx,
+ :prev_cycle, :next_cycle
+
+ # Create a new SleepCycle record.
+ # @param zero_idx_time [Time] This is the time of the 0-th minute. All
+ # time values are stored as minutes past this time.
+ # @param start_idx [Fixnum] Time when the sleep cycle starts. We may start
+ # with an appromated value that gets fine tuned later on.
+ # @param prev_cycle [SleepCycle] A reference to the preceding sleep cycle
+ # or nil if this is the first cycle of the analyzed period.
+ def initialize(zero_idx_time, start_idx, prev_cycle = nil)
+ @zero_idx_time = zero_idx_time
+ @start_idx = start_idx
+ # These values will be determined later.
+ @end_idx = nil
+ # Every sleep cycle has at most one high/low heart rate transition and
+ # one low/high transition. These variables store the time of these
+ # transitions or nil if the transition does not exist. Every cycle must
+ # have at least one of these transitions to be a valid cycle.
+ @high_low_trans_idx = @low_high_trans_idx = nil
+ @prev_cycle = prev_cycle
+ # Register this cycle as successor of the previous cycle.
+ prev_cycle.next_cycle = self if prev_cycle
+ @next_cycle = nil
+ # Array holding the sleep phases of this cycle
+ @phases = []
+ # A hash with the total durations (in secods) of the various sleep
+ # phases.
+ @total_seconds = Hash.new(0)
+ end
+
+ # The start time of the cycle as Time object
+ # @return [Time]
+ def from_time
+ idx_to_time(@start_idx)
+ end
+
+ # The end time of the cycle as Time object.
+ # @return [Time]
+ def to_time
+ idx_to_time(@end_idx + 1)
+ end
+
+ # Remove this cycle from the cycle chain.
+ def unlink
+ @prev_cycle.next_cycle = @next_cycle if @prev_cycle
+ @next_cycle.prev_cycle = @prev_cycle if @next_cycle
+ end
+
+ # Initially, we use the high/low heart rate transition to mark the end
+ # of the cycle. But it's really the end of the REM phase that marks the
+ # end of a sleep cycle. If we find a REM phase, we use its end to adjust
+ # the sleep cycle boundaries.
+ # @param phases [Array] List of symbols that describe the sleep phase at
+ # at the minute corresponding to the Array index.
+ def adjust_cycle_boundaries(phases)
+ end_of_rem_phase_idx = nil
+ @start_idx.upto(@end_idx) do |i|
+ end_of_rem_phase_idx = i if phases[i] == :rem
+ end
+ if end_of_rem_phase_idx
+ # We have found a REM phase. Adjust the end_idx of this cycle
+ # accordingly.
+ @end_idx = end_of_rem_phase_idx
+ if @next_cycle
+ # If we have a successor phase, we also adjust the start.
+ @next_cycle.start_idx = end_of_rem_phase_idx + 1
+ end
+ end
+ end
+
+ # Gather a list of SleepPhase objects that describe the sequence of sleep
+ # phases in the provided Array.
+ # @param phases [Array] List of symbols that describe the sleep phase at
+ # at the minute corresponding to the Array index.
+ def detect_phases(phases)
+ @phases = []
+ current_phase = phases[0]
+ current_phase_start = @start_idx
+
+ @start_idx.upto(@end_idx) do |i|
+ if (current_phase && current_phase != phases[i]) || i == @end_idx
+ # We found a transition in the sequence. Create a SleepPhase object
+ # that describes the prepvious segment and add it to the @phases
+ # list.
+ @phases << (p = SleepPhase.new(idx_to_time(current_phase_start),
+ idx_to_time(i == @end_idx ? i + 1 : i),
+ current_phase))
+ # Add the duration of the phase to the corresponding sum in the
+ # @total_seconds Hash.
+ @total_seconds[current_phase] += p.duration
+
+ # Update the variables that track the start and kind of the
+ # currently read phase.
+ current_phase_start = i
+ current_phase = phases[i]
+ end
+ end
+ end
+
+ # Check if this cycle is really a sleep cycle or not. A sleep cycle must
+ # have at least one deep sleep phase or must be part of a directly
+ # attached series of cycles that contain a deep sleep phase.
+ # @return [Boolean] True if not a sleep cycle, false otherwise.
+ def is_wake_cycle?
+ !has_deep_sleep_phase? && !has_leading_deep_sleep_phase? &&
+ !has_trailing_deep_sleep_phase?
+ end
+
+
+ # Check if the cycle has a deep sleep phase.
+ # @return [Boolean] True of one of the phases is NREM3 phase. False
+ # otherwise.
+ def has_deep_sleep_phase?
+ # A real deep sleep phase must be at least 10 minutes long.
+ @phases.each do |p|
+ return true if p.phase == :nrem3 && p.duration > 10 * 60
+ end
+
+ false
+ end
+
+ # Check if any of the previous cycles that are directly attached have a
+ # deep sleep cycle.
+ # @return [Boolean] True if it has a leading sleep cycle.
+ def has_leading_deep_sleep_phase?
+ return false if @prev_cycle.nil? || @start_idx != @prev_cycle.end_idx + 1
+
+ @prev_cycle.has_deep_sleep_phase? ||
+ @prev_cycle.has_leading_deep_sleep_phase?
+ end
+
+ # Check if any of the trailing cycles that are directly attached have a
+ # deep sleep cycle.
+ # @return [Boolean] True if it has a trailing sleep cycle.
+ def has_trailing_deep_sleep_phase?
+ return false if @next_cycle.nil? || @end_idx + 1 != @next_cycle.start_idx
+
+ @next_cycle.has_deep_sleep_phase? ||
+ @next_cycle.has_trailing_deep_sleep_phase?
+ end
+
+ private
+
+ def idx_to_time(idx)
+ return nil unless idx
+ @zero_idx_time + 60 * idx
+ end
+
+ end
+
+end