入門 · レッスン 5 / 10

デジタル変調とビット誤り率

BPSK・QPSK・16-QAMのシンボルにグレイマッピングでビットを割り当て、雑音中で検出し、ビット誤り率をEb/N0に対してモンテカルロシミュレーションで測定して理論値と比較します

入門45 分更新 2026年11月15日無料期間 2027年1月31日

このレッスンで学ぶこと

  • 3GPP TS 38.211の16QAM規則を用いて、平均シンボルエネルギーが1のBPSK・QPSK・グレイマッピング16-QAMのマッパを実装する
  • グレイマッピングでは最近接点のどの組もちょうど1ビットだけ異なることを、コードで確認する
  • 指定したEb/N0でAWGNチャネルをシミュレーションし、モンテカルロ法でビット誤り率を測定する
  • シミュレーションしたBERを閉形式の式と比較し、BER 1e-5を得るのに各方式が必要とするEb/N0を読み取る

前提レッスン: 雑音・SNR・通信路容量

変調は、ビットを無線機が送信する複素シンボルに変換する処理です。このレッスンでは、どのエンジニアも最初に出会う3つの方式、BPSK・QPSK・16-QAMを実装します。最も起こりやすい誤りが1ビットの損失で済むように、ビットをコンステレーション点へ割り当てる方法を示します。続いて、各方式が雑音中でどれだけの頻度でビットを誤るかを測定します。測定にはモンテカルロシミュレーションを使います。数百万個のランダムなビットを雑音のあるチャネルに通し、誤りを数える方法です。理論も同じ曲線を予測するので、両者を比較すればシミュレータが正しいことを確かめられます。

前提知識は、レッスン02の複素ベースバンドとレッスン04のEb/N0です。

シンボルとコンステレーション

デジタル変調器は一度にkビットを受け取り、M = 2^k 個の複素シンボルのうち1つを出力します。取りうるシンボルの集合をI-Q平面上にプロットしたものが信号点配置(コンステレーション)図です。

  • BPSK(2位相偏移変調)は1シンボルあたり1ビットを運びます。2つの点は実軸上の+1と−1です。ビットが搬送波の符号を決めます。
  • QPSK(4位相偏移変調)は1シンボルあたり2ビットを運びます。4つの点は45°、135°、225°、315°にあります。1ビットがIの符号を、もう1ビットがQの符号を決めるので、QPSKは2つの直交搬送波に載せた2つのBPSK信号と見なせます。
  • 16-QAM(直交振幅変調)は1シンボルあたり4ビットを運びます。16個の点は、各軸のレベルが−3、−1、+1、+3の4 × 4格子を作ります。

公平に比較するため、すべてのコンステレーションは平均シンボルエネルギーが1になるようにスケーリングします。したがって、QPSKの点は√2で、16-QAMの点は√10で割ります。16-QAM格子上でのI² + Q²の平均が10だからです。

5G NRは、マッパを3GPP TS 38.211(NR physical channels and modulation)で定義しています。Release 19版では、変調マッパはπ/2-BPSK、BPSK、QPSK、16QAM、64QAM、256QAM、1024QAMをカバーしています。その16QAM規則では、4ビットの各グループの1番目と3番目のビットでIを、2番目と4番目のビットでQを決めます。スケーリングは同じく√10です。以下のコードはこの規則を使います。NRのBPSKは本レッスンのBPSKを45°回転したもので、誤り率は何も変わりません。

グレイマッピング

どのビットパターンをどの点に割り当てるかは重要です。雑音中で最も起こりやすい誤りは、あるシンボルを最近接点のいずれかと取り違えることです。最近接点とのビットの違いが1ビットだけなら、シンボル誤りは数ビットではなく1ビットの誤りで済みます。この性質を持つラベル付けがグレイマッピングです。図に示すとおり、TS 38.211の規則はこの性質を持っています。

IQ0000000100100011010001010110011110001001101010111100110111101111各軸のレベル: −3, −1, +1, +3ビットの役割b0: Iの符号b1: Qの符号b2: Iの内側/外側b3: Qの内側/外側
3GPP TS 38.211の16QAM規則によるグレイマッピングされた16-QAMコンステレーション(10の平方根の逆数でスケーリングする前)。各点には4ビットb0 b1 b2 b3のラベルを付けている。水平方向または垂直方向の隣接点に移ると、ちょうど1ビットだけ変わる。破線は各軸の0およびプラスマイナス2にある判定境界である。

最初のブロックでは3つのマッパを作り、それぞれの平均エネルギーが1であることを確認し、点間の最小距離を測ります。さらに、すべての最近接点の組について総当たりでグレイ性を確認します。

