diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/confidence/ConfidenceBasedAverager.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/confidence/ConfidenceBasedAverager.java index c1a14b38938..209c011ce78 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/confidence/ConfidenceBasedAverager.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/confidence/ConfidenceBasedAverager.java @@ -1,5 +1,7 @@ package com.sap.sailing.domain.common.confidence; +import java.util.Iterator; + public interface ConfidenceBasedAverager { /** * If a non-null weigher has been set for this averager, at must be a valid reference @@ -15,4 +17,7 @@ public interface ConfidenceBasedAverager { */ HasConfidence getAverage( Iterable> values, RelativeTo at); + + HasConfidence getAverage( + Iterator> values, RelativeTo at); } diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/confidence/impl/ConfidenceBasedAveragerImpl.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/confidence/impl/ConfidenceBasedAveragerImpl.java index 351e8aa9d41..208b1003aea 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/confidence/impl/ConfidenceBasedAveragerImpl.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/confidence/impl/ConfidenceBasedAveragerImpl.java @@ -1,10 +1,11 @@ package com.sap.sailing.domain.common.confidence.impl; +import java.util.Iterator; + import com.sap.sailing.domain.common.confidence.ConfidenceBasedAverager; import com.sap.sailing.domain.common.confidence.HasConfidence; import com.sap.sailing.domain.common.confidence.HasConfidenceAndIsScalable; import com.sap.sailing.domain.common.confidence.Weigher; -import com.sap.sse.common.Util; import com.sap.sse.common.scalablevalue.ScalableValue; @@ -45,13 +46,21 @@ public class ConfidenceBasedAveragerImpl implem @Override public HasConfidence getAverage( Iterable> values, RelativeTo at) { - if (values == null || Util.isEmpty(values)) { + return values == null ? null : getAverage(values.iterator(), at); + } + + @Override + public HasConfidence getAverage( + Iterator> values, RelativeTo at) { + if (values == null || !values.hasNext()) { return null; } else { ScalableValue numerator = null; double confidenceSum = 0; - for (HasConfidenceAndIsScalable next : values) { - double relativeWeight = (getWeigher() == null ? 1.0 : getWeigher().getConfidence(next.getRelativeTo(), at)) * next.getConfidence(); + int count = 0; + while (values.hasNext()) { + HasConfidenceAndIsScalable next = values.next(); + double relativeWeight = getWeight(next, at); ScalableValue weightedNext = next.getScalableValue().multiply(relativeWeight); if (numerator == null) { numerator = weightedNext; @@ -59,15 +68,20 @@ public class ConfidenceBasedAveragerImpl implem numerator = numerator.add(weightedNext); } confidenceSum += relativeWeight; + count++; } // TODO consider greater variance to reduce the confidence - double newConfidence = confidenceSum / Util.size(values); + double newConfidence = confidenceSum / count; BaseType result = numerator.divide(confidenceSum); return new HasConfidenceImpl(result, newConfidence, at); } } - protected Weigher getWeigher() { + protected double getWeight(HasConfidenceAndIsScalable next, RelativeTo at) { + return (getWeigher() == null ? 1.0 : getWeigher().getConfidence(next.getRelativeTo(), at)) * next.getConfidence(); + } + + private Weigher getWeigher() { return weigher; } } diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/impl/ScalableBearing.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/impl/ScalableBearing.java index f833e1ab59b..0608fbed17c 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/impl/ScalableBearing.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/impl/ScalableBearing.java @@ -63,4 +63,25 @@ public class ScalableBearing implements ScalableValueWithDistancea to the same circle. The distance returned by this method will provide + * almost equal values for very small differences (below 1deg) but significantly smaller values for big differences. + * + * @return an approximate difference in degrees, fairly accurate for small differences, but returning 360.0/PI + * (approximately 114) instead of 180 for the maximum difference + */ + public double getApproximateDegreeDistanceTo(Bearing a) { + double scale = Math.sqrt(sin*sin + cos*cos); + double x = cos/scale; + double y = sin/scale; + double ax = Math.cos(a.getRadians()); + double ay = Math.sin(a.getRadians()); + double normalDistance = Math.sqrt((x-ax)*(x-ax) + (y-ay)*(y-ay)); + return normalDistance / Math.PI * 180.; + } } diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/impl/ScalablePosition.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/impl/ScalablePosition.java index 2d0ed6be94c..6115de11082 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/impl/ScalablePosition.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/impl/ScalablePosition.java @@ -14,7 +14,7 @@ public class ScalablePosition implements ScalableValueWithDistance { diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/impl/TrackImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/impl/TrackImpl.java index 27b7813f2c4..0dd4bd4a488 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/impl/TrackImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/impl/TrackImpl.java @@ -8,11 +8,11 @@ import java.util.NavigableSet; import com.sap.sailing.domain.base.Timed; import com.sap.sailing.domain.tracking.Track; -import com.sap.sse.common.ArrayListNavigableSet; import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.concurrent.LockUtil; import com.sap.sse.concurrent.NamedReentrantReadWriteLock; +import com.sap.sse.util.impl.ArrayListNavigableSet; import com.sap.sse.util.impl.UnmodifiableNavigableSet; public class TrackImpl implements Track { diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ArrayListNavigableSetTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ArrayListNavigableSetTest.java index c9c0545dfc3..7d1b2ab4725 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ArrayListNavigableSetTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ArrayListNavigableSetTest.java @@ -11,7 +11,7 @@ import java.util.Set; import org.junit.Test; -import com.sap.sse.common.ArrayListNavigableSet; +import com.sap.sse.util.impl.ArrayListNavigableSet; public class ArrayListNavigableSetTest { @Test diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ConfidenceTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ConfidenceTest.java index 7c1e966affa..05cf9f35265 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ConfidenceTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ConfidenceTest.java @@ -206,7 +206,7 @@ public class ConfidenceTest { @Test public void testAveragingWithNullArrayYieldsNull() { ConfidenceBasedAverager averager = ConfidenceFactory.INSTANCE.createAverager(null); - HasConfidence average = averager.getAverage(null, null); + HasConfidence average = averager.getAverage((Iterable>) null, null); assertNull(average); } @@ -414,7 +414,7 @@ public class ConfidenceTest { public void testConfidenceBasedAveragingWithNullArrayYieldsNull() { ConfidenceBasedAverager averager = ConfidenceFactory.INSTANCE .createAverager(ConfidenceFactory.INSTANCE.createExponentialTimeDifferenceWeigher(1000)); - HasConfidence average = averager.getAverage(null, null); + HasConfidence average = averager.getAverage((Iterable>) null, null); assertNull(average); } @@ -531,5 +531,65 @@ public class ConfidenceTest { assertTrue(average.getObject().getLatDeg() < 49.15); assertEquals((8.0*4/4 + 9.0*2/4 + 8.5*1/4)/(7./4.), average.getObject().getLngDeg(), 0.01); } + + @Test + public void simpleApproximateScalableBearingDistanceTest1() { + com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing sb = new com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing(new DegreeBearingImpl(100)); + Bearing b = new DegreeBearingImpl(280); + assertEquals(360./Math.PI, sb.getApproximateDegreeDistanceTo(b), 0.001); + } + + @Test + public void simpleApproximateScalableBearingDistanceTest2() { + com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing sb = new com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing(new DegreeBearingImpl(0)); + Bearing b = new DegreeBearingImpl(180); + assertEquals(360./Math.PI, sb.getApproximateDegreeDistanceTo(b), 0.001); + } + + @Test + public void simpleApproximateScalableBearingDistanceTest3() { + com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing sb = new com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing(new DegreeBearingImpl(100)); + Bearing b = new DegreeBearingImpl(101); + assertEquals(1, sb.getApproximateDegreeDistanceTo(b), 0.01); + } + + @Test + public void simpleApproximateScalableBearingDistanceTest4() { + com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing sb = new com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing(new DegreeBearingImpl(0)); + Bearing b = new DegreeBearingImpl(1); + assertEquals(1, sb.getApproximateDegreeDistanceTo(b), 0.01); + } + + @Test + public void scaledApproximateScalableBearingDistanceTest1() { + com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing sb = new com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing(new DegreeBearingImpl(100)). + multiply(123); + Bearing b = new DegreeBearingImpl(280); + assertEquals(360./Math.PI, sb.getApproximateDegreeDistanceTo(b), 0.001); + } + + @Test + public void scaledApproximateScalableBearingDistanceTest2() { + com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing sb = new com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing(new DegreeBearingImpl(0)). + multiply(123); + Bearing b = new DegreeBearingImpl(180); + assertEquals(360./Math.PI, sb.getApproximateDegreeDistanceTo(b), 0.001); + } + + @Test + public void scaledApproximateScalableBearingDistanceTest3() { + com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing sb = new com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing(new DegreeBearingImpl(100)). + multiply(123); + Bearing b = new DegreeBearingImpl(101); + assertEquals(1, sb.getApproximateDegreeDistanceTo(b), 0.01); + } + + @Test + public void scaledApproximateScalableBearingDistanceTest4() { + com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing sb = new com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing(new DegreeBearingImpl(0)). + multiply(123); + Bearing b = new DegreeBearingImpl(1); + assertEquals(1, sb.getApproximateDegreeDistanceTo(b), 0.01); + } } diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTest.java index 3ad5d6801fa..304e79df87d 100644 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTest.java @@ -72,11 +72,11 @@ import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; import com.sap.sailing.domain.tracking.impl.MarkPassingImpl; import com.sap.sailing.domain.tracking.impl.TimedComparator; -import com.sap.sse.common.ArrayListNavigableSet; import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; import com.sap.sse.common.impl.MillisecondsTimePoint; +import com.sap.sse.util.impl.ArrayListNavigableSet; public class LeaderboardScoringAndRankingTest extends AbstractLeaderboardTest { private ArrayList series; diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/impl/ConfidenceBasedWindAveragerImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/impl/ConfidenceBasedWindAveragerImpl.java index 287c9bdc07e..8e60e779b67 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/impl/ConfidenceBasedWindAveragerImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/impl/ConfidenceBasedWindAveragerImpl.java @@ -1,5 +1,7 @@ package com.sap.sailing.domain.confidence.impl; +import java.util.Iterator; + import com.sap.sailing.domain.common.Speed; import com.sap.sailing.domain.common.WindSource; import com.sap.sailing.domain.common.WindSourceType; @@ -13,7 +15,6 @@ import com.sap.sailing.domain.tracking.WindWithConfidence; import com.sap.sailing.domain.tracking.impl.ScalableWind; import com.sap.sailing.domain.tracking.impl.WindImpl; import com.sap.sailing.domain.tracking.impl.WindWithConfidenceImpl; -import com.sap.sse.common.Util; /** * In order to enable the aggregation of {@link Wind} objects whose {@link WindSource}'s {@link WindSourceType} suggests @@ -37,16 +38,24 @@ public class ConfidenceBasedWindAveragerImpl extends @Override public WindWithConfidence getAverage( Iterable> values, RelativeTo at) { + return getAverage(values.iterator(), at); + } + + @Override + public WindWithConfidence getAverage( + Iterator> values, RelativeTo at) { boolean atLeastOneFixWasMarkedToUseSpeed = false; - if (values == null || Util.isEmpty(values)) { + if (values == null || !values.hasNext()) { return null; } else { ScalableWind numerator = null; double confidenceSum = 0; double speedConfidenceSum = 0; double knotSum = 0; - for (HasConfidenceAndIsScalable next : values) { - double relativeWeight = (getWeigher() == null ? 1.0 : getWeigher().getConfidence(next.getRelativeTo(), at)) * next.getConfidence(); + int count = 0; + while (values.hasNext()) { + HasConfidenceAndIsScalable next = values.next(); + double relativeWeight = getWeight(next, at); ScalableWind weightedNext = next.getScalableValue().multiply(relativeWeight).getValue(); double weighedNextKnots = next.getObject().getKnots() * relativeWeight; if (numerator == null) { @@ -61,9 +70,10 @@ public class ConfidenceBasedWindAveragerImpl extends speedConfidenceSum += relativeWeight; knotSum += weighedNextKnots; } + count++; } // TODO consider greater variance to reduce the confidence - double newConfidence = confidenceSum / Util.size(values); + double newConfidence = confidenceSum / count; Wind preResult = numerator.divide(confidenceSum); // if only values with useSpeed=false were aggregated, use the original result, otherwise compute // separate speed average: diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CourseBasedWindTrackImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CourseBasedWindTrackImpl.java index 03e4ef61f09..ec6296666fe 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CourseBasedWindTrackImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CourseBasedWindTrackImpl.java @@ -5,9 +5,9 @@ import java.util.NavigableSet; import com.sap.sailing.domain.common.WindSourceType; import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.domain.tracking.Wind; -import com.sap.sse.common.ArrayListNavigableSet; import com.sap.sse.common.TimePoint; import com.sap.sse.common.impl.MillisecondsTimePoint; +import com.sap.sse.util.impl.ArrayListNavigableSet; import com.sap.sse.util.impl.UnmodifiableNavigableSet; /** diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DistanceCache.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DistanceCache.java index a3493dce33c..8e3e55c7479 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DistanceCache.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DistanceCache.java @@ -6,11 +6,11 @@ import java.util.NavigableSet; import com.sap.sailing.domain.common.Distance; import com.sap.sailing.domain.tracking.GPSFix; -import com.sap.sse.common.ArrayListNavigableSet; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; import com.sap.sse.concurrent.LockUtil; import com.sap.sse.concurrent.NamedReentrantReadWriteLock; +import com.sap.sse.util.impl.ArrayListNavigableSet; /** * Computing {@link #getDistanceTraveled(TimePoint, TimePoint)} is more expensive the longer the track is and the diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/GPSFixTrackImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/GPSFixTrackImpl.java index fcda31eda05..649a857ca0c 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/GPSFixTrackImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/GPSFixTrackImpl.java @@ -42,10 +42,10 @@ import com.sap.sailing.domain.tracking.GPSFixMoving; import com.sap.sailing.domain.tracking.GPSFixTrack; import com.sap.sailing.domain.tracking.GPSTrackListener; import com.sap.sailing.domain.tracking.WithValidityCache; -import com.sap.sse.common.ArrayListNavigableSet; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; import com.sap.sse.common.impl.MillisecondsTimePoint; +import com.sap.sse.util.impl.ArrayListNavigableSet; public class GPSFixTrackImpl extends TrackImpl implements GPSFixTrack { private static final Logger logger = Logger.getLogger(GPSFixTrackImpl.class.getName()); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/MaxSpeedCache.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/MaxSpeedCache.java index 1d6cf98bd90..01f4e08b6da 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/MaxSpeedCache.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/MaxSpeedCache.java @@ -12,12 +12,12 @@ import com.sap.sailing.domain.tracking.GPSFix; import com.sap.sailing.domain.tracking.GPSFixMoving; import com.sap.sailing.domain.tracking.GPSFixTrack; import com.sap.sailing.domain.tracking.GPSTrackListener; -import com.sap.sse.common.ArrayListNavigableSet; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; import com.sap.sse.common.Util.Pair; import com.sap.sse.concurrent.LockUtil; import com.sap.sse.concurrent.NamedReentrantReadWriteLock; +import com.sap.sse.util.impl.ArrayListNavigableSet; /** * Re-calculating the maximum speed over a {@link GPSFixTrack} is time consuming. When the track grows the way it diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackBasedEstimationWindTrackImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackBasedEstimationWindTrackImpl.java index c193e491c10..c60f0bcbd58 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackBasedEstimationWindTrackImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackBasedEstimationWindTrackImpl.java @@ -33,7 +33,6 @@ import com.sap.sailing.domain.tracking.TrackedRaceStatus; import com.sap.sailing.domain.tracking.Wind; import com.sap.sailing.domain.tracking.WindTrack; import com.sap.sailing.domain.tracking.WindWithConfidence; -import com.sap.sse.common.ArrayListNavigableSet; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; import com.sap.sse.common.Util.Pair; @@ -42,6 +41,7 @@ import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.common.impl.SerializableComparator; import com.sap.sse.concurrent.LockUtil; import com.sap.sse.concurrent.NamedReentrantReadWriteLock; +import com.sap.sse.util.impl.ArrayListNavigableSet; /** * A virtual wind track that computes and caches the wind bearing based on the boat tracks recorded in the tracked race diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedLegOfCompetitorImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedLegOfCompetitorImpl.java index 41815b7871a..4d8ce19263b 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedLegOfCompetitorImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedLegOfCompetitorImpl.java @@ -31,10 +31,10 @@ import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.domain.tracking.Wind; import com.sap.sailing.domain.tracking.WindLegTypeAndLegBearingCache; import com.sap.sailing.domain.tracking.WindPositionMode; -import com.sap.sse.common.ArrayListNavigableSet; import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; +import com.sap.sse.util.impl.ArrayListNavigableSet; /** * Provides a convenient view on the tracked leg, projecting to a single competitor's performance. diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java index a710a17d49f..2e18f33e706 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java @@ -113,7 +113,6 @@ import com.sap.sailing.domain.tracking.WindPositionMode; import com.sap.sailing.domain.tracking.WindStore; import com.sap.sailing.domain.tracking.WindTrack; import com.sap.sailing.domain.tracking.WindWithConfidence; -import com.sap.sse.common.ArrayListNavigableSet; import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; @@ -124,6 +123,7 @@ import com.sap.sse.concurrent.NamedReentrantReadWriteLock; import com.sap.sse.util.SmartFutureCache; import com.sap.sse.util.SmartFutureCache.AbstractCacheUpdater; import com.sap.sse.util.SmartFutureCache.EmptyUpdateInterval; +import com.sap.sse.util.impl.ArrayListNavigableSet; import difflib.DiffUtils; import difflib.Patch; diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/WindTrackImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/WindTrackImpl.java index 0219e5d6953..04a840c7645 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/WindTrackImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/WindTrackImpl.java @@ -25,9 +25,9 @@ import com.sap.sailing.domain.tracking.Wind; import com.sap.sailing.domain.tracking.WindListener; import com.sap.sailing.domain.tracking.WindTrack; import com.sap.sailing.domain.tracking.WindWithConfidence; -import com.sap.sse.common.ArrayListNavigableSet; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; +import com.sap.sse.util.impl.ArrayListNavigableSet; /** * Records {@link Wind} objects over time and offers to average the last so many of them into an diff --git a/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/windestimation/TestWindEstimationFromManeuversOnAFew505Races.java b/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/windestimation/TestWindEstimationFromManeuversOnAFew505Races.java new file mode 100755 index 00000000000..abf80a36b72 --- /dev/null +++ b/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/windestimation/TestWindEstimationFromManeuversOnAFew505Races.java @@ -0,0 +1,174 @@ +package com.sap.sailing.polars.windestimation; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.text.ParseException; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.junit.Test; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.ManeuverType; +import com.sap.sailing.domain.common.confidence.impl.ScalableDouble; +import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing; +import com.sap.sailing.domain.test.OnlineTracTracBasedTest; +import com.sap.sailing.domain.tracking.Maneuver; +import com.sap.sailing.domain.tracking.Wind; +import com.sap.sailing.domain.tracking.impl.ScalableWind; +import com.sap.sailing.domain.tractracadapter.ReceiverType; +import com.sap.sailing.polars.impl.PolarDataServiceImpl; +import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException; +import com.sap.sailing.polars.windestimation.ManeuverBasedWindEstimationTrackImpl.ManeuverClassification; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.util.kmeans.Cluster; +import com.sap.sse.util.kmeans.KMeansMappingClusterer; +import com.tractrac.model.lib.api.event.CreateModelException; +import com.tractrac.subscription.lib.api.SubscriberInitializationException; + +public class TestWindEstimationFromManeuversOnAFew505Races extends OnlineTracTracBasedTest { + + private String expectedEventName; + + public TestWindEstimationFromManeuversOnAFew505Races() throws URISyntaxException, MalformedURLException { + super(); + } + + private void setUp(final String fileBaseName) throws MalformedURLException, IOException, InterruptedException, URISyntaxException, + ParseException, SubscriberInitializationException, CreateModelException { + super.setUp(); + URI storedUri = new URI("file:///"+new File("../com.sap.sailing.domain.test/resources/"+fileBaseName+".mtb").getCanonicalPath().replace('\\', '/')); + super.setUp(new URL("file:///"+new File("../com.sap.sailing.domain.test/resources/"+fileBaseName+".txt").getCanonicalPath()), + /* liveUri */ null, /* storedUri */ storedUri, + new ReceiverType[] { ReceiverType.RACECOURSE, ReceiverType.RAWPOSITIONS }); // only the tracks; no mark positions, no wind, no mark passings + } + + private void setUp(final String expectedEventName, final String fileBaseName) throws MalformedURLException, IOException, InterruptedException, URISyntaxException, ParseException, SubscriberInitializationException, CreateModelException { + setExpectedEventName(expectedEventName); + setUp(fileBaseName); + } + + @Override + protected String getExpectedEventName() { + final String result; + if (expectedEventName == null) { + result = super.getExpectedEventName(); + } else { + result = expectedEventName; + } + return result; + } + + private void setExpectedEventName(String expectedEventName) { + this.expectedEventName = expectedEventName; + } + + @Test + public void testWindEstimationFromManeuversOnOBMR2012V2Blue() throws MalformedURLException, IOException, + InterruptedException, URISyntaxException, ParseException, SubscriberInitializationException, + CreateModelException, NotEnoughDataHasBeenAddedException { + setUp("OBMR 2012", "event_20121031_OBMR-OBMR_2012_VR_Fr_Fleet_1_2"); + Wind average = getManeuverBasedAverageWind(); + assertEquals(222, average.getFrom().getDegrees(), 5.0); // wind in this race was from 075deg on average + } + + @Test + public void testWindEstimationFromManeuversOn505KW2011Race2() throws MalformedURLException, IOException, + InterruptedException, URISyntaxException, ParseException, SubscriberInitializationException, + CreateModelException, NotEnoughDataHasBeenAddedException { + setUp("event_20110609_KielerWoch-505_Race_2"); + Wind average = getManeuverBasedAverageWind(); + assertEquals(235, average.getFrom().getDegrees(), 5.0); // wind in this race was from 075deg on average + } + + @Test + public void testWindEstimationFromManeuversOn505KW2011Race3() throws MalformedURLException, IOException, + InterruptedException, URISyntaxException, ParseException, SubscriberInitializationException, + CreateModelException, NotEnoughDataHasBeenAddedException { + setUp("event_20110609_KielerWoch-505_Race_3"); + Wind average = getManeuverBasedAverageWind(); + assertEquals(245, average.getFrom().getDegrees(), 5.0); // wind in this race was from 075deg on average + } + + @Test + public void testWindEstimationFromManeuversOn505KW2011Race4() throws MalformedURLException, IOException, + InterruptedException, URISyntaxException, ParseException, SubscriberInitializationException, + CreateModelException, NotEnoughDataHasBeenAddedException { + setUp("event_20110609_KielerWoch-505_race_4"); + Wind average = getManeuverBasedAverageWind(); + assertEquals(275, average.getFrom().getDegrees(), 5.0); // wind in this race was from 075deg on average + } + + @Test + public void testTwoDimensionalClustering() throws NotEnoughDataHasBeenAddedException, MalformedURLException, + IOException, InterruptedException, URISyntaxException, ParseException, SubscriberInitializationException, + CreateModelException { + setUp("event_20110609_KielerWoch-505_Race_3"); + ManeuverBasedWindEstimationTrackImpl windTrack = new ManeuverBasedWindEstimationTrackImpl(new PolarDataServiceImpl(Executors.newFixedThreadPool(4)), + getTrackedRace(), /* millisecondsOverWhichToAverage */ 30000, /* waitForLatest */ true); + final Map maneuvers = windTrack.getAllManeuvers(/* waitForLatest */ true); + final int numberOfClusters = 16; + KMeansMappingClusterer, Pair, ScalableBearingAndScalableDouble> clusterer = + new KMeansMappingClusterer<>(numberOfClusters, + windTrack.getManeuverClassifications(maneuvers), + (mc)->new ScalableBearingAndScalableDouble(mc.getMiddleManeuverCourse(), mc.getManeuverAngleDeg()), // maps maneuver classification to cluster metric + // use an evenly distributed set of cluster seeds for clustering wind direction estimations + Stream.concat(IntStream.range(0, numberOfClusters/2).mapToObj((i)-> + new Pair<>(new DegreeBearingImpl(((double) i)*360./(double) numberOfClusters/2), 45.)), + IntStream.range(0, numberOfClusters/2).mapToObj((i)-> + new Pair<>(new DegreeBearingImpl(((double) i)*360./(double) numberOfClusters/2), -45.)))); + final Set, Pair, ScalableBearingAndScalableDouble>> clusters = clusterer.getClusters(); + assertEquals(16, clusters.size()); + + // Now work towards identifying the two tack clusters + List, Pair, ScalableBearingAndScalableDouble>> clustersSortedByAverageTackLikelihood = + clusters.stream() + .sorted((c1, c2) -> (int) -Math.signum(getAverageLikelihood(c1, ManeuverType.TACK) + - getAverageLikelihood(c2, ManeuverType.TACK))).collect(Collectors.toList()); + // expecting a wind direction that is from around 245deg, +/- 10deg + assertEquals(245., windTrack.getWeightedAverageMiddleManeuverCOGDeg(clustersSortedByAverageTackLikelihood.get(0), ManeuverType.TACK).getDegrees(), 10.); + assertEquals(245., windTrack.getWeightedAverageMiddleManeuverCOGDeg(clustersSortedByAverageTackLikelihood.get(1), ManeuverType.TACK).getDegrees(), 10.); + } + + private double getAverageLikelihood( + Cluster, Pair, ScalableBearingAndScalableDouble> cluster, + ManeuverType maneuverType) { + return cluster.stream().mapToDouble((mc)->mc.getLikelihoodAndTWSBasedOnSpeedAndAngle(maneuverType).getA()).average().getAsDouble(); + } + + private Wind getManeuverBasedAverageWind() throws NotEnoughDataHasBeenAddedException { + ManeuverBasedWindEstimationTrackImpl windTrack = new ManeuverBasedWindEstimationTrackImpl(new PolarDataServiceImpl(Executors.newFixedThreadPool(4)), + getTrackedRace(), /* millisecondsOverWhichToAverage */ 30000, /* waitForLatest */ true); + ScalableWind windSum = null; + int count = 0; + windTrack.lockForRead(); + try { + for (Wind wind : windTrack.getFixes()) { + final ScalableWind scalableWind = new ScalableWind(wind, /* useSpeed */ true); + if (windSum == null) { + windSum = scalableWind; + } else { + windSum = windSum.add(scalableWind); + } + count++; + } + } finally { + windTrack.unlockAfterRead(); + } + Wind average = windSum.divide(count); + return average; + } +} diff --git a/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/windestimation/test/TestWindEstimationFromManeuversOnAFew505Races.java b/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/windestimation/test/TestWindEstimationFromManeuversOnAFew505Races.java deleted file mode 100755 index e4e19ec6fff..00000000000 --- a/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/windestimation/test/TestWindEstimationFromManeuversOnAFew505Races.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.sap.sailing.polars.windestimation.test; - -import static org.junit.Assert.assertEquals; - -import java.io.File; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.text.ParseException; -import java.util.concurrent.Executors; - -import org.junit.Ignore; -import org.junit.Test; - -import com.sap.sailing.domain.test.OnlineTracTracBasedTest; -import com.sap.sailing.domain.tracking.Wind; -import com.sap.sailing.domain.tracking.impl.ScalableWind; -import com.sap.sailing.domain.tractracadapter.ReceiverType; -import com.sap.sailing.polars.impl.PolarDataServiceImpl; -import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException; -import com.sap.sailing.polars.windestimation.ManeuverBasedWindEstimationTrackImpl; -import com.tractrac.model.lib.api.event.CreateModelException; -import com.tractrac.subscription.lib.api.SubscriberInitializationException; - -public class TestWindEstimationFromManeuversOnAFew505Races extends OnlineTracTracBasedTest { - - public TestWindEstimationFromManeuversOnAFew505Races() throws URISyntaxException, MalformedURLException { - super(); - } - - public void setUp(final String fileBaseName) throws MalformedURLException, IOException, InterruptedException, URISyntaxException, - ParseException, SubscriberInitializationException, CreateModelException { - super.setUp(); - URI storedUri = new URI("file:///"+new File("../com.sap.sailing.domain.test/resources/"+fileBaseName+".mtb").getCanonicalPath().replace('\\', '/')); - super.setUp(new URL("file:///"+new File("../com.sap.sailing.domain.test/resources/"+fileBaseName+".txt").getCanonicalPath()), - /* liveUri */ null, /* storedUri */ storedUri, - new ReceiverType[] { ReceiverType.RACECOURSE, ReceiverType.RAWPOSITIONS }); // only the tracks; no mark positions, no wind, no mark passings - } - - @Test - public void testWindEstimationFromManeuversOn505KW2011Race2() throws MalformedURLException, IOException, - InterruptedException, URISyntaxException, ParseException, SubscriberInitializationException, - CreateModelException, NotEnoughDataHasBeenAddedException { - setUp("event_20110609_KielerWoch-505_Race_2"); - Wind average = getManeuverBasedAverageWind(); - assertEquals(235, average.getFrom().getDegrees(), 5.0); // wind in this race was from 075deg on average - } - - @Ignore("The test is currently still red because the clustering doesn't work; see bug 1562 comment #8") - @Test - public void testWindEstimationFromManeuversOn505KW2011Race3() throws MalformedURLException, IOException, - InterruptedException, URISyntaxException, ParseException, SubscriberInitializationException, - CreateModelException, NotEnoughDataHasBeenAddedException { - setUp("event_20110609_KielerWoch-505_Race_3"); - Wind average = getManeuverBasedAverageWind(); - assertEquals(245, average.getFrom().getDegrees(), 5.0); // wind in this race was from 075deg on average - } - - @Ignore("The test is currently still red because the clustering doesn't work; see bug 1562 comment #8") - @Test - public void testWindEstimationFromManeuversOn505KW2011Race4() throws MalformedURLException, IOException, - InterruptedException, URISyntaxException, ParseException, SubscriberInitializationException, - CreateModelException, NotEnoughDataHasBeenAddedException { - setUp("event_20110609_KielerWoch-505_Race_4"); - Wind average = getManeuverBasedAverageWind(); - assertEquals(275, average.getFrom().getDegrees(), 5.0); // wind in this race was from 075deg on average - } - - private Wind getManeuverBasedAverageWind() throws NotEnoughDataHasBeenAddedException { - ManeuverBasedWindEstimationTrackImpl windTrack = new ManeuverBasedWindEstimationTrackImpl(new PolarDataServiceImpl(Executors.newFixedThreadPool(4)), - getTrackedRace(), /* millisecondsOverWhichToAverage */ 30000, /* waitForLatest */ true); - ScalableWind windSum = null; - int count = 0; - windTrack.lockForRead(); - try { - for (Wind wind : windTrack.getFixes()) { - final ScalableWind scalableWind = new ScalableWind(wind, /* useSpeed */ true); - if (windSum == null) { - windSum = scalableWind; - } else { - windSum = windSum.add(scalableWind); - } - count++; - } - } finally { - windTrack.unlockAfterRead(); - } - Wind average = windSum.divide(count); - return average; - } -} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/PolarDataService.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/PolarDataService.java index 9dfb90c7b44..f11d2fc38cb 100755 --- a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/PolarDataService.java +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/PolarDataService.java @@ -10,6 +10,7 @@ import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence; import com.sap.sailing.domain.base.SpeedWithConfidence; import com.sap.sailing.domain.common.Bearing; import com.sap.sailing.domain.common.LegType; +import com.sap.sailing.domain.common.ManeuverType; import com.sap.sailing.domain.common.PolarSheetGenerationSettings; import com.sap.sailing.domain.common.PolarSheetsData; import com.sap.sailing.domain.common.Speed; @@ -18,6 +19,7 @@ import com.sap.sailing.domain.tracking.GPSFixMoving; import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.polars.analysis.PolarSheetAnalyzer; import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException; +import com.sap.sse.common.Util.Pair; /** * Public Facade interface granting clients access to the polar sheets of {@link BoatClass}es. A boat's "polar sheet" @@ -31,6 +33,7 @@ import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException; * which also returns the average angle for the parameters provided. * * @author Frederik Petersen (D054528) + * @autho Axel Uhl (D043530) * */ public interface PolarDataService { @@ -141,4 +144,24 @@ public interface PolarDataService { */ Set> getAverageTrueWindSpeedAndAngleCandidates(BoatClass boatClass, Speed speedOverGround, LegType legType, Tack tack); + /** + * @param intoTackSpeed speed before the tack maneuver + * @param intoJibeSpeed speed before the jibe maneuver + * @param boatClass the boat class for which to calculate the confidence + * @return the likelihood of the ratio between tack and jibe speed, measured at the beginning + * of the maneuver; for example, if the tack speed is much faster than the jibe speed, this seems + * suspicious and will for most polar sheets for most boat classes result in a very low confidence. + * However, if the tack/jibe speed ratio matches up well with what the polar diagram says, a high + * probability will result. The resulting value will be between 0..1 (inclusive). + */ + double getConfidenceForTackJibeSpeedRatio(Speed intoTackSpeed, Speed intoJibeSpeed, BoatClass boatClass); + + /** + * Assuming a boat of the boatClass sailed at speedOverGround and during the maneuver + * changed course by courseChange, how likely was that a maneuver of type maneuverType? + * + * @return a probability between 0..1 (inclusive) in the {@link Pair#getA() first} component, and the true wind + * speed and true wind angle in the {@link Pair#getB() second} component. + */ + Pair> getManeuverLikelihoodAndTwsTwa(BoatClass boatClass, Speed speedOverGround, double courseChangeDeg, ManeuverType maneuverType); } diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/impl/PolarDataServiceImpl.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/impl/PolarDataServiceImpl.java index cc9f52f887c..3c9528b8c76 100755 --- a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/impl/PolarDataServiceImpl.java +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/impl/PolarDataServiceImpl.java @@ -12,6 +12,7 @@ import com.sap.sailing.domain.base.SpeedWithConfidence; import com.sap.sailing.domain.base.impl.SpeedWithBearingWithConfidenceImpl; import com.sap.sailing.domain.common.Bearing; import com.sap.sailing.domain.common.LegType; +import com.sap.sailing.domain.common.ManeuverType; import com.sap.sailing.domain.common.PolarSheetGenerationSettings; import com.sap.sailing.domain.common.PolarSheetsData; import com.sap.sailing.domain.common.Speed; @@ -27,6 +28,7 @@ import com.sap.sailing.polars.data.PolarFix; import com.sap.sailing.polars.generation.PolarSheetGenerator; import com.sap.sailing.polars.mining.PolarDataMiner; import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException; +import com.sap.sse.common.Util.Pair; import com.sap.sse.util.SmartFutureCache; /** @@ -130,4 +132,42 @@ public class PolarDataServiceImpl implements PolarDataService { return polarDataMiner.getDataCountsForWindSpeed(boatClass, windSpeed, startAngleInclusive, endAngleExclusive); } + @Override + public double getConfidenceForTackJibeSpeedRatio(Speed intoTackSpeed, Speed intoJibeSpeed, BoatClass boatClass) { + return Math.min(1., 0.5*intoJibeSpeed.getKnots()/intoTackSpeed.getKnots()); + } + + @Override + public Pair> getManeuverLikelihoodAndTwsTwa(BoatClass boatClass, Speed speedAtManeuverStart, double courseChangeDeg, + ManeuverType maneuverType) { + assert maneuverType == ManeuverType.TACK || maneuverType == ManeuverType.JIBE; + SpeedWithBearingWithConfidence closestTwsTwa = getClosestTwaTws(maneuverType, speedAtManeuverStart, courseChangeDeg, boatClass); + final Pair> result; + if (closestTwsTwa == null) { + result = new Pair<>(0.0, null); + } else { + double minDiffDeg = Math.abs(Math.abs(Math.abs(closestTwsTwa.getObject().getBearing().getDegrees() * 2) + - Math.abs(courseChangeDeg))); + result = new Pair<>(1. / (1. + (minDiffDeg / 10.) * (minDiffDeg / 10.)), closestTwsTwa); + } + return result; + } + + private SpeedWithBearingWithConfidence getClosestTwaTws(ManeuverType type, Speed speedAtManeuverStart, double courseChangeDeg, BoatClass boatClass) { + assert type == ManeuverType.TACK || type == ManeuverType.JIBE; + double minDiff = Double.MAX_VALUE; + SpeedWithBearingWithConfidence closestTwsTwa = null; + for (SpeedWithBearingWithConfidence trueWindSpeedAndAngle : getAverageTrueWindSpeedAndAngleCandidates( + boatClass, speedAtManeuverStart, + type == ManeuverType.TACK ? LegType.UPWIND : LegType.DOWNWIND, + type == ManeuverType.TACK ? courseChangeDeg >= 0 ? Tack.PORT : Tack.STARBOARD + : courseChangeDeg >= 0 ? Tack.STARBOARD : Tack.PORT)) { + double diff = Math.abs(trueWindSpeedAndAngle.getObject().getBearing().getDegrees()*2)-Math.abs(courseChangeDeg); + if (diff < minDiff) { + minDiff = diff; + closestTwsTwa = trueWindSpeedAndAngle; + } + } + return closestTwsTwa; + } } diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/windestimation/ManeuverBasedWindEstimationTrackImpl.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/windestimation/ManeuverBasedWindEstimationTrackImpl.java index 0e7c5a2830b..98a38069681 100755 --- a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/windestimation/ManeuverBasedWindEstimationTrackImpl.java +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/windestimation/ManeuverBasedWindEstimationTrackImpl.java @@ -4,12 +4,15 @@ import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; +import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; +import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence; import com.sap.sailing.domain.base.Waypoint; @@ -22,7 +25,10 @@ import com.sap.sailing.domain.common.ManeuverType; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.Speed; import com.sap.sailing.domain.common.SpeedWithBearing; -import com.sap.sailing.domain.common.Tack; +import com.sap.sailing.domain.common.confidence.ConfidenceBasedAverager; +import com.sap.sailing.domain.common.confidence.ConfidenceFactory; +import com.sap.sailing.domain.common.confidence.HasConfidence; +import com.sap.sailing.domain.common.confidence.impl.ScalableDouble; import com.sap.sailing.domain.common.impl.DegreeBearingImpl; import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; import com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing; @@ -51,39 +57,58 @@ import com.sap.sse.util.kmeans.KMeansMappingClusterer; public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { private static final long serialVersionUID = -764156498143531475L; + private static final Logger logger = Logger.getLogger(ManeuverBasedWindEstimationTrackImpl.class.getName()); + /** * Using a fairly low base confidence for this estimation wind track. It shall only serve as a * default and should be superseded by other wind sources easily. */ private static final double DEFAULT_BASE_CONFIDENCE = 0.01; + /** + * Trying to find the two tack clusters, for each cluster the opposite tack cluster is looked for. The likelihood + * for another cluster to be a cluster's opposite tack cluster is determined by first computing the + * {@link #getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG(Cluster, double, Bearing) angular + * distance}. For each so many degrees of distance as specified by this constant, the likelihood of the cluster to be the + * opposite tack cluster is halved. + */ + private static final double ANGULAR_DISTANCE_FOR_HALF_CONFIDENCE_FOR_OPPOSITE_TACK_CLUSTER_DEG = 20.; + /** * We're trying to find the jibe cluster based on a candidate for the tack cluster. This constant tells how many degrees * the jibe cluster may be off from the reversed tack cluster's centroid in order to be accepted as jibe cluster candidate. */ - private static final double THRESHOLD_OPPOSITE_CLUSTER_DIFFERENCE_DEGREES = 20; + private static final double THRESHOLD_JIBE_CLUSTER_DIFFERENCE_DEGREES = 20; private final PolarDataService polarService; private final TrackedRace trackedRace; + + /** + * Caches the {@link #getWeightedAverageMiddleManeuverCOGDeg(Cluster, ManeuverType)} results because these values are required several times to + * determine the likelihood with which a cluster is a cluster of tacks or a cluster of jibes. Note that the un-weighted averages can simply be obtained + * by asking the cluster for its {@link Cluster#getCentroid() centroid}. + */ + private final Map, Pair, ScalableBearingAndScalableDouble>, ManeuverType>, Bearing> + weightedAverageMiddleCOGForManeuverType; /** * Keeps the clusters that were used during the last {@link #analyzeRace(boolean)} run; this is for debugging, mostly, * and could be removed/commented again when this class works as expected. */ - private final Set> clusters; + private final Set, Pair, ScalableBearingAndScalableDouble>> clusters; /** * References one element of {@link #clutsers} that was selected as the tack cluster; this is for debugging, mostly, * and could be removed/commented again when this class works as expected. */ - private final Cluster tackCluster; + private final List, Pair, ScalableBearingAndScalableDouble>> tackClusters; /** * References one element of {@link #clusters} that was selected as the jibe cluster; this is for debugging, mostly, * and could be removed/commented again when this class works as expected. */ - private final Cluster jibeCluster; + private final List, Pair, ScalableBearingAndScalableDouble>> jibeClusters; public ManeuverBasedWindEstimationTrackImpl(PolarDataService polarService, TrackedRace trackedRace, long millisecondsOverWhichToAverage, boolean waitForLatest) throws NotEnoughDataHasBeenAddedException { @@ -91,12 +116,16 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { /* nameForReadWriteLock */ ManeuverBasedWindEstimationTrackImpl.class.getName()); this.polarService = polarService; this.trackedRace = trackedRace; - Triple>, - Cluster, - Cluster> clusterStructure = analyzeRace(waitForLatest); + this.weightedAverageMiddleCOGForManeuverType = new HashMap<>(); + long start = System.currentTimeMillis(); + Triple, Pair, ScalableBearingAndScalableDouble>>, + List, Pair, ScalableBearingAndScalableDouble>>, + List, Pair, ScalableBearingAndScalableDouble>>> clusterStructure = + analyzeRace(waitForLatest); + logger.fine("Computed virtual wind fixes from maneuvers for race "+trackedRace.getRace().getName()+" in "+(System.currentTimeMillis()-start)+"ms"); clusters = clusterStructure.getA(); - tackCluster = clusterStructure.getB(); - jibeCluster = clusterStructure.getC(); + tackClusters = clusterStructure.getB(); + jibeClusters = clusterStructure.getC(); } /** @@ -117,8 +146,8 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { private final SpeedWithBearing speedAtManeuverStart; private final Bearing middleManeuverCourse; private final Distance maneuverLoss; - private final SpeedWithBearingWithConfidence mostLikelyTackTwsTwa; - private final SpeedWithBearingWithConfidence mostLikelyJibeTwsTwa; + private Pair>[] likelihoodAndTWSBasedOnSpeedAndAngleCache; + private ScalableBearingAndScalableDouble scalableMiddleManeuverCourseAndManeuverAngleDegCache; protected ManeuverClassification(Competitor competitor, Maneuver maneuver) { super(); @@ -129,26 +158,9 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { this.speedAtManeuverStart = maneuver.getSpeedWithBearingBefore(); this.middleManeuverCourse = maneuver.getSpeedWithBearingBefore().getBearing().middle(maneuver.getSpeedWithBearingAfter().getBearing()); this.maneuverLoss = maneuver.getManeuverLoss(); - this.mostLikelyTackTwsTwa = getClosestTwaTws(ManeuverType.TACK); - this.mostLikelyJibeTwsTwa = getClosestTwaTws(ManeuverType.JIBE); - } - - private SpeedWithBearingWithConfidence getClosestTwaTws(ManeuverType type) { - assert type == ManeuverType.TACK || type == ManeuverType.JIBE; - double minDiff = Double.MAX_VALUE; - SpeedWithBearingWithConfidence closestTwsTwa = null; - for (SpeedWithBearingWithConfidence trueWindSpeedAndAngle : polarService.getAverageTrueWindSpeedAndAngleCandidates( - getCompetitor().getBoat().getBoatClass(), getSpeedAtManeuverStart(), - type == ManeuverType.TACK ? LegType.UPWIND : LegType.DOWNWIND, - type == ManeuverType.TACK ? getManeuverAngleDeg() >= 0 ? Tack.PORT : Tack.STARBOARD - : getManeuverAngleDeg() >= 0 ? Tack.STARBOARD : Tack.PORT)) { - double diff = Math.abs(trueWindSpeedAndAngle.getObject().getBearing().getDegrees()*2)-Math.abs(getManeuverAngleDeg()); - if (diff < minDiff) { - minDiff = diff; - closestTwsTwa = trueWindSpeedAndAngle; - } - } - return closestTwsTwa; + @SuppressWarnings("unchecked") + Pair>[] myLikelihoodAndTWSBasedOnSpeedAndAngleCache = (Pair>[]) new Pair[ManeuverType.values().length]; + this.likelihoodAndTWSBasedOnSpeedAndAngleCache = myLikelihoodAndTWSBasedOnSpeedAndAngleCache; } public Competitor getCompetitor() { @@ -174,9 +186,18 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { public Bearing getMiddleManeuverCourse() { return middleManeuverCourse; } + + public ScalableBearingAndScalableDouble getScalableMiddleManeuverCourseAndManeuverAngleDeg() { + if (scalableMiddleManeuverCourseAndManeuverAngleDegCache == null) { + scalableMiddleManeuverCourseAndManeuverAngleDegCache = new ScalableBearingAndScalableDouble( + getMiddleManeuverCourse(), getManeuverAngleDeg()); + } + return scalableMiddleManeuverCourseAndManeuverAngleDegCache; + } public SpeedWithBearingWithConfidence getEstimatedWindSpeedAndBearing(ManeuverType maneuverType) { - Pair> likelihoodAndTWSBasedOnSpeedAndAngle = getLikelihoodAndTWSBasedOnSpeedAndAngle(maneuverType); + Pair> likelihoodAndTWSBasedOnSpeedAndAngle = polarService.getManeuverLikelihoodAndTwsTwa( + getCompetitor().getBoat().getBoatClass(), getSpeedAtManeuverStart(), getManeuverAngleDeg(), maneuverType); final SpeedWithBearingWithConfidenceImpl result; // if no reasonable wind speed was found for the maneuver speed, return null if (likelihoodAndTWSBasedOnSpeedAndAngle.getB() == null) { @@ -216,20 +237,12 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { * * @return a value between 0 and 1 */ - public Pair> getLikelihoodAndTWSBasedOnSpeedAndAngle(final ManeuverType type) { - assert type == ManeuverType.TACK || type == ManeuverType.JIBE; - SpeedWithBearingWithConfidence closestTwsTwa = type == ManeuverType.TACK ? this.mostLikelyTackTwsTwa : this.mostLikelyJibeTwsTwa; - final Pair> result; - if (closestTwsTwa == null) { - result = new Pair<>(0.0, null); - } else { - double minDiffDeg = Math.abs(Math.abs(Math.abs(closestTwsTwa.getObject().getBearing().getDegrees() * 2) - - Math.abs(getManeuverAngleDeg()))); - // TODO ask polar service how likely this is because the polar service knows how narrow or wide the - // maneuver angle range is - result = new Pair<>(1. / (1. + (minDiffDeg / 10.) * (minDiffDeg / 10.)), closestTwsTwa); + public Pair> getLikelihoodAndTWSBasedOnSpeedAndAngle(final ManeuverType maneuverType) { + if (likelihoodAndTWSBasedOnSpeedAndAngleCache[maneuverType.ordinal()] == null) { + likelihoodAndTWSBasedOnSpeedAndAngleCache[maneuverType.ordinal()] = polarService.getManeuverLikelihoodAndTwsTwa(getCompetitor().getBoat().getBoatClass(), + getSpeedAtManeuverStart(), getManeuverAngleDeg(), maneuverType); } - return result; + return likelihoodAndTWSBasedOnSpeedAndAngleCache[maneuverType.ordinal()]; } @Override @@ -240,11 +253,11 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { } public String getManeuverClassificationColumnHeaders() { - return "datapoint\tcompetitor\ttimePoint\tangleDeg\tboatSpeedKn\tcogDeg\tmiddleManeuverCourse\tlossM"; + return "datapoint\tcompetitor\ttimePoint\tangleDeg\tboatSpeedKn\tcogDeg\tmiddleManeuverCourse\tlossM\ttackLikelihood\tjibeLikelihood"; } public String getManeuverClassificationColumnTypes() { - return "infoitem\tstring\tdate\tfloat\tfloat\tfloat\tfloat\tfloat"; + return "infoitem\tstring\tdate\tfloat\tfloat\tfloat\tfloat\tfloat\tfloat\tfloat"; } public String format(ManeuverClassification mc, String id) { @@ -263,9 +276,13 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { result.append(mc.getMiddleManeuverCourse().getDegrees()); result.append("\t"); result.append(mc.getManeuverLoss() == null ? 0.0 : mc.getManeuverLoss().getMeters()); + result.append("\t"); + result.append(mc.getLikelihoodAndTWSBasedOnSpeedAndAngle(ManeuverType.TACK).getA()); + result.append("\t"); + result.append(mc.getLikelihoodAndTWSBasedOnSpeedAndAngle(ManeuverType.JIBE).getA()); return result.toString(); } - + /** * Fetches all the race's maneuvers and tries to find the tacks and jibes. For each such maneuver identified, a wind * fix will be created based on the average COG into and out of the maneuver. @@ -321,145 +338,254 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { * * TODO with enough candidates at hand, apply temporal and spatial segmentation to account for heterogeneous wind fields */ - private Triple>, - Cluster, - Cluster> analyzeRace(boolean waitForLatest) throws NotEnoughDataHasBeenAddedException { + private Triple, Pair, ScalableBearingAndScalableDouble>>, + List, Pair, ScalableBearingAndScalableDouble>>, + List, Pair, ScalableBearingAndScalableDouble>>> analyzeRace( + boolean waitForLatest) throws NotEnoughDataHasBeenAddedException { final Map maneuvers = getAllManeuvers(waitForLatest); // cluster into eight clusters by middle COG first, then aggregate tack likelihoods for each, and jibe likelihoods for opposite cluster - final int numberOfClusters = 8; - KMeansMappingClusterer clusterer = + final int numberOfClusters = 16; + KMeansMappingClusterer, Pair, ScalableBearingAndScalableDouble> clusterer = new KMeansMappingClusterer<>(numberOfClusters, getManeuverClassifications(maneuvers), - (mc)->new ScalableBearing(mc.getMiddleManeuverCourse()), // maps maneuver classification to cluster metric + (mc)->mc.getScalableMiddleManeuverCourseAndManeuverAngleDeg(), // maps maneuver classification to cluster metric // use an evenly distributed set of cluster seeds for clustering wind direction estimations - IntStream.range(0, numberOfClusters).mapToObj((i)->new DegreeBearingImpl(((double) i)*360./(double) numberOfClusters))); - final Set> clusters = clusterer.getClusters(); + Stream.concat(IntStream.range(0, numberOfClusters/2).mapToObj((i)-> + new Pair<>(new DegreeBearingImpl(((double) i)*360./(double) numberOfClusters/2), 45.)), + IntStream.range(0, numberOfClusters/2).mapToObj((i)-> + new Pair<>(new DegreeBearingImpl(((double) i)*360./(double) numberOfClusters/2), -45.)))); + logger.fine("K-Means maneuver clusterer for wind estimation took "+clusterer.getNumberOfIterations()+" iterations"); + final Set, Pair, ScalableBearingAndScalableDouble>> clusters = clusterer.getClusters(); assert maneuvers.size() == clusters.stream().map((c)->c.size()).reduce((s1, s2)->s1+s2).get(); - final Set, Double>> clustersAndLikelihoodOfBeingTackCluster = - clusters.stream().map((c)->new Pair<>(c, getLikelihoodIsTackCluster(c, clusters))).collect(Collectors.toSet()); - // find most likely tack cluster, based on proximity of maneuver angles in cluster and opposite cluster as well as the speed ratio - // between the cluster and its opposite jibe cluster candidate - Pair, Double> dominantClusterAndLikelihoodOfBeingTackCluster = - clustersAndLikelihoodOfBeingTackCluster.stream().max( - (a, b)->(int) Math.signum(a.getB() - b.getB())).get(); - addWindFixes(dominantClusterAndLikelihoodOfBeingTackCluster.getA(), ManeuverType.TACK); - Cluster jibeCluster = getOppositeCluster(dominantClusterAndLikelihoodOfBeingTackCluster.getA(), clusters); - if (jibeCluster != null) { - addWindFixes(jibeCluster, ManeuverType.JIBE); + // Now work towards identifying the two tack clusters + final Map, Pair, ScalableBearingAndScalableDouble>, Double> + likelihoodOfBeingTackCluster = new HashMap<>(); + for (Cluster, Pair, ScalableBearingAndScalableDouble> c : clusters) { + likelihoodOfBeingTackCluster.put(c, getLikelihoodOfBeingTackCluster(c, clusters)); } - return new Triple<>(clusters, dominantClusterAndLikelihoodOfBeingTackCluster.getA(), jibeCluster); + // The first two clusters in the resulting list are now our best bet for the tack clusters, assuming that the likelihood for the third element + // in the list is already much lower. Should there be a significant degradation of likelihood between the first and the second cluster, we + // will only use the first, particularly if the average middle COGs of the first two clusters differ significantly. + final List, Pair, ScalableBearingAndScalableDouble>> tackClusters = + clusters.stream() + .sorted((c1, c2) -> (int) -Math.signum(likelihoodOfBeingTackCluster.get(c1) // TODO cache result of getLikelihoodOfBeingTackCluster + - likelihoodOfBeingTackCluster.get(c2))).limit(2).collect(Collectors.toList()); + addWindFixes(tackClusters.stream(), ManeuverType.TACK); + Bearing estimatedJibeMiddleCOG = + tackClusters.isEmpty() ? null + : (tackClusters.size() == 1 ? getWeightedAverageMiddleManeuverCOGDeg(tackClusters.get(0), ManeuverType.TACK) + : new ScalableBearing(getWeightedAverageMiddleManeuverCOGDeg(tackClusters.get(0), ManeuverType.TACK)).add( + new ScalableBearing(getWeightedAverageMiddleManeuverCOGDeg(tackClusters.get(1), ManeuverType.TACK))). + divide(2)).reverse(); + final List, Pair, ScalableBearingAndScalableDouble>> jibeClusters = + getJibeClusters(estimatedJibeMiddleCOG, clusters).collect(Collectors.toList()); + addWindFixes(jibeClusters.stream(), ManeuverType.JIBE); + return new Triple<>(clusters, tackClusters, jibeClusters); } private void addWindFixes( - Cluster dominantClusterAndLikelihoodOfBeingTackCluster, + Stream, Pair, ScalableBearingAndScalableDouble>> clusters, final ManeuverType maneuverType) { // TODO see bug 1562 comment #8: use WindWithConfidenceImpl internally and implement WindTrack.getAveragedWindWithConfidence - for (ManeuverClassification mc : dominantClusterAndLikelihoodOfBeingTackCluster) { - final SpeedWithBearingWithConfidence estimatedWindSpeedAndBearing = mc.getEstimatedWindSpeedAndBearing(maneuverType); - if (estimatedWindSpeedAndBearing != null) { - Wind windFromTack = new WindImpl(mc.getPosition(), mc.getTimePoint(), estimatedWindSpeedAndBearing.getObject()); - add(windFromTack); + clusters.forEach((cluster) -> { + for (ManeuverClassification mc : cluster) { + final SpeedWithBearingWithConfidence estimatedWindSpeedAndBearing = mc.getEstimatedWindSpeedAndBearing(maneuverType); + if (estimatedWindSpeedAndBearing != null) { + Wind windFromTack = new WindImpl(mc.getPosition(), mc.getTimePoint(), estimatedWindSpeedAndBearing + .getObject()); + add(windFromTack); + } } - } + }); } /** - * For the tackClusterCandidate add up the likelihoods for each maneuver contained that it is a tack. - * If an opposite jibe cluster is found whose centroid middle COG is closer than - * {@link #THRESHOLD_OPPOSITE_CLUSTER_DIFFERENCE_DEGREES} degrees to the expected value, its maneuvers are assessed - * as jibes and their probabilities are added, making the tackClusterCandidate more likely as there are - * more likely jibe candidates in the jibe cluster candidate. - *

+ * Determines the likelihood of cluster being a cluster of tacks, based on the average likelihood of + * the maneuver classifications in cluster to be maneuvers of type {@link ManeuverType#TACK TACK}, in + * turn based on what the {@link #polarService} thinks that this likelihood is and based on whether we find other + * clusters that support this hypothesis. The likelihood is positively affected * - * If a jibe cluster candidate is found, the resulting number is scaled by the likelihood that the average speed - * difference (weighted by the likelihood of being the expected maneuver type) between tack and jibe cluster makes - * sense. Otherwise, the value is scaled down by a factor of .25 as a penalty for not having found a jibe cluster. + *

    + *
  • if a second cluster with similar average middle COG and a similar average maneuver angle with opposite sign + * is found that also has a high average likelihood of the maneuvers contained being tacks
  • + *
  • if other clusters are found that for the average true wind speed estimated from the maneuvers in + * cluster seem like head-up/bear-away clusters on port/starboard tack with an average middle COG that + * is half the tacking angle for that wind speed away from the tack cluster's average middle COG
  • + *
  • if clusters with approximately reversed average middle COG are found, weighted by the maneuvers' likelihood + * to be jibes
  • + *
  • if the jibe cluster(s) are surrounded by head-up/bear-away clusters on port/starboard tack with their average + * middle COG being approximately half the jibing angle at the average jibe maneuvers' wind speed away from the jibe + * clusters' weighted average middle COG
  • + *
  • the jibe clusters' weighted average speed relates to the tack clusters' weighted average speed in a way that + * is supported by the {@link #polarService} (see + * {@link PolarDataService#getConfidenceForTackJibeSpeedRatio(Speed, Speed, BoatClass)}); background: for many boat + * classes and many wind conditions, jibes have a significantly higher entry speed than tacks and can hence be kept + * apart from the tacks even if they have similar maneuver angles.
  • + *
* - * @return not a 0..1 likelihood but some scaled value that can grow beyond 1 and represents the sum for all - * relevant tackClusterCandidate maneuvers' likelihood that they are a tack, added to the - * likelihood sum for an opposite cluster that its maneuvers are jibes, scaled by the probability that the - * speed ratio between tack and jibe cluster makes sense (or .25 in case there is no jibe cluster candidate - * found within the threshold). + * @return a likelihood between 0..1 (inclusive) + * + * @see ManeuverClassification#getLikelihoodAndTWSBasedOnSpeedAndAngle(ManeuverType) + * @see PolarDataService#getManeuverLikelihoodAndTwsTwa(BoatClass, Speed, double, ManeuverType) */ - private double getLikelihoodIsTackCluster(Cluster tackClusterCandidate, - Set> clusters) { - double tackClusterLikelihood = 0; - for (ManeuverClassification mc : tackClusterCandidate) { - tackClusterLikelihood += mc.getLikelihoodAndTWSBasedOnSpeedAndAngle(ManeuverType.TACK).getA(); - } - // TODO the cluster is more likely a tack cluster if two maneuver angle clusters are found that have small variance (no "into-the-wind" maneuvers other than tacks) - Cluster jibeClusterCandidate = getOppositeCluster(tackClusterCandidate, clusters); - final double speedMatchFactor; - double jibeClusterLikelihood = 0; - if (jibeClusterCandidate != null) { - for (ManeuverClassification mc : jibeClusterCandidate) { - jibeClusterLikelihood += mc.getLikelihoodAndTWSBasedOnSpeedAndAngle(ManeuverType.JIBE).getA(); - } - speedMatchFactor = getLikelihoodTackJibeSpeedRatio(tackClusterCandidate, jibeClusterCandidate); - } else { - speedMatchFactor = .25; // no jibe cluster found; penalize for not being able to compare average speeds between tacks and jibes - } - return speedMatchFactor * tackClusterLikelihood * jibeClusterLikelihood; - } + private double getLikelihoodOfBeingTackCluster( + Cluster, Pair, ScalableBearingAndScalableDouble> cluster, + Set, Pair, ScalableBearingAndScalableDouble>> clusters) { + // start with the average of all of the cluster's maneuvers' likelihood to be a tack + double averageTackLikelihood = getAverageLikelihoodOfBeingManeuver(ManeuverType.TACK, cluster.stream()); + final Bearing approximateMiddleCOG = getWeightedAverageMiddleManeuverCOGDeg(cluster, ManeuverType.TACK); + final Bearing approximateMiddleCOGForJibes = approximateMiddleCOG.reverse(); + // search for the opposite tack cluster by finding the one that has the smallest angular distance to the expected + // middle COG (same as for the candidate cluster) and expected maneuver angle (the cluster's average maneuver angle with inverted sign) + final Cluster, Pair, ScalableBearingAndScalableDouble> oppositeTackCluster = + clusters.stream().filter((c)->c!=cluster).min((a, b)->(int) Math.signum( + getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG(a, -cluster.getCentroid().getB(), approximateMiddleCOG)- + getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG(b, -cluster.getCentroid().getB(), approximateMiddleCOG))). + orElse(null); + final double likelihoodOfOppositeCluster = getLikelihoodOfOppositeTackCluster(-cluster.getCentroid().getB(), approximateMiddleCOG, oppositeTackCluster); + + // TODO search for head-up/bear-away cluster(s) to port and to starboard of cluster's weighted middle COG - private Cluster getOppositeCluster( - Cluster cluster, - Set> clusters) { - final Bearing clusterCentroid = cluster.getCentroid(); - final Bearing opposite = clusterCentroid.reverse(); - Cluster oppositeClusterCandidate = - clusters.stream().min((a, b)-> - (int) Math.signum(Math.abs(opposite.getDifferenceTo(a.getCentroid()).getDegrees()) - - Math.abs(opposite.getDifferenceTo(b.getCentroid()).getDegrees()))).get(); - final Cluster result; - if (Math.abs(opposite.getDifferenceTo(oppositeClusterCandidate.getCentroid()).getDegrees()) < THRESHOLD_OPPOSITE_CLUSTER_DIFFERENCE_DEGREES) { - result = oppositeClusterCandidate; + // under the assumption that cluster holds tacks, find the clusters that then most likely hold the jibes + Stream, Pair, ScalableBearingAndScalableDouble>> jibeClusters = + getJibeClusters(approximateMiddleCOGForJibes, clusters); + // increase cluster's score if "good" jibe clusters are found; quality is defined by how well the angle matches and how well the speed ratio fits + Speed tackClusterWeightedAverageSpeed = getWeightedAverageSpeed(cluster.stream(), ManeuverType.TACK); + final double result; + if (tackClusterWeightedAverageSpeed != null) { + // find out how likely the speed ratio is between the candidate tack cluster and the corresponding + // hypothetical jibe clusters + double tackJibeLikelihoodBasedOnSpeedRatioAndAverageManeuverAngle = getLikelihoodOfBeingJibeCluster( + tackClusterWeightedAverageSpeed, jibeClusters.map((jc) -> jc.stream()).reduce(Stream::concat) + .orElse(Stream.empty()), getBoatClass()); + // Likely jibe clusters may raise the general likelihood by up to 20% of the value so far, but not to over 1.0 + result = Math.min(1.0, averageTackLikelihood * likelihoodOfOppositeCluster * + (1.0 + 0.2 * tackJibeLikelihoodBasedOnSpeedRatioAndAverageManeuverAngle)); } else { - result = null; + result = .1; // no elements in the cluster, therefore no average speed; low propability that this would be a tack cluster } return result; } + + /** + * Tells how likely it is that the two clusters are opposite tack clusters. This is determined by looking at the + * {@link #getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG(Cluster, double, Bearing) angular distance} + * between the two clusters regarding their middle COG and their average maneuver angles. The likelihood decreases exponentially + * with increasing angle differences. It is 1.0 for an exact match and reaches 0.5 for an angular distance of 20deg. + */ + private double getLikelihoodOfOppositeTackCluster( + double expectedManeuverAngleDeg, Bearing expectedApproximateMiddleCOG, + Cluster, Pair, ScalableBearingAndScalableDouble> oppositeTackCluster) { + return Math.exp(Math.log(0.5) * (getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG( + oppositeTackCluster, expectedManeuverAngleDeg, expectedApproximateMiddleCOG) / ANGULAR_DISTANCE_FOR_HALF_CONFIDENCE_FOR_OPPOSITE_TACK_CLUSTER_DEG)); + } + + /** + * A euklidian degree distance based on the two degree distances between the maneuver angles and the + * middle COGs. + */ + private double getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG( + Cluster, Pair, ScalableBearingAndScalableDouble> cluster, + double expectedManeuverAngleDeg, Bearing expectedApproximateMiddleCOG) { + final Bearing approximateMiddleCOG = getWeightedAverageMiddleManeuverCOGDeg(cluster, ManeuverType.TACK); + double middleCOGDiff = approximateMiddleCOG.getDifferenceTo(expectedApproximateMiddleCOG).getDegrees(); + double maneuverAngleDiff = cluster.getCentroid().getB() - expectedManeuverAngleDeg; + return Math.sqrt(middleCOGDiff*middleCOGDiff + maneuverAngleDiff*maneuverAngleDiff); + } + + /** + * For an empty stream, 0.0 is returned + */ + private double getAverageLikelihoodOfBeingManeuver(ManeuverType maneuverType, + Stream maneuverClassifications) { + return maneuverClassifications + .mapToDouble((mc)->mc.getLikelihoodAndTWSBasedOnSpeedAndAngle(maneuverType).getA()).average().orElse(0.0); + } + + private BoatClass getBoatClass() { + return trackedRace.getRace().getBoatClass(); + } + + /** + * Returns at most two clusters that hold maneuvers with the highest average probability of being a jibe and whose + * {@link #getWeightedAverageMiddleManeuverCOGDeg(Cluster, ManeuverType) weighted average middle COG} fits that of + * the expected value (approximateMiddleCOGForJibes) within + * {@link #THRESHOLD_JIBE_CLUSTER_DIFFERENCE_DEGREES} degrees. + */ + private Stream, Pair, ScalableBearingAndScalableDouble>> getJibeClusters( + final Bearing approximateMiddleCOGForJibes, + Set, Pair, ScalableBearingAndScalableDouble>> clusters) { + // TODO the result of getAverageLikelihoodOfBeingManeuver(ManeuverType.JIBE, a.stream()) should be cached + return clusters.stream().sorted((a, b)-> + (int) -Math.signum(getAverageLikelihoodOfBeingManeuver(ManeuverType.JIBE, a.stream()) - + getAverageLikelihoodOfBeingManeuver(ManeuverType.JIBE, b.stream()))).filter( + (jibeClusterCandidate)->Math.abs(getWeightedAverageMiddleManeuverCOGDeg(jibeClusterCandidate, ManeuverType.JIBE). + getDifferenceTo(approximateMiddleCOGForJibes).getDegrees()) < THRESHOLD_JIBE_CLUSTER_DIFFERENCE_DEGREES). + limit(2); + } + /** * By looking at the weighted speeds over ground averages, and based on the {@link #polarService} determines how - * likely it is that the tack cluster actually contains tacks and the jibe cluster jibes. This is based on the idea - * that if for the most likely wind speeds the jibes have a different (usually significantly higher) entry speed - * than the tacks then this difference show show in the cluster elements. If they don't, then this should give a - * penalty for the cluster likelihoods. + * likely it is that the jibe cluster contents are actually jibes. This is based on the idea that if for the most + * likely wind speeds the jibes have a different (usually significantly higher) entry speed than the tacks then this + * difference show show in the cluster elements. If they don't, then this should give a penalty for the cluster + * likelihoods. *

* * The weight of a maneuver for computing the weighted speed average is determined to be the - * {@link ManeuverClassification#getLikelihoodAndTWSBasedOnSpeedAndAngle(ManeuverType) likelihood of the maneuver being - * what it is assumed to be}. + * {@link ManeuverClassification#getLikelihoodAndTWSBasedOnSpeedAndAngle(ManeuverType) likelihood of the maneuver + * being what it is assumed to be}. */ - private double getLikelihoodTackJibeSpeedRatio( - Cluster tackClusterCandidate, - Cluster jibeClusterCandidate) { - Speed tackClusterWeightedAverageSpeed = getWeightedAverageSpeed(tackClusterCandidate, ManeuverType.TACK); - Speed jibeClusterWeightedAverageSpeed = getWeightedAverageSpeed(jibeClusterCandidate, ManeuverType.JIBE); - // TODO ask polarService how likely the ratio we found actually is - return Math.min(1., 0.5*jibeClusterWeightedAverageSpeed.getKnots()/tackClusterWeightedAverageSpeed.getKnots()); + private double getLikelihoodOfBeingJibeCluster( + Speed tackClusterWeightedAverageSpeed, + Stream jibeClustersContent, + BoatClass boatClass) { + final int[] count = new int[1]; + final double[] likelihoodSum = new double[1]; + Stream jibeClustersContentPeeker = jibeClustersContent.peek((mc)->{ + count[0]++; + likelihoodSum[0] += mc.getLikelihoodAndTWSBasedOnSpeedAndAngle(ManeuverType.JIBE).getA(); + }); + Speed jibeClusterWeightedAverageSpeed = getWeightedAverageSpeed(jibeClustersContentPeeker, ManeuverType.JIBE); + final double result; + if (jibeClusterWeightedAverageSpeed != null) { + double tackJibeSpeedRatioLikelihood = polarService.getConfidenceForTackJibeSpeedRatio( + tackClusterWeightedAverageSpeed, jibeClusterWeightedAverageSpeed, boatClass); + if (count[0] > 0) { + double averageJibeLikelihood = likelihoodSum[0] / count[0]; + result = averageJibeLikelihood * tackJibeSpeedRatioLikelihood; + } else { + throw new RuntimeException("Internal error: no maneuvers in jibe cluster candidate but still a valid weighted average speed "+jibeClusterWeightedAverageSpeed); + } + } else { + result = 0.5; // no maneuvers in the jibe clusters; could be but may also not be a jibe cluster + } + return result; } - private Speed getWeightedAverageSpeed(Cluster cluster, ManeuverType maneuverType) { - double weightSum = 0; - ScalableSpeed scalableSpeed = new ScalableSpeed(Speed.NULL); - for (ManeuverClassification mc : cluster) { - final Double weight = mc.getLikelihoodAndTWSBasedOnSpeedAndAngle(maneuverType).getA(); - weightSum += weight; - scalableSpeed = scalableSpeed.add(new ScalableSpeed(mc.getSpeedAtManeuverStart()).multiply(weight)); - } - return scalableSpeed.divide(weightSum); + private Speed getWeightedAverageSpeed( + Stream cluster, + ManeuverType maneuverType) { + ConfidenceBasedAverager averager = ConfidenceFactory.INSTANCE.createAverager(/* weigher */ null); + HasConfidence average = averager.getAverage( + cluster.map(new ManeuverClassificationToHasConfidenceAndIsScalableAdapter(maneuverType, + (mc)->new ScalableSpeed(mc.getSpeedAtManeuverStart()), + polarService)).iterator(), /* at */ null); + return average == null ? null : average.getObject(); } /** * For each maneuver from maneuvers, a {@link ManeuverClassification object} is created. */ - private Stream getManeuverClassifications(final Map maneuvers) { + Stream getManeuverClassifications(final Map maneuvers) { return maneuvers.entrySet().stream().map((maneuverAndCompetitor)-> new ManeuverClassification(maneuverAndCompetitor.getValue(), maneuverAndCompetitor.getKey())); } - private Map getAllManeuvers(boolean waitForLatest) { + /** + * Package scope to let test fragment access it + */ + Map getAllManeuvers(boolean waitForLatest) { Map maneuvers = new HashMap<>(); final Waypoint firstWaypoint = trackedRace.getRace().getCourse().getFirstWaypoint(); final Waypoint lastWaypoint = trackedRace.getRace().getCourse().getLastWaypoint(); @@ -478,23 +604,54 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { return Collections.unmodifiableMap(maneuvers); } - public Set> getClusters() { + /** + * For a cluster of maneuver classifications, clustered two-dimensionally by middle COG and maneuver angle, computes the + * weighted average of the middle COG where as the weight of each maneuver the likelihood that the maneuver was of type + * maneuverType is used.

+ * + * The method has default package scope to allow tests in the same package in the test fragment to use it. + * + * @return the weighted average middle COG + */ + Bearing getWeightedAverageMiddleManeuverCOGDeg( + Cluster, Pair, ScalableBearingAndScalableDouble> cluster, + ManeuverType maneuverType) { + Pair, Pair, ScalableBearingAndScalableDouble>, ManeuverType> key = new Pair<>( + cluster, maneuverType); + Bearing result = weightedAverageMiddleCOGForManeuverType.get(key); + if (result == null) { + ConfidenceBasedAverager averager = ConfidenceFactory.INSTANCE + .createAverager(/* weigher */null); + HasConfidence average = averager.getAverage( + cluster.stream() + .map(new ManeuverClassificationToHasConfidenceAndIsScalableAdapter( + maneuverType, (mc) -> new ScalableBearing(mc.getMiddleManeuverCourse()), + polarService)).iterator(), /* at */null); + result = average.getObject(); + weightedAverageMiddleCOGForManeuverType.put(key, result); + } + return result; + } + + public Set, Pair, ScalableBearingAndScalableDouble>> getClusters() { return clusters; } - public Cluster getTackCluster() { - return tackCluster; + public List, Pair, ScalableBearingAndScalableDouble>> getTackClusters() { + return tackClusters; } - public Cluster getJibeCluster() { - return jibeCluster; + public List, Pair, ScalableBearingAndScalableDouble>> getJibeClusters() { + return jibeClusters; } public String getStringRepresentation(boolean waitForLatest) { return getStringRepresentation(getClusters().stream(), waitForLatest); } - public String getStringRepresentation(Stream> clusters, boolean waitForLatest) { + public String getStringRepresentation( + Stream, Pair, ScalableBearingAndScalableDouble>> clusters, + boolean waitForLatest) { StringBuilder stringRepresentation = new StringBuilder(); stringRepresentation.delete(0, stringRepresentation.length()); stringRepresentation.append(getManeuverClassificationColumnHeaders()); diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/windestimation/ManeuverClassificationToHasConfidenceAndIsScalableAdapter.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/windestimation/ManeuverClassificationToHasConfidenceAndIsScalableAdapter.java new file mode 100755 index 00000000000..dd77bcdac4c --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/windestimation/ManeuverClassificationToHasConfidenceAndIsScalableAdapter.java @@ -0,0 +1,74 @@ +package com.sap.sailing.polars.windestimation; + +import java.util.function.Function; + +import com.sap.sailing.domain.common.ManeuverType; +import com.sap.sailing.domain.common.confidence.ConfidenceBasedAverager; +import com.sap.sailing.domain.common.confidence.HasConfidenceAndIsScalable; +import com.sap.sailing.polars.PolarDataService; +import com.sap.sailing.polars.windestimation.ManeuverBasedWindEstimationTrackImpl.ManeuverClassification; +import com.sap.sse.common.scalablevalue.ScalableValue; + +/** + * {@link ManeuverClassification}s need to be aggregated for different figures based on their confidence + * of being a maneuver of a specific {@link ManeuverType type}. This can conveniently be done using a + * {@link ConfidenceBasedAverager} and by adapting the {@link ManeuverClassification} objects to + * what the averaged needs: a {@link HasConfidenceAndIsScalable} implementation.

+ * + * This abstract class only contributes determining the confidence based on the likelihood for the + * maneuver to be of the type specified. Subclasses need to add the logic for extracting the + * + * @author Axel Uhl (D043530) + * + */ +public class ManeuverClassificationToHasConfidenceAndIsScalableAdapter implements + Function> { + private final Function> mapper; + private final ManeuverType maneuverType; + private final PolarDataService polarService; + + /** + * @param maneuverType + * confidences are defined to be the + * {@link PolarDataService#getManeuverLikelihoodAndTwsTwa(com.sap.sailing.domain.base.BoatClass, com.sap.sailing.domain.common.Speed, double, ManeuverType) + * likelihood that the maneuver is of this type}. + * @param mapper + * maps the {@link ManeuverClassification} to the key figure that is a scalable value and that shall be + * averaged by a {@link ConfidenceBasedAverager}. + */ + public ManeuverClassificationToHasConfidenceAndIsScalableAdapter(ManeuverType maneuverType, + Function> mapper, PolarDataService polarService) { + this.maneuverType = maneuverType; + this.mapper = mapper; + this.polarService = polarService; + } + + @Override + public HasConfidenceAndIsScalable apply(final ManeuverClassification t) { + return new HasConfidenceAndIsScalable() { + private static final long serialVersionUID = -5617237614367649676L; + + @Override + public ScalableValue getScalableValue() { + return mapper.apply(t); + } + + @Override + public double getConfidence() { + return polarService.getManeuverLikelihoodAndTwsTwa(t.getCompetitor().getBoat().getBoatClass(), + t.getSpeedAtManeuverStart(), t.getManeuverAngleDeg(), maneuverType).getA(); + } + + @Override + public Void getRelativeTo() { + return null; + } + + @Override + public BaseType getObject() { + return getScalableValue().divide(1); + } + }; + } + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/windestimation/ScalableBearingAndScalableDouble.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/windestimation/ScalableBearingAndScalableDouble.java new file mode 100755 index 00000000000..5a80594a032 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/windestimation/ScalableBearingAndScalableDouble.java @@ -0,0 +1,65 @@ +package com.sap.sailing.polars.windestimation; + +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.confidence.impl.ScalableDouble; +import com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.common.scalablevalue.ScalableValue; +import com.sap.sse.common.scalablevalue.ScalableValueWithDistance; + +/** + * Two {@link Bearings} combined in one scalable object, like a vector of two bearings. {@link #add(ScalableValue) + * Addition} works component wise, and so does {@link #multiply(double) multiplication} and {@link #divide(double) + * division}. The {@link #getDistance(Pair) distance} is calculated as the Euklidian two-dimensional distance between + * the two bearing pairs, with "degrees" as conceptual unit. For example, another object of this type that is 2 and 3 degrees + * apart from this object will have a distance of {@link Math#sqrt(double) Math.sqrt(13)}.

+ * + * The {@link #getDistance(Pair)} implementation uses {@link ScalableBearing#getApproximateDegreeDistanceTo(Bearing)} on + * the scalable bearing part of this object. This is only an approximation but works a lot faster and is still monotonous. + * It works well for small angles. + * + * @author Axel Uhl (D043530) + * + */ +public class ScalableBearingAndScalableDouble implements + ScalableValueWithDistance, Pair> { + private final ScalableBearing b1; + private final ScalableDouble b2; + + public ScalableBearingAndScalableDouble(Bearing b1, double b2) { + this(new ScalableBearing(b1), new ScalableDouble(b2)); + } + + private ScalableBearingAndScalableDouble(ScalableBearing b1, ScalableDouble b2) { + super(); + this.b1 = b1; + this.b2 = b2; + } + + @Override + public Pair divide(double divisor) { + return new Pair<>(b1.divide(divisor), b2.divide(divisor)); + } + + @Override + public Pair getValue() { + return new Pair<>(b1, b2); + } + + @Override + public double getDistance(Pair other) { + final double b1DiffDeg = b1.getApproximateDegreeDistanceTo(other.getA()); + final double b2DiffDeg = Math.abs(b2.divide(1)-other.getB()); + return Math.sqrt(b1DiffDeg*b1DiffDeg + b2DiffDeg*b2DiffDeg); + } + + @Override + public ScalableBearingAndScalableDouble add(ScalableValue, Pair> t) { + return new ScalableBearingAndScalableDouble(b1.add(t.getValue().getA()), b2.add(t.getValue().getB())); + } + + @Override + public ScalableBearingAndScalableDouble multiply(double factor) { + return new ScalableBearingAndScalableDouble(b1.multiply(factor), b2.multiply(factor)); + } +} diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/PolarResource.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/PolarResource.java index 8b758124930..c8af43fc78b 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/PolarResource.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/PolarResource.java @@ -21,10 +21,10 @@ import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence; import com.sap.sailing.domain.base.SpeedWithConfidence; import com.sap.sailing.domain.common.Bearing; -import com.sap.sailing.domain.common.DoublePair; import com.sap.sailing.domain.common.LegType; import com.sap.sailing.domain.common.Speed; import com.sap.sailing.domain.common.Tack; +import com.sap.sailing.domain.common.confidence.impl.ScalableDouble; import com.sap.sailing.domain.common.impl.DegreeBearingImpl; import com.sap.sailing.domain.common.impl.KnotSpeedImpl; import com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing; @@ -34,9 +34,11 @@ import com.sap.sailing.polars.PolarDataService; import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException; import com.sap.sailing.polars.windestimation.ManeuverBasedWindEstimationTrackImpl; import com.sap.sailing.polars.windestimation.ManeuverBasedWindEstimationTrackImpl.ManeuverClassification; +import com.sap.sailing.polars.windestimation.ScalableBearingAndScalableDouble; import com.sap.sailing.server.gateway.jaxrs.AbstractSailingServerResource; import com.sap.sailing.server.gateway.serialization.impl.PositionJsonSerializer; import com.sap.sailing.server.gateway.serialization.impl.WindJsonSerializer; +import com.sap.sse.common.Util.Pair; import com.sap.sse.util.kmeans.Cluster; /** @@ -154,11 +156,11 @@ public class PolarResource extends AbstractSailingServerResource { PolarDataService service = getService().getPolarDataService(); ManeuverBasedWindEstimationTrackImpl maneuverBasedWindEstimationTrackImpl = new ManeuverBasedWindEstimationTrackImpl( service, trackedRace, /* millisecondsOverWhichToAverage */ 30000, /* waitForLatest */ false); - final Stream> clusters; + final Stream, Pair, ScalableBearingAndScalableDouble>> clusters; if ("tack".equals(cluster)) { - clusters = Stream.of(maneuverBasedWindEstimationTrackImpl.getTackCluster()); + clusters = maneuverBasedWindEstimationTrackImpl.getTackClusters().stream(); } else if ("jibe".equals(cluster)) { - clusters = Stream.of(maneuverBasedWindEstimationTrackImpl.getJibeCluster()); + clusters = maneuverBasedWindEstimationTrackImpl.getJibeClusters().stream(); } else if (cluster != null && cluster.matches("[0-9][0-9]*")) { clusters = Stream.of(new ArrayList<>(maneuverBasedWindEstimationTrackImpl.getClusters()).get(Integer.valueOf(cluster))); } else { diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy).launch b/java/com.sap.sailing.server/SailingServer (No Proxy).launch index 2c4007d528e..8ccc5e660b1 100755 --- a/java/com.sap.sailing.server/SailingServer (No Proxy).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy).launch @@ -33,5 +33,5 @@ - + diff --git a/java/com.sap.sse.common/.settings/org.eclipse.core.resources.prefs b/java/com.sap.sse.common/.settings/org.eclipse.core.resources.prefs new file mode 100755 index 00000000000..035eeb1c134 --- /dev/null +++ b/java/com.sap.sse.common/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding//src/com/sap/sse/common/impl/CountryCodeFactoryImpl.java=UTF-8 diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/ArrayListNavigableSet.java b/java/com.sap.sse.shared.android/src/com/sap/sse/util/impl/ArrayListNavigableSet.java similarity index 96% rename from java/com.sap.sse.common/src/com/sap/sse/common/ArrayListNavigableSet.java rename to java/com.sap.sse.shared.android/src/com/sap/sse/util/impl/ArrayListNavigableSet.java index 7945b1139e3..0c4b458aff6 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/ArrayListNavigableSet.java +++ b/java/com.sap.sse.shared.android/src/com/sap/sse/util/impl/ArrayListNavigableSet.java @@ -1,4 +1,4 @@ -package com.sap.sse.common; +package com.sap.sse.util.impl; import java.io.Serializable; import java.util.ArrayList; @@ -11,6 +11,8 @@ import java.util.NavigableSet; import java.util.NoSuchElementException; import java.util.SortedSet; +import com.sap.sse.common.ReverseRandomAccessList; + /** * A {@link NavigableSet} implementation that internally uses an {@link ArrayList} to represent the data structure. * The elements are ordered by a mandatory {@link Comparator}, meaning that the natural element order is not considered. diff --git a/java/com.sap.sse/src/com/sap/sse/util/kmeans/Cluster.java b/java/com.sap.sse/src/com/sap/sse/util/kmeans/Cluster.java index a2d280f62c8..b383b62be8d 100755 --- a/java/com.sap.sse/src/com/sap/sse/util/kmeans/Cluster.java +++ b/java/com.sap.sse/src/com/sap/sse/util/kmeans/Cluster.java @@ -115,7 +115,11 @@ public class ClustergetDistanceFromMean(e)*getDistanceFromMean(e)).average().getAsDouble(); } public Stream stream() { diff --git a/java/com.sap.sse/src/com/sap/sse/util/kmeans/KMeansMappingClusterer.java b/java/com.sap.sse/src/com/sap/sse/util/kmeans/KMeansMappingClusterer.java index 6526680dc30..6a9e62a047e 100755 --- a/java/com.sap.sse/src/com/sap/sse/util/kmeans/KMeansMappingClusterer.java +++ b/java/com.sap.sse/src/com/sap/sse/util/kmeans/KMeansMappingClusterer.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Random; import java.util.Set; import java.util.function.Function; +import java.util.stream.Collectors; import java.util.stream.Stream; import com.sap.sse.common.scalablevalue.ScalableValueWithDistance; @@ -31,6 +32,7 @@ import com.sap.sse.common.scalablevalue.ScalableValueWithDistance; public class KMeansMappingClusterer> { private List> clusters; private final Function mapper; + private int numberOfIterations; /** * Clusters the elements into numberOfClusters clusters such that the sum of squared @@ -64,9 +66,7 @@ public class KMeansMappingClusterer Iterable streamToList(Stream elements) { assert !elements.isParallel(); - final List result = new ArrayList<>(); - elements.forEach((e)->result.add(e)); - return result; + return elements.collect(Collectors.toList()); } /** @@ -139,6 +139,7 @@ public class KMeansMappingClusterer elements) { List> oldClusters; do { + numberOfIterations++; List> newClusters = new ArrayList<>(clusters.size()); for (Cluster c : clusters) { AveragesTo newMean = c.getCentroid(); @@ -153,6 +154,10 @@ public class KMeansMappingClustererelements to their nearest cluster */