Started on documentation
[szyfrow.git] / szyfrow / column_transposition.py
index 6d6fe687810925f990b6b1527fac29860cf0b6a5..687fe873fa8aecbddbebe8b5314ca7606e32daeb 100644 (file)
@@ -1,55 +1,27 @@
+"""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.
+
+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. 
+
+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`.
+"""
+
 import math
 import multiprocessing 
 from itertools import chain
 from szyfrow.support.utilities import *
 from szyfrow.support.language_models import *
 
 import math
 import multiprocessing 
 from itertools import chain
 from szyfrow.support.utilities import *
 from szyfrow.support.language_models import *
 
-# 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.
-
-#     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
-
-
-# transpositions = collections.defaultdict(list)
-# for word in keywords:
-#     transpositions[transpositions_of(word)] += [word]
-
-
-# def pad(message_len, group_len, fillvalue):
-#     """Return the padding needed to extend a message to a multiple of group_len
-#     in length.
-
-#     fillvalue can be a function or a literal value. If a function, it is called
-#     once for each padded character. Use this will fillvalue=random_english_letter
-#     to pad a message with random letters.
-#     """
-#     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=' ', 
       fillcolumnwise=False,
       emptycolumnwise=False):
 def column_transposition_encipher(message, keyword, fillvalue=' ', 
       fillcolumnwise=False,
       emptycolumnwise=False):
@@ -105,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)
@@ -135,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)
@@ -179,15 +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
 
+    If `translist` is not specified, use 
+    [`szyfrow.support.langauge_models.transpositions`](support/language_models.html#szyfrow.support.language_models.transpositions).
+
     >>> len(keywords)
     20
 
     >>> len(keywords)
     20
 
-    >>> 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 \
@@ -199,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 \
@@ -213,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)
@@ -224,7 +212,6 @@ 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):
 
 def column_transposition_break_worker(message, transposition,
         fillcolumnwise, emptycolumnwise, fitness):
@@ -234,12 +221,12 @@ def column_transposition_break_worker(message, transposition,
     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 \
@@ -248,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 \
@@ -270,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