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.EventListener
s. 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"))
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.seconds)
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).days) { Component("Car.${it}") }
// add custom log consumer
addAsyncEventListener<InteractionEvent> { event ->
if(event.current?.name == "ComponentGenerator.1")
println(event)
}
// run the simulation
run(10.weeks)
}
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.
- Interactions via
InteractionEvent
- Entity creation via
EntityCreatedEvent
- Resource requests, see resource 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
)
}
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 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 .