use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub struct CartPoleParams<S: ControlScalar> {
pub cart_mass: S,
pub pole_mass: S,
pub pole_length: S,
pub gravity: S,
}
impl<S: ControlScalar> CartPoleParams<S> {
pub fn new(
cart_mass: S,
pole_mass: S,
pole_length: S,
gravity: S,
) -> Result<Self, CartPoleError> {
if cart_mass <= S::ZERO {
return Err(CartPoleError::InvalidParameter(
"cart_mass must be positive",
));
}
if pole_mass <= S::ZERO {
return Err(CartPoleError::InvalidParameter(
"pole_mass must be positive",
));
}
if pole_length <= S::ZERO {
return Err(CartPoleError::InvalidParameter(
"pole_length must be positive",
));
}
if gravity <= S::ZERO {
return Err(CartPoleError::InvalidParameter("gravity must be positive"));
}
Ok(Self {
cart_mass,
pole_mass,
pole_length,
gravity,
})
}
pub fn gym_default() -> Self {
Self {
cart_mass: S::from_f64(1.0),
pole_mass: S::from_f64(0.1),
pole_length: S::from_f64(0.5),
gravity: S::from_f64(9.8),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CartPoleError {
InvalidParameter(&'static str),
SingularMassMatrix,
}
impl core::fmt::Display for CartPoleError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::InvalidParameter(msg) => write!(f, "Invalid parameter: {}", msg),
Self::SingularMassMatrix => write!(f, "Singular mass matrix in cart-pole"),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CartPoleState<S: ControlScalar> {
pub x: S,
pub x_dot: S,
pub theta: S,
pub theta_dot: S,
}
impl<S: ControlScalar> CartPoleState<S> {
pub fn to_array(&self) -> [S; 4] {
[self.x, self.x_dot, self.theta, self.theta_dot]
}
pub fn from_array(a: &[S; 4]) -> Self {
Self {
x: a[0],
x_dot: a[1],
theta: a[2],
theta_dot: a[3],
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct CartPolePlant<S: ControlScalar> {
params: CartPoleParams<S>,
state: CartPoleState<S>,
}
impl<S: ControlScalar> CartPolePlant<S> {
pub fn new(params: CartPoleParams<S>) -> Self {
Self {
params,
state: CartPoleState::default(),
}
}
pub fn state(&self) -> &CartPoleState<S> {
&self.state
}
pub fn set_state(&mut self, state: CartPoleState<S>) {
self.state = state;
}
pub fn reset(&mut self) {
self.state = CartPoleState::default();
}
pub fn params(&self) -> &CartPoleParams<S> {
&self.params
}
pub fn energy(&self) -> S {
let m = self.params.pole_mass;
let mc = self.params.cart_mass;
let l = self.params.pole_length;
let g = self.params.gravity;
let x_dot = self.state.x_dot;
let theta = self.state.theta;
let theta_dot = self.state.theta_dot;
let cart_ke = S::HALF * mc * x_dot * x_dot;
let vbx = x_dot + l * theta_dot * theta.cos();
let vby = l * theta_dot * theta.sin();
let bob_ke = S::HALF * m * (vbx * vbx + vby * vby);
let bob_pe = m * g * l * theta.cos();
cart_ke + bob_ke + bob_pe
}
fn derivatives(&self, s: &[S; 4], force: S) -> Result<[S; 4], CartPoleError> {
let x_dot = s[1];
let theta = s[2];
let theta_dot = s[3];
let m = self.params.pole_mass;
let mc = self.params.cart_mass;
let l = self.params.pole_length;
let g = self.params.gravity;
let sin_t = theta.sin();
let cos_t = theta.cos();
let mt = mc + m;
let det = m * l * l * (mt - m * cos_t * cos_t);
if det.abs() < S::EPSILON * S::from_f64(1e6) {
return Err(CartPoleError::SingularMassMatrix);
}
let rhs_x = force + m * l * theta_dot * theta_dot * sin_t;
let rhs_t = m * g * l * sin_t;
let x_ddot = (m * l * l * rhs_x - m * l * cos_t * rhs_t) / det;
let theta_ddot = (mt * rhs_t - m * l * cos_t * rhs_x) / det;
Ok([x_dot, x_ddot, theta_dot, theta_ddot])
}
pub fn step(&mut self, force: S, dt: S) -> Result<(), CartPoleError> {
let s = self.state.to_array();
let half = S::HALF;
let two = S::TWO;
let sixth = S::ONE / S::from_f64(6.0);
let k1 = self.derivatives(&s, force)?;
let s2: [S; 4] = core::array::from_fn(|i| s[i] + half * dt * k1[i]);
let k2 = self.derivatives(&s2, force)?;
let s3: [S; 4] = core::array::from_fn(|i| s[i] + half * dt * k2[i]);
let k3 = self.derivatives(&s3, force)?;
let s4: [S; 4] = core::array::from_fn(|i| s[i] + dt * k3[i]);
let k4 = self.derivatives(&s4, force)?;
let new_s: [S; 4] = core::array::from_fn(|i| {
s[i] + sixth * dt * (k1[i] + two * k2[i] + two * k3[i] + k4[i])
});
self.state = CartPoleState::from_array(&new_s);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_force_at_equilibrium_stays_put() {
let params = CartPoleParams::gym_default();
let plant = CartPolePlant::new(params);
let s = [0.0_f64; 4];
let deriv = plant.derivatives(&s, 0.0).expect("derivatives ok");
for (i, &d) in deriv.iter().enumerate() {
assert!(
d.abs() < 1e-12,
"derivative[{}] = {} ≠ 0 at equilibrium",
i,
d
);
}
}
#[test]
fn positive_force_moves_cart_right() {
let params = CartPoleParams::gym_default();
let mut plant = CartPolePlant::new(params);
let dt = 0.001_f64;
for _ in 0..100 {
plant.step(10.0, dt).expect("step ok");
}
assert!(
plant.state().x > 0.0,
"positive force should move cart right: x={}",
plant.state().x
);
assert!(
plant.state().x_dot > 0.0,
"positive force should give positive velocity: ẋ={}",
plant.state().x_dot
);
}
#[test]
fn energy_approximately_conserved_no_force() {
let params = CartPoleParams::gym_default();
let mut plant = CartPolePlant::new(params);
plant.set_state(CartPoleState {
x: 0.0,
x_dot: 0.0,
theta: 0.05,
theta_dot: 0.0,
});
let e0 = plant.energy();
let dt = 1e-4_f64;
for _ in 0..2000 {
plant.step(0.0, dt).expect("step ok");
}
let e1 = plant.energy();
let rel_err = (e1 - e0).abs() / e0.abs().max(1e-12);
assert!(
rel_err < 5e-3,
"energy not conserved: e0={:.6}, e1={:.6}, rel_err={:.2e}",
e0,
e1,
rel_err
);
}
#[test]
fn invalid_params_rejected() {
assert!(CartPoleParams::<f64>::new(-1.0, 0.1, 0.5, 9.8).is_err());
assert!(CartPoleParams::<f64>::new(1.0, -0.1, 0.5, 9.8).is_err());
assert!(CartPoleParams::<f64>::new(1.0, 0.1, -0.5, 9.8).is_err());
assert!(CartPoleParams::<f64>::new(1.0, 0.1, 0.5, -9.8).is_err());
}
}