Merge branch 'feature/release_0.5.5' of github.com:NanoVNA-Saver/nanovna-saver into feature/release_0.5.5

pull/602/head
Holger Müller 2023-02-19 08:55:26 +01:00
commit 09d8b2b866
10 zmienionych plików z 306 dodań i 63 usunięć

Wyświetl plik

@ -72,33 +72,7 @@ class RealImaginaryChart(FrequencyChart):
mode_group.addAction(self.y_action_fixed_span)
self.y_menu.addAction(self.y_action_automatic)
self.y_menu.addAction(self.y_action_fixed_span)
self.y_menu.addSeparator()
self.action_set_fixed_maximum_real = QtWidgets.QAction(
f"Maximum R ({self.maxDisplayReal})")
self.action_set_fixed_maximum_real.triggered.connect(
self.setMaximumRealValue)
self.action_set_fixed_minimum_real = QtWidgets.QAction(
f"Minimum R ({self.minDisplayReal})")
self.action_set_fixed_minimum_real.triggered.connect(
self.setMinimumRealValue)
self.action_set_fixed_maximum_imag = QtWidgets.QAction(
f"Maximum jX ({self.maxDisplayImag})")
self.action_set_fixed_maximum_imag.triggered.connect(
self.setMaximumImagValue)
self.action_set_fixed_minimum_imag = QtWidgets.QAction(
f"Minimum jX ({self.minDisplayImag})")
self.action_set_fixed_minimum_imag.triggered.connect(
self.setMinimumImagValue)
self.y_menu.addAction(self.action_set_fixed_maximum_real)
self.y_menu.addAction(self.action_set_fixed_minimum_real)
self.y_menu.addSeparator()
self.y_menu.addAction(self.action_set_fixed_maximum_imag)
self.y_menu.addAction(self.action_set_fixed_minimum_imag)
def copy(self):
new_chart: RealImaginaryChart = super().copy()
@ -109,23 +83,6 @@ class RealImaginaryChart(FrequencyChart):
new_chart.minDisplayImag = self.minDisplayImag
return new_chart
def drawChart(self, qp: QtGui.QPainter):
qp.setPen(QtGui.QPen(Chart.color.text))
qp.drawText(self.leftMargin + 5, 15,
f"{self.name} (\N{OHM SIGN})")
qp.drawText(10, 15, "R")
qp.drawText(self.leftMargin + self.dim.width + 10, 15, "X")
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(self.leftMargin,
self.topMargin - 5,
self.leftMargin,
self.topMargin + self.dim.height + 5)
qp.drawLine(self.leftMargin - 5,
self.topMargin + self.dim.height,
self.leftMargin + self.dim.width + 5,
self.topMargin + self.dim.height)
self.drawTitle(qp)
def drawValues(self, qp: QtGui.QPainter):
if not self.data and not self.reference:
return
@ -333,7 +290,7 @@ class RealImaginaryChart(FrequencyChart):
max_real = 0
max_imag = -1000
for d in self.data:
imp = self.impedance(d)
imp = self.value(d)
re, im = imp.real, imp.imag
if math.isinf(re): # Avoid infinite scales
continue
@ -345,7 +302,7 @@ class RealImaginaryChart(FrequencyChart):
for d in self.reference:
if d.freq < self.fstart or d.freq > self.fstop:
continue
imp = self.impedance(d)
imp = self.value(d)
re, im = imp.real, imp.imag
if math.isinf(re): # Avoid infinite scales
continue
@ -393,12 +350,12 @@ class RealImaginaryChart(FrequencyChart):
return min_imag, max_imag
def getImYPosition(self, d: Datapoint) -> int:
im = self.impedance(d).imag
im = self.value(d).imag
return int(self.topMargin + (self.max_imag - im) / self.span_imag
* self.dim.height)
def getReYPosition(self, d: Datapoint) -> int:
re = self.impedance(d).real
re = self.value(d).real
return int(self.topMargin + (self.max_real - re) / self.span_real
* self.dim.height if math.isfinite(re) else self.topMargin)
@ -521,5 +478,5 @@ class RealImaginaryChart(FrequencyChart):
f"Maximum jX ({self.maxDisplayImag})")
self.menu.exec_(event.globalPos())
def impedance(self, p: Datapoint) -> complex:
return p.impedance()
def value(self, p: Datapoint) -> complex:
raise NotImplementedError()

Wyświetl plik

