diff --git a/suripu-algorithm/src/main/java/com/hello/suripu/algorithm/sleep/SleepPeriod.java b/suripu-algorithm/src/main/java/com/hello/suripu/algorithm/sleep/SleepPeriod.java index 0d4719cba..5898ffbd2 100644 --- a/suripu-algorithm/src/main/java/com/hello/suripu/algorithm/sleep/SleepPeriod.java +++ b/suripu-algorithm/src/main/java/com/hello/suripu/algorithm/sleep/SleepPeriod.java @@ -83,10 +83,22 @@ private boolean isAwake(final long startMillis, final long endMillis){ return false; } - public List getAwakePeriods(final boolean debug){ + private double getVote(final long startMillis, final long endMillis){ + + double maxVote = 0; + for(int i = 0; i < this.votes.size(); i++){ + final Pair vote = this.votes.get(i); + if(vote.getFirst() >= startMillis && vote.getFirst() <= endMillis && vote.getSecond() > maxVote){ + maxVote = vote.getSecond(); + } + } + return maxVote; + } + + public List getAwakePeriods(final boolean debug){ long startMillis = 0; long endMillis = 0; - final List result = new ArrayList<>(); + final List result = new ArrayList<>(); for(int i = 0; i < this.votes.size(); i++){ final Pair vote = this.votes.get(i); @@ -97,7 +109,7 @@ public List getAwakePeriods(final boolean debug){ endMillis = vote.getFirst(); }else { if(isAwake(startMillis, endMillis)){ - result.add(new Segment(startMillis, endMillis, this.getOffsetMillis())); + result.add(new VotingSegment(startMillis, endMillis, this.getOffsetMillis(), getVote(startMillis, endMillis))); if(debug){ LOGGER.debug("User awake at {} - {}", new DateTime(startMillis, DateTimeZone.forOffsetMillis(this.getOffsetMillis())), @@ -110,7 +122,7 @@ public List getAwakePeriods(final boolean debug){ } if(isAwake(startMillis, endMillis)){ - result.add(new Segment(startMillis, endMillis, this.getOffsetMillis())); + result.add(new VotingSegment(startMillis, endMillis, this.getOffsetMillis(), getVote(startMillis, endMillis))); if(debug){ LOGGER.debug("User awake at {} - {}", new DateTime(startMillis, DateTimeZone.forOffsetMillis(this.getOffsetMillis())), diff --git a/suripu-algorithm/src/main/java/com/hello/suripu/algorithm/sleep/Vote.java b/suripu-algorithm/src/main/java/com/hello/suripu/algorithm/sleep/Vote.java index 4189e5da2..e86d81a05 100644 --- a/suripu-algorithm/src/main/java/com/hello/suripu/algorithm/sleep/Vote.java +++ b/suripu-algorithm/src/main/java/com/hello/suripu/algorithm/sleep/Vote.java @@ -30,6 +30,7 @@ public class Vote { private final MotionScoreAlgorithm motionScoreAlgorithmDefault; private final MotionCluster motionCluster; + private final List alignedAmplitude; private final Map> aggregatedFeatures; private final double rawAmpMean; private final double rawKickOffMean; @@ -62,6 +63,7 @@ public Vote(final List rawData, this.rawKickOffMean = NumericalUtils.mean(noDuplicateKickOffCounts); List dataWithGapFilled = DataUtils.fillMissingValues(noDuplicates, DateTimeConstants.MILLIS_PER_MINUTE); + this.alignedAmplitude = Lists.newArrayList(dataWithGapFilled); List alignedKickOffs = DataUtils.fillMissingValues(noDuplicateKickOffCounts, DateTimeConstants.MILLIS_PER_MINUTE); if(insertEmpty) { final int insertLengthMin = 20; @@ -116,22 +118,26 @@ public Vote(final List rawData, public final List getAwakes(final long fallAsleepMillis, final long wakeUpMillis, final boolean debug){ - final List allAwakesPeriods = this.sleepPeriod.getAwakePeriods(debug); - final List awakesInTheRange = new ArrayList<>(); + final List allAwakesPeriods = this.sleepPeriod.getAwakePeriods(debug); + final List awakesInTheRange = new ArrayList<>(); - for(final Segment segment:allAwakesPeriods){ + for(final VotingSegment segment:allAwakesPeriods){ if(fallAsleepMillis <= segment.getStartTimestamp() && wakeUpMillis >= segment.getEndTimestamp()){ awakesInTheRange.add(segment); } } - final List smoothedAwakes = smoothAwakes(awakesInTheRange, MotionCluster.toSegments(this.motionCluster.getCopyOfClusters())); - return smoothedAwakes; + final List smoothedAwakes = smoothAwakes(awakesInTheRange, MotionCluster.toSegments(this.motionCluster.getCopyOfClusters())); + final List results = new ArrayList<>(); + for(final VotingSegment votingSegment:smoothedAwakes){ + results.add(votingSegment); + } + return results; } - private List getAwakesInTimeSpanMillis(final List awakes, final long startMillis, final long endMillis){ - final List result = new ArrayList<>(); - for(final Segment awake:awakes){ + private List getAwakesInTimeSpanMillis(final List awakes, final long startMillis, final long endMillis){ + final List result = new ArrayList<>(); + for(final VotingSegment awake:awakes){ if(awake.getStartTimestamp() >= startMillis && awake.getEndTimestamp() <= endMillis){ result.add(awake); } @@ -140,14 +146,14 @@ private List getAwakesInTimeSpanMillis(final List awakes, fina return result; } - private List filterAwakeFragments(final List awakesInMotionCluster){ - final List result = new ArrayList<>(); + private List filterAwakeFragments(final List awakesInMotionCluster){ + final List result = new ArrayList<>(); if(awakesInMotionCluster.size() < 2){ return awakesInMotionCluster; } - for(final Segment awake:awakesInMotionCluster){ - if(awake.getDuration() < 20 * DateTimeConstants.MILLIS_PER_MINUTE){ + for(final VotingSegment awake:awakesInMotionCluster){ + if(awake.getDuration() < 10 * DateTimeConstants.MILLIS_PER_MINUTE){ continue; } result.add(awake); @@ -160,10 +166,10 @@ private List filterAwakeFragments(final List awakesInMotionClu * Idea: if multiple awakes is in the same motion cluster, filter out those less than * 20 minutes, less is more. */ - private List smoothAwakes(final List awakes, final List motionClusters){ - final List smoothedAwakes = new ArrayList<>(); + private List smoothAwakes(final List awakes, final List motionClusters){ + final List smoothedAwakes = new ArrayList<>(); for(final Segment currentCluster:motionClusters){ - final List awakesInMotionCluster = getAwakesInTimeSpanMillis(awakes, + final List awakesInMotionCluster = getAwakesInTimeSpanMillis(awakes, currentCluster.getStartTimestamp(), currentCluster.getEndTimestamp()); smoothedAwakes.addAll(filterAwakeFragments(awakesInMotionCluster)); @@ -258,11 +264,10 @@ private SleepEvents aggregate(final SleepEvents defaultEvents) clusterCopy, this.getAggregatedFeatures(), defaultEvents.fallAsleep.getStartTimestamp()); - long sleepTimeMillis = sleepTimesMillis.getSecond(); - inBed = new Segment(sleepTimesMillis.getFirst(), sleepTimesMillis.getFirst() + DateTimeConstants.MILLIS_PER_MINUTE, sleep.getOffsetMillis()); - sleep = new Segment(sleepTimeMillis, - sleepTimeMillis + DateTimeConstants.MILLIS_PER_MINUTE, - defaultEvents.fallAsleep.getOffsetMillis()); + long sleepTimeMillis = findNearestDataTime(this.alignedAmplitude, sleepTimesMillis.getSecond()); + long inBedTimeMillis = findNearestDataTime(this.alignedAmplitude, sleepTimesMillis.getFirst()); + inBed = new Segment(inBedTimeMillis, inBedTimeMillis + DateTimeConstants.MILLIS_PER_MINUTE, defaultEvents.goToBed.getOffsetMillis()); + sleep = new Segment(sleepTimeMillis, sleepTimeMillis + DateTimeConstants.MILLIS_PER_MINUTE, defaultEvents.fallAsleep.getOffsetMillis()); if(inBed.getStartTimestamp() > sleep.getStartTimestamp()){ inBed = new Segment(sleep.getStartTimestamp() - 10 * DateTimeConstants.MILLIS_PER_MINUTE, sleep.getStartTimestamp() - 9 * DateTimeConstants.MILLIS_PER_MINUTE, @@ -273,17 +278,42 @@ private SleepEvents aggregate(final SleepEvents defaultEvents) Segment wakeUp = defaultEvents.wakeUp; Segment outBed = defaultEvents.outOfBed; - final Pair wakeUpTimesMillis = safeGuardPickWakeUp(clusterCopy, + Pair wakeUpTimesMillis = safeGuardPickWakeUp(clusterCopy, sleepPeriod, getAggregatedFeatures(), defaultEvents.wakeUp.getStartTimestamp()); - wakeUp = new Segment(wakeUpTimesMillis.getFirst(), wakeUpTimesMillis.getFirst() + DateTimeConstants.MILLIS_PER_MINUTE, wakeUp.getOffsetMillis()); - outBed = new Segment(wakeUpTimesMillis.getSecond(), wakeUpTimesMillis.getSecond() + DateTimeConstants.MILLIS_PER_MINUTE, inBed.getOffsetMillis()); + + long wakeUpMillis = findNearestDataTime(this.alignedAmplitude, wakeUpTimesMillis.getFirst()); + long outBedMillis = findNearestDataTime(this.alignedAmplitude, wakeUpTimesMillis.getSecond()); + wakeUp = new Segment(wakeUpMillis, wakeUpMillis + DateTimeConstants.MILLIS_PER_MINUTE, defaultEvents.wakeUp.getOffsetMillis()); + outBed = new Segment(outBedMillis, outBedMillis + DateTimeConstants.MILLIS_PER_MINUTE, defaultEvents.outOfBed.getOffsetMillis()); return SleepEvents.create(inBed, sleep, wakeUp, outBed); } + private long findNearestDataTime(final List data, final long targetMillis){ + int minDiff = Integer.MAX_VALUE; + long time = 0; + for(int i = 0; i < data.size(); i++){ + if(data.get(i).amplitude == 0){ + continue; + } + + final int diff = (int) Math.abs(data.get(i).timestamp - targetMillis); + if(diff < minDiff){ + minDiff = diff; + time = data.get(i).timestamp; + } + } + + if(minDiff == Integer.MAX_VALUE){ + return targetMillis; + } + + return time; + } + private static Pair predictionBoundsMillis(final long wakeUpMillisPredicted, final Optional predictionSegment){ if(!predictionSegment.isPresent()){ @@ -292,6 +322,36 @@ private static Pair predictionBoundsMillis(final long wakeUpMillisPr return new Pair<>(wakeUpMillisPredicted, predictionSegment.get().getEndTimestamp()); } + private static Optional getNextAwakeInSleepPeriod(final List awakesUnfiltered, + final SleepPeriod sleepPeriod, + final long wakeUpSafeGuarded){ + for(final VotingSegment votingSegment:awakesUnfiltered){ + if(votingSegment.getStartTimestamp() > sleepPeriod.getEndTimestamp()){ + continue; + } + + if(votingSegment.getDuration() < 20 * DateTimeConstants.MILLIS_PER_MINUTE){ + continue; + } + + if(votingSegment.getStartTimestamp() >= wakeUpSafeGuarded || + (votingSegment.getStartTimestamp() < wakeUpSafeGuarded && votingSegment.getEndTimestamp() >= wakeUpSafeGuarded)){ + return Optional.of(votingSegment); + } + } + + return Optional.absent(); + } + + protected static Optional getAwakeCluster(final List clusters, final VotingSegment awake){ + for(final Segment cluster:clusters){ + if(cluster.getStartTimestamp() >= awake.getStartTimestamp() && cluster.getEndTimestamp() <= awake.getEndTimestamp()){ + return Optional.of(cluster); + } + } + return Optional.absent(); + } + protected static Pair safeGuardPickWakeUp(final List clusters, final SleepPeriod sleepPeriod, final Map> featuresNotCapped, @@ -313,8 +373,10 @@ protected static Pair safeGuardPickWakeUp(final List(wakeUpMillisPredicted, lastSegmentInSleepPeriod.getEndTimestamp()); } + // predict < last cluster if(wakeUpMillisPredicted < lastSegmentInSleepPeriod.getStartTimestamp()) { - // predict << last segment of sleep period + // predict << last segment of sleep period, this user toss and turn a lot during sleep + // The prediction can landed on one of the heavy toss-and-turn cluster. if (lastSegmentInSleepPeriod.getStartTimestamp() - wakeUpMillisPredicted > 40 * DateTimeConstants.MILLIS_PER_MINUTE) { LOGGER.debug("Predicted too far way from end, predicted {}", new DateTime(wakeUpMillisPredicted, DateTimeZone.forOffsetMillis(lastSegment.getOffsetMillis()))); @@ -327,8 +389,8 @@ protected static Pair safeGuardPickWakeUp(final List safeGuardPickWakeUp(final List(maxWakeUpScoreOptional.get().timestamp, clusters.get(maxScoreCluster.getSecond()).timestamp); }else { - + // Prediction is nearby the last cluster, but still not the last + // fallback to the moment has max wake up score. LOGGER.debug("OK USER: Predict not too far from end"); final Optional maxWakeUpScoreOptional = getMaxScore(featuresNotCapped, MotionFeatures.FeatureType.DENSITY_BACKWARD_AVERAGE_AMPLITUDE, @@ -365,11 +428,11 @@ protected static Pair safeGuardPickWakeUp(final List last segment in sleep period + // predict > last segment in sleep period, possible caused by maid or wrong sleep period detection if(lastSegment.getStartTimestamp() == lastSegmentInSleepPeriod.getStartTimestamp() && lastSegment.getEndTimestamp() == lastSegmentInSleepPeriod.getEndTimestamp()){ - // No motion cluster after end of sleep period. + // No motion cluster after end of sleep period. error not caused by maid motion LOGGER.debug("-------------* No maid found, last motion cluster {} - {}", new DateTime(lastSegment.getStartTimestamp(), DateTimeZone.forOffsetMillis(lastSegment.getOffsetMillis())), new DateTime(lastSegment.getEndTimestamp(), DateTimeZone.forOffsetMillis(lastSegment.getOffsetMillis()))); @@ -390,7 +453,7 @@ protected static Pair safeGuardPickWakeUp(final List(wakeUpMillisPredicted, lastMotionMillis); }else { - + // Maid or partner motion found, maid might cause multiple motion clusters // last segment in sleep period < last segment && predict > last segment in sleep period LOGGER.debug("-------------* Maid found, last motion cluster in sleep period {} - {}", new DateTime(lastSegmentInSleepPeriod.getStartTimestamp(), DateTimeZone.forOffsetMillis(lastSegmentInSleepPeriod.getOffsetMillis())), @@ -403,11 +466,12 @@ protected static Pair safeGuardPickWakeUp(final List maxWakeUpScoreOptional = getMaxScore(featuresNotCapped, MotionFeatures.FeatureType.DENSITY_BACKWARD_AVERAGE_AMPLITUDE, lastSegmentInSleepPeriod.getEndTimestamp() - 60 * DateTimeConstants.MILLIS_PER_MINUTE, - lastSegmentInSleepPeriod.getEndTimestamp() ); + lastSegmentInSleepPeriod.getEndTimestamp()); + // No obvious motion, edge case if(!maxWakeUpScoreOptional.isPresent()){ return new Pair<>(lastSegmentInSleepPeriod.getStartTimestamp(), lastSegmentInSleepPeriod.getEndTimestamp()); } diff --git a/suripu-algorithm/src/main/java/com/hello/suripu/algorithm/sleep/VotingSegment.java b/suripu-algorithm/src/main/java/com/hello/suripu/algorithm/sleep/VotingSegment.java new file mode 100644 index 000000000..c4ea580d8 --- /dev/null +++ b/suripu-algorithm/src/main/java/com/hello/suripu/algorithm/sleep/VotingSegment.java @@ -0,0 +1,14 @@ +package com.hello.suripu.algorithm.sleep; + +import com.hello.suripu.algorithm.core.Segment; + +/** + * Created by pangwu on 4/14/15. + */ +public class VotingSegment extends Segment { + public final double vote; + public VotingSegment(final long startTimestampMillis, final long endTimestampMillis, final int offsetMillis, final double vote){ + super(startTimestampMillis, endTimestampMillis, offsetMillis); + this.vote = vote; + } +}