VKVlad KulikovPublic / lectures
Information Theory for ML7 / 16

Module 7. Channel and capacity

Try each exercise first. Expand worked answers or project guidance when needed; their open state is shared between RU/EN.

Download course files (.zip) 33.9 KB

The archive preserves the folder structure of nine Python solution files shared by RU/EN.

Exercise 1. BEC capacity

Let X∼Bernoulli⁡(p)X\sim\operatorname{Bernoulli}(p) pass through a BEC(ε)(\varepsilon).

  1. Show that

    H(X∣Y)=εH(X).H(X\mid Y)=\varepsilon H(X).
  2. Derive

    I(X;Y)=(1−ε)h2(p).I(X;Y)=(1-\varepsilon)h_2(p).
  3. Maximize over pp to obtain C=1−εC=1-\varepsilon.

  4. Explain why comparing BEC(ε)(\varepsilon) and BSC(ε)(\varepsilon) is useful, while the same numerical ε\varepsilon does not represent the same physical severity of noise.

Show answer and solution

Let X∼Bernoulli⁡(p)X\sim\operatorname{Bernoulli}(p) and let Y∈{0,1,?}Y\in\{0,1,?\} be the output of BEC(ε)(\varepsilon).

1. Conditional entropy

If Y∈{0,1}Y\in\{0,1\}, no erasure occurred and the input is known exactly:

H(X∣Y=0)=H(X∣Y=1)=0.H(X\mid Y=0)=H(X\mid Y=1)=0.

If Y=?Y=?, the erasure event is independent of the input value, so the posterior of XX is the prior:

H(X∣Y=?)=H(X)=h2(p).H(X\mid Y=? )=H(X)=h_2(p).

Since Pr⁡(Y=?)=ε\Pr(Y=? )=\varepsilon,

H(X∣Y)=εh2(p).\boxed{H(X\mid Y)=\varepsilon h_2(p)}.

2. Mutual information

I(X;Y)=H(X)−H(X∣Y)=h2(p)−εh2(p)=(1−ε)h2(p).\begin{aligned} I(X;Y) &=H(X)-H(X\mid Y)\\ &=h_2(p)-\varepsilon h_2(p)\\ &=(1-\varepsilon)h_2(p). \end{aligned}

3. Maximization

Binary entropy is maximized at p=1/2p=1/2 and equals one bit. Therefore

CBEC=1−ε.\boxed{C_{\mathrm{BEC}}=1-\varepsilon}.

4. Why equal numerical ε\varepsilon does not mean equally severe noise

At ε=0.1\varepsilon=0.1,

CBEC=0.9,CBSC=1−h2(0.1)≈0.5310.C_{\mathrm{BEC}}=0.9, \qquad C_{\mathrm{BSC}}=1-h_2(0.1)\approx0.5310.

The difference is not that a BEC is physically “better” under every comparison. The parameters describe different observations. A BEC receiver knows the corruption location; a flipped BSC bit looks like an ordinary valid symbol. Thus the same numerical ε\varepsilon is not a universal channel-severity scale.


Exercise 2. The asymmetric Z-channel

For

W=(10α1−α),p=Pr⁡(X=1),W= \begin{pmatrix} 1&0\\ \alpha&1-\alpha \end{pmatrix}, \qquad p=\Pr(X=1),
  1. derive

    I(p)=h2((1−α)p)−ph2(α);I(p)=h_2((1-\alpha)p)-p h_2(\alpha);
  2. find the stationary point;

  3. for α=0.25\alpha=0.25, compute p∗p^* and CC;

  4. compare the result with a uniform input.

Show answer and solution

The channel matrix is

W=(10α1−α),p=Pr⁡(X=1).W= \begin{pmatrix} 1&0\\ \alpha&1-\alpha \end{pmatrix}, \qquad p=\Pr(X=1).

1. Formula for I(p)I(p)

The output is one only when X=1X=1 and no error occurs:

Pr⁡(Y=1)=(1−α)p.\Pr(Y=1)=(1-\alpha)p.

Hence

H(Y)=h2((1−α)p).H(Y)=h_2((1-\alpha)p).

For X=0X=0 the output is deterministic, while for X=1X=1 its entropy is h2(α)h_2(\alpha). Thus

H(Y∣X)=ph2(α),H(Y\mid X)=p h_2(\alpha),

