diff --git a/Calibration.py b/Calibration.py new file mode 100644 index 00000000..9477aa82 --- /dev/null +++ b/Calibration.py @@ -0,0 +1,168 @@ +# 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_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, self.cal_through_label) + calibration_control_layout.addRow(btn_cal_isolation, self.cal_isolation_label) + + calibration_control_layout.addRow(QtWidgets.QLabel("")) + + 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) + + 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") + + 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] = [] + 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 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 + + 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*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.isCalculated = True + return self.isCalculated + + 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 f9a8b645..078ceb4d 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 @@ -56,6 +57,8 @@ def __init__(self): self.referenceS11data : List[Datapoint] = [] self.referenceS21data : List[Datapoint] = [] + self.calibration = Calibration() + self.markers = [] self.serialPort = self.getport() @@ -248,6 +251,18 @@ def __init__(self): left_column.addWidget(tdr_control_box) + ################################################################################################################ + # Calibration + ################################################################################################################ + calibration_control_box = QtWidgets.QGroupBox("Calibration") + calibration_control_box.setMaximumWidth(400) + 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 ################################################################################################################ @@ -280,25 +295,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) ################################################################################################################ @@ -327,6 +323,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) @@ -349,6 +370,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) ################################################################################################################ @@ -356,7 +387,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) @@ -383,11 +414,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()) 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()