earths 0.0.1

High-fidelity Earth simulation engine — orbit, atmosphere, geology, hydrology, biosphere, terrain, lighting, rendering, satellites, and temporal systems with full scientific coupling
Documentation
use sciforge::hub::prelude::constants::G;
use std::cell::{Cell, RefCell};
use std::io::{BufRead, BufReader, Write};
use std::process::{Child, Command, Stdio};
pub const LUNAR_MASS: f64 = 7.342e22;
pub const LUNAR_RADIUS: f64 = 1.7374e6;
pub const EARTH_MOON_DISTANCE: f64 = 3.844e8;
pub const LUNAR_ORBITAL_PERIOD: f64 = 2_360_592.0;
pub struct MoonState {
    pub mass_kg: f64,
    pub radius_m: f64,
    pub semi_major_axis_m: f64,
    pub orbital_period_s: f64,
    pub eccentricity: f64,
    pub inclination_rad: f64,
    pub raan_rad: f64,
    pub arg_perigee_rad: f64,
    pub mean_anomaly_rad: f64,
    pub source: Cell<MoonSource>,
    ipc: RefCell<Option<MoonIpc>>,
}
struct MoonIpc {
    child: Child,
    reader: BufReader<std::process::ChildStdout>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MoonSource {
    Binary,
    Simulation,
}
fn find_moon_binary() -> Option<std::path::PathBuf> {
    if let Ok(path) = std::env::var("MOON_BINARY") {
        let p = std::path::PathBuf::from(path);
        if p.is_file() {
            return Some(p);
        }
    }
    if let Ok(path_var) = std::env::var("PATH") {
        for dir in path_var.split(':') {
            if dir.is_empty() {
                continue;
            }
            let candidate = std::path::Path::new(dir).join("moon");
            if candidate.is_file() {
                return Some(candidate);
            }
        }
    }
    None
}
fn spawn_moon(path: &std::path::Path) -> Option<MoonIpc> {
    let mut child = Command::new(path)
        .arg("--ipc")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::null())
        .spawn()
        .ok()?;
    let stdout = child.stdout.take()?;
    let reader = BufReader::new(stdout);
    Some(MoonIpc { child, reader })
}
impl MoonState {
    pub fn new() -> Self {
        if let Some(path) = find_moon_binary()
            && let Some(ipc) = spawn_moon(&path)
        {
            return Self {
                mass_kg: LUNAR_MASS,
                radius_m: LUNAR_RADIUS,
                semi_major_axis_m: EARTH_MOON_DISTANCE,
                orbital_period_s: LUNAR_ORBITAL_PERIOD,
                eccentricity: crate::LUNAR_ECCENTRICITY,
                inclination_rad: crate::LUNAR_INCLINATION_DEG.to_radians(),
                raan_rad: 125.08_f64.to_radians(),
                arg_perigee_rad: 318.15_f64.to_radians(),
                mean_anomaly_rad: 0.0,
                source: Cell::new(MoonSource::Binary),
                ipc: RefCell::new(Some(ipc)),
            };
        }
        Self::simulated()
    }
    pub fn simulated() -> Self {
        Self {
            mass_kg: LUNAR_MASS,
            radius_m: LUNAR_RADIUS,
            semi_major_axis_m: EARTH_MOON_DISTANCE,
            orbital_period_s: LUNAR_ORBITAL_PERIOD,
            eccentricity: crate::LUNAR_ECCENTRICITY,
            inclination_rad: crate::LUNAR_INCLINATION_DEG.to_radians(),
            raan_rad: 125.08_f64.to_radians(),
            arg_perigee_rad: 318.15_f64.to_radians(),
            mean_anomaly_rad: 0.0,
            source: Cell::new(MoonSource::Simulation),
            ipc: RefCell::new(None),
        }
    }
    fn ipc_query(&self, cmd: &str) -> Option<String> {
        let mut ipc_borrow = self.ipc.borrow_mut();
        let ipc = ipc_borrow.as_mut()?;
        let stdin = ipc.child.stdin.as_mut()?;
        writeln!(stdin, "{}", cmd).ok()?;
        stdin.flush().ok()?;
        let mut line = String::new();
        ipc.reader.read_line(&mut line).ok()?;
        Some(line.trim().to_string())
    }
    fn parse_xyz(s: &str) -> Option<(f64, f64, f64)> {
        let parts: Vec<&str> = s.split_whitespace().collect();
        if parts.len() >= 3 {
            let x = parts[0].parse::<f64>().ok()?;
            let y = parts[1].parse::<f64>().ok()?;
            let z = parts[2].parse::<f64>().ok()?;
            Some((x, y, z))
        } else {
            None
        }
    }
    pub fn step(&mut self, dt_s: f64) {
        if self.source.get() == MoonSource::Binary {
            if let Some(resp) = self.ipc_query(&format!("step {}", dt_s)) {
                if let Some((x, y, z)) = Self::parse_xyz(&resp) {
                    let r = (x * x + y * y + z * z).sqrt();
                    if r > 0.0 {
                        self.semi_major_axis_m = r;
                    }
                }
                return;
            }
            self.source.set(MoonSource::Simulation);
            *self.ipc.borrow_mut() = None;
        }
        let pi2 = 2.0 * std::f64::consts::PI;
        let n = pi2 / self.orbital_period_s;
        self.mean_anomaly_rad = (self.mean_anomaly_rad + n * dt_s) % pi2;
        let raan_rate = -pi2 / (18.61 * crate::SECONDS_PER_YEAR);
        self.raan_rad = (self.raan_rad + raan_rate * dt_s) % pi2;
        let apse_rate = pi2 / (8.85 * crate::SECONDS_PER_YEAR);
        self.arg_perigee_rad = (self.arg_perigee_rad + apse_rate * dt_s) % pi2;
    }
    fn eccentric_anomaly(&self) -> f64 {
        let m = self.mean_anomaly_rad;
        let e = self.eccentricity;
        let mut ea = m + e * m.sin();
        for _ in 0..15 {
            let f = ea - e * ea.sin() - m;
            let fp = 1.0 - e * ea.cos();
            ea -= f / fp;
        }
        ea
    }
    fn true_anomaly(&self) -> f64 {
        let ea = self.eccentric_anomaly();
        let e = self.eccentricity;
        2.0 * f64::atan2(
            (1.0 + e).sqrt() * (ea / 2.0).sin(),
            (1.0 - e).sqrt() * (ea / 2.0).cos(),
        )
    }
    pub fn distance(&self) -> f64 {
        if self.source.get() == MoonSource::Binary {
            let (x, y, z) = self.position();
            return (x * x + y * y + z * z).sqrt();
        }
        let nu = self.true_anomaly();
        let e = self.eccentricity;
        self.semi_major_axis_m * (1.0 - e * e) / (1.0 + e * nu.cos())
    }
    pub fn position(&self) -> (f64, f64, f64) {
        if self.source.get() == MoonSource::Binary
            && let Some(resp) = self.ipc_query("position")
            && let Some((x, y, z)) = Self::parse_xyz(&resp)
        {
            return (x, y, z);
        }
        let nu = self.true_anomaly();
        let r = {
            let e = self.eccentricity;
            self.semi_major_axis_m * (1.0 - e * e) / (1.0 + e * nu.cos())
        };
        let x_orb = r * nu.cos();
        let y_orb = r * nu.sin();
        let w = self.arg_perigee_rad;
        let x_w = x_orb * w.cos() - y_orb * w.sin();
        let y_w = x_orb * w.sin() + y_orb * w.cos();
        let i = self.inclination_rad;
        let x_i = x_w;
        let y_i = y_w * i.cos();
        let z_i = y_w * i.sin();
        let omega = self.raan_rad;
        let x = x_i * omega.cos() - y_i * omega.sin();
        let y = x_i * omega.sin() + y_i * omega.cos();
        let z = z_i;
        (x, y, z)
    }
    pub fn gravity_at(&self, distance_m: f64) -> f64 {
        G * self.mass_kg / (distance_m * distance_m)
    }
    pub fn is_binary(&self) -> bool {
        self.source.get() == MoonSource::Binary
    }
}
impl Default for MoonState {
    fn default() -> Self {
        Self::new()
    }
}
impl Drop for MoonState {
    fn drop(&mut self) {
        if let Some(ref mut ipc) = self.ipc.borrow_mut().as_mut() {
            if let Some(ref mut stdin) = ipc.child.stdin.as_mut() {
                let _ = writeln!(stdin, "quit");
                let _ = stdin.flush();
            }
            let _ = ipc.child.kill();
            let _ = ipc.child.wait();
        }
    }
}