and

I(p)=h2((1−α)p)−ph2(α).\boxed{I(p)=h_2((1-\alpha)p)-p h_2(\alpha)}.

2. Stationary point

For 0<u<10<u<1,

h2′(u)=log⁡21−uu.h_2'(u)=\log_2\frac{1-u}{u}.

Therefore

I′(p)=(1−α)log⁡21−(1−α)p(1−α)p−h2(α).I'(p) =(1-\alpha) \log_2\frac{1-(1-\alpha)p}{(1-\alpha)p} -h_2(\alpha).

Put

t=(1−α)p.t=(1-\alpha)p.

The condition I′(p)=0I'(p)=0 becomes

log⁡21−tt=h2(α)1−α,\log_2\frac{1-t}{t} = \frac{h_2(\alpha)}{1-\alpha},

so

t∗=11+2h2(α)/(1−α)t^*= \frac{1}{1+2^{h_2(\alpha)/(1-\alpha)}}

and

p∗=1(1−α)(1+2h2(α)/(1−α)).\boxed{ p^*= \frac{1}{(1-\alpha) \left(1+2^{h_2(\alpha)/(1-\alpha)}\right)} }.

Mutual information is concave in the input distribution, so the interior stationary point is the global maximum.

3. Numbers for α=0.25\alpha=0.25

h2(0.25)≈0.811278,h_2(0.25)\approx0.811278, p∗≈0.4278256,p^*\approx0.4278256, C≈0.5582386 bits/use.\boxed{C\approx0.5582386\text{ bits/use}}.

4. Uniform input

At p=1/2p=1/2,

I(1/2)=h2(0.375)−12h2(0.25)≈0.5487949.I(1/2)=h_2(0.375)-\frac12h_2(0.25) \approx0.5487949.

The uniform input is close but not optimal. Because symbol 11 is the vulnerable one, it is used slightly less often.


Exercise 3. A converse through Fano

Suppose an (n,M)(n,M) code is used on a DMC of capacity CC, with average error Pe(n)P_e^{(n)}.

  1. Use Fano's inequality to show

    log⁡2M≤nC+1+Pe(n)log⁡2M.\log_2M \le nC+1+P_e^{(n)}\log_2M.
  2. Derive

    Pe(n)≥1−CR−1nR.P_e^{(n)} \ge 1-\frac{C}{R}-\frac{1}{nR}.
  3. Explain the difference between the weak and strong converses.

Show answer and solution

Let JJ be uniform over MM messages and let J^\widehat J be reconstructed from YnY^n.

1. Upper bound on codebook size

Fano's inequality gives

H(J∣Yn)≤h2(Pe(n))+Pe(n)log⁡2(M−1)≤1+Pe(n)log⁡2M.H(J\mid Y^n) \le h_2(P_e^{(n)})+P_e^{(n)}\log_2(M-1) \le 1+P_e^{(n)}\log_2M.

Then

log⁡2M=H(J)=I(J;Yn)+H(J∣Yn)≤I(Xn;Yn)+1+Pe(n)log⁡2M≤nC+1+Pe(n)log⁡2M.\begin{aligned} \log_2M &=H(J)\\ &=I(J;Y^n)+H(J\mid Y^n)\\ &\le I(X^n;Y^n)+1+P_e^{(n)}\log_2M\\ &\le nC+1+P_e^{(n)}\log_2M. \end{aligned}

We used the Markov chain J→Xn→YnJ\to X^n\to Y^n, data processing, and

I(Xn;Yn)≤∑i=1nI(Xi;Yi)≤nCI(X^n;Y^n)\le\sum_{i=1}^n I(X_i;Y_i)\le nC

for a DMC.

2. Lower bound on error

Rearranging,

(1−Pe(n))log⁡2M≤nC+1.(1-P_e^{(n)})\log_2M\le nC+1.

Since R=(1/n)log⁡2MR=(1/n)\log_2M,

1−Pe(n)≤CR+1nR.1-P_e^{(n)} \le \frac{C}{R}+\frac{1}{nR}.

Therefore

Pe(n)≥1−CR−1nR.\boxed{ P_e^{(n)} \ge 1-\frac{C}{R}-\frac{1}{nR} }.

3. Weak versus strong converse

For fixed R>CR>C, this gives

