Converted Enigma notebook to Python file, included tests as doctest
authorNeil Smith <neil.git@njae.me.uk>
Mon, 16 May 2016 10:38:07 +0000 (11:38 +0100)
committerNeil Smith <neil.git@njae.me.uk>
Mon, 16 May 2016 10:38:07 +0000 (11:38 +0100)
enigma.ipynb
enigma.py [new file with mode: 0644]

index 7c9906cbc618ee32b8dd1a5b035545263001dae8..086aa468ce72215d66d3cde54eb25b97f82caf5b 100644 (file)
@@ -14,7 +14,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 733,
+   "execution_count": 1,
    "metadata": {
     "collapsed": true
    },
@@ -40,7 +40,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 734,
+   "execution_count": 2,
    "metadata": {
     "collapsed": true
    },
@@ -72,7 +72,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 735,
+   "execution_count": 3,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 736,
+   "execution_count": 4,
    "metadata": {
     "collapsed": false
    },
        " ('y', 'z')]"
       ]
      },
-     "execution_count": 736,
+     "execution_count": 4,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 737,
+   "execution_count": 5,
    "metadata": {
     "collapsed": false
    },
        "'zyxwcabdefghijklmnopqrstuv'"
       ]
      },
-     "execution_count": 737,
+     "execution_count": 5,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 738,
+   "execution_count": 6,
    "metadata": {
     "collapsed": false,
     "scrolled": true
   },
   {
    "cell_type": "code",
-   "execution_count": 739,
+   "execution_count": 7,
    "metadata": {
     "collapsed": false,
     "scrolled": true
        "  24])"
       ]
      },
-     "execution_count": 739,
+     "execution_count": 7,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 740,
+   "execution_count": 8,
    "metadata": {
     "collapsed": false,
     "scrolled": true
        "  0])"
       ]
      },
-     "execution_count": 740,
+     "execution_count": 8,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 741,
+   "execution_count": 9,
    "metadata": {
     "collapsed": false
    },
        "'zyxwcabdefghijklmnopqrstuv'"
       ]
      },
-     "execution_count": 741,
+     "execution_count": 9,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 742,
+   "execution_count": 10,
    "metadata": {
     "collapsed": false
    },
        "'fgehijklmnopqrstuvwxyzdcba'"
       ]
      },
-     "execution_count": 742,
+     "execution_count": 10,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 743,
+   "execution_count": 11,
    "metadata": {
     "collapsed": true
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 744,
+   "execution_count": 12,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 745,
+   "execution_count": 13,
    "metadata": {
     "collapsed": false
    },
        "'zycdefghijklmnopqrstuvwxba'"
       ]
      },
-     "execution_count": 745,
+     "execution_count": 13,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 746,
+   "execution_count": 14,
    "metadata": {
     "collapsed": false
    },
        "'zycdefghijklmnopqrstuvwxba'"
       ]
      },
-     "execution_count": 746,
+     "execution_count": 14,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 747,
+   "execution_count": 15,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 748,
+   "execution_count": 16,
    "metadata": {
     "collapsed": false
    },
        "('zycdefghijklmnopqrstuvwxba', 'zycdefghijklmnopqrstuvwxba')"
       ]
      },
-     "execution_count": 748,
+     "execution_count": 16,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 749,
+   "execution_count": 17,
    "metadata": {
     "collapsed": false
    },
        "('ugcdypblnzkhmisfrqoxavwtej', 'ugcdypblnzkhmisfrqoxavwtej')"
       ]
      },
-     "execution_count": 749,
+     "execution_count": 17,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 750,
+   "execution_count": 18,
    "metadata": {
     "collapsed": true
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 751,
+   "execution_count": 19,
    "metadata": {
     "collapsed": false
    },
        " ('v', 'w')]"
       ]
      },
-     "execution_count": 751,
+     "execution_count": 19,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 752,
+   "execution_count": 20,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 753,
+   "execution_count": 21,
    "metadata": {
     "collapsed": false
    },
        "'yruhqsldpxngokmiebfzcwvjat'"
       ]
      },
-     "execution_count": 753,
+     "execution_count": 21,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 754,
+   "execution_count": 22,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 755,
+   "execution_count": 23,
    "metadata": {
     "collapsed": false
    },
        "'fvpjiaoyedrzxwgctkuqsbnmhl'"
       ]
      },
-     "execution_count": 755,
+     "execution_count": 23,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 756,
+   "execution_count": 24,
    "metadata": {
     "collapsed": true
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 757,
+   "execution_count": 25,
    "metadata": {
     "collapsed": false,
     "scrolled": true
        " ('z', 'j')]"
       ]
      },
-     "execution_count": 757,
+     "execution_count": 25,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 758,
+   "execution_count": 26,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 759,
+   "execution_count": 27,
    "metadata": {
     "collapsed": false
    },
        "('ekmflgdqvzntowyhxuspaibrcj', 'uwygadfpvzbeckmthxslrinqoj')"
       ]
      },
-     "execution_count": 759,
+     "execution_count": 27,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 760,
+   "execution_count": 28,
    "metadata": {
     "collapsed": false
    },
        "'a'"
       ]
      },
-     "execution_count": 760,
+     "execution_count": 28,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 761,
+   "execution_count": 29,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 762,
+   "execution_count": 30,
    "metadata": {
     "collapsed": false
    },
        "('ajdksiruxblhwtmcqgznpyfvoe', 'ajpczwrlfbdkotyuqgenhxmivs')"
       ]
      },
