Skip to content

Component

Components are the key elements of a simulation. By means of process definition of the business process in study, components allow modeling the interplay with other simulation components as well as its timing.

Components can be either in DATA or an ACTIVE lifecycle state. An ACTIVE component has one or more process definitions of which one was activated at some point in time.

With activate() we can change a DATA component to ACTIVE, if it has an associated process definition.

An ACTIVE component can become DATA either with a cancel() or by reaching the end of a definition.

It is very easy to create a DATA components

val component = Component()

Components will interact with each other through a well-defined vocabulary of process interaction methods.

Info

By default, Components will be named automatically, using the pattern [Class Name].[Instance Number] unless a custom name is provided via the name parameter in Component(name="Foo"). Kalasim also supports auto-indexing if a provided component name ends with a dash -, dot . or underscore _. E.g. A first Component("Foo-") will be named Foo-1, a second one Foo-2 and so on.

Process Definition

Although it is possible to create a component directly with val x = Component(), this makes it very hard to make that component into an active component, because there's no process method. So, nearly always we define our simulation entities by extending Component and by providing a process definition which details out the component's life cycle:

Important

The process definition of a component defines its dynamics and interplay with other simulation entities. Writing down the process definition is the key modelling task when using kalasim.

If there is no process definition, a component will always be a data component.

There are 3 supported methods to provide a process definition.

1. Extend process

Let's start with the most common method. In order to make an active component it is necessary to extend Component to provide (at least) one sequence generator method, normally called process:

class Car: Component(){
    override fun process() = sequence {
        wait(customerArrived)

        request(driver){
            hold(4, "driving")
        }
    }
}

If we then say val car = Car(), a component is created, and it activated from process. This process is nearly always, but not necessarily a generator method (i.e. it has at least one yield statement).

The result is that car is put on the future event list (for time now) and when it's its turn, the component becomes CURRENT.

It is also possible to set a time at which the component (car) becomes active, like val car = Car(at=10). This requires an additional constructor argument to be passed on to Component as in class Car(at:Number): Component(delay=at).

Creation and activation are by default combined when creating a new Component instance:

val car1 = Car()
val car2 = Car()
val car3 = Car()

This causes three cars to be created and to be activated, that is scheduled for execution in the simulation's event queue.

Normally, any process definition will contain at least one yield statement. By doing so, the component can hand-back control to the simulation engine at defined points when a component needs to wait. Typically, the user must not use yield directly, but rather the provided process interaction methods.

2. Extend repeatedProcess

A very common pattern in process definition, iteratively executed processes. This could be modelled with

class Machine : Component(){
    override fun process() = sequence {
        while(true) {
          wait(hasMaterial)
          hold(7, "drilling")
        }
    }
}

This can be expressed more elegantly with repeatedProcess:

class Machine : Component(){
    override fun repeatedProcess() = sequence {
        wait(hasMaterial)
        hold(7, "drilling")
    }
}

Info

Finally, if there is a process or repeatedProcess method, you can disable the automatic activation (i.e. make it a data component), by specifying Component(process = Component::none).

3. Process Reference

A component may be initialized to start at another process definition method. This is achieved by passing a reference to this method which must be part of the component's class definition, like val car = Car(process = Car::wash).

It is also possible to prepare multiple process definition, which may become active later by means of an activate() statement:

////CraneProcess.kts
import org.kalasim.*

class Crane(
    process: ProcessPointer? = Component::process
) : Component(process = Crane::load) {
    fun unload() = sequence<Component> {
        // hold, request, wait ...
    }

    fun load() = sequence<Component> {
        // hold, request, wait ...
    }
}

createSimulation {
    val crane1 = Crane() // load will be activated be default

    val crane2 = Crane(process = Crane::load) // force unloading at start

    val crane3 = Crane(process = Crane::unload) // force unloading at start
    crane3.activate(process = Crane::load) // activate other process
}

Effectively, creation and start of crane1 and crane2 is the same.

Lifecycle

A component is always in one of the following states modelled by org.kalasim.ComponentState:

  • CURRENT - The component's process is currently being executed by the event queue
  • SCHEDULED - The component is scheduled for future execution
  • PASSIVE - The component is idle
  • REQUESTING - The component is waiting for a resource requirement to be met
  • WAITING - The component is waiting for a state predicate to be met
  • STANDBY - The component was put on standby
  • INTERRUPTED - The component was interrupted
  • DATA - The component is non of the active states above. Components without a process definition are always in this state.

A component's status is automatically tracked in the status level monitor component.statusMonitor. Thus, it possible to check how long a component has been in a particuar state with

val passiveDuration = component.statusMonitor[ComponentState.PASSIVE]

It is possible to print a histogram with all the statuses a component has been in with

component.statusMonitor.printHistogram()

