use core::f64::consts::PI;
use metrology::{AU, C, G};
pub const KAPPA: f64 = 4.0 * G / (C * C * AU);
pub fn sis(sigma: f64, dls: f64, ds: f64) -> f64 {
4.0 * PI * (sigma / C).powi(2) * dls / ds
}
pub fn microlensing(u: f64) -> (f64, f64) {
((u * u + 2.0) / (u * (u * u + 4.0).sqrt()), u / (u * u + 2.0))
}
pub fn lens_mass(theta_e: f64, pi_e: f64) -> f64 {
theta_e / (KAPPA * pi_e)
}
pub fn nfw(x: f64) -> (f64, f64) {
let u = x * x - 1.0;
let f = if u > 0.0 { u.sqrt().atan() / u.sqrt() } else if u < 0.0 { (-u).sqrt().atanh() / (-u).sqrt() } else { 1.0 };
let density = if u.abs() < 0.1 {
(0..20).map(|k| (-u).powi(k) / (2 * k + 3) as f64).sum()
} else {
(1.0 - f) / u
};
(density, 2.0 / (x * x) * (f + (x / 2.0).ln()))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Assumption {
Assertive,
Conservative,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Hubble {
pub name: &'static str,
pub value: (f64, f64, f64),
pub assumption: Assumption,
pub data: &'static str,
pub source: &'static str,
}
pub const H0_REGISTRY: [Hubble; 2] = [
Hubble { name: "H0LiCOW (six lenses)", value: (73.3, 1.7, 1.8), assumption: Assumption::Assertive, data: "lenses only", source: "https://arxiv.org/abs/1907.04869" },
Hubble { name: "TDCOSMO 2025 (eight lenses, new kinematics)", value: (71.6, 3.9, 3.3), assumption: Assumption::Conservative, data: "lenses with Pantheon+ supernovae", source: "https://arxiv.org/abs/2506.03023" },
];
#[cfg(test)]
mod tests {
use super::*;
use crate::{H100, cosmology::angular_diameter_distance};
use metrology::SOLAR_MASS;
#[test]
fn ngc_6505() {
let (zl, zs, theta) = (0.042, 0.4058, 2.5 / 206_264.806_247);
let (dls, ds) = (angular_diameter_distance(zl, zs, 0.7 * H100, 0.3), angular_diameter_distance(0.0, zs, 0.7 * H100, 0.3));
let sigma = C * (theta * ds / (4.0 * PI * dls)).sqrt();
assert!((sis(sigma, dls, ds) / theta - 1.0).abs() < 1e-14);
assert!((sigma / 1e3 - 313.0).abs() < 1.0 && (sigma / 1e3 - 303.0).abs() < 15.0, "{}", sigma / 1e3);
let other = angular_diameter_distance(zl, zs, 0.6 * H100, 0.3) / angular_diameter_distance(0.0, zs, 0.6 * H100, 0.3);
assert!((other / (dls / ds) - 1.0).abs() < 1e-12);
}
#[test]
fn microlensing_and_the_lens_mass() {
assert!((microlensing(1e-6).0 * 1e-6 - 1.0).abs() < 1e-11 && (microlensing(1e3).0 - 1.0) < 1e-11);
let peak = microlensing(2f64.sqrt()).1;
assert!((peak - 1.0 / (2.0 * 2f64.sqrt())).abs() < 1e-15 && microlensing(1.3).1 < peak && microlensing(1.5).1 < peak);
let mas = PI / 180.0 / 3600.0 * 1e-3;
assert!((lens_mass(5.496 * mas, 0.0944) / SOLAR_MASS - 7.149).abs() < 1e-3);
assert!((KAPPA * SOLAR_MASS / mas - 8.144).abs() < 5e-4);
}
#[test]
fn an_nfw_halo() {
assert!((nfw(1.0).0 - 1.0 / 3.0).abs() < 1e-16);
let slope = (nfw(1.0 + 1e-6).0 - nfw(1.0 - 1e-6).0) / 2e-6;
assert!((slope + 0.4).abs() < 1e-8);
for edge in [1.1f64.sqrt(), 0.9f64.sqrt()] {
let (a, b) = (nfw(edge * (1.0 - 1e-15)).0, nfw(edge * (1.0 + 1e-15)).0);
assert!((a - b).abs() < 1e-14, "{edge}");
}
for x in [0.3, 1.0, 2.5] {
let n = 4000;
let h = x / n as f64;
let integral: f64 = (0..n).map(|i| { let s = (i as f64 + 0.5) * h; s * nfw(s).0 }).sum::<f64>() * h;
assert!((2.0 / (x * x) * integral / nfw(x).1 - 1.0).abs() < 1e-5, "x = {x}");
}
assert!((nfw(1.0).1 - 2.0 * (1.0 + 0.5f64.ln())).abs() < 1e-15);
}
#[test]
fn the_h0_registry() {
let [h0licow, tdcosmo] = H0_REGISTRY;
assert!(tdcosmo.value.0 < h0licow.value.0 && tdcosmo.value.1 > h0licow.value.1);
assert_eq!((h0licow.assumption, tdcosmo.assumption), (Assumption::Assertive, Assumption::Conservative));
}
}