One of the oldest and most widespread uses of computer programming is to create and run simulations, which are programs that model a particular domain, be it a complex chemical or physical system, the spread of a contagious disease, or financial systems like the stock market. These simulations are parameterizable, meaning we can run the same simulation program several times with different input parameters to see how the system’s behaviour changes.
On this assignment, you’ll create a simulation of a (simplified) elevator system, in which elevators carry people between different floors of a building, using different algorithms for deciding how elevators move between floors.1 We’ll use a time-based simulation, for which the state of the simulation is updated in rounds that each represent a fixed length of time (e.g., 1 second). By varying the different parameters of the system, you’ll be able to compare different algorithms, making judgments about whether one is better than another in some cases, or in all cases.
Before starting this assignment, please update your PythonTA version to 2.6.3 (this fixes a few bugs that some students ran into).
You can do this in PyCharm:
python-ta
in the table of packages.python-ta
.
python-ta
, and check the “Specify
version” box and select 2.6.3.To obtain the starter files for this assignment:
csc148/assignments/a1
folder.csc148
project, just like all of your other course materials.This assignment contains a series of programming tasks to complete. This is similar in structure to the Weekly Preparation Synthesize exercises, but more complex. Your programming work should be completed in the different starter files provided (each part has its own starter file). We have provided code at the bottom of each file for running doctest examples and PythonTA on each file.
Like the prep exercises, this assignment will be automatically graded using a combination of test cases to check the correctness of your work and PythonTA to check the code style and design elements of your work.
This assignment is similar in style to Assignment 0, but more complex. Please start early!
For this assignment you will build a program that runs a simulation of a building’s elevator system, with people using elevators to move between floors. Our elevator simulation consists of four main parts:
We’ll go into more detail for each of these components as you proceed throughout this assignment. But first, watch the short video below to see an example simulation run. Keep in mind the following questions as you watch—you might not know the answers now, but should be able to answer all of these as you work through this assignment!
First, please read the following description of the two key entities in this simulation.
A person in the simulation has three characteristics: which floor they started on, which floor they want to go to, and the number of simulation rounds they have been waiting to reach their target floor. Each person’s waiting time increases both when they are waiting at a floor and when they are traveling on an elevator. Each person arrives at a particular floor and then waits at that floor until they can board an elevator. When their elevator arrives at their target floor, the person leave the elevator.
Each elevator keeps track of the floor it is currently on and a target floor which determines where it is moving to. An elevator is idle (not moving) when its target floor is equal to its current floor. The current floor is always a positive integer—we don’t worry about an elevator being “between floors”. All elevators in the simulation begin a Floor 1 of the building.
Each elevator also keeps track of its passengers (the people who currently on it) as well as its maximum capacity—if an elevator is full, no more people can board it.
Open a1_entities.py
. We have provided the start of the
two classes, Person
and Elevator
, that
represent the key entities in this simulation. Your first task is to
read through the provided code and complete these classes according to
the descriptions above.
While this is similar to the work you did on Assignment 0, there are a few key differences that make this work more complex:
Person
and Elevator
are subclasses of
abstract “sprite” classes from visualizer.py
. You’ll need
to read through their respective superclasses (PersonSprite
and ElevatorSprite
) to see what methods are abstract and
must be implemented. You may also need to override some of their other
methods.
You may need to return to these classes throughout this assignment to add new attributes or methods (especially in Part 4). Don’t worry about getting the classes “perfect” right now. Instead, complete what you can and then move on, and then be prepared to return and modify your design later.
As usual, we’ve provided some sample tests as either doctests or unit
tests (in a1_sample_test.py
). We strongly encourage you to
add your own for this part, and for the rest of the assignment. This
follows the same approach as Assignment 0.
Next, open a1_simulation.py
. We have provided the start
of a Simulation
class that will act as the main simulation
runner for this assignment. This is a big class, so we’ve broken down
what you need to do into multiple steps. For this part, your task is to
complete the Simulation
initializer. Remember that
the purpose of the initializer is to initialize all of the instance
attributes; we have provided descriptions of all instance attributes
already, and part of this task is to read and understand the
expectations for these instance attributes.
To complete the initializer, you’ll need to read the following box to understand the configuration values that will be used to create a simulation.
The simulation initializer takes in a single parameter,
config
, which is a dictionary with the
following keys:
num_floors
: the number of floors in the building
num_floors
num_elevators
: the number of elevatorselevator_capacity
: the capacity of each elevator (every
elevator has the same capacity)arrival_generator
: the algorithm to use to generate new
arrivals for the simulation (more on this in Part 3)moving_algorithm
: the algorithm to use to determine how
elevators move in this simulation (more on this in Part 3)visualize
: whether to visualize the simulation using
PygameWe have started the initializer for you; the only task in this part is to complete it so that it initializes all of the attributes.
In addition to the provided sample test cases, if you scroll down to
the bottom of a1_simulation.py
, where you’ll see a provided
function called run_example_simulation
. If you call this
function in the Python console or the main block, you should see a
Pygame window with the number of elevators and floors specified in the
configuration dictionary.
You can try changing the number of elevators in the example
simulation to test your work visually. Due to the limitations of our
visualizer, the simulation looks best when given fewer than ~8 floors
and elevators. However, your program should be able to handle simulation
configurations beyond this range! When testing these cases, set
visualize
to False
so that the simulation
simply runs in the background, without visualizing anything.
Tip: the Pygame window should close itself after 15 rounds, but if you ever want to stop the simulation early, simply go to PyCharm’s Run pane and click on the red Square (“stop”) button.
Now that the simulation is set up, let’s get it to actually, well, simulate changes! To do so, you’ll implement a series of helper functions (and classes) that represent various forms of state changes in the simulation. To start, read the following box, which describes the actions that occur in each round of the simulation.
Our elevator simulation proceeds in rounds, where each round represents the time it takes for an elevator to move from one floor to another and for people to get on or off the elevators. Each simulation round consists of five different phases, which occur in the following order.
Elevator disembarking. Every person who is currently on an elevator and at their target floor leaves the elevator.
New arrivals. New people are added to the simulation (appearing at their start floor).
Elevator loading. People board elevators on their floor according to the following restrictions:
< 3
or == 3
(idle), but not
> 3
.Note: Unlike “real” elevators, in our simulation an elevator’s target floor is only updated in the next phase, after as many people as possible have boarded it. This means that an idle elevator will allow people to board it independent of their target floors. Someone might board an idle elevator only to find that it begins moving in the opposite direction from where they want to go!2
Elevators move.
Update wait times. Every person in the simulation who is either waiting or on an elevator has their waiting time increased by 1. This will be used to report simulation statistics in Part 4.
Having read this description, go back and watch the demo video we’ve posted above. That video shows the simulation running for 15 rounds, and each round executes each of the five phases we’ve described above. Make sure you can see what’s happening in each phase. You might still have questions about some of the details, which is okay; we’ll explore these details throughout this part of the assignment.
During each simulation round, new people may arrive at the elevators. Since this is a simulation, we need an algorithm to decide how many people arrive at a given round, and the starting and target floors of each person. Moreover, because we want our simulation to support different algorithms to generate arrivals, we’ll use inheritance to allow the programmer to define different kinds of algorithms.
Open a1_algorithms.py
and read through the class
ArrivalGenerator
, which is the abstract superclass that
defines the public interface for the arrival generation
algorithms you’ll define.
(Oct 16) Note: the return value of the
ArrivalGenerator.generate
method should be a dictionary
whose keys are floor numbers and whose corresponding values are
each a list of Person
s who start on that floor.
Then, implement the SingleArrivals
subclass. (For
now you can ignore the other, more complex, subclass; you’ll implement
that in Part 5.)
Now, open a1_simulation.py
and implement
Simulation.generate_arrivals
to actually run your arrival
generation algorithm. This should both update the simulation’s
waiting
instance attribute, as well as call the
Visualizer.show_arrivals
method appropriately to actually
show the new arrivals in the Pygame window.
Finally, implement Simulation.update_wait_times
.
Note that this method isn’t visualized, it should just update the
Person
objects in the simluation.
You should now be able to run the sample simulation and see people arriving at each round. Of course, nothing else in the simulation is happening yet, but that’s a start!
Now that you have people and elevators, you can complete the third
phase of the simulation round: having people board elevators. This can
be done entirely in Simulation.handle_boarding
with one or
two helper functions to help split up your code. You’ll need to remember
to update the state of each elevator as it takes on new passengers, and
to visualize the change by calling Visualizer.show_boarding
appropriately.
Now when you run the sample simulation, you should see that in addition to people arriving at floors, anyone who arrives on the first floor moves to the leftmost elevator that still has room. Note that because you haven’t implemented any elevator moving algorithm, the elevators will fill up on the ground floor, letting you verify that your implementation correctly handles boarding when some elevators are full.
If you read through all the documentation in
visualizer.py
, you should see some visual effects as the
simulation runs: elevators getting full, and people getting angrier.
Note: you’ll notice that the visualizer seems to draw sprites
in a random (or reverse) order, so the “oldest” sprites are covered by
newer ones. This is normal, and not a bug with your code!
Each round, an elevator moving algorithm can update the target floor for each elevator to alter its moving direciton in the simulation. A moving algorithm receives as input two values: a list of all the elevators in the simulation, and a dictionary mapping floor numbers to a list of people who are waiting on that floor.
This is an extremely flexible model of how elevators move, and the reason we do this is so that that you can implement a variety of fun and interesting elevator algorithms!
As with the arrival generation, our simulation will support multiple kinds of elevator moving algorithms. To complete this part:
Open a1_algorithms.py
and read through the
MovingAlgorithm
abstract class.
Then, implement the EndToEndLoop
subclass. (For now
you can ignore the other, more complex, subclass; you’ll implement that
in Part 5.)
Finally, open a1_simulation.py
and complete the
Simulation.move_elevators
method. This method will need to
do three things:
update_target_floors
method
to update elevator target floors.Direction
value), and then call
Visualizer.show_elevator_moves
appropriately.Now when you run the simulation, you should see people arriving,
boarding elevators, and those elevators moving. Notice that with the
EndToEndLoop
algorithm, all elevators (including
empty ones) move in sync, going from the bottom floor to the top floor,
back to the bottom floor, and so on in a loop. Since you haven’t yet
implemented the people disembarking from elevators, no person will
actually get off an elevator (and so you should see them getting angrier
and angrier as they ride the elevator).
Now it’s time to implement the last phase of a simulation round: elevator disembarking. Remember that this occurs at the start of each round: whenever a person is on an elevator and that elevator is at their target floor, the person leaves the elevator.
You can implement this action entirely in the
Simulation.handle_disembarking
method. You’ll need to call
Visualizer.show_disembarking
appropriately to visualize
this happening.
After completing this step, you should now have a fully working
simulation that executes (and visualizes!) all four stages of a
simulation round: people disembarking from elevators once they’ve
reached their target floor, new people arriving (according to
SingleArrivals
), people boarding elevators that have space,
and elevators moving (according to EndToEndLoop
).
Try varying the configurations of the sample simulation run; you’ll likely find some bugs in at least one of these stages, and now is an excellent time to fix them!
Now that you have a working simulation, you’ll next modify your implementation to track a set of summary statistics for this simulation and report them when the simulation ends. Read the box below to understand what statistics you’ll need to report.
At the end of the simulation, you are responsible for reporting summary statistics for the simulation, in a dictionary with the following keys:
num_rounds
: the number of simulation rounds that took
placetotal_people
: the number of people that arrived at some
point during the simulationpeople_completed
: the number of people who reached
their target destination by the end of the simulationmax_time
: the maximum time a person spent before
reaching their target floor during the simulation (note that this
includes time spent waiting on a floor and travelling on an
elevator)avg_time
: the average time a person spent before
reaching their target floor, rounded down to the nearest
integerThe max_time
and avg_time
statistics should
only take into account the people who reached their target
floor. Report -1
for these two statistics if no person
reached their target floor during the simulation run (i.e.,
people_completed
has a value of 0
).
Your task here is to add to your existing code so that you can keep track of and report these statistics. By “add to your existing code”, we mean doing the following:
Simulation._calculate_stats
method
Simulation
methods
Simulation
, Person
, and/or
Elevator
classes by adding new attributes/methods.
Try running a very small simulation (e.g., 1 elevator, 5 rounds) first, and manually calculate the expected value for each statistic. Then after running the simulation, you’ll be able to check the value of each statistic.
Also check out the sample tests we’ve provided, and add to them!
Your final task for this assignment is to implement two new classes
in algorithms.py
: an additional
ArrivalGenerator
subclass and an additional
MovingAlgorithm
subclass. This illustrates the power of
inheritance: after implementing these two subclasses, you should be able
to use them in a simulation without requiring any changes to the
Simulation
code!
Read the following box that describes these two new algorithms, and
then implement them in a1_algorithms.py
.
class FileArrivals(ArrivalGenerator)
: Reading arrival data
from a file
A comma-separated values (csv) file is a text file that uses commas to separate pieces of data. Our second approach for arrival generation is to read arrivals from a csv file, in the following format:
For example, the following data file
(data/sample_arrivals.csv
)
0, 1, 4, 5, 3
2, 1, 2
5, 4, 2
represents the following arrivals:
You may make the following assumptions about input files for this assignment:
class FurthestFloor(MovingAlgorithm)
: taking into account
passenger preferences
Unlike EndToEndLoop
, this algorithm determines the
target floor of each elevator based on the elevator’s passengers (if
any) or the people waiting (if any). See the docstring for the
FurthestFloor
class for more detail.
At this point, it may be tempting to just run your simulation a bunch
of times and watch the Pygame window, but this is not a very robust
approach to checking whether your algorithms are correct. Instead, we
recommend using your FileArrivals
class to create test
inputs for the arrivals, and then carefully trace what should
actually happen on small inputs, and use these inputs to check whether
your FurtherFloor
moving algorithm works as intended as
well. You can then either run the simluation with these custom test
inputs and check the statistics, or (better!) add to the sample tests in
a1_sample_test.py
to encode the expected behaviour as test
cases.
Before submitting your work, please do the following:
python_ta.check_all()
code to check for PythonTA errors, and fix them! Tip:
most formatting errors (e.g., blank lines, whitespace) can be fixed
automatically in PyCharm by using the menu option Code -> Reformat
Code.print
function calls. Remove any large blocks of
commented-out code. If you want to save an old or alternate approach,
create a copy of the file instead.Now, to actually submit your work:
Login to MarkUs.
Go to Assignment 1, then the “Submissions” tab.
If you are working with a partner, make sure you have formed a group on MarkUs.
Submit the following files: a1_algorithms.py
,
a1_entities.py
, and a1_simulation.py
. Do not
submit any other files. Please note that MarkUs is picky with filenames,
and so your filenames must match these exactly, including using
lowercase letters.
Tip: you can select multiple files at once for submission.
If you are working with a partner, only one of you needs to submit the files.
Refresh the page, and then download each file to make sure you submitted the right version.
Remember, you can submit your files multiple times before the due date. So you can aim to submit your work early, and if you find an error or a place to improve before the due date, you can still make your changes and resubmit your files.
After you’ve submitted your work, please give yourself a well-deserved pat on the back and go take a rest or do something fun or eat some chocolate!
David’s favourite elevator-related quotation: “Ethan says Type-A personalities have a whole subset of diseases that they, and only they, share, and the transmission vector for these diseases is the door close button on elevators that only get pushed by impatient, Type-A people.” (Douglas Coupland, Microserfs)↩︎
Again, this is not the case in real life because of the use of elevator buttons on each floor. However, many of us have accidentally entered an elevator that was actually going in the wrong direction!↩︎