デジタル信号とノイズ

サイエンス記事

2月からプログラミング教室の新しいクラスがスタートしました。初級コースの生徒さんから早速、デジタル信号におけるノイズについて質問を頂きました。

様々なノイズの発生原因がありますが、デジタル信号は0と1の2つの状態を使って情報を伝えるので、アナログ信号に比べてノイズに強い特性があります。
それでもノイズが許容範囲を超えるとビットエラーにつながってしまうため、エラーを防止する様々な技術が使われています。
以下では、
ノイズの主な原因、
そもそもデジタル信号がノイズに強い理由、
ノイズが許容範囲を超えたときにエラーを検出・修正するための誤り訂正符号を使った技術の基本概念
を初心者にもわかるように簡単に説明します。

■デジタル信号におけるノイズの発生原因
デジタル信号でもノイズは避けられない存在です。主な原因は以下のようにいくつかあります。

1. 電磁干渉(EMI: Electromagnetic Interference)
電子レンジやスマートフォン、Wi-Fiルーターなど周囲の電子機器や通信機器から放射される電磁波と信号が干渉するために発生するノイズです。

2. クロストーク
プリント基板上での高密度配線や、長距離の並列ケーブル配線において、近接する信号線間で電磁誘導や静電結合が起こり、一方の信号が他方に漏れ出す現象です。

3. パワーサプライノイズ
電源の品質が悪いと電源電圧にスパイクが生じたりし、それが信号に影響を及ぼしてノイズとして現れます。

4. 熱雑音(サーマルノイズ)
電子部品内の電子が熱エネルギーによってランダムに動くことで生じるノイズです。

■デジタル信号がノイズに強い理由
デジタル信号がノイズに強い理由は、基本的にデジタル信号が2つの離散的な値(通常は0と1)から成り立っているためです。
例えば、0という信号がノイズにより0.1となった場合でも四捨五入して0とすることができます。1という信号がノイズにより0.9になった場合も同様に四捨五入すれば1という元の信号に戻ります。この特性により、ノイズの影響を受けにくくなります。
以下は、Pythonプログラムを用いてデジタル信号のノイズ耐性を示す簡単なシミュレーションです。

%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np

# デジタル信号の生成 (0と1のみ)
digital_signal = np.random.choice([0, 1], size=100)

# ノイズの生成(平均0、標準偏差0.1のガウスノイズを加える)
noise = np.random.normal(0, 0.1, size=digital_signal.shape)

# ノイズのある信号
noisy_signal = digital_signal + noise

# デジタル信号の復元
recovered_signal = np.round(noisy_signal).astype(int)

# プロット
plt.figure(figsize=(12, 6))

plt.subplot(3, 1, 1)
plt.plot(digital_signal, 'bo-', label='Original Digital Signal')
plt.legend()

plt.subplot(3, 1, 2)
plt.plot(noisy_signal, 'ro-', label='Noisy Signal')
plt.legend()

plt.subplot(3, 1, 3)
plt.plot(recovered_signal, 'go-', label='Recovered Signal')
plt.legend()

plt.tight_layout()
plt.savefig('digital_noise.png')

このプログラムは以下のステップを実行します:
1. デジタル信号(0と1のみ)を生成します。(上段の図)
2. 信号にノイズを加えます(ガウスノイズ)。(中断の図)
3. ノイズのある信号を四捨五入して元のデジタル信号を復元します。(下段の図)
このように、デジタル信号はノイズが多少あっても元の信号に復元可能であることが視覚的に示されます。

■パリティビットによる誤り訂正
次に、ノイズが許容範囲を超えてビットエラーが生じた場合にエラーを検出・修正する技術の基本概念を説明します。
パリティビットを使うことで、誤り検出や誤り訂正を実現できます。
パリティビットとは、1の数が偶数か奇数かによって、末尾につけるビットのことです。
例えば、1011 という情報を送りたいとします。
1の数が偶数個になるように末尾に0または1をつけるようにします。この付け加えたビットがパリティビットです。
この場合は10111を送ることになります。
もし最初のビットにノイズがのってしまってこの信号が00111となったとします。
受信者は受け取った信号の中に1が奇数個あるので、途中でエラーが生じたことがわかります。
この場合、エラーが生じたことはわかりますが、どのビットにエラーが生じたかはわかりません。
ここで送りたい情報を次のように2次元的に配置してみます。
10
11

