Skip to main content

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}