ADCの評価・実験・原理理解のための解説記事

## ADCの評価・実験・原理理解のための解説記事

近年、アナログ信号をデジタル信号に変換するアナログ・デジタル・コンバータ (ADC) は、多くの電子機器や計測システムで利用されており、その特性評価や原理の理解はエンジニアにとって重要なテーマとなっています。本記事では、サンプリング周波数設定やコヒーレントサンプリング、AC特性評価 (SNDR, ENOB, SFDR など) および DC特性評価 (DNL, INL など) を含む「ADCの評価・実験・原理理解」を行う際のポイントや手法を、サンプルプログラムの機能を例に紹介します。コード自体は掲載せず、考え方やアルゴリズムの概要を解説する記事になっています。

---

### 1. コヒーレントサンプリング (Coherent Sampling) の重要性

ADCの評価を行うとき、入力するサイン波とサンプリング周波数の間に不必要な“ビート”成分が現れにくい環境を作ることが理想です。そのために用いられるのが**コヒーレントサンプリング**という手法です。

- **コヒーレントサンプリングの目的**  
  連続的なアナログ信号を離散的なサンプリングで捉えたときに、できるだけ周期がずれないように設定することで、サンプリングした離散データが正確に1周期ごと一致する（波形が途切れず繋がる）ように狙います。  
- **周波数決定の考え方**  
  サンプル数を\( 2^N \) とし、\( 2^N - 1 \) といった特定の形をもつ周波数倍数を用いることで、サンプリング周波数と入力周波数を「整数比に近い形」に合わせることが可能です。そうすると、取得した離散データがぴったり整数の波形周期数に対応し、振幅スペクトルの解析がシンプルになります。

コヒーレントサンプリングを行うことで、FFT解析時に不要な窓関数などを使用しなくても、スペクトル漏れ (spectral leakage) を抑え、**SNDRやSFDRを正確に解析**しやすくなります。

---

### 2. 理想ADCのモデル化

ADCの評価プログラムを作るうえで、まずはノイズや非線形性のない「理想的なADC」を想定し、それと比較することで実機の性能を客観的に測る手法が一般的です。

- **理想ADCモデルの考え方**  
  - \( n\) ビットの分解能をもつADCでは、量子化段階数は \( 2^n \) となります。  
  - 入力電圧範囲 \( [V_{\min}, V_{\max}] \) を \( 2^n \) 段階に分け、区間ごとに最も近いデジタル値に丸めます。  
  - このモデルでは比較的単純な「切り捨て or 四捨五入」の量子化演算を行い、範囲外の電圧はクリップするように処理します。

理想ADCによる理論値との比較は、ノイズを重畳させたり、実機の特性を導入したりする実験で「実際のADCがどの程度理想値から乖離しているか」を把握する上で重要です。

---

### 3. サイン波の生成とノイズ重畳

ADCをテストする際には、汎用のテスト信号として**サイン波**がよく使われます。以下のような観点でサイン波が生成されます。

- **サンプリング周波数 \( f_s \) とサイン波周波数 \( f_{\mathrm{in}} \)**  
  先述のコヒーレントサンプリング条件に合うように、たとえば \( 2^N - 1 \) といった分母を用いた周波数を選びます。  
- **振幅 (amplitude) とオフセット (offset)**  
  ADCのフルスケール範囲に対して適切な振幅とオフセットを設定し、コード全域をある程度活性化させるようにします。たとえば 0〜5 V 入力を想定するとき、サイン波の振幅を 2.5 V、オフセットを 2.5 V に設定するなどが一般的です。  
- **ノイズの重畳**  
  ADCのSNDRやENOBをより実践的に評価したいとき、ランダムノイズを少しだけ加えることで微小ビットの揺らぎをシミュレートできます。たとえば 10 mV 程度のランダムノイズを加えることで、量子化誤差の評価がしやすくなります。

---

### 4. AC特性評価 (SNDR, ENOB, SFDRなど)

ADCのダイナミックな特性を評価するときは、通常サイン波入力に対して**FFT**を行い、パワースペクトルを解析します。主な評価指標は以下の通りです。

