mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-10-15 00:30:58 +00:00
Merge branch 'master' into ribdashboard
This commit is contained in:
commit
103dc8b56d
32 files changed
+848
-283
No files matched your search
+5
@@ -1,5 +1,7 @@
|
||||
package com.sap.sailing.domain.common.confidence;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
public interface ConfidenceBasedAverager<ValueType, BaseType, RelativeTo> {
|
||||
/**
|
||||
* If a non-<code>null</code> weigher has been set for this averager, <code>at</code> must be a valid reference
|
||||
@@ -15,4 +17,7 @@ public interface ConfidenceBasedAverager<ValueType, BaseType, RelativeTo> {
|
||||
*/
|
||||
HasConfidence<ValueType, BaseType, RelativeTo> getAverage(
|
||||
Iterable<? extends HasConfidenceAndIsScalable<ValueType, BaseType, RelativeTo>> values, RelativeTo at);
|
||||
|
||||
HasConfidence<ValueType, BaseType, RelativeTo> getAverage(
|
||||
Iterator<? extends HasConfidenceAndIsScalable<ValueType, BaseType, RelativeTo>> values, RelativeTo at);
|
||||
}
|
||||
+20
-6
@@ -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<ValueType, BaseType, RelativeTo> implem
|
||||
@Override
|
||||
public HasConfidence<ValueType, BaseType, RelativeTo> getAverage(
|
||||
Iterable<? extends HasConfidenceAndIsScalable<ValueType, BaseType, RelativeTo>> values, RelativeTo at) {
|
||||
if (values == null || Util.isEmpty(values)) {
|
||||
return values == null ? null : getAverage(values.iterator(), at);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HasConfidence<ValueType, BaseType, RelativeTo> getAverage(
|
||||
Iterator<? extends HasConfidenceAndIsScalable<ValueType, BaseType, RelativeTo>> values, RelativeTo at) {
|
||||
if (values == null || !values.hasNext()) {
|
||||
return null;
|
||||
} else {
|
||||
ScalableValue<ValueType, BaseType> numerator = null;
|
||||
double confidenceSum = 0;
|
||||
for (HasConfidenceAndIsScalable<ValueType, BaseType, RelativeTo> next : values) {
|
||||
double relativeWeight = (getWeigher() == null ? 1.0 : getWeigher().getConfidence(next.getRelativeTo(), at)) * next.getConfidence();
|
||||
int count = 0;
|
||||
while (values.hasNext()) {
|
||||
HasConfidenceAndIsScalable<ValueType, BaseType, RelativeTo> next = values.next();
|
||||
double relativeWeight = getWeight(next, at);
|
||||
ScalableValue<ValueType, BaseType> weightedNext = next.getScalableValue().multiply(relativeWeight);
|
||||
if (numerator == null) {
|
||||
numerator = weightedNext;
|
||||
@@ -59,15 +68,20 @@ public class ConfidenceBasedAveragerImpl<ValueType, BaseType, RelativeTo> 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<ValueType, BaseType, RelativeTo>(result, newConfidence, at);
|
||||
}
|
||||
}
|
||||
|
||||
protected Weigher<RelativeTo> getWeigher() {
|
||||
protected double getWeight(HasConfidenceAndIsScalable<ValueType, BaseType, RelativeTo> next, RelativeTo at) {
|
||||
return (getWeigher() == null ? 1.0 : getWeigher().getConfidence(next.getRelativeTo(), at)) * next.getConfidence();
|
||||
}
|
||||
|
||||
private Weigher<RelativeTo> getWeigher() {
|
||||
return weigher;
|
||||
}
|
||||
}
|
||||
+21
@@ -63,4 +63,25 @@ public class ScalableBearing implements ScalableValueWithDistance<DoublePair, Be
|
||||
public double getDistance(Bearing other) {
|
||||
return Math.abs(divide(1).getDifferenceTo(other).getDegrees());
|
||||
}
|
||||
|
||||
/**
|
||||
* Computing the difference to a {@link bearing}, as implemented by {@link #getDistance(Bearing)} is correct but
|
||||
* slow because the {@link Math#atan2(double)} function is used to {@link ScalableBearing#divide(double) bring the
|
||||
* scalable bearing down to a bearing}. A less precise, yet much faster to compute and still monotonous distance
|
||||
* measure computes the length of the line segment connecting this bearing's projection onto a circle with the
|
||||
* projection of the bearing <code>a</code> 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.;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -14,7 +14,7 @@ public class ScalablePosition implements ScalableValueWithDistance<ScalablePosit
|
||||
Math.sin(position.getLatRad()));
|
||||
}
|
||||
|
||||
public ScalablePosition(double x, double y, double z) {
|
||||
private ScalablePosition(double x, double y, double z) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ import com.sap.sailing.domain.common.abstractlog.NotRevokableException;
|
||||
import com.sap.sailing.domain.tracking.Track;
|
||||
import com.sap.sailing.domain.tracking.impl.PartialNavigableSetView;
|
||||
import com.sap.sailing.domain.tracking.impl.TrackImpl;
|
||||
import com.sap.sse.common.ArrayListNavigableSet;
|
||||
import com.sap.sse.util.impl.ArrayListNavigableSet;
|
||||
|
||||
/**
|
||||
* {@link Track} implementation for {@link AbstractLogEvent}s.
|
||||
|
||||
+1
-1
@@ -8,9 +8,9 @@ import com.sap.sailing.domain.abstractlog.race.impl.BaseRaceLogEventVisitor;
|
||||
import com.sap.sailing.domain.abstractlog.race.impl.RaceLogRaceStatusEventComparator;
|
||||
import com.sap.sailing.domain.abstractlog.race.state.racingprocedure.ReadonlyRacingProcedure;
|
||||
import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus;
|
||||
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;
|
||||
|
||||
public class RaceStatusAnalyzer extends RaceLogAnalyzer<RaceLogRaceStatus> {
|
||||
|
||||
|
||||
+1
-1
@@ -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<FixType extends Timed> implements Track<FixType> {
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+62
-2
@@ -206,7 +206,7 @@ public class ConfidenceTest {
|
||||
@Test
|
||||
public void testAveragingWithNullArrayYieldsNull() {
|
||||
ConfidenceBasedAverager<Double, Double, TimePoint> averager = ConfidenceFactory.INSTANCE.createAverager(null);
|
||||
HasConfidence<Double, Double, TimePoint> average = averager.getAverage(null, null);
|
||||
HasConfidence<Double, Double, TimePoint> average = averager.getAverage((Iterable<? extends HasConfidenceAndIsScalable<Double, Double, TimePoint>>) null, null);
|
||||
assertNull(average);
|
||||
}
|
||||
|
||||
@@ -414,7 +414,7 @@ public class ConfidenceTest {
|
||||
public void testConfidenceBasedAveragingWithNullArrayYieldsNull() {
|
||||
ConfidenceBasedAverager<Double, Double, TimePoint> averager = ConfidenceFactory.INSTANCE
|
||||
.createAverager(ConfidenceFactory.INSTANCE.createExponentialTimeDifferenceWeigher(1000));
|
||||
HasConfidence<Double, Double, TimePoint> average = averager.getAverage(null, null);
|
||||
HasConfidence<Double, Double, TimePoint> average = averager.getAverage((Iterable<? extends HasConfidenceAndIsScalable<Double, Double, TimePoint>>) 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);
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -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> series;
|
||||
|
||||
+15
-5
@@ -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<RelativeTo> extends
|
||||
@Override
|
||||
public WindWithConfidence<RelativeTo> getAverage(
|
||||
Iterable<? extends HasConfidenceAndIsScalable<ScalableWind, Wind, RelativeTo>> values, RelativeTo at) {
|
||||
return getAverage(values.iterator(), at);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WindWithConfidence<RelativeTo> getAverage(
|
||||
Iterator<? extends HasConfidenceAndIsScalable<ScalableWind, Wind, RelativeTo>> 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<ScalableWind, Wind, RelativeTo> next : values) {
|
||||
double relativeWeight = (getWeigher() == null ? 1.0 : getWeigher().getConfidence(next.getRelativeTo(), at)) * next.getConfidence();
|
||||
int count = 0;
|
||||
while (values.hasNext()) {
|
||||
HasConfidenceAndIsScalable<ScalableWind, Wind, RelativeTo> 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<RelativeTo> 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:
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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<ItemType, FixType extends GPSFix> extends TrackImpl<FixType> implements GPSFixTrack<ItemType, FixType> {
|
||||
private static final Logger logger = Logger.getLogger(GPSFixTrackImpl.class.getName());
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+174
@@ -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<Maneuver, Competitor> maneuvers = windTrack.getAllManeuvers(/* waitForLatest */ true);
|
||||
final int numberOfClusters = 16;
|
||||
KMeansMappingClusterer<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, 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<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, ScalableBearingAndScalableDouble>> clusters = clusterer.getClusters();
|
||||
assertEquals(16, clusters.size());
|
||||
|
||||
// Now work towards identifying the two tack clusters
|
||||
List<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, 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<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, 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;
|
||||
}
|
||||
}
|
||||
-93
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<SpeedWithBearingWithConfidence<Void>> 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 <code>boatClass</code> sailed at <code>speedOverGround</code> and during the maneuver
|
||||
* changed course by <code>courseChange</code>, how likely was that a maneuver of type <code>maneuverType</code>?
|
||||
*
|
||||
* @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<Double, SpeedWithBearingWithConfidence<Void>> getManeuverLikelihoodAndTwsTwa(BoatClass boatClass, Speed speedOverGround, double courseChangeDeg, ManeuverType maneuverType);
|
||||
}
|
||||
@@ -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<Double, SpeedWithBearingWithConfidence<Void>> getManeuverLikelihoodAndTwsTwa(BoatClass boatClass, Speed speedAtManeuverStart, double courseChangeDeg,
|
||||
ManeuverType maneuverType) {
|
||||
assert maneuverType == ManeuverType.TACK || maneuverType == ManeuverType.JIBE;
|
||||
SpeedWithBearingWithConfidence<Void> closestTwsTwa = getClosestTwaTws(maneuverType, speedAtManeuverStart, courseChangeDeg, boatClass);
|
||||
final Pair<Double, SpeedWithBearingWithConfidence<Void>> 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<Void> getClosestTwaTws(ManeuverType type, Speed speedAtManeuverStart, double courseChangeDeg, BoatClass boatClass) {
|
||||
assert type == ManeuverType.TACK || type == ManeuverType.JIBE;
|
||||
double minDiff = Double.MAX_VALUE;
|
||||
SpeedWithBearingWithConfidence<Void> closestTwsTwa = null;
|
||||
for (SpeedWithBearingWithConfidence<Void> 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;
|
||||
}
|
||||
}
|
||||
+310
-153
@@ -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<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, 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<Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing>> clusters;
|
||||
private final Set<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, 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<ManeuverClassification, DoublePair, Bearing, ScalableBearing> tackCluster;
|
||||
private final List<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, 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<ManeuverClassification, DoublePair, Bearing, ScalableBearing> jibeCluster;
|
||||
private final List<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, 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<Set<Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing>>,
|
||||
Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing>,
|
||||
Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing>> clusterStructure = analyzeRace(waitForLatest);
|
||||
this.weightedAverageMiddleCOGForManeuverType = new HashMap<>();
|
||||
long start = System.currentTimeMillis();
|
||||
Triple<Set<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, ScalableBearingAndScalableDouble>>,
|
||||
List<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, ScalableBearingAndScalableDouble>>,
|
||||
List<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, 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<Void> mostLikelyTackTwsTwa;
|
||||
private final SpeedWithBearingWithConfidence<Void> mostLikelyJibeTwsTwa;
|
||||
private Pair<Double, SpeedWithBearingWithConfidence<Void>>[] 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<Void> getClosestTwaTws(ManeuverType type) {
|
||||
assert type == ManeuverType.TACK || type == ManeuverType.JIBE;
|
||||
double minDiff = Double.MAX_VALUE;
|
||||
SpeedWithBearingWithConfidence<Void> closestTwsTwa = null;
|
||||
for (SpeedWithBearingWithConfidence<Void> 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<Double, SpeedWithBearingWithConfidence<Void>>[] myLikelihoodAndTWSBasedOnSpeedAndAngleCache = (Pair<Double, SpeedWithBearingWithConfidence<Void>>[]) 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<Void> getEstimatedWindSpeedAndBearing(ManeuverType maneuverType) {
|
||||
Pair<Double, SpeedWithBearingWithConfidence<Void>> likelihoodAndTWSBasedOnSpeedAndAngle = getLikelihoodAndTWSBasedOnSpeedAndAngle(maneuverType);
|
||||
Pair<Double, SpeedWithBearingWithConfidence<Void>> likelihoodAndTWSBasedOnSpeedAndAngle = polarService.getManeuverLikelihoodAndTwsTwa(
|
||||
getCompetitor().getBoat().getBoatClass(), getSpeedAtManeuverStart(), getManeuverAngleDeg(), maneuverType);
|
||||
final SpeedWithBearingWithConfidenceImpl<Void> 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<Double, SpeedWithBearingWithConfidence<Void>> getLikelihoodAndTWSBasedOnSpeedAndAngle(final ManeuverType type) {
|
||||
assert type == ManeuverType.TACK || type == ManeuverType.JIBE;
|
||||
SpeedWithBearingWithConfidence<Void> closestTwsTwa = type == ManeuverType.TACK ? this.mostLikelyTackTwsTwa : this.mostLikelyJibeTwsTwa;
|
||||
final Pair<Double, SpeedWithBearingWithConfidence<Void>> 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<Double, SpeedWithBearingWithConfidence<Void>> 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<Set<Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing>>,
|
||||
Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing>,
|
||||
Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing>> analyzeRace(boolean waitForLatest) throws NotEnoughDataHasBeenAddedException {
|
||||
private Triple<Set<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, ScalableBearingAndScalableDouble>>,
|
||||
List<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, ScalableBearingAndScalableDouble>>,
|
||||
List<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, ScalableBearingAndScalableDouble>>> analyzeRace(
|
||||
boolean waitForLatest) throws NotEnoughDataHasBeenAddedException {
|
||||
final Map<Maneuver, Competitor> 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<ManeuverClassification, DoublePair, Bearing, ScalableBearing> clusterer =
|
||||
final int numberOfClusters = 16;
|
||||
KMeansMappingClusterer<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, 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<Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing>> 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<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, ScalableBearingAndScalableDouble>> clusters = clusterer.getClusters();
|
||||
assert maneuvers.size() == clusters.stream().map((c)->c.size()).reduce((s1, s2)->s1+s2).get();
|
||||
final Set<Pair<Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing>, 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<Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing>, Double> dominantClusterAndLikelihoodOfBeingTackCluster =
|
||||
clustersAndLikelihoodOfBeingTackCluster.stream().max(
|
||||
(a, b)->(int) Math.signum(a.getB() - b.getB())).get();
|
||||
addWindFixes(dominantClusterAndLikelihoodOfBeingTackCluster.getA(), ManeuverType.TACK);
|
||||
Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing> jibeCluster = getOppositeCluster(dominantClusterAndLikelihoodOfBeingTackCluster.getA(), clusters);
|
||||
if (jibeCluster != null) {
|
||||
addWindFixes(jibeCluster, ManeuverType.JIBE);
|
||||
// Now work towards identifying the two tack clusters
|
||||
final Map<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, ScalableBearingAndScalableDouble>, Double>
|
||||
likelihoodOfBeingTackCluster = new HashMap<>();
|
||||
for (Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, 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<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, 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<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, ScalableBearingAndScalableDouble>> jibeClusters =
|
||||
getJibeClusters(estimatedJibeMiddleCOG, clusters).collect(Collectors.toList());
|
||||
addWindFixes(jibeClusters.stream(), ManeuverType.JIBE);
|
||||
return new Triple<>(clusters, tackClusters, jibeClusters);
|
||||
}
|
||||
|
||||
private void addWindFixes(
|
||||
Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing> dominantClusterAndLikelihoodOfBeingTackCluster,
|
||||
Stream<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, ScalableBearingAndScalableDouble>> clusters,
|
||||
final ManeuverType maneuverType) {
|
||||
// TODO see bug 1562 comment #8: use WindWithConfidenceImpl internally and implement WindTrack.getAveragedWindWithConfidence
|
||||
for (ManeuverClassification mc : dominantClusterAndLikelihoodOfBeingTackCluster) {
|
||||
final SpeedWithBearingWithConfidence<Void> 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<Void> estimatedWindSpeedAndBearing = mc.getEstimatedWindSpeedAndBearing(maneuverType);
|
||||
if (estimatedWindSpeedAndBearing != null) {
|
||||
Wind windFromTack = new WindImpl(mc.getPosition(), mc.getTimePoint(), estimatedWindSpeedAndBearing
|
||||
.getObject());
|
||||
add(windFromTack);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* For the <code>tackClusterCandidate</code> 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 <code>tackClusterCandidate</code> more likely as there are
|
||||
* more likely jibe candidates in the jibe cluster candidate.
|
||||
* <p>
|
||||
* Determines the likelihood of <code>cluster</code> being a cluster of tacks, based on the average likelihood of
|
||||
* the maneuver classifications in <code>cluster</code> 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
|
||||
* <code>clusters</code> 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.
|
||||
* <ul>
|
||||
* <li>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</li>
|
||||
* <li>if other clusters are found that for the average true wind speed estimated from the maneuvers in
|
||||
* <code>cluster</code> 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</li>
|
||||
* <li>if clusters with approximately reversed average middle COG are found, weighted by the maneuvers' likelihood
|
||||
* to be jibes</li>
|
||||
* <li>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</li>
|
||||
* <li>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.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return not a 0..1 likelihood but some scaled value that can grow beyond 1 and represents the sum for all
|
||||
* relevant <code>tackClusterCandidate</code> 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<ManeuverClassification, DoublePair, Bearing, ScalableBearing> tackClusterCandidate,
|
||||
Set<Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing>> 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<ManeuverClassification, DoublePair, Bearing, ScalableBearing> 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<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, ScalableBearingAndScalableDouble> cluster,
|
||||
Set<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, 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<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, 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<ManeuverClassification, DoublePair, Bearing, ScalableBearing> getOppositeCluster(
|
||||
Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing> cluster,
|
||||
Set<Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing>> clusters) {
|
||||
final Bearing clusterCentroid = cluster.getCentroid();
|
||||
final Bearing opposite = clusterCentroid.reverse();
|
||||
Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing> 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<ManeuverClassification, DoublePair, Bearing, ScalableBearing> 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<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, 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<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, 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<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, 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<ManeuverClassification> 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 (<code>approximateMiddleCOGForJibes</code>) within
|
||||
* {@link #THRESHOLD_JIBE_CLUSTER_DIFFERENCE_DEGREES} degrees.
|
||||
*/
|
||||
private Stream<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, ScalableBearingAndScalableDouble>> getJibeClusters(
|
||||
final Bearing approximateMiddleCOGForJibes,
|
||||
Set<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, 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.
|
||||
* <p>
|
||||
*
|
||||
* 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<ManeuverClassification, DoublePair, Bearing, ScalableBearing> tackClusterCandidate,
|
||||
Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing> 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<ManeuverClassification> jibeClustersContent,
|
||||
BoatClass boatClass) {
|
||||
final int[] count = new int[1];
|
||||
final double[] likelihoodSum = new double[1];
|
||||
Stream<ManeuverClassification> 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<ManeuverClassification, DoublePair, Bearing, ScalableBearing> 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<ManeuverClassification> cluster,
|
||||
ManeuverType maneuverType) {
|
||||
ConfidenceBasedAverager<Double, Speed, Void> averager = ConfidenceFactory.INSTANCE.createAverager(/* weigher */ null);
|
||||
HasConfidence<Double, Speed, Void> average = averager.getAverage(
|
||||
cluster.map(new ManeuverClassificationToHasConfidenceAndIsScalableAdapter<Double, Speed>(maneuverType,
|
||||
(mc)->new ScalableSpeed(mc.getSpeedAtManeuverStart()),
|
||||
polarService)).iterator(), /* at */ null);
|
||||
return average == null ? null : average.getObject();
|
||||
}
|
||||
|
||||
/**
|
||||
* For each maneuver from <code>maneuvers</code>, a {@link ManeuverClassification object} is created.
|
||||
*/
|
||||
private Stream<ManeuverClassification> getManeuverClassifications(final Map<Maneuver, Competitor> maneuvers) {
|
||||
Stream<ManeuverClassification> getManeuverClassifications(final Map<Maneuver, Competitor> maneuvers) {
|
||||
return maneuvers.entrySet().stream().map((maneuverAndCompetitor)->
|
||||
new ManeuverClassification(maneuverAndCompetitor.getValue(), maneuverAndCompetitor.getKey()));
|
||||
}
|
||||
|
||||
private Map<Maneuver, Competitor> getAllManeuvers(boolean waitForLatest) {
|
||||
/**
|
||||
* Package scope to let test fragment access it
|
||||
*/
|
||||
Map<Maneuver, Competitor> getAllManeuvers(boolean waitForLatest) {
|
||||
Map<Maneuver, Competitor> 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<Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing>> 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
|
||||
* <code>maneuverType</code> is used.<p>
|
||||
*
|
||||
* 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<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, ScalableBearingAndScalableDouble> cluster,
|
||||
ManeuverType maneuverType) {
|
||||
Pair<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, ScalableBearingAndScalableDouble>, ManeuverType> key = new Pair<>(
|
||||
cluster, maneuverType);
|
||||
Bearing result = weightedAverageMiddleCOGForManeuverType.get(key);
|
||||
if (result == null) {
|
||||
ConfidenceBasedAverager<DoublePair, Bearing, Void> averager = ConfidenceFactory.INSTANCE
|
||||
.createAverager(/* weigher */null);
|
||||
HasConfidence<DoublePair, Bearing, Void> average = averager.getAverage(
|
||||
cluster.stream()
|
||||
.map(new ManeuverClassificationToHasConfidenceAndIsScalableAdapter<DoublePair, Bearing>(
|
||||
maneuverType, (mc) -> new ScalableBearing(mc.getMiddleManeuverCourse()),
|
||||
polarService)).iterator(), /* at */null);
|
||||
result = average.getObject();
|
||||
weightedAverageMiddleCOGForManeuverType.put(key, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Set<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, ScalableBearingAndScalableDouble>> getClusters() {
|
||||
return clusters;
|
||||
}
|
||||
|
||||
public Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing> getTackCluster() {
|
||||
return tackCluster;
|
||||
public List<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, ScalableBearingAndScalableDouble>> getTackClusters() {
|
||||
return tackClusters;
|
||||
}
|
||||
|
||||
public Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing> getJibeCluster() {
|
||||
return jibeCluster;
|
||||
public List<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, ScalableBearingAndScalableDouble>> getJibeClusters() {
|
||||
return jibeClusters;
|
||||
}
|
||||
|
||||
public String getStringRepresentation(boolean waitForLatest) {
|
||||
return getStringRepresentation(getClusters().stream(), waitForLatest);
|
||||
}
|
||||
|
||||
public String getStringRepresentation(Stream<Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing>> clusters, boolean waitForLatest) {
|
||||
public String getStringRepresentation(
|
||||
Stream<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, ScalableBearingAndScalableDouble>> clusters,
|
||||
boolean waitForLatest) {
|
||||
StringBuilder stringRepresentation = new StringBuilder();
|
||||
stringRepresentation.delete(0, stringRepresentation.length());
|
||||
stringRepresentation.append(getManeuverClassificationColumnHeaders());
|
||||
|
||||
+74
@@ -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.<p>
|
||||
*
|
||||
* 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<ValueType, BaseType> implements
|
||||
Function<ManeuverClassification, HasConfidenceAndIsScalable<ValueType, BaseType, Void>> {
|
||||
private final Function<ManeuverClassification, ScalableValue<ValueType, BaseType>> 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<ManeuverClassification, ScalableValue<ValueType, BaseType>> mapper, PolarDataService polarService) {
|
||||
this.maneuverType = maneuverType;
|
||||
this.mapper = mapper;
|
||||
this.polarService = polarService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HasConfidenceAndIsScalable<ValueType, BaseType, Void> apply(final ManeuverClassification t) {
|
||||
return new HasConfidenceAndIsScalable<ValueType, BaseType, Void>() {
|
||||
private static final long serialVersionUID = -5617237614367649676L;
|
||||
|
||||
@Override
|
||||
public ScalableValue<ValueType, BaseType> 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
+65
@@ -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)}.<p>
|
||||
*
|
||||
* 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<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>> {
|
||||
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<Bearing, Double> divide(double divisor) {
|
||||
return new Pair<>(b1.divide(divisor), b2.divide(divisor));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<ScalableBearing, ScalableDouble> getValue() {
|
||||
return new Pair<>(b1, b2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getDistance(Pair<Bearing, Double> 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<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>> 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));
|
||||
}
|
||||
}
|
||||
+6
-4
@@ -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<Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing>> clusters;
|
||||
final Stream<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, 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 {
|
||||
|
||||
@@ -33,5 +33,5 @@
|
||||
<booleanAttribute key="tracing" value="false"/>
|
||||
<booleanAttribute key="useCustomFeatures" value="false"/>
|
||||
<booleanAttribute key="useDefaultConfigArea" value="false"/>
|
||||
<stringAttribute key="workspace_bundles" value="com.sap.sse.shared.android@default:default,com.google.gwt.osgi@default:default,com.googlecode.java-diff-utils@default:default,com.mongodb.driver@default:default,com.sap.sailing.barbados.resultimport@default:default,com.sap.sailing.datamining.shared@default:default,com.sap.sailing.datamining@default:default,com.sap.sailing.declination@default:default,com.sap.sailing.domain.common@default:default,com.sap.sailing.domain.deckmanadapter@4:true,com.sap.sailing.domain.igtimiadapter.persistence@default:default,com.sap.sailing.domain.igtimiadapter@default:default,com.sap.sailing.domain.persistence@default:default,com.sap.sailing.domain.racelogtrackingadapter@3:default,com.sap.sailing.domain.shared.android@default:default,com.sap.sailing.domain.swisstimingadapter.persistence@default:default,com.sap.sailing.domain.swisstimingadapter@4:true,com.sap.sailing.domain.swisstimingreplayadapter@4:true,com.sap.sailing.domain.tractracadapter.persistence@default:default,com.sap.sailing.domain.tractracadapter@4:true,com.sap.sailing.domain@default:default,com.sap.sailing.ess40.resultimport@default:default,com.sap.sailing.expeditionconnector@default:default,com.sap.sailing.freg.resultimport@default:default,com.sap.sailing.geocoding@default:default,com.sap.sailing.gwt.ui@5:true,com.sap.sailing.kiworesultimport@default:default,com.sap.sailing.manage2sail.resultimport@default:default,com.sap.sailing.manage2sail@default:default,com.sap.sailing.nmeaconnector@default:default,com.sap.sailing.polars@default:default,com.sap.sailing.resultimport@default:default,com.sap.sailing.sailwave.resultimport@default:default,com.sap.sailing.server.gateway.ess40@4:true,com.sap.sailing.server.gateway.serialization.shared.android@default:default,com.sap.sailing.server.gateway.serialization@default:default,com.sap.sailing.server.gateway@5:true,com.sap.sailing.server.trackfiles@default:default,com.sap.sailing.server@default:default,com.sap.sailing.simulator@default:default,com.sap.sailing.udpconnector@default:default,com.sap.sailing.velum.resultimport@default:default,com.sap.sailing.www.events@4:true,com.sap.sailing.www@4:true,com.sap.sailing.xrr.resultimport@default:default,com.sap.sailing.xrr.schema@default:default,com.sap.sailing.xrr.structureimport@default:default,com.sap.sse.common@default:default,com.sap.sse.datamining.shared@default:default,com.sap.sse.datamining@default:default,com.sap.sse.gateway@default:default,com.sap.sse.gwt.adminconsole@default:default,com.sap.sse.gwt@default:default,com.sap.sse.mongodb@default:default,com.sap.sse.operationaltransformation@default:default,com.sap.sse.replication@4:true,com.sap.sse.security.common@default:default,com.sap.sse.security.ui@6:true,com.sap.sse.security.userstore.mongodb@4:true,com.sap.sse.security@5:true,com.sap.sse@default:default,com.tractrac.clientmodule@default:default,net.sf.marineapi@default:default,org.json.simple@default:default,org.moxieapps.gwt.highcharts@default:default"/>
|
||||
<stringAttribute key="workspace_bundles" value="com.google.gwt.osgi@default:default,com.googlecode.java-diff-utils@default:default,com.mongodb.driver@default:default,com.sap.sailing.barbados.resultimport@default:default,com.sap.sailing.datamining.shared@default:default,com.sap.sailing.datamining@default:default,com.sap.sailing.declination@default:default,com.sap.sailing.domain.common@default:default,com.sap.sailing.domain.deckmanadapter@4:true,com.sap.sailing.domain.igtimiadapter.persistence@default:default,com.sap.sailing.domain.igtimiadapter@default:default,com.sap.sailing.domain.persistence@default:default,com.sap.sailing.domain.racelogtrackingadapter@3:default,com.sap.sailing.domain.shared.android@default:default,com.sap.sailing.domain.swisstimingadapter.persistence@default:default,com.sap.sailing.domain.swisstimingadapter@4:true,com.sap.sailing.domain.swisstimingreplayadapter@4:true,com.sap.sailing.domain.tractracadapter.persistence@default:default,com.sap.sailing.domain.tractracadapter@4:true,com.sap.sailing.domain@default:default,com.sap.sailing.ess40.resultimport@default:default,com.sap.sailing.expeditionconnector@default:default,com.sap.sailing.freg.resultimport@default:default,com.sap.sailing.geocoding@default:default,com.sap.sailing.gwt.ui@5:true,com.sap.sailing.kiworesultimport@default:default,com.sap.sailing.manage2sail.resultimport@default:default,com.sap.sailing.manage2sail@default:default,com.sap.sailing.nmeaconnector@default:default,com.sap.sailing.polars@default:default,com.sap.sailing.resultimport@default:default,com.sap.sailing.sailwave.resultimport@default:default,com.sap.sailing.server.gateway.ess40@4:true,com.sap.sailing.server.gateway.serialization.shared.android@default:default,com.sap.sailing.server.gateway.serialization@default:default,com.sap.sailing.server.gateway@5:true,com.sap.sailing.server.trackfiles@default:default,com.sap.sailing.server@default:default,com.sap.sailing.simulator@default:default,com.sap.sailing.udpconnector@default:default,com.sap.sailing.velum.resultimport@default:default,com.sap.sailing.www.events@4:true,com.sap.sailing.www@4:true,com.sap.sailing.xrr.resultimport@default:default,com.sap.sailing.xrr.schema@default:default,com.sap.sailing.xrr.structureimport@default:default,com.sap.sse.common@default:default,com.sap.sse.datamining.shared@default:default,com.sap.sse.datamining@default:default,com.sap.sse.gateway@default:default,com.sap.sse.gwt.adminconsole@default:default,com.sap.sse.gwt@default:default,com.sap.sse.mongodb@default:default,com.sap.sse.operationaltransformation@default:default,com.sap.sse.replication@4:true,com.sap.sse.security.common@default:default,com.sap.sse.security.ui@6:true,com.sap.sse.security.userstore.mongodb@4:true,com.sap.sse.security@5:true,com.sap.sse.shared.android@default:default,com.sap.sse@default:default,com.tractrac.clientmodule@default:default,net.sf.marineapi@default:default,org.json.simple@default:default,org.moxieapps.gwt.highcharts@default:default"/>
|
||||
</launchConfiguration>
|
||||
@@ -0,0 +1,2 @@
|
||||
eclipse.preferences.version=1
|
||||
encoding//src/com/sap/sse/common/impl/CountryCodeFactoryImpl.java=UTF-8
|
||||
+3
-1
@@ -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.
|
||||
@@ -115,7 +115,11 @@ public class Cluster<E, ValueType, AveragesTo, T extends ScalableValueWithDistan
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "{mean: "+getMean()+", centroid: "+getCentroid()+", elements: "+elements+"}";
|
||||
return "{mean: "+getMean()+", centroid: "+getCentroid()+", size: "+size()+", variance: "+getVariance()+", elements: "+elements+"}";
|
||||
}
|
||||
|
||||
private double getVariance() {
|
||||
return stream().mapToDouble((e)->getDistanceFromMean(e)*getDistanceFromMean(e)).average().getAsDouble();
|
||||
}
|
||||
|
||||
public Stream<E> stream() {
|
||||
|
||||
@@ -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<E, ValueType, AveragesTo, T extends ScalableValueWithDistance<ValueType, AveragesTo>> {
|
||||
private List<Cluster<E, ValueType, AveragesTo, T>> clusters;
|
||||
private final Function<E, T> mapper;
|
||||
private int numberOfIterations;
|
||||
|
||||
/**
|
||||
* Clusters the <code>elements</code> into <code>numberOfClusters</code> clusters such that the sum of squared
|
||||
@@ -64,9 +66,7 @@ public class KMeansMappingClusterer<E, ValueType, AveragesTo, T extends Scalable
|
||||
|
||||
private static <E> Iterable<E> streamToList(Stream<E> elements) {
|
||||
assert !elements.isParallel();
|
||||
final List<E> result = new ArrayList<>();
|
||||
elements.forEach((e)->result.add(e));
|
||||
return result;
|
||||
return elements.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,6 +139,7 @@ public class KMeansMappingClusterer<E, ValueType, AveragesTo, T extends Scalable
|
||||
private void iterate(Iterable<E> elements) {
|
||||
List<Cluster<E, ValueType, AveragesTo, T>> oldClusters;
|
||||
do {
|
||||
numberOfIterations++;
|
||||
List<Cluster<E, ValueType, AveragesTo, T>> newClusters = new ArrayList<>(clusters.size());
|
||||
for (Cluster<E, ValueType, AveragesTo, T> c : clusters) {
|
||||
AveragesTo newMean = c.getCentroid();
|
||||
@@ -153,6 +154,10 @@ public class KMeansMappingClusterer<E, ValueType, AveragesTo, T extends Scalable
|
||||
} while (!clusters.equals(oldClusters));
|
||||
}
|
||||
|
||||
public int getNumberOfIterations() {
|
||||
return numberOfIterations;
|
||||
}
|
||||
|
||||
/**
|
||||
* adds all <code>elements</code> to their nearest cluster
|
||||
*/
|
||||
|
||||
Reference in new issue
Block a user