use core::f64::consts::PI;
use metrology::{C, G, HBAR, K_B};
pub fn alcubierre_energy(radius: f64, wall: f64, speed: f64) -> f64 {
-speed * speed / 12.0 * (radius * radius / wall + wall / 12.0) * C * C / G
}
pub fn induced_acceleration(m: f64, a_flow: f64, r: f64) -> f64 {
4.0 * G * m * a_flow / (C * C * r)
}
pub fn quantum_inequality(tau: f64) -> f64 {
-3.0 * HBAR * C / (32.0 * PI * PI * (C * tau).powi(4))
}
pub fn casimir_energy_density(d: f64) -> f64 {
-PI * PI * HBAR * C / (720.0 * d.powi(4))
}
pub fn casimir_pressure(d: f64) -> f64 {
-PI * PI * HBAR * C / (240.0 * d.powi(4))
}
pub fn casimir_sphere_plate(radius: f64, d: f64) -> f64 {
-PI.powi(3) * HBAR * C * radius / (360.0 * d.powi(3))
}
pub fn wormhole_throat_tension(b0: f64) -> f64 {
C.powi(4) / (8.0 * PI * G * b0 * b0)
}
pub fn wormhole_edge_tension() -> f64 {
-C.powi(4) / (8.0 * G)
}
fn alcubierre_slope(r: f64, radius: f64, sigma: f64) -> f64 {
let sech2 = |x: f64| 1.0 / x.cosh().powi(2);
sigma * (sech2(sigma * (r + radius)) - sech2(sigma * (r - radius))) / (2.0 * (sigma * radius).tanh())
}
pub fn alcubierre_energy_density(radius: f64, sigma: f64, speed: f64, point: [f64; 3]) -> f64 {
let [x, y, z] = point;
let r2 = x * x + y * y + z * z;
if r2 == 0.0 {
return 0.0;
}
-C.powi(4) / (32.0 * PI * G) * speed * speed * alcubierre_slope(r2.sqrt(), radius, sigma).powi(2) * (y * y + z * z) / r2
}
pub fn alcubierre_tanh_energy(radius: f64, sigma: f64, speed: f64) -> f64 {
let (lo, hi) = ((radius - 40.0 / sigma).max(0.0), radius + 40.0 / sigma);
let panels = ((hi - lo) * sigma).ceil();
let half = 0.5 * (hi - lo) / panels;
let (x, w) = geopotential::gauss_legendre(16);
let integral: f64 = (0..panels as usize)
.map(|k| {
let mid = lo + (2 * k + 1) as f64 * half;
half * x.iter().zip(&w).map(|(x, w)| w * (mid + half * x).powi(2) * alcubierre_slope(mid + half * x, radius, sigma).powi(2)).sum::<f64>()
})
.sum();
-speed * speed / 12.0 * integral * C * C / G
}
pub fn alcubierre_minimum_energy(r0: f64, speed: f64) -> f64 {
-speed * speed / 12.0 * r0 * C * C / G
}
fn shift_invariants(jacobian: [[f64; 3]; 3]) -> (f64, f64) {
let trace = jacobian[0][0] + jacobian[1][1] + jacobian[2][2];
let square = (0..9).map(|n| (0.5 * (jacobian[n / 3][n % 3] + jacobian[n % 3][n / 3])).powi(2)).sum();
(trace * trace, square)
}
pub fn shift_drive_density(jacobian: [[f64; 3]; 3]) -> f64 {
let (k2, square) = shift_invariants(jacobian);
C.powi(4) / (16.0 * PI * G) * (k2 - square)
}
pub fn shift_drive_null_sum(jacobian: [[f64; 3]; 3], lie_trace: f64) -> f64 {
let (k2, square) = shift_invariants(jacobian);
C.powi(4) / (24.0 * PI * G) * (-2.0 * lie_trace + k2 - 3.0 * square)
}
pub fn thin_shell_wormhole(radius: f64, mass: f64) -> (f64, f64) {
let x = G * mass / (radius * C * C);
let scale = 4.0 * radius * wormhole_throat_tension(radius);
(-scale * (1.0 - 2.0 * x).sqrt(), 0.5 * scale * (1.0 - x) / (1.0 - 2.0 * x).sqrt())
}
pub fn wormhole_throat_bound(f: f64) -> f64 {
(quantum_inequality(f / C) / -wormhole_throat_tension(1.0)).sqrt()
}
pub fn wormhole_band_bound(r0: f64, f: f64) -> f64 {
(quantum_inequality(f / C) * r0 / (-2.0 * wormhole_throat_tension(1.0))).cbrt()
}
pub fn quantum_inequality_gaussian(tau: f64) -> f64 {
0.5 * quantum_inequality(tau)
}
fn nudge(x: [f64; 4], a: usize, dx: f64) -> [f64; 4] {
let mut y = x;
y[a] += dx;
y
}
fn central(v: [f64; 4], h: f64) -> f64 {
(8.0 * (v[2] - v[1]) - (v[3] - v[0])) / (12.0 * h)
}
fn inverse(g: [[f64; 4]; 4]) -> [[f64; 4]; 4] {
core::array::from_fn(|j| {
let rows = (0..4).map(|i| [&g[i][..], &[if i == j { 1.0 } else { 0.0 }]].concat()).collect();
let column = geopotential::linalg::gauss(rows);
core::array::from_fn(|i| column[i])
})
}
fn christoffel(metric: &impl Fn([f64; 4]) -> [[f64; 4]; 4], x: [f64; 4], h: f64) -> [[[f64; 4]; 4]; 4] {
let inv = inverse(metric(x));
let dg: [[[f64; 4]; 4]; 4] = core::array::from_fn(|a| {
let v = [-2.0, -1.0, 1.0, 2.0].map(|k| metric(nudge(x, a, k * h)));
core::array::from_fn(|m| core::array::from_fn(|n| central(v.map(|g| g[m][n]), h)))
});
core::array::from_fn(|l| core::array::from_fn(|m| core::array::from_fn(|n| 0.5 * (0..4).map(|s| inv[l][s] * (dg[m][s][n] + dg[n][s][m] - dg[s][m][n])).sum::<f64>())))
}
pub fn stress_energy(metric: impl Fn([f64; 4]) -> [[f64; 4]; 4], x: [f64; 4], step: f64) -> [[f64; 4]; 4] {
let gamma = christoffel(&metric, x, step);
let dgamma: [[[[f64; 4]; 4]; 4]; 4] = core::array::from_fn(|a| {
let v = [-2.0, -1.0, 1.0, 2.0].map(|k| christoffel(&metric, nudge(x, a, k * step), step));
core::array::from_fn(|l| core::array::from_fn(|m| core::array::from_fn(|n| central(v.map(|c| c[l][m][n]), step))))
});
let ricci: [[f64; 4]; 4] = core::array::from_fn(|m| {
core::array::from_fn(|n| (0..4).map(|l| dgamma[l][l][m][n] - dgamma[n][l][m][l] + (0..4).map(|s| gamma[l][l][s] * gamma[s][m][n] - gamma[l][n][s] * gamma[s][m][l]).sum::<f64>()).sum())
});
let g = metric(x);
let inv = inverse(g);
let scalar: f64 = (0..16).map(|k| inv[k / 4][k % 4] * ricci[k / 4][k % 4]).sum();
core::array::from_fn(|m| core::array::from_fn(|n| (ricci[m][n] - 0.5 * g[m][n] * scalar) / metrology::EINSTEIN_KAPPA))
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct EnergyConditions {
pub eulerian: [[f64; 4]; 4],
pub null: f64,
pub type_one: Option<[f64; 4]>,
}
pub fn energy_conditions(metric: [[f64; 4]; 4], stress: [[f64; 4]; 4]) -> EnergyConditions {
let inv = inverse(metric);
let dot = |u: [f64; 4], v: [f64; 4]| (0..16).map(|k| metric[k / 4][k % 4] * u[k / 4] * v[k % 4]).sum::<f64>();
let alpha = (-1.0 / inv[0][0]).sqrt();
let mut frame = [inv[0].map(|g| -alpha * g); 4];
for i in 1..4 {
let mut u = [0.0; 4];
u[i] = 1.0;
for e in &frame[1..i] {
let p = dot(u, *e);
u.iter_mut().zip(e).for_each(|(x, e)| *x -= p * e);
}
let norm = dot(u, u).sqrt();
frame[i] = u.map(|x| x / norm);
}
let eulerian = core::array::from_fn(|a| core::array::from_fn(|b| (0..16).map(|k| frame[a][k / 4] * frame[b][k % 4] * stress[k / 4][k % 4]).sum()));
EnergyConditions { eulerian, null: null_minimum(eulerian), type_one: hawking_ellis_type_one(eulerian) }
}
fn null_minimum(t: [[f64; 4]; 4]) -> f64 {
let stress: Vec<f64> = (0..9).map(|k| t[k / 3 + 1][k % 3 + 1]).collect();
let (values, vectors) = geopotential::linalg::symmetric_eigen(&stress, 3);
let parts: Vec<(f64, f64)> = (0..3).map(|k| (values[k], (0..3).map(|i| vectors[i * 3 + k] * t[0][i + 1]).sum::<f64>())).filter(|p| p.1 != 0.0).collect();
let low = values.iter().copied().fold(f64::INFINITY, f64::min);
let size = parts.iter().map(|p| p.1 * p.1).sum::<f64>().sqrt();
let length = |mu: f64| parts.iter().map(|(l, g)| (g / (l - mu)).powi(2)).sum::<f64>();
let (mut lo, mut hi) = (low - size, low);
for _ in 0..200 {
let mid = 0.5 * (lo + hi);
if length(mid) > 1.0 { hi = mid } else { lo = mid }
}
t[0][0] + lo - parts.iter().map(|(l, g)| g * g / (l - lo)).sum::<f64>()
}
fn hawking_ellis_type_one(t: [[f64; 4]; 4]) -> Option<[f64; 4]> {
let m: [[f64; 4]; 4] = core::array::from_fn(|a| t[a].map(|x| if a == 0 { -x } else { x }));
let scale = m.iter().flatten().fold(0.0f64, |big, x| big.max(x.abs()));
if scale == 0.0 {
return Some([0.0; 4]);
}
let tol = 1e-4 * scale;
let values = eigenvalues(m)?;
if values.iter().any(|v| v.1.abs() > tol) {
return None;
}
let mut lambda = values.map(|v| v.0);
lambda.sort_by(f64::total_cmp);
let mut bounds = vec![0];
bounds.extend((1..4).filter(|&k| lambda[k] - lambda[k - 1] > tol));
bounds.push(4);
let means: Vec<f64> = bounds.windows(2).map(|w| lambda[w[0]..w[1]].iter().sum::<f64>() / (w[1] - w[0]) as f64).collect();
let apply = |v: [f64; 4], shift: f64| -> [f64; 4] { core::array::from_fn(|a| (0..4).map(|b| m[a][b] * v[b]).sum::<f64>() - shift * v[a]) };
for (c, &mean) in means.iter().enumerate() {
let others = means.iter().enumerate().filter(|&(d, _)| d != c);
let v = others.clone().fold([1.0, 0.0, 0.0, 0.0], |v, (_, &shift)| apply(v, shift)).map(|x| x / others.clone().map(|(_, d)| mean - d).product::<f64>());
let size2: f64 = v.iter().map(|x| x * x).sum();
let norm = size2 - 2.0 * v[0] * v[0];
if norm > -0.5 {
continue;
}
if apply(v, mean).iter().map(|x| x * x).sum::<f64>() > (10.0 * tol).powi(2) * size2 {
return None;
}
let carried = (0..16).map(|k| t[k / 4][k % 4] * v[k / 4] * v[k % 4]).sum::<f64>() / norm;
let k = (bounds[c]..bounds[c + 1]).min_by(|&i, &j| (lambda[i] - carried).abs().total_cmp(&(lambda[j] - carried).abs()))?;
let p: Vec<f64> = (0..4).filter(|&i| i != k).map(|i| lambda[i]).collect();
return Some([-lambda[k], p[0], p[1], p[2]]);
}
None
}
#[allow(clippy::needless_range_loop)]
fn eigenvalues(matrix: [[f64; 4]; 4]) -> Option<[(f64, f64); 4]> {
const N: usize = 4;
let sign = |a: f64, b: f64| if b >= 0.0 { a.abs() } else { -a.abs() };
let mut a = [[0.0; N + 1]; N + 1];
for i in 0..N {
a[i + 1][1..].copy_from_slice(&matrix[i]);
}
for k in 1..=N - 2 {
let norm = (k + 1..=N).map(|i| a[i][k] * a[i][k]).sum::<f64>().sqrt();
if norm == 0.0 {
continue;
}
let mut v = [0.0; N + 1];
v[k + 1] = a[k + 1][k] + sign(norm, a[k + 1][k]);
for i in k + 2..=N {
v[i] = a[i][k];
}
let vv: f64 = v.iter().map(|x| x * x).sum();
for j in 1..=N {
let s = 2.0 * (k + 1..=N).map(|i| v[i] * a[i][j]).sum::<f64>() / vv;
for i in k + 1..=N {
a[i][j] -= s * v[i];
}
}
for i in 1..=N {
let s = 2.0 * (k + 1..=N).map(|j| a[i][j] * v[j]).sum::<f64>() / vv;
for j in k + 1..=N {
a[i][j] -= s * v[j];
}
}
}
for i in 3..=N {
for j in 1..=i - 2 {
a[i][j] = 0.0;
}
}
let (mut wr, mut wi) = ([0.0; N + 1], [0.0; N + 1]);
let mut anorm = 0.0;
for i in 1..=N {
for j in (i - 1).max(1)..=N {
anorm += a[i][j].abs();
}
}
let mut nn = N;
let mut t = 0.0;
while nn >= 1 {
let mut its = 0;
loop {
let mut l = nn;
while l >= 2 {
let mut s = a[l - 1][l - 1].abs() + a[l][l].abs();
if s == 0.0 {
s = anorm;
}
if a[l][l - 1].abs() + s == s {
a[l][l - 1] = 0.0;
break;
}
l -= 1;
}
let mut x = a[nn][nn];
if l == nn {
wr[nn] = x + t;
wi[nn] = 0.0;
nn -= 1;
break;
}
let mut y = a[nn - 1][nn - 1];
let mut w = a[nn][nn - 1] * a[nn - 1][nn];
if l == nn - 1 {
let p = 0.5 * (y - x);
let q = p * p + w;
let z = q.abs().sqrt();
x += t;
if q >= 0.0 {
let z = p + sign(z, p);
wr[nn - 1] = x + z;
wr[nn] = if z != 0.0 { x - w / z } else { x + z };
wi[nn - 1] = 0.0;
wi[nn] = 0.0;
} else {
wr[nn - 1] = x + p;
wr[nn] = x + p;
wi[nn - 1] = -z;
wi[nn] = z;
}
nn -= 2;
break;
}
if its == 30 {
return None;
}
if its == 10 || its == 20 {
t += x;
for i in 1..=nn {
a[i][i] -= x;
}
let s = a[nn][nn - 1].abs() + a[nn - 1][nn - 2].abs();
x = 0.75 * s;
y = x;
w = -0.4375 * s * s;
}
its += 1;
let mut m = nn - 2;
let (mut p, mut q, mut r, mut z);
loop {
z = a[m][m];
r = x - z;
let s = y - z;
p = (r * s - w) / a[m + 1][m] + a[m][m + 1];
q = a[m + 1][m + 1] - z - r - s;
r = a[m + 2][m + 1];
let s = p.abs() + q.abs() + r.abs();
p /= s;
q /= s;
r /= s;
if m == l {
break;
}
let u = a[m][m - 1].abs() * (q.abs() + r.abs());
let v = p.abs() * (a[m - 1][m - 1].abs() + z.abs() + a[m + 1][m + 1].abs());
if u + v == v {
break;
}
m -= 1;
}
for i in m + 2..=nn {
a[i][i - 2] = 0.0;
if i != m + 2 {
a[i][i - 3] = 0.0;
}
}
for k in m..nn {
if k != m {
p = a[k][k - 1];
q = a[k + 1][k - 1];
r = if k != nn - 1 { a[k + 2][k - 1] } else { 0.0 };
x = p.abs() + q.abs() + r.abs();
if x != 0.0 {
p /= x;
q /= x;
r /= x;
}
}
let s = sign((p * p + q * q + r * r).sqrt(), p);
if s == 0.0 {
continue;
}
if k == m {
if l != m {
a[k][k - 1] = -a[k][k - 1];
}
} else {
a[k][k - 1] = -s * x;
}
p += s;
x = p / s;
y = q / s;
z = r / s;
q /= p;
r /= p;
for j in k..=nn {
p = a[k][j] + q * a[k + 1][j];
if k != nn - 1 {
p += r * a[k + 2][j];
a[k + 2][j] -= p * z;
}
a[k + 1][j] -= p * y;
a[k][j] -= p * x;
}
for i in l..=nn.min(k + 3) {
p = x * a[i][k] + y * a[i][k + 1];
if k != nn - 1 {
p += z * a[i][k + 2];
a[i][k + 2] -= p * r;
}
a[i][k + 1] -= p * q;
a[i][k] -= p;
}
}
}
}
Some(core::array::from_fn(|i| (wr[i + 1], wi[i + 1])))
}
pub fn regge_wheeler(metric: Metric, s: u32, l: u32, r: f64) -> f64 {
let (ell, spin) = (f64::from(l * (l + 1)), f64::from(s * s));
match metric {
Metric::GeneralRelativity => (1.0 - 2.0 / r) * (ell / (r * r) + 2.0 * (1.0 - spin) / r.powi(3)),
Metric::PolarizableVacuum => (-4.0 / r).exp() * (ell + (spin - 1.0) * ((1.0 - 1.0 / r).powi(2) - 1.0)) / (r * r),
}
}
pub fn charged_pv_dielectric(mass: f64, charge: f64, r: f64) -> f64 {
let a = G * mass / (C * C);
let b2 = G * charge * charge / (4.0 * PI * metrology::EPS_0 * C.powi(4));
let q = (a * a - b2) / (r * r);
let (even, odd) = if q > 0.0 {
(q.sqrt().cosh(), q.sqrt().sinh() / q.sqrt())
} else if q < 0.0 {
((-q).sqrt().cos(), (-q).sqrt().sin() / (-q).sqrt())
} else {
(1.0, 1.0)
};
(even + a / r * odd).powi(2)
}
pub fn horizon_temperature(kappa: f64) -> f64 {
HBAR * kappa / (2.0 * PI * K_B)
}
pub fn unruh_temperature(a: f64) -> f64 {
horizon_temperature(a / C)
}
pub fn hawking_temperature(m: f64) -> f64 {
horizon_temperature(C.powi(3) / (4.0 * G * m))
}
pub fn kerr_hawking_temperature(m: f64, chi: f64) -> f64 {
let s = (1.0 - chi * chi).sqrt();
horizon_temperature(C.powi(3) * s / (2.0 * G * m * (1.0 + s)))
}
pub fn gibbons_hawking_temperature(h: f64) -> f64 {
horizon_temperature(h)
}
pub fn hawking_lifetime(m: f64) -> f64 {
5120.0 * PI * G * G * m.powi(3) / (HBAR * C.powi(4))
}
pub fn zero_point_energy_density(omega_c: f64) -> f64 {
HBAR * omega_c.powi(4) / (8.0 * PI * PI * C.powi(3))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Metric {
GeneralRelativity,
PolarizableVacuum,
}
pub fn light_bending(mass: f64, b: f64, metric: Metric) -> f64 {
let x = G * mass / (C * C * b);
let second = match metric {
Metric::GeneralRelativity => 15.0 * PI / 4.0,
Metric::PolarizableVacuum => 4.0 * PI,
};
4.0 * x + second * x * x
}
pub fn pv_radiated_power_ratio(e: f64) -> f64 {
let e2 = e * e;
0.5 * (51.0 * e2 * e2 + 396.0 * e2 + 128.0) / (37.0 * e2 * e2 + 292.0 * e2 + 96.0)
}
pub fn exact_deflection(mass: f64, b: f64, metric: Metric) -> Option<f64> {
let b = b * C * C / (G * mass);
let (g, photon): (fn(f64) -> f64, f64) = match metric {
Metric::GeneralRelativity => (|u| u * u * (1.0 - 2.0 * u), 1.0 / 3.0),
Metric::PolarizableVacuum => (|u| u * u * (-4.0 * u).exp(), 0.5),
};
if b * b * g(photon) <= 1.0 {
return None;
}
let (mut lo, mut hi) = (0.0, photon);
while hi - lo > f64::EPSILON * hi {
let mid = 0.5 * (lo + hi);
if b * b * g(mid) < 1.0 { lo = mid } else { hi = mid }
}
let u0 = 0.5 * (lo + hi);
let q = |u: f64| match metric {
Metric::GeneralRelativity => (u + u0) - 2.0 * (u * u + u * u0 + u0 * u0),
Metric::PolarizableVacuum => {
let d = u - u0;
let slope = if d == 0.0 { 4.0 } else { (4.0 * d).exp_m1() / d };
(u + u0) - (4.0 * u0).exp() * slope / (b * b)
}
};
let (x, w) = geopotential::gauss_legendre(16);
let mut sum = 0.0;
let mut upper = 1.0;
for k in 1..=41 {
let lower = if k == 41 { 0.0 } else { 0.5f64.powi(k) };
let (mid, half) = (0.5 * (upper + lower), 0.5 * (upper - lower));
sum += half * x.iter().zip(&w).map(|(x, w)| w * 2.0 * u0.sqrt() / q(u0 * (1.0 - (mid + half * x).powi(2))).sqrt()).sum::<f64>();
upper = lower;
}
Some(2.0 * sum - PI)
}
pub fn multipole_deflection(field: &geopotential::Coefficients, point: [f64; 3], dir: [f64; 3], gamma: f64) -> [f64; 3] {
let dot = |a: [f64; 3], b: [f64; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
let n = dir.map(|x| x / dot(dir, dir).sqrt());
let along = dot(point, n);
let closest: [f64; 3] = core::array::from_fn(|i| point[i] - along * n[i]);
let b = dot(closest, closest).sqrt();
let (x, w) = geopotential::gauss_legendre(128);
let mut sum = [0.0; 3];
for (x, w) in x.iter().zip(&w) {
let theta = core::f64::consts::FRAC_PI_2 * x;
let s = b * theta.tan();
let p: [f64; 3] = core::array::from_fn(|i| closest[i] + s * n[i]);
let r = dot(p, p).sqrt();
let (lat, lon) = ((p[2] / r).asin(), p[1].atan2(p[0]));
let [north, east, up] = field.partials(r, lat, lon).acceleration_neu(r, lat);
let ((sf, cf), (sl, cl)) = (lat.sin_cos(), lon.sin_cos());
let g = [-sf * cl * north - sl * east + cf * cl * up, -sf * sl * north + cl * east + cf * sl * up, cf * north + sf * up];
let gn = dot(g, n);
let weight = core::f64::consts::FRAC_PI_2 * w * b / (theta.cos() * theta.cos());
for i in 0..3 {
sum[i] += weight * (g[i] - gn * n[i]);
}
}
sum.map(|x| (1.0 + gamma) * x / (C * C))
}
pub fn shadow_diameter(mass: f64, distance: f64) -> f64 {
6.0 * 3f64.sqrt() * G * mass / (C * C * distance)
}
pub fn shadow_deviation(diameter: f64, theta_g: f64) -> f64 {
diameter / (6.0 * 3f64.sqrt() * theta_g) - 1.0
}
pub fn kerr_pole_on_shadow(a: f64) -> f64 {
let r = polar_photon_orbit(a);
(8.0 * r * r * r / (r * r - 1.0)).sqrt()
}
fn polar_photon_orbit(a: f64) -> f64 {
let mut r = 3.0;
for _ in 0..100 {
let step = (r * r * r - 3.0 * r * r + a * a * r + a * a) / (3.0 * r * r - 6.0 * r + a * a);
r -= step;
if step.abs() <= 1e-16 * r {
break;
}
}
r
}
fn bardeen(a: f64, inclination: f64, r: f64) -> (f64, f64) {
let xi = (r * r * (3.0 - r) - a * a * (r + 1.0)) / (a * (r - 1.0));
let eta = r.powi(3) * (4.0 * a * a - r * (r - 3.0).powi(2)) / (a * a * (r - 1.0).powi(2));
let (s, c) = inclination.sin_cos();
(-xi / s, eta + a * a * c * c - xi * xi * c * c / (s * s))
}
pub fn kerr_shadow_edge(a: f64, inclination: f64, r: f64) -> Option<[f64; 2]> {
let (alpha, beta2) = bardeen(a, inclination, r);
(beta2 >= 0.0).then(|| [alpha, beta2.sqrt()])
}
pub fn kerr_shadow_size(a: f64, inclination: f64) -> (f64, f64) {
let a = a.abs();
if a == 0.0 {
return (27f64.sqrt(), 0.0);
}
if inclination.sin() == 0.0 {
return (kerr_pole_on_shadow(a), 0.0);
}
let polar = polar_photon_orbit(a);
let end = |sign: f64| {
let (mut inside, mut outside) = (polar, 2.0 * (1.0 + (2.0 / 3.0 * (sign * a).acos()).cos()));
for _ in 0..100 {
let mid = 0.5 * (inside + outside);
if bardeen(a, inclination, mid).1 > 0.0 { inside = mid } else { outside = mid }
}
inside
};
let (r1, r2) = (end(-1.0), end(1.0));
let n = 100_000;
let edge: Vec<[f64; 2]> = (0..=n)
.map(|k| {
let (alpha, beta2) = bardeen(a, inclination, r1 + (r2 - r1) * (1.0 - (PI * k as f64 / n as f64).cos()) / 2.0);
[alpha, if k == 0 || k == n { 0.0 } else { beta2.max(0.0).sqrt() }]
})
.collect();
let (lo, hi) = edge.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| (lo.min(p[0]), hi.max(p[0])));
let d = 0.5 * (lo + hi);
let radius = |p: [f64; 2]| (p[0] - d).hypot(p[1]);
let angle = |p: [f64; 2]| p[1].atan2(p[0]);
let sum: f64 = edge.windows(2).map(|w| 0.5 * (radius(w[0]) + radius(w[1])) * (angle(w[1]) - angle(w[0]))).sum();
(-sum / PI, d)
}
impl Metric {
pub fn g00(self, x: f64) -> f64 {
match self {
Metric::GeneralRelativity => ((1.0 - x / 2.0) / (1.0 + x / 2.0)).powi(2),
Metric::PolarizableVacuum => (-2.0 * x).exp(),
}
}
pub fn spatial(self, x: f64) -> f64 {
match self {
Metric::GeneralRelativity => (1.0 + x / 2.0).powi(4),
Metric::PolarizableVacuum => (2.0 * x).exp(),
}
}
pub fn critical_impact(self) -> f64 {
match self {
Metric::GeneralRelativity => 27f64.sqrt(),
Metric::PolarizableVacuum => 2.0 * core::f64::consts::E,
}
}
pub fn shadow_diameter(self, mass: f64, distance: f64) -> f64 {
shadow_diameter(mass, distance) * (self.critical_impact() / 27f64.sqrt())
}
}
pub fn strong_deflection(metric: Metric) -> (f64, f64) {
let e = core::f64::consts::E;
let (ym, c, c1, c2, a2) = match metric {
Metric::GeneralRelativity => (1.0 / 3.0, 9.0, 6.0, 2.0, -4.0 / 27.0),
Metric::PolarizableVacuum => (1.0 / e, 4.0 * e, 2.0 * e, e, -1.0 / (4.0 * e)),
};
let beta = c * (1.0 - ym).powi(2) * (c2 * ym - c * a2) / (2.0 * ym * ym * c1 * c1);
let r = (1.0 - ym) * c.sqrt();
let radicand = |z: f64| match metric {
Metric::GeneralRelativity => z * z * (1.0 - 2.0 * z / 3.0),
Metric::PolarizableVacuum => {
let s = (e - 1.0) * z;
let q = (1.0 + s) * s.ln_1p() - s;
q * (2.0 - q) / e
}
};
let (x, w) = geopotential::gauss_legendre(32);
let b_r = r * x.iter().zip(&w).map(|(x, w)| {
let z = 0.5 * (1.0 + x);
0.5 * w * (1.0 / radicand(z).sqrt() - 1.0 / (beta.sqrt() * z))
}).sum::<f64>();
let abar = r / (2.0 * beta.sqrt());
(abar, -PI + b_r + abar * (2.0 * beta / ym).ln())
}
pub fn isco(metric: Metric) -> (f64, f64, f64) {
match metric {
Metric::GeneralRelativity => {
let r = 6.0f64;
(r, r.powf(-1.5), (1.0 - 2.0 / r) / (1.0 - 3.0 / r).sqrt())
}
Metric::PolarizableVacuum => {
let r = 3.0 + 5f64.sqrt();
(r * (1.0 / r).exp(), (-2.0 / r).exp() / (r * (r - 1.0).sqrt()), (-1.0 / r).exp() * ((r - 1.0) / (r - 2.0)).sqrt())
}
}
}
pub fn periastron_ratio(metric: Metric, r: f64) -> f64 {
let c = match metric {
Metric::GeneralRelativity => 0.0,
Metric::PolarizableVacuum => 4.0,
};
r / (r * r - 6.0 * r + c).sqrt()
}
pub fn surface_redshift(metric: Metric, u: f64) -> f64 {
let g00 = match metric {
Metric::GeneralRelativity => 1.0 - 2.0 * u,
Metric::PolarizableVacuum => {
if u > (-1.0f64).exp() {
return f64::NAN;
}
let (mut lo, mut hi) = (0.0, 1.0);
while hi - lo > f64::EPSILON * hi {
let mid = 0.5 * (lo + hi);
if mid * (-mid).exp() < u { lo = mid } else { hi = mid }
}
metric.g00(0.5 * (lo + hi))
}
};
1.0 / redshift_factor(g00) - 1.0
}
pub fn shapiro_2pn_difference(mass: f64, b: f64) -> f64 {
let m = G * mass / (C * C);
PI / 4.0 * m * m / (b * C)
}
pub fn redshift_factor(g00: f64) -> f64 {
g00.sqrt()
}
pub fn clock_shift(excess: f64) -> f64 {
(0.5 * excess.ln_1p()).exp_m1()
}
pub fn effective_mass_factor(g00: f64, spatial: f64) -> f64 {
spatial / g00.sqrt()
}
pub fn engineered_mass(excess: f64, radius: f64, metric: Metric) -> f64 {
match metric {
Metric::GeneralRelativity => -excess * radius * C * C / (2.0 * G),
Metric::PolarizableVacuum => {
let y = excess.ln_1p() / 2.0;
-y * radius * y.exp() * C * C / G
}
}
}
pub fn levi_civita_delay(energy_density: f64, length: f64) -> f64 {
length / C * 4.0 * PI * G * energy_density * length * length / (3.0 * C.powi(4))
}
#[cfg(test)]
mod tests {
use super::*;
use metrology::PLANCK_LENGTH;
fn close(a: f64, b: f64, rel: f64) -> bool {
(a / b - 1.0).abs() < rel
}
#[test]
fn warp_bubbles_and_quantum_inequalities() {
assert!(close(alcubierre_energy(100.0, 100.0 * PLANCK_LENGTH, 1.0), -6.94e62, 5e-3));
assert!(close(alcubierre_energy(100.0, 1.0, 1.0) / 1.989e30, -0.564, 5e-3));
assert!(close(quantum_inequality(1e-9), -3.72e-26, 5e-3));
assert!(close(quantum_inequality(1e-15), -3.72e-2, 5e-3));
}
#[test]
fn casimir_closed_forms() {
assert!(close(casimir_pressure(1e-6), -1.300e-3, 1e-3));
let d = 1e-6;
let energy = |d: f64| casimir_energy_density(d) * d;
let h = 1e-12;
assert!(close(-(energy(d + h) - energy(d - h)) / (2.0 * h), casimir_pressure(d), 1e-6));
assert!(close(casimir_sphere_plate(1e-4, d), 2.0 * PI * 1e-4 * energy(d), 1e-12));
}
#[test]
fn wormholes() {
assert!(close(wormhole_throat_tension(10.0), 4.82e40, 5e-3));
assert!(close(wormhole_edge_tension(), -1.513e43, 1e-3));
}
#[test]
fn horizons() {
assert!(close(unruh_temperature(1.0), 4.055e-21, 1e-3));
assert!(close(1.0 / unruh_temperature(1.0), 2.466e20, 1e-3));
let sun = metrology::SOLAR_MASS;
assert!(close(hawking_temperature(sun), 6.17e-8, 2e-3));
assert!(close(hawking_lifetime(sun) / 3.155_76e7, 2.095e67, 3e-3));
assert!(close(gibbons_hawking_temperature(67.4e3 / 3.085_677_581e22), 2.655e-30, 2e-3));
let compton = metrology::M_E * C * C / HBAR;
assert!(close(zero_point_energy_density(compton), 1.801e22, 2e-3));
}
#[test]
fn schwinger_is_a_boltzmann_factor_at_twice_the_unruh_temperature() {
use crate::strong_field::{E_SCHWINGER, schwinger_exponent};
let (m_e, e) = (metrology::M_E, metrology::E_CHARGE);
for f in [1.0, 0.1, 1e-3] {
let field = f * E_SCHWINGER;
let t_u = unruh_temperature(e * field / m_e);
assert!(close(schwinger_exponent(field), m_e * C * C / (2.0 * K_B * t_u), 1e-12));
}
assert!(close(unruh_temperature(e * E_SCHWINGER / m_e), 9.438e8, 1e-3));
}
#[test]
fn polarizable_vacuum_against_general_relativity() {
let (sun, limb) = (metrology::SOLAR_MASS, 6.957e8);
let arcsec = 180.0 / PI * 3600.0;
let gr = light_bending(sun, limb, Metric::GeneralRelativity);
assert!((gr * arcsec - 1.7512).abs() < 1e-4);
let diff = light_bending(sun, limb, Metric::PolarizableVacuum) - gr;
assert!(close(diff * arcsec * 1e6, 0.730, 5e-3));
}
#[test]
fn forward_antigravity() {
assert!((G / (C * C) / 7.426e-28 - 1.0).abs() < 1e-3);
assert!((induced_acceleration(1e4, 1e4, 1.0) / 2.97046e-19 - 1.0).abs() < 1e-5);
assert!((9.8 / induced_acceleration(1.0, 1e6, 1.0) / 3.29915e21 - 1.0).abs() < 1e-5);
}
#[test]
fn the_shadow_of_sgr_a_star() {
let uas = PI / 180.0 / 3600.0 * 1e-6;
let pc = metrology::PARSEC;
let vlti = shadow_diameter(4.297e6 * metrology::SOLAR_MASS, 8277.0 * pc) / uas;
let keck = shadow_diameter(3.951e6 * metrology::SOLAR_MASS, 7935.0 * pc) / uas;
assert!((vlti / (6.0 * 3f64.sqrt() * 5.125) - 1.0).abs() < 1e-3 && (keck / (6.0 * 3f64.sqrt() * 4.92) - 1.0).abs() < 2e-3);
assert!((vlti - 53.3).abs() < 0.05 && (keck - 51.1).abs() < 0.1, "{vlti} {keck}");
assert!((shadow_deviation(48.9, 5.125) + 0.08).abs() < 0.005 && (shadow_deviation(48.9, 4.92) + 0.04).abs() < 0.005);
}
#[test]
fn a_kerr_shadow_seen_from_the_pole() {
assert!(close(kerr_pole_on_shadow(0.0), 27f64.sqrt(), 1e-15) && close(kerr_pole_on_shadow(1.0), 2.0 * (1.0 + 2f64.sqrt()), 1e-15));
for a in [0.1, 0.5, 0.94, -0.7] {
let cubic = |r: f64| r * r * r - 3.0 * r * r + a * a * r + a * a;
let (mut lo, mut hi) = (1.0 + 2f64.sqrt(), 3.0);
for _ in 0..200 {
let mid = 0.5 * (lo + hi);
if cubic(mid) > 0.0 { hi = mid } else { lo = mid }
}
let r = 0.5 * (lo + hi);
let eta = r.powi(3) * (4.0 * a * a - r * (r - 3.0).powi(2)) / (a * a * (r - 1.0).powi(2));
let shadow = kerr_pole_on_shadow(a);
assert!(close(shadow, (eta + a * a).sqrt(), 1e-13), "a = {a}");
assert!(shadow < 27f64.sqrt() && shadow > 2.0 * (1.0 + 2f64.sqrt()));
}
assert!((kerr_pole_on_shadow(1.0) / 27f64.sqrt() - 1.0 + 0.0708).abs() < 1e-3);
}
#[test]
fn a_kerr_shadow_at_any_inclination() {
let edge_on = core::f64::consts::FRAC_PI_2;
for a in [0.1, 0.5, 0.9, 0.998, -0.7] {
let [alpha, beta] = kerr_shadow_edge(a, edge_on, 3.0).unwrap();
assert!((alpha - 2.0 * a).abs() < 1e-14 && close(beta, 27f64.sqrt(), 1e-15), "a = {a}");
assert!([2.95, 2.999, 3.001, 3.05].iter().all(|&r| kerr_shadow_edge(a, edge_on, r).is_none_or(|p| p[1] < beta)));
}
let orbit = |sign: f64| 2.0 * (1.0 + (2.0 / 3.0 * (sign * 0.9f64).acos()).cos());
assert!((bardeen(0.9, edge_on, orbit(-1.0)).0 + 2.844_421_403).abs() < 1e-9);
assert!((bardeen(0.9, edge_on, orbit(1.0)).0 - 6.832_319_230).abs() < 1e-9);
assert!((kerr_pole_on_shadow(0.9) - 4.916_063_38).abs() < 1e-8);
for a in [0.3, 0.9, 0.999] {
assert_eq!(kerr_shadow_size(a, 0.0), (kerr_pole_on_shadow(a), 0.0));
let (radius, d) = kerr_shadow_size(a, 1e-5);
assert!((radius - kerr_pole_on_shadow(a)).abs() < 1e-9 && d.abs() < 1e-4, "a = {a}: {radius}");
}
for (a, degrees, radius, d) in [
(0.5, 90.0, 5.157_024_802_42, 1.020_944_533_00),
(0.9, 17.0, 4.924_470_288_51, 0.600_227_062_12),
(0.9, 60.0, 4.997_506_316_25, 1.746_750_502_48),
(0.9, 90.0, 5.027_489_401_78, 1.993_948_913_49),
(0.998, 90.0, 4.925_689_542_49, 2.442_889_238_42),
] {
let (r, dd) = kerr_shadow_size(a, f64::to_radians(degrees));
assert!((r - radius).abs() < 1e-9 && (dd - d).abs() < 1e-9, "a = {a}, i = {degrees}: {r} {dd}");
assert!(r > 2.0 * (1.0 + 2f64.sqrt()) && r < 27f64.sqrt() && kerr_shadow_size(-a, f64::to_radians(degrees)) == (r, dd));
}
assert_eq!(kerr_shadow_size(0.0, 1.0), (27f64.sqrt(), 0.0));
}
#[test]
fn a_spinning_black_hole_is_colder() {
let sun = metrology::SOLAR_MASS;
let cold = |chi: f64| kerr_hawking_temperature(sun, chi);
assert!(close(cold(0.0), hawking_temperature(sun), 1e-15));
assert!(close(cold(0.6) / cold(0.0), 8.0 / 9.0, 1e-15) && close(cold(0.8) / cold(0.0), 0.75, 1e-15) && cold(1.0) == 0.0);
assert!(close(cold(0.0), 6.170_260e-8, 1e-6) && close(cold(0.8), 4.627_695e-8, 1e-6));
let kappa = 2.0 * PI * K_B * cold(0.7) / HBAR * G * sun / C.powi(3);
let area = |m: f64| 8.0 * PI * (m * m + (m.powi(4) - 0.49).sqrt());
let h = 1e-6;
assert!(close(2.0 * h / (area(1.0 + h) - area(1.0 - h)), kappa / (8.0 * PI), 1e-9));
assert!((kappa / (8.0 * PI) - 0.008_288_352_7).abs() < 1e-10);
}
#[test]
fn exact_deflection_against_darwin_bozza_and_the_series() {
let m = metrology::SOLAR_MASS;
let rg = G * m / (C * C);
let at = |b: f64| exact_deflection(m, b * rg, Metric::GeneralRelativity).unwrap();
for r0 in [3.05f64, 3.5, 4.0, 6.0, 10.0, 100.0] {
let q = ((r0 - 2.0) * (r0 + 6.0)).sqrt();
let k2 = (q - r0 + 6.0) / (2.0 * q);
let s2 = (q + 2.0 - r0) / (q + 6.0 - r0);
let (big_k, _) = geopotential::special::complete_elliptic(k2);
let f = geopotential::special::elliptic_f(s2.sqrt().asin(), k2);
let darwin = 4.0 * (r0 / q).sqrt() * (big_k - f) - PI;
let b = (r0.powi(3) / (r0 - 2.0)).sqrt();
assert!((at(b) - darwin).abs() < 2e-13, "r₀ = {r0}: {} vs {darwin}", at(b));
}
let bbar = (216.0 * (7.0 - 4.0 * 3f64.sqrt())).ln() - PI;
assert!((bbar + 0.4002).abs() < 5e-5);
for (eps, gap) in [(1e-2, 2.5e-2), (1e-3, 3.1e-3), (1e-4, 3.8e-4)] {
let d = at(27f64.sqrt() * (1.0 + eps)) + eps.ln() - bbar;
assert!((d / gap - 1.0).abs() < 0.03, "{eps}: {d:e}");
}
let h = 1e-3;
let series = 4.0 * h + (-4.0 + 15.0 * PI / 4.0) * h * h + (122.0 / 3.0 - 7.5 * PI) * h.powi(3) + (-130.0 + 3465.0 * PI / 64.0) * h.powi(4)
+ (778.3 - 3465.0 * PI / 16.0) * h.powi(5);
assert!((at((1e9f64 / 998.0).sqrt()) - series).abs() < 1e-15, "{:e}", at((1e9f64 / 998.0).sqrt()) - series);
let b = 2e4;
let rest = at(b) - light_bending(m, b * rg, Metric::GeneralRelativity);
assert!((rest / (128.0 / 3.0 / b.powi(3)) - 1.0).abs() < 2e-3, "{rest:e}");
assert!(((rest / at(b)) / 2.67e-8 - 1.0).abs() < 2e-3);
assert!(exact_deflection(m, 27f64.sqrt() * rg * (1.0 - 1e-12), Metric::GeneralRelativity).is_none());
}
#[test]
fn exact_deflection_in_the_polarizable_vacuum() {
let m = metrology::SOLAR_MASS;
let rg = G * m / (C * C);
let b = 2e4;
let pv = exact_deflection(m, b * rg, Metric::PolarizableVacuum).unwrap();
assert!((pv / light_bending(m, b * rg, Metric::PolarizableVacuum) - 1.0).abs() < 1e-7);
let critical = 2.0 * core::f64::consts::E;
assert!(exact_deflection(m, critical * rg * (1.0 - 1e-9), Metric::PolarizableVacuum).is_none());
assert!(exact_deflection(m, critical * rg * (1.0 + 1e-6), Metric::PolarizableVacuum).unwrap() > 10.0);
let gr = exact_deflection(m, 10.0 * rg, Metric::GeneralRelativity).unwrap();
assert!(exact_deflection(m, 10.0 * rg, Metric::PolarizableVacuum).unwrap() > gr);
let at = |b: f64, metric| exact_deflection(m, b * rg, metric).unwrap();
for (b, gr, pv) in [(6.0, 1.719388310, 1.999746113), (10.0, 0.590395788, 0.610437437)] {
assert!((at(b, Metric::GeneralRelativity) - gr).abs() < 1e-9 && (at(b, Metric::PolarizableVacuum) - pv).abs() < 1e-9, "b = {b}");
}
assert!((at(27f64.sqrt() * 1.001, Metric::GeneralRelativity) - 6.5106448).abs() < 1e-7);
assert!((at(critical * 1.001, Metric::PolarizableVacuum) - 6.4454619).abs() < 1e-7);
let eps = 1e-6;
assert!((at(critical * (1.0 + eps), Metric::PolarizableVacuum) + eps.ln() + 0.46581756).abs() < 1e-5);
}
fn next_coefficient(f: impl Fn(f64) -> f64, known: &[f64], h: f64) -> f64 {
let rest = |x: f64| (f(x) - known.iter().rev().fold(0.0, |sum, c| sum * x + c)) / x.powi(known.len() as i32);
2.0 * rest(h / 2.0) - rest(h)
}
#[test]
fn exact_deflection_carries_both_weak_field_series() {
let m = metrology::SOLAR_MASS;
let rg = G * m / (C * C);
for (metric, [c2, c3, c4]) in [
(Metric::GeneralRelativity, [15.0 * PI / 4.0, 128.0 / 3.0, 3465.0 * PI / 64.0]),
(Metric::PolarizableVacuum, [4.0 * PI, 48.0, 64.0 * PI]),
] {
let alpha = |x: f64| exact_deflection(m, rg / x, metric).unwrap();
assert!((next_coefficient(alpha, &[0.0, 4.0], 1e-3) - c2).abs() < 2e-4, "{metric:?}");
assert!((next_coefficient(alpha, &[0.0, 4.0, c2], 1e-3) - c3).abs() < 1e-3, "{metric:?}");
assert!((next_coefficient(alpha, &[0.0, 4.0, c2, c3], 2e-3) - c4).abs() < 2e-2, "{metric:?}");
}
}
#[test]
fn the_metrics_in_isotropic_coordinates() {
let (gr, pv) = (Metric::GeneralRelativity, Metric::PolarizableVacuum);
for (metric, cubic, square) in [(gr, -1.5, 1.5), (pv, -4.0 / 3.0, 2.0)] {
assert!((next_coefficient(|x| metric.g00(x), &[1.0, -2.0, 2.0], 1e-3) - cubic).abs() < 1e-5, "{metric:?}");
assert!((next_coefficient(|x| metric.spatial(x), &[1.0, 2.0], 1e-3) - square).abs() < 1e-5, "{metric:?}");
}
for x in [0.01, 0.3, 1.0] {
assert!(close(gr.g00(x), 1.0 - 2.0 * x / (1.0 + x / 2.0).powi(2), 1e-14), "x = {x}");
assert!(close(pv.g00(x) * pv.spatial(x), 1.0, 1e-15) && close(pv.g00(-x), pv.spatial(x), 1e-15));
}
for metric in [gr, pv] {
let b = |r: f64| r * (metric.spatial(1.0 / r) / metric.g00(1.0 / r)).sqrt();
let (mut lo, mut hi) = (1.0, 10.0);
for _ in 0..100 {
let (a, c) = (hi - 0.618 * (hi - lo), lo + 0.618 * (hi - lo));
if b(a) < b(c) { hi = c } else { lo = a }
}
assert!(close(b(0.5 * (lo + hi)), metric.critical_impact(), 1e-14), "{metric:?}");
let rg = G * metrology::SOLAR_MASS / (C * C);
let at = |scale: f64| exact_deflection(metrology::SOLAR_MASS, scale * metric.critical_impact() * rg, metric);
assert!(at(1.0 - 1e-9).is_none() && at(1.0 + 1e-9).is_some(), "{metric:?}");
}
assert!(close(pv.critical_impact() / gr.critical_impact() - 1.0, 0.046_267, 1e-5));
let (mass, distance) = (4.297e6 * metrology::SOLAR_MASS, 8277.0 * metrology::PARSEC);
assert_eq!(gr.shadow_diameter(mass, distance), shadow_diameter(mass, distance));
}
#[test]
fn strong_deflection_both_ways() {
let (a, b) = strong_deflection(Metric::GeneralRelativity);
let exact = (216.0 * (7.0 - 4.0 * 3f64.sqrt())).ln() - PI;
assert!((a - 1.0).abs() < 1e-15 && (b - exact).abs() < 1e-14 && (b + 0.400_230_04).abs() < 5e-9, "{a} {b}");
let (a, b) = strong_deflection(Metric::PolarizableVacuum);
assert!((a - 1.0).abs() < 1e-15 && (b + 0.465_817_56).abs() < 5e-9, "{a} {b}");
let m = metrology::SOLAR_MASS;
let rg = G * m / (C * C);
let eps = 1e-6;
for (metric, alpha) in [(Metric::GeneralRelativity, 13.415_285_557_8), (Metric::PolarizableVacuum, 13.349_698_822_8)] {
let at = exact_deflection(m, metric.critical_impact() * (1.0 + eps) * rg, metric).unwrap();
assert!((at - alpha).abs() < 5e-11, "{metric:?}");
assert!((at + eps.ln() - strong_deflection(metric).1).abs() < 6e-6, "{metric:?}");
}
}
#[test]
fn the_innermost_stable_orbit_both_ways() {
let (r, omega, energy) = isco(Metric::GeneralRelativity);
assert!(r == 6.0 && close(omega, 6f64.powf(-1.5), 1e-15) && close(energy, (8.0f64 / 9.0).sqrt(), 1e-15));
assert!((omega - 0.068_041_4).abs() < 5e-8 && (energy - 0.942_809_0).abs() < 5e-8);
let (r, omega, energy) = isco(Metric::PolarizableVacuum);
assert!((r - 6.337_940).abs() < 5e-7 && (omega - 0.063_332_6).abs() < 5e-8 && (energy - 0.945_213_2).abs() < 5e-8);
let iso = 3.0 + 5f64.sqrt();
let pv = Metric::PolarizableVacuum;
let l = iso * iso * pv.spatial(1.0 / iso) * omega * energy / pv.g00(1.0 / iso);
assert!((l - 3.523_216).abs() < 5e-7, "{l}");
let gr = isco(Metric::GeneralRelativity);
assert!((omega / gr.1 - 1.0 + 0.069_20).abs() < 5e-6);
assert!(((1.0 - energy) * 100.0 - 5.479).abs() < 5e-4 && ((1.0 - gr.2) * 100.0 - 5.719).abs() < 5e-4);
let e = |r: f64| (-1.0 / r).exp() * ((r - 1.0) / (r - 2.0)).sqrt();
assert!(close(e(iso), energy, 1e-15) && e(iso - 1e-3) > energy && e(iso + 1e-3) > energy);
assert!(periastron_ratio(pv, iso * (1.0 + 1e-12)) > 1e5 && periastron_ratio(Metric::GeneralRelativity, 6.0 * (1.0 + 1e-12)) > 1e5);
}
#[test]
fn periastron_ratio_both_ways() {
for (metric, r, k) in [
(Metric::GeneralRelativity, 7.0, 2.645_75),
(Metric::GeneralRelativity, 10.0, 1.581_14),
(Metric::GeneralRelativity, 20.0, 1.195_23),
(Metric::PolarizableVacuum, 6.0, 3.0),
(Metric::PolarizableVacuum, 9.0, 1.616_45),
(Metric::PolarizableVacuum, 19.0, 1.199_27),
] {
assert!((periastron_ratio(metric, r) - k).abs() < 5e-6, "{metric:?} r = {r}");
}
for (metric, c2, c3) in [(Metric::GeneralRelativity, 13.5, 67.5), (Metric::PolarizableVacuum, 14.5, 76.5)] {
let point = |r: f64| {
let omega = match metric {
Metric::GeneralRelativity => r.powf(-1.5),
Metric::PolarizableVacuum => (-2.0 / r).exp() / (r * (r - 1.0).sqrt()),
};
let x = omega.powf(2.0 / 3.0);
(x, (periastron_ratio(metric, r) - 1.0 - 3.0 * x - c2 * x * x) / x.powi(3))
};
let ((x1, d1), (x2, d2)) = (point(1e3), point(2e3));
assert!(((d2 * x1 - d1 * x2) / (x1 - x2) - c3).abs() < 5e-3, "{metric:?}");
}
}
#[test]
fn surface_redshift_both_ways() {
let (gr, pv) = (Metric::GeneralRelativity, Metric::PolarizableVacuum);
assert!((surface_redshift(gr, 0.172) - 0.234_662_0).abs() < 5e-8 && (surface_redshift(pv, 0.172) - 0.237_117_7).abs() < 5e-8);
let u = 1.0 / 1400.0;
assert!(close(surface_redshift(pv, u) - surface_redshift(gr, u), 6.096e-11, 1e-4) && close(u.powi(3) / 6.0, 6.074e-11, 1e-3));
assert!(close(surface_redshift(pv, (-1.0f64).exp()), core::f64::consts::E - 1.0, 1e-7) && surface_redshift(pv, 0.4).is_nan());
}
#[test]
fn the_second_order_shapiro_difference() {
let (sun, limb) = (metrology::SOLAR_MASS, 6.957e8);
assert!((shapiro_2pn_difference(sun, limb) * 1e12 - 8.211).abs() < 5e-4);
assert!((shapiro_2pn_difference(sun, 1.6 * limb) * 1e12 - 5.132).abs() < 5e-4);
}
#[test]
fn puthoffs_table_one() {
assert_eq!(10.0 * redshift_factor(0.25), 5.0);
let excess = 2.0 * 9.81 * 22.5 / (C * C);
assert!(((redshift_factor(1.0 + excess) - 1.0) / 2.456e-15 - 1.0).abs() < 6e-3);
assert!(close(clock_shift(excess), excess / 2.0, 1e-14) && (clock_shift(excess) / 2.456e-15 - 1.0).abs() < 2e-4);
assert!(close(clock_shift(-0.75), redshift_factor(0.25) - 1.0, 1e-15));
let u = metrology::GM_SUN / (C * C * 6.957e8);
assert!(close(redshift_factor(1.0 - 2.0 * u) - 1.0, -2.1225e-6, 5e-5));
assert_eq!(effective_mass_factor(0.25, 4.0), 8.0);
assert!((effective_mass_factor(0.7, 1.6) - 1.912_366).abs() < 5e-7);
let lagrangian = |v: f64| -(0.7 - 1.6 * v * v).sqrt();
let h = 1e-4;
assert!(close((lagrangian(h) - 2.0 * lagrangian(0.0) + lagrangian(-h)) / (h * h), effective_mass_factor(0.7, 1.6), 1e-7));
for (metric, square) in [(Metric::GeneralRelativity, 4.0), (Metric::PolarizableVacuum, 4.5)] {
let mass = |x: f64| effective_mass_factor(metric.g00(x), metric.spatial(x));
assert!((next_coefficient(mass, &[1.0, 3.0], 1e-3) - square).abs() < 1e-5, "{metric:?}");
}
}
#[test]
fn negative_mass_for_an_engineered_metric() {
let (gr, pv) = (Metric::GeneralRelativity, Metric::PolarizableVacuum);
assert!(close(engineered_mass(1e-18, 10.0, gr), -6.733e9, 1e-4) && close(engineered_mass(1e-18, 10.0, pv), engineered_mass(1e-18, 10.0, gr), 1e-15));
let excess = (2.897_771_955e-3 / 300.0 / 550e-9f64).powi(2) - 1.0;
assert!(close(engineered_mass(excess, 10.0, gr), -2.070e30, 1e-4) && close(engineered_mass(excess, 10.0, pv), -6.777e29, 1e-4));
let target = -G * engineered_mass(excess, 10.0, pv) / (C * C * 10.0);
let (mut lo, mut hi) = (0.0f64, 10.0);
for _ in 0..200 {
let mid = 0.5 * (lo + hi);
if mid * mid.exp() < target { lo = mid } else { hi = mid }
}
assert!(close(pv.g00(-0.5 * (lo + hi)), 1.0 + excess, 1e-13));
}
#[test]
fn the_levi_civita_effect() {
let mu0 = metrology::MU_0;
let tau = levi_civita_delay(1.0 / (2.0 * mu0), 1.0);
assert!(close(tau, 4.594e-47, 1.1e-4) && close(3.0 * C * tau / 4.0, 1.033e-38, 5e-4));
assert!(close(3.0 * C * levi_civita_delay(1e18 / (2.0 * mu0), 1e4) / 4e4, 1.033e-12, 5e-4));
let a = |c: f64| c * c / (4.0 * PI * G / mu0).sqrt();
assert!((a(C) / 3.4789e18 - 1.0).abs() < 1.5e-5 && (a(3e8) / 3.4837e18 - 1.0).abs() < 1.5e-5);
assert!(close(3.0 * C * tau / 4.0, 1.0 / (8.0 * a(C) * a(C)), 1e-13));
}
#[test]
fn jupiters_oblateness_in_the_deflection() {
let (gm, radius, j2) = (DUR2020_GM, DUR2020_RADIUS, -DUR2020_C[0]);
let field = jupiter(2);
let mas = PI / 180.0 / 3600.0 * 1e-3;
for (b, lon) in [(radius, 0.0), (2.0 * radius, 0.7), (radius * 1.1, -2.0)] {
let point = [b * f64::cos(lon), b * f64::sin(lon), 0.0];
let dir = [-f64::sin(lon), f64::cos(lon), 0.0];
let alpha = multipole_deflection(&field, point, dir, 1.0);
let want = 4.0 * gm / (C * C * b) * (1.0 + j2 * radius * radius / (b * b));
let size = (alpha[0] * alpha[0] + alpha[1] * alpha[1] + alpha[2] * alpha[2]).sqrt();
assert!(close(size, want, 1e-13) && alpha[2].abs() < 1e-15 * size, "b = {b}");
assert!(close(-(alpha[0] * point[0] + alpha[1] * point[1]) / (b * size), 1.0, 1e-13));
}
let limb = 4.0 * gm / (C * C * radius);
assert!((limb / mas - 16.27).abs() < 0.005 && (limb * j2 / mas * 1e3 - 239.0).abs() < 0.5);
let mut point = geopotential::Coefficients::zeros(gm, radius, 0);
point.set(0, 0, 1.0, 0.0);
let alpha = multipole_deflection(&point, [1e8, 2e8, -0.5e8], [0.3, -0.2, 0.9], 1.0);
let n = [0.3, -0.2, 0.9].map(|x: f64| x / 0.94f64.sqrt());
let p = [1e8, 2e8, -0.5e8];
let along = p[0] * n[0] + p[1] * n[1] + p[2] * n[2];
let b = (0..3).map(|i| (p[i] - along * n[i]).powi(2)).sum::<f64>().sqrt();
let size = (alpha[0] * alpha[0] + alpha[1] * alpha[1] + alpha[2] * alpha[2]).sqrt();
assert!(close(size, 4.0 * gm / (C * C * b), 1e-13));
let half = multipole_deflection(&point, [1e8, 2e8, -0.5e8], [0.3, -0.2, 0.9], 0.0);
assert!(close(half[0], alpha[0] / 2.0, 1e-14));
}
const DUR2020_GM: f64 = 1.266_865_341e17;
const DUR2020_RADIUS: f64 = 7.1492e7;
const DUR2020_C: [f64; 11] = [-1.469_657_35e-2, 4.5e-8, 5.866_085e-4, 7.23e-8, -3.420_07e-5, -1.2e-7, 2.422e-6, 1.13e-7, -1.81e-7, -1.6e-8, -6.2e-8];
fn jupiter(nmax: usize) -> geopotential::Coefficients {
let mut field = geopotential::Coefficients::zeros(DUR2020_GM, DUR2020_RADIUS, nmax);
field.set(0, 0, 1.0, 0.0);
for n in 2..=nmax {
field.set(n, 0, DUR2020_C[n - 2] / ((2 * n + 1) as f64).sqrt(), 0.0);
}
field
}
#[test]
fn jupiters_quadrupole_at_any_orientation() {
let field = jupiter(2);
let (gm, radius, j2) = (DUR2020_GM, DUR2020_RADIUS, -DUR2020_C[0]);
let uas = PI / 180.0 / 3600.0 * 1e-6;
let dot = |a: [f64; 3], b: [f64; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
let (s30, c30, s45) = (0.5, 0.75f64.sqrt(), 0.5f64.sqrt());
for (point, dir, size, quad) in [
([0.0, 0.0, radius], [0.0, 1.0, 0.0], 16_028.272_6, 239.074_26),
([radius, 0.0, 0.0], [0.0, 0.0, 1.0], 16_267.346_9, 0.0),
([radius * s45, 0.0, radius * s45], [0.0, c30, s30], 17_423.170_1, 219.069_54),
] {
let along = dot(point, dir);
let closest: [f64; 3] = core::array::from_fn(|i| point[i] - along * dir[i]);
let b = dot(closest, closest).sqrt();
let n = closest.map(|x| -x / b);
let m = [dir[1] * n[2] - dir[2] * n[1], dir[2] * n[0] - dir[0] * n[2], dir[0] * n[1] - dir[1] * n[0]];
let mono = 4.0 * gm / (C * C * b);
let q = j2 * (radius / b).powi(2);
let phi1 = mono * (1.0 + q * (1.0 - 2.0 * n[2] * n[2] - dir[2] * dir[2]));
let phi2 = 2.0 * mono * q * m[2] * n[2];
let alpha = multipole_deflection(&field, point, dir, 1.0);
let crosta: [f64; 3] = core::array::from_fn(|i| phi1 * n[i] + phi2 * m[i]);
let gap: [f64; 3] = core::array::from_fn(|i| alpha[i] - crosta[i]);
assert!(dot(gap, gap).sqrt() < 1e-11 * mono * q, "{dir:?}");
assert!((dot(alpha, alpha).sqrt() / uas - size).abs() < 1e-4, "{dir:?}");
let rest: [f64; 3] = core::array::from_fn(|i| alpha[i] - mono * n[i]);
assert!((dot(rest, rest).sqrt() / uas - quad).abs() < 1e-5, "{dir:?}");
}
let limb = 4.0 * gm / (C * C * radius);
let over = multipole_deflection(&field, [0.0, 0.0, radius], [0.0, 1.0, 0.0], 1.0);
assert!(close(-over[2], limb * (1.0 - j2), 1e-12) && over[0].abs() + over[1].abs() < 1e-15 * limb);
let axial = multipole_deflection(&field, [radius, 0.0, 0.0], [0.0, 0.0, 1.0], 1.0);
assert!(close(-axial[0], limb, 1e-12) && axial[1].abs() + axial[2].abs() < 1e-15 * limb);
}
#[test]
fn jupiters_full_zonal_field_on_an_equatorial_ray() {
let field = jupiter(12);
let uas = PI / 180.0 / 3600.0 * 1e-6;
let j = |n: usize| -DUR2020_C[n - 2];
for (scale, plane, north) in [(1.0, 16_516.561_365, 0.004_495), (2.0, 8_163.860_339, -0.000_018)] {
let b = scale * DUR2020_RADIUS;
let x = DUR2020_RADIUS / b;
let mono = 4.0 * DUR2020_GM / (C * C * b);
let even = 1.0 + (1..=6).map(|k| if k % 2 == 1 { 1.0 } else { -1.0 } * j(2 * k) * x.powi(2 * k as i32)).sum::<f64>();
let odd = (1..=5).map(|k| if k % 2 == 1 { 1.0 } else { -1.0 } * j(2 * k + 1) * x.powi(2 * k as i32 + 1)).sum::<f64>();
let alpha = multipole_deflection(&field, [b, 0.0, 0.0], [0.0, 1.0, 0.0], 1.0);
assert!(close(-alpha[0], mono * even, 1e-12) && (alpha[2] - mono * odd).abs() < 1e-12 * mono, "b = {scale}R");
assert!((-alpha[0] / uas - plane).abs() < 1e-6 && (alpha[2] / uas - north).abs() < 1e-6, "b = {scale}R");
}
assert!((DUR2020_GM / (C * C * DUR2020_RADIUS) * 4.0 * j(4) / uas + 9.5426).abs() < 1e-4);
}
}
#[cfg(test)]
mod warp_tests {
use super::*;
use metrology::{EINSTEIN_KAPPA, HBAR, SOLAR_MASS, YEAR};
fn close(a: f64, b: f64, rel: f64) -> bool {
(a / b - 1.0).abs() < rel
}
const GEOMETRIC: f64 = EINSTEIN_KAPPA / (8.0 * PI);
fn alcubierre_shape(r: f64, radius: f64, sigma: f64) -> f64 {
((sigma * (r + radius)).tanh() - (sigma * (r - radius)).tanh()) / (2.0 * (sigma * radius).tanh())
}
fn alcubierre_shift(v: f64, radius: f64, sigma: f64, x: [f64; 4]) -> [f64; 3] {
let r = ((x[1] - v * x[0]).powi(2) + x[2] * x[2] + x[3] * x[3]).sqrt();
[v * alcubierre_shape(r, radius, sigma), 0.0, 0.0]
}
fn natario_shift(v: f64, radius: f64, sigma: f64, x: [f64; 4]) -> [f64; 3] {
let [_, x, y, z] = x;
let (r, rho) = ((x * x + y * y + z * z).sqrt(), y.hypot(z));
let (f, slope) = (0.5 * (1.0 - alcubierre_shape(r, radius, sigma)), -0.5 * alcubierre_slope(r, radius, sigma));
let (c, s) = (x / r, rho / r);
let (radial, polar) = ([x / r, y / r, z / r], [-s, c * y / rho, c * z / rho]);
core::array::from_fn(|i| -2.0 * v * f * c * radial[i] + v * (2.0 * f + r * slope) * s * polar[i])
}
fn shift_metric(v: [f64; 3]) -> [[f64; 4]; 4] {
core::array::from_fn(|a| {
core::array::from_fn(|b| match (a, b) {
(0, 0) => -1.0 + v[0] * v[0] + v[1] * v[1] + v[2] * v[2],
(0, i) | (i, 0) => -v[i - 1],
(i, j) => f64::from(u8::from(i == j)),
})
})
}
fn shift_derivatives(shift: &impl Fn([f64; 4]) -> [f64; 3], x: [f64; 4], h: f64) -> ([[f64; 3]; 3], f64) {
let jacobian = |y: [f64; 4]| -> [[f64; 3]; 3] { core::array::from_fn(|i| core::array::from_fn(|j| central([-2.0, -1.0, 1.0, 2.0].map(|k| shift(nudge(y, j + 1, k * h))[i]), h))) };
let divergence = |y: [f64; 4]| {
let j = jacobian(y);
j[0][0] + j[1][1] + j[2][2]
};
let v = shift(x);
let lie = (0..4).map(|a| if a == 0 { 1.0 } else { v[a - 1] } * central([-2.0, -1.0, 1.0, 2.0].map(|k| divergence(nudge(x, a, k * h))), h)).sum();
(jacobian(x), lie)
}
fn conditions(metric: impl Fn([f64; 4]) -> [[f64; 4]; 4], x: [f64; 4], h: f64) -> EnergyConditions {
let c = energy_conditions(metric(x), stress_energy(&metric, x, h));
EnergyConditions {
eulerian: c.eulerian.map(|row| row.map(|t| t * GEOMETRIC)),
null: c.null * GEOMETRIC,
type_one: c.type_one.map(|t| t.map(|t| t * GEOMETRIC)),
}
}
#[test]
fn alcubierre_density_and_the_capacitance_floor() {
let side = alcubierre_energy_density(1.0, 8.0, 1.0, [0.0, 1.0, 0.0]) * GEOMETRIC;
assert!(close(side, -1.0 / (2.0 * PI), 4.6e-7) && !close(side, -1.0 / (2.0 * PI), 4.4e-7));
assert!(close(side, -16f64.tanh().powi(4) / (2.0 * PI * 8f64.tanh().powi(2)), 1e-14));
assert!(close(alcubierre_energy_density(300.0, 0.015, 0.1, [0.0, 300.0, 0.0]), -6.7751e35, 1e-4));
assert!(alcubierre_energy_density(1.0, 8.0, 1.0, [0.0; 3]) == 0.0 && alcubierre_energy_density(1.0, 8.0, 1.0, [1.0, 0.0, 0.0]) == 0.0);
assert!(close(alcubierre_minimum_energy(100.0, 1.0), -1.122_16e28, 1e-5));
assert!(close(alcubierre_minimum_energy(100.0, 1.0) / -1.898_13e27, 5.912, 1e-3));
}
#[test]
fn alcubierre_total_energy_of_the_tanh_wall() {
let (x, w) = geopotential::gauss_legendre(32);
let radial: f64 = (0..300)
.map(|k| {
x.iter()
.zip(&w)
.map(|(x, w)| {
let r = 0.02 * (k as f64 + 0.5 + 0.5 * x);
0.01 * w * r * r * alcubierre_energy_density(1.0, 8.0, 1.0, [0.0, r, 0.0])
})
.sum::<f64>()
})
.sum();
let total = alcubierre_tanh_energy(1.0, 8.0, 1.0);
assert!(close(8.0 * PI / 3.0 * radial / (C * C), total, 1e-12));
assert!(close(total / alcubierre_minimum_energy(1.0, 1.0), 2.680_104, 1e-6));
let thin = alcubierre_tanh_energy(100.0, 2.0, 1.0);
assert!(close(thin / (-1e4 * 2.0 / 36.0 * C * C / G), 1.000_008_06, 1e-8));
assert!(close(thin / -SOLAR_MASS, 0.3762, 1e-4));
let delta = (1.0 + 200f64.tanh().powi(2)).powi(2) / (2.0 * 2.0 * 200f64.tanh());
assert!((thin / alcubierre_energy(100.0, delta, 1.0) - 2.0 / 3.0).abs() < 1e-6);
assert!(close(-1e4 * 20.0 * C * C / G / alcubierre_tanh_energy(100.0, 20.0, 1.0), 36.0, 1e-6));
let (best, at) = (1000..=1500).map(|k| (alcubierre_tanh_energy(1.0, k as f64 / 1000.0, 1.0), k as f64 / 1000.0)).fold((f64::NEG_INFINITY, 0.0), |a, b| if b.0 > a.0 { b } else { a });
assert!(close(best / alcubierre_minimum_energy(1.0, 1.0), 0.667_477, 1e-6) && (at - 1.229).abs() < 1.5e-3);
}
#[test]
fn shift_drives_at_alcubierre_and_natario_walls() {
let shift = |x: [f64; 4]| alcubierre_shift(1.0, 1.0, 8.0, x);
let wall = [0.0, 0.0, 1.0, 0.0];
let (jacobian, lie) = shift_derivatives(&shift, wall, 1e-3);
let (f, slope) = (alcubierre_shape(1.0, 1.0, 8.0), alcubierre_slope(1.0, 1.0, 8.0));
assert!((jacobian[0][1] - slope).abs() < 1e-8 && (lie - (f - 1.0) * slope).abs() < 1e-7);
assert!(jacobian.iter().flatten().filter(|j| j.abs() > 1e-9).count() == 1);
let rho = shift_drive_density(jacobian) * GEOMETRIC;
let null = shift_drive_null_sum(jacobian, lie) * GEOMETRIC;
assert!((rho - alcubierre_energy_density(1.0, 8.0, 1.0, [0.0, 1.0, 0.0]) * GEOMETRIC).abs() < 1e-8);
assert!(close(null, -7.0 / (6.0 * PI), 1e-6) && (null + 0.37136).abs() < 5e-6);
assert!((shift_drive_null_sum(jacobian.map(|r| r.map(|j| -j)), -lie) * GEOMETRIC + 0.26526).abs() < 5e-6);
let shift = |x: [f64; 4]| natario_shift(1.0, 1.0, 4.0, x);
let point = [0.0, 0.6, 0.85, 0.0];
let (jacobian, _) = shift_derivatives(&shift, point, 1e-3);
let r = 0.6f64.hypot(0.85);
let slope = -0.5 * alcubierre_slope(r, 1.0, 4.0);
let curve = -0.5 * (alcubierre_slope(r + 1e-4, 1.0, 4.0) - alcubierre_slope(r - 1e-4, 1.0, 4.0)) / 2e-4;
let cos2 = 0.36 / (r * r);
let natario = -1.0 / (8.0 * PI) * (3.0 * slope * slope * cos2 + (slope + r * curve / 2.0).powi(2) * (1.0 - cos2));
let rho = shift_drive_density(jacobian) * GEOMETRIC;
assert!(close(rho, natario, 1e-7) && (rho + 0.040_527_2).abs() < 1e-7);
assert!((jacobian[0][0] + jacobian[1][1] + jacobian[2][2]).abs() < 1e-9);
}
#[test]
fn wormhole_shells_and_quantum_inequality_bounds() {
let (sigma, p) = thin_shell_wormhole(1.0, 0.0);
assert!(close(sigma, -1.926_18e43, 1e-5) && close(4.0 * PI * sigma / (C * C), -2.693_18e27, 1e-5));
assert!(close(p, 9.630_91e42, 1e-5) && sigma + p < 0.0);
assert!(close(4.0 * PI * thin_shell_wormhole(1.0, 0.1 * C * C / G).0 / (C * C), -2.408_85e27, 1e-5));
let mass = C * C / G;
let h = 1e-4;
let slope = (thin_shell_wormhole(4.0 + h, mass).0 - thin_shell_wormhole(4.0 - h, mass).0) / (2.0 * h);
let (sigma, p) = thin_shell_wormhole(4.0, mass);
assert!(close(slope, -0.5 * (sigma + p), 1e-8));
let planck = (HBAR * G / C.powi(3)).sqrt();
assert!(close(wormhole_throat_bound(0.01), 7.8971e-32, 1e-4) && close(wormhole_throat_bound(0.01), (0.75 / PI).sqrt() * planck / 1e-4, 1e-13));
let band = |r0: f64| (3.0 * r0 * planck * planck / (8.0 * PI * 1e-8)).cbrt();
assert!(close(wormhole_band_bound(1.0, 0.01), 1.460_94e-21, 1e-5) && close(wormhole_band_bound(1.0, 0.01), band(1.0), 1e-13));
assert!(close(wormhole_band_bound(C * YEAR, 0.01), 3.0899e-16, 1e-4) && close(wormhole_band_bound(1e5 * C * YEAR, 0.01), 1.4342e-14, 1e-4));
assert!(close(quantum_inequality_gaussian(1e-9), -1.858_90e-26, 1e-5));
let (x, w) = geopotential::gauss_legendre(64);
let gauss = (0..24)
.map(|k| {
x.iter()
.zip(&w)
.map(|(x, w)| {
let t = -12.0 + k as f64 + 0.5 + 0.5 * x;
0.5 * w * ((t * t - 1.0) * (-t * t / 2.0).exp()).powi(2)
})
.sum::<f64>()
})
.sum::<f64>()
/ PI.sqrt();
let lorentz = x
.iter()
.zip(&w)
.map(|(x, w)| {
let (s, c) = (PI / 2.0 * x).sin_cos();
PI / 2.0 * w * (2.0 * s * s - c * c).powi(2) * c.powi(4)
})
.sum::<f64>()
/ PI;
let ford_roman = 3.0 / (32.0 * PI * PI);
assert!(close(gauss / (16.0 * PI * PI) / ford_roman, quantum_inequality_gaussian(1.0) / quantum_inequality(1.0), 1e-13));
assert!(close(lorentz / (16.0 * PI * PI) / ford_roman, 9.0 / 64.0, 1e-13));
}
#[test]
fn eigenvalues_of_general_four_by_four_matrices() {
let u = [0.5, -0.5, 0.1, 0.7];
let q: [[f64; 4]; 4] = core::array::from_fn(|i| core::array::from_fn(|j| if i == j { 1.0 } else { 0.0 } - 2.0 * u[i] * u[j]));
let b = [[2.0, -3.0, 0.0, 0.0], [3.0, 2.0, 0.0, 0.0], [0.0, 0.0, 1.0, 5.0], [0.0, 0.0, 0.0, -4.0]];
let m: [[f64; 4]; 4] = core::array::from_fn(|i| core::array::from_fn(|j| (0..16).map(|k| q[i][k / 4] * b[k / 4][k % 4] * q[j][k % 4]).sum()));
let mut values = eigenvalues(m).unwrap();
values.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.total_cmp(&b.1)));
for (got, want) in values.iter().zip([(-4.0, 0.0), (1.0, 0.0), (2.0, -3.0), (2.0, 3.0)]) {
assert!((got.0 - want.0).abs() < 1e-13 && (got.1 - want.1).abs() < 1e-13, "{values:?}");
}
let mut values = eigenvalues([[3.0, 1.0, 4.0, 1.0], [0.0, -5.0, 9.0, 2.0], [0.0, 0.0, 6.0, 5.0], [0.0, 0.0, 0.0, 0.5]]).unwrap().map(|v| v.0);
values.sort_by(f64::total_cmp);
assert_eq!(values, [-5.0, 0.5, 3.0, 6.0]);
assert_eq!(eigenvalues([[0.0; 4]; 4]).unwrap(), [(0.0, 0.0); 4]);
}
#[test]
fn hawking_ellis_types_of_known_stress_energy() {
let n = [1.0 / 3.0, 2.0 / 3.0, 2.0 / 3.0];
let (beta, gamma) = (0.5, 1.0 / 0.75f64.sqrt());
let boost: [[f64; 4]; 4] = core::array::from_fn(|a| {
core::array::from_fn(|b| match (a, b) {
(0, 0) => gamma,
(0, i) | (i, 0) => gamma * beta * n[i - 1],
(i, j) => f64::from(u8::from(i == j)) + (gamma - 1.0) * n[i - 1] * n[j - 1],
})
});
let rest = [1.0, -3.0, 0.5, 2.0];
let t: [[f64; 4]; 4] = core::array::from_fn(|a| core::array::from_fn(|b| (0..4).map(|c| boost[c][a] * rest[c] * boost[c][b]).sum()));
let found = hawking_ellis_type_one(t).unwrap();
assert!(found.iter().zip(rest).all(|(a, b)| (a - b).abs() < 1e-12), "{found:?}");
assert!(null_minimum(t) < 0.0);
assert_eq!(hawking_ellis_type_one([[2.0, 0.0, 0.0, 0.0], [0.0, -2.0, 0.0, 0.0], [0.0, 0.0, -2.0, 0.0], [0.0, 0.0, 0.0, -2.0]]), Some([2.0, -2.0, -2.0, -2.0]));
let k = [-1.0, 0.6, 0.8, 0.0];
let dust: [[f64; 4]; 4] = core::array::from_fn(|a| core::array::from_fn(|b| k[a] * k[b]));
assert!(hawking_ellis_type_one(dust).is_none() && null_minimum(dust).abs() < 1e-15);
assert!(hawking_ellis_type_one([[0.0, 1.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]]).is_none());
let t = [[0.3, -0.4, 0.2, 0.1], [-0.4, 1.0, 0.3, -0.2], [0.2, 0.3, -0.5, 0.6], [0.1, -0.2, 0.6, 0.2]];
let sampled = (0..400 * 400)
.map(|k| {
let (z, phi) = (-1.0 + (k / 400) as f64 / 199.5, PI * (k % 400) as f64 / 200.0);
let s = (1.0 - z * z).max(0.0).sqrt();
let e = [s * phi.cos(), s * phi.sin(), z];
t[0][0] + 2.0 * (0..3).map(|i| t[0][i + 1] * e[i]).sum::<f64>() + (0..9).map(|i| t[i / 3 + 1][i % 3 + 1] * e[i / 3] * e[i % 3]).sum::<f64>()
})
.fold(f64::INFINITY, f64::min);
let exact = null_minimum(t);
assert!(exact <= sampled && sampled - exact < 1e-3, "{exact} {sampled}");
}
#[test]
fn stress_energy_of_flat_space_and_of_schwarzschild() {
let flat = stress_energy(|_| [[-1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]], [0.3, 1.0, -2.0, 0.5], 1e-3);
assert_eq!(flat, [[0.0; 4]; 4]);
let rindler = |x: [f64; 4]| [[-(1.0 + x[1]).powi(2), 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]];
let t = conditions(rindler, [0.0, 0.5, 0.0, 0.0], 1e-3);
assert!(t.eulerian.iter().flatten().all(|t| t.abs() < 1e-11) && t.null.abs() < 1e-11);
let schwarzschild = |x: [f64; 4]| {
let a = 0.5 / (x[1] * x[1] + x[2] * x[2] + x[3] * x[3]).sqrt();
let s = (1.0 + a).powi(4);
[[-((1.0 - a) / (1.0 + a)).powi(2), 0.0, 0.0, 0.0], [0.0, s, 0.0, 0.0], [0.0, 0.0, s, 0.0], [0.0, 0.0, 0.0, s]]
};
for r in [3.0, 6.0] {
let t = conditions(schwarzschild, [0.0, r / 3f64.sqrt(), r / 3f64.sqrt(), r / 3f64.sqrt()], 1e-3);
assert!(t.eulerian.iter().flatten().all(|t| t.abs() < 1e-10), "{:?}", t.eulerian);
}
}
#[test]
fn energy_conditions_of_warp_drives_from_their_metrics() {
let alcubierre = |x: [f64; 4]| shift_metric(alcubierre_shift(1.0, 1.0, 8.0, x));
let c = conditions(alcubierre, [0.0, 0.0, 1.0, 0.0], 1e-3);
let rho = c.eulerian[0][0];
assert!((rho - alcubierre_energy_density(1.0, 8.0, 1.0, [0.0, 1.0, 0.0]) * GEOMETRIC).abs() < 1e-8);
let (jacobian, lie) = shift_derivatives(&|x| alcubierre_shift(1.0, 1.0, 8.0, x), [0.0, 0.0, 1.0, 0.0], 1e-3);
let mean = rho + (c.eulerian[1][1] + c.eulerian[2][2] + c.eulerian[3][3]) / 3.0;
assert!(close(mean, shift_drive_null_sum(jacobian, lie) * GEOMETRIC, 1e-7) && (mean + 0.37136).abs() < 5e-6);
assert!(close(c.null, -5.0 / (2.0 * PI), 1e-6));
let root = 15f64.sqrt();
let want = [-(root - 2.0), -(2.0 + root), -3.0, 1.0].map(|x| x / (4.0 * PI));
let found = c.type_one.unwrap();
assert!(found.iter().zip(want).all(|(a, b)| close(*a, b, 1e-6)), "{found:?}");
assert!(close(found[0] + found[1], -root / (2.0 * PI), 1e-6) && (found[0] + found[1] + 0.616_40).abs() < 5e-6);
#[allow(clippy::approx_constant)] let diagonal = 0.7071;
for (point, null) in [([0.0, diagonal, diagonal, 0.0], -0.762_533), ([0.0, 1.0, 1e-3, 0.0], -0.676_539)] {
let c = conditions(alcubierre, point, 1e-3);
let analytic = alcubierre_energy_density(1.0, 8.0, 1.0, [point[1], point[2], 0.0]) * GEOMETRIC;
assert!((c.eulerian[0][0] - analytic).abs() < 1e-8);
assert!(c.type_one.is_none() && (c.null - null).abs() < 2e-6, "{point:?}: {}", c.null);
}
let comoving = |x: [f64; 4]| {
let b = 0.1 * (1.0 - alcubierre_shape((x[1] * x[1] + x[2] * x[2] + x[3] * x[3]).sqrt(), 300.0, 0.015));
[[-1.0 + b * b, b, 0.0, 0.0], [b, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]]
};
let x = [0.0, 0.0, 300.0, 0.0];
let c = energy_conditions(comoving(x), stress_energy(comoving, x, 0.5));
assert!(close(c.eulerian[0][0], alcubierre_energy_density(300.0, 0.015, 0.1, [0.0, 300.0, 0.0]), 1e-7));
let natario = |x: [f64; 4]| shift_metric(natario_shift(1.0, 1.0, 4.0, x));
let point = [0.0, 0.6, 0.85, 0.0];
let c = conditions(natario, point, 1e-3);
let (jacobian, _) = shift_derivatives(&|x| natario_shift(1.0, 1.0, 4.0, x), point, 1e-3);
assert!(close(c.eulerian[0][0], shift_drive_density(jacobian) * GEOMETRIC, 1e-7) && (c.eulerian[0][0] + 0.040_527_2).abs() < 1e-7);
assert!(c.type_one.is_none() && (c.null + 1.225_409).abs() < 2e-6);
}
#[test]
fn energy_conditions_of_a_morris_thorne_throat() {
let wormhole = |x: [f64; 4]| {
let r2 = x[1] * x[1] + x[2] * x[2] + x[3] * x[3];
let extra = 1.0 / (1.0 - 1.0 / r2) - 1.0;
core::array::from_fn(|a| {
core::array::from_fn(|b| match (a, b) {
(0, 0) => -1.0,
(0, _) | (_, 0) => 0.0,
(i, j) => f64::from(u8::from(i == j)) + extra * x[i] * x[j] / r2,
})
})
};
let r = 1.2f64;
let c = conditions(wormhole, [0.0, r / 2f64.sqrt(), r / 2f64.sqrt(), 0.0], 1e-3);
let a = 1.0 / (8.0 * PI * r.powi(4));
assert!(close(c.eulerian[0][0], -a, 1e-8) && close(c.null, -2.0 * a, 1e-8) && (c.null + 0.038_376_5).abs() < 1e-7);
let found = c.type_one.unwrap();
assert!(found.iter().zip([-a, -a, a, a]).all(|(x, y)| close(*x, y, 1e-6)), "{found:?}");
}
#[test]
fn regge_wheeler_potentials() {
let (gr, pv) = (Metric::GeneralRelativity, Metric::PolarizableVacuum);
let peak = |metric, s, l, lo: f64, hi: f64| {
let (mut lo, mut hi) = (lo, hi);
for _ in 0..200 {
let (a, b) = (hi - 0.618_034 * (hi - lo), lo + 0.618_034 * (hi - lo));
if regge_wheeler(metric, s, l, a) < regge_wheeler(metric, s, l, b) { lo = a } else { hi = b }
}
(0.5 * (lo + hi), regge_wheeler(metric, s, l, 0.5 * (lo + hi)))
};
let (at, top) = peak(gr, 2, 2, 2.5, 5.0);
assert!((at - (9.0 + 17f64.sqrt()) / 4.0).abs() < 1e-7 && (top - 0.151_286_70).abs() < 1e-8);
for (metric, s, l, lo, value, radius) in [
(gr, 2, 2, 2.5, 0.151_286_699_6, 3.280_776_458),
(pv, 2, 2, 1.5, 0.131_876_761_2, 2.437_564_895),
(gr, 0, 2, 2.5, 0.247_116_781_7, 2.951_714_821),
(pv, 0, 2, 1.2, 0.228_686_877_1, 1.928_849_147),
] {
let (at, top) = peak(metric, s, l, lo, 5.0);
assert!((top - value).abs() < 1e-9 && (at - radius).abs() < 1e-6, "{metric:?} {s} {l}: {top} at {at}");
}
assert!((peak(pv, 2, 2, 1.5, 5.0).1 - 0.13188).abs() < 5e-6);
for l in 1..4 {
let ell = f64::from(l * (l + 1));
assert!(close(regge_wheeler(gr, 1, l, 3.0), ell / 27.0, 1e-15) && close(regge_wheeler(pv, 1, l, 2.0), ell / (4.0 * core::f64::consts::E.powi(2)), 1e-15));
assert!((peak(gr, 1, l, 2.5, 5.0).0 - 3.0).abs() < 1e-7 && (peak(pv, 1, l, 1.2, 5.0).0 - 2.0).abs() < 1e-7);
}
let slope = |r: f64| (regge_wheeler(pv, 0, 0, r + 1e-6) - regge_wheeler(pv, 0, 0, r - 1e-6)) / 2e-6;
for r in [1.0 + 1.0 / 3f64.sqrt(), 1.0 - 1.0 / 3f64.sqrt()] {
assert!(slope(r).abs() < 1e-8);
}
assert!(regge_wheeler(pv, 0, 0, 1.0 + 1.0 / 3f64.sqrt()) > 0.0 && regge_wheeler(pv, 0, 0, 1.0 - 1.0 / 3f64.sqrt()) < 0.0);
for (areal, gap) in [(100.0f64, 5.1364e-5), (1000.0, 5.0134e-7)] {
let (mut lo, mut hi) = (1.0, areal);
for _ in 0..200 {
let mid = 0.5 * (lo + hi);
if mid * (1.0 / mid).exp() < areal { lo = mid } else { hi = mid }
}
let relative = regge_wheeler(pv, 2, 2, lo) / regge_wheeler(gr, 2, 2, areal) - 1.0;
assert!(close(-relative, gap, 1e-3), "{areal}: {relative:e}");
}
}
#[test]
fn the_charged_polarizable_vacuum() {
let mass = C * C / G;
let extremal = crate::gem::extremal_charge_to_mass() * mass;
let k = |ratio: f64, r: f64| charged_pv_dielectric(mass, ratio * extremal, r);
assert!(close(k(0.0, 3.0), (2.0f64 / 3.0).exp(), 1e-14) && close(k(1.0, 5.0), 1.44, 1e-12));
let (below, above) = (k(1.0 - 1e-6, 5.0) - 1.44, k(1.0 + 1e-6, 5.0) - 1.44);
assert!(below > 1e-8 && above < -1e-8 && (below + above).abs() < 1e-12, "{below:e} {above:e}");
for ratio in [0.5, 2.0] {
let r = 1e3;
let areal = r + 1.0 + (1.0 - ratio * ratio) / (4.0 * r);
let rn = 1.0 - 2.0 / areal + ratio * ratio / (areal * areal);
assert!((1.0 / k(ratio, r) - rn).abs() < 1e-9, "{ratio}");
let y = |r: f64| k(ratio, r).sqrt();
let (r, h) = (7.0, 1e-3);
let (d1, d2) = ((y(r + h) - y(r - h)) / (2.0 * h), (y(r + h) - 2.0 * y(r) + y(r - h)) / (h * h));
assert!((d2 + 2.0 / r * d1 - (d1 * d1 - ratio * ratio / r.powi(4)) / y(r)).abs() < 1e-9, "{ratio}");
}
assert!((1..2000).all(|i| k(0.9, 0.005 * i as f64) > 1.0));
let zero = 3f64.sqrt() / (PI - 3f64.sqrt().atan());
assert!((zero - 0.826_993_343).abs() < 1e-9 && k(2.0, zero) < 1e-28);
let (mut lo, mut hi) = (zero * 1.0001, 100.0);
for _ in 0..200 {
let mid = 0.5 * (lo + hi);
if k(2.0, mid) < 1.0 { lo = mid } else { hi = mid }
}
assert!((lo - 1.653_986_686).abs() < 1e-8);
}
}