solar_line_core/
plasmoid.rs

1//! Plasmoid perturbation analysis for SOLAR LINE.
2//!
3//! Models the trajectory perturbation from a plasmoid encounter in the
4//! Uranus magnetosphere (EP04). Distinguishes between:
5//!   - **Radiation effect** (biological): modeled elsewhere (EP04 report)
6//!   - **Momentum effect** (trajectory): modeled here
7//!
8//! Based on DiBraccio & Gershman (2019) Voyager 2 observations:
9//!   - Plasmoid dimensions: ~200,000 km × 400,000 km
10//!   - Composition: primarily ionized hydrogen
11//!   - Mass loss rate: ~0.007 kg/s (implies low density)
12//!   - Magnetic field: closed loop topology
13//!
14//! Key physics:
15//!   - Ram pressure: P_ram = 0.5 × ρ × v²
16//!   - Magnetic pressure: P_mag = B² / (2μ₀)
17//!   - Force on ship: F = P × A_cross (cross-sectional area)
18//!   - Impulse: J = F × Δt (transit duration)
19//!   - Velocity change: Δv = J / m_ship
20
21/// Permeability of free space (H/m = kg⋅m/A²/s²)
22const MU_0: f64 = 1.256_637_062e-6;
23
24/// Proton mass (kg) for hydrogen plasma density calculations
25const PROTON_MASS_KG: f64 = 1.672_621_924e-27;
26
27/// Magnetic pressure (Pa) from a magnetic field.
28///
29/// P_mag = B² / (2μ₀)
30///
31/// # Arguments
32/// * `b_tesla` - Magnetic field strength in Tesla
33pub fn magnetic_pressure_pa(b_tesla: f64) -> f64 {
34    b_tesla * b_tesla / (2.0 * MU_0)
35}
36
37/// Ram pressure (Pa) from bulk plasma flow.
38///
39/// P_ram = 0.5 × ρ × v²
40///
41/// # Arguments
42/// * `density_kg_m3` - Plasma mass density in kg/m³
43/// * `velocity_m_s` - Bulk flow velocity in m/s
44pub fn ram_pressure_pa(density_kg_m3: f64, velocity_m_s: f64) -> f64 {
45    0.5 * density_kg_m3 * velocity_m_s * velocity_m_s
46}
47
48/// Plasma number density to mass density conversion.
49///
50/// Assumes pure hydrogen (proton) plasma.
51///
52/// # Arguments
53/// * `n_per_m3` - Number density in particles/m³
54pub fn number_density_to_mass(n_per_m3: f64) -> f64 {
55    n_per_m3 * PROTON_MASS_KG
56}
57
58/// Force on spacecraft from combined magnetic and ram pressure.
59///
60/// F = (P_mag + P_ram) × A_cross
61///
62/// Uses the effective cross-section exposed to the plasmoid flow.
63/// For a magnetically shielded ship, the effective area is the
64/// magnetosphere standoff cross-section, not the physical hull.
65///
66/// # Arguments
67/// * `pressure_pa` - Total dynamic + magnetic pressure (Pa)
68/// * `cross_section_m2` - Effective cross-sectional area (m²)
69pub fn plasmoid_force_n(pressure_pa: f64, cross_section_m2: f64) -> f64 {
70    pressure_pa * cross_section_m2
71}
72
73/// Velocity perturbation from plasmoid encounter.
74///
75/// Δv = F × Δt / m_ship
76///
77/// # Arguments
78/// * `force_n` - Force on spacecraft (N)
79/// * `transit_duration_s` - Duration of plasmoid transit (s)
80/// * `ship_mass_kg` - Spacecraft mass (kg)
81///
82/// Returns velocity perturbation in m/s.
83pub fn velocity_perturbation_m_s(force_n: f64, transit_duration_s: f64, ship_mass_kg: f64) -> f64 {
84    force_n * transit_duration_s / ship_mass_kg
85}
86
87/// Miss distance from a velocity perturbation over remaining travel time.
88///
89/// For a small lateral velocity kick Δv_lateral applied at time t=0,
90/// the lateral displacement after time T is:
91///   miss = Δv_lateral × T
92///
93/// # Arguments
94/// * `dv_lateral_m_s` - Lateral velocity perturbation (m/s)
95/// * `remaining_time_s` - Time from perturbation to destination (s)
96///
97/// Returns miss distance in km.
98pub fn miss_distance_from_perturbation_km(dv_lateral_m_s: f64, remaining_time_s: f64) -> f64 {
99    (dv_lateral_m_s * remaining_time_s) / 1000.0
100}
101
102/// Course-correction ΔV needed to compensate for a perturbation.
103///
104/// For an impulsive correction at time t_corr after the perturbation,
105/// the required ΔV equals the original perturbation (for optimal
106/// immediate correction). For delayed correction:
107///   Δv_corr = miss_distance / remaining_time_after_correction
108///
109/// For immediate correction (best case), Δv_corr ≈ Δv_perturbation.
110///
111/// # Arguments
112/// * `dv_perturbation_m_s` - Velocity perturbation to correct (m/s)
113///
114/// Returns required correction ΔV in m/s.
115pub fn correction_dv_m_s(dv_perturbation_m_s: f64) -> f64 {
116    dv_perturbation_m_s.abs()
117}
118
119/// Full plasmoid perturbation analysis.
120///
121/// Computes trajectory perturbation for a spacecraft transiting
122/// through a plasmoid in the Uranus magnetosphere.
123///
124/// # Arguments
125/// * `b_tesla` - Plasmoid magnetic field strength (T)
126/// * `n_density_per_m3` - Plasma number density (particles/m³)
127/// * `plasma_velocity_m_s` - Bulk plasma flow velocity (m/s)
128/// * `cross_section_m2` - Spacecraft effective cross-section (m²)
129/// * `transit_duration_s` - Plasmoid transit duration (s)
130/// * `ship_mass_kg` - Spacecraft mass (kg)
131/// * `remaining_travel_s` - Time from plasmoid to destination (s)
132pub fn plasmoid_perturbation(
133    b_tesla: f64,
134    n_density_per_m3: f64,
135    plasma_velocity_m_s: f64,
136    cross_section_m2: f64,
137    transit_duration_s: f64,
138    ship_mass_kg: f64,
139    remaining_travel_s: f64,
140) -> PlasmoidPerturbation {
141    let p_mag = magnetic_pressure_pa(b_tesla);
142    let rho = number_density_to_mass(n_density_per_m3);
143    let p_ram = ram_pressure_pa(rho, plasma_velocity_m_s);
144    let total_pressure = p_mag + p_ram;
145
146    let force = plasmoid_force_n(total_pressure, cross_section_m2);
147    let impulse_ns = force * transit_duration_s;
148    let dv = velocity_perturbation_m_s(force, transit_duration_s, ship_mass_kg);
149    let miss_km = miss_distance_from_perturbation_km(dv, remaining_travel_s);
150    let correction = correction_dv_m_s(dv);
151
152    PlasmoidPerturbation {
153        magnetic_pressure_pa: p_mag,
154        ram_pressure_pa: p_ram,
155        total_pressure_pa: total_pressure,
156        force_n: force,
157        impulse_ns,
158        velocity_perturbation_m_s: dv,
159        miss_distance_km: miss_km,
160        correction_dv_m_s: correction,
161    }
162}
163
164/// Results of a plasmoid perturbation analysis.
165#[derive(Debug, Clone)]
166pub struct PlasmoidPerturbation {
167    /// Magnetic pressure component (Pa)
168    pub magnetic_pressure_pa: f64,
169    /// Ram (dynamic) pressure component (Pa)
170    pub ram_pressure_pa: f64,
171    /// Total pressure on spacecraft (Pa)
172    pub total_pressure_pa: f64,
173    /// Force on spacecraft (N)
174    pub force_n: f64,
175    /// Total impulse (N⋅s)
176    pub impulse_ns: f64,
177    /// Velocity change from encounter (m/s)
178    pub velocity_perturbation_m_s: f64,
179    /// Miss distance at destination (km)
180    pub miss_distance_km: f64,
181    /// Required course-correction ΔV (m/s)
182    pub correction_dv_m_s: f64,
183}
184
185/// Parameter scenarios for Uranus plasmoid analysis.
186///
187/// Returns (label, B_tesla, n_per_m3, v_m_s) tuples for
188/// nominal, enhanced, and extreme scenarios based on
189/// Voyager 2 data and magnetosphere models.
190pub fn uranus_plasmoid_scenarios() -> Vec<(&'static str, f64, f64, f64)> {
191    vec![
192        // Nominal: typical magnetotail conditions from Voyager 2
193        // B ~ 1-5 nT, n ~ 0.01-0.1 cm⁻³, v ~ 100-200 km/s
194        ("nominal", 2.0e-9, 0.05e6, 150_000.0),
195        // Enhanced: compressed plasmoid with higher density/field
196        // B ~ 10-20 nT, n ~ 0.5-1 cm⁻³, v ~ 200-300 km/s
197        ("enhanced", 15.0e-9, 0.5e6, 250_000.0),
198        // Extreme: reconnection-driven fast plasmoid
199        // B ~ 50 nT (upper bound), n ~ 5 cm⁻³, v ~ 500 km/s
200        ("extreme", 50.0e-9, 5.0e6, 500_000.0),
201    ]
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn test_magnetic_pressure_earth_field() {
210        // Earth surface field ~50 μT → P = (50e-6)² / (2 × 1.257e-6) ≈ 0.995e-3 Pa
211        let p = magnetic_pressure_pa(50e-6);
212        assert!((p - 9.947e-4).abs() < 1e-5, "P_mag = {p} Pa");
213    }
214
215    #[test]
216    fn test_magnetic_pressure_nanotelsa() {
217        // 10 nT field → P ≈ 4e-11 Pa (very small)
218        let p = magnetic_pressure_pa(10e-9);
219        assert!(p < 1e-9, "P_mag = {p} Pa");
220        assert!(p > 1e-12, "P_mag = {p} Pa");
221    }
222
223    #[test]
224    fn test_ram_pressure() {
225        // Solar wind at 1 AU: ρ ~ 5 cm⁻³ × mp, v ~ 400 km/s
226        // P ≈ 0.5 × 5e6 × 1.67e-27 × (400e3)² ≈ 0.5 × 8.36e-21 × 1.6e11 ≈ 6.7e-10 Pa
227        let rho = number_density_to_mass(5.0e6);
228        let p = ram_pressure_pa(rho, 400_000.0);
229        assert!(p > 1e-10 && p < 1e-8, "P_ram = {p} Pa");
230    }
231
232    #[test]
233    fn test_number_density_conversion() {
234        // 1 proton/cm³ = 1e6 proton/m³
235        let rho = number_density_to_mass(1.0e6);
236        assert!((rho - PROTON_MASS_KG * 1.0e6).abs() < 1e-30);
237    }
238
239    #[test]
240    fn test_velocity_perturbation_basic() {
241        // 1 N force, 480 s, 48e6 kg ship → Δv = 480 / 48e6 = 1e-5 m/s
242        let dv = velocity_perturbation_m_s(1.0, 480.0, 48_000_000.0);
243        assert!((dv - 1e-5).abs() < 1e-10, "dv = {dv} m/s");
244    }
245
246    #[test]
247    fn test_miss_distance_basic() {
248        // 0.001 m/s lateral, 30 days remaining
249        let miss = miss_distance_from_perturbation_km(0.001, 30.0 * 86400.0);
250        // 0.001 * 2.592e6 / 1000 = 2.592 km
251        assert!((miss - 2.592).abs() < 0.01, "miss = {miss} km");
252    }
253
254    #[test]
255    fn test_correction_dv_equals_perturbation() {
256        assert_eq!(correction_dv_m_s(0.005), 0.005);
257        assert_eq!(correction_dv_m_s(-0.005), 0.005);
258    }
259
260    #[test]
261    fn test_ep04_nominal_scenario() {
262        // EP04 Kestrel parameters:
263        // - Mass: 48,000 t = 48e6 kg
264        // - Transit: 8 min = 480 s
265        // - Remaining to Titania: ~9h42m = 34920 s
266        // - Ship cross-section (estimate): ~200 m² (magnetic shield larger)
267        //   Kestrel with magnetic shield → effective standoff ~50 m radius → π×50² ≈ 7854 m²
268        let result = plasmoid_perturbation(
269            2.0e-9,       // 2 nT nominal field
270            0.05e6,       // 0.05 cm⁻³ density
271            150_000.0,    // 150 km/s bulk velocity
272            7854.0,       // π × 50² m² effective cross-section
273            480.0,        // 8 min transit
274            48_000_000.0, // 48,000 t
275            34_920.0,     // 9h42m to Titania
276        );
277
278        // At nominal conditions, perturbation should be very small
279        assert!(
280            result.velocity_perturbation_m_s < 0.001,
281            "nominal Δv = {} m/s (should be < 0.001)",
282            result.velocity_perturbation_m_s
283        );
284        assert!(
285            result.miss_distance_km < 1.0,
286            "nominal miss = {} km",
287            result.miss_distance_km
288        );
289        assert!(result.force_n < 1.0, "nominal force = {} N", result.force_n);
290    }
291
292    #[test]
293    fn test_ep04_extreme_scenario() {
294        // Even at extreme conditions, perturbation on a 48,000 t ship is small
295        let result = plasmoid_perturbation(
296            50.0e-9,      // 50 nT extreme field
297            5.0e6,        // 5 cm⁻³ high density
298            500_000.0,    // 500 km/s fast plasmoid
299            7854.0,       // same cross-section
300            480.0,        // 8 min transit
301            48_000_000.0, // 48,000 t
302            34_920.0,     // 9h42m to Titania
303        );
304
305        // Even extreme scenario: perturbation is negligible for a massive ship
306        // Force should be < 100 N, Δv < 0.01 m/s
307        assert!(
308            result.velocity_perturbation_m_s < 0.1,
309            "extreme Δv = {} m/s",
310            result.velocity_perturbation_m_s
311        );
312        assert!(
313            result.correction_dv_m_s < 0.1,
314            "extreme correction = {} m/s",
315            result.correction_dv_m_s
316        );
317    }
318
319    #[test]
320    fn test_scenarios_provided() {
321        let scenarios = uranus_plasmoid_scenarios();
322        assert_eq!(scenarios.len(), 3);
323        assert_eq!(scenarios[0].0, "nominal");
324        assert_eq!(scenarios[1].0, "enhanced");
325        assert_eq!(scenarios[2].0, "extreme");
326    }
327
328    #[test]
329    fn test_pressure_dominance_at_low_field() {
330        // At low B (2 nT) and higher density (1 cm⁻³), ram pressure dominates
331        let p_mag = magnetic_pressure_pa(2.0e-9);
332        let rho = number_density_to_mass(1.0e6); // 1 cm⁻³
333        let p_ram = ram_pressure_pa(rho, 300_000.0); // 300 km/s
334        assert!(
335            p_ram > p_mag,
336            "p_ram={p_ram} should > p_mag={p_mag} at low B, high density"
337        );
338    }
339
340    #[test]
341    fn test_pressure_dominance_at_high_field() {
342        // At higher B (50 nT), magnetic pressure becomes more significant
343        let p_mag = magnetic_pressure_pa(50.0e-9);
344        // Compare with low-density slow ram pressure
345        let rho = number_density_to_mass(0.01e6);
346        let p_ram = ram_pressure_pa(rho, 100_000.0);
347        // At 50 nT, P_mag ≈ 1e-9 Pa, P_ram for low density ≈ 8.4e-11 Pa
348        // Magnetic pressure should dominate at high field + low density
349        assert!(
350            p_mag > p_ram,
351            "p_mag={p_mag} should > p_ram={p_ram} at high B / low density"
352        );
353    }
354
355    #[test]
356    fn test_radiation_vs_momentum_insight() {
357        // Key insight for EP04 analysis: radiation is dangerous,
358        // but momentum perturbation is negligible for a 48,000 t ship.
359        //
360        // This is because:
361        // 1. The ship is extremely massive (48,000 t)
362        // 2. Plasmoid pressures are ~nPa level
363        // 3. Even with 8 min transit, impulse << ship momentum
364        //
365        // Contrast with radiation: 480 mSv is biologically significant
366        // but mechanically irrelevant.
367        let result = plasmoid_perturbation(
368            15.0e-9,      // enhanced field
369            0.5e6,        // enhanced density
370            250_000.0,    // 250 km/s
371            7854.0,       // shield area
372            480.0,        // 8 min
373            48_000_000.0, // 48,000 t
374            34_920.0,     // 9h42m
375        );
376
377        // Ship velocity change vs. orbital velocity (~5 km/s at Titania orbit)
378        // perturbation / orbital velocity ratio
379        let orbital_v_m_s = 5000.0; // ~5 km/s Titania orbital velocity
380        let ratio = result.velocity_perturbation_m_s / orbital_v_m_s;
381        assert!(
382            ratio < 1e-6,
383            "perturbation/orbital ratio = {ratio} (negligible)"
384        );
385    }
386
387    // --- Edge case tests ---
388
389    #[test]
390    fn zero_magnetic_field() {
391        assert_eq!(magnetic_pressure_pa(0.0), 0.0);
392    }
393
394    #[test]
395    fn zero_density_ram_pressure() {
396        assert_eq!(ram_pressure_pa(0.0, 500_000.0), 0.0);
397    }
398
399    #[test]
400    fn zero_velocity_ram_pressure() {
401        assert_eq!(ram_pressure_pa(1e-20, 0.0), 0.0);
402    }
403
404    #[test]
405    fn zero_transit_duration() {
406        let dv = velocity_perturbation_m_s(100.0, 0.0, 48_000_000.0);
407        assert_eq!(dv, 0.0);
408    }
409
410    #[test]
411    fn zero_remaining_time_zero_miss() {
412        let miss = miss_distance_from_perturbation_km(0.01, 0.0);
413        assert_eq!(miss, 0.0);
414    }
415
416    #[test]
417    fn plasmoid_force_scales_linearly() {
418        let f1 = plasmoid_force_n(1e-9, 1000.0);
419        let f2 = plasmoid_force_n(1e-9, 2000.0);
420        assert!((f2 / f1 - 2.0).abs() < 1e-14);
421    }
422
423    #[test]
424    fn correction_dv_zero_input() {
425        assert_eq!(correction_dv_m_s(0.0), 0.0);
426    }
427
428    #[test]
429    fn plasmoid_perturbation_zero_field_and_density() {
430        let result = plasmoid_perturbation(
431            0.0, // no field
432            0.0, // no density
433            150_000.0,
434            7854.0,
435            480.0,
436            48_000_000.0,
437            34_920.0,
438        );
439        assert_eq!(result.magnetic_pressure_pa, 0.0);
440        assert_eq!(result.ram_pressure_pa, 0.0);
441        assert_eq!(result.velocity_perturbation_m_s, 0.0);
442        assert_eq!(result.miss_distance_km, 0.0);
443    }
444
445    #[test]
446    fn light_ship_larger_perturbation() {
447        // A 300 t ship (EP05 effective mass) gets perturbed more
448        let heavy = plasmoid_perturbation(
449            15.0e-9,
450            0.5e6,
451            250_000.0,
452            7854.0,
453            480.0,
454            48_000_000.0,
455            34_920.0,
456        );
457        let light = plasmoid_perturbation(
458            15.0e-9, 0.5e6, 250_000.0, 7854.0, 480.0, 300_000.0, 34_920.0,
459        );
460        assert!(
461            light.velocity_perturbation_m_s > heavy.velocity_perturbation_m_s * 100.0,
462            "light ship Δv should be >> heavy ship Δv"
463        );
464    }
465
466    #[test]
467    fn scenarios_field_increases_with_severity() {
468        let scenarios = uranus_plasmoid_scenarios();
469        let (_, b0, _, _) = scenarios[0];
470        let (_, b1, _, _) = scenarios[1];
471        let (_, b2, _, _) = scenarios[2];
472        assert!(b0 < b1, "nominal B < enhanced B");
473        assert!(b1 < b2, "enhanced B < extreme B");
474    }
475}