"""Cube module."""
from __future__ import annotations
import math
import warnings
import numpy as np
from copy import deepcopy
from itertools import chain
from typing import Union, Tuple, Dict
from ..defs import CoordType, CoordsType
from .defs import NONE, SIZE, NUM_DIMS, NUM_CORNERS, NUM_EDGES, NUM_ORBIT_ELEMS
from .defs import CORNER_ORIENTATION_SIZE, EDGE_ORIENTATION_SIZE, CORNER_PERMUTATION_SIZE, EDGE_PERMUTATION_SIZE
from .enums import Axis, Orbit, Layer, Color, Face, Cubie, Move
from . import utils
ORIENTATION_AXES = [Axis.Y, Axis.Z, Axis.X]
REPR_ORDER = [Face.UP, Face.LEFT, Face.FRONT, Face.RIGHT, Face.BACK, Face.DOWN]
CORNER_ORBITS = [Orbit.TETRAD_111, Orbit.TETRAD_M11]
EDGE_ORBITS = [Orbit.SLICE_MIDDLE, Orbit.SLICE_EQUATOR, Orbit.SLICE_STANDING]
DEFAULT_COLOR_SCHEME = {
Face.UP: Color.WHITE,
Face.FRONT: Color.GREEN,
Face.RIGHT: Color.RED,
Face.DOWN: Color.YELLOW,
Face.BACK: Color.BLUE,
Face.LEFT: Color.ORANGE
}
INDEX_TO_CUBIE = np.array([
Cubie.UBL, Cubie.UFR, Cubie.DBR, Cubie.DFL, # TETRAD_111 orbit
Cubie.UBR, Cubie.UFL, Cubie.DBL, Cubie.DFR, # TETRAD_M11 orbit
Cubie.UB, Cubie.UF, Cubie.DB, Cubie.DF, # SLICE_MIDDLE orbit
Cubie.UL, Cubie.UR, Cubie.DL, Cubie.DR, # SLICE_STANDING orbit
Cubie.BL, Cubie.BR, Cubie.FL, Cubie.FR, # SLICE_EQUATOR orbit
Cubie.NONE], dtype=int)
CUBIE_TO_INDEX = np.zeros(max(len(Cubie), max(Cubie) + 1), dtype=int)
for cubie in INDEX_TO_CUBIE:
CUBIE_TO_INDEX[cubie] = np.where(INDEX_TO_CUBIE == cubie)[0][0]
CUBIE_TO_INDEX[Cubie.NONE] = NONE
CARTESIAN_AXES = [*Axis.cartesian_axes()]
min_orientation_axes = min([ORIENTATION_AXES[i:] + ORIENTATION_AXES[:i] for i in range(len(ORIENTATION_AXES))])
min_cartesian_axes = min([CARTESIAN_AXES[i:] + CARTESIAN_AXES[:i] for i in range(len(CARTESIAN_AXES))])
SHIFT_MULT = min_orientation_axes == min_cartesian_axes
SWAP_COLORS = {} # swap colors along axis
for axis in CARTESIAN_AXES:
shift = CARTESIAN_AXES.index(axis) - 1
axes = np.roll(np.flip(np.roll(CARTESIAN_AXES, -shift)), shift)
SWAP_COLORS[axis] = [Axis(ax).name for ax in axes]
COLORS_TYPE = [(axis.name, int) for axis in CARTESIAN_AXES]
ORBIT_OFFSET = {
Orbit.TETRAD_111: CUBIE_TO_INDEX[Cubie.UBL],
Orbit.TETRAD_M11: CUBIE_TO_INDEX[Cubie.UBR],
Orbit.SLICE_MIDDLE: CUBIE_TO_INDEX[Cubie.UB],
Orbit.SLICE_STANDING: CUBIE_TO_INDEX[Cubie.UL],
Orbit.SLICE_EQUATOR: CUBIE_TO_INDEX[Cubie.BL]
}
warnings.simplefilter("always")
[docs]
class Cube:
def __init__(self, scramble: Union[str, None] = None, repr: Union[str, None] = None, random_state: bool = False):
"""
Create :class:`Cube` object.
Parameters
----------
scramble : str or None, optional
Initial scramble. If ``None``, no scramble is applied. Default is ``None``.
repr : str or None, optional
Cube string representation. If not ``None``, the ``scramble`` parameter is ignored and
creates a cube with the given string representation. Default is ``None``.
See `Notes` for the string representation format.
random_state : bool, optional
If ``True``, the ``scramble`` and ``repr`` parameters are ignored and
creates a cube with a uniform random state. Default is ``False``.
Notes
-----
The ``repr`` parameter must contain characters from `{'W', 'G', 'R', 'Y', 'B', 'O'}`,
representing the colors :attr:`.Color.WHITE`, :attr:`.Color.GREEN`, :attr:`.Color.RED`,
:attr:`.Color.YELLOW`, :attr:`.Color.BLUE`, and :attr:`.Color.ORANGE`, respectively.
The order of the string representation is::
------------
| 01 02 03 |
| 04 05 06 |
| 07 08 09 |
---------------------------------------------
| 10 11 12 | 19 20 21 | 28 29 30 | 37 38 39 |
| 13 14 15 | 22 23 24 | 31 32 33 | 40 41 42 |
| 16 17 18 | 25 26 27 | 34 35 36 | 43 44 45 |
---------------------------------------------
| 46 47 48 |
| 49 50 51 |
| 52 53 54 |
------------
If the :attr:`orientation`, :attr:`permutation`, or :attr:`permutation_parity` values
cannot be determined correctly from the string representation,
the :attr:`orientation` and :attr:`permutation` arrays will contain ``-1`` at those positions,
and :attr:`permutation_parity` will be set to ``None``.
The default color scheme used for the cube is as follows (note: this may differ when using the ``repr`` parameter):
* :attr:`.Face.UP`: :attr:`.Color.WHITE`
* :attr:`.Face.FRONT`: :attr:`.Color.GREEN`
* :attr:`.Face.RIGHT`: :attr:`.Color.RED`
* :attr:`.Face.DOWN`: :attr:`.Color.YELLOW`
* :attr:`.Face.BACK`: :attr:`.Color.BLUE`
* :attr:`.Face.LEFT`: :attr:`.Color.ORANGE`
Examples
--------
>>> from cube_solver import Cube
Initial scramble.
>>> cube = Cube("U F2 R'")
>>> cube # string representation of the cube state
WWBWWBYYOGGROOROOBGGWGGWRRYBRRBRROOGYOOYBBWBBWWGYYGYYR
Initial string representation.
>>> cube = Cube(repr="WWBWWBYYOGGROOROOBGGWGGWRRYBRRBRROOGYOOYBBWBBWWGYYGYYR")
>>> print(cube) # print a visual layout of the cube state
---------
| W W B |
| W W B |
| Y Y O |
---------------------------------
| G G R | G G W | B R R | Y O O |
| O O R | G G W | B R R | Y B B |
| O O B | R R Y | O O G | W B B |
---------------------------------
| W W G |
| Y Y G |
| Y Y R |
---------
Initial random state.
>>> cube = Cube(random_state=True)
>>> cube.coords # coordinates of the cube state (result might differ) # doctest: +SKIP
(167, 48, 22530, 203841327)
"""
if scramble is not None and not isinstance(scramble, str):
raise TypeError(f"scramble must be str or None, not {type(scramble).__name__}")
if repr is not None and not isinstance(repr, str):
raise TypeError(f"repr must be str or None, not {type(repr).__name__}")
if not isinstance(random_state, bool):
raise TypeError(f"random_state must be bool, not {type(random_state).__name__}")
self._color_scheme: Dict[Face, Color]
"""
Color shceme of the cube.
Used to generate and parse the string representation.
"""
self._colors: np.ndarray
"""
Color representation array of the cube.
Used to generate and parse the string representation.
"""
self.orientation: np.ndarray
"""
Orientation array.
The ``orientation`` array contains the orientation values of the ``8`` corners and ``12`` edges of the cube.
The first ``8`` elements represent the `corner` orientation values, and the remaining ``12`` elements represent the
`edge` orientation values.
A corner is correctly oriented when the `top` or `bottom` facelet of the corner piece matches either the `top` or
`bottom` color of the cube. Corner orientation values are:
* ``0`` if the corner is `correctly` oriented.
* ``1`` if the corner is `twisted clockwise` relative to the correct orientation.
* ``2`` if the corner is `twisted counter-clockwise` relative to the correct orientation.
An edge is correctly oriented if, when placed in its correct position using only
:attr:`.Face.UP`, :attr:`.Face.DOWN`, :attr:`.Face.RIGHT` and :attr:`.Face.LEFT` face turns,
it does not appear `flipped`. Edge orientation values are:
* ``0`` if the edge is `correctly` oriented.
* ``1`` if the edge is incorrectly oriented (i.e. `flipped`).
"""
self.permutation: np.ndarray
"""
Permutation array.
The ``permutation`` array contains the permutation values of the ``8`` corners and ``12`` edges of the cube.
The first ``8`` elements represent the `corner` permutation values, and the remaining ``12`` elements represent the
`edge` permutation values.
The `solved state` permutation goes from ``0`` to ``7`` for the corners,
and from ``8`` to ``19`` for the edges. The piece ordering in the solved state is:
* Corners: [``UBL``, ``UFR``, ``DBR``, ``DFL``, ``UBR``, ``UFL``, ``DBL``, ``DFR``]
* Edges: [``UB``, ``UF``, ``DB``, ``DF``, ``UL``, ``UR``, ``DL``, ``DR``, ``BL``, ``BR``, ``FL``, ``FR``]
"""
self.permutation_parity: Union[bool, None]
"""
Permutation parity.
The ``permutation_parity`` indicates the parity of both `corner` and `edge` permutations
(i.e. both parities are always the same), ``True`` for ``odd`` parity, ``False`` for ``even`` parity,
and ``None`` if the parity cannot be determined.
The `solved state` permutation parity starts with ``even`` corner and endge parity.
"""
self.reset()
if random_state:
self.set_random_state()
elif repr is not None:
self._parse_repr(repr)
elif scramble is not None:
self.apply_maneuver(scramble)
@property
def coords(self) -> Tuple[int, ...]:
"""
Cube coordinates.
`Corner orientation`, `edge orientation`,
`corner permutation`, and `edge permutation` coordinates.
See Also
--------
get_coords
set_coords
Examples
--------
>>> from cube_solver import Cube
>>> cube = Cube("U F2 R'")
Get cube coordinates.
>>> cube.coords
(657, 0, 25253, 85684063)
Set cube coordinates.
>>> cube.coords = (0, 0, 0, 0) # solved state
>>> cube
WWWWWWWWWOOOOOOOOOGGGGGGGGGRRRRRRRRRBBBBBBBBBYYYYYYYYY
"""
coords = self.get_coords()
return tuple(coord if isinstance(coord, int) else coord[0] for coord in coords)
@coords.setter
def coords(self, coords: Tuple[int, ...]):
self.set_coords(coords)
@property
def is_solved(self) -> bool:
"""
Whether the cube is solved.
Examples
--------
>>> from cube_solver import Cube
>>> cube = Cube()
>>> cube.is_solved
True
>>> cube.apply_maneuver("U F2 R'")
>>> cube.is_solved
False
"""
return self.coords == (0, 0, 0, 0)
def __ne__(self, other: object) -> bool:
"""Negation of equality comparison."""
return not self.__eq__(other)
def __eq__(self, other: object) -> bool:
"""Equality comparison."""
if not isinstance(other, Cube):
return False
return repr(self) == repr(other)
def __repr__(self) -> str:
"""String representation of the :class:`Cube` object."""
solved_colors = np.full_like(self._colors, Color.NONE)
for face in Face.faces():
solved_colors[face._index][face.axis.name] = self._color_scheme[face]
# centers
for center in Cubie.centers():
self._colors[center._index] = solved_colors[center._index]
# corners and edges
for cubie in chain(Cubie.corners(), Cubie.edges()):
cubie_orientation = self.orientation[CUBIE_TO_INDEX[cubie]]
cubie_permutation = Cubie(INDEX_TO_CUBIE[self.permutation[CUBIE_TO_INDEX[cubie]]])
cubie_colors = np.array(solved_colors[cubie_permutation._index], dtype=COLORS_TYPE)
if cubie_permutation != Cubie.NONE:
if cubie.is_corner:
if cubie.orbit != cubie_permutation.orbit:
cubie_colors = np.array(cubie_colors[SWAP_COLORS[Axis.Y]], dtype=COLORS_TYPE)
if cubie_orientation:
shift = (cubie_orientation if cubie.orbit == Orbit.TETRAD_M11 else -cubie_orientation) * SHIFT_MULT
cubie_colors = np.array(tuple(np.roll(cubie_colors.tolist(), shift)), dtype=COLORS_TYPE)
else:
orbits = {cubie.orbit, cubie_permutation.orbit}
if orbits == {Orbit.SLICE_MIDDLE, Orbit.SLICE_EQUATOR}:
shift = (1 if cubie.orbit == Orbit.SLICE_EQUATOR else -1) * SHIFT_MULT
cubie_colors = np.array(tuple(np.roll(cubie_colors.tolist(), shift)), dtype=COLORS_TYPE)
elif orbits == {Orbit.SLICE_MIDDLE, Orbit.SLICE_STANDING}:
cubie_colors = np.array(cubie_colors[SWAP_COLORS[Axis.Y]], dtype=COLORS_TYPE)
elif orbits == {Orbit.SLICE_EQUATOR, Orbit.SLICE_STANDING}:
cubie_colors = np.array(cubie_colors[SWAP_COLORS[Axis.X]], dtype=COLORS_TYPE)
if cubie_orientation:
axis = Layer[cubie.orbit.name.split("_")[1]].axis
cubie_colors = np.array(cubie_colors[SWAP_COLORS[axis]], dtype=COLORS_TYPE)
self._colors[cubie._index] = cubie_colors
return "".join([Color(c).char for c in np.ravel([self._colors[face._index][face.axis.name] for face in REPR_ORDER])])
def __str__(self) -> str:
"""Print representation of the `Cube` object."""
repr = self.__repr__()
# up face
str = " " * SIZE + " " + "--" * SIZE + "---\n"
for i in range(SIZE):
j = REPR_ORDER.index(Face.UP) * SIZE * SIZE + i * SIZE
str += " " * SIZE + " | " + " ".join(repr[j:j+SIZE]) + " |\n"
# lateral faces
str += "--------" * SIZE + "---------\n"
for i in range(SIZE):
js = [REPR_ORDER.index(face) * SIZE * SIZE + i * SIZE for face in [Face.LEFT, Face.FRONT, Face.RIGHT, Face.BACK]]
str += "| " + " | ".join(" ".join(repr[j:j+SIZE]) for j in js) + " |\n"
str += "--------" * SIZE + "---------\n"
# down face
for i in range(SIZE):
j = REPR_ORDER.index(Face.DOWN) * SIZE * SIZE + i * SIZE
str += " " * SIZE + " | " + " ".join(repr[j:j+SIZE]) + " |\n"
str += " " * SIZE + " " + "--" * SIZE + "---"
return str
def _parse_repr(self, repr: str):
"""
Parse a string representation into a cube state.
If the :attr:`orientation`, :attr:`permutation`, or :attr:`permutation_parity` values
cannot be determined correctly from the string representation,
the :attr:`orientation` and :attr:`permutation` arrays will contain ``-1`` at those positions,
and :attr:`permutation_parity` will be set to ``None``.
Parameters
----------
repr : str
String representation of the cube.
"""
if len(repr) != len(REPR_ORDER)*SIZE*SIZE:
raise ValueError(f"repr length must be {len(REPR_ORDER)*SIZE*SIZE} (got {len(repr)})")
face_repr = [repr[i:i+SIZE*SIZE] for i in range(0, len(REPR_ORDER)*SIZE*SIZE, SIZE*SIZE)]
for face, _repr in zip(REPR_ORDER, face_repr):
self._colors[face._index][face.axis.name] = np.reshape([*map(Color.from_char, _repr)], (SIZE, SIZE))
# centers
inv_color_scheme = {color: Face.NONE for color in Color.colors()}
for center in Cubie.centers():
self._color_scheme[Face.from_char(center.name)] = Color(self._colors[center._index][center.axis.name])
inv_color_scheme.update({color: face for face, color in self._color_scheme.items()})
# corners and edges
for cubie in chain(Cubie.corners(), Cubie.edges()):
cubie_colors = [self._colors[cubie._index][axis.name] for axis in ORIENTATION_AXES]
cubie_faces = np.array([inv_color_scheme[color] for color in cubie_colors if color != Color.NONE], dtype=Face)
try:
if cubie.is_corner:
if cubie.orbit == Orbit.TETRAD_111:
x, z = [ORIENTATION_AXES.index(axis) for axis in (Axis.X, Axis.Z)]
cubie_faces[x], cubie_faces[z] = cubie_faces[z], cubie_faces[x] # swap along `Y` axis
self.orientation[CUBIE_TO_INDEX[cubie]] = [face.axis for face in cubie_faces].index(Axis.Y)
else:
axis_importance = [ORIENTATION_AXES.index(face.axis) for face in cubie_faces]
self.orientation[CUBIE_TO_INDEX[cubie]] = np.diff(axis_importance)[0] < 0
self.permutation[CUBIE_TO_INDEX[cubie]] = CUBIE_TO_INDEX[Cubie.from_faces(cubie_faces.tolist())]
except Exception:
self.orientation[CUBIE_TO_INDEX[cubie]] = NONE
self.permutation[CUBIE_TO_INDEX[cubie]] = CUBIE_TO_INDEX[Cubie.NONE]
if np.any(self.permutation == CUBIE_TO_INDEX[Cubie.NONE]):
warnings.warn("invalid string representation, setting undefined orientation and permutation values with -1")
self.permutation_parity = None
else:
if np.sum(self.orientation[:NUM_CORNERS]) % 3 != 0:
warnings.warn("invalid corner orientation")
if np.sum(self.orientation[NUM_CORNERS:]) % 2 != 0:
warnings.warn("invalid edge orientation")
is_valid_corner_perm = len(set(self.permutation[:NUM_CORNERS])) == NUM_CORNERS
is_valid_edge_perm = len(set(self.permutation[NUM_CORNERS:])) == NUM_EDGES
if is_valid_corner_perm and is_valid_edge_perm:
corner_parity = utils.get_permutation_parity(self.permutation[:NUM_CORNERS])
edge_parity = utils.get_permutation_parity(self.permutation[NUM_CORNERS:])
if corner_parity == edge_parity:
self.permutation_parity = corner_parity
else:
warnings.warn("invalid cube parity")
self.permutation_parity = None
else:
if not is_valid_corner_perm:
warnings.warn("invalid corner permutation")
if not is_valid_edge_perm:
warnings.warn("invalid edge permutation")
self.permutation_parity = None
[docs]
def reset(self):
"""
Reset the cube to the solved state using the default color scheme.
Examples
--------
>>> from cube_solver import Cube
>>> cube = Cube(random_state=True)
>>> cube.reset()
>>> cube
WWWWWWWWWOOOOOOOOOGGGGGGGGGRRRRRRRRRBBBBBBBBBYYYYYYYYY
"""
self._color_scheme = DEFAULT_COLOR_SCHEME.copy()
self._colors = np.full((SIZE,) * NUM_DIMS, Color.NONE, dtype=COLORS_TYPE)
self.orientation = np.zeros(NUM_CORNERS + NUM_EDGES, dtype=int)
self.permutation = np.arange(NUM_CORNERS + NUM_EDGES, dtype=int)
self.permutation_parity = False
[docs]
def set_random_state(self):
"""
Set a uniform random state.
Sets random :attr:`coords` (`corner orientation`, `edge orientation`,
`corner permutation`, and `edge permutation` coordinates).
Examples
--------
>>> from cube_solver import Cube
>>> cube = Cube()
>>> cube.set_random_state()
>>> cube.coords # result might differ # doctest: +SKIP
(167, 48, 22530, 203841327)
"""
self.set_coord("co", np.random.randint(CORNER_ORIENTATION_SIZE))
self.set_coord("eo", np.random.randint(EDGE_ORIENTATION_SIZE))
self.set_coord("cp", np.random.randint(CORNER_PERMUTATION_SIZE))
self.set_coord("ep", np.random.randint(EDGE_PERMUTATION_SIZE))
[docs]
def apply_move(self, move: Move):
"""
Apply a move to the cube.
Parameters
----------
move : Move
Move to apply.
Examples
--------
>>> from cube_solver import Cube, Move
>>> cube = Cube()
>>> cube.apply_move(Move.U1) # U face move
>>> cube.apply_move(Move.M2) # M2 slice move
>>> cube.apply_move(Move.FW3) # Fw' wide move
>>> cube.apply_move(Move.X1) # x rotation
>>> cube
RGGBBORGGWYWWYWGOOGOOGOOYWYYWYYWYRRBRRBRRBWYWBRBBGBOGO
"""
if not isinstance(move, Move):
raise TypeError(f"move must be Move, not {type(move).__name__}")
if move.is_face:
layer = move.layers[0]
shift = move.shifts[0]
cubies = np.roll(layer.perm, shift, axis=-1)
orientation = self.orientation[CUBIE_TO_INDEX[cubies]]
if shift % 2 == 1:
if move.axis == Axis.Z:
orientation = np.where(orientation != NONE, (orientation + [[1, 2, 1, 2], [1] * 4]) % ([3], [2]), NONE)
elif move.axis == Axis.X:
orientation[0] = np.where(orientation[0] != NONE, (orientation[0] + [2, 1, 2, 1]) % 3, NONE)
if self.permutation_parity is not None:
self.permutation_parity = not self.permutation_parity
self.orientation[CUBIE_TO_INDEX[layer.perm]] = orientation
self.permutation[CUBIE_TO_INDEX[layer.perm]] = self.permutation[CUBIE_TO_INDEX[cubies]]
elif move.is_slice:
shift = move.shifts[0]
base_move = Move[move.axis.name + "1"].layers[move.axis == Axis.Z].char
self.apply_move(Move[base_move + "W" + str(-shift % 4)]) # wide move
self.apply_move(Move[base_move + str(shift % 4)]) # face move
elif move.is_wide:
shift = move.shifts[0]
mult = 1 if Move[move.axis.name + "1"].layers[0].char == move.name[0] else -1
self.apply_move(Move[move.axis.name + str(mult * shift % 4)]) # rotation
self.apply_move(Move[Face.from_char(move.name[0]).opposite.char + str(shift % 4)]) # face move
elif move.is_rotation:
layers_perm = [layer.perm for layer in move.layers]
layers_shifted = [np.roll(layer, shift, axis=-1) for layer, shift in zip(layers_perm, move.shifts)]
rotation = {center: center for center in Cubie.centers()}
rotation.update({Cubie(key): Cubie(val) for key, val in zip(np.ravel(layers_shifted), np.ravel(layers_perm))})
rotation[Cubie.NONE] = Cubie.NONE
# centers
color_scheme = self._color_scheme.copy()
for center in Cubie.centers():
self._color_scheme[Face.from_char(rotation[center].name)] = color_scheme[Face.from_char(center.name)]
# corners and edges
cubies = [*Cubie.corners()] + [*Cubie.edges()]
rotations = [rotation[cubie] for cubie in cubies]
orientation = self.orientation[CUBIE_TO_INDEX[cubies]]
cubie_permutation = [Cubie(INDEX_TO_CUBIE[perm]) for perm in self.permutation[CUBIE_TO_INDEX[cubies]]]
if move.shifts[0] % 2 == 1:
is_corner = np.array([cubie.is_corner for cubie in cubies])
cubie_orbits = np.array([cubie.orbit for cubie in cubies])
perm_orbits = np.array([perm.orbit for perm in cubie_permutation])
corner_comp = edge_comp = [perm_orbits, cubie_orbits]
if move.axis == Axis.X:
corner_comp = [cubie_orbits, Orbit.TETRAD_M11]
edge_comp = [Orbit.SLICE_MIDDLE, Orbit.SLICE_MIDDLE]
elif move.axis == Axis.Y:
edge_comp = [Orbit.SLICE_EQUATOR, Orbit.SLICE_EQUATOR]
else:
corner_comp = [cubie_orbits, Orbit.TETRAD_111]
axis_comp = perm_orbits != np.where(is_corner, corner_comp[0], edge_comp[0])
condition = cubie_orbits == np.where(is_corner, corner_comp[1], edge_comp[1])
incr = np.where(condition, axis_comp, np.where(is_corner, -axis_comp.astype(int), ~axis_comp))
orientation = np.where(orientation != NONE, (orientation + incr) % np.where(is_corner, 3, 2), NONE)
self.orientation[CUBIE_TO_INDEX[rotations]] = orientation
self.permutation[CUBIE_TO_INDEX[rotations]] = [CUBIE_TO_INDEX[rotation[perm]] for perm in cubie_permutation]
[docs]
def apply_maneuver(self, maneuver: str):
"""
Apply a sequence of moves to the cube.
Accepts the following move types:
* Face moves (e.g. `U`, `F2`, `R'`).
* Slice moves (e.g. `M`, `E2`, `S'`).
* Wide moves (e.g. `Uw`, `Fw2`, `Rw'` or `u`, `f2`, `r'`).
* Rotations (e.g. `x`, `y2`, `z'`).
Parameters
----------
maneuver : str
The sequence of moves to apply.
Examples
--------
>>> from cube_solver import Cube
>>> cube = Cube()
>>> cube.apply_maneuver("U M2 Fw' x")
>>> cube
RGGBBORGGWYWWYWGOOGOOGOOYWYYWYYWYRRBRRBRRBWYWBRBBGBOGO
"""
if not isinstance(maneuver, str):
raise TypeError(f"maneuver must be str, not {type(maneuver).__name__}")
# get moves from attr `moves` if maneuver is an instance of the `Maneuver` str subclass
moves = getattr(maneuver, "moves", [Move.from_string(move_str) for move_str in maneuver.split()])
for move in moves:
self.apply_move(move)
[docs]
def get_coord(self, coord_name: str) -> CoordType:
"""
Get cube coordinate value.
Parameters
----------
coord_name : {'co', 'eo', 'cp', 'ep', 'pcp', 'pep'}
Get the specified cube coordinate.
* 'co' means `corner orientation`.
* 'eo' means `edge orientation`.
* 'cp' means `corner permutation`.
* 'ep' means `edge permutation`.
* 'pcp' means `partial corner permutation` (a value for each corner `orbit`).
* 'pep' means `partial edge permutation` (a value for each edge `orbit`).
Returns
-------
coord : int or tuple of int
Cube coordinate value. For partial coordinate values, a value of ``-1``
indicates an `orbit` with no permutation values (e.g., ``(-1, -1, 0)``),
meaning the permutation (and orientation) values for that `orbit` are set to ``-1``.
If only the value of the first `orbit` is available (i.e., ``(coord, -1, -1)``),
returns an `int` representing that partial coordinate value.
The `orbit` order for partial coordinate values is:
* Corner orbits: :attr:`.Orbit.TETRAD_111`, :attr:`.Orbit.TETRAD_M11`
* Edge orbits: :attr:`.Orbit.SLICE_MIDDLE`, :attr:`.Orbit.SLICE_EQUATOR`, :attr:`.Orbit.SLICE_STANDING`
Examples
--------
>>> from cube_solver import Cube
>>> cube = Cube("U F2 R'")
Get corner coordinates.
>>> cube.get_coord('co') # corner orientation
657
>>> cube.get_coord('cp') # corner permutation
25253
>>> cube.get_coord('pcp') # partial corner permutation
(984, 679)
Get edge coordinates.
>>> cube.get_coord('eo') # edge orientation
0
>>> cube.get_coord('ep') # edge permutation
85684063
>>> cube.get_coord('pep') # partial edge permutation
(8087, 7016, 3576)
"""
if not isinstance(coord_name, str):
raise TypeError(f"coord_name must be str, not {type(coord_name).__name__}")
if coord_name in ("co", "eo"):
orientation = self.orientation[:NUM_CORNERS] if coord_name == "co" else self.orientation[NUM_CORNERS:]
return utils.get_orientation_coord(orientation, 3 if coord_name == "co" else 2, is_modulo=True)
if coord_name in ("cp", "ep", "pcp", "pep"):
permutation = self.permutation[:NUM_CORNERS] if coord_name in ("cp", "pcp") else self.permutation[NUM_CORNERS:]
if coord_name in ("cp", "ep"):
coord = utils.get_permutation_coord(permutation)
if coord_name == "ep":
return coord // 2
return coord
orbits = CORNER_ORBITS if coord_name == "pcp" else EDGE_ORBITS
combs = [np.where(np.array([Cubie(INDEX_TO_CUBIE[p]).orbit for p in permutation]) == orbit)[0] for orbit in orbits]
coord = [utils.get_partial_permutation_coord(permutation[comb], comb) if len(comb) else NONE for comb in combs]
if any(c != NONE for c in coord[1:]):
return tuple(coord)
return coord[0]
raise ValueError(f"coord_name must be one of 'co', 'eo', 'cp', 'ep', 'pcp', 'pep' (got '{coord_name}')")
[docs]
def set_coord(self, coord_name: str, coord: CoordType):
"""
Set cube coordinate value.
Parameters
----------
coord_name : {'co', 'eo', 'cp', 'ep', 'pcp', 'pep'}
Set the specified cube coordinate.
* 'co' means `corner orientation`.
* 'eo' means `edge orientation`.
* 'cp' means `corner permutation`.
* 'ep' means `edge permutation`.
* 'pcp' means `partial corner permutation` (a value for each corner `orbit`).
* 'pep' means `partial edge permutation` (a value for each edge `orbit`).
coord : int or tuple of int
Cube coordinate value. For partial coordinate values, a value of ``-1``
indicates an `orbit` with no permutation values (e.g., ``(-1, -1, 0)``),
meaning the permutation (and orientation) values for that `orbit` are set to ``-1``.
If an `int` is passed as a partial coordinate value, the value
will be applied only to the first `orbit` (i.e., ``(coord, -1, -1)``).
The `orbit` order for partial coordinate values is:
* Corner orbits: :attr:`.Orbit.TETRAD_111`, :attr:`.Orbit.TETRAD_M11`
* Edge orbits: :attr:`.Orbit.SLICE_MIDDLE`, :attr:`.Orbit.SLICE_EQUATOR`, :attr:`.Orbit.SLICE_STANDING`
Notes
-----
Corner and edge permutation parities are always either both ``even`` or both ``odd``.
This constraint is enforced when setting the `(partial) corner permutation`
and the normal `edge permutation` coordinates by adjusting the permutation of the edges
in the :attr:`permutation` array while preserving the same normal `edge permutation` coordinate
(i.e. the number of valid edge permutations is effectively halved).
When setting the `partial edge permutation` coordinate, if the edge permutation parity
does not match the corner permutation parity, :attr:`permutation_parity` will be set to ``None``.
Examples
--------
>>> from cube_solver import Cube
>>> cube = Cube()
Set corner coordinates.
>>> cube.set_coord('co', 456) # corner orientation
>>> cube.orientation[:8]
array([0, 1, 2, 1, 2, 2, 0, 1])
>>> cube.set_coord('cp', 28179) # corner permutation
>>> cube.permutation[:8]
array([5, 4, 0, 7, 1, 3, 6, 2])
>>> cube.set_coord('pcp', (1273, 391)) # partial corner permutation
>>> cube.permutation[:8]
array([5, 4, 0, 7, 1, 3, 6, 2])
Set edge coordinates.
>>> cube.set_coord('eo', 673) # edge orientation
>>> cube.orientation[8:]
array([0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0])
>>> cube.set_coord('ep', 96690777) # edge permutation
>>> cube.permutation[8:]
array([12, 18, 10, 19, 9, 13, 14, 17, 16, 8, 11, 15])
>>> cube.set_coord('pep', (7262, 2633, 8640)) # partial edge permutation
>>> cube.permutation[8:]
array([12, 18, 10, 19, 9, 13, 14, 17, 16, 8, 11, 15])
Set some partial coordinates with ``-1``.
>>> cube.set_coord('pcp', (1273, -1)) # same as cube.set_coord('pcp', 1273)
>>> cube.permutation[:8]
array([-1, -1, 0, -1, 1, 3, -1, 2])
>>> cube.set_coord('pep', (7262, -1, -1)) # same as cube.set_coord('pep', 7262)
>>> cube.permutation[8:]
array([-1, -1, 10, -1, 9, -1, -1, -1, -1, 8, 11, -1])
"""
if not isinstance(coord_name, str):
raise TypeError(f"coord_name must be str, not {type(coord_name).__name__}")
if not isinstance(coord, (int, tuple)):
raise TypeError(f"coord must be int or tuple, not {type(coord).__name__}")
if coord_name in ("co", "eo"):
if not isinstance(coord, int):
raise TypeError(f"coord must be int for coord_name '{coord_name}', not {type(coord).__name__}")
orientation = self.orientation[:NUM_CORNERS] if coord_name == "co" else self.orientation[NUM_CORNERS:]
v = 3 if coord_name == "co" else 2
orientation[:] = utils.get_orientation_array(coord, v, len(orientation), force_modulo=True)
elif coord_name in ("cp", "ep", "pcp", "pep"):
permutation = self.permutation[:NUM_CORNERS] if coord_name in ("cp", "pcp") else self.permutation[NUM_CORNERS:]
if coord_name in ("cp", "ep"):
if not isinstance(coord, int):
raise TypeError(f"coord must be int for coord_name '{coord_name}', not {type(coord).__name__}")
permutation[:], permutation_parity = utils.get_permutation_array(coord, len(permutation), coord_name == "ep")
if coord_name == "ep":
permutation += NUM_CORNERS
if self.permutation_parity is None:
other_perm = self.permutation[NUM_CORNERS:] if coord_name == "cp" else self.permutation[:NUM_CORNERS]
if not np.any(other_perm == CUBIE_TO_INDEX[Cubie.NONE]):
other_parity = utils.get_permutation_parity(other_perm)
self.permutation_parity = permutation_parity if permutation_parity == other_parity else other_parity
else:
orbits = CORNER_ORBITS if coord_name == "pcp" else EDGE_ORBITS
if isinstance(coord, int):
coord_tuple = (coord,) + (NONE,) * (len(orbits) - 1)
else:
coord_tuple = coord
size = len(coord_tuple)
if size != len(orbits):
raise ValueError(f"coord tuple length must be {len(orbits)} for coord_name '{coord_name}' (got {size})")
perm = np.full_like(permutation, CUBIE_TO_INDEX[Cubie.NONE])
for coord, orbit in zip(coord_tuple, orbits):
if not isinstance(coord, int):
raise TypeError(f"coord tuple elements must be int, not {type(coord).__name__}")
if coord != NONE:
if coord < 0 or coord >= math.perm(len(perm), NUM_ORBIT_ELEMS):
raise ValueError(f"coord must be >= 0 and < {math.perm(len(perm), NUM_ORBIT_ELEMS)} (got {coord})")
partial_permuttion, combination = utils.get_partial_permutation_array(coord, NUM_ORBIT_ELEMS)
if np.any(perm[combination] != CUBIE_TO_INDEX[Cubie.NONE]):
raise ValueError(f"invalid partial coordinates, overlapping detected (got {coord_tuple})")
perm[combination] = partial_permuttion + ORBIT_OFFSET[orbit]
permutation[:] = perm
if np.any(self.permutation == CUBIE_TO_INDEX[Cubie.NONE]):
self.orientation = np.where(self.permutation == CUBIE_TO_INDEX[Cubie.NONE], NONE, self.orientation)
self.permutation_parity = None
permutation_parity = None
else:
corner_parity = utils.get_permutation_parity(self.permutation[:NUM_CORNERS])
edge_parity = utils.get_permutation_parity(self.permutation[NUM_CORNERS:])
permutation_parity = corner_parity
if corner_parity == edge_parity:
self.permutation_parity = corner_parity
else:
if coord_name == "pcp":
self.permutation_parity = edge_parity
else:
warnings.warn("invalid cube parity")
self.permutation_parity = None
if self.permutation_parity is not None and self.permutation_parity != permutation_parity:
self.permutation[-2:] = self.permutation[[-1, -2]]
if coord_name in ("cp", "pcp"):
self.permutation_parity = permutation_parity
condition = (self.permutation != CUBIE_TO_INDEX[Cubie.NONE]) & (self.orientation == NONE)
self.orientation = np.where(condition, 0, self.orientation)
else:
raise ValueError(f"coord_name must be one of 'co', 'eo', 'cp', 'ep', 'pcp', 'pep' (got '{coord_name}')")
[docs]
def get_coords(
self,
partial_corner_perm: bool = False,
partial_edge_perm: bool = False) -> CoordsType:
"""
Get cube coordinates.
Get the `corner orientation`, `edge orientation`,
`(partial) corner permutation` and `(partial) edge permutation` coordinates.
Parameters
----------
partial_corner_perm : bool, optional
If ``True``, returns the `partial corner permutation` coordinate,
otherwise returns the normal `corner permutation` coordinate. Default is ``False``.
partial_edge_perm : bool, optional
If ``True``, returns the `partial edge permutation` coordinate,
otherwise returns the normal `edge permutation` coordinate. Default is ``False``.
Returns
-------
coords : tuple of (int or tuple of int)
Cube coordinates in the following order:
`corner orientation`, `edge orientation`, `(partial) corner permutation`, `(partial) edge permutation`.
See Also
--------
get_coord
Examples
--------
>>> from cube_solver import Cube
>>> cube = Cube("U F2 R'")
Get cube coordinates.
>>> cube.get_coords()
(657, 0, 25253, 85684063)
Get cube coordinates with `partial corner permutation` and `partial edge permutation`.
>>> cube.get_coords(partial_corner_perm=True, partial_edge_perm=True)
(657, 0, (984, 679), (8087, 7016, 3576))
"""
if not isinstance(partial_corner_perm, bool):
raise TypeError(f"partial_corner_perm must be bool, not {type(partial_corner_perm).__name__}")
if not isinstance(partial_edge_perm, bool):
raise TypeError(f"partial_edge_perm must be bool, not {type(partial_edge_perm).__name__}")
return (self.get_coord("co"), self.get_coord("eo"),
self.get_coord("pcp" if partial_corner_perm else "cp"),
self.get_coord("pep" if partial_edge_perm else "ep"))
[docs]
def set_coords(
self,
coords: CoordsType,
partial_corner_perm: bool = False,
partial_edge_perm: bool = False):
"""
Set cube coordinates.
Set the `corner orientation`, `edge orientation`,
`(partial) corner permutation` and `(partial) edge permutation` coordinates.
Parameters
----------
coords : tuple of (int or tuple of int)
Cube coordinates in the following order:
`corner orientation`, `edge orientation`, `(partial) corner permutation`, `(partial) edge permutation`.
partial_corner_perm : bool, optional
If ``True``, sets the `partial corner permutation` coordinate,
otherwise sets the normal `corner permutation` coordinate. Default is ``False``.
partial_edge_perm : bool, optional
If ``True``, sets the `partial edge permutation` coordinate,
otherwise sets the normal `edge permutation` coordinate. Default is ``False``.
See Also
--------
set_coord
Examples
--------
>>> from cube_solver import Cube
>>> cube = Cube()
Set cube coordinates.
>>> coords = (657, 0, 25253, 85684063)
>>> cube.set_coords(coords)
>>> cube
WWBWWBYYOGGROOROOBGGWGGWRRYBRRBRROOGYOOYBBWBBWWGYYGYYR
Set cube coordinates with `partial corner permutation` and `partial edge permutation`.
>>> coords = (657, 0, (984, 679), (8087, 7016, 3576))
>>> cube.set_coords(coords, partial_corner_perm=True, partial_edge_perm=True)
>>> cube
WWBWWBYYOGGROOROOBGGWGGWRRYBRRBRROOGYOOYBBWBBWWGYYGYYR
"""
if not isinstance(coords, tuple):
raise TypeError(f"coords must be tuple, not {type(coords).__name__}")
if not isinstance(partial_corner_perm, bool):
raise TypeError(f"partial_corner_perm must be bool, not {type(partial_corner_perm).__name__}")
if not isinstance(partial_edge_perm, bool):
raise TypeError(f"partial_edge_perm must be bool, not {type(partial_edge_perm).__name__}")
if len(coords) != 4:
raise ValueError(f"coords tuple length must be 4 (got {len(coords)})")
self.set_coord("co", coords[0])
self.set_coord("eo", coords[1])
self.set_coord("pcp" if partial_corner_perm else "cp", coords[2])
self.set_coord("pep" if partial_edge_perm else "ep", coords[3])
[docs]
def copy(self) -> Cube:
"""
Return a copy of the cube.
Returns
-------
cube : Cube
Copy of cube object.
Examples
--------
>>> from cube_solver import Cube
>>> cube = Cube("U F2 R'")
>>> cube_copy = cube.copy()
>>> cube_copy
WWBWWBYYOGGROOROOBGGWGGWRRYBRRBRROOGYOOYBBWBBWWGYYGYYR
>>> cube_copy == cube
True
"""
return deepcopy(self)
[docs]
def apply_move(cube: Cube, move: Move) -> Cube:
"""
Return a copy of the the cube with the move applyed.
Parameters
----------
cube : Cube
Cube object.
move : Move
Move to apply.
Returns
-------
cube : Cube
Copy of the cube with the move applied.
Examples
--------
>>> from cube_solver import Cube, Move, apply_move
>>> cube = Cube()
>>> apply_move(cube, Move.U1) # U face move
WWWWWWWWWGGGOOOOOORRRGGGGGGBBBRRRRRROOOBBBBBBYYYYYYYYY
>>> apply_move(cube, Move.M2) # M2 slice move
WYWWYWWYWOOOOOOOOOGBGGBGGBGRRRRRRRRRBGBBGBBGBYWYYWYYWY
>>> apply_move(cube, Move.FW3) # Fw' wide move
WWWRRRRRROWWOWWOWWGGGGGGGGGYYRYYRYYRBBBBBBBBBOOOOOOYYY
>>> apply_move(cube, Move.X1) # x rotation
GGGGGGGGGOOOOOOOOOYYYYYYYYYRRRRRRRRRWWWWWWWWWBBBBBBBBB
"""
if not isinstance(cube, Cube):
raise TypeError(f"cube must be Cube, not {type(cube).__name__}")
cube = cube.copy()
cube.apply_move(move)
return cube
[docs]
def apply_maneuver(cube: Cube, maneuver: str) -> Cube:
"""
Return a copy of the cube with the sequence of moves applied.
Accepts the following move types:
* Face moves (e.g. `U`, `F2`, `R'`).
* Slice moves (e.g. `M`, `E2`, `S'`).
* Wide moves (e.g. `Uw`, `Fw2`, `Rw'` or `u`, `f2`, `r'`).
* Rotations (e.g. `x`, `y2`, `z'`).
Parameters
----------
cube : Cube
Cube object.
maneuver : str
The sequence of moves to apply.
Returns
-------
cube : Cube
Copy of the cube with the sequence of moves applied.
Examples
--------
>>> from cube_solver import Cube, apply_maneuver
>>> cube = Cube()
>>> apply_maneuver(cube, "U M2 Fw' x")
RGGBBORGGWYWWYWGOOGOOGOOYWYYWYYWYRRBRRBRRBWYWBRBBGBOGO
"""
if not isinstance(cube, Cube):
raise TypeError(f"cube must be Cube, not {type(cube).__name__}")
cube = cube.copy()
cube.apply_maneuver(maneuver)
return cube