アイダイアグラムの理論 — 符号間干渉の可視化と波形品質の評価

ディジタル通信システムで「0」と「1」を送るとき、受信側では常に正しくビットを判定できるとは限りません。隣り合うビットの波形が互いに重なり合い、判定を困難にする現象が起こり得ます。この現象を符号間干渉(ISI: Inter-Symbol Interference)と呼びます。

では、通信システムの設計者はISIの影響をどうやって「見る」のでしょうか。実は、受信波形を1シンボル周期ごとに切り出して重ね描きするだけで、通信品質を一目で評価できる強力な手法があります。それがアイダイアグラム(Eye Diagram)です。重ね描きされた波形が「目」の形に見え、この目が大きく開いているほど通信品質が良い — 直感的で美しいこの評価法は、理論的にも非常に深い意味を持っています。

アイダイアグラムを理解すると、以下のような応用に直結します。

  • 通信システムの設計・評価: 変調方式やフィルタの設計パラメータ(ロールオフ率など)が受信波形にどう影響するかを視覚的に判断でき、最適な設計を追求できる
  • 伝送路の品質診断: 光ファイバー通信、無線通信、高速シリアルインターフェース(USB, PCIe, HDMI等)など、あらゆるディジタル伝送路の信号品質をアイダイアグラムで定量評価できる
  • ビット誤り率(BER)の予測: アイ開口の大きさからSN比やBERを推定でき、システムの性能限界を把握できる

本記事の内容

  • 符号間干渉(ISI)の発生メカニズムと問題点
  • ナイキストの第1基準(ISIゼロ条件)の数学的導出
  • 理想ナイキストパルス(sinc関数)の性質と限界
  • コサインロールオフフィルタの設計と周波数特性
  • アイダイアグラムの定義と読み取り方(アイ開口、タイミングマージン、ジッタ、SN比)
  • 帯域制限チャネルでのISIの影響
  • Python実装: 各種フィルタでのアイダイアグラム生成とロールオフ率の比較

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

符号間干渉(ISI)とは

ISIの直感的理解

まず日常的なアナロジーで考えてみましょう。静かな池に石を投げると、波紋が同心円状に広がります。1つの石だけなら波紋はきれいですが、立て続けに石を投げるとどうなるでしょうか。前の石が作った波紋がまだ残っているところに次の波紋が重なり、水面は複雑に乱れます。どの波紋がどの石によるものかを見分けるのが難しくなりますね。

ディジタル通信でも全く同じことが起きます。送信側が「1, 0, 1, 1, 0, …」というビット列を送るとき、各ビットに対応するパルス波形がチャネル(伝送路)を通過します。チャネルには帯域制限があるため、パルスは時間的に広がります。シンボルレート(ビットの送出速度)が高くなると、前のパルスの「裾」がまだ消えていないうちに次のパルスが到着し、波形が重なり合ってしまいます。これが符号間干渉(ISI)です。

ISIの数学的定式化

ディジタル通信の基本モデルでは、送信信号 $s(t)$ は次のように表されます。

$$ s(t) = \sum_{k=-\infty}^{\infty} a_k \, p(t – kT) $$

ここで $a_k$ は $k$ 番目のシンボルの値(例: 2値通信なら $+1$ または $-1$)、$p(t)$ は送信パルス波形、$T$ はシンボル周期です。この式は「各シンボルに対応するパルスを時間軸上に並べたもの」を表しています。

受信側では、チャネルの伝達関数やノイズの影響を受けた後、受信フィルタを通して信号が得られます。送信パルス $p(t)$、チャネルの応答 $c(t)$、受信フィルタ $g(t)$ の畳み込みを総合したパルス応答を $h(t) = p(t) * c(t) * g(t)$ とすると、受信信号は次のようになります。

$$ y(t) = \sum_{k=-\infty}^{\infty} a_k \, h(t – kT) + n(t) $$

ここで $n(t)$ はノイズ成分です。

受信側では時刻 $t = mT$($m$ 番目のシンボルの判定タイミング)でサンプリングを行います。このときの受信値は次のとおりです。

$$ y(mT) = \sum_{k=-\infty}^{\infty} a_k \, h(mT – kT) = a_m \, h(0) + \sum_{k \neq m} a_k \, h((m-k)T) + n(mT) $$

右辺の第1項 $a_m \, h(0)$ が「本来受信したいシンボル $a_m$ の成分」であり、第2項 $\sum_{k \neq m} a_k \, h((m-k)T)$ が「他のシンボルからの干渉(ISI)」です。ISIがゼロになるための条件は明確で、次を満たせばよいことがわかります。

$$ h(nT) = \begin{cases} 1 & (n = 0) \\ 0 & (n \neq 0) \end{cases} $$

つまり、総合パルス応答 $h(t)$ がシンボル周期 $T$ の整数倍の時刻で、$n=0$ 以外ではすべてゼロになればよいのです。これがナイキストの第1基準(ISIゼロ条件)の出発点です。ここで $h(0) = 1$ としたのは正規化のためであり、本質的な条件は $n \neq 0$ でゼロになることです。

ISIの問題が明確になったところで、次にこのISIゼロ条件を周波数領域で表現し、具体的にどのようなパルス波形がISIを完全にゼロにできるのかを導出しましょう。

ナイキストの第1基準の導出

時間領域の条件から周波数領域へ

ナイキストの第1基準は、ISIゼロ条件を周波数領域で等価に表現したものです。前節で示した時間領域の条件を周波数領域に変換することで、どのような周波数特性を持つパルスがISIフリーなのかが明らかになります。

$h(t)$ のフーリエ変換を $H(f)$ とすると、$h(t)$ は次のように書けます。

$$ h(t) = \int_{-\infty}^{\infty} H(f) \, e^{j2\pi ft} \, df $$

$t = nT$ を代入すると次のようになります。

$$ h(nT) = \int_{-\infty}^{\infty} H(f) \, e^{j2\pi fnT} \, df $$

ここで、積分区間を $1/T$ ごとの区間に分割するテクニックを使います。$f = f’ + m/T$($m$ は整数、$f’$ は $[-1/(2T), \, 1/(2T)]$ の範囲)と変数置換すると次のとおりです。

$$ h(nT) = \sum_{m=-\infty}^{\infty} \int_{-1/(2T)}^{1/(2T)} H\!\left(f’ + \frac{m}{T}\right) e^{j2\pi (f’ + m/T) nT} \, df’ $$

指数関数の中の $e^{j2\pi (m/T) nT} = e^{j2\pi mn} = 1$($m, n$ は整数)であることを利用すると、式は次のように簡略化されます。

$$ h(nT) = \int_{-1/(2T)}^{1/(2T)} \left[\sum_{m=-\infty}^{\infty} H\!\left(f’ + \frac{m}{T}\right)\right] e^{j2\pi f’ nT} \, df’ $$

ここで、角括弧の中の和を新しい関数として定義します。

$$ H_{\text{fold}}(f) = \sum_{m=-\infty}^{\infty} H\!\left(f + \frac{m}{T}\right) $$

この $H_{\text{fold}}(f)$ は、$H(f)$ を周波数軸上で $1/T$ ごとにずらして足し合わせたもので、折り返しスペクトルと呼ばれます。この関数は周期 $1/T$ の周期関数です。

以上をまとめると次の関係が得られます。

$$ h(nT) = \int_{-1/(2T)}^{1/(2T)} H_{\text{fold}}(f) \, e^{j2\pi fnT} \, df $$

ISIゼロ条件の周波数領域表現

ISIゼロ条件 $h(nT) = \delta_{n,0}$(クロネッカーのデルタ)が成り立つためには、$H_{\text{fold}}(f)$ がどのような関数であればよいでしょうか。

