usearch 2.26.1 → 2.26.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/README.md +1 -1
  2. package/include/usearch/index.hpp +5 -1
  3. package/numkong/CITATION.cff +13 -9
  4. package/numkong/CONTRIBUTING.md +29 -24
  5. package/numkong/Cargo.toml +1 -1
  6. package/numkong/README.md +21 -7
  7. package/numkong/binding.gyp +47 -31
  8. package/numkong/build.rs +15 -5
  9. package/numkong/c/dispatch.h +0 -1
  10. package/numkong/c/parallel.c +139 -0
  11. package/numkong/c/parallel.h +47 -0
  12. package/numkong/cmake/toolchain-wasi.cmake +39 -5
  13. package/numkong/include/README.md +24 -17
  14. package/numkong/include/numkong/attention/sapphireamx.h +6 -6
  15. package/numkong/include/numkong/attention/sme.h +2 -2
  16. package/numkong/include/numkong/capabilities.h +2 -2
  17. package/numkong/include/numkong/cast/README.md +9 -7
  18. package/numkong/include/numkong/cast/loongsonasx.h +48 -40
  19. package/numkong/include/numkong/cast/serial.h +89 -11
  20. package/numkong/include/numkong/cast.h +3 -3
  21. package/numkong/include/numkong/curved/README.md +10 -10
  22. package/numkong/include/numkong/curved/neon.h +1 -0
  23. package/numkong/include/numkong/curved/neonbfdot.h +1 -0
  24. package/numkong/include/numkong/curved/smef64.h +1 -0
  25. package/numkong/include/numkong/dot/README.md +18 -18
  26. package/numkong/include/numkong/dot/alder.h +8 -8
  27. package/numkong/include/numkong/dot/haswell.h +8 -8
  28. package/numkong/include/numkong/dot/icelake.h +12 -12
  29. package/numkong/include/numkong/dot/loongsonasx.h +6 -6
  30. package/numkong/include/numkong/dot/neon.h +2 -1
  31. package/numkong/include/numkong/dot/neonbfdot.h +1 -0
  32. package/numkong/include/numkong/dot/neonfhm.h +1 -0
  33. package/numkong/include/numkong/dot/neonsdot.h +1 -0
  34. package/numkong/include/numkong/dot/powervsx.h +3 -3
  35. package/numkong/include/numkong/dot/serial.h +19 -6
  36. package/numkong/include/numkong/dot/skylake.h +4 -4
  37. package/numkong/include/numkong/dot/svehalf.h +1 -1
  38. package/numkong/include/numkong/dot/v128relaxed.h +24 -23
  39. package/numkong/include/numkong/dots/README.md +26 -26
  40. package/numkong/include/numkong/dots/genoa.h +3 -1
  41. package/numkong/include/numkong/dots/icelake.h +1 -0
  42. package/numkong/include/numkong/dots/powervsx.h +1 -1
  43. package/numkong/include/numkong/dots/serial.h +32 -33
  44. package/numkong/include/numkong/dots/sierra.h +4 -3
  45. package/numkong/include/numkong/each/README.md +5 -3
  46. package/numkong/include/numkong/each/sapphire.h +2 -0
  47. package/numkong/include/numkong/each/skylake.h +1 -0
  48. package/numkong/include/numkong/geospatial/README.md +6 -6
  49. package/numkong/include/numkong/geospatial/v128relaxed.h +1 -0
  50. package/numkong/include/numkong/maxsim/README.md +5 -5
  51. package/numkong/include/numkong/maxsim/genoa.h +4 -0
  52. package/numkong/include/numkong/maxsim/icelake.h +1 -0
  53. package/numkong/include/numkong/maxsim/serial.h +22 -9
  54. package/numkong/include/numkong/maxsim/sme.h +3 -2
  55. package/numkong/include/numkong/mesh/README.md +7 -4
  56. package/numkong/include/numkong/mesh/genoa.h +2 -2
  57. package/numkong/include/numkong/mesh/haswell.h +8 -8
  58. package/numkong/include/numkong/mesh/neon.h +6 -6
  59. package/numkong/include/numkong/mesh/neonbfdot.h +3 -2
  60. package/numkong/include/numkong/mesh/neonfhm.h +3 -2
  61. package/numkong/include/numkong/mesh/rvv.h +4 -4
  62. package/numkong/include/numkong/mesh/serial.h +17 -17
  63. package/numkong/include/numkong/mesh/skylake.h +4 -4
  64. package/numkong/include/numkong/mesh/v128relaxed.h +4 -4
  65. package/numkong/include/numkong/mesh.hpp +2 -2
  66. package/numkong/include/numkong/numkong.h +1 -1
  67. package/numkong/include/numkong/numkong.hpp +1 -1
  68. package/numkong/include/numkong/probability/README.md +17 -15
  69. package/numkong/include/numkong/probability/skylake.h +1 -1
  70. package/numkong/include/numkong/reduce/README.md +27 -21
  71. package/numkong/include/numkong/reduce/genoa.h +2 -1
  72. package/numkong/include/numkong/reduce/icelake.h +1 -0
  73. package/numkong/include/numkong/reduce/neon.h +1 -1
  74. package/numkong/include/numkong/reduce/serial.h +9 -9
  75. package/numkong/include/numkong/reduce/skylake.h +4 -4
  76. package/numkong/include/numkong/reduce.hpp +38 -0
  77. package/numkong/include/numkong/scalar/README.md +29 -28
  78. package/numkong/include/numkong/set/README.md +17 -14
  79. package/numkong/include/numkong/set/haswell.h +15 -16
  80. package/numkong/include/numkong/set/icelake.h +15 -16
  81. package/numkong/include/numkong/set/loongsonasx.h +12 -10
  82. package/numkong/include/numkong/set/neon.h +15 -19
  83. package/numkong/include/numkong/set/powervsx.h +15 -21
  84. package/numkong/include/numkong/set/serial.h +19 -20
  85. package/numkong/include/numkong/set/v128relaxed.h +10 -8
  86. package/numkong/include/numkong/sets/icelake.h +1 -0
  87. package/numkong/include/numkong/sparse/README.md +4 -4
  88. package/numkong/include/numkong/sparse/icelake.h +1 -0
  89. package/numkong/include/numkong/sparse/neon.h +1 -0
  90. package/numkong/include/numkong/sparse/serial.h +10 -3
  91. package/numkong/include/numkong/sparse/turin.h +1 -0
  92. package/numkong/include/numkong/spatial/README.md +25 -24
  93. package/numkong/include/numkong/spatial/alder.h +2 -1
  94. package/numkong/include/numkong/spatial/haswell.h +48 -40
  95. package/numkong/include/numkong/spatial/icelake.h +2 -1
  96. package/numkong/include/numkong/spatial/neon.h +54 -46
  97. package/numkong/include/numkong/spatial/neonbfdot.h +1 -1
  98. package/numkong/include/numkong/spatial/powervsx.h +54 -46
  99. package/numkong/include/numkong/spatial/serial.h +56 -46
  100. package/numkong/include/numkong/spatial/sierra.h +4 -3
  101. package/numkong/include/numkong/spatial/skylake.h +27 -22
  102. package/numkong/include/numkong/spatial/sve.h +6 -6
  103. package/numkong/include/numkong/spatial/svebfdot.h +1 -1
  104. package/numkong/include/numkong/spatial/svehalf.h +1 -1
  105. package/numkong/include/numkong/spatial/v128relaxed.h +36 -30
  106. package/numkong/include/numkong/spatials/README.md +15 -15
  107. package/numkong/include/numkong/spatials/genoa.h +3 -0
  108. package/numkong/include/numkong/spatials/graniteamx.h +2 -0
  109. package/numkong/include/numkong/spatials/icelake.h +1 -0
  110. package/numkong/include/numkong/spatials/sierra.h +1 -0
  111. package/numkong/include/numkong/tensor.hpp +236 -123
  112. package/numkong/include/numkong/trigonometry/README.md +4 -4
  113. package/numkong/include/numkong/trigonometry/serial.h +6 -6
  114. package/numkong/include/numkong/trigonometry/v128relaxed.h +1 -0
  115. package/numkong/include/numkong/types.hpp +46 -16
  116. package/numkong/include/numkong/vector.hpp +116 -23
  117. package/numkong/javascript/@numkong-darwin-arm64/package.json +1 -1
  118. package/numkong/javascript/@numkong-darwin-x64/package.json +1 -1
  119. package/numkong/javascript/@numkong-linux-arm64/package.json +1 -1
  120. package/numkong/javascript/@numkong-linux-x64/package.json +1 -1
  121. package/numkong/javascript/@numkong-win32-arm64/package.json +1 -1
  122. package/numkong/javascript/@numkong-win32-x64/package.json +1 -1
  123. package/numkong/javascript/README.md +18 -16
  124. package/numkong/javascript/numkong.c +70 -40
  125. package/numkong/package.json +8 -8
  126. package/numkong/rust/README.md +19 -9
  127. package/numkong/rust/cast.rs +52 -1
  128. package/numkong/rust/each.rs +480 -7
  129. package/numkong/rust/matrix.rs +877 -216
  130. package/numkong/rust/numkong.rs +12 -8
  131. package/numkong/rust/reduce.rs +433 -5
  132. package/numkong/rust/tensor.rs +450 -842
  133. package/numkong/rust/vector.rs +520 -101
  134. package/numkong/swift/README.md +5 -3
  135. package/numkong/test/README.md +4 -4
  136. package/numkong/test/test-wasi.mjs +17 -0
  137. package/numkong/test/test_each.cpp +30 -1
  138. package/numkong/test/test_each.py +74 -1
  139. package/numkong/test/test_reduce.cpp +51 -0
  140. package/numkong/test/test_tensor.cpp +2 -63
  141. package/numkong/test/test_tensor.py +93 -0
  142. package/package.json +1 -1
  143. package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
  144. package/prebuilds/linux-arm64/usearch.node +0 -0
  145. package/prebuilds/linux-x64/usearch.node +0 -0
  146. package/prebuilds/win32-x64/usearch.node +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "numkong",
