# Reverse (flip) right-left(anti-)diagonals of a non-square numpy array

## Question:

What I am after is Python code able to reverse the order of the values in each of the array anti-diagonals in a numpy array.

I have already tried various combinations of `np.rot90`, `np.fliplr`, `np.transpose`, `np.flipud` but none is able to give me the original shape of the 5×3 array with all the anti-diagonals reversed.

Any idea how to accomplish this?

Example:

``````[[ 1  2  4]
[ 3  5  7]
[ 6  8 10]
[ 9 11 13]
[12 14 15]]
``````

Should become:

``````[[ 1  3  6]
[ 2  5  9]
[ 4  8 12]
[ 7 11 14]
[10 13 15]]
``````

I suppose it must be easy, but somehow I have yet failed to find how to do it efficiently on arrays with millions of values.

Inspired by the already provided answers (status 2024-05-23 11:37 CET) and re-thinking what would be the most efficient way of getting the required transformation done it seems that giving a simple function taking two indices : `iRow`, `jColumn` of a value in an array and returning the required `i,j` indices to access the array as if it were flipped/reversed over the diagonals will provide fastest results. With such function for the over the diagonals flipped version of the array would be getting the right values without operating on the array as easy as in a trivial case of one-based and column/row based access to array values demonstrated below:

``````import numpy as np
srcArr = np.array([[ 1,  2,  3,  4,  5,  6],
[ 7,  8,  9, 10, 11, 12],
[13, 14, 15, 16, 17, 18],
[19, 20, 21, 22, 23, 24]])

def ijOfArrayValueGivenOneBasedColumnRowBasedIndices(i, j):
return ( j - 1, i - 1 )

print( srcArr[
ijOfArrayValueGivenOneBasedColumnRowBasedIndices(
3,4)] ) # gives 21
print( srcArr[3,4] ) # gives 23
``````

From this perspective the question comes down to providing a function

`ijIndicesToSourceArray_gettingValueOfSourceArrayWithReversedRightLeftAntiDiagonalsAt(i,j,arrShapeRows,arrShapeColumns)`

I don’t think there is any efficient way to do this. I would just do it similar to how I said to create the array from its diagonals in this answer, but reversing the diagonals along the way.

``````import numpy as np
from scipy.sparse import diags

a = np.arange(15).reshape(5,3) + 1
n, m = a.shape
print(a)

offsets = np.arange(n+m-1)[::-1]-(a.shape[0]-1)

aflipped = np.fliplr(a)
diagonals = [np.diagonal(aflipped, offset=i) for i in offsets]

b = np.zeros_like(a)
for diagonal, offset in zip(diagonals, offsets):
b += diags(diagonal[::-1], offset, shape=a.shape).astype(int)
b = np.fliplr(b)
print(b)
``````

As long as there is no better answer demonstrating how to refrain from using Python loops, below an approach which uses only numpy and supports arrays with any type of values (also mixed):

