blackholesfactory 0.0.1

Black hole factory — create and simulate stellar, intermediate-mass, and supermassive black holes with full Kerr physics
Documentation
use crate::config::parameters::{C, G, gravitational_radius};

pub struct KerrSpacetime {
    pub mass: f64,
    pub spin: f64,
    pub rg: f64,
    pub a: f64,
}

#[derive(Clone, Copy)]
pub struct SpacetimePoint {
    pub t: f64,
    pub r: f64,
    pub theta: f64,
    pub phi: f64,
}

impl SpacetimePoint {
    pub fn new(t: f64, r: f64, theta: f64, phi: f64) -> Self {
        Self { t, r, theta, phi }
    }
}

impl KerrSpacetime {
    pub fn new(mass: f64, spin: f64) -> Self {
        let rg = gravitational_radius(mass);
        Self {
            mass,
            spin,
            rg,
            a: spin * rg,
        }
    }

    pub fn event_horizon(&self) -> f64 {
        self.rg + (self.rg * self.rg - self.a * self.a).max(0.0).sqrt()
    }

    pub fn cauchy_horizon(&self) -> f64 {
        self.rg - (self.rg * self.rg - self.a * self.a).max(0.0).sqrt()
    }

    pub fn ergosphere(&self, theta: f64) -> f64 {
        self.rg
            + (self.rg * self.rg - self.a * self.a * theta.cos().powi(2))
                .max(0.0)
                .sqrt()
    }

    fn sigma(&self, r: f64, theta: f64) -> f64 {
        r * r + self.a * self.a * theta.cos().powi(2)
    }

    fn delta(&self, r: f64) -> f64 {
        r * r - 2.0 * self.rg * r + self.a * self.a
    }

    pub fn metric_g_tt(&self, r: f64, theta: f64) -> f64 {
        let sigma = self.sigma(r, theta);
        -(1.0 - 2.0 * self.rg * r / sigma)
    }

    pub fn frame_dragging(&self, r: f64, theta: f64) -> f64 {
        let sigma = self.sigma(r, theta);
        let a2 = self.a * self.a;
        2.0 * self.rg * r * self.a * C
            / (sigma * (r * r + a2) + 2.0 * self.rg * r * a2 * theta.sin().powi(2))
    }

    pub fn orbital_frequency(&self, r: f64) -> f64 {
        let m = self.mass;
        let sign = if self.spin >= 0.0 { 1.0 } else { -1.0 };
        C.powi(3) / (G * m) / ((r / self.rg).powf(1.5) + sign * self.spin)
    }

    pub fn trace_photon(
        &self,
        start: SpacetimePoint,
        velocity: [f64; 3],
        max_steps: usize,
        dt: f64,
    ) -> Vec<SpacetimePoint> {
        let rh = self.event_horizon();
        let r_max = start.r * 10.0;
        let mut trajectory = Vec::with_capacity(max_steps + 1);
        trajectory.push(start);

        let mut r = start.r;
        let mut theta = start.theta;
        let mut phi = start.phi;
        let mut t = start.t;
        let mut vr = velocity[0];
        let mut vtheta = velocity[1];
        let mut vphi = velocity[2];

        for _ in 0..max_steps {
            let sigma = self.sigma(r, theta);
            let ar = -self.rg * C * C / sigma
                + r * (vtheta * vtheta + vphi * vphi * theta.sin().powi(2));
            let atheta = -vphi * vphi * theta.sin() * theta.cos()
                + self.a * self.a * theta.sin() * theta.cos() * vr * vr / (sigma * sigma);
            let aphi = -2.0 * vr * vphi / r.max(1e-10)
                + 2.0 * self.a * self.rg * vr / (sigma * r.max(1e-10));

            vr += ar * dt;
            vtheta += atheta * dt;
            vphi += aphi * dt;

            r += vr * dt;
            theta = (theta + vtheta * dt / r.max(1e-10)).clamp(0.01, std::f64::consts::PI - 0.01);
            phi += vphi * dt / (r * theta.sin()).max(1e-10);
            t += dt;

            trajectory.push(SpacetimePoint::new(t, r, theta, phi));

            if r < rh || r > r_max {
                break;
            }
        }

        trajectory
    }

    pub fn christoffel_ttr(&self, r: f64, theta: f64) -> f64 {
        let sigma = self.sigma(r, theta);
        let delta = self.delta(r);
        self.rg * (r * r - self.a * self.a * theta.cos().powi(2)) * delta / (sigma * sigma * sigma)
    }
}