Skill Tracer
Skills › Scientific Agent Skills › simpy › events.md

events.md

231 lines · skills/simpy/references/events.md at 5f2bc03

Open on GitHub
1# Events, environments, and scheduling
2
3Reviewed 2026-10-01 against SimPy 4.1.2 documentation and tagged source.
4

Events, environments, and scheduling

Reviewed 2026-10-01 against SimPy 4.1.2 documentation and tagged source.

Raw lines 1 to 4
5## Scheduler model
6
7`Environment` owns a single event queue and processes one event at a time. Tagged
84.1.2 stores queue entries as `(time, priority, event_id, event)`:
9
101. smallest simulation time first;
112. smallest numeric priority first (`URGENT=0`, `NORMAL=1` in public event APIs);
123. smallest strictly increasing event ID first.
14Thus same-time, same-priority events are FIFO by scheduling order. This is
15deterministic sequential execution, even when model processes represent concurrent
16activities. Floating-point discretization can collapse physically distinct times
17onto the same value, so test tie behavior explicitly.
19The tuple order selects from the queue **at each step**. A running callback can
20schedule an urgent `Initialize` or `Interruption` at the same time; priority can
21therefore decrease in a valid processing trace. A trace without insertion history
22cannot validate the scheduler by sorting all processed tuples afterward.

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.

The tuple order selects from the queue at each step. A running callback can schedule an urgent Initialize or Interruption at the same time; priority can therefore decrease in a valid processing trace. A trace without insertion history cannot validate the scheduler by sorting all processed tuples afterward.

Raw lines 5 to 23
24## Event lifecycle
26An `Event` moves once through:
281. **not triggered** — not scheduled and no value;
292. **triggered** — outcome/value fixed and scheduled; `event.triggered` is true;
303. **processed** — removed for callback execution; `event.processed` is true when
31 `event.callbacks is None`.
33```python
34import simpy
36env = simpy.Environment()
37event = env.event()
38assert not event.triggered and not event.processed
40event.succeed("ready")
41assert event.triggered and not event.processed
43env.step()
44assert event.processed and event.value == "ready"
45```
47`event.succeed(value)` and `event.fail(exception)` return that event and may be
48called only once. Use an `Exception` for `fail()`; the 4.1.2 implementation accepts
49`BaseException` despite the narrower API annotation. `event.trigger(other)`
50copies the other event's success/failure and value, and returns `None`.
52A failed event throws its exception into a waiting process. If no process or
53callback defuses it, `Environment.step()` raises it. Treat private `_ok`, `_value`,
54and `_defused` as implementation details.

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.
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. Use an Exception for fail(); the 4.1.2 implementation accepts BaseException despite the narrower API annotation. 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.

Raw lines 24 to 55
56## Callbacks
58Before processing, `event.callbacks` is a mutable list of one-argument callables.
59Yielding an event adds the waiting process's resume method. Processing executes
60callbacks in list order. Once fully processed, callbacks becomes `None`; appending
61then is invalid.
63```python
64log = []
65timeout = env.timeout(2, value=7)
66timeout.callbacks.append(lambda completed: log.append(completed.value))
67env.run()
68assert log == [7]
69```
71Keep callbacks short and non-blocking. A callback runs synchronously inside
72`Environment.step()` and can affect scheduler latency.

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.

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.

Raw lines 56 to 73
74## Timeout
76`env.timeout(delay, value=None)` creates a `Timeout`, immediately triggers it, and
77schedules it at `env.now + delay`. Because it is already triggered at construction,
78do not call `succeed()` or `fail()` on it.
80```python
81def timer(env):
82 result = yield env.timeout(3, value="elapsed")
83 assert result == "elapsed"
84```
86Reject negative delay. Use a consistent numeric time unit; SimPy does not attach
87units or protect against incompatible scales.

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.

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.

