kopia lustrzana https://github.com/NanoVNA-Saver/nanovna-saver
Merge branch 'upstream_Development'
commit
ef185ae219
|
@ -62,6 +62,8 @@ class Chart(QtWidgets.QWidget):
|
||||||
draggedBox = False
|
draggedBox = False
|
||||||
draggedBoxStart = (0, 0)
|
draggedBoxStart = (0, 0)
|
||||||
draggedBoxCurrent = (-1, -1)
|
draggedBoxCurrent = (-1, -1)
|
||||||
|
moveStartX = -1
|
||||||
|
moveStartY = -1
|
||||||
|
|
||||||
isPopout = False
|
isPopout = False
|
||||||
popoutRequested = pyqtSignal(object)
|
popoutRequested = pyqtSignal(object)
|
||||||
|
@ -204,12 +206,18 @@ class Chart(QtWidgets.QWidget):
|
||||||
if event.buttons() == QtCore.Qt.RightButton:
|
if event.buttons() == QtCore.Qt.RightButton:
|
||||||
event.ignore()
|
event.ignore()
|
||||||
return
|
return
|
||||||
|
elif event.buttons() == QtCore.Qt.MiddleButton:
|
||||||
|
# Drag event
|
||||||
|
event.accept()
|
||||||
|
self.moveStartX = event.x()
|
||||||
|
self.moveStartY = event.y()
|
||||||
|
return
|
||||||
if event.modifiers() == QtCore.Qt.ShiftModifier:
|
if event.modifiers() == QtCore.Qt.ShiftModifier:
|
||||||
self.draggedMarker = self.getNearestMarker(event.x(), event.y())
|
self.draggedMarker = self.getNearestMarker(event.x(), event.y())
|
||||||
elif event.modifiers() == QtCore.Qt.ControlModifier:
|
elif event.modifiers() == QtCore.Qt.ControlModifier:
|
||||||
|
event.accept()
|
||||||
self.draggedBox = True
|
self.draggedBox = True
|
||||||
self.draggedBoxStart = (event.x(), event.y())
|
self.draggedBoxStart = (event.x(), event.y())
|
||||||
event.accept()
|
|
||||||
return
|
return
|
||||||
self.mouseMoveEvent(event)
|
self.mouseMoveEvent(event)
|
||||||
|
|
||||||
|
@ -593,6 +601,18 @@ class FrequencyChart(Chart):
|
||||||
if a0.buttons() == QtCore.Qt.RightButton:
|
if a0.buttons() == QtCore.Qt.RightButton:
|
||||||
a0.ignore()
|
a0.ignore()
|
||||||
return
|
return
|
||||||
|
if a0.buttons() == QtCore.Qt.MiddleButton:
|
||||||
|
# Drag the display
|
||||||
|
a0.accept()
|
||||||
|
if self.moveStartX != -1 and self.moveStartY != -1:
|
||||||
|
dx = self.moveStartX - a0.x()
|
||||||
|
dy = self.moveStartY - a0.y()
|
||||||
|
self.zoomTo(self.leftMargin + dx, self.topMargin + dy,
|
||||||
|
self.leftMargin + self.chartWidth + dx, self.topMargin + self.chartHeight + dy)
|
||||||
|
|
||||||
|
self.moveStartX = a0.x()
|
||||||
|
self.moveStartY = a0.y()
|
||||||
|
return
|
||||||
if a0.modifiers() == QtCore.Qt.ControlModifier:
|
if a0.modifiers() == QtCore.Qt.ControlModifier:
|
||||||
# Dragging a box
|
# Dragging a box
|
||||||
if not self.draggedBox:
|
if not self.draggedBox:
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import collections
|
import collections
|
||||||
import math
|
import math
|
||||||
|
from numbers import Number
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from NanoVNASaver.SITools import Value, Format
|
from NanoVNASaver.SITools import Value, Format
|
||||||
|
@ -33,7 +34,7 @@ class RFTools:
|
||||||
return re50, im50
|
return re50, im50
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def gain(data: Datapoint):
|
def gain(data: Datapoint) -> float:
|
||||||
# re50, im50 = normalize50(data)
|
# re50, im50 = normalize50(data)
|
||||||
# Calculate the gain / reflection coefficient
|
# Calculate the gain / reflection coefficient
|
||||||
# mag = math.sqrt((re50 - 50) * (re50 - 50) + im50 * im50) / \
|
# mag = math.sqrt((re50 - 50) * (re50 - 50) + im50 * im50) / \
|
||||||
|
@ -43,7 +44,6 @@ class RFTools:
|
||||||
mag = math.sqrt(data.re**2 + data.im**2)
|
mag = math.sqrt(data.re**2 + data.im**2)
|
||||||
if mag > 0:
|
if mag > 0:
|
||||||
return 20 * math.log10(mag)
|
return 20 * math.log10(mag)
|
||||||
else:
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -82,85 +82,54 @@ class RFTools:
|
||||||
return str(Value(inductance, "H", Format(max_nr_digits=5, space_str=" ")))
|
return str(Value(inductance, "H", Format(max_nr_digits=5, space_str=" ")))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def formatFrequency(freq):
|
def formatFrequency(freq: Number) -> str:
|
||||||
return str(Value(freq, "Hz", Format(max_nr_digits=6)))
|
return str(Value(freq, "Hz"))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def formatShortFrequency(freq):
|
def formatShortFrequency(freq: Number) -> str:
|
||||||
return str(Value(freq, "Hz", Format(max_nr_digits=4)))
|
return str(Value(freq, "Hz", Format(max_nr_digits=4)))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def formatSweepFrequency(freq: int,
|
def formatSweepFrequency(freq: Number) -> str:
|
||||||
mindigits: int = 2,
|
return str(Value(freq, "Hz", Format(max_nr_digits=5)))
|
||||||
appendHz: bool = True,
|
|
||||||
insertSpace: bool = False,
|
|
||||||
countDot: bool = True,
|
|
||||||
assumeInfinity: bool = True) -> str:
|
|
||||||
""" Format frequency with SI prefixes
|
|
||||||
|
|
||||||
mindigits count refers to the number of decimal place digits
|
|
||||||
that will be shown, padded with zeroes if needed.
|
|
||||||
"""
|
|
||||||
freqstr = str(freq)
|
|
||||||
freqlen = len(freqstr)
|
|
||||||
|
|
||||||
# sanity checks
|
|
||||||
if freqlen > 15:
|
|
||||||
if assumeInfinity:
|
|
||||||
return "\N{INFINITY}"
|
|
||||||
raise ValueError("Frequency too big. More than 15 digits!")
|
|
||||||
|
|
||||||
if freq < 1:
|
|
||||||
return " - " + (" " if insertSpace else "") + ("Hz" if appendHz else "")
|
|
||||||
|
|
||||||
si_index = (freqlen - 1) // 3
|
|
||||||
dot_pos = freqlen % 3 or 3
|
|
||||||
intfstr = freqstr[:dot_pos]
|
|
||||||
decfstr = freqstr[dot_pos:]
|
|
||||||
nzdecfstr = decfstr.rstrip('0')
|
|
||||||
if si_index != 0:
|
|
||||||
while len(nzdecfstr) < mindigits:
|
|
||||||
nzdecfstr += '0'
|
|
||||||
freqstr = intfstr + ("." if len(nzdecfstr) > 0 else "") + nzdecfstr
|
|
||||||
return freqstr + (" " if insertSpace else "") + PREFIXES[si_index] + ("Hz" if appendHz else "")
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parseFrequency(freq: str) -> int:
|
def parseFrequency(freq: str) -> int:
|
||||||
parser = Value(0, "Hz")
|
parser = Value(0, "Hz", Format(parse_sloppy_unit=True, parse_sloppy_kilo=True))
|
||||||
try:
|
try:
|
||||||
return round(parser.parse(freq))
|
return round(parser.parse(freq))
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def phaseAngle(data: Datapoint):
|
def phaseAngle(data: Datapoint) -> float:
|
||||||
re = data.re
|
re = data.re
|
||||||
im = data.im
|
im = data.im
|
||||||
return math.degrees(math.atan2(im, re))
|
return math.degrees(math.atan2(im, re))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def phaseAngleRadians(data: Datapoint):
|
def phaseAngleRadians(data: Datapoint) -> float:
|
||||||
re = data.re
|
re = data.re
|
||||||
im = data.im
|
im = data.im
|
||||||
return math.atan2(im, re)
|
return math.atan2(im, re)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def clamp_int(value: int, min: int, max: int) -> int:
|
||||||
|
assert min <= max
|
||||||
|
if value < min:
|
||||||
|
return min
|
||||||
|
if value > max:
|
||||||
|
return max
|
||||||
|
return value
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def groupDelay(data: List[Datapoint], index: int) -> float:
|
def groupDelay(data: List[Datapoint], index: int) -> float:
|
||||||
if index == 0:
|
index0 = RFTools.clamp_int(index - 1, 0, len(data) - 1)
|
||||||
angle0 = RFTools.phaseAngleRadians(data[0])
|
index1 = RFTools.clamp_int(index + 1, 0, len(data) - 1)
|
||||||
angle1 = RFTools.phaseAngleRadians(data[1])
|
angle0 = RFTools.phaseAngleRadians(data[index0])
|
||||||
freq0 = data[0].freq
|
angle1 = RFTools.phaseAngleRadians(data[index1])
|
||||||
freq1 = data[1].freq
|
freq0 = data[index0].freq
|
||||||
elif index == len(data) - 1:
|
freq1 = data[index1].freq
|
||||||
angle0 = RFTools.phaseAngleRadians(data[-2])
|
|
||||||
angle1 = RFTools.phaseAngleRadians(data[-1])
|
|
||||||
freq0 = data[-2].freq
|
|
||||||
freq1 = data[-1].freq
|
|
||||||
else:
|
|
||||||
angle0 = RFTools.phaseAngleRadians(data[index-1])
|
|
||||||
angle1 = RFTools.phaseAngleRadians(data[index+1])
|
|
||||||
freq0 = data[index-1].freq
|
|
||||||
freq1 = data[index+1].freq
|
|
||||||
delta_angle = (angle1 - angle0)
|
delta_angle = (angle1 - angle0)
|
||||||
if abs(delta_angle) > math.tau:
|
if abs(delta_angle) > math.tau:
|
||||||
if delta_angle > 0:
|
if delta_angle > 0:
|
||||||
|
|
|
@ -15,45 +15,35 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import math
|
import math
|
||||||
|
from typing import NamedTuple
|
||||||
from numbers import Number
|
from numbers import Number
|
||||||
|
|
||||||
PREFIXES = ("y", "z", "a", "f", "p", "n", "µ", "m",
|
PREFIXES = ("y", "z", "a", "f", "p", "n", "µ", "m",
|
||||||
"", "k", "M", "G", "T", "P", "E", "Z", "Y")
|
"", "k", "M", "G", "T", "P", "E", "Z", "Y")
|
||||||
|
|
||||||
|
class Format(NamedTuple):
|
||||||
class Format(object):
|
max_nr_digits: int = 6
|
||||||
def __init__(self,
|
fix_decimals: bool = False
|
||||||
max_nr_digits: int = 6,
|
space_str: str = ""
|
||||||
fix_decimals: bool = False,
|
assume_infinity: bool = True
|
||||||
space_str: str = "",
|
min_offset: int = -8
|
||||||
assume_infinity: bool = True,
|
max_offset: int = 8
|
||||||
min_offset: int = -8,
|
parse_sloppy_unit: bool = False
|
||||||
max_offset: int = 8):
|
parse_sloppy_kilo: bool = False
|
||||||
assert(min_offset >= -8 and max_offset <= 8 and min_offset < max_offset)
|
|
||||||
self.max_nr_digits = max_nr_digits
|
|
||||||
self.fix_decimals = fix_decimals
|
|
||||||
self.space_str = space_str
|
|
||||||
self.assume_infinity = assume_infinity
|
|
||||||
self.min_offset = min_offset
|
|
||||||
self.max_offset = max_offset
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return (f"{self.__class__.__name__}("
|
|
||||||
f"{self.max_nr_digits}, {self.fix_decimals}, "
|
|
||||||
f"'{self.space_str}', {self.assume_infinity}, "
|
|
||||||
f"{self.min_offset}, {self.max_offset})")
|
|
||||||
|
|
||||||
|
|
||||||
class Value(object):
|
class Value():
|
||||||
def __init__(self, value: Number = 0, unit: str = "", fmt=Format()):
|
def __init__(self, value: Number = 0, unit: str = "", fmt=Format()):
|
||||||
|
assert 3 <= fmt.max_nr_digits <= 27
|
||||||
|
assert -8 <= fmt.min_offset <= fmt.max_offset <= 8
|
||||||
self.value = value
|
self.value = value
|
||||||
self._unit = unit
|
self._unit = unit
|
||||||
self.fmt = fmt
|
self.fmt = fmt
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return f"{self.__class__.__name__}({self.value}, '{self._unit}', {self.fmt})"
|
return f"{self.__class__.__name__}({self.value}, '{self._unit}', {self.fmt})"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
fmt = self.fmt
|
fmt = self.fmt
|
||||||
if fmt.assume_infinity and abs(self.value) >= 10 ** ((fmt.max_offset + 1) * 3):
|
if fmt.assume_infinity and abs(self.value) >= 10 ** ((fmt.max_offset + 1) * 3):
|
||||||
return ("-" if self.value < 0 else "") + "\N{INFINITY}" + fmt.space_str + self._unit
|
return ("-" if self.value < 0 else "") + "\N{INFINITY}" + fmt.space_str + self._unit
|
||||||
|
@ -85,22 +75,30 @@ class Value(object):
|
||||||
|
|
||||||
return result + fmt.space_str + PREFIXES[offset + 8] + self._unit
|
return result + fmt.space_str + PREFIXES[offset + 8] + self._unit
|
||||||
|
|
||||||
def parse(self, value: str):
|
def parse(self, value: str) -> float:
|
||||||
value = value.replace(" ", "") # Ignore spaces
|
value = value.replace(" ", "") # Ignore spaces
|
||||||
if self._unit and value.endswith(self._unit) or value.lower().endswith(self._unit.lower()): # strip unit
|
|
||||||
|
if self._unit and (
|
||||||
|
value.endswith(self._unit) or
|
||||||
|
(self.fmt.parse_sloppy_unit and
|
||||||
|
value.lower().endswith(self._unit.lower()))): # strip unit
|
||||||
value = value[:-len(self._unit)]
|
value = value[:-len(self._unit)]
|
||||||
|
|
||||||
factor = 1
|
factor = 1
|
||||||
|
if self.fmt.parse_sloppy_kilo and value[-1] == "K": # fix for e.g. KHz
|
||||||
|
value = value[:-1] + "k"
|
||||||
if value[-1] in PREFIXES:
|
if value[-1] in PREFIXES:
|
||||||
factor = 10 ** ((PREFIXES.index(value[-1]) - 8) * 3)
|
factor = 10 ** ((PREFIXES.index(value[-1]) - 8) * 3)
|
||||||
value = value[:-1]
|
value = value[:-1]
|
||||||
elif value[-1] == 'K':
|
|
||||||
# Fix for the very common KHz
|
if self.fmt.assume_infinity and value == "\N{INFINITY}":
|
||||||
factor = 10 ** ((PREFIXES.index(value[-1].lower()) - 8) * 3)
|
self.value = math.inf
|
||||||
value = value[:-1]
|
elif self.fmt.assume_infinity and value == "-\N{INFINITY}":
|
||||||
|
self.value = -math.inf
|
||||||
|
else:
|
||||||
self.value = float(value) * factor
|
self.value = float(value) * factor
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit(self):
|
def unit(self) -> str:
|
||||||
return self._unit
|
return self._unit
|
||||||
|
|
Ładowanie…
Reference in New Issue