Skip to content

Events

Every simulation includes an internal event bus to provide another way to enable connectivity between simulation components. Components can use log(event) to publish to the event bus from their process definitions.

Also, events can be used to study dynamics in a simulation model. We may want to monitor component creation, the event queue, or the interplay between simulation entities, or custom business dependent events of interest. We may want to trace which process caused an event, or which processes waited for resource. Or a model may require other custom state change events to be monitored.

How to create an event?

To create a custom event type, we need to subcalss org.kalasim.Event. Events can be published to the internal event bus using log() in process definitions. Here's a simple example

import org.kalasim.*

class MyEvent(val context: String, time: SimTime) : Event(time)

createSimulation {

    object : Component("Something") {
        override fun process() = sequence<Component> {
            //... a great history
            log(MyEvent("foo", now))
            //... a promising future
        }
    }

    // register to these events from the environment level
    addEventListener<MyEvent> { println(it.context) }

    run()
}

In this example, we have created custom simulation event type MyEvent which stores some additional context detail about the process. This approach is very common: By using custom event types when building process models with kalasim, state changes can be consumed very selectively for analysis and visualization.

How to listen to events?

The event log can be consumed with one more multiple org.kalasim.EventListeners. The classical publish-subscribe pattern is used here. Consumers can easily route events into arbitrary sinks such as consoles, files, rest-endpoints, and databases, or perform in-place-analytics.

We can register a event handlers with addEventListener(org.kalasim.EventListener). Since an EventListener is modelled as a functional interface, the syntax is very concise and optionally supports generics:

createSimulation { 
    // register to all events
    addEventListener{ it: MyEvent -> println(it)}

    // ... or without the lambda arument just with
    addEventListener<MyEvent>{  println(it)}

    // register listener only for resource-events
    addEventListener<ResourceEvent>{ it: ResourceEvent -> println(it)}    
}

Event listener implementations typically do not want to consume all events but filter for specific types or simulation entities. This filtering can be implemented in the listener or by providing a the type of interest, when adding the listener.

Event Collector

A more selective monitor that will just events of a certain type is the event collector. It needs to be created before running the simulation (or from the moment when events shall be collected).

class MyEvent(time : SimTime) : Event(time)

// run the sim which create many events including some MyEvents
env.run()

val myEvents : List<MyEvent> = collect<MyEvent>()

// or collect with an additional filter condition
val myFilteredEvents :List<MyEvent> = collect<MyEvent> {
    it.toString().startsWith("Foo")
}

// e.g. save them into a csv file with krangl
myEvents.asDataFrame().writeCsv(File("my_events.csv"))
This collector will have a much reduced memory footprint compared to the event log.

Event Log

Another built-in event listener is the trace collector, which simply records all events and puts them in a list for later analysis.

For example to fetch all events in retrospect related to resource requests we could filter by the corresponding event type

////EventCollector.kts
import org.kalasim.*
import org.kalasim.analysis.*

createSimulation {
    enableComponentLogger()

    // enable a global list that will capture all events excluding StateChangedEvent
    val eventLog = enableEventLog(blackList = listOf(StateChangedEvent::class))

    // run the simulation
    run(5.0)

    eventLog.filter { it is InteractionEvent && it.component?.name == "foo" }

    val claims = eventLog //
        .filterIsInstance<ResourceEvent>()
        .filter { it.type == ResourceEventType.CLAIMED }
}

Asynchronous Event Consumption

Sometimes, events can not be consumed in the simulation thread, but must be processed asynchronously. To do so we could use a custom thread or we could setup a coroutines channel for log events to be consumed asynchronously. These technicalities are already internalized in addAsyncEventLister which can be parameterized with a custom coroutine scope if needed. So to consume, events asynchronously, we can do:

////LogChannelConsumerDsl.kt
import org.kalasim.*
import org.kalasim.analysis.InteractionEvent

