4B/5B 문서의 그림 생성 코드다. if __name__ == "__main__": 아래가 자체 테스트다.
# 4B/5B 문서의 그림을 만든다: 42_4b5b_fig1.svg, fig2.svg
# 실행: ~/.venvs/vault-plots/bin/python 42_4b5b_plot.py (matplotlib, numpy 필요)
# fig1: 0010 -> 10100, 0001 -> 01001을 이어 붙였을 때 부호 경계에 걸친 0 세 개와 NRZI의 가장 긴 평평한 구간
# fig2: 데이터 1011 0000을 그냥 NRZI로 보낼 때와 4B/5B 부호로 바꿔 NRZI로 보낼 때 (같은 시간축)
import os
import matplotlib
matplotlib.use("svg")
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import font_manager
from matplotlib.patches import Rectangle
HERE = os.path.dirname(os.path.abspath(__file__))
PREFIX = "42_4b5b"
INK = "#888888" # 밝은 테마와 어두운 테마 모두에서 보이는 회색
C = ["#3b82c4", "#d9622b", "#3a9a5b", "#9b59b6", "#c49a1b"]
_fonts = {f.name for f in font_manager.fontManager.ttflist}
FONT = next((n for n in ["Apple SD Gothic Neo", "NanumGothic", "Noto Sans CJK KR"] if n in _fonts), "DejaVu Sans")
plt.rcParams.update({
"font.family": FONT, "mathtext.fontset": "dejavusans",
"font.size": 11, "axes.unicode_minus": False, "svg.fonttype": "path", "svg.hashsalt": PREFIX,
"axes.edgecolor": INK, "axes.labelcolor": INK, "xtick.color": INK, "ytick.color": INK,
"text.color": INK, "legend.frameon": False,
"axes.facecolor": "none", "figure.facecolor": "none", "savefig.transparent": True,
"axes.spines.top": False, "axes.spines.right": False,
})
def save(fig, k):
fig.savefig(os.path.join(HERE, f"{PREFIX}_fig{k}.svg"), bbox_inches="tight", metadata={"Date": None})
plt.close(fig)
# ---- 파형 공통 도구 (40~43 인코딩 그림 코드에 같은 것을 복사해 쓴다) ----
H = 0.7 # 파형 높이 (낮음 = y0, 높음 = y0 + H)
LW = 1.8 # 파형 선 굵기
HL = C[1] # 볼 곳(평평한 구간, 바뀌는 순간)을 칠하는 색
def draw_wave(ax, halves, y0, color, x0=0.0, unit=1.0):
"""반 칸 높이 목록(0 낮음, 1 높음)을 계단 파형으로. 칸 폭 unit, 반 칸 폭 unit/2."""
xs = [x0 + i * unit / 2 for i in range(len(halves) + 1)]
ys = [y0 + H * v for v in list(halves) + [halves[-1]]]
ax.step(xs, ys, where="post", color=color, lw=LW, solid_joinstyle="miter")
def draw_grid(ax, n, y_lo, y_hi, x0=0.0, unit=1.0, mid=False, mid_hi=None):
"""칸 경계는 옅은 점선, mid=True면 칸 한가운데에 더 옅은 안내선(위끝 mid_hi, 비트 글자를 피한다)."""
for k in range(n + 1):
ax.plot([x0 + k * unit] * 2, [y_lo, y_hi], color=INK, lw=0.6, ls=(0, (1, 2)), zorder=0)
if mid:
top = y_hi if mid_hi is None else mid_hi
for k in range(n):
ax.plot([x0 + (k + 0.5) * unit] * 2, [y_lo, top], color=INK, lw=0.5, alpha=0.35, zorder=0)
def write_bits(ax, bits, y, color=INK, x0=0.0, unit=1.0, size=10, weight="normal"):
for k, b in enumerate(bits):
ax.text(x0 + (k + 0.5) * unit, y, b, ha="center", va="center", fontsize=size, color=color,
fontweight=weight)
def row_label(ax, text, y, color=INK, x=-0.3, size=10):
ax.text(x, y, text, ha="right", va="center", fontsize=size, color=color)
def shade(ax, xa, xb, y_lo, y_hi, color=HL, alpha=0.16):
ax.add_patch(Rectangle((xa, y_lo), xb - xa, y_hi - y_lo, facecolor=color, edgecolor="none",
alpha=alpha, zorder=0.5))
def change_points(halves):
"""높이가 바뀌는 반 칸 번호 i (i번째 반 칸의 시작에서 바뀐다)."""
return [i for i in range(1, len(halves)) if halves[i] != halves[i - 1]]
def flat_runs(halves):
"""(시작 반 칸, 길이) 목록. 높이가 그대로인 덩어리."""
runs, start = [], 0
for i in range(1, len(halves) + 1):
if i == len(halves) or halves[i] != halves[i - 1]:
runs.append((start, i - start))
start = i
return runs
# ---- 공통 도구 끝 ----
# ---- 4B/5B 표와 NRZI 규칙 ----
TABLE = {
"0000": "11110", "0001": "01001", "0010": "10100", "0011": "10101",
"0100": "01010", "0101": "01011", "0110": "01110", "0111": "01111",
"1000": "10010", "1001": "10011", "1010": "10110", "1011": "10111",
"1100": "11010", "1101": "11011", "1110": "11100", "1111": "11101",
}
def encode(bits):
return "".join(TABLE[bits[i:i + 4]] for i in range(0, len(bits), 4))
def nrzi(bits, start=0):
level, out = start, []
for b in bits:
out.append(level)
if b == "1":
level ^= 1
out.append(level)
return out
def lead0(c):
return len(c) - len(c.lstrip("0"))
def trail0(c):
return len(c) - len(c.rstrip("0"))
def max_zero_run(s):
best = run = 0
for ch in s:
run = run + 1 if ch == "0" else 0
best = max(best, run)
return best
def zero_runs(s):
"""(시작, 길이) 목록. 0이 이어지는 덩어리."""
out, i = [], 0
while i < len(s):
if s[i] == "0":
j = i
while j < len(s) and s[j] == "0":
j += 1
out.append((i, j - i))
i = j
else:
i += 1
return out
def longest(halves):
best = max(n for s, n in flat_runs(halves))
return [(s, n) for s, n in flat_runs(halves) if n == best]
# ---- 그림 1: 10100 다음 01001. 경계에 걸친 0 세 개와 NRZI의 가장 긴 평평한 구간 ----
D1 = "00100001"
CODE1 = encode(D1)
NZ1 = nrzi(CODE1)
(z0, zn), = [r for r in zero_runs(CODE1) if r[1] == max_zero_run(CODE1)]
(f0, fn), = longest(NZ1)
fig, ax = plt.subplots(figsize=(6.6, 3.5))
ax.axis("off")
YB, YW = 2.25, 0.55 # 부호 비트 줄, NRZI 파형 바닥
draw_grid(ax, 10, YB - 0.3, YB + 0.3)
draw_grid(ax, 10, YW - 0.15, YW + H + 0.15)
shade(ax, z0, z0 + zn, YB - 0.28, YB + 0.28, alpha=0.22)
write_bits(ax, CODE1, YB, size=11)
row_label(ax, "부호 비트", YB)
for k, d in enumerate([D1[:4], D1[4:]]):
x0 = 5 * k
ax.annotate("", (x0 + 0.1, YB + 0.45), (x0 + 4.9, YB + 0.45),
arrowprops=dict(arrowstyle="-", color=INK, lw=0.8))
ax.text(x0 + 2.5, YB + 0.68, f"데이터 {d} → 부호 {CODE1[5 * k:5 * k + 5]}", ha="center", va="center",
fontsize=9.5)
ax.plot([5, 5], [YB - 0.3, YB + 0.55], color=INK, lw=1.2)
ax.plot([5, 5], [YW - 0.15, YW + H + 0.15], color=INK, lw=1.2)
ax.text(5, YB + 0.95, "부호 경계", ha="center", va="center", fontsize=8.5)
ax.text(z0 + zn / 2, (YB - 0.3 + YW + H + 0.15) / 2, f"부호 경계에 걸친 0 {zn}개 (앞 부호 끝 2개 + 뒤 부호 앞 1개)",
ha="center", va="center", fontsize=9, color=HL)
draw_wave(ax, NZ1, YW, C[3])
row_label(ax, "NRZI\n(처음 낮음)", YW + H / 2, color=C[3])
shade(ax, f0 / 2, (f0 + fn) / 2, YW - 0.12, YW + H + 0.12, alpha=0.16)
yd = YW - 0.4
for xa, xb, t in ((f0 / 2, z0, "반 칸"), (z0, z0 + zn, f"0 {zn}개"), (z0 + zn, (f0 + fn) / 2, "반 칸")):
ax.annotate("", (xa, yd), (xb, yd), arrowprops=dict(arrowstyle="<->", color=HL, lw=0.9,
shrinkA=0, shrinkB=0))
ax.text((xa + xb) / 2, yd - 0.22, t, ha="center", va="center", fontsize=8.5, color=HL)
ax.text((f0 + fn) / 4, yd - 0.55, f"신호가 바뀌지 않는 가장 긴 구간: {fn / 2:g}비트 폭", ha="center",
va="center", fontsize=9.5, color=HL)
ax.set_xlim(-1.6, 10.1)
ax.set_ylim(yd - 0.75, YB + 1.05)
save(fig, 1)
# ---- 그림 2: 데이터 1011 0000을 그냥 NRZI로 보낼 때와 4B/5B로 바꿔 NRZI로 보낼 때 ----
D2 = "10110000"
CODE2 = encode(D2)
PLAIN = nrzi(D2)
CODED = nrzi(CODE2)
U = len(D2) / len(CODE2) # 부호 비트 하나의 폭 = 데이터 비트 0.8개
fig, ax = plt.subplots(figsize=(6.8, 3.7))
ax.axis("off")
YA, YC = 2.05, 0.0
draw_grid(ax, len(D2), YA - 0.15, YA + H + 0.5)
write_bits(ax, D2, YA + H + 0.32, color=C[3])
row_label(ax, "데이터", YA + H + 0.32, color=C[3], size=9)
draw_wave(ax, PLAIN, YA, C[3])
row_label(ax, "데이터를\n그냥 NRZI로", YA + H / 2, color=C[3])
draw_grid(ax, len(CODE2), YC - 0.15, YC + H + 0.5, unit=U)
write_bits(ax, CODE2, YC + H + 0.32, color=C[4], unit=U, size=9.5)
row_label(ax, "부호", YC + H + 0.32, color=C[4], size=9)
draw_wave(ax, CODED, YC, C[4], unit=U)
row_label(ax, "4B/5B 부호를\nNRZI로", YC + H / 2, color=C[4])
for lv, y0, u, col in ((PLAIN, YA, 1.0, C[3]), (CODED, YC, U, C[4])):
for i in change_points(lv):
ax.plot([i * u / 2], [y0 + H / 2], "o", ms=4.5, color=HL, zorder=5)
for s, n in longest(lv):
shade(ax, s * u / 2, (s + n) * u / 2, y0 - 0.12, y0 + H + 0.12)
(sa, na), = longest(PLAIN)
ax.text((sa + na) / 4, YA - 0.38, f"0000 동안 평평: {na / 2:g}비트 폭 (끝난 뒤에도 1이 올 때까지 이어진다)",
ha="center", va="center", fontsize=9, color=HL)
(sc, nc), = longest(CODED)
ax.text((sc + nc / 2) * U / 2, YC - 0.38, f"가장 긴 평평: 부호 비트 {nc / 2:g}개 폭", ha="center",
va="center", fontsize=9, color=HL)
ax.text(8.15, YA + H / 2, f"변화 {len(change_points(PLAIN))}번", ha="left", va="center", fontsize=9,
color=C[3])
ax.text(8.15, YC + H / 2, f"변화 {len(change_points(CODED))}번", ha="left", va="center", fontsize=9,
color=C[4])
ax.annotate("", (0, YC - 0.8), (8, YC - 0.8), arrowprops=dict(arrowstyle="<->", color=INK, lw=0.8,
shrinkA=0, shrinkB=0))
ax.text(4, YC - 1.0, "같은 시간: 데이터 8비트 = 부호 10비트 (부호 비트 하나는 데이터 비트 0.8개 폭)",
ha="center", va="center", fontsize=9)
ax.set_xlim(-1.9, 8.1)
ax.set_ylim(YC - 1.2, YA + H + 0.55)
save(fig, 2)
if __name__ == "__main__":
codes = list(TABLE.values())
# 그림 1: 0010 -> 10100, 0001 -> 01001, 이어 붙이면 1010001001, 0 세 개가 경계(4~6번째 비트)에 걸친다
assert CODE1 == "1010001001" and (z0, zn) == (3, 3) and z0 < 5 < z0 + zn
assert trail0(TABLE["0010"]) == 2 and lead0(TABLE["0001"]) == 1
# NRZI(처음 낮음)의 가장 긴 평평한 구간: 반 칸 8개 = 4비트 폭, 3번째 칸 뒤 절반부터 7번째 칸 앞 절반까지
assert NZ1 == [0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0]
assert (f0, fn) == (5, 8) and f0 / 2 == 2.5 and (f0 + fn) / 2 == 6.5
# 0이 z개 이어지면(양옆이 1) NRZI 평평한 길이는 z + 1비트 폭. 어떤 두 부호를 이어도 가장 긴 평평은 4비트 폭
for z in range(0, 6):
lv = nrzi("1" + "0" * z + "1")
assert max(n for s, n in flat_runs(lv)) == max(2, 2 * z + 2)
assert max(max(n for s, n in flat_runs(nrzi(a + b))) for a in TABLE.values() for b in TABLE.values()) == 8
# 표의 16개 부호: 앞 0 <= 1, 뒤 0 <= 2, 안쪽 0 연속 <= 2, 모두 1을 품는다
assert all(lead0(c) <= 1 and trail0(c) <= 2 and max_zero_run(c) <= 2 and "1" in c for c in codes)
assert max(max_zero_run(a + b) for a in codes for b in codes) == 3
# 21가지 세기: 앞 0 개수(0 또는 1) x 뒤 0 개수(0, 1, 2)로 나눠 센다
def count(lead, trail):
return sum(1 for x in range(32) for c in [format(x, "05b")] if lead0(c) == lead and trail0(c) == trail)
table_count = {(l, t): count(l, t) for l in (0, 1) for t in (0, 1, 2)}
assert table_count == {(0, 0): 8, (0, 1): 4, (0, 2): 2, (1, 0): 4, (1, 1): 2, (1, 2): 1}
assert sum(table_count.values()) == 21
ok = [format(x, "05b") for x in range(32) if lead0(format(x, "05b")) <= 1 and trail0(format(x, "05b")) <= 2]
assert len(ok) == 21 and set(codes) <= set(ok)
assert sorted(set(ok) - set(codes)) == ["01100", "01101", "10001", "11001", "11111"]
# 효율: 100 Mbps 데이터는 초당 125 M 부호 비트
assert 100 * 5 / 4 == 125 and 4 / 5 == 0.8
# 그림 2: 1011 0000 -> 10111 11110, NRZI(처음 낮음) 그림 문자열은 실행 추적 표와 같다
assert CODE2 == "1011111110"
assert "".join("‾" if x else "_" for x in CODED) == "_‾‾‾‾__‾‾__‾‾__‾‾___"
assert longest(PLAIN) == [(7, 9)] # 1의 뒤 절반 + 0 네 개 = 반 칸 9개 = 4.5비트 폭
assert longest(CODED) == [(1, 4)] # 반 칸 4개 = 부호 비트 2개 폭
assert max_zero_run(CODE2) == 1 and CODE2[2:9] == "1" * 7 # 3번째 부호 비트부터 1이 일곱 개
assert len(change_points(PLAIN)) == 3 and len(change_points(CODED)) == 8
# 카드 C2의 데이터(0000 0000)는 그리지 않는다
assert "00000000" not in (D1, D2)
print("ALL CHECKS PASSED")