Как рассчитать все 24 вращения 3D-массива?
У меня есть 3d-массив, описывающий поликуб (представьте себе кусок 3d-тетриса). Как я могу рассчитать все 24 вращения?
Процедуры манипулирования массивом Numpy включают метод rot90, который дает 4 из 24, но я не знаю, как рассчитать остальное. Моя единственная идея - преобразовать трехмерный массив в двумерную матрицу координат, умножить на матрицу вращения и выполнить обратное преобразование. Но я бы предпочел работать напрямую с 3D-массивом.
Пример массива 2x2x2:
>>> from numpy import array
>>> polycube
array([[[1, 0],
[1, 0]],
[[1, 1],
[0, 0]]])
Пример массива 3х3х3:
array([[[1, 1, 0],
[1, 1, 0],
[0, 0, 0]],
[[0, 0, 0],
[1, 0, 0],
[1, 0, 0]],
[[0, 0, 0],
[0, 0, 0],
[0, 0, 0]]])
Редактировать: я хочу только 24 сохраняющих ориентацию изометрии, а не все 48 поворотов и отражений (хотя было бы интересно узнать, как их сделать тоже). Если это помогает проверить, я считаю, что пример 3x3x3 не имеет вращательной симметрии и является киральным (поэтому 48 различны).
Мотивация: я пишу решатель для головоломки Soma cube -style.
Ответы
Ответ 1
Пока у меня их 12, составляющих numpy.transpose
для перестановки осей (xyz, yzx, zxy - все с одинаковыми руками) и rot90.
def rotations12(polycube):
for i in range(3):
polycube = numpy.transpose(polycube, (1, 2, 0))
for angle in range(4):
polycube = numpy.rot90(polycube)
yield polycube
Быстрый тест 12 различны: len(set(str(x) for x in rotations(polycube)))
Обновление: вот как я сделал все 24.
def rotations24(polycube):
# imagine shape is pointing in axis 0 (up)
# 4 rotations about axis 0
yield from rotations4(polycube, 0)
# rotate 180 about axis 1, now shape is pointing down in axis 0
# 4 rotations about axis 0
yield from rotations4(rot90(polycube, 2, axis=1), 0)
# rotate 90 or 270 about axis 1, now shape is pointing in axis 2
# 8 rotations about axis 2
yield from rotations4(rot90(polycube, axis=1), 2)
yield from rotations4(rot90(polycube, -1, axis=1), 2)
# rotate about axis 2, now shape is pointing in axis 1
# 8 rotations about axis 1
yield from rotations4(rot90(polycube, axis=2), 1)
yield from rotations4(rot90(polycube, -1, axis=2), 1)
def rotations4(polycube, axis):
"""List the four rotations of the given cube about the given axis."""
for i in range(4):
yield rot90(polycube, i, axis)
Используя эту вспомогательную функцию, обобщающую rot90 для вращения вокруг любой оси:
def rot90(m, k=1, axis=2):
"""Rotate an array k*90 degrees in the counter-clockwise direction around the given axis"""
m = numpy.swapaxes(m, 2, axis)
m = numpy.rot90(m, k)
m = numpy.swapaxes(m, 2, axis)
return m
Я не уверен, что этот помощник идеален, но, похоже, работает.
Ответ 2
Посмотрите на код для rot90
. Я вижу 3 варианта flip
и swapaxes
зависимости от параметра оси k
.
fliplr(m).swapaxes(0, 1)
fliplr(flipud(m))
fliplr(m.swapaxes(0, 1))
fliplr(m)
- это просто m[:, ::-1]
, и неудивительно, что flipud
- это m[::-1,...]
.
Вы можете перевернуть 3-ю ось с помощью m[:,:,::-1]
или m[...,::-1]
.
np.transpose
- это еще один инструмент для перестановки осей, который может или не может быть проще в использовании, чем swapaxes
.
Если rot90
дает вам 4 из поворотов, вы сможете применить те же процедуры для создания других. Вы просто должны понять логику, лежащую в основе rot90
.
например
def flipbf(m):
return m[:,:,::-1]
flipbf(m).swapaxes(0, 2)
flipbf(m).swapaxes(1, 2)
etc
Ответ 3
Изменение: так как мое решение в основном сводится к произведению четностей осей, умноженных на четность перестановки осей, самый простой метод для генерации всех регулярных вращений n-мерного массива это (пролистывание некоторого кода Форма @Divakar ответ):
import itertools as it
def p_parity(a):
a = np.asarray(a)
l = a.size
i, j = np.tril_indices(l, -1)
return np.product(np.sign(a[i] - a[j]))
def rotations_gen(m):
n = m.ndim
for i in it.product([-1, 1], repeat = n):
for p in it.permutations(np.arange(n)):
if np.product(i) * p_parity(p) == 1:
s = [slice(None, None, j) for j in i]
yield np.transpose(m[s], p)
Это работает для любых (даже неквадратных) тензоров произвольной размерности и основано непосредственно на определении регулярных вращений под тензорной алгеброй ниже.
Фон
Самый простой способ объяснить это в тензорных терминах, поэтому давайте превратим все эти вращения в тензоры вращения. Тензоры вращения - это nxn
матрицы, которые вращают n-мерное пространство. Как таковые они имеют несколько свойств:
np.linalg.det(R) == 1 # determinant = 1
np.inner(R, R.T) == np.eye(R.shape[0]) # Transpose is inverse
Кроме того, для поворотов на 90 градусов все члены должны быть либо 0, 1, либо -1.
В трех измерениях есть три основных семейства, которые составляют вместе, чтобы сделать ваши 24 вращения.
Первая простая перестановка:
A =
[[[1, 0, 0],
[0, 1, 0],
[0, 0, 1]],
[[0, 1, 0],
[0, 0, 1],
[1, 0, 0]],
[[0, 0, 1],
[1, 0, 0],
[0, 1, 0]]]
Второе включает в себя отрицание некоторых членов, так что произведение диагонали всегда равно 1:
B =
[[[ 1, 0, 0],
[ 0, 1, 0],
[ 0, 0, 1]],
[[-1, 0, 0],
[ 0,-1, 0],
[ 0, 0, 1]],
[[-1, 0, 0],
[ 0, 1, 0],
[ 0, 0,-1]],
[[ 1, 0, 0],
[ 0,-1, 0],
[ 0, 0,-1]]]
И третий определяет, является ли перестановка положительной или отрицательной, и отрицает условия, если отрицательная
C =
[[[ 1, 0, 0],
[ 0, 1, 0],
[ 0, 0, 1]],
[[ 0, 0,-1],
[ 0,-1, 0],
[-1, 0, 0]],
Важным моментом в этих семействах является то, что в каждом семействе любой продукт, степень или транспонирование двух матриц дают другую матрицу в семействе. Поскольку у нас есть три семейства, их произведения друг с другом образуют все возможные вращения, в этом случае 3 * 4 * 2 = 24
Примечание: другие 24 "нерегулярных" вращения - это те же матрицы, умноженные на -np.eye(3)
которые дают аналогичные матрицы с определителем = -1
заявка
Это все хорошо, но как это связано с манипулированием массивами? Мы не хотим вращаться с помощью умножения матриц, поскольку это приведет к чрезмерным затратам памяти и обработки. К счастью, каждая семья легко связана с манипулированием массивами, которое создает представление.
def A_(m, i): # i in (0, 1, 2)
idx = np.array([[0, 1, 2], [1, 2, 0], [2, 0, 1]])
return np.transpose(m, idx[i])
def B_(m, j): # j in (0, 1, 2, 3)
idx = np.array([[ 1, 1, 1],
[ 1,-1,-1],
[-1, 1,-1],
[-1,-1, 1]])
return m[::idx[j, 0], ::idx[j, 1], ::idx[j, 2]]
def C_(m, k): # k in (1, -1)
return np.transpose(m, np.arange(3)[::k])[::k, ::k, ::k]
Все они производят представления m
, и вы можете создать генератор, который создает представления, относящиеся ко всем поворотам:
def cube_rot_gen(m):
for i in [0, 1, 2]:
for j in [0, 1, 2, 3]:
for k in [1, -1]:
yield C_(B_(A_(m, i), j), k)
Ответ 4
Другой вариант - объединить вращения вокруг оси куба, который представляет матрицу. Что-то вроде:
import numpy as np
"""
Basic rotations of a 3d matrix.
----------
Example:
cube = array([[[0, 1],
[2, 3]],
[[4, 5],
[6, 7]]])
axis 0: perpendicular to the face [[0,1],[2,3]] (front-rear)
axis 1: perpendicular to the face [[1,5],[3,7]] (lateral right-left)
axis 2: perpendicular to the face [[0,1],[5,4]] (top-bottom)
----------
Note: the command m[:, ::-1, :].swapaxes(0, 1)[::-1, :, :].swapaxes(0, 2) rotates the cube m
around the diagonal axis 0-7.
"""
def basic_rot_ax(m, ax=0):
"""
:param m: 3d matrix
:return: rotate the cube around axis ax, perpendicular to the face [[0,1],[2,3]]
"""
ax %= 3
if ax == 0:
return np.rot90(m[:, ::-1, :].swapaxes(0, 1)[::-1, :, :].swapaxes(0, 2), 3)
if ax == 1:
return np.rot90(m, 1)
if ax == 2:
return m.swapaxes(0, 2)[::-1, :, :]
def axial_rotations(m, rot=1, ax=2):
"""
:param m: 3d matrix
:param rot: number of rotations
:param ax: axis of rotation
:return: m rotate rot times around axis ax, according to convention.
"""
if len(m.shape) is not 3:
assert IOError
rot %= 4
if rot == 0:
return m
for _ in range(rot):
m = basic_rot_ax(m, ax=ax)
return m
Если я не ошибаюсь, 24 оборота, которые вы ищете, представляют собой комбинацию этих 9 преобразований.
Ответ 5
Мы начнем с намерения получить все 48
комбинаций, чтобы получить общее представление о решении этого для массива n-dim. Позже мы отфильтруем ненужные 24
.
Общая идея, чтобы решить для всех вращений
Идея решения для общего случая состояла бы в том, чтобы в основном сделать две вещи - пролистать каждую ось и переставить оси со всеми комбинациями для данного числа осей.
Отразить: для переворота мы будем использовать параметр stepsize для нарезки, т.е. массив [:: stepsize]. Итак, чтобы перевернуть, это будет: [::-1]
и без переворота, просто: [::1]
. Этот stepsize
может быть назначен как переменная, варьирующаяся от 1
до -1
для двух простых комбинаций. Для ndarray просто распространите это на все оси.
Перестановка осей: чтобы достичь этого, мы можем использовать np.transpose
и указать требуемый перестановочный порядок в качестве параметра осей с ним. Мы сгенерируем все возможные заказы с помощью itertools.permutations
.
Это все, что есть! Пусть реализовать с в качестве входного a
3D
массива -
import itertools
def rotations48(a):
# Get all combinations of axes that are permutable
n = a.ndim
axcomb = np.array(list(itertools.permutations(range(n), n)))
# Initialize output array
out = np.zeros((6,2,2,2,) + a.shape,dtype=a.dtype)
# Run loop through all axes for flipping and permuting each axis
for i,ax in enumerate(axcomb):
for j,fx in enumerate([1,-1]):
for k,fy in enumerate([1,-1]):
for l,fz in enumerate([1,-1]):
out[i,j,k,l] = np.transpose(a[::fx,::fy,::fz],ax)
return out
Мы могли бы упростить переворачивание вложенных циклов одним циклом -
def rotations48(a):
n = a.ndim
axcomb = list(itertools.permutations(range(n), n)) # all axes combinations
pcomb = list(itertools.product([1,-1], repeat=n)) # all permuted orders
out = np.zeros((6,8,) + a.shape,dtype=a.dtype) # Initialize output array
for i,ax in enumerate(axcomb): #loop through all axes for permuting
for j,(fx,fy,fz) in enumerate(pcomb): # all flipping combinations
out[i,j] = np.transpose(a[::fx,::fy,::fz],ax)
return out
Итак, это дает нам все 48
комбинаций.
Расширение до большего размера: если мы хотим расширить это до 4D
массива, просто отредактируйте часть инициализации, чтобы расширить на 2
и нарезать вдоль еще одной оси.
Решить за 24
оборота
Теперь, как OP претендует иметь working solution
, чтобы получить желаемые 24
комбинаций, необходимо отфильтровать out
от предложенного нами решения. Я не смог найти общий шаблон для фильтрации, но получил индексы, необходимые для индексации -
idx = np.array([ 0, 3, 5, 6, 9, 10, 12, 15, 17, 18, 20, 23, 24, \
27, 29, 30, 32, 35, 37, 38, 41, 42, 44, 47])
Если вы заботитесь о том, чтобы выходные данные были такими же, как у rotations24
, мы бы имели:
idx = np.array([ 0, 10, 3, 9, 5, 15, 6, 12, 41, 27, 42, 24, 44, \
30, 47, 29, 18, 35, 17, 32, 20, 37, 23, 38])
Следовательно, получите необходимые 24
с indexing
-
final_out = out.reshape(48,-1)[idx]
Это работает для 3D
массивов с любой общей длиной.
Пробный прогон для проверки
# From /questions/1233932/how-to-calculate-all-24-rotations-of-3d-array/3967194#3967194 @Colonel Panic
def rotations24_array(a):
out0 = np.zeros((6,2,2,2,) + a.shape,dtype=a.dtype)
p = [list(i) for i in rotations24(a)]
out0 = np.zeros((6,4,m,m,m),dtype=a.dtype)
for i in range(6):
for j in range(4):
out0[i,j] = p[i][j]
return out0
Проверить -
In [486]: # Setup
...: np.random.seed(0)
...: m = 3
...: a = np.random.randint(11,99,(m,m,m))
...:
...: # Verify results
...: idx = np.array([ 0, 10, 3, 9, 5, 15, 6, 12, 41, 27, 42, 24, 44, \
...: 30, 47, 29, 18, 35, 17, 32, 20, 37, 23, 38])
...: out1 = rotations24_array(a).reshape(-1,m**3)
...: out2 = rotations48(a).reshape(48,-1)[idx]
...: print np.allclose(out1, out2)
True
Ответ 6
Внизу этой страницы находятся 24 матрицы вращения: http://www.euclideanspace.com/maths/algebra/matrix/transforms/examples/index.htm.
Если кусок тетриса был представлен массивом координатных троек, чтобы применить вращение к части, ты бы просто умножил матрицу на каждую тройку в массиве. Вы сделали бы это 24 раза, чтобы получить 24 разных кусочка.
Например, если ваш массив ящиков (1,2,5), (1,2,4), (1,3,4)
И вращение, которое вы рассматриваете в данный момент, например,
1 0 0
M = 0 0 -1
0 1 0
Тогда повернутая фигура имеет представление
(1,2,5).M, (1,2,4).M, (1,3,4).M
где. представляет умножение матрицы. Сделайте это для каждого M в списке, и вы получите все 24 вращения. (Вы можете либо pre-, либо post- умножить на матрицы, это не имеет значения, если вы хотите, чтобы весь набор вращений не имел определенного порядка.)
Так что это очень просто.
Теперь, чтобы получить данные из вашей структуры данных и в структуру массива, которую я использовал выше, вам нужно сделать что-то вроде (извините, я не знаю Python)
for i = 0 to 2
for j = 0 to 2
for k = 0 to 2
if polycube(i,j,k)==1 then push(i-1,j-1,k-1) onto end of newArray
или что-то типа того. Наконец, чтобы перейти от представления newArray к поликубу, вы должны сделать что-то вроде
polycube = all zeros
foreach point (i,j,k) in newArray
polycube(i+1,j+1,k+1) = 1
Для 2x2x2 поликуба вы бы сделали
for i = 0 to 1
for j = 0 to 1
for k = 0 to 1
if polycube(i,j,k)==1 then push(i-0.5,j-0.5,k-0.5) onto end of newArray
и соответствующая обратная функция.
Ответ 7
Сначала давайте определим 48 изометрий как матрицы, которые переставляют базисные векторы, возможно, переворачивая некоторые знаки:
def isometries():
basis = numpy.eye(3)
for ix in range(3):
for iy in range(3):
if iy == ix:
continue
for iz in range(3):
if iz == ix or iz == iy:
continue
for sx in range(-1, 2, 2): # -1, 1
for sy in range(-1, 2, 2):
for sz in range(-1, 2, 2):
yield numpy.array([sx * basis[ix], sy * basis[iy], sz * basis[iz]])
Затем позвольте фильтровать вращения, которые являются изометриями определителя 1:
def rotations():
return filter(lambda x: numpy.linalg.det(x) > 0, isometries())
Наконец, мы можем применить такой поворот к поликубу, сместив индексы массива так, чтобы внутренняя точка была началом координат, и умножив вектор индекса на матрицу вращения:
def rotate(rotation, shape):
shift = numpy.array([1,1,1])
res = numpy.zeros(27).reshape(3,3,3)
it = numpy.nditer(shape, flags=['multi_index'])
while not it.finished:
v = numpy.array(it.multi_index) - shift
w = rotation.dot(v)
dest = tuple(w + shift)
res[dest] = it[0]
it.iternext()
return res
Например:
>>> rotate(list(rotations())[5], polycube)
array([[[ 0., 0., 0.],
[ 0., 0., 0.],
[ 0., 0., 0.]],
[[ 0., 1., 1.],
[ 0., 0., 0.],
[ 0., 0., 0.]],
[[ 1., 1., 0.],
[ 1., 1., 0.],
[ 0., 0., 0.]]])
Ответ 8
Я решил опубликовать другой ответ. Мой первый ответ был очень простым, но не полностью в духе вопроса. Я думаю, что теперь у меня есть лучший ответ, подумав немного.
TL; DR Следующий код вращает поликуб на протяжении всех 24 поворотов куба. Это происходит без умножения матриц, вместо этого используется таблица индексов 3x3x3x3x3, которая дает несколько ключевых поворотов индексов в поликубе, и код Грея длиной 23 для группы вращения куба. В нем всего шесть реальных строк кода, почти все из которых предназначены для циклов. Он очень быстрый и довольно компактный (за исключением таблицы индексов, которая может быть сгенерирована программой (см. Ниже)).
import numpy as np
# polycube that we want to rotate
A = np.array([[['a', 'b', 'c'],
['d', 'e', 'f'],
['g', 'h', 'i']],
[['j', 'k', 'l'],
['m', '+', 'n'],
['o', 'p', 'q']],
[['r', 's', 't'],
['u', 'v', 'w'],
['x', 'y', 'z']]])
# I just do np.copy here because I don't know how to declare an array
T = np.copy(A)
Idx=np.array([
[[[[2,0,0], [2,1,0], [2,2,0]],
[[2,0,1], [2,1,1], [2,2,1]],
[[2,0,2], [2,1,2], [2,2,2]]],
[[[1,0,0], [1,1,0], [1,2,0]],
[[1,0,1], [1,1,1], [1,2,1]],
[[1,0,2], [1,1,2], [1,2,2]]],
[[[0,0,0], [0,1,0], [0,2,0]],
[[0,0,1], [0,1,1], [0,2,1]],
[[0,0,2], [0,1,2], [0,2,2]]]],
[[[[0,2,0], [1,2,0], [2,2,0]],
[[0,1,0], [1,1,0], [2,1,0]],
[[0,0,0], [1,0,0], [2,0,0]]],
[[[0,2,1], [1,2,1], [2,2,1]],
[[0,1,1], [1,1,1], [2,1,1]],
[[0,0,1], [1,0,1], [2,0,1]]],
[[[0,2,2], [1,2,2], [2,2,2]],
[[0,1,2], [1,1,2], [2,1,2]],
[[0,0,2], [1,0,2], [2,0,2]]]],
[[[[2,2,2], [2,1,2], [2,0,2]],
[[2,2,1], [2,1,1], [2,0,1]],
[[2,2,0], [2,1,0], [2,0,0]]],
[[[1,2,2], [1,1,2], [1,0,2]],
[[1,2,1], [1,1,1], [1,0,1]],
[[1,2,0], [1,1,0], [1,0,0]]],
[[[0,2,2], [0,1,2], [0,0,2]],
[[0,2,1], [0,1,1], [0,0,1]],
[[0,2,0], [0,1,0], [0,0,0]]]]
])
# Gray code gives Hamiltonican circuit through cube rotation group
gray = [3,2,1,2,1,2,3,2,3,2,1,2,1,2,3,2,3,2,1,2,1,2,3]
# Oops, index starting at 0
gray[:] = [x-1 for x in gray]
print(A)
# I'm sure there must be a more efficient way to move T (temp) into A
for g in gray:
for i in [0,1,2]:
for j in [0,1,2]:
for k in [0,1,2]:
T[i,j,k] = A[Idx[g,i,j,k,0],Idx[g,i,j,k,1],Idx[g,i,j,k,2]]
A = np.copy(T)
print("-----------------")
print(A)
Мое решение состоит в том, чтобы найти код Грея для группы вращения куба.
Во-первых, мы должны найти хорошее представление для группы вращения, которую вы используете. Рассмотрим куб 2x2x2 с кубами 1x1x1, помеченными следующим образом:
1 2
3 4 4 3
2 1
Мы начинаем с 1, чтобы соответствовать литературе по математике. Верхний левый квадрат должен быть передней панелью, а нижний правый квадрат - задней. Представьте 1, соединенный ребрами с 2 и 3 на передней поверхности, с 4 на задней поверхности и т.д.
Обратите внимание, что по диагонали противоположные кубы имеют одинаковое число. Теперь любое вращение куба соответствует перестановке четырех чисел на лицевой стороне. Кроме того, любая перестановка чисел на лицевой стороне соответствует повороту. Четыре поворота по оси Z, выходящие из страницы,
1 2 3 1 4 3 2 4
3 4 4 3 4 2 2 4 2 1 1 2 1 3 3 1
2 1 1 3 3 4 4 2
Мы можем пометить повороты по эффекту на лицевой стороне. Это 1234, 3142, 4321 и 2413.
Три дополнительных поворота вокруг оси x (плюс единичное вращение, с которого мы начинаем)
1 2 3 4 2 1 4 3
3 4 4 3 2 1 1 2 4 3 3 4 1 2 2 1
2 1 4 3 1 2 3 4
В наших более компактных обозначениях дополнительные вращения - 3421, 2143 и 4312.
Три дополнительных поворота вокруг оси Y
1 2 2 3 3 4 4 1
3 4 4 3 4 1 1 4 1 2 2 1 2 3 3 2
2 1 3 2 4 3 1 4
В компактной записи три дополнительных поворота - 2341, 3412 и 4123.
Есть три поворота, включая идентичность, которые фиксируют 1 диагональ:
1 2 1 4 1 3
3 4 4 3 2 3 3 2 4 2 2 4
2 1 4 1 3 1
с новыми маркированными 1423 и 1342.
Есть три поворота, включая идентичность, которые фиксируют 2 диагонали:
1 2 4 2 3 2
3 4 4 3 1 3 3 1 4 1 1 4
2 1 2 4 2 3
с новыми маркированными 4213 и 3241.
Есть три поворота, включая идентичность, которые фиксируют 3 диагонали:
1 2 2 4 4 1
3 4 4 3 3 1 1 3 3 2 2 3
2 1 4 2 1 4
с новыми маркированными 2431 и 4132.
Есть три поворота, включая идентичность, которые фиксируют 4 диагонали:
1 2 2 3 3 1
3 4 4 3 1 4 4 1 2 4 4 2
2 1 3 2 1 3
с новыми, помеченными 2314 и 3124.
Наконец, есть шесть крайних сальто плюс индивидуальность. Сальто в 1-2 края
1 2 2 1
3 4 4 3 3 4 4 3
2 1 1 2
с новым, представленным 2134. Аналогичным образом, имеются ребра по краям 1-3 (3214), 1-4 (4231), 2-3 (1324), 2-4 (1432) и 3-4 (1243),
Каждое из 24 вращений соответствует отдельной перестановке из 1234, и, поскольку имеется 24 перестановки и 24 вращения, каждая отдельная перестановка соответствует вращению. Мы говорим, что группа вращения куба и группа перестановок S_4 из 1234 изоморфны. (Один способ думать об этом состоит в том, что вращения куба действуют как перестановка диагоналей куба, и соответствие составляет 1-1.)
Мы могли бы выяснить влияние каждого из 24 вращений на ваши массивы, но это утомительно и подвержено ошибкам. Вместо этого мы увидим, что группа вращений генерируется всего тремя вращениями, ребро переворачивается
R1=transpose numbers in position2 1 and 2, and leave numbers in positions 3 and 4 alone
R2=transpose numbers in positions 2 and 3, and leave numbers in positions 1 and 4 alone
R3=transpose numbers in positions 3 and 4, and leave numbers in positions 1 and 2 alone
и их композиции. Мы добавим
R0=identity operation
для полноты. Используя только эти три генератора, мы можем найти гамильтонову цепь через граф Кэли группы вращения:
01 1234 apply R0 to starting position, i.e., do nothing
02 1243 apply R3 to previous
03 1423 apply R2 to previous
04 4123 R1
05 4213 R2
06 2413 R1
07 2143 R2
08 2134 R3
09 2314 R2
10 2341 R3
11 2431 R2
12 4231 R1
13 4321 R2
14 3421 R1
15 3241 R2
16 3214 R3
17 3124 R2
18 3142 R3
19 3412 R2
20 4312 R1
21 4132 R2
22 1432 R1
23 1342 R2
24 1324 R3
Обратите внимание, что каждая перестановка (т.е. Вращение куба) появляется в этом списке ровно один раз, и что переход от одного элемента в списке к следующему выполняется путем транспонирования элементов в положениях 1-2, 2-3 или 3. -4, что соответствует поворотам поворота кромки R1, R2 и R3. Этот код Грея для S_4 (и многое другое, что, к сожалению, довольно сложно понять, если вы не знаете о группах отражений и диаграммах Кокстера), представлен в знаменитой статье " Коды Грея для групп отражений" Конвея, Слоана и Уилкса.
Код Грея суммируется этой последовательностью из 23 чисел:
gray = {3,2,1,2,1,2,3,2,3,2,1,2,1,2,3,2,3,2,1,2,1,2,3}
Таким образом, мы значительно сократили объем работы, которую мы должны выполнить, начиная с расчета эффекта 24 различных вращений на поликубе 2x2x2 (а затем и на поликубе 3x3x3), до измерения эффекта только 3 различных вращений на наших кубах. Эти три функции вращения могут быть сохранены в массиве с индексом 0..3, а затем применены как
nextConfiguration = currentConfiguration.Rotation[gray[i]]
(Обратите внимание, что мы можем даже получить все 48 отражений/поворотов, если захотим, следуя этой последовательности вращений одним отражением P (подойдет любое отражение, поэтому выберите простейшее для реализации), а затем следуя этому отражению серой последовательностью в обратном порядке, который дает код Грея для полной группы из 48 отражений/вращений (построение больших последовательностей путем прикрепления обратной последовательности к себе является одной из "больших идей" при построении кодов Грея.))
Мы прошли долгий путь, даже не рассматривая представление форм, которые вы хотите повернуть, но теперь мы должны реализовать R1, R2 и R3, поэтому нам нужно рассмотреть структуру данных, которая представляет формы, трехмерный массив. Рассмотрим влияние R1, поворота ребра на 180 градусов на ось через середины 1-2 ребер, на наш массив 2x2x2:
(0,0,0) (1,0,0) (1,0,0) (0,0,0)
(0,1,0) (1,1,0) (0,0,1) (1,0,1) --> (1,0,1) (0,0,1) (1,1,0) (0,1,0)
(0,1,1) (1,1,1) (1,1,1) (0,1,1)
Это говорит нам о том, что наша операция R1 может быть реализована
output(0,0,0) = input(1,0,0)
output(1,0,0) = input(0,0,0)
output(0,1,0) = input(1,0,1)
output(1,1,0) = input(0,0,1)
output(0,0,1) = input(1,1,0)
output(1,0,1) = input(0,1,0)
output(0,1,1) = input(1,1,1)
output(1,1,1) = input(0,1,1)
Это определяет операцию R1 в случае 2x2x2. Должно быть понятно, как определить R2 и R3 в случае 2x2x2, а также как определить эти операции в случае 3x3x3.
Позвольте мне в заключение сказать, что определение операторов Ri утомительно и подвержено ошибкам, поэтому мы не хотим делать это слишком много, если сможем помочь. Мы можем избежать определения только двух операторов, R1 (уже определено выше) и F, вращения передней грани:
(0,0,0) (1,0,0) (0,1,0) (0,0,0)
(0,1,0) (1,1,0) (0,0,1) (1,0,1) --> (1,1,0) (1,0,0) (0,1,1) (0,0,1)
(0,1,1) (1,1,1) (1,1,1) (1,0,1)
Тогда мы можем представить R2 = FFFR1.F (где. Означает "с последующим"), а R3 = FFR1.FF (это "сопряженные" из R1.)
Для случая 3x3x3 это некоторая работа, чтобы найти таблицы индексов вручную, поэтому я написал процедуру для этого, а затем я нашел все 24 вращения алфавитного куба. Смотрите код ниже. Я уверен, что эксперт Python может сделать его гораздо короче. Это моя первая программа на Python.
Идея состоит в том, чтобы вращать не содержимое массива 3х3х3, а его индексы.
import numpy as np
# polycube that we want to rotate
A = np.array([[['a', 'b', 'c'],
['d', 'e', 'f'],
['g', 'h', 'i']],
[['j', 'k', 'l'],
['m', '+', 'n'],
['o', 'p', 'q']],
[['r', 's', 't'],
['u', 'v', 'w'],
['x', 'y', 'z']]])
# I just do np.copy here because I don't know how to declare an array
T = np.copy(A)
# matrix representing top front edge rotation (12)
R1 = np.array([[-1, 0, 0],
[ 0, 0, 1],
[ 0, 1, 0]])
# matrix representing right front edge rotation (23)
R2 = np.array([[ 0, 0, 1],
[ 0,-1, 0],
[ 1, 0, 0]])
# matrix representing bottom front edge rotation (34)
R3 = np.array([[-1, 0, 0],
[ 0, 0,-1],
[ 0,-1, 0]])
# Gray code gives Hamiltonican circuit through cube rotation group
gray = [3,2,1,2,1,2,3,2,3,2,1,2,1,2,3,2,3,2,1,2,1,2,3]
# trick for speed: we only need to apply each rotation matrix once
# to this polycube of indices, then we use the result as a table of
# pointers to new positions of polycube after rotation
Idx0 = np.array([[[[0,0,0], [1,0,0], [2,0,0]],
[[0,1,0], [1,1,0], [2,1,0]],
[[0,2,0], [1,2,0], [2,2,0]]],
[[[0,0,1], [1,0,1], [2,0,1]],
[[0,1,1], [1,1,1], [2,1,1]],
[[0,2,1], [1,2,1], [2,2,1]]],
[[[0,0,2], [1,0,2], [2,0,2]],
[[0,1,2], [1,1,2], [2,1,2]],
[[0,2,2], [1,2,2], [2,2,2]]]])
# Again, copy array because I don't know how to declare
Idx1 = np.copy(Idx0)
Idx2 = np.copy(Idx0)
Idx3 = np.copy(Idx0)
# We have to subtract [1,1,1] then add it again to move the center of the
# indexes to [0,0,0]
for i in [0,1,2]:
for j in [0,1,2]:
for k in [0,1,2]:
Idx1[i,j,k] = np.matmul(np.array([i,j,k])-np.array([1,1,1]),R1) + \
np.array([1,1,1])
for i in [0,1,2]:
for j in [0,1,2]:
for k in [0,1,2]:
Idx2[i,j,k] = np.matmul(np.array([i,j,k])-np.array([1,1,1]),R2) + \
np.array([1,1,1])
for i in [0,1,2]:
for j in [0,1,2]:
for k in [0,1,2]:
Idx3[i,j,k] = np.matmul(np.array([i,j,k])-np.array([1,1,1]),R3) + \
np.array([1,1,1])
# note that we have done only 81 vector*matrix multiplications. Now we don't
# have to do any more; we just look up the result in the index tables Idx[123]
print(A)
# I'm sure there must be a more efficient way to move T (temp) into A
for g in gray:
if g == 1:
for i in [0,1,2]:
for j in [0,1,2]:
for k in [0,1,2]:
T[i,j,k] = A[Idx1[i,j,k,0],Idx1[i,j,k,1],Idx1[i,j,k,2]]
A = np.copy(T)
elif g == 2:
for i in [0,1,2]:
for j in [0,1,2]:
for k in [0,1,2]:
T[i,j,k] = A[Idx2[i,j,k,0],Idx2[i,j,k,1],Idx2[i,j,k,2]]
A = np.copy(T)
elif g == 3:
for i in [0,1,2]:
for j in [0,1,2]:
for k in [0,1,2]:
T[i,j,k] = A[Idx3[i,j,k,0],Idx3[i,j,k,1],Idx3[i,j,k,2]]
A = np.copy(T)
print("-----------------")
print(A)
Вот вывод из программы
[[['a' 'b' 'c']
['d' 'e' 'f']
['g' 'h' 'i']]
[['j' 'k' 'l']
['m' '+' 'n']
['o' 'p' 'q']]
[['r' 's' 't']
['u' 'v' 'w']
['x' 'y' 'z']]]
-----------------
[[['z' 'w' 't']
['y' 'v' 's']
['x' 'u' 'r']]
[['q' 'n' 'l']
['p' '+' 'k']
['o' 'm' 'j']]
[['i' 'f' 'c']
['h' 'e' 'b']
['g' 'd' 'a']]]
-----------------
[[['x' 'o' 'g']
['y' 'p' 'h']
['z' 'q' 'i']]
[['u' 'm' 'd']
['v' '+' 'e']
['w' 'n' 'f']]
[['r' 'j' 'a']
['s' 'k' 'b']
['t' 'l' 'c']]]
-----------------
[[['r' 's' 't']
['j' 'k' 'l']
['a' 'b' 'c']]
[['u' 'v' 'w']
['m' '+' 'n']
['d' 'e' 'f']]
[['x' 'y' 'z']
['o' 'p' 'q']
['g' 'h' 'i']]]
-----------------
[[['a' 'd' 'g']
['j' 'm' 'o']
['r' 'u' 'x']]
[['b' 'e' 'h']
['k' '+' 'p']
['s' 'v' 'y']]
[['c' 'f' 'i']
['l' 'n' 'q']
['t' 'w' 'z']]]
-----------------
[[['c' 'l' 't']
['f' 'n' 'w']
['i' 'q' 'z']]
[['b' 'k' 's']
['e' '+' 'v']
['h' 'p' 'y']]
[['a' 'j' 'r']
['d' 'm' 'u']
['g' 'o' 'x']]]
-----------------
[[['i' 'h' 'g']
['f' 'e' 'd']
['c' 'b' 'a']]
[['q' 'p' 'o']
['n' '+' 'm']
['l' 'k' 'j']]
[['z' 'y' 'x']
['w' 'v' 'u']
['t' 's' 'r']]]
-----------------
[[['r' 'u' 'x']
['s' 'v' 'y']
['t' 'w' 'z']]
[['j' 'm' 'o']
['k' '+' 'p']
['l' 'n' 'q']]
[['a' 'd' 'g']
['b' 'e' 'h']
['c' 'f' 'i']]]
-----------------
[[['t' 'l' 'c']
['s' 'k' 'b']
['r' 'j' 'a']]
[['w' 'n' 'f']
['v' '+' 'e']
['u' 'm' 'd']]
[['z' 'q' 'i']
['y' 'p' 'h']
['x' 'o' 'g']]]
-----------------
[[['g' 'h' 'i']
['o' 'p' 'q']
['x' 'y' 'z']]
[['d' 'e' 'f']
['m' '+' 'n']
['u' 'v' 'w']]
[['a' 'b' 'c']
['j' 'k' 'l']
['r' 's' 't']]]
-----------------
[[['x' 'u' 'r']
['o' 'm' 'j']
['g' 'd' 'a']]
[['y' 'v' 's']
['p' '+' 'k']
['h' 'e' 'b']]
[['z' 'w' 't']
['q' 'n' 'l']
['i' 'f' 'c']]]
-----------------
[[['z' 'q' 'i']
['w' 'n' 'f']
['t' 'l' 'c']]
[['y' 'p' 'h']
['v' '+' 'e']
['s' 'k' 'b']]
[['x' 'o' 'g']
['u' 'm' 'd']
['r' 'j' 'a']]]
-----------------
[[['t' 's' 'r']
['w' 'v' 'u']
['z' 'y' 'x']]
[['l' 'k' 'j']
['n' '+' 'm']
['q' 'p' 'o']]
[['c' 'b' 'a']
['f' 'e' 'd']
['i' 'h' 'g']]]
-----------------
[[['c' 'f' 'i']
['b' 'e' 'h']
['a' 'd' 'g']]
[['l' 'n' 'q']
['k' '+' 'p']
['j' 'm' 'o']]
[['t' 'w' 'z']
['s' 'v' 'y']
['r' 'u' 'x']]]
-----------------
[[['a' 'j' 'r']
['b' 'k' 's']
['c' 'l' 't']]
[['d' 'm' 'u']
['e' '+' 'v']
['f' 'n' 'w']]
[['g' 'o' 'x']
['h' 'p' 'y']
['i' 'q' 'z']]]
-----------------
[[['z' 'y' 'x']
['q' 'p' 'o']
['i' 'h' 'g']]
[['w' 'v' 'u']
['n' '+' 'm']
['f' 'e' 'd']]
[['t' 's' 'r']
['l' 'k' 'j']
['c' 'b' 'a']]]
-----------------
[[['i' 'f' 'c']
['q' 'n' 'l']
['z' 'w' 't']]
[['h' 'e' 'b']
['p' '+' 'k']
['y' 'v' 's']]
[['g' 'd' 'a']
['o' 'm' 'j']
['x' 'u' 'r']]]
-----------------
[[['r' 'j' 'a']
['u' 'm' 'd']
['x' 'o' 'g']]
[['s' 'k' 'b']
['v' '+' 'e']
['y' 'p' 'h']]
[['t' 'l' 'c']
['w' 'n' 'f']
['z' 'q' 'i']]]
-----------------
[[['x' 'y' 'z']
['u' 'v' 'w']
['r' 's' 't']]
[['o' 'p' 'q']
['m' '+' 'n']
['j' 'k' 'l']]
[['g' 'h' 'i']
['d' 'e' 'f']
['a' 'b' 'c']]]
-----------------
[[['g' 'd' 'a']
['h' 'e' 'b']
['i' 'f' 'c']]
[['o' 'm' 'j']
['p' '+' 'k']
['q' 'n' 'l']]
[['x' 'u' 'r']
['y' 'v' 's']
['z' 'w' 't']]]
-----------------
[[['i' 'q' 'z']
['h' 'p' 'y']
['g' 'o' 'x']]
[['f' 'n' 'w']
['e' '+' 'v']
['d' 'm' 'u']]
[['c' 'l' 't']
['b' 'k' 's']
['a' 'j' 'r']]]
-----------------
[[['c' 'b' 'a']
['l' 'k' 'j']
['t' 's' 'r']]
[['f' 'e' 'd']
['n' '+' 'm']
['w' 'v' 'u']]
[['i' 'h' 'g']
['q' 'p' 'o']
['z' 'y' 'x']]]
-----------------
[[['t' 'w' 'z']
['l' 'n' 'q']
['c' 'f' 'i']]
[['s' 'v' 'y']
['k' '+' 'p']
['b' 'e' 'h']]
[['r' 'u' 'x']
['j' 'm' 'o']
['a' 'd' 'g']]]
-----------------
[[['g' 'o' 'x']
['d' 'm' 'u']
['a' 'j' 'r']]
[['h' 'p' 'y']
['e' '+' 'v']
['b' 'k' 's']]
[['i' 'q' 'z']
['f' 'n' 'w']
['c' 'l' 't']]]
Ответ 9
Я столкнулся с аналогичной проблемой при попытке случайного поворота трехмерного массива с горячим кодированием непосредственно перед передачей в мою нейронную сеть. Я продемонстрирую здесь для трехмерного массива, но это также работает с 4-м измерением (при использовании OHE).
>>> m = np.reshape(np.arange(8),(2,2,2))
>>> m
array([[[0, 1],
[2, 3]],
[[4, 5],
[6, 7]]])
Затем мы вращаем массив 3 раза, каждый раз в другом направлении. Повторите 24 000 раз, чтобы увидеть распределение (ожидая 1000 отсчетов для каждого уникального поворота):
>>> rot_list = []
>>> for _ in range(24000):
a = np.rot90(m,np.random.randint(0,4),axes=(0,np.random.randint(1,3)))
b = np.rot90(a,np.random.randint(0,4),axes=(np.random.randint(1,3),0))
c = np.rot90(b,np.random.randint(0,4),axes=(1,2)) # or axes=(2,1)
rot_list.append(c)
>>> unique_rotation_matrices, counts = np.unique(np.asarray(rot_list),axis=0, return_counts=True)
>>> len(unique_rotation_matrices)
24
Итак, мы видим, что мы получаем все 24 возможных поворота. Давайте посмотрим на их распределение:
>>> counts
[1062 946 917 982 1096 978 1153 936 939 907 1183 932 958 932 1122
926 1115 954 933 932 1135 924 1138 900]
Распределение выглядит довольно равномерно, но повторение этого несколько раз показывает, что оно слегка смещено.