@ -0,0 +1,178 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 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 <https://www.gnu.org/licenses/>.
import math
import numpy as np
import logging
from scipy.constants import mu_0
from PyQt5 import QtWidgets, QtGui
from NanoVNASaver.Formatting import format_frequency_chart
from NanoVNASaver.RFTools import Datapoint
from NanoVNASaver.Charts.Chart import Chart
from .RI import RealImaginaryChart
logger = logging.getLogger(__name__)
MU = "\N{GREEK SMALL LETTER MU}"
class RealImaginaryMuChart(RealImaginaryChart):
def __init__(self, name=""):
super().__init__(name)
self.y_menu.addSeparator()
self.action_set_fixed_maximum_real = QtWidgets.QAction(
f"Maximum {MU}' ({self.maxDisplayReal})")
self.action_set_fixed_maximum_real.triggered.connect(
self.setMaximumRealValue)
self.action_set_fixed_minimum_real = QtWidgets.QAction(
f"Minimum {MU}' ({self.minDisplayReal})")
self.action_set_fixed_minimum_real.triggered.connect(
self.setMinimumRealValue)
self.action_set_fixed_maximum_imag = QtWidgets.QAction(
f"Maximum {MU}'' ({self.maxDisplayImag})")
self.action_set_fixed_maximum_imag.triggered.connect(
self.setMaximumImagValue)
self.action_set_fixed_minimum_imag = QtWidgets.QAction(
f"Minimum {MU}'' ({self.minDisplayImag})")
self.action_set_fixed_minimum_imag.triggered.connect(
self.setMinimumImagValue)
self.y_menu.addAction(self.action_set_fixed_maximum_real)
self.y_menu.addAction(self.action_set_fixed_minimum_real)
self.y_menu.addSeparator()
self.y_menu.addAction(self.action_set_fixed_maximum_imag)
self.y_menu.addAction(self.action_set_fixed_minimum_imag)
# Manage core parameters
# TODO pick some sane default values?
self.coreLength = 1.
self.coreArea = 1.
self.coreWindings = 1
self.menu.addSeparator()
self.action_set_core_length = QtWidgets.QAction(
"Core effective length")
self.action_set_core_length.triggered.connect(
self.setCoreLength)
self.action_set_core_area = QtWidgets.QAction(
"Core area")
self.action_set_core_area.triggered.connect(
self.setCoreArea)
self.action_set_core_windings = QtWidgets.QAction(
"Core number of windings")
self.action_set_core_windings.triggered.connect(
self.setCoreWindings)
self.menu.addAction(self.action_set_core_length)
self.menu.addAction(self.action_set_core_area)
self.menu.addAction(self.action_set_core_windings)
def copy(self):
new_chart: RealImaginaryMuChart = super().copy()
new_chart.coreLength = self.coreLength
new_chart.coreArea = self.coreArea
new_chart.coreWindings = self.coreWindings
return new_chart
def drawChart(self, qp: QtGui.QPainter):
qp.setPen(QtGui.QPen(Chart.color.text))
qp.drawText(self.leftMargin + 5, 15,
f"{self.name}")
qp.drawText(5, 15, f"{MU}'")
qp.drawText(self.leftMargin + self.dim.width + 10, 15, f"{MU}''")
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(self.leftMargin,
self.topMargin - 5,
self.leftMargin,
self.topMargin + self.dim.height + 5)
qp.drawLine(self.leftMargin-5,
self.topMargin + self.dim.height,
self.leftMargin + self.dim.width + 5,
self.topMargin + self.dim.height)
self.drawTitle(qp)
def contextMenuEvent(self, event):
self.action_set_fixed_start.setText(
f"Start ({format_frequency_chart(self.minFrequency)})")
self.action_set_fixed_stop.setText(
f"Stop ({format_frequency_chart(self.maxFrequency)})")
self.action_set_fixed_minimum_real.setText(
f"Minimum {MU}' ({self.minDisplayReal})")
self.action_set_fixed_maximum_real.setText(
f"Maximum {MU}' ({self.maxDisplayReal})")
self.action_set_fixed_minimum_imag.setText(
f"Minimum {MU}'' ({self.minDisplayImag})")
self.action_set_fixed_maximum_imag.setText(
f"Maximum {MU}'' ({self.maxDisplayImag})")
self.menu.exec_(event.globalPos())
def setCoreLength(self):
val, selected = QtWidgets.QInputDialog.getDouble(
self, "Core effective length",
"Set core effective length in mm", value=self.coreLength,
decimals=2)
if not selected:
return
if not (self.fixedValues and val >= 0):
self.coreLength = val
if self.fixedValues:
self.update()
def setCoreArea(self):
val, selected = QtWidgets.QInputDialog.getDouble(
self, "Core effective area",
"Set core cross section area length in mm\N{SUPERSCRIPT TWO}", value=self.coreArea,
decimals=2)
if not selected:
return
if not (self.fixedValues and val >= 0):
self.coreArea = val
if self.fixedValues:
self.update()
def setCoreWindings(self):
val, selected = QtWidgets.QInputDialog.getInt(
self, "Core number of windings",
"Set core number of windings", value=self.coreWindings)
if not selected:
return
if not (self.fixedValues and val >= 0):
self.coreWindings = val
if self.fixedValues:
self.update()
def value(self, p: Datapoint) -> complex:
return self.mu_r(p)
def mu_r(self, p: Datapoint) -> complex:
inductance = p.impedance()/(2j*math.pi*p.freq)
# Core length and core area are in mm and mm2 respectively
# note: mu_r = mu' - j * mu ''
return np.conj(inductance * (self.coreLength/1e3) / (mu_0 * self.coreWindings**2 * (self.coreArea/1e6)))