ここで、$h(nT) = \delta_{n,0}$ の右辺もフーリエ係数として表現してみましょう。区間 $[-1/(2T), 1/(2T)]$ 上の定数関数 $T$ のフーリエ係数を計算すると次のようになります。

$$ \int_{-1/(2T)}^{1/(2T)} T \cdot e^{j2\pi fnT} \, df = T \cdot \frac{e^{j2\pi fnT}}{j2\pi nT} \Bigg|_{-1/(2T)}^{1/(2T)} $$

$n \neq 0$ の場合、この積分は $\text{sinc}(n) = 0$ となり、$n = 0$ の場合は $T \cdot 1/T = 1$ となります。つまり、定数関数 $T$ のフーリエ係数はちょうど $\delta_{n,0}$ です。

したがって、$H_{\text{fold}}(f) = T$(定数)であれば、$h(nT) = \delta_{n,0}$ が成立します。これがナイキストの第1基準です。

$$ \boxed{\sum_{m=-\infty}^{\infty} H\!\left(f + \frac{m}{T}\right) = T, \quad |f| \leq \frac{1}{2T}} $$

この条件の幾何学的な意味は非常に明快です。$H(f)$ のスペクトルを $1/T$ ごとにシフトしたコピーを全部足し合わせると、$[-1/(2T), 1/(2T)]$ の区間で一定値 $T$ になるということです。言い換えると、スペクトルの「折り返し」が互いにぴったり補い合って平坦になればよいのです。

ナイキストの第1基準を満たす最もシンプルなパルスは何でしょうか。次に、理想的なケースとして sinc 関数について見ていきましょう。

理想ナイキストパルス — sinc関数

矩形スペクトルとsinc関数の関係

ナイキストの第1基準を最も単純に満たすスペクトルは、帯域 $[-1/(2T), 1/(2T)]$ を隙間なく埋め、かつはみ出さない矩形スペクトルです。

$$ H_{\text{ideal}}(f) = \begin{cases} T & |f| \leq \frac{1}{2T} \\ 0 & |f| > \frac{1}{2T} \end{cases} $$

なぜこれがナイキスト基準を満たすのか、確認してみましょう。このスペクトルは帯域 $1/(2T)$ に収まっているため、$m \neq 0$ のシフトコピー $H_{\text{ideal}}(f + m/T)$ は区間 $[-1/(2T), 1/(2T)]$ 上でゼロです。したがって折り返しスペクトルは $H_{\text{fold}}(f) = H_{\text{ideal}}(f) = T$ となり、基準が成立します。

この矩形スペクトルを逆フーリエ変換すると、時間領域のパルス波形が得られます。

$$ h_{\text{ideal}}(t) = \int_{-1/(2T)}^{1/(2T)} T \cdot e^{j2\pi ft} \, df $$

被積分関数を展開して積分を実行します。

$$ h_{\text{ideal}}(t) = T \cdot \frac{e^{j2\pi ft}}{j2\pi t}\Bigg|_{-1/(2T)}^{1/(2T)} = T \cdot \frac{e^{j\pi t/T} – e^{-j\pi t/T}}{j2\pi t} $$

オイラーの公式 $\sin\theta = (e^{j\theta} – e^{-j\theta}) / (2j)$ を適用すると次のようになります。

$$ h_{\text{ideal}}(t) = T \cdot \frac{2j \sin(\pi t / T)}{j 2\pi t} = \frac{\sin(\pi t / T)}{\pi t / T} = \text{sinc}\!\left(\frac{t}{T}\right) $$

ここで $\text{sinc}(x) = \sin(\pi x) / (\pi x)$ です。

sinc関数の性質とISIゼロの確認

sinc関数の最も重要な性質は、$t = nT$($n$ は $0$ でない整数)でゼロ交差することです。

$$ \text{sinc}\!\left(\frac{nT}{T}\right) = \text{sinc}(n) = \frac{\sin(n\pi)}{n\pi} = 0 \quad (n \neq 0) $$

そして $t = 0$ では $\text{sinc}(0) = 1$ です。まさにISIゼロ条件を満たしています。各シンボルの判定時刻において、他のシンボルからの干渉がゼロになるのです。

sinc関数の限界

理論的には完璧な sinc パルスですが、実用上は2つの致命的な問題があります。

第1の問題: 無限の時間広がり。sinc関数は $t \to \pm \infty$ で $1/t$ に比例して減衰しますが、厳密にはゼロにはなりません。つまり、パルスの「裾」が無限に広がり、因果的で有限長のフィルタとして実装することができません。

第2の問題: 減衰の遅さとタイミング感度。sinc関数の裾は $1/t$ でしか減衰しないため、サンプリング時刻がわずかにずれただけでISIが大きくなります。実際の通信システムではクロック同期に微小な誤差(ジッタ)が避けられないため、sinc パルスのようにゼロ交差点のマージンが狭いパルスは実用的ではありません。さらに、$1/t$ 減衰では裾の絶対和 $\sum |h(nT + \epsilon)|$ が発散する可能性があり、わずかなタイミング誤差 $\epsilon$ で累積ISIが無限大になり得ます。

これらの問題を解決するには、矩形スペクトルを「少しだけ広げて」なめらかにロールオフさせることで、時間領域の減衰を速くすればよいのです。この発想から生まれたのがコサインロールオフフィルタです。

コサインロールオフフィルタ(レイズドコサインフィルタ)

設計の発想

sinc 関数の問題は、周波数領域でスペクトルが「角張って」いることに起因します。矩形スペクトルの急峻なカットオフが、時間領域の遅い減衰を引き起こしているのです。そこで、カットオフ部分を余弦(コサイン)関数で滑らかにつなぐという自然なアイデアが生まれます。

ナイキストの第1基準を満たしつつ、スペクトルの端をなめらかにロールオフさせるフィルタをコサインロールオフフィルタ(レイズドコサインフィルタとも呼ばれます)と言います。

周波数特性

コサインロールオフフィルタのスペクトル $H_{\text{RC}}(f)$ は、ロールオフ率 $\alpha$($0 \leq \alpha \leq 1$)をパラメータとして、次のように定義されます。

$$ H_{\text{RC}}(f) = \begin{cases} T & |f| \leq \frac{1-\alpha}{2T} \\ \frac{T}{2}\left[1 + \cos\!\left(\frac{\pi T}{\alpha}\left(|f| – \frac{1-\alpha}{2T}\right)\right)\right] & \frac{1-\alpha}{2T} < |f| \leq \frac{1+\alpha}{2T} \\ 0 & |f| > \frac{1+\alpha}{2T} \end{cases} $$

この式は3つの領域に分かれています。

平坦領域 $|f| \leq (1-\alpha)/(2T)$: スペクトルが定数 $T$ で平坦な部分です。ここが信号のエネルギーの中心帯域です。

ロールオフ領域 $(1-\alpha)/(2T) < |f| \leq (1+\alpha)/(2T)$: コサイン関数で滑らかにゼロへ遷移する部分です。この領域の幅が $\alpha/T$ で、$\alpha$ が大きいほど遷移が緩やかになります。

阻止域 $|f| > (1+\alpha)/(2T)$: スペクトルがゼロの部分です。

ロールオフ率の意味

ロールオフ率 $\alpha$ は、矩形スペクトルからの帯域の「はみ出し」を制御するパラメータです。

  • $\alpha = 0$: 矩形スペクトル(sinc パルス)に一致。帯域幅は最小の $1/(2T)$ だが、時間減衰が最も遅い
  • $\alpha = 1$: 帯域幅は $1/T$(sinc の2倍)に広がるが、時間減衰が最も速い($1/t^3$ 減衰)
  • $0 < \alpha < 1$: 帯域幅と時間減衰のトレードオフ

