Creating a Custom Controller¶
In this tutorial, we will create a custom controller that reads in commands via a .csv
file.
Prerequisites¶
Basic understanding of the command line
Basic understanding of Python
You have
wa_simulator
installed (resources for that)
Note
Note #1 This tutorial assumes you have installed the wa_simulator via Anaconda
Note
Note #2 This tutorial is meant for people using a Linux distribution or MacOS. It should work for Windows users within the Anaconda shell, but it hasn’t been tested.
Setup¶
Since wa_simulator
is installed via conda
, creating a custom demo and controller is as easy as creating a single file. That is what we’ll do in this tutorial.
Please cd
into a location where you would like to place these files. Create a directory called wa_custom_controller
with two files: custom_controller_demo.py
and controller_data.csv
. Your file structure should look like this:
wa_custom_controller
├── custom_controller_demo.py
└── controller_data.csv
Create the Controller¶
To begin, open custom_controller_demo.py
in your favorite editor. I recommend Atom or Visual Studio Code.
All controllers in the wa_simulator
must inherit from the WAController
class. Our custom class is no exception. Per the documentation, our class must implement the synchronize
, advance
abd is_ok
methods. The WAController's
constructor takes a WASystem
and a shared WAVehicleInputs
object, so we need to have those to pass it to the underlying controller. We will also need to have a csv file that we’ll read, so let’s pass that in the constructor. Let’s create our class and implement these methods.
import wa_simulator as wa
class CustomCSVController(wa.WAController):
"""Simple controller that controls the car from data in a csv file"""
def __init__(self, sys, veh_inputs, csv_file):
pass
def synchronize(self, time):
pass
def advance(self, step):
pass
def is_ok(self):
# Will just always return true
return True
With the skeleton done, let’s start implementing our classes. In the __init__
function, we need to do a bit of house keeping. First, let’s save the passed csv_file so we can edit it later. Also, we want to make sure the csv file is structured how we expect before reading it, so let’s call a function to verify everything’s correct and then read the file. The __init__
function should now look like this:
...
def __init__(self, sys, veh_inputs, csv_file):
super().__init__(sys, veh_inputs) # Calls the WAController's constructor
self.csv_file = csv_file
# to be implemented next
self.ctlr_data = self.read_file(self.csv_file)
...
Since we’ve now introduced the read_file
method, we’ll think a little bit about how we want the csv file to actually be structured.
CSV Structure¶
A comma-separated values (csv) file is a intuitive way for structuring easily manipulated data. A typical csv may look like the following:
x,y,z # Header
0,0,0 # Entries ↓
1,1,1
2,2,2
.,.,.
.,.,.
The header line describes what each column represents and each entry is the actual data. We will structure our data in the following format: time,steering,throttle,braking
. Basically, at time
, the steering
, throttle
, and braking
values will be passed to the vehicle. As an example, feel free to use the following file or create your own. Place the information in the controller_data.csv
we created earlier.
time,steering,throttle,braking
0,0,0.1,0
1,1,0.1,0
5,-1,0.1,0
7.5,-1,1,0
10,0,0,1
As you can see in the file, we should expect to see the vehicle accelerate slowly for 1 second, begin to turn right, then turn left, accelerate even more and then brake at 10 seconds.
Parse the CSV¶
Now we need to actually parse the data we created. As mentioned earlier, let’s implement a method to check that the data is in the right format and then let’s actually read in the data and place it in a variable called ctlr_data
.
To make things easier, we’ll us NumPy's
genfromtxt
method to do the heavylifting. As a result, make sure you place import numpy as np
at the top of the custom_controller_demo.py
file.
import numpy as np
...
def read_file(self, file):
# a delimiter is the thing that separates each data value in a row
# data is now a numpy array with our data
data = np.genfromtxt(file, delimiter=',', names=True)
# Errors may occur with the above method, like if delimiter is wrong or inconsistent names
# Check to make sure the data is as we expected
if data.dtype.names != ('time', 'steering', 'throttle', 'braking') or np.isnan([r.tolist() for r in data]).any():
raise ValueError('The csv file is not structured incorrectly!')
return data
...
Implement the Advance and Synchronize Methods¶
With the data parsed, we can now implement our controller logic. We basically want to check at every Synchronize
call whether the time is equal to or past some point in our data. We will then pass the vehicle inputs at the point.
The Synchronize
method is implemented as follows:
...
def synchronize(self, time):
# Check that there is still data left to read
if len(self.ctlr_data) == 0:
return
if time >= self.data['time'][0]:
# Set the vehicle inputs at that time point
self.steering = self.data['steering'][0]
self.throttle = self.data['throttle'][0]
self.braking = self.data['braking'][0]
# Remove that row in the data
self.data = np.delete(self.data, 0, axis=0)
...
That’s basically all the logic we need. The Advance
method doesn’t actually need anything else, so we can just leave it as before!
Creating our Main Method¶
We’ll now create our main function to actually run our demo. We’ll visualize the simulation using matplotlib
and keep everything else rather simple.
In custom_controller_demo.py
, underneath your custom controller, add the following:
def main():
# Create the system
sys = wa.WASystem(step_size=1e-3)
# Create the vehicle inputs object
veh_inputs = wa.WAVehicleInputs()
# Create an vehicle using a premade vehicle description
veh_filename = wa.WALinearKinematicBicycle.GO_KART_MODEL_FILE
veh = wa.WALinearKinematicBicycle(sys, veh_inputs, veh_filename)
# Visualize the simulation using matplotlib
vis = wa.WAMatplotlibVisualization(sys, veh, veh_inputs)
# Create our custom controller!
ctr = CustomCSVController(sys, veh_inputs, 'controller_data.csv')
# Instantiate the simulation manager
sim = wa.WASimulationManager(sys, veh, vis, ctr)
# Run the simulation
sim.run()
# Will call the main function when 'python custom_controller_demo.py' is run
if __name__ == "__main__":
main()
Putting it All Together¶
Your controller_data.csv
should look like this:
time,steering,throttle,braking
0,0,0.1,0
1,1,0.1,0
5,-1,0.1,0
7.5,-1,1,0
10,0,0,1
Your custom_controller_demo.py
should look like this:
import wa_simulator as wa
import numpy as np
class CustomCSVController(wa.WAController):
"""Simple controller that controls the car from data in a csv file"""
def __init__(self, sys, veh_inputs, csv_file):
super().__init__(sys, veh_inputs) # Calls the WAController's constructor
self.csv_file = csv_file
# to be implemented next
self.ctlr_data = self.read_file(self.csv_file)
def read_file(self, file):
# a delimiter is the thing that separates each data value in a row
# data is now a numpy array with our data
data = np.genfromtxt(file, delimiter=',', names=True)
# Errors may occur with the above method, like if delimiter is wrong or inconsistent names
# Check to make sure the data is as we expected
if data.dtype.names != ('time', 'steering', 'throttle', 'braking') or np.isnan([r.tolist() for r in data]).any():
raise ValueError('The csv file is not structured incorrectly!')
return data
def synchronize(self, time):
super().synchronize(time)
# Check that there is still data left to read
if len(self.ctlr_data) == 0:
return
if time >= self.ctlr_data['time'][0]:
# Set the vehicle inputs at that time point
self.steering = self.ctlr_data['steering'][0]
self.throttle = self.ctlr_data['throttle'][0]
self.braking = self.ctlr_data['braking'][0]
# Remove that row in the ctlr_data
self.ctlr_data = np.delete(self.ctlr_data, 0, axis=0)
def advance(self, step):
pass
def is_ok(self):
# Will just always return true
return True
def main():
# Create the system
sys = wa.WASystem(step_size=1e-3)
# Create the vehicle inputs object
veh_inputs = wa.WAVehicleInputs()
# Create an vehicle using a premade vehicle description
veh_filename = wa.WALinearKinematicBicycle.GO_KART_MODEL_FILE
veh = wa.WALinearKinematicBicycle(sys, veh_inputs, veh_filename)
# Visualize the simulation using matplotlib
vis = wa.WAMatplotlibVisualization(sys, veh, veh_inputs, plotter_type='multi')
# Create our custom controller!
ctr = CustomCSVController(sys, veh_inputs, 'controller_data.csv')
# Instantiate the simulation manager
sim = wa.WASimulationManager(sys, veh, vis, ctr)
# Run the simulation
sim.run()
# Will call the main function when 'python custom_controller_demo.py' is run
if __name__ == "__main__":
main()
To run the demo, run:
python custom_controller_demo.py
You can also find the code in our github repo.
A matplotlib window should pop up and the vehicle should move as we expect!
Next Steps¶
You should now have a good understanding of how a controller is made in wa_simulator
. Feel free to edit this demo or add your own logic! Happy coding!
Support¶
Contact Aaron Young for any questions or concerns regarding the contents of this repository.