Résolution de mots croisés japonais avec P̶y̶t̶h̶o̶̶n̶ Rust et WebAssembly

Logo rouille comme nonogramme


Comment faire un solveur nonogram pour Python, réécrivez-le dans Rust pour l'exécuter directement dans le navigateur via WebAssembly.


TL; DR


Commencer


À propos des mots croisés japonais (nonogrammes) sur le Habré, il y avait déjà plusieurs messages. Exemple
et un de plus .


Les images sont cryptées avec des numéros situés à gauche des lignes, ainsi qu'au-dessus des colonnes. Le nombre de nombres indique le nombre de groupes de cellules noires (ou leur couleur, pour les mots croisés de couleur) dans la ligne ou la colonne correspondante, et les nombres eux-mêmes - combien de cellules fusionnées chacun de ces groupes contient (par exemple, un ensemble de trois nombres - 4, 1 et 3 signifie que dans cette ligne, il y a trois groupes: le premier - à partir de quatre, le second - à partir d'un, le troisième - à partir de trois cellules noires). Dans un puzzle de mots croisés en noir et blanc, les groupes doivent être séparés par au moins une cellule vide, en couleur, cette règle ne s'applique qu'aux groupes monochromes, et les groupes multicolores peuvent être étroitement espacés (les cellules vides peuvent également se trouver le long des bords des lignes). Il est nécessaire de déterminer l'emplacement des groupes de cellules.

L'un des points de vue les plus généralement acceptés est que les mots croisés «corrects» ne peuvent être appelés que ceux qui sont résolus de manière «logique». Ceci est généralement appelé la méthode de solution, dans laquelle les dépendances entre différentes lignes et / ou colonnes ne sont pas prises en compte. En d'autres termes, une solution est une séquence de décisions indépendantes de lignes ou de colonnes individuelles jusqu'à ce que toutes les cellules soient remplies (en savoir plus sur l'algorithme ci-dessous). Par exemple, seuls de tels nonogrammes peuvent être trouvés sur le site Web http://nonograms.org/ ( http://nonograms.ru/ ). Les nonogrammes de ce site ont déjà été cités en exemple dans l'article Résoudre les mots croisés de couleur japonaise à la vitesse de la lumière . À des fins de comparaison et de vérification, mon solveur a également ajouté un support pour le téléchargement et l'analyse des mots croisés à partir de ce site (merci à KyberPrizrak pour la permission d'utiliser les matériaux de son site).


Cependant, le concept de nonogrammes peut être étendu à un problème plus général, lorsque la méthode "logique" habituelle conduit à une impasse. Dans de tels cas, il faut faire une hypothèse sur la couleur d'une cellule et, après avoir prouvé que cette couleur conduit à une contradiction, marquer la couleur opposée pour cette cellule. La séquence de ces étapes peut (si vous avez la patience) nous donner toutes les solutions. Cet article portera principalement sur la résolution d'un cas plus général de mots croisés.


Python


