e-neko aka e-cat of IRC told me that buses come in pairs for some reason. I did not believe him. I still find it hard to believe. It looks so strange.

Figured made into a mooovie:

https://kaka.farm/pub/images/2025-12-06-buses-simulation/bus.mp4

Simulation code:

https://kaka.farm/pub/images/2025-12-06-buses-simulation/waiting-for-the-bus.scm

Jupyter notebook on:

https://kaka.farm/pub/images/2025-12-06-buses-simulation/bus.ipynb

(import
 (srfi srfi-1)
 (srfi srfi-9)

 (ice-9 match)
 )

(define *bus-capacity* 50)
(define *bus-departure-interval* 100)
(define *bus-route-length* 10000)
(define *bus-speed* 10)
(define *number-of-stations* 100)
(define *station-fill-rate* 1)
(define *time-passenger-on* 1)
(define *time-passenger-off* 1)

(define-record-type <event>
  (make-event time data)
  event?
  (time event-time)
  (data event-data))

(define-record-type <bus-departure>
  (make-bus-departure)
  bus-departure?)

(define-record-type <bus-at-station>
  (make-bus-at-station station-number number-of-passengers)
  bus-at-station?
  (station-number bus-at-station-station-number)
  (number-of-passengers bus-at-station-number-of-passsangers))

(define-record-type <passenger-waiting>
  (make-passenger-waiting station-number)
  passenger-waiting?
  (station-number passenger-waiting-station-number))

(define-record-type <world>
  (make-world events stations)
  world?
  (events world-events)
  (stations world-stations))

(define (initialise-world)
  (make-world '()
              (make-vector *number-of-stations* 0)))

(define (sort-event-queue event-queue)
  (sort event-queue
        (lambda (event-a event-b)
          (< (event-time event-a)
             (event-time event-b)))))

(define (tick world)
  (match (world-events world)
    ['() (make-world (list (make-event 0 (make-bus-departure))
                           (make-event 0 (make-passenger-waiting (random *number-of-stations*))))
                     (world-stations world))]
    [(and events (head . rest))
     (let* ([sorted-event-queue (sort-event-queue events)]
            [earliest-event (car sorted-event-queue)]
            [rest-of-events (cdr sorted-event-queue)])
       (match earliest-event
         [($ <event> time data)
          (match data
            [($ <passenger-waiting> station-number)
             (let ([new-events (list (make-event (+ time (* (random:uniform)
                                                            *station-fill-rate*))
                                                 (make-passenger-waiting (random *number-of-stations*))))]
                   [stations (world-stations world)])
               (vector-set! stations station-number (1+ (vector-ref stations station-number)))
               (make-world (append new-events rest-of-events)
                           stations))]
            [($ <bus-departure>)
             (let ([new-events (list (make-event (+ time *bus-departure-interval*)
                                                 (make-bus-departure))
                                     (make-event (+ time (/ *bus-route-length* *number-of-stations*))
                                                 (make-bus-at-station 1 0)))])
               (make-world (append new-events rest-of-events)
                           (world-stations world)))]
            [($ <bus-at-station> station-number number-of-passengers)
             (format #t "~A,~A~%" time station-number)
             (cond
              [(= station-number *number-of-stations*)
               (make-world rest-of-events (world-stations world))]
              [else
               (let* ([stations (world-stations world)]
                      [waiting-at-station (vector-ref stations station-number)]
                      [passengers-off (round (* number-of-passengers (random:uniform)))]
                      [number-of-passengers-after-off (- number-of-passengers passengers-off)]
                      [free-seats-after-off (- *bus-capacity* number-of-passengers-after-off)]
                      [passengers-on (min free-seats-after-off waiting-at-station)]
                      [number-of-passengers-after-off-on (+ number-of-passengers-after-off passengers-on)]
                      [time-bus-waiting-in-station (+ (* passengers-off *time-passenger-off*)
                                                      (* passengers-on *time-passenger-on*))]
                      [next-time (+ time
                                    time-bus-waiting-in-station
                                    (/ *bus-route-length* *number-of-stations*))]
                      [new-events (list (make-event next-time
                                                    (make-bus-at-station (1+ station-number)
                                                                         number-of-passengers-after-off-on)))])
                 (vector-set! stations station-number (- (vector-ref stations station-number)
                                                         passengers-on))
                 (make-world (append new-events rest-of-events)
                             stations))])]
            [($ <event> time 'bus-final-arrival data)
             (make-world rest-of-events
                         (world-stations world))])]))]))

