thrust_rl/env/games/
continuous_lqr.rs1use crate::env::{Environment, SpaceInfo, SpaceType, StepInfo, StepResult};
20
21#[derive(Debug, Clone)]
27pub struct ContinuousLqrState {
28 pub position: f32,
30 pub velocity: f32,
32 pub steps: usize,
34}
35
36const ACTION_CLAMP: f32 = 1.0;
38
39const DT: f32 = 1.0;
41
42const VELOCITY_DAMPING: f32 = 0.1;
44
45const DEFAULT_MAX_STEPS: usize = 50;
47
48#[derive(Debug, Clone)]
50pub struct ContinuousLqr {
51 position: f32,
53
54 velocity: f32,
56
57 steps: usize,
59
60 max_steps: usize,
62}
63
64impl ContinuousLqr {
65 pub fn new() -> Self {
67 Self::with_max_steps(DEFAULT_MAX_STEPS)
68 }
69
70 pub fn with_max_steps(max_steps: usize) -> Self {
72 Self { position: 0.5, velocity: 0.0, steps: 0, max_steps }
73 }
74
75 pub fn position(&self) -> f32 {
77 self.position
78 }
79
80 pub fn velocity(&self) -> f32 {
82 self.velocity
83 }
84}
85
86impl Default for ContinuousLqr {
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92impl Environment for ContinuousLqr {
93 type Action = Vec<f32>;
96
97 type State = ContinuousLqrState;
100
101 fn reset(&mut self) {
102 self.position = 0.5;
103 self.velocity = 0.0;
104 self.steps = 0;
105 }
106
107 fn get_observation(&self) -> Vec<f32> {
108 vec![self.position, self.velocity]
109 }
110
111 fn step(&mut self, action: Vec<f32>) -> StepResult {
112 let raw = action.first().copied().unwrap_or(0.0);
116 let force = raw.clamp(-ACTION_CLAMP, ACTION_CLAMP);
117
118 self.velocity += (force - VELOCITY_DAMPING * self.velocity) * DT;
121 self.position += self.velocity * DT;
122
123 self.steps += 1;
124 let truncated = self.steps >= self.max_steps;
125
126 let reward = -(self.position * self.position) - 0.01 * (force * force);
127
128 StepResult {
129 observation: self.get_observation(),
130 reward,
131 terminated: false,
132 truncated,
133 info: StepInfo::default(),
134 }
135 }
136
137 fn observation_space(&self) -> SpaceInfo {
138 SpaceInfo { shape: vec![2], space_type: SpaceType::Box }
140 }
141
142 fn action_space(&self) -> SpaceInfo {
143 SpaceInfo { shape: vec![1], space_type: SpaceType::Box }
145 }
146
147 fn render(&self) -> Vec<u8> {
148 Vec::new()
149 }
150
151 fn close(&mut self) {}
152
153 fn clone_state(&self) -> ContinuousLqrState {
154 ContinuousLqrState { position: self.position, velocity: self.velocity, steps: self.steps }
155 }
156
157 fn restore_state(&mut self, state: &ContinuousLqrState) {
158 self.position = state.position;
159 self.velocity = state.velocity;
160 self.steps = state.steps;
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn continuous_lqr_accepts_vec_f32_action() {
170 let mut env = ContinuousLqr::new();
171 env.reset();
172
173 let result = env.step(vec![0.5]);
176 assert_eq!(result.observation.len(), 2);
177 assert!(env.velocity() > 0.0, "positive force should produce positive velocity");
178 assert!(env.position() > 0.5, "positive velocity should advance position");
179
180 assert!(result.reward < 0.0);
183
184 assert!(!result.terminated);
186 assert!(!result.truncated);
187 }
188
189 #[test]
190 fn continuous_lqr_terminates_after_max_steps() {
191 let mut env = ContinuousLqr::with_max_steps(3);
192 env.reset();
193
194 for _ in 0..2 {
195 let r = env.step(vec![0.0]);
196 assert!(!r.truncated);
197 }
198
199 let r = env.step(vec![0.0]);
200 assert!(r.truncated, "episode should truncate after max_steps");
201 }
202
203 #[test]
204 fn continuous_lqr_clamps_extreme_actions() {
205 let mut env = ContinuousLqr::new();
206 env.reset();
207
208 let _ = env.step(vec![1000.0]);
211 assert!(env.velocity() <= ACTION_CLAMP);
212 }
213
214 #[test]
215 fn continuous_lqr_action_space_is_box() {
216 let env = ContinuousLqr::new();
217 let space = env.action_space();
218 assert_eq!(space.shape, vec![1]);
219 assert!(matches!(space.space_type, SpaceType::Box));
220 }
221
222 #[test]
223 fn continuous_lqr_empty_action_treated_as_zero() {
224 let mut env = ContinuousLqr::new();
225 env.reset();
226 let _ = env.step(Vec::new());
228 assert_eq!(env.velocity(), -VELOCITY_DAMPING * 0.0);
229 }
230}