use rand::distr::{Distribution, Uniform};
use rand::rngs::StdRng;
use rand::SeedableRng;
use std::f64::consts::PI;
use super::{MinimalCatalog, MinimalStar};
use crate::StarfieldError;
pub struct MagnitudeDistribution {
pub min_magnitude: f64,
pub max_magnitude: f64,
pub log_base: f64,
}
impl Default for MagnitudeDistribution {
fn default() -> Self {
Self {
min_magnitude: 1.0, max_magnitude: 12.0, log_base: 2.5, }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SpatialDistribution {
Uniform,
GalacticPlane { concentration: f64 },
Cluster {
center_ra: f64,
center_dec: f64,
radius: f64,
},
}
pub struct SyntheticCatalogConfig {
pub count: usize,
pub seed: u64,
pub magnitude_dist: MagnitudeDistribution,
pub spatial_dist: SpatialDistribution,
pub center_ra: Option<f64>,
pub center_dec: Option<f64>,
pub fov_deg: Option<f64>,
pub description: String,
}
impl Default for SyntheticCatalogConfig {
fn default() -> Self {
Self {
count: 100,
seed: 42,
magnitude_dist: MagnitudeDistribution::default(),
spatial_dist: SpatialDistribution::Uniform,
center_ra: None,
center_dec: None,
fov_deg: None,
description: "Synthetic star catalog".to_string(),
}
}
}
impl SyntheticCatalogConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_count(mut self, count: usize) -> Self {
self.count = count;
self
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = seed;
self
}
pub fn with_magnitude_range(mut self, min: f64, max: f64) -> Self {
self.magnitude_dist.min_magnitude = min;
self.magnitude_dist.max_magnitude = max;
self
}
pub fn with_magnitude_base(mut self, log_base: f64) -> Self {
self.magnitude_dist.log_base = log_base;
self
}
pub fn with_spatial_distribution(mut self, dist: SpatialDistribution) -> Self {
self.spatial_dist = dist;
self
}
pub fn with_field_of_view(mut self, ra: f64, dec: f64, fov_deg: f64) -> Self {
self.center_ra = Some(ra);
self.center_dec = Some(dec);
self.fov_deg = Some(fov_deg);
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
pub fn generate(&self) -> Result<MinimalCatalog, StarfieldError> {
let mut rng = StdRng::seed_from_u64(self.seed);
let mut stars = Vec::with_capacity(self.count);
let generation_count = match (self.center_ra, self.center_dec, self.fov_deg) {
(Some(_), Some(_), Some(fov)) => {
let sphere_area = 4.0 * PI;
let fov_area = 2.0 * PI * (1.0 - (fov / 2.0).to_radians().cos());
let ratio = sphere_area / fov_area;
(self.count as f64 * ratio.min(100.0) * 1.5) as usize
}
_ => self.count,
};
for id in 1..=generation_count {
let (ra, dec) = self.generate_star_position(&mut rng);
if !self.is_in_field_of_view(ra, dec) {
continue;
}
let magnitude = self.generate_star_magnitude(&mut rng);
let star = MinimalStar::new(id as u64, ra, dec, magnitude);
stars.push(star);
if stars.len() >= self.count {
break;
}
}
let final_count = stars.len();
if final_count < self.count {
println!(
"Warning: Could only generate {} of {} requested stars within the specified field of view",
final_count,
self.count
);
}
Ok(MinimalCatalog::from_stars(stars, &self.description))
}
fn generate_star_position(&self, rng: &mut StdRng) -> (f64, f64) {
match self.spatial_dist {
SpatialDistribution::Uniform => {
let u = Uniform::try_from(-1.0..1.0).unwrap();
let v = Uniform::try_from(0.0..1.0).unwrap();
let z: f64 = u.sample(rng); let phi: f64 = v.sample(rng) * 2.0 * PI;
let dec = z.asin().to_degrees(); let ra = phi.to_degrees();
(ra, dec)
}
SpatialDistribution::GalacticPlane { concentration } => {
let u = Uniform::try_from(0.0..1.0).unwrap();
let ra: f64 = u.sample(rng) * 360.0;
let beta = concentration.max(0.1); let x: f64 = u.sample(rng);
let dec = 90.0 - 180.0 * (x.powf(1.0 / beta) + (1.0 - x).powf(1.0 / beta)) / 2.0;
(ra, dec)
}
SpatialDistribution::Cluster {
center_ra,
center_dec,
radius,
} => {
let u = Uniform::try_from(0.0..1.0).unwrap();
let sample: f64 = u.sample(rng);
let r = radius * (1.0 - sample.sqrt());
let theta = u.sample(rng) * 2.0 * PI;
let ra_offset = r * theta.cos();
let dec_offset = r * theta.sin();
let ra = (center_ra + ra_offset).rem_euclid(360.0);
let dec = (center_dec + dec_offset).clamp(-90.0, 90.0);
(ra, dec)
}
}
}
fn generate_star_magnitude(&self, rng: &mut StdRng) -> f64 {
let u = Uniform::try_from(0.0..1.0).unwrap();
let min_mag = self.magnitude_dist.min_magnitude;
let max_mag = self.magnitude_dist.max_magnitude;
let log_base = self.magnitude_dist.log_base;
let exp_range = log_base.powf(max_mag - min_mag) - 1.0;
let uniform_sample = u.sample(rng);
let t = uniform_sample * exp_range + 1.0;
min_mag + t.log(log_base).clamp(0.0, max_mag - min_mag)
}
fn is_in_field_of_view(&self, ra: f64, dec: f64) -> bool {
match (self.center_ra, self.center_dec, self.fov_deg) {
(Some(center_ra), Some(center_dec), Some(fov_deg)) => {
let ra_rad = ra.to_radians();
let dec_rad = dec.to_radians();
let center_ra_rad = center_ra.to_radians();
let center_dec_rad = center_dec.to_radians();
let fov_rad = fov_deg.to_radians();
let d_ra = ra_rad - center_ra_rad;
let d_dec = dec_rad - center_dec_rad;
let a = (d_dec / 2.0).sin().powi(2)
+ center_dec_rad.cos() * dec_rad.cos() * (d_ra / 2.0).sin().powi(2);
let angular_distance = 2.0 * a.sqrt().asin();
angular_distance <= fov_rad / 2.0
}
_ => true, }
}
}
pub fn create_synthetic_catalog(
count: usize,
min_magnitude: f64,
max_magnitude: f64,
seed: u64,
) -> Result<MinimalCatalog, StarfieldError> {
SyntheticCatalogConfig::new()
.with_count(count)
.with_magnitude_range(min_magnitude, max_magnitude)
.with_seed(seed)
.generate()
}
pub fn create_fov_catalog(
count: usize,
min_magnitude: f64,
max_magnitude: f64,
ra: f64,
dec: f64,
fov_deg: f64,
seed: u64,
) -> Result<MinimalCatalog, StarfieldError> {
SyntheticCatalogConfig::new()
.with_count(count)
.with_magnitude_range(min_magnitude, max_magnitude)
.with_field_of_view(ra, dec, fov_deg)
.with_seed(seed)
.generate()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::catalogs::StarPosition;
#[test]
fn test_synthetic_catalog_generation() {
let catalog = create_synthetic_catalog(100, 1.0, 6.0, 42).unwrap();
assert_eq!(catalog.len(), 100);
let stars = catalog.stars();
for star in stars {
assert!(star.magnitude >= 1.0);
assert!(star.magnitude <= 6.0);
}
}
#[test]
fn test_fov_catalog() {
let ra = 100.0;
let dec = 45.0;
let fov = 10.0;
let catalog = create_fov_catalog(50, 1.0, 6.0, ra, dec, fov, 42).unwrap();
let stars = catalog.stars();
for star in stars {
let ra_rad = star.ra().to_radians();
let dec_rad = star.dec().to_radians();
let center_ra_rad = ra.to_radians();
let center_dec_rad = dec.to_radians();
let fov_rad = fov.to_radians();
let d_ra = ra_rad - center_ra_rad;
let d_dec = dec_rad - center_dec_rad;
let a = (d_dec / 2.0).sin().powi(2)
+ center_dec_rad.cos() * dec_rad.cos() * (d_ra / 2.0).sin().powi(2);
let angular_distance = 2.0 * a.sqrt().asin();
assert!(angular_distance <= fov_rad / 2.0);
}
}
#[test]
fn test_magnitude_distribution() {
let catalog = create_synthetic_catalog(10000, 0.0, 10.0, 42).unwrap();
let mut bins = vec![0; 10];
for star in catalog.stars() {
let bin = star.magnitude.floor() as usize;
if bin < bins.len() {
bins[bin] += 1;
}
}
for i in 1..bins.len() {
assert!(
bins[i] > (bins[i - 1] as f64 * 0.9) as usize,
"Expected more stars in bin {} than {}: {} vs {}",
i,
i - 1,
bins[i],
bins[i - 1]
);
}
}
}