Assuming there is a 1-D array/list which defines a color gradient I would like to use it in order to create a 2-D color gradient as follows:
Let's for simplicity replace color information with a single numerical value for an example of a 1-D array/list:
[ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ]
To keep the gradient progressing diagonally with progress of the largest next value diagonally over the entire array I would like to transform the 1-D sequence into a 2D-array with deliberately chosen shape (i.e. width/height, i.e. number of rows x number of columns where row * columns == length of the 1-D gradient array) as follows:
[[ 1 2 4 ]
[ 3 6 7 ]
[ 5 9 10 ]
[ 8 12 13 ]
[ 11 14 15 ]]
or
[[ 1 2 4 7 10 ]
[ 3 6 9 12 13 ]
[ 5 8 11 14 15 ]]
or starting from a sequence:
[ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16]
to
[[ 1 2 4 7 ]
[ 3 6 9 11 ]
[ 5 10 13 14 ]
[ 8 12 15 16 ]]
Is there a ready-to-use out of the box Python module or C-library capable to perform such reshaping of an array or need this special case be coded by hand? And if coding the loops by hand is necessary, what would be the most efficient way of doing this as the sequence I would like to transform is 256³ large in size? I there maybe already ready for use code for such reshaping/transformation out there in the deep space of the Internet I have failed to find asking both the search engines and the LLMs?
From what I can see, this can be done in three steps:
I think it will be enough to find the stopping points, so then we can slice the sequence with them. For this, we can apply the cumulative sum to a sequence of diagonal lengths:
import numpy as np
from numba import njit
@njit
def flat_diagonal_stops(height, width):
'''Return a sequence of breakpoints separating a sequence
of length height*width into a sequence of matrix diagonals
of the shape (height, width)
'''
min_dim = min(height, width)
lengths = np.empty(height + width, dtype='int')
lengths[:min_dim] = [*range(min_dim)] # diagonal lengths in the lower triangle
lengths[min_dim:1-min_dim] = min_dim # diagonal lengths in the main body
lengths[:-min_dim:-1] = lengths[1:min_dim] # diagonal lengths in the upper triangle
return lengths.cumsum()
A sequence transformation like this:
(0, 1, 2, 3, 4, 5) >>> (0, 2, 4, 5, 3, 1)
is actually a separation of elements by the parity of their positional index. Elements with an even index are shifted to the left, while the others - to the right in reverse order:
@njit
def separate_by_index_parity(arr):
'''Return a numpy.ndarray filled with elements of arr,
first those in even-numbered positions,
then those in odd-numbered positions in reverse order
'''
out = np.empty_like(arr)
middle = sum(divmod(len(out), 2))
out[:middle] = arr[::2]
out[:middle-len(out)-1:-1] = arr[1::2]
return out
To do this, we can create a flat representation of the required output and work within it by slicing diagonal positions:
@njit
def assemble_diagonals_separated_by_parity(arr, height, width):
'''Return a matrix of shape (height, width) with elements
of the given sequence arr arranged along diagonals,
where the elements on each diagonal are separated
by the parity of their index in them
'''
out = np.empty(height*width, dtype=arr.dtype)
stops = flat_diagonal_stops(height, width)
out_step = width + 1
for offset, (start, stop) in enumerate(zip(stops[:-1], stops[1:]), 1-height):
# out_from: the first element of an off-diagonal
# out_to : next after the last element of an off-diagonal
# out_step: a stride to get diagonal items
out_from = -offset*width if offset < 0 else offset
out_to = out_from + (stop-start)*out_step # stop - start is equal to the diagonal size
out[out_from:out_to:out_step] = separate_by_index_parity(arr[start:stop])
return out.reshape(height, width)
The result is a stacking of the modified sequence on diagonals from bottom to top and from left to right. To get other types of stacking, we combine flipping and transposing. For example, we can stack elements in the left-to-right and top-to-bottom order along anti-diagonals as follows (note the reverse order of dimensions (width, height)
in a function call):
height, width = 6, 4
arr = np.arange(1, 1+height*width)
out = np.fliplr(assemble_diagonals_separated_by_parity(arr, width, height).T)
print(out)
[[ 1 2 4 7]
[ 3 6 9 11]
[ 5 10 13 15]
[ 8 14 17 19]
[12 18 21 22]
[16 20 23 24]]
import numpy as np
from numba import njit
@njit
def flat_diagonal_stops(height, width):
min_dim = min(height, width)
lengths = np.empty(height + width, dtype='int')
lengths[:min_dim] = [*range(min_dim)]
lengths[min_dim:1-min_dim] = min_dim
lengths[:-min_dim:-1] = lengths[1:min_dim]
return lengths.cumsum()
@njit
def separate_by_index_parity(arr):
out = np.empty_like(arr)
middle = sum(divmod(len(out), 2))
out[:middle] = arr[::2]
out[:middle-len(out)-1:-1] = arr[1::2]
return out
@njit
def assemble_diagonals_separated_by_parity(arr, height, width):
if height == 1 or width == 1:
return arr.reshape(height, width).copy()
out = np.empty(height*width, dtype=arr.dtype)
stops = flat_diagonal_stops(height, width)
out_step = width + 1
for offset, (start, stop) in enumerate(zip(stops[:-1], stops[1:]), 1-height):
out_from = -offset*width if offset < 0 else offset
out_to = out_from + (stop-start)*out_step
out[out_from:out_to:out_step] = separate_by_index_parity(arr[start:stop])
return out.reshape(height, width)
height, width = 6, 4
arr = np.arange(1, 1+height*width)
out = np.fliplr(assemble_diagonals_separated_by_parity(arr, width, height).T)
print(out)
Let's specialize the assembly function to work directly with anti-diagonals, so as not to get confused with flip-transpose tricks. In this case, we have a shorter slicing step, and the starting point will be along the top and right edges. Everything else remains unchanged:
@njit
def assemble_antidiagonals_separated_by_parity(arr, height, width):
if height == 1 or width == 1:
return arr.reshape(height, width).copy()
out = np.empty(height*width, dtype=arr.dtype)
stops = flat_diagonal_stops(height, width)
out_step = width - 1
for offset, (start, stop) in enumerate(zip(stops[:-1], stops[1:])):
out_from = offset if offset < width else (offset-width+2)*width-1
out_to = out_from + (stop-start)*out_step
out[out_from:out_to:out_step] = separate_by_index_parity(arr[start:stop])
return out.reshape(height, width)
>>> height, width = 8, 5
>>> arr = np.arange(1, 1+height*width)
>>> out = assemble_antidiagonals_separated_by_parity(arr, height, width)
>>> print(out)
[[ 1 2 4 7 11]
[ 3 6 9 13 16]
[ 5 10 15 18 21]
[ 8 14 20 23 26]
[12 19 25 28 31]
[17 24 30 33 35]
[22 29 34 37 38]
[27 32 36 39 40]]