from functools import reduce from loguru import logger import torch import torch.nn.functional as F from rubik.action import build_actions_tensor, parse_actions_str, sample_actions_str from rubik.display import stringify from rubik.tensor_utils import build_cube_tensor class Cube: """ A 4D tensor filled with colors. Dimensions have the following interpretation: - Face (from 0 to 5, with 0 = "Up", 1 = "Left", 2 = "Front", 3 = "Right", 4 = "Back", 5 = "Down"). - X coordinate (from 0 to self.size - 1, from Left to Right). - Y coordinate (from 0 to self.size - 1, from Back to Front). - Z coordinate (from 0 to self.size - 1, from Down to Up). Colors filling each tensor cell are from 0 to 6, 0 being the "dark" color, the rest according to order given in "colors" attribute. """ def __init__(self, colors: list[str], size: int): """ Create Cube from a given list of 6 colors and size. Example: cube = Cube(['U', 'L', 'C', 'R', 'B', 'D'], size = 3) """ tensor = build_cube_tensor(colors, size) self.coordinates = tensor.indices().transpose(0, 1).to(torch.int16) self.state = F.one_hot(tensor.values().long(), num_classes=7).to(torch.int16) self.actions = build_actions_tensor(size) self.history: list[list[int]] = [] self.colors = colors self.size = size def to(self, device: str | torch.device) -> "Cube": device = torch.device(device) dtype = torch.int16 if device == torch.device("cpu") else torch.float32 self.coordinates = self.coordinates.to(device=device, dtype=dtype) self.state = self.state.to(device=device, dtype=dtype) self.actions = self.actions.to(device=device, dtype=dtype) logger.info(f"Using device '{self.state.device}' and dtype '{dtype}'") return self def reset_history(self) -> None: """ Reset internal history of moves. """ self.history = [] return def shuffle(self, num_moves: int, seed: int = 0) -> None: """ Randomly shuffle the cube by the supplied number of steps, and reset history of moves. """ moves = sample_actions_str(num_moves, self.size, seed=seed) self.rotate(moves) self.reset_history() return def rotate(self, moves: str) -> None: """ Apply a sequence of moves (defined as plain string) to the cube. """ actions = parse_actions_str(moves) for action in actions: self.rotate_once(*action) return def rotate_once(self, axis: int, slice: int, inverse: int) -> None: """ Apply a move (defined as 3 coordinates) to the cube. """ action = self.actions[axis, slice, inverse] self.state = action @ self.state self.history.append([axis, slice, inverse]) return def compute_changes(self, moves: str) -> dict[int, int]: """ combine a sequence of moves and return the resulting changes. """ actions = parse_actions_str(moves) tensors = [self.actions[*action].to(torch.float32) for action in actions] result = reduce(lambda A, B: A @ B, tensors).to(torch.int16) return dict(result.indices().transpose(0, 1).tolist()) def solve(self, policy: str) -> None: """ Apply the specified solving policy to the cube. """ raise NotImplementedError def __str__(self): """ Compute a string representation of a cube. """ state = self.state.argmax(dim=-1).to(device="cpu", dtype=torch.int16) return stringify(state, self.colors, self.size)