``````#!/usr/bin/python
import numpy as np
## Decide the shape of the test array :
M =3 ; N=5  # ( M-Rows x N-Columns )
## Create a test array :
srcArr = np.arange( 11, 10+N*M+1,
dtype=np.uint8).reshape(M,N) ;      print( srcArr ); print( " ---===--- " )
# ---===---===---===---===---===---===---===---===---===---===---===---===---===---===---===---
## Extend the array to the right with   M-1   columns for later "shear" :
tmpArr=np.zeros((M,M-1), dtype=np.uint8) ;#         print(tmpArr );print( " ---===--- " )
extArr=np.concatenate( ( srcArr, tmpArr ), axis=1 ) ;       print( extArr ); print( " ---===--- " )
## Share the extended array using    np.roll()   :
for index in range( 1, M):
extArr[index]=np.roll(extArr[index], index)
pass;                                                   print( extArr ); print( " ---===--- " )
## And reverse by up/down flipping the order of the former diagonals being now columns :
flpArr = np.flipud(extArr) ;                                print( flpArr ); print( " ---===--- " )
## Diagonals are reversed and will be "rolled" into their former positions in the array :
for index in range( 0, M +N - 1):
if True : ## The "tricky" part because handling non-square arrays requires handling
flpArr[:,index]=np.roll( flpArr[:,index], ## of three algorithmic different phases
( index + 1 ) if index < min(N,M) ## < Phase A : left, "growing" part of array
else ( -1 * (M + abs(N-M) ) + 2* (index + 1 - min(N,M)) ) ## Phase B :
* ( 1 if ( M  - min(N,M) ) and (N+M-1 - 2*N  - 1) ## < same "size":
else 0 )  if index < (N+M) - min(N,M) - 1
else  -(M+N ) + index  + 1 ) ## < Phase C : right, "shinking"
pass;                                                   print(flpArr) ; print( " ---===--- " )
for index in range( 1, M): ## Rolling the array back to its initial shape :
flpArr[index]=np.roll( flpArr[index], -index)
pass;                                                   print( flpArr) ; print( " ---===--- " )
tgtArr = flpArr[ :M, :N] ## < cut the final array out of the extended one
pass;                                                   print(tgtArr) ; print( " ---===--- " )

"""
[[ 1  2   3]
[ 4   5   6]
[  7   8    9]
[  10  11 12]
[13 14 15]]

[[ 1    2    3]
[  4    5    6]
[  7    8    9]
[10 11  12]
[13 14 15]]

[[  1    2    3]
[ 4    5    6]
[ 7    8    9]
[10 11 12]
[13 14 15]]
"""
``````

The code outputs :

``````[[11 12 13 14 15]
[16 17 18 19 20]
[21 22 23 24 25]]
---===---
[[11 12 13 14 15  0  0]
[16 17 18 19 20  0  0]
[21 22 23 24 25  0  0]]
---===---
[[11 12 13 14 15  0  0]
[ 0 16 17 18 19 20  0]
[ 0  0 21 22 23 24 25]]
---===---
[[ 0  0 21 22 23 24 25]
[ 0 16 17 18 19 20  0]
[11 12 13 14 15  0  0]]
---===---
[[11 16 21 22 23  0  0]
[ 0 12 17 18 19 24  0]
[ 0  0 13 14 15 20 25]]
---===---
[[11 16 21 22 23  0  0]
[12 17 18 19 24  0  0]
[13 14 15 20 25  0  0]]
---===---
[[11 16 21 22 23]
[12 17 18 19 24]
[13 14 15 20 25]]
---===---
``````

The above output demonstrates the approach which uses numpy `np.roll()` making it possible to "shear" the array in order to get the diagonals available for flipping in first place and then unrolling it back to the original shape.

However, the indices can be generated using this function:

``````import numpy as np

def flip_inds(a):
m, n = a.shape
i = np.arange(m).reshape(-1, 1)
j = np.arange(n).reshape(1, -1)
jp = np.where(
i + j < min(m, n),
i,
np.where(
i + j < max(m, n),
(
i + j - (m - 1 - i)
if n > m else
n - 1 - j
),
n - (m - i)
)
)
ip = i + j - jp
return ip, jp

ip, jp = flip_inds(a)
a[ip, jp] # diagonals flipped
``````

but I would recommend Numba for this because it will be more efficient:

``````import numba as nb

@nb.njit
def flip(a):
m, n = a.shape
b = np.empty_like(a)
for i in range(m):
for j in range(n):
# Along a diagonal i + j is constant
# denote by (ip, jp) the indices of the value in the input that should be put in the (i, j) entry of the output
# We know that: i + j = ip + jp
jp = (
# Case 1: (i, j) in upper left triangle
# Here we just do the regular transpose
i
if i + j < min(m, n) else (
# Case 2: (i, j) neither in upper left or lower right triangle
# in this case what we do depends on if n > m or not
# If n > m we are bounded by m, locate ip at i steps from the maximum i index:
#    ip = m - 1 - i
# from that we can obtain jp as i + j - ip:
i + j - (m - 1 - i)
if n > m else
# If m > n locate the jp index j steps from the max j index
n - 1 - j
)
if i + j < max(m, n) else
# Case 3: Lower right corner, here we can do a transpose again, but with indices going backwards
n - (m - i)
)
ip = i + j - jp
b[i, j] = a[ip, jp]
return b

