view enigma/rotors/rotor.py @ 50:f3af458a5d2d tip

Add methods to Plugboard to support hill-climbing.
author Brian Neal <bgneal@gmail.com>
date Tue, 19 Jun 2012 21:32:08 -0500
parents f4051bcac8c5
children
line wrap: on
line source
# 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).

"""rotor.py - this module contains the Rotor class for the Enigma simulation."""

import string
import collections

from . import RotorError


ALPHA_LABELS = string.ascii_uppercase

# 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 ALPHA_LABELS)


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 spindle, 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 spindle.

    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
    reflectors as stationary rotors.
    
    """

    def __init__(self, model_name, wiring, ring_setting=0, stepping=None):
        """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 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 should be an
        iterable, for example a string such as "Q". 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, a string of length 2 could
        be used, e.g. "ZM".  Another way to think of this parameter is that when
        a character in the stepping string is visible in the operator window, a
        notch is lined up with the pawl on the left side of the rotor.  This
        will allow the pawl to push up on the rotor *and* the rotor to the left
        when the next key is depressed.

        Note that for purposes of simulation, our rotors will always use
        alphabetic labels A-Z. In reality, the Heer & Luftwaffe devices used
        numbers 01-26, and Kriegsmarine devices used A-Z. Our usage of A-Z is
        simply for simulation convenience. In the future we may allow either
        display.

        """
        self.name = model_name
        self.wiring_str = wiring.upper()
        self.ring_setting = ring_setting
        self.pos = 0
        self.rotations = 0

        # 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 ALPHA_LABELS:
                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

        # build a map of display values to positions
        self.display_map = {}
        for n in range(26):
            self.display_map[chr(ord('A') + n)] = (n - self.ring_setting) % 26

        # build a reverse map of position mapped to display values
        self.pos_map = {v : k for k, v in self.display_map.items()}

        # build step set; this is a set of positions where our notches are in
        # place to allow the pawls to move
        self.step_set = set()
        if stepping is not None:
            for pos in stepping:
                if pos in self.display_map:
                    self.step_set.add(pos)
                else:
                    raise RotorError("stepping: %s" % pos)

        # initialize our position and display value:
        self.set_display('A')

    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.

        The parameter val must be a string in 'A' - 'Z'.

        Setting the display resets the internal rotation counter to 0.

        """
        s = val.upper()
        if s not in self.display_map:
            raise RotorError("bad display value %s" % val)

        self.pos = self.display_map[s]
        self.display_val = s
        self.rotations = 0

    def get_display(self):
        """Returns what is currently being displayed in the operator window."""
        return self.display_val

    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

    def notch_over_pawl(self):
        """Return True if this rotor has a notch in the stepping position and
        False otherwise.

        """
        return self.display_val in self.step_set

    def rotate(self):
        """Rotate the rotor forward due to mechanical stepping action."""

        self.pos = (self.pos + 1) % 26
        self.display_val = self.pos_map[self.pos]
        self.rotations += 1