import itertools
import numpy as np

def map_bpsk(b: np.ndarray) -> np.ndarray:
    return (1 - 2 * b).astype(np.complex128)

def map_qpsk(b: np.ndarray) -> np.ndarray:
    b = b.reshape(-1, 2)
    return ((1 - 2 * b[:, 0]) + 1j * (1 - 2 * b[:, 1])) / np.sqrt(2)

def map_16qam(b: np.ndarray) -> np.ndarray:
    """Same rule as the 16QAM mapper in 3GPP TS 38.211: bits 0 and 2 set I, bits 1 and 3 set Q."""
    b = b.reshape(-1, 4)
    i = (1 - 2 * b[:, 0]) * (2 - (1 - 2 * b[:, 2]))
    q = (1 - 2 * b[:, 1]) * (2 - (1 - 2 * b[:, 3]))
    return (i + 1j * q) / np.sqrt(10)

for name, mapper, k in [("BPSK", map_bpsk, 1), ("QPSK", map_qpsk, 2), ("16-QAM", map_16qam, 4)]:
    labels = np.array(list(itertools.product([0, 1], repeat=k)))
    points = mapper(labels.ravel())
    energy = np.mean(np.abs(points) ** 2)
    dist = np.abs(points[:, None] - points[None, :])
    d_min = dist[dist > 1e-9].min()
    # Gray check: every pair of nearest neighbours differs in exactly one bit
    worst = 0
    for a in range(len(points)):
        for c in range(len(points)):
            if a != c and abs(dist[a, c] - d_min) < 1e-9:
                worst = max(worst, int(np.sum(labels[a] != labels[c])))
    print(f"{name:>6}: {len(points):2d} points, mean energy {energy:.3f}, "
          f"d_min {d_min:.3f}, most bits changed between neighbours: {worst}")

print("16-QAM labels in the first quadrant:")
for lab in itertools.product([0, 1], repeat=4):
    p = map_16qam(np.array(lab)) * np.sqrt(10)
    if p.real > 0 and p.imag > 0:
        print(f"  {''.join(map(str, lab))} -> {p[0].real:+.0f}{p[0].imag:+.0f}j (times 1/sqrt(10))")
  BPSK:  2 points, mean energy 1.000, d_min 2.000, most bits changed between neighbours: 1
  QPSK:  4 points, mean energy 1.000, d_min 1.414, most bits changed between neighbours: 1
16-QAM: 16 points, mean energy 1.000, d_min 0.632, most bits changed between neighbours: 1
16-QAM labels in the first quadrant:
  0000 -> +1+1j (times 1/sqrt(10))
  0001 -> +1+3j (times 1/sqrt(10))
  0010 -> +3+1j (times 1/sqrt(10))
  0011 -> +3+3j (times 1/sqrt(10))

3つのコンステレーションの平均エネルギーはいずれも1.000です。最近接点の組で2ビット以上異なるものはないので、3つともグレイマッピングされています。最小距離は、BPSKの2.000からQPSKの1.414、16-QAMの0.632へと縮みます。この距離の縮小が、同じエネルギーで1シンボルにより多くのビットを詰め込む代償です。

ガウス雑音中での検出

受信機が観測するのはr = s + nです。ここでnは分散N0の複素ガウス雑音で、レッスン04で生成したようにIとQに半分ずつ分かれます。すべてのシンボルが等確率なら、シンボル誤りを最小にする判定は、rに最も近いコンステレーション点を選ぶことです。これらの正方形のコンステレーションでは、最近接点判定は軸ごとの判定に分解できます。16-QAMでは、Iの符号からb0が、|I|が境界2/√10を超えるかどうかからb2が得られます。Qからも同様にb1とb3が得られます。これらの境界が図の破線です。

運ぶビット数が異なる方式を比較するため、誤り率はEb/N0、つまり1ビットあたりのエネルギーと雑音密度の比に対してプロットします。シンボルエネルギーが1で1シンボルあたりkビットなら、Eb = 1/kです。したがって目標のEb/N0に対して、シミュレーションではN0 = (1/k) / (Eb/N0)と設定します。雑音帯域幅がシンボルレートに等しいとき、SNRとの関係はSNR = (Eb/N0) · kです。

理論

BPSKでは、雑音が実部をゼロの反対側へ押しやったときに誤りが起こります。境界までの距離は1、軸あたりの雑音の標準偏差は√(N0/2)なので、次式が得られます。

Pb(BPSK) = Q(√(2 Eb/N0)) = ½ erfc(√(Eb/N0))