Accumulated times in a particular state can be obtained with summed() and be printed to console or displayed with the selected graphics backend

val timeInEachState = component.statusMonitor.summed()

timeInEachState.printConsole()
timeInEachState.display()

Process Interaction

The scheme below shows how interaction relate to component state transitions:

from/to data current scheduled passive requesting waiting standby interrupted
data activate1 activate
current process end yield hold yield passivate yield request yield wait yield standby
. yield cancel yield activate
scheduled cancel next event hold passivate request wait standby interrupt
. activate
passive cancel activate1 activate request wait standby interrupt
. hold2
requesting cancel claim honor activate3 passivate request wait standby interrupt
. time out activate4
waiting cancel wait honor activate5 passivate wait wait standby interrupt
. timeout activate6
standby cancel next event activate passivate request wait interrupt
interrupted cancel resume7 resume7 resume7 resume7 resume7 interrupt8
. activate passivate request wait standby
  1. Via scheduled()
  2. Not recommended
  3. With keepRequest = false (default)
  4. With keepRequest = true. This allows to set a new time out
  5. With keepWait = false (default)
  6. With keepWait = true. This allows to set a new timeout
  7. State at time of interrupt
  8. Increases the interruptLevel

activate

activate() will schedule execution at the specified time. If no time is specified, execution will be scheduled for the current simulation time. If you do not specify a process, the current process will be scheduled for continuation. If a process argument is provided, the process will be started (or restarted if it is equal to the currently active process).

Car() // default to process=Component::process or Component::repeatedProcess   
Car(process=Component::none) // no process, which effectivly makes the car DATA     

val car = Car(process=Car::driving) // start car in driving mode  

car1.activate(process=Car::refilling) //  stop driving (if still ongoing) and activate refilling process

car0.activate()  // activate defined process if set, otherwise error
  • If the component to be activated is DATA, unless provided with process the default Component::process will be scheduled at the specified time.
  • If the component to be activated is PASSIVE, the component will be activated at the specified time.
  • If the component to be activated is SCHEDULED, the component will get a new scheduled time.
  • If the component to be activated is REQUESTING, the request will be terminated, the attribute failed set, and the component will become scheduled. If keep_request=True is specified, only the fail_at will be updated, and the component will stay requesting.
  • If the component to be activated is WAITING, the wait will be terminated, the attribute failed set, and the component will become scheduled. If keepWait=true is specified, only the failAt will be updated, and the component will stay waiting.
  • If the component to be activated is STANDBY, the component will get a new scheduled time and become scheduled.
  • If the component is INTERRUPTED, the component will be activated at the specified time.

Important

It is not possible to activate() the CURRENT component without providing a process argument. kalasim will throw an error in this situation. The effect of a "self"-activate would be that the component becomes scheduled, thus this is essentially equivalent to the preferred hold method, so please use hold instead. The error is a safe-guard mechanism to prevent the user from unintentionally rescheduling the current component again.

In situations where the current process need to be restarted, we can use activate yield(activate(process = Component::process)) which will bypass the internal requirement that the activated component must not be CURRENT.

In situations where a user want's to run/consume an another process definition, without loosing the current process state, it is possible to yield all process steps with yieldAll() the subprocess:

import org.kalasim.*

createSimulation(true) {
    object : Component() {

        override fun process() =sequence {
            hold(1)
            // to consume the sub-process we use yieldAll
            yieldAll(subProcess())
            // it will continue here after the sub-process has been consumed
            hold(1)
        }

        fun subProcess(): Sequence<Component> = sequence {
            hold(1)
        }
    }

    run()
}

Although not very common, it is also possible to activate a component at a certain time or with a specified delay:

ship1.activate(at=100)
ship2.activate(delay=50)

Note

It is possible to use activate() outside of a process definition, e.g. to toggle processes after some time

sim.run(10)
car.activate(process=Car::repair)
sim.run(10)
However, in most situations this is better modelled within a process definition.

hold

Hold is the way to make a - usually current - component scheduled.

object : Component(){
    override fun process() = sequence {
        request(driver) {
            hold(1.0, description="some action that lasts 1 tick")
        }
    }
}
  • If the component is CURRENT, it will suspend execution internally, and the component becomes scheduled for the specified time
  • If the component to be held is passive, the component becomes scheduled for the specified time.
  • If the component to be held is scheduled, the component will be rescheduled for the specified time, thus essentially the same as activate.
  • If the component to be held is standby, the component becomes scheduled for the specified time.
  • If the component to be activated is requesting, the request will be terminated, the attribute failed set and the component will become scheduled. It is recommended to use the more versatile activate method.
  • If the component to be activated is waiting, the wait will be terminated, the attribute failed set and the component will become scheduled. It is recommended to use the more versatile activate method.
  • If the component is interrupted, the component will be activated at the specified time.

