use std::f64::consts::PI;
use nalgebra::{DVector, Vector6};
use crate::constants::{AngleFormat, DEG2RAD, GM_EARTH, RAD2DEG};
use crate::coordinates::state_koe_to_eci;
use crate::orbits::create_tle_lines;
use crate::propagators::{
DNumericalOrbitPropagator, ForceModelConfig, KeplerianPropagator, NumericalPropagationConfig,
SGPPropagator,
};
use crate::time::Epoch;
use crate::utils::{BraheError, Identifiable};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WalkerPattern {
#[default]
Delta,
Star,
}
#[derive(Debug, Clone)]
pub struct WalkerConstellationGenerator {
pub total_satellites: usize,
pub num_planes: usize,
pub phasing: usize,
pub semi_major_axis: f64,
pub eccentricity: f64,
pub inclination: f64,
pub argument_of_perigee: f64,
pub reference_raan: f64,
pub reference_mean_anomaly: f64,
pub epoch: Epoch,
pub pattern: WalkerPattern,
base_name: Option<String>,
}
impl WalkerConstellationGenerator {
#[allow(clippy::too_many_arguments)]
pub fn new(
t: usize,
p: usize,
f: usize,
semi_major_axis: f64,
eccentricity: f64,
inclination: f64,
argument_of_perigee: f64,
reference_raan: f64,
reference_mean_anomaly: f64,
epoch: Epoch,
angle_format: AngleFormat,
pattern: WalkerPattern,
) -> Self {
if p == 0 {
panic!("Number of planes (P) must be greater than zero");
}
if !t.is_multiple_of(p) {
panic!(
"Total satellites ({}) must be divisible by number of planes ({})",
t, p
);
}
if f >= p {
panic!(
"Phasing factor ({}) must be less than number of planes ({})",
f, p
);
}
let (inc, argp, raan, ma) = match angle_format {
AngleFormat::Degrees => (
inclination * DEG2RAD,
argument_of_perigee * DEG2RAD,
reference_raan * DEG2RAD,
reference_mean_anomaly * DEG2RAD,
),
AngleFormat::Radians => (
inclination,
argument_of_perigee,
reference_raan,
reference_mean_anomaly,
),
};
Self {
total_satellites: t,
num_planes: p,
phasing: f,
semi_major_axis,
eccentricity,
inclination: inc,
argument_of_perigee: argp,
reference_raan: raan,
reference_mean_anomaly: ma,
epoch,
pattern,
base_name: None,
}
}
pub fn with_base_name(mut self, name: &str) -> Self {
self.base_name = Some(name.to_string());
self
}
pub fn satellites_per_plane(&self) -> usize {
self.total_satellites / self.num_planes
}
fn satellite_name(&self, plane_index: usize, sat_index: usize) -> Option<String> {
self.base_name
.as_ref()
.map(|name| format!("{}-P{}-S{}", name, plane_index, sat_index))
}
fn satellite_id(&self, plane_index: usize, sat_index: usize) -> u64 {
(plane_index * self.satellites_per_plane() + sat_index) as u64
}
pub fn satellite_elements(&self, plane_index: usize, sat_index: usize) -> Vector6<f64> {
let sats_per_plane = self.satellites_per_plane();
if plane_index >= self.num_planes {
panic!(
"Plane index ({}) must be less than number of planes ({})",
plane_index, self.num_planes
);
}
if sat_index >= sats_per_plane {
panic!(
"Satellite index ({}) must be less than satellites per plane ({})",
sat_index, sats_per_plane
);
}
let raan_spread = match self.pattern {
WalkerPattern::Delta => 2.0 * PI,
WalkerPattern::Star => PI,
};
let raan_spacing = raan_spread / self.num_planes as f64;
let raan = self.reference_raan + (plane_index as f64) * raan_spacing;
let ma_spacing = 2.0 * PI / sats_per_plane as f64;
let base_ma = (sat_index as f64) * ma_spacing;
let phase_per_satellite = 2.0 * PI / self.total_satellites as f64;
let phase_offset = (plane_index as f64) * (self.phasing as f64) * phase_per_satellite;
let mean_anomaly = (self.reference_mean_anomaly + base_ma + phase_offset) % (2.0 * PI);
let raan_normalized = raan % (2.0 * PI);
Vector6::new(
self.semi_major_axis,
self.eccentricity,
self.inclination,
raan_normalized,
self.argument_of_perigee,
mean_anomaly,
)
}
pub fn all_elements(&self) -> Vec<Vector6<f64>> {
let sats_per_plane = self.satellites_per_plane();
let mut elements = Vec::with_capacity(self.total_satellites);
for plane in 0..self.num_planes {
for sat in 0..sats_per_plane {
elements.push(self.satellite_elements(plane, sat));
}
}
elements
}
pub fn as_keplerian_propagators(&self, step_size: f64) -> Vec<KeplerianPropagator> {
let sats_per_plane = self.satellites_per_plane();
let mut propagators = Vec::with_capacity(self.total_satellites);
for plane in 0..self.num_planes {
for sat in 0..sats_per_plane {
let elements = self.satellite_elements(plane, sat);
let mut prop = KeplerianPropagator::from_keplerian(
self.epoch,
elements,
AngleFormat::Radians,
step_size,
);
let id = self.satellite_id(plane, sat);
prop = prop.with_id(id);
if let Some(name) = self.satellite_name(plane, sat) {
prop = prop.with_name(&name);
}
propagators.push(prop);
}
}
propagators
}
pub fn as_sgp_propagators(
&self,
step_size: f64,
bstar: f64,
ndt2: f64,
nddt6: f64,
) -> Result<Vec<SGPPropagator>, BraheError> {
let sats_per_plane = self.satellites_per_plane();
let mut propagators = Vec::with_capacity(self.total_satellites);
for plane in 0..self.num_planes {
for sat in 0..sats_per_plane {
let elements = self.satellite_elements(plane, sat);
let global_id = self.satellite_id(plane, sat);
let norad_id = format!("{:05}", 99000 + global_id);
let mean_motion_rad_per_sec =
(GM_EARTH / (elements[0] * elements[0] * elements[0])).sqrt();
let mean_motion_revs_per_day = mean_motion_rad_per_sec * 86400.0 / (2.0 * PI);
let inclination_deg = elements[2] * RAD2DEG;
let raan_deg = elements[3] * RAD2DEG;
let argp_deg = elements[4] * RAD2DEG;
let ma_deg = elements[5] * RAD2DEG;
let (line1, line2) = create_tle_lines(
&self.epoch,
&norad_id,
'U', "", mean_motion_revs_per_day,
elements[1], inclination_deg,
raan_deg,
argp_deg,
ma_deg,
ndt2, nddt6, bstar, 0, 0, 0, )?;
let mut prop = SGPPropagator::from_tle(&line1, &line2, step_size)?;
prop = prop.with_id(global_id);
if let Some(name) = self.satellite_name(plane, sat) {
prop = prop.with_name(&name);
}
propagators.push(prop);
}
}
Ok(propagators)
}
pub fn as_numerical_propagators(
&self,
propagation_config: NumericalPropagationConfig,
force_config: ForceModelConfig,
params: Option<DVector<f64>>,
) -> Result<Vec<DNumericalOrbitPropagator>, BraheError> {
let sats_per_plane = self.satellites_per_plane();
let mut propagators = Vec::with_capacity(self.total_satellites);
for plane in 0..self.num_planes {
for sat in 0..sats_per_plane {
let elements = self.satellite_elements(plane, sat);
let state_vec6 = state_koe_to_eci(elements, AngleFormat::Radians);
let state = DVector::from_iterator(6, state_vec6.iter().copied());
let mut prop = DNumericalOrbitPropagator::new(
self.epoch,
state,
propagation_config.clone(),
force_config.clone(),
params.clone(),
None, None, None, )?;
let global_id = self.satellite_id(plane, sat);
prop.id = Some(global_id);
if let Some(name) = self.satellite_name(plane, sat) {
prop.name = Some(name);
}
propagators.push(prop);
}
}
Ok(propagators)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
fn test_epoch() -> Epoch {
Epoch::from_datetime(2024, 1, 1, 12, 0, 0.0, 0.0, crate::time::TimeSystem::UTC)
}
#[test]
fn test_walker_new_basic() {
let walker = WalkerConstellationGenerator::new(
12,
3,
1,
7000e3,
0.001,
98.0,
0.0,
0.0,
0.0,
test_epoch(),
AngleFormat::Degrees,
WalkerPattern::Delta,
);
assert_eq!(walker.total_satellites, 12);
assert_eq!(walker.num_planes, 3);
assert_eq!(walker.phasing, 1);
assert_eq!(walker.satellites_per_plane(), 4);
assert_abs_diff_eq!(walker.semi_major_axis, 7000e3);
assert_abs_diff_eq!(walker.inclination, 98.0 * DEG2RAD, epsilon = 1e-10);
assert_eq!(walker.pattern, WalkerPattern::Delta);
}
#[test]
#[should_panic(expected = "Number of planes (P) must be greater than zero")]
fn test_walker_zero_planes() {
WalkerConstellationGenerator::new(
12,
0,
0,
7000e3,
0.001,
98.0,
0.0,
0.0,
0.0,
test_epoch(),
AngleFormat::Degrees,
WalkerPattern::Delta,
);
}
#[test]
#[should_panic(expected = "must be divisible by")]
fn test_walker_not_divisible() {
WalkerConstellationGenerator::new(
10,
3,
1,
7000e3,
0.001,
98.0,
0.0,
0.0,
0.0,
test_epoch(),
AngleFormat::Degrees,
WalkerPattern::Delta,
);
}
#[test]
#[should_panic(expected = "Phasing factor")]
fn test_walker_phasing_too_large() {
WalkerConstellationGenerator::new(
12,
3,
3,
7000e3,
0.001,
98.0,
0.0,
0.0,
0.0,
test_epoch(),
AngleFormat::Degrees,
WalkerPattern::Delta,
);
}
#[test]
fn test_walker_raan_spacing_delta() {
let walker = WalkerConstellationGenerator::new(
6,
3,
0,
7000e3,
0.0,
45.0,
0.0,
0.0,
0.0,
test_epoch(),
AngleFormat::Degrees,
WalkerPattern::Delta,
);
let elem0 = walker.satellite_elements(0, 0);
let elem1 = walker.satellite_elements(1, 0);
let elem2 = walker.satellite_elements(2, 0);
assert_abs_diff_eq!(elem0[3], 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(elem1[3], 120.0 * DEG2RAD, epsilon = 1e-10);
assert_abs_diff_eq!(elem2[3], 240.0 * DEG2RAD, epsilon = 1e-10);
}
#[test]
fn test_walker_raan_spacing_star() {
let walker = WalkerConstellationGenerator::new(
6,
3,
0,
7000e3,
0.0,
86.4, 0.0,
0.0,
0.0,
test_epoch(),
AngleFormat::Degrees,
WalkerPattern::Star,
);
let elem0 = walker.satellite_elements(0, 0);
let elem1 = walker.satellite_elements(1, 0);
let elem2 = walker.satellite_elements(2, 0);
assert_abs_diff_eq!(elem0[3], 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(elem1[3], 60.0 * DEG2RAD, epsilon = 1e-10);
assert_abs_diff_eq!(elem2[3], 120.0 * DEG2RAD, epsilon = 1e-10);
}
#[test]
fn test_walker_ma_spacing_within_plane() {
let walker = WalkerConstellationGenerator::new(
6,
2,
0,
7000e3,
0.0,
45.0,
0.0,
0.0,
0.0,
test_epoch(),
AngleFormat::Degrees,
WalkerPattern::Delta,
);
let elem0 = walker.satellite_elements(0, 0);
let elem1 = walker.satellite_elements(0, 1);
let elem2 = walker.satellite_elements(0, 2);
assert_abs_diff_eq!(elem0[5], 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(elem1[5], 120.0 * DEG2RAD, epsilon = 1e-10);
assert_abs_diff_eq!(elem2[5], 240.0 * DEG2RAD, epsilon = 1e-10);
}
#[test]
fn test_walker_phasing() {
let walker = WalkerConstellationGenerator::new(
12,
3,
1,
7000e3,
0.0,
45.0,
0.0,
0.0,
0.0,
test_epoch(),
AngleFormat::Degrees,
WalkerPattern::Delta,
);
let elem_p0_s0 = walker.satellite_elements(0, 0);
let elem_p1_s0 = walker.satellite_elements(1, 0);
let elem_p2_s0 = walker.satellite_elements(2, 0);
assert_abs_diff_eq!(elem_p0_s0[5], 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(elem_p1_s0[5], 30.0 * DEG2RAD, epsilon = 1e-10);
assert_abs_diff_eq!(elem_p2_s0[5], 60.0 * DEG2RAD, epsilon = 1e-10);
}
#[test]
fn test_walker_all_elements() {
let walker = WalkerConstellationGenerator::new(
6,
2,
0,
7000e3,
0.0,
45.0,
0.0,
0.0,
0.0,
test_epoch(),
AngleFormat::Degrees,
WalkerPattern::Delta,
);
let all = walker.all_elements();
assert_eq!(all.len(), 6);
assert_abs_diff_eq!(all[0][3], 0.0, epsilon = 1e-10); assert_abs_diff_eq!(all[1][3], 0.0, epsilon = 1e-10); assert_abs_diff_eq!(all[2][3], 0.0, epsilon = 1e-10); assert_abs_diff_eq!(all[3][3], 180.0 * DEG2RAD, epsilon = 1e-10); assert_abs_diff_eq!(all[4][3], 180.0 * DEG2RAD, epsilon = 1e-10); assert_abs_diff_eq!(all[5][3], 180.0 * DEG2RAD, epsilon = 1e-10); }
#[test]
fn test_walker_with_base_name() {
let walker = WalkerConstellationGenerator::new(
6,
2,
0,
7000e3,
0.0,
45.0,
0.0,
0.0,
0.0,
test_epoch(),
AngleFormat::Degrees,
WalkerPattern::Delta,
)
.with_base_name("TestSat");
assert_eq!(
walker.satellite_name(0, 0),
Some("TestSat-P0-S0".to_string())
);
assert_eq!(
walker.satellite_name(0, 1),
Some("TestSat-P0-S1".to_string())
);
assert_eq!(
walker.satellite_name(1, 0),
Some("TestSat-P1-S0".to_string())
);
}
#[test]
fn test_walker_satellite_id() {
let walker = WalkerConstellationGenerator::new(
6,
2,
0,
7000e3,
0.0,
45.0,
0.0,
0.0,
0.0,
test_epoch(),
AngleFormat::Degrees,
WalkerPattern::Delta,
);
assert_eq!(walker.satellite_id(0, 0), 0);
assert_eq!(walker.satellite_id(0, 1), 1);
assert_eq!(walker.satellite_id(0, 2), 2);
assert_eq!(walker.satellite_id(1, 0), 3);
assert_eq!(walker.satellite_id(1, 1), 4);
assert_eq!(walker.satellite_id(1, 2), 5);
}
#[test]
fn test_walker_as_keplerian_propagators() {
let walker = WalkerConstellationGenerator::new(
6,
2,
1,
7000e3,
0.001,
45.0,
0.0,
0.0,
0.0,
test_epoch(),
AngleFormat::Degrees,
WalkerPattern::Delta,
)
.with_base_name("Sat");
let props = walker.as_keplerian_propagators(60.0);
assert_eq!(props.len(), 6);
assert_eq!(props[0].get_id(), Some(0));
assert_eq!(props[0].get_name(), Some("Sat-P0-S0"));
assert_eq!(props[3].get_id(), Some(3));
assert_eq!(props[3].get_name(), Some("Sat-P1-S0"));
}
#[test]
fn test_walker_reference_offsets() {
let walker = WalkerConstellationGenerator::new(
4,
2,
0,
7000e3,
0.0,
45.0,
0.0,
30.0, 15.0, test_epoch(),
AngleFormat::Degrees,
WalkerPattern::Delta,
);
let elem0 = walker.satellite_elements(0, 0);
assert_abs_diff_eq!(elem0[3], 30.0 * DEG2RAD, epsilon = 1e-10); assert_abs_diff_eq!(elem0[5], 15.0 * DEG2RAD, epsilon = 1e-10); }
#[test]
fn test_walker_radians_input() {
let walker = WalkerConstellationGenerator::new(
4,
2,
0,
7000e3,
0.0,
std::f64::consts::FRAC_PI_4, 0.0,
0.0,
0.0,
test_epoch(),
AngleFormat::Radians,
WalkerPattern::Delta,
);
assert_abs_diff_eq!(
walker.inclination,
std::f64::consts::FRAC_PI_4,
epsilon = 1e-10
);
}
#[test]
fn test_walker_star_pattern() {
let walker = WalkerConstellationGenerator::new(
66,
6,
2,
7000e3,
0.001,
86.4, 0.0,
0.0,
0.0,
test_epoch(),
AngleFormat::Degrees,
WalkerPattern::Star,
);
assert_eq!(walker.pattern, WalkerPattern::Star);
assert_eq!(walker.total_satellites, 66);
assert_eq!(walker.num_planes, 6);
assert_eq!(walker.satellites_per_plane(), 11);
let elem0 = walker.satellite_elements(0, 0);
let elem1 = walker.satellite_elements(1, 0);
assert_abs_diff_eq!(elem0[3], 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(elem1[3], 30.0 * DEG2RAD, epsilon = 1e-10);
}
}