Skip to main content

thrust_rl/env/
mod.rs

1//! Environment traits and implementations
2//!
3//! This module defines the core environment interface and provides
4//! built-in environments for reinforcement learning.
5//!
6//! # Action types and continuous (Box) action spaces
7//!
8//! The `Environment` trait exposes its action type as an associated
9//! type `Environment::Action`. Discrete envs (CartPole, Snake, Pong,
10//! SimpleBandit, BucketBrigade) set `type Action = i64;`. Continuous
11//! envs set `type Action = Vec<f32>;` (or another float type for
12//! single-dim cases). See `games::continuous_lqr::ContinuousLqr` for
13//! a minimal continuous-action existence proof.
14//!
15//! ## Why the associated-type strategy (Strategy A)?
16//!
17//! When this trait was extended to support continuous-control
18//! algorithms (SAC, TD3, DDPG, PPO with Gaussian heads), there were
19//! two candidate designs:
20//!
21//! - **A. Associated `Action` type on `Environment`** (chosen). One trait,
22//!   parameterised by the action type. Discrete envs default to `type Action =
23//!   i64;` so existing call sites and trainers migrate by adding a single line
24//!   per env impl.
25//! - **B. Parallel `ContinuousEnvironment` trait.** Two distinct traits; envs
26//!   implement one or the other (or both). The trainer dispatches per-trait.
27//!
28//! Strategy A was picked because:
29//!
30//! 1. **One trait.** Downstream code (snapshot/restore, pool, multi-agent
31//!    simulator) does not have to branch on which trait an env implements.
32//!    There is exactly one `Environment` to reason about.
33//! 2. **Cheap default for the common case.** Every discrete env in the repo
34//!    today gets `type Action = i64;` as a one-line addition. Continuous envs
35//!    opt in to `Vec<f32>`.
36//! 3. **Orthogonal to other trait extensions.** The associated-type approach
37//!    composes cleanly with planned additions like a `type State` slot for env
38//!    snapshotting (issue #62 / clone_state / restore_state). Strategy B would
39//!    have forced snapshot machinery to be implemented twice or behind a third
40//!    trait.
41//! 4. **Generic trainers stay generic.** Discrete trainers add a `<Action =
42//!    i64>` bound on their `E: Environment` type parameter, which is purely
43//!    additive.
44//!
45//! The trade-off: any code that wants to handle *both* discrete and
46//! continuous actions polymorphically must either be parametric on
47//! `E::Action` or use a sum-type wrapper. Until SAC lands, no such
48//! call site exists in Thrust.
49//!
50//! # Env state snapshots (`type State` / `clone_state` / `restore_state`)
51//!
52//! The `Environment` trait also exposes a `type State` associated type
53//! plus `clone_state` / `restore_state` methods so callers (MCTS-style
54//! search, replay tooling) can snapshot env state and roll out
55//! hypothetical trajectories without restarting from `reset()`. The
56//! determinism contract is per-env: fully deterministic envs (e.g.
57//! CartPole, ContinuousLqr) reproduce every subsequent step bit-for-bit
58//! after restore; envs that consume internal RNG (Snake food respawn,
59//! Pong ball serve) snapshot the simulation step but not the RNG, so
60//! reproduction is deterministic only across steps that do not draw
61//! from the RNG. Per-env docs spell out the exact guarantee.
62
63/// Core trait for RL environments
64pub trait Environment {
65    /// Action type accepted by [`Environment::step`].
66    ///
67    /// - **Discrete envs** set this to `i64`. The index identifies which of the
68    ///   [`SpaceType::Discrete`] options was chosen.
69    /// - **Continuous envs** set this to `Vec<f32>` (or `f32` for a single-dim
70    ///   Box action). Each element is a real-valued coordinate of the
71    ///   [`SpaceType::Box`] action vector.
72    ///
73    /// Existing call sites that bind actions as `i64` should
74    /// constrain `E: Environment<Action = i64>` to keep the
75    /// type signature compatible.
76    type Action;
77
78    /// Snapshot type for this env. For fully deterministic envs without
79    /// internal RNG, this is the complete env state and `restore_state`
80    /// reproduces every subsequent step exactly. For envs that consume an
81    /// internal RNG (e.g. ball serve direction, food placement), the snapshot
82    /// captures the simulation step but not the RNG; see the env-level
83    /// documentation for which fields are preserved.
84    ///
85    /// This associated type lets callers (MCTS-style search, replay tooling)
86    /// snapshot and later restore env state without re-running from
87    /// [`Environment::reset`]. The exact determinism guarantee is documented
88    /// per env.
89    type State;
90
91    /// Reset the environment and return initial observation
92    fn reset(&mut self);
93
94    /// Get the current observation
95    fn get_observation(&self) -> Vec<f32>;
96
97    /// Step the environment with an action
98    fn step(&mut self, action: Self::Action) -> StepResult;
99
100    /// Get the observation space dimensions
101    fn observation_space(&self) -> SpaceInfo;
102
103    /// Get the action space dimensions
104    fn action_space(&self) -> SpaceInfo;
105
106    /// Render the current environment state
107    fn render(&self) -> Vec<u8>;
108
109    /// Close the environment
110    fn close(&mut self);
111
112    /// Snapshot the current env state for later restoration.
113    ///
114    /// The returned value can be passed back to
115    /// [`Environment::restore_state`] to rewind the env. For envs with
116    /// internal RNG this captures the simulation step only — the determinism
117    /// guarantee is documented per env.
118    fn clone_state(&self) -> Self::State;
119
120    /// Restore the env to a previously-snapshotted state.
121    ///
122    /// After this call, the env's observable state (the value returned by
123    /// [`Environment::get_observation`]) matches the state at the time of the
124    /// snapshot. For purely deterministic envs, the next call to
125    /// [`Environment::step`] with a given action produces the same
126    /// [`StepResult`] as it would have at the time the snapshot was taken.
127    /// For envs with internal RNG, see per-env docs for the exact
128    /// reproducibility contract.
129    fn restore_state(&mut self, state: &Self::State);
130}
131
132/// Result of an environment step
133#[derive(Debug, Clone)]
134pub struct StepResult {
135    /// Next observation
136    pub observation: Vec<f32>,
137
138    /// Reward received
139    pub reward: f32,
140
141    /// Whether the episode terminated
142    pub terminated: bool,
143
144    /// Whether the episode was truncated
145    pub truncated: bool,
146
147    /// Additional info
148    pub info: StepInfo,
149}
150
151/// Space information for observations and actions
152#[derive(Debug, Clone)]
153pub struct SpaceInfo {
154    /// Shape of the space
155    pub shape: Vec<usize>,
156
157    /// Data type
158    pub space_type: SpaceType,
159}
160
161/// Space data types
162#[derive(Debug, Clone, Copy)]
163pub enum SpaceType {
164    /// Discrete space with n options
165    Discrete(usize),
166
167    /// Continuous space (Box)
168    Box,
169}
170
171/// Additional step information
172#[derive(Debug, Clone, Default)]
173pub struct StepInfo {
174    // Add custom fields as needed
175}
176
177// Game environments
178pub mod games;
179
180// Re-export game environments for backwards compatibility
181pub use games::{
182    CartPole, ContinuousLqr, FlickeringCartPole, GridWorld, MaskedCartPole, MountainCarContinuous,
183    PendulumSwingUp, Pong, SimpleBandit, SnakeEnv, TMaze, cartpole, continuous_lqr,
184    flickering_cartpole, grid_world, masked_cartpole, mountain_car_continuous, pendulum, pong,
185    simple_bandit, snake, t_maze,
186};
187// `signaling` depends on `crate::multi_agent`, which is gated behind the
188// `training` feature, so its re-export must be gated the same way.
189#[cfg(feature = "training")]
190pub use games::{SignalingGame, signaling};
191
192// Training utilities. Gated on the `training` feature because the env
193// pool only ships when the trainers are built.
194#[cfg(feature = "training")]
195pub mod pool;