changed from one-dimensional clustering along middle COG to two-dimensional clustering (middle COG / maneuver angle) and starting first tests with this approach

This commit is contained in:
Axel Uhl committed 2015-01-23 17:29:54 +01:00
1 parent ae16bbc0ba
commit eb8c8b2537
9 files changed
+349 -161

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 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;
}
}
@@ -55,7 +55,7 @@ public abstract class AbstractTracTracLiveTest extends StoredTrackBasedTest {
private IRaceSubscriber raceSubscriber;
private final Collection<Receiver> receivers;
@Rule public Timeout AbstractTracTracLiveTestTimeout = new Timeout(3 * 60 * 1000);
// @Rule public Timeout AbstractTracTracLiveTestTimeout = new Timeout(3 * 60 * 1000);
protected AbstractTracTracLiveTest() throws URISyntaxException, MalformedURLException {
receivers = new HashSet<Receiver>();
@@ -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);
}
@@ -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:
@@ -111,33 +111,8 @@ public class TestWindEstimationFromManeuversOnAFew505Races extends OnlineTracTra
.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., getWeightedAverageMiddleManeuverCOGDeg(clustersSortedByAverageTackLikelihood.get(0), ManeuverType.TACK).getDegrees(), 10.);
assertEquals(245., getWeightedAverageMiddleManeuverCOGDeg(clustersSortedByAverageTackLikelihood.get(1), ManeuverType.TACK).getDegrees(), 10.);
}
/**
* 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.
*
* @return the weighted average middle COG
*/
private Bearing getWeightedAverageMiddleManeuverCOGDeg(
Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, ScalableBearingAndScalableDouble> cluster,
ManeuverType maneuverType) {
ScalableBearing weightedMiddleCOGDegSum = null;
double weightSum = 0;
for (ManeuverClassification e : cluster) {
final double weight = e.getLikelihoodAndTWSBasedOnSpeedAndAngle(maneuverType).getA();
final ScalableBearing weightedScalableMiddleManeuverCOG = new ScalableBearing(e.getMiddleManeuverCourse()).multiply(weight);
if (weightedMiddleCOGDegSum == null) {
weightedMiddleCOGDegSum = weightedScalableMiddleManeuverCOG;
} else {
weightedMiddleCOGDegSum = weightedMiddleCOGDegSum.add(weightedScalableMiddleManeuverCOG);
}
weightSum += weight;
}
return weightedMiddleCOGDegSum.divide(weightSum);
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(
@@ -4,6 +4,7 @@ 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.stream.Collectors;
@@ -23,6 +24,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.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;
@@ -61,29 +66,37 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl {
* 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 +104,14 @@ 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<>();
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);
clusters = clusterStructure.getA();
tackCluster = clusterStructure.getB();
jibeCluster = clusterStructure.getC();
tackClusters = clusterStructure.getB();
jibeClusters = clusterStructure.getC();
}
/**
@@ -237,7 +252,7 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl {
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.
@@ -293,137 +308,199 @@ 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)->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
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.))));
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);
}
return new Triple<>(clusters, dominantClusterAndLikelihoodOfBeingTackCluster.getA(), jibeCluster);
// Now work towards identifying the two tack clusters
// 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(getLikelihoodOfBeingTackCluster(c1, clusters)
- getLikelihoodOfBeingTackCluster(c2, clusters))).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 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) {
BoatClass boatClass = null; // TODO what if the race has more than one?
for (ManeuverClassification mc : jibeClusterCandidate) {
jibeClusterLikelihood += mc.getLikelihoodAndTWSBasedOnSpeedAndAngle(ManeuverType.JIBE).getA();
boatClass = mc.getCompetitor().getBoat().getBoatClass();
}
speedMatchFactor = getLikelihoodTackJibeSpeedRatio(tackClusterCandidate, jibeClusterCandidate, boatClass);
} 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());
Bearing approximateMiddleCOGForJibes = getWeightedAverageMiddleManeuverCOGDeg(cluster, ManeuverType.TACK).reverse();
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;
// TODO search for the other tack cluster
// TODO search for head-up/bear-away cluster(s) to port and to starboard of cluster's weighted middle COG
// 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);
// TODO 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 * (1.0 + 0.2 * tackJibeLikelihoodBasedOnSpeedRatioAndAverageManeuverAngle));
} else {
result = null;
result = .5; // no elements in the cluster, therefore no average speed; 50/50 chance that this would be a tack cluster
}
return result;
}
/**
* 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}.
* @param boatClass
* {@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, BoatClass boatClass) {
Speed tackClusterWeightedAverageSpeed = getWeightedAverageSpeed(tackClusterCandidate, ManeuverType.TACK);
Speed jibeClusterWeightedAverageSpeed = getWeightedAverageSpeed(jibeClusterCandidate, ManeuverType.JIBE);
return polarService.getConfidenceForTackJibeSpeedRatio(tackClusterWeightedAverageSpeed,
jibeClusterWeightedAverageSpeed, boatClass);
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();
}
/**
@@ -456,23 +533,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);
}
};
}
}
@@ -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 {