Wyświetl plik

@ -0,0 +1,98 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 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 <https://www.gnu.org/licenses/>.
import logging
from PyQt5 import QtWidgets, QtGui
from NanoVNASaver.Formatting import format_frequency_chart
from NanoVNASaver.RFTools import Datapoint
from NanoVNASaver.Charts.Chart import Chart
from .RI import RealImaginaryChart
logger = logging.getLogger(__name__)
class RealImaginaryZChart(RealImaginaryChart):
def __init__(self, name=""):
super().__init__(name)
self.y_menu.addSeparator()
self.action_set_fixed_maximum_real = QtWidgets.QAction(
f"Maximum R ({self.maxDisplayReal})")
self.action_set_fixed_maximum_real.triggered.connect(
self.setMaximumRealValue)
self.action_set_fixed_minimum_real = QtWidgets.QAction(
f"Minimum R ({self.minDisplayReal})")
self.action_set_fixed_minimum_real.triggered.connect(
self.setMinimumRealValue)
self.action_set_fixed_maximum_imag = QtWidgets.QAction(
f"Maximum jX ({self.maxDisplayImag})")
self.action_set_fixed_maximum_imag.triggered.connect(
self.setMaximumImagValue)
self.action_set_fixed_minimum_imag = QtWidgets.QAction(
f"Minimum jX ({self.minDisplayImag})")
self.action_set_fixed_minimum_imag.triggered.connect(
self.setMinimumImagValue)
self.y_menu.addAction(self.action_set_fixed_maximum_real)
self.y_menu.addAction(self.action_set_fixed_minimum_real)
self.y_menu.addSeparator()
self.y_menu.addAction(self.action_set_fixed_maximum_imag)
self.y_menu.addAction(self.action_set_fixed_minimum_imag)
def drawChart(self, qp: QtGui.QPainter):
qp.setPen(QtGui.QPen(Chart.color.text))
qp.drawText(self.leftMargin + 5, 15,
f"{self.name} (\N{OHM SIGN})")
qp.drawText(10, 15, "R")
qp.drawText(self.leftMargin + self.dim.width + 10, 15, "X")
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(self.leftMargin,
self.topMargin - 5,
self.leftMargin,
self.topMargin + self.dim.height + 5)
qp.drawLine(self.leftMargin-5,
self.topMargin + self.dim.height,
self.leftMargin + self.dim.width + 5,
self.topMargin + self.dim.height)
self.drawTitle(qp)
def contextMenuEvent(self, event):
self.action_set_fixed_start.setText(
f"Start ({format_frequency_chart(self.minFrequency)})")
self.action_set_fixed_stop.setText(
f"Stop ({format_frequency_chart(self.maxFrequency)})")
self.action_set_fixed_minimum_real.setText(
f"Minimum R ({self.minDisplayReal})")
self.action_set_fixed_maximum_real.setText(
f"Maximum R ({self.maxDisplayReal})")
self.action_set_fixed_minimum_imag.setText(
f"Minimum jX ({self.minDisplayImag})")
self.action_set_fixed_maximum_imag.setText(
f"Maximum jX ({self.maxDisplayImag})")
self.menu.exec_(event.globalPos())
def value(self, p: Datapoint) -> complex:
return self.impedance(p)
def impedance(self, p: Datapoint) -> complex:
return p.impedance()

Wyświetl plik

@ -19,12 +19,12 @@
import logging
from NanoVNASaver.RFTools import Datapoint
from .RI import RealImaginaryChart
from .RIZ import RealImaginaryZChart
logger = logging.getLogger(__name__)
class RealImaginarySeriesChart(RealImaginaryChart):
class RealImaginaryZSeriesChart(RealImaginaryZChart):
def impedance(self, p: Datapoint) -> complex:
return p.seriesImpedance()

Wyświetl plik

