Added bombe
authorNeil Smith <neil.git@njae.me.uk>
Tue, 17 May 2016 10:58:12 +0000 (11:58 +0100)
committerNeil Smith <neil.git@njae.me.uk>
Wed, 4 Oct 2017 08:21:12 +0000 (09:21 +0100)
bombe.ipynb [new file with mode: 0644]
enigma.py

diff --git a/bombe.ipynb b/bombe.ipynb
new file mode 100644 (file)
index 0000000..ab1b457
--- /dev/null
@@ -0,0 +1,1836 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 797,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "import string\n",
+    "import collections\n",
+    "import multiprocessing\n",
+    "import itertools\n",
+    "from enigma import *"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 798,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "# wheel_i_spec = 'ekmflgdqvzntowyhxuspaibrcj'\n",
+    "# wheel_ii_spec = 'ajdksiruxblhwtmcqgznpyfvoe'\n",
+    "# wheel_iii_spec = 'bdfhjlcprtxvznyeiwgakmusqo'\n",
+    "# wheel_iv_spec = 'esovpzjayquirhxlnftgkdcmwb'\n",
+    "# wheel_v_spec = 'vzbrgityupsdnhlxawmjqofeck'\n",
+    "# wheel_vi_spec = 'jpgvoumfyqbenhzrdkasxlictw'\n",
+    "# wheel_vii_spec = 'nzjhgrcxmyswboufaivlpekqdt'\n",
+    "# wheel_viii_spec = 'fkqhtlxocbjspdzramewniuygv'\n",
+    "# beta_wheel_spec = 'leyjvcnixwpbqmdrtakzgfuhos'\n",
+    "# gamma_wheel_spec = 'fsokanuerhmbtiycwlqpzxvgjd'\n",
+    "\n",
+    "# wheel_i_pegs = ['q']\n",
+    "# wheel_ii_pegs = ['e']\n",
+    "# wheel_iii_pegs = ['v']\n",
+    "# wheel_iv_pegs = ['j']\n",
+    "# wheel_v_pegs = ['z']\n",
+    "# wheel_vi_pegs = ['z', 'm']\n",
+    "# wheel_vii_pegs = ['z', 'm']\n",
+    "# wheel_viii_pegs = ['z', 'm']\n",
+    "\n",
+    "# reflector_b_spec = 'ay br cu dh eq fs gl ip jx kn mo tz vw'\n",
+    "# reflector_c_spec = 'af bv cp dj ei go hy kr lz mx nw tq su'"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 799,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "class Bank(object):\n",
+    "    def __init__(self):\n",
+    "        self.signals = dict(zip(string.ascii_lowercase, [False]*len(string.ascii_lowercase)))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 800,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'a': False,\n",
+       " 'b': False,\n",
+       " 'c': False,\n",
+       " 'd': False,\n",
+       " 'e': False,\n",
+       " 'f': False,\n",
+       " 'g': False,\n",
+       " 'h': False,\n",
+       " 'i': False,\n",
+       " 'j': False,\n",
+       " 'k': False,\n",
+       " 'l': False,\n",
+       " 'm': False,\n",
+       " 'n': False,\n",
+       " 'o': False,\n",
+       " 'p': False,\n",
+       " 'q': False,\n",
+       " 'r': False,\n",
+       " 's': False,\n",
+       " 't': False,\n",
+       " 'u': False,\n",
+       " 'v': False,\n",
+       " 'w': False,\n",
+       " 'x': False,\n",
+       " 'y': False,\n",
+       " 'z': False}"
+      ]
+     },
+     "execution_count": 800,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "c1 = Bank()\n",
+    "c1.signals"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 801,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "Signal = collections.namedtuple('Signal', ['bank', 'wire'])\n",
+    "Connection = collections.namedtuple('Connection', ['banks', 'scrambler'])\n",
+    "MenuItem = collections.namedtuple('MenuIem', ['before', 'after', 'number'])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 802,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "class Scrambler(object):\n",
+    "    def __init__(self, wheel1_spec, wheel2_spec, wheel3_spec, reflector_spec,\n",
+    "                 wheel1_pos='a', wheel2_pos='a', wheel3_pos='a'):\n",
+    "        self.wheel1 = SimpleWheel(wheel1_spec, position=wheel1_pos)\n",
+    "        self.wheel2 = SimpleWheel(wheel2_spec, position=wheel2_pos)\n",
+    "        self.wheel3 = SimpleWheel(wheel3_spec, position=wheel3_pos)\n",
+    "        self.reflector = Reflector(reflector_spec)\n",
+    "    \n",
+    "    def __getattribute__(self, name):\n",
+    "        if name=='wheel_positions':\n",
+    "            return self.wheel1.position, self.wheel2.position, self.wheel3.position \n",
+    "        elif name=='wheel_positions_l':\n",
+    "            return self.wheel1.position_l, self.wheel2.position_l, self.wheel3.position_l \n",
+    "        else:\n",
+    "            return object.__getattribute__(self, name)\n",
+    "    \n",
+    "    def advance(self, wheel1=False, wheel2=False, wheel3=True):\n",
+    "        if wheel1: self.wheel1.advance()\n",
+    "        if wheel2: self.wheel2.advance()\n",
+    "        if wheel3: self.wheel3.advance()\n",
+    "            \n",
+    "    def lookup(self, letter):\n",
+    "        a = self.wheel3.forward(letter)\n",
+    "        b = self.wheel2.forward(a)\n",
+    "        c = self.wheel1.forward(b)\n",
+    "        d = self.reflector.forward(c)\n",
+    "        e = self.wheel1.backward(d)\n",
+    "        f = self.wheel2.backward(e)\n",
+    "        g = self.wheel3.backward(f)\n",
+    "        return g\n",
+    "    \n",
+    "    def set_positions(self, wheel1_pos, wheel2_pos, wheel3_pos):\n",
+    "        self.wheel1.set_position(wheel1_pos)\n",
+    "        self.wheel2.set_position(wheel2_pos)\n",
+    "        self.wheel3.set_position(wheel3_pos)        "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 803,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "class Bombe(object):\n",
+    "    def __init__(self, wheel1_spec, wheel2_spec, wheel3_spec, reflector_spec,\n",
+    "                menu=None, start_signal=None, use_diagonal_board=True, verify_plugboard=True):\n",
+    "        self.connections = []\n",
+    "        self.wheel1_spec = wheel1_spec\n",
+    "        self.wheel2_spec = wheel2_spec\n",
+    "        self.wheel3_spec = wheel3_spec\n",
+    "        self.reflector_spec = reflector_spec\n",
+    "        if menu:\n",
+    "            self.read_menu(menu)\n",
+    "        if start_signal:\n",
+    "            self.test_start = start_signal\n",
+    "        self.use_diagonal_board = use_diagonal_board\n",
+    "        self.verify_plugboard = verify_plugboard\n",
+    "        \n",
+    "    def __getattribute__(self, name):\n",
+    "        if name=='wheel_positions':\n",
+    "            return self.connections[0].scrambler.wheel_positions\n",
+    "        elif name=='wheel_positions_l':\n",
+    "            return self.connections[0].scrambler.wheel_positions_l\n",
+    "        else:\n",
+    "            return object.__getattribute__(self, name)\n",
+    "        \n",
+    "    def __call__(self, start_positions):\n",
+    "        return start_positions, self.test(start_positions=start_positions, \n",
+    "                                          use_diagonal_board=self.use_diagonal_board,\n",
+    "                                          verify_plugboard=self.verify_plugboard)\n",
+    "        \n",
+    "    def add_connection(self, bank_before, bank_after, scrambler):\n",
+    "        self.connections += [Connection([bank_before, bank_after], scrambler)]\n",
+    "        \n",
+    "    def read_menu(self, menu):\n",
+    "        for item in menu:\n",
+    "            scrambler = Scrambler(self.wheel1_spec, self.wheel2_spec, self.wheel3_spec,\n",
+    "                                  self.reflector_spec,\n",
+    "                                  wheel3_pos=unpos(item.number - 1))\n",
+    "            self.add_connection(item.before, item.after, scrambler)\n",
+    "        most_common_letter = (collections.Counter(m.before for m in menu) + \\\n",
+    "                              collections.Counter(m.after for m in menu)).most_common(1)[0][0]\n",
+    "        self.test_start = Signal(most_common_letter, most_common_letter)\n",
+    "        \n",
+    "    def set_positions(self, wheel1_pos, wheel2_pos, wheel3_pos):\n",
+    "        for i, c in enumerate(self.connections):\n",
+    "            c.scrambler.set_positions(wheel1_pos, wheel2_pos, unpos(pos(wheel3_pos) + i))\n",
+    "    \n",
+    "    def test(self, initial_signal=None, start_positions=None, use_diagonal_board=True,\n",
+    "            verify_plugboard=True):\n",
+    "        self.banks = {label: \n",
+    "                      dict(zip(string.ascii_lowercase, [False]*len(string.ascii_lowercase)))\n",
+    "                      for label in string.ascii_lowercase}\n",
+    "        if start_positions:\n",
+    "            self.set_positions(*start_positions)\n",
+    "        if not initial_signal:\n",
+    "            initial_signal = self.test_start\n",
+    "        self.pending = [initial_signal]\n",
+    "        self.propagate(use_diagonal_board)\n",
+    "        live_wire_count = len([self.banks[self.test_start.bank][w] \n",
+    "                    for w in self.banks[self.test_start.bank] \n",
+    "                    if self.banks[self.test_start.bank][w]])\n",
+    "        if live_wire_count < 26:\n",
+    "            if verify_plugboard:\n",
+    "                possibles = self.possible_plugboards()\n",
+    "                return all(s0.isdisjoint(s1) for s0 in possibles for s1 in possibles if s0 != s1)\n",
+    "            else:\n",
+    "                return True\n",
+    "        else:\n",
+    "            return False\n",
+    "        \n",
+    "    def propagate(self, use_diagonal_board):\n",
+    "        while self.pending:\n",
+    "            current = self.pending[0]\n",
+    "            # print(\"processing\", current)\n",
+    "            self.pending = self.pending[1:]\n",
+    "            if not self.banks[current.bank][current.wire]:\n",
+    "                self.banks[current.bank][current.wire] = True\n",
+    "                if use_diagonal_board:\n",
+    "                    self.pending += [Signal(current.wire, current.bank)]\n",
+    "                for c in self.connections:\n",
+    "                    if current.bank in c.banks:\n",
+    "                        other_bank = [b for b in c.banks if b != current.bank][0]\n",
+    "                        other_wire = c.scrambler.lookup(current.wire)\n",
+    "                        # print(\"  adding\", other_bank, other_wire, \"because\", c.banks)\n",
+    "                        self.pending += [Signal(other_bank, other_wire)]\n",
+    "    \n",
+    "    def run(self, run_start=None, wheel1_pos='a', wheel2_pos='a', wheel3_pos='a', use_diagonal_board=True):\n",
+    "        if not run_start:\n",
+    "            run_start = self.test_start\n",
+    "        self.solutions = []\n",
+    "        self.set_positions(wheel1_pos, wheel2_pos, wheel3_pos)\n",
+    "        for run_index in range(26*26*26):\n",
+    "            if self.test(initial_signal=run_start, use_diagonal_board=use_diagonal_board):\n",
+    "                self.solutions += [self.connections[0].scrambler.wheel_positions_l]\n",
+    "            advance3 = True\n",
+    "            advance2 = False\n",
+    "            advance1 = False\n",
+    "            if (run_index + 1) % 26 == 0: advance2 = True\n",
+    "            if (run_index + 1) % (26*26) == 0: advance1 = True\n",
+    "            for c in self.connections:\n",
+    "                c.scrambler.advance(advance1, advance2, advance3)\n",
+    "        return self.solutions\n",
+    "    \n",
+    "    def possible_plugboards(self):\n",
+    "        possibles = set()\n",
+    "        for b in self.banks:\n",
+    "            active = [w for w in self.banks[b] if self.banks[b][w]]\n",
+    "            inactive = [w for w in self.banks[b] if not self.banks[b][w]]\n",
+    "            if len(active) == 1:\n",
+    "                possibles = possibles.union({frozenset((b, active[0]))})\n",
+    "            if len(inactive) == 1:\n",
+    "                possibles = possibles.union({frozenset((b, inactive[0]))})\n",
+    "        return possibles"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 804,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "bombe = Bombe(wheel_i_spec, wheel_ii_spec, wheel_iii_spec, reflector_b_spec)\n",
+    "# len(bombe.banks), bombe.banks['a'] == bombe.banks['b']"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 805,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "test_enigma = Enigma(reflector_b_spec, \n",
+    "                wheel_i_spec, wheel_i_pegs,\n",
+    "                wheel_ii_spec, wheel_ii_pegs,\n",
+    "                wheel_iii_spec, wheel_iii_pegs,\n",
+    "                1, 1, 1,\n",
+    "                '')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 806,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'opgndxcrwomnlnecjz'"
+      ]
+     },
+     "execution_count": 806,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "test_enigma.set_wheels('a', 'a', 'a')\n",
+    "pt = 'thisisatestmessage'\n",
+    "ct = test_enigma.encipher(pt)\n",
+    "ct"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 807,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'aas'"
+      ]
+     },
+     "execution_count": 807,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "cat(test_enigma.wheel_positions_l)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 808,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[MenuIem(before='t', after='o', number=1),\n",
+       " MenuIem(before='h', after='p', number=2),\n",
+       " MenuIem(before='i', after='g', number=3),\n",
+       " MenuIem(before='s', after='n', number=4),\n",
+       " MenuIem(before='i', after='d', number=5),\n",
+       " MenuIem(before='s', after='x', number=6),\n",
+       " MenuIem(before='a', after='c', number=7),\n",
+       " MenuIem(before='t', after='r', number=8),\n",
+       " MenuIem(before='e', after='w', number=9),\n",
+       " MenuIem(before='s', after='o', number=10),\n",
+       " MenuIem(before='t', after='m', number=11),\n",
+       " MenuIem(before='m', after='n', number=12),\n",
+       " MenuIem(before='e', after='l', number=13),\n",
+       " MenuIem(before='s', after='n', number=14),\n",
+       " MenuIem(before='s', after='e', number=15),\n",
+       " MenuIem(before='a', after='c', number=16),\n",
+       " MenuIem(before='g', after='j', number=17),\n",
+       " MenuIem(before='e', after='z', number=18)]"
+      ]
+     },
+     "execution_count": 808,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "menu = [MenuItem(p, c, i+1) for i, (p, c) in enumerate(zip(pt, ct))]\n",
+    "menu"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 809,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "def make_menu(plaintext, ciphertext):\n",
+    "    return [MenuItem(p, c, i+1) \n",
+    "            for i, (p, c) in enumerate(zip(plaintext, ciphertext))]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 810,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[MenuIem(before='t', after='o', number=1),\n",
+       " MenuIem(before='h', after='p', number=2),\n",
+       " MenuIem(before='i', after='g', number=3),\n",
+       " MenuIem(before='s', after='n', number=4),\n",
+       " MenuIem(before='i', after='d', number=5),\n",
+       " MenuIem(before='s', after='x', number=6),\n",
+       " MenuIem(before='a', after='c', number=7),\n",
+       " MenuIem(before='t', after='r', number=8),\n",
+       " MenuIem(before='e', after='w', number=9),\n",
+       " MenuIem(before='s', after='o', number=10),\n",
+       " MenuIem(before='t', after='m', number=11),\n",
+       " MenuIem(before='m', after='n', number=12),\n",
+       " MenuIem(before='e', after='l', number=13),\n",
+       " MenuIem(before='s', after='n', number=14),\n",
+       " MenuIem(before='s', after='e', number=15),\n",
+       " MenuIem(before='a', after='c', number=16),\n",
+       " MenuIem(before='g', after='j', number=17),\n",
+       " MenuIem(before='e', after='z', number=18)]"
+      ]
+     },
+     "execution_count": 810,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "make_menu(pt, ct)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 811,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'s'"
+      ]
+     },
+     "execution_count": 811,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "(collections.Counter(m.before for m in menu) + collections.Counter(m.after for m in menu)).most_common(1)[0][0]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 812,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "bombe.read_menu(menu)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 813,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "18"
+      ]
+     },
+     "execution_count": 813,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "len(bombe.connections)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 814,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "['t', 'o'] aaa\n",
+      "['h', 'p'] aab\n",
+      "['i', 'g'] aac\n",
+      "['s', 'n'] aad\n",
+      "['i', 'd'] aae\n",
+      "['s', 'x'] aaf\n",
+      "['a', 'c'] aag\n",
+      "['t', 'r'] aah\n",
+      "['e', 'w'] aai\n",
+      "['s', 'o'] aaj\n",
+      "['t', 'm'] aak\n",
+      "['m', 'n'] aal\n",
+      "['e', 'l'] aam\n",
+      "['s', 'n'] aan\n",
+      "['s', 'e'] aao\n",
+      "['a', 'c'] aap\n",
+      "['g', 'j'] aaq\n",
+      "['e', 'z'] aar\n"
+     ]
+    }
+   ],
+   "source": [
+    "for c in bombe.connections:\n",
+    "    print(c.banks, cat(c.scrambler.wheel_positions_l))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 815,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "False"
+      ]
+     },
+     "execution_count": 815,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "bombe.test(Signal('t', 't'))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 816,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'a': True,\n",
+       " 'b': True,\n",
+       " 'c': True,\n",
+       " 'd': True,\n",
+       " 'e': True,\n",
+       " 'f': True,\n",
+       " 'g': True,\n",
+       " 'h': True,\n",
+       " 'i': True,\n",
+       " 'j': True,\n",
+       " 'k': True,\n",
+       " 'l': True,\n",
+       " 'm': True,\n",
+       " 'n': True,\n",
+       " 'o': True,\n",
+       " 'p': True,\n",
+       " 'q': True,\n",
+       " 'r': True,\n",
+       " 's': True,\n",
+       " 't': True,\n",
+       " 'u': True,\n",
+       " 'v': True,\n",
+       " 'w': True,\n",
+       " 'x': True,\n",
+       " 'y': True,\n",
+       " 'z': True}"
+      ]
+     },
+     "execution_count": 816,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "bombe.banks['t']"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 817,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "a : ABCDEFGHIJKLMNOPQRSTUVWXYZ\n",
+      "b : AbCDEfGHIJkLMNOPqRSTuvWXyZ\n",
+      "c : ABCDEFGHIJKLMNOPQRSTUVWXYZ\n",
+      "d : ABCDEFGHIJKLMNOPQRSTUVWXyZ\n",
+      "e : ABCDEFGHIJKLMNOPQRSTUVWXYZ\n",
+      "f : AbCDEfGHIJkLMNOPqRSTuvWXyZ\n",
+      "g : ABCDEFGHIJKLMNOPQRSTuVWXYZ\n",
+      "h : ABCDEFGHIJKLMNOPQRSTUVWXYZ\n",
+      "i : ABCDEFGHIJKLMNOPQRSTUvWXYZ\n",
+      "j : ABCDEFGHIjKLMNOPQRSTUVWXYZ\n",
+      "k : AbCDEfGHIJkLMNOPqRSTuvWXyZ\n",
+      "l : ABCDEFGHIJKLMNOPQRSTUVWXYZ\n",
+      "m : ABCDEFGHIJKLMNOPQRSTUVWXYZ\n",
+      "n : ABCDEFGHIJKLMNOPQRSTUVWXYZ\n",
+      "o : ABCDEFGHIJKLMNOPQRSTUVWXYZ\n",
+      "p : ABCDEFGHIJKLMNOPQRSTUVWXYZ\n",
+      "q : AbCDEfGHIJkLMNOPqRSTuvWXyZ\n",
+      "r : ABCDEFGHIJKLMNOPQRSTUVWXYZ\n",
+      "s : ABCDEFGHIJKLMNOPQRSTUVWXYZ\n",
+      "t : ABCDEFGHIJKLMNOPQRSTUVWXYZ\n",
+      "u : AbCDEfgHIJkLMNOPqRSTuvWXyZ\n",
+      "v : AbCDEfGHiJkLMNOPqRSTuvWXyZ\n",
+      "w : ABCDEFGHIJKLMNOPQRSTUVWXYZ\n",
+      "x : ABCDEFGHIJKLMNOPQRSTUVWXYZ\n",
+      "y : AbCdEfGHIJkLMNOPqRSTuvWXyZ\n",
+      "z : ABCDEFGHIJKLMNOPQRSTUVWXYZ\n"
+     ]
+    }
+   ],
+   "source": [
+    "for b in sorted(bombe.banks):\n",
+    "    print(b, ': ', end='')\n",
+    "    for w in sorted(bombe.banks[b]):\n",
+    "        if bombe.banks[b][w]:\n",
+    "            print(w.upper(), end='')\n",
+    "        else:\n",
+    "            print(w, end='')\n",
+    "    print('')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 818,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "('a', 'a', 'a')"
+      ]
+     },
+     "execution_count": 818,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "bombe.wheel_positions_l"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 819,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "['t', 'o'] pqr\n",
+      "['h', 'p'] pqs\n",
+      "['i', 'g'] pqt\n",
+      "['s', 'n'] pqu\n",
+      "['i', 'd'] pqv\n",
+      "['s', 'x'] pqw\n",
+      "['a', 'c'] pqx\n",
+      "['t', 'r'] pqy\n",
+      "['e', 'w'] pqz\n",
+      "['s', 'o'] pqa\n",
+      "['t', 'm'] pqb\n",
+      "['m', 'n'] pqc\n",
+      "['e', 'l'] pqd\n",
+      "['s', 'n'] pqe\n",
+      "['s', 'e'] pqf\n",
+      "['a', 'c'] pqg\n",
+      "['g', 'j'] pqh\n",
+      "['e', 'z'] pqi\n"
+     ]
+    }
+   ],
+   "source": [
+    "bombe.set_positions('p', 'q', 'r')\n",
+    "for c in bombe.connections:\n",
+    "    print(c.banks, cat(c.scrambler.wheel_positions_l))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 820,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "# bombe.run()\n",
+    "# print('x')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 821,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "a : .b.defghijklmnopqrstuvwxyz\n",
+      "b : a.cde.ghij.lmnop.rst..wx.z\n",
+      "c : .b.defghijklmnopqrstuvwxyz\n",
+      "d : abc.e.gh.jklmnopqrstuvwxyz\n",
+      "e : abcd.fghijklmnopqrstuvwxyz\n",
+      "f : a.c.e.g.ij.lmno..rst..wx.z\n",
+      "g : abcdef.h..klmnopqrstuvwxyz\n",
+      "h : abcde.g.ij.lmno.qrst..wxyz\n",
+      "i : abc.ef.h.jklmnopqrstuvwxyz\n",
+      "j : abcdef.hi.klmnopqrstuvwx.z\n",
+      "k : a.cde.g.ij.lmno..rst..wx.z\n",
+      "l : abcdefghijk.mnopqrstuvwxyz\n",
+      "m : abcdefghijkl.nopqrstuvwxyz\n",
+      "n : abcdefghijklm.opqrstuvwxyz\n",
+      "o : abcdefghijklmn.pqrstuvwxyz\n",
+      "p : abcde.g.ij.lmno.qrst..wxyz\n",
+      "q : a.cde.ghij.lmnop.rst..wx.z\n",
+      "r : abcdefghijklmnopq.stuvwxyz\n",
+      "s : abcdefghijklmnopqr.tuvwxyz\n",
+      "t : abcdefghijklmnopqrs.uvwxyz\n",
+      "u : a.cde.g.ij.lmno..rst..wx.z\n",
+      "v : a.cde.g.ij.lmno..rst..wx.z\n",
+      "w : abcdefghijklmnopqrstuv.xyz\n",
+      "x : abcdefghijklmnopqrstuvw.yz\n",
+      "y : a.cde.ghi..lmnop.rst..wx.z\n",
+      "z : abcdefghijklmnopqrstuvwxy.\n"
+     ]
+    }
+   ],
+   "source": [
+    "bombe.set_positions('a', 'a', 'b')\n",
+    "bombe.test(Signal('s', 'a'))\n",
+    "\n",
+    "for b in sorted(bombe.banks):\n",
+    "    print(b, ': ', end='')\n",
+    "    for w in sorted(bombe.banks[b]):\n",
+    "        if bombe.banks[b][w]:\n",
+    "            print(w, end='')\n",
+    "        else:\n",
+    "            print('.', end='')\n",
+    "    print('')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 822,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "a : ..........................\n",
+      "b : ..........................\n",
+      "c : ..........................\n",
+      "d : ..........................\n",
+      "e : ....e.....................\n",
+      "f : ..........................\n",
+      "g : ..........................\n",
+      "h : ..........................\n",
+      "i : ..........................\n",
+      "j : ..........................\n",
+      "k : ..........................\n",
+      "l : ...........l..............\n",
+      "m : ............m.............\n",
+      "n : .............n............\n",
+      "o : ..............o...........\n",
+      "p : ..........................\n",
+      "q : ..........................\n",
+      "r : .................r........\n",
+      "s : ..................s.......\n",
+      "t : ...................t......\n",
+      "u : ..........................\n",
+      "v : ..........................\n",
+      "w : ......................w...\n",
+      "x : .......................x..\n",
+      "y : ..........................\n",
+      "z : .........................z\n"
+     ]
+    }
+   ],
+   "source": [
+    "bombe.set_positions('a', 'a', 'b')\n",
+    "bombe.test()\n",
+    "\n",
+    "for b in sorted(bombe.banks):\n",
+    "    print(b, ': ', end='')\n",
+    "    for w in sorted(bombe.banks[b]):\n",
+    "        if bombe.banks[b][w]:\n",
+    "            print(w, end='')\n",
+    "        else:\n",
+    "            print('.', end='')\n",
+    "    print('')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 823,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "1"
+      ]
+     },
+     "execution_count": 823,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "len([bombe.banks['t'][w] for w in bombe.banks['t'] if bombe.banks['t'][w]])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 824,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "# %%timeit\n",
+    "# results = bombe.run()\n",
+    "# print(len(results), ('a', 'a', 'b') in results)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 825,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "# %%timeit\n",
+    "# results = bombe.run(use_diagonal_board=False)\n",
+    "# print(len(results), ('a', 'a', 'b') in results)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 826,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "('a', 'a', 'b')"
+      ]
+     },
+     "execution_count": 826,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "bombe.wheel_positions_l"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 827,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "False"
+      ]
+     },
+     "execution_count": 827,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "bombe.test(Signal('t', 't'), ('p', 'p', 'p'))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 828,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "('p', 'p', 'p')"
+      ]
+     },
+     "execution_count": 828,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "bombe.wheel_positions_l"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 829,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "a : abcdefghijklmnop.rst.vwxyz\n",
+      "b : a.cde.g.ij.lmno..rst..wx.z\n",
+      "c : abcdefghijklmnop.rst.vwxyz\n",
+      "d : abcdefghijklmnopqrstuvwxyz\n",
+      "e : abcdefghijklmnopqrstuvwxyz\n",
+      "f : a.cde.g.ij.lmno..rst..wx.z\n",
+      "g : abcdefghijklmnopqrstuvwxyz\n",
+      "h : a.cde.ghijklmnop.rst.vwxyz\n",
+      "i : abcdefghijklmnopqrstuvwxyz\n",
+      "j : abcdefghijklmnopqrstuvwxyz\n",
+      "k : a.cde.ghij.lmnop.rst..wx.z\n",
+      "l : abcdefghijklmnopqrstuvwxyz\n",
+      "m : abcdefghijklmnopqrstuvwxyz\n",
+      "n : abcdefghijklmnopqrstuvwxyz\n",
+      "o : abcdefghijklmnopqrstuvwxyz\n",
+      "p : a.cde.ghijklmnop.rst.vwxyz\n",
+      "q : ...de.g.ij.lmno..rst..wx.z\n",
+      "r : abcdefghijklmnopqrstuvwxyz\n",
+      "s : abcdefghijklmnopqrstuvwxyz\n",
+      "t : abcdefghijklmnopqrstuvwxyz\n",
+      "u : ...de.g.ij.lmno..rst..wx.z\n",
+      "v : a.cde.ghij.lmnop.rst..wx.z\n",
+      "w : abcdefghijklmnopqrstuvwxyz\n",
+      "x : abcdefghijklmnopqrstuvwxyz\n",
+      "y : a.cde.ghij.lmnop.rst..wx.z\n",
+      "z : abcdefghijklmnopqrstuvwxyz\n"
+     ]
+    }
+   ],
+   "source": [
+    "for b in sorted(bombe.banks):\n",
+    "    print(b, ': ', end='')\n",
+    "    for w in sorted(bombe.banks[b]):\n",
+    "        if bombe.banks[b][w]:\n",
+    "            print(w, end='')\n",
+    "        else:\n",
+    "            print('.', end='')\n",
+    "    print('')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 830,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "17576"
+      ]
+     },
+     "execution_count": 830,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "allwheels = itertools.product(string.ascii_lowercase, repeat=3)\n",
+    "len(list(allwheels))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 831,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "(('a', 'a', 'b'), True)"
+      ]
+     },
+     "execution_count": 831,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "b = Bombe(wheel_i_spec, wheel_ii_spec, wheel_iii_spec, reflector_b_spec, menu=menu)\n",
+    "b(('a', 'a', 'b'))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 832,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "b = Bombe(wheel_i_spec, wheel_ii_spec, wheel_iii_spec, reflector_b_spec, menu=menu)(('a', 'a', 'b'))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 833,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[('a', 'a', 'b')]"
+      ]
+     },
+     "execution_count": 833,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "allwheels = itertools.product(string.ascii_lowercase, repeat=3)\n",
+    "\n",
+    "with multiprocessing.Pool() as pool:\n",
+    "    res = pool.map(Bombe(wheel_i_spec, wheel_ii_spec, wheel_iii_spec, reflector_b_spec, menu=menu),\n",
+    "                  allwheels)\n",
+    "[r[0] for r in res if r[1]]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 857,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "def run_multi_bombe(wheel1_spec, wheel2_spec, wheel3_spec, reflector_spec, menu,\n",
+    "                    start_signal=None, use_diagonal_board=True, \n",
+    "                    verify_plugboard=True):\n",
+    "    allwheels = itertools.product(string.ascii_lowercase, repeat=3)\n",
+    "\n",
+    "    with multiprocessing.Pool() as pool:\n",
+    "        res = pool.map(Bombe(wheel1_spec, wheel2_spec, wheel3_spec, \n",
+    "            reflector_spec, menu=menu, start_signal=start_signal, \n",
+    "            use_diagonal_board=use_diagonal_board, \n",
+    "            verify_plugboard=verify_plugboard),\n",
+    "                  allwheels)\n",
+    "    return [r[0] for r in res if r[1]]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 835,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[('a', 'a', 'b')]"
+      ]
+     },
+     "execution_count": 835,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "[r[0] for r in res if r[1]]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 836,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "# Setting sheet line 31 from http://www.codesandciphers.org.uk/enigma/enigma3.htm\n",
+    "# Enigma simulation settings are \n",
+    "# http://enigma.louisedade.co.uk/enigma.html?m3;b;b153;AFTX;AJFE;AU-BG-EY-FP-HL-IN-JZ-OS-QR-TX\n",
+    "w_enigma = Enigma(reflector_b_spec, \n",
+    "                wheel_i_spec, wheel_i_pegs,\n",
+    "                wheel_v_spec, wheel_v_pegs,\n",
+    "                wheel_iii_spec, wheel_iii_pegs,\n",
+    "                6, 20, 24,\n",
+    "                'ua pf rq so ni ey bg hl tx zj')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 837,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "('e', 'l', 'e')"
+      ]
+     },
+     "execution_count": 837,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "w_enigma.set_wheels('j', 'e', 'b')\n",
+    "tuple(unpos(p) for p in w_enigma.wheel_positions)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 838,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'dhnpforeeimgg'"
+      ]
+     },
+     "execution_count": 838,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "w_enigma.set_wheels('j', 'e', 'b')\n",
+    "pt = 'someplaintext'\n",
+    "ct = w_enigma.encipher(pt)\n",
+    "ct"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 839,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "('j', 'e', 'o')"
+      ]
+     },
+     "execution_count": 839,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "w_enigma.wheel_positions_l"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 840,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[MenuIem(before='s', after='d', number=1),\n",
+       " MenuIem(before='o', after='h', number=2),\n",
+       " MenuIem(before='m', after='n', number=3),\n",
+       " MenuIem(before='e', after='p', number=4),\n",
+       " MenuIem(before='p', after='f', number=5),\n",
+       " MenuIem(before='l', after='o', number=6),\n",
+       " MenuIem(before='a', after='r', number=7),\n",
+       " MenuIem(before='i', after='e', number=8),\n",
+       " MenuIem(before='n', after='e', number=9),\n",
+       " MenuIem(before='t', after='i', number=10),\n",
+       " MenuIem(before='e', after='m', number=11),\n",
+       " MenuIem(before='x', after='g', number=12),\n",
+       " MenuIem(before='t', after='g', number=13)]"
+      ]
+     },
+     "execution_count": 840,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "w_menu = [MenuItem(p, c, i+1) for i, (p, c) in enumerate(zip(pt, ct))]\n",
+    "w_menu"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 841,
+   "metadata": {
+    "collapsed": false,
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[('a', 'y', 'm'),\n",
+       " ('c', 'p', 'v'),\n",
+       " ('c', 's', 'f'),\n",
+       " ('c', 'w', 'j'),\n",
+       " ('d', 'r', 'k'),\n",
+       " ('e', 'l', 'f'),\n",
+       " ('e', 's', 'v'),\n",
+       " ('e', 'y', 'd'),\n",
+       " ('e', 'y', 'o'),\n",
+       " ('f', 'z', 'x'),\n",
+       " ('g', 'b', 'l'),\n",
+       " ('g', 'c', 'd'),\n",
+       " ('g', 'c', 'f'),\n",
+       " ('g', 'j', 'p'),\n",
+       " ('h', 'c', 'i'),\n",
+       " ('h', 'm', 'w'),\n",
+       " ('h', 'o', 'd'),\n",
+       " ('h', 'p', 'b'),\n",
+       " ('h', 's', 't'),\n",
+       " ('i', 'b', 's'),\n",
+       " ('i', 'v', 'b'),\n",
+       " ('j', 'y', 'u'),\n",
+       " ('k', 'b', 'x'),\n",
+       " ('k', 'f', 't'),\n",
+       " ('k', 'l', 'e'),\n",
+       " ('k', 'l', 'm'),\n",
+       " ('k', 'r', 'z'),\n",
+       " ('k', 's', 'p'),\n",
+       " ('l', 'd', 'z'),\n",
+       " ('l', 'i', 'y'),\n",
+       " ('l', 'y', 'f'),\n",
+       " ('m', 'b', 'h'),\n",
+       " ('m', 'p', 'l'),\n",
+       " ('n', 'l', 'r'),\n",
+       " ('o', 'k', 'x'),\n",
+       " ('p', 'a', 'g'),\n",
+       " ('p', 'c', 'v'),\n",
+       " ('p', 'f', 'o'),\n",
+       " ('p', 'm', 'i'),\n",
+       " ('p', 'x', 'n'),\n",
+       " ('p', 'x', 'p'),\n",
+       " ('q', 'q', 'n'),\n",
+       " ('q', 'r', 'w'),\n",
+       " ('q', 'v', 'l'),\n",
+       " ('q', 'x', 't'),\n",
+       " ('s', 'a', 'h'),\n",
+       " ('s', 'h', 'v'),\n",
+       " ('s', 'l', 'p'),\n",
+       " ('s', 'l', 's'),\n",
+       " ('u', 'r', 'h'),\n",
+       " ('v', 'v', 'v'),\n",
+       " ('v', 'x', 'a'),\n",
+       " ('w', 'j', 'z'),\n",
+       " ('w', 'k', 'u'),\n",
+       " ('x', 'f', 'p'),\n",
+       " ('x', 'j', 'n'),\n",
+       " ('x', 'o', 'q'),\n",
+       " ('x', 'x', 'x'),\n",
+       " ('y', 'n', 'c'),\n",
+       " ('y', 'r', 'f'),\n",
+       " ('z', 't', 'y'),\n",
+       " ('z', 'z', 'k')]"
+      ]
+     },
+     "execution_count": 841,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "allwheels = itertools.product(string.ascii_lowercase, repeat=3)\n",
+    "\n",
+    "with multiprocessing.Pool() as pool:\n",
+    "    res = pool.map(Bombe(wheel_i_spec, wheel_v_spec, wheel_iii_spec, reflector_b_spec, \n",
+    "                         menu=w_menu, verify_plugboard=False),\n",
+    "                  allwheels)\n",
+    "[r[0] for r in res if r[1]]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 842,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "62"
+      ]
+     },
+     "execution_count": 842,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "len([r[0] for r in res if r[1]])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 843,
+   "metadata": {
+    "collapsed": false,
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[('c', 'p', 'v'),\n",
+       " ('c', 's', 'f'),\n",
+       " ('e', 'l', 'f'),\n",
+       " ('g', 'c', 'f'),\n",
+       " ('j', 'y', 'u'),\n",
+       " ('o', 'k', 'x'),\n",
+       " ('p', 'a', 'g'),\n",
+       " ('q', 'q', 'n'),\n",
+       " ('q', 'v', 'l'),\n",
+       " ('q', 'x', 't'),\n",
+       " ('s', 'l', 'p'),\n",
+       " ('u', 'r', 'h'),\n",
+       " ('y', 'n', 'c')]"
+      ]
+     },
+     "execution_count": 843,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "allwheels = itertools.product(string.ascii_lowercase, repeat=3)\n",
+    "\n",
+    "with multiprocessing.Pool() as pool:\n",
+    "    res = pool.map(Bombe(wheel_i_spec, wheel_v_spec, wheel_iii_spec, reflector_b_spec, \n",
+    "                         menu=w_menu, verify_plugboard=True),\n",
+    "                  allwheels)\n",
+    "[r[0] for r in res if r[1]]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 858,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[('c', 'p', 'v'),\n",
+       " ('c', 's', 'f'),\n",
+       " ('e', 'l', 'f'),\n",
+       " ('g', 'c', 'f'),\n",
+       " ('j', 'y', 'u'),\n",
+       " ('o', 'k', 'x'),\n",
+       " ('p', 'a', 'g'),\n",
+       " ('q', 'q', 'n'),\n",
+       " ('q', 'v', 'l'),\n",
+       " ('q', 'x', 't'),\n",
+       " ('s', 'l', 'p'),\n",
+       " ('u', 'r', 'h'),\n",
+       " ('y', 'n', 'c')]"
+      ]
+     },
+     "execution_count": 858,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "run_multi_bombe(wheel_i_spec, wheel_v_spec, wheel_iii_spec, reflector_b_spec, w_menu)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 844,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "# allwheels = itertools.product(string.ascii_lowercase, repeat=3)\n",
+    "\n",
+    "# with multiprocessing.Pool() as pool:\n",
+    "#     res = pool.map(Bombe(wheel_i_spec, wheel_v_spec, wheel_iii_spec, reflector_b_spec, \n",
+    "#                          menu=w_menu, start_signal=Signal('t', 'x')),\n",
+    "#                   allwheels)\n",
+    "# [r[0] for r in res if r[1]]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 845,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "13"
+      ]
+     },
+     "execution_count": 845,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "len([r[0] for r in res if r[1]])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 846,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "w_bombe = Bombe(wheel_i_spec, wheel_v_spec, wheel_iii_spec, reflector_b_spec, \n",
+    "                         menu=w_menu)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 847,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "Signal(bank='e', wire='e')"
+      ]
+     },
+     "execution_count": 847,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "w_bombe.test_start"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 848,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "True"
+      ]
+     },
+     "execution_count": 848,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "w_bombe.test(start_positions=('e', 'l', 'f'))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 849,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "True"
+      ]
+     },
+     "execution_count": 849,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "w_bombe.test(Signal('t', 'x'), ('e', 'l', 'f'))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 850,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "True\n",
+      "a : a.cdefghi..l.nopqrst.vwx..\n",
+      "b : ....efghi...mnop.......x..\n",
+      "c : a....fg.i..lmn.p.r.t...x..\n",
+      "d : a...efghij.lmn...rstu..xyz\n",
+      "e : ab.defghijklmnopqrstuvwxyz\n",
+      "f : abcde.ghijklmnopqrstuvwxyz\n",
+      "g : abcdef.hijklmnopqrstuvwxyz\n",
+      "h : ab.defghi...mnopq.stuvwx..\n",
+      "i : abcdefgh.jklmnopqrstuvwxyz\n",
+      "j : ...defg.i...mn.p...t...x..\n",
+      "k : ....efg.i..lmnop.r.t...x..\n",
+      "l : a.cdefg.i.k.mnopqr.t..wxyz\n",
+      "m : .bcdefghijklmnopqrstuvwxyz\n",
+      "n : abcdefghijklmnopqr.tuvwxyz\n",
+      "o : ab..efghi.klmnopqr.t...xyz\n",
+      "p : abc.efghijklmnopqrstuvwxyz\n",
+      "q : a...efghi..lmnop.r.t...x..\n",
+      "r : a.cdefg.i.klmnopqrst..w..z\n",
+      "s : a..defghi...m..p.rstuv.xyz\n",
+      "t : a.cdefghijklmnopqrstuvwxyz\n",
+      "u : ...defghi...mn.p..st...x..\n",
+      "v : a...efghi...mn.p..st...x..\n",
+      "w : a...efghi..lmn.p.r.t...x..\n",
+      "x : abcdefghijklmnopq.stuvwxyz\n",
+      "y : ...defg.i..lmnop..st...x..\n",
+      "z : ...defg.i..lmnop.rst...x..\n"
+     ]
+    }
+   ],
+   "source": [
+    "r = w_bombe.test(start_positions=('c', 'p', 'v'))\n",
+    "print(r)\n",
+    "for b in sorted(w_bombe.banks):\n",
+    "    print(b, ': ', end='')\n",
+    "    for w in sorted(w_bombe.banks[b]):\n",
+    "        if w_bombe.banks[b][w]:\n",
+    "            print(w, end='')\n",
+    "        else:\n",
+    "            print('.', end='')\n",
+    "    print('')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 851,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "True\n",
+      "a : abcdefghi..lmnop.rst.v.x.z\n",
+      "b : a...ef.hi..lmnop.r.t...x..\n",
+      "c : a..defg.i..lmn.p.rst...x..\n",
+      "d : a.c.efghi.klmnopqrstu.wxyz\n",
+      "e : abcdefghijklmnopqrstuvwx.z\n",
+      "f : abcdefghijklmno.qrstuvwxyz\n",
+      "g : a.cdefghijklmnopqrstuvwxyz\n",
+      "h : ab.defghi...mnopqrstuvwxyz\n",
+      "i : abcdefghijklm.opqrstuvwxyz\n",
+      "j : ....efg.i..lmnop...t...x..\n",
+      "k : ...defg.i..lmn.p..st...x..\n",
+      "l : abcdefg.ijklmnopqrstuv.x..\n",
+      "m : abcdefghijkl.nopqrstuvwxyz\n",
+      "n : abcdefgh.jklmnopqrstuvwxyz\n",
+      "o : ab.defghij.lmnop.r.tuvwxyz\n",
+      "p : abcde.ghijklmnopqrstuvwxyz\n",
+      "q : ...defghi..lmn.p..st...x..\n",
+      "r : abcdefghi..lmnop.rst.v.x.z\n",
+      "s : a.cdefghi.klmn.pqr.tuvwxyz\n",
+      "t : abcdefghijklmnopqrstuvw.yz\n",
+      "u : ...defghi..lmnop..st...x..\n",
+      "v : a...efghi..lmnop.rst...x..\n",
+      "w : ...defghi...mnop..st...x..\n",
+      "x : abcdefghijklmnopqrs.uvwxyz\n",
+      "y : ...d.fghi...mnop..st...x..\n",
+      "z : a..defghi...mnop.rst...x..\n"
+     ]
+    }
+   ],
+   "source": [
+    "r = w_bombe.test(start_positions=('e', 'l', 'f'))\n",
+    "print(r)\n",
+    "for b in sorted(w_bombe.banks):\n",
+    "    print(b, ': ', end='')\n",
+    "    for w in sorted(w_bombe.banks[b]):\n",
+    "        if w_bombe.banks[b][w]:\n",
+    "            print(w, end='')\n",
+    "        else:\n",
+    "            print('.', end='')\n",
+    "    print('')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 852,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{frozenset({'e', 'y'}),\n",
+       " frozenset({'t', 'x'}),\n",
+       " frozenset({'i', 'n'}),\n",
+       " frozenset({'m'}),\n",
+       " frozenset({'b', 'g'}),\n",
+       " frozenset({'f', 'p'})}"
+      ]
+     },
+     "execution_count": 852,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "ps = w_bombe.possible_plugboards()\n",
+    "ps"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 853,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "True"
+      ]
+     },
+     "execution_count": 853,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "all(s0.isdisjoint(s1) for s0 in ps for s1 in ps if s0 != s1)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 854,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "({frozenset({1, 2}), frozenset({2, 3}), frozenset({3, 4})},\n",
+       " frozenset({1, 2}),\n",
+       " frozenset({3, 4}),\n",
+       " frozenset({2, 3}))"
+      ]
+     },
+     "execution_count": 854,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "s = set()\n",
+    "f1 = frozenset((1, 2))\n",
+    "f2 = frozenset((3, 4))\n",
+    "f3 = frozenset((2, 3))\n",
+    "s = s.union({f1})\n",
+    "s = s.union({f2})\n",
+    "s = s.union({f1})\n",
+    "s = s.union({f3})\n",
+    "s, f1, f2, f3"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 855,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "False"
+      ]
+     },
+     "execution_count": 855,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "all(s0.isdisjoint(s1) for s0 in s for s1 in s if s0 != s1)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 856,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "False"
+      ]
+     },
+     "execution_count": 856,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "{1, 2}.isdisjoint({1, 6})"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.5.1+"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
index 1150763d35eabc8be0e4fa5cf1c4f5cc3ed26b6f..72f480792170725c0b59462781d338961c534510 100644 (file)
--- a/enigma.py
+++ b/enigma.py
@@ -1,7 +1,9 @@
 
 # coding: utf-8
 
