Skip to content

Commit 477f30e

Browse files
committed
Feat: windowed_rank reducer
- implements a windowed rank order reducer, where the maximum rank is given by the number of elements in a window - coerces input to numpy array in order to use numpy.sort
1 parent 7fbeea5 commit 477f30e

File tree

2 files changed

+86
-0
lines changed

2 files changed

+86
-0
lines changed

src/xarray_multiscale/reducers.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,3 +374,56 @@ def lor(a, b):
374374
final_result = lor(reduce(lor, results), sections[-1]) - 1
375375

376376
return final_result
377+
378+
379+
def windowed_rank(
380+
array: NDArray[Any], window_size: Tuple[int, ...], rank: int
381+
) -> NDArray[Any]:
382+
"""
383+
Compute the windowed rank order filter of an array.
384+
Input will be coerced to a numpy array.
385+
386+
Parameters
387+
----------
388+
array: Array-like, e.g. Numpy array, Dask array
389+
The array to be downscaled. The array must have a ``reshape``
390+
method.
391+
392+
window_size: Tuple of ints
393+
The window to use for aggregation. The array is partitioned into
394+
non-overlapping regions with size equal to ``window_size``, and the
395+
values in each window are sorted to generate the result.
396+
397+
rank: int
398+
The index to take from the sorted values in each window. Must be
399+
between 0 and the product of the elements of ``window_size``, minus one.
400+
Use negative indices to count from the end of the sorted window.
401+
402+
Returns
403+
-------
404+
Numpy array
405+
The result of the windowed rank filter. The length of each axis of this array
406+
will be a fraction of the input.
407+
408+
Examples
409+
--------
410+
>>> import numpy as np
411+
>>> from xarray_multiscale.reducers import windowed_rank
412+
>>> data = np.arange(16).reshape(4, 4)
413+
>>> windowed_rank(data, (2, 2), -2)
414+
array([[ 4 6]
415+
[12 14]])
416+
"""
417+
max_rank = np.prod(window_size) - 1
418+
if rank > max_rank or rank < -max_rank - 1:
419+
raise ValueError(
420+
f"Rank must be between either -1 and {-max_rank-1} or 0 and {max_rank} for window_size {window_size}."
421+
)
422+
reshaped = reshape_windowed(array, window_size)
423+
transposed_shape = tuple(range(0, reshaped.ndim, 2)) + tuple(
424+
range(1, reshaped.ndim, 2)
425+
)
426+
transposed = reshaped.transpose(transposed_shape)
427+
collapsed = transposed.reshape(tuple(reshaped.shape[slice(0, None, 2)]) + (-1,))
428+
result = np.take(np.sort(collapsed, axis=-1), rank, axis=-1)
429+
return result

tests/test_reducers.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
windowed_mean,
1111
windowed_min,
1212
windowed_mode,
13+
windowed_rank,
1314
)
1415

1516

@@ -62,6 +63,38 @@ def test_windowed_mode():
6263
assert np.array_equal(results, answer)
6364

6465

66+
def test_windowed_rank():
67+
initial_array = np.array([[[1, 2],
68+
[3, 4]],
69+
[[5, 6],
70+
[7, 8]]])
71+
larger_array = np.tile(initial_array, (2, 2, 2))
72+
window_size = (2, 2, 2)
73+
74+
# 2nd brightest voxel
75+
rank = np.product(window_size) - 2
76+
answer = np.array([[[7, 7],
77+
[7, 7]],
78+
[[7, 7],
79+
[7, 7]]])
80+
results = windowed_rank(larger_array, window_size, rank)
81+
assert np.array_equal(results, answer)
82+
83+
# Test negative rank
84+
rank = -8
85+
answer = np.array([[[1, 1],
86+
[1, 1]],
87+
[[1, 1],
88+
[1, 1]]])
89+
results = windowed_rank(larger_array, window_size, rank)
90+
assert np.array_equal(results, answer)
91+
92+
# Test out-of-bounds rank
93+
rank = 100
94+
with pytest.raises(ValueError):
95+
windowed_rank(larger_array, window_size, rank)
96+
97+
6598
@pytest.mark.parametrize("windows_per_dim", (1, 2, 3, 4, 5))
6699
@pytest.mark.parametrize(
67100
"window_size", ((1,), (2,), (1, 2), (2, 2), (2, 2, 2), (1, 2, 3), (3, 3, 3, 3))

0 commit comments

Comments
 (0)