No description
Find a file
2020-08-03 21:02:11 -06:00
examples Changed checkin example to use logging 2020-07-15 21:53:46 -06:00
src Updated and filled out documentation for desim.nim 2020-07-21 20:40:40 -06:00
tests Added access to all unreceived messages at shutdown 2020-07-14 20:20:05 -06:00
desim.nimble Created basic framework and small unit test 2020-06-18 21:15:08 -06:00
license.txt Create license.txt 2020-08-03 21:02:11 -06:00
README.md Added link to Wikipedia in readme 2020-08-02 21:00:43 -06:00
todo.txt Added access to all unreceived messages at shutdown 2020-07-14 20:20:05 -06:00

Desim

Desim is a Discrete Event Simulator modeled on SST but written in Nim and with a focus on making your own custom components. Desim allows you to model your problem with several components that communicate using messages. The structure of Desim focuses on message sending and receiving in a way that will allow parallel execution. At the moment, however, Desim only runs on a single thread.

Quick Examples

One component Hello World

Below is a simple hello world style program. It creates a single component that prints "Hello, World!" on startup then performs no other actions. The simulation will start this component then immediately stop, as there are no pending messages. This illustrates much of the necessary boilerplate but a useful simulation will have additional functionality.

import desim

# Define a custom component type
type HelloComponent = ref object of Component

# Define our component's constructor. The rest of its functionality is defined with the component template.
proc newHelloComponent(name: string): HelloComponent =
  HelloComponent(name: name)

# Define our component's behavior. The first argument gives a name to the component to use in this template.
# The second is the type of the component.
component comp, HelloComponent:
  # Define behavior for our component to do on startup
  startup:
    echo "Hello, World!"
  # Define behavior when our component is shut down
  shutdown:
    echo "Goodbye, World"

proc main() =
  var
    # Every simulation requires exactly one Simulator object.
    sim = newSimulator()
    hello = newHelloComponent("hello")

  # All components must be registered before the simulator starts
  sim.register hello
  
  # Run the simulator until a shutdown condition occurs
  sim.run

main()

One Component Timer Hello World

This example still uses one component but adds a timer, which sends a message to its component at some time in the future.

import desim

type
  HelloComponent = ref object of Component
    # Define a timer object. The messages type is taken as a generic argument to the Timer type
    timer: Timer[string]

proc newHelloComponent(name: string): HelloComponent =
  HelloComponent(name: name)

component comp, HelloComponent:
  startup:
    # Set the timer to go off to send the message "Hello, World!" in 1 tick.
    comp.timer.set("Hello, World!", 1)

  # Iterate over all messages. All messages must be cleared as soon as they are received.
  for msg in messages(comp.timer):
    # The type of msg is the type given as the generic argument to Timer
    echo msg

proc main() =
  var
    sim = newSimulator()
    hello = newHelloComponent("hello")

  sim.register hello
  
  sim.run

main()

Two component Hello World

import desim

type
  HelloComponent = ref object of Component
    # Messages are sent out on a Link with a given message type
    link: Link[string]
  RecvComponent = ref object of Component
    # Messages of a given type are received on a Port.
    port: Port[string]

proc newHelloComponent(name: string): HelloComponent =
  # All links must be initialized with newLink. It takes the base delay as an argument
  HelloComponent(name: name, link: newLink[string](100))

proc newRecvComponent(name: string): RecvComponent =
  # All ports must be initialized with newPort but take no arguments
  RecvComponent(name: name, port: newPort[string]())

component comp, HelloComponent:
  startup:
    comp.link.send "Hello, World!"

component comp, RecvComponent:
  for msg in messages(comp.port):
    echo msg

proc main() =
  var
    sim = newSimulator()
    hello = newHelloComponent("hello")
    recv = newRecvComponent("recv")

  sim.register hello
  sim.register recv

  # Every Link must be connected to exactly one Port. Multiple Links may connect to the
  # same port.
  connect hello.link, recv.port
  
  sim.run

main()

Logging Hello World

This example uses logging instead of echo to print its message. It uses the only pre-defined component which is the LogComponent. The logger outputs in JSON but this can be configured.

