From 249e614c8cd1d6d341bf5e96aa35d1127c8e8e7e Mon Sep 17 00:00:00 2001 From: "Rune B. Broberg" Date: Tue, 3 Sep 2019 15:36:42 +0200 Subject: [PATCH 1/3] Setting up a framework for in-app calibration --- Calibration.py | 146 ++++++++++++++++++++++++++++++++++++++++++++++++ NanoVNASaver.py | 16 +++++- SweepWorker.py | 7 +++ 3 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 Calibration.py diff --git a/Calibration.py b/Calibration.py new file mode 100644 index 00000000..0675ae7c --- /dev/null +++ b/Calibration.py @@ -0,0 +1,146 @@ +# NanoVNASaver - a python program to view and export Touchstone data from a NanoVNA +# Copyright (C) 2019. Rune B. Broberg +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +import collections +from PyQt5 import QtWidgets +from typing import List +import numpy as np + +Datapoint = collections.namedtuple('Datapoint', 'freq re im') + + +class CalibrationWindow(QtWidgets.QWidget): + def __init__(self, app): + super().__init__() + + from NanoVNASaver import NanoVNASaver + + self.app: NanoVNASaver = app + + self.setMinimumSize(300, 300) + self.setWindowTitle("Calibration") + layout = QtWidgets.QVBoxLayout() + self.setLayout(layout) + + calibration_status_group = QtWidgets.QGroupBox("Active calibration") + calibration_status_layout = QtWidgets.QFormLayout() + self.calibration_status_label = QtWidgets.QLabel("Device calibration") + calibration_status_layout.addRow("Calibration active: ", self.calibration_status_label) + calibration_status_group.setLayout(calibration_status_layout) + layout.addWidget(calibration_status_group) + + calibration_control_group = QtWidgets.QGroupBox("Calibrate") + calibration_control_layout = QtWidgets.QFormLayout(calibration_control_group) + btn_cal_short = QtWidgets.QPushButton("Short") + btn_cal_short.clicked.connect(self.saveShort) + self.cal_short_label = QtWidgets.QLabel("Uncalibrated") + + btn_cal_open = QtWidgets.QPushButton("Open") + btn_cal_open.clicked.connect(self.saveOpen) + self.cal_open_label = QtWidgets.QLabel("Uncalibrated") + + btn_cal_load = QtWidgets.QPushButton("Load") + btn_cal_load.clicked.connect(self.saveLoad) + self.cal_load_label = QtWidgets.QLabel("Uncalibrated") + + btn_cal_through = QtWidgets.QPushButton("Through") + btn_cal_isolation = QtWidgets.QPushButton("Isolation") + + calibration_control_layout.addRow(btn_cal_short, self.cal_short_label) + calibration_control_layout.addRow(btn_cal_open, self.cal_open_label) + calibration_control_layout.addRow(btn_cal_load, self.cal_load_label) + calibration_control_layout.addRow(btn_cal_through) + calibration_control_layout.addRow(btn_cal_isolation) + + btn_run = QtWidgets.QPushButton("Run") + calibration_control_layout.addRow(btn_run) + btn_run.clicked.connect(self.app.calibration.calculateCorrections) + + layout.addWidget(calibration_control_group) + + def saveShort(self): + self.app.calibration.s11short = self.app.data + self.cal_short_label.setText("Calibrated") + + def saveOpen(self): + self.app.calibration.s11open = self.app.data + self.cal_open_label.setText("Calibrated") + + def saveLoad(self): + self.app.calibration.s11load = self.app.data + self.cal_load_label.setText("Calibrated") + + +class Calibration: + s11short: List[Datapoint] = [] + s11open: List[Datapoint] = [] + s11load: List[Datapoint] = [] + s21through: List[Datapoint] = [] + s21isolation: List[Datapoint] = [] + + frequencies = [] + + e00 = [] + e11 = [] + deltaE = [] + + shortIdeal = np.complex(-1, 0) + openIdeal = np.complex(1, 0) + loadIdeal = np.complex(0, 0) + + isCalculated = False + + def isValid2Port(self): + return len(self.s21through) > 0 and len(self.s21isolation) > 0 and self.isValid1Port() + + def isValid1Port(self): + return len(self.s11short) > 0 and len(self.s11open) > 0 and len(self.s11load) > 0 + + def calculateCorrections(self): + if not self.isValid1Port(): + return + self.frequencies = [] * len(self.s11short) + self.e00 = [] * len(self.s11short) + self.e11 = [] * len(self.s11short) + self.deltaE = [] * len(self.s11short) + for i in range(len(self.s11short)): + self.frequencies[i] = self.s11short[i].freq + + g1 = self.shortIdeal + g2 = self.openIdeal + g3 = self.loadIdeal + + gm1 = np.complex(self.s11short[i].re, self.s11short[i].im) + gm2 = np.complex(self.s11open[i].re, self.s11open[i].im) + gm3 = np.complex(self.s11load[i].re, self.s11load[i].im) + + denominator = g1*(g2-g3)*gm1 + g2*g3*gm3 - g2*g3*gm3 - (g2*gm2-g3*gm3)*g1 + self.e00[i] = ((g2*gm3 - g3*gm3)*g1*gm2 - (g2*g3*gm2 - g2*g3*gm3 - (g3*gm2 - g2*gm3)*g1)*gm1) / denominator + self.e11[i] = ((g2-g3)*gm1-g1*(gm2-gm3)+g3*gm2-g2*gm3) / denominator + self.deltaE[i] = ((g1*(gm2-gm3)-g2*gm2+g3*gm3)*gm1+(g2*gm3-g3*gm3)*gm2) / denominator + + self.isCalculated = True + + def correct11(self, re, im, freq): + s11m = np.complex(re, im) + distance = 10**10 + index = 0 + for i in range(len(self.s11short)): + if abs(self.s11short[i].freq - freq) < distance: + index = i + distance = abs(self.s11short[i].freq - freq) + + s11 = (s11m - self.e00[index]) / (s11m * self.e11[index]) - self.deltaE[index] + return s11.real, s11.imag diff --git a/NanoVNASaver.py b/NanoVNASaver.py index ca61d4e4..23279b2d 100644 --- a/NanoVNASaver.py +++ b/NanoVNASaver.py @@ -25,6 +25,7 @@ from serial.tools import list_ports import Chart +from Calibration import CalibrationWindow, Calibration from Marker import Marker from SmithChart import SmithChart from SweepWorker import SweepWorker @@ -55,6 +56,8 @@ def __init__(self): self.referenceS11data : List[Datapoint] = [] self.referenceS21data : List[Datapoint] = [] + self.calibration = Calibration() + self.markers = [] self.serialPort = self.getport() @@ -226,6 +229,17 @@ def __init__(self): left_column.addWidget(tdr_control_box) + ################################################################################################################ + # Calibration + ################################################################################################################ + calibration_control_box = QtWidgets.QGroupBox("Calibration") + calibration_control_layout = QtWidgets.QFormLayout(calibration_control_box) + b = QtWidgets.QPushButton("Calibration") + self.calibrationWindow = CalibrationWindow(self) + b.clicked.connect(self.calibrationWindow.show) + calibration_control_layout.addRow(b) + left_column.addWidget(calibration_control_box) + ################################################################################################################ # Spacer ################################################################################################################ @@ -740,4 +754,4 @@ def loadSweepFile(self): t = Touchstone(filename) t.load() self.saveData(t.s11data, t.s21data) - self.dataUpdated() \ No newline at end of file + self.dataUpdated() diff --git a/SweepWorker.py b/SweepWorker.py index de5c071e..0a0356c5 100644 --- a/SweepWorker.py +++ b/SweepWorker.py @@ -15,6 +15,7 @@ # along with this program. If not, see . import collections from time import sleep +from typing import List from PyQt5 import QtCore from PyQt5.QtCore import pyqtSlot, pyqtSignal @@ -37,6 +38,8 @@ def __init__(self, app: NanoVNASaver): self.noSweeps = 1 self.setAutoDelete(False) self.percentage = 0 + self.data11: List[Datapoint] = [] + self.data12: List[Datapoint] = [] @pyqtSlot() def run(self): @@ -106,8 +109,12 @@ def saveData(self, frequencies, values, values12): re12 = float(reStr) im12 = float(imStr) freq = int(frequencies[i]) + if self.app.calibration.isCalculated: # We only have 1-port calibration for now + re, im = self.app.calibration.correct11(re, im, freq) data += [Datapoint(freq, re, im)] data12 += [Datapoint(freq, re12, im12)] + self.data11 = data + self.data12 = data12 self.app.saveData(data, data12) self.signals.updated.emit() From 9dfa6a9b3ef638a071613d7c9369bf2d9c7a572f Mon Sep 17 00:00:00 2001 From: "Rune B. Broberg" Date: Tue, 3 Sep 2019 16:59:36 +0200 Subject: [PATCH 2/3] Putting all file handling into a separate window. --- NanoVNASaver.py | 68 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 23 deletions(-) diff --git a/NanoVNASaver.py b/NanoVNASaver.py index 23279b2d..28339ac2 100644 --- a/NanoVNASaver.py +++ b/NanoVNASaver.py @@ -170,6 +170,7 @@ def __init__(self): s11_control_box.setTitle("S11") s11_control_layout = QtWidgets.QFormLayout() s11_control_box.setLayout(s11_control_layout) + s11_control_box.setMaximumWidth(400) self.s11_min_swr_label = QtWidgets.QLabel() s11_control_layout.addRow("Min VSWR:", self.s11_min_swr_label) @@ -182,6 +183,7 @@ def __init__(self): s21_control_box.setTitle("S21") s21_control_layout = QtWidgets.QFormLayout() s21_control_box.setLayout(s21_control_layout) + s21_control_box.setMaximumWidth(400) self.s21_min_gain_label = QtWidgets.QLabel() s21_control_layout.addRow("Min gain:", self.s21_min_gain_label) @@ -195,6 +197,7 @@ def __init__(self): tdr_control_box.setTitle("TDR") tdr_control_layout = QtWidgets.QFormLayout() tdr_control_box.setLayout(tdr_control_layout) + tdr_control_box.setMaximumWidth(400) self.tdr_velocity_dropdown = QtWidgets.QComboBox() self.tdr_velocity_dropdown.addItem("Jelly filled (0.64)", 0.64) @@ -233,8 +236,9 @@ def __init__(self): # Calibration ################################################################################################################ calibration_control_box = QtWidgets.QGroupBox("Calibration") + calibration_control_box.setMaximumWidth(400) calibration_control_layout = QtWidgets.QFormLayout(calibration_control_box) - b = QtWidgets.QPushButton("Calibration") + b = QtWidgets.QPushButton("Calibration ...") self.calibrationWindow = CalibrationWindow(self) b.clicked.connect(self.calibrationWindow.show) calibration_control_layout.addRow(b) @@ -272,25 +276,6 @@ def __init__(self): reference_control_layout.addRow(set_reference_layout) reference_control_layout.addRow(self.btnResetReference) - self.referenceFileNameInput = QtWidgets.QLineEdit("") - btnReferenceFilePicker = QtWidgets.QPushButton("...") - btnReferenceFilePicker.setMaximumWidth(25) - btnReferenceFilePicker.clicked.connect(self.pickReferenceFile) - referenceFileNameLayout = QtWidgets.QHBoxLayout() - referenceFileNameLayout.addWidget(self.referenceFileNameInput) - referenceFileNameLayout.addWidget(btnReferenceFilePicker) - - reference_control_layout.addRow(QtWidgets.QLabel("Filename"), referenceFileNameLayout) - - import_button_layout = QtWidgets.QHBoxLayout() - btnLoadReference = QtWidgets.QPushButton("Load reference") - btnLoadReference.clicked.connect(self.loadReferenceFile) - btnLoadSweep = QtWidgets.QPushButton("Load as sweep") - btnLoadSweep.clicked.connect(self.loadSweepFile) - import_button_layout.addWidget(btnLoadReference) - import_button_layout.addWidget(btnLoadSweep) - reference_control_layout.addRow(import_button_layout) - left_column.addWidget(reference_control_box) ################################################################################################################ @@ -319,6 +304,31 @@ def __init__(self): # File control ################################################################################################################ + self.fileWindow = QtWidgets.QWidget() + self.fileWindow.setWindowTitle("Files") + file_window_layout = QtWidgets.QVBoxLayout() + self.fileWindow.setLayout(file_window_layout) + + reference_file_control_box = QtWidgets.QGroupBox("Import file") + reference_file_control_layout = QtWidgets.QFormLayout(reference_file_control_box) + self.referenceFileNameInput = QtWidgets.QLineEdit("") + btnReferenceFilePicker = QtWidgets.QPushButton("...") + btnReferenceFilePicker.setMaximumWidth(25) + btnReferenceFilePicker.clicked.connect(self.pickReferenceFile) + referenceFileNameLayout = QtWidgets.QHBoxLayout() + referenceFileNameLayout.addWidget(self.referenceFileNameInput) + referenceFileNameLayout.addWidget(btnReferenceFilePicker) + + reference_file_control_layout.addRow(QtWidgets.QLabel("Filename"), referenceFileNameLayout) + file_window_layout.addWidget(reference_file_control_box) + + btnLoadReference = QtWidgets.QPushButton("Load reference") + btnLoadReference.clicked.connect(self.loadReferenceFile) + btnLoadSweep = QtWidgets.QPushButton("Load as sweep") + btnLoadSweep.clicked.connect(self.loadSweepFile) + reference_file_control_layout.addRow(btnLoadReference) + reference_file_control_layout.addRow(btnLoadSweep) + file_control_box = QtWidgets.QGroupBox() file_control_box.setTitle("Export file") file_control_box.setMaximumWidth(400) @@ -341,6 +351,16 @@ def __init__(self): self.btnExportFile.clicked.connect(self.exportFileS2P) file_control_layout.addRow(self.btnExportFile) + file_window_layout.addWidget(file_control_box) + + file_control_box = QtWidgets.QGroupBox() + file_control_box.setTitle("Files") + file_control_box.setMaximumWidth(400) + file_control_layout = QtWidgets.QFormLayout(file_control_box) + btnOpenFileWindow = QtWidgets.QPushButton("Files ...") + file_control_layout.addWidget(btnOpenFileWindow) + btnOpenFileWindow.clicked.connect(lambda: self.fileWindow.show()) + left_column.addWidget(file_control_box) ################################################################################################################ @@ -348,7 +368,7 @@ def __init__(self): ################################################################################################################ self.lister = QtWidgets.QPlainTextEdit() - self.lister.setFixedHeight(100) + self.lister.setFixedHeight(80) charts = QtWidgets.QGridLayout() charts.addWidget(self.s11SmithChart, 0, 0) charts.addWidget(self.s21SmithChart, 1, 0) @@ -375,11 +395,13 @@ def getport(self) -> str: def pickReferenceFile(self): filename, _ = QtWidgets.QFileDialog.getOpenFileName(directory=self.referenceFileNameInput.text(), filter="Touchstone Files (*.s1p *.s2p);;All files (*.*)") - self.referenceFileNameInput.setText(filename) + if filename != "": + self.referenceFileNameInput.setText(filename) def pickFile(self): filename, _ = QtWidgets.QFileDialog.getSaveFileName(directory=self.fileNameInput.text(), filter="Touchstone Files (*.s1p *.s2p);;All files (*.*)") - self.fileNameInput.setText(filename) + if filename != "": + self.fileNameInput.setText(filename) def exportFileS1P(self): print("Save file to " + self.fileNameInput.text()) From 8a54890278e5908cb7da81a1309015f42fcdb7a1 Mon Sep 17 00:00:00 2001 From: "Rune B. Broberg" Date: Tue, 3 Sep 2019 19:12:27 +0200 Subject: [PATCH 3/3] 1-port in-app calibration --- Calibration.py | 50 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/Calibration.py b/Calibration.py index 0675ae7c..9477aa82 100644 --- a/Calibration.py +++ b/Calibration.py @@ -56,17 +56,28 @@ def __init__(self, app): self.cal_load_label = QtWidgets.QLabel("Uncalibrated") btn_cal_through = QtWidgets.QPushButton("Through") + btn_cal_through.setDisabled(True) + self.cal_through_label = QtWidgets.QLabel("Uncalibrated") + btn_cal_isolation = QtWidgets.QPushButton("Isolation") + btn_cal_isolation.setDisabled(True) + self.cal_isolation_label = QtWidgets.QLabel("Uncalibrated") calibration_control_layout.addRow(btn_cal_short, self.cal_short_label) calibration_control_layout.addRow(btn_cal_open, self.cal_open_label) calibration_control_layout.addRow(btn_cal_load, self.cal_load_label) - calibration_control_layout.addRow(btn_cal_through) - calibration_control_layout.addRow(btn_cal_isolation) + calibration_control_layout.addRow(btn_cal_through, self.cal_through_label) + calibration_control_layout.addRow(btn_cal_isolation, self.cal_isolation_label) + + calibration_control_layout.addRow(QtWidgets.QLabel("")) - btn_run = QtWidgets.QPushButton("Run") - calibration_control_layout.addRow(btn_run) - btn_run.clicked.connect(self.app.calibration.calculateCorrections) + btn_apply = QtWidgets.QPushButton("Apply") + calibration_control_layout.addRow(btn_apply) + btn_apply.clicked.connect(self.calculate) + + btn_reset = QtWidgets.QPushButton("Reset") + calibration_control_layout.addRow(btn_reset) + btn_reset.clicked.connect(self.reset) layout.addWidget(calibration_control_group) @@ -82,6 +93,16 @@ def saveLoad(self): self.app.calibration.s11load = self.app.data self.cal_load_label.setText("Calibrated") + def reset(self): + self.app.calibration = Calibration() + self.cal_short_label.setText("Uncalibrated") + self.cal_open_label.setText("Uncalibrated") + self.cal_load_label.setText("Uncalibrated") + self.calibration_status_label.setText("Device calibration") + + def calculate(self): + if self.app.calibration.calculateCorrections(): + self.calibration_status_label.setText("Application calibration") class Calibration: s11short: List[Datapoint] = [] @@ -110,11 +131,11 @@ def isValid1Port(self): def calculateCorrections(self): if not self.isValid1Port(): - return - self.frequencies = [] * len(self.s11short) - self.e00 = [] * len(self.s11short) - self.e11 = [] * len(self.s11short) - self.deltaE = [] * len(self.s11short) + return False + self.frequencies = [int] * len(self.s11short) + self.e00 = [np.complex] * len(self.s11short) + self.e11 = [np.complex] * len(self.s11short) + self.deltaE = [np.complex] * len(self.s11short) for i in range(len(self.s11short)): self.frequencies[i] = self.s11short[i].freq @@ -126,12 +147,13 @@ def calculateCorrections(self): gm2 = np.complex(self.s11open[i].re, self.s11open[i].im) gm3 = np.complex(self.s11load[i].re, self.s11load[i].im) - denominator = g1*(g2-g3)*gm1 + g2*g3*gm3 - g2*g3*gm3 - (g2*gm2-g3*gm3)*g1 - self.e00[i] = ((g2*gm3 - g3*gm3)*g1*gm2 - (g2*g3*gm2 - g2*g3*gm3 - (g3*gm2 - g2*gm3)*g1)*gm1) / denominator + denominator = g1*(g2-g3)*gm1 + g2*g3*gm2 - g2*g3*gm3 - (g2*gm2-g3*gm3)*g1 + self.e00[i] = - ((g2*gm3 - g3*gm3)*g1*gm2 - (g2*g3*gm2 - g2*g3*gm3 - (g3*gm2 - g2*gm3)*g1)*gm1) / denominator self.e11[i] = ((g2-g3)*gm1-g1*(gm2-gm3)+g3*gm2-g2*gm3) / denominator - self.deltaE[i] = ((g1*(gm2-gm3)-g2*gm2+g3*gm3)*gm1+(g2*gm3-g3*gm3)*gm2) / denominator + self.deltaE[i] = - ((g1*(gm2-gm3)-g2*gm2+g3*gm3)*gm1+(g2*gm3-g3*gm3)*gm2) / denominator self.isCalculated = True + return self.isCalculated def correct11(self, re, im, freq): s11m = np.complex(re, im) @@ -142,5 +164,5 @@ def correct11(self, re, im, freq): index = i distance = abs(self.s11short[i].freq - freq) - s11 = (s11m - self.e00[index]) / (s11m * self.e11[index]) - self.deltaE[index] + s11 = (s11m - self.e00[index]) / ((s11m * self.e11[index]) - self.deltaE[index]) return s11.real, s11.imag