planetsfactory 0.0.3

Planet factory — classify, build and catalogue planets for any star system: Solar System, TRAPPIST-1, Kepler-90, Proxima Centauri, or fully custom worlds.
Documentation
use crate::config::parameters::*;
use crate::engine::orbits::OrbitalElements;
use crate::types::gas_giant::GasGiant;
use crate::types::ice_giant::IceGiant;
use crate::types::lava_world::LavaWorld;
use crate::types::ocean_world::OceanWorld;
use crate::types::rogue::RoguePlanet;
use crate::types::sub_neptune::SubNeptune;
use crate::types::super_earth::SuperEarth;
use crate::types::terrestrial::TerrestrialPlanet;

#[derive(Debug, Clone)]
pub struct SystemGeneratorConfig {
    pub star_name: String,
    pub star_mass: f64,
    pub star_temperature: f64,
    pub star_radius: f64,
    pub n_planets: usize,
    pub inner_limit_au: f64,
    pub outer_limit_au: f64,
    pub frost_line_au: f64,
    pub metallicity: f64,
    pub seed: u64,
}

impl SystemGeneratorConfig {
    pub fn solar_like(n_planets: usize) -> Self {
        Self {
            star_name: String::from("Star"),
            star_mass: SOLAR_MASS,
            star_temperature: SOLAR_TEMPERATURE,
            star_radius: SOLAR_RADIUS,
            n_planets,
            inner_limit_au: 0.3,
            outer_limit_au: 40.0,
            frost_line_au: 2.7,
            metallicity: 0.02,
            seed: 42,
        }
    }

    pub fn m_dwarf(n_planets: usize) -> Self {
        Self {
            star_name: String::from("Star"),
            star_mass: 0.089 * SOLAR_MASS,
            star_temperature: 2566.0,
            star_radius: 0.121 * SOLAR_RADIUS,
            n_planets,
            inner_limit_au: 0.01,
            outer_limit_au: 0.1,
            frost_line_au: 0.03,
            metallicity: 0.02,
            seed: 42,
        }
    }

    pub fn hot_star(n_planets: usize) -> Self {
        Self {
            star_name: String::from("Star"),
            star_mass: 2.0 * SOLAR_MASS,
            star_temperature: 8500.0,
            star_radius: 1.8 * SOLAR_RADIUS,
            n_planets,
            inner_limit_au: 0.5,
            outer_limit_au: 80.0,
            frost_line_au: 5.0,
            metallicity: 0.01,
            seed: 42,
        }
    }
}

pub fn titius_bode_au(n_planets: usize, a0: f64, b: f64, c: f64) -> Vec<f64> {
    (0..n_planets).map(|n| a0 + b * c.powi(n as i32)).collect()
}

pub fn titius_bode_solar(n_planets: usize) -> Vec<f64> {
    titius_bode_au(n_planets, 0.0, 0.3, 2.0)
}

fn simple_hash(seed: u64, idx: u64) -> f64 {
    let mut x = seed.wrapping_mul(6364136223846793005).wrapping_add(idx);
    x ^= x >> 33;
    x = x.wrapping_mul(0xff51afd7ed558ccd);
    x ^= x >> 33;
    (x as f64) / (u64::MAX as f64)
}

pub enum GeneratedPlanet {
    Terrestrial(TerrestrialPlanet),
    SuperEarth(SuperEarth),
    SubNeptune(SubNeptune),
    IceGiant(IceGiant),
    GasGiant(GasGiant),
    LavaWorld(LavaWorld),
    OceanWorld(OceanWorld),
    Rogue(RoguePlanet),
}

pub fn generate_system(config: &SystemGeneratorConfig) -> Vec<GeneratedPlanet> {
    let n = config.n_planets;
    let ln_inner = config.inner_limit_au.ln();
    let ln_outer = config.outer_limit_au.ln();

    let axes_au: Vec<f64> = (0..n)
        .map(|i| {
            let frac = if n > 1 {
                i as f64 / (n - 1) as f64
            } else {
                0.5
            };
            (ln_inner + frac * (ln_outer - ln_inner)).exp()
        })
        .collect();

    let mut planets = Vec::with_capacity(n);

    for (idx, &a_au) in axes_au.iter().enumerate() {
        let name = format!("{} {}", config.star_name, (b'b' + idx as u8) as char);
        let ecc = simple_hash(config.seed, 10 + idx as u64) * 0.1;
        let inc_deg = simple_hash(config.seed, 20 + idx as u64) * 5.0;
        let omega_big_deg = (360.0 / n as f64) * idx as f64;
        let omega_small_deg = simple_hash(config.seed, 200 + idx as u64) * 360.0;
        let m0_deg = simple_hash(config.seed, 300 + idx as u64) * 360.0;

        let elements = OrbitalElements::from_au_deg(
            a_au,
            ecc,
            inc_deg,
            omega_big_deg,
            omega_small_deg,
            m0_deg,
        );

        let u_mass = simple_hash(config.seed, 100 + idx as u64);
        let u_type = simple_hash(config.seed, 400 + idx as u64);
        let t_eq = equilibrium_temperature(config.star_temperature, config.star_radius, a_au * AU);

        let planet = classify_and_build(config, &name, a_au, t_eq, u_mass, u_type, elements);
        planets.push(planet);
    }

    planets
}

