use std::time::Instant;
use numeris::Vector3;
use tracing::debug;
use crate::{Centroid, Quaternion};
use super::solve::{
centroid_unit_vectors, diagonal_factor, failure, find_centroid_matches,
sort_indices_by_brightness, wahba_rotation,
};
use super::{SolveConfig, SolveResult, SolveStatus, SolverDatabase};
const MIN_HINT_MATCHES: usize = 3;
impl SolverDatabase {
pub(crate) fn solve_with_hint(
&self,
preprocessed: &[Centroid],
star_vectors: &[[f32; 3]],
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 pixel_scale: f32 = config.pixel_scale();
if pixel_scale <= 0.0 {
return failure(SolveStatus::NoMatch, t0);
}
let fov_rad = config.fov_estimate_rad();
if preprocessed.len() < MIN_HINT_MATCHES {
return failure(SolveStatus::TooFew, 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 * diagonal_factor(config);
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 failure(SolveStatus::NoMatch, t0);
}
let sorted_indices = sort_indices_by_brightness(preprocessed);
let verification_stars = self.props.verification_stars_per_fov as usize;
let match_centroid_count = preprocessed.len().min(verification_stars);
let centroid_vectors =
centroid_unit_vectors(preprocessed, &sorted_indices, pixel_scale, parity_sign);
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;
let mut projected: Vec<(usize, f32, f32)> = Vec::with_capacity(nearby_inds.len());
for &cat_idx in &nearby_inds {
let sv = &star_vectors[cat_idx];
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];
if cx.abs() <= half_w && cy.abs() <= half_h {
projected.push((cat_idx, cx, cy));
}
}
}
if projected.len() < MIN_HINT_MATCHES {
return failure(SolveStatus::NoMatch, t0);
}
let hint_match_radius = (config.hint_uncertainty_rad).max(config.match_radius * fov_rad);
let mut match_xy: Vec<(f32, f32)> = Vec::new();
let mut match_scratch = super::matching::MatchScratch::<f32>::default();
let initial_matches = find_centroid_matches(
¢roid_vectors[..match_centroid_count.min(centroid_vectors.len())],
&projected,
hint_match_radius,
&mut match_xy,
&mut match_scratch,
);
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 failure(SolveStatus::NoMatch, t0);
}
let Some(rotation_matrix) = wahba_rotation(
initial_matches
.iter()
.map(|&(cent_idx, cat_idx)| (¢roid_vectors[cent_idx], &star_vectors[cat_idx])),
) else {
return failure(SolveStatus::NoMatch, t0);
};
let det = rotation_matrix.det();
if det.is_nan() || det <= 0.0 {
return failure(SolveStatus::NoMatch, t0);
}
let (verify_matches, prob_mismatch) = self.verify_attitude(
&rotation_matrix,
¢roid_vectors,
match_centroid_count,
fov_rad,
config,
star_vectors,
0,
None,
&mut match_xy,
&mut match_scratch,
);
if prob_mismatch >= config.match_threshold {
debug!(
"Tracking: verification rejected (matches={}, prob={:.2e})",
verify_matches.len(),
prob_mismatch
);
return failure(SolveStatus::NoMatch, t0);
}
debug!(
"Tracking: VERIFIED — {} matches, prob={:.2e}",
verify_matches.len(),
prob_mismatch
);
match self.refine_and_finalize(
&rotation_matrix,
&verify_matches,
preprocessed,
&sorted_indices,
star_vectors,
config,
parity_flip,
fov_rad,
pixel_scale as f64,
match_centroid_count,
MIN_HINT_MATCHES,
prob_mismatch,
t0,
) {
Some(solution) => Ok(solution),
None => failure(SolveStatus::NoMatch, t0),
}
}
}