use super::so3::{rotation_error, vec3_cross, vec3_dot, vec3_norm, SO3};
use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub struct QuadRotorGeomState<S: ControlScalar> {
pub position: [S; 3],
pub velocity: [S; 3],
pub attitude: SO3<S>,
pub omega: [S; 3],
}
impl<S: ControlScalar> QuadRotorGeomState<S> {
pub fn zero() -> Self {
Self {
position: [S::ZERO; 3],
velocity: [S::ZERO; 3],
attitude: SO3::identity(),
omega: [S::ZERO; 3],
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct GeometricRef<S: ControlScalar> {
pub position: [S; 3],
pub velocity: [S; 3],
pub acceleration: [S; 3],
pub yaw: S,
pub yaw_rate: S,
}
impl<S: ControlScalar> GeometricRef<S> {
pub fn hover_at_origin() -> Self {
Self {
position: [S::ZERO; 3],
velocity: [S::ZERO; 3],
acceleration: [S::ZERO; 3],
yaw: S::ZERO,
yaw_rate: S::ZERO,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct GeometricConfig<S: ControlScalar> {
pub k_r: S,
pub k_omega: S,
pub k_x: S,
pub k_v: S,
pub mass: S,
pub inertia: [S; 3],
pub gravity: S,
}
impl<S: ControlScalar> GeometricConfig<S> {
pub fn standard() -> Self {
Self {
k_r: S::from_f64(8.81),
k_omega: S::from_f64(2.54),
k_x: S::from_f64(16.0),
k_v: S::from_f64(5.6),
mass: S::from_f64(0.5),
inertia: [
S::from_f64(4.0e-3),
S::from_f64(4.0e-3),
S::from_f64(8.0e-3),
],
gravity: S::from_f64(9.81),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct GeometricController<S: ControlScalar> {
config: GeometricConfig<S>,
}
impl<S: ControlScalar> GeometricController<S> {
pub fn new(config: GeometricConfig<S>) -> Self {
Self { config }
}
pub fn update(
&self,
state: &QuadRotorGeomState<S>,
ref_state: &GeometricRef<S>,
) -> (S, [S; 3]) {
let cfg = &self.config;
let e_x = [
state.position[0] - ref_state.position[0],
state.position[1] - ref_state.position[1],
state.position[2] - ref_state.position[2],
];
let e_v = [
state.velocity[0] - ref_state.velocity[0],
state.velocity[1] - ref_state.velocity[1],
state.velocity[2] - ref_state.velocity[2],
];
let e3 = [S::ZERO, S::ZERO, S::ONE];
let f_des = [
-cfg.k_x * e_x[0] - cfg.k_v * e_v[0]
+ cfg.mass * cfg.gravity * e3[0]
+ cfg.mass * ref_state.acceleration[0],
-cfg.k_x * e_x[1] - cfg.k_v * e_v[1]
+ cfg.mass * cfg.gravity * e3[1]
+ cfg.mass * ref_state.acceleration[1],
-cfg.k_x * e_x[2] - cfg.k_v * e_v[2]
+ cfg.mass * cfg.gravity * e3[2]
+ cfg.mass * ref_state.acceleration[2],
];
let b3 = state.attitude.apply(e3); let thrust = vec3_dot(f_des, b3);
let r_d = compute_desired_attitude(f_des, ref_state.yaw);
let omega_d_world = [S::ZERO, S::ZERO, ref_state.yaw_rate];
let omega_d = r_d.transpose().apply(omega_d_world);
let e_r = rotation_error(&r_d, &state.attitude);
let rt_rd_omega_d = state.attitude.transpose().apply(r_d.apply(omega_d));
let e_omega = [
state.omega[0] - rt_rd_omega_d[0],
state.omega[1] - rt_rd_omega_d[1],
state.omega[2] - rt_rd_omega_d[2],
];
let j = cfg.inertia;
let j_omega = [
j[0] * state.omega[0],
j[1] * state.omega[1],
j[2] * state.omega[2],
];
let gyro = vec3_cross(state.omega, j_omega);
let torque = [
-cfg.k_r * e_r[0] - cfg.k_omega * e_omega[0] + gyro[0],
-cfg.k_r * e_r[1] - cfg.k_omega * e_omega[1] + gyro[1],
-cfg.k_r * e_r[2] - cfg.k_omega * e_omega[2] + gyro[2],
];
(thrust, torque)
}
}
fn compute_desired_attitude<S: ControlScalar>(f_des: [S; 3], yaw: S) -> SO3<S> {
let f_norm = vec3_norm(f_des);
if f_norm < S::from_f64(1e-6) {
return SO3::identity();
}
let inv_f = S::ONE / f_norm;
let b3_d = [f_des[0] * inv_f, f_des[1] * inv_f, f_des[2] * inv_f];
let b1_c = [yaw.cos(), yaw.sin(), S::ZERO];
let b2_raw = vec3_cross(b3_d, b1_c);
let b2_norm = vec3_norm(b2_raw);
let b2_d = if b2_norm < S::from_f64(1e-6) {
let b2_raw2 = vec3_cross(b3_d, [S::ZERO, S::ONE, S::ZERO]);
let b2n2 = vec3_norm(b2_raw2);
if b2n2 < S::from_f64(1e-6) {
[S::ONE, S::ZERO, S::ZERO]
} else {
let inv_b2 = S::ONE / b2n2;
[
b2_raw2[0] * inv_b2,
b2_raw2[1] * inv_b2,
b2_raw2[2] * inv_b2,
]
}
} else {
let inv_b2 = S::ONE / b2_norm;
[b2_raw[0] * inv_b2, b2_raw[1] * inv_b2, b2_raw[2] * inv_b2]
};
let b1_d = vec3_cross(b2_d, b3_d);
SO3::from_matrix_unchecked([
[b1_d[0], b2_d[0], b3_d[0]],
[b1_d[1], b2_d[1], b3_d[1]],
[b1_d[2], b2_d[2], b3_d[2]],
])
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-9;
const EPS_LOOSE: f64 = 1e-6;
#[test]
fn hover_zero_torque_correct_thrust() {
let cfg = GeometricConfig::<f64>::standard();
let expected_thrust = cfg.mass * cfg.gravity;
let ctrl = GeometricController::new(cfg);
let state = QuadRotorGeomState::<f64>::zero();
let ref_state = GeometricRef::<f64>::hover_at_origin();
let (thrust, torque) = ctrl.update(&state, &ref_state);
assert!(
(thrust - expected_thrust).abs() < EPS_LOOSE,
"hover thrust: got {}, expected {}",
thrust,
expected_thrust
);
for (i, &t) in torque.iter().enumerate() {
assert!(
t.abs() < EPS_LOOSE,
"hover torque[{}] = {} (expected 0)",
i,
t
);
}
}
#[test]
fn small_attitude_perturbation_restoring_torque() {
let cfg = GeometricConfig::<f64>::standard();
let ctrl = GeometricController::new(cfg);
let r_perturbed = SO3::<f64>::from_axis_angle([1.0, 0.0, 0.0], 0.05).unwrap();
let state = QuadRotorGeomState {
position: [0.0; 3],
velocity: [0.0; 3],
attitude: r_perturbed,
omega: [0.0; 3],
};
let ref_state = GeometricRef::<f64>::hover_at_origin();
let (_thrust, torque) = ctrl.update(&state, &ref_state);
assert!(
torque[0] < 0.0,
"restoring torque[0] should be negative, got {}",
torque[0]
);
assert!(
torque[1].abs() < EPS_LOOSE,
"torque[1] = {} should be small",
torque[1]
);
}
#[test]
fn position_error_modifies_thrust() {
let cfg = GeometricConfig::<f64>::standard();
let base_thrust = cfg.mass * cfg.gravity;
let ctrl = GeometricController::new(cfg);
let state = QuadRotorGeomState {
position: [0.0, 0.0, 1.0], velocity: [0.0; 3],
attitude: SO3::identity(),
omega: [0.0; 3],
};
let ref_state = GeometricRef::<f64>::hover_at_origin();
let (thrust, _torque) = ctrl.update(&state, &ref_state);
let expected = base_thrust - cfg.k_x * 1.0_f64;
assert!(
(thrust - expected).abs() < EPS_LOOSE,
"thrust with +1m z offset: got {}, expected {}",
thrust,
expected
);
}
#[test]
fn desired_attitude_hover_is_identity() {
let m = 0.5_f64;
let g = 9.81_f64;
let f_des = [0.0, 0.0, m * g];
let r_d = compute_desired_attitude(f_des, 0.0_f64);
let eye = SO3::<f64>::identity();
for i in 0..3 {
for j in 0..3 {
assert!(
(r_d.mat[i][j] - eye.mat[i][j]).abs() < EPS_LOOSE,
"R_d[{},{}] = {} (expected {})",
i,
j,
r_d.mat[i][j],
eye.mat[i][j]
);
}
}
}
#[test]
fn gyroscopic_torque_at_high_omega() {
let cfg = GeometricConfig::<f64>::standard();
let ctrl = GeometricController::new(cfg);
let omega_spin = [0.0, 0.0, 10.0_f64];
let state = QuadRotorGeomState {
position: [0.0; 3],
velocity: [0.0; 3],
attitude: SO3::identity(),
omega: omega_spin,
};
let ref_state = GeometricRef {
position: [0.0; 3],
velocity: [0.0; 3],
acceleration: [0.0; 3],
yaw: 0.0,
yaw_rate: 0.0,
};
let (_thrust, torque) = ctrl.update(&state, &ref_state);
let expected_z = -cfg.k_omega * 10.0_f64;
assert!(
(torque[2] - expected_z).abs() < EPS_LOOSE,
"torque[2] = {} expected {}",
torque[2],
expected_z
);
let ref_spinning = GeometricRef {
yaw_rate: 10.0,
..ref_state
};
let (_thrust2, torque2) = ctrl.update(&state, &ref_spinning);
for (i, &t2) in torque2.iter().enumerate() {
assert!(
t2.abs() < EPS_LOOSE,
"torque2[{}] = {} (expected ~0)",
i,
t2
);
}
}
#[test]
fn small_angle_matches_linearized() {
let cfg = GeometricConfig::<f64>::standard();
let ctrl = GeometricController::new(cfg);
let dz = 0.01_f64; let state = QuadRotorGeomState {
position: [0.0, 0.0, dz],
velocity: [0.0; 3],
attitude: SO3::identity(),
omega: [0.0; 3],
};
let ref_state = GeometricRef::<f64>::hover_at_origin();
let (thrust, torque) = ctrl.update(&state, &ref_state);
let expected_thrust = cfg.mass * cfg.gravity - cfg.k_x * dz;
assert!(
(thrust - expected_thrust).abs() < 1e-8,
"linearized thrust: got {}, expected {}",
thrust,
expected_thrust
);
for (i, &t) in torque.iter().enumerate() {
assert!(t.abs() < EPS, "torque[{}] = {}", i, t);
}
}
#[test]
fn config_standard_has_positive_params() {
let cfg = GeometricConfig::<f64>::standard();
assert!(cfg.k_r > 0.0);
assert!(cfg.k_omega > 0.0);
assert!(cfg.k_x > 0.0);
assert!(cfg.k_v > 0.0);
assert!(cfg.mass > 0.0);
assert!(cfg.gravity > 0.0);
for j in cfg.inertia.iter() {
assert!(*j > 0.0);
}
}
}