lim inf⁡n→∞Pe(n)≥1−CR>0.\liminf_{n\to\infty}P_e^{(n)} \ge 1-\frac{C}{R}>0.

This is a weak converse: it rules out vanishing error but does not prove convergence to one.

The strong converse for a DMC says

R>C⟹Pe(n)→1.R>C \quad\Longrightarrow\quad P_e^{(n)}\to1.

Thus 1−C/R1-C/R is not the asymptotic error of an optimal code above capacity; it is only the weak lower bound obtained from Fano.


Exercise 4. Symmetric label noise

Let YY be uniform over KK classes. The observed label remains correct with probability 1−η1-\eta and is otherwise replaced uniformly by one of the other K−1K-1 classes.

  1. Derive

    I(Y;Y~)=log⁡2K−h2(η)−ηlog⁡2(K−1).I(Y;\widetilde Y) = \log_2K-h_2(\eta)-\eta\log_2(K-1).
  2. Evaluate it at K=10K=10, η=0.2\eta=0.2.

  3. Find the value of η\eta at which the channel has zero capacity.

  4. Compute the remaining information at η=1\eta=1 and explain why zero accuracy does not imply zero MI.

  5. Explain why instance-dependent label noise cannot be represented by one such matrix.

Show answer and solution

Let YY be uniform over KK classes. Conditional on Y=iY=i, the observed label equals ii with probability 1−η1-\eta, and each incorrect class has probability η/(K−1)\eta/(K-1).

1. Mutual information

The channel is weakly symmetric, so a uniform input produces a uniform output:

H(Y~)=log⁡2K.H(\widetilde Y)=\log_2K.

Each transition row holds the probabilities

1−η,ηK−1,…,ηK−1⏟K−1 times.1-\eta, \quad \underbrace{\frac{\eta}{K-1},\ldots, \frac{\eta}{K-1}}_{K-1\text{ times}}.

Its entropy is

H(Y~∣Y=i)=−(1−η)log⁡2(1−η)−(K−1)ηK−1log⁡2ηK−1=h2(η)+ηlog⁡2(K−1).\begin{aligned} H(\widetilde Y\mid Y=i) &=-(1-\eta)\log_2(1-\eta) -(K-1)\frac{\eta}{K-1} \log_2\frac{\eta}{K-1}\\ &=h_2(\eta)+\eta\log_2(K-1). \end{aligned}

Therefore

I(Y;Y~)=log⁡2K−h2(η)−ηlog⁡2(K−1).\boxed{ I(Y;\widetilde Y) = \log_2K-h_2(\eta)-\eta\log_2(K-1) }.

The uniform input achieves capacity for this weakly symmetric channel, so the same expression is its capacity.

2. K=10K=10, η=0.2\eta=0.2

h2(0.2)≈0.721928,0.2log⁡29≈0.633985.h_2(0.2)\approx0.721928, \qquad 0.2\log_2 9\approx0.633985.

Therefore

I(Y;Y~)≈3.321928−0.721928−0.633985=1.966015 bits.\boxed{ I(Y;\widetilde Y) \approx 3.321928-0.721928-0.633985 = 1.966015\text{ bits} }.

3. Zero capacity

All rows coincide exactly when every output class has probability 1/K1/K. Thus

1−η=1K,1-\eta=\frac1K,

and

η=K−1K.\boxed{\eta=\frac{K-1}{K}}.

For K=10K=10, this is η=0.9\eta=0.9.

4. What remains at η=1\eta=1

At η=1\eta=1, the observed label is always wrong, but it still tells us one class that the example is guaranteed not to belong to. Substituting into the formula gives

I(Y;Y~)=log⁡2K−log⁡2(K−1)=log⁡2KK−1.I(Y;\widetilde Y) = \log_2K-\log_2(K-1) = \log_2\frac{K}{K-1}.

For K=10K=10,

I(Y;Y~)=log⁡2109≈0.1520 bit.I(Y;\widetilde Y) = \log_2\frac{10}{9} \approx0.1520\text{ bit}.

Thus zero accuracy does not imply zero mutual information: a systematically wrong signal can still be partially invertible. Capacity need not be monotone after the full-randomization point.

5. Why one matrix does not describe instance-dependent noise

A fixed matrix Tij=Pr⁡(Y~=j∣Y=i)T_{ij}=\Pr(\widetilde Y=j\mid Y=i) assumes