-     "execution_count": 762,
+     "execution_count": 30,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 763,
+   "execution_count": 31,
    "metadata": {
     "collapsed": false
    },
        "('bdfhjlcprtxvznyeiwgakmusqo', 'tagbpcsdqeufvnzhyixjwlrkom')"
       ]
      },
-     "execution_count": 763,
+     "execution_count": 31,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 764,
+   "execution_count": 32,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 765,
+   "execution_count": 33,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 766,
+   "execution_count": 34,
    "metadata": {
     "collapsed": false
    },
        "(1, [20])"
       ]
      },
-     "execution_count": 766,
+     "execution_count": 34,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 767,
+   "execution_count": 35,
    "metadata": {
     "collapsed": false
    },
        "(25, [24, 11])"
       ]
      },
-     "execution_count": 767,
+     "execution_count": 35,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 768,
+   "execution_count": 36,
    "metadata": {
     "collapsed": false,
     "scrolled": true
   },
   {
    "cell_type": "code",
-   "execution_count": 769,
+   "execution_count": 37,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 770,
+   "execution_count": 38,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 771,
+   "execution_count": 39,
    "metadata": {
     "collapsed": false
    },
        "(0, 'c', [23, 10])"
       ]
      },
-     "execution_count": 771,
+     "execution_count": 39,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 772,
+   "execution_count": 40,
    "metadata": {
     "collapsed": true
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 773,
+   "execution_count": 41,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 774,
+   "execution_count": 42,
    "metadata": {
     "collapsed": false
    },
        "'u'"
       ]
      },
-     "execution_count": 774,
+     "execution_count": 42,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 775,
+   "execution_count": 43,
    "metadata": {
     "collapsed": false
    },
        "'a'"
       ]
      },
-     "execution_count": 775,
+     "execution_count": 43,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 776,
+   "execution_count": 44,
    "metadata": {
     "collapsed": false
    },
        "'uejobtpzwcnsrkdgvmlfaqiyxh'"
       ]
      },
-     "execution_count": 776,
+     "execution_count": 44,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 777,
+   "execution_count": 45,
    "metadata": {
     "collapsed": false,
     "scrolled": true
   },
   {
    "cell_type": "code",
-   "execution_count": 778,
+   "execution_count": 46,
    "metadata": {
     "collapsed": false,
     "scrolled": true
   },
   {
    "cell_type": "code",
-   "execution_count": 779,
+   "execution_count": 47,
    "metadata": {
     "collapsed": false,
     "scrolled": true
   },
   {
    "cell_type": "code",
-   "execution_count": 780,
+   "execution_count": 48,
    "metadata": {
     "collapsed": false
    },
        "'olpfhnvflyn'"
       ]
      },
-     "execution_count": 780,
+     "execution_count": 48,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 781,
+   "execution_count": 49,
    "metadata": {
     "collapsed": false
    },
        "'lawnjgpwjik'"
       ]
      },
-     "execution_count": 781,
+     "execution_count": 49,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 782,
+   "execution_count": 50,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 783,
+   "execution_count": 51,
    "metadata": {
     "collapsed": false
    },
        "'bahxvfrpdc'"
       ]
      },
-     "execution_count": 783,
+     "execution_count": 51,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 784,
+   "execution_count": 52,
    "metadata": {
     "collapsed": false
    },
        "'kvmmwrlqlqsqpeugjrcxzwpfyiyybwloewrouvkpoztceuwtfjzqwpbqldttsr'"
       ]
      },
-     "execution_count": 784,
+     "execution_count": 52,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 785,
+   "execution_count": 53,
    "metadata": {
     "collapsed": false
    },
        "'c'"
       ]
      },