+##################################
 # # Enigma machine
+##################################
 # Specification from [Codes and Ciphers](http://www.codesandciphers.org.uk/enigma/rotorspec.htm) page.
 # 
 # Example Enigma machines from [Louise Dale](http://enigma.louisedade.co.uk/enigma.html) (full simulation) and [EnigmaCo](http://enigmaco.de/enigma/enigma.html) (good animation of the wheels, but no ring settings).
@@ -12,6 +14,8 @@
 
 import string
 import collections
+import multiprocessing
+import itertools
 
 # Some convenience functions
 
@@ -278,7 +282,6 @@ class SimpleWheel(LetterTransformer):
         
     def advance(self):
         self.position = (self.position + 1) % 26
-        # return self.position
 
 
 
@@ -429,16 +432,11 @@ class Wheel(SimpleWheel):
 
     def set_position(self, position):
         self.position = (pos(position) - self.ring_setting + 1) % 26
-        # self.position_l = position
         self.peg_positions = [(pos(p) - pos(position)) % 26  for p in self.ring_peg_letters]
         
     def advance(self):
         super(Wheel, self).advance()
         self.peg_positions = [(p - 1) % 26 for p in self.peg_positions]
-        # self.position_l = unpos(self.position + self.ring_setting - 1)
-        # return self.position
-
-
 
 
 class Enigma(object):
