Started on documentation
[szyfrow.git] / docs / szyfrow / keyword_cipher.html
1 <!doctype html>
2 <html lang="en">
3 <head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
6 <meta name="generator" content="pdoc 0.9.2" />
7 <title>szyfrow.keyword_cipher API documentation</title>
8 <meta name="description" content="Monoalphabetic substitution ciphers, mainly done by keyword. Enciphering
9 and deciphering, and a couple of ways to break these ciphers." />
10 <link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/sanitize.min.css" integrity="sha256-PK9q560IAAa6WVRRh76LtCaI8pjTJ2z11v0miyNNjrs=" crossorigin>
11 <link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/typography.min.css" integrity="sha256-7l/o7C8jubJiy74VsKTidCy1yBkRtiUGbVkYBylBqUg=" crossorigin>
12 <link rel="stylesheet preload" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/github.min.css" crossorigin>
13 <style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:30px;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:1em 0 .50em 0}h3{font-size:1.4em;margin:25px 0 10px 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .3s ease-in-out}a:hover{color:#e82}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900}pre code{background:#f8f8f8;font-size:.8em;line-height:1.4em}code{background:#f2f2f1;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{background:#f8f8f8;border:0;border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0;padding:1ex}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-weight:bold;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em .5em;margin-bottom:1em}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style>
14 <style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.item .name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul{padding-left:1.5em}.toc > ul > li{margin-top:.5em}}</style>
15 <style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style>
16 <script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js" integrity="sha256-Uv3H6lx7dJmRfRvH8TH6kJD1TSK1aFcwgx+mdg3epi8=" crossorigin></script>
17 <script>window.addEventListener('DOMContentLoaded', () => hljs.initHighlighting())</script>
18 </head>
19 <body>
20 <main>
21 <article id="content">
22 <header>
23 <h1 class="title">Module <code>szyfrow.keyword_cipher</code></h1>
24 </header>
25 <section id="section-intro">
26 <p>Monoalphabetic substitution ciphers, mainly done by keyword. Enciphering
27 and deciphering, and a couple of ways to break these ciphers.</p>
28 <details class="source">
29 <summary>
30 <span>Expand source code</span>
31 </summary>
32 <pre><code class="python">&#34;&#34;&#34;Monoalphabetic substitution ciphers, mainly done by keyword. Enciphering
33 and deciphering, and a couple of ways to break these ciphers.
34 &#34;&#34;&#34;
35 from enum import Enum
36 import multiprocessing
37 import math
38 from szyfrow.support.utilities import *
39 from szyfrow.support.language_models import *
40
41
42 class KeywordWrapAlphabet(Enum):
43 &#34;&#34;&#34;Ways to list the rest of the alphabet after use of a keyword.
44
45 * `from_a` : continue the alphabet from &#39;a&#39;: `bayescdfg...`
46 * `from_last`: continue from the last letter of the keyword:
47 `bayestuvwxyzcdf...`
48 * `from_largest`: continue from the &#34;largest&#34; letter of the keyword:
49 `bayeszcdfg...`
50 &#34;&#34;&#34;
51 from_a = 1
52 from_last = 2
53 from_largest = 3
54
55
56 def keyword_cipher_alphabet_of(keyword, wrap_alphabet=KeywordWrapAlphabet.from_a):
57 &#34;&#34;&#34;Find the cipher alphabet given a keyword.
58
59 [`wrap_alphabet`](#szyfrow.keyword_cipher.KeywordWrapAlphabet) controls
60 how the rest of the alphabet is added after the keyword.
61
62 &gt;&gt;&gt; keyword_cipher_alphabet_of(&#39;bayes&#39;)
63 &#39;bayescdfghijklmnopqrtuvwxz&#39;
64 &gt;&gt;&gt; keyword_cipher_alphabet_of(&#39;bayes&#39;, KeywordWrapAlphabet.from_a)
65 &#39;bayescdfghijklmnopqrtuvwxz&#39;
66 &gt;&gt;&gt; keyword_cipher_alphabet_of(&#39;bayes&#39;, KeywordWrapAlphabet.from_last)
67 &#39;bayestuvwxzcdfghijklmnopqr&#39;
68 &gt;&gt;&gt; keyword_cipher_alphabet_of(&#39;bayes&#39;, KeywordWrapAlphabet.from_largest)
69 &#39;bayeszcdfghijklmnopqrtuvwx&#39;
70 &#34;&#34;&#34;
71 if wrap_alphabet == KeywordWrapAlphabet.from_a:
72 cipher_alphabet = cat(deduplicate(sanitise(keyword) +
73 string.ascii_lowercase))
74 else:
75 if wrap_alphabet == KeywordWrapAlphabet.from_last:
76 last_keyword_letter = deduplicate(sanitise(keyword))[-1]
77 else:
78 last_keyword_letter = sorted(sanitise(keyword))[-1]
79 last_keyword_position = string.ascii_lowercase.find(
80 last_keyword_letter) + 1
81 cipher_alphabet = cat(
82 deduplicate(sanitise(keyword) +
83 string.ascii_lowercase[last_keyword_position:] +
84 string.ascii_lowercase))
85 return cipher_alphabet
86
87
88 def keyword_encipher(message, keyword, wrap_alphabet=KeywordWrapAlphabet.from_a):
89 &#34;&#34;&#34;Enciphers a message with a keyword substitution cipher.
90 wrap_alphabet controls how the rest of the alphabet is added
91 after the keyword.
92 0 : from &#39;a&#39;
93 1 : from the last letter in the sanitised keyword
94 2 : from the largest letter in the sanitised keyword
95
96 &gt;&gt;&gt; keyword_encipher(&#39;test message&#39;, &#39;bayes&#39;)
97 &#39;rsqr ksqqbds&#39;
98 &gt;&gt;&gt; keyword_encipher(&#39;test message&#39;, &#39;bayes&#39;, KeywordWrapAlphabet.from_a)
99 &#39;rsqr ksqqbds&#39;
100 &gt;&gt;&gt; keyword_encipher(&#39;test message&#39;, &#39;bayes&#39;, KeywordWrapAlphabet.from_last)
101 &#39;lskl dskkbus&#39;
102 &gt;&gt;&gt; keyword_encipher(&#39;test message&#39;, &#39;bayes&#39;, KeywordWrapAlphabet.from_largest)
103 &#39;qspq jsppbcs&#39;
104 &#34;&#34;&#34;
105 cipher_alphabet = keyword_cipher_alphabet_of(keyword, wrap_alphabet)
106 cipher_translation = &#39;&#39;.maketrans(string.ascii_lowercase, cipher_alphabet)
107 return unaccent(message).lower().translate(cipher_translation)
108
109 def keyword_decipher(message, keyword, wrap_alphabet=KeywordWrapAlphabet.from_a):
110 &#34;&#34;&#34;Deciphers a message with a keyword substitution cipher.
111 wrap_alphabet controls how the rest of the alphabet is added
112 after the keyword.
113 0 : from &#39;a&#39;
114 1 : from the last letter in the sanitised keyword
115 2 : from the largest letter in the sanitised keyword
116
117 &gt;&gt;&gt; keyword_decipher(&#39;rsqr ksqqbds&#39;, &#39;bayes&#39;)
118 &#39;test message&#39;
119 &gt;&gt;&gt; keyword_decipher(&#39;rsqr ksqqbds&#39;, &#39;bayes&#39;, KeywordWrapAlphabet.from_a)
120 &#39;test message&#39;
121 &gt;&gt;&gt; keyword_decipher(&#39;lskl dskkbus&#39;, &#39;bayes&#39;, KeywordWrapAlphabet.from_last)
122 &#39;test message&#39;
123 &gt;&gt;&gt; keyword_decipher(&#39;qspq jsppbcs&#39;, &#39;bayes&#39;, KeywordWrapAlphabet.from_largest)
124 &#39;test message&#39;
125 &#34;&#34;&#34;
126 cipher_alphabet = keyword_cipher_alphabet_of(keyword, wrap_alphabet)
127 cipher_translation = &#39;&#39;.maketrans(cipher_alphabet, string.ascii_lowercase)
128 return message.lower().translate(cipher_translation)
129
130
131 def keyword_break_single_thread(message, wordlist=None, fitness=Pletters):
132 &#34;&#34;&#34;Breaks a keyword substitution cipher using a dictionary and
133 frequency analysis.
134
135 If `wordlist` is not specified, use
136 [`szyfrow.support.langauge_models.keywords`](support/language_models.html#szyfrow.support.language_models.keywords).
137
138 &gt;&gt;&gt; keyword_break(keyword_encipher(&#39;this is a test message for the &#39; \
139 &#39;keyword decipherment&#39;, &#39;elephant&#39;, KeywordWrapAlphabet.from_last), \
140 wordlist=[&#39;cat&#39;, &#39;elephant&#39;, &#39;kangaroo&#39;]) # doctest: +ELLIPSIS
141 ((&#39;elephant&#39;, &lt;KeywordWrapAlphabet.from_last: 2&gt;), -52.834575011...)
142 &#34;&#34;&#34;
143 if wordlist is None:
144 wordlist = keywords
145
146 best_keyword = &#39;&#39;
147 best_wrap_alphabet = True
148 best_fit = float(&#34;-inf&#34;)
149 for wrap_alphabet in KeywordWrapAlphabet:
150 for keyword in wordlist:
151 plaintext = keyword_decipher(message, keyword, wrap_alphabet)
152 fit = fitness(plaintext)
153 if fit &gt; best_fit:
154 best_fit = fit
155 best_keyword = keyword
156 best_wrap_alphabet = wrap_alphabet
157 return (best_keyword, best_wrap_alphabet), best_fit
158
159 def keyword_break(message, wordlist=None, fitness=Pletters,
160 number_of_solutions=1, chunksize=500):
161 &#34;&#34;&#34;Breaks a keyword substitution cipher using a dictionary and
162 frequency analysis.
163
164 If `wordlist` is not specified, use
165 [`szyfrow.support.langauge_models.keywords`](support/language_models.html#szyfrow.support.language_models.keywords).
166
167
168 &gt;&gt;&gt; keyword_break_mp(keyword_encipher(&#39;this is a test message for the &#39; \
169 &#39;keyword decipherment&#39;, &#39;elephant&#39;, KeywordWrapAlphabet.from_last), \
170 wordlist=[&#39;cat&#39;, &#39;elephant&#39;, &#39;kangaroo&#39;]) # doctest: +ELLIPSIS
171 ((&#39;elephant&#39;, &lt;KeywordWrapAlphabet.from_last: 2&gt;), -52.834575011...)
172 &gt;&gt;&gt; keyword_break_mp(keyword_encipher(&#39;this is a test message for the &#39; \
173 &#39;keyword decipherment&#39;, &#39;elephant&#39;, KeywordWrapAlphabet.from_last), \
174 wordlist=[&#39;cat&#39;, &#39;elephant&#39;, &#39;kangaroo&#39;], \
175 number_of_solutions=2) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
176 [((&#39;elephant&#39;, &lt;KeywordWrapAlphabet.from_last: 2&gt;), -52.834575011...),
177 ((&#39;elephant&#39;, &lt;KeywordWrapAlphabet.from_largest: 3&gt;), -52.834575011...)]
178 &#34;&#34;&#34;
179 if wordlist is None:
180 wordlist = keywords
181
182 with multiprocessing.Pool() as pool:
183 helper_args = [(message, word, wrap, fitness)
184 for word in wordlist
185 for wrap in KeywordWrapAlphabet]
186 # Gotcha: the helper function here needs to be defined at the top level
187 # (limitation of Pool.starmap)
188 breaks = pool.starmap(keyword_break_worker, helper_args, chunksize)
189 if number_of_solutions == 1:
190 return max(breaks, key=lambda k: k[1])
191 else:
192 return sorted(breaks, key=lambda k: k[1], reverse=True)[:number_of_solutions]
193
194 def keyword_break_worker(message, keyword, wrap_alphabet, fitness):
195 plaintext = keyword_decipher(message, keyword, wrap_alphabet)
196 fit = fitness(plaintext)
197 return (keyword, wrap_alphabet), fit
198
199
200 def monoalphabetic_break_hillclimbing_single(message,
201 max_iterations=20000,
202 plain_alphabet=None,
203 cipher_alphabet=None,
204 swap_index_finder=None,
205 fitness=Pletters, chunksize=1):
206 &#34;&#34;&#34;Break a monalphabetic substitution cipher using hillclimbing to
207 guess the keyword. Hillclimbing is done by using the simulated annealing
208 approach with a temperature of zero.
209
210 This version uses a single worker.
211 &#34;&#34;&#34;
212 return monoalphabetic_sa_break(message,
213 workers=1,
214 initial_temperature=0,
215 max_iterations=max_iterations,
216 plain_alphabet=plain_alphabet,
217 cipher_alphabet=cipher_alphabet,
218 swap_index_finder=swap_index_finder,
219 fitness=fitness, chunksize=chunksize)
220
221
222 def monoalphabetic_break_hillclimbing(message,
223 workers=10,
224 max_iterations=20000,
225 plain_alphabet=None,
226 cipher_alphabet=None,
227 swap_index_finder=None,
228 fitness=Pletters, chunksize=1):
229 &#34;&#34;&#34;Break a monalphabetic substitution cipher using hillclimbing to
230 guess the keyword. Hillclimbing is done by using the simulated annealing
231 approach with a temperature of zero.
232
233 This version uses a several workers.
234 &#34;&#34;&#34;
235 return monoalphabetic_sa_break(message,
236 workers=workers,
237 initial_temperature=0,
238 max_iterations=max_iterations,
239 plain_alphabet=plain_alphabet,
240 cipher_alphabet=cipher_alphabet,
241 swap_index_finder=swap_index_finder,
242 fitness=fitness, chunksize=chunksize)
243
244
245 def gaussian_swap_index(a):
246 &#34;&#34;&#34;Return an index to use as the partner of `a` in a swap. The partners
247 are drawn from a Gaussian distribution.
248 &#34;&#34;&#34;
249 return (a + int(random.gauss(0, 4))) % 26
250
251 def uniform_swap_index(a):
252 &#34;&#34;&#34;Return an index to use as the partner of `a` in a swap. The partners
253 are drawn from a uniform distribution.
254 &#34;&#34;&#34;
255 return random.randrange(26)
256
257 def monoalphabetic_sa_break(message, workers=10,
258 initial_temperature=200,
259 max_iterations=20000,
260 plain_alphabet=None,
261 cipher_alphabet=None,
262 swap_index_finder=None,
263 fitness=Ptrigrams, chunksize=1):
264 &#34;&#34;&#34;Break a monalphabetic substitution cipher using simulated annealing to
265 guess the keyword. This function just sets up a stable of workers who
266 do the actual work, implemented as
267 `szyfrow.keyword_cipher.monoalphabetic_sa_break_worker`.
268
269 See a [post on simulated annealing](https://work.njae.me.uk/2019/07/08/simulated-annealing-and-breaking-substitution-ciphers/)
270 for detail on how this works.
271 &#34;&#34;&#34;
272 worker_args = []
273 ciphertext = sanitise(message)
274 if swap_index_finder is None:
275 swap_index_finder = gaussian_swap_index
276
277 for i in range(workers):
278 if plain_alphabet is None:
279 used_plain_alphabet = string.ascii_lowercase
280 else:
281 used_plain_alphabet = plain_alphabet
282 if cipher_alphabet is None:
283 used_cipher_alphabet = list(string.ascii_lowercase)
284 random.shuffle(used_cipher_alphabet)
285 used_cipher_alphabet = cat(used_cipher_alphabet)
286 else:
287 used_cipher_alphabet = cipher_alphabet
288 # if not plain_alphabet:
289 # plain_alphabet = string.ascii_lowercase
290 # if not cipher_alphabet:
291 # cipher_alphabet = list(string.ascii_lowercase)
292 # random.shuffle(cipher_alphabet)
293 # cipher_alphabet = cat(cipher_alphabet)
294 worker_args.append((ciphertext, used_plain_alphabet, used_cipher_alphabet,
295 swap_index_finder,
296 initial_temperature, max_iterations, fitness,
297 i))
298 with multiprocessing.Pool() as pool:
299 breaks = pool.starmap(monoalphabetic_sa_break_worker,
300 worker_args, chunksize)
301 return max(breaks, key=lambda k: k[1])
302
303
304 def monoalphabetic_sa_break_worker(message, plain_alphabet, cipher_alphabet,
305 swap_index_finder,
306 t0, max_iterations, fitness,
307 logID):
308 &#34;&#34;&#34;One thread of a simulated annealing run.
309 See a [post on simulated annealing](https://work.njae.me.uk/2019/07/08/simulated-annealing-and-breaking-substitution-ciphers/)
310 for detail on how this works.
311 &#34;&#34;&#34;
312 def swap(letters, i, j):
313 if i &gt; j:
314 i, j = j, i
315 if i == j:
316 return letters
317 else:
318 return (letters[:i] + letters[j] + letters[i+1:j] + letters[i] +
319 letters[j+1:])
320
321 temperature = t0
322
323 dt = t0 / (0.9 * max_iterations)
324
325 current_alphabet = cipher_alphabet
326 alphabet = current_alphabet
327 cipher_translation = &#39;&#39;.maketrans(current_alphabet, plain_alphabet)
328 plaintext = message.translate(cipher_translation)
329 current_fitness = fitness(plaintext)
330
331 best_alphabet = current_alphabet
332 best_fitness = current_fitness
333 best_plaintext = plaintext
334
335 # print(&#39;starting for&#39;, max_iterations)
336 for i in range(max_iterations):
337 swap_a = random.randrange(26)
338 # swap_b = (swap_a + int(random.gauss(0, 4))) % 26
339 swap_b = swap_index_finder(swap_a)
340 alphabet = swap(current_alphabet, swap_a, swap_b)
341 cipher_translation = &#39;&#39;.maketrans(alphabet, plain_alphabet)
342 plaintext = message.translate(cipher_translation)
343 new_fitness = fitness(plaintext)
344 try:
345 sa_chance = math.exp((new_fitness - current_fitness) / temperature)
346 except (OverflowError, ZeroDivisionError):
347 # print(&#39;exception triggered: new_fit {}, current_fit {}, temp {}&#39;.format(new_fitness, current_fitness, temperature))
348 sa_chance = 0
349 if (new_fitness &gt; current_fitness or random.random() &lt; sa_chance):
350 current_fitness = new_fitness
351 current_alphabet = alphabet
352
353 if current_fitness &gt; best_fitness:
354 best_alphabet = current_alphabet
355 best_fitness = current_fitness
356 best_plaintext = plaintext
357
358 temperature = max(temperature - dt, 0.001)
359
360 return best_alphabet, best_fitness # current_alphabet, current_fitness
361
362 if __name__ == &#34;__main__&#34;:
363 import doctest</code></pre>
364 </details>
365 </section>
366 <section>
367 </section>
368 <section>
369 </section>
370 <section>
371 <h2 class="section-title" id="header-functions">Functions</h2>
372 <dl>
373 <dt id="szyfrow.keyword_cipher.cat"><code class="name flex">
374 <span>def <span class="ident">cat</span></span>(<span>iterable, /)</span>
375 </code></dt>
376 <dd>
377 <div class="desc"><p>Concatenate any number of strings.</p>
378 <p>The string whose method is called is inserted in between each given string.
379 The result is returned as a new string.</p>
380 <p>Example: '.'.join(['ab', 'pq', 'rs']) -&gt; 'ab.pq.rs'</p></div>
381 </dd>
382 <dt id="szyfrow.keyword_cipher.gaussian_swap_index"><code class="name flex">
383 <span>def <span class="ident">gaussian_swap_index</span></span>(<span>a)</span>
384 </code></dt>
385 <dd>
386 <div class="desc"><p>Return an index to use as the partner of <code>a</code> in a swap. The partners
387 are drawn from a Gaussian distribution.</p></div>
388 <details class="source">
389 <summary>
390 <span>Expand source code</span>
391 </summary>
392 <pre><code class="python">def gaussian_swap_index(a):
393 &#34;&#34;&#34;Return an index to use as the partner of `a` in a swap. The partners
394 are drawn from a Gaussian distribution.
395 &#34;&#34;&#34;
396 return (a + int(random.gauss(0, 4))) % 26</code></pre>
397 </details>
398 </dd>
399 <dt id="szyfrow.keyword_cipher.keyword_break"><code class="name flex">
400 <span>def <span class="ident">keyword_break</span></span>(<span>message, wordlist=None, fitness=&lt;function Pletters&gt;, number_of_solutions=1, chunksize=500)</span>
401 </code></dt>
402 <dd>
403 <div class="desc"><p>Breaks a keyword substitution cipher using a dictionary and
404 frequency analysis.</p>
405 <p>If <code>wordlist</code> is not specified, use
406 <a href="support/language_models.html#szyfrow.support.language_models.keywords"><code>szyfrow.support.langauge_models.keywords</code></a>.</p>
407 <pre><code class="language-python-repl">&gt;&gt;&gt; keyword_break_mp(keyword_encipher('this is a test message for the ' 'keyword decipherment', 'elephant', KeywordWrapAlphabet.from_last), wordlist=['cat', 'elephant', 'kangaroo']) # doctest: +ELLIPSIS
408 (('elephant', &lt;KeywordWrapAlphabet.from_last: 2&gt;), -52.834575011...)
409 &gt;&gt;&gt; keyword_break_mp(keyword_encipher('this is a test message for the ' 'keyword decipherment', 'elephant', KeywordWrapAlphabet.from_last), wordlist=['cat', 'elephant', 'kangaroo'], number_of_solutions=2) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
410 [(('elephant', &lt;KeywordWrapAlphabet.from_last: 2&gt;), -52.834575011...),
411 (('elephant', &lt;KeywordWrapAlphabet.from_largest: 3&gt;), -52.834575011...)]
412 </code></pre></div>
413 <details class="source">
414 <summary>
415 <span>Expand source code</span>
416 </summary>
417 <pre><code class="python">def keyword_break(message, wordlist=None, fitness=Pletters,
418 number_of_solutions=1, chunksize=500):
419 &#34;&#34;&#34;Breaks a keyword substitution cipher using a dictionary and
420 frequency analysis.
421
422 If `wordlist` is not specified, use
423 [`szyfrow.support.langauge_models.keywords`](support/language_models.html#szyfrow.support.language_models.keywords).
424
425
426 &gt;&gt;&gt; keyword_break_mp(keyword_encipher(&#39;this is a test message for the &#39; \
427 &#39;keyword decipherment&#39;, &#39;elephant&#39;, KeywordWrapAlphabet.from_last), \
428 wordlist=[&#39;cat&#39;, &#39;elephant&#39;, &#39;kangaroo&#39;]) # doctest: +ELLIPSIS
429 ((&#39;elephant&#39;, &lt;KeywordWrapAlphabet.from_last: 2&gt;), -52.834575011...)
430 &gt;&gt;&gt; keyword_break_mp(keyword_encipher(&#39;this is a test message for the &#39; \
431 &#39;keyword decipherment&#39;, &#39;elephant&#39;, KeywordWrapAlphabet.from_last), \
432 wordlist=[&#39;cat&#39;, &#39;elephant&#39;, &#39;kangaroo&#39;], \
433 number_of_solutions=2) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
434 [((&#39;elephant&#39;, &lt;KeywordWrapAlphabet.from_last: 2&gt;), -52.834575011...),
435 ((&#39;elephant&#39;, &lt;KeywordWrapAlphabet.from_largest: 3&gt;), -52.834575011...)]
436 &#34;&#34;&#34;
437 if wordlist is None:
438 wordlist = keywords
439
440 with multiprocessing.Pool() as pool:
441 helper_args = [(message, word, wrap, fitness)
442 for word in wordlist
443 for wrap in KeywordWrapAlphabet]
444 # Gotcha: the helper function here needs to be defined at the top level
445 # (limitation of Pool.starmap)
446 breaks = pool.starmap(keyword_break_worker, helper_args, chunksize)
447 if number_of_solutions == 1:
448 return max(breaks, key=lambda k: k[1])
449 else:
450 return sorted(breaks, key=lambda k: k[1], reverse=True)[:number_of_solutions]</code></pre>
451 </details>
452 </dd>
453 <dt id="szyfrow.keyword_cipher.keyword_break_single_thread"><code class="name flex">
454 <span>def <span class="ident">keyword_break_single_thread</span></span>(<span>message, wordlist=None, fitness=&lt;function Pletters&gt;)</span>
455 </code></dt>
456 <dd>
457 <div class="desc"><p>Breaks a keyword substitution cipher using a dictionary and
458 frequency analysis.</p>
459 <p>If <code>wordlist</code> is not specified, use
460 <a href="support/language_models.html#szyfrow.support.language_models.keywords"><code>szyfrow.support.langauge_models.keywords</code></a>.</p>
461 <pre><code class="language-python-repl">&gt;&gt;&gt; keyword_break(keyword_encipher('this is a test message for the ' 'keyword decipherment', 'elephant', KeywordWrapAlphabet.from_last), wordlist=['cat', 'elephant', 'kangaroo']) # doctest: +ELLIPSIS
462 (('elephant', &lt;KeywordWrapAlphabet.from_last: 2&gt;), -52.834575011...)
463 </code></pre></div>
464 <details class="source">
465 <summary>
466 <span>Expand source code</span>
467 </summary>
468 <pre><code class="python">def keyword_break_single_thread(message, wordlist=None, fitness=Pletters):
469 &#34;&#34;&#34;Breaks a keyword substitution cipher using a dictionary and
470 frequency analysis.
471
472 If `wordlist` is not specified, use
473 [`szyfrow.support.langauge_models.keywords`](support/language_models.html#szyfrow.support.language_models.keywords).
474
475 &gt;&gt;&gt; keyword_break(keyword_encipher(&#39;this is a test message for the &#39; \
476 &#39;keyword decipherment&#39;, &#39;elephant&#39;, KeywordWrapAlphabet.from_last), \
477 wordlist=[&#39;cat&#39;, &#39;elephant&#39;, &#39;kangaroo&#39;]) # doctest: +ELLIPSIS
478 ((&#39;elephant&#39;, &lt;KeywordWrapAlphabet.from_last: 2&gt;), -52.834575011...)
479 &#34;&#34;&#34;
480 if wordlist is None:
481 wordlist = keywords
482
483 best_keyword = &#39;&#39;
484 best_wrap_alphabet = True
485 best_fit = float(&#34;-inf&#34;)
486 for wrap_alphabet in KeywordWrapAlphabet:
487 for keyword in wordlist:
488 plaintext = keyword_decipher(message, keyword, wrap_alphabet)
489 fit = fitness(plaintext)
490 if fit &gt; best_fit:
491 best_fit = fit
492 best_keyword = keyword
493 best_wrap_alphabet = wrap_alphabet
494 return (best_keyword, best_wrap_alphabet), best_fit</code></pre>
495 </details>
496 </dd>
497 <dt id="szyfrow.keyword_cipher.keyword_break_worker"><code class="name flex">
498 <span>def <span class="ident">keyword_break_worker</span></span>(<span>message, keyword, wrap_alphabet, fitness)</span>
499 </code></dt>
500 <dd>
501 <div class="desc"></div>
502 <details class="source">
503 <summary>
504 <span>Expand source code</span>
505 </summary>
506 <pre><code class="python">def keyword_break_worker(message, keyword, wrap_alphabet, fitness):
507 plaintext = keyword_decipher(message, keyword, wrap_alphabet)
508 fit = fitness(plaintext)
509 return (keyword, wrap_alphabet), fit</code></pre>
510 </details>
511 </dd>
512 <dt id="szyfrow.keyword_cipher.keyword_cipher_alphabet_of"><code class="name flex">
513 <span>def <span class="ident">keyword_cipher_alphabet_of</span></span>(<span>keyword, wrap_alphabet=KeywordWrapAlphabet.from_a)</span>
514 </code></dt>
515 <dd>
516 <div class="desc"><p>Find the cipher alphabet given a keyword.</p>
517 <p><a href="#szyfrow.keyword_cipher.KeywordWrapAlphabet"><code>wrap_alphabet</code></a> controls
518 how the rest of the alphabet is added after the keyword.</p>
519 <pre><code class="language-python-repl">&gt;&gt;&gt; keyword_cipher_alphabet_of('bayes')
520 'bayescdfghijklmnopqrtuvwxz'
521 &gt;&gt;&gt; keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_a)
522 'bayescdfghijklmnopqrtuvwxz'
523 &gt;&gt;&gt; keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_last)
524 'bayestuvwxzcdfghijklmnopqr'
525 &gt;&gt;&gt; keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_largest)
526 'bayeszcdfghijklmnopqrtuvwx'
527 </code></pre></div>
528 <details class="source">
529 <summary>
530 <span>Expand source code</span>
531 </summary>
532 <pre><code class="python">def keyword_cipher_alphabet_of(keyword, wrap_alphabet=KeywordWrapAlphabet.from_a):
533 &#34;&#34;&#34;Find the cipher alphabet given a keyword.
534
535 [`wrap_alphabet`](#szyfrow.keyword_cipher.KeywordWrapAlphabet) controls
536 how the rest of the alphabet is added after the keyword.
537
538 &gt;&gt;&gt; keyword_cipher_alphabet_of(&#39;bayes&#39;)
539 &#39;bayescdfghijklmnopqrtuvwxz&#39;
540 &gt;&gt;&gt; keyword_cipher_alphabet_of(&#39;bayes&#39;, KeywordWrapAlphabet.from_a)
541 &#39;bayescdfghijklmnopqrtuvwxz&#39;
542 &gt;&gt;&gt; keyword_cipher_alphabet_of(&#39;bayes&#39;, KeywordWrapAlphabet.from_last)
543 &#39;bayestuvwxzcdfghijklmnopqr&#39;
544 &gt;&gt;&gt; keyword_cipher_alphabet_of(&#39;bayes&#39;, KeywordWrapAlphabet.from_largest)
545 &#39;bayeszcdfghijklmnopqrtuvwx&#39;
546 &#34;&#34;&#34;
547 if wrap_alphabet == KeywordWrapAlphabet.from_a:
548 cipher_alphabet = cat(deduplicate(sanitise(keyword) +
549 string.ascii_lowercase))
550 else:
551 if wrap_alphabet == KeywordWrapAlphabet.from_last:
552 last_keyword_letter = deduplicate(sanitise(keyword))[-1]
553 else:
554 last_keyword_letter = sorted(sanitise(keyword))[-1]
555 last_keyword_position = string.ascii_lowercase.find(
556 last_keyword_letter) + 1
557 cipher_alphabet = cat(
558 deduplicate(sanitise(keyword) +
559 string.ascii_lowercase[last_keyword_position:] +
560 string.ascii_lowercase))
561 return cipher_alphabet</code></pre>
562 </details>
563 </dd>
564 <dt id="szyfrow.keyword_cipher.keyword_decipher"><code class="name flex">
565 <span>def <span class="ident">keyword_decipher</span></span>(<span>message, keyword, wrap_alphabet=KeywordWrapAlphabet.from_a)</span>
566 </code></dt>
567 <dd>
568 <div class="desc"><p>Deciphers a message with a keyword substitution cipher.
569 wrap_alphabet controls how the rest of the alphabet is added
570 after the keyword.
571 0 : from 'a'
572 1 : from the last letter in the sanitised keyword
573 2 : from the largest letter in the sanitised keyword</p>
574 <pre><code class="language-python-repl">&gt;&gt;&gt; keyword_decipher('rsqr ksqqbds', 'bayes')
575 'test message'
576 &gt;&gt;&gt; keyword_decipher('rsqr ksqqbds', 'bayes', KeywordWrapAlphabet.from_a)
577 'test message'
578 &gt;&gt;&gt; keyword_decipher('lskl dskkbus', 'bayes', KeywordWrapAlphabet.from_last)
579 'test message'
580 &gt;&gt;&gt; keyword_decipher('qspq jsppbcs', 'bayes', KeywordWrapAlphabet.from_largest)
581 'test message'
582 </code></pre></div>
583 <details class="source">
584 <summary>
585 <span>Expand source code</span>
586 </summary>
587 <pre><code class="python">def keyword_decipher(message, keyword, wrap_alphabet=KeywordWrapAlphabet.from_a):
588 &#34;&#34;&#34;Deciphers a message with a keyword substitution cipher.
589 wrap_alphabet controls how the rest of the alphabet is added
590 after the keyword.
591 0 : from &#39;a&#39;
592 1 : from the last letter in the sanitised keyword
593 2 : from the largest letter in the sanitised keyword
594
595 &gt;&gt;&gt; keyword_decipher(&#39;rsqr ksqqbds&#39;, &#39;bayes&#39;)
596 &#39;test message&#39;
597 &gt;&gt;&gt; keyword_decipher(&#39;rsqr ksqqbds&#39;, &#39;bayes&#39;, KeywordWrapAlphabet.from_a)
598 &#39;test message&#39;
599 &gt;&gt;&gt; keyword_decipher(&#39;lskl dskkbus&#39;, &#39;bayes&#39;, KeywordWrapAlphabet.from_last)
600 &#39;test message&#39;
601 &gt;&gt;&gt; keyword_decipher(&#39;qspq jsppbcs&#39;, &#39;bayes&#39;, KeywordWrapAlphabet.from_largest)
602 &#39;test message&#39;
603 &#34;&#34;&#34;
604 cipher_alphabet = keyword_cipher_alphabet_of(keyword, wrap_alphabet)
605 cipher_translation = &#39;&#39;.maketrans(cipher_alphabet, string.ascii_lowercase)
606 return message.lower().translate(cipher_translation)</code></pre>
607 </details>
608 </dd>
609 <dt id="szyfrow.keyword_cipher.keyword_encipher"><code class="name flex">
610 <span>def <span class="ident">keyword_encipher</span></span>(<span>message, keyword, wrap_alphabet=KeywordWrapAlphabet.from_a)</span>
611 </code></dt>
612 <dd>
613 <div class="desc"><p>Enciphers a message with a keyword substitution cipher.
614 wrap_alphabet controls how the rest of the alphabet is added
615 after the keyword.
616 0 : from 'a'
617 1 : from the last letter in the sanitised keyword
618 2 : from the largest letter in the sanitised keyword</p>
619 <pre><code class="language-python-repl">&gt;&gt;&gt; keyword_encipher('test message', 'bayes')
620 'rsqr ksqqbds'
621 &gt;&gt;&gt; keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_a)
622 'rsqr ksqqbds'
623 &gt;&gt;&gt; keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_last)
624 'lskl dskkbus'
625 &gt;&gt;&gt; keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_largest)
626 'qspq jsppbcs'
627 </code></pre></div>
628 <details class="source">
629 <summary>
630 <span>Expand source code</span>
631 </summary>
632 <pre><code class="python">def keyword_encipher(message, keyword, wrap_alphabet=KeywordWrapAlphabet.from_a):
633 &#34;&#34;&#34;Enciphers a message with a keyword substitution cipher.
634 wrap_alphabet controls how the rest of the alphabet is added
635 after the keyword.
636 0 : from &#39;a&#39;
637 1 : from the last letter in the sanitised keyword
638 2 : from the largest letter in the sanitised keyword
639
640 &gt;&gt;&gt; keyword_encipher(&#39;test message&#39;, &#39;bayes&#39;)
641 &#39;rsqr ksqqbds&#39;
642 &gt;&gt;&gt; keyword_encipher(&#39;test message&#39;, &#39;bayes&#39;, KeywordWrapAlphabet.from_a)
643 &#39;rsqr ksqqbds&#39;
644 &gt;&gt;&gt; keyword_encipher(&#39;test message&#39;, &#39;bayes&#39;, KeywordWrapAlphabet.from_last)
645 &#39;lskl dskkbus&#39;
646 &gt;&gt;&gt; keyword_encipher(&#39;test message&#39;, &#39;bayes&#39;, KeywordWrapAlphabet.from_largest)
647 &#39;qspq jsppbcs&#39;
648 &#34;&#34;&#34;
649 cipher_alphabet = keyword_cipher_alphabet_of(keyword, wrap_alphabet)
650 cipher_translation = &#39;&#39;.maketrans(string.ascii_lowercase, cipher_alphabet)
651 return unaccent(message).lower().translate(cipher_translation)</code></pre>
652 </details>
653 </dd>
654 <dt id="szyfrow.keyword_cipher.lcat"><code class="name flex">
655 <span>def <span class="ident">lcat</span></span>(<span>iterable, /)</span>
656 </code></dt>
657 <dd>
658 <div class="desc"><p>Concatenate any number of strings.</p>
659 <p>The string whose method is called is inserted in between each given string.
660 The result is returned as a new string.</p>
661 <p>Example: '.'.join(['ab', 'pq', 'rs']) -&gt; 'ab.pq.rs'</p></div>
662 </dd>
663 <dt id="szyfrow.keyword_cipher.monoalphabetic_break_hillclimbing"><code class="name flex">
664 <span>def <span class="ident">monoalphabetic_break_hillclimbing</span></span>(<span>message, workers=10, max_iterations=20000, plain_alphabet=None, cipher_alphabet=None, swap_index_finder=None, fitness=&lt;function Pletters&gt;, chunksize=1)</span>
665 </code></dt>
666 <dd>
667 <div class="desc"><p>Break a monalphabetic substitution cipher using hillclimbing to
668 guess the keyword. Hillclimbing is done by using the simulated annealing
669 approach with a temperature of zero.</p>
670 <p>This version uses a several workers.</p></div>
671 <details class="source">
672 <summary>
673 <span>Expand source code</span>
674 </summary>
675 <pre><code class="python">def monoalphabetic_break_hillclimbing(message,
676 workers=10,
677 max_iterations=20000,
678 plain_alphabet=None,
679 cipher_alphabet=None,
680 swap_index_finder=None,
681 fitness=Pletters, chunksize=1):
682 &#34;&#34;&#34;Break a monalphabetic substitution cipher using hillclimbing to
683 guess the keyword. Hillclimbing is done by using the simulated annealing
684 approach with a temperature of zero.
685
686 This version uses a several workers.
687 &#34;&#34;&#34;
688 return monoalphabetic_sa_break(message,
689 workers=workers,
690 initial_temperature=0,
691 max_iterations=max_iterations,
692 plain_alphabet=plain_alphabet,
693 cipher_alphabet=cipher_alphabet,
694 swap_index_finder=swap_index_finder,
695 fitness=fitness, chunksize=chunksize)</code></pre>
696 </details>
697 </dd>
698 <dt id="szyfrow.keyword_cipher.monoalphabetic_break_hillclimbing_single"><code class="name flex">
699 <span>def <span class="ident">monoalphabetic_break_hillclimbing_single</span></span>(<span>message, max_iterations=20000, plain_alphabet=None, cipher_alphabet=None, swap_index_finder=None, fitness=&lt;function Pletters&gt;, chunksize=1)</span>
700 </code></dt>
701 <dd>
702 <div class="desc"><p>Break a monalphabetic substitution cipher using hillclimbing to
703 guess the keyword. Hillclimbing is done by using the simulated annealing
704 approach with a temperature of zero.</p>
705 <p>This version uses a single worker.</p></div>
706 <details class="source">
707 <summary>
708 <span>Expand source code</span>
709 </summary>
710 <pre><code class="python">def monoalphabetic_break_hillclimbing_single(message,
711 max_iterations=20000,
712 plain_alphabet=None,
713 cipher_alphabet=None,
714 swap_index_finder=None,
715 fitness=Pletters, chunksize=1):
716 &#34;&#34;&#34;Break a monalphabetic substitution cipher using hillclimbing to
717 guess the keyword. Hillclimbing is done by using the simulated annealing
718 approach with a temperature of zero.
719
720 This version uses a single worker.
721 &#34;&#34;&#34;
722 return monoalphabetic_sa_break(message,
723 workers=1,
724 initial_temperature=0,
725 max_iterations=max_iterations,
726 plain_alphabet=plain_alphabet,
727 cipher_alphabet=cipher_alphabet,
728 swap_index_finder=swap_index_finder,
729 fitness=fitness, chunksize=chunksize)</code></pre>
730 </details>
731 </dd>
732 <dt id="szyfrow.keyword_cipher.monoalphabetic_sa_break"><code class="name flex">
733 <span>def <span class="ident">monoalphabetic_sa_break</span></span>(<span>message, workers=10, initial_temperature=200, max_iterations=20000, plain_alphabet=None, cipher_alphabet=None, swap_index_finder=None, fitness=&lt;function Ptrigrams&gt;, chunksize=1)</span>
734 </code></dt>
735 <dd>
736 <div class="desc"><p>Break a monalphabetic substitution cipher using simulated annealing to
737 guess the keyword. This function just sets up a stable of workers who
738 do the actual work, implemented as
739 <code><a title="szyfrow.keyword_cipher.monoalphabetic_sa_break_worker" href="#szyfrow.keyword_cipher.monoalphabetic_sa_break_worker">monoalphabetic_sa_break_worker()</a></code>.</p>
740 <p>See a <a href="https://work.njae.me.uk/2019/07/08/simulated-annealing-and-breaking-substitution-ciphers/">post on simulated annealing</a>
741 for detail on how this works.</p></div>
742 <details class="source">
743 <summary>
744 <span>Expand source code</span>
745 </summary>
746 <pre><code class="python">def monoalphabetic_sa_break(message, workers=10,
747 initial_temperature=200,
748 max_iterations=20000,
749 plain_alphabet=None,
750 cipher_alphabet=None,
751 swap_index_finder=None,
752 fitness=Ptrigrams, chunksize=1):
753 &#34;&#34;&#34;Break a monalphabetic substitution cipher using simulated annealing to
754 guess the keyword. This function just sets up a stable of workers who
755 do the actual work, implemented as
756 `szyfrow.keyword_cipher.monoalphabetic_sa_break_worker`.
757
758 See a [post on simulated annealing](https://work.njae.me.uk/2019/07/08/simulated-annealing-and-breaking-substitution-ciphers/)
759 for detail on how this works.
760 &#34;&#34;&#34;
761 worker_args = []
762 ciphertext = sanitise(message)
763 if swap_index_finder is None:
764 swap_index_finder = gaussian_swap_index
765
766 for i in range(workers):
767 if plain_alphabet is None:
768 used_plain_alphabet = string.ascii_lowercase
769 else:
770 used_plain_alphabet = plain_alphabet
771 if cipher_alphabet is None:
772 used_cipher_alphabet = list(string.ascii_lowercase)
773 random.shuffle(used_cipher_alphabet)
774 used_cipher_alphabet = cat(used_cipher_alphabet)
775 else:
776 used_cipher_alphabet = cipher_alphabet
777 # if not plain_alphabet:
778 # plain_alphabet = string.ascii_lowercase
779 # if not cipher_alphabet:
780 # cipher_alphabet = list(string.ascii_lowercase)
781 # random.shuffle(cipher_alphabet)
782 # cipher_alphabet = cat(cipher_alphabet)
783 worker_args.append((ciphertext, used_plain_alphabet, used_cipher_alphabet,
784 swap_index_finder,
785 initial_temperature, max_iterations, fitness,
786 i))
787 with multiprocessing.Pool() as pool:
788 breaks = pool.starmap(monoalphabetic_sa_break_worker,
789 worker_args, chunksize)
790 return max(breaks, key=lambda k: k[1])</code></pre>
791 </details>
792 </dd>
793 <dt id="szyfrow.keyword_cipher.monoalphabetic_sa_break_worker"><code class="name flex">
794 <span>def <span class="ident">monoalphabetic_sa_break_worker</span></span>(<span>message, plain_alphabet, cipher_alphabet, swap_index_finder, t0, max_iterations, fitness, logID)</span>
795 </code></dt>
796 <dd>
797 <div class="desc"><p>One thread of a simulated annealing run.
798 See a <a href="https://work.njae.me.uk/2019/07/08/simulated-annealing-and-breaking-substitution-ciphers/">post on simulated annealing</a>
799 for detail on how this works.</p></div>
800 <details class="source">
801 <summary>
802 <span>Expand source code</span>
803 </summary>
804 <pre><code class="python">def monoalphabetic_sa_break_worker(message, plain_alphabet, cipher_alphabet,
805 swap_index_finder,
806 t0, max_iterations, fitness,
807 logID):
808 &#34;&#34;&#34;One thread of a simulated annealing run.
809 See a [post on simulated annealing](https://work.njae.me.uk/2019/07/08/simulated-annealing-and-breaking-substitution-ciphers/)
810 for detail on how this works.
811 &#34;&#34;&#34;
812 def swap(letters, i, j):
813 if i &gt; j:
814 i, j = j, i
815 if i == j:
816 return letters
817 else:
818 return (letters[:i] + letters[j] + letters[i+1:j] + letters[i] +
819 letters[j+1:])
820
821 temperature = t0
822
823 dt = t0 / (0.9 * max_iterations)
824
825 current_alphabet = cipher_alphabet
826 alphabet = current_alphabet
827 cipher_translation = &#39;&#39;.maketrans(current_alphabet, plain_alphabet)
828 plaintext = message.translate(cipher_translation)
829 current_fitness = fitness(plaintext)
830
831 best_alphabet = current_alphabet
832 best_fitness = current_fitness
833 best_plaintext = plaintext
834
835 # print(&#39;starting for&#39;, max_iterations)
836 for i in range(max_iterations):
837 swap_a = random.randrange(26)
838 # swap_b = (swap_a + int(random.gauss(0, 4))) % 26
839 swap_b = swap_index_finder(swap_a)
840 alphabet = swap(current_alphabet, swap_a, swap_b)
841 cipher_translation = &#39;&#39;.maketrans(alphabet, plain_alphabet)
842 plaintext = message.translate(cipher_translation)
843 new_fitness = fitness(plaintext)
844 try:
845 sa_chance = math.exp((new_fitness - current_fitness) / temperature)
846 except (OverflowError, ZeroDivisionError):
847 # print(&#39;exception triggered: new_fit {}, current_fit {}, temp {}&#39;.format(new_fitness, current_fitness, temperature))
848 sa_chance = 0
849 if (new_fitness &gt; current_fitness or random.random() &lt; sa_chance):
850 current_fitness = new_fitness
851 current_alphabet = alphabet
852
853 if current_fitness &gt; best_fitness:
854 best_alphabet = current_alphabet
855 best_fitness = current_fitness
856 best_plaintext = plaintext
857
858 temperature = max(temperature - dt, 0.001)
859
860 return best_alphabet, best_fitness # current_alphabet, current_fitness</code></pre>
861 </details>
862 </dd>
863 <dt id="szyfrow.keyword_cipher.uniform_swap_index"><code class="name flex">
864 <span>def <span class="ident">uniform_swap_index</span></span>(<span>a)</span>
865 </code></dt>
866 <dd>
867 <div class="desc"><p>Return an index to use as the partner of <code>a</code> in a swap. The partners
868 are drawn from a uniform distribution.</p></div>
869 <details class="source">
870 <summary>
871 <span>Expand source code</span>
872 </summary>
873 <pre><code class="python">def uniform_swap_index(a):
874 &#34;&#34;&#34;Return an index to use as the partner of `a` in a swap. The partners
875 are drawn from a uniform distribution.
876 &#34;&#34;&#34;
877 return random.randrange(26)</code></pre>
878 </details>
879 </dd>
880 <dt id="szyfrow.keyword_cipher.wcat"><code class="name flex">
881 <span>def <span class="ident">wcat</span></span>(<span>iterable, /)</span>
882 </code></dt>
883 <dd>
884 <div class="desc"><p>Concatenate any number of strings.</p>
885 <p>The string whose method is called is inserted in between each given string.
886 The result is returned as a new string.</p>
887 <p>Example: '.'.join(['ab', 'pq', 'rs']) -&gt; 'ab.pq.rs'</p></div>
888 </dd>
889 </dl>
890 </section>
891 <section>
892 <h2 class="section-title" id="header-classes">Classes</h2>
893 <dl>
894 <dt id="szyfrow.keyword_cipher.KeywordWrapAlphabet"><code class="flex name class">
895 <span>class <span class="ident">KeywordWrapAlphabet</span></span>
896 <span>(</span><span>value, names=None, *, module=None, qualname=None, type=None, start=1)</span>
897 </code></dt>
898 <dd>
899 <div class="desc"><p>Ways to list the rest of the alphabet after use of a keyword.</p>
900 <ul>
901 <li><code>from_a</code> : continue the alphabet from 'a': <code>bayescdfg&hellip;</code></li>
902 <li><code>from_last</code>: continue from the last letter of the keyword:
903 <code>bayestuvwxyzcdf&hellip;</code></li>
904 <li><code>from_largest</code>: continue from the "largest" letter of the keyword:
905 <code>bayeszcdfg&hellip;</code></li>
906 </ul></div>
907 <details class="source">
908 <summary>
909 <span>Expand source code</span>
910 </summary>
911 <pre><code class="python">class KeywordWrapAlphabet(Enum):
912 &#34;&#34;&#34;Ways to list the rest of the alphabet after use of a keyword.
913
914 * `from_a` : continue the alphabet from &#39;a&#39;: `bayescdfg...`
915 * `from_last`: continue from the last letter of the keyword:
916 `bayestuvwxyzcdf...`
917 * `from_largest`: continue from the &#34;largest&#34; letter of the keyword:
918 `bayeszcdfg...`
919 &#34;&#34;&#34;
920 from_a = 1
921 from_last = 2
922 from_largest = 3</code></pre>
923 </details>
924 <h3>Ancestors</h3>
925 <ul class="hlist">
926 <li>enum.Enum</li>
927 </ul>
928 <h3>Class variables</h3>
929 <dl>
930 <dt id="szyfrow.keyword_cipher.KeywordWrapAlphabet.from_a"><code class="name">var <span class="ident">from_a</span></code></dt>
931 <dd>
932 <div class="desc"></div>
933 </dd>
934 <dt id="szyfrow.keyword_cipher.KeywordWrapAlphabet.from_largest"><code class="name">var <span class="ident">from_largest</span></code></dt>
935 <dd>
936 <div class="desc"></div>
937 </dd>
938 <dt id="szyfrow.keyword_cipher.KeywordWrapAlphabet.from_last"><code class="name">var <span class="ident">from_last</span></code></dt>
939 <dd>
940 <div class="desc"></div>
941 </dd>
942 </dl>
943 </dd>
944 </dl>
945 </section>
946 </article>
947 <nav id="sidebar">
948 <h1>Index</h1>
949 <div class="toc">
950 <ul></ul>
951 </div>
952 <ul id="index">
953 <li><h3>Super-module</h3>
954 <ul>
955 <li><code><a title="szyfrow" href="index.html">szyfrow</a></code></li>
956 </ul>
957 </li>
958 <li><h3><a href="#header-functions">Functions</a></h3>
959 <ul class="">
960 <li><code><a title="szyfrow.keyword_cipher.cat" href="#szyfrow.keyword_cipher.cat">cat</a></code></li>
961 <li><code><a title="szyfrow.keyword_cipher.gaussian_swap_index" href="#szyfrow.keyword_cipher.gaussian_swap_index">gaussian_swap_index</a></code></li>
962 <li><code><a title="szyfrow.keyword_cipher.keyword_break" href="#szyfrow.keyword_cipher.keyword_break">keyword_break</a></code></li>
963 <li><code><a title="szyfrow.keyword_cipher.keyword_break_single_thread" href="#szyfrow.keyword_cipher.keyword_break_single_thread">keyword_break_single_thread</a></code></li>
964 <li><code><a title="szyfrow.keyword_cipher.keyword_break_worker" href="#szyfrow.keyword_cipher.keyword_break_worker">keyword_break_worker</a></code></li>
965 <li><code><a title="szyfrow.keyword_cipher.keyword_cipher_alphabet_of" href="#szyfrow.keyword_cipher.keyword_cipher_alphabet_of">keyword_cipher_alphabet_of</a></code></li>
966 <li><code><a title="szyfrow.keyword_cipher.keyword_decipher" href="#szyfrow.keyword_cipher.keyword_decipher">keyword_decipher</a></code></li>
967 <li><code><a title="szyfrow.keyword_cipher.keyword_encipher" href="#szyfrow.keyword_cipher.keyword_encipher">keyword_encipher</a></code></li>
968 <li><code><a title="szyfrow.keyword_cipher.lcat" href="#szyfrow.keyword_cipher.lcat">lcat</a></code></li>
969 <li><code><a title="szyfrow.keyword_cipher.monoalphabetic_break_hillclimbing" href="#szyfrow.keyword_cipher.monoalphabetic_break_hillclimbing">monoalphabetic_break_hillclimbing</a></code></li>
970 <li><code><a title="szyfrow.keyword_cipher.monoalphabetic_break_hillclimbing_single" href="#szyfrow.keyword_cipher.monoalphabetic_break_hillclimbing_single">monoalphabetic_break_hillclimbing_single</a></code></li>
971 <li><code><a title="szyfrow.keyword_cipher.monoalphabetic_sa_break" href="#szyfrow.keyword_cipher.monoalphabetic_sa_break">monoalphabetic_sa_break</a></code></li>
972 <li><code><a title="szyfrow.keyword_cipher.monoalphabetic_sa_break_worker" href="#szyfrow.keyword_cipher.monoalphabetic_sa_break_worker">monoalphabetic_sa_break_worker</a></code></li>
973 <li><code><a title="szyfrow.keyword_cipher.uniform_swap_index" href="#szyfrow.keyword_cipher.uniform_swap_index">uniform_swap_index</a></code></li>
974 <li><code><a title="szyfrow.keyword_cipher.wcat" href="#szyfrow.keyword_cipher.wcat">wcat</a></code></li>
975 </ul>
976 </li>
977 <li><h3><a href="#header-classes">Classes</a></h3>
978 <ul>
979 <li>
980 <h4><code><a title="szyfrow.keyword_cipher.KeywordWrapAlphabet" href="#szyfrow.keyword_cipher.KeywordWrapAlphabet">KeywordWrapAlphabet</a></code></h4>
981 <ul class="">
982 <li><code><a title="szyfrow.keyword_cipher.KeywordWrapAlphabet.from_a" href="#szyfrow.keyword_cipher.KeywordWrapAlphabet.from_a">from_a</a></code></li>
983 <li><code><a title="szyfrow.keyword_cipher.KeywordWrapAlphabet.from_largest" href="#szyfrow.keyword_cipher.KeywordWrapAlphabet.from_largest">from_largest</a></code></li>
984 <li><code><a title="szyfrow.keyword_cipher.KeywordWrapAlphabet.from_last" href="#szyfrow.keyword_cipher.KeywordWrapAlphabet.from_last">from_last</a></code></li>
985 </ul>
986 </li>
987 </ul>
988 </li>
989 </ul>
990 </nav>
991 </main>
992 <footer id="footer">
993 <p>Generated by <a href="https://pdoc3.github.io/pdoc"><cite>pdoc</cite> 0.9.2</a>.</p>
994 </footer>
995 </body>
996 </html>