Paper
Front matterBefore You Start

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.

The lab bench as inputs, tools and outputs. git copies the repository from GitHub to disk. Python with NumPy and galois turns each chapter script into the numbers the chapter prints. Verilator with cocotb turns the designs and testbenches into a pass or fail for every test, compiling with a C++ compiler and make. Surfer draws a waveform file as signals against time. Yosys turns a design and a synthesis script into gate counts and logic depth. Icarus Verilog, a second simulator, and SymbiYosys with the Z3 solver, which proves properties or returns a failing trace, are dashed because they arrive in later Parts. The git, Python and Verilator rows are outlined in green as the tools the first run in this chapter uses. WHAT GOES IN THE TOOL WHAT COMES OUT git Python, NumPy, galois Verilator and cocotb Surfer Yosys Icarus Verilog SymbiYosys and Z3 inside a virtual environment compiles with c++ and make python/chNN_*.py hdl/ tb/*/test_*.py tb/*/sim_build/*/dump.vcd hdl/ synth/*.py hdl/ the repository on GitHub a folder holding every listing every number the chapter prints PASS or FAIL for every test signals drawn against time gate counts and logic depth properties of a design a second simulator's verdict a proof, or a failing trace used by the first run in this chapter needed only from a later Part on
Figure 1. The lab bench as inputs, tools and outputs. git copies the repository from GitHub to disk. Python with NumPy and galois turns each chapter script into the numbers the chapter prints. Verilator with cocotb turns the designs and testbenches into a pass or fail for every test, compiling with a C++ compiler and make. Surfer draws a waveform file as signals against time. Yosys turns a design and a synthesis script into gate counts and logic depth. Icarus Verilog, a second simulator, and SymbiYosys with the Z3 solver, which proves properties or returns a failing trace, are dashed because they arrive in later Parts. The git, Python and Verilator rows are outlined in green as the tools the first run in this chapter uses.

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.

The three install routes. On macOS, the Command Line Tools come from xcode-select, then Homebrew installs Python 3.14 and git, then the hardware tools verilator, icarus-verilog, yosys, sby, z3 and surfer. On Linux, Arch Linux installs python, git, make, gcc, verilator, iverilog, yosys and z3 with pacman, and the OSS CAD Suite adds sby and Surfer, or supplies every hardware tool on any distribution. On Windows, WSL2 with ArchWSL is set up first and the Arch steps then run inside it, shown by an arrow into the Linux column. The macOS and Linux routes meet in the same last steps: the repository is cloned, a virtual environment on Python 3.10 to 3.14 is made inside it, pip installs cocotb, NumPy and galois into it, and the first test runs. The shared steps are outlined in green. macOS Linux Windows Command Line Tools Homebrew: Python and git Homebrew: hardware tools Arch Linux: pacman OSS CAD Suite WSL2 with ArchWSL Clone ecc-book-lab and step into the folder A virtual environment inside it, on Python 3.10 to 3.14 xcode-select --install brew install python@3.14 git verilator icarus-verilog yosys sby z3 surfer python git make gcc verilator iverilog yosys z3 sby and Surfer, or every tool set up as in Project: A Channel Simulator, then the Arch steps inside it python -m pip install "cocotb~=2.1" numpy galois then the repository runs its first test
Figure 2. The three install routes. On macOS, the Command Line Tools come from xcode-select, then Homebrew installs Python 3.14 and git, then the hardware tools verilator, icarus-verilog, yosys, sby, z3 and surfer. On Linux, Arch Linux installs python, git, make, gcc, verilator, iverilog, yosys and z3 with pacman, and the OSS CAD Suite adds sby and Surfer, or supplies every hardware tool on any distribution. On Windows, WSL2 with ArchWSL is set up first and the Arch steps then run inside it, shown by an arrow into the Linux column. The macOS and Linux routes meet in the same last steps. The repository is cloned, a virtual environment on Python 3.10 to 3.14 is made inside it, pip installs cocotb, NumPy and galois into it, and the first test runs. The shared steps are outlined in green.

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.

Plain Text
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.

Bash
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.

Bash
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.

Bash
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.

Bash
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.

Bash
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.

Bash
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.

Bash
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.

How the shell finds python3. It searches the folders listed in PATH from top to bottom and runs the first file named python3 it finds. Before activation the list starts with /opt/homebrew/bin, so python3 is Homebrew's default Python, 3.15, which cocotb 2.1 refuses, and /usr/bin and /bin are never searched. After source .venv/bin/activate, the folder ~/ecc-book-lab/.venv/bin is added at the top, so the same command runs the virtual environment's Python 3.14 with cocotb, NumPy and galois installed, and the three older entries are never reached. Before activation After activation typed: typed: python3 python3 PATH, searched from the top PATH, with one folder added at the top /opt/homebrew/bin ~/ecc-book-lab/.venv/bin /usr/bin /bin /opt/homebrew/bin /usr/bin /bin python3 found python3 found never searched never searched never searched never searched never searched Python 3.15: cocotb 2.1 refuses it Python 3.14 with cocotb, NumPy, galois runs /opt/homebrew/bin/python3 runs ~/ecc-book-lab/.venv/bin/python3
Figure 3. How the shell finds python3. It searches the folders listed in PATH from top to bottom and runs the first file named python3 it finds. Before activation the list starts with /opt/homebrew/bin, so python3 is Homebrew's default Python, 3.15, which cocotb 2.1 refuses, and /usr/bin and /bin are never searched. After source .venv/bin/activate, the folder ~/ecc-book-lab/.venv/bin is added at the top, so the same command runs the virtual environment's Python 3.14 with cocotb, NumPy and galois installed, and the three older entries are never reached.

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 a≠ba \neq b, there is a first index ii where they differ, and there exactly one of ai<bia_i < b_i and bi<aib_i < a_i holds, which settles the first claim. For the second, let ii be the first index where aa and bb differ and jj the first where bb and cc differ, so all three tuples agree before min⁡(i,j)\min(i, j). If i<ji < j, then bi=cib_i = c_i, so ai<bi=cia_i < b_i = c_i and aa and cc first differ at ii with ai<cia_i < c_i. If j<ij < i, then aj=bj<cja_j = b_j < c_j. If i=ji = j, then ai<bi<cia_i < b_i < c_i. In every case a<ca < c. ■\blacksquare

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.0refusedrefused
2.1.0acceptedaccepted
2.1.5acceptedaccepted
2.2.0acceptedrefused
2.9.3acceptedrefused
3.0.0refusedrefused

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.

Bash
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.

Bash
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.

Plain Text
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.

Bash
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.

Bash
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.

The first run, from top to bottom. git clone copies the repository and cd enters the folder ecc-book-lab, which holds python, hdl, tb and synth. python3.14 -m venv .venv makes a virtual environment and source .venv/bin/activate switches to it, so the prompt begins with (.venv). python -m pip install cocotb, NumPy and galois puts the three libraries inside .venv only. The chapter 1 script prints All chapter 1 numbers reproduced, and echo $? prints 0, the exit status for success. The bench script with --bench prints a version table and then Bench passes. The parity testbench prints TESTS=3 PASS=3 FAIL=0 SKIP=0 and then runs two more builds, in about 10 seconds. The lines a working bench prints are in green. TYPED IN THE TERMINAL WHAT IT PRINTS OR MAKES git clone https://github.com/anirbanchakraborty-dev/ecc-book-lab.git cd ecc-book-lab python3.14 -m venv .venv source .venv/bin/activate python -m pip install "cocotb~=2.1" numpy galois python python/ch01_why_codes_exist.py echo $? python python/chf2_ecc_lab_bench.py --bench python tb/parity/run_parity.py the folder ecc-book-lab, holding the prompt now begins with cocotb, NumPy and galois, installed inside .venv only python/ hdl/ tb/ synth/ (.venv) All chapter 1 numbers reproduced. 0 Bench passes. TESTS=3 PASS=3 FAIL=0 SKIP=0 the exit status, success after a table of versions then two more builds, about 10 s
Figure 4. The first run as a terminal session, from the clone to the parity testbench. Each command is followed by the lines it prints, and the lines a working bench prints are in green, from All chapter 1 numbers reproduced and an exit status of 0 to Bench passes and TESTS=3 PASS=3 FAIL=0 SKIP=0.

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.

Python
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.

Python
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 ii of xx, 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.

The byte 01100100 that holds 100, the byte 00011001 left after shifting it right by two places, which holds 25, and the byte 11100100 left after an XOR with 10000000 flips its top bit, which holds 228. Each column is labeled with its bit position and its weight. bit 7 128 bit 6 64 bit 5 32 bit 4 16 bit 3 8 bit 2 4 bit 1 2 bit 0 1 100 0 1 1 0 0 1 0 0 decimal 100 100 >> 2 0 0 0 1 1 0 0 1 decimal 25 100 ^ 0b10000000 1 1 1 0 0 1 0 0 decimal 228 Tinted cells hold a 1. The violet outline marks the one bit the XOR changed.
Figure 5. The byte 01100100 that holds 100, the byte 00011001 left after shifting it right by two places, which holds 25, and the byte 11100100 left after an XOR with 10000000 flips its top bit, which holds 228. Each column is labeled with its bit position and its weight. Data: computed by chf2_charts.py in the companion repository.

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 q=⌊a/b⌋q = \lfloor a/b \rfloor, so q≤a/b<q+1q \le a/b < q + 1. Multiplying by b>0b > 0 gives qb≤a<qb+bqb \le a < qb + b, so r=a−qbr = a - qb satisfies 0≤r<b0 \le r < b. For uniqueness, suppose a=qb+r=q′b+r′a = qb + r = q'b + r' with both remainders in [0,b)[0, b). Then (q−q′) b=r′−r(q - q')\,b = r' - r and ∣r′−r∣<b|r' - r| < b, so ∣q−q′∣ b<b|q - q'|\,b < b, which forces q=q′q = q' and then r=r′r = r'. ■\blacksquare

Python's // gives the quotient ⌊a/b⌋\lfloor a/b \rfloor 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 −4-4 and -7 % 2 is 1, the remainder staying in [0,2)[0, 2) as the theorem demands. When the divisor is negative the remainder takes the divisor's sign, so 7 // -2 is −4-4 and 7 % -2 is −1-1. The trap is int(-7 / 2), which divides in floating point and then cuts toward zero, giving −3-3. 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 nn in binary as n=∑i≥0bi2in = \sum_{i \ge 0} b_i 2^i with each bi∈{0,1}b_i \in \{0, 1\}. Shifting right by kk places drops b0,…,bk−1b_0, \dots, b_{k-1} and moves every other digit down kk places, leaving m=∑i≥kbi2i−km = \sum_{i \ge k} b_i 2^{i-k}. Then

n=m⋅2k+r,r=∑i<kbi2i≤2k−1,n = m \cdot 2^k + r, \qquad r = \sum_{i < k} b_i 2^i \le 2^k - 1,

so 0≤r<2k0 \le r < 2^k, and by the division theorem mm is the quotient ⌊n/2k⌋\lfloor n/2^k \rfloor. Shifting left by kk moves every digit up kk places, which multiplies each term bi2ib_i 2^i, and so the whole sum, by 2k2^k. ■\blacksquare

So 13 >> 2 is ⌊13/4⌋=3\lfloor 13/4 \rfloor = 3 and 100 >> 2 is 25. Python extends the rule to negative integers by definition, so -7 >> 1 is ⌊−7/2⌋=−4\lfloor -7/2 \rfloor = -4 [16]. The bench checks both directions for every nn from −1024-1024 to 10241024 and every kk 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 mm called the significand, and a power of two, so every positive double apart from the 252−12^{52} - 1 smallest, called subnormals, which all lie below 2−10222^{-1022}, is m⋅2fm \cdot 2^f with 252≤m<2532^{52} \le m < 2^{53} and ff 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 q=2kq = 2^k, then p/q=p⋅2−kp/q = p \cdot 2^{-k} is the binary digits of pp with the point moved kk places left, which is finite. Conversely, suppose p/qp/q has a finite expansion with kk places after the point. Multiplying by 2k2^k moves the point past all of them, so 2kp/q=N2^k p / q = N for some integer NN, and 2kp=Nq2^k p = Nq. Since pp and qq share no factor, qq divides 2k2^k, and the only divisors of 2k2^k are powers of 2. ■\blacksquare

So 3/83/8 is 0.0110.011 in binary, exactly, while 1/31/3 is 0.010101…0.010101\ldots forever, and so is 1/101/10, since 10=2⋅510 = 2 \cdot 5. The bench checks the theorem on 19,948 fractions with denominators up to 256. In binary, 1/101/10 begins

0.000110011001100110011001…0.000110011001100110011001\ldots

and repeats the block 0011 forever. Cutting it after kk places leaves an error eke_k. The first kk places hold ⌊2k/10⌋/2k\lfloor 2^k/10 \rfloor / 2^k, so

ek=110−⌊2k/10⌋2k=2k mod 1010⋅2k.e_k = \frac{1}{10} - \frac{\lfloor 2^k/10 \rfloor}{2^k} = \frac{2^k \bmod 10}{10 \cdot 2^k}.

For k≥1k \ge 1 the last digit of 2k2^k cycles through 2, 4, 8 and 6, so 2k mod 102^k \bmod 10 lies between 2 and 8 and 2−k/5≤ek≤0.8⋅2−k2^{-k}/5 \le e_k \le 0.8 \cdot 2^{-k}. The error does not fall at every place. It stays at 0.1 for the first three, and four places on from any kk it is exactly a sixteenth of what it was, since 2k+4 mod 10=2k mod 102^{k+4} \bmod 10 = 2^k \bmod 10, which averages to halving with each place, and it never reaches zero. After eight places the truncation is 25/256=0.0976562525/256 = 0.09765625 and the error is 0.00234375. Figure 6 plots eke_k for kk up to 24.

The error left when 1/10 is cut to its first k binary places, on a base-2 logarithmic scale, for k from 1 to 24. Every point lies below the dashed bound 2 to the minus k and no lower than a fifth of it, and none reaches zero, because 1/10 has no finite binary expansion. -28 -24 -20 -16 -12 -8 -4 0 4 8 12 16 20 24 binary places kept, k log2 of the error the bound 2^-k 1/10 minus its first k bits
Figure 6. The error left when 1/10 is cut to its first k binary places, on a base-2 logarithmic scale, for k from 1 to 24. Every point lies below the dashed bound 2 to the minus k and no lower than a fifth of it, and none reaches zero, because 1/10 has no finite binary expansion. Data: computed by chf2_charts.py in the companion repository.

A double stops at 53 significant bits. Since 0.10.1 lies in [2−4,2−3)[2^{-4}, 2^{-3}), the double nearest it is m⋅2−56m \cdot 2^{-56} with 252≤m<2532^{52} \le m < 2^{53}, and the best mm is the integer nearest to 256/10=7205759403792793.62^{56}/10 = 7205759403792793.6, which is m=7205759403792794m = 7205759403792794. The stored value is therefore

7205759403792794256=3602879701896397255,\frac{7205759403792794}{2^{56}} = \frac{3602879701896397}{2^{55}},

which exceeds 1/101/10 by exactly 1/(5⋅255)≈5.551×10−181/(5 \cdot 2^{55}) \approx 5.551 \times 10^{-18}. Python shows all of this on request [19].

Python
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 2−54≈5.551×10−172^{-54} \approx 5.551 \times 10^{-17}, 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 m⋅2fm \cdot 2^f with 252≤m<2532^{52} \le m < 2^{53} lies in [252+f,253+f)[2^{52+f}, 2^{53+f}), so every double in [2e,2e+1)[2^e, 2^{e+1}) has f=e−52f = e - 52, and the doubles there are exactly m⋅2e−52m \cdot 2^{e-52} for the integers mm from 2522^{52} to 253−12^{53} - 1. Consecutive values of mm give consecutive doubles, so neighbors differ by 2e−522^{e-52}. The largest, (253−1) 2e−52(2^{53} - 1)\,2^{e-52}, is followed by 253⋅2e−52=2e+12^{53} \cdot 2^{e-52} = 2^{e+1}, the same step again. ■\blacksquare

Python's math.ulp(x) returns that gap [20]. The call math.ulp(1.0) gives 2−52≈2.220×10−162^{-52} \approx 2.220 \times 10^{-16}, math.ulp(0.1) gives 2−56≈1.388×10−172^{-56} \approx 1.388 \times 10^{-17} and math.ulp(1000.0) gives 2−43≈1.137×10−132^{-43} \approx 1.137 \times 10^{-13}. 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 [1,2)[1, 2) alone holds 252=45035996273704962^{52} = 4503599627370496 doubles. The bench checks the law on 121 intervals.

The gap between consecutive double-precision numbers, plotted as log2 of the gap against log2 of x for x from 2 to the minus 6 up to 2 to the 12. The gap is constant across each interval from one power of two to the next and doubles at every power of two. The marked points are 0.1 with a gap of 2 to the minus 56, 1.0 with 2 to the minus 52, and 1000.0 with 2 to the minus 43. -60 -56 -52 -48 -44 -40 -6 -4 -2 0 2 4 6 8 10 12 log2 of x log2 of the spacing at x ulp(0.1) = 2^-56 ulp(1.0) = 2^-52 ulp(1000.0) = 2^-43
Figure 7. The gap between consecutive double-precision numbers, plotted as log2 of the gap against log2 of x for x from 2 to the minus 6 up to 2 to the 12. The gap is constant across each interval from one power of two to the next and doubles at every power of two. The marked points are 0.1 with a gap of 2 to the minus 56, 1.0 with 2 to the minus 52, and 1000.0 with 2 to the minus 43. Data: computed by chf2_charts.py in the companion repository.

At 253=90071992547409922^{53} = 9007199254740992 the gap reaches 2, since math.ulp(2.0**53) is 2.0, so not every integer is a double any more. The integer 253+12^{53} + 1 lies exactly halfway between the doubles 2532^{53} and 253+22^{53} + 2, and the tie goes to 2532^{53}, 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 2532^{53} 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].

Python
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 253+12^{53} + 1 became 2532^{53}.

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 0.125=1/80.125 = 1/8 and 0.375=3/80.375 = 3/8 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 dd decimal places has a last-place unit u=10−du = 10^{-d}, and a computed value agrees with it when the two differ by at most u/2u/2. The helper half_unit() computes u/2u/2 from the text as printed.

Python
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 5×10−205 \times 10^{-20} 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 dd decimal places are the multiples of uu, and rounding xx to dd places picks a multiple of uu nearest to xx. Suppose ∣x−s∣≤u/2|x - s| \le u/2. Any other multiple tt is at least uu from ss, so ∣x−t∣≥∣t−s∣−∣x−s∣≥u−u/2≥∣x−s∣|x - t| \ge |t - s| - |x - s| \ge u - u/2 \ge |x - s|, and ss is a nearest multiple. Suppose instead that ∣x−s∣>u/2|x - s| > u/2, say with x>s+u/2x > s + u/2. If x≤s+ux \le s + u, the distance from xx to s+us + u is s+u−x<u/2<x−ss + u - x < u/2 < x - s, and if x>s+ux > s + u it is x−s−u<x−sx - s - u < x - s, so either way s+us + u is nearer than ss and ss is not a nearest multiple. The case x<s−u/2x < s - u/2 is the mirror image. ■\blacksquare

The Lab: A First Hardware Checker reports that its checker flagged 7,379 of 7,640 damaged words. The ratio 7379/7640=0.965837…7379/7640 = 0.965837\ldots 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.

Python
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.

Python
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.

Python
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 jj of GG, and each entry is the sum over the rows ii of miGijm_i G_{ij}, reduced mod 2. Since range(n) counts from 0 to n−1n - 1, 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.

Python
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.

Python
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.

Python
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.

Python
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.

Python
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,

G=(1000110010010100100110001111).G = \begin{pmatrix} 1&0&0&0&1&1&0 \\ 0&1&0&0&1&0&1 \\ 0&0&1&0&0&1&1 \\ 0&0&0&1&1&1&1 \end{pmatrix}.

The message keeps rows 0, 2 and 3, the ordinary column sums are 1,0,1,1,2,3,21, 0, 1, 1, 2, 3, 2, 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 0+0=00 + 0 = 0, 0+1=1+0=10 + 1 = 1 + 0 = 1 and 1+1=21 + 1 = 2, which reduce mod 2 to 00, 11, 11 and 00, the table of a⊕ba \oplus b. For more bits, argue by induction on nn. With s=a1+⋯+an−1s = a_1 + \cdots + a_{n-1}, writing s=2q+(s mod 2)s = 2q + (s \bmod 2) shows that s+ans + a_n and (s mod 2)+an(s \bmod 2) + a_n differ by the even number 2q2q, so they leave the same remainder mod 2. By the two-bit case and the induction hypothesis, that remainder is (a1⊕⋯⊕an−1)⊕an(a_1 \oplus \cdots \oplus a_{n-1}) \oplus a_n. ■\blacksquare

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 product of the message 1011 with the four-row generator matrix G of a Hamming code. The message keeps rows 0, 2 and 3 and drops row 1, the kept rows are added column by column to give 1 0 1 1 2 3 2, and reducing each sum mod 2 gives the codeword 1011010. c0 c1 c2 c3 c4 c5 c6 m0 = 1 row 0 kept 1 0 0 0 1 1 0 m1 = 0 row 1 dropped 0 1 0 0 1 0 1 m2 = 1 row 2 kept 0 0 1 0 0 1 1 m3 = 1 row 3 kept 0 0 0 1 1 1 1 column sums sums mod 2 1 1 0 0 1 1 1 1 2 0 3 1 2 0 c = 1011010
Figure 8. The product of the message 1011 with the four-row generator matrix G of a Hamming code. The message keeps rows 0, 2 and 3 and drops row 1, the kept rows are added column by column to give 1 0 1 1 2 3 2, and reducing each sum mod 2 gives the codeword 1011010. Data: computed by chf2_charts.py in the companion repository.

The galois library supplies finite fields as NumPy arrays [9]. The field of 28=2562^8 = 256 elements is the one many storage and communication codes compute in, and each of its elements is a byte.

Python
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 100+29100 + 29 is 100⊕29=121100 \oplus 29 = 121. Multiplication treats each byte as a polynomial whose coefficients are its bits, so 100=11001002100 = 1100100_2 is x6+x5+x2x^6 + x^5 + x^2 and 29=11101229 = 11101_2 is x4+x3+x2+1x^4 + x^3 + x^2 + 1. 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 1172=1001001010021172 = 10010010100_2, against the ordinary product 2900. That result has eleven bits, too many for a byte, so it is reduced by the field's polynomial x8+x4+x3+x2+1x^8 + x^4 + x^3 + x^2 + 1, written as the integer 0x11D, which is 1000111012100011101_2. Shifting it left two places lines its top bit up with the top bit of 1172, giving 1140, and 1172⊕1140=2241172 \oplus 1140 = 224, 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. [1]Verilator Project “Installation.” Verilator 5.054 documentation. Read 2026-11-14. https://verilator.org/guide/latest/install.html
  2. [2]cocotb Project “Installation.” cocotb 2.1 documentation. Read 2026-11-14. https://docs.cocotb.org/en/stable/install.html
  3. [3]Surfer Project “Surfer.” Project README on GitLab. Read 2026-11-14. https://gitlab.com/surfer-project/surfer
  4. [4]YosysHQ “Installation.” Yosys documentation. Read 2026-11-14. https://yosyshq.readthedocs.io/projects/yosys/en/latest/getting_started/installation.html
  5. [5]YosysHQ “OSS CAD Suite.” GitHub repository. Read 2026-11-14. https://github.com/YosysHQ/oss-cad-suite-build
  6. [6]Homebrew Project “python@3.15.” Homebrew Formulae. Read 2026-11-14. https://formulae.brew.sh/formula/python@3.15
  7. [7]Homebrew Project “python@3.14.” Homebrew Formulae. Read 2026-11-14. https://formulae.brew.sh/formula/python@3.14
  8. [8]Python Package Index “numpy 2.5.4.” PyPI. Read 2026-11-14. https://pypi.org/project/numpy/
  9. [9]Python Package Index “galois 0.4.11.” PyPI. Read 2026-11-14. https://pypi.org/project/galois/
  10. [10]Python Package Index “numba 0.68.1.” PyPI. Read 2026-11-14. https://pypi.org/project/numba/
  11. [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. [12]Homebrew Project “Language Runtimes and Packages.” Homebrew Documentation. Read 2026-11-14. https://docs.brew.sh/Language-Runtimes-and-Packages
  13. [13]cocotb Project “Simulator Support.” cocotb 2.1 documentation. Read 2026-11-14. https://docs.cocotb.org/en/stable/simulator_support.html
  14. [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. [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. [16]Python Software Foundation “Expressions.” The Python 3 documentation. Read 2026-11-14. https://docs.python.org/3/reference/expressions.html
  17. [17]IEEE (2019). “IEEE Standard for Floating-Point Arithmetic.” IEEE Std 754-2019. doi:10.1109/IEEESTD.2019.8766229
  18. [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. [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. [20]Python Software Foundation “math: Mathematical functions.” The Python 3 documentation. Read 2026-11-14. https://docs.python.org/3/library/math.html
  21. [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.

FeedbackBook modePaper
error-correction-codingbefore-you-start