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}