1use crate::ephemeris::{self, Planet, PlanetPosition};
11use crate::units::Km;
12
13pub use crate::constants::C_KM_S;
15
16pub fn light_time_seconds(distance: Km) -> f64 {
22 distance.value() / C_KM_S
23}
24
25pub fn light_time_minutes(distance: Km) -> f64 {
27 light_time_seconds(distance) / 60.0
28}
29
30pub fn round_trip_light_time(distance: Km) -> f64 {
32 2.0 * light_time_seconds(distance)
33}
34
35pub fn distance_between_positions(pos1: &PlanetPosition, pos2: &PlanetPosition) -> Km {
40 let dx = pos1.x - pos2.x;
41 let dy = pos1.y - pos2.y;
42 let dz = pos1.z - pos2.z;
43 Km((dx * dx + dy * dy + dz * dz).sqrt())
44}
45
46pub fn planet_light_delay(planet1: Planet, planet2: Planet, jd: f64) -> f64 {
50 let pos1 = ephemeris::planet_position(planet1, jd);
51 let pos2 = ephemeris::planet_position(planet2, jd);
52 let dist = distance_between_positions(&pos1, &pos2);
53 light_time_seconds(dist)
54}
55
56pub fn ship_planet_light_delay(ship_x: f64, ship_y: f64, planet: Planet, jd: f64) -> f64 {
60 let planet_pos = ephemeris::planet_position(planet, jd);
61 let dx = ship_x - planet_pos.x;
62 let dy = ship_y - planet_pos.y;
63 let dist = Km((dx * dx + dy * dy).sqrt());
64 light_time_seconds(dist)
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum CommFeasibility {
70 RealTime,
72 NearRealTime,
74 Delayed,
76 DeepSpace,
78}
79
80impl CommFeasibility {
81 pub fn classify(one_way_delay_s: f64) -> Self {
83 if one_way_delay_s < 3.0 {
84 CommFeasibility::RealTime
85 } else if one_way_delay_s < 30.0 {
86 CommFeasibility::NearRealTime
87 } else if one_way_delay_s < 1800.0 {
88 CommFeasibility::Delayed
89 } else {
90 CommFeasibility::DeepSpace
91 }
92 }
93
94 pub fn label_ja(&self) -> &'static str {
96 match self {
97 CommFeasibility::RealTime => "リアルタイム通信可能",
98 CommFeasibility::NearRealTime => "準リアルタイム(顕著な遅延あり)",
99 CommFeasibility::Delayed => "遅延通信(蓄積転送)",
100 CommFeasibility::DeepSpace => "深宇宙通信(大幅な遅延)",
101 }
102 }
103}
104
105pub fn free_space_path_loss_db(distance_km: f64, freq_hz: f64) -> f64 {
112 let d_m = distance_km * 1000.0;
113 let c_m_s = C_KM_S * 1000.0;
114 20.0 * d_m.log10()
115 + 20.0 * freq_hz.log10()
116 + 20.0 * (4.0 * std::f64::consts::PI / c_m_s).log10()
117}
118
119pub fn planet_distance_range(planet1: Planet, planet2: Planet) -> (Km, Km) {
124 let r1 = planet1.semi_major_axis().value();
125 let r2 = planet2.semi_major_axis().value();
126 let min = (r2 - r1).abs();
127 let max = r1 + r2;
128 (Km(min), Km(max))
129}
130
131pub fn planet_light_delay_range(planet1: Planet, planet2: Planet) -> (f64, f64) {
133 let (min_dist, max_dist) = planet_distance_range(planet1, planet2);
134 (light_time_seconds(min_dist), light_time_seconds(max_dist))
135}
136
137#[derive(Debug, Clone)]
139pub struct CommTimelineEntry {
140 pub jd: f64,
142 pub elapsed_s: f64,
144 pub ship_x: f64,
146 pub ship_y: f64,
148 pub delay_to_earth_s: f64,
150 pub feasibility: CommFeasibility,
152}
153
154pub fn comm_timeline_linear(
162 departure: Planet,
163 arrival: Planet,
164 departure_jd: f64,
165 travel_time_s: f64,
166 n_steps: usize,
167) -> Vec<CommTimelineEntry> {
168 let dep_pos = ephemeris::planet_position(departure, departure_jd);
169 let arr_jd = departure_jd + travel_time_s / 86400.0;
170 let arr_pos = ephemeris::planet_position(arrival, arr_jd);
171
172 let mut entries = Vec::with_capacity(n_steps + 1);
173 for i in 0..=n_steps {
174 let frac = i as f64 / n_steps as f64;
175 let elapsed = frac * travel_time_s;
176 let jd = departure_jd + elapsed / 86400.0;
177
178 let ship_x = dep_pos.x + frac * (arr_pos.x - dep_pos.x);
180 let ship_y = dep_pos.y + frac * (arr_pos.y - dep_pos.y);
181
182 let earth_pos = ephemeris::planet_position(Planet::Earth, jd);
184 let dx = ship_x - earth_pos.x;
185 let dy = ship_y - earth_pos.y;
186 let dist = (dx * dx + dy * dy).sqrt();
187 let delay = dist / C_KM_S;
188
189 entries.push(CommTimelineEntry {
190 jd,
191 elapsed_s: elapsed,
192 ship_x,
193 ship_y,
194 delay_to_earth_s: delay,
195 feasibility: CommFeasibility::classify(delay),
196 });
197 }
198
199 entries
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use crate::ephemeris::J2000_JD;
206
207 const AU_KM: f64 = 149_597_870.7;
208
209 #[test]
210 fn test_light_time_1au() {
211 let delay = light_time_seconds(Km(AU_KM));
213 assert!(
214 (delay - 499.0).abs() < 0.5,
215 "1 AU light time = {:.1}s, expected ~499s",
216 delay
217 );
218 }
219
220 #[test]
221 fn test_light_time_minutes_1au() {
222 let minutes = light_time_minutes(Km(AU_KM));
223 assert!(
224 (minutes - 8.317).abs() < 0.01,
225 "1 AU light time = {:.3} min, expected ~8.317 min",
226 minutes
227 );
228 }
229
230 #[test]
231 fn test_round_trip_1au() {
232 let rt = round_trip_light_time(Km(AU_KM));
233 assert!(
234 (rt - 998.0).abs() < 1.0,
235 "1 AU round trip = {:.1}s, expected ~998s",
236 rt
237 );
238 }
239
240 #[test]
241 fn test_light_time_zero_distance() {
242 let delay = light_time_seconds(Km(0.0));
243 assert_eq!(delay, 0.0);
244 }
245
246 #[test]
247 fn test_planet_distance_range_earth_mars() {
248 let (min, max) = planet_distance_range(Planet::Earth, Planet::Mars);
249 let min_au = min.value() / AU_KM;
251 let max_au = max.value() / AU_KM;
252 assert!(
253 (min_au - 0.524).abs() < 0.02,
254 "Earth-Mars min = {:.3} AU, expected ~0.524",
255 min_au
256 );
257 assert!(
258 (max_au - 2.524).abs() < 0.02,
259 "Earth-Mars max = {:.3} AU, expected ~2.524",
260 max_au
261 );
262 }
263
264 #[test]
265 fn test_planet_distance_range_earth_jupiter() {
266 let (min, max) = planet_distance_range(Planet::Earth, Planet::Jupiter);
267 let min_au = min.value() / AU_KM;
268 let max_au = max.value() / AU_KM;
269 assert!(
271 (min_au - 4.2).abs() < 0.1,
272 "Earth-Jupiter min = {:.1} AU",
273 min_au
274 );
275 assert!(
276 (max_au - 6.2).abs() < 0.1,
277 "Earth-Jupiter max = {:.1} AU",
278 max_au
279 );
280 }
281
282 #[test]
283 fn test_planet_distance_range_earth_saturn() {
284 let (min, max) = planet_distance_range(Planet::Earth, Planet::Saturn);
285 let min_au = min.value() / AU_KM;
286 let max_au = max.value() / AU_KM;
287 assert!(
289 min_au > 8.0 && min_au < 9.0,
290 "Earth-Saturn min = {:.1} AU",
291 min_au
292 );
293 assert!(
294 max_au > 10.0 && max_au < 11.0,
295 "Earth-Saturn max = {:.1} AU",
296 max_au
297 );
298 }
299
300 #[test]
301 fn test_planet_distance_range_earth_uranus() {
302 let (min, max) = planet_distance_range(Planet::Earth, Planet::Uranus);
303 let min_au = min.value() / AU_KM;
304 let max_au = max.value() / AU_KM;
305 assert!(
307 min_au > 17.5 && min_au < 19.0,
308 "Earth-Uranus min = {:.1} AU",
309 min_au
310 );
311 assert!(
312 max_au > 19.5 && max_au < 21.0,
313 "Earth-Uranus max = {:.1} AU",
314 max_au
315 );
316 }
317
318 #[test]
319 fn test_planet_light_delay_range_earth_mars() {
320 let (min_s, max_s) = planet_light_delay_range(Planet::Earth, Planet::Mars);
321 let min_min = min_s / 60.0;
322 let max_min = max_s / 60.0;
323 assert!(
325 min_min > 3.5 && min_min < 5.0,
326 "Earth-Mars min delay = {:.1} min",
327 min_min
328 );
329 assert!(
330 max_min > 19.0 && max_min < 22.0,
331 "Earth-Mars max delay = {:.1} min",
332 max_min
333 );
334 }
335
336 #[test]
337 fn test_planet_light_delay_at_epoch() {
338 let delay = planet_light_delay(Planet::Earth, Planet::Mars, J2000_JD);
340 assert!(delay > 0.0 && delay.is_finite(), "delay = {}", delay);
341 let (min_s, max_s) = planet_light_delay_range(Planet::Earth, Planet::Mars);
343 assert!(
344 delay >= min_s * 0.9 && delay <= max_s * 1.1,
345 "delay {} outside expected range [{}, {}]",
346 delay,
347 min_s,
348 max_s
349 );
350 }
351
352 #[test]
353 fn test_ship_planet_light_delay() {
354 let earth_pos = ephemeris::planet_position(Planet::Earth, J2000_JD);
356 let delay = ship_planet_light_delay(earth_pos.x, earth_pos.y, Planet::Earth, J2000_JD);
357 assert!(
358 delay < 1.0,
359 "ship at Earth position should have near-zero delay: {}",
360 delay
361 );
362 }
363
364 #[test]
365 fn test_feasibility_classification() {
366 assert_eq!(CommFeasibility::classify(0.5), CommFeasibility::RealTime);
367 assert_eq!(CommFeasibility::classify(2.9), CommFeasibility::RealTime);
368 assert_eq!(
369 CommFeasibility::classify(3.0),
370 CommFeasibility::NearRealTime
371 );
372 assert_eq!(
373 CommFeasibility::classify(15.0),
374 CommFeasibility::NearRealTime
375 );
376 assert_eq!(CommFeasibility::classify(30.0), CommFeasibility::Delayed);
377 assert_eq!(CommFeasibility::classify(600.0), CommFeasibility::Delayed);
378 assert_eq!(
379 CommFeasibility::classify(1800.0),
380 CommFeasibility::DeepSpace
381 );
382 assert_eq!(
383 CommFeasibility::classify(3600.0),
384 CommFeasibility::DeepSpace
385 );
386 }
387
388 #[test]
389 fn test_free_space_path_loss_x_band() {
390 let fspl = free_space_path_loss_db(AU_KM, 8.4e9);
394 assert!(
395 (fspl - 274.4).abs() < 1.0,
396 "X-band FSPL at 1 AU = {:.1} dB, expected ~274.4",
397 fspl
398 );
399 }
400
401 #[test]
402 fn test_free_space_path_loss_increases_with_distance() {
403 let fspl_1au = free_space_path_loss_db(AU_KM, 8.4e9);
404 let fspl_5au = free_space_path_loss_db(5.0 * AU_KM, 8.4e9);
405 assert!(
407 (fspl_5au - fspl_1au - 14.0).abs() < 0.1,
408 "FSPL increase for 5x distance = {:.1} dB, expected ~14.0",
409 fspl_5au - fspl_1au
410 );
411 }
412
413 #[test]
414 fn test_distance_between_positions() {
415 let pos1 = PlanetPosition {
416 longitude: crate::units::Radians(0.0),
417 latitude: crate::units::Radians(0.0),
418 distance: Km(AU_KM),
419 x: AU_KM,
420 y: 0.0,
421 z: 0.0,
422 inclination: crate::units::Radians(0.0),
423 };
424 let pos2 = PlanetPosition {
425 longitude: crate::units::Radians(std::f64::consts::PI),
426 latitude: crate::units::Radians(0.0),
427 distance: Km(AU_KM),
428 x: -AU_KM,
429 y: 0.0,
430 z: 0.0,
431 inclination: crate::units::Radians(0.0),
432 };
433 let dist = distance_between_positions(&pos1, &pos2);
434 assert!(
435 (dist.value() - 2.0 * AU_KM).abs() < 1.0,
436 "opposite sides of 1 AU orbit = {:.0} km, expected {:.0}",
437 dist.value(),
438 2.0 * AU_KM
439 );
440 }
441
442 #[test]
443 fn test_comm_timeline_linear_ep01_mars_ganymede() {
444 let jd = J2000_JD;
447 let travel_time = 72.0 * 3600.0; let timeline = comm_timeline_linear(Planet::Mars, Planet::Jupiter, jd, travel_time, 10);
450
451 assert_eq!(timeline.len(), 11); assert!((timeline[0].elapsed_s - 0.0).abs() < 1e-10);
453 assert!((timeline[10].elapsed_s - travel_time).abs() < 1e-6);
454
455 for entry in &timeline {
457 assert!(entry.delay_to_earth_s > 0.0);
458 assert!(entry.delay_to_earth_s.is_finite());
459 }
460 }
461
462 #[test]
463 fn test_comm_timeline_entries_have_monotonic_elapsed() {
464 let timeline = comm_timeline_linear(
465 Planet::Earth,
466 Planet::Mars,
467 J2000_JD,
468 259.0 * 86400.0, 20,
470 );
471 for i in 1..timeline.len() {
472 assert!(
473 timeline[i].elapsed_s > timeline[i - 1].elapsed_s,
474 "elapsed time not monotonic at step {}",
475 i
476 );
477 }
478 }
479
480 #[test]
483 fn test_ep01_mars_fire_control_is_nearrealtime() {
484 let delay = light_time_seconds(Km(10_000.0));
488 assert!(delay < 0.1, "Mars control at 10,000 km = {:.3}s", delay);
489 assert_eq!(CommFeasibility::classify(delay), CommFeasibility::RealTime);
490 }
491
492 #[test]
493 fn test_ep02_jupiter_saturn_delay_range() {
494 let (min, max) = planet_distance_range(Planet::Jupiter, Planet::Saturn);
497 let min_au = min.value() / AU_KM;
498 let max_au = max.value() / AU_KM;
499 assert!(min_au > 4.0 && min_au < 5.0, "J-S min = {:.1} AU", min_au);
500 assert!(max_au > 14.0 && max_au < 16.0, "J-S max = {:.1} AU", max_au);
501
502 let min_delay_min = light_time_minutes(min);
504 assert!(
505 min_delay_min > 30.0,
506 "J-S min delay = {:.1} min",
507 min_delay_min
508 );
509 }
510
511 #[test]
512 fn test_ep04_uranus_earth_deep_space() {
513 let (min_s, _max_s) = planet_light_delay_range(Planet::Earth, Planet::Uranus);
516 let min_min = min_s / 60.0;
517 assert!(
519 CommFeasibility::classify(min_s) == CommFeasibility::DeepSpace,
520 "Uranus-Earth min delay = {:.0} min should be DeepSpace",
521 min_min
522 );
523 }
524
525 #[test]
526 fn test_speed_of_light_constant() {
527 assert!((C_KM_S - 299_792.458).abs() < 1e-10);
529 }
530
531 #[test]
532 fn test_label_ja_all_variants() {
533 assert_eq!(CommFeasibility::RealTime.label_ja(), "リアルタイム通信可能");
534 assert_eq!(
535 CommFeasibility::NearRealTime.label_ja(),
536 "準リアルタイム(顕著な遅延あり)"
537 );
538 assert_eq!(CommFeasibility::Delayed.label_ja(), "遅延通信(蓄積転送)");
539 assert_eq!(
540 CommFeasibility::DeepSpace.label_ja(),
541 "深宇宙通信(大幅な遅延)"
542 );
543 }
544
545 #[test]
546 fn test_label_ja_via_classify() {
547 let rt = CommFeasibility::classify(1.0);
549 assert!(!rt.label_ja().is_empty());
550 let ds = CommFeasibility::classify(10_000.0);
551 assert!(!ds.label_ja().is_empty());
552 }
553
554 #[test]
555 fn test_fspl_increases_with_distance() {
556 let freq = 1.55e14; let loss_1au = free_space_path_loss_db(149_597_870.7, freq);
559 let loss_2au = free_space_path_loss_db(2.0 * 149_597_870.7, freq);
560 let diff = loss_2au - loss_1au;
562 assert!(
563 (diff - 6.02).abs() < 0.01,
564 "FSPL doubling should add ~6 dB: diff = {diff:.4} dB"
565 );
566 assert!(
568 loss_2au > loss_1au,
569 "2 AU loss {loss_2au:.1} should exceed 1 AU loss {loss_1au:.1}"
570 );
571 }
572
573 #[test]
574 fn test_planet_distance_range_earth_mars_au_values() {
575 let (min_dist, max_dist) = planet_distance_range(Planet::Earth, Planet::Mars);
577 let min_au = min_dist.value() / AU_KM;
578 let max_au = max_dist.value() / AU_KM;
579 assert!(
580 (min_au - 0.52).abs() < 0.1,
581 "Earth-Mars min distance: {min_au:.3} AU (expected ~0.52)"
582 );
583 assert!(
584 (max_au - 2.52).abs() < 0.1,
585 "Earth-Mars max distance: {max_au:.3} AU (expected ~2.52)"
586 );
587 assert!(max_au > 4.0 * min_au, "max should be ~5× min");
589 }
590}