Modeling

A simple model

Let’s start with a very simple model, to demonstrate the basic structure, process interaction, component definition and output.

 1# Car.py
 2import salabim as sim
 3sim.yieldless(False)
 4
 5
 6
 7class Car(sim.Component):
 8    def process(self):
 9        while True:
10            yield self.hold(1)
11
12
13env = sim.Environment(trace=True)
14Car()
15env.run(till=5)

In basic steps:

We always start by importing salabim

import salabim as sim
sim.yieldless(False)  # indicates that we are using yields

Now we can refer to all salabim classes and function with sim..

The main body of every salabim model usually starts with

env = sim.Environment()

or, to be more inline with common practices

app = sim.App()

For each component we define a class as in

class Car(sim.Component):

The class inherits from sim.Component.

Although it is possible to define other processes within a class, the standard way is to define a generator function called process in the class. A generator is a function with at least one yield statement. These are used in salabim context as a signal to give control to the sequence mechanism.

In this example,

yield self.hold(1)

gives control,to the sequence mechanism and comes back after 1 time unit. The self. part means that it is this component to be held for some time. We will see later other uses of yield like passivate, request, wait and standby.

In the main body, an instance of a car is created by Car(). It automatically gets the name car.0. As there is a generator function called process in Car, this process description will be activated (by default at time now, which is 0 here). It is possible to start a process later, but this is by far the most common way to start a process.

With

env.run(till=5)

we start the simulation and get back control after 5 time units. A component called main is defined under the hood to get access to the main process.

When we run this program, we get the following output

line#         time current component    action                               information
-----   ---------- -------------------- -----------------------------------  ------------------------------------------------
                                        line numbers refers to               Example - basic.py
   11                                   default environment initialize
   11                                   main create
   11        0.000 main                 current
   12                                   car.0 create
   12                                   car.0 activate                       scheduled for      0.000 @    6  process=process
   13                                   main run                             scheduled for      5.000 @   13+
    6        0.000 car.0                current
    8                                   car.0 hold                           scheduled for      1.000 @    8+
    8+       1.000 car.0                current
    8                                   car.0 hold                           scheduled for      2.000 @    8+
    8+       2.000 car.0                current
    8                                   car.0 hold                           scheduled for      3.000 @    8+
    8+       3.000 car.0                current
    8                                   car.0 hold                           scheduled for      4.000 @    8+
    8+       4.000 car.0                current
    8                                   car.0 hold                           scheduled for      5.000 @    8+
   13+       5.000 main                 current

A bank example

Now let’s move to a more realistic model. Here customers are arriving in a bank, where there is one clerk. This clerk handles the customers in first in first out (FIFO) order. We see the following components, each with its process:

  • The customer generator that creates the customers, with an inter arrival time of uniform(5,15)

  • The customers

  • The clerk, which serves the customers in a constant time of 30 (overloaded and non steady state system)

And we need a queue for the customers to wait for service.

The model code is

 1# Bank, 1 clerk.py
 2import salabim as sim
 3sim.yieldless(False)
 4
 5
 6
 7class CustomerGenerator(sim.Component):
 8    def process(self):
 9        while True:
10            Customer()
11            yield self.hold(sim.Uniform(5, 15).sample())
12
13
14class Customer(sim.Component):
15    def process(self):
16        self.enter(waitingline)
17        if clerk.ispassive():
18            clerk.activate()
19        yield self.passivate()
20
21
22class Clerk(sim.Component):
23    def process(self):
24        while True:
25            while len(waitingline) == 0:
26                yield self.passivate()
27            self.customer = waitingline.pop()
28            yield self.hold(30)
29            self.customer.activate()
30
31
32env = sim.Environment(trace=True)
33
34CustomerGenerator()
35clerk = Clerk()
36waitingline = sim.Queue("waitingline")
37
38env.run(till=50)
39print()
40waitingline.print_statistics()

Let’s look at some details

yield self.hold(sim.Uniform(5, 15).sample())

will do the statistical sampling and wait for that time till the next customer is created.

With

self.enter(waitingline)

the customer places itself at the tail of the waiting line.

Then, the customer checks whether the clerk is idle, and if so, activates him immediately.

if clerk.ispassive():
    clerk.activate()

Once the clerk is active (again), it gets the first customer out of the waitingline with

self.customer = waitingline.pop()

and holds for 30 time units with

yield self.hold(30)

After that hold the customer is activated and will terminate

self.customer.activate()

