Skip to content

Commit c683fbb

Browse files
committed
Migrate to modern NumPy random API and add STUMPY_SEED test reproducibility (#707, #1112)
1 parent bccb485 commit c683fbb

53 files changed

Lines changed: 820 additions & 756 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.rst

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ Typical usage (1-dimensional time series data) with `STUMP <https://stumpy.readt
8282
import numpy as np
8383
8484
if __name__ == "__main__":
85-
your_time_series = np.random.rand(10000)
85+
your_time_series = np.random.default_rng().random(10000)
8686
window_size = 50 # Approximately, how many data points might be found in a pattern
8787
8888
matrix_profile = stumpy.stump(your_time_series, m=window_size)
@@ -97,7 +97,7 @@ Distributed usage for 1-dimensional time series data with Dask Distributed via `
9797
9898
if __name__ == "__main__":
9999
with Client() as dask_client:
100-
your_time_series = np.random.rand(10000)
100+
your_time_series = np.random.default_rng().random(10000)
101101
window_size = 50 # Approximately, how many data points might be found in a pattern
102102
103103
matrix_profile = stumpy.stumped(dask_client, your_time_series, m=window_size)
@@ -111,7 +111,7 @@ GPU usage for 1-dimensional time series data with `GPU-STUMP <https://stumpy.rea
111111
from numba import cuda
112112
113113
if __name__ == "__main__":
114-
your_time_series = np.random.rand(10000)
114+
your_time_series = np.random.default_rng().random(10000)
115115
window_size = 50 # Approximately, how many data points might be found in a pattern
116116
all_gpu_devices = [device.id for device in cuda.list_devices()] # Get a list of all available GPU devices
117117
@@ -125,7 +125,7 @@ Multi-dimensional time series data with `MSTUMP <https://stumpy.readthedocs.io/e
125125
import numpy as np
126126
127127
if __name__ == "__main__":
128-
your_time_series = np.random.rand(3, 1000) # Each row represents data from a different dimension while each column represents data from the same dimension
128+
your_time_series = np.random.default_rng().random((3, 1000)) # Each row represents data from a different dimension while each column represents data from the same dimension
129129
window_size = 50 # Approximately, how many data points might be found in a pattern
130130
131131
matrix_profile, matrix_profile_indices = stumpy.mstump(your_time_series, m=window_size)
@@ -140,7 +140,7 @@ Distributed multi-dimensional time series data analysis with Dask Distributed `M
140140
141141
if __name__ == "__main__":
142142
with Client() as dask_client:
143-
your_time_series = np.random.rand(3, 1000) # Each row represents data from a different dimension while each column represents data from the same dimension
143+
your_time_series = np.random.default_rng().random((3, 1000)) # Each row represents data from a different dimension while each column represents data from the same dimension
144144
window_size = 50 # Approximately, how many data points might be found in a pattern
145145
146146
matrix_profile, matrix_profile_indices = stumpy.mstumped(dask_client, your_time_series, m=window_size)
@@ -153,7 +153,7 @@ Time Series Chains with `Anchored Time Series Chains (ATSC) <https://stumpy.read
153153
import numpy as np
154154
155155
if __name__ == "__main__":
156-
your_time_series = np.random.rand(10000)
156+
your_time_series = np.random.default_rng().random(10000)
157157
window_size = 50 # Approximately, how many data points might be found in a pattern
158158
159159
matrix_profile = stumpy.stump(your_time_series, m=window_size)
@@ -174,7 +174,7 @@ Semantic Segmentation with `Fast Low-cost Unipotent Semantic Segmentation (FLUSS
174174
import numpy as np
175175
176176
if __name__ == "__main__":
177-
your_time_series = np.random.rand(10000)
177+
your_time_series = np.random.default_rng().random(10000)
178178
window_size = 50 # Approximately, how many data points might be found in a pattern
179179
180180
matrix_profile = stumpy.stump(your_time_series, m=window_size)
@@ -236,7 +236,7 @@ In order to fully understand and appreciate the underlying algorithms and applic
236236
Performance
237237
-----------
238238

239-
We tested the performance of computing the exact matrix profile using the Numba JIT compiled version of the code on randomly generated time series data with various lengths (i.e., ``np.random.rand(n)``) along with different `CPU and GPU hardware resources <hardware_>`_.
239+
We tested the performance of computing the exact matrix profile using the Numba JIT compiled version of the code on randomly generated time series data with various lengths (i.e., ``np.random.default_rng().random(n)``) along with different `CPU and GPU hardware resources <hardware_>`_.
240240

241241
.. image:: https://raw.githubusercontent.com/stumpy-dev/stumpy/main/docs/images/performance.png
242242
:alt: STUMPY Performance Plot

conftest.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,18 @@
44
# to fix eventual module import errors that can arise, for example when
55
# running tests from inside VS code.
66
# See https://stackoverflow.com/a/34520971
7+
8+
import os
9+
import numpy as np
10+
11+
# Set STUMPY_SEED for reproducible test failures.
12+
# If not already set (e.g., by test.sh), generate a random seed.
13+
# The seed is printed so it can be noted and reused to reproduce failures:
14+
# STUMPY_SEED=12345 pytest tests/test_stump.py
15+
if "STUMPY_SEED" not in os.environ:
16+
os.environ["STUMPY_SEED"] = str(np.random.default_rng().integers(2**31))
17+
18+
19+
def pytest_configure(config):
20+
"""Print STUMPY_SEED so failed tests can be reproduced."""
21+
print(f"\nSTUMPY_SEED={os.environ['STUMPY_SEED']}")

stumpy/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
STUMPY_EXCL_ZONE_DENOM = _STUMPY_DEFAULTS["STUMPY_EXCL_ZONE_DENOM"]
4141
STUMPY_FASTMATH_TRUE = _STUMPY_DEFAULTS["STUMPY_FASTMATH_TRUE"]
4242
STUMPY_FASTMATH_FLAGS = _STUMPY_DEFAULTS["STUMPY_FASTMATH_FLAGS"]
43+
RNG = np.random.default_rng()
44+
4345

4446

4547
def _reset(var=None):

stumpy/core.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3717,7 +3717,8 @@ def check_ignore_trivial(T_A, T_B, ignore_trivial):
37173717
import numpy as np
37183718
import warnings
37193719
3720-
T = np.random.rand(10_000)
3720+
rng = np.random.default_rng()
3721+
T = rng.random(10_000)
37213722
m = 50
37223723
with warnings.catch_warnings():
37233724
warnings.filterwarnings("ignore", message="Arrays T_A, T_B are equal")
@@ -4493,7 +4494,8 @@ def check_self_join(ignore_trivial):
44934494
import numpy as np
44944495
import warnings
44954496
4496-
T = np.random.rand(10_000)
4497+
rng = np.random.default_rng()
4498+
T = rng.random(10_000)
44974499
m = 50
44984500
with warnings.catch_warnings():
44994501
warnings.filterwarnings("ignore", message="`ignore_trivial` cannot be `False`")

stumpy/floss.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,13 @@ def _iac(
8484
IAC : numpy.ndarray
8585
Idealized arc curve (IAC)
8686
"""
87-
np.random.seed(seed)
87+
rng = np.random.default_rng(seed)
8888

89-
I = np.random.randint(0, width, size=width, dtype=np.int64)
89+
I = rng.integers(0, width, size=width, dtype=np.int64)
9090
if bidirectional is False: # Idealized 1-dimensional matrix profile index
9191
I[:-1] = width
9292
for i in range(width - 1):
93-
I[i] = np.random.randint(i + 1, width, dtype=np.int64)
93+
I[i] = rng.integers(i + 1, width, dtype=np.int64)
9494

9595
target_AC = _nnmark(I)
9696

@@ -99,7 +99,7 @@ def _iac(
9999
hist_dist = scipy.stats.rv_histogram(
100100
(target_AC, np.append(np.arange(width), width))
101101
)
102-
data = hist_dist.rvs(size=n_samples)
102+
data = hist_dist.rvs(size=n_samples, random_state=rng)
103103
a, b, c, d = scipy.stats.beta.fit(data, floc=0, fscale=width)
104104

105105
params[i, 0] = a

stumpy/scraamp.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def _preprocess_prescraamp(T_A, m, T_B=None, s=None):
7878
else: # AB-join
7979
s = int(np.ceil(m / config.STUMPY_EXCL_ZONE_DENOM))
8080

81-
indices = np.random.permutation(range(0, l, s)).astype(np.int64)
81+
indices = config.RNG.permutation(np.arange(0, l, s)).astype(np.int64)
8282

8383
return (T_A, T_B, T_A_subseq_isfinite, T_B_subseq_isfinite, indices, s, excl_zone)
8484

@@ -718,8 +718,8 @@ def __init__(
718718
core._merge_topk_PI(self._P, P, self._I, I)
719719

720720
if self._ignore_trivial:
721-
self._diags = np.random.permutation(
722-
range(self._excl_zone + 1, self._n_A - self._m + 1)
721+
self._diags = config.RNG.permutation(
722+
np.arange(self._excl_zone + 1, self._n_A - self._m + 1)
723723
).astype(np.int64)
724724
if self._diags.shape[0] == 0: # pragma: no cover
725725
max_m = core.get_max_window_size(self._T_A.shape[0])
@@ -728,8 +728,8 @@ def __init__(
728728
f"Please try a value of `m <= {max_m}`"
729729
)
730730
else:
731-
self._diags = np.random.permutation(
732-
range(-(self._n_A - self._m + 1) + 1, self._n_B - self._m + 1)
731+
self._diags = config.RNG.permutation(
732+
np.arange(-(self._n_A - self._m + 1) + 1, self._n_B - self._m + 1)
733733
).astype(np.int64)
734734

735735
self._n_threads = numba.config.NUMBA_NUM_THREADS

stumpy/scrump.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def _preprocess_prescrump(
116116
else: # AB-join
117117
s = int(np.ceil(m / config.STUMPY_EXCL_ZONE_DENOM))
118118

119-
indices = np.random.permutation(range(0, l, s)).astype(np.int64)
119+
indices = config.RNG.permutation(np.arange(0, l, s)).astype(np.int64)
120120

121121
return (
122122
T_A,
@@ -997,8 +997,8 @@ def __init__(
997997
core._merge_topk_PI(self._P, P, self._I, I)
998998

999999
if self._ignore_trivial:
1000-
self._diags = np.random.permutation(
1001-
range(self._excl_zone + 1, self._n_A - self._m + 1)
1000+
self._diags = config.RNG.permutation(
1001+
np.arange(self._excl_zone + 1, self._n_A - self._m + 1)
10021002
).astype(np.int64)
10031003
if self._diags.shape[0] == 0: # pragma: no cover
10041004
max_m = core.get_max_window_size(self._T_A.shape[0])
@@ -1007,8 +1007,8 @@ def __init__(
10071007
f"Please try a value of `m <= {max_m}`"
10081008
)
10091009
else:
1010-
self._diags = np.random.permutation(
1011-
range(-(self._n_A - self._m + 1) + 1, self._n_B - self._m + 1)
1010+
self._diags = config.RNG.permutation(
1011+
np.arange(-(self._n_A - self._m + 1) + 1, self._n_B - self._m + 1)
10121012
).astype(np.int64)
10131013

10141014
self._n_threads = numba.config.NUMBA_NUM_THREADS

test.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,10 @@ convert_notebooks()
347347
# Main #
348348
###########
349349

350+
# Set STUMPY_SEED for reproducible test failures
351+
export STUMPY_SEED=${STUMPY_SEED:-$RANDOM}
352+
echo "STUMPY_SEED=$STUMPY_SEED"
353+
350354
if [[ $test_mode == "show" ]]; then
351355
echo "Show development/test environment"
352356
show

tests/conftest.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import os
2+
import pytest
3+
import numpy as np
4+
from stumpy import config
5+
6+
seed = int(os.environ.get("STUMPY_SEED", np.random.default_rng().integers(2**31)))
7+
RNG = np.random.default_rng(seed)
8+
config.RNG = RNG
9+
10+
11+
@pytest.fixture(scope="module")
12+
def rng_state():
13+
yield RNG.bit_generator.state

tests/naive.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1818,7 +1818,7 @@ def prescrump(
18181818
P = np.full((l, k), np.inf, dtype=np.float64)
18191819
I = np.full((l, k), -1, dtype=np.int64)
18201820

1821-
for i in np.random.permutation(range(0, l, s)):
1821+
for i in config.RNG.permutation(np.arange(0, l, s)):
18221822
distance_profile = dist_matrix[i]
18231823
if exclusion_zone is not None:
18241824
apply_exclusion_zone(distance_profile, i, exclusion_zone, np.inf)
@@ -1914,13 +1914,13 @@ def scrump(
19141914
pass
19151915

19161916
if exclusion_zone is not None:
1917-
diags = np.random.permutation(range(exclusion_zone + 1, n_A - m + 1)).astype(
1918-
np.int64
1919-
)
1917+
diags = config.RNG.permutation(
1918+
np.arange(exclusion_zone + 1, n_A - m + 1)
1919+
).astype(np.int64)
19201920
else:
1921-
diags = np.random.permutation(range(-(n_A - m + 1) + 1, n_B - m + 1)).astype(
1922-
np.int64
1923-
)
1921+
diags = config.RNG.permutation(
1922+
np.arange(-(n_A - m + 1) + 1, n_B - m + 1)
1923+
).astype(np.int64)
19241924

19251925
n_chunks = int(np.ceil(1.0 / percentage))
19261926
ndist_counts = core._count_diagonal_ndist(diags, m, n_A, n_B)
@@ -1981,7 +1981,7 @@ def prescraamp(T_A, m, T_B, s, exclusion_zone=None, p=2.0, k=1):
19811981
P = np.full((l, k), np.inf, dtype=np.float64)
19821982
I = np.full((l, k), -1, dtype=np.int64)
19831983

1984-
for i in np.random.permutation(range(0, l, s)):
1984+
for i in config.RNG.permutation(np.arange(0, l, s)):
19851985
distance_profile = distance_matrix[i]
19861986
if exclusion_zone is not None:
19871987
apply_exclusion_zone(distance_profile, i, exclusion_zone, np.inf)
@@ -2054,13 +2054,13 @@ def scraamp(T_A, m, T_B, percentage, exclusion_zone, pre_scraamp, s, p=2.0, k=1)
20542054
l = n_A - m + 1
20552055

20562056
if exclusion_zone is not None:
2057-
diags = np.random.permutation(range(exclusion_zone + 1, n_A - m + 1)).astype(
2058-
np.int64
2059-
)
2057+
diags = config.RNG.permutation(
2058+
np.arange(exclusion_zone + 1, n_A - m + 1)
2059+
).astype(np.int64)
20602060
else:
2061-
diags = np.random.permutation(range(-(n_A - m + 1) + 1, n_B - m + 1)).astype(
2062-
np.int64
2063-
)
2061+
diags = config.RNG.permutation(
2062+
np.arange(-(n_A - m + 1) + 1, n_B - m + 1)
2063+
).astype(np.int64)
20642064

20652065
n_chunks = int(np.ceil(1.0 / percentage))
20662066
ndist_counts = core._count_diagonal_ndist(diags, m, n_A, n_B)

0 commit comments

Comments
 (0)