Advanced
Tick Transformation
Simulation time is measured in ticks. Usually, a simulation starts at 0
and then progresses through actions such as hold or wait or component generation.
To express duration more naturally, and to enable a more eye-friendly logging and to stay closer to the system under study, kalasim
supports a built in transformation tickTransform
to convert from simulation to wall clock. Let's consider the following example
//TickTrafoExample.kts
import kotlinx.datetime.Clock
import org.kalasim.*
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
import kotlin.time.DurationUnit
// note MINUTES is also kalasim's default
createSimulation(durationUnit = DurationUnit.MINUTES) {
enableComponentLogger()
object :Component(){
override fun process() =sequence {
hold(1.minutes, description = "dressing")
// we can express fractional durations (1.3 hours = 78 minutes)
hold(1.3.hours, description = "walking")
// we can also hold until a specific time
hold(until= nowWT + 3.hours )
}
}
// run until a specific time
run(until = Clock.System.now() + 1.hours)
// run for a given duration
run(1.days)
println(now)
println(toWallTime(now))
}
As shown in the example there are 2 flavors
1. TickTransform
takes a temporal unit as argument
2. OffsetTransform
takes a temporal offset (expressed as java.time.Instant
) in additional to a temporal unit. By doing so, the simulation can not just map ticks to durations, but also tick-times to wall-time.
This example will run for 2h in total which is transformed to 2x60 ticks, and will report a transform wall time of now
plus 120 minutes. It also illustrates the 3 supported provided transformations:
toWallTime(tickTime)
- Transforms a simulation time (typicallynow
) to the corresponding wall time.asTickDuration(duration)
- Transforms a wallduration
into the corresponding amount of ticks.asTickTime(instant)
- Transforms an wallInstant
to simulation time.
Please note that setting this transformation does not impact the actual simulation, which is always carried out in ticks. It can be configured independently from the clock synchronization described above.
There is one provided implementation OffsetTransform
that can be instantiated with a start time offset the unit of a tick. The user can also implement own transformation by implementing the functional interface TickTransform
.
Clock Synchronization
In simulation a clear distinction is made between real time and simulation time. With real time we refer to the wall-clock time. It represents the execution time of the experiment. The simulation time is an attribute of the simulator.
To support use cases where a simulation may drive a demonstration or system check, the kalasim
API allows to run a simulation at a defined clock speed. Such real-time simulations may be necessary
- If you have hardware-in-the-loop
- If the intent of the simulation is to drive a visualization of a process
- If there is human interaction with your simulation, or
- If you want to analyze the real-time behavior of an algorithm
import org.kalasim.ClockSync
import org.kalasim.createSimulation
import org.kalasim.enableComponentLogger
import kotlin.time.Duration.Companion.seconds
val timeBefore = System.currentTimeMillis()
createSimulation {
enableComponentLogger()
// enable real-time clock synchronization
ClockSync(tickDuration = 1.seconds)
run(10)
}
println("time passed ${System.currentTimeMillis() - timeBefore})")
To enable clock synchronization, we need to add a ClockSync
to our simulation. We need to define what one tick in simulation time corresponds to in wall time. In the example, one tick equals to one second wall time. This is configured with the parameter tickDuration
. It defines the duration of a simulation tick in wall clock coordinates. It can be created with Duration.ofSeconds(1)
, Duration.ofMinutes(10)
and so on.
ClockSync
also provides settings for more advanced uses-cases
- To run simulations, in more than realtime, the user can specify
speedUp
to run a simulation faster (speedUp
> 1) or slower (speedUp
< 1) than realtime. It defaults to1
, that is no speed-up will be applied. - The argument
syncsPerTick
defines how often a clock synchronization should happen. Per default it synchronizes once per tick (i.e. an 1-increment of simulation time).
It may happen that a simulation is too complex to run at a defined clock. In such a situation, it (i.e. Environment.run()
) will throw a ClockOverloadException
if the user has specified a maximum delay maxDelay
parameter between simulation and wall clock coordinates.
Operational Control
Even if kalasim
tries to provide a simplistic, efficient, declarative approach to define a simulation, it may come along with computational demands simulation. To allow introspection into time-complexity of the underlying computations, the user may want to enable the built-in env.tickMetrics
monitor to analyze how much time is spent per time unit (aka tick). This monitor can be enabled by calling enableTickMetrics()
when configuring the simulation.
import org.kalasim.Component
import org.kalasim.createSimulation
import org.kalasim.plot.letsplot.display
createSimulation() {
enableTickMetrics()
object : Component() {
override fun process() = sequence {
while (true) {
// create some artificial non-linear compute load
if (now.value < 7)
Thread.sleep((now.value * 100).toLong())
else {
Thread.sleep(100)
}
hold(1)
}
}
}
run(10)
tickMetrics.display().show()
}
Performance tuning
There are multiple ways to improve the performance of a simulation.
- Disable internal event logging: The interaction model is configured by default to provide insights into the simulation via the event log. However, to optimize performance of a simulation a user may want to consume only custom event-types. If so, internal interaction logging can be adjusted by setting a logging policy.
- Disable component statistics: Components and queues log various component statistics with built-in monitors which can be adjusted by setting a logging policy to reduce compute and memory footprint of a simulation.
- Set the correct
AssertMode
: The assertion mode determines which internal consistency checks are being performed. The mode can be set toFull
(Slowest),Light
(default) orOff
(Fastest). Depending on simulation logic and complexity, this will improve performance by ~20%.
To further fine-tune and optimize simulation performance and to reveal bottlenecks, a JVM profiler (such as yourkit or the built-in profiler of Intellij IDEA Ultimate) can be used. Both call-counts and spent-time analysis have been proven useful here.
Continuous Simulation
For some use-cases, simulations may run for a very long simulation and wall time. To prevent internal metrics gathering from consuming all available memory, it needs to be disabled or at least configured carefully. This can be achieved, but either disabling timelines and monitors manually on a per-entity basis, or by setting a sensible default strategy using the Environment.trackingPolicyFactory
For each entity type a corresponding tracking-policy TrackingConfig
can be provisioned along with an entity matcher to narrow down its scope. A tracking-policy allows to change
- How events are logged
- How internal metrics are gathered
// first define the policy and matcher
env.trackingPolicyFactory
.register(ResourceTrackingConfig().copy(trackUtilization = false)) {
it.name.startsWith("Counter")
}
// Second, we can create entities that will comply to the polices if being matched
val r = Resource("Counter 22")
There are different default implementations, but the user can also implement and register custom tracking-configurations.
- ComponentTrackingConfig
- ResourceTrackingConfig
- StateTrackingConfig
- ComponentCollectionTrackingConfig
Note
Tracking configuration policies must be set before instantiating simulation entities to be used. After entities have been created, the user can still configure via c.trackingConfig
.
To disable all metrics and to minimize internal event logging, the user can run env.trackingPolicyFactory.disableAll()
The same mechanism applies also fine-tune the internal event logging. By disabling some - not-needed for production - events, simulation performance can be improved significantly.
The user can also register her own TrackConfig
implementations using the factory. See here for simple example.
Save and Load Simulations
kalasim
does not include a default mechanism to serialize and deserialize simulations yet. However, it seems that with xstream that Environment
can be saved including its current simulation state across all included entities. It can be restored from the xml snapshot and continued with run()
.
We have not succeeded to do the same with gson yet. Also, some experiments with kotlinx.serialization were not that successful.
Internal State Validation
The simulation engine provides different levels of internal consistency checks. As these are partially computationally expensive these can be be/disabled. There are 3 modes
OFF
- Productive mode, where asserts that may impact performance are disabled.LIGHT
- Disables compute-intensive asserts. This will have a minimal to moderate performance impact on simulations.FULL
- Full introspection, this will have a measurable performance impact on simulations. E.g. it will validate that passive components are not scheduled, and queued components have unique names.
Switching off asserts, will typically optimize performance by another ~20% (depending on simulation logic).