Il y a environ un an et demi, je suis tombé accidentellement sur un article qui décrivait une méthode pour résoudre une seule ligne (comme il s'est avéré plus tard, la méthode était plutôt lente).


Lorsque j'ai implémenté cette méthode en Python (mon principal langage de travail) et ajouté une mise à jour séquentielle de toutes les lignes, j'ai vu que tout cela n'était pas résolu très rapidement. Après avoir étudié le matériel, il s'est avéré que sur ce sujet, il existe de nombreux travaux et implémentations qui proposent différentes approches pour cette tâche.


Il m'a semblé que le travail le plus ambitieux sur l'analyse de diverses implémentations de solveurs a été effectué par Jan Wolter, publiant sur son site (qui, à ma connaissance, reste le plus grand référentiel public de nonogrammes sur Internet), une étude détaillée contenant une multitude d'informations et de liens qui peuvent aider à créer votre propre solveur.


En étudiant de nombreuses sources (elles seront à la fin de l'article), j'ai progressivement amélioré la vitesse et la fonctionnalité de mon solveur. En conséquence, j'étais accro et j'étais engagé dans la mise en œuvre, la refactorisation, le débogage d'algorithmes pendant environ 10 mois dans un temps libre du travail.


Algorithmes de base


Le solveur résultant peut être représenté sous la forme de quatre niveaux de décision:


  • ( ligne ) solveur linéaire: en entrée, une ligne de cellules et une ligne de description (indices), en sortie, une ligne partiellement résolue. Dans la solution python, j'ai implémenté 4 algorithmes différents (3 d'entre eux sont adaptés aux mots croisés couleur). Le plus rapide s'est avéré être l'algorithme BguSolver, nommé d'après la source d'origine . Il s'agit d'une méthode très efficace et pratiquement standard pour résoudre des chaînes de nonogrammes en utilisant la programmation dynamique. Le pseudocode de cette méthode se trouve, par exemple, dans cet article .


  • ( propagation ) nous mettons toutes les lignes et colonnes dans une file d'attente, nous les parcourons avec un solveur linéaire, lorsque nous recevons de nouvelles informations lors de la résolution d'une ligne (colonne), nous mettons à jour la file d'attente, respectivement, avec de nouvelles colonnes (lignes). Continuez jusqu'à ce que la ligne soit vide.


    Exemple et code

    Nous prenons la tâche suivante à résoudre à partir de la file d'attente. Soit une chaîne vide (non résolue) de longueur 7 (nous la désignons comme ??????? ) avec une description des blocs [2, 3] . Le solveur linéaire produira une chaîne partiellement résolue ?X??XX?X est la cellule remplie. Lors de la mise à jour de la ligne, nous voyons que les colonnes avec les numéros 1, 4, 5 ont changé (l'indexation commence à 0). Cela signifie que de nouvelles informations sont apparues dans les colonnes indiquées et peuvent être renvoyées au solveur «linéaire». Nous mettons ces colonnes dans la file d'attente des tâches avec une priorité plus élevée (afin de les donner ensuite au solveur linéaire).


     def propagation(board): line_jobs = PriorityDict() for row_index in range(board.height): new_job = (False, row_index) line_jobs[new_job] = 0 for column_index in range(board.width): new_job = (True, column_index) line_jobs[new_job] = 0 for (is_column, index), priority in line_jobs.sorted_iter(): new_jobs = solve_and_update(board, index, is_column) for new_job in new_jobs: # upgrade priority new_priority = priority - 1 line_jobs[new_job] = new_priority def solve_and_update(board, index, is_column): if is_column: row_desc = board.columns_descriptions[index] row = tuple(board.get_column(index)) else: row_desc = board.rows_descriptions[index] row = tuple(board.get_row(index)) updated = line_solver(row_desc, row) if row != updated: for i, (pre, post) in enumerate(zip(row, updated)): if _is_pixel_updated(pre, post): yield (not is_column, i) if is_column: board.set_column(index, updated) else: board.set_row(index, updated) 



  • ( sondage ) pour chaque cellule non résolue, nous trions toutes les options de couleur et essayons la propagation avec ces nouvelles informations. Si nous obtenons une contradiction, nous jetons cette couleur hors des options de couleur pour la cellule et essayons d'en profiter à nouveau en utilisant la propagation. S'il est résolu jusqu'au bout, nous ajoutons la solution à la liste des solutions, mais continuons d'expérimenter avec d'autres couleurs (il peut y avoir plusieurs solutions). Si nous arrivons à une situation où il est impossible de résoudre davantage, nous ignorons simplement et répétons la procédure avec une couleur / cellule différente.


    Code

    Renvoie True si une contradiction est reçue à la suite de l'échantillon.


     def probe(self, cell_state): board = self.board pos, assumption = cell_state.position, cell_state.color # already solved if board.is_cell_solved(pos): return False if assumption not in board.cell_colors(pos): LOG.warning("The probe is useless: color '%s' already unset", assumption) return False save = board.make_snapshot() try: board.set_color(cell_state) propagation( board, row_indexes=(cell_state.row_index,), column_indexes=(cell_state.column_index,)) except NonogramError: LOG.debug('Contradiction', exc_info=True) # rollback solved cells board.restore(save) else: if board.is_solved_full: self._add_solution() board.restore(save) return False LOG.info('Found contradiction at (%i, %i)', *pos) try: board.unset_color(cell_state) except ValueError as ex: raise NonogramError(str(ex)) propagation( board, row_indexes=(pos.row_index,), column_indexes=(pos.column_index,)) return True 



  • ( retour arrière ) si pendant le sondage vous n'ignorez pas un puzzle partiellement résolu, mais continuez à invoquer récursivement la même procédure, nous obtenons un retour arrière (en d'autres termes, une marche complète dans la profondeur de l'arbre de décision potentiel). Ici, un grand rôle commence à jouer, laquelle des cellules sera choisie comme la prochaine extension de la solution potentielle. De bonnes recherches sur ce sujet figurent dans cette publication .


    Code

    Le retour arrière est assez compliqué avec moi, mais ces deux fonctions décrivent approximativement ce qui se passe lors d'une recherche récursive


     def search(self, search_directions, path=()): """ Return False if the given path is a dead end (no solutions can be found) """ board = self.board depth = len(path) save = board.make_snapshot() try: while search_directions: state = search_directions.popleft() assumption, pos = state.color, state.position cell_colors = board.cell_colors(pos) if assumption not in cell_colors: LOG.warning("The assumption '%s' is already expired. " "Possible colors for %s are %s", assumption, pos, cell_colors) continue if len(cell_colors) == 1: LOG.warning('Only one color for cell %r left: %s. Solve it unconditionally', pos, assumption) try: self._solve_without_search() except NonogramError: LOG.warning( "The last possible color '%s' for the cell '%s' " "lead to the contradiction. " "The path %s is invalid", assumption, pos, path) return False if board.is_solved_full: self._add_solution() LOG.warning( "The only color '%s' for the cell '%s' lead to full solution. " "No need to traverse the path %s anymore", assumption, pos, path) return True continue rate = board.solution_rate guess_save = board.make_snapshot() try: LOG.warning('Trying state: %s (depth=%d, rate=%.4f, previous=%s)', state, depth, rate, path) success = self._try_state(state, path) finally: board.restore(guess_save) if not success: try: LOG.warning( "Unset the color %s for cell '%s'. Solve it unconditionally", assumption, pos) board.unset_color(state) self._solve_without_search() except ValueError: LOG.warning( "The last possible color '%s' for the cell '%s' " "lead to the contradiction. " "The whole branch (depth=%d) is invalid. ", assumption, pos, depth) return False if board.is_solved_full: self._add_solution() LOG.warning( "The negation of color '%s' for the cell '%s' lead to full solution. " "No need to traverse the path %s anymore", assumption, pos, path) return True finally: # do not restore the solved cells on a root path - they are really solved! if path: board.restore(save) return True def _try_state(self, state, path): board = self.board full_path = path + (state,) probe_jobs = self._get_all_unsolved_jobs(board) try: # update with more prioritized cells for new_job, priority in self._set_guess(state): probe_jobs[new_job] = priority __, best_candidates = self._solve_jobs(probe_jobs) except NonogramError as ex: LOG.warning('Dead end found (%s): %s', full_path[-1], str(ex)) return False rate = board.solution_rate LOG.info('Reached rate %.4f on %s path', rate, full_path) if rate == 1: return True cells_left = round((1 - rate) * board.width * board.height) LOG.info('Unsolved cells left: %d', cells_left) if best_candidates: return self.search(best_candidates, path=full_path) return True 