Y~⊥X∣Y.\widetilde Y\perp X\mid Y.

For instance-dependent noise one needs

Pr⁡(Y~=j∣Y=i,X=x),\Pr(\widetilde Y=j\mid Y=i,X=x),

which changes with xx. An averaged matrix can hide error concentration on difficult, ambiguous, or systematically biased subpopulations.


Exercise 5. ECOC

Four classes receive the codewords

text
c1 = 0000000
c2 = 1111000
c3 = 1100110
c4 = 1010101
  1. Compute all pairwise Hamming distances and dmin⁡d_{\min}.

  2. How many bit errors are guaranteed to be corrected by nearest-codeword decoding?

  3. Decode 1111010.

  4. Why can the guarantee be too optimistic for real binary classifiers?

Show answer and solution

The codewords are

text
c1 = 0000000
c2 = 1111000
c3 = 1100110
c4 = 1010101

1. Pairwise distances

Every pair has Hamming distance four:

PairHamming distance
c1,c2c_1,c_24
c1,c3c_1,c_34
c1,c4c_1,c_44
c2,c3c_2,c_34
c2,c4c_2,c_44
c3,c4c_3,c_44

Hence

dmin⁡=4.\boxed{d_{\min}=4}.

2. Guaranteed correction

Nearest-codeword decoding guarantees correction of

t=⌊dmin⁡−12⌋=1t=\left\lfloor\frac{d_{\min}-1}{2}\right\rfloor=1

bit error.

3. Decode 1111010

ClassDistance
c1c_15
c2c_21
c3c_33
c4c_45

Therefore

y^=2.\boxed{\widehat y=2}.

4. Why the coding guarantee is not a classifier guarantee

The distance argument treats arbitrary bit flips in an already formed codeword. In ECOC those “bits” are outputs of learned binary classifiers. Their errors can be correlated, unequal, example-dependent, concentrated in particular columns, and represented by continuous scores rather than ideal bits. Thus dmin⁡d_{\min} provides useful geometric robustness, but actual performance also depends on the class partitions, component learners, and decoding rule.


Exercise 6. Python: Blahut–Arimoto and finite blocklength

Implement two parts.

  1. Use Blahut–Arimoto to compute capacity and the optimal input distribution for

    • BSC(0.1)(0.1);
    • BEC(0.1)(0.1);
    • the Z-channel with α=0.25\alpha=0.25.
  2. For BSC(0.1)(0.1), generate random distinct codewords, decode by nearest Hamming distance, and compare

    • R=0.25<CR=0.25<C;
    • R=0.80>CR=0.80>C;
    • n∈{8,12,16,20}n\in\{8,12,16,20\}.

Average over several codebooks and break decoding ties uniformly at random. Explain why a finite-nn simulation is not a proof of the theorem and why exhaustive decoding does not scale.

Show answer and solution

The implementation below stores binary codewords as packed integers. For n≤20n\le20, this makes nearest-Hamming decoding fast through np.bitwise_count without changing the mathematical experiment.

Python
from __future__ import annotations

import math
import numpy as np


def binary_entropy(p: float) -> float:
    """Binary entropy in bits."""
    if p <= 0.0 or p >= 1.0:
        return 0.0
    return -p * math.log2(p) - (1.0 - p) * math.log2(1.0 - p)


def mutual_information(p_x: np.ndarray, channel: np.ndarray) -> float:
    """I(X;Y) in bits for an input distribution and a DMC matrix."""
    p_y = p_x @ channel
    joint = p_x[:, None] * channel
    x, y = np.nonzero(joint > 0)
    # Only positive joint mass contributes; unreachable outputs have p_y=0.
    return float(np.sum(joint[x, y] * np.log2(channel[x, y] / p_y[y])))


