How can I get a sublist with wraparound in Python

Question:

Simple 1D case

I would like to get a substring with wraparound.

str = "=Hello community of Python="
#      ^^^^^               ^^^^^^^  I want this wrapped substring

str[-7]
> 'P'

str[5]
> 'o'

str[-7:5]
> ''

Why does this slice of a sequence starting at a negative index and ending in a positive one result in an empty string?

How would I get it to output "Python==Hell"?


Higher dimensional cases

In this simple case I could do some cutting and pasting, but in my actual application I want to get every sub-grid of size 2×2 of a bigger grid – with wraparound.

m = np.mat('''1 2 3; 
              4 5 6; 
              7 8 9''')

And I want to get all submatrices centered at some location (x, y), including '9 7; 3 1'. Indexing with m[x-1:y+1] doesn’t work for (x,y)=(0,0), nor does (x,y)=(1,0) give 7 8; 1 2

3D example

m3d = np.array(list(range(27))).reshape((3,3,3))
>
array([[[ 0,  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]]])

m3d[-1:1,-1:1,-1:1]
# doesn't give [[[26, 24], [20, 18]], [8, 6], [2, 0]]]

If need be I could write some code which gets the various sub-matrices and glues them back together, but this approach might get quite cumbersome when I have to apply the same method to 3d arrays.

I was hoping there would be an easy solution. Maybe numpy can help out here?

Asked By: Tim Kuipers

||

Answers:

Simply combine the two halfs yourself:

>>> str[-7:]+str[:5]
'Python==Hell'
Answered By: treuss

You could repeat your data enough so that you don’t need wraparound.

Substrings of length 3:

s = 'Python'
r = 3

s2 = s + s[:r-1]
for i in range(len(s)):
    print(s2[i:i+r])

Output:

Pyt
yth
tho
hon
onP
nPy

Sub-matrices of size 2×2:

import numpy as np

m = np.mat('''1 2 3; 
              4 5 6; 
              7 8 9''')
r = 2

m2 = np.tile(m, (2, 2))
for i in range(3):
    for j in range(3):
        print(m2[i:i+r, j:j+r])

Output (Attempt This Online!):

[[1 2]
 [4 5]]
[[2 3]
 [5 6]]
[[3 1]
 [6 4]]
[[4 5]
 [7 8]]
[[5 6]
 [8 9]]
[[6 4]
 [9 7]]
[[7 8]
 [1 2]]
[[8 9]
 [2 3]]
[[9 7]
 [3 1]]

For larger more-dimensional arrays, the simple np.tile adds mmore than necessary. You really just need to increase the size by + r-1 in each dimension, not by * 2. Like I did with the string. Not sure how to do that well with arrays. Plus I think you can also make your negative indexes work, so we just need someone to come along and do that.

Answered By: Kelly Bundy

Using Advanced indexing (see the section starting with "From a 4×3 array the corner elements should be selected using advanced indexing"):

import numpy as np

m = np.mat('''1 2 3; 
              4 5 6; 
              7 8 9''')

print(m[np.ix_(range(-1, 1), range(-1, 1))])
print(m[np.ix_(range(-2, 2), range(-2, 2))])
print(m[np.arange(-2, 2)[:, np.newaxis], range(-2, 2)])

Output (Attempt This Online!):

[[9 7]
 [3 1]]
[[5 6 4 5]
 [8 9 7 8]
 [2 3 1 2]
 [5 6 4 5]]
[[5 6 4 5]
 [8 9 7 8]
 [2 3 1 2]
 [5 6 4 5]]

Going through all sub-matrices

Since you want to go through all sub-matrices, we can beforehand separately prepare the row ranges and the column ranges, and then use pairs of them to quickly index:

import numpy as np

A = np.mat('''1 2 3; 
              4 5 6; 
              7 8 9''')

m, n = A.shape

rowranges = [
    (np.arange(i, i+2) % m)[:, np.newaxis]
    for i in range(m)
]
colranges = [
    np.arange(j, j+2) % n
    for j in range(n)
]

for rowrange in rowranges:
    for colrange in colranges:
        print(A[rowrange, colrange])

Output (Attempt This Online!):

[[1 2]
 [4 5]]
[[2 3]
 [5 6]]
[[3 1]
 [6 4]]
[[4 5]
 [7 8]]
[[5 6]
 [8 9]]
[[6 4]
 [9 7]]
[[7 8]
 [1 2]]
[[8 9]
 [2 3]]
[[9 7]
 [3 1]]

3D case

m3d = np.array(list(range(27))).reshape((3,3,3))
m3d[np.ix_(range(-1,1), range(-1,1), range(-1,1))]

Output:

array([[[26, 24],
        [20, 18]],

       [[ 8,  6],
        [ 2,  0]]])
Answered By: Kelly Bundy