Skip to content

Commit c259937

Browse files
committed
Replace util.sparse sliding_win_oneaxis with a much faster version. Old one still there in sliding_win_oneaxis_old.
1 parent 60e83fc commit c259937

File tree

1 file changed

+96
-2
lines changed

1 file changed

+96
-2
lines changed

src/ezmsg/sigproc/util/sparse.py

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import numpy as np
12
import sparse
23

34

4-
def sliding_win_oneaxis(
5+
def sliding_win_oneaxis_old(
56
s: sparse.SparseArray, nwin: int, axis: int, step: int = 1
67
) -> sparse.SparseArray:
78
"""
89
Like `ezmsg.util.messages.axisarray.sliding_win_oneaxis` but for sparse arrays.
10+
This approach is about 4x slower than the version that uses coordinate arithmetic below.
911
1012
Args:
1113
s: The input sparse array.
@@ -25,5 +27,97 @@ def sliding_win_oneaxis(
2527
full_slices[: axis + 1] + (sl,) + full_slices[axis + 2 :] for sl in targ_slices
2628
]
2729
result = sparse.concatenate([s[_] for _ in full_slices], axis=axis)
28-
# TODO: Profile this approach vs modifying coords only.
2930
return result
31+
32+
33+
def sliding_win_oneaxis(
34+
s: sparse.SparseArray, nwin: int, axis: int, step: int = 1
35+
) -> sparse.SparseArray:
36+
"""
37+
Generates a view-like sparse array using a sliding window of specified length along a specified axis.
38+
Sparse analog of an optimized dense as_strided-based implementation with these properties:
39+
40+
- Accepts a single `nwin` and a single `axis`.
41+
- Inserts a new 'win' axis immediately BEFORE the original target axis.
42+
Output shape:
43+
s.shape[:axis] + (W,) + (nwin,) + s.shape[axis+1:]
44+
where W = s.shape[axis] - (nwin - 1).
45+
- If `step > 1`, stepping is applied by slicing along the new windows axis (same observable behavior
46+
as doing `slice_along_axis(result, slice(None, None, step), axis)` in the dense version).
47+
48+
Args:
49+
s: Input sparse array (pydata/sparse COO-compatible).
50+
nwin: Sliding window size (must be > 0).
51+
axis: Axis of `s` along which the window slides (supports negative indexing).
52+
step: Stride between windows. If > 1, applied by slicing the windows axis after construction.
53+
54+
Returns:
55+
A sparse array with a new windows axis inserted before the original axis.
56+
57+
Notes:
58+
- Mirrors the dense function’s known edge case: when nwin == shape[axis] + 1, W becomes 0 and
59+
an empty windows axis is returned.
60+
- Built by coordinate arithmetic; no per-window indexing or concatenation.
61+
"""
62+
if -s.ndim <= axis < 0:
63+
axis = s.ndim + axis
64+
if not (0 <= axis < s.ndim):
65+
raise ValueError(f"Invalid axis {axis} for array with {s.ndim} dimensions")
66+
if nwin <= 0:
67+
raise ValueError("nwin must be > 0")
68+
dim = s.shape[axis]
69+
70+
last_win_start = dim - nwin
71+
win_starts = list(range(0, last_win_start + 1, step))
72+
n_win_out = len(win_starts)
73+
if n_win_out <= 0:
74+
# Return array with proper shape except empty along windows axis
75+
return sparse.zeros(
76+
s.shape[:axis] + (0,) + (nwin,) + s.shape[axis + 1 :], dtype=s.dtype
77+
)
78+
79+
coo = s.asformat("coo")
80+
coords = coo.coords # shape: (ndim, nnz)
81+
data = coo.data # shape: (nnz,)
82+
ia = coords[axis] # indices along sliding axis, shape: (nnz,)
83+
84+
# We emit contributions for each offset o in [0, nwin-1].
85+
# For a nonzero at index i, it contributes to window start w = i - o when 0 <= w < W.
86+
out_coords_blocks = []
87+
out_data_blocks = []
88+
89+
# Small speed/memory tweak: reuse dtypes and pre-allocate o-array once per loop.
90+
idx_dtype = coords.dtype
91+
92+
for win_ix, win_start in enumerate(win_starts):
93+
w = ia - win_start
94+
# Valid window starts are those within [0, nwin]
95+
mask = (w >= 0) & (w < nwin)
96+
if not mask.any():
97+
continue
98+
99+
sel = np.nonzero(mask)[0]
100+
w_sel = w[sel]
101+
102+
# Build new coords with windows axis inserted at `axis` and the original axis
103+
# becoming the next axis with fixed offset value `o`.
104+
# Output ndim = s.ndim + 1
105+
before = coords[:axis, sel] # unchanged
106+
after_other = coords[axis + 1 :, sel] # dims after original axis
107+
win_idx_row = np.full((1, sel.size), win_ix, dtype=idx_dtype)
108+
109+
new_coords = np.vstack([before, win_idx_row, w_sel[None, :], after_other])
110+
111+
out_coords_blocks.append(new_coords)
112+
out_data_blocks.append(data[sel])
113+
114+
if not out_coords_blocks:
115+
return sparse.zeros(
116+
s.shape[:axis] + (n_win_out,) + (nwin,) + s.shape[axis + 1 :], dtype=s.dtype
117+
)
118+
119+
out_coords = np.hstack(out_coords_blocks)
120+
out_data = np.hstack(out_data_blocks)
121+
out_shape = s.shape[:axis] + (n_win_out,) + (nwin,) + s.shape[axis + 1 :]
122+
123+
return sparse.COO(out_coords, out_data, shape=out_shape)

0 commit comments

Comments
 (0)