got the first working test case for maneuver-based wind inference; two more tests still fail with a slightly mismatching wind estimation; need to analyze further

This commit is contained in:
Axel Uhl committed 2015-01-21 21:29:04 +01:00
1 parent 0af5b465b7
commit 14412c8290
8 files changed
+173 -37

No files matched your search

@@ -37,6 +37,9 @@ public interface CourseBase extends Named {
Waypoint getFirstWaypoint();
/**
* @return the course's last waypoint if not empty, <code>null</code> otherwise. Acquires the course's read lock.
*/
Waypoint getLastWaypoint();
void addWaypoint(int zeroBasedPosition, Waypoint waypointToAdd);
@@ -112,8 +112,7 @@ public abstract class AbstractTracTracLiveTest extends StoredTrackBasedTest {
e.printStackTrace();
}
}
// TODO Start live and stored data subscribers
// test cases need to start the thread calling startController
// test cases need to start the thread calling addListenersForStoredDataAndStartController
// after adding their listeners
}
@@ -24,6 +24,7 @@ import com.sap.sailing.domain.base.RaceDefinition;
import com.sap.sailing.domain.base.Regatta;
import com.sap.sailing.domain.base.Waypoint;
import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.TrackedRaceStatusEnum;
import com.sap.sailing.domain.common.impl.DegreePosition;
import com.sap.sailing.domain.racelog.impl.EmptyRaceLogStore;
import com.sap.sailing.domain.regattalog.impl.EmptyRegattaLogStore;
@@ -35,6 +36,7 @@ import com.sap.sailing.domain.tracking.impl.DynamicTrackedRegattaImpl;
import com.sap.sailing.domain.tracking.impl.EmptyWindStore;
import com.sap.sailing.domain.tracking.impl.GPSFixImpl;
import com.sap.sailing.domain.tracking.impl.TrackedLegImpl;
import com.sap.sailing.domain.tracking.impl.TrackedRaceStatusImpl;
import com.sap.sailing.domain.tractracadapter.Receiver;
import com.sap.sailing.domain.tractracadapter.ReceiverType;
import com.sap.sailing.domain.tractracadapter.TracTracConnectionConstants;
@@ -109,11 +111,7 @@ public abstract class OnlineTracTracBasedTest extends AbstractTracTracLiveTest {
throws InterruptedException {
setStoredDataLoaded(false);
ArrayList<Receiver> receivers = new ArrayList<Receiver>();
for (Receiver r : domainFactory.getUpdateReceivers(trackedRegatta, getTracTracRace(), EmptyWindStore.INSTANCE, /* delayToLiveInMillis */0l, /* simulator */null, new DynamicRaceDefinitionSet() {
@Override
public void addRaceDefinition(RaceDefinition race, DynamicTrackedRace trackedRace) {
}
},
for (Receiver r : domainFactory.getUpdateReceivers(trackedRegatta, getTracTracRace(), EmptyWindStore.INSTANCE, /* delayToLiveInMillis */0l, /* simulator */null, createRaceDefinitionSet(),
/* trackedRegattaRegistry */null,
/* courseDesignUpdateURI */null, /* tracTracUsername */null, null, /* tracTracPassword */
getEventSubscriber(), getRaceSubscriber(), receiverTypes)) {
@@ -125,6 +123,31 @@ public abstract class OnlineTracTracBasedTest extends AbstractTracTracLiveTest {
@Override
public void gotStoredDataEvent(IStoredDataEvent storedDataEvent) {
TrackedRaceStatusImpl lastStatus;
switch (storedDataEvent.getType()) {
case Begin:
logger.info("Stored data begin");
lastStatus = new TrackedRaceStatusImpl(TrackedRaceStatusEnum.LOADING, 0);
if (getTrackedRace() != null) {
getTrackedRace().setStatus(lastStatus);
}
break;
case End:
logger.info("Stored data end");
lastStatus = new TrackedRaceStatusImpl(TrackedRaceStatusEnum.TRACKING, 1);
if (getTrackedRace() != null) {
getTrackedRace().setStatus(lastStatus);
}
break;
case Progress:
lastStatus = new TrackedRaceStatusImpl(TrackedRaceStatusEnum.LOADING, storedDataEvent.getProgress());
if (getTrackedRace() != null) {
getTrackedRace().setStatus(lastStatus);
}
break;
default:
break;
}
if (storedDataEvent.getProgress() == 1.0) {
synchronized (semaphor) {
storedDataLoaded = true;
@@ -164,6 +187,18 @@ public abstract class OnlineTracTracBasedTest extends AbstractTracTracLiveTest {
trackedRace = (DynamicTrackedRaceImpl) getTrackedRegatta().getTrackedRace(race);
}
/**
* Creates a race definition set that receives a call-back when the tracked race has been created. Subclasses may override this
* in order to be informed at the point in time when the race creation happens.
*/
protected DynamicRaceDefinitionSet createRaceDefinitionSet() {
return new DynamicRaceDefinitionSet() {
@Override
public void addRaceDefinition(RaceDefinition race, DynamicTrackedRace trackedRace) {
}
};
}
private void setStoredDataLoaded(boolean storedDataLoaded) {
this.storedDataLoaded = storedDataLoaded;
}
@@ -2095,23 +2095,29 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
// compute the maneuvers for competitor
com.sap.sse.common.Util.Triple<TimePoint, TimePoint, List<Maneuver>> result = null;
NavigableSet<MarkPassing> markPassings = getMarkPassings(competitor);
boolean markPassingsNotEmpty;
TimePoint extendedFrom = null;
MarkPassing crossedFinishLine = null;
// getLastWaypoint() will wait for a read lock on the course; do this outside the synchronized block to avoid
// deadlocks
final Waypoint lastWaypoint = getRace().getCourse().getLastWaypoint();
lockForRead(markPassings);
try {
markPassingsNotEmpty = markPassings != null && !markPassings.isEmpty();
if (markPassingsNotEmpty) {
extendedFrom = markPassings.iterator().next().getTimePoint();
crossedFinishLine = getMarkPassing(competitor, lastWaypoint);
if (lastWaypoint != null) {
lockForRead(markPassings);
try {
if (markPassings != null && !markPassings.isEmpty()) {
extendedFrom = markPassings.iterator().next().getTimePoint();
crossedFinishLine = getMarkPassing(competitor, lastWaypoint);
}
} finally {
unlockAfterRead(markPassings);
}
} finally {
unlockAfterRead(markPassings);
}
if (markPassingsNotEmpty) {
if (extendedFrom == null) {
GPSFixMoving firstRawFix = getTrack(competitor).getFirstRawFix();
if (firstRawFix != null) {
extendedFrom = firstRawFix.getTimePoint();
}
}
if (extendedFrom != null) {
TimePoint extendedTo;
if (crossedFinishLine != null) {
extendedTo = crossedFinishLine.getTimePoint();
@@ -2125,9 +2131,10 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
}
if (extendedTo != null) {
try {
List<Maneuver> extendedResultForCache = detectManeuvers(competitor, extendedFrom, extendedTo, /* ignoreMarkPassings */ false);
result = new com.sap.sse.common.Util.Triple<TimePoint, TimePoint, List<Maneuver>>(extendedFrom, extendedTo,
extendedResultForCache);
List<Maneuver> extendedResultForCache = detectManeuvers(competitor, extendedFrom, extendedTo, /* ignoreMarkPassings */
false);
result = new com.sap.sse.common.Util.Triple<TimePoint, TimePoint, List<Maneuver>>(extendedFrom,
extendedTo, extendedResultForCache);
} catch (NoWindException ex) {
// Catching the NoWindException here without letting it propagate thru other handlers.
// This is mainly to avoid having logs flooded with stack traces. It is safe to catch
@@ -2139,8 +2146,8 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
// recomputation.
logger.fine("NoWindException during computation of maneuvers for " + competitor.getName());
}
} // else competitor has no fixes to consider; remove any maneuver cache entry
} // else competitor hasn't started yet; remove any maneuver cache entry
}
} // else competitor has no fixes to consider; remove any maneuver cache entry
logger.finest("computeManeuvers(" + competitor.getName() + ") called in tracked race " + this + " took "
+ (System.currentTimeMillis() - startedAt) + "ms");
return result;
@@ -11,4 +11,6 @@ Require-Bundle: org.hamcrest;bundle-version="1.1.0",
org.mockito.mockito-core;bundle-version="1.10.14",
org.junit4;bundle-version="4.8.2",
com.sap.sailing.domain.test,
com.sap.sse.common
com.sap.sse.common,
com.sap.sailing.domain.tractracadapter,
com.tractrac.clientmodule;bundle-version="3.0.7"
@@ -0,0 +1,90 @@
package com.sap.sailing.polars.windestimation.test;
import static org.junit.Assert.assertEquals;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.ParseException;
import java.util.concurrent.Executors;
import org.junit.Test;
import com.sap.sailing.domain.test.OnlineTracTracBasedTest;
import com.sap.sailing.domain.tracking.Wind;
import com.sap.sailing.domain.tracking.impl.ScalableWind;
import com.sap.sailing.domain.tractracadapter.ReceiverType;
import com.sap.sailing.polars.impl.PolarDataServiceImpl;
import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException;
import com.sap.sailing.polars.windestimation.ManeuverBasedWindEstimationTrackImpl;
import com.tractrac.model.lib.api.event.CreateModelException;
import com.tractrac.subscription.lib.api.SubscriberInitializationException;
public class TestWindEstimationFromManeuversOnAFew505Races extends OnlineTracTracBasedTest {
public TestWindEstimationFromManeuversOnAFew505Races() throws URISyntaxException, MalformedURLException {
super();
}
public void setUp(final String fileBaseName) throws MalformedURLException, IOException, InterruptedException, URISyntaxException,
ParseException, SubscriberInitializationException, CreateModelException {
super.setUp();
URI storedUri = new URI("file:///"+new File("../com.sap.sailing.domain.test/resources/"+fileBaseName+".mtb").getCanonicalPath().replace('\\', '/'));
super.setUp(new URL("file:///"+new File("../com.sap.sailing.domain.test/resources/"+fileBaseName+".txt").getCanonicalPath()),
/* liveUri */ null, /* storedUri */ storedUri,
new ReceiverType[] { ReceiverType.RACECOURSE, ReceiverType.RAWPOSITIONS }); // only the tracks; no mark positions, no wind, no mark passings
}
@Test
public void testWindEstimationFromManeuversOn505KW2011Race2() throws MalformedURLException, IOException,
InterruptedException, URISyntaxException, ParseException, SubscriberInitializationException,
CreateModelException, NotEnoughDataHasBeenAddedException {
setUp("event_20110609_KielerWoch-505_Race_2");
Wind average = getManeuverBasedAverageWind();
assertEquals(235, average.getFrom().getDegrees(), 5.0); // wind in this race was from 075deg on average
}
@Test
public void testWindEstimationFromManeuversOn505KW2011Race3() throws MalformedURLException, IOException,
InterruptedException, URISyntaxException, ParseException, SubscriberInitializationException,
CreateModelException, NotEnoughDataHasBeenAddedException {
setUp("event_20110609_KielerWoch-505_Race_3");
Wind average = getManeuverBasedAverageWind();
assertEquals(245, average.getFrom().getDegrees(), 5.0); // wind in this race was from 075deg on average
}
@Test
public void testWindEstimationFromManeuversOn505KW2011Race4() throws MalformedURLException, IOException,
InterruptedException, URISyntaxException, ParseException, SubscriberInitializationException,
CreateModelException, NotEnoughDataHasBeenAddedException {
setUp("event_20110609_KielerWoch-505_Race_4");
Wind average = getManeuverBasedAverageWind();
assertEquals(275, average.getFrom().getDegrees(), 5.0); // wind in this race was from 075deg on average
}
private Wind getManeuverBasedAverageWind() throws NotEnoughDataHasBeenAddedException {
ManeuverBasedWindEstimationTrackImpl windTrack = new ManeuverBasedWindEstimationTrackImpl(new PolarDataServiceImpl(Executors.newFixedThreadPool(4)),
getTrackedRace(), /* millisecondsOverWhichToAverage */ 30000, /* waitForLatest */ true);
ScalableWind windSum = null;
int count = 0;
windTrack.lockForRead();
try {
for (Wind wind : windTrack.getFixes()) {
final ScalableWind scalableWind = new ScalableWind(wind, /* useSpeed */ true);
if (windSum == null) {
windSum = scalableWind;
} else {
windSum = windSum.add(scalableWind);
}
count++;
}
} finally {
windTrack.unlockAfterRead();
}
Wind average = windSum.divide(count);
return average;
}
}
@@ -66,13 +66,13 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl {
private final TrackedRace trackedRace;
public ManeuverBasedWindEstimationTrackImpl(PolarDataService polarService, TrackedRace trackedRace, long millisecondsOverWhichToAverage)
public ManeuverBasedWindEstimationTrackImpl(PolarDataService polarService, TrackedRace trackedRace, long millisecondsOverWhichToAverage, boolean waitForLatest)
throws NotEnoughDataHasBeenAddedException {
super(millisecondsOverWhichToAverage, DEFAULT_BASE_CONFIDENCE, /* useSpeed */ false,
/* nameForReadWriteLock */ ManeuverBasedWindEstimationTrackImpl.class.getName());
this.polarService = polarService;
this.trackedRace = trackedRace;
analyzeRace();
analyzeRace(waitForLatest);
}
/**
@@ -289,8 +289,8 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl {
*
* TODO with enough candidates at hand, apply temporal and spatial segmentation to account for heterogeneous wind fields
*/
private void analyzeRace() throws NotEnoughDataHasBeenAddedException {
final Map<Maneuver, Competitor> maneuvers = getAllManeuvers();
private void 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 =
@@ -336,12 +336,12 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl {
*
* 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.
* sense. Otherwise, the value is scaled down by a factor of .25 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
* speed ratio between tack and jibe cluster makes sense (or .25 in case there is no jibe cluster candidate
* found within the threshold).
*/
private double getLikelihoodIsTackCluster(Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing> tackClusterCandidate,
@@ -360,7 +360,7 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl {
}
speedMatchFactor = getLikelihoodTackJibeSpeedRatio(tackClusterCandidate, jibeClusterCandidate);
} else {
speedMatchFactor = .5; // no jibe cluster found; penalize for not being able to compare average speeds between tacks and jibes
speedMatchFactor = .25; // no jibe cluster found; penalize for not being able to compare average speeds between tacks and jibes
}
return speedMatchFactor * tackClusterLikelihood * jibeClusterLikelihood;
}
@@ -370,13 +370,13 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl {
Set<Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing>> clusters) {
final Bearing clusterCentroid = cluster.getCentroid();
final Bearing opposite = clusterCentroid.reverse();
Cluster<ManeuverClassification, DoublePair, Bearing, ScalableBearing> jibeClusterCandidate =
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(jibeClusterCandidate.getCentroid()).getDegrees()) < THRESHOLD_OPPOSITE_CLUSTER_DIFFERENCE_DEGREES) {
result = jibeClusterCandidate;
if (Math.abs(opposite.getDifferenceTo(oppositeClusterCandidate.getCentroid()).getDegrees()) < THRESHOLD_OPPOSITE_CLUSTER_DIFFERENCE_DEGREES) {
result = oppositeClusterCandidate;
} else {
result = null;
}
@@ -422,7 +422,7 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl {
new ManeuverClassification(maneuverAndCompetitor.getValue(), maneuverAndCompetitor.getKey()));
}
private Map<Maneuver, Competitor> getAllManeuvers() {
private Map<Maneuver, Competitor> getAllManeuvers(boolean waitForLatest) {
Map<Maneuver, Competitor> maneuvers = new HashMap<>();
final Waypoint firstWaypoint = trackedRace.getRace().getCourse().getFirstWaypoint();
final Waypoint lastWaypoint = trackedRace.getRace().getCourse().getLastWaypoint();
@@ -432,9 +432,9 @@ public class ManeuverBasedWindEstimationTrackImpl extends WindTrackImpl {
trackedRace.getStartOfRace(), trackedRace.getStartOfTracking());
final TimePoint to = Util.getFirstNonNull(
lastWaypoint == null ? null : trackedRace.getMarkPassing(competitor, lastWaypoint) == null ? null : trackedRace.getMarkPassing(competitor, lastWaypoint).getTimePoint(),
trackedRace.getEndOfRace() , trackedRace.getEndOfTracking(),
trackedRace.getEndOfRace(), trackedRace.getEndOfTracking(),
MillisecondsTimePoint.now());
for (Maneuver maneuver : trackedRace.getManeuvers(competitor, from, to, /* waitForLatest */ false)) {
for (Maneuver maneuver : trackedRace.getManeuvers(competitor, from, to, waitForLatest)) {
maneuvers.put(maneuver, competitor);
}
}
@@ -112,7 +112,7 @@ public class PolarResource extends AbstractSailingServerResource {
WindJsonSerializer serializer = new WindJsonSerializer(new PositionJsonSerializer());
PolarDataService service = getService().getPolarDataService();
ManeuverBasedWindEstimationTrackImpl maneuverBasedWindEstimationTrackImpl = new ManeuverBasedWindEstimationTrackImpl(
service, trackedRace, /* millisecondsOverWhichToAverage */ 30000);
service, trackedRace, /* millisecondsOverWhichToAverage */ 30000, /* waitForLatest */ false);
maneuverBasedWindEstimationTrackImpl.lockForRead();
try {
for (Wind wind : maneuverBasedWindEstimationTrackImpl.getFixes()) {