Class: PostRunner::DailySleepAnalyzer

Inherits:
Object
  • Object
show all
Defined in:
lib/postrunner/DailySleepAnalyzer.rb

Overview

This class extracts the sleep information from a set of monitoring files and determines when and how long the user was awake or had a light or deep sleep. Determining the sleep state of a person purely based on wrist movement data is not very accurate. It gets a lot more accurate when heart rate data is available as well. The heart rate describes a sinus-like curve that aligns with the sleep cycles. Each sinus cycle corresponds to a sleep cycle. Unfortunately, current Garmin devices only use a default sampling time of 15 minutes. Since a sleep cycle is broken down into various sleep phases that normally last 10 - 15 minutes, there is a fairly high margin of error to determine the exact timing of the sleep cycle.

HR High —–+ ------- ------ HR Low --- -------- — Mov High – ------- ----- +– Mov Low --------- -- ----- Phase wk n1 n3 n2 rem n2 n3 n2 rem n2 n3 n2 Cycle 1 2 3

Legend: wk: wake n1: NREM1, n2: NREM2, n3: NREM3, rem: REM sleep

Too frequent or too strong movements abort the cycle to wake.

Constant Summary collapse

TIME_WINDOW_MINUTES =
24 * 60

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(monitoring_files, day, window_offest_secs) ⇒ DailySleepAnalyzer

Create a new DailySleepAnalyzer object to analyze the given monitoring files.

Parameters:

  • monitoring_files (Array)

    A set of Monitoring_B objects

  • day (String)

    Day to analyze as YY-MM-DD string

  • window_offest_secs (Fixnum)

    Offset (in seconds) of the time window to analyze against the midnight of the specified day



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/postrunner/DailySleepAnalyzer.rb', line 54

def initialize(monitoring_files, day, window_offest_secs)
  @window_start_time = @window_end_time = @utc_offset = nil

  # The following activity types are known:
  #  [ :undefined, :running, :cycling, :transition,
  #    :fitness_equipment, :swimming, :walking, :unknown7,
  #    :resting, :unknown9 ]
  @activity_type = Array.new(TIME_WINDOW_MINUTES, nil)
  # The activity values in the FIT files can range from 0 to 7.
  @activity_intensity = Array.new(TIME_WINDOW_MINUTES, nil)
  # Wrist motion data is not very well suited to determine wake or sleep
  # states. A single movement can be a turning motion, a NREM1 jerk or
  # even a movement while you dream. The fewer motions are detected, the
  # more likely you are really asleep. To even out single spikes, we
  # average the motions over a period of time. This Array stores the
  # weighted activity.
  @weighted_sleep_activity = Array.new(TIME_WINDOW_MINUTES, 8)
  # We classify the sleep activity into :wake, :low_activity and
  # :no_activity in this Array.
  @sleep_activity_classification = Array.new(TIME_WINDOW_MINUTES, nil)

  # The data from the monitoring files is stored in Arrays that cover 24
  # hours at 1 minute resolution. The algorithm currently cannot handle
  # time zone or DST changes. The day is always 24 hours and the local
  # time at noon the previous day is used for the whole window.
  @heart_rate = Array.new(TIME_WINDOW_MINUTES, nil)
  # From the wrist motion data and if available from the heart rate data,
  # we try to guess the sleep phase (:wake, :rem, :nrem1, :nrem2, :nrem3).
  # This Array will hold a minute-by-minute list of the guessed sleep
  # phase.
  @sleep_phase = Array.new(TIME_WINDOW_MINUTES, :wake)
  # The DailySleepAnalzyer extracts the sleep cycles from the monitoring
  # data. Each night usually has 5 - 6 sleep cycles. If we have heart rate
  # data, those cycles can be identified fairly well. If we have to rely
  # on wrist motion data only, we usually find more cycles than there
  # actually were. Each cycle is captured as SleepCycle object.
  @sleep_cycles = []
  # The resting heart rate.
  @resting_heart_rate = nil

  # Day as Time object. Midnight UTC.
  day_as_time = Time.parse(day + "-00:00:00+00:00").gmtime
  extract_data_from_monitor_files(monitoring_files, day_as_time,
                                  window_offest_secs)

  # We must have information about the local time zone the data was
  # recorded in. Abort if not available.
  return unless @utc_offset

  fill_monitoring_data
  categorize_sleep_activity

  if categorize_sleep_heart_rate
    # We have usable heart rate data for the sleep periods. Correlating
    # wrist motion data with heart rate cycles will greatly improve the
    # sleep phase and sleep cycle detection.
    categorize_sleep_phase_by_hr_level
    @sleep_cycles.each do |c|
      # Adjust the cycle boundaries to align with REM phase.
      c.adjust_cycle_boundaries(@sleep_phase)
      # Detect sleep phases for each cycle.
      c.detect_phases(@sleep_phase)
    end
  else
    # We have no usable heart rate data. Just guess sleep phases based on
    # wrist motion data.
    categorize_sleep_phase_by_activity_level
    @sleep_cycles.each { |c| c.detect_phases(@sleep_phase) }
  end
  dump_data
  delete_wake_cycles
  determine_resting_heart_rate
  calculate_totals
end

Instance Attribute Details

#deep_sleepObject (readonly)

Returns the value of attribute deep_sleep.



42
43
44
# File 'lib/postrunner/DailySleepAnalyzer.rb', line 42

def deep_sleep
  @deep_sleep
end

#light_sleepObject (readonly)

Returns the value of attribute light_sleep.



42
43
44
# File 'lib/postrunner/DailySleepAnalyzer.rb', line 42

def light_sleep
  @light_sleep
end

#rem_sleepObject (readonly)

Returns the value of attribute rem_sleep.



42
43
44
# File 'lib/postrunner/DailySleepAnalyzer.rb', line 42

def rem_sleep
  @rem_sleep
end

#resting_heart_rateObject (readonly)

Returns the value of attribute resting_heart_rate.



42
43
44
# File 'lib/postrunner/DailySleepAnalyzer.rb', line 42

def resting_heart_rate
  @resting_heart_rate
end

#sleep_cyclesObject (readonly)

Returns the value of attribute sleep_cycles.



42
43
44
# File 'lib/postrunner/DailySleepAnalyzer.rb', line 42

def sleep_cycles
  @sleep_cycles
end

#total_sleepObject (readonly)

Returns the value of attribute total_sleep.



42
43
44
# File 'lib/postrunner/DailySleepAnalyzer.rb', line 42

def total_sleep
  @total_sleep
end

#utc_offsetObject (readonly)

Returns the value of attribute utc_offset.



42
43
44
# File 'lib/postrunner/DailySleepAnalyzer.rb', line 42

def utc_offset
  @utc_offset
end

#window_end_timeObject (readonly)

Returns the value of attribute window_end_time.



42
43
44
# File 'lib/postrunner/DailySleepAnalyzer.rb', line 42

def window_end_time
  @window_end_time
end

#window_start_timeObject (readonly)

Returns the value of attribute window_start_time.



42
43
44
# File 'lib/postrunner/DailySleepAnalyzer.rb', line 42

def window_start_time
  @window_start_time
end