diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..487e667b
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,7 @@
+# Default for all text files
+* text=auto whitespace=trailing-space,tab-in-indent,tabwidth=2
+*.py text=auto whitespace=trailing-space,tab-in-indent,tabwidth=4
+
+# Denote all files that are truly binary and should not be modified.
+*.png binary
+*.jpg binary
diff --git a/.gitignore b/.gitignore
index 5c11ba05..77b21b34 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,4 @@
settings.json
.gitignore
.coverage
+/nanovna-saver.exe.spec
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 841431ad..1ccc1761 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,14 @@
Changelog
=========
+v0.3.9
+------
+
+- TX Power on V2
+- New analysis
+- Magnitude Z Chart
+- VSWR Chart improvements
+
v0.3.8
------
diff --git a/NanoVNASaver/About.py b/NanoVNASaver/About.py
index 0b195f83..ec594301 100644
--- a/NanoVNASaver/About.py
+++ b/NanoVNASaver/About.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-VERSION = "0.3.8"
+VERSION = "0.3.9"
VERSION_URL = (
"https://raw.githubusercontent.com/"
"NanoVNA-Saver/nanovna-saver/master/NanoVNASaver/About.py")
diff --git a/NanoVNASaver/Analysis/Analysis.py b/NanoVNASaver/Analysis/Analysis.py
index cf0fdd15..241f280f 100644
--- a/NanoVNASaver/Analysis/Analysis.py
+++ b/NanoVNASaver/Analysis/Analysis.py
@@ -18,8 +18,10 @@
# along with this program. If not, see .
import logging
import math
-
+import numpy as np
+from scipy.signal import argrelextrema
from PyQt5 import QtWidgets
+from scipy import signal
logger = logging.getLogger(__name__)
@@ -27,6 +29,98 @@
class Analysis:
_widget = None
+ @classmethod
+ def find_crossing_zero(cls, data, threshold=0):
+ '''
+
+ Find values crossing zero
+ return list of tuples (before, crossing, after)
+ indicating the index of data list
+ crossing is where data == 0
+ or data nearest 0
+
+ at maximum 1 value == 0
+ data must not start or end with 0
+
+
+ :param cls:
+ :param data: list of values
+ :param threshold: unused, for future manage flipping around 0
+ '''
+ my_data = np.array(data)
+ zeroes = np.where(my_data == 0)[0]
+
+ if 0 in zeroes:
+ raise ValueError("Data must non start with 0")
+
+ if len(data) - 1 in zeroes:
+ raise ValueError("Data must non end with 0")
+ crossing = [(n - 1, n, n + 1) for n in zeroes]
+
+ for n in np.where((my_data[:-1] * my_data[1:]) < 0)[0]:
+ if abs(data[n]) <= abs(data[n + 1]):
+ crossing.append((n, n, n + 1))
+ else:
+ crossing.append((n, n + 1, n + 1))
+
+ return crossing
+
+ @classmethod
+ def find_minimums(cls, data, threshold):
+ '''
+
+ Find values above threshold
+ return list of tuples (start, lowest, end)
+ indicating the index of data list
+
+
+ :param cls:
+ :param data: list of values
+ :param threshold:
+ '''
+
+ minimums = []
+ min_start = -1
+ min_idx = -1
+
+ min_val = threshold
+ for i, d in enumerate(data):
+ if d < threshold and i < len(data) - 1:
+ if d < min_val:
+ min_val = d
+ min_idx = i
+ if min_start == -1:
+ min_start = i
+ elif min_start != -1:
+ # We are above the threshold, and were in a section that was
+ # below
+ minimums.append((min_start, min_idx, i - 1))
+ min_start = -1
+ min_idx = -1
+ min_val = threshold
+ return minimums
+
+ @classmethod
+ def find_maximums(cls, data, threshold=None):
+ '''
+
+ Find peacs
+
+
+ :param cls:
+ :param data: list of values
+ :param threshold:
+ '''
+ peaks, _ = signal.find_peaks(
+ data, width=2, distance=3, prominence=1)
+
+# my_data = np.array(data)
+# maximums = argrelextrema(my_data, np.greater)[0]
+ if threshold is None:
+ return peaks
+ else:
+ return [k for k in peaks if data[k] > threshold]
+
def __init__(self, app: QtWidgets.QWidget):
self.app = app
@@ -50,8 +144,10 @@ def calculateRolloff(self, location1, location2):
if frequency_factor < 1:
frequency_factor = 1 / frequency_factor
attenuation = abs(gain1 - gain2)
- logger.debug("Measured points: %d Hz and %d Hz", frequency1, frequency2)
+ logger.debug("Measured points: %d Hz and %d Hz",
+ frequency1, frequency2)
logger.debug("%f dB over %f factor", attenuation, frequency_factor)
- octave_attenuation = attenuation / (math.log10(frequency_factor) / math.log10(2))
+ octave_attenuation = attenuation / \
+ (math.log10(frequency_factor) / math.log10(2))
decade_attenuation = attenuation / math.log10(frequency_factor)
return octave_attenuation, decade_attenuation
diff --git a/NanoVNASaver/Analysis/PeakSearchAnalysis.py b/NanoVNASaver/Analysis/PeakSearchAnalysis.py
index c72465bc..0611f962 100644
--- a/NanoVNASaver/Analysis/PeakSearchAnalysis.py
+++ b/NanoVNASaver/Analysis/PeakSearchAnalysis.py
@@ -23,6 +23,10 @@
import numpy as np
from NanoVNASaver.Analysis import Analysis
+from NanoVNASaver.Formatting import format_vswr
+from NanoVNASaver.Formatting import format_gain
+from NanoVNASaver.Formatting import format_resistance
+from NanoVNASaver.Formatting import format_frequency_short
logger = logging.getLogger(__name__)
@@ -38,8 +42,8 @@ def __init__(self, app):
super().__init__(app)
self._widget = QtWidgets.QWidget()
- outer_layout = QtWidgets.QFormLayout()
- self._widget.setLayout(outer_layout)
+ self.layout = QtWidgets.QFormLayout()
+ self._widget.setLayout(self.layout)
self.rbtn_data_group = QtWidgets.QButtonGroup()
self.rbtn_data_vswr = QtWidgets.QRadioButton("VSWR")
@@ -70,40 +74,56 @@ def __init__(self, app):
self.checkbox_move_markers = QtWidgets.QCheckBox()
- outer_layout.addRow(QtWidgets.QLabel("Settings"))
- outer_layout.addRow("Data source", self.rbtn_data_vswr)
- outer_layout.addRow("", self.rbtn_data_resistance)
- outer_layout.addRow("", self.rbtn_data_reactance)
- outer_layout.addRow("", self.rbtn_data_s21_gain)
- outer_layout.addRow(PeakSearchAnalysis.QHLine())
- outer_layout.addRow("Peak type", self.rbtn_peak_positive)
- outer_layout.addRow("", self.rbtn_peak_negative)
+ self.layout.addRow(QtWidgets.QLabel("Settings"))
+ self.layout.addRow("Data source", self.rbtn_data_vswr)
+ self.layout.addRow("", self.rbtn_data_resistance)
+ self.layout.addRow("", self.rbtn_data_reactance)
+ self.layout.addRow("", self.rbtn_data_s21_gain)
+ self.layout.addRow(PeakSearchAnalysis.QHLine())
+ self.layout.addRow("Peak type", self.rbtn_peak_positive)
+ self.layout.addRow("", self.rbtn_peak_negative)
# outer_layout.addRow("", self.rbtn_peak_both)
- outer_layout.addRow(PeakSearchAnalysis.QHLine())
- outer_layout.addRow("Max number of peaks", self.input_number_of_peaks)
- outer_layout.addRow("Move markers", self.checkbox_move_markers)
- outer_layout.addRow(PeakSearchAnalysis.QHLine())
-
- outer_layout.addRow(QtWidgets.QLabel("Results"))
+ self.layout.addRow(PeakSearchAnalysis.QHLine())
+ self.layout.addRow("Max number of peaks", self.input_number_of_peaks)
+ self.layout.addRow("Move markers", self.checkbox_move_markers)
+ self.layout.addRow(PeakSearchAnalysis.QHLine())
+ self.layout.addRow(QtWidgets.QLabel("Results"))
+ self.results_header = self.layout.rowCount()
def runAnalysis(self):
+ self.reset()
+ data = []
+ sign = 1
count = self.input_number_of_peaks.value()
if self.rbtn_data_vswr.isChecked():
- data = []
+ fn = format_vswr
for d in self.app.data11:
- data11.append(d.vswr)
+ data.append(d.vswr)
elif self.rbtn_data_s21_gain.isChecked():
- data = []
+ fn = format_gain
for d in self.app.data21:
data.append(d.gain)
+ elif self.rbtn_data_resistance.isChecked():
+ fn = format_resistance
+ for d in self.app.data11:
+ data.append(d.impedance().real)
+ elif self.rbtn_data_reactance.isChecked():
+ fn = str
+ for d in self.app.data11:
+ data.append(d.impedance().imag)
+
else:
logger.warning("Searching for peaks on unknown data")
return
if self.rbtn_peak_positive.isChecked():
- peaks, _ = signal.find_peaks(data, width=3, distance=3, prominence=1)
+ peaks, _ = signal.find_peaks(
+ data, width=3, distance=3, prominence=1)
elif self.rbtn_peak_negative.isChecked():
- peaks, _ = signal.find_peaks(np.array(data)*-1, width=3, distance=3, prominence=1)
+ sign = -1
+ data = [x * sign for x in data]
+ peaks, _ = signal.find_peaks(
+ data, width=3, distance=3, prominence=1)
# elif self.rbtn_peak_both.isChecked():
# peaks_max, _ = signal.find_peaks(data, width=3, distance=3, prominence=1)
# peaks_min, _ = signal.find_peaks(np.array(data)*-1, width=3, distance=3, prominence=1)
@@ -117,8 +137,8 @@ def runAnalysis(self):
# Having found the peaks, get the prominence data
- for p in peaks:
- logger.debug("Peak at %d", p)
+ for i, p in np.ndenumerate(peaks):
+ logger.debug("Peak %i at %d", i, p)
prominences = signal.peak_prominences(data, peaks)[0]
logger.debug("%d prominences", len(prominences))
@@ -131,9 +151,13 @@ def runAnalysis(self):
logger.debug("Prominence %f", prominences[i])
logger.debug("Index in sweep %d", peaks[i])
logger.debug("Frequency %d", self.app.data11[peaks[i]].freq)
- logger.debug("Value %f", data[peaks[i]])
+ logger.debug("Value %f", sign * data[peaks[i]])
+ self.layout.addRow(
+ f"Freq {format_frequency_short(self.app.data11[peaks[i]].freq)}",
+ QtWidgets.QLabel(f" value {fn(sign * data[peaks[i]])}"
+ ))
- if self.checkbox_move_markers:
+ if self.checkbox_move_markers.isChecked():
if count > len(self.app.markers):
logger.warning("More peaks found than there are markers")
for i in range(min(count, len(self.app.markers))):
@@ -152,4 +176,10 @@ def runAnalysis(self):
logger.debug("Max peak at %d, value %f", max_idx, max_val)
def reset(self):
- pass
+ logger.debug("Reset analysis")
+
+ logger.debug("Results start at %d, out of %d",
+ self.results_header, self.layout.rowCount())
+ for i in range(self.results_header, self.layout.rowCount()):
+ logger.debug("deleting %s", self.layout.rowCount())
+ self.layout.removeRow(self.layout.rowCount() - 1)
diff --git a/NanoVNASaver/Analysis/VSWRAnalysis.py b/NanoVNASaver/Analysis/VSWRAnalysis.py
index decfdeb9..200dda46 100644
--- a/NanoVNASaver/Analysis/VSWRAnalysis.py
+++ b/NanoVNASaver/Analysis/VSWRAnalysis.py
@@ -23,14 +23,32 @@
from NanoVNASaver.Analysis import Analysis, PeakSearchAnalysis
from NanoVNASaver.Formatting import format_frequency
+from NanoVNASaver.Formatting import format_complex_imp
+from NanoVNASaver.RFTools import reflection_coefficient
+import os
+import csv
+from NanoVNASaver.Marker.Values import Label
+from NanoVNASaver.Marker.Widget import MarkerLabel
+from NanoVNASaver.Marker.Widget import Marker
+from collections import OrderedDict
+from NanoVNASaver.Formatting import format_frequency_short
+from NanoVNASaver.Formatting import format_resistance
logger = logging.getLogger(__name__)
+def round_2(x):
+ return round(x, 2)
+
+
+def format_resistence_neg(x):
+ return format_resistance(x, allow_negative=True)
+
+
class VSWRAnalysis(Analysis):
max_dips_shown = 3
vswr_limit_value = 1.5
-
+
class QHLine(QtWidgets.QFrame):
def __init__(self):
super().__init__()
@@ -61,6 +79,7 @@ def __init__(self, app):
def runAnalysis(self):
max_dips_shown = self.max_dips_shown
data = []
+
for d in self.app.data11:
data.append(d.vswr)
# min_idx = np.argmin(data)
@@ -73,31 +92,17 @@ def runAnalysis(self):
# self.app.markers[0].setFrequency(str(self.app.data11[min_idx].freq))
# self.app.markers[0].frequencyInput.setText(str(self.app.data11[min_idx].freq))
- minimums = []
- min_start = -1
- min_idx = -1
threshold = self.input_vswr_limit.value()
- min_val = threshold
- for i, d in enumerate(data):
- if d < threshold and i < len(data)-1:
- if d < min_val:
- min_val = d
- min_idx = i
- if min_start == -1:
- min_start = i
- elif min_start != -1:
- # We are above the threshold, and were in a section that was below
- minimums.append((min_start, min_idx, i-1))
- min_start = -1
- min_idx = -1
- min_val = threshold
-
- logger.debug("Found %d sections under %f threshold", len(minimums), threshold)
+ minimums = self.find_minimums(data, threshold)
+
+ logger.debug("Found %d sections under %f threshold",
+ len(minimums), threshold)
results_header = self.layout.indexOf(self.results_label)
- logger.debug("Results start at %d, out of %d", results_header, self.layout.rowCount())
+ logger.debug("Results start at %d, out of %d",
+ results_header, self.layout.rowCount())
for i in range(results_header, self.layout.rowCount()):
- self.layout.removeRow(self.layout.rowCount()-1)
+ self.layout.removeRow(self.layout.rowCount() - 1)
if len(minimums) > max_dips_shown:
self.layout.addRow(QtWidgets.QLabel("More than " + str(max_dips_shown) +
@@ -141,7 +146,341 @@ def runAnalysis(self):
format_frequency(self.app.data11[lowest].freq)))
self.layout.addWidget(PeakSearchAnalysis.QHLine())
# Remove the final separator line
- self.layout.removeRow(self.layout.rowCount()-1)
+ self.layout.removeRow(self.layout.rowCount() - 1)
else:
self.layout.addRow(QtWidgets.QLabel(
"No areas found with VSWR below " + str(round(threshold, 2)) + "."))
+
+
+class ResonanceAnalysis(Analysis):
+ # max_dips_shown = 3
+
+ @classmethod
+ def vswr_transformed(cls, z, ratio=49) -> float:
+ refl = reflection_coefficient(z / ratio)
+ mag = abs(refl)
+ if mag == 1:
+ return 1
+ return (1 + mag) / (1 - mag)
+
+ class QHLine(QtWidgets.QFrame):
+ def __init__(self):
+ super().__init__()
+ self.setFrameShape(QtWidgets.QFrame.HLine)
+
+ def __init__(self, app):
+ super().__init__(app)
+
+ self._widget = QtWidgets.QWidget()
+ self.layout = QtWidgets.QFormLayout()
+ self._widget.setLayout(self.layout)
+ self.input_description = QtWidgets.QLineEdit("")
+ self.checkbox_move_marker = QtWidgets.QCheckBox()
+ self.layout.addRow(QtWidgets.QLabel("Settings"))
+ self.layout.addRow("Description", self.input_description)
+ self.layout.addRow(VSWRAnalysis.QHLine())
+
+ self.layout.addRow(VSWRAnalysis.QHLine())
+
+ self.results_label = QtWidgets.QLabel("Results")
+ self.layout.addRow(self.results_label)
+
+ def _get_data(self, index):
+ my_data = {"freq": self.app.data11[index].freq,
+ "s11": self.app.data11[index].z,
+ "lambda": self.app.data11[index].wavelength,
+ "impedance": self.app.data11[index].impedance(),
+ "vswr": self.app.data11[index].vswr,
+ }
+ my_data["vswr_49"] = self.vswr_transformed(
+ my_data["impedance"], 49)
+ my_data["vswr_4"] = self.vswr_transformed(
+ my_data["impedance"], 4)
+ my_data["r"] = my_data["impedance"].real
+ my_data["x"] = my_data["impedance"].imag
+
+ return my_data
+
+ def _get_crossing(self):
+
+ data = []
+ for d in self.app.data11:
+ data.append(d.phase)
+
+ crossing = sorted(self.find_crossing_zero(data))
+ return crossing
+
+ def runAnalysis(self):
+ self.reset()
+ # self.results_label = QtWidgets.QLabel("Results")
+ # max_dips_shown = self.max_dips_shown
+ description = self.input_description.text()
+ if description:
+ filename = os.path.join("/tmp/", "{}.csv".format(description))
+ else:
+ filename = None
+
+ crossing = self._get_crossing()
+
+ logger.debug("Found %d sections ",
+ len(crossing))
+
+ results_header = self.layout.indexOf(self.results_label)
+ logger.debug("Results start at %d, out of %d",
+ results_header, self.layout.rowCount())
+ for i in range(results_header, self.layout.rowCount()):
+ self.layout.removeRow(self.layout.rowCount() - 1)
+
+# if len(crossing) > max_dips_shown:
+# self.layout.addRow(QtWidgets.QLabel("More than " + str(max_dips_shown) +
+# " dips found. Lowest shown."))
+
+# self.crossing = crossing[:max_dips_shown]
+ extended_data = []
+ if len(crossing) > 0:
+
+ for m in crossing:
+ start, lowest, end = m
+ my_data = self._get_data(lowest)
+
+ extended_data.append(my_data)
+ if start != end:
+ logger.debug(
+ "Section from %d to %d, lowest at %d", start, end, lowest)
+
+ self.layout.addRow(
+ "Resonance",
+ QtWidgets.QLabel(
+ f"{format_frequency(self.app.data11[lowest].freq)}"
+ f" ({format_complex_imp(self.app.data11[lowest].impedance())})"))
+ else:
+ self.layout.addRow("Resonance", QtWidgets.QLabel(
+ format_frequency(self.app.data11[lowest].freq)))
+ self.layout.addWidget(PeakSearchAnalysis.QHLine())
+ # Remove the final separator line
+ self.layout.removeRow(self.layout.rowCount() - 1)
+ if filename and extended_data:
+
+ with open(filename, 'w', newline='') as csvfile:
+ fieldnames = extended_data[0].keys()
+ writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
+
+ writer.writeheader()
+ for row in extended_data:
+ writer.writerow(row)
+
+ else:
+ self.layout.addRow(QtWidgets.QLabel(
+ "No resonance found"))
+
+
+class EFHWAnalysis(ResonanceAnalysis):
+ '''
+ find only resonance when HI impedance
+ '''
+ old_data = []
+
+ def reset(self):
+ logger.debug("reset")
+
+ def runAnalysis(self):
+ self.reset()
+ # self.results_label = QtWidgets.QLabel("Results")
+ # max_dips_shown = self.max_dips_shown
+ description = self.input_description.text()
+ if description:
+ filename = os.path.join("/tmp/", "{}.csv".format(description))
+ else:
+ filename = None
+
+ crossing = self._get_crossing()
+
+ data = []
+ for d in self.app.data11:
+ data.append(d.impedance().real)
+
+ maximums = sorted(self.find_maximums(data, threshold=500))
+
+ results_header = self.layout.indexOf(self.results_label)
+ logger.debug("Results start at %d, out of %d",
+ results_header, self.layout.rowCount())
+ for i in range(results_header, self.layout.rowCount()):
+ self.layout.removeRow(self.layout.rowCount() - 1)
+
+ extended_data = OrderedDict()
+
+ #both = np.intersect1d([i[1] for i in crossing], maximums)
+ both = []
+
+ tolerance = 2
+ for i in maximums:
+ for l, _, h in crossing:
+ if l - tolerance <= i <= h + tolerance:
+ both.append(i)
+ continue
+ if l > i:
+ continue
+
+ if both:
+ logger.info("%i crossing HW", len(both))
+ logger.info(crossing)
+ logger.info(maximums)
+ logger.info(both)
+ for m in both:
+ my_data = self._get_data(m)
+ if m in extended_data:
+ extended_data[m].update(my_data)
+ else:
+ extended_data[m] = my_data
+ for i in range(min(len(both), len(self.app.markers))):
+
+ # self.app.markers[i].label = {}
+ # for l in TYPES:
+ # self.app.markers[i][l.label_id] = MarkerLabel(l.name)
+ # self.app.markers[i].label['actualfreq'].setMinimumWidth(
+ # 100)
+ # self.app.markers[i].label['returnloss'].setMinimumWidth(80)
+
+ self.app.markers[i].setFrequency(
+ str(self.app.data11[both[i]].freq))
+ self.app.markers[i].frequencyInput.setText(
+ str(self.app.data11[both[i]].freq))
+ else:
+ logger.info("TO DO: find near data")
+ for m in crossing:
+ start, lowest, end = m
+ my_data = self._get_data(lowest)
+
+ if lowest in extended_data:
+ extended_data[lowest].update(my_data)
+ else:
+ extended_data[lowest] = my_data
+
+ logger.debug("maximumx %s of type %s", maximums, type(maximums))
+ for m in maximums:
+ logger.debug("m %s of type %s", m, type(m))
+
+ my_data = self._get_data(m)
+ if m in extended_data:
+ extended_data[m].update(my_data)
+ else:
+ extended_data[m] = my_data
+
+ # saving and comparing
+
+ fields = [("freq", format_frequency_short),
+ ("r", format_resistence_neg),
+ ("lambda", round_2),
+ ]
+ if self.old_data:
+ diff = self.compare(
+ self.old_data[-1], extended_data, fields=fields)
+ else:
+ diff = self.compare({}, extended_data, fields=fields)
+ self.old_data.append(extended_data)
+
+ for i, index in enumerate(sorted(extended_data.keys())):
+
+ self.layout.addRow(
+ f"{format_frequency_short(self.app.data11[index].freq)}",
+ QtWidgets.QLabel(f" ({diff[i]['freq']})"
+ f" {format_complex_imp(self.app.data11[index].impedance())}"
+ f" ({diff[i]['r']})"
+ f" {diff[i]['lambda']} m"))
+
+ # Remove the final separator line
+ # self.layout.removeRow(self.layout.rowCount() - 1)
+ if filename and extended_data:
+
+ with open(filename, 'w', newline='') as csvfile:
+ fieldnames = extended_data[sorted(
+ extended_data.keys())[0]].keys()
+ writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
+
+ writer.writeheader()
+ for index in sorted(extended_data.keys()):
+ row = extended_data[index]
+ writer.writerow(row)
+
+ def compare(self, old, new, fields=[("freq", str), ]):
+ '''
+ Compare data to help changes
+
+ NB
+ must be same sweep
+ ( same index must be same frequence )
+ :param old:
+ :param new:
+ '''
+
+ def no_compare():
+
+ return {k: "-" for k, _ in fields}
+
+ old_idx = sorted(old.keys())
+ # 'odict_keys' object is not subscriptable
+ new_idx = sorted(new.keys())
+ diff = {}
+ i_max = min(len(old_idx), len(new_idx))
+ i_tot = max(len(old_idx), len(new_idx))
+
+ if i_max == i_tot:
+ logger.debug("may be the same antenna ... analyzing")
+
+ else:
+ logger.warning("resonances changed from %s to %s",
+ len(old_idx), len(new_idx))
+
+ logger.debug("Trying to compare only first %s resonances", i_max)
+
+ split = 0
+ max_delta_f = 1000000 # 1M
+ for i, k in enumerate(new_idx):
+ my_diff = {}
+
+ logger.info("Risonance %s at %s", i,
+ format_frequency(new[k]["freq"]))
+
+ if len(old_idx) <= i + split:
+ diff[i] = no_compare()
+ continue
+
+ delta_f = new[k]["freq"] - old[old_idx[i + split]]["freq"]
+ if abs(delta_f) < max_delta_f:
+ logger.debug("can compare")
+
+ else:
+ logger.debug("can't compare, %s is too much ",
+ format_frequency(delta_f))
+ if delta_f > 0:
+
+ logger.debug("possible missing band, ")
+ if (len(old_idx) > (i + split + 1)):
+ if abs(new[k]["freq"] - old[old_idx[i + split + 1]]["freq"]) < max_delta_f:
+ logger.debug("new is missing band, compare next ")
+ split += 1
+ # FIXME: manage 2 or more band missing ?!?
+ else:
+ logger.debug("new band, non compare ")
+ diff[i] = no_compare()
+ continue
+ else:
+ logger.debug("new band, non compare ")
+ diff[i] = no_compare()
+
+ split -= 1
+ continue
+
+ for d, fn in fields:
+ my_diff[d] = fn(new[k][d] - old[old_idx[i + split]][d])
+ logger.info("Delta %s = %s", d,
+ my_diff[d])
+
+ diff[i] = my_diff
+
+ for i in range(i_max, i_tot):
+ # add missing in old ... if any
+
+ diff[i] = no_compare()
+
+ return diff
diff --git a/NanoVNASaver/Charts/Frequency.py b/NanoVNASaver/Charts/Frequency.py
index 7b99309e..66e9fd68 100644
--- a/NanoVNASaver/Charts/Frequency.py
+++ b/NanoVNASaver/Charts/Frequency.py
@@ -44,6 +44,7 @@ class FrequencyChart(Chart):
fixedValues = False
logarithmicX = False
+ logarithmicY = False
leftMargin = 30
rightMargin = 20
@@ -132,6 +133,23 @@ def __init__(self, name):
self.y_menu.addAction(self.action_set_fixed_maximum)
self.y_menu.addAction(self.action_set_fixed_minimum)
+ if self.logarithmicYAllowed(): # This only works for some plot types
+ self.y_menu.addSeparator()
+ vertical_mode_group = QtWidgets.QActionGroup(self.y_menu)
+ self.action_set_linear_y = QtWidgets.QAction("Linear")
+ self.action_set_linear_y.setCheckable(True)
+ self.action_set_logarithmic_y = QtWidgets.QAction("Logarithmic")
+ self.action_set_logarithmic_y.setCheckable(True)
+ vertical_mode_group.addAction(self.action_set_linear_y)
+ vertical_mode_group.addAction(self.action_set_logarithmic_y)
+ self.action_set_linear_y.triggered.connect(
+ lambda: self.setLogarithmicY(False))
+ self.action_set_logarithmic_y.triggered.connect(
+ lambda: self.setLogarithmicY(True))
+ self.action_set_linear_y.setChecked(True)
+ self.y_menu.addAction(self.action_set_linear_y)
+ self.y_menu.addAction(self.action_set_logarithmic_y)
+
self.menu.addMenu(self.x_menu)
self.menu.addMenu(self.y_menu)
self.menu.addSeparator()
@@ -178,12 +196,21 @@ def setFixedValues(self, fixed_values: bool):
self.fixedValues = False
self.y_action_automatic.setChecked(True)
self.y_action_fixed_span.setChecked(False)
+ if fixed_values and self.minDisplayValue <= 0:
+ self.minDisplayValue = 0.01
self.update()
def setLogarithmicX(self, logarithmic: bool):
self.logarithmicX = logarithmic
self.update()
+ def setLogarithmicY(self, logarithmic: bool):
+ self.logarithmicY = logarithmic and self.logarithmicYAllowed()
+ self.update()
+
+ def logarithmicYAllowed(self) -> bool:
+ return False
+
def setMinimumFrequency(self):
min_freq_str, selected = QtWidgets.QInputDialog.getText(
self, "Start frequency",
@@ -217,6 +244,8 @@ def setMinimumValue(self):
return
if not (self.fixedValues and min_val >= self.maxDisplayValue):
self.minDisplayValue = min_val
+ if self.logarithmicY and min_val <= 0:
+ self.minDisplayValue = 0.01
if self.fixedValues:
self.update()
@@ -239,6 +268,9 @@ def resetDisplayLimits(self):
self.action_automatic.setChecked(True)
self.logarithmicX = False
self.action_set_linear_x.setChecked(True)
+ self.logarithmicY = False
+ if self.logarithmicYAllowed():
+ self.action_set_linear_y.setChecked(True)
self.update()
def getXPosition(self, d: Datapoint) -> int:
@@ -585,6 +617,11 @@ def copy(self):
new_chart.setLogarithmicX(self.logarithmicX)
new_chart.action_set_logarithmic_x.setChecked(self.logarithmicX)
new_chart.action_set_linear_x.setChecked(not self.logarithmicX)
+
+ new_chart.setLogarithmicY(self.logarithmicY)
+ if self.logarithmicYAllowed():
+ new_chart.action_set_logarithmic_y.setChecked(self.logarithmicY)
+ new_chart.action_set_linear_y.setChecked(not self.logarithmicY)
return new_chart
def keyPressEvent(self, a0: QtGui.QKeyEvent) -> None:
diff --git a/NanoVNASaver/Charts/MagnitudeZ.py b/NanoVNASaver/Charts/MagnitudeZ.py
index 6b13bd46..40390bb5 100644
--- a/NanoVNASaver/Charts/MagnitudeZ.py
+++ b/NanoVNASaver/Charts/MagnitudeZ.py
@@ -23,6 +23,7 @@
from PyQt5 import QtWidgets, QtGui
from NanoVNASaver.RFTools import Datapoint
+from NanoVNASaver.SITools import Format, Value
from .Frequency import FrequencyChart
from .LogMag import LogMagChart
@@ -82,13 +83,18 @@ def drawValues(self, qp: QtGui.QPainter):
maxValue = self.maxDisplayValue
minValue = self.minDisplayValue
self.maxValue = maxValue
- self.minValue = minValue
+ if self.logarithmicY and minValue <= 0:
+ self.minValue = 0.01
+ else:
+ self.minValue = minValue
else:
# Find scaling
minValue = 100
maxValue = 0
for d in self.data:
mag = self.magnitude(d)
+ if math.isinf(mag): # Avoid infinite scales
+ continue
if mag > maxValue:
maxValue = mag
if mag < minValue:
@@ -97,13 +103,18 @@ def drawValues(self, qp: QtGui.QPainter):
if d.freq < self.fstart or d.freq > self.fstop:
continue
mag = self.magnitude(d)
+ if math.isinf(mag): # Avoid infinite scales
+ continue
if mag > maxValue:
maxValue = mag
if mag < minValue:
minValue = mag
minValue = 10*math.floor(minValue/10)
+ if self.logarithmicY and minValue <= 0:
+ minValue = 0.01
self.minValue = minValue
+
maxValue = 10*math.ceil(maxValue/10)
self.maxValue = maxValue
@@ -112,28 +123,22 @@ def drawValues(self, qp: QtGui.QPainter):
span = 0.01
self.span = span
- target_ticks = math.floor(self.chartHeight / 60)
-
- for i in range(target_ticks):
- val = minValue + (i / target_ticks) * span
- y = self.topMargin + round((self.maxValue - val) / self.span * self.chartHeight)
- qp.setPen(self.textColor)
- if val != minValue:
- digits = max(0, min(2, math.floor(3 - math.log10(abs(val)))))
- if digits == 0:
- vswrstr = str(round(val))
- else:
- vswrstr = str(round(val, digits))
- qp.drawText(3, y + 3, vswrstr)
+ # We want one horizontal tick per 50 pixels, at most
+ horizontal_ticks = math.floor(self.chartHeight/50)
+ fmt = Format(max_nr_digits=4)
+ for i in range(horizontal_ticks):
+ y = self.topMargin + round(i * self.chartHeight / horizontal_ticks)
qp.setPen(QtGui.QPen(self.foregroundColor))
- qp.drawLine(self.leftMargin - 5, y, self.leftMargin + self.chartWidth, y)
-
- qp.setPen(QtGui.QPen(self.foregroundColor))
- qp.drawLine(self.leftMargin - 5, self.topMargin,
- self.leftMargin + self.chartWidth, self.topMargin)
- qp.setPen(self.textColor)
- qp.drawText(3, self.topMargin + 4, str(maxValue))
- qp.drawText(3, self.chartHeight+self.topMargin, str(minValue))
+ qp.drawLine(self.leftMargin - 5, y,
+ self.leftMargin + self.chartWidth + 5, y)
+ qp.setPen(QtGui.QPen(self.textColor))
+ val = Value(self.valueAtPosition(y)[0], fmt=fmt)
+ qp.drawText(3, y + 4, str(val))
+
+ qp.drawText(3,
+ self.chartHeight + self.topMargin,
+ str(Value(self.minValue, fmt=fmt)))
+
self.drawFrequencyTicks(qp)
self.drawData(qp, self.data, self.sweepColor)
@@ -142,17 +147,32 @@ def drawValues(self, qp: QtGui.QPainter):
def getYPosition(self, d: Datapoint) -> int:
mag = self.magnitude(d)
- return self.topMargin + round((self.maxValue - mag) / self.span * self.chartHeight)
+ if self.logarithmicY and mag == 0:
+ return self.topMargin - self.chartHeight
+ if math.isfinite(mag):
+ if self.logarithmicY:
+ span = math.log(self.maxValue) - math.log(self.minValue)
+ return self.topMargin + round((math.log(self.maxValue) - math.log(mag)) / span * self.chartHeight)
+ return self.topMargin + round((self.maxValue - mag) / self.span * self.chartHeight)
+ else:
+ return self.topMargin
def valueAtPosition(self, y) -> List[float]:
absy = y - self.topMargin
- val = -1 * ((absy / self.chartHeight * self.span) - self.maxValue)
+ if self.logarithmicY:
+ span = math.log(self.maxValue) - math.log(self.minValue)
+ val = math.exp(math.log(self.maxValue) - absy * span / self.chartHeight)
+ else:
+ val = self.maxValue - (absy / self.chartHeight * self.span)
return [val]
@staticmethod
def magnitude(p: Datapoint) -> float:
return abs(p.impedance())
+ def logarithmicYAllowed(self) -> bool:
+ return True;
+
def copy(self):
new_chart: LogMagChart = super().copy()
new_chart.span = self.span
diff --git a/NanoVNASaver/Charts/MagnitudeZSeries.py b/NanoVNASaver/Charts/MagnitudeZSeries.py
new file mode 100644
index 00000000..1d276caf
--- /dev/null
+++ b/NanoVNASaver/Charts/MagnitudeZSeries.py
@@ -0,0 +1,40 @@
+
+# NanoVNASaver
+#
+# A python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019, 2020 Rune B. Broberg
+# Copyright (C) 2020 NanoVNA-Saver Authors
+#
+# 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 math
+import logging
+from typing import List
+
+from PyQt5 import QtWidgets, QtGui
+
+from NanoVNASaver.RFTools import Datapoint
+from .MagnitudeZ import MagnitudeZChart
+
+
+logger = logging.getLogger(__name__)
+
+
+class MagnitudeZSeriesChart(MagnitudeZChart):
+ def __init__(self, name=""):
+ super().__init__(name)
+
+ @staticmethod
+ def magnitude(p: Datapoint) -> float:
+ return abs(p.seriesImpedance())
+
diff --git a/NanoVNASaver/Charts/MagnitudeZShunt.py b/NanoVNASaver/Charts/MagnitudeZShunt.py
new file mode 100644
index 00000000..0bd4d057
--- /dev/null
+++ b/NanoVNASaver/Charts/MagnitudeZShunt.py
@@ -0,0 +1,40 @@
+
+# NanoVNASaver
+#
+# A python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019, 2020 Rune B. Broberg
+# Copyright (C) 2020 NanoVNA-Saver Authors
+#
+# 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 math
+import logging
+from typing import List
+
+from PyQt5 import QtWidgets, QtGui
+
+from NanoVNASaver.RFTools import Datapoint
+from .MagnitudeZ import MagnitudeZChart
+
+
+logger = logging.getLogger(__name__)
+
+
+class MagnitudeZShuntChart(MagnitudeZChart):
+ def __init__(self, name=""):
+ super().__init__(name)
+
+ @staticmethod
+ def magnitude(p: Datapoint) -> float:
+ return abs(p.shuntImpedance())
+
diff --git a/NanoVNASaver/Charts/Permeability.py b/NanoVNASaver/Charts/Permeability.py
index 997019ba..76607f28 100644
--- a/NanoVNASaver/Charts/Permeability.py
+++ b/NanoVNASaver/Charts/Permeability.py
@@ -40,7 +40,6 @@ def __init__(self, name=""):
self.fstop = 0
self.span = 0.01
self.max = 0
- self.logarithmicY = True
self.maxDisplayValue = 100
self.minDisplayValue = -100
@@ -59,27 +58,11 @@ def __init__(self, name=""):
self.setPalette(pal)
self.setAutoFillBackground(True)
- self.y_menu.addSeparator()
- self.y_log_lin_group = QtWidgets.QActionGroup(self.y_menu)
- self.y_action_linear = QtWidgets.QAction("Linear")
- self.y_action_linear.setCheckable(True)
- self.y_action_logarithmic = QtWidgets.QAction("Logarithmic")
- self.y_action_logarithmic.setCheckable(True)
- self.y_action_logarithmic.setChecked(True)
- self.y_action_linear.triggered.connect(lambda: self.setLogarithmicY(False))
- self.y_action_logarithmic.triggered.connect(lambda: self.setLogarithmicY(True))
- self.y_log_lin_group.addAction(self.y_action_linear)
- self.y_log_lin_group.addAction(self.y_action_logarithmic)
- self.y_menu.addAction(self.y_action_linear)
- self.y_menu.addAction(self.y_action_logarithmic)
-
- def setLogarithmicY(self, logarithmic: bool):
- self.logarithmicY = logarithmic
- self.update()
+ def logarithmicYAllowed(self) -> bool:
+ return True;
def copy(self):
new_chart: PermeabilityChart = super().copy()
- new_chart.logarithmicY = self.logarithmicY
return new_chart
def drawChart(self, qp: QtGui.QPainter):
diff --git a/NanoVNASaver/Charts/RI.py b/NanoVNASaver/Charts/RI.py
index e27ad6ac..9d5d2d4b 100644
--- a/NanoVNASaver/Charts/RI.py
+++ b/NanoVNASaver/Charts/RI.py
@@ -24,6 +24,7 @@
from NanoVNASaver.Marker import Marker
from NanoVNASaver.RFTools import Datapoint
+from NanoVNASaver.SITools import Format, Value
from .Chart import Chart
from .Frequency import FrequencyChart
@@ -178,8 +179,10 @@ def drawValues(self, qp: QtGui.QPainter):
max_real = 0
max_imag = -1000
for d in self.data:
- imp = d.impedance()
+ imp = self.impedance(d)
re, im = imp.real, imp.imag
+ if math.isinf(re): # Avoid infinite scales
+ continue
if re > max_real:
max_real = re
if re < min_real:
@@ -191,8 +194,10 @@ def drawValues(self, qp: QtGui.QPainter):
for d in self.reference: # Also check min/max for the reference sweep
if d.freq < fstart or d.freq > fstop:
continue
- imp = d.impedance()
+ imp = self.impedance(d)
re, im = imp.real, imp.imag
+ if math.isinf(re): # Avoid infinite scales
+ continue
if re > max_real:
max_real = re
if re < min_real:
@@ -250,6 +255,7 @@ def drawValues(self, qp: QtGui.QPainter):
# We want one horizontal tick per 50 pixels, at most
horizontal_ticks = math.floor(self.chartHeight/50)
+ fmt = Format(max_nr_digits=4)
for i in range(horizontal_ticks):
y = self.topMargin + round(i * self.chartHeight / horizontal_ticks)
qp.setPen(QtGui.QPen(self.foregroundColor))
@@ -257,13 +263,13 @@ def drawValues(self, qp: QtGui.QPainter):
qp.setPen(QtGui.QPen(self.textColor))
re = max_real - i * span_real / horizontal_ticks
im = max_imag - i * span_imag / horizontal_ticks
- qp.drawText(3, y + 4, str(round(re, 1)))
- qp.drawText(self.leftMargin + self.chartWidth + 8, y + 4, str(round(im, 1)))
+ qp.drawText(3, y + 4, str(Value(re, fmt=fmt)))
+ qp.drawText(self.leftMargin + self.chartWidth + 8, y + 4, str(Value(im, fmt=fmt)))
- qp.drawText(3, self.chartHeight + self.topMargin, str(round(min_real, 1)))
+ qp.drawText(3, self.chartHeight + self.topMargin, str(Value(min_real, fmt=fmt)))
qp.drawText(self.leftMargin + self.chartWidth + 8,
self.chartHeight + self.topMargin,
- str(round(min_imag, 1)))
+ str(Value(min_imag, fmt=fmt)))
self.drawFrequencyTicks(qp)
@@ -397,12 +403,15 @@ def drawValues(self, qp: QtGui.QPainter):
self.drawMarker(x, y_im, qp, m.color, self.markers.index(m)+1)
def getImYPosition(self, d: Datapoint) -> int:
- im = d.impedance().imag
+ im = self.impedance(d).imag
return self.topMargin + round((self.max_imag - im) / self.span_imag * self.chartHeight)
def getReYPosition(self, d: Datapoint) -> int:
- re = d.impedance().real
- return self.topMargin + round((self.max_real - re) / self.span_real * self.chartHeight)
+ re = self.impedance(d).real
+ if math.isfinite(re):
+ return self.topMargin + round((self.max_real - re) / self.span_real * self.chartHeight)
+ else:
+ return self.topMargin
def valueAtPosition(self, y) -> List[float]:
absy = y - self.topMargin
@@ -520,3 +529,6 @@ def contextMenuEvent(self, event):
self.action_set_fixed_maximum_imag.setText(
f"Maximum jX ({self.maxDisplayImag})")
self.menu.exec_(event.globalPos())
+
+ def impedance(self, p: Datapoint) -> complex:
+ return p.impedance()
diff --git a/NanoVNASaver/Charts/RISeries.py b/NanoVNASaver/Charts/RISeries.py
new file mode 100644
index 00000000..b2ca4eff
--- /dev/null
+++ b/NanoVNASaver/Charts/RISeries.py
@@ -0,0 +1,35 @@
+# NanoVNASaver
+#
+# A python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019, 2020 Rune B. Broberg
+# Copyright (C) 2020 NanoVNA-Saver Authors
+#
+# 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 math
+import logging
+from typing import List
+
+from NanoVNASaver.RFTools import Datapoint
+
+from .RI import RealImaginaryChart
+
+logger = logging.getLogger(__name__)
+
+
+class RealImaginarySeriesChart(RealImaginaryChart):
+ def __init__(self, name=""):
+ super().__init__(name)
+
+ def impedance(self, p: Datapoint) -> complex:
+ return p.seriesImpedance()
diff --git a/NanoVNASaver/Charts/RIShunt.py b/NanoVNASaver/Charts/RIShunt.py
new file mode 100644
index 00000000..82e602f8
--- /dev/null
+++ b/NanoVNASaver/Charts/RIShunt.py
@@ -0,0 +1,35 @@
+# NanoVNASaver
+#
+# A python program to view and export Touchstone data from a NanoVNA
+# Copyright (C) 2019, 2020 Rune B. Broberg
+# Copyright (C) 2020 NanoVNA-Saver Authors
+#
+# 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 math
+import logging
+from typing import List
+
+from NanoVNASaver.RFTools import Datapoint
+
+from .RI import RealImaginaryChart
+
+logger = logging.getLogger(__name__)
+
+
+class RealImaginaryShuntChart(RealImaginaryChart):
+ def __init__(self, name=""):
+ super().__init__(name)
+
+ def impedance(self, p: Datapoint) -> complex:
+ return p.shuntImpedance()
diff --git a/NanoVNASaver/Charts/VSWR.py b/NanoVNASaver/Charts/VSWR.py
index 737d96a6..7ce8dd47 100644
--- a/NanoVNASaver/Charts/VSWR.py
+++ b/NanoVNASaver/Charts/VSWR.py
@@ -29,7 +29,6 @@
class VSWRChart(FrequencyChart):
- logarithmicY = False
maxVSWR = 3
span = 2
@@ -51,27 +50,12 @@ def __init__(self, name=""):
pal.setColor(QtGui.QPalette.Background, self.backgroundColor)
self.setPalette(pal)
self.setAutoFillBackground(True)
- self.y_menu.addSeparator()
- self.y_log_lin_group = QtWidgets.QActionGroup(self.y_menu)
- self.y_action_linear = QtWidgets.QAction("Linear")
- self.y_action_linear.setCheckable(True)
- self.y_action_linear.setChecked(True)
- self.y_action_logarithmic = QtWidgets.QAction("Logarithmic")
- self.y_action_logarithmic.setCheckable(True)
- self.y_action_linear.triggered.connect(lambda: self.setLogarithmicY(False))
- self.y_action_logarithmic.triggered.connect(lambda: self.setLogarithmicY(True))
- self.y_log_lin_group.addAction(self.y_action_linear)
- self.y_log_lin_group.addAction(self.y_action_logarithmic)
- self.y_menu.addAction(self.y_action_linear)
- self.y_menu.addAction(self.y_action_logarithmic)
-
- def setLogarithmicY(self, logarithmic: bool):
- self.logarithmicY = logarithmic
- self.update()
+
+ def logarithmicYAllowed(self) -> bool:
+ return True
def copy(self):
new_chart: VSWRChart = super().copy()
- new_chart.logarithmicY = self.logarithmicY
return new_chart
def drawValues(self, qp: QtGui.QPainter):
@@ -211,5 +195,4 @@ def valueAtPosition(self, y) -> List[float]:
def resetDisplayLimits(self):
self.maxDisplayValue = 25
- self.logarithmicY = False
super().resetDisplayLimits()
diff --git a/NanoVNASaver/Charts/__init__.py b/NanoVNASaver/Charts/__init__.py
index ce8c34f2..fb0022be 100644
--- a/NanoVNASaver/Charts/__init__.py
+++ b/NanoVNASaver/Charts/__init__.py
@@ -9,10 +9,14 @@
from .CLogMag import CombinedLogMagChart
from .Magnitude import MagnitudeChart
from .MagnitudeZ import MagnitudeZChart
+from .MagnitudeZShunt import MagnitudeZShuntChart
+from .MagnitudeZSeries import MagnitudeZSeriesChart
from .Permeability import PermeabilityChart
from .Phase import PhaseChart
from .QFactor import QualityFactorChart
from .RI import RealImaginaryChart
+from .RIShunt import RealImaginaryShuntChart
+from .RISeries import RealImaginarySeriesChart
from .Smith import SmithChart
from .SParam import SParameterChart
from .TDR import TDRChart
diff --git a/NanoVNASaver/Hardware/AVNA.py b/NanoVNASaver/Hardware/AVNA.py
index 441bb9df..a26ed9a1 100644
--- a/NanoVNASaver/Hardware/AVNA.py
+++ b/NanoVNASaver/Hardware/AVNA.py
@@ -29,6 +29,7 @@ class AVNA(VNA):
def __init__(self, iface: Interface):
super().__init__(iface)
+ self.sweep_max_freq_Hz = 40e3
self.features.add("Customizable data points")
def isValid(self):
diff --git a/NanoVNASaver/Hardware/NanoVNA.py b/NanoVNASaver/Hardware/NanoVNA.py
index e6ac3c07..d0cea41c 100644
--- a/NanoVNASaver/Hardware/NanoVNA.py
+++ b/NanoVNASaver/Hardware/NanoVNA.py
@@ -40,10 +40,26 @@ def __init__(self, iface: Interface):
super().__init__(iface)
self.sweep_method = "sweep"
self.read_features()
- self.start = 27000000
- self.stop = 30000000
+ logger.debug("Setting initial start,stop")
+ self.start, self.stop = self._get_running_frequencies()
+ self.sweep_max_freq_Hz = 300e6
self._sweepdata = []
+ def _get_running_frequencies(self):
+
+ if self.name == "NanoVNA":
+ logger.debug("Reading values: frequencies")
+ try:
+ frequencies = super().readValues("frequencies")
+ return frequencies[0], frequencies[-1]
+ except Exception as e:
+ logger.warning("%s reading frequencies", e)
+ logger.info("falling back to generic")
+ else:
+ logger.debug("Name %s, fallback to generic", self.name)
+
+ return VNA._get_running_frequencies(self)
+
def _capture_data(self) -> bytes:
timeout = self.serial.timeout
with self.serial.lock:
diff --git a/NanoVNASaver/Hardware/NanoVNA_F.py b/NanoVNASaver/Hardware/NanoVNA_F.py
index 66928232..4852fbda 100644
--- a/NanoVNASaver/Hardware/NanoVNA_F.py
+++ b/NanoVNASaver/Hardware/NanoVNA_F.py
@@ -23,6 +23,7 @@
from PyQt5 import QtGui
from NanoVNASaver.Hardware.NanoVNA import NanoVNA
+from NanoVNASaver.Hardware.Serial import Interface
logger = logging.getLogger(__name__)
@@ -31,3 +32,7 @@ class NanoVNA_F(NanoVNA):
name = "NanoVNA-F"
screenwidth = 800
screenheight = 480
+
+ def __init__(self, iface: Interface):
+ super().__init__(iface)
+ self.sweep_max_freq_Hz = 1500e6
diff --git a/NanoVNASaver/Hardware/NanoVNA_F_V2.py b/NanoVNASaver/Hardware/NanoVNA_F_V2.py
index ae228044..c3fcd92a 100644
--- a/NanoVNASaver/Hardware/NanoVNA_F_V2.py
+++ b/NanoVNASaver/Hardware/NanoVNA_F_V2.py
@@ -1,34 +1,39 @@
-import logging
-from NanoVNASaver.Hardware.Serial import drain_serial, Interface
-import serial
-import struct
-import numpy as np
-from PyQt5 import QtGui
-
-from NanoVNASaver.Hardware.NanoVNA import NanoVNA
-
-logger = logging.getLogger(__name__)
-
-
-class NanoVNA_F_V2(NanoVNA):
- name = "NanoVNA-F_V2"
- screenwidth = 800
- screenheight = 480
-
- def getScreenshot(self) -> QtGui.QPixmap:
- logger.debug("Capturing screenshot...")
- if not self.connected():
- return QtGui.QPixmap()
- try:
- rgba_array = self._capture_data()
- image = QtGui.QImage(
- rgba_array,
- self.screenwidth,
- self.screenheight,
- QtGui.QImage.Format_RGB16)
- logger.debug("Captured screenshot")
- return QtGui.QPixmap(image)
- except serial.SerialException as exc:
- logger.exception(
- "Exception while capturing screenshot: %s", exc)
- return QtGui.QPixmap()
+import logging
+from NanoVNASaver.Hardware.Serial import drain_serial, Interface
+import serial
+import struct
+import numpy as np
+from PyQt5 import QtGui
+
+from NanoVNASaver.Hardware.NanoVNA import NanoVNA
+from NanoVNASaver.Hardware.Serial import Interface
+
+logger = logging.getLogger(__name__)
+
+
+class NanoVNA_F_V2(NanoVNA):
+ name = "NanoVNA-F_V2"
+ screenwidth = 800
+ screenheight = 480
+
+ def __init__(self, iface: Interface):
+ super().__init__(iface)
+ self.sweep_max_freq_Hz = 3e9
+
+ def getScreenshot(self) -> QtGui.QPixmap:
+ logger.debug("Capturing screenshot...")
+ if not self.connected():
+ return QtGui.QPixmap()
+ try:
+ rgba_array = self._capture_data()
+ image = QtGui.QImage(
+ rgba_array,
+ self.screenwidth,
+ self.screenheight,
+ QtGui.QImage.Format_RGB16)
+ logger.debug("Captured screenshot")
+ return QtGui.QPixmap(image)
+ except serial.SerialException as exc:
+ logger.exception(
+ "Exception while capturing screenshot: %s", exc)
+ return QtGui.QPixmap()
diff --git a/NanoVNASaver/Hardware/NanoVNA_H.py b/NanoVNASaver/Hardware/NanoVNA_H.py
index 0615094f..a8ae05ae 100644
--- a/NanoVNASaver/Hardware/NanoVNA_H.py
+++ b/NanoVNASaver/Hardware/NanoVNA_H.py
@@ -19,9 +19,14 @@
import logging
from NanoVNASaver.Hardware.NanoVNA import NanoVNA
+from NanoVNASaver.Hardware.Serial import Interface
logger = logging.getLogger(__name__)
class NanoVNA_H(NanoVNA):
name = "NanoVNA-H"
+
+ def __init__(self, iface: Interface):
+ super().__init__(iface)
+ self.sweep_max_freq_Hz = 1500e6
diff --git a/NanoVNASaver/Hardware/NanoVNA_H4.py b/NanoVNASaver/Hardware/NanoVNA_H4.py
index c689de10..be0c14c8 100644
--- a/NanoVNASaver/Hardware/NanoVNA_H4.py
+++ b/NanoVNASaver/Hardware/NanoVNA_H4.py
@@ -27,10 +27,11 @@ class NanoVNA_H4(NanoVNA_H):
name = "NanoVNA-H4"
screenwidth = 480
screenheight = 320
- valid_datapoints = (101, 11, 51, 201)
+ valid_datapoints = (101, 11, 51, 201, 401)
def __init__(self, iface: Interface):
super().__init__(iface)
+ self.sweep_max_freq_Hz = 1500e6
self.sweep_method = "scan"
if "Scan mask command" in self.features:
self.sweep_method = "scan_mask"
diff --git a/NanoVNASaver/Hardware/NanoVNA_V2.py b/NanoVNASaver/Hardware/NanoVNA_V2.py
index c2c52619..d45a04cc 100644
--- a/NanoVNASaver/Hardware/NanoVNA_V2.py
+++ b/NanoVNASaver/Hardware/NanoVNA_V2.py
@@ -57,6 +57,15 @@
WRITE_SLEEP = 0.05
+_ADF4350_TXPOWER_DESC_MAP = {
+ 0: '9dB attenuation',
+ 1: '6dB attenuation',
+ 2: '3dB attenuation',
+ 3: 'Maximum',
+}
+_ADF4350_TXPOWER_DESC_REV_MAP = {
+ value: key for key, value in _ADF4350_TXPOWER_DESC_MAP.items()}
+
class NanoVNA_V2(VNA):
name = "NanoVNA-V2"
valid_datapoints = (101, 11, 51, 201, 301, 501, 1023)
@@ -94,9 +103,21 @@ def read_features(self):
self.features.add("Customizable data points")
# TODO: more than one dp per freq
self.features.add("Multi data points")
+ self.board_revision = self.read_board_revision()
+ if self.board_revision >= Version("2.0.4"):
+ self.sweep_max_freq_Hz = 4400e6
+ else:
+ self.sweep_max_freq_Hz = 3000e6
if self.version <= Version("1.0.1"):
logger.debug("Hack for s21 oddity in first sweeppoint")
self.features.add("S21 hack")
+ if self.version >= Version("1.0.2"):
+ self.features.update({"Set TX power partial", "Set Average"})
+ # Can only set ADF4350 power, i.e. for >= 140MHz
+ self.txPowerRanges = [
+ ((140e6, self.sweep_max_freq_Hz),
+ [_ADF4350_TXPOWER_DESC_MAP[value] for value in (3, 2, 1, 0)]),
+ ]
def readFirmware(self) -> str:
result = f"HW: {self.read_board_revision()}\nFW: {self.version}"
@@ -210,7 +231,9 @@ def read_board_revision(self) -> 'Version':
if len(resp) != 2:
logger.error("Timeout reading version registers")
return None
- return Version(f"{resp[0]}.0.{resp[1]}")
+ result = Version(f"{resp[0]}.0.{resp[1]}")
+ logger.debug("read_board_revision: %s", result)
+ return result
def setSweep(self, start, stop):
@@ -238,3 +261,21 @@ def _updateSweep(self):
with self.serial.lock:
self.serial.write(cmd)
sleep(WRITE_SLEEP)
+
+ def setTXPower(self, freq_range, power_desc):
+ if freq_range[0] != 140e6:
+ raise ValueError('Invalid TX power frequency range')
+ # 140MHz..max => ADF4350
+ self._set_register(0x42, _ADF4350_TXPOWER_DESC_REV_MAP[power_desc], 1)
+
+ def _set_register(self, addr, value, size):
+ if size == 1:
+ packet = pack(" List[int]:
def resetSweep(self, start: int, stop: int):
pass
+ def _get_running_frequencies(self):
+ '''
+ If possible, read frequencies already runnung
+ if not return default values
+ Overwrite in specific HW
+ '''
+ return 27000000, 30000000
+
def connected(self) -> bool:
return self.serial.is_open
@@ -187,3 +199,6 @@ def readVersion(self) -> 'Version':
def setSweep(self, start, stop):
list(self.exec_command(f"sweep {start} {stop} {self.datapoints}"))
+
+ def setTXPower(self, freq_range, power_desc):
+ raise NotImplementedError()
diff --git a/NanoVNASaver/Marker/Values.py b/NanoVNASaver/Marker/Values.py
index 0cf4e70c..e1b4193b 100644
--- a/NanoVNASaver/Marker/Values.py
+++ b/NanoVNASaver/Marker/Values.py
@@ -54,6 +54,10 @@ class Label(NamedTuple):
Label("s21phase", "S21 Phase", "S21 Phase", True),
Label("s21polar", "S21 Polar", "S21 Polar", False),
Label("s21groupdelay", "S21 Group Delay", "S21 Group Delay", False),
+ Label("s21magshunt", "S21 |Z| shunt", "S21 Z Magnitude shunt", False),
+ Label("s21magseries", "S21 |Z| series", "S21 Z Magnitude series", False),
+ Label("s21realimagshunt", "S21 R+jX shunt", "S21 Z Real+Imag shunt", False),
+ Label("s21realimagseries", "S21 R+jX series", "S21 Z Real+Imag series", False),
)
diff --git a/NanoVNASaver/Marker/Widget.py b/NanoVNASaver/Marker/Widget.py
index 8cac4952..5708f426 100644
--- a/NanoVNASaver/Marker/Widget.py
+++ b/NanoVNASaver/Marker/Widget.py
@@ -351,3 +351,7 @@ def updateLabels(self,
self.label['s21phase'].setText(format_phase(s21.phase))
self.label['s21polar'].setText(
str(round(abs(s21.z), 2)) + "∠" + format_phase(s21.phase))
+ self.label['s21magshunt'].setText(format_magnitude(abs(s21.shuntImpedance())))
+ self.label['s21magseries'].setText(format_magnitude(abs(s21.seriesImpedance())))
+ self.label['s21realimagshunt'].setText(format_complex_imp(s21.shuntImpedance(), allow_negative=True))
+ self.label['s21realimagseries'].setText(format_complex_imp(s21.seriesImpedance(), allow_negative=True))
diff --git a/NanoVNASaver/NanoVNASaver.py b/NanoVNASaver/NanoVNASaver.py
index b60e86a8..6a94c1b1 100644
--- a/NanoVNASaver/NanoVNASaver.py
+++ b/NanoVNASaver/NanoVNASaver.py
@@ -40,9 +40,9 @@
CapacitanceChart,
CombinedLogMagChart, GroupDelayChart, InductanceChart,
LogMagChart, PhaseChart,
- MagnitudeChart, MagnitudeZChart,
+ MagnitudeChart, MagnitudeZChart, MagnitudeZShuntChart, MagnitudeZSeriesChart,
QualityFactorChart, VSWRChart, PermeabilityChart, PolarChart,
- RealImaginaryChart,
+ RealImaginaryChart, RealImaginaryShuntChart, RealImaginarySeriesChart,
SmithChart, SParameterChart, TDRChart,
)
from .Calibration import Calibration
@@ -109,7 +109,6 @@ def __init__(self):
self.calibration = Calibration()
-
logger.debug("Building user interface")
self.baseTitle = f"NanoVNA Saver {NanoVNASaver.version}"
@@ -155,6 +154,10 @@ def __init__(self):
reflective=False)),
("log_mag", LogMagChart("S21 Gain")),
("magnitude", MagnitudeChart("|S21|")),
+ ("magnitude_z_shunt", MagnitudeZShuntChart("S21 |Z| shunt")),
+ ("magnitude_z_series", MagnitudeZSeriesChart("S21 |Z| series")),
+ ("real_imag_shunt", RealImaginaryShuntChart("S21 R+jX shunt")),
+ ("real_imag_series", RealImaginarySeriesChart("S21 R+jX series")),
("phase", PhaseChart("S21 Phase")),
("polar", PolarChart("S21 Polar Plot")),
("s_parameter", SParameterChart("S21 Real/Imaginary")),
@@ -196,7 +199,8 @@ def __init__(self):
left_column = QtWidgets.QVBoxLayout()
right_column = QtWidgets.QVBoxLayout()
right_column.addLayout(self.charts_layout)
- self.marker_frame.setHidden(not self.settings.value("MarkersVisible", True, bool))
+ self.marker_frame.setHidden(
+ not self.settings.value("MarkersVisible", True, bool))
chart_widget = QtWidgets.QWidget()
chart_widget.setLayout(right_column)
self.splitter = QtWidgets.QSplitter()
@@ -317,9 +321,11 @@ def __init__(self):
tdr_control_box.setMaximumWidth(250)
self.tdr_result_label = QtWidgets.QLabel()
- tdr_control_layout.addRow("Estimated cable length:", self.tdr_result_label)
+ tdr_control_layout.addRow(
+ "Estimated cable length:", self.tdr_result_label)
- self.tdr_button = QtWidgets.QPushButton("Time Domain Reflectometry ...")
+ self.tdr_button = QtWidgets.QPushButton(
+ "Time Domain Reflectometry ...")
self.tdr_button.clicked.connect(lambda: self.display_window("tdr"))
tdr_control_layout.addRow(self.tdr_button)
@@ -525,7 +531,7 @@ def connect_device(self):
logger.info("Connection %s", self.interface)
try:
self.interface.open()
- self.interface.timeout = 0.05
+
except (IOError, AttributeError) as exc:
logger.error("Tried to open %s and failed: %s",
self.interface, exc)
@@ -533,13 +539,15 @@ def connect_device(self):
if not self.interface.isOpen():
logger.error("Unable to open port %s", self.interface)
return
+ self.interface.timeout = 0.05
sleep(0.1)
try:
self.vna = get_VNA(self.interface)
except IOError as exc:
logger.error("Unable to connect to VNA: %s", exc)
- self.vna.validateInput = self.settings.value("SerialInputValidation", True, bool)
+ self.vna.validateInput = self.settings.value(
+ "SerialInputValidation", True, bool)
# connected
self.btnSerialToggle.setText("Disconnect")
@@ -563,6 +571,8 @@ def connect_device(self):
self.sweep_control.update_center_span()
self.sweep_control.update_step_size()
+ self.windows["sweep_settings"].vna_connected()
+
logger.debug("Starting initial sweep")
self.sweep_start()
@@ -661,7 +671,7 @@ def dataUpdated(self):
if s21data:
min_gain = min(s21data, key=lambda data: data.gain)
- max_gain = min(s21data, key=lambda data: data.gain)
+ max_gain = max(s21data, key=lambda data: data.gain)
self.s21_min_gain_label.setText(
f"{format_gain(min_gain.gain)}"
f" @ {format_frequency(min_gain.freq)}")
@@ -832,3 +842,6 @@ def changeFont(self, font: QtGui.QFont) -> None:
def update_sweep_title(self):
for c in self.subscribing_charts:
c.setSweepTitle(self.sweep.properties.name)
+
+ def set_tx_power(self, freq_range, power_desc):
+ self.vna.setTXPower(freq_range, power_desc)
diff --git a/NanoVNASaver/RFTools.py b/NanoVNASaver/RFTools.py
index 3803bea0..d52429eb 100644
--- a/NanoVNASaver/RFTools.py
+++ b/NanoVNASaver/RFTools.py
@@ -39,6 +39,7 @@ class Datapoint(NamedTuple):
@property
def z(self) -> complex:
""" return the datapoint impedance as complex number """
+ # FIXME: not impedance, but s11 ?
return complex(self.re, self.im)
@property
@@ -67,6 +68,18 @@ def wavelength(self) -> float:
def impedance(self, ref_impedance: float = 50) -> complex:
return gamma_to_impedance(self.z, ref_impedance)
+ def shuntImpedance(self, ref_impedance: float = 50) -> complex:
+ try:
+ return 0.5 * ref_impedance * self.z / (1 - self.z)
+ except ZeroDivisionError:
+ return math.inf
+
+ def seriesImpedance(self, ref_impedance: float = 50) -> complex:
+ try:
+ return 2 * ref_impedance * (1 - self.z) / self.z
+ except ZeroDivisionError:
+ return math.inf
+
def qFactor(self, ref_impedance: float = 50) -> float:
imp = self.impedance(ref_impedance)
if imp.real == 0.0:
@@ -158,7 +171,7 @@ def corr_att_data(data: List[Datapoint], att: float) -> List[Datapoint]:
if att <= 0:
return data
else:
- att = 10**(att/20)
+ att = 10**(att / 20)
ndata = []
for dp in data:
corrected = dp.z * att
diff --git a/NanoVNASaver/Windows/AnalysisWindow.py b/NanoVNASaver/Windows/AnalysisWindow.py
index bac9206e..04b69950 100644
--- a/NanoVNASaver/Windows/AnalysisWindow.py
+++ b/NanoVNASaver/Windows/AnalysisWindow.py
@@ -23,6 +23,9 @@
from NanoVNASaver.Analysis import Analysis, LowPassAnalysis, HighPassAnalysis, \
BandPassAnalysis, BandStopAnalysis, VSWRAnalysis, \
SimplePeakSearchAnalysis, MagLoopAnalysis
+from NanoVNASaver.Analysis.VSWRAnalysis import ResonanceAnalysis
+from NanoVNASaver.Analysis.VSWRAnalysis import EFHWAnalysis
+from NanoVNASaver.Analysis import PeakSearchAnalysis
logger = logging.getLogger(__name__)
@@ -46,14 +49,25 @@ def __init__(self, app: QtWidgets.QWidget):
select_analysis_box = QtWidgets.QGroupBox("Select analysis")
select_analysis_layout = QtWidgets.QFormLayout(select_analysis_box)
self.analysis_list = QtWidgets.QComboBox()
- self.analysis_list.addItem("Low-pass filter", LowPassAnalysis(self.app))
- self.analysis_list.addItem("Band-pass filter", BandPassAnalysis(self.app))
- self.analysis_list.addItem("High-pass filter", HighPassAnalysis(self.app))
- self.analysis_list.addItem("Band-stop filter", BandStopAnalysis(self.app))
- # self.analysis_list.addItem("Peak search", PeakSearchAnalysis(self.app))
- self.analysis_list.addItem("Peak search", SimplePeakSearchAnalysis(self.app))
+ self.analysis_list.addItem(
+ "Low-pass filter", LowPassAnalysis(self.app))
+ self.analysis_list.addItem(
+ "Band-pass filter", BandPassAnalysis(self.app))
+ self.analysis_list.addItem(
+ "High-pass filter", HighPassAnalysis(self.app))
+ self.analysis_list.addItem(
+ "Band-stop filter", BandStopAnalysis(self.app))
+ self.analysis_list.addItem(
+ "Simple Peak search", SimplePeakSearchAnalysis(self.app))
+ self.analysis_list.addItem(
+ "Peak search", PeakSearchAnalysis(self.app))
self.analysis_list.addItem("VSWR analysis", VSWRAnalysis(self.app))
- self.analysis_list.addItem("MagLoop analysis", MagLoopAnalysis(self.app))
+ self.analysis_list.addItem(
+ "Resonance analysis", ResonanceAnalysis(self.app))
+ self.analysis_list.addItem(
+ "HWEF analysis", EFHWAnalysis(self.app))
+ self.analysis_list.addItem(
+ "MagLoop analysis", MagLoopAnalysis(self.app))
select_analysis_layout.addRow("Analysis type", self.analysis_list)
self.analysis_list.currentIndexChanged.connect(self.updateSelection)
@@ -61,8 +75,10 @@ def __init__(self, app: QtWidgets.QWidget):
btn_run_analysis.clicked.connect(self.runAnalysis)
select_analysis_layout.addRow(btn_run_analysis)
- self.checkbox_run_automatically = QtWidgets.QCheckBox("Run automatically")
- self.checkbox_run_automatically.stateChanged.connect(self.toggleAutomaticRun)
+ self.checkbox_run_automatically = QtWidgets.QCheckBox(
+ "Run automatically")
+ self.checkbox_run_automatically.stateChanged.connect(
+ self.toggleAutomaticRun)
select_analysis_layout.addRow(self.checkbox_run_automatically)
analysis_box = QtWidgets.QGroupBox("Analysis")
@@ -87,7 +103,8 @@ def updateSelection(self):
old_item = self.analysis_layout.itemAt(0)
if old_item is not None:
old_widget = self.analysis_layout.itemAt(0).widget()
- self.analysis_layout.replaceWidget(old_widget, self.analysis.widget())
+ self.analysis_layout.replaceWidget(
+ old_widget, self.analysis.widget())
old_widget.hide()
else:
self.analysis_layout.addWidget(self.analysis.widget())
diff --git a/NanoVNASaver/Windows/SweepSettings.py b/NanoVNASaver/Windows/SweepSettings.py
index 212b4f85..710d519c 100644
--- a/NanoVNASaver/Windows/SweepSettings.py
+++ b/NanoVNASaver/Windows/SweepSettings.py
@@ -44,6 +44,10 @@ def __init__(self, app: QtWidgets.QWidget):
layout.addWidget(self.title_box())
layout.addWidget(self.settings_box())
+ # We can only populate this box after the VNA has been connected.
+ self._power_box = QtWidgets.QGroupBox("Power")
+ self._power_layout = QtWidgets.QFormLayout(self._power_box)
+ layout.addWidget(self._power_box)
layout.addWidget(self.sweep_box())
self.update_band()
@@ -155,6 +159,17 @@ def sweep_box(self) -> 'QtWidgets.QWidget':
layout.addRow(btn_set_band_sweep)
return box
+ def vna_connected(self):
+ while self._power_layout.rowCount():
+ self._power_layout.removeRow(0)
+ for freq_range, power_descs in self.app.vna.txPowerRanges:
+ power_sel = QtWidgets.QComboBox()
+ power_sel.addItems(power_descs)
+ power_sel.currentTextChanged.connect(
+ partial(self.update_tx_power, freq_range))
+ self._power_layout.addRow("TX power {}..{}".format(
+ *map(format_frequency_short, freq_range)), power_sel)
+
def update_band(self, apply: bool = False):
logger.debug("update_band(%s)", apply)
index_start = self.band_list.model().index(self.band_list.currentIndex(), 1)
@@ -233,3 +248,8 @@ def update_title(self, title: str = ""):
with self.app.sweep.lock:
self.app.sweep.properties.name = title
self.app.update_sweep_title()
+
+ def update_tx_power(self, freq_range, power_desc):
+ logger.debug("update_tx_power(%r)", power_desc)
+ with self.app.sweep.lock:
+ self.app.set_tx_power(freq_range, power_desc)
diff --git a/README.md b/README.md
index 3523c683..335f90a5 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,8 @@ points, and generally display and analyze the resulting data.
Latest Changes
--------------
+### Changes in v0.3.9
+
### Changes in v0.3.8
- Allow editing of bands above 2.4GHz
@@ -24,22 +26,6 @@ Latest Changes
- Support for Nanovna-F V2
- Fixes a crash with S21 hack
-### Changes in v0.3.7
-
-- Added a delta marker
-- Segments can now have exponential different step widths
- (see logarithmic sweeping)
-- More different data points selectable
- (shorter are useful on logarithmic sweeping)
-- Scrollable marker column
-- Markers initialize on start, middle, end
-- Frequency input is now more "lazy"
- 10m, 50K and 1g are now valid for 10MHz, 50kHz and 1GHz
-- Added a wavelength field to Markers
-- 32 bit windows binaries build in actions
-- Stability improvements due to better exception handling
-- Workaround for wrong first S21mag value on V2 devices
-
Introduction
------------