Simulating a Drag Race

One of the great things about Python is how easy it is to simulate real-world events, then visualize the data from these results.

For example, a drag race: two cars moving in a straight line, accelerating over a quarter-mile. One beats the other. It’s a very clean model for creating an example simulation. So in this Notebook we’re going to:

Create a model to simulate a car Create a way to “race” both cars Graph the race data

The Cars

In order to simulate a drag race, we need to decide on some cars. I really want to pit the Dodge Challenger SRT Demon against the Tesla P100D

The Demon

The Demon is a madman’s creation of a muscle car. It’s too powerful, too dangerous, and way too expensive. It’s an homage to the power of the combustion engine, and loathe as I am to admit it, there’s a part of my American soul that looks at this thing roar down a drag strip and thinks “HELL YEAH!”

The P100D

This is a sleeping dragon. The all-electric drivetrain means the P100D can deliver ludicrous amounts of torque right away. It’s a kind of acceleration combustion engines can’t deliver — perfect for a drag strip.

By the numbers

Here’s some stats for each:

By the numbers, it’s fairly close. Over a quarter mile, the Tesla’s quickness gives it an edge, but if it maxes out top speed, the Demon could overtake it.

The Model

To simulate this thing, we need to create a Class in Python. Class es are ways of defining types of objects, each with unique characteristics and abilities.

So if we were to define a Car class, you'd have to first consider all the characteristics you need to include for this simulation. Some obvious ones:

Year

Make

Model

Top Speed

Acceleration

But we also need to track performance during the race. Since we’ll have multiple Car s, it'll be easiest to store this info inside the specific object. So we'll add the following properties:

Current Speed

Odometer

Velocity History (How fast the car was going throughout the race)

Now, let’s check out what this looks like in Python:

class Car:

"""Our Car class""" def __init__(self,

year,

make,

model,

top_speed,

acceleration

):

"""Car Constructor function"""

self.year = year

self.make = make

self.model = model

self.top_speed = top_speed

self.acceleration = acceleration

self.current_speed = 0

self.odometer = 0

self.velocity_history = []

We’re not going to actually execute this code yet, because there’s more to add to the class. Like for instance, this function to make a pretty str representation.

def __str__(self):

return str(self.year) + " " + self.make + " " + self.model

The Race

To model the race itself, we’ll need to again consider what it is we’re simulating.

Racers: a list of cars who will do the racing

Distance: how far we’ll go

Elapsed time: A built-in stopwatch

Such a Race might look like this:

class Race:

"""Our Race between two cars""" def __init__(self, racers: list, distance: float):

"""Construct the race"""

self.racers = racers

self.distance = distance

self.elapsed_time = 0

So our Race will take a list of racers , each of which will be a Car . We're going to make each race last a certain distance , like a quarter-mile. The Car s' odometers will help us figure out when a car has completed that distance.

But we still need a way to describe time.

Simulating Time

Python has a way for us to count seconds, microseconds, and milliseconds. But since we’re not actually watching real cars go down a real drag strip, there’s no reason to use real time. We can describe our own tick() function that represents whatever unit of time we like—in this case, a second.

So what happens every second?

The cars move forward some amount.

Some car will be in the lead

If the leader’s odometer is greater than or equal to the race distance, we have a winner!

We can put this logic in a method attached to the Race class called tick() :

def tick(self) -> Car:

"""

Advance the race by 1 unit of time

Return the current leader

"""

self.elapsed_time += 1



# Advance each car in the race

for car in self.racers:

car.advance() leader: Car = self.racers[0]

for car in self.racers[1:]:

if car.odometer > leader.odometer:

leader = car return leader

Did you notice that call to car.advance() for each racer? We haven't written that yet, but it'll be how we tell the Car objects how they move forward.

Car.advance()

So let’s think about how that might go. Every “tick”, or second, of our simulation, a Car must:

Speed up, if it can

Record the new speed

Advanced its distance ( odometer ) by however its current speed in miles per second

So obviously we’ll need a new function inside of Car :

def advance(self):

"""

Speed up the car...if possible

Also, we need to increment the odometer

"""

Speed up, if we can

How do we tell if we can speed up? The word “if” should be a clue. We need to determine whether the Car has reached its top speed or not.

if self.current_speed < self.top_speed:

self.current_speed += self.acceleration

Remember that acceleration was in mph/s, so we can use that unit directly inside each tick() of the simulation.

We have a problem though. Say the Demon was going 150 mph, and we accelerated. The Demon accelerates 25.22 mph every second, so that puts us over the Demon’s top speed. We’ll need a “governor” condition inside the previous condition to reset overspeed results to the top speed:

if self.current_speed < self.top_speed:

self.current_speed += self.acceleration

# The Governor

if self.current_speed > self.top_speed:

