Agents Balking, Reneging, and Jockeying in Queues
The third C tutorial moves from a queue length to active customers. Customers
can choose not to join a long queue, leave after losing patience, or switch to a
shorter queue. In Python, the same behavior is modeled with ordinary Python
objects plus cimba.Process instances.
Process-Like Model Objects
The C version derives struct visitor from cmb_process. Python does not
need that inheritance pattern. A visitor can be a normal class that stores a
reference to the process running it:
class Visitor:
def __init__(self, name: str, patience: float = 1.0):
self.name = name
self.patience = patience
self.entry_time_queue = 0.0
self.riding_time = 0.0
self.waiting_time = 0.0
self.num_attractions_visited = 0
self.status = "new"
def visitor_proc(me, ctx):
visitor = ctx["visitor"]
visitor.process = me
...
This gives the same modeling freedom: the process is an active coroutine while
running, and the visitor object can also be placed into a queue and handled as a
passive object by a server. Start this target with pass_process=True because
it stores the process object on the visitor.
Servers and Priority Queues
The C server gets visitors from a priority queue, clears their timers, runs the ride, and resumes them. The Python version is nearly the same:
def server(ctx):
while True:
sig, visitor = ctx["queue"].get()
assert sig == cimba.SUCCESS
visitor.process.timers_clear()
visitor.waiting_time += cimba.time() - visitor.entry_time_queue
cimba.hold(ctx["ride_duration"])
visitor.riding_time += ctx["ride_duration"]
visitor.process.resume(cimba.SUCCESS)
The resume does not directly run the target process inline. It schedules a wakeup through the dispatcher, preserving Cimba’s asymmetric coroutine model.
Setting and Clearing Timers
Jockeying and reneging are modeled with process timers. timer_set clears
existing timers and sets one new timer; timer_add adds another independent
timer:
TIMER_JOCKEYING = 17
TIMER_RENEGING = 42
me.timer_set(visitor.patience, TIMER_JOCKEYING)
me.timer_add(10.0 * visitor.patience, TIMER_RENEGING)
while True:
sig = cimba.yield_process()
if sig == TIMER_JOCKEYING:
...
elif sig == TIMER_RENEGING:
...
else:
assert sig == cimba.SUCCESS
...
When the visitor joins a cimba.PriorityQueue, put() returns a
handle. The handle is used to check the visitor’s current position, cancel the
queue entry when reneging, or move the visitor to another queue:
sig, handle = queue.put(visitor, priority=me.priority)
assert sig == cimba.SUCCESS
if new_queue.length < queue.position(handle):
assert queue.cancel(handle)
queue = new_queue
sig, handle = queue.put(visitor, priority=me.priority + 1)
assert sig == cimba.SUCCESS
The complete compact demonstration is in tutorial/tut_3_1.py:
import cimba
TIMER_JOCKEYING = 17
TIMER_RENEGING = 42
class Visitor:
def __init__(self, name: str, patience: float = 1.0):
self.name = name
self.patience = patience
self.entry_time_queue = 0.0
self.riding_time = 0.0
self.waiting_time = 0.0
self.num_attractions_visited = 0
self.status = "new"
def _server(ctx):
while True:
sig, visitor = ctx["queue"].get()
assert sig == cimba.SUCCESS
visitor.process.timers_clear()
visitor.waiting_time += cimba.time() - visitor.entry_time_queue
cimba.hold(ctx["ride_duration"])
visitor.riding_time += ctx["ride_duration"]
visitor.process.resume(cimba.SUCCESS)
def _visitor_proc(me, ctx):
visitor = ctx["visitor"]
visitor.process = me
queue = ctx["queues"][0]
visitor.entry_time_queue = cimba.time()
sig, handle = queue.put(visitor, priority=me.priority)
assert sig == cimba.SUCCESS
me.timer_set(visitor.patience, TIMER_JOCKEYING)
me.timer_add(10.0 * visitor.patience, TIMER_RENEGING)
while True:
sig = cimba.yield_process()
if sig == TIMER_JOCKEYING:
new_queue = ctx["queues"][1]
if new_queue.length < queue.position(handle):
assert queue.cancel(handle)
queue = new_queue
visitor.entry_time_queue = cimba.time()
sig, handle = queue.put(visitor, priority=me.priority + 1)
assert sig == cimba.SUCCESS
continue
if sig == TIMER_RENEGING:
assert queue.cancel(handle)
visitor.status = "reneged"
break
assert sig == cimba.SUCCESS
visitor.num_attractions_visited += 1
visitor.status = "served"
break
ctx["departed"].put(visitor)
def run_jockeying_demo() -> Visitor:
with cimba.Simulation(seed=31) as sim:
visitor = Visitor("Visitor_000001")
ctx = {
"visitor": visitor,
"queues": [cimba.PriorityQueue("Queue_00"), cimba.PriorityQueue("Queue_01")],
"departed": cimba.ObjectQueue("Departed"),
"ride_duration": 2.0,
}
cimba.Process("Server_01", _server, {"queue": ctx["queues"][1], "ride_duration": 2.0}).start()
cimba.Process(visitor.name, _visitor_proc, ctx, pass_process=True).start()
sim.execute()
sig, departed = ctx["departed"].get()
assert sig == cimba.SUCCESS
assert departed is visitor
return visitor
def run_reneging_demo() -> tuple[Visitor, int]:
def visitor_proc(me, ctx):
visitor = ctx["visitor"]
visitor.process = me
sig, handle = ctx["queue"].put(visitor, priority=0)
assert sig == cimba.SUCCESS
me.timer_set(visitor.patience, TIMER_RENEGING)
sig = cimba.yield_process()
assert sig == TIMER_RENEGING
assert ctx["queue"].cancel(handle)
visitor.status = "reneged"
ctx["departed"].put(visitor)
with cimba.Simulation(seed=32) as sim:
visitor = Visitor("Visitor_000002", patience=1.0)
ctx = {
"visitor": visitor,
"queue": cimba.PriorityQueue("Queue_00"),
"departed": cimba.ObjectQueue("Departed"),
}
cimba.Process(visitor.name, visitor_proc, ctx, pass_process=True).start()
sim.execute()
sig, departed = ctx["departed"].get()
assert sig == cimba.SUCCESS
assert departed is visitor
return visitor, ctx["queue"].length
def main() -> None:
visitor = run_jockeying_demo()
print(visitor.status, visitor.num_attractions_visited)
visitor, queue_length = run_reneging_demo()
print(visitor.status, queue_length)
if __name__ == "__main__":
main()
Alias Sampling Probabilities
The full C amusement-park tutorial uses Vose alias sampling to choose a next
attraction from transition probabilities. Python exposes the same native idea as
cimba.AliasSampler:
with cimba.AliasSampler([0.1, 0.7, 0.2]) as quo_vadis:
next_attraction = quo_vadis.sample()
For occasional one-shot draws, use cimba.loaded_dice():
next_attraction = cimba.loaded_dice([0.1, 0.7, 0.2])
A Day in the Park
The C tutorial builds a complete amusement park with multiple attractions,
queues, servers, walking times, balking thresholds, and detailed statistics.
The Python API has the pieces needed for the same model: processes, priority
queues, timers, object queues, datasets, cimba.pert() random variates,
and alias sampling. The checked-in Python tutorial intentionally keeps the
model small so the queue/timer/resume mechanics remain visible.
For the full model, collect visitor metrics in cimba.DataSummary
objects and queue histories with cimba.reporting.resource_report(), matching
the C tutorial’s summary lines and detailed queue reports:
num_rides = cimba.DataSummary()
time_in_park = cimba.DataSummary()
# In the departure process:
num_rides.add(visitor.num_attractions_visited)
time_in_park.add(cimba.time() - visitor.arrival_time)
print(cimba.reporting.summarize(num_rides))
print(cimba.reporting.format_report(cimba.reporting.resource_report(queue)))
Parallelizing the full park model would follow the same pattern as
A Simple M/M/1 Queue Parallelized: write one function that builds and runs a complete park day,
return plain Python metrics, and call cimba.run_experiment().