1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// SPDX-FileCopyrightText: Copyright (c) Siemens 2026 contributed by Christoph Kuhmuench christoph.kuhmuench@gmail.com
//
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Monte Carlo helper: run a simulation closure once per seed, in parallel.
//!
//! Two backends are provided, selected at compile time:
//!
//! - **Default (`std::thread`)** — one OS thread is spawned per seed. Simple,
//! zero extra dependencies; ideal for a modest number of seeds.
//! - **`monte-carlo` feature (`rayon`)** — work is distributed over rayon's
//! bounded work-stealing thread pool, so the number of live OS threads stays
//! proportional to the core count rather than the seed count. Preferable when
//! running hundreds or thousands of seeds.
//!
//! Both backends honour the same contract: results are returned in seed order,
//! and a panic in any worker is re-raised on the calling thread.
/// Run `f` once per seed in parallel and return the results in seed order.
///
/// Each invocation of `f` receives a seed and is responsible for constructing
/// its own [`SimEnv`](crate::env::SimEnv) via
/// [`SimEnv::with_seed`](crate::env::SimEnv::with_seed). Because `SimEnv` is
/// created *inside* the closure it never crosses thread boundaries, so its
/// `!Send` nature is not a problem.
///
/// `F` is shared across threads, so it must be `Send + Sync`. A plain function
/// pointer or a closure that captures only `Send + Sync` data satisfies this
/// automatically. The closure need **not** be `'static`: the default backend
/// uses [`std::thread::scope`], so `f` (and the results `R`) may borrow from the
/// caller's stack.
///
/// # Backend
///
/// With the **`monte-carlo`** feature enabled, the seeds are distributed over
/// rayon's global thread pool (bounded by the core count). Without it, one
/// `std::thread` is spawned per seed. The public contract — seed-ordered
/// results and panic propagation — is identical either way.
///
/// # Panics
///
/// If any worker panics, the original panic payload is re-raised on the calling
/// thread via [`std::panic::resume_unwind`], preserving the original backtrace.
/// With the default backend, surviving threads are still joined before the
/// re-raise, so no threads are orphaned.
///
/// # Example
///
/// ```
/// use simu::{SimEnv, monte_carlo};
///
/// // Eight independent replications; each thread builds its own SimEnv.
/// let end_times = monte_carlo::run(0..8u64, |seed| {
/// let mut env = SimEnv::with_seed(seed);
/// let h = env.handle();
/// env.spawn(async move { h.timeout(1.0).await; });
/// env.run();
/// env.now()
/// });
/// assert_eq!(end_times, vec![1.0; 8]); // results in seed order
/// ```
/// rayon backend: distribute seeds over the global work-stealing pool.
///
/// `into_par_iter().map(..).collect()` over an indexed `Vec` preserves order,
/// and rayon re-raises the first worker panic on this thread when `collect`
/// joins — matching the `std::thread` backend's contract.
/// Default backend: one scoped OS thread per seed.
///
/// Uses [`std::thread::scope`] so `f` and the results can borrow from the
/// caller — no `Arc` wrap and no `'static` bound. The scope joins every thread
/// before returning, so no thread is orphaned even on panic.