1#![allow(
2 dead_code,
3 clippy::needless_borrows_for_generic_args,
4 clippy::new_without_default
5)]
6
7#[cfg(feature = "viz")]
16use gnuplot::{AxesCommon, Coordinate, Figure, PlotOption};
17use nalgebra::{Matrix1, Matrix1x4, Matrix4, Vector4};
18
19const L_BAR: f64 = 2.0; const M: f64 = 1.0; const MASS: f64 = 0.3; const G: f64 = 9.8; const DELTA_T: f64 = 0.1; const SIM_TIME: f64 = 5.0; const CART_WIDTH: f64 = 1.0;
30const CART_HEIGHT: f64 = 0.5;
31const WHEEL_RADIUS: f64 = 0.1;
32
33pub struct InvertedPendulumLQR {
34 pub q: Matrix4<f64>, pub r: Matrix1<f64>, pub trajectory: Vec<(f64, Vector4<f64>)>, }
38
39impl InvertedPendulumLQR {
40 pub fn new() -> Self {
41 let mut q = Matrix4::<f64>::zeros();
42 q[(1, 1)] = 1.0; q[(2, 2)] = 1.0; let r = Matrix1::<f64>::from_element(0.01); InvertedPendulumLQR {
48 q,
49 r,
50 trajectory: Vec::new(),
51 }
52 }
53
54 pub fn simulate(&mut self, x0: Vector4<f64>) -> bool {
55 let mut x = x0;
56 let mut time = 0.0;
57
58 self.trajectory.clear();
59 self.trajectory.push((time, x));
60
61 while time < SIM_TIME {
62 time += DELTA_T;
63
64 let u = self.lqr_control(&x);
66
67 x = self.simulation_step(&x, u);
69
70 self.trajectory.push((time, x));
71 }
72
73 println!("Simulation finished");
74 println!(
75 "Final state: x={:.2} [m], theta={:.2} [deg]",
76 x[0],
77 x[2].to_degrees()
78 );
79
80 true
81 }
82
83 fn simulation_step(&self, x: &Vector4<f64>, u: Matrix1<f64>) -> Vector4<f64> {
84 let (a, b) = self.get_model_matrix();
85 a * x + b * u[0]
86 }
87
88 fn lqr_control(&self, x: &Vector4<f64>) -> Matrix1<f64> {
89 let (a, b) = self.get_model_matrix();
90 let (k, _, _) = self.dlqr(&a, &b, &self.q, &self.r);
91 Matrix1::from_element(-(k * x)[0])
92 }
93
94 fn get_model_matrix(&self) -> (Matrix4<f64>, Vector4<f64>) {
95 let mut a = Matrix4::<f64>::new(
96 0.0,
97 1.0,
98 0.0,
99 0.0,
100 0.0,
101 0.0,
102 MASS * G / M,
103 0.0,
104 0.0,
105 0.0,
106 0.0,
107 1.0,
108 0.0,
109 0.0,
110 G * (M + MASS) / (L_BAR * M),
111 0.0,
112 );
113 a = Matrix4::<f64>::identity() + DELTA_T * a;
114
115 let b = Vector4::new(0.0, 1.0 / M, 0.0, 1.0 / (L_BAR * M)) * DELTA_T;
116
117 (a, b)
118 }
119
120 fn solve_dare(
121 &self,
122 a: &Matrix4<f64>,
123 b: &Vector4<f64>,
124 q: &Matrix4<f64>,
125 r: &Matrix1<f64>,
126 ) -> Matrix4<f64> {
127 let mut p = *q;
128 let max_iter = 150;
129 let eps = 0.01;
130
131 for _ in 0..max_iter {
132 let bt_p = b.transpose() * p;
133 let denominator = r + bt_p * b;
134 if denominator[0].abs() < 1e-10 {
135 break;
136 }
137
138 let pn = a.transpose() * p * a
139 - a.transpose() * p * b * (1.0 / denominator[0]) * bt_p * a
140 + q;
141
142 if (pn - p).abs().max() < eps {
143 break;
144 }
145 p = pn;
146 }
147 p
148 }
149
150 fn dlqr(
151 &self,
152 a: &Matrix4<f64>,
153 b: &Vector4<f64>,
154 q: &Matrix4<f64>,
155 r: &Matrix1<f64>,
156 ) -> (Matrix1x4<f64>, Matrix4<f64>, Vector4<f64>) {
157 let p = self.solve_dare(a, b, q, r);
158
159 let bt_p = b.transpose() * p;
160 let denominator = r + bt_p * b;
161 let k = (1.0 / denominator[0]) * bt_p * a;
162
163 let closed_loop = a - b * k;
164 let eigenvalues = closed_loop
166 .eigenvalues()
167 .expect("eigenvalue computation failed on 4x4 matrix");
168
169 (k, p, eigenvalues)
170 }
171}
172
173#[cfg(feature = "viz")]
175impl InvertedPendulumLQR {
176 fn get_cart_polygon(&self, x: f64) -> (Vec<f64>, Vec<f64>) {
178 let y_offset = WHEEL_RADIUS * 2.0;
179 let half_w = CART_WIDTH / 2.0;
180
181 let xs = vec![x - half_w, x + half_w, x + half_w, x - half_w, x - half_w];
183 let ys = vec![
184 y_offset,
185 y_offset,
186 y_offset + CART_HEIGHT,
187 y_offset + CART_HEIGHT,
188 y_offset,
189 ];
190 (xs, ys)
191 }
192
193 fn get_wheel_points(&self, cx: f64, cy: f64) -> (Vec<f64>, Vec<f64>) {
195 let n = 20;
196 let mut xs = Vec::with_capacity(n + 1);
197 let mut ys = Vec::with_capacity(n + 1);
198 for i in 0..=n {
199 let angle = 2.0 * std::f64::consts::PI * (i as f64) / (n as f64);
200 xs.push(cx + WHEEL_RADIUS * angle.cos());
201 ys.push(cy + WHEEL_RADIUS * angle.sin());
202 }
203 (xs, ys)
204 }
205
206 fn get_pendulum_points(&self, x: f64, theta: f64) -> (Vec<f64>, Vec<f64>) {
208 let y_offset = WHEEL_RADIUS * 2.0 + CART_HEIGHT;
209 let bar_x = x + L_BAR * theta.sin();
210 let bar_y = y_offset + L_BAR * theta.cos();
211 (vec![x, bar_x], vec![y_offset, bar_y])
212 }
213
214 pub fn visualize(&self, filename: &str) {
216 let num_frames = 6;
218 let total_steps = self.trajectory.len();
219 let step_interval = total_steps / num_frames;
220
221 let mut fg = Figure::new();
222 {
223 let axes = fg
224 .axes2d()
225 .set_title("Inverted Pendulum LQR Control", &[])
226 .set_x_label("x [m]", &[])
227 .set_y_label("y [m]", &[])
228 .set_aspect_ratio(gnuplot::AutoOption::Fix(1.0))
229 .set_x_range(
230 gnuplot::AutoOption::Fix(-3.0),
231 gnuplot::AutoOption::Fix(3.0),
232 )
233 .set_y_range(
234 gnuplot::AutoOption::Fix(-1.0),
235 gnuplot::AutoOption::Fix(4.0),
236 );
237
238 axes.lines(
240 &[-4.0, 4.0],
241 &[0.0, 0.0],
242 &[PlotOption::Color("gray"), PlotOption::LineWidth(2.0)],
243 );
244
245 let colors = [
247 "#CCCCFF", "#AAAAFF", "#8888FF", "#6666FF", "#4444FF", "#0000FF",
248 ];
249 let pendulum_colors = [
250 "#CCCCCC", "#AAAAAA", "#888888", "#666666", "#444444", "#000000",
251 ];
252
253 for (frame_idx, color_idx) in (0..num_frames).zip(0..num_frames) {
254 let step = if frame_idx == num_frames - 1 {
255 total_steps - 1
256 } else {
257 frame_idx * step_interval
258 };
259
260 let (time, state) = &self.trajectory[step];
261 let x_pos = state[0];
262 let theta = state[2];
263
264 let (cart_x, cart_y) = self.get_cart_polygon(x_pos);
266 axes.lines(
267 &cart_x,
268 &cart_y,
269 &[
270 PlotOption::Color(colors[color_idx]),
271 PlotOption::LineWidth(2.0),
272 ],
273 );
274
275 let y_offset = WHEEL_RADIUS;
277 let (w1x, w1y) = self.get_wheel_points(x_pos - CART_WIDTH / 4.0, y_offset);
278 let (w2x, w2y) = self.get_wheel_points(x_pos + CART_WIDTH / 4.0, y_offset);
279 axes.lines(
280 &w1x,
281 &w1y,
282 &[
283 PlotOption::Color(pendulum_colors[color_idx]),
284 PlotOption::LineWidth(1.5),
285 ],
286 );
287 axes.lines(
288 &w2x,
289 &w2y,
290 &[
291 PlotOption::Color(pendulum_colors[color_idx]),
292 PlotOption::LineWidth(1.5),
293 ],
294 );
295
296 let (pend_x, pend_y) = self.get_pendulum_points(x_pos, theta);
298 axes.lines(
299 &pend_x,
300 &pend_y,
301 &[
302 PlotOption::Color(pendulum_colors[color_idx]),
303 PlotOption::LineWidth(3.0),
304 ],
305 );
306
307 let (mass_x, mass_y) = self.get_wheel_points(pend_x[1], pend_y[1]);
309 axes.lines(
310 &mass_x,
311 &mass_y,
312 &[
313 PlotOption::Color(pendulum_colors[color_idx]),
314 PlotOption::LineWidth(2.0),
315 ],
316 );
317
318 if frame_idx == num_frames - 1 {
320 axes.label(
321 &format!("t={:.1}s", time),
322 Coordinate::Graph(0.02),
323 Coordinate::Graph(0.95),
324 &[],
325 );
326 }
327 }
328
329 let (_, initial_state) = &self.trajectory[0];
331 axes.label(
332 &format!("Initial angle: {:.1} deg", initial_state[2].to_degrees()),
333 Coordinate::Graph(0.02),
334 Coordinate::Graph(0.88),
335 &[],
336 );
337 }
338
339 let output_path = format!("img/inverted_pendulum/{}", filename);
340 std::fs::create_dir_all("img/inverted_pendulum")
341 .expect("failed to create img/inverted_pendulum directory");
342 fg.set_terminal("pngcairo size 800,600", &output_path);
343 fg.show()
344 .expect("failed to render gnuplot figure for inverted pendulum");
345 println!("Inverted pendulum visualization saved to: {}", output_path);
346 }
347
348 pub fn visualize_frame(&self, step: usize, filename: &str) {
350 if step >= self.trajectory.len() {
351 return;
352 }
353
354 let (time, state) = &self.trajectory[step];
355 let x_pos = state[0];
356 let theta = state[2];
357
358 let mut fg = Figure::new();
359 {
360 let axes = fg
361 .axes2d()
362 .set_title(
363 &format!("Inverted Pendulum LQR Control t={:.2}s", time),
364 &[],
365 )
366 .set_x_label("x [m]", &[])
367 .set_y_label("y [m]", &[])
368 .set_aspect_ratio(gnuplot::AutoOption::Fix(1.0))
369 .set_x_range(
370 gnuplot::AutoOption::Fix(-3.0),
371 gnuplot::AutoOption::Fix(3.0),
372 )
373 .set_y_range(
374 gnuplot::AutoOption::Fix(-1.0),
375 gnuplot::AutoOption::Fix(4.0),
376 );
377
378 axes.lines(
380 &[-4.0, 4.0],
381 &[0.0, 0.0],
382 &[PlotOption::Color("gray"), PlotOption::LineWidth(2.0)],
383 );
384
385 let (cart_x, cart_y) = self.get_cart_polygon(x_pos);
387 axes.lines(
388 &cart_x,
389 &cart_y,
390 &[PlotOption::Color("blue"), PlotOption::LineWidth(2.0)],
391 );
392
393 let y_offset = WHEEL_RADIUS;
395 let (w1x, w1y) = self.get_wheel_points(x_pos - CART_WIDTH / 4.0, y_offset);
396 let (w2x, w2y) = self.get_wheel_points(x_pos + CART_WIDTH / 4.0, y_offset);
397 axes.lines(
398 &w1x,
399 &w1y,
400 &[PlotOption::Color("black"), PlotOption::LineWidth(1.5)],
401 );
402 axes.lines(
403 &w2x,
404 &w2y,
405 &[PlotOption::Color("black"), PlotOption::LineWidth(1.5)],
406 );
407
408 let (pend_x, pend_y) = self.get_pendulum_points(x_pos, theta);
410 axes.lines(
411 &pend_x,
412 &pend_y,
413 &[PlotOption::Color("black"), PlotOption::LineWidth(3.0)],
414 );
415
416 let (mass_x, mass_y) = self.get_wheel_points(pend_x[1], pend_y[1]);
418 axes.lines(
419 &mass_x,
420 &mass_y,
421 &[PlotOption::Color("black"), PlotOption::LineWidth(2.0)],
422 );
423
424 axes.label(
426 &format!("x={:.2}m, θ={:.1}°", x_pos, theta.to_degrees()),
427 Coordinate::Graph(0.02),
428 Coordinate::Graph(0.95),
429 &[],
430 );
431 }
432
433 let output_path = format!("img/inverted_pendulum/{}", filename);
434 fg.set_terminal("pngcairo size 640,480", &output_path);
435 fg.show()
436 .expect("failed to render gnuplot frame for inverted pendulum");
437 }
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 #[test]
445 fn test_lqr_simulation_completes() {
446 let mut controller = InvertedPendulumLQR::new();
447 let x0 = Vector4::new(0.0, 0.0, 0.3, 0.0);
448 assert!(controller.simulate(x0));
449 assert!(!controller.trajectory.is_empty());
450 }
451
452 #[test]
453 fn test_lqr_stabilizes_pendulum() {
454 let mut controller = InvertedPendulumLQR::new();
455 let x0 = Vector4::new(0.0, 0.0, 0.3, 0.0);
456 controller.simulate(x0);
457
458 let (_, final_state) = controller.trajectory.last().unwrap();
459 assert!(
461 final_state[2].abs() < 0.1,
462 "Pendulum did not stabilize: angle = {}",
463 final_state[2]
464 );
465 }
466}