そして、各行と各列にパリティビットをつけます。
101
110
01
となります。
こうすれば、どこかのビットにエラーが生じた場合に、どの行、どの列でエラーが生じたかがわかり、エラーが生じたビットが特定できます。

以下はここで説明した、シンプルな冗長性チェック(パリティビット)を使用した誤り訂正の基本的な概念を示すPythonプログラムです。

import numpy as np

# 送信側:データの作成とパリティビットの追加
def create_data_with_parity():
    # 元のデータ(4ビット)
    data = np.array([[1, 0],
                     [1, 1]])
    print("Original Data:")
    print(data)
    print("\n")

    # 行パリティの計算
    row_parity = data.sum(axis=1) % 2
    # 列パリティの計算
    col_parity = data.sum(axis=0) % 2
    # 全体パリティの計算
    total_parity = row_parity.sum() % 2

    # パリティビットを含めたデータの作成
    data_with_parity = np.zeros((3, 3), dtype=int)
    data_with_parity[:2, :2] = data
    data_with_parity[:2, 2] = row_parity
    data_with_parity[2, :2] = col_parity
    data_with_parity[2, 2] = total_parity

    print("Data with Parity Bits:")
    print(data_with_parity)
    print("\n")

    return data_with_parity

# エラーの導入(通信中にノイズが入ることをシミュレーション)
def introduce_error(data_with_parity):
    data_with_error = data_with_parity.copy()
    # エラーをランダムな位置に導入
    error_row = np.random.randint(0, 2)
    error_col = np.random.randint(0, 2)
    data_with_error[error_row, error_col] ^= 1  # ビットを反転
    print(f"Error introduced at position ({error_row}, {error_col}):")
    print(data_with_error)
    print("\n")
    return data_with_error

# 受信側:エラーの検出と修正
def detect_and_correct_error(received_data):
    # 行パリティの再計算
    row_parity = received_data[:2, :2].sum(axis=1) % 2
    # 列パリティの再計算
    col_parity = received_data[:2, :2].sum(axis=0) % 2
    # 全体パリティの再計算
    total_parity = row_parity.sum() % 2

    # パリティビットとの差を確認
    row_error = row_parity != received_data[:2, 2]
    col_error = col_parity != received_data[2, :2]    
    total_error = total_parity != received_data[2, 2]

    # エラーの有無を確認
    if total_error:
        # エラーの位置を特定
        error_row = np.where(row_error)[0][0]
        error_col = np.where(col_error)[0][0]
        print(f"Error detected at position ({error_row}, {error_col})")
        # エラーを修正
        received_data[error_row, error_col] ^= 1
        print("Corrected Data:")
        print(received_data)
        print("\n")
    else:
        print("No error detected.")
        print("Received Data is Correct:")
        print(received_data)
        print("\n")

    # 元のデータを抽出
    corrected_data = received_data[:2, :2]
    return corrected_data

# メインの実行部分
# データの作成とパリティビットの追加
data_with_parity = create_data_with_parity()

# エラーの導入
data_with_error = introduce_error(data_with_parity)

# エラーの検出と修正
corrected_data = detect_and_correct_error(data_with_error)

print("Final Corrected Data:")
print(corrected_data)

データを2次元配列として扱い、各行のパリティビットと各列のパリティビットを計算します。
これにより、エラーの位置を特定し、修正することができます。
データのランダムな位置にビット反転を導入し、エラーをシミュレートしています。
計算したパリティビットと受信したパリティビットを比較し、不一致を検出します。
行のパリティと列のパリティの不一致から、エラーの位置(行と列)を特定します。

出力結果の例を示します。
Original Data:
[[1 0]
[1 1]]

Data with Parity Bits:
[[1 0 1]
[1 1 0]
[0 1 1]]

Error introduced at position (0, 0):
[[0 0 1]
[1 1 0]
[0 1 1]]

Error detected at position (0, 0)
Corrected Data:
[[1 0 1]
[1 1 0]
[0 1 1]]

Final Corrected Data:
[[1 0]
[1 1]]

冗長性を持たせることで、データがノイズに強くなり、信頼性の高い通信が可能になります。
なお、今回のシンプルな方法では、1ビットのエラーのみを検出・修正できます。
2ビット以上のエラーがある場合、正しく検出・修正できない可能性がありますが、より優れた誤り訂正の方法が報告されています。