EventSimulation Tutorial

Installation

In REPL run Pkg.add("EventSimulation") to install the package and next using EventSimulation to start using it.

First simulation

This is a bare minimum simulation using EventSimulation:

using EventSimulation

function arrival(s)
    t = s.now
    println("Arrived at ", s.now)
    register!(s, x -> println("Left at $(x.now) that arrived at $t"), 1.5)
end

s = Scheduler()
for t in 1.0:5.0
    register!(s, arrival, t)
end

go!(s)

In this example five customers arrive at times 1, 2, ..., 5 and stay in the system for 1.5 time units. Observe that in arrival we use a closure to define anonymous function that is registered. In its body x.now will be taken from the state of the scheduler when the anonymous function is invoked but t is fixed in enclosing scope of arrival function as the time of the arrival.

When using EventSimulation working with closures is often the simplest way to develop a simulation so it is important that you understand this example.

Defining infinite source of events

Now we add an infinite stream of customers arriving to the system:

using EventSimulation

function arrival(s)
    t = s.now
    println("Arrived at ", s.now)
    register!(s, x -> println("Left at $(x.now) that arrived at $t"), 1.5)
end

s = Scheduler()
repeat_register!(s, arrival, x -> 1.0)

go!(s, 7)

In this example we show how arrival function can be scheduled to be repeatedly put into event queue in time deltas defined by anonymous function x -> 1.0.

Aslo observe that we have passed second argument 7 to function go! which will force unconditional termination of the simulation after this moment.

Exercise: Think what would happen if the termination time would be omitted in the expression go!(s, 7). How you could use function terminate! inside definition of arrival to get a similar effect. What would be the difference?

Defining custom simulation state

Now let us add a simple counter of number of customers in the system:

using EventSimulation
using Random

mutable struct CounterState <: AbstractState
    count::Int
end

function arrival(s)
    function departure(x)
        x.state.count -= 1
        println("Left at $(x.now) that arrived at $t and left ",
                x.state.count, " other customers")
    end

    t = s.now
    println("Arrived at ", s.now, " and met ", s.state.count, " other customers")
    register!(s, departure, 1.0)
    s.state.count += 1
end

s = Scheduler(CounterState(0))

Random.seed!(1)
repeat_register!(s, arrival, x -> rand())
go!(s, 10)

Observe that all functions in EventSimulation receive Scheduler as an argument and therefore they have acces to current simulation time and simulation state.

Additionally we have changed customer arrival behavior to random.

Exercise: Try changing customer's arrival rate to the exponential distribution using randexp function. Next change time in system distribution to the Gamma distribution. You can find it in Distributions package.

Monitoring simulation

Let us calculate how many customers are present in the above system using custom monitor.

using EventSimulation
using Random

mutable struct CounterState <: AbstractState
    count::Int
    total_time::Float64
    customer_time::Float64
end

function arrival(s)
    register!(s, departure, 1.0)
    s.state.count += 1
end

function departure(s)
    s.state.count -= 1
end

function monitor(s, Δ)
    s.state.total_time += Δ
    s.state.customer_time += Δ * s.state.count
end

cs = CounterState(0, 0.0, 0.0)
s = Scheduler(cs, Float64, monitor)

Random.seed!(1)
repeat_register!(s, arrival, x -> rand())
go!(s, 100_000)
println("Average number of customers in a system is ",
        cs.customer_time/cs.total_time)

Observe that monitor usually will modify simulation state to gather the simulation statistics. Other approaches could use global variables or variables defined in closure of monitor, but using simulation state is the recommended approach.

Exercise: Think if the obtained result is in line with Little's law. Try changing arrival rate and time in the system to check it. As a more advanced exercise make monitor collect data only when simulation time is greater or equal than 100 (i.e. discarding simulation burn-in period).

Introduction to resources

Now we will consider two streams of agents. Supplier provides one unit of good every one unit of time. There are customers that arrive randomly and want to buy a random amount of good. Maximally two customers can wait in the line.

