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.
- numba_utils/__init__.py +137 -0
- numba_utils/_config.py +92 -0
- numba_utils/algorithms/__init__.py +26 -0
- numba_utils/algorithms/_select.py +123 -0
- numba_utils/algorithms/_sort.py +166 -0
- numba_utils/algorithms/_topk.py +87 -0
- numba_utils/arrays/__init__.py +21 -0
- numba_utils/arrays/_hist.py +56 -0
- numba_utils/arrays/_rolling.py +50 -0
- numba_utils/arrays/_search.py +64 -0
- numba_utils/arrays/_transform.py +89 -0
- numba_utils/arrays/_unique.py +34 -0
- numba_utils/collections/__init__.py +28 -0
- numba_utils/collections/_bitset.py +57 -0
- numba_utils/collections/_heap.py +72 -0
- numba_utils/collections/_sparse.py +105 -0
- numba_utils/collections/_stack_queue.py +123 -0
- numba_utils/collections/_typed.py +58 -0
- numba_utils/decorators/__init__.py +19 -0
- numba_utils/decorators/_wrappers.py +118 -0
- numba_utils/diagnostics/__init__.py +13 -0
- numba_utils/diagnostics/_inspect.py +190 -0
- numba_utils/parallel/__init__.py +31 -0
- numba_utils/parallel/_histogram.py +59 -0
- numba_utils/parallel/_reduce.py +86 -0
- numba_utils/parallel/_scan.py +65 -0
- numba_utils/parallel/_topk.py +86 -0
- numba_utils/profiling/__init__.py +21 -0
- numba_utils/profiling/_compare.py +118 -0
- numba_utils/profiling/_timer.py +144 -0
- numba_utils/random/__init__.py +27 -0
- numba_utils/random/_rng.py +59 -0
- numba_utils/random/_sampling.py +142 -0
- numba_utils/testing/__init__.py +24 -0
- numba_utils/testing/_asserts.py +93 -0
- numba_utils/testing/_generators.py +63 -0
- numba_utils-0.1.0.dist-info/METADATA +191 -0
- numba_utils-0.1.0.dist-info/RECORD +41 -0
- numba_utils-0.1.0.dist-info/WHEEL +5 -0
- numba_utils-0.1.0.dist-info/licenses/LICENSE +21 -0
- numba_utils-0.1.0.dist-info/top_level.txt +1 -0
numba_utils/__init__.py
ADDED
|
@@ -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
|