blackholesfactory 0.0.4

Black hole factory — create and simulate stellar, intermediate-mass, and supermassive black holes with full Kerr physics
Documentation
use std::f64::consts::FRAC_PI_2;
use std::process;

use blackholesfactory::config::parameters::*;
use blackholesfactory::engine::accretion_disk::AccretionDisk;
use blackholesfactory::engine::evaporation::HawkingEvaporation;
use blackholesfactory::engine::jet_dynamics::RelativisticJet;
use blackholesfactory::engine::spacetime::{KerrSpacetime, SpacetimePoint};
use blackholesfactory::observables::lensing::GravitationalLens;
use blackholesfactory::observables::radiation::AccretionRadiation;
use blackholesfactory::observables::shadows::BlackHoleShadow;
use blackholesfactory::physics::gravitation::GravitationalField;
use blackholesfactory::physics::relativity::{SchwarzschildMetric, gravitational_wave_power};
use blackholesfactory::types::binary::BinaryBlackHole;
use blackholesfactory::types::intermediate_mass::{FormationChannel, IntermediateMassBlackHole};
use blackholesfactory::types::primordial::{PrimordialBlackHole, PrimordialFormation};
use blackholesfactory::types::stellar_mass::StellarMassBlackHole;
use blackholesfactory::types::supermassive::SupermassiveBlackHole;

struct FactoryValidation {
    stellar_mass_solar: f64,
    stellar_strain: f64,
    intermediate_mass_solar: f64,
    imbh_qpo: f64,
    imbh_recoil: f64,
    supermassive_mass_solar: f64,
    m_sigma_dev: f64,
    jet_power_est: f64,
    can_disrupt: bool,
    shadow_deviation: f64,
    predicted_uas: f64,
    contour_area: f64,
    precession: f64,
    disk_lum: f64,
    disk_eff: f64,
    peak_temp: f64,
    visc: f64,
    edd_ratio: f64,
    inner_temp: f64,
    bz_power: f64,
    kinetic_power: f64,
    max_apparent: f64,
    event_horizon: f64,
    final_photon_r: f64,
    tidal: f64,
    bondi: f64,
    einstein: f64,
    shapiro: f64,
    gw_power: f64,
    evap_time: f64,
    page_time: f64,
    peak_wavelength: f64,
    mass_remaining: f64,
    inspiral: f64,
    friction: f64,
    combined_power: f64,
    ratio_to_edd: f64,
    photon_captured: bool,
    primordial_temp: f64,
    primordial_survives: bool,
    binary_chirp: f64,
    binary_gw_lum: f64,
    binary_final_mass_ok: bool,
}

impl FactoryValidation {
    fn passed(&self) -> bool {
        let stellar_ok = self.stellar_mass_solar >= STELLAR_MASS_MIN_SOLAR
            && self.stellar_mass_solar <= STELLAR_MASS_MAX_SOLAR
            && self.stellar_strain > 0.0;

        let imbh_ok = self.intermediate_mass_solar >= INTERMEDIATE_MASS_MIN_SOLAR
            && self.intermediate_mass_solar <= INTERMEDIATE_MASS_MAX_SOLAR
            && self.imbh_qpo > 0.0
            && self.imbh_recoil > 0.0;

        let smbh_ok = self.supermassive_mass_solar >= SUPERMASSIVE_MASS_MIN_SOLAR
            && self.supermassive_mass_solar <= SUPERMASSIVE_MASS_MAX_SOLAR
            && self.m_sigma_dev.abs() < 1.0
            && self.jet_power_est > 0.0
            && self.can_disrupt;

        let shadow_ok =
            self.shadow_deviation < 0.3 && self.predicted_uas > 0.0 && self.contour_area > 0.0;

        let disk_ok = self.disk_lum > 0.0
            && self.disk_eff > 0.0
            && self.disk_eff < 0.5
            && self.peak_temp > 0.0
            && self.visc > 0.0
            && self.edd_ratio > 0.0
            && self.inner_temp > 0.0;

        let jet_ok = self.bz_power > 0.0 && self.kinetic_power > 0.0 && self.max_apparent > C;

        let spacetime_ok = self.event_horizon > 0.0 && self.final_photon_r > 0.0;

        let gravity_ok = self.precession > 0.0 && self.tidal > 0.0 && self.bondi > 0.0;

        let lens_ok = self.einstein > 0.0 && self.shapiro > 0.0;

        let evap_ok = self.evap_time > 0.0
            && self.evap_time < f64::INFINITY
            && self.page_time > 0.0
            && self.peak_wavelength > 0.0
            && self.mass_remaining > 0.0
            && self.mass_remaining < 1.0;

        let multi_ok = self.inspiral > 0.0 && self.friction > 0.0;

        let combined_ok = self.combined_power > 0.0
            && self.ratio_to_edd > 0.0
            && self.gw_power > 0.0
            && self.photon_captured;

        let primordial_ok = self.primordial_temp > 0.0 && self.primordial_survives;

        let binary_ok =
            self.binary_chirp > 0.0 && self.binary_gw_lum > 0.0 && self.binary_final_mass_ok;

        stellar_ok
            && imbh_ok
            && smbh_ok
            && shadow_ok
            && disk_ok
            && jet_ok
            && spacetime_ok
            && gravity_ok
            && lens_ok
            && evap_ok
            && multi_ok
            && combined_ok
            && primordial_ok
            && binary_ok
    }
}

