From cf93c5b6db51e29adebe04564ba8e8758fe9a1c1 Mon Sep 17 00:00:00 2001 From: Jarred Heinrich Date: Thu, 19 Jan 2017 15:40:09 -0800 Subject: [PATCH 01/10] added sleep disturbance event, first pass --- .../suripu/core/flipper/FeatureFlipper.java | 1 + .../com/hello/suripu/core/models/Event.java | 16 +++- .../models/Events/SleepDisturbanceEvent.java | 32 +++++++ .../processors/FeatureFlippedProcessor.java | 5 + .../suripu/core/translations/English.java | 2 + .../hello/suripu/core/util/TimelineUtils.java | 96 +++++++++++++++++++ .../suripu/core/util/TimelineUtilsTest.java | 8 ++ .../InstrumentedTimelineProcessor.java | 13 +++ 8 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 suripu-core/src/main/java/com/hello/suripu/core/models/Events/SleepDisturbanceEvent.java diff --git a/suripu-core/src/main/java/com/hello/suripu/core/flipper/FeatureFlipper.java b/suripu-core/src/main/java/com/hello/suripu/core/flipper/FeatureFlipper.java index 4bf24fd11..00e0100e9 100644 --- a/suripu-core/src/main/java/com/hello/suripu/core/flipper/FeatureFlipper.java +++ b/suripu-core/src/main/java/com/hello/suripu/core/flipper/FeatureFlipper.java @@ -123,6 +123,7 @@ public class FeatureFlipper { public final static String TIMELINE_EVENT_SLEEP_SCORE_ENFORCEMENT = "timeline_event_sleep_score_enforcement"; public final static String TIMELINE_IN_SLEEP_INSIGHTS = "timeline_in_sleep_insights"; + public final static String TIMELINE_INTERRUPTION_EVENT = "timeline_interruption_event"; public final static String TIMELINE_V2_AVAILABLE = "timeline_v2_available"; public final static String VIEW_SENSORS_UNAVAILABLE = "view_sensors_unavailable"; diff --git a/suripu-core/src/main/java/com/hello/suripu/core/models/Event.java b/suripu-core/src/main/java/com/hello/suripu/core/models/Event.java index 34156ac4c..3a384213b 100644 --- a/suripu-core/src/main/java/com/hello/suripu/core/models/Event.java +++ b/suripu-core/src/main/java/com/hello/suripu/core/models/Event.java @@ -14,6 +14,7 @@ import com.hello.suripu.core.models.Events.OutOfBedEvent; import com.hello.suripu.core.models.Events.PartnerMotionEvent; import com.hello.suripu.core.models.Events.FallingAsleepEvent; +import com.hello.suripu.core.models.Events.SleepDisturbanceEvent; import com.hello.suripu.core.models.Events.SleepMotionEvent; import com.hello.suripu.core.models.Events.SleepingEvent; import com.hello.suripu.core.models.Events.SunRiseEvent; @@ -44,7 +45,8 @@ public enum Type { // in order of display priority SLEEP(12), OUT_OF_BED(13), WAKE_UP(14), - ALARM(15); + ALARM(15), + SLEEP_DISTURBANCE(16); private int value; @@ -149,6 +151,8 @@ public static Event extend(final Event event, final long startTimestamp, final l return new AlarmEvent(startTimestamp, endTimestamp, event.getTimezoneOffset()); case NOISE: return new NoiseEvent(startTimestamp, endTimestamp, event.getTimezoneOffset(), event.getSleepDepth()); + case SLEEP_DISTURBANCE: + return new SleepDisturbanceEvent(startTimestamp, endTimestamp, event.getTimezoneOffset(), event.getSleepDepth()); default: return new NullEvent(startTimestamp, endTimestamp, event.getTimezoneOffset(), event.getSleepDepth()); @@ -186,7 +190,9 @@ public static Event extend(final Event event, final long startTimestamp, final l case SLEEPING: return new SleepingEvent(startTimestamp, endTimestamp, event.getTimezoneOffset(), sleepDepth); case NOISE: - return new NoiseEvent(startTimestamp, endTimestamp, event.getTimezoneOffset(), sleepDepth); + return new NoiseEvent(startTimestamp, endTimestamp, event.getTimezoneOffset(), sleepDepth); + case SLEEP_DISTURBANCE: + return new SleepDisturbanceEvent(startTimestamp, endTimestamp, event.getTimezoneOffset(), sleepDepth); default: return new NullEvent(startTimestamp, endTimestamp, event.getTimezoneOffset(), sleepDepth); @@ -290,6 +296,12 @@ public static Event createFromType(final Type type, return new AlarmEvent(startTimestamp, endTimestamp, offsetMillis, messageOptional.get()); case NOISE: return new NoiseEvent(startTimestamp, endTimestamp, offsetMillis, sleepDepth.get()); + case SLEEP_DISTURBANCE: + if (!messageOptional.isPresent()) { + throw new IllegalArgumentException("message required."); + } + return new SleepDisturbanceEvent(startTimestamp, endTimestamp, offsetMillis, sleepDepth.get()); + default: if(!sleepDepth.isPresent()){ throw new IllegalArgumentException("sleepDepth required."); diff --git a/suripu-core/src/main/java/com/hello/suripu/core/models/Events/SleepDisturbanceEvent.java b/suripu-core/src/main/java/com/hello/suripu/core/models/Events/SleepDisturbanceEvent.java new file mode 100644 index 000000000..659696cff --- /dev/null +++ b/suripu-core/src/main/java/com/hello/suripu/core/models/Events/SleepDisturbanceEvent.java @@ -0,0 +1,32 @@ +package com.hello.suripu.core.models.Events; + +import com.hello.suripu.core.models.Event; +import com.hello.suripu.core.models.SleepSegment; +import com.hello.suripu.core.translations.English; + +/** + * Created by jarredheinrich on 1/19/17. + */ +public class SleepDisturbanceEvent extends Event { + private String description = English.SLEEP_DISTURBANCE_MESSAGE; + private int sleepDepth = 0; + public SleepDisturbanceEvent (final long startTimestamp, final long endTimestamp, final int offsetMillis, final int sleepDepth) { + super(Type.SLEEP_DISTURBANCE, startTimestamp, endTimestamp, offsetMillis); + this.sleepDepth = sleepDepth; + } + + @Override + public String getDescription(){ + return this.description; + } + + @Override + public SleepSegment.SoundInfo getSoundInfo() { + return null; + } + + @Override + public int getSleepDepth() { + return this.sleepDepth; + } +} diff --git a/suripu-core/src/main/java/com/hello/suripu/core/processors/FeatureFlippedProcessor.java b/suripu-core/src/main/java/com/hello/suripu/core/processors/FeatureFlippedProcessor.java index 84954b405..a72c42c69 100644 --- a/suripu-core/src/main/java/com/hello/suripu/core/processors/FeatureFlippedProcessor.java +++ b/suripu-core/src/main/java/com/hello/suripu/core/processors/FeatureFlippedProcessor.java @@ -191,4 +191,9 @@ protected Boolean useUninterruptedDuration(final Long accountId) { protected Boolean useSmartAlarmRefactored(final Long accountId){ return featureFlipper.userFeatureActive(FeatureFlipper.SMART_ALARM_REFACTORED, accountId, Collections.EMPTY_LIST); } + + protected Boolean hasInterruptionEvent(final Long accountId){ + return featureFlipper.userFeatureActive(FeatureFlipper.TIMELINE_INTERRUPTION_EVENT, accountId, Collections.EMPTY_LIST); + } + } diff --git a/suripu-core/src/main/java/com/hello/suripu/core/translations/English.java b/suripu-core/src/main/java/com/hello/suripu/core/translations/English.java index a457b3adc..8057133e1 100644 --- a/suripu-core/src/main/java/com/hello/suripu/core/translations/English.java +++ b/suripu-core/src/main/java/com/hello/suripu/core/translations/English.java @@ -28,9 +28,11 @@ public class English { public final static String ALARM_NOT_SO_SMART_MESSAGE = "Your Smart Alarm rang at **%s**."; public final static String ALARM_SMART_MESSAGE = "Your Smart Alarm rang at **%s**.\nYou set it to wake you up by **%s**."; public final static String NOISE_MESSAGE = "There was a noise disturbance."; + public final static String SLEEP_DISTURBANCE_MESSAGE = "Your sleep was interrupted."; public final static String NULL_MESSAGE = ""; + /* END Events Declaration */ diff --git a/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java b/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java index 44a8d0913..f20ce3e62 100644 --- a/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java +++ b/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java @@ -19,6 +19,7 @@ import com.hello.suripu.algorithm.sleep.scores.WaveAccumulateMotionScoreFunction; import com.hello.suripu.algorithm.sleep.scores.ZeroToMaxMotionCountDurationScoreFunction; import com.hello.suripu.algorithm.utils.MotionFeatures; +import com.hello.suripu.core.algorithmintegration.OneDaysTrackerMotion; import com.hello.suripu.core.logging.LoggerWithSessionId; import com.hello.suripu.core.models.AgitatedSleep; import com.hello.suripu.core.models.AllSensorSampleList; @@ -32,6 +33,7 @@ import com.hello.suripu.core.models.Events.NoiseEvent; import com.hello.suripu.core.models.Events.NullEvent; import com.hello.suripu.core.models.Events.OutOfBedEvent; +import com.hello.suripu.core.models.Events.SleepDisturbanceEvent; import com.hello.suripu.core.models.Events.SleepingEvent; import com.hello.suripu.core.models.Events.WakeupEvent; import com.hello.suripu.core.models.Insight; @@ -1457,6 +1459,100 @@ public List getAlarmEvents(final List ringTimes, final DateTime return events; } + public List getSleepDisturbanceEvents(final OneDaysTrackerMotion oneDaysTrackerMotion, final Long sleepTime, final Long wakeTime, final TimeZoneOffsetMap timeZoneOffsetMap){ + final long sleepBuffer = 30 * DateTimeConstants.MILLIS_PER_MINUTE; + final long wakeBuffer= 60 * DateTimeConstants.MILLIS_PER_MINUTE; + List trackerMotions = new ArrayList<>(); + for(final TrackerMotion trackerMotion : oneDaysTrackerMotion.originalTrackerMotions){ + if (trackerMotion.timestamp > sleepTime + sleepBuffer && trackerMotion.timestamp < wakeTime - wakeBuffer){ + trackerMotions.add(trackerMotion); + } + } + Collections.sort(trackerMotions, new CustomComparator()); + final HashMap sleepDisturbanceTimeStamps= getSleepDisturbances(trackerMotions); + final List sleepDisturbanceEvents = new ArrayList<>(); + for (final long sleepDisturbanceStartTS : sleepDisturbanceTimeStamps.keySet()){ + final SleepDisturbanceEvent sleepDisturbanceEvent = new SleepDisturbanceEvent(sleepDisturbanceStartTS, sleepDisturbanceTimeStamps.get(sleepDisturbanceStartTS), timeZoneOffsetMap.getOffsetWithDefaultAsZero(sleepDisturbanceStartTS), 0); + sleepDisturbanceEvents.add(sleepDisturbanceEvent); + } + + return sleepDisturbanceEvents; + } + + //Finds intances where the pill recorded 15seconds + of motion within a 4 minute window + public HashMap getSleepDisturbances(final List trackerMotions){ + + // computes periods of agitated sleep using on duration. Over 16 seconds of movement within a two minute window initiates a state of agitated sleep that persists until there is a 4 minute window with no motion + final int onDurationSumThreshold = 15; //secs + final long noMotionThreshold = DateTimeConstants.MILLIS_PER_MINUTE * 10; + final long timeWindow = DateTimeConstants.MILLIS_PER_MINUTE * 4; //4 minutes - finds consecutive minutes with some flexibility + final int maxDisturbanceCount = 5; //max number of sleep disturbances to report during night + long currentTS = 0L; + long currentDisturbanceStartTS = 0L; + long currentDisturbanceEndTS = 0L; + long previousDisturbanceStartTS = 0L; + long previousDisturbanceEndTS = 0L; + + List trackerMotionWindowCurrent = new ArrayList<>(); + final HashMap sleepDisturbances = new HashMap<>(); + final HashMap sleepDisturbanceWindows = new HashMap<>(); + for (TrackerMotion trackerMotionCurrent : trackerMotions) { + final List trackerMotionWindowPrevious = trackerMotionWindowCurrent; + trackerMotionWindowCurrent = new ArrayList<>(); + trackerMotionWindowCurrent.add(trackerMotionCurrent); + currentTS = trackerMotionCurrent.timestamp; + currentDisturbanceStartTS = currentTS; + + currentDisturbanceEndTS = currentTS; + int onDurationSum = trackerMotionCurrent.onDurationInSeconds.intValue(); + if (!trackerMotionWindowPrevious.isEmpty()) { + for (final TrackerMotion trackerMotionPrevious : trackerMotionWindowPrevious) { + if (currentTS - trackerMotionPrevious.timestamp <= timeWindow) { + trackerMotionWindowCurrent.add(trackerMotionPrevious); + onDurationSum += trackerMotionPrevious.onDurationInSeconds.intValue(); + currentDisturbanceEndTS = Math.max(currentDisturbanceStartTS, trackerMotionPrevious.timestamp); + currentDisturbanceStartTS = Math.min(currentDisturbanceStartTS, trackerMotionPrevious.timestamp); + } + } + } + if (onDurationSum > onDurationSumThreshold){ + final boolean isNewDisturbance = (currentDisturbanceStartTS - previousDisturbanceEndTS > noMotionThreshold); + if (isNewDisturbance) { + if (!sleepDisturbances.containsKey(currentDisturbanceStartTS)) { + sleepDisturbances.put(currentDisturbanceStartTS, onDurationSum); + previousDisturbanceStartTS = currentDisturbanceStartTS; + } + } + previousDisturbanceEndTS = currentDisturbanceEndTS; + sleepDisturbanceWindows.put(previousDisturbanceStartTS, previousDisturbanceEndTS); + } + + } + + if (sleepDisturbances.size() > maxDisturbanceCount){ + final int discardCount = sleepDisturbances.size() - maxDisturbanceCount; + final List tsKeys = new ArrayList<>(sleepDisturbances.keySet()); + final List onDurationSums = new ArrayList<>(sleepDisturbances.values()); + Collections.sort(onDurationSums); + final int minOnDuration = onDurationSums.get(discardCount); + int removedCount = 0; + for (long tsKey : tsKeys){ + if (sleepDisturbances.get(tsKey)< minOnDuration){ + sleepDisturbanceWindows.remove(tsKey); + removedCount +=1; + } + } + } + //possible for two sleep disturbances to have the same onDurationSum, in this case it is possible to have > 5 sleepDisturbances; + return sleepDisturbanceWindows; + } + + public class CustomComparator { + public boolean compare(TrackerMotion object1, TrackerMotion object2) { + return object1.timestamp < object2.timestamp; + } + } + public List eventsFromOptionalEvents(final List> optionalEvents) { final List events = Lists.newArrayList(); diff --git a/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java b/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java index ccaa5a732..d7bf8079a 100644 --- a/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java +++ b/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java @@ -15,6 +15,7 @@ import java.io.IOException; import java.net.URL; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -106,4 +107,11 @@ public void testMotionDuringSleepCheck(){ } + @Test + public void testGetSleepDisturbances() { + final List trackerMotions = CSVLoader.loadTrackerMotionFromCSV("fixtures/tracker_motion/nn_raw_tracker_motion.csv"); + final HashMap sleepDisturbances = timelineUtils.getSleepDisturbances(trackerMotions); + int i = 0; + } + } \ No newline at end of file diff --git a/suripu-coredropwizard/src/main/java/com/hello/suripu/coredropwizard/timeline/InstrumentedTimelineProcessor.java b/suripu-coredropwizard/src/main/java/com/hello/suripu/coredropwizard/timeline/InstrumentedTimelineProcessor.java index f88ccca02..5e2713597 100644 --- a/suripu-coredropwizard/src/main/java/com/hello/suripu/coredropwizard/timeline/InstrumentedTimelineProcessor.java +++ b/suripu-coredropwizard/src/main/java/com/hello/suripu/coredropwizard/timeline/InstrumentedTimelineProcessor.java @@ -620,9 +620,13 @@ public PopulatedTimelines populateTimeline(final long accountId,final DateTime d final Map timelineEvents = TimelineRefactored.populateTimeline(motionEvents, timeZoneOffsetMap); Optional sleepTime = Optional.absent(); + Optional wakeTime = Optional.absent(); if (sleep.isPresent()){ sleepTime = Optional.of(sleep.get().getEndTimestamp()); } + if (wake.isPresent()) { + wakeTime = Optional.of(wake.get().getStartTimestamp()); + } // LIGHT @@ -693,6 +697,15 @@ public PopulatedTimelines populateTimeline(final long accountId,final DateTime d } } + if (hasInterruptionEvent(accountId)){ + if (sleepTime.isPresent() && wakeTime.isPresent()) { + final List sleepDisturbanceEvents = timelineUtils.getSleepDisturbanceEvents(sensorData.oneDaysTrackerMotion, sleepTime.get(), wakeTime.get(), timeZoneOffsetMap); + for(final Event sleepDisturbanceEvent : sleepDisturbanceEvents){ + timelineEvents.put(sleepDisturbanceEvent.getStartTimestamp(), sleepDisturbanceEvent); + } + } + } + /* add main events */ for (final Event event : reprocessedEvents.mainEvents.values()) { timelineEvents.put(event.getStartTimestamp(), event); From 52de3d968334afb75d4c9e7d58c2a0ac8da907cb Mon Sep 17 00:00:00 2001 From: Jarred Heinrich Date: Mon, 23 Jan 2017 16:46:58 -0800 Subject: [PATCH 02/10] added test --- .../hello/suripu/core/util/TimelineUtils.java | 16 +++++--- .../suripu/core/util/TimelineUtilsTest.java | 37 +++++++++++++++++-- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java b/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java index f20ce3e62..b4c74233d 100644 --- a/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java +++ b/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java @@ -1468,7 +1468,6 @@ public List getSleepDisturbanceEvents(final OneDaysTrackerMotion oneDaysT trackerMotions.add(trackerMotion); } } - Collections.sort(trackerMotions, new CustomComparator()); final HashMap sleepDisturbanceTimeStamps= getSleepDisturbances(trackerMotions); final List sleepDisturbanceEvents = new ArrayList<>(); for (final long sleepDisturbanceStartTS : sleepDisturbanceTimeStamps.keySet()){ @@ -1488,18 +1487,21 @@ public HashMap getSleepDisturbances(final List tracke final long timeWindow = DateTimeConstants.MILLIS_PER_MINUTE * 4; //4 minutes - finds consecutive minutes with some flexibility final int maxDisturbanceCount = 5; //max number of sleep disturbances to report during night long currentTS = 0L; - long currentDisturbanceStartTS = 0L; - long currentDisturbanceEndTS = 0L; + long currentDisturbanceStartTS ; + long currentDisturbanceEndTS ; long previousDisturbanceStartTS = 0L; long previousDisturbanceEndTS = 0L; + long previousMotionTS; List trackerMotionWindowCurrent = new ArrayList<>(); final HashMap sleepDisturbances = new HashMap<>(); final HashMap sleepDisturbanceWindows = new HashMap<>(); + boolean currentlyDisturbed = false; for (TrackerMotion trackerMotionCurrent : trackerMotions) { final List trackerMotionWindowPrevious = trackerMotionWindowCurrent; trackerMotionWindowCurrent = new ArrayList<>(); trackerMotionWindowCurrent.add(trackerMotionCurrent); + previousMotionTS = currentTS; currentTS = trackerMotionCurrent.timestamp; currentDisturbanceStartTS = currentTS; @@ -1516,8 +1518,8 @@ public HashMap getSleepDisturbances(final List tracke } } if (onDurationSum > onDurationSumThreshold){ - final boolean isNewDisturbance = (currentDisturbanceStartTS - previousDisturbanceEndTS > noMotionThreshold); - if (isNewDisturbance) { + if (!currentlyDisturbed) { + currentlyDisturbed = true; if (!sleepDisturbances.containsKey(currentDisturbanceStartTS)) { sleepDisturbances.put(currentDisturbanceStartTS, onDurationSum); previousDisturbanceStartTS = currentDisturbanceStartTS; @@ -1526,6 +1528,10 @@ public HashMap getSleepDisturbances(final List tracke previousDisturbanceEndTS = currentDisturbanceEndTS; sleepDisturbanceWindows.put(previousDisturbanceStartTS, previousDisturbanceEndTS); } + if (currentTS - previousMotionTS > noMotionThreshold && currentlyDisturbed){ + sleepDisturbanceWindows.put(previousDisturbanceStartTS, previousMotionTS); + currentlyDisturbed = false; + } } diff --git a/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java b/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java index d7bf8079a..9665fe4e8 100644 --- a/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java +++ b/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java @@ -2,6 +2,7 @@ import com.google.common.base.Charsets; import com.google.common.base.Optional; +import com.google.common.collect.Lists; import com.google.common.io.Resources; import com.hello.suripu.core.models.Event; import com.hello.suripu.core.models.Events.MotionEvent; @@ -10,6 +11,8 @@ import com.hello.suripu.core.models.TimeZoneHistory; import com.hello.suripu.core.models.TrackerMotion; import com.hello.suripu.core.translations.English; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; import org.junit.Test; import java.io.IOException; @@ -109,9 +112,37 @@ public void testMotionDuringSleepCheck(){ @Test public void testGetSleepDisturbances() { - final List trackerMotions = CSVLoader.loadTrackerMotionFromCSV("fixtures/tracker_motion/nn_raw_tracker_motion.csv"); + List trackerMotions = CSVLoader.loadTrackerMotionFromCSV("fixtures/tracker_motion/nn_raw_tracker_motion.csv"); + trackerMotions = Lists.reverse(trackerMotions); final HashMap sleepDisturbances = timelineUtils.getSleepDisturbances(trackerMotions); - int i = 0; - } + final List startMinutes = new ArrayList<>(); + final List endMinutes = new ArrayList<>(); + final List span = new ArrayList<>(); + for (final long start : sleepDisturbances.keySet()){ + final DateTime startTime = new DateTime(start, DateTimeZone.forID("America/Los_Angeles")); + final DateTime endTime = new DateTime(sleepDisturbances.get(start), DateTimeZone.forID("America/Los_Angeles")); + final int index_start, index_end; + if (startTime.getHourOfDay() > 23 || startTime.getHourOfDay() < 12){ + index_start = (startTime.getHourOfDay() + 6) * 60 + startTime.getMinuteOfHour(); + startMinutes.add(index_start); + } else { + index_start = (startTime.getHourOfDay() - 18) * 60 + startTime.getMinuteOfHour(); + startMinutes.add(index_start); + } + if (endTime.getHourOfDay() > 23 || endTime.getHourOfDay() < 12){ + index_end = (endTime.getHourOfDay() + 6) * 60 + endTime.getMinuteOfHour(); + endMinutes.add(index_end); + } else { + index_end = (endTime.getHourOfDay() - 18) * 60 + endTime.getMinuteOfHour(); + endMinutes.add(index_end); + } + span.add(index_end - index_start); + } + final Integer[] startMinutesActual = {315,333,404,585, 735}; + assertThat(startMinutesActual.length == startMinutes.size(), is(true)); + for (Integer startMinute : startMinutes){ + assertThat(startMinutes.contains(startMinute), is(true)); + } + } } \ No newline at end of file From 182574fefd879abff328ce323ccba6c98140476c Mon Sep 17 00:00:00 2001 From: Jarred Heinrich Date: Tue, 24 Jan 2017 12:43:23 -0800 Subject: [PATCH 03/10] added missing SLEEP_DISTRUBANCE event, fixed sleepDisturbance logic --- .../suripu/core/models/timeline/v2/EventType.java | 3 ++- .../core/models/timeline/v2/TimelineEvent.java | 2 ++ .../core/models/timeline/v2/ValidAction.java | 1 + .../com/hello/suripu/core/util/TimelineUtils.java | 15 +++++---------- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/EventType.java b/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/EventType.java index 4c28f81f5..357fd992b 100644 --- a/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/EventType.java +++ b/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/EventType.java @@ -18,7 +18,8 @@ public enum EventType { GOT_OUT_OF_BED(13), WOKE_UP(14), ALARM_RANG(15), - UNKNOWN(16); + SLEEP_DISTURBANCE(16), + UNKNOWN(17); private static EventType[] cachedValues = null; diff --git a/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/TimelineEvent.java b/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/TimelineEvent.java index 53ef874d4..f82a5962a 100644 --- a/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/TimelineEvent.java +++ b/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/TimelineEvent.java @@ -128,6 +128,8 @@ public static List fromV1(final List segments) { temp.put(Event.Type.SUNSET, EventType.SUNSET); temp.put(Event.Type.SUNRISE, EventType.SUNRISE); + temp.put(Event.Type.SLEEP_DISTURBANCE, EventType.SLEEP_DISTURBANCE);; + typesMapping = ImmutableMap.copyOf(temp); } diff --git a/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/ValidAction.java b/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/ValidAction.java index e32101269..7a55ff5d4 100644 --- a/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/ValidAction.java +++ b/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/ValidAction.java @@ -30,6 +30,7 @@ public static List from(Event.Type type) { case NOISE: case SNORING: case SLEEP_TALK: + case SLEEP_DISTURBANCE: return Lists.newArrayList(VERIFY, INCORRECT); default: return Lists.newArrayList(); diff --git a/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java b/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java index b4c74233d..36aba9991 100644 --- a/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java +++ b/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java @@ -1490,13 +1490,14 @@ public HashMap getSleepDisturbances(final List tracke long currentDisturbanceStartTS ; long currentDisturbanceEndTS ; long previousDisturbanceStartTS = 0L; - long previousDisturbanceEndTS = 0L; - long previousMotionTS; + long previousDisturbanceEndTS; + long previousMotionTS = 0L; List trackerMotionWindowCurrent = new ArrayList<>(); final HashMap sleepDisturbances = new HashMap<>(); final HashMap sleepDisturbanceWindows = new HashMap<>(); boolean currentlyDisturbed = false; + for (TrackerMotion trackerMotionCurrent : trackerMotions) { final List trackerMotionWindowPrevious = trackerMotionWindowCurrent; trackerMotionWindowCurrent = new ArrayList<>(); @@ -1512,7 +1513,7 @@ public HashMap getSleepDisturbances(final List tracke if (currentTS - trackerMotionPrevious.timestamp <= timeWindow) { trackerMotionWindowCurrent.add(trackerMotionPrevious); onDurationSum += trackerMotionPrevious.onDurationInSeconds.intValue(); - currentDisturbanceEndTS = Math.max(currentDisturbanceStartTS, trackerMotionPrevious.timestamp); + currentDisturbanceEndTS = Math.max(currentDisturbanceEndTS, trackerMotionPrevious.timestamp); currentDisturbanceStartTS = Math.min(currentDisturbanceStartTS, trackerMotionPrevious.timestamp); } } @@ -1527,8 +1528,7 @@ public HashMap getSleepDisturbances(final List tracke } previousDisturbanceEndTS = currentDisturbanceEndTS; sleepDisturbanceWindows.put(previousDisturbanceStartTS, previousDisturbanceEndTS); - } - if (currentTS - previousMotionTS > noMotionThreshold && currentlyDisturbed){ + }else if (currentTS - previousMotionTS > noMotionThreshold && currentlyDisturbed){ sleepDisturbanceWindows.put(previousDisturbanceStartTS, previousMotionTS); currentlyDisturbed = false; } @@ -1553,11 +1553,6 @@ public HashMap getSleepDisturbances(final List tracke return sleepDisturbanceWindows; } - public class CustomComparator { - public boolean compare(TrackerMotion object1, TrackerMotion object2) { - return object1.timestamp < object2.timestamp; - } - } public List eventsFromOptionalEvents(final List> optionalEvents) { final List events = Lists.newArrayList(); From c172adbff4434351fa3ef4339f982774ec9286e5 Mon Sep 17 00:00:00 2001 From: Jarred Heinrich Date: Tue, 24 Jan 2017 14:40:17 -0800 Subject: [PATCH 04/10] changed sleep disturbance ranking to event duration --- .../com/hello/suripu/core/util/TimelineUtils.java | 13 ++++++++----- .../hello/suripu/core/util/TimelineUtilsTest.java | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java b/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java index 36aba9991..83f75dfd9 100644 --- a/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java +++ b/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java @@ -1482,7 +1482,7 @@ public List getSleepDisturbanceEvents(final OneDaysTrackerMotion oneDaysT public HashMap getSleepDisturbances(final List trackerMotions){ // computes periods of agitated sleep using on duration. Over 16 seconds of movement within a two minute window initiates a state of agitated sleep that persists until there is a 4 minute window with no motion - final int onDurationSumThreshold = 15; //secs + final int onDurationSumThreshold = 10; //secs final long noMotionThreshold = DateTimeConstants.MILLIS_PER_MINUTE * 10; final long timeWindow = DateTimeConstants.MILLIS_PER_MINUTE * 4; //4 minutes - finds consecutive minutes with some flexibility final int maxDisturbanceCount = 5; //max number of sleep disturbances to report during night @@ -1538,12 +1538,15 @@ public HashMap getSleepDisturbances(final List tracke if (sleepDisturbances.size() > maxDisturbanceCount){ final int discardCount = sleepDisturbances.size() - maxDisturbanceCount; final List tsKeys = new ArrayList<>(sleepDisturbances.keySet()); - final List onDurationSums = new ArrayList<>(sleepDisturbances.values()); - Collections.sort(onDurationSums); - final int minOnDuration = onDurationSums.get(discardCount); + final List disturbanceSpan = new ArrayList<>(); + for (final long sleepDisturbanceStart : sleepDisturbanceWindows.keySet()){ + disturbanceSpan.add((int) (sleepDisturbanceWindows.get(sleepDisturbanceStart) - sleepDisturbanceStart)/ DateTimeConstants.MILLIS_PER_MINUTE); + } + Collections.sort(disturbanceSpan); + final int minDisturbanceSpan = disturbanceSpan.get(discardCount); int removedCount = 0; for (long tsKey : tsKeys){ - if (sleepDisturbances.get(tsKey)< minOnDuration){ + if ((sleepDisturbanceWindows.get(tsKey) - tsKey)/DateTimeConstants.MILLIS_PER_MINUTE < minDisturbanceSpan){ sleepDisturbanceWindows.remove(tsKey); removedCount +=1; } diff --git a/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java b/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java index 9665fe4e8..b9b191b9a 100644 --- a/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java +++ b/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java @@ -139,7 +139,7 @@ public void testGetSleepDisturbances() { } span.add(index_end - index_start); } - final Integer[] startMinutesActual = {315,333,404,585, 735}; + final Integer[] startMinutesActual = {315,390,495,585, 735}; assertThat(startMinutesActual.length == startMinutes.size(), is(true)); for (Integer startMinute : startMinutes){ assertThat(startMinutes.contains(startMinute), is(true)); From 55763ca3c627ce452e58e29a4829970dfd96ff89 Mon Sep 17 00:00:00 2001 From: Jarred Heinrich Date: Thu, 26 Jan 2017 13:37:24 -0800 Subject: [PATCH 05/10] reverted enum ordering --- .../com/hello/suripu/core/models/timeline/v2/EventType.java | 5 +++-- .../hello/suripu/core/models/timeline/v2/ValidAction.java | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/EventType.java b/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/EventType.java index 357fd992b..5860ff21c 100644 --- a/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/EventType.java +++ b/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/EventType.java @@ -18,8 +18,9 @@ public enum EventType { GOT_OUT_OF_BED(13), WOKE_UP(14), ALARM_RANG(15), - SLEEP_DISTURBANCE(16), - UNKNOWN(17); + UNKNOWN(16), + SLEEP_DISTURBANCE(17), + ; private static EventType[] cachedValues = null; diff --git a/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/ValidAction.java b/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/ValidAction.java index 7a55ff5d4..eea76a2d4 100644 --- a/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/ValidAction.java +++ b/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/ValidAction.java @@ -23,6 +23,7 @@ public static List from(Event.Type type) { case IN_BED: case OUT_OF_BED: case WAKE_UP: + case SLEEP_DISTURBANCE: return Lists.newArrayList(ADJUST_TIME, VERIFY, INCORRECT); case LIGHTS_OUT: case MOTION: @@ -30,7 +31,6 @@ public static List from(Event.Type type) { case NOISE: case SNORING: case SLEEP_TALK: - case SLEEP_DISTURBANCE: return Lists.newArrayList(VERIFY, INCORRECT); default: return Lists.newArrayList(); From e237896d0433471ea7702a28119ae289801e6157 Mon Sep 17 00:00:00 2001 From: Jarred Heinrich Date: Thu, 26 Jan 2017 16:56:39 -0800 Subject: [PATCH 06/10] updated event-type for testing, added event filter --- .../suripu/core/flipper/FeatureFlipper.java | 2 +- .../models/timeline/v2/TimelineEvent.java | 2 +- .../core/models/timeline/v2/ValidAction.java | 2 +- .../processors/FeatureFlippedProcessor.java | 4 +- .../hello/suripu/core/util/TimelineUtils.java | 63 +++++++++++++------ .../suripu/core/util/TimelineUtilsTest.java | 4 +- .../InstrumentedTimelineProcessor.java | 12 +++- 7 files changed, 59 insertions(+), 30 deletions(-) diff --git a/suripu-core/src/main/java/com/hello/suripu/core/flipper/FeatureFlipper.java b/suripu-core/src/main/java/com/hello/suripu/core/flipper/FeatureFlipper.java index 00e0100e9..fdda1bfc8 100644 --- a/suripu-core/src/main/java/com/hello/suripu/core/flipper/FeatureFlipper.java +++ b/suripu-core/src/main/java/com/hello/suripu/core/flipper/FeatureFlipper.java @@ -123,7 +123,7 @@ public class FeatureFlipper { public final static String TIMELINE_EVENT_SLEEP_SCORE_ENFORCEMENT = "timeline_event_sleep_score_enforcement"; public final static String TIMELINE_IN_SLEEP_INSIGHTS = "timeline_in_sleep_insights"; - public final static String TIMELINE_INTERRUPTION_EVENT = "timeline_interruption_event"; + public final static String TIMELINE_SLEEP_DISTURBANCE_EVENT = "timeline_sleep_disturbance_event"; public final static String TIMELINE_V2_AVAILABLE = "timeline_v2_available"; public final static String VIEW_SENSORS_UNAVAILABLE = "view_sensors_unavailable"; diff --git a/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/TimelineEvent.java b/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/TimelineEvent.java index f82a5962a..f30c3c8ad 100644 --- a/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/TimelineEvent.java +++ b/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/TimelineEvent.java @@ -128,7 +128,7 @@ public static List fromV1(final List segments) { temp.put(Event.Type.SUNSET, EventType.SUNSET); temp.put(Event.Type.SUNRISE, EventType.SUNRISE); - temp.put(Event.Type.SLEEP_DISTURBANCE, EventType.SLEEP_DISTURBANCE);; + temp.put(Event.Type.SLEEP_DISTURBANCE, EventType.GENERIC_MOTION);; typesMapping = ImmutableMap.copyOf(temp); } diff --git a/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/ValidAction.java b/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/ValidAction.java index eea76a2d4..7a55ff5d4 100644 --- a/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/ValidAction.java +++ b/suripu-core/src/main/java/com/hello/suripu/core/models/timeline/v2/ValidAction.java @@ -23,7 +23,6 @@ public static List from(Event.Type type) { case IN_BED: case OUT_OF_BED: case WAKE_UP: - case SLEEP_DISTURBANCE: return Lists.newArrayList(ADJUST_TIME, VERIFY, INCORRECT); case LIGHTS_OUT: case MOTION: @@ -31,6 +30,7 @@ public static List from(Event.Type type) { case NOISE: case SNORING: case SLEEP_TALK: + case SLEEP_DISTURBANCE: return Lists.newArrayList(VERIFY, INCORRECT); default: return Lists.newArrayList(); diff --git a/suripu-core/src/main/java/com/hello/suripu/core/processors/FeatureFlippedProcessor.java b/suripu-core/src/main/java/com/hello/suripu/core/processors/FeatureFlippedProcessor.java index a72c42c69..8eb26a153 100644 --- a/suripu-core/src/main/java/com/hello/suripu/core/processors/FeatureFlippedProcessor.java +++ b/suripu-core/src/main/java/com/hello/suripu/core/processors/FeatureFlippedProcessor.java @@ -192,8 +192,8 @@ protected Boolean useSmartAlarmRefactored(final Long accountId){ return featureFlipper.userFeatureActive(FeatureFlipper.SMART_ALARM_REFACTORED, accountId, Collections.EMPTY_LIST); } - protected Boolean hasInterruptionEvent(final Long accountId){ - return featureFlipper.userFeatureActive(FeatureFlipper.TIMELINE_INTERRUPTION_EVENT, accountId, Collections.EMPTY_LIST); + protected Boolean hasSleepDisturbanceEvent(final Long accountId){ + return featureFlipper.userFeatureActive(FeatureFlipper.TIMELINE_SLEEP_DISTURBANCE_EVENT, accountId, Collections.EMPTY_LIST); } } diff --git a/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java b/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java index 83f75dfd9..8ee090d98 100644 --- a/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java +++ b/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java @@ -478,6 +478,30 @@ public List smoothEvents(final List eventList){ return result; } + public List cleanEventWindow(final List eventList){ + if(eventList.size() == 0){ + return eventList; + } + + final ArrayList result = new ArrayList<>(); + long distrubanceStartTime = 0L; + long distrubanceEndTime = 0L; + + for(final Event event:eventList) { + if (event.getType() == Event.Type.SLEEP_DISTURBANCE) { + distrubanceStartTime = event.getStartTimestamp(); + distrubanceEndTime = event.getEndTimestamp(); + + } + if ((event.getStartTimestamp() >= distrubanceStartTime && event.getStartTimestamp() <= distrubanceEndTime) && (event.getType() == Event.Type.MOTION || event.getType() == Event.Type.SLEEP_MOTION || event.getType()==Event.Type.SLEEPING)) { + continue; + } else { + result.add(event); + } + } + return result; + } + public List generateAlignedSegmentsByTypeWeight(final List eventList, int slotDurationMS, int mergeSlotCount, boolean collapseNullSegments){ @@ -660,7 +684,7 @@ public Integer normalizeSleepDepth(final int value, int maxValue) { * @param segments * @return */ - public static SleepStats computeStats(final List segments, final List trackerMotions, final int lightSleepThreshold, final boolean hasMediumSleep, final boolean useUninterruptedDuration) { + public static SleepStats computeStats(final List segments, final List trackerMotions, final int lightSleepThreshold, final boolean useUninterruptedDuration, final boolean useSleepDisturbance) { Integer soundSleepDurationInSecs = 0; Integer mediumSleepDurationInSecs = 0; Integer lightSleepDurationInSecs = 0; @@ -718,24 +742,22 @@ public static SleepStats computeStats(final List segments, final L final int sleepDepth = segment.getSleepDepth(); final int duration = segment.getDurationInSeconds(); - if (hasMediumSleep) { - if (sleepDepth >= SleepStats.MEDIUM_SLEEP_DEPTH_THRESHOLD) { - soundSleepDurationInSecs += duration; - } else if (sleepDepth >= SleepStats.LIGHT_SLEEP_DEPTH_THRESHOLD) { - mediumSleepDurationInSecs += duration; - } else if (sleepDepth >= SleepStats.AWAKE_SLEEP_DEPTH_THRESHOLD) { - lightSleepDurationInSecs += duration; - } - } else { - if (sleepDepth >= lightSleepThreshold) { - soundSleepDurationInSecs += duration; - } else if (sleepDepth >= 0 && sleepDepth < lightSleepThreshold) { - lightSleepDurationInSecs += duration; - } + if (sleepDepth >= SleepStats.MEDIUM_SLEEP_DEPTH_THRESHOLD) { + soundSleepDurationInSecs += duration; + } else if (sleepDepth >= SleepStats.LIGHT_SLEEP_DEPTH_THRESHOLD) { + mediumSleepDurationInSecs += duration; + } else if (sleepDepth >= SleepStats.AWAKE_SLEEP_DEPTH_THRESHOLD) { + lightSleepDurationInSecs += duration; } - if(segment.getType() == Event.Type.MOTION){ - numberOfMotionEvents++; + if(useSleepDisturbance){ + if(segment.getType()== Event.Type.SLEEP_DISTURBANCE){ + numberOfMotionEvents++; + } + }else { + if (segment.getType() == Event.Type.MOTION) { + numberOfMotionEvents++; + } } //LOGGER.trace("duration in seconds = {}", segment.getDurationInSeconds()); } @@ -1483,14 +1505,15 @@ public HashMap getSleepDisturbances(final List tracke // computes periods of agitated sleep using on duration. Over 16 seconds of movement within a two minute window initiates a state of agitated sleep that persists until there is a 4 minute window with no motion final int onDurationSumThreshold = 10; //secs - final long noMotionThreshold = DateTimeConstants.MILLIS_PER_MINUTE * 10; + final long noMotionThreshold = DateTimeConstants.MILLIS_PER_MINUTE * 5; + final long minInterDisturbanceInterval = DateTimeConstants.MILLIS_PER_MINUTE * 45; final long timeWindow = DateTimeConstants.MILLIS_PER_MINUTE * 4; //4 minutes - finds consecutive minutes with some flexibility final int maxDisturbanceCount = 5; //max number of sleep disturbances to report during night long currentTS = 0L; long currentDisturbanceStartTS ; long currentDisturbanceEndTS ; long previousDisturbanceStartTS = 0L; - long previousDisturbanceEndTS; + long previousDisturbanceEndTS = 0L; long previousMotionTS = 0L; List trackerMotionWindowCurrent = new ArrayList<>(); @@ -1519,7 +1542,7 @@ public HashMap getSleepDisturbances(final List tracke } } if (onDurationSum > onDurationSumThreshold){ - if (!currentlyDisturbed) { + if (!currentlyDisturbed && currentDisturbanceStartTS - previousDisturbanceEndTS> minInterDisturbanceInterval) { currentlyDisturbed = true; if (!sleepDisturbances.containsKey(currentDisturbanceStartTS)) { sleepDisturbances.put(currentDisturbanceStartTS, onDurationSum); diff --git a/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java b/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java index b9b191b9a..b5a5f13c0 100644 --- a/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java +++ b/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java @@ -88,7 +88,7 @@ public void testComputeStats() { final List greyEvents = timelineUtils.greyNullEventsOutsideBedPeriod(cleanedUpEvents, sleep, wake); final List nonSignificantFilteredEvents = timelineUtils.removeEventBeforeSignificant(greyEvents); final List sleepSegments = timelineUtils.eventsToSegments(nonSignificantFilteredEvents); - final SleepStats sleepStats = TimelineUtils.computeStats(sleepSegments, trackerMotions, 70, true, true); + final SleepStats sleepStats = TimelineUtils.computeStats(sleepSegments, trackerMotions, 70, true, false); final Integer uninterruptedDuration = 380; assertThat(sleepStats.uninterruptedSleepDurationInMinutes, is(uninterruptedDuration)); } @@ -139,7 +139,7 @@ public void testGetSleepDisturbances() { } span.add(index_end - index_start); } - final Integer[] startMinutesActual = {315,390,495,585, 735}; + final Integer[] startMinutesActual = {315,390,495,585}; assertThat(startMinutesActual.length == startMinutes.size(), is(true)); for (Integer startMinute : startMinutes){ assertThat(startMinutes.contains(startMinute), is(true)); diff --git a/suripu-coredropwizard/src/main/java/com/hello/suripu/coredropwizard/timeline/InstrumentedTimelineProcessor.java b/suripu-coredropwizard/src/main/java/com/hello/suripu/coredropwizard/timeline/InstrumentedTimelineProcessor.java index 5e2713597..0efac2717 100644 --- a/suripu-coredropwizard/src/main/java/com/hello/suripu/coredropwizard/timeline/InstrumentedTimelineProcessor.java +++ b/suripu-coredropwizard/src/main/java/com/hello/suripu/coredropwizard/timeline/InstrumentedTimelineProcessor.java @@ -697,10 +697,14 @@ public PopulatedTimelines populateTimeline(final long accountId,final DateTime d } } - if (hasInterruptionEvent(accountId)){ + if (hasSleepDisturbanceEvent(accountId)){ if (sleepTime.isPresent() && wakeTime.isPresent()) { final List sleepDisturbanceEvents = timelineUtils.getSleepDisturbanceEvents(sensorData.oneDaysTrackerMotion, sleepTime.get(), wakeTime.get(), timeZoneOffsetMap); for(final Event sleepDisturbanceEvent : sleepDisturbanceEvents){ + LOGGER.info("action=get-sleep-disturbance account_id={} night={} start-time={} end-time={} offset={}", accountId, targetDate, sleepDisturbanceEvent.getStartTimestamp(), sleepDisturbanceEvent.getEndTimestamp(), sleepDisturbanceEvent.getTimezoneOffset()); + if (timelineEvents.containsKey(sleepDisturbanceEvent.getStartTimestamp())){ + LOGGER.info("action=multiple-events-during-same-window"); + } timelineEvents.put(sleepDisturbanceEvent.getStartTimestamp(), sleepDisturbanceEvent); } } @@ -740,14 +744,16 @@ public PopulatedTimelines populateTimeline(final long accountId,final DateTime d final List nonSignificantFilteredEvents = timelineUtils.removeEventBeforeSignificant(greyEvents); + final List filteredEvents = timelineUtils.cleanEventWindow(nonSignificantFilteredEvents); + /* convert valid events to segment, compute sleep stats and score */ - final List sleepSegments = timelineUtils.eventsToSegments(nonSignificantFilteredEvents); + final List sleepSegments = timelineUtils.eventsToSegments(filteredEvents); final int lightSleepThreshold = 70; // TODO: Generate dynamically instead of hard threshold final boolean useUninterruptedDuration = useUninterruptedDuration(accountId); - final SleepStats sleepStats = timelineUtils.computeStats(sleepSegments, trackerMotions, lightSleepThreshold, hasSleepStatMediumSleep(accountId), useUninterruptedDuration); + final SleepStats sleepStats = timelineUtils.computeStats(sleepSegments, trackerMotions, lightSleepThreshold, useUninterruptedDuration, hasSleepDisturbanceEvent(accountId)); final List reversed = Lists.reverse(sleepSegments); Integer sleepScore = computeAndMaybeSaveScore(sensorData.oneDaysTrackerMotion.processedtrackerMotions, sensorData.oneDaysTrackerMotion.filteredOriginalTrackerMotions, numSoundEvents, allSensorSampleList, targetDate, accountId, sleepStats); From e3fae38cad08465defb60e58a49bae0a4bd6eb29 Mon Sep 17 00:00:00 2001 From: Jarred Heinrich Date: Fri, 27 Jan 2017 16:51:21 -0800 Subject: [PATCH 07/10] cleaned up sleeping events and sleeping event sleepdepth behind FF --- .../hello/suripu/core/util/TimelineUtils.java | 44 ++++++++++++------- .../suripu/core/util/TimelineUtilsTest.java | 4 +- .../InstrumentedTimelineProcessor.java | 13 +++--- 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java b/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java index 8ee090d98..38ed3ecf6 100644 --- a/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java +++ b/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java @@ -163,7 +163,11 @@ public List removeNegativeAmplitudes(final List tr return positiveMotions; } - public List generateMotionEvents(final List trackerMotions) { + public List generateMotionEvents(final List trackerMotions, final boolean hasSleepDisturbance) { + int minSleepDepth = 0; + if (hasSleepDisturbance){ + minSleepDepth = 5; + } final List motionEvents = new ArrayList<>(); final List positiveMotions = removeNegativeAmplitudes(trackerMotions); @@ -183,7 +187,7 @@ public List generateMotionEvents(final List trackerM trackerMotion.timestamp, trackerMotion.timestamp + DateTimeConstants.MILLIS_PER_MINUTE, trackerMotion.offsetMillis, - getSleepDepth(trackerMotion.value, positionMap, maxSVM)); + Math.max(getSleepDepth(trackerMotion.value, positionMap, maxSVM), minSleepDepth)); motionEvents.add(motionEvent); } LOGGER.debug("Generated {} segments from {} tracker motion samples", motionEvents.size(), trackerMotions.size()); @@ -479,25 +483,32 @@ public List smoothEvents(final List eventList){ } public List cleanEventWindow(final List eventList){ + final long disturbanceBlackOut = DateTimeConstants.MILLIS_PER_MINUTE * 20; if(eventList.size() == 0){ return eventList; } final ArrayList result = new ArrayList<>(); - long distrubanceStartTime = 0L; - long distrubanceEndTime = 0L; + long disturbanceStartTime = -1L; + long disturbanceEndTime = -1L; - for(final Event event:eventList) { + for(Event event:eventList) { if (event.getType() == Event.Type.SLEEP_DISTURBANCE) { - distrubanceStartTime = event.getStartTimestamp(); - distrubanceEndTime = event.getEndTimestamp(); - + disturbanceStartTime = event.getStartTimestamp(); + disturbanceEndTime = event.getEndTimestamp(); + result.add(new SleepDisturbanceEvent(event.getStartTimestamp(), event.getStartTimestamp() + DateTimeConstants.MILLIS_PER_MINUTE, event.getTimezoneOffset(), event.getSleepDepth())); + result.add(new SleepingEvent(event.getStartTimestamp(), event.getEndTimestamp(), event.getTimezoneOffset(), 0)); + continue; } - if ((event.getStartTimestamp() >= distrubanceStartTime && event.getStartTimestamp() <= distrubanceEndTime) && (event.getType() == Event.Type.MOTION || event.getType() == Event.Type.SLEEP_MOTION || event.getType()==Event.Type.SLEEPING)) { + if ((event.getStartTimestamp() >= disturbanceStartTime && event.getStartTimestamp() <= disturbanceEndTime + disturbanceBlackOut) && (event.getType() == Event.Type.MOTION || event.getType() == Event.Type.SLEEP_MOTION)){ continue; - } else { - result.add(event); + } else if ((event.getStartTimestamp() >= disturbanceStartTime && event.getEndTimestamp() <= disturbanceEndTime) && (event.getType() == Event.Type.SLEEPING )) { + continue; + }else if ((event.getStartTimestamp() >= disturbanceStartTime && event.getStartTimestamp() < disturbanceEndTime && event.getEndTimestamp() > disturbanceEndTime) && (event.getType() == Event.Type.SLEEPING )) { + event = new SleepingEvent(disturbanceEndTime, event.getEndTimestamp(), event.getTimezoneOffset(), event.getSleepDepth()); } + result.add(event); + } return result; } @@ -1504,11 +1515,12 @@ public List getSleepDisturbanceEvents(final OneDaysTrackerMotion oneDaysT public HashMap getSleepDisturbances(final List trackerMotions){ // computes periods of agitated sleep using on duration. Over 16 seconds of movement within a two minute window initiates a state of agitated sleep that persists until there is a 4 minute window with no motion - final int onDurationSumThreshold = 10; //secs - final long noMotionThreshold = DateTimeConstants.MILLIS_PER_MINUTE * 5; - final long minInterDisturbanceInterval = DateTimeConstants.MILLIS_PER_MINUTE * 45; + final int onDurationSumThreshold = 15; //secs + final long noMotionThreshold = DateTimeConstants.MILLIS_PER_MINUTE * 7; + final long minInterDisturbanceInterval = DateTimeConstants.MILLIS_PER_MINUTE * 30; final long timeWindow = DateTimeConstants.MILLIS_PER_MINUTE * 4; //4 minutes - finds consecutive minutes with some flexibility final int maxDisturbanceCount = 5; //max number of sleep disturbances to report during night + final int minDisturbanceLength = DateTimeConstants.MILLIS_PER_MINUTE * 4; long currentTS = 0L; long currentDisturbanceStartTS ; long currentDisturbanceEndTS ; @@ -1552,7 +1564,7 @@ public HashMap getSleepDisturbances(final List tracke previousDisturbanceEndTS = currentDisturbanceEndTS; sleepDisturbanceWindows.put(previousDisturbanceStartTS, previousDisturbanceEndTS); }else if (currentTS - previousMotionTS > noMotionThreshold && currentlyDisturbed){ - sleepDisturbanceWindows.put(previousDisturbanceStartTS, previousMotionTS); + sleepDisturbanceWindows.put(previousDisturbanceStartTS, Math.max(previousMotionTS, previousDisturbanceStartTS + minDisturbanceLength)); currentlyDisturbed = false; } @@ -1569,7 +1581,7 @@ public HashMap getSleepDisturbances(final List tracke final int minDisturbanceSpan = disturbanceSpan.get(discardCount); int removedCount = 0; for (long tsKey : tsKeys){ - if ((sleepDisturbanceWindows.get(tsKey) - tsKey)/DateTimeConstants.MILLIS_PER_MINUTE < minDisturbanceSpan){ + if ((sleepDisturbanceWindows.get(tsKey) - tsKey)/DateTimeConstants.MILLIS_PER_MINUTE <= minDisturbanceSpan){ sleepDisturbanceWindows.remove(tsKey); removedCount +=1; } diff --git a/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java b/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java index b5a5f13c0..b02851958 100644 --- a/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java +++ b/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java @@ -59,7 +59,7 @@ private List loadTrackerMotionFromCSV(final String resource){ public void testComputeStats() { //false positive night - motion at start and end of night final List trackerMotions = loadTrackerMotionFromCSV("fixtures/algorithm/millionaires_challenge_2015_02_20_raw.csv"); - final List motionEvents = timelineUtils.generateMotionEvents(trackerMotions); + final List motionEvents = timelineUtils.generateMotionEvents(trackerMotions, false); final long sleepTime = trackerMotions.get(0).timestamp; final long wakeTime = trackerMotions.get(0).timestamp + 24000000L; final TimeZoneHistory timeZoneHistory1 = new TimeZoneHistory(1428408400000L, 3600000, "America/Los_Angeles"); @@ -139,7 +139,7 @@ public void testGetSleepDisturbances() { } span.add(index_end - index_start); } - final Integer[] startMinutesActual = {315,390,495,585}; + final Integer[] startMinutesActual = {315,404,585,634}; assertThat(startMinutesActual.length == startMinutes.size(), is(true)); for (Integer startMinute : startMinutes){ assertThat(startMinutes.contains(startMinute), is(true)); diff --git a/suripu-coredropwizard/src/main/java/com/hello/suripu/coredropwizard/timeline/InstrumentedTimelineProcessor.java b/suripu-coredropwizard/src/main/java/com/hello/suripu/coredropwizard/timeline/InstrumentedTimelineProcessor.java index 0efac2717..d2d823881 100644 --- a/suripu-coredropwizard/src/main/java/com/hello/suripu/coredropwizard/timeline/InstrumentedTimelineProcessor.java +++ b/suripu-coredropwizard/src/main/java/com/hello/suripu/coredropwizard/timeline/InstrumentedTimelineProcessor.java @@ -11,6 +11,7 @@ import com.hello.suripu.core.algorithmintegration.AlgorithmFactory; import com.hello.suripu.core.algorithmintegration.NeuralNetEndpoint; import com.hello.suripu.core.algorithmintegration.OneDaysSensorData; +import com.hello.suripu.core.algorithmintegration.OneDaysTrackerMotion; import com.hello.suripu.core.algorithmintegration.TimelineAlgorithm; import com.hello.suripu.core.algorithmintegration.TimelineAlgorithmResult; import com.hello.suripu.core.db.AccountReadDAO; @@ -53,7 +54,6 @@ import com.hello.suripu.core.models.TimelineResult; import com.hello.suripu.core.models.TrackerMotion; import com.hello.suripu.core.models.UserBioInfo; -import com.hello.suripu.core.algorithmintegration.OneDaysTrackerMotion; import com.hello.suripu.core.models.timeline.v2.TimelineLog; import com.hello.suripu.core.processors.FeatureFlippedProcessor; import com.hello.suripu.core.processors.PartnerMotion; @@ -615,7 +615,7 @@ public PopulatedTimelines populateTimeline(final long accountId,final DateTime d Optional outOfBed= Optional.fromNullable(reprocessedEvents.mainEvents.get(Event.Type.OUT_OF_BED)); //CREATE SLEEP MOTION EVENTS - final List motionEvents = timelineUtils.generateMotionEvents(trackerMotions); + final List motionEvents = timelineUtils.generateMotionEvents(trackerMotions, hasSleepDisturbanceEvent(accountId)); final Map timelineEvents = TimelineRefactored.populateTimeline(motionEvents, timeZoneOffsetMap); @@ -661,7 +661,7 @@ public PopulatedTimelines populateTimeline(final long accountId,final DateTime d LOGGER.debug("action=get-sound-events account_id={} use_higher_threshold={}", accountId, this.useHigherThesholdForSoundEvents(accountId)); final List soundEvents = getSoundEvents( - allSensorSampleList.get(Sensor.SOUND_PEAK_ENERGY), + allSensorSampleList.get(Sensor.SOUND_PEAK_DISTURBANCE), motionEvents, lightOutTimeOptional, sleepEventsFromAlgorithm, @@ -703,7 +703,10 @@ public PopulatedTimelines populateTimeline(final long accountId,final DateTime d for(final Event sleepDisturbanceEvent : sleepDisturbanceEvents){ LOGGER.info("action=get-sleep-disturbance account_id={} night={} start-time={} end-time={} offset={}", accountId, targetDate, sleepDisturbanceEvent.getStartTimestamp(), sleepDisturbanceEvent.getEndTimestamp(), sleepDisturbanceEvent.getTimezoneOffset()); if (timelineEvents.containsKey(sleepDisturbanceEvent.getStartTimestamp())){ - LOGGER.info("action=multiple-events-during-same-window"); + final Event.Type otherEventType = timelineEvents.get(sleepDisturbanceEvent.getStartTimestamp()).getType(); + if (otherEventType != Event.Type.NONE && otherEventType != Event.Type.MOTION) { + LOGGER.info("action=multiple-events-during-same-window account-id={} date-of-night={} event1={} event2=SLEEP_DISTURBANCE event-time={}", accountId,targetDate, otherEventType.name(), sleepDisturbanceEvent.getStartTimestamp()); + } } timelineEvents.put(sleepDisturbanceEvent.getStartTimestamp(), sleepDisturbanceEvent); } @@ -913,7 +916,7 @@ private List getPartnerMotionEvents(final Optional fa if (partnerMotionsWithinSleepBounds.size() > 0) { // use un-normalized data segments for comparison //tz offset should be correct - final List partnerMotionEvents = timelineUtils.generateMotionEvents(partnerMotionsWithinSleepBounds); + final List partnerMotionEvents = timelineUtils.generateMotionEvents(partnerMotionsWithinSleepBounds, false); return PartnerMotion.getPartnerData(partnerMotionEvents,motionEvents, 0); } From 71750e2c89141df355ed45c873aa4f55910b8835 Mon Sep 17 00:00:00 2001 From: Jarred Heinrich Date: Fri, 27 Jan 2017 17:00:25 -0800 Subject: [PATCH 08/10] reverted sound event sensor source --- .../coredropwizard/timeline/InstrumentedTimelineProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suripu-coredropwizard/src/main/java/com/hello/suripu/coredropwizard/timeline/InstrumentedTimelineProcessor.java b/suripu-coredropwizard/src/main/java/com/hello/suripu/coredropwizard/timeline/InstrumentedTimelineProcessor.java index d2d823881..dddaac92d 100644 --- a/suripu-coredropwizard/src/main/java/com/hello/suripu/coredropwizard/timeline/InstrumentedTimelineProcessor.java +++ b/suripu-coredropwizard/src/main/java/com/hello/suripu/coredropwizard/timeline/InstrumentedTimelineProcessor.java @@ -661,7 +661,7 @@ public PopulatedTimelines populateTimeline(final long accountId,final DateTime d LOGGER.debug("action=get-sound-events account_id={} use_higher_threshold={}", accountId, this.useHigherThesholdForSoundEvents(accountId)); final List soundEvents = getSoundEvents( - allSensorSampleList.get(Sensor.SOUND_PEAK_DISTURBANCE), + allSensorSampleList.get(Sensor.SOUND_PEAK_ENERGY), motionEvents, lightOutTimeOptional, sleepEventsFromAlgorithm, From 5c1dc722f06e8bca6d2de3858da53327d5789876 Mon Sep 17 00:00:00 2001 From: Jarred Heinrich Date: Thu, 2 Feb 2017 11:00:57 -0800 Subject: [PATCH 09/10] cleaned up logic, added comments --- .../hello/suripu/core/util/TimelineUtils.java | 79 ++++++++++--------- .../suripu/core/util/TimelineUtilsTest.java | 3 +- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java b/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java index 38ed3ecf6..f8b46d9f4 100644 --- a/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java +++ b/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java @@ -1493,15 +1493,15 @@ public List getAlarmEvents(final List ringTimes, final DateTime } public List getSleepDisturbanceEvents(final OneDaysTrackerMotion oneDaysTrackerMotion, final Long sleepTime, final Long wakeTime, final TimeZoneOffsetMap timeZoneOffsetMap){ - final long sleepBuffer = 30 * DateTimeConstants.MILLIS_PER_MINUTE; - final long wakeBuffer= 60 * DateTimeConstants.MILLIS_PER_MINUTE; + final long sleepBuffer = 30 * DateTimeConstants.MILLIS_PER_MINUTE; // ignores first 30 minutes of sleep motions + final long wakeBuffer= 60 * DateTimeConstants.MILLIS_PER_MINUTE; //ignores last 60 minutes of sleep motion List trackerMotions = new ArrayList<>(); for(final TrackerMotion trackerMotion : oneDaysTrackerMotion.originalTrackerMotions){ if (trackerMotion.timestamp > sleepTime + sleepBuffer && trackerMotion.timestamp < wakeTime - wakeBuffer){ trackerMotions.add(trackerMotion); } } - final HashMap sleepDisturbanceTimeStamps= getSleepDisturbances(trackerMotions); + final Map sleepDisturbanceTimeStamps= getSleepDisturbances(trackerMotions); final List sleepDisturbanceEvents = new ArrayList<>(); for (final long sleepDisturbanceStartTS : sleepDisturbanceTimeStamps.keySet()){ final SleepDisturbanceEvent sleepDisturbanceEvent = new SleepDisturbanceEvent(sleepDisturbanceStartTS, sleepDisturbanceTimeStamps.get(sleepDisturbanceStartTS), timeZoneOffsetMap.getOffsetWithDefaultAsZero(sleepDisturbanceStartTS), 0); @@ -1511,84 +1511,87 @@ public List getSleepDisturbanceEvents(final OneDaysTrackerMotion oneDaysT return sleepDisturbanceEvents; } - //Finds intances where the pill recorded 15seconds + of motion within a 4 minute window - public HashMap getSleepDisturbances(final List trackerMotions){ + // computes periods of disturbed sleep using on duration. Over 16 seconds of movement within a 4 minute window initiates a state of disturbed sleep that persists until there is no motion for 7 minutes + public Map getSleepDisturbances(final List trackerMotions){ - // computes periods of agitated sleep using on duration. Over 16 seconds of movement within a two minute window initiates a state of agitated sleep that persists until there is a 4 minute window with no motion final int onDurationSumThreshold = 15; //secs final long noMotionThreshold = DateTimeConstants.MILLIS_PER_MINUTE * 7; final long minInterDisturbanceInterval = DateTimeConstants.MILLIS_PER_MINUTE * 30; final long timeWindow = DateTimeConstants.MILLIS_PER_MINUTE * 4; //4 minutes - finds consecutive minutes with some flexibility final int maxDisturbanceCount = 5; //max number of sleep disturbances to report during night final int minDisturbanceLength = DateTimeConstants.MILLIS_PER_MINUTE * 4; - long currentTS = 0L; - long currentDisturbanceStartTS ; - long currentDisturbanceEndTS ; + + long currentMotionTS = 0L; + long previousMotionTS; + long currentDisturbanceStartTS; + long currentDisturbanceEndTS; long previousDisturbanceStartTS = 0L; long previousDisturbanceEndTS = 0L; - long previousMotionTS = 0L; + final Map sleepDisturbances = new HashMap<>(); List trackerMotionWindowCurrent = new ArrayList<>(); - final HashMap sleepDisturbances = new HashMap<>(); - final HashMap sleepDisturbanceWindows = new HashMap<>(); boolean currentlyDisturbed = false; - for (TrackerMotion trackerMotionCurrent : trackerMotions) { + for (final TrackerMotion trackerMotionCurrent : trackerMotions) { final List trackerMotionWindowPrevious = trackerMotionWindowCurrent; - trackerMotionWindowCurrent = new ArrayList<>(); + trackerMotionWindowCurrent = new ArrayList<>(); //list of tracker motions within time window of current motion event. trackerMotionWindowCurrent.add(trackerMotionCurrent); - previousMotionTS = currentTS; - currentTS = trackerMotionCurrent.timestamp; - currentDisturbanceStartTS = currentTS; + previousMotionTS = currentMotionTS; + currentMotionTS = trackerMotionCurrent.timestamp; + currentDisturbanceStartTS = currentMotionTS; + currentDisturbanceEndTS = currentMotionTS; - currentDisturbanceEndTS = currentTS; + //find all motion events in window of interest. int onDurationSum = trackerMotionCurrent.onDurationInSeconds.intValue(); if (!trackerMotionWindowPrevious.isEmpty()) { + //look at trackerMotions in previous window, if previous motions within current time window, add them to current motion window for (final TrackerMotion trackerMotionPrevious : trackerMotionWindowPrevious) { - if (currentTS - trackerMotionPrevious.timestamp <= timeWindow) { + if (currentMotionTS - trackerMotionPrevious.timestamp <= timeWindow) { trackerMotionWindowCurrent.add(trackerMotionPrevious); onDurationSum += trackerMotionPrevious.onDurationInSeconds.intValue(); + //find start and end time of possible disturbance currentDisturbanceEndTS = Math.max(currentDisturbanceEndTS, trackerMotionPrevious.timestamp); currentDisturbanceStartTS = Math.min(currentDisturbanceStartTS, trackerMotionPrevious.timestamp); } } } - if (onDurationSum > onDurationSumThreshold){ - if (!currentlyDisturbed && currentDisturbanceStartTS - previousDisturbanceEndTS> minInterDisturbanceInterval) { - currentlyDisturbed = true; - if (!sleepDisturbances.containsKey(currentDisturbanceStartTS)) { - sleepDisturbances.put(currentDisturbanceStartTS, onDurationSum); - previousDisturbanceStartTS = currentDisturbanceStartTS; - } + + //check if previous sleep disturbance is expired + if (currentMotionTS - previousMotionTS > noMotionThreshold && currentlyDisturbed) { + sleepDisturbances.put(previousDisturbanceStartTS, Math.max(previousMotionTS, previousDisturbanceStartTS + minDisturbanceLength)); + currentlyDisturbed = false; + } + + //check if current window has a sleep disturbance + if (onDurationSum > onDurationSumThreshold){ //is it a disturbance + if (!currentlyDisturbed && currentDisturbanceStartTS - previousDisturbanceEndTS> minInterDisturbanceInterval) { //is it a new disturbance at least 30 minutes after the previous disturbance + currentlyDisturbed = true; //change disturbance state + previousDisturbanceStartTS = currentDisturbanceStartTS; } previousDisturbanceEndTS = currentDisturbanceEndTS; - sleepDisturbanceWindows.put(previousDisturbanceStartTS, previousDisturbanceEndTS); - }else if (currentTS - previousMotionTS > noMotionThreshold && currentlyDisturbed){ - sleepDisturbanceWindows.put(previousDisturbanceStartTS, Math.max(previousMotionTS, previousDisturbanceStartTS + minDisturbanceLength)); - currentlyDisturbed = false; + sleepDisturbances.put(previousDisturbanceStartTS, previousDisturbanceEndTS); } } + // Limits the number of sleep disturbance based on length of disturbance. if (sleepDisturbances.size() > maxDisturbanceCount){ final int discardCount = sleepDisturbances.size() - maxDisturbanceCount; final List tsKeys = new ArrayList<>(sleepDisturbances.keySet()); final List disturbanceSpan = new ArrayList<>(); - for (final long sleepDisturbanceStart : sleepDisturbanceWindows.keySet()){ - disturbanceSpan.add((int) (sleepDisturbanceWindows.get(sleepDisturbanceStart) - sleepDisturbanceStart)/ DateTimeConstants.MILLIS_PER_MINUTE); + for (final long sleepDisturbanceStart : sleepDisturbances.keySet()){ + disturbanceSpan.add((int) (sleepDisturbances.get(sleepDisturbanceStart) - sleepDisturbanceStart)/ DateTimeConstants.MILLIS_PER_MINUTE); } Collections.sort(disturbanceSpan); final int minDisturbanceSpan = disturbanceSpan.get(discardCount); - int removedCount = 0; - for (long tsKey : tsKeys){ - if ((sleepDisturbanceWindows.get(tsKey) - tsKey)/DateTimeConstants.MILLIS_PER_MINUTE <= minDisturbanceSpan){ - sleepDisturbanceWindows.remove(tsKey); - removedCount +=1; + for (final long tsKey : tsKeys){ + if ((sleepDisturbances.get(tsKey) - tsKey)/DateTimeConstants.MILLIS_PER_MINUTE <= minDisturbanceSpan){ + sleepDisturbances.remove(tsKey); } } } //possible for two sleep disturbances to have the same onDurationSum, in this case it is possible to have > 5 sleepDisturbances; - return sleepDisturbanceWindows; + return sleepDisturbances; } diff --git a/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java b/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java index b02851958..8c00565fd 100644 --- a/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java +++ b/suripu-core/src/test/java/com/hello/suripu/core/util/TimelineUtilsTest.java @@ -18,7 +18,6 @@ import java.io.IOException; import java.net.URL; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -114,7 +113,7 @@ public void testMotionDuringSleepCheck(){ public void testGetSleepDisturbances() { List trackerMotions = CSVLoader.loadTrackerMotionFromCSV("fixtures/tracker_motion/nn_raw_tracker_motion.csv"); trackerMotions = Lists.reverse(trackerMotions); - final HashMap sleepDisturbances = timelineUtils.getSleepDisturbances(trackerMotions); + final Map sleepDisturbances = timelineUtils.getSleepDisturbances(trackerMotions); final List startMinutes = new ArrayList<>(); final List endMinutes = new ArrayList<>(); final List span = new ArrayList<>(); From c7629e40395b5da8fa901480e9aa84ec0d9673a8 Mon Sep 17 00:00:00 2001 From: Jarred Heinrich Date: Fri, 10 Feb 2017 15:21:57 -0800 Subject: [PATCH 10/10] added comments --- .../java/com/hello/suripu/core/util/TimelineUtils.java | 7 ++++++- .../timeline/InstrumentedTimelineProcessor.java | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java b/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java index f8b46d9f4..a1c4690db 100644 --- a/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java +++ b/suripu-core/src/main/java/com/hello/suripu/core/util/TimelineUtils.java @@ -165,6 +165,9 @@ public List removeNegativeAmplitudes(final List tr public List generateMotionEvents(final List trackerMotions, final boolean hasSleepDisturbance) { int minSleepDepth = 0; + //sleepDepth < 5 = "awake" for sleep motion, but is not reflected in sleep stats times awake. + //Users can thumb over sleep motion that is < 5 and see a message of 'awake' + //setting minSleepDepth to 5 removes this inconsistency. if (hasSleepDisturbance){ minSleepDepth = 5; } @@ -482,6 +485,7 @@ public List smoothEvents(final List eventList){ return result; } + //removes sleep motion events during a sleep disturbance public List cleanEventWindow(final List eventList){ final long disturbanceBlackOut = DateTimeConstants.MILLIS_PER_MINUTE * 20; if(eventList.size() == 0){ @@ -1492,6 +1496,7 @@ public List getAlarmEvents(final List ringTimes, final DateTime return events; } + //Searches for instances where a user moves for more than 15 seconds within a 4 minute period public List getSleepDisturbanceEvents(final OneDaysTrackerMotion oneDaysTrackerMotion, final Long sleepTime, final Long wakeTime, final TimeZoneOffsetMap timeZoneOffsetMap){ final long sleepBuffer = 30 * DateTimeConstants.MILLIS_PER_MINUTE; // ignores first 30 minutes of sleep motions final long wakeBuffer= 60 * DateTimeConstants.MILLIS_PER_MINUTE; //ignores last 60 minutes of sleep motion @@ -1511,7 +1516,7 @@ public List getSleepDisturbanceEvents(final OneDaysTrackerMotion oneDaysT return sleepDisturbanceEvents; } - // computes periods of disturbed sleep using on duration. Over 16 seconds of movement within a 4 minute window initiates a state of disturbed sleep that persists until there is no motion for 7 minutes + // computes periods of disturbed sleep using on duration. Over 15 seconds of movement within a 4 minute window initiates a state of disturbed sleep that persists until there is no motion for 7 minutes public Map getSleepDisturbances(final List trackerMotions){ final int onDurationSumThreshold = 15; //secs diff --git a/suripu-coredropwizard/src/main/java/com/hello/suripu/coredropwizard/timeline/InstrumentedTimelineProcessor.java b/suripu-coredropwizard/src/main/java/com/hello/suripu/coredropwizard/timeline/InstrumentedTimelineProcessor.java index dddaac92d..521f74213 100644 --- a/suripu-coredropwizard/src/main/java/com/hello/suripu/coredropwizard/timeline/InstrumentedTimelineProcessor.java +++ b/suripu-coredropwizard/src/main/java/com/hello/suripu/coredropwizard/timeline/InstrumentedTimelineProcessor.java @@ -747,6 +747,7 @@ public PopulatedTimelines populateTimeline(final long accountId,final DateTime d final List nonSignificantFilteredEvents = timelineUtils.removeEventBeforeSignificant(greyEvents); + //removes sleep motion events during a sleep disturbance final List filteredEvents = timelineUtils.cleanEventWindow(nonSignificantFilteredEvents); /* convert valid events to segment, compute sleep stats and score */