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();
}
}
}