@ -19,12 +19,12 @@
import logging
from NanoVNASaver.RFTools import Datapoint
from .RI import RealImaginaryChart
from .RIZ import RealImaginaryZChart
logger = logging.getLogger(__name__)
class RealImaginaryShuntChart(RealImaginaryChart):
class RealImaginaryZShuntChart(RealImaginaryZChart):
def impedance(self, p: Datapoint) -> complex:
return p.shuntImpedance()

Wyświetl plik

@ -258,7 +258,7 @@ class TDRChart(Chart):
return
a0.accept()
width = self.width() - self.leftMargin - self.rightMargin
if self.tdrWindow.td.size:
if self.tdrWindow.td:
if self.fixedSpan:
max_index = np.searchsorted(
self.tdrWindow.distance_axis, self.maxDisplayLength * 2)
@ -436,7 +436,7 @@ class TDRChart(Chart):
qp.end()
def valueAtPosition(self, y):
if self.tdrWindow.td.size:
if self.tdrWindow.td:
height = self.height() - self.topMargin - self.bottomMargin
absy = (self.height() - y) - self.bottomMargin
if self.fixedValues:
@ -454,7 +454,7 @@ class TDRChart(Chart):
return 0
def lengthAtPosition(self, x, limit=True):
if not self.tdrWindow.td.size:
if not self.tdrWindow.td:
return 0
width = self.width() - self.leftMargin - self.rightMargin
absx = x - self.leftMargin

Wyświetl plik

@ -15,8 +15,10 @@ 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 .RIMu import RealImaginaryMuChart
from .RIZ import RealImaginaryZChart
from .RIZShunt import RealImaginaryZShuntChart
from .RIZSeries import RealImaginaryZSeriesChart
from .Smith import SmithChart
from .SParam import SParameterChart
from .TDR import TDRChart

Wyświetl plik

@ -45,7 +45,9 @@ from .Charts import (
MagnitudeChart, MagnitudeZChart, MagnitudeZShuntChart,
MagnitudeZSeriesChart,
QualityFactorChart, VSWRChart, PermeabilityChart, PolarChart,
RealImaginaryChart, RealImaginaryShuntChart, RealImaginarySeriesChart,
RealImaginaryChart,
RealImaginaryMuChart,
RealImaginaryZChart, RealImaginaryZShuntChart, RealImaginaryZSeriesChart,
SmithChart, SParameterChart, TDRChart,
)
from .Calibration import Calibration
@ -151,7 +153,8 @@ class NanoVNASaver(QtWidgets.QWidget):
" X/\N{GREEK SMALL LETTER OMEGA}"),
"phase": PhaseChart("S11 Phase"),
"q_factor": QualityFactorChart("S11 Quality Factor"),
"real_imag": RealImaginaryChart("S11 R+jX"),
"real_imag": RealImaginaryZChart("S11 R+jX"),
"real_imag_mu": RealImaginaryMuChart("S11 \N{GREEK SMALL LETTER MU}"),
"smith": SmithChart("S11 Smith Chart"),
"s_parameter": SParameterChart("S11 Real/Imaginary"),
"vswr": VSWRChart("S11 VSWR"),
@ -163,8 +166,8 @@ class NanoVNASaver(QtWidgets.QWidget):
"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(
"real_imag_shunt": RealImaginaryZShuntChart("S21 R+jX shunt"),
"real_imag_series": RealImaginaryZSeriesChart(
"S21 R+jX series"),
"phase": PhaseChart("S21 Phase"),
"polar": PolarChart("S21 Polar Plot"),

Wyświetl plik

@ -134,7 +134,7 @@ class SweepWorker(QtCore.QRunnable):
start, stop, averages)
self.percentage = (i + 1) * 100 / sweep.segments
self.updateData(freq, values11, values21, i)
if sweep.properties.mode != SweepMode.CONTINOUS:
if sweep.properties.mode != SweepMode.CONTINOUS or self.stopped:
break
def init_data(self):

Wyświetl plik

@ -31,6 +31,7 @@ It's written in __Python 3__ using __PyQt5__ and __scipy__.
- [Usage](#using-the-software)
- [Calibration](#calibration)
- [TDR](#tdr)
- [Measuring inductor core permeability](#measuring-inductor-core-permeability)
- [Latest Changes](#latest-changes)
- [Contributing](#contributing)
- [Contribution Guidlines](docs/CONTRIBUTING.md)
@ -173,6 +174,10 @@ calibration load would be attached. Open the "Time Domain Reflectometry"
window, and select the correct cable type, or manually enter a propagation
factor.
### Measuring inductor core permeability
The permeability (mu) of cores can be measured using a one-port measurement. Put one or more windings on a core of known dimensions and use the "S11 mu" plot from the "Display Setup". The core dimensions (cross section area in mm2, effective length in mm) and number of windings can be set in the context menu for the plot (right click on the plot).
Latest Changes
--------------