Commit ff720219 authored by Tom MARRUCCI's avatar Tom MARRUCCI
Browse files

Merge branch 'master' into stats_courbe

Conflicts:
	algo.py
	stats.py
parents 4e8b0b2b cdd1eeae
[submodule "tests/samples/cubejs"]
path = tests/samples/cubejs
url = git@github.com:akheron/cubejs.git
...@@ -100,7 +100,7 @@ class Cube(): ...@@ -100,7 +100,7 @@ class Cube():
Blue (B) = 1 Blue (B) = 1
Red (R) = 2 Red (R) = 2
Green (G) = 3 Green (G) = 3
Orange (0) = 4 Orange (O) = 4
Yellow (Y) = 5 Yellow (Y) = 5
Convention des couleurs des faces : Convention des couleurs des faces :
...@@ -188,16 +188,19 @@ class Cube(): ...@@ -188,16 +188,19 @@ class Cube():
return '\n'.join(''.join(l) for l in result) #on convertit la liste en chaîne return '\n'.join(''.join(l) for l in result) #on convertit la liste en chaîne
def to_line(self): def to_line(self, colors=True):
""" """
to_line to_line
:Args:
colors {Boolean} Afficher la chaîne en couleur. Defaut True.
:Returns: :Returns:
{String} la représentation du cube format one line {String} la représentation du cube format one line
ex: OGRBWYBGBGYYOYOWOWGRYOOOBGBRRYRBWWWRBWYGROWGRYBRGYWBOG ex: OGRBWYBGBGYYOYOWOWGRYOOOBGBRRYRBWWWRBWYGROWGRYBRGYWBOG
""" """
up, left, front, right, back, down = build_faces(self, colors=True) up, left, front, right, back, down = build_faces(self, colors=colors)
lines = [[]]*5 lines = [[]]*5
lines[0] = ''.join(sum(up, [])) lines[0] = ''.join(sum(up, []))
...@@ -234,13 +237,15 @@ class Cube(): ...@@ -234,13 +237,15 @@ class Cube():
correct correct
''' '''
if not cube in PETITS_CUBES: if not cube in PETITS_CUBES:
return False raise ValueError(str(cube) + " n'est pas un petit cube")
elif not len(cube) == len(val):
raise ValueError("La taille du cube ne correspond pas")
else: else:
groupes = [0] * 3 groupes = [0] * 3
for c in val: for c in val:
i = codeToGroup(c) i = codeToGroup(c)
if i == None: if i == None:
return False raise ValueError(str(c) + " n'est pas une couleur")
else: else:
groupes[i] += 1 groupes[i] += 1
...@@ -253,7 +258,7 @@ class Cube(): ...@@ -253,7 +258,7 @@ class Cube():
self.cubes[cube] = Array(val) self.cubes[cube] = Array(val)
return True return True
else: else:
return False raise ValueError("Un même groupe de couleur est présent 2 fois")
def rot_L(self): def rot_L(self):
""" """
...@@ -820,6 +825,9 @@ class Cube(): ...@@ -820,6 +825,9 @@ class Cube():
:Args: :Args:
petit_cube {String} Le petit cube qu'il faut regarder petit_cube {String} Le petit cube qu'il faut regarder
c1 {int}
c2 {int}
c3 {int} Defaut à None pour le cas d'un cube arrête
:Returns: :Returns:
{Boolean|None} True si les couleurs sont présentes {Boolean|None} True si les couleurs sont présentes
...@@ -837,7 +845,6 @@ class Cube(): ...@@ -837,7 +845,6 @@ class Cube():
else: else:
return None return None
def scramble(self, str): def scramble(self, str):
''' '''
scramble scramble
...@@ -892,9 +899,9 @@ class Cube(): ...@@ -892,9 +899,9 @@ class Cube():
return True return True
def face_resolu(self,face): def face_resolue(self, face):
""" """
face_resolu face_resolue
Fonction qui dit si une face du cube (passé en paramètre) est résolu ou non Fonction qui dit si une face du cube (passé en paramètre) est résolu ou non
...@@ -902,7 +909,7 @@ class Cube(): ...@@ -902,7 +909,7 @@ class Cube():
face {Sting} une face du cube face {Sting} une face du cube
:Example: :Example:
c.face_resolu(('U') c.face_resolue(('U')
:Returns: :Returns:
{Boolean} True toute la face correspond à sa couleur {Boolean} True toute la face correspond à sa couleur
...@@ -919,7 +926,6 @@ class Cube(): ...@@ -919,7 +926,6 @@ class Cube():
self.get_facette('FRU',2), self.get_facette('FRU',2),
self.get_facette('RBU',2), self.get_facette('RBU',2),
self.get_facette('BLU',2), self.get_facette('BLU',2),
) )
return faceJaune == (5,5,5,5,5,5,5,5) # Test si toute les facettes sont jaune return faceJaune == (5,5,5,5,5,5,5,5) # Test si toute les facettes sont jaune
elif face == 'D': # Si la face Down du cube elif face == 'D': # Si la face Down du cube
...@@ -988,7 +994,7 @@ class Cube(): ...@@ -988,7 +994,7 @@ class Cube():
) )
return faceOrange == (4,4,4,4,4,4,4,4) # Test si toute les facettes sont orange return faceOrange == (4,4,4,4,4,4,4,4) # Test si toute les facettes sont orange
else: else:
return "Erreur dans les paramètres de la fonction" raise ValueError(str(face) + "n'est pas une face")
def resolu(self): def resolu(self):
""" """
...@@ -1052,13 +1058,13 @@ if __name__ == '__main__': ...@@ -1052,13 +1058,13 @@ if __name__ == '__main__':
# Exemple d'utilisation du Cube # Exemple d'utilisation du Cube
c = Cube() #par défaut, ce cube est résolu c = Cube() #par défaut, ce cube est résolu
print(c) print(c)
#Test fonction face_resolu #Test fonction face_resolue
print(c.face_resolu("U")) print(c.face_resolue("U"))
print(c.face_resolu("D")) print(c.face_resolue("D"))
print(c.face_resolu("B")) print(c.face_resolue("B"))
print(c.face_resolu("F")) print(c.face_resolue("F"))
print(c.face_resolu("L")) print(c.face_resolue("L"))
print(c.face_resolu("R")) print(c.face_resolue("R"))
print(c.to_line()) print(c.to_line())
print('Couleur facette BLD/indice 0 : ' + str(c.get_facette('BLD',0))) #test du getter print('Couleur facette BLD/indice 0 : ' + str(c.get_facette('BLD',0))) #test du getter
......
...@@ -5,8 +5,6 @@ Ragnulf ...@@ -5,8 +5,6 @@ Ragnulf
Résolution d'un Rubik's Cube par la méthode CFOP. Résolution d'un Rubik's Cube par la méthode CFOP.
Projet d'Algorithmique et Programmation INFO3 Polytech Nantes.
# TL;DR # TL;DR
```bash ```bash
python poqb.y --cube OGRBWYBGBGYYOYOWOWGRYOOOBGBRRYRBWWWRBWYGROWGRYBRGYWBOG python poqb.y --cube OGRBWYBGBGYYOYOWOWGRYOOOBGBRRYRBWWWRBWYGROWGRYBRGYWBOG
...@@ -59,6 +57,19 @@ print(poqb.solve('OGRBWYBGBGYYOYOWOWGRYOOOBGBRRYRBWWWRBWYGROWGRYBRGYWBOG')) ...@@ -59,6 +57,19 @@ print(poqb.solve('OGRBWYBGBGYYOYOWOWGRYOOOBGBRRYRBWWWRBWYGROWGRYBRGYWBOG'))
# En savoir plus # En savoir plus
[:link: Wiki Gitlab](https://gitlab.univ-nantes.fr/E132397K/Ragnulf/wikis/home) [:link: Wiki Gitlab](https://gitlab.univ-nantes.fr/E132397K/Ragnulf/wikis/home)
# Tests
Pour lancer les tests unitaires :
```python
python -m unittest discover -v
```
Ou, avec `green` (`pip3 install green`), pour avoir un peu de couleurs :
```python
green -vvv
#ou
green -vvv -r #avec coverage d'installé sur la machine
```
# Membres : # Membres :
- CLOCHARD Guillaume - CLOCHARD Guillaume
- LANGELIER Arnaud - LANGELIER Arnaud
......
This diff is collapsed.
import matplotlib.pyplot as plt
from Cube import *
from algo import algo_cfop
import json
from lire_entree import lecture_cube
from stats import moyenne
JEU_TEST = 'tests/samples/sample-600.json'
with open(JEU_TEST) as data_file: #on parse le jeu de test JSON
data = json.load(data_file)
tests = data["cubes"]
listeNbMouvements = {} # dictionnaire pour stocké en clé : nbMouvement et en valeur : Occurence sur les n tests
for test in tests: # on parcours tout les cubes
b,c = lecture_cube(test)
c,mouv = algo_cfop(c) # on fais l'algo
if len(mouv) in listeNbMouvements: # si le nombre de mouvements est deja dans la dictionnaire, on ajoute 1 à son occurence
listeNbMouvements[len(mouv)] += 1
else:
listeNbMouvements[len(mouv)] = 1 # sinon on l'ajoute
listeX = []
listeY = []
for x,y in listeNbMouvements.items(): # on "separe" le dictionnaire en deux liste correspondant aux clés et aux valeurs
listeX.append(x)
listeY.append(y)
plt.plot(listeX,listeY) # affichage de la courbe dans pyplot
plt.xlabel('Nombre de mouvements')
plt.ylabel('Occurence sur '+str(len(tests))+' test')
plt.show()
Construction d'une base d'heuristiques
--------------------------------------
# Fonctionnement
Le script `heuristic_builder.py` est un générateur de raccourcis.
Il construit un fichier `json` de la forme :
```json
{
<suite de mouvements> : <suite de mouvements équivalente mais plus courte>,
...
}
```
Il parcourt toutes les combinaisons de mouvements possibles (jusqu'à une
certaine limite) et déduit, pour chaque combinaison, quelle est la
suite `A` de mouvements la plus courte qui y amène.
Ainsi, pour toutes les autres suites `Bi` de mouvements qui amènent au même
état du cube on retient que `Bi` peuvent être remplacée par `A`.
Lancer le script (prévoir du café pour patienter) :coffee: :
```bash
python heuristic_builder.py --max 5 --output-file shortcuts-5.json
```
- `--max` : la taille max de la suite de mouvements. Défaut : `3`
- `--output-file` : le fichier de sortie. Défaut : `shortcuts.json`
| max | temps d'éxécution¹ | nombre de combinaisons | nombre de raccourcis |
| :-: | :---------------: | :--------------------: | :------------------: |
| 1 | instantané | 18 | 0 |
| 2 | instantané | 342 | 54 |
| 3 | ~10sec | 6174 | 1998 |
| 4 | ~5min | 11150 | 50598 |
| 5 | ~30min | 2000718 | 1124358 |
| 130 | ? | ~10^164 | ? |
*¹ Mac-mini i5, 8Go RAM*
Le script utilise le module `multiprocessing` de Python pour répartir le travail
sur les CPUs, tout en ayant des données partagées (dictionnaire des états,
dictionnaire des raccourcis, compteur, etc.).
# Quelques stats
On voit que ce n'est pas pertinent de chercher à remplacer des raccourcis longs.
Il y en a beaucoup et donc le temp de recherche/remplacement devient vite
important.
| Taille max shortcuts | Gain moyen solution | Temps d’éxécution de algo.py |
| :------------------: | :-----------------: | :--------------------------: |
| 1 | 0 | 0.6s |
| 2 | 6 | 0.6s |
| 3 | 7 | 6s |
| 4 | 8 | 38s |
| 5 | 8 | 24min |
from threading import Thread
from datetime import datetime, timedelta
import multiprocessing as mp
import sys
import getopt
import time
import json
import os
#Comme Cube utilise utils.py, qui utilise aussi getopt, mais avec d'autres
#options, on lit les arguments ici et on les efface
optlist, _ = getopt.getopt(sys.argv[1:], [], [
'output-file=',
'max=',
'ratio='
])
sys.argv = [sys.argv[0]]
#fix pour aller chercher un module dans le dossier parent
#@see http://stackoverflow.com/a/279338/2058840
sys.path.append(os.path.dirname(__file__) + "../")
from Cube import Cube
MAX_LENGTH = 3 #taille max de la chaîne de mouvements par défaut
REFRESH_TIME = 1 #1sec refresh affichage procession
RATIO = 2 #ratio entre longueur suite de mouvements et longueur raccourci
MOUVEMENTS = [
"U", "Ui", "U2", "L", "Li", "L2",
"F", "Fi", "F2", "R", "Ri", "R2",
"B", "Bi", "B2", "D", "Di", "D2"
]
SHORTCUTS_FILE = 'shortcuts.json'
def readArgs():
"""
readArgs
Lecture des arguments passés au script, version avancée.
En particulier, on veut lire --max=<taille max movements>
:Returns:
{Dict}
"""
return {k: v for k, v in optlist}
def watchProgress(count, max):
"""
watchProgress
Affiche la progression du process
ie. count/max
:Example:
[=== ] 100 / 10000 combinaisons
:Args:
count {multiprocessing.Value}
max {Number}
"""
INTERVAL = 30
def printLine(p, a, b, start, end):
#temps d'exécution
t = (datetime.now().replace(microsecond=0) - start).total_seconds()
#vitesse d'éxécution
v = a / t if t > 0 else None
#temps restant
restant = str(timedelta(seconds=int((b-a) / v))) if v else '?'
sys.stdout.write(
#représentation avancement
"[" + "=" * p + " " * (30-p) + "] "
#nombre done / nombre total
+ str(a) + "/" + str(b) + " combinaisons - "
#évaluation temps restant
+ ' Remaining : ' + (restant) + 's'
+ (" \r" if not end else " \n")
)
sys.stdout.flush()
start = datetime.now().replace(microsecond=0) #date de début
while count.value < max:
p = int(counter.value / max * INTERVAL) #rapport d'avancement sur INTERVAL
printLine(p, count.value, max, start, False) #afficher les infos
time.sleep(REFRESH_TIME) #on attend
#fin du travail, on affiche une dernière ligne
printLine(INTERVAL, max, max, start, True)
print('Done in', str(datetime.now().replace(microsecond=0) - start) + 's')
time.sleep(0.1)
def makeMove(queue, lock, counter, states, shortcuts, ration, maximum):
"""
makeMove
Un worker. Consomme les tâches de `queue` et enregistre le résultat
dans `states et `shortcuts`.
`queue` représente une liste de rotation à effectuer sur le cube.
Une action est de la forme (cube, history, longueur, mvt) où `mvt` est
la rotation à effectuer sur `cube`.
`historique` représente l'historique des mouvements faits sur ce cube, et
`longueur` le nombre de rotation de l'historique.
:Args:
queue {multiprocessing.manager.Queue}
lock {multiprocessing.Lock}
counter {multiprocessing.Value}
states {multiprocessing.manager.Dict}
shortcuts {multiprocessing.manager.Dict}
ratio {float} Rapport minimal entre la
taille de suite de mouvements
et le raccourci
maximum {int} Taille max de la liste de
mouvements
"""
while not queue.empty():
#on récupère ce qu'on doit faire
cube, history, longueur, mvt = queue.get()
#on applique la rotation
method = getattr(cube, 'rot_' + mvt)
method()
#l'état obtenu
state = cube.to_line(colors=False)
h = history + [mvt]
if state in states: #si on a déjà rencontré l'état
#on regarde quelle suite de mouvements amène à cet état
mouvements, l = states[state]
if longueur + 1 < l / ratio: #si notre solution actuelle est meilleure
#on retient cette suite de mouvements pour arriver à cet état
states[state] = h, longueur + 1
#on ajoute un shortcut pour utiliser notre version plutôt que mouvements
shortcuts[' '.join(mouvements)] = h
elif longueur + 1 > l * ratio: #si la solution historique est meilleure
#on ajoute un shortcut pour utiliser mouvements plutôt que notre version
shortcuts[' '.join(history) + ' ' + mvt] = mouvements
#sinon, on ne fait rien, car ne sert à presque rien de remplacer quoi que ce soit
else: #sinon, nouvel état
states[state] = h, longueur + 1
lock.acquire()
counter.value += 1
lock.release()
queue.task_done() #tâche effectuée
#si on n'atteint pas la limite de taille des mouvements à recherché,
#on relance un niveau supplémentaire
if longueur < maximum - 1:
for m in MOUVEMENTS:
#on ajoute à la queue
queue.put((cube, h, longueur + 1, m))
return
def saveResultShortcuts(shortcuts, file):
"""
saveResultShortcuts
:Args:
shortcuts {Dict}
file {String}
"""
with open(file, 'w') as outfile:
json.dump(shortcuts, outfile)
def calcNbCombinaisons(q, max):
"""
calcNbCombinaisons
Calcule le nombre de combinaisions que l'on va traiter en fonction
de la taille max de la liste de mouvements.
C'est une somme de d'une suite géométrique:
q + q^2 + ... q^MAX_LENGTH
où q = 18 (le nombre de mouvements)
:Args:
q {int}
max {int}
"""
return int((1 - q**(max+1)) / (1 - q) - 1)
if __name__ == '__main__':
args = readArgs()
maximum = int(args['--max']) if '--max' in args else MAX_LENGTH
shortcutsFile = args['--output-file'] if '--output-file' in args else SHORTCUTS_FILE
ratio = float(args['--ratio']) if '--ratio' in args else RATIO
cube = Cube() #un cube résolu
with mp.Manager() as manager:
"""
states
Stocke la liste des état, et le plus court jeu de mouvements
qui mènent à l'état
{
<state> : <list de mouvements>,
YYYY......WWWW: ('U R ... ', <longueur mouvement>)
....
}
"""
states = manager.dict()
#on remplit l'état inital
states[cube.to_line(colors=False)] = ([], 0)
"""
shortcuts
{
<suite de mouvements> : <suite mouvements plus courte>
"U Ui" : '',
...
}
"""
shortcuts = manager.dict()
counter = mp.Value('i', 0) #nombre de combinaisons traitées
queue = manager.Queue()
lock = manager.Lock()
#on commence à remplir queue avec les 18 premiers mouvements
for m in MOUVEMENTS:
queue.put((cube, [], 0, m))
#on lance un watcher de progression
watcher = Thread(
target=watchProgress,
args=(counter, calcNbCombinaisons(len(MOUVEMENTS), maximum)),
daemon=True
)
watcher.start()
#on crée un process par CPU pour distribuer autant que possible le
#travail
cpus = mp.cpu_count()
processes = [
mp.Process(
target=makeMove,
args=(queue, lock, counter, states, shortcuts, ratio, maximum),
daemon=True
) for i in range(cpus - 1 if cpus > 1 else 1)
]
for proc in processes:
proc.start()
#on attend que la queue soit vide
queue.join()
#on attend watche aussi histoire de ne pas avoir de pb d'affichage
watcher.join()
for proc in processes:
proc.terminate()
# listStates = sorted(states.items(), key=lambda l: l[1][1])
saveResultShortcuts(dict(shortcuts), shortcutsFile)
print('Output saved in', shortcutsFile)
from Cube import Cube from Cube import Cube
from utils import colorToCode, colorize from utils import colorToCode, colorize
def decomposition_faces(str): def decomposition_faces(s):
""" """
decomposition_faces decomposition_faces
Décompose la chaîne représentant les 54 facettes en 6 faces Décompose la chaîne représentant les 54 facettes en 6 faces
:Args: :Args:
str {String} chaîne de 54 facettes s {String} chaîne de 54 facettes