Examples
Below are some useful examples to give you an idea of how this package can be leveraged. The code for these examples can also be found on Github in the docs/examples folder.
Event Based
"""
This is a simple process-based discrete-event simulation of an N teller, single
queue bank.
There are two (2) processes:
generator(n::Integer)
Generates n arrivals into the system with exponentially distributed
inter-arrival time with a mean of 4.0.
customer(i::Integer)
The process representing the ith customer in the system. Each customer
acquires a teller and works a uniformly distributed time between 2.0 and
10.0. It then releases the teller and exits.
There is a single resource, tellers, that represents the tellers in the bank.
The number if tellers is set by the N_TELLERS global variable.
Once the simulation runs, statistics are printed for the tellers allocation and
queue length as well as a plot of the queue length over time.
"""
using SimLynx
using Distributions
using Random
const N_TELLERS = 2
const N_CUSTOMERS = 10
struct Customer
id::Int64
Customer(id::Int64) = new(id)
end
Base.show(io::IO, customer::Customer) =
print(io, "Customer($(customer.id))")
mutable struct Teller
id::Int64
serving::Union{Customer, Nothing}
Teller(id::Int64) = new(id, nothing)
end
Base.show(io::IO, teller::Teller) =
print(io, "Teller($(teller.id))")
tellers = nothing
teller_queue = nothing
function available_teller()::Union{Teller, Nothing}
for teller in tellers
if isnothing(teller.serving)
return teller
end
end
return nothing
end
@event generate(i::Integer) begin
if i <= N_CUSTOMERS
@schedule now arrival(Customer(i))
@schedule in rand(Distributions.Exponential(4.0)) generate(i + 1)
end
end
@event arrival(customer::Customer) begin
println("$(current_time()): $customer arrives")
teller = available_teller()
if isnothing(teller)
enqueue!(teller_queue, customer)
else
@schedule now service(teller, customer)
end
end
@event service(teller::Teller, customer::Customer) begin
println("$(current_time()): $teller starts servicing $customer")
teller.serving = customer
@schedule in rand(Distributions.Uniform(2.0, 10.0)) departure(teller, customer)
end
@event departure(teller::Teller, customer::Customer) begin
println("$(current_time()): $teller finishes servicing $customer")
if isempty(teller_queue)
teller.serving = nothing
else
next_customer = dequeue!(teller_queue)
teller.serving = next_customer
@schedule now service(teller, customer)
end
end
function main()
@with_new_simulation begin
global tellers = [Teller(i) for i = 1:N_TELLERS]
global teller_queue = FifoQueue{Customer}()
@schedule at 0.0 generate(1)
println("Hello world?")
start_simulation()
print_stats(teller_queue.n)
plot_history(teller_queue.n, "event_based.png")
end
end
main()
Example-1
"""
A simple implementation of an event-based simulation. This program simulates a bank.
"""
using SimLynx
using Distributions: Exponential, Uniform
using Random
const N_TELLERS = 2
tellers = nothing
"Process to generate n customers arriving into the system."
@process generator(n::Integer) begin
for i = 1:n
work(rand(Exponential(4.0)))
@schedule now customer(i)
end
end
"The ith customer into the system."
@process customer(i::Integer) begin
dist = Uniform(2.0, 10.0)
@with_resource tellers begin
work(rand(dist))
end
end
"Run the simulation for n customers."
function run_simulation(n::Integer)
@with_new_simulation begin
global tellers = Resource(N_TELLERS, "tellers")
@schedule at 0.0 generator(n)
start_simulation()
print_stats(tellers.allocated, "Allocated Statistics")
print_stats(tellers.queue_length, "Queue Length Statistics")
plot_history(tellers.queue_length, "queue_length.png",
"Queue Length History")
end
end
run_simulation(100)
Example-2
"""
Like example-1.jl but using @event instead of @process for the generate
functionality. Also uses the (experimental) trace functionality.
To Do:
--- (1)
We get the following error when we use
'using Distributions: Exponential, Uniform'
but not when we use
'using Distributions'
ERROR: LoadError: TaskFailedException:
UndefVarError: Distributions not defined
Stacktrace:
[1] macro expansion at C:\Users\doug\Develop\SimLynx\example-2.jl:23 [inlined]
[2] (::var"#26#28"{Int64})() at C:\Users\doug\Develop\SimLynx\SimLynx.jl:41
[3] start_simulation() at C:\Users\doug\Develop\SimLynx\SimLynx.jl:349
[4] macro expansion at C:\Users\doug\Develop\SimLynx\example-2.jl:41 [inlined]
[5] macro expansion at C:\Users\doug\Develop\SimLynx\SimLynx.jl:246 [inlined]
[6] (::var"#33#34")() at .\task.jl:356
Stacktrace:
[1] wait at .\task.jl:267 [inlined]
[2] macro expansion at C:\Users\doug\Develop\SimLynx\SimLynx.jl:251 [inlined]
[3] run_simulation() at C:\Users\doug\Develop\SimLynx\example-2.jl:37
[4] top-level scope at C:\Users\doug\Develop\SimLynx\example-2.jl:49
[5] include_string(::Function, ::Module, ::String, ::String) at .\loading.jl:1088
in expression starting at C:\Users\doug\Develop\SimLynx\example-2.jl:49
--- (1)
"""
using SimLynx
using Distributions
using Random
const N_TELLERS = 2
const N_CUSTOMERS = 10
tellers = nothing
"Generate the ith customer and schedule the next arrival."
@event generate(i::Integer) begin
if i <= N_CUSTOMERS
@schedule now customer(i)
@schedule in rand(Distributions.Exponential(4.0)) generate(i + 1)
end
end
"The ith customer into the system."
@process customer(i::Integer) begin
dist = Distributions.Uniform(2.0, 10.0)
@with_resource tellers begin
work(rand(dist))
end
end
"Run the simulation."
function run_simulation()
@with_new_simulation begin
current_trace!(true)
global tellers = Resource(N_TELLERS, "tellers")
@schedule at 0.0 generate(1)
start_simulation()
print_stats(tellers.allocated, "Allocated Statistics")
print_stats(tellers.queue_length, "Queue Length Statistics")
plot_history(tellers.queue_length, "queue_length.png",
"Queue Length History")
end
end
run_simulation()
Example-3
"""
This example demonstrates nested simulations, which is used to run multiple
simulation runs to gather statistics (e.g., distributions) across the runs. This
example just executes the multiple runs without gathering additional data.
"""
using SimLynx
using Distributions: Exponential, Uniform
using Random
const N_TELLERS = 2
tellers = nothing
"Process to generate n customers arriving into the system."
@process generator(n::Integer) begin
dist = Exponential(4.0)
for i = 1:n
work(rand(dist))
@schedule now customer(i)
end
end
"The ith customer into the system."
@process customer(i::Integer) begin
dist = Uniform(2.0, 10.0)
@with_resource tellers begin
work(rand(dist))
end
end
"Run the simulation for n customers."
function run_simulation(n::Integer)
@with_new_simulation begin
for i = 1:10
@with_new_simulation begin
global tellers = Resource(N_TELLERS, "tellers")
@schedule at 0.0 generator(n)
start_simulation()
print_stats(tellers.allocated, "Allocated Statistics")
print_stats(tellers.queue_length, "Queue Length Statistics")
plot_history(tellers.queue_length, "queue_length.png",
"Queue Length History")
print_stats(tellers.wait, "Queue Wait Statistics")
end
end
end
end
run_simulation(1_000)
Example-4
"""
Example nested simulation models with data collection
"""
using SimLynx
using Distributions: Exponential, Uniform
using Random
const N_TELLERS = 2
tellers = nothing
"Process to generate n customers arriving into the system."
@process generator(n::Integer) begin
dist = Exponential(4.0)
for i = 1:n
work(rand(dist))
@schedule now customer(i)
end
end
"The ith customer into the system."
@process customer(i::Integer) begin
@with_resource tellers begin
work(rand(Uniform(2.0, 10.0)))
end
end
"Run the simulation for n customers."
function run_simulation(n₁::Integer, n₂::Integer)
@with_new_simulation begin
avg_wait = Variable{Float64}(data=:tally, history=true)
for i = 1:n₁
@with_new_simulation begin
global tellers = Resource(N_TELLERS, "tellers")
@schedule at 0.0 generator(n₂)
start_simulation()
set!(avg_wait, mean(tellers.wait.stats))
end
end
print_stats(avg_wait)
plot_history(avg_wait, "avg-weight.png")
end
end
@time run_simulation(10_000, 1_000)
Example-5
"""
This is an example on an open-loop simulation model. This example gathers
statistics on the maximum number of tellers needed for no customer waiting.
"""
using SimLynx
using Distributions: Exponential, Uniform
using Random
const N_TELLERS = 2
tellers = nothing
"Process to generate n customers arriving into the system."
@process generator(n::Integer) begin
dist = Exponential(4.0)
for i = 1:n
work(rand(dist))
@schedule now customer(i)
end
end
"The ith customer into the system."
@process customer(i::Integer) begin
@with_resource tellers begin
work(rand(Uniform(2.0, 10.0)))
end
end
"Run the simulation for n customers."
function run_simulation(n₁::Integer, n₂::Integer)
@with_new_simulation begin
max_tellers = Variable{Int64}(data=:tally, history=true)
for i = 1:n₁
@with_new_simulation begin
global tellers = Resource("tellers")
@schedule at 0.0 generator(n₂)
start_simulation()
sync!(tellers.allocated)
set!(max_tellers, tellers.allocated.stats.max)
end
end
print_stats(max_tellers)
plot_history(max_tellers, "max_tellers.png")
end
end
@time run_simulation(10_000, 1_000)
Harbor Model
"""
The Harbor Model is an example simulation that leverages the resume, suspend, and interrupt methods of SimLynx.
"""
using SimLynx
using Distributions: Exponential, Uniform
using Random
cycle_time = nothing
dock = nothing
queue = nothing
@process scheduler() begin
i = 1
while true
@schedule in 0.0 ship(i)
work(rand(Exponential(4.0 / 3.0)))
i += 1
end
end
@process ship(i::Integer) begin
arrival_time = current_time()
current_process_store(:unloading_time, rand(Uniform(1.0, 2.5)))
if !harbor_master(current_process(), :arriving)
enqueue!(queue, current_process())
suspend()
end
work(current_process_store(:unloading_time))
remove(dock, current_process())
set!(cycle_time, current_time() - arrival_time)
harbor_master(current_process(), :leaving)
return nothing
end
function harbor_master(ship::Process, action::Symbol)
if action == :arriving
if length(dock.data) < 2
# The dock is not full
if isempty(dock)
process_store(ship,
:unloading_time,
process_store(ship, :unloading_time) / 2)
else
other_ship = first(dock)
notice = interrupt(other_ship)
notice.time = current_time() + 2*notice.time
resume(other_ship, notice)
end
enqueue!(dock, ship)
return true
else
# The dock is full
return false
end
elseif action == :leaving
if isempty(queue)
if !isempty(dock)
other_ship = first(dock)
notice = interrupt(other_ship)
notice.time = current_time() + 2/notice.time
resume(other_ship, notice)
end
else
next_ship = dequeue!(queue)
enqueue!(dock, next_ship)
resume(next_ship, Notice(current_time(), next_ship))
end
return true
else
error("harbor_master: illegal action value $action")
end
end
@event stop_sim() begin
println("Harbor Model - report after $(current_time()) - $(cycle_time.stats.n)")
println("Minimum unload time was $(cycle_time.stats.min)")
println("Maximum unload time was $(cycle_time.stats.max)")
println("Average unload time was $(cycle_time.stats.max)")
println("Average queue of ships waiting to be unloaded was $(mean(queue.n.stats))")
println("Maximum queue of ships waiting to be unloaded was $(queue.n.stats.max)")
plot_history(queue.n, "harbor-history.png")
stop_simulation()
end
function run_simulation()
@with_new_simulation begin
# current_trace!(true)
global cycle_time = Variable{Float64}(data=:tally)
global dock = FifoQueue{Process}()
global queue = FifoQueue{Process}()
@schedule at 0.0 scheduler()
@schedule at 80.0 stop_sim()
start_simulation()
end
end
run_simulation()
Tally-And-Accumulate
"""
Example usage of the test and accumulate functionality of SimLynx
"""
using SimLynx
tallied = nothing
accumulated = nothing
@process test_process(value_durations) begin
for (value, duration) in value_durations
set!(tallied, value)
set!(accumulated, value)
work(duration)
end
end
function main(value_durations)
@with_new_simulation begin
global tallied = Variable{Int64}(data=:tally, history=true)
global accumulated = Variable{Int64}(0, history=true)
@schedule at 0.0 test_process(value_durations)
start_simulation()
println("--- Test Tally and Accumulate ---")
println("--- Tally ---")
println("N = $(tallied.stats.n)")
println("Sum = $(tallied.stats.sum)")
println("Mean = $(mean(tallied.stats))")
plot_history(tallied, "tallied.png", "Tallied History")
println("--- Accumulate ---")
sync!(accumulated) # Retrieving slots does not sync
println("N = $(accumulated.stats.n)")
println("Sum = $(accumulated.stats.sum)")
println("Mean = $(mean(accumulated.stats))")
plot_history(accumulated, "accumulated.png", "Accumulated History")
end
end
main([(1, 2.0), (2, 1.0), (3, 2.0), (4, 3.0)])