If a tick transformation is configured, it also allows to express run durations more naturally:

hold(2.hours)
hold(until= now + 3.hours )

passivate

Passivate is the way to make a - usually current - component passive. This is essentially the same as scheduling for time=inf.

  • If the component to be passivated is CURRENT, the component becomes passive, and it will suspend execution internally.
  • If the component to be passivated is passive, the component remains passive.
  • If the component to be passivated is scheduled, the component becomes passive.
  • If the component to be held is standby, the component becomes passive.
  • If the component to be activated is requesting, the request will be terminated, the attribute failed set and the component becomes passive. It is recommended to use the more versatile activate method.
  • If the component to be activated is waiting, the wait will be terminated, the attribute failed set and the component becomes passive. It is recommended to use the more versatile activate method.
  • If the component is interrupted, the component becomes passive.

cancel

Cancel has the effect that the component becomes a data component.

  • If the component to be cancelled is CURRENT, it will suspend execution internally.
  • If the component to be cancelled is passive, scheduled, interrupted or standby, the component becomes a data component.
  • If the component to be cancelled is requesting, the request will be terminated, the attribute failed set, and the component becomes a data component.
  • If the component to be cancelled is waiting, the wait will be terminated, the attribute failed set and the component becomes a data component.

Examples

standby

Standby has the effect that the component will be triggered on the next simulation event.

  • If the component is CURRENT, it will suspend execution internally
  • Although theoretically possible, it is not recommended to use standby for non current components. If needed to do so, the pattern to provide the correct receiver is with(nonCurrent){ standby() }
  • Not allowed for DATA components or main

Examples

request

Request has the effect that the component will check whether the requested quantity from a resource is available. It is possible to check for multiple availability of a certain quantity from several resources.

Instead of checking for all of number of resources, it is also possible to check for any of a number of resources, by setting the oneOf parameter to true.

By default, there is no limit on the time to wait for the resource(s) to become available. However, it is possible to set a time with failAt at which the condition has to be met. If that failed, the component becomes CURRENT at the given point of time. This is also known as reneging.

If the component is canceled, activated, passivated, interrupted or held the failed flag will be set as well.

  • If the component is CURRENT, it will suspend execution internally
  • Although theoretically possible it is not recommended to use request for non current components. If needed to do so, the pattern to provide the correct receiver is with(nonCurrent){ request(r) }

A component can also actively renege a pending request by calling release(resource). See Bank3ClerksRenegingResources for an example (as well as Bank3ClerksReneging Bank3ClerksRenegingState for other supported reneging modes).

wait

Wait has the effect that the component will check whether the value of a state meets a given condition. It is possible to check for multiple states. By default, there is no limit on the time to wait for the condition(s) to be met. However, it is possible to set a time with failAt at which the condition has to be met. If that failed, the component becomes CURRENT at the given point of time. The code should then check whether the wait had failed. That can be checked with the Component.failed property.

If the component is canceled, activated, passivated, interrupted or held the failed flag will be set as well.

  • If the component is CURRENT, it will suspend execution internally
  • Although theoretically possible it is not recommended to use wait for non current components. If needed to do so, the pattern to provide the correct receiver is with(nonCurrent){ wait() }

Examples

interrupt

With interrupt components that are not current or data can be temporarily be interrupted. Once a resume is called for the component, the component will continue (for scheduled with the remaining time, for waiting or requesting possibly with the remaining fail_at duration).

Examples

Usage of process interaction methods within a function or method

There is a way to put process interaction statement in another function or method. This requires a slightly different way than just calling the method.

As an example, let's assume that we want a method that holds a component for a number of minutes and that the time unit is actually seconds. So we need a method to wait 60 times the given parameter.

We start with a not so elegant solution:

object : Component() {
    override fun process() = sequence<Component>{
        hold(5.0)
        hold(5.0)
    }
}

Now we just add a method holdMinutes. Direct calling holdMinutes is not possible. Instead, we have to define an extension function on SequenceScope<Component>:

object : Component() {
    override fun process() = sequence {
        holdMinutes()
        holdMinutes()
    }

    private suspend fun SequenceScope<Component>.holdMinutes() {
        hold(5.0)
    }
}

All process interaction statements including passivate, request and wait can be used that way!

So remember if the method contains a yield statement (technically speaking iss a generator method), it should be called with from an extension function.

Component Generator

Since creation/generation of components is a very common element of most simulations, there is a dedicated utility called ComponentGenerator to do so

ComponentGenerator(iat = exponential(lambda, rg)) {
    Customer()
}

It requires 2 main parameters 1. a builder pattern 2. an inter-arrival distribution

See here for a complete listing of supported arguments.

Examples