Transposition ciphers working
[cipher-tools.git] / cipher.py
index 84a4915e0f61952215671a42fe1aee81268ca259..d2619029de591af1d07e5f85160e187629332751 100644 (file)
--- a/cipher.py
+++ b/cipher.py
@@ -1,18 +1,8 @@
 import string
 import collections
 import string
 import collections
-import norms
 import logging
 import logging
-import math
-from itertools import zip_longest, repeat, cycle
-from segment import segment
-from multiprocessing import Pool
-
-# To time a run:
-#
-# import timeit
-# c5a = open('2012/5a.ciphertext', 'r').read()
-# timeit.timeit('keyword_break(c5a)', setup='gc.enable() ; from __main__ import c5a ; from cipher import keyword_break', number=1)
-# timeit.repeat('keyword_break_mp(c5a, chunksize=500)', setup='gc.enable() ; from __main__ import c5a ; from cipher import keyword_break_mp', repeat=5, number=1
+from itertools import zip_longest, cycle, chain
+from language_models import *
 
 logger = logging.getLogger(__name__)
 logger.addHandler(logging.FileHandler('cipher.log'))
 
 logger = logging.getLogger(__name__)
 logger.addHandler(logging.FileHandler('cipher.log'))
@@ -20,57 +10,13 @@ logger.setLevel(logging.WARNING)
 #logger.setLevel(logging.INFO)
 #logger.setLevel(logging.DEBUG)
 
 #logger.setLevel(logging.INFO)
 #logger.setLevel(logging.DEBUG)
 
-english_counts = collections.defaultdict(int)
-with open('count_1l.txt', 'r') as f:
-    for line in f:
-        (letter, count) = line.split("\t")
-        english_counts[letter] = int(count)
-normalised_english_counts = norms.normalise(english_counts)
 
 
-english_bigram_counts = collections.defaultdict(int)
-with open('count_2l.txt', 'r') as f:
-    for line in f:
-        (bigram, count) = line.split("\t")
-        english_bigram_counts[bigram] = int(count)
-normalised_english_bigram_counts = norms.normalise(english_bigram_counts)
-
-english_trigram_counts = collections.defaultdict(int)
-with open('count_3l.txt', 'r') as f:
-    for line in f:
-        (trigram, count) = line.split("\t")
-        english_trigram_counts[trigram] = int(count)
-normalised_english_trigram_counts = norms.normalise(english_trigram_counts)
-
-
-with open('words.txt', 'r') as f:
-    keywords = [line.rstrip() for line in f]
-
-modular_division_table = [[0]*26 for x in range(26)]
+modular_division_table = [[0]*26 for _ in range(26)]
 for a in range(26):
     for b in range(26):
         c = (a * b) % 26
         modular_division_table[b][c] = a
 
 for a in range(26):
     for b in range(26):
         c = (a * b) % 26
         modular_division_table[b][c] = a
 
-def letters(text):
-    """Remove all non-alphabetic characters from a text
-    >>> letters('The Quick')
-    'TheQuick'
-    >>> letters('The Quick BROWN fox jumped! over... the (9lazy) DOG')
-    'TheQuickBROWNfoxjumpedoverthelazyDOG'
-    """
-    return ''.join([c for c in text if c in string.ascii_letters])
-
-def sanitise(text):
-    """Remove all non-alphabetic characters and convert the text to lowercase
-    
-    >>> sanitise('The Quick')
-    'thequick'
-    >>> sanitise('The Quick BROWN fox jumped! over... the (9lazy) DOG')
-    'thequickbrownfoxjumpedoverthelazydog'
-    """
-    # sanitised = [c.lower() for c in text if c in string.ascii_letters]
-    # return ''.join(sanitised)
-    return letters(text).lower()
 
 def ngrams(text, n):
     """Returns all n-grams of a text
 
 def ngrams(text, n):
     """Returns all n-grams of a text
@@ -114,17 +60,33 @@ def combine_every_nth(split_text):
     return ''.join([''.join(l) 
                     for l in zip_longest(*split_text, fillvalue='')])
 
     return ''.join([''.join(l) 
                     for l in zip_longest(*split_text, fillvalue='')])
 
