I have written a general stacking function. It is a bit more complicated but its inputs are just the arrays (tuple) and an axis along which you wish to stack the arrays. E.g.
A = np.random.random((569, 30))
B = np.random.random((569,))
C = stack((A, B), axis=1) # C.shape == (569, 31)
def stack(arrays: tuple | list, axis: int | None = None, reduce: bool = False) -> np.ndarray:
"""
concatenate arrays along the specific axis
if reduce=True, the "arrays" tuple is processed in this way
arrays = (A, B, C, D)
stack((stack((stack((A, B), axis=axis), C), axis=axis), D), axis=axis)
This is potentially slower but allows to concatenate e.g.
A.shape = (2, 4, 4)
B.shape = (3, 4)
C.shape = (4,)
res = stack((C, B, A), axis=0, reduce=True)
res.shape = (3, 4, 4)
res[0] == stack((C, B), axis=0)
res[1:] == A
"""
@reduce_like
def _stack(arrays: tuple | list, axis: int | None = None) -> np.ndarray:
ndim = np.array([np.ndim(array) for array in arrays])
_check_dims(ndim, reduce)
if np.all(ndim == 1): # vector + vector + ...
if axis is None: # -> vector
return np.concatenate(arrays, axis=axis)
else: # -> 2-D array
return np.stack(arrays, axis=axis)
elif np.var(ndim) != 0: # N-D array + (N-1)-D array + ... -> N-D array
max_dim = np.max(ndim)
# longest array
shape = list(np.shape(arrays[np.argmax(ndim)]))
shape[axis] = -1
arrays = [np.reshape(a, shape) if np.ndim(a) < max_dim else a for a in arrays]
return np.concatenate(arrays, axis=axis)
elif is_constant(ndim): # N-D array + N-D array + -> N-D array or (N+1)-D array
ndim = ndim[0]
if axis < ndim: # along existing dimensions
return np.concatenate(arrays, axis=axis)
else: # along a new dimension
return np.stack(arrays, axis=axis)
def _check_dims(ndim: np.ndarray, reduce: bool = False) -> None:
error_msg = "Maximum allowed difference in dimension of concatenated arrays is one."
if np.max(ndim) - np.min(ndim) > 1:
if reduce:
raise ValueError(error_msg)
else:
raise ValueError(f'{error_msg}\nUse "reduce=True" to unlock more general (but slower) stacking.')
# 0-D arrays to 1-D arrays (to e.g. add a number to a vector)
arrays = tuple([np.reshape(array, (1,)) if np.ndim(array) == 0 else array for array in arrays])
if reduce:
return _stack(arrays, axis)
else:
return _stack.undecorated(arrays, axis)
def is_constant(array: np.ndarray, axis: int | bool = None, constant: float = None) -> bool | np.ndarray:
if constant is None: # return True if the array is constant along the axis
return np.var(array, axis=axis) < _num_eps
else: # return True if the array is equal to "constant" along the axis
return np.all(np.abs(array - constant) < _num_eps, axis=axis)
def reduce_like(func: Callable):
@wraps(func)
def _decorator(*args, **kw):
args = list(args)
arrays = args[0]
result = arrays[0]
for array in arrays[1:-1]:
if args[1:]:
new_args = [(result, array), *args[1:]]
else:
new_args = [(result, array)]
result = func(*new_args, **kw)
else:
if args[1:]:
new_args = [(result, arrays[-1]), *args[1:]]
else:
new_args = [(result, arrays[-1])]
return func(*new_args, **kw)
_decorator.undecorated = func
return _decorator