From bef616d474d2a25a56fd0c62d2fef4472e58c392 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Mon, 26 Jan 2015 18:14:57 +0100 Subject: [PATCH] considering head-up / bear-away clusters on port and starboard tack on the upwind legs for tack cluster likelihoods --- .../domain/test/AbstractTracTracLiveTest.java | 2 +- ...EstimationFromManeuversOnAFew505Races.java | 7 +- .../ManeuverBasedWindEstimationTrackImpl.java | 202 +++++++++++++----- 3 files changed, 156 insertions(+), 55 deletions(-) diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/AbstractTracTracLiveTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/AbstractTracTracLiveTest.java index 901f7dd24a5..83e040f62f7 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/AbstractTracTracLiveTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/AbstractTracTracLiveTest.java @@ -55,7 +55,7 @@ public abstract class AbstractTracTracLiveTest extends StoredTrackBasedTest { private IRaceSubscriber raceSubscriber; private final Collection 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(); diff --git a/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/windestimation/TestWindEstimationFromManeuversOnAFew505Races.java b/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/windestimation/TestWindEstimationFromManeuversOnAFew505Races.java index abf80a36b72..3bc763882bf 100755 --- a/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/windestimation/TestWindEstimationFromManeuversOnAFew505Races.java +++ b/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/windestimation/TestWindEstimationFromManeuversOnAFew505Races.java @@ -139,8 +139,11 @@ 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., windTrack.getWeightedAverageMiddleManeuverCOGDeg(clustersSortedByAverageTackLikelihood.get(0), ManeuverType.TACK).getDegrees(), 10.); - assertEquals(245., windTrack.getWeightedAverageMiddleManeuverCOGDeg(clustersSortedByAverageTackLikelihood.get(1), ManeuverType.TACK).getDegrees(), 10.); + assertEquals( + 245., windTrack.getWeightedAverageMiddleManeuverCOGDegAndManeuverAngleDeg( + clustersSortedByAverageTackLikelihood.get(0), ManeuverType.TACK).getA().getDegrees(), 10.); + assertEquals(245., windTrack.getWeightedAverageMiddleManeuverCOGDegAndManeuverAngleDeg( + clustersSortedByAverageTackLikelihood.get(1), ManeuverType.TACK).getA().getDegrees(), 10.); } private double getAverageLikelihood( diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/windestimation/ManeuverBasedWindEstimationTrackImpl.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/windestimation/ManeuverBasedWindEstimationTrackImpl.java index 98a38069681..f107383087f 100755 --- a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/windestimation/ManeuverBasedWindEstimationTrackImpl.java +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/windestimation/ManeuverBasedWindEstimationTrackImpl.java @@ -70,10 +70,35 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { * for another cluster to be a cluster's opposite tack cluster is determined by first computing the * {@link #getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG(Cluster, double, Bearing) angular * distance}. For each so many degrees of distance as specified by this constant, the likelihood of the cluster to be the - * opposite tack cluster is halved. + * opposite tack cluster is halved. This constant can also be used to assign likelihoods for other cluster types + * based on their distance from an expected configuration of maneuver angle and middle COG. */ private static final double ANGULAR_DISTANCE_FOR_HALF_CONFIDENCE_FOR_OPPOSITE_TACK_CLUSTER_DEG = 20.; + /** + * When one or two jibe clusters have been found whose + * {@link #getLikelihoodOfBeingJibeCluster(Speed, Stream, BoatClass) likelihood of being a jibe cluster} is 100%, + * this is the boost factor by which the base likelihood for the tack cluster candidate will be increased (up to a + * maximum of 1.0). + */ + private static final double BOOST_FACTOR_FOR_FULL_JIBE_CLUSTER_LIKELIHOOD = 0.2; + + /** + * When one or two head-up / bear-away clusters on the two tacks adjacent to the middle upwind COG have been found + * whose {@link #getLikelihoodOfClusterBasedOnDistanceFromExpected(double, Bearing, Cluster) likelihood of being + * such a cluster} is 100%, this is the boost factor by which the base likelihood for the tack cluster candidate + * will be increased (up to a maximum of 1.0). + */ + private static final double BOOST_FACTOR_FOR_FULL_HEAD_UP_BEAR_AWAY_CLUSTER_LIKELIHOOD = 0.1; + + /** + * When looking for the head-up/bear-away clusters, which average maneuver angle should be expected? This is a bit + * fuzzy because in some cases the head-up and bear-away maneuvers may be grouped into one cluster, resulting in an + * average maneuver angle that is close to zero whereas in other cases they may form two clusters, one with positive + * and the other with negative maneuver angles of somewhere between 10-20deg, typically. + */ + private static final double EXPECTED_AVERAGE_HEAD_UP_AND_BEAR_AWAY_MANEUVER_ANGLE_DEG = 10.; + /** * 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. @@ -85,11 +110,11 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { private final TrackedRace trackedRace; /** - * Caches the {@link #getWeightedAverageMiddleManeuverCOGDeg(Cluster, ManeuverType)} results because these values are required several times to + * Caches the {@link #getWeightedAverageMiddleManeuverCOGDegAndManeuverAngleDeg(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, ScalableBearingAndScalableDouble>, ManeuverType>, Bearing> + private final Map, Pair, ScalableBearingAndScalableDouble>, ManeuverType>, Pair> weightedAverageMiddleCOGForManeuverType; /** @@ -308,17 +333,16 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { * There are usually no other maneuvers with similar middle COG because boats can't sail straight against the wind. *

* - * 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, - * multiplying hundreds if not thousands of probability values would easily lead to a double value going to 0.0 - * quickly. But is adding adequate to estimate the cluster having the maximum likelihood of being the tack cluster? - * We'd only need a monotonous function.) Finally, the resulting likelihood is multiplied by how likely the speed - * ratio between the tack and jibe cluster candidates is, based on the polar diagram. For this task, the cluster's - * maneuvers' average speed (weighted by each maneuver's likelihood of being a tack) is divided by that of the jibe - * cluster's maneuvers' average speed (weighted by each maneuver's likelihood of being a jibe) and assigned a - * likelihood by the polar service. + * We evaluate each cluster's likelihood of being the tack cluster by computing the average across all its maneuvers + * for that maneuver being a tack, solely based on its speed and maneuver angle, as + * {@link PolarDataService#getManeuverLikelihoodAndTwsTwa(BoatClass, Speed, double, ManeuverType) judged by the + * polar service}. If a cluster exists that has approximately the opposite average middle COG (the jibe cluster + * candidate), the {@link #getLikelihoodOfBeingJibeCluster(Speed, Stream, BoatClass) likelihood for its maneuvers + * being jibes} are determined and may result in a "boost" for the basic probability of up to 20%. Finally, the + * resulting likelihood is multiplied by how likely the speed ratio between the tack and jibe cluster candidates is, + * based on the polar diagram. For this task, the cluster's maneuvers' average speed (weighted by each maneuver's + * likelihood of being a tack) is divided by that of the jibe cluster's maneuvers' average speed (weighted by each + * maneuver's likelihood of being a jibe) and assigned a likelihood by the polar service. *

* * The cluster that has the highest likelihood of being the tack cluster is selected. The confidence of the choice @@ -336,7 +360,8 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { * maneuver angle dimension that show low variance each, compared to all other clusters. *

* - * TODO with enough candidates at hand, apply temporal and spatial segmentation to account for heterogeneous wind fields + * TODO with enough candidates at hand, apply temporal and spatial segmentation to account for heterogeneous wind + * fields */ private Triple, Pair, ScalableBearingAndScalableDouble>>, List, Pair, ScalableBearingAndScalableDouble>>, @@ -373,9 +398,9 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { 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))). + : (tackClusters.size() == 1 ? getWeightedAverageMiddleManeuverCOGDegAndManeuverAngleDeg(tackClusters.get(0), ManeuverType.TACK).getA() + : new ScalableBearing(getWeightedAverageMiddleManeuverCOGDegAndManeuverAngleDeg(tackClusters.get(0), ManeuverType.TACK).getA()).add( + new ScalableBearing(getWeightedAverageMiddleManeuverCOGDegAndManeuverAngleDeg(tackClusters.get(1), ManeuverType.TACK).getA())). divide(2)).reverse(); final List, Pair, ScalableBearingAndScalableDouble>> jibeClusters = getJibeClusters(estimatedJibeMiddleCOG, clusters).collect(Collectors.toList()); @@ -402,12 +427,14 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { /** * Determines the likelihood of cluster being a cluster of tacks, based on the average likelihood of * the maneuver classifications in cluster 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 - * clusters that support this hypothesis. The likelihood is positively affected + * turn based on what the {@link #polarService} thinks that this + * {@link PolarDataService#getManeuverLikelihoodAndTwsTwa(BoatClass, Speed, double, ManeuverType) likelihood} is and + * based on whether we find other clusters that support this hypothesis. The likelihood is positively + * affected * *

    *
  • if a second cluster with similar average middle COG and a similar average maneuver angle with opposite sign - * is found that also has a high average likelihood of the maneuvers contained being tacks
  • + * is found that also has a high average likelihood of its maneuvers being tacks *
  • if other clusters are found that for the average true wind speed estimated from the maneuvers in * cluster 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
  • @@ -423,7 +450,7 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { * apart from the tacks even if they have similar maneuver angles. *
* - * @return a likelihood between 0..1 (inclusive) + * @return a likelihood between 0..1 (inclusive), obtained by multiplying the likelihoods of the * * @see ManeuverClassification#getLikelihoodAndTWSBasedOnSpeedAndAngle(ManeuverType) * @see PolarDataService#getManeuverLikelihoodAndTwsTwa(BoatClass, Speed, double, ManeuverType) @@ -433,7 +460,7 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { Set, Pair, ScalableBearingAndScalableDouble>> clusters) { // start with the average of all of the cluster's maneuvers' likelihood to be a tack double averageTackLikelihood = getAverageLikelihoodOfBeingManeuver(ManeuverType.TACK, cluster.stream()); - final Bearing approximateMiddleCOG = getWeightedAverageMiddleManeuverCOGDeg(cluster, ManeuverType.TACK); + final Bearing approximateMiddleCOG = getWeightedAverageMiddleManeuverCOGDegAndManeuverAngleDeg(cluster, ManeuverType.TACK).getA(); final Bearing approximateMiddleCOGForJibes = approximateMiddleCOG.reverse(); // search for the opposite tack cluster by finding the one that has the smallest angular distance to the expected // middle COG (same as for the candidate cluster) and expected maneuver angle (the cluster's average maneuver angle with inverted sign) @@ -442,9 +469,33 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG(a, -cluster.getCentroid().getB(), approximateMiddleCOG)- getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG(b, -cluster.getCentroid().getB(), approximateMiddleCOG))). orElse(null); - final double likelihoodOfOppositeCluster = getLikelihoodOfOppositeTackCluster(-cluster.getCentroid().getB(), approximateMiddleCOG, oppositeTackCluster); - - // TODO search for head-up/bear-away cluster(s) to port and to starboard of cluster's weighted middle COG + // if the opposite tack cluster is empty, it's pretty unlikely that it's a tack cluster; assign a low default probability of 10% + final double likelihoodOfOppositeCluster = oppositeTackCluster.isEmpty() ? 0.1 : + getLikelihoodOfClusterBasedOnDistanceFromExpected(-cluster.getCentroid().getB(), approximateMiddleCOG, oppositeTackCluster); + + // Now search for head-up/bear-away cluster(s) to port and to starboard of cluster's weighted middle COG + final double averageTackingAngleDeg = (Math.abs(getWeightedAverageMiddleManeuverCOGDegAndManeuverAngleDeg(cluster, ManeuverType.TACK).getB()) + + Math.abs(getWeightedAverageMiddleManeuverCOGDegAndManeuverAngleDeg(oppositeTackCluster, ManeuverType.TACK).getB())) / 2.; + final Bearing averageUpwindCOG = getWeightedAverageMiddleManeuverCOGDegAndManeuverAngleDeg(cluster, ManeuverType.TACK).getA().middle( + getWeightedAverageMiddleManeuverCOGDegAndManeuverAngleDeg(oppositeTackCluster, ManeuverType.TACK).getA()); + final Bearing expectedUpwindStarboardTackCOG = averageUpwindCOG.add(new DegreeBearingImpl(-averageTackingAngleDeg/2.)); + final Bearing expectedUpwindPortTackCOG = averageUpwindCOG.add(new DegreeBearingImpl(averageTackingAngleDeg/2.)); + final Cluster, Pair, ScalableBearingAndScalableDouble> starboardTackHeadUpAndBearAwayCluster = + clusters.stream().filter((c)->c!=cluster).min((a, b)->(int) Math.signum( + getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG(a, EXPECTED_AVERAGE_HEAD_UP_AND_BEAR_AWAY_MANEUVER_ANGLE_DEG, expectedUpwindStarboardTackCOG)- + getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG(b, EXPECTED_AVERAGE_HEAD_UP_AND_BEAR_AWAY_MANEUVER_ANGLE_DEG, expectedUpwindStarboardTackCOG))). + orElse(null); + final Cluster, Pair, ScalableBearingAndScalableDouble> portTackHeadUpAndBearAwayCluster = + clusters.stream().filter((c)->c!=cluster).min((a, b)->(int) Math.signum( + getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG(a, EXPECTED_AVERAGE_HEAD_UP_AND_BEAR_AWAY_MANEUVER_ANGLE_DEG, expectedUpwindPortTackCOG)- + getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG(b, EXPECTED_AVERAGE_HEAD_UP_AND_BEAR_AWAY_MANEUVER_ANGLE_DEG, expectedUpwindPortTackCOG))). + orElse(null); + double starboardHeadUpBearAwayClusterLikelihood = starboardTackHeadUpAndBearAwayCluster == null ? 0.0 : + getLikelihoodOfClusterBasedOnDistanceFromExpected( + EXPECTED_AVERAGE_HEAD_UP_AND_BEAR_AWAY_MANEUVER_ANGLE_DEG, expectedUpwindStarboardTackCOG, starboardTackHeadUpAndBearAwayCluster); + double portHeadUpBearAwayClusterLikelihood = portTackHeadUpAndBearAwayCluster == null ? 0.0 : + getLikelihoodOfClusterBasedOnDistanceFromExpected( + EXPECTED_AVERAGE_HEAD_UP_AND_BEAR_AWAY_MANEUVER_ANGLE_DEG, expectedUpwindPortTackCOG, portTackHeadUpAndBearAwayCluster); // under the assumption that cluster holds tacks, find the clusters that then most likely hold the jibes Stream, Pair, ScalableBearingAndScalableDouble>> jibeClusters = @@ -460,7 +511,9 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { .orElse(Stream.empty()), getBoatClass()); // Likely jibe clusters may raise the general likelihood by up to 20% of the value so far, but not to over 1.0 result = Math.min(1.0, averageTackLikelihood * likelihoodOfOppositeCluster * - (1.0 + 0.2 * tackJibeLikelihoodBasedOnSpeedRatioAndAverageManeuverAngle)); + (1.0 + BOOST_FACTOR_FOR_FULL_JIBE_CLUSTER_LIKELIHOOD * tackJibeLikelihoodBasedOnSpeedRatioAndAverageManeuverAngle + + BOOST_FACTOR_FOR_FULL_HEAD_UP_BEAR_AWAY_CLUSTER_LIKELIHOOD * starboardHeadUpBearAwayClusterLikelihood + + BOOST_FACTOR_FOR_FULL_HEAD_UP_BEAR_AWAY_CLUSTER_LIKELIHOOD * portHeadUpBearAwayClusterLikelihood)); } else { result = .1; // no elements in the cluster, therefore no average speed; low propability that this would be a tack cluster } @@ -468,28 +521,65 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { } /** - * Tells how likely it is that the two clusters are opposite tack clusters. This is determined by looking at the - * {@link #getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG(Cluster, double, Bearing) angular distance} - * between the two clusters regarding their middle COG and their average maneuver angles. The likelihood decreases exponentially - * with increasing angle differences. It is 1.0 for an exact match and reaches 0.5 for an angular distance of 20deg. + * Assigns a likelihood to a cluster based on its + * {@link #getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG(Cluster, double, Bearing) + * "angular distance"} from an expected average middle COG and Tells how likely it is that the two clusters are opposite tack clusters. This is determined + * by looking at the + * {@link #getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG(Cluster, double, Bearing) + * angular distance} between the two clusters regarding their middle COG and their average maneuver angles. The + * likelihood decreases exponentially with increasing angle differences. It is 1.0 for an exact match and reaches + * 0.5 for an angular distance of 20deg. */ - private double getLikelihoodOfOppositeTackCluster( + private double getLikelihoodOfClusterBasedOnDistanceFromExpected( double expectedManeuverAngleDeg, Bearing expectedApproximateMiddleCOG, Cluster, Pair, ScalableBearingAndScalableDouble> oppositeTackCluster) { - return Math.exp(Math.log(0.5) * (getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG( - oppositeTackCluster, expectedManeuverAngleDeg, expectedApproximateMiddleCOG) / ANGULAR_DISTANCE_FOR_HALF_CONFIDENCE_FOR_OPPOSITE_TACK_CLUSTER_DEG)); + final double clusterDistanceFromExpected = getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG( + oppositeTackCluster, expectedManeuverAngleDeg, expectedApproximateMiddleCOG); + return getLikelihoodOfClusterBasedOnDistanceFromExpected(clusterDistanceFromExpected); + } + + /** + * Assigns a likelihood to a cluster based on its + * {@link #getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG(Cluster, double, Bearing) + * "angular distance"} from an expected average middle COG and Tells how likely it is that the two clusters are + * opposite tack clusters. This is determined by looking at the + * {@link #getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG(Cluster, double, Bearing) + * angular distance} between the two clusters regarding their middle COG and their average maneuver angles. The + * likelihood decreases exponentially with increasing angle differences. It is 1.0 for an exact match and reaches + * 0.5 for an angular distance of 20deg (see + * {@link #ANGULAR_DISTANCE_FOR_HALF_CONFIDENCE_FOR_OPPOSITE_TACK_CLUSTER_DEG}). + * + * @param clusterDistanceFromExpected + * an angular distance as computed by {@link #getClusterDistance(double, Bearing, double, Bearing)} and + * {@link #getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG(Cluster, double, Bearing)} + * . + */ + private double getLikelihoodOfClusterBasedOnDistanceFromExpected(final double clusterDistanceFromExpected) { + return Math.exp(Math.log(0.5) * (clusterDistanceFromExpected / ANGULAR_DISTANCE_FOR_HALF_CONFIDENCE_FOR_OPPOSITE_TACK_CLUSTER_DEG)); + } + + /** + * A euklidian degree distance based on the two degree distances between the maneuver angles and the + * middle COGs. The expected pair is passed as expcetedManeuverAngleDeg and expectedApproximateMiddleCOG whereas + * the actuals are determined by computing the {@link ManeuverType#TACK}-weighted average for those values from cluster. + */ + private double getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG( + Cluster, Pair, ScalableBearingAndScalableDouble> cluster, + double expectedManeuverAngleDeg, Bearing expectedApproximateMiddleCOG) { + final Pair weightedAverageMiddleManeuverCOGDegAndManeuverAngleDeg = getWeightedAverageMiddleManeuverCOGDegAndManeuverAngleDeg(cluster, ManeuverType.TACK); + final Bearing weightedAverageMiddleCOG = weightedAverageMiddleManeuverCOGDegAndManeuverAngleDeg.getA(); + final Double weightedAverageManeuverAngle = weightedAverageMiddleManeuverCOGDegAndManeuverAngleDeg.getB(); + return getClusterDistance(expectedManeuverAngleDeg, expectedApproximateMiddleCOG, weightedAverageManeuverAngle, weightedAverageMiddleCOG); } /** * A euklidian degree distance based on the two degree distances between the maneuver angles and the * middle COGs. */ - private double getClusterDistanceBasedOnAverageManeuverLikelihoodAndWeightedAverageMiddleCOG( - Cluster, Pair, ScalableBearingAndScalableDouble> cluster, - double expectedManeuverAngleDeg, Bearing expectedApproximateMiddleCOG) { - final Bearing approximateMiddleCOG = getWeightedAverageMiddleManeuverCOGDeg(cluster, ManeuverType.TACK); - double middleCOGDiff = approximateMiddleCOG.getDifferenceTo(expectedApproximateMiddleCOG).getDegrees(); - double maneuverAngleDiff = cluster.getCentroid().getB() - expectedManeuverAngleDeg; + private double getClusterDistance(double expectedManeuverAngleDeg, Bearing expectedApproximateMiddleCOG, + final double weightedAverageManeuverAngle, final Bearing weightedAverageMiddleCOG) { + double middleCOGDiff = weightedAverageMiddleCOG.getDifferenceTo(expectedApproximateMiddleCOG).getDegrees(); + double maneuverAngleDiff = weightedAverageManeuverAngle - expectedManeuverAngleDeg; return Math.sqrt(middleCOGDiff*middleCOGDiff + maneuverAngleDiff*maneuverAngleDiff); } @@ -508,7 +598,7 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { /** * 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 + * {@link #getWeightedAverageMiddleManeuverCOGDegAndManeuverAngleDeg(Cluster, ManeuverType) weighted average middle COG} fits that of * the expected value (approximateMiddleCOGForJibes) within * {@link #THRESHOLD_JIBE_CLUSTER_DIFFERENCE_DEGREES} degrees. */ @@ -519,17 +609,17 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { 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). + (jibeClusterCandidate)->Math.abs(getWeightedAverageMiddleManeuverCOGDegAndManeuverAngleDeg(jibeClusterCandidate, ManeuverType.JIBE).getA(). 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 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. + * {@link PolarDataService#getManeuverLikelihoodAndTwsTwa(BoatClass, Speed, double, ManeuverType) 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 should + * show in the cluster elements. If they don't, then this should give a penalty for the cluster likelihoods. *

* * The weight of a maneuver for computing the weighted speed average is determined to be the @@ -553,6 +643,7 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { tackClusterWeightedAverageSpeed, jibeClusterWeightedAverageSpeed, boatClass); if (count[0] > 0) { double averageJibeLikelihood = likelihoodSum[0] / count[0]; + // TODO search for head-up/bear-away cluster(s) to port and to starboard of cluster's weighted middle COG result = averageJibeLikelihood * tackJibeSpeedRatioLikelihood; } else { throw new RuntimeException("Internal error: no maneuvers in jibe cluster candidate but still a valid weighted average speed "+jibeClusterWeightedAverageSpeed); @@ -606,28 +697,35 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl { /** * 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 + * weighted average of the middle COG and maneuver angle where as the weight of each maneuver the likelihood that the maneuver was of type * maneuverType is used.

* * 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( + Pair getWeightedAverageMiddleManeuverCOGDegAndManeuverAngleDeg( Cluster, Pair, ScalableBearingAndScalableDouble> cluster, ManeuverType maneuverType) { Pair, Pair, ScalableBearingAndScalableDouble>, ManeuverType> key = new Pair<>( cluster, maneuverType); - Bearing result = weightedAverageMiddleCOGForManeuverType.get(key); + Pair result = weightedAverageMiddleCOGForManeuverType.get(key); if (result == null) { - ConfidenceBasedAverager averager = ConfidenceFactory.INSTANCE + ConfidenceBasedAverager middleCOGAverager = ConfidenceFactory.INSTANCE .createAverager(/* weigher */null); - HasConfidence average = averager.getAverage( + HasConfidence average = middleCOGAverager.getAverage( cluster.stream() .map(new ManeuverClassificationToHasConfidenceAndIsScalableAdapter( maneuverType, (mc) -> new ScalableBearing(mc.getMiddleManeuverCourse()), polarService)).iterator(), /* at */null); - result = average.getObject(); + ConfidenceBasedAverager maneuverAngleAverager = ConfidenceFactory.INSTANCE + .createAverager(/* weigher */null); + HasConfidence maneuverAngleAverageDeg = maneuverAngleAverager.getAverage( + cluster.stream() + .map(new ManeuverClassificationToHasConfidenceAndIsScalableAdapter( + maneuverType, (mc) -> new ScalableDouble(mc.getManeuverAngleDeg()), + polarService)).iterator(), /* at */null); + result = new Pair<>(average.getObject(), maneuverAngleAverageDeg.getObject()); weightedAverageMiddleCOGForManeuverType.put(key, result); } return result;