Toucan Crossing Assignment
An Object-Oriented Approach
In this paper, we outline an object-oriented, easily extensible approach to modelling traffic light behavior using the Python programming language. At its core, the simulation loops over a function whose output corresponds to one second, or “frame” of real time. Every frame, all of the objects in the scene run their update functions; the timers tick forwards, and the various outputs are created.
In its current iteration, the code only updates the traffic light and illuminates the Blinkstick, but the architecture of the code makes it very simple to extend its functionality to include a 2D representation of the scene and its contents, and to simulate the behaviour of pedestrians and cars (and update the traffic light based on that simulated behaviour).
1 Class Structure
The first classes defined are the Period
and TrafficLightAssignments
classes. These classes are simply wrappers to define constants, extending the functionality of Python’s Enum
class. Their purpose is to make the code simpler and more readable: it’s more convenient to store the state of the traffic light as Period.I
than as a simple int
, and the Enum
wrapper adds little overhead to the actual runtime of the program.
All of the scene information is contained in an object of the Scene
class. The Scene
class also implements the function for running a frame of simulation.
The traffic light logic is implemented in the TrafficLight
class. The update
method runs different checks depending on the current state of the light. For example, in the first period of operation (Period.I
), the traffic light is stipulated to change if either:
- A pedestrian has requested a change (by pressing the cross button) AND at least 6 seconds have elapsed since a car was last detected, OR
- 20-60 seconds (depending on the size of the road) have elapsed since the beginning of the period.
To implement this logic, the method checks both the time since its state has changed (window.time_since_update
) and the time since a pedestrian has requested a crossing (window.time_since_button
, currently unimplemented in the rest of the code). If either of the requisite conditions (1) or (2) are met, then the traffic light changes state to the next period.
2 Simulation Output
Currently, the main output of the simulation is the Blinkstick itself, a matrix of RGB LEDs that changes color to match the current state of the traffic light. All of the interaction with the Blinkstick is sandboxed behind an Application Programming Interface (API) class titled BlinkstickAPI
in the code.
When the API class is initialized, it attempts to find a Blinkstick. If none is found, an exception is raised and the program unwinds. Once the Blinkstick has been found, it can be interacted with through the functions within the BlinkstickAPI
class.
BlinkstickAPI.clear_blinkstick
clears the Blinkstick, and is called every frame of the simulation. clear_blinkstick
is a solution to the problem otherwise encountered setting the LEDs on the Blinkstick: any LED set high in one state must be set low before switching to the next state. In future iterations of the program, it might be advisable to call this function only when the traffic light changes state. Currently, since the Blinkstick is cleared and re-lit every frame of the simulation, there is a slight but visible flicker in the Blinkstick as the simulation runs.
BlinkstickAPI.set_light
is a method designed to interact with the TrafficLightAssignments
class through use of the “splat” operator **
. This operator unpacks a dictionary of keys and values into the arguments of a function. Using BlinkstickAPI.set_light
in conjunction with the TrafficLightAssignments
class avoids unnecessary repeated work in specifying the indices and values of the desired LEDs.
The output of the simulation could easily be extended. Since the window
object contains a framework for simulating a top-down map of the crossing, it would be easy to implement a function that displays that map every frame. This could be accomplished through the terminal, with a text output, or through the use of a plotting library like matplotlib
.
3 Further Extensibility
Thanks to the object-oriented paradigm, the written code is easy to extend and change as the engineering requirements of the simulation change over time. A framework is included for scene simulation with realistic pedestrians and cars. These objects would be implemented as subclasses of the SceneObject
class, which allows the creation of arbitrary rectangular regions within the scene.
When the SceneObject
is initialized, the program checks whether the coordinates of the corners are valid, raising an exception if given invalid input. The coordinates are stored, enabling simple methods universal to SceneObject
s, particularly checking whether one SceneObject
contains another (highly important for pedestrian crossing buttons and car detectors).
To make this framework fully functional, the following features need to be implemented:
- A pathfinding function for the pedestrians and cars to update their position towards their destinations during the appropriate periods of the traffic light
- A factory function (or set of factories) that creates cars and pedestrians at random positions with random destinations throughout the simulation
- An
update
function for thePedestrian
andCar
classes that changes the simulation state appropriately.
With these simple additions, a fully-fledged traffic light simulation application could easily be created. And thanks to the class architecture, adding those features would be as simple as extending the functionality of the given classes with a few new methods.
4 Conclusion
This software demonstrates a simple simulation of the state of a “toucan crossing” style traffic light. The result is a highly-readable yet extensible codebase, which makes use of Python’s object-oriented architecture. The code interfaces with external libraries, creates physical and textual outputs, and accurately and completely implements the logic outlined in national standard LTN2/95 for traffic lights.
5 Traffic Light Periods and Timings
6 Toucan Crossing Python Code
# ++============================================++
# || Programming for Engineers: Toucan Crossing ||
# ||-------------+------------+-----------------||
# || Jasper Day | S2265891 | 2022/10/20 ||
# ++============================================++
# Description:
# ============
# `Period` tracks the state of the traffic light as the simulation advances.
# The `window` object contains a grid on which `SceneObjects` are located.
#
# As the simulation advances, every object (the traffic light and the scene
# objects) is updated. The `window` object mutates as these objects are updated.
#
# All of the logic (for traffic light state changes and `SceneObject` updates)
# is contained in the `update` implied functions for their respective members.
from blinkstick import blinkstick
import matplotlib as mpt
import numpy as np
from enum import Enum
import time
class Period(Enum):
# Enum type allows state comparison of our traffic light variable.
# `int` enum lets us augment state by addition operator +=
= 1
I = 2
II = 3
III # I, II, and III are for Mayfield Roads
# IA, IIA, and IIIA are for Westfield Mains
= 4
IA = 5
IIA = 6
IIIA # Pedestrian Cycle
= 7
IV = 8
V = 9
VI = 10
VII = 11
VIII = 12
IX # red / amber for westfield mains
= 13
IXA
class TrafficLightAssignments(dict, Enum):
# Traffic Light assignments.
# Traffic lights can be set with the splat operator **
= {
MAYFIELD_ROADS_RED "index": 0,
"name": "red"
}= {
MAYFIELD_ROADS_YELLOW "index": 1,
"name": "yellow"
}= {
MAYFIELD_ROADS_GREEN "index": 2,
"name": "green"
}= {
WESTFIELD_MAINS_RED "index": 5,
"name": "red"
}= {
WESTFIELD_MAINS_YELLOW "index": 4,
"name": "yellow"
}= {
WESTFIELD_MAINS_GREEN "index": 3,
"name": "green"
}= {
PEDESTRIAN_LIGHT_GREEN "index": 6,
"name": "green"
}= {
PEDESTRIAN_LIGHT_RED "index": 7,
"name": "red"
}
class Scene():
def __init__(self, rows, columns):
= np.zeros([rows, columns])
scene
# Global counters
= 0
time_since_car = 0
time_since_ped = 0
time_since_button # time since period updated
= 0
time_since_update
# Advance simulation by one frame
def simulation_step(self, traffic_light):
global TIME
# TODO: broadcast call for all objects to update themselves (position, state, etc)
self)
traffic_light.update(
# Print current state to terminal
print(f"State: {traffic_light.state}, Time: {self.time_since_update}")
# Tick timers forwards
+= 1
TIME self.time_since_button += 1
self.time_since_car += 1
self.time_since_ped += 1
self.time_since_update += 1
0.5)
time.sleep(
class BlinkstickAPI():
# interface to easily change traffic lights
def __init__(self):
self.bstick = blinkstick.find_first()
if self.bstick == None:
raise Exception("No blinkstick found")
def clear_colors(self):
for i in range(0,8):
self.bstick.set_color(index=i, name="black")
def set_light(self, color_dict):
# For use with TrafficLightAssignments
self.bstick.set_color(**color_dict)
class TrafficLight():
def __init__(self, state: Period):
self.state = state
self.blinkstick = BlinkstickAPI()
def update(self, window: Scene):
# Clear traffic light
self.blinkstick.clear_colors()
if self.state == Period.I:
# Vehicle Running (Mayfield Roads)
self.blinkstick.set_light(TrafficLightAssignments.MAYFIELD_ROADS_GREEN)
self.blinkstick.set_light(TrafficLightAssignments.WESTFIELD_MAINS_RED)
self.blinkstick.set_light(TrafficLightAssignments.PEDESTRIAN_LIGHT_RED)
if (window.time_since_car >= 6 and
>= 10 and
window.time_since_update >= window.time_since_button):
window.time_since_update # Gap condition with pedestrian demand
self.state = Period.II
= 0
window.time_since_update
elif window.time_since_update >= 20:
# Max vehicle running duration
self.state = Period.II
= 0
window.time_since_update
elif self.state == Period.II:
# Amber to Vehicles (Mayfield Roads)
self.blinkstick.set_light(TrafficLightAssignments.MAYFIELD_ROADS_YELLOW)
self.blinkstick.set_light(TrafficLightAssignments.WESTFIELD_MAINS_RED)
self.blinkstick.set_light(TrafficLightAssignments.PEDESTRIAN_LIGHT_RED)
if window.time_since_update >= 3:
self.state = Period.III
= 0
window.time_since_update
elif self.state == Period.III:
# Vehicle Clearance
self.blinkstick.set_light(TrafficLightAssignments.MAYFIELD_ROADS_RED)
self.blinkstick.set_light(TrafficLightAssignments.WESTFIELD_MAINS_RED)
self.blinkstick.set_light(TrafficLightAssignments.PEDESTRIAN_LIGHT_RED)
if window.time_since_car >= 6 and window.time_since_update >= 1:
# Gap condition
self.state = Period.IXA
= 0
window.time_since_update
elif window.time_since_update >= 3:
self.state = Period.IXA
= 0
window.time_since_update
if self.state == Period.IXA:
# Amber / Yellow (Westfield Mains)
self.blinkstick.set_light(TrafficLightAssignments.MAYFIELD_ROADS_RED)
self.blinkstick.set_light(TrafficLightAssignments.WESTFIELD_MAINS_RED)
self.blinkstick.set_light(TrafficLightAssignments.WESTFIELD_MAINS_YELLOW)
self.blinkstick.set_light(TrafficLightAssignments.PEDESTRIAN_LIGHT_RED)
if window.time_since_update >= 2:
self.state = Period.IA
= 0
window.time_since_update
if self.state == Period.IA:
# Vehicle Running (Westfield Mains)
self.blinkstick.set_light(TrafficLightAssignments.MAYFIELD_ROADS_RED)
self.blinkstick.set_light(TrafficLightAssignments.WESTFIELD_MAINS_GREEN)
self.blinkstick.set_light(TrafficLightAssignments.PEDESTRIAN_LIGHT_RED)
if (window.time_since_car >= 6 and
>= 10 and
window.time_since_update <= window.time_since_button):
window.time_since_update # Gap condition with pedestrian demand
self.state = Period.IIA
= 0
window.time_since_update
elif window.time_since_update >= 20:
# Max vehicle running duration
self.state = Period.IIA
= 0
window.time_since_update
elif self.state == Period.IIA:
# Amber to Vehicles (Westfield Mains)
self.blinkstick.set_light(TrafficLightAssignments.MAYFIELD_ROADS_RED)
self.blinkstick.set_light(TrafficLightAssignments.WESTFIELD_MAINS_YELLOW)
self.blinkstick.set_light(TrafficLightAssignments.PEDESTRIAN_LIGHT_RED)
if window.time_since_update >= 3:
self.state = Period.IIIA
= 0
window.time_since_update
elif self.state == Period.IIIA:
# Vehicle Clearance
self.blinkstick.set_light(TrafficLightAssignments.MAYFIELD_ROADS_RED)
self.blinkstick.set_light(TrafficLightAssignments.WESTFIELD_MAINS_RED)
self.blinkstick.set_light(TrafficLightAssignments.PEDESTRIAN_LIGHT_RED)
if window.time_since_car >= 6 and window.time_since_update >= 1:
# Gap condition
self.state = Period.IV
= 0
window.time_since_update
elif window.time_since_update >= 3:
self.state = Period.IV
= 0
window.time_since_update
elif self.state == Period.IV:
# Pedestrian Crossing
self.blinkstick.set_light(TrafficLightAssignments.MAYFIELD_ROADS_RED)
self.blinkstick.set_light(TrafficLightAssignments.WESTFIELD_MAINS_RED)
self.blinkstick.set_light(TrafficLightAssignments.PEDESTRIAN_LIGHT_GREEN)
if window.time_since_update >= 6:
# Appropriate time for 10+ m road
self.state = Period.V
= 0
window.time_since_update
elif self.state == Period.V:
# Fixed black-out
self.blinkstick.set_light(TrafficLightAssignments.MAYFIELD_ROADS_RED)
self.blinkstick.set_light(TrafficLightAssignments.WESTFIELD_MAINS_RED)
if window.time_since_update >= 3:
self.state = Period.VI
= 0
window.time_since_update
elif self.state == Period.VI:
# Pedestrian Clearance
self.blinkstick.set_light(TrafficLightAssignments.MAYFIELD_ROADS_RED)
self.blinkstick.set_light(TrafficLightAssignments.WESTFIELD_MAINS_RED)
if window.time_since_ped >= 2 or window.time_since_update >= 22:
# Pedestrians extend clearance by 2s
self.state = Period.VII
= 0
window.time_since_update
elif self.state == Period.VII:
# Additional Pedestrian Clearance
self.blinkstick.set_light(TrafficLightAssignments.MAYFIELD_ROADS_RED)
self.blinkstick.set_light(TrafficLightAssignments.WESTFIELD_MAINS_RED)
if window.time_since_ped >= 2 or window.time_since_update >= 3:
self.state = Period.VIII
= 0
window.time_since_update
elif self.state == Period.VIII:
# All-Red for 2s
self.blinkstick.set_light(TrafficLightAssignments.MAYFIELD_ROADS_RED)
self.blinkstick.set_light(TrafficLightAssignments.WESTFIELD_MAINS_RED)
self.blinkstick.set_light(TrafficLightAssignments.PEDESTRIAN_LIGHT_RED)
if window.time_since_update >= 2:
self.state = Period.IX
= 0
window.time_since_update
elif self.state == Period.IX:
# Red/Amber Period
self.blinkstick.set_light(TrafficLightAssignments.MAYFIELD_ROADS_RED)
self.blinkstick.set_light(TrafficLightAssignments.MAYFIELD_ROADS_YELLOW)
self.blinkstick.set_light(TrafficLightAssignments.WESTFIELD_MAINS_RED)
self.blinkstick.set_light(TrafficLightAssignments.PEDESTRIAN_LIGHT_RED)
if window.time_since_update >= 2:
self.state = Period.I
= 0
window.time_since_update
class SceneObject():
def __init__(self, window:Scene, row_start:int, row_end:int, col_start:int, col_end:int, letter):
# Check if bounds are valid
if not (row_start <= row_end < window.scene.shape[0] and
<= col_end < window.scene.shape[1]):
col_start raise Exception("Incorrect bounds for object")
self.row_start = row_start
self.row_end = row_end
self.col_start = col_start
self.col_end = col_end
self.letter = letter
def contains(self, scene_object):
return (self.row_start <= scene_object.row_start and
self.row_end >= scene_object.row_end and
self.col_start <= scene_object.col_start and
self.col_end >= scene_object.col_end)
def centroid(self) -> (int, int):
return (np.floor((row_start + row_end)/2), np.floor((col_start + col_end)/2))
class Sidewalk(SceneObject):
pass
class Road(SceneObject):
pass
class MobileObject(SceneObject):
def __init__(self,x:int,y:int,speed,destination: SceneObject, letter):
super().init(row_start=x,row_end=x,col_start=y,col_end=y, letter=letter)
self.destination = destination
self.speed = speed
self.dest_coords = destination.centroid()
def update_coords(self,new_x,new_y):
self.row_start = new_x
self.row_end = new_y
self.col_start = new_y
self.col_end = new_y
def time_step(self) -> (int, int):
pass
class Car(MobileObject): # Contents of Address Register. just kidding.
def __init__(self, x, y, destination: SceneObject):
super().init(x=x, y=y, destination=destination, letter='C')
class Pedestrian(MobileObject): # Contents of Address Register. just kidding.
def __init__(self, x, y, destination: SceneObject):
super().init(x=x, y=y, destination=destination, letter='P')
# CONTROL FLOW STARTS HERE
# Size of the crosswalk simulation matrix. One square is 1 meter on a side.
= 70
ROWS = 30
COLUMNS = 0 # seconds
TIME
= Scene(rows=ROWS, columns=COLUMNS)
window
= TrafficLight(Period.I)
traffic_light
while True:
window.simulation_step(traffic_light)