createSimulation {
    ComponentGenerator(iat = constant(1)) { Component("Car.${it}") }

    // add custom log consumer
    addAsyncEventListener<InteractionEvent> { event ->
        if(event.current?.name == "ComponentGenerator.1")
            println(event)
    }

    // run the simulation
    run(10)
}

In the example, we can think of a channel as a pipe between two coroutines. For details see the great article Kotlin: Diving in to Coroutines and Channels.

Internal Events

kalasim is using the event-bus extensively to publish a rich set of built-int events.

To speed up simulations, internal events can be disabled.

Component Logger

For internal interaction events, the library provides a built-in textual logger. With component logging being enabled, kalasim will print a tabular listing of component state changes and interactions. Example:

time      current component        component                action      info                          
--------- ------------------------ ------------------------ ----------- -----------------------------
.00                                main                     DATA        create
.00       main
.00                                Car.1                    DATA        create
.00                                Car.1                    DATA        activate
.00                                main                     CURRENT     run +5.0
.00       Car.1
.00                                Car.1                    CURRENT     hold +1.0
1.00                               Car.1                    CURRENT
1.00                               Car.1                    DATA        ended
5.00      main
Process finished with exit code 0

Console logging is not active by default as it would considerably slow down larger simulations. It can be enabled when creating a simulation.

createSimuation(enableComponentLogger = true){
    // some great sim in here!!
}

Note

The user can change the width of individual columns with ConsoleTraceLogger.setColumnWidth()

Bus Metrics

By creating a BusMetrics within a simulation environment, log statistics (load & distribution) are computed and logged to the bus.

createSimulation{
    BusMetrics(
        timelineInterval = 3.seconds,
        walltimeInterval =  20.seconds
    )    
}
Here, every 3 seconds in simulation time, the event rate is logged. Additionally, every 20 walltime second, the event rate is logged asynchronously.

Metrics are logged via slf4j. The async logging can be stopped via busMetrics.stop().

Logging Framework Support

kalasim is using slf4j as logging abstraction layer. So, it's very easy to also log kalasim events via another logging library such as log4j, https://logging.apache.org/log4j/2.x/, kotlin-logging or the jdk-bundled util-logger. This is how it works:

////LoggingAdapter.kts
import org.kalasim.examples.er.EmergencyRoom
import java.util.logging.Logger
import kotlin.time.Duration.Companion.days

// Create a simulation of an emergency room
val er = EmergencyRoom()

// Add a custom event handler to forward events to the used logging library
er.addEventListener { event ->
    // resolve the event type to a dedicated logger to allow fine-grained control
    val logger = Logger.getLogger(event::class.java.name)

    logger.info { event.toString() }
}

// Run the model for 100 days
er.run(100.days)

For an in-depth logging framework support discussion see #40.

Tabular Interface

A typesafe data-structure is usually the preferred for modelling. However, accessing data in a tabular format can also be helpful to enable statistical analyses. Enabled by krangl's Iterable<T>.asDataFrame() extension, we can transform records, events and simulation entities easily into tables. This also provides a semantic compatibility layer with other DES engines (such as simmer), that are centered around tables for model analysis.

We can apply such a transformation simulation Events. For example, we can apply an instance filter to the recorded log to extract only log records relating to resource requests. These can be transformed and converted to a csv with just:

// ... add your simulation here ...
data class RequestRecord(val requester: String, val timestamp: Double, 
            val resource: String, val quantity: Double)

val tc = sim.get<TraceCollector>()
val requests = tc.filterIsInstance<ResourceEvent>().map {
    val amountDirected = (if(it.type == ResourceEventType.RELEASED) -1 else 1) * it.amount
    RequestRecord(it.requester.name, it.time, it.resource.name, amountDirected)
}

// transform data into data-frame (for visualization and stats)  
requests.asDataFrame().writeCSV("requests.csv")

The transformation step is optional, List<Event> can be transformed asDataFrame() directly.

Events in Jupyter

When working with jupyter, we can harvest the kernel's built-in rendering capabilities to render events. Note that we need to filter for specific event type to capture all attributes.

For a fully worked out example see one of the example notebooks .