use std::collections::HashSet;
use tracing::info;
use crate::{Star, StarCatalog};
use super::combinations::BreadthFirstCombinations;
use super::pattern::{
self, compute_edge_ratios, compute_pattern_key, compute_pattern_key_hash,
compute_sorted_edge_angles, distance_from_angle, hash_to_index, insert_pattern, next_prime,
sort_u32_pattern_by_centroid_distance, PATTERN_SIZE,
};
use super::{DatabaseProperties, GenerateDatabaseConfig, PatternEntry, SolverDatabase};
fn num_fields_for_sky(fov_rad: f32) -> usize {
let half_fov = fov_rad / 2.0;
let cone_solid_angle = 2.0 * std::f32::consts::PI * (1.0 - half_fov.cos());
if cone_solid_angle <= 0.0 {
return 1;
}
let n = (4.0 * std::f32::consts::PI / cone_solid_angle).ceil() as usize;
n.max(1)
}
fn separation_for_density(fov_rad: f32, stars_per_fov: u32) -> f32 {
(fov_rad / 2.0) * (std::f32::consts::PI / stars_per_fov as f32).sqrt()
}
fn fibonacci_sphere_lattice(n: usize) -> Vec<[f32; 3]> {
let golden_ratio = (1.0 + 5.0_f64.sqrt()) / 2.0;
let mut points = Vec::with_capacity(n);
for i in 0..n {
let z = 1.0 - (2.0 * i as f64 + 1.0) / n as f64;
let r = (1.0 - z * z).sqrt();
let theta = 2.0 * std::f64::consts::PI * i as f64 / golden_ratio;
let x = r * theta.cos();
let y = r * theta.sin();
points.push([x as f32, y as f32, z as f32]);
}
points
}
impl SolverDatabase {
#[cfg(feature = "hipparcos")]
pub fn generate_from_hipparcos(
catalog_path: &str,
config: &GenerateDatabaseConfig,
) -> anyhow::Result<Self> {
use crate::catalogs::hipparcos::load_hipparcos_catalog_from_file;
use crate::star::star_from_hipparcos;
info!("Loading Hipparcos catalog from {}", catalog_path);
let hip_stars = load_hipparcos_catalog_from_file(catalog_path)?;
info!("Loaded {} raw Hipparcos entries", hip_stars.len());
let stars: Vec<Star> = hip_stars
.iter()
.map(|h| star_from_hipparcos(h, config.epoch_proper_motion_year))
.collect();
let default_pm_year = 1991.25; Self::generate_from_star_list(stars, config, default_pm_year)
}
pub fn generate_from_gaia(
catalog_path: &str,
config: &GenerateDatabaseConfig,
) -> anyhow::Result<Self> {
use crate::catalogs::gaia::{load_gaia_binary, read_gaia_csv};
use crate::star::star_from_gaia;
info!("Loading Gaia catalog from {}", catalog_path);
let gaia_stars = if catalog_path.ends_with(".csv") {
read_gaia_csv(catalog_path)?
} else if catalog_path.ends_with(".bin") {
load_gaia_binary(catalog_path)
.map_err(|e| anyhow::anyhow!("Failed to load Gaia binary: {}", e))?
} else {
let magic = std::fs::read(catalog_path)
.map(|b| if b.len() >= 4 { b[0..4].to_vec() } else { vec![] })
.unwrap_or_default();
if magic == b"GDR3" {
load_gaia_binary(catalog_path)
.map_err(|e| anyhow::anyhow!("Failed to load Gaia binary: {}", e))?
} else {
read_gaia_csv(catalog_path)?
}
};
info!("Loaded {} Gaia entries", gaia_stars.len());
let stars: Vec<Star> = gaia_stars
.iter()
.map(|g| star_from_gaia(g, config.epoch_proper_motion_year))
.collect();
let default_pm_year = 2016.0; Self::generate_from_star_list(stars, config, default_pm_year)
}
pub fn generate_from_star_list(
mut stars: Vec<Star>,
config: &GenerateDatabaseConfig,
default_pm_year: f64,
) -> anyhow::Result<Self> {
let max_fov = config.max_fov_deg.to_radians();
let min_fov = config
.min_fov_deg
.map(|d| d.to_radians())
.unwrap_or(max_fov);
let pattern_bins = (0.25 / config.pattern_max_error).round() as u32;
info!("Pattern bins: {}, max_error: {}", pattern_bins, config.pattern_max_error);
let epoch_pm_year = config.epoch_proper_motion_year;
info!("Proper motion epoch: {:?}", epoch_pm_year);
stars.sort_by(|a, b| a.mag.partial_cmp(&b.mag).unwrap_or(std::cmp::Ordering::Equal));
let star_max_magnitude = config.star_max_magnitude.unwrap_or_else(|| {
compute_magnitude_cutoff(&stars, min_fov, config.verification_stars_per_fov)
});
let num_before = stars.len();
stars.retain(|s| s.mag <= star_max_magnitude);
info!(
"Kept {} of {} stars brighter than magnitude {:.1}",
stars.len(),
num_before,
star_max_magnitude
);
let num_stars = stars.len();
let star_vectors: Vec<[f32; 3]> = stars.iter().map(|s| {
let v = s.uvec();
[v[0], v[1], v[2]]
}).collect();
let star_catalog_ids: Vec<i64> = stars.iter().map(|s| s.id).collect();
let star_catalog = StarCatalog::new(config.catalog_nside, stars);
info!("Built star catalog with nside={}", config.catalog_nside);
let fov_ratio = max_fov / min_fov;
let fov_divisions = if fov_ratio < config.multiscale_step.sqrt() {
1
} else {
let log_ratio = fov_ratio.ln() / config.multiscale_step.ln();
log_ratio.ceil() as usize + 1
};
let pattern_fovs: Vec<f32> = if fov_divisions <= 1 {
vec![max_fov]
} else {
(0..fov_divisions)
.map(|i| {
let t = i as f32 / (fov_divisions - 1) as f32;
(min_fov.ln() + t * (max_fov.ln() - min_fov.ln())).exp()
})
.collect()
};
info!(
"Generating patterns at {} FOV scales: {:?} deg",
pattern_fovs.len(),
pattern_fovs.iter().map(|f| f.to_degrees()).collect::<Vec<_>>()
);
let mut pattern_set: HashSet<[u32; PATTERN_SIZE]> = HashSet::new();
for &pattern_fov in pattern_fovs.iter().rev() {
let pattern_stars_separation = if fov_divisions <= 1 {
separation_for_density(min_fov, config.verification_stars_per_fov)
} else {
separation_for_density(pattern_fov, config.verification_stars_per_fov)
};
let _pattern_stars_dist = distance_from_angle(pattern_stars_separation);
info!(
"FOV {:.2}°: cluster-buster separation {:.3}°",
pattern_fov.to_degrees(),
pattern_stars_separation.to_degrees()
);
let mut keep_for_patterns = vec![false; num_stars];
for star_ind in 0..num_stars {
let dir = numeris::Vector3::from_array([
star_vectors[star_ind][0],
star_vectors[star_ind][1],
star_vectors[star_ind][2],
]);
let nearby = star_catalog.query_indices_from_uvec(dir, pattern_stars_separation);
let occupied = nearby.iter().any(|&idx| keep_for_patterns[idx]);
if !occupied {
keep_for_patterns[star_ind] = true;
}
}
let pattern_star_indices: Vec<usize> = (0..num_stars)
.filter(|&i| keep_for_patterns[i])
.collect();
info!("Pattern stars at this FOV: {}", pattern_star_indices.len());
let fov_angle = pattern_fov / 2.0;
let _fov_dist = distance_from_angle(fov_angle);
let n_fields = num_fields_for_sky(pattern_fov)
* config.lattice_field_oversampling as usize;
let lattice_points = fibonacci_sphere_lattice(n_fields);
let mut total_added = 0usize;
for center in &lattice_points {
let center_v = numeris::Vector3::from_array([center[0], center[1], center[2]]);
let field_stars_all = star_catalog.query_indices_from_uvec(center_v, fov_angle);
let field_pattern_stars: Vec<usize> = field_stars_all
.into_iter()
.filter(|&idx| keep_for_patterns[idx])
.collect();
if field_pattern_stars.len() < PATTERN_SIZE {
continue;
}
let mut patterns_this_field = 0u32;
for combo in BreadthFirstCombinations::<PATTERN_SIZE>::new(&field_pattern_stars) {
let mut pat = [
combo[0] as u32,
combo[1] as u32,
combo[2] as u32,
combo[3] as u32,
];
pat.sort_unstable(); let is_new = pattern_set.insert(pat);
if is_new {
total_added += 1;
if pattern_set.len() % 100_000 == 0 {
info!("Generated {} patterns so far...", pattern_set.len());
}
}
patterns_this_field += 1;
if patterns_this_field >= config.patterns_per_lattice_field as u32 {
break;
}
}
}
info!("Added {} new patterns at this FOV ({} total)", total_added, pattern_set.len());
}
let pattern_list: Vec<[u32; PATTERN_SIZE]> = pattern_set.into_iter().collect();
info!("Total unique patterns: {}", pattern_list.len());
let catalog_length = next_prime(2 * pattern_list.len() as u64) as usize;
info!(
"Hash table size: {} (load factor {:.2})",
catalog_length,
pattern_list.len() as f64 / catalog_length as f64
);
let mut pattern_catalog = vec![PatternEntry::EMPTY; catalog_length];
for pat in &pattern_list {
let vectors: [[f32; 3]; 4] = [
star_vectors[pat[0] as usize],
star_vectors[pat[1] as usize],
star_vectors[pat[2] as usize],
star_vectors[pat[3] as usize],
];
let edge_angles = compute_sorted_edge_angles(&vectors);
let largest_angle = edge_angles[pattern::NUM_EDGES - 1];
let edge_ratios = compute_edge_ratios(&edge_angles);
let pkey = compute_pattern_key(&edge_ratios, pattern_bins);
let pkey_hash = compute_pattern_key_hash(&pkey, pattern_bins);
let hidx = hash_to_index(pkey_hash, catalog_length as u64);
let mut sorted_pat = *pat;
sort_u32_pattern_by_centroid_distance(&mut sorted_pat, &star_vectors);
let entry = PatternEntry::new(
sorted_pat,
largest_angle,
(pkey_hash & 0xFFFF) as u16,
);
insert_pattern(entry, hidx, &mut pattern_catalog);
}
info!("Database generation complete.");
info!(
"Star table: {} stars ({} bytes)",
num_stars,
num_stars * std::mem::size_of::<Star>()
);
info!(
"Pattern catalog: {} slots ({} bytes)",
catalog_length,
catalog_length * std::mem::size_of::<PatternEntry>()
);
let props = DatabaseProperties {
pattern_bins,
pattern_max_error: config.pattern_max_error,
max_fov_rad: max_fov,
min_fov_rad: min_fov,
star_max_magnitude,
num_patterns: pattern_list.len() as u32,
epoch_equinox: 2000, epoch_proper_motion_year: epoch_pm_year.unwrap_or(default_pm_year) as f32,
verification_stars_per_fov: config.verification_stars_per_fov,
lattice_field_oversampling: config.lattice_field_oversampling,
patterns_per_lattice_field: config.patterns_per_lattice_field,
};
Ok(SolverDatabase {
star_catalog,
star_vectors,
star_catalog_ids,
pattern_catalog,
props,
})
}
}
fn compute_magnitude_cutoff(stars: &[Star], min_fov: f32, verification_stars_per_fov: u32) -> f32 {
if stars.is_empty() {
return 10.0;
}
let num_fovs = num_fields_for_sky(min_fov);
let total_stars_needed = (num_fovs as f64 * verification_stars_per_fov as f64 * 0.7) as usize;
if total_stars_needed >= stars.len() {
return stars.last().unwrap().mag;
}
stars[total_stars_needed.min(stars.len() - 1)].mag
}
impl SolverDatabase {
pub fn to_rkyv_bytes(&self) -> Vec<u8> {
rkyv::to_bytes::<rkyv::rancor::Error>(self)
.expect("rkyv serialization failed")
.to_vec()
}
pub fn save_to_file(&self, path: &str) -> anyhow::Result<()> {
let bytes = self.to_rkyv_bytes();
std::fs::write(path, &bytes)?;
info!("Saved database to {} ({} bytes)", path, bytes.len());
Ok(())
}
pub fn load_from_file(path: &str) -> anyhow::Result<Self> {
let bytes = std::fs::read(path)?;
let db = rkyv::from_bytes::<Self, rkyv::rancor::Error>(&bytes)
.map_err(|e| anyhow::anyhow!("rkyv deserialization failed: {}", e))?;
info!(
"Loaded database: {} stars, {} patterns",
db.star_catalog.len(),
db.props.num_patterns
);
Ok(db)
}
}