In order to understand what I'm trying to achieve let's imagine an ndarray a with shape (8,8,8) from which I lexicographically take blocks of shape (4,4,4). So while iterating through such blocks the indexes would look as follows:
0: a[0:4, 0:4, 0:4]
1: a[0:4, 0:4, 4:8]
2: a[0:4, 4:8, 0:4]
3: a[0:4, 4:8, 4:8]
4: a[4:8, 0:4, 0:4]
5: a[4:8, 0:4, 4:8]
6: a[4:8, 4:8, 0:4]
7: a[4:8, 4:8, 4:8]
It is these blocks of data which I'm trying to access. Obviously, this can be described by using an expression which converts the current iteration to the corresponding indexes. An example of that is given below.
a = np.ones((8,8,8))
f = 4
length = round(a.shape[0] * a.shape[1] * a.shape[2] / f**3)
x = a.shape[0] / f
y = a.shape[1] / f
z = a.shape[2] / f
for i in range(length):
print(f"{i}: {round((int(i/(z*y))%x)*f)}:{round(f+(int(i/(z*y))%x)*f)}, {round((int(i/z)%y)*f)}:{round(f+(int(i/z)%y)*f)}, {round((i%z)*f)}:{round(f+(i%z)*f)}")
My apologies for having to do that to your eyes but it generates the following output:
0: 0:4, 0:4, 0:4
1: 0:4, 0:4, 4:8
2: 0:4, 4:8, 0:4
3: 0:4, 4:8, 4:8
4: 4:8, 0:4, 0:4
5: 4:8, 0:4, 4:8
6: 4:8, 4:8, 0:4
7: 4:8, 4:8, 4:8
So this does actually generate the right indexes, but it only allows you to access multiple blocks at once if they have the same index in the 0th and 1st axis, so no wrapping around. Ideally I would reshape this whole ndarray into an ndarray b with shape (4, 4, 32) and be ordered in such a way that b[:, :, :4] would return a[0:4, 0:4, 0:4], b[:, :, 4:12] returns an ndarray of shape (4, 4, 8) which contain a[0:4, 0:4, 4:8] and a[0:4, 4:8, 0:4] etc. I want this to be as fast as possible, so ideally, I keep the memory layout and just change the view on the array.
Lastly, if it helps to think about this conceptually, this is basically a variant of the ndarray.flatten() method but using blocks of shape (4, 4, 4) as "atomic size" if you will.
Hope this makes it clear enough!