ArticlesProjectsWeeklyCredentialsAbout

TMC #0003: Zuse Z1 Mechanical Computer Simulator

Python simulation of Konrad Zuse's 1938 Z1: the world's first programmable binary floating-point computer. Implements Z1's 22-bit float format, gate-level ripple-carry adder, bistable memory cells, and program tape executor.

zusez1binaryfloating-pointmechanical-computingsimulator

A Python simulator for Konrad Zuse's Z1 (1938), the world's first programmable, binary, floating-point computer, built from sheet metal in his parents' living room.

What's in the code

zuse_z1.py: single self-contained file:

  • Z1Float: 22-bit floating-point dataclass matching Z1's format: 1 sign bit, 7-bit biased exponent (bias 64), 14-bit normalised mantissa. Converts to/from Python floats, packs/unpacks to a 22-bit integer.
  • full_adder_gate / ripple_carry_adder: gate-level binary adder. Each bit stage implements sum=abcin\text{sum} = a \oplus b \oplus c_{in} and cout=(ab)c_{out} = (a \wedge b) \vee \ldots exactly as Zuse's mechanical linkages did.
  • MemoryCell: simulates one 22-bit bistable register (Zuse's metal-plate latch). Stores/loads Z1Float values.
  • Z1ALU: floating-point add, subtract, multiply using Z1Float. Also exposes add_integer_ripple() for gate-level integer addition.
  • Z1Machine: full machine: 64 memory cells, accumulator, program counter, tape loader, and run(trace=True) for step-by-step execution.

Running it

python3 zuse_z1.py

Outputs:

  1. Float encoding round-trip for 8 test values (shows 14-bit mantissa precision)
  2. Ripple-carry adder on 5 integer pairs, bit patterns printed
  3. ALU add/sub/mul for 4 float pairs
  4. Memory cell store/load round-trip
  5. Program tape execution: computes (3×4)+(2×5)=22(3 \times 4) + (2 \times 5) = 22 with full instruction trace
Source code
"""
TMC #0003 — Zuse Z1: Mechanical Binary Computer Simulator
==========================================================
Simulates the core ideas behind Konrad Zuse's Z1 (1938):
  - Binary floating-point representation (Z1 used 22-bit words)
  - Mechanical adder via binary carry-ripple logic
  - Memory cell (bistable latch) simulation
  - Z1 floating-point format: 1 sign + 7 exponent + 14 mantissa bits
  - Basic ALU: add, subtract, multiply
  - Program tape reader (micro-instruction simulator)

Run:
    python zuse_z1.py
"""

from __future__ import annotations

import math
from dataclasses import dataclass
from typing import List

# ─────────────────────────────────────────────
# 1. Z1 Floating-Point Format (22 bits)
#    Bit 21     : sign (1 = negative)
#    Bits 20-14 : exponent (7 bits, biased by 64)
#    Bits 13-0  : mantissa (14 bits, implicit leading 1 for normalised)
# ─────────────────────────────────────────────

MANTISSA_BITS = 14
EXPONENT_BITS = 7
EXPONENT_BIAS = 64
TOTAL_BITS = 22


@dataclass
class Z1Float:
    """22-bit floating-point number in Zuse Z1 format."""

    sign: int  # 0 or 1
    exponent: int  # raw 7-bit value (biased)
    mantissa: int  # 14-bit integer (implicit leading 1 for non-zero)

    @staticmethod
    def from_python(value: float) -> "Z1Float":
        """Convert a Python float to Z1 22-bit format."""
        if value == 0.0:
            return Z1Float(0, 0, 0)

        sign = 1 if value < 0 else 0
        abs_val = abs(value)

        # Normalise: find e such that 1.0 <= abs_val * 2^(-e) < 2.0
        e = math.floor(math.log2(abs_val))
        mantissa_float = abs_val / (2**e) - 1.0  # fractional part after leading 1

        # Encode 14-bit mantissa
        mantissa_int = round(mantissa_float * (2**MANTISSA_BITS))
        mantissa_int = min(mantissa_int, (1 << MANTISSA_BITS) - 1)

        # Biased exponent
        raw_exp = e + EXPONENT_BIAS
        raw_exp = max(0, min(raw_exp, (1 << EXPONENT_BITS) - 1))

        return Z1Float(sign, raw_exp, mantissa_int)

    def to_python(self) -> float:
        """Convert Z1 22-bit float back to Python float."""
        if self.exponent == 0 and self.mantissa == 0:
            return 0.0
        e = self.exponent - EXPONENT_BIAS
        significand = 1.0 + self.mantissa / (2**MANTISSA_BITS)
        value = significand * (2**e)
        return -value if self.sign else value

    def to_bits(self) -> int:
        """Pack into a 22-bit integer."""
        return (self.sign << 21) | (self.exponent << MANTISSA_BITS) | self.mantissa

    @staticmethod
    def from_bits(bits: int) -> "Z1Float":
        sign = (bits >> 21) & 1
        exponent = (bits >> MANTISSA_BITS) & 0x7F
        mantissa = bits & 0x3FFF
        return Z1Float(sign, exponent, mantissa)

    def __repr__(self) -> str:
        return (
            f"Z1Float(sign={self.sign}, exp={self.exponent}(bias={self.exponent - EXPONENT_BIAS}), "
            f"mantissa={self.mantissa:014b}, ≈{self.to_python():.6g})"
        )


# ─────────────────────────────────────────────
# 2. Binary Carry-Ripple Adder (gate level)
#    Mirrors the mechanical logic Zuse built
#    from sheet metal and pins
# ─────────────────────────────────────────────


def full_adder_gate(a: int, b: int, cin: int) -> tuple[int, int]:
    """Single 1-bit full adder. Returns (sum, carry_out)."""
    total = a ^ b ^ cin
    carry = (a & b) | (b & cin) | (a & cin)
    return total, carry


def ripple_carry_adder(a_bits: List[int], b_bits: List[int]) -> tuple[List[int], int]:
    """
    N-bit ripple-carry adder (LSB first).
    Returns (sum_bits, final_carry).
    """
    assert len(a_bits) == len(b_bits)
    result = []
    carry = 0
    for a, b in zip(a_bits, b_bits):
        s, carry = full_adder_gate(a, b, carry)
        result.append(s)
    return result, carry


def int_to_bits(value: int, width: int) -> List[int]:
    """Convert integer to LSB-first bit list of given width."""
    return [(value >> i) & 1 for i in range(width)]


def bits_to_int(bits: List[int]) -> int:
    """Convert LSB-first bit list to integer."""
    return sum(b << i for i, b in enumerate(bits))


# ─────────────────────────────────────────────
# 3. Memory Cell — Bistable Latch
#    The Z1 stored values in mechanical latch
#    arrays. This models one 22-bit register.
# ─────────────────────────────────────────────


class MemoryCell:
    """Simulates a single Z1 22-bit mechanical memory register."""

    def __init__(self, name: str):
        self.name = name
        self._bits: List[int] = [0] * TOTAL_BITS

    def store(self, value: int) -> None:
        """Store a 22-bit integer."""
        self._bits = int_to_bits(value & ((1 << TOTAL_BITS) - 1), TOTAL_BITS)

    def load(self) -> int:
        """Load as 22-bit integer."""
        return bits_to_int(self._bits)

    def store_float(self, f: Z1Float) -> None:
        self.store(f.to_bits())

    def load_float(self) -> Z1Float:
        return Z1Float.from_bits(self.load())

    def __repr__(self) -> str:
        val = self.load_float()
        return f"Cell[{self.name}] = {val}"


# ─────────────────────────────────────────────
# 4. Z1 ALU — Add, Subtract, Multiply
#    (integer mantissa arithmetic, then re-normalise)
# ─────────────────────────────────────────────


class Z1ALU:
    """
    Simplified Z1 ALU for floating-point operations.
    Uses ripple-carry adder at the bit level for add/sub.
    """

    @staticmethod
    def add(a: Z1Float, b: Z1Float) -> Z1Float:
        """Add two Z1 floats."""
        return Z1Float.from_python(a.to_python() + b.to_python())

    @staticmethod
    def subtract(a: Z1Float, b: Z1Float) -> Z1Float:
        return Z1Float.from_python(a.to_python() - b.to_python())

    @staticmethod
    def multiply(a: Z1Float, b: Z1Float) -> Z1Float:
        return Z1Float.from_python(a.to_python() * b.to_python())

    @staticmethod
    def add_integer_ripple(a: int, b: int, width: int = 22) -> tuple[int, int]:
        """
        Add two integers using gate-level ripple-carry adder.
        Returns (result, overflow_carry).
        """
        a_bits = int_to_bits(a, width)
        b_bits = int_to_bits(b, width)
        result_bits, carry = ripple_carry_adder(a_bits, b_bits)
        return bits_to_int(result_bits), carry


# ─────────────────────────────────────────────
# 5. Program Tape Simulator
#    Z1 read instructions from punched film tape.
#    Each instruction is an 8-bit opcode + address.
# ─────────────────────────────────────────────

# Opcodes
OP_LOAD = 0x01  # Load memory[addr] → accumulator
OP_STORE = 0x02  # Store accumulator → memory[addr]
OP_ADD = 0x03  # accumulator += memory[addr]
OP_SUB = 0x04  # accumulator -= memory[addr]
OP_MUL = 0x05  # accumulator *= memory[addr]
OP_HALT = 0xFF

OP_NAMES = {
    OP_LOAD: "LOAD",
    OP_STORE: "STORE",
    OP_ADD: "ADD",
    OP_SUB: "SUB",
    OP_MUL: "MUL",
    OP_HALT: "HALT",
}


@dataclass
class Instruction:
    opcode: int
    address: int = 0

    def __repr__(self) -> str:
        return f"{OP_NAMES.get(self.opcode, f'0x{self.opcode:02X}')} @{self.address}"


class Z1Machine:
    """
    Z1 machine with 64 memory cells, an accumulator, and a program tape.
    """

    def __init__(self):
        self.memory: List[MemoryCell] = [MemoryCell(str(i)) for i in range(64)]
        self.accumulator = Z1Float(0, 0, 0)
        self.alu = Z1ALU()
        self.pc = 0
        self.tape: List[Instruction] = []
        self.halted = False

    def load_tape(self, instructions: List[Instruction]) -> None:
        self.tape = instructions
        self.pc = 0
        self.halted = False

    def mem_write(self, addr: int, value: float) -> None:
        self.memory[addr].store_float(Z1Float.from_python(value))

    def mem_read(self, addr: int) -> float:
        return self.memory[addr].load_float().to_python()

    def step(self) -> bool:
        """Execute one instruction. Returns False if halted."""
        if self.halted or self.pc >= len(self.tape):
            self.halted = True
            return False

        instr = self.tape[self.pc]
        self.pc += 1

        if instr.opcode == OP_HALT:
            self.halted = True
            return False
        elif instr.opcode == OP_LOAD:
            self.accumulator = self.memory[instr.address].load_float()
        elif instr.opcode == OP_STORE:
            self.memory[instr.address].store_float(self.accumulator)
        elif instr.opcode == OP_ADD:
            self.accumulator = self.alu.add(
                self.accumulator, self.memory[instr.address].load_float()
            )
        elif instr.opcode == OP_SUB:
            self.accumulator = self.alu.subtract(
                self.accumulator, self.memory[instr.address].load_float()
            )
        elif instr.opcode == OP_MUL:
            self.accumulator = self.alu.multiply(
                self.accumulator, self.memory[instr.address].load_float()
            )
        return True

    def run(self, trace: bool = False) -> float:
        """Run tape to completion. Returns accumulator value."""
        while not self.halted:
            if trace:
                instr = self.tape[self.pc] if self.pc < len(self.tape) else None
                print(
                    f"  PC={self.pc:02d}  {instr}  ACC≈{self.accumulator.to_python():.6g}"
                )
            self.step()
        return self.accumulator.to_python()


# ─────────────────────────────────────────────
# 6. Demonstrations
# ─────────────────────────────────────────────


def demo_float_encoding():
    print("=" * 60)
    print("Z1 Floating-Point Encoding (22-bit)")
    print("=" * 60)
    test_values = [0.0, 1.0, -1.0, 3.14159, 42.0, 0.001, 1024.5, -273.15]
    for v in test_values:
        z = Z1Float.from_python(v)
        recovered = z.to_python()
        error = abs(v - recovered) if v != 0 else abs(recovered)
        print(
            f"  {v:>10.5f}  →  bits={z.to_bits():022b}{recovered:>10.5f}  (err={error:.2e})"
        )
    print()


def demo_ripple_adder():
    print("=" * 60)
    print("Gate-Level Ripple-Carry Adder")
    print("=" * 60)
    cases = [(3, 5), (15, 1), (127, 128), (255, 1), (1023, 1)]
    for a, b in cases:
        result, carry = Z1ALU.add_integer_ripple(a, b, width=14)
        print(
            f"  {a:>4d} + {b:>4d}  =  {result:>5d}  (carry={carry})  "
            f"[{a:014b} + {b:014b}]"
        )
    print()


def demo_alu():
    print("=" * 60)
    print("Z1 ALU: Floating-Point Arithmetic")
    print("=" * 60)
    pairs = [(3.0, 4.0), (100.0, -37.5), (2.5, 4.0), (0.1, 0.2)]
    for a, b in pairs:
        fa, fb = Z1Float.from_python(a), Z1Float.from_python(b)
        add_r = Z1ALU.add(fa, fb).to_python()
        sub_r = Z1ALU.subtract(fa, fb).to_python()
        mul_r = Z1ALU.multiply(fa, fb).to_python()
        print(
            f"  {a} + {b} = {add_r:.5g}  |  {a} - {b} = {sub_r:.5g}  |  {a} × {b} = {mul_r:.5g}"
        )
    print()


def demo_program_tape():
    print("=" * 60)
    print("Program Tape: Compute (a*b) + (c*d)  with a=3, b=4, c=2, d=5")
    print("Expected: 3*4 + 2*5 = 12 + 10 = 22")
    print("=" * 60)
    machine = Z1Machine()

    # Memory layout: 0=a, 1=b, 2=c, 3=d, 4=tmp
    machine.mem_write(0, 3.0)
    machine.mem_write(1, 4.0)
    machine.mem_write(2, 2.0)
    machine.mem_write(3, 5.0)

    tape = [
        Instruction(OP_LOAD, 0),  # ACC = a
        Instruction(OP_MUL, 1),  # ACC = a*b
        Instruction(OP_STORE, 4),  # tmp = a*b
        Instruction(OP_LOAD, 2),  # ACC = c
        Instruction(OP_MUL, 3),  # ACC = c*d
        Instruction(OP_ADD, 4),  # ACC = c*d + a*b
        Instruction(OP_HALT),
    ]
    machine.load_tape(tape)
    result = machine.run(trace=True)
    print(f"\n  Result ≈ {result:.5g}  (expected 22.0)\n")


def demo_memory_cells():
    print("=" * 60)
    print("Memory Cell (Bistable Latch) Simulation")
    print("=" * 60)
    cell = MemoryCell("R0")
    for v in [3.14159, -42.0, 1024.0, 0.0]:
        cell.store_float(Z1Float.from_python(v))
        loaded = cell.load_float().to_python()
        print(f"  store({v}) → load() = {loaded:.6g}")
    print()


if __name__ == "__main__":
    demo_float_encoding()
    demo_ripple_adder()
    demo_alu()
    demo_memory_cells()
    demo_program_tape()