Skip to content

Instantly share code, notes, and snippets.

@HexDecimal
Created February 18, 2020 02:21
Show Gist options
  • Save HexDecimal/0c21a64e9a86d25603a4d1ddd2b9c397 to your computer and use it in GitHub Desktop.
Save HexDecimal/0c21a64e9a86d25603a4d1ddd2b9c397 to your computer and use it in GitHub Desktop.
Rotate a NumPy array by 45 degrees.
# To the extent possible under law, Kyle Stewart has waived all copyright and
# related or neighboring rights to this work.
# This work is published from: United States.
import functools
from typing import Any, Tuple
import numpy as np # type: ignore
@functools.lru_cache
def ring(radius: int, offset: int = 0, roll: int = 0) -> Tuple[np.ndarray, np.ndarray]:
"""Return an advanced NumPy index of a ring on the array."""
index: Any = []
for x in range(-radius, radius + 1):
index.append((x, -radius))
for y in range(-radius + 1, radius + 1):
index.append((radius, y))
for x in range(radius - 1, -radius, -1):
index.append((x, radius))
for y in range(radius, -radius, -1):
index.append((-radius, y))
index = np.transpose(index)
index += offset
index = np.roll(index, roll, 1)
index.flags["WRITEABLE"] = False
return index[0], index[1]
def rotate45(arr: Any, times: int = 1) -> np.ndarray:
"""Return a 2D array rotated by 45 degrees.
`arr` is a square array-like object with an odd-numbered size.
`times` is the number of times to rotate the array counter-clockwise by
45 degrees.
"""
out = np.copy(arr)
assert out.shape[0] == out.shape[1]
assert out.shape[0] % 2 != 0
times %= 8
radius = offset = out.shape[0] // 2
for i in range(radius + 1):
out[ring(i, offset)] = out[ring(i, offset, i * times)]
return out
@sam-the-programmer
Copy link

Hi, @HexDecimal, would it be possible for me to use this in a repo 📚 of mine, a reinforcement learning Connect 4 bot? As you expressed want of legal control in:

# To the extent possible under law, Kyle Stewart has waived all copyright and
# related or neighboring rights to this work.
# This work is published from: United States.

I thought I'd ask you first before publishing. I love this, I was trying to work this out for so long, and this fixed it! Thank you for making this 😊.

@sam-the-programmer
Copy link

Furthermore, do you have any perspective on a similar problem for the same project:

I have an array...

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

I want to rotate it 45 degrees in a different way - like this:

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

It has turned into a diamond.

@HexDecimal
Copy link
Author

You can freely use it, even if you didn't ask first. The license is CC0 if that wasn't clear: https://creativecommons.org/publicdomain/zero/1.0/

I want to rotate it 45 degrees in a different way - like this:

That won't be possible in plain NumPy, you'll have to do this by projecting your tilted or isometric view into a 2D array. You either make a 2D rectangle and ignore the indexes outside of the shape or you transform the index so that an index for a diamond translates to a rectangle. It's hard to explain well, but there's a very good guide on working with hexagons in this way.

This algorithm is normally for a quick one-off change of an array, not for storing and accessing projected values. It's also possible that these functions can be improved, I haven't looked at this in a while.

@HexDecimal
Copy link
Author

Projection would be to manipulate how indexes are handled to that the following two shapes are treated as similar:

      0,0
   1,0   0,1
2,0   1,1   0,2
   2,1   1,2
      2,2
0,0 0,1 0,2
1,0 1,1 1,2
2,0 2,1 2,2

You then use the lower shape for storage and the upper shape for displaying the stored data.

@sam-the-programmer
Copy link

Hi there, sorry it took a bit to get back to you.

I am sorry, I am struggling to understand how to change the hexagon example to a rectangular array with code. Do you have any ideas how to implement what you said on the most recent comment you made?

@HexDecimal
Copy link
Author

HexDecimal commented Nov 5, 2021

You wanted diagonal tiles so you don't need to go full hexagonal anything. The hexagon examples were all using rectangular arrays, and if that's difficult to understand then I'll have a hard time explaining it.

So back to these:

      0,0
   1,0   0,1
2,0   1,1   0,2
   2,1   1,2
      2,2
0,0 0,1 0,2
1,0 1,1 1,2
2,0 2,1 2,2

They might look like two different arrays, but that is wrong. They are the same array but with a different way of representing them. The lower representation makes it clear how they're stored as a contiguous array. The above is how you want to display them.

So where X goes SW and Y goes SE:

      0,0
   x,0   0,y
...   x,y   ...

To convert the tile index from a grid to where it's displayed you could use a transform function like this basic one:

def grid_to_diamond(x: int, y: int) -> Tuple[int, int]:
    """
    Example:
        >>> grid_to_diamond(0, 0)
        (0, 0)
        >>> grid_to_diamond(1, 0)  # SW
        (-1, 1)
        >>> grid_to_diamond(0, 1)  # SE
        (1, 1)
        >>> grid_to_diamond(1, 1)  # S
        (0, 2)
    """
    return y - x, y + x  # SW:(-x, x) + SE:(y, y)

This kind of math can be "simplified" if you understand how to use transformation matrices.

>>> import numpy as np
>>> m = np.matrix([[-1, 1], [1, 1]])  # SW:(-x, x) + SE:(y, y)
>>> [1, 1] @ m.A  # Same as grid_to_diamond.
array([2, 0])
>>> m.I  # Inverted matrix.
matrix([[ 0.5,  0.5],
        [-0.5,  0.5]])
>>> [2, 0] @ m.I.A  # Transform the display position back into the grid position.
array([1., 1.])

This is stuff I haven't worked with in a while, and when I did work with it I didn't have NumPy at the time. I might be too rusty on this topic to explain it any better. The best advice I can give you is to tell you that your diamond tiles are just a 2D grid that's been tilted, and you should not "tilt" your data when you store it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment