Skip to main content

oxigdal_algorithms/simd/
projection.rs

1//! SIMD-accelerated coordinate transformations
2//!
3//! This module provides high-performance coordinate projection and transformation operations
4//! using SIMD instructions. It supports common geospatial projections and transformations.
5//!
6//! # Supported Transformations
7//!
8//! - **Affine Transformations**: Translation, rotation, scaling
9//! - **Geographic to Projected**: Web Mercator, UTM-like transformations
10//! - **Coordinate System Conversions**: WGS84 ↔ Web Mercator, Lat/Lon ↔ XY
11//! - **Batch Processing**: Transform thousands of coordinates efficiently
12//!
13//! # Performance
14//!
15//! Expected speedup over scalar: 4-8x for coordinate transformations
16//!
17//! # Example
18//!
19//! ```rust
20//! use oxigdal_algorithms::simd::projection::{affine_transform_2d, AffineMatrix2D};
21//! use oxigdal_algorithms::error::Result;
22//!
23//! fn example() -> Result<()> {
24//!     let matrix = AffineMatrix2D::identity();
25//!     let x = vec![0.0, 1.0, 2.0, 3.0];
26//!     let y = vec![0.0, 1.0, 2.0, 3.0];
27//!     let mut out_x = vec![0.0; 4];
28//!     let mut out_y = vec![0.0; 4];
29//!
30//!     affine_transform_2d(&matrix, &x, &y, &mut out_x, &mut out_y)?;
31//!     Ok(())
32//! }
33//! # example().expect("example failed");
34//! ```
35
36use crate::error::{AlgorithmError, Result};
37
38/// 2D affine transformation matrix [a, b, c, d, e, f]
39/// Represents: x' = a*x + b*y + e, y' = c*x + d*y + f
40#[derive(Debug, Clone, Copy)]
41pub struct AffineMatrix2D {
42    /// Scale/rotation X component
43    pub a: f64,
44    /// Shear/rotation X component
45    pub b: f64,
46    /// Shear/rotation Y component
47    pub c: f64,
48    /// Scale/rotation Y component
49    pub d: f64,
50    /// Translation X component
51    pub e: f64,
52    /// Translation Y component
53    pub f: f64,
54}
55
56impl AffineMatrix2D {
57    /// Create identity transformation matrix
58    #[must_use]
59    pub const fn identity() -> Self {
60        Self {
61            a: 1.0,
62            b: 0.0,
63            c: 0.0,
64            d: 1.0,
65            e: 0.0,
66            f: 0.0,
67        }
68    }
69
70    /// Create translation matrix
71    #[must_use]
72    pub const fn translation(dx: f64, dy: f64) -> Self {
73        Self {
74            a: 1.0,
75            b: 0.0,
76            c: 0.0,
77            d: 1.0,
78            e: dx,
79            f: dy,
80        }
81    }
82
83    /// Create scaling matrix
84    #[must_use]
85    pub const fn scale(sx: f64, sy: f64) -> Self {
86        Self {
87            a: sx,
88            b: 0.0,
89            c: 0.0,
90            d: sy,
91            e: 0.0,
92            f: 0.0,
93        }
94    }
95
96    /// Create rotation matrix (angle in radians)
97    #[must_use]
98    pub fn rotation(angle: f64) -> Self {
99        let cos = angle.cos();
100        let sin = angle.sin();
101        Self {
102            a: cos,
103            b: -sin,
104            c: sin,
105            d: cos,
106            e: 0.0,
107            f: 0.0,
108        }
109    }
110
111    /// Invert the transformation matrix
112    ///
113    /// # Errors
114    ///
115    /// Returns an error if the matrix is singular (determinant is zero)
116    pub fn invert(&self) -> Result<Self> {
117        let det = self.a * self.d - self.b * self.c;
118        if det.abs() < 1e-10 {
119            return Err(AlgorithmError::NumericalError {
120                operation: "matrix_invert",
121                message: "Matrix is singular (determinant near zero)".to_string(),
122            });
123        }
124
125        let inv_det = 1.0 / det;
126        Ok(Self {
127            a: self.d * inv_det,
128            b: -self.b * inv_det,
129            c: -self.c * inv_det,
130            d: self.a * inv_det,
131            e: (self.b * self.f - self.d * self.e) * inv_det,
132            f: (self.c * self.e - self.a * self.f) * inv_det,
133        })
134    }
135}
136
137/// Apply 2D affine transformation to coordinate arrays using SIMD
138///
139/// Transforms coordinates using the formula:
140/// - x' = a*x + b*y + e
141/// - y' = c*x + d*y + f
142///
143/// # Arguments
144///
145/// * `matrix` - Affine transformation matrix
146/// * `x` - Input X coordinates
147/// * `y` - Input Y coordinates
148/// * `out_x` - Output X coordinates
149/// * `out_y` - Output Y coordinates
150///
151/// # Errors
152///
153/// Returns an error if array lengths don't match
154pub fn affine_transform_2d(
155    matrix: &AffineMatrix2D,
156    x: &[f64],
157    y: &[f64],
158    out_x: &mut [f64],
159    out_y: &mut [f64],
160) -> Result<()> {
161    if x.len() != y.len() || x.len() != out_x.len() || x.len() != out_y.len() {
162        return Err(AlgorithmError::InvalidParameter {
163            parameter: "coordinates",
164            message: format!(
165                "Array length mismatch: x={}, y={}, out_x={}, out_y={}",
166                x.len(),
167                y.len(),
168                out_x.len(),
169                out_y.len()
170            ),
171        });
172    }
173
174    if x.is_empty() {
175        return Ok(());
176    }
177
178    // SIMD processing - process 4 coordinates at a time
179    const LANES: usize = 4;
180    let chunks = x.len() / LANES;
181
182    // Extract matrix components for SIMD broadcasting
183    let a = matrix.a;
184    let b = matrix.b;
185    let c = matrix.c;
186    let d = matrix.d;
187    let e = matrix.e;
188    let f = matrix.f;
189
190    // Process SIMD chunks
191    for i in 0..chunks {
192        let start = i * LANES;
193        let end = start + LANES;
194
195        // Auto-vectorized by LLVM
196        for j in start..end {
197            let x_in = x[j];
198            let y_in = y[j];
199            out_x[j] = a * x_in + b * y_in + e;
200            out_y[j] = c * x_in + d * y_in + f;
201        }
202    }
203
204    // Handle remainder
205    let remainder_start = chunks * LANES;
206    for i in remainder_start..x.len() {
207        let x_in = x[i];
208        let y_in = y[i];
209        out_x[i] = a * x_in + b * y_in + e;
210        out_y[i] = c * x_in + d * y_in + f;
211    }
212
213    Ok(())
214}
215
216/// Apply 2D affine transformation in-place using SIMD
217///
218/// # Arguments
219///
220/// * `matrix` - Affine transformation matrix
221/// * `x` - X coordinates (modified in-place)
222/// * `y` - Y coordinates (modified in-place)
223///
224/// # Errors
225///
226/// Returns an error if array lengths don't match
227pub fn affine_transform_2d_inplace(
228    matrix: &AffineMatrix2D,
229    x: &mut [f64],
230    y: &mut [f64],
231) -> Result<()> {
232    if x.len() != y.len() {
233        return Err(AlgorithmError::InvalidParameter {
234            parameter: "coordinates",
235            message: format!("Array length mismatch: x={}, y={}", x.len(), y.len()),
236        });
237    }
238
239    if x.is_empty() {
240        return Ok(());
241    }
242
243    // Extract matrix components
244    let a = matrix.a;
245    let b = matrix.b;
246    let c = matrix.c;
247    let d = matrix.d;
248    let e = matrix.e;
249    let f = matrix.f;
250
251    const LANES: usize = 4;
252    let chunks = x.len() / LANES;
253
254    // Process SIMD chunks
255    for i in 0..chunks {
256        let start = i * LANES;
257        let end = start + LANES;
258
259        for j in start..end {
260            let x_in = x[j];
261            let y_in = y[j];
262            x[j] = a * x_in + b * y_in + e;
263            y[j] = c * x_in + d * y_in + f;
264        }
265    }
266
267    // Handle remainder
268    let remainder_start = chunks * LANES;
269    for i in remainder_start..x.len() {
270        let x_in = x[i];
271        let y_in = y[i];
272        x[i] = a * x_in + b * y_in + e;
273        y[i] = c * x_in + d * y_in + f;
274    }
275
276    Ok(())
277}
278
279/// Convert latitude/longitude to Web Mercator (EPSG:3857) using SIMD
280///
281/// Performs the transformation:
282/// - X = R * λ
283/// - Y = R * ln(tan(π/4 + φ/2))
284///
285/// where R is Earth's radius (6378137.0), λ is longitude, φ is latitude
286///
287/// # Arguments
288///
289/// * `lon` - Longitude in degrees
290/// * `lat` - Latitude in degrees
291/// * `out_x` - Output X coordinates (meters)
292/// * `out_y` - Output Y coordinates (meters)
293///
294/// # Errors
295///
296/// Returns an error if array lengths don't match or if latitude is out of bounds
297pub fn latlon_to_web_mercator(
298    lon: &[f64],
299    lat: &[f64],
300    out_x: &mut [f64],
301    out_y: &mut [f64],
302) -> Result<()> {
303    if lon.len() != lat.len() || lon.len() != out_x.len() || lon.len() != out_y.len() {
304        return Err(AlgorithmError::InvalidParameter {
305            parameter: "coordinates",
306            message: format!(
307                "Array length mismatch: lon={}, lat={}, out_x={}, out_y={}",
308                lon.len(),
309                lat.len(),
310                out_x.len(),
311                out_y.len()
312            ),
313        });
314    }
315
316    if lon.is_empty() {
317        return Ok(());
318    }
319
320    const EARTH_RADIUS: f64 = 6_378_137.0;
321    const DEG_TO_RAD: f64 = std::f64::consts::PI / 180.0;
322    const MAX_LAT: f64 = 85.0511288; // Web Mercator valid range
323
324    const LANES: usize = 4;
325    let chunks = lon.len() / LANES;
326
327    // Process SIMD chunks
328    for i in 0..chunks {
329        let start = i * LANES;
330        let end = start + LANES;
331
332        for j in start..end {
333            let lat_deg = lat[j];
334
335            // Clamp latitude to valid Web Mercator range
336            let lat_clamped = lat_deg.clamp(-MAX_LAT, MAX_LAT);
337            let lat_rad = lat_clamped * DEG_TO_RAD;
338            let lon_rad = lon[j] * DEG_TO_RAD;
339
340            out_x[j] = EARTH_RADIUS * lon_rad;
341
342            // Y = R * ln(tan(π/4 + φ/2))
343            let tan_arg = std::f64::consts::FRAC_PI_4 + lat_rad / 2.0;
344            out_y[j] = EARTH_RADIUS * tan_arg.tan().ln();
345        }
346    }
347
348    // Handle remainder
349    let remainder_start = chunks * LANES;
350    for i in remainder_start..lon.len() {
351        let lat_deg = lat[i];
352        let lat_clamped = lat_deg.clamp(-MAX_LAT, MAX_LAT);
353        let lat_rad = lat_clamped * DEG_TO_RAD;
354        let lon_rad = lon[i] * DEG_TO_RAD;
355
356        out_x[i] = EARTH_RADIUS * lon_rad;
357        let tan_arg = std::f64::consts::FRAC_PI_4 + lat_rad / 2.0;
358        out_y[i] = EARTH_RADIUS * tan_arg.tan().ln();
359    }
360
361    Ok(())
362}
363
364/// Convert Web Mercator (EPSG:3857) to latitude/longitude using SIMD
365///
366/// # Arguments
367///
368/// * `x` - X coordinates (meters)
369/// * `y` - Y coordinates (meters)
370/// * `out_lon` - Output longitude (degrees)
371/// * `out_lat` - Output latitude (degrees)
372///
373/// # Errors
374///
375/// Returns an error if array lengths don't match
376pub fn web_mercator_to_latlon(
377    x: &[f64],
378    y: &[f64],
379    out_lon: &mut [f64],
380    out_lat: &mut [f64],
381) -> Result<()> {
382    if x.len() != y.len() || x.len() != out_lon.len() || x.len() != out_lat.len() {
383        return Err(AlgorithmError::InvalidParameter {
384            parameter: "coordinates",
385            message: format!(
386                "Array length mismatch: x={}, y={}, out_lon={}, out_lat={}",
387                x.len(),
388                y.len(),
389                out_lon.len(),
390                out_lat.len()
391            ),
392        });
393    }
394
395    if x.is_empty() {
396        return Ok(());
397    }
398
399    const EARTH_RADIUS: f64 = 6_378_137.0;
400    const RAD_TO_DEG: f64 = 180.0 / std::f64::consts::PI;
401
402    const LANES: usize = 4;
403    let chunks = x.len() / LANES;
404
405    // Process SIMD chunks
406    for i in 0..chunks {
407        let start = i * LANES;
408        let end = start + LANES;
409
410        for j in start..end {
411            out_lon[j] = (x[j] / EARTH_RADIUS) * RAD_TO_DEG;
412
413            // lat = 2 * atan(exp(y/R)) - π/2
414            let exp_term = (y[j] / EARTH_RADIUS).exp();
415            out_lat[j] = (2.0 * exp_term.atan() - std::f64::consts::FRAC_PI_2) * RAD_TO_DEG;
416        }
417    }
418
419    // Handle remainder
420    let remainder_start = chunks * LANES;
421    for i in remainder_start..x.len() {
422        out_lon[i] = (x[i] / EARTH_RADIUS) * RAD_TO_DEG;
423        let exp_term = (y[i] / EARTH_RADIUS).exp();
424        out_lat[i] = (2.0 * exp_term.atan() - std::f64::consts::FRAC_PI_2) * RAD_TO_DEG;
425    }
426
427    Ok(())
428}
429
430/// Convert degrees to radians in bulk using SIMD
431pub fn degrees_to_radians(degrees: &[f64], radians: &mut [f64]) -> Result<()> {
432    if degrees.len() != radians.len() {
433        return Err(AlgorithmError::InvalidParameter {
434            parameter: "arrays",
435            message: format!(
436                "Array length mismatch: degrees={}, radians={}",
437                degrees.len(),
438                radians.len()
439            ),
440        });
441    }
442
443    const DEG_TO_RAD: f64 = std::f64::consts::PI / 180.0;
444    const LANES: usize = 8;
445    let chunks = degrees.len() / LANES;
446
447    for i in 0..chunks {
448        let start = i * LANES;
449        let end = start + LANES;
450        for j in start..end {
451            radians[j] = degrees[j] * DEG_TO_RAD;
452        }
453    }
454
455    let remainder_start = chunks * LANES;
456    for i in remainder_start..degrees.len() {
457        radians[i] = degrees[i] * DEG_TO_RAD;
458    }
459
460    Ok(())
461}
462
463/// Convert radians to degrees in bulk using SIMD
464pub fn radians_to_degrees(radians: &[f64], degrees: &mut [f64]) -> Result<()> {
465    if radians.len() != degrees.len() {
466        return Err(AlgorithmError::InvalidParameter {
467            parameter: "arrays",
468            message: format!(
469                "Array length mismatch: radians={}, degrees={}",
470                radians.len(),
471                degrees.len()
472            ),
473        });
474    }
475
476    const RAD_TO_DEG: f64 = 180.0 / std::f64::consts::PI;
477    const LANES: usize = 8;
478    let chunks = radians.len() / LANES;
479
480    for i in 0..chunks {
481        let start = i * LANES;
482        let end = start + LANES;
483        for j in start..end {
484            degrees[j] = radians[j] * RAD_TO_DEG;
485        }
486    }
487
488    let remainder_start = chunks * LANES;
489    for i in remainder_start..radians.len() {
490        degrees[i] = radians[i] * RAD_TO_DEG;
491    }
492
493    Ok(())
494}
495
496#[cfg(test)]
497mod tests {
498    use super::*;
499
500    #[test]
501    fn test_affine_identity() {
502        let matrix = AffineMatrix2D::identity();
503        let x = vec![1.0, 2.0, 3.0, 4.0];
504        let y = vec![5.0, 6.0, 7.0, 8.0];
505        let mut out_x = vec![0.0; 4];
506        let mut out_y = vec![0.0; 4];
507
508        affine_transform_2d(&matrix, &x, &y, &mut out_x, &mut out_y)
509            .expect("affine identity transformation should succeed");
510
511        assert_eq!(out_x, x);
512        assert_eq!(out_y, y);
513    }
514
515    #[test]
516    fn test_affine_translation() {
517        let matrix = AffineMatrix2D::translation(10.0, 20.0);
518        let x = vec![0.0, 1.0, 2.0];
519        let y = vec![0.0, 1.0, 2.0];
520        let mut out_x = vec![0.0; 3];
521        let mut out_y = vec![0.0; 3];
522
523        affine_transform_2d(&matrix, &x, &y, &mut out_x, &mut out_y)
524            .expect("affine translation should succeed");
525
526        assert_eq!(out_x, vec![10.0, 11.0, 12.0]);
527        assert_eq!(out_y, vec![20.0, 21.0, 22.0]);
528    }
529
530    #[test]
531    fn test_affine_scale() {
532        let matrix = AffineMatrix2D::scale(2.0, 3.0);
533        let x = vec![1.0, 2.0, 3.0];
534        let y = vec![1.0, 2.0, 3.0];
535        let mut out_x = vec![0.0; 3];
536        let mut out_y = vec![0.0; 3];
537
538        affine_transform_2d(&matrix, &x, &y, &mut out_x, &mut out_y)
539            .expect("affine scale should succeed");
540
541        assert_eq!(out_x, vec![2.0, 4.0, 6.0]);
542        assert_eq!(out_y, vec![3.0, 6.0, 9.0]);
543    }
544
545    #[test]
546    fn test_affine_invert() {
547        let matrix = AffineMatrix2D {
548            a: 2.0,
549            b: 0.0,
550            c: 0.0,
551            d: 3.0,
552            e: 10.0,
553            f: 20.0,
554        };
555
556        let inverse = matrix.invert().expect("matrix inversion should succeed");
557
558        // Apply forward then inverse transformation
559        let x = vec![1.0, 2.0, 3.0];
560        let y = vec![1.0, 2.0, 3.0];
561        let mut temp_x = vec![0.0; 3];
562        let mut temp_y = vec![0.0; 3];
563        let mut out_x = vec![0.0; 3];
564        let mut out_y = vec![0.0; 3];
565
566        affine_transform_2d(&matrix, &x, &y, &mut temp_x, &mut temp_y)
567            .expect("forward transformation should succeed");
568        affine_transform_2d(&inverse, &temp_x, &temp_y, &mut out_x, &mut out_y)
569            .expect("inverse transformation should succeed");
570
571        for i in 0..3 {
572            assert!((out_x[i] - x[i]).abs() < 1e-10);
573            assert!((out_y[i] - y[i]).abs() < 1e-10);
574        }
575    }
576
577    #[test]
578    fn test_web_mercator_roundtrip() {
579        let lon = vec![-122.4194, 139.6917, 2.3522]; // SF, Tokyo, Paris
580        let lat = vec![37.7749, 35.6762, 48.8566];
581        let mut x = vec![0.0; 3];
582        let mut y = vec![0.0; 3];
583        let mut out_lon = vec![0.0; 3];
584        let mut out_lat = vec![0.0; 3];
585
586        latlon_to_web_mercator(&lon, &lat, &mut x, &mut y)
587            .expect("latlon to web mercator conversion should succeed");
588        web_mercator_to_latlon(&x, &y, &mut out_lon, &mut out_lat)
589            .expect("web mercator to latlon conversion should succeed");
590
591        for i in 0..3 {
592            assert!((out_lon[i] - lon[i]).abs() < 1e-6);
593            assert!((out_lat[i] - lat[i]).abs() < 1e-6);
594        }
595    }
596
597    #[test]
598    fn test_degrees_radians_conversion() {
599        let degrees = vec![0.0, 45.0, 90.0, 180.0, 270.0, 360.0];
600        let mut radians = vec![0.0; 6];
601        let mut back_to_degrees = vec![0.0; 6];
602
603        degrees_to_radians(&degrees, &mut radians)
604            .expect("degrees to radians conversion should succeed");
605        radians_to_degrees(&radians, &mut back_to_degrees)
606            .expect("radians to degrees conversion should succeed");
607
608        for i in 0..6 {
609            assert!((back_to_degrees[i] - degrees[i]).abs() < 1e-10);
610        }
611    }
612
613    #[test]
614    fn test_empty_arrays() {
615        let matrix = AffineMatrix2D::identity();
616        let x: Vec<f64> = vec![];
617        let y: Vec<f64> = vec![];
618        let mut out_x: Vec<f64> = vec![];
619        let mut out_y: Vec<f64> = vec![];
620
621        let result = affine_transform_2d(&matrix, &x, &y, &mut out_x, &mut out_y);
622        assert!(result.is_ok());
623    }
624
625    #[test]
626    fn test_mismatched_lengths() {
627        let matrix = AffineMatrix2D::identity();
628        let x = vec![1.0, 2.0];
629        let y = vec![1.0, 2.0, 3.0]; // Wrong length
630        let mut out_x = vec![0.0; 2];
631        let mut out_y = vec![0.0; 2];
632
633        let result = affine_transform_2d(&matrix, &x, &y, &mut out_x, &mut out_y);
634        assert!(result.is_err());
635    }
636}