use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct QuadrotorParams<S: ControlScalar> {
pub mass: S,
pub ixx: S,
pub iyy: S,
pub izz: S,
pub arm_length: S,
pub kt: S,
pub kq: S,
pub gravity: S,
}
impl<S: ControlScalar> QuadrotorParams<S> {
#[allow(clippy::too_many_arguments)]
pub fn new(
mass: S,
ixx: S,
iyy: S,
izz: S,
arm_length: S,
kt: S,
kq: S,
gravity: S,
) -> Result<Self, QuadrotorError> {
if mass <= S::ZERO {
return Err(QuadrotorError::InvalidParameter("mass must be positive"));
}
if ixx <= S::ZERO || iyy <= S::ZERO || izz <= S::ZERO {
return Err(QuadrotorError::InvalidParameter(
"inertia components must be positive",
));
}
if arm_length <= S::ZERO {
return Err(QuadrotorError::InvalidParameter(
"arm_length must be positive",
));
}
if kt <= S::ZERO {
return Err(QuadrotorError::InvalidParameter("kt must be positive"));
}
if kq <= S::ZERO {
return Err(QuadrotorError::InvalidParameter("kq must be positive"));
}
if gravity <= S::ZERO {
return Err(QuadrotorError::InvalidParameter("gravity must be positive"));
}
Ok(Self {
mass,
ixx,
iyy,
izz,
arm_length,
kt,
kq,
gravity,
})
}
pub fn standard() -> Self {
Self {
mass: S::from_f64(0.5),
ixx: S::from_f64(4.0e-3),
iyy: S::from_f64(4.0e-3),
izz: S::from_f64(8.0e-3),
arm_length: S::from_f64(0.12),
kt: S::from_f64(1.5e-5),
kq: S::from_f64(3.0e-7),
gravity: S::from_f64(9.81),
}
}
pub fn hover_omega_sq(&self) -> S {
self.mass * self.gravity / (S::from_f64(4.0) * self.kt)
}
}
#[derive(Debug, Clone, Copy)]
pub struct QuadrotorState<S: ControlScalar> {
pub x: S,
pub y: S,
pub z: S,
pub vx: S,
pub vy: S,
pub vz: S,
pub roll: S,
pub pitch: S,
pub yaw: S,
pub p: S,
pub q: S,
pub r: S,
}
impl<S: ControlScalar> Default for QuadrotorState<S> {
fn default() -> Self {
Self {
x: S::ZERO,
y: S::ZERO,
z: S::ZERO,
vx: S::ZERO,
vy: S::ZERO,
vz: S::ZERO,
roll: S::ZERO,
pitch: S::ZERO,
yaw: S::ZERO,
p: S::ZERO,
q: S::ZERO,
r: S::ZERO,
}
}
}
impl<S: ControlScalar> QuadrotorState<S> {
pub fn to_array(&self) -> [S; 12] {
[
self.x, self.y, self.z, self.vx, self.vy, self.vz, self.roll, self.pitch, self.yaw,
self.p, self.q, self.r,
]
}
pub fn from_array(arr: &[S; 12]) -> Self {
Self {
x: arr[0],
y: arr[1],
z: arr[2],
vx: arr[3],
vy: arr[4],
vz: arr[5],
roll: arr[6],
pitch: arr[7],
yaw: arr[8],
p: arr[9],
q: arr[10],
r: arr[11],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuadrotorError {
InvalidParameter(&'static str),
NumericalSingularity,
}
impl core::fmt::Display for QuadrotorError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::InvalidParameter(msg) => write!(f, "Invalid parameter: {}", msg),
Self::NumericalSingularity => write!(f, "Numerical singularity in quadrotor dynamics"),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct QuadrotorPlant<S: ControlScalar> {
params: QuadrotorParams<S>,
state: QuadrotorState<S>,
}
impl<S: ControlScalar> QuadrotorPlant<S> {
pub fn new(params: QuadrotorParams<S>) -> Self {
Self {
params,
state: QuadrotorState::default(),
}
}
pub fn state(&self) -> &QuadrotorState<S> {
&self.state
}
pub fn set_state(&mut self, state: QuadrotorState<S>) {
self.state = state;
}
pub fn reset(&mut self) {
self.state = QuadrotorState::default();
}
pub fn params(&self) -> &QuadrotorParams<S> {
&self.params
}
fn rotation_matrix(roll: S, pitch: S, yaw: S) -> [[S; 3]; 3] {
let (sr, cr) = (roll.sin(), roll.cos());
let (sp, cp) = (pitch.sin(), pitch.cos());
let (sy, cy) = (yaw.sin(), yaw.cos());
[
[cy * cp, cy * sp * sr - sy * cr, cy * sp * cr + sy * sr],
[sy * cp, sy * sp * sr + cy * cr, sy * sp * cr - cy * sr],
[-sp, cp * sr, cp * cr],
]
}
fn derivatives(&self, s: &[S; 12], u: &[S; 4]) -> Result<[S; 12], QuadrotorError> {
let vx = s[3];
let vy = s[4];
let vz = s[5];
let roll = s[6];
let pitch = s[7];
let yaw = s[8];
let p = s[9];
let q = s[10];
let r = s[11];
let p_params = &self.params;
let kt = p_params.kt;
let kq = p_params.kq;
let l = p_params.arm_length;
let m = p_params.mass;
let g = p_params.gravity;
let ixx = p_params.ixx;
let iyy = p_params.iyy;
let izz = p_params.izz;
let t1 = kt * u[0];
let t2 = kt * u[1];
let t3 = kt * u[2];
let t4 = kt * u[3];
let t_total = t1 + t2 + t3 + t4;
let tau_roll = l * kt * (u[3] - u[1]);
let tau_pitch = l * kt * (u[2] - u[0]);
let tau_yaw = kq * (u[0] - u[1] + u[2] - u[3]);
let rot = Self::rotation_matrix(roll, pitch, yaw);
let ax = (rot[0][2] * t_total) / m;
let ay = (rot[1][2] * t_total) / m;
let az = (rot[2][2] * t_total) / m - g;
let gyro_x = q * izz * r - r * iyy * q;
let gyro_y = r * ixx * p - p * izz * r;
let gyro_z = p * iyy * q - q * ixx * p;
let p_dot = (tau_roll - gyro_x) / ixx;
let q_dot = (tau_pitch - gyro_y) / iyy;
let r_dot = (tau_yaw - gyro_z) / izz;
let cp = pitch.cos();
if cp.abs() < S::from_f64(1e-6) {
return Err(QuadrotorError::NumericalSingularity);
}
let tp = pitch.sin() / cp;
let sr = roll.sin();
let cr = roll.cos();
let roll_dot = p + (sr * tp) * q + (cr * tp) * r;
let pitch_dot = cr * q + (-sr) * r;
let yaw_dot = (sr / cp) * q + (cr / cp) * r;
Ok([
vx, vy, vz, ax, ay, az, roll_dot, pitch_dot, yaw_dot, p_dot, q_dot, r_dot,
])
}
pub fn step(&mut self, u: &[S; 4], dt: S) -> Result<(), QuadrotorError> {
let s = self.state.to_array();
let half = S::HALF;
let two = S::TWO;
let sixth = S::ONE / S::from_f64(6.0);
let k1 = self.derivatives(&s, u)?;
let s2: [S; 12] = core::array::from_fn(|i| s[i] + half * dt * k1[i]);
let k2 = self.derivatives(&s2, u)?;
let s3: [S; 12] = core::array::from_fn(|i| s[i] + half * dt * k2[i]);
let k3 = self.derivatives(&s3, u)?;
let s4: [S; 12] = core::array::from_fn(|i| s[i] + dt * k3[i]);
let k4 = self.derivatives(&s4, u)?;
let new_s: [S; 12] = core::array::from_fn(|i| {
s[i] + sixth * dt * (k1[i] + two * k2[i] + two * k3[i] + k4[i])
});
self.state = QuadrotorState::from_array(&new_s);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hover_equilibrium_altitude_constant() {
let params = QuadrotorParams::standard();
let omega_sq = params.hover_omega_sq();
let mut plant = QuadrotorPlant::new(params);
let u = [omega_sq; 4];
let dt = 0.001_f64;
let z0 = plant.state().z;
for _ in 0..500 {
plant.step(&u, dt).expect("step should succeed");
}
let z1 = plant.state().z;
assert!(
(z1 - z0).abs() < 1e-3,
"altitude should be stable at hover: z0={:.6}, z1={:.6}",
z0,
z1
);
}
#[test]
fn positive_roll_torque_tilts_right() {
let params = QuadrotorParams::standard();
let omega_sq = params.hover_omega_sq();
let mut plant = QuadrotorPlant::new(params);
let delta = omega_sq * 0.1_f64;
let u = [omega_sq, omega_sq, omega_sq, omega_sq + delta];
let dt = 0.001_f64;
for _ in 0..200 {
plant.step(&u, dt).expect("step should succeed");
}
assert!(
plant.state().roll > 0.0,
"positive roll torque should produce positive roll: roll={}",
plant.state().roll
);
}
#[test]
fn params_rejects_nonpositive_mass() {
let result = QuadrotorParams::<f64>::new(-1.0, 4e-3, 4e-3, 8e-3, 0.12, 1.5e-5, 3e-7, 9.81);
assert!(
result == Err(QuadrotorError::InvalidParameter("mass must be positive")),
"expected invalid-mass error"
);
}
#[test]
fn hover_omega_sq_formula() {
let params = QuadrotorParams::<f64>::standard();
let omega_sq = params.hover_omega_sq();
let thrust = 4.0_f64 * params.kt * omega_sq;
let weight = params.mass * params.gravity;
assert!(
(thrust - weight).abs() < 1e-10,
"hover thrust should equal weight: thrust={:.6}, weight={:.6}",
thrust,
weight
);
}
#[test]
fn rotation_matrix_is_orthogonal() {
let roll = 0.3_f64;
let pitch = 0.2_f64;
let yaw = 1.1_f64;
let r = QuadrotorPlant::<f64>::rotation_matrix(roll, pitch, yaw);
for i in 0..3 {
for j in 0..3 {
let mut dot = 0.0_f64;
for row in &r {
dot += row[i] * row[j];
}
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
(dot - expected).abs() < 1e-10,
"R^T R [{},{}] = {:.2e} ≠ {:.1}",
i,
j,
dot,
expected
);
}
}
}
}