Add Lambert solver, transfer matrix, Dijkstra routing, and route planner UI
- Lambert's problem solver using universal variable method with bisection (handles elliptic, parabolic, hyperbolic transfers + anti-podal cases) - Transfer matrix: precompute pairwise station transfers over time window using Lambert solver with configurable launch velocity - Dijkstra routing on time-expanded graph (station × week nodes, transfer + wait edges) to find minimum-time routes - Route Planner UI: from/to station dropdowns, search window selector (1-10 years), "Find Optimal Route" button with results card - Route visualization: orange dashed trajectory lines with arrow heads and leg numbers on the 2D canvas - Tested: Mercury L1 → Jupiter L1 computes 6-month direct transfer at 30 km/s — physically reasonable Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
150
crates/mass-driver-core/src/transfer_matrix.rs
Normal file
150
crates/mass-driver-core/src/transfer_matrix.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
use crate::station::{Station, station_position};
|
||||
use orbital_mechanics::bodies;
|
||||
use orbital_mechanics::constants::*;
|
||||
use orbital_mechanics::lambert;
|
||||
use nalgebra::Vector3;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Precomputed transfer costs between station pairs over time.
|
||||
///
|
||||
/// Stored as a sparse list of feasible transfers to save memory.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TransferMatrix {
|
||||
pub station_count: usize,
|
||||
pub week_count: usize,
|
||||
/// Start Julian Date
|
||||
pub start_jd: f64,
|
||||
/// Sparse entries: (from, to, departure_week, travel_weeks)
|
||||
pub entries: Vec<TransferEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TransferEntry {
|
||||
pub from: u16,
|
||||
pub to: u16,
|
||||
pub departure_week: u16,
|
||||
pub travel_weeks: u16,
|
||||
}
|
||||
|
||||
/// Compute the transfer matrix for a set of stations.
|
||||
///
|
||||
/// For each station pair and each weekly departure time, solve Lambert's problem
|
||||
/// to find the minimum transfer time achievable at the given launch velocity.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `stations` - The station network
|
||||
/// * `max_launch_v_kms` - Maximum launch velocity in km/s
|
||||
/// * `start_jd` - Start of the time window (Julian Date)
|
||||
/// * `week_count` - Number of weeks to compute
|
||||
/// * `progress_callback` - Called with (completed, total) for progress reporting
|
||||
pub fn compute_transfer_matrix(
|
||||
stations: &[Station],
|
||||
max_launch_v_kms: f64,
|
||||
start_jd: f64,
|
||||
week_count: usize,
|
||||
mut progress_callback: impl FnMut(usize, usize),
|
||||
) -> TransferMatrix {
|
||||
let all_bodies = bodies::all_bodies();
|
||||
let n = stations.len();
|
||||
let total_work = n * (n - 1) * week_count;
|
||||
let mut completed = 0;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
|
||||
// Candidate TOFs to try (in days) - logarithmic spacing from weeks to years
|
||||
let candidate_tofs_days: Vec<f64> = {
|
||||
let mut tofs = Vec::new();
|
||||
// From 1 week to 5 years, ~20 samples
|
||||
let min_days: f64 = 7.0;
|
||||
let max_days: f64 = 5.0 * 365.25;
|
||||
for i in 0..20 {
|
||||
let t: f64 = i as f64 / 19.0;
|
||||
let days = min_days * (max_days / min_days).powf(t);
|
||||
tofs.push(days);
|
||||
}
|
||||
tofs
|
||||
};
|
||||
|
||||
for week in 0..week_count {
|
||||
let jd = start_jd + (week as f64) * DAYS_PER_WEEK;
|
||||
|
||||
// Precompute all station positions at this epoch
|
||||
let station_positions: Vec<Vector3<f64>> = stations
|
||||
.iter()
|
||||
.map(|s| station_position(s, &all_bodies, jd))
|
||||
.collect();
|
||||
|
||||
for from in 0..n {
|
||||
for to in 0..n {
|
||||
if from == to {
|
||||
continue;
|
||||
}
|
||||
|
||||
let r1 = station_positions[from] * AU_KM; // Convert AU to km
|
||||
let r2 = station_positions[to] * AU_KM;
|
||||
|
||||
// Try different TOFs and find the minimum that works
|
||||
let mut best_travel_weeks: Option<u16> = None;
|
||||
|
||||
for &tof_days in &candidate_tofs_days {
|
||||
let tof_seconds = tof_days * SECONDS_PER_DAY;
|
||||
|
||||
if let Some(sol) = lambert::solve_lambert(r1, r2, tof_seconds, MU_SUN, true) {
|
||||
// Check if departure delta-v is within launch capability
|
||||
// For simplicity, we check if the departure velocity magnitude
|
||||
// is achievable (station is co-moving with its parent body)
|
||||
let v1_mag = sol.v1.norm();
|
||||
|
||||
// Station orbital velocity (approximate)
|
||||
let r1_au = station_positions[from].norm();
|
||||
let v_station = if r1_au > 0.01 {
|
||||
(MU_SUN / (r1_au * AU_KM)).sqrt() // km/s
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// The departure delta-v is roughly v1 - v_station
|
||||
// This is simplified — in reality we'd need the vector difference
|
||||
let dv_approx = (v1_mag - v_station).abs();
|
||||
|
||||
if dv_approx <= max_launch_v_kms {
|
||||
let travel_weeks = (tof_days / 7.0).ceil() as u16;
|
||||
if travel_weeks > 0 && travel_weeks < 5200 {
|
||||
match best_travel_weeks {
|
||||
None => best_travel_weeks = Some(travel_weeks),
|
||||
Some(prev) if travel_weeks < prev => {
|
||||
best_travel_weeks = Some(travel_weeks);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(travel_weeks) = best_travel_weeks {
|
||||
entries.push(TransferEntry {
|
||||
from: from as u16,
|
||||
to: to as u16,
|
||||
departure_week: week as u16,
|
||||
travel_weeks,
|
||||
});
|
||||
}
|
||||
|
||||
completed += 1;
|
||||
if completed % 1000 == 0 {
|
||||
progress_callback(completed, total_work);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
progress_callback(total_work, total_work);
|
||||
|
||||
TransferMatrix {
|
||||
station_count: n,
|
||||
week_count,
|
||||
start_jd,
|
||||
entries,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user