flip(a) # diagonals flipped
``````

### Without comments (easier to copy):

``````@nb.njit
def flip(a):
m, n = a.shape
b = np.empty_like(a)
for i in range(m):
for j in range(n):
jp = (
i
if i + j < min(m, n) else (
i + j - (m - 1 - i)
if n > m else
n - 1 - j
)
if i + j < max(m, n) else
n - (m - i)
)
ip = i + j - jp
b[i, j] = a[ip, jp]
return b
``````

### Vector abstraction

Let’s consider the coordinates of cells as vectors on the vector plane (see the picture below). In this case, we can find the target point as follows:

``````def mirror(pi, pj, m, n) -> list:
'''Returns the point on the reverse transposed diagonal.
(pi, pj) - source point, (m, n) - maximum axis coordinates'''
P = np.array([pi, pj])
p_sum = P.sum()
A = np.array([min(p_sum, m), max(p_sum - m, 0)])
B = np.array([max(p_sum - n, 0), min(p_sum, n)])
M = A + B - P
return M.tolist()
``````

In this figure:

• P is a source point
• M is a target point
• A and B are the extreme points of the diagonal with P

In vectorized form, the formula might look like this:

``````def transpose_reversed_diag(arr):
m, n = arr.shape
P = np.array(np.meshgrid(np.arange(m), np.arange(n), indexing='ij'))
m -= 1
n -= 1
psum = P.sum(0)
A = np.where(
psum < m,
np.stack([psum, np.zeros_like(psum)]),
np.stack([np.full_like(psum, m), psum - m]),
)
B = np.where(
psum < n,
np.stack([np.zeros_like(psum), psum]),
np.stack([psum - n, np.full_like(psum, n)])
)
return arr[*(A + B - P)]
``````

### Diagonal as part of a square

Suppose we know the top left corner and the length of a square. In this case, we can transpose the opposite diagonal in the square this way:

``````import numpy as np
from numba import njit

@njit
def _flip_diag(ai, aj, length, arr):
'''Transpose the diagonal of a square
with the top left corner (ai, aj) and the given length
which is placed in the 2d-array arr'''
i, j = 0, length
while i < j:
left = ai+i, aj+j
right = ai+j, aj+i
arr[left], arr[right] = arr[right], arr[left]
i += 1
j -= 1
``````

So we can solve the initial task by iterating over the squares build on each diagonal of the array:

``````@njit
def transpose(arr, inplace=False):
out = arr if inplace else arr.copy()

m, n = arr.shape
if m == n:
return out.T
if m < n:
return transpose(out.T, inplace=True).T

for d in range(n-1):     # upper left triangle
_flip_diag(0, 0, d, out)
for d in range(m-n+1):   # main body
_flip_diag(d, 0, n-1, out)
for d in range(1, n):    # lower right triangle
_flip_diag(m-n+d, d, n-1-d, out)
return out
``````

A simple way to break down this problem, without using any loops:

• Step 1: Transpose the upper and lower triangles (red and blue)
• Step 2: Flip the "parallelogram" in the middle (purple)

When the matrix is square (h=w), just transpose is enough. I’m showing an example when the matrix is "tall" (h > w):

``````def flip_along_diag(a):
h, w = a.shape
out = np.zeros_like(a)

# step 1: flip upper and lower triangles
u, v = np.triu_indices(w, 0)
p, q = np.tril_indices(w, 0)
out[u, w-v-1] = a[w-v-1, u]
out[h-w+p, w-1-q] = a[h-1-q, p]

