Skip to content

Commit

Permalink
simplified dependency injection
Browse files Browse the repository at this point in the history
  • Loading branch information
holgerbrandl committed Jun 12, 2023
1 parent bc9625f commit 892c1e2
Show file tree
Hide file tree
Showing 54 changed files with 378 additions and 204 deletions.
6 changes: 6 additions & 0 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ https://www.baeldung.com/java-a-star-pathfinding
Add tickUnit to constructor of Environment and remove setter

---
better landing page

https://github.com/squidfunk/mkdocs-material/issues/1996
https://sdk.up42.com/


## Later


Expand Down
2 changes: 1 addition & 1 deletion docs/userguide/docs/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ It may happen that a simulation is too complex to run at a defined clock. In suc

## 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](monitors.md) 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`
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](monitors.md) to analyze how much time is spent per time unit (aka *tick*). This monitor can be enabled by calling `enableTickMetrics()` when [configuring](basics.md#configuring-a-simulation) the simulation.

```kotlin hl_lines="5"
{!api/TickMetricsExample.kts!}
Expand Down
11 changes: 10 additions & 1 deletion docs/userguide/docs/basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ All entities in a simulation are governed by an environment context. Every simul
The environment context of a kalasim simulation is an instance of `org.kalasim.Environment`, which can be created using simple instantiation or via a builder called `createSimulation`

```kotlin
val env : Environment = createSimulation(enableConsoleLogger = true){
val env : Environment = createSimulation(){
// enable logging of built-in simulation metrics
enableComponentLogger()

// Create simulation entities in here
Car()
Resource("Car Wash")
Expand Down Expand Up @@ -122,6 +125,12 @@ So the key points to recall are
* Race-conditions between events can be avoided by setting a `priority`


## Configuring a Simulation

To minimze initial complexity when creating an environment, some options can be enabled within the scope of an environment
* `enableTickMetrics()` - See [tick metrics](advanced.md#operational-control)
* `enableComponentLogger()` - Enable the [component logger](events.md#component-logger) to track component status

## Dependency Injection

Kalasim is building on top of [koin](https://insert-koin.io/) to inject dependencies between elements of a simulation. This allows creating simulation entities such as resources, components or states conveniently without passing around references.
Expand Down
11 changes: 11 additions & 0 deletions docs/userguide/docs/changes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Kalasim Release History

## v0.10

Not yet released

Breaking changes
* tick metrics and component-logger are now [configured](basics.md#configuring-a-simulation) and not enabled via constructor parameter any longer (to minimize constructor complexity)
*

Improvments
* More robust [dependency injection](basics.md#dependency-injection) with

## v0.9

Released at 2023-04-13
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@ import org.openrndr.draw.*
import org.openrndr.extra.gui.GUI
import org.openrndr.extra.parameters.ActionParameter
import org.openrndr.extra.parameters.IntParameter
import org.openrndr.ffmpeg.ScreenRecorder
import org.openrndr.math.Vector3
import org.openrndr.math.transforms.buildTransform
import org.openrndr.math.transforms.project
import org.openrndr.shape.*
import kotlin.time.DurationUnit
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds

Expand Down
28 changes: 26 additions & 2 deletions src/main/kotlin/org/kalasim/Component.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ data class Priority(val value: Int) : Comparable<Priority?> {
override fun compareTo(other: Priority?): Int = compareValuesBy(this, other ?: NORMAL) { value }
}

//class SimSequence(val component: Component) : Sequence<Component> {
// override fun iterator(): Iterator<Component> {
// return super.iterator()
// }
//}
//
//public inline fun <T> Component.simSequence(crossinline iterator: () -> Iterator<T>): Sequence<T> = object : SimSequence<T> {
// override fun iterator(): Iterator<T> = iterator()
//}


// TODO reassess if we should use these type-aliases
//typealias ProcessDefinition = SequenceScope<Component>
Expand Down Expand Up @@ -1294,8 +1304,13 @@ open class Component(
until: TickTime? = null,
priority: Priority = NORMAL,
urgent: Boolean = false
) = yieldCurrent {
this@Component.hold(duration, description, until, priority, urgent)
) {
// val component = getThis()
// val other = this
// println(component)
yieldCurrent {
this@Component.hold(duration, description, until, priority, urgent)
}
}

/**
Expand Down Expand Up @@ -1601,6 +1616,15 @@ open class Component(
private suspend fun SequenceScope<Component>.yieldCurrent(builder: () -> Unit = {}) {
val initialStatus = componentState

require(initialStatus == CURRENT) {
"""Component context violated. Kalasim has detected an invalid call of
an outer scope function. This can happen if a sub-process defined
within a component class is calling methods of the parent scope.
Unfortunately, this can't be resolved at compile time.
""".trimIndent()
}


builder()

if(initialStatus == CURRENT) {
Expand Down
74 changes: 46 additions & 28 deletions src/main/kotlin/org/kalasim/Environment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import org.koin.core.qualifier.Qualifier
import org.koin.dsl.koinApplication
import org.koin.dsl.module
import kotlinx.datetime.Instant
import org.koin.core.qualifier.named
import java.util.*
import kotlin.time.*
import kotlin.time.Duration.Companion.days
Expand All @@ -28,59 +29,49 @@ internal const val MAIN = "main"

typealias KoinModule = org.koin.core.module.Module

//internal class EnvBuildContext : KoinModule() {
// var enableConsoleLogger: Boolean = true
//}
// --> not possible because Module is not open

// https://github.com/InsertKoinIO/koin/issues/801
@Deprecated("Use createSimulation() instead", ReplaceWith("declareDependencies(builder).createSimulation {}"))
fun configureEnvironment(
enableConsoleLogger: Boolean = false, builder: KoinModule.() -> Unit
): Environment = declareDependencies(builder).createSimulation(enableConsoleLogger) {}
enableComponentLogger: Boolean = false, builder: KoinModule.() -> Unit
): Environment = declareDependencies(builder).createSimulation {}


@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated("Use createSimulation() instead")
fun declareDependencies(
builder: KoinModule.() -> Unit
): KoinModule = module(createdAtStart = true) { builder() }


fun KoinModule.createSimulation(
enableConsoleLogger: Boolean = false,
/** The duration unit of this environment. Every tick corresponds to a unit duration. See https://www.kalasim.org/basics/#running-a-simulation */
durationUnit: DurationUnit = MINUTES,
/** The absolute time at tick-time 0. Defaults to `null`.*/
startDate: Instant? = null,
enableTickMetrics: Boolean = false,
useCustomKoin: Boolean = false,
randomSeed: Int = DEFAULT_SEED,
builder: Environment.() -> Unit
): Environment = createSimulation(
durationUnit = durationUnit,
startDate = startDate,
enableConsoleLogger = enableConsoleLogger,
enableTickMetrics = enableTickMetrics,
dependencies = this,
useCustomKoin = useCustomKoin,
randomSeed = randomSeed,
builder = builder
)

fun createSimulation(
enableConsoleLogger: Boolean = false,
/** The duration unit of this environment. Every tick corresponds to a unit duration. See https://www.kalasim.org/basics/#running-a-simulation */
durationUnit: DurationUnit = MINUTES,
/** The absolute time at tick-time 0. Defaults to `null`.*/
startDate: Instant? = null,
enableTickMetrics: Boolean = false,
dependencies: KoinModule? = null,
useCustomKoin: Boolean = false,
randomSeed: Int = DEFAULT_SEED,
builder: Environment.() -> Unit
): Environment = Environment(
durationUnit = durationUnit,
startDate = startDate,
enableConsoleLogger = enableConsoleLogger,
enableTickMetrics = enableTickMetrics,
dependencies = dependencies,
randomSeed = randomSeed,
koin = if(useCustomKoin) koinApplication { }.koin else null
Expand Down Expand Up @@ -113,7 +104,7 @@ open class Environment(
/** The absolute time at tick-time 0. Defaults to `null`.*/
var startDate: Instant? = null,
/** If enabled, it will render a tabular view of recorded interaction and resource events. */
enableConsoleLogger: Boolean = false,
enableComponentLogger: Boolean = false,
/** Measure the compute time per tick as function of time. For details see https://www.kalasim.org/advanced/#operational-control */
enableTickMetrics: Boolean = false,
dependencies: KoinModule? = null,
Expand Down Expand Up @@ -141,7 +132,7 @@ open class Environment(
// get() = eventQueue.map { it.component }
get() = eventQueue.sortedIterator().map { it.component }.toList()

private val eventListeners = listOf<EventListener>().toMutableList()
internal val eventListeners = listOf<EventListener>().toMutableList()


val trackingPolicyFactory = TrackingPolicyFactory()
Expand Down Expand Up @@ -193,14 +184,19 @@ open class Environment(
qualifier: Qualifier? = null, noinline parameters: ParametersDefinition? = null
): T = getKoin().get(qualifier, parameters)

/** Resolves a dependency in the simulation. Dependencies can be disambiguated by using a qualifier.*/
inline fun <reified T : Any> get(
qualifier: String , noinline parameters: ParametersDefinition? = null
): T = getKoin().get(named(qualifier), parameters)


init {

// start console logger

// addTraceListener { print(it) }
if(enableConsoleLogger) {
enableConsoleLogger()
if(enableComponentLogger) {
enableComponentLogger()
}

_koin = koin ?: run {
Expand Down Expand Up @@ -238,16 +234,20 @@ open class Environment(
// curComponent = main
}

fun enableConsoleLogger() {
addEventListener(ConsoleTraceLogger())
}

private val _tm: TickMetrics? = if(enableTickMetrics) TickMetrics(koin = koin) else null
fun enableTickMetrics(){
require(queue.none{ it is TickMetrics}){
"The tick-metrics monitor is already registered"
}

TickMetrics(koin = getKoin())
}

val tickMetrics: MetricTimeline<Int>
get() {
require(_tm != null) { "Use enableTickMetrics=true to enable tick metrics" }
return _tm.timeline
val tm = queue.filterIsInstance<TickMetrics>().firstOrNull()
require(tm != null) { "Use enableTickMetrics=true to enable tick metrics" }
return tm.timeline
}

// private var endOnEmptyEventlist = false
Expand Down Expand Up @@ -528,6 +528,15 @@ data class QueueElement(
}


fun Environment.enableComponentLogger() {
require(eventListeners.none{ it is ConsoleTraceLogger}){
"The component-logger is already registered"
}

addEventListener(ConsoleTraceLogger())
}


internal fun Environment.calcScheduleTime(until: TickTime?, duration: Number?): TickTime {
return (until?.value to duration?.toDouble()).let { (till, duration) ->
if(till == null) {
Expand All @@ -541,19 +550,28 @@ internal fun Environment.calcScheduleTime(until: TickTime?, duration: Number?):
}


inline fun <reified T> KoinModule.add(
/** Register dependency in simulation context. For details see https://www.kalasim.org/basics/#dependency-injection */
inline fun <reified T> KoinModule.dependency(
qualifier: Qualifier? = null, noinline definition: Definition<T>
) {
single(qualifier = qualifier, createdAtStart = true, definition = definition)

}


/** Register dependency in simulation context. For details see https://www.kalasim.org/basics/#dependency-injection */
inline fun <reified T> Environment.dependency(qualifier: String, builder: Environment.() -> T) = dependency(named(qualifier), builder)

/** Register dependency in simulation context. For details see https://www.kalasim.org/basics/#dependency-injection */
inline fun <reified T> Environment.dependency(qualifier: Qualifier? = null, builder: Environment.() -> T): T {
val something = builder(this)

getKoin().loadModules(listOf(module(createdAtStart = true) {
add(qualifier) { something }
dependency(qualifier) { something }
}))

require(something !is Unit){
"dependency must not be last element in createSimulation{} as this is causing type inference to fail internally. Add println() or any other terminal statement to work around this problem."
}
return something
}

Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/org/kalasim/analysis/ConsoleTraceLogger.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import org.kalasim.misc.TRACE_DF
import org.kalasim.misc.titlecaseFirstChar
import java.util.logging.Level



class ConsoleTraceLogger(var logLevel: Level = Level.INFO) : EventListener {

enum class EventsTableColumn { Time, Current, Receiver, Action, Info }
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/org/kalasim/analysis/Logging.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ fun interface EventListener {
}




/**
* Activates a global event-log, which stores all events on the kalasim event bus.
*
Expand All @@ -54,6 +56,7 @@ fun Environment.enableEventLog(): EventLog {
return tc
}


/** A list of all events that were created in a simulation run. See [Event Log](https://www.kalasim.org/events/) for details. */
class EventLog(val events: MutableList<Event> = mutableListOf()) : EventListener,
MutableList<Event> by events {
Expand Down
9 changes: 8 additions & 1 deletion src/main/kotlin/org/kalasim/analysis/TickMetrics.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import kotlin.math.round

/** Allows introspection of time-complexity of the underlying computations. The user may want to use the built-in env.tickMetrics timeline to analyze how much time is spent per time unit (aka tick).
*
* https://www.kalasim.org/advanced/#operational-control */
* https://www.kalasim.org/advanced/#operational-control
*/
class TickMetrics(
val sampleTicks: Double = 1.0,
/** Enable recording of tick metrics via a `timeline` attribute of this object. */
Expand All @@ -24,6 +25,12 @@ class TickMetrics(

val timeline = MetricTimeline(name, 0)

init{
require( env.queue.count{ it is TickMetrics} ==1){
"tick metrics must be enabled just once"
}
}

override fun process() = sequence {
// hold(ceil(now.value))

Expand Down
2 changes: 0 additions & 2 deletions src/main/kotlin/org/kalasim/examples/elevator/Elevator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import org.kalasim.animation.AnimationComponent
import org.kalasim.examples.elevator.Car.DoorState.CLOSED
import org.kalasim.examples.elevator.Car.DoorState.OPEN
import org.kalasim.examples.elevator.Direction.*
import org.kalasim.misc.AmbiguousDuration
import org.kalasim.misc.repeat
import org.kalasim.plot.kravis.display
import java.awt.Point
import kotlin.time.Duration.Companion.days
import kotlin.time.DurationUnit
Expand Down
9 changes: 4 additions & 5 deletions src/main/kotlin/org/kalasim/examples/er/EmergencyRoom.kt
Original file line number Diff line number Diff line change
Expand Up @@ -247,20 +247,19 @@ class EmergencyRoom(
val numRooms: Int = 4,
/** The execution planning policy of the ER. */
val nurse: HeadNurse = FifoNurse(),
enableConsoleLogger: Boolean = false,
enableComponentLogger: Boolean = false,
enableTickMetrics: Boolean = false,
) : Environment(
enableConsoleLogger = enableConsoleLogger,
enableComponentLogger = enableComponentLogger,
enableTickMetrics = enableTickMetrics,
durationUnit = DurationUnit.HOURS
) {

val waitingAreaSize = 300

// todo this should be opt-in anyway https://github.com/holgerbrandl/kalasim/issues/66
init {
init{
trackingPolicyFactory.disableAll()
}
val waitingAreaSize = 300

// todo also here having sorted queue is causing almost more problems than solving
// val waitingLine = ComponentQueue(comparator = compareBy <Patient>{ it.severity.value }, name = "ER Waiting Area")
Expand Down
Loading

0 comments on commit 892c1e2

Please sign in to comment.