+def chunks(text, n, fillvalue=None):
+    """Split a text into chunks of n characters
+
+    >>> chunks('abcdefghi', 3)
+    ['abc', 'def', 'ghi']
+    >>> chunks('abcdefghi', 4)
+    ['abcd', 'efgh', 'i']
+    >>> chunks('abcdefghi', 4, fillvalue='!')
+    ['abcd', 'efgh', 'i!!!']
+    """
+    if fillvalue:
+        padding = fillvalue[0] * (n - len(text) % n)
+    else:
+        padding = ''
+    return [(text+padding)[i:i+n] for i in range(0, len(text), n)]
+
 def transpose(items, transposition):
     """Moves items around according to the given transposition
     
 def transpose(items, transposition):
     """Moves items around according to the given transposition
     
-    >>> transpose(['a', 'b', 'c', 'd'], [0,1,2,3])
+    >>> transpose(['a', 'b', 'c', 'd'], (0,1,2,3))
     ['a', 'b', 'c', 'd']
     ['a', 'b', 'c', 'd']
-    >>> transpose(['a', 'b', 'c', 'd'], [3,1,2,0])
+    >>> transpose(['a', 'b', 'c', 'd'], (3,1,2,0))
     ['d', 'b', 'c', 'a']
     ['d', 'b', 'c', 'a']
-    >>> transpose([10,11,12,13,14,15], [3,2,4,1,5,0])  
+    >>> transpose([10,11,12,13,14,15], (3,2,4,1,5,0))
     [13, 12, 14, 11, 15, 10]
     """
     [13, 12, 14, 11, 15, 10]
     """
-    transposed = list(repeat('', len(transposition)))
+    transposed = [''] * len(transposition)
     for p, t in enumerate(transposition):
        transposed[p] = items[t]
     return transposed
     for p, t in enumerate(transposition):
        transposed[p] = items[t]
     return transposed
@@ -139,51 +101,15 @@ def untranspose(items, transposition):
     >>> untranspose([13, 12, 14, 11, 15, 10], [3,2,4,1,5,0])
     [10, 11, 12, 13, 14, 15]
     """
     >>> untranspose([13, 12, 14, 11, 15, 10], [3,2,4,1,5,0])
     [10, 11, 12, 13, 14, 15]
     """
-    transposed  = list(repeat('', len(transposition)))
+    transposed = [''] * len(transposition)
     for p, t in enumerate(transposition):
        transposed[t] = items[p]
     return transposed
 
     for p, t in enumerate(transposition):
        transposed[t] = items[p]
     return transposed
 
-
-def frequencies(text):
-    """Count the number of occurrences of each character in text
-    
-    >>> sorted(frequencies('abcdefabc').items())
-    [('a', 2), ('b', 2), ('c', 2), ('d', 1), ('e', 1), ('f', 1)]
-    >>> sorted(frequencies('the quick brown fox jumped over the lazy ' \
-         'dog').items()) # doctest: +NORMALIZE_WHITESPACE
-    [(' ', 8), ('a', 1), ('b', 1), ('c', 1), ('d', 2), ('e', 4), ('f', 1), 
-     ('g', 1), ('h', 2), ('i', 1), ('j', 1), ('k', 1), ('l', 1), ('m', 1), 
-     ('n', 1), ('o', 4), ('p', 1), ('q', 1), ('r', 2), ('t', 2), ('u', 2), 
-     ('v', 1), ('w', 1), ('x', 1), ('y', 1), ('z', 1)]
-    >>> sorted(frequencies('The Quick BROWN fox jumped! over... the ' \
-         '(9lazy) DOG').items()) # doctest: +NORMALIZE_WHITESPACE
-    [(' ', 8), ('!', 1), ('(', 1), (')', 1), ('.', 3), ('9', 1), ('B', 1), 
-     ('D', 1), ('G', 1), ('N', 1), ('O', 2), ('Q', 1), ('R', 1), ('T', 1), 
-     ('W', 1), ('a', 1), ('c', 1), ('d', 1), ('e', 4), ('f', 1), ('h', 2), 
-     ('i', 1), ('j', 1), ('k', 1), ('l', 1), ('m', 1), ('o', 2), ('p', 1), 
-     ('r', 1), ('t', 1), ('u', 2), ('v', 1), ('x', 1), ('y', 1), ('z', 1)]
-    >>> sorted(frequencies(sanitise('The Quick BROWN fox jumped! over... ' \
-         'the (9lazy) DOG')).items()) # doctest: +NORMALIZE_WHITESPACE
-    [('a', 1), ('b', 1), ('c', 1), ('d', 2), ('e', 4), ('f', 1), ('g', 1), 
-     ('h', 2), ('i', 1), ('j', 1), ('k', 1), ('l', 1), ('m', 1), ('n', 1), 
-     ('o', 4), ('p', 1), ('q', 1), ('r', 2), ('t', 2), ('u', 2), ('v', 1), 
-     ('w', 1), ('x', 1), ('y', 1), ('z', 1)]
-    >>> frequencies('abcdefabcdef')['x']
-    0
-    """
-    #counts = collections.defaultdict(int)
-    #for c in text: 
-    #    counts[c] += 1
-    #return counts
-    return collections.Counter(c for c in text)
-letter_frequencies = frequencies
-
 def deduplicate(text):
     return list(collections.OrderedDict.fromkeys(text))
 
 
 def deduplicate(text):
     return list(collections.OrderedDict.fromkeys(text))
 
 
