Skip to content

Commit 1f06f4b

Browse files
author
Luke Shaw
committed
Update benchmark and add oindex setitem
1 parent e5a2a94 commit 1f06f4b

4 files changed

Lines changed: 142 additions & 98 deletions

File tree

bench/ndarray/fancy_index.py

Lines changed: 100 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -14,123 +14,128 @@
1414
import time
1515
from memory_profiler import memory_usage, profile
1616
import matplotlib.pyplot as plt
17+
import zarr
18+
import h5py
19+
plt.rcParams.update({'text.usetex':True,'font.serif': ['cm'],'font.size':16})
20+
plt.rcParams['figure.dpi'] = 1000
21+
plt.rcParams['savefig.dpi'] = 1000
22+
plt.rc('text', usetex=True)
23+
plt.rc('font',**{'serif':['cm']})
24+
plt.style.use('seaborn-v0_8-paper')
1725

18-
MEM_PROFILE = False
26+
NUMPY_BLOSC = False
1927

20-
# @profile
21-
def fancyBlosc2(arr, fancyIdx):
28+
def genarray(r, ndims=1, verbose=True):
29+
d = int((r*2**30/8)**(1/ndims))
30+
shape = (d,) * ndims
31+
chunks = (d // 4,) * ndims
32+
blocks = (max(d // 10, 1),) * ndims
2233
t = time.time()
23-
res = arr[fancyIdx]
24-
dt = time.time() - t
25-
return res, dt
26-
27-
# @profile
28-
def fancyNumpy(arr, fancyIdx):
29-
t = time.time()
30-
res = arr[:]
31-
res = res[fancyIdx]
32-
dt = time.time() - t
33-
return res, dt
34-
35-
def genarray(d, verbose=False):
36-
shape = (d,) * 4
37-
chunks = (d // 4,) * 4
38-
blocks = (max(d // 10, 1),) * 4
39-
t = time.time()
40-
arr = blosc2.ones(shape=shape, chunks=chunks, blocks=blocks) # , urlpath=file, mode="w")
34+
arr = blosc2.ones(shape=shape, chunks=chunks, blocks=blocks, dtype=np.int64) # , urlpath=file, mode="w")
4135
t = time.time() - t
4236
if verbose:
4337
print(f"Array shape: {arr.shape}")
4438
print(f"Array size: {np.prod(arr.shape) * arr.dtype.itemsize / 2 ** 30:.6f} GB")
4539
print(f"Time to create array: {t:.6f} seconds")
4640
return arr
4741

48-
if __name__ == '__main__':
49-
blosc_times = []
50-
np_times = []
51-
sizes = []
52-
# dims = np.int64(np.array([2**6.76, 2**6.8, 2**6.85, 2**6.9, 2**6.95]))
53-
dims = np.int64(np.array([2**4.3, 2**5.25, 2**6.25, 2**6.75, 2**7, 2**7.25, 2**7.4, 2**7.5]))
54-
rng = np.random.default_rng()
42+
blosc_times = []
43+
np_times = []
44+
zarr_times = []
45+
sizes = []
46+
dims = np.int64(np.array([1, 2, 4, 6, 8]))
47+
rng = np.random.default_rng()
48+
blosctimes = []
49+
nptimes = []
50+
zarrtimes = []
51+
h5pytimes = []
5552

56-
for d in dims:
57-
arr = genarray(d)
58-
arr_size = np.prod(arr.shape) * arr.dtype.itemsize / 2 ** 30
59-
print(arr_size)
60-
sizes.append(arr_size)
61-
idx = rng.integers(low=0, high=d, size=(d,))
62-
if MEM_PROFILE:
63-
fancyIdx = np.s_[idx, :, :d // 2, d // 2:]
53+
for d in dims:
54+
arr = genarray(d, ndims=2)
55+
sizes.append(d)
56+
idx = rng.integers(low=0, high=arr.shape[0], size=(arr.shape[0],))
57+
row = np.arange(arr.shape[0])
58+
col = row
59+
mask = rng.integers(low=0, high=2, size=(d,)) == 1
6460

65-
interval = 0.001
66-
offset = 0
67-
for f in [fancyBlosc2, fancyNumpy]:
68-
mem = memory_usage((f, (arr, fancyIdx)), interval=interval)
69-
times = offset + interval * np.arange(len(mem))
70-
offset = times[-1]
71-
plt.plot(times, mem)
7261

73-
plt.xlabel('Time (s)')
74-
plt.ylabel('Memory usage (MiB)')
75-
plt.title('Memory usage fancy indexing')
76-
plt.legend(['blosc2', 'numpy'])
77-
plt.savefig(f'plots/MemoryUsagefancyIdx_d{d}.png', format="png")
62+
## Test fancy indexing for different use cases
63+
m, M = np.min(idx), np.max(idx)
64+
def timer(arr, skip_flag=True, row=row, col=col):
65+
time_list = []
66+
if not skip_flag:
67+
t = time.time()
68+
b = arr[row, col]
69+
time_list += [time.time() - t]
70+
t = time.time()
71+
b = arr[slice(1, M // 2, 5), col]
72+
time_list += [time.time() - t]
73+
t = time.time()
74+
b = arr[[[m // 2, M // 2], [m // 4, M // 4]]]
75+
time_list += [time.time() - t]
76+
t = time.time()
77+
b = arr[[m, M//2, M]]
78+
time_list += [time.time() - t]
79+
t = time.time()
80+
b = arr[m, col]
81+
time_list += [time.time() - t]
82+
return np.array(time_list)
7883

79-
row = idx
80-
col = rng.permutation(idx)
81-
mask = rng.integers(low=0, high=2, size=(d,)) == 1
84+
if NUMPY_BLOSC:
85+
blosctimes += [timer(arr, skip_flag=False, row=idx, col=idx)]
86+
arr=arr[:]
87+
nptimes += [timer(arr, skip_flag=False, row=idx, col=idx)]
88+
else:
89+
blosctimes += [timer(arr)]
90+
arr = arr[:]
91+
nptimes += [timer(arr)]
92+
z_test = zarr.zeros(shape=arr.shape, dtype=arr.dtype)
93+
z_test[:] = arr
94+
# zarr is more limited, as must provide same number of coord arrays as dims of array
95+
# also cannot mix with slices
96+
zarrtimes += [timer(z_test)]
97+
with h5py.File('my_hdf5_file.h5', 'w') as f:
98+
dset = f.create_dataset("init", data=arr)
99+
h5pytimes += [timer(dset)]
82100

83-
## Test fancy indexing for different use cases
84-
loc_blosc_times = []
85-
loc_np_times = []
86-
m, M = np.min(idx), np.max(idx)
87-
for i, fancyIdx in enumerate([
88-
[m, M//2, M], # i)
89-
[[[m//2, M//2], [m//4, M//4]]], # ii)
90-
[row, col], # iii)
91-
# [row[:, None], col], # iv)
92-
[m, col], # v)
93-
[slice(1, M//2, 5), col], # vi)
94-
# [row[:, None], mask], # vii)
95-
]):
96-
# print(f'\n(case {i + 1})')
97-
try:
98-
r, c = fancyIdx
99-
idx = (r, c)
100-
except:
101-
r, c = fancyIdx, None
102-
idx = r
103-
b, blosctime = fancyBlosc2(arr,idx)
104-
n, nptime = fancyNumpy(arr,idx)
105-
slice_size = np.prod(b.shape) * b.dtype.itemsize / 2 ** 30
106-
np.testing.assert_allclose(b, n)
107-
loc_blosc_times.append(blosctime)
108-
loc_np_times.append(nptime)
109-
blosc_times.append(loc_blosc_times)
110-
np_times.append(loc_np_times)
101+
x = np.arange(len(sizes))
102+
width = 0.2
103+
blosctimes = np.array(blosctimes)
104+
nptimes = np.array(nptimes)
105+
if NUMPY_BLOSC:
106+
# Create bars for axis 0 plot
107+
for i, r in enumerate((["Numpy", nptimes, -width], ["Blosc2", blosctimes, 0])):
108+
label, times, w = r
109+
c = ['b', 'r'][i]
110+
plt.bar(x + w, times.mean(axis=1), width, color=c, alpha=0.5)
111+
plt.bar(x + w, times.max(axis=1), width, color=c, alpha=0.25)
112+
plt.bar(x + w, times.min(axis=1), width, label=label, color=c, alpha=1)
111113

112-
blosc_times = np.array(blosc_times)
113-
np_times = np.array(np_times)
114-
x = np.arange(len(sizes))
115-
width = 0.25
114+
plt.xlabel('Array size (GB)')
115+
plt.legend()
116+
plt.xticks(x, np.round(sizes, 2))
117+
plt.ylabel("Time (s)")
118+
plt.title('Fancy indexing NumPy vs Blosc2')
119+
plt.savefig('plots/fancyIdxNumpyVsBlosc.png', format="png")
120+
plt.show()
121+
else:
122+
zarrtimes = np.array(zarrtimes)
123+
h5pytimes = np.array(h5pytimes)
116124

117125
# Create bars for axis 0 plot
118-
plt.bar(x - width, np_times.mean(axis=1), width, color='b', alpha=0.5)
119-
plt.bar(x - width, np_times.max(axis=1), width, color='b', alpha=0.25)
120-
plt.bar(x - width, np_times.min(axis=1), width, label='NumPy', color='b', alpha=1)
121-
plt.bar(x, blosc_times.mean(axis=1), width, color='r', alpha=0.5)
122-
plt.bar(x, blosc_times.max(axis=1), width, color='r', alpha=0.25)
123-
plt.bar(x, blosc_times.min(axis=1), width, label='Blosc2', color='r', alpha=1)
124-
plt.bar([],[], label='max', color='k', alpha=0.25)
125-
plt.bar([],[], label='min', color='k', alpha=1)
126-
plt.bar([],[], label='mean', color='k', alpha=0.5)
126+
for i, r in enumerate((["Numpy",nptimes,-2*width],["Blosc2",blosctimes, -width],["Zarr",zarrtimes, 0],["HDF5",h5pytimes, width])):
127+
label,times,w = r
128+
c = ['b', 'r', 'g', 'm'][i]
129+
plt.bar(x + w, times.mean(axis=1), width, color=c, alpha=0.5)
130+
plt.bar(x + w, times.max(axis=1), width, color=c, alpha=0.25)
131+
plt.bar(x + w, times.min(axis=1), width, label=label, color=c, alpha=1)
127132

128133
plt.xlabel('Array size (GB)')
129134
plt.legend()
130135
plt.xticks(x, np.round(sizes, 2))
131136
plt.ylabel("Time (s)")
132-
plt.title('Fancy indexing, Blosc2 vs NumPy')
137+
plt.title('Fancy indexing performance comparison')
133138
plt.savefig('plots/fancyIdx.png', format="png")
134139
plt.show()
135-
## slowest cases are the ones with broadcasting
136-
## in fact it's a memory problem for blosc2 seemingly
140+
141+
print("Finished everything!")

src/blosc2/blosc2_ext.pyx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,9 @@ cdef extern from "b2nd.h":
513513
int b2nd_get_orthogonal_selection(const b2nd_array_t *array, int64_t ** selection,
514514
int64_t *selection_size, void *buffer,
515515
int64_t *buffershape, int64_t buffersize)
516+
int b2nd_set_orthogonal_selection(const b2nd_array_t *array, int64_t ** selection,
517+
int64_t *selection_size, void *buffer,
518+
int64_t *buffershape, int64_t buffersize)
516519
int b2nd_from_schunk(blosc2_schunk *schunk, b2nd_array_t **array)
517520

518521
void blosc2_unidim_to_multidim(uint8_t ndim, int64_t *shape, int64_t i, int64_t *index)
@@ -2426,7 +2429,6 @@ cdef class NDArray:
24262429
"""
24272430
Orthogonal indexing. Key is a tuple of lists of integer indices.
24282431
"""
2429-
## have to maybe edit key to be C-compatible
24302432
if len(key) != self.array.ndim:
24312433
raise ValueError(f"Key must have {self.array.ndim} dimensions, got {len(key)}.")
24322434
cdef int64_t[B2ND_MAX_DIM] buffershape_
@@ -2455,6 +2457,38 @@ cdef class NDArray:
24552457
free(key_)
24562458
return arr
24572459

2460+
def set_oindex_numpy(self, key, arr):
2461+
"""
2462+
Orthogonal indexing. Set elements of self with arr using key.
2463+
"""
2464+
if len(key) != self.array.ndim:
2465+
raise ValueError(f"Key must have {self.array.ndim} dimensions, got {len(key)}.")
2466+
cdef int64_t[B2ND_MAX_DIM] buffershape_
2467+
cdef int64_t** key_
2468+
cdef int64_t buffersize_ = self.array.sc.typesize
2469+
cdef int64_t[B2ND_MAX_DIM] sel_size
2470+
2471+
key_ = <int64_t**> malloc(len(key) * sizeof(int64_t *))
2472+
2473+
for i in range(self.array.ndim):
2474+
buffershape_[i] = len(key[i])
2475+
buffersize_ *= buffershape_[i]
2476+
sel_size[i] = len(key[i])
2477+
key_[i] = <int64_t *> malloc(sel_size[i] * sizeof(int64_t))
2478+
for j in range(len(key[i])):
2479+
key_[i][j] = key[i][j]
2480+
2481+
cdef Py_buffer buf
2482+
PyObject_GetBuffer(arr, &buf, PyBUF_SIMPLE)
2483+
2484+
_check_rc(b2nd_set_orthogonal_selection(self.array, key_, sel_size, buf.buf,
2485+
buffershape_, buffersize_), "Error while getting orthogonal selection")
2486+
PyBuffer_Release(&buf)
2487+
for i in range(len(key)):
2488+
free(key_[i]) # Free the allocated memory for each key
2489+
free(key_)
2490+
return arr
2491+
24582492

24592493
def get_slice(self, key, mask, **kwargs):
24602494
start, stop = key

src/blosc2/ndarray.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1469,9 +1469,11 @@ def get_oselection_numpy(self, key):
14691469
shape = tuple(len(k) for k in key) + self.shape[len(key) :]
14701470
# Create the array to store the result
14711471
arr = np.empty(shape, dtype=self.dtype)
1472-
14731472
return super().get_oindex_numpy(arr, key)
14741473

1474+
def set_oselection_numpy(self, key, arr):
1475+
return super().set_oindex_numpy(key, arr)
1476+
14751477
def __getitem__( # noqa: C901
14761478
self,
14771479
key: int | slice | Sequence[slice | int] | np.ndarray[np.bool_] | NDArray | blosc2.LazyExpr | str,
@@ -4386,10 +4388,12 @@ class OIndex:
43864388
def __init__(self, array: NDArray):
43874389
self.array = array
43884390

4389-
# TODO: add __setitem__
43904391
def __getitem__(self, selection) -> np.ndarray:
43914392
return self.array.get_oselection_numpy(selection)
43924393

4394+
def __setitem__(self, selection, input) -> np.ndarray:
4395+
return self.array.set_oselection_numpy(selection, input)
4396+
43934397

43944398
class VIndex:
43954399
def __init__(self, array: NDArray):

tests/ndarray/test_ndarray.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ def test_save():
269269

270270

271271
def test_oindex():
272+
# Test Get
272273
ndim = 3
273274
shape = (10,) * ndim
274275
arr = blosc2.linspace(0, 100, num=np.prod(shape), shape=shape, dtype="i4")

0 commit comments

Comments
 (0)