thrust_rl/env/pool.rs
1//! Vectorized environment pool for parallel execution
2//!
3//! This module provides high-performance parallel environment execution using
4//! Rayon. Inspired by EnvPool (<https://arxiv.org/abs/2206.10558>), it achieves significant
5//! speedups by executing multiple environments in parallel.
6//!
7//! # Example
8//!
9//! ```rust
10//! use thrust_rl::env::{cartpole::CartPole, pool::EnvPool};
11//!
12//! // Create pool with 4 parallel environments
13//! let mut pool = EnvPool::new(|| CartPole::new(), 4);
14//!
15//! // Reset all environments in parallel
16//! let observations = pool.reset();
17//!
18//! // Step all environments in parallel
19//! let actions = vec![0, 1, 0, 1]; // One action per environment
20//! let results = pool.step(&actions);
21//! ```
22
23use rayon::prelude::*;
24
25use crate::env::{Environment, StepResult};
26
27/// A pool of environments for parallel execution
28///
29/// EnvPool manages multiple environment instances and executes operations
30/// across them in parallel using Rayon's thread pool. This provides
31/// significant performance improvements over sequential execution.
32///
33/// # Performance
34///
35/// For N environments with average step time T:
36/// - Sequential: O(N * T)
37/// - Parallel: O(max(T)) ≈ O(T) when N ≤ num_cores
38///
39/// This can provide 10-100x speedups depending on environment complexity
40/// and number of CPU cores available.
41///
42/// # Action type
43///
44/// `EnvPool` is currently restricted to discrete-action envs
45/// (`E::Action = i64`). The pool's `step` signature passes
46/// `&[i64]` because every existing trainer and example expects a
47/// dense vector of integer action indices. Adding pool support for
48/// continuous-action envs (e.g. SAC) is a separate follow-up; see
49/// issue #61 for the rationale behind the associated-type design.
50pub struct EnvPool<E: Environment> {
51 /// Vector of environment instances
52 envs: Vec<E>,
53
54 /// Number of environments
55 num_envs: usize,
56}
57
58impl<E: Environment<Action = i64> + Send> EnvPool<E> {
59 /// Create a new environment pool
60 ///
61 /// # Arguments
62 ///
63 /// * `env_fn` - Factory function to create environment instances
64 /// * `num_envs` - Number of parallel environments
65 ///
66 /// # Example
67 ///
68 /// ```rust
69 /// use thrust_rl::env::{cartpole::CartPole, pool::EnvPool};
70 ///
71 /// let pool = EnvPool::new(|| CartPole::new(), 8);
72 /// ```
73 pub fn new<F>(env_fn: F, num_envs: usize) -> Self
74 where
75 F: Fn() -> E,
76 {
77 let envs = (0..num_envs).map(|_| env_fn()).collect();
78 Self { envs, num_envs }
79 }
80
81 /// Reset all environments in parallel
82 ///
83 /// Returns a vector of initial observations, one per environment.
84 ///
85 /// # Example
86 ///
87 /// ```rust
88 /// # use thrust_rl::env::pool::EnvPool;
89 /// # use thrust_rl::env::cartpole::CartPole;
90 /// # let mut pool = EnvPool::new(|| CartPole::new(), 4);
91 /// let observations = pool.reset();
92 /// assert_eq!(observations.len(), 4);
93 /// ```
94 pub fn reset(&mut self) -> Vec<Vec<f32>>
95 where
96 E: Send,
97 {
98 use rayon::iter::ParallelIterator;
99 self.envs
100 .par_iter_mut()
101 .map(|env| {
102 env.reset();
103 env.get_observation()
104 })
105 .collect()
106 }
107
108 /// Step all environments in parallel with given actions
109 ///
110 /// # Arguments
111 ///
112 /// * `actions` - Slice of actions, one per environment
113 ///
114 /// # Panics
115 ///
116 /// Panics if the number of actions doesn't match the number of
117 /// environments.
118 ///
119 /// # Example
120 ///
121 /// ```rust
122 /// # use thrust_rl::env::pool::EnvPool;
123 /// # use thrust_rl::env::cartpole::CartPole;
124 /// # let mut pool = EnvPool::new(|| CartPole::new(), 4);
125 /// # pool.reset();
126 /// let actions = vec![0, 1, 0, 1];
127 /// let results = pool.step(&actions);
128 /// assert_eq!(results.len(), 4);
129 /// ```
130 pub fn step(&mut self, actions: &[i64]) -> Vec<StepResult>
131 where
132 E: Send,
133 {
134 use rayon::iter::ParallelIterator;
135 assert_eq!(
136 actions.len(),
137 self.num_envs,
138 "Number of actions must match number of environments"
139 );
140
141 self.envs
142 .par_iter_mut()
143 .zip(actions.par_iter())
144 .map(|(env, &action)| env.step(action))
145 .collect()
146 }
147
148 /// Get the number of environments in the pool
149 pub fn num_envs(&self) -> usize {
150 self.num_envs
151 }
152
153 /// Get observation space information from first environment
154 pub fn observation_space(&self) -> crate::env::SpaceInfo {
155 self.envs[0].observation_space()
156 }
157
158 /// Get action space information from first environment
159 pub fn action_space(&self) -> crate::env::SpaceInfo {
160 self.envs[0].action_space()
161 }
162
163 /// Reset a specific environment by index
164 ///
165 /// # Arguments
166 ///
167 /// * `env_id` - Index of environment to reset
168 ///
169 /// # Returns
170 ///
171 /// Initial observation from the reset environment
172 pub fn reset_env(&mut self, env_id: usize) -> anyhow::Result<Vec<f32>> {
173 self.envs[env_id].reset();
174 Ok(self.envs[env_id].get_observation())
175 }
176}
177
178/// Result of stepping an environment pool
179///
180/// Contains observations, rewards, and done flags for all environments.
181#[derive(Debug, Clone)]
182pub struct PoolStepResult<O> {
183 /// Observations for each environment
184 pub observations: Vec<O>,
185
186 /// Rewards for each environment
187 pub rewards: Vec<f32>,
188
189 /// Termination flags for each environment
190 pub terminated: Vec<bool>,
191
192 /// Truncation flags for each environment
193 pub truncated: Vec<bool>,
194}
195
196impl<E: Environment<Action = i64> + Send> EnvPool<E> {
197 /// Step all environments and return structured result
198 ///
199 /// This is a convenience method that unpacks individual StepResults
200 /// into a single PoolStepResult with parallel vectors.
201 pub fn step_structured(&mut self, actions: &[i64]) -> PoolStepResult<Vec<f32>> {
202 let results = self.step(actions);
203
204 let mut observations = Vec::with_capacity(self.num_envs);
205 let mut rewards = Vec::with_capacity(self.num_envs);
206 let mut terminated = Vec::with_capacity(self.num_envs);
207 let mut truncated = Vec::with_capacity(self.num_envs);
208
209 for result in results {
210 observations.push(result.observation);
211 rewards.push(result.reward);
212 terminated.push(result.terminated);
213 truncated.push(result.truncated);
214 }
215
216 PoolStepResult { observations, rewards, terminated, truncated }
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use crate::env::cartpole::CartPole;
224
225 #[test]
226 fn test_pool_creation() {
227 let pool = EnvPool::new(CartPole::new, 4);
228 assert_eq!(pool.num_envs(), 4);
229 }
230
231 #[test]
232 fn test_pool_reset() {
233 let mut pool = EnvPool::new(CartPole::new, 4);
234 let observations = pool.reset();
235
236 assert_eq!(observations.len(), 4);
237 for obs in observations {
238 assert_eq!(obs.len(), 4); // CartPole has 4D observations
239 }
240 }
241
242 #[test]
243 fn test_pool_step() {
244 let mut pool = EnvPool::new(CartPole::new, 4);
245 pool.reset();
246
247 let actions = vec![0, 1, 0, 1];
248 let results = pool.step(&actions);
249
250 assert_eq!(results.len(), 4);
251 for result in results {
252 assert_eq!(result.observation.len(), 4);
253 assert!(result.reward == 0.0 || result.reward == 1.0);
254 }
255 }
256
257 #[test]
258 fn test_pool_step_structured() {
259 let mut pool = EnvPool::new(CartPole::new, 4);
260 pool.reset();
261
262 let actions = vec![0, 1, 0, 1];
263 let result = pool.step_structured(&actions);
264
265 assert_eq!(result.observations.len(), 4);
266 assert_eq!(result.rewards.len(), 4);
267 assert_eq!(result.terminated.len(), 4);
268 assert_eq!(result.truncated.len(), 4);
269 }
270
271 #[test]
272 #[should_panic(expected = "Number of actions must match number of environments")]
273 fn test_pool_step_wrong_action_count() {
274 let mut pool = EnvPool::new(CartPole::new, 4);
275 pool.reset();
276
277 let actions = vec![0, 1]; // Wrong number of actions
278 pool.step(&actions);
279 }
280
281 #[test]
282 fn test_pool_multiple_steps() {
283 let mut pool = EnvPool::new(CartPole::new, 4);
284 pool.reset();
285
286 // Run multiple steps
287 for _ in 0..10 {
288 let actions = vec![0, 1, 0, 1];
289 let results = pool.step(&actions);
290 assert_eq!(results.len(), 4);
291 }
292 }
293
294 #[test]
295 fn test_pool_observation_space() {
296 let pool = EnvPool::new(CartPole::new, 4);
297 let obs_space = pool.observation_space();
298 assert_eq!(obs_space.shape, vec![4]);
299 }
300
301 #[test]
302 fn test_pool_action_space() {
303 let pool = EnvPool::new(CartPole::new, 4);
304 let action_space = pool.action_space();
305 // CartPole action space reports shape `[2]` alongside SpaceType::Discrete(2);
306 // see PR #16 for the rationale. Pool delegates to env[0].action_space(),
307 // so the pool inherits the same shape semantics.
308 assert_eq!(action_space.shape, vec![2]);
309 assert!(matches!(action_space.space_type, crate::env::SpaceType::Discrete(2)));
310 }
311
312 #[test]
313 fn test_pool_large_batch() {
314 // Test with larger batch to verify parallelism
315 let mut pool = EnvPool::new(CartPole::new, 16);
316 let observations = pool.reset();
317 assert_eq!(observations.len(), 16);
318
319 let actions = vec![0; 16];
320 let results = pool.step(&actions);
321 assert_eq!(results.len(), 16);
322 }
323
324 #[test]
325 fn test_pool_alternating_actions() {
326 let mut pool = EnvPool::new(CartPole::new, 8);
327 pool.reset();
328
329 // Test alternating left/right actions
330 for i in 0..5 {
331 let actions: Vec<i64> = (0..8).map(|j| ((i + j) % 2) as i64).collect();
332 let results = pool.step(&actions);
333
334 for result in results {
335 // Should not immediately terminate with alternating actions
336 if i == 0 {
337 assert!(!result.terminated);
338 }
339 }
340 }
341 }
342}