• トップ
  • ブログ一覧
  • 【第3回】Pythonによるオシロスコープ波形データ解析の秘訣【アナログデータ編】
  • 【第3回】Pythonによるオシロスコープ波形データ解析の秘訣【アナログデータ編】

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

    IT技術

    Python でオシロスコープ波形データを解析しよう!

    前回は、オシロスコープで波形データを取得する意義について解説しました。

    今回は、オシロスコープで得た「csv 形式の波形データを、Python で解析する方法」を紹介します。

    Python で波形データを解析するメリット

    波形データ解析に Python を用いると、以下のようなメリットがあります。

    1. スクリプト型言語なので、トライアンドエラーでデバッグが迅速
    2. 数学に強いライブラリが充実しているため、複雑な演算も可能
    3. グラフ描画ライブラリで、解析結果をすぐに可視化できる
    4. PyVISA との連携で、データ取得から解析まで一括でできる

    実際にどうやって解析するのか?

    今回ご紹介するのは、PC メモリを圧迫しない「時系列逐次処理」を使った解析方法です。

    実際のアナログ波形データを想定した正弦波と RC 回路の過渡応答波形を用いて、最大・最小と立ち上がり時間の測定を行います。

    最後に、csv 読み込みからデータ取得まで行えるサンプルコードを記載していますので、活用してみてください。

    前回の記事はこちら

    Pythonでオシロスコープの波形データを自動転送してみた!2021.02.16Pythonでオシロスコープの波形データを自動転送してみた!Python + PyVISA でオシロスコープの波形データを自動転送してみよう!前回は、PyVISA でオシロスコー...

    環境準備

    実行環境は、「Python(3.6)」「Spyder(3.2.6)」となっています。

    また、コード実行には、以下のモジュールが波形データ生成に必要です。

    1. Pandas モジュール
    2. math モジュール
    3. random モジュール

    解析する波形データ

    使用する波形データは、Python で簡単に生成できますので、下記のコードを実行してみてください。

    1import pandas as pd
    2import random
    3import math 
    4
    5X_SAMPLE = 3000
    6NOISE_SEED_OFF = 123
    7#sin_wave
    8SIN_INT = 1000
    9NOISE_AMP_1=0.1
    10AMP_1= 5.0
    11
    12#rise_wave
    13RISE_TIME=300
    14AMP_2 =5
    15NOISE_AMP_2=0.1
    16RC=300
    17
    18xx = [ i for i in range(3000)]
    19analog_wave_1=[]
    20noise_1=[]
    21for i in range(len(xx)):
    22    random.seed(NOISE_SEED_OFF+i)
    23    noise_1.append(NOISE_AMP_1*random.randint(-100,100)/100)
    24    analog_wave_1.append(AMP_1* math.sin(2/SIN_INT*xx[i]*math.pi)+noise_1[i])
    25
    26analog_wave_2=[]
    27noise_2=[]
    28for i in range(len(xx)):
    29    random.seed(NOISE_SEED_OFF+i)
    30    noise_2.append(NOISE_AMP_2*random.randint(-100,100)/100)
    31    
    32    if i<RISE_TIME:
    33        analog_wave_2.append(noise_2[i])
    34    else:
    35        rise_dt = AMP_2*(1 - math.exp(-1/RC*xx[i-RISE_TIME]))
    36        analog_wave_2.append(rise_dt+noise_2[i])
    37
    38wave_data= pd.DataFrame({"wave_1":analog_wave_1,"wave_2":analog_wave_2})
    39wave_data.to_csv("analog_wave_test.csv")

    実際のアナログデータを想定しているので、上記にはランダムノイズも含まれます。

    今回生成した波形データのサンプル数は3,000個で、仕様は以下のとおりです。

    1. 振幅 5V の正弦波(チャンネル1)
    2. RC 回路の過渡応答を想定した 5V の立ち上がり波形(チャンネル2)

    チャンネル1

    理解を深めるため、グラフで見ていきましょう。

    振幅 5V の正弦波(チャンネル1)は、このようになっています。

    振幅 5V の正弦波(チャンネル1)

    チャンネル2

    一方、RC 回路の過渡応答を想定した5Vの立ち上がり波形(チャンネル2)は、こんな感じです。

    RC 回路の過渡応答を想定した5Vの立ち上がり波形(チャンネル2)

    求める特性値

    これで求める特性値は、以下の2つです。

    1. 振幅 5V の正弦波における最大・最小値
    2. RC 回路における立ち上がり時間

    時系列逐次処理でデータ解析!

    それでは、いよいよ実際にデータを解析してみましょう!

    ただし、csv ファイルをそのまま読み込むとメモリを大幅に圧迫してしまいます。

    そこで、使うのが「時系列逐次処理」です。

    時系列逐次処理とは?

    時系列逐次処理は、一行ごとに csv データを読み捨てるため、メモリをほとんど使用せずに解析ができます。

    オシロスコープの波形が、横軸を時間とする時系列データだからこそ、採用できる方法です。

    あるトリガタイミングから過去に遡ってデータ解析を行うことは難しいですが、大抵の特性値なら簡単に取得することができます。

    1wave_path = "./analog_wave_test.csv"
    2wave_file = open(wave_path,'r')
    3skip_row_count = 1
    4
    5#ヘッダ行等のスキップ
    6while skip_row_count !=0:
    7    row_data = wave_file.readline()
    8    skip_row_count-=1
    9
    10#データの読み込み開始
    11##最初の1行目はループ前に読み込む
    12row_data = wave_file.readline()
    13split_data = row_data.split(",")
    14
    15dt_cnt=0
    16while row_data:
    17    index_dt = float(split_data[0])
    18    ch1_dt = float(split_data[1])
    19    ch2_dt = float(split_data[2])
    20    ###########この間に解析処理を入れる##########
    21
    22    #######################################
    23    ##次の行を読み込む。ここで最終行なら空白が返ってくるためループ終了
    24    dt_cnt+=1
    25    row_data = wave_file.readline()
    26    split_data = row_data.split(",")
    27wave_file.close()

    読み込んだ csv データの1列目がインデックスで、2列目がチャンネル1、3列目がチャンネル2のデータです。

    csv ファイル中のデータ開始行が先頭からでないケースに備えて、開始行までスキップする機能も含まれています。

    今回のサンプル波形データは1行目がヘッダになっているので、1行分スキップしていますね。

    以降の処理は、この繰り返し文の中に記述していく形になります。

    ①正弦波データの最大・最小

    まず、正弦波データの最大・最小値から確認していきましょう。

    全データを一括で読み込んだ場合なら「Max」「Min」のメソッドを使えば完了ですが、逐次処理なので少々ややこしい手順となります。

    以下のコードで、最大値と最小値を大きいものから順に5個測定できます。

    ※別途、各種リストの初期化処理が必要なので、最後のサンプルコードを参照ください。

    1    #最大値とそのインデックスを大きい順に5つ格納する
    2    for i in range(len(ch1_max)):
    3        if ch1_dt > ch1_max[i][1]:
    4            ch1_max.insert(i,[index_dt,ch1_dt])
    5            if len(ch1_max) >MAX_REC:
    6                del ch1_max[-1]
    7            break
    8
    9    #最小値とそのインデックスを大きい順に5つ格納する
    10    for i in range(len(ch1_min)):
    11        if ch1_dt < ch1_min[i][1]:
    12            ch1_min.insert(i,[index_dt,ch1_dt])
    13            if len(ch1_min) >MAX_REC:
    14                del ch1_min[-1]
    15            break

    最大値

    行を読み込んでいく過程で最大値を更新した場合に、検出した値とインデックス番号を、ch1_max というリストへ保存していきます。

    リスト用の「insert」メソッドを活用すれば、更新データを自由に挿入できるため、自動的に順位を更新可能です。

    溢れた6個目のデータは、「del」メソッドで削除されていきます。

    最小値

    最小値も、基本的な動作は最大値と同じです。

    不等号が逆な点と、使用するリスト名が異なるくらいしか違いはありません。

    ②RC 回路の立ち上がり時間

    次に、RC 回路立ち上がりデータにおける、電圧20%から80%までの時間を測定してみましょう。

    コードは以下になります。

    1    #CH2RC回路立ち上がり
    2    #バッファを使って閾値の超過タイミングを検出する
    3    ##サンプル間隔を開けてノイズの影響を軽減する
    4    if index_dt % DET_SMP_INT == 0 and rise_st_f:
    5        #リングバッファへの保存
    6        for i in range(len(ch2_rise_st_buf)-1):
    7            ch2_rise_st_buf[len(ch2_rise_st_buf)-1-i] = ch2_rise_st_buf[len(ch2_rise_st_buf)-1-i-1]
    8        ch2_rise_st_buf[0]=ch2_dt
    9        #5回連続で値が閾値を超えた場合に閾値超過と判定する
    10        for i in range(len(ch2_rise_st_buf)):
    11            if ch2_rise_st_buf[i] > CH2_20:
    12                if i >4 :
    13                    ch2_rise_st.append([index_dt,ch2_dt])
    14                    ch1_at_ch2_rise.append([index_dt,ch1_dt])
    15                    rise_st_f=False
    16                    break
    17            else:
    18                break

    タイミング検出のポイント

    ノイズ重畳のせいで、指定された電圧値の到達タイミング検出が難しくなっています。

    そのため、要素数10個のリングバッファを使い、5回連続で指定電圧を超えたタイミングを記録します。

    この時、以下のポイントを意識してください。

    1. 検出処理は1サンプルごとではなく、任意に行う(ノイズに強い検出ができる)
    2. rise_st_f フラグを使い、検出が終了したら以後は行わない(フラグ管理しないと延々と検出・更新され続ける)

    電圧80%の検出タイミングも、変数や閾値が違うだけで基本は同じです。

    ③両データの連携:立ち上がりの指定タイミングでの正弦波の値

    最後に、あるチャンネルの指定タイミングにおけるもう片方のチャンネルの測定値を取得する方法をご紹介します。

    チャンネル2で電圧20%を検出したときに、チャンネル1の電圧値を取得してみましょう。

    ②のコード内にあるch1_at_ch2_rise.append([index_dt,ch1_dt]) がその処理にあたります。

    処理中は全チャンネル時間的な同期がとれているので、片方のチャンネル処理中に割り込む形で処理を追加すればいいのです。

    サンプルコード:波形データの最大・最小と立ち上がり時間の解析

    本記事で紹介したテクニックをまとめて、1つのコードにしてみました。

    コード自体は今回使用するサンプルの波形データに合わせたものですが、ファイル名やスキップする行を変えれば、どんな波形データでも対応できますよ。

    1wave_path = "./analog_wave_test.csv"
    2wave_file = open(wave_path,'r')
    3skip_row_count = 1
    4
    5#設定値関連
    6MAX_REC = 5 #最大・最小を記録する際に何個まで記録するか
    7CH2_20 = 5*0.2 #チャンネル2で検出する電圧の値(既定電圧の20%8CH2_80 = 5*0.8 #チャンネル2で検出する電圧の値(既定電圧の80%9DET_SMP_INT = 2 #電圧閾値を検出する際にデータ比較をするサンプル間隔
    10
    11#ヘッダ行等のスキップ
    12while skip_row_count !=0:
    13    row_data = wave_file.readline()
    14    skip_row_count-=1
    15
    16#変数の定義
    17ch1_max=[]
    18ch2_max=[]
    19ch1_min=[]
    20ch2_min=[]    
    21ch2_rise_st=[]
    22ch2_rise_ed=[]
    23ch2_rise_st_buf =[ 0 for i in range(10)]
    24ch2_rise_ed_buf =[ 0 for i in range(10)]
    25ch1_at_ch2_rise=[]
    26#立ち上がり検出を1回だけ実施するためのフラグ
    27rise_st_f = True
    28rise_ed_f = True
    29#データの読み込み開始
    30##最初の1行目はループ前に読み込む
    31row_data = wave_file.readline()
    32split_data = row_data.split(",")
    33
    34#各測定値の初期値を設定
    35ch1_max.insert(0,[float(split_data[0]),float(split_data[1])])
    36ch2_max.insert(0,[float(split_data[0]),float(split_data[2])])
    37ch1_min.insert(0,[float(split_data[0]),float(split_data[1])])
    38ch2_min.insert(0,[float(split_data[0]),float(split_data[2])])
    39
    40#2行目以降の読み出し処理を開始
    41dt_cnt=0
    42while row_data: #読み出した値がデータ終了後の空白なら終了
    43    index_dt = float(split_data[0])
    44    ch1_dt = float(split_data[1])
    45    ch2_dt = float(split_data[2])
    46    ###########この間に解析処理を入れる##########
    47    
    48    #CH1:正弦波の処理
    49    #最大値とそのインデックスを大きい順に5つ格納する
    50    for i in range(len(ch1_max)):
    51        if ch1_dt > ch1_max[i][1]:
    52            ch1_max.insert(i,[index_dt,ch1_dt])
    53            if len(ch1_max) >MAX_REC:
    54                del ch1_max[-1]
    55            break
    56    
    57    #最小値とそのインデックスを大きい順に5つ格納する
    58    for i in range(len(ch1_min)):
    59        if ch1_dt < ch1_min[i][1]:
    60            ch1_min.insert(i,[index_dt,ch1_dt])
    61            if len(ch1_min) >MAX_REC:
    62                del ch1_min[-1]
    63            break
    64        
    65    #CH2RC回路立ち上がり
    66    #バッファを使って閾値の超過タイミングを検出する
    67    ##サンプル間隔を開けてノイズの影響を軽減する
    68    if index_dt % DET_SMP_INT == 0 and rise_st_f:
    69        #リングバッファへの保存
    70        for i in range(len(ch2_rise_st_buf)-1):
    71            ch2_rise_st_buf[len(ch2_rise_st_buf)-1-i] = ch2_rise_st_buf[len(ch2_rise_st_buf)-1-i-1]
    72        ch2_rise_st_buf[0]=ch2_dt
    73        #5回連続で値が閾値を超えた場合に閾値超過と判定する
    74        for i in range(len(ch2_rise_st_buf)):
    75            if ch2_rise_st_buf[i] > CH2_20:
    76                if i >4 :
    77                    ch2_rise_st.append([index_dt,ch2_dt])
    78                    ch1_at_ch2_rise.append([index_dt,ch1_dt])
    79                    rise_st_f=False
    80                    break
    81            else:
    82                break
    83            
    84    #バッファを使って閾値の超過タイミングを検出する
    85    ##サンプル間隔を開けてノイズの影響を軽減する
    86    if index_dt % DET_SMP_INT == 0 and rise_ed_f:
    87        #リングバッファへの保存
    88        for i in range(len(ch2_rise_ed_buf)-1):
    89            ch2_rise_ed_buf[len(ch2_rise_ed_buf)-1-i] = ch2_rise_ed_buf[len(ch2_rise_ed_buf)-1-i-1]
    90        ch2_rise_ed_buf[0]=ch2_dt
    91        #5回連続で値が閾値を超えた場合に閾値超過と判定する
    92        for i in range(len(ch2_rise_ed_buf)):
    93            if ch2_rise_ed_buf[i] > CH2_80:
    94                if i >4 :
    95                    ch2_rise_ed.append([index_dt,ch2_dt])
    96                    rise_ed_f=False
    97                    break
    98            else:
    99                break          
    100    
    101    #######################################
    102    
    103    ##次の行を読み込む。ここで最終行なら空白が返ってくるためループ終了
    104    dt_cnt+=1
    105    row_data = wave_file.readline()
    106    split_data = row_data.split(",")
    107wave_file.close()
    108
    109#測定結果を表示
    110print("正弦波の最大値:"+str(ch1_max[0][1]))
    111print("正弦波の最小値:"+str(ch1_min[0][1]))
    112print("RC回路の立ち上がり時間:"+str(ch2_rise_ed[0][0]-ch2_rise_st[0][0]))
    113print("RC回路の20%時の正弦波の電圧値:"+str(ch1_at_ch2_rise[0][1]))

    さいごに

    今回は、電圧の最大・最小や立ち上がり時間といった「アナログ特性の取得方法」を紹介しました。

    1つの波形データだけなら、オシロスコープのカーソル機能や測定機能だけでも事足りるでしょう。

    しかし、Python を使えば、大量のデータ処理ができる上、解析結果の集約も非常に簡単です。

    回路動作を自動化し、オシロスコープと連携してしまえば、無人測定も可能ですよ!

    そして、次回は「シリアル通信の解析」を行っていきたいと思います。

    お楽しみに!

    第4回はこちら!

    Pythonによるオシロスコープ波形データ解析の秘訣(デジタルデータ編)2021.09.22【第4回】Pythonによるオシロスコープ波形データ解析の秘訣【デジタルデータ編】Python でオシロスコープ波形データを解析しよう!前回に引き続き、オシロスコープから得た csv ファイルを Py...

    第1回はこちら!

    Pythonを使ってオシロスコープを遠隔操作したり、画面キャプチャをしてみよう2021.02.08Pythonを使ってオシロスコープを遠隔操作したり、画面キャプチャをしてみようpython でオシロスコープを操作しよう!オシロスコープなどの計測器は「NI-VISA」という通信規格に対応していま...

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

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

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    広告メディア事業部

    広告メディア事業部

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background