usearch 2.26.1 → 2.26.3

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
@@ -43,7 +43,7 @@ Packing handles internal layout itself.
43
43
 
44
44
  ## Ecosystem Comparison
45
45
 
46
- | Feature | NumKong | Accelerate/vDSP | [MLX](https://github.com/ml-explore/mlx) |
46
+ | Feature | NumKong | Accelerate/vDSP | [MLX][mlx] |
47
47
  | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------ |
48
48
  | Operation families | dots, distances, binary, geospatial, MaxSim | dots, distances, FFT, some BLAS | matmul, elementwise, reductions, FFT |
49
49
  | Precision | BFloat16 through sub-byte — Float8, Float6, packed bits; automatic widening; Kahan summation; 0 ULP in Float32/Float64 | Float32/Float64, limited Float16; no auto-widening; IEEE defaults | Float16/BFloat16/Float32; no Float8 or sub-byte; backend-dependent |
@@ -53,6 +53,8 @@ Packing handles internal layout itself.
53
53
  | Collection-based API | works with any `RandomAccessCollection` conforming type | pointer-based vDSP functions | `MLXArray`-based |
54
54
  | Memory model | caller-owned buffers; `Tensor`/`PackedMatrix` own their storage | caller-managed via `UnsafePointer` | graph-managed; implicit allocation and caching |
55
55
 
56
+ [mlx]: https://github.com/ml-explore/mlx
57
+
56
58
  ## Installation
57
59
 
58
60
  Add NumKong to `Package.swift`:
@@ -74,7 +76,7 @@ Then add the product to your target:
74
76
  )
