Skip to content

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 org.kalasim.*
import java.time.Instant
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.hours


createSimulation(true) {
//    tickTransform = OffsetTransform(Instant.now(), TimeUnit.MINUTES)
    tickTransform = TickTransform(TimeUnit.MINUTES)

    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= now + 3.hours )
        }
    }

    // run until a specific time
    run(until = Instant.now() + 1.hours)

    // run for a given duration
    run(1.days)

    println(now)
    println(asWallTime(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:

  • asWallTime(tickTime) - Transforms a simulation time (typically now) to the corresponding wall time.
  • asTickDuration(duration) - Transforms a wall duration into the corresponding amount of ticks.
  • asTickTime(instant) - Transforms an wall Instant 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 kotlin.time.Duration.Companion.seconds

val timeBefore = System.currentTimeMillis()

createSimulation(true) {
    // enable real-time clock synchronization
    ClockSync(tickDuration = 1.seconds)

    run(10)
}

println("time passed ${System.currentTimeMillis() - timeBefore})")
This example will execute in 10 seconds. Since the simulation is empty (for educational reasons to keep the focus on the clock here), it is entirely idle during that time.

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 to 1, 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 use the built-in env.tickMetrics monitor to analyze how much time is spent per time unit (aka tick). This monitor is not enabled by default and needs to be enabled when the environment is created by passing enableTickMetrics=true

import org.kalasim.Component
import org.kalasim.createSimulation
import org.kalasim.plot.letsplot.display

createSimulation(enableTickMetrics = true) {

    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.

  1. 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.
  2. 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.
  3. Set the correct AssertMode: The assertion mode determines which internal consistency checks are being performed. The mode can be set to Full (Slowest), Light (default) or Off (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) can be used. Both call-counts and spent-time analysis have been proven useful here.

Continuous Simulation

For some use-cases, simulations may for a very long tick 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

  1. How events are logged
  2. 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).