diff --git a/alchemist-implementationbase/src/main/kotlin/it/unibo/alchemist/model/terminators/StableMetrics.kt b/alchemist-implementationbase/src/main/kotlin/it/unibo/alchemist/model/terminators/StableMetrics.kt new file mode 100644 index 0000000000..db1165c231 --- /dev/null +++ b/alchemist-implementationbase/src/main/kotlin/it/unibo/alchemist/model/terminators/StableMetrics.kt @@ -0,0 +1,172 @@ +package it.unibo.alchemist.model.terminators + +import it.unibo.alchemist.model.Environment +import it.unibo.alchemist.model.Position +import it.unibo.alchemist.model.TerminationPredicate +import it.unibo.alchemist.model.Time +import it.unibo.alchemist.model.Time.Companion.INFINITY +import it.unibo.alchemist.model.Time.Companion.ZERO +import kotlin.Long.Companion.MAX_VALUE + +/** + * [stableForTotalSteps] for how many steps the [metricsToCheck] should be stable, + * zero if taking into account just the time. + * [checkStepInterval] every step it should check, if zero it checks at every invocation. + * [stableForTotalTime] for how much time the [metricsToCheck] should be stable, + * zero if taking into account just the steps. + * [checkTimeInterval] every time-step it should check, if zero it checks at every invocation. + */ +class StableMetrics( + private val stableForTotalSteps: Long, // total steps to be stable + private val checkStepInterval: Long, // steps interval to check + private val stableForTotalTime: Time, // total time to be stable + private val checkTimeInterval: Time, // time interval to check + private val metricsToCheck: (Environment>) -> Map, +) : TerminationPredicate> { + constructor( + stableForTotalSteps: Long, + checkStepInterval: Long, + metricsToCheck: (Environment>) -> Map, + ) : this( + stableForTotalSteps = stableForTotalSteps, + checkStepInterval = checkStepInterval, + stableForTotalTime = ZERO, + checkTimeInterval = ZERO, + metricsToCheck = metricsToCheck, + ) + constructor( + stableForTotalTime: Time, + checkTimeInterval: Time, + metricsToCheck: (Environment>) -> Map, + ) : this( + stableForTotalSteps = 0, + checkStepInterval = 0, + stableForTotalTime = stableForTotalTime, + checkTimeInterval = checkTimeInterval, + metricsToCheck = metricsToCheck, + ) + + private val stepTracker by lazy { StepMetricTracker() } + private val timeTracker by lazy { TimeMetricTracker() } + init { + require(stableForTotalTime > ZERO || stableForTotalSteps > 0) { + "At least one of the stability conditions (stableForTime or stableForSteps) must be greater than zero." + } + require(checkTimeInterval <= stableForTotalTime) { + "The time interval to check should be less than or equal to the stable time." + } + require(checkStepInterval <= stableForTotalSteps) { + "The step interval to check should be less than or equal to the stable steps." + } + } + + override fun invoke(environment: Environment>): Boolean = when { + stableForTotalTime > DEFAULT_TIME && stableForTotalSteps > DEFAULT_STEP -> { + val stableSteps = checkStability(environment, stepTracker, STEP_MAX_VALUE, DEFAULT_STEP) + val stableTime = checkStability(environment, timeTracker, TIME_MAX_VALUE, DEFAULT_TIME) + stableTime && stableSteps + } + stableForTotalTime > DEFAULT_TIME -> + checkStability(environment, timeTracker, TIME_MAX_VALUE, DEFAULT_TIME) + stableForTotalSteps > DEFAULT_STEP -> + checkStability(environment, stepTracker, STEP_MAX_VALUE, DEFAULT_STEP) + else -> error("This should never happen.") + } + + private fun > checkStability( + env: Environment>, + tracker: MetricTracker, + maxValue: Type, + defaultValue: Type, + ): Boolean = with(tracker) { + val current = current(env) + val interval = evaluateInterval(current) + return when { + stableValue >= maxValue -> true + shouldBeChecked(interval) -> { + lastChecked = current + val currentMetrics = metricsToCheck(env).also { + require(it.isNotEmpty()) { "There should be at least one metric to check." } + } + when { + currentMetrics == lastMetrics -> { + increaseStability(interval) + return (stableValue >= totalStability).also { if (it) stableValue = maxValue } + } + else -> { + stableValue = defaultValue + lastMetrics = currentMetrics + false + } + } + } + else -> false + } + } + + private interface MetricTracker> { + var lastChecked: Type + var stableValue: Type + var lastMetrics: Map + val totalStability: Type + val checkInterval: Type + fun current(env: Environment>): Type + fun evaluateInterval(current: Type): Type + fun shouldBeChecked(interval: Type): Boolean = interval >= checkInterval + fun increaseStability(interval: Type) + } + + private inner class StepMetricTracker : MetricTracker { + override var lastChecked = DEFAULT_STEP + override var stableValue = DEFAULT_STEP + override var lastMetrics: Map = emptyMap() + override val totalStability = stableForTotalSteps + override val checkInterval = checkStepInterval + override fun current(env: Environment>): Long = env.simulation.step + override fun evaluateInterval(current: Long): Long = current - lastChecked + override fun increaseStability(interval: Long) { + stableValue += interval + } + } + + private inner class TimeMetricTracker : MetricTracker { + override var lastChecked = DEFAULT_TIME + override var stableValue = DEFAULT_TIME + override var lastMetrics: Map = emptyMap() + override val totalStability = stableForTotalTime + override val checkInterval = checkTimeInterval + override fun current(env: Environment>): Time = env.simulation.time + override fun evaluateInterval(current: Time): Time = current - lastChecked + override fun increaseStability(interval: Time) { + stableValue += interval + } + } + + /** + * Companion object for StableMetrics. + * + * Provides constants relevant to the stability checks, such as maximum + * and default values for steps and time. + */ + companion object { + /** + * The maximum value for steps, used to determine when the step stability is reached. + */ + const val STEP_MAX_VALUE: Long = MAX_VALUE + + /** + * The default value for steps, used when no stability is required. + */ + const val DEFAULT_STEP: Long = 0L + + /** + * The maximum value for time, used to determine when the time stability is reached. + */ + val TIME_MAX_VALUE: Time = INFINITY + + /** + * The default time value, used when no stability is required. + */ + val DEFAULT_TIME: Time = ZERO + } +}