Skip to content

Commit b4deb3a

Browse files
authored
Merge pull request #423 from Blosc/concatenate
Concatenate
2 parents 8bc8ce2 + 0eefad5 commit b4deb3a

File tree

10 files changed

+440
-3
lines changed

10 files changed

+440
-3
lines changed

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ else()
5050
include(FetchContent)
5151
FetchContent_Declare(blosc2
5252
GIT_REPOSITORY https://github.com/Blosc/c-blosc2
53-
GIT_TAG 4ef3c7440a85632e6c8b6c5d2a9e651e45569fc1 # v2.17.1 + mmap fix
53+
GIT_TAG 1c2f8bb0c914c43e23b751fbcf6642cd7aec09db # v2.18.0 (concatenate added)
5454
)
5555
FetchContent_MakeAvailable(blosc2)
5656
include_directories("${blosc2_SOURCE_DIR}/include")

README_DEVELOPERS.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@ You are done!
2222
pip install . # add -e for editable mode
2323
```
2424

25+
There are situations where you may want to build the C-Blosc2 library separately, for example, when debugging issues in the C library. In that case, let's assume you have the C-Blosc2 library installed in `/usr/local`:
26+
27+
```bash
28+
CMAKE_PREFIX_PATH=/usr/local USE_SYSTEM_BLOSC2=1 pip install -e .
29+
```
30+
31+
and then, you can run the tests with:
32+
33+
```bash
34+
LD_LIBRARY_PATH=/usr/local/lib pytest
35+
```
36+
37+
[replace `LD_LIBRARY_PATH` with the appropriate environment variable for your system, such as `DYLD_LIBRARY_PATH` on macOS or `PATH` on Windows, if necessary].
38+
2539
That's it! You can now proceed to the testing section.
2640

2741
## Testing

bench/ndarray/concatenate.py

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
#######################################################################
2+
# Copyright (c) 2019-present, Blosc Development Team <[email protected]>
3+
# All rights reserved.
4+
#
5+
# This source code is licensed under a BSD-style license (found in the
6+
# LICENSE file in the root directory of this source tree)
7+
#######################################################################
8+
9+
import numpy as np
10+
import blosc2
11+
import time
12+
import matplotlib.pyplot as plt
13+
import os
14+
from matplotlib.ticker import ScalarFormatter
15+
16+
17+
def run_benchmark(num_arrays=10, size=500, aligned_chunks=False, axis=0):
18+
"""
19+
Benchmark blosc2.concatenate performance with different chunk alignments.
20+
21+
Parameters:
22+
- num_arrays: Number of arrays to concatenate
23+
- size: Base size for array dimensions
24+
- aligned_chunks: Whether to use aligned chunk shapes
25+
- axis: Axis along which to concatenate (0 or 1)
26+
27+
Returns:
28+
- duration: Time taken in seconds
29+
- result_shape: Shape of the resulting array
30+
- data_size_gb: Size of data processed in GB
31+
"""
32+
if axis == 0:
33+
# For concatenating along axis 0, the second dimension must be consistent
34+
shapes = [(size // num_arrays, size) for _ in range(num_arrays)]
35+
elif axis == 1:
36+
# For concatenating along axis 1, the first dimension must be consistent
37+
shapes = [(size, size // num_arrays) for _ in range(num_arrays)]
38+
else:
39+
raise ValueError("Only axis 0 and 1 are supported")
40+
41+
# Create appropriate chunk shapes
42+
if aligned_chunks:
43+
# Aligned chunks: divisors of the shape dimensions
44+
chunk_shapes = [(shape[0] // 4, shape[1] // 4) for shape in shapes]
45+
else:
46+
# Unaligned chunks: not divisors of shape dimensions
47+
chunk_shapes = [(shape[0] // 4 + 1, shape[1] // 4 - 1) for shape in shapes]
48+
49+
# Create arrays
50+
arrays = []
51+
for i, (shape, chunk_shape) in enumerate(zip(shapes, chunk_shapes)):
52+
arr = blosc2.arange(
53+
i * np.prod(shape), (i + 1) * np.prod(shape), 1, dtype="i4", shape=shape, chunks=chunk_shape
54+
)
55+
arrays.append(arr)
56+
57+
# Calculate total data size in GB (4 bytes per int32)
58+
total_elements = sum(np.prod(shape) for shape in shapes)
59+
data_size_gb = total_elements * 4 / (1024**3) # Convert bytes to GB
60+
61+
# Time the concatenation
62+
start_time = time.time()
63+
result = blosc2.concatenate(arrays, axis=axis)
64+
duration = time.time() - start_time
65+
66+
return duration, result.shape, data_size_gb
67+
68+
69+
def run_numpy_benchmark(num_arrays=10, size=500, axis=0):
70+
"""
71+
Benchmark numpy.concatenate performance for comparison.
72+
73+
Parameters:
74+
- num_arrays: Number of arrays to concatenate
75+
- size: Base size for array dimensions
76+
- axis: Axis along which to concatenate (0 or 1)
77+
78+
Returns:
79+
- duration: Time taken in seconds
80+
- result_shape: Shape of the resulting array
81+
- data_size_gb: Size of data processed in GB
82+
"""
83+
if axis == 0:
84+
# For concatenating along axis 0, the second dimension must be consistent
85+
shapes = [(size // num_arrays, size) for _ in range(num_arrays)]
86+
elif axis == 1:
87+
# For concatenating along axis 1, the first dimension must be consistent
88+
shapes = [(size, size // num_arrays) for _ in range(num_arrays)]
89+
else:
90+
raise ValueError("Only axis 0 and 1 are supported")
91+
92+
# Create arrays
93+
numpy_arrays = []
94+
for i, shape in enumerate(shapes):
95+
arr = np.arange(
96+
i * np.prod(shape),
97+
(i + 1) * np.prod(shape),
98+
1,
99+
dtype="i4"
100+
).reshape(shape)
101+
numpy_arrays.append(arr)
102+
103+
# Calculate total data size in GB (4 bytes per int32)
104+
total_elements = sum(np.prod(shape) for shape in shapes)
105+
data_size_gb = total_elements * 4 / (1024**3) # Convert bytes to GB
106+
107+
# Time the concatenation
108+
start_time = time.time()
109+
result = np.concatenate(numpy_arrays, axis=axis)
110+
duration = time.time() - start_time
111+
112+
return duration, result.shape, data_size_gb
113+
114+
115+
def create_combined_plot(num_arrays, sizes, numpy_speeds_axis0, unaligned_speeds_axis0, aligned_speeds_axis0,
116+
numpy_speeds_axis1, unaligned_speeds_axis1, aligned_speeds_axis1, output_dir="plots"):
117+
"""
118+
Create a figure with two side-by-side bar plots comparing the performance for both axes.
119+
120+
Parameters:
121+
- sizes: List of array sizes
122+
- *_speeds_axis0: Lists of speeds (GB/s) for axis 0 concatenation
123+
- *_speeds_axis1: Lists of speeds (GB/s) for axis 1 concatenation
124+
- output_dir: Directory to save the plot
125+
"""
126+
# Create output directory if it doesn't exist
127+
os.makedirs(output_dir, exist_ok=True)
128+
129+
# Set up the figure with two subplots side by side
130+
fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(20, 8), sharey=True)
131+
132+
# Convert sizes to strings for the x-axis
133+
x_labels = [str(size) for size in sizes]
134+
x = np.arange(len(sizes))
135+
width = 0.25
136+
137+
# Create bars for axis 0 plot
138+
rect1_axis0 = ax0.bar(x - width, numpy_speeds_axis0, width, label='NumPy', color='#1f77b4')
139+
rect2_axis0 = ax0.bar(x, unaligned_speeds_axis0, width, label='Blosc2 Unaligned', color='#ff7f0e')
140+
rect3_axis0 = ax0.bar(x + width, aligned_speeds_axis0, width, label='Blosc2 Aligned', color='#2ca02c')
141+
142+
# Create bars for axis 1 plot
143+
rect1_axis1 = ax1.bar(x - width, numpy_speeds_axis1, width, label='NumPy', color='#1f77b4')
144+
rect2_axis1 = ax1.bar(x, unaligned_speeds_axis1, width, label='Blosc2 Unaligned', color='#ff7f0e')
145+
rect3_axis1 = ax1.bar(x + width, aligned_speeds_axis1, width, label='Blosc2 Aligned', color='#2ca02c')
146+
147+
# Add labels and titles
148+
for ax, axis in [(ax0, 0), (ax1, 1)]:
149+
ax.set_xlabel('Array Size (N for NxN array)', fontsize=12)
150+
ax.set_title(f'Concatenation Performance for {num_arrays} arrays (axis={axis})', fontsize=14)
151+
ax.set_xticks(x)
152+
ax.set_xticklabels(x_labels)
153+
ax.grid(True, axis='y', linestyle='--', alpha=0.7)
154+
ax.yaxis.set_major_formatter(ScalarFormatter(useOffset=False))
155+
156+
# Add legend inside each plot
157+
ax.legend(title="Concatenation Methods",
158+
loc='upper left',
159+
fontsize=12,
160+
frameon=True,
161+
facecolor='white',
162+
edgecolor='black',
163+
framealpha=0.8)
164+
165+
# Add y-label only to the left subplot
166+
ax0.set_ylabel('Throughput (GB/s)', fontsize=12)
167+
168+
# Add value labels on top of the bars
169+
def autolabel(rects, ax):
170+
for rect in rects:
171+
height = rect.get_height()
172+
ax.annotate(f'{height:.2f} GB/s',
173+
xy=(rect.get_x() + rect.get_width() / 2, height),
174+
xytext=(0, 3), # 3 points vertical offset
175+
textcoords="offset points",
176+
ha='center', va='bottom', rotation=90, fontsize=8)
177+
178+
autolabel(rect1_axis0, ax0)
179+
autolabel(rect2_axis0, ax0)
180+
autolabel(rect3_axis0, ax0)
181+
182+
autolabel(rect1_axis1, ax1)
183+
autolabel(rect2_axis1, ax1)
184+
autolabel(rect3_axis1, ax1)
185+
186+
# Save the plot
187+
plt.tight_layout()
188+
plt.savefig(os.path.join(output_dir, 'concatenate_benchmark_combined.png'), dpi=300)
189+
plt.show()
190+
plt.close()
191+
192+
print(f"Combined plot saved to {os.path.join(output_dir, 'concatenate_benchmark_combined.png')}")
193+
194+
195+
def main():
196+
print(f"{'=' * 60}")
197+
print(f"Blosc2 vs NumPy concatenation benchmark")
198+
print(f"{'=' * 60}")
199+
200+
# Parameters
201+
sizes = [500, 1000, 2000, 4000] #, 10000] # must be divisible by 4 for aligned chunks
202+
num_arrays = 10
203+
204+
# Lists to store results for both axes
205+
numpy_speeds_axis0 = []
206+
unaligned_speeds_axis0 = []
207+
aligned_speeds_axis0 = []
208+
numpy_speeds_axis1 = []
209+
unaligned_speeds_axis1 = []
210+
aligned_speeds_axis1 = []
211+
212+
for axis in [0, 1]:
213+
print(f"\nConcatenating {num_arrays} arrays along axis {axis}")
214+
print(f"{'Size':<10} {'NumPy (GB/s)':<14} {'Unaligned (GB/s)':<18} {'Aligned (GB/s)':<16} {'Alig vs Unalig':<16} {'Alig vs NumPy':<16}")
215+
print(f"{'-' * 90}")
216+
217+
for size in sizes:
218+
# Run the benchmarks
219+
numpy_time, numpy_shape, data_size_gb = run_numpy_benchmark(num_arrays, size, axis=axis)
220+
unaligned_time, shape1, _ = run_benchmark(num_arrays, size, aligned_chunks=False, axis=axis)
221+
aligned_time, shape2, _ = run_benchmark(num_arrays, size, aligned_chunks=True, axis=axis)
222+
223+
# Calculate throughputs in GB/s
224+
numpy_speed = data_size_gb / numpy_time if numpy_time > 0 else float("inf")
225+
unaligned_speed = data_size_gb / unaligned_time if unaligned_time > 0 else float("inf")
226+
aligned_speed = data_size_gb / aligned_time if aligned_time > 0 else float("inf")
227+
228+
# Store speeds in the appropriate list
229+
if axis == 0:
230+
numpy_speeds_axis0.append(numpy_speed)
231+
unaligned_speeds_axis0.append(unaligned_speed)
232+
aligned_speeds_axis0.append(aligned_speed)
233+
else:
234+
numpy_speeds_axis1.append(numpy_speed)
235+
unaligned_speeds_axis1.append(unaligned_speed)
236+
aligned_speeds_axis1.append(aligned_speed)
237+
238+
# Calculate speedup ratios
239+
aligned_vs_unaligned = aligned_speed / unaligned_speed if unaligned_speed > 0 else float("inf")
240+
aligned_vs_numpy = aligned_speed / numpy_speed if numpy_speed > 0 else float("inf")
241+
242+
# Print results
243+
print(f"{size:<10} {numpy_speed:<14.2f} {unaligned_speed:<18.2f} {aligned_speed:<16.2f} "
244+
f"{aligned_vs_unaligned:>10.2f}x {aligned_vs_numpy:>10.2f}x")
245+
246+
# Quick verification of result shape
247+
if axis == 0:
248+
expected_shape = (size, size) # After concatenation along axis 0
249+
else:
250+
expected_shape = (size, size) # After concatenation along axis 1
251+
252+
# Verify shapes match
253+
shapes = [numpy_shape, shape1, shape2]
254+
if any(shape != expected_shape for shape in shapes):
255+
for i, shape_name in enumerate(["NumPy", "Blosc2 unaligned", "Blosc2 aligned"]):
256+
if shapes[i] != expected_shape:
257+
print(f"Warning: {shape_name} shape {shapes[i]} does not match expected {expected_shape}")
258+
259+
print(f"{'=' * 90}")
260+
261+
# Create the combined plot with both axes
262+
create_combined_plot(
263+
num_arrays,
264+
sizes,
265+
numpy_speeds_axis0, unaligned_speeds_axis0, aligned_speeds_axis0,
266+
numpy_speeds_axis1, unaligned_speeds_axis1, aligned_speeds_axis1
267+
)
268+
269+
270+
if __name__ == "__main__":
271+
main()
243 KB
Loading

doc/reference/ndarray.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ Constructors
7272

7373
asarray
7474
copy
75+
concatenate
7576
empty
7677
frombuffer
7778
fromiter

src/blosc2/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ class Tuner(Enum):
238238
sort,
239239
reshape,
240240
copy,
241+
concatenate,
241242
empty,
242243
frombuffer,
243244
fromiter,

src/blosc2/blosc2_ext.pyx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,8 @@ cdef extern from "b2nd.h":
507507
int b2nd_squeeze_index(b2nd_array_t *array, const c_bool *index)
508508
int b2nd_resize(b2nd_array_t *array, const int64_t *new_shape, const int64_t *start)
509509
int b2nd_copy(b2nd_context_t *ctx, b2nd_array_t *src, b2nd_array_t **array)
510+
int b2nd_concatenate(b2nd_context_t *ctx, b2nd_array_t *src1, b2nd_array_t *src2,
511+
int8_t axis, c_bool copy, b2nd_array_t **array)
510512
int b2nd_from_schunk(blosc2_schunk *schunk, b2nd_array_t **array)
511513

512514
void blosc2_unidim_to_multidim(uint8_t ndim, int64_t *shape, int64_t i, int64_t *index)
@@ -2867,3 +2869,26 @@ def schunk_get_slice_nchunks(schunk: SChunk, key):
28672869
res[i] = chunks_idx[i]
28682870
free(chunks_idx)
28692871
return res
2872+
2873+
2874+
def concatenate(arr1: NDArray, arr2: NDArray, axis: int, **kwargs):
2875+
"""
2876+
Concatenate two NDArray objects along a specified axis.
2877+
"""
2878+
cdef c_bool copy = kwargs.pop("copy", True)
2879+
cdef b2nd_context_t *ctx = create_b2nd_context(arr1.shape, arr1.chunks, arr1.blocks, arr1.dtype, kwargs)
2880+
if ctx == NULL:
2881+
raise RuntimeError("Error while creating the context for concatenation")
2882+
2883+
cdef b2nd_array_t *array
2884+
_check_rc(b2nd_concatenate(ctx, arr1.array, arr2.array, axis, copy, &array),
2885+
"Error while concatenating the arrays")
2886+
_check_rc(b2nd_free_ctx(ctx), "Error while freeing the context")
2887+
2888+
if copy:
2889+
# We have copied the concatenated data into a new array
2890+
return blosc2.NDArray(_schunk=PyCapsule_New(array.sc, <char *> "blosc2_schunk*", NULL),
2891+
_array=PyCapsule_New(array, <char *> "b2nd_array_t*", NULL))
2892+
else:
2893+
# Return the first array, which now contains the concatenated data
2894+
return arr1

src/blosc2/exceptions.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
21
class MissingOperands(ValueError):
32
def __init__(self, expr, missing_ops):
43
self.expr = expr
54
self.missing_ops = missing_ops
65

7-
message = f"Lazy expression \"{expr}\" with missing operands: {missing_ops}"
6+
message = f'Lazy expression "{expr}" with missing operands: {missing_ops}'
87
super().__init__(message)

0 commit comments

Comments
 (0)