3
- "version": "7.6.0",
3
+ "version": "7.8.1",
4
4
  "description": "Portable mixed-precision math, linear-algebra, & retrieval library with 2000+ SIMD kernels for x86, Arm, RISC-V, LoongArch, Power, & WebAssembly",
5
5
  "homepage": "https://github.com/ashvardanian/NumKong",
6
6
  "author": "Ash Vardanian",
@@ -72,7 +72,7 @@
72
72
  "@playwright/test": "^1.58.0",
73
73
  "@types/bindings": "^1.5.5",
74
74
  "@types/node": "^24.10.0",
75
- "node-gyp": "^11.5.0",
75
+ "node-gyp": "^13.0.1",
76
76
  "playwright": "^1.58.0",
77
77
  "http-server": "^14.1.1",
78
78
  "esbuild": "^0.25.0",
@@ -98,11 +98,11 @@
98
98
  "printWidth": 120
99
99
  },
100
100
  "optionalDependencies": {
101
- "@numkong/darwin-arm64": "7.6.0",
102
- "@numkong/darwin-x64": "7.6.0",
103
- "@numkong/linux-arm64": "7.6.0",
104
- "@numkong/linux-x64": "7.6.0",
105
- "@numkong/win32-arm64": "7.6.0",
106
- "@numkong/win32-x64": "7.6.0"
101
+ "@numkong/darwin-arm64": "7.8.1",
102
+ "@numkong/darwin-x64": "7.8.1",
103
+ "@numkong/linux-arm64": "7.8.1",
104
+ "@numkong/linux-x64": "7.8.1",
105
+ "@numkong/win32-arm64": "7.8.1",
106
+ "@numkong/win32-x64": "7.8.1"
107
107
  }
108
108
  }
@@ -42,7 +42,7 @@ The `parallel` feature is the intended native orchestration layer.
42
42
 
43
43
  ## Ecosystem Comparison
44
44
 
