Back to Claude Scientific Skills

Events, environments, and scheduling

skills/simpy/references/events.md

2.55.07.1 KB
Original Source

Events, environments, and scheduling

Verified 2026-07-23 against SimPy 4.1.2 documentation and tagged source.

Scheduler model

Environment owns a single event queue and processes one event at a time. Tagged 4.1.2 stores queue entries as (time, priority, event_id, event):

  1. smallest simulation time first;
  2. smallest numeric priority first (URGENT=0, NORMAL=1 in public event APIs);
  3. smallest strictly increasing event ID first.

Thus same-time, same-priority events are FIFO by scheduling order. This is deterministic sequential execution, even when model processes represent concurrent activities. Floating-point discretization can collapse physically distinct times onto the same value, so test tie behavior explicitly.

Event lifecycle

An Event moves once through:

  1. not triggered — not scheduled and no value;
  2. triggered — outcome/value fixed and scheduled; event.triggered is true;
  3. processed — removed for callback execution; event.processed is true when event.callbacks is None.
python
import simpy

env = simpy.Environment()
event = env.event()
assert not event.triggered and not event.processed

event.succeed("ready")
assert event.triggered and not event.processed

env.step()
assert event.processed and event.value == "ready"

event.succeed(value) and event.fail(exception) return that event and may be called only once. In 4.1.2 fail() requires an Exception. event.trigger(other) copies the other event's success/failure and value, and returns None.

A failed event throws its exception into a waiting process. If no process or callback defuses it, Environment.step() raises it. Treat private _ok, _value, and _defused as implementation details.

Callbacks

Before processing, event.callbacks is a mutable list of one-argument callables. Yielding an event adds the waiting process's resume method. Processing executes callbacks in list order. Once fully processed, callbacks becomes None; appending then is invalid.

python
log = []
timeout = env.timeout(2, value=7)
timeout.callbacks.append(lambda completed: log.append(completed.value))
env.run()
assert log == [7]

Keep callbacks short and non-blocking. A callback runs synchronously inside Environment.step() and can affect scheduler latency.

Timeout

env.timeout(delay, value=None) creates a Timeout, immediately triggers it, and schedules it at env.now + delay. Because it is already triggered at construction, do not call succeed() or fail() on it.

python
def timer(env):
    result = yield env.timeout(3, value="elapsed")
    assert result == "elapsed"

Reject negative delay. Use a consistent numeric time unit; SimPy does not attach units or protect against incompatible scales.

Process

env.process(generator) requires a generator object, not an ordinary function result. It schedules an urgent Initialize event. Each yielded event suspends the generator; after the event's outcome, SimPy sends its value back or throws its failure into the generator.

python
def child(env):
    yield env.timeout(1)
    return 42

def parent(env):
    child_process = env.process(child(env))
    result = yield child_process
    assert result == 42

env = simpy.Environment()
env.process(parent(env))
env.run()

A Process is itself an event. It succeeds with the generator's return value or fails with an uncaught exception. process.is_alive, process.target, and process.name expose its current status.

Common mistakes:

  • env.process(worker) instead of env.process(worker(env));
  • a function without any reachable yield, which is not a generator;
  • performing blocking I/O or time.sleep() inside a normal Environment process;
  • yielding a number rather than an Event.

Condition events

AnyOf(env, events) / a | b and AllOf(env, events) / a & b return a Condition. Yielding one produces a ConditionValue, an ordered dict-like mapping whose keys are the original Event objects and whose values are their values.

python
def coordinate(env):
    fast = env.timeout(1, value="fast")
    slow = env.timeout(2, value="slow")

    first = yield fast | slow
    assert fast in first
    assert first[fast] == "fast"

    both = yield fast & slow
    assert list(both.items()) == [(fast, "fast"), (slow, "slow")]

For AllOf, all input events appear. For AnyOf, all target events that occurred before the condition itself is processed can appear; do not assume exactly one winner when events tie. Input order determines result order.

If any input event fails before the condition succeeds, AnyOf and AllOf fail. Conditions can be nested. AnyOf does not cancel losing events:

python
with resource.request() as request:
    patience = env.timeout(5)
    result = yield request | patience
    if request not in result:
        # Context-manager exit cancels the still-pending request.
        return
    yield env.timeout(2)

An ordinary losing timeout remains scheduled. This is usually harmless but matters for event counts and traces.

Environment.run() boundaries

No criterion

env.run() stops only when the event queue is empty. An endless generator such as while True: yield env.timeout(1) makes it nonterminating.

Numeric criterion

env.run(until=10) creates an internal urgent stop event at time 10. It advances env.now to 10 but does not process ordinary events scheduled exactly at 10:

python
env = simpy.Environment()
boundary = env.timeout(10)
env.run(until=10)
assert env.now == 10
assert not boundary.processed

The numeric target must be strictly greater than env.now.

Event criterion

env.run(until=event) attaches a stop callback and returns the event value when that callback fires. It raises RuntimeError if the schedule empties before the criterion is triggered.

Important 4.1.2 implementation detail: Environment.step() catches StopSimulation, preserves callbacks after the stopping callback, and reschedules the event at priority -1. Since processed means callbacks is None, the target can still report processed == False immediately after run() returns:

python
env = simpy.Environment()
target = env.timeout(5, value="done")
assert env.run(until=target) == "done"
assert target.triggered
assert not target.processed
env.step()
assert target.processed

This behavior follows tagged 4.1.2 simpy/core.py; the topical guide's informal "processed" wording is not a safe postcondition. Depend on the returned value and your model state, not solely on processed.

A timeout event and a numeric time can reach the same clock value but differ in same-time ordering. Numeric stopping is the clearer half-open horizon.

Manual stepping

python
until = 10
steps = 0
max_steps = 100_000
while env.peek() < until and steps < max_steps:
    env.step()
    steps += 1
if steps == max_steps:
    raise RuntimeError("event budget reached")

peek() returns infinity when empty; step() raises simpy.core.EmptySchedule when no event remains. Explicit step/event caps prevent a zero-delay loop from hanging diagnostics.

Sources

See sources.md for versioned environment/event guides, API references, tagged core.py, and the scheduling guide.