use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub struct NonlinearPendulum<S: ControlScalar> {
pub length: S,
pub mass: S,
pub gravity: S,
pub damping: S,
theta: S,
theta_dot: S,
}
impl<S: ControlScalar> NonlinearPendulum<S> {
pub fn new(length: S, mass: S, gravity: S, damping: S) -> Self {
Self {
length,
mass,
gravity,
damping,
theta: S::ZERO,
theta_dot: S::ZERO,
}
}
pub fn standard() -> Self {
Self::new(S::ONE, S::ONE, S::from_f64(9.81), S::ZERO)
}
pub fn set_state(&mut self, theta: S, theta_dot: S) {
self.theta = theta;
self.theta_dot = theta_dot;
}
pub fn state(&self) -> [S; 2] {
[self.theta, self.theta_dot]
}
pub fn angle(&self) -> S {
self.theta
}
pub fn angular_velocity(&self) -> S {
self.theta_dot
}
fn derivatives(&self, theta: S, theta_dot: S, torque: S) -> [S; 2] {
let ml2 = self.mass * self.length * self.length;
if ml2.abs() < S::EPSILON {
return [S::ZERO; 2];
}
let theta_ddot = -(self.gravity / self.length) * theta.sin()
- (self.damping / ml2) * theta_dot
+ torque / ml2;
[theta_dot, theta_ddot]
}
pub fn step(&mut self, torque: S, dt: S) {
let [th, thd] = [self.theta, self.theta_dot];
let [k1_th, k1_thd] = self.derivatives(th, thd, torque);
let [k2_th, k2_thd] = self.derivatives(
th + S::HALF * dt * k1_th,
thd + S::HALF * dt * k1_thd,
torque,
);
let [k3_th, k3_thd] = self.derivatives(
th + S::HALF * dt * k2_th,
thd + S::HALF * dt * k2_thd,
torque,
);
let [k4_th, k4_thd] = self.derivatives(th + dt * k3_th, thd + dt * k3_thd, torque);
let sixth = S::ONE / S::from_f64(6.0);
self.theta += sixth * dt * (k1_th + S::TWO * k2_th + S::TWO * k3_th + k4_th);
self.theta_dot += sixth * dt * (k1_thd + S::TWO * k2_thd + S::TWO * k3_thd + k4_thd);
}
pub fn energy(&self) -> S {
let kinetic =
S::HALF * self.mass * self.length * self.length * self.theta_dot * self.theta_dot;
let potential = self.mass * self.gravity * self.length * (S::ONE - self.theta.cos());
kinetic + potential
}
pub fn kinetic_energy(&self) -> S {
S::HALF * self.mass * self.length * self.length * self.theta_dot * self.theta_dot
}
pub fn potential_energy(&self) -> S {
self.mass * self.gravity * self.length * (S::ONE - self.theta.cos())
}
pub fn reset(&mut self) {
self.theta = S::ZERO;
self.theta_dot = S::ZERO;
}
pub fn natural_frequency(&self) -> S {
if self.length > S::ZERO {
(self.gravity / self.length).sqrt()
} else {
S::ZERO
}
}
pub fn small_oscillation_period(&self) -> S {
let omega = self.natural_frequency();
if omega > S::EPSILON {
S::TWO * S::PI / omega
} else {
S::from_f64(f64::INFINITY)
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct PendulumOnCart<S: ControlScalar> {
pub cart_mass: S,
pub bob_mass: S,
pub length: S,
pub gravity: S,
pub cart_friction: S,
state: [S; 4],
}
impl<S: ControlScalar> PendulumOnCart<S> {
pub fn new(cart_mass: S, bob_mass: S, length: S, gravity: S) -> Self {
Self {
cart_mass,
bob_mass,
length,
gravity,
cart_friction: S::ZERO,
state: [S::ZERO; 4],
}
}
pub fn with_friction(mut self, friction: S) -> Self {
self.cart_friction = friction;
self
}
pub fn set_state(&mut self, state: [S; 4]) {
self.state = state;
}
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 angle(&self) -> S {
self.state[2]
}
pub fn angular_velocity(&self) -> S {
self.state[3]
}
fn derivatives(&self, s: &[S; 4], force: S) -> [S; 4] {
let x_dot = s[1];
let theta = s[2];
let theta_dot = s[3];
let m = self.bob_mass;
let mc = self.cart_mass;
let l = self.length;
let g = self.gravity;
let mt = mc + m;
let sin_t = theta.sin();
let cos_t = theta.cos();
let denom = l * (m * cos_t * cos_t - mt);
let f_eff = force - self.cart_friction * x_dot;
let (x_ddot, theta_ddot) = if denom.abs() > S::EPSILON {
let theta_ddot = (f_eff * cos_t - m * l * theta_dot * theta_dot * sin_t * cos_t
+ mt * g * sin_t)
/ denom;
let x_ddot =
(f_eff + m * l * (theta_dot * theta_dot * sin_t - theta_ddot * cos_t)) / mt;
(x_ddot, theta_ddot)
} else {
(S::ZERO, S::ZERO)
};
[x_dot, x_ddot, theta_dot, theta_ddot]
}
pub fn step(&mut self, force: S, dt: S) {
let s = self.state;
let k1 = self.derivatives(&s, force);
let s2: [S; 4] = core::array::from_fn(|i| s[i] + S::HALF * dt * k1[i]);
let k2 = self.derivatives(&s2, force);
let s3: [S; 4] = core::array::from_fn(|i| s[i] + S::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 sixth = S::ONE / S::from_f64(6.0);
for i in 0..4 {
self.state[i] += sixth * dt * (k1[i] + S::TWO * k2[i] + S::TWO * k3[i] + k4[i]);
}
}
pub fn energy(&self) -> S {
let x_dot = self.state[1];
let theta = self.state[2];
let theta_dot = self.state[3];
let l = self.length;
let m = self.bob_mass;
let mc = self.cart_mass;
let g = self.gravity;
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
}
pub fn reset(&mut self) {
self.state = [S::ZERO; 4];
}
pub fn bob_height(&self) -> S {
-self.length * self.state[2].cos()
}
pub fn bob_x(&self) -> S {
self.length * self.state[2].sin()
}
pub fn bob_y(&self) -> S {
self.bob_height()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pendulum_energy_conserved_undamped() {
let mut p = NonlinearPendulum::<f64>::standard();
p.set_state(0.5, 0.0); let e0 = p.energy();
let dt = 0.0001_f64;
for _ in 0..10000 {
p.step(0.0, dt);
}
let e1 = p.energy();
assert!(
(e1 - e0).abs() / e0 < 1e-4,
"energy not conserved: e0={:.6}, e1={:.6}",
e0,
e1
);
}
#[test]
fn pendulum_period_matches_theory() {
let mut p = NonlinearPendulum::<f64>::new(1.0, 1.0, 9.81, 0.0);
let theta0 = 0.05_f64; p.set_state(theta0, 0.0);
let t_theory = p.small_oscillation_period();
let dt = 0.0001_f64;
let max_steps = (3.0 * t_theory / dt) as usize;
let mut t_down: Option<f64> = None;
let mut t_up: Option<f64> = None;
let mut prev_theta = theta0;
for k in 0..max_steps {
p.step(0.0, dt);
let theta = p.angle();
let t = (k + 1) as f64 * dt;
if t_down.is_none() && theta < 0.0 && prev_theta >= 0.0 && t > dt * 100.0 {
t_down = Some(t);
}
if t_down.is_some() && t_up.is_none() && theta >= 0.0 && prev_theta < 0.0 {
t_up = Some(t);
break;
}
prev_theta = theta;
}
let half_period = match (t_down, t_up) {
(Some(td), Some(tu)) => tu - td,
_ => 0.0,
};
let full_period = half_period * 2.0;
assert!(
(full_period - t_theory).abs() / t_theory < 0.02,
"period: measured={:.4}, theory={:.4}",
full_period,
t_theory
);
}
#[test]
fn pendulum_damping_reduces_energy() {
let mut p = NonlinearPendulum::<f64>::new(1.0, 1.0, 9.81, 0.5);
p.set_state(1.0, 0.0);
let e0 = p.energy();
for _ in 0..10000 {
p.step(0.0, 0.001);
}
let e1 = p.energy();
assert!(
e1 < e0,
"damped energy should decrease: e0={:.4}, e1={:.4}",
e0,
e1
);
}
#[test]
fn pendulum_zero_initial_stays_zero() {
let mut p = NonlinearPendulum::<f64>::standard();
for _ in 0..1000 {
p.step(0.0, 0.01);
}
assert!(p.angle().abs() < 1e-12);
assert!(p.angular_velocity().abs() < 1e-12);
}
#[test]
fn pendulum_natural_frequency() {
let p = NonlinearPendulum::<f64>::new(1.0, 1.0, 9.81, 0.0);
let omega = p.natural_frequency();
let expected = 9.81_f64.sqrt();
assert!((omega - expected).abs() < 1e-10);
}
#[test]
fn cart_pendulum_force_moves_cart() {
let mut p = PendulumOnCart::<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(1.0, 0.001);
}
assert!(
p.cart_position() > 0.0,
"cart should move with positive force: x={}",
p.cart_position()
);
}
#[test]
fn cart_pendulum_reset() {
let mut p = PendulumOnCart::<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 cart_pendulum_energy_nonneg() {
let mut p = PendulumOnCart::<f64>::new(1.0, 0.1, 0.5, 9.81);
p.set_state([0.0, 0.0, 0.2, 0.1]);
for _ in 0..100 {
p.step(0.0, 0.001);
}
let e = p.energy();
assert!(e.is_finite(), "energy should be finite: {}", e);
}
}