leodos_protocols/network/isl/
projection.rs1use core::f32::consts::PI;
7
8use heapless::Vec;
9
10use super::geo::{Ecef, GeoAoi, LatLon};
11use super::shell::Shell;
12use super::torus::Point;
13use super::aoi::Aoi;
14
15const SIDEREAL_DAY_S: f32 = 86_164.1;
18
19pub struct Projection {
21 shell: Shell,
22}
23
24impl Projection {
25 pub fn new(shell: Shell) -> Self {
27 Self { shell }
28 }
29
30 pub fn nadir(&self, point: Point) -> LatLon {
38 let num_planes = self.shell.torus.num_sats as f32;
39 let sats_per_plane = self.shell.torus.num_orbs as f32;
40
41 let raan = 2.0 * PI * (point.sat as f32) / num_planes;
42 let true_anomaly = 2.0 * PI * (point.orb as f32) / sats_per_plane;
43
44 let sin_i = libm::sinf(self.shell.inclination_rad);
45 let cos_i = libm::cosf(self.shell.inclination_rad);
46 let sin_v = libm::sinf(true_anomaly);
47 let cos_v = libm::cosf(true_anomaly);
48
49 let lat_rad = libm::asinf(sin_i * sin_v);
50 let lon_rad = raan + libm::atan2f(cos_i * sin_v, cos_v);
51
52 let mut lon_deg = lon_rad * 180.0 / PI;
53 if lon_deg > 180.0 {
54 lon_deg -= 360.0;
55 } else if lon_deg < -180.0 {
56 lon_deg += 360.0;
57 }
58
59 LatLon::new(lat_rad * 180.0 / PI, lon_deg)
60 }
61
62 pub fn satellites_in_geo_aoi<const N: usize>(&self, geo_aoi: &GeoAoi) -> Vec<Point, N> {
64 let mut result = Vec::new();
65
66 for x in 0..self.shell.torus.num_sats {
67 for y in 0..self.shell.torus.num_orbs {
68 let point = Point::new(y, x);
69 let pos = self.nadir(point);
70 if geo_aoi.contains(pos) && result.push(point).is_err() {
71 return result;
72 }
73 }
74 }
75
76 result
77 }
78
79 pub fn satellite_ecef(&self, point: Point, time_s: f32) -> Ecef {
85 let num_planes = self.shell.torus.num_sats as f32;
86 let sats_per_plane = self.shell.torus.num_orbs as f32;
87
88 let raan = 2.0 * PI * (point.sat as f32) / num_planes;
89 let base_anomaly = 2.0 * PI * (point.orb as f32) / sats_per_plane;
90 let mean_motion = 2.0 * PI / self.shell.orbital_period_s();
91 let true_anomaly = base_anomaly + mean_motion * time_s;
92
93 let r = self.shell.orbital_radius();
94 let cos_v = libm::cosf(true_anomaly);
95 let sin_v = libm::sinf(true_anomaly);
96 let cos_i = libm::cosf(self.shell.inclination_rad);
97 let sin_i = libm::sinf(self.shell.inclination_rad);
98 let cos_raan = libm::cosf(raan);
99 let sin_raan = libm::sinf(raan);
100
101 let x_eci = r * (cos_raan * cos_v - sin_raan * cos_i * sin_v);
103 let y_eci = r * (sin_raan * cos_v + cos_raan * cos_i * sin_v);
104 let z_eci = r * sin_i * sin_v;
105
106 let earth_angle = 2.0 * PI * time_s / SIDEREAL_DAY_S;
108 let cos_e = libm::cosf(earth_angle);
109 let sin_e = libm::sinf(earth_angle);
110
111 Ecef {
112 x: cos_e * x_eci + sin_e * y_eci,
113 y: -sin_e * x_eci + cos_e * y_eci,
114 z: z_eci,
115 }
116 }
117
118 pub fn find_gateway(
123 &self,
124 station: LatLon,
125 time_s: f32,
126 min_elevation_deg: f32,
127 ) -> Option<Point> {
128 let mut best: Option<(Point, f32)> = None;
129
130 for s in 0..self.shell.torus.num_sats {
131 for o in 0..self.shell.torus.num_orbs {
132 let p = Point::new(o, s);
133 let sat_pos = self.satellite_ecef(p, time_s);
134 let elev = sat_pos.elevation_from(station);
135 if elev > min_elevation_deg {
136 if best.map_or(true, |(_, best_el)| elev > best_el) {
137 best = Some((p, elev));
138 }
139 }
140 }
141 }
142
143 best.map(|(p, _)| p)
144 }
145
146 pub fn geo_to_grid_aoi(&self, geo_aoi: &GeoAoi) -> Option<Aoi> {
150 let mut min_y = u8::MAX;
151 let mut max_y = u8::MIN;
152 let mut min_x = u8::MAX;
153 let mut max_x = u8::MIN;
154 let mut found = false;
155
156 for x in 0..self.shell.torus.num_sats {
157 for y in 0..self.shell.torus.num_orbs {
158 let point = Point::new(y, x);
159 if geo_aoi.contains(self.nadir(point)) {
160 min_y = min_y.min(y);
161 max_y = max_y.max(y);
162 min_x = min_x.min(x);
163 max_x = max_x.max(x);
164 found = true;
165 }
166 }
167 }
168
169 if !found {
170 return None;
171 }
172 Some(Aoi::new(Point::new(min_y, min_x), Point::new(max_y, max_x)))
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use crate::network::isl::torus::Torus;
180
181 #[test]
182 fn test_nadir_equator_first_plane() {
183 let torus = Torus::new(20, 72);
184 let shell = Shell::new(torus, 550_000.0, 87.0);
185 let proj = Projection::new(shell);
186
187 let pos = proj.nadir(Point::new(0, 0));
188 assert!((pos.lat_deg).abs() < 0.1, "lat={}", pos.lat_deg);
189 assert!((pos.lon_deg).abs() < 0.1, "lon={}", pos.lon_deg);
190 }
191
192 #[test]
193 fn test_nadir_near_pole() {
194 let torus = Torus::new(20, 72);
195 let shell = Shell::new(torus, 550_000.0, 87.0);
196 let proj = Projection::new(shell);
197
198 let pos = proj.nadir(Point::new(5, 0));
200 assert!(pos.lat_deg > 80.0, "lat={}", pos.lat_deg);
201 }
202
203 #[test]
204 fn test_satellites_in_geo_aoi() {
205 let torus = Torus::new(20, 72);
206 let shell = Shell::new(torus, 550_000.0, 87.0);
207 let proj = Projection::new(shell);
208
209 let geo_aoi = GeoAoi::new(LatLon::new(10.0, -5.0), LatLon::new(-10.0, 5.0));
210
211 let covering: Vec<Point, 64> = proj.satellites_in_geo_aoi(&geo_aoi);
212 assert!(!covering.is_empty(), "should find satellites near equator");
213 }
214
215 #[test]
216 fn test_geo_to_grid_aoi() {
217 let torus = Torus::new(20, 72);
218 let shell = Shell::new(torus, 550_000.0, 87.0);
219 let proj = Projection::new(shell);
220
221 let geo_aoi = GeoAoi::new(LatLon::new(10.0, -5.0), LatLon::new(-10.0, 5.0));
222
223 let grid_aoi = proj.geo_to_grid_aoi(&geo_aoi);
224 assert!(grid_aoi.is_some());
225 }
226
227 #[test]
228 fn test_satellite_ecef_at_t0_matches_nadir() {
229 let torus = Torus::new(20, 72);
230 let shell = Shell::new(torus, 550_000.0, 87.0);
231 let proj = Projection::new(shell);
232
233 let p = Point::new(0, 0);
234 let ecef = proj.satellite_ecef(p, 0.0);
235 let nadir = proj.nadir(p);
236 let nadir_ecef = Ecef::from_latlon(nadir);
237
238 let sat_r = libm::sqrtf(ecef.x * ecef.x + ecef.y * ecef.y + ecef.z * ecef.z);
241 let nadir_r = libm::sqrtf(
242 nadir_ecef.x * nadir_ecef.x + nadir_ecef.y * nadir_ecef.y + nadir_ecef.z * nadir_ecef.z,
243 );
244 assert!(
245 sat_r > nadir_r,
246 "sat_r={} should exceed nadir_r={}",
247 sat_r,
248 nadir_r
249 );
250
251 let dot = (ecef.x * nadir_ecef.x + ecef.y * nadir_ecef.y + ecef.z * nadir_ecef.z)
254 / (sat_r * nadir_r);
255 assert!(dot > 0.99, "dot={} — satellite not above nadir", dot);
256 }
257
258 #[test]
259 fn test_satellite_moves_with_time() {
260 let torus = Torus::new(20, 72);
261 let shell = Shell::new(torus, 550_000.0, 87.0);
262 let proj = Projection::new(shell);
263
264 let p = Point::new(0, 0);
265 let pos0 = proj.satellite_ecef(p, 0.0);
266 let pos1 = proj.satellite_ecef(p, 600.0); let dist = pos0.distance(&pos1);
269 assert!(
270 dist > 100_000.0,
271 "satellite should move significantly in 10 min, dist={}",
272 dist
273 );
274 }
275
276 #[test]
277 fn test_elevation_from_directly_below() {
278 let torus = Torus::new(20, 72);
279 let shell = Shell::new(torus, 550_000.0, 87.0);
280 let proj = Projection::new(shell);
281
282 let p = Point::new(0, 0);
283 let sat = proj.satellite_ecef(p, 0.0);
284 let nadir = proj.nadir(p);
285
286 let elev = sat.elevation_from(nadir);
287 assert!(
288 elev > 85.0,
289 "elevation from nadir should be ~90°, got {}",
290 elev
291 );
292 }
293
294 #[test]
295 fn test_find_gateway_at_t0() {
296 let torus = Torus::new(20, 72);
297 let shell = Shell::new(torus, 550_000.0, 87.0);
298 let proj = Projection::new(shell);
299
300 let station = LatLon::new(0.0, 0.0);
303 let gw = proj.find_gateway(station, 0.0, 5.0);
304 assert!(gw.is_some(), "should find a gateway for equatorial station");
305 }
306
307 #[test]
308 fn test_find_gateway_changes_with_time() {
309 let torus = Torus::new(20, 72);
310 let shell = Shell::new(torus, 550_000.0, 87.0);
311 let proj = Projection::new(shell);
312
313 let station = LatLon::new(0.0, 0.0);
314 let gw0 = proj.find_gateway(station, 0.0, 5.0);
315 let half_period = shell.orbital_period_s() / 2.0;
318 let gw1 = proj.find_gateway(station, half_period, 5.0);
319
320 assert!(gw0 != gw1, "gateway should change: {:?} vs {:?}", gw0, gw1);
321 }
322}