Setting Up the Lab Bench
November 14, 202647 min readbeginner
Installs the book's tools on macOS, Linux and Windows, sets up a virtual environment, runs the companion repository's first test, and teaches the Python and NumPy that every listing in the book uses.
The companion scripts and hardware labs of Error Correction Coding run on a small set of free tools, and this chapter installs them on macOS, on Linux and on Windows through WSL, the Windows Subsystem for Linux. It then clones the companion repository, makes a virtual environment for its Python libraries and runs the first tests, and its second half teaches the Python and NumPy that the book's listings use. Every number it states is reproduced by chf2_ecc_lab_bench.py in the book's companion repository, ecc-book-lab, which prints each value beside the one stated here and stops at the first disagreement. That run needs nothing beyond Python 3.10 or newer and its standard library. Given the option --bench, the same script also asks every tool on the machine for its version and reports whether the bench is complete. The four computed figures are drawn by chf2_charts.py.
The Python taught here goes only as far as reading the listings needs. The book does not teach SystemVerilog or cocotb, since every hardware listing arrives finished from the repository. For readers who want the languages themselves, the section on learning the hardware languages in Before You Start points to RTL Design and Verification and to Computer Architecture.
Each install step follows the tool's own documentation, paraphrased and cited, and the versions named were the current ones when read on 2026-11-14. Tools move faster than books, so where a step here and a project's own page disagree, the project's page wins.
01.What the Bench Holds
A lab bench in electronics is the table of instruments a circuit is tested on. The book's bench is a set of programs, and Figure 1 sorts them by what each one takes in and what it gives back. The version-control program git copies the companion repository from GitHub onto the disk and later fetches its updates. Python runs the chapter scripts, and two Python libraries widen what it can do. NumPy, short for Numerical Python, adds arrays and matrix arithmetic, and galois adds arithmetic in finite fields, the number systems that the algebraic codes of later Parts are built from.
The hardware chapters need more. Verilator reads a design written in SystemVerilog, the language chips are described in, and translates it into a C++ program that simulates the circuit cycle by cycle, so a C++ compiler and make, the program that runs a build's steps in order, must be present too [1]. The cocotb library drives the simulated circuit from test functions written in Python and reports a pass or a fail for each [2]. Surfer draws the waveform file a simulation can record, each signal as a trace against time [3]. Yosys performs synthesis, turning a design into a network of logic gates and reporting how many gates there are and how deep the longest path through them runs [4].
Three tools wait for later Parts. Icarus Verilog is a second simulator. SymbiYosys runs formal verification, which proves that a property holds for every input instead of testing some of them, and it hands each proof to a solver such as Z3, which either finds the property true or returns a trace that breaks it.
02.Choosing a Route
Every tool on the bench is free and open source, but each operating system delivers them differently, and Figure 2 lays out the three routes this chapter takes. On macOS, Apple's Command Line Tools supply the compiler and make, and Homebrew, the package manager most macOS developers use, supplies everything else. On Linux, the distribution's own package manager supplies most of the bench. Arch Linux is the worked case here because its packages follow each project's releases closely, and the OSS CAD Suite, a prebuilt bundle published by YosysHQ, fills whatever a distribution lacks or ships too old [5]. On Windows the bench runs inside WSL2, which runs a real Linux system beside Windows, so the Windows route is the Linux route after one extra stage of setup.
The routes meet at the same final steps. The repository is cloned, a virtual environment is made inside it, pip, Python's installer, puts the three Python libraries into that environment, and the first test runs. Those shared steps are the same commands on every system.
03.Python and git
Python has a window, not just a floor. The companion scripts need Python 3.10 or newer, as Before You Start says, and the ceiling comes from cocotb. Its release 2.1.0, the one the book's labs use, refuses to install on any Python newer than 3.14, and pip stops with this message.
| RuntimeError: cocotb 2.1.0 only supports a maximum Python version of 3.14. |
That matters on a Mac, because Homebrew's default python3 was Python 3.15.2 when read on 2026-11-14 [6]. Homebrew also packages older series under versioned names, and its python@3.14 package, 3.14.10 on the same day, installs a command named python3.14 [7]. The window is therefore Python 3.10 to 3.14.
The libraries narrow it no further. NumPy 2.5.4 needs Python 3.12 or newer [8], so on 3.10 or 3.11 pip quietly picks an older NumPy, which galois still accepts, since galois 0.4.11 asks for NumPy 1.21 or newer, Numba 0.55 or newer and typing_extensions 4.0 or newer [9]. Numba, a compiler that galois uses to speed up its arithmetic, supported Python 3.10 to 3.15 in its release 0.68.1 [10].
On macOS, the Command Line Tools come first, then Python and git.
| xcode-select --install | |
| brew install python@3.14 | |
| brew install git |
On Arch Linux, pacman installs both, and Arch's python package was 3.14.10, inside the window.
| sudo pacman -S python git |
On Debian and Ubuntu, the system Python lacks the module that makes virtual environments until sudo apt install python3-venv adds it [11]. On Windows, the setup section of Project: A Channel Simulator installs WSL2 and ArchWSL step by step, and every Arch command in this chapter then runs unchanged inside it.
One more rule applies everywhere. Homebrew's Python, like the system Python of many Linux distributions, is marked externally managed under PEP 668, so pip refuses to install a library into it directly [12]. The libraries go into a virtual environment instead, made later in this chapter, and pip is always run as python -m pip, which ties it to the interpreter the command names.
04.The Hardware Tools
macOS
Homebrew packages the whole hardware bench.
| brew install verilator icarus-verilog yosys sby z3 surfer |
When read on 2026-11-14 those packages were Verilator 5.054, Icarus Verilog 13.0, Yosys 0.71, SymbiYosys 0.71, Z3 5.4.1 and Surfer 0.7.0. SymbiYosys bundles no solver of its own, which is why Z3 is installed beside it. Verilator 5.054 clears the floor cocotb sets, since cocotb needs Verilator 5.036 or newer [13].
Linux
On Arch Linux, pacman installs the compiler, make and four of the tools.
| sudo pacman -S make gcc verilator iverilog yosys z3 |
SymbiYosys and Surfer were in none of the official Arch, Ubuntu or Fedora repositories, and some distributions' own Verilator fell below the 5.036 floor on 2026-11-14, Debian 13's 5.032 and those of Ubuntu's long-term-support releases among them, while Fedora's was 5.046. The OSS CAD Suite covers both gaps on any distribution. Its project page publishes a nightly archive for each platform holding Verilator, Icarus Verilog, Yosys, SymbiYosys, Surfer, Z3 and cocotb, already built [5]. Once the archive is extracted, one command puts its tools first on the search path for the current terminal.
| source <extracted_location>/oss-cad-suite/environment |
On macOS the same bundle works too, once xattr -d com.apple.quarantine has been run on the downloaded archive to clear the flag macOS puts on downloads, or once the bundle's ./activate script has been run.
The suite also carries a Python of its own, and with the suite on the search path python3 names that bundled Python. The virtual environment is still made from an interpreter outside the suite, python3.14 on a Mac and /usr/bin/python3 on Linux, as the section on the virtual environment shows. An environment records the full path of the interpreter it was made from, so one made from the suite's Python would break when a newer nightly replaces the suite's folder.
Windows
Inside WSL2 with ArchWSL, the Arch commands above apply unchanged. The OSS CAD Suite's own documentation recommends the same arrangement, its linux-x64 archive running under WSL, over its native Windows build, and for a native build on Windows it recommends a folder whose path contains no spaces.
Containers
Verilator's documentation also offers a container image, a packaged Linux system that Docker runs in isolation, and docker run -ti verilator/verilator:latest --version confirms that it works [1]. The rest of the bench is not inside that image, so the book treats it as a last resort.
05.Cloning the Repository
Three commands make the copy in the home folder and enter it.
| cd ~ | |
| git clone https://github.com/anirbanchakraborty-dev/ecc-book-lab.git | |
| cd ecc-book-lab |
The folder holds the files LICENSE and README.md and four folders. The folder python holds the chapter scripts, hdl the SystemVerilog designs, tb the cocotb testbenches and synth the Yosys synthesis scripts. The repository grows as chapters are published, and git pull, run inside the folder, fetches whatever has been added since the clone.
06.A Virtual Environment
A virtual environment is a folder that ties one Python interpreter to its own private set of installed libraries [14]. Libraries installed while it is active land in that folder and nowhere else, so the book's pinned cocotb cannot disturb another project's, and deleting the folder removes every trace. Three commands make one inside the repository, switch to it and fill it.
| python3.14 -m venv .venv | |
| source .venv/bin/activate | |
| python -m pip install "cocotb~=2.1" numpy galois |
The option -m venv runs Python's venv module, and .venv is the conventional folder name, the leading dot hiding it from ordinary listings. On Arch Linux, whose python3 is already 3.14, the first line reads python3 -m venv .venv, and with the OSS CAD Suite on the search path it reads /usr/bin/python3 -m venv .venv.
Activation works through PATH, and Figure 3 shows its effect. Before activation, python3 on a Mac finds Homebrew's 3.15, which cocotb refuses. The command source .venv/bin/activate puts the environment's own bin folder at the top of the list, so the same name now finds the environment's 3.14 first, and the prompt gains a (.venv) prefix as a reminder. Inside the environment the plain names python and pip both belong to it.
Activation lasts for that one terminal. A new window starts deactivated, and deactivate undoes activation by hand. The environment cannot be moved or renamed, because its scripts record the full path where it was made, but it is disposable, so a broken or misplaced one is simply deleted and made again with the same three commands.
07.Reading Version Numbers
The --bench run compares every tool's version with the oldest the book needs, and comparing versions is easier to get wrong than it looks. A version such as Python 3.10 can be read in three ways, and only one of them orders releases correctly.
Read as a string of characters, "3.10" < "3.9" is true, because the comparison runs character by character and stops at the first difference, where 1 comes before 9. Read as a decimal number, 3.10 is the same number as 3.1, so 3.10 < 3.9 is true again. Both readings put Python 3.10 before Python 3.9, which is backwards. Read as a tuple of whole numbers, Python's name for a fixed sequence of values written in parentheses, (3, 10) < (3, 9) is false, because the second places compare as the integers 10 and 9. The tuple reading is the right one. The same trap catches Yosys, whose release 0.71 is far newer than 0.9 although the string "0.71" sorts first.
Tuples compare lexicographically, the way a dictionary orders words. The first places are compared, and the next place is consulted only when the places so far are equal.
Proof. If , there is a first index where they differ, and there exactly one of and holds, which settles the first claim. For the second, let be the first index where and differ and the first where and differ, so all three tuples agree before . If , then , so and and first differ at with . If , then . If , then . In every case .
The theorem needs tuples of equal length, and Python's rule for unequal ones holds a trap. A tuple that is a prefix of a longer one counts as smaller, so (3, 10) < (3, 10, 0) is true although both name the same release. The bench's version() function therefore pads every number to three places before comparing, which also explains how it prints Verilator 5.054, as 5.54.0, with the integer 54 in the middle place and a padded zero in the third. Python's own version, sys.version_info, is a tuple, so sys.version_info >= (3, 10) is the standard test a script makes.
A pin such as cocotb~=2.1 uses the compatible-release operator of PEP 440, which means at least 2.1 and still in the 2 series, written >= 2.1, == 2.* [15]. The last place written is the one allowed to grow, and Table 1 shows the difference one more place makes.
Table 1. Which versions two compatible-release pins accept
| Version | ~=2.1 | ~=2.1.0 |
|---|---|---|
| 2.0.0 | refused | refused |
| 2.1.0 | accepted | accepted |
| 2.1.5 | accepted | accepted |
| 2.2.0 | accepted | refused |
| 2.9.3 | accepted | refused |
| 3.0.0 | refused | refused |
08.Running the First Test
With the environment active, the chapter 1 script is the first check that the software half of the bench works.
| python python/ch01_why_codes_exist.py | |
| echo $? |
The script prints every value from Why Codes Exist beside the one the chapter states and ends with the line All chapter 1 numbers reproduced. The second command prints the exit status. A clean run prints 0, while a disagreement would have stopped the script with status 1, through a failed assert in this script and through a line beginning MISMATCH in the later ones, and scripts and build tools read that number to decide whether to carry on.
The second check asks the bench script to inspect the machine.
| python python/chf2_ecc_lab_bench.py --bench |
After the chapter's own numbers it prints a table like the one below, captured on a Mac with the full bench on 2026-11-14.
| The lab bench on this machine | |
| Python 3.14.10 ok | |
| git 2.56.1 no stated floor, the book used 2.56.1 | |
| Verilator 5.54.0 ok, at least 5.36.0 | |
| C++ compiler 21.0.0 found | |
| make 3.81.0 found | |
| Yosys 0.71.0 no stated floor, the book used 0.71 | |
| Surfer 0.7.0 no stated floor, the book used 0.7.0 | |
| Icarus Verilog 13.0.0 ok, at least 11.0.0 | |
| SymbiYosys 0.71.0 no stated floor, the book used 0.71 | |
| Z3 5.4.1 no stated floor, the book used 5.4.1 | |
| cocotb 2.1.0 ok | |
| numpy 2.5.4 ok | |
| galois 0.4.11 ok | |
| NumPy: (m @ G) % 2 [1, 0, 1, 1, 0, 1, 0] [1, 0, 1, 1, 0, 1, 0] | |
| galois: GF(100) + GF(29) 121 121 | |
| galois: GF(100) * GF(29) 224 224 | |
| galois: the field's polynomial x^8 + x^4 + x^3 + x^2 + 1 x^8 + x^4 + x^3 + x^2 + 1 | |
| Bench passes. |
Each row names a tool, the version it reported padded to three places, and a verdict. Where an official source states a floor, as cocotb's documentation does for Verilator and the script records for Icarus Verilog, the verdict compares against it. Where none does, the row records the version the book's own runs used. The last four lines repeat this chapter's NumPy and galois results through the real libraries. The bench fails on a Python outside 3.10 to 3.14 and on anything the companion scripts or the first hardware lab need that is missing or too old, while Icarus Verilog, SymbiYosys and Z3 are reported but never fail it, since a later Part is the first to call on them.
The third check is the hardware lab's testbench.
| python tb/parity/run_parity.py |
It compiles the parity checker of Lab: A First Hardware Checker with Verilator, runs it under cocotb in three builds, and prints a summary for each. The first ends in TESTS=3 PASS=3 FAIL=0 SKIP=0, and the other two pass their one and two tests. When measured on 2026-11-14 the three builds took about 6 seconds with their compiled programs already present and about 11 seconds on a first run, which compiles them. The second build records a waveform in a VCD file, short for value change dump, a text file that lists every change of every signal with its time [13], and Surfer opens it with the signals already arranged.
| surfer tb/parity/sim_build/k7-wave/dump.vcd --command-file tb/parity/parity_bench.sucl |
Figure 4 collects the whole sequence. When every green line in it appears, the bench is ready for the rest of the book.
09.Reading the Python in the Listings
The listings use a small part of Python, and this section teaches that part from examples, every result shown being one the bench script prints. A line such as bin(100) # '0b1100100' shows an expression and, after the # that starts a comment, the value Python gives for it.
Integers and Bits
Python writes whole numbers in decimal by default and in binary on request. The function bin() gives the binary digits with a 0b prefix, format() with the specification 08b pads them to eight places, and int() with base 2 reads them back. A 0x prefix writes a number in hexadecimal, base 16.
| bin(100) # '0b1100100' | |
| format(100, "08b") # '01100100' | |
| int("01100100", 2) # 100 | |
| 0x64 # 100 | |
| bin(100).count("1") # 3, the number of 1 bits |
Five operators act on the bits themselves. The operator & keeps a bit where both operands have one, | where either does, and ^, the XOR of Before You Start, where exactly one does. The shift >> moves the bits right, dropping the lowest, and << moves them left, filling with zeros. Figure 5 draws two of them.
| 100 ^ 0b10000000 # 228, the top bit of the byte flipped | |
| 100 >> 2 # 25 | |
| 100 << 1 # 200 | |
| 100 & 1 # 0, the lowest bit | |
| (100 >> 2) & 1 # 1, bit 2 | |
| 100 | 1 # 101 | |
| 100 ^ 29 # 121 | |
| 121 ^ 29 # 100, a second XOR with 29 undoes the first |
The idiom (x >> i) & 1, which reads bit of , appears in nearly every listing. Python integers also have no fixed width. An integer in C or SystemVerilog holds a set number of bits and wraps around when a result overflows, but a Python integer grows as needed, so 2**100 is the exact 31-digit number 1267650600228229401496703205376. That exactness is why the scripts count errors and codewords in integers.
Division with Remainder
Integer division is the arithmetic beneath shifts, cyclic codes and finite fields, and it rests on one theorem.
Proof. For existence, let , so . Multiplying by gives , so satisfies . For uniqueness, suppose with both remainders in . Then and , so , which forces and then .
Python's // gives the quotient and % the remainder, and the pair always satisfies a == (a // b) * b + a % b [16]. For positive operands nothing surprises, so 7 // 2 is 3 and 7 % 2 is 1. For a negative dividend the floor rounds down, toward minus infinity, so -7 // 2 is and -7 % 2 is 1, the remainder staying in as the theorem demands. When the divisor is negative the remainder takes the divisor's sign, so 7 // -2 is and 7 % -2 is . The trap is int(-7 / 2), which divides in floating point and then cuts toward zero, giving . The built-in divmod(17, 5) returns both results at once, (3, 2). The bench checks the theorem and both operators on 8,020 pairs.
Shifts as Division
The shift operators are division and multiplication by powers of 2 in disguise.
Proof. Write in binary as with each . Shifting right by places drops and moves every other digit down places, leaving . Then
so , and by the division theorem is the quotient . Shifting left by moves every digit up places, which multiplies each term , and so the whole sum, by .
So 13 >> 2 is and 100 >> 2 is 25. Python extends the rule to negative integers by definition, so -7 >> 1 is [16]. The bench checks both directions for every from to and every from 0 to 11, which is 24,588 cases.
Floating-Point Numbers
Python's float is a double, the 64-bit binary floating-point format of IEEE 754 [17]. A double holds a sign, a 53-bit whole number called the significand, and a power of two, so every positive double apart from the smallest, called subnormals, which all lie below , is with and an integer. Goldberg's survey remains the standard account of what follows from that design [18], and the first consequence is that most decimal fractions cannot be stored exactly.
Proof. If , then is the binary digits of with the point moved places left, which is finite. Conversely, suppose has a finite expansion with places after the point. Multiplying by moves the point past all of them, so for some integer , and . Since and share no factor, divides , and the only divisors of are powers of 2.
So is in binary, exactly, while is forever, and so is , since . The bench checks the theorem on 19,948 fractions with denominators up to 256. In binary, begins
and repeats the block 0011 forever. Cutting it after places leaves an error . The first places hold , so
For the last digit of cycles through 2, 4, 8 and 6, so lies between 2 and 8 and . The error does not fall at every place. It stays at 0.1 for the first three, and four places on from any it is exactly a sixteenth of what it was, since , which averages to halving with each place, and it never reaches zero. After eight places the truncation is and the error is 0.00234375. Figure 6 plots for up to 24.
A double stops at 53 significant bits. Since lies in , the double nearest it is with , and the best is the integer nearest to , which is . The stored value is therefore
which exceeds by exactly . Python shows all of this on request [19].
| from fractions import Fraction | |
| from decimal import Decimal | |
| Fraction(0.1) # Fraction(3602879701896397, 36028797018963968) | |
| Decimal(0.1) # Decimal('0.1000000000000000055511151231257827021181583404541015625') | |
| 0.1 # 0.1 |
A Fraction holds an exact ratio of integers, and Decimal writes out a double's exact decimal value. The prompt still shows 0.1 because repr, the function that turns a value into the text Python displays, picks the shortest decimal that reads back as the same double.
Rounding errors surface in arithmetic. The sum 0.1 + 0.2 gives 0.30000000000000004, and 0.1 + 0.2 == 0.3 is False. The exact values explain it. The sum's double is 0.3000000000000000444089209850062616169452667236328125, the double nearest 0.3 is 0.299999999999999988897769753748434595763683319091796875, and they differ by , the gap between neighboring doubles near 0.3, so the sum lands on the next double up. A computed float is therefore compared against a tolerance, as in abs(x - y) < 1e-9, or with the half-unit test below, never with ==.
The gap between neighbors follows a simple law.
Proof. A double with lies in , so every double in has , and the doubles there are exactly for the integers from to . Consecutive values of give consecutive doubles, so neighbors differ by . The largest, , is followed by , the same step again.
Python's math.ulp(x) returns that gap [20]. The call math.ulp(1.0) gives , math.ulp(0.1) gives and math.ulp(1000.0) gives . The gap is the same across each interval between powers of two and doubles at every power, the staircase of Figure 7, and the interval alone holds doubles. The bench checks the law on 121 intervals.
At the gap reaches 2, since math.ulp(2.0**53) is 2.0, so not every integer is a double any more. The integer lies exactly halfway between the doubles and , and the tie goes to , the one with the even significand, so float(2**53 + 1) == float(2**53) is True, and float(2**53) + 1.0 gives back float(2**53) for the same reason. A count that may pass therefore belongs in a Python integer, never a float.
Rounding Half to Even
When a value lies exactly halfway between two candidates, Python's round() picks the even one [21].
| round(2.5) # 2 | |
| round(3.5) # 4 | |
| round(-2.5) # -2 | |
| round(0.5) # 0 | |
| round(1.5) # 2 |
The rule removes a bias. The four ties 0.5, 1.5, 2.5 and 3.5 sum to 8. Rounded half to even they become 0, 2, 2 and 4, which still sum to 8, while rounding every tie up would give 1, 2, 3 and 4, which sum to 10. Over a long run of ties, always rounding up drifts upward and half to even does not. IEEE 754 rounds the same way by default [17], which is why became .
With a second argument, round() keeps that many decimal places. The call round(0.125, 2) gives 0.12 and round(0.375, 2) gives 0.38, each tie broken toward the even last digit, which works because and are stored exactly. The call round(2.675, 2) gives 2.67, which looks like a broken tie but is not one, because the stored double is 2.67499999999999982236431605997495353221893310546875, just below the halfway point. Formatting rounds the stored value the same way, so f"{2.675:.2f}" is '2.67' too.
The Half-Unit Test
Every companion script compares its computed values with the chapter's printed ones through a helper named check(), and the comparison it makes is the half-unit test. A stated value printed to decimal places has a last-place unit , and a computed value agrees with it when the two differ by at most . The helper half_unit() computes from the text as printed.
| def half_unit(stated: str) -> float: | |
| text = stated.replace(",", "").rstrip("%") | |
| mantissa, _, exponent = text.lower().partition("e") | |
| places = len(mantissa.partition(".")[2]) | |
| return 0.5 * 10.0 ** (int(exponent or 0) - places) |
It strips thousands separators and a percent sign, splits off any exponent, counts the digits after the point, and returns half a unit in that place. So "0.96584" gives 0.000005, "7,379" gives 0.5, "2.220e-16" gives and "96.58%" gives 0.005, a percentage being compared against 100 times the computed value. The test is exactly the right one.
Proof. The numbers with decimal places are the multiples of , and rounding to places picks a multiple of nearest to . Suppose . Any other multiple is at least from , so , and is a nearest multiple. Suppose instead that , say with . If , the distance from to is , and if it is , so either way is nearer than and is not a nearest multiple. The case is the mirror image.
The Lab: A First Hardware Checker reports that its checker flagged 7,379 of 7,640 damaged words. The ratio agrees with "0.96584", with "0.9658" and with "96.58%", but not with "0.9659", which it misses by 0.0000623, more than the half unit of 0.00005. The bench checks the theorem itself on 3,204 grid points.
Formatted Strings
The line check() prints is an f-string, a string literal with an f before its opening quote whose braces hold Python expressions.
| print(f" {label:<64} {float(computed):<16.9g} {stated}") |
Each pair of braces is replaced by the value of the expression inside it, formatted by whatever follows a colon. The specification <64 left-aligns the label in a field 64 characters wide, which is what lines up the bench's columns, and <16.9g left-aligns a number in 16 characters with 9 significant digits in general format, which switches to an exponent for very large or very small values. Other specifications in the listings follow the same pattern. The specification 08b writes an integer as 8 binary digits padded with zeros, and .5f writes a float with 5 places after the point, so f"{0.96583769633507:.5f}" is '0.96584'.
Lists, Tuples and Comprehensions
A list, written in square brackets, is a sequence that can be changed after it is made, and a tuple, written in parentheses, is one that cannot. Both are indexed from 0, so G[0] is the first row of a matrix stored as a list of lists. The bench keeps its tool table as a list of tuples and unpacks each tuple into named variables as it loops.
| for name, cmd, floor, measured, first in TOOLS: | |
| path = shutil.which(cmd[0]) |
A comprehension builds a list from a loop written inside the brackets, and the encoder in the bench script is two of them nested.
| def encode(m: list) -> list: | |
| """The row vector m times G, reduced mod 2: what (m @ G) % 2 computes in NumPy.""" | |
| return [sum(m[i] * G[i][j] for i in range(len(G))) % 2 for j in range(len(G[0]))] |
Read from the outside in, it makes one entry for each column of , and each entry is the sum over the rows of , reduced mod 2. Since range(n) counts from 0 to , range(len(G)) runs over the four rows and range(len(G[0])) over the seven columns. This is the matrix product of the next section written out by hand.
Functions and Docstrings
The keyword def defines a function, and the header of the field-reduction helper shows every part the listings use.
| def reduce(x: int, poly: int = 0x11D) -> int: | |
| """The remainder of x after division by the degree-8 polynomial poly, mod 2.""" |
The names x and poly are its parameters. The annotations : int and -> int record that both arguments and the result are integers, and = 0x11D gives poly a default, used when a call such as reduce(1172) leaves it out. The string in triple quotes on the first line of the body is the docstring, the function's own description, which help(reduce) displays.
Modules and the Main Guard
Every .py file is a module, and import runs another module and makes its names available. The chart script reuses the bench script's generator matrix rather than typing it twice.
| from chf2_ecc_lab_bench import G |
Importing runs the whole file, so a script that checks numbers when run would also check them when imported, unless it is guarded. The bench script ends with the two lines that guard it.
| if __name__ == "__main__": | |
| main() |
Python sets the name __name__ to "__main__" only in the file being run directly, so main() runs for python python/chf2_ecc_lab_bench.py and not for the import. Inside the checks, sys.exit(1) ends the program with exit status 1, the failure the shell sees, and the argparse module reads options such as --bench from the command line.
Coroutines in the Testbenches
The cocotb testbenches use two words the scripts do not, async and await, and a sample from tb/parity/test_parity.py shows both.
| async def apply(dut, data: int, mask: int, k: int) -> int: | |
| """Drive one vector, check all three outputs against the model, return the flag.""" | |
| dut.data.value = data | |
| dut.error_mask.value = mask | |
| await Timer(STEP_NS, unit="ns") | |
| codeword = model_codeword(data, k) |
The phrase async def defines a coroutine, a function that can pause partway and resume later. The line await Timer(STEP_NS, unit="ns") pauses this test until simulated time has advanced by that many nanoseconds, and during the pause the simulated circuit computes its outputs from the inputs just assigned. A test function carries the decorator @cocotb.test() on the line above it, which registers it with cocotb, and cocotb runs every registered test and reports each pass or fail. The name dut, short for device under test, is the simulated design, and its signals are read and driven as attributes such as dut.data. That is as much of cocotb as reading the listings needs.
10.Reading NumPy and galois
NumPy stores a vector or a matrix as an array and supplies arithmetic on whole arrays at once [8], so the encoder above becomes one line.
| import numpy as np | |
| m = np.array([1, 0, 1, 1]) | |
| (m @ np.array(G)) % 2 # array([1, 0, 1, 1, 0, 1, 0]) |
The operator @ is the matrix product. A row vector times a matrix adds up the rows the vector selects, and Figure 8 works it for the message 1011 and the generator matrix of a Hamming code,
The message keeps rows 0, 2 and 3, the ordinary column sums are , and % 2, applied to every entry of the array, turns them into the codeword 1011010. Reducing an ordinary sum mod 2 gives the same bit as XOR-ing the kept rows, by a short argument.
Proof. For two bits, the four cases give , and , which reduce mod 2 to , , and , the table of . For more bits, argue by induction on . With , writing shows that and differ by the even number , so they leave the same remainder mod 2. By the two-bit case and the induction hypothesis, that remainder is .
All 16 messages give 16 different codewords, and the smallest number of 1s in a nonzero one is 3, the property that lets this code correct any single error, as a later Part proves. The second message the bench tries, 1100, gives 1100011.
The galois library supplies finite fields as NumPy arrays [9]. The field of elements is the one many storage and communication codes compute in, and each of its elements is a byte.
| import galois | |
| GF = galois.GF(2**8) | |
| int(GF(100) + GF(29)) # 121 | |
| int(GF(100) * GF(29)) # 224 | |
| str(GF.irreducible_poly) # 'x^8 + x^4 + x^3 + x^2 + 1' |
Addition in this field is XOR of the bytes, so is . Multiplication treats each byte as a polynomial whose coefficients are its bits, so is and is . Multiplying them with coefficients added mod 2 is the carry-less product, the long multiplication of school with each column added by XOR instead of with carries, and it gives , against the ordinary product 2900. That result has eleven bits, too many for a byte, so it is reduced by the field's polynomial , written as the integer 0x11D, which is . Shifting it left two places lines its top bit up with the top bit of 1172, giving 1140, and , which fits in eight bits and is the product galois printed. The Part on Finite Fields and Their Arithmetic explains why this polynomial is chosen, why the result is a field, and how hardware performs the reduction.
11.Looking Ahead
With the bench installed and its first tests passing, the book's front matter is complete. Part I opens with Why Codes Exist, whose script was the first one run above, and it asks the question every later chapter answers in its own way, how a message can survive a channel that corrupts it. Each chapter from there names its script in its opening paragraph, and each script runs the same way, inside the activated environment, ending in a line that reports every number reproduced and an exit status of 0. The hardware labs add the testbench and synthesis commands of Lab: A First Hardware Checker, and the Parts that first need Icarus Verilog, SymbiYosys and Z3 say so where they begin. The Python taught here is enough to read every listing, and anything new a later listing reaches for is explained where it first appears.
12.Worked Examples
13.Exercises
References
- [1]Verilator Project “Installation.” Verilator 5.054 documentation. Read 2026-11-14. https://verilator.org/guide/latest/install.html
- [2]cocotb Project “Installation.” cocotb 2.1 documentation. Read 2026-11-14. https://docs.cocotb.org/en/stable/install.html
- [3]Surfer Project “Surfer.” Project README on GitLab. Read 2026-11-14. https://gitlab.com/surfer-project/surfer
- [4]YosysHQ “Installation.” Yosys documentation. Read 2026-11-14. https://yosyshq.readthedocs.io/projects/yosys/en/latest/getting_started/installation.html
- [5]YosysHQ “OSS CAD Suite.” GitHub repository. Read 2026-11-14. https://github.com/YosysHQ/oss-cad-suite-build
- [6]Homebrew Project “python@3.15.” Homebrew Formulae. Read 2026-11-14. https://formulae.brew.sh/formula/python@3.15
- [7]Homebrew Project “python@3.14.” Homebrew Formulae. Read 2026-11-14. https://formulae.brew.sh/formula/python@3.14
- [8]Python Package Index “numpy 2.5.4.” PyPI. Read 2026-11-14. https://pypi.org/project/numpy/
- [9]Python Package Index “galois 0.4.11.” PyPI. Read 2026-11-14. https://pypi.org/project/galois/
- [10]Python Package Index “numba 0.68.1.” PyPI. Read 2026-11-14. https://pypi.org/project/numba/
- [11]Python Packaging Authority “Installing pip/setuptools/wheel with Linux Package Managers.” Python Packaging User Guide. Read 2026-11-14. https://packaging.python.org/en/latest/guides/installing-using-linux-tools/
- [12]Homebrew Project “Language Runtimes and Packages.” Homebrew Documentation. Read 2026-11-14. https://docs.brew.sh/Language-Runtimes-and-Packages
- [13]cocotb Project “Simulator Support.” cocotb 2.1 documentation. Read 2026-11-14. https://docs.cocotb.org/en/stable/simulator_support.html
- [14]Python Software Foundation “venv: Creation of virtual environments.” The Python 3 documentation. Read 2026-11-14. https://docs.python.org/3/library/venv.html
- [15]Coghlan, Alyssa and Stufft, Donald (2013). “PEP 440: Version Identification and Dependency Specification.” Python Enhancement Proposals. Read 2026-11-14. https://peps.python.org/pep-0440/
- [16]Python Software Foundation “Expressions.” The Python 3 documentation. Read 2026-11-14. https://docs.python.org/3/reference/expressions.html
- [17]IEEE (2019). “IEEE Standard for Floating-Point Arithmetic.” IEEE Std 754-2019. doi:10.1109/IEEESTD.2019.8766229
- [18]Goldberg, David (1991). “What Every Computer Scientist Should Know About Floating-Point Arithmetic.” ACM Computing Surveys, 23(1), pp. 5–48. doi:10.1145/103162.103163
- [19]Python Software Foundation “Floating-Point Arithmetic: Issues and Limitations.” The Python 3 documentation. Read 2026-11-14. https://docs.python.org/3/tutorial/floatingpoint.html
- [20]Python Software Foundation “math: Mathematical functions.” The Python 3 documentation. Read 2026-11-14. https://docs.python.org/3/library/math.html
- [21]Python Software Foundation “Built-in Functions.” The Python 3 documentation. Read 2026-11-14. https://docs.python.org/3/library/functions.html
— Anirban
Hardware–software at the seam where compilers meet microcode.