# step 2: flip the "parallelogram" of size (r, s) in the middle
r, s = h - w - 1, w
if r >= 1:
i, j = np.mgrid[:r, :s]
i = i + np.arange(s)[::-1] + 1
out[i, j] = np.fliplr(a[i, j])

return out
``````

I believe the same algorithm can be extended when the matrix is wide (w > h) instead of tall, with a little modification.

Update: For handling the case of wide (w > h) matrices, we can simply first check if w > h, and then simply modify the indices as needed.

``````def flip_along_diag(a):
h, w = a.shape

if h == w: # for square matrix, a.T is enough
return a.T

out = np.zeros_like(a)
k, d = min(h, w), abs(h - w)

# step 1: flip two triangles
u, v = np.triu_indices(k, 0)
p, q = np.tril_indices(k, 0)

if h > w: # h > w: upper and lower triangle
out[u, k-v-1] = a[k-v-1, u]
out[d+p, w-q-1] = a[h-q-1, p]
else: # w > h: left and right triangle
out[u, k-v-1] = a[k-v-1, u]
out[p, w-q-1] = a[h-q-1, d+p]

# step 2: flip parallelogram
if h > w: # h > w: flip left-right
r, s = h - w - 1, w
if r >= 1:
i, j = np.mgrid[:r, :s]
i = i + np.arange(s)[::-1] + 1
out[i, j] = np.fliplr(a[i, j])
else: # w > h: flip up-down
r, s = h, w - h - 1
if s >= 1:
i, j = np.mgrid[:r, :s]
j = j + np.arange(r)[::-1, None] + 1
out[i, j] = np.flipud(a[i, j])

return out
``````

The sum of indices on anti-diagonals preserves. This means you can construct array with indices sum, like this (given `a` is your array):

``````rows, cols = np.indices(a.shape)
indices_sum = rows + cols

array([[0, 1, 2],
[1, 2, 3],
[2, 3, 4],
[3, 4, 5],
[4, 5, 6]])
``````

and iterate per each unique value, get indices and flip like this:

``````import numpy as np

rows, cols = np.indices(a.shape)
indices_sum = rows + cols
for i in np.unique(indices_sum):
r, c = np.where(indices_sum==i)
a[r, c] = a[r[::-1], c[::-1]]
``````

Note: you can use `np.arange(1, a.shape[0] + a.shape[1] - 2)` instead of `np.unique(indices_sum)`

Here I’m still using python loop, but I think this is simpler solution. You may try to adapt it to exclude the loop.

I would propose approach similar to the one proposed by previous contributor, but with fast for loops in numba.

Step by step:

1. First let’s mirror upper left triangle and bottom right triangle

2. Switch elements in the parallelogram between this two triangles:

• such parallelogram could either be vertical
• or it could be horizontal
``````import numpy as np
import numba as nb
from timeit import timeit

@nb.njit(cache=True,fastmath=True)
def mirror(a):

n_rows, n_cols = a.shape[:2]
b = a.copy()

t = min(n_rows, n_cols)

# mirror upper left and bottom right triangles
for i in range(1, t-1):
for j in range(min(i, t-1-i)):
# inverting upper left triangle
b[i,j], b[j,i] = a[j,i], a[i,j]

# inverting bottom right triangle
b[n_rows-1-i,n_cols-1-j], b[n_rows-1-j,n_cols-1-i] = a[n_rows-1-j,n_cols-1-i], a[n_rows-1-i,n_cols-1-j]

# mirror middle parallelogram
if n_rows>=n_cols:
for i in range(int(t/2)):
b[t-1-i:n_rows-i,i], b[i:n_rows-t+1+i, n_cols-1-i] = a[i:n_rows-t+1+i, n_cols-1-i], a[t-1-i:n_rows-i,i]
else:
for i in range(int(t/2)):
b[i, t-1-i:n_cols-i ], b[n_rows-1-i, i:n_cols-t+1+i ] =  a[n_rows-1-i, i:n_cols-t+1+i ], a[i, t-1-i:n_cols-i ]