このトレードオフは通信システム設計の本質的な問題です。帯域が限られた環境(例: 混雑した無線周波数帯)ではできるだけ $\alpha$ を小さくして帯域効率を高めたいですが、タイミング誤差への耐性を考えると $\alpha$ を大きくしたい。設計者はこのバランスを取る必要があります。

ナイキスト基準の検証

コサインロールオフフィルタがナイキストの第1基準を満たすことを確認しましょう。ロールオフ領域のスペクトルは、$f = 1/(2T)$ に関して奇対称に設計されています。つまり、$f = 1/(2T)$ を中心にコサインの半波が配置されており、$H_{\text{RC}}(1/(2T)) = T/2$ となります。

折り返しスペクトルを計算すると、ロールオフ領域で $H_{\text{RC}}(f)$ と $H_{\text{RC}}(f – 1/T)$ が互いに補い合い、その和が常に $T$ になります。具体的に、ロールオフ領域の任意の $f$ について次が成立します。

$$ H_{\text{RC}}(f) + H_{\text{RC}}\!\left(f – \frac{1}{T}\right) = T $$

これはコサイン関数の性質 $\cos(\theta) + \cos(\pi – \theta) = 0$ に由来します。ロールオフ部分で「削られた」エネルギーが、隣のシフトコピーの「盛り上がった」部分でちょうど補填されるわけです。したがって折り返しスペクトルは全域で $T$ となり、ナイキスト基準が満たされます。

時間領域のパルス波形

コサインロールオフフィルタの逆フーリエ変換は、次のように閉じた形で表されます。

$$ h_{\text{RC}}(t) = \text{sinc}\!\left(\frac{t}{T}\right) \cdot \frac{\cos(\pi \alpha t / T)}{1 – (2\alpha t / T)^2} $$

この式は2つの因子の積として理解できます。

第1因子 $\text{sinc}(t/T)$: 理想ナイキストパルス。シンボル周期 $T$ の整数倍でゼロ交差します。

第2因子 $\cos(\pi \alpha t / T) / [1 – (2\alpha t / T)^2]$: ロールオフによる補正項。この因子の効果で、パルスの裾が $1/t^3$ で減衰するようになります($\alpha > 0$ の場合)。$1/t$ から $1/t^3$ への減衰の加速は劇的で、タイミング誤差に対する耐性が大幅に向上します。

$\alpha = 0$ のとき第2因子は恒等的に $1$ となり、sinc パルスに帰着します。$\alpha = 1$ のときは次のようにさらに簡潔な形になります。

$$ h_{\text{RC}}(t)\big|_{\alpha=1} = \frac{\sin(\pi t / T)}{\pi t / T} \cdot \frac{\cos(\pi t / T)}{1 – (2t/T)^2} = \frac{\cos(\pi t / T)}{1 – (2t/T)^2} \cdot \text{sinc}\!\left(\frac{t}{T}\right) $$

ここまでで、ISIを発生させないパルス波形の設計方法がわかりました。しかし、理論的にISIゼロを達成するフィルタを使っても、実際の受信波形は本当にきれいなのでしょうか? ノイズやタイミング誤差の影響はどの程度なのでしょうか? これらを一目で評価できる手法がアイダイアグラムです。

アイダイアグラムとは

アイダイアグラムの生成方法

アイダイアグラムの作り方は驚くほどシンプルです。次の3ステップで生成できます。

  1. 受信波形を取得する: 実際に受信した(またはシミュレーションで生成した)ベースバンド波形 $y(t)$ を連続的に記録する
  2. 1シンボル周期(または2シンボル周期)ごとに切り出す: 波形を長さ $T$(または $2T$)の区間に分割する
  3. すべての区間を同じ時間軸上に重ね描きする: オシロスコープのトリガと同じ発想で、切り出した波形を全部重ねて表示する

歴史的には、アナログオシロスコープのトリガ機能を使ってシンボルクロックに同期させ、受信波形を画面上に重ね描きすることで生成していました。波形が重なった結果、表示パターンが人間の「目」のような形に見えることから、アイダイアグラム(eye diagram / eye pattern)と名付けられました。

なぜ「目」の形になるのか

直感的に理解するために、最も単純な場合を考えましょう。2値通信(NRZ: Non-Return-to-Zero)で、ISIが全くない理想的なケースです。

送信パルスがシンボル周期 $T$ の間だけ $+1$ または $-1$ の矩形波であれば、受信波形の各シンボル区間は上($+1$)か下($-1$)の水平線です。これを重ねると、上と下に水平線が集まり、中央に大きな空白ができます。これが「完全に開いた目」です。

実際にはパルス整形フィルタによって波形は滑らかに遷移し、ノイズも加わるため、$+1$ と $-1$ の間の遷移部分で波形が扇状に広がります。この広がりが目の左右の端に相当し、中央部分(判定タイミング付近)では波形が $+1$ 付近と $-1$ 付近に集中して「開いた目」が形成されます。

ISIやノイズが増えると、本来 $+1$ であるべき波形が下がったり、$-1$ であるべき波形が上がったりして、目が閉じていきます。目が完全に閉じたとき、受信波形の振幅分布が上下で完全に重なり、もはやビットの判定が不可能になります。

アイダイアグラムの形状から通信品質の多くの情報を読み取ることができます。次に、具体的にどのような指標が読み取れるのかを詳しく見ていきましょう。

アイダイアグラムから読み取れる情報

アイダイアグラムは単なる「見た目がきれいかどうか」の定性的評価ツールではありません。以下の定量的な指標を読み取ることができます。

アイ開口(Eye Opening)

アイダイアグラムの最も重要な指標がアイ開口です。最適判定タイミング(通常はシンボル周期の中央)における「目」の縦方向の開き幅を指します。

2値通信の場合、理想的にはアイ開口は $2A$($A$ はパルス振幅)ですが、ISIとノイズによって減少します。アイ開口が大きいほど、受信側でのビット判定のマージンが大きく、ビット誤り率(BER)が低くなります。

数学的に表現すると、ISIのみを考慮した場合のアイ開口は次のようになります。

$$ \text{Eye Opening} = 2\left(|h(0)| – \sum_{k \neq 0} |h(kT)|\right) $$

ここで $\sum_{k \neq 0} |h(kT)|$ は最悪ケースのISIを表します。すべての隣接シンボルが同時に最も不利な符号の組み合わせをとったときの干渉の総和です。

タイミングマージン(Timing Margin)

目の横方向の幅がタイミングマージンです。アイが「開いている」水平方向の範囲を示し、サンプリング時刻のずれ(タイミング誤差)にどれだけ耐えられるかを表します。

タイミングマージンが広いほど、クロック同期回路のジッタに対する耐性が高いということです。sinc パルスはアイ開口が大きくてもタイミングマージンが非常に狭いのに対し、ロールオフ率 $\alpha$ を大きくしたコサインロールオフフィルタはタイミングマージンが広くなります。

ジッタ(Jitter)

アイダイアグラムの左右の交差点(ゼロクロッシング点)の水平方向の広がりがジッタを示します。理想的にはゼロクロッシングは1点に集中しますが、ISIやノイズの影響で複数の時刻に分散します。

ジッタは主に2種類に分類されます。

  • 確定性ジッタ(Deterministic Jitter): ISI、デューティサイクル歪み、クロストークなど、再現性のある要因によるジッタ。アイダイアグラム上では、ゼロクロッシングの分布の端(バウンダリ)として現れる
  • ランダムジッタ(Random Jitter): 熱雑音、ショット雑音など統計的な揺らぎによるジッタ。ガウス分布に従い、アイダイアグラム上ではゼロクロッシングの裾(テール)として現れる

