Simulua Discrete-event simulation in Lua

Introduction

What Simulua is

Simulua is a discrete-event simulation (DES) library for Lua, in the same tradition and flavor of the SIMULA family of programming languages. The simulation in Simulua is process-oriented, that is, the operation path of a simulated system is obtained from interactions of processes running in parallel. Events are then implicitly managed through process management and scheduled through an event list. Processes are implemented in Simulua using property tables and their corresponding coroutines as execution threads.

The Simulation section gives more details about the simulation engine, Processes explains processes, Control describes how the simulation is managed, and section Examples provides some examples. The examples, in particular, are very helpful in understanding the concepts. Containers, accumulators and other important auxiliary routines are described in Facilities.

Simulua is licensed under the same license as Lua—the MIT license—and so can be freely used for academic and commercial purposes. Please refer to the Installation section for more details.

Simulation

Process-oriented DES in Lua

The main concept in Simulua is the process, represented by a pair: a table that stores properties—the process "object" itself—and a corresponding coroutine that creates and manages events. The sequence of events that composes a simulation run is executed through an event list that is implemented as a binomial heap with event times as priorities. The event list, and hence process execution, is managed by an implicit scheduler/dispatcher coroutine. The scheduler advances the simulation time to the next event time of the latest retrieved process from the list. Note that it is possible for two processes to execute in "parallel", that is, at the same simulation time.

The currently active process and current simulation time can be queried with simulua.current and simulua.time :

simulua.current()

Returns the process that is currently executing.

simulua.time([proc])

If proc is nil returns the current simulation time, otherwise returns the event time of process proc .

Processes

The main actors

A process is created with simulua.process :

simulua.process(exec [, prop])

Returns a process that executes function exec and has optional property table prop .

A process resumes execution when it is retrieved from the event list at the current time. If a process is not in the event list it is said to be idle. A process is inserted into the event list when it is activated; the activation can occur after either some time delay or process in the event list. A process can cancel the execution of another process, effectively removing the latter from the event list; if the process cancels itself it is said to passivate. A hold operation reschedules a process to resume after a time delay. A process can also wait for a container, like a queue: in this case the process is inserted, or pushed, into the container and passivates. The relevant Simulua process methods are listed below.

simulua.idle(proc)

Returns true if process proc is idle.

simulua.activate(proc [, delay [, after]])

Activates process proc if it is idle, inserting it into the event list, or reactivates it by changing its event time in the list.

Parameter delay can be either a number, in which case proc receives priority time simulua.time() + delay in the event list, or a process, when proc acquires event time simulua.time(proc) . If delay is nil or delay < 0 , then delay is set to zero. If delay is an idle process, proc is canceled.

If after is true the insertion or change operation has no priority, that is, proc is positioned after processes with the same priority time in the event list. Note that heap operations assume priority by default.

simulua.cancel(proc)

Removes process proc from the event list (it has no effect if proc is not scheduled).

simulua.passivate()

Equivalent to simulua.cancel(simulua.current()) .

simulua.hold(delay)

Reschedules the current process to event time simulua.time() + delay . If delay is nil or negative, delay is set to zero.

simulua.wait(cont, ...)

Inserts current process into container cont and passivates. A valid container must have a method into for insertions. Any extra parameters to simulua.wait are passed to cont.into .

Control

Managing simulation execution

There are only two methods that control the simulation: simulua.start and simulua.stop . The simulation itself is run by a main process that is created and scheduled at time zero by simulua.start . The simulation ends when the main process finishes executing.

simulua.start(exec)

Starts the simulation by creating the main process having exec as function. simulua.start initializes the event priority heap, creates the main process, and resumes the scheduler.

simulua.stop()

Stops the simulation. It is equivalent to simulua.cancel(main) .

Examples

Understanding simulation

The following examples are adapted from Chapter 19 of Rob Pooley's book, An introduction to programming in Simula (we strongly recommend reading—and referring to—this chapter to better understand the examples and the simulation concepts from SIMULA). The main methods from Simulua are highlighted in bold in the code.

The first example implements a simple, deterministic simulation of a mill worker day. We have two processes, mill and worker , that run concurrently after worker is first activated by the main process: the worker loads the mill in five units of time, activates the mill and checks regularly for it to finish its load, and finally unloads the mill; the mill simply processes its load at two time units per component and finishes when it is empty.

-- Mill model, example 19.1 from -- Pooley, R., "An introduction to programming in Simula" local simulua = require "simulua" -- variables local count = 0 -- processes local mill = {components = 0} mill = simulua.process(function() while true do print("Machine starts", simulua.time()) while mill.components > 0 do simulua.hold(2) -- machining time for one component mill.components = mill.components - 1 end simulua.passivate() end end, mill) local worker = simulua.process(function() while simulua.time() < 400 do print("Loading starts", simulua.time()) count = count + 1 -- keep a tally simulua.hold(5) mill.components = mill.components + 50 -- load up simulua.activate(mill) -- restart machine while mill.components > 0 do simulua.hold(0.5) end -- check regularly simulua.cancel(mill) -- switch off simulua.hold(10) -- unloading takes longer print("Unloading finishes", simulua.time()) end simulua.passivate() end) -- simulation simulua.start(function() -- main simulua.activate(worker) print(string.format("count = %d", count)) simulua.hold(800) print("Simulation ends", simulua.time()) end)

The main methods in this example are simulua.activate , simulua.hold and simulua.cancel (also implicit in simulua.passivate ) as they control process cooperation and thus the simulation execution. Note that simulua.time() is used to check for the end of a worker's day, the end of the simulation, and to report activities. Chapter 19 of Rob Pooley's book gives more details about the simulation run.

