use crate::error::{ArcsecError, Result};
use crate::types::PlateConstants;
pub fn lsq_fit(a_matrix: &[Vec<f64>], b_matrix: &[f64]) -> Result<Vec<f64>> {
const TINY: f64 = 1e-10;
let nr_columns = a_matrix.len();
let nr_equations = b_matrix.len();
if nr_columns == 0
|| nr_equations < nr_columns
|| a_matrix.iter().any(|col| col.len() != nr_equations)
{
return Err(ArcsecError::Singular);
}
let mut temp: Vec<Vec<f64>> = a_matrix.to_vec();
let mut b: Vec<f64> = b_matrix.to_vec();
for j in 0..nr_columns {
for i in (j + 1)..nr_equations {
if temp[j][i] == 0.0 {
continue;
}
let (p, q) = if temp[j][j].abs() < TINY * temp[j][i].abs() {
let old_ji = temp[j][i];
temp[j][j] = -old_ji;
temp[j][i] = 0.0;
(0.0_f64, 1.0_f64)
} else {
let mut h = (temp[j][j] * temp[j][j] + temp[j][i] * temp[j][i]).sqrt();
if temp[j][j] < 0.0 {
h = -h;
}
let p = temp[j][j] / h;
let q = -temp[j][i] / h;
temp[j][j] = h;
temp[j][i] = 0.0;
(p, q)
};
for col in temp.iter_mut().skip(j + 1) {
let h = p * col[j] - q * col[i];
col[i] = q * col[j] + p * col[i];
col[j] = h;
}
let h = p * b[j] - q * b[i];
b[i] = q * b[j] + p * b[i];
b[j] = h;
}
}
let mut x = vec![0.0f64; nr_columns];
for i in (0..nr_columns).rev() {
let mut h = b[i];
for k in (i + 1)..nr_columns {
h -= temp[k][i] * x[k];
}
if temp[i][i].abs() <= 1e-30 {
return Err(ArcsecError::Singular);
}
x[i] = h / temp[i][i];
}
Ok(x)
}
pub fn solve_plate_constants(
img_xy: &[(f64, f64)],
ref_xy: &[(f64, f64)],
) -> Result<PlateConstants> {
let n = img_xy.len();
if ref_xy.len() != n {
return Err(ArcsecError::Singular);
}
let col_x: Vec<f64> = img_xy.iter().map(|&(x, _)| x).collect();
let col_y: Vec<f64> = img_xy.iter().map(|&(_, y)| y).collect();
let col_ones: Vec<f64> = vec![1.0; n];
let a_matrix = vec![col_x, col_y, col_ones];
let b_x: Vec<f64> = ref_xy.iter().map(|&(x, _)| x).collect();
let b_y: Vec<f64> = ref_xy.iter().map(|&(_, y)| y).collect();
let sol_x = lsq_fit(&a_matrix, &b_x)?;
let sol_y = lsq_fit(&a_matrix, &b_y)?;
let xy_sqr_ratio =
(sol_x[0].powi(2) + sol_x[1].powi(2)) / (1e-8 + sol_y[0].powi(2) + sol_y[1].powi(2));
if !(0.9..=1.1).contains(&xy_sqr_ratio) {
return Err(ArcsecError::BadSolution {
ratio: xy_sqr_ratio,
});
}
Ok(PlateConstants {
a: sol_x[0],
b: sol_x[1],
c: sol_x[2],
d: sol_y[0],
e: sol_y[1],
f: sol_y[2],
})
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_close(a: f64, b: f64, tol: f64) {
assert!(
(a - b).abs() < tol,
"expected {b:.6} got {a:.6} (diff {:.2e})",
(a - b).abs()
);
}
fn grid_stars() -> Vec<(f64, f64)> {
(0..4)
.flat_map(|i| (0..4).map(move |j| (i as f64 * 100.0 + 50.0, j as f64 * 100.0 + 50.0)))
.collect()
}
#[test]
fn identity_transform() {
let stars = grid_stars();
let pc = solve_plate_constants(&stars, &stars).unwrap();
assert_close(pc.a, 1.0, 1e-10);
assert_close(pc.b, 0.0, 1e-10);
assert_close(pc.c, 0.0, 1e-10);
assert_close(pc.d, 0.0, 1e-10);
assert_close(pc.e, 1.0, 1e-10);
assert_close(pc.f, 0.0, 1e-10);
}
#[test]
fn pure_translation() {
let img = grid_stars();
let rf: Vec<(f64, f64)> = img.iter().map(|&(x, y)| (x + 100.0, y + 200.0)).collect();
let pc = solve_plate_constants(&img, &rf).unwrap();
assert_close(pc.a, 1.0, 1e-10);
assert_close(pc.b, 0.0, 1e-10);
assert_close(pc.c, 100.0, 1e-8);
assert_close(pc.d, 0.0, 1e-10);
assert_close(pc.e, 1.0, 1e-10);
assert_close(pc.f, 200.0, 1e-8);
}
#[test]
fn pure_scale() {
let img = grid_stars();
let rf: Vec<(f64, f64)> = img.iter().map(|&(x, y)| (2.0 * x, 2.0 * y)).collect();
let pc = solve_plate_constants(&img, &rf).unwrap();
assert_close(pc.a, 2.0, 1e-8);
assert_close(pc.b, 0.0, 1e-8);
assert_close(pc.d, 0.0, 1e-8);
assert_close(pc.e, 2.0, 1e-8);
}
#[test]
fn rotation_90() {
let img = grid_stars();
let rf: Vec<(f64, f64)> = img.iter().map(|&(x, y)| (-y, x)).collect();
let pc = solve_plate_constants(&img, &rf).unwrap();
assert_close(pc.a, 0.0, 1e-8);
assert_close(pc.b, -1.0, 1e-8);
assert_close(pc.d, 1.0, 1e-8);
assert_close(pc.e, 0.0, 1e-8);
}
#[test]
fn singular_input() {
let img: Vec<(f64, f64)> = vec![(1.0, 1.0); 5];
let rf: Vec<(f64, f64)> = vec![(2.0, 2.0); 5];
assert!(matches!(
solve_plate_constants(&img, &rf),
Err(ArcsecError::Singular)
));
}
#[test]
fn bad_solution_ratio() {
let img: Vec<(f64, f64)> = (0..10)
.map(|i| (i as f64 * 50.0 + 1.0, i as f64 * 3.0 + 1.0))
.collect();
let rf: Vec<(f64, f64)> = img.iter().map(|&(x, y)| (x, y * 10.0)).collect();
assert!(matches!(
solve_plate_constants(&img, &rf),
Err(ArcsecError::BadSolution { .. })
));
}
#[test]
fn lsq_fit_1d() {
let a = vec![vec![2.0f64, 2.0, 2.0, 2.0, 2.0]];
let b = vec![6.0, 6.0, 6.0, 6.0, 6.0];
let x = lsq_fit(&a, &b).unwrap();
assert_close(x[0], 3.0, 1e-10);
}
#[test]
fn too_few_or_mismatched_points_are_errors() {
for n in 0..3 {
let img: Vec<(f64, f64)> = (0..n).map(|i| (i as f64, 2.0 * i as f64)).collect();
assert!(
matches!(
solve_plate_constants(&img, &img),
Err(ArcsecError::Singular)
),
"n = {n}"
);
}
let img = grid_stars();
assert!(matches!(
solve_plate_constants(&img, &img[1..]),
Err(ArcsecError::Singular)
));
assert!(matches!(lsq_fit(&[], &[]), Err(ArcsecError::Singular)));
}
}