Eye Diagram Viewer - 파이썬 소스

도구/기타 2018. 9. 16. 22:00



XML 디자인:

XML 소스:

eyediagram_viewer.ui


Python 소스:

import sys

from PyQt5.QtWidgets import *

from PyQt5 import uic

import csv

import matplotlib.pyplot as plt

import numpy as np

from scipy.interpolate import interp1d

from matplotlib.backends.qt_compat import QtCore, QtWidgets, is_pyqt5

if is_pyqt5():

    from matplotlib.backends.backend_qt5agg import (

            FigureCanvas, NavigationToolbar2QT as NavigationToolbar)

else:

    from matplotlib.backends.backend_qt4agg import (

            FigureCanvas, NavigationToolbar2QT as NavigationToolbar)



def bres_segment_count(x0, y0, x1, y1, grid, endpoint):

    """

    Bresenham's algorithm.

    See http://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm

    """


    nrows = grid.shape[0]

    ncols = grid.shape[1]


    if x1 > x0:

        dx = x1 - x0

    else:

        dx = x0 - x1

    if y1 > y0:

        dy = y1 - y0

    else:

        dy = y0 - y1


    sx = 0

    if x0 < x1:

        sx = 1

    else:

        sx = -1

    sy = 0

    if y0 < y1:

        sy = 1

    else:

        sy = -1


    err = dx - dy


    while True:

        # When endpoint is 0, this test occurs before we increment the

        # grid value, so we don't count the last point.

        if endpoint == 0 and x0 == x1 and y0 == y1:

            break


        if (0 <= x0 < nrows) and (0 <= y0 < ncols):

            grid[x0, y0] += 1


        if x0 == x1 and y0 == y1:

            break


        e2 = 2 * err

        if e2 > -dy:

            err -= dy

            x0 += sx

        if e2 < dx:

            err += dx

            y0 += sy


    return 0



def bres_curve_count(x, y, grid):

    for k in range(len(x)-1):

        x0 = x[k]

        y0 = y[k]

        x1 = x[k+1]

        y1 = y[k+1]

        bres_segment_count(x0, y0, x1, y1, grid, 0)


    if 0 <= x1 < grid.shape[0] and 0 <= y1 < grid.shape[1]:

        # Count the last point in the curve.

        grid[x1, y1] += 1



def grid_count(y, window_size, offset, bounds, size=None, fuzz=True):

    """

    파라미터

    ----------

    `y` - 1차원 신호 샘플 배열

    `window_size` - eye diagram에서 수평으로 보여질 샘플 수. 통상 심볼(비트) 샘플의 2배

    `offset` - eye diagram을 계산하기 전에 건너 뛸 초기 샘플 수. eye의 전반적 위상 조정

    `bounds` - 두 부동소수 튜 플이어야 함. 돌려질 배열의 y 범위(ymin, ymax) 설정.

    `size` - 두 정수 튜플이어야만 함. 배열의 크기(height, width) 설정. 기본값은(800,600)

    `fuzz` - True일 경우, y 값이 랜덤 퍼즈 요소로 강제 인터폴레이션 됨. 알리아싱 감소

    반환 값 - 정수 numpy 배열

    """

    if size is None:

        size = (800, 640)

    width, height = size

    dt = width / window_size

    counts = np.zeros((width, height), dtype=np.int32)


    start = offset

    while start + window_size < len(y):

        end = start + window_size

        yy = y[start:end+1]

        k = np.arange(len(yy))

        xx = dt*k

        if fuzz:

            f = interp1d(xx, yy, kind='cubic')

            jiggle = dt*(np.random.beta(a=3, b=3, size=len(xx)-2) - 0.5)

            xx[1:-1] += jiggle

            yd = f(xx)

        else:

            yd = yy

        iyd = (height * (yd - bounds[0])/(bounds[1] - bounds[0])).astype(np.int32)

        bres_curve_count(xx.astype(np.int32), iyd, counts)


        start = end

    return counts



