use core::f64::consts::SQRT_2;
pub const SCALE_STEP: f64 = SQRT_2;
pub const UNKNOWN_STEPS: (i32, i32) = (-4, 12);
pub const WRONG_STEPS: i32 = 4;
pub const LADDER_FIELDS: f64 = 1.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum ScaleSearch {
Never,
#[default]
IfUnknown,
AlsoIfWrong,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Hypothesis {
pub step: i32,
pub scale: f64,
}
#[must_use]
pub fn ladder(centre: f64, lo: i32, hi: i32, skip_centre: bool) -> Vec<Hypothesis> {
let mut steps: Vec<i32> = (lo..=hi).filter(|&k| !(skip_centre && k == 0)).collect();
steps.sort_by_key(|&k| (k.unsigned_abs(), k < 0));
steps
.into_iter()
.map(|step| Hypothesis {
step,
scale: centre * SCALE_STEP.powi(step),
})
.collect()
}
pub const INACCURATE_SCALE: f64 = 0.05;
#[must_use]
pub fn inaccurate_scale_warning(
start_scale: f64,
solved_scale: f64,
height: usize,
) -> Option<String> {
let ratio = start_scale / solved_scale;
if !ratio.is_finite() || (ratio - 1.0).abs() <= INACCURATE_SCALE {
return None;
}
Some(format!(
"Warning scale was inaccurate! Set FOV={:.2}d, scale={:.1}\"",
solved_scale * height as f64 / 3600.0,
solved_scale
))
}
#[cfg(test)]
mod tests {
use super::*;
fn steps(v: &[Hypothesis]) -> Vec<i32> {
v.iter().map(|h| h.step).collect()
}
#[test]
fn the_ladder_starts_at_the_centre_and_alternates_outwards() {
let l = ladder(1.0, -2, 3, false);
assert_eq!(steps(&l), [0, 1, -1, 2, -2, 3]);
assert!((l[1].scale - SQRT_2).abs() < 1e-12);
assert!((l[2].scale - 1.0 / SQRT_2).abs() < 1e-12);
assert!((l[5].scale - 2.0 * SQRT_2).abs() < 1e-12);
}
#[test]
fn the_unknown_ladder_covers_a_quarter_to_64_arcsec_per_pixel() {
let (lo, hi) = UNKNOWN_STEPS;
let l = ladder(1.0, lo, hi, false);
assert_eq!(l.len(), 17);
let min = l.iter().map(|h| h.scale).fold(f64::INFINITY, f64::min);
let max = l.iter().map(|h| h.scale).fold(0.0, f64::max);
assert!((min - 0.25).abs() < 1e-12 && (max - 64.0).abs() < 1e-9);
assert_eq!(steps(&l)[8..], [-4, 5, 6, 7, 8, 9, 10, 11, 12]);
let mut s = 0.25_f64;
while s <= 64.0 {
let nearest = l
.iter()
.map(|h| (h.scale / s).ln().abs())
.fold(f64::INFINITY, f64::min);
assert!(nearest <= SCALE_STEP.ln() / 2.0 + 1e-12, "{s}");
s *= 1.01;
}
}
#[test]
fn a_given_scale_that_failed_is_not_tried_again() {
let l = ladder(2.0, -WRONG_STEPS, WRONG_STEPS, true);
assert_eq!(steps(&l), [1, -1, 2, -2, 3, -3, 4, -4]);
assert!((l[6].scale - 8.0).abs() < 1e-9 && (l[7].scale - 0.5).abs() < 1e-12);
}
#[test]
fn the_warning_follows_astap() {
assert_eq!(inaccurate_scale_warning(1.25, 1.241, 2900), None);
assert_eq!(
inaccurate_scale_warning(1.0, 1.2857, 1400).as_deref(),
Some("Warning scale was inaccurate! Set FOV=0.50d, scale=1.3\"")
);
assert!(inaccurate_scale_warning(1.31, 1.241, 2900).is_some());
assert!(inaccurate_scale_warning(1.17, 1.241, 2900).is_some());
assert_eq!(inaccurate_scale_warning(1.0, 0.0, 100), None);
}
}