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.