use crate::env::{Environment, SpaceInfo, SpaceType, StepInfo, StepResult};
#[derive(Debug, Clone)]
pub struct ContinuousLqrState {
pub position: f32,
pub velocity: f32,
pub steps: usize,
}
const ACTION_CLAMP: f32 = 1.0;
const DT: f32 = 1.0;
const VELOCITY_DAMPING: f32 = 0.1;
const DEFAULT_MAX_STEPS: usize = 50;
#[derive(Debug, Clone)]
pub struct ContinuousLqr {
position: f32,
velocity: f32,
steps: usize,
max_steps: usize,
}
impl ContinuousLqr {
pub fn new() -> Self {
Self::with_max_steps(DEFAULT_MAX_STEPS)
}
pub fn with_max_steps(max_steps: usize) -> Self {
Self { position: 0.5, velocity: 0.0, steps: 0, max_steps }
}
pub fn position(&self) -> f32 {
self.position
}
pub fn velocity(&self) -> f32 {
self.velocity
}
}
impl Default for ContinuousLqr {
fn default() -> Self {
Self::new()
}
}
impl Environment for ContinuousLqr {
type Action = Vec<f32>;
type State = ContinuousLqrState;
fn reset(&mut self) {
self.position = 0.5;
self.velocity = 0.0;
self.steps = 0;
}
fn get_observation(&self) -> Vec<f32> {
vec![self.position, self.velocity]
}
fn step(&mut self, action: Vec<f32>) -> StepResult {
let raw = action.first().copied().unwrap_or(0.0);
let force = raw.clamp(-ACTION_CLAMP, ACTION_CLAMP);
self.velocity += (force - VELOCITY_DAMPING * self.velocity) * DT;
self.position += self.velocity * DT;
self.steps += 1;
let truncated = self.steps >= self.max_steps;
let reward = -(self.position * self.position) - 0.01 * (force * force);
StepResult {
observation: self.get_observation(),
reward,
terminated: false,
truncated,
info: StepInfo::default(),
}
}
fn observation_space(&self) -> SpaceInfo {
SpaceInfo { shape: vec![2], space_type: SpaceType::Box }
}
fn action_space(&self) -> SpaceInfo {
SpaceInfo { shape: vec![1], space_type: SpaceType::Box }
}
fn render(&self) -> Vec<u8> {
Vec::new()
}
fn close(&mut self) {}
fn clone_state(&self) -> ContinuousLqrState {
ContinuousLqrState { position: self.position, velocity: self.velocity, steps: self.steps }
}
fn restore_state(&mut self, state: &ContinuousLqrState) {
self.position = state.position;
self.velocity = state.velocity;
self.steps = state.steps;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn continuous_lqr_accepts_vec_f32_action() {
let mut env = ContinuousLqr::new();
env.reset();
let result = env.step(vec![0.5]);
assert_eq!(result.observation.len(), 2);
assert!(env.velocity() > 0.0, "positive force should produce positive velocity");
assert!(env.position() > 0.5, "positive velocity should advance position");
assert!(result.reward < 0.0);
assert!(!result.terminated);
assert!(!result.truncated);
}
#[test]
fn continuous_lqr_terminates_after_max_steps() {
let mut env = ContinuousLqr::with_max_steps(3);
env.reset();
for _ in 0..2 {
let r = env.step(vec![0.0]);
assert!(!r.truncated);
}
let r = env.step(vec![0.0]);
assert!(r.truncated, "episode should truncate after max_steps");
}
#[test]
fn continuous_lqr_clamps_extreme_actions() {
let mut env = ContinuousLqr::new();
env.reset();
let _ = env.step(vec![1000.0]);
assert!(env.velocity() <= ACTION_CLAMP);
}
#[test]
fn continuous_lqr_action_space_is_box() {
let env = ContinuousLqr::new();
let space = env.action_space();
assert_eq!(space.shape, vec![1]);
assert!(matches!(space.space_type, SpaceType::Box));
}
#[test]
fn continuous_lqr_empty_action_treated_as_zero() {
let mut env = ContinuousLqr::new();
env.reset();
let _ = env.step(Vec::new());
assert_eq!(env.velocity(), -VELOCITY_DAMPING * 0.0);
}
}