import desim
# The LogComponent requires this import
import desim/components/logger

type
  HelloComponent = ref object of Component
    # Although not necessary, it is recommended that you name Logger logger.
    logger: Logger

proc newHelloComponent(name: string): HelloComponent =
  HelloComponent(name: name)

component comp, HelloComponent:
  startup:
    # The logger has convenience methods corresponding to each default log level.
    # In this case we use the info log level.
    comp.logger.info "Hello, World"

proc main() =
  var
    sim = newSimulator()
    hello = newHelloComponent("hello")
    # If the simulation uses logging then it must have one LogComponent
    logcomp = newLogComponent()
    # The LogBuilder is a convenience class that connects components to the LogComponent.
    # It is initialized with a reference to the logging component.
    logbuilder = newLoggerBuilder(logcomp)

  # Indicate that we want to log everything at or above the info level
  logbuilder.setLevel LogLevel.info
  # Connect the hello component's logger to the LogComponent associated with this
  # LoggerBuilder. This by default assumes the Logger is called logger (though this
  # is configurable).
  logbuilder.attach hello

  sim.register hello
  # The LogComponent is a regular component and must be registered
  sim.register logcomp

  sim.run

main()

Types

The operation of a discrete event simulation using Desim requires several types. Most of them have already been introduced in the examples section. More detailed API information can be found by compiling the documentation from the code.

Simulator

This class is in charge of orchestrating a simulation. Since Desim uses no global variables, all simulation state not contained in a Component is found in the Simulator. From the user's perspective this is mostly the current simulation time retrieved with the currentTime proc. All components must be register'd with the same simulator before calling the simulator's run command.

Component

All functionality is expected to be provided by components derived from the Component base class. The base class has one user-accessible field, name, which can be any string. The name is currently not used by the Desim framework but is used by the logging framework. Communication between components requires creating fields that are a Link, Port, or Timer, explained further in their own sections.

The behavior of a component is described with the component template. The code inside this macro is run once when the component is first created, once every simulation step in which it will receive at least one message, and once right before it is shut down. Any code in the template will be run every time one of those events happens. To limit code execution to those events, use the startup and shutdown templates and the messages iterator. See the examples section for usage ideas.

A Link object handles the outgoing end of inter-component communication. A Link is a generic type that takes the message type as its generic argument. A Link will also have a base latency which cannot be zero. This is in immitation of SST, which uses this nonzero latency as a way to automatically separate components into different threads or MPI ranks. While Desim is currently single-threaded, it takes the same approach so as to leave open the option of efficient parallelization in the future. A message is sent with the send proc which takes the message and an optional amount of extra time, which is zero by default.

Each Link must be connected to exactly one Port. It is an error to not connect a Link or to connect it to more than one Port. The Port must accept the same message type as the Link sends.

A BcastLink is used to broadcast messages to many Port objects. It operates identically to a Link, except without the restriction that it be connected to one, or indeed any, Port.

The BatchLink type is another type of Link which is likely not directly useful. It does not take a latency; rather the framework defines the latency for each message. The framework is therefore free to choose an efficient latency, or buffering scheme, in order to combine communication between components in the most efficient way possible. Since Desim is currently single threaded this is not taken advantage of, but could be.

Since the whole point of a discrete event simulation is to simulate timing of actions between components, this link type is rarely useful. It is used in the logger, since the logger is a component but not part of the domain being simulated. Log messages may thus be batched and efficiently delivered to the logger. This behavior is all hidden from the user.

Port

All messages sent on a link are received on a Port. The Port is a generic type that takes the message type as its argument. The messages on a port can be read out using the messages iterator inside the component template. All messages must be read in the same simulation tick that they are received.

Timer

A timer takes care of the degenerate, but useful, case where a link and port that are connected together are part of the same component by design. Such self-loops may occur frequently enough to warrant special handling. A Timer is also a generic type that takes the message type as its argument. Messages are sent with the send proc and received with the messages iterator.

SimulationTime

The time in the simulation is tracked with a SimulationTime object. Currently implemented as an int, this value will always act like an integer but the actual implementation may change to accommodate larger numbers.

SimulationError

All exceptions thrown by the Desim framework will be a SimulationError.