1pub const JUPITER_RADIUS_KM: f64 = 71_492.0;
30
31#[derive(Debug, Clone, Copy)]
36pub struct RadiationRegion {
37 pub r_min_rj: f64,
39 pub r_max_rj: f64,
41 pub d0_krad_per_hour: f64,
43 pub r0_rj: f64,
45 pub alpha: f64,
47}
48
49#[derive(Debug, Clone)]
51pub struct JupiterRadiationConfig {
52 pub regions: Vec<RadiationRegion>,
55 pub magnetopause_rj: f64,
57 pub latitude_factor: f64,
59 pub plasma_sheet_factor: f64,
61 pub storm_factor: f64,
63}
64
65#[derive(Debug, Clone)]
67pub struct JupiterTransitResult {
68 pub total_dose_krad: f64,
70 pub departure_dose_rate_krad_h: f64,
72 pub arrival_dose_rate_krad_h: f64,
74 pub shield_life_at_departure_h: f64,
76 pub shield_life_at_arrival_h: f64,
78 pub shield_survives: bool,
80 pub region_dose_fractions: Vec<(f64, f64, f64)>, pub peak_dose_rate_krad_h: f64,
84}
85
86impl JupiterRadiationConfig {
87 pub fn default_model() -> Self {
94 let inner = RadiationRegion {
98 r_min_rj: 6.0,
99 r_max_rj: 15.0,
100 d0_krad_per_hour: 0.616,
101 r0_rj: 9.4,
102 alpha: 4.92,
103 };
104
105 let middle = RadiationRegion {
112 r_min_rj: 15.0,
113 r_max_rj: 30.0,
114 d0_krad_per_hour: 0.0616,
115 r0_rj: 15.0,
116 alpha: 9.5,
117 };
118
119 let d_at_30 = middle.d0_krad_per_hour * (30.0 / middle.r0_rj).powf(-middle.alpha);
123 let outer = RadiationRegion {
124 r_min_rj: 30.0,
125 r_max_rj: 100.0,
126 d0_krad_per_hour: d_at_30,
127 r0_rj: 30.0,
128 alpha: 2.0,
129 };
130
131 JupiterRadiationConfig {
132 regions: vec![inner, middle, outer],
133 magnetopause_rj: 63.0, latitude_factor: 1.0,
135 plasma_sheet_factor: 1.0,
136 storm_factor: 1.0,
137 }
138 }
139
140 pub fn dose_rate_krad_h(&self, r_rj: f64) -> f64 {
144 if r_rj >= self.magnetopause_rj || r_rj < 0.0 {
145 return 0.0;
146 }
147
148 let geometry_mult = self.latitude_factor * self.plasma_sheet_factor * self.storm_factor;
149
150 for region in &self.regions {
151 if r_rj >= region.r_min_rj && r_rj < region.r_max_rj {
152 let rate = region.d0_krad_per_hour * (r_rj / region.r0_rj).powf(-region.alpha);
153 return rate * geometry_mult;
154 }
155 }
156
157 0.0
159 }
160
161 fn analytical_dose_segment(
172 region: &RadiationRegion,
173 r_start_rj: f64,
174 r_end_rj: f64,
175 v_radial_km_s: f64,
176 ) -> f64 {
177 if v_radial_km_s.abs() < 1e-10 {
178 return 0.0; }
180
181 let alpha = region.alpha;
182 let d0 = region.d0_krad_per_hour;
183 let r0 = region.r0_rj;
184
185 let v_rj_per_h = v_radial_km_s * 3600.0 / JUPITER_RADIUS_KM;
189
190 let (a, b) = if r_start_rj < r_end_rj {
191 (r_start_rj, r_end_rj) } else {
193 (r_end_rj, r_start_rj) };
195
196 let coeff = d0 * r0.powf(alpha) / v_rj_per_h.abs();
197
198 if (alpha - 1.0).abs() < 1e-10 {
199 coeff * (b.ln() - a.ln())
202 } else {
203 let exp = 1.0 - alpha;
205 coeff * (b.powf(exp) - a.powf(exp)) / exp
206 }
207 }
208
209 pub fn transit_analysis(
220 &self,
221 r_start_rj: f64,
222 r_end_rj: f64,
223 v_radial_km_s: f64,
224 shield_budget_krad: f64,
225 ) -> JupiterTransitResult {
226 let geometry_mult = self.latitude_factor * self.plasma_sheet_factor * self.storm_factor;
227
228 let mut total_dose = 0.0;
229 let mut region_doses: Vec<(f64, f64, f64)> = Vec::new();
230
231 let effective_end = if r_end_rj > self.magnetopause_rj {
233 self.magnetopause_rj
234 } else {
235 r_end_rj
236 };
237
238 for region in &self.regions {
239 let seg_start = r_start_rj.max(region.r_min_rj);
241 let seg_end = effective_end.min(region.r_max_rj);
242
243 if seg_start < seg_end {
244 let dose = Self::analytical_dose_segment(region, seg_start, seg_end, v_radial_km_s)
245 * geometry_mult;
246 total_dose += dose;
247 region_doses.push((seg_start, seg_end, dose));
248 }
249 }
250
251 let region_fractions: Vec<(f64, f64, f64)> = region_doses
253 .iter()
254 .map(|(a, b, d)| {
255 (
256 *a,
257 *b,
258 if total_dose > 0.0 {
259 d / total_dose
260 } else {
261 0.0
262 },
263 )
264 })
265 .collect();
266
267 let departure_rate = self.dose_rate_krad_h(r_start_rj);
268 let arrival_rate = self.dose_rate_krad_h(r_end_rj.min(self.magnetopause_rj));
269 let peak_rate = departure_rate.max(arrival_rate); let shield_life_at_departure = if departure_rate > 0.0 {
272 shield_budget_krad / departure_rate
273 } else {
274 f64::INFINITY
275 };
276
277 let remaining_budget = shield_budget_krad - total_dose;
278 let shield_life_at_arrival = if arrival_rate > 0.0 && remaining_budget > 0.0 {
279 remaining_budget / arrival_rate
280 } else if remaining_budget <= 0.0 {
281 0.0
282 } else {
283 f64::INFINITY
284 };
285
286 JupiterTransitResult {
287 total_dose_krad: total_dose,
288 departure_dose_rate_krad_h: departure_rate,
289 arrival_dose_rate_krad_h: arrival_rate,
290 shield_life_at_departure_h: shield_life_at_departure,
291 shield_life_at_arrival_h: shield_life_at_arrival,
292 shield_survives: total_dose < shield_budget_krad,
293 region_dose_fractions: region_fractions,
294 peak_dose_rate_krad_h: peak_rate,
295 }
296 }
297}
298
299pub fn ep02_jupiter_escape_analysis() -> Vec<(&'static str, JupiterTransitResult)> {
314 let config = JupiterRadiationConfig::default_model();
315 let departure_rate = config.dose_rate_krad_h(15.0);
316 let shield_budget_42min = departure_rate * (42.0 / 60.0);
317
318 let mut results = Vec::new();
319
320 let result_passive = config.transit_analysis(15.0, 50.0, 7.0, shield_budget_42min);
323 results.push(("弾道脱出 7 km/s", result_passive));
324
325 let result_brach = config.transit_analysis(15.0, 50.0, 60.0, shield_budget_42min);
330 results.push(("加速脱出 60 km/s", result_brach));
331
332 let result_moderate = config.transit_analysis(15.0, 50.0, 20.0, shield_budget_42min);
335 results.push(("損傷状態 20 km/s", result_moderate));
336
337 let mut lat_config = JupiterRadiationConfig::default_model();
339 lat_config.latitude_factor = 0.15; let result_lat = lat_config.transit_analysis(15.0, 50.0, 7.0, shield_budget_42min);
341 results.push(("高緯度脱出 7 km/s", result_lat));
342
343 results
344}
345
346pub fn minimum_survival_velocity(
351 config: &JupiterRadiationConfig,
352 r_start_rj: f64,
353 r_end_rj: f64,
354 shield_budget_krad: f64,
355) -> f64 {
356 let mut v_low = 1.0_f64;
357 let mut v_high = 200.0_f64;
358 for _ in 0..60 {
359 let v_mid = (v_low + v_high) / 2.0;
360 let result = config.transit_analysis(r_start_rj, r_end_rj, v_mid, shield_budget_krad);
361 if result.shield_survives {
362 v_high = v_mid;
363 } else {
364 v_low = v_mid;
365 }
366 }
367 (v_low + v_high) / 2.0
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373
374 #[test]
375 fn test_default_model_creation() {
376 let config = JupiterRadiationConfig::default_model();
377 assert_eq!(config.regions.len(), 3);
378 assert_eq!(config.magnetopause_rj, 63.0);
379 assert_eq!(config.latitude_factor, 1.0);
380 }
381
382 #[test]
383 fn test_dose_rate_at_europa_order_of_magnitude() {
384 let config = JupiterRadiationConfig::default_model();
386 let rate = config.dose_rate_krad_h(9.4);
387 assert!(
389 (rate - 0.616).abs() < 0.01,
390 "Europa rate = {rate:.4} krad/h (expected ~0.616)"
391 );
392 }
393
394 #[test]
395 fn test_dose_rate_at_ganymede_order_of_magnitude() {
396 let config = JupiterRadiationConfig::default_model();
398 let rate = config.dose_rate_krad_h(15.0);
399 assert!(
403 (rate - 0.0616).abs() < 0.005,
404 "Ganymede rate = {rate:.6} krad/h (expected ~0.0616)"
405 );
406 }
407
408 #[test]
409 fn test_dose_rate_decreases_outward() {
410 let config = JupiterRadiationConfig::default_model();
411 let rate_15 = config.dose_rate_krad_h(15.0);
412 let rate_20 = config.dose_rate_krad_h(20.0);
413 let rate_30 = config.dose_rate_krad_h(30.0);
414 let rate_50 = config.dose_rate_krad_h(50.0);
415 assert!(
416 rate_15 > rate_20,
417 "rate should decrease: 15 RJ ({rate_15}) > 20 RJ ({rate_20})"
418 );
419 assert!(
420 rate_20 > rate_30,
421 "rate should decrease: 20 RJ ({rate_20}) > 30 RJ ({rate_30})"
422 );
423 assert!(
424 rate_30 > rate_50,
425 "rate should decrease: 30 RJ ({rate_30}) > 50 RJ ({rate_50})"
426 );
427 }
428
429 #[test]
430 fn test_dose_rate_steep_falloff_ganymede_to_callisto() {
431 let config = JupiterRadiationConfig::default_model();
433 let rate_15 = config.dose_rate_krad_h(15.0);
434 let rate_26 = config.dose_rate_krad_h(26.0);
435 let ratio = rate_15 / rate_26;
436 assert!(
438 ratio > 100.0 && ratio < 2000.0,
439 "Ganymede/Callisto ratio = {ratio:.0}x (expected ~500x)"
440 );
441 }
442
443 #[test]
444 fn test_dose_rate_zero_beyond_magnetopause() {
445 let config = JupiterRadiationConfig::default_model();
446 assert_eq!(config.dose_rate_krad_h(100.0), 0.0);
447 assert_eq!(config.dose_rate_krad_h(63.0), 0.0);
448 assert_eq!(config.dose_rate_krad_h(63.1), 0.0);
449 }
450
451 #[test]
452 fn test_dose_rate_negative_distance() {
453 let config = JupiterRadiationConfig::default_model();
454 assert_eq!(config.dose_rate_krad_h(-1.0), 0.0);
455 }
456
457 #[test]
458 fn test_geometry_multipliers() {
459 let mut config = JupiterRadiationConfig::default_model();
460 let base_rate = config.dose_rate_krad_h(15.0);
461
462 config.storm_factor = 2.0;
463 let storm_rate = config.dose_rate_krad_h(15.0);
464 assert!(
465 (storm_rate - 2.0 * base_rate).abs() < 1e-10,
466 "Storm factor should double rate"
467 );
468
469 config.storm_factor = 1.0;
470 config.latitude_factor = 0.3;
471 let lat_rate = config.dose_rate_krad_h(15.0);
472 assert!(
473 (lat_rate - 0.3 * base_rate).abs() < 1e-10,
474 "Latitude factor should scale rate"
475 );
476 }
477
478 #[test]
479 fn test_analytical_dose_nonzero() {
480 let config = JupiterRadiationConfig::default_model();
481 let result = config.transit_analysis(15.0, 50.0, 7.0, 100.0);
482 assert!(
483 result.total_dose_krad > 0.0,
484 "Total dose should be positive for transit through radiation belt"
485 );
486 }
487
488 #[test]
489 fn test_dose_dominated_by_inner_region() {
490 let config = JupiterRadiationConfig::default_model();
492 let result = config.transit_analysis(15.0, 50.0, 7.0, 100.0);
493
494 assert!(!result.region_dose_fractions.is_empty());
495 let first_fraction = result.region_dose_fractions[0].2;
497 assert!(
498 first_fraction > 0.9,
499 "Inner region should dominate dose: {first_fraction:.3} (expected > 0.9)"
500 );
501 }
502
503 #[test]
504 fn test_shield_42min_at_constant_position_exceeds_transit_dose() {
505 let config = JupiterRadiationConfig::default_model();
516 let departure_rate = config.dose_rate_krad_h(15.0);
517 let shield_budget_42min = departure_rate * (42.0 / 60.0);
518
519 let result = config.transit_analysis(15.0, 50.0, 7.0, shield_budget_42min);
520
521 assert!(
523 !result.shield_survives,
524 "At 7 km/s, shield should NOT survive: dose={:.4} > budget={:.4}",
525 result.total_dose_krad, shield_budget_42min,
526 );
527
528 assert!(
530 (result.shield_life_at_departure_h - 0.7).abs() < 0.01,
531 "Departure shield life = {:.3} h (expected 0.7 h = 42 min)",
532 result.shield_life_at_departure_h,
533 );
534 }
535
536 #[test]
537 fn test_shield_survives_with_higher_velocity() {
538 let config = JupiterRadiationConfig::default_model();
542 let departure_rate = config.dose_rate_krad_h(15.0);
543 let shield_budget_42min = departure_rate * (42.0 / 60.0);
544
545 let threshold = minimum_survival_velocity(&config, 15.0, 50.0, shield_budget_42min);
547
548 let result = config.transit_analysis(15.0, 50.0, threshold + 5.0, shield_budget_42min);
550 assert!(
551 result.shield_survives,
552 "Above threshold ({:.1} km/s), shield should survive: dose={:.6}",
553 threshold, result.total_dose_krad,
554 );
555
556 assert!(
558 threshold > 30.0 && threshold < 80.0,
559 "Threshold = {threshold:.1} km/s (expected 30-80)"
560 );
561 }
562
563 #[test]
564 fn test_shield_survives_high_latitude() {
565 let mut config = JupiterRadiationConfig::default_model();
567 config.latitude_factor = 0.3;
568 let departure_rate = config.dose_rate_krad_h(15.0);
569 let shield_budget_42min = departure_rate * (42.0 / 60.0);
570
571 let result = config.transit_analysis(15.0, 50.0, 7.0, shield_budget_42min);
572 assert!(
575 !result.shield_survives || result.total_dose_krad < shield_budget_42min * 1.1,
576 "High-latitude should be marginal: dose={:.6}, budget={:.6}",
577 result.total_dose_krad,
578 shield_budget_42min,
579 );
580 }
581
582 #[test]
583 fn test_ep02_scenarios_run() {
584 let results = ep02_jupiter_escape_analysis();
585 assert_eq!(results.len(), 4);
586 for (label, result) in &results {
587 assert!(
588 result.total_dose_krad > 0.0,
589 "Scenario '{label}' should have positive dose"
590 );
591 }
592
593 assert!(
595 !results[0].1.shield_survives,
596 "Passive escape should NOT survive"
597 );
598 assert!(
600 results[1].1.shield_survives,
601 "Brachistochrone escape should survive"
602 );
603 }
604
605 #[test]
606 fn test_minimum_velocity_for_shield_survival() {
607 let config = JupiterRadiationConfig::default_model();
609 let departure_rate = config.dose_rate_krad_h(15.0);
610 let shield_budget = departure_rate * (42.0 / 60.0);
611
612 let mut v_low = 5.0_f64;
614 let mut v_high = 100.0_f64;
615 for _ in 0..50 {
616 let v_mid = (v_low + v_high) / 2.0;
617 let result = config.transit_analysis(15.0, 50.0, v_mid, shield_budget);
618 if result.shield_survives {
619 v_high = v_mid;
620 } else {
621 v_low = v_mid;
622 }
623 }
624 let threshold_v = (v_low + v_high) / 2.0;
625
626 assert!(
628 threshold_v > 10.0 && threshold_v < 80.0,
629 "Threshold velocity = {threshold_v:.1} km/s (should be 10-80 km/s)"
630 );
631 }
632
633 #[test]
634 fn test_transit_time_physical() {
635 let dist_km = (50.0 - 15.0) * JUPITER_RADIUS_KM;
638 let time_h = dist_km / 7.0 / 3600.0;
639 assert!(
641 (time_h - 99.4).abs() < 1.0,
642 "Transit time = {time_h:.1} h (expected ~99.4 h)"
643 );
644 }
645
646 #[test]
647 fn test_dose_rate_continuity_at_boundaries() {
648 let config = JupiterRadiationConfig::default_model();
650
651 let rate_inner = config.regions[0].d0_krad_per_hour
653 * (14.999 / config.regions[0].r0_rj).powf(-config.regions[0].alpha);
654 let rate_middle = config.dose_rate_krad_h(15.0);
655 let ratio = rate_inner / rate_middle;
658 assert!(
659 ratio > 0.5 && ratio < 2.0,
660 "15 RJ boundary discontinuity: inner={rate_inner:.4} vs middle={rate_middle:.4} (ratio={ratio:.2})"
661 );
662 }
663
664 #[test]
665 fn test_zero_velocity_returns_zero_dose() {
666 let config = JupiterRadiationConfig::default_model();
667 let result = config.transit_analysis(15.0, 50.0, 0.0, 100.0);
668 assert_eq!(result.total_dose_krad, 0.0);
669 }
670
671 #[test]
672 fn test_higher_velocity_means_lower_dose() {
673 let config = JupiterRadiationConfig::default_model();
675 let result_slow = config.transit_analysis(15.0, 50.0, 5.0, 100.0);
676 let result_fast = config.transit_analysis(15.0, 50.0, 10.0, 100.0);
677 assert!(
678 result_slow.total_dose_krad > result_fast.total_dose_krad,
679 "Slower transit should accumulate more dose: slow={:.4} > fast={:.4}",
680 result_slow.total_dose_krad,
681 result_fast.total_dose_krad,
682 );
683 }
684
685 #[test]
686 fn test_dose_rate_continuity_at_30rj_boundary() {
687 let config = JupiterRadiationConfig::default_model();
689
690 let rate_just_below = config.dose_rate_krad_h(29.999);
692 let rate_at_30 = config.dose_rate_krad_h(30.0);
694
695 let ratio = rate_just_below / rate_at_30;
698 assert!(
699 ratio > 0.5 && ratio < 2.0,
700 "30 RJ boundary: below={rate_just_below:.8} vs at={rate_at_30:.8} (ratio={ratio:.3})"
701 );
702 }
703
704 #[test]
705 fn test_shield_life_increases_outward() {
706 let config = JupiterRadiationConfig::default_model();
710 let departure_rate = config.dose_rate_krad_h(15.0);
711 let large_budget = departure_rate * 10.0; let result = config.transit_analysis(15.0, 50.0, 60.0, large_budget);
714
715 assert!(
718 result.shield_life_at_arrival_h > result.shield_life_at_departure_h,
719 "Shield life should increase outward: arrival={:.2}h > departure={:.2}h",
720 result.shield_life_at_arrival_h,
721 result.shield_life_at_departure_h,
722 );
723 }
724
725 #[test]
726 fn test_dose_inversely_proportional_to_velocity() {
727 let config = JupiterRadiationConfig::default_model();
730 let result_10 = config.transit_analysis(15.0, 50.0, 10.0, 100.0);
731 let result_20 = config.transit_analysis(15.0, 50.0, 20.0, 100.0);
732
733 let ratio = result_10.total_dose_krad / result_20.total_dose_krad;
735 assert!(
736 (ratio - 2.0).abs() < 0.1,
737 "Dose ratio (v=10/v=20) = {ratio:.3} (expected ~2.0)"
738 );
739 }
740
741 #[test]
744 fn test_print_analysis_report() {
745 let config = JupiterRadiationConfig::default_model();
746 let dep_rate = config.dose_rate_krad_h(15.0);
747 let budget = dep_rate * (42.0 / 60.0);
748 let threshold = minimum_survival_velocity(&config, 15.0, 50.0, budget);
749
750 eprintln!("=== EP02 Jupiter Radiation Analysis ===");
751 eprintln!("Departure rate at 15 RJ: {dep_rate:.6} krad/h");
752 eprintln!("Shield budget (42 min): {budget:.6} krad");
753 eprintln!("Min survival velocity: {threshold:.1} km/s");
754
755 let results = ep02_jupiter_escape_analysis();
756 for (label, r) in &results {
757 eprintln!("\n--- {label} ---");
758 eprintln!(
759 " Dose: {:.6} krad, Survives: {}",
760 r.total_dose_krad, r.shield_survives
761 );
762 eprintln!(
763 " Dep rate: {:.6} krad/h, Arr rate: {:.10} krad/h",
764 r.departure_dose_rate_krad_h, r.arrival_dose_rate_krad_h
765 );
766 eprintln!(
767 " Life@dep: {:.2}h ({:.1}min), Life@arr: {:.1}h",
768 r.shield_life_at_departure_h,
769 r.shield_life_at_departure_h * 60.0,
770 r.shield_life_at_arrival_h
771 );
772 }
773
774 eprintln!("\n=== Dose Rate Profile ===");
775 for r in [10, 12, 15, 17, 20, 25, 30, 40, 50, 63] {
776 let rate = config.dose_rate_krad_h(r as f64);
777 eprintln!(" {r:3} RJ: {rate:.8} krad/h");
778 }
779 }
780}