In the main section of the program, we create the CustomerGenerator, the Clerk and a queue called waitingline. After the simulation is finished, the statistics of the queue are presented with

waitingline.print_statistics()

The output looks like

line#        time current component    action                               information
------ ---------- -------------------- -----------------------------------  ------------------------------------------------
                                       line numbers refers to               Bank, 1 clerk.py
   30                                  default environment initialize
   30                                  main create
   30       0.000 main                 current
   32                                  customergenerator.0 create
   32                                  customergenerator.0 activate         scheduled for 0.000 @    6+ process=process
   33                                  clerk.0 create
   33                                  clerk.0 activate                     scheduled for 0.000 @   21+ process=process
   34                                  waitingline create
   36                                  main run +50.000                     scheduled for 50.000 @   36+
   6+      0.000 customergenerator.0  current
   8                                  customer.0 create
   8                                  customer.0 activate                  scheduled for 0.000 @   13+ process=process
   9                                  customergenerator.0 hold +14.631     scheduled for 14.631 @    9+
   21+      0.000 clerk.0              current
   24                                  clerk.0 passivate                    @   24+
   13+      0.000 customer.0           current
   14                                  customer.0                           enter waitingline
   16                                  clerk.0 activate                     scheduled for 0.000 @   24+
   17                                  customer.0 passivate                 @   17+
   24+      0.000 clerk.0              current
   25                                  customer.0                           leave waitingline
   26                                  clerk.0 hold +30.000                 scheduled for 30.000 @   26+
   9+     14.631 customergenerator.0  current
   8                                  customer.1 create
   8                                  customer.1 activate                  scheduled for 14.631 @   13+ process=process
   9                                  customergenerator.0 hold +7.357      scheduled for 21.989 @    9+
   13+     14.631 customer.1           current
   14                                  customer.1                           enter waitingline
   17                                  customer.1 passivate                 @   17+
   9+     21.989 customergenerator.0  current
   8                                  customer.2 create
   8                                  customer.2 activate                  scheduled for 21.989 @   13+ process=process
   9                                  customergenerator.0 hold +10.815     scheduled for 32.804 @    9+
   13+     21.989 customer.2           current
   14                                  customer.2                           enter waitingline
   17                                  customer.2 passivate                 @   17+
   26+     30.000 clerk.0              current
   27                                  customer.0 activate                  scheduled for 30.000 @   17+
   25                                  customer.1                           leave waitingline
   26                                  clerk.0 hold +30.000                 scheduled for 60.000 @   26+
   17+     30.000 customer.0           current
   17+                                 customer.0 ended
   9+     32.804 customergenerator.0  current
   8                                  customer.3 create
   8                                  customer.3 activate                  scheduled for 32.804 @   13+ process=process
   9                                  customergenerator.0 hold +7.267      scheduled for 40.071 @    9+
   13+     32.804 customer.3           current
   14                                  customer.3                           enter waitingline
   17                                  customer.3 passivate                 @   17+
   9+     40.071 customergenerator.0  current
   8                                  customer.4 create
   8                                  customer.4 activate                  scheduled for 40.071 @   13+ process=process
   9                                  customergenerator.0 hold +14.666     scheduled for 54.737 @    9+
   13+     40.071 customer.4           current
   14                                  customer.4                           enter waitingline
   17                                  customer.4 passivate                 @   17+
   36+     50.000 main                 current

Statistics of waitingline at        50
                                                                     all    excl.zero         zero
-------------------------------------------- -------------- ------------ ------------ ------------
Length of waitingline                        duration             50           35.369       14.631
                                             mean                  1.410        1.993
                                             std.deviation         1.107        0.754

                                             minimum               0            1
                                             median                2            2
                                             90% percentile        3            3
                                             95% percentile        3            3
                                             maximum               3            3

Length of stay in waitingline                entries               2            2            0
                                             mean                  7.684        7.684
                                             std.deviation         7.684        7.684

                                             minimum               0            0
                                             median                7.684        7.684
                                             90% percentile       13.832       13.832
                                             95% percentile       14.600       14.600
                                             maximum              15.369       15.369

Now, let’s add more clerks. Here we have chosen to put the three clerks in a list

clerks = [Clerk() for _ in range(3)]

although in this case we could have also put them in a salabim queue, like

clerks = sim.Queue('clerks')
for _ in range(3):
    Clerk().enter(clerks)

or even

clerks = sim.Queue('clerks', fill=[Clerk() for _ in range(3)])

And, to restart a clerk

