The Wolfram Language in a Sailboat Racing Telemetry System

Todd Gayley
Wolfram

Join the Conversation #WolframTechConf201717olframTechConf

Touring Machine

System Components

Wolfram Technologies

Device Framework  (DeviceOpen, DeviceRead, etc.)
ScheduledTasks

To read data from the boat network over a serial interface.

Wolfram Technologies

Device Framework  (DeviceOpen, DeviceRead, etc.)
ScheduledTasks

To read data from the boat network over a serial interface.

SocketListen

To establish a “micro web server” running in the background in Mathematica.
	This is how the Android tablet in the cockpit acquires data to display.

Wolfram Technologies

Device Framework  (DeviceOpen, DeviceRead, etc.)
ScheduledTasks

To read data from the boat network over a serial interface.

SocketListen

To establish a “micro web server” running in the background in Mathematica.
	This is how the Android tablet in the cockpit acquires data to display.

Alexa package

For creating the Alexa skill that runs on the Amazon Echo in the cockpit (voice interface).
	This package is freely available (PacletUpdate[“Alexa”], then type Alexa into the Documentation Center).

Wolfram Technologies

Device Framework  (DeviceOpen, DeviceRead, etc.)
ScheduledTasks

To read data from the boat network over a serial interface.

SocketListen

To establish a “micro web server” running in the background in Mathematica.
	This is how the Android tablet in the cockpit acquires data to display.

Alexa package

For creating the Alexa skill that runs on the Amazon Echo in the cockpit (voice interface).
	This package is freely available (PacletUpdate[“Alexa”], then type Alexa into the Documentation Center).

Wolfram Cloud

Hosts the Alexa skill for the voice interface.

Assorted analysis and visualization functions

For number-crunching during and after the race.

Tacking Upwind(The Importance of Wind Shifts)

boatImage[color_] :=Graphics[{color,Thickness[0.08],CapForm["Round"],Line[{Scaled@{0.33,0},Scaled@{0.66,0}}],BezierCurve[{Scaled@{0.33,0},Scaled@{0.1,0.5},Scaled@{0.5,1}}],BezierCurve[{Scaled@{0.66,0},Scaled@{0.9,0.5},Scaled@{0.5,1}}]}]

redBoat=boatImage[Red];
greenBoat=boatImage[Green];

$windArrowLength=.12;  (* fraction of total image height *)
$windArrowCenter={0.5,0.98-$windArrowLength/2};
$markHeight=.05;
$markPoint=$windArrowCenter-{0,$markHeight+.1};
$boatSize=0.1;
$startPos={0.5,0.1};
$tackingAngleLineLength = 0.15;
$cornerAngle=ArcTan[0.5/$markPoint[[2]]];

Clear[tackingDiagram]
(*
boats looks like:
  {  { boatGraphic, {trackPoints} | trackFunc}... }

*)

