Skip to content

Commit

Permalink
v1.0.3 release
Browse files Browse the repository at this point in the history
  • Loading branch information
holgerbrandl committed Dec 13, 2024
1 parent 3ba2bbf commit 3fbb4b9
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 21 deletions.
5 changes: 2 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ plugins {
id("io.github.gradle-nexus.publish-plugin") version "2.0.0"
}

version = "1.0.2"
version = "1.0.3"


allprojects {
Expand All @@ -34,8 +34,7 @@ dependencies {

api("io.github.oshai:kotlin-logging-jvm:6.0.9")

// api("org.jetbrains.kotlinx:dataframe-core:0.14.1")
api("com.github.holgerbrandl:kdfutils:1.4.5")
api("com.github.holgerbrandl:kdfutils:1.5.0")

implementation("com.google.code.gson:gson:2.10.1")
// implementation("org.jetbrains.kotlin:kotlin-reflect:2.0.21")
Expand Down
4 changes: 4 additions & 0 deletions docs/userguide/docs/changes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Kalasim Release History

## 1.0.3

* Added slicing for `CategoryTimeline`

## 1.0


Expand Down
12 changes: 11 additions & 1 deletion docs/userguide/docs/monitors.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,17 @@ See [`MergeMonitorTests`](https://github.com/holgerbrandl/kalasim/blob/master/sr

## Slicing of monitors

**Note**: Slicing of monitors as in [salabim](https://www.salabim.org/manual/Monitor.html#slicing-of-monitors) is not yet supported. If needed please file a [ticket](https://github.com/holgerbrandl/kalasim/issues).
Slicing of monitors is supported for `CategoryTimeline`

```kotlin
val ct = CategoryTimeline("foo")

// compute frequency table for range
ct.summed(now-3.hours, now.hours)

// same but output EnumeratedDistribution
val dist: EnumeratedDistribution<String> = ct.valueDistribution(now-3.hours, now.hours)
```

Use-cases for slicing are

Expand Down
4 changes: 2 additions & 2 deletions docs/userguide/docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

## Gradle

To get started simply add it as a dependency:
To get started, simply add it as a dependency:
```
dependencies {
implementation 'com.github.holgerbrandl:kalasim:1.0.2'
implementation 'com.github.holgerbrandl:kalasim:1.0.3'
}
```

Expand Down
2 changes: 1 addition & 1 deletion experimental/optimization/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ dependencies {
api("ch.qos.logback:logback-classic:1.4.12")

implementation("com.google.code.gson:gson:2.10")
api("org.jetbrains.kotlinx:dataframe-excel:0.12.1")
api("org.jetbrains.kotlinx:dataframe-excel:0.15.0")

testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.0")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.0")
Expand Down
2 changes: 1 addition & 1 deletion modules/kravis/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ repositories {
dependencies {
api(project(":"))

api("com.github.holgerbrandl:kravis:1.0.2")
api("com.github.holgerbrandl:kravis:1.0.4")

testImplementation(kotlin("test"))
}
Expand Down
3 changes: 2 additions & 1 deletion src/main/kotlin/org/kalasim/Component.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import java.util.logging.Logger
import kotlin.math.*
import kotlin.reflect.*
import kotlin.time.Duration
import kotlin.time.DurationUnit


internal const val EPS = 1E-8
Expand Down Expand Up @@ -2125,7 +2126,7 @@ data class ComponentLifecycleRecord(
fun Component.toLifeCycleRecord(): ComponentLifecycleRecord {
val c = this

val histogram: Map<ComponentState, Double> = c.stateTimeline.summed()
val histogram = c.stateTimeline.summed().mapValues{ it.value.toDouble(DurationUnit.MINUTES)}

return ComponentLifecycleRecord(
c.name,
Expand Down
8 changes: 8 additions & 0 deletions src/main/kotlin/org/kalasim/monitors/CategoryMonitor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import org.kalasim.misc.*
import org.koin.core.Koin
import kotlin.math.roundToInt


import org.apache.commons.math3.distribution.EnumeratedDistribution
import org.apache.commons.math3.util.Pair
/**
* Frequency tally levels irrespective of current (simulation) time.
*
Expand Down Expand Up @@ -65,6 +68,11 @@ fun <T> CategoryMonitor<T>.printHistogram(values: List<T>? = null, sortByWeight:
typealias FrequencyTable<T> = Map<T, Double>


fun <T> FrequencyTable<T>.toDistribution(): EnumeratedDistribution<T> {
val distribution = this.map { Pair(it.key, it.value) }
return EnumeratedDistribution(distribution)
}

fun <T> FrequencyTable<T>.printConsole(
colWidth: Double = 40.0,
sortByWeight: Boolean = false,
Expand Down
112 changes: 101 additions & 11 deletions src/main/kotlin/org/kalasim/monitors/CategoryTimeline.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package org.kalasim.monitors

import kotlinx.datetime.Instant
import org.apache.commons.math3.distribution.EnumeratedDistribution
import org.kalasim.*
import org.kalasim.misc.AmbiguousDuration
import org.kalasim.misc.DependencyContext
import org.kalasim.misc.time.sum
import org.kalasim.misc.time.sumOf
import org.koin.core.Koin
import kotlin.time.Duration
import kotlin.time.DurationUnit
Expand Down Expand Up @@ -37,7 +40,7 @@ class CategoryTimeline<T>(
}

override fun addValue(value: T) {
if(!enabled) return
if (!enabled) return

timestamps.add(env.now)
values.add(value)
Expand All @@ -50,19 +53,19 @@ class CategoryTimeline<T>(
.zip(values)
.groupBy { it.second }
.mapValues { (_, values) ->
values.sumOf { it.first }
values.map { it.first }.sum()
}

val total = freqHist.values.sum()

return (freqHist[value] ?: error("Invalid or non-observed state")) / total
}

private fun xDuration(): DoubleArray =
private fun xDuration(unit: DurationUnit = DurationUnit.MINUTES): DoubleArray =
timestamps.toMutableList()
.apply { add(env.now) }
.zipWithNext { first, second -> second - first }
.map { it.toDouble(DurationUnit.MINUTES) }
.map { it.toDouble(unit) }
.toDoubleArray()


Expand Down Expand Up @@ -100,21 +103,105 @@ class CategoryTimeline<T>(
println("# Levels: ${this.values.distinct().size}")
println()

if(this.values.size <= 1) {
if (this.values.size <= 1) {
println("Skipping histogram of '$name' because of to few data")
return
}

// val ed = EnumeratedDistribution(hist.asCM())
// repeat(1000){ ed.sample()}.c

summed().printConsole(sortByWeight = sortByWeight, values = values)
summed()
.map { it.key to it.value.toDouble(unit = DurationUnit.MINUTES) }
.toMap()
.printConsole(sortByWeight = sortByWeight, values = values)
}

/** Accumulated retention time of the ComponentState. Only visited states will be included. */
fun summed(): FrequencyTable<T> = xDuration().zip(this.values)
.groupBy { (_, value) -> value }
.map { kv -> kv.key to kv.value.sumOf { (it.first) } }.toMap()

/**
* Calculates the distribution of timeline values within a specified time range or all available data.
*
* @param start The start time of the interval for which the distribution is to be computed.
* Defaults to `null`, indicating that the distribution starts from the beginning of the timeline.
*
* @param end The end time of the interval for which the distribution is to be computed.
* Defaults to `null`, indicating that the distribution extends to the current time.
*
* @param includeAll If `true`, includes all distinct values in the distribution even if they
* have no associated duration in the given time range. Defaults to `false`.
*
* @return An `EnumeratedDistribution` instance representing the probability distribution of values
* based on their accumulated duration within the specified time range.
*
* @throws IllegalArgumentException If the specified start time is before the timeline's earliest value
* or the specified end time is after the current time.
*/
fun valueDistribution(
start: SimTime? = null,
end: SimTime? = null,
includeAll: Boolean = false
): EnumeratedDistribution<T> {
val map = summed(start, end)
.map { it.key to it.value.toDouble(DurationUnit.MINUTES) }.toMap()

// val if(includeAll) else this.
val data = if (includeAll) values.distinct().associateWith { map[it]!! } else map

return data
.toDistribution()
}

/**
* Sums the timeline duration of values within a specified time range or all available data.
*
* @param start The start time of the interval for which the distribution is to be computed.
* Defaults to `null`, indicating that the distribution starts from the beginning of the timeline.
*
* @param end The end time of the interval for which the distribution is to be computed.
* Defaults to `null`, indicating that the distribution extends to the current time.
*
* @param includeAll If `true`, includes all distinct values in the distribution even if they
* have no associated duration in the given time range. Defaults to `false`.
*
* @return A map where the keys are the distinct values from the timeline and the values are
* their summed durations within the specified time range.
*
* @throws IllegalArgumentException If the specified start time is before the timeline's earliest value
* or the specified end time is after the current time.
*/
fun summed(
start: SimTime? = null,
end: SimTime? = null,
): Map<T, Duration> {
require(start == null || start >= timestamps.first()) { "start $start is out of timeline range [${timestamps.first()}, $now}]" }
require(end == null || end <= now) { "end $end is out of timeline range [${timestamps.first()}, $now}]" }

val statsData = statsData().asList(false)

val queryInterval = (start ?: Instant.DISTANT_PAST)..(end ?: Instant.DISTANT_FUTURE)

fun ClosedRange<Instant>.intersection(other: ClosedRange<Instant>): ClosedRange<Instant>? {
val newStart = maxOf(this.start, other.start)
val newEnd = minOf(this.endInclusive, other.endInclusive)
return if (newStart <= newEnd) newStart..newEnd else null
}

val trimmed = statsData.map {
val segment = it.timestamp..(it.timestamp + it.duration!!)
val intersect = queryInterval.intersection(segment)

if (intersect != null) {
it.copy(timestamp = intersect.start, duration = intersect.endInclusive - intersect.start)
} else {
null
}
}.filterNotNull().filter { it.duration!! > Duration.ZERO }

return trimmed
.map { it.duration!! to it.value }
.groupBy { (_, value) -> value }
.map { kv -> kv.key to kv.value.sumOf { (it.first) } }.toMap()
}


override fun statisticsSummary() = statsData().statisticalSummary()
Expand All @@ -127,6 +214,7 @@ class CategoryTimeline<T>(
val timepointsExt = timestamps + env.now
val durations = timepointsExt.toMutableList().zipWithNext { first, second -> second - first }

// xDuration()
return LevelStatsData(valuesLst, timestamps, durations)
}

Expand All @@ -140,12 +228,14 @@ class CategoryTimeline<T>(
override fun clearHistory(before: SimTime) {
val startFromIdx = timestamps.withIndex().firstOrNull { before > it.value }?.index ?: return

for(i in 0 until startFromIdx) {
for (i in 0 until startFromIdx) {
val newTime = timestamps.subList(0, startFromIdx)
val newValues = values.subList(0, startFromIdx)

timestamps.apply { clear(); addAll(newTime) }
values.apply { clear(); addAll(newValues) }
}
}


}
2 changes: 2 additions & 0 deletions src/main/kotlin/org/kalasim/monitors/ValueTimeline.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ interface ValueTimeline<T> {

/** Discards all history before the given time. */
fun clearHistory(before: SimTime)

// operator fun get(from: SimTime, to: SimTime): ValueTimeline<T>
}

// replacement for Pair to get better auto-conversion to data-frame
Expand Down
49 changes: 48 additions & 1 deletion src/test/kotlin/org/kalasim/test/MonitorTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.kotest.matchers.shouldBe
import org.apache.commons.math3.distribution.EnumeratedDistribution
import org.apache.commons.math3.stat.descriptive.StatisticalSummaryValues
import org.junit.jupiter.api.Test
import org.kalasim.get
import org.kalasim.hour
import org.kalasim.misc.*
import org.kalasim.monitors.*
Expand Down Expand Up @@ -199,7 +200,53 @@ class MonitorTests {
}
}

// TODO test the others

@Test
fun `CategoryTimeline should allow to retrieve range selection`() =
createTestSimulation {
val ct = CategoryTimeline("A")
run(2.minutes)

ct.addValue("B")
run(2.minutes)

ct.addValue("C")
run(2.minutes)

ct.addValue("D")
run(4.minutes)
ct.addValue("A")
run(3.minutes)


val rangeDist = ct.valueDistribution(startDate + 1.minutes, startDate + 4.minutes)

println(rangeDist.pmf)

rangeDist["A"] shouldBe 1.0/3
rangeDist["B"] shouldBe 2.0/3

shouldThrow<java.lang.IllegalArgumentException> {
ct.valueDistribution(startDate - 10.minutes, startDate + 4.minutes)
}
shouldThrow<java.lang.IllegalArgumentException> {
ct.valueDistribution(startDate - 10.minutes, now + 12.minutes)
}

// now we try to get a value for `now`
ct[now] shouldBe "A"

val rangeDistEnd = ct.valueDistribution(now-5.minutes)
println(rangeDistEnd)

rangeDistEnd["D"] shouldBe 0.4
rangeDistEnd["A"] shouldBe 0.6

// repeat over another range but include all values
val initRangeAll = ct.valueDistribution(end = now-5.minutes, includeAll = true)
initRangeAll.pmf.size shouldBe 4
}

}


Expand Down

0 comments on commit 3fbb4b9

Please sign in to comment.