def blahut_arimoto(
    channel: np.ndarray,
    tol: float = 1e-12,
    max_iter: int = 100_000,
) -> tuple[float, np.ndarray, int]:
    """Return DMC capacity, a maximizing input distribution, and iterations."""
    channel = np.asarray(channel, dtype=float)
    if channel.ndim != 2 or np.any(channel < 0):
        raise ValueError("channel must be a nonnegative 2D array")
    if not np.allclose(channel.sum(axis=1), 1.0):
        raise ValueError("each row of channel must sum to one")

    p_x = np.full(channel.shape[0], 1.0 / channel.shape[0])
    for iteration in range(1, max_iter + 1):
        p_y = p_x @ channel
        ratio = np.divide(
            channel,
            p_y[None, :],
            out=np.ones_like(channel),
            where=channel > 0,
        )
        d_x = np.sum(
            np.where(channel > 0, channel * np.log(ratio), 0.0),
            axis=1,
        )
        p_new = p_x * np.exp(d_x)
        p_new /= p_new.sum()
        if np.max(np.abs(p_new - p_x)) < tol:
            p_x = p_new
            break
        p_x = p_new
    else:
        raise RuntimeError("Blahut-Arimoto did not converge")

    return mutual_information(p_x, channel), p_x, iteration


def random_noise_mask(
    rng: np.random.Generator,
    n: int,
    epsilon: float,
) -> np.uint32:
    """Generate an n-bit BSC noise mask packed into uint32."""
    bits = rng.random(n) < epsilon
    powers = np.left_shift(np.uint32(1), np.arange(n, dtype=np.uint32))
    return np.sum(bits.astype(np.uint32) * powers, dtype=np.uint32)


def simulate_random_codebook(
    rng: np.random.Generator,
    n: int,
    rate: float,
    epsilon: float,
    trials: int = 300,
) -> float:
    """Estimate block error for one distinct random binary codebook."""
    universe = 1 << n
    m = max(2, int(round(2 ** (n * rate))))
    if m > universe:
        raise ValueError("rate requires more distinct codewords than exist")

    codebook = rng.choice(universe, size=m, replace=False).astype(np.uint32)
    errors = 0

    for _ in range(trials):
        message = int(rng.integers(m))
        received = codebook[message] ^ random_noise_mask(rng, n, epsilon)
        distances = np.bitwise_count(codebook ^ received)
        min_distance = distances.min()
        candidates = np.flatnonzero(distances == min_distance)
        decoded = int(rng.choice(candidates))
        errors += decoded != message

    return errors / trials


def main() -> None:
    bsc = np.array([[0.9, 0.1], [0.1, 0.9]])
    bec = np.array([[0.9, 0.0, 0.1], [0.0, 0.9, 0.1]])
    z_channel = np.array([[1.0, 0.0], [0.25, 0.75]])

    print("Blahut-Arimoto")
    for name, channel in [
        ("BSC(0.1)", bsc),
        ("BEC(0.1)", bec),
        ("Z(0.25)", z_channel),
    ]:
        capacity, p_x, iterations = blahut_arimoto(channel)
        print(
            f"{name:9s}: C={capacity:.9f}, "
            f"p_X={p_x}, iterations={iterations}"
        )

    epsilon = 0.1
    capacity = 1.0 - binary_entropy(epsilon)
    print(f"\nBSC(0.1) capacity: {capacity:.9f} bits/use")

    rng = np.random.default_rng(7)
    blocklengths = [8, 12, 16, 20]
    rates = [0.25, 0.80]
    n_codebooks = 10
    trials = 300

    for rate in rates:
        side = "below" if rate < capacity else "above"
        print(f"\nR={rate:.2f} ({side} capacity)")
        for n in blocklengths:
            estimates = np.array(
                [
                    simulate_random_codebook(rng, n, rate, epsilon, trials)
                    for _ in range(n_codebooks)
                ]
            )
            m = max(2, int(round(2 ** (n * rate))))
            print(
                f"n={n:2d}, M={m:6d}: "
                f"mean P_e={estimates.mean():.6f}, "
                f"std={estimates.std(ddof=1):.6f}"
            )


if __name__ == "__main__":
    main()

With the fixed seed, one run gives

text
Blahut-Arimoto
BSC(0.1) : C=0.531004406, p_X=[0.5 0.5], iterations=1
BEC(0.1) : C=0.900000000, p_X=[0.5 0.5], iterations=1
Z(0.25)  : C=0.558238627, p_X=[0.5721744 0.4278256], iterations=26

BSC(0.1) capacity: 0.531004406 bits/use

R=0.25 (below capacity)
n= 8, M=     4: mean P_e=0.082333, std=0.044055
n=12, M=     8: mean P_e=0.061000, std=0.020248
n=16, M=    16: mean P_e=0.046667, std=0.008165
n=20, M=    32: mean P_e=0.036000, std=0.011089

