Chapter 2: Core Design Concepts¶
2.1 The Stack¶
DSSim is organized in six layers. Each layer builds on the one below it and can be used independently.
flowchart TB
subgraph L0["Layer 0 — Time Queue"]
direction LR
TQ1["TQBinTree (default) — heap + buckets · bound-heavy workloads"] ~~~ TQ2["TQBisect — sorted deque · clean forward-event stream"]
end
subgraph L1["Layer 1 — Simulation Engine"]
direction LR
E["DSSimulation — event dispatch · process context · time advancement"]
end
subgraph L2L["Layer 2 — LiteLayer2"]
direction LR
LP["DSLiteProcess"] ~~~ LPB["DSLitePub · DSLiteSub"]
end
subgraph L2P["Layer 2 — PubSubLayer2"]
direction LR
PB["DSPub · DSSub (routing-rich)"] ~~~ DP["DSProcess · DSFuture"] ~~~ Fi["Filters · Circuits"]
end
subgraph L3L["Layer 3 — Lite Components"]
direction LR
LQ["DSLiteQueue"] ~~~ LRes["DSLiteResource"] ~~~ LTi["DSLiteTimer · DSLiteDelay"]
end
subgraph L3P["Layer 3 — PubSub Components"]
direction LR
Q["DSQueue"] ~~~ R["DSResource"] ~~~ Co["DSContainer"] ~~~ Ti["DSTimer"] ~~~ St["DSState"] ~~~ HW["HW models"]
end
subgraph L4L["Layer 4 — Lite Agent"]
direction LR
LA["DSLiteAgent"]
end
subgraph L4P["Layer 4 — PubSub Agent"]
direction LR
AG["DSAgent — self-driving components with queue / resource helpers"]
end
subgraph L5["Layer 5 — Shim Layers (PubSub only)"]
direction LR
SH["SimPy · salabim · asyncio compatibility shims"]
end
L0 --> L1
L1 --> L2L
L1 --> L2P
L2L --> L3L --> L4L
L2P --> L3P --> L4P --> L5
Layer 0 — Time Queue¶
The time queue is the engine's memory. Every scheduled event is stored as a (time, subscriber, event_object) tuple in a sorted structure. When the simulation runs, it repeatedly pops the earliest bucket and delivers the events inside it.
- Insert: O(log n) search into a sorted list of time buckets.
- Pop: O(1) removal of the front bucket.
- Zero-time events bypass the sorted structure and are always drained before advancing time.
Nothing at this layer knows about processes, publishers, or routing. It is a pure scheduling primitive.
DSSim ships two Layer 0 implementations, selectable at construction time via the timequeue= parameter:
from dssim import DSSimulation, TQBinTree, TQBisect
sim = DSSimulation() # TQBinTree — default
sim = DSSimulation(timequeue=TQBisect) # TQBisect
TQBinTree (default) uses a min-heap of timestamps plus one FIFO bucket per timestamp. It handles arbitrary insertion patterns efficiently and degrades gracefully when many distinct future timestamps are in flight simultaneously — for example, when resources, timeouts, and process wake-ups are all competing. It is the safer general-purpose choice.
TQBisect uses a sorted deque with a bisect-based insertion. It has lower per-operation overhead on clean insertion patterns where new events tend to land near the end of the queue (close to or beyond the current horizon). It is the better choice for models with a regular, forward-moving event stream and few cancellations or mid-queue insertions.
Layer 1 — Simulation Engine¶
DSSimulation sits on top of the time queue and provides the main event loop. Its responsibilities are:
- Advancing simulation time to the next event.
- Dispatching the event to the right subscriber via
send_object(). - Tracking which process is currently running (
pid), so that wait registrations are attributed to the right owner. - Providing
schedule_event(),signal(), andcleanup()as the fundamental building blocks that all higher layers use.
The engine itself is unaware of pubsub routing or conditions. It only knows how to move time forward and call send(event) on a subscriber.
Layer 2 — Event Routing Profiles¶
Layer 2 is where the simulation's programming model is defined. DSSim ships two Layer 2 profiles, selectable at construction time:
sim = DSSimulation() # PubSubLayer2 (default)
sim = DSSimulation(layer2=LiteLayer2) # LiteLayer2
Both profiles provide the publisher/subscriber concept — a publisher fires events and delivers them to a list of subscribers. They differ in routing richness:
LiteLayer2 is the lightweight profile. Its pub/sub primitive (DSLitePub / DSLiteSub) does simple fan-out: every event is delivered to all subscribers in registration order. There are no tiers, no condition filtering, and no circuit machinery. It also provides generators and coroutines as schedulable units, plus queue and resource primitives. It is the right choice when you want maximum throughput and your model does not need selective routing.
PubSubLayer2 is the routing-rich profile. Its pub/sub primitive (DSPub / DSSub) extends fan-out with four ordered delivery tiers, condition-filtered wake-ups, composable filter circuits, futures, and context managers for safe complex wait patterns. All PubSubLayer2 components are built on top of these routing primitives. The rest of this guide focuses on PubSubLayer2. See Chapter 3 for a full breakdown.
The two profiles were designed with interchangeability in mind. Switching between them requires only changing the layer2= argument at simulation construction. Moving from Lite to PubSub is straightforward — PubSub is a superset. Moving the other way is possible with some limitations: tier-based routing, condition filtering, and circuits have no Lite equivalent and must be removed or rewritten.
Which profile to use¶
| Situation | Recommendation |
|---|---|
| Small or exploratory project | PubSubLayer2 — full features available from the start without any upfront commitment |
| Debugging and observability matter | PubSubLayer2 — built-in probes, POST_HIT/POST_MISS tiers, and condition filtering make failures visible |
| Simulation throughput is the primary constraint | LiteLayer2 — lower per-event overhead, no routing machinery |
| Unsure | Start with LiteLayer2; switch to PubSubLayer2 when you hit the first blocker (conditions, probes, tiers) |
Switching from Lite to PubSub later is a one-line change at construction. The only work is adding tier arguments to add_subscriber calls and, if needed, adopting conditions and circuits. Switching in the other direction requires removing those features.
Choosing a time queue¶
The two time queue implementations provide identical functionality — the choice is purely a performance trade-off. Neither adds nor removes any simulation features; swapping one for the other is always a one-argument change with no model code affected.
| Implementation | Internal structure | Better for |
|---|---|---|
TQBinTree (default) |
Min-heap of timestamps + one FIFO bucket per timestamp | Models with many concurrent timeouts, resource bounds, or frequent mid-queue insertions |
TQBisect |
Sorted deque with bisect insertion | Models with a regular forward-moving event stream and few or no mid-queue operations |
Because performance depends on your specific event pattern, the recommended approach is to benchmark both on your model and keep whichever is faster. Switching is a single constructor argument:
sim = DSSimulation(timequeue=TQBisect) # try the alternative; revert if no improvement
Layer 3 — Components¶
Each Layer 2 profile has its own Layer 3:
- Lite —
DSLiteQueue,DSLiteResource,DSLiteTimer,DSLiteDelay. Minimal component set, no pubsub overhead. - PubSub —
DSQueue,DSResource,DSContainer,DSTimer,DSState, hardware models. Full condition and endpoint support.
Layer 4 — Agents¶
Each Layer 2 profile has its own Layer 4 agent:
- Lite —
DSLiteAgent: lightweight self-driving component with queue and resource helpers, no condition machinery. - PubSub —
DSAgent: full self-driving component with queue, resource, and condition helpers.
Layer 5 — Shim Layers (PubSub only)¶
Compatibility adapters for SimPy, salabim, and asyncio. Shims sit above the PubSub stack and translate the foreign framework's API into DSSim calls. Migrated code and native DSSim code share the same event loop without modification. See Chapter 9.
2.2 Key Takeaways¶
- DSSim is a six-layer stack. Each layer adds behavior on top of the one below without coupling back down.
- Layer 0 (time queue) and Layer 1 (simulation engine) are shared by both Layer 2 profiles.
- Both Layer 2 profiles provide the publisher/subscriber concept; they differ in routing richness, not in whether pub/sub exists.
- LiteLayer2 offers simple fan-out pub/sub (
DSLitePub/DSLiteSub): all subscribers receive every event, no tiers or conditions. - PubSubLayer2 offers routing-rich pub/sub (
DSPub/DSSub): 4-phase tiers, condition filtering, filter circuits, processes, and futures. - The two L2 profiles are interchangeable with some limitations: switching is a one-argument change; tier-based routing and condition filtering have no Lite equivalent.
- Layer 3 is profile-specific: Lite has
DSLiteQueue/DSLiteResource/timers; PubSub addsDSContainer,DSState, hardware models, and full condition support. - Layer 4 is also profile-specific:
DSLiteAgentfor Lite,DSAgentfor PubSub. - Layer 5 shims (SimPy, salabim, asyncio) are PubSub-only — they rely on condition filtering and routing machinery not present in Lite.