• トップ
  • ブログ一覧
  • Pythonによるオシロスコープ波形データ解析の秘訣(ノイズ分析編)
  • Pythonによるオシロスコープ波形データ解析の秘訣(ノイズ分析編)

    広告メディア事業部広告メディア事業部
    2021.09.02

    IT技術

    Pythonでノイズ分析してみた!

    今回は、オシロスコープで得られた波形中の周期的なノイズ成分を Python で解析する方法を紹介します。

    解析の方法としては、周期成分を抽出できるフーリエ変換を活用していき、同時にフーリエ変換の結果を上手くグラフ化する方法も説明していきます。

    今回も Python で生成した波形データサンプルを使ってデータ解析をしていきます。

    波形データの周期ノイズ

    設計した電気回路の現物評価を行なっていく上で、直面する代表的な障害にノイズの存在があります。

    電気回路のノイズには様々なタイプがありますが、最もやっかいなものは回路内の電子部品が発するタイプです。

    このタイプは、電子部品自体の動作周波数に合わせる形でノイズが発生し、関係のない回路に混入することでその回路の動作を妨害することがあります。

    このようなノイズの発生源を特定するためには、その特性であるノイズ周波数を特定することが重要となっています。

    その周波数を特定する方法として有用な方法がフーリエ変換です。

    フーリエ変換とは

    フーリエ変換とは、あらゆるデータ群は周期の異なる sin/cos の三角関数の重ね合わせでできていると想定し、それぞれの周期の三角関数がデータ群にどの程度含まれているかを数値化することができる手段です。

    上記はかなり曲解した表現ではありますが、ここでは他のサイトであるような学術的な省き、実用面にフォーカスした説明をしていきます。

    例えば、

    単純な正弦波

    のような単純な正弦波であればフーリエ変換を行うと下図のように1つの鋭いピークが得られます。

    単純な正弦波のフーリエ変換後の画像

    このピークが立った位置の X 座標を読み取ることで、正弦波の周波数を検出することができるのです。

    三角関数の含まれないデータであったとしても周期的なパターンであれば同様にピークとして検出することができます。

    これは、どのような波形であっても周期がわずかにずれた三角関数の重ね合わせとして扱われるためです。

    フーリエ変換のための条件

    ノイズの周波数を分析したい波形データに対してフーリエ変換を行えば、波形に含まれる周期パターンの周波数を検出できることはわかりました。

    では、実際にどのようなデータでも分析することができるかといいますと、それには条件があります。

    まず、検出するノイズの周波数について、元の波形のサンプリング間隔が十分細かくなければ高い周波数のノイズは検出することはできません。

    また、元のデータのサンプリング数、つまり周期パターンの数を十分大きくデータ内に収めなければ正確な周波数の値を検出することができません。

    波形データは有限かつ離散的なものですので、解析するデータを取得する段階でどのようなサンプリング間隔でどの程度の期間のものとするかが重要となってくるわけです。

    フーリエ変換後のデータ

    波形データのような離散的かつ、有限なデータをフーリエ変換する場合、これは離散フーリエ変換と呼ばれ、フーリエ変換後のデータのサンプリング間隔は以下のような式で表せます。

    (フーリエ変換後のサンプリング間隔[Hz])=1÷(データのサンプル数)÷(元データのサンプリング間隔[sec])

    前述したようにデータ数が多ければ間隔が短くなり、より正確な周波数を特定できることがわかります。

    本記事で Python を使って周波数分析結果をプロットしていく際には、X 軸をこの単位の整数倍とする点に注意しておきましょう。

    Python によるフーリエ変換

    上で述べたフーリエ変換を Python で行うために特別なモジュールは必要ではありません。

    基本モジュールの numpy 高速フーリエ変換(FFT)のメソッドがあり、それを活用する形です。

    FFTは通常のフーリエ変換に対して計算を省略した方式ですが、精度面から言っても問題はありません。

    使い方は分析したいデータ群を配列として引き渡すだけとなっています。

    先程述べたように、変換結果のサンプル単位がデータ数に応じた周波数となることには注意しましょう。

    この記事の目的

    本記事では、正弦波に2つの周期的なノイズが重畳した波形を対象に、フーリエ変換を活用してノイズの周期成分を特定します。

    ここでの波形は、オシロスコープで取得されたデータを想定しており、今までの記事と同様にcsv形式のデータを読み出すことを行います。

    手順としては、まず Python のコード上で csv 波形データを読み出し、次に読み出したデータをフーリエ変換しつつ、フーリエ変換後の X 座標データを作成します。

    グラフだけではピーク値がよく分かりませんので、ピーク検出をコード上で行い、整数倍の周波数を除いた結果が得られるようにします。

    準備

    今回のコード実行に必要な環境は下記になります。

    インストールが必要なもの

    1. Python の開発環境(Spyder など)
    2. Pandas モジュール(波形データ生成及び csv 読み出しに使用)
    3. math モジュール(波形データ生成に必要)
    4. random モジュール(波形データ生成に必要

    筆者の実行環境

    1. Python(3.6)
    2. Spyder(3.2.6)

    解析する波形データ

    今回、解析に使用する波形データの例は下記のコードを実行することで作成することができます。

    コード本文にもあるように、データのサンプリング間隔を10usとし、1000Hz の正弦波に対して、

    1. ノイズ1:周期600us、周波数1666.7Hz
    2. ノイズ2:周期1400us、周波数714.3Hz

    の2つの波形が加算された合成波形が対象となるデータです。

    1import pandas as pd
    2import random
    3import math 
    4import matplotlib.pyplot as plt
    5import numpy as np
    6
    7X_SAMPLE = 6000
    8NOISE_SEED_OFF = 123
    9WAVE_SMP_TIME = 10*10**-6   #[sec] => 10us
    10
    11#sin_wave
    12SIN_INT = 100
    13RANDOM_NOISE_AMP=0.1
    14AMP= 5.0
    15
    16#noise_1
    17NOISE_1_INT = 60
    18NOISE_1_INT_OFF = -20
    19AMP_NOISE_1 = 1.4
    20SIGMA_NOISE_1 = 5
    21noise1_int = NOISE_1_INT*WAVE_SMP_TIME
    22noise1_freq = np.round(1/noise1_int,1)
    23
    24#noise_2
    25NOISE_2_INT = 140
    26NOISE_2_INT_OFF = -35
    27AMP_NOISE_2 = 1.0
    28SIGMA_NOISE_2 = 8
    29noise2_int = NOISE_2_INT*WAVE_SMP_TIME
    30noise2_freq = np.round(1/noise2_int,1)
    31
    32xx = [ i for i in range(X_SAMPLE)]
    33
    34analog_wave=[]
    35random_noise=[]
    36for i in range(len(xx)):
    37    random.seed(NOISE_SEED_OFF+i)
    38    random_noise.append(RANDOM_NOISE_AMP*random.randint(-100,100)/100)
    39    analog_wave.append(AMP* math.sin(2/SIN_INT*xx[i]*math.pi)+random_noise[i])
    40
    41noise_1=[]
    42cnt_1=0
    43for i in range(len(xx)):
    44    if i % NOISE_1_INT == 0:
    45        cnt_1+=1
    46    noise_peak_time = NOISE_1_INT* cnt_1 + NOISE_1_INT_OFF
    47    noise_1.append(AMP_NOISE_1*math.exp(-((i- noise_peak_time)**2 )/(2*SIGMA_NOISE_1**2)))
    48
    49noise_2=[]
    50cnt_2=0
    51for i in range(len(xx)):
    52    if i % NOISE_2_INT == 0:
    53        cnt_2+=1
    54    noise_peak_time = NOISE_2_INT* cnt_2 + NOISE_2_INT_OFF
    55    noise_2.append(AMP_NOISE_2*math.exp(-((i- noise_peak_time)**2 )/(2*SIGMA_NOISE_2**2)))
    56
    57
    58#合成
    59merge_wave =[analog_wave[i]+noise_1[i]+noise_2[i] for i in range(X_SAMPLE)]
    60
    61#ノイズ1のプロット
    62plt.figure(figsize=(10,8))    
    63plt.plot(xx,noise_1)
    64plt.title("Noise1,Int=" + str( np.round(noise1_int*10**(6),1))+ "[us],Freq="+str(noise1_freq)+"[Hz]")
    65plt.savefig("./analog_noise_1.jpg")
    66plt.show()
    67
    68#ノイズ2のプロット
    69plt.figure(figsize=(10,8))    
    70plt.plot(xx,noise_2)
    71plt.title("Noise2,Int=" + str( np.round(noise2_int*10**(6),1))+ "[us],Freq="+str(noise2_freq)+"[Hz]")
    72plt.savefig("./analog_noise_2.jpg")
    73plt.show()
    74
    75#合成波形のプロット
    76plt.figure(figsize=(10,8))   
    77plt.plot(xx,merge_wave)
    78# plt.title("Merged Wave")
    79plt.savefig("./analog_multi_noise.jpg")
    80plt.show()
    81    
    82wave_data= pd.DataFrame({"multi_noise":merge_wave})
    83wave_data.to_csv("analog_multi_noise.csv")
    波形データ

    コード本文

    それでは、いよいよ分析を行うコードを実行していきます。

    今回のフーリエ変換では、今までの記事のようなメモリを節約できる逐次処理は使うことができません。

    波形データ全体を同時に使うことでしかフーリエ変換を行うことはできないからです。

    ⓪モジュールのインポート

    最初は、使用するモジュールのインポートからです。

    普段よく使う3つをインポートすれば問題ありません。

    1import matplotlib.pyplot as plt
    2import numpy as np
    3import pandas as pd

    ①波形データの読み出しとフーリエ変換後のX軸単位を計算

    次にcsvの読み出しとフーリエ変換後の検出可能な周波数単位を確認していきます。

    1WAVE_SMP_TIME = 10*10**-6 #[sec] => 10us
    2wave_path = "./analog_multi_noise.csv"
    3wave_df = pd.read_csv(wave_path,index_col=0)
    4smp_count = len(wave_df.index.values)
    5x_unit_fft = 1/WAVE_SMP_TIME/smp_count  #[Hz]
    6print(str(x_unit_fft)+"[Hz]")

    今回は csv データの呼び出しを Pandas で行っています。

    これは、今回のように全データ一括の処理を行う場合には、シンプルかつ管理がしやすいためです。

    読み出したデータのサンプル数を取得し、データのサンプリング間隔を定義した後は、前述した式に基づいてフーリエ変換後の X 軸の単位(周波数分解能)を算出します。

    今回の例では10us ごとのデータとしているので、16.67Hz となりました。

    ここで算出した値は分解能としての意味を持つので、この周波数の整数倍でしか検出ができないということになります。

    本記事ではこのまま進めますが、実際に波形測定から行う場合、なるべくたくさんのサンプルデータを取ることで測定精度上昇につながることを留意しておいてください。

    データ数が多ければ処理時間が長くなり、メモリも多く必要となりますが、高周波ノイズを捉えることが目的でないならば、サンプリング間隔を広くとることでも周波数分解能を上げられることも覚えておきましょう。

    ②フーリエ変換の実行

    そしていよいよフーリエ変換を実行します。

    今回は numpy の FFT メソッドを使っていき、同時にグラフ作成用のX軸データも生成します。

    変換後のデータは複素数なので絶対値も忘れずにとりましょう。

    1##フーリエ変換後のX座標単位からグラフプロット用のリストを作成する
    2fft_x = [i*x_unit_fft for i in range(smp_count)]
    3##FFT処理を実施する
    4wave_fft = np.fft.fft(wave_df.iloc[:,0].values)
    5##処理後の値は複素数なので絶対値をとる
    6wave_fft_abs=abs(wave_fft)

    ③ピーク位置の算出

    次に、ピーク位置の検出を行います。

    Python のモジュールには自動でピークを検出するメソッドは存在しないので、処理は工夫する必要があります。

    1peak_lst=[]
    2peak_lst_raw=[]
    3itr_ct = int(len(wave_fft_abs)/2)   #FFT後のX軸で中心から右半分は負の周波数なので無視する
    4PEAK_TH=40 #データに合わせて調整する
    5##変換後のリストの半分を対象にピーク検出を行う
    6for i in range(itr_ct):
    7    #ピーク検出の仕様上、最初と最後のインデックスは計算不可なのでスキップ
    8    if i!=0 and i!= itr_ct:
    9        #簡易的に対象のFFT値が前後の値より大きければピークと判定する
    10        if wave_fft_abs[i] - wave_fft_abs[i-1]>PEAK_TH \
    11        and wave_fft_abs[i] - wave_fft_abs[i+1]>PEAK_TH:
    12            peak_lst_raw.append([i,fft_x[i],wave_fft_abs[i]])
    13            #最初のピーク値は必ず記録する
    14            if len(peak_lst)==0:
    15                peak_lst.append([i,fft_x[i],wave_fft_abs[i]])
    16            #検出したピーク値が既に検出したピークの整数倍なら無視する(離散化の影響を考慮し、±1のずれも同等とみなす)
    17            elif not 0 in [i % peak_lst[ii][0] for ii in range(len(peak_lst))] \
    18                and not 0 in [(i-1) % peak_lst[ii][0] for ii in range(len(peak_lst))] \
    19                 and not 0 in [(i+1) % peak_lst[ii][0] for ii in range(len(peak_lst))]:
    20                peak_lst.append([i,fft_x[i],wave_fft_abs[i]])

    検出方法としてはデータの最初からひとつずつ見ていき、前後の値より大きいポイントをピークとしてその X 座標と値を記録していく形です。

    ここで、

    1itr_ct = int(len(wave_fft_abs)/2)   #FFT後のX軸で中心から右半分は負の周波数なので無視する

    のように検出範囲をデータの半分としていますが、これはフーリエ変換の性質上、負の周波数が後半の半分に現れるため、それを検出しないようにしているためです。

    また、ポイントとして、検出したい周波数の整数倍の周波数もピークとして出てきてしまうという特徴があります。

    ノイズの解析においては、検出したいのは整数倍されていない周波数なので、for 文の中の処理で、既に検出した周波数の整数倍は無視するようにしています。

    分解能の関係で、必ずしも整数倍で表れませんので、プラスマイナス1ポイントのズレも整数倍とみなすコードになっています。

    そして、検出した結果はリストに追加します。

    ④FFTによる周波数分析結果をグラフにプロット

    最後に分析結果をグラフ化します。

    今回のデータの分解能からすると、負の周波数を除けば最大で50MHz まで表示されてしまうので、グラフの描画範囲を絞ります。

    1FQ_SEARCH_ST = 10  #[Hz]
    2FQ_SEARCH_ED = 8000   #[Hz]
    3##グラフ描画範囲を設定する
    4st_index = np.abs(np.asarray(fft_x) - FQ_SEARCH_ST).argmin()
    5ed_index = np.abs(np.asarray(fft_x) - FQ_SEARCH_ED).argmin()

    そしておなじみ matplotlib でグラフ作成を行います。

    1##グラフのプロットとピーク検出で検出した値を表示させる
    2plt.figure(figsize=(10,8))    
    3plt.plot(fft_x[st_index:ed_index],wave_fft_abs[st_index:ed_index],lw=1)
    4for i in range(len(peak_lst)):
    5    plt.annotate(str(np.round(peak_lst[i][1],1))+"Hz",xy =(peak_lst[i][1],peak_lst[i][2]))
    6plt.savefig("./fft_result.jpg")
    7plt.show()

    annotate を使うことでプロット上にコメントを表示することができ、先ほど検出したピークのみに周波数を表示させることができます。

    周波数分析結果をグラフにプロット

    作成したグラフは上図の通りで、データ作成時に1000Hz の正弦波に重畳させた1666.7Hz 714.3Hz のノイズを検出できていることがわかります。

    分解能の影響でわずかに周波数値が異なる結果となっていますが、この程度の差であれば問題なく識別できるはずです。

    さらにピークの大きさからノイズの大きさも相対的に判別することができるため、影響度の大きさも比較することができます。

    サンプルスクリプト

    最後に今まで行ったコードをコピペで実行できるように全文を下記に記載します。

    基本的には、元データのサンプリング間隔と表示したい周波数範囲さえ調整すればどのようなデータにも対応できるはずです。

    1#⓪モジュールのインポート
    2import matplotlib.pyplot as plt
    3import numpy as np
    4import pandas as pd
    5
    6#設定値関連
    7WAVE_SMP_TIME = 10*10**-6   #[sec] => 10us
    8FQ_SEARCH_ST = 10  #[Hz]
    9FQ_SEARCH_ED = 8000   #[Hz]
    10PEAK_TH=40  #データに合わせて調整する
    11
    12#①波形データの読み出しとフーリエ変換後のX軸単位を計算
    13wave_path = "./analog_multi_noise.csv"
    14wave_df = pd.read_csv(wave_path,index_col=0)
    15smp_count = len(wave_df.index.values)
    16x_unit_fft = 1/WAVE_SMP_TIME/smp_count  #[Hz]
    17print(str(x_unit_fft)+"[Hz]")
    18
    19#②フーリエ変換の実行
    20##フーリエ変換後のX座標単位からグラフプロット用のリストを作成する
    21fft_x = [i*x_unit_fft for i in range(smp_count)]
    22##FFT処理を実施する
    23wave_fft = np.fft.fft(wave_df.iloc[:,0].values)
    24##処理後の値は複素数なので絶対値をとる
    25wave_fft_abs=abs(wave_fft)
    26
    27#③ピーク位置の算出
    28peak_lst=[]
    29peak_lst_raw=[]
    30itr_ct = int(len(wave_fft_abs)/2)   #FFT後のX軸で中心から右半分は負の周波数なので無視する
    31##変換後のリストの半分を対象にピーク検出を行う
    32for i in range(itr_ct):
    33    #ピーク検出の仕様上、最初と最後のインデックスは計算不可なのでスキップ
    34    if i!=0 and i!= itr_ct:
    35        #簡易的に対象のFFT値が前後の値より大きければピークと判定する
    36        if wave_fft_abs[i] - wave_fft_abs[i-1]>PEAK_TH \
    37        and wave_fft_abs[i] - wave_fft_abs[i+1]>PEAK_TH:
    38            peak_lst_raw.append([i,fft_x[i],wave_fft_abs[i]])
    39            #最初のピーク値は必ず記録する
    40            if len(peak_lst)==0:
    41                peak_lst.append([i,fft_x[i],wave_fft_abs[i]])
    42            #検出したピーク値が既に検出したピークの整数倍なら無視する(離散化の影響を考慮し、±1のずれも同等とみなす)
    43            elif not 0 in [i % peak_lst[ii][0] for ii in range(len(peak_lst))] \
    44                and not 0 in [(i-1) % peak_lst[ii][0] for ii in range(len(peak_lst))] \
    45                 and not 0 in [(i+1) % peak_lst[ii][0] for ii in range(len(peak_lst))]:
    46                peak_lst.append([i,fft_x[i],wave_fft_abs[i]])
    47
    48#④FFTによる周波数分析結果をグラフにプロット
    49##グラフ描画範囲を設定する
    50st_index = np.abs(np.asarray(fft_x) - FQ_SEARCH_ST).argmin()
    51ed_index = np.abs(np.asarray(fft_x) - FQ_SEARCH_ED).argmin()
    52##グラフのプロットとピーク検出で検出した値を表示させる
    53plt.figure(figsize=(10,8))    
    54plt.plot(fft_x[st_index:ed_index],wave_fft_abs[st_index:ed_index],lw=1)
    55for i in range(len(peak_lst)):
    56    plt.annotate(str(np.round(peak_lst[i][1],1))+"Hz",xy =(peak_lst[i][1],peak_lst[i][2]))
    57plt.savefig("./fft_result.jpg")
    58plt.show()

    こちらの記事もオススメ!

    featureImg2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
    featureImg2020.07.30Python 特集実装編※最新記事順Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた!P...

    広告メディア事業部

    広告メディア事業部

    おすすめ記事