1use clarabel::algebra::CscMatrix;
18use clarabel::solver::{
19 DefaultSettings, DefaultSettingsBuilder, DefaultSolver, IPSolver, SolverStatus,
20};
21use nalgebra::Vector2;
22
23const GRAVITY: f64 = 9.81;
25
26#[derive(Debug, Clone)]
28pub struct RocketLandingConfig {
29 pub n_steps: usize,
31 pub dt: f64,
33 pub thrust_max: f64,
35 pub mass: f64,
37 pub gravity: f64,
39 pub w_terminal_pos: f64,
41 pub w_terminal_vel: f64,
43 pub w_thrust: f64,
45}
46
47impl Default for RocketLandingConfig {
48 fn default() -> Self {
49 Self {
50 n_steps: 40,
51 dt: 0.1,
52 thrust_max: 24.0,
53 mass: 1.0,
54 gravity: GRAVITY,
55 w_terminal_pos: 1000.0,
56 w_terminal_vel: 1000.0,
57 w_thrust: 1.0,
58 }
59 }
60}
61
62#[derive(Debug, Clone)]
64pub struct RocketTrajectory {
65 pub positions: Vec<Vector2<f64>>,
67 pub velocities: Vec<Vector2<f64>>,
69 pub thrusts: Vec<Vector2<f64>>,
71 pub fuel_cost: f64,
73}
74
75#[derive(Debug, Clone)]
77pub struct RocketState {
78 pub position: Vector2<f64>,
80 pub velocity: Vector2<f64>,
82}
83
84#[derive(Debug, Clone)]
86pub struct LandingTarget {
87 pub position: Vector2<f64>,
89 pub velocity: Vector2<f64>,
91}
92
93impl Default for LandingTarget {
94 fn default() -> Self {
95 Self {
96 position: Vector2::zeros(),
97 velocity: Vector2::zeros(),
98 }
99 }
100}
101
102pub fn solve_landing_trajectory(
110 initial: &RocketState,
111 target: &LandingTarget,
112 config: &RocketLandingConfig,
113) -> Option<RocketTrajectory> {
114 use clarabel::solver::SupportedConeT;
115
116 let n = config.n_steps;
117 let dt = config.dt;
118 let m = config.mass;
119 let g = config.gravity;
120 let t_max = config.thrust_max;
121
122 let n_u = 2 * n;
126 let n_x = 4 * (n + 1);
127 let n_vars = n_u + n_x;
128
129 let u_idx = |t: usize, k: usize| -> usize { 2 * t + k };
130 let x_idx = |t: usize, k: usize| -> usize { n_u + 4 * t + k };
131
132 let mut p_rows = Vec::new();
137 let mut p_cols = Vec::new();
138 let mut p_vals = Vec::new();
139
140 for t in 0..n {
142 for k in 0..2 {
143 let idx = u_idx(t, k);
144 p_rows.push(idx);
145 p_cols.push(idx);
146 p_vals.push(2.0 * config.w_thrust);
147 }
148 }
149
150 for k in 0..2 {
152 let idx = x_idx(n, k);
153 p_rows.push(idx);
154 p_cols.push(idx);
155 p_vals.push(2.0 * config.w_terminal_pos);
156 }
157
158 for k in 2..4 {
160 let idx = x_idx(n, k);
161 p_rows.push(idx);
162 p_cols.push(idx);
163 p_vals.push(2.0 * config.w_terminal_vel);
164 }
165
166 let p = CscMatrix::new_from_triplets(n_vars, n_vars, p_rows, p_cols, p_vals);
167
168 let mut q_vec = vec![0.0; n_vars];
170 q_vec[x_idx(n, 0)] = -2.0 * config.w_terminal_pos * target.position[0];
172 q_vec[x_idx(n, 1)] = -2.0 * config.w_terminal_pos * target.position[1];
173 q_vec[x_idx(n, 2)] = -2.0 * config.w_terminal_vel * target.velocity[0];
174 q_vec[x_idx(n, 3)] = -2.0 * config.w_terminal_vel * target.velocity[1];
175
176 let n_eq = 4 * (n + 1);
186 let n_soc_rows = 3 * n; let total_constraints = n_eq + n_soc_rows;
189
190 let mut a_rows = Vec::new();
191 let mut a_cols = Vec::new();
192 let mut a_vals = Vec::new();
193 let mut b_vec = vec![0.0; total_constraints];
194
195 for k in 0..2 {
198 let row = k;
199 a_rows.push(row);
200 a_cols.push(x_idx(0, k));
201 a_vals.push(1.0);
202 b_vec[row] = initial.position[k];
203 }
204 for k in 0..2 {
205 let row = 2 + k;
206 a_rows.push(row);
207 a_cols.push(x_idx(0, k + 2));
208 a_vals.push(1.0);
209 b_vec[row] = initial.velocity[k];
210 }
211
212 for t in 0..n {
231 let base_row = 4 + 4 * t;
232
233 for k in 0..4 {
235 a_rows.push(base_row + k);
236 a_cols.push(x_idx(t + 1, k));
237 a_vals.push(-1.0);
238 }
239
240 a_rows.push(base_row);
243 a_cols.push(x_idx(t, 0));
244 a_vals.push(1.0);
245 a_rows.push(base_row);
246 a_cols.push(x_idx(t, 2));
247 a_vals.push(dt);
248
249 a_rows.push(base_row + 1);
251 a_cols.push(x_idx(t, 1));
252 a_vals.push(1.0);
253 a_rows.push(base_row + 1);
254 a_cols.push(x_idx(t, 3));
255 a_vals.push(dt);
256
257 a_rows.push(base_row + 2);
259 a_cols.push(x_idx(t, 2));
260 a_vals.push(1.0);
261
262 a_rows.push(base_row + 3);
264 a_cols.push(x_idx(t, 3));
265 a_vals.push(1.0);
266
267 a_rows.push(base_row + 2);
270 a_cols.push(u_idx(t, 0));
271 a_vals.push(dt / m);
272
273 a_rows.push(base_row + 3);
275 a_cols.push(u_idx(t, 1));
276 a_vals.push(dt / m);
277
278 b_vec[base_row + 3] = dt * g;
280 }
281
282 for t in 0..n {
298 let soc_base = n_eq + 3 * t;
299
300 b_vec[soc_base] = t_max;
302
303 a_rows.push(soc_base + 1);
305 a_cols.push(u_idx(t, 0));
306 a_vals.push(-1.0);
307
308 a_rows.push(soc_base + 2);
310 a_cols.push(u_idx(t, 1));
311 a_vals.push(-1.0);
312 }
313
314 let a_csc = CscMatrix::new_from_triplets(total_constraints, n_vars, a_rows, a_cols, a_vals);
315
316 let mut cones: Vec<SupportedConeT<f64>> = Vec::new();
318 cones.push(SupportedConeT::ZeroConeT(n_eq));
319 for _ in 0..n {
320 cones.push(SupportedConeT::SecondOrderConeT(3));
321 }
322
323 let settings: DefaultSettings<f64> = DefaultSettingsBuilder::default()
324 .verbose(false)
325 .max_iter(500)
326 .build()
327 .unwrap();
328
329 let mut solver = DefaultSolver::new(&p, &q_vec, &a_csc, &b_vec, &cones, settings).ok()?;
330 solver.solve();
331
332 if solver.solution.status != SolverStatus::Solved {
333 return None;
334 }
335
336 let z = &solver.solution.x;
337
338 let mut positions = Vec::with_capacity(n + 1);
340 let mut velocities = Vec::with_capacity(n + 1);
341 for t in 0..=n {
342 positions.push(Vector2::new(z[x_idx(t, 0)], z[x_idx(t, 1)]));
343 velocities.push(Vector2::new(z[x_idx(t, 2)], z[x_idx(t, 3)]));
344 }
345
346 let mut thrusts = Vec::with_capacity(n);
347 let mut fuel_cost = 0.0;
348 for t in 0..n {
349 let thrust = Vector2::new(z[u_idx(t, 0)], z[u_idx(t, 1)]);
350 fuel_cost += thrust.norm() * dt;
351 thrusts.push(thrust);
352 }
353
354 Some(RocketTrajectory {
355 positions,
356 velocities,
357 thrusts,
358 fuel_cost,
359 })
360}
361
362pub fn simulate_freefall(
364 initial: &RocketState,
365 n_steps: usize,
366 dt: f64,
367 gravity: f64,
368) -> (Vec<Vector2<f64>>, Vec<Vector2<f64>>) {
369 let mut positions = Vec::with_capacity(n_steps + 1);
370 let mut velocities = Vec::with_capacity(n_steps + 1);
371
372 let mut pos = initial.position;
373 let mut vel = initial.velocity;
374
375 positions.push(pos);
376 velocities.push(vel);
377
378 for _ in 0..n_steps {
379 pos += vel * dt;
380 vel += Vector2::new(0.0, -gravity) * dt;
381 positions.push(pos);
382 velocities.push(vel);
383 }
384
385 (positions, velocities)
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
394 fn test_freefall_no_thrust() {
395 let initial = RocketState {
396 position: Vector2::new(0.0, 100.0),
397 velocity: Vector2::new(0.0, 0.0),
398 };
399
400 let dt = 0.1;
401 let n_steps = 50;
402 let (positions, velocities) = simulate_freefall(&initial, n_steps, dt, GRAVITY);
403
404 assert_eq!(positions.len(), n_steps + 1);
405 assert_eq!(velocities.len(), n_steps + 1);
406
407 assert!((positions[0] - initial.position).norm() < 1e-10);
409 assert!((velocities[0] - initial.velocity).norm() < 1e-10);
410
411 let t_final = n_steps as f64 * dt; let expected_y = 100.0 - 0.5 * GRAVITY * t_final * t_final;
414 let expected_vy = -GRAVITY * t_final;
415
416 assert!(
418 (positions[n_steps][1] - expected_y).abs() < 3.0,
419 "Final y: {}, expected: {}",
420 positions[n_steps][1],
421 expected_y
422 );
423 assert!(
424 (velocities[n_steps][1] - expected_vy).abs() < 1.0,
425 "Final vy: {}, expected: {}",
426 velocities[n_steps][1],
427 expected_vy
428 );
429
430 assert!((positions[n_steps][0]).abs() < 1e-10);
432 }
433
434 #[test]
436 fn test_vertical_landing() {
437 let initial = RocketState {
438 position: Vector2::new(0.0, 50.0),
439 velocity: Vector2::new(0.0, -5.0),
440 };
441 let target = LandingTarget::default(); let config = RocketLandingConfig {
444 n_steps: 60,
445 dt: 0.1,
446 thrust_max: 20.0,
447 mass: 1.0,
448 gravity: GRAVITY,
449 w_terminal_pos: 1e4,
450 w_terminal_vel: 1e4,
451 w_thrust: 1.0,
452 };
453
454 let result = solve_landing_trajectory(&initial, &target, &config);
455 assert!(result.is_some(), "Solver should find a solution");
456
457 let traj = result.unwrap();
458 assert_eq!(traj.positions.len(), config.n_steps + 1);
459 assert_eq!(traj.thrusts.len(), config.n_steps);
460
461 let final_pos = traj.positions.last().unwrap();
463 let final_vel = traj.velocities.last().unwrap();
464
465 assert!(
466 final_pos.norm() < 1.0,
467 "Final position should be near origin, got: {:?}",
468 final_pos
469 );
470 assert!(
471 final_vel.norm() < 1.0,
472 "Final velocity should be near zero, got: {:?}",
473 final_vel
474 );
475
476 for (t, thrust) in traj.thrusts.iter().enumerate() {
478 assert!(
479 thrust.norm() <= config.thrust_max + 1e-6,
480 "Thrust at step {} exceeds limit: {}",
481 t,
482 thrust.norm()
483 );
484 }
485
486 assert!(traj.fuel_cost > 0.0, "Fuel cost should be positive");
488 }
489
490 #[test]
492 fn test_angled_approach_landing() {
493 let initial = RocketState {
494 position: Vector2::new(30.0, 80.0),
495 velocity: Vector2::new(-5.0, -10.0),
496 };
497 let target = LandingTarget::default();
498
499 let config = RocketLandingConfig {
500 n_steps: 80,
501 dt: 0.1,
502 thrust_max: 30.0,
503 mass: 1.0,
504 gravity: GRAVITY,
505 w_terminal_pos: 1e4,
506 w_terminal_vel: 1e4,
507 w_thrust: 1.0,
508 };
509
510 let result = solve_landing_trajectory(&initial, &target, &config);
511 assert!(result.is_some(), "Solver should find a solution");
512
513 let traj = result.unwrap();
514
515 let final_pos = traj.positions.last().unwrap();
516 let final_vel = traj.velocities.last().unwrap();
517
518 assert!(
519 final_pos.norm() < 2.0,
520 "Final position should be near origin, got: {:?}",
521 final_pos
522 );
523 assert!(
524 final_vel.norm() < 2.0,
525 "Final velocity should be near zero, got: {:?}",
526 final_vel
527 );
528
529 for (t, thrust) in traj.thrusts.iter().enumerate() {
531 assert!(
532 thrust.norm() <= config.thrust_max + 1e-6,
533 "Thrust at step {} exceeds limit: {}",
534 t,
535 thrust.norm()
536 );
537 }
538
539 let has_horizontal_thrust = traj.thrusts.iter().any(|t| t[0].abs() > 0.1);
541 assert!(
542 has_horizontal_thrust,
543 "Angled approach should use horizontal thrust"
544 );
545
546 for (t, pos) in traj.positions.iter().enumerate() {
549 assert!(
550 pos[1] > -5.0,
551 "Position y at step {} is too negative: {}",
552 t,
553 pos[1]
554 );
555 }
556 }
557
558 #[test]
560 fn test_insufficient_thrust() {
561 let initial = RocketState {
562 position: Vector2::new(0.0, 100.0),
563 velocity: Vector2::new(0.0, -50.0), };
565 let target = LandingTarget::default();
566
567 let config = RocketLandingConfig {
569 n_steps: 10,
570 dt: 0.05,
571 thrust_max: 5.0, mass: 1.0,
573 gravity: GRAVITY,
574 w_terminal_pos: 1e4,
575 w_terminal_vel: 1e4,
576 w_thrust: 1.0,
577 };
578
579 let result = solve_landing_trajectory(&initial, &target, &config);
582 assert!(
583 result.is_some(),
584 "Solver should still return a solution (soft constraints)"
585 );
586
587 let traj = result.unwrap();
588 let final_pos = traj.positions.last().unwrap();
590 assert!(
591 final_pos.norm() > 5.0,
592 "With insufficient thrust/time, landing should not be precise: {:?}",
593 final_pos
594 );
595 }
596}