proof_engine/relativistic/
doppler.rs1use glam::{Vec3, Vec4};
4use super::lorentz::lorentz_factor;
5
6pub fn relativistic_doppler(f_source: f64, v: f64, c: f64, approaching: bool) -> f64 {
11 let beta = (v / c).abs().min(0.9999999);
12 if approaching {
13 f_source * ((1.0 + beta) / (1.0 - beta)).sqrt()
14 } else {
15 f_source * ((1.0 - beta) / (1.0 + beta)).sqrt()
16 }
17}
18
19pub fn transverse_doppler(f_source: f64, v: f64, c: f64) -> f64 {
22 let gamma = lorentz_factor(v, c);
23 f_source / gamma
24}
25
26pub fn wavelength_shift(lambda_source: f64, v: f64, c: f64) -> f64 {
30 let beta = v / c;
31 let beta_clamped = beta.clamp(-0.9999999, 0.9999999);
32 lambda_source * ((1.0 + beta_clamped) / (1.0 - beta_clamped)).sqrt()
33}
34
35pub fn redshift_z(v: f64, c: f64) -> f64 {
38 let beta = (v / c).abs().min(0.9999999);
39 ((1.0 + beta) / (1.0 - beta)).sqrt() - 1.0
40}
41
42pub fn color_from_wavelength(wavelength_nm: f64) -> Vec4 {
46 let w = wavelength_nm;
47 let (mut r, mut g, mut b);
48
49 if w < 380.0 {
50 r = 0.2;
52 g = 0.0;
53 b = 0.3;
54 } else if w < 440.0 {
55 r = -(w - 440.0) / (440.0 - 380.0);
56 g = 0.0;
57 b = 1.0;
58 } else if w < 490.0 {
59 r = 0.0;
60 g = (w - 440.0) / (490.0 - 440.0);
61 b = 1.0;
62 } else if w < 510.0 {
63 r = 0.0;
64 g = 1.0;
65 b = -(w - 510.0) / (510.0 - 490.0);
66 } else if w < 580.0 {
67 r = (w - 510.0) / (580.0 - 510.0);
68 g = 1.0;
69 b = 0.0;
70 } else if w < 645.0 {
71 r = 1.0;
72 g = -(w - 645.0) / (645.0 - 580.0);
73 b = 0.0;
74 } else if w < 780.0 {
75 r = 1.0;
76 g = 0.0;
77 b = 0.0;
78 } else {
79 r = 0.3;
81 g = 0.0;
82 b = 0.0;
83 }
84
85 let intensity = if w < 380.0 || w > 780.0 {
87 0.3
88 } else if w < 420.0 {
89 0.3 + 0.7 * (w - 380.0) / (420.0 - 380.0)
90 } else if w > 700.0 {
91 0.3 + 0.7 * (780.0 - w) / (780.0 - 700.0)
92 } else {
93 1.0
94 };
95
96 r *= intensity;
97 g *= intensity;
98 b *= intensity;
99
100 Vec4::new(r as f32, g as f32, b as f32, 1.0)
101}
102
103pub fn doppler_color_shift(
106 base_color: Vec4,
107 v: f64,
108 c: f64,
109 direction: Vec3,
110 observer_dir: Vec3,
111) -> Vec4 {
112 let dir_norm = direction.normalize_or_zero();
113 let obs_norm = observer_dir.normalize_or_zero();
114 let v_radial = -(v as f32) * dir_norm.dot(obs_norm);
116 let v_r = v_radial as f64;
117
118 let beta = (v_r / c).clamp(-0.9999999, 0.9999999);
121 let doppler = ((1.0 + beta) / (1.0 - beta)).sqrt();
122
123 let shift = (1.0 / doppler) as f32;
125
126 if shift > 1.0 {
128 let t = (shift - 1.0).min(1.0);
130 Vec4::new(
131 base_color.x * (1.0 - t) + base_color.y * t,
132 base_color.y * (1.0 - t) + base_color.z * t,
133 base_color.z + base_color.x * t,
134 base_color.w,
135 )
136 } else {
137 let t = (1.0 - shift).min(1.0);
139 Vec4::new(
140 base_color.x + base_color.z * t,
141 base_color.y * (1.0 - t) + base_color.x * t,
142 base_color.z * (1.0 - t) + base_color.y * t,
143 base_color.w,
144 )
145 }
146}
147
148#[derive(Debug, Clone)]
150pub struct DopplerRenderer {
151 pub c: f64,
152 pub observer_pos: Vec3,
153 pub intensity_shift: bool,
154}
155
156impl DopplerRenderer {
157 pub fn new(c: f64, observer_pos: Vec3) -> Self {
158 Self {
159 c,
160 observer_pos,
161 intensity_shift: true,
162 }
163 }
164
165 pub fn shifted_color(
167 &self,
168 base_color: Vec4,
169 entity_pos: Vec3,
170 entity_velocity: Vec3,
171 ) -> Vec4 {
172 let to_observer = (self.observer_pos - entity_pos).normalize_or_zero();
173 let v = entity_velocity.length() as f64;
174 if v < 1e-10 {
175 return base_color;
176 }
177 let direction = entity_velocity.normalize();
178 doppler_color_shift(base_color, v, self.c, direction, to_observer)
179 }
180
181 pub fn frequency_ratio(&self, entity_pos: Vec3, entity_velocity: Vec3) -> f64 {
183 let to_observer = (self.observer_pos - entity_pos).normalize_or_zero();
184 let v_radial = -entity_velocity.dot(to_observer) as f64;
188 let beta = (v_radial / self.c).clamp(-0.9999999, 0.9999999);
189 ((1.0 - beta) / (1.0 + beta)).sqrt()
190 }
191
192 pub fn shift_colors(
194 &self,
195 entities: &[(Vec3, Vec3, Vec4)], ) -> Vec<Vec4> {
197 entities.iter().map(|(pos, vel, col)| {
198 self.shifted_color(*col, *pos, *vel)
199 }).collect()
200 }
201
202 pub fn intensity_factor(&self, entity_pos: Vec3, entity_velocity: Vec3) -> f32 {
205 let ratio = self.frequency_ratio(entity_pos, entity_velocity);
206 (ratio.powi(3)).min(10.0) as f32
207 }
208}
209
210pub fn cosmic_redshift(z: f64, base_wavelength: f64) -> f64 {
212 base_wavelength * (1.0 + z)
213}
214
215pub fn velocity_from_redshift(z: f64, c: f64) -> f64 {
217 let z1 = z + 1.0;
218 c * (z1 * z1 - 1.0) / (z1 * z1 + 1.0)
219}
220
221pub fn general_doppler(f_source: f64, v: f64, c: f64, theta: f64) -> f64 {
225 let beta = v / c;
226 let gamma = lorentz_factor(v, c);
227 f_source / (gamma * (1.0 - beta * theta.cos()))
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 const C: f64 = 299_792_458.0;
235
236 #[test]
237 fn test_doppler_blueshift() {
238 let f_obs = relativistic_doppler(1000.0, 0.5 * C, C, true);
239 assert!(f_obs > 1000.0, "Approaching should blueshift: {}", f_obs);
240 }
241
242 #[test]
243 fn test_doppler_redshift() {
244 let f_obs = relativistic_doppler(1000.0, 0.5 * C, C, false);
245 assert!(f_obs < 1000.0, "Receding should redshift: {}", f_obs);
246 }
247
248 #[test]
249 fn test_doppler_symmetry() {
250 let f_blue = relativistic_doppler(1000.0, 0.5 * C, C, true);
251 let f_red = relativistic_doppler(1000.0, 0.5 * C, C, false);
252 assert!(
254 (f_blue * f_red - 1000.0 * 1000.0).abs() < 1.0,
255 "Product should equal f^2: {} * {} = {}",
256 f_blue, f_red, f_blue * f_red
257 );
258 }
259
260 #[test]
261 fn test_transverse_doppler_redshift() {
262 let f_obs = transverse_doppler(1000.0, 0.5 * C, C);
263 assert!(f_obs < 1000.0, "Transverse Doppler should always redshift: {}", f_obs);
264 }
265
266 #[test]
267 fn test_transverse_less_than_longitudinal() {
268 let f_trans = transverse_doppler(1000.0, 0.5 * C, C);
269 let f_long = relativistic_doppler(1000.0, 0.5 * C, C, false);
270 assert!(
272 f_trans > f_long,
273 "Transverse {} should be less shifted than longitudinal receding {}",
274 f_trans, f_long
275 );
276 }
277
278 #[test]
279 fn test_wavelength_shift_receding() {
280 let lambda_obs = wavelength_shift(500.0, 0.5 * C, C);
281 assert!(lambda_obs > 500.0, "Receding should increase wavelength: {}", lambda_obs);
282 }
283
284 #[test]
285 fn test_wavelength_shift_approaching() {
286 let lambda_obs = wavelength_shift(500.0, -0.5 * C, C);
287 assert!(lambda_obs < 500.0, "Approaching should decrease wavelength: {}", lambda_obs);
288 }
289
290 #[test]
291 fn test_redshift_z_zero_at_rest() {
292 let z = redshift_z(0.0, C);
293 assert!(z.abs() < 1e-10);
294 }
295
296 #[test]
297 fn test_redshift_z_approaches_infinity() {
298 let z = redshift_z(0.9999 * C, C);
299 assert!(z > 100.0, "z should be very large near c: {}", z);
300 }
301
302 #[test]
303 fn test_color_from_wavelength_visible() {
304 let red = color_from_wavelength(650.0);
306 assert!(red.x > red.y && red.x > red.z);
307 let green = color_from_wavelength(520.0);
309 assert!(green.y > green.x && green.y > green.z);
310 let blue = color_from_wavelength(460.0);
312 assert!(blue.z > blue.y);
313 }
314
315 #[test]
316 fn test_cosmic_redshift() {
317 let shifted = cosmic_redshift(1.0, 500.0);
318 assert!((shifted - 1000.0).abs() < 1e-10, "z=1 should double wavelength");
319 }
320
321 #[test]
322 fn test_velocity_from_redshift_roundtrip() {
323 let v = 0.6 * C;
324 let z = redshift_z(v, C);
325 let v_back = velocity_from_redshift(z, C);
326 assert!((v - v_back).abs() / v < 1e-6);
327 }
328
329 #[test]
330 fn test_general_doppler_forward() {
331 let f1 = general_doppler(1000.0, 0.5 * C, C, 0.0);
333 let f2 = relativistic_doppler(1000.0, 0.5 * C, C, true);
334 assert!((f1 - f2).abs() < 1e-6);
335 }
336
337 #[test]
338 fn test_general_doppler_transverse() {
339 let f1 = general_doppler(1000.0, 0.5 * C, C, std::f64::consts::FRAC_PI_2);
341 let f2 = transverse_doppler(1000.0, 0.5 * C, C);
342 assert!((f1 - f2).abs() < 1e-6);
343 }
344
345 #[test]
346 fn test_doppler_renderer() {
347 let renderer = DopplerRenderer::new(C, Vec3::new(0.0, 0.0, 0.0));
348 let ratio = renderer.frequency_ratio(
349 Vec3::new(10.0, 0.0, 0.0),
350 Vec3::new(-0.5 * C as f32, 0.0, 0.0), );
352 assert!(ratio > 1.0, "Should be blueshifted: {}", ratio);
353 }
354}