Simulation is a powerful tool to interact and experiment with complex systems. Often, a larger system contains multiple processes with connected dependencies, depending on stochastic processes with different distributions.
SimPy is a convenient python library that can be used to quickly set up discrete-event simulations to try out a hypothesis. SimPy is especially useful for simulations of active users (e.g., customer or agents) are depending on using a limited resource (e.g., servers, products in-stock, checkout desks). For this kind of problem, it is crucial to understand certain metrics like what effect the number of resources has on system throughput or what the waiting time looks like over time or on average.
One such system that can be used as an example is an elevator system in a building, where there are active users (elevator passengers) and a limited resource (elevators).
There are several ways to simulate an elevator system with users, and they will most likely differ in how accurate it represents reality or how optimal it generates throughput.
Let’s start with a simple version, where Simpy.Resource is used to represent a set of elevators:
This way elevators are created and can be requested and used upon availability using elevator_travel_request. The request to the Resource is done through req and simulation will wait for the resource to be available before the resource is used (and automatically returned at end of with-statement. Let’s create the environment for the elevators, as well to trigger the requests:
This sets up the simpy. Environment in which events (such as Environment.timeout() and Resource.request()) can be processed and yielded for. An object Building holds the resource Elevators and simulates elevator usage with sporadic calls of elevator_travel_request().
This is a full example of how you could simulate a discrete-event elevator system using SimPy with only 50 lines of code. Adding usage of counters and timestamps (simpy.Environment.now) you can custom the code to monitor metrics of your interest, e.g., waiting time, average number of available elevators, number of processed travel requests.
This implementation, however, is probably in most cases a too simplified model for this specific case. Looking closer to what happens in the process, we notice a few (small but relevant) properties that differs from a real system with elevators:
- One elevator will only handle one request/passenger at the time
- Simulation does not consider travel time between requests
- Elevator provided is not necessarily the closest one, rather the one “first in line”
Looking at these points, this is not the best representation of an elevator system. It would instead be a better model for a “limited resource” system where the state of the resource is negligible, e.g., a charging station or a washing machine. The position of elevators between requests should not be disregarded in this simulation.
To make a better model for the elevators, we need to be able to monitor the state of elevators and to distinguish between them. Using simpy.Resource, each resource is “anonymous” in a way. With this in mind, we will in the following section extend the previous example to resolve the above issues 2 and 3. This will be done by saving information on each individual elevator after each time it moves.
(The issue number 1 regarding “one request/passenger at the time” will be disregarded for now, and the elevators in the simulations will continue to only take one passenger at the time. This is to simplify the implementation and is most likely an acceptable assumption to make if the interval between requests is not too small and if there are not too many floors in the building. Also, it is related to request handling and not related to the inherent system properties. Therefore, it can be seen as a point of improvement for future implementations.)
Now let’s look at how we handle each elevator individually using the tools in SimPy:
The idea is to use the SimPy class Store, where you can store individual objects in a container which are – adding to the container using .put() and accessing items using .get() (when available). We create a class Elevator where each individual instance has a member “id” and a “current_floor”. In class ElevatorStore (replacing previous class Elevators) these instances of type Elevator will be stored and handled centrally.
In addition to this, a class Traveller is created to keep track of individual users as well:
ElevatorStore can then use Traveller class information when getting requests. Note that the elevator “container” is now a “Store” (previously “Resource”) and instances are accessed with .get() and returned with .put()
The class Building setup is also updated using the new Traveller and ElevatorStore classes:
This sums up a (for most applications) decent simulation of an elevator system to monitor how the queue/flow behaves for different configurations.
As a last point we choose one metric to monitor and try to improve it – let’s choose “waiting time” i.e., the time from request is sent from traveller until the elevator arrives at traveller’s floor.
Adding a few lines in the building class and main function the average waiting time can be presented at end of simulation:
The resulting average waiting time stabilizes at about 5.3 ticks for a long simulation time.
For this implementation the way the available elevators are chosen, it is not considering which elevator is the closest to the requester. Let’s change that in the implementation and see if there is an improvement in the average waiting time.
Using alternative 2 above for the choice of elevator at request, the new average waiting time per request has improved from 5.3 ticks to 2.6 ticks (>50% decrease!).
The plots below show samples of waiting time for the two scheduling alternatives (Left: “Alt. 1 – random”, Right: “Alt. 2 – smart”):
The waiting time is more scattered in the first scheduling alternative than the second alternative. The waiting times in the smarter scheduling are considerably shorter on average and the numbers are more concentrated in the lower segment. This is due to the scheduling is choosing the closest elevator when providing one for the requester. There are still spikes in long individual waiting times, but those are most likely because most (or all) elevators are busy when the request is sent, and therefore the scheduler does not have many options in providing a nearby elevator.
There are of course more possible improvements for this case, both for accuracy in the system representation and for the operations optimization, e.g., for request handling. The important part to note here is what SimPy enables – a simple way to create a discrete-event simulation or digital twin.
To summarize what was presented:
- Created discrete-event simulation for using SimPy
- For the simulation implementation, large parts of the queue/event handling are taken care of by the SimPy framework which simplifies the creation of the simulation
- Tested and evaluated different approaches and SimPy tools for system representation
- Measured a key metric of the system using the discrete time representation in SimPy
- Improved request handling algorithm to improve the measured metric