changeset 0:2426d1d52025

Initial commit. Started a Rotor class.
author Brian Neal <bgneal@gmail.com>
date Fri, 25 May 2012 19:38:22 -0500
parents
children 9a403915a740
files .hgignore enigma/__init__.py enigma/rotors/__init__.py enigma/rotors/rotor.py
diffstat 2 files changed, 203 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Fri May 25 19:38:22 2012 -0500
@@ -0,0 +1,3 @@
+syntax: glob
+*.pyc
+*.swp
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/enigma/rotors/rotor.py	Fri May 25 19:38:22 2012 -0500
@@ -0,0 +1,200 @@
+"""rotor.py - this module contains the Rotor class for the Enigma simulation.
+
+Copyright (C) 2012 by Brian Neal.
+This file is part of Py-Enigma, the Enigma Machine simulation.
+Py-Enigma is released under the MIT License (see License.txt).
+
+"""
+import string
+import collections
+
+
+class RotorError(Exception):
+    pass
+
+
+# In specifying a wiring for a rotor, every letter must be included exactly
+# once. This variable helps us enforce that:
+WIRING_FREQ_SET = set((letter, 1) for letter in string.ascii_uppercase)
+
+
+class Rotor:
+    """The Rotor class represents the Enigma Machine rotors (Walzen).
+    
+    A rotor has 26 circularly arranged pins on the right (entry) side and 26
+    contacts on the left side. Each pin is connected to a single contact by
+    internal wiring, thus establishing a substitution cipher. We represent this
+    wiring by establishing a mapping from a pin to a contact (and vice versa for
+    the return path). Internally we number the pins and contacts from 0-25 in a
+    clockwise manner with 0 being the "top".
+
+    An alphabetic or numeric ring is fastened to the rotor by the operator. The
+    labels of this ring are displayed to the operator through a small window on
+    the top panel. The ring can be fixed to the rotor in one of 26 different
+    positions; this is called the ring setting (Ringstellung). We will number
+    the ring settings from 0-25 where 0 means no offset (e.g. the letter "A" is
+    mapped to pin 0 on an alphabetic ring). A ring setting of 1 means the letter
+    "B" is mapped to pin 0.
+
+    Each rotor can be in one of 26 positions on the axle, with position 0 where
+    pin/contact 0 is being indicated in the operator window. The rotor rotates
+    towards the operator by mechanical means during normal operation as keys are
+    being pressed during data entry. Position 1 is thus defined to be one step
+    from position 0. Likewise, position 25 is the last position before another
+    step returns it to position 0, completing 1 trip around the axle.
+
+    Finally, a rotor has a "stepping" or "turnover" parameter. Physically this
+    is implemented by putting a notch on the alphabet ring and it controls when
+    the rotor will "kick" the rotor to its left, causing the neighbor rotor to
+    rotate. Most rotors had one notch, but some Kriegsmarine rotors had 2
+    notches and thus rotated twice as fast.
+
+    Note that due to the system of ratchets and pawls, the middle rotor (in a 3
+    rotor Enigma) can "double-step". The middle rotor will advance on the next
+    step of the first rotor a second time in a row, if the middle rotor is in
+    its own turnover position.
+
+    Note that we allow the stepping parameter to be None. This indicates the
+    rotor does not rotate. This allows us to model the entry wheel and receivers
+    as stationary rotors.
+    
+    """
+
+    def __init__(self, model_name, wiring, ring_setting=0, stepping=None,
+            alpha_labels=True):
+        """Establish rotor characteristics:
+
+        model_name - e.g. "I", "II", "III", "Beta", "Gamma"
+
+        wiring - this should be a string of 26 alphabetic characters that
+        represents the internal wiring transformation of the signal as it enters
+        from the right side. This is the format used in various online
+        resources. For example, for the Wehrmacht & Luftwaffe Enigma type I
+        rotor the mapping is "EKMFLGDQVZNTOWYHXUSPAIBRCJ".
+
+        ring_setting - this should be an integer from 0-25, inclusive, which
+        indicates the Ringstellung. A value of 0 means there is no offset; e.g.
+        the letter "A" is fixed to pin 0. A value of 1 means "B" is mapped to
+        pin 0.
+
+        stepping - this is the stepping or turnover parameter. It can be a
+        simple string such as "R". This will indicate that when the rotor
+        transitions from "Q" to "R" (by observing the operator window), the
+        rotor will "kick" the rotor to its left, causing it to rotate. If the
+        rotor has more than one notch, this parameter should be a list of letter
+        positions, e.g. ['A', 'N'].
+
+        alpha_labels - when True, the letters A-Z are used for the rotor ring
+        labels. If False, numeric string labels (01-26) are used.
+
+        """
+        self.name = model_name
+        self.wiring_str = wiring.upper()
+        self.ring_setting = ring_setting
+        self.stepping_parms = stepping
+        self.alpha_labels = alpha_labels
+        self.pos = None     # not installed in an Enigma machine yet
+
+        # check wiring length
+        if len(self.wiring_str) != 26:
+            raise RotorError("invalid wiring length")
+
+        # check wiring format; must contain A-Z
+        for c in self.wiring_str:
+            if c not in string.ascii_uppercase:
+                raise RotorError("invalid wiring: %s" % wiring)
+
+        # check wiring format; ensure every letter appears exactly once
+        input_set = set(collections.Counter(self.wiring_str).items())
+        if input_set != WIRING_FREQ_SET:
+            raise RotorError("invalid wiring frequency")
+
+        if not isinstance(ring_setting, int) or not (0 <= ring_setting < 26):
+            raise RotorError("invalid ring_setting")
+
+        # Create two lists to describe the internal wiring. Two lists are used
+        # to do fast lookup from both entry (from the right) and exit (from the
+        # left). 
+        
+        self.entry_map = [ord(pin) - ord('A') for pin in self.wiring_str]
+        
+        self.exit_map = [0] * 26
+        for i, v in enumerate(self.entry_map):
+            self.exit_map[v] = i
+
+        # configure ring labels
+        if alpha_labels:
+            self.ring_labels = string.ascii_uppercase
+        else:
+            self.ring_labels = ['{:02d}'.format(n) for n in range(1, 27)]
+
+        # for mapping window display values to indices
+        self.display_map = dict(zip(self.ring_labels, range(26)))
+
+    def set_display(self, val):
+        """Spin the rotor such that the string val appears in the operator
+        window.
+
+        This sets the internal position of the rotor on the axle and thus
+        rotates the pins and contacts accordingly.
+
+        A value of 'A' for example puts the rotor in position 0, assuming an
+        internal ring setting of 0 and alphabetic labels.
+
+        If the rotor is using alphabetic ring labels, val must be a string in
+        'A' - 'Z'.
+
+        If the rotor is not using alphabetic ring labels, val must be a string
+        of the form '01' - '26'.
+
+        """
+        s = val.upper()
+        if s not in self.display_map:
+            raise RotorError("bad display value %s" % s)
+
+        index = self.display_map[s]
+
+        self.pos = (index - self.ring_setting) % 26
+
+    def get_display(self):
+        """Returns what is currently being displayed in the operator window."""
+
+        index = (self.pos + self.ring_setting) % 26
+        return self.ring_labels[index]
+
+    def signal_in(self, n):
+        """Simulate a signal entering the rotor from the right at a given pin
+        position n.
+
+        n must be an integer between 0 and 25.
+
+        Returns the contact number of the output signal (0-25).
+
+        """
+        # determine what pin we have at that position due to rotation
+        pin = (n + self.pos) % 26
+
+        # run it through the internal wiring
+        contact = self.entry_map[pin]
+
+        # turn back into a position due to rotation
+        return (contact - self.pos) % 26
+
+    def signal_out(self, n):
+        """Simulate a signal entering the rotor from the left at a given
+        contact position n.
+
+        n must be an integer between 0 and 25.
+
+        Returns the pin number of the output signal (0-25).
+
+        """
+        # determine what contact we have at that position due to rotation
+        contact = (n + self.pos) % 26
+
+        # run it through the internal wiring
+        pin = self.exit_map[contact]
+
+        # turn back into a position due to rotation
+        return (pin - self.pos) % 26
+