proof_engine/relativistic/
lensing.rs1use glam::{Vec2, Vec3, Vec4};
4
5#[allow(non_snake_case)]
8pub fn deflection_angle(mass: f64, impact_param: f64, c: f64, G: f64) -> f64 {
9 if impact_param.abs() < 1e-30 {
10 return std::f64::consts::PI; }
12 4.0 * G * mass / (impact_param * c * c)
13}
14
15#[allow(non_snake_case)]
18pub fn einstein_radius(mass: f64, d_lens: f64, d_source: f64, c: f64, G: f64) -> f64 {
19 let d_ls = d_source - d_lens;
20 if d_lens <= 0.0 || d_source <= 0.0 || d_ls <= 0.0 {
21 return 0.0;
22 }
23 (4.0 * G * mass * d_ls / (c * c * d_lens * d_source)).sqrt()
24}
25
26#[allow(non_snake_case)]
29pub fn lens_equation(theta: f64, beta: f64, theta_E: f64) -> f64 {
30 if theta.abs() < 1e-30 {
31 return f64::INFINITY;
32 }
33 theta - beta - theta_E * theta_E / theta
34}
35
36#[allow(non_snake_case)]
39pub fn image_positions(beta: f64, theta_E: f64) -> (f64, f64) {
40 let disc = (beta * beta + 4.0 * theta_E * theta_E).sqrt();
41 let theta_plus = (beta + disc) / 2.0;
42 let theta_minus = (beta - disc) / 2.0;
43 (theta_plus, theta_minus)
44}
45
46#[allow(non_snake_case)]
50pub fn magnification(theta: f64, theta_E: f64) -> f64 {
51 if theta_E.abs() < 1e-30 {
52 return 1.0;
53 }
54 let u = theta / theta_E;
55 let u4 = u * u * u * u;
56 if (u4 - 1.0).abs() < 1e-15 {
57 return f64::INFINITY; }
59 (u4 / (u4 - 1.0)).abs()
60}
61
62#[derive(Debug, Clone)]
64pub struct LensingField {
65 pub width: usize,
66 pub height: usize,
67 pub center: Vec2,
68 pub mass: f64,
69 pub c: f64,
70 pub g_const: f64,
71 pub deflections: Vec<Vec2>,
73 pub cell_size: f32,
74}
75
76impl LensingField {
77 #[allow(non_snake_case)]
78 pub fn new(width: usize, height: usize, center: Vec2, mass: f64, c: f64, G: f64, cell_size: f32) -> Self {
79 let mut deflections = Vec::with_capacity(width * height);
80 for iy in 0..height {
81 for ix in 0..width {
82 let x = (ix as f32 - width as f32 / 2.0) * cell_size + center.x;
83 let y = (iy as f32 - height as f32 / 2.0) * cell_size + center.y;
84 let pos = Vec2::new(x, y);
85 let r = pos - center;
86 let dist = r.length() as f64;
87 if dist < 1e-10 {
88 deflections.push(Vec2::ZERO);
89 continue;
90 }
91 let alpha = deflection_angle(mass, dist, c, G);
92 let dir = r.normalize();
93 deflections.push(dir * alpha as f32);
94 }
95 }
96 Self {
97 width,
98 height,
99 center,
100 mass,
101 c,
102 g_const: G,
103 deflections,
104 cell_size,
105 }
106 }
107
108 pub fn get(&self, ix: usize, iy: usize) -> Vec2 {
110 if ix < self.width && iy < self.height {
111 self.deflections[iy * self.width + ix]
112 } else {
113 Vec2::ZERO
114 }
115 }
116
117 pub fn sample(&self, pos: Vec2) -> Vec2 {
119 let r = pos - self.center;
120 let dist = r.length() as f64;
121 if dist < 1e-10 {
122 return Vec2::ZERO;
123 }
124 let alpha = deflection_angle(self.mass, dist, self.c, self.g_const);
125 let dir = r.normalize();
126 dir * alpha as f32
127 }
128
129 pub fn magnification_at(&self, pos: Vec2) -> f64 {
131 let r = (pos - self.center).length() as f64;
132 let rs = 4.0 * self.g_const * self.mass / (self.c * self.c);
133 let theta_E = (rs).sqrt(); if theta_E < 1e-30 {
135 return 1.0;
136 }
137 magnification(r, theta_E)
138 }
139}
140
141#[allow(non_snake_case)]
144pub fn apply_lensing(
145 glyph_positions: &[Vec2],
146 lens_center: Vec2,
147 lens_mass: f64,
148 c: f64,
149 G: f64,
150) -> Vec<Vec2> {
151 glyph_positions.iter().map(|pos| {
152 let r = *pos - lens_center;
153 let dist = r.length() as f64;
154 if dist < 1e-10 {
155 return *pos;
156 }
157 let alpha = deflection_angle(lens_mass, dist, c, G);
158 let dir = r.normalize();
159 *pos + dir * alpha as f32
160 }).collect()
161}
162
163#[derive(Debug, Clone)]
165pub struct LensingRenderer {
166 pub lens_center: Vec2,
167 pub lens_mass: f64,
168 pub c: f64,
169 pub g_const: f64,
170 pub einstein_ring_visible: bool,
171}
172
173impl LensingRenderer {
174 #[allow(non_snake_case)]
175 pub fn new(lens_center: Vec2, lens_mass: f64, c: f64, G: f64) -> Self {
176 Self {
177 lens_center,
178 lens_mass,
179 c,
180 g_const: G,
181 einstein_ring_visible: true,
182 }
183 }
184
185 pub fn lens_positions(&self, positions: &[Vec2]) -> Vec<Vec2> {
187 apply_lensing(positions, self.lens_center, self.lens_mass, self.c, self.g_const)
188 }
189
190 pub fn magnifications(&self, positions: &[Vec2]) -> Vec<f64> {
192 let d_lens = 1.0; let d_source = 2.0;
194 let theta_E = einstein_radius(self.lens_mass, d_lens, d_source, self.c, self.g_const);
195
196 positions.iter().map(|pos| {
197 let theta = (*pos - self.lens_center).length() as f64;
198 magnification(theta, theta_E)
199 }).collect()
200 }
201
202 pub fn render(&self, positions: &[Vec2]) -> (Vec<Vec2>, Vec<f64>) {
204 let lensed = self.lens_positions(positions);
205 let mags = self.magnifications(positions);
206 (lensed, mags)
207 }
208
209 pub fn einstein_ring_points(&self, d_lens: f64, d_source: f64, n_points: usize) -> Vec<Vec2> {
211 let theta_E = einstein_radius(self.lens_mass, d_lens, d_source, self.c, self.g_const);
212 let mut points = Vec::with_capacity(n_points);
213 for i in 0..n_points {
214 let angle = (i as f64 / n_points as f64) * std::f64::consts::TAU;
215 let p = self.lens_center + Vec2::new(
216 (theta_E * angle.cos()) as f32,
217 (theta_E * angle.sin()) as f32,
218 );
219 points.push(p);
220 }
221 points
222 }
223}
224
225pub fn microlensing_lightcurve(u_min: f64, t_E: f64, times: &[f64]) -> Vec<f64> {
229 let t0 = if times.is_empty() {
230 0.0
231 } else {
232 (times[0] + times[times.len() - 1]) / 2.0
233 };
234
235 times.iter().map(|&t| {
236 let tau = (t - t0) / t_E;
237 let u = (u_min * u_min + tau * tau).sqrt();
238 if u < 1e-15 {
239 return f64::INFINITY;
240 }
241 (u * u + 2.0) / (u * (u * u + 4.0).sqrt())
242 }).collect()
243}
244
245pub fn total_magnification(u: f64) -> f64 {
248 if u < 1e-15 {
249 return f64::INFINITY;
250 }
251 (u * u + 2.0) / (u * (u * u + 4.0).sqrt())
252}
253
254pub fn sis_critical_radius(velocity_dispersion: f64, c: f64, d_lens: f64, d_source: f64) -> f64 {
256 let d_ls = d_source - d_lens;
257 if d_source <= 0.0 || d_ls <= 0.0 {
258 return 0.0;
259 }
260 4.0 * std::f64::consts::PI * velocity_dispersion * velocity_dispersion * d_ls / (c * c * d_source)
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 const C: f64 = 299_792_458.0;
268 const G: f64 = 6.674e-11;
269
270 #[test]
271 fn test_deflection_angle_sun() {
272 let m_sun = 1.989e30;
274 let r_sun = 6.96e8; let alpha = deflection_angle(m_sun, r_sun, C, G);
276 let arcsec = alpha * 206265.0; assert!(
278 (arcsec - 1.75).abs() < 0.1,
279 "Solar deflection: {} arcsec, expected ~1.75",
280 arcsec
281 );
282 }
283
284 #[test]
285 fn test_deflection_decreases_with_distance() {
286 let m = 1e30;
287 let a1 = deflection_angle(m, 1e8, C, G);
288 let a2 = deflection_angle(m, 1e9, C, G);
289 assert!(a2 < a1, "Deflection should decrease with impact param");
290 }
291
292 #[test]
293 fn test_deflection_approaches_zero() {
294 let m = 1e30;
295 let a = deflection_angle(m, 1e20, C, G);
296 assert!(a < 1e-10, "Deflection at large b should be tiny: {}", a);
297 }
298
299 #[test]
300 fn test_einstein_radius() {
301 let m = 1e30;
302 let d_l = 1e20;
303 let d_s = 2e20;
304 let theta_E = einstein_radius(m, d_l, d_s, C, G);
305 assert!(theta_E > 0.0);
306 }
307
308 #[test]
309 fn test_image_positions() {
310 let theta_E = 1.0;
311 let beta = 0.5;
312 let (tp, tm) = image_positions(beta, theta_E);
313 assert!(tp > 0.0);
315 assert!(tm < 0.0);
317 let err_p = lens_equation(tp, beta, theta_E);
319 let err_m = lens_equation(tm, beta, theta_E);
320 assert!(err_p.abs() < 1e-10, "Lens equation error +: {}", err_p);
321 assert!(err_m.abs() < 1e-10, "Lens equation error -: {}", err_m);
322 }
323
324 #[test]
325 fn test_einstein_ring() {
326 let theta_E = 1.0;
328 let (tp, tm) = image_positions(0.0, theta_E);
329 assert!((tp - theta_E).abs() < 1e-10);
330 assert!((tm + theta_E).abs() < 1e-10); }
332
333 #[test]
334 fn test_magnification_at_einstein_ring() {
335 let theta_E = 1.0;
337 let mag = magnification(theta_E, theta_E);
338 assert!(mag.is_infinite() || mag > 1e10, "Magnification at caustic: {}", mag);
339 }
340
341 #[test]
342 fn test_magnification_far_from_lens() {
343 let theta_E = 1.0;
344 let mag = magnification(100.0 * theta_E, theta_E);
345 assert!((mag - 1.0).abs() < 0.01, "Far magnification should be ~1: {}", mag);
346 }
347
348 #[test]
349 fn test_microlensing_lightcurve_peak() {
350 let u_min = 0.5;
351 let t_E = 10.0;
352 let times: Vec<f64> = (-50..=50).map(|i| i as f64).collect();
353 let curve = microlensing_lightcurve(u_min, t_E, ×);
354
355 let mid = curve.len() / 2;
357 assert!(curve[mid] > curve[0], "Peak should be at center");
358 assert!(curve[mid] > curve[curve.len() - 1], "Peak should be at center");
359
360 for &a in &curve {
362 assert!(a >= 1.0, "Amplification should be >= 1: {}", a);
363 }
364 }
365
366 #[test]
367 fn test_apply_lensing() {
368 let positions = vec![Vec2::new(10.0, 0.0), Vec2::new(0.0, 10.0)];
369 let lensed = apply_lensing(&positions, Vec2::ZERO, 1e30, C, G);
370 assert!(lensed[0].x > positions[0].x || (lensed[0] - positions[0]).length() > 0.0);
372 }
373
374 #[test]
375 fn test_total_magnification() {
376 let a = total_magnification(1.0);
378 let expected = 3.0 / 5.0_f64.sqrt();
379 assert!((a - expected).abs() < 1e-10);
380 }
381
382 #[test]
383 fn test_lensing_field() {
384 let field = LensingField::new(10, 10, Vec2::ZERO, 1e30, C, G, 1.0);
385 assert_eq!(field.deflections.len(), 100);
386 let center_defl = field.get(5, 5);
388 let corner_defl = field.get(0, 0);
390 assert!(corner_defl.length() > 0.0 || center_defl.length() >= 0.0);
391 }
392}