The discrete-event simulation package
Please see- the introduction for an overview.
- the examples for some coding examples.
Introduction
This package provides the basic mechanisms needed for scheduling calls and tasks, managing simulation time, and for queuing calls and tasks. Calls are instances of the interface org.bzdev.lang.Callable, which provides a method with no argument and no return value named call(). Typically a Callable is implemented using an anonymous class. Tasks are represented by threads. For threads associated with a simulation, only one thread will run at a time.The Simulation class handles the scheduling of events where a specific time to wait is given. It also manages a name space for all instances of SimulationObject, so that objects used in a simulation can be referenced via string giving the object's name. In addition, the class Simulation allows the user to schedule calls and tasks implemented as scripts. To enable scripting, the user must implement the protected methods getScriptEngine and getScriptLanguage, inherited from the class org.bzdev.scripting.ScriptingContext. The user should implement the protected method getDefaultBindings(). Additional
The following diagram shows a high-level view of the structure of this package, except for "factory" classes, emphasizing the classes used to write simulations:
A Callable can be scheduled directly. For a task thread, one normally schedules a Runnable and the simulation will then create the task thread. The subclasses of DefaultSimObject shown in the diagram above have implementations that interact with the Simulation's event queue, and allow tasks and instances of Callable to be put onto queues and processed in a queue-specific order. The ProcessClock class is used to handle the case in which a single entity interleaves several activities but can only do one at a time - this is similar to what happens when multiple threads run on a single CPU.
When a simulation is run, one may either specify the duration for which it is run or one can implement the interface SimulationMonitor to determine dynamically when a simulation should stop. In addition, a SimulationListener can be added to the simulation, which responds to instances of SimulationStateEvent, used to indicate what the simulation is currently doing. A SimulationListener may be used for debugging, but it can also be used if a GUI needs to track a simulation for purposes of updating the contents of a window. These classes and how they interact is shown in the following figure:
The class DefaultSimAdapter implements SimulationListener but provide defaults to make it easy to write a listener that responds to particular types of events. The class DefaultSimAdapter also allows its behavior to be specified via scripts.
In addition, it is possible to tag events with a stack trace relevant
to when the event was created. This is a relatively expensive operation,
and should normally be disabled (it is by default), but can be useful
for debugging. It is handled by the method
Simulation.setStackTraceMode(boolean)
.
The implementation of the Simulation class uses instances of the class SimulationEvent to handle scheduling events, whether events associated with a Callable, a TaskThread, a queue, or events defined by packages that implement extensions of the current package for specific kinds of simulations. The class hierarchy for simulation events is shown in the following figure:
- The class
SimulationEvent
can be subclassed to add more types of events. This is done in the org.bzdev.drama and org.bzdev.drama.generic packages to allows simulation objects called actors to send messages to each other. - The
class
TaskSimulationEvent
is the common superclass for scheduling method calls (via instances of the class org.bzdev.lang.Callable) and instances of TaskThread (the class that handles Simulation-specific threads). - The class TaskObjectSimEvent is not public, and is used to schedule a Callable and in the creation of an unscheduled TaskThread.
-
TaskThreadSimEvent
handles the case of a TaskThread pausing for some amount of simulation time. -
TaskQueueSimEvent
allows a TaskThread or a Callable to be placed on a TaskQueue. It is used internally as queues used to store instances of TaskThread and Callable have elements of this type, but the class is public so that other packages can create subclasses of TaskQueue. The constructor, however, is not public. - TaskQueuePauseSimEvent is not public and is used when a running task that is being serviced by the queue has pause, but without freeing resources that would allow another queued task to run.
Simulations may have a parent. The parent must be a scripting context
(the class Simulation
extends
ScriptingContext
)
and the case in which a parent is itself a Simulation is treated
specially. When a simulation has a parent, the parent's scripting
context is shared by the simulation's scripting context: both use the
same script engine and default bindings unless explicitly overridden.
When the parent is a simulation, the parent's event queue is shared.
The parent's name space is also used to find simulation objects. One
use of a parent simulation occurs when one has an existing simulation
and would like to add an animation to it. The org.bzdev.anim2d
package defines the Animation2D as a subclass of Simulation, so one
can constuct an Animation2D with a parent simulation. Since both
share the same event queue, one can easily generate animation frames
in sync with a simulation. This also helps keep the simulation code
separate from the animation code.
Factory classes can be used to configure simulation objects. The class hierarchy is shown in the following diagram:
The class hierarchy follows the guidelines suggested for the org.bzdev.obnaming package. where abstract factory classes with type parameters follow the class hierarchy for the objects the factories create, and non-abstract subclasses of these allow objects to be created.
Examples
As an example, the following programwill create a simulation that schedules several events. The scheduleCall method's first argument is an object whose type isimport org.bzdev.devqsim.Simulation; import org.bzdev.devqsim.TaskThread; public class stest { public static void main(String argv[]) throws Exception { Simulation sim = new Simulation(1000.0); sim.scheduleCall(() -> { System.out.println("time = " + sim.currentTime()); }, sim.getTicks(4.0)); sim.scheduleCall(() -> { System.out.println("time = " + sim.currentTime()); }, sim.getTicks(2.0)); sim.scheduleTask(() -> { for (int i = 0; i < 5; i++) { System.out.println("time = " + sim.currentTime()); TaskThread.pause(sim.getTicks(0.1)); } }, sim.getTicks(3.0)); sim.run(); } }
Callable
, which provides a single method named 'call'.
This interface is a functional interface, so the first argument for
schedulCall can be a lambda expression. The scheduleTask method's first
argument has the type Runnable
and is also a functional
method. It will schedule a task that will run in a separate thread, starting
at a particular simulation time. In the example, there is a loop that
prints a statement at successive times. The call to
TaskThread.pause(long)
will make the thread wait
for a specified amount of simulation time.
The corresponding program when scripting is used is shown below. The
program must be run by using the scrunner command, which predefines
the variable scripting
, an instance of
ExtendedScriptingContext
.
scripting.importClass("org.bzdev.devqsim.Simulation"); scripting.importClass("org.bzdev.devqsim.TaskThread"); var out = scripting.getWriter(); sim = new Simulation(scripting, 1000.0); sim.scheduleCall(function() { out.println("time = " + sim.currentTime()); }, sim.getTicks(4.0)); sim.scheduleCall(function() { out.println("time = " + sim.currentTime()); }, sim.getTicks(2.0)); sim.scheduleTaskObject(function() { for (var i = 0; i < 5; i++) { out.println("time = " + sim.currentTime()); TaskThread.pause(sim.getTicks(0.1)); } }, sim.getTicks(3.0)); sim.run();
More complex examples would define subclasses of
SimObject
. One can override the protected method
SimObject.update(double,long)
if the object's
state is explicitly dependent on time so that every call to
SimObject.update()
will change the object's
state to what it should be at the current simulation time. One can
also create various queues and place instances of
Callable
, Runnable
, or
TaskThread
on them (for a TaskThread, placing
it on a queue automatically causes the thread to pause).
For example,
scripting.importClass("org.bzdev.devqsim.Simulation"); scripting.importClass("org.bzdev.devqsim.TaskThread"); var out = scripting.getWriter(); sim = new Simulation(scripting, 1000.0); sim.createFactories("org.bzdev.devqsim", {fifof: "FifoTaskQueueFactory"}); var fifo = fifof.createObject("fifo"); sim.scheduleTask(function() { out.println("time for task = " + sim.currentTime()); fifo.addCurrentTask(sim.getTicks(1.0)); out.println("time for task = " + sim.currentTime()); fifo.addCurrentTask(sim.getTicks(1.0)); out.println("time for task = " + sim.currentTime()); fifo.addCurrentTask(sim.getTicks(1.0)); out.println("time for task = " + sim.currentTime()); }); var count = 0; var callable = { call: function() { out.println("time for call = " + sim.currentTime()); if (count++ == 3) return; fifo.addCallObject(callable, sim.getTicks(1.0)); } }; sim.scheduleCallObject(callable, 0.0); sim.run();
In this example, the call to createFactories
creates a
factory named fifof
. Its first argument is a package name
and its second argument is an ECMAScript object whose property keys
are treated as variable names and whose values are the simple class
name of a class in the package provided by its first argument. The
simulation task pauses when it calls the method
addCurrentTask
and places itself on the queue
fifo
. Similarly, the call to the
method addCallObject
restarts the Callable
that
invokes method call
defined by the ECMAScript object
callable
by queuing this Callable
with a
specified service time. A test using the variable count
terminates the sequence.