R=0.80 (above capacity)
n= 8, M=    84: mean P_e=0.462333, std=0.033740
n=12, M=   776: mean P_e=0.594667, std=0.022233
n=16, M=  7132: mean P_e=0.674333, std=0.031113
n=20, M= 65536: mean P_e=0.714000, std=0.030217

Interpretation

Blahut–Arimoto reproduces the analytical values:

  • BSC(0.1)(0.1): C≈0.5310044C\approx0.5310044, uniform maximizing input;
  • BEC(0.1)(0.1): C=0.9C=0.9, uniform maximizing input;
  • Z(0.25)(0.25): C≈0.5582386C\approx0.5582386 and PX∗≈(0.5721744,0.4278256).P_X^*\approx(0.5721744,0.4278256).

In the BSC experiment:

  • at R=0.25<CR=0.25<C, ensemble-average error decreases over these blocklengths;
  • at R=0.80>CR=0.80>C, it moves upward toward one, consistently with the strong converse;
  • individual values need not be monotone for every seed or every codebook;
  • a finite simulation is not a proof of an asymptotic theorem;
  • exhaustive decoding scans all M≈2nRM\approx2^{nR} codewords and is exponential in nn.

The last point is the gap between an existence proof based on random codes and an engineering-ready coding system.


Code solution · the same source file for RU/EN

PythonM7_blahut_arimoto_and_simulation.pyDownload .py
from __future__ import annotations

import math
import numpy as np


def binary_entropy(p: float) -> float:
    """Binary entropy in bits."""
    if p <= 0.0 or p >= 1.0:
        return 0.0
    return -p * math.log2(p) - (1.0 - p) * math.log2(1.0 - p)


def mutual_information(p_x: np.ndarray, channel: np.ndarray) -> float:
    """I(X;Y) in bits for an input distribution and a DMC matrix."""
    p_y = p_x @ channel
    joint = p_x[:, None] * channel
    x, y = np.nonzero(joint > 0)
    # Only positive joint mass contributes; unreachable outputs have p_y=0.
    return float(np.sum(joint[x, y] * np.log2(channel[x, y] / p_y[y])))


def blahut_arimoto(
    channel: np.ndarray,
    tol: float = 1e-12,
    max_iter: int = 100_000,
) -> tuple[float, np.ndarray, int]:
    """Return DMC capacity, a maximizing input distribution, and iterations."""
    channel = np.asarray(channel, dtype=float)
    if channel.ndim != 2 or np.any(channel < 0):
        raise ValueError("channel must be a nonnegative 2D array")
    if not np.allclose(channel.sum(axis=1), 1.0):
        raise ValueError("each row of channel must sum to one")

    p_x = np.full(channel.shape[0], 1.0 / channel.shape[0])
    for iteration in range(1, max_iter + 1):
        p_y = p_x @ channel
        ratio = np.divide(
            channel,
            p_y[None, :],
            out=np.ones_like(channel),
            where=channel > 0,
        )
        d_x = np.sum(
            np.where(channel > 0, channel * np.log(ratio), 0.0),
            axis=1,
        )
        p_new = p_x * np.exp(d_x)
        p_new /= p_new.sum()
        if np.max(np.abs(p_new - p_x)) < tol:
            p_x = p_new
            break
        p_x = p_new
    else:
        raise RuntimeError("Blahut-Arimoto did not converge")

    return mutual_information(p_x, channel), p_x, iteration


def random_noise_mask(
    rng: np.random.Generator,
    n: int,
    epsilon: float,
) -> np.uint32:
    """Generate an n-bit BSC noise mask packed into uint32."""
    bits = rng.random(n) < epsilon
    powers = np.left_shift(np.uint32(1), np.arange(n, dtype=np.uint32))
    return np.sum(bits.astype(np.uint32) * powers, dtype=np.uint32)


def simulate_random_codebook(
    rng: np.random.Generator,
    n: int,
    rate: float,
    epsilon: float,
    trials: int = 300,
) -> float:
    """Estimate block error for one distinct random binary codebook."""
    universe = 1 << n
    m = max(2, int(round(2 ** (n * rate))))
    if m > universe:
        raise ValueError("rate requires more distinct codewords than exist")

    codebook = rng.choice(universe, size=m, replace=False).astype(np.uint32)
    errors = 0

    for _ in range(trials):
        message = int(rng.integers(m))
        received = codebook[message] ^ random_noise_mask(rng, n, epsilon)
        distances = np.bitwise_count(codebook ^ received)
        min_distance = distances.min()
        candidates = np.flatnonzero(distances == min_distance)
        decoded = int(rng.choice(candidates))
        errors += decoded != message

    return errors / trials


