mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-10-16 09:11:02 +00:00
implemented a first outline of the algorithm described in a comment in an earlier commit
This commit is contained in:
1 parent
8264e1d3c4
commit
0ccd2629e8
4 files changed
+198
-184
No files matched your search
+3
-3
@@ -96,7 +96,7 @@ public class KMeansMappingClusterer<E, ValueType, AveragesTo, T extends Scalable
|
||||
* Same as {@link #KMeansMappingClusterer(int, Iterable, Function, Iterator)}, only that the elements and the seeds are
|
||||
* provided by streams instead of iterables and iterators.
|
||||
*/
|
||||
public KMeansMappingClusterer(int numberOfClusters, Stream<E> elements, Function<E, T> mapper, Stream<AveragesTo> seeds) {
|
||||
public KMeansMappingClusterer(int numberOfClusters, Stream<E> elements, Function<E, T> mapper, Stream<? extends AveragesTo> seeds) {
|
||||
this(numberOfClusters, streamToList(elements), mapper, seeds.iterator());
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ public class KMeansMappingClusterer<E, ValueType, AveragesTo, T extends Scalable
|
||||
* elements to use as the seeds; if fewer than <code>numberOfClusters</code> seeds are provided, the
|
||||
* number of clusters is reduced to the number of seeds provided.
|
||||
*/
|
||||
public KMeansMappingClusterer(int numberOfClusters, Iterable<E> elements, Function<E, T> mapper, Iterator<AveragesTo> seeds) {
|
||||
public KMeansMappingClusterer(int numberOfClusters, Iterable<E> elements, Function<E, T> mapper, Iterator<? extends AveragesTo> seeds) {
|
||||
this.mapper = mapper;
|
||||
clusters = new ArrayList<>();
|
||||
initClusters(elements, seeds);
|
||||
@@ -123,7 +123,7 @@ public class KMeansMappingClusterer<E, ValueType, AveragesTo, T extends Scalable
|
||||
* <code>elements</code> provided; in case fewer elements are provided, only a prefix of the <code>seeds</code>
|
||||
* iterator will be used.
|
||||
*/
|
||||
private void initClusters(Iterable<E> elements, Iterator<AveragesTo> seeds) {
|
||||
private void initClusters(Iterable<E> elements, Iterator<? extends AveragesTo> seeds) {
|
||||
Iterator<E> elementsIter = elements.iterator();
|
||||
while (elementsIter.hasNext() && seeds.hasNext()) {
|
||||
elementsIter.next();
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ public class PolarDataServiceImpl implements PolarDataService {
|
||||
public SpeedWithConfidence<Void> getSpeed(BoatClass boatClass, Speed windSpeed, Bearing trueWindAngle) throws NotEnoughDataHasBeenAddedException {
|
||||
return polarDataMiner.estimateBoatSpeed(boatClass, windSpeed, trueWindAngle);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Set<SpeedWithBearingWithConfidence<Void>> getAverageTrueWindSpeedAndAngleCandidates(BoatClass boatClass,
|
||||
Speed speedOverGround, LegType legType, Tack tack) {
|
||||
|
||||
+184
-169
@@ -2,36 +2,36 @@ package com.sap.sailing.polars.windestimation;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.sap.sailing.domain.base.Competitor;
|
||||
import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence;
|
||||
import com.sap.sailing.domain.base.SpeedWithConfidence;
|
||||
import com.sap.sailing.domain.base.Waypoint;
|
||||
import com.sap.sailing.domain.base.impl.SpeedWithConfidenceImpl;
|
||||
import com.sap.sailing.domain.base.impl.SpeedWithBearingWithConfidenceImpl;
|
||||
import com.sap.sailing.domain.common.Bearing;
|
||||
import com.sap.sailing.domain.common.Distance;
|
||||
import com.sap.sailing.domain.common.DoublePair;
|
||||
import com.sap.sailing.domain.common.LegType;
|
||||
import com.sap.sailing.domain.common.ManeuverType;
|
||||
import com.sap.sailing.domain.common.Position;
|
||||
import com.sap.sailing.domain.common.Speed;
|
||||
import com.sap.sailing.domain.common.SpeedWithBearing;
|
||||
import com.sap.sailing.domain.common.Tack;
|
||||
import com.sap.sailing.domain.common.confidence.ConfidenceBasedAverager;
|
||||
import com.sap.sailing.domain.common.confidence.ConfidenceFactory;
|
||||
import com.sap.sailing.domain.common.confidence.HasConfidence;
|
||||
import com.sap.sailing.domain.common.impl.DegreeBearingImpl;
|
||||
import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl;
|
||||
import com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing;
|
||||
import com.sap.sailing.domain.common.scalablevalue.impl.ScalableSpeed;
|
||||
import com.sap.sailing.domain.tracking.Maneuver;
|
||||
import com.sap.sailing.domain.tracking.TrackedRace;
|
||||
import com.sap.sailing.domain.tracking.Wind;
|
||||
import com.sap.sailing.domain.tracking.impl.WindImpl;
|
||||
import com.sap.sailing.domain.tracking.impl.WindTrackImpl;
|
||||
import com.sap.sailing.polars.PolarDataService;
|
||||
import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException;
|
||||
@@ -71,20 +71,23 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl {
|
||||
* will be used as a classifcation criterion.
|
||||
*/
|
||||
private static final double RATIO_JIBE_VERSUS_TACK_START_SPEED_THRESHOLD = 1.2;
|
||||
|
||||
/**
|
||||
* 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 final PolarDataService polarService;
|
||||
|
||||
private final TrackedRace trackedRace;
|
||||
|
||||
private final StringBuilder stringRepresentation;
|
||||
|
||||
public ManeuverBasedWindEstimationTrackImpl(PolarDataService polarService, TrackedRace trackedRace, long millisecondsOverWhichToAverage)
|
||||
throws NotEnoughDataHasBeenAddedException {
|
||||
super(millisecondsOverWhichToAverage, DEFAULT_BASE_CONFIDENCE, /* useSpeed */ false,
|
||||
/* nameForReadWriteLock */ ManeuverBasedWindEstimationTrackImpl.class.getName());
|
||||
this.polarService = polarService;
|
||||
this.trackedRace = trackedRace;
|
||||
this.stringRepresentation = new StringBuilder();
|
||||
analyzeRace();
|
||||
}
|
||||
|
||||
@@ -95,7 +98,7 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl {
|
||||
* @author Axel Uhl (D043530)
|
||||
*
|
||||
*/
|
||||
private static class ManeuverClassification {
|
||||
private class ManeuverClassification {
|
||||
private final Competitor competitor;
|
||||
private final TimePoint timePoint;
|
||||
private final Position position;
|
||||
@@ -106,30 +109,38 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl {
|
||||
private final SpeedWithBearing speedAtManeuverStart;
|
||||
private final Bearing middleManeuverCourse;
|
||||
private final Distance maneuverLoss;
|
||||
private final LegType legType;
|
||||
private final Tack tack;
|
||||
private final SpeedWithBearingWithConfidence<Void> estimatedTrueWindSpeedAndAngle;
|
||||
private final SpeedWithBearingWithConfidence<Void> mostLikelyTackTwsTwa;
|
||||
private final SpeedWithBearingWithConfidence<Void> mostLikelyJibeTwsTwa;
|
||||
|
||||
/**
|
||||
* Each maneuver classification will usually have an "opposite" that has {@link #legType} {@link LegType#UPWIND} when
|
||||
* this one has {@link #legType} {@link LegType#DOWNWIND} and vice versa. Since this is a cyclic relationship it is not
|
||||
* set up during object construction, but a {@link #setOpposite(ManeuverClassification) setter} is offered for it.
|
||||
*/
|
||||
private ManeuverClassification opposite;
|
||||
|
||||
protected ManeuverClassification(Competitor competitor, Maneuver maneuver, LegType legType, Tack tack,
|
||||
SpeedWithBearingWithConfidence<Void> estimatedTrueWindSpeedAndAngle) {
|
||||
protected ManeuverClassification(Competitor competitor, Maneuver maneuver) {
|
||||
super();
|
||||
this.competitor = competitor;
|
||||
this.timePoint = maneuver.getTimePoint();
|
||||
this.position = maneuver.getPosition();
|
||||
this.legType = legType;
|
||||
this.tack = tack;
|
||||
this.estimatedTrueWindSpeedAndAngle = estimatedTrueWindSpeedAndAngle;
|
||||
this.maneuverAngleDeg = maneuver.getDirectionChangeInDegrees();
|
||||
this.speedAtManeuverStart = maneuver.getSpeedWithBearingBefore();
|
||||
this.middleManeuverCourse = maneuver.getSpeedWithBearingBefore().getBearing().middle(maneuver.getSpeedWithBearingAfter().getBearing());
|
||||
this.maneuverLoss = maneuver.getManeuverLoss();
|
||||
this.mostLikelyTackTwsTwa = getClosestTwaTws(ManeuverType.TACK);
|
||||
this.mostLikelyJibeTwsTwa = getClosestTwaTws(ManeuverType.JIBE);
|
||||
}
|
||||
|
||||
private SpeedWithBearingWithConfidence<Void> getClosestTwaTws(ManeuverType type) {
|
||||
assert type == ManeuverType.TACK || type == ManeuverType.JIBE;
|
||||
double minDiff = Double.MAX_VALUE;
|
||||
SpeedWithBearingWithConfidence<Void> closestTwsTwa = null;
|
||||
for (SpeedWithBearingWithConfidence<Void> trueWindSpeedAndAngle : polarService.getAverageTrueWindSpeedAndAngleCandidates(
|
||||
getCompetitor().getBoat().getBoatClass(), getSpeedAtManeuverStart(),
|
||||
type == ManeuverType.TACK ? LegType.UPWIND : LegType.DOWNWIND,
|
||||
type == ManeuverType.TACK ? getManeuverAngleDeg() >= 0 ? Tack.PORT : Tack.STARBOARD
|
||||
: getManeuverAngleDeg() >= 0 ? Tack.STARBOARD : Tack.PORT)) {
|
||||
double diff = Math.abs(trueWindSpeedAndAngle.getObject().getBearing().getDegrees()*2)-Math.abs(getManeuverAngleDeg());
|
||||
if (diff < minDiff) {
|
||||
minDiff = diff;
|
||||
closestTwsTwa = trueWindSpeedAndAngle;
|
||||
}
|
||||
}
|
||||
return closestTwsTwa;
|
||||
}
|
||||
|
||||
public Competitor getCompetitor() {
|
||||
@@ -156,43 +167,51 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl {
|
||||
return middleManeuverCourse;
|
||||
}
|
||||
|
||||
public Bearing getEstimatedWindBearing() {
|
||||
switch (getLegType()) {
|
||||
case UPWIND:
|
||||
return getMiddleManeuverCourse().reverse();
|
||||
case DOWNWIND:
|
||||
return getMiddleManeuverCourse();
|
||||
public SpeedWithBearingWithConfidence<Void> getEstimatedWindSpeedAndBearing(ManeuverType maneuverType) {
|
||||
Pair<Double, SpeedWithBearingWithConfidence<Void>> likelihoodAndTWSBasedOnSpeedAndAngle = getLikelihoodAndTWSBasedOnSpeedAndAngle(maneuverType);
|
||||
final Bearing bearing;
|
||||
final double confidence = likelihoodAndTWSBasedOnSpeedAndAngle.getB().getConfidence() * likelihoodAndTWSBasedOnSpeedAndAngle.getA();
|
||||
final Speed speed = likelihoodAndTWSBasedOnSpeedAndAngle.getB().getObject();
|
||||
switch (maneuverType) {
|
||||
case TACK:
|
||||
bearing = getMiddleManeuverCourse().reverse();
|
||||
break;
|
||||
case JIBE:
|
||||
bearing = getMiddleManeuverCourse();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Found leg type "+getLegType()+" but can only handle "+LegType.UPWIND.name()+
|
||||
throw new IllegalStateException("Found leg type "+maneuverType+" but can only handle "+LegType.UPWIND.name()+
|
||||
" and "+LegType.DOWNWIND.name());
|
||||
}
|
||||
return new SpeedWithBearingWithConfidenceImpl<Void>(new KnotSpeedWithBearingImpl(speed.getKnots(), bearing), confidence,
|
||||
/* relativeTo */ null);
|
||||
}
|
||||
|
||||
public Distance getManeuverLoss() {
|
||||
return maneuverLoss;
|
||||
}
|
||||
|
||||
public LegType getLegType() {
|
||||
return legType;
|
||||
}
|
||||
|
||||
public Tack getTack() {
|
||||
return tack;
|
||||
}
|
||||
|
||||
public SpeedWithBearingWithConfidence<Void> getEstimatedTrueWindSpeedAndAngle() {
|
||||
return estimatedTrueWindSpeedAndAngle;
|
||||
/**
|
||||
* Computes the likelihood that the maneuver represented by this object is of the <code>type</code> requested. The polar
|
||||
* service may offer more than one possible wind condition for the given speed. In this case, the true wind angle that
|
||||
* fits the actual {@link #getManeuverAngleDeg() maneuver angle} better is chosen to judge how close the maneuver is to
|
||||
* the expected angle. The likelihood as well as the estimated true wind speed are returned.
|
||||
*
|
||||
* @return a value between 0 and 1
|
||||
*/
|
||||
public Pair<Double, SpeedWithBearingWithConfidence<Void>> getLikelihoodAndTWSBasedOnSpeedAndAngle(final ManeuverType type) {
|
||||
assert type == ManeuverType.TACK || type == ManeuverType.JIBE;
|
||||
SpeedWithBearingWithConfidence<Void> closestTwsTwa = type == ManeuverType.TACK ? this.mostLikelyTackTwsTwa : this.mostLikelyJibeTwsTwa;
|
||||
double minDiffDeg = Math.abs(Math.abs(closestTwsTwa.getObject().getBearing().getDegrees()*2)-Math.abs(getManeuverAngleDeg()));
|
||||
// TODO ask polar service how likely this is because the polar service knows how narrow or wide the maneuver angle range is
|
||||
return new Pair<>(1. / (1.+(minDiffDeg/10.)*(minDiffDeg/10.)), closestTwsTwa);
|
||||
}
|
||||
|
||||
public double getAbsoluteOffsetBetweenActualAndExpectedManeuverAngle() {
|
||||
return Math.abs(getEstimatedTrueWindSpeedAndAngle().getObject().getBearing().getDegrees()*2)-Math.abs(getManeuverAngleDeg());
|
||||
}
|
||||
|
||||
public static String getToStringColumnHeaders() {
|
||||
public String getToStringColumnHeaders() {
|
||||
return "datapoint\tcompetitor\ttimePoint\tangleDeg\tboatSpeedKn\tcogDeg\tmiddleManeuverCourse\twindEstimationFromDeg\tlossM\tassumedLegType\tassumedTack\testimatedTrueWindSpeedKn\testimatedTrueWindAngleDeg\toffsetFromExpectedManeuverAngle";
|
||||
}
|
||||
|
||||
public static String getToStringColumnTypes() {
|
||||
public String getToStringColumnTypes() {
|
||||
return "infoitem\tstring\tdate\tfloat\tfloat\tfloat\tfloat\tfloat\tfloat\tstring\tstring\tfloat\tfloat\tfloat";
|
||||
}
|
||||
|
||||
@@ -216,29 +235,10 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl {
|
||||
result.append("\t");
|
||||
result.append(getMiddleManeuverCourse().getDegrees());
|
||||
result.append("\t");
|
||||
result.append(getEstimatedWindBearing().reverse().getDegrees());
|
||||
result.append("\t");
|
||||
result.append(getManeuverLoss() == null ? 0.0 : getManeuverLoss().getMeters());
|
||||
result.append("\t");
|
||||
result.append(getLegType());
|
||||
result.append("\t");
|
||||
result.append(getTack());
|
||||
result.append("\t");
|
||||
result.append(getEstimatedTrueWindSpeedAndAngle().getObject().getKnots());
|
||||
result.append("\t");
|
||||
result.append(getEstimatedTrueWindSpeedAndAngle().getObject().getBearing().getDegrees());
|
||||
result.append("\t");
|
||||
result.append(Math.abs(Math.abs(getEstimatedTrueWindSpeedAndAngle().getObject().getBearing().getDegrees()*2)-Math.abs(getManeuverAngleDeg())));
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public void setOpposite(ManeuverClassification maneuverClassification) {
|
||||
this.opposite = maneuverClassification;
|
||||
}
|
||||
|
||||
public ManeuverClassification getOpposite() {
|
||||
return opposite;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -266,7 +266,7 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl {
|
||||
* There are usually no other maneuvers with similar middle COG because boats can't sail straight against the wind.
|
||||
* <p>
|
||||
*
|
||||
* We evaluate each clusters likelihood of being the tack cluster by for each of its maneuvers adding up the
|
||||
* We evaluate each cluster's likelihood of being the tack cluster by for each of its maneuvers adding up the
|
||||
* likelihood of that maneuver being a tack, solely based on its maneuver angle, as judged by the polar service. If
|
||||
* a cluster exists that has approximately the opposite average middle COG (the jibe cluster candidate), the
|
||||
* likelihoods for its maneuvers being a jibe are added. (TODO adding is statistically not adequate here; however,
|
||||
@@ -298,116 +298,131 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl {
|
||||
*/
|
||||
private void analyzeRace() throws NotEnoughDataHasBeenAddedException {
|
||||
final Map<Maneuver, Competitor> maneuvers = getAllManeuvers();
|
||||
Set<ManeuverClassification> maneuverClassifications = getManeuverClassifications(maneuvers);
|
||||
// Now cluster the maneuvers by estimated maneuver angle
|
||||
// constrain the clustering to those maneuvers that are sufficiently close to the expected maneuver angle
|
||||
// TODO cluster into eight clusters by middle COG first, then aggregate tack likelihoods for each, and jibe likelihoods for opposite cluster
|
||||
// TODO compare weighted speed average for tack and jibe cluster candidates and let polar service assign probability for the ratio
|
||||
// TODO determine confidence of tack cluster being the tack cluster by comparing ratings against other cluster pairs
|
||||
// TODO create wind fixes from tack and jibe cluster maneuvers (optionally with a threshold for minimum confidence of the maneuver being what it was guessed to be)
|
||||
// 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 =
|
||||
new KMeansMappingClusterer<>(4,
|
||||
maneuverClassifications.stream().filter(
|
||||
(mc)->mc.getAbsoluteOffsetBetweenActualAndExpectedManeuverAngle()<MANEUVER_ANGLE_DEVIATION_IN_DEGREES_THRESHOLD),
|
||||
(mc)->new ScalableBearing(mc.getEstimatedWindBearing()), // maps maneuver classification to cluster metric
|
||||
new KMeansMappingClusterer<>(numberOfClusters,
|
||||
getManeuverClassifications(maneuvers),
|
||||
(mc)->new ScalableBearing(mc.getMiddleManeuverCourse()), // maps maneuver classification to cluster metric
|
||||
// use an evenly distributed set of cluster seeds for clustering wind direction estimations
|
||||
Arrays.<Bearing>asList(new DegreeBearingImpl(0), new DegreeBearingImpl(90), new DegreeBearingImpl(180), new DegreeBearingImpl(270)).stream());
|
||||
IntStream.range(0, numberOfClusters).mapToObj((i)->new DegreeBearingImpl(((double) i)*360./(double) numberOfClusters)));
|
||||
final Set<Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing>> clusters = clusterer.getClusters();
|
||||
Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing> dominantCluster = clusters.stream().max((a, b)->a.size()-b.size()).get();
|
||||
Pair<Set<SpeedWithConfidence<Void>>, Set<SpeedWithConfidence<Void>>> averageSpeedsIntoTacksAndJibes = dominantCluster.stream().collect(
|
||||
()->new Pair<Set<SpeedWithConfidence<Void>>, Set<SpeedWithConfidence<Void>>>(new HashSet<>(), new HashSet<>()),
|
||||
(resultSoFar, mc)->{
|
||||
if (mc.getLegType() == LegType.UPWIND) {
|
||||
resultSoFar.getA().add(new SpeedWithConfidenceImpl<Void>(mc.getSpeedAtManeuverStart(),
|
||||
/* high confidence for small deviation from expected maneuver angle */
|
||||
Math.max(0, (MANEUVER_ANGLE_DEVIATION_IN_DEGREES_THRESHOLD-mc.getAbsoluteOffsetBetweenActualAndExpectedManeuverAngle()))/
|
||||
MANEUVER_ANGLE_DEVIATION_IN_DEGREES_THRESHOLD, /* relative to */ null));
|
||||
} else { // DOWNWIND
|
||||
resultSoFar.getB().add(new SpeedWithConfidenceImpl<Void>(mc.getSpeedAtManeuverStart(),
|
||||
/* high confidence for small deviation from expected maneuver angle */
|
||||
Math.max(0, (MANEUVER_ANGLE_DEVIATION_IN_DEGREES_THRESHOLD-mc.getAbsoluteOffsetBetweenActualAndExpectedManeuverAngle()))/
|
||||
MANEUVER_ANGLE_DEVIATION_IN_DEGREES_THRESHOLD, /* relative to */ null));
|
||||
}
|
||||
},
|
||||
(s1, s2)->{ s1.getA().addAll(s2.getA()); s1.getB().addAll(s2.getB()); });
|
||||
final ConfidenceBasedAverager<Double, Speed, Void> speedAverager = ConfidenceFactory.INSTANCE.createAverager(/* weigher */ null);
|
||||
HasConfidence<Double, Speed, Void> averageSpeedIntoTack = speedAverager.getAverage(averageSpeedsIntoTacksAndJibes.getA(), /* at */ null);
|
||||
HasConfidence<Double, Speed, Void> averageSpeedIntoJibe = speedAverager.getAverage(averageSpeedsIntoTacksAndJibes.getB(), /* at */ null);
|
||||
// it is possible that the speed differences are miniscule, not sufficient to decide with certainty what was a tack and what a jibe
|
||||
if (averageSpeedIntoJibe.getObject().getKnots() > RATIO_JIBE_VERSUS_TACK_START_SPEED_THRESHOLD * averageSpeedIntoTack.getObject().getKnots()) {
|
||||
Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing> potentiallyFlippedCluster = clusters.stream().max((a, b)->
|
||||
(int) Math.signum(Math.abs(a.getMean().getDifferenceTo(dominantCluster.getMean()).getDegrees())-
|
||||
Math.abs(b.getMean().getDifferenceTo(dominantCluster.getMean()).getDegrees()))).get();
|
||||
for (ManeuverClassification potentiallyFlippedManeuverClassification : potentiallyFlippedCluster) {
|
||||
ManeuverClassification opposite = potentiallyFlippedManeuverClassification.getOpposite();
|
||||
|
||||
// TODO the oppositeCluster holds candidates for wind direction flipping; however, if tacking and jibing angles are similar, we may have gotten it the wrong way around and need to swap clusters altogether
|
||||
// TODO decide this based on speed comparison: the boat should approach a jibe faster than a tack, assuming they were in similar wind conditions...
|
||||
}
|
||||
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
|
||||
// TODO compute a likelihood for the tack cluster candidate actually being the tack cluster, based on the ratio between its "likelihood number" and the "likelihood numbers" of the other clusters; then use this as an additional factor for the wind fix confidences
|
||||
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);
|
||||
}
|
||||
Bearing mostLikelyCandidateForWindBearingSoFar = dominantCluster.getMean();
|
||||
// FIXME remove again when done with debugging
|
||||
int id=0;
|
||||
stringRepresentation.delete(0, stringRepresentation.length());
|
||||
stringRepresentation.append(ManeuverClassification.getToStringColumnHeaders());
|
||||
stringRepresentation.append('\n');
|
||||
stringRepresentation.append(ManeuverClassification.getToStringColumnTypes());
|
||||
stringRepresentation.append('\n');
|
||||
stringRepresentation.append(ManeuverClassification.getToStringColumnHeaders());
|
||||
stringRepresentation.append('\n');
|
||||
for (ManeuverClassification i : maneuverClassifications) {
|
||||
stringRepresentation.append(i.toString(""+id++));
|
||||
stringRepresentation.append('\n');
|
||||
}
|
||||
|
||||
private void addWindFixes(
|
||||
Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing> dominantClusterAndLikelihoodOfBeingTackCluster,
|
||||
final ManeuverType maneuverType) {
|
||||
for (ManeuverClassification mc : dominantClusterAndLikelihoodOfBeingTackCluster) {
|
||||
Wind windFromTack = new WindImpl(mc.getPosition(), mc.getTimePoint(), mc.getEstimatedWindSpeedAndBearing(maneuverType).getObject());
|
||||
add(windFromTack);
|
||||
}
|
||||
}
|
||||
|
||||
public String getStringRepresentation() {
|
||||
return stringRepresentation.toString();
|
||||
/**
|
||||
* 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>
|
||||
*
|
||||
* 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 .5 as a penalty for not having found a jibe cluster.
|
||||
*
|
||||
* @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 .5 in case there is no jibe cluster candidate
|
||||
* found within the threshold).
|
||||
*/
|
||||
private double getLikelihoodIsTackCluster(Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing> tackClusterCandidate,
|
||||
Set<Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing>> clusters) {
|
||||
double likelihoodSum = 0;
|
||||
for (ManeuverClassification mc : tackClusterCandidate) {
|
||||
likelihoodSum += mc.getLikelihoodAndTWSBasedOnSpeedAndAngle(ManeuverType.TACK).getA();
|
||||
}
|
||||
Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing> jibeClusterCandidate = getOppositeCluster(tackClusterCandidate, clusters);
|
||||
final double speedMatchFactor;
|
||||
if (jibeClusterCandidate != null) {
|
||||
for (ManeuverClassification mc : jibeClusterCandidate) {
|
||||
likelihoodSum += mc.getLikelihoodAndTWSBasedOnSpeedAndAngle(ManeuverType.JIBE).getA();
|
||||
}
|
||||
speedMatchFactor = getLikelihoodTackJibeSpeedRatio(tackClusterCandidate, jibeClusterCandidate);
|
||||
} else {
|
||||
speedMatchFactor = .5; // no jibe cluster found; penalize for not being able to compare average speeds between tacks and jibes
|
||||
}
|
||||
return likelihoodSum;
|
||||
}
|
||||
|
||||
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> jibeClusterCandidate =
|
||||
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(jibeClusterCandidate.getCentroid()).getDegrees()) < THRESHOLD_OPPOSITE_CLUSTER_DIFFERENCE_DEGREES) {
|
||||
result = jibeClusterCandidate;
|
||||
} else {
|
||||
result = null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* 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.
|
||||
* <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}.
|
||||
*/
|
||||
private double getLikelihoodTackJibeSpeedRatio(
|
||||
Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing> tackClusterCandidate,
|
||||
Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing> jibeClusterCandidate) {
|
||||
Speed tackClusterWeightedAverageSpeed = getWeightedAverageSpeed(tackClusterCandidate, ManeuverType.TACK);
|
||||
Speed jibeClusterWeightedAverageSpeed = getWeightedAverageSpeed(jibeClusterCandidate, ManeuverType.JIBE);
|
||||
// TODO ask polarService how likely the ratio we found actually is
|
||||
return .5;
|
||||
}
|
||||
|
||||
private Speed getWeightedAverageSpeed(Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing> cluster, ManeuverType maneuverType) {
|
||||
double weightSum = 0;
|
||||
ScalableSpeed scalableSpeed = new ScalableSpeed(Speed.NULL);
|
||||
for (ManeuverClassification mc : cluster) {
|
||||
weightSum += mc.getLikelihoodAndTWSBasedOnSpeedAndAngle(maneuverType).getA();
|
||||
scalableSpeed = scalableSpeed.add(new ScalableSpeed(mc.getSpeedAtManeuverStart()));
|
||||
}
|
||||
return scalableSpeed.divide(weightSum);
|
||||
}
|
||||
|
||||
/**
|
||||
* For each maneuver from <code>maneuvers</code>, two hypothetical maneuver classifications are created: one that
|
||||
* assumes the maneuver happened on a {@link LegType#DOWNWIND downwind}, the other assuming the maneuver happened on
|
||||
* an {@link LegType#UPWIND upwind} leg. The maneuver's course change direction helps decide on which tack the
|
||||
* competitor sailed when starting the maneuver.
|
||||
* <p>
|
||||
*
|
||||
* Each such hypothetical maneuver classification stores the leg type and tack, the maneuver's course change angle
|
||||
* and the speed and course before the maneuver. Additionally, from these (partly assumed) values, the
|
||||
* {@link #polarService} determines the average true wind angle and average true wind speed that the competitor
|
||||
* would have experienced at the beginning of the maneuver. The middle maneuver angle is used to infer the wind direction
|
||||
* under the assumption that the maneuver may have been a tack when on an upwind leg and a jibe when on a downwind leg.<p>
|
||||
* For each maneuver from <code>maneuvers</code>, a {@link ManeuverClassification object} is created.
|
||||
*/
|
||||
private Set<ManeuverClassification> getManeuverClassifications(final Map<Maneuver, Competitor> maneuvers) {
|
||||
private Stream<ManeuverClassification> getManeuverClassifications(final Map<Maneuver, Competitor> maneuvers) {
|
||||
Set<ManeuverClassification> maneuverClassifications = new HashSet<>();
|
||||
for (final Entry<Maneuver, Competitor> maneuverAndCompetitor : maneuvers.entrySet()) {
|
||||
// Now for each maneuver's starting speed (speed into the maneuver) get the approximated
|
||||
// wind speed and direction assuming average upwind / downwind performance based on the polar service.
|
||||
// for course changes to PORT use only UPWIND/PORT and DOWNWIND/STARBOARD and analogously for course changes to STARBOARD
|
||||
List<ManeuverClassification> currentManeuverClassifications = new ArrayList<>();
|
||||
List<ManeuverClassification> oppositeManeuverClassifications = null;
|
||||
// the following loop is executed exactly twice
|
||||
for (Pair<LegType, Tack> i : maneuverAndCompetitor.getKey().getDirectionChangeInDegrees() > 0 ?
|
||||
Arrays.asList(new Pair<LegType, Tack>(LegType.UPWIND, Tack.STARBOARD), new Pair<LegType, Tack>(LegType.DOWNWIND, Tack.PORT)) :
|
||||
Arrays.asList(new Pair<LegType, Tack>(LegType.UPWIND, Tack.PORT), new Pair<LegType, Tack>(LegType.DOWNWIND, Tack.STARBOARD))) {
|
||||
for (SpeedWithBearingWithConfidence<Void> s : polarService.getAverageTrueWindSpeedAndAngleCandidates(
|
||||
maneuverAndCompetitor.getValue().getBoat().getBoatClass(),
|
||||
maneuverAndCompetitor.getKey().getSpeedWithBearingBefore(), i.getA(), i.getB())) {
|
||||
ManeuverClassification maneuverClassification = new ManeuverClassification(
|
||||
maneuverAndCompetitor.getValue(), maneuverAndCompetitor.getKey(), i.getA(), i.getB(), s);
|
||||
// note that for different leg types the polar service may return a different number of inverse TWA/TWS lookup results
|
||||
if (oppositeManeuverClassifications != null && oppositeManeuverClassifications.size() > currentManeuverClassifications.size()) {
|
||||
oppositeManeuverClassifications.get(currentManeuverClassifications.size()).setOpposite(maneuverClassification);
|
||||
maneuverClassification.setOpposite(oppositeManeuverClassifications.get(currentManeuverClassifications.size()));
|
||||
}
|
||||
currentManeuverClassifications.add(maneuverClassification);
|
||||
maneuverClassifications.add(maneuverClassification);
|
||||
}
|
||||
oppositeManeuverClassifications = currentManeuverClassifications;
|
||||
currentManeuverClassifications = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
return maneuverClassifications;
|
||||
return maneuvers.entrySet().stream().map((maneuverAndCompetitor)->
|
||||
new ManeuverClassification(maneuverAndCompetitor.getValue(), maneuverAndCompetitor.getKey()));
|
||||
}
|
||||
|
||||
private Map<Maneuver, Competitor> getAllManeuvers() {
|
||||
|
||||
+10
-11
@@ -95,7 +95,7 @@ public class PolarResource extends AbstractSailingServerResource {
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces("text/plain;charset=UTF-8")
|
||||
@Produces("application/json;charset=UTF-8")
|
||||
@Path("windestimation/{regattaname}/races/{racename}")
|
||||
public Response getCompetitorPositions(@PathParam("regattaname") String regattaName, @PathParam("racename") String raceName) throws NotEnoughDataHasBeenAddedException {
|
||||
Regatta regatta = findRegattaByName(regattaName);
|
||||
@@ -113,16 +113,15 @@ public class PolarResource extends AbstractSailingServerResource {
|
||||
PolarDataService service = getService().getPolarDataService();
|
||||
ManeuverBasedWindEstimationTrackImpl maneuverBasedWindEstimationTrackImpl = new ManeuverBasedWindEstimationTrackImpl(
|
||||
service, trackedRace, /* millisecondsOverWhichToAverage */ 30000);
|
||||
// maneuverBasedWindEstimationTrackImpl.lockForRead();
|
||||
// try {
|
||||
// for (Wind wind : maneuverBasedWindEstimationTrackImpl.getFixes()) {
|
||||
// resultAsJson.add(serializer.serialize(wind));
|
||||
// }
|
||||
// } finally {
|
||||
// maneuverBasedWindEstimationTrackImpl.unlockAfterRead();
|
||||
// }
|
||||
ResponseBuilder responseBuilder = Response.ok(maneuverBasedWindEstimationTrackImpl.getStringRepresentation(), MediaType.TEXT_PLAIN);
|
||||
response = responseBuilder.build();
|
||||
maneuverBasedWindEstimationTrackImpl.lockForRead();
|
||||
try {
|
||||
for (Wind wind : maneuverBasedWindEstimationTrackImpl.getFixes()) {
|
||||
resultAsJson.add(serializer.serialize(wind));
|
||||
}
|
||||
} finally {
|
||||
maneuverBasedWindEstimationTrackImpl.unlockAfterRead();
|
||||
}
|
||||
response = Response.ok(resultAsJson.toJSONString(), MediaType.APPLICATION_JSON).build();
|
||||
}
|
||||
}
|
||||
return response;
|
||||
|
||||
Reference in new issue
Block a user