using EventSimulation
using Random

mutable struct GoodState <: AbstractState
    good::SimResource{Float64}
end

function delivery(s)
    provide!(s, s.state.good, 1.0)
    printstyled("Delivered 1.0 at ", s.now, "\n", color=:green)
end

function customer(s)
    function leave(x)
        println("Left at ", x.now, " with quantity ", round(demand, digits=4))
    end
    demand = rand()
    print("Arrived at ", round(s.now, digits=4),
          " with demand ", round(demand, digits=4))
    if !request!(s, s.state.good, demand, leave)[1]
        printstyled(" but line was too long and left with nothing\n", color=:red)
    else
        println(" and went into a queue")
    end
end

function balance(s)
    printstyled("Amount of good in storage: ", round(s.state.good.quantity, digits=4),
                " at time ", s.now, "\n", color=:magenta)
end

s = Scheduler(GoodState(SimResource{Float64}(max_requests=2)))

Random.seed!(1)
repeat_register!(s, delivery, x -> 1.0)
repeat_register!(s, customer, x -> rand())
repeat_register!(s, balance, x -> x.now == 0 ? 1.000001 : 1.0)
go!(s, 5)

Observe that fulfillment of pending requests by SimResource is immediate. This means that the amount is passed to a request from the container before the request event executed.

Exercise: We have used a fixed value of 0.000001 as an increment over an integer number to invoke balance. Rewrite the example using PriorityTime type in such a way that balance is invoked at integer times but with priority low enough.

Nothing delta in repeat_register!

In general EventSimulation does not check the values passed to the engine in order to maximize execution speed. The only exception is interval argument of repeat_register! function. If it returns a value that is nothing then the repeated scheduling of events is interrupted. Here is a simple example

using EventSimulation
using Statistics

mutable struct Jumps <: AbstractState
    n::Int
end

jump(s) = s.state.n+=1
next(s) = s.now >= 1.0 ? nothing : rand()

function main()
    s = Scheduler(Jumps(0))
    repeat_register!(s, jump, next)
    go!(s)
    s.state.n
end

println(mean(main() for i in 1:10^6))

The above code implements a well known puzzle. Assume that we have a bug that gets hungry every rand() time units and eats then. Assume that the fist time it eats is 0. What is the expected number of times it will eat till time reaches 1.0? Run the code to learn the answer (if you did not know the puzzle before). Observe that returning nothing from next forces the simulation to stop.

Bulk execution of events

EventSimulation allows you to plan execution of a batch of actions at the same time. They can be either executed in a predefined order or in random order (this example actually does not require DES, but is a MWE of bulk_register!).

using EventSimulation
using Statistics
using Random

mutable struct Airplane <: AbstractState
    free::Set{Int}
    ok::Bool
end

function sit(s, i)
    s.state.ok = i in s.state.free
    pop!(s.state.free, s.state.ok ? i : rand(s.state.free))
end

function main(n)
    s = Scheduler(Airplane(Set(1:n), false))
    tickets = randperm(n)
    tickets[1] = rand(1:n)
    bulk_register!(s, tickets, sit, 0.0)
    go!(s)
    s.state.ok
end

for n in [10, 50, 250]
    println(n, "\t", mean(main(n) for i in 1:10^4))
end

Another simple puzzle. We have an airplane with n seats and n customers. Every customer has a ticket with seat number. It is represented by a vector tickets. Customers enter the airplane in the order of the vector tickets and everyone tries to sit on ones own seat. Unfortunately tickets[1] = rand(1:n) so the firs customer has forgotten the seat number and has chosen a random seat. When the following customers enter the airplane and find their seat taken they pick a random free seat. The question is what is the probability that last customer that enters the plane finds the seat taken as a function of n. Run the simulation to find out if you do not know the answer!

Next steps

This is the end of this introductory tutorial. More advanced features are covered in examples contained in /examples/ directory.