Minor documentation updates
[szyfrow.git] / szyfrow / column_transposition.py
index 7e0fc28a3d002208ccfd0ea0c6958216aa61fa09..687fe873fa8aecbddbebe8b5314ca7606e32daeb 100644 (file)
@@ -1,49 +1,26 @@
-import math
-import multiprocessing 
-from itertools import chain
-from support.utilities import *
-from support.language_models import *
-
-from logger import logger
-
-def transpositions_of(keyword):
-    """Finds the transpostions given by a keyword. For instance, the keyword
-    'clever' rearranges to 'celrv', so the first column (0) stays first, the
-    second column (1) moves to third, the third column (2) moves to second, 
-    and so on.
+"""Enciphering and deciphering using the [Column transposition cipher](https://en.wikipedia.org/wiki/Bifid_cipher). 
+Also attempts to break messages that use a column transpositon cipher.
 
 
-    If passed a tuple, assume it's already a transposition and just return it.
-
-    >>> transpositions_of('clever')
-    (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
+A grid is layed out, with one column for each distinct letter in the keyword.
+The grid is filled by the plaintext, one letter per cell, either in rows or
+columns. The columns are rearranged so the keyword's letters are in alphabetical
+order, then the ciphertext is read from the rearranged grid, either in rows
+or columns. 
 
 
+The Scytale cipher is a column cipher with an identity transposition, where the 
+message is written in rows and read in columns. 
 
 
-transpositions = collections.defaultdict(list)
-for word in keywords:
-    transpositions[transpositions_of(word)] += [word]
+Messages that do not fill the grid are padded with fillvalue. Note that 
+`szyfrow.support.utilities.pad` allows a callable, so that the message can be 
+padded by random letters, for instance by calling 
+`szyfrow.support.language_models.random_english_letter`.
+"""
 
 
-
-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
+import math
+import multiprocessing 
+from itertools import chain
+from szyfrow.support.utilities import *
+from szyfrow.support.language_models import *
 
 def column_transposition_encipher(message, keyword, fillvalue=' ', 
       fillcolumnwise=False,
 
 def column_transposition_encipher(message, keyword, fillvalue=' ', 
       fillcolumnwise=False,
@@ -100,6 +77,10 @@ def column_transposition_decipher(message, keyword, fillvalue=' ',
     """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.
 
+    Note that `fillcolumnwise` and `emptycolumnwise` refer to how the message
+    is enciphered. To decipher a message, the operations are performed as an 
+    inverse-empty, then inverse-transposition, then inverse-fill.
+
     >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
     'hellothere'
     >>> column_transposition_decipher('hlohreltee', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
     >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
     'hellothere'
     >>> column_transposition_decipher('hlohreltee', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
@@ -130,9 +111,15 @@ def column_transposition_decipher(message, keyword, fillvalue=' ',
         return cat(chain(*untransposed))
 
 def scytale_encipher(message, rows, fillvalue=' '):
         return cat(chain(*untransposed))
 
 def scytale_encipher(message, rows, fillvalue=' '):
-    """Enciphers using the scytale transposition cipher.
+    """Enciphers using the scytale transposition cipher. `rows` is the 
+    circumference of the rod. The message is fitted inot columns so that
+    all rows are used.
+    
     Message is padded with spaces to allow all rows to be the same length.
 
     Message is padded with spaces to allow all rows to be the same length.
 
+    For ease of implementation, the cipher is performed on the transpose
+    of the grid
+
     >>> scytale_encipher('thequickbrownfox', 3)
     'tcnhkfeboqrxuo iw '
     >>> scytale_encipher('thequickbrownfox', 4)
     >>> scytale_encipher('thequickbrownfox', 3)
     'tcnhkfeboqrxuo iw '
     >>> scytale_encipher('thequickbrownfox', 4)
@@ -147,7 +134,7 @@ def scytale_encipher(message, rows, fillvalue=' '):
     # transpositions = [i for i in range(math.ceil(len(message) / rows))]
     # return column_transposition_encipher(message, transpositions, 
     #     fillvalue=fillvalue, fillcolumnwise=False, emptycolumnwise=True)
     # transpositions = [i for i in range(math.ceil(len(message) / rows))]
     # return column_transposition_encipher(message, transpositions, 
     #     fillvalue=fillvalue, fillcolumnwise=False, emptycolumnwise=True)
-    transpositions = [i for i in range(rows)]
+    transpositions = (i for i in range(rows))
     return column_transposition_encipher(message, transpositions, 
         fillvalue=fillvalue, fillcolumnwise=True, emptycolumnwise=False)
 
     return column_transposition_encipher(message, transpositions, 
         fillvalue=fillvalue, fillcolumnwise=True, emptycolumnwise=False)
 
@@ -174,12 +161,18 @@ def scytale_decipher(message, rows):
         fillcolumnwise=True, emptycolumnwise=False)
 
 
         fillcolumnwise=True, emptycolumnwise=False)
 
 
-def column_transposition_break_mp(message, translist=transpositions,
+def column_transposition_break(message, translist=None,
                                   fitness=Pbigrams, chunksize=500):
     """Breaks a column transposition cipher using a dictionary and
     n-gram frequency analysis
 
                                   fitness=Pbigrams, chunksize=500):
     """Breaks a column transposition cipher using a dictionary and
     n-gram frequency analysis
 
-    >>> column_transposition_break_mp(column_transposition_encipher(sanitise( \
+    If `translist` is not specified, use 
+    [`szyfrow.support.langauge_models.transpositions`](support/language_models.html#szyfrow.support.language_models.transpositions).
+
+    >>> len(keywords)
+    20
+
+    >>> column_transposition_break(column_transposition_encipher(sanitise( \
             "It is a truth universally acknowledged, that a single man in \
              possession of a good fortune, must be in want of a wife. However \
              little known the feelings or views of such a man may be on his \
             "It is a truth universally acknowledged, that a single man in \
              possession of a good fortune, must be in want of a wife. However \
              little known the feelings or views of such a man may be on his \
@@ -191,7 +184,7 @@ def column_transposition_break_mp(message, translist=transpositions,
                    (5, 0, 6, 1, 3, 4, 2): ['fourteen'], \
                    (6, 1, 0, 4, 5, 3, 2): ['keyword']}) # doctest: +ELLIPSIS
     (((2, 0, 5, 3, 1, 4, 6), False, False), -709.4646722...)
                    (5, 0, 6, 1, 3, 4, 2): ['fourteen'], \
                    (6, 1, 0, 4, 5, 3, 2): ['keyword']}) # doctest: +ELLIPSIS
     (((2, 0, 5, 3, 1, 4, 6), False, False), -709.4646722...)
-    >>> column_transposition_break_mp(column_transposition_encipher(sanitise( \
+    >>> column_transposition_break(column_transposition_encipher(sanitise( \
             "It is a truth universally acknowledged, that a single man in \
              possession of a good fortune, must be in want of a wife. However \
              little known the feelings or views of such a man may be on his \
             "It is a truth universally acknowledged, that a single man in \
              possession of a good fortune, must be in want of a wife. However \
              little known the feelings or views of such a man may be on his \
@@ -205,6 +198,9 @@ def column_transposition_break_mp(message, translist=transpositions,
         fitness=Ptrigrams) # doctest: +ELLIPSIS
     (((2, 0, 5, 3, 1, 4, 6), False, False), -997.0129085...)
     """
         fitness=Ptrigrams) # doctest: +ELLIPSIS
     (((2, 0, 5, 3, 1, 4, 6), False, False), -997.0129085...)
     """
+    if translist is None:
+        translist = transpositions
+
     with multiprocessing.Pool() as pool:
         helper_args = [(message, trans, fillcolumnwise, emptycolumnwise,
                         fitness)
     with multiprocessing.Pool() as pool:
         helper_args = [(message, trans, fillcolumnwise, emptycolumnwise,
                         fitness)
@@ -216,26 +212,21 @@ def column_transposition_break_mp(message, translist=transpositions,
         breaks = pool.starmap(column_transposition_break_worker,
                               helper_args, chunksize) 
         return max(breaks, key=lambda k: k[1])
         breaks = pool.starmap(column_transposition_break_worker,
                               helper_args, chunksize) 
         return max(breaks, key=lambda k: k[1])
-column_transposition_break = column_transposition_break_mp
 
 def column_transposition_break_worker(message, transposition,
         fillcolumnwise, emptycolumnwise, fitness):
     plaintext = column_transposition_decipher(message, transposition,
         fillcolumnwise=fillcolumnwise, emptycolumnwise=emptycolumnwise)
     fit = fitness(sanitise(plaintext))
 
 def column_transposition_break_worker(message, transposition,
         fillcolumnwise, emptycolumnwise, fitness):
     plaintext = column_transposition_decipher(message, transposition,
         fillcolumnwise=fillcolumnwise, emptycolumnwise=emptycolumnwise)
     fit = fitness(sanitise(plaintext))
-    logger.debug('Column transposition break attempt using key {0} '
-                         'gives fit of {1} and decrypt starting: {2}'.format(
-                             transposition, fit, 
-                             sanitise(plaintext)[:50]))
     return (transposition, fillcolumnwise, emptycolumnwise), fit
 
 
     return (transposition, fillcolumnwise, emptycolumnwise), fit
 
 
-def scytale_break_mp(message, max_key_length=20,
+def scytale_break(message, max_key_length=20,
                      fitness=Pbigrams, chunksize=500):
     """Breaks a scytale cipher using a range of lengths and
     n-gram frequency analysis
 
                      fitness=Pbigrams, chunksize=500):
     """Breaks a scytale cipher using a range of lengths and
     n-gram frequency analysis
 
-    >>> scytale_break_mp(scytale_encipher(sanitise( \
+    >>> scytale_break(scytale_encipher(sanitise( \
             "It is a truth universally acknowledged, that a single man in \
              possession of a good fortune, must be in want of a wife. However \
              little known the feelings or views of such a man may be on his \
             "It is a truth universally acknowledged, that a single man in \
              possession of a good fortune, must be in want of a wife. However \
              little known the feelings or views of such a man may be on his \
@@ -244,7 +235,7 @@ def scytale_break_mp(message, max_key_length=20,
              rightful property of some one or other of their daughters."), \
         5)) # doctest: +ELLIPSIS
     (5, -709.4646722...)
              rightful property of some one or other of their daughters."), \
         5)) # doctest: +ELLIPSIS
     (5, -709.4646722...)
-    >>> scytale_break_mp(scytale_encipher(sanitise( \
+    >>> scytale_break(scytale_encipher(sanitise( \
             "It is a truth universally acknowledged, that a single man in \
              possession of a good fortune, must be in want of a wife. However \
              little known the feelings or views of such a man may be on his \
             "It is a truth universally acknowledged, that a single man in \
              possession of a good fortune, must be in want of a wife. However \
              little known the feelings or views of such a man may be on his \
@@ -266,7 +257,6 @@ def scytale_break_mp(message, max_key_length=20,
                               helper_args, chunksize)
         best = max(breaks, key=lambda k: k[1])
         return math.trunc(len(message) / len(best[0][0])), best[1]
                               helper_args, chunksize)
         best = max(breaks, key=lambda k: k[1])
         return math.trunc(len(message) / len(best[0][0])), best[1]
-scytale_break = scytale_break_mp
 
 if __name__ == "__main__":
     import doctest
\ No newline at end of file
 
 if __name__ == "__main__":
     import doctest
\ No newline at end of file