mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-10-16 09:11:02 +00:00
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:
1 parent
ae16bbc0ba
commit
eb8c8b2537
9 files changed
+349
-161
No files matched your search
+5
@@ -1,5 +1,7 @@
|
||||
package com.sap.sailing.domain.common.confidence;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
public interface ConfidenceBasedAverager<ValueType, BaseType, RelativeTo> {
|
||||
/**
|
||||
* If a non-<code>null</code> weigher has been set for this averager, <code>at</code> must be a valid reference
|
||||
@@ -15,4 +17,7 @@ public interface ConfidenceBasedAverager<ValueType, BaseType, RelativeTo> {
|
||||
*/
|
||||
HasConfidence<ValueType, BaseType, RelativeTo> getAverage(
|
||||
Iterable<? extends HasConfidenceAndIsScalable<ValueType, BaseType, RelativeTo>> values, RelativeTo at);
|
||||
|
||||
HasConfidence<ValueType, BaseType, RelativeTo> getAverage(
|
||||
Iterator<? extends HasConfidenceAndIsScalable<ValueType, BaseType, RelativeTo>> values, RelativeTo at);
|
||||
}
|
||||
+20
-6
@@ -1,10 +1,11 @@
|
||||
package com.sap.sailing.domain.common.confidence.impl;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
import com.sap.sailing.domain.common.confidence.ConfidenceBasedAverager;
|
||||
import com.sap.sailing.domain.common.confidence.HasConfidence;
|
||||
import com.sap.sailing.domain.common.confidence.HasConfidenceAndIsScalable;
|
||||
import com.sap.sailing.domain.common.confidence.Weigher;
|
||||
import com.sap.sse.common.Util;
|
||||
import com.sap.sse.common.scalablevalue.ScalableValue;
|
||||
|
||||
|
||||
@@ -45,13 +46,21 @@ public class ConfidenceBasedAveragerImpl<ValueType, BaseType, RelativeTo> implem
|
||||
@Override
|
||||
public HasConfidence<ValueType, BaseType, RelativeTo> getAverage(
|
||||
Iterable<? extends HasConfidenceAndIsScalable<ValueType, BaseType, RelativeTo>> values, RelativeTo at) {
|
||||
if (values == null || Util.isEmpty(values)) {
|
||||
return 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;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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>();
|
||||
|
||||
+2
-2
@@ -206,7 +206,7 @@ public class ConfidenceTest {
|
||||
@Test
|
||||
public void testAveragingWithNullArrayYieldsNull() {
|
||||
ConfidenceBasedAverager<Double, Double, TimePoint> averager = ConfidenceFactory.INSTANCE.createAverager(null);
|
||||
HasConfidence<Double, Double, TimePoint> average = averager.getAverage(null, null);
|
||||
HasConfidence<Double, Double, TimePoint> average = averager.getAverage((Iterable<? extends HasConfidenceAndIsScalable<Double, Double, TimePoint>>) null, null);
|
||||
assertNull(average);
|
||||
}
|
||||
|
||||
@@ -414,7 +414,7 @@ public class ConfidenceTest {
|
||||
public void testConfidenceBasedAveragingWithNullArrayYieldsNull() {
|
||||
ConfidenceBasedAverager<Double, Double, TimePoint> averager = ConfidenceFactory.INSTANCE
|
||||
.createAverager(ConfidenceFactory.INSTANCE.createExponentialTimeDifferenceWeigher(1000));
|
||||
HasConfidence<Double, Double, TimePoint> average = averager.getAverage(null, null);
|
||||
HasConfidence<Double, Double, TimePoint> average = averager.getAverage((Iterable<? extends HasConfidenceAndIsScalable<Double, Double, TimePoint>>) null, null);
|
||||
assertNull(average);
|
||||
}
|
||||
|
||||
|
||||
+15
-5
@@ -1,5 +1,7 @@
|
||||
package com.sap.sailing.domain.confidence.impl;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
import com.sap.sailing.domain.common.Speed;
|
||||
import com.sap.sailing.domain.common.WindSource;
|
||||
import com.sap.sailing.domain.common.WindSourceType;
|
||||
@@ -13,7 +15,6 @@ import com.sap.sailing.domain.tracking.WindWithConfidence;
|
||||
import com.sap.sailing.domain.tracking.impl.ScalableWind;
|
||||
import com.sap.sailing.domain.tracking.impl.WindImpl;
|
||||
import com.sap.sailing.domain.tracking.impl.WindWithConfidenceImpl;
|
||||
import com.sap.sse.common.Util;
|
||||
|
||||
/**
|
||||
* In order to enable the aggregation of {@link Wind} objects whose {@link WindSource}'s {@link WindSourceType} suggests
|
||||
@@ -37,16 +38,24 @@ public class ConfidenceBasedWindAveragerImpl<RelativeTo> extends
|
||||
@Override
|
||||
public WindWithConfidence<RelativeTo> getAverage(
|
||||
Iterable<? extends HasConfidenceAndIsScalable<ScalableWind, Wind, RelativeTo>> values, RelativeTo at) {
|
||||
return getAverage(values.iterator(), at);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WindWithConfidence<RelativeTo> getAverage(
|
||||
Iterator<? extends HasConfidenceAndIsScalable<ScalableWind, Wind, RelativeTo>> values, RelativeTo at) {
|
||||
boolean atLeastOneFixWasMarkedToUseSpeed = false;
|
||||
if (values == null || Util.isEmpty(values)) {
|
||||
if (values == null || !values.hasNext()) {
|
||||
return null;
|
||||
} else {
|
||||
ScalableWind numerator = null;
|
||||
double confidenceSum = 0;
|
||||
double speedConfidenceSum = 0;
|
||||
double knotSum = 0;
|
||||
for (HasConfidenceAndIsScalable<ScalableWind, Wind, RelativeTo> next : values) {
|
||||
double relativeWeight = (getWeigher() == null ? 1.0 : getWeigher().getConfidence(next.getRelativeTo(), at)) * next.getConfidence();
|
||||
int count = 0;
|
||||
while (values.hasNext()) {
|
||||
HasConfidenceAndIsScalable<ScalableWind, Wind, RelativeTo> next = values.next();
|
||||
double relativeWeight = getWeight(next, at);
|
||||
ScalableWind weightedNext = next.getScalableValue().multiply(relativeWeight).getValue();
|
||||
double weighedNextKnots = next.getObject().getKnots() * relativeWeight;
|
||||
if (numerator == null) {
|
||||
@@ -61,9 +70,10 @@ public class ConfidenceBasedWindAveragerImpl<RelativeTo> extends
|
||||
speedConfidenceSum += relativeWeight;
|
||||
knotSum += weighedNextKnots;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
// TODO consider greater variance to reduce the confidence
|
||||
double newConfidence = confidenceSum / Util.size(values);
|
||||
double newConfidence = confidenceSum / count;
|
||||
Wind preResult = numerator.divide(confidenceSum);
|
||||
// if only values with useSpeed=false were aggregated, use the original result, otherwise compute
|
||||
// separate speed average:
|
||||
|
||||
+2
-27
@@ -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(
|
||||
|
||||
+224
-116
@@ -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());
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package com.sap.sailing.polars.windestimation;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.sap.sailing.domain.common.ManeuverType;
|
||||
import com.sap.sailing.domain.common.confidence.ConfidenceBasedAverager;
|
||||
import com.sap.sailing.domain.common.confidence.HasConfidenceAndIsScalable;
|
||||
import com.sap.sailing.polars.PolarDataService;
|
||||
import com.sap.sailing.polars.windestimation.ManeuverBasedWindEstimationTrackImpl.ManeuverClassification;
|
||||
import com.sap.sse.common.scalablevalue.ScalableValue;
|
||||
|
||||
/**
|
||||
* {@link ManeuverClassification}s need to be aggregated for different figures based on their confidence
|
||||
* of being a maneuver of a specific {@link ManeuverType type}. This can conveniently be done using a
|
||||
* {@link ConfidenceBasedAverager} and by adapting the {@link ManeuverClassification} objects to
|
||||
* what the averaged needs: a {@link HasConfidenceAndIsScalable} implementation.<p>
|
||||
*
|
||||
* This abstract class only contributes determining the confidence based on the likelihood for the
|
||||
* maneuver to be of the type specified. Subclasses need to add the logic for extracting the
|
||||
*
|
||||
* @author Axel Uhl (D043530)
|
||||
*
|
||||
*/
|
||||
public class ManeuverClassificationToHasConfidenceAndIsScalableAdapter<ValueType, BaseType> implements
|
||||
Function<ManeuverClassification, HasConfidenceAndIsScalable<ValueType, BaseType, Void>> {
|
||||
private final Function<ManeuverClassification, ScalableValue<ValueType, BaseType>> mapper;
|
||||
private final ManeuverType maneuverType;
|
||||
private final PolarDataService polarService;
|
||||
|
||||
/**
|
||||
* @param maneuverType
|
||||
* confidences are defined to be the
|
||||
* {@link PolarDataService#getManeuverLikelihoodAndTwsTwa(com.sap.sailing.domain.base.BoatClass, com.sap.sailing.domain.common.Speed, double, ManeuverType)
|
||||
* likelihood that the maneuver is of this type}.
|
||||
* @param mapper
|
||||
* maps the {@link ManeuverClassification} to the key figure that is a scalable value and that shall be
|
||||
* averaged by a {@link ConfidenceBasedAverager}.
|
||||
*/
|
||||
public ManeuverClassificationToHasConfidenceAndIsScalableAdapter(ManeuverType maneuverType,
|
||||
Function<ManeuverClassification, ScalableValue<ValueType, BaseType>> mapper, PolarDataService polarService) {
|
||||
this.maneuverType = maneuverType;
|
||||
this.mapper = mapper;
|
||||
this.polarService = polarService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HasConfidenceAndIsScalable<ValueType, BaseType, Void> apply(final ManeuverClassification t) {
|
||||
return new HasConfidenceAndIsScalable<ValueType, BaseType, Void>() {
|
||||
private static final long serialVersionUID = -5617237614367649676L;
|
||||
|
||||
@Override
|
||||
public ScalableValue<ValueType, BaseType> getScalableValue() {
|
||||
return mapper.apply(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getConfidence() {
|
||||
return polarService.getManeuverLikelihoodAndTwsTwa(t.getCompetitor().getBoat().getBoatClass(),
|
||||
t.getSpeedAtManeuverStart(), t.getManeuverAngleDeg(), maneuverType).getA();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void getRelativeTo() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseType getObject() {
|
||||
return getScalableValue().divide(1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
+6
-4
@@ -21,10 +21,10 @@ import com.sap.sailing.domain.base.Regatta;
|
||||
import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence;
|
||||
import com.sap.sailing.domain.base.SpeedWithConfidence;
|
||||
import com.sap.sailing.domain.common.Bearing;
|
||||
import com.sap.sailing.domain.common.DoublePair;
|
||||
import com.sap.sailing.domain.common.LegType;
|
||||
import com.sap.sailing.domain.common.Speed;
|
||||
import com.sap.sailing.domain.common.Tack;
|
||||
import com.sap.sailing.domain.common.confidence.impl.ScalableDouble;
|
||||
import com.sap.sailing.domain.common.impl.DegreeBearingImpl;
|
||||
import com.sap.sailing.domain.common.impl.KnotSpeedImpl;
|
||||
import com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing;
|
||||
@@ -34,9 +34,11 @@ import com.sap.sailing.polars.PolarDataService;
|
||||
import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException;
|
||||
import com.sap.sailing.polars.windestimation.ManeuverBasedWindEstimationTrackImpl;
|
||||
import com.sap.sailing.polars.windestimation.ManeuverBasedWindEstimationTrackImpl.ManeuverClassification;
|
||||
import com.sap.sailing.polars.windestimation.ScalableBearingAndScalableDouble;
|
||||
import com.sap.sailing.server.gateway.jaxrs.AbstractSailingServerResource;
|
||||
import com.sap.sailing.server.gateway.serialization.impl.PositionJsonSerializer;
|
||||
import com.sap.sailing.server.gateway.serialization.impl.WindJsonSerializer;
|
||||
import com.sap.sse.common.Util.Pair;
|
||||
import com.sap.sse.util.kmeans.Cluster;
|
||||
|
||||
/**
|
||||
@@ -154,11 +156,11 @@ public class PolarResource extends AbstractSailingServerResource {
|
||||
PolarDataService service = getService().getPolarDataService();
|
||||
ManeuverBasedWindEstimationTrackImpl maneuverBasedWindEstimationTrackImpl = new ManeuverBasedWindEstimationTrackImpl(
|
||||
service, trackedRace, /* millisecondsOverWhichToAverage */ 30000, /* waitForLatest */ false);
|
||||
final Stream<Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing>> clusters;
|
||||
final Stream<Cluster<ManeuverClassification, Pair<ScalableBearing, ScalableDouble>, Pair<Bearing, Double>, ScalableBearingAndScalableDouble>> clusters;
|
||||
if ("tack".equals(cluster)) {
|
||||
clusters = Stream.of(maneuverBasedWindEstimationTrackImpl.getTackCluster());
|
||||
clusters = maneuverBasedWindEstimationTrackImpl.getTackClusters().stream();
|
||||
} else if ("jibe".equals(cluster)) {
|
||||
clusters = Stream.of(maneuverBasedWindEstimationTrackImpl.getJibeCluster());
|
||||
clusters = maneuverBasedWindEstimationTrackImpl.getJibeClusters().stream();
|
||||
} else if (cluster != null && cluster.matches("[0-9][0-9]*")) {
|
||||
clusters = Stream.of(new ArrayList<>(maneuverBasedWindEstimationTrackImpl.getClusters()).get(Integer.valueOf(cluster)));
|
||||
} else {
|
||||
|
||||
Reference in new issue
Block a user