/home/noah/src/trueno/src/vector/ops/norms.rs
Line | Count | Source |
1 | | //! Vector norm operations |
2 | | //! |
3 | | //! This module provides vector norm calculations: |
4 | | //! - `norm_l1()` - L1 norm (Manhattan norm) |
5 | | //! - `norm_l2()` - L2 norm (Euclidean norm) |
6 | | //! - `norm_linf()` - L∞ norm (infinity norm / max norm) |
7 | | |
8 | | #[cfg(target_arch = "x86_64")] |
9 | | use crate::backends::avx2::Avx2Backend; |
10 | | #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] |
11 | | use crate::backends::neon::NeonBackend; |
12 | | use crate::backends::scalar::ScalarBackend; |
13 | | #[cfg(target_arch = "x86_64")] |
14 | | use crate::backends::sse2::Sse2Backend; |
15 | | #[cfg(target_arch = "wasm32")] |
16 | | use crate::backends::wasm::WasmBackend; |
17 | | use crate::backends::VectorBackend; |
18 | | use crate::{Backend, Result, Vector}; |
19 | | |
20 | | impl Vector<f32> { |
21 | | /// L2 norm (Euclidean norm) |
22 | | /// |
23 | | /// Computes the Euclidean length of the vector: sqrt(sum(a\[i\]^2)). |
24 | | /// This is mathematically equivalent to sqrt(dot(self, self)). |
25 | | /// |
26 | | /// # Performance |
27 | | /// |
28 | | /// Uses optimized SIMD implementations via the dot product operation. |
29 | | /// |
30 | | /// # Examples |
31 | | /// |
32 | | /// ``` |
33 | | /// use trueno::Vector; |
34 | | /// |
35 | | /// let v = Vector::from_slice(&[3.0, 4.0]); |
36 | | /// let norm = v.norm_l2().unwrap(); |
37 | | /// assert!((norm - 5.0).abs() < 1e-5); // sqrt(3^2 + 4^2) = 5 |
38 | | /// ``` |
39 | | /// |
40 | | /// # Empty vectors |
41 | | /// |
42 | | /// Returns 0.0 for empty vectors (consistent with the mathematical definition). |
43 | | /// |
44 | | /// ``` |
45 | | /// use trueno::Vector; |
46 | | /// |
47 | | /// let v: Vector<f32> = Vector::from_slice(&[]); |
48 | | /// assert_eq!(v.norm_l2().unwrap(), 0.0); |
49 | | /// ``` |
50 | 0 | pub fn norm_l2(&self) -> Result<f32> { |
51 | 0 | if self.as_slice().is_empty() { |
52 | 0 | return Ok(0.0); |
53 | 0 | } |
54 | | |
55 | | // SAFETY: Unsafe block delegates to backend implementation which maintains safety invariants |
56 | 0 | let result = unsafe { |
57 | 0 | match self.backend() { |
58 | 0 | Backend::Scalar => ScalarBackend::norm_l2(self.as_slice()), |
59 | | #[cfg(target_arch = "x86_64")] |
60 | 0 | Backend::SSE2 | Backend::AVX => Sse2Backend::norm_l2(self.as_slice()), |
61 | | #[cfg(target_arch = "x86_64")] |
62 | 0 | Backend::AVX2 | Backend::AVX512 => Avx2Backend::norm_l2(self.as_slice()), |
63 | | #[cfg(not(target_arch = "x86_64"))] |
64 | | Backend::SSE2 | Backend::AVX | Backend::AVX2 | Backend::AVX512 => { |
65 | | ScalarBackend::norm_l2(self.as_slice()) |
66 | | } |
67 | | #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] |
68 | | Backend::NEON => NeonBackend::norm_l2(self.as_slice()), |
69 | | #[cfg(not(any(target_arch = "aarch64", target_arch = "arm")))] |
70 | 0 | Backend::NEON => ScalarBackend::norm_l2(self.as_slice()), |
71 | | #[cfg(target_arch = "wasm32")] |
72 | | Backend::WasmSIMD => WasmBackend::norm_l2(self.as_slice()), |
73 | | #[cfg(not(target_arch = "wasm32"))] |
74 | 0 | Backend::WasmSIMD => ScalarBackend::norm_l2(self.as_slice()), |
75 | 0 | Backend::GPU | Backend::Auto => ScalarBackend::norm_l2(self.as_slice()), |
76 | | } |
77 | | }; |
78 | | |
79 | 0 | Ok(result) |
80 | 0 | } |
81 | | |
82 | | /// Compute the L1 norm (Manhattan norm) of the vector |
83 | | /// |
84 | | /// Returns the sum of absolute values: ||v||₁ = sum(|v\[i\]|) |
85 | | /// |
86 | | /// The L1 norm is used in: |
87 | | /// - Machine learning (L1 regularization, Lasso regression) |
88 | | /// - Distance metrics (Manhattan distance) |
89 | | /// - Sparse modeling and feature selection |
90 | | /// - Signal processing |
91 | | /// |
92 | | /// # Examples |
93 | | /// |
94 | | /// ``` |
95 | | /// use trueno::Vector; |
96 | | /// |
97 | | /// let v = Vector::from_slice(&[3.0, -4.0, 5.0]); |
98 | | /// let norm = v.norm_l1().unwrap(); |
99 | | /// |
100 | | /// // |3| + |-4| + |5| = 12 |
101 | | /// assert!((norm - 12.0).abs() < 1e-5); |
102 | | /// ``` |
103 | | /// |
104 | | /// # Empty Vector |
105 | | /// |
106 | | /// ``` |
107 | | /// use trueno::Vector; |
108 | | /// |
109 | | /// let v: Vector<f32> = Vector::from_slice(&[]); |
110 | | /// assert_eq!(v.norm_l1().unwrap(), 0.0); |
111 | | /// ``` |
112 | 0 | pub fn norm_l1(&self) -> Result<f32> { |
113 | 0 | if self.as_slice().is_empty() { |
114 | 0 | return Ok(0.0); |
115 | 0 | } |
116 | | |
117 | | // SAFETY: Unsafe block delegates to backend implementation which maintains safety invariants |
118 | 0 | let result = unsafe { |
119 | 0 | match self.backend() { |
120 | 0 | Backend::Scalar => ScalarBackend::norm_l1(self.as_slice()), |
121 | | #[cfg(target_arch = "x86_64")] |
122 | 0 | Backend::SSE2 | Backend::AVX => Sse2Backend::norm_l1(self.as_slice()), |
123 | | #[cfg(target_arch = "x86_64")] |
124 | 0 | Backend::AVX2 | Backend::AVX512 => Avx2Backend::norm_l1(self.as_slice()), |
125 | | #[cfg(not(target_arch = "x86_64"))] |
126 | | Backend::SSE2 | Backend::AVX | Backend::AVX2 | Backend::AVX512 => { |
127 | | ScalarBackend::norm_l1(self.as_slice()) |
128 | | } |
129 | | #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] |
130 | | Backend::NEON => NeonBackend::norm_l1(self.as_slice()), |
131 | | #[cfg(not(any(target_arch = "aarch64", target_arch = "arm")))] |
132 | 0 | Backend::NEON => ScalarBackend::norm_l1(self.as_slice()), |
133 | | #[cfg(target_arch = "wasm32")] |
134 | | Backend::WasmSIMD => WasmBackend::norm_l1(self.as_slice()), |
135 | | #[cfg(not(target_arch = "wasm32"))] |
136 | 0 | Backend::WasmSIMD => ScalarBackend::norm_l1(self.as_slice()), |
137 | 0 | Backend::GPU | Backend::Auto => ScalarBackend::norm_l1(self.as_slice()), |
138 | | } |
139 | | }; |
140 | | |
141 | 0 | Ok(result) |
142 | 0 | } |
143 | | |
144 | | /// Compute the L∞ norm (infinity norm / max norm) of the vector |
145 | | /// |
146 | | /// Returns the maximum absolute value: ||v||∞ = max(|v\[i\]|) |
147 | | /// |
148 | | /// The L∞ norm is used in: |
149 | | /// - Numerical analysis (error bounds, stability analysis) |
150 | | /// - Optimization (Chebyshev approximation) |
151 | | /// - Signal processing (peak detection) |
152 | | /// - Distance metrics (Chebyshev distance) |
153 | | /// |
154 | | /// # Examples |
155 | | /// |
156 | | /// ``` |
157 | | /// use trueno::Vector; |
158 | | /// |
159 | | /// let v = Vector::from_slice(&[3.0, -7.0, 5.0, -2.0]); |
160 | | /// let norm = v.norm_linf().unwrap(); |
161 | | /// |
162 | | /// // max(|3|, |-7|, |5|, |-2|) = 7 |
163 | | /// assert!((norm - 7.0).abs() < 1e-5); |
164 | | /// ``` |
165 | | /// |
166 | | /// # Empty Vector |
167 | | /// |
168 | | /// ``` |
169 | | /// use trueno::Vector; |
170 | | /// |
171 | | /// let v: Vector<f32> = Vector::from_slice(&[]); |
172 | | /// assert_eq!(v.norm_linf().unwrap(), 0.0); |
173 | | /// ``` |
174 | 0 | pub fn norm_linf(&self) -> Result<f32> { |
175 | 0 | if self.as_slice().is_empty() { |
176 | 0 | return Ok(0.0); |
177 | 0 | } |
178 | | |
179 | | // Use optimized SIMD backend for single-pass abs+max |
180 | | // SAFETY: Unsafe block delegates to backend implementation which maintains safety invariants |
181 | 0 | let max_abs = unsafe { |
182 | 0 | match self.backend() { |
183 | 0 | Backend::Scalar => ScalarBackend::norm_linf(self.as_slice()), |
184 | | #[cfg(target_arch = "x86_64")] |
185 | 0 | Backend::SSE2 | Backend::AVX => Sse2Backend::norm_linf(self.as_slice()), |
186 | | #[cfg(target_arch = "x86_64")] |
187 | 0 | Backend::AVX2 | Backend::AVX512 => Avx2Backend::norm_linf(self.as_slice()), |
188 | | #[cfg(not(target_arch = "x86_64"))] |
189 | | Backend::SSE2 | Backend::AVX | Backend::AVX2 | Backend::AVX512 => { |
190 | | ScalarBackend::norm_linf(self.as_slice()) |
191 | | } |
192 | | #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] |
193 | | Backend::NEON => ScalarBackend::norm_linf(self.as_slice()), // NEON fallback |
194 | | #[cfg(not(any(target_arch = "aarch64", target_arch = "arm")))] |
195 | 0 | Backend::NEON => ScalarBackend::norm_linf(self.as_slice()), |
196 | | #[cfg(target_arch = "wasm32")] |
197 | | Backend::WasmSIMD => ScalarBackend::norm_linf(self.as_slice()), // WASM fallback |
198 | | #[cfg(not(target_arch = "wasm32"))] |
199 | 0 | Backend::WasmSIMD => ScalarBackend::norm_linf(self.as_slice()), |
200 | 0 | Backend::GPU | Backend::Auto => ScalarBackend::norm_linf(self.as_slice()), |
201 | | } |
202 | | }; |
203 | | |
204 | 0 | Ok(max_abs) |
205 | 0 | } |
206 | | } |
207 | | |
208 | | #[cfg(test)] |
209 | | mod tests { |
210 | | use super::*; |
211 | | |
212 | | #[test] |
213 | | fn test_norm_l2_pythagorean() { |
214 | | let v = Vector::from_slice(&[3.0, 4.0]); |
215 | | let norm = v.norm_l2().unwrap(); |
216 | | assert!((norm - 5.0).abs() < 1e-5); // 3-4-5 triangle |
217 | | } |
218 | | |
219 | | #[test] |
220 | | fn test_norm_l2_empty() { |
221 | | let v: Vector<f32> = Vector::from_slice(&[]); |
222 | | assert_eq!(v.norm_l2().unwrap(), 0.0); |
223 | | } |
224 | | |
225 | | #[test] |
226 | | fn test_norm_l2_unit() { |
227 | | let v = Vector::from_slice(&[1.0, 0.0, 0.0]); |
228 | | assert!((v.norm_l2().unwrap() - 1.0).abs() < 1e-5); |
229 | | } |
230 | | |
231 | | #[test] |
232 | | fn test_norm_l1_basic() { |
233 | | let v = Vector::from_slice(&[3.0, -4.0, 5.0]); |
234 | | let norm = v.norm_l1().unwrap(); |
235 | | assert!((norm - 12.0).abs() < 1e-5); |
236 | | } |
237 | | |
238 | | #[test] |
239 | | fn test_norm_l1_empty() { |
240 | | let v: Vector<f32> = Vector::from_slice(&[]); |
241 | | assert_eq!(v.norm_l1().unwrap(), 0.0); |
242 | | } |
243 | | |
244 | | #[test] |
245 | | fn test_norm_linf_basic() { |
246 | | let v = Vector::from_slice(&[3.0, -7.0, 5.0, -2.0]); |
247 | | let norm = v.norm_linf().unwrap(); |
248 | | assert!((norm - 7.0).abs() < 1e-5); |
249 | | } |
250 | | |
251 | | #[test] |
252 | | fn test_norm_linf_empty() { |
253 | | let v: Vector<f32> = Vector::from_slice(&[]); |
254 | | assert_eq!(v.norm_linf().unwrap(), 0.0); |
255 | | } |
256 | | |
257 | | #[test] |
258 | | fn test_norm_linf_all_negative() { |
259 | | let v = Vector::from_slice(&[-1.0, -5.0, -3.0]); |
260 | | let norm = v.norm_linf().unwrap(); |
261 | | assert!((norm - 5.0).abs() < 1e-5); |
262 | | } |
263 | | } |