-
 def caesar_encipher_letter(letter, shift):
     """Encipher a letter, given a shift amount
 
 def caesar_encipher_letter(letter, shift):
     """Encipher a letter, given a shift amount
 
@@ -291,8 +217,9 @@ def affine_decipher_letter(letter, multiplier=1, adder=0, one_based=True):
             alphabet_start = ord('a')
         cipher_number = ord(letter) - alphabet_start
         if one_based: cipher_number += 1
             alphabet_start = ord('a')
         cipher_number = ord(letter) - alphabet_start
         if one_based: cipher_number += 1
-        plaintext_number = ( modular_division_table[multiplier]
-                                                   [(cipher_number - adder) % 26] )
+        plaintext_number = ( 
+            modular_division_table[multiplier]
+                                  [(cipher_number - adder) % 26] )
         if one_based: plaintext_number -= 1
         return chr(plaintext_number % 26 + alphabet_start) 
     else:
         if one_based: plaintext_number -= 1
         return chr(plaintext_number % 26 + alphabet_start) 
     else:
@@ -445,56 +372,124 @@ def transpositions_of(keyword):
     second column (1) moves to third, the third column (2) moves to second, 
     and so on.
 
     second column (1) moves to third, the third column (2) moves to second, 
     and so on.
 
+    If passed a tuple, assume it's already a transposition and just return it.
+
     >>> transpositions_of('clever')
     >>> transpositions_of('clever')
-    [0, 2, 1, 4, 3]
-    """
-    key = deduplicate(keyword)
-    transpositions = [key.index(l) for l in sorted(key)]
-    return transpositions
+    (0, 2, 1, 4, 3)
+    >>> transpositions_of('fred')
+    (3, 2, 0, 1)
+    >>> transpositions_of((3, 2, 0, 1))
+    (3, 2, 0, 1)
+    """
+    if isinstance(keyword, tuple):
+        return keyword
+    else:
+        key = deduplicate(keyword)
+        transpositions = tuple(key.index(l) for l in sorted(key))
+        return transpositions
+
+def pad(message_len, group_len, fillvalue):
+    padding_length = group_len - message_len % group_len
+    if padding_length == group_len: padding_length = 0
+    padding = ''
+    for i in range(padding_length):
+        if callable(fillvalue):
+            padding += fillvalue()
+        else:
+            padding += fillvalue
+    return padding
 
 
-def column_transposition_encipher(message, keyword, fillvalue=' '):
+def column_transposition_encipher(message, keyword, fillvalue=' ', 
+      fillcolumnwise=False,
+      emptycolumnwise=False):
     """Enciphers using the column transposition cipher.
     Message is padded to allow all rows to be the same length.
 
     """Enciphers using the column transposition cipher.
     Message is padded to allow all rows to be the same length.
 
-    >>> column_transposition_encipher('hellothere', 'clever')
+    >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True)
+    'hlohr eltee '
+    >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True, emptycolumnwise=True)
+    'hellothere  '
+    >>> column_transposition_encipher('hellothere', 'abcdef')
+    'hellothere  '
+    >>> column_transposition_encipher('hellothere', 'abcde')
+    'hellothere'
+    >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
+    'hellothere'
+    >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
+    'hlohreltee'
+    >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
+    'htehlelroe'
+    >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
+    'hellothere'
+    >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=True)
+    'heotllrehe'
+    >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=False)
+    'holrhetlee'
+    >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=True)
+    'htleehoelr'
+    >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=False)
     'hleolteher'
     'hleolteher'