for clerk in clerks:
    if clerk.ispassive():
       clerk.activate()
       break  # reactivate only one clerk

The complete source of a three clerk post office:

 1# Bank, 3 clerks.py
 2import salabim as sim
 3sim.yieldless(False)
 4
 5
 6
 7class CustomerGenerator(sim.Component):
 8    def process(self):
 9        while True:
10            Customer()
11            yield self.hold(sim.Uniform(5, 15).sample())
12
13
14class Customer(sim.Component):
15    def process(self):
16        self.enter(waitingline)
17        for clerk in clerks:
18            if clerk.ispassive():
19                clerk.activate()
20                break  # activate at most one clerk
21        yield self.passivate()
22
23
24class Clerk(sim.Component):
25    def process(self):
26        while True:
27            while len(waitingline) == 0:
28                yield self.passivate()
29            self.customer = waitingline.pop()
30            yield self.hold(30)
31            self.customer.activate()
32
33
34env = sim.Environment(trace=False)
35CustomerGenerator()
36clerks = [Clerk() for _ in range(3)]
37
38waitingline = sim.Queue("waitingline")
39
40env.run(till=50000)
41waitingline.print_histograms()
42
43waitingline.print_info()

The bank office example with stores

The salabim package contains a very useful concept for modelling: stores.

A store is essentially a queue (optionally with limited capacity) that can hold components.

And we can request components from the store. If there’s a component in the store, it is returned. But if it is not the requesting component goes into the requesting state, until something is available in the store.

The same holds for processes putting components in the store: if it is full, the component that want to add someting to the store goes into the requesting state. Here we have an unlimited waiting room, though.

The code is:

 1# Bank, 3 clerks (store).py
 2import salabim as sim
 3sim.yieldless(False)
 4
 5
 6
 7class CustomerGenerator(sim.Component):
 8    def process(self):
 9        while True:
10            Customer().enter(waiting_room)
11            yield self.hold(sim.Uniform(5, 15))
12
13
14class Clerk(sim.Component):
15    def process(self):
16        while True:
17            customer = yield self.from_store(waiting_room)
18            yield self.hold(30)
19
20
21class Customer(sim.Component):
22    ...
23
24
25env = sim.Environment(trace=False)
26CustomerGenerator()
27for _ in range(3):
28    Clerk()
29waiting_room = sim.Store("waiting_room")
30
31
32env.run(till=50000)
33
34waiting_room.print_statistics()
35waiting_room.print_info()

The bank office example with resources

The salabim package contains another useful concept for modelling: resources. Resources have a limited capacity and can be claimed by components and released later.

In the model of the bank with the same functionality as the above example, the clerks are defined as a resource with capacity 3.

The model code is:

 1# Bank, 3 clerks (resources).py
 2import salabim as sim
 3sim.yieldless(False)
 4
 5
 6
 7class CustomerGenerator(sim.Component):
 8    def process(self):
 9        while True:
10            Customer()
11            yield self.hold(sim.Uniform(5, 15).sample())
12
13
14class Customer(sim.Component):
15    def process(self):
16        yield self.request(clerks)
17        yield self.hold(30)
18        self.release()  # not really required
19
20
21env = sim.Environment(trace=False)
22CustomerGenerator()
23clerks = sim.Resource("clerks", capacity=3)
24
25env.run(till=50000)
26
27clerks.print_statistics()
28clerks.print_info()

Let’s look at some details.

clerks = sim.Resource('clerks', capacity=3)

This defines a resource with a capacity of 3.

And then, a customer, just tries to claim one unit (=clerk) from the resource with

yield self.request(clerks)

Here, we use the default of 1 unit. If the resource is not available, the customer just waits for it to become available (in order of arrival).

In contrast with the previous example, the customer now holds itself for 30 time units.

And after these 30 time units, the customer releases the resource with

self.release()

The effect is that salabim then tries to honor the next pending request, if any.

(actually, in this case this release statement is not required, as resources that were claimed are automatically released when a process terminates).

The statistics are maintained in two system queue, called clerk.requesters() and clerk.claimers().

The output is very similar to the earlier example. The statistics are exactly the same.

The bank office example with balking and reneging

Now, we assume that clients are not going to the queue when there are more than 5 clients waiting (balking). On top of that, if a client is waiting longer than 50, he/she will leave as well (reneging).

The model code is:

 1# Example - bank, 3 clerks, reneging.py
 2import salabim as sim
 3sim.yieldless(False)
 4
 5
 6
 7class CustomerGenerator(sim.Component):
 8    def process(self):
 9        while True:
