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
- Interactions via
InteractionEvent
- Entity creation via
EntityCreatedEvent
- Resource requests, see resource 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.Event
s that can be consumed with one more multiple org.kalasim.EventListener
s. 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"))
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 Event
s. 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 .