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=a⊕b⊕cin and cout=(a∧b)∨… 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 exposesadd_integer_ripple()for gate-level integer addition.Z1Machine: full machine: 64 memory cells, accumulator, program counter, tape loader, andrun(trace=True)for step-by-step execution.
Running it
python3 zuse_z1.py
Outputs:
- Float encoding round-trip for 8 test values (shows 14-bit mantissa precision)
- Ripple-carry adder on 5 integer pairs, bit patterns printed
- ALU add/sub/mul for 4 float pairs
- Memory cell store/load round-trip
- Program tape execution: computes (3×4)+(2×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()