SN比とBERの推定

アイ開口の大きさは、実効的なSN比(信号対雑音比)に直結します。判定閾値(通常はゼロ)からアイの上端/下端までの距離がノイズマージンであり、これとノイズの標準偏差の比からBERを推定できます。

2値通信で加法的ガウスノイズを仮定した場合、BERは次のQ関数で表されます。

$$ \text{BER} = Q\!\left(\frac{d_{\min}}{2\sigma_n}\right) $$

ここで $d_{\min}$ はアイ開口(最小垂直距離)、$\sigma_n$ はノイズの標準偏差です。Q関数は標準正規分布の上側確率 $Q(x) = \frac{1}{\sqrt{2\pi}} \int_x^{\infty} e^{-u^2/2} \, du$ です。アイ開口が大きいほど $d_{\min} / (2\sigma_n)$ が大きくなり、BERが指数関数的に減少します。

ノイズマージンと判定閾値の最適化

アイダイアグラム上で目の上端(正のシンボル群の下限)と下端(負のシンボル群の上限)が非対称になることがあります。これは正と負のシンボルに対するISIの影響が異なる場合に発生し、判定閾値をゼロから上下にシフトさせることでBERを最適化できることを示唆します。

これらの指標の理解が深まったところで、次は帯域制限がISIに与える影響を理論的に分析しましょう。

帯域制限によるISIの影響

帯域制限チャネルの問題

実際の通信チャネルには必ず帯域制限があります。チャネルの帯域幅が $W$ のとき、理想的にはシンボルレート $R_s = 1/T$ は最大 $2W$ シンボル/秒まで伝送できます(ナイキストレート)。しかし、これはISIゼロの理想ナイキストパルス(sinc関数)が使える場合の理論上限です。

コサインロールオフフィルタを使う場合、帯域幅 $W$ のチャネルで伝送できるシンボルレートは次のようになります。

$$ R_s = \frac{2W}{1 + \alpha} $$

ロールオフ率 $\alpha$ が大きくなるほど、同じ帯域で伝送できるシンボルレートが低下します。$\alpha = 0$ ならば $R_s = 2W$(理論上限)ですが、$\alpha = 1$ では $R_s = W$ と半分になってしまいます。

帯域効率(bits/s/Hz)を $\eta$ とすると、2値通信の場合は次のようになります。

$$ \eta = \frac{R_b}{2W} = \frac{R_s}{2W} = \frac{1}{1 + \alpha} \quad [\text{bits/s/Hz}] $$

ここで $R_b$ はビットレートで、2値通信では $R_b = R_s$ です。

ISIの定量的評価 — ピークディストーション

帯域が不足している場合やフィルタがナイキスト基準を完全には満たさない場合、ISIの大きさを定量化する必要があります。最もよく使われる指標がピークディストーション(Peak Distortion)です。

$$ D_{\text{peak}} = \frac{1}{|h(0)|} \sum_{k \neq 0} |h(kT)| $$

これはISIの最悪ケースを正規化した値で、$D_{\text{peak}} < 1$ であればアイは開いており、$D_{\text{peak}} \geq 1$ ではアイが閉じてビット判定が不可能になります。

ISIの確率的評価

ピークディストーションは最悪ケースの評価ですが、実際にはすべてのシンボルが同時に最も不利な符号をとる確率は低いです。ISIの統計的な性質を考慮したRMS(二乗平均平方根)ISIも有用な指標です。

シンボル $a_k$ が独立で等確率($+1$ と $-1$)のとき、ISIの分散は次のとおりです。

$$ \sigma_{\text{ISI}}^2 = \sum_{k \neq 0} |h(kT)|^2 $$

この式は、ISIが多数の独立なランダム変数の和であることから、中心極限定理によりガウス分布に近づくことを示しています。したがって、ISIの影響をノイズと同様に扱い、等価ノイズとして $\sigma_n^2 + \sigma_{\text{ISI}}^2$ を考えることができます。

マッチドフィルタとの関係

送信フィルタと受信フィルタの設計には、ノイズ耐性を最大化するマッチドフィルタの概念が関わります。AWGN(加法的白色ガウスノイズ)チャネルでSN比を最大化するには、受信フィルタを送信パルスの時間反転複素共役にすべきです。

しかし、送受合わせてナイキスト基準を満たす必要があるため、ルートレイズドコサイン(Root Raised Cosine: RRC)フィルタが使われます。送信フィルタと受信フィルタの両方をRRCフィルタにすると、総合パルス応答がレイズドコサインフィルタになり、ISIゼロ条件とマッチドフィルタ条件の両方を同時に満たします。

$$ H_{\text{RRC}}(f) = \sqrt{H_{\text{RC}}(f)} $$

送信側で $H_{\text{RRC}}(f)$ によるパルス整形を行い、受信側で同じ $H_{\text{RRC}}(f)$ のマッチドフィルタを適用すると、総合応答は次のようになります。

$$ H_{\text{total}}(f) = H_{\text{RRC}}(f) \cdot H_{\text{RRC}}(f) = H_{\text{RC}}(f) $$

これによりISIゼロと最大SN比が両立します。

帯域制限とISIの理論的な関係が明確になりました。ここからは、これらの理論をPythonで実装し、アイダイアグラムを実際に生成してみましょう。理論の予測が可視化で確認できるはずです。

Pythonでの実装 — パルス波形とスペクトルの可視化

レイズドコサインパルスの実装

まず、コサインロールオフフィルタの時間領域パルスと周波数特性を実装し、ロールオフ率による違いを可視化します。

import numpy as np
import matplotlib.pyplot as plt

def raised_cosine_pulse(t, T, alpha):
    """レイズドコサインパルスの時間波形を計算する"""
    h = np.zeros_like(t, dtype=float)
    for i, ti in enumerate(t):
        x = ti / T
        if np.isclose(ti, 0):
            h[i] = 1.0
        elif alpha > 0 and np.isclose(abs(2 * alpha * x), 1.0):
            # 分母がゼロになる特異点: L'Hopitalの定理で極限値を求める
            h[i] = (alpha / 2.0) * np.sin(np.pi / (2 * alpha))
        else:
            sinc_part = np.sinc(x)  # np.sinc(x) = sin(pi*x)/(pi*x)
            cos_part = np.cos(np.pi * alpha * x) / (1 - (2 * alpha * x)**2)
            h[i] = sinc_part * cos_part
    return h

def raised_cosine_spectrum(f, T, alpha):
    """レイズドコサインフィルタの周波数特性"""
    H = np.zeros_like(f, dtype=float)
    f1 = (1 - alpha) / (2 * T)
    f2 = (1 + alpha) / (2 * T)
    for i, fi in enumerate(f):
        af = abs(fi)
        if af <= f1:
            H[i] = T
        elif af <= f2:
            H[i] = T / 2 * (1 + np.cos(np.pi * T / alpha * (af - f1)))
        else:
            H[i] = 0.0
    return H

# パラメータ
T = 1.0  # シンボル周期
t = np.linspace(-6 * T, 6 * T, 2001)
f = np.linspace(-1.5 / T, 1.5 / T, 2001)
alphas = [0.0, 0.25, 0.5, 0.75, 1.0]

# 時間波形のプロット
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

for alpha in alphas:
    h = raised_cosine_pulse(t, T, alpha)
    axes[0].plot(t / T, h, label=f'α = {alpha}')