@@ -912,20 +910,20 @@ class Enigma(object):
     'wenpbqrouxlkychdfgzvitajms'
 
 
-    >>> enigma31.set_wheels('i', 'd', 'z')
+    >>> enigma31.set_wheels('i', 'z', 'd')
     >>> enigma31.encipher('verylongtestmessagewithanextrabitofmessageforgoodmeasure')
-    'gstsegeqdrthkfwesljjomfvcqwcfspxpfqqmewvddybarzwubxtpejz'
+    'apocwtjuikurcfivlozvhffkoacxufcekthcvodfqpxdjqyckdozlqki'
     >>> enigma31.wheel_positions
-    (3, 12, 6)
+    (4, 9, 10)
     >>> cat(enigma31.wheel_positions_l)
-    'ifd'
+    'jch'
     >>> enigma31.peg_positions
-    ([8], [20], [18])
+    ([7], [23], [14])
     >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'urygzpdmxtwshqvfnbljaokice'
+    'mopnigfuesqwadbcktjrhylzvx'
 
-    >>> enigma31.set_wheels('i', 'd', 'z')
-    >>> enigma31.decipher('gstsegeqdrthkfwesljjomfvcqwcfspxpfqqmewvddybarzwubxtpejz')
+    >>> enigma31.set_wheels('i', 'z', 'd')
+    >>> enigma31.decipher('apocwtjuikurcfivlozvhffkoacxufcekthcvodfqpxdjqyckdozlqki')
     'verylongtestmessagewithanextrabitofmessageforgoodmeasure'
     """
     def __init__(self, reflector_spec,
@@ -1002,6 +1000,186 @@ class Enigma(object):
 #     print()
 
 
+##################################
+# # Bombe
+##################################
+
+Signal = collections.namedtuple('Signal', ['bank', 'wire'])
+Connection = collections.namedtuple('Connection', ['banks', 'scrambler'])
+MenuItem = collections.namedtuple('MenuIem', ['before', 'after', 'number'])
+
+
+class Scrambler(object):
+    def __init__(self, wheel1_spec, wheel2_spec, wheel3_spec, reflector_spec,
+                 wheel1_pos='a', wheel2_pos='a', wheel3_pos='a'):
+        self.wheel1 = SimpleWheel(wheel1_spec, position=wheel1_pos)
+        self.wheel2 = SimpleWheel(wheel2_spec, position=wheel2_pos)
+        self.wheel3 = SimpleWheel(wheel3_spec, position=wheel3_pos)
+        self.reflector = Reflector(reflector_spec)
+    
+    def __getattribute__(self, name):
+        if name=='wheel_positions':
+            return self.wheel1.position, self.wheel2.position, self.wheel3.position 
+        elif name=='wheel_positions_l':
+            return self.wheel1.position_l, self.wheel2.position_l, self.wheel3.position_l 
+        else:
+            return object.__getattribute__(self, name)
+    
+    def advance(self, wheel1=False, wheel2=False, wheel3=True):
+        if wheel1: self.wheel1.advance()
+        if wheel2: self.wheel2.advance()
+        if wheel3: self.wheel3.advance()
+            
+    def lookup(self, letter):
+        a = self.wheel3.forward(letter)
+        b = self.wheel2.forward(a)
+        c = self.wheel1.forward(b)
+        d = self.reflector.forward(c)
+        e = self.wheel1.backward(d)
+        f = self.wheel2.backward(e)
+        g = self.wheel3.backward(f)
+        return g
+    
+    def set_positions(self, wheel1_pos, wheel2_pos, wheel3_pos):
+        self.wheel1.set_position(wheel1_pos)
+        self.wheel2.set_position(wheel2_pos)
+        self.wheel3.set_position(wheel3_pos)      
+
+
+class Bombe(object):
+    def __init__(self, wheel1_spec, wheel2_spec, wheel3_spec, reflector_spec,
+                menu=None, start_signal=None, use_diagonal_board=True, 
+                verify_plugboard=True):
+        self.connections = []
+        self.wheel1_spec = wheel1_spec
+        self.wheel2_spec = wheel2_spec
+        self.wheel3_spec = wheel3_spec
+        self.reflector_spec = reflector_spec
+        if menu:
+            self.read_menu(menu)
+        if start_signal:
+            self.test_start = start_signal
+        self.use_diagonal_board = use_diagonal_board
+        self.verify_plugboard = verify_plugboard
+        
+    def __getattribute__(self, name):
+        if name=='wheel_positions':
+            return self.connections[0].scrambler.wheel_positions
+        elif name=='wheel_positions_l':
+            return self.connections[0].scrambler.wheel_positions_l
+        else:
+            return object.__getattribute__(self, name)
+        
+    def __call__(self, start_positions):
+        return start_positions, self.test(initial_signal=self.test_start,
+            start_positions=start_positions, 
+            use_diagonal_board=self.use_diagonal_board,
+            verify_plugboard=self.verify_plugboard)
+        
+    def add_connection(self, bank_before, bank_after, scrambler):
+        self.connections += [Connection([bank_before, bank_after], scrambler)]
+        
+    def read_menu(self, menu):
+        for item in menu:
+            scrambler = Scrambler(self.wheel1_spec, self.wheel2_spec, self.wheel3_spec,
+                                  self.reflector_spec,
+                                  wheel3_pos=unpos(item.number - 1))
+            self.add_connection(item.before, item.after, scrambler)
+        most_common_letter = (collections.Counter(m.before for m in menu) + \
+                              collections.Counter(m.after for m in menu)).most_common(1)[0][0]
+        self.test_start = Signal(most_common_letter, most_common_letter)
+        
+    def set_positions(self, wheel1_pos, wheel2_pos, wheel3_pos):
+        for i, c in enumerate(self.connections):
+            c.scrambler.set_positions(wheel1_pos, wheel2_pos, unpos(pos(wheel3_pos) + i))
+    
+    def test(self, initial_signal=None, start_positions=None, use_diagonal_board=True,
+            verify_plugboard=True):
+        self.banks = {label: 
+                      dict(zip(string.ascii_lowercase, [False]*len(string.ascii_lowercase)))
+                      for label in string.ascii_lowercase}
+        if start_positions:
+            self.set_positions(*start_positions)
+        if not initial_signal:
+            initial_signal = self.test_start
+        self.pending = [initial_signal]
+        self.propagate(use_diagonal_board)
+        live_wire_count = len([self.banks[self.test_start.bank][w] 
+                    for w in self.banks[self.test_start.bank] 
+                    if self.banks[self.test_start.bank][w]])
+        if live_wire_count < 26:
+            if verify_plugboard:
+                possibles = self.possible_plugboards()
+                return all(s0.isdisjoint(s1) for s0 in possibles for s1 in possibles if s0 != s1)
+            else:
+                return True
+        else:
+            return False
+        
+    def propagate(self, use_diagonal_board):
+        while self.pending:
+            current = self.pending[0]
+            # print("processing", current)
+            self.pending = self.pending[1:]
+            if not self.banks[current.bank][current.wire]:
+                self.banks[current.bank][current.wire] = True
+                if use_diagonal_board:
+                    self.pending += [Signal(current.wire, current.bank)]
+                for c in self.connections:
+                    if current.bank in c.banks:
+                        other_bank = [b for b in c.banks if b != current.bank][0]
+                        other_wire = c.scrambler.lookup(current.wire)
+                        # print("  adding", other_bank, other_wire, "because", c.banks)
+                        self.pending += [Signal(other_bank, other_wire)]
+    
+    def run(self, run_start=None, wheel1_pos='a', wheel2_pos='a', wheel3_pos='a', use_diagonal_board=True):
+        if not run_start:
+            run_start = self.test_start
+        self.solutions = []
+        self.set_positions(wheel1_pos, wheel2_pos, wheel3_pos)
+        for run_index in range(26*26*26):
+            if self.test(initial_signal=run_start, use_diagonal_board=use_diagonal_board):
+                self.solutions += [self.connections[0].scrambler.wheel_positions_l]
+            advance3 = True
+            advance2 = False
+            advance1 = False
+            if (run_index + 1) % 26 == 0: advance2 = True
+            if (run_index + 1) % (26*26) == 0: advance1 = True
+            for c in self.connections:
+                c.scrambler.advance(advance1, advance2, advance3)
+        return self.solutions
+    
+    def possible_plugboards(self):
+        possibles = set()
+        for b in self.banks:
+            active = [w for w in self.banks[b] if self.banks[b][w]]
+            inactive = [w for w in self.banks[b] if not self.banks[b][w]]
+            if len(active) == 1:
+                possibles = possibles.union({frozenset((b, active[0]))})
+            if len(inactive) == 1:
+                possibles = possibles.union({frozenset((b, inactive[0]))})
+        return possibles
+
+
+def make_menu(plaintext, ciphertext):
+    return [MenuItem(p, c, i+1) 
+            for i, (p, c) in enumerate(zip(plaintext, ciphertext))]
+
+
+def run_multi_bombe(wheel1_spec, wheel2_spec, wheel3_spec, reflector_spec, menu,
+                    start_signal=None, use_diagonal_board=True, 
+                    verify_plugboard=True):
+    allwheels = itertools.product(string.ascii_lowercase, repeat=3)
+
+    with multiprocessing.Pool() as pool:
+        res = pool.map(Bombe(wheel1_spec, wheel2_spec, wheel3_spec, 
+            reflector_spec, menu=menu, start_signal=start_signal, 
+            use_diagonal_board=use_diagonal_board, 
+            verify_plugboard=verify_plugboard),
+                  allwheels)
+    return [r[0] for r in res if r[1]]
+
+
 if __name__ == "__main__":
     import doctest
     # doctest.testmod(extraglobs={'lt': LetterTransformer(1, 'a')})