1use rand::Rng;
22
23use crate::env::{Environment, SpaceInfo, SpaceType, StepInfo, StepResult};
24
25#[derive(Debug, Clone)]
32pub struct CartPoleState {
33 pub x: f32,
35 pub x_dot: f32,
37 pub theta: f32,
39 pub theta_dot: f32,
41 pub steps: usize,
43}
44
45#[derive(Debug)]
57pub struct CartPole {
58 x: f32, x_dot: f32, theta: f32, theta_dot: f32, steps: usize,
66 max_steps: usize,
67
68 gravity: f32,
70 #[allow(dead_code)]
71 mass_cart: f32,
72 mass_pole: f32,
73 total_mass: f32,
74 length: f32, pole_mass_length: f32, force_mag: f32,
77 tau: f32, theta_threshold: f32,
81 x_threshold: f32,
82}
83
84impl CartPole {
85 pub fn new() -> Self {
95 let gravity = 9.8;
96 let mass_cart = 1.0;
97 let mass_pole = 0.1;
98 let total_mass = mass_cart + mass_pole;
99 let length = 0.5; let pole_mass_length = mass_pole * length;
101 let force_mag = 10.0;
102 let tau = 0.02;
103 let theta_threshold = 12.0 * 2.0 * std::f32::consts::PI / 360.0; let x_threshold = 2.4;
105 let max_steps = 500;
106
107 Self {
108 x: 0.0,
109 x_dot: 0.0,
110 theta: 0.0,
111 theta_dot: 0.0,
112 steps: 0,
113 max_steps,
114 gravity,
115 mass_cart,
116 mass_pole,
117 total_mass,
118 length,
119 pole_mass_length,
120 force_mag,
121 tau,
122 theta_threshold,
123 x_threshold,
124 }
125 }
126
127 fn reset_state(&mut self) {
132 let mut rng = rand::rng();
133
134 self.x = rng.random_range(-0.05..0.05);
136 self.x_dot = rng.random_range(-0.05..0.05);
137 self.theta = rng.random_range(-0.05..0.05);
138 self.theta_dot = rng.random_range(-0.05..0.05);
139 }
140
141 fn physics_step(&mut self, action: i64) {
151 let force = if action == 1 {
153 self.force_mag
154 } else {
155 -self.force_mag
156 };
157
158 let cos_theta = self.theta.cos();
159 let sin_theta = self.theta.sin();
160
161 let temp = (force + self.pole_mass_length * self.theta_dot * self.theta_dot * sin_theta)
163 / self.total_mass;
164 let theta_acc = (self.gravity * sin_theta - cos_theta * temp)
165 / (self.length
166 * (4.0 / 3.0 - self.mass_pole * cos_theta * cos_theta / self.total_mass));
167 let x_acc = temp - self.pole_mass_length * theta_acc * cos_theta / self.total_mass;
168
169 self.x_dot += self.tau * x_acc;
171 self.x += self.tau * self.x_dot;
172 self.theta_dot += self.tau * theta_acc;
173 self.theta += self.tau * self.theta_dot;
174 }
175
176 fn is_terminated(&self) -> bool {
182 self.x < -self.x_threshold
183 || self.x > self.x_threshold
184 || self.theta < -self.theta_threshold
185 || self.theta > self.theta_threshold
186 }
187
188 fn is_truncated(&self) -> bool {
190 self.steps >= self.max_steps
191 }
192
193 fn get_observation(&self) -> Vec<f32> {
195 vec![self.x, self.x_dot, self.theta, self.theta_dot]
196 }
197
198 pub fn get_state(&self) -> [f32; 4] {
200 [self.x, self.x_dot, self.theta, self.theta_dot]
201 }
202}
203
204impl Default for CartPole {
205 fn default() -> Self {
206 Self::new()
207 }
208}
209
210impl Environment for CartPole {
211 type Action = i64;
212 type State = CartPoleState;
213
214 fn reset(&mut self) {
215 self.reset_state();
216 self.steps = 0;
217 }
218
219 fn get_observation(&self) -> Vec<f32> {
220 self.get_observation()
221 }
222
223 fn step(&mut self, action: i64) -> StepResult {
224 self.physics_step(action);
226
227 self.steps += 1;
229
230 let terminated = self.is_terminated();
232 let truncated = self.is_truncated();
233
234 let reward = if terminated || truncated { 0.0 } else { 1.0 };
237
238 StepResult {
239 observation: self.get_observation(),
240 reward,
241 terminated,
242 truncated,
243 info: StepInfo::default(),
244 }
245 }
246
247 fn observation_space(&self) -> SpaceInfo {
248 SpaceInfo { shape: vec![4], space_type: SpaceType::Box }
249 }
250
251 fn action_space(&self) -> SpaceInfo {
252 SpaceInfo { shape: vec![2], space_type: SpaceType::Discrete(2) }
253 }
254
255 fn render(&self) -> Vec<u8> {
256 vec![] }
258
259 fn close(&mut self) {
260 }
262
263 fn clone_state(&self) -> CartPoleState {
264 CartPoleState {
265 x: self.x,
266 x_dot: self.x_dot,
267 theta: self.theta,
268 theta_dot: self.theta_dot,
269 steps: self.steps,
270 }
271 }
272
273 fn restore_state(&mut self, state: &CartPoleState) {
274 self.x = state.x;
275 self.x_dot = state.x_dot;
276 self.theta = state.theta;
277 self.theta_dot = state.theta_dot;
278 self.steps = state.steps;
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn test_cartpole_init() {
288 let env = CartPole::new();
289 assert_eq!(env.max_steps, 500);
290 assert_eq!(env.gravity, 9.8);
291 assert_eq!(env.mass_cart, 1.0);
292 assert_eq!(env.mass_pole, 0.1);
293 }
294
295 #[test]
296 fn test_cartpole_reset() {
297 let mut env = CartPole::new();
298 env.reset();
299 let obs = env.get_observation();
300
301 assert_eq!(obs.len(), 4, "Observation should have 4 elements");
302 assert_eq!(env.steps, 0, "Steps should be reset to 0");
303
304 for &val in &obs {
306 assert!(val.abs() < 0.1, "Initial state should be small perturbation, got {}", val);
307 }
308 }
309
310 #[test]
311 fn test_cartpole_step() {
312 let mut env = CartPole::new();
313 env.reset();
314
315 let result = env.step(1);
316
317 assert_eq!(result.observation.len(), 4, "Observation should have 4 elements");
318 assert!(result.reward == 0.0 || result.reward == 1.0, "Reward should be 0 or 1");
319 }
320
321 #[test]
322 fn test_cartpole_termination() {
323 let mut env = CartPole::new();
324 env.reset();
325
326 env.x = 3.0; let result = env.step(0);
330 assert!(
331 result.terminated,
332 "Episode should terminate when cart exceeds position threshold"
333 );
334
335 env.reset();
337 env.theta = 0.5; let result = env.step(0);
340 assert!(result.terminated, "Episode should terminate when pole exceeds angle threshold");
341 }
342
343 #[test]
344 fn test_cartpole_truncation() {
345 let mut env = CartPole::new();
346 env.reset();
347
348 env.steps = env.max_steps - 1;
350
351 let result = env.step(0);
352 assert!(result.truncated, "Episode should truncate at max steps");
353 }
354
355 #[test]
356 fn test_cartpole_rewards() {
357 let mut env = CartPole::new();
358 env.reset();
359
360 let result = env.step(1);
362 if !result.terminated && !result.truncated {
363 assert_eq!(result.reward, 1.0, "Should receive reward of 1.0 per step");
364 }
365 }
366
367 #[test]
368 fn test_cartpole_action_left() {
369 let mut env = CartPole::new();
370 env.reset();
371
372 let x_before = env.x;
373 env.step(0); assert_ne!(env.x, x_before, "State should change after step");
378 }
379
380 #[test]
381 fn test_cartpole_action_right() {
382 let mut env = CartPole::new();
383 env.reset();
384
385 let x_before = env.x;
386 env.step(1); assert_ne!(env.x, x_before, "State should change after step");
390 }
391
392 #[test]
393 fn test_cartpole_observation_space() {
394 let env = CartPole::new();
395 let obs_space = env.observation_space();
396
397 assert_eq!(obs_space.shape, vec![4]);
398 assert!(matches!(obs_space.space_type, SpaceType::Box));
399 }
400
401 #[test]
402 fn test_cartpole_action_space() {
403 let env = CartPole::new();
404 let action_space = env.action_space();
405
406 assert_eq!(action_space.shape, vec![2]);
411 assert!(matches!(action_space.space_type, SpaceType::Discrete(2)));
412 }
413
414 #[test]
415 fn clone_restore_round_trips() {
416 let mut env = CartPole::new();
417 env.reset();
418
419 for i in 0..5 {
421 env.step((i % 2) as i64);
422 }
423 let snap = env.clone_state();
424
425 let r1 = env.step(1);
427
428 env.restore_state(&snap);
430 let r2 = env.step(1);
431
432 assert_eq!(r1.observation, r2.observation);
434 assert_eq!(r1.reward, r2.reward);
435 assert_eq!(r1.terminated, r2.terminated);
436 assert_eq!(r1.truncated, r2.truncated);
437 }
438
439 #[test]
440 fn test_cartpole_episode() {
441 let mut env = CartPole::new();
442 env.reset();
443
444 let mut steps = 0;
445
446 for _ in 0..1000 {
448 let action = steps % 2; let result = env.step(action);
450 steps += 1;
451
452 if result.terminated || result.truncated {
453 break;
454 }
455 }
456
457 assert!(steps > 0, "Episode should run at least one step");
458 assert!(steps <= 500, "Episode should not exceed max_steps");
459 }
460}