use oxictl::core::matrix::Matrix;
use oxictl::mpc::robust_mpc::{RobustBoxConstraint, RobustMpc, TerminalSet, UncertaintyVertex};
fn make_pendulum_vertex(mass: f64, l: f64, g: f64, dt: f64) -> UncertaintyVertex<f64, 2, 1> {
let inertia = mass * l * l;
let a = Matrix::<f64, 2, 2> {
data: [[1.0, dt], [(g / l) * dt, 1.0]],
};
let b = Matrix::<f64, 2, 1> {
data: [[0.0], [dt / inertia]],
};
UncertaintyVertex::new(a, b)
}
#[allow(clippy::too_many_arguments)]
fn pendulum_step_nonlinear(
theta: f64,
theta_dot: f64,
u: f64,
tau_dist: f64,
mass: f64,
l: f64,
g: f64,
dt: f64,
) -> (f64, f64) {
let inertia = mass * l * l;
let theta_ddot = (g / l) * libm::sin(theta) + (u + tau_dist) / inertia;
(theta + dt * theta_dot, theta_dot + dt * theta_ddot)
}
fn main() {
let g = 9.81_f64;
let l = 0.3_f64; let m_nom = 0.5_f64; let m_lo = m_nom * 0.8; let m_hi = m_nom * 1.2; let dt = 0.05_f64;
let v_lo = make_pendulum_vertex(m_lo, l, g, dt);
let v_hi = make_pendulum_vertex(m_hi, l, g, dt);
let q = Matrix::<f64, 2, 2> {
data: [[100.0, 0.0], [0.0, 1.0]],
};
let r_mat = Matrix::<f64, 1, 1> { data: [[0.1]] };
let constraints = RobustBoxConstraint::new(0.5_f64, 2.0_f64);
let mut p_f_diag = Matrix::<f64, 2, 1>::zeros();
p_f_diag.data[0][0] = 200.0;
p_f_diag.data[1][0] = 2.0;
let terminal_set = TerminalSet::new(p_f_diag, 1.0_f64);
let mut mpc = RobustMpc::<f64, 2, 1, 8, 2>::new([v_lo, v_hi], q, r_mat, constraints, 60)
.with_terminal_set(terminal_set)
.with_tightening_margin(0.02_f64);
mpc.step_size = 5e-4_f64;
let theta0 = 0.15_f64;
let theta_dot0 = 0.0_f64;
let m_sim = m_nom;
let w_max = 0.1_f64;
println!("# Robust MPC Inverted Pendulum");
println!(
"# Uncertainty: m ∈ [{:.2}, {:.2}] kg (nom {:.2}), wind ±{:.2} N",
m_lo, m_hi, m_nom, w_max
);
println!("# Horizon H=8, tightening δ=0.02, u_max=2 N·m");
println!("step,t_s,theta_rad,theta_dot,u_Nm,tau_dist,worst_cost,in_terminal");
let mut theta = theta0;
let mut theta_dot = theta_dot0;
let n_steps = 100_usize;
let mut max_angle = theta0.abs();
let mut total_control_effort = 0.0_f64;
for step in 0..n_steps {
let t = step as f64 * dt;
let mut x = Matrix::<f64, 2, 1>::zeros();
x.data[0][0] = theta;
x.data[1][0] = theta_dot;
mpc.set_state(x);
let u_mat = mpc
.solve()
.expect("Robust MPC solve: feasibility expected for small perturbations");
let u = u_mat.data[0][0];
let u_seq = [u_mat; 8];
let wc_cost = mpc.worst_case_cost(&u_seq);
let in_term = mpc.in_terminal_set();
let w = w_max * libm::sin(2.0 * core::f64::consts::PI * 1.0 * t);
let tau_dist = w * l;
println!(
"{},{:.3},{:.5},{:.5},{:.5},{:.5},{:.4},{}",
step,
t,
theta,
theta_dot,
u,
tau_dist,
wc_cost,
if in_term { 1 } else { 0 }
);
let (theta_new, theta_dot_new) =
pendulum_step_nonlinear(theta, theta_dot, u, tau_dist, m_sim, l, g, dt);
theta = theta_new;
theta_dot = theta_dot_new;
if theta.abs() > max_angle {
max_angle = theta.abs();
}
total_control_effort += u.abs() * dt;
}
eprintln!("\n=== Robust MPC Pendulum Summary ===");
eprintln!(
"Initial perturbation: θ₀ = {:.3} rad ({:.1}°)",
theta0,
theta0.to_degrees()
);
eprintln!(
"Mass uncertainty: [{:.2}, {:.2}] kg (sim uses nom = {:.2} kg)",
m_lo, m_hi, m_sim
);
eprintln!(
"Wind disturbance: ±{:.2} N → torque ±{:.3} N·m",
w_max,
w_max * l
);
eprintln!(
"Final state: θ = {:.4} rad, θ̇ = {:.4} rad/s",
theta, theta_dot
);
eprintln!(
"Max angle reached: {:.4} rad ({:.2}°)",
max_angle,
max_angle.to_degrees()
);
eprintln!("Total control effort: {:.4} N·m·s", total_control_effort);
if theta.abs() < 0.05 && theta_dot.abs() < 0.2 {
eprintln!("PASS: pendulum stabilized near upright equilibrium.");
} else {
eprintln!(
"WARN: pendulum did not fully converge in {} steps.",
n_steps
);
}
}