1use crate::projection::Projection;
2use crate::render::raster::RasterGrid;
3use crate::simd;
4use std::f64::consts::PI;
5
6#[derive(Debug, Clone, Copy)]
7pub struct MollweideProjection;
8
9impl Default for MollweideProjection {
10 fn default() -> Self {
11 Self::new()
12 }
13}
14
15impl MollweideProjection {
16 pub fn new() -> Self {
17 Self
18 }
19}
20
21impl Projection for MollweideProjection {
22 fn inverse(&self, u: f64, v: f64) -> Option<(f64, f64)> {
23 let x = 2.0 - 4.0 * u;
25 let y = 1.0 - 2.0 * v;
26
27 if x * x / 4.0 + y * y > 1.0 {
28 return None;
29 }
30
31 let theta_aux = y.asin();
32 let sin_lat = (2.0 * theta_aux + (2.0 * theta_aux).sin()) / std::f64::consts::PI;
33 if sin_lat.abs() > 1.0 {
34 return None;
35 }
36
37 let lat = sin_lat.asin();
38 let lon = std::f64::consts::PI * x / (2.0 * theta_aux.cos());
39
40 Some((lon, lat))
41 }
42 fn forward(&self, lon: f64, lat: f64) -> Option<(f64, f64)> {
43 let mut theta = lat;
45 for _ in 0..10 {
46 let f = 2.0 * theta + (2.0 * theta).sin() - PI * lat.sin();
47 let df = 2.0 + 2.0 * (2.0 * theta).cos();
48 theta -= f / df;
49 }
50
51 let x = (2.0 * lon / PI) * theta.cos();
52 let y = theta.sin();
53
54 let u = (2.0 - x) * 0.25;
56 let v = (1.0 - y) * 0.5;
57
58 if !(0.0..=1.0).contains(&u) || !(0.0..=1.0).contains(&v) {
59 return None;
60 }
61
62 Some((u, v))
63 }
64 fn pixel_to_ang(&self, x: u32, y: u32, grid: &RasterGrid) -> Option<(f64, f64)> {
65 let nx = x as f64 / ((grid.width - 1) as f64);
67 let ny = y as f64 / ((grid.height - 1) as f64);
68
69 let px = 2.0 - 4.0 * nx;
70 let py = 1.0 - 2.0 * ny;
71
72 if px * px + 4.0 * py * py > 4.0 {
76 return None;
77 }
78
79 let theta_aux = py.asin();
80 let sin_lat = (2.0 * theta_aux + (2.0 * theta_aux).sin()) / PI;
81
82 if sin_lat.abs() > 1.0 {
83 return None;
84 }
85
86 let lat = sin_lat.asin();
87 let c = theta_aux.cos();
88 if c.abs() < 1e-12 {
89 return None;
90 }
91 let lon = PI * px / (2.0 * c);
92
93 Some((lon, lat))
94 }
95
96 fn pixel_to_ang_batch(
101 &self,
102 px_coords: &[u32; 8],
103 py_coords: &[u32; 8],
104 grid: &RasterGrid,
105 ) -> (
106 [f64; 8], [f64; 8], [bool; 8], ) {
110 let w_inv = 1.0 / ((grid.width - 1) as f64);
111 let h_inv = 1.0 / ((grid.height - 1) as f64);
112
113 let mut lons = [0.0_f64; 8];
115 let mut lats = [0.0_f64; 8];
116 let mut mask = [false; 8];
117
118 for i in 0..8 {
121 let nx = px_coords[i] as f64 * w_inv;
122 let ny = py_coords[i] as f64 * h_inv;
123
124 let px = 2.0 - 4.0 * nx;
125 let py = 1.0 - 2.0 * ny;
126
127 if px * px + 4.0 * py * py > 4.0 {
129 continue;
130 }
131
132 let theta_aux = py.asin();
133 let sin_lat = (2.0 * theta_aux + (2.0 * theta_aux).sin()) / PI;
134
135 if sin_lat.abs() > 1.0 {
136 continue;
137 }
138
139 let lat = sin_lat.asin();
140 let c = theta_aux.cos();
141 if c.abs() < 1e-12 {
142 continue;
143 }
144 let lon = PI * px / (2.0 * c);
145
146 lons[i] = lon;
147 lats[i] = lat;
148 mask[i] = true;
149 }
150
151 (lons, lats, mask)
152 }
153}
154
155impl MollweideProjection {
156 #[inline]
162 pub fn pixel_to_ang_batch_simd(
163 &self,
164 px_coords: &[u32; 8],
165 py_coords: &[u32; 8],
166 grid: &RasterGrid,
167 ) -> (
168 [f64; 8], [f64; 8], [bool; 8], ) {
172 let w_inv = 1.0 / ((grid.width - 1) as f64);
173 let h_inv = 1.0 / ((grid.height - 1) as f64);
174
175 let px_f64: [f64; 8] = [
177 px_coords[0] as f64,
178 px_coords[1] as f64,
179 px_coords[2] as f64,
180 px_coords[3] as f64,
181 px_coords[4] as f64,
182 px_coords[5] as f64,
183 px_coords[6] as f64,
184 px_coords[7] as f64,
185 ];
186
187 let py_f64: [f64; 8] = [
188 py_coords[0] as f64,
189 py_coords[1] as f64,
190 py_coords[2] as f64,
191 py_coords[3] as f64,
192 py_coords[4] as f64,
193 py_coords[5] as f64,
194 py_coords[6] as f64,
195 py_coords[7] as f64,
196 ];
197
198 let nx = simd::simd_mul_8(px_f64, [w_inv; 8]);
200 let ny = simd::simd_mul_8(py_f64, [h_inv; 8]);
201
202 let px = simd::simd_add_8([2.0; 8], simd::simd_mul_8(nx, [-4.0; 8]));
204 let py = simd::simd_add_8([1.0; 8], simd::simd_mul_8(ny, [-2.0; 8]));
205
206 let px_sq = simd::simd_mul_8(px, px);
208 let py_sq_4 = simd::simd_mul_8(simd::simd_mul_8(py, py), [4.0; 8]);
209 let oval_check = simd::simd_add_8(px_sq, py_sq_4);
210
211 let theta_aux = simd::simd_asin_8(py);
213
214 let (sin_2theta, _cos_2theta) = simd::simd_sin_cos_8(simd::simd_mul_8(theta_aux, [2.0; 8]));
216
217 let sin_lat = simd::simd_mul_8(
219 simd::simd_add_8(simd::simd_mul_8(theta_aux, [2.0; 8]), sin_2theta),
220 [1.0 / PI; 8],
221 );
222
223 let lat = simd::simd_asin_8(sin_lat);
225
226 let c = simd::simd_cos_8(theta_aux);
228
229 let two_c = simd::simd_mul_8(c, [2.0; 8]);
232 let inv_two_c = simd::simd_recip_8(two_c);
233 let lon = simd::simd_mul_8(simd::simd_mul_8(px, [PI; 8]), inv_two_c);
234
235 let mut mask = [false; 8];
237 for i in 0..8 {
238 if oval_check[i] > 4.0 {
240 continue;
241 }
242 if sin_lat[i].abs() > 1.0 {
244 continue;
245 }
246 if c[i].abs() < 1e-12 {
248 continue;
249 }
250 mask[i] = true;
251 }
252
253 (lon, lat, mask)
254 }
255}
256
257#[test]
258fn mollweide_inverse_rejects_outside_oval() {
259 let p = MollweideProjection;
260
261 assert!(p.inverse(0.5, -0.1).is_none());
263 assert!(p.inverse(0.5, 1.1).is_none());
264
265 assert!(p.inverse(-0.1, 0.5).is_none());
267 assert!(p.inverse(1.1, 0.5).is_none());
268}
269
270#[test]
271fn mollweide_inverse_center() {
272 let p = MollweideProjection;
273 let (lon, lat) = p.inverse(0.5, 0.5).unwrap();
274
275 assert!(lon.abs() < 1e-12);
276 assert!(lat.abs() < 1e-12);
277}
278
279#[test]
280fn mollweide_roundtrip() {
281 let p = MollweideProjection;
282
283 let lon = 1.0;
284 let lat = 0.5;
285
286 let (u, v) = p.forward(lon, lat).unwrap();
287 let (lon2, lat2) = p.inverse(u, v).unwrap();
288
289 assert!((lon - lon2).abs() < 1e-6);
290 assert!((lat - lat2).abs() < 1e-6);
291}
292
293#[test]
294fn raster_and_inverse_agree_on_validity() {
295 let p = MollweideProjection;
296 let grid = RasterGrid::new(100, 50);
297
298 for (_, _, u, v) in grid.iter() {
299 let inv = p.inverse(u, v);
300
301 let x = 2.0 - 4.0 * u;
302 let y = 1.0 - 2.0 * v;
303 let oval = (x * x) / 4.0 + y * y <= 1.0;
304
305 assert_eq!(inv.is_some(), oval);
306 }
307}
308
309#[test]
310fn pixel_to_ang_matches_inverse() {
311 let p = MollweideProjection;
312 let grid = RasterGrid::new(100, 50);
313
314 for (px, py, u, v) in grid.iter() {
315 let inv = p.inverse(u, v);
316 let pixel_to_ang = p.pixel_to_ang(px, py, &grid);
317
318 match (inv, pixel_to_ang) {
319 (Some((lon1, lat1)), Some((lon2, lat2))) => {
320 assert!(
321 (lon1 - lon2).abs() < 1e-10,
322 "lon mismatch at ({}, {}): inverse={}, pixel_to_ang={}",
323 px,
324 py,
325 lon1,
326 lon2
327 );
328 assert!(
329 (lat1 - lat2).abs() < 1e-10,
330 "lat mismatch at ({}, {}): inverse={}, pixel_to_ang={}",
331 px,
332 py,
333 lat1,
334 lat2
335 );
336 }
337 (None, None) => {} _ => panic!(
339 "Validity mismatch at ({}, {}): inverse={}, pixel_to_ang={}",
340 px,
341 py,
342 inv.is_some(),
343 pixel_to_ang.is_some()
344 ),
345 }
346 }
347}
348
349#[test]
350fn pixel_to_ang_center() {
351 let p = MollweideProjection;
352 let grid = RasterGrid::new(512, 256);
353
354 let (lon1, lat1) = p.pixel_to_ang(255, 127, &grid).unwrap();
358 let (lon2, lat2) = p.pixel_to_ang(256, 128, &grid).unwrap();
359
360 assert!(lon1.abs() < 0.02, "Center-1 lon should be ~0: {}", lon1);
362 assert!(lat1.abs() < 0.02, "Center-1 lat should be ~0: {}", lat1);
363 assert!(lon2.abs() < 0.02, "Center lon should be ~0: {}", lon2);
364 assert!(lat2.abs() < 0.02, "Center lat should be ~0: {}", lat2);
365}
366
367#[test]
368fn pixel_to_ang_returns_lon_lat_in_correct_order() {
369 let p = MollweideProjection;
370 let grid = RasterGrid::new(512, 256);
371
372 if let Some((lon, lat)) = p.pixel_to_ang(112, 128, &grid) {
375 assert!(
377 lon > 0.0,
378 "Right side (px>0) should have positive lon: {}",
379 lon
380 );
381 assert!(
382 lat.abs() < 0.3,
383 "Near equator should have small lat: {}",
384 lat
385 );
386 }
387
388 if let Some((lon, lat)) = p.pixel_to_ang(400, 128, &grid) {
391 assert!(
393 lon < 0.0,
394 "Left side (px<0) should have negative lon: {}",
395 lon
396 );
397 assert!(
398 lat.abs() < 0.3,
399 "Near equator should have small lat: {}",
400 lat
401 );
402 }
403}
404
405#[test]
406fn test_mollweide_all_pixels_inside_ellipse() {
407 let proj = MollweideProjection;
412 let grid = RasterGrid::new(1200, 600); let mut inside_count = 0;
415 let mut outside_count = 0;
416 let mut boundary_pixels = Vec::new();
417 let mut invalid_returns = Vec::new();
418
419 for (px, py, u, v) in grid.iter() {
420 let x = 2.0 - 4.0 * u;
422 let y = 1.0 - 2.0 * v;
423 let ellipse_val = (x * x) / 4.0 + y * y;
424 let should_be_inside = ellipse_val <= 1.0;
425
426 if (ellipse_val - 1.0).abs() < 1e-10 {
428 boundary_pixels.push((px, py, ellipse_val, should_be_inside));
429 }
430
431 let result = proj.pixel_to_ang(px, py, &grid);
433 let is_valid = result.is_some();
434
435 if should_be_inside {
436 inside_count += 1;
437 if !is_valid {
438 invalid_returns.push((px, py, ellipse_val));
439 }
440 } else {
441 outside_count += 1;
442 if is_valid {
444 panic!(
445 "Pixel ({}, {}) outside ellipse (val={:.6}) returned valid coordinates",
446 px, py, ellipse_val
447 );
448 }
449 }
450 }
451
452 println!(
453 "Mollweide ellipse coverage: {} inside, {} outside",
454 inside_count, outside_count
455 );
456 if !boundary_pixels.is_empty() {
457 println!("Boundary pixels (ellipse_val ≈ 1.0):");
458 for (px, py, val, inside) in &boundary_pixels {
459 println!(
460 " ({:4}, {:3}): ellipse_val={:.16}, should_be_inside={}",
461 px, py, val, inside
462 );
463 }
464 }
465}
466
467#[test]
468fn batch_projection_matches_scalar() {
469 use crate::projection::Projection;
470
471 let proj = MollweideProjection;
472 let grid = RasterGrid::new(512, 256);
473
474 let px_array = [10u32, 50, 100, 200, 300, 400, 450, 500];
477 let py_array = [10u32, 50, 100, 128, 150, 200, 240, 250];
478
479 let (batch_lons, batch_lats, batch_mask) = proj.pixel_to_ang_batch(&px_array, &py_array, &grid);
480
481 for i in 0..8 {
482 let scalar_result = proj.pixel_to_ang(px_array[i], py_array[i], &grid);
483
484 match (scalar_result, batch_mask[i]) {
485 (Some((scalar_lon, scalar_lat)), true) => {
486 assert!(
488 (batch_lons[i] - scalar_lon).abs() < 1e-14,
489 "Longitude mismatch at ({}, {}): batch={}, scalar={}",
490 px_array[i],
491 py_array[i],
492 batch_lons[i],
493 scalar_lon
494 );
495 assert!(
496 (batch_lats[i] - scalar_lat).abs() < 1e-14,
497 "Latitude mismatch at ({}, {}): batch={}, scalar={}",
498 px_array[i],
499 py_array[i],
500 batch_lats[i],
501 scalar_lat
502 );
503 }
504 (None, false) => {
505 }
507 _ => {
508 panic!(
509 "Mismatch at ({}, {}): scalar valid={}, batch valid={}",
510 px_array[i],
511 py_array[i],
512 scalar_result.is_some(),
513 batch_mask[i]
514 );
515 }
516 }
517 }
518}
519
520#[test]
521fn simd_batch_projection_matches_scalar() {
522 use crate::projection::Projection;
523
524 let proj = MollweideProjection;
525 let grid = RasterGrid::new(512, 256);
526
527 let px_array = [10u32, 50, 100, 200, 300, 400, 450, 500];
529 let py_array = [10u32, 50, 100, 128, 150, 200, 240, 250];
530
531 let (simd_lons, simd_lats, simd_mask) =
532 proj.pixel_to_ang_batch_simd(&px_array, &py_array, &grid);
533
534 for i in 0..8 {
535 let scalar_result = proj.pixel_to_ang(px_array[i], py_array[i], &grid);
536
537 match (scalar_result, simd_mask[i]) {
538 (Some((scalar_lon, scalar_lat)), true) => {
539 assert!(
541 (simd_lons[i] - scalar_lon).abs() < 1e-12,
542 "SIMD Longitude mismatch at ({}, {}): simd={}, scalar={}",
543 px_array[i],
544 py_array[i],
545 simd_lons[i],
546 scalar_lon
547 );
548 assert!(
549 (simd_lats[i] - scalar_lat).abs() < 1e-12,
550 "SIMD Latitude mismatch at ({}, {}): simd={}, scalar={}",
551 px_array[i],
552 py_array[i],
553 simd_lats[i],
554 scalar_lat
555 );
556 }
557 (None, false) => {
558 }
560 _ => {
561 panic!(
562 "SIMD Mismatch at ({}, {}): scalar valid={}, simd valid={}",
563 px_array[i],
564 py_array[i],
565 scalar_result.is_some(),
566 simd_mask[i]
567 );
568 }
569 }
570 }
571}
572
573#[test]
574fn simd_batch_projection_edge_cases() {
575 let proj = MollweideProjection;
576 let grid = RasterGrid::new(512, 256);
577
578 let px_array = [0u32, 0, 511, 511, 256, 256, 1, 510];
580 let py_array = [0u32, 255, 0, 255, 128, 128, 1, 254];
581
582 let (simd_lons, simd_lats, simd_mask) =
583 proj.pixel_to_ang_batch_simd(&px_array, &py_array, &grid);
584
585 for i in 0..8 {
587 assert!(
588 simd_lons[i].is_finite() || !simd_mask[i],
589 "SIMD longitude is not finite at index {} (mask={})",
590 i,
591 simd_mask[i]
592 );
593 assert!(
594 simd_lats[i].is_finite() || !simd_mask[i],
595 "SIMD latitude is not finite at index {} (mask={})",
596 i,
597 simd_mask[i]
598 );
599 }
600
601 let valid_count = simd_mask.iter().filter(|&&m| m).count();
603 assert!(
604 valid_count > 0 && valid_count < 8,
605 "SIMD edge case test: expected mixed valid/invalid, got {} valid",
606 valid_count
607 );
608}
609
610#[test]
611fn simd_batch_matches_scalar_batch() {
612 use crate::projection::Projection;
613
614 let proj = MollweideProjection;
615 let grid = RasterGrid::new(512, 256);
616
617 let px_array = [50u32, 100, 150, 200, 250, 300, 350, 400];
619 let py_array = [50u32, 75, 100, 125, 150, 175, 200, 225];
620
621 let (batch_lons, batch_lats, batch_mask) = proj.pixel_to_ang_batch(&px_array, &py_array, &grid);
622 let (simd_lons, simd_lats, simd_mask) =
623 proj.pixel_to_ang_batch_simd(&px_array, &py_array, &grid);
624
625 for i in 0..8 {
626 assert_eq!(
628 batch_mask[i], simd_mask[i],
629 "Mask mismatch at index {}: batch={}, simd={}",
630 i, batch_mask[i], simd_mask[i]
631 );
632
633 if batch_mask[i] && simd_mask[i] {
635 assert!(
636 (batch_lons[i] - simd_lons[i]).abs() < 1e-12,
637 "Batch vs SIMD longitude mismatch at index {}: batch={}, simd={}",
638 i,
639 batch_lons[i],
640 simd_lons[i]
641 );
642 assert!(
643 (batch_lats[i] - simd_lats[i]).abs() < 1e-12,
644 "Batch vs SIMD latitude mismatch at index {}: batch={}, simd={}",
645 i,
646 batch_lats[i],
647 simd_lats[i]
648 );
649 }
650 }
651}