use crate::integrate_log;
use metrology::{C, G};
use core::f64::consts::PI;
pub fn chirp_mass(m1: f64, m2: f64) -> f64 {
(m1 * m2).powf(0.6) / (m1 + m2).powf(0.2)
}
pub fn f_isco(m: f64) -> f64 {
C.powi(3) / (6f64.powf(1.5) * PI * G * m)
}
pub fn kerr_isco(a: f64) -> (f64, f64, f64) {
let z1 = 1.0 + (1.0 - a * a).cbrt() * ((1.0 + a).cbrt() + (1.0 - a).cbrt());
let z2 = (3.0 * a * a + z1 * z1).sqrt();
let r = 3.0 + z2 - a.signum() * ((3.0 - z1) * (3.0 + z1 + 2.0 * z2)).sqrt();
(r, (1.0 - 2.0 / (3.0 * r)).sqrt(), 2.0 / (3.0 * 3f64.sqrt()) * (1.0 + 2.0 * (3.0 * r - 2.0).sqrt()))
}
pub fn kerr_f_isco(m: f64, a: f64) -> f64 {
let (r, _, _) = kerr_isco(a);
C.powi(3) / (PI * G * m * (r.powf(1.5) + a))
}
pub fn kerr_photon_orbit(a: f64) -> f64 {
2.0 * (1.0 + (2.0 / 3.0 * (-a).acos()).cos())
}
pub fn time_to_merger(f: f64, chirp: f64) -> f64 {
5.0 / 256.0 * (PI * f).powf(-8.0 / 3.0) * (G * chirp / C.powi(3)).powf(-5.0 / 3.0)
}
pub fn frequency_drift(f: f64, chirp: f64) -> f64 {
96.0 / 5.0 * PI.powf(8.0 / 3.0) * (G * chirp / C.powi(3)).powf(5.0 / 3.0) * f.powf(11.0 / 3.0)
}
pub fn amplitude(f: f64, chirp: f64, d: f64) -> f64 {
(5.0f64 / 24.0).sqrt() * PI.powf(-2.0 / 3.0) * (C / d) * (G * chirp / C.powi(3)).powf(5.0 / 6.0) * f.powf(-7.0 / 6.0)
}
pub fn snr(h: impl Fn(f64) -> f64, s_n: impl Fn(f64) -> f64, f_lo: f64, f_hi: f64) -> f64 {
(4.0 * integrate_log(f_lo, f_hi, 4000, |f| { let a = h(f); a * a / s_n(f) })).sqrt()
}
pub fn horizon(m1: f64, m2: f64, threshold: f64, s_n: impl Fn(f64) -> f64, f_lo: f64) -> f64 {
let chirp = chirp_mass(m1, m2);
let f_hi = f_isco(m1 + m2);
if f_lo >= f_hi {
return 0.0;
}
snr(|f| amplitude(f, chirp, 1.0), s_n, f_lo, f_hi) / threshold
}
pub fn energy_spectrum(chirp: f64, f: f64) -> f64 {
PI.powf(2.0 / 3.0) / 3.0 * G.powf(2.0 / 3.0) * chirp.powf(5.0 / 3.0) * f.powf(-1.0 / 3.0)
}
pub const HORIZON_TO_RANGE: f64 = 2.264;
#[cfg(test)]
mod tests {
use super::*;
use crate::curves;
use metrology::{MPC, SOLAR_MASS};
#[test]
fn binary_neutron_star_numbers() {
let m = 1.4 * SOLAR_MASS;
let mc = chirp_mass(m, m);
assert!((mc / SOLAR_MASS / 1.21877 - 1.0).abs() < 1e-5, "{}", mc / SOLAR_MASS);
assert!((time_to_merger(20.0, mc) / 157.9 - 1.0).abs() < 1e-3, "{}", time_to_merger(20.0, mc));
assert!((frequency_drift(100.0, mc) / 17.36 - 1.0).abs() < 1e-3);
assert!((f_isco(2.0 * m) / 1570.4 - 1.0).abs() < 1e-4);
assert!((f_isco(SOLAR_MASS) / 4397.0 - 1.0).abs() < 1e-3);
}
#[test]
fn kerr_orbits() {
let close = |a: f64, b: f64| (a - b).abs() < 1e-14 * b.abs().max(1.0);
let cases = [(0.0, 6.0, (8.0f64 / 9.0).sqrt(), 2.0 * 3f64.sqrt()), (1.0, 1.0, 1.0 / 3f64.sqrt(), 2.0 / 3f64.sqrt()), (-1.0, 9.0, (25.0f64 / 27.0).sqrt(), 22.0 / (3.0 * 3f64.sqrt()))];
for (a, r, e, l) in cases {
let (ri, ei, li) = kerr_isco(a);
assert!(close(ri, r) && close(ei, e) && close(li, l), "a = {a}: {ri} {ei} {li}");
}
for a in [-0.9, -0.5, 0.3, 0.7, 0.99] {
let (r, e, l) = kerr_isco(a);
assert!((r * r - 6.0 * r + 8.0 * a * r.sqrt() - 3.0 * a * a).abs() < 1e-13 * r * r, "a = {a}");
let d = r.powf(0.75) * (r.powf(1.5) - 3.0 * r.sqrt() + 2.0 * a).sqrt();
assert!(close(e, (r.powf(1.5) - 2.0 * r.sqrt() + a) / d) && close(l, (r * r - 2.0 * a * r.sqrt() + a * a) / d), "a = {a}");
let p = kerr_photon_orbit(a);
assert!((p.powf(1.5) - 3.0 * p.sqrt() + 2.0 * a).abs() < 1e-14, "photon orbit at a = {a}");
}
assert!(close(kerr_photon_orbit(0.0), 3.0) && close(kerr_photon_orbit(1.0), 1.0) && close(kerr_photon_orbit(-1.0), 4.0));
assert!(close(f_isco(SOLAR_MASS), kerr_f_isco(SOLAR_MASS, 0.0)));
}
#[test]
fn kerr_isco_frequency() {
let m_omega = |a: f64| kerr_f_isco(SOLAR_MASS, a) * PI * G * SOLAR_MASS / C.powi(3);
for (a, exact) in [(0.0, 6f64.powf(-1.5)), (1.0, 0.5), (-1.0, 1.0 / 26.0)] {
assert!((m_omega(a) / exact - 1.0).abs() < 1e-15, "a = {a}: {}", m_omega(a));
}
assert!((m_omega(0.9) - 0.225441708835).abs() < 1e-12, "{}", m_omega(0.9));
}
#[test]
fn snr_scales_inversely_with_distance() {
let mc = chirp_mass(30.0 * SOLAR_MASS, 30.0 * SOLAR_MASS);
let near = snr(|f| amplitude(f, mc, 400.0 * MPC), curves::aligo, 20.0, 70.0);
let far = snr(|f| amplitude(f, mc, 800.0 * MPC), curves::aligo, 20.0, 70.0);
assert!((near / far - 2.0).abs() < 1e-9);
}
#[test]
fn horizon_with_the_analytic_fit() {
let d = horizon(1.4 * SOLAR_MASS, 1.4 * SOLAR_MASS, 8.0, curves::aligo, 20.0) / MPC;
assert!((d / 496.5 - 1.0).abs() < 1e-3, "{d}");
}
#[test]
fn heavy_binaries_that_merge_below_the_band_have_no_horizon() {
let m = 500.0 * SOLAR_MASS;
assert_eq!(horizon(m, m, 8.0, crate::curves::aligo, 10.0), 0.0);
}
#[test]
fn gw170817_spectral_fluence() {
let fluence = energy_spectrum(1.188 * SOLAR_MASS, 900.0) / (4.0 * PI * (40.0 * MPC).powi(2));
assert!((fluence / 2.66691e-7 - 1.0).abs() < 1e-5);
}
}