Project: A Channel Simulator
November 14, 202647 min readbeginner
A seeded channel simulator in Python for the binary symmetric, erasure and Gaussian channels, with Box–Muller noise, repetition and parity codes, Wilson intervals and a check of every measured error rate against its closed form.
Error-Control Strategies and Performance measured the word error rate of the three-fold repetition code on the binary symmetric channel by simulation, counting 266 failures in 10,000 words against an exact rate of 0.028. Case Study: Codes in the Wild then quoted error rates from standards and products on trust. This chapter builds a simulator that produces numbers like these and asks what every simulation owes its reader, a reason to believe what it prints.
The simulator drives three channels, the binary symmetric channel, the binary erasure channel and BPSK over Gaussian noise, from nothing but seeded uniform random numbers, and it makes the Gaussian noise by the Box–Muller transform. Repetition codes with hard and soft decoding and the single parity code ride on those channels. Counted failures become an estimate, and the Wald and Wilson intervals, the rule of three and a formula for the number of trials turn the estimate into a claim with a stated chance of being wrong.
Every code here also has an exact error rate, so the simulator is checked against seven closed forms. Six of the seven 95% intervals contain the exact value and one does not. The chapter explains the miss, since a 95% interval that never missed would be a broken interval.
The simulator is ch11_project_channel_simulator.py in the python/ folder of the companion repository ecc-book-lab. Run as a script, it repeats every seeded experiment in the chapter, prints each value it computes beside the value stated here, and stops at the first disagreement. The four charts are drawn by ch11_charts.py. Both use only the Python standard library.
01.Setting Up
The simulator needs Python 3.10 or newer and git, and nothing else, no Python packages, no compiler and no Docker, since everything the scripts import ships with Python. Each step below follows the official documentation of the tool it installs and prefers the platform's own package manager. Commands are typed into a terminal, the window that accepts typed commands, which is the Terminal app on macOS, any terminal emulator on Linux and PowerShell on Windows.
macOS
Homebrew, the package manager most macOS developers use, is installed by the one-line command on its home page, brew.sh, which is best copied from there since the project changes it from time to time. Python and git are then one command each, and the last two lines confirm that both work.
| brew install python3 | |
| brew install git | |
| python3 --version | |
| git --version |
Homebrew's documentation notes that the unversioned python command of its Python package is not on the default search path, so this chapter always types python3. Git's download page also names Apple's Xcode Command Line Tools, which ship their own git, as another route.
Linux
The Python documentation says that Python comes preinstalled on most Linux distributions and is packaged on all others, and it gives no install command. Check with python3 --version. If the version is older than 3.10, install the distribution's Python package as that distribution's documentation describes, since no official Python source covers this step. Git's download page gives one command per distribution, each run with administrator rights, which sudo provides.
| sudo apt-get install git # Debian and Ubuntu | |
| sudo dnf install git # Fedora | |
| sudo pacman -S git # Arch Linux | |
| sudo zypper install git # openSUSE |
Windows
The Python documentation installs Python on Windows through the Python install manager, published in the Microsoft Store and fetched with WinGet, the package manager built into current Windows. The second line installs a Python version, and any version from 3.10 on serves. Git's download page gives the WinGet command for git.
| winget install 9NQ7512CXL7T -e --accept-package-agreements --disable-interactivity | |
| py install 3.13 | |
| py --version | |
| winget install --id Git.Git -e --source winget | |
| git --version |
Python then runs as py or as python. The other route is Linux inside Windows, through the Windows Subsystem for Linux, version 2, or WSL2. Microsoft's documentation requires Windows 10 version 2004 (build 19041) or later, or Windows 11, and installs WSL2 with one command in a PowerShell window opened as administrator, followed by a restart.
| wsl --install |
That installs Ubuntu, where the Linux steps apply unchanged. wsl --list --online lists the other distributions, wsl --install -d <Distro> installs one by name, wsl --set-default-version 2 makes WSL2 the version for every distribution installed afterward, and wsl --set-version <Distro> 2 converts a distribution already installed under the first version of WSL.
ArchWSL, a community project, packages Arch Linux for WSL. Its documentation installs it by extracting the release zip from the project's GitHub page into a folder the user can write to and running Arch.exe. The first session runs as root, the administrator account. It sets a root password, lets members of the wheel group use sudo, creates an everyday user in that group and exits, with yourname standing for the chosen name.
| passwd | |
| echo "%wheel ALL=(ALL) ALL" > /etc/sudoers.d/wheel | |
| useradd -m -G wheel -s /bin/bash yourname | |
| passwd yourname | |
| exit |
Back in PowerShell, in the folder that holds Arch.exe, one command makes that user the default. The .\ prefix is how PowerShell runs a program from the current folder.
| .\Arch.exe config --default-user yourname |
The documentation then initializes the keys that sign Arch's packages and updates the system, after which pacman, Arch's package manager, installs Python and git.
| sudo pacman-key --init | |
| sudo pacman-key --populate | |
| sudo pacman -Sy archlinux-keyring | |
| sudo pacman -Su | |
| sudo pacman -S python git |
Cloning the Repository
The same three commands then fetch the companion repository and run the simulator on every platform.
| git clone https://github.com/anirbanchakraborty-dev/ecc-book-lab.git | |
| cd ecc-book-lab | |
| python3 python/ch11_project_channel_simulator.py |
In native Windows PowerShell the last line is py python\ch11_project_channel_simulator.py. The run takes well under a minute, prints each computed value beside the value stated here, and ends with All chapter 11 numbers reproduced. A disagreement prints a line starting MISMATCH and exits with a nonzero status, a program's way of telling the shell that it failed. The command python3 python/ch11_charts.py --out figures redraws the four charts as SVG files in a new folder named figures.
02.Why Simulate When a Formula Exists
Every error rate in this chapter has an exact formula, so why simulate at all? The first reason is that formulas run out. Repetition and single parity have short exact answers because their decoders fail in few enough ways to list, but the iterative decoders of the later Parts have no formula at the lengths and noise levels that matter. The second is that a formula describes the decoder on paper, not the code someone wrote. A simulator running the real encoder and decoder catches the off-by-one count and the swapped sign that a derivation never sees. The third is calibration. A simulator that will be trusted where no formula exists must first agree with every formula it can be checked against, and that is this chapter's job, which is why its codes are simple enough for every simulated number to have an exact partner.
Figure 1 shows the loop. A source draws random data bits, an encoder adds redundancy, a channel corrupts the codeword and a decoder makes its best guess. Each wrong guess adds one to the failure count , and after words the estimate is .
The simulator keeps two generators. The channel generator starts from the seed and supplies every draw the channel uses, and the data generator starts from the seed plus and supplies the data bits. Because the streams never mix, the channel sees the same draws whatever data is sent and whatever decoder is listening. Two decoders run with one seed therefore face identical noise, a technique called common random numbers, and the difference in their failure counts measures the decoders rather than luck. The worked example Hard and Soft Decoders Word by Word uses it.
03.Pseudorandom Numbers and Seeds
A computer following instructions cannot be random, but it can run a fixed recipe whose output passes the statistical tests that matter, and such a recipe is called a pseudorandom number generator. Python's is the Mersenne Twister of Matsumoto and Nishimura [1], whose sequence repeats only after steps, a number with more than 6,000 digits.
| import random | |
| rng = random.Random(17) | |
| print(rng.random()) # 0.5219839097124932 | |
| print(rng.random()) # 0.8066907771186791 |
The call random.Random(17) builds a private generator started from the seed 17, and each call of .random() returns the next number of its sequence, a value in . The same seed gives the same sequence of uniforms every time the script runs. A colleague who runs the same script with the same seed and trial count therefore gets the same failure count in practice, within limits set out after the definition below, and a disagreement points at a change in the code rather than at chance.
The simulator calls only .random(), and the restriction is deliberate. The Python documentation guarantees that random() reproduces the same sequence for the same seed across Python versions, and it makes no such guarantee for the methods built on top of it, such as randint, gauss and choice. Every random quantity in the chapter is therefore built from .random() by the simulator's own code. The guarantee covers the uniforms and nothing computed from them. The counts on the discrete channels compare uniforms with fixed thresholds, so in practice they reproduce everywhere. The Gaussian counts pass the uniforms through log, cos and sin from the platform's math library and scale them by a sigma computed with pow, so they reproduce everywhere too, unless two libraries round one of those functions differently in the last bit and a sample lands on the other side of a decision. The values of .random() are whole multiples of , so to within , and a comparison rng.random() < p is true with probability to within . The Mersenne Twister is unfit for cryptography, since enough of its output predicts the rest, but a simulation needs statistical quality rather than secrecy.
04.Building the Channels
Each discrete channel turns one uniform draw into one decision about one bit, and Figure 2 shows the rule. The binary symmetric channel of Three Discrete Channels flips a bit when its draw falls below , which happens with probability . The binary erasure channel replaces a bit by a mark meaning "unknown" when its draw falls below .
| def bsc(word, p, rng): | |
| return [b ^ int(rng.random() < p) for b in word] | |
| def bec(word, eps, rng): | |
| return [None if rng.random() < eps else b for b in word] |
In bsc the comparison gives True or False, int turns that into 1 or 0, and ^, exclusive or, flips the bit exactly when the value is 1. In bec, Python's None plays the erasure mark. Each bit consumes one draw whether or not anything happens to it, so two runs with the same seed stay aligned draw for draw.
The Gaussian channel of The Gaussian Channel and BPSK sends bit 0 as the voltage and bit 1 as and adds independent Gaussian noise of mean 0 and variance to each. Energy per Bit and the Decibel tied to the energy per information bit. With unit energy per transmitted symbol and code rate , each information bit carries energy , the noise variance is , and so
A value in decibels becomes a ratio through , so 4 dB is a ratio of 2.5119. Uncoded BPSK has and . The rate- code splits each bit's energy over three pulses, so the noise on each is relatively larger, . Comparing codes at equal charges each code for the energy its redundancy spends, the fair comparison of Coding Gain.
| def awgn_sigma(ebn0_db, rate): | |
| ebn0 = 10 ** (ebn0_db / 10) | |
| return sqrt(1.0 / (2.0 * rate * ebn0)) | |
| def bpsk_awgn(word, sigma, noise): | |
| return [(1.0 - 2.0 * b) + sigma * noise.next() for b in word] |
The expression 1.0 - 2.0 * b maps bit 0 to and bit 1 to , and noise.next() supplies one standard normal sample, with mean 0 and variance 1, which sigma scales. The uniform generator cannot supply that sample directly, and the next section builds it.
05.Gaussian Samples by Box–Muller
The Box–Muller transform [2] makes two Gaussian samples out of two uniform numbers. Take and , and compute a radius and an angle . Here , so , and radians, which is . The point at distance from the origin in direction has coordinates and , and those two coordinates are the samples. Figure 3 draws the step.
Why bell-curve numbers? The answer lies in how two independent standard normals look together. Take and independent, each with the density of The Gaussian and the Q-Function. The density of the pair is the product,
where is the distance of from the origin. The density depends on that distance alone, so a cloud of such points looks the same from every direction, and the theorem turns that symmetry into a recipe.
Proof. Describe by its distance from the origin and its angle , and show that the transform builds a distance and an angle that behave the same way.
The patch of the plane between radii and and between angles and is nearly a rectangle with sides and , since an arc of angle at radius has length . The chance that lands in it is the density times the area, . Adding up the patches, the chance that the point lies within distance and at an angle below is
since the inner integral is and the outer one multiplies it by .
The result is a function of alone times a function of alone. Letting grow without bound gives , so the angle is uniform on . Setting gives . The joint probability is the product of the two, which is what independence of and means, and in terms of the squared distance for every .
Now compare the transform. Its angle is uniform on . Its squared radius exceeds exactly when , which has probability , the law of . The two are independent because and are. A point is fixed by its radius and angle, so behaves exactly like .
One consequence is that the squared radius exceeds 2 with probability , so about 37% of Box–Muller points land outside the circle of radius . The simulator's version keeps one sample in reserve, since each pair of draws gives two.
| class GaussianSource: | |
| def __init__(self, rng): | |
| self.rng = rng | |
| self.spare = None | |
| def next(self): | |
| if self.spare is not None: | |
| z, self.spare = self.spare, None | |
| return z | |
| u1 = 1.0 - self.rng.random() # in (0, 1], so log(u1) is finite | |
| u2 = self.rng.random() # in [0, 1) | |
| r = sqrt(-2.0 * log(u1)) | |
| theta = 2.0 * pi * u2 | |
| self.spare = r * sin(theta) | |
| return r * cos(theta) |
The class hands out the cosine sample at once and keeps the sine sample for the next call. The line that sets u1 subtracts the draw from 1 because random() can return exactly 0.0, and does not exist. Subtracting moves the range from to without changing the uniform distribution.
With seed 17 the first two draws are 0.52198 and 0.80669, so , , and radians. Then and , so the first two samples are and . Over 100,000 samples from the same seed the mean is 0.0044 and the variance 0.9981, against 0 and 1 for a true standard normal. The mean of 100,000 standard normal samples has a standard deviation, or standard error, of , so 0.0044 lies about 1.4 standard errors from 0, well within the spread chance allows. Of the same samples, 5,014 exceed 1.96 in size against of them expected, and the worked example Both Intervals for a Tail Fraction shows that the two agree. Figure 4 lays the histogram over the bell curve.
Soft decoding will need one more fact, that suitably weighted sums of Gaussians are Gaussian again.
Proof. Choose the angle with and , which exists because lies on the unit circle. Rotating by about the origin gives . A rotation changes no distance from the origin and no area, and the density Equation 3 depends on the distance alone, so the rotated point has the same density as , and its first coordinate has the same distribution as .
A corollary follows by induction. The sum of independent standard normals has the distribution of , with standard normal. It holds for , since . If it holds for , then behaves like , whose two weights have squares adding to 1, so by the lemma the bracket is standard normal.
06.Encoders and Decoders
The repetition code sends each data bit times, and on the binary symmetric channel its decoder takes a majority vote. On the Gaussian channel there are the two ways of Hard and Soft Decisions. A hard decoder first turns each received voltage into a bit, 1 if negative and 0 otherwise, and then votes. A soft decoder adds the voltages and decides on the sign of the sum.
The two can disagree. Suppose receives , and . The hard decoder sees 0, 1 and 0 and votes for 0. The soft decoder adds the voltages to and decides 1, weighing the strong above two weak positives, and Likelihoods and Log-Likelihood Ratios explains why that is right. The log-likelihood ratio of a bit given a received value on this channel is , positive values favoring 0, and independent evidence about one bit adds, so the total evidence from three pulses is
The factor is positive, so the sign of is the sign of the plain sum. The hard decoder throws away the size of each pulse before it votes, and that is where its loss comes from.
| def repetition_encode(bit, n): | |
| return [bit] * n | |
| def majority(word): | |
| return int(sum(word) > len(word) // 2) | |
| def hard_repetition(received): | |
| return majority([int(y < 0) for y in received]) | |
| def soft_repetition(received): | |
| return int(sum(received) < 0) |
On the erasure channel a bit that arrives is certainly correct, since this channel never flips anything. A repetition decoder takes any surviving copy and fails only when all copies are erased, with probability .
The single parity code of Single Parity appends one bit so that the number of 1s in the codeword is even. Figure 5 follows the word 10110010, the data 1011001 with parity bit 0. One flip leaves an odd count and is detected, while two flips restore an even count and pass unseen. On the erasure channel the same parity bit does better than detect.
Proof. The bits of a codeword add to 0 modulo 2. Write the erased bit as and the sum of the others as . Then , and since adding 1 and subtracting 1 agree modulo 2, . With two erased bits and , the one equation is met both by and by , two codewords the arrived bits cannot tell apart.
In the erased word 1?110010 the seven known bits hold four 1s, so the erased bit is 0. This is the smallest case of the point of Errors and Erasures Together. A code of minimum distance 2 detects one error but fills one erasure, because an erasure announces where the damage is.
| def parity_encode(data): | |
| return data + [sum(data) % 2] | |
| def parity_ok(word): | |
| return sum(word) % 2 == 0 | |
| def parity_fill(received): | |
| missing = [i for i, b in enumerate(received) if b is None] | |
| if len(missing) > 1: | |
| return None | |
| word = list(received) | |
| if missing: | |
| word[missing[0]] = sum(b for b in received if b is not None) % 2 | |
| return word |
Each trial sends one word and reports whether the decoder failed, and the loop around the trials is short.
| def count_failures(trials, seed, trial, gaussian=False): | |
| source = random.Random(seed + 10**9) | |
| channel = random.Random(seed) | |
| noise = GaussianSource(channel) if gaussian else channel | |
| failures = 0 | |
| for _ in range(trials): | |
| failures += trial(source, noise) | |
| return failures |
Each trial draws data bits from source, encodes them, passes the word through a channel driven by channel or noise, decodes, and returns True on a failure, which Python adds as 1. For single parity on the erasure channel the trial also raises an error if a filled word ever differs from the word sent, so the lemma is checked on every word of that run.
07.The Monte Carlo Estimator
A run reports failures in words and estimates the error rate under study , here a word error rate, by . Another seed gives another . The coverage experiment of the next section sends over , where , in 1,000 runs of 1,000 words with seeds 1 to 1,000, and the failure counts range from 13 to 47. A single estimate means little until its spread is known.
Measuring Error Rates by Simulation supplies that spread. When the trials are independent and each fails with probability , the estimate is unbiased, , so it is right on average, and its variance is , so its standard deviation shrinks as . The seeded runs bear this out. At the predicted standard deviation is , and the 1,000 seeded estimates have mean 0.0280 and standard deviation 0.00520.
08.Intervals That Miss
The variance gives the spread of in terms of , the one thing a simulation does not know. A confidence interval turns this around. It is a rule that takes and and returns a range, built so that over many repeated experiments the range contains the true a stated fraction of the time, 95% here.
Measuring Error Rates by Simulation also built the two rules this chapter uses, and a short recap is enough here. The Wald interval, with , puts the estimate in place of the unknown rate inside the standard deviation. Wilson's interval [3] keeps the unknown rate there and collects every with . Squaring that condition at its two ends gives the quadratic with , and , and its roots are the ends of the interval.
The simulator here repeats that section's run, on with seed 2026 and 10,000 words, and counts the same 266 failures. The Wald interval runs from 0.02345 to 0.02975 and the Wilson interval from 0.02362 to 0.02994. Both contain 0.028, and at 266 failures they nearly agree. The difference shows when failures are few.
| def wald(k, n, z=1.96): | |
| ph = k / n | |
| half = z * sqrt(ph * (1 - ph) / n) | |
| return ph - half, ph + half | |
| def wilson(k, n, z=1.96): | |
| ph = k / n | |
| center = ph + z * z / (2 * n) | |
| half = z * sqrt(ph * (1 - ph) / n + z * z / (4 * n * n)) | |
| scale = 1 + z * z / n | |
| return (center - half) / scale, (center + half) / scale |
Here the truth is known, so coverage can be measured. The coverage experiment repeats the run with seeds 1 to 1,000, first at 1,000 words per run and then at 100, and counts the intervals of each kind that contain 0.028. Table 1 gives the result.
Table 1. Coverage of 95% intervals for the word error rate of on , exact value 0.028, over 1,000 seeded runs with seeds 1 to 1,000
| Words per run | Wilson intervals containing 0.028 | Wald intervals containing 0.028 |
|---|---|---|
| 1,000 | 957 | 947 |
| 100 | 981 | 950 |
Source: reproduced by ch11_project_channel_simulator.py.
At 1,000 words per run both rules come close to the expected 950. Figure 6 draws the first 50 Wilson intervals, and two miss against 2.5 expected. Run 20 counted 45 failures, and its interval, from 0.0338 to 0.0597, lies wholly above 0.028. Run 50 counted 16, and its interval, from 0.0099 to 0.0258, lies wholly below. Neither run is broken. Each drew an unusually harsh or gentle stretch of noise, the kind a 95% rule is built to be fooled by about once in twenty runs.
At 100 words per run the rules part ways, and the reasons explain every miss. Only about 2.8 failures are expected, and the chance of none is , so about 58 runs should see , and 49 did. At the Wald interval collapses to the point 0, since , and cannot contain 0.028. Those 49 runs make 49 of Wald's 50 misses, and the last is a run with nine or more failures, whose interval cannot reach down to 0.028. At the Wald interval reaches below zero, to , an impossible probability. Wald's 950 is a coincidence, since nearly all its misses come from one value of at which it cannot succeed.
Wilson never collapses. At its interval runs from 0 to 0.0370 and contains 0.028. Its 19 misses all come from runs with 7, 8 or 9 failures, whose intervals start above 0.028, at 0.0343 for . Because takes only whole values, no rule hits exactly 95% at small , and the exact coverage of a rule is the total binomial probability of the values of whose intervals contain the true rate. For Wilson at and that total is 0.9774, against the 981 in 1,000 measured, so here Wilson errs on the safe side.
It does not always. At and a single failure already lifts Wilson's lower end above the true rate, to 0.0018, so only the runs with no failures cover it, and the exact coverage falls to . Brown, Cai and DasGupta [4] document such dips near 0 and 1, where the discreteness of bites hardest, and they still recommend Wilson's interval over Wald's. Agresti and Coull [5] showed that approximate intervals of this kind serve better in practice than the so-called exact ones.
09.When No Errors Appear
A good code at low noise often shows no failures at all. Send , the five-fold repetition code, over for 20,000 words with seed 18, and . That is no surprise. fails when three or more of its five bits flip, with probability , so 20,000 words expect 0.197 failures and see none with probability 0.821.
The Wald interval at is the point 0, a claim that the code never fails. The honest question is the largest still consistent with zero failures, say the largest that gives zero failures at least 5% of the time. Zero failures in independent words has probability , and setting that to 0.05 gives , which for is .
Proof. The curve lies above its tangent line at 0, so , and with this gives . Both sides are nonnegative for , so raising them to the power keeps the order, . For the right side is at most , below 0.05.
The exact bound solves , and since , the rounded 3 works well. For the run the rule gives , against the exact . Hanley and Lippman-Hand [6] made the rule popular in medicine, where a trial that sees no side effects in patients faces the same question.
The Wilson interval at reduces to the range from 0 to , here , close to a rule of 3.84 rather than 3. A two-sided 95% interval leaves 2.5% at each end, so its upper end matches a one-sided bound at 97.5%, and gives , near Wilson's value. The rule of three answers the one-sided question of how large the rate could be, usually the question asked after a clean run.
None of this says that fails at a rate of . It says that 20,000 clean words cannot rule out a rate that high, about 15 times the true one. Pinning the rate down takes a run long enough to see failures, about 10.2 million words for 100 of them.
10.How Many Trials to Run
Measuring Error Rates by Simulation also turned the variance into a trial count. Asking for a 95% half-width of at most times gives
For 20% precision , and is the expected number of failures, so for small the formula asks for about 96 of them, the source of that section's rule of thumb of counting about 100 errors. With failures and small , the relative half-width is about . For 10% precision the factor is . Precision depends on failures, not trials, which is why rare events are expensive. At , 20% precision needs 9.60 million trials.
One rule matters more than the formula. Fix before the run. An experimenter who checks the interval after every word and stops when it contains the hoped-for value stops at a lucky moment far more often than the 95% level allows, because that level assumes a trial count chosen in advance. Jeruchim [7] surveys methods for estimating bit error rates by simulation, including ways to cut the trial count at rates too low for plain counting.
11.Checking the Closed Forms
Every experiment can now meet its exact answer. The binary channels take their closed forms from Counting Errors with the Binomial Distribution. on fails when two or three bits flip, with probability , which is 0.028 at . On it fails only when all three are erased, with probability at .
The Gaussian forms need , the probability that a standard normal exceeds . Python has no , but its math.erfc, the complementary error function, gives it through
Uncoded BPSK fails on bit 0 when , that is when , which by symmetry has probability , and Equation 1 with makes this . Bit 1 is the mirror image. The hard decoder sees each pulse wrong with probability and fails when two or three pulses are wrong, with probability . The worked example The Gaussian Closed Forms at 4 dB evaluates both. The soft decoder needs a theorem.
Proof. Take bit 0, sent as on each of pulses, with noise variance per pulse from Equation 1 with . The decoder fails when . By the corollary of the rotation lemma the sum of the behaves like , so the decoder fails when . Since , the failure probability is , and bit 1 is the mirror image.
Repetition with soft decoding therefore gains nothing over sending the bit once. The pulses spend the bit's energy in pieces, and the soft decoder gathers all of it back. Hard decoding throws part of it away and does worse, more than twice the error rate of uncoded BPSK at 4 dB.
Single parity on the binary symmetric channel misses an error when an even, nonzero number of bits flip, and the binomial theorem gives that probability in closed form.
Proof. Write . The binomial theorem expands , the sum over of the probabilities of exactly flips, and into the same terms with a minus sign on every odd . Adding the two cancels the odd terms and doubles the even ones, so . Zero flips, with probability , is no error, and subtracting it leaves the undetected errors.
For the (8,7) code at , , so the even probability is , and subtracting leaves . Lin and Costello [8] derive the same quantity for every linear code through its weight distribution. The detected fraction is the odd part, , so about 7,461.8 of 100,000 words should be flagged, and the run with seed 13 flags 7,565. On the erasure channel single parity fails to fill a word when two or more bits are erased, with probability , which at and is .
Table 2 lines up all seven experiments with their 95% Wilson intervals and exact values, and Figure 7 draws each interval divided by its exact value.
Table 2. The chapter's seven seeded simulations against their closed forms, each with its 95% Wilson interval. One interval misses
| Experiment | Seed | Words | Failures | Wilson interval | Exact |
|---|---|---|---|---|---|
| on | 2026 | 10,000 | 266 | 0.02362 to 0.02994 | 0.028 |
| on | 12 | 100,000 | 741 | 0.006897 to 0.007961 | 0.008, outside |
| Parity (8,7) undetected, | 13 | 100,000 | 265 | 0.002350 to 0.002988 | 0.0026368 |
| Parity (8,7) unfilled, | 14 | 20,000 | 3,720 | 0.18067 to 0.19145 | 0.18690 |
| Uncoded BPSK, 4 dB | 15 | 100,000 | 1,246 | 0.011791 to 0.013167 | 0.012501 |
| Hard , 4 dB | 16 | 100,000 | 2,691 | 0.025925 to 0.027931 | 0.026835 |
| Soft , 4 dB | 16 | 100,000 | 1,250 | 0.011830 to 0.013208 | 0.012501 |
Source: reproduced by ch11_project_channel_simulator.py.
The erasure run is the miss. It counted 741 failures where 800 were expected, and with a standard deviation of failures, 741 lies 2.09 standard deviations low, so its interval stops at 0.007961, just short of 0.008. Nothing is wrong with the simulator. Seven 95% intervals expect misses, and if they were independent the chance that at least one misses would be . They are not quite independent, since the hard and soft runs share seed 16 and fail together often, on 894 of the soft decoder's 1,250 failures, so 0.3017 is an approximation, though the expected 0.35 misses holds whatever the dependence. Two further runs confirm the diagnosis. The same seed continued to 1,000,000 words gives 7,974 failures and an interval from 0.007802 to 0.008150, which contains 0.008, and the 100,000-word run repeated with seeds 1 to 100 gives 98 intervals that contain it.
The tempting repair is to change the seed until the miss goes away. That is seed shopping, and it destroys the coverage the interval claims. The seeds here are reported with their misses, and the miss stays in the table.
Figure 8 repeats the Gaussian check from 0 to 7 dB in steps of 1 dB, with 100,000 words per point and seed , for uncoded BPSK and hard and soft . The soft points sit on the uncoded curve, as the theorem says, and the hard points on their own higher curve. Of the 24 intervals one misses, hard at 1 dB with seed 101, which counted 8,727 failures against 8,536.0 expected. Twenty-four intervals expect 1.2 misses, and if they were independent the chance that all 24 cover would be only . The three curves share a seed at each point, and the hard and soft counts are strongly correlated, as the worked example Hard and Soft Decoders Word by Word shows at 4 dB, so 0.2920 is an approximation. One miss is unremarkable.
12.Looking Ahead
The simulator is now calibrated. It agrees with every closed form it can be checked against, its misses, one in seven intervals and one in twenty-four, sit close to the 0.35 and 1.2 a 95% rule expects, and every number it reports carries a seed and a trial count that reproduce it. With longer codes and stronger decoders in place of repetition and parity, the same loop will later measure error rates that have no formula.
The next chapter is a lab that takes single parity into hardware. It builds a parity generator and checker in SystemVerilog, simulates them with Verilator, and drives them from a cocotb testbench in Python that compares every hardware output with a software model like this one. Part II then turns to the arithmetic longer codes are built from, starting with the integers and arithmetic modulo a whole number.
13.Worked Examples
14.Exercises
References
- [1]Matsumoto, Makoto and Nishimura, Takuji (1998). “Mersenne Twister: A 623-Dimensionally Equidistributed Uniform Pseudo-Random Number Generator.” ACM Transactions on Modeling and Computer Simulation, 8(1), pp. 3–30. doi:10.1145/272991.272995
- [2]Box, G. E. P. and Muller, Mervin E. (1958). “A Note on the Generation of Random Normal Deviates.” The Annals of Mathematical Statistics, 29(2), pp. 610–611. doi:10.1214/aoms/1177706645
- [3]Wilson, Edwin B. (1927). “Probable Inference, the Law of Succession, and Statistical Inference.” Journal of the American Statistical Association, 22(158), pp. 209–212. doi:10.1080/01621459.1927.10502953
- [4]Brown, Lawrence D. and Cai, T. Tony and DasGupta, Anirban (2001). “Interval Estimation for a Binomial Proportion.” Statistical Science, 16(2), pp. 101–133. doi:10.1214/ss/1009213286
- [5]Agresti, Alan and Coull, Brent A. (1998). “Approximate is Better than "Exact" for Interval Estimation of Binomial Proportions.” The American Statistician, 52(2), pp. 119–126. doi:10.1080/00031305.1998.10480550
- [6]Hanley, James A. and Lippman-Hand, Abby (1983). “If Nothing Goes Wrong, Is Everything All Right? Interpreting Zero Numerators.” JAMA, 249(13), pp. 1743–1745. doi:10.1001/jama.1983.03330370053031
- [7]Jeruchim, Michel C. (1984). “Techniques for Estimating the Bit Error Rate in the Simulation of Digital Communication Systems.” IEEE Journal on Selected Areas in Communications, 2(1), pp. 153–170. doi:10.1109/JSAC.1984.1146031
- [8]Lin, Shu and Costello, Jr., Daniel J. (2004). “Error Control Coding: Fundamentals and Applications.” 2nd edition. Pearson Prentice Hall.
— Anirban
Hardware–software at the seam where compilers meet microcode.