use two probabilities for start mark passings: one based on start line proximity, one based on variance of distance from start line

Change-Id: I2d25867c2b78c242e5dc007b255300d54175725e
This commit is contained in:
Axel Uhl committed 2016-03-19 22:44:56 +01:00
1 parent ab5c097c35
commit 9f6a38ec7a
2 files changed
+270 -33

No files matched your search

@@ -0,0 +1,109 @@
package com.sap.sailing.domain.markpassingcalculation.impl;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.base.DomainFactory;
import com.sap.sailing.domain.base.RaceDefinition;
import com.sap.sailing.domain.common.impl.MeterDistance;
import com.sap.sailing.domain.markpassingcalculation.impl.CandidateFinderImpl.AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine;
import com.sap.sailing.domain.tracking.DynamicTrackedRace;
public class CandidateFinderImplWhiteBoxTest {
private CandidateFinderImpl finder;
@Before
public void setUp() {
DynamicTrackedRace trackedRace = mock(DynamicTrackedRace.class);
final BoatClass boatClass = DomainFactory.INSTANCE.getOrCreateBoatClass("29er");
RaceDefinition race = mock(RaceDefinition.class);
when(trackedRace.getRace()).thenReturn(race);
when(race.getBoatClass()).thenReturn(boatClass);
when(race.getCompetitors()).thenReturn(Collections.emptySet());
finder = new CandidateFinderImpl(trackedRace);
}
@Test
public void testOneProbabilityForEmptyList() {
List<AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine> emptyDistancesToStartLineOfOtherCompetitors = new ArrayList<>();
final double probability = finder.getProbabilityOfStartBasedOnOtherCompetitorsStartLineDistances(emptyDistancesToStartLineOfOtherCompetitors,
/* startIsLine */ true);
assertEquals(1.0, probability, 0.00001);
}
@Test
public void testHighProbabilityForCloseProximityAndNoLine() {
List<AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine> distancesToStartLineOfOtherCompetitors = new ArrayList<>();
distancesToStartLineOfOtherCompetitors.add(createDistancePair(3.0, null));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(5.0, null));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(2.0, null));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(8.0, null));
final double probability = finder.getProbabilityOfStartBasedOnOtherCompetitorsStartLineDistances(distancesToStartLineOfOtherCompetitors,
/* startIsLine */ false);
assertTrue(probability > 0.95);
}
@Test
public void testHighProbabilityForCloseProximityExceptOneAndNoLine() {
List<AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine> distancesToStartLineOfOtherCompetitors = new ArrayList<>();
distancesToStartLineOfOtherCompetitors.add(createDistancePair(3.0, null));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(5.0, null));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(2.0, null));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(8.0, null));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(1.0, null));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(2.5, null));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(3.2, null));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(2739.0, null));
final double probability = finder.getProbabilityOfStartBasedOnOtherCompetitorsStartLineDistances(distancesToStartLineOfOtherCompetitors,
/* startIsLine */ false);
assertTrue("Expected probability to exceed 55% but got "+probability, probability > 0.55);
}
@Test
public void testLowProbabilityForMostFarAwayAndNoLine() {
List<AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine> distancesToStartLineOfOtherCompetitors = new ArrayList<>();
distancesToStartLineOfOtherCompetitors.add(createDistancePair(50.0, null));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(250.0, null));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(120.0, null));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(200.0, null));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(300.0, null));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(2.5, null));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(3.2, null));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(2739.0, null));
final double probability = finder.getProbabilityOfStartBasedOnOtherCompetitorsStartLineDistances(distancesToStartLineOfOtherCompetitors,
/* startIsLine */ false);
assertTrue("Expected probability to be below 10% but got "+probability, probability <= 0.1);
}
@Test
public void testHighProbabilityForLateStartAndStartIsLine() {
List<AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine> distancesToStartLineOfOtherCompetitors = new ArrayList<>();
distancesToStartLineOfOtherCompetitors.add(createDistancePair(100.0, -80.));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(120.0, -82.));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(130.0, -81.));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(90.0, -79.));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(140.0, -75.));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(150, -88.));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(135, -77.));
distancesToStartLineOfOtherCompetitors.add(createDistancePair(20.0, -10.)); // another late starter just across the line
final double probability = finder.getProbabilityOfStartBasedOnOtherCompetitorsStartLineDistances(distancesToStartLineOfOtherCompetitors,
/* startIsLine */ true);
assertTrue("Expected probability to be above 85% but got "+probability, probability >= 0.85);
}
private AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine createDistancePair(double absoluteDistanceToLine,
Double signedXTEDistanceFromLine) {
return new AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine(
new MeterDistance(absoluteDistanceToLine), signedXTEDistanceFromLine == null ? null : new MeterDistance(signedXTEDistanceFromLine));
}
}
@@ -2,6 +2,7 @@ package com.sap.sailing.domain.markpassingcalculation.impl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
@@ -35,6 +36,7 @@ import com.sap.sailing.domain.tracking.GPSFixTrack;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.TimeRange;
import com.sap.sse.common.Util;
import com.sap.sse.common.Util.Pair;
/**
* The standard implemantation of {@link CandidateFinder}. There are two kinds of {@link Candidate}s. First of all,
@@ -77,8 +79,8 @@ public class CandidateFinderImpl implements CandidateFinder {
*/
private static final double PENALTY_FOR_DISTANCE_CANDIDATES = 0.9 * PENALTY_FOR_WRONG_DIRECTION;
private static final double WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START = 0.6;
private static final double NUMBER_OF_HULL_LENGTHS_DISTANCE_FROM_START_AT_WHICH_WORST_PENALTY_APPLIES = 5;
private static final double WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START = 0.1;
private static final double NUMBER_OF_HULL_LENGTHS_DISTANCE_FROM_START_AT_WHICH_WORST_PENALTY_APPLIES = 10;
private static final Logger logger = Logger.getLogger(CandidateFinderImpl.class.getName());
@@ -426,7 +428,7 @@ public class CandidateFinderImpl implements CandidateFinder {
: PENALTY_FOR_DISTANCE_CANDIDATES * PENALTY_FOR_WRONG_SIDE;
final Double newStartProbabilityBasedOnOtherCompetitors;
if (isStartWaypoint(w)) {
newStartProbabilityBasedOnOtherCompetitors = getPenaltyForStartCandidateBasedOnOtherCompetitorsBehavior(c, t);
newStartProbabilityBasedOnOtherCompetitors = getProbabilityForStartCandidateBasedOnOtherCompetitorsBehavior(c, t);
if (newStartProbabilityBasedOnOtherCompetitors != null) {
newProbability *= newStartProbabilityBasedOnOtherCompetitors;
}
@@ -680,7 +682,7 @@ public class CandidateFinderImpl implements CandidateFinder {
// at time point t the other competitors are largely not even close to the start waypoint;
// this will make start candidates much more likely if many other competitors are also very close to the
// start, and it will help ruling out those candidates where someone is practicing a start just for themselves
startProbabilityBasedOnOtherCompetitors = getPenaltyForStartCandidateBasedOnOtherCompetitorsBehavior(c, t);
startProbabilityBasedOnOtherCompetitors = getProbabilityForStartCandidateBasedOnOtherCompetitorsBehavior(c, t);
if (startProbabilityBasedOnOtherCompetitors != null) {
probability *= startProbabilityBasedOnOtherCompetitors;
}
@@ -690,13 +692,59 @@ public class CandidateFinderImpl implements CandidateFinder {
return new XTECandidateImpl(race.getRace().getCourse().getIndexOfWaypoint(w) + 1, t, probability,
startProbabilityBasedOnOtherCompetitors, w, onCorrectSideOfWaypoint, passesInTheRightDirectionProbability);
}
static class AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine {
private final Distance absoluteGeometricDistance;
private final Distance signedProjectedDistance;
public AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine(Distance absoluteGeometricDistance,
Distance signedProjectedDistance) {
super();
this.absoluteGeometricDistance = absoluteGeometricDistance;
this.signedProjectedDistance = signedProjectedDistance;
}
public Distance getAbsoluteGeometricDistance() {
return absoluteGeometricDistance;
}
public Distance getSignedProjectedDistance() {
return signedProjectedDistance;
}
@Override
public String toString() {
return "AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine [absoluteGeometricDistance="
+ absoluteGeometricDistance + ", signedProjectedDistance=" + signedProjectedDistance + "]";
}
}
/**
* For a fixed line start it is very likely that all competitors pass the start waypoint at about the same time.
* This requires everyone to at least be pretty close to the start waypoint at the start time. Therefore, a start
* mark passing candidate is very likely when a large share of competitors is very close to the start line, whereas
* it becomes pretty unlikely when everyone is sailing around anywhere on the course or behind the line but not
* being in close proximity to the line.
* For a fixed line start it is very likely that all competitors pass the start waypoint at about the same time, or
* that only a few competitors lag behind, e.g., because of start time penalties. This requires everyone to at least
* be pretty close to the start waypoint at the start time, of if we're looking at a competitor starting late, the
* field would at least have little variance and be consistently on the course side of the start line.
* <p>
*
* Therefore, a start mark passing candidate is very likely when a large share of competitors is very close to the
* start line. If the other competitors are not very close to the start line, a start may still be likely if the
* others are on the course side and have similar distance to the start line, whereas it becomes pretty unlikely
* when everyone is sailing around anywhere on the course or behind the line but not being in close proximity to the
* line and not having similar distances to the start line while being on course side.
*
* <ul>
* <li>check distance variance of other competitors; low variance could mean they started jointly</li>
* <li>for start *line*, consider the side on which the other competitors are; don't rule out a late start if other
* competitors have low distance variance and are on course side of the line</li>
* <li>generally consider distance of other competitors to line</li>
* <li>dampen "outliers" such as trackers forgotten in harbor or trackers not currently sending (80% of competitors
* that look like they are starting seems like a good guess)</li>
* </ul>
*
* Approach: compute signed distances to start line (positive meaning on course side). Sort by absolute distance and
* compute a {@link #weight(double) weighted} average where the boats farthest away are considered less relevant than
* the ones closer, leading to a natural outlier removal.<p>
*
* For the special case of a late starter, consider the weighted variance of the signed distance from the start line.
* A low variance with a positive average (meaning on course side) indicates that everyone else may have started around the
* same time, making this a probable late starter's candidate. Therefore, low variance with positive average shall increase
* a probability that was low because of a large weighted average distance.
*
* @param t
* the time point at which to consider the other competitors behavior relative to the start waypoint
@@ -705,18 +753,18 @@ public class CandidateFinderImpl implements CandidateFinder {
* completely unlikely that a candidate for {@code c} at time {@code t} could have been a start candidate; 1
* meaning that based on the other competitors' relation to the start line at time point {@code t} is seems
* a fact that this must have been the start.
*
*/
private Double getPenaltyForStartCandidateBasedOnOtherCompetitorsBehavior(Competitor c, TimePoint t) {
private Double getProbabilityForStartCandidateBasedOnOtherCompetitorsBehavior(Competitor c, TimePoint t) {
final Double result;
if (race.getStartOfRace(/* inferred */ false) == null && race.isGateStart() != Boolean.TRUE) {
// boats within one hull length of the start line are great indicators that we're at the start:
final double hullLengthInMeters = race.getRace().getBoatClass().getHullLength().getMeters();
final Waypoint start = race.getRace().getCourse().getFirstWaypoint();
final Iterable<Pair<Position, Bearing>> crossingInformationForStart = getCrossingInformation(start, t);
if (start == null) {
result = 1.0;
} else {
int numberOfCompetitorsForWhichStartLineDistanceIsAvailable = 0;
double log10OfMeterDistanceSum = 0;
final boolean startIsLine = start.getPassingInstructions() == PassingInstruction.Line;
final List<AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine> distancesToStartLineOfOtherCompetitors = new ArrayList<>();
for (final Competitor otherCompetitor : race.getRace().getCompetitors()) {
if (otherCompetitor != c) {
final DynamicGPSFixTrack<Competitor, GPSFixMoving> track = race.getTrack(otherCompetitor);
@@ -724,29 +772,21 @@ public class CandidateFinderImpl implements CandidateFinder {
final Position estimatedPositionAtT = track.getEstimatedPosition(t, /* extrapolate */ true);
final Distance otherCompetitorsDistanceToStartAtT = getMinDistanceOrNull(calculateDistance(estimatedPositionAtT, start, t));
if (otherCompetitorsDistanceToStartAtT != null) {
numberOfCompetitorsForWhichStartLineDistanceIsAvailable++;
log10OfMeterDistanceSum += Math.log10(otherCompetitorsDistanceToStartAtT.getMeters());
final Distance crossTrackError;
if (startIsLine) {
Pair<Position, Bearing> crossingInformationForStartLine = crossingInformationForStart.iterator().next();
crossTrackError = estimatedPositionAtT.crossTrackError(crossingInformationForStartLine.getA(),
crossingInformationForStartLine.getB());
} else {
crossTrackError = null;
}
distancesToStartLineOfOtherCompetitors.add(new
AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine(otherCompetitorsDistanceToStartAtT, crossTrackError));
}
}
}
}
// TODO use a function that will approach 1 as we get, say, 50% of the other competitors within a reasonable distance from the start line, say 10m; the closer, the more likely
// To make outliers such as trackers left in the harbor not hurt the results unnecessarily, the logarithm of
// distances to the start waypoint is averaged, giving more relevance to the competitors close to the line when
// computing an arithmetic mean across the logarithms.
// f(1) := 1
// f(NUMBER_OF_HULL_LENGTHS_DISTANCE_FROM_START_AT_WHICH_WORST_PENALTY_APPLIES) := WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START
// f(x) := 1-(1-WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START)/NUMBER_OF_HULL_LENGTHS_DISTANCE_FROM_START_AT_WHICH_WORST_PENALTY_APPLIES * (hullLengthInMeters / weightedMeterAverageDistance - 1)
if (numberOfCompetitorsForWhichStartLineDistanceIsAvailable > 0) {
double log10Avg = log10OfMeterDistanceSum / numberOfCompetitorsForWhichStartLineDistanceIsAvailable;
double weightedMeterAverageDistance = Math.pow(10, log10Avg);
result = Math.max(WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START, Math.min(1.0,
(1.-(1.-WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START)/NUMBER_OF_HULL_LENGTHS_DISTANCE_FROM_START_AT_WHICH_WORST_PENALTY_APPLIES
* (weightedMeterAverageDistance / hullLengthInMeters-1))));
} else {
// no distance for any other competitor from the start line was available
result = 1.0;
}
result = getProbabilityOfStartBasedOnOtherCompetitorsStartLineDistances(distancesToStartLineOfOtherCompetitors, startIsLine);
}
} else {
result = null;
@@ -754,6 +794,94 @@ public class CandidateFinderImpl implements CandidateFinder {
return result;
}
/**
* The method computes two probabilities of the candidate being a start: one based on the other competitors largely
* being close to the start line, meaning a regular start where the competitor starts roughly at the same time as
* everybody else; the other probability computed is based on the other competitors largely being in a similar
* distance to the start line on the course side, suggesting that the competitor is starting late, but mostly
* everybody else did start earlier and around the same time. The probability returned is the probability of
* the start being the one <em>or</em> the other scenario which mathematically is represented by using the inverted
* probabilities of the two start options, multiplying them and inverting the resulting probability.
*/
Double getProbabilityOfStartBasedOnOtherCompetitorsStartLineDistances(
final List<AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine> distancesToStartLineOfOtherCompetitors, boolean startIsLine) {
final Double result;
// sort by the absolute distance to start line
// boats within one hull length of the start line are great indicators that we're at the start:
final Distance hullLength = race.getRace().getBoatClass().getHullLength();
final Distance weightedAverageAbsoluteDistance;
if (!distancesToStartLineOfOtherCompetitors.isEmpty()) {
Collections.sort(distancesToStartLineOfOtherCompetitors, (a, b)->a.getAbsoluteGeometricDistance().compareTo(b.getAbsoluteGeometricDistance()));
Distance weightedAbsoluteDistanceSum = Distance.NULL;
int i=0;
double weightSum = 0;
for (final AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine d : distancesToStartLineOfOtherCompetitors) {
final double weight = weight(((double) i)/distancesToStartLineOfOtherCompetitors.size());
final Distance weightedDistance = d.getAbsoluteGeometricDistance().scale(weight);
weightedAbsoluteDistanceSum = weightedAbsoluteDistanceSum.add(weightedDistance);
weightSum += weight;
i++;
}
weightedAverageAbsoluteDistance = weightedAbsoluteDistanceSum.scale(1. / weightSum);
final double probabilityOfStartWithOthers = Math.max(WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START, Math.min(1.0,
(1.-(1.-WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START)/NUMBER_OF_HULL_LENGTHS_DISTANCE_FROM_START_AT_WHICH_WORST_PENALTY_APPLIES
* (weightedAverageAbsoluteDistance.divide(hullLength)-1))));
if (startIsLine) {
// Now look at the variance, particularly on course side: low variance may mean we are looking at a late starter
// sort such that the boats with the least value (greatest negative amount) comes first and the last ~20% are largely ignored
Collections.sort(distancesToStartLineOfOtherCompetitors, (a, b)->a.getSignedProjectedDistance().compareTo(b.getSignedProjectedDistance()));
Distance weightedSignedDistanceSum = Distance.NULL;
weightSum = 0;
i=0;
for (final AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine d : distancesToStartLineOfOtherCompetitors) {
final double weight = weight(((double) i)/distancesToStartLineOfOtherCompetitors.size());
weightedSignedDistanceSum = weightedSignedDistanceSum.add(d.getSignedProjectedDistance().scale(
weight));
weightSum += weight;
i++;
}
final Distance weightedAverageSignedDistance = weightedSignedDistanceSum.scale(1. / weightSum);
i=0;
double signedDistanceVarianceSumInSquareMeters = 0;
weightSum = 0;
for (final AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine d : distancesToStartLineOfOtherCompetitors) {
final double differenceFromWeightedAverageInMeters = d.getSignedProjectedDistance().getMeters() - weightedAverageSignedDistance.getMeters();
final double weight = weight(((double) i)/distancesToStartLineOfOtherCompetitors.size());
signedDistanceVarianceSumInSquareMeters += weight*differenceFromWeightedAverageInMeters*differenceFromWeightedAverageInMeters;
weightSum += weight;
i++;
}
final double weightedVarianceInSquareMeters = signedDistanceVarianceSumInSquareMeters / weightSum;
final double weightedVarianceRatioToSquaredHullLength = weightedVarianceInSquareMeters / hullLength.getMeters() / hullLength.getMeters();
final double probabilityOfLateStart = Math.max(WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START, Math.min(1.0,
(1.-(1.-WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START)/NUMBER_OF_HULL_LENGTHS_DISTANCE_FROM_START_AT_WHICH_WORST_PENALTY_APPLIES
* (weightedVarianceRatioToSquaredHullLength-1))));
result = 1. - (1.-probabilityOfLateStart) * (1.-probabilityOfStartWithOthers);
} else {
result = probabilityOfStartWithOthers;
}
} else {
// no distance for any other competitor from the start line was available
result = 1.0;
}
return result;
}
/**
* Idea: sort and cut off the worst 20%. We played with weight functions for averaging the distances such that after sorting them
* in increasing order the smaller ones get greater weight and the "outlier" ones towards the end of the sorted set receive small
* weights. Still, if even the "best" (least) distances are far away, the result will be a low probability.
* A function that could be helpful is this: -atan(20*(x-0.8)) + Pi/2)/Pi. The 0.8 represents the 80% starting from which the
* weight declines sharply. The factor of 20 represents how sharp the decline is. x is between 0 and 1, representing the start
* and the end of the sorted distance collection, respectively. Outputs are between 0 and 1, representing the weight to assign
* to a distance while averaging.
*/
private double weight(double relativePositionInDistanceCollectionInIncreasingOrder) {
final double SHARPNESS_OF_DECLINE = 100;
final double CENTER_OF_DECLINE = 0.8;
return (-Math.atan(SHARPNESS_OF_DECLINE*(relativePositionInDistanceCollectionInIncreasingOrder-CENTER_OF_DECLINE))+Math.PI/2) / Math.PI;
}
private Distance getMinDistanceOrNull(List<Distance> distances) {
Distance result = null;
for (Distance d : distances) {