mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-10-16 09:11:02 +00:00
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:
1 parent
0af5b465b7
commit
14412c8290
8 files changed
+173
-37
No files matched your search
+3
@@ -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);
|
||||
|
||||
+1
-2
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+40
-5
@@ -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;
|
||||
}
|
||||
|
||||
+22
-15
@@ -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"
|
||||
+90
@@ -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;
|
||||
}
|
||||
}
|
||||
+13
-13
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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()) {
|
||||
|
||||
Reference in new issue
Block a user