Skip to main content

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}