45
- | Feature | NumKong | [nalgebra](https://github.com/dimforge/nalgebra) | [ndarray](https://github.com/rust-ndarray/ndarray) |
45
+ | Feature | NumKong | [nalgebra][nalgebra] | [ndarray][ndarray] |
46
46
  | ---------------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------- |
47
47
  | Operation families | dots, distances, binary, probability, geospatial, curved, mesh, sparse, MaxSim, elementwise, reductions, cast, trig | linear algebra, decompositions | general n-dimensional arithmetic |
48
48
  | Precision | BFloat16 through sub-byte; automatic widening; Kahan summation; 0 ULP in Float32/Float64 | Float32/Float64 only; no widening; standard accuracy | Float32/Float64 only; no widening; standard accuracy |
@@ -52,6 +52,9 @@ The `parallel` feature is the intended native orchestration layer.
52
52
  | Memory model | Caller-owned; `Tensor`/`PackedMatrix` support custom allocators | Heap-allocated matrices; custom storage trait | Heap-allocated; no custom allocator support |
53
53
  | Host-side parallelism | row-range partitioning via reusable `ThreadPool`; no hidden threads | Rayon-based parallelism possible | Rayon-based parallelism possible |
54
54
 
55
+ [nalgebra]: https://github.com/dimforge/nalgebra
56
+ [ndarray]: https://github.com/rust-ndarray/ndarray
57
+
55
58
  NumKong validates `f16` and `bf16` interop against the `half` crate in its own test suite.
56
59
  That lets you move between ecosystem-standard half types and NumKong's kernel-facing wrappers without ambiguity.
57
60
 
@@ -235,7 +238,8 @@ assert!(jaccard_sets > 0.0 && jaccard_sets < 1.0); // |A ∩ B| / |A ∪ B|
235
238
  ```rust
236
239
  use numkong::{JensenShannon, KullbackLeibler};
237
240
 
238
- let p = [0.2_f32, 0.3, 0.5], q = [0.1_f32, 0.3, 0.6];
241
+ let p = [0.2_f32, 0.3, 0.5];
242
+ let q = [0.1_f32, 0.3, 0.6];
239
243
  let kl_forward = f32::kullbackleibler(&p, &q).unwrap();
240
244
  let kl_reverse = f32::kullbackleibler(&q, &p).unwrap();
241
245
  assert!(kl_forward != kl_reverse); // KLD is asymmetric
@@ -254,15 +258,19 @@ Outputs are meters.
254
258
  use numkong::{Haversine, Vincenty};
255
259
 
256
260
  // Statue of Liberty (40.6892°N, 74.0445°W) → Big Ben (51.5007°N, 0.1246°W)
257
- let liberty_lat = [0.7101605100_f64], liberty_lon = [-1.2923203180_f64];
258
- let big_ben_lat = [0.8988567821_f64], big_ben_lon = [-0.0021746802_f64];
261
+ let liberty_lat = [0.7101605100_f64];
262
+ let liberty_lon = [-1.2923203180_f64];
263
+ let big_ben_lat = [0.8988567821_f64];
264
+ let big_ben_lon = [-0.0021746802_f64];
259
265
  let mut distance = [0.0_f64; 1];
260
266
  f64::vincenty(&liberty_lat, &liberty_lon, &big_ben_lat, &big_ben_lon, &mut distance).unwrap(); // ≈ 5,589,857 m (ellipsoidal, baseline)
261
267
  f64::haversine(&liberty_lat, &liberty_lon, &big_ben_lat, &big_ben_lon, &mut distance).unwrap(); // ≈ 5,543,723 m (spherical, ~46 km less)
262
268
 
263
269
  // Vincenty in f32 — drifts ~2 m from f64
264
- let liberty_lat32 = [0.7101605100_f32], liberty_lon32 = [-1.2923203180_f32];
265
- let big_ben_lat32 = [0.8988567821_f32], big_ben_lon32 = [-0.0021746802_f32];
270
+ let liberty_lat32 = [0.7101605100_f32];
271
+ let liberty_lon32 = [-1.2923203180_f32];
272
+ let big_ben_lat32 = [0.8988567821_f32];
273
+ let big_ben_lon32 = [-0.0021746802_f32];
266
274
  let mut distance_f32 = [0.0_f32; 1];
267
275
  f32::vincenty(&liberty_lat32, &liberty_lon32, &big_ben_lat32, &big_ben_lon32, &mut distance_f32).unwrap(); // ≈ 5,589,859 m (+2 m drift)
268
276
  ```
@@ -466,12 +474,14 @@ Sparse helpers cover both sorted-index intersection and weighted sparse dot prod
466
474
  ```rust
467
475
  use numkong::{SparseIntersect, SparseDot};
468
476
 
469
- let a_idx = [1_u32, 3, 5, 7], b_idx = [3_u32, 4, 5, 8];
477
+ let a_idx = [1_u32, 3, 5, 7];
478
+ let b_idx = [3_u32, 4, 5, 8];
470
479
  let count = u32::sparse_intersection_size(&a_idx, &b_idx);
471
480
  assert_eq!(count, 2); // indices 3 and 5
472
481
 
473
- let a_weights = [1.0_f32, 2.0, 3.0, 4.0], b_weights = [5.0_f32, 6.0, 7.0, 8.0];
474
- let dot = u32::sparse_dot(&a_idx, &b_idx, &a_weights, &b_weights).unwrap();
482
+ let a_weights = [1.0_f32, 2.0, 3.0, 4.0];
483
+ let b_weights = [5.0_f32, 6.0, 7.0, 8.0];
484
+ let dot = u32::sparse_dot(&a_idx, &b_idx, &a_weights, &b_weights);
475
485
  assert!(dot > 0.0); // weighted dot over shared indices
476
486
  ```
477
487
 
@@ -1,9 +1,11 @@
1
- //! Type casting between scalar formats.
1
+ //! Type casting between scalar formats — slice traits and tensor-shaped wrappers.
2
2
  //!
3
3
  //! This module provides:
4
4
  //!
5
5
  //! - [`CastDtype`]: Trait marking types eligible for bulk casting
6
6
  //! - [`cast`]: Bulk-converts a slice from one scalar format to another
7
+ //! - [`CastOps`]: Tensor-shaped extension trait — auto-implemented on every
8
+ //! [`crate::tensor::TensorRef`] so any container can do `tensor.try_cast_dtype::<Destination>()`
7
9
 
8
10
  use crate::types::{bf16, bf16c, e2m3, e3m2, e4m3, e5m2, f16, f16c, f32c, f64c, StorageElement};
9
11
 
@@ -214,6 +216,34 @@ pub fn cast<S: CastDtype, D: CastDtype>(source: &[S], dest: &mut [D]) -> Option<
214
216
  Some(())
215
217
  }