fn classify_and_build(
    config: &SystemGeneratorConfig,
    name: &str,
    a_au: f64,
    t_eq: f64,
    u_mass: f64,
    u_type: f64,
    elements: OrbitalElements,
) -> GeneratedPlanet {
    let frost = config.frost_line_au;
    let parent = &config.star_name;

    if t_eq > 1500.0 && a_au < 0.1 {
        let mass = EARTH_MASS * (0.5 + 4.5 * u_mass);
        let radius = radius_from_mass(mass);
        return GeneratedPlanet::LavaWorld(
            LavaWorld::new(name, parent, mass, radius, 1e-3, 0.0, elements)
                .with_surface(0.10, t_eq, 200.0e3),
        );
    }

    if a_au < frost * 0.3 {
        let mass_e = TERRESTRIAL_MAX_MASS + (SUPER_EARTH_MAX_MASS - TERRESTRIAL_MAX_MASS) * u_mass;
        let mass = mass_e * EARTH_MASS;
        let radius = radius_from_mass(mass);
        if u_type < 0.4 {
            return GeneratedPlanet::SuperEarth(
                SuperEarth::new(
                    name,
                    parent,
                    mass,
                    radius,
                    1e-3,
                    inc_from_u(u_type),
                    elements,
                )
                .with_composition(
                    0.3,
                    0.30,
                    0.01 + 0.05 * config.metallicity / 0.02,
                ),
            );
        }
        let tm = EARTH_MASS * (0.1 + 1.9 * u_mass);
        return GeneratedPlanet::Terrestrial(
            TerrestrialPlanet::new(
                name,
                parent,
                tm,
                radius_from_mass(tm),
                1e-3,
                inc_from_u(u_type),
                elements,
            )
            .with_surface(0.2 + 0.3 * u_type, 0.25 + 0.20 * u_mass),
        );
    }

    if a_au < frost {
        let mass_e = 1.0 + (SUPER_EARTH_MAX_MASS - 1.0) * u_mass;
        let mass = mass_e * EARTH_MASS;
        let radius = radius_from_mass(mass);
        if u_type < 0.3 && mass_e > 2.0 {
            let water_frac = 0.05 + 0.15 * u_type / 0.3;
            return GeneratedPlanet::OceanWorld(
                OceanWorld::new(
                    name,
                    parent,
                    mass,
                    radius,
                    1e-3,
                    inc_from_u(u_type),
                    elements,
                )
                .with_hydrosphere(0.35, water_frac, 0.0),
            );
        }
        if mass_e > TERRESTRIAL_MAX_MASS {
            return GeneratedPlanet::SuperEarth(SuperEarth::new(
                name,
                parent,
                mass,
                radius,
                1e-3,
                inc_from_u(u_type),
                elements,
            ));
        }
        return GeneratedPlanet::Terrestrial(TerrestrialPlanet::new(
            name,
            parent,
            mass,
            radius,
            1e-3,
            inc_from_u(u_type),
            elements,
        ));
    }

    if a_au < frost * 2.0 {
        let mass_e = SUPER_EARTH_MAX_MASS + (MINI_NEPTUNE_MAX_MASS - SUPER_EARTH_MAX_MASS) * u_mass;
        let mass = mass_e * EARTH_MASS;
        let radius = radius_from_mass(mass);
        if mass_e < MINI_NEPTUNE_MAX_MASS {
            return GeneratedPlanet::SubNeptune(
                SubNeptune::new(
                    name,
                    parent,
                    mass,
                    radius,
                    2e-3,
                    inc_from_u(u_type),
                    elements,
                )
                .with_envelope(0.30, 0.02 + 0.08 * u_type, 0.50),
            );
        }
    }

    if a_au < frost * 5.0 {
        let mass_j = 0.1 + 5.0 * u_mass;
        let mass = mass_j * JUPITER_MASS;
        let radius = radius_from_mass(mass);
        return GeneratedPlanet::GasGiant(GasGiant::new(
            name,
            parent,
            mass,
            radius,
            1.5e-2,
            3.0 * DEG,
            elements,
        ));
    }

    let mass_e = ICE_GIANT_MAX_MASS * (0.3 + 0.7 * u_mass);
    let mass = mass_e * EARTH_MASS;
    let radius = radius_from_mass(mass);
    GeneratedPlanet::IceGiant(IceGiant::new(
        name,
        parent,
        mass,
        radius,
        3.3e-3,
        30.0 * DEG,
        elements,
    ))
}

fn inc_from_u(u: f64) -> f64 {
    u * 30.0 * DEG
}

pub fn generate_rogue_planets(n: usize, seed: u64) -> Vec<GeneratedPlanet> {
    let mut rogues = Vec::with_capacity(n);
    for i in 0..n {
        let u = simple_hash(seed, 500 + i as u64);
        let mass = EARTH_MASS * (0.01 + 20.0 * u * u);
        let radius = radius_from_mass(mass);
        let age = 1e9 * YEAR * (1.0 + 9.0 * simple_hash(seed, 600 + i as u64));
        let vel = 10.0e3 + 50.0e3 * simple_hash(seed, 700 + i as u64);
        let name = format!("Rogue-{}", i + 1);
        rogues.push(GeneratedPlanet::Rogue(RoguePlanet::with_details(
            &name, mass, radius, age, 0.1, 0.30, vel,
        )));
    }
    rogues
}