75
77
  ```
76
78
 
77
- The root package manifest already exposes the `NumKong` and `CNumKong` targets.
79
+ The root package manifest already exposes the `NumKong` and `CNumKongDispatch` library products.
78
80
  Xcode package integration uses the same URL.
79
81
 
80
82
  ## Collection-Based Dot Products
@@ -415,7 +417,7 @@ let haversine = haversine(aLat: libertyLat, aLon: libertyLon, bLat: bigBenLat, b
415
417
 
416
418
  The low-level `UnsafeBufferPointer` static methods on `Float64` and `Float32` remain available for zero-copy use cases.
417
419
 
418
- ## Runtime Capabilities and Thread Configuration
420
+ ## Runtime Capabilities
419
421
 
420
422
  Capability detection is exposed directly for diagnostics and tests:
421
423
 
@@ -70,10 +70,10 @@ Foreign flag mapping for muscle-memory compatibility:
70
70
 
71
71
  Each kernel is assigned a __comparison family__ that determines which error metrics are reported and what constitutes failure.
72
72
  Families are defined in `test.hpp` as `comparison_family_t`.
73
- All floating-point families report `max_abs`, `max_rel`, `mean_ulp`, `max_ulp`, and `exact` match counts; some also add `mean_abs` or `mean_rel`.
73
+ All floating-point families report `max_abs`, `max_rel`, and `mean_ulp`; most also report `max_ulp` and `exact` match counts, and some substitute `mean_abs` or `mean_rel`.
74
74
 
75
75
  - __`exact_k`__ — integer and binary metrics: Hamming, Jaccard, set intersections, integer min/max.
76
- Reports `max_dist`, `mean_dist`, `mismatch`, `exact`.
76
+ Reports `max_dist`, `mean_dist`, `max_abs`, `mismatch`, `exact`.
77
77
  Fails on any `max_dist > 0`.
78
78
  - __`narrow_arithmetic_k`__ — elementwise float ops: sum, scale, blend, fma, sin, cos, atan, cast.
79
79
  Fails on `max_ulp > NK_ULP_THRESHOLD_{F32,F16,BF16}`.
@@ -139,7 +139,7 @@ The toolchain files configure memory limits appropriate for each target:
139
139
  | ------ | -------------- | ---------- | ------------- | ------------------ |
140
140
  | wasm32 | 64 MB | 2 GB | 32-bit | 3.1.27+ |
141
141
  | wasm64 | 256 MB | 16 GB | 64-bit | 3.1.35+ |
142
- | WASI | — | 256 MB | 32-bit | — |
142
+ | WASI | — | 2 GB | 32-bit | — |
143
143
 
144
144
  All three targets enable `-msimd128` and `-mrelaxed-simd` automatically.
145
145
  Stack size is 5 MB across all Emscripten targets.
@@ -156,7 +156,7 @@ For up-to-date engine support, see [WebAssembly Roadmap](https://webassembly.org
156
156
 
157
157
  ### Cross-Compilation
158
158
 
159
- NumKong ships 8 toolchain files in `cmake/` for cross-compiling to non-native targets.
159
+ NumKong ships 11 toolchain files in `cmake/` for cross-compiling to non-native targets.
160
160
  Tests run transparently under QEMU via `CMAKE_CROSSCOMPILING_EMULATOR`.
161
161
  Set `NK_IN_QEMU=1` to relax half-precision accuracy thresholds under emulation.
162
162
 
@@ -0,0 +1,17 @@
1
+ // Minimal WASI command runner: `node --experimental-wasi-unstable-preview1 run-wasi.mjs <module.wasm> [args...]`
2
+ // Lets CTest use node as a cross-runtime engine for the portable WASI tests, alongside wasmtime and wasmer
3
+ // (see cmake/toolchain-wasi.cmake, NK_WASM_RUNTIME=node). Node cannot execute a bare `.wasm` from the CLI,
4
+ // so this thin wrapper instantiates it with a WASI import object and forwards the process exit code.
5
+ import { readFileSync } from 'node:fs';
6
+ import { WASI } from 'node:wasi';
7
+ import { argv, exit } from 'node:process';
8
+
9
+ const modulePath = argv[2];
10
+ if (!modulePath) {
11
+ console.error('usage: node --experimental-wasi-unstable-preview1 run-wasi.mjs <module.wasm> [args...]');
12
+ exit(2);
13
+ }
14
+ const wasi = new WASI({ version: 'preview1', args: argv.slice(2), returnOnExit: true });
15
+ const module = new WebAssembly.Module(readFileSync(modulePath));
16
+ const instance = new WebAssembly.Instance(module, wasi.getImportObject());
17
+ exit(wasi.start(instance));
@@ -6,7 +6,8 @@
6
6
  */
7
7
 
8
8
  #include "test.hpp"
9
- #include "numkong/each.hpp" // `nk::sum`, `nk::scale`, `nk::blend`, `nk::fma`
9
+ #include "numkong/each.hpp" // `nk::sum`, `nk::scale`, `nk::blend`, `nk::fma`
10
+ #include "numkong/trigonometry.hpp" // `nk::try_sin`, `nk::try_cos`, `nk::try_atan` wrappers
10
11
 
11
12
  template <typename scalar_type_, typename generator_type_>
12
13
  typename scalar_type_::scale_t random_coef(generator_type_ &gen) {
@@ -150,9 +151,37 @@ error_stats_t test_fma(typename scalar_type_::fma_kernel_t kernel) {
150
151
  return stats;
151
152
  }
152
153
 
154
+ /**
155
+ * @brief Smoke-test for the tensor-shaped trig wrappers (`nk::try_sin`/`cos`/`atan`).
156
+ * Runs allocating + into-span variants on a small zero tensor — just exercises the dispatch
157
+ * paths, not the numerical accuracy (the latter is covered by the kernel tests above).
158
+ */
159
+ template <typename value_type_>
160
+ void test_tensor_trig_for_type() {
161
+ using tensor_t = nk::tensor<value_type_>;
162
+ auto a = tensor_t::try_zeros({4, 8});
163
+ auto out = tensor_t::try_zeros({4, 8});
164
+ auto av = a.view();
165
+
166
+ { [[maybe_unused]] auto r = nk::try_sin<value_type_>(av); }
167
+ { [[maybe_unused]] auto r = nk::try_cos<value_type_>(av); }
168
+ { [[maybe_unused]] auto r = nk::try_atan<value_type_>(av); }
169
+ { [[maybe_unused]] bool ok = nk::sin<value_type_>(av, out.span()); }
170
+ { [[maybe_unused]] bool ok = nk::cos<value_type_>(av, out.span()); }
171
+ { [[maybe_unused]] bool ok = nk::atan<value_type_>(av, out.span()); }
172
+ }
173
+
153
174
  void test_each() {
154
175
  error_stats_section_t check("Elementwise Operations");
155
176
 
177
+ // Tensor-shaped trig wrappers (float-capable types).
178
+ test_tensor_trig_for_type<nk::f32_t>();
179
+ test_tensor_trig_for_type<nk::f64_t>();
180
+ test_tensor_trig_for_type<nk::f16_t>();
181
+ test_tensor_trig_for_type<nk::bf16_t>();
182
+ std::printf(" trig (4 types): OK\n");
183
+
184
+
156
185
  #if NK_DYNAMIC_DISPATCH
157
186
  // Dynamic dispatch - only test the dispatcher itself
158
187
  check("each_scale_f32", test_scale<f32_t>, nk_each_scale_f32);
@@ -405,7 +405,7 @@ def test_add_multiply_noncontiguous(dtype: str, kernel, capability: str):
405
405
  if mismatch_count:
406
406
  collect_warnings(f"NumPy overflow in ({a.dtype} {operator} {b.dtype} -> {output_dtype})", stats)
407
407
 
408
- # out= with nk.Tensor buffer (same-dtype only, to avoid cast complexity)
408
+ # out= with nk.Tensor buffer; differing dtypes are covered by test_strided_out_with_dtype_change
409
409
  if first_dtype == second_dtype == output_dtype and inplace_numkong.ndim == 1:
410
410
  out_nk = nk.zeros(inplace_numkong.shape, dtype=output_dtype)
411
411
  ret = simd_kernel(a, b, out=out_nk)
@@ -754,3 +754,76 @@ def test_blend_known(ndim: int, dtype: str, capability: str):
754
754
  result = list(nk.blend(a, b, alpha=2.0, beta=3.0))
755
755
  for i in range(ndim):
756
756
  assert abs(result[i] - expected) < NK_ATOL, f"blend[{i}] = {result[i]}, expected {expected}"
757
+
758
+
759
+ @pytest.mark.skipif(not numpy_available, reason="NumPy is not installed")
760
+ @pytest.mark.parametrize("kernel", ["add", "multiply"])
761
+ @pytest.mark.parametrize("second_is_scalar", [True, False], ids=["scalar_b", "array_b"])
762
+ @pytest.mark.parametrize("capability", possible_capabilities)
763
+ def test_strided_out_with_dtype_change(kernel: str, second_is_scalar: bool, capability: str):
764
+ """A strided `out` of a differing dtype must be scattered, not packed over its neighbours.
765
+
766
+ The scalar and array forms of `b` select different implementations, and each of
767
+ `add`/`multiply` has its own pair, so this covers all four writeback paths.
768
+ """
769
+ keep_one_capability(capability)
770
+ simd_kernel = getattr(nk, kernel)
771
+ first = np.arange(6, dtype=np.int16)
772
+ second = 5 if second_is_scalar else np.arange(6, dtype=np.int16)
773
+ expected = first + second if kernel == "add" else first * second
774
+
775
+ # Column 0 of a 2-column array: same shape as the input, stride of two float64s.
776
+ surrounding = np.zeros((6, 2), dtype=np.float64)
777
+ simd_kernel(first, second, out=surrounding[:, 0])
778
+ assert_allclose(surrounding[:, 0], expected.astype(np.float64), atol=NK_ATOL, rtol=NK_RTOL)
779
+ assert not surrounding[:, 1].any(), "writeback spilled into the neighbouring column"
780
+
781
+ # Reversed view exercises the negative-stride path.
782
+ reversed_out = np.zeros(6, dtype=np.float64)[::-1]
783
+ simd_kernel(first, second, out=reversed_out)
784
+ assert_allclose(reversed_out, expected.astype(np.float64), atol=NK_ATOL, rtol=NK_RTOL)
785
+
786
+ # A packed `out` must still take the single bulk-cast path.
787
+ contiguous_out = np.zeros(6, dtype=np.float64)
788
+ simd_kernel(first, second, out=contiguous_out)
789
+ assert_allclose(contiguous_out, expected.astype(np.float64), atol=NK_ATOL, rtol=NK_RTOL)
790
+
791
+
792
+ @pytest.mark.skipif(not numpy_available, reason="NumPy is not installed")
793
+ @pytest.mark.parametrize("kernel", ["add", "multiply"])
794
+ def test_strided_out_multidimensional(kernel: str):
795
+ """Rank-2 and rank-3 outputs cover both a fully scattered tail and a partially packed one."""
796
+ simd_kernel = getattr(nk, kernel)
797
+
798
+ # Rank 2, innermost dimension strided: no contiguous tail at all.
799
+ first = np.arange(12, dtype=np.int16).reshape(3, 4)
800
+ expected = first + 2 if kernel == "add" else first * 2
801
+ surrounding = np.zeros((3, 4, 2), dtype=np.float64)
802
+ simd_kernel(first, 2, out=surrounding[:, :, 0])
803
+ assert_allclose(surrounding[:, :, 0], expected.astype(np.float64), atol=NK_ATOL, rtol=NK_RTOL)
804
+ assert not surrounding[:, :, 1].any(), "writeback spilled into the neighbouring column"
805
+
806
+ # Rank 3 with a packed innermost pair: recursion plus a bulk slice per row.
807
+ first = np.arange(24, dtype=np.int16).reshape(2, 6, 2)
808
+ expected = first + 3 if kernel == "add" else first * 3
809
+ surrounding = np.zeros((2, 6, 4), dtype=np.float64)
810
+ simd_kernel(first, 3, out=surrounding[:, :, 0:2])
811
+ assert_allclose(surrounding[:, :, 0:2], expected.astype(np.float64), atol=NK_ATOL, rtol=NK_RTOL)
812
+ assert not surrounding[:, :, 2:].any(), "writeback spilled past the requested columns"
813
+
814
+
815
+ @pytest.mark.skipif(not numpy_available, reason="NumPy is not installed")
816
+ @pytest.mark.parametrize(
817
+ "dtype,distinct_values",
818
+ [pytest.param("uint4", 8, id="uint4"), pytest.param("int4", 8, id="int4"), pytest.param("uint1", 2, id="uint1")],
819
+ )
820
+ def test_packed_out_requires_contiguity(dtype: str, distinct_values: int):
821
+ """Sub-byte dtypes have no per-value byte stride, so only a packed `out` can be filled."""
822
+ first = np.arange(8, dtype=np.uint8) % distinct_values
823
+
824
+ packed_out = nk.zeros(first.shape, dtype=dtype)
825
+ nk.add(first, 0, out=packed_out)
826
+ assert_allclose(np.asarray(nk.astype(packed_out, "uint8")), first, atol=NK_ATOL, rtol=NK_RTOL)
827
+
828
+ with pytest.raises(ValueError, match="C-contiguous"):
829
+ nk.add(first[:4], 0, out=nk.zeros(first.shape, dtype=dtype)[::2])
@@ -62,9 +62,60 @@ error_stats_t test_reduce_minmax(typename input_type_::reduce_minmax_kernel_t ke
62
62
  return stats;
63
63
  }
64
64
 
65
+ /**
66
+ * @brief Smoke-test for the vector-shaped reduction wrappers (`nk::moments`/`minmax`/`sum`/...).
67
+ * Exercises every accessor variant on a small random vector. Numerical accuracy of the
68
+ * underlying kernels is validated by `test_reduce_moments` / `test_reduce_minmax` above.
69
+ */
70
+ template <typename value_type_>
71
+ void test_vector_reductions_for_type() {
72
+ auto v = make_vector<value_type_>(32);
73
+ std::mt19937 generator(42);
74
+ fill_random(generator, v);
75
+ auto view = nk::vector_view<value_type_>(v.values_data(), static_cast<std::size_t>(v.size()));
76
+
77
+ { [[maybe_unused]] auto r = nk::moments(view); }
78
+ { [[maybe_unused]] auto r = nk::minmax(view); }
79
+ { [[maybe_unused]] auto r = nk::sum(view); }
80
+ { [[maybe_unused]] auto r = nk::min(view); }
81
+ { [[maybe_unused]] auto r = nk::max(view); }
82
+ { [[maybe_unused]] auto r = nk::argmin(view); }
83
+ { [[maybe_unused]] auto r = nk::argmax(view); }
84
+ }
85
+
86
+ /**
87
+ * @brief Golden-value test for the vector-shaped reduction wrappers.
88
+ */
89
+ inline void test_vector_reductions_correctness() {
90
+ nk::f32_t data[] = {nk::f32_t(1), nk::f32_t(2), nk::f32_t(3), nk::f32_t(4), nk::f32_t(5)};
91
+ auto view = nk::vector_view<nk::f32_t>(data, std::size_t {5});
92
+
93
+ auto m = nk::moments(view);
94
+ assert(m.sum.raw_ == 15.0 && "vector moments sum");
95
+ assert(m.sumsq.raw_ == 55.0 && "vector moments sumsq");
96
+
97
+ auto mm = nk::minmax(view);
98
+ assert(mm.min_value.raw_ == 1.0f && "vector min");
99
+ assert(mm.max_value.raw_ == 5.0f && "vector max");
100
+ assert(mm.min_index == 0 && "vector argmin");
101
+ assert(mm.max_index == 4 && "vector argmax");
102
+ }
103
+
65
104
  void test_reduce() {
66
105
  error_stats_section_t check("Reductions");
67
106
 
107
+ // Tensor- and vector-shaped wrapper smoke tests (independent of ISA dispatch).
108
+ test_vector_reductions_for_type<nk::f32_t>();
109
+ test_vector_reductions_for_type<nk::f64_t>();
110
+ test_vector_reductions_for_type<nk::f16_t>();
111
+ test_vector_reductions_for_type<nk::bf16_t>();
112
+ test_vector_reductions_for_type<nk::i8_t>();
113
+ test_vector_reductions_for_type<nk::u8_t>();
114
+ std::printf(" vector reductions (6 types): OK\n");
115
+
116
+ test_vector_reductions_correctness();
117
+ std::printf(" vector reductions correct: OK\n");
118
+
68
119
  #if NK_DYNAMIC_DISPATCH
69
120
  check("reduce_moments_f32", test_reduce_moments<f32_t>, nk_reduce_moments_f32, nk_reduce_moments_f32_serial);
70
121
  check("reduce_moments_f64", test_reduce_moments<f64_t>, nk_reduce_moments_f64, nk_reduce_moments_f64_serial);
@@ -679,21 +679,6 @@ void test_tensor_ops_for_type() {
679
679
  }
680
680
  }
681
681
 
682
- template <typename value_type_>
683
- void test_tensor_trig_for_type() {
684
- using tensor_t = nk::tensor<value_type_>;
685
- auto a = tensor_t::try_zeros({4, 8});
686
- auto out = tensor_t::try_zeros({4, 8});
687
- auto av = a.view();
688
-
689
- { [[maybe_unused]] auto r = nk::try_sin<value_type_>(av); }
690
- { [[maybe_unused]] auto r = nk::try_cos<value_type_>(av); }
691
- { [[maybe_unused]] auto r = nk::try_atan<value_type_>(av); }
692
- { [[maybe_unused]] bool ok = nk::sin<value_type_>(av, out.span()); }
693
- { [[maybe_unused]] bool ok = nk::cos<value_type_>(av, out.span()); }
694
- { [[maybe_unused]] bool ok = nk::atan<value_type_>(av, out.span()); }
695
- }
696
-
697
682
  template <typename value_type_>
698
683
  void test_tensor_symmetric_for_type() {
699
684
  using tensor_t = nk::tensor<value_type_>;
@@ -761,37 +746,6 @@ void test_custom_allocator_try_fns() {
761
746
  { auto r = nk::try_sum<nk::f32_t, 8, sum_alloc_t>(av, 0); }
762
747
  }
763
748
 
764
- template <typename value_type_>
765
- void test_vector_reductions_for_type() {
766
- auto v = make_vector<value_type_>(32);
767
- std::mt19937 generator(42);
768
- fill_random(generator, v);
769
- auto view = nk::vector_view<value_type_>(v.values_data(), static_cast<std::size_t>(v.size()));
770
-
771
- { [[maybe_unused]] auto r = nk::moments(view); }
772
- { [[maybe_unused]] auto r = nk::minmax(view); }
773
- { [[maybe_unused]] auto r = nk::sum(view); }
774
- { [[maybe_unused]] auto r = nk::min(view); }
775
- { [[maybe_unused]] auto r = nk::max(view); }
776
- { [[maybe_unused]] auto r = nk::argmin(view); }
777
- { [[maybe_unused]] auto r = nk::argmax(view); }
778
- }
779
-
780
- void test_vector_reductions_correctness() {
781
- nk::f32_t data[] = {nk::f32_t(1), nk::f32_t(2), nk::f32_t(3), nk::f32_t(4), nk::f32_t(5)};
782
- auto view = nk::vector_view<nk::f32_t>(data, std::size_t {5});
783
-
784
- auto m = nk::moments(view);
785
- assert(m.sum.raw_ == 15.0 && "vector moments sum");
786
- assert(m.sumsq.raw_ == 55.0 && "vector moments sumsq");
787
-
788
- auto mm = nk::minmax(view);
789
- assert(mm.min_value.raw_ == 1.0f && "vector min");
790
- assert(mm.max_value.raw_ == 5.0f && "vector max");
791
- assert(mm.min_index == 0 && "vector argmin");
792
- assert(mm.max_index == 4 && "vector argmax");
793
- }
794
-
795
749
  template <typename from_type_, typename to_type_>
796
750
  void test_cast_for_types() {
797
751
  auto src = make_vector<from_type_>(64);
@@ -884,12 +838,7 @@ void test_tensor_ops() {
884
838
  test_packed_tensor_fail_closed_views();
885
839
  std::printf(" tensor fail-closed views: OK\n");
886
840
 
887
- // Trig (float-capable types)
888
- test_tensor_trig_for_type<nk::f32_t>();
889
- test_tensor_trig_for_type<nk::f64_t>();
890
- test_tensor_trig_for_type<nk::f16_t>();
891
- test_tensor_trig_for_type<nk::bf16_t>();
892
- std::printf(" trig (4 types): OK\n");
841
+ // (Trig wrappers moved to test_each.cpp — they are elementwise operations.)
893
842
 
894
843
  // Symmetric distances
895
844
  test_tensor_symmetric_for_type<nk::f32_t>();
@@ -919,17 +868,7 @@ void test_tensor_ops() {
919
868
  test_custom_allocator_try_fns();
920
869
  std::printf(" custom allocator try_fns: OK\n");
921
870
 
922
- // Vector-level reductions
923
- test_vector_reductions_for_type<nk::f32_t>();
924
- test_vector_reductions_for_type<nk::f64_t>();
925
- test_vector_reductions_for_type<nk::f16_t>();
926
- test_vector_reductions_for_type<nk::bf16_t>();
927
- test_vector_reductions_for_type<nk::i8_t>();
928
- test_vector_reductions_for_type<nk::u8_t>();
929
- std::printf(" vector reductions (6 types): OK\n");
930
-
931
- test_vector_reductions_correctness();
932
- std::printf(" vector reductions correct: OK\n");
871
+ // (Vector-level reduction wrappers moved to test_reduce.cpp.)
933
872
 
934
873
  // Cast wrapper
935
874
  test_cast_for_types<nk::f32_t, nk::f16_t>();
@@ -102,6 +102,99 @@ KERNELS_TENSOR: dict[str, tuple[Callable, Callable, Callable | None]] = {
102
102
  }
103
103
 
104
104
 
105
+ @pytest.mark.skipif(not numpy_available, reason="NumPy is not installed")
106
+ def test_astype_accepts_ndarray_without_tensor_copy():
107
+ """Top-level astype converts an N-D NumPy buffer while preserving its shape."""
108
+ source = np.arange(24, dtype=np.uint8).reshape(2, 3, 4)
109
+
110
+ result = nk.astype(source, "float32")
111
+
112
+ assert result.shape == source.shape
113
+ assert result.dtype == "float32"
114
+ assert_allclose(np.asarray(result), source.astype(np.float32))
115
+
116
+
117
+ @pytest.mark.skipif(not numpy_available, reason="NumPy is not installed")
118
+ def test_astype_writes_to_caller_owned_output():
119
+ """Top-level astype converts directly into a matching caller-owned buffer and returns it."""
120
+ source = np.arange(12, dtype=np.float32).reshape(3, 4) + 0.75
121
+ output = np.empty(source.shape, dtype=np.uint8)
122
+
123
+ assert nk.astype(source, "uint8", out=output) is output
124
+ assert_allclose(output, source.astype(np.uint8))
125
+
126
+
127
+ @pytest.mark.skipif(not numpy_available, reason="NumPy is not installed")
128
+ @pytest.mark.parametrize("dtype", ["bfloat16", "uint4"])
129
+ def test_astype_writes_to_tensor_output(dtype):
130
+ """A NumKong Tensor names dtypes NumPy cannot express, so it must be accepted as `out`."""
131
+ source = np.arange(8, dtype=np.uint8)
132
+ output = nk.empty(source.shape, dtype=dtype)
133
+
134
+ assert nk.astype(source, dtype, out=output) is output
135
+ np.testing.assert_array_equal(np.asarray(nk.astype(output, "uint8")), source)
136
+
137
+
138
+ @pytest.mark.skipif(not numpy_available, reason="NumPy is not installed")
139
+ def test_astype_linearizes_strided_input():
140
+ """Top-level astype accepts a non-contiguous input buffer."""
141
+ source = np.arange(48, dtype=np.int16).reshape(2, 3, 8)[:, ::-1, ::2]
142
+
143
+ result = nk.astype(source, "float32")
144
+
145
+ assert result.shape == source.shape
146
+ assert_allclose(np.asarray(result), source.astype(np.float32))
147
+
148
+
149
+ @pytest.mark.skipif(not numpy_available, reason="NumPy is not installed")
150
+ def test_astype_float_to_integer_policy():
151
+ """Float-to-integer casts round ties to even, saturate, and map NaN to zero."""
152
+ source = np.array([-np.inf, -127.5, -0.5, 0.5, 1.5, 126.5, np.inf, np.nan], dtype=np.float64)
153
+
154
+ result = nk.astype(source, "int8")
155
+
156
+ np.testing.assert_array_equal(np.asarray(result), np.array([-128, -128, 0, 0, 2, 126, 127, 0], dtype=np.int8))
157
+
158
+
159
+ @pytest.mark.skipif(not numpy_available, reason="NumPy is not installed")
160
+ def test_astype_round_trips_through_packed_dtype():
161
+ """Top-level astype preserves logical values when a packed Tensor is used in between."""
162
+ source = np.arange(8, dtype=np.uint8)
163
+
164
+ packed = nk.astype(source, "uint4")
165
+ result = nk.astype(packed, "uint8")
166
+
167
+ assert packed.dtype == "uint4"
168
+ np.testing.assert_array_equal(np.asarray(result), source)
169
+
170
+
171
+ @pytest.mark.skipif(not numpy_available, reason="NumPy is not installed")
172
+ def test_astype_validates_output_shape_dtype_and_layout():
173
+ """Top-level astype requires an exact, C-contiguous output buffer."""
174
+ source = np.arange(24, dtype=np.uint8).reshape(2, 3, 4)
175
+
176
+ with pytest.raises(ValueError, match="shapes don't match"):
177
+ nk.astype(source, "float32", out=np.empty((2, 3, 3), dtype=np.float32))
178
+ with pytest.raises(TypeError, match="expected"):
179
+ nk.astype(source, "float32", out=np.empty(source.shape, dtype=np.float64))
180
+ with pytest.raises(ValueError, match="C-contiguous"):
181
+ nk.astype(source, "float32", out=np.empty((4, 3, 2), dtype=np.float32).transpose(2, 1, 0))
182
+
183
+ readonly = np.empty(source.shape, dtype=np.float32)
184
+ readonly.flags.writeable = False
185
+ with pytest.raises((ValueError, BufferError, TypeError)):
186
+ nk.astype(source, "float32", out=readonly)
187
+
188
+
189
+ @pytest.mark.skipif(not numpy_available, reason="NumPy is not installed")
190
+ def test_astype_rejects_overlapping_output():
191
+ """Top-level astype rejects an output buffer that aliases its input."""
192
+ source = np.arange(12, dtype=np.float32)
193
+
194
+ with pytest.raises(ValueError, match="must not overlap"):
195
+ nk.astype(source, "float32", out=source)
196
+
197
+
105
198
  def test_pointers_availability():
106
199
  """Tests the availability of pre-compiled functions for compatibility with USearch."""
107
200
  assert nk.pointer_to_sqeuclidean("float64") != 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "2.26.1",
3
+ "version": "2.26.3",
4
4
  "description": "Smaller & Faster Single-File Vector Search Engine from Unum",
5
5
  "author": "Ash Vardanian (https://ashvardanian.com/)",
6
6
  "license": "Apache 2.0",
Binary file
Binary file
Binary file