codexdata-processing-binary-cascade.py
data-processing-binary-cascade.py
"""Draw a binary-channel cascade and information bounds; save PNG to cwd.
Uses only matplotlib and numpy from the root pyproject.toml.
"""
from pathlib import Path
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch, FancyBboxPatch
import numpy as np
def binary_entropy(probability):
p = np.asarray(probability, dtype=float)
result = np.zeros_like(p)
interior = (p > 0) & (p < 1)
values = p[interior]
result[interior] = -values * np.log2(values) - (1 - values) * np.log2(1 - values)
return result
def draw_channel(ax, left, right, error, color):
"""Draw all four conditional transition probabilities of one BSC."""
for value in (0, 1):
y = 0.66 if value == 0 else 0.16
ax.add_patch(FancyArrowPatch((left + 0.14, y), (right - 0.14, y),
arrowstyle='-|>', mutation_scale=13,
linewidth=1.7, color=color))
ax.text((left + right) / 2, y + 0.045, f'{1-error:.2f}',
ha='center', va='bottom', color=color, fontsize=11,
bbox={'facecolor': 'white', 'edgecolor': 'none', 'pad': 1})
other_y = 0.16 if value == 0 else 0.66
bend = 0.14 if value == 0 else -0.14
ax.add_patch(FancyArrowPatch((left + 0.14, y), (right - 0.14, other_y),
connectionstyle=f'arc3,rad={bend}',
arrowstyle='-|>', mutation_scale=12,
linewidth=1.2, color=color, alpha=0.65))
ax.text(left + 0.30, y + (-0.13 if value == 0 else 0.12), f'{error:.2f}',
ha='center', va='center', color=color, fontsize=11,
bbox={'facecolor': 'white', 'edgecolor': 'none', 'pad': 1})
def main():
q = 0.1
chosen_r = 0.2
r = np.linspace(0, 0.5, 501)
effective = q + r - 2 * q * r
i_xy = float(1 - binary_entropy(q))
i_yz = 1 - binary_entropy(r)
i_xz = 1 - binary_entropy(effective)
assert np.all(i_xz <= i_xy + 1e-14)
assert np.all(i_xz <= i_yz + 1e-14)
assert np.allclose([i_xz[0], i_xz[-1], i_yz[-1]], [i_xy, 0, 0])
colors = {'xy': '#2965a5', 'yz': '#aa531c', 'xz': '#167c60'}
plt.rcParams.update({'font.size': 11, 'axes.spines.top': False,
'axes.spines.right': False})
fig = plt.figure(figsize=(10, 8.2), layout='constrained', facecolor='white')
grid = fig.add_gridspec(2, 1, height_ratios=[1.05, 1.55])
diagram = fig.add_subplot(grid[0])
diagram.set(xlim=(-0.35, 4.35), ylim=(-0.44, 1.13))
diagram.axis('off')
diagram.set_title('Independent noises: two binary symmetric channels', pad=14)
for x, name in [(0, 'X'), (2, 'Y'), (4, 'Z')]:
diagram.text(x, 0.93, f'{name}: fair bit', ha='center', fontsize=12, weight='bold')
for value, y in [(0, 0.66), (1, 0.16)]:
diagram.add_patch(FancyBboxPatch((x - 0.13, y - 0.10), 0.26, 0.20,
boxstyle='round,pad=0.01',
facecolor='#edf2f7', edgecolor='#42576b'))
diagram.text(x, y, str(value), ha='center', va='center', fontsize=12)
draw_channel(diagram, 0, 2, q, colors['xy'])
draw_channel(diagram, 2, 4, chosen_r, colors['yz'])
diagram.text(1, 0.94, 'flip q = 0.10', ha='center', color=colors['xy'])
diagram.text(3, 0.94, 'flip r = 0.20', ha='center', color=colors['yz'])
diagram.text(2, -0.15, 'Each arrow label is a conditional probability; each input has probability 1/2.',
ha='center', fontsize=10)
diagram.text(2, -0.35, 'Net flip: s = q(1 - r) + (1 - q)r = 0.26',
ha='center', fontsize=12, color=colors['xz'])
ax = fig.add_subplot(grid[1])
ax.axhline(i_xy, color=colors['xy'], linewidth=2, linestyle='--',
label=r'$I(X;Y)=1-h_2(0.1)$')
ax.plot(r, i_yz, color=colors['yz'], linewidth=2.3,
label=r'$I(Y;Z)=1-h_2(r)$')
ax.plot(r, i_xz, color=colors['xz'], linewidth=2.7,
label=r'$I(X;Z)=1-h_2(0.1+0.8r)$')
ax.axvline(chosen_r, color='#728091', linestyle=':', linewidth=1.2)
chosen = [i_xy, float(1 - binary_entropy(chosen_r)),
float(1 - binary_entropy(q + chosen_r - 2*q*chosen_r))]
for info, key, offset in zip(chosen, ['xy', 'yz', 'xz'], [0.04, 0.03, -0.105]):
ax.scatter([chosen_r], [info], color=colors[key], s=35, zorder=5)
ax.annotate(f'{info:.4f} bits', (chosen_r, info),
xytext=(chosen_r + 0.018, info + offset),
color=colors[key], fontsize=11)
ax.set(xlim=(0, 0.5), ylim=(-0.025, 1.025),
xlabel='Second-channel flip probability r (first-channel q = 0.1)',
ylabel='Mutual information (bits)',
title='The cascade stays below both individual-channel bounds')
ax.set_xticks(np.arange(0, 0.51, 0.1))
ax.grid(alpha=0.2)
ax.legend(loc='upper right', fontsize=11)
fig.savefig(Path.cwd() / 'data-processing-binary-cascade.png', dpi=120,
facecolor='white', transparent=False)
plt.close(fig)
if __name__ == '__main__':
main()