Skip to content

Commit 82e6309

Browse files
authored
Merge pull request #349 from European-XFEL/reorder_axes_to_shape
Add reorder_axes_to_shape function
2 parents 2231831 + ad38b09 commit 82e6309

5 files changed

Lines changed: 76 additions & 13 deletions

File tree

docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Added:
3030
[DataArray][xarray.DataArray]s properly (!333).
3131
- Added [hyperslicer2()][extra.utils.hyperslicer2] to make plotting image arrays
3232
easier (!348).
33+
- [reorder_axes_to_shape][extra.utils.reorder_axes_to_shape] utility function (!349).
3334

3435
Changed:
3536
- [Timepix3.spatial_bins()] is now a static method.

docs/utilities.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ else.
1616

1717
::: extra.utils.find_nearest_value
1818

19+
::: extra.utils.reorder_axes_to_shape
20+
1921
## Plotting functions
2022

2123
::: extra.utils.imshow2

src/extra/components/utils.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import sys
1+
from ..utils.misc import _isinstance_no_import
22

33
# Source prefixes in use at each SASE.
44
SASE_TOPICS = {
@@ -63,15 +63,6 @@ def _instrument_to_sase(instrument):
6363
return 3
6464

6565

66-
def _isinstance_no_import(obj, mod: str, cls: str):
67-
"""Check if isinstance(obj, mod.cls) without loading mod"""
68-
m = sys.modules.get(mod)
69-
if m is None:
70-
return False
71-
72-
return isinstance(obj, getattr(m, cls))
73-
74-
7566
def _select_subcomponent_trains(src, keys, dst=None):
7667
if dst is None:
7768
from copy import copy

src/extra/utils/misc.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
import sys
2+
13
import numpy as np
24

35
from typing import Any
46

5-
from ..components.utils import _isinstance_no_import
6-
77

88
def find_nearest_index(array, value: Any) -> np.int64:
99
"""Find array index for the nearest value.
@@ -33,6 +33,40 @@ def find_nearest_value(array, value: Any) -> Any:
3333
return array[find_nearest_index(array, value)]
3434

3535

36+
def reorder_axes_to_shape(a, target_shape):
37+
"""Transpose an array to match the axis order specified by a shape tuple.
38+
39+
All dimensions must have different sizes. One axis in target_shape may be
40+
None, a wildcard for the remainining axis in the array shape.
41+
"""
42+
t = target_shape
43+
if len(set(t)) != len(t):
44+
raise ValueError(f"Target shape {t} has non-unique axes")
45+
if len(t) != len(a.shape):
46+
raise ValueError(f"Number of dimensions differs: {a.shape} -> {t}")
47+
if None in t:
48+
unmatched = set(a.shape) - set(t)
49+
if len(unmatched) != 1:
50+
raise ValueError(f"Cannot rearrange array shape {a.shape} to {t}")
51+
t = list(t)
52+
t[t.index(None)] = unmatched.pop()
53+
54+
if set(t) != set(a.shape):
55+
raise ValueError(f"Cannot rearrange array shape {a.shape} to {t}")
56+
57+
order = tuple([a.shape.index(l) for l in t])
58+
return a.transpose(order)
59+
60+
61+
def _isinstance_no_import(obj, mod: str, cls: str):
62+
"""Check if isinstance(obj, mod.cls) without loading mod"""
63+
m = sys.modules.get(mod)
64+
if m is None:
65+
return False
66+
67+
return isinstance(obj, getattr(m, cls))
68+
69+
3670
def imshow2(image, *args, lognorm=False, ax=None, **kwargs):
3771
"""Display an image with reasonable defaults.
3872

tests/test_utils.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import numpy as np
2+
import pytest
23
import xarray as xr
34

4-
from extra.utils import imshow2, hyperslicer2, fit_gaussian, gaussian
5+
from extra.utils import (
6+
imshow2, hyperslicer2, fit_gaussian, gaussian, reorder_axes_to_shape,
7+
)
58

69

710
def test_imshow2():
@@ -43,3 +46,35 @@ def test_fit_gaussian():
4346
data = gaussian(np.arange(100), *params, norm=False)
4447
popt = fit_gaussian(data, A_sign=-1)
4548
assert np.allclose(popt, params)
49+
50+
51+
def test_reorder_axes_to_shape():
52+
arr = np.zeros((512, 1024, 16), dtype=np.float32) # E.g. burst mode JUNGFRAU data
53+
res = reorder_axes_to_shape(arr, (16, 512, 1024))
54+
assert res.shape == (16, 512, 1024)
55+
assert res.base is arr
56+
57+
res = reorder_axes_to_shape(arr, (None, 512, 1024))
58+
assert res.shape == (16, 512, 1024)
59+
assert res.base is arr
60+
61+
with pytest.raises(ValueError):
62+
reorder_axes_to_shape(arr, (12, 512, 1024)) # Wrong dimension sizes
63+
64+
with pytest.raises(ValueError):
65+
reorder_axes_to_shape(arr, (16, 1, 512, 1024)) # Wrong number of dimensions
66+
67+
with pytest.raises(ValueError):
68+
reorder_axes_to_shape(arr[:, :512], (16, 512, 512)) # Ambiguous order
69+
70+
with pytest.raises(ValueError):
71+
reorder_axes_to_shape(arr, (None, None, 1024)) # Only 1 None allowed
72+
73+
with pytest.raises(ValueError):
74+
reorder_axes_to_shape(arr, (None, 256, 1024)) # Wildcard & wrong number
75+
76+
# Check we've transposed, not reshaped
77+
arr = np.arange(15).reshape(3, 5)
78+
res = reorder_axes_to_shape(arr, (5, 3))
79+
assert res.shape == (5, 3)
80+
np.testing.assert_array_equal(res[0], [0, 5, 10])

0 commit comments

Comments
 (0)