(let loop ([world (tick (make-world '() (make-vector *number-of-stations* 0)))]
           [n 0])
  (cond
   [(= n 100000)
    '()]
   [else
    (loop (tick world)
          (1+ n))]))

Jupyter notebook source, sorta:

from matplotlib import pyplot as plt
import numpy as np

s = open('data-file.csv', 'r').read()

l = [[float(n.split(',')[0]), int(n.split(',')[1])] for n in s.split("\n")]

def f(station_number):
    last = [n for n in l if n[1] == station_number]
    last = [n_1[0] - n_0[0] for n_0, n_1 in zip(last, last[1:])]
    fig, ax = plt.subplots(1, 1)
    ax.set_title(f'bus station #{station_number:03}')
    ax.set_xlabel('time difference between two consecutive buses')
    ax.set_ylabel('number of busess')
    ax.hist(last, bins=100)
    fig.savefig(f'bus-{station_number:03}.png')

for range(1, 101):
    f(n)
  • BartyDeCanter@lemmy.sdf.org
    link
    fedilink
    arrow-up
    7
    ·
    1 month ago

    I read a paper a veeerrryyy long time ago that discussed this topic. What I remember from it is that yes, this absolutely happens and no, there isn’t a very good fix. I don’t remember all of the details, but IIRC one bus gets delayed until the second bus catches up and then they are stuck together until shift changes happen.

    Speculating, what I think happens is this: Imagine a simple circular bus route with evenly spaced stops and two busses on it. The busses start at opposite sides of the circle at the beginning of a shift. Each bus is going to be hit with random delays from time to time. Now, if the distribution of the delays is evenly random, then the spacing between them will be a random walk that tends towards them staying the same distance apart. But I suspect that the distribution of delays skews both both toward a dragon-king distribution and toward one bus.

    A typical skewed delay could look like an unruly passenger. There aren’t too many of them, but enough that one bus will get one in a shift. The buss is first delayed when they get on, then maybe again before they get to their stop, as the driver gives them a warning, then a third time as they get off or are kicked off.

    Once one bus is delayed it will have more passengers, since more time will have passed since the non-delayed bus came by, which will end up delaying it even more until the non-delayed bus catches up with it.

    All of the possible fixes I can think of either cost $ or reduce service.

    1. Add more slack time between stops, so that most of the time a bus can wait for a bit at each stop. This could work, but reduces capacity.
    2. When one bus catches the other, have stop taking passengers and jump to the other schedule. If the 2nd bus passes the first it’s going to have the extra passenger issue, so the two buses will just start playing leapfrog. This doesn’t fix anything and possibly just makes it worse. If the second bus hangs back until the first is far enough ahead, it just lost capacity.
    3. Have a spare bus that can jump onto the first busses schedule until it can empty and catch up. That requires a spare bus and operator on standby, which is expensive.
    • 🇮🇱🦬@lemmy.sdf.orgOP
      link
      fedilink
      arrow-up
      8
      ·
      1 month ago

      Holy hell! Thank you! I am too tired to understand anything, but when I wake up and read it I will still be too dumb to understand anything. I will try and read it anyway. Thank you!

      • BartyDeCanter@lemmy.sdf.org
        link
        fedilink
        arrow-up
        8
        ·
        1 month ago

        Thanks! I suspect this is not too difficult to Monte Carlo model, with different routes, number of busses and delay distribution curves, possible adaptations, and impact on costs.