simu/env.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
5use std::cell::RefCell;
6use std::cmp::Reverse;
7use std::future::Future;
8use std::rc::Rc;
9use std::sync::Arc;
10use std::task::{Context, Waker};
11
12use rand::rngs::StdRng;
13use rand::{RngCore, SeedableRng};
14
15use crate::event::{new_event, EventAwaitable, EventTrigger};
16use crate::executor::{make_waker, ProcessEntry, SimState};
17use crate::process::{spawn_with_handle, ProcessHandle};
18use crate::rng::RandomSource;
19use crate::timeout::Timeout;
20
21/// The boxed, pluggable randomness source shared by a `SimEnv` and its handles.
22type SharedRng = Rc<RefCell<Box<dyn RandomSource>>>;
23
24/// The simulation environment. Central coordinator for a single simulation run.
25///
26/// `SimEnv` is `!Send + !Sync` (via `Rc`) and must live on one thread.
27/// For Monte Carlo parallelism, create independent `SimEnv` instances on
28/// separate threads.
29///
30/// The typical shape of every simulation: create the env, pass
31/// [`handle()`](SimEnv::handle) clones into spawned processes, run, read out
32/// results.
33///
34/// ```
35/// use simu::SimEnv;
36///
37/// let mut env = SimEnv::with_seed(1);
38/// let h = env.handle();
39/// env.spawn(async move {
40/// h.timeout(10.0).await; // suspend for 10 simulated time units
41/// });
42/// env.run(); // drive the event loop until the queue drains
43/// assert_eq!(env.now(), 10.0);
44/// ```
45pub struct SimEnv {
46 state: Rc<RefCell<SimState>>,
47 rng: SharedRng,
48}
49
50/// A lightweight handle to the simulation environment, intended to be cloned
51/// and passed into spawned processes.
52///
53/// Both `SimEnv` and all `EnvHandle` clones share the same underlying
54/// `SimState` and the same [`RandomSource`](crate::rng::RandomSource).
55#[derive(Clone)]
56pub struct EnvHandle {
57 state: Rc<RefCell<SimState>>,
58 rng: SharedRng,
59}
60
61impl Default for SimEnv {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67impl std::fmt::Debug for SimEnv {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 let mut d = f.debug_struct("SimEnv");
70 if let Ok(state) = self.state.try_borrow() {
71 let live = state.processes.iter().filter(|p| p.is_some()).count();
72 d.field("now", &state.current_time)
73 .field("queued_events", &state.event_queue.len())
74 .field("processes", &live);
75 }
76 d.finish_non_exhaustive()
77 }
78}
79
80impl std::fmt::Debug for EnvHandle {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 let mut d = f.debug_struct("EnvHandle");
83 if let Ok(state) = self.state.try_borrow() {
84 d.field("now", &state.current_time);
85 }
86 d.finish_non_exhaustive()
87 }
88}
89
90impl SimEnv {
91 /// Create a new environment seeded from OS entropy.
92 ///
93 /// Use [`with_seed`](SimEnv::with_seed) when reproducibility is required.
94 #[must_use]
95 pub fn new() -> Self {
96 SimEnv::from_source(Box::new(StdRng::from_os_rng()))
97 }
98
99 /// Create a new environment with a fixed RNG seed.
100 ///
101 /// Given the same seed and process logic the simulation will produce
102 /// identical results across runs. Uses `rand`'s `StdRng`; for a portable,
103 /// cross-language stream use [`with_source`](SimEnv::with_source) with a
104 /// [`SplitMix64`](crate::rng::SplitMix64) feed instead.
105 #[must_use]
106 pub fn with_seed(seed: u64) -> Self {
107 SimEnv::from_source(Box::new(StdRng::seed_from_u64(seed)))
108 }
109
110 /// Create a new environment driven by a custom [`RandomSource`].
111 ///
112 /// Use this to plug in an external feed — e.g. the portable
113 /// [`SplitMix64`](crate::rng::SplitMix64) generator that the SimPy
114 /// comparison harness re-implements in Python:
115 ///
116 /// ```
117 /// use simu::{SimEnv, rng::SplitMix64};
118 /// let env = SimEnv::with_source(SplitMix64::new(42));
119 /// ```
120 #[must_use]
121 pub fn with_source<R: RandomSource + 'static>(source: R) -> Self {
122 SimEnv::from_source(Box::new(source))
123 }
124
125 fn from_source(source: Box<dyn RandomSource>) -> Self {
126 SimEnv {
127 state: Rc::new(RefCell::new(SimState::new())),
128 rng: Rc::new(RefCell::new(source)),
129 }
130 }
131
132 /// Re-seed the environment's randomness source, restarting its stream.
133 ///
134 /// Takes `&mut self` for consistency with [`run`](SimEnv::run) — reseeding
135 /// mid-run would change the draw stream, so it is an owner-level operation.
136 /// Delegates to [`RandomSource::reseed`]; panics if the active source does
137 /// not support reseeding.
138 pub fn set_seed(&mut self, seed: u64) {
139 self.rng.borrow_mut().reseed(seed);
140 }
141
142 /// Return a cloneable handle suitable for passing into spawned processes.
143 #[must_use]
144 pub fn handle(&self) -> EnvHandle {
145 EnvHandle {
146 state: Rc::clone(&self.state),
147 rng: Rc::clone(&self.rng),
148 }
149 }
150
151 /// Current simulation time.
152 #[must_use]
153 pub fn now(&self) -> f64 {
154 self.state.borrow().current_time
155 }
156
157 /// Spawn a process. The future is queued and will be polled on the next
158 /// executor iteration. May be called before or during `run()`.
159 ///
160 /// Returns a [`ProcessHandle`] that resolves to the process's output when
161 /// it finishes. Drop the handle to detach (fire-and-forget).
162 pub fn spawn<F>(&self, future: F) -> ProcessHandle<F::Output>
163 where
164 F: Future + 'static,
165 F::Output: 'static,
166 {
167 self.handle().spawn(future)
168 }
169
170 /// Create a `Timeout` that resolves after `delay` simulated time units.
171 ///
172 /// # Panics
173 ///
174 /// Panics if `delay` is negative or not finite — see
175 /// [`EnvHandle::timeout`].
176 #[must_use = "futures do nothing unless awaited"]
177 pub fn timeout(&self, delay: f64) -> Timeout {
178 self.handle().timeout(delay)
179 }
180
181 /// Create a paired `(EventTrigger, EventAwaitable)` for inter-process signalling.
182 #[must_use]
183 pub fn event(&self) -> (EventTrigger, EventAwaitable) {
184 new_event()
185 }
186
187 /// Run until the event queue is empty.
188 pub fn run(&mut self) {
189 self.drain_pending_spawns();
190 self.poll_ready();
191
192 loop {
193 let next = self.state.borrow_mut().event_queue.pop();
194 match next {
195 None => break,
196 Some(Reverse(entry)) => {
197 self.state.borrow_mut().current_time = entry.time;
198 entry.waker.wake();
199 self.drain_pending_spawns();
200 self.poll_ready();
201 }
202 }
203 }
204 }
205
206 /// Run until simulated time reaches `until`.
207 ///
208 /// When the event queue empties (or its next event is beyond `until`) the
209 /// clock advances *to* `until` — but never backwards: simulated time is
210 /// monotonic, so calling `run_until` with a boundary at or before the
211 /// current time is a no-op that leaves `now()` unchanged. An event scheduled
212 /// exactly at `until` is not run (its time is not strictly less than the
213 /// boundary), yet `now()` will report `until`.
214 pub fn run_until(&mut self, until: f64) {
215 self.drain_pending_spawns();
216 self.poll_ready();
217
218 loop {
219 let should_stop = {
220 let state = self.state.borrow();
221 state
222 .event_queue
223 .peek()
224 .is_none_or(|Reverse(e)| e.time > until)
225 };
226
227 if should_stop {
228 // Advance to the boundary, but never rewind: time is monotonic.
229 let mut state = self.state.borrow_mut();
230 state.current_time = until.max(state.current_time);
231 break;
232 }
233
234 let next = self.state.borrow_mut().event_queue.pop();
235 if let Some(Reverse(entry)) = next {
236 self.state.borrow_mut().current_time = entry.time;
237 entry.waker.wake();
238 self.drain_pending_spawns();
239 self.poll_ready();
240 }
241 }
242 }
243
244 /// Move all pending spawns into the process table and mark them ready.
245 fn drain_pending_spawns(&self) {
246 let spawns: Vec<_> = std::mem::take(&mut self.state.borrow_mut().pending_spawns);
247 if spawns.is_empty() {
248 return;
249 }
250 // Collect IDs before mutating processes so we can batch the ready_queue
251 // push without holding two borrows of SimState simultaneously.
252 let ids: Vec<usize> = spawns.iter().map(|(id, _)| *id).collect();
253 {
254 let mut state = self.state.borrow_mut();
255 let ready_queue = Arc::clone(&state.ready_queue);
256 for (id, future) in spawns {
257 // Cache one waker per process now, at admission (see ProcessEntry).
258 let waker = make_waker(id, Arc::clone(&ready_queue));
259 if id >= state.processes.len() {
260 state.processes.resize_with(id + 1, || None);
261 }
262 state.processes[id] = Some(ProcessEntry { future, waker });
263 }
264 }
265 // Clone the Arc so the Ref<SimState> is dropped before we lock.
266 let rq = Arc::clone(&self.state.borrow().ready_queue);
267 rq.lock().unwrap().extend(ids);
268 }
269
270 /// Poll every process in the ready queue until the queue is empty.
271 ///
272 /// Each process is taken out of its table slot before polling so that it can
273 /// freely borrow `SimState` via its `EnvHandle` without triggering a
274 /// `RefCell` panic. Processes that return `Pending` are put back. A slot that
275 /// is already `None` (a completed process, or a duplicate wake for one whose
276 /// entry is currently taken) is simply skipped — polling a stale id is benign.
277 fn poll_ready(&self) {
278 // Clone the ready_queue Arc once; it never changes after construction.
279 let ready_queue = Arc::clone(&self.state.borrow().ready_queue);
280
281 loop {
282 let ready: Vec<usize> = std::mem::take(&mut *ready_queue.lock().unwrap());
283
284 if ready.is_empty() {
285 break;
286 }
287
288 for id in ready {
289 // `id` was allocated by `alloc_process_id`, so the slot exists.
290 let entry = self.state.borrow_mut().processes[id].take();
291
292 if let Some(mut entry) = entry {
293 let mut cx = Context::from_waker(&entry.waker);
294
295 if entry.future.as_mut().poll(&mut cx).is_pending() {
296 self.state.borrow_mut().processes[id] = Some(entry);
297 }
298
299 // A process may spawn children during its poll.
300 self.drain_pending_spawns();
301 }
302 }
303 }
304 }
305}
306
307impl Drop for SimEnv {
308 /// Break the `SimState` ↔ process reference cycle on teardown.
309 ///
310 /// A suspended process future captures an `EnvHandle`, which holds an
311 /// `Rc<RefCell<SimState>>` — so `SimState → processes → future → EnvHandle →
312 /// SimState` is a cycle. If a simulation ends with processes still suspended
313 /// (e.g. one blocked forever on a resource that never frees), that cycle
314 /// keeps the whole `SimState` alive and leaks it; across many replications
315 /// the leak accumulates. Clearing the process tables drops those futures,
316 /// releasing their handles so `SimState` can be reclaimed.
317 fn drop(&mut self) {
318 // Move the tables out from under the borrow, then drop them *after*
319 // releasing it: a suspended future's destructor may itself touch the
320 // env (e.g. deregistering a waiter), which would re-enter the borrow.
321 let leftovers = self.state.try_borrow_mut().ok().map(|mut state| {
322 (
323 std::mem::take(&mut state.processes),
324 std::mem::take(&mut state.pending_spawns),
325 )
326 });
327 drop(leftovers);
328 }
329}
330
331impl EnvHandle {
332 /// Current simulation time.
333 #[must_use]
334 pub fn now(&self) -> f64 {
335 self.state.borrow().current_time
336 }
337
338 /// Borrow the shared RNG mutably.
339 ///
340 /// The returned guard implements `rand::RngCore`, so it works with the
341 /// [`rng::sample`](crate::rng::sample) transforms and with `rand_distr`
342 /// distributions alike. Sample *before* awaiting — the guard cannot be held
343 /// across an `.await` point.
344 ///
345 /// ```
346 /// use simu::{SimEnv, rng::sample};
347 /// let env = SimEnv::with_seed(0);
348 /// let h = env.handle();
349 /// let duration = sample::exponential(&mut h.rng(), 20.0); // mean = 20
350 /// assert!(duration >= 0.0);
351 /// ```
352 #[must_use = "an RngGuard holds a mutable borrow of the env RNG; bind or use it directly"]
353 pub fn rng(&self) -> impl rand::RngCore + '_ {
354 RngGuard(self.rng.borrow_mut())
355 }
356
357 /// Create a `Timeout` that resolves after `delay` simulated time units.
358 ///
359 /// The deadline is computed **when the `Timeout` is created**
360 /// (`now() + delay`), not when it is first awaited — relevant when a
361 /// `Timeout` is stored and raced later inside `any_of!`.
362 ///
363 /// # Panics
364 ///
365 /// Panics if `delay` is negative or not finite (NaN / infinity). Simulated
366 /// time is monotonic, so a negative delay is a programming error; a zero
367 /// delay is allowed and fires on the next event-loop iteration.
368 #[must_use = "futures do nothing unless awaited"]
369 pub fn timeout(&self, delay: f64) -> Timeout {
370 assert!(
371 delay >= 0.0 && delay.is_finite(),
372 "timeout delay must be finite and non-negative (got {delay})"
373 );
374 let deadline = self.state.borrow().current_time + delay;
375 Timeout::new(deadline, self.clone())
376 }
377
378 /// Create a paired `(EventTrigger, EventAwaitable)` for inter-process signalling.
379 #[must_use]
380 pub fn event(&self) -> (EventTrigger, EventAwaitable) {
381 new_event()
382 }
383
384 /// Spawn a child process from within a running process.
385 ///
386 /// Returns a [`ProcessHandle`] that resolves to the process's output when
387 /// it finishes. Drop the handle to detach (fire-and-forget).
388 pub fn spawn<F>(&self, future: F) -> ProcessHandle<F::Output>
389 where
390 F: Future + 'static,
391 F::Output: 'static,
392 {
393 let (wrapped, handle) = spawn_with_handle(future);
394 let mut state = self.state.borrow_mut();
395 let id = state.alloc_process_id();
396 state.pending_spawns.push((id, wrapped));
397 handle
398 }
399
400 /// Schedule a wakeup at `deadline` in the event queue.
401 ///
402 /// Called by [`Timeout`] — the only crate-internal user that needs direct
403 /// access to the event queue.
404 pub(crate) fn schedule_wakeup(&self, deadline: f64, waker: Waker) {
405 self.state.borrow_mut().schedule_wakeup(deadline, waker);
406 }
407}
408
409/// Newtype wrapper so `EnvHandle::rng()` can return an `impl RngCore + '_`
410/// without exposing `RefMut` or the boxed source in the public API.
411struct RngGuard<'a>(std::cell::RefMut<'a, Box<dyn RandomSource>>);
412
413impl RngCore for RngGuard<'_> {
414 fn next_u32(&mut self) -> u32 {
415 (**self.0).next_u32()
416 }
417 fn next_u64(&mut self) -> u64 {
418 (**self.0).next_u64()
419 }
420 fn fill_bytes(&mut self, dest: &mut [u8]) {
421 (**self.0).fill_bytes(dest)
422 }
423}