-     "execution_count": 785,
+     "execution_count": 53,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 786,
+   "execution_count": 54,
    "metadata": {
     "collapsed": true
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 787,
+   "execution_count": 55,
    "metadata": {
     "collapsed": true
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 788,
+   "execution_count": 56,
    "metadata": {
     "collapsed": true
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 789,
+   "execution_count": 57,
    "metadata": {
     "collapsed": true
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 790,
+   "execution_count": 58,
    "metadata": {
     "collapsed": false
    },
        " 'urygzpdmxtwshqvfnbljaokice')"
       ]
      },
-     "execution_count": 790,
+     "execution_count": 58,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 791,
+   "execution_count": 59,
    "metadata": {
     "collapsed": false
    },
diff --git a/enigma.py b/enigma.py
new file mode 100644 (file)
index 0000000..1150763
--- /dev/null
+++ b/enigma.py
@@ -0,0 +1,1009 @@
+
+# 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).
+# 
+# There's also the nice Enigma simulator for Android by [Franklin Heath](https://franklinheath.co.uk/2012/02/04/our-first-app-published-enigma-simulator/), available on the [Google Play store](https://play.google.com/store/apps/details?id=uk.co.franklinheath.enigmasim&hl=en_GB).
+
+
+
+import string
+import collections
+
+# Some convenience functions
+
+cat = ''.join
+
+def clean(text): return cat(l.lower() for l in text if l in string.ascii_letters)
+
+def pos(letter): 
+    if letter in string.ascii_lowercase:
+        return ord(letter) - ord('a')
+    elif letter in string.ascii_uppercase:
+        return ord(letter) - ord('A')
+    else:
+        return ''
+    
+def unpos(number): return chr(number % 26 + ord('a'))
+
+
+wheel_i_spec = 'ekmflgdqvzntowyhxuspaibrcj'
+wheel_ii_spec = 'ajdksiruxblhwtmcqgznpyfvoe'
+wheel_iii_spec = 'bdfhjlcprtxvznyeiwgakmusqo'
+wheel_iv_spec = 'esovpzjayquirhxlnftgkdcmwb'
+wheel_v_spec = 'vzbrgityupsdnhlxawmjqofeck'
+wheel_vi_spec = 'jpgvoumfyqbenhzrdkasxlictw'
+wheel_vii_spec = 'nzjhgrcxmyswboufaivlpekqdt'
+wheel_viii_spec = 'fkqhtlxocbjspdzramewniuygv'
+beta_wheel_spec = 'leyjvcnixwpbqmdrtakzgfuhos'
+gamma_wheel_spec = 'fsokanuerhmbtiycwlqpzxvgjd'
+
+wheel_i_pegs = ['q']
+wheel_ii_pegs = ['e']
+wheel_iii_pegs = ['v']
+wheel_iv_pegs = ['j']
+wheel_v_pegs = ['z']
+wheel_vi_pegs = ['z', 'm']
+wheel_vii_pegs = ['z', 'm']
+wheel_viii_pegs = ['z', 'm']
+
+reflector_b_spec = 'ay br cu dh eq fs gl ip jx kn mo tz vw'
+reflector_c_spec = 'af bv cp dj ei go hy kr lz mx nw tq su'
+
+
+
+class LetterTransformer(object):
+    """A generic substitution cipher, that has different transforms in the 
+    forward and backward directions. It requires that the transforms for all
+    letters by provided.
+
+    >>> lt = LetterTransformer([('z', 'a')] + [(l, string.ascii_lowercase[i+1]) \
+            for i, l in enumerate(string.ascii_lowercase[:-1])], \
+            raw_transform = True)
+    >>> lt.forward_map
+    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 0]
+    >>> lt.backward_map
+    [25, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
+
+    >>> lt = LetterTransformer(cat(collections.OrderedDict.fromkeys('zyxwc' + string.ascii_lowercase)))
+    >>> lt.forward_map
+    [25, 24, 23, 22, 2, 0, 1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]
+    >>> lt.backward_map
+    [5, 6, 4, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 3, 2, 1, 0]
+    >>> cat(lt.forward(l) for l in string.ascii_lowercase)
+    'zyxwcabdefghijklmnopqrstuv'
+    >>> cat(lt.backward(l) for l in string.ascii_lowercase)
+    'fgehijklmnopqrstuvwxyzdcba'
+    """
+    def __init__(self, specification, raw_transform=False):
+        if raw_transform:
+            transform = specification
+        else:
+            transform = self.parse_specification(specification)
+        self.validate_transform(transform)
+        self.make_transform_map(transform)
+    
+    def parse_specification(self, specification):
+        return list(zip(string.ascii_lowercase, clean(specification)))
+        # return specification
+    
+    def validate_transform(self, transform):
+        """A set of pairs, of from-to"""
+        if len(transform) != 26:
+            raise ValueError("Transform specification has {} pairs, requires 26".
+                format(len(transform)))
+        for p in transform:
+            if len(p) != 2:
+                raise ValueError("Not all mappings in transform "
+                    "have two elements")
+        if len(set([p[0] for p in transform])) != 26:
+            raise ValueError("Transform specification must list 26 origin letters") 
+        if len(set([p[1] for p in transform])) != 26:
+            raise ValueError("Transform specification must list 26 destination letters") 
+
+    def make_empty_transform(self):
+        self.forward_map = [0] * 26
+        self.backward_map = [0] * 26
+            
+    def make_transform_map(self, transform):
+        self.make_empty_transform()
+        for p in transform:
+            self.forward_map[pos(p[0])] = pos(p[1])
+            self.backward_map[pos(p[1])] = pos(p[0])
+        return self.forward_map, self.backward_map
+    
+    def forward(self, letter):
+        if letter in string.ascii_lowercase:
+            return unpos(self.forward_map[pos(letter)])
+        else:
+            return ''
+                
+    def backward(self, letter):
+        if letter in string.ascii_lowercase:
+            return unpos(self.backward_map[pos(letter)])
+        else:
+            return ''
+
+
+class Plugboard(LetterTransformer):
+    """A plugboard, a type of letter transformer where forward and backward
+    transforms are the same. If a letter isn't explicitly transformed, it is 
+    kept as it is.
+
+    >>> pb = Plugboard('ua pf rq so ni ey bg hl tx zj'.upper())
+    >>> pb.forward_map
+    [20, 6, 2, 3, 24, 15, 1, 11, 13, 25, 10, 7, 12, 8, 18, 5, 17, 16, 14, 23, 0, 21, 22, 19, 4, 9]
+    >>> pb.forward_map == pb.backward_map
+    True
+    >>> cat(pb.forward(l) for l in string.ascii_lowercase)
+    'ugcdypblnzkhmisfrqoxavwtej'
+    >>> cat(pb.backward(l) for l in string.ascii_lowercase)
+    'ugcdypblnzkhmisfrqoxavwtej'
+    """
+    def parse_specification(self, specification):
+        return [tuple(clean(p)) for p in specification.split()]
+    
+    def validate_transform(self, transform):
+        """A set of pairs, of from-to"""
+        for p in transform:
+            if len(p) != 2:
+                raise ValueError("Not all mappings in transform"
+                    "have two elements")
+    
+    def make_empty_transform(self):
+        self.forward_map = list(range(26))
+        self.backward_map = list(range(26))
+        
+    def make_transform_map(self, transform):
+        expanded_transform = transform + [tuple(reversed(p)) for p in transform]
+        return super(Plugboard, self).make_transform_map(expanded_transform)
+
+
+
+
+class Reflector(Plugboard):
+    """A reflector is a plugboard that requires 13 transforms.
+
+    >>> reflector_b = Reflector(reflector_b_spec)
+    >>> reflector_b.forward_map == reflector_b.backward_map
+    True
+    >>> reflector_b.forward_map
+    [24, 17, 20, 7, 16, 18, 11, 3, 15, 23, 13, 6, 14, 10, 12, 8, 4, 1, 5, 25, 2, 22, 21, 9, 0, 19]
+    >>> cat(reflector_b.forward(l) for l in string.ascii_lowercase)
+    'yruhqsldpxngokmiebfzcwvjat'
+    >>> cat(reflector_b.backward(l) for l in string.ascii_lowercase)
+    'yruhqsldpxngokmiebfzcwvjat'
+    """
+    def validate_transform(self, transform):
+        if len(transform) != 13:
+            raise ValueError("Reflector specification has {} pairs, requires 13".
+                format(len(transform)))
+        if len(set([p[0] for p in transform] + 
+                    [p[1] for p in transform])) != 26:
+            raise ValueError("Reflector specification does not contain 26 letters")
+        try:
+            super(Reflector, self).validate_transform(transform)
+        except ValueError as v:
+            raise ValueError("Not all mappings in reflector have two elements")
+
+
+
+
+class SimpleWheel(LetterTransformer):
+    """A wheel is a transform that rotates.
+
+    Looking from the right, letters go in sequence a-b-c clockwise around the 
+    wheel. 
+
+    The position of the wheel is the number of spaces anticlockwise the wheel
+    has turned.
+
+    Letter inputs and outputs are given relative to the frame holding the wheel,
+    so if the wheel is advanced three places, an input of 'p' will enter the 
+    wheel on the position under the wheel's 'q' label.
+
+    >>> rotor_1_transform = list(zip(string.ascii_lowercase, 'EKMFLGDQVZNTOWYHXUSPAIBRCJ'.lower()))
+    >>> wheel_1 = SimpleWheel(rotor_1_transform, raw_transform=True)
+    >>> cat(wheel_1.forward(l) for l in string.ascii_lowercase)
+    'ekmflgdqvzntowyhxuspaibrcj'
+    >>> cat(wheel_1.backward(l) for l in string.ascii_lowercase)
+    'uwygadfpvzbeckmthxslrinqoj'
+
+
+    >>> wheel_2 = SimpleWheel(wheel_ii_spec)
+    >>> cat(wheel_2.forward(l) for l in string.ascii_lowercase)
+    'ajdksiruxblhwtmcqgznpyfvoe'
+    >>> cat(wheel_2.backward(l) for l in string.ascii_lowercase)
+    'ajpczwrlfbdkotyuqgenhxmivs'
+
+    >>> wheel_3 = SimpleWheel(wheel_iii_spec)
+    >>> wheel_3.set_position('a')
+    >>> wheel_3.advance()
+    >>> cat(wheel_3.forward(l) for l in string.ascii_lowercase)
+    'cegikboqswuymxdhvfzjltrpna'
+    >>> cat(wheel_3.backward(l) for l in string.ascii_lowercase)
+    'zfaobrcpdteumygxhwivkqjnls'
+    >>> wheel_3.position
+    1
+    >>> wheel_3.position_l
+    'b'
+
+    >>> for _ in range(24): wheel_3.advance()
+    >>> wheel_3.position
+    25
+    >>> wheel_3.position_l
+    'z'
+    >>> cat(wheel_3.forward(l) for l in string.ascii_lowercase)
+    'pcegikmdqsuywaozfjxhblnvtr'
+    >>> cat(wheel_3.backward(l) for l in string.ascii_lowercase)
+    'nubhcqdterfvgwoaizjykxmslp'
+
+    >>> wheel_3.advance()
+    >>> wheel_3.position
+    0
+    >>> wheel_3.position_l
+    'a'
+    >>> cat(wheel_3.forward(l) for l in string.ascii_lowercase)
+    'bdfhjlcprtxvznyeiwgakmusqo'
+    >>> cat(wheel_3.backward(l) for l in string.ascii_lowercase)
+    'tagbpcsdqeufvnzhyixjwlrkom'
+    """
+    def __init__(self, transform, position='a', raw_transform=False):
+        super(SimpleWheel, self).__init__(transform, raw_transform)
+        self.set_position(position)
+        
+    def __getattribute__(self,name):
+        if name=='position_l':
+            return unpos(self.position)
+        else:
+            return object.__getattribute__(self, name)
+    
+    def set_position(self, position):
+        self.position = ord(position) - ord('a')
+    
+    def forward(self, letter):
+        if letter in string.ascii_lowercase:
+            return unpos((self.forward_map[(pos(letter) + self.position) % 26] - self.position))
+        else:
+            return ''
+                
+    def backward(self, letter):
+        if letter in string.ascii_lowercase:
+            return unpos((self.backward_map[(pos(letter) + self.position) % 26] - self.position))
+        else:
+            return ''
+        
+    def advance(self):
+        self.position = (self.position + 1) % 26
+        # return self.position
+
+
+
+class Wheel(SimpleWheel):
+    """A wheel with a movable ring.
+
+    The ring holds the letters and the pegs that turn other wheels. The core
+    holds the wiring that does the transformation.
+
+    The ring position is how many steps the core is turned relative to the ring.
+    This is one-based, so a ring setting of 1 means the core and ring are 
+    aligned.
+
+    The position of the wheel is the position of the core (the transforms) 
+    relative to the neutral position. 
+
+    The position_l is the position of the ring, or what would be observed
+    by the user of the Enigma machine. 
+
+    The peg_positions are the number of advances of this wheel before it will 
+    advance the next wheel.
+
+    >>> wheel_3 = Wheel(wheel_iii_spec, wheel_iii_pegs, position='b', ring_setting=1)
+    >>> wheel_3.position
+    1
+    >>> wheel_3.peg_positions
+    [20]
+    >>> wheel_3.position_l
+    'b'
+    >>> wheel_3.advance()
+    >>> wheel_3.position
+    2
+    >>> wheel_3.peg_positions
+    [19]
+    >>> wheel_3.position_l
+    'c'
+
+    >>> wheel_6 = Wheel(wheel_vi_spec, wheel_vi_pegs, position='b', ring_setting=3)
+    >>> cat(wheel_6.forward(l) for l in string.ascii_lowercase)
+    'xkqhwpvngzrcfoiaselbtymjdu'
+    >>> cat(wheel_6.backward(l) for l in string.ascii_lowercase)
+    'ptlyrmidoxbswhnfckquzgeavj'
+    >>> wheel_6.position
+    25
+    >>> 11 in wheel_6.peg_positions
+    True
+    >>> 24 in wheel_6.peg_positions
+    True
+    >>> wheel_6.position_l
+    'b'
+
+    >>> wheel_6.advance()
+    >>> cat(wheel_6.forward(l) for l in string.ascii_lowercase)
+    'jpgvoumfyqbenhzrdkasxlictw'
+    >>> cat(wheel_6.backward(l) for l in string.ascii_lowercase)
+    'skxqlhcnwarvgmebjptyfdzuio'
+    >>> wheel_6.position
+    0
+    >>> 10 in wheel_6.peg_positions
+    True
+    >>> 23 in wheel_6.peg_positions
+    True
+    >>> wheel_6.position_l
+    'c'
+
+    >>> for _ in range(22): wheel_6.advance()
+    >>> cat(wheel_6.forward(l) for l in string.ascii_lowercase)
+    'mgxantkzsyqjcufirldvhoewbp'
+    >>> cat(wheel_6.backward(l) for l in string.ascii_lowercase)
+    'dymswobuplgraevzkqifntxcjh'
+    >>> wheel_6.position
+    22
+    >>> 1 in wheel_6.peg_positions
+    True
+    >>> 14 in wheel_6.peg_positions
+    True
+    >>> wheel_6.position_l
+    'y'
+
+    >>> wheel_6.advance()
+    >>> cat(wheel_6.forward(l) for l in string.ascii_lowercase)
+    'fwzmsjyrxpibtehqkcugndvaol'
+    >>> cat(wheel_6.backward(l) for l in string.ascii_lowercase)
+    'xlrvnatokfqzduyjphemswbigc'
+    >>> wheel_6.position
+    23
+    >>> 0 in wheel_6.peg_positions
+    True
+    >>> 13 in wheel_6.peg_positions
+    True
+    >>> wheel_6.position_l
+    'z'
+
+    >>> wheel_6.advance()
+    >>> cat(wheel_6.forward(l) for l in string.ascii_lowercase)
+    'vylrixqwohasdgpjbtfmcuznke'
+    >>> cat(wheel_6.backward(l) for l in string.ascii_lowercase)
+    'kqumzsnjepyctxiogdlrvahfbw'
+    >>> wheel_6.position
+    24
+    >>> 25 in wheel_6.peg_positions
+    True
+    >>> 12 in wheel_6.peg_positions
+    True
+    >>> wheel_6.position_l
+    'a'
+
+    >>> wheel_6.advance()
+    >>> cat(wheel_6.forward(l) for l in string.ascii_lowercase)
+    'xkqhwpvngzrcfoiaselbtymjdu'
+    >>> cat(wheel_6.backward(l) for l in string.ascii_lowercase)
+    'ptlyrmidoxbswhnfckquzgeavj'
+    >>> wheel_6.position
+    25
+    >>> 24 in wheel_6.peg_positions
+    True
+    >>> 11 in wheel_6.peg_positions
+    True
+    >>> wheel_6.position_l
+    'b'
+
+    >>> wheel_6.advance()
+    >>> cat(wheel_6.forward(l) for l in string.ascii_lowercase)
+    'jpgvoumfyqbenhzrdkasxlictw'
+    >>> cat(wheel_6.backward(l) for l in string.ascii_lowercase)
+    'skxqlhcnwarvgmebjptyfdzuio'
+    >>> wheel_6.position
+    0
+    >>> 23 in wheel_6.peg_positions
+    True
+    >>> 10 in wheel_6.peg_positions
+    True
+    >>> wheel_6.position_l
+    'c'
+
+    """
+    def __init__(self, transform, ring_peg_letters, ring_setting=1, position='a', raw_transform=False):
+        self.ring_peg_letters = ring_peg_letters
+        self.ring_setting = ring_setting
+        super(Wheel, self).__init__(transform, position=position, raw_transform=raw_transform)
+        self.set_position(position)
+        
+    def __getattribute__(self,name):
+        if name=='position_l':
+            return unpos(self.position + self.ring_setting - 1)
+        else:
+            return object.__getattribute__(self, name)
+
+    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):
+    """An Enigma machine.
+
+    >>> enigma = Enigma(reflector_b_spec, \
+                wheel_i_spec, wheel_i_pegs, \
+                wheel_ii_spec, wheel_ii_pegs, \
+                wheel_iii_spec, wheel_iii_pegs, \
+                1, 1, 1, \
+                '')
+    >>> enigma.set_wheels('a', 'a', 't')
+    >>> enigma.wheel_positions
+    (0, 0, 19)
+    >>> cat(enigma.wheel_positions_l)
+    'aat'
+    >>> enigma.peg_positions
+    ([16], [4], [2])
+    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
+    'puvioztjdhxmlyeawsrgbcqknf'
+
+    >>> enigma.advance()
+    >>> enigma.wheel_positions
+    (0, 0, 20)
+    >>> cat(enigma.wheel_positions_l)
+    'aau'
+    >>> enigma.peg_positions
+    ([16], [4], [1])
+    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
+    'baigpldqcowfyzjehvtsxrkumn'
+
+    >>> enigma.advance()
+    >>> enigma.wheel_positions
+    (0, 0, 21)
+    >>> cat(enigma.wheel_positions_l)
+    'aav'
+    >>> enigma.peg_positions
+    ([16], [4], [0])
+    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
+    'mnvfydiwgzsoablrxpkutchqej'
+
+    >>> enigma.advance()
+    >>> enigma.wheel_positions
+    (0, 1, 22)
+    >>> cat(enigma.wheel_positions_l)
+    'abw'
+    >>> enigma.peg_positions
+    ([16], [3], [25])
+    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
+    'ulfopcykswhbzvderqixanjtgm'
+
+    >>> enigma.advance()
+    >>> enigma.wheel_positions
+    (0, 1, 23)
+    >>> cat(enigma.wheel_positions_l)
+    'abx'
+    >>> enigma.peg_positions
+    ([16], [3], [24])
+    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
+    'qmwftdyovursbzhxaklejicpgn'
+
+    >>> enigma.advance()
+    >>> enigma.wheel_positions
+    (0, 1, 24)
+    >>> cat(enigma.wheel_positions_l)
+    'aby'
+    >>> enigma.peg_positions
+    ([16], [3], [23])
+    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
+    'oljmzxrvucybdqasngpwihtfke'
+
+
+
+
+    >>> enigma.set_wheels('a', 'd', 't')
+    >>> enigma.wheel_positions
+    (0, 3, 19)
+    >>> cat(enigma.wheel_positions_l)
+    'adt'
+    >>> enigma.peg_positions
+    ([16], [1], [2])
+    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
+    'zcbpqxwsjiuonmldethrkygfva'
+
+    >>> enigma.advance()
+    >>> enigma.wheel_positions
+    (0, 3, 20)
+    >>> cat(enigma.wheel_positions_l)
+    'adu'
+    >>> enigma.peg_positions
+    ([16], [1], [1])
+    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
+    'ehprawjbngotxikcsdqlzyfmvu'
+
+    >>> enigma.advance()
+    >>> enigma.wheel_positions
+    (0, 3, 21)
+    >>> cat(enigma.wheel_positions_l)
+    'adv'
+    >>> enigma.peg_positions
+    ([16], [1], [0])
+    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
+    'eqzxarpihmnvjkwgbfuyslodtc'
+
+    >>> enigma.advance()
+    >>> enigma.wheel_positions
+    (0, 4, 22)
+    >>> cat(enigma.wheel_positions_l)
+    'aew'
+    >>> enigma.peg_positions
+    ([16], [0], [25])
+    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
+    'qedcbtpluzmhkongavwfirsyxj'
+
+    >>> enigma.advance()
+    >>> enigma.wheel_positions
+    (1, 5, 23)
+    >>> cat(enigma.wheel_positions_l)
+    'bfx'
+    >>> enigma.peg_positions
+    ([15], [25], [24])
+    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
+    'iwuedhsfazqxytvrkpgncoblmj'
+
+    >>> enigma.advance()
+    >>> enigma.wheel_positions
+    (1, 5, 24)
+    >>> cat(enigma.wheel_positions_l)
+    'bfy'
+    >>> enigma.peg_positions
+    ([15], [25], [23])
+    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
+    'baknstqzrmcxjdvygiefwoulph'
+
+
+    >>> enigma.set_wheels('a', 'a', 'a')
+    >>> ct = enigma.encipher('testmessage')
+    >>> ct
+    'olpfhnvflyn'
+
+    >>> enigma.set_wheels('a', 'd', 't')
+    >>> ct = enigma.encipher('testmessage')
+    >>> ct
+    'lawnjgpwjik'
+
+
+    >>> enigma.set_wheels('b', 'd', 'q')
+    >>> ct = enigma.encipher('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
+    >>> ct
+    'kvmmwrlqlqsqpeugjrcxzwpfyiyybwloewrouvkpoztceuwtfjzqwpbqldttsr'
+    >>> enigma.left_wheel.position_l
+    'c'
+    >>> enigma.middle_wheel.position_l
+    'h'
+    >>> enigma.right_wheel.position_l
+    'a'
+
+    # Setting sheet line 31 from http://www.codesandciphers.org.uk/enigma/enigma3.htm
+    # Enigma simulation settings are 
+    # http://enigma.louisedade.co.uk/enigma.html?m3;b;b153;AFTX;AJEU;AU-BG-EY-FP-HL-IN-JZ-OS-QR-TX
+    >>> enigma31 = Enigma(reflector_b_spec, \
+                wheel_i_spec, wheel_i_pegs, \
+                wheel_v_spec, wheel_v_pegs, \
+                wheel_iii_spec, wheel_iii_pegs, \
+                6, 20, 24, \
+                'ua pf rq so ni ey bg hl tx zj')
+
+    >>> enigma31.set_wheels('j', 'e', 'u')
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (4, 11, 24)
+    >>> cat(enigma31.wheel_positions_l)
+    'jev'
+    >>> enigma31.peg_positions
+    ([7], [21], [0])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'mvqjlyowkdieasgzcunxrbhtfp'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (4, 12, 25)
+    >>> cat(enigma31.wheel_positions_l)
+    'jfw'
+    >>> enigma31.peg_positions
+    ([7], [20], [25])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'sjolzuyvrbwdpxcmtiaqfhknge'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (4, 12, 0)
+    >>> cat(enigma31.wheel_positions_l)
+    'jfx'
+    >>> enigma31.peg_positions
+    ([7], [20], [24])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'qrxedkoywufmlvgsabpzjnicht'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (4, 12, 1)
+    >>> cat(enigma31.wheel_positions_l)
+    'jfy'
+    >>> enigma31.peg_positions
+    ([7], [20], [23])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'hpsukliagqefwvtbjxcodnmrzy'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (4, 12, 2)
+    >>> cat(enigma31.wheel_positions_l)
+    'jfz'
+    >>> enigma31.peg_positions
+    ([7], [20], [22])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'zevnbpyqowrtxdifhkulscjmga'
+
+
+    >>> enigma31.set_wheels('i', 'd', 'z')
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 10, 3)
+    >>> cat(enigma31.wheel_positions_l)
+    'ida'
+    >>> enigma31.peg_positions
+    ([8], [22], [21])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'ikhpqrvcambzjondefwyxgsutl'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 10, 4)
+    >>> cat(enigma31.wheel_positions_l)
+    'idb'
+    >>> enigma31.peg_positions
+    ([8], [22], [20])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'cdabskhgzwfmlqvunyexpojtri'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 10, 5)
+    >>> cat(enigma31.wheel_positions_l)
+    'idc'
+    >>> enigma31.peg_positions
+    ([8], [22], [19])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'pcbwiqhgemyvjsuaftnroldzkx'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 10, 6)
+    >>> cat(enigma31.wheel_positions_l)
+    'idd'
+    >>> enigma31.peg_positions
+    ([8], [22], [18])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'xcbfvdnouptmlghjzwykierasq'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 10, 7)
+    >>> cat(enigma31.wheel_positions_l)
+    'ide'
+    >>> enigma31.peg_positions
+    ([8], [22], [17])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'xfvglbdynuseriwqpmkzjcoaht'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 10, 8)
+    >>> cat(enigma31.wheel_positions_l)
+    'idf'
+    >>> enigma31.peg_positions
+    ([8], [22], [16])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'tfpqlbouynsewjgcdxkahzmriv'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 10, 9)
+    >>> cat(enigma31.wheel_positions_l)
+    'idg'
+    >>> enigma31.peg_positions
+    ([8], [22], [15])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'cjaunvlwtbygzexrspqidfhokm'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 10, 10)
+    >>> cat(enigma31.wheel_positions_l)
+    'idh'
+    >>> enigma31.peg_positions
+    ([8], [22], [14])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'yltxkrqvowebzpingfucshjdam'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 10, 11)
+    >>> cat(enigma31.wheel_positions_l)
+    'idi'
+    >>> enigma31.peg_positions
+    ([8], [22], [13])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'myktluzrnxceaiqsohpdfwvjbg'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 10, 12)
+    >>> cat(enigma31.wheel_positions_l)
+    'idj'
+    >>> enigma31.peg_positions
+    ([8], [22], [12])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'pynjrmiugdqxfcvakewzhoslbt'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 10, 13)
+    >>> cat(enigma31.wheel_positions_l)
+    'idk'
+    >>> enigma31.peg_positions
+    ([8], [22], [11])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'mwvedyplnoxhaijgrqtszcbkfu'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 10, 14)
+    >>> cat(enigma31.wheel_positions_l)
+    'idl'
+    >>> enigma31.peg_positions
+    ([8], [22], [10])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'qcbrfeutvoxpnmjladzhgiykws'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 10, 15)
+    >>> cat(enigma31.wheel_positions_l)
+    'idm'
+    >>> enigma31.peg_positions
+    ([8], [22], [9])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'dnoahryetsmukbcvwfjilpqzgx'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 10, 16)
+    >>> cat(enigma31.wheel_positions_l)
+    'idn'
+    >>> enigma31.peg_positions
+    ([8], [22], [8])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'nidcfehgbqsovalyjzkxwmutpr'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 10, 17)
+    >>> cat(enigma31.wheel_positions_l)
+    'ido'
+    >>> enigma31.peg_positions
+    ([8], [22], [7])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'joifxdulcarhzpbntkwqgysevm'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 10, 18)
+    >>> cat(enigma31.wheel_positions_l)
+    'idp'
+    >>> enigma31.peg_positions
+    ([8], [22], [6])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'ptnlsxvozmwdjchayuebrgkfqi'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 10, 19)
+    >>> cat(enigma31.wheel_positions_l)
+    'idq'
+    >>> enigma31.peg_positions
+    ([8], [22], [5])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'slwopzqnmxybihdeguavrtcjkf'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 10, 20)
+    >>> cat(enigma31.wheel_positions_l)
+    'idr'
+    >>> enigma31.peg_positions
+    ([8], [22], [4])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'hcbedwlamzogixkytsrqvufnpj'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 10, 21)
+    >>> cat(enigma31.wheel_positions_l)
+    'ids'
+    >>> enigma31.peg_positions
+    ([8], [22], [3])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'odxbjwzrmelkisavuhnyqpfctg'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 10, 22)
+    >>> cat(enigma31.wheel_positions_l)
+    'idt'
+    >>> enigma31.peg_positions
+    ([8], [22], [2])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'udgbfeclrwnhxksvtioqapjmzy'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 10, 23)
+    >>> cat(enigma31.wheel_positions_l)
+    'idu'
+    >>> enigma31.peg_positions
+    ([8], [22], [1])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'nrdczqxmowvshaiufblypkjgte'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 10, 24)
+    >>> cat(enigma31.wheel_positions_l)
+    'idv'
+    >>> enigma31.peg_positions
+    ([8], [22], [0])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'hkifjdoacebqtzgulyvmpsxwrn'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 11, 25)
+    >>> cat(enigma31.wheel_positions_l)
+    'iew'
+    >>> enigma31.peg_positions
+    ([8], [21], [25])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'yptzuhofqvnmlkgbixwcejsrad'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 11, 0)
+    >>> cat(enigma31.wheel_positions_l)
+    'iex'
+    >>> enigma31.peg_positions
+    ([8], [21], [24])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'vkdcwhqfjibzsptngumoraeyxl'
+
+    >>> enigma31.advance()
+    >>> enigma31.wheel_positions
+    (3, 11, 1)
+    >>> cat(enigma31.wheel_positions_l)
+    'iey'
+    >>> enigma31.peg_positions
+    ([8], [21], [23])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'wenpbqrouxlkychdfgzvitajms'
+
+
+    >>> enigma31.set_wheels('i', 'd', 'z')
+    >>> enigma31.encipher('verylongtestmessagewithanextrabitofmessageforgoodmeasure')
+    'gstsegeqdrthkfwesljjomfvcqwcfspxpfqqmewvddybarzwubxtpejz'
+    >>> enigma31.wheel_positions
+    (3, 12, 6)
+    >>> cat(enigma31.wheel_positions_l)
+    'ifd'
+    >>> enigma31.peg_positions
+    ([8], [20], [18])
+    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
+    'urygzpdmxtwshqvfnbljaokice'
+
+    >>> enigma31.set_wheels('i', 'd', 'z')
+    >>> enigma31.decipher('gstsegeqdrthkfwesljjomfvcqwcfspxpfqqmewvddybarzwubxtpejz')
+    'verylongtestmessagewithanextrabitofmessageforgoodmeasure'
+    """
+    def __init__(self, reflector_spec,
+                 left_wheel_spec, left_wheel_pegs,
+                 middle_wheel_spec, middle_wheel_pegs,
+                 right_wheel_spec, right_wheel_pegs,
+                 left_ring_setting, middle_ring_setting, right_ring_setting,
+                 plugboard_setting):
+        self.reflector = Reflector(reflector_spec)
+        self.left_wheel = Wheel(left_wheel_spec, left_wheel_pegs, ring_setting=left_ring_setting)
+        self.middle_wheel = Wheel(middle_wheel_spec, middle_wheel_pegs, ring_setting=middle_ring_setting)
+        self.right_wheel = Wheel(right_wheel_spec, right_wheel_pegs, ring_setting=right_ring_setting)
+        self.plugboard = Plugboard(plugboard_setting)
+        
+    def __getattribute__(self,name):
+        if name=='wheel_positions':
+            return self.left_wheel.position, self.middle_wheel.position, self.right_wheel.position 
+        elif name=='wheel_positions_l':
+            return self.left_wheel.position_l, self.middle_wheel.position_l, self.right_wheel.position_l 
+        elif name=='peg_positions':
+            return self.left_wheel.peg_positions, self.middle_wheel.peg_positions, self.right_wheel.peg_positions
+        else:
+            return object.__getattribute__(self, name)
+
+    def set_wheels(self, left_wheel_position, middle_wheel_position, right_wheel_position):
+        self.left_wheel.set_position(left_wheel_position)
+        self.middle_wheel.set_position(middle_wheel_position)
+        self.right_wheel.set_position(right_wheel_position)
+        
+    def lookup(self, letter):
+        a = self.plugboard.forward(letter)
+        b = self.right_wheel.forward(a)
+        c = self.middle_wheel.forward(b)
+        d = self.left_wheel.forward(c)
+        e = self.reflector.forward(d)
+        f = self.left_wheel.backward(e)
+        g = self.middle_wheel.backward(f)
+        h = self.right_wheel.backward(g)
+        i = self.plugboard.backward(h)
+        return i
+    
+    def advance(self):
+        advance_middle = False
+        advance_left = False
+        if 0 in self.right_wheel.peg_positions:
+            advance_middle = True
+        if 0 in self.middle_wheel.peg_positions:
+            advance_left = True
+            advance_middle = True
+        self.right_wheel.advance()
+        if advance_middle: self.middle_wheel.advance()
+        if advance_left: self.left_wheel.advance()
+            
+    def encipher_letter(self, letter):
+        self.advance()
+        return self.lookup(letter)
+    
+    def encipher(self, message):
+        enciphered = ''
+        for letter in clean(message):
+            enciphered += self.encipher_letter(letter)
+        return enciphered
+
+    decipher = encipher
+
+
+# for i in range(26):
+#     enigma.advance()
+#     print('enigma.advance()')
+#     print("assert(enigma.wheel_positions == {})".format(enigma.wheel_positions))
+#     print("assert(cat(enigma.wheel_positions_l) == '{}')".format(cat(enigma.wheel_positions_l)))
+#     print("assert(enigma.peg_positions == {})".format(enigma.peg_positions))
+#     print("assert(cat(enigma.lookup(l) for l in string.ascii_lowercase) == '{}')".format(cat(enigma.lookup(l) for l in string.ascii_lowercase)))
+#     print()
+
+
+if __name__ == "__main__":
+    import doctest
+    # doctest.testmod(extraglobs={'lt': LetterTransformer(1, 'a')})
+    doctest.testmod()
+