Implemented playfair cipher
authorNeil Smith <neil.git@njae.me.uk>
Thu, 15 Nov 2018 17:34:32 +0000 (17:34 +0000)
committerNeil Smith <neil.git@njae.me.uk>
Thu, 15 Nov 2018 17:34:32 +0000 (17:34 +0000)
Untitled.ipynb [deleted file]
cipher/keyword_cipher.py
cipher/playfair.py [new file with mode: 0644]
playfair_develop.ipynb [new file with mode: 0644]

diff --git a/Untitled.ipynb b/Untitled.ipynb
deleted file mode 100644 (file)
index 023ad11..0000000
+++ /dev/null
@@ -1,141 +0,0 @@
-{
- "cells": [
-  {
-   "cell_type": "code",
-   "execution_count": 2,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "from support.utilities import *\n",
-    "from support.language_models import *\n",
-    "from support.norms import *\n",
-    "from cipher.keyword_cipher import *"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 3,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "def keyword_encipher_p(message, keyword, wrap_alphabet=KeywordWrapAlphabet.from_a):\n",
-    "    cipher_alphabet = keyword_cipher_alphabet_of(keyword, wrap_alphabet)\n",
-    "    cipher_translation = {p: c for p, c in zip(string.ascii_lowercase, cipher_alphabet)}\n",
-    "    return cat(keyword_encipher_letter(letter, cipher_translation) for letter in message)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 8,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "def keyword_decipher_p(message, keyword, wrap_alphabet=KeywordWrapAlphabet.from_a):\n",
-    "    cipher_alphabet = keyword_cipher_alphabet_of(keyword, wrap_alphabet)\n",
-    "    plaintext_translation = {c: p for p, c in zip(string.ascii_lowercase, cipher_alphabet)}\n",
-    "    return cat(keyword_encipher_letter(letter, plaintext_translation) for letter in message)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 4,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "def keyword_encipher_letter(letter, cipher_translation):\n",
-    "    if letter in cipher_translation:\n",
-    "        return cipher_translation[letter]\n",
-    "    else:\n",
-    "        return letter"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 5,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "'qopq hoppkdo'"
-      ]
-     },
-     "execution_count": 5,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "keyword_encipher('test message', 'keyword')"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 9,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "'qopq hoppkdo'"
-      ]
-     },
-     "execution_count": 9,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "keyword_encipher_p('test message', 'keyword')"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 10,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "'test message'"
-      ]
-     },
-     "execution_count": 10,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "keyword_decipher_p('qopq hoppkdo', 'keyword')"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": []
-  }
- ],
- "metadata": {
-  "kernelspec": {
-   "display_name": "Python 3",
-   "language": "python",
-   "name": "python3"
-  },
-  "language_info": {
-   "codemirror_mode": {
-    "name": "ipython",
-    "version": 3
-   },
-   "file_extension": ".py",
-   "mimetype": "text/x-python",
-   "name": "python",
-   "nbconvert_exporter": "python",
-   "pygments_lexer": "ipython3",
-   "version": "3.4.5"
-  }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
index c5e7cd8b27b2c381d1421660296b9a26c8454c69..20281828030a550948c433f246c8bc05a40622c1 100644 (file)
@@ -194,13 +194,23 @@ def simulated_annealing_break(message, workers=10,
     worker_args = []
     ciphertext = sanitise(message)
     for i in range(workers):
-        if not plain_alphabet:
-            plain_alphabet = string.ascii_lowercase
-        if not cipher_alphabet:
-            cipher_alphabet = list(string.ascii_lowercase)
-            random.shuffle(cipher_alphabet)
-            cipher_alphabet = cat(cipher_alphabet)
-        worker_args.append((ciphertext, plain_alphabet, cipher_alphabet, 
+        if plain_alphabet is None:
+            used_plain_alphabet = string.ascii_lowercase
+        else:
+            used_plain_alphabet = plain_alphabet
+        if cipher_alphabet is None:
+            used_cipher_alphabet = list(string.ascii_lowercase)
+            random.shuffle(used_cipher_alphabet)
+            used_cipher_alphabet = cat(used_cipher_alphabet)
+        else:
+            used_cipher_alphabet = cipher_alphabet
+        # if not plain_alphabet:
+        #     plain_alphabet = string.ascii_lowercase
+        # if not cipher_alphabet:
+        #     cipher_alphabet = list(string.ascii_lowercase)
+        #     random.shuffle(cipher_alphabet)
+        #     cipher_alphabet = cat(cipher_alphabet)
+        worker_args.append((ciphertext, used_plain_alphabet, used_cipher_alphabet, 
                             initial_temperature, max_iterations, fitness))
     with multiprocessing.Pool() as pool:
         breaks = pool.starmap(simulated_annealing_break_worker,
diff --git a/cipher/playfair.py b/cipher/playfair.py
new file mode 100644 (file)
index 0000000..0c0bc6e
--- /dev/null
@@ -0,0 +1,298 @@
+from support.utilities import *
+from support.language_models import *
+from cipher.keyword_cipher import KeywordWrapAlphabet, keyword_cipher_alphabet_of
+from cipher.polybius import polybius_grid
+import multiprocessing
+
+from logger import logger
+
+def playfair_wrap(n, lowest, highest):
+    skip = highest - lowest + 1
+    while n > highest or n < lowest:
+        if n > highest:
+            n -= skip
+        if n < lowest:
+            n += skip
+    return n
+
+def playfair_encipher_bigram(ab, grid, padding_letter='x'):
+    a, b = ab
+    max_row = max(c[0] for c in grid.values())
+    max_col = max(c[1] for c in grid.values())
+    min_row = min(c[0] for c in grid.values())
+    min_col = min(c[1] for c in grid.values())
+    if a == b:
+        b = padding_letter
+    if grid[a][0] == grid[b][0]:  # same row
+        cp = (grid[a][0], playfair_wrap(grid[a][1] + 1, min_col, max_col))
+        dp = (grid[b][0], playfair_wrap(grid[b][1] + 1, min_col, max_col))
+    elif grid[a][1] == grid[b][1]:  # same column
+        cp = (playfair_wrap(grid[a][0] + 1, min_row, max_row), grid[a][1])
+        dp = (playfair_wrap(grid[b][0] + 1, min_row, max_row), grid[b][1])
+    else:
+        cp = (grid[a][0], grid[b][1])
+        dp = (grid[b][0], grid[a][1])
+    c = [k for k, v in grid.items() if v == cp][0]
+    d = [k for k, v in grid.items() if v == dp][0]
+    return c + d
+
+def playfair_decipher_bigram(ab, grid, padding_letter='x'):
+    a, b = ab
+    max_row = max(c[0] for c in grid.values())
+    max_col = max(c[1] for c in grid.values())
+    min_row = min(c[0] for c in grid.values())
+    min_col = min(c[1] for c in grid.values())
+    if a == b:
+        b = padding_letter
+    if grid[a][0] == grid[b][0]:  # same row
+        cp = (grid[a][0], playfair_wrap(grid[a][1] - 1, min_col, max_col))
+        dp = (grid[b][0], playfair_wrap(grid[b][1] - 1, min_col, max_col))
+    elif grid[a][1] == grid[b][1]:  # same column
+        cp = (playfair_wrap(grid[a][0] - 1, min_row, max_row), grid[a][1])
+        dp = (playfair_wrap(grid[b][0] - 1, min_row, max_row), grid[b][1])
+    else:
+        cp = (grid[a][0], grid[b][1])
+        dp = (grid[b][0], grid[a][1])
+    c = [k for k, v in grid.items() if v == cp][0]
+    d = [k for k, v in grid.items() if v == dp][0]
+    return c + d
+
+def playfair_bigrams(text, padding_letter='x', padding_replaces_repeat=True):
+    i = 0
+    bigrams = []
+    while i < len(text):
+        bigram = text[i:i+2]
+        if len(bigram) == 1:
+            i = len(text) + 1
+            bigram = bigram + padding_letter
+        else:
+            if bigram[0] == bigram[1]:
+                bigram = bigram[0] + padding_letter
+                if padding_replaces_repeat:
+                    i += 2
+                else:
+                    i += 1
+            else:
+                i += 2
+        bigrams += [bigram]
+    return bigrams
+
+def playfair_encipher(message, keyword, padding_letter='x',
+                      padding_replaces_repeat=False, letters_to_merge=None, 
+                      wrap_alphabet=KeywordWrapAlphabet.from_a):
+    column_order = list(range(5))
+    row_order = list(range(5))
+    if letters_to_merge is None: 
+        letters_to_merge = {'j': 'i'}   
+    grid = polybius_grid(keyword, column_order, row_order,
+                        letters_to_merge=letters_to_merge,
+                        wrap_alphabet=wrap_alphabet)
+    message_bigrams = playfair_bigrams(sanitise(message), padding_letter=padding_letter, 
+                                       padding_replaces_repeat=padding_replaces_repeat)
+    ciphertext_bigrams = [playfair_encipher_bigram(b, grid, padding_letter=padding_letter) for b in message_bigrams]
+    return cat(ciphertext_bigrams)
+
+def playfair_decipher(message, keyword, padding_letter='x',
+                      padding_replaces_repeat=False, letters_to_merge=None, 
+                      wrap_alphabet=KeywordWrapAlphabet.from_a):
+    column_order = list(range(5))
+    row_order = list(range(5))
+    if letters_to_merge is None: 
+        letters_to_merge = {'j': 'i'}   
+    grid = polybius_grid(keyword, column_order, row_order,
+                        letters_to_merge=letters_to_merge,
+                        wrap_alphabet=wrap_alphabet)
+    message_bigrams = playfair_bigrams(sanitise(message), padding_letter=padding_letter, 
+                                       padding_replaces_repeat=padding_replaces_repeat)
+    plaintext_bigrams = [playfair_decipher_bigram(b, grid, padding_letter=padding_letter) for b in message_bigrams]
+    return cat(plaintext_bigrams)
+
+def playfair_break_mp(message, 
+                      letters_to_merge=None, padding_letter='x',
+                      wordlist=keywords, fitness=Pletters,
+                      number_of_solutions=1, chunksize=500):
+    if letters_to_merge is None: 
+        letters_to_merge = {'j': 'i'}   
+
+    with multiprocessing.Pool() as pool:
+        helper_args = [(message, word, wrap, 
+                        letters_to_merge, padding_letter,
+                        pad_replace,
+                        fitness)
+                       for word in wordlist
+                       for wrap in KeywordWrapAlphabet
+                       for pad_replace in [False, True]]
+        # Gotcha: the helper function here needs to be defined at the top level
+        #   (limitation of Pool.starmap)
+        breaks = pool.starmap(playfair_break_worker, helper_args, chunksize)
+        if number_of_solutions == 1:
+            return max(breaks, key=lambda k: k[1])
+        else:
+            return sorted(breaks, key=lambda k: k[1], reverse=True)[:number_of_solutions]
+
+def playfair_break_worker(message, keyword, wrap, 
+                          letters_to_merge, padding_letter,
+                          pad_replace,
+                          fitness):
+    plaintext = playfair_decipher(message, keyword, padding_letter,
+                                  pad_replace,
+                                  letters_to_merge, 
+                                  wrap)
+    if plaintext:
+        fit = fitness(plaintext)
+    else:
+        fit = float('-inf')
+    logger.debug('Playfair break attempt using key {0} (wrap={1}, merging {2}, '
+                 'pad replaces={3}), '
+                 'gives fit of {4} and decrypt starting: '
+                 '{5}'.format(keyword, wrap, letters_to_merge, pad_replace,
+                              fit, sanitise(plaintext)[:50]))
+    return (keyword, wrap, letters_to_merge, padding_letter, pad_replace), fit
+
+def playfair_simulated_annealing_break(message, workers=10, 
+                              initial_temperature=200,
+                              max_iterations=20000,
+                              plain_alphabet=None, 
+                              cipher_alphabet=None, 
+                              fitness=Pletters, chunksize=1):
+    worker_args = []
+    ciphertext = sanitise(message)
+    for i in range(workers):
+        if plain_alphabet is None:
+            used_plain_alphabet = string.ascii_lowercase
+        else:
+            used_plain_alphabet = plain_alphabet
+        if cipher_alphabet is None:
+            # used_cipher_alphabet = list(string.ascii_lowercase)
+            # random.shuffle(used_cipher_alphabet)
+            # used_cipher_alphabet = cat(used_cipher_alphabet)
+            used_cipher_alphabet = random.choice(keywords)
+        else:
+            used_cipher_alphabet = cipher_alphabet
+        worker_args.append((ciphertext, used_plain_alphabet, used_cipher_alphabet, 
+                            initial_temperature, max_iterations, fitness))
+    with multiprocessing.Pool() as pool:
+        breaks = pool.starmap(playfair_simulated_annealing_break_worker,
+                              worker_args, chunksize)
+    return max(breaks, key=lambda k: k[1])
+
+def playfair_simulated_annealing_break_worker(message, plain_alphabet, cipher_alphabet, 
+                                     t0, max_iterations, fitness):
+    def swap(letters, i, j):
+        if i > j:
+            i, j = j, i
+        if i == j:
+            return letters
+        else:
+            return (letters[:i] + letters[j] + letters[i+1:j] + letters[i] +
+                    letters[j+1:])
+    
+    temperature = t0
+
+    dt = t0 / (0.9 * max_iterations)
+    
+    current_alphabet = cipher_alphabet
+    current_wrap = KeywordWrapAlphabet.from_a
+    current_letters_to_merge = {'j': 'i'}
+    current_pad_replace = False
+    current_padding_letter = 'x'
+    
+    alphabet = current_alphabet
+    wrap = current_wrap
+    letters_to_merge = current_letters_to_merge
+    pad_replace = current_pad_replace
+    padding_letter = current_padding_letter
+    plaintext = playfair_decipher(message, alphabet, padding_letter,
+                                  pad_replace,
+                                  letters_to_merge, 
+                                  wrap)
+    current_fitness = fitness(plaintext)
+
+    best_alphabet = current_alphabet
+    best_fitness = current_fitness
+    best_plaintext = plaintext
+    
+    # print('starting for', max_iterations)
+    for i in range(max_iterations):
+        chosen = random.random()
+        # if chosen < 0.7:
+        #     swap_a = random.randrange(26)
+        #     swap_b = (swap_a + int(random.gauss(0, 4))) % 26
+        #     alphabet = swap(current_alphabet, swap_a, swap_b)
+        # elif chosen < 0.8:
+        #     wrap = random.choice(list(KeywordWrapAlphabet))
+        # elif chosen < 0.9:
+        #     pad_replace = random.choice([True, False])
+        # elif chosen < 0.95:
+        #     letter_from = random.choice(string.ascii_lowercase)
+        #     letter_to = random.choice([c for c in string.ascii_lowercase if c != letter_from])
+        #     letters_to_merge = {letter_from: letter_to}
+        # else:
+        #     padding_letter = random.choice(string.ascii_lowercase)
+        if chosen < 0.7:
+            swap_a = random.randrange(len(current_alphabet))
+            swap_b = (swap_a + int(random.gauss(0, 4))) % len(current_alphabet)
+            alphabet = swap(current_alphabet, swap_a, swap_b)
+        elif chosen < 0.85:
+            new_letter = random.choice(string.ascii_lowercase)
+            alphabet = swap(current_alphabet + new_letter, random.randrange(len(current_alphabet)), len(current_alphabet))
+        else:
+            if len(current_alphabet) > 1:
+                deletion_position = random.randrange(len(current_alphabet))
+                alphabet = current_alphabet[:deletion_position] + current_alphabet[deletion_position+1:]
+            else:
+                alphabet = current_alphabet
+        
+        try:
+            plaintext = playfair_decipher(message, alphabet, padding_letter,
+                                  pad_replace,
+                                  letters_to_merge, 
+                                  wrap)
+        except:
+            print("Error", alphabet, padding_letter,
+                                  pad_replace,
+                                  letters_to_merge, 
+                                  wrap)
+            raise
+
+        new_fitness = fitness(plaintext)
+        try:
+            sa_chance = math.exp((new_fitness - current_fitness) / temperature)
+        except (OverflowError, ZeroDivisionError):
+            # print('exception triggered: new_fit {}, current_fit {}, temp {}'.format(new_fitness, current_fitness, temperature))
+            sa_chance = 0
+        if (new_fitness > current_fitness or random.random() < sa_chance):
+            # logger.debug('Simulated annealing: iteration {}, temperature {}, '
+            #     'current alphabet {}, current_fitness {}, '
+            #     'best_plaintext {}'.format(i, temperature, current_alphabet, 
+            #     current_fitness, best_plaintext[:50]))
+
+            # logger.debug('new_fit {}, current_fit {}, temp {}, sa_chance {}'.format(new_fitness, current_fitness, temperature, sa_chance))
+            current_fitness = new_fitness
+            current_alphabet = alphabet
+            current_wrap = wrap
+            current_letters_to_merge = letters_to_merge
+            current_pad_replace = pad_replace
+            current_padding_letter = padding_letter
+            
+        if current_fitness > best_fitness:
+            best_alphabet = current_alphabet
+            best_wrap = current_wrap
+            best_letters_to_merge = current_letters_to_merge
+            best_pad_replace = current_pad_replace
+            best_padding_letter = current_padding_letter
+            best_fitness = current_fitness
+            best_plaintext = plaintext
+        if i % 500 == 0:
+            logger.debug('Simulated annealing: iteration {}, temperature {}, '
+                'current alphabet {}, current_fitness {}, '
+                'best_plaintext {}'.format(i, temperature, current_alphabet, 
+                current_fitness, plaintext[:50]))
+        temperature = max(temperature - dt, 0.001)
+
+    return { 'alphabet': best_alphabet
+           , 'wrap': best_wrap
+           , 'letters_to_merge': best_letters_to_merge
+           , 'pad_replace': best_pad_replace
+           , 'padding_letter': best_padding_letter
+           }, best_fitness # current_alphabet, current_fitness
diff --git a/playfair_develop.ipynb b/playfair_develop.ipynb
new file mode 100644 (file)
index 0000000..ea1b9a7
--- /dev/null
@@ -0,0 +1,1014 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 103,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import multiprocessing\n",
+    "from support.utilities import *\n",
+    "from support.language_models import *\n",
+    "from support.norms import *\n",
+    "from cipher.keyword_cipher import *\n",
+    "from cipher.polybius import *"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 23,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'p': (1, 1),\n",
+       " 'l': (1, 2),\n",
+       " 'a': (1, 3),\n",
+       " 'y': (1, 4),\n",
+       " 'f': (1, 5),\n",
+       " 'i': (2, 1),\n",
+       " 'r': (2, 2),\n",
+       " 'e': (2, 3),\n",
+       " 'x': (2, 4),\n",
+       " 'm': (2, 5),\n",
+       " 'b': (3, 1),\n",
+       " 'c': (3, 2),\n",
+       " 'd': (3, 3),\n",
+       " 'g': (3, 4),\n",
+       " 'h': (3, 5),\n",
+       " 'k': (4, 1),\n",
+       " 'n': (4, 2),\n",
+       " 'o': (4, 3),\n",
+       " 'q': (4, 4),\n",
+       " 's': (4, 5),\n",
+       " 't': (5, 1),\n",
+       " 'u': (5, 2),\n",
+       " 'v': (5, 3),\n",
+       " 'w': (5, 4),\n",
+       " 'z': (5, 5),\n",
+       " 'j': (2, 1)}"
+      ]
+     },
+     "execution_count": 23,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "g = polybius_grid('playfair example', [1,2,3,4,5], [1,2,3,4,5])\n",
+    "g"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 14,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def playfair_wrap(n, lowest, highest):\n",
+    "    skip = highest - lowest + 1\n",
+    "    while n > highest or n < lowest:\n",
+    "        if n > highest:\n",
+    "            n -= skip\n",
+    "        if n < lowest:\n",
+    "            n += skip\n",
+    "    return n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 20,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "1"
+      ]
+     },
+     "execution_count": 20,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "playfair_wrap(11, 1, 5)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 66,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def playfair_encipher_bigram(ab, grid, padding_letter='x'):\n",
+    "    a, b = ab\n",
+    "    max_row = max(c[0] for c in grid.values())\n",
+    "    max_col = max(c[1] for c in grid.values())\n",
+    "    min_row = min(c[0] for c in grid.values())\n",
+    "    min_col = min(c[1] for c in grid.values())\n",
+    "    if a == b:\n",
+    "        b = padding_letter\n",
+    "    if grid[a][0] == grid[b][0]:  # same row\n",
+    "        cp = (grid[a][0], playfair_wrap(grid[a][1] + 1, min_col, max_col))\n",
+    "        dp = (grid[b][0], playfair_wrap(grid[b][1] + 1, min_col, max_col))\n",
+    "    elif grid[a][1] == grid[b][1]:  # same column\n",
+    "        cp = (playfair_wrap(grid[a][0] + 1, min_row, max_row), grid[a][1])\n",
+    "        dp = (playfair_wrap(grid[b][0] + 1, min_row, max_row), grid[b][1])\n",
+    "    else:\n",
+    "        cp = (grid[a][0], grid[b][1])\n",
+    "        dp = (grid[b][0], grid[a][1])\n",
+    "    c = [k for k, v in grid.items() if v == cp][0]\n",
+    "    d = [k for k, v in grid.items() if v == dp][0]\n",
+    "    return c + d"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 68,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'xm'"
+      ]
+     },
+     "execution_count": 68,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "playfair_encipher_bigram('ex', g)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 69,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def playfair_decipher_bigram(ab, grid, padding_letter='x'):\n",
+    "    a, b = ab\n",
+    "    max_row = max(c[0] for c in grid.values())\n",
+    "    max_col = max(c[1] for c in grid.values())\n",
+    "    min_row = min(c[0] for c in grid.values())\n",
+    "    min_col = min(c[1] for c in grid.values())\n",
+    "    if a == b:\n",
+    "        b = padding_letter\n",
+    "    if grid[a][0] == grid[b][0]:  # same row\n",
+    "        cp = (grid[a][0], playfair_wrap(grid[a][1] - 1, min_col, max_col))\n",
+    "        dp = (grid[b][0], playfair_wrap(grid[b][1] - 1, min_col, max_col))\n",
+    "    elif grid[a][1] == grid[b][1]:  # same column\n",
+    "        cp = (playfair_wrap(grid[a][0] - 1, min_row, max_row), grid[a][1])\n",
+    "        dp = (playfair_wrap(grid[b][0] - 1, min_row, max_row), grid[b][1])\n",
+    "    else:\n",
+    "        cp = (grid[a][0], grid[b][1])\n",
+    "        dp = (grid[b][0], grid[a][1])\n",
+    "    c = [k for k, v in grid.items() if v == cp][0]\n",
+    "    d = [k for k, v in grid.items() if v == dp][0]\n",
+    "    return c + d"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 70,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'ex'"
+      ]
+     },
+     "execution_count": 70,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "playfair_decipher_bigram('xm', g)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 36,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "['hi', 'de', 'th', 'eg', 'ol', 'di', 'nt', 'he', 'tr', 'ee', 'st', 'um', 'p']"
+      ]
+     },
+     "execution_count": 36,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "chunks(sanitise('hide the gold in the tree stump'), 2)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 75,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def playfair_bigrams(text, padding_letter='x', padding_replaces_repeat=True):\n",
+    "    i = 0\n",
+    "    bigrams = []\n",
+    "    while i < len(text):\n",
+    "        bigram = text[i:i+2]\n",
+    "        if len(bigram) == 1:\n",
+    "            i = len(text) + 1\n",
+    "            bigram = bigram + padding_letter\n",
+    "        else:\n",
+    "            if bigram[0] == bigram[1]:\n",
+    "                bigram = bigram[0] + padding_letter\n",
+    "                if padding_replaces_repeat:\n",
+    "                    i += 2\n",
+    "                else:\n",
+    "                    i += 1\n",
+    "            else:\n",
+    "                i += 2\n",
+    "        bigrams += [bigram]\n",
+    "    return bigrams\n",
+    "    "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 76,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "['hi', 'de', 'th', 'eg', 'ol', 'di', 'nt', 'he', 'tr', 'ex', 'st', 'um', 'px']"
+      ]
+     },
+     "execution_count": 76,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "playfair_bigrams(sanitise('hide the gold in the tree stump'), padding_replaces_repeat=True)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 77,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "['hi', 'de', 'th', 'eg', 'ol', 'di', 'nt', 'he', 'tr', 'ex', 'es', 'tu', 'mp']"
+      ]
+     },
+     "execution_count": 77,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "playfair_bigrams(sanitise('hide the gold in the tree stump'), padding_replaces_repeat=False)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 73,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'bmodzbxdnabekudmuixmmouvif'"
+      ]
+     },
+     "execution_count": 73,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "ct = cat(playfair_encipher_bigram((b[0], b[1]), g) for b in playfair_bigrams(sanitise('hide the gold in the tree stump')))\n",
+    "ct"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 74,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'hidethegoldinthetrexestump'"
+      ]
+     },
+     "execution_count": 74,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "cat(playfair_decipher_bigram((b[0], b[1]), g) for b in playfair_bigrams(sanitise(ct)))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 128,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def playfair_encipher(message, keyword, padding_letter='x',\n",
+    "                      padding_replaces_repeat=False,\n",
+    "#                       column_order=None, row_order=None, \n",
+    "#                       column_first=False, \n",
+    "                      letters_to_merge=None, \n",
+    "                      wrap_alphabet=KeywordWrapAlphabet.from_a):\n",
+    "    column_order = list(range(5))\n",
+    "    row_order = list(range(5))\n",
+    "    if letters_to_merge is None: \n",
+    "        letters_to_merge = {'j': 'i'}   \n",
+    "    grid = polybius_grid(keyword, column_order, row_order,\n",
+    "                        letters_to_merge=letters_to_merge,\n",
+    "                        wrap_alphabet=wrap_alphabet)\n",
+    "    message_bigrams = playfair_bigrams(sanitise(message), padding_letter=padding_letter, \n",
+    "                                       padding_replaces_repeat=padding_replaces_repeat)\n",
+    "    ciphertext_bigrams = [playfair_encipher_bigram(b, grid, padding_letter=padding_letter) for b in message_bigrams]\n",
+    "    return cat(ciphertext_bigrams)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 129,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def playfair_decipher(message, keyword, padding_letter='x',\n",
+    "                      padding_replaces_repeat=False,\n",
+    "#                       column_order=None, row_order=None, \n",
+    "#                       column_first=False, \n",
+    "                      letters_to_merge=None, \n",
+    "                      wrap_alphabet=KeywordWrapAlphabet.from_a):\n",
+    "    column_order = list(range(5))\n",
+    "    row_order = list(range(5))\n",
+    "    if letters_to_merge is None: \n",
+    "        letters_to_merge = {'j': 'i'}   \n",
+    "    grid = polybius_grid(keyword, column_order, row_order,\n",
+    "                        letters_to_merge=letters_to_merge,\n",
+    "                        wrap_alphabet=wrap_alphabet)\n",
+    "    message_bigrams = playfair_bigrams(sanitise(message), padding_letter=padding_letter, \n",
+    "                                       padding_replaces_repeat=padding_replaces_repeat)\n",
+    "    plaintext_bigrams = [playfair_decipher_bigram(b, grid, padding_letter=padding_letter) for b in message_bigrams]\n",
+    "    return cat(plaintext_bigrams)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 130,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "('bmodzbxdnabekudmuixmkzzryi', 'hidethegoldinthetrexstumpx')"
+      ]
+     },
+     "execution_count": 130,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "prr = True\n",
+    "plaintext = 'hide the gold in the tree stump'\n",
+    "key = 'playfair example'\n",
+    "ciphertext = playfair_encipher(plaintext, key, padding_replaces_repeat=prr)\n",
+    "recovered_plaintext = playfair_decipher(ciphertext, key, padding_replaces_repeat=prr)\n",
+    "ciphertext, recovered_plaintext"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 131,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "('bmodzbxdnabekudmuixmmouvif', 'hidethegoldinthetrexestump')"
+      ]
+     },
+     "execution_count": 131,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "prr = False\n",
+    "plaintext = 'hide the gold in the tree stump'\n",
+    "key = 'playfair example'\n",
+    "ciphertext = playfair_encipher(plaintext, key, padding_replaces_repeat=prr)\n",
+    "recovered_plaintext = playfair_decipher(ciphertext, key, padding_replaces_repeat=prr)\n",
+    "ciphertext, recovered_plaintext"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 132,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "('dlckztactiokoncbntaucenzpl', 'hidethegoldinthetrexestump')"
+      ]
+     },
+     "execution_count": 132,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "prr = False\n",
+    "plaintext = 'hide the gold in the tree stump'\n",
+    "key = 'simple key'\n",
+    "ciphertext = playfair_encipher(plaintext, key, padding_replaces_repeat=prr)\n",
+    "recovered_plaintext = playfair_decipher(ciphertext, key, padding_replaces_repeat=prr)\n",
+    "ciphertext, recovered_plaintext"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 134,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def playfair_break_mp(message, \n",
+    "                      letters_to_merge=None, padding_letter='x',\n",
+    "                      wordlist=keywords, fitness=Pletters,\n",
+    "                      number_of_solutions=1, chunksize=500):\n",
+    "    if letters_to_merge is None: \n",
+    "        letters_to_merge = {'j': 'i'}   \n",
+    "\n",
+    "    with multiprocessing.Pool() as pool:\n",
+    "        helper_args = [(message, word, wrap, \n",
+    "                        letters_to_merge, padding_letter,\n",
+    "                        pad_replace,\n",
+    "                        fitness)\n",
+    "                       for word in wordlist\n",
+    "                       for wrap in KeywordWrapAlphabet\n",
+    "                       for pad_replace in [False, True]]\n",
+    "        # Gotcha: the helper function here needs to be defined at the top level\n",
+    "        #   (limitation of Pool.starmap)\n",
+    "        breaks = pool.starmap(playfair_break_worker, helper_args, chunksize)\n",
+    "        if number_of_solutions == 1:\n",
+    "            return max(breaks, key=lambda k: k[1])\n",
+    "        else:\n",
+    "            return sorted(breaks, key=lambda k: k[1], reverse=True)[:number_of_solutions]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 135,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def playfair_break_worker(message, keyword, wrap, \n",
+    "                          letters_to_merge, padding_letter,\n",
+    "                          pad_replace,\n",
+    "                          fitness):\n",
+    "    plaintext = playfair_decipher(message, keyword, padding_letter,\n",
+    "                                  pad_replace,\n",
+    "                                  letters_to_merge, \n",
+    "                                  wrap)\n",
+    "    if plaintext:\n",
+    "        fit = fitness(plaintext)\n",
+    "    else:\n",
+    "        fit = float('-inf')\n",
+    "    logger.debug('Playfair break attempt using key {0} (wrap={1}, merging {2}, '\n",
+    "                 'pad replaces={3}), '\n",
+    "                 'gives fit of {4} and decrypt starting: '\n",
+    "                 '{5}'.format(keyword, wrap, letters_to_merge, pad_replace,\n",
+    "                              fit, sanitise(plaintext)[:50]))\n",
+    "    return (keyword, wrap, letters_to_merge, padding_letter, pad_replace), fit"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 136,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "(('elephant', <KeywordWrapAlphabet.from_a: 1>, {'j': 'i'}, 'x', False),\n",
+       " -54.53880323982303)"
+      ]
+     },
+     "execution_count": 136,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "playfair_break_mp(playfair_encipher('this is a test message for the ' \\\n",
+    "          'polybius decipherment', 'elephant'), \\\n",
+    "          wordlist=['cat', 'elephant', 'kangaroo']) "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 200,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def playfair_simulated_annealing_break(message, workers=10, \n",
+    "                              initial_temperature=200,\n",
+    "                              max_iterations=20000,\n",
+    "                              plain_alphabet=None, \n",
+    "                              cipher_alphabet=None, \n",
+    "                              fitness=Pletters, chunksize=1):\n",
+    "    worker_args = []\n",
+    "    ciphertext = sanitise(message)\n",
+    "    for i in range(workers):\n",
+    "        if plain_alphabet is None:\n",
+    "            used_plain_alphabet = string.ascii_lowercase\n",
+    "        else:\n",
+    "            used_plain_alphabet = plain_alphabet\n",
+    "        if cipher_alphabet is None:\n",
+    "#             used_cipher_alphabet = list(string.ascii_lowercase)\n",
+    "#             random.shuffle(used_cipher_alphabet)\n",
+    "#             used_cipher_alphabet = cat(used_cipher_alphabet)\n",
+    "            used_cipher_alphabet = random.choice(keywords)\n",
+    "        else:\n",
+    "            used_cipher_alphabet = cipher_alphabet\n",
+    "        worker_args.append((ciphertext, used_plain_alphabet, used_cipher_alphabet, \n",
+    "                            initial_temperature, max_iterations, fitness))\n",
+    "    with multiprocessing.Pool() as pool:\n",
+    "        breaks = pool.starmap(playfair_simulated_annealing_break_worker,\n",
+    "                              worker_args, chunksize)\n",
+    "    return max(breaks, key=lambda k: k[1])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 205,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def playfair_simulated_annealing_break_worker(message, plain_alphabet, cipher_alphabet, \n",
+    "                                     t0, max_iterations, fitness):\n",
+    "    def swap(letters, i, j):\n",
+    "        if i > j:\n",
+    "            i, j = j, i\n",
+    "        if i == j:\n",
+    "            return letters\n",
+    "        else:\n",
+    "            return (letters[:i] + letters[j] + letters[i+1:j] + letters[i] +\n",
+    "                    letters[j+1:])\n",
+    "    \n",
+    "    temperature = t0\n",
+    "\n",
+    "    dt = t0 / (0.9 * max_iterations)\n",
+    "    \n",
+    "    current_alphabet = cipher_alphabet\n",
+    "#     current_wrap = KeywordWrapAlphabet.from_a\n",
+    "    current_letters_to_merge = {'j': 'i'}\n",
+    "    current_pad_replace = False\n",
+    "    current_padding_letter = 'x'\n",
+    "    \n",
+    "    alphabet = current_alphabet\n",
+    "#     wrap = current_wrap\n",
+    "    letters_to_merge = current_letters_to_merge\n",
+    "    pad_replace = current_pad_replace\n",
+    "    padding_letter = current_padding_letter\n",
+    "    plaintext = playfair_decipher(message, alphabet, padding_letter,\n",
+    "                                  pad_replace,\n",
+    "                                  letters_to_merge, \n",
+    "                                  KeywordWrapAlphabet.from_a)\n",
+    "    current_fitness = fitness(plaintext)\n",
+    "\n",
+    "    best_alphabet = current_alphabet\n",
+    "#     best_wrap = current_wrap\n",
+    "    best_letters_to_merge = current_letters_to_merge\n",
+    "    best_pad_replace = current_pad_replace\n",
+    "    best_padding_letter = current_padding_letter\n",
+    "    best_fitness = current_fitness\n",
+    "    best_plaintext = plaintext\n",
+    "    \n",
+    "    # print('starting for', max_iterations)\n",
+    "    for i in range(max_iterations):\n",
+    "        chosen = random.random()\n",
+    "#         if chosen < 0.7:\n",
+    "#             swap_a = random.randrange(26)\n",
+    "#             swap_b = (swap_a + int(random.gauss(0, 4))) % 26\n",
+    "#             alphabet = swap(current_alphabet, swap_a, swap_b)\n",
+    "# #         elif chosen < 0.8:\n",
+    "# #             wrap = random.choice(list(KeywordWrapAlphabet))\n",
+    "#         elif chosen < 0.8:\n",
+    "#             pad_replace = random.choice([True, False])\n",
+    "#         elif chosen < 0.9:\n",
+    "#             letter_from = random.choice(string.ascii_lowercase)\n",
+    "#             letter_to = random.choice([c for c in string.ascii_lowercase if c != letter_from])\n",
+    "#             letters_to_merge = {letter_from: letter_to}\n",
+    "#         else:\n",
+    "#             padding_letter = random.choice(string.ascii_lowercase)\n",
+    "\n",
+    "        if chosen < 0.7:\n",
+    "            swap_a = random.randrange(len(current_alphabet))\n",
+    "            swap_b = (swap_a + int(random.gauss(0, 4))) % len(current_alphabet)\n",
+    "            alphabet = swap(current_alphabet, swap_a, swap_b)\n",
+    "        elif chosen < 0.85:\n",
+    "            new_letter = random.choice(string.ascii_lowercase)\n",
+    "            alphabet = swap(current_alphabet + new_letter, random.randrange(len(current_alphabet)), len(current_alphabet))\n",
+    "        else:\n",
+    "            if len(current_alphabet) > 1:\n",
+    "                deletion_position = random.randrange(len(current_alphabet))\n",
+    "                alphabet = current_alphabet[:deletion_position] + current_alphabet[deletion_position+1:]\n",
+    "            else:\n",
+    "                alphabet = current_alphabet\n",
+    "\n",
+    "        try:\n",
+    "            plaintext = playfair_decipher(message, alphabet, padding_letter,\n",
+    "                                  pad_replace,\n",
+    "                                  letters_to_merge, \n",
+    "                                  KeywordWrapAlphabet.from_a)\n",
+    "        except:\n",
+    "            print(\"Error\", alphabet, padding_letter,\n",
+    "                                  pad_replace,\n",
+    "                                  letters_to_merge)\n",
+    "            raise\n",
+    "\n",
+    "        new_fitness = fitness(plaintext)\n",
+    "        try:\n",
+    "            sa_chance = math.exp((new_fitness - current_fitness) / temperature)\n",
+    "        except (OverflowError, ZeroDivisionError):\n",
+    "            # print('exception triggered: new_fit {}, current_fit {}, temp {}'.format(new_fitness, current_fitness, temperature))\n",
+    "            sa_chance = 0\n",
+    "        if (new_fitness > current_fitness or random.random() < sa_chance):\n",
+    "            # logger.debug('Simulated annealing: iteration {}, temperature {}, '\n",
+    "            #     'current alphabet {}, current_fitness {}, '\n",
+    "            #     'best_plaintext {}'.format(i, temperature, current_alphabet, \n",
+    "            #     current_fitness, best_plaintext[:50]))\n",
+    "\n",
+    "            # logger.debug('new_fit {}, current_fit {}, temp {}, sa_chance {}'.format(new_fitness, current_fitness, temperature, sa_chance))\n",
+    "            current_fitness = new_fitness\n",
+    "            current_alphabet = alphabet\n",
+    "#             current_wrap = wrap\n",
+    "            current_letters_to_merge = letters_to_merge\n",
+    "            current_pad_replace = pad_replace\n",
+    "            current_padding_letter = padding_letter\n",
+    "            \n",
+    "        if current_fitness > best_fitness:\n",
+    "            best_alphabet = current_alphabet\n",
+    "#             best_wrap = current_wrap\n",
+    "            best_letters_to_merge = current_letters_to_merge\n",
+    "            best_pad_replace = current_pad_replace\n",
+    "            best_padding_letter = current_padding_letter\n",
+    "            best_fitness = current_fitness\n",
+    "            best_plaintext = plaintext\n",
+    "        if i % 500 == 0:\n",
+    "            logger.debug('Simulated annealing: iteration {}, temperature {}, '\n",
+    "                'current alphabet {}, current_fitness {}, '\n",
+    "                'best_plaintext {}'.format(i, temperature, current_alphabet, \n",
+    "                current_fitness, plaintext[:50]))\n",
+    "        temperature = max(temperature - dt, 0.001)\n",
+    "\n",
+    "    print(best_alphabet, best_plaintext[:50])\n",
+    "    return { 'alphabet': best_alphabet\n",
+    "#            , 'wrap': best_wrap\n",
+    "           , 'letters_to_merge': best_letters_to_merge\n",
+    "           , 'pad_replace': best_pad_replace\n",
+    "           , 'padding_letter': best_padding_letter\n",
+    "           }, best_fitness # current_alphabet, current_fitness"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 149,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'dlckztactiokoncbntaucenzpl'"
+      ]
+     },
+     "execution_count": 149,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "ciphertext"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 168,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "({'alphabet': 'ipknagqxvszjrmhlfyeutwdcbo',\n",
+       "  'wrap': <KeywordWrapAlphabet.from_largest: 3>,\n",
+       "  'letters_to_merge': {'x': 'z'},\n",
+       "  'pad_replace': False,\n",
+       "  'padding_letter': 't'},\n",
+       " -85.75243058399522)"
+      ]
+     },
+     "execution_count": 168,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "key, score = playfair_simulated_annealing_break(ciphertext, fitness=Ptrigrams)\n",
+    "key, score"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 169,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# polybius_grid(key['alphabet'], [1,2,3,4,5], [1,2,3,4,5], letters_to_merge=key['letters_to_merge'], wrap_alphabet=key['wrap'])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 170,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'orecalkofacabadcauntemasar'"
+      ]
+     },
+     "execution_count": 170,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "playfair_decipher(ciphertext, key['alphabet'], key['padding_letter'], key['pad_replace'], key['letters_to_merge'], key['wrap'])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 171,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "('gmearkafusalkufbutbvfeuopl', 'hidethegoldinthetrexestump')"
+      ]
+     },
+     "execution_count": 171,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "prr = False\n",
+    "plaintext = 'hide the gold in the tree stump'\n",
+    "key = 'simple'\n",
+    "ciphertext = playfair_encipher(plaintext, key, padding_replaces_repeat=prr)\n",
+    "recovered_plaintext = playfair_decipher(ciphertext, key, padding_replaces_repeat=prr)\n",
+    "ciphertext, recovered_plaintext"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 174,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "(('simple', <KeywordWrapAlphabet.from_a: 1>, {'j': 'i'}, 'x', False),\n",
+       " -80.29349856508469)"
+      ]
+     },
+     "execution_count": 174,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "playfair_break_mp(ciphertext, fitness=Ptrigrams)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 175,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'hidethegoldinthetrexestump'"
+      ]
+     },
+     "execution_count": 175,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "playfair_decipher(ciphertext, 'simple')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 179,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "({'alphabet': 'rhbylupiwzevjdxfakcqtnomgs',\n",
+       "  'wrap': <KeywordWrapAlphabet.from_a: 1>,\n",
+       "  'letters_to_merge': {'p': 'f'},\n",
+       "  'pad_replace': True,\n",
+       "  'padding_letter': 'a'},\n",
+       " -78.01490096572304)"
+      ]
+     },
+     "execution_count": 179,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "key, score = playfair_simulated_annealing_break(ciphertext, fitness=Ptrigrams, workers=50, max_iterations=int(1e5))\n",
+    "key, score"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 180,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'mouthatventraidleardelines'"
+      ]
+     },
+     "execution_count": 180,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "playfair_decipher(ciphertext, key['alphabet'], key['padding_letter'], key['pad_replace'], key['letters_to_merge'], key['wrap'])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 184,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'the april uprising in bulgaria and its brutal suppression by the turks has caused outrage in the\\nchancelleries of europe there is a risk that russia will take this as the excuse it seeks to engage\\nthe ottomans and if they act and take constantinople then our trading routes to india will be under\\nthreatat home gladstones pamphlet bulgarian horrors and the question of the east has stirred a\\npublic appetite for action which could lead to support for intervention and make things difficult\\nfor the prime minister he is faced with mortons fork if he supports action then it will be difficult\\nto condemn russian interference if he counsels inaction then he risk appearing weak and callous at\\nhome and abroad it may appear unfortunate that our political leaders are unable to agree on policy\\nstrategy or tactics and it is true that this could lead to confusion about our aims but on\\nreflection i think that the public disagreement between gladstone and disraeli presents an\\nopportunity their dispute conducted in parliament and the press demonstrates to the world the two\\nfaces of the empire at the same time morally engaged and yet prudent this may allow us to proceed\\nwith discretion to try to influence the actors and to direct the play away from the glare of the\\nfootlights it may be possible to engage the league of the three emperors to our causebismarck is\\nparticularly keen to maintain a balance of power in the region and to avoid further war and he will\\nnot need to be convinced that an unbridled russia is not to his advantage so i think we can rely on\\nhim to rein in russias expansionary visionon the other hand the league itself may present a longer\\nterm threat to the empire given the breadth of its influence in northern europe and we must tread\\ncarefullythe emperors envoys will be meeting in reichstadt soon to determine the response to the\\ncrisis and i need a plan to influence the outcome as always our strategy must be to sow confusion\\nand on this i plan to ask for advice from baron playfair he has recently concluded his commission of\\nenquiry into the civil service and if anyone knows how to control an agenda it must be our own civil\\nservants'"
+      ]
+     },
+     "execution_count": 184,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "plaintext = open('2018/5b.plaintext').read()\n",
+    "plaintext"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 186,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'beitnicknqecarqsrpmzqlsiakspkratshobgkbnxinirtarpogzbetfnhdaibgrbptrfnobistcrpbeihibqrcffcecrtwohoenoibeieictgecadbegahnavartxckfgkptfrctgtariiwhqtreatriftawtqsgbtfriwffwkbvdspkrofrixgegspfskpihpotaspaeopqktfriopnhseskrpscpnfttapevnakxekymghovniebeeigagaeufhlqsktaportxkkucmtfmzqlsiakurneensdspfsriunrtaepowobeiwittaibavtaceeiksqngmchkxoiaeftowisegepovrchreqqmfmitfsntnqqpesowecosiewroseppsvnkbfiberpbtkrkwkehqfgowesrinihkhfrprafterictdgirfxebefuespotdnepamertnqqpestgegeposriprfeckmgrfekkehqfgfweqvnhfvsnbarsprpftedierohiekrieqnotrdgrpgiaepoberoriecadkxoisirptyitpkvnigkyfqnbgaeufhspksshptkrbfgxkxoisinoowesnogatfibfwnhqpkcaeigkyfcskietgeinogsfcfwgbeitwoqqfchvgsegactwqesgiaergspkraetahntfibawberaeqqmfmitfsqepomoarpogspnfwnhkadbmzfwvstofcegepprberpfaibawbeiozmkcrlragbeihfroastfetrolqsktapoitvnkrdstikcnirtroatsppqqpesnoeawgricekranobihpomnegrfrpxkcdakfhosspfsrinirtdnhfpotaisfttawfriewcdfsrifewogirtwobeiwhfxaeigabertbktfhkhfnegkqcrobgtcksvnwcaohnfrosberakbxgkyfqzotapqenhirfxebekrgreiaepofwsewgpeodmqrohibeitegnetgvnfwkreiegbeiokgxgxtwlenfbrilqsitwofriowwfkcbcateakbzgiontargmtfwtqsgbtfrifcgbohwobetfreiwiwhfoiensdfwpnehbptrahbdsiilraxkeschqmsiqcfirofwkbrpagrpgsgksphiwoqpetecosrieiacpospfsptwnrkmoesrievsispmrteckdqwforrffwtheqvrrphifsibagonusecfmrfhnavarkadvwffwrctgnrspagtctnearcpdetigvscfwqurhkfweirprphnavargtiwxkvdeppscvxrarpopobetwbeieibvnbecfitbqicatcfkdgxnirtroagfqqsiefthdbeeigafwbeiwhfxaeiacwrosrishitfseukeatrpkmohqricpvopesrisvhoenoispfvhfnbawseitlrsitoqmqcbeiwhfoiensdrownvgxekymghfiwtfrpacvsichrtaskatpwpofwfrfthdrptfrieianpotrfwbeihecartgvnrpiwrfkxkgospeodmqrohibetwobeqfhgtgkxtvgpnsdsegactzlnbastfntweeqodnbeppsvnpoberaaxkgosptadowisnrchtoenumsipoqkgxktceriibdsihrogfcgpogqnmrfrcgrufkhavarpoworouncexcoswfrihcxrdgiexrhispkrktqvpoifopvteuefqeposeqfspgbrokseauztathpnenvohcxrdgiexsosav'"
+      ]
+     },
+     "execution_count": 186,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "ciphertext = playfair_encipher(plaintext, 'reichstag')\n",
+    "ciphertext"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 189,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'theapriluprisinginbulgariaanditsbrutalsupxpressionbytheturkshascausedoutrageinthechancelleriesofeuropethereisariskthatrusxsiawilltakethisastheexcuseitseekstoengagetheottomansandiftheyactandtakeconstantinoplethenourtradingroutestoindiawilxlbeunderthreatathomegladstonespamphletbulgarianhorrorsandthequestionofthexeasthasxstirredapublicappetiteforactionwhichcouldleadtosupportforinterventionandmakethingsdifxficultfortheprimeministerheisfacedwithmortonsforkifhesupportsactionthenitwillbedifficulttocondemnrussianinterferenceifhecounselsinactionthenheriskappearingweakandcalxlousathomeandabroaditmayappearunfortunatethatourpoliticalxleadersareunabletoagreeonpolicystrategyortacticsanditistruethatxthiscouldleadtoconfusionaboutouraimsbutonreflectionithinkthatxthepublicdisagreementbetweengladstoneanddisraelipresentsanopportunitytheirdisputeconductedinparliamentandthepressdemonstratestotheworldthetwofacesofthexempireatthesametimemorallyengagedandyetprudentthismayalxlowustoproceedwithdiscretiontotrytoinfluencetheactorsandtodirecttheplayawayfromtheglareofthefootlightsitmaybepossibletoengagetheleagueofthethrexexemperorstoourcausebismarckisparticularlykeentomaintainabalanceofpowerintheregionandtoavoidfurtherwarandhewillnotneedtobeconvincedthatanunbridledrusxsiaisnottohisadvantagesoithinkwecanrelyonhimtoreininrusxsiasexpansionaryvisionontheotherhandtheleagueitselfmaypresentalongertermthreattothexempiregiventhebreadthofitsinfluenceinxnortherneuropeandwemustxtreadcarefullythexemperorsenvoyswilxlbemexetinginreichstadtsoxontodeterminetheresponsetothecrisisandinexedaplantoinfluencetheoutcomeasalwaysourstrategymustbetosowconfusionandonthisiplantoaskforadvicefrombaronplayfairhehasrecentlyconcludedhiscommisxsionofenquiryintothecivilserviceandifanyoneknowshowtocontrolanagendaitmustbeourowncivilservantsx'"
+      ]
+     },
+     "execution_count": 189,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "playfair_decipher(ciphertext, 'reichstag')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 206,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "seidfrts tdeapsamupfttcndanculbfeiaingatregqtmhrqpxpscrtcon\n",
+      "napqbmkvyzgocxuseiftadkianrs atefpsovbpiorenfhazylfterainaebeiduaomqbcfpswereac\n",
+      "qyobxhweskuzpnvmlfgdapxfechdsnvzhbmyubuhiqmnj oscrprtwubwitibhinluymerthensttrekyodsrsotprirtiei\n",
+      "reicbstaigk creaprbguprisinginkulgartgansbtsadueagrupxpression\n",
+      "reichstarg theapriluprisinginbulgariaanditsbrutalsupxpression\n",
+      "reichstaig theapriluprisinginbulgariaanditsbrutalsupxpression\n",
+      "reifhst tfeapramuphtsinbinculbariaanditscrqtdgsqpxpression\n",
+      "stagbreicech theapriluprisinginhulcarxianditsbrutalsupapression\n",
+      "retarhsi itbeosblupaithnctndulcdstlbnfttedrpifgspwboshethon\n",
+      "reichstag theapriluprisinginbulgariaanditsbrutalsupxpression\n"
+     ]
+    },
+    {
+     "data": {
+      "text/plain": [
+       "({'alphabet': 'reichstaig',\n",
+       "  'letters_to_merge': {'j': 'i'},\n",
+       "  'pad_replace': False,\n",
+       "  'padding_letter': 'x'},\n",
+       " -6173.17111571937)"
+      ]
+     },
+     "execution_count": 206,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "key, score = playfair_simulated_annealing_break(ciphertext, fitness=Ptrigrams, workers=10, max_iterations=int(1e5), initial_temperature=500)\n",
+    "key, score"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.6.6"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}