Raw lines 74 to 88
89## Process
91`env.process(generator)` requires a **generator object**, not an ordinary function
92result. It schedules an urgent `Initialize` event. Each yielded event suspends the
93generator; after the event's outcome, SimPy sends its value back or throws its
94failure into the generator.
96```python
97def child(env):
98 yield env.timeout(1)
99 return 42
101def parent(env):
102 child_process = env.process(child(env))
103 result = yield child_process
104 assert result == 42
106env = simpy.Environment()
107env.process(parent(env))
108env.run()
109```
111A `Process` is itself an event. It succeeds with the generator's return value or
112fails with an uncaught exception. `process.is_alive`, `process.target`, and
113`process.name` expose its current status.
115Common mistakes:
117- `env.process(worker)` instead of `env.process(worker(env))`;
118- a function without any reachable `yield`, which is not a generator;
119- performing blocking I/O or `time.sleep()` inside a normal Environment process;
120- yielding a number rather than an Event.

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.

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.
Raw lines 89 to 121
122## Condition events
124`AnyOf(env, events)` / `a | b` and `AllOf(env, events)` / `a & b` return a
125`Condition`. Yielding one produces a `ConditionValue`, an ordered dict-like mapping
126whose keys are the original Event objects and whose values are their values.
128```python
129def coordinate(env):
130 fast = env.timeout(1, value="fast")
131 slow = env.timeout(2, value="slow")
133 first = yield fast | slow
134 assert fast in first
135 assert first[fast] == "fast"
137 both = yield fast & slow
138 assert list(both.items()) == [(fast, "fast"), (slow, "slow")]
139```
141For `AllOf`, all input events appear. For `AnyOf`, all target events that occurred
142before the condition itself is processed can appear; do not assume exactly one
143winner when events tie. Input order determines result order.
145If any input event fails before the condition succeeds, `AnyOf` and `AllOf` fail.
146Conditions can be nested. `AnyOf` does **not** cancel losing events:
148```python
149with resource.request() as request:
150 patience = env.timeout(5)
151 result = yield request | patience
152 if request not in result:
153 # Context-manager exit cancels the still-pending request.
154 return
155 yield env.timeout(2)
156```
158An ordinary losing timeout remains scheduled. This is usually harmless but matters
159for event counts and traces.

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.

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:

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.

Raw lines 122 to 160
161## `Environment.run()` boundaries

Environment.run() boundaries

Raw lines 161 to 162
163### No criterion
165`env.run()` stops only when the event queue is empty. An endless generator such as
166`while True: yield env.timeout(1)` makes it nonterminating.

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.

Raw lines 163 to 167
168### Numeric criterion
170`env.run(until=10)` creates an internal urgent stop event at time 10. It advances
171`env.now` to 10 but does **not** process ordinary events scheduled exactly at 10:
173```python
174env = simpy.Environment()
175boundary = env.timeout(10)
176env.run(until=10)
177assert env.now == 10
178assert not boundary.processed
179```
181The numeric target must be strictly greater than `env.now`.

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:

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.

Raw lines 168 to 182
183### Event criterion
185`env.run(until=event)` attaches a stop callback and returns the event value when
186that callback fires. It raises `RuntimeError` if the schedule empties before the
187criterion is triggered.
189Important 4.1.2 implementation detail: `Environment.step()` catches
190`StopSimulation`, preserves callbacks after the stopping callback, and reschedules
191the event at priority `-1`. Since `processed` means callbacks is `None`, the target
192can still report `processed == False` immediately after `run()` returns:
194```python
195env = simpy.Environment()
196target = env.timeout(5, value="done")
197assert env.run(until=target) == "done"
198assert target.triggered
199assert not target.processed
200env.step()
201assert target.processed
202```
204This behavior follows tagged 4.1.2 `simpy/core.py`; the topical guide's informal
205"processed" wording is not a safe postcondition. Depend on the returned value and
206your model state, not solely on `processed`.
208A timeout event and a numeric time can reach the same clock value but differ in
209same-time ordering. Numeric stopping is the clearer half-open horizon.

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:

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.

Raw lines 183 to 210
211## Manual stepping
213```python
214until = 10
215steps = 0
216max_steps = 100_000
217while env.peek() < until and steps < max_steps:
218 env.step()
219 steps += 1
220if steps == max_steps and env.peek() < until:
221 raise RuntimeError("event budget reached")
222```
224`peek()` returns infinity when empty; `step()` raises `simpy.core.EmptySchedule`
225when no event remains. Explicit step/event caps prevent a zero-delay loop from
226hanging diagnostics.

Manual stepping

until = 10
steps = 0
max_steps = 100_000
while env.peek() < until and steps < max_steps:
    env.step()
    steps += 1
if steps == max_steps and env.peek() < until:
    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.

Raw lines 211 to 227
228## Sources
230See `sources.md` for versioned environment/event guides, API references, tagged
231`core.py`, and the scheduling guide.

Sources

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

Raw lines 228 to 231