Started on documentation
[szyfrow.git] / szyfrow / playfair.py
index 0c0bc6e04c97fba11e323995fd68e0f9b394b7c3..6dd377a84645242ed2d5e83873f358e1dfd696f6 100644 (file)
@@ -1,12 +1,15 @@
-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
+"""Enciphering and deciphering using the [Playfair cipher](https://en.wikipedia.org/wiki/Playfair_cipher). 
+Also attempts to break messages that use a Playfair cipher.
+"""
+from szyfrow.support.utilities import *
+from szyfrow.support.language_models import *
+from szyfrow.keyword_cipher import KeywordWrapAlphabet, keyword_cipher_alphabet_of
+from szyfrow.polybius import polybius_grid
 import multiprocessing
 
-from logger import logger
-
 def playfair_wrap(n, lowest, highest):
+    """Ensures _n_ is between _lowest_ and _highest_ (inclusive at both ends).
+    """
     skip = highest - lowest + 1
     while n > highest or n < lowest:
         if n > highest:
@@ -16,6 +19,7 @@ def playfair_wrap(n, lowest, highest):
     return n
 
 def playfair_encipher_bigram(ab, grid, padding_letter='x'):
+    """Encipher a single pair of letters using the Playfair method."""
     a, b = ab
     max_row = max(c[0] for c in grid.values())
     max_col = max(c[1] for c in grid.values())
@@ -37,6 +41,7 @@ def playfair_encipher_bigram(ab, grid, padding_letter='x'):
     return c + d
 
 def playfair_decipher_bigram(ab, grid, padding_letter='x'):
+    """Decipher a single pair of letters using the Playfair method."""
     a, b = ab
     max_row = max(c[0] for c in grid.values())
     max_col = max(c[1] for c in grid.values())
@@ -58,6 +63,14 @@ def playfair_decipher_bigram(ab, grid, padding_letter='x'):
     return c + d
 
 def playfair_bigrams(text, padding_letter='x', padding_replaces_repeat=True):
+    """Find all the bigrams in a method to be enciphered. 
+
+    If both letters are the same, the `padding_letter` is used instead of the
+    second letter. If `padding_replaces_repeat` is `True`, the `padding_letter`
+    replaces the second letter of the bigram. If `padding_replaces_repeat` is
+    `False`, the second letter of this bigram becomes the first letter of the 
+    next, effectively lengthening the message by one letter.
+    """
     i = 0
     bigrams = []
     while i < len(text):
@@ -80,6 +93,7 @@ def playfair_bigrams(text, padding_letter='x', padding_replaces_repeat=True):
 def playfair_encipher(message, keyword, padding_letter='x',
                       padding_replaces_repeat=False, letters_to_merge=None, 
                       wrap_alphabet=KeywordWrapAlphabet.from_a):
+    """Encipher a message using the Playfair cipher."""
     column_order = list(range(5))
     row_order = list(range(5))
     if letters_to_merge is None: 
@@ -87,14 +101,16 @@ def playfair_encipher(message, keyword, padding_letter='x',
     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)
+    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):
+    """Decipher a message using the Playfair cipher."""
     column_order = list(range(5))
     row_order = list(range(5))
     if letters_to_merge is None: 
@@ -102,15 +118,23 @@ def playfair_decipher(message, keyword, padding_letter='x',
     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)
+    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, 
+def playfair_break(message, 
                       letters_to_merge=None, padding_letter='x',
                       wordlist=keywords, fitness=Pletters,
                       number_of_solutions=1, chunksize=500):
+    """Break a message enciphered using the Playfair cipher, using a dictionary
+    of keywords and frequency analysis. 
+
+    If `wordlist` is not specified, use 
+    [`szyfrow.support.langauge_models.keywords`](support/language_models.html#szyfrow.support.language_models.keywords).
+    """
+
     if letters_to_merge is None: 
         letters_to_merge = {'j': 'i'}   
 
@@ -142,11 +166,6 @@ def playfair_break_worker(message, keyword, wrap,
         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, 
@@ -155,6 +174,14 @@ def playfair_simulated_annealing_break(message, workers=10,
                               plain_alphabet=None, 
                               cipher_alphabet=None, 
                               fitness=Pletters, chunksize=1):
+    """Break a message enciphered using the Playfair cipher, using simulated
+    annealing to determine the keyword.  This function just sets up a stable 
+    of workers who     do the actual work, implemented as 
+    `szyfrow.playfair.playfair_simulated_annealing_break_worker`.
+
+    See a [post on simulated annealing](https://work.njae.me.uk/2019/07/08/simulated-annealing-and-breaking-substitution-ciphers/)
+    for detail on how this works.
+    """
     worker_args = []
     ciphertext = sanitise(message)
     for i in range(workers):
@@ -178,6 +205,10 @@ def playfair_simulated_annealing_break(message, workers=10,
 
 def playfair_simulated_annealing_break_worker(message, plain_alphabet, cipher_alphabet, 
                                      t0, max_iterations, fitness):
+    """One thread of a simulated annealing run. 
+    See a [post on simulated annealing](https://work.njae.me.uk/2019/07/08/simulated-annealing-and-breaking-substitution-ciphers/)
+    for detail on how this works.
+    """
     def swap(letters, i, j):
         if i > j:
             i, j = j, i
@@ -262,12 +293,6 @@ def playfair_simulated_annealing_break_worker(message, plain_alphabet, cipher_al
             # 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
@@ -283,11 +308,6 @@ def playfair_simulated_annealing_break_worker(message, plain_alphabet, cipher_al
             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