use crate::epoch::eigensolver::SpectralCoords;
type M3 = [[f32; 3]; 3];
fn mat3_zero() -> M3 {
[[0.0; 3]; 3]
}
fn mat3_identity() -> M3 {
[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
}
fn mat3_mul(a: &M3, b: &M3) -> M3 {
let mut c = mat3_zero();
for i in 0..3 {
for j in 0..3 {
for k in 0..3 {
c[i][j] += a[i][k] * b[k][j];
}
}
}
c
}
fn mat3_transpose(a: &M3) -> M3 {
let mut t = mat3_zero();
for i in 0..3 {
for j in 0..3 {
t[i][j] = a[j][i];
}
}
t
}
fn mat3_det(a: &M3) -> f32 {
a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1])
- a[0][1] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
+ a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0])
}
fn sym_schur2(a: &M3, p: usize, q: usize) -> (f32, f32) {
if a[p][q].abs() < 1e-10 {
return (1.0, 0.0); }
let tau = (a[q][q] - a[p][p]) / (2.0 * a[p][q]);
let t = if tau >= 0.0 {
1.0 / (tau + (1.0 + tau * tau).sqrt())
} else {
-1.0 / (-tau + (1.0 + tau * tau).sqrt())
};
let c = 1.0 / (1.0 + t * t).sqrt();
let s = t * c;
(c, s)
}
fn jacobi_rotate(a: &mut M3, v: &mut M3, p: usize, q: usize, c: f32, s: f32) {
let apq = a[p][q];
let app = a[p][p];
let aqq = a[q][q];
a[p][p] = c * c * app - 2.0 * s * c * apq + s * s * aqq;
a[q][q] = s * s * app + 2.0 * s * c * apq + c * c * aqq;
a[p][q] = 0.0;
a[q][p] = 0.0;
for r in 0..3 {
if r != p && r != q {
let arp = a[r][p];
let arq = a[r][q];
a[r][p] = c * arp - s * arq;
a[p][r] = a[r][p];
a[r][q] = s * arp + c * arq;
a[q][r] = a[r][q];
}
}
for r in 0..3 {
let vrp = v[r][p];
let vrq = v[r][q];
v[r][p] = c * vrp - s * vrq;
v[r][q] = s * vrp + c * vrq;
}
}
pub fn jacobi_svd_3x3(m: M3) -> (M3, [f32; 3], M3) {
let mt = mat3_transpose(&m);
let mut a = mat3_mul(&mt, &m);
let mut v = mat3_identity();
let off_diag_pairs = [(0, 1), (0, 2), (1, 2)];
for _ in 0..100 {
let off: f32 = off_diag_pairs
.iter()
.map(|&(p, q)| a[p][q] * a[p][q])
.sum::<f32>()
.sqrt();
if off < 1e-12 {
break;
}
for &(p, q) in &off_diag_pairs {
let (c, s) = sym_schur2(&a, p, q);
jacobi_rotate(&mut a, &mut v, p, q, c, s);
}
}
let mut sigma = [a[0][0].max(0.0).sqrt(), a[1][1].max(0.0).sqrt(), a[2][2].max(0.0).sqrt()];
let mut u = mat3_zero();
for i in 0..3 {
for row in 0..3 {
let mut acc = 0.0f32;
for k in 0..3 {
acc += m[row][k] * v[k][i];
}
u[row][i] = acc;
}
let s = sigma[i];
if s > 1e-8 {
for row in 0..3 {
u[row][i] /= s;
}
} else {
sigma[i] = 0.0;
for basis in 0..3 {
let mut candidate = [0.0f32; 3];
candidate[basis] = 1.0;
for j in 0..i {
let proj: f32 = (0..3).map(|r| candidate[r] * u[r][j]).sum();
for r in 0..3 {
candidate[r] -= proj * u[r][j];
}
}
let nrm: f32 = candidate.iter().map(|&x| x * x).sum::<f32>().sqrt();
if nrm > 1e-8 {
for r in 0..3 {
u[r][i] = candidate[r] / nrm;
}
break;
}
}
}
}
(u, sigma, v)
}
pub fn apply_rotation(positions: &mut [f32], q: M3) {
let n = positions.len() / 3;
for i in 0..n {
let x = positions[i * 3];
let y = positions[i * 3 + 1];
let z = positions[i * 3 + 2];
positions[i * 3] = q[0][0] * x + q[0][1] * y + q[0][2] * z;
positions[i * 3 + 1] = q[1][0] * x + q[1][1] * y + q[1][2] * z;
positions[i * 3 + 2] = q[2][0] * x + q[2][1] * y + q[2][2] * z;
}
}
pub fn align(coords: &mut SpectralCoords, anchor_ref: &span>; 3) {
let n = coords.n;
if anchor_ref.is_empty() || n == 0 {
return; }
let m_anchors = anchor_ref.len().min(n);
let mut centroid_cur = [0.0f32; 3];
let mut centroid_ref = [0.0f32; 3];
for i in 0..m_anchors {
let pos = coords.position(i);
for d in 0..3 {
centroid_cur[d] += pos[d];
centroid_ref[d] += anchor_ref[i][d];
}
}
for d in 0..3 {
centroid_cur[d] /= m_anchors as f32;
centroid_ref[d] /= m_anchors as f32;
}
let mut cross_cov = mat3_zero();
for p in 0..m_anchors {
let cur = coords.position(p);
let a = [
cur[0] - centroid_cur[0],
cur[1] - centroid_cur[1],
cur[2] - centroid_cur[2],
];
let b = [
anchor_ref[p][0] - centroid_ref[0],
anchor_ref[p][1] - centroid_ref[1],
anchor_ref[p][2] - centroid_ref[2],
];
for i in 0..3 {
for j in 0..3 {
cross_cov[i][j] += a[i] * b[j];
}
}
}
let (u, sigma, v) = jacobi_svd_3x3(cross_cov);
let ut = mat3_transpose(&u);
let mut q = mat3_mul(&v, &ut);
if mat3_det(&q) < 0.0 {
let mut v_corr = v;
for row in 0..3 {
v_corr[row][2] = -v_corr[row][2];
}
q = mat3_mul(&v_corr, &ut);
}
let trace_s: f32 = sigma[0] + sigma[1] + sigma[2];
let trace_ata: f32 = (0..m_anchors).map(|p| {
let cur = coords.position(p);
let a = [cur[0]-centroid_cur[0], cur[1]-centroid_cur[1], cur[2]-centroid_cur[2]];
a[0]*a[0] + a[1]*a[1] + a[2]*a[2]
}).sum::<f32>();
let s = if trace_ata > 1e-10 { trace_s / trace_ata } else { 1.0f32 };
let qc = [
q[0][0]*centroid_cur[0] + q[0][1]*centroid_cur[1] + q[0][2]*centroid_cur[2],
q[1][0]*centroid_cur[0] + q[1][1]*centroid_cur[1] + q[1][2]*centroid_cur[2],
q[2][0]*centroid_cur[0] + q[2][1]*centroid_cur[1] + q[2][2]*centroid_cur[2],
];
let t = [
centroid_ref[0] - s * qc[0],
centroid_ref[1] - s * qc[1],
centroid_ref[2] - s * qc[2],
];
let n = coords.n;
for i in 0..n {
let x = coords.coords[i*3];
let y = coords.coords[i*3+1];
let z = coords.coords[i*3+2];
coords.coords[i*3] = s*(q[0][0]*x + q[0][1]*y + q[0][2]*z) + t[0];
coords.coords[i*3+1] = s*(q[1][0]*x + q[1][1]*y + q[1][2]*z) + t[1];
coords.coords[i*3+2] = s*(q[2][0]*x + q[2][1]*y + q[2][2]*z) + t[2];
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_coords(positions: &span>; 3) -> SpectralCoords {
let n = positions.len();
let mut coords = Vec::with_capacity(n * 3);
for p in positions {
coords.extend_from_slice(p);
}
crate::epoch::eigensolver::SpectralCoords { n, coords, extra: vec![0.0f32; n * 2] }
}
#[test]
fn jacobi_svd_identity() {
let m = mat3_identity();
let (u, s, v) = jacobi_svd_3x3(m);
for &si in &s {
assert!((si - 1.0).abs() < 1e-4, "singular value = {si}");
}
let q = mat3_mul(&u, &mat3_transpose(&v));
for i in 0..3 {
for j in 0..3 {
let expected = if i == j { 1.0 } else { 0.0 };
assert!((q[i][j] - expected).abs() < 1e-4, "Q[{i}][{j}] = {}", q[i][j]);
}
}
}
#[test]
fn apply_rotation_identity() {
let q = mat3_identity();
let original = vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
let mut positions = original.clone();
apply_rotation(&mut positions, q);
for (i, (&a, &b)) in original.iter().zip(positions.iter()).enumerate() {
assert!((a - b).abs() < 1e-6, "positions[{i}] changed under identity rotation");
}
}
#[test]
fn align_empty_anchor_is_identity() {
let mut coords = make_coords(&[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
let original = coords.coords.clone();
align(&mut coords, &[]);
assert_eq!(coords.coords, original);
}
#[test]
fn align_self_reference_is_identity() {
let positions: Vec<[f32; 3]> = vec![
[1.0, 0.0, 0.0],
[0.0, 2.0, 0.0],
[0.0, 0.0, 3.0],
[1.0, 1.0, 0.0],
];
let mut coords = make_coords(&positions);
let original = coords.coords.clone();
align(&mut coords, &positions);
let n = positions.len();
for i in 0..n {
for j in i + 1..n {
let orig_d2: f32 = (0..3)
.map(|d| {
let diff = original[i * 3 + d] - original[j * 3 + d];
diff * diff
})
.sum();
let new_d2: f32 = (0..3)
.map(|d| {
let diff = coords.coords[i * 3 + d] - coords.coords[j * 3 + d];
diff * diff
})
.sum();
assert!(
(orig_d2.sqrt() - new_d2.sqrt()).abs() < 1e-3,
"distance between particles {i} and {j} changed: {} โ {}",
orig_d2.sqrt(),
new_d2.sqrt()
);
}
}
}
#[test]
fn jacobi_svd_known_matrix() {
let m = [[3.0f32, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 1.0]];
let (u, s, v) = jacobi_svd_3x3(m);
let mut s_sorted = s;
s_sorted.sort_by(|a, b| b.partial_cmp(a).unwrap());
assert!((s_sorted[0] - 3.0).abs() < 1e-3, "s[0] = {}", s_sorted[0]);
assert!((s_sorted[1] - 2.0).abs() < 1e-3, "s[1] = {}", s_sorted[1]);
assert!((s_sorted[2] - 1.0).abs() < 1e-3, "s[2] = {}", s_sorted[2]);
let mut recon = mat3_zero();
for i in 0..3 {
for j in 0..3 {
for k in 0..3 {
recon[i][j] += u[i][k] * s[k] * v[j][k];
}
}
}
for i in 0..3 {
for j in 0..3 {
assert!(
(recon[i][j] - m[i][j]).abs() < 1e-3,
"recon[{i}][{j}] = {} vs {}",
recon[i][j],
m[i][j]
);
}
}
}
}