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;