def read_and_plot(appWin):

    # 소수점 출력 자리수 설정

    unitx = '.' + str(appWin.unitX) + 'f'                                      

    unity = '.' + str(appWin.unitY) + 'f'

    

    samples_per_symbol = 20

    

    npx = np.array(appWin.x)    # numpy 배열로 변환

    number_of_samples = int((npx[len(npx)-1]/appWin.ui)*samples_per_symbol)  # 총샘플 수

    ipx = np.linspace(0, npx[len(npx)-1], number_of_samples)  # 총 샘플수에 맞게 x 샘플 수 조정

    

    npy= np.array(appWin.y)

    ipy = np.interp(ipx, npx, npy)  # *** x 를 참조하여 y 샘플수를 조정

    

    # ------ high level과 low level을 histogram을 사용하여 찾기 ------------------

    n, bins, patches = plt.hist(ipy, bins=100)

    peak = 0                                 # 히스토그램 최대 분포

    high = 0                                 # high와 low 두 부분으로 나누어 진행

    for i in range(50,99):

        if n[i] > peak:

            high = i

    peak = 0

    low = 0

    for i in range(50,49):

        if n[i] > peak:

            low = i

    high = (bins[high]+bins[high+1])/2

    low = (bins[low]+bins[low+1])/2

    mid = (high+low)/2

    amp = high - low

    appWin.lineEditHigh.setText(format(high, unity))

    appWin.lineEditLow.setText(format(low, unity))

    appWin.lineEditMid.setText(format(mid, unity))

    

    # ------- 상승 및 하강 시간 찾기 ---------------------------------------------

    p10 = amp*0.1 + low

    p90 = amp*0.9 + low

    tran = False

    step = 0                 # 누적 상승 구간 스텝 수

    count = 0                # 상승 구간 개수

    for i in range(0, len(ipy)-1):

        if ipy[i] <= p10 and ipy[i+1] >= p10 and tran == False:      # 10%를 지날 때 step 카운트 시작

            tran = True

            start = i

        elif ipy[i] <= p90 and ipy[i+1] >= p90 and tran == True:     # 90%를 지날 때 step 카운트 종료

            tran = False

            step = step + i - start

            count = count + 1

    appWin.lineEditRise.setText(format(appWin.ui*(step/count)/samples_per_symbol, unitx))

    tran = False                                                     # 하강 구간에 대해서 동일하게

    step = 0

    count = 0

    for i in range(0, len(ipy)-1):

        if ipy[i] >= p90 and ipy[i+1] <= p90 and tran == False:

            tran = True

            start = i

        elif ipy[i] >= p10 and ipy[i+1] <= p10 and tran == True:

            tran = False

            step = step + i - start

            count = count + 1

    appWin.lineEditFall.setText(format(appWin.ui*(step/count)/samples_per_symbol, unitx))

    

    # eye diagram을 shift 시키기 위한 offset값. 반대방향으로 적용

    offset = appWin.offset

    while offset < 0:                 # grid count에서 offset은 +만 되므로 -로 변환

        offset = offset + appWin.ui

    offset = int(samples_per_symbol*offset/appWin.ui)   # 시간 단위 옵셋을 샘플 수 단위로 변환

    if offset > samples_per_symbol:                     # 옵셋이 UI를 넘으면 보정

        offset = offset % samples_per_symbol

    skip = samples_per_symbol - offset

    if appWin.ampMinMax[0] == appWin.ampMinMax[1]:

        ymax = ipy.max()

        ymin = ipy.min()

        yamp = ymax - ymin

        min_y = ymin - 0.05*yamp

        max_y = ymax + 0.05*yamp

    else:

        min_y = appWin.ampMinMax[0]

        max_y = appWin.ampMinMax[1]

    bounds = (min_y, max_y)

    span = max_y - min_y

    """

    plot an eye diagram using matplotlib by creating an image 

    and calling the 'imshow' function.

    """

    counts = grid_count(ipy, 2*samples_per_symbol, skip, bounds)

    counts = counts.astype(np.float32)

    counts[counts==0] = np.nan

    

    appWin.fig.clf()

    appWin.verticalLayout.removeWidget(appWin.canvas)

    appWin.canvas = FigureCanvas(appWin.fig)

    appWin.verticalLayout.addWidget(appWin.canvas)

    

    plt.imshow(counts.T[::-1, :],aspect='auto',

                extent=[0, 2*appWin.ui, min_y, max_y],

                cmap=plt.cm.RdYlGn)

    plt.grid(color='#929591', linestyle=':')

    #if colorbar:

    #   cb = plt.colorbar()

    #   cb.set_ticks([])                  # tick 제거

    

    plt.xlabel(appWin.matrix[0][0])

    plt.ylabel(appWin.matrix[0][appWin.index])

    #file_name = matrix[0][j] + '.png'

    #plt.savefig(file_name)

    #plt.show()

    

    # ------------ eye width 찾기 ----------------------------------------------

    if appWin.vh == None:

        vh = mid

        appWin.vh =mid

        appWin.lineEditVH.setText(format(mid, unity))

    else:

        vh = appWin.vh

    if appWin.vl == None:

        vl = mid

        appWin.vl =mid

        appWin.lineEditVL.setText(format(mid, unity))

    else:

        vl = appWin.vl

    vhi = int(639 * (vh - min_y)/span)

    row_data_h = np.nan_to_num(counts[0:800, vhi])

    vli = int(639 * (vl - min_y)/span)

    row_data_l = np.nan_to_num(counts[0:800, vli])

    zeros_h = 0

    for i in range(400, 800):

        if row_data_h[i] == 0:

            zeros_h = zeros_h + 1

        else:

            break

    zeros_l = 0

    for i in range(400, 800):

        if row_data_l[i] == 0:

            zeros_l = zeros_l + 1

        else:

            break

    if zeros_h > zeros_l:

        zeros_right = zeros_l

    else:

        zeros_right = zeros_h

    zeros_h = 0

    for i in range(400, 0, -1):

        if row_data_h[i] == 0:

            zeros_h = zeros_h + 1

        else:

            break

    zeros_l = 0

    for i in range(400, 0, -1):

        if row_data_l[i] == 0:

            zeros_l = zeros_l + 1

        else:

            break

    if zeros_h > zeros_l:

        zeros_left = zeros_l

    else:

        zeros_left = zeros_h    

    zeros = zeros_right + zeros_left

    

    eye_width = zeros * (appWin.ui * 2) / 800

    appWin.lineEditWidth.setText(format(eye_width, unitx))

    appWin.lineEditWidth2.setText(format(100*eye_width/appWin.ui, '.1f')+'%')


    # -------------- eye height 찾기 -------------------------------------------

    col_data = np.nan_to_num(counts[400+int((zeros_right-zeros_left)/2)])

    sp = int(639 * (low-min_y)/span)

    ep = int(639 * (high-min_y)/span)

    if ep > 639:

        ep = 639

    cp = int((sp+ep)/2)

    zeros = 0

    for i in range(cp, ep):

        if col_data[i] == 0:

            zeros = zeros + 1

        else:

            break

    for i in range(cp, sp, -1):

        if col_data[i] == 0:

            zeros = zeros + 1

        else:

            break

    eye_height =zeros * span / 640

    appWin.lineEditHeight.setText(format(eye_height, unity))

    appWin.lineEditHeight2.setText(format(100*eye_height/amp, '.1f') + '%')

    

    # -------------- VH, VL 활성화 결정

    if eye_height > 0:

        appWin.vh = vh

        appWin.vl = vl

        appWin.lineEditVH.setText(format(vh, unity))

        appWin.lineEditVL.setText(format(vl, unity))

        appWin.lineEditVH.setEnabled(True)

        appWin.lineEditVL.setEnabled(True)

    else:

        appWin.vh = None

        appWin.vl = None

        appWin.lineEditVH.setText('')

        appWin.lineEditVL.setText('')

        appWin.lineEditVH.setEnabled(False)

        appWin.lineEditVL.setEnabled(False)