10            Customer()
11            yield self.hold(sim.Uniform(5, 15).sample())
12
13
14class Customer(sim.Component):
15    def process(self):
16        if len(waitingline) >= 5:
17            env.number_balked += 1
18            env.print_trace("", "", "balked")
19            print(env.now(), "balked",self.name())            
20            yield self.cancel()
21        self.enter(waitingline)
22        for clerk in clerks:
23            if clerk.ispassive():
24                clerk.activate()
25                break  # activate only one clerk
26        yield self.hold(50)  # if not serviced within this time, renege
27        if self in waitingline:
28            self.leave(waitingline)
29            env.number_reneged += 1
30            env.print_trace("", "", "reneged")
31        else:
32            yield self.passivate()  # wait for service to be completed
33
34
35class Clerk(sim.Component):
36    def process(self):
37        while True:
38            while len(waitingline) == 0:
39                yield self.passivate()
40            self.customer = waitingline.pop()
41            self.customer.activate()  # get the customer out of it's hold(50)
42            yield self.hold(30)
43            self.customer.activate()  # signal the customer that's all's done
44
45
46env = sim.Environment()
47CustomerGenerator()
48env.number_balked = 0
49env.number_reneged = 0
50clerks = [Clerk() for _ in range(3)]
51
52waitingline = sim.Queue("waitingline")
53env.run(duration=300000)
54waitingline.length.print_histogram(30, 0, 1)
55waitingline.length_of_stay.print_histogram(30, 0, 10)
56print("number reneged", env.number_reneged)
57print("number balked", env.number_balked)

Let’s look at some details.

yield self.cancel()

This makes the current component (a customer) a data component (and be subject to garbage collection), if the queue length is 5 or more.

The reneging is implemented by a hold of 50. If a clerk can service a customer, it will take the customer out of the waitingline and will activate it at that moment. The customer just has to check whether he/she is still in the waiting line. If so, he/she has not been serviced in time and thus will renege.

yield self.hold(50)
if self in waitingline:
    self.leave(waitingline)
    env.number_reneged += 1
else:
     self.passivate()

All the clerk has to do when starting servicing a client is to get the next customer in line out of the queue (as before) and activate this customer (at time now). The effect is that the hold of the customer will end.

self.customer = waitingline.pop()
self.customer.activate()

The bank office example with balking and reneging (store)

Now we show how the balking and reneging is implemented with a store.

The model code is:

 1# Bank, 3 clerks (store, reneging).py
 2import salabim as sim
 3sim.yieldless(False)
 4
 5
 6
 7class CustomerGenerator(sim.Component):
 8    def process(self):
 9        while True:
10            customer = Customer()
11            yield self.to_store(waiting_room, customer, fail_at=env.now())
12            if self.failed():
13                customer.cancel()
14                env.number_balked += 1
15                print(env.now(), "balked",customer.name())
16                env.print_trace("", "", "balked",customer.name())
17            yield self.hold(sim.Uniform(5, 15))
18
19
20class Clerk(sim.Component):
21    def process(self):
22        while True:
23            customer = yield self.from_store(waiting_room)
24            yield self.hold(30)
25
26
27class Customer(sim.Component):
28    def process(self):
29        yield self.hold(50)
30        if self in waiting_room:
31            self.leave(waiting_room)
32            env.number_reneged += 1
33            env.print_trace("", "", "reneged")
34
35env = sim.Environment(trace=False)
36env.number_balked = 0
37env.number_reneged = 0
38CustomerGenerator()
39for _ in range(3):
40    Clerk()
41waiting_room = sim.Store("waiting_room", capacity=5)
42
43env.run(till=30000)
44
45waiting_room.length.print_histogram(30, 0, 1)
46waiting_room.length_of_stay.print_histogram(30, 0, 10)
47print("number reneged", env.number_reneged)
48print("number balked", env.number_balked)

As you can see, the balking part is done by setting a fail_at value of 0 on the to_store, which means means that if the request is not honoured immediately, the customer balks.

For the renenging, we do the same as with the ordinary solution.

The bank office example with balking and reneging (resources)

Now we show how the balking and reneging is implemented with resources.

The model code is:

 1# Example - bank, 3 clerks, reneging (resources).py
 2import salabim as sim
 3sim.yieldless(False)
 4
 5
 6
 7class CustomerGenerator(sim.Component):
 8    def process(self):
 9        while True:
