Last active
March 23, 2017 19:29
-
-
Save sgrif/14bb6f4a1f32b9cf09a8c40a004e86f7 to your computer and use it in GitHub Desktop.
moar rockets
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#![allow(dead_code)] | |
extern crate ansi_term; | |
mod engines; | |
mod fuels; | |
use std::fmt; | |
use std::marker::PhantomData; | |
use ansi_term::Colour::{Red, Yellow, Blue}; | |
use self::engines::*; | |
#[allow(unused_variables, unused_mut)] | |
fn main() { | |
// Probes | |
let probe_200kg = SimpleStage { dry_mass: 138.0, engines: vec![THRUSTER.with_burn_time(108.0)] }; | |
let geo_probe_200kg = SimpleStage { dry_mass: 156.0, engines: vec![THRUSTER.with_burn_time(78.0)] }; | |
let interplanetary_probe = SimpleStage { dry_mass: 195.0, engines: vec![THRUSTER.with_burn_time(185.0)] }; | |
let geo_probe_2 = SimpleStage { dry_mass: 237.0, engines: vec![THRUSTER_2.with_burn_time(95.5)] }; | |
// Upper stages | |
let ablestar = SimpleStage { dry_mass: 667.0, engines: vec![AJ10_104D] }; | |
let agena_a = SimpleStage { dry_mass: 640.0, engines: vec![BELL_8048] }; | |
let agena_b = SimpleStage { dry_mass: 675.0, engines: vec![BELL_8081] }; | |
let agena_d = SimpleStage { dry_mass: 730.0, engines: vec![BELL_8247] }; | |
let baby_sergeant_1 = SimpleStage { dry_mass: BABY_SERGEANT.mass, engines: vec![BABY_SERGEANT] }; | |
let baby_sergeant_3 = SimpleStage { dry_mass: BABY_SERGEANT.mass * 3.0, engines: vec![BABY_SERGEANT; 3] }; | |
let baby_sergeant_11 = SimpleStage { dry_mass: BABY_SERGEANT.mass * 11.0, engines: vec![BABY_SERGEANT; 11] }; | |
// Lower stages | |
let atlas_vernier = LR101_NA_3.with_burn_time(LR105_NA_3.burn_time); | |
let atlas_a = BoostedStage { | |
core: SimpleStage { dry_mass: 5400.0, engines: vec![LR105_NA_3, atlas_vernier, atlas_vernier] }, | |
booster: SimpleStage { dry_mass: LR89_NA_3.mass, engines: vec![LR89_NA_3] }, | |
booster_count: 2, | |
}; | |
let atlas_b = BoostedStage { | |
core: SimpleStage { dry_mass: 3020.0, engines: vec![LR105_NA_5, atlas_vernier, atlas_vernier] }, | |
booster: SimpleStage { dry_mass: LR89_NA_5.mass, engines: vec![LR89_NA_5] }, | |
booster_count: 2, | |
}; | |
let atlas_vernier_2 = LR101_NA_11.with_burn_time(LR105_NA_5.burn_time); | |
let atlas_c = BoostedStage { | |
core: SimpleStage { dry_mass: 3920.0, engines: vec![LR105_NA_5, atlas_vernier_2, atlas_vernier_2] }, | |
booster: SimpleStage { dry_mass: LR89_NA_7_1.mass, engines: vec![LR89_NA_7_1] }, | |
booster_count: 2, | |
}; | |
let saturn_i = SimpleStage { dry_mass: 21150.0, engines: vec![H1; 8] }; | |
let mut rocket = Rocket { | |
stages: vec![Box::new(atlas_c), Box::new(agena_d), Box::new(geo_probe_2)], | |
payload_mass: 0.0, | |
}; | |
// print_max_payloads(&mut rocket); | |
// rocket.set_payload_for_target_deltav(DV_TO_ORBIT); | |
println!("{:5} {:>10} {:>10} {:>10} {:>10}", "stage", "delta-v", "wet mass", "dry mass", "burn time"); | |
let reversed_stages = rocket.stages().enumerate().collect::<Vec<_>>().into_iter().rev(); | |
for (i, stage) in reversed_stages { | |
println!("{:5}: {:10.0} {:10.0} {:10.0} {:>10}", i, stage.delta_v(), stage.wet_mass(), stage.dry_mass(), BurnTime(stage.burn_time())); | |
} | |
println!("Total: {:10.0}", rocket.delta_v()); | |
println!("Max G Force Endured: {}", rocket.max_g_force()); | |
print_where_rocket_can_go(&rocket); | |
} | |
const GRAVITY: f64 = 9.82; | |
const DV_TO_ORBIT: f64 = 9200.0; | |
const DV_TO_GTO: f64 = DV_TO_ORBIT + 2440.0; | |
const DV_TO_GEO: f64 = DV_TO_GTO + 1850.0; | |
const DV_TO_TLI: f64 = DV_TO_GTO + 680.0; | |
const DV_TO_LLO: f64 = DV_TO_TLI + 140.0 + 680.0; | |
const DV_TO_VENUS: f64 = DV_TO_TLI + 370.0; | |
const DV_TO_MARS: f64 = DV_TO_TLI + 480.0; | |
const DV_TO_MERCURY: f64 = DV_TO_VENUS + 2060.0; | |
trait Stage { | |
fn engines(&self) -> Vec<Engine>; | |
fn dry_mass(&self) -> f64; | |
fn wet_mass(&self) -> f64; | |
fn burn_time(&self) -> f64 { | |
self.engines().iter().map(|e| e.burn_time) | |
.max_by_key(|x| *x as u64).unwrap_or(0.0) | |
} | |
fn isp(&self) -> f64 { | |
let engines = self.engines(); | |
engines.iter().map(|e| e.thrust).sum::<f64>() / | |
engines.iter().map(|e| e.thrust / e.isp).sum::<f64>() | |
} | |
/// Simple stages don't need to implement this method. It is used to | |
/// calculate delta-v when there are boosters involved. To combine multiple | |
/// simple stages, use `Rocket` instead. | |
fn next_stage(&self) -> Option<Box<Stage>> { | |
None | |
} | |
fn delta_v(&self) -> f64 { | |
self.isp() * (self.wet_mass() / self.dry_mass()).ln() * GRAVITY | |
} | |
fn max_g_force(&self) -> f64 { | |
self.engines().iter().map(|e| e.thrust * 1000.0).sum::<f64>() / | |
self.dry_mass() / GRAVITY | |
} | |
} | |
impl<T: ?Sized + Stage> Stage for Box<T> { | |
fn engines(&self) -> Vec<Engine> { (&**self).engines() } | |
fn dry_mass(&self) -> f64 { (&**self).dry_mass() } | |
fn wet_mass(&self) -> f64 { (&**self).wet_mass() } | |
fn burn_time(&self) -> f64 { (&**self).burn_time() } | |
fn next_stage(&self) -> Option<Box<Stage>> { (&**self).next_stage() } | |
fn delta_v(&self) -> f64 { (&**self).delta_v() } | |
} | |
impl<'a, T: ?Sized + Stage> Stage for &'a T { | |
fn engines(&self) -> Vec<Engine> { (&**self).engines() } | |
fn dry_mass(&self) -> f64 { (&**self).dry_mass() } | |
fn wet_mass(&self) -> f64 { (&**self).wet_mass() } | |
fn burn_time(&self) -> f64 { (&**self).burn_time() } | |
fn next_stage(&self) -> Option<Box<Stage>> { (&**self).next_stage() } | |
fn delta_v(&self) -> f64 { (&**self).delta_v() } | |
} | |
#[derive(Debug, Clone)] | |
struct SimpleStage { | |
dry_mass: f64, | |
engines: Vec<Engine>, | |
} | |
impl SimpleStage { | |
fn with_remaining_burn_time(&self, burn_time: f64) -> Self { | |
let mut new_stage = self.clone(); | |
for engine in &mut new_stage.engines { | |
engine.burn_time = burn_time; | |
} | |
new_stage | |
} | |
} | |
impl Stage for SimpleStage { | |
fn engines(&self) -> Vec<Engine> { self.engines.clone() } | |
fn dry_mass(&self) -> f64 { self.dry_mass } | |
fn wet_mass(&self) -> f64 { | |
self.dry_mass + self.engines.iter().map(|e| e.propellant_mass_for_full_burn()).sum::<f64>() | |
} | |
} | |
#[derive(Debug, Clone)] | |
struct BoostedStage { | |
core: SimpleStage, | |
booster: SimpleStage, | |
booster_count: usize, | |
} | |
impl BoostedStage { | |
fn stage_after_booster_separation(&self) -> SimpleStage { | |
self.core.with_remaining_burn_time(self.core.burn_time() - self.booster.burn_time()) | |
} | |
} | |
impl Stage for BoostedStage { | |
fn engines(&self) -> Vec<Engine> { | |
let mut engines = self.core.engines(); | |
let booster_engines = self.booster.engines(); | |
for _ in 0..self.booster_count { | |
engines.extend_from_slice(&booster_engines); | |
} | |
engines | |
} | |
fn burn_time(&self) -> f64 { | |
self.booster.burn_time() | |
} | |
fn dry_mass(&self) -> f64 { | |
self.stage_after_booster_separation().wet_mass() + self.booster.dry_mass() * self.booster_count as f64 | |
} | |
fn wet_mass(&self) -> f64 { | |
self.core.wet_mass() + self.booster.wet_mass() * self.booster_count as f64 | |
} | |
fn next_stage(&self) -> Option<Box<Stage>> { | |
Some(Box::new(self.stage_after_booster_separation())) | |
} | |
} | |
struct StageWithPayload<T> { | |
stage: T, | |
payload_mass: f64, | |
} | |
impl<T: Stage> Stage for StageWithPayload<T> { | |
fn engines(&self) -> Vec<Engine> { | |
self.stage.engines() | |
} | |
fn dry_mass(&self) -> f64 { | |
self.stage.dry_mass() + self.payload_mass | |
} | |
fn wet_mass(&self) -> f64 { | |
self.stage.wet_mass() + self.payload_mass | |
} | |
fn next_stage(&self) -> Option<Box<Stage>> { | |
self.stage.next_stage().map(|s| Box::new(StageWithPayload { | |
stage: s, | |
payload_mass: self.payload_mass, | |
}) as Box<Stage>) | |
} | |
fn burn_time(&self) -> f64 { | |
self.stage.burn_time() | |
} | |
} | |
struct Rocket { | |
stages: Vec<Box<Stage>>, | |
payload_mass: f64, | |
} | |
impl Rocket { | |
fn stages<'a>(&'a self) -> Box<Iterator<Item=Box<Stage + 'a>> + 'a> { | |
let mut iterator = RocketStages { | |
current: None, | |
remaining: self.stages.iter(), | |
payload_mass: self.payload_mass, | |
_marker: PhantomData, | |
}; | |
iterator.next(); | |
Box::new(iterator) | |
} | |
fn delta_v(&self) -> f64 { | |
self.stages().map(|s| s.delta_v()).sum() | |
} | |
fn set_payload_for_target_deltav(&mut self, target_delta_v: f64) { | |
self.payload_mass = 0.0; | |
let mut last_mass = 0.0; | |
while self.delta_v() > target_delta_v { | |
last_mass = self.payload_mass; | |
if self.payload_mass < 50.0 { | |
self.payload_mass += 10.0; | |
} else if self.payload_mass < 500.0 { | |
self.payload_mass += 50.0; | |
} else { | |
self.payload_mass += 100.0; | |
} | |
} | |
self.payload_mass = last_mass; | |
} | |
fn max_g_force(&self) -> f64 { | |
let mut g_forces = self.stages().map(|s| s.max_g_force()).collect::<Vec<_>>(); | |
g_forces.sort_by(|a, b| b.partial_cmp(a).expect("We should never get NaN here")); | |
*g_forces.first().unwrap_or(&0.0) | |
} | |
} | |
struct RocketStages<'a, T> { | |
current: Option<Box<Stage + 'a>>, | |
remaining: T, | |
payload_mass: f64, | |
_marker: PhantomData<&'a ()> | |
} | |
impl<'a, T, U> Iterator for RocketStages<'a, U> where | |
T: Stage + 'a, | |
U: Iterator<Item=T> + Clone, | |
{ | |
type Item = Box<Stage + 'a>; | |
fn next(&mut self) -> Option<Self::Item> { | |
let result = self.current.take(); | |
self.current = result.as_ref().and_then(|s| s.next_stage()) | |
.or_else(|| { | |
self.remaining.next().map(|s| { | |
let upper_stage_weight = self.remaining.clone().map(|s| s.wet_mass()).sum::<f64>(); | |
Box::new(StageWithPayload { | |
stage: s, | |
payload_mass: upper_stage_weight + self.payload_mass, | |
}) as Box<Stage> | |
}) | |
}); | |
result | |
} | |
} | |
struct BurnTime(f64); | |
impl fmt::Display for BurnTime { | |
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { | |
let minutes = self.0 as u64 / 60; | |
let seconds = self.0 as u64 % 60; | |
if seconds == 0 { | |
format!("{}m", minutes).fmt(fmt) | |
} else if minutes == 0 { | |
format!("{}s", seconds).fmt(fmt) | |
} else { | |
format!("{}m {}s", minutes, seconds).fmt(fmt) | |
} | |
} | |
} | |
fn print_where_rocket_can_go(rocket: &Rocket) { | |
let dv = rocket.delta_v(); | |
if dv <= DV_TO_ORBIT { | |
println!("{}", Red.bold().paint("This rocket will not reach orbit")); | |
} | |
print_if_rocket_can_go_to(dv, DV_TO_GTO, "GTO"); | |
print_if_rocket_can_go_to(dv, DV_TO_GEO, "GEO"); | |
print_if_rocket_can_go_to(dv, DV_TO_TLI, "The Moon"); | |
print_if_rocket_can_go_to(dv, DV_TO_LLO, "Lunar Orbit"); | |
print_if_rocket_can_go_to(dv, DV_TO_VENUS, "Venus"); | |
print_if_rocket_can_go_to(dv, DV_TO_MARS, "Mars"); | |
print_if_rocket_can_go_to(dv, DV_TO_MERCURY, "Mercury"); | |
if dv > DV_TO_GTO { | |
println!("Note: Assumes no gravity assists"); | |
} | |
} | |
fn print_if_rocket_can_go_to(dv: f64, required_dv: f64, name: &str) { | |
if dv > required_dv * 1.05 { | |
let excess = dv - required_dv; | |
println!("{}", Blue.paint(format!("This rocket can go to {} with {:.0} m/s excess dV", name, excess))); | |
} else if dv > required_dv { | |
println!("{}", Yellow.paint(format!("This rocket can go to {} without safety margins", name))); | |
} | |
} | |
fn print_max_payloads(rocket: &mut Rocket) { | |
print_max_payoad(rocket, DV_TO_ORBIT, "orbit"); | |
print_max_payoad(rocket, DV_TO_GTO * 1.05, "GTO"); | |
print_max_payoad(rocket, DV_TO_GEO * 1.05, "GEO"); | |
print_max_payoad(rocket, DV_TO_TLI * 1.05, "TLI"); | |
print_max_payoad(rocket, DV_TO_LLO * 1.05, "Lunar Orbit"); | |
print_max_payoad(rocket, DV_TO_VENUS * 1.05, "Venus"); | |
print_max_payoad(rocket, DV_TO_MERCURY * 1.05, "Mercury"); | |
} | |
fn print_max_payoad(rocket: &mut Rocket, required_dv: f64, name: &str) { | |
rocket.set_payload_for_target_deltav(required_dv); | |
if rocket.payload_mass > 0.0 { | |
println!("Max to {}: {}", name, rocket.payload_mass); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
pub use super::fuels::*; | |
#[derive(Debug, Clone, Copy)] | |
pub struct Engine { | |
pub name: &'static str, | |
pub fuel_consumption: &'static [(Fuel, f64)], | |
pub isp: f64, | |
pub thrust: f64, | |
pub mass: f64, | |
pub burn_time: f64, | |
} | |
impl Engine { | |
pub fn propellant_mass_per_second(&self) -> f64 { | |
self.fuel_consumption.iter() | |
.map(|&(fuel, rate)| fuel.density * rate) | |
.sum() | |
} | |
pub fn propellants_required(&self) -> Vec<(Fuel, f64)> { | |
self.fuel_consumption.iter() | |
.map(|&(fuel, rate)| (fuel, rate * self.burn_time)) | |
.collect() | |
} | |
pub fn propellant_mass_for_full_burn(&self) -> f64 { | |
self.propellant_mass_per_second() * self.burn_time | |
} | |
pub fn with_burn_time(&self, burn_time: f64) -> Self { | |
let mut result = self.clone(); | |
result.burn_time = burn_time; | |
result | |
} | |
} | |
pub const BELL_8048: Engine = Engine { | |
name: "Bell 8048 (XLR81-BA-5, Agena A)", | |
fuel_consumption: &[(UDMH, 8.8115), (IRFNA_III, 10.7262)], | |
isp: 276.0, | |
thrust: 67.0, | |
mass: 132.0, | |
burn_time: 120.0, | |
}; | |
pub const BELL_8081: Engine = Engine { | |
name: "Bell 8081 (XLR81-BA-7, Agena B)", | |
fuel_consumption: &[(UDMH, 8.9903), (IRFNA_III, 11.0327)], | |
isp: 285.0, | |
thrust: 71.0, | |
mass: 132.0, | |
burn_time: 240.0, | |
}; | |
pub const BELL_8247: Engine = Engine { | |
name: "Bell 8247 (XLR81-BA-13, Gemini ATV)", | |
fuel_consumption: &[(UDMH, 8.8049), (IRFNA_III, 10.8052)], | |
isp: 291.0, | |
thrust: 71.0, | |
mass: 132.0, | |
burn_time: 240.0, | |
}; | |
pub const LR105_NA_3: Engine = Engine { | |
name: "LR105-NA-3", | |
fuel_consumption: &[(LIQUID_OXYGEN, 70.5326), (KEROSENE, 43.5978)], | |
isp: 309.0, | |
thrust: 352.2, | |
mass: 460.0, | |
burn_time: 330.0, | |
}; | |
pub const LR105_NA_5: Engine = Engine { | |
name: "LR105-NA-5/6", | |
fuel_consumption: &[(LIQUID_OXYGEN, 72.8447), (KEROSENE, 45.0270)], | |
isp: 311.0, | |
thrust: 366.1, | |
mass: 413.0, | |
burn_time: 350.0, | |
}; | |
pub const LR101_NA_3: Engine = Engine { | |
name: "LR101-NA-3 Vernier", | |
fuel_consumption: &[(LIQUID_OXYGEN, 1.3296), (KEROSENE, 0.8222)], | |
isp: 238.0, | |
thrust: 4.448, | |
mass: 24.0, | |
burn_time: 360.0, | |
}; | |
pub const LR101_NA_11: Engine = Engine { | |
name: "LR101-NA-11 Vernier", | |
fuel_consumption: &[(LIQUID_OXYGEN, 1.3153), (KEROSENE, 0.8512)], | |
isp: 249.0, | |
thrust: 4.524, | |
mass: 24.0, | |
burn_time: 360.0, | |
}; | |
pub const LR89_NA_3: Engine = Engine { | |
name: "LR89-NA-3", | |
fuel_consumption: &[(LIQUID_OXYGEN, 166.4868), (KEROSENE, 102.9093)], | |
isp: 282.0, | |
thrust: 758.7, | |
mass: 641.0, | |
burn_time: 135.0, | |
}; | |
pub const LR89_NA_5: Engine = Engine { | |
name: "LR89-NA-5", | |
fuel_consumption: &[(LIQUID_OXYGEN, 166.4868), (KEROSENE, 102.9093)], | |
isp: 282.0, | |
thrust: 758.7, | |
mass: 720.0, | |
burn_time: 150.0, | |
}; | |
pub const LR89_NA_6: Engine = Engine { | |
name: "LR89-NA-6", | |
fuel_consumption: &[(LIQUID_OXYGEN, 177.4070), (KEROSENE, 109.5694)], | |
isp: 290.0, | |
thrust: 733.926, | |
mass: 782.0, | |
burn_time: 160.0, | |
}; | |
pub const LR89_NA_7_1: Engine = Engine { | |
name: "LR89-NA-7.1", | |
fuel_consumption: &[(LIQUID_OXYGEN, 197.3125), (KEROSENE, 121.9634)], | |
isp: 292.2, | |
thrust: 922.651, | |
mass: 1019.0, | |
burn_time: 165.0, | |
}; | |
pub const LR79_NA_9: Engine = Engine { | |
name: "LR79-NA-9", | |
fuel_consumption: &[(LIQUID_OXYGEN, 166.2447), (KEROSENE, 107.5894)], | |
isp: 284.0, | |
thrust: 774.0, | |
mass: 934.0, | |
burn_time: 165.0, | |
}; | |
pub const LR79_NA_11: Engine = Engine { | |
name: "LR79-NA-11", | |
fuel_consumption: &[(LIQUID_OXYGEN, 181.1651), (KEROSENE, 117.2455)], | |
isp: 286.2, | |
thrust: 850.0, | |
mass: 980.0, | |
burn_time: 165.0, | |
}; | |
pub const AJ10_104D: Engine = Engine { | |
name: "AJ10-104D", | |
fuel_consumption: &[(UDMH, 4.2831), (IRFNA_III, 5.7219)], | |
isp: 278.0, | |
thrust: 35.1, | |
mass: 90.0, | |
burn_time: 300.0, | |
}; | |
pub const BABY_SERGEANT: Engine = Engine { | |
name: "Baby Sergeant", | |
fuel_consumption: &[(PSPC, 1.9950)], | |
isp: 235.0, | |
thrust: 8.0, | |
mass: 5.670, | |
burn_time: 6.345, | |
}; | |
pub const THRUSTER: Engine = Engine { | |
name: "1kN Thruster", | |
fuel_consumption: &[(HYDRAZINE, 0.5643)], | |
isp: 198.0, | |
thrust: 1.1, | |
mass: 15.0, | |
burn_time: 20.0 * 60.0, | |
}; | |
pub const THRUSTER_2: Engine = Engine { | |
name: "1kN Thruster", | |
fuel_consumption: &[(AEROZINE50, 0.2817), (NTO, 0.2795)], | |
isp: 281.725, | |
thrust: 1.82, | |
mass: 15.0, | |
burn_time: 20.0 * 60.0, | |
}; | |
pub const H1: Engine = Engine { | |
name: "H-1 Saturn I", | |
fuel_consumption: &[(LIQUID_OXYGEN, 202.1917), (KEROSENE, 126.1482)], | |
isp: 289.0, | |
thrust: 947.0, | |
mass: 635.0, | |
burn_time: 150.0, | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#[derive(Debug, Clone, Copy, PartialEq)] | |
pub struct Fuel { | |
pub name: &'static str, | |
pub density: f64, | |
} | |
pub const KEROSENE: Fuel = Fuel { name: "Kerosene", density: 0.82 }; | |
pub const LIQUID_OXYGEN: Fuel = Fuel { name: "LqdOxygen", density: 1.141 }; | |
pub const UDMH: Fuel = Fuel { name: "UDMH", density: 0.791 }; | |
pub const IRFNA_III: Fuel = Fuel { name: "IRFNA-III", density: 1.658 }; | |
pub const PSPC: Fuel = Fuel { name: "PSPC", density: 1.74 }; | |
pub const HYDRAZINE: Fuel = Fuel { name: "Hydrazine", density: 1.004 }; | |
pub const AEROZINE50: Fuel = Fuel { name: "Aerozine50", density: 0.9 }; | |
pub const NTO: Fuel = Fuel { name: "NTO", density: 1.45 }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment