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()