Ainsi, nous commençons à résoudre notre puzzle de mots croisés à partir du deuxième niveau (le premier ne convient que pour le cas dégénéré, alors que dans le puzzle de mots croisés entier, il n'y a qu'une seule ligne ou colonne) et montons progressivement les niveaux. Comme vous pouvez le deviner, chaque niveau provoque le niveau sous-jacent plusieurs fois, donc pour une solution efficace, il est impératif d'avoir des premier et deuxième niveaux rapides, qui peuvent être appelés des millions de fois pour des puzzles complexes.


À ce stade, il s'avère (plutôt attendu) que python n'est pas du tout l'outil qui convient aux performances maximales dans une telle tâche gourmande en CPU: tous les calculs y sont extrêmement inefficaces par rapport aux langages de bas niveau. Par exemple, le solveur BGU le plus proche algorithmiquement (en Java), selon les résultats des mesures, s'est avéré 7 à 17 (parfois jusqu'à 27) fois plus rapide sur une variété de tâches.


Plus de détails
         pynogram_my BGU_my speedup
 Danseur 0,976 0,141 6,921986      
 Cat 1.064 0.110 9.672727      
 Skid 1.084 0.101 10.732673     
 Bucks 1.116 0.118 9.457627      
 Bord 1,208 0,094 12,851064     
 Fumée 1,464 0,120 12.200000     
 Nœud 1,332 0,140 9,514286      
 Balançoire 1,784 0,138 12,927536     
 Maman 2.108 0.147 14.340136     
 DiCap 2.076 0.176 11.795455     
 Tragic 2,368 0,265 8,935849      
 Merka 2.084 0.196 10.632653     
 Petro 2,948 0,219 13,461187     
 M&M 3,588 0,375 9,568000      
 Signé 4.068 0.242 16.809917     
 Léger 3,848 0,488 7,885246      
 Forever 111.000 13.570 8.179808  
 Centre 5,700 0,327 17,431193     
 Chaud 3.150 0.278 11.330935     
 Karaté 2.500 0.219 11.415525     
 9-Dom 510.000 70.416 7.242672      
 Drapeau 149.000 5.628 26.474769     
 Lion 71,000 2,895 24,525043     
 Marley 12.108 4.405 2.748695      
 Chose 321.000 46.166 6.953169      
 Nature inf 433.138 inf     
 Sierp inf inf NaN      
 Gettys inf inf NaN      

Les mesures ont été effectuées sur ma voiture, les puzzles sont tirés de l'ensemble standard que Jan Wolter a utilisé dans sa comparaison


Et c'est déjà après avoir commencé à utiliser PyPy, et sur CPython standard, le temps de calcul était plus long que sur PyPy 4-5 fois! Nous pouvons dire que les performances d'un solveur Java similaire étaient 28 à 85 fois supérieures à celles du code CPython.


Les tentatives pour améliorer les performances de mon solveur à l'aide du profilage (cProfile, SnakeViz, line_profiler) ont conduit à une certaine accélération, mais bien sûr, elles n'ont pas donné un résultat étrange.


Résumé :


+ le solveur peut résoudre tous les puzzles des sites https://webpbn.com , http://nonograms.org et son propre format (basé sur l'ini)


+ résout les nonogrammes en noir et blanc et en couleur avec n'importe quel nombre de couleurs (le nombre maximum de couleurs rencontrées est de 10)


+ résout des puzzles avec des tailles de blocs manquantes (blott). Un exemple d'un tel puzzle .


+ peut restituer des puzzles à la console / aux curses / fenêtre du navigateur (lors de l'installation de l'option pynogram-web supplémentaire). Pour tous les modes, la visualisation de la progression de la solution en temps réel est prise en charge.


- calculs lents (par rapport aux implémentations décrites dans l'article-comparaison des solveurs, voir tableau ).


- retour arrière inefficace: certains casse-tête peuvent être résolus pendant des heures (lorsque l'arbre de décision est très grand).


Rouille


Au début de l'année, j'ai commencé à étudier Rust. J'ai commencé, comme d'habitude, avec The Book , j'ai découvert WASM, j'ai suivi le tutoriel proposé . Cependant, je voulais une tâche réelle dans laquelle vous pouvez vous impliquer dans les points forts du langage (principalement sa super-performance), et non des exemples inventés par quelqu'un. Je suis donc retourné aux nonogrammes. Mais maintenant, j'avais déjà une version de travail de tous les algorithmes en Python, il ne restait plus qu'à "réécrire".


La bonne nouvelle m'attendait depuis le tout début: il s'est avéré que Rust, avec son système de type, décrit parfaitement les structures de données pour ma tâche. Par exemple, l'une des correspondances de base BinaryColor + BinaryBlock / MultiColor + ColoredBlock vous permet de séparer de façon permanente les nonogrammes noir et blanc et couleur. Si, quelque part dans le code, nous essayons de résoudre une chaîne colorée à l'aide de blocs de description binaires ordinaires, nous obtenons une erreur de compilation sur la non-concordance de type.


Les types de base ressemblent à ceci
 pub trait Color { fn blank() -> Self; fn is_solved(&self) -> bool; fn solution_rate(&self) -> f64; fn is_updated_with(&self, new: &Self) -> Result<bool, String>; fn variants(&self) -> Vec<Self>; fn as_color_id(&self) -> Option<ColorId>; fn from_color_ids(ids: &[ColorId]) -> Self; } pub trait Block { type Color: Color; fn from_str_and_color(s: &str, color: Option<ColorId>) -> Self { let size = s.parse::<usize>().expect("Non-integer block size given"); Self::from_size_and_color(size, color) } fn from_size_and_color(size: usize, color: Option<ColorId>) -> Self; fn size(&self) -> usize; fn color(&self) -> Self::Color; } #[derive(Debug, PartialEq, Eq, Hash, Clone)] pub struct Description<T: Block> where T: Block, { pub vec: Vec<T>, } // for black-and-white puzzles #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, PartialOrd)] pub enum BinaryColor { Undefined, White, Black, BlackOrWhite, } impl Color for BinaryColor { // omitted } #[derive(Debug, PartialEq, Eq, Hash, Default, Clone)] pub struct BinaryBlock(pub usize); impl Block for BinaryBlock { type Color = BinaryColor; // omitted } // for multicolor puzzles #[derive(Debug, PartialEq, Eq, Hash, Default, Copy, Clone, PartialOrd, Ord)] pub struct MultiColor(pub ColorId); impl Color for MultiColor { // omitted } #[derive(Debug, PartialEq, Eq, Hash, Default, Clone)] pub struct ColoredBlock { size: usize, color: ColorId, } impl Block for ColoredBlock { type Color = MultiColor; // omitted } 

Lors du portage du code, certains points ont clairement indiqué qu'un langage typé statiquement tel que Rust (enfin, ou, par exemple, C ++) est plus approprié pour cette tâche. Plus précisément, les génériques et les traits décrivent un domaine mieux que les hiérarchies de classes. Donc, dans le code Python, j'avais deux classes pour un BguSolver linéaire, BguSolver et BguColoredSolver qui résolvaient respectivement une ligne en noir et blanc et une ligne en couleur. Dans le code Rust, j'ai toujours la seule struct DynamicSolver<B: Block, S = <B as Block>::Color> générique struct DynamicSolver<B: Block, S = <B as Block>::Color> , qui peut résoudre les deux types de tâches, selon le type passé lors de la création ( DynamicSolver<BinaryBlock>, DynamicSolver<ColoredBlock> ). Bien sûr, cela ne signifie pas que quelque chose de similaire ne peut pas être fait en Python, juste dans Rust, le système de type m'a clairement indiqué que si vous n'alliez pas de cette façon, vous auriez à écrire une tonne de code répétitif.


De plus, quiconque a essayé d'écrire dans Rust, a sans aucun doute remarqué l'effet de la «confiance» dans le compilateur, lorsque le processus d'écriture du code se résume à l'algorithme pseudo-méta suivant:


 write_initial_code
 while (compiler_hints = $ (cargo check))! = 0;  faire
     fix_errors (compiler_hints)
 fin

Lorsque le compilateur cesse d'émettre des erreurs et des avertissements, votre code sera cohérent avec le système de type et le vérificateur d'emprunt et vous préviendrez à l'avance l'occurrence d'un tas de bogues potentiels (bien sûr, sous réserve d'une conception soignée des types de données).


Je vais donner quelques exemples de fonctions qui montrent à quel point le code Rust peut être concis (par rapport aux équivalents Python).


unsolved_neighbours

Donne une liste de "voisins" non résolus pour un point donné (x, y)


 def unsolved_neighbours(self, position): for pos in self.neighbours(position): if not self.is_cell_solved(*pos): yield pos 

 fn unsolved_neighbours(&self, point: &Point) -> impl Iterator<Item = Point> + '_ { self.neighbours(&point) .into_iter() .filter(move |n| !self.cell(n).is_solved()) } 

partial_sums

Pour un ensemble de blocs décrivant une ligne, donnez des sommes partielles (en tenant compte des espaces requis entre les blocs). Les indices résultants indiqueront la position minimale à laquelle le bloc peut se terminer (cette information sera utilisée plus tard pour un solveur linéaire).


Par exemple, pour un tel ensemble de blocs [2, 3, 1] nous avons à la sortie [2, 6, 8] , ce qui signifie que le premier bloc peut être déplacé au maximum vers la gauche afin que son bord droit occupe la 2e cellule, de même pour le reste blocs:


             1 2 3 4 5 6 7 8 9 
             _ _ _ _ _ _ _ _ _ _
      2 3 1 | _ | _ | _ | _ | _ | _ | _ | _ | _ | 
               ^ ^ ^
               |  |  |
 fin d'un bloc |  |  | 
 fin du bloc 2 -------- |
 fin du bloc 3 ------------

 @expand_generator def partial_sums(blocks): if not blocks: return sum_so_far = blocks[0] yield sum_so_far for block in blocks[1:]: sum_so_far += block + 1 yield sum_so_far 

 fn partial_sums(desc: &[Self]) -> Vec<usize> { desc.iter() .scan(None, |prev, block| { let current = if let Some(ref prev_size) = prev { prev_size + block.0 + 1 } else { block.0 }; *prev = Some(current); *prev }) .collect() } 

Lors du portage, j'ai apporté plusieurs modifications


  • le noyau du solveur (algorithmes) a subi des modifications mineures (principalement pour prendre en charge les types génériques pour les cellules et les blocs)
  • laissé le seul algorithme (le plus rapide) pour le solveur linéaire
  • au lieu du format ini, introduit un format TOML légèrement modifié
  • n'a pas ajouté la prise en charge des mots croisés effacés, car, à proprement parler, il s'agit d'une classe de tâches différente
  • laissé le seul moyen de sortie - juste à la console, mais maintenant les cellules colorées dans la console sont dessinées vraiment colorées (grâce à cette caisse )


    comme ça

    Jack moineau




Des outils utiles


  • clippy est un analyseur statique standard qui peut même parfois donner des conseils augmentant légèrement les performances du code.
  • valgrind est un outil d'analyse dynamique des applications. Je l'ai utilisé comme profileur pour rechercher des botneks ( valrgind --tool=callgrind ) et en particulier des sections de code valrgind --tool=massif ( valrgind --tool=massif ). Astuce: définissez [profile.release] debug = true sur Cargo.toml avant de démarrer le profileur. Cela laissera des caractères de débogage dans l'exécutable.
  • kcachegrind pour afficher les fichiers de callgrind. Un outil très utile pour trouver les endroits les plus problématiques en termes de performances.

Performances


C'est pour ça que la réécriture sur Rust a commencé. Nous prenons les mots croisés du tableau de comparaison déjà mentionné et les exécutons à travers les meilleurs solveurs décrits dans l'article d'origine. Résultats et description des runs ici . Nous prenons le fichier résultant et construisons quelques graphiques dessus. Comme le temps de résolution varie de quelques millisecondes à des dizaines de minutes, le graphique est fait avec une échelle logarithmique.


courir dans un ordinateur portable jupyter
 import pandas as pd import numpy as np import matplotlib.pyplot as plt %matplotlib inline # strip the spaces df = pd.read_csv('perf.csv', skipinitialspace=True) df.columns = df.columns.str.strip() df['name'] = df['name'].str.strip() # convert to numeric df = df.replace('\+\ *', np.inf, regex=True) ALL_SOLVERS = list(df.columns[3:]) df.loc[:,ALL_SOLVERS] = df.loc[:,ALL_SOLVERS].apply(pd.to_numeric) # it cannot be a total zero df = df.replace(0, 0.001) #df.set_index('name', inplace=True) SURVEY_SOLVERS = [s for s in ALL_SOLVERS if not s.endswith('_my')] MY_MACHINE_SOLVERS = [s for s in ALL_SOLVERS if s.endswith('_my') and s[:-3] in SURVEY_SOLVERS] MY_SOLVERS = [s for s in ALL_SOLVERS if s.endswith('_my') and s[:-3] not in SURVEY_SOLVERS] bar_width = 0.17 df_compare = df.replace(np.inf, 10000, regex=True) plt.rcParams.update({'font.size': 20}) def compare(first, others): bars = [first] + list(others) index = np.arange(len(df)) fig, ax = plt.subplots(figsize=(30,10)) df_compare.sort_values(first, inplace=True) for i, column in enumerate(bars): ax.bar(index + bar_width*i, df_compare[column], bar_width, label=column[:-3]) ax.set_xlabel("puzzles") ax.set_ylabel("Time, s (log)") ax.set_title("Compare '{}' with others (lower is better)".format(first[:-3])) ax.set_xticks(index + bar_width / 2) ax.set_xticklabels("#" + df_compare['ID'].astype(str) + ": " + df_compare['name'].astype(str)) ax.legend() plt.yscale('log') plt.xticks(rotation=90) plt.show() fig.savefig(first[:-3] + '.png', bbox_inches='tight') for my in MY_SOLVERS: compare(my, MY_MACHINE_SOLVERS) compare(MY_SOLVERS[0], MY_SOLVERS[1:]) 

solveur python

performances du pynogramme

(l' image est cliquable )


Nous voyons que le pynogramme ici est plus lent que tous les solveurs présentés. La seule exception à cette règle est le solveur Tamura / Copris basé sur SAT, qui résout les puzzles les plus simples (la partie gauche du graphique) plus longtemps que le nôtre. Cependant, c'est une caractéristique des solveurs SAT: ils sont conçus pour des mots croisés super complexes dans lesquels un solveur régulier est coincé dans le retour arrière pendant une longue période. Ceci est clairement visible sur le côté droit du graphique, où Tamura / Copris résout les puzzles les plus difficiles des dizaines et des centaines de fois plus rapidement que tout le monde.


solveur de rouille

performance non-grille

(l' image est cliquable )


Ce graphique montre que les non - grids sur des tâches simples supportent également ou légèrement pire que les solveurs hautes performances écrits en C et C ++ ( Wolter et Syromolotov ). Avec la complication des tâches, notre solveur répète approximativement la trajectoire du solveur BGU (Java), mais presque toujours devant lui d'environ un ordre de grandeur. Sur les tâches les plus difficiles, Tamura / Copris est toujours en avance sur tout le monde.


rouille vs python

py-vs-rust-performance

(l' image est cliquable )


Et enfin, une comparaison de nos deux solveurs décrits ici. On peut voir que le solveur Rust est presque toujours à 1 à 3 ordres de grandeur devant le solveur Python.


Résumé :


+ le solveur peut résoudre tous les puzzles des sites https://webpbn.com (sauf buvard - avec des tailles de bloc partiellement masquées), http://nonograms.org et son propre format (basé sur TOML)


+ résout les nonogrammes noir et blanc et couleur avec n'importe quel nombre de couleurs


+ peut rendre des puzzles sur la console (la couleur c webpbn.com dessine la vraie couleur)


+ fonctionne rapidement (en comparaison avec les implémentations décrites dans l'article-comparaison des solveurs, voir tableau).


- le retour en arrière est resté inefficace, comme dans la solution Python: certains casse-tête (par exemple , un tel 20x20 inoffensif ) peuvent être résolus pendant des heures (lorsque l'arbre de décision est très grand). Peut-être qu'au lieu de revenir en arrière, vous devriez utiliser les solveurs SAT déjà mentionnés sur le concentrateur . Certes, le seul solveur SAT que j'ai trouvé sur Rust à première vue semble inachevé et abandonné.


Webassembly


La réécriture du code dans Rust a donc porté ses fruits: le solveur est devenu beaucoup plus rapide. Cependant, Rust nous offre une autre fonctionnalité incroyablement cool: la compilation dans WebAssembly et la possibilité d'exécuter votre code directement dans le navigateur.


Pour implémenter cette fonctionnalité, il existe un outil spécial pour Rust qui fournit les liants nécessaires et génère un passe-partout pour que vous puissiez exécuter sans douleur les fonctions Rust en code JS - ce wasm-pack (+ wasm-bindgen ). La plupart du travail avec celui-ci et d'autres outils importants est déjà décrit dans le didacticiel Rust et WebAssembly . Cependant, il y a quelques points que j'ai dû comprendre par moi-même:


  • lors de la lecture, cela donne l'impression que le tutoriel est principalement écrit pour un développeur JS qui souhaite accélérer son code avec Rust. Eh bien, ou du moins pour quelqu'un qui connaît bien npm . Pour moi, en tant que personne loin du front-end, il était surprenant de constater que même l'exemple standard du livre ne voulait pas fonctionner avec un serveur Web tiers différent de npm run start .


    Heureusement, wasm-pack dispose d'un mode qui vous permet de générer du code JS standard (qui n'est pas un module npm). wasm-pack build --target no-modules --no-typescript ne donnera que deux fichiers dans la sortie: project-name.wasm - le binaire du code Rust compilé dans WebAssembly et project-name.js . Le dernier fichier peut être ajouté à n'importe quelle page HTML <script src="project-name.js"></script> et utiliser les fonctions WASM sans se soucier de npm, webpack, ES6, des modules et autres joies du développeur JS moderne. Le mode no-modules est idéal pour les développeurs non frontaux lors du développement d'une application WASM, ainsi que pour les exemples et les démos, car il ne nécessite aucune infrastructure frontale supplémentaire.


  • WebAssembly convient aux tâches trop lourdes pour JavaScript. Tout d'abord, ce sont des tâches qui effectuent de nombreux calculs. Et si c'est le cas, une telle tâche peut être effectuée pendant longtemps même avec WebAssembly et ainsi violer le principe asynchrone du web moderne. Je parle de toutes sortes d' avertissement: script ne répondant pas que j'ai observé lorsque mon solveur fonctionnait. Pour résoudre ce problème, vous pouvez utiliser le mécanisme de travail Web . Dans ce cas, le schéma de travail avec les fonctions WASM "lourdes" peut ressembler à ceci:


    1. à partir du script principal d'un événement (par exemple, en cliquant sur un bouton) envoyer un message au travailleur avec la tâche de lancer une fonction lourde.
    2. , .
    3. - ()


WASM- JS, WASM . - ( ), HashMap , . ( JS) , / .


, Mutex , thread-safe. smart- . thread-safe Rc Arc RefCell RwLock . : 30%. --features=threaded thread-safe , WASM-.


6574 8098 ( 10 ):


idnon-thread-safethread-safeweb-interface
65745.47.49.2
809821.528.429.9

, - 40..70% , , (32..37%) thread-safe ( cargo build --release --features=threaded ).


Firefox 67.0 Chromium 74.0.


WASM- ( ). https://webpbn.com/ http://www.nonograms.org/


TODO


  • "" /, / .


  • , . "" , , . .


  • ( , 3600 ). WASM , ( , (!) , , WASM). , , - , nonogrid .


  • . : , , WASM . , ( ) , JS , .


  • JS. backtrace, .


  • ( TOML- )



Résumé


  • , (, , etc).


  • Rust 1-3 PyPy 1.5-2 ( ).


  • Python Rust , Python (, , comprehensions), Rust-.


  • Rust WebAssembly . Rust , WASM, ( 1.5 ).




  1. The 'pbnsolve' Paint-by-Number Puzzle Solver by Jan Wolter and the survey
  2. The BGU Nonograms Project
  3. Solving Nonograms by combining relaxations
  4. An Efficient Approach to Solving Nonograms
  5. Color and black and white Japanese crosswords on-line
  6. 'Nonolib' library by Dr. Steven Simpson
  7. Rust and WebAssembly

Source: https://habr.com/ru/post/fr454586/


All Articles