10            Customer()
11            yield self.hold(sim.Uniform(5, 15).sample())
12
13
14class Customer(sim.Component):
15    def process(self):
16        if len(clerks.requesters()) >= 5:
17            env.number_balked += 1
18            env.print_trace("", "", "balked")
19            yield self.cancel()
20        yield self.request(clerks, fail_delay=50)
21        if self.failed():
22            env.number_reneged += 1
23            env.print_trace("", "", "reneged")
24        else:
25            yield self.hold(30)
26            self.release()
27
28
29env = sim.Environment()
30CustomerGenerator()
31env.number_balked = 0
32env.number_reneged = 0
33clerks = sim.Resource("clerks", 3)
34
35env.run(till=50000)
36
37clerks.requesters().length.print_histogram(30, 0, 1)
38print()
39clerks.requesters().length_of_stay.print_histogram(30, 0, 10)
40print("number reneged", env.number_reneged)
41print("number balked", env.number_balked)

As you can see, the balking part is exactly the same as in the example without resources.

For the renenging, all we have to do is add a fail_delay

yield self.request(clerks, fail_delay=50)

If the request is not honored within 50 time units, the process continues after that request statement. And then, we just check whether the request has failed

if self.failed():
    env.number_reneged += 1

This example shows clearly the advantage of the resource solution over the passivate/activate method, in this example.

The bank office example with states

The salabim package contains yet another useful concept for modelling: states. In this case, we define a state called worktodo.

The model code is:

 1# Example - bank, 3 clerks (state).py
 2import salabim as sim
 3sim.yieldless(False)
 4
 5
 6
 7class CustomerGenerator(sim.Component):
 8    def process(self):
 9        while True:
10            Customer()
11            yield self.hold(sim.Uniform(5, 15).sample())
12
13
14class Customer(sim.Component):
15    def process(self):
16        self.enter(waitingline)
17        worktodo.trigger(max=1)
18        yield self.passivate()
19
20
21class Clerk(sim.Component):
22    def process(self):
23        while True:
24            if len(waitingline) == 0:
25                yield self.wait((worktodo, True, 1))
26            self.customer = waitingline.pop()
27            yield self.hold(30)
28            self.customer.activate()
29
30
31env = sim.Environment()
32CustomerGenerator()
33for i in range(3):
34    Clerk()
35waitingline = sim.Queue("waitingline")
36worktodo = sim.State("worktodo")
37
38env.run(till=50000)
39waitingline.print_histograms()
40worktodo.print_histograms()

Let’s look at some details.

worktodo = sim.State('worktodo')

This defines a state with an initial value False.

In the code of the customer, the customer tries to trigger one clerk with

worktodo.trigger(max=1)

The effect is that if there are clerks waiting for worktodo, the first clerk’s wait is honored and that clerk continues its process after

yield self.wait(worktodo)

Note that the clerk is only going to wait for worktodo after completion of a job if there are no customers waiting.

The bank office example with standby

The salabim package contains yet another powerful process mechanism, called standby. When a component is in standby mode, it will become current after each event. Normally, the standby will be used in a while loop where at every event one or more conditions are checked.

The model with standby is

 1# Example - bank, 3 clerks (standby).py
 2import salabim as sim
 3sim.yieldless(False)
 4
 5
 6
 7class CustomerGenerator(sim.Component):
 8    def process(self):
 9        while True:
10            Customer()
11            yield self.hold(sim.Uniform(5, 15).sample())
12
13
14class Customer(sim.Component):
15    def process(self):
16        self.enter(waitingline)
17        yield self.passivate()
18
19
20class Clerk(sim.Component):
21    def process(self):
22        while True:
23            while len(waitingline) == 0:
24                yield self.standby()
25            self.customer = waitingline.pop()
26            yield self.hold(30)
27            self.customer.activate()
28
29
30env = sim.Environment(trace=True)
31CustomerGenerator()
32for _ in range(3):
33    Clerk()
34waitingline = sim.Queue("waitingline")
35
36env.run(till=50000)
37waitingline.length.print_histogram(30, 0, 1)
38print()
39waitingline.length_of_stay.print_histogram(30, 0, 10)

In this case, the condition is checked frequently with

while len(waitingline) == 0:
    yield self.standby()

The rest of the code is very similar to the version with states.

Warning

It is very important to realize that this mechanism can have significant impact on the performance, as after EACH event, the component becomes current and has to be checked. In general it is recommended to try and use states or a more straightforward passivate/activate construction.