def main() -> None:
    bsc = np.array([[0.9, 0.1], [0.1, 0.9]])
    bec = np.array([[0.9, 0.0, 0.1], [0.0, 0.9, 0.1]])
    z_channel = np.array([[1.0, 0.0], [0.25, 0.75]])

    print("Blahut-Arimoto")
    for name, channel in [
        ("BSC(0.1)", bsc),
        ("BEC(0.1)", bec),
        ("Z(0.25)", z_channel),
    ]:
        capacity, p_x, iterations = blahut_arimoto(channel)
        print(
            f"{name:9s}: C={capacity:.9f}, "
            f"p_X={p_x}, iterations={iterations}"
        )

    epsilon = 0.1
    capacity = 1.0 - binary_entropy(epsilon)
    print(f"\nBSC(0.1) capacity: {capacity:.9f} bits/use")

    rng = np.random.default_rng(7)
    blocklengths = [8, 12, 16, 20]
    rates = [0.25, 0.80]
    n_codebooks = 10
    trials = 300

    for rate in rates:
        side = "below" if rate < capacity else "above"
        print(f"\nR={rate:.2f} ({side} capacity)")
        for n in blocklengths:
            estimates = np.array(
                [
                    simulate_random_codebook(rng, n, rate, epsilon, trials)
                    for _ in range(n_codebooks)
                ]
            )
            m = max(2, int(round(2 ** (n * rate))))
            print(
                f"n={n:2d}, M={m:6d}: "
                f"mean P_e={estimates.mean():.6f}, "
                f"std={estimates.std(ddof=1):.6f}"
            )


if __name__ == "__main__":
    main()

Bonus. AWGN capacity

Prove

C=12log⁡2(1+P/N)C=\frac12\log_2(1+P/N)

for Y=X+ZY=X+Z, using

I(X;Y)=h(Y)−h(Z)I(X;Y)=h(Y)-h(Z)

and the fact that a Gaussian distribution maximizes differential entropy at fixed variance.

Show answer and solution

Let

Y=X+Z,Z∼N(0,N),E[X2]≤P,Y=X+Z, \qquad Z\sim\mathcal N(0,N), \qquad \mathbb E[X^2]\le P,

with ZZ independent of XX.

For every admissible input,

I(X;Y)=h(Y)−h(Y∣X)=h(Y)−h(Z),I(X;Y)=h(Y)-h(Y\mid X)=h(Y)-h(Z),

because conditional on XX, YY is a shift of ZZ.

The noise entropy is

h(Z)=12log⁡2(2πeN).h(Z)=\frac12\log_2(2\pi eN).

Also,

Var⁡(Y)=Var⁡(X)+N≤P+N.\operatorname{Var}(Y) = \operatorname{Var}(X)+N \le P+N.

The Gaussian maximizes differential entropy at fixed variance, hence

h(Y)≤12log⁡2(2πe(P+N)).h(Y)\le\frac12\log_2(2\pi e(P+N)).

Therefore

I(X;Y)≤12log⁡2(2πe(P+N))−12log⁡2(2πeN)=12log⁡2(1+PN).\begin{aligned} I(X;Y) &\le \frac12\log_2(2\pi e(P+N)) - \frac12\log_2(2\pi eN)\\ &= \frac12\log_2\left(1+\frac PN\right). \end{aligned}

To attain the bound it is enough to choose

X∼N(0,P).X\sim\mathcal N(0,P).

Then

Y∼N(0,P+N),Y\sim\mathcal N(0,P+N),

the upper bound on h(Y)h(Y) holds with equality, and

CAWGN=12log⁡2(1+PN).\boxed{ C_{\mathrm{AWGN}} = \frac12\log_2\left(1+\frac PN\right) }.

This is bits per real discrete channel use. The continuous-time formula Blog⁡2(1+S/N)B\log_2(1+S/N) bits/s uses a different normalization and should not be conflated with it.