def is_number(num):

    try:

        float(num)

        return True                         # num을 float으로 변환할 수 있는 경우

    except ValueError:

        return False                        # num을 float으로 변환할 수 없는 경우

    


form_class = uic.loadUiType("eyediagram_viewer.ui")[0]


class ApplicationWindow(QMainWindow, form_class):

    def __init__(self):

        super().__init__()

        self.setupUi(self)

        

        self.fig = plt.figure(figsize=(10, 7.5), dpi=80)

        self.canvas = FigureCanvas(self.fig)

        self.verticalLayout.addWidget(self.canvas)

        #self.addToolBar(NavigationToolbar(self.canvas, self))

        

        self.offset = 0.0

        self.ampMinMax = (0,0)

        self.ui = 1.0

        self.matrix = []

        self.x = []

        self.y = []

        self.index = 1

        self.vh = None

        self.vl = None

        self.unitX = 2

        self.unitY = 2

        

        self.actionOpen.triggered.connect(self.menu_open_checked)

        self.actionExport_to_PNG.triggered.connect(self.menu_export_checked)

        self.comboBox.currentIndexChanged.connect(self.combo_choose)

        self.radioButtonAuto.clicked.connect(self.radio_auto)

        self.radioButtonManual.clicked.connect(self.radio_manual)

        self.lineEditMax.returnPressed.connect(self.edit_max)

        self.lineEditMin.returnPressed.connect(self.edit_min)

        self.lineEditUI.returnPressed.connect(self.edit_ui)

        self.lineEditOffset.returnPressed.connect(self.edit_offset)

        self.lineEditVH.returnPressed.connect(self.edit_vh)

        self.lineEditVL.returnPressed.connect(self.edit_vl)

        self.lineEditUnitX.returnPressed.connect(self.edit_unit_x)

        self.lineEditUnitY.returnPressed.connect(self.edit_unit_y)

        

    def menu_open_checked(self):

        fname = QFileDialog.getOpenFileName(self)

        if fname[0] == '':

            return


        self.matrix = []

        with open(fname[0], 'r') as file:

            reader = csv.reader(file)

            for line in reader:

                self.matrix.append(line)

                

        self.x = []

        try:

            for i in range(1, len(self.matrix), 1):

                newx = float(self.matrix[i][0])

                self.x.append(newx)

                

            self.comboBox.clear()

            for j in range(1, len(self.matrix[0]), 1):

                self.comboBox.addItem(self.matrix[0][j])

                

            self.comboBox.setEnabled(True)          # 파일을 읽었으므로 기능 활성화

            self.radioButtonAuto.setEnabled(True)

            self.radioButtonManual.setEnabled(True)

            self.lineEditUI.setEnabled(True)

            self.lineEditOffset.setEnabled(True)

            self.actionExport_to_PNG.setEnabled(True)

        except ValueError:

            self.statusbar.showMessage('The file is not a eye diagram CSV.')

            

    def menu_export_checked(self):

        file_name = self.matrix[0][self.index] + '.png'

        plt.savefig(file_name)


    def radio_auto(self):

        self.lineEditMin.setEnabled(False)

        self.lineEditMax.setEnabled(False)

        self.ampMinMax = (0, 0)

        read_and_plot(self)

        

    def radio_manual(self):

        self.lineEditMin.setEnabled(True)

        self.lineEditMax.setEnabled(True)

        max = self.lineEditMax.text()

        min = self.lineEditMin.text()        

        self.ampMinMax = (float(min), float(max))

        read_and_plot(self)


    def edit_max(self):

        if is_number(self.lineEditMax.text()):

            self.ampMinMax = (self.ampMinMax[0], float(self.lineEditMax.text()))

            read_and_plot(self)

        else:

            self.lineEditMax.setText(str(self.ampMinMax[1]))

    

    def edit_min(self):

        if is_number(self.lineEditMin.text()):

            self.ampMinMax = (float(self.lineEditMin.text()), self.ampMinMax[1])

            read_and_plot(self)

        else:

            self.lineEditMin.setText(str(self.ampMinMax[0]))

   

    def edit_ui(self):

        if is_number(self.lineEditUI.text()):

            self.ui = float(self.lineEditUI.text())

            read_and_plot(self)

        else:

            self.lineEditUI.setText(str(self.ui))

    

    def edit_offset(self):

        if is_number(self.lineEditOffset.text()):

            self.offset = float(self.lineEditOffset.text())

            read_and_plot(self)

        else:

            self.lineEditOffset.setText(str(self.offset))

    

    def edit_vh(self):

        if is_number(self.lineEditVH.text()):

            self.vh = float(self.lineEditVH.text())

            read_and_plot(self)

        else:

            self.vh = None

            self.lineEditVH.setText('')

            

    def edit_vl(self):

        if is_number(self.lineEditVL.text()):

            self.vl = float(self.lineEditVL.text())

            read_and_plot(self)

        else:

            self.vl = None

            self.lineEditVL.setText('')

            

    def edit_unit_x(self):

        if is_number(self.lineEditUnitX.text()) and int(self.lineEditUnitX.text()) >= 0:

            self.unitX = int(self.lineEditUnitX.text())

            read_and_plot(self)

        else:

            self.lineEditUnitX.setText(str(self.unitX))

            

    def edit_unit_y(self):

        if is_number(self.lineEditUnitY.text()) and int(self.lineEditUnitY.text()) >= 0:

            self.unitY = int(self.lineEditUnitY.text())

            read_and_plot(self)

        else:

            self.lineEditUnitX.setText(str(self.unitY))

        

    def combo_choose(self):

        self.index = self.comboBox.currentIndex() + 1

        if self.index > 0:

            self.y = []

            for i in range(1, len(self.matrix), 1):

                newy = float(self.matrix[i][self.index])

                self.y.append(newy)

            read_and_plot(self)

            


if __name__ == "__main__":

    app = QApplication(sys.argv)

    myWindow = ApplicationWindow()

    myWindow.show()

    app.exec_()


샘플 transient simulation 데이터:

4000mbps_prbs.csv






'도구 > 기타' 카테고리의 다른 글

사양 BOM 생성기 (매트릭스 버전)  (0) 2020.04.05
사양 BOM 생성기  (0) 2020.03.25
Wave 2 Table  (0) 2018.09.03
Spreadsheet Schematic Editor for Allegro  (0) 2016.12.05
패키지/커넥터 핀맵 작성기  (0) 2016.11.21
: