use core::f64::consts::{FRAC_PI_2, PI};
use metrology::{C, G};
pub const GW170817_LOW_SPIN: (f64, f64) = (70.0, 720.0);
pub const GW170817_HIGH_SPIN_MAX: f64 = 630.0;
pub fn lambda(k2: f64, radius: f64, mass: f64) -> f64 {
2.0 / 3.0 * k2 * (C * C * radius / (G * mass)).powi(5)
}
pub fn lambda_tilde(m1: f64, m2: f64, l1: f64, l2: f64) -> f64 {
16.0 / 13.0 * ((m1 + 12.0 * m2) * m1.powi(4) * l1 + (m2 + 12.0 * m1) * m2.powi(4) * l2) / (m1 + m2).powi(5)
}
pub fn love_number(compactness: f64, y: f64) -> f64 {
let (c, z) = (compactness, y + 3.0);
if c < 0.02 {
let b1 = (y * y + 6.0 * y - 6.0) / (z * z);
let b2 = (y.powi(3) + 34.0 * y * y - 8.0 * y + 12.0) / (7.0 * z.powi(3));
let b3 = (y.powi(4) + 62.0 * y.powi(3) + 84.0 * y * y + 48.0 * y + 36.0) / (7.0 * z.powi(4));
return (1.0 - 2.0 * c).powi(2) / 2.0 * ((2.0 - y) / z + c * (b1 + c * (b2 + c * b3)));
}
let u = 2.0 - y + 2.0 * c * (y - 1.0);
let braces = 2.0 * c * (6.0 - 3.0 * y + 3.0 * c * (5.0 * y - 8.0))
+ 4.0 * c.powi(3) * (13.0 - 11.0 * y + c * (3.0 * y - 2.0) + 2.0 * c * c * (1.0 + y))
+ 3.0 * (1.0 - 2.0 * c).powi(2) * u * (-2.0 * c).ln_1p();
8.0 / 5.0 * c.powi(5) * (1.0 - 2.0 * c).powi(2) * u / braces
}
#[derive(Clone, Debug)]
pub struct PiecewisePolytrope {
pieces: Vec<[f64; 5]>,
}
const SLY_CRUST: [(f64, f64, f64); 4] =
[(0.0, 6.80110e-9, 1.58425), (2.44034e7, 1.06186e-6, 1.28733), (3.78358e11, 5.32697e1, 0.62223), (2.62780e12, 3.99874e-8, 1.35692)];
const STEPS: f64 = 2000.0;
impl PiecewisePolytrope {
pub fn read(log_p1: f64, gamma: [f64; 3]) -> Self {
let p1 = 0.1 * 10f64.powf(log_p1) / (C * C);
let (rho1, rho2) = (10f64.powf(17.7), 1e18);
let k1 = p1 / rho1.powf(gamma[0]);
let k2 = p1 / rho1.powf(gamma[1]);
let mut pieces: Vec<_> = SLY_CRUST.iter().map(|&(rho, k, g)| (rho * 1e3, k * 1e3f64.powf(1.0 - g), g)).collect();
let (_, kc, gc) = pieces[3];
pieces.extend([((kc / k1).powf(1.0 / (gamma[0] - gc)), k1, gamma[0]), (rho1, k2, gamma[1]), (rho2, k2 * rho2.powf(gamma[1] - gamma[2]), gamma[2])]);
Self::new(&pieces)
}
pub fn sly() -> Self {
Self::read(34.384, [3.005, 2.988, 2.851])
}
pub fn ap4() -> Self {
Self::read(34.269, [2.830, 3.445, 3.348])
}
fn new(pieces: &[(f64, f64, f64)]) -> Self {
let mut out: Vec<[f64; 5]> = Vec::new();
for &(rho, k, g) in pieces {
let a = out.last().map_or(0.0, |&[_, kp, gp, ap, _]| ap + kp * rho.powf(gp - 1.0) / (gp - 1.0) - k * rho.powf(g - 1.0) / (g - 1.0));
out.push([rho, k, g, a, (a + k * g / (g - 1.0) * rho.powf(g - 1.0)).ln_1p()]);
}
Self { pieces: out }
}
fn at_enthalpy(&self, h: f64, inside: f64) -> [f64; 3] {
let &[_, k, g, a, _] = self.pieces.iter().rev().find(|p| p[4] <= inside).unwrap_or(&self.pieces[0]);
let x = (h.exp_m1() - a) * (g - 1.0) / g; if x <= 0.0 {
return [0.0; 3];
}
let rho = (x / k).powf(1.0 / (g - 1.0));
let (p, e) = (x * rho, (1.0 + a) * rho + x * rho / (g - 1.0));
[e, p, (e + p) * (1.0 + a + g * x / (g - 1.0)) / (g * x)]
}
pub fn star(&self, central_density: f64) -> (f64, f64, f64) {
let &[_, k, g, a, _] = self.pieces.iter().rev().find(|p| p[0] <= central_density).unwrap_or(&self.pieces[0]);
let hc = (a + k * g / (g - 1.0) * central_density.powf(g - 1.0)).ln_1p();
let geo = G / (C * C);
let breaks: Vec<f64> = self.pieces.iter().map(|p| p[4]).collect();
let (m, r, y) = tov_love(hc, &breaks, |h, inside| self.at_enthalpy(h, inside).map(|v| v * geo));
(m / geo, r, love_number(m / r, y))
}
pub fn star_with_mass(&self, mass: f64) -> Option<(f64, f64, f64)> {
stable_root(|x| self.star(x.exp()), 3e17f64.ln(), |s| s.0 - mass)
}
}
fn tov_love(hc: f64, breaks: &[f64], eos: impl Fn(f64, f64) -> [f64; 3]) -> (f64, f64, f64) {
let rhs = |t: f64, [r, mu, y]: [f64; 3], inside: f64| {
let [e, p, w] = eos(hc * t.cos().powi(2), inside);
let el = 1.0 / (1.0 - 2.0 * mu * r * r);
let dr = hc * (2.0 * t).sin() / (el * r * (mu + 4.0 * PI * p));
let nu = 2.0 * el * (mu + 4.0 * PI * p) * r;
let q = 4.0 * PI * el * (5.0 * e + 9.0 * p + w) - 6.0 * el / (r * r) - nu * nu;
[dr, (4.0 * PI * e - 3.0 * mu) * dr / r, -(y * y + y * el * (1.0 + 4.0 * PI * r * r * (p - e)) + r * r * q) / r * dr]
};
let t0: f64 = 1e-6;
let [e, p, _] = eos(hc, hc);
let mut u = [(3.0 * hc * t0.sin().powi(2) / (2.0 * PI * (e + 3.0 * p))).sqrt(), 4.0 / 3.0 * PI * e, 2.0];
let mut edges: Vec<f64> = breaks.iter().filter(|&&b| b > 0.0 && b < hc).map(|b| (b / hc).sqrt().acos()).collect();
edges.extend([t0, FRAC_PI_2]);
edges.sort_by(f64::total_cmp);
for w in edges.windows(2) {
let n = (STEPS * (w[1] - w[0]) / FRAC_PI_2).ceil().max(8.0);
let dt = (w[1] - w[0]) / n;
let inside = hc * (0.5 * (w[0] + w[1])).cos().powi(2);
for i in 0..n as usize {
let t = w[0] + i as f64 * dt;
let add = |u: [f64; 3], d: [f64; 3], f: f64| [u[0] + f * d[0], u[1] + f * d[1], u[2] + f * d[2]];
let k1 = rhs(t, u, inside);
let k2 = rhs(t + dt / 2.0, add(u, k1, dt / 2.0), inside);
let k3 = rhs(t + dt / 2.0, add(u, k2, dt / 2.0), inside);
let k4 = rhs(t + dt, add(u, k3, dt), inside);
u = core::array::from_fn(|j| u[j] + dt / 6.0 * (k1[j] + 2.0 * k2[j] + 2.0 * k3[j] + k4[j]));
}
}
let [r, mu, y] = u;
(mu * r.powi(3), r, y - 4.0 * PI * eos(0.0, 0.0)[0] / mu)
}
fn stable_root(star: impl Fn(f64) -> (f64, f64, f64), x0: f64, f: impl Fn((f64, f64, f64)) -> f64) -> Option<(f64, f64, f64)> {
let (mut lo, mut last) = (x0, star(x0));
if f(last) >= 0.0 {
return None;
}
let mut hi = lo;
loop {
hi += 0.2;
let s = star(hi);
if f(s) >= 0.0 {
break;
}
if s.0 < last.0 {
return None;
}
(lo, last) = (hi, s);
}
for _ in 0..50 {
let mid = 0.5 * (lo + hi);
if f(star(mid)) < 0.0 { lo = mid } else { hi = mid }
}
Some(star(hi))
}
#[cfg(test)]
mod tests {
use super::*;
use metrology::SOLAR_MASS;
fn k2_at(c: f64, star: impl Fn(f64) -> (f64, f64, f64)) -> f64 {
let (m, r, y) = stable_root(star, 1e-6f64.ln(), |(m, r, _)| m / r - c).unwrap();
love_number(m / r, y)
}
fn e_polytrope(gamma: f64) -> impl Fn(f64) -> (f64, f64, f64) {
move |x| {
tov_love(x.exp(), &[], |h, _| {
let e = ((gamma - 1.0) * h / gamma).exp_m1().max(0.0).powf(1.0 / (gamma - 1.0));
if e == 0.0 { [0.0; 3] } else { [e, e.powf(gamma), (e.powf(2.0 - gamma) + e) / gamma] }
})
}
}
#[test]
fn love_numbers_in_closed_form() {
let dn09 = 4096.0 / (10935.0 * (308.0 - 81.0 * 3f64.ln()));
assert!((love_number(4.0 / 9.0, 2.0) / dn09 - 1.0).abs() < 1e-14 && (dn09 - 0.001_710_300_596_149_0).abs() < 5e-17);
assert!((love_number(0.0, -1.0) - 0.75).abs() < 1e-15);
assert!((love_number(0.0, (PI * PI - 9.0) / 3.0) - (15.0 - PI * PI) / (2.0 * PI * PI)).abs() < 1e-15);
for y in [-1.0, 0.0, 0.29, 1.0, 2.0] {
assert!((love_number(0.02 - 1e-15, y) - love_number(0.02, y)).abs() < 3e-8, "{y}");
}
}
#[test]
fn an_incompressible_star() {
for (c, want) in [(1e-8f64, 0.75), (0.05, 0.556_257), (0.2, 0.183_019), (0.4, 0.010_347)] {
let q = (1.0 - 2.0 * c).sqrt();
let (m, r, y) = tov_love(((1.0 - q) / (3.0 * q - 1.0)).ln_1p(), &[], |h, _| [1.0, h.exp_m1(), 0.0]);
assert!((m / r / c - 1.0).abs() < 1e-10, "{c}: {}", m / r);
assert!((love_number(m / r, y) - want).abs() < 6e-7, "{c}: {}", love_number(m / r, y));
}
}
#[test]
fn polytropes_and_their_two_conventions() {
let mu = PiecewisePolytrope::new(&[(0.0, 1e-19, 2.0)]);
let a = [0.9991, -2.9287, -1.1373, 14.0013, -50.9711];
for c in [0.05f64, 0.1, 0.15, 0.2] {
let fit = (15.0 - PI * PI) / (2.0 * PI * PI) * (1.0 - 2.0 * c).powi(2) * a.iter().rev().fold(0.0, |s, an| s * c + an);
let (_, _, k2) = stable_root(|x| mu.star(x.exp()), 1e16f64.ln(), |(m, r, _)| G * m / (C * C * r) - c).unwrap();
assert!((k2 / fit - 1.0).abs() < 6e-4, "{c}: {k2} against {fit}");
}
for (c, hinderer) in [(0.1, 0.122), (0.15, 0.0776), (0.2, 0.0459)] {
assert!((k2_at(c, e_polytrope(2.0)) / hinderer - 1.0).abs() < 0.01, "{c}");
}
for (c, ours, hinderer) in [(0.1, 0.22842, 0.251), (0.15, 0.15314, 0.173), (0.2, 0.09676, 0.095)] {
let k2 = k2_at(c, e_polytrope(3.0));
assert!((k2 - ours).abs() < 1e-5 && (hinderer / k2 - 1.0).abs() > 0.015, "{c}: {k2}");
}
}
#[test]
fn read_et_al_piecewise_polytropes() {
for (eos, radius, read, k2, our_lambda, m_max) in [
(PiecewisePolytrope::sly(), 11.706, 11.711, 0.07617, 295.6, 2.0494),
(PiecewisePolytrope::ap4(), 11.320, 11.325, 0.07545, 247.6, 2.1891),
] {
let (m, r, k) = eos.star_with_mass(1.4 * SOLAR_MASS).unwrap();
assert!((r / 1e3 - radius).abs() < 5e-4 && (r / 1e3 / read - 1.0).abs() < 1e-3, "{r}");
assert!((k - k2).abs() < 5e-6, "{k}");
assert!((lambda(k, r, m) - our_lambda).abs() < 0.05, "{}", lambda(k, r, m));
let (mut a, mut b, g) = (1.5e18f64.ln(), 6e18f64.ln(), (5f64.sqrt() - 1.0) / 2.0);
for _ in 0..60 {
let (x1, x2) = (b - g * (b - a), a + g * (b - a));
if eos.star(x1.exp()).0 > eos.star(x2.exp()).0 { b = x2 } else { a = x1 }
}
let most = eos.star(a.exp()).0 / SOLAR_MASS;
assert!((most / m_max - 1.0).abs() < 1e-3, "{most}");
assert!(eos.star_with_mass(1.001 * most * SOLAR_MASS).is_none());
}
let (_, _, k) = PiecewisePolytrope::sly().star_with_mass(1.4 * SOLAR_MASS).unwrap();
assert!((k / 0.0763 - 1.0).abs() < 2e-3);
}
#[test]
fn the_injections_of_table_vi() {
for (m1, m2, l1, l2, want) in [(1.38, 1.37, 275.0, 309.0, 292.0), (1.68, 1.13, 77.0, 973.0, 303.0), (1.38, 1.37, 1018.0, 1063.0, 1040.0)] {
let got = lambda_tilde(m1, m2, l1, l2);
assert!((got - want).abs() < 0.5, "({m1}, {m2}): {got}");
}
assert!((lambda_tilde(1.4, 1.4, 190.0, 190.0) - 190.0).abs() < 1e-12);
assert!((lambda_tilde(1.4, 1.2, 190.0, 500.0) - lambda_tilde(1.2, 1.4, 500.0, 190.0)).abs() < 1e-12);
}
#[test]
fn a_star_deforms_as_its_compactness_to_the_minus_fifth() {
let k2 = (15.0 - PI * PI) / (2.0 * PI * PI);
assert!((k2 - 0.25991).abs() < 5e-6);
let (m, r) = (1.4 * SOLAR_MASS, 12e3);
let c = G * m / (C * C * r);
assert!((lambda(k2, r, m) * c.powi(5) - 2.0 / 3.0 * k2).abs() < 1e-15);
assert!((lambda(k2, 2.5 * r, m) / lambda(k2, r, m) - 2.5f64.powi(5)).abs() < 1e-9);
}
}