All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.kalasim.ClockSync.kt Maven / Gradle / Ivy

There is a newer version: 1.0.3
Show newest version
package org.kalasim

import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import org.kalasim.misc.*
import org.koin.core.Koin
import kotlin.math.roundToLong
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds

/**
 * A component that allows to synchronize wall time to simulation. See the [user manual](https://www.kalasim.org/advanced/#clock-synchronization) for an in-depth overview about clock synchronization.
 *
 * @param tickDuration The duration of a simulation tick in wall clock coordinates. Can be created with Duration.ofSeconds(1), Duration.ofMinutes(n) and so on.
 * @param syncsPerTick Defines how often a clock synchronization should happen. Per default it synchronizes once per _tick_ (i.e. an 1-increment of simulation time).
 * @param maxDelay If provided, `kalasim` will throw an `ClockOverloadException` if the specified delay is exceeded by difference of wall-time and (transformed) simulation time. Defaults to `null`.
 */
class ClockSync(
    tickDuration: Duration,
    val syncsPerTick: Number = 10,
    val maxDelay: Duration? = null,
    koin: Koin = DependencyContext.get()
) : Component(
    trackingConfig = ComponentTrackingConfig(logCreation = false, logStateChangeEvents = false),
    koin = koin
) {
    // todo add secondary constructor that accepts speed-parameter instead of too complex
    /**
     * A component that allows to synchronize wall time to simulation. See the [user manual](https://www.kalasim.org/advanced/#clock-synchronization) for an in-depth overview about clock synchronization.
     *
     * @param speedUp If set to 1.0 this will run with wall-time. Setting it to 2 would be twice as fast as wall-time and so on.
     * @param syncsPerTick Defines how often a clock synchronization should happen. Per default it synchronizes once per _tick_ (i.e. an 1-increment of simulation time).
     * @param maxDelay If provided, `kalasim` will throw an `ClockOverloadException` if the specified delay is exceeded by difference of wall-time and (transformed) simulation time. Defaults to `null`.
     */
    @OptIn(AmbiguousDuration::class)
    constructor(
        speedUp: Double = 1.0,
        syncsPerTick: Number = 10,
        maxDelay: Duration? = null,
        koin: Koin = DependencyContext.get()
    ) : this(
        // compute necessary tick duration
        koin.get().asDuration(1.0 / speedUp),
        syncsPerTick,
        maxDelay,
        koin
    )

    fun adjustSpeedUp(speedUp: Double) {
        require(speedUp > 0) { "speed up must be positive" }
        logger.info { "adjusting speed up to $speedUp" }

        tickDuration = env.asDuration(1.0 / speedUp)
    }

    var tickDuration: Duration = tickDuration
        set(value) {
            field = value

            syncStartWall = null
            syncStartSim = null
        }


    val holdTime = (1.0 / syncsPerTick.toDouble()).toDuration()

    private var syncStartWall: Instant? = null
    private var syncStartSim: SimTime? = null

    override fun process() = sequence {

        while(true) {
            //wait until we have caught up with wall clock
            val tickDurationMs = tickDuration.inWholeMilliseconds.toDouble()

            // set the start time if not yet done (happens only after changing tickDuration
            syncStartSim = syncStartSim ?: env.now
            val now = Clock.System.now()
            syncStartWall = syncStartWall ?: now


//            val simTimeSinceSyncStart = ((env.now - syncStartTicks!!) * tickDurationMs).roundToLong().milliseconds
            val simTimeSinceSyncStart =
                ((env.now - syncStartSim!!).asTicks() * tickDurationMs).roundToLong().milliseconds
            val wallTimeSinceSyncStart = now - syncStartWall!!

            val sleepDuration = simTimeSinceSyncStart - wallTimeSinceSyncStart

//            println("synchronizing will wall by sleeping for $sleepDuration")

//            if (abs(sleepDuration.toMillis()) > 5000L) {
//                println("sim $simTimeSinceSyncStart wall $wallTimeSinceSyncStart; Resync by sleep for ${sleepDuration}ms")
//            }

            if(maxDelay != null && sleepDuration > maxDelay) {
                throw ClockOverloadException(
                    env.now,
                    "Maximum delay between wall clock and simulation clock exceeded at time ${env.now}. " +
                            "Difference was $sleepDuration but allowed delay was $maxDelay"
                )
            }

            // simulation is too fast if value is larger
            if(sleepDuration > Duration.ZERO) {
                // wait accordingly to let wall clock catch up
                Thread.sleep(sleepDuration.inWholeMilliseconds)
            }

            // wait until the next sync event is due
            hold(holdTime)
        }
    }
}

/**
 * Will be thrown if the maximum delay time is exceeded when using [clock synchronization](https://www.kalasim.org/advanced/#clock-synchronization).
 */
class ClockOverloadException(val timestamp: SimTime, msg: String) : RuntimeException(msg)






© 2015 - 2025 Weber Informatics LLC | Privacy Policy