numba-utils 0.1.0__py3-none-any.whl

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 (41) hide show
  1. numba_utils/__init__.py +137 -0
  2. numba_utils/_config.py +92 -0
  3. numba_utils/algorithms/__init__.py +26 -0
  4. numba_utils/algorithms/_select.py +123 -0
  5. numba_utils/algorithms/_sort.py +166 -0
  6. numba_utils/algorithms/_topk.py +87 -0
  7. numba_utils/arrays/__init__.py +21 -0
  8. numba_utils/arrays/_hist.py +56 -0
  9. numba_utils/arrays/_rolling.py +50 -0
  10. numba_utils/arrays/_search.py +64 -0
  11. numba_utils/arrays/_transform.py +89 -0
  12. numba_utils/arrays/_unique.py +34 -0
  13. numba_utils/collections/__init__.py +28 -0
  14. numba_utils/collections/_bitset.py +57 -0
  15. numba_utils/collections/_heap.py +72 -0
  16. numba_utils/collections/_sparse.py +105 -0
  17. numba_utils/collections/_stack_queue.py +123 -0
  18. numba_utils/collections/_typed.py +58 -0
  19. numba_utils/decorators/__init__.py +19 -0
  20. numba_utils/decorators/_wrappers.py +118 -0
  21. numba_utils/diagnostics/__init__.py +13 -0
  22. numba_utils/diagnostics/_inspect.py +190 -0
  23. numba_utils/parallel/__init__.py +31 -0
  24. numba_utils/parallel/_histogram.py +59 -0
  25. numba_utils/parallel/_reduce.py +86 -0
  26. numba_utils/parallel/_scan.py +65 -0
  27. numba_utils/parallel/_topk.py +86 -0
  28. numba_utils/profiling/__init__.py +21 -0
  29. numba_utils/profiling/_compare.py +118 -0
  30. numba_utils/profiling/_timer.py +144 -0
  31. numba_utils/random/__init__.py +27 -0
  32. numba_utils/random/_rng.py +59 -0
  33. numba_utils/random/_sampling.py +142 -0
  34. numba_utils/testing/__init__.py +24 -0
  35. numba_utils/testing/_asserts.py +93 -0
  36. numba_utils/testing/_generators.py +63 -0
  37. numba_utils-0.1.0.dist-info/METADATA +191 -0
  38. numba_utils-0.1.0.dist-info/RECORD +41 -0
  39. numba_utils-0.1.0.dist-info/WHEEL +5 -0
  40. numba_utils-0.1.0.dist-info/licenses/LICENSE +21 -0
  41. numba_utils-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,137 @@