ここでQはガウス分布の裾関数です。グレイマッピングしたQPSKは2つの独立したBPSKチャネルで、それぞれがシンボルエネルギーの半分と1ビットを担います。そのため、Eb/N0に対するビット誤り率はBPSKとまったく同じです。

16-QAMでは、各軸は4値の信号です。外側の2つのレベルには隣接する境界が1つ、内側の2つのレベルには2つあります。そのため、軸ごとの判定が誤る確率は約1.5 Q(d/2σ)です。ここでd/2 = 1/√10は最小距離の半分です。グレイマッピングでは、このような誤りはそれぞれ、その軸の2ビットのうち1ビットを反転させます。Eb = 1/4を代入すると、最近接近似が得られます。

Pb(16-QAM) ≈ 3/8 · erfc(√(2 Eb / (5 N0)))

この近似は2レベル跳ぶ誤りを無視していますが、それが問題になるのはEb/N0が非常に低い場合だけです。どちらの式もscipy.special.erfcで計算できます。

モンテカルロ法によるビット誤り率

2番目のブロックでは、各方式について、Eb/N0を0 dBから12 dBまで変えながら、1点あたり1000万個のランダムなビットをチャネルに通します。メモリ使用量を抑えるため、100万ビットずつのチャンクに分けて処理します。ビット誤りを数え、理論値と並べて表示します。チャートには、誤りが20個以上の点だけを載せます。n個の誤りを数えたとき、推定値の相対標準偏差は約1/√nです。したがって誤り20個ならおよそ±22%、100個なら±10%です。

import time
import numpy as np
from scipy.special import erfc

rng = np.random.default_rng(2026)
N_BITS = 10_000_000                      # bits per Eb/N0 point
SCALE_16 = 1 / np.sqrt(10)

def modulate(b: np.ndarray, k: int) -> np.ndarray:
    """Unit-energy symbols: BPSK (k=1), QPSK (k=2), Gray 16-QAM as in TS 38.211 (k=4)."""
    if k == 1:
        return (1 - 2 * b).astype(np.complex128)
    if k == 2:
        b = b.reshape(-1, 2)
        return ((1 - 2 * b[:, 0]) + 1j * (1 - 2 * b[:, 1])) / np.sqrt(2)
    b = b.reshape(-1, 4)
    i = (1 - 2 * b[:, 0]) * (2 - (1 - 2 * b[:, 2]))
    q = (1 - 2 * b[:, 1]) * (2 - (1 - 2 * b[:, 3]))
    return (i + 1j * q) * SCALE_16

def demodulate(r: np.ndarray, k: int) -> np.ndarray:
    """Minimum-distance decisions, done per axis because the constellations are square."""
    if k == 1:
        return (r.real < 0).astype(np.int8)
    if k == 2:
        return np.stack([r.real < 0, r.imag < 0], axis=1).astype(np.int8).ravel()
    thr = 2 * SCALE_16                   # boundary between the inner and outer levels
    return np.stack([r.real < 0, r.imag < 0, np.abs(r.real) > thr, np.abs(r.imag) > thr],
                    axis=1).astype(np.int8).ravel()

