Skip to content

The Ferryman

A wild river, one boat only, and a patient ferryman transporting batches of passengers across the body of water.

Covers:

  • Batching to consume queue elements in defined blocks
  • Monitors for stats and visualization

Stanhope Forbes A Ferryman at Flushing

Stanhope Forbes - A Ferryman at Flushing (oil on canvas, CC0 1.0)

Simulation

To form groups of passengers before passing the waters, we use batch() in the ferryman's process definition. It has multiple arguments:

  • A mandatory queue with elements of type <T> to be consumed
  • The size of the batch to be created. A positive integer is expected here.
  • An optional timeout describing how long it shall wait before forming an incomplete/empty batch

batch will return a list of type <T> of size batchSize or lesser (and potentially even empty) if timed out before filling the batch.

////Ferryman.kts
package org.kalasim.examples

import org.kalasim.*
import org.kalasim.monitors.NumericStatisticMonitor
import org.kalasim.plot.kravis.display
import kotlin.time.Duration.Companion.minutes

createSimulation {

    class Passenger : Component()

    val fm = object : Component("ferryman") {
        val left2Right = ComponentQueue<Passenger>()
        val right2Left = ComponentQueue<Passenger>()

        val l2rMonitor = NumericStatisticMonitor()
        val r2lMonitor = NumericStatisticMonitor()

        override fun process() = sequence {
            val batchLR: List<Passenger> = batch(left2Right, 4, timeout = 10.minutes)
            l2rMonitor.addValue(batchLR.size)
            hold(5.minutes, description = "shipping ${batchLR.size} l2r")

            val batchRL: List<Passenger> = batch(right2Left, 4, timeout = 10.minutes)
            r2lMonitor.addValue(batchRL.size)
            hold(5.minutes, description = "shipping ${batchRL.size} r2l")

            // we could also use an infinite while loop instead of activate
            activate(process = Component::process)
        }
    }

    ComponentGenerator(uniform(0, 15)) { Passenger() }
        .addConsumer { fm.left2Right.add(it) }

    ComponentGenerator(uniform(0, 12)) { Passenger() }
        .addConsumer { fm.right2Left.add(it) }

    run(10000.minutes)

    fm.l2rMonitor.display("Passengers left->right")
    fm.r2lMonitor.display("Passengers right->left")
}

Analysis

The ferryman tries to max out his boat with 4 passengers, but after 10 minutes he will start anyway (even if the boat is entirely emtpy). kalasim will suspend execution when using batch() until timeout or indefinitely (if timeout is not set).

Since both banks have different arrival distributions, we observe different batch-size patterns:

Right→Left

Since passengers on the right bank arrive with a higher rate (that is shorter inter-arrival time between 0 and 12), the ferry is usually packed with people. Only occasionally the ferryman traverses from left to right banks with less than 4 passengers.

Left→Right

Because of a slightly higher inter-arrival time (up to 15 minutes) on the left banks, it often happens that the ferry starts its journey across the river with some seats unoccupied. On average, just 3 seats are taken. However, at least during this simulation we did not encounter a passing with just the ferryman and his thoughts.