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:

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>
}