Merge branch 'master' into ribdashboard

This commit is contained in:
Axel Uhl committed 2015-01-24 15:49:01 +01:00
commit 103dc8b56d
32 files changed
+848 -283

No files matched your search

@@ -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);
}
@@ -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;
}
}
@@ -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.;
}
}
@@ -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;
@@ -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.
@@ -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> {
@@ -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> {
@@ -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
@@ -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);
}
}
@@ -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;
@@ -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:
@@ -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;
/**
@@ -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
@@ -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());
@@ -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
@@ -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
@@ -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.
@@ -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;
@@ -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
@@ -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;
}
}
@@ -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;
}
}
@@ -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());
@@ -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);
}
};
}
}
@@ -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));
}
}
@@ -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
@@ -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
*/