1use nalgebra::{Matrix3, Matrix6, Vector3, Vector6};
21
22const GRAVITY: f64 = 9.81;
24
25#[derive(Debug, Clone)]
31pub struct QuinticCoeffs {
32 pub c: [f64; 6],
34}
35
36impl QuinticCoeffs {
37 pub fn position(&self, t: f64) -> f64 {
39 let c = &self.c;
40 c[0] * t.powi(5) + c[1] * t.powi(4) + c[2] * t.powi(3) + c[3] * t.powi(2) + c[4] * t + c[5]
41 }
42
43 pub fn velocity(&self, t: f64) -> f64 {
45 let c = &self.c;
46 5.0 * c[0] * t.powi(4)
47 + 4.0 * c[1] * t.powi(3)
48 + 3.0 * c[2] * t.powi(2)
49 + 2.0 * c[3] * t
50 + c[4]
51 }
52
53 pub fn acceleration(&self, t: f64) -> f64 {
55 let c = &self.c;
56 20.0 * c[0] * t.powi(3) + 12.0 * c[1] * t.powi(2) + 6.0 * c[2] * t + 2.0 * c[3]
57 }
58
59 pub fn jerk(&self, t: f64) -> f64 {
61 let c = &self.c;
62 60.0 * c[0] * t.powi(2) + 24.0 * c[1] * t + 6.0 * c[2]
63 }
64
65 pub fn snap(&self, t: f64) -> f64 {
67 let c = &self.c;
68 120.0 * c[0] * t + 24.0 * c[1]
69 }
70}
71
72#[derive(Debug, Clone)]
74pub struct TrajectorySegment {
75 pub x: QuinticCoeffs,
76 pub y: QuinticCoeffs,
77 pub z: QuinticCoeffs,
78}
79
80impl TrajectorySegment {
81 pub fn jerk(&self, t: f64) -> Vector3<f64> {
83 Vector3::new(self.x.jerk(t), self.y.jerk(t), self.z.jerk(t))
84 }
85
86 pub fn snap(&self, t: f64) -> Vector3<f64> {
88 Vector3::new(self.x.snap(t), self.y.snap(t), self.z.snap(t))
89 }
90}
91
92struct AxisBoundary {
94 start_pos: f64,
95 end_pos: f64,
96 start_vel: f64,
97 end_vel: f64,
98 start_acc: f64,
99 end_acc: f64,
100}
101
102fn solve_quintic_axis(b: &AxisBoundary, duration: f64) -> QuinticCoeffs {
106 let big_t = duration;
107 let t2 = big_t * big_t;
108 let t3 = t2 * big_t;
109 let t4 = t3 * big_t;
110 let t5 = t4 * big_t;
111
112 #[rustfmt::skip]
120 let a = Matrix6::new(
121 0.0, 0.0, 0.0, 0.0, 0.0, 1.0,
122 t5, t4, t3, t2, big_t, 1.0,
123 0.0, 0.0, 0.0, 0.0, 1.0, 0.0,
124 5.0*t4, 4.0*t3, 3.0*t2, 2.0*big_t, 1.0, 0.0,
125 0.0, 0.0, 0.0, 2.0, 0.0, 0.0,
126 20.0*t3, 12.0*t2, 6.0*big_t, 2.0, 0.0, 0.0,
127 );
128
129 let rhs = Vector6::new(
130 b.start_pos,
131 b.end_pos,
132 b.start_vel,
133 b.end_vel,
134 b.start_acc,
135 b.end_acc,
136 );
137
138 let decomp = a.lu();
139 let coeffs = decomp
140 .solve(&rhs)
141 .expect("Quintic matrix should be invertible");
142
143 QuinticCoeffs {
144 c: [
145 coeffs[0], coeffs[1], coeffs[2], coeffs[3], coeffs[4], coeffs[5],
146 ],
147 }
148}
149
150pub fn generate_trajectory_segment(
154 start: &Vector3<f64>,
155 end: &Vector3<f64>,
156 duration: f64,
157) -> TrajectorySegment {
158 generate_trajectory_segment_full(
159 start,
160 end,
161 &Vector3::zeros(),
162 &Vector3::zeros(),
163 &Vector3::zeros(),
164 &Vector3::zeros(),
165 duration,
166 )
167}
168
169pub fn generate_trajectory_segment_full(
171 start_pos: &Vector3<f64>,
172 end_pos: &Vector3<f64>,
173 start_vel: &Vector3<f64>,
174 end_vel: &Vector3<f64>,
175 start_acc: &Vector3<f64>,
176 end_acc: &Vector3<f64>,
177 duration: f64,
178) -> TrajectorySegment {
179 let solve = |i: usize| {
180 solve_quintic_axis(
181 &AxisBoundary {
182 start_pos: start_pos[i],
183 end_pos: end_pos[i],
184 start_vel: start_vel[i],
185 end_vel: end_vel[i],
186 start_acc: start_acc[i],
187 end_acc: end_acc[i],
188 },
189 duration,
190 )
191 };
192 TrajectorySegment {
193 x: solve(0),
194 y: solve(1),
195 z: solve(2),
196 }
197}
198
199pub fn generate_waypoint_trajectory(
203 waypoints: &[Vector3<f64>],
204 segment_duration: f64,
205) -> Vec<TrajectorySegment> {
206 let segment_durations = vec![segment_duration; waypoints.len()];
207 generate_waypoint_trajectory_with_durations(waypoints, &segment_durations)
208}
209
210pub fn generate_waypoint_trajectory_with_durations(
213 waypoints: &[Vector3<f64>],
214 segment_durations: &[f64],
215) -> Vec<TrajectorySegment> {
216 let n = waypoints.len();
217 assert!(n >= 2, "Need at least 2 waypoints");
218 assert_eq!(
219 n,
220 segment_durations.len(),
221 "Need one segment duration per waypoint-to-waypoint segment"
222 );
223 (0..n)
224 .map(|i| {
225 generate_trajectory_segment(
226 &waypoints[i],
227 &waypoints[(i + 1) % n],
228 segment_durations[i],
229 )
230 })
231 .collect()
232}
233
234pub fn sample_trajectory_segments(
236 segments: &[TrajectorySegment],
237 segment_durations: &[f64],
238 dt: f64,
239) -> Vec<DesiredState> {
240 assert!(dt > 0.0, "dt must be positive");
241 assert_eq!(
242 segments.len(),
243 segment_durations.len(),
244 "Need one duration per trajectory segment"
245 );
246
247 let mut samples = Vec::new();
248 for (segment, duration) in segments.iter().zip(segment_durations.iter().copied()) {
249 let mut t = 0.0;
250 while t <= duration {
251 samples.push(DesiredState::from_segment(segment, t));
252 t += dt;
253 }
254 }
255 samples
256}
257
258#[derive(Debug, Clone)]
264pub struct QuadrotorParams {
265 pub mass: f64,
267 pub ixx: f64,
269 pub iyy: f64,
271 pub izz: f64,
273 pub gravity: f64,
275}
276
277impl Default for QuadrotorParams {
278 fn default() -> Self {
279 Self {
280 mass: 0.2,
281 ixx: 1.0,
282 iyy: 1.0,
283 izz: 1.0,
284 gravity: GRAVITY,
285 }
286 }
287}
288
289#[derive(Debug, Clone)]
291pub struct ControllerGains {
292 pub kp_pos: Vector3<f64>,
294 pub kd_pos: Vector3<f64>,
296 pub kp_att: Vector3<f64>,
298}
299
300impl Default for ControllerGains {
301 fn default() -> Self {
302 Self {
303 kp_pos: Vector3::new(1.0, 1.0, 1.0),
304 kd_pos: Vector3::new(10.0, 10.0, 1.0),
305 kp_att: Vector3::new(25.0, 25.0, 25.0),
306 }
307 }
308}
309
310#[derive(Debug, Clone)]
312pub struct QuadrotorState {
313 pub position: Vector3<f64>,
315 pub velocity: Vector3<f64>,
317 pub orientation: Vector3<f64>,
319 pub angular_velocity: Vector3<f64>,
321}
322
323impl QuadrotorState {
324 pub fn new(position: Vector3<f64>) -> Self {
326 Self {
327 position,
328 velocity: Vector3::zeros(),
329 orientation: Vector3::zeros(),
330 angular_velocity: Vector3::zeros(),
331 }
332 }
333}
334
335#[derive(Debug, Clone)]
337pub struct DesiredState {
338 pub position: Vector3<f64>,
339 pub velocity: Vector3<f64>,
340 pub acceleration: Vector3<f64>,
341 pub yaw: f64,
342}
343
344impl DesiredState {
345 pub fn from_segment(seg: &TrajectorySegment, t: f64) -> Self {
347 Self {
348 position: Vector3::new(seg.x.position(t), seg.y.position(t), seg.z.position(t)),
349 velocity: Vector3::new(seg.x.velocity(t), seg.y.velocity(t), seg.z.velocity(t)),
350 acceleration: Vector3::new(
351 seg.x.acceleration(t),
352 seg.y.acceleration(t),
353 seg.z.acceleration(t),
354 ),
355 yaw: 0.0,
356 }
357 }
358}
359
360#[derive(Debug, Clone)]
362pub struct ControlOutput {
363 pub thrust: f64,
365 pub roll_torque: f64,
367 pub pitch_torque: f64,
369 pub yaw_torque: f64,
371}
372
373pub fn rotation_matrix(roll: f64, pitch: f64, yaw: f64) -> Matrix3<f64> {
375 let (sr, cr) = roll.sin_cos();
376 let (sp, cp) = pitch.sin_cos();
377 let (sy, cy) = yaw.sin_cos();
378
379 Matrix3::new(
380 cy * cp,
381 -sy * cr + cy * sp * sr,
382 sy * sr + cy * sp * cr,
383 sy * cp,
384 cy * cr + sy * sp * sr,
385 -cy * sr + sy * sp * cr,
386 -sp,
387 cp * sr,
388 cp * cr,
389 )
390}
391
392pub fn compute_control(
400 state: &QuadrotorState,
401 desired: &DesiredState,
402 params: &QuadrotorParams,
403 gains: &ControllerGains,
404) -> ControlOutput {
405 let g = params.gravity;
406 let m = params.mass;
407
408 let thrust = m
410 * (g + desired.acceleration.z
411 + gains.kp_pos.z * (desired.position.z - state.position.z)
412 + gains.kd_pos.z * (desired.velocity.z - state.velocity.z));
413
414 let des_yaw = desired.yaw;
415 let (sy, cy) = des_yaw.sin_cos();
416
417 let des_roll = (desired.acceleration.x * sy - desired.acceleration.y * cy) / g;
419 let des_pitch = (desired.acceleration.x * cy - desired.acceleration.y * sy) / g;
420
421 let roll_torque = gains.kp_att.x * (des_roll - state.orientation.x);
422 let pitch_torque = gains.kp_att.y * (des_pitch - state.orientation.y);
423 let yaw_torque = gains.kp_att.z * (des_yaw - state.orientation.z);
424
425 ControlOutput {
426 thrust,
427 roll_torque,
428 pitch_torque,
429 yaw_torque,
430 }
431}
432
433pub fn step_dynamics(
435 state: &mut QuadrotorState,
436 control: &ControlOutput,
437 params: &QuadrotorParams,
438 dt: f64,
439) {
440 let m = params.mass;
441 let g = params.gravity;
442
443 state.angular_velocity.x += control.roll_torque * dt / params.ixx;
445 state.angular_velocity.y += control.pitch_torque * dt / params.iyy;
446 state.angular_velocity.z += control.yaw_torque * dt / params.izz;
447
448 state.orientation += state.angular_velocity * dt;
449
450 let r = rotation_matrix(
452 state.orientation.x,
453 state.orientation.y,
454 state.orientation.z,
455 );
456 let thrust_body = Vector3::new(0.0, 0.0, control.thrust);
457 let gravity_world = Vector3::new(0.0, 0.0, m * g);
458 let acc = (r * thrust_body - gravity_world) / m;
459
460 state.velocity += acc * dt;
461 state.position += state.velocity * dt;
462}
463
464#[derive(Debug, Clone)]
466pub struct SimulationRecord {
467 pub positions: Vec<Vector3<f64>>,
469 pub orientations: Vec<Vector3<f64>>,
471 pub controls: Vec<ControlOutput>,
473}
474
475#[derive(Debug, Clone)]
477pub struct SimulationConfig {
478 pub dt: f64,
480 pub segment_duration: f64,
482 pub n_loops: usize,
484 pub params: QuadrotorParams,
486 pub gains: ControllerGains,
488}
489
490impl Default for SimulationConfig {
491 fn default() -> Self {
492 Self {
493 dt: 0.1,
494 segment_duration: 5.0,
495 n_loops: 2,
496 params: QuadrotorParams::default(),
497 gains: ControllerGains::default(),
498 }
499 }
500}
501
502pub fn simulate_desired_states(
504 start_position: Vector3<f64>,
505 desired_states: &[DesiredState],
506 config: &SimulationConfig,
507) -> SimulationRecord {
508 let mut state = QuadrotorState::new(start_position);
509 let mut record = SimulationRecord {
510 positions: vec![state.position],
511 orientations: vec![state.orientation],
512 controls: Vec::with_capacity(desired_states.len()),
513 };
514
515 for desired in desired_states {
516 let control = compute_control(&state, desired, &config.params, &config.gains);
517 step_dynamics(&mut state, &control, &config.params, config.dt);
518
519 record.positions.push(state.position);
520 record.orientations.push(state.orientation);
521 record.controls.push(control);
522 }
523
524 record
525}
526
527pub fn simulate_trajectory_following(
534 waypoints: &[Vector3<f64>],
535 config: &SimulationConfig,
536) -> SimulationRecord {
537 let segment_durations = vec![config.segment_duration; waypoints.len()];
538 let segments = generate_waypoint_trajectory_with_durations(waypoints, &segment_durations);
539 let mut desired_states = Vec::new();
540 for _ in 0..config.n_loops {
541 desired_states.extend(sample_trajectory_segments(
542 &segments,
543 &segment_durations,
544 config.dt,
545 ));
546 }
547 simulate_desired_states(waypoints[0], &desired_states, config)
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553
554 const EPSILON: f64 = 1e-9;
555
556 #[test]
561 fn test_quintic_boundary_conditions() {
562 let start = Vector3::new(0.0, 0.0, 5.0);
563 let end = Vector3::new(10.0, 0.0, 5.0);
564 let duration = 5.0;
565 let seg = generate_trajectory_segment(&start, &end, duration);
566
567 assert!((seg.x.position(0.0) - 0.0).abs() < EPSILON);
569 assert!((seg.y.position(0.0) - 0.0).abs() < EPSILON);
570 assert!((seg.z.position(0.0) - 5.0).abs() < EPSILON);
571
572 assert!((seg.x.position(duration) - 10.0).abs() < EPSILON);
574 assert!((seg.y.position(duration) - 0.0).abs() < EPSILON);
575 assert!((seg.z.position(duration) - 5.0).abs() < EPSILON);
576
577 assert!(seg.x.velocity(0.0).abs() < EPSILON);
579 assert!(seg.x.velocity(duration).abs() < EPSILON);
580
581 assert!(seg.x.acceleration(0.0).abs() < EPSILON);
583 assert!(seg.x.acceleration(duration).abs() < EPSILON);
584 }
585
586 #[test]
587 fn test_quintic_constant_z() {
588 let seg = generate_trajectory_segment(
590 &Vector3::new(0.0, 0.0, 5.0),
591 &Vector3::new(10.0, 10.0, 5.0),
592 5.0,
593 );
594 for i in 0..=50 {
595 let t = i as f64 * 0.1;
596 assert!(
597 (seg.z.position(t) - 5.0).abs() < EPSILON,
598 "z({}) = {} != 5.0",
599 t,
600 seg.z.position(t)
601 );
602 }
603 }
604
605 #[test]
606 fn test_quintic_with_initial_velocity() {
607 let start_pos = Vector3::new(0.0, 0.0, 0.0);
608 let end_pos = Vector3::new(10.0, 0.0, 0.0);
609 let start_vel = Vector3::new(2.0, 0.0, 0.0);
610 let end_vel = Vector3::new(0.0, 0.0, 0.0);
611 let duration = 5.0;
612
613 let seg = generate_trajectory_segment_full(
614 &start_pos,
615 &end_pos,
616 &start_vel,
617 &end_vel,
618 &Vector3::zeros(),
619 &Vector3::zeros(),
620 duration,
621 );
622
623 assert!((seg.x.velocity(0.0) - 2.0).abs() < EPSILON);
624 assert!(seg.x.velocity(duration).abs() < EPSILON);
625 }
626
627 #[test]
628 fn test_waypoint_trajectory_generation() {
629 let waypoints = vec![
630 Vector3::new(-5.0, -5.0, 5.0),
631 Vector3::new(5.0, -5.0, 5.0),
632 Vector3::new(5.0, 5.0, 5.0),
633 Vector3::new(-5.0, 5.0, 5.0),
634 ];
635 let segments = generate_waypoint_trajectory(&waypoints, 5.0);
636 assert_eq!(segments.len(), 4);
637
638 for (i, seg) in segments.iter().enumerate() {
640 let next = (i + 1) % waypoints.len();
641 assert!(
642 (seg.x.position(0.0) - waypoints[i].x).abs() < EPSILON,
643 "Segment {} start x mismatch",
644 i
645 );
646 assert!(
647 (seg.x.position(5.0) - waypoints[next].x).abs() < EPSILON,
648 "Segment {} end x mismatch",
649 i
650 );
651 }
652 }
653
654 #[test]
655 fn test_waypoint_trajectory_generation_with_durations() {
656 let waypoints = vec![
657 Vector3::new(0.0, 0.0, 1.0),
658 Vector3::new(4.0, 0.0, 2.0),
659 Vector3::new(4.0, 3.0, 3.0),
660 ];
661 let durations = vec![2.0, 3.0, 4.0];
662
663 let segments = generate_waypoint_trajectory_with_durations(&waypoints, &durations);
664 assert_eq!(segments.len(), 3);
665 assert!((segments[0].x.position(2.0) - 4.0).abs() < EPSILON);
666 assert!((segments[1].y.position(3.0) - 3.0).abs() < EPSILON);
667 assert!((segments[2].z.position(4.0) - 1.0).abs() < EPSILON);
668 }
669
670 #[test]
671 fn test_sample_trajectory_segments_counts() {
672 let waypoints = vec![Vector3::new(0.0, 0.0, 0.0), Vector3::new(2.0, 0.0, 0.0)];
673 let segments = generate_waypoint_trajectory_with_durations(&waypoints, &[1.0, 2.0]);
674 let samples = sample_trajectory_segments(&segments, &[1.0, 2.0], 0.5);
675
676 assert_eq!(samples.len(), 8);
677 assert!((samples[0].position.x - 0.0).abs() < EPSILON);
678 assert!((samples[2].position.x - 2.0).abs() < 1e-6);
679 }
680
681 #[test]
686 fn test_rotation_identity() {
687 let r = rotation_matrix(0.0, 0.0, 0.0);
688 let identity = Matrix3::identity();
689 assert!((r - identity).norm() < EPSILON);
690 }
691
692 #[test]
693 fn test_rotation_orthogonal() {
694 let r = rotation_matrix(0.3, 0.5, 1.0);
695 let should_be_identity = r * r.transpose();
696 assert!(
697 (should_be_identity - Matrix3::identity()).norm() < 1e-12,
698 "Rotation matrix not orthogonal"
699 );
700 }
701
702 #[test]
707 fn test_hover_control() {
708 let params = QuadrotorParams::default();
710 let gains = ControllerGains::default();
711 let pos = Vector3::new(0.0, 0.0, 5.0);
712 let state = QuadrotorState::new(pos);
713 let desired = DesiredState {
714 position: pos,
715 velocity: Vector3::zeros(),
716 acceleration: Vector3::zeros(),
717 yaw: 0.0,
718 };
719
720 let ctrl = compute_control(&state, &desired, ¶ms, &gains);
721 let expected_thrust = params.mass * params.gravity;
722 assert!(
723 (ctrl.thrust - expected_thrust).abs() < EPSILON,
724 "Hover thrust should be m*g, got {}",
725 ctrl.thrust
726 );
727 assert!(ctrl.roll_torque.abs() < EPSILON);
728 assert!(ctrl.pitch_torque.abs() < EPSILON);
729 assert!(ctrl.yaw_torque.abs() < EPSILON);
730 }
731
732 #[test]
733 fn test_control_z_error_increases_thrust() {
734 let params = QuadrotorParams::default();
735 let gains = ControllerGains::default();
736 let state = QuadrotorState::new(Vector3::new(0.0, 0.0, 4.0));
737 let desired = DesiredState {
738 position: Vector3::new(0.0, 0.0, 5.0),
739 velocity: Vector3::zeros(),
740 acceleration: Vector3::zeros(),
741 yaw: 0.0,
742 };
743
744 let ctrl = compute_control(&state, &desired, ¶ms, &gains);
745 let hover_thrust = params.mass * params.gravity;
746 assert!(
747 ctrl.thrust > hover_thrust,
748 "Below desired z => thrust should exceed hover"
749 );
750 }
751
752 #[test]
757 fn test_dynamics_freefall() {
758 let params = QuadrotorParams::default();
759 let mut state = QuadrotorState::new(Vector3::new(0.0, 0.0, 10.0));
760 let zero_control = ControlOutput {
761 thrust: 0.0,
762 roll_torque: 0.0,
763 pitch_torque: 0.0,
764 yaw_torque: 0.0,
765 };
766
767 let dt = 0.01;
768 for _ in 0..100 {
769 step_dynamics(&mut state, &zero_control, ¶ms, dt);
770 }
771
772 let expected_z = 10.0 - 0.5 * GRAVITY * 1.0;
774 assert!(
775 (state.position.z - expected_z).abs() < 0.5,
776 "Freefall z: {}, expected ~{}",
777 state.position.z,
778 expected_z
779 );
780 assert!(
782 (state.velocity.z - (-GRAVITY)).abs() < 0.2,
783 "Freefall vz: {}, expected ~{}",
784 state.velocity.z,
785 -GRAVITY
786 );
787 assert!(state.position.x.abs() < EPSILON);
789 assert!(state.position.y.abs() < EPSILON);
790 }
791
792 #[test]
793 fn test_dynamics_hover() {
794 let params = QuadrotorParams::default();
795 let mut state = QuadrotorState::new(Vector3::new(0.0, 0.0, 5.0));
796 let hover_control = ControlOutput {
797 thrust: params.mass * params.gravity,
798 roll_torque: 0.0,
799 pitch_torque: 0.0,
800 yaw_torque: 0.0,
801 };
802
803 let dt = 0.01;
804 for _ in 0..1000 {
805 step_dynamics(&mut state, &hover_control, ¶ms, dt);
806 }
807
808 assert!(
810 (state.position.z - 5.0).abs() < 0.01,
811 "Hover z drifted to {}",
812 state.position.z
813 );
814 assert!(state.velocity.z.abs() < 0.01);
815 }
816
817 #[test]
822 fn test_simulate_square_trajectory() {
823 let waypoints = vec![
824 Vector3::new(-5.0, -5.0, 5.0),
825 Vector3::new(5.0, -5.0, 5.0),
826 Vector3::new(5.0, 5.0, 5.0),
827 Vector3::new(-5.0, 5.0, 5.0),
828 ];
829
830 let config = SimulationConfig {
831 n_loops: 2,
832 ..Default::default()
833 };
834
835 let record = simulate_trajectory_following(&waypoints, &config);
836
837 assert!(record.positions.len() > 100);
839 assert_eq!(record.positions.len(), record.orientations.len());
840 assert_eq!(record.controls.len(), record.positions.len() - 1);
842
843 let z_values: Vec<f64> = record.positions.iter().map(|p| p.z).collect();
845 let z_min = z_values.iter().cloned().fold(f64::INFINITY, f64::min);
846 let z_max = z_values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
847 assert!(
848 z_min > 2.0 && z_max < 8.0,
849 "Altitude should stay near 5.0, got range [{}, {}]",
850 z_min,
851 z_max
852 );
853 }
854
855 #[test]
856 fn test_simulate_ascending_trajectory() {
857 let waypoints = vec![Vector3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 10.0)];
858
859 let config = SimulationConfig {
860 n_loops: 1,
861 segment_duration: 5.0,
862 ..Default::default()
863 };
864
865 let record = simulate_trajectory_following(&waypoints, &config);
866
867 let final_z = record.positions.last().unwrap().z;
869 assert!(
870 final_z > 0.0,
871 "Drone should have ascended, final z = {}",
872 final_z
873 );
874 }
875
876 #[test]
877 fn test_simulation_record_consistency() {
878 let waypoints = vec![
879 Vector3::new(0.0, 0.0, 5.0),
880 Vector3::new(5.0, 0.0, 5.0),
881 Vector3::new(5.0, 5.0, 5.0),
882 ];
883
884 let config = SimulationConfig {
885 dt: 0.05,
886 segment_duration: 3.0,
887 n_loops: 1,
888 ..Default::default()
889 };
890
891 let record = simulate_trajectory_following(&waypoints, &config);
892
893 let start = &record.positions[0];
895 assert!((start.x - 0.0).abs() < EPSILON);
896 assert!((start.y - 0.0).abs() < EPSILON);
897 assert!((start.z - 5.0).abs() < EPSILON);
898
899 for (i, p) in record.positions.iter().enumerate() {
901 assert!(
902 p.x.is_finite() && p.y.is_finite() && p.z.is_finite(),
903 "Non-finite position at step {}: {:?}",
904 i,
905 p
906 );
907 }
908 }
909
910 #[test]
911 fn test_simulate_desired_states_hover() {
912 let config = SimulationConfig {
913 dt: 0.05,
914 ..Default::default()
915 };
916 let desired_states = vec![
917 DesiredState {
918 position: Vector3::new(0.0, 0.0, 3.0),
919 velocity: Vector3::zeros(),
920 acceleration: Vector3::zeros(),
921 yaw: 0.0,
922 };
923 20
924 ];
925
926 let record = simulate_desired_states(Vector3::new(0.0, 0.0, 3.0), &desired_states, &config);
927 let final_z = record.positions.last().unwrap().z;
928 assert!((final_z - 3.0).abs() < 0.1);
929 assert_eq!(record.controls.len(), desired_states.len());
930 }
931}