use crate::frames::Vec3;
use crate::inertial::attitude::Quaternion;
pub type Mat3 = [[f64; 3]; 3];
#[derive(Clone, Copy, Debug)]
pub struct VectorObs {
pub body: Vec3,
pub reference: Vec3,
pub weight: f64,
}
#[derive(Clone, Copy, Debug)]
pub struct AttitudeSolution {
pub dcm: Mat3,
pub quat: Quaternion,
pub max_eigenvalue: f64,
pub loss: f64,
}
fn dot(a: Vec3, b: Vec3) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn cross(a: Vec3, b: Vec3) -> Vec3 {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn norm(a: Vec3) -> f64 {
dot(a, a).sqrt()
}
fn normalize(a: Vec3) -> Option<Vec3> {
let n = norm(a);
if n < 1e-300 {
return None;
}
Some([a[0] / n, a[1] / n, a[2] / n])
}
pub fn mat_vec3(m: &Mat3, v: Vec3) -> Vec3 {
[
m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
]
}
pub fn transpose3(a: &Mat3) -> Mat3 {
let mut t = [[0.0; 3]; 3];
for i in 0..3 {
for j in 0..3 {
t[i][j] = a[j][i];
}
}
t
}
pub fn matmul3(a: &Mat3, b: &Mat3) -> Mat3 {
let mut c = [[0.0; 3]; 3];
for i in 0..3 {
for j in 0..3 {
let mut s = 0.0;
for (k, brow) in b.iter().enumerate() {
s += a[i][k] * brow[j];
}
c[i][j] = s;
}
}
c
}
fn det3(m: &Mat3) -> f64 {
m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
- m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
}
fn inv3(m: &Mat3) -> Option<Mat3> {
let d = det3(m);
if d.abs() < 1e-18 {
return None;
}
let inv_d = 1.0 / d;
let mut out = [[0.0; 3]; 3];
out[0][0] = (m[1][1] * m[2][2] - m[1][2] * m[2][1]) * inv_d;
out[0][1] = (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * inv_d;
out[0][2] = (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * inv_d;
out[1][0] = (m[1][2] * m[2][0] - m[1][0] * m[2][2]) * inv_d;
out[1][1] = (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * inv_d;
out[1][2] = (m[0][2] * m[1][0] - m[0][0] * m[1][2]) * inv_d;
out[2][0] = (m[1][0] * m[2][1] - m[1][1] * m[2][0]) * inv_d;
out[2][1] = (m[0][1] * m[2][0] - m[0][0] * m[2][1]) * inv_d;
out[2][2] = (m[0][0] * m[1][1] - m[0][1] * m[1][0]) * inv_d;
Some(out)
}
pub fn triad(b1: Vec3, r1: Vec3, b2: Vec3, r2: Vec3) -> Option<Mat3> {
let t1b = normalize(b1)?;
let t2b = normalize(cross(b1, b2))?;
let t3b = cross(t1b, t2b);
let t1r = normalize(r1)?;
let t2r = normalize(cross(r1, r2))?;
let t3r = cross(t1r, t2r);
let mb: Mat3 = [
[t1b[0], t2b[0], t3b[0]],
[t1b[1], t2b[1], t3b[1]],
[t1b[2], t2b[2], t3b[2]],
];
let mr: Mat3 = [
[t1r[0], t2r[0], t3r[0]],
[t1r[1], t2r[1], t3r[1]],
[t1r[2], t2r[2], t3r[2]],
];
Some(matmul3(&mb, &transpose3(&mr)))
}
pub fn b_matrix(obs: &[VectorObs]) -> Mat3 {
let mut b = [[0.0; 3]; 3];
for o in obs {
let (bv, rv) = match (normalize(o.body), normalize(o.reference)) {
(Some(bv), Some(rv)) => (bv, rv),
_ => continue,
};
let w = o.weight;
for i in 0..3 {
for j in 0..3 {
b[i][j] += w * bv[i] * rv[j];
}
}
}
b
}
pub fn davenport_k(b: &Mat3) -> [[f64; 4]; 4] {
let sigma = b[0][0] + b[1][1] + b[2][2];
let s = [
[2.0 * b[0][0], b[0][1] + b[1][0], b[0][2] + b[2][0]],
[b[1][0] + b[0][1], 2.0 * b[1][1], b[1][2] + b[2][1]],
[b[2][0] + b[0][2], b[2][1] + b[1][2], 2.0 * b[2][2]],
];
let z = [b[1][2] - b[2][1], b[2][0] - b[0][2], b[0][1] - b[1][0]];
let mut k = [[0.0; 4]; 4];
k[0][0] = sigma;
for i in 0..3 {
k[0][i + 1] = z[i];
k[i + 1][0] = z[i];
for j in 0..3 {
k[i + 1][j + 1] = s[i][j] - if i == j { sigma } else { 0.0 };
}
}
k
}
pub fn attitude_matrix_from_quat(q: [f64; 4]) -> Mat3 {
let n = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt();
let (q0, q1, q2, q3) = (q[0] / n, q[1] / n, q[2] / n, q[3] / n);
let s = q0 * q0 - (q1 * q1 + q2 * q2 + q3 * q3);
[
[
s + 2.0 * q1 * q1,
2.0 * (q1 * q2 + q0 * q3),
2.0 * (q1 * q3 - q0 * q2),
],
[
2.0 * (q1 * q2 - q0 * q3),
s + 2.0 * q2 * q2,
2.0 * (q2 * q3 + q0 * q1),
],
[
2.0 * (q1 * q3 + q0 * q2),
2.0 * (q2 * q3 - q0 * q1),
s + 2.0 * q3 * q3,
],
]
}
pub fn wahba_loss(a: &Mat3, obs: &[VectorObs]) -> f64 {
let mut l = 0.0;
for o in obs {
let (bv, rv) = match (normalize(o.body), normalize(o.reference)) {
(Some(bv), Some(rv)) => (bv, rv),
_ => continue,
};
let ar = mat_vec3(a, rv);
let d = [bv[0] - ar[0], bv[1] - ar[1], bv[2] - ar[2]];
l += o.weight * dot(d, d);
}
l
}
pub fn solve_davenport(obs: &[VectorObs]) -> Option<AttitudeSolution> {
let usable = obs
.iter()
.filter(|o| {
normalize(o.body).is_some() && normalize(o.reference).is_some() && o.weight > 0.0
})
.count();
if usable < 2 {
return None;
}
let b = b_matrix(obs);
let k = davenport_k(&b);
let (evals, evecs) = jacobi_eigen4(k);
let mut imax = 0;
for i in 1..4 {
if evals[i] > evals[imax] {
imax = i;
}
}
let q = [
evecs[0][imax],
evecs[1][imax],
evecs[2][imax],
evecs[3][imax],
];
finalize(q, evals[imax], obs)
}
fn char_det(k: &[[f64; 4]; 4], lambda: f64) -> f64 {
let mut m = *k;
for (i, row) in m.iter_mut().enumerate() {
row[i] -= lambda;
}
det4(&m)
}
#[allow(clippy::needless_range_loop)]
fn det4(m: &[[f64; 4]; 4]) -> f64 {
let mut det = 0.0;
for col in 0..4 {
let mut sub = [[0.0; 3]; 3];
for i in 1..4 {
let mut cc = 0;
for j in 0..4 {
if j == col {
continue;
}
sub[i - 1][cc] = m[i][j];
cc += 1;
}
}
let sign = if col % 2 == 0 { 1.0 } else { -1.0 };
det += sign * m[0][col] * det3(&sub);
}
det
}
pub fn solve_quest(obs: &[VectorObs]) -> Option<AttitudeSolution> {
let usable: Vec<&VectorObs> = obs
.iter()
.filter(|o| {
normalize(o.body).is_some() && normalize(o.reference).is_some() && o.weight > 0.0
})
.collect();
if usable.len() < 2 {
return None;
}
let sum_w: f64 = usable.iter().map(|o| o.weight).sum();
let b = b_matrix(obs);
let k = davenport_k(&b);
let mut l0 = sum_w;
let mut l1 = sum_w * (1.0 - 1e-6) - 1e-9;
let mut f0 = char_det(&k, l0);
let mut f1 = char_det(&k, l1);
let mut lambda = l1;
for _ in 0..100 {
let denom = f1 - f0;
if denom.abs() < 1e-300 {
break;
}
lambda = l1 - f1 * (l1 - l0) / denom;
let fl = char_det(&k, lambda);
if (lambda - l1).abs() <= 1e-12 * (1.0 + lambda.abs()) {
break;
}
l0 = l1;
f0 = f1;
l1 = lambda;
f1 = fl;
}
let sigma = b[0][0] + b[1][1] + b[2][2];
let s = [
[2.0 * b[0][0], b[0][1] + b[1][0], b[0][2] + b[2][0]],
[b[1][0] + b[0][1], 2.0 * b[1][1], b[1][2] + b[2][1]],
[b[2][0] + b[0][2], b[2][1] + b[1][2], 2.0 * b[2][2]],
];
let z = [b[1][2] - b[2][1], b[2][0] - b[0][2], b[0][1] - b[1][0]];
let mut m = [[0.0; 3]; 3];
for i in 0..3 {
for j in 0..3 {
m[i][j] = if i == j { lambda + sigma } else { 0.0 } - s[i][j];
}
}
let minv = inv3(&m)?; let y = mat_vec3(&minv, z);
let denom = (1.0 + dot(y, y)).sqrt();
let q = [1.0 / denom, y[0] / denom, y[1] / denom, y[2] / denom];
finalize(q, lambda, obs)
}
fn finalize(q: [f64; 4], lambda: f64, obs: &[VectorObs]) -> Option<AttitudeSolution> {
let a = attitude_matrix_from_quat(q);
let quat = Quaternion::from_dcm(transpose3(&a));
let loss = wahba_loss(&a, obs);
Some(AttitudeSolution {
dcm: a,
quat,
max_eigenvalue: lambda,
loss,
})
}
#[allow(clippy::needless_range_loop)]
pub fn jacobi_eigen4(a_in: [[f64; 4]; 4]) -> ([f64; 4], [[f64; 4]; 4]) {
let mut a = a_in;
let mut v = [[0.0; 4]; 4];
for (i, row) in v.iter_mut().enumerate() {
row[i] = 1.0;
}
for _sweep in 0..100 {
let mut off = 0.0;
for p in 0..4 {
for q in (p + 1)..4 {
off += a[p][q] * a[p][q];
}
}
if off < 1e-30 {
break;
}
for p in 0..4 {
for q in (p + 1)..4 {
if a[p][q].abs() < 1e-300 {
continue;
}
let theta = (a[q][q] - a[p][p]) / (2.0 * a[p][q]);
let t = if theta == 0.0 {
1.0
} else {
theta.signum() / (theta.abs() + (theta * theta + 1.0).sqrt())
};
let c = 1.0 / (t * t + 1.0).sqrt();
let s = t * c;
for k in 0..4 {
let akp = a[k][p];
let akq = a[k][q];
a[k][p] = c * akp - s * akq;
a[k][q] = s * akp + c * akq;
}
for k in 0..4 {
let apk = a[p][k];
let aqk = a[q][k];
a[p][k] = c * apk - s * aqk;
a[q][k] = s * apk + c * aqk;
}
for vrow in v.iter_mut() {
let vp = vrow[p];
let vq = vrow[q];
vrow[p] = c * vp - s * vq;
vrow[q] = s * vp + c * vq;
}
}
}
}
let evals = [a[0][0], a[1][1], a[2][2], a[3][3]];
(evals, v)
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol
}
#[test]
#[allow(clippy::needless_range_loop)]
fn attitude_matrix_of_identity_quat_is_identity() {
let a = attitude_matrix_from_quat([1.0, 0.0, 0.0, 0.0]);
for i in 0..3 {
for j in 0..3 {
let e = if i == j { 1.0 } else { 0.0 };
assert!(approx(a[i][j], e, 1e-15), "A[{i}][{j}]={}", a[i][j]);
}
}
}
#[test]
fn jacobi_diagonalises_known_symmetric() {
let a = [
[4.0, 1.0, 0.5, 0.2],
[1.0, 3.0, 0.3, 0.1],
[0.5, 0.3, 2.0, 0.4],
[0.2, 0.1, 0.4, 1.0],
];
let (evals, evecs) = jacobi_eigen4(a);
for j in 0..4 {
let v = [evecs[0][j], evecs[1][j], evecs[2][j], evecs[3][j]];
for i in 0..4 {
let kv: f64 = (0..4).map(|m| a[i][m] * v[m]).sum();
assert!(
approx(kv, evals[j] * v[i], 1e-9),
"eigenpair {j} row {i}: {kv} vs {}",
evals[j] * v[i]
);
}
}
let tr_in = 4.0 + 3.0 + 2.0 + 1.0;
let tr_out: f64 = evals.iter().sum();
assert!(approx(tr_in, tr_out, 1e-9));
}
#[test]
#[allow(clippy::needless_range_loop)]
fn davenport_k_is_symmetric() {
let obs = [
VectorObs {
body: [1.0, 0.0, 0.0],
reference: [0.0, 1.0, 0.0],
weight: 0.5,
},
VectorObs {
body: [0.0, 0.0, 1.0],
reference: [1.0, 0.0, 0.0],
weight: 0.5,
},
];
let k = davenport_k(&b_matrix(&obs));
for i in 0..4 {
for j in 0..4 {
assert!(approx(k[i][j], k[j][i], 1e-15), "K[{i}][{j}] not symmetric");
}
}
}
}