216
218
 
219
+ // region: Tensor-shaped cast (moved from crate::tensor)
220
+
221
+ use crate::tensor::{try_reborrow_tensor_into, Global, Tensor, TensorError, TensorRef};
222
+
223
+ /// Extension trait: type casting for any [`TensorRef`] implementor.
224
+ pub trait CastOps<Source: Clone + CastDtype, const MAX_RANK: usize>:
225
+ TensorRef<Source, MAX_RANK>
226
+ {
227
+ fn try_cast_dtype<Destination: Clone + CastDtype>(
228
+ &self,
229
+ ) -> Result<Tensor<Destination, Global, MAX_RANK>, TensorError> {
230
+ self.view().try_cast_dtype()
231
+ }
232
+ }
233
+
234
+ impl<Source: Clone + CastDtype, const R: usize, C: TensorRef<Source, R>> CastOps<Source, R> for C {}
235
+
236
+ impl<Source: Clone + CastDtype, const MAX_RANK: usize> Tensor<Source, Global, MAX_RANK> {
237
+ pub fn try_cast_dtype_into<Destination: Clone + CastDtype>(
238
+ &self,
239
+ out: &mut Tensor<Destination, Global, MAX_RANK>,
240
+ ) -> Result<(), TensorError> {
241
+ try_reborrow_tensor_into(self, out, |view, span| view.try_cast_dtype_into(span))
242
+ }
243
+ }
244
+
245
+ // endregion: Tensor-shaped cast
246
+
217
247
  #[cfg(test)]
218
248
  mod tests {
219
249
  use super::*;
@@ -311,4 +341,25 @@ mod tests {
311
341
  assert_eq!(narrowed[1].re.to_f32(), bf16::from_f32(-3.5).to_f32());
312
342
  assert_eq!(narrowed[1].im.to_f32(), bf16::from_f32(4.25).to_f32());
313
343
  }
344
+
345
+ #[test]
346
+ fn cast_via_tensor_view_round_trip() {
347
+ // Exercises `CastOps::try_cast_dtype` on a strided `TensorView`,
348
+ // mirroring how callers reach the trait through the tensor-shaped wrapper.
349
+ use crate::tensor::{SliceRange, Tensor};
350
+ let data: Vec<f32> = (0..12).map(|i| i as f32).collect();
351
+ let source = Tensor::<f32>::try_from_slice(&data, &[3, 4]).unwrap();
352
+ let even_columns = source
353
+ .slice(&[SliceRange::full(), SliceRange::range_step(0, 4, 2)])
354
+ .unwrap();
355
+
356
+ let widened = even_columns.try_cast_dtype::<f64>().unwrap();
357
+ assert_eq!(widened.shape(), &[3, 2]);
358
+ assert_eq!(widened.as_slice(), &[0.0, 2.0, 4.0, 6.0, 8.0, 10.0]);
359
+
360
+ let complexified = even_columns.try_cast_dtype::<f32c>().unwrap();
361
+ assert_eq!(complexified.shape(), &[3, 2]);
362
+ assert_eq!(complexified.as_slice()[0], f32c::from_real_imag(0.0, 0.0));
363
+ assert_eq!(complexified.as_slice()[5], f32c::from_real_imag(10.0, 0.0));
364
+ }
314
365
  }
@@ -1,13 +1,18 @@
1
- //! Elementwise operations and trigonometry.
1
+ //! Elementwise operations and trigonometry — slice traits and tensor-shaped wrappers.
2
2
  //!
3
3
  //! This module provides:
4
4
  //!
5
- //! - [`EachSin`], [`EachCos`], [`EachATan`]: Trigonometric functions
6
- //! - [`EachScale`]: Linear scaling (alpha * x + beta)
7
- //! - [`EachSum`]: Elementwise addition of two vectors
8
- //! - [`EachBlend`]: Weighted blend of two vectors
9
- //! - [`EachFMA`]: Fused multiply-add (a * alpha + b * beta)
10
- //! - [`Trigonometry`]: Blanket trait combining `EachSin + EachCos + EachATan`
5
+ //! - Slice-level traits:
6
+ //! - [`EachScale`]: Linear scaling (alpha * x + beta)
7
+ //! - [`EachSum`]: Elementwise addition of two vectors
8
+ //! - [`EachBlend`]: Weighted blend of two vectors
9
+ //! - [`EachFMA`]: Fused multiply-add (a * alpha + b * beta)
10
+ //! - [`EachSin`], [`EachCos`], [`EachATan`]: Trigonometric functions
11
+ //! - [`Trigonometry`]: Blanket trait combining `EachSin + EachCos + EachATan`
12
+ //! - Tensor-shaped extension traits (auto-implemented on every [`crate::tensor::TensorRef`]):
13
+ //! - [`ScaleOps`], [`SumOps`], [`BlendOps`], [`FmaOps`]: Tensor wrappers around the slice traits
14
+ //! - [`TrigSinOps`], [`TrigCosOps`], [`TrigAtanOps`]: Tensor wrappers around the trig traits
15
+ //! - [`AllCloseOps`]: Tolerance-based equality for any [`crate::tensor::TensorRef`]
11
16
  //!
12
17
  //! # In-Place vs Allocating Semantics
13
18
  //!
@@ -43,6 +48,9 @@
43
48
  //! assert_eq!(output, [2.5, 4.5, 6.5, 8.5]);
44
49
  //! ```
45
50
 
51
+ use crate::tensor::{
52
+ try_reborrow_tensor_inplace, try_reborrow_tensor_into, Global, Tensor, TensorError, TensorRef,
53
+ };
46
54
  use crate::types::{bf16, bf16c, e2m3, e3m2, e4m3, e5m2, f16, f16c, f32c, f64c, StorageElement};
47
55
 
48
56
  #[link(name = "numkong")]
