Minor documentation updates
[szyfrow.git] / szyfrow / cadenus.py
index b56500c6de1f01df5d602664b04731cb5008429d..f466749d4f092c2bf1cd8a747c978686dc7558bc 100644 (file)
@@ -1,8 +1,72 @@
+"""Enciphering and deciphering using the [Cadenus cipher](https://www.thonky.com/kryptos/cadenus-cipher). 
+Also attempts to break messages that use a Cadenus cipher.
+
+The plaintext is written out in a grid, with one column per letter of the 
+keyword. The plaintext is written out left to right in rows. The plaintext 
+needs to fill 25 rows: if it is shorter, the text is padded; if longer, it is
+broken into 25-row chunks.
+
+For instance, the 100 letter chunk:
+
+> Whoever has made a voyage up the Hudson must remember the Kaatskill mountains. 
+> They are a dismembered branch of the great
+
+and the keyword "wink" would written out as the leftmost grid below.
+
+The columns are then rotated according to the _keycolumn_. For each column, the
+keyword letter in that column is found in the keycolumn. This identifies a 
+specific row in the grid. That column only is rotated upwards until the selected
+row is at the top of the column. Each column is rotated independently, according
+to its keyword letter.
+
+For instance, the middle grid below is formed from the leftmost grid by 
+rotating the first column up four positions, the second column up 17 positions,
+and so on. (The letters chosen to head the new colums are capitalised in the
+leftmost grid.)
+
+Finally, each row is transposed given the alphabetic order of the keyword (as
+seen in the rightmost grid below).
+
+The ciphertext is read out in rows, starting with the now-leftmost column. For
+the example, the ciphertext would be 
+
+> antodeleeeuhrsidrbhmhdrrhnimefmthgeaetakseomehetyaasuvoyegrastmmuuaeenabbtpchehtarorikswosmvaleatned'
+
+```
+w i n k         w i n k    i k n w
+-------         -------    -------
+w h o e    a    o a t n    a n t o
+v e r h    z    e d l e    d e l e
+a s m a    y    h e u e    e e u h
+d e a v    x    d r i s    r s i d
+O y a g    vw   m r h b    r b h m
+e u p t    u    r h r d    h d r r
+h e h u    t    m h i n    h n i m
+d s o n    s    t e m f    e f m t
+m u s t    r    a h e g    h g e a
+r e m e    q    k e a t    e t a k
+m b e r    p    m s o e    s e o m
+t h e k    o    t e e h    e h e t
+a a T s    n    s y a a    y a a s
+k i l l    m    y u o v    u v o y
+m o u n    l    a e r g    e g r a
+t a i N    k    m s m t    s t m m
+s t h e    j    e u a u    u u a e
+y A r e    i    b e a n    e n a b
+a d i s    h    c b p t    b t p c
+m e m b    g    t h h e    h e h t
+e r e d    f    r a o r    a r o r
+b r a n    e    w i s k    i k s w
+c h o f    d    v o m s    o s m v
+t h e g    c    a a e l    a l e a
+r e a t    b    d t e n    t n e d
+```
+
+"""
 from itertools import chain
 import multiprocessing
 from szyfrow.support.utilities import *
 from szyfrow.support.language_models import *
-from szyfrow.column_transposition import transpositions_of
 
 
 def make_cadenus_keycolumn(doubled_letters = 'vw', start='a', reverse=False):
@@ -62,13 +126,17 @@ def cadenus_encipher(message, keyword, keycolumn, fillvalue='a'):
                 make_cadenus_keycolumn(doubled_letters='vw', start='a', reverse=True))
     'systretomtattlusoatleeesfiyheasdfnmschbhneuvsnpmtofarenuseieeieltarlmentieetogevesitfaisltngeeuvowul'
     """
-    rows = chunks(message, len(message) // 25, fillvalue=fillvalue)
-    columns = zip(*rows)
-    rotated_columns = [col[start:] + col[:start] for start, col in zip([keycolumn[l] for l in keyword], columns)]    
-    rotated_rows = zip(*rotated_columns)
     transpositions = transpositions_of(keyword)
-    transposed = [transpose(r, transpositions) for r in rotated_rows]
-    return cat(chain(*transposed))
+    enciphered_chunks = []
+    for message_chunk in chunks(message, len(transpositions) * 25, 
+                                fillvalue=fillvalue):
+        rows = chunks(message_chunk, len(transpositions), fillvalue=fillvalue)
+        columns = zip(*rows)
+        rotated_columns = [col[start:] + col[:start] for start, col in zip([keycolumn[l] for l in keyword], columns)]    
+        rotated_rows = zip(*rotated_columns)
+        transposed = [transpose(r, transpositions) for r in rotated_rows]
+        enciphered_chunks.append(cat(chain(*transposed)))
+    return cat(enciphered_chunks)
 
 def cadenus_decipher(message, keyword, keycolumn, fillvalue='a'):
     """
@@ -83,28 +151,41 @@ def cadenus_decipher(message, keyword, keycolumn, fillvalue='a'):
                  make_cadenus_keycolumn(reverse=True))
     'aseverelimitationontheusefulnessofthecadenusisthateverymessagemustbeamultipleoftwentyfiveletterslong'
     """
-    rows = chunks(message, len(message) // 25, fillvalue=fillvalue)
     transpositions = transpositions_of(keyword)
-    untransposed_rows = [untranspose(r, transpositions) for r in rows]
-    columns = zip(*untransposed_rows)
-    rotated_columns = [col[-start:] + col[:-start] for start, col in zip([keycolumn[l] for l in keyword], columns)]    
-    rotated_rows = zip(*rotated_columns)
-    # return rotated_columns
-    return cat(chain(*rotated_rows))
+    deciphered_chunks = []
+    for message_chunk in chunks(message, len(transpositions) * 25, 
+                                fillvalue=fillvalue):
+        rows = chunks(message_chunk, len(transpositions), fillvalue=fillvalue)
+        untransposed_rows = [untranspose(r, transpositions) for r in rows]
+        columns = zip(*untransposed_rows)
+        rotated_columns = [col[-start:] + col[:-start] for start, col in zip([keycolumn[l] for l in keyword], columns)]    
+        rotated_rows = zip(*rotated_columns)
+        deciphered_chunks.append(cat(chain(*rotated_rows)))
+    return cat(deciphered_chunks)
+    
 
 
-def cadenus_break(message, words=keywords
+def cadenus_break(message, wordlist=None
     doubled_letters='vw', fitness=Pbigrams):
-    c = make_cadenus_keycolumn(reverse=True)
-    valid_words = [w for w in words 
-        if len(transpositions_of(w)) == len(message) // 25]
+    """Breaks a Cadenus cipher using a dictionary and
+    frequency analysis
+
+    If `wordlist` is not specified, use 
+    [`szyfrow.support.langauge_models.keywords`](support/language_models.html#szyfrow.support.language_models.keywords).
+    """
+    if wordlist is None:
+        wordlist = keywords
+
+    # c = make_cadenus_keycolumn(reverse=True)
+    # valid_words = [w for w in wordlist
+    #     if len(transpositions_of(w)) == len(message) // 25]
     with multiprocessing.Pool() as pool:
         results = pool.starmap(cadenus_break_worker, 
                 [(message, w, 
                     make_cadenus_keycolumn(doubled_letters=doubled_letters, 
                         start=s, reverse=r), 
                     fitness)
-                for w in words 
+                for w in wordlist 
                 for s in string.ascii_lowercase 
                 for r in [True, False]
                 # if max(transpositions_of(w)) <= len(