Aaron

Initial commit;

### Usage
```
$ python3 solver.py -h
usage: solver.py [-h] -f STATEFILE [-l LOGLEVEL] [-m MAXDEPTH]
optional arguments:
-h, --help show this help message and exit
-f STATEFILE, --state-file STATEFILE
JSON file containing cube data.
-l LOGLEVEL, --log-level LOGLEVEL
DEBUG|INFO|WARNING|ERROR|CRITICAL
-m MAXDEPTH, --max-depth MAXDEPTH
Max number of moves for solution. 8 might take a day.
9 might take a year.
```
### Methods
Everything about this is super basic. The RubiksCube class utilizes a dictionary
containing a 3x3 array for each face of the cube.
Rotating a face maps each individual square to its new location in the array.
The solver is 100% brute force with one optimization: don't do the same move twice
in a row. See below for benchmark.
There is plenty of room for improvement.
### Benchmark
```
Depth Seconds Hours Days
1 7.51E-05 2.08616256713867E-08 8.69234402974447E-10
2 0.005095720291138 1.41547785864936E-06 5.897824411039E-08
3 0.096453905105591 2.67927514182197E-05 1.11636464242582E-06
4 0.846158742904663 0.000235044095251 9.79350396880397E-06
5 17.1716315746307 0.00476989765962 0.000198745735817
6 291.108149528503 0.080863374869029 0.00336930728621
7 6492.00966076708 1.80333601687975 0.075139000703323
8 82340.502260004 22.87236173889 0.95301507245375
9 1392582.81363459 386.828559342942 16.1178566392892
10 26932679.8603269 7481.29996120191 311.720831716746
11 442059408.487385 122794.280135385 5116.42833897436
12 7370699820.7483 2047416.61687453 85309.0257031054
13 119587158616.281 33218655.171189 1384110.63213288
14 2153078857082.02 598077460.300561 24919894.17919
CPU Specs:
Architecture: x86_64
CPU(s): 4
On-line CPU(s) list: 0-3
Thread(s) per core: 2
Core(s) per socket: 2
Socket(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 61
Model name: Intel(R) Core(TM) i5-5300U CPU @ 2.30GHz
Stepping: 4
CPU MHz: 2709.210
CPU max MHz: 2900.0000
L1d cache: 32K
L1i cache: 32K
L2 cache: 256K
L3 cache: 3072K
```
### Future Plans
Rethink the cube class. Can it be faster? Simpler?
Add distributed processing. Multiprocess.
... ...
{
"U": [
[
"W",
"W",
"W"
],
[
"W",
"W",
"W"
],
[
"W",
"W",
"W"
]
],
"D": [
[
"Y",
"Y",
"Y"
],
[
"Y",
"Y",
"Y"
],
[
"Y",
"Y",
"Y"
]
],
"F": [
[
"O",
"O",
"O"
],
[
"O",
"O",
"O"
],
[
"O",
"O",
"O"
]
],
"B": [
[
"R",
"R",
"R"
],
[
"R",
"R",
"R"
],
[
"R",
"R",
"R"
]
],
"R": [
[
"G",
"G",
"G"
],
[
"G",
"G",
"G"
],
[
"G",
"G",
"G"
]
],
"L": [
[
"B",
"B",
"B"
],
[
"B",
"B",
"B"
],
[
"B",
"B",
"B"
]
]
}
\ No newline at end of file
... ...
###
# A Rubik's cube has 6 surfaces, each a different color:
# - Red
# - White
# - Blue
# - Orange
# - Green
# - Yellow
#
# Each face is comprised of 9 different squares, each of which can
# be moved relative to the other pieces.
#
# The center square of each face is fixed relative to the other
# center squares on each face:
# - White opposite yellow
# - Blue opposite green
# - Red opposite orange
#
# On each cube, there are:
# - 8 corners, with 3 squares each
# - 12 edges, with 2 squares each
# - 6 middles, with 1 square each
#
# - 54 total squares, 9 of each color
#
###
import json
import random
class RubiksCube(object):
def __init__(self):
self.cube = {'U': [['W','W','W'],
['W','W','W'],
['W','W','W']],
'D': [['Y','Y','Y'],
['Y','Y','Y'],
['Y','Y','Y']],
'F': [['O','O','O'],
['O','O','O'],
['O','O','O']],
'B': [['R','R','R'],
['R','R','R'],
['R','R','R']],
'R': [['G','G','G'],
['G','G','G'],
['G','G','G']],
'L': [['B','B','B'],
['B','B','B'],
['B','B','B']]
}
def __repr__(self):
cube_size = len(self.cube)
width = 3 * cube_size - 1
out = (" " * cube_size) + '%s %s %s' % tuple(self.cube['U'][0]) + (" " * cube_size) + "\n"
out += (" " * cube_size) + '%s %s %s' % tuple(self.cube['U'][1]) + (" " * cube_size) + "\n"
out += (" " * cube_size) + '%s %s %s' % tuple(self.cube['U'][2]) + (" " * cube_size) + "\n"
out += '%s %s %s %s %s %s %s %s %s' % tuple(self.cube['L'][0] + self.cube['F'][0] + self.cube['R'][0]) + "\n"
out += '%s %s %s %s %s %s %s %s %s' % tuple(self.cube['L'][1] + self.cube['F'][1] + self.cube['R'][1]) + "\n"
out += '%s %s %s %s %s %s %s %s %s' % tuple(self.cube['L'][2] + self.cube['F'][2] + self.cube['R'][2]) + "\n"
out += (" " * cube_size) + '%s %s %s' % tuple(self.cube['D'][0]) + (" " * cube_size) + "\n"
out += (" " * cube_size) + '%s %s %s' % tuple(self.cube['D'][1]) + (" " * cube_size) + "\n"
out += (" " * cube_size) + '%s %s %s' % tuple(self.cube['D'][2]) + (" " * cube_size) + "\n"
out += (" " * cube_size) + '%s %s %s' % tuple(self.cube['B'][0]) + (" " * cube_size) + "\n"
out += (" " * cube_size) + '%s %s %s' % tuple(self.cube['B'][1]) + (" " * cube_size) + "\n"
out += (" " * cube_size) + '%s %s %s' % tuple(self.cube['B'][2]) + (" " * cube_size) + "\n"
return out
def uMove(self, repeat=0):
self.rotateFaceClockwise('U')
f_copy = [x for x in self.cube['F'][0]]
self.cube['F'][0][0] = self.cube['R'][0][0]
self.cube['F'][0][1] = self.cube['R'][0][1]
self.cube['F'][0][2] = self.cube['R'][0][2]
self.cube['R'][0][0] = self.cube['B'][0][0]
self.cube['R'][0][1] = self.cube['B'][0][1]
self.cube['R'][0][2] = self.cube['B'][0][2]
self.cube['B'][0][0] = self.cube['L'][0][0]
self.cube['B'][0][1] = self.cube['L'][0][1]
self.cube['B'][0][2] = self.cube['L'][0][2]
self.cube['L'][0][0] = f_copy[0]
self.cube['L'][0][1] = f_copy[1]
self.cube['L'][0][2] = f_copy[2]
if repeat > 0:
self.uMove(repeat-1)
def fMove(self, repeat=0):
self.rotateFaceClockwise('F')
u_copy = [x for x in self.cube['U'][2]]
self.cube['U'][2][0] = self.cube['L'][2][2]
self.cube['U'][2][1] = self.cube['L'][1][2]
self.cube['U'][2][2] = self.cube['L'][0][2]
self.cube['L'][0][2] = self.cube['D'][0][0]
self.cube['L'][1][2] = self.cube['D'][0][1]
self.cube['L'][2][2] = self.cube['D'][0][2]
self.cube['D'][0][0] = self.cube['R'][2][0]
self.cube['D'][0][1] = self.cube['R'][1][0]
self.cube['D'][0][2] = self.cube['R'][0][0]
self.cube['R'][0][0] = u_copy[0]
self.cube['R'][1][0] = u_copy[1]
self.cube['R'][2][0] = u_copy[2]
if repeat > 0:
self.fMove(repeat-1)
def dMove(self, repeat=0):
self.rotateFaceClockwise('D')
f_copy = [x for x in self.cube['F'][2]]
self.cube['F'][2][0] = self.cube['L'][2][0]
self.cube['F'][2][1] = self.cube['L'][2][1]
self.cube['F'][2][2] = self.cube['L'][2][2]
self.cube['L'][2][0] = self.cube['B'][0][2]
self.cube['L'][2][1] = self.cube['B'][0][1]
self.cube['L'][2][2] = self.cube['B'][0][0]
self.cube['B'][0][0] = self.cube['R'][2][2]
self.cube['B'][0][1] = self.cube['R'][2][1]
self.cube['B'][0][2] = self.cube['R'][2][0]
self.cube['R'][2][0] = f_copy[0]
self.cube['R'][2][1] = f_copy[1]
self.cube['R'][2][2] = f_copy[2]
if repeat > 0:
self.dMove(repeat - 1)
def bMove(self, repeat=0):
self.rotateFaceClockwise('B')
d_copy = [x for x in self.cube['D'][2]]
self.cube['D'][2][0] = self.cube['L'][0][0]
self.cube['D'][2][1] = self.cube['L'][1][0]
self.cube['D'][2][2] = self.cube['L'][2][0]
self.cube['L'][0][0] = self.cube['U'][0][2]
self.cube['L'][1][0] = self.cube['U'][0][1]
self.cube['L'][2][0] = self.cube['U'][0][0]
self.cube['U'][0][0] = self.cube['R'][0][2]
self.cube['U'][0][1] = self.cube['R'][1][2]
self.cube['U'][0][2] = self.cube['R'][2][2]
self.cube['R'][0][2] = d_copy[0]
self.cube['R'][1][2] = d_copy[1]
self.cube['R'][2][2] = d_copy[2]
if repeat > 0:
self.bMove(repeat - 1)
def lMove(self, repeat=0):
self.rotateFaceClockwise('L')
b_copy = [self.cube['B'][x][0] for x in range(len(self.cube['B']))]
self.cube['B'][0][0] = self.cube['D'][0][0]
self.cube['B'][1][0] = self.cube['D'][1][0]
self.cube['B'][2][0] = self.cube['D'][2][0]
self.cube['D'][0][0] = self.cube['F'][0][0]
self.cube['D'][1][0] = self.cube['F'][1][0]
self.cube['D'][2][0] = self.cube['F'][2][0]
self.cube['F'][0][0] = self.cube['U'][0][0]
self.cube['F'][1][0] = self.cube['U'][1][0]
self.cube['F'][2][0] = self.cube['U'][2][0]
self.cube['U'][0][0] = b_copy[0]
self.cube['U'][1][0] = b_copy[1]
self.cube['U'][2][0] = b_copy[2]
if repeat > 0:
self.lMove(repeat - 1)
def rMove(self, repeat=0):
self.rotateFaceClockwise('R')
u_copy = [self.cube['U'][x][2] for x in range(len(self.cube['U']))]
self.cube['U'][0][2] = self.cube['F'][0][2]
self.cube['U'][1][2] = self.cube['F'][1][2]
self.cube['U'][2][2] = self.cube['F'][2][2]
self.cube['F'][0][2] = self.cube['D'][0][2]
self.cube['F'][1][2] = self.cube['D'][1][2]
self.cube['F'][2][2] = self.cube['D'][2][2]
self.cube['D'][0][2] = self.cube['B'][0][2]
self.cube['D'][1][2] = self.cube['B'][1][2]
self.cube['D'][2][2] = self.cube['B'][2][2]
self.cube['B'][0][2] = u_copy[0]
self.cube['B'][1][2] = u_copy[1]
self.cube['B'][2][2] = u_copy[2]
if repeat > 0:
self.rMove(repeat - 1)
def rotateFaceClockwise(self, face):
cube_size = len(self.cube[face])
copy = [[0 for x in range(cube_size)] for y in range(cube_size)]
###
# 0,0 -> 0,2
# 0,1 -> 1,2
# 0,2 -> 2,2
# 1,0 -> 0,1
# 1,1 -> 1,1
# 1,2 -> 2,1
# 2,0 -> 0,0
# 2,1 -> 1,0
# 2,2 -> 2,0
#
# To generalize:
# 1. Mirror y about vertical axis
# 2. Flip coordinates x and y
###
# Copy current cube face into temp variable with new orientation
for y in range(cube_size):
# Calculate "y-mirror about vertical axis"
cy = (cube_size - 1) - y
for x in range(len(self.cube[face])):
copy[x][cy] = self.cube[face][y][x]
# Copy temp variable back into self.cube
# Do this to prevent self.cube[face] from becoming a
# pointer to "copy" variable
for yidx, row in enumerate(copy):
for xidx, square in enumerate(row):
self.cube[face][yidx][xidx] = square
return True
def isSolved(self):
for face, data in self.cube.items():
cube_size = len(data)
color = data[0][0]
for y in range(cube_size):
for x in range(cube_size):
if color != data[y][x]:
return False
return True
def randMoves(self, iters=20):
faces = list(self.cube)
degrees = ["", "'", "''"]
out = ''
for _ in range(iters):
face = random.choice(faces)
deg = random.choice(degrees)
move = '%s%s' % (face, deg)
out += move
return out
def readMoves(self, moves):
mmap = {'U': self.uMove,
'D': self.dMove,
'L': self.lMove,
'R': self.rMove,
'F': self.fMove,
'B': self.bMove}
i = 0
end = False
while not end:
try:
move = moves[i]
except IndexError as e:
end = False
break
if move not in mmap:
raise Exception('Invalid move')
func = mmap[move]
repeat = 0
try:
if moves[i+1] == "'":
repeat += 1
i += 1
except IndexError as e:
pass
try:
if moves[i+1] == "'":
repeat += 1
i += 1
except IndexError as e:
pass
func(repeat)
i += 1
def loadState(self, filename):
with open(filename, 'r') as f:
data = f.read()
try:
self.cube = json.loads(data)
except Exception as e:
raise e
def saveState(self, filename):
with open(filename, 'w') as f:
out = json.dumps(self.cube, indent=4)
f.write(out)
... ...
import sys
import logging
import argparse
from time import sleep, time
from copy import deepcopy
from rubiks import RubiksCube
def bruteForce(orig_state, max_depth=8, move_list=[]):
###
# This is a VERY exponential brute force. Using GROWTH() in
# Office Calc, we find that it is really only reasonable to
# explore up to max depth of 7 or 8.
#
# TODO: Implement multi-processing
#
# Depth Seconds Hours Days
# 1 7.51E-05 2.08616256713867E-08 8.69234402974447E-10
# 2 0.005095720291138 1.41547785864936E-06 5.897824411039E-08
# 3 0.096453905105591 2.67927514182197E-05 1.11636464242582E-06
# 4 0.846158742904663 0.000235044095251 9.79350396880397E-06
# 5 17.1716315746307 0.00476989765962 0.000198745735817
# 6 291.108149528503 0.080863374869029 0.00336930728621
# 7 6492.00966076708 1.80333601687975 0.075139000703323
# 8 82340.502260004 22.87236173889 0.95301507245375
# 9 1392582.81363459 386.828559342942 16.1178566392892
# 10 26932679.8603269 7481.29996120191 311.720831716746
# 11 442059408.487385 122794.280135385 5116.42833897436
# 12 7370699820.7483 2047416.61687453 85309.0257031054
# 13 119587158616.281 33218655.171189 1384110.63213288
# 14 2153078857082.02 598077460.300561 24919894.17919
#
# CPU Specs:
# Architecture: x86_64
# CPU(s): 4
# On-line CPU(s) list: 0-3
# Thread(s) per core: 2
# Core(s) per socket: 2
# Socket(s): 1
# Vendor ID: GenuineIntel
# CPU family: 6
# Model: 61
# Model name: Intel(R) Core(TM) i5-5300U CPU @ 2.30GHz
# Stepping: 4
# CPU MHz: 2709.210
# CPU max MHz: 2900.0000
# L1d cache: 32K
# L1i cache: 32K
# L2 cache: 256K
# L3 cache: 3072K
###
logging.debug('Depth: %s' % max_depth)
logging.debug('List: %s' % move_list)
moves = {'U': ["U", "U'", "U''"],
'D': ["D", "D'", "D''"],
'L': ["L", "L'", "L''"],
'R': ["R", "R'", "R''"],
'F': ["F", "F'", "F''"],
'B': ["B", "B'", "B''"]}
solutions = []
if max_depth <= 0:
return False
for face, moveset in moves.items():
logging.debug('Face: %s', face)
logging.debug('Set: %s', moveset)
# Don't do the same move twice in a row
if len(move_list) > 0 and move_list[-1] in moveset:
continue
for move in moveset:
logging.debug('Move: %s', move)
o_move_list = move_list[:]
o_move_list.append(move)
rc = RubiksCube()
rc.cube = deepcopy(orig_state)
do_moves = ''.join(o_move_list)
rc.readMoves(do_moves)
logging.debug('Try: %s' % do_moves)
if rc.isSolved():
logging.debug('Solved: %s' % do_moves)
return do_moves
else:
next_depth = max_depth - 1
solved = bruteForce(orig_state, max_depth=next_depth, move_list=o_move_list[:])
if solved:
return solved
return False
if __name__ == '__main__':
p = argparse.ArgumentParser()
p.add_argument("-f", "--state-file",
action="store", dest="statefile", default=None,
help="JSON file containing cube data.", required=True)
p.add_argument("-l", "--log-level",
action="store", dest="loglevel", default='INFO',
help="DEBUG|INFO|WARNING|ERROR|CRITICAL")
p.add_argument("-m", "--max-depth",
action="store", dest="maxdepth", default=12,
help="Max number of moves for solution. 8 might take a day. 9 might take a year.")
opts = p.parse_args()
if not hasattr(logging, opts.loglevel):
print('You have specified and invalid log level: %s' % opts.loglevel)
p.print_help()
sys.exit(1)
else:
log_level = int(getattr(logging, opts.loglevel))
logging.basicConfig(level=log_level, format='%(asctime)s %(message)s')
rc = RubiksCube()
rc.loadState(opts.statefile)
orig_state = deepcopy(rc.cube)
start = time()
logging.info('Solving input file: %s', opts.statefile)
logging.info('Start time: %s', start)
solution = bruteForce(orig_state, max_depth=opts.maxdepth)
end = time()
duration = end - start
if solution:
print('Found solution: %s' % solution)
else:
print('No solution was found for max depth of %s' % max_depth)
logging.info('Process execution took %s seconds', duration)
... ...
import logging
from time import sleep, time
from copy import deepcopy
from rubiks import RubiksCube
def bruteForce(orig_state, max_depth=10, move_list=[]):
logging.debug('Depth: %s' % max_depth)
logging.debug('List: %s' % move_list)
moves = {'U': ["U", "U'", "U''"],
'D': ["D", "D'", "D''"],
'L': ["L", "L'", "L''"],
'R': ["R", "R'", "R''"],
'F': ["F", "F'", "F''"],
'B': ["B", "B'", "B''"]}
solutions = []
if max_depth <= 0:
return []
for face, moveset in moves.items():
logging.debug('Face: %s', face)
logging.debug('Set: %s', moveset)
if len(move_list) > 0 and move_list[-1] in moveset:
continue
for move in moveset:
logging.debug('Move: %s', move)
o_move_list = move_list[:]
o_move_list.append(move)
rc = RubiksCube()
rc.cube = deepcopy(orig_state)
do_moves = ''.join(o_move_list)
rc.readMoves(do_moves)
logging.debug('Try: %s' % do_moves)
if rc.isSolved():
logging.debug('Solved: %s' % do_moves)
solutions.append(do_moves)
else:
next_depth = max_depth - 1
solutions += bruteForce(orig_state, max_depth=next_depth, move_list=o_move_list[:])
return solutions
def get_shorty(solutions):
out = []
shortest_len = None
for sol in solutions:
if shortest_len is None:
shortest_len = len(sol)
if len(sol) < shortest_len:
out = []
shortest_len = len(sol)
if len(sol) == shortest_len:
out.append(sol)
return out
if __name__ == '__main__':
rc = RubiksCube()
rc.loadState('solve_me.json')
orig_state = deepcopy(rc.cube)
for i in range(10):
print('Starting %s iteration test' % i)
start = time()
solutions = bruteForce(orig_state, max_depth=i)
finish = time()
print('Finished %s iterations in %s seconds' % (i, (finish - start)))
... ...