axes[0].set_xlabel('t / T')
axes[0].set_ylabel('h(t)')
axes[0].set_title('Raised Cosine Pulse (Time Domain)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
axes[0].set_xlim(-6, 6)
axes[0].axhline(y=0, color='k', linewidth=0.5)
# ゼロ交差点を示す
for n in range(-5, 6):
    if n != 0:
        axes[0].axvline(x=n, color='gray', linewidth=0.3, linestyle='--')

# 周波数特性のプロット
for alpha in alphas:
    H = raised_cosine_spectrum(f, T, alpha)
    axes[1].plot(f * T, H / T, label=f'α = {alpha}')

axes[1].set_xlabel('f × T')
axes[1].set_ylabel('H(f) / T')
axes[1].set_title('Raised Cosine Spectrum (Frequency Domain)')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
axes[1].set_xlim(-1.5, 1.5)

plt.tight_layout()
plt.savefig('raised_cosine_pulse_spectrum.png', dpi=150, bbox_inches='tight')
plt.show()

上のグラフから、ロールオフ率 $\alpha$ がパルス波形とスペクトルの双方に与える影響が明確に読み取れます。

  1. 時間領域(左図): $\alpha = 0$(sinc パルス)は $t/T$ の整数点でゼロ交差しますが、裾の減衰が遅く、ピークとゼロの間を大きく振動しています。$\alpha$ を大きくするほどメインローブが広がる一方で、裾(サイドローブ)が急速に減衰し、$\alpha = 1$ では $|t/T| > 1$ の範囲でほぼゼロに収束しています。この速い減衰こそが、タイミング誤差への耐性を向上させる鍵です。
  2. 周波数領域(右図): $\alpha = 0$ は帯域 $|f| \leq 1/(2T)$ の矩形スペクトルであり、最小帯域幅ですが急峻なカットオフを持ちます。$\alpha$ の増加に伴いスペクトルの端がコサイン関数で滑らかにロールオフし、占有帯域幅が $1/(2T)$ から $(1+\alpha)/(2T)$ に広がります。帯域効率と時間応答のトレードオフが視覚的に確認できます。

次に、これらのパルスを用いて実際にアイダイアグラムを生成してみましょう。

Pythonでの実装 — アイダイアグラムの生成

基本的なアイダイアグラムの生成

ランダムなビット列からベースバンド信号を生成し、1シンボル周期ごとに切り出して重ね描きするコードを実装します。

import numpy as np
import matplotlib.pyplot as plt

def raised_cosine_pulse(t, T, alpha):
    """レイズドコサインパルスの時間波形を計算する"""
    h = np.zeros_like(t, dtype=float)
    for i, ti in enumerate(t):
        x = ti / T
        if np.isclose(ti, 0):
            h[i] = 1.0
        elif alpha > 0 and np.isclose(abs(2 * alpha * x), 1.0):
            h[i] = (alpha / 2.0) * np.sin(np.pi / (2 * alpha))
        else:
            sinc_part = np.sinc(x)
            cos_part = np.cos(np.pi * alpha * x) / (1 - (2 * alpha * x)**2)
            h[i] = sinc_part * cos_part
    return h

def generate_baseband_signal(bits, T, alpha, samples_per_symbol):
    """ビット列からベースバンド信号を生成する"""
    # NRZシンボルマッピング: 0 -> -1, 1 -> +1
    symbols = 2 * bits - 1
    N_symbols = len(symbols)

    # パルスの時間範囲(両側に余裕を持たせる)
    pulse_span = 8  # シンボル数
    t_pulse = np.arange(-pulse_span * samples_per_symbol,
                         pulse_span * samples_per_symbol + 1) / samples_per_symbol * T
    pulse = raised_cosine_pulse(t_pulse, T, alpha)

    # 信号の総サンプル数
    total_samples = N_symbols * samples_per_symbol
    signal = np.zeros(total_samples + len(pulse) - 1)

    # 各シンボルのパルスを重ね合わせる
    for k, ak in enumerate(symbols):
        start = k * samples_per_symbol
        signal[start:start + len(pulse)] += ak * pulse

    return signal

def plot_eye_diagram(signal, samples_per_symbol, num_traces, title, ax):
    """アイダイアグラムをプロットする"""
    # 2シンボル周期分を切り出して重ね描き
    trace_len = 2 * samples_per_symbol
    offset = 4 * samples_per_symbol  # 先頭の過渡応答をスキップ

    t_axis = np.linspace(-1, 1, trace_len)
    for i in range(num_traces):
        start = offset + i * samples_per_symbol
        if start + trace_len > len(signal):
            break
        trace = signal[start:start + trace_len]
        ax.plot(t_axis, trace, color='cyan', alpha=0.05, linewidth=0.5)

    ax.set_xlabel('t / T')
    ax.set_ylabel('Amplitude')
    ax.set_title(title)
    ax.set_xlim(-1, 1)
    ax.grid(True, alpha=0.3)
    ax.axhline(y=0, color='white', linewidth=0.5, alpha=0.5)
    ax.axvline(x=0, color='white', linewidth=0.5, alpha=0.5)

# パラメータ
T = 1.0
samples_per_symbol = 32
num_bits = 2000
np.random.seed(42)
bits = np.random.randint(0, 2, num_bits)

# ロールオフ率ごとにアイダイアグラムを生成
alphas = [0.0, 0.25, 0.5, 1.0]
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.patch.set_facecolor('black')

for ax, alpha in zip(axes.flat, alphas):
    ax.set_facecolor('black')
    signal = generate_baseband_signal(bits, T, alpha, samples_per_symbol)
    plot_eye_diagram(signal, samples_per_symbol, 500,
                     f'Eye Diagram (α = {alpha})', ax)
    ax.tick_params(colors='white')
    ax.xaxis.label.set_color('white')
    ax.yaxis.label.set_color('white')
    ax.title.set_color('white')

plt.tight_layout()
plt.savefig('eye_diagram_rolloff_comparison.png', dpi=150,
            bbox_inches='tight', facecolor='black')
plt.show()

上の4つのアイダイアグラムから、ロールオフ率がアイの形状に与える影響がはっきりと確認できます。

  1. $\alpha = 0$(sinc パルス): アイは垂直方向には最大限に開いていますが、ゼロクロッシングが非常にシャープで、水平方向のマージン(タイミングマージン)が極めて狭いです。サンプリング時刻が少しでもずれると、ISIの影響が急激に増大します。
  2. $\alpha = 0.25$: $\alpha = 0$ に比べてゼロクロッシング付近の波形トレースの広がりがわずかに緩やかになり、タイミングマージンが改善されています。一方、アイ開口の垂直方向の幅は依然として十分に開いています。
  3. $\alpha = 0.5$: タイミングマージンがさらに広がり、実用的なバランスの良い形状です。多くの通信規格(DVB-S2, LTE等)でこの付近のロールオフ率が採用されています。
  4. $\alpha = 1.0$: アイの「目」の形が最も丸みを帯び、タイミングマージンが最大です。しかし占有帯域幅は sinc の2倍に広がっており、帯域効率は $0.5$ bits/s/Hz に低下しています。ゼロクロッシングが1点に集中しており、ジッタが最小であることも確認できます。

理論で予測した「$\alpha$ が大きいほどタイミングマージンが広がるが帯域効率が低下する」というトレードオフが、アイダイアグラムで視覚的に裏付けられました。

ノイズがアイダイアグラムに与える影響

AWGN環境でのアイダイアグラム

実際の通信では常にノイズが存在します。AWGN(加法的白色ガウスノイズ)環境でアイダイアグラムがどのように変化するかを確認しましょう。

import numpy as np
import matplotlib.pyplot as plt

def raised_cosine_pulse(t, T, alpha):
    """レイズドコサインパルスの時間波形"""
    h = np.zeros_like(t, dtype=float)
    for i, ti in enumerate(t):
        x = ti / T
        if np.isclose(ti, 0):
            h[i] = 1.0
        elif alpha > 0 and np.isclose(abs(2 * alpha * x), 1.0):
            h[i] = (alpha / 2.0) * np.sin(np.pi / (2 * alpha))
        else:
            h[i] = np.sinc(x) * np.cos(np.pi * alpha * x) / (1 - (2 * alpha * x)**2)
    return h

def generate_baseband_signal(bits, T, alpha, sps):
    """ベースバンド信号生成"""
    symbols = 2 * bits - 1
    pulse_span = 8
    t_pulse = np.arange(-pulse_span * sps, pulse_span * sps + 1) / sps * T
    pulse = raised_cosine_pulse(t_pulse, T, alpha)
    total_samples = len(symbols) * sps
    signal = np.zeros(total_samples + len(pulse) - 1)
    for k, ak in enumerate(symbols):
        start = k * sps
        signal[start:start + len(pulse)] += ak * pulse
    return signal

def plot_eye_diagram(signal, sps, num_traces, title, ax):
    """アイダイアグラム描画"""
    trace_len = 2 * sps
    offset = 4 * sps
    t_axis = np.linspace(-1, 1, trace_len)
    for i in range(num_traces):
        start = offset + i * sps
        if start + trace_len > len(signal):
            break
        ax.plot(t_axis, signal[start:start + trace_len],
                color='cyan', alpha=0.05, linewidth=0.5)
    ax.set_xlabel('t / T')
    ax.set_ylabel('Amplitude')
    ax.set_title(title)
    ax.set_xlim(-1, 1)
    ax.grid(True, alpha=0.3)

# パラメータ
T = 1.0
sps = 32
num_bits = 2000
alpha = 0.5
np.random.seed(42)
bits = np.random.randint(0, 2, num_bits)

# ノイズなし信号の生成
signal_clean = generate_baseband_signal(bits, T, alpha, sps)

# 各SNRでアイダイアグラムを比較
snr_dbs = [100, 20, 10, 5]  # 100dBは実質ノイズなし
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.patch.set_facecolor('black')

for ax, snr_db in zip(axes.flat, snr_dbs):
    ax.set_facecolor('black')
    signal = signal_clean.copy()
    if snr_db < 50:
        # ノイズの追加
        signal_power = np.mean(signal**2)
        noise_power = signal_power / (10**(snr_db / 10))
        noise = np.random.normal(0, np.sqrt(noise_power), len(signal))
        signal = signal + noise

    label = 'No Noise' if snr_db >= 50 else f'SNR = {snr_db} dB'
    plot_eye_diagram(signal, sps, 500,
                     f'Eye Diagram (α=0.5, {label})', ax)
    ax.tick_params(colors='white')
    ax.xaxis.label.set_color('white')
    ax.yaxis.label.set_color('white')
    ax.title.set_color('white')

plt.tight_layout()
plt.savefig('eye_diagram_noise_comparison.png', dpi=150,
            bbox_inches='tight', facecolor='black')
plt.show()

このグラフからノイズがアイダイアグラムに与える影響が定量的に読み取れます。

  1. ノイズなし(SNR ≒ ∞): アイは完全に開いており、波形トレースが鮮明なラインを形成しています。ISIゼロ条件を満たすコサインロールオフフィルタの理想的な振る舞いが確認できます。
  2. SNR = 20 dB: アイの開口はまだ十分大きいですが、トレースにわずかな「太さ」が生じています。これはノイズによる波形の揺らぎであり、アイの上端と下端が少しぼやけ始めています。実用上は十分な品質です。
  3. SNR = 10 dB: アイ開口が明確に縮小しています。上側のトレース群と下側のトレース群の間隔が狭まり、判定マージンが低下していることが視覚的にわかります。BERの悪化が懸念される領域です。
  4. SNR = 5 dB: アイがかなり閉じかけており、上下のトレースが重なり始めています。この状態では判定閾値付近でノイズにより誤判定が頻発し、BERが大幅に悪化します。

理論で予測した「アイ開口の減少がBER悪化に直結する」という関係を、アイダイアグラムの視覚的な変化として確認できました。

ここまでで単一のフィルタでのノイズの影響を見てきました。次に、ルートレイズドコサインフィルタの実装と、送受信ペアとしての動作を確認しましょう。

ルートレイズドコサイン(RRC)フィルタの実装

RRCフィルタとレイズドコサインフィルタの比較

前述のとおり、実用的な通信システムでは送信側と受信側の両方でRRCフィルタを使用し、総合応答としてレイズドコサインフィルタ(ISIゼロ)を実現します。ここでは、RRCフィルタのインパルス応答を実装し、送受の畳み込みがレイズドコサインパルスに一致することを確認します。

import numpy as np
import matplotlib.pyplot as plt

def rrc_pulse(t, T, alpha):
    """ルートレイズドコサインパルスの時間波形"""
    h = np.zeros_like(t, dtype=float)
    for i, ti in enumerate(t):
        if np.isclose(ti, 0):
            h[i] = (1 / T) * (1 + alpha * (4 / np.pi - 1))
        elif alpha > 0 and np.isclose(abs(ti), T / (4 * alpha)):
            h[i] = (alpha / (T * np.sqrt(2))) * (
                (1 + 2/np.pi) * np.sin(np.pi / (4*alpha)) +
                (1 - 2/np.pi) * np.cos(np.pi / (4*alpha))
            )
        else:
            num = np.sin(np.pi * ti / T * (1 - alpha)) + \
                  4 * alpha * ti / T * np.cos(np.pi * ti / T * (1 + alpha))
            den = np.pi * ti / T * (1 - (4 * alpha * ti / T)**2)
            h[i] = num / (den * T) if not np.isclose(den * T, 0) else 0.0
    return h

def raised_cosine_pulse(t, T, alpha):
    """レイズドコサインパルス"""
    h = np.zeros_like(t, dtype=float)
    for i, ti in enumerate(t):
        x = ti / T
        if np.isclose(ti, 0):
            h[i] = 1.0
        elif alpha > 0 and np.isclose(abs(2 * alpha * x), 1.0):
            h[i] = (alpha / 2.0) * np.sin(np.pi / (2 * alpha))
        else:
            h[i] = np.sinc(x) * np.cos(np.pi * alpha * x) / (1 - (2 * alpha * x)**2)
    return h

# パラメータ
T = 1.0
alpha = 0.5
sps = 32
t = np.arange(-8 * sps, 8 * sps + 1) / sps * T

# RRCパルスとRCパルスの計算
h_rrc = rrc_pulse(t, T, alpha)
h_rc = raised_cosine_pulse(t, T, alpha)

# RRC * RRC = RC の検証(畳み込み)
h_conv = np.convolve(h_rrc, h_rrc, mode='full') / sps
t_conv = np.arange(len(h_conv)) / sps * T
t_conv = t_conv - t_conv[len(t_conv)//2]  # 中央をゼロに

# 比較用のRCパルス(同じ時間軸)
h_rc_ref = raised_cosine_pulse(t_conv, T, alpha)

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# RRCパルスの表示
axes[0].plot(t / T, h_rrc * T, 'b-', label='RRC pulse', linewidth=1.5)
axes[0].plot(t / T, h_rc, 'r--', label='RC pulse', linewidth=1.5)
axes[0].set_xlabel('t / T')
axes[0].set_ylabel('Amplitude')
axes[0].set_title(f'RRC vs RC Pulse (α = {alpha})')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
axes[0].set_xlim(-6, 6)

# 畳み込み結果の検証
conv_norm = h_conv / np.max(h_conv)
rc_ref_norm = h_rc_ref / np.max(np.abs(h_rc_ref))
axes[1].plot(t_conv / T, conv_norm, 'b-',
             label='RRC ∗ RRC (convolution)', linewidth=1.5)
axes[1].plot(t_conv / T, rc_ref_norm, 'r--',
             label='RC pulse (reference)', linewidth=1.5)
axes[1].set_xlabel('t / T')
axes[1].set_ylabel('Normalized Amplitude')
axes[1].set_title('Verification: RRC ∗ RRC ≈ RC')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
axes[1].set_xlim(-8, 8)

plt.tight_layout()
plt.savefig('rrc_vs_rc_verification.png', dpi=150, bbox_inches='tight')
plt.show()

左図と右図から、RRCフィルタの重要な特性が確認できます。

  1. 左図(RRC vs RC パルス比較): RRCパルス(青実線)はRCパルス(赤破線)と比べて、メインローブがやや狭く、サイドローブの振動が大きいことがわかります。RRCパルス単体ではISIゼロ条件を満たしませんが、これは設計上意図されたものです。送信と受信の両方でRRCフィルタを適用することで、はじめてISIゼロが実現されます。
  2. 右図(畳み込み検証): RRCパルス同士の畳み込み(青実線)がRCパルス(赤破線)と正確に一致していることが確認できます。わずかな数値誤差(離散畳み込みの打ち切り効果)を除けば、両者は完全に重なっています。これは $H_{\text{RRC}}(f) \cdot H_{\text{RRC}}(f) = H_{\text{RC}}(f)$ という理論的関係がPythonの数値計算で裏付けられたことを意味します。

アイ開口の定量的測定

アイ開口の数値計算

アイダイアグラムの「見た目」だけでなく、アイ開口を数値として測定するコードを実装しましょう。これにより、ロールオフ率やSNRがアイ開口に与える定量的な影響を分析できます。

import numpy as np
import matplotlib.pyplot as plt

def raised_cosine_pulse(t, T, alpha):
    """レイズドコサインパルス"""
    h = np.zeros_like(t, dtype=float)
    for i, ti in enumerate(t):
        x = ti / T
        if np.isclose(ti, 0):
            h[i] = 1.0
        elif alpha > 0 and np.isclose(abs(2 * alpha * x), 1.0):
            h[i] = (alpha / 2.0) * np.sin(np.pi / (2 * alpha))
        else:
            h[i] = np.sinc(x) * np.cos(np.pi * alpha * x) / (1 - (2 * alpha * x)**2)
    return h

def generate_baseband_signal(bits, T, alpha, sps):
    """ベースバンド信号生成"""
    symbols = 2 * bits - 1
    pulse_span = 8
    t_pulse = np.arange(-pulse_span * sps, pulse_span * sps + 1) / sps * T
    pulse = raised_cosine_pulse(t_pulse, T, alpha)
    total_samples = len(symbols) * sps
    signal = np.zeros(total_samples + len(pulse) - 1)
    for k, ak in enumerate(symbols):
        start = k * sps
        signal[start:start + len(pulse)] += ak * pulse
    return signal

def measure_eye_opening(signal, sps, num_traces):
    """アイ開口を測定する"""
    trace_len = 2 * sps
    offset = 4 * sps
    # 判定タイミング(中央)でのサンプル値を収集
    center_idx = sps  # 2T区間の中央
    upper_values = []
    lower_values = []
    for i in range(num_traces):
        start = offset + i * sps
        if start + trace_len > len(signal):
            break
        val = signal[start + center_idx]
        if val > 0:
            upper_values.append(val)
        else:
            lower_values.append(val)
    if len(upper_values) == 0 or len(lower_values) == 0:
        return 0.0
    eye_opening = np.min(upper_values) - np.max(lower_values)
    return max(eye_opening, 0.0)

# ロールオフ率 vs アイ開口(ノイズなし)
T = 1.0
sps = 32
num_bits = 5000
np.random.seed(42)
bits = np.random.randint(0, 2, num_bits)

alphas_fine = np.linspace(0.01, 1.0, 50)
eye_openings_noiseless = []

for alpha in alphas_fine:
    signal = generate_baseband_signal(bits, T, alpha, sps)
    eo = measure_eye_opening(signal, sps, 3000)
    eye_openings_noiseless.append(eo)

# SNR vs アイ開口(α = 0.5固定)
alpha_fixed = 0.5
snr_range = np.linspace(3, 30, 30)
eye_openings_snr = []

signal_clean = generate_baseband_signal(bits, T, alpha_fixed, sps)
signal_power = np.mean(signal_clean**2)

for snr_db in snr_range:
    noise_power = signal_power / (10**(snr_db / 10))
    noise = np.random.normal(0, np.sqrt(noise_power), len(signal_clean))
    signal_noisy = signal_clean + noise
    eo = measure_eye_opening(signal_noisy, sps, 3000)
    eye_openings_snr.append(eo)

# プロット
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

axes[0].plot(alphas_fine, eye_openings_noiseless, 'b-o', markersize=3)
axes[0].set_xlabel('Roll-off factor α')
axes[0].set_ylabel('Eye Opening')
axes[0].set_title('Eye Opening vs Roll-off Factor (No Noise)')
axes[0].grid(True, alpha=0.3)

axes[1].plot(snr_range, eye_openings_snr, 'r-o', markersize=3)
axes[1].set_xlabel('SNR [dB]')
axes[1].set_ylabel('Eye Opening')
axes[1].set_title(f'Eye Opening vs SNR (α = {alpha_fixed})')
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('eye_opening_quantitative.png', dpi=150, bbox_inches='tight')
plt.show()

この定量分析の結果から、2つの重要な知見が得られます。

  1. 左図(ロールオフ率 vs アイ開口): ノイズがない場合、ナイキスト基準を満たすレイズドコサインフィルタではどの $\alpha$ でもアイ開口がほぼ $2.0$(シンボル間隔の最大値)を維持しています。理論的にはISIゼロなので当然ですが、有限長のパルス打ち切りにより $\alpha$ が非常に小さい領域ではわずかな低下が見られます。これは sinc パルスの裾の打ち切り誤差によるものです。
  2. 右図(SNR vs アイ開口): $\alpha = 0.5$ でSNRを変化させると、アイ開口はSNRの増加とともにほぼ線形に増大し、高SNR領域で理論値の $2.0$ に漸近します。SNR が 10 dB を下回ると急激にアイ開口が縮小し、通信品質の悪化が顕著になります。この曲線の傾きからBERの劣化速度を推定でき、システム設計における最低SNR要件の設定に直結します。

帯域制限チャネルでのアイダイアグラム

理想フィルタと帯域制限の比較

ここまではナイキスト基準を満たす理想的なフィルタを使ってきました。最後に、チャネルの帯域制限によりナイキスト基準が崩れた場合にISIがどのように発生するかを、アイダイアグラムで確認しましょう。

import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import butter, lfilter

def raised_cosine_pulse(t, T, alpha):
    """レイズドコサインパルス"""
    h = np.zeros_like(t, dtype=float)
    for i, ti in enumerate(t):
        x = ti / T
        if np.isclose(ti, 0):
            h[i] = 1.0
        elif alpha > 0 and np.isclose(abs(2 * alpha * x), 1.0):
            h[i] = (alpha / 2.0) * np.sin(np.pi / (2 * alpha))
        else:
            h[i] = np.sinc(x) * np.cos(np.pi * alpha * x) / (1 - (2 * alpha * x)**2)
    return h

def generate_baseband_signal(bits, T, alpha, sps):
    """ベースバンド信号生成"""
    symbols = 2 * bits - 1
    pulse_span = 8
    t_pulse = np.arange(-pulse_span * sps, pulse_span * sps + 1) / sps * T
    pulse = raised_cosine_pulse(t_pulse, T, alpha)
    total = len(symbols) * sps
    signal = np.zeros(total + len(pulse) - 1)
    for k, ak in enumerate(symbols):
        start = k * sps
        signal[start:start + len(pulse)] += ak * pulse
    return signal

def plot_eye(signal, sps, num_traces, title, ax):
    """アイダイアグラム描画"""
    trace_len = 2 * sps
    offset = 8 * sps
    t_ax = np.linspace(-1, 1, trace_len)
    for i in range(num_traces):
        start = offset + i * sps
        if start + trace_len > len(signal):
            break
        ax.plot(t_ax, signal[start:start + trace_len],
                color='cyan', alpha=0.05, linewidth=0.5)
    ax.set_xlabel('t / T')
    ax.set_ylabel('Amplitude')
    ax.set_title(title)
    ax.set_xlim(-1, 1)
    ax.grid(True, alpha=0.3)

# パラメータ
T = 1.0
sps = 32
alpha = 0.5
fs = sps / T  # サンプリング周波数
num_bits = 2000
np.random.seed(42)
bits = np.random.randint(0, 2, num_bits)

signal = generate_baseband_signal(bits, T, alpha, sps)

# 帯域制限フィルタの適用(バターワースLPF)
# ナイキスト帯域幅の倍率を変える
bw_factors = [1.0, 0.8, 0.6, 0.4]
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.patch.set_facecolor('black')

for ax, bw_factor in zip(axes.flat, bw_factors):
    ax.set_facecolor('black')
    if bw_factor >= 1.0:
        sig_filtered = signal.copy()
        label = 'No band limitation'
    else:
        # カットオフ: ナイキスト帯域幅 × bw_factor
        nyquist_bw = (1 + alpha) / (2 * T)
        cutoff = nyquist_bw * bw_factor
        b, a = butter(5, cutoff / (fs / 2), btype='low')
        sig_filtered = lfilter(b, a, signal)
        label = f'BW = {bw_factor:.0%} of Nyquist'

    plot_eye(sig_filtered, sps, 500,
             f'Eye Diagram ({label})', ax)
    ax.tick_params(colors='white')
    ax.xaxis.label.set_color('white')
    ax.yaxis.label.set_color('white')
    ax.title.set_color('white')

plt.tight_layout()
plt.savefig('eye_diagram_bandwidth_limitation.png', dpi=150,
            bbox_inches='tight', facecolor='black')
plt.show()

帯域制限がアイダイアグラムに及ぼす影響が顕著に観察されます。

  1. 帯域制限なし(BW = 100%): ナイキスト基準を完全に満たすフィルタの出力であり、アイは完全に開いています。レイズドコサインフィルタの理想的な動作が確認できます。
  2. BW = 80%: わずかなISIが発生し始め、アイの上端と下端にトレースの広がり(太さ)が現れます。しかし、アイ開口はまだ十分に大きく、実用上の問題はありません。
  3. BW = 60%: ISIの影響が明確になり、アイ開口が顕著に縮小しています。波形トレースの分散が増大し、判定マージンが低下しています。この状態では等化器(イコライザ)による補償が必要です。
  4. BW = 40%: アイがほとんど閉じており、上下のトレースが大きく重なっています。この帯域制限ではナイキスト基準が大幅に崩れ、単純な閾値判定ではBERが極めて高くなります。適応等化やMLSE(最尤系列推定)などの高度な受信技術が不可欠です。

この結果は、チャネルの帯域幅がシンボルレートに対して十分でない場合、パルス整形フィルタだけではISIを抑制できないことを明確に示しています。通信システム設計では、使用可能な帯域幅に応じてシンボルレートとロールオフ率を適切に設定することが不可欠です。

アイダイアグラムの実用的な読み方

実践的な評価手順

ここまでの理論と実装を踏まえ、実際の通信システム評価でアイダイアグラムをどのように読むかをまとめます。

ステップ1: アイ開口の確認。判定タイミング(シンボル周期の中央)での目の縦方向の開きを確認します。この値が大きいほどノイズマージンが大きく、BERが低くなります。規格によって最小アイ開口が定められており(例: 光通信のIEEE 802.3で OMA: Optical Modulation Amplitude の仕様がある)、これを満たさなければ製品として出荷できません。

ステップ2: タイミングマージンの確認。目の横方向の幅を確認します。ゼロクロッシング付近でアイが閉じ始めるポイントから、判定タイミングまでの距離がタイミングマージンです。通常、シンボル周期の20〜30%以上のマージンが求められます。

ステップ3: ジッタの評価。ゼロクロッシング点の水平方向の分散を確認します。ジッタが大きいと、クロック再生回路がシンボルタイミングを正確に追従できなくなり、実効的なアイ開口が縮小します。

ステップ4: アイの対称性の確認。理想的にはアイの上半分と下半分は対称ですが、実際のシステムではDCオフセットや非線形歪みにより非対称になることがあります。非対称なアイは判定閾値の最適値がゼロからずれていることを意味し、閾値の調整が必要です。

ステップ5: 複数レベルへの拡張。ここまでは2値(NRZ)通信を扱いましたが、4値PAM(PAM-4)や多値QAMではアイの「段数」が増えます。PAM-4では3つのアイが縦に並び、各アイの開口がそれぞれ評価されます。多値変調ではシンボル間の距離が小さくなるため、アイ開口が2値通信より狭くなり、より高いSNRが要求されます。

アイダイアグラムのマスクテスト

通信規格の適合性試験では、アイマスク(Eye Mask)と呼ばれる禁止領域をアイダイアグラムに重ねて表示します。マスクは六角形や菱形の領域で定義され、波形トレースがこの領域に入ってはならないという条件です。

マスクテストに合格するためには、アイ開口がマスクの高さ以上、タイミングマージンがマスクの幅以上である必要があります。これにより、異なるベンダーの機器間での相互接続性が保証されます。USB、Ethernet、PCIeなどの高速シリアルインターフェースでは、マスクテストが必須の試験項目です。

まとめ

本記事では、ディジタル通信における符号間干渉(ISI)の可視化手法であるアイダイアグラムについて、理論から実装まで体系的に解説しました。

  • 符号間干渉(ISI) は、帯域制限のあるチャネルでパルスの裾が隣接シンボルに干渉する現象であり、ビット誤り率を劣化させる主要因である
  • ナイキストの第1基準 は、ISIゼロ条件の周波数領域表現であり、折り返しスペクトルが定数(平坦)になることを要求する。この基準を満たす最もシンプルなパルスが sinc 関数だが、裾の減衰が遅く実用的でない
  • コサインロールオフフィルタ はナイキスト基準を満たしつつスペクトルの端を滑らかにロールオフさせることで、時間領域の減衰を $1/t^3$ に加速する。ロールオフ率 $\alpha$ は帯域効率とタイミング耐性のトレードオフを制御する
  • ルートレイズドコサイン(RRC)フィルタ を送受信の両方で使用することで、ISIゼロ条件とマッチドフィルタ条件を同時に満たせる
  • アイダイアグラム は受信波形を1シンボル周期で切り出して重ね描きしたもので、アイ開口(ノイズマージン)、タイミングマージン、ジッタ、SN比を一目で評価できる
  • ロールオフ率 $\alpha$ の増大はタイミングマージンを改善するが帯域効率を犠牲にし、帯域制限はISIを増大させてアイを閉じさせる — これらのトレードオフを可視化できるのがアイダイアグラムの最大の利点である

アイダイアグラムは、アナログ通信時代のオシロスコープ観測から始まり、現在でも光通信やミリ波通信などの最先端システムの評価に不可欠なツールです。本記事で学んだ理論は、等化器(イコライザ)の設計やビット誤り率の解析へと直接つながっていきます。

次のステップとして、以下の記事も参考にしてください。