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():
Blue (B) = 1
Red (R) = 2
Green (G) = 3
Orange (0) = 4
Orange (O) = 4
Yellow (Y) = 5
Convention des couleurs des faces :
......@@ -188,16 +188,19 @@ class Cube():
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
:Args:
colors {Boolean} Afficher la chaîne en couleur. Defaut True.
:Returns:
{String} la représentation du cube format one line
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[0] = ''.join(sum(up, []))
......@@ -234,13 +237,15 @@ class Cube():
correct
'''
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:
groupes = [0] * 3
for c in val:
i = codeToGroup(c)
if i == None:
return False
raise ValueError(str(c) + " n'est pas une couleur")
else:
groupes[i] += 1
......@@ -253,7 +258,7 @@ class Cube():
self.cubes[cube] = Array(val)
return True
else:
return False
raise ValueError("Un même groupe de couleur est présent 2 fois")
def rot_L(self):
"""
......@@ -820,6 +825,9 @@ class Cube():
:Args:
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:
{Boolean|None} True si les couleurs sont présentes
......@@ -837,7 +845,6 @@ class Cube():
else:
return None
def scramble(self, str):
'''
scramble
......@@ -892,9 +899,9 @@ class Cube():
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
......@@ -902,7 +909,7 @@ class Cube():
face {Sting} une face du cube
:Example:
c.face_resolu(('U')
c.face_resolue(('U')
:Returns:
{Boolean} True toute la face correspond à sa couleur
......@@ -919,7 +926,6 @@ class Cube():
self.get_facette('FRU',2),
self.get_facette('RBU',2),
self.get_facette('BLU',2),
)
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
......@@ -988,7 +994,7 @@ class Cube():
)
return faceOrange == (4,4,4,4,4,4,4,4) # Test si toute les facettes sont orange
else:
return "Erreur dans les paramètres de la fonction"
raise ValueError(str(face) + "n'est pas une face")
def resolu(self):
"""
......@@ -1052,13 +1058,13 @@ if __name__ == '__main__':
# Exemple d'utilisation du Cube
c = Cube() #par défaut, ce cube est résolu
print(c)
#Test fonction face_resolu
print(c.face_resolu("U"))
print(c.face_resolu("D"))
print(c.face_resolu("B"))
print(c.face_resolu("F"))
print(c.face_resolu("L"))
print(c.face_resolu("R"))
#Test fonction face_resolue
print(c.face_resolue("U"))
print(c.face_resolue("D"))
print(c.face_resolue("B"))
print(c.face_resolue("F"))
print(c.face_resolue("L"))
print(c.face_resolue("R"))
print(c.to_line())
print('Couleur facette BLD/indice 0 : ' + str(c.get_facette('BLD',0))) #test du getter
......
......@@ -5,8 +5,6 @@ Ragnulf
Résolution d'un Rubik's Cube par la méthode CFOP.
Projet d'Algorithmique et Programmation INFO3 Polytech Nantes.
# TL;DR
```bash
python poqb.y --cube OGRBWYBGBGYYOYOWOWGRYOOOBGBRRYRBWWWRBWYGROWGRYBRGYWBOG
......@@ -59,6 +57,19 @@ print(poqb.solve('OGRBWYBGBGYYOYOWOWGRYOOOBGBRRYRBWWWRBWYGROWGRYBRGYWBOG'))
# En savoir plus
[: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 :
- CLOCHARD Guillaume
- 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 utils import colorToCode, colorize
def decomposition_faces(str):
def decomposition_faces(s):
"""
decomposition_faces
Décompose la chaîne représentant les 54 facettes en 6 faces
:Args:
str {String} chaîne de 54 facettes
s {String} chaîne de 54 facettes
:Returns:
{Boolean|String}, {List|None} (error, faces)
Liste de 6 liste de 9 éléments (les 6 faces)
couleurs codées en entiers
{List|None} Liste de 6 liste de 9 éléments (les 6 faces)
couleurs codées en entiers
"""
if not len(str) == 54: #check taille str
return 'La chaîne ne fait pas 54 caractères', None #returns error
if not type(s) == str:
raise TypeError(str(s) + " n'est pas une chaîne")
elif not len(s) == 54: #check taille str
raise ValueError('La chaîne ne fait pas 54 caractères')
else:
faces = [[] for i in range(6)] #init des faces
for i in range(len(str)):
case = colorToCode(str[i])
for i in range(len(s)):
case = colorToCode(s[i])
if i < 9: #face up
faces[0].append(case)
elif i > 44: #face down
......@@ -39,7 +40,7 @@ def decomposition_faces(str):
else: # i_ < 12, face back
faces[4].append(case)
return False, faces
return faces
def check_faces(faces):
'''
......@@ -47,7 +48,6 @@ def check_faces(faces):
Contrôle de la validité des faces passées en paramètes
On vérifie ici :
- on a bien 9 facettes par face
- on a bien une face de chaque couleur (facette du milieu)
- on a bien 9 facettes pour chacune des 6 couleurs
......@@ -67,24 +67,17 @@ def check_faces(faces):
while i < l:
face = faces[i]
if not len(face) == 9: #on vérifie le nombre de facettes dans la face
error = 'Face ' + str(i) + ' n\'a pas 9 facettes'
elif face[4] == None: #on valide la couleur de la face
error = 'Caractères non autorisés'
else:
couleurs_faces[face[4]] = True
for c in face:
couleurs[c] += 1 #on compte le nombre de facettes de la couleur `c`
couleurs_faces[face[4]] = True #on enregistre la présence de cette face
for c in face:
couleurs[c] += 1 #on compte le nombre de facettes de la couleur `c`
i += 1
if not error:
if not sum(couleurs_faces) == 6: #on a pas une couleur différente par face
error = 'Chaque face ne possède pas une couleur différente'
if not sum(couleurs_faces) == 6: #on a pas une couleur différente par face
error = 'Chaque face ne possède pas une couleur différente'
if not couleurs.count(9) == 6: #on a pas 6 couleurs présentes 9 fois, erreur
error = 'Toutes les couleurs ne sont pas présentes 9 fois'
if not couleurs.count(9) == 6: #on a pas 6 couleurs présentes 9 fois, erreur
error = 'Toutes les couleurs ne sont pas présentes 9 fois'
return error
......@@ -110,9 +103,10 @@ def lecture_cube(str_cube):
#1. Découpage des faces de la chaîne en entrée
c = Cube()
error, faces = decomposition_faces(str_cube) #on découpe en faces
if error:
return error, None
try:
faces = decomposition_faces(str_cube) #on découpe en faces