return b

def generate_random_matrx(n_rows: int, n_cols: int):
return np.arange(1,1+n_rows*n_cols).reshape(n_rows, n_cols)

def test1():
A = [[ 1,  2,  3,  4,  5],
[ 6,  7,  8,  9, 10],
[11, 12, 13, 14, 15],
[16, 17, 18, 19, 20],
[21, 22, 23, 24, 25]]
A = np.array(A)
B = mirror(A)

A_out = [[ 1,  6, 11, 16, 21],
[ 2,  7, 12, 17, 22],
[ 3,  8, 13, 18, 23],
[ 4,  9, 14, 19, 24],
[ 5, 10, 15, 20, 25]]
A_out = np.array(A_out)
assert np.all(B == A_out)

def test2():
A = [[ 1,  2,  3],
[ 4,  5,  6],
[ 7,  8,  9],
[10, 11, 12],
[13, 14, 15],
[16, 17, 18],
[19, 20, 21],
[22, 23, 24],
[25, 26, 27],
[28, 29, 30]]
A = np.array(A)
B = mirror(A)

A_out = [[ 1,  4,  7],
[ 2,  5, 10],
[ 3,  8, 13],
[ 6, 11, 16],
[ 9, 14, 19],
[12, 17, 22],
[15, 20, 25],
[18, 23, 28],
[21, 26, 29],
[24, 27, 30]]
A_out = np.array(A_out)
assert np.all(B == A_out)

