1use crate::base::{Action, Observation, Reward, State};
11use std::collections::BTreeMap;
12use std::fmt::Debug;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum EpisodeStatus {
21 Running,
23 Terminated,
25 Truncated,
27}
28
29impl EpisodeStatus {
30 pub const fn is_done(self) -> bool {
32 matches!(self, Self::Terminated | Self::Truncated)
33 }
34
35 pub const fn is_terminated(self) -> bool {
37 matches!(self, Self::Terminated)
38 }
39
40 pub const fn is_truncated(self) -> bool {
42 matches!(self, Self::Truncated)
43 }
44}
45
46#[derive(Debug, Clone, Default)]
52pub struct SnapshotMetadata {
53 pub components: BTreeMap<&'static str, f32>,
55 pub positions: BTreeMap<&'static str, [f32; 3]>,
57}
58
59impl SnapshotMetadata {
60 pub fn new() -> Self {
62 Self::default()
63 }
64
65 pub fn with(mut self, key: &'static str, value: f32) -> Self {
67 self.components.insert(key, value);
68 self
69 }
70
71 pub fn with_position(mut self, key: &'static str, xyz: [f32; 3]) -> Self {
73 self.positions.insert(key, xyz);
74 self
75 }
76}
77
78#[derive(Debug)]
90pub enum EnvironmentError {
91 InvalidAction(String),
93 RenderFailed(String),
95 IoError(std::io::Error),
97}
98
99impl std::error::Error for EnvironmentError {
100 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
101 match self {
102 EnvironmentError::IoError(io_err) => Some(io_err),
103 _ => None,
104 }
105 }
106}
107
108impl std::fmt::Display for EnvironmentError {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 match self {
111 EnvironmentError::InvalidAction(action_error) => {
112 write!(f, "Invalid action: {}", action_error)
113 }
114 EnvironmentError::RenderFailed(render_error) => {
115 write!(f, "Render failed: {}", render_error)
116 }
117 EnvironmentError::IoError(io_err) => {
118 write!(f, "IO operation failed: {}", io_err)
119 }
120 }
121 }
122}
123
124impl From<std::io::Error> for EnvironmentError {
125 fn from(error: std::io::Error) -> Self {
126 EnvironmentError::IoError(error)
127 }
128}
129
130pub trait Snapshot<const D: usize>: Debug {
137 type ObservationType: Observation<D>;
138
139 type RewardType: Reward;
141
142 fn observation(&self) -> &Self::ObservationType;
144
145 fn reward(&self) -> &Self::RewardType;
147
148 fn status(&self) -> EpisodeStatus;
150
151 fn is_done(&self) -> bool {
153 self.status().is_done()
154 }
155
156 fn is_terminated(&self) -> bool {
158 self.status().is_terminated()
159 }
160
161 fn is_truncated(&self) -> bool {
163 self.status().is_truncated()
164 }
165
166 fn metadata(&self) -> Option<&SnapshotMetadata> {
168 None
169 }
170}
171
172#[derive(Debug, Clone)]
184pub struct SnapshotBase<const D: usize, ObservationType: Observation<D>, RewardType: Reward> {
185 pub observation: ObservationType,
187 pub reward: RewardType,
189 pub status: EpisodeStatus,
191}
192
193impl<const D: usize, ObservationType: Observation<D>, RewardType: Reward>
194 SnapshotBase<D, ObservationType, RewardType>
195{
196 pub fn running(observation: ObservationType, reward: RewardType) -> Self {
198 Self {
199 observation,
200 reward,
201 status: EpisodeStatus::Running,
202 }
203 }
204
205 pub fn terminated(observation: ObservationType, reward: RewardType) -> Self {
207 Self {
208 observation,
209 reward,
210 status: EpisodeStatus::Terminated,
211 }
212 }
213
214 pub fn truncated(observation: ObservationType, reward: RewardType) -> Self {
216 Self {
217 observation,
218 reward,
219 status: EpisodeStatus::Truncated,
220 }
221 }
222}
223
224impl<const D: usize, ObservationType: Observation<D>, RewardType: Reward> Snapshot<D>
225 for SnapshotBase<D, ObservationType, RewardType>
226{
227 type ObservationType = ObservationType;
228 type RewardType = RewardType;
229
230 fn observation(&self) -> &Self::ObservationType {
231 &self.observation
232 }
233
234 fn reward(&self) -> &Self::RewardType {
235 &self.reward
236 }
237
238 fn status(&self) -> EpisodeStatus {
239 self.status
240 }
241}
242
243pub trait Environment<const D: usize, const SD: usize, const AD: usize> {
263 type StateType: State<SD>;
265
266 type ObservationType: Observation<D>;
268
269 type ActionType: Action<AD>;
271
272 type RewardType: Reward;
274
275 type SnapshotType: Snapshot<D, ObservationType = Self::ObservationType, RewardType = Self::RewardType>;
277
278 fn new(render: bool) -> Self;
288
289 fn reset(&mut self) -> Result<Self::SnapshotType, EnvironmentError>;
299
300 fn step(&mut self, action: Self::ActionType) -> Result<Self::SnapshotType, EnvironmentError>;
314}
315
316#[cfg(test)]
317mod tests {
318 use serde::{Deserialize, Serialize};
319
320 use super::*;
321 use crate::action::DiscreteAction;
322
323 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
324 pub struct MockObservation {
325 position: i32,
327 }
328
329 impl Observation<1> for MockObservation {
330 fn shape() -> [usize; 1] {
331 [1]
332 }
333 }
334
335 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
339 pub struct MockState {
340 position: i32,
342 }
343
344 impl MockState {
345 fn new(position: i32) -> Self {
346 Self { position }
347 }
348
349 fn is_in_bounds(position: i32) -> bool {
351 (0..=6).contains(&position)
352 }
353 }
354
355 impl State<1> for MockState {
356 type Observation = MockObservation;
357 fn numel(&self) -> usize {
358 7
359 }
360
361 fn shape() -> [usize; 1] {
362 [7]
363 }
364
365 fn is_valid(&self) -> bool {
366 Self::is_in_bounds(self.position)
367 }
368
369 fn observe(&self) -> Self::Observation {
370 MockObservation {
371 position: self.position,
372 }
373 }
374 }
375
376 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
377 enum MockAction {
378 MoveLeft, MoveRight, }
381
382 impl Action<1> for MockAction {
383 fn is_valid(&self) -> bool {
384 true }
386
387 fn shape() -> [usize; 1] {
388 [1]
389 }
390 }
391
392 impl DiscreteAction<1> for MockAction {
393 const ACTION_COUNT: usize = 2;
394 fn from_index(index: usize) -> Self {
395 match index {
396 0 => MockAction::MoveLeft,
397 1 => MockAction::MoveRight,
398 _ => panic!("Unknown action index: {}", index),
399 }
400 }
401
402 fn to_index(&self) -> usize {
403 match self {
404 MockAction::MoveLeft => 0,
405 MockAction::MoveRight => 1,
406 }
407 }
408 }
409
410 use crate::reward::ScalarReward;
411
412 struct MockEnvironment {
417 current_state: MockState,
418 step_count: usize,
419 max_steps: usize,
420 }
421
422 impl MockEnvironment {
423 const START_STATE: i32 = 3;
424 const MAX_STEPS: usize = 20;
425 const GOAL_STATE: i32 = 6;
426
427 fn with_defaults(_render: bool) -> Self {
428 Self {
429 current_state: MockState::new(Self::START_STATE),
430 step_count: 0,
431 max_steps: Self::MAX_STEPS,
432 }
433 }
434 }
435
436 impl Environment<1, 1, 1> for MockEnvironment {
437 type StateType = MockState;
438 type ObservationType = MockObservation;
439 type ActionType = MockAction;
440 type RewardType = ScalarReward;
441 type SnapshotType = SnapshotBase<1, MockObservation, ScalarReward>;
442
443 fn new(render: bool) -> Self {
444 Self::with_defaults(render)
445 }
446
447 fn reset(&mut self) -> Result<Self::SnapshotType, EnvironmentError> {
448 self.current_state = MockState::new(Self::START_STATE);
449 self.step_count = 0;
450 Ok(SnapshotBase::running(
451 self.current_state.observe(),
452 ScalarReward(0.0),
453 ))
454 }
455
456 fn step(
457 &mut self,
458 action: Self::ActionType,
459 ) -> Result<Self::SnapshotType, EnvironmentError> {
460 if !action.is_valid() {
461 return Err(EnvironmentError::InvalidAction(format!(
462 "Invalid action: {:?}.",
463 action
464 )));
465 }
466
467 let next_position = if action == MockAction::MoveLeft {
469 self.current_state.position - 1 } else {
471 self.current_state.position + 1 };
473
474 let (new_state, reward, terminated) = if next_position < 0 {
476 (MockState::new(0), -1.0, true)
477 } else if next_position > 6 {
478 (MockState::new(6), -1.0, true)
479 } else {
480 let new_state = MockState::new(next_position);
481 let reward = if next_position == Self::GOAL_STATE {
482 1.0
483 } else {
484 0.0
485 };
486 let done = next_position == Self::GOAL_STATE;
487 (new_state, reward, done)
488 };
489
490 self.current_state = new_state;
491 self.step_count += 1;
492
493 let status = if terminated {
494 EpisodeStatus::Terminated
495 } else if self.step_count >= self.max_steps {
496 EpisodeStatus::Truncated
497 } else {
498 EpisodeStatus::Running
499 };
500
501 Ok(SnapshotBase {
502 observation: new_state.observe(),
503 reward: ScalarReward(reward),
504 status,
505 })
506 }
507 }
508
509 #[derive(Debug, Clone)]
511 pub struct CustomSnapshot {
512 observation: MockObservation,
513 reward: ScalarReward,
514 status: EpisodeStatus,
515 step_count: usize,
516 cumulative_reward: f32,
517 }
518
519 impl Snapshot<1> for CustomSnapshot {
520 type ObservationType = MockObservation;
521 type RewardType = ScalarReward;
522
523 fn observation(&self) -> &MockObservation {
524 &self.observation
525 }
526
527 fn reward(&self) -> &ScalarReward {
528 &self.reward
529 }
530
531 fn status(&self) -> EpisodeStatus {
532 self.status
533 }
534 }
535
536 #[test]
538 fn test_snapshot_base_creation() {
539 let obs = MockObservation { position: 42 };
540 let snapshot = SnapshotBase::running(obs, ScalarReward(1.5));
541
542 assert_eq!(snapshot.observation(), &obs);
543 assert_eq!(snapshot.reward(), &ScalarReward(1.5));
544 assert!(!snapshot.is_done());
545 assert_eq!(snapshot.status(), EpisodeStatus::Running);
546 }
547
548 #[test]
549 fn test_snapshot_base_terminal() {
550 let obs = MockObservation { position: 0 };
551 let snapshot = SnapshotBase::terminated(obs, ScalarReward(-1.0));
552
553 assert!(snapshot.is_done());
554 assert!(snapshot.is_terminated());
555 assert!(!snapshot.is_truncated());
556 assert_eq!(snapshot.reward(), &ScalarReward(-1.0));
557 }
558
559 #[test]
560 fn test_snapshot_base_clone() {
561 let obs = MockObservation { position: 10 };
562 let snapshot1 = SnapshotBase::running(obs, ScalarReward(0.5));
563 let snapshot2 = snapshot1.clone();
564
565 assert_eq!(snapshot1.observation(), snapshot2.observation());
566 assert_eq!(snapshot1.reward(), snapshot2.reward());
567 assert_eq!(snapshot1.is_done(), snapshot2.is_done());
568 }
569
570 #[test]
571 fn test_snapshot_debug() {
572 let obs = MockObservation { position: 5 };
573 let snapshot = SnapshotBase::terminated(obs, ScalarReward(2.0));
574 let debug_str = format!("{:?}", snapshot);
575
576 assert!(debug_str.contains("SnapshotBase"));
577 assert!(debug_str.contains("position: 5"));
578 assert!(debug_str.contains("reward: ScalarReward(2.0)"));
579 assert!(debug_str.contains("Terminated"));
580 }
581
582 #[test]
584 fn test_custom_snapshot_trait_impl() {
585 let snapshot = CustomSnapshot {
586 observation: MockObservation { position: 1 },
587 reward: ScalarReward(10.0),
588 status: EpisodeStatus::Running,
589 step_count: 5,
590 cumulative_reward: 25.0,
591 };
592
593 assert_eq!(snapshot.observation().position, 1);
595 assert_eq!(snapshot.reward(), &ScalarReward(10.0));
596 assert!(!snapshot.is_done());
597
598 assert_eq!(snapshot.step_count, 5);
600 assert_eq!(snapshot.cumulative_reward, 25.0);
601 }
602
603 #[test]
605 fn test_environment_creation() {
606 let env = MockEnvironment::new(false);
607 assert_eq!(env.step_count, 0);
608 }
609
610 #[test]
611 fn test_environment_reset() {
612 let mut env = MockEnvironment::new(false);
613 let snapshot = env.reset().expect("Reset should succeed");
614
615 assert_eq!(snapshot.observation().position, 3);
616 assert_eq!(snapshot.reward(), &ScalarReward(0.0));
617 assert!(!snapshot.is_done());
618 }
619
620 #[test]
621 fn test_environment_step_valid_action() {
622 let mut env = MockEnvironment::new(false);
623 env.reset().expect("Reset should succeed");
624
625 let action = MockAction::MoveRight;
626 let snapshot = env
627 .step(action)
628 .expect("Step with valid action should succeed");
629
630 assert_eq!(snapshot.observation().position, 4);
631 assert_eq!(snapshot.reward(), &ScalarReward(0.0));
632 }
633
634 #[test]
635 fn test_environment_episode_termination() {
636 let mut env = MockEnvironment::new(false);
637 env.reset().expect("Reset should succeed");
638 env.current_state.position = 0;
639
640 for i in 0..6 {
642 let action = MockAction::MoveRight;
643 let snapshot = env.step(action).expect("Step should succeed");
644
645 if i < 5 {
646 assert!(
647 !snapshot.is_done(),
648 "Episode should not be done before reaching goal"
649 );
650 } else {
651 assert!(
652 snapshot.is_done(),
653 "Episode should be done upon reaching goal"
654 );
655 }
656 }
657 }
658
659 #[test]
660 fn test_environment_reset_clears_state() {
661 let mut env = MockEnvironment::new(false);
662
663 env.reset().expect("Reset should succeed");
665 for _ in 0..5 {
666 let action = MockAction::MoveRight;
667 let _ = env.step(action);
668 }
669
670 let snapshot = env.reset().expect("Second reset should succeed");
672 assert_eq!(snapshot.observation().position, 3);
673 assert!(!snapshot.is_done());
674 }
675
676 #[test]
677 fn test_environment_error_display() {
678 let error = EnvironmentError::InvalidAction("test action".to_string());
679 let display_str = format!("{}", error);
680 assert!(display_str.contains("Invalid action"));
681 assert!(display_str.contains("test action"));
682 }
683
684 #[test]
685 fn test_environment_error_io_conversion() {
686 let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
687 let env_error = EnvironmentError::from(io_error);
688
689 match env_error {
690 EnvironmentError::IoError(_) => {
691 }
693 _ => panic!("Expected IoError variant"),
694 }
695 }
696
697 #[test]
698 fn test_environment_error_source() {
699 let io_error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
700 let env_error = EnvironmentError::IoError(io_error);
701
702 use std::error::Error;
703 assert!(env_error.source().is_some());
704 }
705
706 #[test]
707 fn test_environment_multiple_episodes() {
708 let mut env = MockEnvironment::new(false);
709
710 for _episode in 0..3 {
711 let mut snapshot = env.reset().expect("Reset should succeed");
712 let mut step = 0;
713
714 while !snapshot.is_done() && step < 5 {
715 let action = MockAction::MoveRight;
716 snapshot = env.step(action).expect("Step should succeed");
717 step += 1;
718 }
719 }
720 }
721
722 #[test]
723 fn test_snapshot_reward_conversion() {
724 let observation = MockObservation { position: 1 };
725 let snapshot = SnapshotBase::running(observation, ScalarReward(42.5));
726
727 let reward_as_f32: f32 = (*snapshot.reward()).into();
729 assert_eq!(reward_as_f32, 42.5);
730 }
731
732 #[test]
733 fn test_metadata_default_is_empty() {
734 let meta = SnapshotMetadata::default();
735 assert!(meta.components.is_empty());
736 assert!(meta.positions.is_empty());
737 }
738
739 #[test]
740 fn test_metadata_builder_components_and_positions() {
741 let meta = SnapshotMetadata::new()
742 .with("forward", 1.25)
743 .with("ctrl", -0.1)
744 .with_position("torso", [0.5, 0.0, 1.1])
745 .with_position("com", [0.4, 0.0, 0.9]);
746
747 assert_eq!(meta.components.len(), 2);
748 assert_eq!(meta.components.get("forward"), Some(&1.25));
749 assert_eq!(meta.positions.len(), 2);
750 assert_eq!(meta.positions.get("torso"), Some(&[0.5, 0.0, 1.1]));
751 }
752}