Fixed an issue in updatePositions(...) where unconstrained bounds could be passed into the calculateVMG function (e.g., lower = -182391, upper = 188221), but they need to be in the interval [0, 360).

Mock.Polars:
Added more test cases. Still working on getting the assertions correct.
Refactored and bugfixed the calculateVMG(...) function. There were a few bugs (and ugly "fixes") because we didn't bother enforcing a strict interval on the lower and upper bound angles.
Also added a few more comments to explain the function better.

#story[873]
main
fjc40 9 years ago
parent 12b6f49f2e
commit 656212d06d

@ -45,7 +45,7 @@ public class Event {
System.out.println("Sending Boat");
sendBoatData();
int scaleFactor = 25;//TEMP - was 15.
int scaleFactor = 12;//TEMP - was 15.
Race newRace = new Race(raceDataSource, scaleFactor, mockOutput);
new Thread((newRace)).start();
}

@ -2,8 +2,7 @@ package seng302.Model;
import javafx.util.Pair;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.*;
/**
* Created by hba56 on 10/05/17.
@ -15,11 +14,12 @@ import java.util.HashMap;
public class Polars {
///Internal store of data. Maps pair<windSpeed, windAngle> to boatSpeed.
private HashMap<Pair<Double, Double>, Double> polarValues = new HashMap<>();
private Map<Pair<Double, Double>, Double> polarValues = new HashMap<>();
///Stores a list of angles from the polar table - this is used during the calculateVMG function.
///Maps between windSpeed and a list of angles for that wind speed.
private HashMap<Double, ArrayList<Double>> polarAngles = new HashMap<>();
private HashMap<Double, List<Double>> polarAngles = new HashMap<>();
@ -40,7 +40,7 @@ public class Polars {
*/
public void addEstimate(double trueWindSpeed, double relativeWindAngle, double boatSpeed){
//We also add the same values with a negative angle, as the data file contains data for 0-180 degrees, but we also need 180-360 degrees. This is because it may turn out that going 5 degrees into the wind gives us, say, 9knots, but -5 into the wind may give us 10knots, towards our destination.
//We also add the same values with a complementary angle (e.g., angle = 50, complement = 360 - 50 = 310). This is because the data file contains angles [0, 180), but we need [0, 360).
//Create the array to store angles for this wind speed if it doesn't exist.
if (!this.polarAngles.containsKey(trueWindSpeed)) {
@ -56,6 +56,7 @@ public class Polars {
while (negativeAngle >= 360d) {
negativeAngle -= 360d;
}
//Ensure that the positive and negative angles aren't the same (e.g., pos = 0, neg = 360 - 0 = 0.
if (negativeAngle != relativeWindAngle) {
Pair newKeyNegative = new Pair(trueWindSpeed, negativeAngle);
@ -63,7 +64,7 @@ public class Polars {
}
//Add angle to angle list.
//Add angle to angle list. Don't add if it already contains them.
if (!this.polarAngles.get(trueWindSpeed).contains(relativeWindAngle)) {
this.polarAngles.get(trueWindSpeed).add(relativeWindAngle);
}
@ -73,55 +74,66 @@ public class Polars {
}
}
/**
* Calculates the VMG for a given wind angle, wind speed, and angle to destination. Will only return VMGs that have a true bearing (angle) within a given bound - this is to ensure that you can calculate VMGs without going out of bounds. If you don't care about bearing bounds, simple pass in lower = 0, upper = 360.
* @param trueWindAngle The current true wind angle.
* @param trueWindSpeed The current true wind speed.
* @param destinationAngle The angle between the boat and the destination point.
* @param bearingLowerBound The lowest bearing (angle) that the boat may travel on.
* @param bearingUpperBound The highest bearing (angle) that the boat may travel on.
* The resulting angle of the VMG will be within the interval [bearingLowerBound, bearingUpperBound). Note the exclusive end of interval.
* If the lower bound is greater than the upper bound (e.g., lower = 70, upper = 55), then it checks that VMGAngle >= lower OR VMGAngle < upper (e.g., [70, 55) means angle >= 70, OR angle < 55).
* @param trueWindAngle The current true wind angle. Interval is [0, 360).
* @param trueWindSpeed The current true wind speed. Knots.
* @param destinationAngle The angle between the boat and the destination point. Interval is [0, 360).
* @param bearingLowerBound The lowest bearing (angle) that the boat may travel on. Interval is [0, 360).
* @param bearingUpperBound The highest bearing (angle) that the boat may travel on. Interval is [0, 360]. Note the inclusive end of interval.
* @return The VMG.
*/
public VMG calculateVMG(double trueWindAngle, double trueWindSpeed, double destinationAngle, double bearingLowerBound, double bearingUpperBound) {
//TODO BUG there may be a bug when you provide a bearingUpperBound < bearingLowerBound (e.g., lower = 340, upper = 15).
//Sorts polar angles.
for (List<Double> angles : this.polarAngles.values()) {
angles.sort(null);
}
//Get the lower bound into the interval [0, 360)
bearingLowerBound = ((bearingLowerBound % 360d) + 360d) % 360d;
//We use a bound of [lower, upper), so we allow the upper to be 360 degrees.
if (bearingUpperBound != 360d) {
bearingUpperBound = ((bearingUpperBound % 360d) + 360d) % 360d;
if (bearingLowerBound > bearingUpperBound) {
bearingLowerBound -= 360d;
}
//Sorts polar angles.
for (ArrayList<Double> angles : this.polarAngles.values()) {
angles.sort(null);
//If the lower bound is greater than the upper bound, we have a "flipped" interval. That is for, e.g., [70, 55) the lower bound is greater than the upper bound, and so it checks that (VMGAngle >= 70 OR VMGAngle < 55), instead of (VMGAngle >= 70 AND VMGAngle < 55).
boolean flippedInterval = false;
if (bearingLowerBound > bearingUpperBound) {
flippedInterval = true;
}
//We need to find the upper and lower wind speeds from the Polars table, for a given current wind speed (e.g., current wind speed is 11kn, therefore lower = 8kn, upper = 12kn).
double polarWindSpeedLowerBound = 0;
double polarWindSpeedUpperBound = 9999999;//Start this off with a value larger than any in the Polars table so that it actually works.
double polarWindSpeedLowerBound = 0d;
double polarWindSpeedUpperBound = 9999999d;//Start this off with a value larger than any in the Polars table so that it actually works.
//This indicates whether or not we've managed to find a wind speed larger than the current wind speed (the upper bound) in the Polars table (in cases where the current wind speed is larger than any in the file we will never find an upper bound).
boolean foundUpperBoundSpeed = false;
boolean foundLowerBoundSpeed = false;
boolean foundUpperBoundWindSpeed = false;
boolean foundLowerBoundWindSpeed = false;
for (Pair<Double, Double> key : this.polarValues.keySet()) {
//The key is Pair<windSpeed, windAngle>, so pair.key is windSpeed.
double currentPolarSpeed = key.getKey();
//Lower bound.
if ((currentPolarSpeed >= polarWindSpeedLowerBound) && (currentPolarSpeed <= trueWindSpeed)) {
polarWindSpeedLowerBound = currentPolarSpeed;
foundLowerBoundSpeed = true;
foundLowerBoundWindSpeed = true;
}
//Upper bound.
if ((currentPolarSpeed < polarWindSpeedUpperBound) && (currentPolarSpeed > trueWindSpeed)) {
polarWindSpeedUpperBound = currentPolarSpeed;
foundUpperBoundSpeed = true;
foundUpperBoundWindSpeed = true;
}
}
@ -129,70 +141,79 @@ public class Polars {
//Find the angle with the best VMG.
//We need to find the VMGs for both lower and upper bound wind speeds, and interpolate betweent them.
ArrayList<VMG> vmgs = new ArrayList<>();
//We need to find the VMGs for both lower and upper bound wind speeds, and interpolate between them.
List<VMG> vmgs = new ArrayList<>();
//Put wind speed bounds we found above into a list.
List<Double> windSpeedBounds = new ArrayList<>(2);
//Put wind speed bounds we found above into an array.
double[] windSpeedBounds = new double[2];
windSpeedBounds[0] = polarWindSpeedLowerBound;
if (foundUpperBoundSpeed) {
windSpeedBounds[1] = polarWindSpeedUpperBound;
if (foundLowerBoundWindSpeed) {
windSpeedBounds.add(polarWindSpeedLowerBound);
}
else {
//If we never found an upper bound, give it a wind speed of 0, so that we don't bother calculating a VMG for it.
windSpeedBounds[1] = 0d;
if (foundUpperBoundWindSpeed) {
windSpeedBounds.add(polarWindSpeedUpperBound);
}
//Calculate VMG for both bounds.
for (int i = 0; i < windSpeedBounds.length; i++) {
double polarWindSpeed = windSpeedBounds[i];
//We don't calculate anything for wind speeds of 0, as boats will not move.
if (polarWindSpeed == 0d) {
continue;
}
//Calculate VMG for any wind speed bounds we found.
for (double polarWindSpeed : windSpeedBounds) {
//The list of polar angles for this wind speed.
ArrayList<Double> polarAngles = this.polarAngles.get(polarWindSpeed);
List<Double> polarAngles = this.polarAngles.get(polarWindSpeed);
double bestVMGVelocity = 0;
double bestVMGSpeed = 0;
double bestVMGAngle = 0;
//For all angles in the accepted interval (in 1 degree increments).
//Calculate the VMG for all possible angles at this wind speed.
for (double angle = 0; angle < 360; angle += 1) {
//This is the true bearing of the boat, if it went at the angle against the wind.
//For angle > 90 and angle < 270, it means that the boat is actually going _with_ the wind (gybe).
//For angle < 90 OR angle > 270, it means that the boat is going into the wind (tacking).
//For angle > 90 AND angle < 270, it means that the boat is actually going with the wind (gybing).
double trueBoatBearing = trueWindAngle + angle + 180d;
//We put trueBoatBearing into the interval [0, 360).
while (trueBoatBearing >= 360) {
trueBoatBearing -= 360;
}
//Check that the boat's bearing would actually be acceptable.
if ((trueBoatBearing <= bearingLowerBound) || (trueBoatBearing > bearingUpperBound)) {
//If the angle is too small or too great, don't use it - skip to the next iteration.
//We continue (skip to next iteration) if it is outside of the interval.
if (flippedInterval) {
//Bearing must be inside [lower, upper), where lower > upper. So, bearing must be >= lower, or bearing < upper. We use inverted logic since we are skipping if it is true.
if ((trueBoatBearing < bearingLowerBound) & (trueBoatBearing >= bearingUpperBound)) {
continue;
}
} else {
//Bearing must be inside [lower, upper).
if ((trueBoatBearing < bearingLowerBound) || (trueBoatBearing >= bearingUpperBound)) {
continue;
}
}
//Basic linear interpolation. Find the nearest two angles from the table, and interpolate between them.
//Check which pair of adjacent points the angle is between.
boolean foundInterval = false;
//Check which pair of adjacent angles the angle is between.
boolean foundAdjacentAngles = false;
double lowerBound = 0;
double upperBound = 0;
for (int j = 0; j < polarAngles.size() - 1; j++) {
if ((angle >= polarAngles.get(j)) && (angle < polarAngles.get(j + 1))) {
foundInterval = true;
lowerBound = polarAngles.get(j);
upperBound = polarAngles.get(j + 1);
for (int i = 0; i < polarAngles.size() - 1; i++) {
//Check that angle is in interval [lower, upper).
if ((angle >= polarAngles.get(i)) && (angle < polarAngles.get(i + 1))) {
foundAdjacentAngles = true;
lowerBound = polarAngles.get(i);
upperBound = polarAngles.get(i + 1);
break;
}
}
if (!foundInterval) {
if (!foundAdjacentAngles) {
//If we never found the interval, then it must be the "last" interval, between the i'th and 0'th values - angles are periodic, so they wrap around.
lowerBound = polarAngles.get(polarAngles.size() - 1);
upperBound = polarAngles.get(0);
@ -217,13 +238,15 @@ public class Polars {
//This is the delta angle between the boat's true bearing and the destination.
double angleBetweenDestAndTack = trueBoatBearing - destinationAngle;
//This is the estimated velocity towards the target (e.g., angling away from the target reduces velocity).
double vmgTemp = Math.cos(Math.toRadians(angleBetweenDestAndTack)) * interpolatedSpeed;
double interpolatedVelocity = Math.cos(Math.toRadians(angleBetweenDestAndTack)) * interpolatedSpeed;
//Check that the velocity is better.
if (vmgTemp > bestVMGVelocity) {
bestVMGVelocity = interpolatedSpeed;
//Check that the velocity is better, if so, update our best VMG so far, for this wind speed.
if (interpolatedVelocity > bestVMGVelocity) {
bestVMGVelocity = interpolatedVelocity;
bestVMGSpeed = interpolatedSpeed;
bestVMGAngle = trueBoatBearing;
}
@ -231,17 +254,16 @@ public class Polars {
//Angle iteration loop is finished.
//Create the VMG, and add to list.
VMG vmg = new VMG(bestVMGVelocity, bestVMGAngle);
VMG vmg = new VMG(bestVMGSpeed, bestVMGAngle);
vmgs.add(vmg);
}
//If we never found an upper bound for the wind speed, we will only have one VMG (for the lower bound), so we can't interpolate/extrapolate anything.
if (!foundUpperBoundSpeed) {
if (!foundUpperBoundWindSpeed) {
return vmgs.get(0);
}
else {
} else {
//We may have more than one VMG. If we found an upper and lower bound we will have two, if we only found an upper bound (e.g., wind speed = 2kn, upper = 4kn, lower = n/a) we will only have one VMG, but must interpolate between that and a new VMG with 0kn speed.
//We do a simple linear interpolation.
@ -251,10 +273,11 @@ public class Polars {
if (vmgs.size() > 1) {
//If we have a second VMG use it.
vmg2 = vmgs.get(1);
}
else {
//Otherwise create a VMG with zero speed, but the same angle.
vmg2 = new VMG(0, vmg1.getBearing());
} else {
//Otherwise create a VMG with zero speed, but the same angle. This is what our VMG would be with 0 knot wind speed (boats don't move at 0 knots).
//We also need to swap them around, as vmg1 needs to be the vmg for the lower bound wind speed, and vmg2 is the upper bound wind speed.
vmg2 = vmg1;
vmg1 = new VMG(0, vmg1.getBearing());
}
@ -265,6 +288,7 @@ public class Polars {
double interpolatedSpeed = calculateLinearInterpolation(vmg1.getSpeed(), vmg2.getSpeed(), interpolationScalar);
double interpolatedAngle = calculateLinearInterpolation(vmg1.getBearing(), vmg2.getBearing(), interpolationScalar);
//Return the interpolated VMG.
return new VMG(interpolatedSpeed, interpolatedAngle);
@ -353,10 +377,10 @@ public class Polars {
}
/**
* Returns the hashmap used to store polar data.
* @return A hashmap containing estimated boat speeds for a given (windSpeed, windAngle) pair.
* Returns the map used to store polar data.
* @return A map containing estimated boat speeds for a given (windSpeed, windAngle) pair.
*/
public HashMap<Pair<Double, Double>, Double> getPolarValues() {
public Map<Pair<Double, Double>, Double> getPolarValues() {
return polarValues;
}
}

@ -341,6 +341,9 @@ public class Race implements Runnable {
bound1 = boat.getHeading() - turnAngle;
bound2 = boat.getHeading() + turnAngle;
//The bounds cap out at [0, 360).
bound1 = Math.max(bound1, 0);
bound2 = Math.min(bound2, 360);
VMG newHeading = boat.getPolars().calculateVMG(this.windDirection, this.windSpeed,
boat.calculateBearingToDestination(), bound1, bound2);

@ -45,7 +45,7 @@ public class PolarsTest {
double calcVMGAngle1 = calcVMG1.getBearing();
double calcVMGSpeed1 = calcVMG1.getSpeed();
System.out.println("VMG speed = " + calcVMGSpeed1 + " , VMG angle = " + calcVMGAngle1);//TEMP DEBUG REMOVE
System.out.println("Test 1 VMG speed = " + calcVMGSpeed1 + " , VMG angle = " + calcVMGAngle1);//TEMP DEBUG REMOVE
//assertEquals(calcVMGAngle1, vmgAngle1, angleEpsilon);
@ -64,7 +64,8 @@ public class PolarsTest {
double calcVMGAngle2 = calcVMG2.getBearing();
double calcVMGSpeed2 = calcVMG2.getSpeed();
System.out.println("VMG speed = " + calcVMGSpeed2 + " , VMG angle = " + calcVMGAngle2);//TEMP DEBUG REMOVE
System.out.println("Test 2 VMG speed = " + calcVMGSpeed2 + " , VMG angle = " + calcVMGAngle2);//TEMP DEBUG REMOVE
//assertEquals(calcVMGAngle2, vmgAngle2, angleEpsilon);
@ -83,7 +84,7 @@ public class PolarsTest {
double calcVMGAngle3 = calcVMG3.getBearing();
double calcVMGSpeed3 = calcVMG3.getSpeed();
System.out.println("VMG speed = " + calcVMGSpeed3 + " , VMG angle = " + calcVMGAngle3);//TEMP DEBUG REMOVE
System.out.println("Test 3 VMG speed = " + calcVMGSpeed3 + " , VMG angle = " + calcVMGAngle3);//TEMP DEBUG REMOVE
//assertEquals(calcVMGAngle3, vmgAngle3, angleEpsilon);
@ -102,12 +103,107 @@ public class PolarsTest {
double calcVMGAngle4 = calcVMG4.getBearing();
double calcVMGSpeed4 = calcVMG4.getSpeed();
System.out.println("VMG speed = " + calcVMGSpeed4 + " , VMG angle = " + calcVMGAngle4);//TEMP DEBUG REMOVE
System.out.println("Test 4 VMG speed = " + calcVMGSpeed4 + " , VMG angle = " + calcVMGAngle4);//TEMP DEBUG REMOVE
//assertEquals(calcVMGAngle4, vmgAngle4, angleEpsilon);
//assertEquals(calcVMGSpeed4, vmgSpeed4, speedEpsilon);
//Test 5.
//This test has a bearing bound of [55, 70), which only contains a suboptimal VMG.
double windAngle5 = 5;//TODO
double destAngle5 = 100;
double windSpeed5 = 9;//knots
double vmgAngle5 = 88;
double vmgSpeed5 = 12;
double bearingUpperBound5 = 70;
double bearingLowerBound5 = 55;
VMG calcVMG5 = polars.calculateVMG(windAngle5, windSpeed5, destAngle5, bearingLowerBound5, bearingUpperBound5);
double calcVMGAngle5 = calcVMG5.getBearing();
double calcVMGSpeed5 = calcVMG5.getSpeed();
System.out.println("Test 5 VMG speed = " + calcVMGSpeed5 + " , VMG angle = " + calcVMGAngle5);//TEMP DEBUG REMOVE
//assertEquals(calcVMGAngle5, vmgAngle5, angleEpsilon);
//assertEquals(calcVMGSpeed5, vmgSpeed5, speedEpsilon);
//assertTrue(calcVMGAngle5 >= bearingLowerBound5);
//assertTrue(calcVMGAngle5 < bearingUpperBound5);
//Test 6.
//This test has a bearing bound of [70, 55), which has a lower bound > upper bound, which is complementary to [55, 70).
double windAngle6 = 5;//TODO
double destAngle6 = 100;
double windSpeed6 = 11;//knots
double vmgAngle6 = 88;
double vmgSpeed6 = 12;
double bearingUpperBound6 = 55;
double bearingLowerBound6 = 70;
VMG calcVMG6 = polars.calculateVMG(windAngle6, windSpeed6, destAngle6, bearingLowerBound6, bearingUpperBound6);
double calcVMGAngle6 = calcVMG6.getBearing();
double calcVMGSpeed6 = calcVMG6.getSpeed();
System.out.println("Test 6 VMG speed = " + calcVMGSpeed6 + " , VMG angle = " + calcVMGAngle6);//TEMP DEBUG REMOVE
//assertEquals(calcVMGAngle6, vmgAngle6, angleEpsilon);
//assertEquals(calcVMGSpeed6, vmgSpeed6, speedEpsilon);
if (bearingLowerBound6 > bearingUpperBound6) {
assertTrue((calcVMGAngle6 >= bearingLowerBound6) || (calcVMGAngle6 <= bearingUpperBound6));
} else {
assertTrue(calcVMGAngle6 >= bearingLowerBound6);
assertTrue(calcVMGAngle6 < bearingUpperBound6);
}
//Test 7.
//This test has a bearing bound of [340, 5), which has a lower bound > upper bound, which is complementary to [5, 340).
double windAngle7 = 340;
double destAngle7 = 30;
double windSpeed7 = 7;//knots
double vmgAngle7 = 5;
double vmgSpeed7 = 11;
double bearingUpperBound7 = 5;
double bearingLowerBound7 = 340;
VMG calcVMG7 = polars.calculateVMG(windAngle7, windSpeed7, destAngle7, bearingLowerBound7, bearingUpperBound7);
double calcVMGAngle7 = calcVMG7.getBearing();
double calcVMGSpeed7 = calcVMG7.getSpeed();
assertEquals(calcVMGAngle7, vmgAngle7, angleEpsilon);
assertEquals(calcVMGSpeed7, vmgSpeed7, speedEpsilon);
if (bearingLowerBound7 > bearingUpperBound7) {
assertTrue((calcVMGAngle7 >= bearingLowerBound7) || (calcVMGAngle7 <= bearingUpperBound7));
} else {
assertTrue(calcVMGAngle7 >= bearingLowerBound7);
assertTrue(calcVMGAngle7 < bearingUpperBound7);
}
//Test 8.
//This test has a bearing bound of [340, 5), which has a lower bound > upper bound, which is complementary to [5, 340). Due to the wind, dest angles, and bearing bounds, it cannot actually find a VMG > 0 (valid VMGs will actually be in the angle interval [10, 190]), so it will return the VMG(angle=0, speed=0).
double windAngle8 = 5;
double destAngle8 = 100;
double windSpeed8 = 7;//knots
double vmgAngle8 = 0;
double vmgSpeed8 = 0;
double bearingUpperBound8 = 5;
double bearingLowerBound8 = 340;
VMG calcVMG8 = polars.calculateVMG(windAngle8, windSpeed8, destAngle8, bearingLowerBound8, bearingUpperBound8);
double calcVMGAngle8 = calcVMG8.getBearing();
double calcVMGSpeed8 = calcVMG8.getSpeed();
assertEquals(calcVMGAngle8, vmgAngle8, 0);
assertEquals(calcVMGSpeed8, vmgSpeed8, 0);
}
}

Loading…
Cancel
Save