def test3():
A =  [[ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10],
[11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
[21, 22, 23, 24, 25, 26, 27, 28, 29, 30],
[31, 32, 33, 34, 35, 36, 37, 38, 39, 40]]
A = np.array(A)
B = mirror(A)

A_out = [[ 1, 11, 21, 31, 32, 33, 34, 35, 36, 37],
[ 2, 12, 22, 23, 24, 25, 26, 27, 28, 38],
[ 3, 13, 14, 15, 16, 17, 18, 19, 29, 39],
[ 4,  5,  6,  7,  8,  9, 10, 20, 30, 40]]
A_out = np.array(A_out)
assert np.all(B == A_out)

A = generate_random_matrx(3, 10)
print(A)
B = mirror(A)
print(B)

test1()
test2()
test3()

``````

Function that takes indices as inputs:

``````@nb.njit(cache=True,fastmath=True)
def get_indices(i:int,j:int,n_rows:int,n_cols:int):

t = min(n_rows, n_cols)

# upper left triangle
if i<t and i+j<t:
return (j,i)

# bottom right triangle
if (n_rows-1-i)<t and (n_rows-1-i) + (n_cols-1-j)<t:
i, j = (n_rows-1-i), (n_cols-1-j)
return (n_rows-1-j,n_cols-1-i)

# parallelograms
if n_rows>=n_cols:
new_j = n_cols-1-j
return (i- (new_j-j), new_j)
else:
new_i = n_rows-1-i
return (new_i, j - (new_i-i))
``````

I believe this is similar to one of the other answers, but easier to follow (to me, anyway). Basically, we transpose the upper and lower triangles and flip the remaining diagonals. Because Numpy has several functions to deal with main diagonals, start by flipping the array along axis 1.

``````def flip_anti_diags(x: np.ndarray) -> np.ndarray:
# Handle wide matrices. Square matrices just work
transpose = x.shape[0] < x.shape[1]
if transpose:
x = x.T

# Smallest dimension
s = x.shape[1]

# Numpy has various functions to obtain main diagonals and triangles,
# so flip x so these work
x = np.fliplr(x)

# Get the upper and lower triangles, without any other diagonals,
# but don't transpose them yet as we need them for the diagonals
u = np.triu(x)
l = np.tril(x, x.shape[1] - x.shape[0])

# Get the diagonals and flip them, returning the flipped anti-diagonals
m = x - u - l
m = np.flipud(m)

# Get the transposed upper and lower triangles
u = np.fliplr(u)
u = u[:s].T

l = np.fliplr(l)
l = l[-s:].T

# Combine the diagonals and the transposed triangles
x = m
x[:s] += u
x[-s:] += l

# Flip back, if necessary
if transpose:
x = x.T
return x
``````

For the sake of completeness and for the sake of having an accepted answer below both the by me written fastest approach among all other in the other answers provided ones ( status 2024-06-03 ) and as reference an approach allowing getting single values out of the source array without the need of creating a target one just by calculating appropriate indices and therefore very fast for this purpose (based on code provided in the bountied answer by Johnny Cheesecutter).

The fastest approach provided below is using Python loops and numpy slices and for to me unknown reason actually slows down if "accelerated" by `numba` :

``````import numpy as np
from time import perf_counter as T
import numba as nb

#""" # <- "Magic Switch" : add/remove leading `#` to switch between two blocks of code
testShapes=[ (5,3), (3,5), (4,8), (8,4), (4,5), (5,4), (4,4), (5,5) ]
"""
testShapes=[ (4,7) ]
#"""

@nb.njit(cache=True,fastmath=True)
def flipOverRLdiags(a):
''' Can easily be adapted to provide  `new_i, new_j` given  i , j  and mRows, nCols
and is the fastest numba based approach used here as reference . Constructed
using in the bountied answer by Johnny Cheesecutter provided code for
obtaining the target indices and faster than in this answer itself provided
code for obtaining the resulting array: '''
mRows, nCols = a.shape
t = min(mRows, nCols)
b = a.copy() # faster than   np.empty_like(a)  -> how does it come ?
for i in range(mRows):
for j in range(nCols):
# top/left "triangle"
if i < t and i + j < t :
new_i , new_j = j , i
b[i , j] = a[ new_i , new_j  ]
continue
# bottom/right "triangle"
if (mRows-1-i) < t   and   (mRows-1-i) + (nCols-1-j) < t:
new_i ,  new_j = (mRows-1-i) , (nCols-1-j)
new_i, new_j = mRows - 1 - new_j , nCols  - 1 - new_i
b[i , j] = a[ new_i , new_j  ]
continue
# "parallelogram"
if mRows >= nCols:
new_j = nCols - 1 - j
new_i = i - (new_j - j )
b[ i , j ] = a[ new_i , new_j  ]
continue
else:
new_i = mRows - 1 - i
new_j = j - (new_i-i)
b[i , j] = a[ new_i , new_j  ]
continue
return b

#@nb.njit( cache=True, fastmath=True) # <- for to me unknown reason  using numba
#       ^-- SLOWS DOWN  instead of making faster
def flipRLdiagsOf(arrA) :
'''  Python loops plus numpy slices appears to be the overall fastest approach. '''
a = arrA
mRows, nCols = a.shape
absDiffColsRows =  abs(nCols-mRows)

if absDiffColsRows == 0 : # we have a square array where transposing is enough so:
b = a.T.copy()
return b    ## <- END : because of square array case

b = a.copy() # for to me unknown reason faster than   `np.empty_like(a)`
aT = a.T
maxDiagLen = min(mRows, nCols)

for diagLen in range(1 , maxDiagLen + 1) :
b[  maxDiagLen - diagLen    , :diagLen] =
aT[ maxDiagLen - diagLen    , :diagLen]                     # copy flipped upper/left "triangle"
b[  mRows   - diagLen   , -(maxDiagLen - diagLen) -1 : ] =
aT[ nCols   - diagLen   , -(maxDiagLen - diagLen) -1 : ]    # copy flipped lower/right "triangle"

if absDiffColsRows <= 1 : # we are ready because all values are already assigned :
return b    ## <- END : both "triangles" are flipped

# Now if reached this line flipping the "parallelogram" part of the array:
if mRows > nCols : # we have the case of a "narrow: array (more rows than columns) :
for col  in range(nCols):
startRow    =  (nCols - 1) - col
b[  startRow    :   startRow    + absDiffColsRows       , col ] =
a[  col             :   col             + absDiffColsRows       , startRow ] #
if nCols > mRows : # we have the case of a "wide" array (more columns than rows) :
for row  in range(mRows):
startCol    =  (mRows - 1) - row
b[  row         , startCol  : startCol  + absDiffColsRows ] =
a[  startCol    , row       : row       + absDiffColsRows ] #

return b    ## <- END : both "triangles" and the "parallelogram" are flipped

for nCols, mRows in testShapes :
srcArr = np.arange(11, nCols * mRows      +11, dtype=np.uint8).reshape((mRows , nCols))
print(srcArr, end="")
print("n  -=-=-=-=-")
tgtArr1 = flipOverRLdiags(srcArr)
tgtArr2 = flipRLdiagsOf(srcArr)
print(tgtArr2, end="")
print("n============")
assert np.all(tgtArr1 == tgtArr2)

# exit()
# from srcArr import srcArr
srcArr = np.arange(1,1920*1080+1, dtype=np.uint32).reshape((1080, 1920))
sT1=T()
tgtArr1 = flipOverRLdiags(srcArr)
dT1=T()-sT1
sT2=T()
tgtArr2 = flipRLdiagsOf(srcArr)
dT2=T()-sT2
print()
print(dT1, dT2)
assert np.all(tgtArr1 == tgtArr2)
``````

outputs:

``````[[11 12 13 14 15]
[16 17 18 19 20]
[21 22 23 24 25]]
-=-=-=-=-
[[11 16 21 22 23]
[12 17 18 19 24]
[13 14 15 20 25]]
============
[[11 12 13]
[14 15 16]
[17 18 19]
[20 21 22]
[23 24 25]]
-=-=-=-=-
[[11 14 17]
[12 15 20]
[13 18 23]
[16 21 24]
[19 22 25]]
============
[[11 12 13 14]
[15 16 17 18]
[19 20 21 22]
[23 24 25 26]
[27 28 29 30]
[31 32 33 34]
[35 36 37 38]
[39 40 41 42]]
-=-=-=-=-
[[11 15 19 23]
[12 16 20 27]
[13 17 24 31]
[14 21 28 35]
[18 25 32 39]
[22 29 36 40]
[26 33 37 41]
[30 34 38 42]]
============
[[11 12 13 14 15 16 17 18]
[19 20 21 22 23 24 25 26]
[27 28 29 30 31 32 33 34]
[35 36 37 38 39 40 41 42]]
-=-=-=-=-
[[11 19 27 35 36 37 38 39]
[12 20 28 29 30 31 32 40]
[13 21 22 23 24 25 33 41]
[14 15 16 17 18 26 34 42]]
============
[[11 12 13 14]
[15 16 17 18]
[19 20 21 22]
[23 24 25 26]
[27 28 29 30]]
-=-=-=-=-
[[11 15 19 23]
[12 16 20 27]
[13 17 24 28]
[14 21 25 29]
[18 22 26 30]]
============
[[11 12 13 14 15]
[16 17 18 19 20]
[21 22 23 24 25]
[26 27 28 29 30]]
-=-=-=-=-
[[11 16 21 26 27]
[12 17 22 23 28]
[13 18 19 24 29]
[14 15 20 25 30]]
============
[[11 12 13 14]
[15 16 17 18]
[19 20 21 22]
[23 24 25 26]]
-=-=-=-=-
[[11 15 19 23]
[12 16 20 24]
[13 17 21 25]
[14 18 22 26]]
============
[[11 12 13 14 15]
[16 17 18 19 20]
[21 22 23 24 25]
[26 27 28 29 30]
[31 32 33 34 35]]
-=-=-=-=-
[[11 16 21 26 31]
[12 17 22 27 32]
[13 18 23 28 33]
[14 19 24 29 34]
[15 20 25 30 35]]
============

0.01775594600258046 0.013043331997323548

``````

The core idea of the Python loops and numpy slices using approach, which turned out to be the most effective, is creating in first step a view into transposed source array to copy the flipped top and bottom "triangles" from it to the resulting array. The second and final step is "mirroring" column by column (or row by row, depending on array shape) the vertical (or horizontal) slices within the "parallelogram" part of the source array copying them over from the source into the target array.

Here again the line with timing of flipping an `np.uint32` 1080×1920 array over the right-left(anti-)diagonals:

``````0.01775594600258046 0.013043331997323548
``````

The first time is the time of the reference code and the second result the time of the up to now fastest approach ( status 2024-06-03 ).

You can create a list of all the diagonals and reassigned them back to the array by vectors

``````def flip_diagonals(arr):
rows, cols = arr.shape
arr = np.fliplr(arr)
diagonals = [arr.diagonal(offset=i).copy() for i in range(cols - 1, -rows, -1)]

inc_rows = 0
inc_cols = 0
for i, diag in enumerate(diagonals, start=1):
diag_len = len(diag)
if i > rows:
rows_diag = np.arange(rows)[-diag_len:]
if min(rows, cols) > diag_len:
cols_diag = np.arange(cols)[-diag_len:]
else:
inc_cols += 1
cols_diag = np.arange(cols)[:diag_len] + inc_cols
arr[rows_diag[::-1], cols_diag] = diag
else:
new_diag = np.arange(diag_len)
arr[new_diag[::-1] + inc_rows, new_diag] = diag
inc_rows = inc_rows + 1 if diag_len == cols else 0

return arr

arr1 = np.array([[1, 2, 4],
[3, 5, 7],
[6, 8, 10],
[9, 11, 13],
[12, 14, 15]])

arr2 = np.array([[ 1,  2,  3,  4,  5,  6],
[ 7,  8,  9, 10, 11, 12],
[13, 14, 15, 16, 17, 18],
[19, 20, 21, 22, 23, 24]])

arr1 = flip_diagonals(arr1)
print(arr1)

arr2 = flip_diagonals(arr2)
print(arr2)
``````

Output:

``````[[ 1  3  6]
[ 2  5  9]
[ 4  8 12]
[ 7 11 14]
[10 13 15]]

[[ 1  7 13 19 20 21]
[ 2  8 14 15 16 22]
[ 3  9 10 11 17 23]
[ 4  5  6 12 18 24]]
``````

For a square matrix, you could do this:

``````def mirror(a):
i, j = np.indices(a.shape)
return a[j, i]
``````

For the general case, we can do the same if we just offset each diagonal as needed:

``````def mirror(a):
m, n = a.shape
if m > n:
return mirror(a.T).T
i, j = np.indices(a.shape)
o = np.clip(i+j+1, m, n) - m
return a[j-o, i+o]
``````

(`np.indices(a.shape, dtype=np.uint16)` seems faster and enough for you use case.)

Demo:

``````import numpy as np

for m, n in (5, 3), (4, 6):
a = np.arange(m*n).reshape((m, n))
print(a)
print('->')
print(mirror(a))
print()
``````

Output (Attempt This Online!):

``````[[ 0  1  2]
[ 3  4  5]
[ 6  7  8]
[ 9 10 11]
[12 13 14]]
->
[[ 0  3  6]
[ 1  4  9]
[ 2  7 12]
[ 5 10 13]
[ 8 11 14]]

[[ 0  1  2  3  4  5]
[ 6  7  8  9 10 11]
[12 13 14 15 16 17]
[18 19 20 21 22 23]]
->
[[ 0  6 12 18 19 20]
[ 1  7 13 14 15 21]
[ 2  8  9 10 16 22]
[ 3  4  5 11 17 23]]
``````
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.