Initial project setup: Rust/WASM solar system simulator with SvelteKit frontend
- Rust workspace with 4 crates: orbital-mechanics, mass-driver-core, mass-driver-wasm, mass-driver-backend - Keplerian orbital mechanics engine with JPL elements for 14 bodies (Sun, 8 planets, Pluto, Ceres, Europa, Titan, Ganymede) - Kepler equation solver and orbital position computation compiled to WASM - SvelteKit frontend with Tailwind CSS, Canvas2D renderer showing animated solar system - Orbit ellipses, planet dots with labels, Sun glow, grid, scale bar, pan/zoom controls - Time controls (play/pause, 5 speed levels, date picker) driving live simulation - 2D/3D view toggle (3D placeholder for Threlte integration) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
16
crates/mass-driver-backend/Cargo.toml
Normal file
16
crates/mass-driver-backend/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "mass-driver-backend"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
orbital-mechanics = { path = "../orbital-mechanics" }
|
||||
mass-driver-core = { path = "../mass-driver-core" }
|
||||
axum = "0.8"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres"] }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tower-http = { version = "0.6", features = ["cors", "compression-gzip"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
3
crates/mass-driver-backend/src/main.rs
Normal file
3
crates/mass-driver-backend/src/main.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("mass-driver backend — not yet implemented");
|
||||
}
|
||||
13
crates/mass-driver-core/Cargo.toml
Normal file
13
crates/mass-driver-core/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "mass-driver-core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
orbital-mechanics = { path = "../orbital-mechanics" }
|
||||
nalgebra = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
petgraph = "0.8"
|
||||
ordered-float = "5"
|
||||
rmp-serde = "1"
|
||||
2
crates/mass-driver-core/src/lib.rs
Normal file
2
crates/mass-driver-core/src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod station;
|
||||
pub mod route;
|
||||
24
crates/mass-driver-core/src/route.rs
Normal file
24
crates/mass-driver-core/src/route.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A single leg of a route between two stations.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RouteLeg {
|
||||
pub from_station: usize,
|
||||
pub to_station: usize,
|
||||
/// Departure week index (0 = first week of simulation)
|
||||
pub departure_week: u32,
|
||||
/// Arrival week index
|
||||
pub arrival_week: u32,
|
||||
/// Weeks spent waiting at departure station before this leg
|
||||
pub wait_weeks: u32,
|
||||
}
|
||||
|
||||
/// The result of a route computation.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RouteResult {
|
||||
pub legs: Vec<RouteLeg>,
|
||||
/// Total elapsed time in weeks from departure to final arrival
|
||||
pub total_time_weeks: u32,
|
||||
/// The week index when the package departs the origin
|
||||
pub departure_week: u32,
|
||||
}
|
||||
44
crates/mass-driver-core/src/station.rs
Normal file
44
crates/mass-driver-core/src/station.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// How a station is placed in the solar system.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum StationPlacement {
|
||||
/// Station in heliocentric orbit
|
||||
SolarOrbit {
|
||||
a: f64, // Semi-major axis (AU)
|
||||
e: f64, // Eccentricity
|
||||
i: f64, // Inclination (degrees)
|
||||
omega: f64, // Longitude of ascending node (degrees)
|
||||
w: f64, // Argument of perihelion (degrees)
|
||||
m0: f64, // Mean anomaly at epoch (degrees)
|
||||
},
|
||||
/// Station in orbit around a planet
|
||||
PlanetaryOrbit {
|
||||
parent_body_id: usize,
|
||||
altitude_km: f64,
|
||||
inclination: f64,
|
||||
},
|
||||
/// Station at a Lagrange point
|
||||
LagrangePoint {
|
||||
primary_body_id: usize, // e.g., Sun
|
||||
secondary_body_id: usize, // e.g., Earth
|
||||
point: LagrangePointId,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum LagrangePointId {
|
||||
L1,
|
||||
L2,
|
||||
L3,
|
||||
L4,
|
||||
L5,
|
||||
}
|
||||
|
||||
/// A mass driver relay station.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Station {
|
||||
pub id: usize,
|
||||
pub name: String,
|
||||
pub placement: StationPlacement,
|
||||
}
|
||||
8
crates/mass-driver-wasm/.vite/deps/_metadata.json
Normal file
8
crates/mass-driver-wasm/.vite/deps/_metadata.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"hash": "bef44088",
|
||||
"configHash": "3d8fe0fc",
|
||||
"lockfileHash": "e3b0c442",
|
||||
"browserHash": "7d9faa18",
|
||||
"optimized": {},
|
||||
"chunks": {}
|
||||
}
|
||||
3
crates/mass-driver-wasm/.vite/deps/package.json
Normal file
3
crates/mass-driver-wasm/.vite/deps/package.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
20
crates/mass-driver-wasm/Cargo.toml
Normal file
20
crates/mass-driver-wasm/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "mass-driver-wasm"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
orbital-mechanics = { path = "../orbital-mechanics" }
|
||||
mass-driver-core = { path = "../mass-driver-core" }
|
||||
wasm-bindgen = "0.2"
|
||||
serde-wasm-bindgen = "0.6"
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
js-sys = "0.3"
|
||||
web-sys = { version = "0.3", features = ["console"] }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3"
|
||||
59
crates/mass-driver-wasm/src/api.rs
Normal file
59
crates/mass-driver-wasm/src/api.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use orbital_mechanics::bodies;
|
||||
use orbital_mechanics::orbits;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// Initialize the WASM module. Called once on page load.
|
||||
#[wasm_bindgen]
|
||||
pub fn init() -> Result<(), JsValue> {
|
||||
web_sys::console::log_1(&"mass-driver WASM initialized".into());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the number of celestial bodies in the simulation.
|
||||
#[wasm_bindgen]
|
||||
pub fn get_body_count() -> usize {
|
||||
bodies::all_bodies().len()
|
||||
}
|
||||
|
||||
/// Get positions of all celestial bodies at a given Julian Date.
|
||||
///
|
||||
/// Returns a Float64Array of [x0, y0, z0, x1, y1, z1, ...] in AU.
|
||||
#[wasm_bindgen]
|
||||
pub fn get_body_positions_at_epoch(jd: f64) -> Vec<f64> {
|
||||
let all = bodies::all_bodies();
|
||||
orbits::all_positions_at_epoch(&all, jd)
|
||||
}
|
||||
|
||||
/// Get body names as a JSON array of strings.
|
||||
#[wasm_bindgen]
|
||||
pub fn get_body_names() -> String {
|
||||
let all = bodies::all_bodies();
|
||||
let names: Vec<&str> = all.iter().map(|b| b.name).collect();
|
||||
serde_json::to_string(&names).unwrap()
|
||||
}
|
||||
|
||||
/// Get body colors as a flat array [r0, g0, b0, r1, g1, b1, ...].
|
||||
#[wasm_bindgen]
|
||||
pub fn get_body_colors() -> Vec<u8> {
|
||||
let all = bodies::all_bodies();
|
||||
all.iter().flat_map(|b| b.color.iter().copied()).collect()
|
||||
}
|
||||
|
||||
/// Get body radii in km as a Float64Array.
|
||||
#[wasm_bindgen]
|
||||
pub fn get_body_radii() -> Vec<f64> {
|
||||
let all = bodies::all_bodies();
|
||||
all.iter().map(|b| b.radius_km).collect()
|
||||
}
|
||||
|
||||
/// Get orbit points for a given body, sampled around its full orbit.
|
||||
/// Returns a Float64Array of [x0, y0, z0, x1, y1, z1, ...] in AU.
|
||||
/// `samples` is the number of points around the orbit.
|
||||
#[wasm_bindgen]
|
||||
pub fn get_orbit_points(body_id: usize, jd: f64, samples: usize) -> Vec<f64> {
|
||||
let all = bodies::all_bodies();
|
||||
if body_id >= all.len() || body_id == bodies::id::SUN {
|
||||
return vec![];
|
||||
}
|
||||
orbits::orbit_points(&all, body_id, jd, samples)
|
||||
}
|
||||
3
crates/mass-driver-wasm/src/lib.rs
Normal file
3
crates/mass-driver-wasm/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod api;
|
||||
|
||||
pub use api::*;
|
||||
8
crates/orbital-mechanics/Cargo.toml
Normal file
8
crates/orbital-mechanics/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "orbital-mechanics"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
nalgebra = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
413
crates/orbital-mechanics/src/bodies.rs
Normal file
413
crates/orbital-mechanics/src/bodies.rs
Normal file
@@ -0,0 +1,413 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Keplerian orbital elements at J2000.0 epoch.
|
||||
///
|
||||
/// For planets: heliocentric, ecliptic coordinates.
|
||||
/// Values from JPL "Approximate Positions of the Planets"
|
||||
/// (https://ssd.jpl.nasa.gov/planets/approx_pos.html)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct KeplerianElements {
|
||||
/// Semi-major axis (AU)
|
||||
pub a: f64,
|
||||
/// Eccentricity
|
||||
pub e: f64,
|
||||
/// Inclination (degrees)
|
||||
pub i: f64,
|
||||
/// Mean longitude (degrees)
|
||||
pub l: f64,
|
||||
/// Longitude of perihelion (degrees)
|
||||
pub w_bar: f64,
|
||||
/// Longitude of ascending node (degrees)
|
||||
pub omega: f64,
|
||||
}
|
||||
|
||||
/// Rates of change of Keplerian elements per Julian century.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct KeplerianRates {
|
||||
pub a: f64,
|
||||
pub e: f64,
|
||||
pub i: f64,
|
||||
pub l: f64,
|
||||
pub w_bar: f64,
|
||||
pub omega: f64,
|
||||
}
|
||||
|
||||
/// A celestial body with orbital elements and physical properties.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CelestialBody {
|
||||
pub name: &'static str,
|
||||
pub id: usize,
|
||||
/// Gravitational parameter (km³/s²)
|
||||
pub mu: f64,
|
||||
/// Mean radius (km)
|
||||
pub radius_km: f64,
|
||||
/// Keplerian elements at J2000.0
|
||||
pub elements: KeplerianElements,
|
||||
/// Rates of change per century
|
||||
pub rates: KeplerianRates,
|
||||
/// Display color as [r, g, b]
|
||||
pub color: [u8; 3],
|
||||
}
|
||||
|
||||
/// Enumeration of body IDs for quick lookup.
|
||||
pub mod id {
|
||||
pub const SUN: usize = 0;
|
||||
pub const MERCURY: usize = 1;
|
||||
pub const VENUS: usize = 2;
|
||||
pub const EARTH: usize = 3;
|
||||
pub const MARS: usize = 4;
|
||||
pub const JUPITER: usize = 5;
|
||||
pub const SATURN: usize = 6;
|
||||
pub const URANUS: usize = 7;
|
||||
pub const NEPTUNE: usize = 8;
|
||||
pub const PLUTO: usize = 9;
|
||||
pub const CERES: usize = 10;
|
||||
pub const EUROPA: usize = 11;
|
||||
pub const TITAN: usize = 12;
|
||||
pub const GANYMEDE: usize = 13;
|
||||
}
|
||||
|
||||
/// Returns all celestial bodies with JPL Keplerian elements.
|
||||
///
|
||||
/// Planet elements from JPL "Approximate Positions of the Planets"
|
||||
/// valid for 3000 BC to 3000 AD.
|
||||
pub fn all_bodies() -> Vec<CelestialBody> {
|
||||
vec![
|
||||
// Sun (placeholder — no orbit, always at origin)
|
||||
CelestialBody {
|
||||
name: "Sun",
|
||||
id: id::SUN,
|
||||
mu: 1.327_124_400_41e11,
|
||||
radius_km: 695_700.0,
|
||||
elements: KeplerianElements {
|
||||
a: 0.0, e: 0.0, i: 0.0, l: 0.0, w_bar: 0.0, omega: 0.0,
|
||||
},
|
||||
rates: KeplerianRates {
|
||||
a: 0.0, e: 0.0, i: 0.0, l: 0.0, w_bar: 0.0, omega: 0.0,
|
||||
},
|
||||
color: [255, 204, 0],
|
||||
},
|
||||
// Mercury
|
||||
CelestialBody {
|
||||
name: "Mercury",
|
||||
id: id::MERCURY,
|
||||
mu: 2.2032e4,
|
||||
radius_km: 2_439.7,
|
||||
elements: KeplerianElements {
|
||||
a: 0.387_099_27,
|
||||
e: 0.205_301_04,
|
||||
i: 7.004_986_8,
|
||||
l: 252.250_32,
|
||||
w_bar: 77.457_796,
|
||||
omega: 48.330_893,
|
||||
},
|
||||
rates: KeplerianRates {
|
||||
a: 0.000_000_37,
|
||||
e: 0.000_010_02,
|
||||
i: -0.005_946_30,
|
||||
l: 149_472.674_11,
|
||||
w_bar: 0.160_476_24,
|
||||
omega: -0.125_340_2,
|
||||
},
|
||||
color: [183, 168, 153],
|
||||
},
|
||||
// Venus
|
||||
CelestialBody {
|
||||
name: "Venus",
|
||||
id: id::VENUS,
|
||||
mu: 3.24859e5,
|
||||
radius_km: 6_051.8,
|
||||
elements: KeplerianElements {
|
||||
a: 0.723_332_01,
|
||||
e: 0.006_773_23,
|
||||
i: 3.394_662_0,
|
||||
l: 181.979_73,
|
||||
w_bar: 131.602_467,
|
||||
omega: 76.679_920,
|
||||
},
|
||||
rates: KeplerianRates {
|
||||
a: 0.000_000_60,
|
||||
e: -0.000_048_98,
|
||||
i: -0.007_889_0,
|
||||
l: 58_517.815_39,
|
||||
w_bar: 0.002_688_29,
|
||||
omega: -0.278_008_0,
|
||||
},
|
||||
color: [231, 196, 150],
|
||||
},
|
||||
// Earth
|
||||
CelestialBody {
|
||||
name: "Earth",
|
||||
id: id::EARTH,
|
||||
mu: 3.986e5,
|
||||
radius_km: 6_371.0,
|
||||
elements: KeplerianElements {
|
||||
a: 1.000_002_61,
|
||||
e: 0.016_711_23,
|
||||
i: -0.000_015_31,
|
||||
l: 100.464_572,
|
||||
w_bar: 102.937_682,
|
||||
omega: 0.0,
|
||||
},
|
||||
rates: KeplerianRates {
|
||||
a: 0.000_005_62,
|
||||
e: -0.000_043_92,
|
||||
i: -0.012_946_68,
|
||||
l: 35_999.372_47,
|
||||
w_bar: 0.323_281_26,
|
||||
omega: 0.0,
|
||||
},
|
||||
color: [100, 149, 237],
|
||||
},
|
||||
// Mars
|
||||
CelestialBody {
|
||||
name: "Mars",
|
||||
id: id::MARS,
|
||||
mu: 4.2828e4,
|
||||
radius_km: 3_389.5,
|
||||
elements: KeplerianElements {
|
||||
a: 1.523_679_40,
|
||||
e: 0.093_400_62,
|
||||
i: 1.849_726_0,
|
||||
l: -4.553_432,
|
||||
w_bar: -23.943_629_5,
|
||||
omega: 49.559_539,
|
||||
},
|
||||
rates: KeplerianRates {
|
||||
a: 0.000_018_47,
|
||||
e: 0.000_079_48,
|
||||
i: -0.006_813_52,
|
||||
l: 19_140.303_00,
|
||||
w_bar: 0.445_265_40,
|
||||
omega: -0.295_257_0,
|
||||
},
|
||||
color: [193, 68, 14],
|
||||
},
|
||||
// Jupiter
|
||||
CelestialBody {
|
||||
name: "Jupiter",
|
||||
id: id::JUPITER,
|
||||
mu: 1.26687e8,
|
||||
radius_km: 69_911.0,
|
||||
elements: KeplerianElements {
|
||||
a: 5.202_603_0,
|
||||
e: 0.048_498_26,
|
||||
i: 1.303_270,
|
||||
l: 34.396_441,
|
||||
w_bar: 14.728_479,
|
||||
omega: 100.473_909,
|
||||
},
|
||||
rates: KeplerianRates {
|
||||
a: 0.000_016_05,
|
||||
e: 0.000_163_94,
|
||||
i: -0.001_983_72,
|
||||
l: 3_034.746_12,
|
||||
w_bar: 0.216_519_4,
|
||||
omega: 0.205_831_6,
|
||||
},
|
||||
color: [216, 165, 108],
|
||||
},
|
||||
// Saturn
|
||||
CelestialBody {
|
||||
name: "Saturn",
|
||||
id: id::SATURN,
|
||||
mu: 3.7931e7,
|
||||
radius_km: 58_232.0,
|
||||
elements: KeplerianElements {
|
||||
a: 9.554_909_6,
|
||||
e: 0.055_086_22,
|
||||
i: 2.488_878,
|
||||
l: 49.954_244,
|
||||
w_bar: 92.598_78,
|
||||
omega: 113.662_424,
|
||||
},
|
||||
rates: KeplerianRates {
|
||||
a: -0.000_025_06,
|
||||
e: -0.000_050_32,
|
||||
i: 0.004_530_33,
|
||||
l: 1_222.113_94,
|
||||
w_bar: -0.411_897_0,
|
||||
omega: -0.288_668_0,
|
||||
},
|
||||
color: [210, 180, 140],
|
||||
},
|
||||
// Uranus
|
||||
CelestialBody {
|
||||
name: "Uranus",
|
||||
id: id::URANUS,
|
||||
mu: 5.7940e6,
|
||||
radius_km: 25_362.0,
|
||||
elements: KeplerianElements {
|
||||
a: 19.218_446_2,
|
||||
e: 0.047_317_65,
|
||||
i: 0.773_196,
|
||||
l: 313.238_105,
|
||||
w_bar: 170.954_276,
|
||||
omega: 74.016_925,
|
||||
},
|
||||
rates: KeplerianRates {
|
||||
a: -0.000_198_58,
|
||||
e: -0.000_004_07,
|
||||
i: -0.002_446_1,
|
||||
l: 428.481_40,
|
||||
w_bar: 0.408_030_0,
|
||||
omega: 0.046_863_9,
|
||||
},
|
||||
color: [172, 229, 238],
|
||||
},
|
||||
// Neptune
|
||||
CelestialBody {
|
||||
name: "Neptune",
|
||||
id: id::NEPTUNE,
|
||||
mu: 6.8351e6,
|
||||
radius_km: 24_622.0,
|
||||
elements: KeplerianElements {
|
||||
a: 30.110_386_9,
|
||||
e: 0.008_590_48,
|
||||
i: 1.769_952,
|
||||
l: -55.120_029,
|
||||
w_bar: 44.964_762,
|
||||
omega: 131.784_057,
|
||||
},
|
||||
rates: KeplerianRates {
|
||||
a: 0.000_006_08,
|
||||
e: 0.000_006_30,
|
||||
i: 0.000_062_6,
|
||||
l: 218.457_64,
|
||||
w_bar: -0.327_55,
|
||||
omega: -0.006_066_2,
|
||||
},
|
||||
color: [63, 84, 186],
|
||||
},
|
||||
// Pluto (approximate elements)
|
||||
CelestialBody {
|
||||
name: "Pluto",
|
||||
id: id::PLUTO,
|
||||
mu: 8.71e2,
|
||||
radius_km: 1_188.3,
|
||||
elements: KeplerianElements {
|
||||
a: 39.481_686_77,
|
||||
e: 0.248_905_87,
|
||||
i: 17.140_175,
|
||||
l: 238.928_06,
|
||||
w_bar: 224.068_76,
|
||||
omega: 110.303_47,
|
||||
},
|
||||
rates: KeplerianRates {
|
||||
a: -0.007_608_12,
|
||||
e: 0.000_060_65,
|
||||
i: 0.003_681_9,
|
||||
l: 145.205_80,
|
||||
w_bar: -0.045_07,
|
||||
omega: -0.018_16,
|
||||
},
|
||||
color: [190, 180, 170],
|
||||
},
|
||||
// Ceres (approximate)
|
||||
CelestialBody {
|
||||
name: "Ceres",
|
||||
id: id::CERES,
|
||||
mu: 63.13,
|
||||
radius_km: 469.73,
|
||||
elements: KeplerianElements {
|
||||
a: 2.769_1,
|
||||
e: 0.075_8,
|
||||
i: 10.594,
|
||||
l: 153.94,
|
||||
w_bar: 73.597 + 80.329,
|
||||
omega: 80.329,
|
||||
},
|
||||
rates: KeplerianRates {
|
||||
a: 0.0,
|
||||
e: 0.0,
|
||||
i: 0.0,
|
||||
l: 78_362.73, // ~4.6 yr period
|
||||
w_bar: 0.0,
|
||||
omega: 0.0,
|
||||
},
|
||||
color: [148, 137, 121],
|
||||
},
|
||||
// Europa (orbits Jupiter — elements are Jovicentric)
|
||||
// For visualization, we compute Jupiter's position and add Europa's offset
|
||||
CelestialBody {
|
||||
name: "Europa",
|
||||
id: id::EUROPA,
|
||||
mu: 3_202.7,
|
||||
radius_km: 1_560.8,
|
||||
elements: KeplerianElements {
|
||||
a: 0.004_486, // ~671,100 km in AU
|
||||
e: 0.009_4,
|
||||
i: 0.47,
|
||||
l: 171.016,
|
||||
w_bar: 0.0,
|
||||
omega: 0.0,
|
||||
},
|
||||
rates: KeplerianRates {
|
||||
a: 0.0,
|
||||
e: 0.0,
|
||||
i: 0.0,
|
||||
l: 101_375.3, // ~3.55 day period
|
||||
w_bar: 0.0,
|
||||
omega: 0.0,
|
||||
},
|
||||
color: [200, 190, 160],
|
||||
},
|
||||
// Titan (orbits Saturn)
|
||||
CelestialBody {
|
||||
name: "Titan",
|
||||
id: id::TITAN,
|
||||
mu: 8_978.1,
|
||||
radius_km: 2_574.7,
|
||||
elements: KeplerianElements {
|
||||
a: 0.008_168, // ~1,221,870 km in AU
|
||||
e: 0.028_8,
|
||||
i: 0.34,
|
||||
l: 120.0,
|
||||
w_bar: 0.0,
|
||||
omega: 0.0,
|
||||
},
|
||||
rates: KeplerianRates {
|
||||
a: 0.0,
|
||||
e: 0.0,
|
||||
i: 0.0,
|
||||
l: 22_577.0, // ~15.95 day period
|
||||
w_bar: 0.0,
|
||||
omega: 0.0,
|
||||
},
|
||||
color: [230, 190, 100],
|
||||
},
|
||||
// Ganymede (orbits Jupiter)
|
||||
CelestialBody {
|
||||
name: "Ganymede",
|
||||
id: id::GANYMEDE,
|
||||
mu: 9_887.8,
|
||||
radius_km: 2_634.1,
|
||||
elements: KeplerianElements {
|
||||
a: 0.007_155, // ~1,070,400 km in AU
|
||||
e: 0.001_3,
|
||||
i: 0.20,
|
||||
l: 317.54,
|
||||
w_bar: 0.0,
|
||||
omega: 0.0,
|
||||
},
|
||||
rates: KeplerianRates {
|
||||
a: 0.0,
|
||||
e: 0.0,
|
||||
i: 0.0,
|
||||
l: 50_317.6, // ~7.15 day period
|
||||
w_bar: 0.0,
|
||||
omega: 0.0,
|
||||
},
|
||||
color: [160, 145, 130],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/// Returns the parent body ID for moons (None for planets/Sun).
|
||||
pub fn parent_body(body_id: usize) -> Option<usize> {
|
||||
match body_id {
|
||||
id::EUROPA | id::GANYMEDE => Some(id::JUPITER),
|
||||
id::TITAN => Some(id::SATURN),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
26
crates/orbital-mechanics/src/constants.rs
Normal file
26
crates/orbital-mechanics/src/constants.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
/// Gravitational parameter of the Sun (km³/s²)
|
||||
pub const MU_SUN: f64 = 1.327_124_400_41e11;
|
||||
|
||||
/// One Astronomical Unit in kilometers
|
||||
pub const AU_KM: f64 = 1.495_978_707e8;
|
||||
|
||||
/// Seconds per day
|
||||
pub const SECONDS_PER_DAY: f64 = 86_400.0;
|
||||
|
||||
/// Days per Julian century
|
||||
pub const DAYS_PER_CENTURY: f64 = 36_525.0;
|
||||
|
||||
/// J2000.0 epoch in Julian Date
|
||||
pub const J2000_JD: f64 = 2_451_545.0;
|
||||
|
||||
/// Days per week
|
||||
pub const DAYS_PER_WEEK: f64 = 7.0;
|
||||
|
||||
/// Pi
|
||||
pub const PI: f64 = std::f64::consts::PI;
|
||||
|
||||
/// Two Pi
|
||||
pub const TAU: f64 = std::f64::consts::TAU;
|
||||
|
||||
/// Degrees to radians
|
||||
pub const DEG_TO_RAD: f64 = PI / 180.0;
|
||||
56
crates/orbital-mechanics/src/kepler.rs
Normal file
56
crates/orbital-mechanics/src/kepler.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use crate::constants::TAU;
|
||||
|
||||
/// Solve Kepler's equation M = E - e*sin(E) for eccentric anomaly E.
|
||||
///
|
||||
/// Uses Newton-Raphson iteration. Converges in 3-6 iterations for
|
||||
/// eccentricities < 0.9. All angles in radians.
|
||||
pub fn solve_kepler(mean_anomaly: f64, eccentricity: f64) -> f64 {
|
||||
let m = mean_anomaly % TAU;
|
||||
let e = eccentricity;
|
||||
|
||||
// Initial guess (Markley's starting value for better convergence)
|
||||
let mut ea = if e < 0.8 {
|
||||
m + e * m.sin()
|
||||
} else {
|
||||
std::f64::consts::PI
|
||||
};
|
||||
|
||||
// Newton-Raphson iteration
|
||||
for _ in 0..50 {
|
||||
let delta = (ea - e * ea.sin() - m) / (1.0 - e * ea.cos());
|
||||
ea -= delta;
|
||||
if delta.abs() < 1e-12 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ea
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_circular_orbit() {
|
||||
// For e=0, E should equal M
|
||||
let m = 1.0;
|
||||
let e = solve_kepler(m, 0.0);
|
||||
assert!((e - m).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_known_values() {
|
||||
// For M=π, any eccentricity, E should be π
|
||||
let e = solve_kepler(std::f64::consts::PI, 0.5);
|
||||
assert!((e - std::f64::consts::PI).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_high_eccentricity() {
|
||||
let ea = solve_kepler(1.0, 0.9);
|
||||
// Verify it satisfies Kepler's equation
|
||||
let m_check = ea - 0.9 * ea.sin();
|
||||
assert!((m_check - 1.0).abs() < 1e-10);
|
||||
}
|
||||
}
|
||||
6
crates/orbital-mechanics/src/lib.rs
Normal file
6
crates/orbital-mechanics/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod bodies;
|
||||
pub mod constants;
|
||||
pub mod kepler;
|
||||
pub mod orbits;
|
||||
|
||||
pub use nalgebra::Vector3;
|
||||
192
crates/orbital-mechanics/src/orbits.rs
Normal file
192
crates/orbital-mechanics/src/orbits.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
use crate::bodies::{self, CelestialBody};
|
||||
use crate::constants::*;
|
||||
use crate::kepler::solve_kepler;
|
||||
use nalgebra::Vector3;
|
||||
|
||||
/// Compute the heliocentric ecliptic position of a body at a given Julian Date.
|
||||
///
|
||||
/// Returns position in AU in the ecliptic coordinate frame:
|
||||
/// - X toward vernal equinox
|
||||
/// - Y in ecliptic plane, 90° from X
|
||||
/// - Z toward ecliptic north pole
|
||||
///
|
||||
/// For moons, returns the heliocentric position (parent position + moon offset).
|
||||
pub fn position_at_epoch(bodies: &[CelestialBody], body_id: usize, jd: f64) -> Vector3<f64> {
|
||||
let body = &bodies[body_id];
|
||||
|
||||
// Sun is at origin
|
||||
if body_id == bodies::id::SUN {
|
||||
return Vector3::zeros();
|
||||
}
|
||||
|
||||
// Centuries since J2000.0
|
||||
let t = (jd - J2000_JD) / DAYS_PER_CENTURY;
|
||||
|
||||
// Compute current elements
|
||||
let a = body.elements.a + body.rates.a * t;
|
||||
let e = body.elements.e + body.rates.e * t;
|
||||
let i = (body.elements.i + body.rates.i * t) * DEG_TO_RAD;
|
||||
let l = (body.elements.l + body.rates.l * t) * DEG_TO_RAD;
|
||||
let w_bar = (body.elements.w_bar + body.rates.w_bar * t) * DEG_TO_RAD;
|
||||
let omega = (body.elements.omega + body.rates.omega * t) * DEG_TO_RAD;
|
||||
|
||||
// Argument of perihelion
|
||||
let w = w_bar - omega;
|
||||
|
||||
// Mean anomaly
|
||||
let m = l - w_bar;
|
||||
|
||||
// Solve Kepler's equation for eccentric anomaly
|
||||
let ea = solve_kepler(m, e);
|
||||
|
||||
// True anomaly
|
||||
let nu = 2.0 * ((1.0 + e).sqrt() * (ea / 2.0).sin())
|
||||
.atan2((1.0 - e).sqrt() * (ea / 2.0).cos());
|
||||
|
||||
// Distance from focus
|
||||
let r = a * (1.0 - e * ea.cos());
|
||||
|
||||
// Position in orbital plane
|
||||
let x_orb = r * nu.cos();
|
||||
let y_orb = r * nu.sin();
|
||||
|
||||
// Rotate to ecliptic coordinates
|
||||
let cos_w = w.cos();
|
||||
let sin_w = w.sin();
|
||||
let cos_o = omega.cos();
|
||||
let sin_o = omega.sin();
|
||||
let cos_i = i.cos();
|
||||
let sin_i = i.sin();
|
||||
|
||||
let x_ecl = (cos_w * cos_o - sin_w * sin_o * cos_i) * x_orb
|
||||
+ (-sin_w * cos_o - cos_w * sin_o * cos_i) * y_orb;
|
||||
let y_ecl = (cos_w * sin_o + sin_w * cos_o * cos_i) * x_orb
|
||||
+ (-sin_w * sin_o + cos_w * cos_o * cos_i) * y_orb;
|
||||
let z_ecl = (sin_w * sin_i) * x_orb + (cos_w * sin_i) * y_orb;
|
||||
|
||||
let pos = Vector3::new(x_ecl, y_ecl, z_ecl);
|
||||
|
||||
// For moons, add parent body position
|
||||
if let Some(parent_id) = bodies::parent_body(body_id) {
|
||||
let parent_pos = position_at_epoch(bodies, parent_id, jd);
|
||||
parent_pos + pos
|
||||
} else {
|
||||
pos
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute positions of all bodies at a given epoch.
|
||||
/// Returns a flat Vec of [x, y, z, x, y, z, ...] in AU.
|
||||
pub fn all_positions_at_epoch(bodies: &[CelestialBody], jd: f64) -> Vec<f64> {
|
||||
let mut result = Vec::with_capacity(bodies.len() * 3);
|
||||
for i in 0..bodies.len() {
|
||||
let pos = position_at_epoch(bodies, i, jd);
|
||||
result.push(pos.x);
|
||||
result.push(pos.y);
|
||||
result.push(pos.z);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Sample points around a body's orbit for drawing orbit ellipses.
|
||||
/// For planets, samples one full heliocentric orbit.
|
||||
/// For moons, samples one full orbit around the parent and adds parent position.
|
||||
/// Returns flat [x, y, z, x, y, z, ...] in AU.
|
||||
pub fn orbit_points(
|
||||
bodies: &[CelestialBody],
|
||||
body_id: usize,
|
||||
jd: f64,
|
||||
samples: usize,
|
||||
) -> Vec<f64> {
|
||||
let body = &bodies[body_id];
|
||||
let t = (jd - J2000_JD) / DAYS_PER_CENTURY;
|
||||
|
||||
let a = body.elements.a + body.rates.a * t;
|
||||
let e = body.elements.e + body.rates.e * t;
|
||||
let i = (body.elements.i + body.rates.i * t) * DEG_TO_RAD;
|
||||
let w_bar = (body.elements.w_bar + body.rates.w_bar * t) * DEG_TO_RAD;
|
||||
let omega = (body.elements.omega + body.rates.omega * t) * DEG_TO_RAD;
|
||||
let w = w_bar - omega;
|
||||
|
||||
let cos_w = w.cos();
|
||||
let sin_w = w.sin();
|
||||
let cos_o = omega.cos();
|
||||
let sin_o = omega.sin();
|
||||
let cos_i = i.cos();
|
||||
let sin_i = i.sin();
|
||||
|
||||
// Parent offset for moons
|
||||
let parent_pos = if let Some(parent_id) = bodies::parent_body(body_id) {
|
||||
position_at_epoch(bodies, parent_id, jd)
|
||||
} else {
|
||||
Vector3::zeros()
|
||||
};
|
||||
|
||||
let mut result = Vec::with_capacity(samples * 3);
|
||||
for s in 0..samples {
|
||||
let nu = TAU * (s as f64) / (samples as f64);
|
||||
let r = a * (1.0 - e * e) / (1.0 + e * nu.cos());
|
||||
|
||||
let x_orb = r * nu.cos();
|
||||
let y_orb = r * nu.sin();
|
||||
|
||||
let x = (cos_w * cos_o - sin_w * sin_o * cos_i) * x_orb
|
||||
+ (-sin_w * cos_o - cos_w * sin_o * cos_i) * y_orb
|
||||
+ parent_pos.x;
|
||||
let y = (cos_w * sin_o + sin_w * cos_o * cos_i) * x_orb
|
||||
+ (-sin_w * sin_o + cos_w * cos_o * cos_i) * y_orb
|
||||
+ parent_pos.y;
|
||||
let z = (sin_w * sin_i) * x_orb + (cos_w * sin_i) * y_orb + parent_pos.z;
|
||||
|
||||
result.push(x);
|
||||
result.push(y);
|
||||
result.push(z);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::bodies::all_bodies;
|
||||
|
||||
#[test]
|
||||
fn test_sun_at_origin() {
|
||||
let bodies = all_bodies();
|
||||
let pos = position_at_epoch(&bodies, bodies::id::SUN, J2000_JD);
|
||||
assert!(pos.norm() < 1e-15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_earth_distance_from_sun() {
|
||||
let bodies = all_bodies();
|
||||
let pos = position_at_epoch(&bodies, bodies::id::EARTH, J2000_JD);
|
||||
let distance_au = pos.norm();
|
||||
// Earth should be ~1 AU from the Sun
|
||||
assert!(
|
||||
(distance_au - 1.0).abs() < 0.02,
|
||||
"Earth distance: {} AU",
|
||||
distance_au
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_jupiter_distance() {
|
||||
let bodies = all_bodies();
|
||||
let pos = position_at_epoch(&bodies, bodies::id::JUPITER, J2000_JD);
|
||||
let distance_au = pos.norm();
|
||||
// Jupiter should be ~5.2 AU
|
||||
assert!(
|
||||
(distance_au - 5.2).abs() < 0.3,
|
||||
"Jupiter distance: {} AU",
|
||||
distance_au
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_positions_length() {
|
||||
let bodies = all_bodies();
|
||||
let positions = all_positions_at_epoch(&bodies, J2000_JD);
|
||||
assert_eq!(positions.len(), bodies.len() * 3);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user