1. **SNDR (Signal-to-Noise and Distortion Ratio)**  
   入力サイン波成分 (Signal) と、それ以外のノイズ・歪み成分 (Noise + Distortion) のパワー比をデシベル (dB) で表したもの。一般的には以下の式で示されます。  
   \[
   \mathrm{SNDR} = 10 \log_{10}\left(\frac{P_{\mathrm{signal}}}{P_{\mathrm{noise+dist}}}\right).
   \]

2. **ENOB (Effective Number Of Bits)**  
   実効ビット数。SNDRから計算され、理想ADCに対して「実際にどの程度のビット精度があるか」を示す指標。  
   \[
   \mathrm{ENOB} \approx \frac{\mathrm{SNDR} - 1.76}{6.02}.
   \]

3. **SFDR (Spurious-Free Dynamic Range)**  
   メインのサイン波成分に対して、最も大きなスプリアス (高調波歪みを含む) 成分の差をdBで示す指標。例えば最大4次高調波まで調べ、最もレベルが高いスプリアス成分とメイン信号成分との差を取り、その値が高いほどダイナミックレンジが大きくなります。

これらのパラメータを測定する手順としては、

1. コヒーレントサンプリングでサイン波を取得  
2. FFTを実行  
3. スペクトル上でメインのシグナル成分や高調波成分を検出  
4. ノイズパワーやスプリアス成分の計算  
5. 各種dB値や実効ビット数を算出  

という流れです。

---

### 5. DC特性評価 (DNL, INLなど)

高精度なADCでは、個々のコード (デジタル値) の分布も重要になります。サイン波を十分にゆっくり入れるあるいは多数サンプリングして、ヒストグラム解析を行う手法によって、**DNL (Differential Non-Linearity)** と **INL (Integral Non-Linearity)** を推定することができます。

- **DNL (微分線形性)**  
  連続するコード幅のばらつきを示し、「理想よりも区間幅が広すぎる (もしくは狭すぎる)」といった情報を得られます。  
  - ヒストグラム・テストでは、サイン波の理想的な分布 (arcsine分布) と実測分布を比較し、その比率の偏差をDNLとみなす方法が一般的です。

- **INL (積分線形性)**  
  DNLを積分 (累積) することで得られる指標で、ADC全体が「どの程度、理想直線からずれているか」を示します。  
  - 連続するDNLのズレが蓄積されることで、コードのスケール全体でどれくらい誤差があるかを把握できます。

DC特性評価は、高分解能ADCや精密計測向けADCで特に重要となり、最終的な計測精度を左右します。

---

### 6. 実験・解析手順のまとめ

1. **コヒーレントサンプリング用の入力周波数を決定**  
   - サンプリング周波数 \( f_s \) とデータ点数 \( 2^N \) を設定し、\( 2^N - 1 \) を用いた “理想的な” 周波数 \(\approx f_s / \text{(整数)}\) を求めます。

2. **理想ADCモデルによる変換 (テスト信号生成)**  
   - 振幅やオフセット、必要に応じてノイズを加えたサイン波を生成します。  
   - その後、理想ADCの量子化演算でデジタル値を得ます。

3. **AC特性評価**  
   - サンプルデータにFFTをかけ、SNDR・ENOB・SFDRを計算します。  
   - スペクトルを可視化して、ノイズフロアや歪み成分を確認します。

4. **DC特性評価**  
   - ヒストグラムを作成して各コードの出現頻度を調べ、理想分布との誤差からDNLを計算します。  
   - DNLを積分してINLを求め、最大値や最小値が仕様を満たしているか検証します。

5. **実測器 (オシロスコープや任意波形発生器) との組み合わせ**  
   - 実機で同様のテストを行う場合には、任意波形発生器でコヒーレントサンプリングが可能な周波数を生成し、オシロスコープや評価ボード等でサンプリングして同様の解析を行います。

---

### 7. Google Colab などでの解析環境

- **インライン表示**  
  Google Colab を利用する場合は、`%matplotlib inline` もしくは `%matplotlib notebook` などのマジックコマンドでグラフをインライン表示できます。