def simulate_ber(k: int, ebn0_db: float, chunk: int = 1_000_000) -> tuple[float, int]:
    n0 = (1 / k) / 10 ** (ebn0_db / 10)  # Es = 1, so Eb = 1/k
    errors = 0
    for _ in range(N_BITS // chunk):     # chunks keep memory use small
        bits = rng.integers(0, 2, chunk, dtype=np.int8)
        s = modulate(bits, k)
        noise = np.sqrt(n0 / 2) * (rng.standard_normal(s.size) + 1j * rng.standard_normal(s.size))
        errors += int(np.count_nonzero(demodulate(s + noise, k) != bits))
    return errors / N_BITS, errors

def theory(k: int, ebn0_db: float) -> float:
    g = 10 ** (ebn0_db / 10)
    if k in (1, 2):
        return 0.5 * erfc(np.sqrt(g))                      # exact for BPSK and Gray QPSK
    return 3 / 8 * erfc(np.sqrt(2 * g / 5))                 # nearest-neighbour approximation

start = time.time()
results = {}
print(" Eb/N0 |  BPSK sim  theory   |  QPSK sim  |  16-QAM sim  approx")
for e in range(0, 13):
    row = [simulate_ber(k, e) for k in (1, 2, 4)]
    results[e] = row
    print(f"{e:5d}  | {row[0][0]:9.2e} {theory(1, e):9.2e} | {row[1][0]:9.2e}  "
          f"| {row[2][0]:9.2e} {theory(4, e):9.2e}")

def series(k_index: int, k: int, sim: bool) -> str:
    pts = []
    for e, row in results.items():
        ber, errs = row[k_index]
        if sim and errs >= 20:           # only points with enough errors to trust
            pts.append(f"[{e}, {ber:.3g}]")
        if not sim and theory(k, e) > 1e-7:
            pts.append(f"[{e}, {theory(k, e):.3g}]")
    return ", ".join(pts)

print("BPSK sim:", series(0, 1, True))
print("QPSK sim:", series(1, 2, True))
print("16-QAM sim:", series(2, 4, True))
print("BPSK theory:", series(0, 1, False))
print("16-QAM theory:", series(2, 4, False))
print(f"Eb/N0 for BER 1e-5 (theory): BPSK "
      f"{min(e / 10 for e in range(0, 200) if theory(1, e / 10) < 1e-5):.1f} dB, 16-QAM "
      f"{min(e / 10 for e in range(0, 200) if theory(4, e / 10) < 1e-5):.1f} dB")
print(f"run time {time.time() - start:.0f} s")
 Eb/N0 |  BPSK sim  theory   |  QPSK sim  |  16-QAM sim  approx
    0  |  7.87e-02  7.86e-02 |  7.86e-02  |  1.41e-01  1.39e-01
    1  |  5.62e-02  5.63e-02 |  5.63e-02  |  1.19e-01  1.18e-01
    2  |  3.75e-02  3.75e-02 |  3.75e-02  |  9.77e-02  9.76e-02
    3  |  2.29e-02  2.29e-02 |  2.29e-02  |  7.75e-02  7.74e-02
    4  |  1.25e-02  1.25e-02 |  1.24e-02  |  5.86e-02  5.86e-02
    5  |  5.96e-03  5.95e-03 |  5.99e-03  |  4.20e-02  4.19e-02
    6  |  2.39e-03  2.39e-03 |  2.39e-03  |  2.79e-02  2.79e-02
    7  |  7.81e-04  7.73e-04 |  7.77e-04  |  1.69e-02  1.70e-02
    8  |  1.99e-04  1.91e-04 |  1.92e-04  |  9.25e-03  9.25e-03
    9  |  3.16e-05  3.36e-05 |  3.55e-05  |  4.41e-03  4.39e-03
   10  |  3.90e-06  3.87e-06 |  4.40e-06  |  1.75e-03  1.75e-03
   11  |  3.00e-07  2.61e-07 |  5.00e-07  |  5.58e-04  5.65e-04
   12  |  0.00e+00  9.01e-09 |  0.00e+00  |  1.36e-04  1.39e-04
BPSK sim: [0, 0.0787], [1, 0.0562], [2, 0.0375], [3, 0.0229], [4, 0.0125], [5, 0.00596], [6, 0.00239], [7, 0.000781], [8, 0.000199], [9, 3.16e-05], [10, 3.9e-06]
QPSK sim: [0, 0.0786], [1, 0.0563], [2, 0.0375], [3, 0.0229], [4, 0.0124], [5, 0.00599], [6, 0.00239], [7, 0.000777], [8, 0.000192], [9, 3.55e-05], [10, 4.4e-06]
16-QAM sim: [0, 0.141], [1, 0.119], [2, 0.0977], [3, 0.0775], [4, 0.0586], [5, 0.042], [6, 0.0279], [7, 0.0169], [8, 0.00925], [9, 0.00441], [10, 0.00175], [11, 0.000558], [12, 0.000136]
BPSK theory: [0, 0.0786], [1, 0.0563], [2, 0.0375], [3, 0.0229], [4, 0.0125], [5, 0.00595], [6, 0.00239], [7, 0.000773], [8, 0.000191], [9, 3.36e-05], [10, 3.87e-06], [11, 2.61e-07]
16-QAM theory: [0, 0.139], [1, 0.118], [2, 0.0976], [3, 0.0774], [4, 0.0586], [5, 0.0419], [6, 0.0279], [7, 0.017], [8, 0.00925], [9, 0.00439], [10, 0.00175], [11, 0.000565], [12, 0.000139]
Eb/N0 for BER 1e-5 (theory): BPSK 9.6 dB, 16-QAM 13.5 dB
run time 4 s

シミュレーションと理論は一致しています。6 dBのBPSKでは、シミュレーション値2.39e-03に対して理論値も2.39e-03です。予測どおり、QPSKはBPSKと点ごとに一致します。16-QAMの近似は2レベルの誤りを無視するため、0 dBではシミュレーションよりわずかに低くなります(1.41e-01に対して1.39e-01)。約2 dB以上では、両者はモンテカルロのばらつきの範囲内で一致します。Eb/N0が高い側では誤りの数が少なくなります。11 dBのBPSKは誤り3個に基づくため、チャートから除いています。

1.00e-70.0000010.000010.00010.0010.010.11024681012BPSKシミュレーションQPSKシミュレーション16-QAMシミュレーションBPSK理論値16-QAM近似ビット誤り率Eb/N0 (dB)
加法性白色ガウス雑音(AWGN)中のBPSK・QPSK・グレイマッピングした16-QAMについて、Eb/N0に対するビット誤り率。上記のモンテカルロシミュレーション(1点あたり1000万ビット)の結果を、BPSKの閉形式解および16-QAMの最近接近似とともに示す。QPSKはBPSKの曲線上にある。BER 1e-5を得るには、16-QAMは約3.9 dB多くのEb/N0を必要とする。

曲線の読み方

このチャートから3つのことが読み取れます。

QPSKは「タダ」です。同じEb/N0・同じビット誤り率のまま、BPSKの1シンボルあたりのビット数を2倍にします。BPSKが使わずに空けている直交搬送波を使うことで、これを実現しています。

16-QAMはエネルギーを要します。理論上、BER 1e-5を得るのにBPSKとQPSKは9.6 dB、16-QAMは13.5 dBを必要とし、3.9 dB多くなります。その代わり、16-QAMは1シンボルあたり4ビットを運び、同じ帯域幅でQPSKの2倍になります。より高次の変調もこのトレードオフを続けます。16-QAMから64-QAM、256-QAMへと進むごとに、1シンボルあたりのビット数が増え、より高いSNRが必要になります。適応変調は、現在のSNRで成立する最も高い次数を選びます。これらの式のクイックリファレンスは変調とBERのチートシートです。

符号化なしの変調は限界から遠く離れています。レッスン04では、Eb/N0が−1.59 dBまで下がっても信頼性のある伝送が可能であることを示しました。符号化なしのBPSKはBER 1e-5に9.6 dBを必要とし、約11 dB多くなります。チャネル符号化がこの差の大部分を埋めます。実用システムがこれらの変調を強力な符号と組み合わせるのはそのためです。続きはチャネル符号化ハブで扱います。

実際のモデムにはパルス整形も必要です。これはレッスン06で紹介します。QPSKモデムプロジェクトでは、このシミュレーションをさらに発展させます。ここで示したビット誤り率曲線は、以降のすべての受信機ブロックを評価する基準になります。

演習

  1. 16-QAMのグレイマッピングを自然2進ラベルに置き換えてください。各軸でビット対00、01、10、11をレベル−3、−1、+1、+3に対応させます。グレイ性チェックとBERシミュレーションを再実行してください。Eb/N0が10 dBのとき、BERはどれだけ悪化しますか。
  2. グレイマッピングした8-PSK(円周上に配置し、1シンボルあたり3ビット)を追加してください。その曲線はQPSKと16-QAMの間のどこに位置しますか。
  3. 検出の前に、受信したすべてのQPSKシンボルを10°の固定位相誤差で回転させてください。BER 1e-3において、この誤差はEb/N0で何dBの損失になりますか。
  4. 16-QAMについて、ビット誤り率に加えてシンボル誤り率も計算してください。Eb/N0が高いときの両者の比はいくらで、なぜ4に近いのですか。
  5. N_BITSを10万に変更して再実行してください。シミュレーション曲線はそれぞれ、どのEb/N0から信頼できなくなりますか。

まとめ

  • 変調器はkビットを2^k個の複素シンボルのうち1つに写像します。BPSK・QPSK・16-QAMは1シンボルあたりそれぞれ1、2、4ビットを運び、単位エネルギーでの最小距離はそれぞれ2.000、1.414、0.632です。
  • グレイマッピングでは最近接点同士のラベルが1ビットだけ異なるので、最も起こりやすいシンボル誤りは1ビットの損失で済みます。3GPP TS 38.211の16QAM規則はグレイマッピングです。
  • AWGN中では、BPSKとグレイQPSKでPb = ½ erfc(√(Eb/N0))、グレイ16-QAMで約3/8 erfc(√(2Eb/5N0))です。1点あたり1000万ビットのモンテカルロシミュレーションは両方と一致します。
  • BER 1e-5に必要なEb/N0は、BPSKとQPSKで9.6 dB、16-QAMで13.5 dBです。符号化なしのBPSKはシャノン限界から約11 dB離れており、その差は符号化で取り戻せます。

さらに読む