uav/
lib.rs

1use fast_ode;
2
3#[derive(Clone, Copy, Debug)]
4pub struct State {
5    pub position_x: f64,
6    pub position_y: f64,
7    pub position_z: f64,
8    pub velocity_x: f64,
9    pub velocity_y: f64,
10    pub velocity_z: f64,
11    pub roll: f64,
12    pub pitch: f64,
13    pub yaw: f64,
14    pub roll_rate: f64,
15    pub pitch_rate: f64,
16    pub yaw_rate: f64,
17}
18
19#[derive(Clone, Copy)]
20pub struct Consts {
21    pub g: f64,
22    pub mass: f64,
23    pub ixx: f64,
24    pub iyy: f64,
25    pub izz: f64,
26}
27
28#[derive(Clone, Copy)]
29pub struct Forces {
30    pub x: f64,
31    pub y: f64,
32    pub z: f64,
33}
34
35#[derive(Clone, Copy)]
36pub struct Torques {
37    pub x: f64,
38    pub y: f64,
39    pub z: f64,
40}
41
42impl State {
43    pub fn to_array(&self) -> [f64; 12] {
44        [
45            self.position_x as f64,
46            self.position_y as f64,
47            self.position_z as f64,
48            self.velocity_x as f64,
49            self.velocity_y as f64,
50            self.velocity_z as f64,
51            self.roll as f64,
52            self.pitch as f64,
53            self.yaw as f64,
54            self.roll_rate as f64,
55            self.pitch_rate as f64,
56            self.yaw_rate as f64,
57        ]
58    }
59
60    pub fn from_array(arr: &[f64; 12]) -> Self {
61        State {
62            position_x: arr[0],
63            position_y: arr[1],
64            position_z: arr[2],
65            velocity_x: arr[3],
66            velocity_y: arr[4],
67            velocity_z: arr[5],
68            roll: arr[6],
69            pitch: arr[7],
70            yaw: arr[8],
71            roll_rate: arr[9],
72            pitch_rate: arr[10],
73            yaw_rate: arr[11],
74        }
75    }
76}
77
78pub struct DroneOde {
79    pub consts: Consts,
80    pub forces: Forces,
81    pub torques: Torques,
82}
83
84impl fast_ode::DifferentialEquation<12> for DroneOde {
85    fn ode_dot_y(&self, _t: f64, y: &fast_ode::Coord<12>) -> (fast_ode::Coord<12>, bool) {
86        let state = y.0;
87
88        // Extract state variables
89        let phi = state[6]; // roll
90        let theta = state[7]; // pitch
91        let psi = state[8]; // yaw
92        let p = state[9]; // roll rate
93        let q = state[10]; // pitch rate
94        let r = state[11]; // yaw rate
95
96        // Trigonometric values
97        let cos_phi = phi.cos();
98        let sin_phi = phi.sin();
99        let cos_theta = theta.cos();
100        let sin_theta = theta.sin();
101        let tan_theta = theta.tan();
102
103        // Transform body forces to inertial frame using rotation matrix
104        let cos_psi = psi.cos();
105        let sin_psi = psi.sin();
106
107        let fx_body = self.forces.x as f64;
108        let fy_body = self.forces.y as f64;
109        let fz_body = self.forces.z as f64;
110
111        // Full rotation matrix transformation (Body to Inertial)
112        // R = Rz(ψ) * Ry(θ) * Rx(φ)
113        let fx_inertial = (cos_theta * cos_psi) * fx_body
114            + (sin_phi * sin_theta * cos_psi - cos_phi * sin_psi) * fy_body
115            + (cos_phi * sin_theta * cos_psi + sin_phi * sin_psi) * fz_body;
116
117        let fy_inertial = (cos_theta * sin_psi) * fx_body
118            + (sin_phi * sin_theta * sin_psi + cos_phi * cos_psi) * fy_body
119            + (cos_phi * sin_theta * sin_psi - sin_phi * cos_psi) * fz_body;
120
121        let fz_inertial = (-sin_theta) * fx_body
122            + (sin_phi * cos_theta) * fy_body
123            + (cos_phi * cos_theta) * fz_body;
124
125        // State derivatives
126        let mut dot_y = [0.0; 12];
127
128        // Position derivatives (velocities)
129        dot_y[0] = state[3]; // ẋ = vx
130        dot_y[1] = state[4]; // ẏ = vy
131        dot_y[2] = state[5]; // ż = vz
132
133        // Velocity derivatives (accelerations)
134        dot_y[3] = fx_inertial / self.consts.mass as f64; // v̇x = Fx/m
135        dot_y[4] = fy_inertial / self.consts.mass as f64; // v̇y = Fy/m
136        dot_y[5] = fz_inertial / self.consts.mass as f64 - self.consts.g as f64; // v̇z = Fz/m - g
137
138        // Attitude derivatives (Euler angle rates)
139        dot_y[6] = p + q * sin_phi * tan_theta + r * cos_phi * tan_theta; // φ̇
140        dot_y[7] = q * cos_phi - r * sin_phi; // θ̇
141        dot_y[8] = if cos_theta.abs() > 1e-6 {
142            q * sin_phi / cos_theta + r * cos_phi / cos_theta // ψ̇
143        } else {
144            0.0 // Avoid singularity at θ = ±π/2
145        };
146
147        // Angular velocity derivatives (Euler's equations)
148        let ixx = self.consts.ixx as f64;
149        let iyy = self.consts.iyy as f64;
150        let izz = self.consts.izz as f64;
151
152        dot_y[9] = (self.torques.x as f64 + (iyy - izz) * q * r) / ixx; // ṗ
153        dot_y[10] = (self.torques.y as f64 + (izz - ixx) * r * p) / iyy; // q̇
154        dot_y[11] = (self.torques.z as f64 + (ixx - iyy) * p * q) / izz; // ṙ
155
156        (fast_ode::Coord(dot_y), true)
157    }
158}
159
160pub fn simulate_drone(
161    initial_state: State,
162    consts: Consts,
163    forces: Forces,
164    torques: Torques,
165    time_span: (f64, f64),
166    tolerance: f64,
167) -> Result<State, &'static str> {
168    let ode = DroneOde {
169        consts,
170        forces,
171        torques,
172    };
173
174    let initial_coord = fast_ode::Coord(initial_state.to_array());
175
176    let result = fast_ode::solve_ivp(
177        &ode,
178        time_span,
179        initial_coord,
180        |_, _| true,
181        tolerance,
182        tolerance * 10.0,
183    );
184
185    match result {
186        fast_ode::IvpResult::FinalTimeReached(final_coord) => Ok(State::from_array(&final_coord.0)),
187        _ => Err("Integration failed"),
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn test_hover_simulation() {
197        let initial_state = State {
198            position_x: 0.0,
199            position_y: 0.0,
200            position_z: 1.0,
201            velocity_x: 0.0,
202            velocity_y: 0.0,
203            velocity_z: 0.0,
204            roll: 0.0,
205            pitch: 0.0,
206            yaw: 0.0,
207            roll_rate: 0.0,
208            pitch_rate: 0.0,
209            yaw_rate: 0.0,
210        };
211
212        let consts = Consts {
213            g: 9.81,
214            mass: 1.0,
215            ixx: 0.1,
216            iyy: 0.1,
217            izz: 0.2,
218        };
219
220        // Hover forces (thrust equals weight)
221        let forces = Forces {
222            x: 0.0,
223            y: 0.0,
224            z: consts.mass * consts.g, // Thrust to counteract gravity
225        };
226
227        let torques = Torques {
228            x: 0.0,
229            y: 0.0,
230            z: 0.0,
231        };
232
233        let final_state =
234            simulate_drone(initial_state, consts, forces, torques, (0.0, 1.0), 1e-6).unwrap();
235
236        // In hover, position should remain approximately constant
237        assert!((final_state.position_z - 1.0).abs() < 0.1);
238        assert!(final_state.velocity_z.abs() < 0.1);
239    }
240
241    #[test]
242    fn test_free_fall() {
243        let initial_state = State {
244            position_x: 0.0,
245            position_y: 0.0,
246            position_z: 10.0,
247            velocity_x: 0.0,
248            velocity_y: 0.0,
249            velocity_z: 0.0,
250            roll: 0.0,
251            pitch: 0.0,
252            yaw: 0.0,
253            roll_rate: 0.0,
254            pitch_rate: 0.0,
255            yaw_rate: 0.0,
256        };
257
258        let consts = Consts {
259            g: 9.81,
260            mass: 1.0,
261            ixx: 0.1,
262            iyy: 0.1,
263            izz: 0.2,
264        };
265
266        // No forces (free fall)
267        let forces = Forces {
268            x: 0.0,
269            y: 0.0,
270            z: 0.0,
271        };
272
273        let torques = Torques {
274            x: 0.0,
275            y: 0.0,
276            z: 0.0,
277        };
278
279        let t = 1.0;
280        let final_state =
281            simulate_drone(initial_state, consts, forces, torques, (0.0, t), 1e-6).unwrap();
282
283        // Check free fall physics: z = z0 - 0.5*g*t^2
284        let expected_z = 10.0 - 0.5 * 9.81 * t * t;
285        let expected_vz = -9.81 * t;
286
287        assert!((final_state.position_z - expected_z).abs() < 0.1);
288        assert!((final_state.velocity_z - expected_vz).abs() < 0.1);
289    }
290}