1
+ """numba-utils: high-performance building blocks for Numba."""
2
+
3
+ from numba_utils import diagnostics, testing
4
+ from numba_utils._config import config, configure
5
+ from numba_utils.algorithms import (
6
+ argmax2,
7
+ counting_sort,
8
+ fast_argpartition,
9
+ insertion_sort,
10
+ nth_element,
11
+ partial_sort,
12
+ quickselect,
13
+ radix_sort,
14
+ topk,
15
+ )
16
+ from numba_utils.arrays import (
17
+ bincount,
18
+ binary_search,
19
+ cumulative_sum,
20
+ fast_clip,
21
+ histogram,
22
+ lower_bound,
23
+ normalize,
24
+ rolling_mean,
25
+ rolling_sum,
26
+ unique_sorted,
27
+ upper_bound,
28
+ )
29
+ from numba_utils.collections import (
30
+ BitSet,
31
+ FixedQueue,
32
+ ObjectPool,
33
+ PriorityQueue,
34
+ RingBuffer,
35
+ SparseSet,
36
+ Stack,
37
+ counter,
38
+ typed_defaultdict,
39
+ )
40
+ from numba_utils.decorators import (
41
+ boundscheck,
42
+ cached_njit,
43
+ njit_fast,
44
+ njit_parallel,
45
+ )
46
+ from numba_utils.parallel import (
47
+ parallel_histogram,
48
+ parallel_prefix_sum,
49
+ parallel_reduce,
50
+ parallel_sum,
51
+ parallel_topk,
52
+ )
53
+ from numba_utils.profiling import (
54
+ BenchmarkResult,
55
+ ComparisonResult,
56
+ TimingStats,
57
+ benchmark,
58
+ compare,
59
+ compile_stats,
60
+ compile_time,
61
+ warmup,
62
+ )
63
+ from numba_utils.random import (
64
+ alias_draw,
65
+ alias_sample,
66
+ alias_setup,
67
+ choice,
68
+ permutation,
69
+ reservoir_sampling,
70
+ seed,
71
+ shuffle,
72
+ weighted_sampling,
73
+ )
74
+
75
+ __version__ = "0.1.0"
76
+
77
+ __all__ = [
78
+ "BenchmarkResult",
79
+ "BitSet",
80
+ "ComparisonResult",
81
+ "FixedQueue",
82
+ "ObjectPool",
83
+ "PriorityQueue",
84
+ "RingBuffer",
85
+ "SparseSet",
86
+ "Stack",
87
+ "TimingStats",
88
+ "alias_draw",
89
+ "alias_sample",
90
+ "alias_setup",
91
+ "argmax2",
92
+ "benchmark",
93
+ "bincount",
94
+ "binary_search",
95
+ "boundscheck",
96
+ "cached_njit",
97
+ "choice",
98
+ "compare",
99
+ "compile_stats",
100
+ "compile_time",
101
+ "config",
102
+ "configure",
103
+ "counter",
104
+ "counting_sort",
105
+ "cumulative_sum",
106
+ "diagnostics",
107
+ "fast_argpartition",
108
+ "fast_clip",
109
+ "histogram",
110
+ "insertion_sort",
111
+ "lower_bound",
112
+ "njit_fast",
113
+ "njit_parallel",
114
+ "normalize",
115
+ "nth_element",
116
+ "parallel_histogram",
117
+ "parallel_prefix_sum",
118
+ "parallel_reduce",
119
+ "parallel_sum",
120
+ "parallel_topk",
121
+ "partial_sort",
122
+ "permutation",
123
+ "quickselect",
124
+ "radix_sort",
125
+ "reservoir_sampling",
126
+ "rolling_mean",
127
+ "rolling_sum",
128
+ "seed",
129
+ "shuffle",
130
+ "testing",
131
+ "topk",
132
+ "typed_defaultdict",
133
+ "unique_sorted",
134
+ "upper_bound",
135
+ "warmup",
136
+ "weighted_sampling",
137
+ ]
numba_utils/_config.py ADDED
@@ -0,0 +1,92 @@
1
+ """Global configuration: code-level overrides with environment fallbacks.
2
+
3
+ Two levels, resolved per option at decoration time:
4
+
5
+ 1. Code: ``configure(cache=False)`` or ``config.cache = False``.
6
+ 2. Environment: ``NUMBA_UTILS_CACHE=0`` (checked only when the code level
7
+ is unset).
8
+
9
+ A set option (``True``/``False``) is a GLOBAL override: it wins over
10
+ decorator defaults and per-call keyword arguments alike. That is the
11
+ point — overrides exist for environment-level policy, like disabling the
12
+ on-disk cache on machines where it is unsafe (see docs/numba-cache.md).
13
+ ``None`` means "no override": decorator defaults and per-call arguments
14
+ apply unchanged.
15
+
16
+ Overrides affect only functions decorated AFTER the change — configure
17
+ before defining your jitted functions.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import os
23
+
24
+ _TRUTHY = frozenset({"1", "true", "yes", "on"})
25
+ _FALSY = frozenset({"0", "false", "no", "off"})
26
+
27
+ OPTION_ENV_VARS = {
28
+ "cache": "NUMBA_UTILS_CACHE",
29
+ "fastmath": "NUMBA_UTILS_FASTMATH",
30
+ "parallel": "NUMBA_UTILS_PARALLEL",
31
+ "nogil": "NUMBA_UTILS_NOGIL",
32
+ }
33
+
34
+
35
+ class GlobalConfig:
36
+ """Tri-state global overrides for every numba-utils decorator.
37
+
38
+ Attributes ``cache``, ``fastmath``, ``parallel``, ``nogil`` are each
39
+ ``None`` (no override), ``True`` or ``False`` (forced globally).
40
+ """
41
+
42
+ __slots__ = ("cache", "fastmath", "parallel", "nogil")
43
+
44
+ def __init__(self) -> None:
45
+ self.reset()
46
+
47
+ def reset(self) -> None:
48
+ """Clear all code-level overrides. Environment fallbacks remain."""
49
+ for name in OPTION_ENV_VARS:
50
+ setattr(self, name, None)
51
+
52
+ def resolve(self, name: str) -> bool | None:
53
+ """Effective override for ``name``: code level, else environment,
54
+ else ``None``. Unrecognized environment values count as unset."""
55
+ value = getattr(self, name)
56
+ if value is not None:
57
+ return bool(value)
58
+ raw = os.environ.get(OPTION_ENV_VARS[name], "").strip().lower()
59
+ if raw in _TRUTHY:
60
+ return True
61
+ if raw in _FALSY:
62
+ return False
63
+ return None
64
+
65
+
66
+ config = GlobalConfig()
67
+
68
+
69
+ def configure(**options: bool | None) -> None:
70
+ """Set global decorator overrides in code.
71
+
72
+ ::
73
+
74
+ import numba_utils as nu
75
+ nu.configure(cache=False, fastmath=True)
76
+
77
+ Valid options: ``cache``, ``fastmath``, ``parallel``, ``nogil``.
78
+ Pass ``None`` to clear a single override; ``config.reset()`` clears
79
+ all. Raises ``ValueError`` on unknown names (fail fast, no typos
80
+ silently ignored).
81
+ """
82
+ for name, value in options.items():
83
+ if name not in OPTION_ENV_VARS:
84
+ valid = ", ".join(sorted(OPTION_ENV_VARS))
85
+ raise ValueError(
86
+ f"configure: unknown option {name!r}; valid options: {valid}"
87
+ )
88
+ if value is not None and not isinstance(value, bool):
89
+ raise ValueError(
90
+ f"configure: option {name!r} must be True, False or None"
91
+ )
92
+ setattr(config, name, value)
@@ -0,0 +1,26 @@
1
+ """Selection and sorting algorithms specialized for Numba."""
2
+
3
+ from numba_utils.algorithms._select import (
4
+ fast_argpartition,
5
+ nth_element,
6
+ quickselect,
7
+ )
8
+ from numba_utils.algorithms._sort import (
9
+ counting_sort,
10
+ insertion_sort,
11
+ partial_sort,
12
+ radix_sort,
13
+ )
14
+ from numba_utils.algorithms._topk import argmax2, topk
15
+
16
+ __all__ = [
17
+ "argmax2",
18
+ "counting_sort",
19
+ "fast_argpartition",
20
+ "insertion_sort",
21
+ "nth_element",
22
+ "partial_sort",
23
+ "quickselect",
24
+ "radix_sort",
25
+ "topk",
26
+ ]
@@ -0,0 +1,123 @@
1
+ """Selection: quickselect, nth_element and argpartition.
2
+
3
+ Mutation contract, stated per function and consistent across the module:
4
+ ``nth_element`` partitions IN PLACE (the zero-allocation building block);
5
+ ``quickselect`` and ``fast_argpartition`` never touch their input.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import numpy as np
11
+
12
+ from numba_utils.decorators import cached_njit
13
+
14
+
15
+ @cached_njit
16
+ def _median3(a, b, c):
17
+ if a < b:
18
+ if b < c:
19
+ return b
20
+ elif a < c:
21
+ return c
22
+ else:
23
+ return a
24
+ else:
25
+ if a < c:
26
+ return a
27
+ elif b < c:
28
+ return c
29
+ else:
30
+ return b
31
+
32
+
33
+ @cached_njit
34
+ def nth_element(arr, k):
35
+ """Partition ``arr`` IN PLACE so ``arr[k]`` is its k-th smallest value.
36
+
37
+ After the call: ``arr[:k] <= arr[k] <= arr[k+1:]`` (both sides
38
+ unordered). Returns ``arr[k]``. Iterative Hoare quickselect with
39
+ median-of-three pivots. This is the C++ ``std::nth_element``.
40
+
41
+ Complexity: average O(n), worst O(n²). Memory: O(1).
42
+ """
43
+ n = arr.shape[0]
44
+ if n == 0:
45
+ raise ValueError("nth_element: empty array")
46
+ if k < 0 or k >= n:
47
+ raise ValueError("nth_element: k out of range")
48
+ lo = 0
49
+ hi = n - 1
50
+ while lo < hi:
51
+ mid = (lo + hi) >> 1
52
+ pivot = _median3(arr[lo], arr[mid], arr[hi])
53
+ i = lo
54
+ j = hi
55
+ while i <= j:
56
+ while arr[i] < pivot:
57
+ i += 1
58
+ while arr[j] > pivot:
59
+ j -= 1
60
+ if i <= j:
61
+ arr[i], arr[j] = arr[j], arr[i]
62
+ i += 1
63
+ j -= 1
64
+ if k <= j:
65
+ hi = j
66
+ elif k >= i:
67
+ lo = i
68
+ else:
69
+ break
70
+ return arr[k]
71
+
72
+
73
+ @cached_njit
74
+ def quickselect(arr, k):
75
+ """k-th smallest value of ``arr`` (0-indexed). Input is NOT modified.
76
+
77
+ Convenience wrapper: copies, then runs :func:`nth_element`. Call
78
+ ``nth_element`` directly in hot loops to skip the copy.
79
+
80
+ Complexity: average O(n), worst O(n²). Memory: O(n) for the copy.
81
+ """
82
+ tmp = arr.copy()
83
+ return nth_element(tmp, k)
84
+
85
+
86
+ @cached_njit
87
+ def fast_argpartition(arr, k):
88
+ """Indices of the k smallest elements of ``arr``, in no particular order.
89
+
90
+ Like ``np.argpartition(arr, k)[:k]`` but returns exactly k indices and
91
+ skips NumPy's full output. Input is NOT modified; the quickselect runs
92
+ on an index array compared through ``arr``.
93
+
94
+ Complexity: average O(n), worst O(n²). Memory: O(n) for the indices.
95
+ """
96
+ n = arr.shape[0]
97
+ if k < 1 or k > n:
98
+ raise ValueError("fast_argpartition: k must be in [1, len(arr)]")
99
+ idx = np.arange(n)
100
+ target = k - 1
101
+ lo = 0
102
+ hi = n - 1
103
+ while lo < hi:
104
+ mid = (lo + hi) >> 1
105
+ pivot = _median3(arr[idx[lo]], arr[idx[mid]], arr[idx[hi]])
106
+ i = lo
107
+ j = hi
108
+ while i <= j:
109
+ while arr[idx[i]] < pivot:
110
+ i += 1
111
+ while arr[idx[j]] > pivot:
112
+ j -= 1
113
+ if i <= j:
114
+ idx[i], idx[j] = idx[j], idx[i]
115
+ i += 1
116
+ j -= 1
117
+ if target <= j:
118
+ hi = j
119
+ elif target >= i:
120
+ lo = i
121
+ else:
122
+ break
123
+ return idx[:k].copy()
@@ -0,0 +1,166 @@
1
+ """Sorting: insertion sort, partial sort, counting sort, radix sort.
2
+
3
+ Mutation contract: ``insertion_sort`` and ``partial_sort`` work IN PLACE
4
+ (zero allocation — that's their reason to exist) and return the array for
5
+ chaining. ``counting_sort`` and ``radix_sort`` allocate scratch space
6
+ inherently, so they return a NEW array and leave the input untouched.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import numpy as np
12
+
13
+ from numba_utils.algorithms._select import nth_element
14
+ from numba_utils.decorators import cached_njit
15
+
16
+ _COUNTING_SORT_MAX_RANGE = 2**27
17
+
18
+
19
+ @cached_njit
20
+ def insertion_sort(arr):
21
+ """Sort ``arr`` ascending IN PLACE; returns it.
22
+
23
+ O(n²), but with tiny constants and O(n) on nearly-sorted input —
24
+ the right tool below ~64 elements or as a small-partition finisher.
25
+
26
+ Complexity: O(n²) worst, O(n) nearly-sorted. Memory: O(1).
27
+ """
28
+ for i in range(1, arr.shape[0]):
29
+ x = arr[i]
30
+ j = i - 1
31
+ while j >= 0 and arr[j] > x:
32
+ arr[j + 1] = arr[j]
33
+ j -= 1
34
+ arr[j + 1] = x
35
+ return arr
36
+
37
+
38
+ @cached_njit
39
+ def partial_sort(arr, k):
40
+ """Rearrange ``arr`` IN PLACE so ``arr[:k]`` holds the k smallest, sorted.
41
+
42
+ ``arr[k:]`` contains the rest in arbitrary order. Returns ``arr``.
43
+ The C++ ``std::partial_sort``: select with :func:`nth_element`, then
44
+ sort only the front.
45
+
46
+ Complexity: average O(n + k log k). Memory: O(1).
47
+ """
48
+ n = arr.shape[0]
49
+ if k < 1 or k > n:
50
+ raise ValueError("partial_sort: k must be in [1, len(arr)]")
51
+ if k < n:
52
+ nth_element(arr, k - 1)
53
+ arr[:k].sort()
54
+ return arr
55
+
56
+
57
+ @cached_njit
58
+ def counting_sort(arr):
59
+ """Return a NEW sorted array of integers via counting sort.
60
+
61
+ Non-comparison sort: O(n + range) where range = max - min + 1. Wins
62
+ when the value range is small relative to n (codes, buckets, bytes).
63
+ Raises ``ValueError`` if range exceeds 2**27 (a 1 GiB counts array) —
64
+ use :func:`radix_sort` there. Integer dtypes only.
65
+
66
+ Complexity: O(n + range). Memory: O(n + range).
67
+ """
68
+ n = arr.shape[0]
69
+ out = np.empty_like(arr)
70
+ if n == 0:
71
+ return out
72
+ mn = arr[0]
73
+ mx = arr[0]
74
+ for i in range(1, n):
75
+ v = arr[i]
76
+ if v < mn:
77
+ mn = v
78
+ elif v > mx:
79
+ mx = v
80
+ value_range = np.int64(mx) - np.int64(mn) + 1
81
+ if value_range > _COUNTING_SORT_MAX_RANGE:
82
+ raise ValueError(
83
+ "counting_sort: value range too large (> 2**27), use radix_sort"
84
+ )
85
+ counts = np.zeros(value_range, np.int64)
86
+ for i in range(n):
87
+ counts[np.int64(arr[i]) - np.int64(mn)] += 1
88
+ j = 0
89
+ for v in range(value_range):
90
+ value = mn + v
91
+ for _ in range(counts[v]):
92
+ out[j] = value
93
+ j += 1
94
+ return out
95
+
96
+
97
+ @cached_njit
98
+ def _radix_key(x, mn):
99
+ # Modular difference (x - mn) mod 2**64 equals the true nonnegative
100
+ # distance for every integer dtype, so one biased uint64 key handles
101
+ # signed and unsigned inputs alike.
102
+ return np.uint64(np.int64(x) - np.int64(mn))
103
+
104
+
105
+ @cached_njit
106
+ def radix_sort(arr):
107
+ """Return a NEW sorted array of integers via LSD radix sort.
108
+
109
+ 8-bit digits over min-biased uint64 keys; handles all signed and
110
+ unsigned integer dtypes, negatives included. All digit histograms are
111
+ built in ONE pass, then each byte gets a scatter pass — and bytes
112
+ that are constant across the array (including the high bytes the
113
+ value range doesn't reach) are skipped entirely. Integer dtypes only.
114
+
115
+ When it wins: values spanning <= 3-4 bytes (each byte is a full
116
+ scatter pass). For full-range 64-bit keys, NumPy's SIMD introsort
117
+ is faster — see BENCHMARKS.md for both cases.
118
+
119
+ Complexity: O(n · (1 + bytes_used)). Memory: O(n) scratch + counters.
120
+ """
121
+ n = arr.shape[0]
122
+ a = arr.copy()
123
+ if n <= 1:
124
+ return a
125
+ b = np.empty_like(arr)
126
+ mn = arr[0]
127
+ mx = arr[0]
128
+ for i in range(1, n):
129
+ v = arr[i]
130
+ if v < mn:
131
+ mn = v
132
+ elif v > mx:
133
+ mx = v
134
+ max_key = _radix_key(mx, mn)
135
+ eight = np.uint64(8)
136
+ mask = np.uint64(0xFF)
137
+ passes = 1
138
+ while passes < 8 and (max_key >> np.uint64(8 * passes)) != np.uint64(0):
139
+ passes += 1
140
+ counts = np.zeros((passes, 256), np.int64)
141
+ for i in range(n):
142
+ key = _radix_key(a[i], mn)
143
+ for p in range(passes):
144
+ counts[p, key & mask] += 1
145
+ key = key >> eight
146
+ for p in range(passes):
147
+ shift = np.uint64(8 * p)
148
+ cp = counts[p]
149
+ constant_byte = False
150
+ for d in range(256):
151
+ if cp[d] == n:
152
+ constant_byte = True
153
+ break
154
+ if constant_byte:
155
+ continue
156
+ total = 0
157
+ for d in range(256):
158
+ c = cp[d]
159
+ cp[d] = total
160
+ total += c
161
+ for i in range(n):
162
+ digit = (_radix_key(a[i], mn) >> shift) & mask
163
+ b[cp[digit]] = a[i]
164
+ cp[digit] += 1
165
+ a, b = b, a
166
+ return a
@@ -0,0 +1,87 @@
1
+ """Top-k and extremum queries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from numba_utils.algorithms._select import nth_element
6
+ from numba_utils.decorators import cached_njit
7
+
8
+ # Below n/16, a size-k min-heap beats quickselect: one read-only pass,
9
+ # no O(n) copy, and most elements fail the `x > heap[0]` test immediately.
10
+ _HEAP_PATH_FACTOR = 16
11
+
12
+
13
+ @cached_njit
14
+ def _sift_down(heap, start, size):
15
+ root = start
16
+ while True:
17
+ child = 2 * root + 1
18
+ if child >= size:
19
+ return
20
+ if child + 1 < size and heap[child + 1] < heap[child]:
21
+ child += 1
22
+ if heap[child] < heap[root]:
23
+ heap[root], heap[child] = heap[child], heap[root]
24
+ root = child
25
+ else:
26
+ return
27
+
28
+
29
+ @cached_njit
30
+ def _topk_heap(arr, k):
31
+ n = arr.shape[0]
32
+ heap = arr[:k].copy()
33
+ for start in range(k // 2 - 1, -1, -1):
34
+ _sift_down(heap, start, k)
35
+ for i in range(k, n):
36
+ x = arr[i]
37
+ if x > heap[0]:
38
+ heap[0] = x
39
+ _sift_down(heap, 0, k)
40
+ heap.sort()
41
+ return heap[::-1].copy()
42
+
43
+
44
+ @cached_njit
45
+ def topk(arr, k):
46
+ """The k LARGEST values of ``arr``, sorted descending. Input untouched.
47
+
48
+ Small k (``k * 16 <= n``): single read-only pass with a size-k
49
+ min-heap, O(n) time and O(k) extra memory. Large k: quickselect via
50
+ :func:`nth_element` on a copy, then sort only the k winners. Either
51
+ way the full sort never happens. For the k smallest, use
52
+ :func:`fast_argpartition`.
53
+
54
+ Complexity: average O(n + k log k). Memory: O(k) small k, O(n) large k.
55
+ """
56
+ n = arr.shape[0]
57
+ if k < 1 or k > n:
58
+ raise ValueError("topk: k must be in [1, len(arr)]")
59
+ if k * _HEAP_PATH_FACTOR <= n:
60
+ return _topk_heap(arr, k)
61
+ tmp = arr.copy()
62
+ if k < n:
63
+ nth_element(tmp, n - k)
64
+ result = tmp[n - k :]
65
+ result.sort()
66
+ return result[::-1].copy()
67
+
68
+
69
+ @cached_njit
70
+ def argmax2(arr):
71
+ """Index AND value of the maximum, in one pass: ``(idx, value)``.
72
+
73
+ Saves the second scan of the ``arr[np.argmax(arr)]`` idiom. First
74
+ occurrence wins on ties. Raises ``ValueError`` on empty input.
75
+
76
+ Complexity: O(n). Memory: O(1).
77
+ """
78
+ n = arr.shape[0]
79
+ if n == 0:
80
+ raise ValueError("argmax2: empty array")
81
+ best = arr[0]
82
+ best_idx = 0
83
+ for i in range(1, n):
84
+ if arr[i] > best:
85
+ best = arr[i]
86
+ best_idx = i
87
+ return best_idx, best
@@ -0,0 +1,21 @@
1
+ """Array utilities: searching, transforms, rolling windows, histograms."""
2
+
3
+ from numba_utils.arrays._hist import bincount, histogram
4
+ from numba_utils.arrays._rolling import rolling_mean, rolling_sum
5
+ from numba_utils.arrays._search import binary_search, lower_bound, upper_bound
6
+ from numba_utils.arrays._transform import cumulative_sum, fast_clip, normalize
7
+ from numba_utils.arrays._unique import unique_sorted
8
+
9
+ __all__ = [
10
+ "bincount",
11
+ "binary_search",
12
+ "cumulative_sum",
13
+ "fast_clip",
14
+ "histogram",
15
+ "lower_bound",
16
+ "normalize",
17
+ "rolling_mean",
18
+ "rolling_sum",
19
+ "unique_sorted",
20
+ "upper_bound",
21
+ ]
@@ -0,0 +1,56 @@
1
+ """Integer-optimized counting: bincount and fixed-range histogram."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+
7
+ from numba_utils.decorators import cached_njit
8
+
9
+
10
+ @cached_njit
11
+ def bincount(arr, minlength=0):
12
+ """Count occurrences of each value in a 1-D array of nonnegative ints.
13
+
14
+ ``result[v]`` is how many times ``v`` appears. Output length is
15
+ ``max(arr.max() + 1, minlength)``. Raises ``ValueError`` on negatives.
16
+
17
+ Complexity: O(n + max). Memory: O(max).
18
+ """
19
+ mx = minlength - 1
20
+ for i in range(arr.shape[0]):
21
+ v = arr[i]
22
+ if v < 0:
23
+ raise ValueError("bincount: negative values are not allowed")
24
+ if v > mx:
25
+ mx = v
26
+ counts = np.zeros(mx + 1, np.int64)
27
+ for i in range(arr.shape[0]):
28
+ counts[arr[i]] += 1
29
+ return counts
30
+
31
+
32
+ @cached_njit
33
+ def histogram(arr, bins, lo, hi):
34
+ """Histogram of 1-D ``arr`` over ``bins`` equal-width bins in ``[lo, hi]``.
35
+
36
+ Single pass, no edge array: bin index is computed by scaling, which is
37
+ why this beats ``np.histogram``. Values outside ``[lo, hi]`` are
38
+ ignored; ``hi`` itself lands in the last bin (NumPy convention).
39
+
40
+ Complexity: O(n + bins). Memory: O(bins).
41
+ """
42
+ if bins < 1:
43
+ raise ValueError("histogram: bins must be >= 1")
44
+ if not lo < hi:
45
+ raise ValueError("histogram: lo must be < hi")
46
+ counts = np.zeros(bins, np.int64)
47
+ scale = bins / (hi - lo)
48
+ for i in range(arr.shape[0]):
49
+ x = arr[i]
50
+ if x < lo or x > hi:
51
+ continue
52
+ idx = int((x - lo) * scale)
53
+ if idx >= bins:
54
+ idx = bins - 1
55
+ counts[idx] += 1
56
+ return counts