fn main() {
    let stellar = StellarMassBlackHole::from_mass(21.2, 0.998);
    let stellar_mass_solar = stellar.mass_solar();
    let stellar_dist = 6.85e3 * LIGHT_YEAR;
    let stellar_strain = stellar.gravitational_wave_strain(10.0 * SOLAR_MASS, stellar_dist, 1e10);

    let imbh = IntermediateMassBlackHole::new(2e4, 0.5, FormationChannel::RunawayMerger);
    let imbh_qpo = imbh.ulf_qpo_frequency();
    let imbh_recoil = imbh.merger_recoil_velocity(0.5);

    let smbh = SupermassiveBlackHole::new(6.5e9, 0.9, 375e3);
    let smbh_mass = smbh.singularity.mass;
    let smbh_spin = 0.9;
    let smbh_dist = 16.8e6 * PARSEC;
    let m_sigma_dev = smbh.m_sigma_deviation();
    let jet_power_est = smbh.jet_power_estimate();
    let can_disrupt = smbh.can_disrupt_star(SOLAR_MASS, 6.957e8);

    let shadow = BlackHoleShadow::new(smbh_mass, smbh_spin, 0.3);
    let predicted_uas = shadow.shadow_diameter_uas(smbh_dist);
    let shadow_deviation = (predicted_uas - 42.0).abs() / 42.0;
    let contour = shadow.shadow_contour(64);
    let contour_area: f64 = contour
        .windows(2)
        .map(|w| w[0].0 * w[1].1 - w[1].0 * w[0].1)
        .sum::<f64>()
        .abs()
        * 0.5;

    let schw = SchwarzschildMetric::new(21.2 * SOLAR_MASS);
    let precession = schw.perihelion_precession(0.2 * PARSEC, 0.0);

    let mdot_smbh = 0.1 * SOLAR_MASS / (365.25 * 86400.0);
    let disk = AccretionDisk::with_spin(smbh_mass, mdot_smbh, smbh_spin);
    let disk_lum = disk.luminosity();
    let disk_eff = disk.radiative_efficiency();
    let peak_temp = disk.peak_temperature();
    let visc = disk.viscous_timescale(disk.inner_radius * 10.0);

    let accr = AccretionRadiation::new(smbh_mass, mdot_smbh);
    let edd_ratio = accr.eddington_ratio();
    let inner_temp = accr.disk_inner_temperature(smbh_spin);

    let jet = RelativisticJet::new(smbh_mass, smbh_spin)
        .with_lorentz_factor(6.0)
        .with_magnetic_field(50.0)
        .with_opening_angle(0.05);
    let bz = jet.blandford_znajek_power();
    let kin = jet.kinetic_luminosity();
    let max_app = jet.max_apparent_velocity();

    let ks = KerrSpacetime::new(smbh_mass, smbh_spin);
    let rh = ks.event_horizon();
    let traj = ks.trace_photon(
        SpacetimePoint::new(0.0, 50.0 * rh, FRAC_PI_2, 0.0),
        [C, 0.0, C / (50.0 * rh)],
        500,
        0.01,
    );
    let final_r = traj.last().map(|p| p.r).unwrap_or(0.0);

    let gf = GravitationalField::new(smbh_mass);
    let tidal = gf.tidal_force(rh, 1.0);
    let bondi = gf.bondi_radius(1e6);

    let lens = GravitationalLens::new(smbh_mass, smbh_dist, 2.0 * smbh_dist);
    let einstein = lens.einstein_radius();
    let shapiro = lens.shapiro_delay(1e15);

    let gw = gravitational_wave_power(stellar.singularity.mass, 10.0 * SOLAR_MASS, 1e10);

    let mut evap = HawkingEvaporation::new(1e12);
    let evap_time = evap.evaporation_time();
    let page = evap.page_time();
    let peak_wl = evap.peak_emission_wavelength();
    evap.evolve(1e10);
    let remaining = evap.mass_fraction_remaining();

    let inspiral = smbh.inspiral_timescale(imbh.singularity.mass, 0.1 * PARSEC);
    let friction =
        smbh.dynamical_friction_timescale(imbh.singularity.mass, PARSEC, smbh.host_sigma);

    let combined_power = disk_lum + bz + kin;

    let pbh = PrimordialBlackHole::new(1e12, PrimordialFormation::DensityFluctuation);
    let primordial_temp = pbh.hawking_temperature();
    let primordial_survives = !pbh.is_evaporated(UNIVERSE_AGE);

    let bbh = BinaryBlackHole::new(30.0 * SOLAR_MASS, 35.0 * SOLAR_MASS, 1e9).with_spins(0.7, 0.3);
    let binary_chirp = bbh.chirp_mass();
    let binary_gw_lum = bbh.gravitational_wave_luminosity();
    let binary_final_mass_ok = bbh.final_mass_estimate() < bbh.total_mass();

    let v = FactoryValidation {
        stellar_mass_solar,
        stellar_strain,
        intermediate_mass_solar: imbh.mass_solar(),
        imbh_qpo,
        imbh_recoil,
        supermassive_mass_solar: smbh.mass_solar(),
        m_sigma_dev,
        jet_power_est,
        can_disrupt,
        shadow_deviation,
        predicted_uas,
        contour_area,
        precession,
        disk_lum,
        disk_eff,
        peak_temp,
        visc,
        edd_ratio,
        inner_temp,
        bz_power: bz,
        kinetic_power: kin,
        max_apparent: max_app,
        event_horizon: rh,
        final_photon_r: final_r,
        tidal,
        bondi,
        einstein,
        shapiro,
        gw_power: gw,
        evap_time,
        page_time: page,
        peak_wavelength: peak_wl,
        mass_remaining: remaining,
        inspiral,
        friction,
        combined_power,
        ratio_to_edd: combined_power / accr.eddington_luminosity(),
        photon_captured: final_r < rh * 1.1,
        primordial_temp,
        primordial_survives,
        binary_chirp,
        binary_gw_lum,
        binary_final_mass_ok,
    };

    process::exit(if v.passed() { 0 } else { 1 });
}