Skip to main content

simu/
tutorial.rs

1// SPDX-FileCopyrightText: Copyright (c) Siemens 2026 contributed by Christoph Kuhmuench christoph.kuhmuench@gmail.com
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! **simu in 10 minutes** — a guided tour, one concept per chapter.
6//!
7//! This tutorial mirrors the structure of SimPy's
8//! ["SimPy in 10 minutes"](https://simpy.readthedocs.io/en/latest/simpy_intro/)
9//! so that readers who know SimPy can map their knowledge directly, and
10//! newcomers get the gentlest possible on-ramp. Every code block is a doc-test:
11//! it compiles and runs on every `cargo test`, so the tutorial cannot drift out
12//! of date.
13//!
14//! | Chapter | You will learn |
15//! |---------|----------------|
16//! | [`ch01_basic_concepts`] | What a process is; spawning; timeouts; running the clock |
17//! | [`ch02_waiting_for_processes`] | One process waiting for another to finish |
18//! | [`ch03_events_and_cancellation`] | Signalling between processes; cancelling work early |
19//! | [`ch04_shared_resources`] | Queuing for limited resources |
20//! | [`ch05_how_to_proceed`] | The rest of the toolbox, and where to go next |
21//!
22//! Each chapter's example also exists as a runnable program in `examples/`
23//! (`cargo run --example intro_car`, etc.).
24//!
25//! This module contains no code — only documentation.
26
27/// Chapter 1: Basic concepts — processes, timeouts, and the clock.
28///
29/// A discrete-event simulation models a system as **processes** that do
30/// something, wait, and do something again. While a process waits, simulated
31/// time jumps directly to the next interesting moment — nothing "runs" in
32/// between, which is why a simulated year can take milliseconds of wall-clock
33/// time.
34///
35/// In simu, a process is an ordinary `async` block. Waiting is `.await`ing a
36/// [`Timeout`](crate::Timeout): the executor suspends the process and resumes
37/// it when the simulated clock reaches the deadline. There is no tokio and no
38/// threads — one [`SimEnv`](crate::SimEnv) owns the clock and drives
39/// everything.
40///
41/// Our first process models a car that alternately parks and drives:
42///
43/// ```
44/// use simu::SimEnv;
45///
46/// let mut env = SimEnv::with_seed(42);
47/// let h = env.handle(); // cheap Clone handle, moved into the process
48///
49/// env.spawn(async move {
50///     loop {
51///         println!("Start parking at {}", h.now());
52///         h.timeout(5.0).await; // park for 5 time units
53///
54///         println!("Start driving at {}", h.now());
55///         h.timeout(2.0).await; // drive for 2 time units
56///     }
57/// });
58///
59/// env.run_until(15.0); // drive the event loop until t = 15
60/// assert_eq!(env.now(), 15.0);
61/// ```
62///
63/// Output:
64///
65/// ```text
66/// Start parking at 0
67/// Start driving at 5
68/// Start parking at 7
69/// Start driving at 12
70/// Start parking at 14
71/// ```
72///
73/// Things worth noticing:
74///
75/// - [`SimEnv::with_seed`](crate::SimEnv::with_seed) makes the run
76///   reproducible; same seed + same logic = identical results, always.
77/// - The process gets an [`EnvHandle`](crate::EnvHandle) (`env.handle()`),
78///   not the env itself: the env stays outside driving the loop, the handle
79///   goes inside for `now()` / `timeout()` / `spawn()`.
80/// - The process loops forever; that is fine.
81///   [`run_until`](crate::SimEnv::run_until) stops the world at t = 15, and
82///   dropping the env reclaims the still-suspended process.
83/// - Time is `f64` and unit-less — *you* decide whether 1.0 means a second or
84///   a day.
85pub mod ch01_basic_concepts {}
86
87/// Chapter 2: Waiting for another process.
88///
89/// Processes can start other processes and wait for them — the building block
90/// for "do this sub-task, then continue". In SimPy you `yield env.process(...)`;
91/// in simu, [`spawn`](crate::EnvHandle::spawn) returns a
92/// [`ProcessHandle`](crate::ProcessHandle) which is itself a future: awaiting
93/// it suspends you until the child process finishes and hands you its return
94/// value.
95///
96/// Our car is now electric. After every trip it must charge before it can
97/// drive again — and charging is its own process:
98///
99/// ```
100/// use simu::SimEnv;
101///
102/// let mut env = SimEnv::with_seed(42);
103/// let h = env.handle();
104///
105/// env.spawn(async move {
106///     loop {
107///         println!("Start driving at {}", h.now());
108///         h.timeout(2.0).await;
109///
110///         println!("Start charging at {}", h.now());
111///         let hc = h.clone();
112///         let charging = h.spawn(async move {
113///             hc.timeout(5.0).await;
114///             42.0 // a process can return a value, e.g. the kWh charged
115///         });
116///         let kwh = charging.await; // suspend until charging finishes
117///         assert_eq!(kwh, 42.0);
118///     }
119/// });
120///
121/// env.run_until(15.0);
122/// ```
123///
124/// Output:
125///
126/// ```text
127/// Start driving at 0
128/// Start charging at 2
129/// Start driving at 7
130/// Start charging at 9
131/// Start driving at 14
132/// ```
133///
134/// Two details:
135///
136/// - Handles are cheap clones sharing one env; clone freely
137///   (`let hc = h.clone()`) whenever a child process needs its own.
138/// - If you *don't* need the result, just drop the
139///   [`ProcessHandle`](crate::ProcessHandle) — the child keeps running,
140///   fire-and-forget.
141pub mod ch02_waiting_for_processes {}
142
143/// Chapter 3: Events, and cancelling work early.
144///
145/// Timeouts model *known* waiting times. For "wait until something happens",
146/// simu has manual events: [`env.event()`](crate::SimEnv::event) returns a
147/// paired ([`EventTrigger`](crate::EventTrigger),
148/// [`EventAwaitable`](crate::EventAwaitable)). Awaiting the awaitable suspends
149/// a process until someone calls [`fire()`](crate::EventTrigger::fire) on the
150/// trigger. The awaitable is `Clone`, so many processes can wait on one event;
151/// firing after the fact is fine too — late awaiters resolve immediately.
152///
153/// SimPy's version of "stop what you're doing" is throwing an `Interrupt`
154/// into a process. simu has no interrupt (it is on the roadmap — SPEC §6);
155/// instead, cancellation is expressed by **racing futures** with
156/// [`any_of!`](crate::any_of): await *either* the work finishing *or* a stop
157/// signal, whichever comes first. The losing future is dropped — and dropping
158/// *is* cancellation in Rust.
159///
160/// The driver gets impatient and stops a 5-unit charge after 3 units:
161///
162/// ```
163/// use simu::{SimEnv, any_of};
164///
165/// let mut env = SimEnv::with_seed(42);
166/// let (stop_charging, stop_signal) = env.event();
167///
168/// // The car: charge fully — unless told to stop.
169/// let h = env.handle();
170/// let car = env.spawn(async move {
171///     println!("Start charging at {}", h.now());
172///     any_of![h.timeout(5.0), stop_signal].await;
173///     println!("Stop charging at {}", h.now());
174///     h.now() // return when charging actually ended
175/// });
176///
177/// // The driver: after 3 time units, wants to leave.
178/// let h2 = env.handle();
179/// env.spawn(async move {
180///     h2.timeout(3.0).await;
181///     stop_charging.fire(); // wake everyone awaiting the signal
182/// });
183///
184/// let h3 = env.handle();
185/// env.spawn(async move {
186///     let stopped_at = car.await;
187///     assert_eq!(stopped_at, 3.0); // the event won the race, not the timeout
188///     let _ = h3; // (nothing else to do)
189/// });
190///
191/// env.run();
192/// ```
193///
194/// Notes:
195///
196/// - `fire()` **consumes** the trigger — an event fires at most once. A
197///   dropped, never-fired trigger simply means the signal never arrives
198///   (waiters stay suspended until the run ends), which is a normal
199///   discrete-event outcome, not an error.
200/// - After the race, the abandoned `timeout(5.0)` still has a queue entry;
201///   its wakeup at t = 5 is a benign no-op. That is why `env.run()` above
202///   ends at t = 5, not t = 3 — use `run_until` if the end time matters.
203/// - For being kicked off a *resource* by higher-priority work, see
204///   [`PreemptiveResource`](crate::PreemptiveResource) — same racing pattern,
205///   built in.
206pub mod ch03_events_and_cancellation {}
207
208/// Chapter 4: Shared resources — queuing for limited capacity.
209///
210/// Real systems have contention: two charging spots, one doctor, three beds.
211/// A [`Resource`](crate::Resource) models a pool of identical units.
212/// [`request()`](crate::Resource::request) resolves immediately if a unit is
213/// free, otherwise the process suspends in a FIFO queue. The resolved value is
214/// an RAII [`ResourceGuard`](crate::ResourceGuard): the unit is released when
215/// the guard drops — no explicit `release()` call, and no way to forget it.
216///
217/// Sharing works by **cloning the handle** — every clone is the same pool.
218/// (No `Arc`, no `Mutex`: the whole simulation is single-threaded by design.)
219///
220/// Four cars arrive, staggered, at a two-spot battery charging station:
221///
222/// ```
223/// use simu::{SimEnv, Resource};
224///
225/// let mut env = SimEnv::with_seed(42);
226/// let bcs = Resource::new(2); // battery charging station, 2 spots
227///
228/// for i in 0..4u32 {
229///     let h = env.handle();
230///     let station = bcs.clone(); // same pool, cheap Rc clone
231///     env.spawn(async move {
232///         h.timeout(f64::from(i) * 2.0).await; // drive to the station
233///         println!("Car {i} arriving at {}", h.now());
234///
235///         let _spot = station.request().await; // queue for a spot (FIFO)
236///         println!("Car {i} starting to charge at {}", h.now());
237///
238///         h.timeout(5.0).await; // charge
239///         println!("Car {i} leaving at {}", h.now());
240///     }); // _spot drops here → spot handed to the next car in line
241/// }
242///
243/// env.run();
244/// assert_eq!(env.now(), 12.0); // last car: arrives t=6, waits, charges 7→12
245/// ```
246///
247/// Output:
248///
249/// ```text
250/// Car 0 arriving at 0
251/// Car 0 starting to charge at 0
252/// Car 1 arriving at 2
253/// Car 1 starting to charge at 2
254/// Car 2 arriving at 4
255/// Car 0 leaving at 5
256/// Car 2 starting to charge at 5
257/// Car 3 arriving at 6
258/// Car 1 leaving at 7
259/// Car 3 starting to charge at 7
260/// Car 2 leaving at 10
261/// Car 3 leaving at 12
262/// ```
263///
264/// Cars 0 and 1 charge immediately; cars 2 and 3 queue and take over spots the
265/// moment earlier cars leave. Release-to-next-waiter is a **direct handoff**:
266/// a freshly released unit can never be stolen by a same-instant new request
267/// jumping the queue.
268pub mod ch04_shared_resources {}
269
270/// Chapter 5: How to proceed.
271///
272/// You now know the core loop of every simu model: spawn processes, await
273/// timeouts / events / resources, run, read out results. The rest of the
274/// toolbox, in the order you are likely to need it:
275///
276/// - [`PriorityResource`](crate::PriorityResource) — like
277///   [`Resource`](crate::Resource), but `request(priority)` serves lower
278///   numbers first (FIFO within a level). Triage queues, VIP lanes.
279/// - [`PreemptiveResource`](crate::PreemptiveResource) — a priority pool where
280///   an urgent request can *evict* a lower-priority holder mid-service; the
281///   victim observes it via `guard.preempted()`. See the type docs for the
282///   full pattern.
283/// - [`Container`](crate::Container) — continuous quantity instead of discrete
284///   units: tanks, silos, blood banks. `put(amount)` / `get(amount)` with
285///   strict FIFO waiters.
286/// - [`all_of!`](crate::all_of) — the dual of
287///   [`any_of!`](crate::any_of): wait for *every* sub-future (barrier /
288///   fork-join).
289/// - **Randomness** — [`h.rng()`](crate::EnvHandle::rng) borrows the env's
290///   seeded RNG; combine with [`rng::sample`](crate::rng::sample) or
291///   `rand_distr` for stochastic arrival/service times. Sample *before*
292///   `.await` — the borrow cannot be held across a suspension point.
293/// - **Monte Carlo** — [`monte_carlo::run`](crate::monte_carlo::run) executes
294///   one full, independent simulation per seed in parallel threads:
295///
296/// ```
297/// use simu::{SimEnv, monte_carlo};
298///
299/// let end_times = monte_carlo::run(0..8u64, |seed| {
300///     let mut env = SimEnv::with_seed(seed);
301///     let h = env.handle();
302///     env.spawn(async move { h.timeout(1.0).await; });
303///     env.run();
304///     env.now()
305/// });
306/// assert_eq!(end_times.len(), 8); // results arrive in seed order
307/// ```
308///
309/// When you are ready for full models, three commented showcases combine
310/// everything above, each with a walkthrough document in `examples/`:
311///
312/// - `cargo run --example hospital` — ER with priority triage, bed eviction,
313///   and a blood bank (`PriorityResource`, `PreemptiveResource` precursor
314///   patterns, `Container`).
315/// - `cargo run --example brewery` — a fermentation line with contamination
316///   events and cleanup priorities (`EventTrigger`, `PriorityResource`).
317/// - `cargo run --example warehouse` — a forklift fleet shared between
318///   receiving and shipping (`PreemptiveResource` end-to-end).
319///
320/// Coming from SimPy? The repository root has `llms.txt` with a complete
321/// SimPy → simu translation table.
322pub mod ch05_how_to_proceed {}