self.current_speed = self.top_speed

Record the new speed

This is easy. We just append the current_speed to the velocity_history list:

self.velocity_history.append(self.current_speed)

Advance the odometer

Each second/tick, we need to record how far we’ve traveled, which will be the current distance + current_speed in miles per second. Some quick division then. Mph / 60 = Miles/Minute; Miles/Minute / 60 = Miles/Second. So...

self.odometer += (self.current_speed / 60) / 60

Putting it all together, we get:

def advance(self):

"""

Speed up the car...if possible

Also, we need to increment the odometer

""" if self.current_speed < self.top_speed:

self.current_speed += self.acceleration

# The Governor

if self.current_speed > self.top_speed:

self.current_speed = self.top_speed



# Add speed to history

self.velocity_history.append(self.current_speed)



# Advance odometer

self.odometer += (self.current_speed / 60) / 60

Now we have a complete Car class. Check it out below and run it.

class Car:

"""Our Car class""" def __init__(self,

year,

make,

model,

top_speed,

acceleration

):

"""Car Constructor function"""

self.year = year

self.make = make

self.model = model

self.top_speed = top_speed

self.acceleration = acceleration

self.current_speed = 0

self.odometer = 0

self.velocity_history = [] def __str__(self):

return str(self.year) + " " + self.make + " " + self.model def advance(self):

"""

Speed up the car...if possible

Also, we need to increment the odometer

"""

if self.current_speed < self.top_speed:

self.current_speed += self.acceleration



# The Governor

if self.current_speed > self.top_speed:

self.current_speed = self.top_speed



# Add speed to history

self.velocity_history.append(self.current_speed)



# Advance odometer

self.odometer += (self.current_speed / 60 ) / 60

Running the Race

We have one more thing to add to Race before it's done: the run() method, which actually makes the cars race. So this this going to call tick() until there's a winner. We'll make a switch variable called finish_line to check for this condition.

Don’t forget that the way we wrote tick() , it returns the Car currently in the lead. We'll use that in our run() .

def run(self):

"""

Run the race

"""



# Switch variable for if there's a winner

finish_line = False



# Advance time until we have a winner

while not finish_line:

leader = self.tick()



# Check if the leader crossed the finish line

# If yes, return that Car

if leader.odometer >= self.distance:

return leader

Below is the full code for Race :

class Race:

"""

Our Race between two cars

""" def __init__(self, racers, distance):

"""Construct the race"""

self.racers = racers

self.distance = distance

self.elapsed_time = 0 def tick(self):

"""

Advance the race by 1 unit of time

Return the current leader

"""

self.elapsed_time += 1

for car in self.racers:

car.advance() leader = self.racers[0]

for car in self.racers[1:]:

if car.odometer > leader.odometer:

leader = car return leader



def run(self):

"""Run the race"""



# Switch variable for if there's a winner

finish_line = False



# Advance time until we have a winner

while not finish_line:

leader = self.tick() # Check if the leader crossed the finish line

# If yes, return that Car

if leader.odometer >= self.distance:

return leader

Let’s Race!

Time to make some cars. Below are definitions for the Demon and the P100D.

demon = Car(2017,"Dodge","Demon",168,25.22)

tesla = Car(2017,"Tesla","P100D",155,26.37) print(demon)

print(tesla) 2017 Dodge Demon

2017 Tesla P100D

Now, let’s build a race with these cars.

race = Race([demon, tesla],0.25)

The race is ready to be run. If we print the result, we’ll see the winner!

print(race.run()) 2017 Dodge Demon

Who won? Go back and change the distance to see if anything changes.

More Data

So running the race is fine, but we built in the ability for more interesting information than just the winner. By storing the velocity history in the racers, we can graph the race itself!

The Bokeh Library allows us to graph these lists fairly easily.

# Don't forget to run this code first!

from bokeh.plotting import figure, show

from bokeh.io import output_notebook

output_notebook()

# Create our figure

p = figure(plot_width=500, plot_height=500)

tesla_x = list(range(len(tesla.velocity_history)))

demon_x = list(range(len(demon.velocity_history)))

p.multi_line([tesla_x,demon_x],

[tesla.velocity_history,demon.velocity_history],

color=["blue","red"],

line_width=4,

)

p.title.text = "Tesla (Blue) vs. Demon (Red)"

show(p)

So you can see that the P100D is actually killing the Demon until the P100 hits its top speed, at which point the Demon overtakes the Tesla.

Going Further

Here are some recommendations for playing some more:

Add more cars! Make a huge race. Don’t forget to add them all to the graph.

Modify the Car class so that it handles acceleration differently at different speeds. These acceleration rates are averages.

class so that it handles acceleration differently at different speeds. These acceleration rates are averages. Graph distance, not just speed.

I hope you enjoyed this notebook. More to come.