Skip to content

Events

To analyze state changes in a simulation model, we may want to monitor component creation, the event queue, or the interplay between simulation entities. 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.

kalasim internally triggers a rich set of built-int events

In addition, it also allows for custom event types that can be triggered with log() in process definitions

class MyEvent(time : TickTime) : Event(time)

object : Component() {
    override fun process() = sequence {
        //... a great history
        log(MyEvent(now))
        //... a promising future
    }
}

The event log is modelled as a sequence of org.kalasim.Events that can be consumed with one more multiple org.kalasim.EventListeners. The classical publish-subscribe pattern is used here. Consumers can easily route events into such as consoles, files, rest-endpoints, databases, or in-place-analytics.

To get started, we can register new 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: Event -> 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.

import org.kalasim.*

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

createSimulation {

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

    addEventListener<MyEvent> { println(it.context) }

    run()
}

In this example, we have created custom simulation event type. This approach is very common: By using custom event types when building process models with kalasim state changes can be consumed very selectively in analysis and visualization.

Component Logger

There are a few provided event listeners, most notable the built-in component logger. With component logging being enabled, kalasim will print a tabular listing of component state changes. 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, and but must be enabled when creating a simulation.

Note

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

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 : TickTime) : Event(time)

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

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

// 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.analysis.*
import org.kalasim.createSimulation
import org.kalasim.enableComponentLogger
import org.kalasim.enableEventLog

createSimulation {
    enableComponentLogger()

    val tc = enableEventLog()

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

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

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.

Logging Configuration

Typically, only some types of event logging are required in a given simulation. To optimize simulation performance, the engine allows to suppress selectively per event type and simulation entity. This is configured via tracking policy factory

Logging Framework Support

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.analysis.ResourceEvent
import org.kalasim.examples.er.EmergencyRoom
import java.util.logging.Logger
import kotlin.time.Duration.Companion.days

// Create simulation
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 sim
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 .