진폭·주파수·위상 변조 문서의 그림 생성 코드다. if __name__ == "__main__": 아래가 자체 테스트다.
# 진폭·주파수·위상 변조 문서의 그림을 만든다: 39_digital-modulation_fig1.svg, 39_digital-modulation_fig2.svg
# 실행: ~/.venvs/vault-plots/bin/python 39_digital-modulation_plot.py (matplotlib, numpy 필요)
import math
import os
import matplotlib
matplotlib.use("svg")
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import font_manager
HERE = os.path.dirname(os.path.abspath(__file__))
PREFIX = "39_digital-modulation"
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)
BITS = [1, 0, 1, 1, 0]
SPB = 400 # 비트 하나에 점 400개
CYC = 3 # 반송파가 비트 하나 동안 3번 출렁인다 (그림용 값)
def waves(bits):
t = np.arange(len(bits) * SPB) / SPB # 비트 칸 단위 시간
b = np.repeat(bits, SPB)
carrier = np.sin(2 * np.pi * CYC * t)
ask = np.where(b == 1, carrier, 0.0) # 1이면 보내고 0이면 끈다
fsk = np.where(b == 1, np.sin(2 * np.pi * 2 * CYC * t), carrier) # 1이면 두 배 빠르게
psk = np.where(b == 1, -carrier, carrier) # 1이면 반 바퀴 밀린다
return t, ask, fsk, psk
T, ASK, FSK, PSK = waves(BITS)
# 그림 1: 같은 비트 1 0 1 1 0을 세 방법으로 실은 모양
fig, axes = plt.subplots(3, 1, figsize=(6.5, 4.4), sharex=True)
for ax, y, name in zip(axes, [ASK, FSK, PSK], ["진폭 변조", "주파수 변조", "위상 변조"]):
ax.plot(T, y, color=C[0], lw=1.2)
for k in range(1, len(BITS)):
ax.axvline(k, color=INK, lw=0.6, ls=":")
ax.set_ylabel(name, rotation=0, ha="right", va="center", fontsize=10)
ax.set_yticks([])
ax.spines["left"].set_visible(False)
ax.set_ylim(-1.3, 1.3)
for k, b in enumerate(BITS):
axes[0].text(k + 0.5, 1.55, str(b), ha="center", fontsize=11)
axes[-1].set_xticks([])
axes[-1].set_xlabel("시간 (점선 사이가 비트 한 칸)")
save(fig, 1)
# 그림 2: 같은 잡음이 섞일 때, 높이 2단계와 4단계에서 받은 값의 분포
SIGMA = 0.6
rng = np.random.default_rng(3)
LEVELS2 = [0.0, 5.0]
LEVELS4 = [0.0, 1.5, 3.5, 5.0] # 문서의 네 단계 (V)
def received(levels, n=20000):
sent = rng.integers(0, len(levels), n)
return sent, np.array(levels)[sent] + rng.normal(0, SIGMA, n)
def error_rate(levels, sent, rx):
th = [(a + b) / 2 for a, b in zip(levels, levels[1:])]
guess = np.searchsorted(th, rx)
return float(np.mean(guess != sent))
S2, R2 = received(LEVELS2)
S4, R4 = received(LEVELS4)
fig, axes = plt.subplots(2, 1, figsize=(6, 3.8), sharex=True)
bins = np.linspace(-2.5, 7.5, 121)
for ax, levels, sent, rx, title in [(axes[0], LEVELS2, S2, R2, "2단계 (0, 5 V)"),
(axes[1], LEVELS4, S4, R4, "4단계 (0, 1.5, 3.5, 5 V)")]:
for i, lv in enumerate(levels):
ax.hist(rx[sent == i], bins=bins, color=C[i], alpha=0.7)
for a, b in zip(levels, levels[1:]):
ax.axvline((a + b) / 2, color=INK, lw=0.8, ls="--")
ax.set_title(f"{title}: 잘못 읽은 비율 {error_rate(levels, sent, rx) * 100:.1f}%", fontsize=10,
loc="left", color=INK)
ax.set_yticks([])
ax.spines["left"].set_visible(False)
axes[1].set_xlabel("받은 높이 (V). 점선은 받는 쪽이 단계를 가르는 기준")
fig.subplots_adjust(hspace=0.5)
save(fig, 2)
if __name__ == "__main__":
# 그림 1: 칸마다 반송파가 정해진 성질만 바뀐다
cell = lambda y, k: y[k * SPB:(k + 1) * SPB]
assert np.allclose(cell(ASK, 1), 0) and np.allclose(cell(ASK, 0), cell(PSK, 1)) # 0 비트의 위상 = 반송파
assert np.allclose(cell(PSK, 0), -cell(PSK, 1)) # 1 비트는 반 바퀴 밀림
assert np.allclose(cell(FSK, 1), cell(PSK, 1)) and not np.allclose(cell(FSK, 0), cell(FSK, 1))
# 그림 2: 같은 잡음에서 4단계가 2단계보다 훨씬 자주 틀린다 (2단계 거의 0%, 4단계 약 10%)
e2, e4 = error_rate(LEVELS2, S2, R2), error_rate(LEVELS4, S4, R4)
assert e2 < 0.001 and 0.05 < e4 < 0.2, (e2, e4)
# 4단계는 심볼 하나에 lg 4 = 2비트
assert math.log2(len(LEVELS4)) == 2
print("ALL CHECKS PASSED")