use std::time::Instant;
use numeris::{Matrix3, Vector3};
use tracing::debug;
use crate::{Centroid, Quaternion};
use super::solve::{aberration_correct, binomial_cdf, elapsed_ms, find_centroid_matches, C_KM_S};
use super::wcs_refine;
use super::{SolveConfig, SolveResult, SolveStatus, SolverDatabase};
const MIN_HINT_MATCHES: usize = 3;
impl SolverDatabase {
pub(crate) fn solve_with_hint(
&self,
preprocessed: &[Centroid],
config: &SolveConfig,
hint: &Quaternion,
t0: Instant,
) -> SolveResult {
let cam = &config.camera_model;
let parity_flip = cam.parity_flip;
let parity_sign: f32 = if parity_flip { -1.0 } else { 1.0 };
let camera_model_initialized =
cam.image_width == config.image_width && cam.focal_length_px > 2.0;
let pixel_scale: f32 = if camera_model_initialized {
(1.0 / cam.focal_length_px) as f32
} else if config.fov_estimate_rad > 0.0 && config.image_width > 0 {
let f = (config.image_width as f32 / 2.0)
/ (config.fov_estimate_rad / 2.0).tan();
1.0 / f
} else {
return SolveResult::failure(SolveStatus::NoMatch, elapsed_ms(t0));
};
let fov_rad = 2.0 * (config.image_width as f32 / 2.0 * pixel_scale).atan();
if preprocessed.len() < MIN_HINT_MATCHES {
return SolveResult::failure(SolveStatus::TooFew, elapsed_ms(t0));
}
let r_hint = hint.to_rotation_matrix();
let boresight_icrs = Vector3::from_array([
r_hint[(2, 0)],
r_hint[(2, 1)],
r_hint[(2, 2)],
]);
let fov_diagonal = fov_rad * 1.42;
let cone_radius =
fov_diagonal / 2.0 + config.hint_uncertainty_rad + 2.0 * pixel_scale;
let nearby_inds = self.star_catalog.query_indices_from_uvec_cached(
boresight_icrs,
cone_radius,
&self.star_vectors,
);
debug!(
"Tracking: hint cone {:.3}° → {} catalog stars",
cone_radius.to_degrees(),
nearby_inds.len()
);
if nearby_inds.len() < MIN_HINT_MATCHES {
return SolveResult::failure(SolveStatus::NoMatch, elapsed_ms(t0));
}
let beta = config
.observer_velocity_km_s
.map(|v| [v[0] / C_KM_S, v[1] / C_KM_S, v[2] / C_KM_S]);
let candidate_vecs: Vec<[f32; 3]> = nearby_inds
.iter()
.map(|&idx| {
let raw = &self.star_vectors[idx];
match beta {
Some(b) => aberration_correct(raw, &b),
None => *raw,
}
})
.collect();
let mut sorted_indices: Vec<usize> = (0..preprocessed.len()).collect();
sorted_indices.sort_by(|&a, &b| {
let ma = preprocessed[a].mass.unwrap_or(f32::MIN);
let mb = preprocessed[b].mass.unwrap_or(f32::MIN);
mb.partial_cmp(&ma).unwrap_or(std::cmp::Ordering::Equal)
});
let verification_stars = self.props.verification_stars_per_fov as usize;
let match_centroid_count = preprocessed.len().min(verification_stars);
let centroid_vectors: Vec<[f32; 3]> = sorted_indices
.iter()
.map(|&i| {
let x = parity_sign * preprocessed[i].x * pixel_scale;
let y = preprocessed[i].y * pixel_scale;
let z = 1.0f32;
let norm = (x * x + y * y + z * z).sqrt();
[x / norm, y / norm, z / norm]
})
.collect();
let mut projected: Vec<(usize, f32, f32)> = Vec::with_capacity(candidate_vecs.len());
for (local_i, sv) in candidate_vecs.iter().enumerate() {
let icrs_v = Vector3::from_array([sv[0], sv[1], sv[2]]);
let cam_v = r_hint * icrs_v;
if cam_v[2] > 0.0 {
let cx = cam_v[0] / cam_v[2];
let cy = cam_v[1] / cam_v[2];
let half_w = (config.image_width as f32 / 2.0 + 4.0) * pixel_scale;
let half_h = (config.image_height as f32 / 2.0 + 4.0) * pixel_scale;
if cx.abs() <= half_w && cy.abs() <= half_h {
let cat_star_idx = nearby_inds[local_i];
projected.push((cat_star_idx, cx, cy));
}
}
}
if projected.len() < MIN_HINT_MATCHES {
return SolveResult::failure(SolveStatus::NoMatch, elapsed_ms(t0));
}
let hint_match_radius =
(config.hint_uncertainty_rad).max(config.match_radius * fov_rad);
let initial_matches = find_centroid_matches(
¢roid_vectors[..match_centroid_count.min(centroid_vectors.len())],
&projected,
hint_match_radius,
);
debug!(
"Tracking: initial NN match → {} pairs (radius {:.1}″)",
initial_matches.len(),
hint_match_radius.to_degrees() * 3600.0
);
if initial_matches.len() < MIN_HINT_MATCHES {
return SolveResult::failure(SolveStatus::NoMatch, elapsed_ms(t0));
}
let (rotation_matrix, det_sign_ok) =
wahba_svd_dynamic(¢roid_vectors, &candidate_vecs, &nearby_inds, &initial_matches);
if !det_sign_ok {
return SolveResult::failure(SolveStatus::NoMatch, elapsed_ms(t0));
}
let match_radius_rad = config.match_radius * fov_rad;
let image_center_icrs = rotation_matrix
.transpose()
* Vector3::from_array([0.0, 0.0, 1.0]);
let verify_inds = self.star_catalog.query_indices_from_uvec_cached(
image_center_icrs,
fov_diagonal / 2.0,
&self.star_vectors,
);
let mut verify_positions: Vec<(usize, f32, f32)> = Vec::new();
for &cat_idx in &verify_inds {
let raw = &self.star_vectors[cat_idx];
let sv = match beta {
Some(b) => aberration_correct(raw, &b),
None => *raw,
};
let icrs_v = Vector3::from_array([sv[0], sv[1], sv[2]]);
let cam_v = rotation_matrix * icrs_v;
if cam_v[2] > 0.0 {
verify_positions.push((cat_idx, cam_v[0] / cam_v[2], cam_v[1] / cam_v[2]));
}
}
verify_positions.truncate(2 * match_centroid_count);
let num_nearby = verify_positions.len();
let verify_matches = find_centroid_matches(
¢roid_vectors[..match_centroid_count.min(centroid_vectors.len())],
&verify_positions,
match_radius_rad,
);
let current_num_matches = verify_matches.len();
let prob_single = num_nearby as f64 * (config.match_radius as f64).powi(2);
let prob_mismatch = binomial_cdf(
(match_centroid_count as i64 - (current_num_matches as i64 - 2)).max(0) as u32,
match_centroid_count as u32,
1.0 - prob_single.min(1.0),
);
if prob_mismatch >= config.match_threshold {
debug!(
"Tracking: verification rejected (matches={}, prob={:.2e})",
current_num_matches, prob_mismatch
);
return SolveResult::failure(SolveStatus::NoMatch, elapsed_ms(t0));
}
debug!(
"Tracking: VERIFIED — {} matches, prob={:.2e}",
current_num_matches, prob_mismatch
);
let centroids_px: Vec<(f64, f64)> = sorted_indices
.iter()
.map(|&i| {
let px = parity_sign as f64 * preprocessed[i].x as f64;
let py = preprocessed[i].y as f64;
(px, py)
})
.collect();
let ps_refine = pixel_scale as f64;
let wcs_result = wcs_refine::wcs_refine(
&rotation_matrix,
&verify_matches,
¢roids_px,
&self.star_vectors,
&self.star_catalog,
ps_refine,
parity_flip,
match_radius_rad,
match_centroid_count,
10,
);
if wcs_result.matches.len() < MIN_HINT_MATCHES {
return SolveResult::failure(SolveStatus::NoMatch, elapsed_ms(t0));
}
self.finalize_solve_result(
&wcs_result,
&self.star_vectors,
&sorted_indices,
¢roids_px,
config,
parity_flip,
prob_mismatch,
t0,
)
}
}
fn wahba_svd_dynamic(
centroid_vectors: &[[f32; 3]],
candidate_vecs: &[[f32; 3]],
nearby_inds: &[usize],
matches: &[(usize, usize)],
) -> (Matrix3<f32>, bool) {
let cat_to_local = |cat_idx: usize| -> Option<usize> {
nearby_inds.iter().position(|&x| x == cat_idx)
};
let mut h = numeris::Matrix3::<f64>::zeros();
let mut n_pairs = 0u32;
for &(cent_idx, cat_idx) in matches {
let local_i = match cat_to_local(cat_idx) {
Some(i) => i,
None => continue,
};
let img = ¢roid_vectors[cent_idx];
let cat = &candidate_vecs[local_i];
let img_v = numeris::Vector3::<f64>::from_array([img[0] as f64, img[1] as f64, img[2] as f64]);
let cat_v = numeris::Vector3::<f64>::from_array([cat[0] as f64, cat[1] as f64, cat[2] as f64]);
h = h + img_v.outer(&cat_v);
n_pairs += 1;
}
if n_pairs < MIN_HINT_MATCHES as u32 {
return (Matrix3::<f32>::zeros(), false);
}
let svd = h.svd().expect("SVD failed");
let u = svd.u();
let v_t = svd.vt();
let r64 = *u * *v_t;
let r = r64.cast::<f32>();
let det_ok = r.det() > 0.0;
(r, det_ok)
}