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;
185 let beta = (v_radial / self.c).clamp(-0.9999999, 0.9999999);
186 ((1.0 + beta) / (1.0 - beta)).sqrt()
187 }
188
189 pub fn shift_colors(
191 &self,
192 entities: &[(Vec3, Vec3, Vec4)], ) -> Vec<Vec4> {
194 entities.iter().map(|(pos, vel, col)| {
195 self.shifted_color(*col, *pos, *vel)
196 }).collect()
197 }
198
199 pub fn intensity_factor(&self, entity_pos: Vec3, entity_velocity: Vec3) -> f32 {
202 let ratio = self.frequency_ratio(entity_pos, entity_velocity);
203 (ratio.powi(3)).min(10.0) as f32
204 }
205}
206
207pub fn cosmic_redshift(z: f64, base_wavelength: f64) -> f64 {
209 base_wavelength * (1.0 + z)
210}
211
212pub fn velocity_from_redshift(z: f64, c: f64) -> f64 {
214 let z1 = z + 1.0;
215 c * (z1 * z1 - 1.0) / (z1 * z1 + 1.0)
216}
217
218pub fn general_doppler(f_source: f64, v: f64, c: f64, theta: f64) -> f64 {
222 let beta = v / c;
223 let gamma = lorentz_factor(v, c);
224 f_source / (gamma * (1.0 - beta * theta.cos()))
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 const C: f64 = 299_792_458.0;
232
233 #[test]
234 fn test_doppler_blueshift() {
235 let f_obs = relativistic_doppler(1000.0, 0.5 * C, C, true);
236 assert!(f_obs > 1000.0, "Approaching should blueshift: {}", f_obs);
237 }
238
239 #[test]
240 fn test_doppler_redshift() {
241 let f_obs = relativistic_doppler(1000.0, 0.5 * C, C, false);
242 assert!(f_obs < 1000.0, "Receding should redshift: {}", f_obs);
243 }
244
245 #[test]
246 fn test_doppler_symmetry() {
247 let f_blue = relativistic_doppler(1000.0, 0.5 * C, C, true);
248 let f_red = relativistic_doppler(1000.0, 0.5 * C, C, false);
249 assert!(
251 (f_blue * f_red - 1000.0 * 1000.0).abs() < 1.0,
252 "Product should equal f^2: {} * {} = {}",
253 f_blue, f_red, f_blue * f_red
254 );
255 }
256
257 #[test]
258 fn test_transverse_doppler_redshift() {
259 let f_obs = transverse_doppler(1000.0, 0.5 * C, C);
260 assert!(f_obs < 1000.0, "Transverse Doppler should always redshift: {}", f_obs);
261 }
262
263 #[test]
264 fn test_transverse_less_than_longitudinal() {
265 let f_trans = transverse_doppler(1000.0, 0.5 * C, C);
266 let f_long = relativistic_doppler(1000.0, 0.5 * C, C, false);
267 assert!(
269 f_trans > f_long,
270 "Transverse {} should be less shifted than longitudinal receding {}",
271 f_trans, f_long
272 );
273 }
274
275 #[test]
276 fn test_wavelength_shift_receding() {
277 let lambda_obs = wavelength_shift(500.0, 0.5 * C, C);
278 assert!(lambda_obs > 500.0, "Receding should increase wavelength: {}", lambda_obs);
279 }
280
281 #[test]
282 fn test_wavelength_shift_approaching() {
283 let lambda_obs = wavelength_shift(500.0, -0.5 * C, C);
284 assert!(lambda_obs < 500.0, "Approaching should decrease wavelength: {}", lambda_obs);
285 }
286
287 #[test]
288 fn test_redshift_z_zero_at_rest() {
289 let z = redshift_z(0.0, C);
290 assert!(z.abs() < 1e-10);
291 }
292
293 #[test]
294 fn test_redshift_z_approaches_infinity() {
295 let z = redshift_z(0.9999 * C, C);
296 assert!(z > 100.0, "z should be very large near c: {}", z);
297 }
298
299 #[test]
300 fn test_color_from_wavelength_visible() {
301 let red = color_from_wavelength(650.0);
303 assert!(red.x > red.y && red.x > red.z);
304 let green = color_from_wavelength(520.0);
306 assert!(green.y > green.x && green.y > green.z);
307 let blue = color_from_wavelength(460.0);
309 assert!(blue.z > blue.y);
310 }
311
312 #[test]
313 fn test_cosmic_redshift() {
314 let shifted = cosmic_redshift(1.0, 500.0);
315 assert!((shifted - 1000.0).abs() < 1e-10, "z=1 should double wavelength");
316 }
317
318 #[test]
319 fn test_velocity_from_redshift_roundtrip() {
320 let v = 0.6 * C;
321 let z = redshift_z(v, C);
322 let v_back = velocity_from_redshift(z, C);
323 assert!((v - v_back).abs() / v < 1e-6);
324 }
325
326 #[test]
327 fn test_general_doppler_forward() {
328 let f1 = general_doppler(1000.0, 0.5 * C, C, 0.0);
330 let f2 = relativistic_doppler(1000.0, 0.5 * C, C, true);
331 assert!((f1 - f2).abs() < 1e-6);
332 }
333
334 #[test]
335 fn test_general_doppler_transverse() {
336 let f1 = general_doppler(1000.0, 0.5 * C, C, std::f64::consts::FRAC_PI_2);
338 let f2 = transverse_doppler(1000.0, 0.5 * C, C);
339 assert!((f1 - f2).abs() < 1e-6);
340 }
341
342 #[test]
343 fn test_doppler_renderer() {
344 let renderer = DopplerRenderer::new(C, Vec3::new(0.0, 0.0, 0.0));
345 let ratio = renderer.frequency_ratio(
346 Vec3::new(10.0, 0.0, 0.0),
347 Vec3::new(-0.5 * C as f32, 0.0, 0.0), );
349 assert!(ratio > 1.0, "Should be blueshifted: {}", ratio);
350 }
351}