The next example models an employment office with two interviewers and a shared receptionist. We again refer to Chapter 19 for a better description of the simulation, but the concepts and methods should be getting more familiar. The new resources in this example are the queue container and simulua.wait : the main process pushes four job hunters, each of different skill, through the system, where they should wait for the receptionist in queue receptionist.Q and then for their respective interviewer in either manual.Q or skilled.Q .

-- Employment office queuing model, example 19.2 from -- Pooley, R., "An introduction to programming in Simula" local simulua = require "simulua" local queue = require "queue" -- variables local MANUAL = 1 -- skill category -- processes local manual, skilled -- interviewers local receptionist local function interviewer (title) local interviewerQ = queue() return simulua.process(function() while true do if not interviewerQ:isempty() then simulua.hold(3.5) -- interview time taken as 3.5 minutes local next = interviewerQ:retrieve() simulua.activate(next, simulua.current(), true) -- after current simulua.hold(3) -- 3 minutes to clear desk else simulua.hold(5) -- wait 5 minutes before checking queue again end end end, {Q = interviewerQ}) end local function jobhunter (skill) return simulua.process(function() print(string.format( "Job hunter %d joins receptionist queue at time %.1f", skill, simulua.time())) simulua.wait(receptionist.Q) print(string.format( "Job hunter %d joins interview queue at time %.1f", skill, simulua.time())) simulua.hold(1) -- 1 minute to join new queue if skill == MANUAL then simulua.wait(manual.Q) else simulua.wait(skilled.Q) end print(string.format( "Job hunter %d leaves employment office at time %.1f", skill, simulua.time())) end) end do -- receptionist local receptionistQ = queue() receptionist = simulua.process(function() while true do if not receptionistQ:isempty() then simulua.hold(2) local customer = receptionistQ:retrieve() simulua.activate(customer) else simulua.hold(1) end end end, {Q = receptionistQ}) end -- simulation simulua.start(function() simulua.activate(receptionist) manual = interviewer"Manual" simulua.activate(manual) skilled = interviewer"Skilled" simulua.activate(skilled) for _, skill in ipairs{1, 2, 2, 1} do simulua.activate(jobhunter(skill)) simulua.hold(2) end simulua.hold(100) end)

All three examples in Chapter 19 can be found in the examples folder of the Simulua distribution.

Facilities

Containers, accumulators, and probabilities

We have already seen a container, a queue, in the last section. The motivation behind containers in Simulua is to provide mechanisms for process scheduling through simulua.wait . To this end we provide three containers: queues, that implement a LIFO policy (module queue ); stacks, for FIFO policy (module stack ); and binomial heaps for priority based policy (module binomial ). For a container to be used by simulua.wait it needs to provide a into method for insertion.

Sometimes we want to report the mean of a variable in the simulation. In this case, another useful facility is an accumulator, that updates the value of the mean as requested during a simulation run. The mean of a variable as tracked by an accumulator accum can be recovered in accum.mean .

simulua.accumulator()

Returns an accumulator, a table with keys last storing the last updated time and mean representing the last updated mean.

Updates accumulator accum at time last according to last observed value . If last is nil it is set to simulua.time() .

For convenience, Simulua also includes libraries for random number generation (module rng ) and cumulative distribution functions (module cdf ), both borrowed from Numeric Lua.

The next example simulates a warehouse where both batch arrival and processing times are exponentially distributed. Batches can be rejected if the number of units exceeds warehouse storage. We also keep track of the number of items in the warehouse and report its mean along with the proportion of rejected batches at the end of the simulation.

-- The "versatile warehouse model" from Section 3.1 of -- Mitrani, I. (1982), "Simulation techniques for discrete event systems" local simulua = require "simulua" local rng = require "rng" local queue = require "queue" -- variables local r = rng() -- note: change seed for different runs local warehouse = simulua.accumulator() -- for number of items local n = 0 -- number of current items in warehouse local arrived, rejected = 0, 0 -- number of items -- parameters local arr, rem = 20, 10 -- arrival and removal means local in1, in2 = 3, 5 -- range for arrivals local out1, out2 = 4, 6 -- range for removals local m = 10 -- units of storage local simperiod = 1000 -- simulation period -- processes local arrivals, worker do -- arrivals local number -- of items in batch arrivals = simulua.process(function() while true do simulua.hold(r:exp(arr)) arrived = arrived + 1 number = r:unifint(in1, in2) if number > m - n then rejected = rejected + 1 else warehouse:update(n) n = n + number if simulua.idle(worker) then simulua.activate(worker) end end end end) end do -- worker local size, number -- size of outgoing batch and number removed worker = simulua.process(function() while true do while n > 0 do simulua.hold(r:exp(rem)) warehouse:update(n) size = r:unifint(out1, out2) number = size < n and size or n n = n - number end simulua.passivate() -- warehouse is now empty end end) end -- simulation simulua.start(function() simulua.activate(arrivals) simulua.activate(worker) simulua.hold(simperiod) print(string.format("Proportion of rejected batches: %.2f", rejected / arrived)) print(string.format("Average no. of items in warehouse: %.2f", warehouse.mean)) end)

Installation

How to obtain and install Simulua

Simulua is distributed as a source package and can be obtained at Luaforge. Depending on how Lua 5.1 is installed in your system you might have to edit the Makefile and copy the modules to their suitable places. Alternatively, you can use LuaRocks and install from the rockspec also available in Luaforge. Simulua can also be built standalone from folder etc in the distribution.

License

Copyright (c) 2008 Luis Carvalho

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.