use core::f64::consts::{PI, SQRT_2};
use metrology::{C, G};
pub const PLANCK_LUMINOSITY: f64 = C * C * C * C * C / G;
pub fn strain_from_luminosity(fraction: f64, f: f64, r: f64) -> f64 {
2.0 * SQRT_2 * fraction.sqrt() * C / (2.0 * PI * f * r)
}
pub fn circular_flux(eta: f64, x: f64) -> f64 {
32.0 / 5.0 * eta * eta * x.powi(5)
}
pub fn energy_flux_coefficients(l: u32) -> (f64, f64) {
let factorial = |n: u32| (1..=n).map(f64::from).product::<f64>();
let double_factorial = |n: u32| (1..=n).rev().step_by(2).map(f64::from).product::<f64>();
let l_f = f64::from(l);
let a = (l_f + 1.0) * (l_f + 2.0) / ((l_f - 1.0) * l_f * factorial(l) * double_factorial(2 * l + 1));
let b = 4.0 * l_f * (l_f + 2.0) / ((l_f - 1.0) * factorial(l + 1) * double_factorial(2 * l + 1));
(a, b)
}
pub fn peters_enhancement(e: f64) -> f64 {
let e2 = e * e;
(1.0 + 73.0 / 24.0 * e2 + 37.0 / 96.0 * e2 * e2) / (1.0 - e2).powf(3.5)
}
pub fn peters_pbdot(mp: f64, mc: f64, pb: f64, e: f64) -> f64 {
-192.0 * PI / 5.0 * (G / C.powi(3)).powf(5.0 / 3.0) * (pb / (2.0 * PI)).powf(-5.0 / 3.0) * peters_enhancement(e) * mp * mc
/ (mp + mc).cbrt()
}
pub fn shapiro_range(mc: f64) -> f64 {
G * mc / C.powi(3)
}
pub fn shapiro_shape(mp: f64, mc: f64, pb: f64, x: f64) -> f64 {
let m = mp + mc;
x * (2.0 * PI / pb).powf(2.0 / 3.0) * (G * m / C.powi(3)).powf(-1.0 / 3.0) * m / mc
}
pub fn periastron_advance(gm: f64, a: f64, e: f64) -> f64 {
6.0 * PI * gm / (C * C * a * (1.0 - e * e))
}
pub fn geodetic_precession(gm: f64, a: f64) -> f64 {
1.5 * gm.powf(1.5) / (C * C * a.powf(2.5))
}
pub fn shapiro_delay(r: f64, s: f64, e: f64, omega: f64, u: f64) -> f64 {
-2.0 * r * lambda_u(s, e, omega, u).ln()
}
fn lambda_u(s: f64, e: f64, omega: f64, u: f64) -> f64 {
let (sw, cw) = omega.sin_cos();
let (su, cu) = u.sin_cos();
1.0 - e * cu - s * (sw * (cu - e) + (1.0 - e * e).sqrt() * cw * su)
}
fn retardation(s: f64, e: f64, omega: f64, u: f64, x_b: f64, pb: f64) -> f64 {
let (sw, cw) = omega.sin_cos();
let (su, cu) = u.sin_cos();
let (k, q) = (2.0 * PI * x_b / pb, (1.0 - e * e).sqrt());
k / s * e * su - k * s / q * (sw * (cu - e) + q * cw * su) * (e * cw + ((cu - e) * cw - q * sw * su) / (1.0 - e * cu))
}
#[allow(clippy::too_many_arguments)]
pub fn shapiro_delay_nlo(r: f64, s: f64, e: f64, omega: f64, u: f64, x: f64, x_b: f64, pb: f64, q_nlo: f64) -> f64 {
let lensing = 2.0 * r * s / (x + x_b);
-2.0 * r * (lambda_u(s, e, omega, u) + q_nlo * (lensing + retardation(s, e, omega, u, x_b, pb))).ln()
}
pub fn aberration_amplitude(period: f64, pb: f64, x: f64, e: f64, s: f64) -> f64 {
period * x / (pb * (1.0 - e * e).sqrt() * s * s)
}
#[allow(clippy::too_many_arguments)]
pub fn aberration_bending(period: f64, r: f64, s: f64, e: f64, omega: f64, u: f64, x: f64, x_b: f64, q_nlo: f64) -> f64 {
let theta = 2.0 * ((1.0 + e).sqrt() * (u / 2.0).sin()).atan2((1.0 - e).sqrt() * (u / 2.0).cos());
q_nlo * period / PI * r / (x + x_b) * (omega + theta).cos() / lambda_u(s, e, omega, u)
}
#[cfg(test)]
mod tests {
use super::*;
use metrology::{MPC, SOLAR_MASS};
fn close(a: f64, b: f64, rel: f64) -> bool {
(a / b - 1.0).abs() < rel
}
#[test]
fn luminosity_and_strain() {
assert!(close(PLANCK_LUMINOSITY, 3.62825e52, 1e-5) && close(PLANCK_LUMINOSITY / 4.0, 9.07064e51, 1e-5));
assert!(close(3.7e49 / PLANCK_LUMINOSITY, 1.01977e-3, 1e-5)); assert!(close(strain_from_luminosity(1e-3, 250.0, 410.0 * MPC), 1.34931e-21, 1e-5));
assert!(close(strain_from_luminosity(1.0, 1.0, C), 0.450158, 1e-5));
assert!(close(circular_flux(0.25, 1.0 / 6.0), 5.14403e-5, 1e-5));
}
#[test]
fn multipole_coefficients() {
for (l, a, b) in [(2, 1.0 / 5.0, 16.0 / 45.0), (3, 1.0 / 189.0, 1.0 / 84.0), (4, 1.0 / 9072.0, 4.0 / 14175.0)] {
let (al, bl) = energy_flux_coefficients(l);
assert!(close(al, a, 1e-12) && close(bl, b, 1e-12), "{l}: {al} {bl}");
}
}
#[test]
fn binary_pulsars() {
assert!(close(peters_enhancement(0.6171340), 11.8568, 1e-5));
let day = 86_400.0;
let b1913 = peters_pbdot(1.438 * SOLAR_MASS, 1.390 * SOLAR_MASS, 0.322997448918 * day, 0.6171340);
assert!(close(b1913, -2.40210e-12, 1e-5) && close(b1913, -2.40263e-12, 1e-3));
let j0737 = peters_pbdot(1.338185 * SOLAR_MASS, 1.248868 * SOLAR_MASS, 0.1022515592973 * day, 0.087777023);
assert!(close(j0737, -1.24781e-12, 1e-5));
}
#[test]
fn periastron_advances_and_geodetic_precession() {
use metrology::{AU, GM_SUN, YEAR};
let arcsec = PI / 648_000.0;
let (a, e) = (0.387_099_27 * AU, 0.205_635_93);
let period = 2.0 * PI * (a * a * a / GM_SUN).sqrt();
assert!(close(periastron_advance(GM_SUN, a, e) * 100.0 * YEAR / period / arcsec, 42.98, 5e-4));
let (pb, e, m) = (0.1022515592973 * 86_400.0, 0.087777023, 2.587052 * GM_SUN);
let a = (m * (pb / (2.0 * PI)).powi(2)).cbrt();
let omega_dot = periastron_advance(m, a, e) / pb * YEAR * 180.0 / PI;
assert!(close(omega_dot, 16.899_367, 1e-6) && close(omega_dot - 16.899_323, 4.4e-5, 0.05), "{omega_dot}");
let s2 = periastron_advance(4.261e6 * GM_SUN, 125.058e-3 * 8246.7 * AU, 0.884_649) / arcsec / 60.0;
assert!(close(s2, 12.156, 1e-3) && close(s2, 12.1, 0.06));
assert!(close(geodetic_precession(GM_SUN, AU) * 100.0 * YEAR / arcsec, 1.919, 1e-3));
let mas_per_year = YEAR * 180.0 / PI * 3.6e6;
assert!(close(geodetic_precession(metrology::GM_EARTH, 6371e3 + 642e3) * mas_per_year / 6606.1, 1.0, 6e-3));
assert!(close(periastron_advance(GM_SUN, AU, 0.0), 6.0 * PI * GM_SUN / (C * C * AU), 1e-15));
assert!(periastron_advance(GM_SUN, AU, 0.99) > 50.0 * periastron_advance(GM_SUN, AU, 0.0));
}
#[test]
fn the_double_pulsar_shapiro_delay() {
let (ma, mb) = (1.338185 * SOLAR_MASS, 1.248868 * SOLAR_MASS);
let r = shapiro_range(mb);
assert!((r / 6.1513e-6 - 1.0).abs() < 1e-5 && ((6.162e-6 - r) / 0.021e-6 - 0.51).abs() < 0.01);
let measured = 1.0 - (-9.65f64).exp();
assert!((measured - 0.999936).abs() < 1e-6);
let s = shapiro_shape(ma, mb, 0.1022515592973 * 86_400.0, 1.415028603);
assert!((measured / s - 1.00009).abs() < 0.00018 && (measured / s - 1.000013).abs() < 1e-6, "{s}");
let peak = shapiro_delay(r, measured, 0.0, 0.3, PI / 2.0 - 0.3);
assert!((peak / (2.0 * r * 9.65) - 1.0).abs() < 1e-12 && peak > shapiro_delay(r, measured, 0.0, 0.3, 1.0));
}
fn double_pulsar() -> (f64, f64, f64, f64, f64, f64, f64) {
let x = 1.415028603;
(0.1022515592973 * 86_400.0, x, x * 1.338185 / 1.248868, 0.087777023, 204.753686f64.to_radians(), 6.162e-6, 1.0 - (-9.65f64).exp())
}
fn near_conjunction(e: f64, omega: f64) -> impl Iterator<Item = f64> {
let f = PI / 2.0 - omega;
let u = 2.0 * ((1.0 - e).sqrt() * (f / 2.0).sin()).atan2((1.0 + e).sqrt() * (f / 2.0).cos());
(0..=40_000).map(move |i| u - 0.2 + f64::from(i) * 1e-5)
}
#[test]
fn the_double_pulsar_at_next_to_leading_order() {
let (pb, x, xb, e, om, r, s) = double_pulsar();
let spin = 1.0 / 44.05406864196281;
let a = aberration_amplitude(spin, pb, x, e, s);
assert!((a / 3.65e-6 - 1.0).abs() < 1e-3 && (a - 3.6503e-6).abs() < 5e-11, "{a}");
for u in [0.0, 1.0, 2.5, 4.0] {
assert_eq!(shapiro_delay_nlo(r, s, e, om, u, x, xb, pb, 0.0), shapiro_delay(r, s, e, om, u));
let circular = -2.0 * PI * xb / pb * s * (om + u).sin() * (om + u).cos();
assert!((retardation(s, 0.0, om, u, xb, pb) - circular).abs() < 1e-18);
}
let lo = |u| shapiro_delay(r, s, e, om, u);
let u_min = near_conjunction(e, om).min_by(|&a, &b| lambda_u(s, e, om, a).total_cmp(&lambda_u(s, e, om, b))).unwrap();
let lensing = -2.0 * r * (lambda_u(s, e, om, u_min) + 2.0 * r * s / (x + xb)).ln() - lo(u_min);
assert!((lensing + 0.7569e-6).abs() < 5e-11, "{lensing}");
let range = |f: &dyn Fn(f64) -> f64| near_conjunction(e, om).map(f).fold((f64::MAX, f64::MIN), |(a, b), v| (a.min(v), b.max(v)));
let ret = range(&|u| -2.0 * r * (lambda_u(s, e, om, u) + retardation(s, e, om, u, xb, pb)).ln() - lo(u));
let both = range(&|u| shapiro_delay_nlo(r, s, e, om, u, x, xb, pb, 1.0) - lo(u));
let bend = range(&|u| aberration_bending(spin, r, s, e, om, u, x, xb, 1.0));
for ((got_lo, got_hi), (want_lo, want_hi)) in [(ret, (-1.0820, 1.1885)), (both, (-1.4925, 0.8357)), (bend, (-1.3002, 1.2977))] {
assert!((got_lo * 1e6 - want_lo).abs() < 5e-5 && (got_hi * 1e6 - want_hi).abs() < 5e-5, "{got_lo} {got_hi}");
}
let d = aberration_bending(spin, r, s, e, om, 1.0, x, xb, 1.0) * lambda_u(s, e, om, 1.0);
let theta = 2.0 * ((1.0 + e).sqrt() * 0.5f64.sin()).atan2((1.0 - e).sqrt() * 0.5f64.cos());
assert!((d / (om + theta).cos() - 15.1891e-9).abs() < 5e-14);
}
#[test]
fn retardation_is_the_moving_companion_to_first_order() {
let (pb, x, xb, e, om, r, s) = double_pulsar();
let (ci, ar, lam) = ((1.0 - s * s).sqrt(), (x + xb) / s, 0.03);
let p = [om.cos(), om.sin() * ci, om.sin() * s];
let q = [-om.sin(), om.cos() * ci, om.cos() * s];
let dot = |a: [f64; 3], b: [f64; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
let delay = |rv: [f64; 3], v: [f64; 3]| {
let (g2, w) = (1.0 / (1.0 - dot(v, v)), [-v[0], -v[1], -1.0 - v[2]]);
let inv_r = |t: f64| {
let d: [f64; 3] = core::array::from_fn(|j| rv[j] + w[j] * t);
1.0 / (dot(d, d) + g2 * dot(v, d).powi(2)).sqrt()
};
2.0 * r * g2 * (1.0 + v[2]).powi(2) * crate::integrate_log(1e-9, 1e7, 200_000, inv_r)
};
let f = PI / 2.0 - om;
let u_sc = 2.0 * ((1.0 - e).sqrt() * (f / 2.0).sin()).atan2((1.0 + e).sqrt() * (f / 2.0).cos());
let us = (-10..=10).map(|i| u_sc + 0.02 * f64::from(i)).chain((0..7).map(|i| u_sc + 0.5 + f64::from(i) * (2.0 * PI - 1.0) / 6.0));
let (mut kv, mut effect, mut misfit) = (vec![], vec![], vec![]);
for u in us {
let f = 2.0 * ((1.0 + e).sqrt() * (u / 2.0).sin()).atan2((1.0 - e).sqrt() * (u / 2.0).cos());
let rv: [f64; 3] = core::array::from_fn(|j| ar * (1.0 - e * u.cos()) * (p[j] * f.cos() + q[j] * f.sin()));
let speed = -lam * xb / (x + xb) * 2.0 * PI / pb * ar / (1.0 - e * e).sqrt();
let v: [f64; 3] = core::array::from_fn(|j| speed * (-p[j] * f.sin() + q[j] * (f.cos() + e)));
let lu = lambda_u(s, e, om, u);
assert!(((dot(rv, rv).sqrt() - rv[2]) / ar - lu).abs() < 1e-12);
let moving = delay(rv, v) - delay(rv, [0.0; 3]);
kv.push(-v[2]);
effect.push(moving);
misfit.push(moving + 2.0 * r * (1.0 + v[2]) * (lu + retardation(s, e, om, u, xb, pb / lam)).ln() - 2.0 * r * lu.ln());
}
let detrended = |y: &[f64]| {
let n = y.len() as f64;
let (sx, sy, sxx, sxy) = kv.iter().zip(y).fold((0.0, 0.0, 0.0, 0.0), |(a, b, c, d), (k, y)| (a + k, b + y, c + k * k, d + k * y));
let slope = (n * sxy - sx * sy) / (n * sxx - sx * sx);
let icpt = (sy - slope * sx) / n;
kv.iter().zip(y).map(|(k, y)| (y - icpt - slope * k).abs()).fold(0.0, f64::max)
};
let ratio = detrended(&misfit) / detrended(&effect);
assert!(ratio < 5e-3 && detrended(&effect) > 2.5e-8, "{ratio}");
}
}