use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct PointingSystem {
pub platform_jitter_urad_rms: f64,
pub tracking_bandwidth_hz: f64,
pub tracking_residual_urad: f64,
pub tracking_delay_ms: f64,
pub link_distance_km: f64,
}
impl PointingSystem {
pub fn new(jitter_urad: f64, bw_hz: f64, delay_ms: f64, dist_km: f64) -> Self {
let bw = bw_hz.max(0.1);
let residual = jitter_urad / (bw / 10.0).sqrt();
Self {
platform_jitter_urad_rms: jitter_urad,
tracking_bandwidth_hz: bw,
tracking_residual_urad: residual,
tracking_delay_ms: delay_ms.max(0.0),
link_distance_km: dist_km.max(0.001),
}
}
pub fn effective_jitter_urad(&self) -> f64 {
let delay_error_urad = self.platform_jitter_urad_rms
* (2.0 * PI * self.tracking_bandwidth_hz * self.tracking_delay_ms * 1e-3).min(1.0);
(self.tracking_residual_urad.powi(2) + delay_error_urad.powi(2)).sqrt()
}
pub fn pointing_loss_db(&self, beam_divergence_mrad: f64) -> f64 {
let theta_e_urad = self.effective_jitter_urad();
let theta_half_urad = beam_divergence_mrad * 1e3 / 2.0; if theta_half_urad <= 0.0 {
return f64::INFINITY;
}
8.686 * (theta_e_urad / theta_half_urad).powi(2)
}
pub fn power_penalty_db(&self, beam_divergence_mrad: f64) -> f64 {
self.pointing_loss_db(beam_divergence_mrad)
}
pub fn acquisition_probability(&self, field_of_regard_mrad: f64, scan_time_s: f64) -> f64 {
let n_dwells = (scan_time_s * self.tracking_bandwidth_hz) as usize;
if n_dwells == 0 {
return 0.0;
}
let theta_beam_urad = self.effective_jitter_urad().max(1.0);
let theta_for_urad = field_of_regard_mrad * 1e3;
if theta_for_urad <= 0.0 {
return 0.0;
}
let p_hit = (theta_beam_urad / theta_for_urad).powi(2).min(1.0);
1.0 - (1.0 - p_hit).powi(n_dwells as i32)
}
pub fn mean_acquisition_time_s(&self, uncertainty_mrad: f64) -> f64 {
let theta_beam_urad = self.effective_jitter_urad().max(1.0);
let theta_unc_urad = uncertainty_mrad * 1e3;
if theta_unc_urad <= 0.0 || theta_beam_urad <= 0.0 {
return f64::INFINITY;
}
let p_hit = (theta_beam_urad / theta_unc_urad).powi(2).min(1.0);
let bw = self.tracking_bandwidth_hz.max(1.0);
1.0 / (bw * p_hit)
}
}
#[derive(Debug, Clone)]
pub struct SatelliteOpticalLink {
pub orbit_altitude_km: f64,
pub satellite_speed_km_s: f64,
pub pointing_system: PointingSystem,
pub wavelength: f64,
pub tx_aperture_m: f64,
}
impl SatelliteOpticalLink {
pub fn new_leo(altitude_km: f64) -> Self {
let r_earth_km = 6371.0;
let gm: f64 = 3.986_004_418e5; let r = r_earth_km + altitude_km;
let v_km_s = (gm / r).sqrt();
let pointing = PointingSystem::new(5.0, 1000.0, 1.0, altitude_km);
Self {
orbit_altitude_km: altitude_km,
satellite_speed_km_s: v_km_s,
pointing_system: pointing,
wavelength: 1550e-9,
tx_aperture_m: 0.1,
}
}
pub fn new_geo() -> Self {
let altitude_km = 35_786.0_f64;
let r_earth_km = 6371.0_f64;
let gm: f64 = 3.986_004_418e5;
let r = r_earth_km + altitude_km;
let v_km_s = (gm / r).sqrt();
let pointing = PointingSystem::new(1.0, 100.0, 5.0, altitude_km);
Self {
orbit_altitude_km: altitude_km,
satellite_speed_km_s: v_km_s,
pointing_system: pointing,
wavelength: 1550e-9,
tx_aperture_m: 0.25,
}
}
pub fn max_doppler_shift_mhz(&self) -> f64 {
let c = 2.997_924_58e8; let v_m_s = self.satellite_speed_km_s * 1e3;
let freq = c / self.wavelength;
v_m_s / c * freq * 1e-6 }
pub fn round_trip_time_ms(&self) -> f64 {
let c_km_s = 2.997_924_58e5; 2.0 * self.orbit_altitude_km / c_km_s * 1e3 }
pub fn ground_track_velocity_km_s(&self) -> f64 {
let r_earth = 6371.0;
let h = self.orbit_altitude_km;
self.satellite_speed_km_s * r_earth / (r_earth + h)
}
pub fn path_length_km(&self, elevation_deg: f64) -> f64 {
let theta = elevation_deg.to_radians();
let r_e = 6371.0;
let h = self.orbit_altitude_km;
let r_sat = r_e + h;
let cos_theta = theta.cos();
let discriminant = r_sat * r_sat - r_e * r_e * cos_theta * cos_theta;
if discriminant < 0.0 {
return 0.0; }
-r_e * theta.sin() + discriminant.sqrt()
}
pub fn required_pointing_accuracy_urad(&self) -> f64 {
let theta_div_rad = 2.44 * self.wavelength / self.tx_aperture_m;
theta_div_rad * 1e6 / 10.0 }
pub fn availability_percent(&self, cloud_cover_fraction: f64) -> f64 {
let cc = cloud_cover_fraction.clamp(0.0, 1.0);
100.0 * (1.0 - cc).powf(1.2)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_pointing() -> PointingSystem {
PointingSystem::new(10.0, 500.0, 2.0, 5.0)
}
#[test]
fn test_pointing_loss_non_negative() {
let ps = default_pointing();
assert!(ps.pointing_loss_db(1.0) >= 0.0);
}
#[test]
fn test_pointing_loss_increases_with_jitter() {
let ps_small = PointingSystem::new(1.0, 500.0, 2.0, 5.0);
let ps_large = PointingSystem::new(20.0, 500.0, 2.0, 5.0);
assert!(
ps_large.pointing_loss_db(0.5) > ps_small.pointing_loss_db(0.5),
"Larger jitter must give more pointing loss"
);
}
#[test]
fn test_geo_slant_range_zenith() {
let geo = SatelliteOpticalLink::new_geo();
let r = geo.path_length_km(90.0);
let expected = geo.orbit_altitude_km;
assert!(
(r - expected).abs() < 50.0,
"GEO zenith slant range = {r:.0} km (expected ≈ {expected:.0} km)"
);
}
#[test]
fn test_leo_orbital_speed() {
let leo = SatelliteOpticalLink::new_leo(550.0);
let v = leo.satellite_speed_km_s;
assert!(v > 7.0 && v < 8.5, "LEO v = {v:.3} km/s (expected 7.0–8.5)");
}
#[test]
fn test_doppler_shift_leo() {
let leo = SatelliteOpticalLink::new_leo(550.0);
let df = leo.max_doppler_shift_mhz();
assert!(df > 1000.0, "Doppler = {df:.0} MHz (expected > 1000 MHz)");
}
#[test]
fn test_acquisition_prob_increases_with_time() {
let ps = default_pointing();
let p1 = ps.acquisition_probability(5.0, 0.1);
let p10 = ps.acquisition_probability(5.0, 10.0);
assert!(p10 >= p1, "p10={p10:.4} p1={p1:.4}");
}
#[test]
fn test_availability_decreases_with_clouds() {
let leo = SatelliteOpticalLink::new_leo(550.0);
let avail_clear = leo.availability_percent(0.0);
let avail_overcast = leo.availability_percent(0.9);
assert!(avail_clear > avail_overcast);
assert!((avail_clear - 100.0).abs() < 1e-6);
}
#[test]
fn test_slant_range_elevation_dependence() {
let leo = SatelliteOpticalLink::new_leo(550.0);
let r_zenith = leo.path_length_km(90.0);
let r_10deg = leo.path_length_km(10.0);
assert!(
r_10deg > r_zenith,
"Low elevation must give longer slant range"
);
}
#[test]
fn test_required_pointing_accuracy() {
let leo = SatelliteOpticalLink::new_leo(550.0);
let acc = leo.required_pointing_accuracy_urad();
assert!(acc > 1.0 && acc < 10.0, "Accuracy = {acc:.2} µrad");
}
}