tackingDiagram[timeStep_Integer, speed_, windDir_Integer, boats_List, drawLaylines:(True|False), drawTackingAngles:(True|False),drawTrack:(True|False)]:=
Module[{windArrowStart,windArrowEnd, portTackingAngle, starboardTackingAngle,trackPts,currentPos},
windArrowStart=$windArrowCenter +{$windArrowLength/2 Sin[windDir Degree],$windArrowLength/2 Cos[windDir Degree]};
windArrowEnd=$windArrowCenter -{$windArrowLength/2 Sin[windDir Degree],$windArrowLength/2 Cos[windDir Degree]};
portTackingAngle=(45+windDir)Degree;
starboardTackingAngle=(-45+windDir)Degree;
Graphics[{
LightBlue, Rectangle[],
 Black,Thick,Arrow[{windArrowStart,windArrowEnd}],
Text[ToString[windDir] <> "°", Scaled@($windArrowCenter + {0.1,0}), BaseStyle->Large],
Orange,Thickness[0.01],Line[{$markPoint,$markPoint+{0,$markHeight}}],Triangle[{$markPoint+{0.005,$markHeight/2},$markPoint+{0.005,$markHeight},$markPoint+{$markHeight/1.5,3$markHeight/4}}],
If[drawLaylines,
{Yellow,Dashing[{.04,.02}],Thickness[.006],
If[windDir Degree > 45 Degree-$cornerAngle,
(* right layline intersects bottom of field *)
Line[{$markPoint,{0.5+$markPoint[[2]]Tan[(45-windDir)Degree], 0}}],
(* else, right layline intersects right edge of field *)
Line[{$markPoint,$markPoint+{0.5,-0.5 Tan[(45+windDir)Degree]}}]
],
If[windDir Degree < -45 Degree+$cornerAngle,
(* left layline intersects bottom of field *)Line[{$markPoint,{0.5-$markPoint[[2]]/Tan[(45-windDir)Degree], 0}}],
(* else, left layline intersects left edge of field *)
Line[{$markPoint,$markPoint+{-0.5,-0.5 Tan[(45-windDir)Degree]}}]
]},
(* else *)
Nothing
],
Function[boatRec,
If[ListQ[boatRec[[2]]],
trackPts=Take[boatRec[[2]],UpTo[timeStep]],
(* else is track function *)
trackPts=boatRec[[2]][timeStep,speed (* speed; adjust *),windDir]
];
currentPos=Last[trackPts];
{
Inset[boatRec[[1]],adjustBoatToCenter[currentPos[[2]]], Scaled@{0.5,0.5},Scaled@$boatSize, If[currentPos[[1]]==="port",{Cos[portTackingAngle],-Sin[portTackingAngle]},{Cos[starboardTackingAngle],-Sin[starboardTackingAngle]}], Alignment->Center],
If[drawTackingAngles,
{boatRec[[1,1,1]],Dashing[{.02,.01}],Thickness[.004],Line[{currentPos[[2]],currentPos[[2]]+$tackingAngleLineLength {Cos[(45-windDir)Degree],Sin[(45-windDir)Degree]}}],Line[{currentPos[[2]],currentPos[[2]]+$tackingAngleLineLength {-Cos[(45+windDir)Degree],Sin[(45+windDir)Degree]}}]},
(* else *)
Nothing
],
If[drawTrack,
Join[{boatRec[[1,1,1]]}, Disk[#[[2]],.005]& /@trackPts],
(* else *)
Nothing
]
}
] /@ boats
}]
]

(* Hack fix for bad rotation of Inset *)
adjustBoatToCenter[pos_]:=Scaled[pos+{0,0.018}]

Clear[trackFuncOnce]
trackFuncOnce["init"]:=trackFuncOnceHolder[0,0,0]
trackFuncOnce[timeStep_Integer, speed_,windDir_Integer] := 
Module[{currentPos, nextPos,tackingAngle,angleToMark},
Which[
timeStep===0,
	$trackFuncOnceHolder={{"port",$startPos}},
timeStep≥50,
	$trackFuncOnceHolder,
timeStep >= Length[$trackFuncOnceHolder],
	currentPos=Last[$trackFuncOnceHolder];
	tackingAngle=If[currentPos[[1]]=="port",(45+windDir)Degree,(-45+windDir)Degree];
	angleToMark=ArcTan[($markPoint[[2]]-currentPos[[2,2]])/Abs[currentPos[[2,1]]-0.4999]];
	If[currentPos[[1]]=="port",
		If[currentPos[[2,1]]>0.5  (* right side of course *)
		    && angleToMark<(45+windDir)Degree,
		(* tack *)
			nextPos={"starboard", currentPos[[2]]+speed{-Sin[Abs[-45+windDir]Degree],Cos[Abs[-45+windDir]Degree]}},
		(* else no tack *)
			nextPos={"port", currentPos[[2]]+speed{Sin[(45+windDir)Degree],Cos[(45+windDir)Degree]}}
		],
	(* else starboard tack *)
		If[currentPos[[2,1]]<=0.5  (* left side of course *)
		    && angleToMark<(45-windDir)Degree,
		(* tack *)
			nextPos={"port", currentPos[[2]]+speed{Sin[(45+windDir)Degree],Cos[(45+windDir)Degree]}},
		(* else no tack *)
			nextPos={"starboard", currentPos[[2]]+speed{-Sin[Abs[-45+windDir]Degree],Cos[Abs[-45+windDir]Degree]}}
		]
	];
	AppendTo[$trackFuncOnceHolder, nextPos],
timeStep < Length[$trackFuncOnceHolder],
	$trackFuncOnceHolder=Take[$trackFuncOnceHolder, timeStep]
]
]

Clear[trackFuncRandom]
trackFuncRandom["init"]:=trackFuncRandom[0,0,0]
trackFuncRandom[timeStep_Integer, speed_,windDir_Integer] := 
Module[{currentPos, nextPos,tackingAngle,angleToMark, doTack},
Which[
timeStep===0,
	$trackFuncRandomHolder={{"starboard",$startPos}},
timeStep≥50,
	$trackFuncRandomHolder,
timeStep >= Length[$trackFuncRandomHolder],
	currentPos=Last[$trackFuncRandomHolder];
	tackingAngle=If[currentPos[[1]]=="port",(45+windDir)Degree,(-45+windDir)Degree];
	angleToMark=ArcTan[($markPoint[[2]]-currentPos[[2,2]])/Abs[currentPos[[2,1]]-0.4999]];
	doTack=False;
	If[currentPos[[1]]=="port",
		If[currentPos[[2,1]]>0.5 , (* right side of course *)
		    If[ angleToMark<(45+windDir)Degree || RandomReal[]<0.2,
			doTack=True;
		],
		(* else left side of course *)
		    If[ angleToMark>(45-windDir)Degree && RandomReal[]<0.2,
			doTack=True;
		]
	];
	  If[!doTack,
		nextPos={"port", currentPos[[2]]+speed{Sin[(45+windDir)Degree],Cos[(45+windDir)Degree]}},
	(* else tack *)
		nextPos={"starboard", currentPos[[2]]+speed{-Sin[Abs[-45+windDir]Degree],Cos[Abs[-45+windDir]Degree]}}
	],
(* else starboard tack *)
	If[currentPos[[2,1]]<=0.5 , (* left side of course *)
	    If[angleToMark<(45-windDir)Degree|| RandomReal[]<0.2,
		doTack=True;
	],
	(* else right side of course *)
		 If[ angleToMark>(45+windDir)Degree && RandomReal[]<0.2,
		doTack=True;
	   ]
	];
	If[!doTack,
		nextPos={"starboard", currentPos[[2]]+speed{-Sin[Abs[-45+windDir]Degree],Cos[Abs[-45+windDir]Degree]}},
	(* else tack *)
		nextPos={"port", currentPos[[2]]+speed{Sin[(45+windDir)Degree],Cos[(45+windDir)Degree]}}
       ]
        ];
	AppendTo[$trackFuncRandomHolder, nextPos],
timeStep < Length[$trackFuncRandomHolder],
	$trackFuncRandomHolder=Take[$trackFuncRandomHolder, timeStep]
]
]

Manipulate[tackingDiagram[time, 0.02, windDir,{{redBoat,trackFuncOnce},{greenBoat,trackFuncRandom}},showLaylines,showTackingAngles, True], {{windDir,0, "Wind direction"},-45,45,1},{{showLaylines, False, "Show laylines"},{False,True}},{{showTackingAngles, False, "Show tacking angles"},{False,True}},{time, 0,50,1,Animator,AnimationRunning->False,AnimationRate->5,DisplayAllSteps->True,AnimationRepetitions->1,AppearanceElements->{"ProgressSlider","PlayPauseButton","ResetButton"}},LabelStyle->Medium, Initialization:>(trackFuncOnce["init"];trackFuncRandom["init"]),TrackedSymbols:>{time, windDir,showLaylines,showTackingAngles}]

Tacking Upwind(The Importance of Wind Shifts)

Clear[trackFuncWind]
trackFuncWind["init"]:=trackFuncWind[0,0,0]
trackFuncWind[timeStep_Integer, speed_,windDir_Integer] := 
Module[{currentPos, nextPos,tackingAngle,angleToMark, doTack},
Which[
timeStep===0,
	$trackFuncWindHolder={{"port",$startPos}},
timeStep≥50,
	$trackFuncWindHolder,
timeStep >= Length[$trackFuncWindHolder],
	currentPos=Last[$trackFuncWindHolder];
	tackingAngle=If[currentPos[[1]]=="port",(45+windDir)Degree,(-45+windDir)Degree];
	angleToMark=ArcTan[($markPoint[[2]]-currentPos[[2,2]])/Abs[currentPos[[2,1]]-0.4999]];
	doTack=False;
	If[currentPos[[1]]=="port",
		If[currentPos[[2,1]]>0.5 , (* right side of course *)
		    If[ angleToMark<(45+windDir)Degree || windDir≥5,
			doTack=True;
		],
		(* else left side of course *)
		    If[ angleToMark>(45-windDir)Degree && windDir≥5,
			doTack=True;
		]
	];
	  If[!doTack,
		nextPos={"port", currentPos[[2]]+speed{Sin[(45+windDir)Degree],Cos[(45+windDir)Degree]}},
	(* else tack *)
		nextPos={"starboard", currentPos[[2]]+speed{-Sin[Abs[-45+windDir]Degree],Cos[Abs[-45+windDir]Degree]}}
	],
(* else starboard tack *)
	If[currentPos[[2,1]]<=0.5 , (* left side of course *)
	    If[angleToMark<(45-windDir)Degree|| windDir≤-5,
		doTack=True;
	],
	(* else right side of course *)
		 If[ angleToMark>(45+windDir)Degree && windDir≤-5,
		doTack=True;
	   ]
	];
	If[!doTack,
		nextPos={"starboard", currentPos[[2]]+speed{-Sin[Abs[-45+windDir]Degree],Cos[Abs[-45+windDir]Degree]}},
	(* else tack *)
		nextPos={"port", currentPos[[2]]+speed{Sin[(45+windDir)Degree],Cos[(45+windDir)Degree]}}
       ]
        ];
	AppendTo[$trackFuncWindHolder, nextPos],
timeStep < Length[$trackFuncWindHolder],
	$trackFuncWindHolder=Take[$trackFuncWindHolder, timeStep]
]
]

Manipulate[tackingDiagram[time, 0.025, windDir,{{redBoat,trackFuncWind},{greenBoat,trackFuncRandom}},showLaylines,showTackingAngles, True], {{windDir,0, "Wind direction"},-45,45,1},{{showLaylines, True, "Show laylines"},{False,True}},{{showTackingAngles, False, "Show tacking angles"},{False,True}},{time, 0,50,1,Animator,AnimationRunning->False,AnimationRate->2,DisplayAllSteps->True,AnimationRepetitions->1,AppearanceElements->{"ProgressSlider","PlayPauseButton","ResetButton"}},LabelStyle->Medium, Initialization:>(trackFuncOnce["init"];trackFuncRandom["init"]),TrackedSymbols:>{time, windDir,showLaylines,showTackingAngles}]

Tacking Upwind(The Importance of Wind Shifts)

The question “Should I tack?” is not easy to answer on the race course.
	How big is the shift?
	Is this a persistent shift, or only momentary?
	Is it worth the speed loss of a tack (and 2 tacks if you are wrong)?
	Is there an overall pattern of oscillating wind today?
	
These are questions that can be answered easily by Mathematica and communicated to a display in the cockpit.

Polar Diagrams

A polar diagram is a representation of the expected performance of a sailboat in every direction relative to the wind.

It is different for each boat, and for each sail configuration and wind speed.

Often produced from theoretical considerations only (that is, wild guesses).

Polar Diagrams

Polar Diagrams

Polar Diagrams

Data from Touring Machine, jib and main sail, 10 knots of wind.

ListPolarPlot[#+{Pi/2,0}&/@culled,PolarAxes->True, AspectRatio->1,PolarGridLines->{Automatic,Range[7]},PolarAxesOrigin->{Pi/2,6},PolarTicks->{Table[{p,Abs[ p-Pi/2]180/Pi },{p,0,2Pi-Pi/6,Pi/6}],Automatic}]

Polar Diagrams

Polar Diagrams

Data from Touring Machine, spinnaker and main sail, 10 knots of wind.

ListPolarPlot[#+{Pi/2,0}&/@culledSpin,PolarAxes->True, AspectRatio->1,PolarGridLines->{Automatic,Range[8]},PolarAxesOrigin->{Pi/2,8},PolarTicks->{Table[{p,Abs[ p-Pi/2]180/Pi },{p,0,2Pi-Pi/6,Pi/6}],Automatic}]

Some Implementation

SocketListen to implement a web server in Mathematica

StartServer[]:=SocketListen["192.168.137.1:7000",boatDataServer]

(* This is the handler function for incoming HTTP requests from the tablet. *)
boatDataServer[assoc_Association]:=
Module[{json,client},
client=assoc["SourceSocket"];
json=boatRecordToJSON[$boatRecord];
WriteString[client,
"HTTP/1.1 200 OK\r\n"<>
"Content-Type: text/json\r\n"<>
"Content-Length: "<>ToString[StringLength[json]]<>"\r\n"<>"\r\n"<>
json
];
Close[client]
]

Full System Demo

Some questions the system can answer

How far to the next mark?

What is the bearing to the next mark?

How long until I reach the next mark?

Am I sailing as fast as I should be?

Am I sailing on the optimum angle to the wind?

How far away is the layline?

Should I do a jibe set or bear-away set at the next mark?

What sails should I use on the next leg?

Is there a consistent wind shift?