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)])