Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new sleep disturbance event #1861

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
16 changes: 14 additions & 2 deletions suripu-core/src/main/java/com/hello/suripu/core/models/Event.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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());

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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.");
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't change the enum values if you can avoid it.



private static EventType[] cachedValues = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ public static List<TimelineEvent> fromV1(final List<SleepSegment> 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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public static List<ValidAction> from(Event.Type type) {
case NOISE:
case SNORING:
case SLEEP_TALK:
case SLEEP_DISTURBANCE:
return Lists.newArrayList(VERIFY, INCORRECT);
default:
return Lists.newArrayList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has this been approved by product/copy/james?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked it with copy/product. Consulting with Matt Walker following James' advices

public final static String NULL_MESSAGE = "";



/* END Events Declaration */


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -1457,6 +1459,101 @@ public List<Event> getAlarmEvents(final List<RingTime> ringTimes, final DateTime
return events;
}

public List<Event> getSleepDisturbanceEvents(final OneDaysTrackerMotion oneDaysTrackerMotion, final Long sleepTime, final Long wakeTime, final TimeZoneOffsetMap timeZoneOffsetMap){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

describe algo briefly in comments

final long sleepBuffer = 30 * DateTimeConstants.MILLIS_PER_MINUTE;
final long wakeBuffer= 60 * DateTimeConstants.MILLIS_PER_MINUTE;
List<TrackerMotion> trackerMotions = new ArrayList<>();
for(final TrackerMotion trackerMotion : oneDaysTrackerMotion.originalTrackerMotions){
if (trackerMotion.timestamp > sleepTime + sleepBuffer && trackerMotion.timestamp < wakeTime - wakeBuffer){
trackerMotions.add(trackerMotion);
}
}
final HashMap<Long, Long> sleepDisturbanceTimeStamps= getSleepDisturbances(trackerMotions);
final List<Event> 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<Long, Long> getSleepDisturbances(final List<TrackerMotion> trackerMotions){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Map<Long, Long> instead of the more specific HashMap


// 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 ;
long currentDisturbanceEndTS ;
long previousDisturbanceStartTS = 0L;
long previousDisturbanceEndTS;
long previousMotionTS = 0L;

List<TrackerMotion> trackerMotionWindowCurrent = new ArrayList<>();
final HashMap<Long,Integer> sleepDisturbances = new HashMap<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

final HashMap<Long, Long> sleepDisturbanceWindows = new HashMap<>();
boolean currentlyDisturbed = false;

for (TrackerMotion trackerMotionCurrent : trackerMotions) {
final List<TrackerMotion> trackerMotionWindowPrevious = trackerMotionWindowCurrent;
trackerMotionWindowCurrent = new ArrayList<>();
trackerMotionWindowCurrent.add(trackerMotionCurrent);
previousMotionTS = currentTS;
currentTS = trackerMotionCurrent.timestamp;
currentDisturbanceStartTS = currentTS;

currentDisturbanceEndTS = currentTS;
int onDurationSum = trackerMotionCurrent.onDurationInSeconds.intValue();
if (!trackerMotionWindowPrevious.isEmpty()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that some of this would benefit from being refactored. It's a little too much of twisted logic to be easily readable.

for (final TrackerMotion trackerMotionPrevious : trackerMotionWindowPrevious) {
if (currentTS - trackerMotionPrevious.timestamp <= timeWindow) {
trackerMotionWindowCurrent.add(trackerMotionPrevious);
onDurationSum += trackerMotionPrevious.onDurationInSeconds.intValue();
currentDisturbanceEndTS = Math.max(currentDisturbanceEndTS, trackerMotionPrevious.timestamp);
currentDisturbanceStartTS = Math.min(currentDisturbanceStartTS, trackerMotionPrevious.timestamp);
}
}
}
if (onDurationSum > onDurationSumThreshold){
if (!currentlyDisturbed) {
currentlyDisturbed = true;
if (!sleepDisturbances.containsKey(currentDisturbanceStartTS)) {
sleepDisturbances.put(currentDisturbanceStartTS, onDurationSum);
previousDisturbanceStartTS = currentDisturbanceStartTS;
}
}
previousDisturbanceEndTS = currentDisturbanceEndTS;
sleepDisturbanceWindows.put(previousDisturbanceStartTS, previousDisturbanceEndTS);
}else if (currentTS - previousMotionTS > noMotionThreshold && currentlyDisturbed){
sleepDisturbanceWindows.put(previousDisturbanceStartTS, previousMotionTS);
currentlyDisturbed = false;
}

}

if (sleepDisturbances.size() > maxDisturbanceCount){
final int discardCount = sleepDisturbances.size() - maxDisturbanceCount;
final List<Long> tsKeys = new ArrayList<>(sleepDisturbances.keySet());
final List<Integer> 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 List<Event> eventsFromOptionalEvents(final List<Optional<Event>> optionalEvents) {
final List<Event> events = Lists.newArrayList();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -10,11 +11,14 @@
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;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -106,4 +110,39 @@ public void testMotionDuringSleepCheck(){

}

@Test
public void testGetSleepDisturbances() {
List<TrackerMotion> trackerMotions = CSVLoader.loadTrackerMotionFromCSV("fixtures/tracker_motion/nn_raw_tracker_motion.csv");
trackerMotions = Lists.reverse(trackerMotions);
final HashMap<Long, Long> sleepDisturbances = timelineUtils.getSleepDisturbances(trackerMotions);
final List<Integer> startMinutes = new ArrayList<>();
final List<Integer> endMinutes = new ArrayList<>();
final List<Integer> 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));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -620,9 +620,13 @@ public PopulatedTimelines populateTimeline(final long accountId,final DateTime d
final Map<Long, Event> timelineEvents = TimelineRefactored.populateTimeline(motionEvents, timeZoneOffsetMap);

Optional<Long> sleepTime = Optional.absent();
Optional<Long> wakeTime = Optional.absent();
if (sleep.isPresent()){
sleepTime = Optional.of(sleep.get().getEndTimestamp());
}
if (wake.isPresent()) {
wakeTime = Optional.of(wake.get().getStartTimestamp());
}

// LIGHT

Expand Down Expand Up @@ -693,6 +697,15 @@ public PopulatedTimelines populateTimeline(final long accountId,final DateTime d
}
}

if (hasInterruptionEvent(accountId)){
if (sleepTime.isPresent() && wakeTime.isPresent()) {
final List<Event> 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);
Expand Down