@@ -2403,6 +2411,395 @@ impl EachFMA for bf16c {
2403
2411
  pub trait Trigonometry: EachSin + EachCos + EachATan {}
2404
2412
  impl<Scalar: EachSin + EachCos + EachATan> Trigonometry for Scalar {}
2405
2413
 
2414
+ // region: Tensor-shaped trigonometry (moved from crate::tensor)
2415
+
2416
+ /// Extension trait: element-wise sine for any [`TensorRef`] implementor.
2417
+ pub trait TrigSinOps<Scalar: Clone + EachSin, const MAX_RANK: usize>:
2418
+ TensorRef<Scalar, MAX_RANK>
2419
+ {
2420
+ fn try_sin(&self) -> Result<Tensor<Scalar, Global, MAX_RANK>, TensorError> {
2421
+ self.view().try_sin()
2422
+ }
2423
+ }
2424
+
2425
+ impl<Scalar: Clone + EachSin, const R: usize, C: TensorRef<Scalar, R>> TrigSinOps<Scalar, R> for C {}
2426
+
2427
+ /// Extension trait: element-wise cosine for any [`TensorRef`] implementor.
2428
+ pub trait TrigCosOps<Scalar: Clone + EachCos, const MAX_RANK: usize>:
2429
+ TensorRef<Scalar, MAX_RANK>
2430
+ {
2431
+ fn try_cos(&self) -> Result<Tensor<Scalar, Global, MAX_RANK>, TensorError> {
2432
+ self.view().try_cos()
2433
+ }
2434
+ }
2435
+
2436
+ impl<Scalar: Clone + EachCos, const R: usize, C: TensorRef<Scalar, R>> TrigCosOps<Scalar, R> for C {}
2437
+
2438
+ /// Extension trait: element-wise arctangent for any [`TensorRef`] implementor.
2439
+ pub trait TrigAtanOps<Scalar: Clone + EachATan, const MAX_RANK: usize>:
2440
+ TensorRef<Scalar, MAX_RANK>
2441
+ {
2442
+ fn try_atan(&self) -> Result<Tensor<Scalar, Global, MAX_RANK>, TensorError> {
2443
+ self.view().try_atan()
2444
+ }
2445
+ }
2446
+
2447
+ impl<Scalar: Clone + EachATan, const R: usize, C: TensorRef<Scalar, R>> TrigAtanOps<Scalar, R>
2448
+ for C
2449
+ {
2450
+ }
2451
+
2452
+ impl<Scalar: Clone + EachSin, const MAX_RANK: usize> Tensor<Scalar, Global, MAX_RANK> {
2453
+ /// Element-wise sine: result\[i\] = sin(self\[i\])
2454
+ pub fn sin(&self) -> Result<Tensor<Scalar, Global, MAX_RANK>, TensorError> {
2455
+ self.view().try_sin()
2456
+ }
2457
+
2458
+ /// Element-wise sine in-place (infallible — self vs self always matches).
2459
+ pub fn sin_inplace(&mut self) {
2460
+ try_reborrow_tensor_inplace(self, |view, span| view.try_sin_into(span))
2461
+ .expect("inplace trig op on self cannot fail")
2462
+ }
2463
+
2464
+ pub fn try_sin_inplace(&mut self) -> Result<(), TensorError> {
2465
+ self.sin_inplace();
2466
+ Ok(())
2467
+ }
2468
+ }
2469
+
2470
+ impl<Scalar: Clone + EachCos, const MAX_RANK: usize> Tensor<Scalar, Global, MAX_RANK> {
2471
+ /// Element-wise cosine: result\[i\] = cos(self\[i\])
2472
+ pub fn cos(&self) -> Result<Tensor<Scalar, Global, MAX_RANK>, TensorError> {
2473
+ self.view().try_cos()
2474
+ }
2475
+
2476
+ /// Element-wise cosine in-place (infallible — self vs self always matches).
2477
+ pub fn cos_inplace(&mut self) {
2478
+ try_reborrow_tensor_inplace(self, |view, span| view.try_cos_into(span))
2479
+ .expect("inplace trig op on self cannot fail")
2480
+ }
2481
+
2482
+ pub fn try_cos_inplace(&mut self) -> Result<(), TensorError> {
2483
+ self.cos_inplace();
2484
+ Ok(())
2485
+ }
2486
+ }
2487
+
2488
+ impl<Scalar: Clone + EachATan, const MAX_RANK: usize> Tensor<Scalar, Global, MAX_RANK> {
2489
+ /// Element-wise arctangent: result\[i\] = atan(self\[i\])
2490
+ pub fn atan(&self) -> Result<Tensor<Scalar, Global, MAX_RANK>, TensorError> {
2491
+ self.view().try_atan()
2492
+ }
2493
+
2494
+ /// Element-wise arctangent in-place (infallible — self vs self always matches).
2495
+ pub fn atan_inplace(&mut self) {
2496
+ try_reborrow_tensor_inplace(self, |view, span| view.try_atan_into(span))
2497
+ .expect("inplace trig op on self cannot fail")
2498
+ }
2499
+
2500
+ pub fn try_atan_inplace(&mut self) -> Result<(), TensorError> {
2501
+ self.atan_inplace();
2502
+ Ok(())
2503
+ }
2504
+ }
2505
+
2506
+ // endregion: Tensor-shaped trigonometry
2507
+
2508
+ // region: Tensor-shaped tolerance equality (moved from crate::tensor)
2509
+
2510
+ use crate::types::{is_close, FloatConvertible, NumberLike};
2511
+
2512
+ /// Extension trait: tolerance-based equality for any [`TensorRef`] implementor.
2513
+ ///
2514
+ /// Uses the formula `|a - b| <= atol + rtol * |b|` per element.
2515
+ /// Returns `false` if shapes differ.
2516
+ pub trait AllCloseOps<Scalar: FloatConvertible, const MAX_RANK: usize>:
2517
+ TensorRef<Scalar, MAX_RANK>
2518
+ where
2519
+ Scalar::DimScalar: NumberLike,
2520
+ {
2521
+ fn allclose(
2522
+ &self,
2523
+ other: &(impl TensorRef<Scalar, MAX_RANK> + ?Sized),
2524
+ atol: f64,
2525
+ rtol: f64,
2526
+ ) -> bool {
2527
+ let a = self.view();
2528
+ let b = other.view();
2529
+ a.ndim() == b.ndim()
2530
+ && a.shape() == b.shape()
2531
+ && a.iter()
2532
+ .dims()
2533
+ .zip(b.iter().dims())
2534
+ .all(|(x, y)| is_close((*x).to_f64(), (*y).to_f64(), atol, rtol))
2535
+ }
2536
+ }
2537
+
2538
+ impl<C, Scalar: FloatConvertible, const R: usize> AllCloseOps<Scalar, R> for C
2539
+ where
2540
+ C: TensorRef<Scalar, R>,
2541
+ Scalar::DimScalar: NumberLike,
2542
+ {
2543
+ }
2544
+
2545
+ // endregion: Tensor-shaped tolerance equality
2546
+
2547
+ // region: Tensor-shaped scale / sum / blend / fma (moved from crate::tensor)
2548
+
2549
+ /// Extension trait: scalar arithmetic for any [`TensorRef`] implementor.
2550
+ pub trait ScaleOps<Scalar: Clone + EachScale, const MAX_RANK: usize>:
2551
+ TensorRef<Scalar, MAX_RANK>
2552
+ where
2553
+ Scalar::Scalar: From<f32> + core::ops::Mul<Output = Scalar::Scalar> + Copy,
2554
+ {
2555
+ fn try_add_scalar(
2556
+ &self,
2557
+ scalar: Scalar::Scalar,
2558
+ ) -> Result<Tensor<Scalar, Global, MAX_RANK>, TensorError> {
2559
+ self.view().try_add_scalar(scalar)
2560
+ }
2561
+
2562
+ fn try_sub_scalar(
2563
+ &self,
2564
+ scalar: Scalar::Scalar,
2565
+ ) -> Result<Tensor<Scalar, Global, MAX_RANK>, TensorError> {
2566
+ self.view().try_sub_scalar(scalar)
2567
+ }
2568
+
2569
+ fn try_mul_scalar(
2570
+ &self,
2571
+ scalar: Scalar::Scalar,
2572
+ ) -> Result<Tensor<Scalar, Global, MAX_RANK>, TensorError> {
2573
+ self.view().try_mul_scalar(scalar)
2574
+ }
2575
+ }
2576
+
2577
+ impl<Scalar: Clone + EachScale, const R: usize, C: TensorRef<Scalar, R>> ScaleOps<Scalar, R> for C where
2578
+ Scalar::Scalar: From<f32> + core::ops::Mul<Output = Scalar::Scalar> + Copy
2579
+ {
2580
+ }
2581
+
2582
+ impl<Scalar: Clone + EachScale, const MAX_RANK: usize> Tensor<Scalar, Global, MAX_RANK>
2583
+ where
2584
+ Scalar::Scalar: From<f32> + core::ops::Mul<Output = Scalar::Scalar> + Copy,
2585
+ {
2586
+ pub fn try_add_scalar_into(
2587
+ &self,
2588
+ scalar: Scalar::Scalar,
2589
+ out: &mut Tensor<Scalar, Global, MAX_RANK>,
2590
+ ) -> Result<(), TensorError> {
2591
+ try_reborrow_tensor_into(self, out, |view, span| {
2592
+ view.try_add_scalar_into(scalar, span)
2593
+ })
2594
+ }
2595
+
2596
+ pub fn try_sub_scalar_into(
2597
+ &self,
2598
+ scalar: Scalar::Scalar,
2599
+ out: &mut Tensor<Scalar, Global, MAX_RANK>,
2600
+ ) -> Result<(), TensorError> {
2601
+ try_reborrow_tensor_into(self, out, |view, span| {
2602
+ view.try_sub_scalar_into(scalar, span)
2603
+ })
2604
+ }
2605
+
2606
+ pub fn try_mul_scalar_into(
2607
+ &self,
2608
+ scalar: Scalar::Scalar,
2609
+ out: &mut Tensor<Scalar, Global, MAX_RANK>,
2610
+ ) -> Result<(), TensorError> {
2611
+ try_reborrow_tensor_into(self, out, |view, span| {
2612
+ view.try_mul_scalar_into(scalar, span)
2613
+ })
2614
+ }
2615
+
2616
+ pub fn try_add_scalar_inplace(&mut self, scalar: Scalar::Scalar) -> Result<(), TensorError> {
2617
+ self.add_scalar_inplace(scalar);
2618
+ Ok(())
2619
+ }
2620
+
2621
+ pub fn try_sub_scalar_inplace(&mut self, scalar: Scalar::Scalar) -> Result<(), TensorError> {
2622
+ self.sub_scalar_inplace(scalar);
2623
+ Ok(())
2624
+ }
2625
+
2626
+ pub fn try_mul_scalar_inplace(&mut self, scalar: Scalar::Scalar) -> Result<(), TensorError> {
2627
+ self.mul_scalar_inplace(scalar);
2628
+ Ok(())
2629
+ }
2630
+
2631
+ /// Element-wise add scalar in-place (infallible — self vs self always matches).
2632
+ pub fn add_scalar_inplace(&mut self, scalar: Scalar::Scalar) {
2633
+ try_reborrow_tensor_inplace(self, |view, span| view.try_add_scalar_into(scalar, span))
2634
+ .expect("inplace scalar op on self cannot fail")
2635
+ }
2636
+
2637
+ /// Element-wise subtract scalar in-place (infallible — self vs self always matches).
2638
+ pub fn sub_scalar_inplace(&mut self, scalar: Scalar::Scalar) {
2639
+ try_reborrow_tensor_inplace(self, |view, span| view.try_sub_scalar_into(scalar, span))
2640
+ .expect("inplace scalar op on self cannot fail")
2641
+ }
2642
+
2643
+ /// Element-wise multiply scalar in-place (infallible — self vs self always matches).
2644
+ pub fn mul_scalar_inplace(&mut self, scalar: Scalar::Scalar) {
2645
+ try_reborrow_tensor_inplace(self, |view, span| view.try_mul_scalar_into(scalar, span))
2646
+ .expect("inplace scalar op on self cannot fail")
2647
+ }
2648
+ }
2649
+
2650
+ impl<Scalar: Clone + EachScale, const MAX_RANK: usize> core::ops::AddAssign<Scalar::Scalar>
2651
+ for Tensor<Scalar, Global, MAX_RANK>
2652
+ where
2653
+ Scalar::Scalar: From<f32> + core::ops::Mul<Output = Scalar::Scalar> + Copy,
2654
+ {
2655
+ fn add_assign(&mut self, scalar: Scalar::Scalar) {
2656
+ self.add_scalar_inplace(scalar);
2657
+ }
2658
+ }
2659
+
2660
+ impl<Scalar: Clone + EachScale, const MAX_RANK: usize> core::ops::SubAssign<Scalar::Scalar>
2661
+ for Tensor<Scalar, Global, MAX_RANK>
2662
+ where
2663
+ Scalar::Scalar: From<f32> + core::ops::Mul<Output = Scalar::Scalar> + Copy,
2664
+ {
2665
+ fn sub_assign(&mut self, scalar: Scalar::Scalar) {
2666
+ self.sub_scalar_inplace(scalar);
2667
+ }
2668
+ }
2669
+
2670
+ impl<Scalar: Clone + EachScale, const MAX_RANK: usize> core::ops::MulAssign<Scalar::Scalar>
2671
+ for Tensor<Scalar, Global, MAX_RANK>
2672
+ where
2673
+ Scalar::Scalar: From<f32> + core::ops::Mul<Output = Scalar::Scalar> + Copy,
2674
+ {
2675
+ fn mul_assign(&mut self, scalar: Scalar::Scalar) {
2676
+ self.mul_scalar_inplace(scalar);
2677
+ }
2678
+ }
2679
+
2680
+ /// Extension trait: element-wise addition for any [`TensorRef`] implementor.
2681
+ pub trait SumOps<Scalar: Clone + EachSum, const MAX_RANK: usize>:
2682
+ TensorRef<Scalar, MAX_RANK>
2683
+ {
2684
+ fn try_add_tensor(
2685
+ &self,
2686
+ other: &(impl TensorRef<Scalar, MAX_RANK> + ?Sized),
2687
+ ) -> Result<Tensor<Scalar, Global, MAX_RANK>, TensorError> {
2688
+ self.view().try_add_tensor(&other.view())
2689
+ }
2690
+ }
2691
+
2692
+ impl<Scalar: Clone + EachSum, const R: usize, C: TensorRef<Scalar, R>> SumOps<Scalar, R> for C {}
2693
+
2694
+ impl<Scalar: Clone + EachSum, const MAX_RANK: usize> Tensor<Scalar, Global, MAX_RANK> {
2695
+ pub fn try_add_tensor_into(
2696
+ &self,
2697
+ other: &Tensor<Scalar, Global, MAX_RANK>,
2698
+ out: &mut Tensor<Scalar, Global, MAX_RANK>,
2699
+ ) -> Result<(), TensorError> {
2700
+ try_reborrow_tensor_into(self, out, |view, span| {
2701
+ view.try_add_tensor_into(&other.view(), span)
2702
+ })
2703
+ }
2704
+
2705
+ pub fn try_add_tensor_inplace(
2706
+ &mut self,
2707
+ other: &Tensor<Scalar, Global, MAX_RANK>,
2708
+ ) -> Result<(), TensorError> {
2709
+ try_reborrow_tensor_inplace(self, |view, span| {
2710
+ view.try_add_tensor_into(&other.view(), span)
2711
+ })
2712
+ }
2713
+ }
2714
+
2715
+ /// Extension trait: element-wise subtraction for any [`TensorRef`] implementor.
2716
+ pub trait BlendOps<Scalar: Clone + EachBlend, const MAX_RANK: usize>:
2717
+ TensorRef<Scalar, MAX_RANK>
2718
+ where
2719
+ Scalar::Scalar: From<f32> + Copy,
2720
+ {
2721
+ fn try_sub_tensor(
2722
+ &self,
2723
+ other: &(impl TensorRef<Scalar, MAX_RANK> + ?Sized),
2724
+ ) -> Result<Tensor<Scalar, Global, MAX_RANK>, TensorError> {
2725
+ self.view().try_sub_tensor(&other.view())
2726
+ }
2727
+ }
2728
+
2729
+ impl<Scalar: Clone + EachBlend, const R: usize, C: TensorRef<Scalar, R>> BlendOps<Scalar, R> for C where
2730
+ Scalar::Scalar: From<f32> + Copy
2731
+ {
2732
+ }
2733
+
2734
+ impl<Scalar: Clone + EachBlend, const MAX_RANK: usize> Tensor<Scalar, Global, MAX_RANK>
2735
+ where
2736
+ Scalar::Scalar: From<f32> + Copy,
2737
+ {
2738
+ pub fn try_sub_tensor_into(
2739
+ &self,
2740
+ other: &Tensor<Scalar, Global, MAX_RANK>,
2741
+ out: &mut Tensor<Scalar, Global, MAX_RANK>,
2742
+ ) -> Result<(), TensorError> {
2743
+ try_reborrow_tensor_into(self, out, |view, span| {
2744
+ view.try_sub_tensor_into(&other.view(), span)
2745
+ })
2746
+ }
2747
+
2748
+ pub fn try_sub_tensor_inplace(
2749
+ &mut self,
2750
+ other: &Tensor<Scalar, Global, MAX_RANK>,
2751
+ ) -> Result<(), TensorError> {
2752
+ try_reborrow_tensor_inplace(self, |view, span| {
2753
+ view.try_sub_tensor_into(&other.view(), span)
2754
+ })
2755
+ }
2756
+ }
2757
+
2758
+ /// Extension trait: element-wise multiplication for any [`TensorRef`] implementor.
2759
+ pub trait FmaOps<Scalar: Clone + EachFMA, const MAX_RANK: usize>:
2760
+ TensorRef<Scalar, MAX_RANK>
2761
+ where
2762
+ Scalar::Scalar: From<f32> + Copy,
2763
+ {
2764
+ fn try_mul_tensor(
2765
+ &self,
2766
+ other: &(impl TensorRef<Scalar, MAX_RANK> + ?Sized),
2767
+ ) -> Result<Tensor<Scalar, Global, MAX_RANK>, TensorError> {
2768
+ self.view().try_mul_tensor(&other.view())
2769
+ }
2770
+ }
2771
+
2772
+ impl<Scalar: Clone + EachFMA, const R: usize, C: TensorRef<Scalar, R>> FmaOps<Scalar, R> for C where
2773
+ Scalar::Scalar: From<f32> + Copy
2774
+ {
2775
+ }
2776
+
2777
+ impl<Scalar: Clone + EachFMA, const MAX_RANK: usize> Tensor<Scalar, Global, MAX_RANK>
2778
+ where
2779
+ Scalar::Scalar: From<f32> + Copy,
2780
+ {
2781
+ pub fn try_mul_tensor_into(
2782
+ &self,
2783
+ other: &Tensor<Scalar, Global, MAX_RANK>,
2784
+ out: &mut Tensor<Scalar, Global, MAX_RANK>,
2785
+ ) -> Result<(), TensorError> {
2786
+ try_reborrow_tensor_into(self, out, |view, span| {
2787
+ view.try_mul_tensor_into(&other.view(), span)
2788
+ })
2789
+ }
2790
+
2791
+ pub fn try_mul_tensor_inplace(
2792
+ &mut self,
2793
+ other: &Tensor<Scalar, Global, MAX_RANK>,
2794
+ ) -> Result<(), TensorError> {
2795
+ try_reborrow_tensor_inplace(self, |view, span| {
2796
+ view.try_mul_tensor_into(&other.view(), span)
2797
+ })
2798
+ }
2799
+ }
2800
+
2801
+ // endregion: Tensor-shaped scale / sum / blend / fma
2802
+
2406
2803
  #[cfg(test)]
2407
2804
  mod tests {
2408
2805
  use super::*;
@@ -2718,4 +3115,80 @@ mod tests {
2718
3115
  }
2719
3116
 
2720
3117
  // endregion
3118
+
3119
+ // region: tensor-shaped wrappers (ScaleOps / SumOps / TrigSinOps)
3120
+
3121
+ #[test]
3122
+ fn tensor_add_tensor_via_sum_ops() {
3123
+ use crate::tensor::{SliceRange, Tensor};
3124
+ let data: Vec<f32> = (0..12).map(|i| i as f32).collect();
3125
+ let left = Tensor::<f32>::try_from_slice(&data, &[3, 4]).unwrap();
3126
+ let right = Tensor::<f32>::try_full(&[3, 4], 2.0).unwrap();
3127
+
3128
+ let left_even = left
3129
+ .slice(&[SliceRange::full(), SliceRange::range_step(0, 4, 2)])
3130
+ .unwrap();
3131
+ let right_even = right
3132
+ .slice(&[SliceRange::full(), SliceRange::range_step(0, 4, 2)])
3133
+ .unwrap();
3134
+
3135
+ let added = left_even.try_add_tensor(&right_even).unwrap();
3136
+ assert_eq!(added.shape(), &[3, 2]);
3137
+ assert_eq!(added.as_slice(), &[2.0, 4.0, 6.0, 8.0, 10.0, 12.0]);
3138
+ }
3139
+
3140
+ #[test]
3141
+ fn tensor_add_tensor_into_owning_destination() {
3142
+ use crate::tensor::Tensor;
3143
+ let data: Vec<f32> = (0..12).map(|i| i as f32).collect();
3144
+ let left = Tensor::<f32>::try_from_slice(&data, &[3, 4]).unwrap();
3145
+ let right = Tensor::<f32>::try_full(&[3, 4], 2.0).unwrap();
3146
+
3147
+ let mut out = Tensor::<f32>::try_full(&[3, 4], 0.0).unwrap();
3148
+ left.try_add_tensor_into(&right, &mut out).unwrap();
3149
+ assert_eq!(out.as_slice()[0], 2.0);
3150
+ assert_eq!(out.as_slice()[11], 13.0);
3151
+ }
3152
+
3153
+ #[test]
3154
+ fn tensor_mul_scalar_via_scale_ops() {
3155
+ use crate::tensor::{SliceRange, Tensor};
3156
+ let data: Vec<f32> = (0..12).map(|i| i as f32).collect();
3157
+ let source = Tensor::<f32>::try_from_slice(&data, &[3, 4]).unwrap();
3158
+ let even = source
3159
+ .slice(&[SliceRange::full(), SliceRange::range_step(0, 4, 2)])
3160
+ .unwrap();
3161
+ let scaled = even.try_mul_scalar(0.5).unwrap();
3162
+ assert_eq!(scaled.as_slice(), &[0.0, 1.0, 2.0, 3.0, 4.0, 5.0]);
3163
+ }
3164
+
3165
+ #[test]
3166
+ fn tensor_add_scalar_inplace_via_scale_ops() {
3167
+ use crate::tensor::Tensor;
3168
+ let data: Vec<f32> = (0..12).map(|i| i as f32).collect();
3169
+ let mut tensor = Tensor::<f32>::try_from_slice(&data, &[3, 4]).unwrap();
3170
+ tensor.try_add_scalar_inplace(1.0).unwrap();
3171
+ assert_eq!(tensor.as_slice()[0], 1.0);
3172
+ assert_eq!(tensor.as_slice()[11], 12.0);
3173
+ }
3174
+
3175
+ #[test]
3176
+ fn tensor_sin_into_via_trig_sin_ops() {
3177
+ use crate::tensor::{SliceRange, Tensor};
3178
+ let data: Vec<f32> = (0..12).map(|i| i as f32).collect();
3179
+ let source = Tensor::<f32>::try_from_slice(&data, &[3, 4]).unwrap();
3180
+ let even = source
3181
+ .slice(&[SliceRange::full(), SliceRange::range_step(0, 4, 2)])
3182
+ .unwrap();
3183
+ let mut sin_out = Tensor::<f32>::try_full(&[3, 2], 0.0).unwrap();
3184
+ {
3185
+ let mut span = sin_out.span();
3186
+ even.try_sin_into(&mut span).unwrap();
3187
+ }
3188
+ assert_eq!(sin_out.shape(), &[3, 2]);
3189
+ // First element is sin(0) which is exactly 0.
3190
+ assert!((sin_out.as_slice()[0] - 0.0).abs() < 1e-6);
3191
+ }
3192
+
3193
+ // endregion
2721
3194
  }