/home/noah/src/trueno/src/vector/ops/arithmetic.rs
Line | Count | Source |
1 | | //! Arithmetic operations for Vector<f32> |
2 | | //! |
3 | | //! This module provides element-wise arithmetic operations: |
4 | | //! - Basic: `add`, `sub`, `mul`, `div` |
5 | | //! - Scalar: `scale` |
6 | | //! - Fused: `fma` (fused multiply-add) |
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::vector::Vector; |
19 | | use crate::{dispatch_binary_op, Backend, Result, TruenoError}; |
20 | | |
21 | | impl Vector<f32> { |
22 | | /// Element-wise addition |
23 | | /// |
24 | | /// # Performance |
25 | | /// |
26 | | /// Auto-selects the best available backend: |
27 | | /// - **AVX2**: ~4x faster than scalar for 1K+ elements |
28 | | /// - **GPU**: ~50x faster than scalar for 10M+ elements |
29 | | /// |
30 | | /// # Examples |
31 | | /// |
32 | | /// ``` |
33 | | /// use trueno::Vector; |
34 | | /// |
35 | | /// let a = Vector::from_slice(&[1.0, 2.0, 3.0]); |
36 | | /// let b = Vector::from_slice(&[4.0, 5.0, 6.0]); |
37 | | /// let result = a.add(&b)?; |
38 | | /// |
39 | | /// assert_eq!(result.as_slice(), &[5.0, 7.0, 9.0]); |
40 | | /// # Ok::<(), trueno::TruenoError>(()) |
41 | | /// ``` |
42 | | /// |
43 | | /// # Errors |
44 | | /// |
45 | | /// Returns [`TruenoError::SizeMismatch`] if vectors have different lengths. |
46 | 15 | pub fn add(&self, other: &Self) -> Result<Self> { |
47 | 15 | if self.len() != other.len() { |
48 | 0 | return Err(TruenoError::SizeMismatch { |
49 | 0 | expected: self.len(), |
50 | 0 | actual: other.len(), |
51 | 0 | }); |
52 | 15 | } |
53 | | |
54 | 15 | let mut result = vec![0.0; self.len()]; |
55 | | |
56 | | // Use parallel processing for large arrays |
57 | | #[cfg(feature = "parallel")] |
58 | | { |
59 | | const PARALLEL_THRESHOLD: usize = 100_000; // Threshold for element-wise ops |
60 | | const CHUNK_SIZE: usize = 65536; // 64K elements = 256KB, cache-friendly |
61 | | |
62 | | if self.len() >= PARALLEL_THRESHOLD { |
63 | | use rayon::prelude::*; |
64 | | |
65 | | self.data |
66 | | .par_chunks(CHUNK_SIZE) |
67 | | .zip(other.data.par_chunks(CHUNK_SIZE)) |
68 | | .zip(result.par_chunks_mut(CHUNK_SIZE)) |
69 | | .for_each(|((chunk_a, chunk_b), chunk_out)| { |
70 | | dispatch_binary_op!(self.backend, add, chunk_a, chunk_b, chunk_out); |
71 | | }); |
72 | | |
73 | | return Ok(Self { |
74 | | data: result, |
75 | | backend: self.backend, |
76 | | }); |
77 | | } |
78 | | } |
79 | | |
80 | 15 | dispatch_binary_op!0 (self.backend, add, &self.data0 , &other.data0 , &mut result0 ); |
81 | | |
82 | 15 | Ok(Self { |
83 | 15 | data: result, |
84 | 15 | backend: self.backend, |
85 | 15 | }) |
86 | 15 | } |
87 | | |
88 | | /// Element-wise subtraction |
89 | | /// |
90 | | /// # Performance |
91 | | /// |
92 | | /// Auto-selects the best available backend: |
93 | | /// - **AVX2**: ~4x faster than scalar for 1K+ elements |
94 | | /// - **GPU**: ~50x faster than scalar for 10M+ elements |
95 | | /// |
96 | | /// # Examples |
97 | | /// |
98 | | /// ``` |
99 | | /// use trueno::Vector; |
100 | | /// |
101 | | /// let a = Vector::from_slice(&[5.0, 7.0, 9.0]); |
102 | | /// let b = Vector::from_slice(&[1.0, 2.0, 3.0]); |
103 | | /// let result = a.sub(&b)?; |
104 | | /// |
105 | | /// assert_eq!(result.as_slice(), &[4.0, 5.0, 6.0]); |
106 | | /// # Ok::<(), trueno::TruenoError>(()) |
107 | | /// ``` |
108 | | /// |
109 | | /// # Errors |
110 | | /// |
111 | | /// Returns [`TruenoError::SizeMismatch`] if vectors have different lengths. |
112 | 15 | pub fn sub(&self, other: &Self) -> Result<Self> { |
113 | 15 | if self.len() != other.len() { |
114 | 0 | return Err(TruenoError::SizeMismatch { |
115 | 0 | expected: self.len(), |
116 | 0 | actual: other.len(), |
117 | 0 | }); |
118 | 15 | } |
119 | | |
120 | 15 | let mut result = vec![0.0; self.len()]; |
121 | | |
122 | | // Use parallel processing for large arrays |
123 | | #[cfg(feature = "parallel")] |
124 | | { |
125 | | const PARALLEL_THRESHOLD: usize = 100_000; |
126 | | const CHUNK_SIZE: usize = 65536; |
127 | | |
128 | | if self.len() >= PARALLEL_THRESHOLD { |
129 | | use rayon::prelude::*; |
130 | | |
131 | | self.data |
132 | | .par_chunks(CHUNK_SIZE) |
133 | | .zip(other.data.par_chunks(CHUNK_SIZE)) |
134 | | .zip(result.par_chunks_mut(CHUNK_SIZE)) |
135 | | .for_each(|((chunk_a, chunk_b), chunk_out)| { |
136 | | dispatch_binary_op!(self.backend, sub, chunk_a, chunk_b, chunk_out); |
137 | | }); |
138 | | |
139 | | return Ok(Self { |
140 | | data: result, |
141 | | backend: self.backend, |
142 | | }); |
143 | | } |
144 | | } |
145 | | |
146 | 15 | dispatch_binary_op!0 (self.backend, sub, &self.data0 , &other.data0 , &mut result0 ); |
147 | | |
148 | 15 | Ok(Self { |
149 | 15 | data: result, |
150 | 15 | backend: self.backend, |
151 | 15 | }) |
152 | 15 | } |
153 | | |
154 | | /// Element-wise multiplication |
155 | | /// |
156 | | /// # Examples |
157 | | /// |
158 | | /// ``` |
159 | | /// use trueno::Vector; |
160 | | /// |
161 | | /// let a = Vector::from_slice(&[2.0, 3.0, 4.0]); |
162 | | /// let b = Vector::from_slice(&[5.0, 6.0, 7.0]); |
163 | | /// let result = a.mul(&b)?; |
164 | | /// |
165 | | /// assert_eq!(result.as_slice(), &[10.0, 18.0, 28.0]); |
166 | | /// # Ok::<(), trueno::TruenoError>(()) |
167 | | /// ``` |
168 | 61 | pub fn mul(&self, other: &Self) -> Result<Self> { |
169 | 61 | if self.len() != other.len() { |
170 | 0 | return Err(TruenoError::SizeMismatch { |
171 | 0 | expected: self.len(), |
172 | 0 | actual: other.len(), |
173 | 0 | }); |
174 | 61 | } |
175 | | |
176 | 61 | let mut result = vec![0.0; self.len()]; |
177 | | |
178 | | // Use parallel processing for large arrays |
179 | | #[cfg(feature = "parallel")] |
180 | | { |
181 | | const PARALLEL_THRESHOLD: usize = 100_000; |
182 | | const CHUNK_SIZE: usize = 65536; |
183 | | |
184 | | if self.len() >= PARALLEL_THRESHOLD { |
185 | | use rayon::prelude::*; |
186 | | |
187 | | self.data |
188 | | .par_chunks(CHUNK_SIZE) |
189 | | .zip(other.data.par_chunks(CHUNK_SIZE)) |
190 | | .zip(result.par_chunks_mut(CHUNK_SIZE)) |
191 | | .for_each(|((chunk_a, chunk_b), chunk_out)| { |
192 | | dispatch_binary_op!(self.backend, mul, chunk_a, chunk_b, chunk_out); |
193 | | }); |
194 | | |
195 | | return Ok(Self { |
196 | | data: result, |
197 | | backend: self.backend, |
198 | | }); |
199 | | } |
200 | | } |
201 | | |
202 | 61 | dispatch_binary_op!0 (self.backend, mul, &self.data0 , &other.data0 , &mut result0 ); |
203 | | |
204 | 61 | Ok(Self { |
205 | 61 | data: result, |
206 | 61 | backend: self.backend, |
207 | 61 | }) |
208 | 61 | } |
209 | | |
210 | | /// Element-wise division |
211 | | /// |
212 | | /// # Examples |
213 | | /// |
214 | | /// ``` |
215 | | /// use trueno::Vector; |
216 | | /// |
217 | | /// let a = Vector::from_slice(&[10.0, 20.0, 30.0]); |
218 | | /// let b = Vector::from_slice(&[2.0, 4.0, 5.0]); |
219 | | /// let result = a.div(&b)?; |
220 | | /// |
221 | | /// assert_eq!(result.as_slice(), &[5.0, 5.0, 6.0]); |
222 | | /// # Ok::<(), trueno::TruenoError>(()) |
223 | | /// ``` |
224 | 0 | pub fn div(&self, other: &Self) -> Result<Self> { |
225 | 0 | if self.len() != other.len() { |
226 | 0 | return Err(TruenoError::SizeMismatch { |
227 | 0 | expected: self.len(), |
228 | 0 | actual: other.len(), |
229 | 0 | }); |
230 | 0 | } |
231 | | |
232 | 0 | let mut result = vec![0.0; self.len()]; |
233 | | |
234 | | // Use parallel processing for large arrays |
235 | | #[cfg(feature = "parallel")] |
236 | | { |
237 | | const PARALLEL_THRESHOLD: usize = 100_000; |
238 | | const CHUNK_SIZE: usize = 65536; |
239 | | |
240 | | if self.len() >= PARALLEL_THRESHOLD { |
241 | | use rayon::prelude::*; |
242 | | |
243 | | self.data |
244 | | .par_chunks(CHUNK_SIZE) |
245 | | .zip(other.data.par_chunks(CHUNK_SIZE)) |
246 | | .zip(result.par_chunks_mut(CHUNK_SIZE)) |
247 | | .for_each(|((chunk_a, chunk_b), chunk_out)| { |
248 | | dispatch_binary_op!(self.backend, div, chunk_a, chunk_b, chunk_out); |
249 | | }); |
250 | | |
251 | | return Ok(Self { |
252 | | data: result, |
253 | | backend: self.backend, |
254 | | }); |
255 | | } |
256 | | } |
257 | | |
258 | 0 | dispatch_binary_op!(self.backend, div, &self.data, &other.data, &mut result); |
259 | | |
260 | 0 | Ok(Self { |
261 | 0 | data: result, |
262 | 0 | backend: self.backend, |
263 | 0 | }) |
264 | 0 | } |
265 | | |
266 | | /// Scalar multiplication (scale all elements by a scalar value) |
267 | | /// |
268 | | /// Returns a new vector where each element is multiplied by the scalar. |
269 | | /// |
270 | | /// # Examples |
271 | | /// |
272 | | /// ``` |
273 | | /// use trueno::Vector; |
274 | | /// |
275 | | /// let v = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0]); |
276 | | /// let result = v.scale(2.0)?; |
277 | | /// |
278 | | /// assert_eq!(result.as_slice(), &[2.0, 4.0, 6.0, 8.0]); |
279 | | /// # Ok::<(), trueno::TruenoError>(()) |
280 | | /// ``` |
281 | | /// |
282 | | /// # Scaling by Zero |
283 | | /// |
284 | | /// ``` |
285 | | /// use trueno::Vector; |
286 | | /// |
287 | | /// let v = Vector::from_slice(&[1.0, 2.0, 3.0]); |
288 | | /// let result = v.scale(0.0)?; |
289 | | /// assert_eq!(result.as_slice(), &[0.0, 0.0, 0.0]); |
290 | | /// # Ok::<(), trueno::TruenoError>(()) |
291 | | /// ``` |
292 | | /// |
293 | | /// # Negative Scaling |
294 | | /// |
295 | | /// ``` |
296 | | /// use trueno::Vector; |
297 | | /// |
298 | | /// let v = Vector::from_slice(&[1.0, -2.0, 3.0]); |
299 | | /// let result = v.scale(-2.0)?; |
300 | | /// assert_eq!(result.as_slice(), &[-2.0, 4.0, -6.0]); |
301 | | /// # Ok::<(), trueno::TruenoError>(()) |
302 | | /// ``` |
303 | 1 | pub fn scale(&self, scalar: f32) -> Result<Vector<f32>> { |
304 | 1 | let mut result_data = vec![0.0; self.len()]; |
305 | | |
306 | 1 | if !self.data.is_empty() { |
307 | | // SAFETY: Unsafe block delegates to backend implementation which maintains safety invariants |
308 | | unsafe { |
309 | 1 | match self.backend { |
310 | 0 | Backend::Scalar => ScalarBackend::scale(&self.data, scalar, &mut result_data), |
311 | | #[cfg(target_arch = "x86_64")] |
312 | | Backend::SSE2 | Backend::AVX => { |
313 | 0 | Sse2Backend::scale(&self.data, scalar, &mut result_data) |
314 | | } |
315 | | #[cfg(target_arch = "x86_64")] |
316 | | Backend::AVX2 | Backend::AVX512 => { |
317 | 1 | Avx2Backend::scale(&self.data, scalar, &mut result_data) |
318 | | } |
319 | | #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] |
320 | | Backend::NEON => NeonBackend::scale(&self.data, scalar, &mut result_data), |
321 | | #[cfg(target_arch = "wasm32")] |
322 | | Backend::WasmSIMD => WasmBackend::scale(&self.data, scalar, &mut result_data), |
323 | 0 | Backend::GPU => return Err(TruenoError::UnsupportedBackend(Backend::GPU)), |
324 | | Backend::Auto => { |
325 | | // Auto should have been resolved at creation time |
326 | 0 | return Err(TruenoError::UnsupportedBackend(Backend::Auto)); |
327 | | } |
328 | | #[allow(unreachable_patterns)] |
329 | 0 | _ => ScalarBackend::scale(&self.data, scalar, &mut result_data), |
330 | | } |
331 | | } |
332 | 0 | } |
333 | | |
334 | 1 | Ok(Vector { |
335 | 1 | data: result_data, |
336 | 1 | backend: self.backend, |
337 | 1 | }) |
338 | 1 | } |
339 | | |
340 | | /// Fused multiply-add: result\[i\] = self\[i\] * b\[i\] + c\[i\] |
341 | | /// |
342 | | /// Computes element-wise fused multiply-add operation. On hardware with FMA support |
343 | | /// (AVX2, NEON), this is a single instruction with better performance and numerical |
344 | | /// accuracy (no intermediate rounding). On platforms without FMA (SSE2, WASM), uses |
345 | | /// separate multiply and add operations. |
346 | | /// |
347 | | /// # Arguments |
348 | | /// |
349 | | /// * `b` - The second vector to multiply with |
350 | | /// * `c` - The vector to add to the product |
351 | | /// |
352 | | /// # Returns |
353 | | /// |
354 | | /// A new vector where each element is `self\[i\] * b\[i\] + c\[i\]` |
355 | | /// |
356 | | /// # Errors |
357 | | /// |
358 | | /// Returns `SizeMismatch` if vector lengths don't match |
359 | | /// |
360 | | /// # Examples |
361 | | /// |
362 | | /// ``` |
363 | | /// use trueno::Vector; |
364 | | /// |
365 | | /// let a = Vector::from_slice(&[2.0, 3.0, 4.0]); |
366 | | /// let b = Vector::from_slice(&[5.0, 6.0, 7.0]); |
367 | | /// let c = Vector::from_slice(&[1.0, 2.0, 3.0]); |
368 | | /// let result = a.fma(&b, &c)?; |
369 | | /// assert_eq!(result.as_slice(), &[11.0, 20.0, 31.0]); // [2*5+1, 3*6+2, 4*7+3] |
370 | | /// # Ok::<(), trueno::TruenoError>(()) |
371 | | /// ``` |
372 | | /// |
373 | | /// # Use Cases |
374 | | /// |
375 | | /// - Neural networks: matrix multiplication, backpropagation |
376 | | /// - Scientific computing: polynomial evaluation, numerical integration |
377 | | /// - Graphics: transformation matrices, shader computations |
378 | | /// - Physics simulations: force calculations, particle systems |
379 | 0 | pub fn fma(&self, b: &Vector<f32>, c: &Vector<f32>) -> Result<Vector<f32>> { |
380 | 0 | if self.len() != b.len() { |
381 | 0 | return Err(TruenoError::SizeMismatch { |
382 | 0 | expected: self.len(), |
383 | 0 | actual: b.len(), |
384 | 0 | }); |
385 | 0 | } |
386 | 0 | if self.len() != c.len() { |
387 | 0 | return Err(TruenoError::SizeMismatch { |
388 | 0 | expected: self.len(), |
389 | 0 | actual: c.len(), |
390 | 0 | }); |
391 | 0 | } |
392 | | |
393 | 0 | let mut result_data = vec![0.0; self.len()]; |
394 | | |
395 | 0 | if !self.data.is_empty() { |
396 | | // SAFETY: Unsafe block delegates to backend implementation which maintains safety invariants |
397 | | unsafe { |
398 | 0 | match self.backend { |
399 | | Backend::Scalar => { |
400 | 0 | ScalarBackend::fma(&self.data, &b.data, &c.data, &mut result_data) |
401 | | } |
402 | | #[cfg(target_arch = "x86_64")] |
403 | | Backend::SSE2 | Backend::AVX => { |
404 | 0 | Sse2Backend::fma(&self.data, &b.data, &c.data, &mut result_data) |
405 | | } |
406 | | #[cfg(target_arch = "x86_64")] |
407 | | Backend::AVX2 | Backend::AVX512 => { |
408 | 0 | Avx2Backend::fma(&self.data, &b.data, &c.data, &mut result_data) |
409 | | } |
410 | | #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] |
411 | | Backend::NEON => { |
412 | | NeonBackend::fma(&self.data, &b.data, &c.data, &mut result_data) |
413 | | } |
414 | | #[cfg(target_arch = "wasm32")] |
415 | | Backend::WasmSIMD => { |
416 | | WasmBackend::fma(&self.data, &b.data, &c.data, &mut result_data) |
417 | | } |
418 | 0 | Backend::GPU => return Err(TruenoError::UnsupportedBackend(Backend::GPU)), |
419 | | Backend::Auto => { |
420 | 0 | return Err(TruenoError::UnsupportedBackend(Backend::Auto)); |
421 | | } |
422 | | #[allow(unreachable_patterns)] |
423 | 0 | _ => ScalarBackend::fma(&self.data, &b.data, &c.data, &mut result_data), |
424 | | } |
425 | | } |
426 | 0 | } |
427 | | |
428 | 0 | Ok(Vector { |
429 | 0 | data: result_data, |
430 | 0 | backend: self.backend, |
431 | 0 | }) |
432 | 0 | } |
433 | | } |
434 | | |
435 | | #[cfg(test)] |
436 | | mod tests { |
437 | | use super::*; |
438 | | |
439 | | // ===== Add Tests ===== |
440 | | |
441 | | #[test] |
442 | | fn test_add_basic() { |
443 | | let a = Vector::from_slice(&[1.0, 2.0, 3.0]); |
444 | | let b = Vector::from_slice(&[4.0, 5.0, 6.0]); |
445 | | let result = a.add(&b).unwrap(); |
446 | | assert_eq!(result.as_slice(), &[5.0, 7.0, 9.0]); |
447 | | } |
448 | | |
449 | | #[test] |
450 | | fn test_add_size_mismatch() { |
451 | | let a = Vector::from_slice(&[1.0, 2.0]); |
452 | | let b = Vector::from_slice(&[1.0, 2.0, 3.0]); |
453 | | let result = a.add(&b); |
454 | | assert!(result.is_err()); |
455 | | match result { |
456 | | Err(TruenoError::SizeMismatch { expected, actual }) => { |
457 | | assert_eq!(expected, 2); |
458 | | assert_eq!(actual, 3); |
459 | | } |
460 | | _ => panic!("Expected SizeMismatch error"), |
461 | | } |
462 | | } |
463 | | |
464 | | #[test] |
465 | | fn test_add_empty() { |
466 | | let a = Vector::from_slice(&[]); |
467 | | let b = Vector::from_slice(&[]); |
468 | | let result = a.add(&b).unwrap(); |
469 | | assert!(result.as_slice().is_empty()); |
470 | | } |
471 | | |
472 | | #[test] |
473 | | fn test_add_single_element() { |
474 | | let a = Vector::from_slice(&[1.5]); |
475 | | let b = Vector::from_slice(&[2.5]); |
476 | | let result = a.add(&b).unwrap(); |
477 | | assert!((result.as_slice()[0] - 4.0).abs() < 1e-6); |
478 | | } |
479 | | |
480 | | #[test] |
481 | | fn test_add_negatives() { |
482 | | let a = Vector::from_slice(&[-1.0, -2.0, -3.0]); |
483 | | let b = Vector::from_slice(&[1.0, 2.0, 3.0]); |
484 | | let result = a.add(&b).unwrap(); |
485 | | assert_eq!(result.as_slice(), &[0.0, 0.0, 0.0]); |
486 | | } |
487 | | |
488 | | #[test] |
489 | | fn test_add_large_array() { |
490 | | let n = 10000; |
491 | | let a = Vector::from_slice(&vec![1.0; n]); |
492 | | let b = Vector::from_slice(&vec![2.0; n]); |
493 | | let result = a.add(&b).unwrap(); |
494 | | for val in result.as_slice() { |
495 | | assert!((val - 3.0).abs() < 1e-6); |
496 | | } |
497 | | } |
498 | | |
499 | | // ===== Sub Tests ===== |
500 | | |
501 | | #[test] |
502 | | fn test_sub_basic() { |
503 | | let a = Vector::from_slice(&[5.0, 7.0, 9.0]); |
504 | | let b = Vector::from_slice(&[1.0, 2.0, 3.0]); |
505 | | let result = a.sub(&b).unwrap(); |
506 | | assert_eq!(result.as_slice(), &[4.0, 5.0, 6.0]); |
507 | | } |
508 | | |
509 | | #[test] |
510 | | fn test_sub_size_mismatch() { |
511 | | let a = Vector::from_slice(&[1.0, 2.0, 3.0]); |
512 | | let b = Vector::from_slice(&[1.0]); |
513 | | let result = a.sub(&b); |
514 | | assert!(result.is_err()); |
515 | | } |
516 | | |
517 | | #[test] |
518 | | fn test_sub_empty() { |
519 | | let a = Vector::from_slice(&[]); |
520 | | let b = Vector::from_slice(&[]); |
521 | | let result = a.sub(&b).unwrap(); |
522 | | assert!(result.as_slice().is_empty()); |
523 | | } |
524 | | |
525 | | #[test] |
526 | | fn test_sub_self() { |
527 | | let a = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0]); |
528 | | let result = a.sub(&a).unwrap(); |
529 | | for val in result.as_slice() { |
530 | | assert!((val - 0.0).abs() < 1e-6); |
531 | | } |
532 | | } |
533 | | |
534 | | // ===== Mul Tests ===== |
535 | | |
536 | | #[test] |
537 | | fn test_mul_basic() { |
538 | | let a = Vector::from_slice(&[1.0, 2.0, 3.0]); |
539 | | let b = Vector::from_slice(&[2.0, 3.0, 4.0]); |
540 | | let result = a.mul(&b).unwrap(); |
541 | | assert_eq!(result.as_slice(), &[2.0, 6.0, 12.0]); |
542 | | } |
543 | | |
544 | | #[test] |
545 | | fn test_mul_size_mismatch() { |
546 | | let a = Vector::from_slice(&[1.0]); |
547 | | let b = Vector::from_slice(&[1.0, 2.0]); |
548 | | let result = a.mul(&b); |
549 | | assert!(result.is_err()); |
550 | | } |
551 | | |
552 | | #[test] |
553 | | fn test_mul_empty() { |
554 | | let a = Vector::from_slice(&[]); |
555 | | let b = Vector::from_slice(&[]); |
556 | | let result = a.mul(&b).unwrap(); |
557 | | assert!(result.as_slice().is_empty()); |
558 | | } |
559 | | |
560 | | #[test] |
561 | | fn test_mul_by_zero() { |
562 | | let a = Vector::from_slice(&[1.0, 2.0, 3.0]); |
563 | | let b = Vector::from_slice(&[0.0, 0.0, 0.0]); |
564 | | let result = a.mul(&b).unwrap(); |
565 | | assert_eq!(result.as_slice(), &[0.0, 0.0, 0.0]); |
566 | | } |
567 | | |
568 | | #[test] |
569 | | fn test_mul_by_one() { |
570 | | let a = Vector::from_slice(&[5.0, 10.0, 15.0]); |
571 | | let b = Vector::from_slice(&[1.0, 1.0, 1.0]); |
572 | | let result = a.mul(&b).unwrap(); |
573 | | assert_eq!(result.as_slice(), &[5.0, 10.0, 15.0]); |
574 | | } |
575 | | |
576 | | // ===== Div Tests ===== |
577 | | |
578 | | #[test] |
579 | | fn test_div_basic() { |
580 | | let a = Vector::from_slice(&[4.0, 6.0, 8.0]); |
581 | | let b = Vector::from_slice(&[2.0, 2.0, 2.0]); |
582 | | let result = a.div(&b).unwrap(); |
583 | | assert_eq!(result.as_slice(), &[2.0, 3.0, 4.0]); |
584 | | } |
585 | | |
586 | | #[test] |
587 | | fn test_div_size_mismatch() { |
588 | | let a = Vector::from_slice(&[1.0, 2.0]); |
589 | | let b = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0]); |
590 | | let result = a.div(&b); |
591 | | assert!(result.is_err()); |
592 | | } |
593 | | |
594 | | #[test] |
595 | | fn test_div_empty() { |
596 | | let a = Vector::from_slice(&[]); |
597 | | let b = Vector::from_slice(&[]); |
598 | | let result = a.div(&b).unwrap(); |
599 | | assert!(result.as_slice().is_empty()); |
600 | | } |
601 | | |
602 | | #[test] |
603 | | fn test_div_by_one() { |
604 | | let a = Vector::from_slice(&[5.0, 10.0, 15.0]); |
605 | | let b = Vector::from_slice(&[1.0, 1.0, 1.0]); |
606 | | let result = a.div(&b).unwrap(); |
607 | | assert_eq!(result.as_slice(), &[5.0, 10.0, 15.0]); |
608 | | } |
609 | | |
610 | | #[test] |
611 | | fn test_div_by_zero_produces_inf() { |
612 | | let a = Vector::from_slice(&[1.0, 2.0]); |
613 | | let b = Vector::from_slice(&[0.0, 0.0]); |
614 | | let result = a.div(&b).unwrap(); |
615 | | assert!(result.as_slice()[0].is_infinite()); |
616 | | assert!(result.as_slice()[1].is_infinite()); |
617 | | } |
618 | | |
619 | | // ===== Scale Tests ===== |
620 | | |
621 | | #[test] |
622 | | fn test_scale_basic() { |
623 | | let a = Vector::from_slice(&[1.0, 2.0, 3.0]); |
624 | | let result = a.scale(2.0).unwrap(); |
625 | | assert_eq!(result.as_slice(), &[2.0, 4.0, 6.0]); |
626 | | } |
627 | | |
628 | | #[test] |
629 | | fn test_scale_empty() { |
630 | | let a = Vector::from_slice(&[]); |
631 | | let result = a.scale(5.0).unwrap(); |
632 | | assert!(result.as_slice().is_empty()); |
633 | | } |
634 | | |
635 | | #[test] |
636 | | fn test_scale_by_zero() { |
637 | | let a = Vector::from_slice(&[1.0, 2.0, 3.0]); |
638 | | let result = a.scale(0.0).unwrap(); |
639 | | assert_eq!(result.as_slice(), &[0.0, 0.0, 0.0]); |
640 | | } |
641 | | |
642 | | #[test] |
643 | | fn test_scale_by_one() { |
644 | | let a = Vector::from_slice(&[5.0, 10.0, 15.0]); |
645 | | let result = a.scale(1.0).unwrap(); |
646 | | assert_eq!(result.as_slice(), &[5.0, 10.0, 15.0]); |
647 | | } |
648 | | |
649 | | #[test] |
650 | | fn test_scale_negative() { |
651 | | let a = Vector::from_slice(&[1.0, -2.0, 3.0]); |
652 | | let result = a.scale(-1.0).unwrap(); |
653 | | assert_eq!(result.as_slice(), &[-1.0, 2.0, -3.0]); |
654 | | } |
655 | | |
656 | | // ===== FMA Tests ===== |
657 | | |
658 | | #[test] |
659 | | fn test_fma_basic() { |
660 | | let a = Vector::from_slice(&[1.0, 2.0, 3.0]); |
661 | | let b = Vector::from_slice(&[2.0, 2.0, 2.0]); |
662 | | let c = Vector::from_slice(&[1.0, 1.0, 1.0]); |
663 | | // a * b + c = [2+1, 4+1, 6+1] = [3, 5, 7] |
664 | | let result = a.fma(&b, &c).unwrap(); |
665 | | assert_eq!(result.as_slice(), &[3.0, 5.0, 7.0]); |
666 | | } |
667 | | |
668 | | #[test] |
669 | | fn test_fma_size_mismatch_b() { |
670 | | let a = Vector::from_slice(&[1.0, 2.0, 3.0]); |
671 | | let b = Vector::from_slice(&[2.0]); |
672 | | let c = Vector::from_slice(&[1.0, 1.0, 1.0]); |
673 | | let result = a.fma(&b, &c); |
674 | | assert!(result.is_err()); |
675 | | } |
676 | | |
677 | | #[test] |
678 | | fn test_fma_size_mismatch_c() { |
679 | | let a = Vector::from_slice(&[1.0, 2.0, 3.0]); |
680 | | let b = Vector::from_slice(&[2.0, 2.0, 2.0]); |
681 | | let c = Vector::from_slice(&[1.0]); |
682 | | let result = a.fma(&b, &c); |
683 | | assert!(result.is_err()); |
684 | | } |
685 | | |
686 | | #[test] |
687 | | fn test_fma_empty() { |
688 | | let a = Vector::from_slice(&[]); |
689 | | let b = Vector::from_slice(&[]); |
690 | | let c = Vector::from_slice(&[]); |
691 | | let result = a.fma(&b, &c).unwrap(); |
692 | | assert!(result.as_slice().is_empty()); |
693 | | } |
694 | | |
695 | | #[test] |
696 | | fn test_fma_multiply_by_zero() { |
697 | | let a = Vector::from_slice(&[5.0, 10.0, 15.0]); |
698 | | let b = Vector::from_slice(&[0.0, 0.0, 0.0]); |
699 | | let c = Vector::from_slice(&[1.0, 2.0, 3.0]); |
700 | | // a * 0 + c = c |
701 | | let result = a.fma(&b, &c).unwrap(); |
702 | | assert_eq!(result.as_slice(), &[1.0, 2.0, 3.0]); |
703 | | } |
704 | | |
705 | | #[test] |
706 | | fn test_fma_add_zero() { |
707 | | let a = Vector::from_slice(&[2.0, 3.0, 4.0]); |
708 | | let b = Vector::from_slice(&[3.0, 2.0, 1.0]); |
709 | | let c = Vector::from_slice(&[0.0, 0.0, 0.0]); |
710 | | // a * b + 0 = a * b |
711 | | let result = a.fma(&b, &c).unwrap(); |
712 | | assert_eq!(result.as_slice(), &[6.0, 6.0, 4.0]); |
713 | | } |
714 | | |
715 | | // ===== Backend Tests ===== |
716 | | |
717 | | #[test] |
718 | | fn test_add_scalar_backend() { |
719 | | let a = Vector::from_slice_with_backend(&[1.0, 2.0, 3.0], Backend::Scalar); |
720 | | let b = Vector::from_slice_with_backend(&[4.0, 5.0, 6.0], Backend::Scalar); |
721 | | let result = a.add(&b).unwrap(); |
722 | | assert_eq!(result.as_slice(), &[5.0, 7.0, 9.0]); |
723 | | } |
724 | | |
725 | | #[test] |
726 | | #[cfg(target_arch = "x86_64")] |
727 | | fn test_add_sse2_backend() { |
728 | | let a = Vector::from_slice_with_backend(&[1.0, 2.0, 3.0, 4.0], Backend::SSE2); |
729 | | let b = Vector::from_slice_with_backend(&[4.0, 5.0, 6.0, 7.0], Backend::SSE2); |
730 | | let result = a.add(&b).unwrap(); |
731 | | assert_eq!(result.as_slice(), &[5.0, 7.0, 9.0, 11.0]); |
732 | | } |
733 | | |
734 | | #[test] |
735 | | #[cfg(target_arch = "x86_64")] |
736 | | fn test_add_avx2_backend() { |
737 | | if !std::arch::is_x86_feature_detected!("avx2") { |
738 | | return; // Skip if AVX2 not available |
739 | | } |
740 | | let data: Vec<f32> = vec![1.0; 16]; |
741 | | let a = Vector::from_slice_with_backend(&data, Backend::AVX2); |
742 | | let b_data: Vec<f32> = vec![2.0; 16]; |
743 | | let b = Vector::from_slice_with_backend(&b_data, Backend::AVX2); |
744 | | let result = a.add(&b).unwrap(); |
745 | | for &val in result.as_slice() { |
746 | | assert!((val - 3.0).abs() < 1e-6); |
747 | | } |
748 | | } |
749 | | |
750 | | // ===== Edge Cases ===== |
751 | | |
752 | | #[test] |
753 | | fn test_add_non_aligned_size() { |
754 | | // Test with sizes that don't align to SIMD register widths |
755 | | let a = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]); // 7 elements |
756 | | let b = Vector::from_slice(&[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]); |
757 | | let result = a.add(&b).unwrap(); |
758 | | assert_eq!(result.as_slice(), &[2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]); |
759 | | } |
760 | | |
761 | | #[test] |
762 | | fn test_mul_preserves_sign() { |
763 | | let a = Vector::from_slice(&[2.0, -2.0, 2.0, -2.0]); |
764 | | let b = Vector::from_slice(&[3.0, 3.0, -3.0, -3.0]); |
765 | | let result = a.mul(&b).unwrap(); |
766 | | assert_eq!(result.as_slice(), &[6.0, -6.0, -6.0, 6.0]); |
767 | | } |
768 | | |
769 | | #[test] |
770 | | fn test_operations_with_special_floats() { |
771 | | let a = Vector::from_slice(&[f32::INFINITY, f32::NEG_INFINITY, 0.0]); |
772 | | let b = Vector::from_slice(&[1.0, 1.0, 1.0]); |
773 | | let result = a.add(&b).unwrap(); |
774 | | assert!(result.as_slice()[0].is_infinite()); |
775 | | assert!(result.as_slice()[1].is_infinite()); |
776 | | assert!((result.as_slice()[2] - 1.0).abs() < 1e-6); |
777 | | } |
778 | | } |