- **ファイル保存パス**  
  通常のWindowsパスとは異なり、`os.getcwd() + "/folder_name"` のようにスラッシュ (`/`) 区切りを使うのが無難です。
- **リアルタイムプロット**  
  Pythonの `matplotlib` を用いて、ループ内で `plt.pause(0.1)` のようにポーズを入れると、コールバック的なリアルタイム描画が可能です。ただし処理速度には注意が必要です。

---

### Pythonコード


```
# -*- coding: utf-8 -*-
"""
ADC Evaluation and Experiment Program with Detailed Comments
ADCの評価・実験・原理理解のためのサンプルプログラム（コメントを日本語と英語で充実させています）

This code includes:
- save_fig(): Function to save matplotlib figures
- freq_search(): Function to calculate an input frequency for coherent sampling
- wave_sort(): Function to reorder ADC output data for a single-cycle waveform
- ideal_adc(): Function that emulates an ideal ADC
- sine_wave_generator(): Function to generate a sinusoidal signal
- AC_evaluation(): Function that evaluates AC characteristics (e.g. SNDR, ENOB, SFDR)
- DC_evaluation(): Function that evaluates DC characteristics (e.g. DNL, INL)

Google Colab Usage:
-------------------
1. Google Colabではファイルの保存パスなどに注意してください。OSの区切り文字 "\\"
   の代わりに "/" を使用してください。例) dir_path + "/svg/"
2. そのまま動かす場合は、グラフ描画の際に %matplotlib inline や %matplotlib notebook など
   を使ってください。

"""

import os
import numpy as np
import matplotlib.pyplot as plt

def save_fig(fig, dir_path, fig_name):
    """matplotlibのグラフを保存する関数 / Function to save a matplotlib figure

    Parameters
    ----------
    fig : matplotlib.figure.Figure
        保存するmatplotlibのFigure / The matplotlib Figure to be saved.
    dir_path : str
        保存先のパス / Path to the directory where the figure will be saved.
    fig_name : str
        ファイル名（拡張子は不要） / File name (no extension needed).
    """

    # Google Colabなどの場合、パスの区切り文字は"/"を使用した方がよいです。
    # For Google Colab, you may want to replace "\\" with "/".

    # svg保存用フォルダの作成 / Create a folder for svg files
    if not os.path.isdir(dir_path + "/svg"):
        os.makedirs(dir_path + "/svg")

    # 拡張子を変えて2種類で保存 / Save in two different formats
    fig.savefig(dir_path + "/svg/" + fig_name + ".svg")
    fig.savefig(dir_path + "/" + fig_name + ".png")


def freq_search(N: int, fs: float, finRate=8):
    """コヒーレントサンプリングに必要な入力周波数を求める関数 / 
    Function to calculate the input frequency for coherent sampling.

    Parameters
    ----------
    N : int
        サンプル数を2^Nで表したときのN。例)2^12=4096ならN=12 / 
        The exponent if sample size is 2^N. e.g., if 2^12=4096, then N=12.
    fs : float
        サンプリング周波数 [Hz] / Sampling frequency in Hz.
    finRate : int, optional
        入力周波数がfsの何分の1程度になるようにするかの目安 / 
        An approximate ratio of fs to choose the input frequency. Default is 8.

    Returns
    -------
    fin : float
        算出された入力周波数 [Hz] / Calculated input frequency in Hz.
    """

    freq = []
    FFT_points = 2**N

    # (2^n - 1)で割った周波数をすべてリストに入れる / 
    # Compute the fraction (2^n - 1)/2^N for all n < N
    for n in range(2, N):
        # オーバーフロー対策をした (2^n - 1) 計算 / 
        # Calculation of (2^n - 1) with overflow protection
        prime = np.exp(n * np.log(2)) - 1  
        fin_per_fs = prime / FFT_points
        freq.append(fin_per_fs)

    # 1/finRateに最も近い値をfreq配列から探してインデックスを得る / 
    # Find index of freq[] closest to (1 / finRate)
    array = np.array(freq)
    index = (np.abs(array - (1 / finRate))).argmin()

    # 周波数を計算 / Calculate the frequency
    fin = freq[index] * fs
    return fin


def wave_sort(data, fs: float, f_signal: float):
    """コヒーレントサンプリングされた波形データを並び替えて1周期分の波形にする関数 / 
    Function to reorder coherently-sampled data for a single-cycle waveform.

    Parameters
    ----------
    data : ndarray
        元のADC出力波形データ / Original ADC output data array.
    fs : float
        サンプリング周波数 [Hz] / Sampling frequency in Hz.
    f_signal : float
        入力信号の周波数 [Hz] / Input signal frequency in Hz.

    Returns
    -------
    sorted_wave : ndarray
        並び替えられた1周期分の波形 / Reordered single-cycle waveform.
    """

    data_num = len(data)            # データの個数 / Number of data points
    t_signal = 1 / f_signal         # 信号の周期 / Signal period
    Ts = 1 / fs                     # サンプリング周期 / Sampling period

    # 各データが1周期開始時点からどれくらい経過しているかを計算 / 
    # Compute the elapsed time from the start of each cycle
    dx_list = []
    for n in range(data_num):
        i = int((Ts * n) / t_signal)
        dx_list.append((Ts * n) - (t_signal * i))

    # dx_listの小さい順に並び替えるインデックスを取得 / 
    # Get the sorted indices based on dx_list
    sorted_indices = sorted(range(len(dx_list)), key=lambda k: dx_list[k])

    # 並び替えたデータをリストに格納 / 
    # Reorder the original data according to sorted indices
    sorted_wave = [data[i] for i in sorted_indices]
    return sorted_wave


def ideal_adc(input_voltage, n_bits: int, v_min: float, v_max: float):
    """理想的なADCとして動作する関数 / Function that emulates an ideal ADC.

    Parameters
    ----------
    input_voltage : ndarray
        入力波形の配列 / Input voltage waveform array.
    n_bits : int
        ADCの分解能 (ビット数) / ADC resolution in bits.
    v_min : float
        入力レンジの最小値 / Minimum input range.
    v_max : float
        入力レンジの最大値 / Maximum input range.

    Returns
    -------
    digital_level : ndarray
        ADCのデジタルコード / Quantized digital levels (codes).
    """

    # LSBの計算 / Calculate the LSB (Least Significant Bit) size
    LSB = (v_max - v_min) / (2**n_bits)

    # 量子化 / Quantize the input voltages
    digital_level = ((input_voltage - v_min) / LSB).astype(int)

    # コードが範囲外にならないようにクリップ / Clip the levels to valid range
    digital_level = np.clip(digital_level, 0, 2**n_bits - 1)

    return digital_level


def sine_wave_generator(N: int, f_signal: float, fs: float, amplitude: float, offset: float):
    """サイン波を生成する関数 / Function to generate a sine wave.

    Parameters
    ----------
    N : int
        データ点数 / Number of data points.
    f_signal : float
        信号の周波数 [Hz] / Signal frequency in Hz.
    fs : float
        サンプリング周波数 [Hz] / Sampling frequency in Hz.
    amplitude : float
        振幅 / Signal amplitude.
    offset : float
        オフセット電圧 / DC offset of the signal.

    Returns
    -------
    signal : ndarray
        生成されたサイン波 / Generated sine wave array.
    """

    t_sampling = 1 / fs         # サンプリング周期 / Sampling period
    duration = N * t_sampling   # 波形の全長(時間) / Total time length
    t = np.arange(0, duration, t_sampling)

    # サイン波を生成 / Generate sine wave
    signal = amplitude * np.sin(2 * np.pi * f_signal * t) + offset
    return signal


# ======== Global variables for real-time AC plots (リアルタイムプロット用のグローバル変数) ========
is_firstplot_AC = True
AC_axes1 = None
AC_axes2 = None
AC_wav_lines1 = None
AC_wav_lines2 = None
AC_text1 = ""
AC_text2 = ""
AC_text3 = ""


def AC_evaluation(bit: int, data, N: int, fs: float, f_in: float, realtime=False):
    """ADCのAC特性評価を行う関数 / Function for AC characteristic evaluation of ADC

    - SNDR, ENOB, SFDR等を算出してプロットします
      Computes SNDR, ENOB, SFDR, and plots results.

    Parameters
    ----------
    bit : int
        ビット数 / ADC resolution in bits.
    data : ndarray
        AD変換後のデータ配列 / Array of ADC output codes.
    N : int
        データ点数 / Number of data points.
    fs : float
        サンプリング周波数 [Hz] / Sampling frequency in Hz.
    f_in : float
        入力信号の周波数 [Hz] / Input signal frequency in Hz.
    realtime : bool, optional
        リアルタイム測定を行うかどうか(デフォルト：False) / 
        Whether to update plots in real-time (default: False)
    """
    global is_firstplot_AC, AC_axes1, AC_axes2, AC_wav_lines1, AC_wav_lines2
    global AC_text1, AC_text2, AC_text3

    # ADC出力をフルスケールで正規化 / Normalize the ADC output by full-scale range
    f = np.divide(data, 2 ** (bit - 1))

    dt = 1 / fs               # サンプリング周期 / Sampling period
    t = np.arange(0, N * dt, dt)  # 時間軸 / Time axis

    # FFT周波数軸を作成 / Create frequency axis for FFT
    freq = np.fft.fftfreq(N, dt)
    freq_norm = freq / fs  # fsで正規化 / Normalize by fs

    # FFTを計算 / Perform FFT
    F = np.fft.fft(f) * 2 / N

    # パワースペクトルを計算 / Compute power spectrum
    Power = np.abs(F) ** 2

    # パワースペクトル[dB] (フルスケール基準) / 
    # Convert to dB (Full-scale reference, power => multiply by 10)
    Pow_dB = 10 * np.log10(Power / 1.0)

    # シグナル成分のインデックスを探す(DC成分を除いた最大値) / 
    # Find the index of maximum (excluding DC)
    signal_index = np.argmax(Power[1 : int(N / 2)]) + 1
    Signal = Power[signal_index]

    # ノイズパワー計算(直流成分とシグナルを除いた部分) / 
    # Noise power excluding DC and the main signal
    Noise = np.sum(Power[1 : int(N / 2)]) - Signal

    # SNDR, ENOBを算出 / Calculate SNDR and ENOB
    SNDR = 10 * np.log10(Signal / Noise)
    ENOB = (SNDR - 1.76) / 6.02

    # 高調波スプリアス(SFDR)計算(最大4次を検索) / 
    # Compute SFDR by checking up to 4th harmonic
    spurious = []
    for i in range(2, 5):
        # n次高調波近傍(±2)を検索 / 
        # Search around the harmonic index ±2
        spurious.append(np.max(Pow_dB[(signal_index * i) - 2 : signal_index * i + 2]))
    SFDR = Pow_dB[signal_index] - np.max(spurious)

    print("SNDR=", SNDR, "dB")
    print("ENOB=", ENOB, "bit")
    print("SFDR=", SFDR, "dB")

    # 波形を1周期に並び替え / Sort waveform data to single cycle
    sorted_data = wave_sort(data, 1 / dt, f_in)

    # ---------------- Plotting ----------------
    if is_firstplot_AC:
        fig = plt.figure(figsize=(10, 5))

        # 1つ目のグラフ: Sorted Wave
        AC_axes1 = fig.add_subplot(1, 2, 1)
        AC_axes1.set_title("Sorted Wave")
        AC_axes1.set_xlabel("Time [sec]")
        AC_axes1.set_ylabel("Code")
        AC_axes1.grid()
        AC_axes1.set_ylim(0, 2**bit)

        # 2つ目のグラフ: Power Spectrum
        AC_axes2 = fig.add_subplot(1, 2, 2)
        AC_axes2.set_title("Power Spectrum")
        AC_axes2.set_xlabel("f/fs")
        AC_axes2.set_ylabel("Power [dB]")
        AC_axes2.grid()

        # テキスト表示領域 / Text boxes to display calculated values
        AC_text1 = AC_axes2.text(0.3, -5.3, "SNDR = " + str(round(SNDR, 2)) + " dB")
        AC_text2 = AC_axes2.text(0.3, -10.3, "ENOB = " + str(round(ENOB, 2)) + " bit")
        AC_text3 = AC_axes2.text(0.3, -15.3, "SFDR = " + str(round(SFDR, 2)) + " dB")

        # 波形のプロット / Plot the sorted waveform
        (AC_wav_lines1,) = AC_axes1.plot(
            t, sorted_data, marker=".", markersize=5, label="f(n)"
        )

        # FFTスペクトラムのプロット / Plot the FFT spectrum
        (AC_wav_lines2,) = AC_axes2.plot(
            freq_norm[1 : int(N / 2)], Pow_dB[1 : int(N / 2)], label="|F(k)|"
        )

        fig.tight_layout()

        # 画像を保存 / Save figures 
        # (Google Colabの場合はパス区切りに注意)
        save_fig(plt, os.getcwd() + "/temp", "AC_Analysis")

        is_firstplot_AC = False

    else:
        # 値の更新 / Update text
        AC_text1.set_text("SNDR = " + str(round(SNDR, 2)) + " dB")
        AC_text2.set_text("ENOB = " + str(round(ENOB, 2)) + " bit")
        AC_text3.set_text("SFDR = " + str(round(SFDR, 2)) + " dB")

        # 波形プロットの更新 / Update waveform plot
        AC_wav_lines1.set_data(t, sorted_data)

        # スペクトラムプロットの更新 / Update spectrum plot
        AC_wav_lines2.set_data(freq_norm[1 : int(N / 2)], Pow_dB[1 : int(N / 2)])

    # リアルタイム表示 / Real-time update
    if realtime:
        plt.pause(0.1)
    else:
        plt.show()


# ======== Global variables for real-time DC plots (リアルタイムプロット用のグローバル変数) ========
is_firstplot_DC = True
DC_axes1 = None
DC_axes2 = None
DC_wav_lines1 = None
DC_wav_lines2 = None
DC_text1 = ""
DC_text2 = ""


def DC_evaluation(data, n_bits: int, realtime=False):
    """ADCのDC特性評価を行う関数 / Function for DC characteristic evaluation of an ADC

    - ヒストグラムからDNL/INLを求めてプロットします
      Computes DNL and INL from the histogram of ADC codes, and plots results.

    Parameters
    ----------
    data : ndarray
        AD変換後のデータ配列 / Array of ADC output codes.
    n_bits : int
        ADCのビット数 / ADC resolution in bits.
    realtime : bool, optional
        リアルタイム測定を行うかどうか(デフォルト：False) / 
        Whether to update plots in real-time (default: False)
    """
    global is_firstplot_DC, DC_axes1, DC_axes2
    global DC_wav_lines1, DC_wav_lines2, DC_text1, DC_text2

    # 変数の準備 / Prepare local variables
    wave = data
    code_bit = n_bits

    # =============== ヒストグラム生成 / Generate histogram ================
    # 各コードが出現した回数をカウント / Count occurrences of each code
    wave_hist, hist_bins = np.histogram(
        wave, bins=2**code_bit, range=(0, 2**code_bit)
    )
    del hist_bins  # bins情報は使わないので削除

    # ========= 範囲指定(DNL計算のため、ピークコードを検出) =========
    # Detect the code range that forms the main "sine wave region" peaks

    # 前半の最大ピーク / Find the highest peak in the first half
    peek_1 = np.argmax(wave_hist[: len(wave_hist) // 2 - 1])
    # 後半の最大ピーク / Find the highest peak in the second half
    peek_2 = (len(wave_hist) // 2) + np.argmax(wave_hist[len(wave_hist) // 2 :])

    # これから微調整を行うため、シフト量カウンタを準備 / 
    # We will tweak these peaks, so track the shift
    shift_peek_1 = 0
    shift_peek_2 = 0

    # ピーク位置を微調整 / Adjust left peak
    reset = 1
    while True:
        # 新しいヒストグラムの一部 / Recalculate partial histogram
        wave_hist_pp = np.histogram(
            wave, bins=([0] + list(range(peek_1 + 1, peek_2)) + [2**code_bit])
        )[0]
        wave_pDensity_pp = wave_hist_pp / np.sum(wave_hist_pp)

        # 期待される正規化分布(Pik)をsin^-1で計算 / 
        # Ideal probability distribution for a sine wave (arcsine method)
        wave_hist_full = peek_2 - peek_1
        k = np.arange(wave_hist_full)
        Pa = 2 * (k + 1) / wave_hist_full - 1
        Pb = 2 * k / wave_hist_full - 1
        Pik = (1 / np.pi) * (np.arcsin(Pa) - np.arcsin(Pb))

        if reset == 1:
            peek_1 -= 1
            shift_peek_1 += 1
            temp = np.abs(wave_pDensity_pp[0] - Pik[0])
            reset = 0
            continue
        else:
            diff = np.abs(wave_pDensity_pp[0] - Pik[0])
            if diff < temp:
                peek_1 -= 1
                temp = diff
                shift_peek_1 += 1
                continue
            else:
                peek_1 += 1
                shift_peek_1 -= 1
                break

    # ピーク位置を微調整 / Adjust right peak
    reset = 1
    flag = 0
    while True:
        wave_hist_pp = np.histogram(
            wave, bins=([0] + list(range(peek_1 + 1, peek_2)) + [2**code_bit])
        )[0]
        wave_pDensity_pp = wave_hist_pp / np.sum(wave_hist_pp)

        code_pp = peek_2 - peek_1
        k = np.arange(code_pp)
        Pa = (2 * (k + 1) / code_pp) - 1
        Pb = (2 * k / code_pp) - 1
        Pik = (1 / np.pi) * (np.arcsin(Pa) - np.arcsin(Pb))

        if reset == 1:
            peek_2 += 1
            shift_peek_2 += 1
            temp = np.abs(wave_pDensity_pp[-1] - Pik[-1])
            reset = 0
            continue
        if flag == 1:
            break
        else:
            diff = np.abs(wave_pDensity_pp[-1] - Pik[-1])
            if diff < temp:
                peek_2 += 1
                temp = diff
                shift_peek_2 += 1
                continue
            else:
                peek_2 -= 1
                shift_peek_2 -= 1
                flag = 1
                continue

    # 再計算 / Recompute final partial histogram and ideal distribution
    wave_hist_pp = np.histogram(
        wave, bins=([0] + list(range(peek_1 + 1, peek_2)) + [2**code_bit])
    )[0]
    wave_pDensity_pp = wave_hist_pp / np.sum(wave_hist_pp)

    code_pp = peek_2 - peek_1
    k = np.arange(code_pp)
    Pa = (2 * (k + 1) / code_pp) - 1
    Pb = (2 * k / code_pp) - 1
    Pik = (1 / np.pi) * (np.arcsin(Pa) - np.arcsin(Pb))

    # ゼロ埋めして全範囲で扱えるようにする / Zero-padding to reconstruct full range
    wave_pDensity = np.array(
        [0] * peek_1 + list(wave_pDensity_pp) + [0] * (len(wave_hist) - peek_2)
    )
    wave_ideal_pDensity = np.array(
        [0] * peek_1 + list(Pik) + [0] * (len(wave_hist) - peek_2)
    )

    # ============ DNL(微分線形性)算出 / Calculate DNL ============
    # DNL_ppは部分範囲のみ計算 / Partial range DNL
    DNL_pp = (wave_pDensity_pp - Pik) / Pik
    # DNLの平均値を0に調整(積分時のオフセットエラーを考慮) / 
    # Adjust mean to 0 to handle offset error
    DNL_pp = DNL_pp - np.mean(DNL_pp)

    # フルコードに合わせてゼロ埋め / Zero-pad to full code range
    DNL = np.array([0] * peek_1 + list(DNL_pp) + [0] * (len(wave_hist) - peek_2))

    MAX_DNL = np.max(DNL)
    MIN_DNL = np.min(DNL)

    # ============ INL(積分線形性)算出 / Calculate INL ============
    temp_inl = 0
    INL = [0 for i in range(2**code_bit)]
    for i in range(2**code_bit):
        temp_inl += DNL[i]
        INL[i] = temp_inl

    MAX_INL = np.max(INL)
    MIN_INL = np.min(INL)

    print("Left/Right offset for peaks: -" + str(shift_peek_1) + "/+" + str(shift_peek_2))
    print(f"DNL range [LSB]: {MIN_DNL:.2f} to {MAX_DNL:.2f}")
    print(f"INL range [LSB]: {MIN_INL:.2f} to {MAX_INL:.2f}")

    # -------------- Plotting --------------
    if is_firstplot_DC:
        fig = plt.figure(figsize=(10, 8))

        # 1つ目のグラフ: DNL
        DC_axes1 = fig.add_subplot(2, 1, 1)
        DC_axes1.set_title("DNL")
        DC_axes1.set_xlabel("Code")
        DC_axes1.set_ylabel("DNL [LSB]")
        DC_axes1.grid()

        # 2つ目のグラフ: INL
        DC_axes2 = fig.add_subplot(2, 1, 2)
        DC_axes2.set_title("INL")
        DC_axes2.set_xlabel("Code")
        DC_axes2.set_ylabel("INL [LSB]")
        DC_axes2.grid()

        # テキスト表示: DNLとINLの最大最小 / Show text for DNL/INL max/min
        DC_text1 = DC_axes1.text(
            0, 1.5,
            "DNL = "
            + format(MIN_DNL, "4.2f")
            + "[LSB] / +"
            + format(MAX_DNL, "4.2f")
            + "[LSB]"
        )
        DC_text2 = DC_axes2.text(
            0, 1.5,
            "INL = "
            + format(MIN_INL, "4.2f")
            + "[LSB] / +"
            + format(MAX_INL, "4.2f")
            + "[LSB]"
        )

        (DC_wav_lines1,) = DC_axes1.plot(DNL)
        (DC_wav_lines2,) = DC_axes2.plot(INL)

        fig.tight_layout()
        # 画像を保存 / Save figure (adjust path for Colab)
        save_fig(plt, os.getcwd() + "/temp", "DNL_INL")

        is_firstplot_DC = False

    else:
        # テキスト更新 / Update text
        DC_text1.set_text(
            "DNL = "
            + format(MIN_DNL, "4.2f")
            + "[LSB] / +"
            + format(MAX_DNL, "4.2f")
            + "[LSB]"
        )
        DC_text2.set_text(
            "INL = "
            + format(MIN_INL, "4.2f")
            + "[LSB] / +"
            + format(MAX_INL, "4.2f")
            + "[LSB]"
        )

        # グラフデータ更新 / Update graph data
        x = np.arange(0, len(DNL))
        DC_wav_lines1.set_data(x, DNL)
        DC_wav_lines2.set_data(x, INL)

    # リアルタイム表示 / Real-time update
    if realtime:
        plt.pause(0.1)
    else:
        plt.show()


# =========================== Usage Example (使用例) ============================
if __name__ == "__main__":
    # ----------------- AC評価 -----------------
    # データ点数: 2^N
    N = 12
    # サンプリング周波数 [Hz]
    fs = 200e3

    # コヒーレントサンプリングに必要な入力周波数を算出
    f_in = freq_search(N, fs, 8)

    # 入力信号を生成 (振幅2.5V, オフセット2.5V)
    v_signal = sine_wave_generator(2**N, f_in, fs, 2.5, 2.5)

    # ノイズを付加
    v_signal += 10e-3 * np.random.rand(2**N)

    # 理想ADCで変換 (8ビット, 0~5V)
    data = ideal_adc(v_signal, 8, 0, 5)

    # AC解析
    AC_evaluation(8, data, 2**N, fs, f_in)

    # ----------------- DC評価 -----------------
    # データ点数: 2^N
    N = 16
    # サンプリング周波数 [Hz]
    fs = 200e3

    # コヒーレントサンプリングに必要な入力周波数を算出
    f_in = freq_search(N, fs, 8)

    # 入力信号を生成 (振幅5V, オフセット5V)
    v_signal = sine_wave_generator(2**N, f_in, fs, 5, 5)

    # ノイズを付加
    v_signal += 10e-3 * np.random.rand(2**N)

    # 理想ADCで変換 (8ビット, 0~10V)
    data = ideal_adc(v_signal, 8, 0, 10)

    # DC解析
    DC_evaluation(data, 8)

```