use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub struct InvertedPendulum<S: ControlScalar> {
pub m_cart: S,
pub m_pole: S,
pub l_pole: S,
pub g: S,
pub b_cart: S,
state: [S; 4],
}
impl<S: ControlScalar> InvertedPendulum<S> {
pub fn new(m_cart: S, m_pole: S, l_pole: S, g: S) -> Self {
Self {
m_cart,
m_pole,
l_pole,
g,
b_cart: S::ZERO,
state: [S::ZERO; 4],
}
}
pub fn standard() -> Self
where
S: From<f32>,
{
Self::new(
S::from_f64(1.0),
S::from_f64(0.1),
S::from_f64(0.5),
S::from_f64(9.81),
)
}
pub fn with_initial_angle(mut self, theta_rad: S) -> Self {
self.state[2] = theta_rad;
self
}
pub fn with_friction(mut self, b: S) -> Self {
self.b_cart = b;
self
}
pub fn step(&mut self, f: S, dt: S) {
let x = self.state[0];
let xd = self.state[1];
let theta = self.state[2];
let thetad = self.state[3];
let _ = x;
let m = self.m_pole;
let mc = self.m_cart;
let l = self.l_pole;
let g = self.g;
let mt = mc + m;
let sin_t = theta.sin();
let cos_t = theta.cos();
let f_eff = f - self.b_cart * xd;
let denom = l * (S::from_f64(4.0 / 3.0) - m * cos_t * cos_t / mt);
let theta_ddot = if denom.abs() > S::EPSILON {
(g * sin_t - cos_t * (f_eff + m * l * thetad * thetad * sin_t) / mt) / denom
} else {
S::ZERO
};
let x_ddot = (f_eff + m * l * (thetad * thetad * sin_t - theta_ddot * cos_t)) / mt;
self.state[0] += xd * dt;
self.state[1] += x_ddot * dt;
self.state[2] += thetad * dt;
self.state[3] += theta_ddot * dt;
}
pub fn state(&self) -> &[S; 4] {
&self.state
}
pub fn cart_position(&self) -> S {
self.state[0]
}
pub fn cart_velocity(&self) -> S {
self.state[1]
}
pub fn pole_angle(&self) -> S {
self.state[2]
}
pub fn pole_angular_velocity(&self) -> S {
self.state[3]
}
pub fn is_fallen(&self, max_angle_rad: S) -> bool {
self.state[2].abs() > max_angle_rad
}
pub fn reset(&mut self) {
self.state = [S::ZERO; 4];
}
pub fn set_state(&mut self, state: [S; 4]) {
self.state = state;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unforced_falls_over() {
let mut p = InvertedPendulum::<f64>::new(1.0, 0.1, 0.5, 9.81);
p.set_state([0.0, 0.0, 0.1, 0.0]); for _ in 0..1000 {
p.step(0.0, 0.001);
}
assert!(
p.pole_angle().abs() > 0.5,
"Pendulum should fall: θ={:.3}",
p.pole_angle()
);
}
#[test]
fn falls_detected() {
let mut p = InvertedPendulum::<f64>::new(1.0, 0.1, 0.5, 9.81);
p.set_state([0.0, 0.0, 1.0, 0.0]); for _ in 0..1000 {
p.step(0.0, 0.001);
}
assert!(p.is_fallen(1.5));
}
#[test]
fn zero_angle_upright_no_force() {
let mut p = InvertedPendulum::<f64>::new(1.0, 0.1, 0.5, 9.81);
for _ in 0..100 {
p.step(0.0, 0.001);
}
assert!(p.pole_angle().abs() < 1e-10);
}
#[test]
fn force_moves_cart() {
let mut p = InvertedPendulum::<f64>::new(1.0, 0.1, 0.5, 9.81);
for _ in 0..1000 {
p.step(1.0, 0.001);
}
assert!(p.cart_position() > 0.0, "Cart should move forward");
}
#[test]
fn reset_clears_state() {
let mut p = InvertedPendulum::<f64>::new(1.0, 0.1, 0.5, 9.81);
p.set_state([1.0, 2.0, 0.3, 0.4]);
p.reset();
assert_eq!(p.state(), &[0.0_f64; 4]);
}
#[test]
fn lqr_stabilizes_linearized_pendulum() {
let k = [0.0_f64, 0.0, -50.0, -10.0];
let mut p = InvertedPendulum::<f64>::new(1.0, 0.1, 0.5, 9.81);
p.set_state([0.0, 0.0, 0.05, 0.0]);
for _ in 0..5000 {
let s = p.state();
let f = -(k[0] * s[0] + k[1] * s[1] + k[2] * s[2] + k[3] * s[3]);
p.step(f, 0.001);
if p.is_fallen(1.0) {
break;
}
}
assert!(
p.pole_angle().abs() < 0.1,
"LQR should stabilize: θ={:.4}",
p.pole_angle()
);
}
}