+    >>> column_transposition_encipher('hellothere', 'cleverly')
+    'hleolthre e '
     >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue='!')
     'hleolthre!e!'
     >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue='!')
     'hleolthre!e!'
+    >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue=lambda: '*')
+    'hleolthre*e*'
     """
     """
-    return column_transposition_worker(message, keyword, encipher=True, 
-                                       fillvalue=fillvalue)
+    transpositions = transpositions_of(keyword)
+    message += pad(len(message), len(transpositions), fillvalue)
+    if fillcolumnwise:
+        rows = every_nth(message, len(message) // len(transpositions))
+    else:
+        rows = chunks(message, len(transpositions))
+    transposed = [transpose(r, transpositions) for r in rows]
+    if emptycolumnwise:
+        return combine_every_nth(transposed)
+    else:
+        return ''.join(chain(*transposed))
 
 
-def column_transposition_decipher(message, keyword, fillvalue=' '):
+def column_transposition_decipher(message, keyword, fillvalue=' ', 
+      fillcolumnwise=False,
+      emptycolumnwise=False):
     """Deciphers using the column transposition cipher.
     Message is padded to allow all rows to be the same length.
 
     """Deciphers using the column transposition cipher.
     Message is padded to allow all rows to be the same length.
 
-    >>> column_transposition_decipher('hleolteher', 'clever')
+    >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
     'hellothere'
     'hellothere'
-    >>> column_transposition_decipher('hleolthre!e!', 'cleverly', fillvalue='?')
-    'hellothere!!'
-    """
-    return column_transposition_worker(message, keyword, encipher=False, 
-                                       fillvalue=fillvalue)
-
-def column_transposition_worker(message, keyword, 
-                                encipher=True, fillvalue=' '):
-    """Does the actual work of the column transposition cipher.
-    Message is padded with spaces to allow all rows to be the same length.
-
-    >>> column_transposition_worker('hellothere', 'clever')
-    'hleolteher'
-    >>> column_transposition_worker('hellothere', 'clever', encipher=True)
-    'hleolteher'
-    >>> column_transposition_worker('hleolteher', 'clever', encipher=False)
+    >>> column_transposition_decipher('hlohreltee', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
+    'hellothere'
+    >>> column_transposition_decipher('htehlelroe', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
+    'hellothere'
+    >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
+    'hellothere'
+    >>> column_transposition_decipher('heotllrehe', 'clever', fillcolumnwise=True, emptycolumnwise=True)
+    'hellothere'
+    >>> column_transposition_decipher('holrhetlee', 'clever', fillcolumnwise=True, emptycolumnwise=False)
+    'hellothere'
+    >>> column_transposition_decipher('htleehoelr', 'clever', fillcolumnwise=False, emptycolumnwise=True)
+    'hellothere'
+    >>> column_transposition_decipher('hleolteher', 'clever', fillcolumnwise=False, emptycolumnwise=False)
     'hellothere'
     """
     'hellothere'
     """
+    # >>> column_transposition_decipher('hleolteher', 'clever')
+    # 'hellothere'
+    # >>> column_transposition_decipher('hleolthre!e!', 'cleverly', fillvalue='?')
+    # 'hellothere!!'
+    # >>> column_transposition_decipher('htleehoelr', 'clever', columnwise=True)
+    # 'hellothere'
+    # """
     transpositions = transpositions_of(keyword)
     transpositions = transpositions_of(keyword)
-    columns = every_nth(message, len(transpositions), fillvalue=fillvalue)
-    if encipher:
-        transposed_columns = transpose(columns, transpositions)
+    message += pad(len(message), len(transpositions), '*')
+    if emptycolumnwise:
+        rows = every_nth(message, len(message) // len(transpositions))
+    else:
+        rows = chunks(message, len(transpositions))
+    untransposed = [untranspose(r, transpositions) for r in rows]
+    if fillcolumnwise:
+        return combine_every_nth(untransposed)
     else:
     else:
-        transposed_columns = untranspose(columns, transpositions)
-    return combine_every_nth(transposed_columns)
+        return ''.join(chain(*untransposed))
+
 
 def vigenere_encipher(message, keyword):
     """Vigenere encipher
 
 def vigenere_encipher(message, keyword):
     """Vigenere encipher
@@ -516,339 +511,8 @@ def vigenere_decipher(message, keyword):
     pairs = zip(message, cycle(shifts))
     return ''.join([caesar_decipher_letter(l, k) for l, k in pairs])
 
     pairs = zip(message, cycle(shifts))
     return ''.join([caesar_decipher_letter(l, k) for l, k in pairs])
 
-
-
-def caesar_break(message, 
-                 metric=norms.euclidean_distance, 
-                 target_counts=normalised_english_counts, 
-                 message_frequency_scaling=norms.normalise):
-    """Breaks a Caesar cipher using frequency analysis
-    
-    >>> caesar_break('ibxcsyorsaqcheyklxivoexlevmrimwxsfiqevvmihrsasrxliwyrh' \
-          'ecjsppsamrkwleppfmergefifvmhixscsymjcsyqeoixlm') # doctest: +ELLIPSIS
-    (4, 0.31863952890183...)
-    >>> caesar_break('wxwmaxdgheetgwuxztgptedbgznitgwwhpguxyhkxbmhvvtlbhgtee' \
-          'raxlmhiixweblmxgxwmhmaxybkbgztgwztsxwbgmxgmert') # doctest: +ELLIPSIS
-    (19, 0.42152901235832...)
-    >>> caesar_break('yltbbqnqnzvguvaxurorgenafsbezqvagbnornfgsbevpnaabjurer' \
-          'svaquvzyvxrnznazlybequrvfohgriraabjtbaruraprur') # doctest: +ELLIPSIS
-    (13, 0.316029208075451...)
-    """
-    sanitised_message = sanitise(message)
-    best_shift = 0
-    best_fit = float("inf")
-    for shift in range(26):
-        plaintext = caesar_decipher(sanitised_message, shift)
-        counts = message_frequency_scaling(letter_frequencies(plaintext))
-        fit = metric(target_counts, counts)
-        logger.debug('Caesar break attempt using key {0} gives fit of {1} '
-                      'and decrypt starting: {2}'.format(shift, fit, plaintext[:50]))
-        if fit < best_fit:
-            best_fit = fit
-            best_shift = shift
-    logger.info('Caesar break best fit: key {0} gives fit of {1} and '
-                'decrypt starting: {2}'.format(best_shift, best_fit, 
-                    caesar_decipher(sanitised_message, best_shift)[:50]))
-    return best_shift, best_fit
-
-def affine_break(message, 
-                 metric=norms.euclidean_distance, 
-                 target_counts=normalised_english_counts, 
-                 message_frequency_scaling=norms.normalise):
-    """Breaks an affine cipher using frequency analysis
-    
-    >>> affine_break('lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg jfaoe ls ' \
-          'omytd jlaxe mh jm bfmibj umis hfsul axubafkjamx. ls kffkxwsd jls ' \
-          'ofgbjmwfkiu olfmxmtmwaokttg jlsx ls kffkxwsd jlsi zg tsxwjl. jlsx ' \
-          'ls umfjsd jlsi zg hfsqysxog. ls dmmdtsd mx jls bats mh bkbsf. ls ' \
-          'bfmctsd kfmyxd jls lyj, mztanamyu xmc jm clm cku tmmeaxw kj lai kxd ' \
-          'clm ckuxj.') # doctest: +ELLIPSIS
-    ((15, 22, True), 0.23570361818655...)
-    """
-    sanitised_message = sanitise(message)
-    best_multiplier = 0
-    best_adder = 0
-    best_one_based = True
-    best_fit = float("inf")
-    for one_based in [True, False]:
-        for multiplier in range(1, 26, 2):
-            for adder in range(26):
-                plaintext = affine_decipher(sanitised_message, 
-                                            multiplier, adder, one_based)
-                counts = message_frequency_scaling(letter_frequencies(plaintext))
-                fit = metric(target_counts, counts)
-                logger.debug('Affine break attempt using key {0}x+{1} ({2}) '
-                             'gives fit of {3} and decrypt starting: {4}'.
-                             format(multiplier, adder, one_based, fit, 
-                                    plaintext[:50]))
-                if fit < best_fit:
-                    best_fit = fit
-                    best_multiplier = multiplier
-                    best_adder = adder
-                    best_one_based = one_based
-    logger.info('Affine break best fit with key {0}x+{1} ({2}) gives fit of {3} '
-                'and decrypt starting: {4}'.format(
-                    best_multiplier, best_adder, best_one_based, best_fit, 
-                    affine_decipher(sanitised_message, best_multiplier, 
-                        best_adder, best_one_based)[:50]))
-    return (best_multiplier, best_adder, best_one_based), best_fit
-
-def keyword_break(message, 
-                  wordlist=keywords, 
-                  metric=norms.euclidean_distance, 
-                  target_counts=normalised_english_counts, 
-                  message_frequency_scaling=norms.normalise):
-    """Breaks a keyword substitution cipher using a dictionary and 
-    frequency analysis
-
-    >>> keyword_break(keyword_encipher('this is a test message for the ' \
-          'keyword decipherment', 'elephant', 1), \
-          wordlist=['cat', 'elephant', 'kangaroo']) # doctest: +ELLIPSIS
-    (('elephant', 1), 0.41643991598441...)
-    """
-    best_keyword = ''
-    best_wrap_alphabet = True
-    best_fit = float("inf")
-    for wrap_alphabet in range(3):
-        for keyword in wordlist:
-            plaintext = keyword_decipher(message, keyword, wrap_alphabet)
-            counts = message_frequency_scaling(letter_frequencies(plaintext))
-            fit = metric(target_counts, counts)
-            logger.debug('Keyword break attempt using key {0} (wrap={1}) '
-                         'gives fit of {2} and decrypt starting: {3}'.format(
-                             keyword, wrap_alphabet, fit, 
-                             sanitise(plaintext)[:50]))
-            if fit < best_fit:
-                best_fit = fit
-                best_keyword = keyword
-                best_wrap_alphabet = wrap_alphabet
-    logger.info('Keyword break best fit with key {0} (wrap={1}) gives fit of '
-                '{2} and decrypt starting: {3}'.format(best_keyword, 
-                    best_wrap_alphabet, best_fit, sanitise(
-                        keyword_decipher(message, best_keyword, 
-                                         best_wrap_alphabet))[:50]))
-    return (best_keyword, best_wrap_alphabet), best_fit
-
-def keyword_break_mp(message, 
-                     wordlist=keywords, 
-                     metric=norms.euclidean_distance, 
-                     target_counts=normalised_english_counts, 
-                     message_frequency_scaling=norms.normalise, 
-                     chunksize=500):
-    """Breaks a keyword substitution cipher using a dictionary and 
-    frequency analysis
-
-    >>> keyword_break_mp(keyword_encipher('this is a test message for the ' \
-          'keyword decipherment', 'elephant', 1), \
-          wordlist=['cat', 'elephant', 'kangaroo']) # doctest: +ELLIPSIS
-    (('elephant', 1), 0.41643991598441...)
-    """
-    with Pool() as pool:
-        helper_args = [(message, word, wrap, metric, target_counts, 
-                        message_frequency_scaling) 
-                       for word in wordlist for wrap in range(3)]
-        # Gotcha: the helper function here needs to be defined at the top level 
-        #   (limitation of Pool.starmap)
-        breaks = pool.starmap(keyword_break_worker, helper_args, chunksize) 
-        return min(breaks, key=lambda k: k[1])
-
-def keyword_break_worker(message, keyword, wrap_alphabet, metric, target_counts, 
-                      message_frequency_scaling):
-    plaintext = keyword_decipher(message, keyword, wrap_alphabet)
-    counts = message_frequency_scaling(letter_frequencies(plaintext))
-    fit = metric(target_counts, counts)
-    logger.debug('Keyword break attempt using key {0} (wrap={1}) gives fit of '
-                 '{2} and decrypt starting: {3}'.format(keyword, 
-                     wrap_alphabet, fit, sanitise(plaintext)[:50]))
-    return (keyword, wrap_alphabet), fit
-
-def scytale_break(message, 
-                  metric=norms.euclidean_distance, 
-                  target_counts=normalised_english_bigram_counts, 
-                  message_frequency_scaling=norms.normalise):
-    """Breaks a Scytale cipher
-    
-    >>> scytale_break('tfeulchtrtteehwahsdehneoifeayfsondmwpltmaoalhikotoere' \
-           'dcweatehiplwxsnhooacgorrcrcraotohsgullasenylrendaianeplscdriioto' \
-           'aek') # doctest: +ELLIPSIS
-    (6, 0.83453041115025...)
-    """
-    best_key = 0
-    best_fit = float("inf")
-    ngram_length = len(next(iter(target_counts.keys())))
-    for key in range(1, 20):
-        if len(message) % key == 0:
-            plaintext = scytale_decipher(message, key)
-            counts = message_frequency_scaling(frequencies(
-                         ngrams(sanitise(plaintext), ngram_length)))
-            fit = metric(target_counts, counts)
-            logger.debug('Scytale break attempt using key {0} gives fit of '
-                         '{1} and decrypt starting: {2}'.format(key, 
-                             fit, sanitise(plaintext)[:50]))
-            if fit < best_fit:
-                best_fit = fit
-                best_key = key
-    logger.info('Scytale break best fit with key {0} gives fit of {1} and '
-                'decrypt starting: {2}'.format(best_key, best_fit, 
-                    sanitise(scytale_decipher(message, best_key))[:50]))
-    return best_key, best_fit
-
-def column_transposition_break(message, 
-                  wordlist=keywords, 
-                  metric=norms.euclidean_distance, 
-                  target_counts=normalised_english_bigram_counts, 
-                  message_frequency_scaling=norms.normalise):
-    """Breaks a column transposition cipher using a dictionary and 
-    n-gram frequency analysis
-
-    >>> column_transposition_break(column_transposition_encipher(sanitise( \
-        "Turing's homosexuality resulted in a criminal prosecution in 1952, \
-        when homosexual acts were still illegal in the United Kingdom. "), \
-        'encipher'), \
-        wordlist=['encipher', 'keyword', 'fourteen']) # doctest: +ELLIPSIS
-    ('encipher', 0.898128626285...)
-    >>> column_transposition_break(column_transposition_encipher(sanitise( \
-        "Turing's homosexuality resulted in a criminal prosecution in 1952, " \
-        "when homosexual acts were still illegal in the United Kingdom."), \
-        'encipher'), \
-        wordlist=['encipher', 'keyword', 'fourteen'], \
-        target_counts=normalised_english_trigram_counts) # doctest: +ELLIPSIS
-    ('encipher', 1.1958792913127...)
-    """
-    best_keyword = ''
-    best_fit = float("inf")
-    ngram_length = len(next(iter(target_counts.keys())))
-    for keyword in wordlist:
-        if len(message) % len(deduplicate(keyword)) == 0:
-            plaintext = column_transposition_decipher(message, keyword)
-            counts = message_frequency_scaling(frequencies(
-                         ngrams(sanitise(plaintext), ngram_length)))
-            fit = metric(target_counts, counts)
-            logger.debug('Column transposition break attempt using key {0} '
-                         'gives fit of {1} and decrypt starting: {2}'.format(
-                             keyword, fit, 
-                             sanitise(plaintext)[:50]))
-            if fit < best_fit:
-                best_fit = fit
-                best_keyword = keyword
-    logger.info('Column transposition break best fit with key {0} gives fit '
-                'of {1} and decrypt starting: {2}'.format(best_keyword, 
-                    best_fit, sanitise(
-                        column_transposition_decipher(message, 
-                            best_keyword))[:50]))
-    return best_keyword, best_fit
-
-
-def column_transposition_break_mp(message, 
-                     wordlist=keywords, 
-                     metric=norms.euclidean_distance, 
-                     target_counts=normalised_english_bigram_counts, 
-                     message_frequency_scaling=norms.normalise, 
-                     chunksize=500):
-    """Breaks a column transposition cipher using a dictionary and 
-    n-gram frequency analysis
-
-    >>> column_transposition_break_mp(column_transposition_encipher(sanitise( \
-        "Turing's homosexuality resulted in a criminal prosecution in 1952, \
-        when homosexual acts were still illegal in the United Kingdom. "), \
-        'encipher'), \
-        wordlist=['encipher', 'keyword', 'fourteen']) # doctest: +ELLIPSIS
-    ('encipher', 0.898128626285...)
-    >>> column_transposition_break_mp(column_transposition_encipher(sanitise( \
-        "Turing's homosexuality resulted in a criminal prosecution in 1952, " \
-        "when homosexual acts were still illegal in the United Kingdom."), \
-        'encipher'), \
-        wordlist=['encipher', 'keyword', 'fourteen'], \
-        target_counts=normalised_english_trigram_counts) # doctest: +ELLIPSIS
-    ('encipher', 1.1958792913127...)
-    """
-    ngram_length = len(next(iter(target_counts.keys())))
-    with Pool() as pool:
-        helper_args = [(message, word, metric, target_counts, ngram_length,
-                        message_frequency_scaling) 
-                       for word in wordlist]
-        # Gotcha: the helper function here needs to be defined at the top level 
-        #   (limitation of Pool.starmap)
-        breaks = pool.starmap(column_transposition_break_worker, helper_args, chunksize) 
-        return min(breaks, key=lambda k: k[1])
-
-def column_transposition_break_worker(message, keyword, metric, target_counts, 
-                      ngram_length, message_frequency_scaling):
-    plaintext = column_transposition_decipher(message, keyword)
-    counts = message_frequency_scaling(frequencies(
-                         ngrams(sanitise(plaintext), ngram_length)))
-    fit = metric(target_counts, counts)
-    logger.debug('Column transposition break attempt using key {0} '
-                         'gives fit of {1} and decrypt starting: {2}'.format(
-                             keyword, fit, 
-                             sanitise(plaintext)[:50]))
-    return keyword, fit
-
-def vigenere_keyword_break(message, 
-                  wordlist=keywords, 
-                  metric=norms.euclidean_distance, 
-                  target_counts=normalised_english_counts, 
-                  message_frequency_scaling=norms.normalise):
-    """Breaks a vigenere cipher using a dictionary and 
-    frequency analysis
-    
-    >>> vigenere_keyword_break(keyword_encipher('this is a test message for the ' \
-             'keyword decipherment', 'elephant', 1), \
-             wordlist=['cat', 'elephant', 'kangaroo']) # doctest: +ELLIPSIS
-    ('elephant', 0.7166585201707...)
-    """
-    best_keyword = ''
-    best_fit = float("inf")
-    for keyword in wordlist:
-        plaintext = vigenere_decipher(message, keyword)
-        counts = message_frequency_scaling(letter_frequencies(plaintext))
-        fit = metric(target_counts, counts)
-        logger.debug('Vigenere break attempt using key {0} '
-                         'gives fit of {1} and decrypt starting: {2}'.format(
-                             keyword, fit, 
-                             sanitise(plaintext)[:50]))
-        if fit < best_fit:
-            best_fit = fit
-            best_keyword = keyword
-    logger.info('Vigenere break best fit with key {0} gives fit '
-                'of {1} and decrypt starting: {2}'.format(best_keyword, 
-                    best_fit, sanitise(
-                        vigenere_decipher(message, best_keyword))[:50]))
-    return best_keyword, best_fit
-
-def vigenere_keyword_break_mp(message, 
-                     wordlist=keywords, 
-                     metric=norms.euclidean_distance, 
-                     target_counts=normalised_english_counts, 
-                     message_frequency_scaling=norms.normalise, 
-                     chunksize=500):
-    """Breaks a vigenere cipher using a dictionary and 
-    frequency analysis
-
-    >>> vigenere_keyword_break_mp(keyword_encipher('this is a test message for the ' \
-             'keyword decipherment', 'elephant', 1), \
-             wordlist=['cat', 'elephant', 'kangaroo']) # doctest: +ELLIPSIS
-    ('elephant', 0.7166585201707...)
-    """
-    with Pool() as pool:
-        helper_args = [(message, word, metric, target_counts, 
-                        message_frequency_scaling) 
-                       for word in wordlist]
-        # Gotcha: the helper function here needs to be defined at the top level 
-        #   (limitation of Pool.starmap)
-        breaks = pool.starmap(vigenere_keyword_break_worker, helper_args, chunksize) 
-        return min(breaks, key=lambda k: k[1])
-
-def vigenere_keyword_break_worker(message, keyword, metric, target_counts, 
-                      message_frequency_scaling):
-    plaintext = vigenere_decipher(message, keyword)
-    counts = message_frequency_scaling(letter_frequencies(plaintext))
-    fit = metric(target_counts, counts)
-    logger.debug('Vigenere keyword break attempt using key {0} gives fit of '
-                 '{1} and decrypt starting: {2}'.format(keyword, 
-                     fit, sanitise(plaintext)[:50]))
-    return keyword, fit
-
+beaufort_encipher=vigenere_decipher
+beaufort_decipher=vigenere_encipher
 
 
 if __name__ == "__main__":
 
 
 if __name__ == "__main__":