Node Types

There are many kinds of nodes in SimPort, here are examples of each of them:

Arrivals

Objects are generated at given intervals:

val pushOutput = arrivals("Truck Arrivals", Generators.constant(::Truck, Delays.exponentialWithMean(1.hours)))

Sinks

Objects “leave” the network:

input
    .thenSink("Truck Departures")

We also allow a provide loss sinks for objects that disappear from the network rather than leaving. This is useful for lossy arrials or after a fork node in some cases.

input
    .thenLossSink("Trucks fall into the sea")

Delays

Objects coming in are emitted after a given delay:

val pushOutput =
    input
        .thenDelay("Road", Delays.exponentialWithMean(10.minutes))

Services

Objects are delayed but can only be served 1 at a time:

val pushOutput =
    input
        .thenService("Passport Check", Delays.exponentialWithMean(10.minutes))

Forks

Objects are forwarded to one of multiple outputs:

// Push forks choose their destination via a policy, random by default
val pushOutputs =
    pushInput
        .thenFork("Truck Split", numLanes = 5, policy = forkPolicy(RandomPolicy())) { i, lane ->
            lane.thenDelay("Road $i", Delays.fixed(i.seconds))
        }
// Instead of a number of lanes, you can also provide a list of lanes, each of which is a lambda to build the lane
val pushOutputs =
    pushInput
        .thenFork(
            "Truck Split",
            lanes = listOf(
                { it.thenService("Short", Delays.fixed(1.minutes)) },
                { it.thenService("Long", Delays.fixed(10.minutes)) }
            ),
            policy = ...
)

// Pull forks instead forward objects as they are requested by lanes, and as such have no policy
// In this example, each service will pull from the input when it is ready
val pullOutputs =
    pullInput
        .thenFork("Truck Split", numLanes = 5) { i, lane ->
            lane.thenService("Service $i", Delays.fixed(i.seconds))
        }

Joins

Objects are combined back into a single output:

// Push joins simply accept items as they arrive and as such have no policy
val pushOutput =
    pushInputs
        .thenJoin("Truck Join")

// Pull joins choose from the available inputs when their downstream requests an item, and as such do have a policy, 
// random by default
val pullOutput =
    pullInputs
        .thenJoin("Truck Join", policy = joinPolicy(RandomPolicy()))

Matches

Objects are combined as per some function when both ready:

val output =
    // The main input can be either a push or a pull, and the output will match
    input
        .thenMatch(
            "Container Mount",
            // The side input must be a pull
            pullInput
        ) { vehicle, container ->
            // Somehow produce a new object from the two objects
            VehicleWithContainer(vehicle, container)
        }

Splits

Objects are split into 2 outputs as per some function:

val (output, pushOutput) =
    // The main input can be either a push or a pull, and the output will match
    input
        .thenSplit("Container Unmount") { vehicleWithContainer ->
            // Somehow produce a Pair of objects from the object
            Pair(vehicleWithContainer.vehicle, vehicleWithContainer.container)
        }

Queues

Objects are queued and can be extracted by the downstream in an order specified by the policy (FIFO by default):

val pullOutput =
    pushInput
        .thenQueue("Queue", policy = FIFOQueuePolicy())

Subnetworks

Subnetworks have a fixed capacity and are an example of a compound node, which you can make yourself (LINK???). Internally, they use a queue of Tokens, along with Match and Split nodes, to implement their behaviour.

val output =
    input
        .thenSubnetwork("Car Park", capacity = 10) { inner ->
            // Only 10 trucks can be in the park at once
            inner
                .thenQueue("Leaving Queue")
                .thenService("Ticket Gate", Delays.exponentialWithMean(10.seconds))
        }

Connections

It is often desirable to make ports which involve cycles, e.g. when a node can’t be serviced it might go back round. This can be achieved using Connections:

fun cyclicPort() = buildScenario {
    // Make a deferred push connection, equivalent concepts exist for pull channels
    val backEdge = newPushConnection<Truck>()

    val arrivals = arrivals("Truck Arrivals", Generators.constant(::Truck, Delays.exponentialWithMean(10.minutes)))

    listOf(arrivals, backEdge)
        // Merge the new arrivals and the looping trucks
        .thenJoin("Trucks Merge")
        .thenDelay("Road", Delays.fixed(5.minutes))
        // After the road, send trucks back round
        .thenConnect(backEdge)
}

This gives us the following basic layout:

cyclic-port.png

Helpers

Slightly simpler than compound nodes, you can make regular helper functions to extract new components:

// Here, we define that we accept either a pull or push input of a generic item
// type, and we will give back a Push output of the same type.
fun <T> NodeBuilder<T, *>.thenCarPark(capacity: Int): NodeBuilder<T, ChannelType.Push> =
    this
        .thenSubnetwork("Car Park", capacity = capacity) { inner ->
            // Only 10 trucks can be in the park at once
            inner
                .thenQueue("Leaving Queue")
                .thenService("Ticket Gate", Delays.exponentialWithMean(10.seconds))
        }

which can be used as normal:

val pushOutput = 
    pushInput
        .thenCarPark(capacity = 10)

The NodeBuilder interface that is passed throughout the DSL functions takes the types:

// ChannelT representing the type of the channel connecting from the previous node
sealed interface NodeBuilder<out ItemT, ChannelT : ChannelType<ChannelT>>

sealed interface ChannelType<SelfT : ChannelType<SelfT>> {
    data object Push : ChannelType<Push>
    data object Pull : ChannelType<Pull>
}

This site uses Just the Docs, a documentation theme for Jekyll.